今天,借反转链表这个题目来分享一下写递归代码的心得。
我们人的思维很喜欢顺着思考,比如在计算 5 的阶乘的时候,我们很自然的会把他拆分成:
1 * 2 * 3 * 4 * 5
计算机的思维方式和我们人类不同,或许下面的这种写法对它来说更容易理解:
function factorial(n) {
if (n === 1) {
return 1
}
return n * factorial(n-1)
}
从上面那个例子可以看出,对待一个问题,我们人类更倾向于使用递推的思路,这种思路没问题,合情合理。
但正因为它的合情合理,导致了我们难以理解计算机中的递归,因为我们总想用递推的思路来理解递归。
正确的思路应该是什么样的呢?(不保证绝对正确,只是对于我自己来说的)
我们以反转链表为例来演示一下。
PS: 反转链表的意思就是,假设我们有一个形如 1->2->3->2->4
的链表,我们想把它反转过来,变为:
4->2->3->2->1
。
写递归时,首先要考虑的是递归到底的情况
在此就是访问到最后一个节点或者链表为空的时候,这个正是我们新链表的头结点,我们返回。
if (head === null || head.next == null) {
return head;
}
剩下的就是从宏观的角度考虑第 n 个和第 n-1 个
从宏观的角度来看,我们的思路是先反转好后 n-1 个节点,如果后 n-1 个节点都反转好了,轮到倒数第 n 个节点的时候,只需要把第 n 个节点也反转一下就好了
const ret = reverseList(head.next); // 反转 n - 1 并返回 head
const nextNode = head.next; // 下面三行是反转当前节点
nextNode.next = head;
head.next = null;
从微观的角度验证我们写的对不对
到了这里,我们的递归代码就基本写出来了,但是我们还是要验证写的对不对,思路有没有问题,这时候,我们使用人类惯用的递推思路,给我们的递归代码带入一个小数据规模的例子测试一下。
在这里,我们假设我们的链表是 1-> 2 -> 3
此时的调用顺序就是:
// 伪代码,理解意思就好!
reverseList(1)
reverseList(2)
reverseList(3) // 最后一个节点了,直接返回 3
反转节点 2 并返回节点 3
反转节点 1 并返回节点 3
通过微观角度分析,发现我们的思路没问题,此时一段递归代码就算写完了。
总结起来就是下面这段代码:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
if (head === null || head.next == null) {
return head;
}
const ret = reverseList(head.next);
const nextNode = head.next;
nextNode.next = head;
head.next = null;
return ret;
};
总结一下,在写递归的时候,不要试图使用递推的思路去理解它,要先想好递归到底的情况,接下来分析第 n 次和第 n-1 次如何去写,最后使用微观角度去验证一下我们的思路。这样大部分递归代码都能轻松写出来了!