二叉树寻路 图解题解
这道题到底在问什么
- 输入
- label = 14
- 输出
- [1,3,4,14]
- 输入
- label = 26
- 输出
- [1,2,6,10,26]
最优解:为什么这么做
一句话答案:LeetCode 1104 二叉树寻路:满二叉树按之字形逐行编号,给 label 求根到它的路径。先翻倍定行,再逐行「镜像加折半」抵消偶数行的翻转爬到父节点,时间 O(log label)。
题目给一个标号,要哪条路径
有一棵无限大的满二叉树,也就是每层都填满节点的二叉树,标号自上而下一行行贴。第 i 行有 2 的 (i-1) 次方个节点,占满 2 的 (i-1) 次方 到 2 的 i 次方减 1。奇数行从左往右贴、偶数行从右往左贴,来回掉头,这就是之字形。给你标号 label,把根到它路上的标号从上到下列出来。题面 label=14 答案 [1,3,4,14],label=26 答案 [1,2,6,10,26]。
直接除以 2 找父节点,为什么爬岔了
普通满二叉树里父亲就是自己除以 2,一路除到 1 就是路径。可这棵树偶数行被从右往左翻转过,排布变了。拿 14 试:除以 2 是 7,但 7 在第 3 行,14 真正的父亲是 4,一除就爬岔了。把无限树建出来再找也不行,标号能到几十亿。
偶数行的翻转,靠一次镜像抵消
盯住相邻两行:编号方向永远相反,一个从左往右、下一个从右往左,正是这个相反让直接除以 2 失灵。补救是先在本行做镜像:镜像 = 本行最小标号 加 本行最大标号 减自己。第 i 行范围是 2 的 (i-1) 次方 到 2 的 i 次方减 1,镜像后的值方向已和父节点那行对齐,这时再除以 2,就落到父节点的真实标号。
先翻倍定行,再逐行往上爬
过程分先后两段。定行:让 x 和 i 都从 1 起步,x 是第 i 行的最小标号,不停把 x 翻倍、i 加一,直到再翻一倍就超过 label,此时的 i 就是 label 所在行。上爬:从这行起,每行镜像加折半得到父标号、收进答案、行号减一,直到爬回根。Python 用 ans[i-1] 从后往前填正好是根到 label 顺序;C++、Java 顺着收集则末尾要 reverse。
label=14,四行怎么一步步爬回根
先定行。x 从 1 出发翻倍:2、4、8,行号 i 数到 4;下一次翻倍是 16,超过 14,停。14 在第 4 行,答案 4 个标号。
从第 4 行的 14 往上。第 4 行范围 8 到 15,镜像 8+15-14=9,除以 2 得 4,是 14 的父亲。第 3 行范围 4 到 7,镜像 4+7-4=7,得 3。第 2 行范围 2 到 3,镜像 2+3-3=2,得 1。第 1 行是根 1。自底向上收到 14、4、3、1,倒过来就是 [1,3,4,14],和题面一致。
只走行号那么多步,剩三处要盯牢
复杂度:label 的行号约是以 2 为底 label 的对数。定行和上爬各走行号那么多步,时间 O(log label);路径长度就等于行号,是结果占的空间,额外只用常数个变量。
三处细节得盯牢。忘了镜像、直接拿 label 除以 2,会爬到翻转前的错标号,像 14 会错成 7。把第 i 行范围记成 2 的 i 次方 开头,最小最大全偏一位、镜像跟着全错,范围应是 2 的 (i-1) 次方 到 2 的 i 次方减 1。收集顺序也别忘:C++、Java 漏了 reverse 就头尾颠倒。边界上根标号 1 单独成路;第 2 行的 2 在最右,先镜像成 3 再除以 2 回到根 1。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢两步:先翻倍定行,再「镜像加折半」逐行爬到父节点。镜像 = 本行最小加最大减自己,目的就是抵消之字形的方向翻转。下面每一帧都在套这套规则。
- 4行1:1 行2:3 2 行3:4 5 6 7 行4:15 14 ...先看清这棵树长什么样。树是无限大的,这里只画出和我们这次寻路有关的一小块。每个圆圈里的数字就是这个节点的标号,注意行与行之间,编号方向是反复掉头的。
- 5行1 范围 [1,1]最上面第 1 行,只有一个根节点,标号就是 1。它的范围是 2 的 0 次方 到 2 的 1 次方减 1,也就是只有 1 这一个数。第 1 行算奇数行,从左到右编。
- 6行2 范围 [2,3] · 从右往左第 2 行有两个节点,标号占 2 和 3。可它是偶数行,要从右往左编号,于是右边那个反而是 2、左边那个是 3。看清楚了吗,这一行被掉了个头,这正是之字形最容易绕晕的地方。
- 7行3 范围 [4,7] · 从左往右第 3 行是奇数行,又回到从左往右,标号规规矩矩是 4、5、6、7。可以看到方向又掉回来了,和第 2 行正好相反。
- 8行4 范围 [8,15] · 从右往左第 4 行又是偶数行,再次从右往左,整行应该是 15、14、13 一直到 8。屏幕地方有限,只画出靠左的 15 和 14 两个,它们正是节点 4 的两个孩子。我们要找的 14 就藏在这一行里。
- 9x = 1,行 i = 1开始定位 14 在第几行。从 x 等于 1、i 等于 1 出发,x 始终代表第 i 行标号范围的起点、也就是这一行的最小标号 2 的 (i-1) 次方(注意之字形里偶数行被翻转,屏幕上最左端那个未必是它)。试着翻倍:1 乘 2 等于 2,2 还小于等于 14,说明 14 在更下面,往下走一行。
- 10x = 2,行 i = 2x 翻成 2,i 变成 2,现在停在第 2 行。再翻倍:2 乘 2 等于 4,4 仍然小于等于 14,14 还在更深处,接着往下。
- 11x = 4,行 i = 3x 翻成 4,i 变成 3。再翻倍:4 乘 2 等于 8,8 还是小于等于 14,14 依旧在下面,再下一行看看。
- 12x = 8,行 i = 4 · x*2 = 16 > 14 停x 翻成 8,i 变成 4。这回再翻倍是 16,16 已经大于 14 了,不能再往下,停手。于是确定 14 就落在第 4 行,这一行标号范围是 8 到 15。
- 13行号 i = 4,答案将有 4 个标号行号定下来了,14 在第 4 行。因为它在第 4 行,从根到它一共要经过 4 个节点,所以答案会是 4 个标号。接下来从 14 出发,一行一行往上爬到根。
- 14label = 14,本行范围 [8,15]现在处理标号 14,它在第 4 行,这一行的标号范围是 8 到 15。普通满二叉树里父节点就是自己除以 2,但偶数行被翻转过,直接除不是可靠通用规则,这里统一先镜像再折半。
- 15镜像 = 8 + 15 - 14 = 9做镜像:把本行的最小 8 加最大 15,再减掉自己 14,得到 9。这个 9 就是 14 在本行里左右对称的那个位置。镜像这一下,正好抵消了相邻两行方向相反带来的错位。
- 16父 = 9 / 2 = 4把镜像值 9 除以 2,向下取整得到 4。神奇的地方在这里:因为上一行的编号方向和这一行正好相反,镜像加折半两步一合,4 不是别的中间量,它就是 14 父节点的真实标号。把 4 记下来,继续往上爬。
- 17label = 4,本行范围 [4,7]现在处理标号 4,它在第 3 行,这一行的标号范围是 4 到 7。普通满二叉树里父节点就是自己除以 2,但偶数行被翻转过,直接除不是可靠通用规则,这里统一先镜像再折半。
- 18镜像 = 4 + 7 - 4 = 7做镜像:把本行的最小 4 加最大 7,再减掉自己 4,得到 7。这个 7 就是 4 在本行里左右对称的那个位置。镜像这一下,正好抵消了相邻两行方向相反带来的错位。
- 19父 = 7 / 2 = 3把镜像值 7 除以 2,向下取整得到 3。神奇的地方在这里:因为上一行的编号方向和这一行正好相反,镜像加折半两步一合,3 不是别的中间量,它就是 4 父节点的真实标号。把 3 记下来,继续往上爬。
- 20label = 3,本行范围 [2,3]现在处理标号 3,它在第 2 行,这一行的标号范围是 2 到 3。普通满二叉树里父节点就是自己除以 2,但偶数行被翻转过,直接除不是可靠通用规则,这里统一先镜像再折半。
- 21镜像 = 2 + 3 - 3 = 2做镜像:把本行的最小 2 加最大 3,再减掉自己 3,得到 2。这个 2 就是 3 在本行里左右对称的那个位置。镜像这一下,正好抵消了相邻两行方向相反带来的错位。
- 22父 = 2 / 2 = 1把镜像值 2 除以 2,向下取整得到 1。神奇的地方在这里:因为上一行的编号方向和这一行正好相反,镜像加折半两步一合,1 不是别的中间量,它就是 3 父节点的真实标号。把 1 记下来,继续往上爬。
- 23label = 1 已是根,停止爬到第 1 行了,当前标号是 1,也就是根。根没有父节点,上爬到此结束。一路自底向上收集到的标号是 14、4、3、1,马上把它倒过来就是答案。
- 24自底向上 14, 4, 3, 1 ➜ 倒序 1, 3, 4, 14上爬是从底往上收的,顺序是 14、4、3、1。题目要的是从根到 14 自上而下的顺序,所以把它倒过来,得到 1、3、4、14。
- 25答案 [1,3,4,14]紫色这条链就是从根到 14 的路:1 到 3 到 4 到 14,答案 [1,3,4,14]。回顾一下,我们压根没有真的把这棵无限树建出来,只靠翻倍定行、再镜像折半逐行上爬,几步就把路径算了出来。
⚠️ 容易写错的地方
✗ 错:直接拿 label 一路除以 2 当父节点
✓ 对:先在本行做镜像(最小+最大-自己)再除以 2
偶数行被从右往左翻转过,不镜像直接除会爬到错误的标号
✗ 错:把第 i 行范围记成 [2^i, ...]
✓ 对:第 i 行范围是 [2^(i-1), 2^i - 1]
行起点是 2 的 (i-1) 次方,记错一位会让镜像的最小最大全错
✗ 错:自底向上收集后忘了倒序
✓ 对:C++/Java 收完要 reverse;Python 用 ans[i-1] 倒着填省掉反转
上爬是从 label 往根收的,不倒过来路径方向就反了
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def pathInZigZagTree(self, label: int) -> List[int]:
x = i = 1
while (x << 1) <= label:
x <<= 1
i += 1
ans = [0] * i
while i:
ans[i - 1] = label
label = ((1 << (i - 1)) + (1 << i) - 1 - label) >> 1
i -= 1
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> pathInZigZagTree(int label) {
int x = 1, i = 1;
while ((x << 1) <= label) {
x <<= 1;
++i;
}
vector<int> ans;
for (; i > 0; --i) {
ans.push_back(label);
label = ((1 << (i - 1)) + (1 << i) - 1 - label) >> 1;
}
reverse(ans.begin(), ans.end());
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> pathInZigZagTree(int label) {
int x = 1, i = 1;
while ((x << 1) <= label) {
x <<= 1;
++i;
}
List<Integer> ans = new ArrayList<>();
for (; i > 0; --i) {
ans.add(label);
label = ((1 << (i - 1)) + (1 << i) - 1 - label) >> 1;
}
Collections.reverse(ans);
return ans;
}
}复杂度
时间
O(log label)
行号约等于以 2 为底 label 的对数。先翻倍定行走 O(log label) 次,再逐行上爬同样 O(log label) 次,合起来还是对数级
空间
O(log label)
要返回的路径长度等于行号,约 log label;这是结果本身占的空间。除答案外只用了几个整数变量,额外空间 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树寻路 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么镜像再除以 2 得到的是父节点真实标号,不是某个中间值?+
关键是相邻两行编号方向永远相反。镜像 = 本行最小加最大减自己,等于把当前标号换成假如本行按相反方向编号时它落在的值,而上一行恰好就是那个相反方向。所以镜像后的值方向上已经和父节点那行对齐,再除以 2 落下去,正好命中父节点。拿 14 验:镜像 8+15-14=9,9 除以 2 得 4,4 正是 14 的父亲;若跳过镜像直接 14 除以 2 得 7,7 却在第 3 行,是错的。
可以不先定行、直接往上爬吗?+
不行。每次上爬都要本行的最小和最大标号才能算镜像,而这两个值由行号决定,像第 4 行是 8 到 15、第 3 行是 4 到 7。行号没定出来,最小最大就无从谈起,镜像也就算不了。定行和上爬是先后依赖的两段,第一段省不掉。
复杂度为什么是对数级,不是线性?+
满二叉树第 i 行有 2 的 (i-1) 次方个节点,标号最大到 2 的 i 次方减 1,所以 label 对应的行号大约是以 2 为底 label 的对数。全程既不建树也不逐个节点扫,只是先翻倍定行、再逐行往上爬,两段都只走行号那么多步。label 到十亿,行号也就三十上下,所以是 O(log label)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二叉树寻路 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。