力扣 347. 前 K 个高频元素

题目:前 K 个高频元素

https://leetcode.cn/problems/top-k-frequent-elements/description/

给定一个整数数组 nums 和一个整数 k,返回出现频率前 k 高的元素。可以按任意顺序返回答案。

示例 1:

复制代码
输入:nums = [1,1,1,2,2,3], k = 2
输出:[1,2]

示例 2:

复制代码
输入:nums = [1], k = 1
输出:[1]

示例 3:

复制代码
输入:nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出:[1,2]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,即前 k 个高频元素的集合是唯一的

进阶: 设计一个时间复杂度 优于 O(n log n) 的算法。


解法一:哈希表 + 最小堆(O(n log k))

思路 :先用哈希表统计每个元素出现的次数,然后用一个大小为 k 的最小堆(优先队列)维护当前出现次数最多的 k 个元素。遍历哈希表,将每个元素加入堆中,若堆大小超过 k,则弹出堆顶(频率最小的元素)。最终堆中留下的就是前 k 个高频元素。

复杂度 :时间复杂度 O(n log k),空间复杂度 O(n)。由于 k ≤ n,且 log k ≤ log n,当 k 较小时优于 O(n log n)。

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> freq;
        for (int num : nums) freq[num]++;

        // 最小堆,按频率排序,存储 pair<频率, 元素>
        using pii = pair<int, int>;
        priority_queue<pii, vector<pii>, greater<pii>> minHeap;

        for (auto& [num, cnt] : freq) {
            minHeap.push({cnt, num});
            if (minHeap.size() > k) minHeap.pop();
        }

        vector<int> result;
        while (!minHeap.empty()) {
            result.push_back(minHeap.top().second);
            minHeap.pop();
        }
        return result;
    }
};

解法二:哈希表 + 桶排序(O(n))

思路:利用频率的范围(最大为 n),创建一个大小为 n+1 的桶数组,桶的下标表示频率,桶内存储对应频率的元素列表。先统计频率,然后将每个元素放入对应频率的桶中,最后从高到低遍历桶,收集前 k 个元素。

复杂度:时间复杂度 O(n),空间复杂度 O(n)。

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> freq;
        for (int num : nums) freq[num]++;

        int n = nums.size();
        vector<vector<int>> bucket(n + 1);  // 桶下标为频率
        for (auto& [num, cnt] : freq) {
            bucket[cnt].push_back(num);
        }

        vector<int> result;
        for (int i = n; i >= 0 && result.size() < k; --i) {
            for (int num : bucket[i]) {
                result.push_back(num);
                if (result.size() == k) break;
            }
        }
        return result;
    }
};

总结

  • 解法一 适用于 k 较小或中等的情况,代码简洁。
  • 解法二 达到了 O(n) 的最优时间复杂度,但需要额外 O(n) 空间。两种解法均可通过题目测试。

最大堆和最小堆详解

堆(Heap)是一种特殊的完全二叉树,它满足堆性质:对于最大堆 ,任意节点的值大于等于其子节点的值;对于最小堆,任意节点的值小于等于其子节点的值。堆通常用数组实现,因其完全二叉树特性,可以高效地完成插入、删除、取极值等操作。


一、基本概念

1. 完全二叉树

堆是一种完全二叉树,即除了最后一层外,所有层都填满,且最后一层的节点尽量靠左排列。这种结构可以用数组紧凑表示,根节点下标为 0(或 1),节点 i 的左孩子下标为 2i+1,右孩子为 2i+2,父节点为 (i-1)/2

2. 最大堆

  • 定义:对于任意节点 iheap[i] ≥ heap[2i+1]heap[i] ≥ heap[2i+2](若子节点存在)。
  • 性质:根节点是整个堆的最大值。

3. 最小堆

  • 定义:对于任意节点 iheap[i] ≤ heap[2i+1]heap[i] ≤ heap[2i+2](若子节点存在)。
  • 性质:根节点是整个堆的最小值。

二、堆的基本操作

1. 向上调整(上浮,Heapify Up)

当插入新元素时,先将新元素放在数组末尾,然后不断与其父节点比较,若不满足堆性质则交换,直到满足或到达根节点。

cpp 复制代码
void heapifyUp(vector<int>& heap, int idx) {
    while (idx > 0) {
        int parent = (idx - 1) / 2;
        if (heap[parent] < heap[idx]) { // 最大堆:父小于子则交换
            swap(heap[parent], heap[idx]);
            idx = parent;
        } else break;
    }
}

2. 向下调整(下沉,Heapify Down)

当删除堆顶元素时,将堆顶与最后一个元素交换,删除最后一个元素,然后从堆顶开始不断与较大的子节点交换(最大堆),直到满足堆性质。

cpp 复制代码
void heapifyDown(vector<int>& heap, int idx, int size) {
    while (2*idx+1 < size) {
        int child = 2*idx+1;
        // 选择较大的子节点(最大堆)
        if (child+1 < size && heap[child+1] > heap[child]) child++;
        if (heap[idx] < heap[child]) {
            swap(heap[idx], heap[child]);
            idx = child;
        } else break;
    }
}

3. 建堆(Build Heap)

给定一个无序数组,可以通过从最后一个非叶子节点开始依次向下调整,在 O(n) 时间内建成堆。

cpp 复制代码
void buildHeap(vector<int>& heap) {
    int n = heap.size();
    for (int i = n/2 - 1; i >= 0; --i) {
        heapifyDown(heap, i, n);
    }
}

4. 插入

在数组末尾添加新元素,然后向上调整。

cpp 复制代码
void push(vector<int>& heap, int val) {
    heap.push_back(val);
    heapifyUp(heap, heap.size()-1);
}

5. 删除堆顶

将堆顶与最后一个元素交换,删除最后一个元素,然后向下调整新的堆顶。

cpp 复制代码
void pop(vector<int>& heap) {
    if (heap.empty()) return;
    heap[0] = heap.back();
    heap.pop_back();
    heapifyDown(heap, 0, heap.size());
}

6. 获取堆顶

直接返回 heap[0]


三、时间复杂度

操作 时间复杂度
建堆 O(n)
插入 O(log n)
删除堆顶 O(log n)
获取堆顶 O(1)

四、C++ 中的堆实现:priority_queue

C++ 标准库提供了优先队列 priority_queue,默认是最大堆 (通过 std::less<T> 比较)。要得到最小堆,可以使用 std::greater<T>

1. 最大堆

cpp 复制代码
#include <queue>
priority_queue<int> maxHeap;   // 默认最大堆
maxHeap.push(3);
maxHeap.push(1);
maxHeap.push(4);
cout << maxHeap.top(); // 4

2. 最小堆

cpp 复制代码
priority_queue<int, vector<int>, greater<int>> minHeap;
minHeap.push(3);
minHeap.push(1);
minHeap.push(4);
cout << minHeap.top(); // 1

3. 自定义类型

需要提供比较函数,例如存储 pair<int,int> 按第一个元素排序:

cpp 复制代码
struct Compare {
    bool operator()(const pair<int,int>& a, const pair<int,int>& b) {
        return a.first > b.first; // 最小堆,按 first 升序
    }
};
priority_queue<pair<int,int>, vector<pair<int,int>>, Compare> pq;

五、典型应用场景

  1. 优先队列:任务调度、事件模拟。
  2. 求 Top K 问题:用最小堆维护前 K 大元素,用最大堆维护前 K 小元素。
  3. 堆排序:堆排序算法,时间复杂度 O(n log n),空间 O(1)。
  4. 合并 K 个有序链表:用最小堆每次取出最小值。
  5. 中位数维护:用最大堆存较小的一半,最小堆存较大的一半,可动态求中位数。

六、堆与 STL 其他容器的对比

特性 堆(priority_queue) 有序容器(set/map) 无序容器(unordered_set/map)
顺序 只保证堆顶最值 全局有序 无序
插入 O(log n) O(log n) O(1) 平均
查找 不支持快速查找 O(log n) O(1) 平均
删除 只能删除堆顶 可删除任意元素 可删除任意元素
适用场景 需要频繁取极值 需要有序遍历或查找 需要快速查找

七、总结

  • 堆是一种高效维护极值的数据结构,核心是上浮和下沉操作。
  • C++ 的 priority_queue 封装了堆,使用方便。
  • 理解堆的性质和操作,是解决许多算法问题的基础,如 Top K、合并 K 个有序序列、中位数等。
相关推荐
x_xbx2 小时前
LeetCode:217. 存在重复元素
数据结构·leetcode·哈希算法
漫随流水2 小时前
c++编程:求阶乘和
数据结构·c++·算法
再卷也是菜2 小时前
算法基础篇(13)单调栈
数据结构·c++
Frostnova丶2 小时前
LeetCode 2839. 判断通过操作能否让字符串相等 I
算法·leetcode
会编程的土豆2 小时前
【leetcode hot 100】二叉树3
算法·深度优先·图论
云栖梦泽2 小时前
Linux内核与驱动:2.驱动基础(编译驱动)
linux·服务器·c++
电商API_180079052472 小时前
API分享:获取淘宝商品价格|详情|主图|sku信息
开发语言·c++·人工智能·数据分析
ofoxcoding3 小时前
GPT-5.4 API 完全指南:性能实测、成本测算与接入方案(2026)
人工智能·gpt·算法·ai
码农的神经元3 小时前
基于改进 VMD 与自适应小波的水声信号去噪算法实现与分析
算法