1. 程式人生 > >程式設計之美2:尋找最大的K個數

程式設計之美2:尋找最大的K個數

根據樓樓參加筆試或者面試的經驗而言,尋找最大的K個數這個問題,被問到已經不只兩三次了,所以樓樓決定認認真真地把這個問題寫一下,解法思想參照《程式設計之美》一書。

題目簡介

有很多無序的數,我們姑且假定他們各不相等,怎麼選出其中最大的K個數呢?

相關知識點

排序

題目解答

解法一:直接排序

這個解法是第一反應,假設有N個數,我們使用一個N個長度的陣列將其儲存下來,並且使用排序演算法將其從大到小依次排列。排序完成後,輸出前K個數。如果N不小,但是也不大,比如幾千什麼的,可以採用快速排序或者堆排序來完成。
程式碼

#include <iostream>  

using
namespace std; int findMaxN(int *pArray, int len); int comp(const void*a , const void*b) { return *(int *)b - *(int *)a; } int main() { int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28}; int K = 5; int len = sizeof(a) / sizeof(int); //利用快速排序法進行排序 qsort(a, len, sizeof(int
), comp); for (int i = 0; i < K; i++) { cout << a[i] << " "; } system("pause"); }

複雜度分析
堆排序或者快速排序平均的複雜度為O(NlogN)
延伸:qsort()用法

qsort(void*base, size_t num, size_t width, int(__cdecl*compare)(const void*,const void*))

第一個引數:待排序陣列首地址
第二個引數:陣列中待排序元素數量
第三個引數: 各元素的佔用空間大小
第四個引數:指向函式的指標,用於確定排序的順序。
以下為compare函式原型

compare( (void *) & elem1, (void *) & elem2 );
Compare 函式的返回值 描述
小於 0 elem1將被排在elem2前面
等於0 elem1 等於 elem2
大於0 elem1將被排在elem2後面

解法二:部分排序法

簡單分析一下,我們就能發現解法一的一個明顯不足之處,那就是我們將所有的元素都進行了排序,而題目要求只是尋找最大的K個數,也就是說我們只要將最大的K個數排好序就好了,沒必要將剩下的N-K個數也進行排序。

在這裡,我們可以使用快速排序來完成這個部分排序的功能。在快速排序中,每一輪都需要選定一個pivot,每一輪排序完成後,比pivot大的數都排在它前(後)面,而比pivot小的數都排在它的後(前)面。假設前面的序列為Sa,後面的序列為Sb,Sa的長度為n.
n>K時,我們直接輸出Sa的前K個元素就好了;
n=K時,我們直接輸出Sa這個序列;
n<K時,我們就需要從Sb中找出Kn個元素和Sa一起輸出就好了。

程式碼

#include <iostream>  

using namespace std;  

int kBig(int *pArray, int low, int high, int K);
int partion(int *pArray, int low, int high);

int main()  
{  
    int a[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17, 20};
    for (int i = 0; i <= kBig(a, 0, sizeof(a)/sizeof(int), 2); i++)
    {
        cout << a[i] << " ";
    }
    system("pause");
}  

//對前K大的數進行排序,並返回第K大數的下標
int kBig(int *pArray, int low, int high, int K)
{
    int index, n;
    if (low < high)
    {
        //對陣列進行劃分,並返回劃分的位置
        index = partion(pArray, low, high);
        n = index - low + 1;     //Sa的個數
        if (n == K)     //如果恰好是K個的話,那麼返回下標就可以了
        {
            return index;
        }
        if (n < K)     //如果Sa的個數不夠的話,那麼再從Sb中找K-n個
        {
            return kBig(pArray, index + 1, high, K - n);
        }
        if (n > K)     //如果Sa的個數大於K的話,那麼就從Sa裡面返回K個
        {
            return kBig(pArray, low, index, K);
        }
    }
}

//快速排序的劃分函式並返回pivot的座標
int partion(int *pArray, int low, int high)
{
    int i = low; int j = low;
    int pivot = pArray[low];
    for (; i < high, j < high;j++)
    {
        if (pArray[j] > pivot)
        {
            i++;
            swap(pArray[i], pArray[j]);
        }
    }
    swap(pArray[i], pArray[low]);
    return i;
}

複雜度分析
很顯然,相對解法一而言,解法二的複雜度為O(NlogK)

解法三:堆排序法

就樓樓的面試經驗來看,如果這個問題你能答到堆排序演算法的話,這時候面試官就基本滿意了。這是他們想問的點,因為在他們問題裡會不斷地強調這個N是如何之大,記憶體受到限制之類的。比如如果N都是幾百萬的話,那用這麼大的陣列來儲存,這就是非常不明智地做法了。用堆就可以完美解決儲存問題。

#include <iostream>  

using namespace std;  

void buildMinHeap(int *pArray, int K);
void adjustHeap(int *pArray, int rootIndex, int heapSize);

int main()  
{  
    int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28};
    int K = 5 ;

    //建一個K個元素大小的最小堆
    buildMinHeap(a, K);

    //從第K個元素開始掃描,看有沒有比根節點更大的節點,若有則替換,並更新堆;若沒有比根節點大則掃描下一個元素,直到陣列結束
    for (int i = K; i < sizeof(a) / sizeof(int); i++)
    {
        if (a[i] > a[0])
        {
            swap(a[i], a[0]);
            adjustHeap(a, 0, K);
        }
    }

    //打印出前K大的數,沒有排序。
    for (int i = 0; i < K; i++)
    {
        cout << a[i] << " ";
    }
    system("pause");
}  

//建一個K個元素大小的最小堆
void buildMinHeap(int *pArray, int K)
{
    for (int i = (K - 2) / 2; i >= 0; i--)
    {
        adjustHeap (pArray, i, K);
    }
}

void adjustHeap (int *pArray, int rootIndex, int heapSize)
{
    int minIndex = rootIndex;

    //左孩子節點
    int leftIndex = 2 * rootIndex + 1;

    //右孩子節點
    int rightIndex = 2 * (rootIndex + 1);

    //如果左孩子比根節點和右孩子節點小的話,則左孩子和根節點進行交換
    if ((leftIndex < heapSize) && (rightIndex < heapSize) && (pArray[leftIndex] < pArray[rightIndex]) && (pArray[leftIndex] < pArray[rootIndex]))
    {
        minIndex = leftIndex;
    }
    if ((leftIndex < heapSize) && (rightIndex >= heapSize) && (pArray[leftIndex] < pArray[rootIndex]))
    {
        minIndex = leftIndex;
    }


    if ((rightIndex < heapSize) && (pArray[rightIndex] < pArray[leftIndex]) && (pArray[rightIndex] < pArray[rootIndex]))
    {
        minIndex = rightIndex;
    }

    if (minIndex != rootIndex) 
    {
        //如果左孩子或者右孩子比根節點小的話,那麼就交換,並且重新調整以minIndex為根節點的子樹
        swap(pArray[rootIndex], pArray[minIndex]);
        adjustHeap(pArray, minIndex, heapSize);
    }
}

複雜度分析O(NlogK)可以看到堆排序這種做法並沒有怎麼提高時間複雜度,但是卻極大的降低了對空間的儲存要求,只需要維護一個K大小的堆。雖然上面程式同樣是用了一個N大小的陣列來存整個資料,但只是為了演示方便。事實上,很可能所有的資料包都是放在檔案裡面的,只需要掃描比較再更新堆就好了。

解法四:計數排序法

分析一下上面的三種解法,時間複雜度都不是線性的,那我們會問存不存在一種線性的解法?事實上是存在的,但存在的這種解法存在限制。解法思想如下:
如果所有N個數都是正整數,且他們的取值範圍不大,我們知道最大的數是MAXN。那麼我們可以申請一個數組count[MAXN]來記錄每個數出現的次數。然後我們就可以找出最大的K個數。
看程式碼:

#include <iostream>  

using namespace std;  

int findMaxN(int *pArray, int len);

int main()  
{  
    int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28};
    int K = 5;
    int MAXN = findMaxN(a, sizeof(a) / sizeof(int));
    //申請一個count陣列,記錄每一個數出現的次數
    int *count = new int[MAXN + 1]();
    for (int i = 0; i < sizeof(a) / sizeof(int); i++)
    {
        count[a[i]]++;
    }
    int index = MAXN;
    int sumCount = 0;
    for (;index >= 0; index--)
    {
        sumCount += count[index];
        if (sumCount == K)
        {
            break;
        }
    }

    //打印出最大的K個數
    for (int i = MAXN; i >= index; i--)
    {
        if (0 != count[i])
        {
            cout << i << " ";
        }
    }
    system("pause");
}  

//找出一個數組中最大的值
int findMaxN(int *pArray, int len)
{
    int MAXN = pArray[0];
    for (int i = 1; i < len; i++)
    {
        if (pArray[i] > MAXN)
        {
            MAXN = pArray[i];
        }
    }
    return MAXN;
}

複雜度分析O(N)

同學們,有啥建議或者想法請給我留言哦~~~