为什么需要链表?
对于上部分顺序表的学习,我们了解到在构建顺序表时需要预先知道数据大小来申请连续的存储空间,而在进行扩充的时候又需要进行数据的搬迁,所以使用起来并不是很灵活。
那我们就想,能不能存在一种数据结构是的在数据扩充的时候,在原有的数据完全不变化,扩充一个数据就增加一个,我们需要这样的一个数据结构,那么这样的数据结构怎么存储呢?
比如构造了一组数据Li=[200],然后申请了一个存储空间将200存储下来,然后补充了一个数据400,Li=[200,400],这时候又申请了一个空间用来储存400,现在我们要做的是不利用顺序表的方式进行存储,把他们顺序链接在一起,并且也不考虑我们最终扩充多少的数据,即扩充一个数据就申请一个单元来储存它,比如在扩充一个数据600,数据结构Li=[200,400,600],那么就在申请一个空间来储存600。如下表所示,我们怎么吧这种离散的空间关联在一起呢?
上表的结构已经没有了连续的概念了,但是我们可以通过找一根线将它们串联起来,这个概念已经在顺序表中的元素外置中引入过,具体的做法是:首先申请一个空间存储元素200,在数据扩充个400以后,在申请一个空间用于存储,然后将200与400之间通过一条线进行连接,使之建立一种关系,同样在扩充600的时候,也会申请一个空间用于存储600,然后通过一条线将400与600进行链接,重复这个过程,就可以扩充任意个数据,当然这里暂时先不用明白这条线是怎么具体链接的,后续会详细讲解。如下表所示
这样链接好了以后,在想获取数据的时候就可以根据200,沿着这条线往下寻找,就能根据这条线将所有元素串联在一起,而且并不需要预估元素的占用空间是多大(整型数据和char)。这种数据结构就叫做链表。
链表的定义
链表(Linked list)是一种常见的基础数据结构。是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
即在申请储存单元的时候,对单个的单元进行拓展,不仅保留该数据,还要保留另外的一部分数据,这两部分称为一个整体,叫做节点。其中第一部分保存数据,第二部分保存地址。而为了链接200和400这两个节点,就将第一个节点的链接区储存为400的地址,后面执行相同的步骤,这样就达到了一种线性关系,通过构建这样的一种数据结构来达到这样的链接关系。
单项链表
单项链表也叫单链表,是链表中最简单的一种形式,他的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
- 表元素elem用来存放具体的数据。
- 链接域next用来存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
单链表的操作
下面对该数据结构通过类的方式进行实现,一方面是指数据的保存,一方面是对数据的操作。
具体操作包括:
- is_empty():链表是否为空
- length():链表长度
- travel():遍历链表
- add(item):链表头部添加元素
- append(item):链表尾部添加元素
- insert(pos,item):指定位置添加元素
- remove(item):删除节点
- search(item):查找节点是否存在
单链表实现操作
#节点类
class Node(object):
"节点"
def __init__(self,elem): #构造函数
self.elem = elem
self.next = None #对于一个节点的初始状态指向为空,因为不知道指向谁
#链表类
class SingleLinkList(object):
"""单链表"""
def __init__(self,node=None):
self._head = node #私有属性head前加下划线
def is_empty(self): #对象操作
"""链表是否为空"""
return self._head == None
def length(self):
"""链表长度"""
#cur游标,用来移动遍历节点
cur = self._head
#count记录数量
count = 0
while cur != None:
count = count + 1
cur = cur.next
return count
def travel(self):
"""遍历整个链表"""
#cur游标,用来移动遍历节点
cur = self._head
#count记录数量
while cur != None:
print(cur.elem,end=" ")
cur = cur.next
def add(self, item):
"""头部添加元素"""
node = Node(item)
node.next = self._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 != None: #游标移动循环过程
cur = cur.next
cur.next = node
def insert(self, pos, item):
"""指定位置添加元素
param:pos,从0开始索引
"""
if pos < 0: #认为是头插法(add)
self.add(item)
elif pos > (self.length()-1):
self.append(item)
else:
node = Node(item)
pre = self._head #指针pre
count = 0
while count < (pos - 1):
pre = pre.next
count += 1
#当循环结束后,pre指向pos-1位置
node.next = pre.next
pre.next = node
def remove(self, item):
"""删除节点"""
#用两个游标,一个游标同样可以pre.next = pre.next.next
cur = self._head
pre = None
while cur != None:
if cur.elem == item:
#判断当前节点是否为头节点
#如果是头节点
if cur == self._head:
self._head = cur.next
else:
pre.next = cur.next
break
else:
pre = cur
cur = cur.next
def search(self, item):
"""查找节点是否存在"""
cur = self._head
while cur != None:
if cur.elem == item:
return True
else:
cur = cur.next
return False
if __name__ == "__main__":
ll = SingleLinkList()
print(ll.is_empty())
print(ll.length())
ll.append(1)
print(ll.is_empty())
print(ll.length())
ll.append(2)
ll.append(4)
ll.add(12)
ll.append(5)
ll.append(3)
ll.append(5)
ll.append(6)
ll.remove(12)
ll.append(4)
ll.insert(3,34)
ll.insert(-2,44)
ll.insert(15,55)
ll.travel()
结果
True
0
False
1
44 1 2 4 34 5 3 5 6 4 55
链表和顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了节点的指针域,空间开销大,但对储存空间的使用要相对灵活。
链表域顺序表的各种操作复杂度如下所示:
操作 | 链表 | 顺序表 |
---|---|---|
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
注意虽然表面上看起来复杂度都是O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝和覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作之间的元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。