平衡二叉树
题目
版本1 错误
public boolean isBalanced(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 判断一棵树是不是平衡二叉树
// 即左右子树分别是不是平衡的
if (root.left == null) {
if (root.right != null && (root.right.left != null || root.right.right != null)) {
return Boolean.FALSE;
}
}
if (root.right == null) {
if (root.left != null && (root.left.left != null || root.left.right != null)) {
return Boolean.FALSE;
}
}
boolean left = isBalanced(root.left);
boolean right = isBalanced(root.right);
return left && right;
}
错误的原因
(1) 一棵二叉树, 对于每一个root节点, 左右子树都平衡, 并不代表整棵二叉树是平衡的, 例如下面这种情况
版本2 错误
public boolean isBalanced(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 判断一棵树是不是平衡二叉树
// 求出左右子树的高度即可
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
return Math.abs(leftDeep - rightDeep) <= 1;
}
public int getDeep(TreeNode root) {
if (root == null) {
return 0;
}
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
return 1 + Math.max(leftDeep, rightDeep);
}
错误的原因
(1) 只考虑了root节点左右两个子树的最大深度是否满足要求, 但是没有考虑单独一棵子树的话, 里面的所有分叉是否满足要求
(2) getDeep函数中, 在后序遍历的位置, 每次都应该进行一次判断
版本3 正确
public boolean isBalanced(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 判断一棵树是不是平衡二叉树
// 求出左右子树的高度即可
if (root.left == null) {
if (root.right != null && (root.right.left != null || root.right.right != null)) {
return Boolean.FALSE;
}
}
if (root.right == null) {
if (root.left != null && (root.left.left != null || root.left.right != null)) {
return Boolean.FALSE;
}
}
boolean left = isBalanced(root.left);
boolean right = isBalanced(root.right);
if (left && right) {
// 如果左右两个子树是平衡的, 再校验一次高度
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
return Math.abs(leftDeep - rightDeep) <= 1;
}
return left && right;
}
public int getDeep(TreeNode root) {
if (root == null) {
return 0;
}
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
return 1 + Math.max(leftDeep, rightDeep);
}
不足的地方
(1) 想的太复杂了, 前序遍历的位置, if (root.left == null)的判断, 其实就是想说root.left是不是一个平衡的二叉树嘛, 那不就是boolean left = isBalanced(root.left);应该做的事情嘛? 为啥还要再提前判断一下
(2) 后序遍历那里if (left && right) {}, 是想说在二叉树左右都平衡的时候, 判断一次两边的高度差, 杜绝版本1那种错误, 但是这种方式会造成大量重复求解高度的情况.对于每一个root, 都要求一下两边子树的高度.
版本4 正确
public boolean isBalanced(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 判断一棵树是不是平衡二叉树
// 求出左右子树的高度即可
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
if (leftDeep == -1 || rightDeep == -1) {
return Boolean.FALSE;
}
return Math.abs(leftDeep - rightDeep) <= 1;
}
public int getDeep(TreeNode root) {
if (root == null) {
return 0;
}
int leftDeep = getDeep(root.left);
int rightDeep = getDeep(root.right);
if (leftDeep == -1 || rightDeep == -1) {
return -1;
}
if (Math.abs(leftDeep - rightDeep) <= 1) {
return 1 + Math.max(leftDeep, rightDeep);
}
return -1;
}
正确的原因
(1) 在版本2的基础上, 增加减枝的过程, 只需要对最根部的root求一次左右的高度, 如果求解的过程中, 一直平衡, 就正常返回高度值, 如果不平衡了, 就返回-1, 因此只用求解一次高度, 就得到了结果
二叉树的高度
题目
版本1 正确
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 树的最大深度为左右子节点深度的最大值加1
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
对称的二叉树
题目
版本1 正确
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 对称二叉树, 只能层级遍历, 判断每一层的数据是否是对称的
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root.left);
queue.offer(root.right);
while (!queue.isEmpty()) {
// 每一层数据的个数, 因为会填充null, 所以元素的个数一定为偶数个
int size = queue.size();
int [] temp = new int[size / 2];
for (int i = 0; i < size; i ++) {
TreeNode node = queue.poll();
if (i < size / 2) {
// 往temp中添加元素
temp[i] = node == null ? -1 : node.val;
} else {
// 比较元素是否是镜像
if (temp[size - i - 1] != (node == null ? -1 : node.val)) {
return Boolean.FALSE;
}
}
// 如果node不为null, 添加下一层的元素
if (node != null) {
queue.offer(node.left);
queue.offer(node.right);
}
}
}
return Boolean.TRUE;
}
不足的地方
(1) 层级遍历, 每一层再判断元素是否是镜像的.
版本2 正确
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
return isSymmetric(root.left, root.right);
}
public boolean isSymmetric(TreeNode left, TreeNode right) {
// 二叉树中, 镜像指的都是两个节点
if (left == null && right == null) {
return Boolean.TRUE;
}
// 如果一个为null, 一个不为null
// 或者两个都不为null, 但是值不相同
if (left == null || right == null || left.val != right.val) {
return Boolean.FALSE;
}
// 寻找下一对对称的节点
// 两个节点会产生两对对称的节点
boolean result1 = isSymmetric(left.left, right.right);
boolean result2 = isSymmetric(left.right, right.left);
return result1 && result2;
}
正确的思路
(1) 对称是针对两个节点的, 因此每次只需要判断两个节点是否是对称的即可
(2) 每两个节点, 会产生出两对新的对称节点, 递归判断即可.
二叉树的镜像
题目
版本1 错误
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
// 对于每一棵二叉树, 交换左右节点
root.left = mirrorTree(root.right);
root.right = mirrorTree(root.left);
return root;
}
错误的原因
(1) 第一个递归就修改了root.left的值了, 第二个递归的时候, 参数就不对了.
版本2 正确
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
// 对于每一棵二叉树, 交换左右节点
TreeNode left = root.left;
TreeNode right = root.right;
root.left = mirrorTree(right);
root.right = mirrorTree(left);
return root;
}
正确的原因
(1) 提前记录下来左右节点, 再作为递归的参数条件.
二叉树的最近公共祖先
题目
版本1 正确
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return root;
}
// 寻找公共祖先
// 遇见目标节点了, 就返回目标节点, 此时返回值才不为null
if (root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 当left和right都不为null的时候, 当前的root就是最近的公共祖先
if (left != null && right != null) {
return root;
}
// 如果有其中一个为null, 就将不为null的节点一直返回
// 如果两个都为null, 就随便返回个null
return left == null ? right : left;
}
正确的原因
(1) 遇见目标节点了才开始返回非null的值, 并且每次返回如果存在非null的值, 返回非null的
(2) 当left和right第一次非null的时候, 此时的root就是最近公共祖先了.
从上到下打印二叉树2
题目
版本1 错误
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> ans = new ArrayList<>();
// BFS 层级遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> temp = new ArrayList<>();
for(int i = 0; i < size; i ++) {
TreeNode node = queue.poll();
if (node != null) {
temp.add(node.val);
queue.offer(node.left);
queue.offer(node.right);
}
}
ans.add(temp);
}
return ans;
}
错误的原因
(1) 每次往队列添加元素的时候, 没有考虑是否为null, 然后在使用的时候判断, 这种方式呢, 看起来没啥错误, 但是如果某一层都是null, 此时就会生成一个空temp, 如下:
版本2 正确
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> ans = new ArrayList<>();
// BFS 层级遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> temp = new ArrayList<>();
for(int i = 0; i < size; i ++) {
TreeNode node = queue.poll();
temp.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
ans.add(temp);
}
return ans;
}
正确的原因
(1) 在往队列添加元素的时候, 就判断所添加的元素是否为null
二叉搜索树的第K大节点
题目
版本1 错误
TreeNode ans = null;
public int kthLargest(TreeNode root, int k) {
if (root == null) {
return 0;
}
// 在二叉搜索树中, 寻找第K大的节点. 返回最大节点的val值
// 按照右 根 左的形式遍历, 返回非null的元素个数
int right = kthLargest(root.right, k);
if (right == k && ans == null) {
ans = root.right;
}
if (right == k - 1 && ans == null) {
ans = root;
}
int left = kthLargest(root.left, k);
if (left + right + 1 == k && ans == null) {
ans = root.left;
}
if (ans != null) {
return ans.val;
}
return left + right + 1;
}
错误的原因
(1) 只能正确处理一边子树的数量, 如果要寻找的目标在root的右边子树上, 是可以的, 但是如果在左边子树上, 数量就计算的不对.
版本2 错误
TreeNode ans = null;
public int kthLargest(TreeNode root, int k) {
digui(root, k);
return ans.val;
}
public void digui(TreeNode root, int k) {
if (root == null) {
return;
}
digui(root.right, k);
// 中序遍历, 就是在每个非null的节点处做什么
if (k == 0) {
return;
}
k --;
if (k == 0) {
ans = root;
}
digui(root.left, k);
}
错误的原因
(1) k这里不作为全局变量的话, 每次递归返回的时候, k的值会恢复啊 !!!! 那还计数个p.
版本3 正确
TreeNode ans = null;
int k;
public int kthLargest(TreeNode root, int k) {
this.k = k;
digui(root);
return ans.val;
}
public void digui(TreeNode root) {
if (root == null) {
return;
}
digui(root.right);
// 中序遍历, 就是在每个非null的节点处做什么
if (k == 0) {
return;
}
k --;
if (k == 0) {
ans = root;
}
digui(root.left);
}
正确的原因
(1) 明确中序遍历的含义, 以及什么时候该用全局变量
二叉搜索树的最近公共祖先
题目
版本1 正确
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return null;
}
// 二叉搜索树在寻找公共祖先
// 与普通二叉树寻找公共祖先相比, 寻找的时候, 只需要寻找一边了
// 如果p, q分散在root两边
if (p.val >= root.val && q.val <= root.val || p.val <= root.val && q.val >= root.val) {
return root;
}
if (p.val < root.val) {
// p和q都分散在左边
return lowestCommonAncestor(root.left, p, q);
} else {
return lowestCommonAncestor(root.right, p, q);
}
}
正确的原因
(1) 利用二叉搜索树的特点, 判断p.val, root.val和q.val三个值的关系, 可以迅速得到公共祖先.
合并二叉树
题目
版本1 正确
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
// 类似判断二叉树是否是镜像, 寻找对称的节点相加元素即可
// 例如这里将root1叠加到root2上
if (root1 == null) {
return root2;
}
if (root2 == null) {
return root1;
}
root2.val = root1.val + root2.val;
// 会产生两对 对称节点
root2.left = mergeTrees(root1.left, root2.left);
root2.right = mergeTrees(root1.right, root2.right);
return root2;
}
正确的原因
(1) 明确对称元素值相加的含义
单值二叉树
题目
版本1 正确
public boolean isUnivalTree(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
boolean left = Boolean.TRUE;
if (root.left != null) {
if (root.val == root.left.val) {
left = isUnivalTree(root.left);
} else {
return Boolean.FALSE;
}
}
boolean right = Boolean.TRUE;
if (root.right != null) {
if (root.val == root.right.val) {
right = isUnivalTree(root.right);
} else {
return Boolean.FALSE;
}
}
return left && right;
}
二叉树的直径
题目
版本1 正确
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
// 二叉树的直径, 是任意两个节点之间的最大长度
// 一定是两个叶子节点之间的长度是最大值
// 任意两个子叶节点, 往上层一定有一个交点A
// 交点为A可能对应多组子叶节点, 例如(B, D), (C, E)...
// 交点为A对应的多组子叶节点, 构成的直径最大值, 就是A左子树的最大高度 + A右子树的最大高度 + 1
// 因此对于每个节点, 计算 leftMax + rightMax + 1, 不断更新最大值, 遍历完所有节点就得到结果
getDeep(root);
return ans;
}
public int getDeep(TreeNode root) {
if (root == null) {
return 0;
}
int lfet = getDeep(root.left);
int right = getDeep(root.right);
ans = Math.max(ans, lfet + right);
return Math.max(lfet, right) + 1;
}
正确的原因
(1) 问题转化成求每个节点左子树的最大高度和右子树最大高度和的最大值.
重建二叉树
题目
版本1 正确 数组拷贝版本
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0) {
return null;
}
// 前序的第一个元素就是root
TreeNode root = new TreeNode(preorder[0]);
// 在中序中找到root所在的位置
int index = 0;
for (int i = 0; i < inorder.length; i ++) {
if (inorder[i] == preorder[0]) {
index = i;
break;
}
}
// Arrays.copyOfRange(array, form, to) 包括下标from, 不包括下标to
root.left = buildTree(Arrays.copyOfRange(preorder, 1, 1 + index), Arrays.copyOfRange(inorder, 0, index));
root.right = buildTree(Arrays.copyOfRange(preorder, 1 + index, preorder.length), Arrays.copyOfRange(inorder, index + 1, inorder.length));
return root;
}
正确的原因
(1) 因为元素不重复, 因此可以通过前序的结果找到root,然后在中序中, 根据root分成两个部分, 两个部分数组的大小就是左右子树的大小, 再根据左子树的大小, 就可以将前序分成两个部分
(2) 就得到了左子树的前序和中序结果, 右子树的前序和中序结果, 就化解成了子问题, 可以继续递归计算.
版本2 正确 最优参传递版本
Map<Integer, Integer> map = new HashMap<>();
int [] preorder;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
// 每次递归的时候, 不创建新的数组, 通过指针
// 将中序遍历的值和index存入map, 方便直接寻找到根的索引
for(int i = 0; i < inorder.length; i ++) {
map.put(inorder[i], i);
}
return digui(0, 0, inorder.length - 1);
}
/**
*
* @param root 是根节点在preOrder中的索引
* @param left left 到 map.get(preOrder[root]) - 1是在inorder中划分得到的左边数组
* @param right right 到 map.get(preOrder[root]) + 1是在inorder中划分得到的右边数组
* @return
*/
public TreeNode digui(int root, int left, int right) {
if (left > right) {
return null;
}
TreeNode rootNode = new TreeNode(preorder[root]);
// rootIndex是root元素在inorder里的位置
int rootIndex = map.get(preorder[root]);
rootNode.left = digui(root + 1, left, rootIndex - 1);
rootNode.right = digui(root - left + rootIndex + 1, rootIndex + 1, right);
return rootNode;
}
正确的原因
(1) 通过root, left, right三个数字, 计算出左右数组的大小.
(2) rootIndex 是根节点在中序中的索引位置, root是根节点在前序中的索引
(3) 第一步: 计算左右两个数组的根节点的索引
对于左数组:
新的root的索引一定是root + 1;
对于右边数组:
新的root的索引 = root + 左数组的长度 + 1
左数组的长度 = rootIndex - left (就是在中序中的长度)
(4) 第二步: 计算左右两个数组新的left和right
对于左边数组:
新的left = left
新的right = rootIndex - 1
对于右边数组:
新的lfet = rootIndex + 1
新的right = right
版本3 正确 参数传递不是最优 但是非常好理解
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 根据前序和中序遍历 构建二叉树
// 这种题目 元素值一定不会重复的, 因此可以遍历一遍中序, 得到元素值和索引的映射
// 用于计算左右子数的大小
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i ++) {
inorderMap.put(inorder[i], i);
}
return recursion(preorder, 0, preorder.length - 1, inorderMap, 0, inorder.length - 1);
}
public TreeNode recursion(int [] preOrder, int preStart, int preEnd, Map<Integer, Integer> inorderMap, int inStart, int inEnd) {
// 根据前序和中序的特点, 不断划分数组, 添加根节点
if (preStart > preEnd) {
return null;
}
// 前序的第一个元素是根节点
TreeNode root = new TreeNode(preOrder[preStart]);
// 根节点将inOrder分成两部分, 分别是左右子树
int rootInOrderIndex = inorderMap.get(preOrder[preStart]);
root.left = recursion(preOrder, preStart + 1, preStart + rootInOrderIndex - inStart, inorderMap, inStart, rootInOrderIndex - 1);
root.right = recursion(preOrder, preStart + rootInOrderIndex - inStart + 1, preEnd, inorderMap, rootInOrderIndex + 1, inEnd);
return root;
}
从上到下打印二叉树1
题目
版本1 正确
public int[] levelOrder(TreeNode root) {
if (root == null) {
return new int[0];
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
List<Integer> ans = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
ans.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return ans.stream().mapToInt(Integer::intValue).toArray();
}
正确的原因
(1) 层级遍历, 注意List 转int[] 数组的写法, 这种写法非常耗时, 完全不如新建一个数组, 然后遍历添加元素, 经过实验 是真的慢不少.
版本2 正确
public int[] levelOrder(TreeNode root) {
if (root == null) {
return new int[0];
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
List<Integer> ans = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
ans.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
int [] array = new int[ans.size()];
for (int i = 0; i < ans.size(); i ++) {
array[i] = ans.get(i);
}
return array;
}
正确的原因
改成for循环转数组, 快很多
树的子结构
题目
版本1 错误
public boolean isSubStructure(TreeNode A, TreeNode B) {
// B是不是A的子结构
// 对于A的每一个节点, 判断和B是不是镜像的即可
boolean ans1 = isMirror(A, B);
boolean ans2 = Boolean.FALSE;
boolean ans3 = Boolean.FALSE;
if (A.left != null) {
ans2 = isSubStructure(A.left, B);
}
if (A.right != null) {
ans3 = isSubStructure(A.right, B);
}
return ans1 || ans2 || ans3;
}
public boolean isMirror(TreeNode root, TreeNode B) {
// 判断root和B是否是镜像
if (root == null && B == null) {
return Boolean.TRUE;
}
if (root == null || B == null) {
return Boolean.FALSE;
}
if (root.val != B.val) {
return Boolean.FALSE;
}
boolean leftIsMirror = isMirror(root.left, B.left);
boolean rightIsMirror = isMirror(root.right, B.right);
return leftIsMirror && rightIsMirror;
}
错误的原因
(1) 思路是判断A中是否存在某个节点, 能够和B成为镜像关系, 如果存在, 那么B是A的子结构.
(2) 其实这个思路不太正确, 如果B刚好能构成A的某棵子树的话, 这个思路是对的, 但是子结构的话, B可以和A中一部分相等, 但是B又不是A的子树, 例如下面这种情况
版本2 正确
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B == null && A != null) {
return Boolean.FALSE;
}
// B是不是A的子结构
// 对于A的每一个节点, 判断和B是不是镜像的即可
boolean ans1 = isMirror(A, B);
boolean ans2 = Boolean.FALSE;
boolean ans3 = Boolean.FALSE;
if (A.left != null) {
ans2 = isSubStructure(A.left, B);
}
if (A.right != null) {
ans3 = isSubStructure(A.right, B);
}
return ans1 || ans2 || ans3;
}
public boolean isMirror(TreeNode root, TreeNode B) {
// 判断root和B是否是镜像
if (root == null && B == null) {
return Boolean.TRUE;
}
// 如果A匹配完了, B还没匹配完
if (root == null) {
return Boolean.FALSE;
}
// 当B匹配完了, A是啥都行
if (B == null) {
return Boolean.TRUE;
}
if (root.val != B.val) {
return Boolean.FALSE;
}
boolean leftIsMirror = isMirror(root.left, B.left);
boolean rightIsMirror = isMirror(root.right, B.right);
return leftIsMirror && rightIsMirror;
}
正确的原因
(1) 在判断A和B是否是镜像的时候, 对于B匹配完, A还有的情况, 返回true, 即可让B和A部分匹配. 解决了版本1的问题
(2) 需要注意B如果一开始就是null的情况, 需要返回false
从上倒下打印二叉树3
题目
版本1 正确
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> ans = new ArrayList<>();
// BFS 层级遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean flag = Boolean.FALSE;
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> temp = new ArrayList<>();
for(int i = 0; i < size; i ++) {
TreeNode node = queue.poll();
temp.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
if (flag) {
// 翻转temp
Collections.reverse(temp);
}
flag = !flag;
ans.add(temp);
}
return ans;
}
正确的原因
(1) 设置一个标志位, 每次为true的时候, 对list进行一次翻转
二叉树中和为某一值的路径
题目
版本1 错误
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
digui(new ArrayList<>(), root, target, 0);
return ans;
}
public void digui(List<Integer> temp, TreeNode root, int target, int tempSum) {
if (tempSum == target) {
ans.add(new ArrayList<>(temp));
}
if (root == null) {
return;
}
tempSum += root.val;
temp.add(root.val);
// 做出选择
digui(temp, root.left, target, tempSum);
if (root.left != null) {
temp.remove(temp.size() - 1);
}
// 做出选择
digui(temp, root.right, target, tempSum);
if (root.right != null) {
temp.remove(temp.size() - 1);
}
}
错误的原因
(1) 假设我到达某个最底层的节点, 值为2, 加上这个2之后, tempSum刚好等于target, 此时是会递归它的左右两个null节点, 此时就会往ans中, 添加两个同样的temp. 例如下图这个例子
版本2 错误
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
digui(new ArrayList<>(), root, target, 0);
return ans;
}
public void digui(List<Integer> temp, TreeNode root, int target, int tempSum) {
if (root == null) {
return;
}
tempSum += root.val;
temp.add(root.val);
if (tempSum == target) {
ans.add(new ArrayList<>(temp));
return;
}
// 做出选择
digui(temp, root.left, target, tempSum);
if (root.left != null) {
temp.remove(temp.size() - 1);
}
// 做出选择
digui(temp, root.right, target, tempSum);
if (root.right != null) {
temp.remove(temp.size() - 1);
}
}
错误的原因
(1) 在版本1的基础上, 修改了递归函数中, 添加到答案的位置. 解决了版本1的问题.
(2) 题目要求, 路径的终点一定要是子叶节点, 递归函数中没有校验这个
版本3 正确
public void digui(List<Integer> temp, TreeNode root, int target, int tempSum) {
if (root == null) {
return;
}
tempSum += root.val;
temp.add(root.val);
// 路径终点必须是子叶节点
if (tempSum == target && root.left == null && root.right == null) {
ans.add(new ArrayList<>(temp));
return;
}
// 做出选择
digui(temp, root.left, target, tempSum);
if (root.left != null) {
temp.remove(temp.size() - 1);
}
// 做出选择
digui(temp, root.right, target, tempSum);
if (root.right != null) {
temp.remove(temp.size() - 1);
}
}
正确的原因
(1) 在版本2的基础上, 新增了root.left == null && root.right == null的判断条件
二叉搜索树的后序遍历序列
题目
版本1 错误
public boolean verifyPostorder(int[] postorder) {
// 针对的是BST 二叉搜索树
// 判断这个后序遍历是不是BST的
// 后序遍历最后一个元素为root, 然后在root前的比root大的元素为右子树
// 比root小的为左子树
// 判断左右子树是不是BST
return digui(postorder, 0, postorder.length - 1);
}
public boolean digui(int[] postOrder, int start, int end) {
if (start >= end) {
return Boolean.TRUE;
}
int rootVal = postOrder[end];
// 寻找右子树的长度
int lenRight = 0;
for(int i = end - 1; i >= start; i --) {
if (postOrder[i] > rootVal) {
lenRight ++;
}
}
// 左子树的长度
int lenLeft = 0;
for (int i = start; i <= end; i ++) {
if (postOrder[i] < rootVal) {
lenLeft ++;
}
}
if (lenLeft + lenRight != end - start) {
return Boolean.FALSE;
}
boolean left = digui(postOrder, start, start + lenLeft - 1);
boolean right = digui(postOrder, start + lenLeft, end - 1);
return left && right;
}
错误的原因
(1) 在判断左右子树的时候, 想的是寻找root左边连续比它大的个数, 但是没有写break;
版本2 正确
public static boolean verifyPostorder(int[] postorder) {
// 针对的是BST 二叉搜索树
// 判断这个后序遍历是不是BST的
// 后序遍历最后一个元素为root, 然后在root前的比root大的元素为右子树
// 比root小的为左子树
// 判断左右子树是不是BST
return digui(postorder, 0, postorder.length - 1);
}
public static boolean digui(int[] postOrder, int start, int end) {
if (start >= end) {
return Boolean.TRUE;
}
int rootVal = postOrder[end];
// 寻找右子树的长度
int lenRight = 0;
for(int i = end - 1; i >= start; i --) {
if (postOrder[i] > rootVal) {
lenRight ++;
} else {
break;
}
}
// 左子树的长度
int lenLeft = 0;
for (int i = start; i <= end; i ++) {
if (postOrder[i] < rootVal) {
lenLeft ++;
} else {
break;
}
}
if (lenLeft + lenRight != end - start) {
return Boolean.FALSE;
}
boolean left = digui(postOrder, start, start + lenLeft - 1);
boolean right = digui(postOrder, start + lenLeft, end - 1);
return left && right;
}
正确的原因
(1) 在求解左右子树长度的时候, 添加else {break;}, 这才是求左右子树长度, 然后通过长度判断是否合法的正确做法
二叉搜索树与双向链表
题目
版本1 正确
List<Node> temp = new ArrayList<>();
public Node treeToDoublyList(Node root) {
if (root == null) {
return root;
}
// 对中序遍历的结果改变指针
digui(root);
for (int i = 0; i < temp.size(); i ++) {
if (i + 1 < temp.size()) {
temp.get(i).right = temp.get(i + 1);
temp.get(i + 1).left = temp.get(i);
}
if (i == 0) {
temp.get(i).left = temp.get(temp.size() - 1);
}
if (i == temp.size() - 1) {
temp.get(i).right = temp.get(0);
}
}
return temp.get(0);
}
public void digui(Node root) {
if (root == null) {
return;
}
// 中序遍历
digui(root.left);
temp.add(root);
digui(root.right);
}
不足之处
(1) 利用了额外的空间来存储中序遍历的结果, 然后才改变指针.
(2) 期望在一次遍历的时候就改变好指针
版本2 错误
Node first;
public Node treeToDoublyList(Node root) {
if (root == null) {
return root;
}
// 对中序遍历的结果改变指针
digui(root);
// 将头节点和尾节点连接
Node lastNode = first;
while (lastNode.right != null) {
lastNode = lastNode.right;
}
first.left = lastNode;
lastNode.right = first;
return first;
}
public Node digui(Node root) {
if (root == null) {
return root;
}
Node left = digui(root.left);
if (left == null && first == null) {
// 记录头节点
first = root;
}
Node right = digui(root.right);
root.left = left;
if (left != null) {
left.right = root;
}
root.right = right;
if (right != null) {
right.left = root;
}
return right == null ? root : right;
}
错误的原因
(1) 在设置指针的时候, 对于左边子树, 应该设置为左子树的最后一个节点, 我这个写法是ok的. return right == null ? root : right;
(2) 但是对于右子树, 应该是设置为第一个节点, 应该是return left == null ? root : left; 因此这两种情况无法同时兼容
(3) 就只能统一返回root, 然后在设置的时候, 递归找到第一个节点或者最后一个节点
版本3 正确
Node first;
public Node treeToDoublyList(Node root) {
if (root == null) {
return root;
}
// 对中序遍历的结果改变指针
digui(root);
// 将头节点和尾节点连接
Node lastNode = first;
while (lastNode.right != null) {
lastNode = lastNode.right;
}
first.left = lastNode;
lastNode.right = first;
return first;
}
public Node digui(Node root) {
if (root == null) {
return root;
}
Node left = digui(root.left);
if (left == null && first == null) {
// 记录头节点
first = root;
}
Node right = digui(root.right);
if (left != null) {
// 将left变为左子树的最后一个节点
while (left.right != null) {
left = left.right;
}
root.left = left;
left.right = root;
} else {
root.left = left;
}
if (right != null) {
// 将right变为右子树的第一个节点
while (right.left != null) {
right = right.left;
}
right.left = root;
root.right = right;
} else {
root.right = right;
}
return root;
}
正确的原因
(1) 每次得到左右子树的结果, 循环找到第一个或者最后一个节点, 然后再连接
验证二叉树
题目
版本1 正确
int countNum;
public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
if (n == 1) {
return Boolean.TRUE;
}
// 数组leftChild和rightChild如果某个索引i存在值不为-1, 代表val为i的节点
// 左孩子为leftChile[i], 右孩子为rightChile[i].
// 那么反过来说如果leftChild或者rightChild中如果有元素x不为-1, 那么就代表元素x是某个元素的孩子
// 可能的元素节点的值为0 -> n - 1
// 如果0 到n - 1中, 某个元素不作为某些元素的孩子, 那么它就是root节点
// 一个元素A如果作为作为元素B的孩子, 那么元素A的度就为1, 元素A如果还是C的孩子, 那么元素A的度为2, 一次类推
// 寻找度为0的元素, 才是根节点, 如果存在多个度为0的节点, 就代表会构成多个二叉树, 不正确
// 如果一个元素的度 > 1, 表示一个孩子作为另外两个元素的孩子, 也不正确
// 其余的情况, 从root节点, 遍历一遍二叉树, 统计元素的个数是否为n.
// 统计每个元素的度
int [] du = new int[leftChild.length];
for(int i = 0; i < n; i ++) {
if (leftChild[i] != -1) {
du[leftChild[i]] ++;
}
if (rightChild[i] != -1) {
du[rightChild[i]] ++;
}
}
// 遍历一遍du, 看看是否有不合规的情况出现
int countZero = 0;
int root = -1;
for (int i = 0; i < n; i ++) {
if (du[i] == 0) {
root = i;
countZero ++;
if (countZero > 1) {
// 两个度为0的节点
return Boolean.FALSE;
}
}
if (du[i] > 1) {
return Boolean.FALSE;
}
}
if (root == -1) {
return Boolean.FALSE;
}
// 以root为根, 遍历一边二叉树
digui(root, leftChild, rightChild);
return countNum == n;
}
public void digui(int root, int[] leftChild, int[] rightChild) {
if (root == -1) {
return;
}
digui(leftChild[root], leftChild, rightChild);
countNum ++;
digui(rightChild[root], leftChild, rightChild);
}
正确的原因
(1) 将问题转化成寻找二叉树每个节点的度, 然后找到root节点, 在遍历一遍, 比较二叉树的元素数量即可
验证搜索二叉树
题目
版本1 错误
public static boolean isValidBST(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 验证二叉树是否是BST
// 当前节点是否满足BST
// 左子树能否构成一个BST 同时左子树中的右子树的所有节点必须小于于root.val
// 右子树能否构成一个BST 同时右子树的所有左子树的所有节点必须大于root.val
if(root.left != null && root.left.val >= root.val || root.right != null && root.right.val <= root.val) {
return Boolean.FALSE;
}
boolean left = digui(root.left, -1, root.val);
boolean right = digui(root.right, root.val, -1);
return left && right;
}
// min和max不会同时生效, max用来限定左子树中的右孩子节点, min用来限定右子树中的左孩子节点
public static boolean digui(TreeNode root, int min, int max) {
if (root == null) {
return Boolean.TRUE;
}
boolean left = Boolean.FALSE;
if (root.left == null) {
// 如果root.left为null, 此时是满足条件的
left = Boolean.TRUE;
} else {
if (root.left.val < root.val && min != -1 && root.left.val > min) {
left = digui(root.left, -1, root.val);
}
}
boolean right = Boolean.FALSE;
if (root.right == null) {
right = Boolean.TRUE;
} else {
if (root.right.val > root.val && max != -1 && root.right.val < max) {
right = digui(root.right, root.val, -1);
}
}
return left && right;
}
错误的原因
(1) root.left.val < root.val && min != -1 && root.left.val > min 这个判断的本意是想如果min == -1的时候, 不要判断root.left.val > min这个条件. 但是这么写实际的效果就是min == -1的时候, 就不会走left赋值的这个语句了, 就没判断left是不是BST.
版本2 错误
public static boolean isValidBST(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 验证二叉树是否是BST
// 当前节点是否满足BST
// 左子树能否构成一个BST 同时左子树中的右子树的所有节点必须小于于root.val
// 右子树能否构成一个BST 同时右子树的所有左子树的所有节点必须大于root.val
if(root.left != null && root.left.val >= root.val || root.right != null && root.right.val <= root.val) {
return Boolean.FALSE;
}
boolean left = digui(root.left, -1, root.val);
boolean right = digui(root.right, root.val, -1);
return left && right;
}
// min和max不会同时生效, max用来限定左子树中的右孩子节点, min用来限定右子树中的左孩子节点
public static boolean digui(TreeNode root, int min, int max) {
if (root == null) {
return Boolean.TRUE;
}
boolean left = Boolean.FALSE;
if (root.left == null) {
// 如果root.left为null, 此时是满足条件的
left = Boolean.TRUE;
} else {
if (root.left.val < root.val) {
if (min == -1) {
left = digui(root.left, -1, root.val);
} else {
// 如果min != -1, 多一个条件
left = root.left.val > min && digui(root.left, -1, root.val);
}
}
}
boolean right = Boolean.FALSE;
if (root.right == null) {
right = Boolean.TRUE;
} else {
if (root.right.val > root.val) {
if (max == -1) {
right = digui(root.right, root.val, -1);
} else {
right = digui(root.right, root.val, -1) && root.right.val < max;
}
}
}
return left && right;
}
错误的原因
(1) 对于每个root, 它的左子树, 除了要满足左子树的右孩子要小于root.val以外, 左子树的左孩子, 同时也要满足大于root的父亲.
例如B = A.right, C = B.left; D = C.left;
此时D必须满足>B
版本3 正确
public static boolean isValidBST(TreeNode root) {
if (root == null) {
return Boolean.TRUE;
}
// 验证二叉树是否是BST
// 当前节点是否满足BST
// 左子树能否构成一个BST 同时左子树中的右子树的所有节点必须小于于root.val
// 因此对于左子树中的右子树不断更新max
// 右子树能否构成一个BST 同时右子树的所有左子树的所有节点必须大于root.val
if(root.left != null && root.left.val >= root.val || root.right != null && root.right.val <= root.val) {
return Boolean.FALSE;
}
boolean left = digui(root.left, -1, root.val);
boolean right = digui(root.right, root.val, -1);
return left && right;
}
// min和max不会同时生效, max用来限定左子树中的右孩子节点, min用来限定右子树中的左孩子节点
public static boolean digui(TreeNode root, int min, int max) {
if (root == null) {
return Boolean.TRUE;
}
boolean left = Boolean.FALSE;
if (root.left == null) {
// 如果root.left为null, 此时是满足条件的
left = Boolean.TRUE;
} else {
if (root.left.val < root.val) {
if (min == -1) {
left = digui(root.left, -1, root.val);
} else {
// 如果min != -1, 多一个条件
left = root.left.val > min && digui(root.left, min, root.val);
}
}
}
boolean right = Boolean.FALSE;
if (root.right == null) {
right = Boolean.TRUE;
} else {
if (root.right.val > root.val) {
if (max == -1) {
right = digui(root.right, root.val, -1);
} else {
right = digui(root.right, root.val, max) && root.right.val < max;
}
}
}
return left && right;
}
正确的原因
(1) max本意是赋予左子树的, 用来限制左子树的右孩子, min本意是赋予右子树, 用来限制右子树的左孩子
(2) 假设B = A.left, 对于B作为root来说, max是有值的, B同样会拥有左子树和右子树, 那么对于B的左子树, 我们会将max = B.val, 那么对于B的右边子树呢, 我们除了赋予它min = B.val以外, 同时B的右子树, 是不是也应该遵守B的max值呢? 答案是肯定的, B都需要遵守, 那B的孩子肯定也需要遵守啊, 因此B的右子树也需要遵守max, 即digui(root.right, root.val, max)
(3) 同理假设C = A.right, 此时min是有值的, 那么C的左孩子, 同样要遵守min的值, 即digui(root.left, min, root.val)
(4) 也就是说, 如果root一直是左孩子, 那么它不需要遵守min, 如果root是某个右孩子的左孩子, 那么就要遵守min
版本4 最棒的写法
public boolean isValidBST(TreeNode root) {
// 判断一个树是否是二叉搜索树
// 对于每个节点, 在前序遍历的时候, 判断是否符合
// 而不是在每个节点根据左右孩子判断
return digui(root, null, null);
}
public boolean digui(TreeNode root, TreeNode min, TreeNode max) {
if (root == null) {
return Boolean.TRUE;
}
if (min != null) {
if (root.val <= min.val) {
return Boolean.FALSE;
}
}
if (max != null) {
if (root.val >= max.val) {
return Boolean.FALSE;
}
}
return digui(root.left, min, root) && digui(root.right, root, max);
}
正确的原因
(1) 后置了对于root节点的判断, 然后min和max更加的清晰, 很明显就会这样写, 很自然的做到了传递
修改搜索二叉树
题目
版本1 正确
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) {
return root;
}
// 修剪一棵BST, 使得在范围的节点得以保存, 同时保持原来的结构
if (root.val < low) {
return trimBST(root.right, low, high);
}
if (root.val > high) {
return trimBST(root.left, low, high);
}
// 此时root.val 在low和high之间
root.left = trimBST(root.left, low, root.val);
root.right = trimBST(root.right, root.val, high);
return root;
}
正确的原因
(1) 对于每个root, 根据root.val的值, 判断应该怎么寻找范围内的值, 然后就可以了
最大二叉树
题目
版本1 正确
public TreeNode constructMaximumBinaryTree(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
return digui(nums, 0, nums.length - 1);
}
public TreeNode digui(int [] nums, int start, int end) {
if (start > end) {
return null;
}
// 寻找nums里的最大值作为root
int max = 0;
int maxIndex = start;
for (int i = start; i <= end; i ++) {
if (nums[i] > max) {
max = nums[i];
maxIndex = i;
}
}
TreeNode root = new TreeNode(max);
root.left = digui(nums, start, maxIndex - 1);
root.right = digui(nums, maxIndex + 1, end);
return root;
}
正确的原因
(1) 注意int maxIndex = start; 这里maxIndex的初识值要注意, 要是设置成0的话, 当start = end = 1的时候, 会死循环
翻转等价二叉树
题目
版本1 正确
public boolean flipEquiv(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) {
return Boolean.TRUE;
}
if (root1 == null || root2 == null) {
return Boolean.FALSE;
}
if (root1.val != root2.val) {
return Boolean.FALSE;
}
// 对root1和root2进行同样形式的遍历, 一但发现节点不同, 则将root2交换一次, 继续遍历
// 每个树里的值都是唯一的, 因此可以通过val是否相同, 来判断是否是同一节点
int root1Left = root1.left == null ? -1 : root1.left.val;
int root2Left = root2.left == null ? -1 : root2.left.val;
int root1Right = root1.right == null ? -1 : root1.right.val;
int root2Right = root2.right == null ? -1 : root2.right.val;
boolean left = Boolean.FALSE;
if (root1Left == root2Left) {
left = flipEquiv(root1.left, root2.left);
} else if (root1Left == root2Right) {
// 修改root2的结构
TreeNode temp = root2.left;
root2.left = root2.right;
root2.right = temp;
int tempNum = root2Left;
root2Left = root2Right;
root2Right = tempNum;
// 修改完结构后 递归
left = flipEquiv(root1.left, root2.left);
} else {
left = Boolean.FALSE;
}
boolean right = Boolean.FALSE;
if (root1Right == root2Right) {
right = flipEquiv(root1.right, root2.right);
} else {
right = Boolean.FALSE;
}
return left && right;
}
正确的原因
(1) 从root开始, 先比较root1.val和root2.val, 再比较root1.left和root2.left, 如果相等, 继续, 如果不想等, 将root2交换一次左右节点, 然后再比较,
二叉树的最大宽度
题目
版本1 正确 BFS
public int widthOfBinaryTree(TreeNode root) {
// 这道题要求的不是统计每一层二叉树有多少非null的节点
// 而是统计这一层非null的两个节点之间的最大宽度
// 因此在BFS遍历的时候, 还需要记录下二叉树非null结点的具体的索引值(即是满二叉树的第多少个节点)
Queue<Pair<TreeNode, Integer>> queue = new LinkedList<>();
queue.offer(new Pair<>(root, 1));
int max = Integer.MIN_VALUE;
while (!queue.isEmpty()) {
int size = queue.size();
// 对于每一层, 只需要得到这一层非null节点的第一个和最后一个的索引即可
int left = 0;
int right = 0;
for (int i = 0; i < size; i ++) {
Pair<TreeNode, Integer> temp = queue.poll();
TreeNode tempNode = temp.getKey();
int index = temp.getValue();
if (i == 0) {
left = index;
}
if (i == size - 1) {
right = index;
}
if (tempNode.left != null) {
queue.offer(new Pair<>(tempNode.left, 2 * index));
}
if (tempNode.right != null) {
queue.offer(new Pair<>(tempNode.right, 2 * index + 1));
}
}
max = Math.max(right - left + 1, max);
}
return max;
}
正确的原因
(1) BFS遍历的时候, 每个节点需要记录该节点在满二叉树中的索引值
(2) 每一层节点, 只需要对头尾的两个非null结点求一下宽度, 剩下的节点用于填充下一层的节点
二叉树的中序遍历
题目
版本1 正确
List<Integer> ans = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
digui(root);
return ans;
}
public void digui(TreeNode root) {
if (root == null) {
return;
}
digui(root.left);
// 中序
ans.add(root.val);
digui(root.right);
}
二叉树中的最大路径和
题目
版本1 正确
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if (root == null) {
return 0;
}
recursion(root);
return max;
}
public int recursion(TreeNode root) {
if (root == null) {
return 0;
}
// 求二叉树路径上的最大值
// 对于左子树, 只需要保留大于0的数值
int left = Math.max(0, recursion(root.left));
// 右子树同理
int right = Math.max(0, recursion(root.right));
max = Math.max(left + right + root.val, max);
// 返回只能返回左右子树的一边 + root.val, 两边的值不能同时取到
return Math.max(left, right) + root.val;
}
正确的原因
(1) 注意如何获取左右子树的值, 以及如何更新答案
递增顺序搜索树
题目
版本1 正确
public TreeNode increasingBST(TreeNode root) {
// 将一棵二叉树, 变成只具有右孩子的二叉树, 然后节点的值递增
if (root == null) {
return root;
}
TreeNode left = increasingBST(root.left);
if (left != null) {
TreeNode tempLeft = left;
// 寻找左子树的最右边节点
while (tempLeft.right != null) {
tempLeft = tempLeft.right;
}
tempLeft.right = root;
}
// 当前节点的左孩子应该断开
root.left = null;
// 右子树返回的结果就是排序好的结果,直接链接即可
TreeNode right = increasingBST(root.right);
root.right = right;
// left节点就是排序好的部分的头节点
// 如果left为null, root就是头节点
return left == null ? root : left;
}
正确的原因呢
(1) 注意方法的返回, 必须是排序好二叉树的最左边节点, 因为如果直接返回root的话, 没有办法去寻找root的最左边节点, 构建右子树就有问题
(2) 注意每次寻找left的最右边节点的时候, 要用临时变量, 不然left指针就变化了, 就不能作为返回值了