本文轉自:十大經典排序算法,其中有動圖+代碼詳解,本文簡單介紹+個人理解。
排序算法
經典的算法問題,也是面試過程中經常被問到的問題。排序算法簡單分類如下:

這些排序算法的時間復雜度等參數如下:

其中,n代表數據規模,k代表桶的個數,In-place代表不需要額外空間,Out-place代表需要額外的空間。
冒泡排序(Bubble Sort)
最簡單易懂的排序方法。每次比較兩個元素,如果順序錯誤,則交換之。重復地訪問整個序列,直到沒有元素需要交換。
算法描述
- 比較相鄰的元素。如果順序錯誤,就交換之;
- 遍歷序列,重復步驟一,一次遍歷后最大元素處於最右邊;
- 重復步驟二,每次重復,可以將一個元素移至相應位置(已排好),直至所有元素排好;
算法分析
最佳情況:\(T(n) = O(n)\),最差情況:\(T(n) = O(n^2)\),平均情況:\(T(n) = O(n^2)\)。
參考代碼
void bubbleSort(vector<int> &nums) {
int n = nums.size();
for(int i = 0; i < n; i++) {
for(int j = 0; j < n-1-i; j++) {
if(nums[j] > nums[j+1]) {
// 元素交換
nums[j] = nums[j]^nums[j+1];
nums[j+1] = nums[j]^nums[j+1];
nums[j] = nums[j]^nums[j+1];
}
}
}
}
選擇排序(Selection Sort)
最穩定的排序方法之一,無論什么情況時間復雜度都是 \(O(n^2)\),不需要額外空間。簡單直觀,每次找到未排序中的最小(最大)元素,放至相應位置,執行(n-1)次排序完成。
算法描述
- 遍歷無序序列,找到最小(最大)元素,將該元素與對應位置交換;
- 重復步驟一(n-1)次,將所有元素放至相應位置,即排序完畢;
算法分析
最佳情況:\(T(n) = O(n^2)\),最差情況:\(T(n) = O(n^2)\),平均情況:\(T(n) = O(n^2)\)。
參考代碼
void selectSort(vector<int> &nums) {
int n = nums.size(), minIndex;
for(int i = 0; i < n-1; i++) {
minIndex = i;
for(int j = i+1; j < n; j++) {
if(nums[j] < nums[minIndex])//尋找最小元素
minIndex = j;
}
if(i == minIndex) continue;//相同位置元素不可異或交換
// 元素交換
nums[i] = nums[i]^nums[minIndex];
nums[minIndex] = nums[i]^nums[minIndex];
nums[i] = nums[i]^nums[minIndex];
}
}
插入排序(Insertion Sort)
同樣是一種簡單易懂的排序算法,不需要額外空間。通過構建有序序列,對於未排序元素,在已排序序列中從后向前掃描,找到相應位置並插入。插入排序在實現上,通常采用in-place排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中,需要反復把已排序元素逐步向后挪位,為最新元素提供插入空間。
算法描述
一般來說,插入排序都采用in-place在數組上實現。具體步驟如下:
- 第一個元素視為已排序;
- 取出下一元素,在已排序列中從后向前掃描;
- 如果該已排序元素大於新元素,將該元素移到下一位置;
- 重復步驟3,直到找到已排序的元素≤新元素的位置,將新元素插入到該位置后;
- 重復步驟2~4,直至全部排好序。
算法分析
最佳情況:\(T(n) = O(n)\),最壞情況:\(T(n) = O(n^2)\),平均情況:\(T(n) = O(n^2)\)。
參考代碼
void insertSort(vector<int> &nums) {
int n = nums.size(), prev, num;
for(int i = 0; i < n; i++) {
prev = i-1;//有序序列尾部
num = nums[i];//當前元素
while(prev>=0 && nums[prev]>num) {
nums[prev+1] = nums[prev];//后移
prev--;
}
nums[prev+1] = num;
}
}
希爾排序(Shell Sort)
第一個突破O(n^2)的排序算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
希爾排序的核心在於間隔序列的設定。既可以提前設定好間隔序列,也可以動態的定義間隔序列。動態定義間隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。
算法描述
- 定義增量序列:{t1,t2,...,tk},其中一般有 t1>t2>...>tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子序列進行直接插入排序。僅增量因子為1 時,整個序列作為一個序列來處理,序列長度即為整個序列的長度。
舉例說明
上面的算法描述可能不是很好懂,舉個例子說明一下。對與{5, 2, 4, 1, 5, 9, 7, 8, 9, 0}序列,第一趟排序,增量t1=4(自定義),序列分為{5, 5, 9},{2,9,0},{4,7},{1,8},分別對其進行插入排序,序列變為{5,0,4,1, 5,2,7,8, 9,9};第二趟排序,增量t2=2,序列分為{5,4,5,7,9},{0,1,2,8,9},對其進行插入排序,序列變為{4,0, 5,1, 5,2, 7,8, 9,9};第三趟排序,增量t3=1,序列為{4,0,5,1,5,2,7,8,9,9},對其進行插入排序,變為{0,1,2,4,5,5,7,8,9,9}。
算法分析
希爾排序的時間復雜度和其增量序列有關系,這涉及到數學上尚未解決的難題;不過在某些序列中復雜度可以視為O(n^1.3);希爾排序時間復雜度的下界是n*log^2 n。
參考代碼
void shellSort(vector<int> &nums) {
int n = nums.size();
int gap, i, j;
for(gap = n/2; gap > 0; gap /= 2) {
//插入排序簡潔寫法
for(i = gap; i < n; i++) {
int num = nums[i];
for(j = i-gap; j>=0 && nums[j]>num; j-=gap)
nums[j+gap] = nums[j];
nums[j+gap] = num;
}
}
}
歸並排序(Merge Sort)
和選擇排序一樣,歸並排序的性能不受輸入數據的影響,但表現比選擇排序好的多,時間復雜度始終都是O(n log n)。代價是需要額外的內存空間。
歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。歸並排序是一種穩定的排序方法。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。
歸並排序有不少的應用,比如求解逆序對問題,只需要在歸並排序的過程中添加一行代碼就可以。
算法描述
- 把長度為n的輸入序列分成兩個長度為n/2的子序列;(分)
- 對這兩個子序列分別采用歸並排序;
- 將兩個排序好的子序列合並成一個最終的排序序列。(合)
合並的過程需要額外的空間,利用一個新數組,比較兩個子序列,不斷將較小元素加入新數組,最后再將新數組更新至原序列。
算法分析
最佳情況:\(T(n) = O(nlogn)\),最差情況:\(T(n) = O(nlogn)\),平均情況:\(T(n) = O(nlogn)\)。
空間復雜度為\(O(n)\)。
參考代碼
void Merge(vector<int> &nums, int first, int med, int last) {
int i = first, j = med+1;
vector<int> temp(nums.size());//額外空間
int cur=0;//當前位置
while(i<=med && j<=last) {
if(nums[i] <= nums[j])
temp[cur++] = nums[i++];
else
temp[cur++] = nums[j++];
}
while(i <= med)
temp[cur++] = nums[i++];
while(j <= last)
temp[cur++] = nums[j++];
for(int m = 0; m < cur; m++)//更新數組
nums[first++] = temp[m];
}
void mergeSort(vector<int> &nums, int first, int last) {
if(first < last) {
int med = first+(last-first)/2;
mergeSort(nums, first, med);
mergeSort(nums, med+1, last);
Merge(nums, first, med, last);
}
}
快速排序(Quick Sort)
基本思想:選定一個排序基准進行一趟排序,將所有元素分為兩部分(大於基准和小於基准),分別對兩部分在此進行快速排序。
快速排序可以用於求解第K大問題,因為每一次排序之后,可以固定一個元素。
算法描述
快速排序使用分治法來把一個序列分為兩個子序列。具體算法描述如下:
- 從序列中選取排序基准(pivot);
- 對序列進行排序,所有比基准值小的擺放在基准前面,所有比基准值大的擺在基准的后面,序列分為左右兩個子序列。稱為分區操作(partition);
- 遞歸,對左右兩個子序列進行快速排序。
算法分析
最佳情況:\(T(n) = O(nlogn)\),最差情況:\(T(n) = O(n2)\),平均情況:\(T(n) = O(nlogn)\)。
參考代碼
void quickSort(vector<int> &nums, int left, int right) {
if(left<right) {
int l=left, r=right;
int pivot = nums[left];//判斷標准值
while(l<r) {
while(l<r && nums[r]>=nums[l])//一定記住要加等於號,在下面加也行
r--;
swap(nums[l], nums[r]);
while(l<r && nums[l]<nums[r])//在這里加等於號也行,但必須有一個加
l++;
swap(nums[l], nums[r]);
}
nums[l]=pivot;
quickSort(nums, l+1, right);
quickSort(nums, left, l-1);
}
}
堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
算法描述
- 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
- 將堆頂元素R[1]與最后一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
- 由於交換后新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,然后再次將R[1]與無序區最后一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重復此過程直到有序區的元素個數為n-1,則整個排序過程完成。
可能看起來看起來有點復雜,在本文的參考鏈接中有動圖解釋,可能好容易理解一些。
算法分析
最佳情況:\(T(n) = O(nlogn)\),最差情況:\(T(n) = O(nlogn)\),平均情況:\(T(n) = O(nlogn)\)。
參考代碼
int len;
void heapify(vector<int> &nums, int i) {
int left = 2*i+1;
int right = 2*i+2;
int largest = i;
if(left<len && nums[left] > nums[largest])
largest = left;
if(right<len && nums[right] > nums[largest])
largest = right;
if(largest != i) {
swap(nums[i], nums[largest]);
heapify(nums, largest);
}
}
void buildMaxHeap(vector<int> &nums) {
len = nums.size();
for(int i = len/2; i>=0; i--)
heapify(nums, i);
}
void heapSort(vector<int> &nums) {
buildMaxHeap(nums);
for(int i = nums.size()-1; i>0; i--) {
swap(nums[0], nums[i]);
len--;
heapify(nums, 0);
}
}
計數排序(Counting Sort)
計數排序的核心在於將輸入的數據值轉化為鍵存儲在額外開辟的數組空間中。 作為一種線性時間復雜度的排序,計數排序要求輸入的數據必須是有確定范圍的整數。
計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素C[i]是待排序數組A中值等於i的元素的個數。然后根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。這種做法其實就是map的基本用法。
計數排序限制性太大,要求必須是確定范圍的整數。實際做題中根本用不到,不過在某些特殊場景中可能可以用上。
算法描述
- 找出待排序的數組中最大和最小的元素;
- 統計數組中每個值為i的元素出現的次數,存入數組C的第i項;
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
- 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。
算法分析
當輸入的元素是n 個0到k之間的整數時,它的運行時間是 \(O(n + k)\)。計數排序不是比較排序,排序的速度快於任何比較排序算法。由於用來計數的數組C的長度取決於待排序數組中數據的范圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據范圍很大的數組,需要大量時間和內存。
最佳情況:\(T(n) = O(n+k)\),最差情況:\(T(n) = O(n+k)\),平均情況:\(T(n) = O(n+k)\)。
參考代碼
void countingSort(vector<int> &nums, int maxValue) {
int bucket[maxValue+1] = {0};
int n = nums.size();
int sorted = 0;
for(int i = 0; i < nums.size(); i++) {
bucket[nums[i]]++;
}
for(int i = 0; i < maxValue+1; i++) {
while(bucket[i] > 0) {
nums[sorted++] = i;
bucket[i]--;
}
}
}
桶排序(Bucket Sort)
桶排序是計數排序的升級版。它利用了函數的映射關系,高效與否的關鍵就在於這個映射函數的確定(代碼中通過設定每個桶的容量間接設定此映射關系)。
桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分布,將數據分到有限數量的桶里,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序。
算法描述
- 設置桶的數量:先設定桶的容量,再根據數組的最大值和最小值計算出桶的數量;
- 遍歷輸入數據,並且把數據一個一個放到對應的桶里去;
- 對每個不是空的桶進行排序:由於每個桶數據量很小,可以采用插入排序;
- 將非空桶里排好序的數據拼接起來。
算法分析
桶排序最好情況下使用線性時間O(n),桶排序的時間復雜度,取決與對各個桶之間數據進行排序的時間復雜度,因為其它部分的時間復雜度都為O(n)。很顯然,桶划分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
最佳情況:T(n) = O(n+k),最差情況:T(n) = O(n+k),平均情況:T(n) = O(n2)。
參考代碼
void bucketSort(vector<int> &nums, int bucketSize) {
int n = nums.size();
if(n == 0) return;
int minValue = nums[0], maxValue = nums[0];
for(int i = 1; i < n; i++) {
if(nums[i] > maxValue) maxValue = nums[i];
if(nums[i] < minValue) minValue = nums[i];
}
if(bucketSize < 5) bucketSize = 5;//默認每個桶的容量為5
int bucketNum = (maxValue-minValue)/bucketSize + 1;//桶的數量
vector< vector<int> > buckets(bucketNum);
for(int i = 0; i < nums.size(); i++)
buckets[(nums[i]-minValue)/bucketSize].push_back(nums[i]);
int sorted = 0;
for(int i = 0; i < buckets.size(); i++) {
insertSort(buckets[i]);//插入排序
for(int j = 0; j < buckets[i].size(); j++)
nums[sorted++] = buckets[i][j];
}
}
基數排序(Radix Sort)
基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,復雜度為O(kn),n為數組長度,k為數組中的數的最大的位數;
基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最后的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的。
算法描述
- 取得數組中的最大數,並取得位數;
- nums為原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(利用計數排序適用於小范圍數的特點);
算法分析
最佳情況:\(T(n) = O(n * k)\),最差情況:\(T(n) = O(n * k)\),平均情況:\(T(n) = O(n * k)\)。
基數排序有兩種方法:MSD,從高位開始進行排序;LSD,從低位開始進行排序。
參考代碼
//LSD
void redixSort(vector<int> &nums, int maxDigit) {
int mod = 10;
int dev = 1;
vector< vector<int> > buckets(10);
for(int i = 0; i < maxDigit; i++, dev*=10, mod*=10) {
for(int j = 0; j < nums.size(); j++) {
int bid = nums[j] % mod / dev;//取出對應數位作為桶編號
buckets[bid].push_back(nums[j]);
}
int sorted = 0;
for(int i = 0; i < buckets.size(); i++) {
for(int j = 0; j < buckets[i].size(); j++)
nums[sorted++] = buckets[i][j];
buckets[i].clear();
}
}
}
總結
冒泡排序是基礎,每輪遍歷將“最大元素”移至正確位置(“最右邊”),不穩定的O(n^2);
選擇排序要了解,選擇排序每輪遍歷將“最小(大)元素”移至正確位置(“最左(右)邊”),穩定的O(n^2);
插入排序最簡單,適合數據量較小的排序,依然是O(n^2);
希爾排序是插入排序升級版,不好用,為O(nlog^2n);
歸並排序和快速排序要熟記原理並會寫代碼。時間復雜度都是O(nlogn),前者不穩定,后者穩定。最常用的排序方法。
堆排序代碼復雜,不太好理解,也不好用,為O(nlogn)。
計數排序、桶排序、基數排序都不是比較排序,可以歸為一類,對數據有特殊的要求。其中計數排序是基礎,類似建立map對應;桶排序將數據的大小分到不同的桶中,桶內再微小排序;基數排序則是多次的桶排序,每次桶排序根據對應數位將數據分到不同桶中。
本文版權歸作者AlvinZH和博客園所有,歡迎轉載和商用,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利.
