[Golang修仙之路] 算法专题:二叉树

91 阅读10分钟

开一个帖子,后续更新

我这帖子都是看灵神的课,刷灵神的题单,做的一些笔记。

如果能找到和「原问题」相似的「子问题」,并建立联系,我们就能用「递归」解决

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,在递归的过程中就计算了。

叶子结点计算:

题目:129. 求根节点到叶节点数字之和

代码:

/**
 * 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
}

递归的过程中计算:

题目:1022. 从根到叶的二进制数之和

代码:

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种可能:

  • 当前值 - 最小值
  • 最大值 - 当前值

然后再用当前值去更新最大值最小值,即可

例题:1026. 节点与其祖先之间的最大差值

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 二叉树本质上是一堆指针

要对二叉树进行插入或者删除,可以把需要更新的节点全部保存到一个「指针数组中」,然后再执行简单的「链表的插入操作」即可。

例题:623. 在二叉树中增加一行

代码:

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. 二叉树的直径

二叉树的直径:任意两个节点之间的最长链路。

image.png

结论1: 直径一定至少包含一个叶子结点。

反证法即可,如果直径不包含叶子结点,一定可以再延伸包含叶子结点,得到大于当前长度的路径。有了这一点,很容易就得到了结论2.

结论2: 以当前节点为「拐弯点」的最长路径 = 左子树最大深度 + 右子树最大深度 + 2

有了结论2,就可以得到算法的雏形。

算法

枚举每个点,假设它是「拐弯点」,求最长路径。在这些最长路径中取最长的,就是整棵二叉树的「直径」。

注意返回的是最长链(不拐弯),但我们要的答案是最长路径(拐弯)。

灵神讲解的截图:

image.png

代码:自己看懂了算法之后写的,意思一样,灵神的更简洁。

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. 二叉树最大路径和

image.png

更新的是答案,即「最大路径和」。 返回的是「最大链路和」,技巧是:如果最大链路和是一个负数,那么返回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. 最近公共祖先

最近公共祖先是分类讨论

image.png

6.1 二叉搜索树的最近公共祖先

二叉搜索树有个特点:可以根据当前节点值,判断需要往哪边递归

image.png

代码:

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 验证二叉搜索树

核心思路:前序遍历,卡住合法区间

image.png

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 就可以快速找到
  • 如果是给你一个前序遍历的结果,那每次以当前下标作为 中间节点, 就必须划分出 左子树 和 右子树,那就双指针遍历就完事儿了。

例题1:1008. 前序遍历构造二叉搜索树

代码: 感觉不是很优雅,如果把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
}

另一个题目:根据后序和中序创建二叉树,和此题十分类似。