目录

题目描述

1104. 二叉树寻路

题意分析

有一棵无限深的完全二叉树,节点按「之字形」编号:最上面一行从左往右编号,下一行从右往左,再下一行又从左往右,如此交替。给定某个节点的编号 label,要求返回从根节点到它的完整路径上所有节点的编号。

首先要把两套编号分清楚。如果按常规的「逐行从左到右」编号,节点的编号有一个极好的性质:父节点编号等于子节点编号除以 2。之字形编号破坏了这个性质,但只破坏在「奇数行被整体翻转」这一件事上——每一行包含的编号集合仍然是 $[2^L, 2^{L+1}-1]$,只是排列顺序反了。这个观察是全题的入口:编号集合没变,变的只是行内位置。

其次要意识到树是无限的,不能建出来。题目只给一个整数,答案长度等于层数,也就是 $O(\log label)$。这种「输入是一个数、输出是对数级长度的序列」的形态,几乎总是在暗示:直接用数学关系从下往上一步步推,而不是搜索。

还有一个方向问题:从根往下找目标需要在每层判断走左还是走右,反而麻烦;从 label 往上回溯到根则每步只有唯一选择——父节点是确定的。所以自然的走向是自底向上,最后把序列反转。

边界:label = 1 时路径就是根自己,答案是 [1]label 上限 $10^6$,层数不超过 20,中间量远不会溢出 int

解法:从节点回溯到根

核心思路

从根向下需要判断每层走左还是走右;从目标向上时父节点唯一,因此选择自底向上回溯

设当前节点标签为 $x$,位于第 $L$ 层(根为第 0 层)。这一层的标签范围为

\[[s,e]=[2^L,2^{L+1}-1]\]

相邻两层的标号方向相反,由此可得到统一的父节点公式:

\[parent(x)=\left\lfloor\frac{s+e-x}{2}\right\rfloor\]

这个公式不能简单解释成“每层都先把标签转换为普通编号”,因为偶数层本来就是正序。严谨地分两种情况验证:

  • $L$ 为奇数时,当前层倒序。节点在普通完全二叉树中的编号是 $s+e-x$;父层为正序,所以除以 2 后就是父节点的之字形标签。
  • $L$ 为偶数时,当前层正序,普通编号就是 $x$;父层倒序,其范围为 $[s/2,s-1]$。把普通父编号 $\lfloor x/2\rfloor$ 在父层镜像,得到 $(s/2+s-1)-\lfloor x/2\rfloor$,与上式相等。

因而无论当前层方向如何,都可以使用同一条转移。循环不变量是:每轮开始时,label 恰好是原目标到根路径上第 level 层的节点。记录它并按公式转移后,不变量在父层继续成立;到根再转移会得到 0,循环结束。最后反转自底向上的记录即可。

解题步骤

  1. level = 0 开始,持续检查 2^(level+1) <= label,定位目标所在层。循环结束时满足 $2^{level}\le label<2^{level+1}$。
  2. 将当前标签加入路径,计算本层左右端点 start = 1 << levelend = (start << 1) - 1
  3. (start + end - label) / 2 跳到父节点,同时令 level--。Java 和 Go 的整数除法对非负数自动向下取整。
  4. 重复到 label == 0,再反转路径。

例如 label = 14:第 3 层区间为 [8,15],父节点是 (8+15-14)/2=4;第 2 层区间为 [4,7],父节点是 (4+7-4)/2=3;随后得到 1。回溯序列 [14,4,3,1] 反转后为 [1,3,4,14]

边界 label = 1 时,根先被记录,公式得到 0,最终直接返回 [1]

代码实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Solution {
    public List<Integer> pathInZigZagTree(int label) {
        int level = 0;
        while ((1 << (level + 1)) <= label) {
            level++;
        }

        List<Integer> path = new ArrayList<>();
        while (label > 0) {
            path.add(label);
            int start = 1 << level;
            int end = (start << 1) - 1;
            label = (start + end - label) / 2;
            level--;
        }

        Collections.reverse(path);
        return path;
    }
}
func pathInZigZagTree(label int) []int {
	level := 0
	for (1 << (level + 1)) <= label {
		level++
	}

	path := make([]int, 0, level+1)
	for label > 0 {
		path = append(path, label)
		start := 1 << level
		end := (start << 1) - 1
		label = (start + end - label) / 2
		level--
	}

	for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
		path[i], path[j] = path[j], path[i]
	}
	return path
}

复杂度分析

  • 时间复杂度:$O(\log label)$。定位层数、回溯和反转都只处理路径上的 $O(\log label)$ 个节点。
  • 空间复杂度:$O(\log label)$ 用于返回路径;不计返回值时,额外空间为 $O(1)$。

关键点总结

  • 每层标签的取值区间不变,只有排列方向改变;区间端点提供了镜像关系。
  • 统一父节点公式是 $\lfloor(start+end-label)/2\rfloor$,它同时处理当前层和父层相反的编号方向。
  • 正确性不依赖记忆公式:分别验证当前层正序、倒序两种情况即可覆盖所有层。
  • 从目标回溯只有唯一父节点;末尾反转比反复头插更直接。
  • 用整数位移定位层数,避免浮点对数在 2 的幂边界上产生精度问题。

易错点总结

  • 直接用 label / 2 求父节点。反例 label = 14:会得到 7,而真实父节点是 4。
  • 只在倒序层应用镜像。反例路径经过 4 时,第 2 层虽然正序,但它的父层倒序;4 / 2 = 2,正确父节点却是 3。
  • 层号多算一层。若写成 while ((1 << level) <= label) level++label = 8 会被误判到第 4 层;应比较下一层起点 1 << (level + 1)
  • 循环写成 label > 1 会漏掉根;label = 1 应返回 [1]
  • 忘记最终反转时,label = 14 会得到 [14,4,3,1],方向与题目要求相反。

相似题目

题目 难度 考察点
222. 完全二叉树的节点个数 中等 同样利用完全二叉树的编号规律,改为二分定位最后一层的节点位置
103. 二叉树的锯齿形层序遍历 中等 之字形出现在遍历输出顺序上而非编号上,用双端插入或逐层反转实现
662. 二叉树最大宽度 中等 给节点赋完全二叉树式的下标,靠同层首尾编号相减求宽度
919. 完全二叉树插入器 中等 由编号推父子关系用于动态插入,考察对完全二叉树结构的维护
1261. 在受污染的二叉树中查找元素 中等 由编号反推路径上的左右走向,是本题「自顶向下」方向的对照写法
958. 二叉树的完全性检验 中等 用编号连续性判断完全性,强化「完全二叉树编号即位置」这一核心认识