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

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

img

​ 这道题最简单的思路是排序,时间复杂度是O(nlog(n))。但是这样做在那n-k 个数的排序上浪费了资源。

​ 改进一下,将数组的前k个数作为最小的k数的缓存。从第k+1个数开始遍历,如果有比前k个数小的,就将其和前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)?

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

​ 先选取一个数作为基准比较数(作者称为“枢纽元”,即pivot),用快排方法把数据分为两部分Sa和Sb。

如果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;
		}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值