在文章【数据结构与算法Python描述】——双向链表简介、Python实现及应用,我们基于双向链表实现了在头尾均可高效地插入或删除对象元素的双端队列,但实际生活中对于队列模型的要求更高,如:
即我们希望实现一种可以在任意指定位置插入和删除元素的更一般性队列(以下简称“一般队列”),本文所要讨论的基于双向链表实现的位置列表(Positional List)就可以满足这一要求。
既然需要在任意指定位置插入和删除元素,那么首先需要进行位置的描述,第一反应可能是使用元素的整数索引来描述,但这存在以下问题:
通过文章【数据结构与算法Python描述】——单向线性链表简介、Python实现及应用,我们已经知道链表的一大优势在于,如果知道某任意结点的引用,可以在该结点附近进行 O ( 1 ) O(1) O(1)时间复杂度的插入和删除操作,因此你可能考虑使用结点的引用来描述位置。
实际上,文章【数据结构与算法Python描述】——双向链表简介、Python实现及应用中的_DoublyLinkedBase
类,其_insert_between
和_delete_node
两个方法的确接收结点引用作为参数,但在暴露给用户的接口中(这也是为什么_DoublyLinkedBase
类及其上述两个方法都定义为私有的缘故)这么做却有如下几个缺陷:
_DoublyLinkedBase
类中的_insert_between
和_delete_node
方法需要了解其内部使用了哨兵结点的技巧;_DoublyLinkedBase
类中的_insert_between
和_delete_node
方法传入不符合链表可接受格式的结点时,程序就会崩溃。实际上,为了准确描述位置列表中对象元素的位置,可以从文本编辑器的光标找到启发:在如Word等文本编辑器中,可以将光标放在某任意位置,然后在该位置插入和删除字符。
因此,如果将文本编辑器的光标在代码中抽象成一个Position
类,就可以用其实例对象描述位置列表中对象元素的位置。
为实现位置列表的ADT,首先需要定义用于描述元素位置的Position
类,在位置Position
类实例的内部,该实例维护了:
除此之外,由于数据结构的使用者最关心的地方之一是通过位置获取位置列表中对象元素,因此Position
类中还定义了一个element()
方法,用以返回该位置对应的对象元素。
基于上述分析,下面给出Position
类的完整代码:
class _Position:
"""代表对象元素在位置列表中位置的抽象"""
def __init__(self, container, node):
self.container = container
self.node = node
def element(self):
"""返回某一位置处的对象元素"""
return self.node.element
def __eq__(self, other):
"""如果两个Position实例代表了同一个位置,则返回True"""
return type(other) is type(self) and other.node is self.node
def __ne__(self, other):
"""如果两个Position的实例代表不同位置,则返回True"""
return not (self == other)
位置列表的ADT大致可以分为以下两个大类:
方法名称 | 方法描述 |
---|---|
__len__() |
返回当前位置列表的对象元素个数 |
__iter__() |
返回一个前向迭代器,该迭代器可返回位置列表中每一个对象元素 |
L.first() |
返回位置列表L 的第一个对象元素的位置,当位置列表为空返回None |
L.last() |
返回位置列表L 的最后一个对象元素的位置,当位置列表为空返回None |
L.before(p) |
返回位置列表L 中位置p 前一个对象元素的位置,当此时p 为第一个位置时返回None |
L.after(p) |
返回位置列表L 中位置p 后一个对象元素的位置,当此时p 为最后一个位置时返回None |
L.is_empty() |
如果位置列表L 不包含任何对象元素则返回None |
方法名称 | 方法描述 |
---|---|
L.add_first(e) |
在位置列表L 的头部插入对象元素e ,并返回新元素的位置p |
L.add_last(e) |
在位置列表L 的尾部插入对象元素e ,并返回新元素的位置p |
L.add_before(p, e) |
在位置列表L 的位置前插入对象元素e ,并返回新元素的位置p |
L.add_after(p, e) |
在位置列表L 的位置后插入对象元素e ,并返回新元素的位置p |
L.replace(p, e) |
将位置列表L 位置p处的对象元素替换为e ,并返回被替换的对象元素 |
L.delete(p) |
将位置列表L 在位置p 处的元素删除并返回 |
对于上述所有接受位置p
作为参数的方法,如果该位置不合法则抛出异常。
在后面实现位置列表ADT之前,为了让读者先对其中的各个方法有一个感性认识,下面给出了使用上述一系列方法对一个初始为空的位置列表进行一系列操作后的期望结果,其中 p i p_i pi表示Position
的实例对象:
操作 | 返回值 | 位置列表 |
---|---|---|
L.add_last(8) |
p 1 p_1 p1 | 8 p 1 8_{p_1} 8p1 |
L.first() |
p 1 p_1 p1 | 8 p 1 8_{p_1} 8p1 |
L.add_after(p1, 5) |
p 2 p_2 p2 | 8 p 1 8_{p_1} 8p1, 5 p 2 5_{p_2} 5p2 |
L.before(p2) |
p 1 p_1 p1 | 8 p 1 8_{p_1} 8p1, 5 p 2 5_{p_2} 5p2 |
L.add_before(p2, 3) |
p 3 p_3 p3 | 8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3, 5 p 2 5_{p_2} 5p2 |
p3.element() |
3 | 8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3, 5 p 2 5_{p_2} 5p2 |
L.after(p1) |
p 3 p_3 p3 | 8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3, 5 p 2 5_{p_2} 5p2 |
L.before(p1) |
None |
8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3, 5 p 2 5_{p_2} 5p2 |
L.add_first(9) |
p 4 p_4 p4 | 9 p 4 9_{p_4} 9p4, 8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3, 5 p 2 5_{p_2} 5p2 |
L.delete(L.last()) |
5 | 9 p 4 9_{p_4} 9p4, 8 p 1 8_{p_1} 8p1, 3 p 3 3_{p_3} 3p3 |
L.replace(p1, 7) |
8 | 9 p 4 9_{p_4} 9p4, 7 p 1 7_{p_1} 7p1, 3 p 3 3_{p_3} 3p3 |
如本文开头所述,本文将通过继承的方式使用双向链表_DoublyLinkedBase
作为对象元素存储容器来实现位置列表,此时位置列表的所有ADT方法均具有 O ( 1 ) O(1) O(1)最坏时间复杂度。
实际上,在即将实现的位置列表PositionList
类内部,为了提高代码重用性,需要先实现下列两个私有的实用方法:
_validate(p)
对于上述给出的位置列表ADT,由于大部分方法都接收Position
的实例作为参数,因而为确保程序的健壮性,需要使用该方法实现:
p
为描述该位置列表对象元素位置的Position
类的实例;p
描述的位置处,其对象元素属于当前列表;p
描述的位置处,其对象元素没有被删除;p
处的底层结点。def _validate(self, p: _Position):
"""返回位置实例处的结点引用,或当位置实例非法时抛出对应异常"""
if not isinstance(p, _Position):
raise TypeError('位置p:', p, '必须为准确的_Position类型!')
if p.container is not self:
raise ValueError('位置p', p, '不属于当前位置列表!')
if p.node.element is None:
raise ValueError('位置p', p, '已失效!')
return p.node
_node2pos(node)
为了提高代码重用性,以及提高数据结构的接口友好性,位置列表的ADT方法在需要返回值时,都应该提供Position
的实例而非底层结点对象给用户,因此定义该方法:
def _node2pos(self, node: _Node):
"""返回给定结点的Position实例对象,当给定哨兵结点时返回None"""
if node is self._header or node is self._trailer:
return None # node代表的结点非法
else:
return _Position(self, node)
_insert_between(e, predecessor, successor)
虽然从_DoublyLinkedBase
类中已经继承了该方法,但是由于其返回的是结点引用,为了让其更适合位置列表的场景,此处重写该方法使其返回结点的位置:
def _insert_between(self, e, predecessor, successor):
"""重写父类_DoublyLinkedBase中的同名方法,在两个已有结点之间插入经封装为结点后的对象元素,并返回新结点的位置"""
node = super()._insert_between(e, predecessor, successor)
return self._node2pos(node)
所有上述非修改类ADT方法均使用上述两个私有实用方法实现,且由于这些方法的实现较为简单,此处不再赘述:
def first(self):
"""返回位置列表中的第一个位置,如列表为空则返回None"""
return self._node2pos(self._header.next)
def last(self):
"""返回位置列表中的最后一个位置,如列表为空则返回None"""
return self._node2pos(self._trailer.prev)
def before(self, p: _Position):
"""返回位置p前的一个元素,如p为第一个业务元素的位置,则返回None"""
node = self._validate(p)
return self._node2pos(node.prev)
def after(self, p: _Position):
"""返回位置p后的一个元素,如p为最后一个业务元素的位置,则返回None"""
node = self._validate(p)
return self._node2pos(node.next)
def __iter__(self):
"""生成一个前向迭代器,该迭代器可依次返回列表中对象元素"""
cursor = self.first()
while cursor is not None: # first()方法所使用的_node2pos()方法确保了哨兵结点的位置为None
yield cursor.element()
cursor = self.after(cursor)
其中,__len__()
和is_empty()
两个方法直接继承自_DoublyLinkedBase
,故此处不会体现。
add_first(e)
在位置列表的第一个位置插入将对象元素封装后得到的结点,即相当于在头哨兵结点和该结点的后一个结点之间插入新结点:
def add_first(self, e):
"""在列表的第一个位置出插入经封装后的对象元素,并返回结点位置"""
return self._insert_between(e, self._header, self._header.next)
add_last(e)
在位置列表的最后一个位置插入将对象元素封装后得到的结点,即相当于在尾哨兵结点和该结点的前一个结点之间插入一个新结点:
def add_last(self, e):
"""在列表的最后一个位置出插入经封装后的对象元素,并返回结点位置"""
return self._insert_between(e, self._trailer.prev, self._trailer)
add_before(p, e)
为了能在位置p
之前将对象元素e
经封装后得到的结点插入列表中,只需:
p
处的结点引用;_insert_between()
插入新结点。def add_before(self, p: _Position, e):
"""在位置p之前将对象元素经封装后得到的结点插入列表中"""
node = self._validate(p)
return self._insert_between(e, node.prev, node)
add_after(p, e)
实现方法和实现上述add_before(p, e)
类似:
def add_after(self, p: _Position, e):
"""在位置p之后将对象元素经封装后得到的结点插入列表中"""
node = self._validate(p)
return self._insert_between(e, node, node.next)
delete(p)
为了删除并返回位置p
处对应的对象元素,只需:
p
处的结点引用;_DoublyLinkedBase
类中的_delete_node()
方法即可。def delete(self, p):
"""删除并返回位置p处对应的对象元素"""
node = self._validate(p)
return self._delete_node(node) # 集成自_DoublyLinkedBase类
replace(p, e)
为了将位置p
处的对象元素替换为e
,并返回原先位置p
处的对象元素,只需按照下列步骤进行即可:
def replace(self, p: _Position, e):
"""将位置p处的对象元素替换为e,并返回原先位置p处的对象元素"""
node = self._validate(p)
original_val = node.element # 暂存位置p处原先的对象元素
node.element = e # 替换
return original_val # 返回位置p处原先的对象元素
下面是实现位置列表的完整代码以及相关测试结果:
class Empty(Exception):
"""尝试对空队列进行删除操作时抛出的异常"""
pass
class _Node:
"""用于封装双向链表结点的类"""
def __init__(self, element=None, prev=None, next=None):
self.element = element # 对象元素
self.prev = prev # 前驱结点引用
self.next = next # 后继结点引用
class _Position:
"""代表对象元素在位置列表中位置的抽象"""
def __init__(self, container, node: _Node):
self.container = container
self.node = node
def element(self):
"""返回某一位置处的对象元素"""
return self.node.element
def __eq__(self, other):
"""如果两个Position实例代表了同一个位置,则返回True"""
return type(other) is type(self) and other.node is self.node
def __ne__(self, other):
"""如果两个Position的实例代表不同位置,则返回True"""
return not (self == other)
class _DoublyLinkedBase:
"""双向链表的基类"""
def __init__(self):
"""创建一个空的双向链表"""
self._header = _Node(element=None, prev=None, next=None)
self._trailer = _Node(element=None, prev=None, next=None)
self._header.next = self._trailer # 尾哨兵结点在头哨兵结点之后
self._trailer.prev = self._header # 头哨兵结点在尾哨兵结点之前
self._size = 0 # 元素数量
def __len__(self):
"""返回链表元素数量"""
return self._size
def is_empty(self):
"""如果链表为空则返回True"""
return self._size == 0
def _insert_between(self, element, predecessor, successor):
"""
在两个已有结点之间插入封装了元素element的新结点,并将该结点返回
:param element: 新结点中的对象元素
:param predecessor: 前驱结点
:param successor: 后继结点
:return: 封装了element的新结点
"""
new_node = _Node(element, predecessor, successor)
predecessor.next = new_node
successor.prev = new_node
self._size += 1
return new_node
def _delete_node(self, node):
"""删除非哨兵结点并将结点返回"""
predecessor = node.prev
successor = node.next
predecessor.next = successor
successor.prev = predecessor
self._size -= 1
element = node.element
node.prev = node.next = node.element = None
return element
class PositionalList(_DoublyLinkedBase):
"""允许通过位置实例操作其中对象元素的序列"""
def _validate(self, p: _Position):
"""返回位置实例处的结点引用,或当位置实例非法时抛出对应异常"""
if not isinstance(p, _Position):
raise TypeError('位置p:', p, '必须为准确的_Position类型!')
if p.container is not self:
raise ValueError('位置p', p, '不属于当前位置列表!')
if p.node.element is None:
raise ValueError('位置p', p, '已失效!')
return p.node
def _node2pos(self, node: _Node):
"""返回给定结点的Position实例对象,当给定哨兵结点时返回None"""
if node is self._header or node is self._trailer:
return None # node代表的结点非法
else:
return _Position(self, node)
def first(self):
"""返回位置列表中的第一个位置,如列表为空则返回None"""
return self._node2pos(self._header.next)
def last(self):
"""返回位置列表中的最后一个位置,如列表为空则返回None"""
return self._node2pos(self._trailer.prev)
def before(self, p: _Position):
"""返回位置p前的一个元素,如p为第一个业务元素的位置,则返回None"""
node = self._validate(p)
return self._node2pos(node.prev)
def after(self, p: _Position):
"""返回位置p后的一个元素,如p为最后一个业务元素的位置,则返回None"""
node = self._validate(p)
return self._node2pos(node.next)
def __iter__(self):
"""生成一个前向迭代器,该迭代器可依次返回列表中对象元素"""
cursor = self.first()
while cursor is not None: # first()方法所使用的_node2pos()方法确保了哨兵结点的位置为None
yield cursor.element()
cursor = self.after(cursor)
def _insert_between(self, e, predecessor, successor):
"""重写父类_DoublyLinkedBase中的同名方法,在两个已有结点之间插入经封装为结点后的对象元素,并返回新结点的位置"""
node = super()._insert_between(e, predecessor, successor)
return self._node2pos(node)
def add_first(self, e):
"""在列表的第一个位置出插入经封装后的对象元素,并返回结点位置"""
return self._insert_between(e, self._header, self._header.next)
def add_last(self, e):
"""在列表的最后一个位置出插入经封装后的对象元素,并返回结点位置"""
return self._insert_between(e, self._trailer.prev, self._trailer)
def add_before(self, p: _Position, e):
"""在位置p之前将对象元素经封装后得到的结点插入列表中"""
node = self._validate(p)
return self._insert_between(e, node.prev, node)
def add_after(self, p: _Position, e):
"""在位置p之后将对象元素经封装后得到的结点插入列表中"""
node = self._validate(p)
return self._insert_between(e, node, node.next)
def delete(self, p):
"""删除并返回位置p处对应的对象元素"""
node = self._validate(p)
return self._delete_node(node) # 集成自_DoublyLinkedBase类
def replace(self, p: _Position, e):
"""将位置p处的对象元素替换为e,并返回原先位置p处的对象元素"""
node = self._validate(p)
original_val = node.element # 暂存位置p处原先的对象元素
node.element = e # 替换
return original_val # 返回位置p处原先的对象元素
if __name__ == '__main__':
L = PositionalList()
p1 = L.add_first(8)
print('p1 = ', p1) # p1 = <__main__._Position object at 0x7f60c5fa6040>
print('L = ', list(L)) # L = [8]
p2 = L.first()
print('p2 = ', p2) # p2 = <__main__._Position object at 0x7f60c5fa6100>
p3 = L.add_after(p2, 5)
print('p3 = ', p3) # p3 = <__main__._Position object at 0x7f60c5fa61c0>
print('L = ', list(L)) # L = [8, 5]
p4 = L.before(p3)
print('p4 = ', p4) # p4 = <__main__._Position object at 0x7f60c5fa6280>
print(p1 == p4) # True
p5 = L.add_before(p3, 3)
print('p5 = ', p5) # p5 = <__main__._Position object at 0x7f60c5fa62e0>
print('L = ', list(L)) # L = [8, 3, 5]
print('p5.element() = ', p5.element()) # p5.element() = 3
p6 = L.before(p1)
print('p6 = ', p6) # p6 = None
print(L.delete(L.last())) # 5
print('L = ', list(L)) # L = [8, 3]
print(L.replace(p1, 7)) # 8
print('L = ', list(L)) # L = [7, 3]
早在前面定义_Position
类的时候你可能就对其中的__eq__()
和__ne__()
两个方法的作用存在疑问,实际上这两个方法分别实现了对运算符==
和!=
的重载,这么做的原因在于:
PositionalList
类中,first()
、last()
、before()
、after()
几个方法底层都调用_node2pos
方法,该方法并未实现单例模式,故很可能这几个方法返回的位置指向同一个结点,但是位置各不相同,如:测试代码中位置p1
和p4
应该都指向元素8,但是两个位置并不相同;==
或!=
运算符判断不同位置是否指向同一个位置列表的对象元素。