该部分为重点内容。 建议多刷几遍 中序遍历的迭代法和最近公共祖先。
树结构
-
满二叉树:一个二叉树的所有非叶子节点都存在左右子节点,并且所有叶子节点都在同一层级上。(所以节点都是满的)
-
完全二叉树:相比满二叉树,完全二叉树的最后一层的叶子节点可以不满,并且如果减少就只能按照从右往左的顺序减少。
-
二叉搜索/查找树:左子树节点值都小于根节点,右子树节点值都大于根节点,并且左右子树也都为二叉搜索树。(没有相同值)
-
平衡二叉树:(1)要么是空树;(2)要么左右子树的高度之差不大于1;(3)子树也都是平衡二叉树。
-
红黑树:也叫自平衡二叉搜索树,综合了平衡二叉树和二叉搜索树的性质。
递归终止条件:当参数为xx时,递归结束,返回结果。
找出函数的等价关系式:通过一些辅助的变量或操作不断缩小参数的范围,并且保证原函数的结果不变。
路径之和问题
112. 路径总和:分三种情况讨论
分别判断 空节点、叶子节点和其他节点 这三种情况。
const hasPathSum = function(root, targetSum) {
// 遍历到空节点
if (!root) return false;
// 遍历到叶子节点,判断值相等
if (!root.left && !root.right) return root.val == targetSum;
// 用 ||,只要有一个满足即可返回 true!!!
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
时间复杂度:O(n),其中 n 是树的节点数,对每个节点访问一次。
空间复杂度:O(H), 其中H是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况树呈现链状,此时空间复杂度O(n),平均情况下树的高度与节点数的对数正相关,此时空间复杂度O(logn)。
129. 求根节点到叶节点数字之和:分三种情况 + preSum
- 分别判断 空节点、叶子节点和其他节点 三种情况
- 注意函数参数要加一个 preSum。
const dfs = function(root, preSum = 0) { // 注意
if (!root) return 0; // 空节点
preSum = preSum * 10 + root.val;
if (!root.left && !root.right) return preSum;
return dfs(root.left, preSum) + dfs(root.right, preSum);
}
时间复杂度:O(n),其中 n 是二叉树的节点个数。
空间复杂度:O(H)。
二叉树的前中后序遍历:递归、迭代
- 前序遍历顺序:根节点 -> 左子树 -> 右子树
- 中序遍历顺序:左子树 -> 根节点 -> 右子树
- 后序遍历顺序:左子树 -> 右子树 -> 根节点
遍历的顺序可以记为根节点的遍历位置。
1. 递归:借助三元运算符
// 前序遍历
const preorder = function(root) {
return root ? [root.val, ...preorder(root.left), ...preorder(root.right)] : [];
}
// 中序遍历
const inorder = function(root) {
return root ? [...inorder(root.left), root.val, ...inorder(root.right)] : [];
}
// 后序遍历
const postorder = function(root) {
return root ? [...postorder(root.left), ...postorder(root.right), root.val] : [];
}
时间复杂度:O(n), 其中n是二叉树的节点数。
空间复杂度:O(H),H是树的高度。
2. 迭代:root & stack.length
由于递归解法较简单,在面试时通常会让你使用非递归的算法,这就需要借助 栈 来实现。「递归的代替就是 栈 + 循环」
对于前序遍历,我们先让父节点进栈再出栈,然后根据栈 「后进先出」 的性质,先让root.right入栈、再让root.left入栈。
const preorder = function(root) {
if (!root) return [];
const res = []; // 遍历结果
const stack = [root]; // 栈,模拟递归调用
while (stack.length > 0) { // 关注 stack.length
let node = stack.pop();
res.push(node.val);
node.right && stack.push(node.right); // 注意 1.判断; 2. node,不是root
node.left && stack.push(node.left);
}
return res;
}
「那么后序遍历呢?」
前序遍历是中左右,后序遍历是左右中,根据前序遍历的迭代解法,我们将左右子节点的入栈顺序调换,使得输出顺序为 中右左,然后我们返回倒序的数组使得顺序为 左右中。
- 改变前序遍历的左右子树入栈顺序;
reverse颠倒顺序。
const postorder = function(root) {
if (!root) return [];
const res = [];
const stack = [root];
while (stack.length > 0) {
let node = stack.pop();
res.push(node.val);
node.left && stack.push(node.left);
node.right && stack.push(node.right);
}
return res.reverse(); // 唯一区别点
};
「那么中序遍历呢? - 中序迭代最容易考到」
中序遍历的顺序是左中右,和前后序的迭代遍历思路不一样。多记多熟悉。
const inorder = function(root) {
const res = [];
const stack = [];
// 思路随着 最左侧叶子节点 走
while (root || stack.length > 0) { // 注意
if (root) { // 将根节点和所有左子节点都入栈,从最左侧的叶子节点开始
stack.push(root);
root = root.left;
} else {
root = stack.pop(); // root 为空就给 root 重新赋值
res.push(root.val);
root = root.right; // 右节点入栈
}
}
return res;
};
230. 二叉搜索树中第K小的元素:中序遍历迭代法
根据二叉搜索树的性质,中序遍历得到的是升序数组 ,因此可以借助中序遍历的迭代法,在找到答案后就提前返回,不需要遍历整棵树。
const kthSmallest = function(root, k) {
const stack = [];
while (root || stack.length > 0) {
if (root) {
stack.push(root);
root = root.left;
} else {
root = stack.pop();
k--;
if (k == 0) return root.val;
root = root.right;
}
}
}
236. 二叉树的最近公共祖先:分三种情况讨论
该题较典型,大公司考察比较多,多刷几遍。
最近公共祖先指的是同时为两个节点的祖先,且深度尽可能大!
「最近公共祖先的情况有3种」
root是p、q中的一个,这时公共祖先就是root。p、q分别在root的左右子树上,这时公共祖先也只有root本身。p、q同时在root的左子树,则公共祖先就是递归得到的左子树结果;同理当都在右子树,那么公共祖先就是递归得到的右子树结果。
const common = function(root, p, q) {
if (!root) return null; // 递归终止条件
if (root == p || root == q) return root; // 第1种情况
// 递归遍历,判断 p 和 q 在两侧子树还是都在一侧
let left = common(root.left, p, q);
let right = common(root.right, p, q);
if (left && right) return root;
return left ? left : right; // 有一侧为空的情况
}
226.翻转二叉树
递归反转即可。
const invertTree = function(root) {
if (!root) return null;
[root.left, root.right] = [root.right, root.left];
invertTree(root.left);
invertTree(root.right);
return root;
};
时间复杂度:O(n)。
根据两个顺序构造二叉树
首先,对二叉树的3种遍历顺序进行总结:
「前序遍历」:[根节点, [左子树的前序遍历结果], [右子树的前序遍历结果]]。
「中序遍历」:[[左子树的中序遍历结果], 根节点, [右子树的中序遍历结果]]。
「后序遍历」:[[左子树的后序遍历结果], [右子树的后序遍历结果], 根节点]。
然后我们来看看构造的思路,
「前序 + 中序 => 二叉树」: 以前序遍历的第一个元素[根节点] 为切割点,由于题意给出二叉树没有重复元素,可以在中序数组中用indexOf()定位出根节点索引。然后我们就知道左子树和右子树的节点数目、知道左右子树的前序和中序遍历结果,递归构造根节点即可。
「中序 + 后序 => 二叉树」: 以后序遍历的最后一个元素为切割点,其他同上。
注意,前序和后序不能构造唯一的二叉树,因为无法计算出左右子树的节点数目。
105. 从前序和中序遍历序列构造二叉树
const buildTree = function(preorder, inorder) {
if (preorder.length == 0) return null; // 递归终止条件
let root = new TreeNode(preorder[0]); // 创建节点
let index = inorder.indexOf(root.val);
root.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index));
root.right = buildTree(preorder.slice(index + 1),inorder.slice(index + 1));
return root; // 注意
};
106. 从中序和后序遍历序列构造二叉树
const buildTree = function(inorder, postorder) {
if (inorder.length == 0) return null;
let root = new TreeNode(postorder[postorder.length - 1]);
let index = inorder.indexOf(root.val);
root.left = buildTree(inorder.slice(0, index), postorder.slice(0, index));
root.right = buildTree(inorder.slice(index + 1), postorder.slice(index, -1)); // slice(,-1)
return root;
};
最大深度问题
104. 二叉树的最大深度
最大深度是根节点到最远叶子节点的最长路径上的节点数。
递归法较为常用,迭代法也建议掌握。
1. 递归
const maxDepth = function(root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
时间复杂度:O(n)。
空间复杂度:O(H)。 H是二叉树的高度,递归需要栈空间,而栈空间取决于递归的深度。
2. 迭代
- 根节点入队
- 队头出队列,然后左右子节点入队
- 当前这一层的所有节点都出队后,再对下一层重复该操作
通过一个 while 循环控制从上向下一层层遍历, for 循环控制每一层从左向右遍历
// 广度优先搜索
const maxDepth = function(root) {
if (!root) return 0;
let res = 0; // 最大深度
const stack = [root];
while (stack.length > 0) {
// 必须要有 n,因为下一层循环要把上一层的节点全部 pop
let n = stack.length;
for (let i = 0; i < n; i++) {
let node = stack.shift(); // 注意shift
node.left && stack.push(node.left);
node.right && stack.push(node.right);
}
res++;
}
return res;
}
110.平衡二叉树
「自顶向下地递归」: 借助求二叉树最大深度的函数,递归比较每个节点的左右子树的最大高度差。
const maxDepth = function(root) { // 求二叉树最大深度
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
const isBalanced = function(root) {
if (!root) return true; // 空树也是平衡二叉树
// 当高度差小于等于1时,继续递归左右子节点
return Math.abs(maxDepth(root.left) - maxDepth(root.right)) <= 1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
时间复杂度:O(n^2)。
空间复杂度:O(H)。 主要取决于递归调用的层数。
543. 二叉树的直径
分析:
- 假设一条路径上有
N个节点,那么它的长度就是N - 1。 - 对二叉树的任一节点,以它为根节点,假设我们知道它的左子树向下遍历经过的最多节点数是
L(也就是深度)、右子树向下遍历经过的最多节点数是R,那么 「经过该节点的路径上最多包含的节点数」 是L + R + 1,「路径长度」 是L + R。 - 二叉树的直径就是 「以所有节点为根、求得的路径长度中最大的那个」。
思路: 我们借助求二叉树最大深度的函数,对二叉树递归遍历、更新最大值。
注意: 「二叉树的直径」 等于max(root.left的直径, root.right的直径, root.left的最大深度 + root.right的最大深度),因为最长的路径不一定经过根节点root,所以左右子树也要写上。
const maxDepth = function(root) { // 求最大深度,即经过的节点数
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
const diam = function(root) {
if (!root) return 0;
let height = maxDepth(root.left) + maxDepth(root.right); // 对左右子节点求最大深度,路径长度 L + R
return Math.max(height, diam(root.left), diam(root.right));
}