0347-Top-K-Frequent-Elements
Problem Description
Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order.
Solution
以下做法一到三共同部份是要先計算出項目出現頻率
初步撰寫,將項目與頻率存入 vector 後做排序
// Time: O(nlog(n))
// Space: O(n)
vector<int> topKFrequent(vector<int> &nums, int k) {
unordered_map<int, int> mp;
for (int n : nums) {
++mp[n];
}
vector<pair<int, int>> x;
for (auto [k, v] : mp) {
x.push_back({k, v});
}
ranges::sort(x, [](auto a, auto b) { return a.second > b.second; });
vector<int> ans;
for (int i = 0; i < k; ++i) {
ans.push_back(x[i].first);
}
return ans;
}
可以將 sort 修改成 C++17 後的 partial_sort 達到 O(nlog(k))
第二種做法,使用 min heap 或 red-black tree 將求 top k 的時間複雜度拉低
// Time: O(nlog(k))
// Space: O(n)
vector<int> topKFrequent(vector<int> &nums, int k) {
unordered_map<int, int> mp;
for (int n : nums) {
++mp[n];
}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
for (auto [num, freq] : mp) {
pq.push({freq, num});
if (pq.size() > k) {
pq.pop();
}
}
vector<int> ans;
while (pq.size()) {
ans.push_back(pq.top().second);
pq.pop();
}
return ans;
}
注意 C++ 沒有辦法用 iterator 遍歷 priority queue
單純的 set 做法,時間複雜度與 heap 相同,但維持紅黑樹的成本比 heap 還高,所以不推薦,統一用 heap 會比較快
值得注意的是沒有開優化時反而 set 比較快
// Time: O(nlog(k))
// Space: O(n)
vector<int> topKFrequent(vector<int> &nums, int k) {
unordered_map<int, int> mp;
for (int n : nums) {
++mp[n];
}
set<pair<int, int>> st;
for (auto [num, freq] : mp) {
st.insert({freq, num});
if (st.size() > k) {
st.erase(st.begin());
}
}
vector<int> ans;
for (auto [freq, num] : st) {
ans.push_back(num);
}
return ans;
}
第三種做法使用桶排序,桶子編號是出現頻率,內容是數字,會快很多
// Time: O(n)
// Space: O(n)
vector<int> topKFrequent(vector<int> &nums, int k) {
unordered_map<int, int> mp;
for (int n : nums) {
++mp[n];
}
vector<vector<int>> buckets(nums.size() + 1);
for (auto [num, freq] : mp) {
buckets[freq].push_back(num);
}
vector<int> ans;
for (int i = nums.size(); i >= 0; --i) {
for (auto n : buckets[i]) {
ans.push_back(n);
if (ans.size() == k) {
return ans;
}
}
}
return {};
}
關鍵點是桶子數量上限可以開輸入數字數量 + 1,因為最極端就是全部都是相同的數字,加一是包含 0 方便計算
另外注意可能有重複情況,所以要開 vector<vector<int>> 而不是 vector<int>
第四種做法,使用 PBDS 裡的 priority queue,支援修改值,因為頻繁修改此處表現會最差
// Time: O(nlog(k))
// Space: O(n)
using pq_type = __gnu_pbds::priority_queue<pair<int, int>, greater<>,
__gnu_pbds::pairing_heap_tag>;
vector<int> topKFrequent(vector<int> &nums, int k) {
pq_type pq;
unordered_map<int, pq_type::point_iterator> iterators;
unordered_map<int, int> count;
for (int num : nums) {
count[num]++;
int freq = count[num];
if (iterators.contains(num)) {
pq.modify(iterators[num], {freq, num});
} else {
iterators[num] = pq.push({freq, num});
if (pq.size() > k) {
int removed = pq.top().second; // 使用 min-heap 因為打算移掉最低頻率
iterators.erase(removed);
pq.pop();
}
}
}
vector<int> result;
while (!pq.empty()) {
result.push_back(pq.top().second);
pq.pop();
}
return result;
}
// Time: O(nlog(n))
// Space: O(n)
using pq_type = __gnu_pbds::priority_queue<pair<int, int>, less<>,
__gnu_pbds::pairing_heap_tag>;
vector<int> topKFrequent(vector<int> &nums, int k) {
pq_type pq;
unordered_map<int, pq_type::point_iterator> iterators;
for (int num : nums) {
if (iterators.contains(num)) {
pq.modify(iterators[num], {iterators[num]->first + 1, num});
} else {
iterators[num] = pq.push({1, num});
}
}
// 一次建立完,然後從 heap 取頻率最大,所以用 max-heap
vector<int> ans;
while (k--) {
ans.push_back(pq.top().second);
pq.pop();
}
return ans;
}
| 版本 | 時間複雜度 | 空間複雜度 | 原理 |
|---|---|---|---|
| v1 排序法 | O(nlog(n)) | O(n) | 統計頻率後,將所有元素按頻率排序,取前 k 個 |
| v2 最小堆**(最佳選擇)** | O(nlog(k)) | O(n) | 維護大小為 k 的最小堆,堆頂永遠是第 k 大,超過就踢掉最小的 |
| v3 Set | O(nlog(k)) | O(n) | 用 set 自動維護有序,保持大小為 k,超過就刪除最小的 |
| v4 桶排序**(理論最佳)** | O(n) | O(n) | 用頻率當索引建立桶,從最高頻率的桶開始收集元素 |
| v5 PBDS-K | O(nlog(k)) | O(n) | 邊遍歷邊用 modify 更新頻率,維護 k 大小最小堆 |
| v6 PBDS-All | O(nlog(n)) | O(n) | 所有元素入最大堆,邊遍歷邊 modify 更新頻率,最後取前 k 個 |