淺析數據結構與算法2--基本排序算法

本篇開始學習排序算法。排序與我們日常生活中息息相關,比如,我們要從電話簿中找到某個聯繫人首先會按照姓氏排序、買火車票會按照出發時間或者時長排序、買東西會按照銷量或者好評度排序、查找文件會按照修改時間排序等等。在計算機程序設計中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法爲基礎,在一般的數據處理或分析中,通常第一步就是進行排序,比如說二分查找,首先要對數據進行排序。在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

nlog2
或 
n3/2

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

1

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

發佈了92 篇原創文章 · 獲贊 17 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章