【数据结构与算法Python描述】——位置列表简介与Python版手工实现

在文章【数据结构与算法Python描述】——双向链表简介、Python实现及应用,我们基于双向链表实现了在头尾均可高效地插入或删除对象元素的双端队列,但实际生活中对于队列模型的要求更高,如:

  • 一个处于队列中间的顾客可能觉得队列过长提前结束排队;
  • 一个处于队列中间的游客可能正在代替朋友排队,等到朋友到达后出队然后其朋友插入该位置。

即我们希望实现一种可以在任意指定位置插入和删除元素的更一般性队列(以下简称“一般队列”),本文所要讨论的基于双向链表实现的位置列表(Positional List)就可以满足这一要求。

一、关于位置的描述

既然需要在任意指定位置插入和删除元素,那么首先需要进行位置的描述,第一反应可能是使用元素的整数索引来描述,但这存在以下问题:

  • 对于使用链表作为对象元素存储容器的队列来说,使用整数索引需要从头遍历链表,故而效率很低;
  • 在一些情况下使用整数索引来描述位置不准确,例如当对象元素前方发生了数次的插入和删除操作后,该对象元素的索引基本不可能是原先的数值。

1. 使用结点引用描述位置的弊端

通过文章【数据结构与算法Python描述】——单向线性链表简介、Python实现及应用,我们已经知道链表的一大优势在于,如果知道某任意结点的引用,可以在该结点附近进行 O ( 1 ) O(1) 时间复杂度的插入和删除操作,因此你可能考虑使用结点的引用来描述位置。

实际上,文章【数据结构与算法Python描述】——双向链表简介、Python实现及应用中的_DoublyLinkedBase类,其_insert_between_delete_node两个方法的确接收结点引用作为参数,但在暴露给用户的接口中(这也是为什么_DoublyLinkedBase类及其上述两个方法都定义为私有的缘故)这么做却有如下几个缺陷:

  • 可能导致接口友好性降低,例如:用户使用_DoublyLinkedBase类中的_insert_between_delete_node方法需要了解其内部使用了哨兵结点的技巧;
  • 可能使得数据结构的鲁棒性降低,例如:用户向_DoublyLinkedBase类中的_insert_between_delete_node方法传入不符合链表可接受格式的结点时,程序就会崩溃。

2. 使用类似编辑器光标描述位置

实际上,为了准确描述位置列表中对象元素的位置,可以从文本编辑器的光标找到启发:在如Word等文本编辑器中,可以将光标放在某任意位置,然后在该位置插入和删除字符。

因此,如果将文本编辑器的光标在代码中抽象成一个Position类,就可以用其实例对象描述位置列表中对象元素的位置。

3. 用于描述元素位置的代码抽象

为实现位置列表的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

位置列表的ADT大致可以分为以下两个大类:

1. 非修改类操作

方法名称 方法描述
__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

2. 修改类操作

方法名称 方法描述
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作为参数的方法,如果该位置不合法则抛出异常。

3. ADT功能期望

在后面实现位置列表ADT之前,为了让读者先对其中的各个方法有一个感性认识,下面给出了使用上述一系列方法对一个初始为空的位置列表进行一系列操作后的期望结果,其中 p i p_i 表示Position的实例对象:

操作 返回值 位置列表
L.add_last(8) p 1 p_1 8 p 1 8_{p_1}
L.first() p 1 p_1 8 p 1 8_{p_1}
L.add_after(p1, 5) p 2 p_2 8 p 1 8_{p_1} , 5 p 2 5_{p_2}
L.before(p2) p 1 p_1 8 p 1 8_{p_1} , 5 p 2 5_{p_2}
L.add_before(p2, 3) p 3 p_3 8 p 1 8_{p_1} , 3 p 3 3_{p_3} , 5 p 2 5_{p_2}
p3.element() 3 8 p 1 8_{p_1} , 3 p 3 3_{p_3} , 5 p 2 5_{p_2}
L.after(p1) p 3 p_3 8 p 1 8_{p_1} , 3 p 3 3_{p_3} , 5 p 2 5_{p_2}
L.before(p1) None 8 p 1 8_{p_1} , 3 p 3 3_{p_3} , 5 p 2 5_{p_2}
L.add_first(9) p 4 p_4 9 p 4 9_{p_4} , 8 p 1 8_{p_1} , 3 p 3 3_{p_3} , 5 p 2 5_{p_2}
L.delete(L.last()) 5 9 p 4 9_{p_4} , 8 p 1 8_{p_1} , 3 p 3 3_{p_3}
L.replace(p1, 7) 8 9 p 4 9_{p_4} , 7 p 1 7_{p_1} , 3 p 3 3_{p_3}

三、位置列表的实现

如本文开头所述,本文将通过继承的方式使用双向链表_DoublyLinkedBase作为对象元素存储容器来实现位置列表,此时位置列表的所有ADT方法均具有 O ( 1 ) O(1) 最坏时间复杂度。

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)

2. 非修改类方法

所有上述非修改类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,故此处不会体现。

3. 修改类方法

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方法,该方法并未实现单例模式,故很可能这几个方法返回的位置指向同一个结点,但是位置各不相同,如:测试代码中位置p1p4应该都指向元素8,但是两个位置并不相同;
  • 有时候需要根据==!=运算符判断不同位置是否指向同一个位置列表的对象元素。

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/108372285