今天上课看到了,2019年计算机408数据结构部分的链表答题,看完了之后特此来csdn整理一下思路,也算留存一个解题答案。
1.思路整理
题目的内容如下:
题目要求就是将链表内的后半部分的结点进行逆序然后进行交叉重新组合,为了完成这个题目,是需要封装三个函数,分别对应解题思路中的三个步骤 -----取中结点,链表逆转,链表重组。
下面先来理一下解题思路,说一下为什么要使用上述的三个步骤,观察题目可以看出,题目给我们的链表和最后要得到的链表之间的关系,最终要得到的链表中,本来的后半部分接结点被逆序了,例如原链表的顺序为:1-2-3-4-5-6,最后要变为1-6-2-5-3-4.
首先,我们要将原链表拆分。然后我们对得到的后半部分链表(L2表示)进行逆序,逆序完成后,我们在把两个链表进行重组。所以上述的三个方法对应解决的问题就是:拆分,逆序,重组。
2.方法实现
2.1 拆分
//找到列表中间结点,并设置L2结点
void find_middle(NODE* L ,NODE* &L2)
{
//给L2设置空间,第二条链表的头结点
L2 = (NODE*)malloc(sizeof(node));
NODE *p1,*p2;//双指针法
p1 = p2 = L->next;
while (p1)
{
p1 = p1->next;
if(p1 == NULL)//为了防止p1为NULL
{
break;
}
p1 = p1->next;
if(p1 == NULL)//为了使得偶数个时,避免p2多走一步
{
break;
}
p2 = p2->next;
}
L2->next = p2->next;//由L2结点头指向后面的一半链表
p2->next = NULL;//前一半链表的最后一个结点的next要为NULL
}
这一部分的代码就如上图所示,这里使用到了c++中的知识,函数声明中的&不是取地址,而是对L2的引用,引用之后函数内部就可以修改函数之外的变量了,减轻了书写代码的难度,因为如果是在纯c语言中,只有传递给函数相应的指针才可以修改其变量(或其他方法)。
首先,这一部分是为了找到链表的中间部分,可以首先进行一次遍历,得到链表的结点数,然后取中间值,再遍历到中间结点处,取出结点。但是,这里为了减小代码的时间复杂度,我们采用了双指针法,只需要对链表进行一次遍历。我们这里初始化了两个指针变量p1和p2,p1指针每次走两个结点,而p2指针每次走一个结点,当p1指针为NULL时,代表走到了链表的结尾处,所以此时的p2就在链表的中间位置。
前几步的思路就如上图所示,每一次循环就是一次while循环,当然我们要时刻对p1指针进行判断是否为NULL,如果为NULL,则结束循环。
while (p1)
{
p1 = p1->next;
if(p1 == NULL)//为了防止p1为NULL
{
break;
}
p1 = p1->next;
if(p1 == NULL)//为了使得偶数个时,避免p2多走一步
{
break;
}
p2 = p2->next;
这里第一次判断是当走两步后,下一次会不会走到结尾。这个就很好理解就不再阐述了。主要是第二个判断,再画图表示。
假如说,没有第二个判断,则此时p2就应该是上图的样子,p2已经指向了第四个结点,显然不是我们想要的结果,所以我们要在第二次移动时也进行一次判断,防止p2多移动一步。假如按照上述代码进行允许,得到的结果应该和下图所示的一致。
L2->next = p2->next;//由L2结点头指向后面的一半链表
p2->next = NULL;//前一半链表的最后一个结点的next要为NULL
此时我们进行一下两步操作,首先给第二部分插入一个链表头(L2),然后给最后一个结点点next指针赋值为NULL,断开链表。
2.2逆转
//逆转
void reverse(NODE* L2)
{
if(L2->next == NULL)
{
return;//链表为空时退出
}
NODE *r,*s,*t;//三结点法,每次循环,前两个结点逆转
r = L2->next;
s = r->next;
if(s == NULL)
{
return;
}
t = s->next;
while (t)
{
s->next = r;//原地逆转
r = s;//一下三句是三个指针同时走一步
s = t;
t = t->next;
}
s->next = r;
L2->next->next = NULL;//逆转后的,链表的第一个结点要为NULL
L2->next = s;//s为链表的第一个结点
}
这一部分主要定义了三个指针(r,s,t),然后前两个指针(r,s)进行逆转,第三个指针(t)负责判断是否到达终点。首先第一步判断当链表为空时直接退出,因为题目中原链表只有一个结点时,此时L2中是没有结点的,因此这里进行一次判断。
当判断完成后,创建三个NODE*类型的指针,r指向第一个结点,s指向第二个结点,这里还要有一次判断,判断此时r所指向的是否为最后一个元素,如果是则直接退出,因为此时L2内就一个结点,不需要逆转。
判断完成后,将s的下一个结点赋值给t,然后进入while循环,首先第一步是将前两个结点进行逆转,然后后面三行,是将这三个结点统一向右移一位,(记住此时L2中,原本的第一个结点是依旧是指向原本第二个结点的)此时的情况应该如下图。
接下来进行while循环判断t是否为NULL,假如t为空则退出循环 。当t为空时,此时的情况应该如下图所示。
记住此时的r和s是没有逆转的,所以在循环结束后,再进行一次逆转。 如下图所示。
接下来的两步是很重要的。
L2->next->next = NULL;//逆转后的,链表的第一个结点要为NULL
L2->next = s;//s为链表的第一个结点
L2->next->next是等同于此时的1->next的,作用就是将此时1结点的下一个结点设置为NULL,因为此时1结点成为了最后一个结点。,然后将L2(链表头)的next指针指向s,也就是此时的第一个结点。
此时,逆转的工作就完成了。
2.3组合
//合并链表
void merge(NODE* L,NODE* L2)
{
NODE *pcur,*p1,*p2;
pcur = L->next;//pcur始终指向组合后的链表尾部
p1 = pcur->next;//p1用来遍历L列表
p2 = L2->next;//p2指向L2的第一个结点,p2用来遍历L2链表
while (p1 != NULL && p2 != NULL)
{
pcur->next = p2;
p2 = p2->next;//指向下一个
pcur = pcur->next;
pcur->next = p1;
p1 = p1->next;
pcur = pcur->next;
}
//任何一个链表都可能剩余一个结点,放进来即可
if(p1 != NULL)
{
pcur->next = p1;
}
if(p1 != NULL)
{
pcur->next = p2;
}
}
接下来就进行最后一步----组合,详细的代码就如上图所示。
首先还是要定义三个指针,pcur,p1,p2。p1指针--指向L中的结点,,p2指针--指向L2中的结点,pcur指针,则一直指向组合后链表的尾部结点。
先给pcur赋值L中的第一个结点的地址,然后再给p1赋值pcur的下一个结点的地址,然后再给p2赋值L2中的第一个结点的地址。至于为什么不给p1赋值L的第一个结点的地址,而是第二个,是因为L中的第一个结点已经不需要组合了,它就是最终组合完毕的链表的第一个结点。
·然后进入while循环,先判断p1和p2是否为空,如果为空则停止循环,然后给pcur的next指针赋值为p2所指向的结点,然后p2继续往后走,pcur也往后走一步,接下来再组合进入p1所指向的结点,具体步骤和刚刚的操作都是一样的,这三个指针的作用一定要记住,p1和p2就是用来找结点使用的,而pcur是用来把找到的结点一个一个串起来的。
当p1和p2其中有一个走的最末尾时,结束循环,此时任何一个链表都可能剩余一个结点,可以自己画图看看,找到了剩余的结点,就把pcur的next指针赋值给剩余的那个结点的指针。
此时整体的工作就完成了,这里可能有人会问,结尾指针的next里存放的是什么呢?答案是肯定的,是NULL指针,因为在第一步拆分时,我们就已经将L中的最后一个结点的next设置为NULL,在第二步时把L2中的最后一个结点的next设置为NULL。
此时所有的工作就完成了,这一题也就写出来了,最后算一下时间复杂度为1.5n,然后舍去最高项系数,所以时间复杂度T(n) = O(n).
最后我会把源码附在下面,各位可以复制尝试一下,如果有什么问题和可以改进的地方可以尽管给我留言,我会定时查看的,谢谢各位大佬!!!(在源码中尾插法新建链表在最后要输入9999作为结束的标志,其余的代码各位可以自行了解一下,很简单的)
3.源码
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
int data;
struct node* next;
}NODE;
//尾插法新建链表
void list_end_insert(NODE* &L)
{
L = (NODE*)malloc(sizeof(NODE));
L->next = NULL;
int x;
scanf("%d",&x);
NODE *s,*r = L;//s指向下一个元素(s没有赋值为L),r一直指向链表尾部(被赋值为L)
while (x != 9999)
{
s = (NODE*)malloc(sizeof(node));
s->data = x;
r->next = s;//把新结点给尾部结点的next指针
r = s;//r要指向新的尾部
//r是一个远程中继器,用来传递地址,每一次新建一个结点便把新结点的指针赋值给上一个接点的next指针,然后r更改又指向尾部结点
scanf("%d",&x);
}
r->next = NULL;//存储完毕后,将最后一个结点的next指针赋值为NUll
}
//打印链表
void print_list(NODE* L)
{
L = L->next;
while (L)
{
printf("%d ",L->data);
L = L->next;
}
printf("\n");
}
//找到列表中间结点,并设置L2结点
void find_middle(NODE* L ,NODE* &L2)
{
//给L2设置空间,第二条链表的头结点
L2 = (NODE*)malloc(sizeof(node));
NODE *p1,*p2;//双指针法
p1 = p2 = L->next;
while (p1)
{
p1 = p1->next;
if(p1 == NULL)//为了防止p1为NULL
{
break;
}
p1 = p1->next;
if(p1 == NULL)//为了使得偶数个时,避免p2多走一步
{
break;
}
p2 = p2->next;
}
L2->next = p2->next;//由L2结点头指向后面的一半链表
p2->next = NULL;//前一半链表的最后一个结点的next要为NULL
}
//逆转
void reverse(NODE* L2)
{
if(L2->next == NULL)
{
return;//链表为空时退出
}
NODE *r,*s,*t;//三结点法,每次循环,前两个结点逆转
r = L2->next;
s = r->next;
if(s == NULL)
{
return;
}
t = s->next;
while (t)
{
s->next = r;//原地逆转
r = s;//一下三句是三个指针同时走一步
s = t;
t = t->next;
}
s->next = r;
L2->next->next = NULL;//逆转后的,链表的第一个结点要为NULL
L2->next = s;//s为链表的第一个结点
}
//合并链表
void merge(NODE* L,NODE* L2)
{
NODE *pcur,*p1,*p2;
pcur = L->next;//pcur始终指向组合后的链表尾部
p1 = pcur->next;//p1用来遍历L列表
p2 = L2->next;//p2指向L2的第一个结点,p2用来遍历L2链表
while (p1 != NULL && p2 != NULL)
{
pcur->next = p2;
p2 = p2->next;//指向下一个
pcur = pcur->next;
pcur->next = p1;
p1 = p1->next;
pcur = pcur->next;
}
//任何一个链表都可能剩余一个结点,放进来即可
if(p1 != NULL)
{
pcur->next = p1;
}
if(p1 != NULL)
{
pcur->next = p2;
}
}
int main()
{
NODE* L;//链表头,是结构体指针类型
list_end_insert(L);//输入数据3 4 5 6 9999
print_list(L);//链表打印
NODE *L2 = NULL;
find_middle(L,L2);//只有一个结点时,L2是没有结点的
print_list(L);
print_list(L2);
printf("--------------------\n");
reverse(L2);
print_list(L2);
merge(L,L2);
free(L2);
print_list(L);
return 0;
}
//find_middle reverse merge 时间复杂度均为n/2