大家好,我是雨樂。
今天在搜論文的時候,偶然發現一篇文章,名為<Is this the simplest (and most surprising) sorting algorithm ever?>,看了裡面的內容,蠻有意思,所以今天藉助此文,分享給大家。
演算法
下面我看下虛擬碼實現,在證明該排序演算法正確性之前,我們暫且將其命名為ICan’tBelieveItCanSort?。
ICan’tBelieveItCanSort(A[1..n]) {
for i = 1 to n do
for j = 1 to n do
if A[i] < A[j] then
swap(A[i], A[j])
}
看了上面程式碼的第一反應是什麼?會不會跟我一樣,覺得
❝這不就是一個錯誤的氣泡排序演算法麼,只是把第二行把範圍寫錯,第三行交換的條件寫反了罷了?。
❞
下面是氣泡排序的虛擬碼:
BubbleSort(A[1..n]) {
for i = 1 to n do
for j = i + 1 to n do
if (A[i] > A[j]) then
swap(A[i], A[j]);
}
❝為了後續描述方便,我將該演算法統一稱之為"新演算法"。
❞
從上面兩個虛擬碼的實現來看,新演算法ICan’tBelieveItCanSort和傳統的氣泡排序演算法BubbleSort的區別如下:
-
在新演算法中,內迴圈為 for j = 1 to n do
而在傳統的冒泡演算法中,內迴圈為 for j = i + 1 to n do
-
在新演算法中,交換的條件為 if A[i] < A[j] then
而在傳統的氣泡排序演算法中,交換條件為 if A[i] > A[j] then
好了,我們言歸正傳,重新轉回到新演算法,為了方便大家閱讀,再此重新貼一次新演算法的虛擬碼:
ICan’tBelieveItCanSort(A[1..n]) {
for i = 1 to n do
for j = 1 to n do
if A[i] < A[j] then
swap(A[i], A[j])
}
先不論演算法的正確與否,因為在A[i] < A[j]時候才進行交換,所以上述程式碼給我們的第一印象就是 按照降序排列。但實際上,通過程式碼執行結果來分析,其確實是升序排列。
下面給出證明過程。
證明
下面將通過數學歸納法來證明此演算法的正確性。
假設Pᵢ是經過 i 次(1 ≤ i ≤ n)外迴圈後得到的陣列,那麼前i項已經是升序排列,即 A[1] ≤ A[2] ≤ . . . ≤ A[i]。
要證明該演算法正確,只需要證明P對於任何[i + 1..n]都成立。
根據數學歸納法,我們只要證明 P₁成立,假設 Pᵢ成立,接著再證明 Pi+1 也成立,命題即可得證。
P₁顯然是正確的,而且這一步和普通的冒泡演算法降序沒有區別,經過第1次外迴圈,A[1]就是整個陣列的最大元素。
接著我們假設Pᵢ成立,然後證明 Pi+1 成立。
下面我們開始證明新演算法的正確性?。
首先假設存在一個下標K:
❝首先假設 A [k](k 介於 1~i 之間)滿足 A[k] > A[i+1] 最小的一個數,那麼 A [k−1]≤A [i+1](k≠1)。
如果 A [i+1]≥A [i],那麼這樣的 k 不存在,我們就令 k=i+1。
❞
現在,我們考慮以下幾種情況:
-
1 ≤ j ≤ k−1 此時,由於A[1..i]是遞增有序,且A[K]是滿足A[k] > A[i+1] 最小的一個數,所以A[j] < A[i +1],沒有任何元素交換髮生。
-
k ≤ j ≤ i (顯然,當k = i+1的時候,不會進入此步驟)
由於 A[j] > A[i+1],所以每次比較後都會有元素交換髮生。
我們使用 A[] 和 A′[] 來表示交換前和交換後的元素,所以A[i+1] = A[k],A′[k]=A[i+1]。
經過一系列交換,最大元素最終被放到了 A[i+1] 位置上,原來的A[i+1]變成了最大元素,A[k]被插入了大小介於原來A[k]和A[k-1]之間的元素。
-
i+1 ≤ j ≤ n
由於最大元素已經交換到前 i+1 個元素中,此過程也沒有任何元素交換。
經過上面一系列條件,最終,P就是升序排序演算法執行完以後的結果。
❝由於內外迴圈完全一樣,所以此演算法可以說是最簡單的排序演算法了。
❞
優化
從上面的證明過程中,我們可以發現,除了 i=1 的迴圈以外,其餘迴圈裡 j=i-1 之後的部分完全無效,因此可以將這部分省略,得到簡化後的演算法。
ICan’tBelieveItCanSort(A[1..n]) {
for i = 2 to n do
for j = 1 to i - 1 do
if A[i] < A[j] then
swap(A[i], A[j])
}
對比
但從程式碼來看,新演算法像是冒泡演算法的變種,但是從上面證明過程來看,新演算法實際上是一種插入演算法。
下面為新演算法的模擬圖:
新演算法
下面為冒泡演算法的模擬圖:
冒泡演算法
實現
程式碼實現比較簡單,如下:
#include
#include
#include
void SimplestSort(std::vector &v) {
for (int i = 0; i < v.size(); ++i) {
for (int j = 0; j < v.size(); ++j) {
if (v[i] < v[j]) {
std::swap(v[i], v[j]);
}
}
}
}
int main() {
std::vector v = {9, 8, 1, 3,2, 5, 4, 7, 6};
SimplestSort(v);
for (auto item : v) {
std::cout << item << std::endl;
}
return 0;
}
輸出結果:
1
2
3
4
5
6
7
8
9
更簡單的演算法?
看完這篇論文,突然想起之前有個更簡單且容易理解的演算法,我們暫且稱之為休眠演算法。
思想:
❝構造n個執行緒,它們和這n個數一一對應。初始化後,執行緒們開始睡眠,等到對應的數那麼多個時間單位後各自醒來,然後輸出它對應的數。這樣最小的數對應的執行緒最早醒來,這個數最早被輸出。等所有執行緒都醒來,排序就結束了。
❞
例如對於 [4,2,3,5,9] 這樣一組數字,就建立 5 個執行緒,每個執行緒睡眠 4s,2s,3s,5s,9s。這些執行緒睡醒之後,就把自己對應的數報出來即可。這樣等所有執行緒都醒來,排序就結束了。
演算法思路很簡單,但是存在一個問題,建立的執行緒數依賴於需要排序的陣列的元素個數,因此這個演算法暫且只能算是一個思路吧。
結語
這個演算法不一定是史上最簡單的排序演算法,但卻是最神奇的排序演算法。神奇之處在於 大於號和小於號顛倒了卻得到了正確的結果。
其實,我們完全可以用另外一個簡單的思路來理解這個演算法,那就是冒泡兩次,第一次非遞增排序,第二次非遞減排序,算是負負得正,得到了正確的結果吧。
由於"最簡單"演算法的時間複雜度過高,其僅僅算是一種實現思路,也算是開拓一下思路,實際使用的時候,還是建議使用 十大經典排序演算法。
今天的文章就到這裡,下期見。