Bottom View of Binary Tree
数组 root 表示一棵二叉树:根节点位于索引 0,索引 i 的子节点位于 2i+1(左)和 2i+2(右),空位置用 -1 表示,末尾可能还有多余的 -1 项。
根节点位于第 0 列;向左走一步,列号减一,向右走一步,列号加一。从下方观察时,每列显示最深的节点;如果深度相同,则显示该层从左到右最后出现的节点(索引较大的节点)。返回你看到的值,按列从最左侧开始排列。
函数
- rootinteger-array
- 树按堆顺序排列,空位用 -1 表示
- 返回值integer-array
- 从下方看到的每一列的值,从最左侧的列开始
约束条件
1 ≤ root.length ≤ 32767- 每个
root[i]都是-1,或者是满足0 ≤ root[i] ≤ 104的值,并且没有重复值。 root[0]永远不会是-1,因此这棵树至少有一个节点。- 数组的末尾可能会有一些额外的
-1项,位于最后一个节点之后。 - 空位置的两个子节点也都是空的,而且这棵树最多有
14层。
示例
- 输入
- root = [5, 2, 9, 4, 7, 1, 6]
- 输出
- [4, 2, 1, 9, 6]
- 解释
- 第 0 列存放
5,向下两层的位置存放7(索引 4)和1(索引 5)。1在同一层中出现得更晚,所以会显示出来。第 -2、-1、1 和 2 列分别存放4、2、9和6。
- 输入
- root = [1, 2, 3, -1, 4, -1, -1, -1, -1, 5]
- 输出
- [5, 4, 3]
- 解释
- 第 -1 列存放着
2,往下两层是5(索引 9),所以你会看到5。在第 0 列中,4位于根节点下方。第 1 列中只有3。
- 输入
- root = [8, -1, -1, -1]
- 输出
- [8]
- 解释
- 根节点之后的每个位置都是
-1,所以8是唯一的节点,列 0 是唯一的列。
进阶
如果某一列最深层出现并列,应该从左到右返回所有并列的节点,那么广度优先版本需要如何修改?示例 1 的第 0 列会得到什么?
提示
逐个打开提示,每个都会多透露一点。
给列编号:根节点为 0,向左移动一步减 1,向右移动一步加 1。从下方看,你会看到某一列的哪个节点?如果这一列的两个节点处于同一层呢?
按层遍历节点,每层从左到右,并将每个节点的值写入对应的列,替换原有内容。最后每列剩下什么?
使用由 (index, column) 对组成的队列,并先将左子节点加入队列,再将右子节点加入队列。每遇到一个节点,就覆盖该列的值,同时记录最小列和最大列,最后按顺序读取各列。
题解
每一列显示该列最深的节点;如果深度相同,则选择层序遍历中靠后的节点。广度优先搜索会按层序访问节点,因此它在某一列中最后访问的节点就是答案:让每个节点都覆盖该列对应的位置。
深度优先,每列最深的节点
思路
按深度优先的顺序遍历这棵树,为每个节点传入它所在的列和深度,并记录每列目前最深节点的深度和值。
先遍历左子树再遍历右子树,就能按从左到右的顺序遇到同一层的节点,所以你要找的节点就是最后遇到的那个:当深度更大或相等时替换。在示例 1 中,遍历会先遇到 7,再遇到 1,它们都在第 2 层的第 0 列,因此 1 会成为该列的节点。
这些列构成一个连续的范围,因此记录最小列和最大列,并读取它们之间的每一列。每个节点只遍历一次,时间复杂度为 O(n);对于有 h 层的树,递归深度为 h 次调用。
算法
- 从列 0、深度 0 开始访问根节点。
- 如果索引
i超出数组末尾,或root[i]是-1,则跳过。 - 如果该列还没有节点,或者当前深度大于或等于已存储的深度,就存储当前深度和值。
- 先访问
2i+1,列为col-1,再访问2i+2,列为col+1,两者的深度都增加一层。 - 返回存储的值,按列从小到大的顺序排列。
def bottomView(root):
n = len(root)
best = {} # column -> (depth, value) of the deepest node found so far
def visit(i, col, depth):
if i >= n or root[i] == -1:
return
# Replace on a greater or equal depth: on a tie, the node
# met last is the one further right, which comes later in level order.
if col not in best or depth >= best[col][0]:
best[col] = (depth, root[i])
visit(2 * i + 1, col - 1, depth + 1)
visit(2 * i + 2, col + 1, depth + 1)
visit(0, 0, 0)
# The columns that hold a node form one unbroken range.
return [best[col][1] for col in range(min(best), max(best) + 1)]广度优先,每列的最后一个节点
思路
当一个节点出队时,队列会依次添加它的左子节点和右子节点。这样,节点会按层级顺序出队:所有深度为 d 的节点都先于深度为 d+1 的节点出队,并且同一层从左到右出队。在同一列的节点中,最深的节点最后出队;深度相同时,靠右的节点最后出队。因此,当每个节点都覆盖所在列的槽位时,每个槽位最终都会保存从下方看到的节点,无须比较深度。
在示例 1 中,7 写入列 0,1 覆盖了它。
每个节点需要访问队列一次,并写入映射一次:时间复杂度为 O(n)。队列最多可以容纳整整一层节点,约占所有节点的一半,因此空间复杂度为 O(n)。
算法
- 将对(索引 0,列 0)放入队列。
- 取出一对(
i,col),并将root[i]存储在col下,替换之前的任何值。 - 对于数组内且不为
-1的每个子节点,添加(2i+1,col-1),然后添加(2i+2,col+1)。 - 重复此过程,直到队列为空,同时记录最小列和最大列。
- 按列从小到大的顺序返回映射中的值。
from collections import deque
def bottomView(root):
n = len(root)
bottom = {} # column -> value of the last node seen in that column
queue = deque([(0, 0)]) # (node index, column); the root is never empty
while queue:
i, col = queue.popleft()
# The queue hands out deeper nodes later, and right after left
# on one level, so the last node to reach a column is the one seen from below.
bottom[col] = root[i]
left, right = 2 * i + 1, 2 * i + 2
if left < n and root[left] != -1:
queue.append((left, col - 1))
if right < n and root[right] != -1:
queue.append((right, col + 1))
# The columns that hold a node form one unbroken range.
return [bottom[col] for col in range(min(bottom), max(bottom) + 1)]
易错点和边界情况
- 在深度优先版本中,只在深度严格更大时替换。因此,示例 1 会保留
7,而不是1。 - 按广度优先搜索的顺序保留每一列遇到的第一个节点。这就是顶视图。
- 先将右子节点加入队列,再加入左子节点。这样每一层的节点会按从右到左的顺序出现,深度相同时就会选错节点。
- 把叶节点作为答案。在示例 1 中,叶节点
7被1遮住;在示例 2 中,节点4有子节点,却是第 0 列的底部节点。 - 将各列按文本排序。作为字符串,
"-10"会排在"-1"和"-2"之间。
常见问题3
二叉树的底视图是什么?
根节点的列号为 0,每向左一步,列号减 1;每向右一步,列号加 1。底视图会从左到右列出每一列中最深的节点。如果多个节点深度相同,这道题会按层序遍历打破平局,保留后面的节点。
二叉树底视图的时间复杂度是多少?
对于 n 个节点,时间复杂度为 O(n):广度优先搜索会处理每个节点一次,并进行一次哈希映射写入。树映射等有序映射会额外增加一个 log w 因子,其中 w 是列数。
顶视图和底视图有什么区别?
两种方法都会为每一列选取一个节点。俯视图选取最浅的节点,仰视图选取最深的节点。在广度优先版本中,俯视图保留每列收到的第一个值;仰视图则会在每次收到值时覆盖原值。
def bottomView(root):
# 在这里编写代码测试 #1
测试 #2
测试 #3
参数
root = [5, 2, 9, 4, 7, 1, 6]
预期输出
[4, 2, 1, 9, 6]