8種主要排序算法的C#實現 (二)


 

歸併排序

歸併排序也是採用“分而治之”的方式。剛發現分治法是一種算法範式,我還一直以爲是一種需要意會的思想呢。

不好意思了,孤陋寡聞了,哈哈!

原理:將兩個有序的數列,通過比較,合併爲一個有序數列。 維基入口

爲方便理解,此處實現用了List<int>的一些方法,隨後有IList<int>版本。

實現如下:

public static List<int> MergeSortOnlyList(List<int> data, int low, int high)
        {
            if (low == high)
                return new List<int> { data[low] };
            List<int> mergeData = new List<int>();
            int mid = (low + high) / 2;
            List<int> leftData = MergeSortOnlyList(data, low, mid);
            List<int> rightData = MergeSortOnlyList(data, mid + 1, high);
            int i = 0, j = 0;
            while (true)
            {
                if (leftData[i] < rightData[j])
                {
                    mergeData.Add(leftData[i]);
                    if (++i == leftData.Count)
                    {
                        mergeData.AddRange(rightData.GetRange(j, rightData.Count - j));
                        break;
                    }
                }
                else
                {
                    mergeData.Add(rightData[j]);
                    if (++j == rightData.Count)
                    {
                        mergeData.AddRange(leftData.GetRange(i, leftData.Count - i));
                        break;
                    }
                }
            }
            return mergeData;
        }

        public static List<int> MergeSortOnlyList(List<int> data)
        {
            data = MergeSortOnlyList(data, 0, data.Count - 1);  //不會改變外部引用 參照C#參數傳遞
            return data;
        }

過程解析:將數列分爲兩部分,分別得到兩部分數列的有序版本,然後逐個比較,將比較出的小數逐個放進

新的空數列中。當一個數列放完後,將另一個數列剩餘數全部放進去。

IList<int>版本

實現如下:

public static IList<int> MergeSort(IList<int> data)
        {
            data = MergeSort(data, 0, data.Count - 1);
            return data;
        }

        public static IList<int> MergeSort(IList<int> data, int low, int high)
        {
            int length = high - low + 1;
            IList<int> mergeData = NewInstance(data, length);
            if (low == high)
            {
                mergeData[0] = data[low];
                return mergeData;
            }
            int mid = (low + high) / 2;
            IList<int> leftData = MergeSort(data, low, mid);
            IList<int> rightData = MergeSort(data, mid + 1, high);
            int i = 0, j = 0;
            while (true)
            {
                if (leftData[i] < rightData[j])
                {
                    mergeData[i + j] = leftData[i++]; //不能使用Add,Array Length不可變
                    if (i == leftData.Count)
                    {
                        int rightLeft = rightData.Count - j;
                        for (int m = 0; m < rightLeft; m++)
                        {
                            mergeData[i + j] = rightData[j++];
                        }
                        break;
                    }
                }
                else
                {
                    mergeData[i + j] = rightData[j++];
                    if (j == rightData.Count)
                    {
                        int leftleft = leftData.Count - i;
                        for (int n = 0; n < leftleft; n++)
                        {
                            mergeData[i + j] = leftData[i++];
                        }
                        break;
                    }
                }
            }
            return mergeData;

        }

過程原理與上個一樣,此處就不贅述了。

堆排序

堆排序是根據堆這種數據結構設計的一種算法。堆的特性:父節點的值總是小於(或大於)它的子節點。近似二叉樹。

原理:將數列構建爲最大堆數列(即父節點總是最大值),將最大值(即根節點)交換到數列末尾。這樣要排序的數列數總和減少,

同時根節點不再是最大值,調整最大堆數列。如此重複,最後得到有序數列。 維基入口   有趣的演示

實現準備:如何將數列構造爲堆——父節點i的左子節點爲2i+1,右子節點爲2i+2。節點i的父節點爲floor((i-1)/2)。

實現如下(這個實現判斷和臨時變量使用太多,導致效率低,評論中@小城故事提出了更好的實現):

public static void HeapSort(IList<int> data)
        {
            BuildMaxHeapify(data);
            int j = data.Count;
            for (int i = 0; i < j; )
            {
                Swap(data, i, --j);
                if (j - 2 < 0)  //只剩下1個數 j代表餘下要排列的數的個數
                    break;
                int k = 0;
                while (true)
                {
                    if (k > (j - 2) / 2) break;  //即:k > ((j-1)-1)/2 超出最後一個父節點的位置  
                    else
                    {
                        int temp = k;
                        k = ReSortMaxBranch(data, k, 2 * k + 1, 2 * k + 2, j - 1);
                        if (temp == k) break;
                    }
                }
            }
        }

        public static void BuildMaxHeapify(IList<int> data)
        {
            for (int i = data.Count / 2 - 1; i >= 0; i--)  //(data.Count-1)-1)/2爲數列最大父節點索引
            {
                int temp = i;
                temp = ReSortMaxBranch(data, i, 2 * i + 1, 2 * i + 2, data.Count - 1);
                if (temp != i)
                {
                    int k = i;
                    while (k != temp && temp <= data.Count / 2 - 1)
                    {
                        k = temp;
                        temp = ReSortMaxBranch(data, temp, 2 * temp + 1, 2 * temp + 2, data.Count - 1);
                    }
                }
            }
        }

        public static int ReSortMaxBranch(IList<int> data, int maxIndex, int left, int right, int lastIndex)
        {
            int temp;
            if (right > lastIndex)  //父節點只有一個子節點
                temp = left;
            else
            {
                if (data[left] > data[right])
                    temp = left;
                else temp = right;
            }

            if (data[maxIndex] < data[temp])
                Swap(data, maxIndex, temp);
            else temp = maxIndex;
            return temp;
        }

過程解析:BuildMaxHeapify爲排序前構建的最大堆數列方法,主要內容爲從最後一個父節點開始往前將每個三角組合

(即父節點與它的兩個子節點)符合父節點值最大的規則。ReSortMaxBranch爲將三角調整爲父節點值最大,

並返回該值之前的索引,用來判斷是否進行了交換,以及原來的父節點值交換到了什麼位置。在HeapSort裏首先

構建了最大堆數列,然後將根節點交換到末尾,根節點不是最大值了,在while語句中對最大堆數列進行調整。

插曲:自從看了Martin Fowler大師《重構》第三版,我發現我更不喜歡寫註釋了。每次都想着儘量讓方法的名字更貼切,

即使會造成方法的名字很長很醜。這算不算曲解了大師的意思啊!?上面的代碼註釋都是寫博客的時候現加的(源代碼很乾淨的。汗!)。

希爾排序

希爾排序是插入排序的一種更高效的改進版本。

在前面介紹的插入排序,我們知道1.它對有序數列排序的效率是非常高的 2.要排序的數向前移動是一步步進行的導致插入排序效率低。

希爾排序正是利用第一點,改善第二點,達到更理想的效果。

原理:通過奇妙的步長,插入排序間隔步長的元素,隨後逐漸縮短步長至1,實現數列的插入排序。 維基入口

疑問:可以想象到排序間隔步長的數,會逐漸讓數列變得有序,提升最後步長爲1時標準插入排序的效率。在維基上看到這麼

一句話“可能希爾排序最重要的地方在於當用較小步長排序後,以前用的較大步長仍然是有序的”注意用詞是‘可能’。我的疑問是

這是個正確的命題嗎?如何證明呢?看維基上也是由果推因,說是如果不是這樣,就不會排序那麼快了。可這我感覺還是太牽強了,

哪位大哥發現相關資料,希望能分享出來,不勝感激。

實現如下:

public static void ShellSortCorrect(IList<int> data)
        {
            int temp;
            for (int gap = data.Count / 2; gap > 0; gap /= 2)
            {
                for (int i = gap; i < data.Count; i++)      // i+ = gap 改爲了 i++
                {
                    temp = data[i];
                    for (int j = i - gap; j >= 0; j -= gap)
                    {
                        if (data[j] > temp)
                        {
                            data[j + gap] = data[j];
                            if (j == 0)
                            {
                                data[j] = temp;
                                break;
                            }
                        }
                        else
                        {
                            data[j + gap] = temp;
                            break;
                        }
                    }
                }
            }
        }

基數排序

基數排序是一種非比較型整數排序。

“非比較型”是什麼意思呢?因爲它內部使用的是桶排序,而桶排序是非比較型排序。

這裏就要說說桶排序了。一個非常有意思的排序。

桶排序

原理:取一定數量(數列中的最大值)的編好序號的桶,將數列每個數放進編號爲它的桶裏,然後將不是空的桶依次倒出來,

就組成有序數列了。  維基入口

好吧!聰明的人一眼就看出桶排序的破綻了。假設只有兩個數1,10000,豈不是要一萬個桶!?這確實是個問題啊!我也

沒想出解決辦法。我起初也以爲桶排序就是一個通過犧牲空間來換取時間的排序算法,它不需要比較,所以是非比較型算法。

但看了有趣的演示桶排序後,發現世界之大,你沒有解決,不代表別人沒解決,睿智的人總是很多。

1,9999的桶排序實現:new Int[2];總共有兩個數,得出最大數9999的位數4,取10的4次冪即10000作爲分母,

要排序的數(1或9999)作爲分子,並乘以數列總數2,即1*2/10000,9999*2/10000得到各自的位置0,1,完成排序。

如果是1,10000進行排序的話,上面的做法就需要稍微加一些處理——發現最大數是10的n次冪,就將它作爲分母,並

放在數列末尾就好了。

如果是9999,10000進行排序的話,那就需要二維數組了,兩個都在位置1,位置0沒數。這個時候就需要在放

入每個位置時採用其它排序(比如插入排序)辦法對這個位置的多個數排序了。

爲基數排序做個過渡,我這裏實現了一個個位數桶排序

涉及到了當重複的數出現的處理。

實現如下:

public static void BucketSortOnlyUnitDigit(IList<int> data)
        {
            int[] indexCounter = new int[10];
            for (int i = 0; i < data.Count; i++)
            {
                indexCounter[data[i]]++;
            }
            int[] indexBegin = new int[10];
            for (int i = 1; i < 10; i++)
            {
                indexBegin[i] = indexBegin[i-1]+ indexCounter[i-1];
            }
            IList<int> tempList = NewInstance(data, data.Count);
            for (int i = 0; i < data.Count; i++)
            {
                int number = data[i];
                tempList[indexBegin[number]++] = data[i];
            }
            data = tempList;
        }

過程解析:indexCounter進行對每個數出現的頻率的統計。indexBegin存儲每個數的起始索引。

比如 1 1 2,indexCounter統計到0個0,2個1,1個2。indexBegin計算出0,1,2的起始索引分別爲

0,0,2。當1個1已取出排序,那索引將+1,變爲0,1,2。這樣就通過提前給重複的數空出位置,解決了

重複的數出現的問題。當然,你也可以考慮用二維數組來解決重複。

下面繼續基數排序。

基數排序原理:將整數按位數切割成不同的數字,然後按每個位數分別比較。

取得最大數的位數,從低位開始,每個位上進行桶排序。

實現如下:

public static IList<int> RadixSort(IList<int> data)
        {
            int max = data[0];
            for (int i = 1; i < data.Count; i++)
            {
                if (data[i] > max)
                    max = data[i];
            }
            int digit = 1;
            while (max / 10 != 0)
            {
                digit++;
                max /= 10;
            }
            for (int i = 0; i < digit; i++)
            {
                int[] indexCounter = new int[10];
                IList<int> tempList = NewInstance(data, data.Count);
                for (int j = 0; j < data.Count; j++)
                {
                    int number = (data[j] % Convert.ToInt32(Math.Pow(10, i + 1))) / Convert.ToInt32(Math.Pow(10, i));  //得出i+1位上的數
                    indexCounter[number]++;
                }
                int[] indexBegin = new int[10];
                for (int k = 1; k < 10; k++)
                {
                    indexBegin[k] = indexBegin[k - 1] + indexCounter[k - 1];
                }
                for (int k = 0; k < data.Count; k++)
                {
                    int number = (data[k] % Convert.ToInt32(Math.Pow(10, i + 1))) / Convert.ToInt32(Math.Pow(10, i));
                    tempList[indexBegin[number]++] = data[k];
                }
                data = tempList;
            }
            return data;
        }

過程解析:得出最大數的位數,從低位開始桶排序。我寫的這個實現代碼並不簡潔,但邏輯更清晰。

後面測試的時候我們就會發現,按理來說這個實現也還行吧! 但並不如想象的那麼快!

循環的次數太多?(統計頻率n次+9次計算+n次放到新的數組)*位數。

創建的新實例太多?(new int[10]兩次+NewInstance is反射判斷創建實例+new int[n])*位數

測試比較

添加隨機數組,數組有序校驗,微軟Linq排序

代碼如下:

public static int[] RandomSet(int length, int max)
        {
            int[] result = new int[length];
            Random rand = new Random();
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = rand.Next(max);
            }
            return result;
        }

        public static bool IsAscOrdered(IList<int> data)
        {
            bool flag = true;
            for (int i = 0; i < data.Count - 1; i++)
            {
                if (data[i] > data[i + 1])
                    flag = false;
            }
            return flag;
        }

        public static void TestMicrosoft(IList<int> data)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            List<int> result = data.OrderBy(a => a).ToList();
            stopwatch.Stop();
            string methodName = "TestMicrosoft";
            int length = methodName.Length;
            for (int i = 0; i < 40 - length; i++)
            {
                methodName += " ";
            }
            Console.WriteLine(methodName +
                "  IsAscOrdered:" + IsAscOrdered(result) + "  Time:" + stopwatch.Elapsed.TotalSeconds);

        }

測試主體如下:

static void Main(string[] args)
        {
            int[] aa = RandomSet(50000, 99999);
            //int[] aa = OrderedSet(5000);
            Console.WriteLine("Array Length:" + aa.Length);
            RunTheMethod((Action<IList<int>>)SelectSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)BubbleSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)BubbleSortImprovedWithFlag, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)BubbleCocktailSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)InsertSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)InsertSortImprovedWithBinarySearch, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)QuickSortStrict, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)QuickSortRelax, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)QuickSortRelaxImproved, aa.Clone() as int[]);
            RunTheMethod((Func<IList<int>, IList<int>>)MergeSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)ShellSort, aa.Clone() as int[]);
            RunTheMethod((Func<IList<int>, IList<int>>)RadixSort, aa.Clone() as int[]);
            RunTheMethod((Action<IList<int>>)HeapSort, aa.Clone() as int[]);
            TestMicrosoft(aa.Clone() as int[]);
            Console.Read();
        }

        public static void RunTheMethod(Func<IList<int>, IList<int>> method, IList<int> data)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            IList<int> result = method(data);
            stopwatch.Stop();
            string methodName = method.Method.Name;
            int length = methodName.Length;
            for (int i = 0; i < 40 - length; i++)
            {
                methodName += " ";
            }
            Console.WriteLine(methodName +
                "  IsAscOrdered:" + IsAscOrdered(result) + "  Time:" + stopwatch.Elapsed.TotalSeconds);
        }

        public static void RunTheMethod(Action<IList<int>> method, IList<int> data)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            method(data);
            stopwatch.Stop();
            string methodName = method.Method.Name;
            int length = methodName.Length;
            for (int i = 0; i < 40 - length; i++)
            {
                methodName += " ";
            }
            Console.WriteLine(methodName +
                "  IsAscOrdered:" + IsAscOrdered(data) + "  Time:" + stopwatch.Elapsed.TotalSeconds);
        }

剩餘代碼摺疊在此處

public static void Swap(IList<int> data, int a, int b)
        {
            int temp = data[a];
            data[a] = data[b];
            data[b] = temp;
        }

        public static int[] OrderedSet(int length)
        {
            int[] result = new int[length];
            for (int i = 0; i < length; i++)
            {
                result[i] = i;
            }
            return result;
        }
 
        public static IList<int> NewInstance(IList<int> data, int length)
        {
            IList<int> instance;
            if (data is Array)
            {
                instance = new int[length];
            }
            else
            {
                instance = new List<int>(length);
                for (int n = 0; n < length; n++)
                {
                    instance.Add(0);  // 初始添加
                }
            }
            return instance;
        }
 

以上動圖由“圖鬥羅”提供

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章