剑指offer_面试题40,最小的k个数,堆解法和快速选择解法

匿名 (未验证) 提交于 2019-12-02 23:05:13

面试题40,最小的k个数,堆解法和快速选择解法。

照这个思路,可以引入一个结构,使得前k个数总是最大的数在第一个,这样每次遇到一个数值需要和前k个数中排在第一位的那个最大数比较就可以了。

这个结构就是最大堆。

思路一:维护一个maxSize为k的最大堆,用来存放这k个数,遍历数组,如果堆未满,插入堆中。如果堆已满,如果数字比堆的根节点小,则删除堆的根节点,向堆中插入这个数字。

时间复杂度为 O(nlog(k))。如果求最大的k个数,就是使用最小堆了。

思路二:其实我们也可以在n个整数的范围内构建最小堆,然后每次将堆顶的最小值取走,然后调整,再取走堆顶值,取k次,自然就找到了最小的k个数。

这样做的时间复杂度是多少呢?基于n个无序数构建最小堆,时间是O(n),而取走堆顶元素,将堆末元素放到堆顶重新调整的时间复杂度是 O(h),h为堆高度,这里堆高度h总是logn保持不变。

因此时间复杂度是O(n+klogn),这个思路可以再改进:每次重新调整,只需要基于堆顶部k层进行调整,堆末元素被放到堆顶后,最多只需要下调至k层即可,因此调整的时间复杂度成了O(k)。这样做的时间复杂度成了 O(n+kk)。

可以证明O(n+k*k) < O(nlog(k)),但是实际情况下,是否思路二更好呢?

我们别忘了思路二的初始化需要基于整个n个数构建堆,而思路一则不需要。实际情况下,我们往往需要基于大量分布式存储的n个数找出k个数,例如:在分布式存在10台服务器上的总大小大约2T的访问页面记录中找出访问量最高的100个页面。想要跨10台服务器整体构建最大堆,显然不现实,而思路一则只需要维护一个100的最小堆,然后顺序遍历10台服务器上的记录即可。

因此思路一更加具有实际意义。

这里给出思路一的实现。

如果真正写代码,要知道堆是没有STL的,也就是说我们要自己实现。

书中使用了multiset,multiset和set一样,都是基于红黑树实现,区别是set不允许重复而multiset允许重复。

那么multiset和vector区别在哪里?区别在于multiset支持排序。multiset的插入和删除的时间复杂度也都为 O(logk)

代码:

[](javascript:void(0)?

typedef multiset<int, greater<int> >            intSet; typedef multiset<int, greater<int> >::iterator  setIterator;  void GetLeastNumbers_Solution2(const vector<int>& data, intSet& leastNumbers, int k) {     leastNumbers.clear();      if(k < 1 || data.size() < k)         return;      vector<int>::const_iterator iter = data.begin();     for(; iter != data.end(); ++ iter)     {         if((leastNumbers.size()) < k)             leastNumbers.insert(*iter);          else         {             setIterator iterGreatest = leastNumbers.begin();              if(*iter < *(leastNumbers.begin()))             {                 leastNumbers.erase(iterGreatest);                 leastNumbers.insert(*iter);             }         }     } } 

[](javascript:void(0)?

第三种思路,快速选择 法。

如果K< |Sa|( |Sa|表示Sa的大小),则对Sa部分用同样的方法继续操作;

如果K= |Sa|,则Sa是所求的数;

如果K= |Sa| + 1,则Sa和这个pivot一起构成所求解;

如果K> |Sa| + 1,则对Sb部分用同样的方法查找最小的(K- |Sa|-1)个数(其中Sa和pivot已经是解的一部分了)。

当pivot选择的足够好的时候,可以做到时间复杂度是O(n)

那么如何选择一个好的pivot?

这里必须提BFPRT算法,这个算法就是为了寻找数组中第k小的数而设。

BFPRT是一种获得较优秀pivot的方法。其过程有一个flash动画作为演示。其过程是将n个数5个一组划分,求出没一个5元组的中位数,然后再基于这些中位数继续求中位数,这个重复的次数应该是由k的大小来定,随后将选出的中位数作为pivot,将小于pivot的数交换到数组的左侧。接着基于这些小于pivot的值,继续通过“寻找中位数,定pivot,交换法” 来缩小范围,直到最后在一个较小范围内找到k个最小值。

BFPRT算法的时间复杂度做到了O(n)。这个算法的原文链接在此:Time Bounds for Selection

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) { 			vector<int> output; 			if (input.empty() || k == 0||k>input.size()) 				return ; 			int start = 0; 			int end = input.size() - 1; 			int index = partition(input, start, end); 			while (index != k - 1){ 				if (index > k - 1){ 					end = index - 1; 					index = partition(input, start, end);  				} 				else{ 					start = index + 1; 					index = partition(input, start, end); 				} 			}  			for (int i = 0; i < k; ++i) 				output.push_back(input[i]); 			return output; 		}  		int partition(vector<int> &array, int start, int end) { 			// 选择一个pivotIndex 			int pivotIndex = rand() % (end - start + 1) + start; // 随机选一个 			swap(array[pivotIndex], array[end]); // 将pivot换到末尾 			int small = start; 			for (int i = start; i < end; i++) { 				if (array[i] < array[end]) { 					swap(array[small], array[i]); 					small++; 				} 			} 			swap(array[small], array[end]); 			return small; 		} 
文章来源: https://blog.csdn.net/qq29898765/article/details/86567511
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!