# 堆積排序 > 上一篇文章 [Euler路徑與Euler圖](https://hackmd.io/@iDoNotWantToCoding/Hydgjiaske) > 下一篇文章 無 > 先備知識  無 > 延伸閱讀  無 --- ## <font size=6>前言</font> 在了解堆積排序之前,我們需要了解以下幾個概念。 ### 圖論的基礎概念 >傳送門:point_right:[圖論基礎觀念](/CbclZG4dSyuVAtq1YOEt8A) ### 二元樹(binary tree) 二元樹是資料結構與演算法中最重要的工具之一。與一般的樹結構不同,二元樹的每個節點最多只能有兩個子節點,分別稱為左子樹與右子樹。也就是說,在二元樹中,每個節點的子節點數量上限為兩個,並具有明確的左右之分。 ![binary_tree__complete-binary-tree](https://hackmd.io/_uploads/HyS66yUlle.jpg) ### 完整二元樹(complete binary tree) 完整二元樹屬於二元樹的一種,該二元樹除了最後一層的節點,其他層的節點皆為全滿的狀態,如果最後一層的節點沒有全滿的話則靠左。 >要注意的是 **完整二元樹(complete binary tree)** 與 **滿二元樹(full binary tree)** 概念不相同,許多人容易搞混。 >![images](https://hackmd.io/_uploads/SJYoyg8exx.png) >> #### 完整二元樹(complete binary tree): >> 除了最後一層的節點,其他層的節點皆為全滿的狀態 > >> #### 滿二元樹(full binary tree): >> 除了樹葉以外,每個節點都有兩個小孩。 ### 堆序性(Heap Order Property) >#### 最大堆(Max-Heap): >每個節點的值都**大於等於**其子節點的值。 >#### 最小堆(Min-Heap): >每個節點的值都**小於等於**其子節點的值。 ## <font size=6>概述</font> 堆積排序(Heap Order Property)是一種以 **堆(Heap)** 的資料結構進行排序演算法的方式,堆是一種特殊的完整二元樹,其中按照堆序性分為 **最大堆(Max-Heap)** 以及 **最小堆(Min-Heap)**。 ### 堆積排序可以分成以下兩個步驟。 > ### 建構最大堆或最小堆 > #### 使用list作為建堆的架構,其中list與堆的樹架構之關係為: > **父節點(Parent)**:`index` = (i - 1) // 2 > **左子節點(Left Child)**:`index` = 2 * i + 1 > **右子節點(Right Child)**:`index` = 2 * i + 2 > > **範例**:`list` = [10, 5, 3, 2, 1] > ![index_example](https://hackmd.io/_uploads/ByUylNvgeg.png) > > 以下以建構最大堆為範例,將原始無序陣列轉換成最大堆,確保父節點 ≥ 子節點。 > ```python= > def heapify(arr, n, i): > largest = i # 假設目前的節點為最大值 > left = 2 * i + 1 # 左子節點 > right = 2 * i + 2 # 右子節點 > > # 比較左子節點與父節點 > if left < n and arr[left] > arr[largest]: > largest = left > > # 比較右子節點與目前最大值 > if right < n and arr[right] > arr[largest]: > largest = right > > # 如果最大值不是父節點,交換並遞迴 heapify > if largest != i: > arr[i], arr[largest] = arr[largest], arr[i] # 交換 > heapify(arr, n, largest) > ``` > > ### 排序 > 在建構完成之後,`list`中的第0個元素必定會是資料中最大的數值。之後將這個最大的數值與末端的數進行交換,此時這個最大的數就是已經被排序完成的狀態,之後忽略它並且對其他數值進行重新建堆,**重複這個建堆以及交換的過程,最後就可以完成排序了**。 > ```python=18 > def heap_sort(arr): > n = len(arr) > > # 第一步:建構最大堆 > for i in range(n // 2 - 1, -1, -1): # 從最後一個非葉節點往上 heapify > heapify(arr, n, i) > > # 第二步:排序過程 > for i in range(n - 1, 0, -1): > arr[0], arr[i] = arr[i], arr[0] # 將最大值移至末端 > heapify(arr, i, 0) # 對新的堆頂元素進行 heapify >``` > > #### 以`list` = [10, 5, 3, 2, 1]為例 > 1. 初始陣列:[4, 10, 3, 5, 1] > 2. 建構最大堆:[10, 5, 3, 4, 1] > 3. 取最大值10放到末尾:[1, 5, 3, 4, 10] > 4. 重新建堆: [5, 4, 3, 1, 10] > 5. 重複交換及建堆:[1, 4, 3, 5, 10] > 6. 最後結果:[1, 3, 4, 5, 10] ## <font size=6>範例題目</font> 在了解堆積排序的基本排序概念後,我們可以將它連接一些實際的例子以幫助我們熟悉。 將堆積排序實際運用在撲克牌的題目:Hand of Straights > ### Straights是什麼?Straights的成立條件? > Straights的中文名稱稱為"順子",在德州撲克、大老二等鋪克牌遊戲中運用。 > 順子的成立條件為五張牌的數子相連,其中`A`只能出現在兩端不可出現在中間。 ### 題目敘述: > 愛麗絲有一定數量的卡片,她想將這些卡片重新排列成群組,使得每組大小為 `groupSize`,並由 `groupSize` 張連續的卡片組成。 > >給定一個整數陣列 `hand`,其中 `hand[i]` 是第 i 張卡片上寫的值和一個整數 `groupSize`,如果她可以重新排列卡片,則傳回 `True`,否則傳回 `False`。 > 在此題目中的順子並不局限於必須要五張相連號碼的牌,可以從`input`中的`groupSize`決定出一組順子需要幾張相連號碼的牌。 ### 解題思路 1. 首先我們可以分析手中的牌數量是否為`groupSize`的倍數,如果不是的話,則無法完成分組,直接回傳`False`即可。 ```python=6 # 張數不是 groupSize 的倍數,一定不可能分組 if len(hand) % groupSize != 0: return False ``` 2. 使用`Counter`來計算每個號碼的數量。 ```python=10 # 統計每張牌的數量 count = collections.Counter(hand) ``` 3. 使用Min-Heap由小至大牌序陣列`hand`中的撲克牌。 ```python=13 # 將所有出現過的牌放入最小堆 min_heap = list(count.keys()) heapq.heapify(min_heap) ``` 4. 選出最小的牌,並且向後看`groupSize`張牌是否能形成順子,若不能滿足順子,則直接回傳`False`。 ```python=18 first = min_heap[0] # 最小牌作為一組的起點 for i in range(first, first + groupSize): if count[i] == 0: return False # 某張牌不夠用 ``` 5. 形成順子後,將成員的`count`全部減1,如果剩餘的`count`為0則直接刪除。 ``` python=22 count[i] -= 1 if count[i] == 0: # 如果此牌用完了,且正是 heap 頂部,從 heap 移除 if i == min_heap[0]: heapq.heappop(min_heap) ``` 6. 回到第4步繼續檢查是否形成順子,直到`min heap`中的牌全部清空則回傳`True`。 ### 完整程式碼 ```python= import collections import heapq class Solution(object): def isNStraightHand(self, hand, groupSize): # 張數不是 groupSize 的倍數,一定不可能分組 if len(hand) % groupSize != 0: return False # 統計每張牌的數量 count = collections.Counter(hand) # 將所有出現過的牌放入最小堆 min_heap = list(count.keys()) heapq.heapify(min_heap) while min_heap: first = min_heap[0] # 最小牌作為一組的起點 for i in range(first, first + groupSize): if count[i] == 0: return False # 某張牌不夠用 count[i] -= 1 if count[i] == 0: # 如果此牌用完了,且正是 heap 頂部,從 heap 移除 if i == min_heap[0]: heapq.heappop(min_heap) return True ``` :::spoiler 額外補充(點我展開) `heapq`為python內建標準函式庫中的一部分。 可以使用`heapq.heapify(lst)`直接進行最小堆排序。 ::: --- > 上一篇文章 [Euler路徑與Euler圖](https://hackmd.io/@iDoNotWantToCoding/Hydgjiaske) > 下一篇文章 無 > 先備知識  無 > 延伸閱讀  無 > 觀念是非題 [圖論觀念是非題](https://docs.google.com/forms/d/e/1FAIpQLSfynh26cxO2G8SfBJrqFgO9_2BrMKEYtRbYspk3yM0djuzzEQ/viewform?usp=header)