线性表的链式表示-单链表、循环链表

迷南。 2022-10-11 14:53 281阅读 0赞

一 单链表

与顺序表相同,链表也是一种线性表,它的数据的逻辑组织是一维的。而与顺序表不同的是,链表的物理存储结构是用一组地址任意的存储单元存储数据的。也就是说,它不像顺序表那样占据一段连续的内存空间,而是将存储单元分散在内存的任意地址上。在链表结构中,每一个数据元素都存放在链表的一个结点(Node)中,而每个结点之间是由指针将其连接在一起,这样就形成了一条如同“链子”的结构。

1.1 单链表结点(node)

结点 = 数据域 + 指针域

  • 数据域用来存放数据元素本身的信息,指针域用来存放后继结点的地址。
  • 链表在逻辑上是连续的,而物理上并不一定是连续的存储结点。
  • 只要获得链表的头结点,就可以通过指针遍历整条链表。

1.2 单链表的数据结构定义

  1. typedef int ElemType;
  2. //单链表结点类型的描述
  3. typedef struct LNode{
  4. ElemType data; //数据域
  5. struct LNode *next; //指针域
  6. } LNode,*LinkList;

<说明> ElemType 可以是C语言的基本数据类型,也可以是结构体类型,当然也可以是其他的构造类型。

1.3 创建一个单链表

可以使用 尾插法(含头结点)创建一个单链表。实现代码如下:

  1. //尾插法(含头结点)创建单链表
  2. LinkList createLinkList(int n)
  3. {
  4. int i;
  5. LinkList L,tail,p;
  6. ElemType e;
  7. //动态内存分配
  8. L=(LNode*)malloc(sizeof(LNode)); //创建头结点,数据域不使用
  9. memset(L, 0, sizeof(LNode));
  10. tail=L->next; //tail指针始终指向链表的尾结点
  11. for(i=0; i<n; i++)
  12. {
  13. scanf("%d", &e);
  14. p=(LNode*)malloc(sizeof(LNode)); //创建数据结点
  15. memset(p, 0, sizeof(LNode));
  16. p->data=e;
  17. tail->next=p;
  18. tail=p;
  19. }
  20. tail->next=NULL; //尾结点的指针域的值为NULL
  21. return L;
  22. }

<说明> 当然也可以使用 头插法 创建一个链表,即将新结点插入到头结点的后面,然后新结点的指针域指向原来的链表。可以自己尝试实现一下。

1.4 向单链表中插入一个结点

在指针q指向的结点后面插入一个新结点,该过程的步骤如下:

(1)首先创建一个新结点,并用指针p指向该新结点。

(2)将q指向的结点的next的值(即q的后继结点的指针)赋值给p指向的结点的next域。

(3)将p的值赋值给q的next域。

通过以上3步,就可以实现在链表中由q指向的结点后面插入p所指向的新结点。如下图所示:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA0Mjk4MzE_size_16_color_FFFFFF_t_70 向链表插入结点过程

具体实现代码如下:

  1. void insertListNode(LinkList L, LinkList q, ElemType e)
  2. {
  3. //向链表中由指针q指向的结点后面插入新结点,结点数据为e
  4. LinkList p;
  5. p=(LNode*)malloc(sizeof(LNode));
  6. p->data = e;
  7. if(!L->next) //如果链表的内容为空,表示该链表为空
  8. {
  9. L->next = p;
  10. p->next = NULL; //在头结点后面插入第1个元素
  11. }
  12. else //如果链表不为空时
  13. {
  14. p->next = q->next;
  15. q->next = p;
  16. }
  17. }

1.5 从单链表中删除一个结点

从链表中删除一个结点,必须考虑以下3种情况。

  1. 待删除结点是链表的第1个数据结点。
  2. 待删除结点的前驱结点的指针已知。
  3. 待删除结点的前驱结点的指针未知。

对于前面两种情况,可以使用下面这段代码来描述:

  1. void delListNode(LinkList L, LinkList q, LinkList p)
  2. {
  3. //q指针指向待删除结点的前继结点,指针p指向待删除结点
  4. if(p == L->next)
  5. L->next = p->next;
  6. else
  7. q->next = p->next;
  8. free(p);
  9. }

对于第3种情况,当p所指向的结点的前驱结点的指针未知时,需要先遍历链表,找到p的前驱结点的指针,并将该指针赋值给一个指针变量q,再按照第2种情况的方式去做。具体的代码描述如下:

  1. void delListNode(LinkList L, LinkList p)
  2. {
  3. LinkList q; //指针q指向待删除结点的前驱结点
  4. q = L->next; //指针q初始指向链表的第1个数据结点
  5. if(q == p) //删除的是第1个数据结点
  6. {
  7. L->next = p->next;
  8. free(p);
  9. }
  10. else
  11. {
  12. for(; q->next!=p; q=q->next); //遍历链表,找到p的前驱结点,并使q指向它
  13. if(q->next != NULL){
  14. q->next = p->next; //从链表中删除p指向的结点
  15. free(p); //释放掉p指向的结点空间
  16. }
  17. }
  18. }

1.6 销毁一个单链表

在链表使用完毕后建议销毁它,因为链表本身会占用内存空间。如果一个系统中使用很多的链表,而使用完毕后又不及时地销毁它,那么这些垃圾空间积累过多,最终会导致内存泄漏甚至程序的崩溃。因此应当养成及时销毁不使用的链表的良好编程习惯。销毁一个链表的代码描述如下:

  1. void destroyLinkList(LinkList L)
  2. {
  3. LinkList p,q;
  4. p = L->next;
  5. while(p){ //删除所有数据结点
  6. q = p->next;
  7. free(p);
  8. p=q;
  9. }
  10. free(L); //删除头结点
  11. }

1.7 输出单链表中所有元素

通过遍历链表,就可以逐个输出链表中数据结点的数据域的值。具体代码描述如下:

  1. //打印输出函数
  2. void printLinkList(LinkList L)
  3. {
  4. LinkList p;
  5. p=L->next; //p初始指向第一个数据结点
  6. while(p)
  7. {
  8. printf("%d ",p->data);
  9. p=p->next;
  10. }
  11. }

测试用例

  1. int main()
  2. {
  3. int e,i;
  4. LinkList L, q;
  5. q=L=createLinkList(1);
  6. q=L->next; //指针q指向链表的第1个数据结点
  7. scanf("%d", &e);
  8. while(e){
  9. insertListNode(L,q,e);
  10. q=q->next;
  11. scanf("%d", &e);
  12. }
  13. printf("The content of the linklist:\n");
  14. printLinkList(L);
  15. q=L->next;
  16. printf("\nDelete the 5th element\n");
  17. for(i=0;i<4;i++){
  18. if(q == NULL){
  19. printf("The length of the linklist is smaller than 5!\n");
  20. //getche();
  21. return -1;
  22. }
  23. q = q->next;
  24. }
  25. delListNode(L, q);
  26. printLinkList(L);
  27. destroyLinkList(L); //销毁该链表
  28. //getche();
  29. return 0;
  30. }

编译命令:gcc demo.c -std=c99

示例运行结果:

a.exe
1 2 3 4 5 6 7 8 9 10 0
The content of the linklist:
1 2 3 4 5 6 7 8 9 10
Delete the 5th element
1 2 3 4 6 7 8 9 10

1.8 逆置单链表

逆置链表就是将链表的数据结点按逆序的方式重新组装成一条链表。具体代码描述如下:

  1. //逆置一个带头结点的单链表L
  2. void reverseLinkList(LinkList L)
  3. {
  4. LinkList p,q; //p用来遍历链表
  5. if(L->next && L->next->next)
  6. {
  7. p=q=L->next; //p,q初始指向第一个结点
  8. p=p->next; //p指向第二个结点
  9. q->next=NULL;
  10. while(p)
  11. {
  12. q=p; //q指向待插入的结点
  13. p=p->next; //p指向原链表的下一个结点
  14. //插入结点
  15. q->next=L->next;
  16. L->next=q; //两条语句的顺序不能颠倒
  17. }
  18. }
  19. }

测试用例

  1. int main()
  2. {
  3. int e,i;
  4. LinkList L, q;
  5. q=L=createLinkList(1);
  6. q=L->next; //指针q指向链表的第1个数据结点
  7. scanf("%d", &e);
  8. while(e){
  9. insetListNode(L,q,e);
  10. q=q->next;
  11. scanf("%d", &e);
  12. }
  13. printf("The content of the linklist:\n");
  14. printLinkList(L);
  15. reverseLinkList(L);
  16. printf("\nAfter reverse linklist:\n");
  17. printLinkList(L);
  18. destroyLinkList(L); //销毁该链表
  19. //getche();
  20. return 0;
  21. }

示例运行结果:

a.exe
1 2 3 4 5 6 7 8 9 10 0
The content of the linklist:
1 2 3 4 5 6 7 8 9 10
After reverse linklist:
10 9 8 7 6 5 4 3 2 1

1.9 归并两个有序的单链表

问题描述】已知单链表 La 和单链表 Lb 的元素按值非递减排列,现要归并 La 和 Lb 得到单链表 Lc。(单链表带头结点)

问题分析】因为单链表的结点之间的关系是通过指针指向建立起来的,所以用链表进行合并并不需要另外开辟存储空间,可以直接利用原来的两个单链表的存储空间,合并过程中只需要把 La 和 Lb 两个单链表中的结点重新进行链接即可。

算法步骤

(1) 设立3个指针 pa、pb 和 pc,其中 pa 和 pb 初始分别指向 La 和 Lb 的第一个数据结点。

(2) 链表Lc 取 La 的头结点作为自己的头结点。

(3) 指针 pc 初始指向 Lc 的头结点。

(4) 当指针 pa 和 pb 均未到达相应的表尾时,则依次比较 pa 和 pb 所指向的元素值,从 La 或 Lb 中“摘取”元素较小的结点插入到 Lc 链表的尾部。

(5) 将非空链表的剩余结点段插入到 pc 所指向的结点之后。

(6) 释放掉 Lb 的头结点存储空间。

算法描述】C语言实现

  1. //归并两个有序单链表
  2. void MergeList_L(LinkList La,LinkList Lb,LinkList Lc)
  3. {
  4. ListList pa,pb,pc;
  5. pa=La->next; //pa初始指向La的第一个数据结点
  6. pb=Lb->next; //pb初始指向La的第一个数据结点
  7. Lc=La; //Lc初始取La的头结点为自己的头结点
  8. pc=Lc; //pc初始指向Lc的头结点
  9. while(pa && pb)
  10. {
  11. if(pa->data <= pb->data)
  12. {
  13. pc->next=pa; //将pa所指结点链接到pc所指结点之后
  14. pc=pa; //pc 指向 pa,亦即pc指向链表Lc的尾结点
  15. pa=pa->next; //pa 指向下一个结点
  16. }
  17. else
  18. {
  19. pc->next=pb; //将pb所指结点链接到pc所指结点之后
  20. pc=pb; //pc 指向 pb,亦即pc指向链表Lc的尾结点
  21. pb=pb->next; //pb 指向下一个结点
  22. }
  23. }
  24. pc->next = pa ? pa:pb; //将非空链表的剩余结点段插入到pc所指结点之后
  25. free(Lb); //释放Lb的头结点存储空间
  26. }

二 循环链表

循环链表(Circular linked list) 是另一种形式的链式存储结构。

它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他节点,如下图所示为单链的循环链表。类似地,还可以有多重链的循环链表。

watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAeXVuZmFuMTg4_size_20_color_FFFFFF_t_70_g_se_x_16

循环链表的操作和线性链表基本一样,差别仅在于:当链表遍历时,判别当前指针 p 是否指向表尾结点的终止条件不同。在单链表中,判别条件为:p != NULL 或 p->next != NULL;而循环单链表的判别条件为:p != L 或 p->next != L。

在某些情况下,若在循环链表中设立尾指针而不设头指针(如下图 2.13(a) 所示),可使某些操作简化。例如,将两个线性表合并成一个表时,仅需将第一个表的尾指针指向第二个表的第一个数据结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点存储空间。

当线性表以 图 2.13(b) 的循环链表作为存储结构时,这个操作仅需改变两个指针值即可,主要语句段如下:

  1. //指针A,B分别指向循环链表A,B的尾结点
  2. p = B->next->next; //指针p指向链表B的第一个数据结点
  3. B->next = A->next; //循环链表B的尾结点指针域指向A的头结点
  4. A->next = p; //循环链表A的尾结点指针域指向链表B的第一个数据结点

上述操作的时间复杂度为 O(1),合并后的表如图 2.13(b) 所示。

watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAeXVuZmFuMTg4_size_16_color_FFFFFF_t_70_g_se_x_16

2.0 循环链表的数据结构定义

代码描述】C语言实现

  1. //相关的头文件
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <time.h> //srand((unsigned)time(NULL));
  6. //函数结果状态代码
  7. #define TRUE 1
  8. #define FALSE 0
  9. #define OK 1
  10. #define ERROR 0
  11. #define INFEASIBLE -1
  12. #define OVERFLOW -2
  13. //Status是函数的类型,其值是函数结果状态代码
  14. typedef int Status;
  15. //双向链表元素类型定义
  16. typedef int ElemType;
  17. //循环链表结点的数据结构定义
  18. typedef struct LNode {
  19. ElemType data; // 数据域
  20. struct LNode *next; // 指针域
  21. }LNode, *CircleList;

2.1 构造一个空的循环链表(带头结点)

算法描述】C语言实现

  1. //构造一个空的循环链表(带头结点)
  2. Status InitList_CL(CircleList *L)
  3. {
  4. *L = (LNode*)malloc(sizeof(LNode));
  5. if(!L) { //存储空间分配失败
  6. printf("malloc failed.\n");
  7. exit(OVERFLOW); //exit(-2)程序异常退出
  8. }
  9. memset(*L, 0, sizeof(LNode)); //存储空间初始置零
  10. (*L)->next = *L; //先建立一个带头结点的单链表,并使头结点指向链表自身(即头指针L)
  11. return OK;
  12. }

<Tips> 函数参数 L 是一个二级指针,之所以要使用一个二级指针作为函数形参,是为了将 malloc 动态分配的内存空间的首地址通过这个二级指针传递给调用该函数的实参一级指针。*L 运算就是实参一级指针的值,当然这个值是一个地址。

2.2 置空一个循环链表

算法描述】C语言实现

  1. //置空一个循环链表
  2. //所谓“置空”循环链表,即释放循环链表中所有非头结点的存储空间
  3. Status ClearList_CL(CircleList L)
  4. {
  5. if(!L)
  6. {
  7. printf("CircleList uninitialized.\n");
  8. return INFEASIBLE;
  9. }
  10. if(L->next == L)
  11. {
  12. printf("CircleList has been empty.\n");
  13. return OK;
  14. }
  15. CircleList p, q; //指针p为循环链表的工作指针,q为临时指针
  16. p = p->next; //p初始指向第1个数据结点
  17. while(p != L) //p指向头结点时,while循环结束
  18. {
  19. q = p->next; //将p当前指向的结点的后继节点的位置指针暂存于指针q中
  20. free(p); //释放p当前指向的结点的存储空间
  21. p = q; //p访问下一个结点
  22. }
  23. L->next = L; //头结点指针域指向自身结点
  24. return OK;
  25. }

2.2 销毁一个循环链表

算法描述】C语言实现

  1. //销毁一个循环链表
  2. Status DestroyList_CL(CircleList L)
  3. {
  4. if(!L)
  5. {
  6. printf("CircleList uninitialized.\n");
  7. return ERROR;
  8. }
  9. ClearList_CL(L); //置空循环链表
  10. free(L); //释放头结点
  11. return OK;
  12. }

2.3 判定一个循环链表是否为空

算法描述】C语言实现

  1. //判定一个循环链表是否为空
  2. //若循环链表为空,则返回TRUE,否则返回FALSE
  3. Status ListIsEmpty_CL(CircleList L)
  4. {
  5. if(!L)
  6. {
  7. printf("CircleList uninitialized.\n");
  8. return INFEASIBLE;
  9. }
  10. else if(L->next == L)
  11. {
  12. printf("CircleList is empty.\n");
  13. return TRUE;
  14. }
  15. else
  16. {
  17. printf("CircleList is not empty.\n");
  18. return FALSE;
  19. }
  20. }

2.4 计算一个循环链表的长度

算法描述】C语言实现

  1. //计算一个循环链表中数据结点的个数
  2. int ListLength_CL(CircleList L)
  3. {
  4. if(!L)
  5. {
  6. printf("CircleList uninitialized.\n");
  7. return INFEASIBLE;
  8. }
  9. int count = 0;
  10. CircleList p = L->next; //p为循环链表的工作指针,初始指向链表的第一个数据结点
  11. while(p != L) {
  12. count++;
  13. p = p->next;
  14. }
  15. return count;
  16. }

2.5 获取循环链表中第 i 个位置的结点元素值

算法描述】C语言实现

  1. //获取循环链表中第i个位置的结点元素值
  2. //用参数e接收循环链表L中第i个结点元素的值
  3. Status GetElem_CL(CircleList L, int i, ElemType *e)
  4. {
  5. if(!ListIsEmpty_CL(L) || i<=0)
  6. return ERROR;
  7. CircleList p = L; //p为循环链表的工作指针,初始指向链表的头结点
  8. int j = 0; //j为计数器
  9. //查找第i个结点的位置,且该结点不能为表头结点
  10. while(p->next!=L && j<i)
  11. {
  12. p = p->next; //p指向下一个结点
  13. j++; //计时器j相应加1
  14. }
  15. //如果遍历到头了,说明没有找到合乎目标的结点
  16. if(p==L || j>i)
  17. return ERROR;
  18. *e = p->data;
  19. return OK;
  20. }

算法分析

该算法的基本操作是比较 j 和 i,并后移指针 p,while 循环体中的语句频度与位置 i 有关。若 1\\leqslant i \\leqslant n,则频度为 i-1,一定能取值成功;若 i > n,则频度为 n,取值失败。因此该算法的最坏时间复杂度为 O(n),其平均时间复杂度也为 O(n)

2.6 循环链表的按值查找

算法描述】C语言实现

  1. /*
  2. * 循环链表的按值查找
  3. * 在带头结点循环链表L中查找值为e的结点元素
  4. * 若查找成功,则返回结点位置指针,否则返回NULL
  5. */
  6. LNode* LocateElem_CL(CircleList L, ElemType e)
  7. {
  8. if(!ListIsEmpty_CL(L)) //循环链表为空,返回NULL
  9. return NULL;
  10. CircleList p = L->next; //p为循环链表的工作指针,初始指向首元结点
  11. while(p!=L && p->data!=e)
  12. p = p->next;
  13. if(p != L) //查找成功则返回结点地址p;查找失败则返回NULL
  14. return p;
  15. else
  16. return NULL;
  17. }

算法分析

该算法的执行时间与待查的值 e 相关,其平均时间复杂度类似于算法 2.5,也为 O(n)

2.7 循环链表的插入操作

算法描述】C语言实现

  1. /*
  2. * 循环链表的插入
  3. * 在带头结点循环链表L中的第i个位置插入值为e的新结点
  4. */
  5. Status ListInsert_CL(CircleList L, int i, ElemType e)
  6. {
  7. if(!L)
  8. {
  9. printf("CircleList uninitialized.\n");
  10. return INFEASIBLE;
  11. }
  12. CircleList p;
  13. int j;
  14. p = L; //p为循环链表的工作指针,初始指向链表的头结点
  15. j = 0; //j为计数器
  16. //先查找第i-1个位置的结点,并使p指向该结点
  17. while(p->next!=L && j<i-1)
  18. {
  19. p = p->next;
  20. j++;
  21. }
  22. if(p==L || j>i-1)
  23. return ERROR;
  24. CircleList s = (CircleList)malloc(sizeof(LNode));
  25. //将新结点s插入循环链表L中
  26. s->data = e;
  27. s->next = p->next; //新结点的指针域指向原链表的第i个结点
  28. p->next = s; //p此时是指向第i-1个结点的,将该结点指针域指向新结点s
  29. return OK;
  30. }

算法分析

该算法的平均时间复杂度也为 O(n)。这是因为,为了在第 i 个结点之前插入一个新结点,必须首先找到第 i-1 个结点的位置,其时间复杂度与算法 2.6相同,为 O(n)

2.8 循环链表的删除操作

算法描述】C语言实现

  1. /*
  2. * 循环链表的删除操作
  3. * 在带头结点循环链表L中,删除第i个结点元素
  4. */
  5. Status ListDelete_CL(CircleList L, int i)
  6. {
  7. if(!L)
  8. {
  9. printf("CircleList uninitialized.\n");
  10. return INFEASIBLE;
  11. }
  12. CircleList p, q;
  13. int j;
  14. p = L; //p为循环链表的工作指针,初始指向链表的头结点
  15. j = 0; //j为计数器
  16. while(p->next!=L && j<i-1) //查找第i-1个结点的位置,并使p指向该结点
  17. {
  18. p = p->next;
  19. j++;
  20. }
  21. if(p->next==L || i<1 || j>i-1)
  22. return ERROR;
  23. q = p->next; //q临时保存被删结点的位置地址
  24. p->next = q->next; //改变被删结点的前驱结点的指针域
  25. free(q); //释放被删结点的空间
  26. return OK;
  27. }

算法分析

删除操作类似于插入操作,删除操作的算法时间复杂度亦为 O(n)

2.9 遍历循环链表

算法描述】C语言实现

  1. /* 遍历循环链表
  2. * 遍历带头结点的循环链表L,并输出所有数据结点的值
  3. */
  4. Status ListTraverse_CL(CircleList L)
  5. {
  6. if(!L)
  7. {
  8. printf("CircleList uninitialized.\n");
  9. return INFEASIBLE;
  10. }
  11. if(L->next == L)
  12. {
  13. printf("CircleList is empty.\n");
  14. return ERROR;
  15. }
  16. CircleList p = L->next; //p为循环链表的工作指针,初始指向链表的第1个数据结点
  17. while(p != L)
  18. {
  19. printf("%d->", p->data);
  20. p = p->next;
  21. }
  22. return OK;
  23. }

算法分析

对于一个有 n 个结点元素的循环链表,该算法的时间复杂度为 O(n)

2.10 创建循环链表(带头结点)

方式1 - 头插法创建循环链表

算法描述】C语言实现

  1. /*
  2. * 头插法创建循环链表
  3. * 逆位序输入(随机产生)n个元素的值,建立带头结点的循环链表L
  4. */
  5. void CreateList_CL_Head(CircleList *L, int n)
  6. {
  7. srand((size_t)time(NULL)); //初始化随机种子
  8. //先建立一个带头结点的空循环链表
  9. *L = (CircleList)malloc(sizeof(LNode));
  10. if(!(*L))
  11. exit(OVERFLOW);
  12. (*L)->next = *L; //初始头结点指针域指向自身
  13. for(int i=0; i<n; i++)
  14. {
  15. CircleList s = (CircleList)malloc(sizeof(LNode)); //生产新结点s
  16. //scanf("%d", &s->data); //手动输入元素值
  17. s->data = rand()%100; //随机生成100以内的数字,将生成的元素值赋给新生成结点的数据域
  18. //将新结点插入到表头结点之后
  19. s->next = *L->next;
  20. *L->next = s;
  21. }
  22. }

方式2 - 尾插法创建循环链表

算法描述】C语言实现

  1. /*
  2. * 尾插法创建循环链表
  3. * 正位序输入(随机产生)n个元素的值,建立带头结点的循环链表L
  4. */
  5. void CreateList_CL_Tail(CircleList *L, int n)
  6. {
  7. srand((size_t)time(NULL)); //初始化随机种子
  8. //先建立一个带头结点的空循环链表
  9. *L = (CircleList)malloc(sizeof(LNode));
  10. if(!(*L))
  11. exit(OVERFLOW);
  12. (*L)->next = *L; //初始头结点指针域指向自身
  13. CircleList r = *L; //指针r初始指向头结点,作用是指向尾结点
  14. for(int i=0; i<n; i++)
  15. {
  16. CircleList s = (CircleList)malloc(sizeof(LNode)); //生产新结点s
  17. //scanf("%d", &s->data); //手动输入元素值
  18. s->data = rand()%100; //随机生成100以内的数字,将生成的元素值赋给新生成结点的数据域
  19. //将新结点插入到表头结点之后
  20. s->next = L; //新结点s的指针域指向头结点L
  21. r->next = s; //当前链表尾结点指针域指向新结点s
  22. r = s; //r指向链表新的尾结点s
  23. }
  24. }

三 算法设计题

Q1:将两个递增的有序链表合并为一个递增的有序链表。

要求结果链表仍使用原来两个链表的存储空间,不另外占用其他的存储空间。表中不允许有重复的数据。

问题分析】要求利用现有的表中结点空间建立新链表,可通过更改结点的指针域来重新建立新的元素之间的线性关系。为保证新表和原来的表一样递增有序,可以利用 尾插法 建立新的单链表。

算法思路】假设待合并的两个递增有序的单链表分别为 La 和 Lb,合并后的新表使用头指针 Lc(Lc的头结点使用La的头结点)指向。

(1)指针 pa 和 pb 分别为链表 La 和 Lb 的工作指针,初始化时分别指向链表的元结点(即第一个数据结点)。

(2)从元结点开始进行大小比较,当两个链表 La 和 Lb 均为到达表尾结点时,依次摘取其中较小者重新链接到 Lc 表的最后。

(3)如果两个表中的元素相等,只摘取 La 表中的元素。删除 Lb 表中的元素,这样确保合并后的表中无重复的元素。

(4)当一个表到达表尾结点为空时,将非空表的剩余结点段直接链接在 Lc 表的最后。

(5)最后释放掉链表 Lb 的头结点存储空间。

算法描述】C语言实现

  1. //将两个递增的有序链表La和Lb合并为一个递增的有序链表Lc
  2. void MergeList(LinkList La, LinkList Lb, LinkList Lc)
  3. {
  4. LinkList pa, pb, pc;
  5. LinkList ps;
  6. pa = La->next; //pa是链表La的工作指针,初始指向La的元结点
  7. pb = Lb->next; //pb是链表Lb的工作指针,初始指向Lb的元结点
  8. Lc = La; //用La的头结点作为Lc的头结点
  9. Lc = pc; //pc是链表Lc的工作指针,初始指向Lc的头结点
  10. while(pa && pb) //链表La和Lb均未达到尾结点时
  11. {
  12. if(pa->data < pb->data)
  13. {//取较小者La中的元素,将pa指向的结点链接在pc指向的结点之后,pa指针后移
  14. pc->next = pa;
  15. pc = pa;
  16. pa = pa->next;
  17. }
  18. else if(pa->data > pb->data)
  19. {//取较小者Lb中的元素,将pb指向的结点链接在pc指向的结点之后,pb指针后移
  20. pc->next = pb;
  21. pc = pb;
  22. pb = pb->next;
  23. }
  24. else
  25. {//相等时取La中的元素,删除Lb中的元素
  26. pc->next = pa;
  27. pc = pa;
  28. pa = pa->next;
  29. ps = pb->next; //将待删除结点的后继结点地址临时存放在ps指针中
  30. free(pb);
  31. pb = ps; //pb指向下一个结点
  32. }
  33. }
  34. pc->next = pa ? pa:pb;
  35. free(Lb);
  36. }

Q2:将两个非递减的有序链表合并为一个非递增的有序链表。

要求结果链表仍使用原来两个链表的存储空间,不另外占用其他的存储空间。表中允许有重复的数据。

问题分析】这道题与 Q1 是类似的思路,从原有的两个有序链表中依次摘取结点,通过更改结点的指针域来重新建立新的元素之间的线性关系,得到一个新的链表。与 Q1 不同的关键点有两个:(1) 为保证新表与原表顺序相反,需要利用头插法建立单链表,而不是利用尾插法。(2)当一个表到达表尾时,另一个非空表的剩余元素应该依次摘取,逐个依次链接在 Lc 表的表头结点之后,而不能一次性全部链接在 Lc 表的最后。

算法思路】假设待合并的两个递增有序的单链表分别为 La 和 Lb,合并后的新表使用头指针 Lc(Lc的头结点使用La的头结点)指向。

(1)指针 pa 和 pb 分别为链表 La 和 Lb 的工作指针,初始化时分别指向链表的元结点(即第一个数据结点)。

(2)从元结点开始进行大小比较,当两个链表 La 和 Lb 均为到达表尾结点时,依次摘取其中较小者重新链接到 Lc 表的表头结点之后。

(3)如果两个表中的元素相等,只摘取 La 表中的元素,同时保留 Lb 表中的元素。

(4)当一个表到达表尾结点为空时,将非空表的剩余结点依次摘取,链接在 Lc 表的表头结点之后。

(5)最后释放掉链表 Lb 的头结点存储空间。

算法描述】C语言实现

  1. //将两个非递减的有序链表La和Lb合并为一个非递增的有序链表Lc
  2. void MergeList(LinkList La, LinkList Lb, LinkList Lc)
  3. {
  4. LinkList pa, pb;
  5. LinkList ps;
  6. pa = La->next; //pa是链表La的工作指针,初始指向La的元结点
  7. pb = Lb->next; //pb是链表Lb的工作指针,初始指向Lb的元结点
  8. Lc = La; //用La的头结点作为Lc的头结点
  9. Lc = ps; //ps是临时指针,指向待摘取的元素,初始指向Lc的头结点
  10. Lc->next = NULL; //将Lc表的尾结点的指针域设置为空
  11. while(pa || pb) //只要有一个链表未达到表尾结点
  12. {
  13. if(!pa) //如果La表遍历完,则将Lb表的剩余结点逐个插入到Lc表的表头结点
  14. {
  15. ps = pb;
  16. pb = pb->next;
  17. }
  18. else if(!pb) //如果Lb表遍历完,则将La表的剩余结点逐个插入到Lc表的表头结点
  19. {
  20. ps = pa;
  21. pa = pa->next;
  22. }
  23. else if(pa->data <= pb->data) //取较小者La中的元素
  24. {
  25. ps = pa;
  26. pa = pa->next;
  27. }
  28. else //取较小者Lb中的元素
  29. {
  30. ps = pb;
  31. pb = pb->next;
  32. }
  33. //将ps指向的待插结点插在Lc的表头结点之后(头插法)
  34. ps->next = Lc->next;
  35. Lc->next = ps;
  36. }
  37. free(Lb); //释放Lb的头结点
  38. }

参考

《数据结构(C语言版)》严蔚敏,吴伟民 (编著)

《数据结构题集(C语言版)》严蔚敏,吴伟民,米宁 (编著)

《数据结构(C语言版-第2版)》严蔚敏 , 李冬梅 , 吴伟民 (编著)

《数据结构习题解析与实验指导》李冬梅 张琪 (编著)

第2章 线性表 - 单链表链式存储

《数据结构(C语言版)》- 循环链表

发表评论

表情:
评论列表 (有 0 条评论,281人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线性表示

    上篇文章是线性表的顺序表示,本篇便是线性表的链式表示。 主函数的步骤包括,输入线性表数据,对链表的删除,插入。利用指针进行对链表的访问。 同时为了增加程序可读性,将结构体定

    相关 线性表示

    上篇文章是线性表的顺序表示,本篇便是线性表的链式表示。 主函数的步骤包括,输入线性表数据,对链表的删除,插入。利用指针进行对链表的访问。 同时为了增加程序可读性,将结构体定

    相关 线性

    线性链表存储结构的特点:用一组任意的存储单元存储线性表的数据元素(存储单元可以是连续的,也可以是不连续的) 数据元素a与其直接后继a+1之间的逻辑关系,对数据元素a来说,除了