快速排序(Quick Sort)演算法全解析:C語言範例、原理與效能最佳化

1. 什麼是快速排序?基礎概念與概要

快速排序是一種排序演算法,廣泛應用於C語言及許多其他程式語言中,以高效率地對資料進行排序。此演算法由C. A. R. Hoare發明,最大的特色是執行速度極快。

快速排序的基本思路

快速排序會利用稱為樞紐(Pivot)的基準值將資料分割,並以遞迴方式對資料進行排序。透過這種分割治療法(Divide and Conquer),最終可以讓所有元素達到排序狀態。

  • 樞紐(Pivot):任意選定的值,根據此值將資料分為兩組。
  • 分割治療法:將問題拆解成較小的子問題,各自解決後,再整合出全體問題的答案。

相較於其他排序演算法(如泡沫排序、插入排序),快速排序在處理大型資料集時具有極高的效能優勢

2. 快速排序演算法說明

接下來,將詳細介紹快速排序的演算法運作機制。

樞紐的選擇

快速排序的第一步,是從陣列中選出樞紐。樞紐的選擇會影響演算法執行效率,選取不同的值會帶來不同的效能結果。

  • 範例:可選擇陣列的第一個元素、最後一個元素,或中間的元素作為樞紐。

遞迴分割處理

選好樞紐後,以此為基準把陣列分為兩部分。小於樞紐的值移到一邊,大於的移到另一邊。完成後,對剩餘的部分再次以相同方式遞迴處理。

  • 步驟1:將比樞紐小的元素移到左側,比樞紐大的元素移到右側。
  • 步驟2:對每個分割後的子陣列再次進行快速排序。

這個遞迴分割的過程持續下去,直到整個陣列都排序完成。

3. 用C語言實現快速排序

下面說明如何以C語言實際實現快速排序。以下是基本的快速排序程式碼範例:

#include <stdio.h>

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int partition(int arr[], int low, int high) {
    int pivot = arr[high]; // 選擇最後一個元素作為樞紐
    int i = (low - 1);     // 小於樞紐的元素索引

    for (int j = low; j <= high - 1; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return (i + 1);
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);

        // 對分割後的子陣列遞迴進行快速排序
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(arr) / sizeof(arr[0]);
    quickSort(arr, 0, n - 1);
    printf("Sorted array:n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("n"); // 輸出換行,讓結果更易讀
    return 0;
}

這段程式碼中,swap函式用於交換兩個元素,partition函式以樞紐為基準分割陣列,然後由quickSort函式進行遞迴排序。

4. 快速排序的效能與最佳化

快速排序的效能相較於其他排序演算法非常優異,但在最糟情況下運算時間也可能惡化。本節將說明效能細節與最佳化方法。

計算複雜度與最差情境

快速排序的平均複雜度為O(n log n),但在排序過或重複值過多的陣列中,最差情境會降至O(n^2),這時遞迴深度變大、效率降低。

最佳化技巧

  • 優化樞紐選擇:不要只用第一個或最後一個元素,選擇中間或隨機的元素可讓分割更平均。
  • 控制遞迴呼叫深度:可考慮改用非遞迴方式實現,降低堆疊溢位的風險。

5. 快速排序與其他排序法的比較

快速排序非常高效,但也有其他常見的排序演算法。本節將比較各排序法的特點與優缺點。

快速排序 vs. 合併排序

  • 演算法機制
    快速排序與合併排序同樣採用分割治療法,但分割和合併的方式不同。快速排序著重在分割,每個子陣列各自排序;合併排序著重於合併,將排序後的子陣列組合成最終結果。
  • 效能比較
    兩者的平均時間複雜度皆為O(n log n),但快速排序最差情境為O(n^2),合併排序即使在最差情況下也可保持O(n log n),效能較穩定。
  • 優缺點
    快速排序通常記憶體效率更佳(不需額外空間),適合大數據,但要注意最差情境。合併排序是穩定排序,即便資料近乎排序也可保證效能。

快速排序 vs. 堆積排序

  • 演算法機制
    快速排序採用遞迴分割,堆積排序則是將資料結構轉成堆積(Heap),依序取出最大(最小)元素進行排序。
  • 效能比較
    堆積排序與快速排序平均皆為O(n log n),但通常快速排序實際執行較快,尤其資料較亂時更明顯。
  • 優缺點
    快速排序一般比堆積排序快,但堆積排序即使在最壞情境下也能保證O(n log n),因此在極端情況下較安全。

快速排序 vs. 氣泡排序

  • 演算法機制
    氣泡排序會重複比較相鄰元素並交換,方式非常簡單但效率很低,與快速排序完全不同。
  • 效能比較
    氣泡排序時間複雜度為O(n^2),比快速排序低效很多。僅適合元素極少的場合。
  • 優缺點
    氣泡排序容易理解與實作,但效能差,實務上極少使用。

6. 快速排序常見錯誤與除錯方法

實作快速排序或使用qsort()時,常會遇到一些錯誤。這裡整理幾個常見錯誤及其對策。

1. 堆疊溢位(Stack Overflow)

快速排序屬於遞迴演算法,當遞迴層數過多時可能會發生堆疊溢位。避免方式包括限制遞迴深度,或考慮改為非遞迴實作

  • 解決方式:將遞迴處理改用迴圈,可有效降低堆疊溢位的風險。

2. 無窮迴圈

若遞迴處理或樞紐選擇出錯,可能會發生無窮迴圈。常見於樞紐選擇條件或結束條件未正確設定。

  • 解決方式:檢查樞紐選擇與排序終止條件,確保能正確結束遞迴。

3. 記憶體存取違規

以指標操作陣列元素時,若超出陣列範圍會出現記憶體存取違規錯誤。

  • 解決方式:確認未超出陣列範圍,且遞迴處理正確進行。

7. 總結與後續應用

快速排序是C語言中效率最高的排序演算法之一,特別適合大數據集。運用樞紐選擇與分割治療法,可遞迴完成高效排序。此外,標準函式庫的qsort()也可輕鬆完成強大排序功能。

實務應用時,建議依資料特性調整樞紐選擇,以提升效能。若遇到錯誤,請檢查遞迴與記憶體管理。希望本篇能幫助你從基礎到實作全面理解快速排序,靈活應用於實際開發中。

侍エンジニア塾