堆是一棵完全二叉树,这棵二叉树需要满足堆序:任何分支结点(即除去叶结点所剩余的结点)的值都大于等于(或小于等于)其左右子结点的值。一般用列表来表示堆(Python中的列表下标从0开始),i结点父结点位置为(i-1)/2,i结点的左右子结点位置为2*i+1和2*i+2。
如果堆序是小元素优先,则构造出来的称为‘小顶堆’(小元素在上)。如果堆序是大元素优先,则构造出来的称为‘大顶堆’(大元素在上)。 以下的内容默认为小顶堆。
向堆中插入元素需要用到‘向上筛选’来使得新插入的元素符合堆序。插入元素可以在O(log n)时间内完成。
向上筛选大体步骤为:将需要插入的元素首先插入到二叉树最下层最右边的新的叶结点位置,然后,比较插入元素与其父结点的大小,如果插入元素值较小,则交换两个元素的位置,重复次操作,不断向上进行比较,直到某一父结点的元素值小于插入元素的值,或者插入元素已经到达根结点为止。
弹出堆顶元素后,剩余的元素已经不再满足堆序。因此需要通过‘向下筛选’来使得剩余的元素满足堆序。其时间复杂度也为O(log n)。
向下筛选大体步骤为:在弹出堆顶元素后,将堆中的最后一个元素e放在堆顶,然后比较堆顶元素e(父结点)与其两个左右子结点的大小,将三者中最小的元素放置在父结点位置。如果左子结点最小,则交换父结点与左子结点中的元素(这样e就被下移了一层),这个左子结点又有以其为父结点的子树,然后重复进行相同的比较和交换步骤,直到元素e在某次比较中是三者中最小的那个,则说明当前已经满足堆序,停止;或者元素e已经被下移到了最后一层,此时也满足堆序了,停止。
基于堆实现的优先队列,在创建时需要O(n)的时间复杂度。插入和弹出的时间复杂度为O(log n)。此外,插入时第一步是在表的最后加一个元素,可能导致列表替换元素存储区,此时就会出现O(n)的最糟糕情况。所有的操作都只用了一个简单变量,没有其他的结构,所以空间复杂度是O(1)。
以下是代码实现:
class HeapPriQueueError(ValueError): pass class Heap_Pri_Queue(object): def __init__(self, elems = []): self._elems = list(elems) if self._elems: self.buildheap() def is_empty(self): return self._elems is [] def peek(self): if self.is_empty(): raise HeapPriQueueError("in pop") return self._elems[0] def enqueue(self, e): self._elems.append(None) #此时,总的元素的长度增加了1位 self.siftup(e, len(self._elems) - 1) def siftup(self, e, last): #向上筛选 elems, i, j = self._elems, last, (last-1)//2 #j为last位置的父结点 while i>0 and e < elems[j]: #如果需要插入的元素小于当前的父结点的值 elems[i] = elems[j] #则将父结点的值下放到其子结点中去 i, j = j, (j-1)//2 #更新i为当前父结点的位置,j更新为当前父结点的父结点的位置 elems[i] = e #如果i已经更新为0了,直接将e的值赋给位置0.或者需要插入的元素 #大于当前父结点的值,则将其赋给当前父结点的子结点 def dequeue(self): if self.is_empty(): raise HeapPriQueueError("in pop") elems = self._elems e0 = elems[0] #根结点元素 e = elems.pop() #将最后一个元素弹出,作为一个新的元素经过比较后找到插入的位置,以维持栈序 if len(elems)>0: self.siftdown(e, 0, len(elems)) return e0 def siftdown(self, e, begin, end): #向下筛选 elems, i, j = self._elems, begin, begin*2+1 #j为i的左子结点 while j < end: if j+1 < end and elems[j] > elems[j+1]: #如果左子结点大于右子结点 j += 1 #则将j指向右子结点 if e < elems[j]: #j已经指向两个子结点中较小的位置, break #如果插入元素e小于j位置的值,则为3者中最小的 elems[i] = elems[j] #能执行到这一步的话,说明j位置元素是三者中最小的,则将其上移到父结点位置 i, j = j, j*2+1 #更新i为被上移为父结点的原来的j的位置,更新j为更新后i位置的左子结点 elems[i] = e #如果e已经是某个子树3者中最小的元素,则将其赋给这个子树的父结点 #或者位置i已经更新到叶结点位置,则将e赋给这个叶结点。 def buildheap(self): end = len(self._elems) for i in range(end//2-1, -1, -1): #初始位置设置为end//2 - 1。 # print(self._elems[i]) self.siftdown(self._elems[i], i, end) # print(self._elems) if __name__=="__main__": temp = Heap_Pri_Queue([5,6,8,1,2,4,9]) print(temp._elems) temp.dequeue() print(temp._elems) temp.enqueue(0) print(temp._elems) print(temp.peek())
下面演示如何将无序的列表元素构造成符合堆序的结构(小顶堆)。假设列表初始为[5,6,8,1,2,4,9]。
其初始的对结构图为:
然后,从下面开始找到第一个非叶子结点,对其进行向下筛选;紧接着,对其余的非叶子结点也依次进行向下筛选,操作完成后,这时得到的结构就是堆序了。
最后得到的这个结构,已经都满足堆序了。