資料結構與演算法C#版筆記–排序(Sort)-下

來源:互聯網
上載者:User

5、堆排序(HeapSort)

在接觸“堆排序”前,先回顧一下資料結構C#版筆記--樹與二叉樹 ,其中提到了“完全二叉樹”有一些重要的數學特性:

就是一顆完全二叉樹,如果每個節點按從上到下,從左至右標上序號,則可以用數組來實現順性儲存,同時其序號:

1、如果i>1,則序號為i的父結節序號為i/2(這裡/指整除) 言外之意:整個數組前一半都是父節點,後一半則是分葉節點

2、如果2*i<=n(這裡n為整顆樹的節點總數),則序號為i的左子節點序號為2*i

3、如果2*i +1 <=n,則序號為i的右子節點序號為2*i + 1

 

好了,再來看看"堆(Heap)"是個神馬玩意兒?

其實,堆就是一顆完全二叉樹,由上面的知識點回顧可以知道,任意給定一個數組,我們就能將它構造成一顆完全二叉樹,也就是建立一個“堆”--ps:還好業內標準稱它為一堆,而不是一坨 :)

 

其中,堆又可以分為最大堆與最小堆,就是一個最大堆:

簡言之:每個(父)節點的值,都比其子節點值大,這樣的堆就稱為最大堆;反過來類推,如果每個(父)節點的值,都比其子節點小,就叫最小堆。

 

下面該"堆排序"(HeapSort)登場了,其思路為:

1、先將給定待排序的數組通過一定處理,形成一個“最大堆”

2、然後將根節點(root)與最後一個序號的節點(lastNode)對換,這樣值最大的根節點,就“沉”到所有節點最後了(也就是墊底了),下輪處理就不用理會它了.

3、因為第2步的操作,剩下的這些節點肯定已經不滿足最大堆的定義了(因為把小值的末節點換成根節點了,它的子節點中肯定會有值比它大的),然後再類似第1步的處理,把這些剩下的節點重新排成“最大堆”

4、重複第2步的操作,將“新最大堆的根節點”與“新最大堆的末結點”(其實就是整個數組的倒數第二個節點,因為在第一輪處理中,最大值的節點已經沉到最後了,所以新最大堆的最末節點就是整個數組的倒數第二個節點)對換,這樣第二大的元素也沉到適當的位置了,以後就不用理它了,然後繼續把剩下的節點,重組成最大堆

5、反覆以上過程,直到最後剩下的節點只剩一個為止(即沒辦法再繼續重組成最大堆),這時排序結束,最後剩下的節點,肯定就是值最小的

假設給定數組new int[] {1,3,5,6,4,2},要求用“堆排序演算法”從小到大排序,上面的演算法描述圖解為:

理解以上思路後,堆排序就拆分成了二個問題:

A、如何將數組指定範圍的N個元素建立一個"最大堆"?

B、如何用一定的演算法,反覆調用A中的"最大堆建立"方法,以處理剩下的節點,直到最終只剩一個元素為止

建立最大堆的演算法,完全依賴於完全二叉樹的數學特性,代碼如下:

        /// <summary>        /// 建立最大堆        /// </summary>        /// <param name="arr">待處理的數組</param>        /// <param name="low">指定連續待處理元素範圍的下標下限</param>        /// <param name="high">指定連續待處理元素範圍的下標上限</param>        static void CreateMaxHeap(int[] arr, int low, int high)        {            if ((low < high) && (high <= arr.Length - 1))            {                int j = 0, k = 0, t = 0;                //根據完全二叉樹特性,前一半元素都是父節點,所以只需要遍曆前一半即可                for (int i = high / 2; i >= low; --i)                {                    k = i;                    t = arr[i];//暫存當前節點值                    j = 2 * i + 1;//計算左節點下標(注意:數組下標是從0開始的,而完全二叉樹的序號是從1開始的,所以這裡的2*i+1是左子節點,而非右子節點!)                                        while (j <= high) //如果左節點存在                    {                        //如果右節點也存在,且右節點更大                        if ((j < high) && (j + 1 <= high) && (arr[j] < arr[j + 1]))                        {                            ++j;//將j值調整到右節點的序號,即經過該if判斷後,j對應的元素就是i元素的左、右子節點中值最大的                        }                        //如果本身節點比子節點小                        if (t < arr[j])                        {                            arr[k] = arr[j];//將自己節點的值,更新為左右子節點中最大的值                            //然後儲存左右子節點中最大元素的下標(因為實際上要做的是將最大子節點與自身進行值交換,                            //上一步只完成了交換值的一部分,後面還會繼續處理才能完成整個交換)                            k = j;                            j = 2 * k + 1;//交換後,j元素就是父節點了,然後重新以j元素為父節點,繼續考量其"左子節點",準備進入新一輪while迴圈                        }                        else //如果本身已經是最大值了,則說明元素i所對應的子樹,已經是最大堆,則直接跳出迴圈                        {                            break;                        }                    }                    //接上面的交換值操作,將最大子節點的元素值替換為t(因為最近的一次if語句中,k=j 了,                    //所以這裡的arr[k]其實就是arr[j]=t,即完成了值交換的最後一步,                    //當然如果最近一次的if語句為false,根本沒進入,則這時的k仍然是i,維持原值而已)                    arr[k] = t;                }            }        }

反覆調用該演算法排序的代碼:

        /// <summary>        /// 堆排序        /// </summary>        /// <param name="arr"></param>        static void HeapSort(int[] arr)        {            int tmp = 0;            //初始時,將整個數組排列成"初始最大堆"            CreateMaxHeap(arr, 0, arr.Length - 1);            for (int i = arr.Length - 1; i > 0; --i)            {                //將根節點與最末結點對換                tmp = arr[0];                arr[0] = arr[i];                arr[i] = tmp;                //去掉沉底的元素,剩下的元素重新排列成“最大堆”                CreateMaxHeap(arr, 0, i - 1);            }        }

點評:這是一種思維方式很獨特的排序方式,時間複雜度跟快速排序類似,也是跟二叉樹有關,為O(Nlog2N),同樣它也是一種不穩定的排序。

6、歸併排序演算法(MergeSort)

思路:將數組中的每個元素看作一個小序列,然後二二合并成一個有序的新序列(這樣序列個數從N變成了N/2,但是每個小序列的長度從1變成2),然後繼續將這些新序列二二合并,得到N/4個序列(每個序列的長度從2變成4),如此反覆,最終得到一個全部排列好的完整序列。這也是演算法中"分治法"的經典案例之一,即分而治之。

這裡反覆要用到將二個序列合并為新序列的處理,封裝成以下方法 :

        /// <summary>        /// 歸併處理        /// </summary>        /// <param name="arr">需要歸併處理的數組</param>        /// <param name="len">每段小序列的長度</param>        static void Merge(int[] arr, int len)        {            int m = 0; //臨時順序表的起始位置            int low1 = 0; //第1個有序表的起始位置            int high1; //第1個有序表的結束位置            int low2; //第2個有序表的起始位置            int high2; //第2個有序表的結束位置            int i = 0;            int j = 0;            //暫存資料表,用於臨時將兩個有序表合并為一個有序表            int[] tmp = new int[arr.Length];            //歸併處理            while ((low1 + len) < arr.Length)            {                low2 = low1 + len; //第2個有序表的起始位置                high1 = low2 - 1; //第1個有序表的結束位置                //第2個有序表的結束位置                high2 = ( (low2 + len - 1) < arr.Length) ? low2 + len - 1 : arr.Length - 1;                                                i = low1;                j = low2;                //如果二個有序表都還沒整完                while ((i <= high1) && (j <= high2))                {                    if (arr[i] <= arr[j])//如果 第1個有序表的元素小於第2個有序表的對應元素,則直接複製第1個有序表的元素到暫存資料表                    {                        tmp[m++] = arr[i++];                    }                    else//否則,複製第2個有序表的元素到暫存資料表                    {                        tmp[m++] = arr[j++];                    }                }                //經過上面的處理後,如果第1個有序表還有元素                while (i <= high1)                {                    tmp[m++] = arr[i++];                }                //經過上面的處理後,如果第2個有序表還有元素                while (j <= high2)                {                    tmp[m++] = arr[j++];                }                low1 = high2 + 1;//將low1"指標"指到“處理完的2個有序表”之後,以方便下面將剩餘未處理完的元素複製到暫存資料表            }            i = low1;            //將尚未處理到的元素複製到暫存資料表            while (i < arr.Length)            {                tmp[m++] = arr[i++];            }            //將暫存資料表的元素複製到原數組            for (i = 0; i < arr.Length; ++i)            {                arr[i] = tmp[i];            }        }

排序處理:

        /// <summary>        /// 歸併排序        /// </summary>        /// <param name="arr"></param>        static void MergeSort(int[] arr)        {            int k = 1; //歸併增量            while (k < arr.Length)            {                Merge(arr, k);                k *= 2;            }        }    }

點評:倆倆合并,又是跟2有關的,哈哈,電腦領域真的很2啊!所以其時間複雜度又是O(Nlog2N),但是該演算法需要很多的臨時數組,所以其空間複雜度較其它演算法都要大一些為O(N),此外它是穩定的排序方法。

 

排序方法小結:(原書的小結還算不錯,就懶得自己再寫了,直接從原電子書上copy過來記錄下)

     排序在電腦程式設計中非常重要,上面介紹的各種排序方法各有優缺點,適用的場合也各不相同。

    在選擇排序方法時應考慮的因素有:

   (1)待排序記錄的數目 n 的大小;

   (2)記錄本身除關鍵碼外的其它資訊量的大小;

   (3)關鍵碼(即元素值)的情況;

   (4)對排序穩定性的要求;

   (5)語言工具的條件,輔助空間的大小等。

   綜合考慮以上因素,可以得出如下結論:

   (1)若排序記錄的數目 n 較小(如 n≤50)時,可採用直接插入排序或簡單選擇排序。由於直接插入排序所需的記錄移動操作較簡單選擇排序多,因而當記錄本身資訊量較大時,用簡單選擇排序比較好。

   (2)若記錄的初始狀態已經按關鍵碼基本有序,可採用直接插入排序或冒泡排序。

   (3)若排序記錄的數目n較大,則可採用時間複雜度為O(nlog2n)的排序方法(如快速排序、堆排序或歸併排序等)。

快速排序的平均效能最好,在待排序序列已經按關鍵碼隨機分布時,快速排序最適合。快速排序在最壞情況下的時間複雜度是O(n2),而堆排序在最壞情況下的時間複雜度不會發生變化,並且所需的輔助空間少於快速排序。但這兩種排序方法都是不穩定的排序,若需要穩定的排序方法,則可採用歸併排序。

   (4)前面討論的排序演算法,都是採用順序儲存結構。在待排序的記錄非常多時,為避免花大量的時間進行記錄的移動,可以採用鏈式儲存結構。直接插入排序和歸併排序都可以非常容易地在鏈表上實現,但快速排序和堆排序卻很難在鏈表上實現。此時,可以提取關鍵碼建立索引表,然後對索引表進行排序。也可以引入一個整形數組 t[n]作為輔助表,排序前令t[i]=i,1≤i≤n。若排序演算法中要求交換記錄 R[i]和 R[j],則只須交換 t[i]和 t[j]即可。排序結束後,數組 t[n]就存放了記錄之間的循序關聯性

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.