Open ShannonChenCHN opened 3 years ago
参考:
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
满二叉树:叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,如下图所示。
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
想要存储一棵二叉树,我们有两种方法:
链式存储法示意图:
顺序存储法示意图:
如图所示,如果节点 X 存储在数组中下标为 i 的位置,下标为 2 i 的位置存储的就是左子节点,下标为 2 i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。
图中的例子是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置。如果是非完全二叉树,其实会浪费比较多的数组存储空间。
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。
补充:后面要讲到的堆也是一种完全二叉树,最常用的存储方式就是数组。
二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。
四种遍历方式:
实际上,二叉树的前、中、后序遍历就是一个递归的过程。 这三种遍历方式的递推公式如下:
前序遍历的递推公式:
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)。
关于前、中、后序遍历,以及层序遍历的实现,详见下面的练习题。
参考:
二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。
二叉查找树:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
13
/ \
10 16
/ \ / \
9 11 14
-----------------------
16
/
10
/ \
9 13
/ \
11 14
原理跟二分查找有几分相似,详见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714
二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。
详见 https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714
针对要删除节点的子节点个数的不同,我们需要分三种情况来处理:
代码实现见:https://time.geekbang.org/column/article/68334?utm_term=pc_interstitial_714
除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。(代码实现见 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)。
2021.02.21 05:00 pm ~ 06:30 pm 2021.02.21 07:30 pm ~ 08:30 pm
分析:
preorderTraversal(root, &list) = list.append(root.val) + preorderTraversal(root.left, &list) + preorderTraversal(root.right, &list)
时间复杂度: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
}
}
TODO
2021.02.21 05:00 pm ~ 06:30 pm 2021.02.21 07:30 pm ~ 08:30 pm
分析:
inorderTraversal(root, &list) = inorderTraversal(root.left, &list) + list.append(root.val) + inorderTraversal(root.right, &list)
时间复杂度: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)
}
}
任何算法的递归版本都可以改成非递归版本,因为函数递归调用其实质就是压栈的过程,那么我们完全可以使用堆栈来模拟这个过程。二叉树的遍历(递归法)很容易实现,本质是采用了栈帧的实现方式,函数调用就是压栈,函数求解就是出栈的过程,那么我们完全可以手动创建栈来模拟这个过程,按照指定遍历顺序迭代的访问二叉树的所有结点。
分析:
curr
指针为空(也就是所有的右子节点也访问完了)时间复杂度: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
}
}
TODO
2021.02.21 05:00 pm ~ 06:30 pm 2021.02.27 04:40 pm ~ 06:40 pm
分析:
postorderTraversal(root, &list) = postorderTraversal(root.left, &list) + list.append(root.val) + postorderTraversal(root.right, &list)
时间复杂度: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
}
}
TODO
2021.02.27 07:10 pm ~ 07:40 pm
循环终止条件:最后一层也遍历完了,也就是队列中没有节点数据 循环内容:当前层节点出队列,并且将下一层的节点加入到队列末尾
时间复杂度: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
}
}
2021.02.27 07:45 pm ~ 08:15 pm
递推公式: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
}
}
注:本题同剑指 Offer 28. 对称的二叉树
2021.02.28 05:00 pm ~ 06:00 pm
输入:[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
}
}
2021.02.28 07:10 pm ~2021.02.28 09:00 pm
若 root 是节点 A,B 的最近公共祖先 ,则可能为以下情况之一:
思路: 采用深度优先的方式进行遍历,也就是先访问子节点后访问父节点,所以我们可以使用递归法进行后序遍历:左-右-根
递归三要素:
leftResult = lowestCommonAncestor(root.left, p, q)
, rightResult = lowestCommonAncestor(root.right, p, q)
leftResult == nil && rightResult == nil
,说明左右子树中都不包含 p、q 节点,所以返回 nil
leftResult != nil && rightResult == nil
,说明左子树中包含 p 或者 q 节点,或者同时包含 p、q 节点,所以返回 leftResult
leftResult
可能是 p 或者 q 节点,也有可能是 p、q 节点的最近公共祖先leftResult == nil && rightResult != nil
,跟上一种情况相似leftResult != nil && rightResult != nil
,说明 p、q 分别位于左右子树中,所以当前节点就是他们的最近公共祖先,返回 root
复杂度跟后序遍历一样。 时间复杂度: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
}
}
2021.03.06 04:45 pm ~ 05:35 pm
递推公式: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
}
}
注:本题同 剑指 Offer 27. 二叉树的镜像
2021.03.06 05:50 pm ~ 06:10 pm
关于树的题目,一般怎么分析呢?首先观察是否可以通过递归法解决,我们可以从叶子节点往上分析,看是否可以找到共同规律。
思路:采用后序遍历,如果当前遍历到的节点 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
}
}
2021.03.06 08:30 pm ~ 09:00 pm
递推公式: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
}
}
注:本题同 剑指 Offer 07. 重建二叉树
2021.03.06 07:30 pm ~ 08:15 pm
参考题解:
分析:
前序遍历:[根, [左子树的前序遍历], [右子树的前序遍历]]
中序遍历:[[左子树的中序遍历], 根, [右子树的中序遍历]]
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。
递推公式: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
注:本题同剑指 Offer 37. 序列化二叉树
2021.03.07 07:20 pm ~ 09:10 pm
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)
}
}
}
2021.03.07 03:45 pm ~ 04:20 pm
分两步:1. 依次(先序)遍历树 A 的每个节点;2. 在遍历树 A 时,将各个节点作为子树的根节点与树 B 进行比较 所以,这里要用到两个递归。
递推公式1:isSubStructure(A, B) = compare(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B)
终止条件1:A == nil || B == nil
递推公式2:compare(A, B) = (A.val == B.val) && compare(A.left, B.left) && compare(A.right, B.right)
终止条件2:A == 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
}
}
2021.03.13 11:50 am ~ 01:00 pm
输入:[2, 2, 3]
输出:false
输入:[5, 4, 6, null, null, 3, 7]
输出:false
分析二叉搜索树的特征:
递推公式: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)
}
}
2021.03.13 08:00 pm ~ 08:30 pm
根据二叉搜索树的特点,中序遍历([ [左子树],根,[右子树] ]
)时得到的序列正好是递增序列,所以,反过来,我们可以将一个递增序列按照中序遍历的方式组装成一个二叉搜索树。将数组分成左右两半以及中间元素三部分,中间的元素转成根节点,左半部分转成左子树,右半部分转成右子树。
递推公式: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
}
}
2021.03.13 08:35 pm ~ 09:00 pm
基于二叉搜索树的特点,可以从上往下遍历,遍历的过程中可能会遇到以下 3 种情况:
参考题解:
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
}
}
2021.03.09 09:10 pm ~ 09:30 pm
根据二叉搜索树的特点,我们可以知道中序遍历(左-根-右)可以得到一个递增序列,而想要知道第 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
}
}
2021.03.09 09:40 pm ~ 10:40 pm
输入:[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
}
}
题目列表
1. 二叉树
二叉搜索树
堆 见 #25
平衡二叉树
其他
总结