冒泡排序 Bubble Sort
簡明解釋
通過依次比較、交換相鄰的元素大小(按照由小到大的順序,如果符合這個順序就不用交換)。1 次這樣的循環可以得到一個最大值,n - 1
次這樣的循環可以排序完畢。
屬性
- 穩定
- 時間複雜度
O(n²)
- 交換
O(n²)
- 對即將排序完成的數組進行排序
O(n)
(但是這種情況下不如插入排序塊,請繼續看下文)
核心概念
- 利用交換,將最大的數冒泡到最後
基本實現
首先定義一個數組,並求出數組長度,將這兩個以參數形式傳入函數實現
int arr[] = {1, 5, 4, 2, 7, 8, 9, 3, 6};
size_t length = end(arr) - begin(arr);
cout << "冒泡排序:" << endl;
BubbleSort(arr, length);
for (auto iter : arr) {
cout << iter << " ";
}
實現:
void BubbleSort(int *p, size_t length) {
for (auto i = 0; i < length; ++i) {
for (auto j = 0; j < length - i - 1; ++j) {
swap(p[j], p[j + 1]);
}
}
}
加入我有一個需求,以一個函數指針爲參數,用以實現升序或降序的需求呢
BubbleSort(arr, length, [](int a, int b) { return b - a; });
void BubbleSort(int *p, size_t length, function<int(int lhs, int rhs)> compareFunc) {
for (auto i = 0; i < length; ++i) {
for (auto j = 0; j < length - i - 1; ++j) {
if (compareFunc(p[j], p[j + 1]) > 0) {
swap(p[j], p[j + 1]);
}
}
}
}
調試輸出:
選擇排序 Selection Sort
簡明解釋
每一次內循環遍歷尋找最小的數,記錄下 minIndex
,並在這次內循環結束後交換 minIndex
和 i
的位置。
重複這樣的循環 n - 1
次即得到結果。
屬性
- 不穩定
Θ(n²)
無論什麼輸入,均爲Θ(n²)
Θ(n) 交換
: 注意,這裏只有n
次的交換,選擇排序的唯一優點*
核心概念
- “可預測”的時間複雜度,什麼進來都是
O(n²)
,但不穩定,唯一的優點是減少了swap
次數。
基本實現
void SelectionSort(int *p, size_t length) {
for (auto i = 0; i < length - 1; ++i) {
int min = i;
for (auto j = i + 1; j < length; ++j) {
if (p[min] > p[j]) {
min = j;
}
}
if (min != i) {
swap(p[i], p[min]);
}
}
}
插入排序 Insertion Sort
默認 a[0]
爲已排序數組中的元素,從 arr[1]
開始逐漸往已排序數組中插入元素,從後往前一個個比較,如果待插入元素小於已排序元素,則已排序元素往後移動一位,直到待插入元素找到合適的位置並插入已排序數組。
經過 n - 1
次這樣的循環插入後排序完畢。
屬性
- 穩定
- 適合場景:對快要排序完成的數組時間複雜度爲
O(n)
- 非常低的開銷
- 時間複雜度
O(n²)
由於它的優點(自適應,低開銷,穩定,幾乎排序時的
O(n)
時間),插入排序通常用作遞歸基本情況(當問題規模較小時)針對較高開銷分而治之排序算法, 如希爾排序或快速排序。
核心概念
- 高性能(特別是接近排序完畢時的數組),低開銷,且穩定
- 利用二分查找來優化
基本實現
void InsertSort(int *p, int *begin, int *end) {
for (auto iter = begin + 1; iter != end; ++iter) {
const int tmp = *iter;
auto index = iter - begin;
auto j = index;
while (j > 0 && tmp > p[j - 1]) {
p[j] = p[j - 1];
--j;
}
if (j != index) {
p[j] = tmp;
}
}
}
代碼可能比for i循環類型的難理解,原理一樣,也可以利用其它語言的語法糖。
希爾排序 Shell Sort
簡明解釋
希爾排序是插入排序的改進版,它克服了插入排序只能移動一個相鄰位置的缺陷(希爾排序可以一次移動 gap
個距離),利用了插入排序在排序幾乎已經排序好的數組的非常快的優點。
使用可以動態定義的 gap
來漸進式排序,先排序距離較遠的元素,再逐漸遞進,而實際上排序中元素最終位置距離初始位置遠的概率是很大的,所以希爾排序大大提升了性能(尤其是 reverse 的時候非常快,想象一下這時候冒泡排序和插入排序的速度)。
而且希爾排序不僅效率較高(比冒泡和插入高),它的代碼相對要簡短,低開銷(繼承插入排序的優點),追求這些特點(效率要求過得去就好,代碼簡短,開銷低,且數據量較小)的時候希爾排序是好的 O(n·log(n))
算法的替代品。
總而言之:希爾排序的性能優化來自增量隊列的輸入和 gap
的設定。
屬性
- 不穩定
- 在快要排序完成的數組有
O(n·log(n))
的時間複雜度(並且它對於反轉數組的速度非常快) O(n^3/2)
time as shown
關於不穩定:
我們知道, 單次直接插入排序是穩定的,它不會改變相同元素之間的相對順序,但在多次不同的插入排序過程中, 相同的元素可能在各自的插入排序中移動,可能導致相同元素相對順序發生變化。因此, 希爾排序並不穩定。
關於 worse-case time 有一點複雜:
The worse-case time complexity of shell sort depends on the increment sequence. For the increments 1 4 13 40 121…, which is what is used here, the time complexity is O(n3/2). For other increments, time complexity is known to be O(n4/3) and even O(n·log2(n)).
核心概念
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到
O(n)
的效率; - 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位 ;
其中 gap
(增量)的選擇是希爾排序的重要部分。只要最終 gap
爲 1 任何 gap
序列都可以工作。算法最開始以一定的 gap
進行排序。然後會繼續以一定 gap
進行排序,直到 gap = 1
時,算法變爲插入排序。
Donald Shell 最初建議 gap
選擇爲 n / 2
並且對 gap
取半直到 gap
達到 1 。雖然這樣取可以比 O(n²) 類的算法(插入排序、冒泡排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。
void ShellSort(int *arr, size_t len) {
int gap = 1;
while (gap < len) {
gap = gap * 3 + 1;
}
while (gap > 0) {
for (auto i = gap; i < len; ++i) {
auto idx = i - gap;
auto tmp = arr[i];
while (idx >= 0 && tmp < arr[idx]) {
arr[idx + gap] = arr[idx];
idx -= gap;
}
arr[idx + gap] = tmp;
}
gap = (int)floor(gap / 3);
}
}
快速排序 Quick Sort
簡明解釋
- 從數列中挑出一個元素,稱爲"基準"(pivot),
- 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任何一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
- 遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。
屬性
- 不穩定
- O(n²) time, 但是通常都是 O(n·log(n)) time (或者更快)
- O(log(n)) extra space
When implemented well, it can be about two or three times faster than its main competitors, merge sort and heap sort
核心概念
- 使用了分而治之的思想
基本實現
cout << "\n快速排序:" << endl;
QuickSort(arr, 0, end(arr) - begin(arr) - 1);
for (auto iter : arr) {
cout << iter << " ";
}
int Partition(int *p, int l, int r) {
auto x = p[l];
while (l < r) {
while (l < r && p[r] >= x) // 從右向左找第一個小於x的數
r--;
if (l < r)
p[l++] = p[r];
while (l < r && p[l] < x) // 從左向右找第一個大於等於x的數
l++;
if (l < r)
p[r--] = p[l];
}
p[l] = x;
return l;
}
void QuickSort(int *p, int l, int r) {
auto position = 0;
if (l < r) {
position = Partition(p, l, r); //返回劃分元素的最終位置
QuickSort(p, l, position - 1); //劃分左邊遞歸
QuickSort(p, position + 1, r); //劃分右邊遞歸
}
}
二分查找 BinarySearch
這裏補充一下二分查找的算法的實現。
核心概念是:折半。
//調用處
cout << "\n二分查找:" << endl;
auto index = BinarySearch(arr, length, 9);
cout << "\n二分查找結果:" << index << endl;
//定義
int BinarySearch(int p[] /*此處int *p 也可以*/, size_t len, int target) {
int min = 0;
int max = static_cast<int>(len - 1);
while (min <= max) {
const auto mid = (min + max) / 2;
cout << " find index: " << p[mid] << endl;
if (target == p[mid]) {
cout << " target finded ! " << p[mid] << endl;
return mid;
} else if (target < p[mid]) {
max = mid - 1;
} else if (target > p[mid]) {
min = mid + 1;
}
}
cout << " target not found" << endl;
return 0;
}
總結
- 數據幾乎快排序完成時?
插入排序不解釋
- 數據量小,對效率要求不高,代碼簡單時?
性能大小:希爾排序 > 插入排序 > 冒泡排序 > 選擇排序
- 數據量大,要求穩定的效率(不會像快速排序一樣有
O(n²)
的情況)(如數據庫中)?
堆排序
- 數據量大,要求效率高,而且要穩定?
歸併排序
- 數據量大,要求最好的平均效率?
性能大小:快速排序 > 堆排序 > 歸併排序
因爲雖然堆排序做到了 O(n·log(n)
,而快速排序的最差情況是 O(n²)
,但是快速排序的絕大部分時間的效率比 O(n·log(n)
還要快,所以快速排序真的無愧於它的名字。(十分快速)
- 選擇排序絕對沒用嗎?
選擇排序只需要 O(n)
次交換,這一點它完爆冒泡排序。