ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

数据结构与算法——线性表(完结)

2022-06-13 09:32:08  阅读:154  来源: 互联网

标签:结点 NULL return 线性表 next 完结 数据结构 data LNode


线性表

定义和基本操作

定义

线性表是具有相同

数据类型的n(n大于0)个数据元素的有限序列

,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为
$$
L = (a1, a2, ..., ai, ai+1, ..., an)
$$
ai时线性表中第i个元素线性表中的位序

a1表头元素

an表尾元素

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后驱。

基本操作

一个数据结构的基本操作是指最核心、最基本的操作。其它较复杂的操作可通过调用其基本操作来实现。线性表的主要操作如下:

// 1. 初始化表。构造一个空的线性表
InitList(&L)
    
// 2. 求表长,返回线性表L的长度,即L中数据元素的个数
Length(L)
    
// 3. 按值查找操作。在表L中查找具有给定关键字值得元素
LocateElem(L, e)
// 4. 按位查找操作。获取表L中第i个位置的元素的值

Tips:

  1. 对数据的操作——创建销毁、增删改查
  2. c语言函数的定义需要指定参数类型
  3. 实际开发中,可根据实际需求定义其它的基本操作
  4. 函数名和参数的形式、命名都可以改变(Reference:严蔚敏版)
  5. 当需要对参数的修改结果“带回来”的时候传入引用 &

为什么要实现对数据结构的基本操作?

  1. 团队合作编程,你定义的数据结构要让别人可以很方便的使用。
  2. 将常用的操作或运算封装成函数,避免重复工作,降低出错风险。

总结

线性表的物理结构

线性表的顺序表示

线性表的定义

线性表的顺序存储又叫做顺序表。用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:

#define MAXSIZE 50
typedef struct {
    ElemType data[MAXSIZE];
    int length;
}

此种表示方式是由缺陷的。如果当第51个元素到来时,它是没有地方可以插入的。也就是说,一旦空间占满,在加入新的数据就会产生溢出,进而导致程序崩溃。

所以引出动态分配。

动态分配时,存储空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就需要开辟一块更大的存储空间,用以替代原来的存储空间,从而达到扩充数据空间的目的,而不需要为线性表一次性的划分所有空间。

#define INITSIZE 50
typedef struct {
    // 动态分配数组的指针
    ElemType *data;
    // 数组的最大容量
    int maxsize;
    // 数组的当前个数
    int length;
}

C的初始动态分配语句为

L.data = (ElemType *)malloc(sizeof(ElemType) * INITSIZE)

C++的初始化分配语句为

L.data = new ElemType[INITSIZE]

顺序表的最主要特点是随机访问,即通过首地址和元素需要可在时间O(1)内找到指定的元素。

顺序表的存储密度高,每个节点只存储数据元素。

顺序表逻辑相邻的元素物理上也相邻,所以插入和删除操作需要移动大量的元素。

顺序表上基本操作的实现

插入操作
boll ListInsert(SqList &L, int i, ElemType e) {
    if (i < 1 || i > L.length)
        return false;
    if (L.length > MAXSIZE) 
        return false;
    for(int j = L.length; j >= i; j--) {
        L.data[j] = L.data[j-1]
    }
    L.data[i-1] = e;
    L.length++;
    return true;
}
删除操作
bool ListDelete(SqList &L, int i, int &e) {
    if (i < 1 || i > L.length)
        return false;
    e = data[i-1];
    for(int j = i; j < L.length; j++) {
        L.data[j-1] = L.data[j];
    }
    L.length--;
    return true;
}

最好情况:删除表尾元素,时间复杂度为O(1)

最坏情况:删除表头元素,时间复杂度为O(n)

平均情况:i=1循环n-1次;i=2循环n-2次

故为 0 + 1 + 2 + ... + n-1 平均为(n-1) / 2

按值查找
int LocateElem(SqList L, ElemType e) {
    for(int i=0;i<L.length;i++) {
        if(L.data[i] == e)
            return i+1;
    }
    return 0;
}

最好情况:目标元素在表头,循环一次 O(1)

最坏情况:目标元素在表尾,循环n次 O(n)

平均情况:(1 + 2 + ... + n)/n = (n+1)/n

按位查找

静态分配

ElemType GetElem(SqList L, int i) {
    if (i < 1 || i > L.length) {
        return null;
    }
    return L.data[i-1];
}

《数据结构》考研初试中,手写代码可以直接使用“==”,无论是结构体类型还是基本结构类型。

基本操作小结

线性表的链式存储

单链表的定义

线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。

优点:不要求大片连续空间,改变容量方便。

缺点:不可随机存储,要耗费一定空间存放指针。

单链表节点类型的描述如下:

// 定义单链表节点类型
typedef struct LNode {
    // 数据域
    ElemType data;
    // 指针域
    struct LNode *next;
}LNode, *LinkList;

// 增加一个新的节点:在内存中申请一个节点所需空间,并用指针p指向这个节点
struct LNode *p = (struct LNode*) malloc(sizeof(struct LNode));

typedef的作用是对数据类型重命名。

使用LinkList时表示 强调这是一个单链表

使用LNode时表示 强调这是一个节点

不带头节点的单链表初始化
bool InitList(LinkList &L) {
    // 空表,暂时还没有任何节点
    L = NULL;
    return true;
}

int main() {
    // 声明一个指向单链表的指针
    LinkList L;
    // 初始化一个空表
    InitList(L);
}

不带头节点,写代码更麻烦

对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑。对空表和非空表的处理需要用不同的代码逻辑。

带头节点的单链表初始化
bool InitList(LinkList &L) {
    // 分配一个头节点
    L = (LNode *)malloc(sizeof(LNode));
    // 内存不足,分配失败
    if(L == NULL)
        return false;
    // 头节点之后暂时还没有其它的节点
    L->next = NULL;
    return true;
}

int main() {
    // 声明一个指向单链表的指针
    LinkList L;
    // 初始化一个空表
    InitList(L);
}

单链表上基本操作的实现

按位序插入(带头结点)
// 在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e) {
    if(i<1)
        return false;
    // 指针p指向当前扫描到的结点
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 0;
    // L指向头结点,头结点是第0个结点,不存数据
    p = L;
    while(p!=NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}
按位序插入(不带头结点)

不带头节点写代码更不方便,推荐使用带头节点。

但在考试中,两种都有可能进行考察,所以需要了解。

bool ListInsert(LinkList &L, int i, ElemType e) {
    if (i < 1)
        return false;
    if (i == 1) {
        LNode *s = (LNode *)malloc(sizeof(LNode));
        s->data = e;
        s->next = L;
        L = s;
        return true;
    }
    // 指针p指向当前扫描到的结点
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 1;
    // L指向第一个结点
    p = L;
    while(p!=NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
    
}
指定结点的后插操作
bool InsertNextNode(LNode *p, ElemType e) {
    if(p==NULL)
        return false;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    if(s==NULL)
        return false;
    
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}
指定结点的前插操作
bool InsertPriorNode(LNode *p, ELemType e) {
    if (p == NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if (s == NULL)
        return false;
    
    s->next = p->next;
    p->next =s;
    s->data = p->data;
    p->data = e;
    return true;
}
按位序删除(带头结点)
bool ListDelete(LinkList &L, int i, ElemType &e) {
    if(i<1)
        return false;
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 0;
    // 指向头结点
    p = L;
    while (p != NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL || p->next == NULL)
        return false;
    
    LNode *q = p->next;
    e = q->data;
    p->next = q->next;
    free(q);
    return true;
}
指定结点删除
bool DeleteNode(LNode *p) {
    if(p == NULL) 
        return false;
    LNode *q = p->next;
    p->data = p->next->data;
    p->next = q->next;
    free(q);
    return true;
}

单链表的局限性:无法逆向检索,有时候不太方便。

按位查找
// 按位查找,返回第i个元素(带头结点)
LNode* GetElem(LinkList L, int i) {
    if (i < 0)
        return NULL;
    LNode *p;
    int j = 0;
    p = L;
    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    return p;
}
按值查找
LNode* LocateElem(LinkList L,ElemType e) {
    LNode *p = L->next;
    // 从第一个结点开始查找数据域为e的结点
    while(p != NULL && p->data !=e) {
        p = p->next;
    }
    return p;
}
求表的长度
int Length(LinkList L) {
    int len = 0;
    LNode *p = L;
    while(p->next != NULL) {
        p = p->next;
        len++;
    }
    return len;
}

我们看出了单链表无法逆向检索的缺点,即找到p结点的前驱结点,所以引出了双链表。

双链表

双链表结点中有两个指针prior和next,分别指向前驱结点和后继结点。

双链表逻辑结构

双链表中结点类型描述如下:

typedef struct DNode {
    ElemType data;
    struct DNode *prior;
    struct DNode *next;
}DNode, *DLinkList;

双链表的初始化

// 初始化双链表
bool InitDLinkList(DLinkList &L) {
    L = (DNode*)malloc(sizeof(DNode));
    // 内存不足则分配失败
    if (L == NULL) {
        return false;
    }
    // 头结点的prior指针永远指向null
    L->prior = NULL;
    // 头结点之后暂时还没有其它的结点
    L->next = NULL;
    return true;
}

双链表的插入

// 在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
    // 判断非法参数
    if (p == NULL || s == NULL) {
        return false;
    }
    s->next = p->next;
    // 如果p没有后继结点
    if (p->next->prior != NULL) {
        p->next->prior = s;
    }
    
    p->next = s;
    s->prior = p;
    return true;
}

双链表的删除

// 删除p结点的后继结点q
bool DeleteNextDNode(DNode *p) {
    if (p == NULL)
        return false;
    DNode *q = p->next;
    if(q == NULL)
        return false;
    p->next = q->next;
    if (q->next != NULL)
    	q->next->prior = p;
    free(q);
    return true;
}

双链表的遍历

// 后向遍历
while (p != NULL) {
    // 对结点p做相应处理
    p = p->next;
}

// 前向遍历
while (p != NULL) {
    // 对结点p做相应处理
    p = p->prior;
}

// 前向遍历(跳过头结点)
while (p->prior != NULL) {
    // 对结点p做相应处理
    p = p->prior;
}

双链表不可随机存取,按位查找、按值查找操作都只能用遍历方式实现。时间复杂度为O(n)

双链表总结

循环链表

循环单链表

循环单链表逻辑结构

表尾结点next指回了头结点。

初始化循环单链表
// 初始化循环单链表
bool InitList(LinkList &L) {
    L = (LNode *) malloc(sizeof(LNode));
    if (L == NULL)
        return false;
    L->next = L;
    return true;
}
// 判断循环单链表是否为空
bool Empty(LinkList L) {
    return L->next == L  
}

// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p) {
    return p->next == L;
}

循环双链表

静态链表

定义

借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址,又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

优点:增删操作不需要大量移动元素

缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变

适用场景:

  1. 不支持指针的低级语言;
  2. 数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

标签:结点,NULL,return,线性表,next,完结,数据结构,data,LNode
来源: https://www.cnblogs.com/Gazikel/p/16369623.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有