經典排序算法C++全實現:插入、選擇、冒泡、快排、歸併、基數,堆排、希爾...

以下代碼是個人學習排序算法的一些實踐,實現了大部分排序算法的升序版本,並且對每一種算法進行了簡要的介紹和複雜度分析。

涉及的算法如下:

  1. 插入排序:直接插入排序、折半插入排序、希爾排序
  2. 交換排序:冒泡排序、快速排序
  3. 選擇排序:簡單選擇排序、堆排序
  4. 其他類型:歸併排序、基數排序
#include <iostream>
#include <string>
#include <vector>
using namespace std;


// 以下排序均爲升序算法
void printArray(vector<int> A){
    for(int i = 0; i < A.size(); i++){
        cout << A[i] << " ";
    }
    cout << endl << endl;
}

/************************************

        插入排序

        1. 直接插入排序
        2. 折半插入排序
        3. 希爾排序(shell)

*************************************/

vector<int> simpleInsertSort(vector<int> A){
    /**
    -- 簡單插入排序的思想
    從數組的第二個元素開始,將其選爲目標待排序
    認定元素左邊的序列爲有序,而右邊的序列爲無序
    將目標元素和左邊序列從後向前進行比較,找到插入有序的位置t之後,
    先將該位置之後的元素往後移動1位,然後將目標元素插入到位置t

    空間效率:O(1)
    時間效率:O(n^2)
    最好情況 順序,O(n)
    最壞情況:逆序,比較次數達到最大,移動次數達到最大
    穩定的排序算法:不會改變相同關鍵字元素的相對位置。

    適用於順序存儲和鏈式存儲的線性表,適合基本有序和數據量不大的情況
    **/
    if(A.size() <= 1) return A;
    for(int i = 1; i < A.size(); i++){
        if(A[i] < A[i - 1]){
            int tmp = A[i];
            int j = 0;
            for(j = i - 1; j >= 0 && tmp < A[j]; j--)
                A[j + 1] = A[j];
            A[j + 1] = tmp;
        }
    }
    return A;
}

vector<int> binaryInsertSort(vector<int> A){
    /**
    折半插入在簡單插入排序的基礎上進行了改進
    在向前查找插入位置時使用了二分查找的方法,減少了比較的次數
    折半插入減少了比較的次數,約爲 O(nlog2), 元素移動次數沒有發生改變O(n^2)
    **/
    int i, j, low, high, mid;
    for(i = 1; i < A.size(); i++){
        int tmp = A[i];
        low = 0;
        high = i - 1;
        while(low <= high){
            mid = (low + high) / 2;
            if(A[mid] > tmp) high = mid-1;
            else low = mid + 1;
        }
        for(j = i - 1; j >= high + 1; j--)
            A[j + 1] = A[j];
        A[high + 1] = tmp;
    }
    return A;
}

vector<int> shellSort(vector<int> A){
    /****
    希爾排序,又稱縮小增量排序,由於簡單插入排序很適合基本有序的序列,
    那麼可以對一些較亂的序列進行處理,使之變爲基本有序的序列,
    然後在進行一次直接插入排序。
    思想如下:
    先將數組分割成若干子數組,如[i, i+d, i+2d, i+kd]。
    d爲步長,小於,數組長度。這樣整個數組就可以分成d個子數組。
    分別對這d個子數組進行直接插入排序。
    進行下一輪,取更小的步長e,重複上述動作,知道步長等於1,在進行一次直接插入排序完成。

    希爾排序是不穩定的,僅適用於順序存儲的線性表
    時間複雜度爲O(n^2),空間複雜度爲O(1)
    *****/
    for(int d = A.size() / 2; d > 0; d /= 2){
        for(int i = d; i < A.size(); i++){
            int tmp = A[i];
            int j = 0;
            for(j = i; j >= d && A[j - d] > tmp; j -= d)
                A[j] = A[j-d];
            A[j] = tmp;
        }
    }
    return A;
}


/*****************************

        交換排序

        1. 冒泡排序
        2. 快速排序

*****************************/

vector<int> bubbleSort(vector<int> A){
    /***
    冒泡排序是一種簡單的通過比較關鍵字來交換元素的排序方法
    助記碼:
     i∈[0,N-1)               //循環N-1遍
        j∈[0,N-1-i)           //每遍循環要處理的無序部分
            swap(j,j+1)          //兩兩排序(升序/降序)

    空間複雜度分析 常數個輔助單元 O(1)
    最優時間複雜度 O(n)
    最壞時間複雜度 O(n^2)
    平均時間複雜度 O(n^2)

    最壞情況下
    比較次數爲: n(n-1)/ 2
    移動次數爲:3n(n-1)/2

    屬於穩定排序
    ***/
    int i, j;
    for(i = 0; i < A.size() - 1; i++){ // 進行N-1趟比較
        bool flag = false;
        for(j = 0; j < A.size() - 1 - i; j++){
        // 在該趟比較中,0~N-1-i的序列中最大的元素移動到N-2-i的位置
        // 所以下一趟待冒泡的序列不需要考慮已經確定好位置的元素了
        // 冒泡序列需要減1.
            if(A[j] > A[j + 1]){
                int tmp = A[j];
                A[j] = A[j+1];
                A[j+1] = tmp;
                flag = true;
            }
        }
        if(!flag) return A;  // 若未發生交換,則說明排序已經完成
    }

    /* 上面的版本是從前向後比較的版本,這裏寫一下從後向前比較的版本,依舊是升序
    但不同的是,每趟冒泡總是將冒泡序列最小的元素移動到最前
    for(int i = 0; i < A.size() - 1; i++){
        for(int j = A.size() - 1; j > i; j--){
            if(A[j - 1] > A[j]){
                int tmp = A[j - 1];
                A[j-1] = A[j];
                A[j] = tmp;
            }
        }
    }
    */

    return A;
}


/**********************************************************************
                  快速排序   from wiki:

快速排序使用分治法(Divide and conquer)策略來把一個序列(list)
分爲較小和較大的2個子序列,然後遞歸地排序兩個子序列。

步驟爲:

1. 挑選基準值:從數列中挑出一個元素,稱爲“基準”(pivot),
2. 分割:重新排序數列,所有比基準值小的元素擺放在基準前面,
   所有比基準值大的元素擺在基準後面(與基準值相等的數可以到任何一邊)。
   在這個分割結束之後,對基準值的排序就已經完成,
3. 遞歸排序子序列:遞歸地將小於基準值元素的子序列和
   大於基準值元素的子序列排序。

遞歸到最底部的判斷條件是數列的大小是零或一,此時該數列顯然已經有序。


時間複雜度分析!!!

最壞的情況就是每次遞歸劃分時,pivot選取的位置位於子序列的第一個或者最後一個。
T (n) = O(n) + T(1) + T(n-1)
最壞時間複雜度爲 O(n^2)  選取的pivot後劃分的兩個區域大小爲n-1,0此時會發生較多的交換操作

平均時間複雜度爲 O(nlogn)

最好的情況就是每次劃分pivot,恰好位於序列的中間
T(n) = O(n) + 2 T(n/2)
最好時間複雜度爲 O(nlogn)選取的pivot恰好滿足左邊區域小於,右邊區域恰好大於。

空間效率分析!!!
由於快排是遞歸進行的,需要一個遞歸工作棧來保存每層遞歸調用的必要信息
其容量應該與遞歸調用的最大深度一直。
最好情況下棧深度爲 log2(n+1)向上取整
最壞情況下棧深度爲 n - 1
故最好空間複雜度爲 O(logn)
最壞情況下,空間複雜度爲O(n)


快排的問題是:
使用遞歸,空間代價較高
對於小規模數據,還不如插入排序

解決方案:
當遞歸數據規模充分小,則停止遞歸,調用一些簡單排序如插入排序

這裏實現的版本是以每個子表的第一個元素作爲pivot。
每趟快速排序,都會將pivot放置在最終合適的位置上

快排是一種不穩定的排序方法,例如,在pivot左邊有兩個關鍵字大於pivot且二者相等,
當其進行交換之後,相對位置發生了改變

***************************************************************************/

int partition(vector<int> &A, int low, int high){
    // 劃分操作,選定low位置的元素作爲pivot,對low~high中的元素進行處理
    // 使得pivot左邊元素小於pivot,右邊元素小於pivot
    int pivot = A[low];
    while(low < high){
        while(low < high && A[high] >= pivot) high--;  //從後往前數,找到第一個小於pivot的數
        A[low] = A[high];  // 將該數移動到pivot的左邊
        while(low < high && A[low] <= pivot) low ++;  //從前往後數,找到第一個大於pivot的數
        A[high] = A[low]; // 將該數移動到pivot的右邊
    }
    A[low] = pivot;
    return low;
}

void recursiveQuickSort(vector<int> &A, int low, int high){
    if(low < high){
        int pivotPos = partition(A, low, high);
        recursiveQuickSort(A, low, pivotPos - 1);
        recursiveQuickSort(A, pivotPos + 1, high);
    }
}

vector<int> quickSort(vector<int> A){
    recursiveQuickSort(A, 0, A.size() -1);
    return A;
}


/**** 快排的一個改進 (僞代碼)

void QuickSort(vector<int> A, int left, int right){
    cutoff = 50;  // 閾值,當進行快排的區間大於該值時,進行快排
    if(cutoff <= right - left){
        pivot = partition(A, left, right);
        quickSort(A, left, pivot-1);
        quickSort(A, pivot, right);
    }
    else{
        insertSort(A, left, right);  // 區間較小使用簡單排序
    }
}

*******/


/**************************

        選擇排序

        1. 簡單選擇排序
        2. 堆排序

***************************/



vector<int> simpleSelectionSort(vector<int> A){
    /**
    簡單選擇排序

    簡單選擇算法思想如下:
    假設數組大小爲n
    第一趟,選擇一個最小的數,選擇交換將其放置到位置0,
    第二趟,從位置1開始考慮,從剩下的位置中繼續找一個最小的元素,放置到位置1
    如此反覆,直到第n-1趟結束。

    時間複雜度爲 O(n^2)
    比較次數和序列的初始狀態無關,次數爲 n(n-1)/2
    不穩定排序
    **/
    int min = 0;
    for(int i = 0; i < A.size() - 1; i++){
        min = i;
        for(int j = i; j < A.size(); j++){
            if(A[j] < A[min]) min = j;
        }
        if(min !=i){
            int tmp = A[i];
            A[i] = A[min];
            A[min] = tmp;
        }
    }
    return A;
}

/***

    堆排序

    在簡單選擇算法的基礎上進行改進,我們可以考慮使用堆來快速查找最小元素

    在排序過程中,我們將數組視爲一個完全二叉樹順序存儲結構
    利用雙親和孩子結點的關係,快速查找最小或最大元素

    堆的定義如下:
    n個關鍵字L[1...n]序列稱爲堆,當且僅當序列滿足
    L(i) <= L(2i) && L(i) <= L(2i+1) (小根堆)
    or L(i) >= L(2i) && L(i) >= L(2i+1)  (大根堆)

    算法的過程就是:
    - 將數組建成大根堆
    - 將根頭結點和堆底結點交換(將最大值放置到當前數組的末尾)
    - 重新調整剩下的結點,變成大根堆。

    時間複雜度分析
    建立堆時間複雜度爲 O(n),之後有n-1次的向下調整的操作
    平均,最好,最壞的時間複雜度都爲O(nlogn)

    這裏需要了解一下堆的插入和刪除操作。

    堆的插入,我們先將元素放置在數組的末尾,對該結點執行向上調整操作
    向上調整的操作就是和父結點比較,如果大於父結點,那麼就和父結點對調,繼續和新的父結點比較

    刪除堆頂元素,那麼就讓最後一個元素和堆頂交換,刪除最後一個結點,然後重新從堆頂向下調整

**/


/***自底向上調整爲根大堆***/
vector<int> AdjustDown(vector<int> A, int start, int end){
    int dad = start;
    int son = 2 * dad + 1;
    while(son <= end){
        if(son + 1 <= end && A[son] < A[son+1])
            son++;
        if(A[dad] > A[son]) break;
        else{
            int tmp = A[dad];
            A[dad] = A[son];
            A[son] = tmp;
            dad = son;
            son = 2 * dad + 1;
        }
    }
    return A;
}


/*** 插入堆底元素,需要向上調整 **/
vector<int> AdjustUp(vector<int> A){
    int son = A.size() - 1;
    int dad = son / 2;
    while(son > 0 && A[0] > A[son]){
        if(A[son] > A[dad]){
            int tmp = A[son];
            A[son] = A[dad];
            A[dad] = tmp;
            son = dad;
            dad = son / 2;
        }
    }
    return A;
}


 vector<int> heapSort(vector<int> A){
    // 建立根大堆
    for(int i = A.size() / 2; i >= 0; i--)
        A = AdjustDown(A, i, A.size() - 1);
    // 循環地將根大堆的頂點和堆底進行交換
    for(int i = A.size() - 1; i > 0; i--){
        int tmp = A[0];
        A[0] = A[i];
        A[i] = tmp;
        A = AdjustDown(A, 0, i - 1);
    }
    return A;
 }

/********************************

    歸併排序(2路歸併排序)

    歸併的思想如下:
    一開始把數組看成是n個有序的子表,每個表長度爲1
    然後將兩個或兩個以上的有序表進行組合成一個新的有序表
    得到新的有序表的集合(n/2個),重新兩兩進行組合排序
    直到最終有序表的個數爲1個。

    時間複雜度爲 O(nlogn)進行歸併需要O(logn)趟,每趟的合併兩個有序表的時間複雜度爲O(n)

    由於merge操作不會修改兩個表中相同關鍵字的記錄次序,所以歸併排序是穩定的排序算法

    但是空間複雜度卻爲O(n)

*********************************/

void Merge(vector<int> &A, vector<int> &B, int low, int mid, int high){
    // 將A中數據全部賦值給B
    for(int k = low; k <= high; k++){
        B[k] = A[k];
    }
    int i = low, j = mid+1, k = i;

    // 合併兩個有序表,簡單來說就是用兩個指針,比較兩個指針的大小,將較小的存入A中
    // 然後移動指針
    for(; i <= mid && j <= high; k++){
        if(B[i] < B[j]) A[k] = B[i++];
        else A[k] = B[j++];
    }
    while(i<=mid) A[k++] = B[i++];
    while(j<=high) A[k++] = B[j++];
}

void recursiveMergeSort(vector<int> & A, vector<int> & B,int low, int high){
    if(low < high){
        int mid = (low + high) / 2;
        recursiveMergeSort(A, B, low, mid);
        recursiveMergeSort(A, B, mid + 1, high);
        Merge(A, B, low, mid, high);
    }
}

vector<int> mergeSort(vector<int> A){
    vector<int> B;  // 輔助數組
    for(int i = 0; i < A.size(); i ++){
        B.push_back(0);
    }
    recursiveMergeSort(A, B, 0, A.size() - 1);
    return A;
}

/******************************************************************************

    基數排序

    不是基於比較的排序,而是基於多關鍵字的排序思想。
    藉助分配和收集兩種操作對單邏輯關鍵字完成排序。

    在開始談基數排序之前,我們得先了解一下桶排序
    假設現在有 N 個整數,其值位於0~100之間,我們要在線性的時間內對其排序
    使用空間換時間的思路,我們可以申請一個長度爲M=101的數組A,初始化爲0
    讀N個整數,記爲i,並將其A[i]+= 1.
    然後從頭開始遍歷數組,讀取不爲0的A[i]的i,如果A[i]=2就需要讀兩次,
    這樣就完成了線性時間內的排序過程

    但是如果M>>N的話,而且數據不是整數,那麼要怎麼辦呢。

    由此就引申出了基數排序算法。算法分爲高位優先MSD和低位優先順序
    這裏只介紹低位優先順序的實現。

    首先明確基數是什麼,此處記爲r,簡單來說就是要滿足進位的數就是基數,如逢十進一,十就是基數

    對於一組數來說,我們可以得到其最大的數所包含的位數K,所有未到達K位的數,我們默認補0
    我們申請 r 個隊列來作爲桶保存數據(0~r-1),從低位第一位開始考慮,
    分配:如果該位所在的數是 1,那麼我們就將其存入隊列 1。當所有數都存入隊列之後
    收集:將隊列中的數從0~1-r中依次首尾連接,構成新的數組,重新上述操作,直到達到最高位,排序完成。


    空間複雜度爲 O(r)  r個隊列
    時間複雜度O(d(n+r)) 需要d趟收集和分配,一趟分配需要O(n),一趟收集需要O(r)

*******************************************************************************/

int maxBit(vector<int> A){
    // 獲取數組中最大數的位數
    if(!A.size()) return 0;
    int maxVal = A[0];
    for(int i = 1; i < A.size(); i++){
        if(A[i] > maxVal){
            maxVal = A[i];
        }
    }
    //  這裏假設數都是十位數
    int d = 1;
    int p = 10;
    while(maxVal>=p){
        maxVal /= p;
        ++d;
    }
    return d;
}

vector<int> radixSort(vector<int> A){
    int d = maxBit(A);  //獲取最大位數
    int len = A.size();

    int *tmp = new int[len];  // 存取臨時數組元素
    int *count = new int[10]; // 計數器
    int radix = 1;
    for(int i = 1; i <= d; i++){
        // 清空計數器
        for(int j = 0; j < len; j++)
            count[j] = 0;

        //按位計入桶
        for(int j = 0; j < len; j++){
            int k = (A[j] / radix) % 10;
            count[k]++;
        }

        // 按位累加計數器,將數轉換成排序好數組的位置信息
        for(int j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j];

        // 按照計數器的位置信息,從後向前將數存入tmp,
        for(int j = len - 1; j >= 0; j--){
            int k = (A[j]  / radix) % 10;
            tmp[count[k] - 1] = A[j];
            count[k]--;
        }

        for(int j = 0; j < len; j++){
            A[j] = tmp[j];
        }
        radix *= 10;
    }
    delete []tmp;
    delete []count;
    return A;
}




int main()
{
    int num = 0;
    vector<int> A;
    int a[10] = {18, 22, 31, 15, 13, 10, 11, 59, 67, 13};
    for(int i = 0; i < 10; i++){
        A.push_back(a[i]);
    }
    cout << "原序列: ";
    printArray(A);

    cout << "簡單插入排序: ";
    printArray(simpleInsertSort(A));

    cout << "二分插入排序:";
    printArray(binaryInsertSort(A));

    cout << "希爾排序:";
    printArray(shellSort(A));

    cout << "冒泡排序:";
    printArray(bubbleSort(A));

    cout << "快速排序:";
    printArray(quickSort(A));

    cout << "簡單選擇排序:";
    printArray(simpleSelectionSort(A));

    cout << "堆排序:";
    printArray(heapSort(A));

    cout << "歸併排序:";
    printArray(mergeSort(A));

    cout << "基數排序:";
    printArray(radixSort(A));

    cout <<"原序列:";
    printArray(A);
    return 0;
}

一個簡單的測試運行結果:
在這裏插入圖片描述
這裏給出一個總結排序算法的速記表,供各位參考。
在這裏插入圖片描述
本博客代碼來自於個人的一個數據結構與算法的總結項目,彙總了常見的數據結構和算法的實現。
歡迎點此訪問!
歡迎大家指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章