上一节我们介绍了线性表,线形表分为顺序表和链表,在python中封装好的顺序表就是列表,接下来我们来介绍链表。
文章目录
- 单向链表
- 节点实现
- 单链表的操作
- 单链表的操作实现
- 链表与顺序表的操作时间复杂度对比
单向链表
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值(None)。
- 表元素域elem用来存放具体的数据。
- 链接域next用来存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
在这里介绍一下python中的’=‘(赋值)、‘==’、‘is’之间的区别? - =:等于号意味着在将等号右边的变量所指向内容的地址或者常数的地址取出赋值给左边的变量,左边的变量就相当于存放地址的指针向右边数据。
- ==:逻辑判断符,用来判断双等号两边的变量的值是否相等
- is:判断地址是否相等,即用来判断两个变量是否指向同一块内存地址。
节点实现
# 定义存放节点的类
class Node():
def __init__(self, elem):
self.elem = elem # 节点的元素域
self.next = None # 节点的链接域,指向下一个节点
单链表的操作
- is_empty() 链表是否为空
- length() 链表长度
- travel() 遍历整个链表
- add(item) 链表头部添加元素
- append(item) 链表尾部添加元素
- insert(pos, item) 指定位置添加元素
- remove(item) 删除节点
- search(item) 查找节点是否存在
单链表的操作实现
# 定义单链表的类
class Singlelinklist():
def __init__(self, node=None):
self.__head = node
def is_empty(self):
return self.__head == None
def length(self):
# 定义一个游标cur,用来遍历所有节点
cur = self.__head
# count用来记录链表的长度
count = 0
while cur != None: # 当游标指向None的时候退出循环
count += 1
cur = cur.next # 游标向后移一个
return count
def travel(self):
# 定义一个cur游标用来遍历节点
cur = self.__head
while cur != None:
print(cur.elem, end=' ') # 打印元素域
cur = cur.next
头部插入节点
应该先改变node中next的指向,然后再将头指针指向node,如果先改变头指针的指向node的话,后边的数据就丢失了,无法在定义node的next域指向之前的头结点。
def add(self, item):
# 链表头部添加
node = Node(item) # 定义元素为item的节点
node.next = self.__head # 先让node节点指向,首节点
self.__head = node # 在将头指针指向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 # 让最后一个节点的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): # 将pre移动到指定位置之前的一个节点时
count += 1
pre = pre.next
# 当循环推出后,pre指向pos-1位置
node = Node(item) # 改变node和pre的指向
node.next = pre.next
pre.next = node
删除节点
删除节点需要定义两个游标cur和pre,pre表示cur之前的那个节点,当cur移动到需要删除元素的位置时,直接将pre的next域指向cur的next域就可以了。
def remove(self, item):
cur = self.__head
pre = None
while cur != None: # 当游标移动到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):
# 查询item是否在链表中
cur = self.__head
while cur != None: # 遍历列表中的所有节点
if cur.elem == item:
return True
else:
cur = cur.next
return False
链表与顺序表的操作时间复杂度对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
链表与顺序表的区别主要表现在三个方面:
- 开辟空间的方式 顺序表的数据存储实行的是一次存储,永久使用,但是动态顺序表的存储空间是可以改变的,改变的策略也与链表的空间扩展不同。链表一次只申请一个节点的存储空间,后边如果要填加节点,可以再申请。
- 空间利用率 从空间利用率的角度上看,顺序表的空间利用率显然要比链表高,这是因为链表在存储数据时,每次只申请一个节点的空间,且空间的位置是随机的,这种申请存储空间的方式会产生很多空间碎片,一定程序上造成了空间浪费。不仅如此,由于链表中每个数据元素都必须携带至少一个指针,因此,链表对所申请空间的利用率也没有顺序表高。
- 时间复杂度
链表与顺序表的各种操作复杂度如下所示:
操作 | 链表 | 顺序表 |
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。