閑着的時候看到一篇“九大排序算法在總結”,瞬間覺得之前數據結構其實都有學過,但當初大多數都只是老師隨口帶過,並沒有仔細研究一下。遂覺:這是欠下的賬,現在該還了。
排序按照空間分類:
In-place sort不占用額外內存或占用常數的內存 插入排序、選擇排序、冒泡排序、堆排序、快速排序。
Out-place sort:歸並排序、計數排序、基數排序、桶排序。
或者按照穩定性分類:
stable sort:插入排序、冒泡排序、歸並排序、計數排序、基數排序、桶排序。
unstable sort:選擇排序(5 8 5 2 9)、快速排序、堆排序。
針對以上九種算法,我都根據時空復雜性,還有實現的思路做了簡要介紹。並且進行了簡單的測試。希望能給同樣學習排序算法的同學一點幫助。
#ifndef __INCLUDE_MY_SORT__ #define __INCLUDE_MY_SORT__ #include <iostream> #include <cstdio> #include <vector> #define INF 0x3f3f3f3f using namespace std; /* 插入排序: 時間復雜度 最壞情況(數組逆序): O(n*n) 最好情況(數組有序): O(n) 空間復雜度 線性空間 */ namespace Insert_sort{ template<class T> void sort(T *a, int len){ int i, j; for( i = 0; i < len; i++){ j = i - 1; T key = a[i]; while(j >= 0 && a[j] > key){ a[j + 1] = a[j]; j --; } a[j + 1] = key; } } } /* 冒泡排序: 時間復雜度 最壞情況 O(n*n) 最好情況 O(n*n) 空間復雜度 線性空間 思路:每次將待排序的值(0 : len - i - 1)按照大小盡可能放到最右邊 第一個循環是冒泡的輪數,第二個循環是冒泡的范圍區域(未有序的區域)因為經過一次循環最大的一定在最右邊,2次循環次大的一定在倒數第二個以此類推。 這樣就保證了,下一次只需要在未有序的范圍內進行冒泡,算法是完備並且正確的。 */ namespace Bubble_sort{ template<class T> void sort(T *a, int len) { int i, j; for(i = 0; i < len; i ++) { for(j = 0; j < len - 1 - i; j ++){ if(a[j] > a[j + 1]) swap(a[j],a[j + 1]); } } } } /* 選擇排序: 時間復雜度 最壞情況 O(n*n) 最好情況 O(n*n) 空間復雜度 線性空間 思路: 每次選出最小的放到當前最走邊 */ namespace Selection_sort{ template <class T> void sort(T *a, int len) { int i , j, min_val = a[0], min_pos = 0; for(i = 0; i < len; i ++) { min_val = a[i], min_pos = i; for(j = i; j < len; j ++) { if(min_val > a[j]){ min_val = a[j]; min_pos = j; } } swap(a[i],a[min_pos]); } } } /* 歸並排序: 時間復雜度 最壞情況 O(nlgn) 最好情況 O(nlgn) 空間復雜度 線性空間 思路 :分治的思想 Divide(划分子問題)、Conquer(子問題求解)、Combine(將子解合並成原問題的解)。 不斷遞推分解,將大區間每次從中分段,直到左右區間只剩下一個元素,進行求解並合並左右兩個子區間,使其有序。 不斷遞推返回,直到合並到最大的區間。 */ namespace Merge_sort{ template<class T> void merge(T *a, int p, int m, int q) { //printf("Merge: p = %d, m = %d, q = %d\n",p,m,q); int l = m - p + 1, r = q - m; T *L = (T*)malloc((l + 1) * sizeof(T)); T *R = (T*)malloc((r + 1) * sizeof(T)); memcpy(L, a + p, l * sizeof(T));//這里很重要:左側包含了下標為 m 的元素 memcpy(R, a + m + 1,r * sizeof(T)); L[l] = INF; R[r] = INF; int i = 0,j = 0, k; for(k = p; k <= q; k ++) { if(L[i] < R[j]){ a[k] = L[i]; i ++; } else{ a[k] = R[j]; j ++; } } free(L); free(R); } template<class T> void divide(T *a, int p, int q) { if(p < q) { int m = (p + q) >>1; //cout<<" p = "<<p<<" m = "<<m<<" q = "<<q<<endl; divide(a, p, m); divide(a, m + 1, q); merge(a, p, m, q); } } template<class T> void sort(T *a, int len) { divide(a, 0, len - 1); } } /* 快速排序: 時間復雜度 最壞情況(數組有序,每次Partition都划分成1 | n - 1,一共需要划分n次,每個partition的復雜度也是o(n)) : O(n*n) 最好情況 : O(nlgn) 空間復雜度 線性空間 思路 :分治的思想 Divide(划分子問題)、Conquer(子問題求解)、Combine(將子解合並成原問題的解)。 不斷以r = partition的位置換分小區間,這樣讓r左邊區間都小於r,r右邊區間都大於r。 */ namespace Quick_sort{ template <class T> int partition(T *a, int p, int q) { int rand_index = rand() % (q - p + 1) + p;//隨機選擇key值避免退化n*n復雜度 if(rand_index < p || rand_index > q) rand_index = p; swap(a[p], a[rand_index]) ; int i = p,j = q,pVal = a[p]; while(i < j){ while(i < j && a[j] >= pVal) j --; swap(a[j], a[i]); while(i < j && a[i] <= pVal) i ++; swap(a[i], a[j]); } return i; } template <class T> void recursive_qsort(T *a, int p, int q) { if(p < q){ int r = partition(a, p, q); recursive_qsort(a, p, r); recursive_qsort(a, r+1, q); } } template <class T> void sort(T *a, int len) { recursive_qsort(a, 0, len - 1); } } /* 堆排序: 時間復雜度 最壞情況 O(nlgn) 最好情況 O(nlgn) 空間復雜度 線性空間 算法動態示意圖: https://en.wikipedia.org/wiki/Heapsort 思路 :step 1 建立大頂堆(同一父節點的兩個孩子之間的大小關系,不用糾結,只需要保證parent > max(lchild, rchild)) step 2 排序,將大頂堆的第一個元素(最大)與最后一個元素(最小 or 次小 or 次次小 ...)交換位置,再次調整heap,使maximum到堆頂。 repeat step 1. */ namespace Heap_sort{ template <class T> void heap_adjust(T *a, int i, int len) { T tmp = a[i]; int lchild = i * 2 + 1, rchild = i * 2 + 2, largest = i; if(rchild < len){ if(a[largest] < a[rchild]) largest = rchild; } if(lchild < len){ if(a[largest] < a[lchild]) largest = lchild; } if(largest != i){ swap(a[largest], a[i]); heap_adjust(a, largest, len); } } template <class T> void build_max_heap(T *a, int len) { for(int i = len / 2 - 1; i >= 0; i --) { heap_adjust(a, i, len); } } template <class T> void sort(T *a, int len) { build_max_heap(a, len); int heap_num = len; for(int i = len - 1; i >= 1; i --) { swap(a[0],a[i]); heap_num --;//已經有序的元素不在參與堆排序 heap_adjust(a, 0, heap_num); } } } /* 計數排序: 時間復雜度 最壞情況 O(n+k) 最好情況 O(n+k) 空間復雜度 線性空間 思路 :非比較排序,用空間換時間,適用於固定范圍的且元素較小的數組排序。 對於val ai, 比它小的元素有cnt[ai]個,那么ai一定放在cnt[ai] - 1 (下標從0開始)這個位置。 */ namespace Counting_sort{ const int Max_val= 10000, Max_len = 10000; template <class T> void sort(T *a, int len) { int i, j, cnt[Max_val + 1]; T *rank = (T*)malloc(len * sizeof(T)); for(i = 0; i <= Max_val; i ++) cnt[i] = 0; for(i = 0; i < len; i ++) cnt[a[i]] ++; for(i = 0; i < Max_val; i ++) cnt[i + 1] += cnt[i]; for(i = 0; i < len; i ++) { rank[ --cnt[a[i]]] = a[i];//val ai 可能有多個所以下一個 ai 的位置應該會靠前1位 } memcpy(a, rank, len * sizeof(T)); free(rank); } } /* 基數排序: 時間復雜度 O(d(n+radix)) (設待排序列為n個記錄,d個關鍵碼,關鍵碼的取值范圍為radix) 空間復雜度 線性空間 思路 :非比較排序,用空間換時間。 按照位數進行排序,從第0位開始,使用桶輔助排序,類似計數排序的思想,數個數,就是把對應位相同的num放到一起,最后按照0 - 9的優先級從新排序數組。 一共重復最大數的位數次。 */ namespace Radix_sort{ template <class T> T get_max_val(T *a, int len) { int i; T max_val = a[0]; for(i = 0; i < len; i ++) { if(max_val < a[i]) max_val = a[i]; } return max_val; } template <class T> int compute_dig_num(T max_val) { int dig_num = 1, test_val = 9, radix = 10; while(test_val < max_val) { dig_num ++; radix *= 10; test_val = radix - 1; } return dig_num; } template <class T> void sort(T *a, int len) { if(len <= 0) return ; T max_val = get_max_val(a, len); int dig_num = compute_dig_num(max_val), i, j, k, cnt = 0; vector<T>bucket[10]; int radix = 1; for( i = 0; i < dig_num; i ++) { for( j = 0; j < 10; j ++) bucket[j].clear(); for( j = 0; j < len; j ++) { int dig = (a[j] / radix) %10; bucket[dig].push_back(a[j]); } cnt = 0; for( j = 0; j < 10; j ++) { for(k = 0; k < bucket[j].size(); k ++) { a[cnt ++] = bucket[j][k]; } } radix *= 10; } } } /* 桶排序: 時間復雜度 最優情況 O(n)) 最壞情況 O(nlgn) 空間復雜度 線性空間 思路 :非比較排序,用空間換時間。 最壞情況運行時間:當分布不均勻時,全部元素都分到一個桶中,則O(n^2), 這里實現的是整數排序,正常的話桶排序的數據范圍是[0,1)。主要體現的是桶排序的思想。 桶內排序可以使用插入 堆 或者快速排序。這樣最壞情況就是O(nlgn)。 */ namespace Bucket_sort{ const int Max_val = 91000; template <class T> void sort(T *a, int len) { int i, j ; T cnt[Max_val]; for(i = 0; i < Max_val; i ++) cnt[i] = 0; for(i = 0; i < len; i ++) { if(a[i] > Max_val) return ; cnt[a[i]]++; } j = 0; for(i = 0; i < Max_val; i ++) { while(cnt[i]){ a[j++] = i; cnt[i] --; } } } } #endif//__INCLUDE_MY_SORT__
/* 希爾排序: 時間復雜度 最壞情況(數組逆序): O(N1.5) 最好情況(數組有序): O(Nlog2N) 空間復雜度 O(1) 思路:每次選擇增量d(d每次縮減一半,直到=1進行最后一次),進行一次插入排序; 這樣一開始d比較大,元素個數較少,元素無序的概率較高,使得插入排序次數較少 后來d比較小,元素較多,但是由於進行到后期元素有序概率比較高,從而也減小了排序次數。 */ namespace Shell_sort { template<class T> void sort(T *a, int len) { int i, j, d = len / 2; //gap每次縮短一半 while (d) { //倒着枚舉每一個區間 for (i = d; i < len; i ++) { int tmp = a[i]; //給插入元素挪出位置 for (j = i - d; j >= 0 && tmp < a[j]; j -= d) { a[j + d] = a[j]; }//插入 a[j + d] = tmp; } d /= 2; } } }
/* 堆排序: 最好最壞:O(n*log2n) 建堆O(n*log2n) 篩選法調整堆O(log2n) 總共循環了n-1次調整函數,所以調整堆時間復雜度為O(n*log2n) 熟悉了堆排序的過程后,可以發現堆排序不存在最佳情況,待排序序列是有序或者逆序時,並不對應於堆排序的最佳或最壞情況。且在最壞情況下時間復雜度也是O(n*log2n)。此外堆排序是不穩定的原地排序算法。 空間復雜度: O(1) 思路:先建立大頂堆, 然后每次把最有把握的最大的數(堆頂元素)放到數組尾部,確定有序,調整被扔到堆頂的無辜小一些的元素 調整整個堆,又得到最大元素。重復此步驟直到最后只剩下一個元素。 要點:1 只需要調整非葉子節點 一共 n/2個。這是因為堆是一個完全二叉樹。 2 倒着調整節點,可以使下層的最大元素傳遞到上層去。 */ template<class T> void updateMaxHeap(T *a, int i, int n) { int left = LS(i), right = RS(i), largest; if(left > n) return ; largest = left; if(right <= n && a[right] > a[largest]){ largest = right; } if(a[i] < a[largest]){ // 孩子 大於 父節點 需要調整 swap(a[i], a[largest]); updateMaxHeap(a, largest, n); }//如果根節點最大 那么不用繼續調整下去了 } template<class T> void create_max_heap(T *a, int n) { // 1...n/2 是樹中所有非葉子結點 只需要調整非葉子節點即可 // 倒着調整建堆:這點很重要! for(int i = n/2; i >= 1; i --){ updateMaxHeap(a, i, n); } } template<class T> void heap_sort(T *a, int n) { create_max_heap(a, n); cout<<"Max Heap: "<<endl; for(int i = 1; i <= n; i ++) cout<<a[i]<<" "; for(int i = n; i >= 2; i --){ swap(a[i], a[1]); updateMaxHeap(a, 1, i - 1); } cout<<"Sort: "<<endl; for(int i = 1; i <= n; i ++) cout<<a[i]<<" "; }
PS : 優先隊列使用堆實現的!不是什么樹!!!!
╮(╯▽╰)╭ 論文還沒看完 又開始瞎搗鼓了
注意最好的情況下算法的時間復雜度