天天看點

python 資料結構與算法——優先隊列和堆優先隊列堆

文章目錄

  • 優先隊列
    • 優先隊列的應用
    • 優先隊列的實作
    • 堆的 python 實作
      • 堆中節點的索引
      • 查找元素
      • 查找最小值
      • 查找最小 K 個元素
      • 查找最大值
      • 删除最小值
      • 删除任意元素
      • 插入元素
      • 堆的初始化
      • 堆排序
    • 構造堆的時間複雜度為 O(n)

優先隊列

優先隊列的基本操作:

  • 插入元素
  • 删除最小(大)元素
  • 擷取最小(大)元素

實作了這三個需求的抽象資料類型就叫優先隊列

優先隊列的應用

  • 資料壓縮——huffman 編碼
  • 最短路徑——dijkstra 算法
  • 最小生成樹——prim 算法
  • 查找最小 k 個元素

優先隊列的實作

其實大家都知道優先隊列一般是用“堆”實作的,但原則上這不是必須的,下面看看用各種方法實作優先隊列時的工作效率:

實作方法 插入 删除最小 查找最小
無序數組 1 n n
無序連結清單 1 n n
有序數組 n 1 1
有序連結清單 n 1 1
二叉搜尋樹 logn logn logn
平衡二叉搜尋樹 logn logn logn
二叉堆 logn logn 1

現在你知道為什麼要用堆來實作優先隊列了吧

從結構來看,堆是完全二叉樹

從節點來看,堆中的父節點的值必須不大于(不小于)其子節點的值

一般也叫做最小堆(最大堆)

堆的 python 實作

以下是最小堆的實作

堆中節點的索引

class Heap:

    def __init__(self, l=[]):
        self.heapList = [None]+l  # Elements in Heap
        self.buildHeap()

    @property
    def size(self):
        return len(self.heapList) - 1

    def parent(self, index):
        return index // 2

    def leftChildIndex(self, index):
        return 2 * index

    def rightChildIndex(self, index):
        return 2 * index + 1

    def validIndex(self, index):
        return index <= self.size and index >= 1
    
    def hasChild(self, index):
        return self.validIndex(index*2)
    
    def leftChild(self, index):
        lindex = self.leftChildIndex(index)
        return self.heapList[lindex] if self.validIndex(lindex) else None

    def rightChild(self, index):
        rindex = self.rightChildIndex(index)
        return self.heapList[rindex] if self.validIndex(rindex) else None

    def minimumChild(self, i):
        if not self.hasChild(i):
            return None
        elif not self.validIndex(self.rightChildIndex(i)):
            return self.leftChildIndex(i)
        lindex = self.leftChildIndex(i)
        rindex = self.rightChildIndex(i)
        return lindex if self.heapList[lindex] < self.heapList[rindex] else rindex

	def __repr__(self):
		return str(self.heapList[1:])

           

首先從上面的代碼可以發現在完全二叉樹中索引父節點和子節點非常容易!

下面來看一些常用操作:

查找元素

這個對于堆來說沒有優勢,隻能周遊

def searchElement(self, itm):
        i = 1
        while (self.validIndex(i)):
            if itm == self.heapList[i]:
                return i
            i += 1
        return None
           

查找最小值

直接傳回根節點,需要 O ( log ⁡ ( 1 ) ) O(\log(1)) O(log(1))時間

def getMinimum(self):
        return None if self.size == 0 else self.heapList[1]
           

查找最小 K 個元素

使用另一個堆來儲存臨時結果,時間複雜度為 O ( K log ⁡ K ) O(K\log K) O(KlogK)

def topK(self, k):
        assert(k >= 0 and k <= self.size)
        result = []
        HAux = Heap()
        item = self.getMinimum()
        HAux.insert(item)
        while (len(result) < k):
            item = HAux.deleteMin()
            result.append(item)
            if self.rightChild(self.searchElement(item)) is not None:
                HAux.insert(self.rightChild(self.searchElement(item)))
            if self.leftChild(self.searchElement(item)) is not None:
                HAux.insert(self.leftChild(self.searchElement(item)))
        return result
           

查找最大值

從最小堆裡找最大值,隻要周遊葉子節點即可, O ( n 2 ) = O ( n ) O(\frac{n}{2})=O(n) O(2n​)=O(n)

def getMaximum(self):
        maximum = self.heapList[self.parent(self.size)]
        for i in range(self.parent(self.size)+1, self.size+1):
            if self.heapList[i] > maximum:
                maximum = self.heapList[i]
        return maximum
           

删除最小值

删除根節點後需要從上之下調整一遍:percolateDown,需要 O ( log ⁡ ( n ) ) O(\log(n)) O(log(n))時間

def swap(self, a, b):
        self.heapList[a], self.heapList[b] = self.heapList[b], self.heapList[a]

    def percolateDown(self, i):
        while self.hasChild(i):
            minimumChild = self.minimumChild(i)
            if self.heapList[i] > self.heapList[minimumChild]:
                self.swap(i, minimumChild)
            i = minimumChild

    def deleteMin(self):
        retval = self.heapList[1]
        self.swap(1, self.size)
        self.heapList.pop()
        self.percolateDown(1)
        return retval
           

删除任意元素

和删除最小值相比,删除其他元素的前提是先從堆中找到該元素,這需要 O ( n ) O(n) O(n) 時間,然後将該元素和末端的葉子節點交換,删除末端節點,然後從被删除節點的原始位置開始調整堆。

def delete(self, key):
        loc = self.searchElement(key)
        if loc is not None:
            self.swap(loc, self.size)
            self.heapList.pop()
            self.percolateDown(loc)
           

插入元素

一般直接插入末尾,然後從下至上調整堆 percolateUp,需要 O ( l o g n ) O(logn) O(logn)時間

def percolateUp(self, i):
        while self.validIndex(i // 2):
            if self.heapList[i] < self.heapList[i // 2]:
                self.swap(i, i//2)
            i = i // 2
            
    def insert(self, k):
        self.heapList.append(k)
        self.percolateUp(self.size)
           

堆的初始化

從數組構造堆,圖示可見《堆排序建立初始堆》

def buildHeap(self):
        n = self.size
        for i in range(self.parent(n), 0, -1):
            self.heapify(i)

    def heapify(self, index):
        left = self.leftChildIndex(index)
        right = self.rightChildIndex(index)

        minimum = index

        if self.validIndex(left) and self.heapList[left] < self.heapList[index]:
            minimum = left

        if self.validIndex(right) and self.heapList[right] < self.heapList[minimum]:
            minimum = right

        if minimum != index:
            self.swap(minimum, index)
            self.heapify(minimum)
           

堆排序

def heapSort(self):
		result = []
		while self.size > 0:
			result.append(self.deleteMin())
		return result
           

構造堆的時間複雜度為 O(n)

證明:

  1. 調整堆的順序是從下至上,從右往左,從最後一個非葉子節點開始進行 heapify 操作
  2. 從一個節點開始進行 heapify 操作的複雜度等于該節點的高度,即從該節點向下到最遠的葉子節點的路徑的長度
  3. 是以構造堆的時間複雜度 T ( n ) T(n) T(n) 等于所有非葉子節點的高度之和
  4. 不妨設堆的高度為 h ≃ l o g 2 ( n ) h \simeq log_2(n) h≃log2​(n),則 T ( n ) = h + 2 ( h − 1 ) + 2 2 ( h − 2 ) + ⋯ + 2 h − 1 T(n) = h + 2(h-1) + 2^2(h-2) + \cdots + 2^{h-1} T(n)=h+2(h−1)+22(h−2)+⋯+2h−1 = ∑ i = 1 h − 1 2 i ( h − i )                                  = \sum_{i=1}^{h-1} 2^i (h-i) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =i=1∑h−1​2i(h−i)                                

    如此一來,變成一個聯考常見的數列求和問題: 2 T ( n ) = ∑ i = 1 h − 1 2 i + 1 ( h − i ) = ∑ j = 2 h 2 j ( h − j + 1 ) 2T(n) = \sum_{i=1}^{h-1} 2^{i+1} (h-i) = \sum_{j=2}^{h} 2^j (h-j+1) 2T(n)=i=1∑h−1​2i+1(h−i)=j=2∑h​2j(h−j+1) T ( n ) = 2 T ( n ) − T ( n ) = ∑ i = 2 h 2 i ( h − i + 1 ) − ∑ i = 1 h − 1 2 i ( h − i ) T(n) = 2T(n)-T(n) = \sum_{i=2}^{h} 2^i (h-i+1) -\sum_{i=1}^{h-1} 2^i (h-i) T(n)=2T(n)−T(n)=i=2∑h​2i(h−i+1)−i=1∑h−1​2i(h−i) = 2 h + ∑ i = 2 h − 1 2 i − 2 ( h − 1 ) =2^h+\sum_{i=2}^{h-1} 2^i -2(h-1) =2h+i=2∑h−1​2i−2(h−1) = O ( 2 h ) = O(2^h) =O(2h) = O ( n ) =O(n) =O(n)