别总想着排序!我在数据看板中悟出的O(N)求第三大数神技(414. 第三大的数)

别总想着排序!我在数据看板中悟出的O(N)求第三大数神技😎

大家好,我是一个热爱在代码海洋里冲浪的老兵。今天想和大家分享一个我在实际项目中遇到的趣事,以及它如何让我对一个看似简单的算法题有了更深的理解。这趟旅程充满了"踩坑"的教训和"原来如此"的顿悟,希望能给你带来一些启发!

我遇到了什么问题

故事发生在一个数据看板的开发任务中。产品经理小姐姐(PM)希望在我们的电商后台首页加一个"爆款商品追踪"模块,实时展示销量排名前三的商品。

听起来很简单,对吧?我当时也是这么想的。但PM接着补充了几个细节:

  1. 销量数据是实时流动的,我们需要一个高效的方法来更新排名。
  2. 排名是基于不同商品的。一个商品卖出多少件都只算一个排名位。
  3. 如果当天开张,商品种类还不足3种,那就显示当前销量最高的商品。

这下问题就变得具体了。我需要处理一个动态的数据流,并快速找出其中"第三大的不同数值"。如果不够三个,就返回最大的那个。

这场景,简直和 LeetCode 上的一道题不谋而合:

414. 第三大的数

给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。

在动手之前,我们先看看题目的"友情提示"里藏着什么玄机:

  • 1 <= nums.length <= 10^4:数组长度不算太大,O(N log N) 的解法(比如排序)是可以通过的。但这暗示了有更优的 O(N) 解法,这也是进阶目标。
  • -2^31 <= nums[i] <= 2^31 - 1:这是一个关键的"坑"!数值范围覆盖了整个 int 类型。这意味着,如果你想用一个很小的数(比如 Integer.MIN_VALUE)作为初始的"最大值",一旦数组里真的出现了 Integer.MIN_VALUE,你的逻辑可能就乱套了。我们得小心处理!

好,热身完毕,开干!

我是如何用算法"组合拳"解决的

解法一:先排序,大力出奇迹

这是我最开始想到的方法,简单粗暴,但有效。思路三步走:去重 -> 排序 -> 取值

先把数组里所有重复的商品 ID 去掉,然后从大到小排个序,第三个不就是我们想要的嘛!

java 复制代码
import java.util.Collections;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/* 
 * 思路:排序与去重。先用 HashSet 去除重复元素,然后将 Set 转换为 List 进行排序,
 * 最后根据 List 的大小判断返回第三大还是最大的数。
 * 时间复杂度:O(N log N),主要耗时在排序上。
 * 空间复杂度:O(N),最坏情况下所有元素都不同,需要存储所有元素。
 */
class Solution {
    public int thirdMax(int[] nums) {
        // 1. 去重
        // 为啥用 HashSet?因为它基于哈希表,add 操作的平均时间复杂度是 O(1),
        // 能非常高效地帮我们过滤掉重复的商品ID。
        Set<Integer> distinctNums = new HashSet<>();
        for (int num : nums) {
            distinctNums.add(num);
        }

        // 2. 排序
        // Set 是无序的,没法按下标取值。所以要转成 List。
        List<Integer> sortedList = new ArrayList<>(distinctNums);
        // Collections.sort() 是 Java 的标准排序工具,配合 Collections.reverseOrder() 实现降序,简单好用。
        Collections.sort(sortedList, Collections.reverseOrder());

        // 3. 取值
        // 如果去重后的商品种类少于3种,说明没有"第三名"
        if (sortedList.size() < 3) {
            // 按要求返回"冠军"
            return sortedList.get(0);
        } else {
            // 否则,返回光荣的"季军",也就是列表里下标为2的元素
            return sortedList.get(2);
        }
    }
}

复杂度

时间复杂度:O(N log N)。遍历数组构建 HashSet 是 O(N),但瓶颈在于对去重后的列表排序,最坏情况下是 O(N log N)。

空间复杂度:O(N)。在最坏情况(所有商品都不同)下,HashSetArrayList 都需要存储 N 个元素。

这个方法虽然能解决问题,但每次来新数据都要全盘排序一次,效率不高。PM小姐姐要的是"实时",这显然不够快。

解法二:有序集合,优雅永不过时

排序太慢,那有没有办法在遍历的时候就顺便把大小关系维护好呢?当然有!这时候,TreeSet 闪亮登场。

TreeSet 是一个神奇的数据结构,它不仅能像 HashSet 一样自动去重,还能随时保持内部元素处于有序状态!我们可以用它来维护一个始终不超过3个元素的"优胜者小组"。

java 复制代码
import java.util.TreeSet;

/* 
 * 思路:使用有序集合 TreeSet。在一次遍历中维护一个最多包含3个元素的
 * 有序集合。这样可以同时完成去重、排序和筛选前三大数的操作。
 * 时间复杂度:O(N),因为每次 TreeSet 操作是 O(log K),K最大为3,是常数。
 * 空间复杂度:O(1),因为 TreeSet 最多存储3个元素。
 */
class Solution {
    public int thirdMax(int[] nums) {
        // TreeSet 是一个有序且唯一的集合,基于红黑树实现。
        // 它的 add, remove, first, last 操作都是 O(log K) 级别,K是集合大小。
        // 在我们这,K最多是3,所以这些操作几乎是 O(1) 的,非常快!
        TreeSet<Integer> topThree = new TreeSet<>();
        for (int num : nums) {
            topThree.add(num);
            // 如果"优胜者小组"人数超过了3个
            if (topThree.size() > 3) {
                // 就把成绩最差的(最小的那个)淘汰掉
                topThree.remove(topThree.first());
            }
        }

        // 遍历结束后,看看小组里有几个人
        if (topThree.size() == 3) {
            // 如果正好3个人,第三大的就是里面成绩最差的那个(因为TreeSet升序排列)
            return topThree.first();
        } else {
            // 如果不足3个人,就返回成绩最好的那个(最大的)
            return topThree.last();
        }
    }
}

复杂度

时间复杂度:O(N)。我们遍历数组一次,每次对 TreeSet 的操作是 O(log K)。因为我们把K的大小限制在3,所以 O(log 3) 是一个常数。总复杂度就是 O(N)。

空间复杂度:O(1)。TreeSet 里最多只放3个元素,占用的空间是恒定的。

这个解法简直太棒了!😉 它不仅满足了 O(N) 的进阶要求,代码还非常优雅。这算是我当时的一个"恍然大悟"的瞬间。

解法三:一次遍历,手动挡的极致性能

作为一个追求极致的开发者,我还想:能不能不用任何额外的数据结构,只用几个变量就搞定?答案是肯定的,这就是"手动挡"的乐趣所在!

我们可以用三个变量 firstMax, secondMax, thirdMax 来实时追踪前三名。

这里有个大坑! 正如前面提示分析的,我们不能简单地把变量初始化为 Integer.MIN_VALUE。我选择用 Integer 包装类,它们的默认值是 null,这个 null 完美地代表了"这个名次目前虚位以待"的状态。

java 复制代码
/* 
 * 思路:一次遍历,使用三个变量来维护当前找到的前三大数。
 * 这种方法避免了使用额外的数据结构,实现了时间和空间的最优化。
 * 时间复杂度:O(N),仅需遍历数组一次。
 * 空间复杂度:O(1),只使用了固定的几个变量。
 */
class Solution {
    public int thirdMax(int[] nums) {
        // 使用 Integer 包装类,用 null 来表示"尚未找到"的状态,
        // 这比用 Long.MIN_VALUE 或其他哨兵值更清晰,也更安全。
        Integer firstMax = null;
        Integer secondMax = null;
        Integer thirdMax = null;

        for (Integer num : nums) { // 自动装箱为 Integer,方便与 null 比较
            // 如果这个商品ID已经榜上有名,直接跳过
            if (num.equals(firstMax) || num.equals(secondMax) || num.equals(thirdMax)) {
                continue;
            }

            // 如果 num 发现了新的"冠军"
            if (firstMax == null || num > firstMax) {
                thirdMax = secondMax;  // 原亚军变季军
                secondMax = firstMax; // 原冠军变亚军
                firstMax = num;       // 新科冠军登场
            } 
            // 否则,如果 num 发现了新的"亚军"
            else if (secondMax == null || num > secondMax) {
                thirdMax = secondMax; // 原亚军变季军
                secondMax = num;      // 新亚军就位
            } 
            // 否则,如果 num 发现了新的"季军"
            else if (thirdMax == null || num > thirdMax) {
                thirdMax = num;       // 新季军补位
            }
        }

        // 比赛结束,如果"季军"位置还是空的,说明商品不足3种
        // 此时按规则返回"冠军"
        return thirdMax == null ? firstMax : thirdMax;
    }
}

复杂度

时间复杂度:O(N)。只需一次完整的遍历。

空间复杂度:O(1)。只用了三个变量,空间消耗是常数级别的。

这个解法是性能上的王者,它用最少的资源完成了任务。虽然逻辑判断稍微复杂一些,但这正是算法的魅力所在------于细微处见真章。

举一反三:Top-K 思想的应用

掌握了找"第三大"的技巧,实际上你已经掌握了更通用的"Top-K"问题的钥匙。这个思想可以用在很多地方:

  • 游戏排行榜:实时更新的玩家得分 Top 10。
  • 系统监控:找出系统中响应最慢的 5 个 API 接口。
  • 社交网络:计算一篇文章下点赞数最多的 3 条评论。

练练手,更熟练

如果你对这类问题意犹未尽,不妨挑战一下这些"亲戚"题目:

希望这次的分享能让你有所收获。记住,面对问题,别总想着排序,有时候换个思路,就能发现一片新天地!😎

解法对比

特性 解法一 (排序去重) 解法二 (有序集合) 解法三 (一次遍历)
核心思想 先去重,再排序,最后取值 边遍历边维护一个大小为K的有序集合 用K个变量实时追踪Top-K元素
代码简洁度 中等 低(逻辑判断多)
时间复杂度 O(N log N) O(N) O(N)
空间复杂度 O(N) O(1) O(1)
适用场景 数据量小,或一次性计算 需要高效实时更新,代码优雅 追求极致性能,不介意稍复杂的逻辑
相关推荐
江畔柳前堤2 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore3 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考5 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX5 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面6 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号6 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽6 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI6 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者8 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者8 小时前
【征程6】校准量化中HistogramObserver解析
算法