史上最簡單的排序演算法?看起來卻滿是bug

高效能架構探索發表於2021-11-12

大家好,我是雨樂。

今天在搜論文的時候,偶然發現一篇文章,名為<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。這些執行緒睡醒之後,就把自己對應的數報出來即可。這樣等所有執行緒都醒來,排序就結束了。

演算法思路很簡單,但是存在一個問題,建立的執行緒數依賴於需要排序的陣列的元素個數,因此這個演算法暫且只能算是一個思路吧。

結語

這個演算法不一定是史上最簡單的排序演算法,但卻是最神奇的排序演算法。神奇之處在於 大於號和小於號顛倒了卻得到了正確的結果。

其實,我們完全可以用另外一個簡單的思路來理解這個演算法,那就是冒泡兩次,第一次非遞增排序,第二次非遞減排序,算是負負得正,得到了正確的結果吧。

由於"最簡單"演算法的時間複雜度過高,其僅僅算是一種實現思路,也算是開拓一下思路,實際使用的時候,還是建議使用 十大經典排序演算法。

今天的文章就到這裡,下期見。

相關文章