从二叉搜索树到更大和树 图解题解
这道题到底在问什么
- 输入
- root=[50,30,70,20,40,60,80]
- 输出
- [260,330,150,350,300,210,80]
最优解:为什么这么做
一句话答案:LeetCode 1038 从二叉搜索树到更大和树:把每个节点改成树里所有不小于它的值之和;BST 反中序天然从大到小,用累加器边加边回填一趟就成,时间 O(n)、空间 O(h)。
每个节点要变成哪些值的和
给一棵二叉搜索树,也就是左小右大的那种树 root,要求把每个节点的值改成整棵树里所有大于或等于它的节点值之和,它自己也算一份。题面例子 root=[50,30,70,20,40,60,80],改完是 [260,330,150,350,300,210,80]:最大的 80 头顶没有更大的,还是 80;根 50 上面压着 80、70、60 再加它自己,滚成了 260。
对每个节点都扫一遍全树,慢在哪
一个节点要知道有多少值不小于自己,最直白的办法是拿它和其余每个节点比一遍,把够大的加起来。可这样每个节点都得走一趟全树,n 个节点就是 n 乘 n,O(n²),上万个节点就扛不住。退一步,把所有值收集起来排好序、再求后缀和也能算,但排序本身要 O(n log n),而且白白丢掉了 BST 早就排好的顺序。
BST 反着走,正好是从大到小
BST 有个现成的性质:中序遍历,也就是按左、根、右的顺序走,吐出来的值是升序的。把这个顺序整个反过来,右、根、左,走出来就是降序,从最大的节点开始一路变小。这条反中序的路恰好对上题目要的「比它大的先算」:轮到某个节点时,所有比它大的值都已经走过了。
一个累加器边走边加、边回填
备一个累加器 s,起始为 0,全程只增不清零。递归按反中序走:先钻进右子树 dfs(root.right),把更大的那些先处理完;回到当前节点做两件事,s 先加上它的原值,再把它的值改写成现在的 s;最后钻进左子树 dfs(root.left)。轮到某节点时,s 里装的正是所有比它大的值之和,加上它自己这一份,回填的就是它该有的更大和。两步不能倒:先加进 s 再回填,s 里才含它自己那份。
拿题面这棵树,从 80 倒着填到 20
反中序的访问顺序是 80、70、60、50、40、30、20,s 从 0 起:到 80,s=0+80=80,80 回填还是 80;到 70,s=80+70=150,70 变 150;到 60,s=150+60=210,60 变 210;到 50,s=210+50=260,50 变 260;到 40,s=260+40=300,40 变 300;到 30,s=300+30=330,30 变 330;到 20,s=330+20=350,20 变 350。倒着填一圈,把这些值按原位摆回去,[260,330,150,350,300,210,80] 正是题面的答案。
走反了中序、或中途把 sum 清零,回填就全错
每个节点只被访问一次,一次加法配一次赋值,时间 O(n);额外开销是递归栈,深度等于树高 h,平衡时约 O(log n),退化成一条链时到 O(n)。只有一个节点时,s 加上它自己、回填仍是原值;整棵树往右斜成一条链时,反中序就是从最右最大的那个节点起倒着走。
方向、清零、顺序,走反哪个答案都会崩。方向写成普通中序从小到大走,轮到某节点时比它大的还没进 s,回填必然偏小。s 中途清零,前面攒的更大值全丢,剩下的节点只累到自己往后那一截。加和回填的顺序一颠倒,先回填再加就是拿旧 s 盖掉原值,它自己那份反倒少算。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套「右根左降序、sum 先加原值、再把 sum 回填」,下面每帧都在套它。
- 4从根 50 先扎向右子树这是一棵二叉搜索树,右边的值都比左边大。我们要按从大到小的顺序访问,所以反中序:先右、再根、最后左。从根 50 出发,先一路往右下扎,去找整棵树最大的那个节点。
- 5去往更大的 70还轮不到 50,因为比它大的右子树还没处理。反中序先吃大的,沿右边往下走到右孩子 70,那边的值都比 50 大。
- 6去往更大的 80还轮不到 70,因为比它大的右子树还没处理。反中序先吃大的,沿右边往下走到右孩子 80,那边的值都比 70 大。
- 7sum 0 + 80 = 8080 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 80 加进累加和:0 加 80 等于 80。此刻 sum 里装的,就是所有不小于 80 的节点值之和。
- 8节点值 80 → 8080 是整棵树最大的值,没有比它更大的节点,sum 就等于它自己,回填后还是 80。
- 9sum 80 + 70 = 15070 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 70 加进累加和:80 加 70 等于 150。此刻 sum 里装的,就是所有不小于 70 的节点值之和。
- 10节点值 70 → 150把当前节点的值改写成 sum,也就是从 70 变成 150。比 70 大的节点早就加进 sum 了,所以这个 150 正是它该有的更大和。
- 11去往更小的 6070 自己回填完了,标成蓝色。反中序最后一步是左子树,走向左孩子 60,它比 70 小。注意 sum 不清零,继续往下累加。
- 12sum 150 + 60 = 21060 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 60 加进累加和:150 加 60 等于 210。此刻 sum 里装的,就是所有不小于 60 的节点值之和。
- 13节点值 60 → 210把当前节点的值改写成 sum,也就是从 60 变成 210。比 60 大的节点早就加进 sum 了,所以这个 210 正是它该有的更大和。
- 14sum 210 + 50 = 26050 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 50 加进累加和:210 加 50 等于 260。此刻 sum 里装的,就是所有不小于 50 的节点值之和。
- 15节点值 50 → 260把当前节点的值改写成 sum,也就是从 50 变成 260。比 50 大的节点早就加进 sum 了,所以这个 260 正是它该有的更大和。
- 16去往更小的 3050 自己回填完了,标成蓝色。反中序最后一步是左子树,走向左孩子 30,它比 50 小。注意 sum 不清零,继续往下累加。
- 17去往更大的 40还轮不到 30,因为比它大的右子树还没处理。反中序先吃大的,沿右边往下走到右孩子 40,那边的值都比 30 大。
- 18sum 260 + 40 = 30040 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 40 加进累加和:260 加 40 等于 300。此刻 sum 里装的,就是所有不小于 40 的节点值之和。
- 19节点值 40 → 300把当前节点的值改写成 sum,也就是从 40 变成 300。比 40 大的节点早就加进 sum 了,所以这个 300 正是它该有的更大和。
- 20sum 300 + 30 = 33030 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 30 加进累加和:300 加 30 等于 330。此刻 sum 里装的,就是所有不小于 30 的节点值之和。
- 21节点值 30 → 330把当前节点的值改写成 sum,也就是从 30 变成 330。比 30 大的节点早就加进 sum 了,所以这个 330 正是它该有的更大和。
- 22去往更小的 2030 自己回填完了,标成蓝色。反中序最后一步是左子树,走向左孩子 20,它比 30 小。注意 sum 不清零,继续往下累加。
- 23sum 330 + 20 = 35020 的右子树(比它大的那些)已经全部访问完了,现在轮到它。把它的原值 20 加进累加和:330 加 20 等于 350。此刻 sum 里装的,就是所有不小于 20 的节点值之和。
- 24节点值 20 → 350把当前节点的值改写成 sum,也就是从 20 变成 350。比 20 大的节点早就加进 sum 了,所以这个 350 正是它该有的更大和。
- 25答案 = [260,330,150,350,300,210,80]反中序访问顺序是 80、70、60、50、40、30、20,sum 一路累加成 80、150、210、260、300、330、350,依次回填到各节点。最大的 80 还是 80,最小的 20 收齐了所有节点变成 350。一遍 DFS 就把整棵树改成了更大和树。
⚠️ 容易写错的地方
✗ 错:用普通中序(左、根、右)从小到大走
✓ 对:要反中序(右、根、左)从大到小走
从小到大走,访问到某点时比它大的还没加进 sum,回填就是错的
✗ 错:每访问一个节点就把 sum 清零
✓ 对:sum 全程只增不清零
sum 要持续累积「比当前更大的所有值」,清零会丢掉前面的累加
✗ 错:先回填再加(顺序反了)
✓ 对:必须先 sum 加原值,再回填
先回填会用旧 sum 覆盖节点,少加了它自己这一份
完整代码(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 TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def bstToGst(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def dfs(root: Optional[TreeNode]):
if root is None:
return
dfs(root.right)
nonlocal s
s += root.val
root.val = s
dfs(root.left)
s = 0
dfs(root)
return rootC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* bstToGst(TreeNode* root) {
int s = 0;
function<void(TreeNode*)> dfs = [&]( TreeNode* root ) {
if (!root) {
return;
}
dfs(root->right);
s += root->val;
root->val = s;
dfs(root->left);
};
dfs(root);
return root;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private int s;
public TreeNode bstToGst(TreeNode root) {
dfs(root);
return root;
}
private void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.right);
s += root.val;
root.val = s;
dfs(root.left);
}
}复杂度
时间
O(n)
反中序遍历每个节点恰好访问一次,每次只做一次加法和一次赋值
空间
O(h)
递归栈深 = 树高 h;平衡时约 O(log n),最坏退化成链时 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 从二叉搜索树到更大和树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和 538「把二叉搜索树转换为累加树」是不是同一道?+
是同一道题的两个编号,做法一字不差,都是反中序遍历一路累加、边加边回填。1038 的题面里也点明了这一点,遇到 538 直接照搬这套即可。
如果给的不是 BST,是普通二叉树呢?+
那反中序就不再是降序,不能顺着一路累加了。得先把所有节点值收集下来,再对每个节点单独求「大于或等于它的值之和」,或者排好序后借前缀和来算,时间都会往上走。BST 占的便宜正是反中序天然降序,一趟就够。
能不能不用递归,把空间压到 O(1)?+
用栈写迭代版的反中序,空间还是 O(h)。想真做到 O(1) 可以上 Morris 反中序,靠临时改指针在遍历后再还原,不额外借栈;面试里把这个思路讲清楚一般就够了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 从二叉搜索树到更大和树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。