图文结合、利于理解的数据结构学习笔记(4)——优先级队列、堆

机械工业出版社《数据结构与算法——Python语言实现》学习笔记。
画图工具:draw.io

1 优先级队列的ADT

形式化地将一个元素和它的优先级用一个 key-value 对进行建模。优先级队列 P 支持以下方法:

  • P.add(k, v):向优先级队列中插入一个拥有键 k 和值 v 的元组。
  • P.min():返回一个优先级队列 P 中拥有最小键值的元组 (k, v) (但没有移除该元组);如果队列为空,抛出错误。
  • P.remove_min():从优先级队列 P 中移除一个拥有最小键值的元组 (k, v),并返回这个元组;如果队列为空,抛出错误。
  • P.is_empty():如果优先级队列不包含任何元组,将返回True。
  • len(P):返回优先级队列中元组的数量。

一个优先级队列中可能包含多个键值相等的条目,在这种情况下 min 和 remove_min 方法可能从具有最小键值的元组中任选一个返回。值可以是任何对象类型

2 优先级队列的实现

2.1 使用未排序列表实现优先级队列

使用位置列表+组合设计模式的方法实现

操作运行时间
lenO(1)
is_emptyO(1)
addO(1)
minO(n)
remove_minO(n)

2.2 使用排序列表实现优先级队列

实现方法与未排序列表基本相同,在add()时扫描列表找到合适的位置

操作运行时间
lenO(1)
is_emptyO(1)
addO(n)
minO(1)
remove_minO(1)

3 堆

3.1 堆的数据结构

堆是一颗二叉树 T ,该树在它的节点上存储了集合中的元组并且满足两个附加属性:关系属性 以存储键的形式在 T 中定义;结构属性 以树 T 自身形状的方式定义。
关系属性:

  • Heap-Order属性: 在堆 T 中,对于除了根的每个位置 p ,存储在 p 中的键值大于或等于存储在 p 的父节点的键值。

根据该属性容易知道 T 中从根到叶子的路径上的键值是以非递减顺序排列的,也就是说 一个最小的键总是存储在 T 的根节点中
由于效率的缘故,我们想让堆 T 的高度尽可能小,为此我们可以让堆 T 满足结构属性中的附加属性——它必须是完全二叉树——来满足这一需求。
在这里插入图片描述

  • 堆的高度: 堆 T 有 n 个元组,则它的高度为 h = ⌊ l o g   n ⌋ h=\lfloor log\ n \rfloor h=log n

3.2 使用堆实现优先级队列

min操作

堆的属性保证了树的根部元组有最小的键值。

在堆中增加一个元组

先将新节点放在树底层最右节点相邻的位置,(如果树的底层节点已满或堆为空,则应存放在新一层的最左位置上)

插入元组后向上冒泡

在这个操作后,树 T 为完全二叉树,但是它 可能破坏了heap-order属性 ,因此我们应该将 p 位置上的键值与 p 的父节点 q 上的键值进行比较,通过元组交换操作向上冒泡使堆再次满足heap-order属性。
在这里插入图片描述

在堆中删除键值最小的元组

为了不破坏堆的结构,元组被删除后,由来自 最后位置(即树最底层的最靠右的位置) 的元组所填充。

删除操作后向下冒泡

在这个操作后,树 T 为完全二叉树,但是它 可能破坏了heap-order属性 ,因此我们应该将 p 位置上的键值与 p 的 两个孩子节点中键值较小的那个节点 q 上的键值进行比较,通过元组交换操作向下冒泡使堆再次满足heap-order属性。
在这里插入图片描述

3.3 基于堆的优先级队列的分析

由于堆是一颗二叉树,因此可以采用 基于数组的二叉树表示 来实现堆。

利用堆实现的优先级队列 P 操作运行时间
len§O(1)
P.is_empty()O(1)
P.add()O(log n)
P.min()O(1)
P.remove_min()O(log n)
  • 堆 T 是完全二叉树,高度为O(log n)
  • 堆向上冒泡和向下冒泡执行交换的次数在最坏情况下等于 T 的高度(log n)。

3.4 自底向上构建堆*

略,详情可看书。

3.5 Python 的 heapq 模块

该模块提供对基于堆的优先级队列的支持。该模块不提供任何优先级队列类,而是提供一些函数,这些函数 把标准 Python 列表作为堆进行管理
Heapq 模块支持如下函数,假设这些函数调用之前,现有列表 L 已经满足 heap-order 属性:

函数描述复杂度
heappush(L,e)将元素 e 存入列表 L ,并重新调整列表以满足 heap-order 属性O(log n)
heappop(L)取出并返回列表 L 中拥有最小值的元素,并重新调整列表以满足 heap-order 属性O(log n)
heappushpop(L,e)将元素 e 存入列表 L,同时取出并返回列表 L 中拥有最小值的元素O(log n)
heapreplace(L,e)与 heappushpop 方法类似,但相当于在插入操作前执行 pop 操作O(log n)
heapify(L)改变未排序的列表,使其满足 heap-order 属性,这个函数使用自底向上的堆构造方法O(n)
nlargest(k,iterable)从一个给定的迭代中生成含有 k 个最大值的列表O(n + k log n) n为迭代的长度
nsmallest(k,iterable)从一个给定的迭代中生成含有 k 个最小值的列表O(n + k log n) n为迭代的长度

4 堆排序

堆排序算法分为两个阶段:

  1. 构造堆: 由于第 i 次 add 操作完成后堆有 i 个元组,所以第 i 次 add 操作的时间复杂度为 O(log i)。这一阶段整体的时间复杂度为O(n log n)(采用 自底向上堆构造的方法时间复杂度为O(n) )。
  2. 重复remove_min操作: 由于在第 j 次 remove_min 操作执行时堆中有 (n-j+1) 个元素,因此第 j 次 remove_min 操作的时间复杂度为 O(log(n-j+1))。这一阶段的时间复杂度为 O(n log n)。
  • 命题:假设集合 C 的任意两个元素能在 O(1) 时间内完成比较,则堆排序算法能在 O(n log n)时间内完成含有 n 个元素的集合 C 的排序。

实现原地堆排序

一般来说,除了存储要排序的对象的序列之外,如果只使用少量的内存,我们就说该算法为原地(in-place)算法。
如果要排列的集合 C 是通过基于数组的序列实现的,我们可以使用 C 的左半部分来存储 堆中的元组 ,并且使用 C 的右半部分来存储 序列的元素

  1. 在算法的第一阶段,我们从一个空堆开始,并从左向右移动堆与序列之间的边界,一次一步,直至全部元素入堆,此时 C 的右半部分为空。这就是 堆构建 的过程。
  2. 在算法的第二阶段,我们从一个空的序列开始,并从右向左移动堆与序列之间的边界,一次一步,在第 i 步(i = 1, …, n)将堆中当前最大值元素从堆中移除并将其存储到索引为 n-i 的位置上。这就是 重复remove_min 的过程,可将其看成一种“选择排序”。
    在这里插入图片描述

5 适应性优先级队列

增加两个方法:

  • remove(): 用来删除优先级队列中的任一元组
  • update(): 用新键替换元组现有的键

定位器

我们需要一种在优先级队列中找到用户元组的机制,因此提出 定位器(locator) 对象。 (定位器和位置不同,优先级队列的定位器并不代表结构中一个元素的具体位置) 对于一个优先级队列 P ,当执行 update 或者 remove 方法时,需要用户提供一个合适的定位器作为参数:

  • P.update(loc, k, v): 用定位器 loc 代替键和值作为元组的标识。
  • P.remove(loc): 从优先级队列中删除以 loc 标识的特定元组,并返回它的 (key, value) 对。

在具体实现时,可将定位器实例设计为 存储一个 key, value 和列表内元素的当前索引的元组 (key, value, index) 。然后用一个定位器序列表示堆,用户则会获得每个插入的元素的定位器实例的引用。注意,为了维护每个定位器实例在列表中的当前索引,我们需要 重写 swap() 方法 ,在每次 swap 操作,即元组交换时交换他们的index。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值