Diameter of Binary Tree
给定一个按层序存储在数组 tree 中的二叉树。根节点位于索引 0,索引为 i 的节点的左子节点位于 2*i+1,右子节点位于 2*i+2,-1 表示空位置,数组末尾可能还有多余的 -1 项。返回这棵树的直径:任意两个节点之间最长路径上的边数。路径可以经过根节点,也可以只位于某个子树内。
函数
- treeinteger-array
- 按层序表示的二叉树,空位置用 -1 表示
- 返回值integer
- 两个节点之间最长路径上的边数
约束条件
1 ≤ tree.length ≤ 32767- 每个
tree[i]都是-1,或满足0 ≤ tree[i] ≤ 1000的值。 tree[0]永远不会是-1,所以这棵树至少有一个节点。- 数组末尾在最后一个节点之后可能还有额外的
-1条目。 - 空位置的两个子节点也都是空的,且深度最多为
14。
示例
- 输入
- tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
- 输出
- 4
- 解释
- 路径
7、4、3、8、6有 5 个节点和 4 条边。它在根节点处分叉:左边向下 3 条边,右边向下 1 条边。
- 输入
- tree = [2, 5, -1, 1, 9, -1, -1, 3, -1, -1, 4]
- 输出
- 4
- 解释
- 路径
3、1、5、9、4有四条边,并在索引1处的5转弯。根节点没有右子节点,因此经过它的路径只有三条边。
- 输入
- tree = [6, -1, -1]
- 输出
- 0
- 解释
- 单个节点没有边。最长路径只有这个节点本身,长度为
0。
进阶
你会如何返回路径本身,也就是从直径一端到另一端的节点值?
提示
逐个打开提示,每个都会多透露一点。
树中的每条路径都有一个最高节点,路径会在这里从向上转为向下。如果你知道这个节点,那么经过它的路径可以有多长?
在节点
i处转弯的路径,会向下进入左子树,再向下进入右子树。它的长度最多为左子节点的高度加上右子节点的高度,其中高度是最长向下路径上的节点数,空位置的高度为0。从底部向上进行一次后序遍历,计算各节点的高度:节点的高度为
1 + max(left, right)。在节点处获取left和right后,用left + right更新答案。
题解
最长路径不一定经过根节点。每条路径都有一个最高节点,路径会在这里转弯。在某个节点转弯的最长路径,其边数等于左子树的高度加上右子树的高度。一次后序遍历就能自底向上计算每个节点的高度,并检查每个转弯点,时间复杂度为 O(n)。
测量每对节点
结果正确,但无法在最大的测试中运行完毕
思路
测量每对节点之间的距离。索引 i 的父节点是 (i-1)/2 向下取整,因此索引 a 和 b 之间的距离,就是在它们相遇前,将较大的索引替换为其父节点的次数。较大的索引永远不会处于更高的层级,因此不会越过相遇点。以第一个示例中的 9 和 2 为例:9 到 4,再到 1;2 到 0;1 到 0,共 4 步。
一棵包含 16383 个节点的满二叉树约有 1.3 × 10^8 对节点,每对最多需要 26 步:只求一个答案就要执行数十亿步。
算法
- 收集所有实际节点的索引。
- 对于每一对
(a, b),将edges = 0,然后重复以下操作,直到a == b:将较大的索引替换为它的父节点,并将1加到edges上。 - 保留你看到的最大
edges值并返回它。
def diameterOfBinaryTree(tree):
nodes = [i for i, value in enumerate(tree) if value != -1]
best = 0
for x in range(len(nodes)):
for y in range(x + 1, len(nodes)):
a, b = nodes[x], nodes[y]
edges = 0
while a != b:
# A larger index is never higher up, so climb from it.
if a > b:
a = (a - 1) // 2
else:
b = (b - 1) // 2
edges += 1
best = max(best, edges)
return best在每个节点测量两个高度
思路
令 height(c) 统计从 c 开始向下的最长路径上的节点数,空位置的高度为 0。路径在节点 i 处转弯时,最长路径的边数为 height(2*i+1) + height(2*i+2),也就是转弯处下方每个节点对应一条边。尝试每个节点作为转弯点,并保留最大的结果。在第二个示例中,索引 1 处的 5 两侧的高度都是 2,共有四条边,而根节点只能得到 3 + 0。
每次调用 height 都会遍历整个子树,而每个节点都会因为它的每个祖先而被重复遍历,因此时间复杂度为 O(n·h)。当 h ≤ 14 时,这种方法很快,但对于链状树,时间复杂度会变成 O(n²)。
算法
- 编写
height(i):空位置为0,否则为1 + max(height(2*i+1), height(2*i+2))。 - 对于每个实际节点
i,计算height(2*i+1) + height(2*i+2)。 - 返回这些和中的最大值。
def diameterOfBinaryTree(tree):
n = len(tree)
def height(i):
# Nodes on the longest downward path from i; an empty spot has 0.
if i >= n or tree[i] == -1:
return 0
return 1 + max(height(2 * i + 1), height(2 * i + 2))
best = 0
for i in range(n):
if tree[i] != -1:
# The longest path that turns at node i goes down both sides.
best = max(best, height(2 * i + 1) + height(2 * i + 2))
return best对高度进行一次后序遍历
思路
一个节点的高度只需要知道它的子节点的高度,也就是检查转折点时用到的那两个数字。后序遍历会先处理完两个子节点,再处理它们的父节点,因此在每个节点处,你都会得到 left 和 right:用 left + right 更新答案,并将 1 + max(left, right) 返回给父节点。
在第一个示例中,7 返回 1,4 返回 2,3 返回 3。6 返回 1,所以在根节点处,left + right = 3 + 1 = 4。每个节点只会被访问一次:时间复杂度为 O(n),递归深度为 O(h)。
算法
- 将
best = 0设为初始值,并编写height(i)。如果位置为空,则返回0。 - 计算
left = height(2*i+1)和right = height(2*i+2)。 - 将
best设为best和left + right中较大的值。 - 返回
1 + max(left, right)。 - 调用
height(0),并返回best。
def diameterOfBinaryTree(tree):
n = len(tree)
best = 0
def height(i):
# Returns the height of node i and updates best on the way back up.
nonlocal best
if i >= n or tree[i] == -1:
return 0
left = height(2 * i + 1)
right = height(2 * i + 2)
best = max(best, left + right) # the longest path that turns at node i
return 1 + max(left, right)
height(0)
return best
易错点和边界情况
- 计算节点数而不是边数。路径
7、4、3、8、6有 5 个节点,但长度是4。 - 只测量经过根节点的路径。第二个例子中这样会得到 3,但答案是 4。
- 从递归调用中返回直径。父节点需要的是高度,因此要用单独的变量保存直径。
- 混用高度的定义。按节点数计算高度,空位置的高度为
0,此时边数为left + right。按边数计算高度,空位置的高度需要为-1,此时边数为left + right + 2。混用这两种定义会导致结果相差一或两。 - 读取越过末尾的数据。靠近数组末尾的叶节点,其子节点索引会超出数组的最后一个元素,应将这些位置视为空位置。
常见问题3
二叉树的直径的时间复杂度是多少?
后序遍历的解法会访问每个节点一次:时间复杂度为 O(n),递归需要的额外空间为 O(h),其中 h 是树的高度。在每个节点分别计算高度需要 O(n·h),对于链状树来说,时间复杂度为 O(n²)。
二叉树的直径一定会经过根节点吗?
不是。最长路径可能位于一棵子树中,例如,根节点的一侧只有一条较短的分支,而另一侧有一棵很深、分支很多的子树。这就是为什么你要在每个节点检查 left + right。
如何在不使用递归的情况下求二叉树的直径?
访问各个节点,确保每个子节点都排在其父节点之前:将根节点压入栈中,将节点弹出并放入列表,同时压入它们的子节点,然后从后往前遍历该列表。将每个高度存储在数组中,并在每个节点处根据两个子节点的高度更新答案。时间复杂度仍为 O(n)。
相似题目
运用相同思路的题目。做两三道,才能真正掌握一种模式。
def diameterOfBinaryTree(tree):
# 在这里编写代码测试 #1
测试 #2
测试 #3
参数
tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
预期输出
4