前言
上篇文章中,我们已经了解了生成一棵BST的过程,遗留的问题是如何遍历里面的节点。这一篇文章,我们使用递归的方法来解决一下这个问题,顺便探讨与二叉树相关的算法递归问题。
递归函数
什么是递归函数呢?
简单来说,就是自己调用自己,下面是一个栗子:
//累加1 -> 5
function add(nums) {
// 设立递归终止的条件
if(nums === 1) {
return 1;
}
// 返回结果
return nums + add(nums-1);
}
console.log(add(5));
上面这个栗子中,add这个函数里面自己调用了自己,所以这是一个递归。
一个递归函数一般由两部分组成:
- 递归终止的条件
- 循环体
在这个函数中,递归终止的条件是当nums === 1的时候。这个函数的执行过程是这样的:
5 + add(4)
5 + 4 + add (3)
5 + 4 + 3 + add (2)
5 + 4 + 3 + 2 + 1 (注意:当数字为1 的时候终止递归,此时返回1,与前面的数字相加)
5 + 4 + 3 + 3
5 + 4 + 6
5 + 10
15
所以我们在写一个递归函数的过程中,脑子里需要重现的是如何定义好上面两个条件。
下面,我们通过遍历一个二叉树来练练手。
遍历二叉查找树(BST)
上篇文章结尾处给大家解释过遍历一棵二叉查找树有三种方式:前序,中序,后序。这里我用中序遍历(左子树 -> 根节点 -> 右字树)来讲解这个递归的过程。
首先,由定义可知,循环体为 : 遍历左子树 -> 根节点 -> 遍历右字树;
那么,递归终止的条件是什么呢?
这里有一个小窍门,可以通过模拟一棵最简单的二叉树遍历的过程来找到结论。比如下面这棵二叉查找树。
首先它的root节点会先进入我们的函数体,这里我们管这个可以遍历BST的函数体叫inOrder(root),传入进去之后呢,先不管递归终止条件,直接进入循环体,也就是 遍历左子树 -> 根节点 -> 遍历右字树。那么到目前为止,这个函数长这样
function inOrder(node) {
inOrder(node.left);
console.log(node);
inOrder(node.right);
}
在执行完第一行代码inOrder(node.left)之后,函数调用了它自身,此时inOrder传入的参数是23的左节点16,接着又会执行inOrder(node.left),此时16没有左节点,所以inOrder里传入的参数是null。
这时候我们应该知道,既然都没有节点了,那就无需在遍历。
所以停止递归的条件就是节点为null。如果没有设立停止终止的条件,整个程序就会不断的跑,直到内存溢出。加上递归停止的条件之后,这个函数就完成了:
function inOrder(node) {
if(node === null) {
return;
}
inOrder(node.left);
console.log(node);
inOrder(node.right);
}
下面来验证一下,首先要生成一棵二叉树,方法跟上篇文章一样。
var nums = new BST();
nums.insert(23);
nums.insert(45);
nums.insert(16);
nums.insert(37);
nums.insert(3);
nums.insert(99);
nums.insert(22);
完成之后,使用我们的inOrder函数给它排个序
inOrder(nums.root)
顺利的话,你就会在你的浏览器看到,此处给自己一个掌声吧~~~
递归函数一种非常有魅力的函数,而恰巧我们的二叉树就是天然的递归结构,也就是说:
基本上跟二叉树有关的算法都可以用递归来解决,是不是很6!赶紧在多举两个例子来验证一下。
LeetCode 226. Invert Binary Tree
描述:反转一颗二叉树,下面是一个例子:
Example:
Input:
4
/ \
2 7
/ \ / \
1 3 6 9
Output:
4
/ \
7 2
/ \ / \
9 6 3 1
思路:要反转这个树,可以先考虑反转一颗最简单的树。
7
/ \
9 6
要反转这颗树,只要将左右节点交换位置就行了。所以循环体就是交换左右节点的位置。接下来放到这颗有三层的二叉树来看看这个交换的过程。
首先先分别反转左,右子树的节点,那么节点就变为
4
/ \
2 7
/ \ / \
3 1 9 6
接着在root节点里,在去反转左,右两个节点即可。
4
/ \
7 2
/ \ / \
9 6 3 1
由此论证了我们的循环体里,任意一个二叉树要做的事情很简单, 就是交换左右子节点。
终止循环的条件是什么呢?
其实跟上面一样,就是当我们这个节点为空的时候,停止递归,
上述翻译成JS的代码就是
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
if(root === null) {
return;
}
invertTree(root.left); // 先反转左子树的子节点
invertTree(root.right); // 在反转右子树的子节点
// 最后交换当前节点的左右两个子节点
let tmp;
tmp = root.left;
root.left = root.right;
root.right = tmp;
return root;
};
如果还不懂的朋友可以自己在纸上写一下这个计算机程序执行的过程:
一开始JS栈内指令为:
执行inverTree(4.left)之后,又调用inverTree这个函数,此时传入的节点是4的左孩子2,这时候变成
圆心标记已经执行过的语句
再接着调用inverTree这个函数,此时传入的节点是1,
圆心标记已经执行过的语句
在调用inverTree, 此时传入的1的左节点为null。由于节点为null的时候直接返回,所以这个递归函数到这就终止了。接着执行下一句 inverTree(1.right) ,因为1的右节点也为null,所以这里也直接返回。执行交换1的左右节点,由于1没有左右节点,所以最后return 1这个节点回去。
圆心标记已经执行过的语句
后面的步骤大家可以自己画一画,我就不一一说了,最后这个图会变成
圆心标记已经执行过的语句
那到这里root节点的左子树的子节点就反转完成了。右边同理,最后右边返回的节点是
7
/ \
9 6
最后一步就是交换4的左右节点,最终返回
4
/ \
7 2
/ \ / \
9 6 3 1
到这,我们就完成了反转一颗二叉树。
最后做个总结吧,在遇到二叉树的问题,可以用一颗最简单的二叉树,来确定循环体。因为我们写的递归函数就是每一个二叉树都会执行的过程,而每一颗大的二叉树都是由很多的子二叉树来组成的。所以只要子二叉树可以实现目标,那么递归往上就会使得整个树都实现目标。
比如遍历节点的循环体:左子树 -> 根节点 -> 右子树,比如反转二叉树的循环体:反转左子树 -> 反转右子树 ->交换左右节点。
然后再确立一下循环终止的条件,就可以写出一个递归函数。
如果实在想不清楚这个递归函数执行的过程,可以向上图一样模拟一下计算机的执行,相信思路会清晰很多。
觉得文章有帮助的话,点个赞鼓励一下作者吧 ( *・ω・)✄
参考资料:
1、<玩转算法面试>
2、<数据结构与算法 JS描述>