1递归的反转整个链表
对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的reverse
函数定义是这样的:
输入一个节点head,将「以head为起点」的链表反转,并返回反转之后的头结点。
明白了函数的定义,再来看这个问题。比如说我们想反转这个链表:
那么输入reverse(head)后,会在这里进行递归:不要跳进递归,而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:
按照定义,这个reverse(head.next)
执行完成后,整个链表应该变成了这样:
并且根据函数定义,reverse函数会返回反转之后的头结点,我们用变量last接收了:
head.next.next = head;
head.next = null;
return last;
递归函数要有 base case,也就是这句:
if (head.next == null) return head;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
2递归反转链表前 N 个节点
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n - 1 个节点
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
base case 变为n == 1,反转一个元素,就是它本身,同时要记录后驱节点。
刚才我们直接把head.next
设置为 null
,因为整个链表反转后原来的head
变成了整个链表的最后一个节点。但现在head节点在递归反转之后不一定是最后一个节点了,所以要记录后驱successor(第 n + 1 个节点),反转之后将head连接上。
3递归的反转链表的一部分
ListNode reverseBetween(ListNode head, int m, int n)
首先,如果m == 1
,就相当于反转链表开头的n
个元素嘛,也就是我们刚才实现的功能:
ListNode reverseBetween(ListNode head, int m, int n) {
// base case
if (m == 1) {
// 相当于反转前 n 个元素
return reverseN(head, n);
}
// ...
}
如果m != 1
怎么办?如果我们把head的索引视为 1,那么我们是想从第m个元素开始反转对吧;如果把head.next的索引视为 1 呢?那么相对于head.next,反转的区间应该是从第m - 1个元素开始的;那么对于head.next.next呢……
区别于迭代思想,这就是递归思想,所以我们可以完成代码:
ListNode reverseBetween(ListNode head, int m, int n) {
// base case
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
4递归k 个一组反转链表
reverseKGroup(head, 2)
,即以 2 个节点为一组反转链表:
如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫子问题。
我们可以直接递归调用 reverseKGroup(head, 2)
,因为子问题和原问题的结构完全相同,这就是所谓的递归性质。发现了递归性质,就可以得到大致的算法流程:
1、先反转以 head 开头的 k 个元素:
2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数:
3、将上述两个过程的结果连接起来:
如果最后的元素不足 k 个,就保持不变。这就是 base case。
反转整个链表:
// 反转以 a 为头结点的链表
ListNode reverse(ListNode a) {
ListNode pre, cur, nxt;
pre = null; cur = a; nxt = a;
while (cur != null) {
nxt = cur.next;
// 逐个结点反转
cur.next = pre;
// 更新指针位置
pre = cur;
cur = nxt;
}
// 返回反转后的头结点
return pre;
}
「反转以 a 为头结点的链表」其实就是「反转 a 到 null 之间的结点」,那么如果让你「反转 a 到 b 之间的结点」:
/** 反转区间 [a, b) 的元素,注意是左闭右开 */
ListNode reverse(ListNode a, ListNode b) {
ListNode pre, cur, nxt;
pre = null; cur = a; nxt = a;
// while 终止的条件改一下就行了
while (cur != b) {
nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
// 返回反转后的头结点
return pre;
}
现在我们迭代实现了反转部分链表的功能,接下来就按照之前的逻辑编写 reverseKGroup 函数即可:
ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return null;
// 区间 [a, b) 包含 k 个待反转元素
ListNode a, b;
a = b = head;
for (int i = 0; i < k; i++) {
// 不足 k 个,不需要反转,base case
if (b == null) return head;
b = b.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(a, b);
// 递归反转后续链表并连接起来
a.next = reverseKGroup(b, k);
return newHead;
}
解释一下 for 循环之后的几句代码,注意 reverse 函数是反转区间 [a, b),所以情形是这样的: