七種常見經典排序算法總結(C++)

最近想複習下C++,好久沒怎麼用了,畢業時的一些經典排序算法也忘差很少了,因此恰好一塊兒再學習一遍。html

除了冒泡、插入、選擇這幾個複雜度O(n^2)的基本排序算法,希爾、歸併、快速、堆排序,多多少少還有些晦澀難懂,幸虧又博客園大神dreamcatcher-cx都總結成了圖解,一步步很詳細,十分感謝。ios

並且就時間複雜度來講,這幾種算法到底有什麼區別呢,恰好作了下測試。算法

代碼參考: http://yansu.org/2015/09/07/sort-algorithms.htmlshell

//: basic_sort

#include <iostream>
#include <vector>
#include <ctime>
#include <string>

using namespace std;

// 獲取函數名字的宏
#define GET_NAME(x) #x
// 生成隨機數的宏
#define random(a,b) (rand()%(b-a+1)+a)
// 打印容器對象(vector)的宏
#define PRT(nums) { \
for(int i =0; i<nums.size(); i++){ \
    cout << nums[i] << " "; \
}\
}

/*
 冒泡排序
 基本思想: 對相鄰的元素進行兩兩比較,順序相反則進行交換,這樣,每一趟會將最小或最大的元素「浮」到頂端,最終達到徹底有序
 圖解: http://www.cnblogs.com/chengxiao/p/6103002.html
 考的最多的排序了吧。
 1. 兩層循環,最裏面判斷兩個數的大小,大的換到後面(正序)
 2. 內部循環一遍後,最大的數已經到最後面了
 3. 下一次內部循環從0到倒數第二個數(最後一個數經過第一步循環比較已經最大了)
 4. 依次循環下去
 時間複雜度O(n^2),空間複雜度是O(n)
 */
void bubble_sort(vector<int> &nums)
{
    for (int i = 0; i < nums.size() - 1; i++) {  // i用來控制已經冒泡的數字個數
        for (int j = 0; j < nums.size() - i - 1; j++) {  // 從最左邊遍歷到最後一個沒有浮動的數字
            if (nums[j] > nums[j + 1]) {
                nums[j] += nums[j + 1];
                nums[j + 1] = nums[j] - nums[j + 1];
                nums[j] -= nums[j + 1];
            }
        }
    }
}

/*
 插入排序
 基本思想: 每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完全部元素爲止。
 圖解: http://www.cnblogs.com/chengxiao/p/6103002.html
 1. 兩層循環,第一層i表示從左開始已經排好虛的部分
 2. 第二層循環,將當前的數以及它前面的全部數兩兩比較,交換大的數到後面(正序)
 3. 保證前面的數是排序好的,將新讀取的數經過遍歷前面排好序的部分並比較,插入到合適的位置
 時間複雜度O(n^2),空間複雜度是O(n)
 */
void insert_sort(vector<int> &nums)
{
    for (int i = 1; i < nums.size(); i++) {  // i表示從左開始已經排好序的部分
        for (int j = i; j > 0; j--) {  // 從當前數字位置遍歷到最左邊的數字位置
            if (nums[j] < nums[j - 1]) {
                int temp = nums[j];
                nums[j] = nums[j - 1];
                nums[j - 1] = temp;
            }
        }
    }
}

/*
 選擇排序
 圖解: http://www.cnblogs.com/chengxiao/p/6103002.html
 基本思想: 每一趟從待排序的數據元素中選擇最小(或最大)的一個元素做爲首元素
 1. 兩層循環,第一層從左到右遍歷,讀取當前的數
 2. min存放最小元素,初始化爲當前數字
 3. 內部循環遍歷和比較當前數字後後面全部數字的大小,若是有更小的,替換min爲更小數字的位置
 4. 內部遍歷以後檢查min是否變化,若是變化,說明最小的數字不在以前初始化的min位置,交換使每次循環最小的元素被移動到最左邊。
 時間複雜度O(n^2),空間複雜度是O(n)
 */
void selection_sort(vector<int> &nums)
{
    for (int i = 0; i < nums.size(); i++) {  // 從左到右遍歷全部數字
        int min = i;  // 每一趟循環比較時,min用於存放較小元素的數組下標,這樣當前批次比較完畢最終存放的就是此趟內最小的元素的下標,避免每次遇到較小元素都要進行交換。
        for (int j = i + 1; j < nums.size(); j++) {
            if (nums[j] < nums[min]) {
                min = j;
            }
        }
        if (min != i) {  //進行交換,若是min發生變化,則進行交換
            int temp = nums[i];
            nums[i] = nums[min];
            nums[min] = temp;
        }
    }
}

/*
 希爾排序
 圖解: http://www.cnblogs.com/chengxiao/p/6104371.html
 基本思想: 希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。
 1. 最外層循環設置間隔(gap),按常規取gap=length/2,並以gap = gap/2的方式縮小增量
 2. 第二個循環從gap位置向後遍歷,讀取當前元素
 3. 第三個循環從當前元素所在分組的上一個元素開始(即減去gap的位置),經過遞減gap向前遍歷分組內的元素,其實就是比較分組內i和i-gap元素的大小,交換大的到後面
希爾排序的時間複雜度受步長的影響,不穩定。
 */
void shell_sort(vector<int> &nums)
{
    for (int gap = int(nums.size()) >> 1; gap > 0; gap >>= 1) {  // 遍歷gap
        for (int i = gap; i < nums.size(); i++) {  // 從第gap個元素向後遍歷,逐個對其所在組進行直接插入排序操做
            int j = i - gap;   // j是這個分組內i元素的上一個元素
            for (; j >= 0 && nums[j] > nums[i]; j -= gap) {  // 從i向前遍歷這個分組內全部元素,把大的交換到後面
                swap(nums[j + gap], nums[j]);
            }
        }
    }
}

// 合併兩個有序序列
void merge_array(vector<int> &nums, int b, int m, int e, vector<int> &temp)
{
//    cout << "b: " << b << "  " << "m: " << m << "  " << "e: " << e << endl;
    int lb = b, rb = m, tb = b;
    while (lb != m && rb != e)
        if (nums[lb] < nums[rb])
            temp[tb++] = nums[lb++];
        else
            temp[tb++] = nums[rb++];
    
    while (lb < m)
        temp[tb++] = nums[lb++];
    
    while (rb < e)
        temp[tb++] = nums[rb++];
    
    for (int i = b;i < e; i++)
        nums[i] = temp[i];
//    cout << "temp: ";
//    PRT(temp);
//    cout << endl;
}

//遞歸對序列拆分,從b(開始)到e(結束)的序列,取中間點(b + e) / 2拆分
void merge_sort_recur(vector<int> &nums, int b, int e, vector<int> &temp)
{
    int m = (b + e) / 2;  // 取中間位置m
    if (m != b) {
        merge_sort_recur(nums, b, m, temp);
        merge_sort_recur(nums, m, e, temp);
        merge_array(nums, b, m, e, temp);  // 開始(b)到中間(m) 和 中間(m)到結束(e) 兩個序列傳給合併函數
    }
}

/*
 歸併排序
 圖解: http://www.cnblogs.com/chengxiao/p/6194356.html
 基本思想: 利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題而後遞歸求解,而治(conquer)的階段則將分的階段獲得的各答案"修補"在一塊兒,即分而治之)。
 1. 合併兩個有序序列的函數,合併後結果存入臨時的temp
 2. 從中間分,一直遞歸分到最小序列,即每一個序列只有一個元素,單位爲1(一個元素確定是有序的)
 3. 而後兩兩比較合併成單位爲2的n/2個子數組,在結果上繼續兩兩合併
時間複雜度是O(nlogn),空間複雜度是O(n)。
 */
void merge_sort(vector<int> &nums){
    vector<int> temp;
    temp.insert(temp.begin(), nums.size(), 0);  // 定義和初始化temp用於保存合併的中間序列
    merge_sort_recur(nums, 0, int(nums.size()), temp);
}


// 將啓始位置b做爲基準,大於基準的數移動到右邊,小於基準的數移動到左邊
void quick_sort_recur(vector<int> &nums, int b, int e)
{
    if (b < e - 1) {
        int lb = b, rb = e - 1;
        while (lb < rb) {  // 遍歷一遍,把大於基準的數移動到右邊,小於基準的數移動到左邊
            while (nums[rb] >= nums[b] && lb < rb)  //默認第一個數nums[b]做爲基準
                rb--;
            while (nums[lb] <= nums[b] && lb < rb)
                lb++;
            swap(nums[lb], nums[rb]);
        }
        swap(nums[b], nums[lb]);
//        cout << "nums: ";
//        PRT(nums);
//        cout << endl;
        quick_sort_recur(nums, b, lb);
        quick_sort_recur(nums, lb + 1, e);
    }
}

/*
 快速排序
 圖解: http://www.cnblogs.com/chengxiao/p/6262208.html
 基本思想: 快速排序也是利用分治法實現的一個排序算法。快速排序和歸併排序不一樣,它不是一半一半的分子數組,而是選擇一個基準數,把比這個數小的挪到左邊,把比這個數大的移到右邊。而後不斷對左右兩部分也執行相同步驟,直到整個數組有序。
 1. 用一個基準數將數組分紅兩個子數組,取第一個數爲基準
 2. 將大於基準數的移到右邊,小於的移到左邊
 3. 遞歸的對子數組重複執行1,2,直到整個數組有序
空間複雜度是O(n),時間複雜度不穩定。
 */
void quick_sort(vector<int> &nums){
    quick_sort_recur(nums, 0, int(nums.size()));
}

// 調整單個二叉樹的根節點和左右子樹的位置,構建大頂堆
// 在左右子樹中挑出最大的和根節點比較,把最大的數放在根節點便可
void max_heapify(vector<int> &nums, int root, int end)
{
    int curr = root;  // 根結點
    int child = curr * 2 + 1;  // 左子樹
    while (child < end) {
        if (child + 1 < end && nums[child] < nums[child + 1]) {
            child++;
        }
        if (nums[curr] < nums[child]) {
            int temp = nums[curr];
            nums[curr] = nums[child];
            nums[child] = temp;
            curr = child;
            child = 2 * curr + 1;
        } else {
            break;
        }
    }
}

/*
 堆排序
 圖解: http://www.cnblogs.com/chengxiao/p/6262208.html
 基本思想: 將待排序序列構形成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。而後將剩餘n-1個元素從新構形成一個堆,這樣會獲得n個元素的次小值。如此反覆執行,便能獲得一個有序序列了
 堆的概念(i是一個二叉樹的根節點位置,2i+1和2i+2分別是左右子樹):
 大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
 小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
 1. 由底(最後一個有葉子的根節點n/2-1)自上構建大頂堆
 2. 根節點(0)和末尾交換,末尾變爲最大
 3. 對餘下的0到n-1個數的根節點(0)二叉樹進行大頂堆調整(調用max_heapify)(根節點(0)的葉子節點已經大於下面的全部數字了)
堆執行一次調整須要O(logn)的時間,在排序過程當中須要遍歷全部元素執行堆調整,因此最終時間複雜度是O(nlogn)。空間複雜度是O(n)。
 */
void heap_sort(vector<int> &nums)
{
    int n = int(nums.size());
    for (int i = n / 2 - 1; i >= 0; i--) { // 構建大頂堆
        max_heapify(nums, i, n);
    }
    
    for (int i = n - 1; i > 0; i--) { // 排序, 將第一個節點和最後一個節點交換,確保最後一個節點最大
        int temp = nums[i];
        nums[i] = nums[0];
        nums[0] = temp;
        max_heapify(nums, 0, i);  // 從新調整最頂部的根節點
    }
}

void func_excute(void(* func)(vector<int> &), vector<int> nums, string func_name){
    clock_t start, finish;
    start=clock();
    (*func)(nums);
    finish=clock();
//    PRT(nums);  // 打印每次的排序結果
    cout << endl;
    cout << func_name << "耗時:" << float(finish-start)/float(CLOCKS_PER_SEC)*1000 << " (ms) "<< endl;
}

int main() {
    vector<int> b;
    srand((unsigned)time(NULL));
    for(int i=0;i<5000;i++)
        b.insert(b.end(), random(1,100));
    cout << "數組長度: " << b.size() << "; ";
//    PRT(b);  // 打印隨機數組
    cout << endl;

    void (*pFun)(vector<int> &);
    string func_name;

    pFun = bubble_sort;
    func_name = GET_NAME(bubble_sort);
    func_excute(pFun, b, func_name);

    pFun = insert_sort;
    func_name = GET_NAME(insert_sort);
    func_excute(pFun, b, func_name);

    pFun = selection_sort;
    func_name = GET_NAME(selection_sort);
    func_excute(pFun, b, func_name);
    
    pFun = shell_sort;
    func_name = GET_NAME(shell_sort);
    func_excute(pFun, b, func_name);
    
    pFun = merge_sort;
    func_name = GET_NAME(merge_sort);
    func_excute(pFun, b, func_name);
    
    pFun = quick_sort;
    func_name = GET_NAME(quick_sort);
    func_excute(pFun, b, func_name);
    
    pFun = heap_sort;
    func_name = GET_NAME(heap_sort);
    func_excute(pFun, b, func_name);
} ///:~

在數組很小的狀況下,沒有太大區別。可是較長數組,考的最多的冒泡排序就明顯比較吃力了~api

具體緣由只能從時間複雜度上面來看,但爲何差這麼多,我也不是徹底明白~數組

運行結果,排序算法分別耗時:dom

數組長度: 5000; 

bubble_sort耗時:183.4 (ms) 

insert_sort耗時:106.525 (ms) 

selection_sort耗時:68.036 (ms) 

shell_sort耗時:1.096 (ms) 

merge_sort耗時:1.226 (ms) 

quick_sort耗時:1.398 (ms) 

heap_sort耗時:1.514 (ms) 
Program ended with exit code: 0
相關文章
相關標籤/搜索