最小的k个数

来源:互联网 发布:全球网络卫星电视直播 编辑:程序博客网 时间:2024/05/23 13:14

面试题 30,最小的k个数,堆解法和快速选择解法。 (附最大堆的插入/删除操作实现)

这道题最简单的思路是排序,时间复杂度是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+k*logn),这个思路可以再改进:每次重新调整,只需要基于堆顶部k层进行调整,堆末元素被放到堆顶后,最多只需要下调至k层即可,因此调整的时间复杂度成了O(k)。这样做的时间复杂度成了 O(n+k*k)。

可以证明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) 

树上代码:

复制代码
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);            }        }    }}
复制代码

 

引申

还有第三种思路,快速选择 法。这个方法参考了July的文章,以及csdn上huagong_adu所写的另一篇对它的解读。

试想一下,如果存在这么一个数,它在数组的第k位,而且正好前k-1个数比它小,后n-k个数比它大。那么这个时候,我们只需要这个数前面的那k个数即可。

这是不有点像快排中的 pivot?

先选取一个数作为基准比较数(作者称为“枢纽元”,即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。

 

附加,最大堆的插入删除操作:

复制代码
template <typename T>Class MaxHeap(){public:    MaxHeap(int maxSize);    ~MaxHeap();    bool Insert(T element);    bool DeleteMax();private:    T *elements;    int MaxSize = 0;    int size = 0;}//创建堆,记住堆的根节点是element[1]//这样做的目的,是为了可以使用i/2来访问其父结点,可以使用2i表示其左结点。 template <typename T>MaxHeap::MaxHeap(int maxSize){    MaxSize = maxSize;    elements = new T[maxSize+1];    size = 0;}//插入节点,其实就是从末节点开始,往上找,直到找一个地方,让新元素放进去后,比它的父节点小。//找的过程中,找过的节点下移到它的子女所在的平台,为了给新结点腾地方//找到这个地方后,就把新结点安进去 template <typename T>bool MaxHeap::Insert(T ele){    if(size == MaxSize){        return false;    }    size ++;    int i = size;    while(i > 1){        if(elements[i/2] >= ele)            break;    //找到合适的位子了        element[i] = element[i/2];    //没找到,把父节点往子节点上移,腾位置        i = i/2;     }    element[i] = ele;    return true;}//删除节点,就是将根节点删除,但是我们知道,接下来必须调整,因为数组中间不能有空缺。//调整的过程其实就是将数组末尾的那个元素找个地方放。根节点原来的空位不行,就把空位往下挪,//一直挪到这个空位放末尾节点合适了,所谓合适,就是末尾节点放在这里比其子女都大,//(就算中间没有合适的位置,移到叶节点,必然合适) ,把末尾节点放进去。 template <typename T>bool MaxHeap::DeleteMax(){    if(size == 0){        return false;    }    T temp = elements[size];    //末尾节点记下来。     size--;     int i = 1;    while(2*i <= size){    //当i变成叶节点,就会跳出循环        int j = 2 * i;        if(j+1 <= size && elements[j+1] > elements[j])    //别忘右子节点可能不存在的情况             j++;    //如果存在右子节点,而且比左子节点大,我们就和右子节点比 (就是验证是否temp比左右子节点都大罢了,所以和大的比)         if(element[j] <= temp)    //符合条件,左右子节点都比temp,也就是末节点大,那么空位置找到了,退出循环            break;        elements[i] = elements[j];        //不符合条件,继续往下腾位置        i *= 2;    }    elementp[i] = temp;    //就算一直找到最后也没找到,这个时候i已经是某一个叶节点,而且这个叶节点的值已经转移到父节点上,我们把temp付给它就可以         return true;}
复制代码
0 0
原创粉丝点击