算法问题分类---Top-K问题与多路归并排序

本文探讨了寻找数组中前K大数的多种高效算法,包括使用小根堆、快排分区思想、以及在不同场景下的应用如K路合并等,提供了详细的时间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Pro1:寻找前K大数

方法1:K小根堆  后面的值若大于当前根,则替换之,并调整堆

大部分人都推荐的做法是用堆,小根堆。下面具体解释下:

如果K = 1,那么什么都不需要做,直接遍历一遍,时间复杂度O(N)。

下面讨论K 比较大的情况,比如1万。

建立一个小根堆,则根是当前最小的第K个数。然后读入N-K个数,每次读入一个数就与当前的根进行比较,如果大于当前根,则替换之,并调整堆。如果小,则读入下一个。

时间复杂度ON*logK)。

 

方法2:利用快排分区思想:

本题还有一个时间复杂度比较好的做法。在编程之美上提到过该算法。

首先找到最大的第K个数。这个时间复杂度可以做到ON,具体做法如下(利用快排分区思想):

从N个数中随机选择一个数,扫描一遍,n大的放在右边,r个元素,比n小的放左边,l个元素

如果:  a:l = K-1   返回n

           b:l > K-1 在l个元素中继续执行前面的操作。

           c:l < K-1  在r个元素中继续执行前面的操作。

b,c每次只需执行一项,因此平均复杂度大概为O(n+n/2+n/4...)=O(2n)=O(n)

 

Pro2: K路合并求Top-K

20路已经有序,20路合并  求Top500

有 20 个数组,每个数组有 500 个元素,并且是有序排列好的,现在在这 20*500个数中找出排名前 500 的数。

答:

20个数组中各取一个数,并记录每个数的来源数组,建立一个含20个元素的大根堆。此时堆顶就是最大的数,取出堆顶元素,并从堆顶元素的来源数组中取下一个数加入堆,再取最大值,一直这样进行500次即可

 

Pro3: K路合并排序

请给出一个时间为O(nlgk)、用来k个已排序链表合并为一个排序链表的算法,此处n为所有输入链表中元素的总数。

算法思想:

1. 从k个链表中取出每个链表的第一个元素,组成一个大小为k的数组arr,然后将数组arr转换为最小堆,那么arr[0]就为最小元素了;

2. 取出arr[0],将其放到新的链表中,然后将arr[0]元素在原链表中的下一个元素补到arr[0]处,即arr[0].next,如果 arr[0].next为空,即它所在的链表的元素已经取完了,那么将堆的最后一个元素补到arr[0]处,堆的大小自动减1循环即可。

http://www.programlife.net/stl-priority-queue.html

 

Pro4: 整体有序局部无序问题

一个有100亿个元素的整型数组,它的元素是有序的,现在把它分成若干段,每段不超过20个元素,每段的元素个数不等,现在在每段内将这些元素的顺序打乱,然后重新将这100亿个元素的数组排序,请问时间复杂度最小的算法是什么?并给出时间复杂度。

http://bbs.youkuaiyun.com/topics/390252481

http://blog.youkuaiyun.com/burningsheep/article/details/8104493

分析:

如果每段长度相等,则可以考虑采用上面的K路归并,但此处长度不相等,需另行考虑其它方法。

解:(直接插入排序)

假设第1到第5n个数已经有序为sort(5n),那么我们要将5n+1到5n+5这5个数据添加到已排序的数组中,只需要进行插入排序,将这5个数添加进即可。由于分段的长度不超过5,所以第5n+1个数在插入的时候,最多只需要搜索到第5n-4个数就可以了,比较个数不会超过5次。又因为5n+1到5n+5是已经排好序的,所以,后面的数比较次数也不会超过5次(最多比较到前一个插入的位置)。因此,每加入5个数到已排序数组中,时间复杂度是O5*5),

假设长度为N,每段长不超过K。则每段插入的时间复杂度即为OK*K)。

而对于以段为单位插入的操作,需要进行N/K次,所以,总的时间复杂度是O(K*K)*O(N/K)=O(NK)

 

Pro5:100亿个数,求最大的1万个数,并说出算法的时间复杂度

建一个堆,先把最开始的1万个数放进去。以后每进一个,都把最小的赶出来。


上述算法的可选实现工具:自己建立数组进行堆排序、使用优先队列priority_queue、使用集合set multiset
在最大堆中,根结点的值总是大于它的子树中任意结点的值。于是我们每次可以在O(1)得到已有的k个数字中的最大值18,但需要O(logk)时间完成删除以及插入操作。
队列(queue)维护了一组对象,进入队列的对象被放置在尾部,下一个被取出的元素则取自队列的首部。priority_queue特别之处在于,允许用户为队列中存储的元素设置优先级。这种队列不是直接将新元素放置在队列尾部,而是放在比它优先级低的元素前面。优先队列有两种,一种是最大优先队列;一种是最小优先队列;每次取自队列的第一个元素分别是优先级最大和优先级最小的元素。
在STL中set和multiset都是基于红黑树实现的,查找、删除和插入操作都只需要O(logk)。





### 多路归并算法的实现应用 多路归并是一种经典的外部排序方法,适用于内存有限而数据量较大的场景。其核心思想是将大规模的数据划分为若干个小规模子集,在内存中完成这些子集的排序后将其保存至外存;随后利用多路归并技术逐步合并这些已排序的小文件,最终得到全局有序的结果。 #### 一、多路归并的核心原理 当面临海量数据无法一次性加载到内存的情况时,传统的内部排序算法(如快速排序)不再适用[^2]。此时可以通过分治策略先对外部数据进行划分和局部排序,再采用多路归并的方式减少外存访问次数从而提升性能[^3]。具体而言: - **初始阶段**:将原始大文件分割成多个较小的部分,并分别对其进行独立排序。 - **中间阶段**:通过多次迭代执行k-路平衡归并操作,每次都将当前层的所有小文件两两或多两组合并为更大的有序文件。 - **终止条件**:直到最后只剩下一个完整的超大数据块为止,则该块即为我们所求得完全排列好的序列集合。 #### 二、C++代码示例展示如何高效地运用STL组件构建一个多路归并解决方案 ```cpp #include <iostream> #include <fstream> #include <vector> #include <queue> using namespace std; struct FileIterator { ifstream file; int value; explicit FileIterator(const string& filename):file(filename),value(-1){ if(file.is_open()){ file >> value; } } bool has_next(){ return !file.eof(); } void next(){ if(has_next()) { file>>value; }else{ value=-1;//标记结束符 } } }; void merge_files(vector<string>& filenames, const string &output_filename) { priority_queue<pair<int,string>, vector<pair<int,string>>, greater<>> pq; vector<FileIterator> iterators; for(auto&& name :filenames ){ iterators.emplace_back(name); if(iterators.back().has_next()) pq.push({iterators.back().value,name}); } ofstream output(output_filename); while(!pq.empty()){ auto top=pq.top();pq.pop(); output<<top.first<<"\n"; for(auto &it:iterators){ if(it.file.peek()==top.second.c_str()[0]){ it.next(); if(it.has_next()){ pq.push({it.value,top.second}); } break; } } } } int main() { // Assume we have already created sorted temporary files named as 'temp_*.txt' vector<string> input_filenames = {"temp_1.txt", "temp_2.txt", ..., "temp_n.txt"}; string final_sorted_file = "final_output.txt"; merge_files(input_filenames, final_sorted_file); } ``` 上述程序片段展示了基于标准模板库(STL)函数`priority_queue`, `ifstream`, 和 `ofstream` 的实际编码过程[^4]. 这里我们定义了一个辅助结构体用于封装单个输入流的行为模式及其对应数值表现形式. #### 三、关于时间复杂度分析及优化建议 对于大小约为 \(10^7\) 字节级别的数据集来说,即使采用了高效的缓存机制仍然不可避免存在大量的磁盘I/O开销[^5] 。因此为了进一步降低总耗时可以从以下几个方面入手考虑改进措施: - 提高每轮参比较的候选者数量\(K\), 即增大归并路径宽度. - 减少不必要的重复读取动作频率. - 利用预读技术和写回缓冲区管理技巧来隐藏部分延迟效应.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值