文章目录
- 一、双向链表引入
- 1. 定义
- 2. 原型
- 使用哨兵结点
- 哨兵结点的优势
- 3. 实现
- 双向链表结点插入
- 双向链表结点删除
- 双向链表基本实现
- 二、双向链表应用
- 再议双端队列
- 双向链表实现双端队列
在文章【数据结构与算法Python描述】——单向线性链表简介及其Python实现中,我们讨论了相对于列表,使用单向线性链表(以下简称“单向链表”)保存元素(例如:存储栈、队列的元素)的优势。
然而,虽然相较于列表,单向链表在头尾处插入结点(对于在尾部插入结点,可以维护一个引用尾结点的属性)以及在头部删除结点时可实现真正意义上(和经摊销后得到的 O ( 1 ) O(1) O(1)时间复杂度相对应)的 O ( 1 ) O(1) O(1)时间复杂度,但是删除单向链表的尾结点效率却比较低,因为该需求要求能够得到尾结点的前一个结点信息,这必须通过从头遍历单向链表来实现。
为了解决上述问题,可以使用单向链表的另一个变形——双向链表。
一、双向链表引入
1. 定义
双向链表是这样一种单向链表的一种变形,双向链表的所有结点不仅保存了其后一个结点的引用,还保存了其前一个结点的引用。
2. 原型
下图给出了双向链表的模型,即一个结点除了有element
域用于保存对象元素、next
域保存下一个结点信息外,还有一个prev
域用于保存前一个结点的信息。
使用哨兵结点
在上述给出的双向链表原型中,头结点和尾结点较为特殊,即头结点的prev
域为空,尾结点的next
域为空,这在编写代码时将需要每次都做特殊处理(具体请见下列“哨兵结点的优势”)。
基于上述原因,为了使得双向链表中每一个存储了对象元素的结点都不失一般性,这里引入两个特殊的结点作为头尾结点,这样双向链表的模型即变成下图所示:
在上述经改进后的双向链表中,特地引入header
和trailer
两个结点分别标记双向链表的头和尾,这两个结点就像镇守边界的哨兵一样,因此称其为哨兵结点。
当使用哨兵结点时:
- 对于初始为空的双向链表,头哨兵结点
header
的next
域保存尾哨兵结点trailer
的引用,而尾哨兵结点trailer
的prev
域保存了头哨兵结点的引用,而这两个哨兵结点的其他域均引用None
; - 对于初始不为空的双向链表,头哨兵结点
header
的next
域保存第一个业务结点1的引用,尾哨兵结点trailer
的prev
域保存最后一个业务结点的引用。
哨兵结点的优势
尽管不使用哨兵结点也可以实现双向链表,但使用哨兵结点却有如下明显优势:
- 由于头尾结点永远都不变而只有这二者之间的结点会改变,因此我们可以以一种统一的方式来处理业务结点的插入和删除操作,因为对于业务结点插入和删除操作,可以确保被操作结点始终在某两个结点之间;
- 相反,在单链表完整实现中,当希望使用
append
向空链表尾部插入结点时,由于此时不存在任何结点,因此需要做特殊处理,即让self._head
引用新的结点,但使用哨兵结点就可以使得无论何时向双向链表插入元素,链表都已有结点。
3. 实现
双向链表结点插入
如下图所示,双向链表的业务结点插入操作均发生在两个已有结点之间,下图分别代表了向双向链表中插入业务结点的三个状态:插入前、创建新业务结点后、链接新业务结点和周围结点后。
双向链表结点删除
对于从双向链表中删除结点操作,其步骤和上述插入操作恰好相反,只需要将待删除结点的前后两个结点直接链接起来即可,后续Python解释器将自动回收被“旁路”的结点。
双向链表基本实现
由于双向链表是单向链表的一个特例,即在单向链表相反的方向又加了一条链接,因此单向链表的ADT实现中的:
- 链表遍历方法
_traverse()
; - 尾部结点追加方法
append(element)
; - 头部插入元素方法
add_first(element)
; - 指定位置插入元素方法
insert(pos, element)
; - 元素查找方法
search(element)
; - 元素删除方法
remove(element)
。
上述方法的在双向链表中的实现基本一致,故此处不再赘述,此处仅实现双向链表ADT的下列两个方法:
insert_between(element, predecessor, successor)
:在结点predecessor
和successor
之间插入element
经封装后得到的结点;delete_node(node)
:删除结点node
。
为实现一个基本的双向链表,首先需要定义结点类_Node
,该类在形式上和文章【数据结构与算法Python描述】——单向线性链表简介、Python实现及应用中的结点类十分类似,仅是多了一个prev
属性。
class _Node:
"""用于封装双向链表结点的类"""
def __init__(self, element=None, prev=None, next=None):
self.element = element # 对象元素
self.prev = prev # 前驱结点引用
self.next = next # 后继结点引用
基于上述结点类,下面给出双向链表基本实现类_DoublyLinkedBase
,之所以将该类定义为私有的,是因为双向链表和【数据结构与算法Python描述】——单向循环链表简介、Python语言实现及应用提及的循环链表一样,都是单向链表的变形,一般仅用于业务的底层实现而并不暴露给用户,例如后面将使用该类实现一个双端队列。
class _Node:
"""用于封装双向链表结点的类"""
pass
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
在上述实现中,方法_insert_between
和_delete_node
的实现分别基于上述双向链表结点插入和删除,其中:
- 对于
_insert_between
:该方法先将element
封装成了结点对象,另外在此过程还分别建立了该新结点到其前驱结点及后继结点的单向链接,然后该新结点的前驱结点和后继结点分别和该新结点建立链接,至此新结点插入了双向链表中; - 对于
_delete_node
:该方法直接将待删除结点的前驱和后继结点链接在了一起,因而将待删除结点旁路了。此外,我们还将待删除结点的prev
、next
、element
域都置为了None
,这有助于Python解释器进行垃圾回收。
二、双向链表应用
再议双端队列
在文章【数据结构与算法Python描述】——队列和双端队列简介及其高效率版本Python实现中,我们基于Python的列表作为对象元素存储容器,并通过循环使用列表的方式实现了内存利用率和时间复杂度均较高的双端队列。
尽管如此,由于需要间或调整底层容器大小并且进行对象元素拷贝的操作,因此对于通过上述描述实现的双端队列,其队头(队尾)的入队(出队)操作的时间复杂度也均为经摊销后的 O ( 1 ) O(1) O(1)。
双向链表实现双端队列
为了解决上述问题,下面通过继承上述的_DoublyLinkedBase
类来实现双端队列。下面给出了通过此方法实现的双端队列LinkedDeque
全部代码,其中:
LinkedDeque
中并未实现__init__
方法,因为从_DoublyLinkedBase
中继承的初始化方法已经可以实现初始化一个双端队列的要求。同样LinkedDeque
中继承的__len__
和is_empty
方法分别可以返回当前队列中元素个数和是否为空的信息;LinkedDeque
中使用继承得到的_insert_between
方法实现向双端队列的对头/队尾插入对象元素:- 当希望向对头插入对象元素时,只需要将该对象元素封装后得到的结点插入头哨兵结点和其之后的一个结点之间即可;
- 当希望向队尾插入对象元素时,只需要将该对象元素封装后得到的结点插入尾哨兵结点和其之前的一个结点之间即可;
LinkedDeque
中使用继承得到的_delete_node
方法删除队头或队尾结点。
class Empty(Exception):
"""尝试对空队列进行删除操作时抛出的异常"""
pass
class _Node:
"""用于封装双向链表结点的类"""
pass
class _DoublyLinkedBase:
"""双向链表的基类"""
pass
class LinkedDeque(_DoublyLinkedBase):
"""使用双向链表实现的双端队列"""
def __iter__(self):
""" 通过前向迭代生成队列中的元素 """
cursor = self._header.next
while cursor.element is not None:
yield cursor.element
cursor = cursor.next
@property
def first(self):
"""返回但不删除队头元素"""
if self.is_empty():
raise Empty('队列为空!')
return self._header.next.element
@property
def last(self):
"""返回但不删除队尾元素"""
if self.is_empty():
raise Empty('队列为空!')
return self._trailer.prev.element
def insert_first(self, element):
"""在队列头部插入元素"""
self._insert_between(element, self._header, self._header.next)
def insert_last(self, element):
"""在队列尾部插入元素"""
self._insert_between(element, self._trailer.prev, self._trailer)
def delete_first(self):
"""删除队头结点,并返回结点元素域"""
if self.is_empty():
raise Empty('队列为空!')
return self._delete_node(self._header.next)
def delete_last(self):
"""删除尾结点,并返回结点元素域"""
if self.is_empty():
raise Empty('队列为空!')
return self._delete_node(self._trailer.prev)
if __name__ == '__main__':
lnk_deque = LinkedDeque()
lnk_deque.insert_first(9)
lnk_deque.insert_last(5)
print(len(lnk_deque)) # 2
lnk_deque.insert_first(3)
lnk_deque.insert_last(8)
print(list(lnk_deque)) # [3, 9, 5, 8]
print(lnk_deque.delete_first()) # 3
print(list(lnk_deque)) # [9, 5, 8]
print(lnk_deque.delete_last()) # 8
print(len(lnk_deque)) # 2
print(list(lnk_deque)) # [9, 5]
element
域保存对象元素的结点。 ↩︎