7.10.1 优先队列及堆

树这一章真是又臭又长,这一节从优先队列着手,学习堆的概念。

优先队列

队列是一种FIFO(First-In-First-Out)先进先出的数据结构,优先队列(Priority Queue)是特殊的队列,从“优先”一词,可看出有“插队现象”,取出元素的顺序是依照元素的 优先权(关键字)。

优先队列有两种特殊的操作:删除最大元素和插入元素

我们来看一下优先队列的实现方案,元素插入时如果不处理O(1),取出最大元素时就需要查找O(n);元素插入时如果要按序存储O(n),取出元素时就可O(1)。

优先队列的实现常选用二叉堆,在数据结构中,优先队列一般也是指堆。 

我们来认识一下堆:

  1.  二叉堆是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。
  2. 堆序性:由于我们想很快找出最小元,则最小元应该在根上,任意节点都小于它的后裔,这就是小顶堆(Min-Heap);如果是查找最大元,则最大元应该在根上,任意节点都要大于它的后裔,这就是大顶堆(Max-heap)。也可以称为最大堆、最小堆。
  3. 由完全二叉树的性质可知,我们可以用一个数组来表示堆,不需要去建树。

我们来回忆一下完全二叉树的性质:(假设一棵完全二叉树,结点编号为1~n。)

  • 子节点计算:对于i结点,若i结点有左孩子,则编号为2i,若i结点有右孩子,则编号为(2i+1);
  • 父结点计算:除根结点外,i结点的父结点编号为i/2(向下取整);
  • 对于该完全二叉树,若i ≤ n/2(向下取整),则i结点为分支结点,否则为子结点。编号最大的分支结点是n/2向下取整。

堆的基本操作

首先看一下元素如何插入:当我们需要插入一个元素时,总是把它加到数组末尾(最后一个叶子节点),然后调整到合适的位置。

以最大堆为例,当堆的结构被破坏,某个节点(如插入的结点)大于其父节点时,我们就需要通过交换它和它的父节点来修复堆,这个操作称为“上浮(swim)”。通过循环不断交换当前节点与父节点直到其键值不大于父节点为止。

这一操作在代码中仅仅体现为 : k 结点大于其父, 交换 k,k/2; 再令k = k/2,直到堆有序。需要注意的是,上文提到的完全二叉树的性质中,结点编号从1开始,这跟数组中的索引是不同的。

相反,当某个节点的键值小于其子节点的键值时,我们要交换它和它的较大子节点来修复堆,直到堆有序。这个操作称为“下沉(sink)”。 

堆中元素的删除,一般是指删除堆的根结点,也就是最大堆删除堆中最大值,最小堆删除堆中最小值。

当我们要删除根节点时,与最后一个叶子节点交换,然后使新的根节点下沉到合适的位置,并将最后一个叶子节点置空。

算法复杂度分析:

大小为N的完全二叉树的高度小于等于logN,所以对于二叉堆实现优先队列来说,从代码层面来看插入和删除操作也显而易见,算法复杂度为 O(logN).

参考博客:

最详细版图解优先队列(堆)

基于堆的优先队列

堆-神奇的优先队列

 

python 实现

补充一下,从堆的原理也可理解堆排序的算法,堆的相关问题,其实都可以认为是堆排序的应用,但是我们还是到讲排序的时候再学一学。

我们先看一下 自己如何实现二叉堆:

import copy

class MaxHeap(object):
    def __init__(self):
        self.data = []  # 创建堆 #
        self.count = len(self.data)  # 元素数量

    def init(self, arr):
        self.data = copy.copy(arr)
        self.count = len(self.data)
        i = self.count // 2  # 从倒数第二层向上 检查堆的性质
        while i >= 1:
            self.sink(i)
            i -= 1

    def size(self):
        return self.count

    def is_empty(self):
        return self.count == 0

    def insert(self, item): # 插入元素入堆
        self.data.append(item)
        self.count += 1
        self.swim(self.count)

    def swim(self, pos):
        # 注意pos 是完全二叉树中的编号,而不是索引
        # 将插入的元素放到合适位置,保持最大堆
        while pos > 1 and self.data[(pos // 2) - 1] < self.data[pos - 1]:
            self.data[(pos // 2) - 1], self.data[pos - 1] = self.data[pos - 1], self.data[(pos // 2) - 1]
            pos //= 2

    def sink(self, pos):
        # 注意pos 是完全二叉树中的编号,而不是索引
        # 将堆的索引位置元素向下移动到合适位置,保持最大堆
        while 2 * pos <= self.count:
            # 证明有孩子
            j = 2 * pos
            if j + 1 <= self.count:
                # 证明有右孩子
                if self.data[j] > self.data[j-1]:  # 选择左右孩子中值较大的
                    j += 1
            if self.data[pos - 1] >= self.data[j - 1]:
                # 堆的索引位置已经大于两个孩子节点,不需要交换了
                break
            self.data[pos - 1], self.data[j - 1] = self.data[j - 1], self.data[pos - 1]
            pos = j

    def top_max(self): # 出堆
        if self.count > 0:
            ret = self.data[0]
            self.data[0], self.data[self.count-1] = self.data[self.count-1], self.data[0]
            self.data.pop()
            self.count -= 1
            self.sink(1)
            return ret
        else:
            return None


if __name__ == '__main__':
    heap = MaxHeap()
    heap.init([4,2,7,13,6,1,20,15,3,11])
    print(heap.data)
    heap.insert(10)
    heap.insert(28)
    print(heap.data)
    print(heap.top_max())
    print(heap.top_max())
    print(heap.top_max())
    print(heap.data)

主要的代码还是sink和swim的操作,也并不复杂,虽然工具包提供了相关算法,还是建议亲自手撕一下,关注一些细节,比如完全二叉树在这个问题上的应用。

接下来介绍Python中已有的 heapq,该工具最重要的一点是,堆本身,是作为参数传进去的,heapq只提供操作方法。我们可以看这几篇博客,也包括了堆排序的实现。

python堆排序heapq

Python堆排序之heapq

文章其实都是在介绍heapq的使用,其实,建堆的过程就是在堆排序。heapq提供了建堆出堆的相关方法,让一个list对象符合堆的性质。我们先看heapq提供的函数:

>>> heapq.__all__
['heappush', 'heappop', 'heapify', 'heapreplace', 'merge', 'nlargest', 'nsmallest', 'heappushpop']

这些函数的功能及接口大致如下,从heappop可以看出,heapq实现的是一个最小堆。

  • heappush(heap, item):将 item 元素加入堆。
  • heappop(heap):将堆中最小元素弹出。
  • heapify(heap):将堆属性应用到列表上。
  • heapreplace(heap, x):将堆中最小元素弹出,并将元素x 入堆。
  • merge(*iterables, key=None, reverse=False):将多个有序的堆合并成一个大的有序堆,然后再输出。
  • heappushpop(heap, item):将item 入堆,然后弹出并返回堆中最小的元素。
  • nlargest(n, iterable, key=None):返回堆中最大的 n 个元素。
  • nsmallest(n, iterable, key=None):返回堆中最小的 n 个元素。

我们简单看一下各函数使用栗子:

from heapq import *

# 建堆的两种方法
data = [1,5,3,2,8,5]
heap = []

# 一个一个push
for n in data:
    heappush(heap, n)
# 或者这样
heapify(data)

#####################################
# merge 合并多个有序堆
a = [4, 7, 11]
b = [1, 5, 8]
c = merge(a, b)
print(list(c))  # [1, 4, 5, 7, 8, 11]

######################################
heap = []
# 向堆中依次增加数值 heap一直是堆
heappush(heap, 8)
heappush(heap, 5)
heappush(heap, 13)
print(heap)  # [5, 8, 13]
# 取出最小值
print(heappop(heap))  # 5

# 如果采用其他方法操作heap, 就会破坏堆的性质
heap.append(2)
print(heappop(heap))  # 8

###################################
data = [1,5,3,2,8,5]
print(nlargest(3, data))
print(nsmallest(3, data))

# 这个两个函数还接受一个关键字参数key, 用于更加复杂的数据结构中。
portfolio = [
    {'name': 'IBM', 'shares': 100, 'price': 91.1},
    {'name': 'AAPL', 'shares': 50, 'price': 543.22},
    {'name': 'FB', 'shares': 200, 'price': 21.09},
    {'name': 'HPQ', 'shares': 35, 'price': 31.75},
    {'name': 'YHOO', 'shares': 45, 'price': 16.35},
    {'name': 'ACME', 'shares': 75, 'price': 115.65}
]
# 对每个元素进行比较时,会以price的值进行比较。
cheap = nsmallest(3, portfolio, key=lambda s: s['price'])
expensive = nlargest(3, portfolio, key=lambda s: s['price'])


##############################################
# 由于堆中的元素可以为元组,所以可以对带权值的元素进行排序。
h = []
heappush(h, (5, 'write code'))
heappush(h, (7, 'release product'))
heappush(h, (1, 'write spec'))
heappush(h, (3, 'create tests'))
heappop(h)
'''
(1, 'write spec')
'''

##################################
'''
heappushpop(heap, item) 是heappush和heappop的合体,同时完成两者的功能.
注意:相当于先操作了heappush(heap,item),然后操作heappop(heap)
'''
h = [1, 2, 9, 5]
print(heappop(h))  # 1
print(heappushpop(h, 4))  # 2
print(h)  # [4, 5, 9]

'''
heapreplace(heap, item) 是heappop和heappush的联合操作。
注意,与heappushpop(heap,item)的区别在于,顺序不同,这里是先进行删除,后压入堆
'''
print(heapreplace(h, 2))  # 4
print(h)  # [2, 5 ,9]

这一工具基本上提供了堆的操作,使用方便。但是可以看出,需要外部维护一个list 作为堆本身,有破坏堆性质的危险;所以在复杂的使用场景中,我们一般可以利用heapq工具封装一个堆的class,再进行各种操作比较安全,使用栗子可以看后面的一些题目。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值