[数据结构习题]链表——单链表重排
👉知识点导航💎:【数据结构】线性表——顺序存储
👉知识点导航💎:【数据结构】线性表——链式存储
👉[王道数据结构]习题导航💎: p a g e 41.25 page41.25 page41.25
本节为链表相关操作的习题 |
☀️题目描述:
🎇思路:单链表逆置
🔱思路分析:
题目解析:我们先分析题目,对于初始单链表 L ( a 1 , a 2 , a 3 , . . . , a n ) L(a1,a2,a3,...,an) L(a1,a2,a3,...,an)和目标单链表 L ′ ( a 1 , a n , a 2 , a n − 1 , . . . ) L'(a1,an,a2,a_{n-1},...) L′(a1,an,a2,an−1,...),不难发现 L ′ L' L′是通过 L L L摘取第 1 1 1和第 n n n个元素,再摘取第 2 2 2和第 n − 1 n-1 n−1元素,…,最后摘取第 k k k和第 n − k + 1 n-k+1 n−k+1个元素(奇数个结点时也可能没有),最后拼接而成的单链表
即首尾对应依次取出,可以分为前后两段,一个指针不断向后,一个指针不断向前? ❌
这就是这道题的关键问题:单链表只能从前向后找,而不能从后向前,且题目要求空间复杂度为 O ( 1 ) O(1) O(1),也就是不能构造辅助数组存储,那么应该怎么做呢?
既然不能从后向前,那我们就想办法让前段和后段在遍历时保持一致,可以通过对单链表后段进行逆置处理
step:
1. 找中间结点
要想逆置后段链表,则必须先找到前段和后段的分界点,所以我们第一步是找到中间结点
对于奇数个结点 ( 2 n + 1 ) (2n+1) (2n+1)的链表:我们需要找到第 n + 1 n+1 n+1个结点;对于偶数个结点 2 n 2n 2n的链表:我们需要找到第 n n n个结点
算法思路:
p移动1步,q移动2步(最后一步时,若为奇数个结点则移动1步),当q指向尾结点时,p正好指向中间结点:
- 设置两个指针 p , q p,q p,q,开始时,同时指向头结点,开始向后移动;
- 当q指向的下一个结点不为 N U L L NULL NULL时, p , q p,q p,q同时移动1步;
- 此时,继续判断q指向的下一个结点是否为 N U L L NULL NULL,若不为空,则再向后移动1步;若为空,则结束,此时的p即指向中间结点
①偶数个结点:
②奇数个结点:
key:q每走一步都要进行一次判空操作
代码实现:
LNode* p = L, * q = L; //p,q初始都指向头结点
while (q->next != NULL) {
p = p->next;
q = q->next;
if (q->next != NULL) //q每走一步都要先判断
q = q->next;
}
2. 后段链表逆置
在找到中间结点后,我们就可以对结点后面的链表进行逆置处理啦~
对于单链表的逆置,其无法像数组一样随机访问,直接交换首尾的数据,所以我们的思路是:“让单链表的箭头反向”,再将中间结点连接至最后一个结点
算法思路:
-
先让指针 q q q指向后段的首结点,再将指向中间点的指针 p p p 指向 N U L L NULL NULL
这里为什么要让指针 p p p 指向NULL呢?其实目的就是让前段和后段暂时断开
图解:
-
当 q q q 不指向 N U L L NULL NULL时(即后段还有剩余结点未处理时),我们先记录 q q q 的下一个结点指针 r r r,再让当前 q q q的 n e x t next next指向 p p p的下一个结点, q q q的 n e x t next next结点指向 p p p,最后移动 q q q至 r r r位置;
这里我们要思考:
1. q->next=p->next;p->next=q的含义是什么?
p作为中间结点的指针,若要链表逆序,则最后p的next指针一定是要指向链表的尾结点的,所以,我们每扫到一个后段结点,都把他当作尾结点来看,则 q的next指针要指向上一轮 p的next指向的结点,这样就完成了箭头反向(因为上一轮p的next指向前一个结点),再让 p的next指向当前结点 q,即连接了表尾,实现了部分逆序2. 为什么要单独记录 q q q的 n e x t next next指针 r r r?
因为当q->next发生改变后,q之后的后段结点与当前逆序的部分断开,若不记录,则无法访问下一个结点
图解:
①第一次连接:
②第二次连接:
③第三次连接:
我们可以看到,这样就逐步实现了单链表的后段逆置操作
代码实现:
q = p->next; //q指向后段的首结点
p->next = NULL;
LNode* r; //每次都指向q的下一个结点
while (q != NULL) {
r = q->next; //记录
q->next = p->next; //相当于将单链表箭头反向
p->next = q;
q = r; //移动至下一结点
}
3. 重排单链表
在完成上述操作后,我们就可以对前段和后段进行交替插入,即完成单链表的重排啦~
算法思路:
-
让 s s s指向前段的首结点, q q q指向后段的首结点,中间指针 p p p指向 N U L L NULL NULL
注意:
这里中间指针 p p p指向 N U L L NULL NULL的操作很重要!!由于中间结点(两个或一个)在重排后一定是不动的,所以中间结点或中间结点的 next在重排后会变为尾结点,因此必须使其指向 NULL,相当于特判,否则最后打印单链表时会进入死循环
-
接下来就是插入操作,即不断让当前 q q q指向的结点插入到 s s s结点后,直至 q q q指向 N U L L NULL NULL(即后段结点全部处理完毕)
代码实现:
q= p->next; //q重新指向重排后的后段首结点
LNode* s = L->next; //s指向前段的首结点
p->next = NULL; //这里很重要,让中间结点指向NULL,否则会进入死循环
while (q != NULL) { //当后段还有结点没被重排
r = q->next; //记录下一结点——防止重排后丢失
//插入
q->next = s->next;
s->next = q;
s = q->next; //s移动至下一个
q = r; //q移动至下一个
}
完整代码实现:
#include<iostream>
using namespace std;
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
//0.初始化
void InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode)); //定义头结点
L->next = NULL;
LNode* p = L;
int x;
while (cin >> x) {
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = x;
p->next = s;
p = s;
if (cin.get() == '\n')
break;
}
p->next = NULL;
}
//重排
void reset(LinkList& L) {
//1.找到中间结点
LNode* p = L, * q = L; //p,q初始都指向头结点
while (q->next != NULL) {
p = p->next;
q = q->next;
if (q->next != NULL) //q每走一步都要先判断
q = q->next;
}
//最终p指向中点,q指向尾结点
//2.单链表后段逆置
q = p->next; //q指向后段的首结点
p->next = NULL;
LNode* r; //每次都指向q的下一个结点
while (q != NULL) {
r = q->next; //记录
q->next = p->next; //相当于将单链表箭头反向
p->next = q;
q = r; //移动至下一结点
}
//3.重排插入结点
q= p->next; //q重新指向重排后的后段首结点
LNode* s = L->next; //s指向前段的首结点
p->next = NULL; //这里很重要,让中间结点指向NULL,否则会进入死循环
while (q != NULL) { //当后段还有结点没被重排
r = q->next; //记录下一结点——防止重排后丢失
//插入
q->next = s->next;
s->next = q;
s = q->next; //s移动至下一个
q = r; //q移动至下一个
}
}
void Print(LinkList& L) {
LNode* p = L;
while (p->next != NULL) {
p = p->next;
cout << p->data << " ";
}cout << endl;
}
int main() {
LinkList L;
cout << "请输入单链表的元素值:" << endl;
InitList(L);
cout << "原始链表为:" << endl;
Print(L);
//重排
reset(L);
cout << "重排后链表为:" << endl;
Print(L);
system("pause");
return 0;
}
输出结果: