ShannonChenCHN / algorithm-and-data-structure

Algorithms + Data Structures = Programs
2 stars 1 forks source link

算法练习:树(Tree) #20

Open ShannonChenCHN opened 3 years ago

ShannonChenCHN commented 3 years ago

题目列表

1. 二叉树

  1. 二叉搜索树

  2. 见 #25

  3. 平衡二叉树

  4. 其他

总结

  1. 关于树的一些基本概念一定要掌握,比如高度、深度、层数、叶子节点。
  2. 需要知道什么是二叉搜索树、平衡二叉树、完全二叉树和满二叉树,像红黑树这类了解即可。
  3. 完全二叉树的由来(如何表示/存储一棵二叉树?)
  4. 根据二叉搜索树的典型特征,中序遍历可以得到一个递增序列。一般来讲,只要遇到二叉搜索树的题目,马上就要能想到两点:
    • 中序遍历得到的序列是单调递增的
    • 所有的左子树上的节点值都小于根节点,所有的右子树上的节点值都大于根节点
  5. 需要熟练掌握二叉树的前序、中序、后序遍历的递归实现(迭代实现了解即可),以及层序遍历的实现,这两类遍历对应的也就是常说的 DFS 和 BFS,绝大部分二叉树的题目都是考察这两点的。另外还需要熟练掌握这类算法的复杂度分析。
  6. 只要是二叉树的题目,第一反应就是递归怎么实现?递推公式怎么写?
  7. 上面题目列表中标记 ⭐️ 的题目都是典型的二叉树的题目,需要多做几遍,达到非常熟练的程度。
ShannonChenCHN commented 3 years ago

理论部分(一)

参考:

一、树的基本概念

image image

二、分类

三、二叉树

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

满二叉树:叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,如下图所示。

            A
          /   \
        B       C
      /   \    /  \
     D    E   F    G
    / \  / \ / \  / \
   H  I  J K L M  N  O

完全二叉树:叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。

            A
          /   \
        B       C
      /   \    /  \
     D    E   F    G
    / \  / \ 
   H  I  J K 

Swift 实现的二叉树:

/// Definition for a binary tree node.
class TreeNode<T> {
    var data: T
    var left: TreeNode<T>?
    var right: TreeNode<T>?

    init(_ data: T, _ left: TreeNode?, _ right: TreeNode?) {
        self.data = data
        self.left = left
        self.right = right
    }

    convenience init(_ data: T) {
        self.init(data, nil, nil)
    }
}

二叉树的可视化:

extension TreeNode: CustomStringConvertible {
    var description: String {
        return toString()
    }

    /// Print a [large] tree by lines.
    /// See  https://stackoverflow.com/a/8948691
    func toString(_ content: String = "", _ prefix: String = "", _ childrenPrefix: String = "") -> String {

        var content = content
        content += prefix
        content += "\(data)"
        content += "\n"
        var children = [TreeNode<T>]()
        if let left = left {
            children.append(left)
        }
        if let right = right {
            children.append(right)
        }
        for (i, child) in children.enumerated() {
            if i == children.count - 1 {
                content = child.toString(content, childrenPrefix + "└── ", childrenPrefix + "    ")
            } else {
                content = child.toString(content, childrenPrefix + "├── ", childrenPrefix + "│   ")
            }
        }
        return content
    }
}

打印出来的效果:

15
├── 14
│   ├── 13
│   └── 15
└── 16
    ├── 15
    └── 17

四、完全二叉树的由来(如何表示/存储一棵二叉树?)

想要存储一棵二叉树,我们有两种方法:

链式存储法示意图: image

顺序存储法示意图: image

如图所示,如果节点 X 存储在数组中下标为 i 的位置,下标为 2 i 的位置存储的就是左子节点,下标为 2 i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。

图中的例子是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置。如果是非完全二叉树,其实会浪费比较多的数组存储空间。

所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。

补充:后面要讲到的堆也是一种完全二叉树,最常用的存储方式就是数组。

五、二叉树的遍历(重点)

二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。

四种遍历方式:

image

实际上,二叉树的前、中、后序遍历就是一个递归的过程。 这三种遍历方式的递推公式如下:


前序遍历的递推公式:
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)

中序遍历的递推公式:
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)

后序遍历的递推公式:
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r

从上面的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)。

关于前、中、后序遍历,以及层序遍历的实现,详见下面的练习题。

ShannonChenCHN commented 3 years ago

理论部分(二)

参考:

一、二叉查找树简介

二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。

二叉查找树:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。

            13
          /   \
        10       16
      /   \    /  \
     9    11  14 

-----------------------
          16
          /   
        10       
      /   \   
     9    13  
         / \ 
        11 14

二、二叉查找树的查找操作

原理跟二分查找有几分相似,详见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714

image

三、二叉查找树的插入操作

二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。

详见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714 image

四、二叉查找树的删除操作

针对要删除节点的子节点个数的不同,我们需要分三种情况来处理:

代码实现见:https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714

image

五、二叉查找树的其他操作

除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。(代码实现见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714

二叉查找树除了支持上面几个操作之外,还有一个重要的特性,就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。因此,二叉查找树也叫作二叉排序树。

六、支持重复数据的二叉查找树

详见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714

七、二叉查找树的时间复杂度分析

不管操作是插入、删除还是查找,时间复杂度其实都跟树的高度成正比,也就是 O(height)。

借助等比数列的求和公式,我们可以计算出,L 的范围是 [log2(n+1), log2n +1]。完全二叉树的层数小于等于log2n +1,也就是说,完全二叉树的高度小于等于 log2n

平衡二叉查找树的高度接近 logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。

ShannonChenCHN commented 3 years ago

理论部分(三)

参考:

ShannonChenCHN commented 3 years ago

144. 二叉树的前序遍历

2021.02.21 05:00 pm ~ 06:30 pm 2021.02.21 07:30 pm ~ 08:30 pm

解法一:递归法

分析:

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)

Swift 代码实现:

class Solution {
    // 前序遍历:根-左-右
    // 递推公式:preorderTraversal(root, &list) = list.append(root.val) + preorderTraversal(root.left, &list) + preorderTraversal(root.right, &list)
    // 终止条件:节点为空
    func preorderTraversal(_ root: TreeNode?) -> [Int] {
        var result = [Int]()
        preorderTraversal(root, &result)
        return result
    }

    func preorderTraversal(_ root: TreeNode?, _ list: inout [Int]) {
        guard let root = root else {
            return
        }

        list.append(root.val)
        preorderTraversal(root.left, &list)
        preorderTraversal(root.right, &list)
    }
}

解法二:迭代法

任何算法的递归版本都可以改成非递归版本,因为函数递归调用其实质就是压栈的过程,那么我们完全可以使用堆栈来模拟这个过程。二叉树的遍历(递归法)很容易实现,本质是采用了栈帧的实现方式,函数调用就是压栈,函数求解就是出栈的过程,那么我们完全可以手动创建栈来模拟这个过程,按照指定遍历顺序迭代的访问二叉树的所有结点。

分析:

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),空间主要是迭代过程中“模拟栈”的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)

图解示例:

            A
          /   \
        B       C
      /   \    /  \
     D    E   F    G
    / \  / \ 
   H  I  J K 

   stack            pop    push     list 

   [A]              A      C, B     [A]
   [C, B]           B      E, D     [A, B]
   [C, E, D]        D      I, H     [A, B, D]
   [C, E, I, H]     H       /       [A, B, D, H]
   [C, E, I]        I       /       [A, B, D, H, I]
   [C, E]           E      K, J     [A, B, D, H, I, E]
   [C, K, J]        J       /       [A, B, D, H, I, E, J]
   [C, K]           K       /       [A, B, D, H, I, E, J, K]
   [C]              C      G, F     [A, B, D, H, I, E, J, K, C]
   [G, F]           F       /       [A, B, D, H, I, E, J, K, C, F]
   [G]              G       /       [A, B, D, H, I, E, J, K, C, F, G]

Swift 代码实现:

class Solution {
    /// 前序遍历:根-左-右
    /// 用栈来模拟递归法中的函数压栈过程
    /// 循环终止条件:栈为空
    /// 循环中的重复内容:其实就是递归的重复内容,取出栈顶的节点值存入 list 中,将右节点压入栈中,将左节点压入栈中
    func preorderTraversal(_ root: TreeNode?) -> [Int] {
        guard let root = root else {
            return []
        }
        var list = [Int]()
        var stack = [TreeNode]()
        stack.append(root)
        while !stack.isEmpty {
            let top = stack.popLast()!
            list.append(top.val)
            if let right = top.right {
                stack.append(right)
            }
            if let left = top.left {
                stack.append(left)
            }
        }

        return list
    }
}

解法三:Morris 遍历

TODO

ShannonChenCHN commented 3 years ago

94. 二叉树的中序遍历

2021.02.21 05:00 pm ~ 06:30 pm 2021.02.21 07:30 pm ~ 08:30 pm

image image

解法一:递归法

分析:

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),控件小号主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)

Swift 代码实现:

class Solution {
    /// 中序遍历:左-根-右
    /// 递推公式:inorderTraversal(root, &list) = inorderTraversal(root.left, &list) + list.append(root.val) + inorderTraversal(root.right, &list)
    /// 终止条件:节点为空
    func inorderTraversal(_ root: TreeNode?) -> [Int] {
        var list = [Int]()
        inorderTraversal(root, &list)
        return list
    }

    func inorderTraversal(_ root: TreeNode?, _ list: inout [Int]) {
        guard let root = root else {
            return 
        }

        inorderTraversal(root.left, &list)
        list.append(root.val)
        inorderTraversal(root.right, &list)
    }
}

解法二:迭代法

任何算法的递归版本都可以改成非递归版本,因为函数递归调用其实质就是压栈的过程,那么我们完全可以使用堆栈来模拟这个过程。二叉树的遍历(递归法)很容易实现,本质是采用了栈帧的实现方式,函数调用就是压栈,函数求解就是出栈的过程,那么我们完全可以手动创建栈来模拟这个过程,按照指定遍历顺序迭代的访问二叉树的所有结点。

分析:

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),空间主要是迭代过程中“模拟栈”的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

图解示例:

            A
          /   \
        B       C
      /   \    /  \
     D    E   F    G
    / \  / \ 
   H  I  J K 

  cur    stack            pop      list

   A     [A, B, D, H]      H       [H]
  nil    [A, B, D]         D       [H, D]
   I     [A, B, I]         I       [H, D, I]
  nil    [A, B]            B       [H, D, I, B]
   E     [A, E, J]         J       [H, D, I, B, J]
  nil    [A, E]            E       [H, D, I, B, J, E]
   K     [A, K]            K       [H, D, I, B, J, E, K]
  nil    [A]               A       [H, D, I, B, J, E, K, A]
   C     [C, F]            F       [H, D, I, B, J, E, K, F]
  nil    [C]               C       [H, D, I, B, J, E, K, F, C]
   G     [G]               G       [H, D, I, B, J, E, K, F, C, G]

Swift 代码实现:

class Solution {
    /// 终止条件:栈为空(也就是根节点左侧所有的节点都遍历完了),并且 curr 指针为空(也就是所有的右子节点也访问完了)
    /// 循环中重复的内容:将所有的左子节点压入栈中,直到访问到最左的叶节点,然后再将 cur 指针指向右子节点
    func inorderTraversal(_ root: TreeNode?) -> [Int] {
        var list = [Int]()
        var curr = root
        var stack = [TreeNode]()
        while !stack.isEmpty || curr != nil {
            // 左:将所有的左子节点压入栈中,并找到当前节点的左叶子节点
            var temp = curr
            while temp != nil {
                stack.append(temp!)
                temp = temp!.left
            }

            // 根:访问 curr 的左叶节点
            let top = stack.popLast()!
            list.append(top.val)

            // 右:将 cur 指针指向右子节点
            curr = top.right
        }
        return list
    }
}

解法三:Morris 遍历

TODO

ShannonChenCHN commented 3 years ago

145. 二叉树的后序遍历

2021.02.21 05:00 pm ~ 06:30 pm 2021.02.27 04:40 pm ~ 06:40 pm

image image

解法一:递归法

分析:

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),控件小号主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)

class Solution {
    /// 后序遍历:左-右-根
    /// 递推公式:postorderTraversal(root, &list) = postorderTraversal(root.left, &list) + list.append(root.val) + postorderTraversal(root.right, &list)
    /// 终止条件:节点为空
    func postorderTraversal(_ root: TreeNode?) -> [Int] {
        var list = [Int]()
        postorderTraversal(root, &list)
        return list
    }

    func postorderTraversal(_ root: TreeNode?, _ list: inout [Int]) {
        guard let root = root else {
            return
        }

        postorderTraversal(root.left, &list)
        postorderTraversal(root.right, &list)
        list.append(root.val)
    }
}

解法二:迭代法

参考题解

分析

时间复杂度O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度O(n),空间主要是迭代过程中“模拟栈”的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

class Solution {
    /// 后序遍历:左-右-根
    /// 终止条件:栈为空(也就是根节点左侧所有的节点都遍历完了),并且 cur 指针为空(也就是所有的右子节点也访问完了)
    /// 循环中重复的内容:将所有的左子节点压入栈中,直到访问到最左的叶节点,然后再判断左叶子节点是否没有右子节点,或
    ///                者右子节点是否已经被访问过,如果为是,则保存该节点的值,反之则将 cur 指针指向右子节点
    func postorderTraversal(_ root: TreeNode?) -> [Int] {
        guard let root = root else {
            return []
        }

        var result = [Int]()
        var cur: TreeNode? = root
        var stack = [TreeNode]()
        var prev: TreeNode? = nil
        while !stack.isEmpty || cur != nil {
            // 找到左叶子节点
            var tmp = cur
            while tmp != nil {
                stack.append(tmp!)
                tmp = tmp!.left
            }
            cur = stack.last!

            // 判断左叶子节点是否没有右子节点,或者右子节点已经被访问过
            if cur!.right == nil || cur!.right! === prev {
                result.append(cur!.val)
                prev = cur
                cur = nil
                stack.popLast()
            } else {
                cur = cur?.right
            }
        }

        return result
    }
}

解法三:Morris 遍历

TODO

ShannonChenCHN commented 3 years ago

剑指 Offer 32 - I. 从上到下打印二叉树

2021.02.27 07:10 pm ~ 07:40 pm

image

基本解法:借助队列

循环终止条件:最后一层也遍历完了,也就是队列中没有节点数据 循环内容:当前层节点出队列,并且将下一层的节点加入到队列末尾

时间复杂度:O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度:最坏情况下为 O(n)(也就是满二叉树的情况,叶子节点那一层的个数为 2^k≈ n),最好情况下树退化成链表,为 O(1)。

class Solution {
    func levelOrder(_ root: TreeNode?) -> [Int] {
        guard let root = root else {
            return []
        }

        var result = [Int]()
        var queue = [TreeNode]()
        queue.append(root) // 第一层
        while !queue.isEmpty {
            var nextLevelNodes = [TreeNode]() // 用于临时保存下一层的节点
            for node in queue { // 访问当前层所有节点
                result.append(node.val)

                if let left = node.left {
                    nextLevelNodes.append(left)
                }

                if let right = node.right {
                    nextLevelNodes.append(right)
                }
            }

            // 更新队列到下一层
            queue = nextLevelNodes
        }

        return result
    }
}

简洁版

class Solution {
    func levelOrder(_ root: TreeNode?) -> [Int] {
        guard let root = root else {
            return []
        }

        var result = [Int]()
        var queue = [TreeNode]()
        queue.append(root) // 第一层
        while !queue.isEmpty {
            let node = queue.removeFirst()
            result.append(node.val)

            // 保存下一层的节点
            if let left = node.left {
                queue.append(left)
            }

            if let right = node.right {
                queue.append(right)
            }
        }

        return result
    }
}
ShannonChenCHN commented 3 years ago

104. 二叉树的最大深度

注:本题同剑指 Offer 55 - I. 二叉树的深度

2021.02.27 07:45 pm ~ 08:15 pm

image

解法一:递归法(深度优先)

递推公式depth(node) = 1 + max(depth(left), depth(right)) 终止条件node == nil

时间复杂度:O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度:空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

class Solution {
    /// 深度优先的实现(递归)
    /// 递推公式:depth(node) = 1 + max(depth(left), depth(right))
    /// 终止条件:node == nil
    func maxDepth(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        return 1 + max(maxDepth(root.left), maxDepth(root.right))
    }
}

解法二:迭代法(广度优先)

循环终止条件:最后一层也遍历完了,也就是队列中没有节点数据 循环内容:深度加 1,当前层节点出队列,并且将下一层的节点加入到队列末尾

时间复杂度:O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度:最坏情况下为 O(n)(也就是满二叉树的情况,叶子节点那一层的个数为 2^k≈ n),最好情况下树退化成链表,为 O(1)。

class Solution {
    /// 广度优先的实现(迭代)
    /// 循环终止条件:最后一层也遍历完了,也就是队列中没有节点数据
    /// 循环内容:深度加 1,当前层节点出队列,并且将下一层的节点加入到队列末尾
    func maxDepth(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        var depth = 0
        var queue = [TreeNode]()
        queue.append(root) // 将第一层的节点加入到队列中
        while !queue.isEmpty {
            var nextLevelNodes = [TreeNode]() // 保存下一层的节点
            for node in queue {
                if let left = node.left {
                    nextLevelNodes.append(left)
                }

                if let right = node.right {
                    nextLevelNodes.append(right)
                }
            }
            depth += 1 // 每遍历完一层,深度加 1
            queue = nextLevelNodes // 更新队列
        }
        return depth
    }
}
ShannonChenCHN commented 3 years ago

101. 对称二叉树

注:本题同剑指 Offer 28. 对称的二叉树

2021.02.28 05:00 pm ~ 06:00 pm

image image

测试 case

输入:[1, null, null]
输入:[]
输入:[2, 3, 3]
输入:[2, 3, 3, 9, null, null, 9]

分析

对称二叉树的特点是:左子树和右子树是对称的,仔细观察可以发现,左子树中任一节点的左子节点跟右子树中任一节点的右子节点是相等的,同样,左子树中任一节点的右子节点跟右子树中任一节点的左子节点也是相等的

解法一:递归法

递推公式:isSymmetric(A, B) = (A.val == B.val) && isSymmetric(A.left, B.right) && isSymmetric(A.right, B.left) 终止条件:A == nil || B == nil

时间复杂度:最好的情况是 O(1),最坏的情况是 O(n),也就是遍历完所有节点,n 是二叉树的节点数。 空间复杂度:空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

class Solution {

    func isSymmetric(_ root: TreeNode?) -> Bool {
        return isSymmetricPair(root?.left, root?.right)
    }

    func isSymmetricPair(_ rootA: TreeNode?, _ rootB: TreeNode?) -> Bool {
        guard let treeA = rootA, let treeB = rootB else {
            return rootA == nil && rootB == nil
        }

        return (treeA.val == treeB.val) && 
        isSymmetric(treeA.left, treeB.right) && 
        isSymmetric(treeA.right, treeB.left)
    }
}

解法二:迭代法

循环内容:将队列中保存的左右子树单层的节点出列,比较相邻两个节点是否相等,然后再将下一层的节点加入队列 终止条件:队列中没有节点,或者出现相邻节点的值不相等的情况

时间复杂度:最好的情况是 O(1),最坏的情况是 O(n),也就是遍历完所有节点,n 是二叉树的节点数。 空间复杂度:这里需要用一个队列来临时节点,每个节点最多进队一次,出队一次,队列中最多不会超过 n 个点,故渐进空间复杂度为 O(n)。

class Solution {
    func isSymmetric(_ root: TreeNode?) -> Bool {

        var queue = [TreeNode?]()
        queue.append(root?.left)
        queue.append(root?.right)
        while !queue.isEmpty {
            let nodeA = queue.removeFirst()
            let nodeB = queue.removeFirst()

            if nodeA == nil && nodeB == nil {
                continue
            }

            if nodeA?.val != nodeB?.val {
                return false
            }

            queue.append(nodeA?.left)
            queue.append(nodeB?.right)
            queue.append(nodeA?.right)
            queue.append(nodeB?.left)
        }

        return true
    }
}
ShannonChenCHN commented 3 years ago

236. 二叉树的最近公共祖先

注:本题同剑指 Offer 68 - II. 二叉树的最近公共祖先

2021.02.28 07:10 pm ~2021.02.28 09:00 pm

image image

解法一:递归法

参考题解:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/solution/236-er-cha-shu-de-zui-jin-gong-gong-zu-xian-hou-xu/

若 root 是节点 A,B 的最近公共祖先 ,则可能为以下情况之一:

思路: 采用深度优先的方式进行遍历,也就是先访问子节点后访问父节点,所以我们可以使用递归法进行后序遍历:左-右-根

递归三要素:

复杂度跟后序遍历一样。 时间复杂度:O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度:O(n),控件小号主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

注意点:这道题比较难理解,需要多练习几次。


class Solution {

    func lowestCommonAncestor(_ root: TreeNode?, _ p: TreeNode?, _ q: TreeNode?) -> TreeNode? {
        guard root != nil else { // 访问到叶子节点下一层了
            return nil
        }

        guard root !== p && root !== q else { // 访问到 p 节点或者 q 节点了
            return root
        }

        let leftResult = lowestCommonAncestor(root?.left, p, q)
        let rightResult = lowestCommonAncestor(root?.right, p, q)

        if leftResult != nil && rightResult != nil {
            return root
        }

        if leftResult != nil {
            return leftResult
        }

        if rightResult != nil {
            return rightResult
        }

        return nil
    }
}
ShannonChenCHN commented 3 years ago

111. 二叉树的最小深度

2021.03.06 04:45 pm ~ 05:35 pm

image image

解法一:递归法

递推公式minDepth(root) = min(minDepth(root.left), minDepth(root.right)) + 1 终止条件:递归时需要考虑到三种情况,第一种是左右子树都不为空,就按正常的递推公式来,第二种是左右子树其中有一个为空,这种情况只需要加上不为空的那一个子树的 minDepth,第三种情况就是左右子树都为空,直接返回 1 即可。

时间复杂度:O(n),n 是二叉树的节点数。每一个节点恰好被遍历一次。 空间复杂度:O(n),空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

class Solution {
    func minDepth(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        if root.left == nil && root.right == nil {
            return 1
        }

        if root.left == nil {
            return 1 + minDepth(root.right);
        }
        if root.right == nil {
            return 1 + minDepth(root.left);
        }
        return 1 + min(minDepth(root.left), minDepth(root.right))
    }
}

解法二:迭代法

思路:层序遍历,开始遍历每一层时,记录层数,如果遇到叶节点(即左右子节点都为空的节点)时停止循环

时间复杂度:O(n),其中 n 是树的节点数。对每个节点访问一次。 空间复杂度:O(n),其中 n 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。

class Solution {
    func minDepth(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        var queue = [TreeNode]()
        var level = 0
        queue.append(root)
        while !queue.isEmpty {
            // 开始遍历新的一层
            level += 1
            var nextLevelQueue = [TreeNode]()
            for node in queue {
                // 只要发现叶子节点就立即返回结果
                if node.left == nil && node.right == nil {
                    return level
                }

                // 将下一层的节点加入到队列中
                if let left = node.left {
                    nextLevelQueue.append(left)
                }
                if let right = node.right {
                    nextLevelQueue.append(right)
                }
            }
            queue = nextLevelQueue
        }
        return level
    }
}
ShannonChenCHN commented 3 years ago

226. 翻转二叉树

注:本题同 剑指 Offer 27. 二叉树的镜像

2021.03.06 05:50 pm ~ 06:10 pm

image image

解法一:递归法(深度优先,后序遍历)

关于树的题目,一般怎么分析呢?首先观察是否可以通过递归法解决,我们可以从叶子节点往上分析,看是否可以找到共同规律。

思路:采用后序遍历,如果当前遍历到的节点 root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root 为根节点的整棵子树的翻转。

递推公式invertTree(root) = invertTree(root.left) + invertTree(root.right) + swap(root.left, root.right) 终止条件root.left == nil && root.right == nil

时间复杂度: O(N),其中 N 为二叉树节点的数目。我们会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。 空间复杂度: O(N)。使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(log N)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。

class Solution {
    func invertTree(_ root: TreeNode?) -> TreeNode? {
        guard let root = root else {
            return nil
        }

        if root.left == nil && root.right == nil {
            return root
        }

        invertTree(root.left)
        invertTree(root.right)

        let left = root.left
        root.left = root.right
        root.right = left

        return root
    }
}

解法二:迭代法(自顶向下)

因为翻转二叉树的操作实际上是将每个节点的左右子树进行对调,所以我们同样可以通过层序遍历来实现。

时间复杂度:同样每个节点都需要入队列/出队列一次,所以是 O(n) 空间复杂度:最坏的情况下队列中会包含所有的叶子节点,完全二叉树叶子节点是 n/2 个,所以时间复杂度是 0(n)

class Solution {
    func invertTree(_ root: TreeNode?) -> TreeNode? {
        guard let root = root else {
            return nil
        }

        var queue = [TreeNode]()
        queue.append(root)
        while !queue.isEmpty {
            var nextLevelQueue = [TreeNode]()
            for node in queue {
                // 翻转子树
                let tmpLeft = node.left
                node.left = node.right
                node.right = tmpLeft

                if let left = node.left {
                    nextLevelQueue.append(left)
                }
                if let right = node.right {
                    nextLevelQueue.append(right)
                }
            }

            queue = nextLevelQueue

        }
        return root
    }
}
ShannonChenCHN commented 3 years ago

113. 路径总和 II

注:本题同 剑指 Offer 34. 二叉树中和为某一值的路径

2021.03.06 08:30 pm ~ 09:00 pm

image image

解法一:递归法(先序遍历)

递推公式pathSum(root, sum) = pathSum(root.left, sum-root.val), pathSum(root.right, sum-root.val) 终止条件root.left == nil && root.right == nil && sum == root.value

时间复杂度:O(n)。其中 n 是树的节点数。对每个节点访问一次。(但是官方题解中说是 O(n^2),为何?) 空间复杂度:O(n)。空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

class Solution {
    var result = [[Int]]()
    var path = [Int]()

    func pathSum(_ root: TreeNode?, _ targetSum: Int) -> [[Int]] {
        guard let root = root else {
            return []
        }

        // 保存数据到路径数组中
        path.append(root.val)

        if root.left == nil && root.right == nil && root.val == targetSum {
            result.append(path) // 因为 Swift 中的数组和 Int 都是值类型,所以这里不用担心后面 removeLast 的影响
        }

        if let left = root.left {
            pathSum(root.left, targetSum-root.val)
        }
        if let right = root.right {
            pathSum(root.right, targetSum-root.val)
        }
        // 递归结束,之前保存的节点数据“出栈”
        path.removeLast()

        return result
    }
}
ShannonChenCHN commented 3 years ago

105. 从前序与中序遍历序列构造二叉树

注:本题同 剑指 Offer 07. 重建二叉树

2021.03.06 07:30 pm ~ 08:15 pm

image image

参考题解:

解法一:递归法

分析:

    前序遍历:[根, [左子树的前序遍历], [右子树的前序遍历]]
    中序遍历:[[左子树的中序遍历], 根, [右子树的中序遍历]]

只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。

递推公式:buildTree(preorder, inorder) = TreeNode(preorder.first, buildTree(preorder[1...i], inorder[0..<i]), buildTree(preorder[i+1..<n], inorder[i+1..<n])) 终止条件:preorder.isEmpty || inorder.isEmpty || preorder.count != inorder.count

时间复杂度: O(n),其中 n 是树的节点个数。 空间复杂度: O(n),除去返回的答案需要的 O(n) 空间之外,因为递归函数压栈,我们还需要使用 O(h)(其中 h 是树的高度)的空间存储栈。这里 h < n,所以(在最坏情况下退化成链表)总空间复杂度为 O(n)。最好的情况是完全二叉树,复杂度是 O(logn)。

class Solution {
    func buildTree(_ preorder: [Int], _ inorder: [Int]) -> TreeNode? {
        guard !preorder.isEmpty && !inorder.isEmpty && preorder.count == inorder.count else {
            return nil
        }

        let rootValue = preorder.first!
        let rootIndexInorder = inorder.firstIndex(of: rootValue)!

        var left: TreeNode? = nil
        if rootIndexInorder > 0 {
            let preorderLeft = Array(preorder[1...rootIndexInorder])
            let inorderLeft = Array(inorder[0..<rootIndexInorder])
            left = buildTree(preorderLeft, inorderLeft)
        }
        var right: TreeNode? = nil
        if rootIndexInorder < preorder.count - 1 {
            let preorderRight = Array(preorder[rootIndexInorder+1..<preorder.count])
            let inorderRight = Array(inorder[rootIndexInorder+1..<preorder.count])
            right = buildTree(preorderRight, inorderRight)
        }

        return TreeNode(rootValue, left, right)
    }
}

解法二:迭代法

TODO

ShannonChenCHN commented 3 years ago

297. 二叉树的序列化与反序列化

注:本题同剑指 Offer 37. 序列化二叉树

2021.03.07 07:20 pm ~ 09:10 pm

image image

class Codec {
    /*
    递归法+前序遍历
    输入:root = [1,2,3,null,null,4,5]
    输出:"1,2,3,#,#,4,#,#,5,#,#"
    递推公式:_serialize(root, &result) = result.append(root.val), _serialize(root.left, &result), _serialize(root.right, &result)
    终止条件:root == nil
    */
    func serialize(_ root: TreeNode?) -> String {
        guard let root = root else {
            return ""
        }

        var result = ""
        _serialize(root, &result)
        return result
    }

    func _serialize(_ root: TreeNode?, _ result: inout String) {
        if result.length > 0 {
            result += ","
        }

        guard let root = root else {
            result += "#"
            return
        }

        result += "\(root.val)"
        _serialize(root.left, &result)
        _serialize(root.right, &result)
    }

    /*
    前序遍历:[根, [左子树], [右子树]]

    递推公式:deserialize(data, node) = TreeNode(data.first), deserialize(data, &newNode.left), deserialize(data, &newNode.right)
    终止条件:data.length == 0
    */
    func deserialize(_ data: String) -> TreeNode? {
        guard data.length > 0 else {
            return nil
        }
        var root: TreeNode? = nil
        var str = data
        _deserialize(&str, &root)
        return root
    }

    func _deserialize(_ data: inout String, _ node: inout TreeNode?) {
        guard data.length > 0 else {
            return
        }

        var val = 0
        var negtive = false
        while !data.isEmpty {
            let firstChar = data.removeFirst()
            if firstChar == "," {
                break
            } else if firstChar == "-" { // 负数
                negtive = true
            } else if firstChar == "#" { // 空节点
                if !data.isEmpty {
                    data.removeFirst() // 删除逗号
                }
                return
            } else {
                val = val * 10 + Int(String(firstChar))!
            }
        }
        val = negtive ? -val : val 
        node = TreeNode(val)

        if node != nil {
            _deserialize(&data, &node!.left)
            _deserialize(&data, &node!.right)
        }

    }
}
ShannonChenCHN commented 3 years ago

剑指 Offer 26. 树的子结构

2021.03.07 03:45 pm ~ 04:20 pm

image image

递归法

分两步:1. 依次(先序)遍历树 A 的每个节点;2. 在遍历树 A 时,将各个节点作为子树的根节点与树 B 进行比较 所以,这里要用到两个递归。

递推公式1isSubStructure(A, B) = compare(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B) 终止条件1A == nil || B == nil

递推公式2compare(A, B) = (A.val == B.val) && compare(A.left, B.left) && compare(A.right, B.right) 终止条件2A == nil || B != nil

时间复杂度: O(M*N)。其中 M, N 分别为树 A 和 树 B 的节点数量;先序遍历树 A 占用 O(M) ,每次调用 compare(A, B) 判断占用 O(N) 。 空间复杂度: O(M)。当树 A 和树 B 都退化为链表时,递归调用深度最大。当 M ≤ N 时,遍历树 A 与递归判断的总递归深度为 M ;当 M > N 时,最差情况为遍历至树 A 叶子节点,此时总递归深度为 M。

class Solution {
    func isSubStructure(_ A: TreeNode?, _ B: TreeNode?) -> Bool {
        guard let _A = A, let _B = B else {
            return (A == nil && B == nil)
        }

        if compare(_A, _B) {
            return true
        }

        if isSubStructure(_A.left, _B) {
            return true
        }

        if isSubStructure(_A.right, _B) {
            return true
        }

        return false
    }

    func compare(_ A: TreeNode?, _ B: TreeNode?) -> Bool {
        guard let _A = A, let _B = B else {
            return !(A == nil && B != nil)
        }

        if _A.val != _B.val {
            return false
        } 

        if !compare(_A.left, _B.left) {
            return false
        }

        if !compare(_A.right, _B.right) {
            return false
        }

        return true
    }
}
ShannonChenCHN commented 3 years ago

98. 验证二叉搜索树

2021.03.13 11:50 am ~ 01:00 pm

image image

测试 case:

输入:[2, 2, 3] 
输出:false

输入:[5, 4, 6, null, null, 3, 7] 
输出:false

解法一:常规递归法

分析二叉搜索树的特征:

image

递推公式isValidBST(root, max, min) = root.val < max && root.val > min && isValidBST(root.left, root.val, min) && isValidBST(root.right, max, root.val) 终止条件root != nil

时间复杂度:O(n)。其中 n 是树的节点数。对每个节点访问一次。 空间复杂度:O(n)。空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。

参考题解:

class Solution {
    func isValidBST(_ root: TreeNode?) -> Bool {

      return isValidBST(root, Int.max, Int.min)
    }

    func isValidBST(_ root: TreeNode?, _ max: Int, _ min: Int) -> Bool {
      guard let root = root else {
        return true
      }

      if root.val >= max || root.val <= min {
        return false
      }

      return isValidBST(root.left, root.val, min) && isValidBST(root.right, max, root.val)
    }
}

方法二:中序遍历+升序验证

思路:根据二叉搜索树的特点,二叉搜索树中序遍历得到的值构成的序列一定是升序的,这意味着我们可以在中序遍历的时候,判断当前节点的值是否大于前一个中序遍历到的节点的值即可。

递推公式isValidBST(root) = isValidBST(root.left) && root.val > previous && isValidBST(root.right) 终止条件root != nil

参考题解

时间复杂度:O(n)。其中 n 是树的节点数。对每个节点访问一次。 空间复杂度:O(n)。空间消耗主要是递归过程中栈的开销,平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。


class Solution {
    var previous = Int.min

    func isValidBST(_ root: TreeNode?) -> Bool {
      guard let root = root else {
        return true
      }

      if !isValidBST(root.left) {
        return false
      }

      if root.val <= previous {
        return false
      }
      previous = root.val

      return isValidBST(root.right)
    }
}
ShannonChenCHN commented 3 years ago

108. 将有序数组转换为二叉搜索树

2021.03.13 08:00 pm ~ 08:30 pm

image image image

中序遍历法

根据二叉搜索树的特点,中序遍历([ [左子树],根,[右子树] ])时得到的序列正好是递增序列,所以,反过来,我们可以将一个递增序列按照中序遍历的方式组装成一个二叉搜索树。将数组分成左右两半以及中间元素三部分,中间的元素转成根节点,左半部分转成左子树,右半部分转成右子树。

递推公式sortedArrayToBST(nums, low, high) = TreeNode(nums[middle], sortedArrayToBST(nums, low, middle-1), sortedArrayToBST(nums, middle+1, high)), 其中,middle = low + (high - low) / 2 终止条件nums.count == 0 || low > high

时间复杂度:O(n)。其中 n 是数组元素的个数。 空间复杂度:O(logn)。空间消耗主要是递归过程中栈的开销,因为我们每次都是取中间的元素作为根节点,所以最后得到的是一颗高度平衡的二叉树,递归的次数也就是树的高度,因此空间复杂度为 O(logn)

class Solution {
    func sortedArrayToBST(_ nums: [Int]) -> TreeNode? {
        guard nums.count > 0 else {
            return nil
        }
        return sortedArrayToBST(nums, 0, nums.count - 1)
    }

    func sortedArrayToBST(_ nums: [Int], _ low: Int, _ high: Int) -> TreeNode? {
        guard nums.count > 0 && low <= high else {
            return nil
        }
        let middle = low + (high - low) / 2 
        let root = TreeNode(nums[middle])
        root.left = sortedArrayToBST(nums, low, middle - 1)
        root.right = sortedArrayToBST(nums, middle + 1, high)
        return root
    }
}
ShannonChenCHN commented 3 years ago

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

注:本题同 剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

2021.03.13 08:35 pm ~ 09:00 pm

image image

迭代法

基于二叉搜索树的特点,可以从上往下遍历,遍历的过程中可能会遇到以下 3 种情况:

  1. 两个给定节点都比 root 节点小,则都在左子树中;
  2. 两个给定节点都比 root 节点大,则都在右子树中;
  3. 一个给定节点比 root 节点大,另一个比 root 节点小,则分别处于左右子树中; 时间复杂度:O(n)。平均情况下为 O(logn)(也就是满二叉树的情况),最坏情况下树退化成链表,为 O(n)。 空间复杂度:O(1)

参考题解:

class Solution {
    func lowestCommonAncestor(_ root: TreeNode?, _ p: TreeNode?, _ q: TreeNode?) -> TreeNode? {
        guard let root = root, let p = p, let q = q else {
            return nil
        }

        var cur = root
        while true {
            if cur.val > p.val && cur.val > q.val {
                cur = cur.left!
            } else if cur.val < p.val && cur.val < q.val {
                cur = cur.right!
            } else {
                break
            }
        }

        return cur
    }
}
ShannonChenCHN commented 3 years ago

剑指 Offer 54. 二叉搜索树的第k大节点

2021.03.09 09:10 pm ~ 09:30 pm

image image

“反转中序遍历法”

根据二叉搜索树的特点,我们可以知道中序遍历(左-根-右)可以得到一个递增序列,而想要知道第 k 大的元素,则需要一个递减序列。既然 左-根-右 得到的是 [小, 中, 大][大, 中, 小] 对应的是:右-根-左。

时间复杂度: O(n) 空间复杂度: O(n)

class Solution {
    var currentIdx = 0
    var result = 0
    func kthLargest(_ root: TreeNode?, _ k: Int) -> Int {
        guard let root = root else {
            return result
        }

        kthLargest(root.right, k)
        currentIdx += 1
        if currentIdx == k {
            result = root.val
        }
        kthLargest(root.left, k)
        return result
    }
}
ShannonChenCHN commented 3 years ago

110. 平衡二叉树

注:本题同剑指 Offer 55 - II. 平衡二叉树

2021.03.09 09:40 pm ~ 10:40 pm

image image

测试 case

输入:[6, null, 7, null, 8]

输入:[1]

解法一:自顶向下递归

根据“一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。” 我们可以得出,高度平衡二叉树需要满足以下两个条件:

递推公式isBalanced(root) = abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right) 终止条件root == nil

时间复杂度: O(n²) 空间复杂度: O(n)

class Solution {
    func isBalanced(_ root: TreeNode?) -> Bool {
        guard let root = root else {
            return true
        }

        return abs(level(root.left) - level(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right)
    }

    func level(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        return max(level(root.left), level(root.right)) + 1
    }
}

解法二:自底向上递归(深度优先、后序遍历)

根据“一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。”,我们可以自底向上遍历,只要发现其中一个节点的左右子树高度差绝对值大于 1,就说明这棵树不是平衡二叉树。

我们可以通过后序遍历(左-右-根)来实现自底向上遍历。

递推公式calculateHeight(root) = calculateHeight(root.left), calculateHeight(root.right), if (abs(leftHeight - rightHeight) > 1) result = false 终止条件root == nil 返回值:result

时间复杂度: O(n) 空间复杂度: O(n)

class Solution {
    var result = true
    func isBalanced(_ root: TreeNode?) -> Bool {
        guard let root = root else {
            return true
        }
        calculateHeight(root)
        return result
    }

    func calculateHeight(_ root: TreeNode?) -> Int {
        guard let root = root else {
            return 0
        }

        let heightLeft = calculateHeight(root.left)
        let heightRight = calculateHeight(root.right)
        if abs(heightLeft - heightRight) > 1 {
            result = false
        }
        return max(heightLeft, heightRight) + 1
    }
}