力扣:92.反转链表II(中等)
参考:labuladong
题目:反转单链表的一部分,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变:
答案:
ListNode reverseBetween(ListNode head, int m, int n) {
// base case 如果 m == 1,就相当于反转链表开头的 n 个元素
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n - 1); // 以 head.next 为起点,反转前 n - 1 个节点
head.next.next = head; // head.next 节点指向 head (此时1,2相互指向)
head.next = successor; // head 节点指向后驱节点
return last;
}
赶时间的朋友直接复制走,力扣能够通过,下面开始是解析。
通过递归实现,就需要由浅入深,先从反转整个单链表说起。
一、递归反转整个链表
ListNode reverse(ListNode head) {
if (head.next == null) return head; // 只剩头结点,直接返回
ListNode last = reverse(head.next); // 将 head.next 进行反转,last 接收反转后的头结点
head.next.next = head; // head 放到 head.next 的后面
head.next = null; // head 变成尾结点,末尾 null
return last; // last 作为头结点返回
}
代码解释:
输入一个节点 head
,将「以 head
为起点」的链表反转,并返回反转之后的头结点。
比如说我们想反转这个链表:
这个 reverse(head.next)
执行完成后,整个链表就成了这样:(1,3均指向2)
并且根据函数定义,reverse
函数会返回反转之后的头结点,我们用变量 last
接收了。
接下来:head.next(2) 指向 head(1)
head.next(2) 从2改到指向 null
-
base case:
if (head.next == null) return head;
意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。
-
当链表递归反转之后,新的头结点是
last
,而之前的head
变成了最后一个节点,别忘了链表的末尾要指向 null:head.next = null;
二、反转前N个节点
比如说对于下图链表,执行 reverseN(head, 3)
:
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n - 1); // 以 head.next 为起点,反转前 n - 1 个节点
head.next.next = head; // head.next 节点指向 head (此时1,2相互指向)
head.next = successor; // head 节点指向后驱节点
return last;
}
举例:
- 原链表:1->2->3->4->5->6
- 指定 head.next 为起点(2->3…)的反转已经完成,即:last 指向 3->2->4->5->6
- head:1->2…(其他节点按last顺序,1,3都指向2),head.next 节点(2)指向 head(1) (此时1,2相互指向)
- head 节点(1)指向后驱节点(4->5->6)
- 最终:3->2->1->4->5->6
具体的区别:
1、base case 变为 n == 1
,反转一个元素,就是它本身,同时要记录后驱节点。
2、刚才我们直接把 head.next
设置为 null,因为整个链表反转后原来的 head
变成了整个链表的最后一个节点。但现在 head
节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor
(第 n + 1 个节点),反转之后将 head
连接上。
三、反转链表的一部分
ListNode reverseBetween(ListNode head, int m, int n) {
// base case 如果 m == 1,就相当于反转链表开头的 n 个元素
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
四、最后总结
递归的思想:不要跳进递归,而是利用明确的定义来实现算法逻辑。
处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。