回忆线性表的定义,它就是一些元素的序列,维持着元素之间的一种线性关系。实现线性表的基本需要是:
1、能够找到表中的首元素;
2、从表里的任一元素出发,可以找到它的下一个元素。
在上一篇中,把表元素保存在连续的存储区里(顺序表),自然可以满足这两点,其中元素间的顺序关联是隐含的。但是考虑到计算机内存的特点,为了满足以上两点,并不一定需要连续存储元素,基于对象之间的链接也可以看做一种顺序关联,基于它也可以实现线性表。
实现线性表的另一种常用方式就是基于链接结构,用链接关系显式表示元素之间的顺序关联。基于链接技术实现的线性表称为链接表或链表。
采用链表方式实现线性表的基本思想如下:
1、把表中元素分别存储在一批独立的存储块(称为表的结点)里;
2、保证从组成表结构中的任一结点可以找到与其相关的下一个结点;
3、在前一个结点里用链接的方式显式的记录与下一个结点之间的关联。
采用这样的结构,只要找到组成一个表结构的第一个结点,就能顺序找到属于这个表的其他结点。从这些结点里可以看到这个表的所有元素。
链接技术是一类非常灵活的数据组织技术,实现链表有多种不同的方式。下面首先讨论最简单的单链表,其中在每个表结点里记录着存储下一个表元素的结点的标识(引用/链接)。
(一)单链表
单向链接表(下面简记为链表或者单链表)的结点是一个二元组,形式大概如下图所示,其表元素域elem保存着作为表元素的数据项(或者数据项的关联信息),链接域next里保存同一个表里的下一个结点的标识。
在最常见形式的单链表里,与表里的n个元素对应的n个结点通过链接形成一条结点链。从引用表里首节点的变量可以找到这个表的首节点,从表中任一结点可以找到保存着该表下一个元素的结点(表中下一结点),这样,从p出发就能找到这个表里的任一个结点。
也就是说,为了掌握一个表,只需要用一个变量保存着这个表的首节点的引用(标识或称为链接)。今后把这样的变量称为表头指针或表头变量。
总结一下:
1、一个单链表由一些具体的表结点构成;
2、每个结点是一个对象,有自己的标识,下面也常称其为该结点的链接;
3、结点之间通过结点链接建立起单向的顺序联系。
通过判断一个(域或变量的)值是否为空链接,可知是否已到链表的结束。在顺序扫描表结点时,应该用这种方式确定操作是否完成。如果一个表头指针的值是空链接,就说明‘它所引用的链接已经结束’,这是没有元素就已结束,说明该表为空表。
在实现链表上的算法时,并不需要关心在某个具体的表里各个结点的具体链接值是什么(虽然保存在表结构里的值都是具体的),只需要关心链表的逻辑结构。这个具体在后面会体现。
单链表操作中,为了方便,我们定义一个简单的表结点类:
class LNode(object): # 定义一个节点类
def __init__(self, elem, next_=None):
self.elem = elem
self.next = next_
定义结点类时,只有一个初始化方法,它给对象的两个域赋值。方法的第二个参数用名字next_,是为了避免Python标准函数next重名。这也是Python程序中命名的一个惯例。第二个参数提供了默认了默认值,只是为了使用方便。
下面考虑单链表的操作,这里主要关注加入元素和删除元素:
1、加入元素
考虑给单链表加入元素的操作,同样需要考虑插入的位置,可以做首端插入、尾端插入或者定位插入。但是我们需要注意,在链表中插入元素时,并不需要移动已有的数据,只需要为新元素安排一个新结点,然后根据操作要求,把新结点连在表中正确的位置,也即,插入元素的操作是通过修改链接,接入新结点,从而改变表结构的方式实现的。
这里首先考虑表首端插入:首端插入元素要求把新数据插入表中,作为表的第一个元素,这是最简单的情况。这一操作大概分为三步:
1)创建一个新结点并存入数据(下图表示向表头变量head的链接加入新手元素13,为它创建了新结点,变量q指向改结点。这是插入前的状态);
2)把原链表首节点的链接域存入新结点的链接域next(head.next--->q.next),这一操作将原表的一串结点链接在刚创建的新结点之后;
3)修改表头指针,使之指向新结点,这一操作使新结点实际称为表头变量所指的结点,即表的首节点。
注意,即时插入前head指向的是空表,上面三步也能正确完成工作。这个插入只是一次安排新存储和几次赋值,操作具有常量时间复杂度。
示例代码如下:
q = LNode(13)
q.next = head
head = q
一般情况的元素插入:
如果要在一般位置插入元素,
首先得找到该位置之前的那个结点
,因为需要把新建结点的链接存到它之前结点的链接域中,当然这里如何找到该结点,之后会有讲到。
现在假设pre已指向要插入元素位置的前一个结点,这里同样是三步:
1)创建一个新结点并存入数据
2)把pre所指向结点next域的值存入新结点的链接域next(q.next = pre.next),这个操作将原表在pre所指结点之后的一段链接到新结点之后。
3)修改pre的next域,使之指向新结点,这个操作把新结点链入被操作的表。
示例代码如下:
q = LNode(13)
q.next = pre.next
pre.next = q
2、删除元素
删除链表中的元素,也可以通过调整表结构删除表中结点的方式完成。这里也区分表首删除元素和一般位置删除元素两种情况:删除表首元素可以直接完成,删除其他结点也需要掌握它的前一个结点。
删除表首元素:删除表中第一个元素对应于删除表的第一个结点,为此只需要修改表头指针,令其指向表中的第二个结点。丢弃不用的结点将被Python解释器自动回收。
实例代码如下:
head = head.next
删除一般位置的元素:
一般情况删除元素需要先找到要删除元素所在结点的前一个结点,设用变量pre指向,然后修改pre的next域,使之指向被删结点的下一个结点。
实例代码如下:
pre.next = pre.next.next
显然这两个操作都要求被删结点存在。
考虑到是在Python中实现删除结点,因此不需要自释放存储,因为Python的自动存储管理机制能自动处理这方面的问题。
总结一下链表操作的复杂度:
1)创建空表:O(1)
2)删除表:在python中是O(1)。当然,Python解释器做存储管理也需要时间
3)判断空表:O(1)
4)加入元素:
首端加入元素:O(1)
尾端加入元素:O(n),因为需要找到表的最后结点
定位加入元素:O(n),平均情况和最坏情况
5)删除元素:
首端删除元素:O(1)
尾端删除元素:O(n)
定位删除元素:O(n),平均情况和最坏情况
6)扫描、定位和遍历操作都需要检查一批表结点,其复杂度受到表结点数的约束,都是O(n)操作。
下面是单链表的实现程序:
class LNode(object): # 定义一个节点类
def __init__(self, elem, next_=None):
self.elem = elem
self.next = next_
class LinkListUnderflow(ValueError): # 自定义一个异常
pass
class Llist(object): # 定义一个链表
def __init__(self):
self._head = None
def is_empty(self): # 判断链表是否为空,通过self._head来判断
return self._head is None
def prepend(self, elem): # 在链表的头部加上元素
self._head = LNode(elem, self._head)
def pop(self): # 删除表头节点并返回其中的值
if self._head is None: # 无结点
raise LinkListUnderflow('in pop')
e = self._head.elem
self._head = self._head.next
print e
def append(self, elem):
if self._head is None: # 在链表的结尾处添加元素
self._head = LNode(elem)
return
p = self._head
while p.next is not None:
p = p.next
p.next = LNode(elem)
def pop_last(self): # 删除链表最后的一个元素
if self._head is None:
raise LinkListUnderflow('in pop_last')
p = self._head
if p.next is None:
e = p.elem
self._head = None
return e
while p.next.next is not None: # 找到链表的倒数第二个节点
p = p.next
e = p.next.elem
p.next = None
return e
def printall(self): # 自定义打印出来的结果
p = self._head
while p is not None:
print p.elem
p = p.next
def rev(self): # 列表翻转的操作
p = None
while self._head is not None:
q = self._head
self._head = q.next # 摘下原来的首节点
q.next = p # 将摘下来的节点放到p引用的节点序列中
p = q
self._head = p # 反转的结点序列做好后,重置表头链接
def sort1(self): # 通过移动表中的元素进行排序
if self._head is None:
return
cart = self._head.next # 从首节点之后的结点开始处理
while cart is not None:
p = self._head
x = cart.elem
while p is not cart and p.elem <= x: # 跳过小的元素
p = p.next
while p is not cart: # 置换大的元素和现在的值
y = p.elem
p.elem = x
x = y
p = p.next
cart.elem = x # 回填最后的一个元素
cart = cart.next