【算法】链表反转并不简单
- 作者: Wel2018
- 文章状态: 更新中
- 系列状态: 未完结
- 阅读预计用时:10 分钟
- 编程语言:C++
本文概要
如果在求职面试过程中遇到【链表反转】这道题,很可能是面试官对你非常满意,并没有为难你的意思,对整个链表反转大部分人都能一次通过。但也不可高兴太早,面试官看你代码写的如同行云流水,说不定他一高兴表示要给题目加点限制,比如要求 实现一下链表的局部反转、局部旋转、两两旋转、分组反转 等等,这些题有难有易,但是只要平时好好分析一下,相信也能一次通过的。
更新记录
- 2022-08-11:发布
待更新:K 个一组翻转链表。
整体反转
- 题目要求:给你单链表的头节点
head,请你反转链表,并返回反转后的链表。 - 链接:206. 反转链表 - 力扣(LeetCode)
- 方法:递归法、迭代法
方法 1:迭代法
思路:使用三个指针分别记录当前位置、上一个和下一个位置,那么“在指针滑行的途中”呢:
- 先备份下一个位置
- 修改指向关系,完成指针当前位置的反转
- 迭代,重复上述操作
- 遍历完成,此时当前位置到达 nullptr,新的头结点位于上一个位置
代码:
class Solution {
public:
// 迭代法、三指针
ListNode* reverseList(ListNode* head)
{
// 1、参数检查:至少要有两个节点
if(!head || !head->next) return head;
// 2、准备工作
ListNode* pre = nullptr;
auto cur = head;
ListNode* nex = nullptr;
// 3、反转
while(cur) {
nex = cur->next; // 备份下一个位置
cur->next = pre; // 当前位置链接前一个节点
pre = cur;
cur = nex;
}
// 4、返回
return pre;
}
};
注:
auto cur = head没问题,写auto pre = nullptr是错的,因为 nullptr 是 nullptr_t 类型,此处无法推导出 ListNode 指针类型,会提示error: assigning to 'nullptr_t' from incompatible type 'ListNode *'(看起来很低级,但面试时一紧张可能出现各种难以想到的失误)cur->next = pre操作很好理解,关键是其他几个指针操作的顺序,nex = cur->next本来是写在最后的,但是由于反转操作,导致 cur 的 next 会丢失,因此要将它写在反转操作之前。- 返回 pre 而不是 cur:
while(cur)的退出条件时cur == null,此时 pre 停留在最后一个有效元素,它为链表的新 head。
方法 2:递归法
class Solution {
public:
// 递归法
ListNode* reverseList(ListNode* head) {
// 1、参数检查:至少要有两个节点
if(!head || !head->next) return head;
// 2、准备工作
ListNode* cur = head;
ListNode* nex = cur->next;
// 3、反转
// 返回 nex 链表反转后的头结点
auto nh = reverseList(nex); // (1)
// 让 nex 指向 cur
cur->next = nullptr;
nex->next = cur;
// 4、返回 (2)
return nh;
}
};
注:
- (1)必须写在修改指向之前,因为
nex->next = cur修改了 nex 的指向; - (2)返回的是新头结点,由递归(前序遍历)到最后一个有效节点时返回。
局部反转
- 题目要求:给你单链表的头指针
head和两个整数left和right,其中left <= right。请你反转从位置left到位置right的链表节点,返回 反转后的链表 。 - 链接:92. 反转链表 II
- 方法:穿针引线法
对于如下示例:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
反转前后的指针位置如图所示:
具体步骤如下:
- 给定的区间范围是从 1 开始的,先建立头部伪节点
- 穿针引线,确定两个边界,将待反转部分切分出来
- 对切分部分直接反转
- 将反转完成的局部链表拼接回去
代码:
class Solution {
ListNode* reverseList(ListNode* head) {
...
}
public:
ListNode* reverseBetween(ListNode* head, int left, int right)
{
if(!head) return head;
// 1、给定的区间范围是从 1 开始的,先建立头部伪节点
auto dummy = new ListNode(-1);
dummy->next = head;
auto p_left = dummy, p_right = dummy;
auto p = p_left->next;
// 2、确定两个边界 L、R,将中间待反转部分 [p_l, p_r] 提取出来
for(int i = 0; i < left-1; i++) {
p = p->next;
p_left = p_left->next;
}
auto L = p_left;
for(int i = 0; i < right; i++) {
p_right = p_right->next;
}
auto R = p_right->next;
p_right->next = nullptr;
// 3、对中间待反转部分直接反转
auto nh = reverseList(p);
p = nh;
// 4、再拼接回去
L->next = nh;
while(p && p->next) p = p->next;
p->next = R;
// 5、返回
return dummy->next;
}
};
局部旋转
- 题目要求:给你一个链表的头节点
head,旋转链表,将链表每个节点向右移动k个位置。 - 链接:61. 旋转链表 - 力扣(LeetCode)
- 方法:迭代法(穿针引线)
旋转前后示意图如下:
代码:
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k)
{
// 当只有一个或没有节点时,直接返回
if(!head || !head->next) return head;
// 求链表长度
int n = 0;
auto p = head;
while(p) { n++; p = p->next; }
k %= n;
if(k == 0) return head;
// 准备
auto dummy = new ListNode(-1);
dummy->next = head;
auto p1 = dummy->next;
auto p2 = dummy;
int n1 = n - k;
while(n1--) { p2 = p2->next; }
// 根据 k 将链表分为两组
auto q1 = p2->next;
auto q2 = q1;
while(q2 && q2->next) { q2 = q2->next; }
// 交换这两个子链表的位置
p2->next = nullptr;
dummy->next = q1;
q2->next = p1;
// 返回
auto ret = dummy->next;
delete dummy;
return ret;
}
};
两两反转
- 题目要求:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
- 链接: 24. 两两交换链表中的节点 - 力扣(LeetCode)
- 方法:迭代法
反转前后示意图如下:
代码:
class Solution {
public:
// 迭代法
ListNode* swapPairs(ListNode* head) {
// check
if(!head) return nullptr;
auto dummy = new ListNode(-1);
dummy->next = head;
auto p = dummy;
// [p, p1, p2] => [p, p2, p1]
while(p->next && p->next->next) {
auto p1 = p->next;
auto p2 = p->next->next;
// 更新链接
p->next = p2;
auto p3 = p2->next;
p2->next = p1;
p1->next = p3;
// 指向下一位置
p = p1;
}
return dummy->next;
}
};
总结
注:待内容全部同步完成后再做总结。
鸣谢
无
说明
- 【算法】系列相关博客正在更新中,感兴趣的朋友欢迎 star,您的支持是我继续更新下去的最大动力!
- 由于本人水平、精力有限,文中可能存在疏漏之处,欢迎读者大佬们指正。
- 对于高质量、格式规范的建议,确认无误后会合并到博客中,并将贡献者加入【鸣谢】名单中。
- 可以随意转载,但要注明出处。