1. 程式人生 > 其它 >[洛谷 P1177] 【模板】快速排序

[洛谷 P1177] 【模板】快速排序

技術標籤:演算法每日一題

題目描述

利用快速排序演算法將讀入的NN個數從小到大排序後輸出。

快速排序是資訊學競賽的必備演算法之一。對於快速排序不是很瞭解的同學可以自行上網查詢相關資料,掌握後獨立完成。(C++C++選手請不要試圖使用STL,雖然你可以使用sort一遍過,但是你並沒有掌握快速排序演算法的精髓。)

輸入格式

第11行為一個正整數NN,第22行包含NN個空格隔開的正整數a_iai​,為你需要進行排序的數,資料保證了A_iAi​不超過10^9109。

輸出格式

將給定的NN個數從小到大輸出,數之間空格隔開,行末換行且無空格。

輸入輸出樣例

輸入 #1複製

5
4 2 4 5 1

輸出 #1

複製

1 2 4 4 5

說明/提示

對於20\%20%的資料,有N\leq 10^3N≤103;

對於100\%100%的資料,有N\leq 10^5N≤105。

AC:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>

int a[100005];

template <typename T>
void swap(T& a, T& b)
{
    T tmp = a; a = b; b = tmp;
}


template <typename T>
void insert_sort(T a[], int left, int right)
{
    for (int i = left+1; i <= right; ++i)  // 依次拿牌
        for (int j = i ; j - 1 >= left && a[j] < a[j-1]; --j) // 每張牌嘗試向前移動   
            swap(a[j-1], a[j]);  
}

template <typename T>
void sort(T a[], int left, int right)
{
    if (left >= right) return;
    if (right - left < 20)  // 資料少於20個的時候採用插入排序
    {
        insert_sort(a, left, right);
        return;
    }
    bool _is_ordered = true;
    for (int i = left + 1; i <= right; ++i) if (a[i] < a[i - 1]) _is_ordered = false;
    if (_is_ordered) return;  // 本身有序的資料不排序
    int l = left, r = right, pivot = (l + r) >> 1;
    swap(a[pivot], a[l++]);  // 排序時選取pivot之後,將pivot放到陣列最低的位置(left), 同時將左邊的指標向後移動一位
    while (l <= r)
    {
        while (a[l] <= a[left] && l < right) ++l; // 找比 pivot 大的值,最大不能超過右邊邊界
        while (a[r] >= a[left] && r > left) --r;  // 找比 pivot 小的值,最小不能超過左邊邊界
        if (l >= r) break; // 能交換的都已經交換結束了,可以退出交換的步驟
        swap(a[l], a[r]);  // l < r 成立,依然可以交換
    }
    // 退出上面的迴圈的時候, i >= L 的資料都滿足 a[i] < a[left], i <= R 的資料都滿足 a[i] < a[left]
    swap(a[left], a[r]);  // 因為 a[left] 放在左邊,所以要和比其小的資料進行交換,即和 a[r] 進行交換
    if (r > left) sort(a, left, r - 1);
    if (r < right) sort(a, r + 1, right);
}

int main(void)
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
    sort(a, 0, n-1);
    printf("%d", a[0]);
    for (int i = 1; i < n; ++i) printf(" %d", a[i]); 
    return 0;
}

分析:

while 有兩個作用

1. 在 l < r 的時候進行 swap(a[l], a[r])

2. 保證完成了這個 while 流程之後,swap(a[left], a[r]) 是正確的,也就是在完成了這個 while 流程之後,應該保證:

對於 任意的 i, 滿足 left + 1<= i <= r, 有 a[left] < a[i]。

對於 任意的 i, 滿足 r <= i <= right,有 a[right] > a[i]。

為什麼 while(l < r) 不對

如果寫成 while(l < r),則考慮簡單的情況 : a[] = {1, 2}, left = 0, right = 1;

假設編譯器是取下整的,則 pivot = lower(0+1) >> 1 = 0, 然後 swap(a[left], a[pivot]) 得到的結果是

swap(a[0], a[0]), a[] = {1, 2} 不變。

接著 ++l ,即將左邊的指標向右移動一位。此時 l = r = 1,即左右兩個指標重合了。

然後嘗試進入外層的 while 迴圈:while (l < r),進不去因為此時 l == r && l == 1。

此時能否保證上面的第二個作用生效呢?答案是不能。因為沒有交換,也就是沒有辦法保證 <= r 的所有資料都滿足 小於開始的 a[pivot],那麼後面再進行 swap(a[left], a[r]) 的時候,就發生了 :

swap(a[left], a[r]) = swap(a[0], a[1]),結果是 a[] = {2, 1} 資料從有有序變成了無序。

但是如果寫成是 while (1) 或者是 while (l <= r) 就可以避免這個問題,即使區間大小為2,此時移動完 pivot 到了左邊之後,仍然可以進入 while 迴圈,將 l 和 r 指標移動到正確的位置。看看寫成 while (1) 或者 while (l <= r) 之後發生的情況:

a[] = {1, 2}

依然假設編譯器自動向下取整,首先 pivot = (l + r) >> 1 = 0,swap(a[pivot], a[l++]),此時就會發生

swap(a[0], a[0]) ,a[] = {1, 2},之後得到的情況為:a[] = {1, 2}, l == r && l == 1,然後進入了 while 迴圈,在這個迴圈裡,首先移動 l , r 指標到正確的位置上,左邊的指標 l 無法移動,已經到了最右邊,但是右邊的指標還可以移動,右邊的指標會發現此時 2 >= 1,繼續左移,然後來到最左邊,r == 0,之後判斷 l >= r 成立,break,然後 swap(a[l], a[r]),swap(a[0], a[0]),a[] = {1, 2} 沒有問題,演算法結束。

和STL::SORT對比

手寫的:

STL:

第三組資料比標準庫慢了很多,其他的都達到了標準庫的水平。