TOP-K

什么是TOP-K问题: 

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。

解决方法:

1.堆解决:
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决。

以下是一个使用std::priority_queue实现的示例代码,这个例子中我们使用最小堆来找到数组中最大的K个元素:
 

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

void findTopK(const std::vector<int>& nums, int k) {
    std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;

    for (int num : nums) {
        if (minHeap.size() < k) {
            minHeap.push(num);
        } else if (num > minHeap.top()) {
            minHeap.pop();
            minHeap.push(num);
        }
    }

    std::vector<int> topKElements;
    while (!minHeap.empty()) {
        topKElements.push_back(minHeap.top());
        minHeap.pop();
    }

    // 打印结果
    for (int num : topKElements) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> nums = {3, 2, 1, 5, 6, 4};
    int k = 2;
    findTopK(nums, k);
    return 0;
}

这段代码首先定义了一个函数findTopK,它接受一个整数数组nums和一个整数k作为参数。函数内部使用了一个最小堆minHeap来存储当前找到的K个最大元素。遍历数组中的每个元素,如果堆的大小小于K,就将元素推入堆中;如果堆的大小等于K,并且当前元素大于堆顶元素,则弹出堆顶元素,并将当前元素推入堆中。这样,堆中始终维护着K个最大的元素。

最后,代码将堆中的元素转移到一个向量topKElements中,并逆序打印这些元素,因为最小堆的顶部是最小的元素,而我们需要最大的元素。

请注意,这个实现假定数组中的元素是唯一的,如果数组中有重复的元素,需要根据具体需求调整代码逻辑。此外,这个实现的时间复杂度是O(n log K),其中n是数组中元素的数量。


当数据中有重复值时,使用堆来解决TOP-K问题需要稍作修改,以确保能够正确处理重复的元素。我们可以使用一个额外的数据结构(例如哈希表)来记录每个元素出现的次数,这样即使元素值相同,我们也能知道它们是否是不同的实例。

以下是一个C++示例,演示如何在考虑重复元素的情况下,使用最小堆来找到数组中最大的K个元素:

#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <functional>

// 自定义比较函数,用于优先队列
struct Compare {
    bool operator()(const std::pair<int, int>& a, const std::pair<int, int>& b) {
        // 优先级队列默认是大顶堆,我们使用负值来实现小顶堆
        return a.first < b.first || (a.first == b.first && a.second < b.second);
    }
};

void findTopKWithDuplicates(const std::vector<int>& nums, int k) {
    std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, Compare> minHeap;
    std::map<int, int> count; // 记录每个元素出现的次数

    // 统计每个元素出现的次数
    for (int num : nums) {
        count[num]++;
    }

    // 构建最小堆
    for (auto& kv : count) {
        if (minHeap.size() < k) {
            minHeap.push({kv.first, kv.second});
        } else if (kv.first > minHeap.top().first) {
            minHeap.pop();
            minHeap.push({kv.first, kv.second});
        } else if (kv.first == minHeap.top().first && kv.second > minHeap.top().second) {
            minHeap.pop();
            minHeap.push({kv.first, kv.second});
        }
    }

    // 打印结果
    std::vector<int> topKElements;
    while (!minHeap.empty()) {
        topKElements.push_back(minHeap.top().first);
        minHeap.pop();
    }
    std::reverse(topKElements.begin(), topKElements.end());

    for (int num : topKElements) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> nums = {3, 1, 1, 5, 9, 2, 2, 5};
    int k = 4;
    findTopKWithDuplicates(nums, k);
    return 0;
}

在这个示例中,使用了一个std::pair<int, int>作为堆中的元素,其中第一个int是元素的值,第二个int是该元素出现的次数。然后定义了一个自定义比较函数Compare来确保堆的元素按照值和次数排序。这样,即使有重复的值,我们也能根据次数来区分它们。

注意,这种方法的时间复杂度仍然是O(n log k),但是它能够正确处理重复元素的情况。 


2.其他方法: 

除了使用最小堆的方法外,还有其他几种方法可以在C++中实现TOP-K问题的解决方案:

  • 遍历排序方法:遍历数组,进行比较查找,非常耗时。

k 轮遍历,分别在每轮中提取第 1、2、…、k 大的元素,时间复杂度为 O(nk) 。

此方法只适用于 k≪n 的情况,因为当 k 与 n 比较接近时,其时间复杂度趋向于 O(n^{2}) ,非常耗时。

看看就好,最好别用!!! 


  • 排序方法: 对整个数组进行排序,然后选择最大的K个元素。这种方法简单,但时间复杂度较高。

我们可以先对数组 nums 进行排序,再返回最右边的 k 个元素,时间复杂度为 O(n*log⁡n) 。

显然,该方法“超额”完成任务了,因为我们只需找出最大的 k 个元素即可,而不需要排序其他元素。

#include <algorithm>
#include <vector>
#include <iostream>

void topKSort(std::vector<int>& nums, int k) {
    std::sort(nums.begin(), nums.end(), std::greater<int>());
    nums.resize(k); // 只保留最大的K个元素
}

// 使用示例
int main() {
    std::vector<int> nums = {3, 2, 1, 5, 6, 4};
    int k = 2;
    topKSort(nums, k);
    for (int num : nums) {
        std::cout << num << " ";
    }
    return 0;
}

  • 快速选择方法: 快速选择是快速排序的变种,可以用于找到第K大的元素,然后根据这个元素将数组分为两部分。
#include <vector>
#include <iostream>
#include <cstdlib>

int partition(std::vector<int>& nums, int left, int right) {
    int pivot = nums[right];
    int i = left;
    for (int j = left; j < right; j++) {
        if (nums[j] > pivot) {
            std::swap(nums[i], nums[j]);
            i++;
        }
    }
    std::swap(nums[i], nums[right]);
    return i;
}

void quickSelect(std::vector<int>& nums, int left, int right, int k) {
    if (left < right) {
        int pivotIndex = partition(nums, left, right);
        if (k == pivotIndex) {
            return;
        } else if (k < pivotIndex) {
            quickSelect(nums, left, pivotIndex - 1, k);
        } else {
            quickSelect(nums, pivotIndex + 1, right, k);
        }
    }
}

void topKQuickSelect(std::vector<int>& nums, int k) {
    quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}

// 使用示例
int main() {
    std::vector<int> nums = {3, 2, 1, 5, 6, 4};
    int k = 2;
    topKQuickSelect(nums, k);
    for (int i = nums.size() - k; i < nums.size(); i++) {
        std::cout << nums[i] << " ";
    }
    return 0;
}

  • 线性时间算法: 使用BFPRT算法(中位数的中位数算法)来找到第K大的元素,但实现较为复杂,通常不推荐在实际编程中使用。

  • 计数排序: 如果元素的范围不大,可以使用计数排序来快速找到TOP-K。
#include <vector>
#include <iostream>

void topKCountingSort(std::vector<int>& nums, int k) {
    int maxVal = *std::max_element(nums.begin(), nums.end());
    std::vector<int> count(maxVal + 1, 0);
    for (int num : nums) {
        count[num]++;
    }

    std::vector<int> topK;
    for (int i = maxVal; i >= 0 && k > 0; i--) {
        if (count[i] > 0) {
            topK.insert(topK.end(), count[i], i);
            k -= count[i];
        }
    }

    nums = topK;
}

// 使用示例
int main() {
    std::vector<int> nums = {3, 2, 1, 5, 6, 4};
    int k = 2;
    topKCountingSort(nums, k);
    for (int num : nums) {
        std::cout << num << " ";
    }
    return 0;
}


  • 近似算法: 对于大数据集,可以使用近似算法如LSH(局部敏感哈希),但这通常涉及到更复杂的数据结构和算法。

每种方法都有其适用场景和优缺点。在实际应用中,需要根据数据的特性和性能要求来选择最合适的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值