修改日期 2022/03/22 新增递归方法
修改日期 2021/09/13 新增了两个链表中元素相同的处理方法
我们考虑如何将两个值域有序的单链表合并成一个新的有序单链表,使用原有的单链表的头结点作为新链表的头结点。
第一种(递归方法)
所谓递归,就是自己调用自己。当解决一个问题时,处理第一步和后面n步的每一步,方法都相同,我们可以考虑将其定义为递归方法。
在处理这个合并两个有序单链表时,我们可以通过不断比较两个表的数据域的大小,开始时是先比较第一个链表的第一个节点的数据域与第二个链表的第一个节点的数据域的大小。先将较小者所在的链表,“一分为二”,一部分是正在比较数据域大小的那个节点,另一部分是剩下的节点。然后再将一分为二的链表的后面部分,与另一个单链表合并(递归方法),最后再将合并好的链表插在刚刚一分为二的那个链表的前一部分上,这样就完成了合并。
而前面所写的“将一分为二的链表的后面部分,与另一个单链表合并”,这一操作正体现了递归的思想。如果再将这一操作细分,就是将”一分为二的链表的后面部分“视为我们合并两个有序单链表的第一个链表,将”另一个单链表“视为我们合并两个有序单链表的第二个链表,然后再这次操作中,我们又会遇到这次操作中的“将一分为二的链表的后面部分,与另一个单链表合并”的操作。一直这样细分下去,就会将一开始的两个链表变成都只有一个节点的链表。这样对于只有一个节点的链表我们就很容易合并了。
下面是递归实现
LinkList combine_linklist(LinkList L1, LinkList L2)
{
if(L1==NULL)
return L2;
if(L2==NULL)
return L1;
if(L1->data < L2->data)
{
L1 = combine_linklist(L1->pNext, L2);
return L1;
}
else
{
L2 = combine_linklist(L2->pNext, L1);
return L2;
}
}
第二种(非递归)
依旧通过不断比较两个表的数据域的大小,先将较小者插入到以原有表的其中一个头结点构造的空表尾部,再比较再将较小者插入新表的尾部,一直执行下去(如果遇到两者相同,则保留任意一个)。
当遇到原有两个表其中一个表,先到达尾节点时,只需将没有到达尾节点的表剩余的节点全部插入到新表尾部即可。
下面我们代码实现(注意看代码注释)
void combine_linklist(LinkList Ll1, LinkList Ll2)
{
LinkList C; //指向空表C的头结点的指针(后续构造的新表)
LinkList p = Ll1->pNext; //指针p始终指向表一中将要判断指针域大小的节点,初始指向表一头结点的后驱节点
LinkList q = Ll2->pNext; //指针q始终指向表二中将要判断指针域大小的节点,初始指向表二头结点的后驱节点
LinkList r; //指针r始终指向空表C的尾节点,初始指向空表C的头结点
LinkList s; //临时保存指针p或q,可以通过直接操作s,来操作未进行后移的指针p或q
//借助第一个链表的头结点构造一个新的空表,该空表用来存储新合并的有序链表
C = Ll1;
C->pNext = NULL;
//指针r,始终指向空表C的尾节点。(初始指向空表C的头结点)
r = C;
//释放表二的头结点
free(Ll2);
//当p和q都不为空的时候执行循环,当其中有一个为空时,只需将不为空所在的表剩余的节点全部插入到表C尾部即可
while (p && q)
{
//整个if语句是用来找到,表一和表二中将要比较数据域的两个节点中较小的节点,并将指向该节点的指针用s临时保存
//如果表一的数据域小于表二的数据域
if (p->data < q->data)
{
//先将指针p保存到指针s, 因为后续p要后移,以保证可以通过直接操作s,来操作未进行后移的指针p
s = p;
//p后移,以保证始终指向表一中将要判断指针域大小的节点
p = p->pNext;
}
//如果表二的数据域小于或等于表一的数据域
else if(p->data < q->data)
{
//先将指针q保存到指针s, 因为后续q要后移,以保证可以通过直接操作s,来操作未进行后移的指针q
s = q;
//q后移,以保证始终指向表二中将要判断指针域大小的节点
q = q->pNext;
}
else //如果两个相等
{
//找出任意一个处理即可,但是两个始终指向链表中将要判断指针域大小的节点的指针p,q都要后移
s = q;
q = q->pNext;
p = p->pNext;
}
//将找到的较小者,通过操作临时保存过指向该较小者的指针s,来插入表C的尾部
s->pNext = r->pNext; //相当于将插入表C尾部的节点的指针域置空
r->pNext = s; //将表C的尾节点的指针域指向要插入的节点(执行插入操作)
r = r->pNext; //r后移,以保证始终指向表C的尾节点
}
//判断循环是因为p空而结束,还是因为q空而结束
if (p)
//如果是q空导致,则将表一剩余部分插入到C表的尾部
r->pNext = p;
else
//如果p空导致,则将表二剩余部分插入到C表的尾部
r->pNext = q;
//结束函数
return;
}
因为上述代码只是整个可以运行的代码中独立封装的一个函数,不太完整,最后下面给出完整函数(完整函数中去除了这个合并函数)
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
typedef struct Node
{
int data; //数据域
struct Node * pNext; //指针域
}LNode, * LinkList;
//函数声明
LinkList create_linklist(); //创建一个链表
void combine_linklist(LinkList, LinkList); //合并两个有序表
void show_linklist(LinkList); //输出链表数据域
int main(void)
{
LinkList Ll1 = NULL; //定义一个指向空的节点指针
LinkList Ll2 = NULL; //定义一个指向空的节点指针
Ll1 = create_linklist(); //创建第一个单链表
Ll2 = create_linklist(); //创建第二个单链表
show_linklist(Ll1); //输出表一
show_linklist(Ll2); //输出表二
combine_linklist(Ll1, Ll2); //合并表一表二
show_linklist(Ll1); //输出合并的新表
return 0;
}
LinkList create_linklist()
{
int len; //用来存储节点的个数
int i; //for循环中的循环变量
int val; //用来存放节点的值域
//为头结点分配内存
LinkList Ll = (LinkList)malloc(sizeof(LNode));
Ll->pNext = NULL;
//判断是否分配成功
if(!Ll)
{
printf("动态内存分配失败,程序退出!\n");
exit(-1);
}
//请求用户输入节点个数
printf("请输入节点的个数:\n");
scanf("%d", &len);
//定义一个始终指向当前链表尾节点的指针,初始指向头结点
LinkList pTail = Ll;
pTail->pNext = NULL;
for (i = 0; i < len; i++)
{
//每循环一次生成一个新的节点
LinkList LNew = (LinkList)malloc(sizeof(LNode));
//判断是否分配成功
if(!LNew)
{
printf("动态内存分配失败,程序退出!\n");
exit(-1);
}
//请求用户输入新节点的值
printf("请输入第%d个节点的值\n", i+1);
scanf("%d", &val);
//将用户输入的值存储到数据域
LNew->data = val;
//将原始链表的最后一个节点挂在新节点上
pTail->pNext = LNew;
//新节点变成当前的尾节点,需要将其指针域变为空
LNew->pNext = NULL;
//pTail后移,使其指向当前的尾节点
pTail = LNew;
}
//提示用户链表创建成功
printf("链表创建成功!\n");
//返回指向头结点的指针
return Ll;
}
void show_linklist(LinkList Ll)
{
LinkList p = Ll->pNext;
while(p != NULL)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
return;
}