1. 程式人生 > 其它 >資料結構與演算法 - 快速排序

資料結構與演算法 - 快速排序

快速排序

快速排序的核心思想也是分治法,分而治之。它的實現方式是每次從序列中選出一個基準值,其他數依次和基準值做比較,比基準值大的放右邊,比基準值小的放左邊,然後再對左邊和右邊的兩組數分別選出一個基準值,進行同樣的比較移動,重複步驟,直到最後都變成單個元素,整個陣列就成了有序的序列。

我們以[ 8,2,5,0,7,4,6,1 ]這組數字來進行演示

首先,我們隨機選擇一個基準值:

與其他元素依次比較,大的放右邊,小的放左邊:

然後我們以同樣的方式排左邊的資料:

繼續排 0 和 1 :

由於只剩下一個數,所以就不用排了,現在的陣列序列是下圖這個樣子:

右邊以同樣的操作進行,即可排序完成。

單邊掃描

快速排序的關鍵之處在於切分,切分的同時要進行比較和移動,這裡介紹一種叫做單邊掃描的做法。

我們隨意抽取一個數作為基準值,同時設定一個標記 mark 代表左邊序列最右側的下標位置,當然初始為 0 ,接下來遍歷陣列,如果元素大於基準值,無操作,繼續遍歷,如果元素小於基準值,則把 mark + 1 ,再將 mark 所在位置的元素和遍歷到的元素交換位置,mark 這個位置儲存的是比基準值小的資料,當遍歷結束後,將基準值與 mark 所在元素交換位置即可。

程式碼實現

void sort(vector<int> &arr)
{
    sort(arr, 0, arr.size()-1)
}

void sort(vector<int> &arr, int start, int end)
{
    if(end <= start) return;
    //切分
    int pivotIndex = partition(arr, start, end);
    sort(arr, start, pivotIndex-1);
    sort(arr, pivotIndex+1, end);
}

int partition(vector<int>& arr, int start, int end)
{
    int pivot = arr[start];
    int mark = start;

    for(int i = start+1; i <= end; i++) {
        if(arr[i] < pivot) {
            //小於基準值 則mark+1,並交換位置。
            mark++;
            int p = arr[mark];
            arr[mark] = arr[i];
            arr[i] = p;
        }
    }
    //基準值與mark對應元素調換位置
    arr[start] = arr[mark];
    arr[mark] = pivot;
    return mark;
}

雙邊掃描

另外還有一種雙邊掃描的做法,看起來比較直觀:我們隨意抽取一個數作為基準值,然後從陣列左右兩邊進行掃描,先從左往右找到一個大於基準值的元素,將下標指標記錄下來,然後轉到從右往左掃描,找到一個小於基準值的元素,交換這兩個元素的位置,重複步驟,直到左右兩個指標相遇,再將基準值與左側最右邊的元素交換。

我們來看一下實現程式碼,不同之處只有 partition 方法:

int partition(vector<int>& arr, int start, int end)
{
    int left = start;
    int right = end;
    int pivot = arr[start]; // 第一個元素作為基準值

    while(true) {
        //從左往右掃描
        while(arr[left] <= pivot) {
            left++;
            if(left == right) break;
        }

        //從右往左掃描
        while(pivot < arr[right]) {
            right--;
            if(left == right) break;
        }

        // 左右指標相遇
        if(left >= right) {
            break;
        }

        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
    }
    int temp = arr[start];
    arr[start] = arr[right];
    arr[right] = temp;
    return right;
}

極端情況

快速排序的時間複雜度和歸併排序一樣,\(O(n \log n)\),但這是建立在每次切分都能把陣列一刀切兩半差不多大的前提下,如果出現極端情況,比如排一個有序的序列,如[ 9,8,7,6,5,4,3,2,1 ],選取基準值 9 ,那麼需要切分 n - 1 次才能完成整個快速排序的過程,這種情況下,時間複雜度就退化成了 \(O(n^2)\),當然極端情況出現的概率也是比較低的。

所以說,快速排序的時間複雜度是 \(O(n \log n)\),極端情況下會退化成 \(O(n^2)\),為了避免極端情況的發生,選取基準值應該做到隨機選取,或者是打亂一下陣列再選取。

另外,快速排序的空間複雜度為 \(O(1)\)