二叉树的中序遍历
算法概述
原题
本题要求为中序遍历。三种解法: 递归 , 迭代 , Morris 。
对于递归:
- 时间复杂度为O(n):全部遍历一次
- 空间复杂度为O(n):隐式的栈空间
对于迭代:
- 时间复杂度为O(n)
- 空间复杂度为O(n):双向队列实现栈,只是变得显式
对于Morris:
- 时间复杂度为O(n)
- 空间复杂度为O(1):通过判断左节点的情况,和将右节点与根节点相连接等算法,避免栈
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); inorder(root, res); return res; } public void inorder(TreeNode root, List<Integer> res) { if (root == null) { return; } inorder(root.left, res); res.add(root.val); inorder(root.right, res); } }
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); Deque<TreeNode> stk = new LinkedList<TreeNode>(); while (root != null || !stk.isEmpty()) { while (root != null) { stk.push(root); root = root.left; } root = stk.pop(); res.add(root.val); root = root.right; } return res; } }
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); TreeNode predecessor = null; while (root != null) { if (root.left != null) { predecessor = root.left; while (predecessor.right != null && predecessor.right != root) { predecessor = predecessor.right; } if (predecessor.right == null) { predecessor.right = root; root = root.left; } else { res.add(root.val); predecessor.right = null; root = root.right; } } else { res.add(root.val); root = root.right; } } return res; } }
|
C++
注意
对于一个树根为1,1的右子树为3,3的左子树为2的树,函数调用栈如上所示。
对于迭代,代码的执行过程是差不多的,但内在逻辑的差别较大。迭代中root
用右子节点刷新,所以不可避免会为null,所以循环条件特意这么设置 while (root != null || !stk.isEmpty())
,同时,root
还从模拟栈中取值刷新root = stk.pop();
,
1 2 3 4
| while (root != null) { stk.push(root); root = root.left; }
|
此时右子节点又会被初始时用于遍历到左下的循环 压入栈中 , 很巧妙。
对于Morris,代码的核心逻辑是将所有节点用中序遍历的逻辑连接起来, 对于左子结点,将其右子节点设为根节点 ,而 对于右子节点,将其右子节点设置为根节点的根节点 ,这样就得到了本质为中序遍历的 右序遍历 。每次遍历的时候,核心是 找到当前节点在中序遍历中的前驱并成为其右子节点 。
最后就是
1 2 3 4
| else { res.add(root.val); root = root.right; }
|
右序遍历。
常温常新
二叉树的最大深度
算法概述
原题
本题要求为求出给出二叉树的最大深度。可使用 深度优先搜索(DFS) ,或者 广度优先搜索(BFS) ,前者更佳。
对于DFS:
- 时间复杂度为O(n)
- 空间复杂度为O(height):递归深度为二叉树高度
对于BFS:
- 时间复杂度为O(n)
- 空间复杂度为O(n):取决于需要存储多少个元素
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| class Solution { public int maxDepth(TreeNode root) { if(root==null){return 0;} else{ int leftHeight=maxDepth(root.left); int rightHeight=maxDepth(root.right); return Math.max(leftHeight,rightHeight)+1; } } }
class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } Queue<TreeNode> queue = new LinkedList<TreeNode>(); queue.offer(root); int ans = 0; while (!queue.isEmpty()) { int size = queue.size(); while (size > 0) { TreeNode node = queue.poll(); size--; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } ans++; } return ans; } }
|
重要实例方法及属性(JAVA)
Queue.poll()
:移除并返回头元素
Queue.size()
:返回队列长度
C++
1 2 3 4 5 6 7 8 9 10 11
| class Solution { public: int maxDepth(TreeNode* root) { if (root == nullptr) return 0; return max(maxDepth(root->left), maxDepth(root->right)) + 1; } };
|
重要实例方法及属性(C++)
Queue.front()
:返回头元素
Queue.pop()
:不返回任何值,只删除头元素
注意
都是非常经典的算法, 必须熟练掌握 。
翻转二叉树
算法概述
原题
本题要求为将给出的二叉树翻转后返回。对二叉树操作不像对链表操作那么便捷,所以需要一个栈(函数调用栈)作为辅助来帮助翻转,同时,对于二叉树而言,因为每个子树的结构都很清晰,所以递归比较适合统一处理整个树。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public TreeNode invertTree(TreeNode root) { if (root == null) { return null; } TreeNode left = invertTree(root.left); TreeNode right = invertTree(root.right); root.left = right; root.right = left; return root; } }
|
C++
注意
二叉树的很多操作要通过递归完成,所以熟悉递归下的函数调用栈的顺序是很重要的。
这道题中的TreeNode left = invertTree(root.left);
和TreeNode right = invertTree(root.right);
一定要记得是要在对左右子节点的操作之前运行,还有if (root == null)
底部情况也要处理。
总而言之:
一定要熟练掌握
对称二叉树
算法概述
原题
本题要求为判断二叉树是否对称。还是使用 递归 分别判断左右子节点。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public boolean isSymmetric(TreeNode root) { return check(root.left,root.right); }
public boolean check(TreeNode p, TreeNode q){ if(p==null&&q==null){ return true; } if(p==null||q==null){ return false; } return p.val==q.val&&check(p.left,q.right)&&check(p.right,q.left); } }
|
C++
注意
在使用递归的时候,一定要注意二叉树的基本单位(子树)是 三个节点 。
二叉树的直径
算法概述
原题
本题要求为求出二叉树中隔得最远的两个节点的距离。还是需要使用深度优先搜索的思路,应该以 “求最大深度” 的思路求这道题。
- 时间复杂度为O(n)
- 空间复杂度为O(height):栈的深度最坏情况可能还是最大深度
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { int ans=0; public int diameterOfBinaryTree(TreeNode root) { depth(root); return ans; } public int depth(TreeNode node) { if (node == null) { return 0; } int L = depth(node.left); int R = depth(node.right); ans = Math.max(ans, L+R); return Math.max(L, R) + 1; } }
|
C++
注意
计算的路径的时候是不应该包含节点本身的,所以应该ans = Math.max(ans, L+R);
这么更新,与递归更新深度的return Math.max(L, R) + 1;
不一样。还要记得用全局变量追踪,以免作用域冲突。
二叉树的层序遍历
算法概述
原题
本题要求为把二叉树每一层的元素提取成一个列表,然后按照顺序组合成一个完整的列表返回。其实 只要在“二叉树的最大深度”那道题上稍微改一点 就可以了。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Solution { public List<List<Integer>> levelOrder(TreeNode root) { if (root == null) return new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>(); Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root);
while (!queue.isEmpty()) { int size = queue.size(); List<Integer> temp = new ArrayList<>();
while (size > 0) { TreeNode node = queue.poll(); temp.add(node.val); size--; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } ans.add(temp); }
return ans; } }
|
C++
注意
之前的BFS和DFS精通了之后,对这种题型就会信手拈来。所以 一定要完全掌握BFS和DFS 。
将有序数组转换为二叉搜索树
算法概述
原题
本题要求为将有序数组转换为平衡的二叉搜索树(BST),平衡即所有节点的子树高度差不超过1,二叉搜索树意为 左子树只包含小于当前节点的数,右子树只包含大于当前节点的数 。给出的解法使用的是 递归的二分法 ,随机选取mid节点,然后直接生成就好。
- 时间复杂度为O(n)
- 空间复杂度为O(logn):平均,因为栈的使用空间会回退,如果退化成链表或者单边树,那就还是O(n)
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { Random rand = new Random();
public TreeNode sortedArrayToBST(int[] nums) { return helper(nums, 0, nums.length - 1); } public TreeNode helper(int[] nums, int left, int right) { if (left > right) { return null; } int mid = (left + right + rand.nextInt(2)) / 2; TreeNode root = new TreeNode(nums[mid]); root.left = helper(nums, left, mid - 1); root.right = helper(nums, mid + 1, right); return root; } }
|
重要实例方法及属性(JAVA)
Random.nextInt(Integer)
:生成一个[0,Integer]范围的整数
C++
重要实例方法及属性(C++)
rand()%2
:能达到Random.nextInt(2)
相同的效果
注意
这道题使用了 二分 来确保树的 平衡性 。
要记住的是,二分法一般要采用if (left > right)
这样的判断条件来终止递归,而能达到这样的终止条件是由于递归中的右指针mid - 1
和左指针mid+1
达到的,同时,因为是二分法处理,所以是 先处理再递归 。
验证二叉搜索树
算法概述
原题
本题要求为验证给出的树是否为二叉搜索树(定义上题有)。递归可以完成,或者使用中序遍历(迭代)模拟一个栈,其实空间复杂度一样,那还不如用前者。
- 时间复杂度为O(n)
- 空间复杂度为O(height):最坏还是n
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public boolean isValidBST(TreeNode root) { return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE); } public boolean isValidBST(TreeNode node, long lower, long upper) { if (node == null) { return true; } if (node.val <= lower || node.val >= upper) { return false; } return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper); } }
|
C++
注意
java里的防溢出常量是Long.MIN_VALUE, Long.MAX_VALUE
这俩,而C++中的是LONG_MIN, LONG_MAX
这俩。
无论是判断还是操作(这里是操作), 递归的作用是定位 ,这 与题目要求是分离的 。
二叉搜索树中第K小的元素
算法概述
原题
本题要求为找出二叉搜索树(定义上题有)中第k小的元素。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public int kthSmallest(TreeNode root, int k) { Deque<TreeNode> stack = new ArrayDeque<TreeNode>(); while (root != null || !stack.isEmpty()) { while (root != null) { stack.push(root); root = root.left; } root = stack.pop(); --k; if (k == 0) { break; } root = root.right; } return root.val; } }
|
C++
注意
对于二叉平衡树(AVL), 中序遍历很重要 。要理解root = stack.pop();
这里得到的就是按照中序遍历的节点。
二叉树的右视图
算法概述
原题
本题要求为按照根节点向下的顺序返回所有节点的右子节点。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Solution { public List<Integer> rightSideView(TreeNode root) { if (root == null) { return new ArrayList<>(); }
List<Integer> result = new ArrayList<>(); Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root);
while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); if (i == size - 1) { result.add(node.val); } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
return result; } }
|
C++
注意
正如 栈(双向队列)很适合这类问题 ,BFS也很适合操作具有层内关系的单层节点 。
二叉树展开为链表
算法概述
原题
本题要求为将二叉树按照先序遍历展开成链表。除了先序遍历以外,根据先序遍历的规律,还可以通过不断将遍历节点的右子树 向左子树的尾部拼接 。
- 时间复杂度为O(n)
- 空间复杂度为O(1):在原二叉树上操作
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Solution { public void flatten(TreeNode root) { TreeNode curr = root; while (curr != null) { if (curr.left != null) { TreeNode next = curr.left; TreeNode predecessor = next; while (predecessor.right != null) { predecessor = predecessor.right; } predecessor.right = curr.right; curr.left = null; curr.right = next; } curr = curr.right; } } }
|
C++
注意
其实本质上还是一个 找前驱后驱 的工作,先序遍历是 根->左->右 ,也就是说这样相当于为每个子树(三个节点的结构)找到后驱,把所有左子结点转移到右子树上去,所以当if (curr.left != null)
再也没有左子节点的时候,就成功了。而且符合先序遍历的顺序。
当然还是要了解先序遍历的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public void flatten(TreeNode root) { List<TreeNode> list = new ArrayList<TreeNode>(); preorderTraversal(root, list); int size = list.size(); for (int i = 1; i < size; i++) { TreeNode prev = list.get(i - 1), curr = list.get(i); prev.left = null; prev.right = curr; } }
public void preorderTraversal(TreeNode root, List<TreeNode> list) { if (root != null) { list.add(root); preorderTraversal(root.left, list); preorderTraversal(root.right, list); } } }
|
就是说白了根在哪都是list.add(root);
直接就能处理,但左子树和右子树的遍历需要用递归,顺序改改就行。
从前序与中序遍历构造二叉树
算法概述
原题
本题要求为给出两种方式遍历得到的列表,返回一个确定的二叉树。关键是题目保证无重复元素,所以用哈希表能够很便捷地求出必要信息,从两个遍历本身的特性上。
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Solution { private Map<Integer, Integer> indexMap;
private TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) { if (preorder_left > preorder_right) { return null; } int preorder_root = preorder_left; int inorder_root = indexMap.get(preorder[preorder_root]); int size_left_subtree = inorder_root - inorder_left; TreeNode root = new TreeNode(preorder[preorder_root]); root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1); root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right); return root; }
public TreeNode buildTree(int[] preorder, int[] inorder) { int n = preorder.length; indexMap = new HashMap<Integer, Integer>(); for (int i = 0; i < n; i++) { indexMap.put(inorder[i], i); } return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1); } }
|
C++
注意
其实重点还是是否了解 先序遍历和中序遍历的特点 ,正因为中序遍历是左根右,所以能够从中序遍历的结果得到 左右子树的边界信息 ,而也正是因为先序遍历根左右,所以在逐层遍历的时候更加直观,也就 更易于创建二叉树 。
代码中的顺序也可以自然地这样写: 先创建根节点,再递归创建左子树,再是右子树 。
if (preorder_left > preorder_right)
注意这里不是大于等于,而是大于,等于时也就意味着有一个独立的节点。
迭代,也就是外显函数调用栈,也可以实现,大致的思路也是如此,主要的核心还是在于 中序遍历和先序遍历的特性 。
路径总和 III
算法概述
原题
本题要求为求出所有总和为目标值的方向向下的路径。可以直接用深度优先搜索直接每个节点算一次,也可以用 前缀和+回溯法 。
- 时间复杂度为O(n):前缀和只需遍历一次
- 空间复杂度为O(n):哈希表存储前缀和
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Solution { public int pathSum(TreeNode root, int targetSum) { Map<Long, Integer> prefix = new HashMap<Long, Integer>(); prefix.put(0L, 1); return dfs(root, prefix, 0, targetSum); }
public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) { if (root == null) { return 0; } int ret = 0; curr += root.val; ret = prefix.getOrDefault(curr - targetSum, 0); prefix.put(curr, prefix.getOrDefault(curr, 0) + 1); ret += dfs(root.left, prefix, curr, targetSum); ret += dfs(root.right, prefix, curr, targetSum); prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);
return ret; } }
|
C++
注意
求多个节点的和的时候如果无法用DFS或者BFS直接得到O(n),就应该想到前缀和,这个算法最大的优点就是 只需遍历一次 。
但同时,还需要考虑到的是,因为操作对象是二叉树,所以需要用到 回溯 。 回溯 的关键是要在隐式的函数递归回到当前节点的根节点时, 消除当前节点(子树) 的影响。
prefix.put(0L, 1);
还要记住,因为是前缀和,所以 最后的答案应该是0 ,不设置这个答案键值对,路径总和也更新不了。
prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);
这里是把哈希表中的统计清除了,但并不是清除curr
当前前缀和,因为 回溯之后前缀和自然保持的是上一层根节点的前缀和 。这就是要把 前缀和更新以及路径匹配放在递归之前,而回溯更新放在递归之后 的原因。
还要会的是时间复杂度为O(n^2)的深度优先搜索的算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { public int pathSum(TreeNode root, long targetSum) { if (root == null) { return 0; }
int ret = rootSum(root, targetSum); ret += pathSum(root.left, targetSum); ret += pathSum(root.right, targetSum); return ret; }
public int rootSum(TreeNode root, long targetSum) { int ret = 0;
if (root == null) { return 0; } int val = root.val; if (val == targetSum) { ret++; } ret += rootSum(root.left, targetSum - val); ret += rootSum(root.right, targetSum - val); return ret; } }
|
要注意的是:
targetSum - val
,看似左右并行,但实际上是独立的,对每个子树来说,只是先算左边还是右边的问题,匹配的减法是独立的。
ret += pathSum(root.left, targetSum);
这里相当于对每个节点都用递归循环一遍路径匹配的递归过程,所以是双递归,所以时间复杂度为O(n^2)。
二叉树的最近公共祖先
算法概述
原题
本题要求为找出给定两个节点的最近公共祖先,公共祖先的定义是在是祖先的同时深度尽可能大(深度越大,也就是离两个节点更近),还可以是自身。还是 深度优先搜索递归 ,从底向上,判断子树中包不包含两个要的节点。
- 时间复杂度为O(n)
- 空间复杂度为O(height):最坏还是n
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { private TreeNode ans=null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { dfs(root,p,q); return ans; }
private boolean dfs(TreeNode root, TreeNode p, TreeNode q){ if(root==null) return false; boolean lson=dfs(root.left,p,q); boolean rson=dfs(root.right,p,q); if((lson&&rson)||((root.val==p.val||root.val==q.val)&&(lson||rson))){ ans=root; } return lson||rson||(root.val==p.val||root.val==q.val); } }
|
C++
注意
核心的底层实现思路肯定还是 深度优先搜索 ,在此之上要搞清楚 怎么把判断放入递归 。
if((lson&&rson)||((root.val==p.val||root.val==q.val)&&(lson||rson)))
可见 判断应该放在递归取值之后 。同时要考虑两种情况,即(lson&&rson)
包含两个节点和(root.val==p.val||root.val==q.val)&&(lson||rson)
包含其中一个节点,而 另外一个节点也是最近共同祖先 。
return lson||rson||(root.val==p.val||root.val==q.val)
递归函数的返回只需要证明当前节点与目标节点相关即可,可以是 包含左或右目标节点 ,也可以是 本身就是左或有目标节点 。
ans=root;
因为是从底向上(最大深度)的,所以第一个就是我们要的,然后这个算法最妙的点就在于当ans
第一次更新之后,即使它的上层根节点其实也符合条件,但是对于上层根节点来说,只会有lson和rson中的一个是true,但条件要求必须两个都为true ,所以上层根节点不会再被用来更新root
。
二叉树中的最大路径和
算法概述
原题
本题要求为求出二叉树中的最大路径和(路径即以边连接的节点序列)。就是把“路径总和 III”那道题拿过来改一改就行了。
- 时间复杂度为O(n)
- 空间复杂度为O(height):最坏为n
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { private int maxPath = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) { dfs(root); return maxPath; }
private int dfs(TreeNode root) { if (root == null) { return 0; } int left = Math.max(dfs(root.left), 0); int right = Math.max(dfs(root.right), 0);
maxPath = Math.max(maxPath, root.val + left + right);
return root.val + Math.max(left, right); } }
|
C++
注意
maxPath = Math.max(maxPath, root.val + left + right)
全局最大路径和是一个 缺了一条边的三角形 ,所以在函数调用栈弹出栈顶时,也就是为上一层根节点计算提供left
和right
时,应该是return root.val + Math.max(left, right)
,也就是 只返回一条边 。