Java資料結構和演算法 - 進階排序

來源:互聯網
上載者:User

標籤:應用   href   重複   使用   info   先後   電腦   檢測   應該   

希爾排序Q: 什麼是希爾排序?

A: 希爾排序因電腦科學家Donald L.Shell而得名,他在1959年發現了希爾排序演算法。

A: 希爾排序基於插入排序,但是增加了一個新的特性,大大地提高了插入排序的執行效率。

Q: 回憶之前的插入排序,有哪些缺點?

A: 回憶之前的簡單排序的“插入排序”一節,在插入排序執行一半的時候,標記位i左邊這部分資料項目都是排過序的,而標記位右邊的資料項目則沒有排過序。這個演算法取出標記位所指的資料項目,把它儲存在一個臨時變數裡,接著,從剛剛被移除的資料項目的左邊第一個元素開始,每次把有序的資料項目向右移動一個元素,直到儲存在臨時變數裡的資料項目能夠有序回插。

A: 假設一個很小的元素在很靠近右端的位置,要把這個很小的元素移動到在左邊的正確位置上,所有的中間元素都必須向右移動一位。這個步驟對每一個元素都執行了近N次的複製,雖不是所有的元素都必須移動N個位置,但是資料項目平均移動了N/2個位置,就相當於執行了N次N/2個移位,總共是N2/2次複製,因此插入排序的執行效率是O(N2)。

A: 如果能以某種方式不必一個一個地移動所有中間的資料項目,就能把較小的資料項目移動到左邊,那麼這個演算法的執行效率就會有很大的改進。

Q: 希爾排序的原理是什嗎?

A: 希爾排序通過加大插入排序中元素之間的間隔,並在這些有間隔的元素中進行插入排序,從而使資料項目能大跨度地移動。當這些資料項目排過一趟序後,希爾排序演算法減少資料項目的間隔再進行排序,依次進行下去。

A: 進行這些排序時資料項目之前的間隔被稱為增量,並且習慣上用字母h表示。 
顯示了增量為4時對包含10個資料項目的數組進行排序的第一個步驟情況,在0、4和8號的位置上的資料項目已經有序了。 
 
當對0、4和8號資料項目完成排序之後,演算法向右遊一步,對1、5和9號資料項目進行排序,這個排序過程持續進行,直到所有的資料項目已經完成了增量為4的排序。這個過程如所示: 

在完成增量為4的希爾排序之後,數組可以看成是有4個子數組組成:(0, 4, 8), (1, 5, 9), (2, 6), (3, 7)。這4個子數組分別完全有序,這些子數組相互交錯排列,然而彼此獨立。

A: 上面圖解了以4為增量對包含10個資料項目的數組進行排序的情況。對於更大的數組,開始的間隔也應該更大,然後間隔不斷減小,直到間隔變成1。接下來就是對於任意大小的數組,如何選擇間隔呢?

Q: 如何選擇間隔呢?

A: 舉例來說,含有1000個資料項目的數組可能先以364為增量,然後以121為增量,然後以40為增量,接著以13為增量,再接著以4為增量,最有以1為增量進行希爾排序。用來形成間隔的數列(121,40,13,4,1)被稱為間隔序列。這裡所表示的間隔序列由Knuth提出。

A: 數列以逆向的形式從1開始,通過遞迴運算式h = 3 * h + 1來產生,初始值為1。下表的前兩欄顯示了這個公式的序列。 

A: 在排序演算法中,首先在一個短小的迴圈中使用序列的產生公式來計算出最初的間隔。h值最初被賦值為1,然後用公式h = 3 * h + 1產生序列1,4,13,40,121,364等等。當間隔大於數組大小的時候這個過程停止。

A: 對於一個含有1000個資料項目的數組,序列的第七個數字1093太大了。因此使用序列的第6個數字364作為最大的數字來開始這個排序過程,作增量為364的排序。然後,每完成一次排序全程的外部迴圈,用前面提供的此公式反向推算式來減小間隔: h = (h - 1) / 3。這個反向推算的公式產生逆置的序列364,121,40,13,4,1。從364開始,以每一個數字作為增量進行排序。當數組用增量為1排序後,演算法結束。

A: 樣本:ShellSort.java

Q: 有沒有其他間隔序列?

A: 選擇間隔序列可以稱得上是一種魔法,除了h = h * 3 + 1產生間隔序列外,還有其他間隔序列。這些間隔只有一個絕對條件,就是逐漸減小的間隔最後一定要等於1,因此最後一趟排序是一次普通的插入排序。

A: 在最開始的時候,希爾排序初始的間隔為N/2,簡單地把每一趟排序分成兩半,因此對於大小為100的數組逐漸減小的間隔序列為50,25,12,6,3,1。這個方法的好處是不需要開始排序前為找到初始的間隔而計算序列,而只需用2整除N。但是這種證明並不是最好的數列。儘管對於大多數的資料來說這個方法還是比插入排序效果好,但是這種方法有時會使已耗用時間降到O(2)。

A: Flaming間隔的代碼如下:

if (h < 5) {    h = 1;} else {    h = (5 * h - 1) / 11;}  

這個方法是用2.2而非2來整除每一個間隔。對於n=100的數組,會產生序列45,20,9,4,1。這比用2整除好多了,因為這樣避免了某些導致時間複雜度O(N2)的最壞情況發生。

A: 間隔序列的數字互質通常被認為很重要,也就是說除了1之外它們沒有公約數,這個約束條件使每一趟排序更有可能保持前一趟排序已排好的效果。而以N/2為間隔的低效性就是歸咎於它沒有遵循這個準則。

A: 或許還可以設計出像上面講述的間隔序列一樣好甚至更好的序列。但是不管怎麼樣,都應該能夠快速地計算,而不會降低演算法的執行速度。

Q: 希爾排序的效率如何?

A: 迄今為止,除了在一些特殊的情況下,還沒有人能夠從理論上分析希爾排序的效率。有各種各樣基於實驗的評估,估計它的時間級是從O(N3/2)到O(N7/6) 。

A: 下表對比了速度較慢的插入排序和速度較快的快速排序,中間還列出了希爾排序的一些估計的大O值。注意Nx/y表示N的x方的y次方根(N等於100,N3/2就是1003的平方根,結果是1000)。另外(logN)2表示N對數的平方,通常協作log2N。 

劃分Q: 什麼是劃分演算法?

A: 劃分(partitioning)是後面討論的快速排序的根本基礎,因此把它作為單獨的一節來講解。

A: 劃分資料就是把資料分為兩組,使所有關鍵字大於特定值的資料項目在一組,使所有關鍵字小於特定值的資料項目在另一組。

A: 劃分演算法:當leftPointer遇到比樞紐小的資料項目時,它繼續右移,因為這個資料項目的位置已經處在數組的正確一邊了。但是,當遇到比樞紐大的資料項目時,它就停下來。同理rightPointer。兩個內層的while迴圈,第一個應用於leftPointer,第二個應用於rightPointer,控制這個掃描過程,因為指標退出了while迴圈,所以它停止移動。下面是一段掃描不在適當位置上的資料項目的簡化代碼:

    while (leftPointer < right && mLArray[++leftPointer] < pivot) {}    while (rightPointer > left && mLArray[--rightPointer] > pivot) {}    swap(leftPointer, rightPointer);

當這兩個迴圈都退出之後,leftPointer和rightPointer都指著在數組的錯誤一方位置上的資料項目,所以交換這兩個資料項目。交換之後,繼續移動這兩個資料項目。當兩個指標最終相遇的時候,劃分過程結束,並且退出外層while迴圈。

樣本: ArrayPartition.java

A: 劃分演算法的已耗用時間為O(N)。

快速排序Q: 什麼是快速排序 ?

A: 毫無疑問,快速排序是最流行的排序演算法,因為有充足的理由,在大多數情況下,快速排序都是最快的,執行時間為O(N * logN)級。快速排序是在1962年由C.A.RHoare發現的。

A: 有了前面劃分演算法的介紹,再來理解快速排序就很容易了。快速排序演算法本質上通過把一個數組劃分為兩個子數組,然後遞迴地調用自身為每一個子數組進行快速排序。

A: 基本的遞迴的快速排序演算法代碼很簡單,下面是一個樣本:

public void recQuickSort(int left, int right) {    if (right - left <= 0) {        // if size is 1, it‘s already sorted        return;    } else {        // size is 2 or larger        // partition range        int partitionIndex = partitioning(left, right);        // sort left side        recQuickSort(left, partitionIndex - 1);        // sort right side        recQuickSort(partitionIndex + 1, right);    }}                        

有三個基本的步驟: 
1) 把數組或者子數組劃分左邊和右邊; 
2) 調用自身對左邊的進行排序; 
3) 調用自身對右邊的進行排序。 
經過一次劃分之後,所有在左邊子數組的資料項目都小於在右邊子數組的。 
只要對左邊子數組和右邊子數組分別進行排序,整個數組就是有序的了。

A: 如何對子數組進行排序呢?通過遞迴來實現。這個方法首先檢查數組是否只包含一個資料項目,如果數組只包含一個,那麼數組就已經有序,方法立即返回,這個就是遞迴過程中的基值條件。 
如果數組包含兩個或者更多的資料項目,演算法就調用前面講過的partitioning()方法對這個數組進行劃分。方法返回分割邊界的下標index。劃分pivot給出兩個子數組的分界,如所示。 
 
對數組進行劃分之後,recQuickSort()遞迴地調用自身,數組左邊的部分調用一次(從left到partitionIndex - 1位置上的資料項目進行排序),數組右邊的部分也調用一次(從partitionIndex + 1到right位置上的資料項目進行排序)。注意這兩個遞迴調用都不包含數組下標partitionIndex的資料項目。為什麼不包含這個資料項目呢?難道下標為partitionIndex的資料項目不需要排序?

Q: 劃分應該選擇什麼樣的樞紐(pivot)?

A: 那麼partitioning()方法如何選擇樞紐呢?以下是一些相關思想: 
1) 應該選擇具體的一個資料項目的關鍵字的值作為樞紐:成這個資料項目為pivot(樞紐); 
2) 可以選擇任意一個資料項目作為樞紐。為了簡便,我們假設總是選擇待劃分的子數組最右端的資料項目作為pivot; 
3) 劃分完成之後,如果樞紐被插入到左右子數組之間的分界處,那麼樞紐就落在排序之後的最終位置上了。

顯示了用關鍵字為36的項作為樞紐的情況。因為不能真正像圖中顯示的那樣把一個數組分開,所以這個圖只是一個想象的情況。那麼怎樣才能把樞紐移動到它正確的位置上來呢? 
 
可以把右邊子數組的所有資料項目都像右移動一位,以騰出樞紐的位置。但是,這樣做即低效又不必要。記住儘管右邊子數組的所有資料項目都大於樞紐,但它們都還沒有排序,所以它們可以在右邊子數組內部移動,而沒有任何影響。因此,為了簡化把樞紐插入正確位置的操作,只要交換樞紐和右邊子數組的最左邊的資料項目(目前是63)即可。 
這個交換操作把樞紐放在了正確的位置上,也就是左右子數組之間。63跳到了最右邊,如所示: 
 
當樞紐被換到分界的位置時,它落在它最後應該在的位置上。以後所有的操作或者發生在左邊或者右邊,樞紐本身不會再移動了。

樣本: QuickSort.java

Q: 為什麼效能會降到O(n2)?

A: 如果資料是逆序的,然後採用上面的程式進行排序,就會發現演算法運行得相當緩慢。

A: 問題出在樞紐的選擇上,理想狀態下,應該選擇被排序的資料項目的中值資料項目作為樞紐。也就是說,應該由一半的資料項目大於樞紐,一半的資料項目小於樞紐。對快速排序演算法來說擁有兩個大小相等的子數組是最優的情況。如果快速排序演算法必須要對劃分的一大一小兩個子數組排序,那麼將會降低演算法的效率,這是因為較大的子數組必須要被劃分更多次。

A: N個資料項目數組的最壞的劃分是一個子數組只有一個資料項目,另一個子數組含有N-1個資料項目。

A: 在這種情況下,劃分所帶來的好處就沒有了,演算法的執行效率降低到O(N2)。除了慢,還有另外一個潛在的問題,當劃分的次數增加時,遞迴方法的調用次數也增加,每一個方法調用都要增加所需遞迴工作棧的大小。如果調用次數太多,遞迴工作棧可能會發生溢出,從而使系統癱瘓。那麼能否改進選擇樞紐的方法呢?

Q: 什麼是"三資料項目取中" 劃分?

A: 方法應該簡單但能避免出現選擇最大或者最小資料項目作為樞紐的情況。可以檢測所有的資料項目,並且實際計算哪一個資料項目是中值資料項目,這應該是理想的樞紐,可是由於這個過程需要比排序本身更長的時間,因此它不可行。

A: 折衷的解決方案是找到數組的第一個、最後和中間元素的中間值,並將其用於樞紐。這個方案被稱為“三資料項目取中”,如: 
 
尋找三個資料項目的中值資料項目自然比尋找所有資料項目的中值資料項目快得多,同時這也有效地避免了在資料已經有序或者逆序的情況下,選擇最大的或者最小的資料項目作為樞紐的機會。

A: 當然很可能存在一些很特殊的資料排列使得三資料項目取中的執行效率很低,但是通常情況下,對於選擇樞紐它都是一個又快又有效好方法。

A: 因為在選擇的過程中使用三資料項目取中的方法不僅選擇了樞紐,而且還對三個資料項目進行了排序。這時就可以保證子數組最左端的資料項目小於樞紐,最右端的資料項目大於樞紐,這就意味著即便取消了leftPointer > rightrightPointer < left的檢測,leftPointer和rightPointer也不會分別越過數組。如:

A: 三資料項目取中的另一個好處是,對左端、中間以及右端的資料項目排序之後,劃分過程就不需要再考慮這三個資料項目了。劃分可以從left + 1和right - 1開始,因為left和right已經被有效地劃分了。

A: 樣本:QuickSort.java

Q: 對小劃分使用插入排序?

A: 如果使用三資料項目取中劃分的方法,則必須要遵循快速排序不能執行三個或者少於三個資料項目的劃分規則,在這種情況下,數字3則被稱為切割點(cutoff)。在上面的樣本中,是用一段代碼手動地對兩個或者三個資料項目的子數組進行排序。那麼這個是最好的方法嗎?

A: 處理小劃分的另一個選擇是使用插入排序。當使用插入排序的時候,不用限制以3為切割點。可以把界限定為10、20或者其他任何數。Knuth推薦使用9作為切割點。但是最好的選擇值取決於電腦、作業系統、編譯器(或者解譯器)等。

A: 樣本:QuickSort.java

Q: 快速排序之後使用插入排序?

A: 另一個選擇是對數組整個使用快速排序。當快排結束時,數組已經是基本有序了,然後可以對整個數組應用插入排序。插入排序對基本有序的數組執行效率很高,而且很多專家都提倡使用這個方法。

A: 樣本:QuickSort.java

Q: 消除遞迴?

A: 很多人提倡對快速排序的演算法採用循壞代替遞迴來執行子數組的劃分,這個思想源於早起的編譯器以及電腦體繫結構,對於每一次方法調用那種舊的系統都會導致大量的時間消耗。對於現在的系統來說,消除遞迴所帶來的改進不是很明顯,因為現在的系統可以更為有效地處理方法調用。

Q: 快速排序的效率?

A: 快速排序的時間複雜度為O(N*logN)。對於分治演算法總體都是這樣的,遞迴的方法把一列資料項目分為2組,然後調用自身來分別處理每一組資料項目。這種情況下,演算法實際以2為底,已耗用時間和N*log2N成正比。

基數排序Q: 什麼是基數排序?

A: 基數排序(Radix Sort)也稱為桶排序,是一種當關鍵字為整數類型時非常高效的排序方法。

Q: 基數排序的基本思想?

A: 設待排序的資料元素的關鍵字是m位d進位整數(不足m位的關鍵字在高位上補0),設定d個桶,令其編號為0,1,2,3,...,d-1。

A: 首先,按關鍵字最低位的數值依次把各資料元素放在對應的桶中。然後,按照桶號從小到大和進入桶中的先後次序收集分配在個桶中的資料元素,這樣就形成了資料元素集合的一個新的排列。稱這樣的依次排序過程為一次基數排序。

A: 再對一次基數排序所得到的資料元素序列按關鍵字次低位的數值依次把各資料元素放到對應的桶中,然後按照桶號從小到大和進入桶中資料元素的先後次序收集分配在各桶中的資料元素。

A: 這樣的過程重複進行,當完成了第m次基數排序後,就可以得到了排好序的資料元素的序列。

A: 下面是一個例子,有7個資料項目{421, 240, 035, 532, 305, 430, 124},每個資料項目都有三位。 

Q: 基數排序的實現?

A: 分析基數排序演算法,因為要求進出桶中的資料元素序列滿足FIFO原則,因此這裡所說的桶實際就是隊列。隊列有順序隊列和鏈式隊列,因此在實現中就有這兩種方式。

A: 考慮到個位,十位,百位…每一位元值的個數不可能完全相同,因此很難確定隊列的大小,因此採用鏈式隊列最好,因為它可以任意擴充。請參閱:用鏈表實現的隊列

A: 基於鏈式隊列基數排序演算法的儲存結構: 
 
A: 一個十進位關鍵字K的第i位元值Ki的計算公式: 
 
其中,int()函數為取整函數,如int(3.5) = 3。 
設k = 6321, K1, K2, K3, K4的計算結果如下: 
K1 = int(6321 / 100) - 10 * (int(6321 / 101)) = 6321 - 6320 = 1; 
K2 = int(6321 / 101) - 10 * (int(6321 / 102)) = 632 - 630 = 2; 
K3 = int(6321 / 102) - 10 * (int(6321 / 103)) = 63 - 60 = 3; 
K4 = int(6321 / 103) - 10 * (int(6321 / 104)) = 6 - 0 = 6;

A: 樣本:RadixSort.java

Q: 基數排序的效率?

A: 所有要做的只是把原始的資料項目從數組拷貝到鏈表,然後再拷貝回來。如果有10個資料項目,則有20次拷貝。拷貝的次數和資料項目的個數成正比,即O(N)。

A: 對每一位重複一次這個過程,假設對5位的數字排序,就需要20*5次拷貝。位元我們設為M。因此基於鏈式隊列的基數排序演算法的時間複雜度為O(MN)。

A: 儘管從數字中提取出每一位需要花費時間,但是沒有比較。現代電腦中位提取操作要快於比較操作。

Java資料結構和演算法 - 進階排序

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.