开一个帖子,后续更新
我这帖子都是看灵神的课,刷灵神的题单,做的一些笔记。
如果能找到和「原问题」相似的「子问题」,并建立联系,我们就能用「递归」解决
1. 二叉树的递归遍历
1.1 模板
func preorderTraversal(root *TreeNode) (vals []int) {
var preorder func(*TreeNode)
preorder = func(node *TreeNode) {
if node == nil {
return
}
vals = append(vals, node.Val)
preorder(node.Left)
preorder(node.Right)
}
preorder(root)
return
}
为啥最好在内部用一个闭包?
因为,如果数组用一个全局变量表示,全局变量需要在每次调用函数的时候都清空。否则多次调用函数,但是使用同一个全局变量,是错误的。
用全局变量的写法如下(不建议):
var ans []int func preorderTraversal(root *TreeNode) []int { ans = ans[:0] dfs(root) return ans } func dfs(root *TreeNode) { if root == nil { return } ans = append(ans, root.Val) dfs(root.Left) dfs(root.Right) }
1.2 相关知识(带例题)
1.2.1 Golang对参数修改,需要传入指针
用 星号 解引用,用 & 表示指针。
例题:872. 叶子相似的树
代码:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func leafSimilar(root1 *TreeNode, root2 *TreeNode) (ans bool) {
a1, a2 := []int{}, []int{}
var dfs func(r *TreeNode, a *[]int)
dfs = func(r *TreeNode, a *[]int) {
if r == nil {
return
}
if r.Left == nil && r.Right == nil {
*a = append(*a, r.Val)
return
}
dfs(r.Left, a)
dfs(r.Right, a)
}
dfs(root1, &a1)
dfs(root2, &a2)
// fmt.Println(a1, a2)
ans = true
if len(a1) != len(a2) {
return false
}
for k, v := range a1 {
if a2[k] != v {
ans = false
break
}
}
return
}
1.2.2 二叉树递归遍历,可以通过参数保存parent节点
例题:404. 左叶子之和
代码:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func sumOfLeftLeaves(root *TreeNode) int {
var dfs func(r *TreeNode, pre *TreeNode)
ans := 0
dfs = func(r *TreeNode, pre *TreeNode) {
if r == nil {
return
}
if pre != nil && r.Left == nil && r.Right == nil && pre.Left == r {
ans += r.Val
return
}
dfs(r.Left, r)
dfs(r.Right, r)
}
dfs(root, nil)
return ans
}
「指针」因为是内存地址,所以可以用 == 来比较相等。而所有二叉树的题目,传入的节点基本都是「指针」
2. 自顶向下DFS
2.1 在递的过程中维护「值」
这个「值」其实有说法:
- 可以是一个 path 数组,到了叶子结点再进行计算。
- 也可以是一个变量 x,在递归的过程中就计算了。
叶子结点计算:
代码:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func sumNumbers(root *TreeNode) int {
sum := 0
var dfs func(r *TreeNode, path []int)
dfs = func(r *TreeNode, p []int) {
if r == nil {
return
}
p = append(p, r.Val)
if r.Left == nil && r.Right == nil {
sum += getNum(p)
return
}
dfs(r.Left, p)
dfs(r.Right, p)
p = p[:len(p)-1]
}
path := []int{}
dfs(root, path)
return sum
}
func getNum(a []int) int {
ans := 0
for i := 0; i < len(a); i++ {
ans = ans * 10 + a[i]
}
return ans
}
递归的过程中计算:
代码:
func sumRootToLeaf(root *TreeNode) int {
var dfs func(r *TreeNode, x int)
sum := 0
dfs = func(r *TreeNode, x int) {
if r == nil {
return
}
x = x * 2 + r.Val
if r.Left == r.Right {
sum += x
return
}
dfs(r.Left, x)
dfs(r.Right, x)
}
dfs(root, 0)
return sum
}
2.2 技巧
技巧篇算不得知识,但是没有这些,很多题真是无从下手。
2.2.1 求差值的题,往往转化成找最大值和最小值
差值的绝对值的最大值,只有2种可能:
- 当前值 - 最小值
- 最大值 - 当前值
然后再用当前值去更新最大值最小值,即可
func maxAncestorDiff(root *TreeNode) int {
var dfs func(r *TreeNode, maxNum, minNum int)
ans := 0
dfs = func(r *TreeNode, maxNum, minNum int) {
if r == nil {
return
}
if maxNum != -1 && minNum != 10001 {
ans = max(ans, r.Val - minNum)
ans = max(ans, maxNum - r.Val)
}
maxNum = max(maxNum, r.Val)
minNum = min(minNum, r.Val)
dfs(r.Left, maxNum, minNum)
dfs(r.Right, maxNum, minNum)
}
dfs(root, -1, 10001)
return ans
}
2.2.2 二叉树本质上是一堆指针
要对二叉树进行插入或者删除,可以把需要更新的节点全部保存到一个「指针数组中」,然后再执行简单的「链表的插入操作」即可。
代码:
func addOneRow(root *TreeNode, val int, depth int) *TreeNode {
if depth == 1 {
t := &TreeNode{
Val: val,
Left: root,
}
return t
}
level := []*TreeNode{}
var dfs func(r *TreeNode, curDepth int)
dfs = func(r *TreeNode, curDepth int) {
if r == nil {
return
}
if curDepth == depth-1 {
level = append(level, r)
return
}
dfs(r.Left, curDepth+1)
dfs(r.Right, curDepth+1)
}
dfs(root, 1)
for _, node := range level {
tr := &TreeNode{
Val: val,
Right: node.Right,
}
tl := &TreeNode{
Val: val,
Left: node.Left,
}
node.Left, node.Right = tl, tr
}
return root
}
这个题还告诉我一个事儿,就是:乍一看完全没思路的题目,别急着放弃。
3. 自底向上DFS
自底向上DFS,递归函数大概率需要有一个「返回值」
3.1 自底向上,删除结点
这个题巧妙利用了递归函数的返回值,当目标节点需要删除,递归函数返回nil,反之返回节点本身。
利用返回值更新当前节点的左右子树。
模板题:1325. 删除给定值的叶子节点
代码:
func removeLeafNodes(root *TreeNode, target int) *TreeNode {
if root == nil {
return nil
}
root.Left = removeLeafNodes(root.Left, target)
root.Right = removeLeafNodes(root.Right, target)
if root.Val == target && root.Left == nil && root.Right == nil {
return nil
}
return root
}
4. 二叉树的直径
二叉树的直径:任意两个节点之间的最长链路。
结论1: 直径一定至少包含一个叶子结点。
反证法即可,如果直径不包含叶子结点,一定可以再延伸包含叶子结点,得到大于当前长度的路径。有了这一点,很容易就得到了结论2.
结论2: 以当前节点为「拐弯点」的最长路径 = 左子树最大深度 + 右子树最大深度 + 2
有了结论2,就可以得到算法的雏形。
算法
枚举每个点,假设它是「拐弯点」,求最长路径。在这些最长路径中取最长的,就是整棵二叉树的「直径」。
注意返回的是最长链(不拐弯),但我们要的答案是最长路径(拐弯)。
灵神讲解的截图:
代码:自己看懂了算法之后写的,意思一样,灵神的更简洁。
func diameterOfBinaryTree(root *TreeNode) (ans int) {
// dfs更新的是「直径」
// dfs返回的是「最长链」
var dfs func(root *TreeNode) int
dfs = func(root *TreeNode) int {
if root == nil {
return -1
}
leftLen := dfs(root.Left)
rightLen := dfs(root.Right)
ans = max(ans, leftLen + rightLen + 2)
return max(leftLen, rightLen) + 1
}
_ = dfs(root)
return
}
4.1 感染:求必须包含某个点的二叉树的直径
例题:感染二叉树所需要的时间
代码:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
// 题目思路:
// 1.求出start节点的高度
// 2.把start节点的左右子树删除(start变成叶子结点)
// 3.求包含start节点的树的直径
func amountOfTime(root *TreeNode, start int) int {
startNode := root
// 找到start节点
var getStartNode func(root *TreeNode)
getStartNode = func(root *TreeNode) {
if root == nil {
return
}
if root.Val == start {
startNode = root
return
}
getStartNode(root.Left)
getStartNode(root.Right)
}
getStartNode(root)
// 求最大深度
var getDeep func(root *TreeNode) int
getDeep = func(root *TreeNode) int {
if root == nil {
return -1
}
l, r := getDeep(root.Left) + 1, getDeep(root.Right) + 1
return max(l, r)
}
deep := getDeep(startNode)
startNode.Left, startNode.Right = nil, nil
// 求直径(把目标节点的孩子去掉)
maxLen := 0
var getMaxLen func(root *TreeNode) (int, bool)
getMaxLen = func(root *TreeNode) (int, bool) {
if root == nil {
return -1, false
}
lLen, lFound := getMaxLen(root.Left)
rLen, rFound := getMaxLen(root.Right)
if root == startNode {
maxLen = max(maxLen, lLen + rLen + 2)
return max(lLen, rLen) + 1, true
}
if lFound || rFound {
maxLen = max(maxLen, lLen + rLen + 2)
if lFound {
return lLen + 1, true
}
return rLen + 1, true
}
return max(lLen, rLen) + 1, false
}
getMaxLen(root)
return max(maxLen, deep)
}
这个题就涉及到,不是简单的求直径,而是求「包含某个节点」的树的直径。
先复习一下求直径:“返回最长链,叶子结点最长链路=0;更新直径”
现在要求「包含某个节点」的直径,利用到Go的多返回值,返回「最长链」和「是否包含目标节点」,更新直径的思路不变。
// 要点1: 只有找到,才尝试更新答案
// 要点2: 左边找到了,最长链只能=左边+1,右边同理
maxLen := 0
var getMaxLen func(root *TreeNode) (int, bool)
getMaxLen = func(root *TreeNode) (int, bool) {
if root == nil {
return -1, false
}
lLen, lFound := getMaxLen(root.Left)
rLen, rFound := getMaxLen(root.Right)
if root == startNode {
maxLen = max(maxLen, lLen + rLen + 2)
return max(lLen, rLen) + 1, true
}
if lFound || rFound {
maxLen = max(maxLen, lLen + rLen + 2)
if lFound {
return lLen + 1, true
}
return rLen + 1, true
}
return max(lLen, rLen) + 1, false
}
getMaxLen(root)
5. 二叉树最大路径和
更新的是答案,即「最大路径和」。 返回的是「最大链路和」,技巧是:如果最大链路和是一个负数,那么返回0即可。
代码:
func maxPathSum(root *TreeNode) (ans int) {
ans = math.MinInt
var dfs func(root *TreeNode) int // 返回的是最大链和
dfs = func(root *TreeNode) int {
if root == nil {
return 0
}
lMax, rMax := dfs(root.Left), dfs(root.Right)
ans = max(ans, lMax + rMax + root.Val)
maxSum := max(lMax, rMax) + root.Val
if maxSum < 0 {
return 0
}
return maxSum
}
dfs(root)
return
}
6. 最近公共祖先
最近公共祖先是分类讨论
6.1 二叉搜索树的最近公共祖先
二叉搜索树有个特点:可以根据当前节点值,判断需要往哪边递归
代码:
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == p || root == q || root == nil {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
return right
}
7. 二叉搜索树
7.1 验证二叉搜索树
核心思路:前序遍历,卡住合法区间
func isValidBST(root *TreeNode) bool {
// 此题思路:前序遍历,参数传入合法区间
return dfs(root, math.MinInt, math.MaxInt)
}
func dfs (root *TreeNode, lower, upper int) bool {
if root == nil {
return true
}
x := root.Val
return x > lower && x < upper && dfs(root.Left, lower, x) && dfs(root.Right, x, upper)
}
7.2 性质:二叉搜索树的中序遍历是严格升序的
这个综合题:1382. 将二叉搜索树变平衡
就用到了这个性质,先获得严格升序的数组,再根据升序数组,获得平衡二叉搜索树。
代码:
func balanceBST(root *TreeNode) *TreeNode {
arr := []int{}
var inorder func(root *TreeNode)
inorder = func(root *TreeNode) {
if root == nil {
return
}
inorder(root.Left)
arr = append(arr, root.Val)
inorder(root.Right)
}
inorder(root)
var dfs func(nums []int) *TreeNode
dfs = func(nums []int) *TreeNode {
if len(nums) == 0 {
return nil
}
i := len(nums)/2
node := &TreeNode{
Val: nums[i],
Left: dfs(nums[:i]),
Right: dfs(nums[i+1:]),
}
return node
}
return dfs(arr)
}
7.3 根据数组构建二叉搜索树
核心是 找到那个「中间节点」
- 如果是给你一个有序数组,那么直接通过 len(arr)/2 就可以快速找到
- 如果是给你一个前序遍历的结果,那每次以当前下标作为 中间节点, 就必须划分出 左子树 和 右子树,那就双指针遍历就完事儿了。
代码: 感觉不是很优雅,如果把j,k通过参数传入会好一些。
func bstFromPreorder(preorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
i, j, k := 0, 0, 0
for j < len(preorder) {
if preorder[j] < preorder[i] {
break
}
j++
}
for k < len(preorder) {
if preorder[k] > preorder[i] {
break
}
k++
}
var left *TreeNode
if j > k {
left = nil
} else {
left = bstFromPreorder(preorder[j:k])
}
return &TreeNode{
Val: preorder[i],
Left: left,
Right: bstFromPreorder(preorder[k:]),
}
}
例题2: 108. 将有序数组转换为二叉搜索树
这样构造出来,不仅仅是二叉搜索,还是平衡的二叉搜索。
代码:
func sortedArrayToBST(nums []int) *TreeNode {
if len(nums) == 0 {
return nil
}
n := len(nums)
node := &TreeNode{
Val: nums[n / 2],
}
node.Left = sortedArrayToBST(nums[:n/2])
node.Right = sortedArrayToBST(nums[n/2 + 1:])
return node
}
8. 创建二叉树
8.1 根据前序和中序创建二叉树
这是一个很经典的递归问题:
- 有了前序和中序,就可以分别找到左右子树各自的前序和中序,然后递归。(把原问题,转化成了更小的子问题)
- 最小的字问题是 空数组, return nil
- 有一个api:slices.Index(数组, 值) 返回下标。知道这个,代码写起来会比较简洁。
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
// slices.Index(数组, 值) 返回下标
leftSize := slices.Index(inorder, preorder[0])
node := &TreeNode{Val: preorder[0]}
node.Left = buildTree(preorder[1:1+leftSize], inorder[:leftSize])
node.Right = buildTree(preorder[1+leftSize:], inorder[leftSize+1:])
return node
}
另一个题目:根据后序和中序创建二叉树,和此题十分类似。