幾種經典的排序演算法
綜述
最近複習了各種排序演算法,記錄了一下學習總結和心得,希望對大家能有所幫助。本文介紹了氣泡排序、插入排序、選擇排序、快速排序、歸併排序、堆排序、計數排序、桶排序、基數排序9種經典的排序演算法。針對每種排序演算法分析了演算法的主要思路,每個演算法都附上了虛擬碼和C++實現。
電梯直達
1. 氣泡排序
2. 插入排序
3 .選擇排序
4. 快速排序
5. 歸併排序
6. 堆排序
7. 計數排序
8. 桶排序
9. 基數排序
演算法分類
原地排序(in-place):沒有使用輔助資料結構來儲存中間結果的排序**演算法。
非原地排序(not-in-place / out-of-place):使用了輔助資料結構來儲存中間結果的排序演算法
穩定排序:數列值(key)相等的元素排序後相對順序維持不變
不穩定排序:不屬於穩定排序的排序演算法
演算法複雜度
演算法複雜度參考了Big-O Cheat Sheet
1. 氣泡排序(Bubble Sort)
思路
不斷地遍歷數列,比較相鄰元素,每次把無序部分最大的元素放到最後,遍歷n-1次後,數列就是有序的了。
虛擬碼
BUBBLE_SORT(A, n)
for( i from 0 to n-2) //遍歷n-1次
for(j from 0 to n-2-i) //比較無序部分的所有相鄰元素
if(A[j] > A[j+1]) //如果前面的元素大,放到後面去
swap(A[i],A[j+1])
swapped = true
if(not swapped) //如果以第j個數為起點遍歷,沒有發生交換,說明後面已經有序了
break;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
最好情況
輸入數列有序,第一次遍歷結束就會完成排序,時間複雜度最好為Ω(n)
C++實現
void bubbleSort(vector<int> &arr)
{
for(int i = 0; i < arr.size() - 1; i++)
{
bool swapped = false;
for(int j = 0; j < arr.size() - 1 - i; j++)
{
if(arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if(!swapped)
{
break;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
2. 插入排序(Insertion Sort)
思路
把數列分為有序和無序部分,每次從無序部分拿出第一個元素,然後從後向前掃描有序部分,找到相應位置並插入,具體來說就是對於比當前元素大的元素,往後移動一位。直到找到比當前元素小的,在該元素後面插入當前元素
虛擬碼
INSERTION_SORT(A,n)
for(i from 1 to n-1) //從1開始遍歷無序陣列
temp = A[i] //取出當前元素
j = i-1
while(j >= 0 and temp < A[j]) //比temp大的元素後移
A[j+1] = A[j]
j -= 1
arr[j+1] = temp; //temp 放入第0個或者第一個不比temp大的元素
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
最好情況
輸入數列有序,每次插入都是直接插在了有序部分的後面,時間複雜度最好為Ω(n)
C++實現
void insertionSort(vector<int> &arr)
{
for(int i=1; i<arr.size(); i++)
{
int temp = arr[i];
int j = i -1;
while(j >=0 && temp < arr[j])
{
arr[j+1] = arr[j];
j--;
}
arr[j+1] = temp;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
3. 選擇排序(Selection Sort)
思路
數列分為有序部分和無序部分,重複下列過程n次:找到無序部分中最小的數,放到有序部分的最後面(即和無序部分的第一個置換)
虛擬碼
SELECTION_SORT(A, n)
for(i from 0 to n-2) //i指向無序部分的開頭,n-2為倒數第二個元素的索引
for(j from i to n-1) // 找到無序部分最小的元素
minLoc = findMin()
swap(A[i],A[minLoc]) //最小的元素置換到i位置上(加入了有序部分)
- 1
- 2
- 3
- 4
- 5
C++實現
void selectionSort(vector<int> &arr)
{
for(int i=0; i<arr.size()-1; i++)
{
int min = INT_MAX;
int minLoc = -1;
for(int j=i; j<arr.size(); j++)
{
if(arr[j] < min)
{
min = arr[j];
minLoc = j;
}
}
arr[minLoc] = arr[i];
arr[i] = min;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
4. 快速排序 (Quick Sort)
思路
使用分治演算法,每次以pivot為基準將數列分成兩部分,左邊的都小於等於基準,右邊的都大於基準,然後分別遞迴地對左右兩部分進行快速排序(終止條件是元素個數為1個或者0個)。演算法的核心在於分割槽(把數列分成兩部分)
分割槽時,從數列中選擇一個元素作為pivot(一般選最後一個,翻譯為“基準”或者“哨兵”),使用兩個指標,第一個指標始終指向左邊部分的結尾(初始位置為-1),第二個指標用於遍歷數列(初始位置為0),發現小於等於pivot的就和右部分第一個數字互換(相當於把數加入了左邊部分),比pivot大的數就跳過(相當於把數加入了右邊部分)
虛擬碼
QUICK_SORT(A,head,tail) //輸入數列A,[head, tail),不包含tail
if(tail - head > 1) //元素個數低於1個,有序,停止遞迴
pivot = PARTITION(A,head,tail) //分割槽,獲得pivot索引
QUICK_SORT(A,head,pivot)//遞迴
QUICK_SORT(A,pivot+1,tail)//遞迴,pivot已經在正確的位置上了,不參與後續排序
PARTITION(A,head,tail) //分割槽,[head,tail)
i = head - 1 //i初始化為head-1,代表著左半邊現在沒有元素
pivot = A[tail-1] //選擇最後一個元素作為pivot
for(j from head to tail-2) //遍歷全部元素(除了最後一個 tail-1)
if(A[j] <= pivot)//發現小於等於pivot的元素,置換(大於的話,j就直接後移了)
i += 1 //i此時指向了大於pivot的區的第一個元素
swap(A[i],A[j])
swap(A[i+1],A[tail-1]) //最後把pivot放到中間位置
return i+1 //返回pivot
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
最壞情況
- 輸入的數列是有序數列,這樣每次分割槽選到的pivot都是當前最大值,每次分割槽的結果都是左邊n-1個數,右邊0個數,需要進行n-1分割槽,遞迴深度為n,時間複雜度為O(n^2)
- 輸入的數列是逆序數列,與上面的情況類似,時間複雜也也為O(n^2)
最好情況
每次分割槽的結果都是均勻的分成了左右兩部分,那麼時間複雜度就是Θ(n log(n))
C++實現
void quickSort(vector<int> &arr)
{
quickSort(arr, 0, arr.size());
}
void quickSort(vector<int> &arr, int head, int tail)
{
if(tail - head > 1)
{
int pivot = partition(arr, head, tail);
quickSort(arr, head, pivot);
quickSort(arr, pivot+1, tail);
}
}
int partition(vector<int> &arr, int head, int tail)
{
int i = head - 1;
int pivot = arr[tail - 1];
for(int j = head; j < tail - 1; j++)
{
//這裡不能用<,陣列為[3,3]這樣時,i沒有移動過,一直為-1,quickSort在右半部分永久進行遞迴[0,2)
if(arr[j] <= pivot)
{
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
arr[tail - 1] = arr[i + 1];
arr[i + 1] = pivot;
return i + 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
5. 歸併排序(Merge Sort)
思路
使用分治演算法,把數列均勻地分成左右兩部分,分別進行歸併排序(遞迴終止條件為只有一個或者0個元素),然後將左右兩個有序數列合併到一起。
虛擬碼
MERGE_SORT(A,head,tail)
if(tail - head < 2) //元素個數小於2個就停止了
return
mid = (head + tail)/2
MERGE_SORT(A,head,mid) //左邊歸併排序
MERGE_SORT(A,mid,tail) //右邊歸併排序
copy A[head,mid) to B //複製A的左半部分到B,B有序
copy A[mid,tail) to C //複製A的右半部分到C,C有序
merge B,C to A //合併B和C兩個有序數列,將結果放在A中
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
C++實現
void mergeSort(vector<int> &arr)
{
mergeSort(arr, 0, arr.size());
}
void mergeSort(vector<int> &arr, int head, int tail)
{
if(tail - head < 2)
{
return;
}
int mid = (head + tail) / 2;
mergeSort(arr, head, mid);
mergeSort(arr, mid, tail);
vector<int> left(arr.begin() + head, arr.begin() + mid);
vector<int> right(arr.begin() + mid, arr.begin() + tail);
int i = 0, j = 0, k = head;
while(i < left.size() && j < right.size() && k < tail) //這裡k判斷條件是小於tail,不是arr.size()!!
{
if(left[i] < right[j])
{
arr[k++] = left[i++];
} else
{
arr[k++] = right[j++];
}
}
if(i == left.size())
{
while(j < right.size() && k < tail)
{
arr[k++] = right[j++];
}
} else if(j == right.size())
{
while(i < left.size() && k < tail)
{
arr[k++] = left[i++];
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
6. 堆排序(Heap Sort)
思路
構建大頂堆(降序構建小頂堆),然後交換root節點和最後一個節點,此時將堆的大小減1,並利用堆化演算法把堆重新調整為大頂堆,重複上述過程,直到大小為1,堆排序完成,因為每次都是把堆當前最大的數放到堆後面,所以數列最終變成有序了。
堆化演算法針對root的左右孩子均為大頂堆,但是root自己可能比左右孩子小的情況,演算法比較root和左右孩子,選擇最大的和root進行置換(如果root最大就不用置換了),置換後以被置換的孩子為root繼續執行堆化演算法,直到當前root比左右孩子大了或者已經是葉子節點了。
虛擬碼
//堆化演算法,左右孩子均為大頂堆,root可能比左右孩子小,違反大頂堆性質
MAX_HEAPIFY(A,i) //i是當前root的索引
left = i*2+1
right = i*2 + 2
//找到左、右孩子,root中的最大值
max = i
if(left < heapSize and A[left] > A[i])
max = left;
if(right <headpSize and A[right] > A[max])
max = right
if(max != i) //最大值不是root節點,交換之,並繼續堆化被破壞的子堆
swap(A[i],A[max])
MAX_HEAPIFY(A,max)
//自底向上構建大頂堆
BUILD_MAX_HEAP(A,n)
//從最後一個父節點開始(n-1)為最後一個元素的索引,自底向上執行堆化演算法
for(i from ((n-1)-1)/2 to 0)
MAX_HEAPIFY(A,i)
///堆排序演算法,不斷把root置換到堆的後面,heapSize減一併執行堆化演算法
HEAP_SORT(A,n)
BUILD_MAX_HEAP(A,n)
for(i from n-1 to 1)
swap(A[i],A[0])
heapSize -= 1
MAX_HEAPIFY(A,0)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
C++實現
void heapSort(vector<int> &arr)
{
buildMaxHeap(arr);
int heapSize = arr.size();
for(int i = arr.size() - 1; i > 0; i--)
{
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
heapSize--;
maxHeapify(arr,heapSize,0);
}
}
void buildMaxHeap(vector<int> &arr)
{
for(int i = ((arr.size() - 1) - 1) / 2; i >= 0; i--)
{
maxHeapify(arr,arr.size(),i);
}
}
void maxHeapify(vector<int> &arr, int heapSize, int root)
{
int left = root * 2 + 1;
int right = root * 2 + 2;
int max = root;
if(left < heapSize && arr[left] > arr[max])
{
max = left;
}
if(right < heapSize && arr[right] > arr[max])
{
max = right;
}
if(max != root)
{
int temp = arr[max];
arr[max] = arr[root];
arr[root] = temp;
maxHeapify(arr,heapSize,max);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
7. 計數排序(Counting Sort)
思路
計數排序是一種非比較排序,對數列中每個元素X,通過統計數列中小於等於X的元素個數來計算X所處的位置進行排序。使用陣列統計元素個數, counts[i]記錄的是小於等於 i 的元素個數。
虛擬碼
COUNTING_SORT(A,n)
for(i from 0 to n-1) //計數
counts[A[i]]++
for(i from 1 to n-1) //累加,以便進行反向填充
counts[i] += counts[i-1]
for(i from n-1 to 0) //反向填充是為了保證排序是穩定的
B[counts[A[i]]-1] = A[i]
counts[A[i]] -= 1
A = B
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
C++實現:使用陣列計數
void countingSort(vector<int> &arr)
{
int max = 0;
for(int i = 0; i < arr.size(); i++)
{
if(arr[i] > max)
{
max = arr[i];
}
}
vector<int> counts(max + 1, 0);
for(int i = 0; i < arr.size(); i++)
{
counts[arr[i]]++;
}
for(int i = 1; i <= max; i++)
{
counts[i] += counts[i - 1];
}
vector<int> tempArr(arr.size(), -1);
for(int i = arr.size() - 1; i >= 0; i--)
{
tempArr[counts[arr[i]] - 1] = arr[i];
counts[arr[i]]--;
}
arr = move(tempArr);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
8. 桶排序(Bucket Sort)
思路
把數列中元素的值範圍分割成多個長度相等的區間,稱為桶,把元素按值所在區間分別放到不同的桶中。然後桶內分別進行排序(比如使用插入排序),最終獲得有序數列
虛擬碼
#下面的假設值範圍為0~999,桶數目BNUM為100
INDEX_OF(j)
return j/10 //這裡根據實際的資料情況和執行環境可以調整桶的分配方式
BUCKET_SORT(A,n)
list B //桶們
for(i from 0 to n-1) //分桶
insert A[i] to B[INDEX_OF[A[i]]]
for(i from 0 to BUMN-1)
INSERTION_SORT(B[i]) //分別桶內排序
A = B[0] + B[1] + ... + B[BNUM-1] //按序連線各個桶
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
C++實現
#define BUCKETS_NUM 100
int indexOf(int num)
{
return num/10;
}
void bucketSort(vector<int> &arr)
{
vector<vector<int>> buckets(BUCKETS_NUM);
for(int i=0; i<arr.size(); i++)
{
buckets[indexOf(arr[i])].push_back(arr[i]);
}
for(int i=0; i<BUCKETS_NUM; i++)
{
insertionSort(buckets[i]);
}
int k =0;
for(int i=0; i<BUCKETS_NUM; i++)
{
for(int j=0; j<buckets[i].size(); j++)
{
arr[k++] = buckets[i][j];
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
9. 基數排序(Radix Sort)
思路
把元素按照位置分割成不同的數字,從最低位部分開始,一直到最高位部分,分別對每部分進行入桶操作,入桶時桶內元素的相對順序不變,然後將桶按順序連線起來,進行下一部分的入桶排序。對整數來說,位數較短的前面補0,下面敘述假定數列元素都是非負整數
為什麼多次入桶後數列就有序了
因為進行高位入桶時是按序入桶的,所以高位相同的數字,低位的順序仍然保留下來了。只有高位不同的數字,低位的順序才會被打亂,高位不同肯定是按照高位的順序排的,所以打亂沒有影響。
下面舉個例子說明一下。
假設待排序數列為 01, 88, 13, 78, 56, 79, 07 , 28, 76
這裡為了方便理解,位數不夠的前面已經補0了。
第一次按照最低位(個位數)入桶
桶編號 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
- | 01 | 13 | 56 | 07 | 78 | 79 | ||||
- | 76 | 88 | ||||||||
- | 28 |
將桶按順序連線起來,形成新的數列01, 13, 56, 76, 07, 78, 88, 28, 79
第二次按照次最低位(十位數)入桶
桶編號 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
01 | 13 | 28 | 56 | 76 | 88 | |||||
07 | 78 | |||||||||
79 |
將桶按順序連線起來,得到新的數列01, 07, 13, 28, 56, 76, 78, 79, 88
,排序完成
為什麼要從最低位開始
以非負整數為例,每次從最低位開始的話,每次入桶都有10個桶。如果從最高位開始的話,第一次10個桶,第二次如果還是10個桶,那麼如果低位不同,高位的相對順序就會被打亂,這顯然是錯誤的。那麼為了保證高位的順序不被打亂。就必須要在高位的桶內進行排序,即每個桶裡面要再分10個桶。第二位總共需要10*10=100個桶。以此類推n位就需要10^個桶,雖然也可以實現,但是開銷太大,所以不從最高位開始。
虛擬碼
//以對非負整數進行基數排序為例
RADIX_SORT(A,n)
for(i from d-1 to 0) //d是數字的位數,進行d次入桶(排序)
for(j from 0 to n-1)
put A[j] into B[D[A[j]]] //D[x]是x第i位的值。
A = B[0] + B[1] + ... + B[9] //按序連線桶
- 1
- 2
- 3
- 4
- 5
- 6
C++實現
void radixSort(vector<int> &arr)
{
vector<vector<int>> buckets(10);
int radix = 1;
for(int i=0; i<10; i++) //INT_MAX為10位數,所以最多進行10次入桶
{
for(int j = 0; j < arr.size(); j++)
{
buckets[(arr[j] / radix) % 10].push_back(arr[j]);
}
int k = 0;
for(int i = 0; i < 10; i++)
{
for(int j = 0; j < buckets[i].size(); j++)
{
arr[k++] = buckets[i][j];
}
if(buckets[i].size() == arr.size())//全部在一個桶裡了,提前結束
{
return;
}
buckets[i].clear();
}
radix*=10;
}
}
相關文章
- Swift實現八種經典排序演算法Swift排序演算法
- 幾種常用的排序演算法排序演算法
- 排序演算法(七大經典排序演算法)排序演算法
- 經典排序演算法回顧:排序演算法
- JavaScript實現經典排序演算法JavaScript排序演算法
- 經典排序演算法PHP實現排序演算法PHP
- 簡述幾種常用的排序演算法排序演算法
- 五種C語言非數值計算的常用經典排序演算法C語言排序演算法
- 十大經典排序演算法之氣泡排序排序演算法
- 用 PHP 實現經典排序演算法PHP排序演算法
- 經典排序演算法 — C# 版(上)排序演算法C#
- 10大經典排序演算法動畫排序演算法動畫
- 經典排序演算法 — C#版本(中)排序演算法C#
- 幾種常見的排序演算法總結排序演算法
- 35.幾種常見的排序演算法排序演算法
- PHP 中四大經典排序演算法PHP排序演算法
- 經典排序演算法的 C語言 | Java 實現排序演算法C語言Java
- 幾種常見排序演算法總結排序演算法
- 幾種常用的排序演算法之JavaScript實現排序演算法JavaScript
- 幾種排序演算法的原理以及 Java 實現排序演算法Java
- 十大經典排序演算法(動圖演示)排序演算法
- 你“聽”過這些經典排序演算法嗎?排序演算法
- 經典排序之選擇排序(Java)排序Java
- 幾種排序的比較排序
- 轉載:十大經典排序的演算法部落格排序演算法
- 11.經典O(n²)比較型排序演算法排序演算法
- 十大經典排序演算法動畫與解析排序演算法動畫
- 【演算法與資料結構】經典排序演算法總結演算法資料結構排序
- python3實現幾種常見的排序演算法Python排序演算法
- 經典十大排序演算法(含升序降序,基數排序含負數排序)排序演算法
- 幾種常用的排序程式碼排序
- BAT 經典演算法筆試題 —— 磁碟多路歸併排序BAT演算法筆試排序
- Java十大經典排序演算法最強總結Java排序演算法
- 歸併排序的經典-求逆序對排序
- Golang語言排序的幾種方式Golang排序
- 經典演算法(19)教你兩分鐘學會【選擇排序】演算法排序
- 十大經典排序演算法動畫,看我就夠了!排序演算法動畫
- 十大經典排序演算法(動圖演示,收藏好文)排序演算法