1. 程式人生 > >淺談演算法和資料結構: 三 合併排序

淺談演算法和資料結構: 三 合併排序

合併排序,顧名思義,就是通過將兩個有序的序列合併為一個大的有序的序列的方式來實現排序。合併排序是一種典型的分治演算法:首先將序列分為兩部分,然後對每一部分進行迴圈遞迴的排序,然後逐個將結果進行合併。

Definition of Merge Sort 

合併排序最大的優點是它的時間複雜度為O(nlgn),這個是我們之前的選擇排序和插入排序所達不到的。他還是一種穩定性排序,也就是相等的元素在序列中的相對位置在排序前後不會發生變化。他的唯一缺點是,需要利用額外的N的空間來進行排序。

一 原理

Merge_sort_animation2

合併排序依賴於合併操作,即將兩個已經排序的序列合併成一個序列,具體的過程如下:

  1. 申請空間,使其大小為兩個已經排序序列之和,然後將待排序陣列複製到該陣列中。
  2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
  3. 比較複製陣列中兩個指標所指向的元素,選擇相對小的元素放入到原始待排序陣列中,並移動指標到下一位置
  4. 重複步驟3直到某一指標達到序列尾
  5. 將另一序列剩下的所有元素直接複製到原始陣列末尾

該過程實現如下,註釋比較清楚:

private static void Merge(T[] array, int lo, int mid, int hi)
{
    int i = lo, j = mid + 1;
    //把元素拷貝到輔助陣列中
    for (int k = lo; k <= hi; k++)
    {
        aux[k] = array[k];
    }
    
//然後按照規則將資料從輔助陣列中拷貝回原始的array中 for (int k = lo; k <= hi; k++) { //如果左邊元素沒了, 直接將右邊的剩餘元素都合併到到原陣列中 if (i > mid) { array[k] = aux[j++]; }//如果右邊元素沒有了,直接將所有左邊剩餘元素都合併到原陣列中 else if (j > hi) { array[k] = aux[i++]; }
//如果左邊右邊小,則將左邊的元素拷貝到原陣列中 else if (aux[i].CompareTo(aux[j]) < 0) { array[k] = aux[i++]; } else { array[k] = aux[j++]; } } }

下圖是使用以上方法將EEGMR和ACERT這兩個有序序列合併為一個大的序列的過程演示:

Merge Step in Merge Sort

二 實現

合併排序有兩種實現,一種是至上而下(Top-Down)合併,一種是至下而上 (Bottom-Up)合併,兩者演算法思想差不多,這裡僅介紹至上而下的合併排序。

至上而下的合併是一種典型的分治演算法(Divide-and-Conquer),如果兩個序列已經排好序了,那麼採用合併演算法,將這兩個序列合併為一個大的序列也就是對大的序列進行了排序。

首先我們將待排序的元素均分為左右兩個序列,然後分別對其進去排序,然後對這個排好序的序列進行合併,程式碼如下:

public class MergeSort<T> where T : IComparable<T>
{
    private static T[] aux; // 用於排序的輔助陣列
    public static void Sort(T[] array)
    {
        aux = new T[array.Length]; // 僅分配一次
        Sort(array, 0, array.Length - 1);
    }
    private static void Sort(T[] array, int lo, int hi)
    {
        if (lo >= hi) return; //如果下標大於上標,則返回
        int mid = lo + (hi - lo) / 2;//平分陣列
        Sort(array, lo, mid);//迴圈對左側元素排序
        Sort(array, mid + 1, hi);//迴圈對右側元素排序
        Merge(array, lo, mid, hi);//對左右排好的序列進行合併
    }
    ...
}

以排序一個具有15個元素的陣列為例,其呼叫堆疊為:

Top-Down merge的呼叫堆疊

我們單獨將Merge步驟拿出來,可以看到合併的過程如下:

Trace of merge reuslt for top-down merge sort

三 圖示及動畫

如果以排序38,27,43,3,9,82,10為例,將合併排序畫出來的話,可以看到如下圖:

Merge_sort_algorithm_diagram

下圖是合併排序的視覺化效果圖:

Merge Sort Visualization

對6 5 3 1 8 7 24 進行合併排序的動畫效果如下:

Merge-sort-example

下圖演示了合併排序在不同的情況下的效率:

merge sort

四 分析

1. 合併排序的平均時間複雜度為O(nlgn)

證明:合併排序是目前我們遇到的第一個時間複雜度不為n2的時間複雜度為nlgn(這裡lgn代表log2n)的排序演算法,下面給出對合並排序的時間複雜度分析的證明:

假設D(N)為對整個序列進行合併排序所用的時間,那麼一個合併排序又可以二分為兩個D(N/2)進行排序,再加上與N相關的比較和計算中間數所用的時間。整個合併排序可以用如下遞迴式表示:

D(N)=2D(N/2)+N,N>1;

D(N)=0,N=1; (當N=1時,陣列只有1個元素,已排好序,時間為0)

因為在分治演算法中經常會用到遞迴式,所以在CLRS中有一章專門講解遞迴式的求解和證明,使用主定理(master theorem)可以直接求解出該遞迴式的值,後面我會簡單介紹。這裡簡單的列舉兩種證明該遞迴式時間複雜度為O(nlgn)的方法:

Prof1:處於方便性考慮,我們假設陣列N為2的整數冪,這樣根據遞迴式我們可以畫出一棵樹:

merge sort analysis

可以看到我們對陣列N進行MergeSort的時候,是逐級劃分的,這樣就形成了一個滿二叉樹,樹的每一及子節點都為N,樹的深度即為層數lgN+1,滿二叉樹的深度的計算可以查閱相關資料,上圖中最後一層子節點沒有畫出來。這樣,這棵樹有lgN+1層,每一層有N個節點,所以

                             D(N)=(lgN+1)N=NlgN+N=NlgN

Prof2:我們在為遞迴表示式求解的時候,還有一種常用的方法就是數學歸納法,

首先根據我們的遞迴表示式的初始值以及觀察,我們猜想D(N)=NlgN.

  1. 當N=1 時,D(1)=0,滿足初始條件。
  2. 為便於推導,假設N是2的整數次冪N=2k, 即D(2k)=2klg2k = k*2k
  3. 在N+1 的情況下D(N+1)=D(2k+1)=2k+1lg2k+1=(k+1) * 2k+1,所以假設成立,D(N)=NlgN.

2. 合併排序需要額外的長度為N的輔助空間來完成排序

如果對長度為N的序列進行排序需要<=clogN 的額外空間,認為就是就地排序(in place排序)也就是完成該排序操作需要較小的,固定數量的額外輔助記憶體空間。之前學習過的選擇排序,插入排序,希爾排序都是原地排序。

但是在合併排序中,我們要建立一個大小為N的輔助排序陣列來存放初始的陣列或者存放合併好的陣列,所以需要長度為N的額外輔助空間。當然也有前人已經將合併排序改造為了就地合併排序,但是演算法的實現變得比較複雜。

需要額外N的空間來輔助排序是合併排序的最大缺點,如果在記憶體比較關心的環境中可能需要採用其他演算法。

五 幾點改進

對合並排序進行一些改進可以提高合併排序的效率。

1. 當劃分到較小的子序列時,通常可以使用插入排序替代合併排序

對於較小的子序列(通常序列元素個數為7個左右),我們就可以採用插入排序直接進行排序而不用繼續遞迴了),演算法改造如下:

private const int CUTOFF = 7;//採用插入排序的閾值
private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //如果下標大於上標,則返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分陣列
    Sort(array, lo, mid);//迴圈對左側元素排序
    Sort(array, mid + 1, hi);//迴圈對右側元素排序
    Merge(array, lo, mid, hi);//對左右排好的序列進行合併
}

2. 如果已經排好序了就不用合併了

當已排好序的左側的序列的最大值<=右側序列的最小值的時候,表示整個序列已經排好序了。

Stop if already sorted

演算法改動如下:

private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //如果下標大於上標,則返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分陣列
    Sort(array, lo, mid);//迴圈對左側元素排序
    Sort(array, mid + 1, hi);//迴圈對右側元素排序
   if (array[mid].CompareTo(array[mid + 1]) <= 0) return;
    Merge(array, lo, mid, hi);//對左右排好的序列進行合併
}

3. 並行化

分治演算法通常比較容易進行並行化,在淺談併發與並行這篇文章中已經展示瞭如何對快速排序進行並行化(快速排序在下一篇文章中講解),合併排序一樣,因為我們均分的左右兩側的序列是獨立的,所以可以進行並行,值得注意的是,並行化也有一個閾值,當序列長度小於某個閾值的時候,停止並行化能夠提高效率,這些詳細的討論在淺談併發與並行這篇文章中有詳細的介紹了,這裡不再贅述。

六 用途

合併排序和快速排序一樣都是時間複雜度為nlgn的演算法,但是和快速排序相比,合併排序是一種穩定性排序,也就是說排序關鍵字相等的兩個元素在整個序列排序的前後,相對位置不會發生變化,這一特性使得合併排序是穩定性排序中效率最高的一個。在Java中對引用物件進行排序,Perl、C++、Python的穩定性排序的內部實現中,都是使用的合併排序。

七 結語

本文介紹了分治演算法中比較典型的一個合併排序演算法,這也是我們遇到的第一個時間複雜度為nlgn的排序演算法,並簡要對演算法的複雜度進行的分析,希望本文對您理解合併排序有所幫助,下文將介紹快速排序演算法。

相關推薦

演算法資料結構: 合併排序

合併排序,顧名思義,就是通過將兩個有序的序列合併為一個大的有序的序列的方式來實現排序。合併排序是一種典型的分治演算法:首先將序列分為兩部分,然後對每一部分進行迴圈遞迴的排序,然後逐個將結果進行合併。   合併排序最大的優點是它的時間複雜度為O(nlgn),這個是我們之前的選擇排序和插入排序所達不到的。他還

演算法資料結構: 四 快速排序

上篇文章介紹了時間複雜度為O(nlgn)的合併排序,本篇文章介紹時間複雜度同樣為O(nlgn)但是排序速度比合並排序更快的快速排序(Quick Sort)。 快速排序也是一種採用分治法解決問題的一個典型應用。在很多程式語言中,對陣列,列表進行的非穩定排序在內部實現中都使用的是快速排序。而且快速排序在

演算法資料結構: 二 基本排序演算法

本篇開始學習排序演算法。排序與我們日常生活中息息相關,比如,我們要從電話簿中找到某個聯絡人首先會按照姓氏排序、買火車票會按照出發時間或者時長排序、買東西會按照銷量或者好評度排序、查詢檔案會按照修改時間排序等等。在計算機程式設計中,排序和查詢也是最基本的演算法,很多其他的演算法都是以排序演算法為基礎,在一般的資

查詢演算法 演算法資料結構: 七 二叉查詢樹 演算法資料結構: 十一 雜湊表

閱讀目錄 1. 順序查詢 2. 二分查詢 3. 插值查詢 4. 斐波那契查詢 5. 樹表查詢 6. 分塊查詢 7. 雜湊查詢   查詢是在大量的資訊中尋找一個特定的資訊元素,在計算機應用中,查詢是常用的基本運算,例如編譯程式中符號表的查詢。本文

演算法資料結構: 五 優先順序佇列與堆排序

在很多應用中,我們通常需要按照優先順序情況對待處理物件進行處理,比如首先處理優先順序最高的物件,然後處理次高的物件。最簡單的一個例子就是,在手機上玩遊戲的時候,如果有來電,那麼系統應該優先處理打進來的電話。 在這種情況下,我們的資料結構應該提供兩個最基本的操作,一個是返回最高優先

演算法資料結構: 八 平衡查詢樹之2-3樹

前面介紹了二叉查詢樹(Binary Search Tree),他對於大多數情況下的查詢和插入在效率上來說是沒有問題的,但是他在最差的情況下效率比較低。本文及後面文章介紹的平衡查詢樹的資料結構能夠保證在最差的情況下也能達到lgN的效率,要實現這一目標我們需要保證樹在插入完成之後

演算法資料結構: 九 平衡查詢樹之紅黑樹

前面一篇文章介紹了2-3查詢樹,可以看到,2-3查詢樹能保證在插入元素之後能保持樹的平衡狀態,最壞情況下即所有的子節點都是2-node,樹的高度為lgN,從而保證了最壞情況下的時間複雜度。但是2-3樹實現起來比較複雜,本文介紹一種簡單實現2-3樹的資料結構,即紅黑樹(

演算法資料結構: 十 平衡查詢樹之B樹

前面講解了平衡查詢樹中的2-3樹以及其實現紅黑樹。2-3樹種,一個節點最多有2個key,而紅黑樹則使用染色的方式來標識這兩個key。 維基百科對B樹的定義為“在電腦科學中,B樹(B-tree)是一種樹狀資料結構,它能夠儲存資料、對其進行排序並允許以O(log n)的時間複雜度執行進行查詢、順序讀取、插入和刪

演算法資料結構: 十一 雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是他們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼有沒有查詢效率更高的資料結構呢,答案就是本文接下來要介紹了

演算法資料結構: 六 符號表及其基本實現

前面幾篇文章介紹了基本的排序演算法,排序通常是查詢的前奏操作。從本文開始介紹基本的查詢演算法。 在介紹查詢演算法,首先需要了解符號表這一抽象資料結構,本文首先介紹了什麼是符號表,以及這一抽象資料結構的的API,然後介紹了兩種簡單的符號表的實現方式。 一符號表 在開始介紹查詢演算法之前,我們需要定義一個名

演算法資料結構----無向圖相關演算法基礎

最近幾個專案用到了求所有最小哈密爾頓迴路,貪婪遍歷查詢等演算法,都是自己想或者查論文,雖然都是資料結構的基礎內容,但感覺比較零散,很糾結。 前幾天突然聽到“圖計算”這個名詞,覺得應該是找到組織了,因此轉載如下,後續會不斷轉載其他有用的文章。 以下內容轉載自:http:/

演算法資料結構: 一 棧佇列

最近晚上在家裡看Algorithems,4th Edition,我買的英文版,覺得這本書寫的比較淺顯易懂,而且“圖碼並茂”,趁著這次機會打算好好學習做做筆記,這樣也會印象深刻,這也是寫這一系列文章的原因。另外普林斯頓大學在Coursera 上也有這本書同步的公開課,還有另外一門演算法分析課,這門課程的作者也是

演算法資料結構(11):雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是它們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼

演算法資料結構(7):二叉查詢樹

前文介紹了符號表的兩種實現,無序連結串列和有序陣列,無序連結串列在插入的時候具有較高的靈活性,而有序陣列在查詢時具有較高的效率,本文介紹的二叉查詢樹(Binary Search Tree,BST)這一資料結構綜合了以上兩種資料結構的優點。 二叉查詢樹具有很高的靈活性

演算法資料結構:雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是它們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼有沒

《常見演算法資料結構》元素排序(1)——簡單排序(附動畫)

元素排序(1)——簡單排序 本系列文章主要介紹常用的演算法和資料結構的知識,記錄的是《Algorithms I/II》課程的內容,採用的是“演算法(第4版)”這本紅寶書作為學習教材的,語言是

拒絕調包俠,不需要高階演算法資料結構技巧

前言 大多數工科學生或者剛剛入門近年來比較火的“人工智慧”相關演算法的同學,在選擇語言的時候,都會選擇MATLAB、Python、R等等這些高階語言,對自己所學的演算法進行實現和除錯。這些高階語言中,包含了實現複雜演算法的基礎數學演算法、基本統計演算法、基礎資料結構的實現,比如均值(mean)、方差(std

java中各種演算法資料結構的使用場景

一。通用資料結構:陣列,連結串列,樹,雜湊表 通用資料結構通過關鍵字的值來儲存並查詢資料,如報表,合同,記錄,業績等資料。通用資料結構可以用速度的快慢來分類,陣列和連結串列是最慢的,樹相對較快,雜湊表是最快的。請注意,並不是最快的就一定是最好的,因為最快的結構的

演算法資料結構

資料結構 堆 長度為n的陣列構建成最小堆的時間複雜度 B、B+樹、紅黑樹 說一下B+樹和二叉搜尋樹的區別? 說一下二叉搜尋樹和AVL樹、紅黑樹之間的差別 說下紅黑樹原理,紅黑樹你看虛擬碼的時候他有兩坨比較一樣的有沒有注意過 哪些情況下用棧 知道雜湊嗎?二叉樹比

演算法資料結構單鏈表的逆轉

public void reverse(SinglyList<T> list) { Node<T> p = list.head.next, succ = null, front = null; while (p != null) {