[关闭]
@guoxs 2015-11-29T13:53:57.000000Z 字数 11499 阅读 4278

数据结构之线性表

数据结构与算法


一、线性表的定义

线性表:零个或多个数据元素的有限序列。
A1——A2——A3——...——Ai-1——Ai——Ai+1——...——An
其中,Ai-1称为Ai的直接前驱元素,Ai+1称为Ai的直接后继元素。直接前驱元素与直接后继元素都是唯一的。n(n>=0)为线性表的长度,n=0称为空表,i为数据元素Ai在线性表中的为位序。在复杂的线性表中,一个数据元素可以由若干个数据项组成。

1.1 线性表的抽象数据定义

  1. ADT 线性表(List)
  2. Data
  3. 线性表的数据对象集合为{a1, a2, ......, an},每个元素的类型均为DataType
  4. 其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,
  5. 除了最后一个元素an外,每一个元素有且只有一个直接后继元素。
  6. 数据元素之间的关系是一对一的关系。
  7. Operation
  8. InitList(*L): 初始化操作,建立一个空的线性表L
  9. ListEmpty(L): 若线性表为空,返回true,否则返回false
  10. ClearList(*L): 将线性表清空。
  11. GetElem(L, i, *e): 将线性表L中的第i个位置元素值返回给e
  12. LocateElem(L, e): 在线性表L中查找与给定值e相等的元素,
  13. 如果查找成功,返回该元素在表中序号表示成功;
  14. ListInsert(*L,i,e): L的第i个位置插入新元素e
  15. ListDelete(*L,i,*e): 删除L中的第i个元素,并用e返回其值。
  16. ListLength(L): 返回L中的元素个数
  17. endADT

1.2 例子:两线性表的并集操作:A=A∪B

  1. /* 将所有的在线性表Lb中但不在La中的数据元素插入到La中 */
  2. void unionL(List *La, List Lb)
  3. {
  4. int La_len, Lb_len, i;
  5. /* 声明与La和Lb相同的数据元素e */
  6. ElemType e;
  7. /* 求线性表的长度 */
  8. La_len = ListLength(*La);
  9. Lb_len = ListLength(Lb);
  10. for (i = 1; i <= Lb_len; i++)
  11. {
  12. /* 取Lb中第i个数据元素赋给e */
  13. GetElem(Lb, i, &e);
  14. /* La中不存在和e相同数据元素 */
  15. if (!LocateElem(*La, e))
  16. /* 插入 */
  17. ListInsert(La, ++La_len, e);
  18. }
  19. }

二、线性表的顺序存储结构

2.1 顺序存储结构的定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表顺序存储的结构代码

  1. /* 存储空间初始分配量 */
  2. #define MAXSIZE 20
  3. /* ElemType类型根据实际情况而定,这里假设为int */
  4. typedef int ElemType;
  5. typedef struct
  6. {
  7. /* 数组存储数据元素,最大值为MAXSIZE */
  8. ElemType data[MAXSIZE];
  9. /* 线性表当前长度 */
  10. int length;
  11. } SqList;

描述顺序存储结构需要三个属性:

  1. LOC(ai+1)=LOC(ai)+c
  2. LOC(ai)=LOC(a1)+(i-1)*c

2.2 插入与删除操作

获取元素

  1. #define OK 1
  2. #define ERROR 0
  3. #define TRUE 1
  4. #define FALSE 0
  5. typedef int Status;
  6. /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
  7. /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
  8. /* 操作结果:用e返回L中第i个数据元素的值 */
  9. Status GetElem(SqList L, int i, ElemType *e)
  10. {
  11. if (L.length == 0 || i < 1 ||
  12. i > L.length)
  13. return ERROR;
  14. *e = L.data[i - 1];
  15. return OK;
  16. }

插入操作
基本思路:

  1. /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
  2. /* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
  3. Status ListInsert(SqList *L, int i, ElemType e)
  4. {
  5. int k;
  6. /* 顺序线性表已经满 */
  7. if (L->length == MAXSIZE)
  8. return ERROR;
  9. /* 当i不在范围内时 */
  10. if (i < 1 || i >L->length + 1)
  11. return ERROR;
  12. /* 若插入数据位置不在表尾 */
  13. if (i <= L->length)
  14. {
  15. /*将要插入位置后数据元素向后移动一位 */
  16. for (k = L->length - 1; k >= i - 1; k--)
  17. L->data[k + 1] = L->data[k];
  18. }
  19. /* 将新元素插入 */
  20. L->data[i - 1] = e;
  21. L->length++;
  22. return OK;
  23. }

iidata[i1]

删除操作
删除算法的思路:

  1. /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
  2. /* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
  3. Status ListDelete(SqList *L, int i, ElemType *e)
  4. {
  5. int k;
  6. /* 线性表为空 */
  7. if (L->length == 0)
  8. return ERROR;
  9. /* 删除位置不正确 */
  10. if (i < 1 || i > L->length)
  11. return ERROR;
  12. *e = L->data[i - 1]; /*返回删除的元素*/
  13. /* 如果删除不是最后位置 */
  14. if (i < L->length)
  15. {
  16. /* 将删除位置后继元素前移 */
  17. for (k = i; k < L->length; k++)
  18. L->data[k - 1] = L->data[k];
  19. }
  20. L->length--;
  21. return OK;
  22. }

O(n)
线性表顺序存储结构的优缺点

优点 缺点
无须为表示表中元素之间的逻辑关系而增加额外的存储空间 插入和删除需要移动大量的元素
可以快速地存取表中任一位置的元素 当线性表长度变化较大时,难以确定存储空间的容量
造成存储空间的“碎片”

三、线性表的链式存储结构

3.1 链式存储结构的基本概念

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
链式存储结构
链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
存储数据元素信息的域称为数据域,存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)

n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起,如下图所示:
链式存储结构
链表中第一个结点的存储位置叫做头指针,整个链表的存取必须是从头指针开始。之后的每一个结点,是上一个的后继指针指向的位置。线性链表的最后一个结点指针为“空”(通常用NULL“^”符号表示)。
有时,为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,指针域存储指向第一个结点的指针。
链式存储结构
头指针与头结点的异同

头指针 头结点
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据一般无意义(也可能存放链表的长度)
头指针具有标识作用,所以常用头指针冠以链表的名字 有了头指针,对在第一元素结点前插入结点和删除第一节点,其操作与其它结点的操作就统一了
无论链表是否为空,头指针均不为空,头指针是链表的必要元素 头结点不一定是链表的必要元素

线性表链式存储结构代码描述
不带头指针的单链表示意图:
单链表

  1. /* 线性表的单链表存储结构 */
  2. typedef struct Node
  3. {
  4. ElemType data;
  5. struct Node *next;
  6. } Node;
  7. /* 定义LinkList */
  8. typedef struct Node *LinkList;

结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
单链表

3.2 单链表的读取

获得链表第i个数据的算法思路:

实现代码算法如下:

  1. /* 初始条件:顺序线性表L已存在,1≤i≤
  2. ListLength(L) */
  3. /* 操作结果:用e返回L中第i个数据元素的值 */
  4. Status GetElem(LinkList L, int i, ElemType *e)
  5. {
  6. int j;
  7. LinkList p; /* 声明一指针p */
  8. p = L->next; /* 让p指向链表L的第个结点 */
  9. j = 1; /* j为计数器 */
  10. /* p不为空且计数器j还没有等于i时,循环继续 */
  11. while (p && j < i)
  12. {
  13. p = p->next; /* 让p指向下一个结点 */
  14. ++j;
  15. }
  16. if (!p || j > i)
  17. return ERROR; /* 第i个结点不存在 */
  18. *e = p->data; /* 取第i个结点的数据 */
  19. return OK;
  20. }

O(n)
单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是“工作指针后移”,这也是很多算法的常用技术。

3.3 单链表的插入与删除

单链表的插入
单链表的插入
单链表第i个数据插入结点的算法思路:

实现代码算法如下:

  1. /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
  2. /* 操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1 */
  3. Status ListInsert(LinkList *L, int i, ElemType e)
  4. {
  5. int j;
  6. LinkList p, s;
  7. p = *L;
  8. j = 1;
  9. /* 寻找第i-1个结点 */
  10. while (p && j < i)
  11. {
  12. p = p->next;
  13. ++j;
  14. }
  15. /* 第i个结点不存在 */
  16. if (!p || j > i)
  17. return ERROR;
  18. /* 生成新结点(C标准函数) */
  19. s = (LinkList)malloc(sizeof(Node));
  20. s->data = e;
  21. /* 将p的后继结点赋值给s的后继 */
  22. s->next = p->next;
  23. /* 将s赋值给p的后继 */
  24. p->next = s;
  25. return OK;
  26. }

C语言的malloc标准函数:作用就是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备用来存放数据e的s结点。
单链表的删除
单链表的删除
删除结点的算法思路:
- 声明一指针p指向链表头结点,初始化j从1开始;
- 当j - 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功。

实现代码算法如下:

  1. /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
  2. /* 操作结果:删除L的第i个结点,并用e返回其值,L的长度减1 */
  3. Status ListDelete(LinkList *L, int i, ElemType *e)
  4. {
  5. int j;
  6. LinkList p, q;
  7. p = *L;
  8. j = 1;
  9. /* 遍历寻找第i-1个结点 */
  10. while (p->next && j < i)
  11. {
  12. p = p->next;
  13. ++j;
  14. }
  15. /* 第i个结点不存在 */
  16. if (!(p->next) || j > i)
  17. return ERROR;
  18. q = p->next;
  19. /* 将q的后继赋值给p的后继 */
  20. p->next = q->next;
  21. /* 将q结点中的数据给e */
  22. *e = q->data;
  23. /* 让系统回收此结点,释放内存 */
  24. free(q);
  25. return OK;
  26. }

C语言的标准函数free:作用就是让系统回收一个Node结点,释放内存。
On,i

3.4 单链表的整表创建

顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。而单链表和顺序存储结构就不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:

  1. /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
  2. void CreateListHead(LinkList *L, int n)
  3. {
  4. LinkList p;
  5. int i;
  6. /* 初始化随机数种子 */
  7. srand(time(0));
  8. *L = (LinkList)malloc(sizeof(Node));
  9. /* 先建立一个带头结点的单链表 */
  10. (*L)->next = NULL;
  11. for (i = 0; i < n; i++)
  12. {
  13. /* 生成新结点 */
  14. p = (LinkList)malloc(sizeof(Node));
  15. /* 随机生成100以内的数字 */
  16. p->data = rand() % 100 + 1;
  17. p->next = (*L)->next;
  18. /* 插入到表头 */
  19. (*L)->next = p;
  20. }
  21. }

头插法
这种方法叫做头插法,始终让新结点在第一的位置。
类似的还有一种叫做尾插法,其实现代码为:

  1. /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
  2. void CreateListTail(LinkList *L, int n)
  3. {
  4. LinkList p,r;
  5. int i;
  6. /* 初始化随机数种子 */
  7. srand(time(0));
  8. /* *L为整个线性表 */
  9. *L = (LinkList)malloc(sizeof(Node));
  10. /* r为指向尾部的结点 */
  11. r = *L;
  12. for (i = 0; i < n; i++)
  13. {
  14. /* 生成新结点 */
  15. p = (Node *)malloc(sizeof(Node));
  16. /* 随机生成100以内的数字 */
  17. p->data = rand() % 100 + 1;
  18. /* 将表尾终端结点的指针指向新结点 */
  19. r->next = p;
  20. /* 将当前的新结点定义为表尾终端结点 */
  21. r = p;
  22. }
  23. /* 表示当前链表结束 */
  24. r->next = NULL;
  25. }

3.5 单链表的整表删除

整表删除的算法思路如下:

实现代码算法如下:

  1. /* 初始条件:顺序线性表L已存在,操作结果:将L重置为空表 */
  2. Status ClearList(LinkList *L)
  3. {
  4. LinkList p, q;
  5. /* p指向第一个结点 */
  6. p = (*L)->next;
  7. /* 没到表尾 */
  8. while (p)
  9. {
  10. q = p->next;
  11. free(p);
  12. p=q;
  13. }
  14. /* 头结点指针域为空 */
  15. (*L)->next = NULL;
  16. return OK;
  17. }

3.6 单链表结构与顺序存储结构优缺点

存储类别 顺序存储结构 单链表
存储分配方式 用一段连续的存储单元依次存储线性表的数据元素 采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能 查找O(1)、插入和删除O(n) 查找O(n)、插入和删除O(1)
空间性能 需要预分配存储空间,分大了浪费,小了容易发生上溢 不需要分配存储空间,只要有就可以分配,元素个数不受限制

通过上面的对比,可以得出一些经验性的结论:

四、静态链表

4.1 静态链表基本概念

对于可以使用指针的语言,单链表已经很方便了,但是并不是所有语言都有指针功能。那么对于没有执政功能的语言如何实现单链表呢?答案就是利用数组

如果让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫做游标。
用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。
对于数组第一个和最后一个元素作为特殊元素处理,不存数据。通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0。
静态链表
此时的图示相当于初始化的数组状态,见下面代码:

  1. /* 将一维数组space中各分量链成一备用链表, */
  2. /* space[0].cur为头指针,"0"表示空指针 */
  3. Status InitList(StaticLinkList space)
  4. {
  5. int i;
  6. for (i = 0; i < MAXSIZE - 1; i++)
  7. space[i].cur = i + 1;
  8. /* 目前静态链表为空,最后一个元素的cur为0 */
  9. space[MAXSIZE - 1].cur = 0;
  10. return OK;
  11. }

静态链表

4.2 静态链表的插入操作

先定义数组的申请与释放操作
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

  1. /* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
  2. int Malloc_SLL(StaticLinkList space)
  3. {
  4. /* 当前数组第一个元素的cur存的值,就是要返回的第一个备用空闲的下标 */
  5. int i = space[0].cur;
  6. /* 由于要拿出一个分量来使用了,所以我们就得把它的下一个分量用来做备用 */
  7. if (space[0].cur)
  8. space[0].cur = space[i].cur;
  9. return i;
  10. }

于是就有实现代码:

  1. /* 在L中第i个元素之前插入新的数据元素e */
  2. Status ListInsert(StaticLinkList L, int i, ElemType e)
  3. {
  4. int j, k, l;
  5. /* 注意k首先是最后一个元素的下标 */
  6. k = MAX_SIZE - 1;
  7. if (i < 1 || i > ListLength(L) + 1)
  8. return ERROR;
  9. /* 获得空闲分量的下标 */
  10. j = Malloc_SSL(L);
  11. if (j)
  12. {
  13. /* 将数据赋值给此分量的data */
  14. L[j].data = e;
  15. /* 找到第i个元素之前的位置 */
  16. for (l = 1; l <= i - 1; l++)
  17. k = L[k].cur;
  18. /* 把第i个元素之前的cur赋值给新元素的cur */
  19. L[j].cur = L[k].cur;
  20. /* 把新元素的下标赋值给第i个元素之前元素的cur */
  21. L[k].cur = j;
  22. return OK;
  23. }
  24. return ERROR;
  25. }

4.3 静态链表的删除操作

先定义释放元素函数:

  1. /* 删除在L中第i个数据元素e */
  2. Status ListDelete(StaticLinkList L, int i)
  3. {
  4. int j, k;
  5. if (i < 1 || i > ListLength(L))
  6. return ERROR;
  7. k = MAX_SIZE - 1;
  8. for (j = 1; j <= i - 1; j++)
  9. k = L[k].cur;
  10. j = L[k].cur;
  11. L[k].cur = L[j].cur;
  12. Free_SSL(L, j);
  13. return OK;
  14. }
  15. /* 将下标为k的空闲结点回收到备用链表 */
  16. void Free_SSL(StaticLinkList space, int k)
  17. {
  18. /* 把第一个元素cur值赋给要删除的分量cur */
  19. space[k].cur = space[0].cur;
  20. /* 把要删除的分量下标赋值给第一个元素的cur */
  21. space[0].cur = k;
  22. }

4.4 静态链表优缺点

优点 缺点
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点 没有解决连续存储分配带来的表长难以确定的问题,失去了顺序存储结构随机存取的特性

静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。尽管不一定会用得上,但这样的思考方式是非常巧妙的。

五、循环链表

5.1 循环链表的定义

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)。
循环链表解决了一个很麻烦的问题:如何从当中一个结点出发,访问到链表的全部结点。
非空循环链表示意图:
循环链表
从上图中可以看到,终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next->next,其时间复杂也为O(1)。

5.2 循环链表的并集操作

考虑两个循环链表:
循环链表1
要想把它们合并,只需要如下的操作即可:
循环链表2
对应的代码为:

  1. /* 保存A表的头结点,即① */
  2. p = rearA->next;
  3. /*将本是指向B表的第一个结点(不是头结点) */
  4. rearA->next = rearB->next->next;
  5. /* 赋值给reaA->next,即② */
  6. q = rearB->next;
  7. /* 将原A表的头结点赋值给rearB->next,即③ */
  8. rearB->next = p;
  9. /* 释放q */
  10. free(q);

六、双向链表

6.1 双向链表定义

双向链表(double linkedlist)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

  1. /* 线性表的双向链表存储结构 */
  2. typedef struct DulNode
  3. {
  4. ElemType data;
  5. struct DuLNode *prior; /* 直接前驱指针 */
  6. struct DuLNode *next; /* 直接后继指针 */
  7. } DulNode, *DuLinkList;

双向链表也可以是循环表。
双向链表的循环带头结点的空链表:
空链表
非空的循环的带头结点的双向链表:
带头结点的双向链表
对于双向链表:

  1. p->next->prior = p = p->prior->next

6.2 双向链表的插入操作

插入

  1. /* 把p赋值给s的前驱,如图中① */
  2. s->prior = p;
  3. /* 把p->next赋值给s的后继,如图中② */
  4. s->next = p->next;
  5. /* 把s赋值给p->next的前驱,如图中③ */
  6. p->next->prior = s;
  7. /* 把s赋值给p的后继,如图中④ */
  8. p->next = s;

关键在于它们的顺序,由于第2步和第3步都用到了p->next。如果第4步先执行,则会使得p->next提前变成了s,使得插入的工作完不成。

6.2 双向链表的删除操作

删除

  1. /* 把p->next赋值给p->prior的后继,如图中① */
  2. p->prior->next = p->next;
  3. /* 把p->prior赋值给p->next的前驱,如图中② */
  4. p->next->prior = p->prior;
  5. /* 释放结点 */
  6. free(p);

七、广义表

给定二元多项式:

P(x,y)=9x12y2+4x12+15x8y3x8y+3x2

如何用线性表表示?
其实,把上式稍稍变形,有
P(x,y)=(9y2+4)x12+(15y3y)x8+3x2==>ax12+bx8+cx2

故可以用一“复杂”的链表表示:
广义表

7.1 广义表定义

广义表(Generalized List)

  1. typedef struct GNode{
  2. int Tag; /*标志域:0表示结点是单元素,1表示结点是广义表 */
  3. union { /* 子表指针域Sublist与单元素数据域Data复用,即共用存储空间 */
  4. ElementType Data;
  5. struct GNode *SubList;
  6. } URegion;
  7. struct GNode *Next; /* 指向后继结点 */
  8. } GList;

7.2 多重链表

多重链表:链表中的节点可能同时隶属于多个链

矩阵可以用二维数组表示,但二维数组表示有两个缺陷:

解决方案是:采用十字链表来存储稀疏矩阵

结点
对于稀疏矩阵A:

180023027010000204000012

十字链表

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注