求數組中第k個最小數

一、問題描述

給定一個數組,數組中的數據無序,在一個數組中找出其第k個最小的數,例如對於數組x,x = {3,2,1,4,5,6},則其第2個最小的數爲2。


二、解題思路

本算法跟快排的思想相似,首先在數組中選取一個數centre作爲樞紐,將比centre小的數,放到centre的前面將比centre大的數,放到centre的後面。如果此時centre的位置剛好爲k,則centre爲第k個最小的數;如果此時centre的位置比k前,則第k個最小數一定在centre後面,遞歸地在其右邊尋找;如果此時centre的位置比k後,則第k個最小數一定在centre後面,遞歸地在其左邊尋找。


注意:centre的位置=其下標值+1,因爲數組中的第一個元素的下標爲0。


從上面的描述中,我們可以看到這個算法運用了減治的方法求解。減治的思想與分治非常相似,同樣是在一次操作中,削減問題的規模,只是分治把每個子問題求解後,要合併每個子問題的解才能得到問題,而減治的方法,卻不用合併子問題的解,子問題的解,直接就是原問題的解。舉個例子來說,就像快排和二分查找算法,前者是分治,後者是減治。因爲快排要等到所有的子數組都排完序,原數組纔有序,而二分查找卻不用,它每執行一次查找,直接丟棄一半的數組,而不用合併子問題的解。不過也有不少書,把他們都歸爲分治法。


三、代碼實現

考慮到代碼的通用性,使用了模板函數,如果看不懂模板函數,則只需要忽略template<typename T>,並把T看作是一個類型即可。代碼如下:

//返回數組中的第k個最小元素的啓動函數,注意會破壞原數組
template<typename T>
T FindTheKMin(T *x, int x_size, int k);
//實現查找數組中第K個最小元的功能函數
template<typename T>
T TheKMin(T *x, int left, int right, int k);
template<typename T>
T FindTheKMin(T *x, int x_size, int k)
{
    //判斷k的值是否過大,即超過數組的大小
    //若是則返回第0個元素,主要是爲了防止無效的遞歸
    if(x_size < k)
        return x[0];
    return TheKMin(x, 0, x_size-1, k);
}
template<typename T>
T TheKMin(T *x, int left, int right, int k)
{
    //取數組最後一個元素爲樞紐
    T centre = x[right];
    int i = left;
    int j = right - 1;
    while(true)
    {
        //從前向後掃描,找到第一個小於樞紐的值,
        //在到達數組末尾前,必定結束循環,因爲最後一個值爲centre
        while(x[i] < centre)
            ++i;
        //從後向前掃描,此時要檢查下標,防止數組越界
        while(j >= left && x[j] > centre)
            --j;
        //如果沒有完成一趟交換,則交換
        if(i < j)
            Swap(x[i], x[j]);
        else
            break;
    }
    //把樞紐放在正確的位置
    Swap(x[i], x[right]);
    //如果此時centre的位置剛好爲k,則centre爲第k個最小的數
    if(i+1 == k)
        return x[i];
    else if(i+1 < k)
    {
        //如果此時centre的位置比k前,遞歸地在其右邊尋找
        TheKMin(x, i+1, right, k);
    }
    else
    {
        //如果此時centre的位置比k後,遞歸地在其左邊尋找
        TheKMin(x, left, i-1, k);
    }
}

代碼說明:

在上面的代碼中,我們要注意,TheKMin函數的最後的if-else,這個算法不同於快排,當樞紐不是要找到元素時,它只會選擇其中一個方向的子數組繼續尋找,而不像快排那樣,會在兩個方向的子數組中繼續。從上面的代碼來看,其運行速度應該在使用相同選取樞紐的策略的快排之上,時間複雜度爲O(N)。


同時,當K值不合理時,我們只能返回第0個元素,這點有一點的不合理,但是,我不知道該返回一個什麼樣的合適的值,因爲它是泛型的。


其實,這段代碼有兩個缺陷,第一個,就是在查找時,破壞了數組原來的數據(交換了位置);第二個是,當類型T的複製和構造開銷較大時,直接多次交換兩個元素,可能會帶來相當大。


另一種實現

下面,再來看看另一種實現,算法的思想和策略相同,但是使用了一個跟蹤數組track,用來跟蹤使用第一種方法下的數據的交換情況,利用跟蹤數組的元素交換代替原數組中元素的交換,解決了上面提到的兩個問題。它的實現如下:

//返回數組中的第中個最小元素的下標的啓動函數,不破壞原數組
template<typename T>
int IndexOfKMin(const T *x, int x_size, int k);
//實現查找數組中第K個最小元下標的功能函數
template<typename T>
int TheKMin(const T *x, int *track, int left, int right, int k);
template<typename T>
int IndexOfKMin(const T *x, int x_size, int k)
{
    //判斷k的值是否過大,即超過數組的大小
    //若是則返回下標-1,主要是爲了防止無效的遞歸
    if(x_size < k)
        return -1;
    //創建一個跟蹤數組,其內容爲原數組中元素的下標,
    //用於記錄元素的交換(即代替元素的交換)
    //按順序以track數組中的數據爲下標訪問元素,訪問順序與上一方法相同
    int *track = new int[x_size];
    for(int i = 0; i < x_size; ++i) //初始化跟蹤數組,其值與下標值相對應
        track[i] = i;
    int i = TheKMin(x, track, 0, x_size-1, k);
    delete []track;
    return i;
}
template<typename T>
int TheKMin(const T *x, int *track, int left, int right, int k)
{
    //取數組最後一個元素爲樞紐
    T centre = x[track[right]];
    int i = left;
    int j = right - 1;
    while(true)
    {
        //從前向後掃描,找到第一個小於樞紐的值,
        //在到達數組末尾前,必定結束循環,因爲最後一個值爲centre
        //注意此時的數據的下標不是i,而是track[i]
        while(x[track[i]] < centre)
            ++i;
        //從後向前掃描時要檢查下標,防止數組越界
        while(j >= left && x[track[j]] > centre)
            --j;
        //如果沒有完成一趟交換,則交換,注意,是交換跟蹤數組的值
        if(i < j)
            Swap(track[i], track[j]);
        else
            break;
    }
    //把樞紐放在正確的位置
    Swap(track[i], track[right]);
    //如果此時centre的位置剛好爲k,則centre爲第k個最小的數,
    //返回其在真實數組中的下標,即track[i]
    if(i+1 == k)
        return track[i];
    else if(i+1 < k)
    {
        //如果此時centre的位置比k前,遞歸地在其右邊尋找
        TheKMin(x, track, i+1, right, k);
    }
    else
    {
        //如果此時centre的位置比k後,遞歸地在其左邊尋找
        TheKMin(x, track, left, i-1, k);
    }
}

代碼說明:

從上面的代碼,我們可以看出,這個函數是返回數組中的第k個最小元的下標,所以當k不合理時,就可以返回-1來表示這個錯誤,同時,它使用了一個跟蹤數組,track數組中的內容,實質是原數組中數據的一個索引,利用跟蹤數組的元素的交換來代替了原數組元素的交換,因爲該跟蹤數組的數據類型是int,所以其交換速度相當快,從而解決了上面提到的兩個問題。


從上面的代碼,我們也可以看到,其時間複雜度與前面的實現是一樣的,也爲O(N),但是,這個實現方法卻帶來了一定的空間開銷,它開闢了一個與原數組元素個數相等的一維數組,用於跟蹤原數組中的元素的交換情況。

至於在實際中,要使用哪一種算法,取決於使用者的需要!


發佈了126 篇原創文章 · 獲贊 456 · 訪問量 268萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章