力扣 692:巧用小顶堆高效求解前K个高频单词

@

📖 前言

在算法刷题与工程开发之中,词频统计、高频元素筛选是极为经典的核心场景✨。无论是文本数据分析、关键词提取、日志统计,还是LeetCode经典算法题型,前K个高频单词的求解思路,都是程序员必须掌握的基础高阶算法思维。

寻常解题之法,多以暴力排序遍历,虽逻辑直白,却效率堪忧;而哈希表统计频次 + 小顶堆筛选极值的组合解法,兼顾时空复杂度优势,章法严谨、思路精妙🌿。本文将以骈文雅致之语,层层拆解算法核心逻辑,附完整C++可运行代码、原理流程图解、细节易错点解析,带你彻底吃透这一经典算法。

Bilibili 同步视频

力扣 692:巧用小顶堆高效求解前K个高频单词

🌐 算法核心场景与解题痛点剖析

1. 问题场景定义

给定一组单词字符串数组与整数K,需求为:筛选出数组中出现频次最高的前K个单词,排序规则严格遵循双优先级:

  • 🥇 第一优先级:单词出现频次从高到低排序

  • 🥈 第二优先级:频次相同时,按单词字典序从小到大排序

2. 传统解法弊端

若采用朴素思路:先遍历统计所有单词频次,再对全部单词直接排序,虽可实现功能,却存在显著缺陷❌:

  • 数据量庞大时,全局排序时间复杂度极高,冗余计算过多

  • 无需对所有数据排序,仅需保留前K个极值,全局排序造成性能浪费

是以业界最优解,皆依托哈希表+小顶堆的组合思想,择优选取、去芜存菁,以最低时间复杂度实现核心需求✅。

⚙️ 核心算法原理(图文拆解)

此番解题之术,分三步行云流水、环环相扣:哈希表统计词频 → 小顶堆筛选前K元素 → 结果二次规整排序,层层递进、逻辑闭环🔗。

1. 算法整体流程示意图(Plain Text)

原始单词数组 → 哈希表遍历统计 → 生成【单词-频次】映射关系
       ↓
构建自定义规则小顶堆 → 逐个插入单词元素 → 堆超K则弹出最小值(低频单词)
       ↓
堆内留存TopK高频单词 → 按题目双规则二次排序 → 输出最终有序结果

2. 分步原理深度解析

✅ 第一步:哈希表遍历,精准统计词频

天下算法,统计为先;万物有序,数据为基📊。想要筛选高频单词,必先量化每个单词的出现次数。

哈希表(Hash Map)凭借O(1)级别的增删查改效率,成为词频统计的最优数据结构。我们以单词为键(key)、出现频次为值(value),遍历原始单词数组,逐一对对应单词的频次进行累加,最终得到所有单词的完整频次映射关系。

此步核心要义:去重统计、精准量化,将无序的原始文本数据,转化为结构化的频次数据,为后续筛选排序筑牢根基。

✅ 第二步:自定义小顶堆,筛选TopK元素

求前K大极值,必用小顶堆;求前K小极值,必用大顶堆🌲。此为算法解题亘古不变的核心准则。

为何舍弃大顶堆而选用小顶堆?缘由精妙:小顶堆堆顶始终为当前堆内最小值元素,遍历插入所有单词时,若堆中元素数量超出K值,直接弹出堆顶低频元素,全程保留最优的K个高频单词,无需存储全部数据,极大节省内存空间。

且本题需自定义堆排序规则,双维度约束、精准适配题意:

  • 频次不等:频次更高的单词优先级更高

  • 频次相等:字典序更小的单词优先级更高

✅ 第三步:二次规整排序,输出标准结果

小顶堆筛选完成后,堆内元素为前K个高频单词,但堆结构本身无法保证全局有序。是以最后需对留存元素,再次按照「频次降序、字典序升序」的规则排序,最终输出完全符合题意的有序结果🎯。

💻 C++ 完整可运行代码实现

依托上述原理,结合C++ STL容器特性,编写完整版高效代码,注释详尽、可直接编译运行,适配各类刷题场景与工程测试👇

#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
#include <algorithm>
using namespace std;

// 自定义比较规则:适配小顶堆排序逻辑
struct CMP {
    // 存储单词与对应频次
    pair<string, int> val;
    CMP(pair<string, int> v) : val(v) {}

    // 重载比较运算符:构建符合题意的排序规则
    bool operator<(const CMP& other) const {
        // 频次不同:频次低的优先弹出(小顶堆核心)
        if (val.second != other.val.second) {
            return val.second > other.val.second;
        }
        // 频次相同:字典序大的优先弹出,保留字典序小的单词
        return val.first < other.val.first;
    }
};

vector<string> topKFrequent(vector<string>& words, int k) {
    // 1. 哈希表统计所有单词频次 O(n)
    unordered_map<string, int> frequency;
    for (string word : words) {
        frequency[word]++;
    }

    // 2. 构建自定义小顶堆
    priority_queue<CMP> minHeap;
    for (auto& item : frequency) {
        minHeap.push(CMP(item));
        // 堆元素超过K,弹出频次最小/字典序最大的元素
        if (minHeap.size() > k) {
            minHeap.pop();
        }
    }

    // 3. 提取堆内结果,二次规整排序
    vector<pair<string, int>> tempRes;
    while (!minHeap.empty()) {
        tempRes.push_back(minHeap.top().val);
        minHeap.pop();
    }

    // 最终排序:频次降序,同频次字典序升序
    sort(tempRes.begin(), tempRes.end(), [](pair<string, int>& a, pair<string, int>& b) {
        if (a.second != b.second) {
            return a.second > b.second;
        }
        return a.first < b.first;
    });

    // 提取最终单词结果
    vector<string> res;
    for (auto& item : tempRes) {
        res.push_back(item.first);
    }
    return res;
}

// 测试主函数
int main() {
    vector<string> testWords = {"i", "love", "leetcode", "i", "love", "coding"};
    int k = 2;
    vector<string> result = topKFrequent(testWords, k);
    
    cout << "前" << k << "个高频单词:" << endl;
    for (string word : result) {
        cout << word << " ";
    }
    return 0;
}

⚡ 算法性能复杂度分析

算法之优劣,必以时空复杂度为标尺,此番解法性能优异、适配海量数据场景📈:

1. 时间复杂度

  • 词频统计:遍历所有单词,耗时 O(n)(n为单词总数)

  • 堆筛选:每个元素入堆、出堆操作耗时 O(logK),总耗时O(nlogK)

  • 结果排序:仅对K个元素排序,耗时 O(KlogK)

  • 整体复杂度:O(nlogK),远优于全局排序的 O(nlogn)

2. 空间复杂度

  • 哈希表存储所有不重复单词,空间 O(m)(m为不重复单词数)

  • 小顶堆仅存储K个元素,空间 O(K)

  • 整体空间复杂度:O(m + K),内存占用可控、轻量化高效

💡 拓展答疑与学习干货

1. 可否用Map替代UnorderedMap?

可也,但非最优✨。ordered map(有序map)可自动维护键值有序性,但其底层为红黑树,增删查改效率低于哈希表。本题无需预处理数据有序性,unordered_map 哈希表的无序存储特性,更贴合高效统计的核心需求,冗余开销更低。

2. 直接全局排序可行吗?

可行但低效❌。全局排序依旧需要先通过哈希表统计词频,并未省略核心步骤,且海量数据下,全局排序的时间开销远大于堆筛选,数据量级越大,性能差距越明显。

3. 堆排序是最优排序算法吗?

非也🌿。在专业算法与数据结构体系中,存在多种优于堆排序、快速排序的高阶排序算法。算法学习的核心,不在于死记排序模板,而在于掌握场景适配思维——按需择取最优解法,方为算法之道。

📝 编程学习核心感悟

算法之力,为思维之魂;代码之力,为落地之躯🔑。二者看似独立,实则相辅相成、共生共长:算法思维决定解题高度,代码功底决定落地精度。

听课求学,重在参悟解题逻辑、搭建思维框架,而非拘泥于单一语言的代码细节;技能精进,贵在躬身实操、线下深耕,而非浅尝辄止、线上虚学。C++语法晦涩精妙,非一书可尽学,需多册典籍相辅、千行代码沉淀,方能融会贯通、运用自如✨。

🏁 总结

前K个高频单词的解法,以哈希表统计、小顶堆筛选、自定义排序为三重核心,化繁为简、去冗存精。相较于暴力排序,此算法极大优化时空复杂度,是极值类算法场景的经典范式🌐。

力扣 692:巧用小顶堆高效求解前K个高频单词

吃透此番逻辑,不仅可秒杀刷题题型,更能迁移应用于文本统计、数据筛选、流量分析等各类工程场景,切实提升算法思维与代码实战能力!

posted on 2026-08-02 22:16  郝学胜-神的一滴  阅读(0)  评论(0)    收藏  举报