引言
这篇文章主要是讲单链表的一些常用的进阶操作,主要方法就是双指针,本文篇幅较长,在这里给出问题的索引,可以根据问题索引迅速定位自己需要看的内容(引申问题之后会找机会补充)
-
有序链表的合并——>划分链表
-
单链表反转——>链表相加求和
-
寻找链表的倒数第k个节点——>旋转链表
-
寻找链表的中间节点——>回文串
-
判断链表中是否有环——>求环的长度
双指针
普通双指针(1、2、3)
有序链表的合并
过程思路
有序链表的合并做法跟归并排序的并的时候操作是一个思路,开辟两个指针,分别对应一个链表,指向小元素的指针移动。
代码实现
/*
有序链表的合并,两条链表是上图的两条链表
*/
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE
{
int data;
struct NODE *pNext;
} node; //定义链表节点的类型
node *mergeinto_node(node *pHead1, node *pHead2);
node *create_node(int size);
void print_node(node *);
int main()
{
node *pHead1;
node* pHead2;
pHead1 = create_node(4);
pHead2 = create_node(3);
pHead2 = mergeinto_node(pHead1,pHead2);
print_node(pHead2);
}
node *create_node(int size)
{
int i, val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0; i < size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL; //尾节点要置NULL否则野指针循环无法结束
return phead;
}
void print_node(node *pHead)
{
printf("It's the time to print node.\n");
node *q = pHead->pNext;
while (q != NULL)
{
printf("%d\t", q->data);
q = q->pNext;
}
q = NULL;
}
node* mergeinto_node(node* pHead1,node* pHead2)
{
node *p = pHead1->pNext; //指向第一个链表的第一个节点
node *q = pHead2->pNext; //指向第二个链表的第一个节点
node *pNewhead=(node*)malloc(sizeof(node)); //用于储存合并的链表.
node *t=pNewhead; //合并链表的分身
while(p!=NULL&&q!=NULL) //当两个链表都没有遍历完成的时候循环插入链表元素
{
if(p->data <= q->data) //第一个链表元素更小插入第一个链表元素到合并链表中
{
t->pNext = p;
t = p;
p = p->pNext;
}
if (p->data > q->data) //第二个链表元素更小,插入第二个链表元素到合并链表中
{
t->pNext = q;
t = q;
q = q->pNext;
}
if(p==NULL) //如果第一个链表遍历完成了,但是第二个链表还没有完成(解决链表不等长的问题)
{ //循环把第二个链表的剩下的元素全部加入合并链表中
for (; q != NULL;)
{
t->pNext = q;
t = q;
q = q->pNext;
}
}
if (q==NULL)
{
for (; p != NULL;)
{
t->pNext = p;
t = p;
p = p->pNext;
}
}
}
t->pNext = NULL; //合并链表尾节点置NULL
return pNewhead; //返回合并链表头节点
}
链表的反转(三指针)
过程思路
需要将链表反转,对于第一个和第二个节点,我们可以直接得到的信息是把第二个节点->pNext = 第一个节点的地址,我们改变了第二个节点的pNext,让原本储存第三个节点的地址,变成了第一个节点的地址,此时问题出现了——指针丢失,我们丢失了第三个节点的地址,后续所有节点也找不到了。所以我们要用一个临时节点储存下一个节点的pNext域。这样问题得到解决,而同时我们也需要一个指针储存前一个当前节点的前一个指针域,否则下一次运行的时候就找不到前一个结点了,也就无法把当前节点的指针域指向前一个结点。
代码实现
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE
{
int data;
struct NODE *pNext;
} node; //定义链表节点的类型
node* reverse_node(node *pHead);
node *create_node(int size);
void print_node(node *);
int main()
{
node *pHead;
pHead = create_node(7);
pHead=reverse_node(pHead);
print_node(pHead);
}
node *create_node(int size)
{
int i, val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0; i < size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL; //尾节点要置NULL否则野指针循环无法结束
return phead;
}
node* reverse_node(node* pHead)
{
node *pre = NULL; //首节点没有前一个结点
node *cur = pHead->pNext; //当前节点为首节点
node *next; //储存后继节点
while (cur != NULL)
{
next = cur->pNext; //先储存好会指针丢失的后继节点
cur->pNext = pre; //改变当前节点和前驱结点的指向关系
pre = cur; //更新前驱结点
cur = next; //更新当前节点
}
return pre; //返回前驱结点,这里容易错因为cur已经指向NULL了,说明前驱结点才是有意义的
}
void print_node(node *pHead)
{
printf("It's the time to print node.\n");
node *q = pHead;
while (q != NULL)
{
printf("%d\t", q->data);
q = q->pNext;
}
q = NULL;
}
寻找倒数第n个节点(只用一次遍历)
过程思路
中间节点和倒数第n个节点,都不同于我们直接顺序遍历那么方便,如果是顺序遍历要找第k个节点循环k次就好了,既然顺序遍历这么简单,那我们就思考如何把这个问题转化成顺序遍历的第m个节点。譬如倒数第n个节点,那就是需要顺序查找len-k个节点,如果不是限定了一次遍历,那我们就可以暴力解决,求出len,再循环。
那如果要求只循环一次,怎么控制置顺序查找len-k个节点呢。这时候双指针就出现了。我们需要控制两个指针的距离是m那么,当第一个先走的指针走到表尾的时候,第二个指针走过的路程就是len-m,就符合我们的要求
代码实现
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE
{
int data;
struct NODE *pNext;
} node; //定义链表节点的类型
int find_node(node *pHead,int position);
node *create_node(int size);
int main()
{
node *pHead;
pHead = create_node(7);
printf("the val of position is %d", find_node(pHead, 3));
}
node *create_node(int size) //创建一个链表
{
int i, val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0; i < size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL; //尾节点要置NULL否则野指针循环无法结束
return phead;
}
int find_node(node *pHead,int position)
{
node *first = pHead;
node *second = pHead;
int cnt = 0;
while(first->pNext!=NULL)
{
cnt++; //cnt控制着first与second距离,值一旦到达positon第二个指针出发
first = first->pNext;
if(cnt>=position)
{
second = second->pNext;
}
}
return second->data;
}
快慢双指针(4、5)
寻找链表的中间节点
过程思路
从找倒数第k个节点同样来思考,我可以通过一个指针先走一半节点长,再出发一个指针,当第一个节点到末尾的时候,第二个节点所在的地方就是中间节点。可是面对了一个问题,那就是并不知道链表长度,如果直接用上面的程序无法控制position变量。
此时注意到,指针一半一半的关系,想到快慢指针,第一个指针——》快指针,每次移动两个节点。第二个指针——》慢指针,每次移动一个节点。当快指针到表尾的时候,慢指针自然到中间节点。
代码实现
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE
{
int data;
struct NODE *pNext;
} node; //定义链表节点的类型
int find_midnode(node *pHead);
node *create_node(int size);
int main()
{
node *pHead;
pHead = create_node(7);
find_midnode(pHead);
}
node *create_node(int size)
{
int i, val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0; i < size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL; //尾节点要置NULL否则野指针循环无法结束
return phead;
}
int find_midnode(node *pHead)
{
node *fast = pHead; //如果是pHead->pNext的话,当它长度为偶数,就会指向中间两个的后面那个.
node *slow = pHead;
if(pHead->pNext==NULL) //链表为空
{
return -1;
}
while (fast != NULL && fast->pNext !=NULL) //只要有一个不成立就退出循环所以应该是&&不是|| 否则slow指向尾节点的话退不出来
{
fast = fast->pNext->pNext;
slow = slow->pNext;
}
printf("the mid of nodelist is %d ",slow->data);
if (fast->pNext == NULL) //说明到达链表长度为偶数,慢指针后一个节点也需要输出
{
printf("%d", slow->pNext->data);
}
return 0;
}
判断是否有环
过程思路
同样是快慢指针,如果有环,那么快指针必然会与慢指针相遇,因为两个指针最后都会进入环,而快指针每次又比慢指针多走一步,快指针总会追上慢指针。但是如果快指针先指向NULL,说明那就没有环。
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE
{
int data;
struct NODE *pNext;
} node; //定义链表节点的类型
void guess_circle(node *pHead);
node *create_node(int size);
int main()
{
node *pHead;
pHead = create_node(7);
guess_circle(pHead);
}
node *create_node(int size)
{
int i, val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0; i < size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL; //尾节点要置NULL否则野指针循环无法结束
return phead;
}
void guess_circle(node* pHead)
{
node *fast=pHead;
node *slow=pHead;
while(fast!=NULL)
{
fast = fast->pNext->pNext;
slow = slow->pNext;
if(fast==slow)
{
printf("there exits a circle");
}
}
printf("there don't have circle");
}
这个是上述代码执行结果
将上述代码建立单链表改为循环链表
结果如下判断正确