1. 程式人生 > >七種常見經典排序算法總結(C++)

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

ble cto line shell dream 得到 分而治之 十分 發生

最近想復習下C++,很久沒怎麽用了,畢業時的一些經典排序算法也忘差不多了,所以剛好一起再學習一遍。

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

而且就時間復雜度來說,這幾種算法到底有什麽區別呢,剛好做了下測試。

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

//: 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++) { // 從左到右遍歷所有數字 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. 兩層循環,第一層從左到右遍歷,讀取當前的數 2. 第二層循環,將當前的數以及它前面的所有數兩兩比較,交換大的數到後面(正序) 3. 保證前面的數是排序好的,將新讀取的數通過遍歷前面排好序的部分並比較,插入到合適的位置 時間復雜度O(n^2),空間復雜度是O(n) */ void insert_sort(vector<int> &nums) { for (int i = 1; i < nums.size(); 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); } ///:~

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

具體原因只能從時間復雜度上面來看,但為什麽差這麽多,我也不是完全明白~

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

數組長度: 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

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