教你手撕排序,這里有一個概念就是穩定排序。假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的。並不是其算法復雜度的穩定,注意一下
代碼均經過測試,如果找到代碼bug可以留言
算法(Algorithm) 代表着用系統的方法描述解決問題的策略機制,可以通過一定規范的 輸入,在有限時間內獲得所需要的 輸出。
算法的好壞
一個算法的好壞是通過 時間復雜度 與 空間復雜度 來衡量的。就是代碼需要的時間和內存,也就你時間成本和空間成本。其實這個一個動態的調整,到一定程度,往往就是用空間去換取時間,或者去時間去換取空間(dp其實就是在用空間去換取時間)。當然優秀的代碼就是很優秀,排序也是這樣,他們的目標都是把一堆數字進行排序。
常見時間復雜度的 “大O表示法” 描述有以下幾種:
時間復雜度 | 非正式術語 |
---|---|
O(1) | 常數階 |
O(n) | 線性階 |
O(n2) | 平方階 |
O(log n) | 對數階 |
O(n log n) | 線性對數階 |
O(n3) | 立方階 |
O(2n) | 指數階 |
一個算法在N規模下所消耗的時間消耗從大到小如下:
O(1) < O(log n) < O(n) < O(n log n) < O(n2) < O(n3) < O(2n)
指數級的增長是非常快的
常見的排序算法
根據時間復雜度的不同,常見的算法可以分為3大類。
1.O(n²) 的排序算法
-
冒泡排序
-
選擇排序
-
插入排序
2.O(n log n) 的排序算法
- 希爾排序
-
歸並排序
-
快速排序
-
堆排序
3.線性的排序算法
-
計數排序
-
桶排序
-
基數排序
各種排序的具體信息
冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort) 是一種基礎的 交換排序。
冒泡排序之所以叫冒泡排序,是因為它每一種元素都像小氣泡一樣根據自身大小一點一點往數組的一側移動。
算法步驟如下:
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個;
-
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數;
-
針對所有的元素重復以上的步驟,除了最后一個;
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
圖示如下:
代碼如下:
void bubbleSort(vector<int> &a) { int len = a.size(); for (int i = 0; i < len - 1; i++) //需要循環次數 { for (int j = 0; j < len - 1 - i; j++) //每次需要比較個數 { if (a[j] > a[j + 1]) { swap(a[j], a[j + 1]); //不滿足偏序,交換 } } } }
還有一種假的寫法就是保證第一個最小,第二個次小,比較的是i和j,雖然也是對的,有點像選擇排序,但也不是。其不是冒泡排序
選擇排序(Selection Sort)
選擇排序(Selection sort) 是一種簡單直觀的排序算法。
選擇排序的主要優點與數據移動有關。
如果某個元素位於正確的最終位置上,則它不會被移動。
選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對 n 個元素的表進行排序總共進行至多 n - 1 次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。
選擇排序的算法步驟如下:
-
在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
-
然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾;
-
以此類推,直到所有元素均排序完畢。
圖示如下:
代碼如下:
void selectionSort(vector<int> &a) { int len = a.size(); for (int i = 0, minIndex; i < len - 1; i++) //需要循環次數 { minIndex = i; //最小下標 for (int j = i + 1; j < len; j++) //訪問未排序的元素 { if (a[j] < a[minIndex]) minIndex = j; //找到最小的 } swap(a[i], a[minIndex]); } }
插入排序(Insertion Sort)
插入排序(Insertion sort) 是一種簡單直觀的排序算法。
它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從后向前掃描,找到相應位置並插入。
插入排序的算法步驟如下:
-
從第一個元素開始,該元素可以認為已經被排序;
-
取出下一個元素,在已經排序的元素序列中從后向前掃描;
-
如果該元素(已排序)大於新元素,將該元素移到下一位置;
-
重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
-
將新元素插入到該位置后;
-
重復步驟2~5。
圖示如下:
代碼如下:
void insertionSort(vector<int> &a) { int len = a.size(); for (int i = 0, j, temp; i < len - 1; i++) //需要循環次數 { j = i; temp = a[i + 1]; while (j >= 0 && a[j] > temp) { a[j + 1] = a[j]; j--; } a[j + 1] = temp; } }
希爾排序(Shell Sort)
希爾排序,也稱 遞減增量排序算法,是 插入排序 的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
-
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到 線性排序 的效率;
-
但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位。
步長的選擇是希爾排序的重要部分。
只要最終步長為1任何步長序列都可以工作。
算法最開始以一定的步長進行排序。
然后會繼續以一定步長進行排序,最終算法以步長為1進行排序。
當步長為1時,算法變為普通插入排序,這就保證了數據一定會被排序。
插入排序的算法步驟如下:
-
定義一個用來分割的步長;
-
按步長的長度K,對數組進行K趟排序;
-
不斷重復上述步驟。
圖示如下:
代碼如下:
void shell_Sort(vector<int> &a) { int len = a.size(); for (int gap = len / 2; gap > 0; gap /= 2) { for (int i = 0; i < gap; i++) { for (int j = i + gap, temp, preIndex; j < len; j = j + gap) //依舊需要temp作為哨兵 { temp = a[j]; //保存哨兵 preIndex = j - gap; //將要對比的編號 while (preIndex >= 0 && a[preIndex]>temp) { a[preIndex + gap] = a[preIndex]; //被替換 preIndex -= gap; //向下走一步 } a[preIndex + gap] = temp; //恢復被替換的值 } } } }
快速排序(Quick Sort)
快速排序(Quicksort),又稱 划分交換排序(partition-exchange sort) 。
快速排序(Quicksort) 在平均狀況下,排序 n 個項目要 O(n log n) 次比較。在最壞狀況下則需要 O(n2) 次比較,但這種狀況並不常見。事實上,快速排序 O(n log n) 通常明顯比其他算法更快,因為它的 內部循環(inner loop) 可以在大部分的架構上很有效率地達成。
快速排序使用 分治法(Divide and conquer) 策略來把一個序列分為較小和較大的2個子序列,然后遞歸地排序兩個子序列。
快速排序的算法步驟如下:
-
挑選基准值:從數列中挑出一個元素,稱為 “基准”(pivot) ;
-
分割:重新排序序列,所有比基准值小的元素擺放在基准前面,所有比基准值大的元素擺在基准后面(與基准值相等的數可以到任何一邊)。在這個分割結束之后,對基准值的排序就已經完成;
-
遞歸排序子序列:遞歸地將小於基准值元素的子序列和大於基准值元素的子序列排序。
遞歸到最底部的判斷條件是序列的大小是零或一,此時該數列顯然已經有序。
選取基准值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
圖示如下:
代碼如下:
int partition(vector<int> &a, int left, int right) { int pivot = a[right]; int i = left - 1; for (int j = left; j < right; j++) { if (a[j] <= pivot) { i++; swap(a[i], a[j]); } } swap(a[i + 1], a[right]); return i + 1; } void quickSort(vector<int> &a, int left, int right) { if (left < right) { int mid = partition(a, left, right); quickSort(a, left, mid - 1); quickSort(a, mid + 1, right); } } void qSort(vector<int> &a) { quickSort(a, 0, a.size() - 1); }
歸並排序(Merge Sort)
歸並排序(Merge sort) ,是創建在歸並操作上的一種有效的排序算法,時間復雜度為 O(n log n) 。1945年由約翰·馮·諾伊曼首次提出。該算法是采用 分治法(Divide and Conquer) 的一個非常典型的應用,且各層分治遞歸可以同時進行。
其實說白了就是將兩個已經排序的序列合並成一個序列的操作。
並歸排序有兩種實現方式
第一種是 自上而下的遞歸 ,算法步驟如下:
-
申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列;
-
設定兩個指針,最初位置分別為兩個已經排序序列的起始位置;
-
比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置;
-
重復步驟3直到某一指針到達序列尾;
-
將另一序列剩下的所有元素直接復制到合並序列尾。
具體代碼:
void mergeSort(vector<int> &a, vector<int> &T, int left, int right) { if (right - left == 1) return; int mid = left + right >> 1, tmid = left + right >> 1, tleft = left, i = left; mergeSort(a, T, left, mid), mergeSort(a, T, mid, right); while (tleft < mid || tmid < right) { if (tmid >= right || (tleft < mid && a[tleft] <= a[tmid])) { T[i++] = a[tleft++]; } else { T[i++] = a[tmid++]; } } for (int i = left; i < right; i++) a[i] = T[i]; } void mSort(vector<int> &a) { int len = a.size(); vector<int> T(len); mergeSort(a, T, 0, len); }
迭代比起遞歸還是安全很多,太深的遞歸容易導致堆棧溢出。所以建議可以試下迭代實現,acm里是夠用了
堆排序(Heap Sort)
堆排序(Heapsort) 是指利用 二叉堆 這種數據結構所設計的一種排序算法。堆是一個近似 完全二叉樹 的結構,並同時滿足 堆積的性質 :即子節點的鍵值或索引總是小於(或者大於)它的父節點。
二叉堆是什么?
二叉堆分以下兩個類型:
1.最大堆:最大堆任何一個父節點的值,都大於等於它左右孩子節點的值。
-
圖示如下:
-
數組表示如下:
[10, 8, 9, 7, 5, 4, 6, 3, 2]
2.最小堆:最小堆任何一個父節點的值,都小於等於它左右孩子節點的值。
-
圖示如下:
-
數組表示如下:
[1, 3, 2, 6, 5, 7, 8, 9, 10]
堆排序的算法步驟如下:
-
把無序數列構建成二叉堆;
-
循環刪除堆頂元素,替換到二叉堆的末尾,調整堆產生新的堆頂。
代碼如下:
代碼如下:
void adjustHeap(vector<int> &a, int i,int len) { int maxIndex = i; //如果有左子樹,且左子樹大於父節點,則將最大指針指向左子樹 if (i * 2 + 1 < len && a[i * 2 + 1] > a[maxIndex]) maxIndex = i * 2 + 1; //如果有右子樹,且右子樹大於父節點和左節點,則將最大指針指向右子樹 if (i * 2 + 2 < len && a[i * 2 + 2] > a[maxIndex]) maxIndex = i * 2 + 2; //如果父節點不是最大值,則將父節點與最大值交換,並且遞歸調整與父節點交換的位置。 if (maxIndex != i) { swap(a[maxIndex], a[i]); adjustHeap(a, maxIndex,len); } } void Sort(vector<int> &a) { int len = a.size(); //1.構建一個最大堆 for (int i = len / 2 - 1; i >= 0; i--) //從最后一個非葉子節點開始 { adjustHeap(a, i,len); } //2.循環將堆首位(最大值)與末位交換,然后在重新調整最大堆 for (int i = len - 1; i > 0; i--) { swap(a[0], a[i]); adjustHeap(a, 0, i); } }
我這里用了遞歸寫法,非遞歸也很簡單,就是比較哪個葉子節點大,再繼續for下去
計數排序(Counting Sort)
計數排序(Counting sort) 是一種穩定的線性時間排序算法。該算法於1954年由 Harold H. Seward 提出。計數排序使用一個額外的數組來存儲輸入的元素,計數排序要求輸入的數據必須是有確定范圍的整數。
當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k) 。計數排序不是比較排序,排序的速度快於任何比較排序算法。
計數排序的算法步驟如下:
-
找出待排序的數組中最大和最小的元素;
-
統計數組中每個值為 i 的元素出現的次數,存入數組 C 的第 i 項;
-
對所有的計數累加(從數組 C 中的第一個元素開始,每一項和前一項相加);
-
反向填充目標數組:將每個元素 i 放在新數組的第 C[i] 項,每放一個元素就將 C[i] 減去1。
代碼如下:
void CountingSort(vector<int> &a) { int len = a.size(); if (len == 0) return; int Min = a[0], Max = a[0]; for (int i = 1; i < len; i++) { Max = max(Max, a[i]); Min = min(Min, a[i]); } int bias = 0 - Min; vector<int> bucket(Max - Min + 1, 0); for (int i = 0; i < len; i++) { bucket[a[i] + bias]++; } int index = 0, i = 0; while (index < len) { if (bucket[i]) { a[index] = i - bias; bucket[i]--; index++; } else i++; } }
桶排序(Bucket Sort)
桶排序(Bucket Sort) 跟 計數排序(Counting sort) 一樣是一種穩定的線性時間排序算法,不過這次需要的輔助不是計數,而是桶。
工作的原理是將數列分到有限數量的桶里。每個桶再個別排序。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間 O(n)。
桶排序的算法步驟如下:
-
設置一個定量的數組當作空桶子;
-
尋訪序列,並且把項目一個一個放到對應的桶子去;
-
對每個不是空的桶子進行排序;
-
從不是空的桶子里把項目再放回原來的序列中。
代碼如下:
我覺得遞歸調用桶排序比較慢,這里直接用了sort函數,其實這個函數能決定這個算法的優劣,這些排序都是針對固定的序列的,可以自己嘗試不同的算法去優化
size為1是,其實和計數排序是一樣的,不過這里使用了輔助的空間,沒有合並相同的,內存消耗要更大
void bucketSort(vector<int> &a, int bucketSize) { int len = a.size(); if (len < 2) return; int Min = a[0], Max = a[0]; for (int i = 1; i < len; i++) { Max = max(Max, a[i]); Min = min(Min, a[i]); } int bucketCount = (Max - Min) / bucketSize + 1; //這個區間是max-min+1,但是我們要向上取整,就是+bucketSize-1,和上面的形式是一樣的 vector<int> bucketArr[bucketCount]; for (int i = 0; i < len; i++) { bucketArr[(a[i] - Min) / bucketSize].push_back(a[i]); } a.clear(); for (int i = 0; i < bucketCount; i++) { int tlen = bucketArr[i].size(); sort(bucketArr[i].begin(),bucketArr[i].end()); for (int j = 0; j < tlen; j++) a.push_back(bucketArr[i][j]); } }
基數排序(Radix Sort)
基數排序(Radix sort) 是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然后按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是只能使用於整數。
工作原理是將所有待比較數值(正整數)統一為同樣的數字長度,數字較短的數前面補零。然后,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以后,數列就變成一個有序序列。
基數排序的方式可以采用 LSD(Least significant digital) 或 MSD(Most significant digital) 。
LSD 的排序方式由鍵值的 最右邊(最小位) 開始,而 MSD 則相反,由鍵值的 最左邊(最大位) 開始。
MSD 方式適用於位數多的序列,LSD 方式適用於位數少的序列。
基數排序 、 桶排序 、 計數排序 原理都差不多,都借助了 “桶” 的概念,但是使用方式有明顯的差異,其差異如下:
-
基數排序:根據鍵值的每位數字來分配桶;
-
桶排序:每個桶存儲一定范圍的數值;
-
計數排序:每個桶只存儲單一鍵值。
LSD 圖示如下:
LSD 實現如下:
注意不要用負數,用負數完全相反,正負都有可以都轉換為正數
void RadixSortSort(vector<int> &a) { int len = a.size(); if (len < 2) return; int Max = a[0]; for (int i = 1; i < len; i++) { Max = max(Max, a[i]); } int maxDigit = log10(Max) + 1; //直接使用log10函數獲取位數,這樣的話就不用循環了,這里被強制轉換是向下取整 int mod = 10, div = 1; vector<int> bucketList[10]; for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) { for (int j = 0; j < len; j++) { int num = (a[j] % mod) / div; bucketList[num].push_back(a[j]); } int index = 0; for (int j = 0; j < 10; j++) { int tlen=bucketList[j].size(); for (int k = 0; k < tlen; k++) a[index++] = bucketList[j][k]; bucketList[j].clear(); } } }