數組里最小的k個元素&&快排

這個題目之前在看數據結構和算法分析的時候見過,其實解法也挺多的。

  • 排序后找出前k個元素
    快排O(NlogN),然后O(K),總的復雜度為O(NlogN)

  • 創建一個長度為k的容器,然后不斷地維護更新,這樣的話,每次都要:
    1,將當前的k個元素中的最大者與當前遍歷的元素比較
    2,若當前的元素更小,則需要替換這個值
    3,若有上一步的話,將新的數組排序
    據說用二叉樹的話復雜度為O(logK)。。
    然后遍歷n-k的元素
    總的復雜度為O(Nlog(k))

  • 和快速排序一樣,我們改造一下得到快速選擇quickSelect
    首先大家如果不太熟悉快排,可以去看一下。
    這里我們要打印的元素只是前k個,因此我們把快排的算法改進一下

快排:

void quick_sort(int* a, int left, int right)
{
    int i, j;
    int pivot;

    if (left + 3<= right)
    {
        //pivot = Median3(a, left, right);
        pivot = get_pivot(a, left, right);//error1
        i = left;
        j = right - 1;
        for (;;)
        {
            while (a[++i] < pivot){}
            while (a[--j] > pivot){}
            if (i < j)
                swap(&a[i], &a[j]);
            else
                break;
        }
        swap(&a[i], &a[right - 1]);

        quick_sort(a, left, i - 1);
        quick_sort(a, i + 1, right);
    }
    else
        InsertionSort(a + left, right - left + 1);
}

特別要提到的是if (left + 3<= right)這里加上了一個cutoff量
我試了一下,如果這個值是0,1,2的話,會出錯
大家可以宏定義一個cutoff,然后自己設置值,這一點呢,我猜是為了防止越界的時候出現問題,交給插入排序來做

其中pivot的程序:

//不僅是找到中位數,而且為了方便要把pivot移動到最右邊(hide),這樣就不用去處理這個元素了
//技巧:找中位數不要局限在比較大小,這樣很麻煩的,可以通過swap快速達到效果
int get_pivot(int*arr, int beg, int end)
{
    
    int middle = (beg + end) / 2;                   //change2
    if (arr[middle] < arr[beg])
        swap(&arr[middle], &arr[beg]);
    if (arr[end] < arr[beg])
        swap(&arr[beg], &arr[end]);
    if (arr[middle]>arr[end])
        swap(&arr[middle], &arr[end]);
    //hide pivot
    swap(&arr[middle], &arr[end - 1]);//we don't need to deal with arr[end],but it's ok if you do

    return arr[end - 1];
}

swap:
void swap(int* a, int *b) { int tmp = *a; *a = *b; *b = tmp; }
快排里面,
quick_select(arr,beg,i-1); quick_select(arr,i+1,end);

這一句可以改進而使quickselect更快速,快排的復雜度為O(N*logN),而下面的算法則可以達到O(N)!

.

因為我們只需要保證前k個元素是最小的;而我們每次調用,都保證了pivot前面的元素不大于pivot && pivot后面的元素不小于pivot

我們有一個下標 i 來記錄pivot的位置,所以我們可以判斷k與i的關系來決定對那一部分繼續進行遞歸:

1,如果k小于i,那么只需要對前面的i個進行遞歸調用
2,如果k等于i+1,說明剛好有k個元素,直接打印
3,如果k大于i,那么前i個元素就不需要管理,處理后面的一部分

如下:

        //quickSelect:
        //attention that Kth value in array is arr[k-1]
        if (k < i)
            quick_select(arr, beg, i - 1, k);
        else
            if (k>i + 1)
                quick_select(arr, i + 1, end, k);
        //如果k比中間要長,那么 i 之前的都不需要管了,只需要看后面的部分

        //所以我們看見,quickSelect只需要對一邊進行遞歸,所以,復雜度會低于quicksort

以上是quickSort,接下來是select:
函數的本身并沒有什么變化,只是改動了上面提到的兩句遞歸部分:

/*if (k - 1 == i)          //it seems that it dosen't matter
        return;
else*/ 
  if (k < i)
    quick_select(arr, beg, i - 1, k);
else
  if (k>i + 1)
    quick_select(arr, i + 1, end, k);

*不過我試了一下,第一種情況k - 1 == i可以注釋掉。。

void quick_select(int* arr, int beg, int end, int k)
{
    int pivot;
    int i, j;

    if (beg +2 <= end)//<= matters
    {
        pivot = get_pivot(arr, beg, end);
        i = beg;
        j = end - 1;//it is okay to be j=end
        while (1)
        {
            while (arr[++i] < pivot);//it doesn't matter wether it is "++i" or "i++"
            while (arr[--j] > pivot);
            if (i < j)
                swap(&arr[i], &arr[j]);
            else break;
        }
        swap(&arr[i], &arr[end - 1]);//since you hide, restore here

        //follow the quickSort :
        //quick_select(arr, beg, i - 1, k);
        //quick_select(arr, i + 1, end, k);

        //quickSelect:
        //attention that Kth value in array is arr[k-1]
        if (k - 1 == i)
            return;
            else if (k < i)
            quick_select(arr, beg, i - 1, k);
            else
            if (k>i + 1)
            quick_select(arr, i + 1, end, k);
        //如果k比中間要長,那么 i 之前的都不需要管了,只需要看后面的部分

        //所以我們看見,quickSelect只需要對一邊進行遞歸,所以,復雜度會低于quicksort
    }
    else
        InsertionSort(arr + beg, end - beg + 1);
}

我用例子試了一下是沒有問題的:

    int a[] = { 5, 3, 8, 99, 10, 2, 4, 7, 1, 22 };
    quick_select(a, 0, 9, 6);
    //Qsort(a, 0, 9);
    //quick_sort(a, 0, 9);
    for (int i = 0; i < 10; i++)
        std::cout << a[i] << " ";

補充

我在做這道題的時候在網上搜索過,還有一種題目是讓你輸出第k個元素。。在以上程序中稍稍改進,加入返回的值然后輸出
像這樣就可以了:

if (k - 1 == i)
        return arr[i];
        else if (k < i)
            return quick_select_k(arr, beg, i - 1, k);
        else
            if (k>i + 1)
                return quick_select_k(arr, i + 1, end, k);

  • 記錄一下我遇到的坑:
    在寫get_pivot()的時候,我最開始是這樣寫的:

    //int middle = arr[(beg + end) / 2];                    //change2
    //if (middle < arr[beg])
    //  swap(&middle, &arr[beg]);
    //if (arr[end] < arr[beg])
    //  swap(&arr[beg], &arr[end]);
    //if (middle>arr[end])
    //  swap(&middle, &arr[end]);
    //swap(&middle, &arr[end - 1]);
    //beg<middle<end

第一行就出錯了??!
int middle = arr[(beg + end) / 2];
我tm調試了一兩個小時才找到這個問題啊!一直以為我算法錯了?。?br> 這里在數組里面用除法,似乎是有問題的!


update:
上stackoverflow問了一下發現果然很sb??!
我那種錯誤的寫法根本沒有達到交換的效果,只是把middle這個值賦給了數組里面的一個元素!

學習了!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,776評論 0 33
  • 總結一下常見的排序算法。 排序分內排序和外排序。內排序:指在排序期間數據對象全部存放在內存的排序。外排序:指在排序...
    jiangliang閱讀 1,391評論 0 1
  • quicksort可以說是應用最廣泛的排序算法之一,它的基本思想是分治法,選擇一個pivot(中軸點),將小于pi...
    黎景陽閱讀 471評論 0 1
  • 一秒變對稱。。。
    黑人不是牙膏閱讀 263評論 0 0
  • 回家的季節,也是欠薪的季節。到家后,被欠薪也是一種談資。他在村里雄赳赳地大談在那個南方城市里,有多少身家過億的老板...
    妄想先生閱讀 299評論 0 1