1. 程式人生 > >淺談演算法和資料結構: 二 基本排序演算法

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

本篇開始學習排序演算法。排序與我們日常生活中息息相關,比如,我們要從電話簿中找到某個聯絡人首先會按照姓氏排序、買火車票會按照出發時間或者時長排序、買東西會按照銷量或者好評度排序、查詢檔案會按照修改時間排序等等。在計算機程式設計中,排序和查詢也是最基本的演算法,很多其他的演算法都是以排序演算法為基礎,在一般的資料處理或分析中,通常第一步就是進行排序,比如說二分查詢,首先要對資料進行排序。在Donald Knuth 的計算機程式設計的藝術這四卷書中,有一卷是專門介紹排序和查詢的。

DonaldKnuth Volumn3

排序的演算法有很多,在維基百科上有這麼一個分類,另外大家有興趣也可以直接上維基百科上看相關演算法,本文也參考了上面的內容。

Sort algorithem in wikipedia

首先來看比較簡單的選擇排序(Selection sort),插入排序(Insertion sort),然後在分析插入排序的特徵和缺點的基礎上,介紹在插入排序基礎上改進的希爾排序(Shell sort)。

一 選擇排序

原理

選擇排序很簡單,他的步驟如下:

  1. 從左至右遍歷,找到最小(大)的元素,然後與第一個元素交換。
  2. 從剩餘未排序元素中繼續尋找最小(大)元素,然後與第二個元素進行交換。
  3. 以此類推,直到所有元素均排序完畢。

之所以稱之為選擇排序,是因為每一次遍歷未排序的序列我們總是從中選擇出最小的元素。下面是選擇排序的動畫演示:

Selection sort animation

實現:

演算法實現起來也很簡單,我們新建一個Sort泛型類,讓該型別必須實現IComparable介面,然後我們定義SelectionSort方法,方法傳入T陣列,程式碼如下:

/// <summary>
/// 排序演算法泛型類,要求型別實現IComparable介面
/// </summary>
/// <typeparam name="T"></typeparam>
public class Sort<T> where T : IComparable<T>
{
    /// <summary>
    /// 選擇排序
    /// </summary>
    /// <param name="array"></param>
    public static void 
SelectionSort(T[] array) { int n = array.Length; for (int i = 0; i < n; i++) { int min = i; //從第i+1個元素開始,找最小值 for (int j = i + 1; j < n; j++) { if (array[min].CompareTo(array[j]) > 0) min = j; } //找到之後和第i個元素交換 Swap(array, i, min); } } /// <summary> /// 元素交換 /// </summary> /// <param name="array"></param> /// <param name="i"></param> /// <param name="min"></param> private static void Swap(T[] array, int i, int min) { T temp = array[i]; array[i] = array[min]; array[min] = temp; } }

下圖分析了選擇排序中每一次排序的過程,您可以對照圖中右邊的柱狀圖來看。

Selection Sort Code Analysis C

測試如下:

static void Main(string[] args)
{
    Int32[] array = new Int32[] { 1, 3, 1, 4, 2, 4, 2, 3, 2, 4, 7, 6, 6, 7, 5, 5, 7, 7 };
    Console.WriteLine("Before SelectionSort:");
    PrintArray(array);
    Sort<Int32>.SelectionSort(array);
    Console.WriteLine("After SelectionSort:");
    PrintArray(array);
    Console.ReadKey();
}

輸出結果:

Output of selection sort

分析:

選擇排序的在各種初始條件下的排序效果如下:

selection sort

  1. 選擇排序需要花費 (N – 1) + (N – 2) + ... + 1 + 0 = N(N- 1) / 2 ~ N2/2次比較 和 N-1次交換操作。
  2. 對初始資料不敏感,不管初始的資料有沒有排好序,都需要經歷N2/2次比較,這對於一些原本排好序,或者近似排好序的序列來說並不具有優勢。在最好的情況下,即所有的排好序,需要0次交換,最差的情況,倒序,需要N-1次交換。
  3. 資料交換的次數較少,如果某個元素位於正確的最終位置上,則它不會被移動。在最差情況下也只需要進行N-1次資料交換,在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於比較好的一種。

二 插入排序

原理

插入排序也是一種比較直觀的排序方式。可以以我們平常打撲克牌為例來說明,假設我們那在手上的牌都是排好序的,那麼插入排序可以理解為我們每一次將摸到的牌,和手中的牌從左到右依次進行對比,如果找到合適的位置則直接插入。具體的步驟為:

  1. 從第一個元素開始,該元素可以認為已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素小於前面的元素(已排序),則依次與前面元素進行比較如果小於則交換,直到找到大於該元素的就則停止;
  4. 如果該元素大於前面的元素(已排序),則重複步驟2
  5. 重複步驟2~4 直到所有元素都排好序 。

下面是插入排序的動畫演示:

Insertionsortanimation

實現:

在Sort泛型方法中,我們新增如下方法,下面的方法和上面的定義一樣

/// <summary>
/// 插入排序
/// </summary>
/// <param name="array"></param>
public static void InsertionSort(T[] array)
{
    int n = array.Length;
    //從第二個元素開始
    for (int i = 1; i < n; i++)
    {
        //從第i個元素開始,一次和前面已經排好序的i-1個元素比較,如果小於,則交換
        for (int j = i; j > 0; j--)
        {
            if (array[j].CompareTo(array[j - 1]) < 0)
            {
                Swap(array, j, j - 1);
            }
            else//如果大於,則不用繼續往前比較了,因為前面的元素已經排好序,比較大的大就是教大的了。
                break;
        }
    }
}

Insertionsortanimationstep

測試如下:

Int32[] array1 = new Int32[] { 1, 3, 1, 4, 2, 4, 2, 3, 2, 4, 7, 6, 6, 7, 5, 5, 7, 7 };
Console.WriteLine("Before InsertionSort:");
PrintArray(array1);
Sort<Int32>.InsertionSort(array1);
Console.WriteLine("After InsertionSort:");
PrintArray(array1);
Console.ReadKey();

輸出結果:
Output of insertion sort

分析:

插入排序的在各種初始條件下的排序效果如下:

Insertion sort

1. 插入排序平均需要N2/4次比較和N2/4 次交換。在最壞的情況下需要N2/2 次比較和交換;在最好的情況下只需要N-1次比較和0次交換。

Worset case for insertion sort

先考慮最壞情況,那就是所有的元素逆序排列,那麼第i個元素需要與前面的i-1個元素進行i-1次比較和交換,所有的加起來大概等於N(N- 1) / 2 ~ N2 / 2,在陣列隨機排列的情況下,只需要和前面一半的元素進行比較和交換,所以平均需要N2/4次比較和N2/4 次交換。

Best case for Insertion Sort

在最好的情況下,所有元素都排好序,只需要從第二個元素開始都和前面的元素比較一次即可,不需要交換,所以為N-1次比較和0次交換。

2. 插入排序中,元素交換的次數等於序列中逆序元素的對數。元素比較的次數最少為元素逆序元素的對數,最多為元素逆序的對數 加上陣列的個數減1。

3.總體來說,插入排序對於部分有序序列以及元素個數比較小的序列是一種比較有效的方式。

Invention of Insertion Sort

如上圖中,序列AEELMOTRXPS,中逆序的對數為T-R,T-P,T-S,R-P,X-S 6對。典型的部分有序佇列的特徵有:

  • 陣列中每個元素離最終排好序後的位置不太遠
  • 小的未排序的陣列新增到大的已排好序的陣列後面
  • 陣列中只有個別元素未排好序

對於部分有序陣列,插入排序是比較有效的。當陣列中逆元素的對數越低,插入排序要比其他排序方法要高效的多。

選擇排序和插入排序的比較

Selection Sort VS Insertion Sort

上圖展示了插入排序和選擇排序的動畫效果。圖中灰色的柱子是不用動的,黑色的是需要參與到比較中的,紅色的是參與交換的。圖中可以看出:

插入排序不會動右邊的元素,選擇排序不會動左邊的元素;由於插入排序涉及到的未觸及的元素要比插入的元素要少,涉及到的比較操作平均要比選擇排序少一半

三 希爾排序(Shell Sort)

原理:

希爾排序也稱之為遞減增量排序,他是對插入排序的改進。在第二部插入排序中,我們知道,插入排序對於近似已排好序的序列來說,效率很高,可以達到線性排序的效率。但是插入排序效率也是比較低的,他一次只能將資料向前移一位。比如如果一個長度為N的序列,最小的元素如果恰巧在末尾,那麼使用插入排序仍需一步一步的向前移動和比較,要N-1次比較和交換。

希爾排序通過將待比較的元素劃分為幾個區域來提升插入排序的效率。這樣可以讓元素可以一次性的朝最終位置邁進一大步,然後演算法再取越來越小的步長進行排序,最後一步就是步長為1的普通的插入排序的,但是這個時候,整個序列已經是近似排好序的,所以效率高。

如下圖,我們對下面陣列進行排序的時候,首先以4為步長,這是元素分為了LMPT,EHSS,ELOX,AELR幾個序列,我們對這幾個獨立的序列進行插入排序,排序完成之後,我們減小步長繼續排序,最後直到步長為1,步長為1即為一般的插入排序,他保證了元素一定會被排序。

Shell Sort Partion

希爾排序的增量遞減演算法可以隨意指定,可以以N/2遞減,只要保證最後的步長為1即可。

實現:

/// <summary>
/// 希爾排序
/// </summary>
/// <param name="array"></param>
public static void ShellSort(T[] array)
{
    int n = array.Length;
    int h = 1;
    //初始最大步長
    while (h < n / 3) h = h * 3 + 1;
    while (h >= 1)
    {
        //從第二個元素開始
        for (int i = 1; i < n; i++)
        {
            //從第i個元素開始,依次次和前面已經排好序的i-h個元素比較,如果小於,則交換
            for (int j = i; j >= h; j = j - h)
            {
                if (array[j].CompareTo(array[j - h]) < 0)
                {
                    Swap(array, j, j - h);
                }
                else//如果大於,則不用繼續往前比較了,因為前面的元素已經排好序,比較大的大就是教大的了。
                    break;
            }
        }
        //步長除3遞減
        h = h / 3;
    }
}

可以看到,希爾排序的實現是在插入排序的基礎上改進的,插入排序的步長為1,每一次遞減1,希爾排序的步長為我們定義的h,然後每一次和前面的-h位置上的元素進行比較。演算法中,我們首先獲取小於N/3 的最大的步長,然後逐步長遞減至步長為1的一般的插入排序。

下面是希爾排序在各種情況下的排序動畫:

shell sort

分析:

1. 希爾排序的關鍵在於步長遞減序列的確定,任何遞減至1步長的序列都可以,目前已知的比較好的序列有

  • Shell's 序列: N/2 , N/4 , ..., 1 (重複除以2);
  • Hibbard's 序列: 1, 3, 7, ..., 2k - 1 ;
  • Knuth's 序列: 1, 4, 13, ..., (3k - 1) / 2 ;該序列是本文程式碼中使用的序列。
  • 已知最好的序列是 Sedgewick's (Knuth的學生,Algorithems的作者)的序列: 1, 5, 19, 41, 109, ....

          該序列由下面兩個表示式互動獲得:

  • 1, 19, 109, 505, 2161,….., 9(4k – 2k) + 1, k = 0, 1, 2, 3,…
  • 5, 41, 209, 929, 3905,…..2k+2 (2k+2 – 3 ) + 1, k = 0, 1, 2, 3, …

“比較在希爾排序中是最主要的操作,而不是交換。”用這樣步長的希爾排序比插入排序和堆排序都要快,甚至在小陣列中比快速排序還快,但是在涉及大量資料時希爾排序還是比快速排序慢。

2. 希爾排序的分析比較複雜,使用Hibbard’s 遞減步長序列的時間複雜度為O(N3/2),平均時間複雜度大約為O(N5/4) ,具體的複雜度目前仍存在爭議。

3. 實驗表明,對於中型的序列( 萬),希爾排序的時間複雜度接近最快的排序演算法的時間複雜度nlogn。

四 總結

最後總結一下本文介紹的三種排序演算法的最好最壞和平均時間複雜度。

名稱

最好

平均

最壞

記憶體佔用

穩定排序

插入排序

n

n2

n2

1

選擇排序

n2

n2

n2

1

希爾排序

n

nlog2n

n3/2

依賴於增量遞減序列目前最好的是 nlog2n

1

     希望本文對您瞭解以上三個基本的排序演算法有所幫助,後面將會介紹合併排序和快速排序。

相關推薦

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

演算法資料結構: 八 平衡查詢樹之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的時間複雜度。 那麼有沒有查詢效率更高的資料結構呢,答案就是本文接下來要介紹了

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

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

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

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

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

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

演算法資料結構:雜湊表

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

《常見演算法資料結構》優先佇列(2)——叉堆

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

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

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

演算法資料結構專場】BitMap演算法基本操作程式碼實現

上篇我們講了BitMap是如何對資料進行儲存的,沒看過的可以看一下【演算法與資料結構專場】BitMap演算法介紹 這篇我們來講一下BitMap這個資料結構的程式碼實現。 回顧下資料的儲存原理 一個二進位制位對應一個非負數n,如果n存在,則對應的二進位制位的值為1,否則為0。這個時候,我們的第一個問題:我們在

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

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