1. 程式人生 > 實用技巧 >堆與堆排序

堆與堆排序

堆與堆排序


堆的定義

  • 堆是一個完全二叉樹:除了最後一層外,其他層幾點必須是滿的,且最後一層節點從左向右逐步填充
  • 堆中每一個父節點的值必須大於等於(或小於等於)其兩子樹節點的值:大於等於是大頂堆,小於等於是小頂堆

堆的實現

    完全二叉樹使用使用陣列來儲存,所以堆也是基於陣列來實現的,堆最重要的是下面兩個基礎操作:

  • 新增(插入)新元素
  • 刪除堆頂元素

    下面詳細說明下其操作方法:

新增元素

    新增元素的演算法如下:

  • 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))

參考資料