C/C++ 的十大排序法對比
文章將從低級到高級講解c/c++的幾種排序算法;並附上代碼和說明。
文章偏長(博主也是寫了一週才完成),不想看太多解釋的可以直接看代碼;
(博主試了一下手機用戶看錶格對齊有點怪,所以建議手機用戶看圖片,電腦用戶看錶格)
排序算法 | 平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序法 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
選擇排序法 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩定 |
插入排序法 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
希爾排序法 | O(nlog n) | O(nlog n) | O(n^2) | O(1) | 不穩定 |
歸併排序法 | O(nlog n) | O(nlog n) | O(nlog n) | O(n) | 穩定 |
快速排序法 | O(nlog n) | O(nlog n) | O(n^2) | O(log n) | 不穩定 |
堆 排 序 法 | O(nlog n) | O(nlog n) | O(nlog n) | O(1) | 不穩定 |
計數排序法 | O(n+k) | O(n+k) | O(n+k) | O(k) | 穩定 |
桶 排 序 法 | O(n+k) | O(n+k) | O(n^2) | O(n+k) | 穩定 |
基數排序法 | O(n+k) | O(n+k) | O(n*k) | O(n+k) | 穩定 |
排序穩定性是指:通俗地講就是能保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。
不穩定的排序算法:快、選、希、堆。
幾種常用的排序算法性能比較:
一千數據量:
快速排序 > 希爾排序 > 堆排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序
一萬數據量:
快速排序 > 堆排序 > 希爾排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序
十萬數據量:
堆排序 > 希爾排序 > 快速排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序
百萬數據量:
快速排序 > 堆排序 > 歸併排序 > 希爾排序 > 插入排序 > 選擇排序 > 冒泡排序
ps:談不上那種排序法就是最好的,要看應用場景,每種算法都有自己的優勢。
通常來說,快速排序在數據量較小數組特別混亂的情況時,表現得最優秀,而在數據量較大時堆排序表現得更優秀,平均來說希爾排序會比歸併排序和快速排序快一點,堆排序、歸併排序最壞情況都不會超過O(nlogn);
1.冒泡排序
思路: 相鄰數據兩兩比較,不斷循環輪次,每輪冒出最大(最小)的數放到有序區
穩定性解釋: 冒泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個
元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以冒泡排序是一種穩定排序算法。
僞代碼:
//兩兩比較,不斷循環輪次,每輪冒出最大(最小的數放到有序區)
int buf[10] = { 1,3,5,4,6,8,7,9,10,2 };
int len = sizeof(buf) / sizeof(int);
for (int i = 0; i < len-1; i++)
{
for (int j = 0; j < len-1-i; j++)
{
if(buf[j]>buf[j+1])
swap(buf[j], buf[j + 1]);
}
}
2.選擇排序
思路: 顧名思意,就是直接從待排序數組裏選擇一個最小(或最大)的數字,每次都拿一個最小數字出來,順序放入新數組,直到全部拿完。再簡單點,對着一羣數組說,你們誰最小出列,站到最後邊,然後繼續對剩餘的無序數組說,你們誰最小出列,站到最後邊,再繼續剛纔的操作,一直到最後一個,繼續站到最後邊,現在數組有序了,從小到大。
穩定性解釋: 選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裏面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因爲只剩下它一個最大的元素了。那麼,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。
比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序算法。
僞代碼:
//選擇 選擇第一個,與後面所有的數比較;如果小就交換,然後前面變成有序區,後面變成無序區
for (int i = 1; i < len - 1; i++)
{
for (int j = i+1; j < len ; j++)
{
if (buf[i] > buf[j])
{
swap(buf[i], buf[j]);
}
}
}
3.插入排序
思路: 插入排序就是每一步都將一個待排數據按其大小插入到已經排序的數據中的適當位置,直到全部插入完畢。
插入排序方法分直接插入排序和折半插入排序兩種。
直接插入:1.數據的第一個數是有序樹,其他爲無序數;2.遍歷無序數,把無序數逐個和有序數進行比較;3.定義一個臨時變量,存儲無序數,循環,把無序數賦值給有序樹
折半插入:排序基本思想和直接插入排序一樣,區別在於尋找插入位置的方法不同,折半插入排序採用折半查找法來尋找插入位置。折半查找法只能對有序的序列使用。基本思想就是查找插入位置的時候,把序列分成兩半(選擇一箇中間數mid),如果帶插入數據大於mid則到右半部分序列去在進行折半查找;反之,則到左半部分序列去折半查找。
穩定性解釋:
- 插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。
- 比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。
- 如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序。所以插入排序是穩定的。
僞代碼:
//方法一
for (int i = 1; i < Max; i++)
{
if (p[i - 1] > p[i])
{
int tmp = p[i];
int j = i - 1;
for (; j >= 0 && p[j] > tmp; --j)
{
p[j + 1] = p[j];
}
p[j + 1] = tmp;
}
}
//方法二
for (size_t i = 1; i < n; ++i)//用end的位置控制邊界
{
//單趟排序
int end = i - 1;
int tmp = a[i];
while (end >= 0)//循環繼續條件
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
break;1
}
a[end + 1] = tmp;
}
//方法三
//從第二個元素開始,加入第一個元素是已排序數組
for (int i = 1; i < N; i++) {
//待插入元素 array[i]
if (array[i] < array[i - 1]) {
int wait = array[i];
int j = i;
while (j > 0 && array[j - 1] > wait) {
//從後往前遍歷已排序數組,若待插入元素小於遍歷的元素,則遍歷元素向後挪位置
array[j] = array[j - 1];
j--;
}
array[j] = wait;
}
}
4.希爾排序
思路: 希爾排序其實就是跨固定步長的插入排序,然後依次縮減步長再進行排序,待整個序列中的元素基本有序(步長變1)時,演變成對全體元素進行一次直接插入排序。當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。因爲直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率有較大提高。
穩定性解釋: 希爾排序是按照不同步長對元素進行插入排序,所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂。 所以shell排序是不穩定的。
僞代碼:
// 希爾排序 時間複雜度O(nlogn)~O(n^2) 空間複雜度O(1)
void shell_sort(vector<int>& array)
{
int n = array.size();
//間隙每次都變小一般;知道步長爲1是變成插入排查,這時候數據大部分已經有序了,使用插入排序效率很高
for (int gap = n / 2; gap >= 1; gap /= 2)
{
for (int i = gap; i < n; i++)
{
// 使用插入排序算法,將元素依次插入所在小組的已排序列表中
int tmp = array[i];// 待插入元素
int j = i - gap;
for (; j >= 0 && array[j] > tmp; j -= gap)
{
array[j + gap] = array[j];
}
array[j + gap] = tmp;
}
}
}
5.歸併排序法
思路: 歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用,歸併排序將兩個已排序的表合併成一個表。類似二叉排序樹原理
第一, 分解: 把待排序的 n 個元素的序列分解成兩個子序列, 每個子序列包括 n/2 個元素.
第二, 治理: 對每個子序列分別調用歸併排序__MergeSort, 進行遞歸操作
第三, 合併: 合併兩個排好序的子序列,生成排序結果.
如圖,把最初的數據拆分成n個有序的列(長度爲1),然後兩兩歸併
如:數組 { 4,5,0,7,1,3 }
看成七個獨立的有序列:[4] [5] [0] [7] [1] [3]
每趟都得到長度爲n/2^n 個長度爲=<2n的有序列。然後繼續歸併
穩定性解釋: 歸併排序最好、最差和平均時間複雜度都是O(nlogn),穩定算法
缺點: 需要分配額外的空間
僞代碼:
//方法一 易懂
void Merge_sort(int array[], int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
Merge_sort(array, low, mid);
Merge_sort(array, mid + 1, high);
//i 遍歷第一區間[low mid]
int i = low;
//j 遍歷第二區間[mid+1 high]
int j = mid + 1;
int* temp = new int[high - low + 1];//堆區空間不隨着本函數結束而結束
memset(temp, 0, sizeof(temp));
int count = 0;
while (i <= mid && j <= high) {
//依次比較兩個區間較小的數,然後裝入temp數組
if (array[i] <= array[j]) {
temp[count++] = array[i++];
}
else {
temp[count++] = array[j++];
}
}
//比較完成後,假如第一區間還有剩餘,繼續裝載
while (i <= mid) {
temp[count++] = array[i++];
}
//比較完成後,假如第二區間還有剩餘,繼續裝載
while (j <= high) {
temp[count++] = array[j++];
}
//將歸併排好序的元素賦值給原數組
for (int i = low, k = 0; i <= high; i++, k++) {
array[i] = temp[k];
}
delete[]temp;
}
}
// 方法二:歸併排序容器
void merge_Sort(vector<int>& V, vector<int>& copyArray, int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
merge_Sort(V, copyArray, left, mid);
merge_Sort(V, copyArray, mid + 1, right);
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
{
if (i > mid)
{
copyArray[k] = V[j];
j++;
}
else if (j > right)
{
copyArray[k] = V[i];
i++;
}
else if (V[i] > V[j])
{
copyArray[k] = V[j];
j++;
}
else
{
copyArray[k] = array[i];
i++;
}
k++;
}
for (size_t i = left; i <= right; i++)
{
array[i] = copyArray[i - left];
}
}
}
void mergeSort(vector<int>& V)
{
vector<int> copyArray(V);
merge_Sort(V, copyArray, 0, array.size() - 1);
}
int main()
{
vector<int> V;
srand((unsigned)time(NULL));
for (int i = 0; i < 10; i++)
{
V.push_back(rand());
}
mergeSort(V);
for (auto it : V)
{
cout << it << " ";
}
}
6.快速排序法
思路: 快速排序的原理就是先選擇一個哨兵(爲了方便理解可以直接選擇中間數),然後將序列的值與哨兵值比較,小於哨兵的放在左邊,大於哨兵的放在右邊從而將序列分成兩部分,再重複對這兩部分進行排序直到所有序列有序。
穩定性解釋: 最壞情況演變成冒泡排序法,不穩定排序
僞代碼:
//快速排序 -遞歸法
template <class T>
void Quick_Sort(T* array, int left, int right)
{
if (left < right)
{
int i = left - 1, j = right + 1;//爲了使用前置++和--在這裏前後移動一位
T mid = array[(left + right) / 2];//取中間作爲基準
while (true)
{
while (array[++i] < mid);//移動前迭代器,大於哨兵記錄該迭代器
while (array[--j] > mid);//移動後迭代器,小於哨兵記錄該迭代器
if (i >= j)//直到前後迭代器相遇就退出循環
{
break;
}
//交換前大於哨兵和後小於哨兵的值;循環交換直到小於哨兵都在左邊,大於哨兵都在右邊
swap(array[i], array[j]);
}
Quick_Sort(array, left, i - 1);//break的時候中間認爲是有序了,所以可以往前後移動一位重新排序
Quick_Sort(array, j + 1, right);
}
}
7.堆排序
此排序比較難,需要讀者花時間去理解;如果之前沒有了解過堆排,建議先去了解下堆區結構。
思路: 堆排序的原理就是先構造一個最大堆(完全二叉樹),父結點大於左右子結點,然後取出根結點(最大值)與最後一個結點交換,重複調整剩餘的結點成最大堆,得到有序的序列。
堆分爲大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在數組的非降序排序中,需要使用的就是大根堆,因爲根據大根堆的要求可知,最大的值一定在堆頂。
既然是堆排序,自然需要先建立一個堆,而建堆的核心內容是調整堆,使二叉樹滿足堆的定義(每個節點的值都不大於其父節點的值)。調堆的過程應該從最後一個非葉子節點開始
穩定性解釋: 我們知道堆的結構是節點i的孩子爲2i和2i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長爲n 的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當爲n /2-1, n/2-2, …1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了, 所以堆排序不是穩定算法。
代碼:
ps:我封裝了一個堆排的類,由於有點長,這裏只給了僞代碼,有需要的可以私聊我
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
#define Max 10
void Rand(vector<int> &V)
{
srand((unsigned)time(NULL));
while (this->V.size() < 10)
{
int tmp = rand() % 20;
vector<int>::iterator it = find(V.begin(), V.end(), tmp);
if (it == V.end())
this->V.push_back(tmp);
}
for (auto it : V)
{
cout << it << " ";
}
cout << endl;
}
//方法一
void MaxHeap(vector<int>& nums, int beg, int end)
{
int curr = beg;
int child = curr * 2 + 1;
while (child < end)
{
if (child + 1 < end && nums[child] < nums[child + 1])
child++;
if (nums[curr] < nums[child]) {
swap(nums[curr], nums[child]);
curr = child;
child = 2 * curr + 1;
}
else
break;
}
}
void heap_sort(vector<int>& nums)
{
int n = nums.size();
for (int i = n / 2 + 1; i >= 0; i--)
{
MaxHeap(nums, i, nums.size() - 1);
}
for (int i = n - 1; i > 0; i--)
{
swap(nums[0], nums[i]);
MaxHeap(nums, 0, i);
}
}
int main()
{
vector<int> V;
Rand(V);
heap_sort(V);
for (auto it : V)
{
cout << it << " ";
}
}
8.計數排序
思路:
- 遍歷待排序數組A,找出其最小值min和最大值max;
- 創建一個長度爲max-min+1的數組B,其所有元素初始化爲0,數組首位對應數組A的min元素,索引爲i位置對應A中值爲min+i的元素;
- 遍歷數組A,在B中對應位置記錄A中各元素出現的次數;
- 遍歷數組B,按照之前記錄的出現次數,輸出幾次對應元素;
穩定性解釋: 穩定排序算法;
代碼:
// 計數排序
void count_Sort(vector<int>& array)
{
if (array.empty()){
return;
}
//找出最大最小值
int min = array.front(),max = array.front();
for (int i = 1; i < array.size(); i++)
{
if (min > array[i])
{
min = array[i];
}
else if (max < array[i])
{
max = array[i];
}
}
// 記錄各元素出現次數
vector<int> counts(max - min + 1);
for (int i = 0; i < array.size(); i++)
{
counts[array[i] - min]++;
}
// 根據記錄的次數輸出對應元素
int index = 0;
for (int j = 0; j < counts.size(); j++)
{
int n = counts[j];
while (n--){
array[index] = j + min;
index++;
}
}
}
9.桶排序
思路:
- 設置固定數量的空桶;
- 找出待排序數組的最大值和最小值;
- 根據最大最小值平均劃分各桶對應的範圍,並將待排序數組放入對應桶中;
- 爲每個不爲空的桶中數據進行排序(例如,插入排序);
- 拼接不爲空的桶中數據,得到排序後的結果。
穩定性解釋: 常見排序算法中最快的一種穩定算法;可以計算大批量數據,符合線性期望時間;外部排序方式,需額外耗費n個空間;
代碼:
// 桶排序
void bucketSort (vector<int>& array, int bucketCount)
{
if (array.empty())
{
return;
}
// 找出最大最小值
int max = array.front(), min = array.front();
for (int i = 1; i < array.size(); i++)
{
if (min > array[i])
{
min = array[i];
}
else if (max < array[i])
{
max = array[i];
}
}
// 將待排序的各元素分入對應桶中
vector<vector<int>> buckets(bucketCount);
int bucketSize = ceil((double)(max - min + 1) / bucketCount);
for (int i = 0; i < array.size(); i++)
{
int bucketIndex = (array[i] - min) / bucketSize;
buckets[bucketIndex].push_back(array[i]);
}
// 對各桶中元素進行選擇排序
int index = 0;
for (vector<int> bucket : buckets)
{
if (!bucket.empty())
{
// 使用選擇排序算法對桶內元素進行排序
selectSort(bucket);
for (int value : bucket)
{
array[index] = value;
index++;
}
}
}
}
// 桶排序
void bucketSort (vector<int>& array)
{
bucketSort (array, array.size() / 2);
}
10.基數排序
思路: 將各待比較元素數值統一數位長度,即對數位短者在前補零;根據個位數值大小,對數組進行排序;重複上一步驟,依次根據更高位數值進行排序,直至到達最高位;
穩定性解釋: 穩定算法;適用於正整數數據(若包含負數,那麼需要額外分開處理);對於實數,需指定精度,纔可使用此算法。
代碼:
// 基數排序,對array的left到right區段,按照curDigit位進行排序
void radixSortImprove(vector<int>& array, int left, int right, int curDigit)
{
if (left >= right || curDigit < 10)
{
return;
}
// 將各元素按當前位數值大小分入各桶
vector<vector<int>> buckets(10);
for (int i = left; i <= right; i++)
{
int bucketIndex = (array[i] % curDigit - array[i] % (curDigit / 10)) / (curDigit / 10);
buckets[bucketIndex].push_back(array[i]);
}
// 按照桶的順序,將桶中元素拼接
// 對於元素個數大於1的桶,桶內元素按照更低位來進行排序
int index = 0;
for (vector<int> bucket : buckets)
{
int newLeft = index, newRight = index;
for (int value : bucket)
{
array[index] = value;
index++;
}
newRight = index - 1;
radixSortImprove(array, newLeft, newRight, curDigit / 10);
}
}
// 基數排序(從高位開始)
void radix_Sort(vector<int>& v)
{
// 計算當前數組最大數位數
int curDigit = 10;
for (autovalue : v)
{
if (value / curDigit) {
curDigit *= 10;
}
}
radixSortImprove(array, 0, array.size() - 1, curDigit);
}
參考:https://blog.csdn.net/DeepLies/article/details/52593597
參考:https://blog.csdn.net/kuaizi_sophia/article/details/87954222
參考:https://blog.csdn.net/sunmc1204953974/article/details/39396449