← 返回题目列表

如何找到二叉搜索树中的众数?

中等 第 24 / 27 题 更新于 2026/07/30
BST中序遍历众数计数

简化版

BST 中序遍历会得到有序序列,相同值会连续出现。遍历时维护当前值、当前连续次数、历史最大次数和答案列表,就能 O(n) 找到所有众数。

详细版

如果不利用 BST 性质,可以用 HashMap 统计每个值出现次数,时间 O(n)、空间 O(n)。但 BST 的中序序列有序,相同值连续,所以可以只用几个变量在线统计:

  1. prev:上一个访问的节点值;
  2. count:当前值连续出现次数;
  3. maxCount:目前发现的最大次数;
  4. modes:所有出现次数等于 maxCount 的值。

每访问一个值:

  • 若等于 prevcount++
  • 否则切换到新值,count=1
  • count > maxCount,清空答案并加入当前值;
  • count == maxCount,追加当前值。

时间复杂度 O(n),递归栈空间 O(h),答案空间不计时额外空间接近 O(h)。

完整版教学

一、众数问题和 BST 有什么关系

众数是出现次数最多的值。普通二叉树里,相同值可能分散在任意位置,所以通常要 HashMap 统计全局频次。BST 不同:它的中序遍历是非降序序列,所有相同值会被挤在一起,像排序数组一样连续出现。于是问题就从「统计任意散落的频次」变成「统计连续段长度」。

举个例子,BST 中序结果是 [1,2,2,2,3,4,4]。值 2 连续 3 次,值 4 连续 2 次,所以众数是 2。我们不需要记住每个值的次数,只要在遍历过程中记录当前连续段长度即可。

记忆钩子:BST 的中序序列像一排排好队的人,相同值站在一起;找众数就是找最长的那一段队伍。

二、为什么相同值会连续出现

BST 的定义通常是左子树值不大于根,右子树值不小于根,或者更严格地左小右大。无论采用哪种重复值放置策略,只要它仍保证中序遍历非降序,相同值在中序序列里就不会被更大或更小的值隔开。因为一旦出现 x ... y ... xy != x,非降序顺序就会被破坏。

这个性质非常关键。它让我们可以用 prevcount 模拟排序数组的游程统计:

中序序列: 1 2 2 2 3 4 4
连续段:   [1] [2 2 2] [3] [4 4]
频次:      1      3     1    2

如果不是 BST,DFS 顺序可能是 [2,1,2,4,2],相同值不连续,单靠 prev/count 就会算错。

三、一次遍历如何维护答案

核心变量有 4 个:prevValcountmaxCountans。访问一个新值 x 时,先判断它是否和上一个值相同。如果相同,连续段长度加 1;如果不同,开启新连续段,长度回到 1。然后拿当前 countmaxCount 比较。

Integer prev = null;
int count = 0, maxCount = 0;
List<Integer> ans = new ArrayList<>();

void handle(int x) {
    if (prev != null && prev == x) count++;
    else count = 1;
    prev = x;

    if (count > maxCount) {
        maxCount = count;
        ans.clear();
        ans.add(x);
    } else if (count == maxCount) {
        ans.add(x);
    }
}

注意 prevInteger 是为了表达「还没有上一个值」。如果节点值范围很宽,不建议用某个哨兵值假装空值。

四、完整代码和执行顺序

handle 嵌入中序遍历即可。中序访问顺序必须是左、根、右;如果写成前序或后序,相同值不一定连续,统计就失效。

List<Integer> findMode(TreeNode root) {
    inorder(root);
    return ans;
}

void inorder(TreeNode node) {
    if (node == null) return;
    inorder(node.left);
    handle(node.val);
    inorder(node.right);
}

执行过程可以用一个小表看清楚:

访问值countmaxCountans
111[1]
211[1,2]
222[2]
312[2]
322[2,3]

当出现新的更大频次时要清空旧答案,因为旧答案已经不是众数;当频次追平时要追加,因为众数可能有多个。

五、空间复杂度为什么常说接近 O(h)

如果不算输出答案,算法只维护几个变量,递归调用栈是 O(h),h 是树高。平衡 BST 中 h≈log n,退化 BST 中 h≈n。如果题目要求严格 O(1) 额外空间,可以用 Morris 中序遍历消除递归栈,但代码复杂度会上升。

很多题解会说「不用额外空间」,这通常默认不计算递归栈或输出数组。面试时最好说严谨一点:递归版额外空间 O(h),Morris 版可做到 O(1) 额外空间但会临时改动树指针再恢复。

六、和 HashMap 统计相比有什么取舍

HashMap 统计更通用,不要求树有序。它遍历每个节点,把值映射到次数,最后扫描 Map 找最大频次。缺点是空间 O(k),k 是不同值个数,最坏 O(n)。

方法利用 BST时间额外空间实现风险
HashMap 统计O(n)O(n)
中序连续计数O(n)O(h)
Morris 中序O(n)O(1)

如果题目明确是 BST,应优先讲中序连续计数;如果题目只是普通二叉树,HashMap 才是稳妥答案。

七、常见误区与追问

  • 误区:BST 众数可以直接看根节点。 根的位置不代表频次,众数必须基于全树统计。
  • 追问:为什么中序能让相同值连续? 因为中序结果非降序,相同值之间不可能夹着不同大小的值。
  • 误区:count == maxCount 时不需要追加答案。 众数可能有多个,追平最大频次也要记录。
  • 追问:如果树不允许重复值怎么办? 每个值次数都是 1,所有节点都是众数,算法仍然成立。
  • 误区:递归版一定是 O(1) 空间。 递归调用栈是 O(h),除非改成 Morris 遍历并恢复指针。

八、加强记忆

这题的核心不是「众数」两个字,而是 BST 中序的排序效果。排序后,相同值自然连续,统计频次就变成统计连续段长度。牢记 4 个变量:上一个值、当前连续次数、最大次数、答案列表。新的最大次数出现时清空答案,追平最大次数时追加答案;这样才能正确处理多个众数。