前言
- 二叉树是算法的基础,很多经典算法(回溯算法、动态规划、分治算法)其实都是树的问题,而树的问题就永远逃不开树的递归遍历框架这几行破代码:
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
// 前序遍历
traverse(root.left)
// 中序遍历
traverse(root.right)
// 后序遍历
}
二叉树的重要性
- 快速排序就是个二叉树的前序遍历
- 归并排序就是个二叉树的后序遍历
快速排序的逻辑
- 若要对
nums[lo..hi]
进行排序,我们先找一个分界点p
,通过交换元素使得nums[lo..p-1]
都小于等于nums[p]
,且nums[p+1..hi]
都大于nums[p]
,然后递归地去nums[lo..p-1]
和nums[p+1..hi]
中寻找新的分界点,最后整个数组就被排序了。
void sort(int[] nums,int lo,int hi){
/**前序遍历位置**/
// 通过交换元素构建分界点p
int p = partition(nums,lo,hi);
/**************/
sort(nums,lo,p-1);
sort(nums,p+1,hi);
}
- 先构造分界点,然后去左右子数组构造分界点,本质就是一个二叉树的前序遍历
归并排序的逻辑
- 若要对
nums[lo..hi]
进行排序,我们先对nums[lo..mid]
排序,再对nums[mid+1..hi]
排序,最后把这两个有序的子数组合并,整个数组就排好序了。
void sort(int []nums,int lo,int hi){
int mid = (lo+hi)/2;
sort(nums,li,mid);
sort(nums,mid+1,hi);
/**后序遍历位置**/
// 合并两个排好序的子数组
merge(num,lo,mid,hi);
/**************/
}
- 左右子数组排序,然后合并(类似合并有序链表的逻辑),其实就类似二叉树的后序遍历框架,也可以启发分治算法
递归算法的秘诀
-
写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节。
-
例如计算二叉树有多少个节点
int count(TreeNode root){
if(root == null){
return 0;
}
return 1 + count(root.left)+count(root.right);
}
226.翻转二叉树(简单)
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
-
这道题目比较简单,关键思路在于我们发现翻转整棵树就是交换每个节点的左右子节点,于是我们把交换左右子节点的代码放在了前序遍历的位置。值得一提的是,如果把交换左右子节点的代码放在后序遍历的位置也是可以的.
-
首先讲这道题目是想告诉你,二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情。
114.二叉树展开为链表(中等)
void flatten(TreeNode root) {
// base case
if (root == null) return;
flatten(root.left);
flatten(root.right);
/**** 后序遍历位置 ****/
// 1、左右子树已经被拉平成一条链表
TreeNode left = root.left;
TreeNode right = root.right;
// 2、将左子树作为右子树
root.left = null;
root.right = left;
// 3、将原先的右子树接到当前右子树的末端
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
- 这就是递归的魅力,你说
flatten
函数是怎么把左右子树拉平的?说不清楚,但是只要知道flatten
的定义如此,相信这个定义,让root
做它该做的事情,然后flatten
函数就会按照定义工作。另外注意递归框架是后序遍历,因为我们要先拉平左右子树才能进行后续操作。
116.填充每个节点的下一个右侧节点指针(中等)
// 主函数
Node connect(Node root) {
if (root == null) return null;
connectTwoNode(root.left, root.right);
return root;
}
// 辅助函数
void connectTwoNode(Node node1, Node node2) {
if (node1 == null || node2 == null) {
return;
}
/**** 前序遍历位置 ****/
// 将传入的两个节点连接
node1.next = node2;
// 连接相同父节点的两个子节点
connectTwoNode(node1.left, node1.right);
connectTwoNode(node2.left, node2.right);
// 连接跨越父节点的两个子节点
connectTwoNode(node1.right, node2.left);
}
- 要注意跨越父节点的两个子节点也要连接起来
总结
-
递归算法的关键要明确函数的定义,相信这个定义,而不要跳进递归细节。
-
写二叉树的算法题,都是基于递归框架的,我们先要搞清楚
root
节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。