两个套路,一个是递归遍历二叉树traverse() 无返回值 ,一个是分解处理子数,有返回值。
综上,遇到一道二叉树的题目时的通用思考过程是:
1、是否可以通过遍历一遍二叉树得到答案 ?如果可以,用一个 traverse
函数配合外部变量来实现。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案 ?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做 。
1. 常考 1.1 二叉树的最大深度 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 var res int var depth int func maxDepth (root *TreeNode) int { traverse(root) return res } func traverse (root *TreeNode) { if root == nil { return } depth++ if root.Left == nil && root.Right == nil { res = max(res, depth) } traverse(root.Left) traverse(root.Right) depth-- } func max (a, b int ) int { if a > b { return a } return b }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func maxDepth (root *TreeNode) int { if root == nil { return 0 } left := maxDepth(root.Left) right := maxDepth(root.Right) return Max(left,right)+1 } func Max (a, b int ) int { if a > b { return a } return b }
1.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 30 31 32 33 func isBalanced (root *TreeNode) bool { if root == nil { return true } if !isBalanced(root.Left) || !isBalanced(root.Right){ return false } if abs(maxDepth(root.Left)-maxDepth(root.Right)) > 1 { return false } return true } func maxDepth (root *TreeNode) int { if root == nil { return 0 } return max(maxDepth(root.Left),maxDepth(root.Right)) + 1 } func max (a,b int ) int { if a > b { return a } return b } func abs (a int ) int { if a < 0 { return -a } return a }
1.3 翻转二叉树🔥 1 2 3 4 5 6 7 8 9 10 11 func mirrorTree (root *TreeNode) *TreeNode { if root == nil { return nil } left := mirrorTree(root.Left) right := mirrorTree(root.Right) root.Left = right root.Right = left return root }
2. 其他 2.1 二叉树的直径 每一条二叉树的「直径」长度,就是一个节点的左右子树的最大深度之和 。
画图, len(preorder[:pos]) 是左子树的长度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func buildTree (preorder []int , inorder []int ) *TreeNode { if len (preorder) == 0 { return nil } root := &TreeNode{preorder[0 ],nil ,nil } pos := 0 for ; pos < len (inorder); pos++{ if inorder[pos] == preorder[0 ]{ break } } root.Left = buildTree(preorder[1 :len (preorder[:pos])+1 ],inorder[:pos]) root.Right = buildTree(preorder[len (preorder[:pos])+1 :],inorder[pos+1 :]) return root }
1 2 3 4 5 6 7 8 9 10 11 12 13 func mergeTrees (root1 *TreeNode, root2 *TreeNode) *TreeNode { if root1 == nil { return root2 } if root2 == nil { return root1 } root1.Val = root1.Val + root2.Val root1.Left = mergeTrees(root1.Left, root2.Left) root1.Right = mergeTrees(root1.Right, root2.Right) return root1 }
1 2 3 4 5 6 7 8 9 10 func isSameTree (p *TreeNode, q *TreeNode) bool { if p == nil && q == nil { return true } if p == nil || q == nil { return false } return p.Val == q.Val && isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func isSubtree (root *TreeNode, subRoot *TreeNode) bool { if root == nil || subRoot == nil { return false } if isSameTree(root, subRoot) { return true } return isSubtree(root.Left, subRoot) || isSubtree(root.Right, subRoot) } func isSameTree (p *TreeNode, q *TreeNode) bool { if p == nil && q == nil { return true } if p == nil || q == nil { return false } return p.Val == q.Val && isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func isSubStructure (A *TreeNode, B *TreeNode) bool { if A == nil || B == nil { return false } return isContain(A, B) || isSubStructure(A.Left, B) || isSubStructure(A.Right, B) } func isContain (A *TreeNode, B *TreeNode) bool { if B == nil { return true } if A == nil { return false } if A.Val != B.Val { return false } return isContain(A.Left, B.Left) && isContain(A.Right, B.Right) }
1 2 3 4 5 6 7 8 9 10 11 12 func isUnivalTree (root *TreeNode) bool { if root == nil { return true } if root.Left != nil && root.Val != root.Left.Val { return false } if root.Right != nil && root.Val != root.Right.Val { return false } return isUnivalTree(root.Left) && isUnivalTree(root.Right) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func isSymmetric (root *TreeNode) bool { if root == nil { return true } return isMirror(root.Left, root.Right) } func isMirror (l, r *TreeNode) bool { if l == nil && r == nil { return true } if l == nil || r == nil { return false } return l.Val == r.Val && isMirror(l.Right, r.Left) && isMirror(l.Left,r.Right) }
1 2 3 4 5 6 7 8 9 10 11 12 13 func lowestCommonAncestor (root, p, q *TreeNode) *TreeNode { val := root.Val pv := p.Val qv := q.Val if pv > val && qv > val { return lowestCommonAncestor(root.Right, p, q) }else if pv < val && qv < val { return lowestCommonAncestor(root.Left, p, q) }else { return root } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func lowestCommonAncestor (root, p, q *TreeNode) *TreeNode { if root == nil { return nil } if p == root || q == root { return root } l := lowestCommonAncestor(root.Left, p, q) r := lowestCommonAncestor(root.Right,p, q) if l != nil && r != nil { return root } if l != nil { return l } if r != nil { return r } return nil }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var arr[]int func kthLargest (root *TreeNode, k int ) int { if root == nil { return 0 } arr = []int {} dfs(root) return arr[k-1 ] } func dfs (root *TreeNode) { if root == nil { return } dfs(root.Right) arr = append (arr, root.Val) dfs(root.Left) }
3. 算法理解 3.1 两种思路解题 二叉树的前序遍历结果怎么算?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 List<Integer> res = new LinkedList <>(); List<Integer> preorder (TreeNode root) { traverse(root); return res; } void traverse (TreeNode root) { if (root == null ) { return ; } res.addLast(root.val); traverse(root.left); traverse(root.right); }
1 2 3 4 5 6 7 8 9 10 11 12 List<Integer> preorder (TreeNode root) { List<Integer> res = new LinkedList <>(); if (root == null ) { return res; } res.add(root.val); res.addAll(preorder(root.left)); res.addAll(preorder(root.right)); return res }
3.2 深入理解前中后序 前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点 ,绝不仅仅是三个顺序不同的List
前序位置的代码在刚刚进入一个二叉树节点的时候执行;
后序位置的代码在将要离开一个二叉树节点的时候执行;
中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
3.3 后序加强 前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据 。
1 2 3 4 5 6 7 8 9 10 11 12 13 int count(TreeNode root) { if (root == null) { return 0 ; } int leftCount = count(root.left); int rightCount = count(root.right); printf("节点 %s 的左子树有 %d 个节点,右子树有 %d 个节点" , root, leftCount, rightCount); return leftCount + rightCount + 1 ; }
只有后序位置才能通过返回值获取子树的信息。换句话说,一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了 。
遇到子树问题,首先想到的是给函数设置返回值,然后在后序位置做文章。
3.4 DFS 又是什么 动态规划/DFS/回溯算法 都可以看做二叉树问题的扩展,只是它们的关注点不同:
动态规划算法属于分解问题的思路,它的关注点在整棵「子树」。 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」。 DFS 算法属于遍历的思路,它的关注点在单个「节点」。 动态规划 1 2 3 4 5 6 7 8 9 10 11 int count(TreeNode root) { if (root == null) { return 0 ; } int leftCount = count(root.left); int rightCount = count(root.right); return leftCount + rightCount + 1 ; }
动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」。
回溯算法 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 void traverse(TreeNode root) { if (root == null) return ; printf("从节点 %s 进入节点 %s" , root, root.left); traverse(root.left); printf("从节点 %s 回到节点 %s" , root.left, root); printf("从节点 %s 进入节点 %s" , root, root.right); traverse(root.right); printf("从节点 %s 回到节点 %s" , root.right, root); } void backtrack(...) { for (int i = 0 ; i < ...; i++) { ... backtrack(...); ... } }
这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」。
DFS (深度优先搜索) 1 2 3 4 5 6 7 8 9 void traverse (TreeNode root) { if (root == null ) return ; root.val++; traverse(root.left); traverse(root.right); }
这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」。
3.5 DFS 和回朔区别 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void dfs (Node root) { if (root == null ) return ; print("我已经进入节点 %s 啦" , root) for (Node child : root.children) { dfs(child); } print("我将要离开节点 %s 啦" , root) } void backtrack (Node root) { if (root == null ) return ; for (Node child : root.children) { print("我站在节点 %s 到节点 %s 的树枝上" , root, child) backtrack(child); print("我将要离开节点 %s 到节点 %s 的树枝上" , child, root) } }
看到了吧,你回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点?
3.6 层序遍历 (BFS) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void levelTraverse (TreeNode* root) { if (root == nullptr ) return ; queue<TreeNode*> q; q.push (root); while (!q.empty ()) { int sz = q.size (); for (int i = 0 ; i < sz; i++) { TreeNode* cur = q.front (); q.pop (); if (cur->left != nullptr ) { q.push (cur->left); } if (cur->right != nullptr ) { q.push (cur->right); } } } }
4. 参考资料