Skip to content

二叉搜索树中的众数

约 1659 字大约 6 分钟

2025-02-28

501. 二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

示例 1:

img.png

输入:root = [1,null,2,2]
输出:[2]

示例 2:

输入:root = [0]
输出:[0]

提示:

  • 树中节点的数目在范围 [1, 10^4]
  • -10^5 <= Node.val <= 10^5

进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

中序遍历+贪心

在中序位置更新众数及其次数。

之所以用中序,是因为二叉搜索树的中序遍历是有序的,所以重复的数字会在一起。所以只需要变量记录当前数字即可, 如果遇到新数字,则更新变量,因为后面不可能出现前面的数字了,所以直接更新即可。 如果无序,则后面也可能出现前面的数字,所以不能舍弃历史数据,所以要用数组或哈希表这种具有记忆能力的数据结构。

class Solution {
    List<Integer> answer = new ArrayList<>(); // 存储众数的列表
    int base, count, maxCount; // base 表示当前值,count 表示当前值的出现次数,maxCount 表示众数的出现次数

    public int[] findMode(TreeNode root) {
        dfs(root); // 深度优先搜索遍历树
        
        int[] mode = new int[answer.size()]; // 将结果从列表转换为数组
        for (int i = 0; i < answer.size(); ++i) {
            mode[i] = answer.get(i);
        }
        return mode; // 返回众数数组
    }

    public void dfs(TreeNode node) {
        if (node == null) {
            return; // 如果节点为空,返回
        }
        dfs(node.left); // 递归遍历左子树
        update(node.val); // 更新当前值的出现次数。当处理当前值时,我们已经把左子树的节点的出现次数更新过了。
        dfs(node.right); // 递归遍历右子树
    }

    public void update(int x) {
        // base默认是0,count默认是0
        if (x == base) { // base其实是前一个数,count是这个数连续出现的次数,注意是连续,在中序遍历中,相同的数一定连续出现。
            ++count; // 如果当前值与 base 相同,增加出现次数
        } else {
            count = 1; // 如果当前值与 base 不同,说明遇到了新数,重置出现次数为 1
            base = x; // 更新 base 为当前值。重点。
        }
        
        // 计算完次数后,检查该次数是不是当前最大值。
        if (count == maxCount) {
            answer.add(base); // 如果当前值的出现次数等于最大出现次数,将其加入众数列表
        }
        if (count > maxCount) {
            maxCount = count; // 如果当前值的出现次数超过最大出现次数,更新最大出现次数
            answer.clear(); // 清空众数列表。重点。
            answer.add(base); // 将当前值加入众数列表
        }
    }
}
  • 时间复杂度:O(n)。即遍历这棵树的复杂度。
  • 空间复杂度:O(n)。即递归的栈空间的空间代价。

Morris中序遍历

Morris 中序遍历的一个重要步骤就是寻找当前节点的前驱节点,并且 Morris 中序遍历寻找下一个点始终是通过转移到 right 指针指向的位置来完成的。

  • 如果当前节点没有左子树,则遍历这个点,然后跳转到当前节点的右子树。
  • 如果当前节点有左子树,那么它的前驱节点一定在左子树上,我们可以在左子树上一直向右行走,找到当前点的前驱节点。
    • 如果前驱节点没有右子树,就将前驱节点的 right 指针指向当前节点。这一步是为了在遍历完前驱节点后能找到前驱节点的后继,也就是当前节点。
    • 如果前驱节点的右子树为当前节点,说明前驱节点已经被遍历过并被修改了 right 指针,这个时候我们重新将前驱的右孩子设置为空, 遍历当前的点,然后跳转到当前节点的右子树。
class Solution {
    int base, count, maxCount; // base 表示当前值,count 表示当前值的出现次数,maxCount 表示众数的出现次数
    List<Integer> answer = new ArrayList<>(); // 存储众数的列表

    /**
     * 查找二叉搜索树中的众数
     *
     * @param root 二叉搜索树的根节点
     * @return 众数数组
     */
    public int[] findMode(TreeNode root) {
        TreeNode cur = root, pre = null; // cur 表示当前节点,pre 表示当前节点的前驱节点
        
        while (cur != null) {
            if (cur.left == null) {
                // 如果当前节点的左子节点为空,直接更新当前值的出现次数
                update(cur.val);
                cur = cur.right; // 移动到右子节点
                continue;
            }
            
            pre = cur.left; // 找到当前节点左子树的最右节点
            while (pre.right != null && pre.right != cur) {
                pre = pre.right;
            }
            
            if (pre.right == null) {
                // 如果前驱节点的右子节点为空,将其指向当前节点
                pre.right = cur;
                cur = cur.left; // 移动到左子节点
            } else {
                // 如果前驱节点的右子节点指向当前节点,说明已访问过左子树
                pre.right = null;
                update(cur.val); // 更新当前值的出现次数
                cur = cur.right; // 移动到右子节点
            }
        }
        
        int[] mode = new int[answer.size()]; // 将结果从列表转换为数组
        for (int i = 0; i < answer.size(); ++i) {
            mode[i] = answer.get(i);
        }
        return mode; // 返回众数数组
    }

    /**
     * 更新当前值的出现次数,并检查是否需要更新众数列表
     *
     * @param x 当前节点的值
     */
    public void update(int x) {
        if (x == base) {
            ++count; // 如果当前值与 base 相同,增加出现次数
        } else {
            count = 1; // 如果当前值与 base 不同,重置出现次数为 1
            base = x; // 更新 base 为当前值
        }
        if (count == maxCount) {
            answer.add(base); // 如果当前值的出现次数等于最大出现次数,将其加入众数列表
        }
        if (count > maxCount) {
            maxCount = count; // 如果当前值的出现次数超过最大出现次数,更新最大出现次数
            answer.clear(); // 清空众数列表
            answer.add(base); // 将当前值加入众数列表
        }
    }
}
  • 时间复杂度:O(n)。每个点被访问的次数不会超过两次,故这里的时间复杂度是 O(n)。
  • 空间复杂度:O(1)。使用临时空间的大小和输入规模无关。