分类目录:《算法设计与分析》总目录
链表是一种这样的数据结构,其中的各对象按线性顺序排列。数组的线性顺序是由数组下标决定的,然而与数组不同的是,链表的顺序是由各个对象里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法,并且能支持《算法设计与分析——栈和队列》中列出的所有操作。
如下图所示,双向链表
L
L
L的每个元素都是一个对象,每个对象有一个关键字
k
e
y
key
key和两个指针:
n
e
x
t
next
next和
p
r
e
v
prev
prev。对象中还可以包含其他的辅助数据(或称卫星数据)。设
x
x
x为链表的一个元素,则
x
.
n
e
r
t
x.nert
x.nert指向它在链表中的后继元素,
x
.
p
r
e
v
x.prev
x.prev指向它的前驱元素。如果
x
.
p
r
e
v
=
N
I
L
x.prev=NIL
x.prev=NIL,则元素
x
x
x没有前驱,因此是链表的第一个元素,即链表的头。如果
x
.
n
e
r
t
=
N
I
L
x.nert=NIL
x.nert=NIL,则元素
x
x
x没有后继,因此是链表的最后一个元素,即链表的尾。属性
L
.
h
e
a
d
L.head
L.head指向链表的第一个元素。如果
L
.
h
e
a
d
=
N
I
L
L.head=NIL
L.head=NIL,则链表为空。
链表可以有多种形式。它可以是单链接的或双链接的,可以是已排序的或未排序的,可以是循环的或非循环的。如果一个链表是单链接的,则省略每个元素中的
p
r
e
v
prev
prev指针。
class Node(object):
"""双向链表的结点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next 指向下一个节点的标识
self.next = None
# prev 指向上一结点
self.prev = None
如果链表是已排序的,则链表的线性顺序与链表元素中关键字的线性顺序一致;据此,最小的元素就是表头元素,而最大的元素则是表尾元素。如果链表是未排序的,则各元素可以以任何顺序出现。在循环链表中,表头元素的 p r e v prev prev指针指向表尾元素,而表尾元素的 n e x t next next指针则指向表头元素。我们可以将循环链表想象成一个各元素组成的圆环。在本文余下的部分中,我们假设所处理的链表都是未排序的且是双链接的。
class BilateralLinkList(object):
"""双向链表"""
def __init__(self):
self._head = None
def is_empty(self):
"""判断链表是否为空"""
return self._head is None
def length(self):
"""链表长度"""
# 初始指针指向head
cur = self._head
count = 0
# 指针指向None 表示到达尾部
while cur is not None:
count += 1
# 指针下移
cur = cur.next
return count
def items(self):
"""遍历链表"""
# 获取head指针
cur = self._head
# 循环遍历
while cur is not None:
# 返回生成器
yield cur.item
# 指针下移
cur = cur.next
def add(self, item):
"""向链表头部添加元素"""
node = Node(item)
if self.is_empty():
# 头部结点指针修改为新结点
self._head = node
else:
# 新结点指针指向原头部结点
node.next = self._head
# 原头部 prev 指向 新结点
self._head.prev = node
# head 指向新结点
self._head = node
def append(self, item):
"""尾部添加元素"""
node = Node(item)
if self.is_empty(): # 链表无元素
# 头部结点指针修改为新结点
self._head = node
else: # 链表有元素
# 移动到尾部
cur = self._head
while cur.next is not None:
cur = cur.next
# 新结点上一级指针指向旧尾部
node.prev = cur
# 旧尾部指向新结点
cur.next = node
def insert(self, index, item):
""" 指定位置插入元素"""
if index <= 0:
self.add(item)
elif index > self.length() - 1:
self.append(item)
else:
node = Node(item)
cur = self._head
for i in range(index):
cur = cur.next
# 新结点的向下指针指向当前结点
node.next = cur
# 新结点的向上指针指向当前结点的上一结点
node.prev = cur.prev
# 当前上一结点的向下指针指向node
cur.prev.next = node
# 当前结点的向上指针指向新结点
cur.prev = node
def remove(self, item):
""" 删除结点 """
if self.is_empty():
return
cur = self._head
# 删除元素在第一个结点
if cur.item == item:
# 只有一个元素
if cur.next is None:
self._head = None
return True
else:
# head 指向下一结点
self._head = cur.next
# 下一结点的向上指针指向None
cur.next.prev = None
return True
# 移动指针查找元素
while cur.next is not None:
if cur.item == item:
# 上一结点向下指针指向下一结点
cur.prev.next = cur.next
# 下一结点向上指针指向上一结点
cur.next.prev = cur.prev
return True
cur = cur.next
# 删除元素在最后一个
if cur.item == item:
# 上一结点向下指针指向None
cur.prev.next = None
return True
def find(self, item):
"""查找元素是否存在"""
return item in self.items()
def search(k):
"""搜索某个元素的key"""
x = self._head
while x != None and x != k:
x = x.next
return x
哨兵是一个哑对象,其作用是简化边界条件的处理。例如,假设在链表 L L L中设置个对象 L . n o n e L.none L.none,该对象代表 N o n e None None,但也具有和其他对象相同的各个属性。对于链表代码中出现的每一处对 N o n e None None的引用,都代之以对哨兵 L . n o n e L.none L.none的引用。如下图所示,这样的调整将一个常规的双向链表转变为一个有哨兵的双向循环链表,哨兵 L . n o n e L.none L.none位于表头和表尾之间。属性 L . n o n e . n e x t L.none.next L.none.next指向表头, L . n o n e . p r e v L.none.prev L.none.prev指向表尾。类似地,表尾的 n e x t next next属性和表头的 p r e v prev prev属性同时指向 L . n o n e L.none L.none。因为 L . n o n e . n e x t L.none.next L.none.next指向表头,我们就可以去掉属性 L . h e a d L.head L.head,并把对它的引用代替为对 L . n o n e . n e x t L.none.next L.none.next的引用。下图显示,一个空的链表只由一个哨兵构成, L . n o n e . n e x t L.none.next L.none.next和 L . n o n e . p r e v L.none.prev L.none.prev同时指向 L . n o n e L.none L.none.
哨兵基本不能降低数据结构相关操作的渐近时间界,但可以降低常数因子。在循环语句中使用哨兵的好处往往在于可以使代码简洁,而非提高速度。我们应当慎用哨兵。假如有许多个很短的链表,它们的哨兵所占用的额外的存储空间会造成严重的存储浪费。