【数据结构与算法Python描述】——单向线性链表简介及其Python实现给出了链表的最简单形式——单向线性链表,以及其Python语言实现和相关应用。
实际上,基于单向线性链表的变形很多,而单向循环链表(本文简称“循环链表”)是其中一种较为常见的形式。
一、单向循环链表引入
在文章【数据结构与算法Python描述】——队列和双端队列简介及其高效率版本Python实现中,我们首次引入了循环的概念,但实际上列表的内存模型并不存在任何首尾相连的特征,只是通过使用取模运算可以使得类似循环的特点。
对于链表,特别是单向线性链表,通过对其进行一定的变形可以获得真正意义上的环状形态,即如下图所示,让原本引用None的单向线性链表尾结点的next域引用头结点。
实际上,对于循环链表,已经不存在所谓头和尾的概念了,所以一般地可将循环链表画成如下图所述形式。对于这一说法以及循环链表的模型类比理解,一个现实中的例子是上海的地铁4号线,这是一条以横贯方式连接上海所有主要地铁线的环形线。
尽管说循环链表不存在开始和结束的概念,还是有必要定义一个变量使其引用循环链表中的某一个结点,上图使用名为current的变量,只有这样才能通过类似current = current.next的语法遍历找到链表中的所有节点。
二、单向循环链表应用
对于循环链表,我们不会像单向线性链表一样去实现其ADT的所有方法,因为循环不存在所谓头(尾)部插入(删除)等概念,即使实现了也大同小异。更多地,我们将循环链表的一些特性来实现特定的功能,比如下面的循环调度算法。
1. 循环调度算法
循环调度算法可以实现这样一种功能:对于某一个对象集合,该算法可以一种循环的方式挨个获取对象中的每个元素,然后对该元素做某种处理。
典型地使用循环调度算法的案例是计算机CPU为电脑上的不同应用分配执行时间片,我们知道计算机上的CPU数量一般远小于正在运行的应用数量,计算机就是利用诸如循环调度算法实现CPU在不同应用之间轮流切换执行,从而达到看似多个应用同时执行的效果。
2. 循环链表实现队列
2.1 分析
对于循环调度算法的实现,可以使用一个普通的队列来实现,即循环执行下列三个步骤:
使用e = queue.dequeue()获取应用e;
为元素e代表的应用服务;
使用queue.enqueue(e)将已被服务的元素e重新入队。
实际上,如果使用【数据结构与算法Python描述】——队列和双端队列简介及其高效率版本Python实现中的ListQueue来实现循环调度算法,则在每次循环中都需要先对某一元素执行一次队头出队操作,再对同一个元素执行一次队尾入队操作。
如果使用循环链表的思想,那么头结点出队然后在尾结点处入队的动作只需一个方法(一般名为rotate())即可实现,如下图所示:
先将队列的头尾结点链接在一起;
然后在该方法中使用标记当前头和尾结点的变量,使得在一次循环中将当前头结点变为尾结点,当前头结点的下一个结点成为新的头结点。
2.2 ADT
由上述分析,如果将使用循环链表思想实现支持循环调度算法的队列命名为CircularQueue,则其ADT至少包含下列方法:
方法名称
方法描述
__len__()
重写该方法,使对于CircularQueue的对象可以使用len()方法
is_empty()
判断CircularQueue对象是否为空
first()
返回但并不删除队列的头部元素
rotate()
完成头部元素出队并从尾部入队操作
enqueue(e)
向当前队列的尾部加入新的元素
dequeue()
删除当前队列中在队头的元素
2.3 实现
为了实现上述ADT包含的方法,乍一看似乎两个指向结点的变量,即self._head和self._tail。实际上,使用一个self._tail即可,因为总是可以通过self._tail.next来获取头结点的引用。
因此,使用下列两个变量即可实现CircularQueue类:
self._tail:指向尾结点的变量;
self._size:保存当前队列中元素数量的变量。
class Empty(Exception):
"""尝试对空队列进行删除操作时抛出的异常"""
pass
class _Node:
"""节点类"""
def __init__(self, element, next=None):
"""
:param element: 节点代表的对象元素
:param next: 下一个节点
"""
self.element = element
self.next = next
class CircularQueue:
"""支持循环调度算法的队列"""
def __init__(self):
"""创建一个空的队列"""
self._tail = None
self._size = 0
def __len__(self):
"""
返回队列中元素个数
:return: 队列中元素个数
"""
return self._size
def is_empty(self):
"""
如果队列为空则返回True,否则返回False
:return: 队列是否为空的标识
"""
return self._size == 0
def first(self):
"""
返回但不删除队头结点的元素
:return: 队头结点的元素
"""
if self.is_empty():
raise Empty('当前队列为空!')
head = self._tail
return head.element
def rotate(self):
"""
完成头部元素出队并从尾部入队操作
:return: None
"""
if self._size > 0:
self._tail = self._tail.next # 标识尾结点的变量指向当前头结点
def enqueue(self, element):
"""
向当前队列的尾部加入新的对象元素
:param element: 待从尾部入队的对象元素
:return: None
"""
node = _Node(element) # 元素封装成结点
if self.is_empty():
self._tail = node
self._tail.next = node # 链表成环
node.next = self._tail.next # 新结点和当前头结点建链
self._tail.next = node # 当前尾结点和新结点建链
self._tail = node # 新结点成为尾结点
self._size += 1
def dequeue(self):
"""
删除当前队列中在队头的元素
:return: 队头的元素
"""
if self.is_empty():
raise Empty('当前队列为空!')
old_head = self._tail.next
if self._size == 1:
self._tail = None # 将队列置为空
else:
self._tail.next = old_head.next # 直接跳过旧的头结点
self._size -= 1
return old_head.element
对于上述代码,有两点值得记录的是:
对于rotate()方法,在Python的collections模块中,其deque类有个功能类似的同名方法;
对于enqueue(e)方法,由于出现了两次完全一样的语句self._tail = node,其代码可以修改成如下形式:
def enqueue(self, element):
"""
向当前队列的尾部加入新的对象元素
:param element: 待从尾部入队的对象元素
:return: None
"""
node = _Node(element) # 元素封装成结点
if self.is_empty():
node.next = node # 链表成环
node.next = self._tail.next # 新结点和当前头结点建链
self._tail.next = node # 当前尾结点和新结点建链
self._tail = node # 新结点成为尾结点
self._size += 1