堆與堆排序
阿新 • • 發佈:2020-08-05
堆與堆排序
堆的定義
- 堆是一個完全二叉樹:除了最後一層外,其他層幾點必須是滿的,且最後一層節點從左向右逐步填充
- 堆中每一個父節點的值必須大於等於(或小於等於)其兩子樹節點的值:大於等於是大頂堆,小於等於是小頂堆
堆的實現
完全二叉樹使用使用陣列來儲存,所以堆也是基於陣列來實現的,堆最重要的是下面兩個基礎操作:
- 新增(插入)新元素
- 刪除堆頂元素
下面詳細說明下其操作方法:
新增元素
新增元素的演算法如下:
- 1.將新增的元素放到堆的最後,也就是陣列的最後,但此時可能導致不符合堆的特性了,所以需要第二步的自下向上堆化
- 2.堆化,這裡採用自下向上的堆化,即沿著節點的路徑,與其父節點進行比較,如果不滿足父子節點關係則交換,再繼續比較;無法交換則堆化完成
下圖是一個大頂堆的堆化過程,首先將新元素22插入尾部,然後進行堆化
刪除堆頂元素
刪除堆頂元素的演算法如下:
- 1.交換堆頂元素和堆尾元素的值,這樣堆就減小,堆頂元素也就刪除了,但此時可能導致不符合堆的特性,所有需要第二步的從上向下的堆化
- 2.堆化,這裡採用自上向下的堆化,沿著節點的路徑,與其子節點進行比較,如果不滿足父子節點關係則交換,重複這個過程,知道滿足父子節點關係為止
下圖是一個大頂堆的刪除過程過程
大頂堆的實現
這裡使用Python3簡單模擬實現了一個大頂堆,小頂堆類似,也就是比較不同,這裡就不重複實現了
class MaxHeap: def __init__(self, size: int): """ 堆的初始化,設定堆的大小 :param size: 堆的大小 """ # 這裡有個小技巧,陣列下標不從0開始,而從1開始,這樣父子節點的下標獲取和計算方便,所有這裡陣列空間為size+1 self.data = [None] * (size + 1) self.size = size self.used = 0 print("init heap:", self.data) def push(self, value: int) -> None: """ 堆的插入操作 :return: """ if self.used == self.size: self.pop() self.used += 1 print("insert:", self.used, self.data, end="==>") self.data[self.used] = value self._shitUp() print(self.data) def pop(self) -> None: """ 堆的刪除操作 :return: """ if self.used == 0: return self.data[1] = self.data[self.used] print("pop:", self.data, end="==>") self.used -= 1 self._shitDown() print(self.data) def _shitUp(self): """ 自下向上的堆化 與父節點進行比較,大於則交換 :return: """ child = self.used while child // 2 > 0 and self.data[child] > self.data[child//2]: self.data[child], self.data[child//2] = self.data[child//2], self.data[child] child = child // 2 def _shitDown(self): """ 自上向下的堆化 這裡是與最大值的子節點進行交換,則用了一個maxPos儲存最大值的位置 如果是當前父節點的位置,則堆化結束 不是則交換父子節點,繼續迴圈 :return: """ parent = 1 while True: maxPos = parent if parent * 2 <= self.used and self.data[parent*2] > self.data[parent]: maxPos = parent * 2 if parent * 2 + 1 <= self.used and self.data[parent*2+1] > self.data[maxPos]: maxPos = parent*2+1 if maxPos == parent: break self.data[maxPos], self.data[parent] = self.data[parent], self.data[maxPos] parent = maxPos def toList(self): return self.data if __name__ == "__main__": maxHeap = MaxHeap(2) maxHeap.push(1) maxHeap.push(2) maxHeap.push(3) print(maxHeap.toList()) init heap: [None, None, None] insert: 1 [None, 1, None] insert: 2 [None, 2, 1] pop: [None, 1, 1] insert: 2 [None, 3, 1] [None, 3, 1]
LeetCode 347:前 K 個高頻元素的使用自寫堆
如何需要去跑測試的話,需要把程式碼裡面的print列印的去掉,不然會超出輸出限制。
LeetCode裡面有一個是使用堆來解的,答案裡面有是使用語言實現,這裡就自己實現一個小頂堆來嘗試。小頂堆做的唯一修改就是加入一個字典,進行比較時進行轉換再比較。大致的程式碼和思路在下面的程式碼中。
""" 347. 前 K 個高頻元素 給定一個非空的整數陣列,返回其中出現頻率前 k 高的元素。 示例 1: 輸入: nums = [1,1,1,2,2,3], k = 2 輸出: [1,2] 示例 2: 輸入: nums = [1], k = 1 輸出: [1] 提示: 你可以假設給定的 k 總是合理的,且 1 ≤ k ≤ 陣列中不相同的元素的個數。 你的演算法的時間複雜度必須優於 O(n log n) , n 是陣列的大小。 題目資料保證答案唯一,換句話說,陣列中前 k 個高頻元素的集合是唯一的。 你可以按任意順序返回答案。 解題思路: 使用小頂堆實現,因為要返回前最大K個數,小頂堆就可以儲存著K個數,而堆頂是最小數,剩下的都是大於它的 1.先使用hashmap統計儲存數字的出現次數 2.使用k個數據初始化小頂堆 3.比堆頂大的就插入,小於就說明前K大的數沒它的份 統計N,遍歷N,大頂堆操作logK,則最大時間複雜度O(N) 自己實現個堆來嘗試嘗試 內建的跑了60ms,自寫的56,感覺差不多 """ import collections from typing import List import heapq class SolutionP: def topKFrequent(self, nums, k): """ :type nums: List[int] :type k: int :rtype: List[int] """ count = collections.Counter(nums) return heapq.nlargest(k, count.keys(), key=count.get) class Solution: def topKFrequent(self, nums: List[int], k: int) -> List[int]: count = {} for num in nums: count[num] = count.get(num, 0) + 1 print(count) # 使用K個數初始化堆 minHeap = MinHeap(k, count) keys = list(count.keys()) print(keys) for i in range(0, k): minHeap.push(keys[i]) # 大於堆頂才插入,小於的就說明前K個數沒有它的份 for i in range(k, len(keys)): if count[keys[i]] > count[minHeap.getMinest()]: minHeap.push(keys[i]) return minHeap.toList() class MinHeap: def __init__(self, size: int, myDict: dict): """ 堆的初始化,設定堆的大小 :param size: 堆的大小 """ # 這裡有個小技巧,陣列下標不從0開始,而從1開始,這樣父子節點的下標獲取和計算方便,所有這裡陣列空間為size+1 self.data = [None] * (size + 1) self.size = size self.used = 0 self.myDict = myDict print("init heap:", self.data) def push(self, value: int) -> None: """ 堆的插入操作 :return: """ print("insert value:", value, end=" ") if self.used == self.size: self.pop() self.used += 1 print("insert:", self.used, value, self.data, end="==>") self.data[self.used] = value self._shitUp() print(self.data) def pop(self) -> None: """ 堆的刪除操作 :return: """ if self.used == 0: return self.data[1] = self.data[self.used] print("pop:", self.data, end="==>") self.used -= 1 self._shitDown() print(self.data) def _shitUp(self): """ 自下向上的堆化 與父節點進行比較,大於則交換 :return: """ child = self.used while child // 2 > 0 and self.myDict[self.data[child]] < self.myDict[self.data[child // 2]]: self.data[child], self.data[child // 2] = self.data[child // 2], self.data[child] child = child // 2 def _shitDown(self): """ 自上向下的堆化 這裡是與最大值的子節點進行交換,則用了一個maxPos儲存最大值的位置 如果是當前父節點的位置,則堆化結束 不是則交換父子節點,繼續迴圈 :return: """ parent = 1 while True: minPos = parent if parent * 2 <= self.used and self.myDict[self.data[parent * 2]] < self.myDict[self.data[parent]]: minPos = parent * 2 if parent * 2 + 1 <= self.used and self.myDict[self.data[parent * 2 + 1]] < self.myDict[self.data[minPos]]: minPos = parent * 2 + 1 if minPos == parent: break self.data[minPos], self.data[parent] = self.data[parent], self.data[minPos] parent = minPos def getMinest(self) -> int: """ 返回堆頂元素值 :return: """ return self.data[1] def toList(self): return self.data[1:] if __name__ == "__main__": s = Solution() print(s.topKFrequent(nums=[1, 1, 1, 2, 2, 3], k=2)) # [-3,-4,0,1,4,9] print(s.topKFrequent(nums=[6, 0, 1, 4, 9, 7, -3, 1, -4, -8, 4, -7, -3, 3, 2, -3, 9, 5, -4, 0], k=6))