面試題 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;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM