先在开头总结一下,二叉树解题的思维模式分两类:
1、是否可以通过遍历一遍二叉树得到答案 ?如果可以,用一个 traverse
函数配合外部变量来实现,这叫「遍历」的思维模式。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案 ?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
无论使用哪种思维模式,你都需要思考:
如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做 ?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
下文通过几道简单的题目,实践运用这几条总纲,理解「遍历」和「分解问题」的思维又和区别和联系
二叉树相关的题目,无非是在二叉树遍历时的前序/中序/后续位置做文章,当然这并不能包含所有的,但是大部分如此,万变不离其宗,因此我们可以定义出一个基础框架:
1 2 3 4 5 6 7 8 9 10 func traverse (root *TreeNode) { if root == nil { return } traverse(root.Left) traverse(root.Right) }
先不管所以为的前中后序,单看traverse
,他在做的无非就是把整个二叉树所有节点遍历一遍,本质上与你遍历数组和链表类似。
解题思路:
经典二叉树前序遍历,二叉树入门级题目,套入上文的框架,前序遍历则在前序位置将节点放入数组即可
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var list []int func preorderTraversal (root *TreeNode) []int { preorder(root) return list } func preorder (node *TreeNode) { if node == nil { return } list = append (list, node.Val) preorder(node.Left) preorder(node.Right) }
上述代码使用了全局变量
,在leetcode
提交时会有问题,稍微改造一下即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func preorderTraversal (root *TreeNode) []int { var list []int var preorder func (node *TreeNode) preorder = func (node *TreeNode) { if node == nil { return } list = append (list, node.Val) preorder(node.Left) preorder(node.Right) } preorder(root) return list }
解题思路:
二叉树的前序遍历,是先拿出根节点,再拿到左子树的节点,再拿到右子树的节点,将他们三个合并到一起,就是一颗二叉树的前序遍历。我们把一整颗二叉树的遍历分解成一个又一个小数的遍历,最后合并到一起,这就是分解的思路
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func preorderTraversal (root *TreeNode) []int { var res []int if root == nil { return res } left := preorderTraversal(root.Left) right := preorderTraversal(root.Right) res = append (res, root.Val) res = append (res, left...) res = append (res, right...) return res }
解题思路:
两种思路与上一题都一致,唯一不同时根据顺序调整插入当前节点的位置
示例代码:
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 postorderTraversal (root *TreeNode) []int { var list []int var preorder func (node *TreeNode) preorder = func (node *TreeNode) { if node == nil { return } preorder(node.Left) preorder(node.Right) list = append (list, node.Val) } preorder(root) return list } func postorderTraversal2 (root *TreeNode) []int { var res []int if root == nil { return res } left := postorderTraversal2(root.Left) right := postorderTraversal2(root.Right) res = append (res, left...) res = append (res, right...) res = append (res, root.Val) return res }
解题思路:
定义两个全局变量res
和depth
,res
负责记录最大深度,depth
像一个游标,跟着递归遍历到每一个节点记录其深度,向下遍历时depth++
,返回时depth--
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unc maxDepth(root *TreeNode) int { var traverse func (root *TreeNode) var depth int var result int traverse = func (root *TreeNode) { if root == nil { return } depth++ if root.Left == nil && root.Right == nil { if depth > result { result = depth } } traverse(root.Left) traverse(root.Right) depth-- } traverse(root) return result }
解题思路:
一个二叉树的最大深度可以通过子树的最大深度推倒出来,所以我们只需要求出一个子树的最大深度即可
一个树的最大深度就是左子树或是右子树深度中最大值,在加上自身就是这颗树的最大深度
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 func maxDepth (root *TreeNode) int { if root == nil { return 0 } leftMax := maxDepth(root.Left) rightMax := maxDepth(root.Right) if leftMax < rightMax { return rightMax + 1 } return leftMax + 1 }
解题思路:
此题中二叉树的左右节点连接很简单,比如示例中的2 -> 3
,4 -> 5
,6 -> 7
,比较难得是左右节点子节点之间的连接,比如5 -> 6
的连接,采用分解问题的思路:
我们把两个二叉树中两个节点抽象成一个节点,这样一颗二叉树就变成了「三叉树」,然后我们遍历这颗「三叉树」,把每个「三叉树节点」中的连个节点链接就可以了,下图中的每个方块看做是一个节点
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func connect (root *Node) *Node { connectTraverse(root.Left, root.Right) return root } func connectTraverse (leftNode *Node, rightNode *Node) { if leftNode == nil || rightNode == nil { return } leftNode.Next = rightNode connectTraverse(leftNode.Left, leftNode.Right) connectTraverse(rightNode.Left, rightNode.Right) connectTraverse(leftNode.Right, rightNode.Left) }
思考:上述函数还有优化空间吗?
有,在连接相同父节点的两个子节点时,左子树的右节点,和右子树的左节点已经完成了其子节点的连接,在连接相邻节点的时候会重复,即leftNode.Right.Left -> leftNode.Right.Right
和rightNode.Left.Left -> rightNode.Left.Right
,可以小优化一下,但不优化也问题不大
解题思路:
示例代码:
1 2 3 4 5 6 7 8 9 func invertTree (root *TreeNode) *TreeNode { if root == nil { return root } root.Left, root.Right = root.Right, root.Left invertTree1(root.Left) invertTree1(root.Right) return root }
解题思路:
将树分解成N个树,每棵树先翻转自己的左右子树,然后交换左右子节点,放回翻转完的树,将他们组合起来就可以了。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 func invertTree (root *TreeNode) *TreeNode { if root == nil { return root } left := invertTree(root.Left) right := invertTree(root.Right) root.Left = right root.Right = left return root }
解题思路:
此题关键在于每一条二叉树的「直径」,就是一个节点的左右子树的最大深度之和
使用分解的思路,分别计算出每个节点的左右最大深度,相加之后加上自身返回给上级,组合起来就求出了每个节点的最大直径,取其中最大的一个即可
示例代码:
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 maxDiameter int func diameterOfBinaryTree (root *TreeNode) int { return diameter(root) } func diameter (node *TreeNode) int { if node == nil { return 0 } leftDiameter := diameter(node.Left) rightDiameter := diameter(node.Right) diameter := leftDiameter + rightDiameter maxDiameter = max(maxDiameter, diameter) if leftDiameter > rightDiameter { return leftDiameter + 1 } return max(leftDiameter, rightDiameter) + 1 } func max (a, b int ) int { if a > b { return a } return b }
参考链接: