1. 程式人生 > >查詢第K小元素(C語言版)

查詢第K小元素(C語言版)

       今天在看《演算法:C語言實現》時,在快速排序那一章最後一節講述了利用快速排序的思想,快速排序每次劃分後在樞軸的左邊的元素都比樞軸小(或相等),在樞軸右邊的數都比樞軸大(或相等),而劃分後樞軸本身就放在了(有序時)它自身應該在的位置,在每次劃分後判斷樞軸下標和k的大小就可以快速找出數列中第k小的數了。

       看完之後,我想既然利用快速排序的思想可以很快的找到第k小的數,那麼能不能利用計數排序的思想來查詢第k小的數呢,仔細一想,完全可以!計數排序是利用一個計數陣列C來記錄待排序陣列中各個不同數值出現的次數,然後通過一次遍歷計數陣列C,利用C[i] += C[i-1]就可以得到小於等於數值i的元素的個數,然後按照C[i]就可以把待排序陣列中值為i的數放到它應該在的位置上。那怎麼利用這個計數陣列C呢?在對計數陣列進行遍歷後(執行C[i] += C[i-1]),C[i]代表待排序陣列中值小於等於i的元素個數,而C[i-1]代表值小於等於i-1的元素個數,如果 C[i-1] < 

k並且k <= C[i],那麼待排序陣列中第k小的數必定就是值為i的數!查詢就可以結束了。所以只需要找到第一個滿足條件k <= C[i]的i即可。

       在說具體的演算法之前先約定第k小的數為在數列有序時從0開始的第k-1個數,也就是k>0。

       既然談到關於查詢第k小元素的問題,那麼就乾脆把我最近學習到的和自己想到的方法寫下來和大家一起交流。

下面先從最簡單的演算法講起:

一、先排序整個數列然後取第k-1個數

這個方法簡單粗暴,先把數列按從小到大的方式排列,就可以輕鬆得到第k-1個數了,但是,這個方法讓整個數列都有序了,做了太多的無用功。

二、利用選擇排序

第一個方法簡單直接,但是由於我們只需要得到的是整個數列的第k小的數,所以,我們可以用選擇排序的思想:選擇排序每一次遍歷數列都從剩餘N-i個數中選取一個最小的數和前面的第i個值進行交換,直到i等於N-1時整個數列就是有序的了。對於選取第k小的數的問題,我們只需要進行k趟選擇排序就可以得到第k小的數了。

程式碼如下:

//利用選擇排序的思想,找k趟就能找到第k小的元素
void selectSortSearch(int array[], int size, int k)
{
    int minIndex;
    int i = 0;
    int j = 0;
    for (i = 0; i < k; ++i) {
        minIndex = i;
        for (j = i + 1; j < size; ++j) {
            if (array[j] < array[minIndex]) {
                minIndex = j;
            }
        }
        swap(&array[i], &array[minIndex]);
    }

    printf("find: %d\n", array[k - 1]);
}

void swap(int *value1, int *value2)
{
    int temp = *value1;
    *value1 = *value2;
    *value2 = temp;
}

這種方法使用於k值比較小的情況,在k值較小的時這種方法的效率和選擇演算法以及基於計數排序思想的查詢的效率差不多,但是一旦k的值比較大時這種方的效率就降低了。

三、選擇演算法

演算法描述:

利用快速排序的的劃分方法重排陣列a[l],....,a[r],返回一個整數i,滿足a[l],.....,a[i-1]都小於a[i],a[i+1],....,a[r]都大於或等於a[i],如果k等於i,那麼我們的工作就完成了。否則,如果k小於i,那麼繼續對左邊的子序列進行處理;如果i大於k,那麼繼續對右邊的子序列進行處理。

程式碼如下:

//利用遞迴的快速排序的思想進行查詢第k小的元素
void recQuickSearch(int array[], int low, int high, int k)
{
    int index;
    if (high < low) {//注意這裡不能用<=
        return ;
    }

    index = findPivotIndex(array, low, high);
    if (index == k - 1) {
        printf("find: %d\n", array[index]);
        return ;
    }
    if (index > k - 1) {
        recQuickSearch(array, low, index - 1, k);
    }
    if (index < k - 1) {
        recQuickSearch(array, index + 1, high, k);
    }
}

int findPivotIndex(int array[], int low, int high)
{
    int pivot = array[low];

    while (low < high) {
        while (low < high && array[high] >= pivot) {
            --high;
        }
        array[low] = array[high];

        while (low < high && array[low] <= pivot) {
            ++low;
        }
        array[high] = array[low];
    }
    array[low] = pivot;

    return low;
}
以上是遞迴形式的選擇演算法,下面是非遞迴形式的選擇演算法:
//利用非遞迴的快速排序的思想查詢第k小的元素
void quickSearch(int array[], int low, int high, int k)
{
    int index = 0;
    while (high >= low) {
        index = findPivotIndex(array, low, high);
        if (index == k - 1) {
            printf("find: %d\n", array[index]);
            break ;
        }
        if (index > k - 1) {
            high = index - 1;
        }
        if (index < k - 1) {
            low = index + 1;
        }
    }
}

四、利用計數排序的思想

演算法描述:

在對計數陣列進行遍歷後(執行C[i] += C[i-1]),C[i]代表待排序陣列中值小於等於i的元素個數,而C[i-1]代表值小於等於i-1的元素個數,如果 C[i-1] < k並且k <= C[i],那麼待排序陣列中第k小的數必定就是值為i的數!查詢就可以結束了。所以只需要找到第一個滿足條件k <= C[i]的i即可。

程式碼如下:

//利用計數排序的思想查詢第k小的元素
void countSearch(int array[], int size, int k)
{
    assert(array != NULL && size > 0);

    //計數陣列,用於統計陣列array中各個不同數出現的次數
    //由於陣列array中的數屬於0...RANDMAX-1之間,所以countArray的大小要夠容納RANDMAX個int型的值
    int *countArray = (int *) calloc(RANDMAX, sizeof(int));

    //統計陣列array中各個不同數出現的次數,迴圈結束後countArray[i]表示數值i在array中出現的次數
    int index = 0;
    for (index = 0; index < size; ++index) {
        countArray[array[index]]++;
    }

    //有可能countArray[0]就已經比k大了
    if (countArray[0] >= k) {
        printf("find: 0\n");
    } else {
        //統計數值比index小的數的個數,迴圈結束之後countArray[i]表示array中小於等於數值i的元素個數
        for (index = 1; index < RANDMAX; ++index) {
            countArray[index] += countArray[index - 1];
            //當第一次滿足條件時就代表第k小的數在小於等於index的元素並且大於index-1的之間
            if (countArray[index] >= k) {
                printf("find: %d\n", index);
                break ;
            }
        }
    }

    free(countArray);
}

利用計數排序思想查詢第k小的數雖然執行速度很快,但是它也有一個和計數排序一樣的缺點,就是空間複雜度太高了,不適合用於在元素值的範圍很大的數列中尋找第k小的數。還有,不適合於數列中含有負數的情況。

五、利用堆排序思想(一)

演算法描述:

對待查詢陣列進行堆排序,只需進行k趟即可找出第k小的元素了。

//利用小頂堆,維護一個初始大小為size的堆,k次堆排就可得到第k小的元素
void heapSearch1(int array[], int size, int k)
{
    assert(array != NULL && size > 0 && k < size);
    int *heapPointer = array - 1;

    //自底向上調整陣列,使其成為一個堆
    int index = 0;
    for (index = size / 2; index > 0; --index) {
        fixDown1(heapPointer, index, size);
    }

    //交換堆頂元素和最後一個元素並調整堆結構
    //執行k次就可以得到第k小的元素了
    for (index = 0; index < k; ++index) {
        swap(&heapPointer[1], &heapPointer[size--]);
        fixDown1(heapPointer, 1, size);
    }
    printf("find: %d\n", heapPointer[size + 1]);
}

//從下標為index的節點開始向下調整,使樹變成堆有序的(小頂堆)
void fixDown1(int array[], int index, int size)
{
    int i = index;
    int j = 2 * index;
    while (2 * i <= size) {//當下標為i的節點有孩子時
        j = 2 * i;//讓j指向左孩子
        //當i有右孩子並且右孩子比左孩子小時,讓j指向右孩子
        if (j + 1 <= size && array[j + 1] < array[j]) {
            ++j;
        }
        //如果待調整元素的值小於較大孩子時,調整結束退出迴圈
        if (array[i] <= array[j]) {
            break;
        }
        //否則交換待調整元素和其較大子節點
        swap(&array[i], &array[j]);
        i = j;//讓i指向調整後的待調整節點
    }
}

構造一個大小為N的堆所用的比較次數少於2N次,移去k個最小元素所用的比較次數少於2k*lgN次,總共需要2N + 2k*lgN次比較。

六、利用堆排序思想(二)

上一種思想是利用堆排序進行k趟排序就可以得到第k小的數了,但是這中方法需要維護大小為N的陣列,而我們需要的只是第k小的元素,那麼,我們可以使用一個大小為k的大頂堆來維護最小的k個數,然後用待查詢陣列中剩下的元素array[j]和堆頂元素進行比較,如果比堆頂元素小則把堆頂元素值置為array[j],然後進行堆調整,遍歷整個陣列之後堆頂元素就是第k小的元素了。

程式碼如下:

//維護一個大小為k的大頂堆,當待查詢陣列中的元素array[i]比堆頂元素小的時候,把堆頂元素替換為array[i],
//然後調整堆結構,使其保持大頂堆的性質,這樣遍歷完整個待查詢陣列後堆頂元素就是第k小的元素
//堆排序,利用大頂堆,從小到大排序
void heapSearch2(int array[], int size, int k)
{
    int heapSize = k;
    int *heap = (int *) calloc(heapSize + 1, sizeof(int));

    int i = 0;
    for (i = 0; i < heapSize; ++i) {
        heap[i + 1] = array[i];
    }

    //自底向上調整陣列heap,使其成為一個大頂堆
    for (i = heapSize / 2; i > 0; --i) {
        fixDown2(heap, i, heapSize);
    }

    int j = 0;
    for (j = k; j < size; ++j) {
        if (heap[1] > array[j]) {
            heap[1] = array[j];
            fixDown2(heap, 1, heapSize);
        }
    }
    printf("find: %d\n", heap[1]);
}

//從下標為index的節點開始向下調整,使樹變成堆有序的
void fixDown2(int array[], int index, int size)
{
    int i = index;
    int j = 2 * index;
    while (2 * i <= size) {//當下標為i的節點有孩子時
        j = 2 * i;//讓j指向左孩子
        //當i有右孩子並且右孩子比左孩子大時,讓j指向右孩子
        if (j + 1 <= size && array[j] < array[j + 1]) {
            ++j;
        }
        //如果待調整元素的值大於較大孩子時,調整結束退出迴圈
        if (array[i] > array[j]) {
            break;
        }
        //否則交換待調整元素和其較大子節點
        swap(&array[i], &array[j]);
        i = j;//讓i指向調整後的待調整節點
    }
}
構造大小為k的堆需要2k次比較,然後用待查詢陣列剩下的N-k個元素和堆頂元素進行比較,比較次數為N-k,若小於堆頂元素,就置堆頂元素值為該值,接著對堆進行調整以維持大頂堆的性質,每次至多需要2lgk次比較,也就是2(N-k)*lgk次比較,所以總的比較次數為2k + (N - k) + 2(N - k)*lgk = N + k + 2(N - k)*lgk次比較。這種方法使用的空間和k成正比,所以在k較小且N很大時,對於找出N個元素中的第k小的元素有很高的時間效率,對於隨即關鍵字以及其他情況,這種方法中堆操作的上界lgk在k相對N較小是可能為O(1)!