反转链表系列问题
作者:Grey
原文地址:
反转单链表
题目描述见:LeetCode 206. Reverse Linked List
思路如下
对于任何一个节点 cur
来说,记录一个前驱节点 pre
(第一个节点的前驱节点是 null
)
先用一个临时节点 tmp
记录 cur
的下一个节点,然后设置
cur.next = pre;
pre = cur;
cur = tmp;
以下是示例图
假设原始链表如下
第一个节点的反转流程如下
第二个节点的反转流程如下
最后返回 pre
节点即为反转后的节点。
代码如下
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
时间复杂度 O ( N ) O(N) O(N),空间复杂度 O ( 1 ) O(1) O(1)。
反转链表也可以用递归方法来实现
定义递归函数 ListNode reverse(ListNode cur)
,这个递归函数的含义是
反转以 cur 为头的链表,并把反转后的头节点返回。
这个递归函数的 base case 是,只有一个节点的时候,即
if (cur == null || cur.next == null) {
return cur;
}
这种情况下,直接返回当前节点即可。
接下来是普遍情况:
当前来到 cur 节点,c,d,e 已经完成了反转。
此时 cur 需要做如下操作:把 c , d, e 反转后的头节点获取到,假设为 pre , 在上图中,pre 就是 e 节点,然后 cur 再做如下操作
cur.next.next = cur;
cur.next = null;
其中cur.next = null
非常重要,只有这样,才能防止出现环。完整代码如下
class Solution {
public ListNode reverseList(ListNode cur) {
return reverse(cur);
}
// 反转cur为头的链表,并把反转后的头节点返回
public ListNode reverse(ListNode cur) {
if (cur == null || cur.next == null) {
return cur;
}
ListNode pre = reverse(cur.next);
cur.next.next = cur;
cur.next = null;
return pre;
}
}
时间复杂度 O ( N ) O(N) O(N)
空间复杂度 O ( N ) O(N) O(N)(递归栈占用的空间)
反转双向链表
双向链表和单链表的反转类似,每个节点要多处理一次每个节点的前驱指针,
完整代码如下
public static DoubleNode reverseDoubleList(DoubleNode head) {
if (head == null || head.next == null) {
return head;
}
DoubleNode pre = null;
DoubleNode cur = head;
while (cur != null) {
DoubleNode tmp = cur.next;
cur.next = pre;
cur.last = tmp;
pre = cur;
cur = tmp;
}
return pre;
}
反转单链表一部分
题目描述见:LeetCode 92. Reverse Linked List II
本题核心依然是反转链表,只是增加了一些变量来记录需要反转链表的头位置和结尾位置,不过需要注意的是,本题的链表开始位置是从 1 开始,所以,如果m = 1 && n != 1
,说明反转链表后需要返回新的头部,只要m > 1
,反转链表一部分以后,返回原先的头即可。
此外,本题的 follow up 提到
Could you do it in one pass?
就是遍历一次链表能否解决问题,具体代码如下
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if (head.next == null || left == right) {
// 只有一个节点,怎么反转都一样
// 只要反转一个节点,反转前后链表还是一样的
return head;
}
if (left == 1) {
// 需要换头
ListNode pre = null;
ListNode end = head;
ListNode cur = head;
int gap = right - left + 1;
while (gap != 0) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
gap--;
}
end.next = cur;
return pre;
} else {
ListNode pre = null;
for (int i = 1; i < left; i++) {
pre = pre == null ? head : pre.next;
}
ListNode end = pre;
ListNode cur = pre == null ? head : pre.next;
ListNode last = cur;
int gap = right - left + 1;
while (gap != 0) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
gap--;
}
if (end != null) end.next = pre;
if (last != null) last.next = cur;
// 不需要换头,返回原先的头节点
return head;
}
}
}
整个过程只遍历一遍链表,时间复杂度 O ( N ) O(N) O(N),空间复杂度 O ( 1 ) O(1) O(1)。
本题也可以利用递归方式解,主函数可以直接定义为递归函数
public ListNode reverseBetween(ListNode head, int left, int right)
该函数表示反转 left
到 right
区间内的链表,并返回反转后的最终链表的头节点。
然后定义一个辅助函数(这个函数也是递归实现)
ListNode reverseN(ListNode head, int n)
该函数表示:反转以 head 为头的链表的前 n 个节点,并返回反转后的链表头节点。
在left == 1
的条件下,主函数直接调用reverseN(head, right)
即可。
即:
public static ListNode reverseBetween(ListNode head, int left, int right) {
if (left == 1) {
// 相当于直接反转前 right 个元素
return reverseN(head, right);
}
// ...
}
接下来是普遍情况:要实现 left
到 right
区间内的链表反转,可以先考虑以head.next
为头的对应区间的链表反转
如下图
然后将head.next
指向反转后的节点即可
接下来是reverseN
方法的实现,这个函数也是递归函数,表示反转以head
为头的前n
个节点,并返回反转后的头节点,当n == 1
时,返回head
,接下来是普遍情况,反转以head.next
为头的前n - 1
个节点,如下图
反转后的链表头节点假设是last
,
这个last
节点就是整个链表反转前n
个元素后的头节点,然后将head
节点进行最后一次反转
head.next.next = head;
最后需要将head
和e
节点连接起来,所以递归过程中,需要一直记录反转的部分链表的后继节点,设置一个全局变量
static ListNode successor = null;
将head
指向successor
节点
完成了前n
个节点的反转,完整代码如下:
// 递归解法
public static ListNode reverseBetween(ListNode head, int left, int right) {
if (left == 1) {
return reverseN(head, right);
}
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
}
// 反转链表前N个节点
public static ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
head.next = successor;
return last;
}
static ListNode successor = null;