在文章【数据结构Python描述】队列和双端队列简介及其高效率版本Python实现中,我们引入并实现了队列这种数据结构,对其进行元素的增删遵循先进先出的原则,并以现实中排队享受服务作为类比。
然而,上述普通队列的模型并不能完全满足实际的需求,例如:你仅是某银行的普通等级客户,当你去银行取号办理业务时,可能虽然后面的人来得比你晚,但是由于他可能是该行的白金等级客户,那么很有可能他会在你之前被叫号。
因此,对于上述情况,取号的早晚并不会被当作判断下一个被叫号的唯一标准,更重要是看客户的等级,本文就将介绍并实现用以描述这类场景的数据结构——优先级队列。实际上,在上述案例中,客户等级就是优先级。
优先级队列:优先级队列是指这样一种抽象数据类型,即其中包含一系列具有不同优先级的元素,除了像普通队列可以在队尾插入元素外,优先级队列可以在任意位置插入元素,最重要的是在每次从优先级队列中移除的元素都具有最高的优先级。
对于优先级队列,在向其中插入一个元素时,用户就会同时以键的形式为其指定一个优先级,而键值最小的元素会在下一次元素出队操作时被从优先级队列中移除。例如:如果元素键值为1的优先级要高于元素键值为2的元素。
需要注意的是,尽管通常元素的优先级即键都用整数来表示,但原则上任意两个通过同一个类实例化后得到的Python对象a
、b
,只要对于a < b
运算有一致的含义,则这些对象都可以作为代表元素优先级的键。
为了后续实现优先级队列,这里将一个元素和其优先级表示为一个键值对的形式,而且在本文实现的优先级队列ADT包含以下几个方法:
p.add(k, v)
:将键k
和表示元素的值v
作为一个记录插入优先级队列p
中;p.min()
:返回一个元组(k, v)
,k
和v
分别代表优先级队列p
中一条记录的键和值;p.remove_min()
:将优先级队列p
中键值k
最小的记录删除并返回一个元组(k, v)
,如果此时p
为空则抛出异常;p.is_empty()
:如果优先级队列p
不包含任何记录则返回True
;__len__()
:返回优先级队列p
中记录条目数。需要注意的是,一个优先级队列中的多条记录可能会有相同的键,在这种情况下min()
和remove_min()
两个方法将会随机返回有最小键那条记录的元素。
下面是使用上述ADT中的方法对一个优先级队列进行一系列操作后预期的结果:
操作 | 返回值 | 优先级队列 |
---|---|---|
p.add(5, A) |
None |
{(5, A)} |
p.add(9, C) |
None |
{(5, A), (9, C)} |
p.add(3, B) |
None |
{(3, B), (5, A), (9, C)} |
p.add(7, D) |
None |
{(3, B), (5, A), (7, D), (9, C)} |
p.min() |
(3, B) |
{(3, B), (5, A), (7, D), (9, C)} |
p.remove_min() |
(3, B) |
{(5, A), (7, D), (9, C)} |
p.remove_min() |
(5, A) |
{(7, D), (9, C)} |
len(p) |
2 | {(7, D), (9, C)} |
p.remove_min() |
(7, D) |
{(9, C)} |
p.remove_min() |
(9, C) |
{} |
p.is_empty() |
True |
{} |
本节将使用【数据结构Python描述】位置列表简介与Python版手工实现中的位置列表存储优先级队列中以键值对形式存储的每条记录。并且,基于队列中的记录是否按照键排序,下面提供两种实现方式。
对于优先级队列的实现,首先需要考虑的是如何保存优先级队列一条记录中的键k
和值v
。实际上,早在之前实现单链表时,我们就遇到过类似情况,即单链表的每一个结点也需要引用两个对象,即:业务对象元素和下一个结点。
这里考虑仿照单链表实现中定义的结点类_Node
定义一个_Item
类,其实例属性key
引用一条记录的键,实例属性value
引用同一条记录的值,不同的是,在优先级队列的实现类中嵌套定义类_Item
。
实际上,由于后续我们将根据优先级队列中的记录是否按照键的大小进行排序提供两种不同实现,因此为提高代码复用程度并建立不同实现之间的联系,下面仿照树的实现先定义一个包含两种实现共有方法的抽象基类PriorityQueueBase
:
from abc import ABCMeta, abstractmethod
class PriorityQueueBase(metaclass=ABCMeta):
"""优先级队列的基类"""
class _Item:
"""用于表示优先级队列中一条记录的类"""
def __init__(self, k, v):
self.key = k
self.value = v
def __lt__(self, other):
"""
重载运算符<,使得可按照两个_Item实例对象的属性key来比较这两个实例对象
:param other: _Item的另一个实例对象
:return: Boolean
"""
return self.key < other.key
def __repr__(self):
"""返回_Item对象的无歧义字符串表示形式"""
return '('f'{self.key!r}, {self.value!r})'
@abstractmethod
def __len__(self):
"""
具体子类中需实现的抽象方法,用于具体子类的实例对象支持len(obj)语法
:return: 对象记录条目数
"""
@abstractmethod
def add(self, key, value):
"""
具体子类中需实现的抽象方法,用于向优先级队列中插入一条包含键和值的记录
:param key: 键
:param value: 值
:return: None
"""
@abstractmethod
def min(self):
"""
具体子类中需实现的抽象方法,用于返回(但不删除)优先级队列中键最小的记录
:return: 优先级队列中键最小的记录
"""
@abstractmethod
def remove_min(self):
"""
具体子类中需实现的抽象方法,用于返回并删除优先级队列中键最小的记录
:return: 优先级队列中键最小的记录
"""
def is_empty(self):
"""判断优先级队列是否为空"""
return len(self) == 0
需要注意的是:
下面给出继承抽象基类PriorityQueueBase
实现的第一个优先级队列UnsortedPriorityQueue
,其中的所有键值对记录均保存在【数据结构Python描述】位置列表简介与Python版手工实现中实现的位置列表PositionalList
中,且每次向队列中添加一条记录时直接向队尾添加。
__init__()
首先,对于初始化方法,由于优先级队列的使用者无需关心底层实现,因此这里将用于存储优先级队列记录的实例属性_data
定义为非公有的且引用一个位置列表的实例对象:
def __init__(self):
"""创建一个空的优先级队列,其记录底层由位置列表保存"""
self._data = PositionalList()
_find_min()
由于min()
和remove_min()
两个方法均需要先找到队列中键最小的那条记录,因此为提高代码复用程度,这里定义这样一个非公有方法,该方法返回队列中键最小记录所在位置:
def _find_min(self):
"""查找并返回优先级队列中键最小的记录,并返回其在位置列表中的位置"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
small = self._data.first()
walk = self._data.after(small)
while walk is not None:
if walk.element() < small.element():
small = walk
walk = self._data.after(walk)
return small
__len__()
该方法只需直接返回底层位置列表容器的元素个数:
def __len__(self):
"""支持实例对象使用len(obj)语法返回obj长度的方法"""
return len(self._data)
add(k, v)
该方法只需:
key
-value
封装为_Item
的一个实例;add_last()
方法将该条记录插入位置列表中即可:def add(self, key, value):
"""将键值对key-value封装为一条记录插入位置列表最后位置"""
self._data.add_last(self._Item(key, value))
min()
该方法只需:
_find_min()
得到键最小记录的位置;def min(self):
"""返回(但不删除)优先级队列中键最小的记录"""
p = self._find_min()
item = p.element()
return item.key, item.value
remove_min()
实现类似min()
,只是在返回键最小记录的键和值之前要删除该条记录:
def remove_min(self):
"""返回并删除优先级队列中键最小的记录"""
p = self._find_min()
item = self._data.delete(p)
return item.key, item.value
下面给出继承抽象基类PriorityQueueBase
实现的第二个优先级队列SortedPriorityQueue
,其中的所有键值对记录也均保存在【数据结构Python描述】位置列表简介与Python版手工实现中实现的位置列表PositionalList
中,但与上述实现每次向队列中添加一条记录时都直接向队尾添加不同的是,下面的实现中每次添加记录的操作完成后都确保该记录插入后队列中记录按键的大小升序排列。
__init__()
实现同上述UnsortedPriorityQueue
中的同名方法:
def __init__(self):
"""创建一个空的优先级队列,其所有记录在底层由位置列表保存"""
self._data = PositionalList()
__len__()
实现同上述UnsortedPriorityQueue
中的同名方法:
def __len__(self):
"""支持优先级队列实例对象使用len(obj)语法返回obj长度的方法"""
return len(self._data)
add(k, v)
同上述UnsortedPriorityQueue
中的同名方法实现不同的是,这里在向优先级队列插入一条键值对形式的记录后都保证所有记录是按照键大小从左到右升序排列:
def add(self, key, value):
"""将键值对key-value封装为一条记录插入位置列表,保证该记录插入后队列中记录按键的大小升序排列"""
item = self._Item(key, value) # 根据键值对key-value创建一条记录
walk = self._data.last() # 将辅助指针初始化为引用当前队列最后一条记录
while walk is not None and item < walk.element(): # 遍历队列,保证该记录插入后队列中记录按键的大小升序排列
walk = self._data.before(walk)
if walk is None: # 如果新记录的键小于当前队列所有键,则在队列头部插入记录
self._data.add_first(item)
else: # 否则在辅助指针引用的记录后插入记录
self._data.add_after(walk, item)
min()
实现类似上述UnsortedPriorityQueue
中的同名方法:
def min(self):
"""返回(但不删除)优先级队列中键最小的记录"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
p = self._data.first()
item = p.element()
return item.key, item.value
remove_min()
实现类似上述UnsortedPriorityQueue
中的同名方法:
def remove_min(self):
"""返回并删除优先级队列中键最小的记录"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
item = self._data.delete(self._data.first())
return item.key, item.value
对于上述两种不同的优先级队列实现,其ADT方法的时间复杂度对比如下表所示:
方法 | UnsortedPriorityQueue |
SortedPriorityQueue |
---|---|---|
__len__ |
O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
is_empty |
O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
add |
O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
min |
O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
remove_min |
O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
上述时间复杂度分析成立的前提是:用于存储优先级队列记录的位置列表底层使用双向链表实现。
下面是对上述实现的UnsortedPriorityQueue
和SortedPriorityQueue
中的ADT方法进行测试的完整代码:
# priority_queue.py
from abc import ABCMeta, abstractmethod
from positional_list import PositionalList
class Empty(Exception):
"""尝试对空优先级队列进行删除操作时抛出的异常"""
pass
class PriorityQueueBase(metaclass=ABCMeta):
"""优先级队列的基类"""
class _Item:
"""用于表示优先级队列中一条记录的类"""
def __init__(self, k, v):
self.key = k
self.value = v
def __lt__(self, other):
"""
重载运算符<,使得可按照两个_Item实例对象的属性key来比较这两个实例对象
:param other: _Item的另一个实例对象
:return: Boolean
"""
return self.key < other.key
def __repr__(self):
"""返回_Item对象的无歧义字符串表示形式"""
return '('f'{self.key!r}, {self.value!r})'
@abstractmethod
def __iter__(self):
"""具体子类中需实现的抽象方法,用于生成优先级队列中所有记录的一个迭代"""
@abstractmethod
def __len__(self):
"""
具体子类中需实现的抽象方法,用于具体子类的实例对象支持len(obj)语法
:return: 对象记录条目数
"""
@abstractmethod
def add(self, key, value):
"""
具体子类中需实现的抽象方法,用于向优先级队列中插入一条包含键和值的记录
:param key: 键
:param value: 值
:return: None
"""
@abstractmethod
def min(self):
"""
具体子类中需实现的抽象方法,用于返回(但不删除)优先级队列中键最小的记录
:return: 优先级队列中键最小的记录
"""
@abstractmethod
def remove_min(self):
"""
具体子类中需实现的抽象方法,用于返回并删除优先级队列中键最小的记录
:return: 优先级队列中键最小的记录
"""
def __str__(self):
"""
返回对象的字符串表示形式,使得可以使用print(obj)语法打印对象obj得到直观的字符串
:return: 对象的字符串表示形式
"""
return str(list(self))
def is_empty(self):
"""判断优先级队列是否为空"""
return len(self) == 0
class UnsortedPriorityQueue(PriorityQueueBase):
"""记录不按键的大小排序的优先级队列具体实现类"""
def __init__(self):
"""创建一个空的优先级队列,其所有记录在底层由位置列表保存"""
self._data = PositionalList()
def __iter__(self):
"""生成优先级队列中所有记录的一个迭代"""
cursor = self._data.first()
while cursor is not None:
item = cursor.element()
yield item.key, item.value
cursor = self._data.after(cursor)
def _find_min(self):
"""查找并返回优先级队列中键最小的记录,并返回其在位置列表中的位置"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
small = self._data.first()
walk = self._data.after(small)
while walk is not None:
if walk.element() < small.element():
small = walk
walk = self._data.after(walk)
return small
def __len__(self):
"""支持优先级队列实例对象使用len(obj)语法返回obj长度的方法"""
return len(self._data)
def add(self, key, value):
"""将键值对key-value封装为一条记录插入位置列表最后位置"""
self._data.add_last(self._Item(key, value))
def min(self):
"""返回(但不删除)优先级队列中键最小的记录"""
p = self._find_min()
item = p.element()
return item.key, item.value
def remove_min(self):
"""返回并删除优先级队列中键最小的记录"""
p = self._find_min()
item = self._data.delete(p)
return item.key, item.value
class SortedPriorityQueue(PriorityQueueBase):
"""记录按照键的大小排序的优先级队列具体实现类"""
def __init__(self):
"""创建一个空的优先级队列,其所有记录在底层由位置列表保存"""
self._data = PositionalList()
def __iter__(self):
"""生成优先级队列中所有记录的一个迭代"""
cursor = self._data.first()
while cursor is not None:
item = cursor.element()
yield item.key, item.value
cursor = self._data.after(cursor)
def __len__(self):
"""支持优先级队列实例对象使用len(obj)语法返回obj长度的方法"""
return len(self._data)
def add(self, key, value):
"""将键值对key-value封装为一条记录插入位置列表,保证该记录插入后队列中记录按键的大小升序排列"""
item = self._Item(key, value) # 根据键值对key-value创建一条记录
walk = self._data.last() # 将辅助指针初始化为引用当前队列最后一条记录
while walk is not None and item < walk.element(): # 遍历队列,保证该记录插入后队列中记录按键的大小升序排列
walk = self._data.before(walk)
if walk is None: # 如果新记录的键小于当前队列所有键,则在队列头部插入记录
self._data.add_first(item)
else: # 否则在辅助指针引用的记录后插入记录
self._data.add_after(walk, item)
def min(self):
"""返回(但不删除)优先级队列中键最小的记录"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
p = self._data.first()
item = p.element()
return item.key, item.value
def remove_min(self):
"""返回并删除优先级队列中键最小的记录"""
if self.is_empty():
raise Empty('当前优先级队列为空!')
item = self._data.delete(self._data.first())
return item.key, item.value
if __name__ == '__main__':
# 测试UnsortedPriorityQueue
u_q = UnsortedPriorityQueue()
u_q.add(5, 'A')
print(u_q) # [(5, 'A')]
u_q.add(9, 'C')
print(u_q) # [(5, 'A'), (9, 'C')]
u_q.add(3, 'B')
print(u_q) # [(5, 'A'), (9, 'C'), (3, 'B')]
u_q.add(7, 'D')
print(u_q) # [(5, 'A'), (9, 'C'), (3, 'B'), (7, 'D')]
print(u_q.min()) # (3, 'B')
print(u_q.remove_min()) # (3, 'B')
print(u_q) # [(5, 'A'), (9, 'C'), (7, 'D')]
print(len(u_q)) # 3
print(u_q.is_empty()) # False
# 测试SortedPriorityQueue
s_q = SortedPriorityQueue()
s_q.add(5, 'A')
print(s_q) # [(5, 'A')]
s_q.add(9, 'C')
print(s_q) # [(5, 'A'), (9, 'C')]
s_q.add(3, 'B')
print(s_q) # [(3, 'B'), (5, 'A'), (9, 'C')]
s_q.add(7, 'D')
print(s_q) # [(3, 'B'), (5, 'A'), (7, 'D'), (9, 'C')]
print(s_q.min()) # (3, 'B')
print(s_q.remove_min()) # (3, 'B')
print(s_q) # [(5, 'A'), (7, 'D'), (9, 'C')]
print(len(s_q)) # 3
print(s_q.is_empty()) # False