把二叉搜索树转换为累加树 图解题解
倒着走一遍 BST,一个累加变量就能把每个节点改写完毕。
BST 里按「右→根→左」倒着中序走,就像读排行榜从第一名往后翻:先见到最大的,边走边攒一个累计积分。轮到某个人时,比他强的都已经算进积分里了,把自己的值往上一加,新积分就是题目要的结果。整棵树只走一遍,一个临时变量从头攒到尾,不回头不重查。
这道题到底在问什么
- 输入
- BST 如图(14 节点)
- 输出
- 每个节点 = 原值 + 比它大的所有节点之和
最优解:为什么这么做
一句话答案:LeetCode 538 把二叉搜索树转换为累加树的最优解是反中序遍历:按右、根、左的顺序访问,节点值恰好从大到小出现,维护一个运行累加和 sum,每到一个节点先把原值加进 sum、再把节点值改写成 sum。一趟遍历完成全部改写,时间 O(n)、空间 O(h) 递归栈。
累加树到底要算什么
给一棵二叉搜索树(BST),要把每个节点的值替换成:原值加上树中所有大于它的节点值之和。换句话说,每个节点都在问同一个问题——比我大的那些数加起来是多少。输出仍是这棵树本身,所有值就地改写,返回改造后的根。
为什么逐个节点单独求和是浪费
最直白的做法是对每个节点扫一遍全树,把比它大的值累加起来,n 个节点各扫一次就是 O(n²)。浪费在哪里很清楚:「比 12 大的数」和「比 11 大的数」这两个集合几乎完全重叠,暴力法把同一批大数反复加了一遍又一遍。只要能按从大到小的顺序处理节点,「比当前大的所有值之和」就变成一个可以滚动维护的量——每处理完一个节点就把它并进去,谁都不用重复加。
反中序遍历为什么恰好给出从大到小
二叉搜索树的定义保证:中序遍历(左、根、右)得到升序序列。把顺序整个镜像成右、根、左——先钻右子树、再访问根、最后进左子树——得到的自然是降序序列,这就是反中序遍历。降序意味着走到任何一个节点时,所有比它大的节点都恰好已经被访问过,它们的总和正好就是此刻的运行累加和 sum。BST 的有序性在这里不是背景设定,而是让「从大到小枚举」零额外成本实现的关键。
为什么必须先累加再赋值,顺序不能反
每个节点上只做两步:先 sum += 原值,再 node.val = sum。先加后赋的顺序是硬要求,因为新值的定义是原值加上比它大的和——必须把自己也算进去,先赋值再累加就会漏掉自身。另一个方向性的坑:用正常中序(左、根、右)访问是升序,累加出来的是「比它小的和」,整棵树全错——遍历方向和累加语义是绑死的。
复杂度与迭代写法
每个节点恰好访问一次、做常数次运算,时间 O(n);递归栈深等于树高 h,平衡树 O(log n),最坏退化成链 O(n)。
不想用递归可以改成显式栈的迭代版:一路向右压栈到底,弹出即访问(累加并改值),再转向该节点的左子树,逻辑与递归完全一致,空间同为 O(h)。若题目反过来要求「比它小的节点之和」,把遍历方向换回正常中序即可,累加框架原样保留。
▶ 动画逐步走查(共 31 步)——想跟着动画一帧帧对照就展开
- 3核心一句话:累加树 = 每个节点 = 原值 + 所有比它大的节点之和,反中序(右根左)降序累加。
- 4原始 BST,所有节点还是原值。反中序遍历从最右下角(最大值 14)开始,按「右子树 → 根 → 左子树」访问,越访问越小。
- 5反中序走到当前最大的未访问节点,原值 = 14(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 0 + 14 = 14。
- 6节点新值落定:14 → 14(蓝色=已累加)。新值 = 原值 14 + 比它大的节点之和 0。继续反中序访问下一个更小的节点。
- 7反中序走到当前最大的未访问节点,原值 = 13(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 14 + 13 = 27。
- 8节点新值落定:13 → 27(蓝色=已累加)。新值 = 原值 13 + 比它大的节点之和 14。继续反中序访问下一个更小的节点。
- 9反中序走到当前最大的未访问节点,原值 = 12(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 27 + 12 = 39。
- 10节点新值落定:12 → 39(蓝色=已累加)。新值 = 原值 12 + 比它大的节点之和 27。继续反中序访问下一个更小的节点。
- 11反中序走到当前最大的未访问节点,原值 = 11(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 39 + 11 = 50。
- 12节点新值落定:11 → 50(蓝色=已累加)。新值 = 原值 11 + 比它大的节点之和 39。继续反中序访问下一个更小的节点。
- 13反中序走到当前最大的未访问节点,原值 = 10(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 50 + 10 = 60。
- 14节点新值落定:10 → 60(蓝色=已累加)。新值 = 原值 10 + 比它大的节点之和 50。继续反中序访问下一个更小的节点。
- 15反中序走到当前最大的未访问节点,原值 = 9(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 60 + 9 = 69。
- 16节点新值落定:9 → 69(蓝色=已累加)。新值 = 原值 9 + 比它大的节点之和 60。继续反中序访问下一个更小的节点。
- 17反中序走到当前最大的未访问节点,原值 = 8(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 69 + 8 = 77。
- 18节点新值落定:8 → 77(蓝色=已累加)。新值 = 原值 8 + 比它大的节点之和 69。继续反中序访问下一个更小的节点。
- 19反中序走到当前最大的未访问节点,原值 = 7(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 77 + 7 = 84。
- 20节点新值落定:7 → 84(蓝色=已累加)。新值 = 原值 7 + 比它大的节点之和 77。继续反中序访问下一个更小的节点。
- 21反中序走到当前最大的未访问节点,原值 = 6(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 84 + 6 = 90。
- 22节点新值落定:6 → 90(蓝色=已累加)。新值 = 原值 6 + 比它大的节点之和 84。继续反中序访问下一个更小的节点。
- 23反中序走到当前最大的未访问节点,原值 = 5(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 90 + 5 = 95。
- 24节点新值落定:5 → 95(蓝色=已累加)。新值 = 原值 5 + 比它大的节点之和 90。继续反中序访问下一个更小的节点。
- 25反中序走到当前最大的未访问节点,原值 = 4(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 95 + 4 = 99。
- 26节点新值落定:4 → 99(蓝色=已累加)。新值 = 原值 4 + 比它大的节点之和 95。继续反中序访问下一个更小的节点。
- 27反中序走到当前最大的未访问节点,原值 = 3(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 99 + 3 = 102。
- 28节点新值落定:3 → 102(蓝色=已累加)。新值 = 原值 3 + 比它大的节点之和 99。继续反中序访问下一个更小的节点。
- 29反中序走到当前最大的未访问节点,原值 = 2(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 102 + 2 = 104。
- 30节点新值落定:2 → 104(蓝色=已累加)。新值 = 原值 2 + 比它大的节点之和 102。继续反中序访问下一个更小的节点。
- 31反中序走到当前最大的未访问节点,原值 = 1(橙色)。它比所有还没访问的节点都大。先把累加和更新:sum = 104 + 1 = 105。
- 32最后一个节点:原值 1,新值 = 105(= 原值 1 + 比它大的所有节点之和 104)。整棵累加树改造完成,累加和最终 = 105。
- 33累加树改造完成(绿色=已转换)。最大节点新值 = 它自己,越小的节点累加了越多更大节点之和,最左下角最小节点新值 = 全部之和 105。
⚠️ 容易写错的地方
✗ 错:用正常中序(左→根→右)累加
✓ 对:必须反中序(右→根→左)
要从大到小累加,正常中序是升序、累加方向反了
✗ 错:先 node.val = sum 再 sum += 原值
✓ 对:先 sum += 原值,再 node.val = sum
节点新值要含它自己,顺序反了会漏掉自身或多算
✗ 错:为每个节点单独求「比它大的和」
✓ 对:一个运行累加和 sum 边走边累
反中序天然降序,O(n) 一遍即可,无需重复求和
完整代码(Python / C++ / Java)
Python
def convertBST(root):
sum = 0
def dfs(node):
nonlocal sum
if not node: return
dfs(node.right) # 先右(更大)
sum += node.val # 并入累加和
node.val = sum # 新值 = 累加和
dfs(node.left) # 再左(更小)
dfs(root)
return rootC++
class Solution {
int sum = 0;
public:
TreeNode* convertBST(TreeNode* root){
if(!root) return root;
convertBST(root->right); // 先右(更大)
sum += root->val; // 并入累加和
root->val = sum; // 新值 = 累加和
convertBST(root->left); // 再左(更小)
return root;
}
};Java
class Solution {
private int sum = 0;
public TreeNode convertBST(TreeNode root) {
if (root == null) return root;
convertBST(root.right); // 先右(更大)
sum += root.val; // 并入累加和
root.val = sum; // 新值 = 累加和
convertBST(root.left); // 再左(更小)
return root;
}
}复杂度
时间
O(n)
每个节点恰好访问一次
空间
O(H)
递归栈深 = 树高 H;最坏退化链 O(n),平衡树 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 把二叉搜索树转换为累加树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用递归怎么做?+
用栈做迭代反中序:一路向右压栈到底,弹出即访问(sum += 原值、node.val = sum),再转向该节点的左子树。逻辑与递归完全一致,空间同为 O(H)。
如果要求的是「比它小的节点之和」而不是更大,怎么改?+
改成正常中序(左→根→右),按从小到大访问,同样维护运行累加和即可——遍历方向反过来。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 把二叉搜索树转换为累加树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。