1. 程式人生 > >C#算法系列(6)——歸併排序

C#算法系列(6)——歸併排序

       本文主要描述了歸併排序的兩種實現方式,遞迴方式和非遞迴方式,以及二者的優缺點比較。下面首先介紹一下歸併排序的原理。

一、理解歸併排序

       歸併排序的本質:通過兩兩合併排序再合併,最終獲得了一個有序的陣列。通過在紙上演示,就會發現它其實是一棵倒置的完全二叉樹。
       原理:假設初始序列含有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度為1,然後兩兩歸併,得到n/2個長度為2或1的有序子序列;再兩兩歸併,…,如此重複,直到得到一個長度為n的有序序列為止,這種排序方法稱為2路歸併排序。也是分治法的典型應用。下面來張圖,直觀的感受一下:

這裡寫圖片描述

二、遞迴方式實現

1.實現過程

       (1)拆分:先將待排序列拆分成單個元素。
       (2)合併:拆分之後,再按照有序的進行合併。
       具體程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 歸併排序
{
    class MergeSortTest
    {
        private static MergeSortTest _instance;
        public
static MergeSortTest Instance { get { if (_instance == null) { _instance = new MergeSortTest(); } return _instance; } } //遞迴實現 public void MergeSort(int[] datas) { //拆分時遞迴 呼叫與合併
MSort(datas,datas,0,datas.Length-1); } /// <summary> /// 將SR[s..t]歸併排序為TR1[s..t] /// </summary> /// <param name="SR">歸併前原始序列</param> /// <param name="TR1">每次歸併後的結果</param> /// <param name="s">SR歸併開始的索引</param> /// <param name="t">SR歸併結束的索引</param> private void MSort(int[] SR, int[] TR1, int s, int t) { int m; int[] TR2 = new int[SR.Length]; //用於每次遞迴臨時存放待排序的子序列 //遞迴終止條件 if (s == t) { TR1[s] = SR[s]; } else { m = (s + t) / 2 ; //遞迴拆分,並把子序列儲存在TR2中 MSort(SR,TR2, s,m);//左拆分 MSort(SR,TR2,m+1,t);//右拆分 //遞迴合併排序 //將TR2[s..m]和TR2[m+1..t]歸併到TR1[s..t] Merge(TR2,TR1,s,m,t); //TR2為有序的,TR1為空 } } /// <summary> /// 將有序的SR[i..m]和SR[m+1..n]歸併為有序的TR[i..n] /// </summary> /// <param name="SR">上述裡面的TR2</param> /// <param name="TR">儲存排序的結果</param> /// <param name="i">TR2序列的起始位置</param> /// <param name="m">TR2序列的中間位置</param> /// <param name="n">TR2序列的結束位置</param> private void Merge(int[] SR, int[] TR, int i, int m, int n) { int j, k, l; //i指向SR的[i..m],j指向SR的[m+1..n],k指向TR for (j = m + 1, k = i; i <= m && j <= n;k++) { if (SR[i] < SR[j]) { TR[k] = SR[i++]; } else { TR[k] = SR[j++]; } } if (i <= m) { //將剩餘的SR[i..m]複製到TR for (l = 0; l <= m - i; l++) { TR[k + l] = SR[i+l]; } } if (j <= n) { //將剩餘的SR[j..m]複製到TR for (l = 0; l <= n - j; l++) { TR[k + l] = SR[j+l]; } } } } }

2.遞迴方式的特點

       一趟歸併需要將SR[0]~SR[n]中相鄰的長度為h的有序序列進行兩兩歸併,並將結果放到TR1[0]~TR1[n]中,這需要將待排序列序列中的所有記錄掃描一遍,因此耗費O(n)時間,而由完全二叉樹的深度可知,這個歸併排序的需要進行log2(n)次,因此總的時間複雜度為O(nlogn),而且這是歸併排序演算法中最好、最壞、平均的時間效能。由於歸併排序的過程中需要與原始記錄序列同樣數量的儲存空間存放歸併結果以及遞迴時深度為log2(n)的棧空間,因此空間複雜度為O(n+logn)。由於是挨個比較,因此歸併排序是一種穩定的排序演算法,比較佔用記憶體,但效率高且穩定的演算法。

三、非遞迴方式實現

1.實現過程

       非遞迴方式就少了拆分過程,直接按照2,4,…,2n方式進行歸併,歸併的方式採用迴圈的來實現。具體實現方式如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 歸併排序
{
    class MergeSortTest
    {
        private static MergeSortTest _instance;
        public static MergeSortTest Instance {
            get {
                if (_instance == null)
                {
                    _instance = new MergeSortTest();
                }
                return _instance;
            }
        }

        //非遞迴實現
        public void MergeSort2(int[] datas)
        {
            int[] TR = new int[datas.Length];
            int k = 1; //歸併增量
            while (k < datas.Length)
            {
                //datas與TR 來回歸併
                MergePass(datas, TR, k, datas.Length - 1);
                k = 2 * k;
                MergePass(TR, datas, k, datas.Length - 1);
                k = 2 * k;
            }
        }

        //將SR[]中相鄰長度為s的子序列兩兩歸併到TR[],s歸併長度增量,n待排序列最後元素的下標
        public void MergePass(int[] SR, int[] TR, int s, int n)
        {
            int i = 0, j;
            //兩兩歸併
            while (i <= n - 2 * s + 1)
            {
                Merge(SR, TR, i, i + s - 1, i + 2 * s - 1);
                i = i + 2 * s;
            }
            //歸併最後兩個子序列
            if (i < n - s + 1)
            {
                Merge(SR, TR, i, i + s - 1, n);
            }
            //若最後只剩單個子序列
            else
            {
                for (j = i; j <= n; j++)
                {
                    TR[j] = SR[j];
                }
            }
        }

        /// <summary>
        /// 將有序的SR[i..m]和SR[m+1..n]歸併為有序的TR[i..n]
        /// </summary>
        /// <param name="SR">上述裡面的TR2</param>
        /// <param name="TR">儲存排序的結果</param>
        /// <param name="i">TR2序列的起始位置</param>
        /// <param name="m">TR2序列的中間位置</param>
        /// <param name="n">TR2序列的結束位置</param>
        private void Merge(int[] SR, int[] TR, int i, int m, int n)
        {
            int j, k, l; //i指向SR的[i..m],j指向SR的[m+1..n],k指向TR
            for (j = m + 1, k = i; i <= m && j <= n;k++)
            {
                if (SR[i] < SR[j])
                {
                    TR[k] = SR[i++];
                }
                else
                {
                    TR[k] = SR[j++];
                }
            }

            if (i <= m)
            {
                //將剩餘的SR[i..m]複製到TR
                for (l = 0; l <= m - i; l++)
                {
                    TR[k + l] = SR[i+l];
                }
            }

            if (j <= n)
            {
                //將剩餘的SR[j..m]複製到TR
                for (l = 0; l <= n - j; l++)
                {
                    TR[k + l] = SR[j+l];
                }
            }
        }
    }
}

2.非遞迴方式的特點

       非遞迴的迭代方法,避免了遞迴時深度為log2(n)的棧空間,空間只是用到申請歸併臨時用的TR陣列,因此空間複雜度為O(n),並且避免遞迴也在時間效能上有一定的提升,應該說,使用歸併排序時,儘量考慮用非遞迴方式

       若文中有不當或不正確的地方,歡迎指出。虛心求教,謝謝!