【演算法】6 比較排序之外學習新的線性時間排序

nomasp發表於2015-06-11

回顧比較排序

相信閱讀過前面5篇博文的童鞋們已經發現了“在排序的最終結果中,各元素的次序依賴於它們之間的比較”。於是乎,這類排序演算法被統稱為”比較排序“。

比較排序是通過一個單一且抽象的比較運算(比如“小於等於”)讀取列表元素,而這個比較運算則決定了每兩個元素中哪一個應該先出現在最終的排序列表中。

宣告:下面通過在維基百科中找到的非常完美的圖示來介紹一系列比較排序。

插入排序

在該系列的【演算法】1中我們便介紹了這個基本的演算法,它的比較過程如下:

這裡寫圖片描述

以下是用插入排序對30個元素的陣列進行排序的動畫:

這裡寫圖片描述

選擇排序

選擇排序的比較過程如下:

這裡寫圖片描述

其動畫效果如下:

這裡寫圖片描述

歸併排序

前面多次寫到歸併排序,它的比較過程如下:

這裡寫圖片描述

歸併排序的動畫如下:

這裡寫圖片描述

堆排序

在該系列的【演算法】4中我們便介紹了快排,構建堆的過程如下:

這裡寫圖片描述

堆排序的動畫如下:

這裡寫圖片描述

快速排序

在該系列的【演算法】5中我們便介紹了快排,它的比較過程如下:

這裡寫圖片描述

快速排序的動畫如下:

這裡寫圖片描述

另外一些比較排序

以下這些排序同樣也是比較排序,但該系列中之前並未提到。

Intro sort

該演算法是一種混合排序演算法,開始於快速排序,當遞迴深度超過基於正在排序的元素數目的水平時便切換到堆排序。它包含了這兩種演算法優良的部分,它實際的效能相當於在典型資料集上的快速排序和在最壞情況下的堆排序。由於它使用了兩種比較排序,因而它也是一種比較排序。

氣泡排序

大家應該多少都聽過氣泡排序(也被稱為下沉排序),它是一個非常基本的排序演算法。反覆地比較相鄰的兩個元素並適當的互換它們,如果列表中已經沒有元素需要互換則表示該列表已經排好序了。(看到列表就想到半年前在學的Scheme,歡迎大家也去看看,我開了2個專欄來介紹它們)

上面的描述中已經體現了比較的過程,因而氣泡排序也是一個比較排序,較小的元素被稱為“泡(Bubble)”,它將“浮”到列表的頂端。

儘管這個演算法非常簡單,但大家應該也聽說了,它真的非常的慢。

氣泡排序的過程如下:

這裡寫圖片描述

氣泡排序的動畫演示:

這裡寫圖片描述

其最好情況、最壞情況的執行時間分別是:Θ(n)

\Theta(n)
Θ(n2)
\Theta(n^2)

奇偶排序

奇偶排序和氣泡排序有很多類似的特點,它通過比較在列表中所有的單雙號索引的相鄰元素,如果有一對是錯誤排序(也就是前者比後者大),那麼將它們交換,之後不斷的重複這一步驟,直到整個列表排好序。

而鑑於此,它的最好情況、最壞情況的執行時間均和氣泡排序相同:Θ(n)

\Theta(n)
Θ(n2)
\Theta(n^2)

奇偶排序的演示如下:

這裡寫圖片描述

下面是C++中奇偶排序的示例:

template <class T>
void OddEvenSort (T a[], int n)
{
    for (int i = 0 ; i < n ; i++)
    {
         if (i & 1) // 'i' is odd
         {
             for (int j = 2 ; j < n ; j += 2)
             {     
                  if (a[j] < a[j-1])
                      swap (a[j-1], a[j]) ;
             }
          }
          else
          {  
              for (int j = 1 ; j < n ; j += 2)
              {
                   if (a[j] < a[j-1])
                       swap (a[j-1], a[j]) ;
              } 
          }
    }
}

雙向氣泡排序

雙向氣泡排序也被稱為雞尾酒排序、雞尾酒調酒器排序、搖床排序、漣漪排序、洗牌排序、班車排序等。(再多再華麗麗的名字也難以彌補它的低效)

和氣泡排序的區別在於它是在兩個方向上遍歷列表進行排序,雖然如此但並不能提高漸近效能,和插入排序比起來也沒太多優勢。

它的最好情況、最壞情況的執行時間均和氣泡排序相同:Θ(n)

\Theta(n)
Θ(n2)
\Theta(n^2)

這裡寫圖片描述


排序演算法的下界

我們可以將排序操作進行得多塊?

這取決於計算模型,模型簡單來說就是那些你被允許的操作。

決策樹

決策樹(decision tree)是一棵完全二叉樹,它可以表示在給定輸入規模情況下,其一特定排序演算法對所有元素的比較操作。其中的控制、資料移動等其他操作都被忽略了。

這裡寫圖片描述

這是一棵作用於3個元素時的插入排序的決策樹。標記為i:j

i:j
的內部結點表示ai
a_i
aj
a_j
之間的比較。

由於它作用於3個元素,因此共有A33=6

A_3^3=6
種可能的排列。也正因此,它並不具有一般性。

而對序列<a1=7,a2=2,a3=5>

<a_1=7,a_2=2,a_3=5>
和序列<a1=5,a2=9,a3=6>
<a_1=5,a_2=9,a_3=6>
進行排序時所做的決策已經由灰色和黑色粗箭頭指出了。

這裡寫圖片描述

決策樹排序的下界

如果決策樹是針對n個元素排序,那麼它的高度至少是nlgn

nlgn

在最壞情況下,任何比較排序演算法都需要做Ω(nlgn)

\Omega(nlgn)
次比較。

因為輸入資料的Ann

A_n^n
種可能的排列都是葉結點,所以Annl
A_n^n\leq l
,由於在一棵高位h
h
的二叉樹中,葉結點的數目不多於2h
2^h
,所以有:

n!l2h

n!\leq l\leq 2^h

對兩邊取對數:

=> lg2hlgn!

lg^{2^h}\geq lg^{n!}

=> lg2h=hlg2lgn!

lg^{2^h}=hlg^2\geq lg^{n!}

又因為:

lg2<1

lg^2<1

所以:

nlgn!=Ω(nlgn)

n\geq lg^{n!}=\Omega(nlgn)

因為堆排序和歸併排序的執行時間上界均為O(nlgn)

O(nlgn)
,因此它們都是漸近最優的比較排序演算法。

線性時間排序

計數排序

計數排序(counting sort)的思路很簡單,就是確定比x小的數有多少個。加入有10個,那麼x就排在第11位。

嚴謹來講,在電腦科學中,計數排序是一個根據比較鍵值大小的排序演算法,它也是一個整數排序演算法。它通過比較物件的數值來操作,並通過這些計數來確定它們在即將輸出的序列中的位置。它的執行時間是線性的且取決於最大值和最小值之間的差異,當值的變化明顯大於數目時就不太適用了。而它也可以作為基排序的子程式。

COUNTING-SORT(A,B,k)
1   let C[0...k] be a new array
2   for i=0 to k
3       C[i]=o
4   for j=1 to A.length
5       C[A[j]]=C[A[j]]+1
6   // C[i] now contains the number of element equal to i.
7   for i=1 to k
8       C[i]=C[i]+C[i-1]
9   // C[i] now contains the number of element less than or equal to i.
10  for j=A.length downto 1
11      B[C[A[j]]]=A[j]
12      C[A[j]]=C[A[j]]-1

第2-3步,C陣列的元素被全部初始化為0,此時耗費Θ(k)

\Theta(k)
時間。

第4-5步,也許不太好想象,其實就是在C陣列中來計數A陣列中的數。比如說,A

A
陣列中元素”3”有4個,那麼C[3]=4
C[3]=4
。此時耗費Θ(n)
\Theta(n)
時間。

第7-8步,也是不太好想象的計算,也就是說如果C[0]=1

C[0]=1
C[1]=4
C[1]=4
,那麼計算後的C[0]
C[0]
不變,C[1]=5
C[1]=5
。此時耗費Θ(k)
\Theta(k)
時間。

第10-12步,把每個元素A[j]

A[j]
放到它在輸出陣列B
B
中的合適位置。比如此時的第一次迴圈,先找到A[8]
A[8]
,然後找到C[A[8]]
C[A[8]]
的值,此時C[A[8]]
C[A[8]]
的意義就在於A[8]
A[8]
應在B陣列中的位置。完成這一步後將C[A[8]]
C[A[8]]
的值減一,因為它只是一個計數器。這裡耗費的時間為Θ(n)
\Theta(n)

這裡寫圖片描述

k=O(n)

k=O(n)
時,計數排序的執行時間為Θ(n)
\Theta(n)

基數排序

基數排序(radix sort)是一個古老的演算法,它用於卡片排序機上。說來也巧,寫這篇部落格的前一天晚上還在書上看到這種機器,它有80列,每一列都有12個孔可以打。

它可以使用前面介紹的計數排序作為子程式,然而它並不是原址排序;相比之下,很多執行時間為Θ(nlgn)

\Theta(nlgn)
的比較排序卻是原址排序。因此當資料過大而記憶體不太夠時,使用它並不是一個明智的選擇。

這裡寫圖片描述

關鍵在於依次對從右往左每一列數進行排序,其他的列也相應移動。

桶排序

這倒是一個有趣的演算法了,它充分利用了連結串列的思想。

桶排序(bucket sort)在平均情況下的執行時間為O(n)

O(n)

計數排序假設n

n
個輸入元素中的每一個都在0和k之間,桶排序假設輸入資料是均勻分佈的,所以他們的速度都非常快。但並不能因為這些是假設就說它們不實用不準確,真正的意義在於你可以根據情況選擇合適的演算法。比如說,輸入的n個元素並不是均勻分佈的,但它們都在0到k之間,那麼就可以用計數排序。

說到桶,我想到的是裝滿葡萄酒的酒桶以及裝滿火藥的火藥桶。這裡是桶是指的演算法將[0,1)

[0,1)
區域劃分為了n
n
個相同大小的空間,它們被稱為桶。

既然有了這個劃分,那麼就要用到它們。假設輸入的是n個元素的陣列A,且對於所有的i都有0A[i]<1

0\leq A[i]< 1
。你也許會覺得怎麼可能輸入的陣列元素都湊巧滿足呢,當然不會這麼湊巧,但是你可以人為地改造它們呀。比如<10,37,31,87>
<10,37,31,87>
,你可以將它們都除以100,得到<0.10,0.37,0.31,0.87>
<0.10,0.37,0.31,0.87>

還需要一個臨時的陣列B[0…n-1]來儲存這些桶(也就是連結串列),而連結串列支援搜尋,刪除和插入。關於連結串列的部分後面的部落格中會有詳細介紹。

BUCKET-SORT(A)
1   n=A.length
2   let B[0...n-1] be a new array
3   for i=0 to n-1
4       make B[i] an empty list
5   for i=1 to n
6       insert A[i] into list B[小於等於nA[i]的最大整數]
7   for i=0 to n-1
8       sort list B[i] with insertion sort
9   concatenate the lists B[0],B[1],...B[n-1] together in order

這裡寫圖片描述

學習演算法一定要體會到這種演算法內每一步的改變,也要體會不同演算法之間的演化和進步。在後面的連結串列中,我會更加側重於思路以及演算法的進化。



感謝您的訪問,希望對您有所幫助。 歡迎大家關注、收藏以及評論。


為使本文得到斧正和提問,轉載請註明出處:
http://blog.csdn.net/nomasp


相關文章