Py学习  »  Python

【数据结构Python描述】树堆(heap)简介和Python手工实现及使用树堆实现优先级队列

TakingCoding4Granted • 5 年前 • 1050 次点击  

【数据结构Python描述】优先级队列简介及Python手工实现 中,我们使用 位置列表 作为键值对形式记录的存储容器,通过两种策略分别给出了一个优先级队列实现:

  • UnsortedPriorityQueue :队列中的 记录不按照键的大小顺序排列
  • SortedPriorityQueue :队列中的 记录按照键的大小顺序排列

由此,在 比较这两种不同策略的实现 后,可以发现一个有趣的现象:

  • UnsortedPriorityQueue 中插入记录方法 add() 的最坏时间复杂度虽然为 O ( 1 ) O(1) O ( 1 ) ,但是查找(或同时删除)键最小的记录的方法 min() (或 remove_min() )的最坏时间复杂度为 O ( n ) O(n) O ( n )
  • SortedPriorityQueue 中虽然查找(或同时删除)键最小记录的方法 min() (或 remove_min() )的最坏时间复杂度为 O ( 1 ) O(1) O ( 1 ) ,但插入记录方法 add() 的最坏时间复杂度虽然为 O ( n ) O(n) O ( n )

实际上,由上述分析可知,如果实际要求 add() min() remove_min() 三个方法的最坏时间复杂度均低于 O ( n ) O(n) O ( n ) ,则上述两种实现方式均无法满足要求。

为了实现上述需求,则需要使用本文介绍的一种新的数据结构—— 二叉堆 。更具体地,可以使用本文将介绍的 二叉堆 来实现优先级队列,以达到记录插入和删除的最坏时间复杂度均为 l o g ( n ) log(n) l o g ( n )

一、堆数据结构简介

1. 定义

二叉堆 :堆首先是一种特殊的二叉树 T ,该二叉树 T 的每个结点处保存了一条键值对形式的记录,除此之外该二叉树 T 还具有以下两条性质:

  • 堆序性质 :如果 p 代表堆 T 任意一个除根结点的结点位置,则对于保存在位置 p 处的记录,其键 大于或等于 p 的父结点处记录的键;
  • 完全二叉树性质 :堆 T 是一个 完全二叉树 ,即如果二叉树的高度为 h ,则树的第 0 0 0 1 1 1 2 2 2 . . . ... . . . h − 1 h-1 h 1 层均有 2 i 2^i 2 i (其中 0 ≤ i ≤ h − 1 0\le{i}\le{h-1} 0 i h 1 )个结点,且第 h h h 层的结点都保存在尽量靠左的位置。

下面给出一个满足上述堆定义的例子:

在这里插入图片描述

需要注意的是:

  • 堆序性质 保证了沿着从根结点开始到叶子结点结束的任意一条路径,路径上每一个位置处的键(非严格)单调递增,且键最小的记录在根结点处;
  • 本文所描述的数据结构 和计算机内存中的堆无任何关系,请读者注意区分;
  • 由于 数据结构堆实际是一种特殊的完全二叉树 ,因此本文后续将不区分堆和完全二叉树的概念。

2. 性质

高度 :对于保存 n n n 条记录的堆 T ,其高度为 ⌊ l o g ( n ) ⌋ \lfloor log(n) \rfloor l o g ( n ) ,其中 ⌊ x ⌋ \lfloor x \rfloor x 表示对 x x x 向下取整。
证明 :由于堆T是一个 完全二叉树 ,则其从第 0 0 0 层到第 h − 1 h-1 h 1 层的结点数目为 1 + 2 + 2 2 + ⋅ ⋅ ⋅ + 2 h − 1 = 2 h − 1 1+2+2^2+\cdot\cdot\cdot+2^{h-1}=2^h-1 1 + 2 + 2 2 + + 2 h 1 = 2 h 1 ,而第 h h h 层的结点数目最少为 1 1 1 个,最多为 2 h 2^h 2 h 个,因此:
( 2 h − 1 ) + 1 ≤ n ≤ ( 2 h − 1 ) + 2 h (2^h-1)+1\le{n}\le(2^h-1)+2^h ( 2 h 1 ) + 1 n ( 2 h 1 ) + 2 h
即:
2 h ≤ n ≤ 2 h + 1 − 1 2^h\le{n}\le2^{h+1}-1 2 h n 2 h + 1 1
对上面表达式的左边部分同时两边取对数得 h ≤ l o g ( n ) h\le{log(n)} h l o g ( n ) ,对上面表达式右边部分移项后两边取对数再移项后得 h ≥ ( l o g ( n + 1 ) − 1 ) h\ge({log(n+1)-1}) h ( l o g ( n + 1 ) 1 )
即:
( l o g ( n + 1 ) − 1 ) ≤ h ≤ l o g ( n ) (log(n+1)-1)\le{h}\le{log(n)} ( l o g ( n + 1 ) 1 ) h l o g ( n )
又由于 h h h 为正整数,则必有 h = ⌊ l o g ( n ) ⌋ h=\lfloor log(n) \rfloor h = l o g ( n )

二、堆数据结构应用

由上述关于堆 T 的高度性质可知,如果对 T 进行的修改类操作(如添加、删除类操作)可实现 正比于其高度 h h h 的时间复杂度,则对于有 n n n 条记录的 T 这写修改类操作的时间复杂度为 l o g ( n ) log(n) l o g ( n )

自然地,如果可以使用堆 T 作为保存优先级队列的所有 键值对形式记录 ,则基于此实现的优先级队列,其 add() min() remove_min() 等操作有望实现 l o g ( n ) log(n) l o g ( n ) 的最坏时间复杂度。

实际上,如果使用堆实现的优先级队列,对于其 ADT中的所有方法

  • __len__() is_empty() 方法的实现非常简单,仅需返回或判断底层完全二叉树的结点个数属性即可;
  • min() 方法的实现也很直接,因为上述 堆序性质 保证了键最小的记录保存在根结点处;
  • add() remove_min() 两个方法的实现较为复杂,因为要保证在记录条目增删前后仍然保持堆的 堆序性质 完全二叉树性质

因此,下面着重 从理论上分析 如何基于堆来实现优先级队列的 add() remove_min() 两个方法:

1. add(k, v)

首先考虑将键值对形式的记录保存在堆的结点中,但需要注意的是该操作之后需要同时保证 堆序性质 完全二叉树性质

完全二叉树性质保证

为确保不违背 完全二叉树性质 add(k, v) 方法需要将结点保存在 二叉树最底层最右侧结点的右侧紧邻位置 或者 新一层结点的最左侧位置

下图以满足上述堆定义的图为例,假设现在需要向其中增加一条键为 2 ,值为 'T' 的记录 (2, 'T') ,则图(a)和(b)分别表示执行 add(2, 'T') 之前和之后(部分)堆的状态:

在这里插入图片描述

堆序性质保证

上述步骤虽然确保了满足堆的 完全二叉树性质 ,但是当调用 add(k, v) 方法之前堆不为空,则有可能仅完成上述步骤将违背堆的 堆序性质 。原因在于,如果假设插入 完全二叉树 的结点的位置为 p ,且该结点的父结点位置为 q ,则有可能 k p < k q k_p\lt{k_q} k p < k q

针对上述可能,我们需要交换位置 p q 处结点的键值对形式记录,此操作后原本位置 p 处的记录将上移一层,如果此时完全二叉树已满足 堆序性质 ,则插入操作才算完成,否则交换操作继续。

针对上述分析,有如下图的示例:

  • 图(c)和(d)表示继图(b)状态后,完成记录 (20, 'B') (2, 'T') 交换的过程和结果;
  • 图(e)和(f)、图(g)和(h)分别表示继续进行结点间记录交换的过程和结果。

在这里插入图片描述

实际上,上述一系列操作也称为 自堆底向上冒泡 (up-heap bubbling)算法,而且在最坏情况下(如上图所示),此算法将使得新结点处的记录一直被交换至根结点处,此时交换操作的次数为堆的高度即 l o g ( n ) log(n) l o g ( n ) n n n 为堆的总结点数)。

2. remove_min()

根据堆的定义,使用堆 T 实现的优先级队列中键最小的记录必然保存在根结点处,但实现 remove_min() 方法时不能直接将根结点删除,因为此时会形成两个互不相连的子树,即破坏了堆的 完全二叉树性质

完全二叉树性质保证

为解决上述问题,可以:

  • 先删除堆最底层最右侧结点并返回保存的记录;
  • 然后使用上述返回的记录替换根结点处的记录。

下图为上述操作的示例:

在这里插入图片描述

堆序性质保证

完成上述操作后仍存在的问题是,此时树虽然是完全二叉树,但可能并不是堆,因为该二叉树可能不满足 堆序性质

实际上,如果 删除堆的最底层最右侧结点后 ,完全二叉树中:

  • 仅有一个结点 ,则该完全二叉树同时也是堆;
  • 不止一个结点 ,则令 p 表示堆 T 的根结点位置,在以下两种情况下:
    • p 只有左子结点,且其位置为 c
    • p 有左、右两个子结点,令其 键较小的子结点 的位置为 c

如果 k p > k c k_p\gt{k_c} k p > k c ,则类似上述实现 add(k, v) ,此时完全二叉树不是堆,需要交换位置 p c 处的记录直至整个完全二叉树满足 堆序性质 。下面系列图例表达了这一具体过程:

在这里插入图片描述
实际上,与之前 自堆底向上冒泡 (up-heap bubbling)算法相对,上述过程也称为 自堆顶向下冒泡 (down-heap bubbling),且类似地该算法的最坏时间复杂度为 l o g ( n ) log(n) l o g ( n ) (其中 n n n 为堆中元素个数)。

三、堆数据结构实现

1. 实现方式选择

由于堆实际上是一种特殊的二叉树,则你可能第一反应是根据 【数据结构Python描述】树的简介、基本概念和手动实现一个二叉树 中定义专门的结点类,并以结点的链式方式来实现堆。

实际上,通过上述方式实现堆本身并没有问题,但问题在于本文在开头引出的问题及其解决方案,即本文实现堆的目的是使用堆来进一步实现更加优化的优先级队列(相比于之前 使用位置列表实现优先级队列 ),因此使用 基于结点链式结构 实现堆有以下主要缺陷:

  • 实现起来较为复杂
  • 进一步实现的优先级队列的部分操作时间复杂度高 ,对于优先级队列的 add() remove_min() 方法,其实现依赖于先找到堆中的最后一个结点,而这对于 基于结点链式结构 实现的堆来说时间复杂度为 O ( n ) O(n) O ( n )

实际上,除了使用 基于结点链式结构 来实现二叉树,另一种方式是使用列表作为存储每一个结点元素的容器来实现。

具体地,假设完全二叉树 T 的所有结点均保存在列表 A 中,假设函数 f ( p ) f(p) f ( p ) 将结点位置 p 映射为列表 A 中的正整数索引,则 T 中所有结点在列表 A 中对应的索引为:

  • 如果位置 p 处为根结点,则 f ( p ) = 0 f(p)=0 f ( p ) = 0
  • 如果 p 是位置 q 处结点的左子结点的位置,则 f ( p ) = 2 f ( q ) + 1 f(p)=2f(q)+1 f ( p ) = 2 f ( q ) + 1
  • 如果 p 是位置 q 处结点的右子结点的位置,则 f ( p ) = 2 f ( q ) + 2 f(p)=2f(q)+2 f ( p ) = 2 f ( q ) + 2

基于上述定义,则将上述满足堆定义的完全二叉树转换为列表存储形式为:

在这里插入图片描述

由上述可知:

  • 将保存业务元素的结点进行链式存储,其优势是可以较为直观地体现完全二叉树的结构,劣势是访问任意结点的遍历较为耗时;
  • 将完全二叉树各个结点的业务元素保存在列表中,其优势是根据列表索引访问任意结点的业务元素,劣势是完全二叉树的结构是隐式的。

2. 数据结构实现

下面结合上述理论分析,继承 优先级队列抽象基类 PriorityQueueBase ,通过使用普通列表作为堆中结点的存储容器,进而使用堆保存键值对形式记录,实现高效的优先级队列 HeapPriorityQueue

2.1 堆的ADT

下面是堆的常用ADT方法,需要注意的是所有方法均为非公共方法,因为本文最终目的是实现一个面向用户的高效优先级队列,而用户无需了解或使用底层的堆是如何实现的。

方法名称 方法描述
_parent(j) 对于堆 T ,如果其某结点处的业务元素保存在列表的索引 j 处,返回其父结点处业务元素在列表中的索引。
_left(j) 对于堆 T ,如果其某结点处的业务元素保存在列表的索引 j 处,返回其左子结点处业务元素在列表中的索引。
_right(j) 对于堆 T ,如果其某结点处的业务元素保存在列表的索引 j 处,返回其右子结点处业务元素在列表中的索引。
_has_left(j) 对于堆 T ,如果其某结点处的业务元素保存在列表的索引 j 处,如果 该结点有左子结点 则返回 True ,否则返回 False
_has_right(j) 对于堆 T ,如果其某结点处的业务元素保存在列表的索引 j 处,如果 该结点有右子结点 则返回 True ,否则返回 False
_swap(i, j) 对于 完全二叉树 T ,假设其一对父子结点的业务元素分别保存在列表的索引 i j 处,如果父子结点的业务元素违背了 堆序性质 则交换这对结点的业务元素。
_upheap(j) 自堆底向上冒泡 (up-heap bubbling)算法实现。
_downheap(j) 自堆顶向下冒泡 (up-heap bubbling)算法实现。

2.2 堆方法实现

_parent()

根据堆中子结点和其父结点的业务元素在列表中的索引关系可得:

def _parent(self, j):
    """
    返回父结点处业务元素在列表中的索引
    :param j: 任意结点处的业务元素在列表中的索引
    :return: 父结点处业务元素在列表中的索引
    """
    return (j - 1) // 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_left()

该方法实现类似 _parent() 方法:

def _left(self, j):
    """
    返回左子结点处业务元素在列表中的索引
    :param j: 任意结点处的业务元素在列表中的索引
    :return: 左子结点处业务元素在列表中的索引
    """
    return 2 * j + 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_right()

该方法实现类似 _parent() 方法:

def _right(self, j):
    


    
"""
    返回右子结点处业务元素在列表中的索引
    :param j: 任意结点处的业务元素在列表中的索引
    :return: 右子结点处业务元素在列表中的索引
    """
    return 2 * j + 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_has_left()

该方法实现依赖于 _left() 方法:

def _has_left(self, j):
    """
    如结点有左子结点则返回True,否则返回False
    :param j: 任意结点处的业务元素在列表中的索引
    :return: 判断结点是否有左子结点的Boolean结果
    """
    return self._left(j) < len(self._data)  # 确保列表索引不越界
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_has_right()

该方法实现依赖于 _right() 方法:

def _has_right(self, j):
    """
    如结点有右子结点则返回True,否则返回False
    :param j: 任意结点处的业务元素在列表中的索引
    :return: 判断结点是否有右子结点的Boolean结果
    """
    return self._right(j) < len(self._data)  # 确保列表索引不越界
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_swap()

实现该方法仅需交换列表两个索引处的业务元素即可:

def _swap(self, i, j):
    """
    交换一对父子结点的业务元素
    :param i: 业务元素在列表中的索引
    :param j: 业务元素在列表中的索引
    :return: None
    """
    self._data[i], self._data[j] = self._data[j], self._data[i]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

_upheap()

该方法实现遵照本文 前述理论分析 以确保向堆中插入新业务元素后不破坏其 堆序性质

def _upheap(self, j):
    """
    自堆底向上冒泡算法
    :param j: 堆底结点处业务元素在列表中的索引
    :return: None
    """
    parent = self._parent(j)
    if j > 0 and self._data[j] < self._data[parent]:
        self._swap(j, parent)
        self._upheap(parent)  # 递归调用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

_downheap()

该方法实现遵照本文 前述理论分析 以确保删除堆中根结点业务元素后不破坏其 堆序性质

def _downheap(self, j):
    """
    自堆顶向下冒泡算法
    :param j: 结点处业务元素在列表中的索引
    :return: None
    """
    if self._has_left(j):
        left = self._left(j)
        small_child = left
        # 如果有左、右两个子结点,令small_child引用键较小子结点的业务元素在列表中的索引
        if self._has_right(j):
            right = self._right(j)
            if self._data[right] < self._data[left]:
                small_child = right
        # 至少根结点有左子结点时,才有可能self虽然是完全二叉树但不是堆
        if self._data[small_child] < self._data[j]:
            self._swap(j, small_child)
            self._downheap(small_child)  # 递归调用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.3 优先级队列方法实现

下面基于上述堆的各ADT方法 实现优先级队列

__init__()

实现初始化方法仅需指定用于保存优先级队列记录的容器的空列表:

def __init__(self):
    """将创建的优先级队列初始化为空"""
    self._data = list()
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

__len__()

实现该方法仅需返回列表长度即可:

def __len__(self):
    """返回优先级队列中的记录条目数"""
    return len(self._data)
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

add()

实现该方法需要:

  • 先将新键值对封装为记录对象后追加至列表尾部;
  • 然后调用 自堆底向上冒泡 算法的实现 _upheap() 确保完全二叉树满足 堆序性质
def add(self, key, value):
    """向优先级队列中插入一条key-value记录"""
    self._data.append(self._Item(key, value))  # 新记录条目插入并确保完全二叉树性质
    self._upheap(len(self._data) - 1)  # 确保满足堆序性质


    

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

min()

实现该方法仅需返回根结点处的键值对即可:

def min(self):
    """返回(但不删除)优先级队列中键最小的记录,如优先级队列此时为空则抛出异常"""
    if self.is_empty():
        raise Empty('优先级队列为空!')
    item = self._data[0]
    return item.key, item.value
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

remove_min()

实现该方法需要:

  • 先判断优先级队列是否为空;
  • 其次交换根结点和堆最底层最右侧结点处记录;
  • 然后删除堆最底层最右侧结点,并保存该结点处记录(经上述交换操作后,此记录的键为堆中最小的);
  • 再调用 自堆顶向下冒泡 算法的实现 _downheap() 确保经过交换操作后完全二叉树满足 堆序性质
  • 最后返回键最小记录的键和值。
def remove_min(self):
    """回并删除优先级队列中键最小的记录,如优先级队列此时为空则抛出异常"""
    if self.is_empty():
        raise Empty('优先级队列为空!')
    self._swap(0, len(self._data) - 1)  # 将根结点处键最小记录交换至完全二叉树最底层最右侧结点处
    item = self._data.pop()
    self._downheap(0)  # 确保完全二叉树满足堆序性质,即确定需保存在根结点处的键最小记录
    return item.key, item.value
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.4 基于堆的优先级队列方法复杂度分析

针对基于堆实现的优先级队列,其 ADT方法 的时间复杂度如下表所示,其中 n n n 表示方法执行时优先级队列中记录条目数:

方法名称 时间复杂度
__len__() O ( 1 ) O(1) O ( 1 )
is_empty() O ( 1 ) O(1) O ( 1 )
min() O ( 1 ) O(1) O ( 1 )
add() O ( l o g ( n ) ) O(log(n)) O ( l o g ( n ) ) 1
remove_min() O ( l o g ( n ) ) O(log(n)) O ( l o g ( n ) ) 1

实际上,上述表格的分析基于以下事实:

  • T 具有 n n n 个结点,每个结点保存一个键值对的引用;
  • T 的高度为 l o g ( n ) log(n) l o g ( n ) ,因为 T 为完全二叉树;
  • min() 方法的最坏时间复杂度为 O ( 1 ) O(1) O ( 1 ) ,因为根结点保存了键最小的记录;
  • 实现 add() remove_min() 方法需要找到键最小的记录,对于基于列表实现的堆和结点链式存储的堆,该操作的时间复杂度分别为 O ( 1 ) O(1) O ( 1 ) O ( l o g ( n ) ) O(log(n)) O ( l o g ( n ) )
  • 在最坏情况下, 自堆底向上冒泡 自堆顶向下冒泡 的次数等于堆的高度。

至此,我们实现了本文开头期望的均能高效执行增( add(k, v) )、删( remove_min() )、查( min() )操作的优先级队列。

四、完整测试代码

完整测试代码如下,其中使用了 【数据结构Python描述】优先级队列简介及Python手工实现 中的优先级队列抽象基类 PriorityQueueBase

from priority_queue import PriorityQueueBase


class Empty(Exception):
    """尝试对空优先级队列进行删除操作时抛出的异常"""
    pass


class HeapPriorityQueue(PriorityQueueBase):
    """使用堆存储键值对形式记录的优先级队列"""

    def __init__(self):
        """将创建的优先级队列初始化为空"""
        self._data = list()

    def _parent(self, j):
        """
        返回父结点处业务元素在列表中的索引
        :param j: 任意结点处的业务元素在列表中的索引
        :return: 父结点处业务元素在列表中的索引
        """
        return (j - 1) // 2

    def _left(self, j):
        """
        返回左子结点处业务元素在列表中的索引
        :param j: 任意结点处的业务元素在列表中的索引
        :return: 左子结点处业务元素在列表中的索引
        """
        return 2 * j + 1

    def _right(self, j):
        """
        返回右子结点处业务元素在列表中的索引
        :param j: 任意结点处的业务元素在列表中的索引
        :return: 右子结点处业务元素在列表中的索引
        """
        return 2 * j + 2

    def _has_left(self, j):
        """
        如结点有左子结点则返回True,否则返回False
        :param j: 任意结点处的业务元素在列表中的索引
        :return: 判断结点是否有左子结点的Boolean结果
        """
        return self._left(j) < len(self._data)  # 确保列表索引不越界

    def _has_right(self, j):
        """
        如结点有右子结点则返回True,否则返回False
        :param j: 任意结点处的业务元素在列表中的索引
        :return: 判断结点是否有右子结点的Boolean结果
        """
        return self._right(j) < len(self._data)  # 确保列表索引不越界

    def _swap(self, i, j):
        """
        交换一对父子结点的业务元素
        :param i: 业务元素在列表中的索引
        :param j: 业务元素在列表中的索引
        :return: None
        """
        self._data[i], self._data[j] = self._data[j], self._data[i]

    def _upheap(self, j):
        """
        自堆底向上冒泡算法
        :param j: 结点处业务元素在列表中的索引
        :return: None
        """
        parent = self._parent(j)
        if j > 0 and self._data[j] < self._data[parent]:
            self._swap(j, parent)
            self._upheap(parent)  # 递归调用

    def _downheap(self, j):
        """
        自堆顶向下冒泡算法
        :param j: 结点处业务元素在列表中的索引
        :return: None
        """
        if self._has_left(j):
            left = self._left(j)
            small_child = left
            # 如果有左、右两个子结点,令small_child引用键较小子结点的业务元素在列表中的索引
            if self._has_right(j):
                right = self


    
._right(j)
                if self._data[right] < self._data[left]:
                    small_child = right
            # 至少根结点有左子结点时,才有可能self虽然是完全二叉树但不是堆
            if self._data[small_child] < self._data[j]:
                self._swap(j, small_child)
                self._downheap(small_child)  # 递归调用

    def __len__(self):
        """返回优先级队列中的记录条目数"""
        return len(self._data)

    def __iter__(self):
        """生成优先级队列中所有记录的一个迭代"""
        for each in self._data:
            yield each

    def add(self, key, value):
        """向优先级队列中插入一条key-value记录"""
        self._data.append(self._Item(key, value))  # 新记录条目插入并确保完全二叉树性质
        self._upheap(len(self._data) - 1)  # 确保满足堆序性质

    def min(self):
        """返回(但不删除)优先级队列中键最小的记录,如优先级队列此时为空则抛出异常"""
        if self.is_empty():
            raise Empty('优先级队列为空!')
        item = self._data[0]
        return item.key, item.value

    def remove_min(self):
        """回并删除优先级队列中键最小的记录,如优先级队列此时为空则抛出异常"""
        if self.is_empty():
            raise Empty('优先级队列为空!')
        self._swap(0, len(self._data) - 1)  # 将根结点处键最小记录交换至完全二叉树最底层最右侧结点处
        item = self._data.pop()
        self._downheap(0)  # 确保完全二叉树满足堆序性质,即确定需保存在根结点处的键最小记录
        return item.key, item.value


if __name__ == '__main__':
    heap_queue = HeapPriorityQueue()
    print(heap_queue.is_empty())  # True

    heap_queue.add(4, 'C')
    heap_queue.add(6, 'Z')
    heap_queue.add(7, 'Q')
    heap_queue.add(5, 'A')
    print(heap_queue)  # [(4, 'C'), (5, 'A'), (7, 'Q'), (6, 'Z')]

    heap_queue.add(2, 'T')
    print(heap_queue)  # [(2, 'T'), (4, 'C'), (7, 'Q'), (6, 'Z'), (5, 'A')]

    print(heap_queue.remove_min())  # (2, 'T')
    print(heap_queue.remove_min())  # (4, 'C')
    print(heap_queue)  # [(5, 'A'), (6, 'Z'), (7, 'Q')]
    print


    
(heap_queue.min())  # (5, 'A')
    print(heap_queue.is_empty())  # False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143

  1. 经摊销后的时间复杂度 ↩︎ ↩︎

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/74056