文章目录
前言
写递归算法的关键是要明确方法的「定义」 是什么, 然后相信这个定义, 利⽤这个定义推导最终结果, 绝不要跳⼊递归的细节。 比如,求二叉树一共有几个节点。 只需要关注count(root)方法返回以root为根的树的节点数,不需要关注细节问题(人脑模拟不了几个栈)。这个问题就可以化简为左右子树的节点加上自己的这个节点就可以满足count(root)的定义了,剩下的就是看先序、中序、后序遍历了,1在前面就是先序,中间就是中序,后面就是后续。// 定义: count(root) 返回以 root 为根的树有多少节点
int count(TreeNode root) {
if (root == null) return 0;
return 1 + count(root.left) + count(root.right);// ⾃⼰加上⼦树的节点数就是整棵树的节点数
}
一、应用场景
递归皆可用树的三种遍历来解释,如最常见的快速排序和归并排序,前者是二叉树的前序遍历,后者是二叉树的后序遍历
快速排序代码:
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);
}
归并排序代码:
void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
sort(nums, lo, mid);
sort(nums, mid + 1, hi);
/****** 后序遍历位置 ******/
merge(nums, lo, mid, hi); // 合并两个排好序的⼦数组
/************************/
}
二、模板
二叉树遍历代码:
/* ⼆叉树遍历框架 */
void traverse(TreeNode root) {
// 前序遍历
traverse(root.left)
// 中序遍历
traverse(root.right)
// 后序遍历
}
具体应用时对前序位置|中序位置|后续位置进行微调就可以了
三、实践
226. 翻转二叉树
函数定义:给 invertTree函数输⼊一个节点 root, 将以root为根的树的节点翻转。
1、交换左右子节点
2、翻转左子树和右子树。
注意事项:前序和后续都可以,中序不行。因为先翻转左子树,交换左右子节点,再反转右子树,此时的右子树就是原来的左子树,但是原来的右子树没有被反转到,如上图就会变成4,7,2,1,3,6,9
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
/**** 前序遍历位置 ****/
// root 节点需要交换它的左右⼦节点
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
// 让左右⼦节点继续翻转它们的⼦节点
invertTree(root.left);//翻转左子树
invertTree(root.right);//翻转右子树
return root;
}
116. 填充每个节点的下一个右侧节点指针
函数定义:给 f函数输⼊两个节点 root1,root2, 将传⼊的两个节点连接。
1、root1.next指向root2。
2、连接root1的左右子树,root2的左右子树,root1和root2。
// 主函数
public Node connect(Node root) {
f(root.left, root.right);
return root;
}
// 辅助函数
public void f(Node root1, Node root2) {
if (root1 == null || root2 == null) {
return;
}
/**** 前序遍历位置 ****/
root1.next = root2;// 将传⼊的两个节点连接
// 连接相同⽗节点的两个⼦节点
f(root1.left, root1.right);//连接root1的左右子树
f(root2.left, root2.right);//连接root2的左右子树
// 连接跨越⽗节点的两个⼦节点
f(root1.right, root2.left);//连接root1和root2
}
114. 二叉树展开为链表
函数定义:给 flatten 函数输⼊⼀个节点 root, 那么以 root 为根的⼆叉树就会被拉平为⼀条链表。
1、将 root 的左⼦树和右⼦树拉平。
2、交换root的左右子树, 然后将整个左子树(原先的右子树)接到右子树下,左子树设空。
你说 flatten 函数是怎么把左右子树拉平的? 说不清楚, 但是只要知道 flatten 的定义如此, 相信这个定义, 让 root 做它该做的事情, 然后 flatten 函数就会按照定义⼯作。
另外注意递归框架是后序遍历, 因为我们要先拉平左右⼦树才能进⾏后续操作。
// 定义: 将以 root 为根的树拉平为链表
void flatten(TreeNode root) {
if (root == null) {
return;
}
flatten(root.left);
flatten(root.right);
/**** 后序遍历位置 ****/
// 左右⼦树已经被拉平成⼀条链表
//交换左右子树
TreeNode left = root.left;
root.left = root.right;
root.right = left;
// 找到root的最右的节点
TreeNode head = root;
while (head.right != null) {
head = head.right;
}
head.right = root.left;//将原先的右⼦树接到当前右⼦树的末端
root.left = null;//左子树设空
}
654. 最大二叉树
函数定义:给 build函数输⼊⼀个数组 nums[lo..hi], 构造符合条件的树并返回根节点。
1 、构造root
2、 构造root的左子树和右子树
/* 主函数 */
public TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums, 0, nums.length - 1);
}
/* 将 nums[lo..hi] 构造成符合条件的树, 返回根节点 */
public TreeNode build(int[] nums, int left, int right) {
if (left > right) {
return null;
}
// 找到数组中的最⼤值和对应的索引
int index = 0, max = Integer.MIN_VALUE;
for (int i = left; i <= right; i++) {
if (max < nums[i]) {
max = nums[i];
index = i;
}
}
TreeNode root = new TreeNode(max);//构造root
// 递归调⽤构造左右⼦树
root.left = build(nums, left, index - 1);
root.right = build(nums, index + 1, right);
return root;
}
总结
把题⽬的要求细化, 搞清楚根节点应该做什么, 然后剩下的事情抛给前/中/后序的遍历框架就⾏了, 千万不要跳进递归的细节⾥, 你的脑袋才能压⼏个栈呀。递归算法的关键要明确函数的定义, 相信这个定义, ⽽不要跳进递归细节。
写⼆叉树的算法题, 都是基于递归框架的, 我们先要搞清楚 root 节点它⾃⼰要做什么, 然后根据题⽬要求 选择使⽤前序, 中序, 后续的递归框架。⼆叉树题⽬的难点在于如何通过题⽬的要求思考出每⼀个节点需要做什么, 这个只能通过多刷题进⾏练习了。