链表
为什么需要链表
顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时有需要进行数据的搬迁,所以使用起来并不是很灵活。链表结构可以充分的利用计算机内存空间,实现灵活的内存动态管理。
链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里 存放下一个节点的位置信息(地址)。
和顺序表相比,顺序表存储空间必须连续,但如果有动态存储的需求时,顺序表就不太适用。
在顺序表中元素外置就是用一种联系来链接数据和地址。
如图所示,存储数据的三个内存空间并不是连续的内存空间,它们之间通过链相互链接。像这样,节点和节点之间通过链来链接起来的表叫做链表。
链表和顺序表统称为线性表,都是一维空间的线性结构。
链表中的每个节点由两部分组成:存储数据的区域和存储地址的区域。链接区存的是下一个节点的地址。
单向链表
单向链表也叫做单链表,是链表中最简单的一种形式。它的每个即诶单包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域指向一个空值。
- 表元素域elem用来存放具体的数据
- 链接域next用力啊存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中任意节点。
想要实现单链表的结构,必须先找到第一个节点的位置。图中,尾部位置的符号,表示这是最后一个节点,指向空。第一个节点叫做头节点,最后一个节点叫做尾节点。
节点实现
在python中,一切皆对象。即节点对象都有两个区域,因此需要先定义一个节点类。
class SingLinkNode(object):#节点有两个区域:数据区和链接区
"""单链表的节点"""
def __init__(self,elem):
#_item中存数据元素
self.elem=elem
#_next是下一个节点的标识
self.next=None
python中没有地址的表示方式,所以我们需要给python进行扩充:
示例:在python中交换两个变量如何交换?
这里首先要看等式的右边,b,a分别等于20,10(是因为a,b变量分别指向存入10,20的内存空间),接着看等式的左边,让a,b分别改变指向的内存空间,以达到交换两个变量的目的。
由此可知这里的等号不是赋值,而是改变a,b的地址导向。python的变量名保存的是地址。a指向的是引用对象的地址,=就是产生一个引用的链接。所以也可以说变量的本质就是引用和链接。
同理,变量名中还可以指向对象方法,调用类方法。(因为python中一切都是对象都可以调用)
单链表的操作
- is_empty()链表是否为空
- length()链表长度
- travel()遍历整个链表
- add(item)链表头部添加元素
- append(item链表尾部添加元素
- insert(pos,item)指定位置添加元素
- remove(item)删除节点(这里指的是指定的元素节点)
- search(item)查找即诶单是逗存在
使用单链表的方法前需要先构造类,对象和函数,才可以调用。
#单链表
class SingLinkNode(object):#节点有两个区域:数据区和链接区
"""单链表的节点"""
def __init__(self,elem):
#_item中存数据元素
self.elem=elem
#_next是下一个节点的标识
self.next=None
#node=Node(100)
class SingleLinkList(object):
"""单链表"""
#def __init__(self):#外部不需要知道这个属性,所以需要进行私有
#self._head=None#一开始头节点指向空
def __init__(self,none=None):
# 对象属性
self.__head = none#设置默认的参数,使用户传入的链表可以是空链表也可以是含参链表
def is_empty(self):
"""链表是否为空"""
def length(self):
"""链表长度"""
def travel(self):
"""遍历整个链表"""
def add(self):
"""链表头部添加元素"""
注意在链表中需要有一个指向第一个节点的标识。
具体方法的实现
1.首先要创建一个新的单链表:
sll=SingleLinkList()
这是新创建的一个单链表中为空,所以sll单链表的头指针指向为空。
2.接着构造一个节点:
node=Node(100)
插入一个带数据为 100的节点
3.将节点挂到链表上,实际上就是让head指向node(第一个节点)
4.就可以通过构造函数去实现单链表中的各种操作方法
判断链表是否为空
def is_empty(self):
"""链表是否为空"""
return self.__head==None
当为空的时候,直接返回True
求链表长度(一次遍历)
遍历的时候就需要有一个辅助的东西从头到尾去计——叫做游标或者指针。
遍历的第一步就是需要有一个辅助的cur来指向当前的位置
那到底选择哪一个语句来作为循环条件,和起始值有关。
游标cur从第一个节点开始遍历(指向head时,count为0),当遍历到count=3时,已经满足cur.next=None,则不会进入循环体,则计数比实际会少1,因此在这时,应当选择cur!=none这条判断语句。
def travel(self):
"""遍历整个链表"""
cur = self.__head
while cur != None:
print(cur.elem, end=" ")
cur = cur.next
print(" ")
def length(self):
"""链表求长度"""
cur=self.__head#cur游标,用来移动遍历节点
count=0#count记录数量
while cur!=None:
count+=1
cur=cur.next#移动游标
return count
这时需要去考虑存在链表为空的特殊情况:选择cur.next==None这个判断条件。
尾部插入:
先遍历链表,遍历到最后一个极点后,把心节点挂上去,就是尾插法。
但要注意的是,在append(item)方法中,传入的item不是一个节点,而是一个具体的数据元素。
def append(self,item):
"""链表尾部插入元素"""
node=SingLinkNode(item)#创建一个节点对象
cur=self.__head#遍历节点的游标
while cur.next!=None:
cur=cur.next
cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点
但是以上代码没有包含特殊情况,所以特殊情况需要特殊处理。就是当链表是空链表的情况:
def append(self,item):
"""链表尾部插入元素"""
node=SingLinkNode(item)#创建一个节点对象
if self.is_empty(node):#判断链表是否为空,为空的话,则头节点指向空
self.__head=node
else:
cur=self.__head#遍历节点的游标
while cur.next!=None:
cur=cur.next
cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点
头部插入:
这里需要注意的是修改指针的先后顺序,为了保证不丢失后面的节点,我们需要先将待插入节点的next区域指向原头节点,再将__head的next区域指向待插入节点。这样才能顺利完成头部插入。
def add(self,item):
"""链表头部添加元素"""
node=SingLinkNode(item)
node.next=self.__head#要注意这里的self.head不是指head这个节点,而是指haed指向的原头节点
self.__head=node
在指定位置插入元素:
def insert(self,pos,item):
"""在指定位置插入元素"""
pre=self.__head
count=0#需要有一个计数的,这样才可以找到指定位置
while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
count+=1
pre=pre.next
#当循环结束时,pre指向pos-1的位置
node=SingLinkNode(item)
node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
pre.next=node
函数的形参pos指的是元素的下标,为了符合数据结构类型,所以pos也是从0开始索引。pre是指指定位置的前一位。
pre=self.__head理解这句话,首先看等式的右边,通过self.__head找到对应的元素,所以在上述代码中,pre指向的应该是第一个节点。
现在考虑关于pos的特殊情况:如果用户传过来的pos<0,则认为是头插法,pos传入的数据比链表本身长度值更大,则表示是尾插法。
因此可以更家完善这个方法:
def insert(self,pos,item):
"""在指定位置插入元素"""
if pos<=0:
self.add(item)
elif pos>(self.length()-1):
self.append(item)
else:
pre=self.__head
count=0#需要有一个计数的,这样才可以找到指定位置
while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
count+=1
pre=pre.next
#当循环结束时,pre指向pos-1的位置
node=SingLinkNode(item)
node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
pre.next=node
单链表节点查找:
查找的本质就是判断一个数据是否在链表中。
def search(self,item):
"""查找节点是否存在"""
cur=self.__head#从头开始遍历
while cur!=None:
if cur.elem==item:
return True
else:
cur=cur.next
return False
同样需要考虑一开始就是空链表的特殊情况,如果一开始为空的链表,则cur一开始就为None,因此不进入while循环,直接return False。
单链表的删除:
注意这里的删除不是按照具体的位置去删除,而是删除具体的数据。因为要找到具体的数据,所以是需要游标辅助遍历。
删除需要两个辅助游标,一个pre(指向前一个节点),一个cur(指向找到的那个节点)。那是否可以用一个游标去完成删除操作呢?答案是可以的,只需要一个pre游标,也可以时下删除指定数据操作。
那么在使用两个游标的情况下,如歌初始化两个游标:pre和cur始终相差一个节点的距离,pre在cur之前。因为一开始cur指向第一个节点,在cur之前没有节点,所以pre一开始是pre=None。在进行游标移动时:第一步是让pre=cur,第二步是让cur=cur.next。
def remove(self,item):
"""删除节点"""
cur=self.__head#从头开始遍历
pre=None#pre和cur始终相差一个节点的距离
while cur!=None:
if cur.elem==item:
pre.next=cur.next
else:
pre=cur
cur=cur.next
考虑两端的极限情况:链表为空或要删除的节点恰好是首节点的情况
一开始是空链表,则在执行方法时,不进入循环,对空链表不执行任何操作,上述代码满足要求。
那删除的节点恰好是头节点的情况:
def remove(self,item):
"""删除节点"""
cur=self.__head#从头开始遍历
pre=None#pre和cur始终相差一个节点的距离
while cur != None:
if cur.elem==item:#首先判断一下第一个节点是不是指定数据节点
# #头节点是指定节点
if cur==self.__head:#cur指向的是头节点
self.__head=cur.next#直接删除
else:#指向的不是头节点
pre.next=cur.next
break
else:
pre=cur
cur=cur.next
特殊情况:
1.当在这个链表中,只有头节点这一个节点,且需要指定删除头节点。
2.当删除的这个指定节点恰好也是最尾节点。
都可以通过以上代码解决。
测试以上方法:
#单链表
class SingLinkNode(object):#节点有两个区域:数据区和链接区
"""单链表的节点"""
def __init__(self,elem):
#_item中存数据元素
self.elem=elem
#_next是下一个节点的标识
self.next=None
#node=Node(100)
class SingleLinkList(object):
"""单链表"""
#def __init__(self):#外部不需要知道这个属性,所以需要进行私有
#self._head=None#一开始头节点指向空
def __init__(self,none=None):
# 对象属性
self.__head = none#设置默认的参数,使用户传入的链表可以是空链表也可以是含参链表
def is_empty(self):
"""链表是否为空"""
return self.__head==None
def length(self):
"""链表长度"""
cur=self.__head#cur游标,用来移动遍历节点
count=0#count记录数量
while cur!=None:
count+=1
cur=cur.next#移动游标
return count
def travel(self):
"""遍历整个链表"""
cur = self.__head
while cur != None:
print(cur.elem, end=" ")
cur = cur.next
print(" ")
def add(self,item):
"""链表头部添加元素"""
node=SingLinkNode(item)
node.next=self.__head#要注意这里的self.head不是指head这个节点,而是指haed指向的原头节点
self.__head=node
def append(self,item):
"""链表尾部插入元素"""
node=SingLinkNode(item)#创建一个节点对象
if self.is_empty():
self.__head=node
else:
cur=self.__head#遍历节点的游标
while cur.next!=None:
cur=cur.next
cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点
def insert(self,pos,item):
"""在指定位置插入元素"""
if pos<=0:
self.add(item)
elif pos>(self.length()-1):
self.append(item)
else:
pre=self.__head
count=0#需要有一个计数的,这样才可以找到指定位置
while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
count+=1
pre=pre.next
#当循环结束时,pre指向pos-1的位置
node=SingLinkNode(item)
node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
pre.next=node
def search(self,item):
"""查找节点是否存在"""
cur=self.__head#从头开始遍历
while cur!=None:
if cur.elem==item:
return True
else:
cur=cur.next
return False
def remove(self,item):
"""删除节点"""
cur=self.__head#从头开始遍历
pre=None#pre和cur始终相差一个节点的距离
while cur != None:
if cur.elem==item:#首先判断一下第一个节点是不是指定数据节点
# #头节点是指定节点
if cur==self.__head:#cur指向的是头节点
self.__head=cur.next#直接删除
else:#指向的不是头节点
pre.next=cur.next
break
else:
pre=cur
cur=cur.next
if __name__=="__main__":#一个python文件通常有两种使用方法,第一是作为脚本直接执行,第二是 import 到其他的 python 脚本中被调用(模块重用)执行。因此if __name__ == 'main': 的作用就是控制这两种情况执行代码的过程,在if __name__ == 'main': 下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而 import 到其他脚本中是不会被执行的。
ll=SingleLinkList()
print(ll.is_empty())
print(ll.length())
ll.append(1)#1
print(ll.is_empty())
print(ll.length())
ll.append(2)#1 2
ll.add(8)#8 1 2
ll.append(3)
ll.append(4)
ll.append(5)
ll.append(6)#8 1 2 3 4 5 6
ll.travel()
ll.insert(-1,9)#9 8 1 2 3 4 5 6
ll.travel()
ll.insert(3,100)#9 8 1 100 2 3 4 5 6
ll.travel()
ll.insert(10,200)#9 8 1 100 23456 200
ll.travel()
ll.remove(100)
ll.travel()
ll.remove(9)
ll.travel()
ll.remove(200)
ll.travel()
运行结果:
单链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了节点的指针域,空间开销比较大,但对于存储空间的使用要相对灵活。
链表域顺序表的各种操作时间复杂度如下:
需要注意的是,虽然时间复杂度都是O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表存的主要耗时操作是遍历查找(链表只记录头节点地址,需要通过遍历来查找指定节点),删除和插入操作本身的时间复杂度是O(1)。顺序表查找很快,主要耗时操作是数据的搬迁。因为除了目标元素在尾部的特殊情况外,顺便表进行插入和删除时,需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
链表和顺序表优缺点:
- 链表存储数据时,内存可以不是连续的空间,可以通过链表来串联分散不连续的存储空间,但是由于链表节点包含两个部分(数据域和指针域),因此链表会消耗比顺序表更大的空间。
- 链表充分利用计算机中零碎的内存空间,存储更灵活,能够动态的进行存储,不需要像顺序表一样事先预估申请内存空间。