聊一聊那些腦洞大開、有趣又奇葩的排序演算法

小爭哥發表於2019-05-10

作者:王爭,前Google工程師
微信公眾號:小爭哥

前段時間,網上瘋傳這樣一個段子,”寫完這個排序演算法之後,我就被開除了“。我們一塊來看看,他到底寫了什麼樣的程式碼,能讓主管一怒之下,把他開除了。

聊一聊那些腦洞大開、有趣又奇葩的排序演算法
當我看到這段程式碼的時候,首先感嘆的是,作者真的腦洞大開啊。不過,你還真先別嘲笑作者的智商。如果你去查一下資料的話,你就發現,這是一個經典的排序演算法,叫做”睡眠排序“。哈哈,是不是很形象呢?

為了防止你寫個排序演算法被開除,又或者作為主管,一怒之下,開除員工,我今天就帶你看看,歷史上那些腦洞大開、有趣又奇葩的排序演算法。

1. 睡眠排序演算法(Sleep Sort)

我們要講的第一個腦洞大開的排序演算法,就是上面被開除的同學寫的那個,睡眠排序演算法。實際上,它的原理非常簡單,你看程式碼就應該能看懂的。

如果對6個資料進行大小排序,我們建立6個執行緒,每個執行緒處理一個數字,數字對應的執行緒sleep的時間。比如,我們要排序{102, 338, 62, 9132, 580, 666}這樣一組資料,我們就讓這6個執行緒分別sleep 102ms、338ms、62ms、9132ms、580ms、666ms。當執行緒喚醒之後,最先喚醒的執行緒,列印出來的資料最小。以此類推,最後喚醒的執行緒,列印出來的資料最大。

這個排序演算法,總的耗時,就是最大那個數字對應的執行緒sleep的時間。你可能會說,如果最大的數字很大,那等待最後一個執行緒睡醒,是不是要花很長時間呢?你說的沒錯。不過,我們可以讓執行緒以更小的時間粒度來睡眠,比如我們把上面的睡眠時間的單位從毫秒(ms)換成微妙(us)。

而且,你可別小看這個看起來不切實際的排序演算法。如果我們在未來的哪一天,真的能造出速度極快的量子計算機,那這個排序演算法可能就真的切合實際了。

2. 猴子排序演算法(Bogo Sort)

如果說剛剛那個排序演算法還有點用的話,現在馬上要講的這個排序演算法就是一個既不實用又非常低效的排序演算法了。

這個排序演算法的名字來自於無限猴子理論。這個理論實際上也很簡單,意思就是,如果我們有無限只猴子,給他們無限的時間,讓他們在鍵盤上隨便亂敲,也可能敲出一本莎士比亞。這實際上是一個概率問題。

看明白了無限猴子理論,我們再來看下什麼是猴子排序演算法。猴子排序演算法也很簡單。它利用的也概率論知識。針對要排序的資料集合,我們每次隨機生成一個排列,看是否完全滿足從小到大排列,如果不滿足,我們就繼續再隨機生成一個排列,直到隨機出一個排好序的排列。總有一天會歪打正著,正好遇到一個有序的排列。

while not isInOrder(deck):
    shuffle(deck)
複製程式碼

3. 慢速排序演算法(Slow Sort)

這個排序演算法是1986年由Andrei Broder和Jorge Stolfi發表。從名字上就能看出很慢的排序演算法:慢速排序演算法。它從結構上,看起來有點類似歸併排序演算法,虛擬碼如下。

procedure slowsort(A,i,j)
  if i >= j then return
    m := ⌊(i+j)/2⌋                            
    slowsort(A,i,m) // 先排序前半段
    slowsort(A,m+1,j) // 再排序後半段
  if A[j] < A[m] then swap A[j] and A[m] // 找到最大數,放到末尾
    slowsort(A,i,j-1) // 再排序除了最大數之外的資料
複製程式碼

從程式碼上實現上看,這個排序演算法看似很牛逼的樣子,分治思想、遞迴實現都用上了。我們想要排序下標是i到j之間的資料,演算法先排序好前半段,再排序好後半段,然後把最大值放到下標為j的位置,最後還要再把除了最大值之外的下標是i到j之前的資料,再重新排序。

演算法是正確,可以實現將一個無序資料集合排序的效果。但如果我們把時間複雜計算的遞迴公式寫出來,你就知道,它的時間複雜度很高了。

T(n) = 2*T(n/2) + T(n-1) + C(C表示常量時間)

這個時間複雜度的公式推導很複雜,我直接給出結論: ,並不是一個多項式時間複雜度的演算法,也就是說,是一個無效的演算法。

4. 侏儒排序演算法(Stupid Sort)

Stupid排序演算法,起初由 Hamid Sarbazi-Azad 於 2000 年提出,後來被 Dick Grune 命名為 “Gnome排序” 。從名字上就可以看出,它也並不是一個很高明的排序演算法。這個演算法是如何工作的呢?我們先看它的虛擬碼實現,稍後再解釋。

procedure gnomeSort(a[]):
  pos := 0
  while pos < length(a):
   if (pos == 0 or a[pos] >= a[pos-1]):
     pos := pos + 1
   else:
     swap a[pos] and a[pos-1]
     pos := pos - 1
複製程式碼

這個演算法的思想非常貼合我們平時生活中整理東西的邏輯。假設我們站在pos這個下標位置上,0到pos-1這pos個資料已經是從小到大排好序的了。如果pos-1這個位置的資料小於等於pos,那我們前進一位(pos++);相反,如果pos-1這個位置的資料大於pos這個位置的資料,我們就將兩個數交換,並且後退一步(pos--),繼續跟已經排好序的資料比較。

實際上,從上面的工作原理的分析來看,這個演算法有點類似氣泡排序。而且,儘管名字叫Stupid算啊,實際上,效能並不是太差,最壞情況是時間複雜度是O(n^3)。

5. 臭皮匠排序演算法(Stooge Sort)

Stooge排序演算法,從實現上來看,有點類似於Stupid演算法。不過,它比Stupid演算法的效能稍強點,時間複雜度是O(nlog 3 / log 1.5 ) = O(n2.7095...)。

Stooge排序演算法是怎麼工作的呢?我們先來看下它的虛擬碼實現。

function stoogesort(array L, i = 0, j = length(L)-1) {
  if L[i] > L[j] then
    swap L[i], L[j]
  if (j - i + 1) > 2 then
    t = (j - i + 1) / 3
    stoogesort(L, i  , j-t)
    stoogesort(L, i+t, j)
    stoogesort(L, i  , j-t)
  return L
}
複製程式碼

從程式碼實現上來看,這個演算法非常規整、非常優美。我稍微解釋一下。

如果最後一個元素小於第一個元素,我們交換兩個數。如果當前集合元素數量大於等於3,那先遞迴排序前2/3的元素,再遞迴排序後2/3的元素,最後再遞迴排序一次前2/3的元素。

實現原理很巧妙哈,我們來看下,演算法結束之後,是否能產生排好序的陣列呢?

實際上,這個演算法的正確性證明很簡單。我們把整個陣列分成三段,每段佔1/3。前1/3段我們記作A,中間1/3段我們記作B,後面1/3段我們記作C。

經過第一輪排序之後,[AB]已經有序了,也就說,B中的資料肯定大於A中的資料。經過第二輪排序之後,[BC]就有序了,也就說C中資料肯定大於[AB]中的資料,也就是說,C中的資料肯定是這個陣列中最大的1/3資料了。

那這個時候,[AB]種的資料是否仍然有序呢?經過第二輪排序之後,[AB]中的資料變得無序了,所以我們需要再進行一輪排序,也就是程式碼中的最後一次排序。聽起來是不是有點類似漢諾塔演算法呢?

今天,我只是給你展示了這些奇葩的排序演算法,如果你對哪個感興趣,可以自己去更深入的研究下。除此之外,你還知道其他哪些腦洞大開的排序演算法呢?歡迎留言區說說。

關注我的微信公眾號:小爭哥,獲取更多、更新的技術、非技術分享。
作者:前Google工程師,5萬人訂閱《資料結構和演算法之美》專欄作者。
希望通過我加速你的技術、職場進步。

聊一聊那些腦洞大開、有趣又奇葩的排序演算法

相關文章