一、什么是链表?
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
链表的内存分配方式:随机存储
二、链表的分类
1.单向链表
单向链表的每一个节点包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。
2.单向循环链表
单向循环链表的尾节点的next指针指向头节点
3.双向链表
双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
4.双向循环链表
双向循环链表的尾节点的next指针指向头节点,头节点的prev指针指向尾节点。
三、链表的基本操作(以“单向链表”为例)
1.查找节点
在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。
2.更新节点
先从头节点开始向后一个一个节点逐一查找,查找到后把旧数据替换成新数据即可。
3.插入节点
(1)尾部插入
(2)头部插入
(3)中间插入
a. 某个节点之前插入
b.某个节点之后插入
4.删除节点
删除链表中的某个value,可能会遇到下面几种情况:
1. 链表可能为空:通过head判断该链表是否为空,如果链表为空则结束
2. 链表不为空但可能没有value:如果从头找到尾都没有找到value,说明该链表中没有value
3. 链表的头节点可能为value:头节点没有前驱节点
4. 链表不为空&存在value:通过value的前驱节点进行删除
具体代码实现如下:
# -*- coding: utf-8 -*-
class SinglyLinkedList:
def __init__(self) ->None:
self.head = None
# 声明一个内部类,代表一个节点,每个节点有两个属性
class Node:
def __init__(self, data) -> None:
# 第一个属性用来存数据
self.data = data
# 第二个属性用来指向下一个节点
self.next = None
def insert_to_tail(self,value):
# 先判断头节点是否为空,如果头节点为空则调用insert_to_head方法
if self.head is None:
self.insert_to_head(value)
return
# 如果头节点不为空,则找到头节点
q = self.head
# 通过头节点的next节点一直循环找到尾节点
# 当q的next节点不为空时则继续查找,当q的next节点为空时说明找到了尾节点
while q.next is not None:
q = q.next
# 声明一个新节点
new_node = self.Node(value)
# 把尾节点的next指针指向新插入的节点即可
q.next = new_node
def insert_to_head(self,value):
# 声明一个新节点
new_node = self.Node(value)
# 如果头节点为空,则直接将新节点变为链表的头节点
if self.head is None:
self.head = new_node
# 如果头节点不为空:
else:
# 将新节点的next指针指向原先的头节点
new_node.next = self.head
# 把新节点变为链表的头节点
self.head = new_node
def find_by_value(self,value):
if self.head is None:
return Exception("链表为空!")
q = self.head
while q is not None and q.data != value:
q = q.next
if q is None:
return Exception("链表内没有该value!")
return q
def insert_after(self,node,value):
if node is None:
return Exception("该node不存在!")
# 声明一个新节点
new_node = self.Node(value)
# 先将新插入节点的next指针指向node的next指针指向的节点 (注意这两步不要写反,写反后会进入死循环)
new_node.next = node.next
# 再将指定node的next指针指向新插入的节点
node.next = new_node
def insert_before(self,node,value):
if self.head is None:
return Exception("该链表为空!")
# 否则,找到头节点,从头节点开始遍历查找,利用q找node的前驱节点:
q = self.head
while q is not None and q.next != node:
q = q.next
# 该链表中没有找到node节点
if q is None:
return Exception("该node不存在!")
# 找到node的前驱节点之后,声明一个新节点
new_node = self.Node(value)
# 新节点的next指针指向node
new_node.next = node
# node的前一个节点指向新节点
q.next = new_node
def print_all(self):
if self.head is None:
return Exception("该链表为空!")
q = self.head
while q is not None:
print(q.data)
q = q.next
def delete_by_value(self,value):
# 1. 通过head判断该链表是否为空,如果链表为空则结束
if self.head is None:
return Exception("链表为空!")
# 2. 如果链表不为空,则通过头节点依次往后找下一个节点
q = self.head
# 因为要想删除链表中的一个节点,必须找到该节点的前驱节点,所以这里定义一个p
p = None
while q is not None and q != value:
# 在q进行迭代前,先让p=q,这样p就是q的前驱节点了
p = q
q = q.next
# 如果从头找到尾都没有找到value,说明该链表中没有value
if q is None:
return Exception("该链表中没有value!")
# 如果p为None,说明头节点即为该value
if p is None:
# 那么直接将链表中的头节点变为之前头节点的下一个节点即可
self.head = self.head.next
# 否则就是将q的前驱节点p直接指向q的下一个节点
else:
p.next = q.next
return True
def test_link():
link = SinglyLinkedList()
data = [1, 2, 5, 3, 1]
for i in data:
link.insert_to_tail(i)
link.insert_to_tail(6)
link.insert_to_head(0)
# 打印内容为 0 1 2 5 3 1 6
# link.print_all()
# assert link.find_by_value(2) is not None
# node = link.find_by_value(3)
# link.insert_after(node, 10)
# assert link.find_by_value(3).next.data == 10
# # 打印内容为 0 1 2 5 3 10 1 6
# link.print_all()
# node = link.find_by_value(0)
# print(node)
# link.insert_after(node, 10)
# # assert link.find_by_value(0).next.data == 10
# # # 打印内容为 0 10 1 2 5 3 1 6
# link.print_all()
# #链表内没有该value,所以不进行插入
# node = link.find_by_value(100)
# link.insert_before(node, 30)
# # # 打印内容为 0 1 2 5 3 1 6
# link.print_all()
# node = link.find_by_value(0)
# print(node)
# link.insert_before(node, 30)
# link.print_all()
# link.delete_by_value(2)
# assert not link.delete_by_value(999)
# assert link.delete_by_value(99)
# # 打印内容为 1 5 3 1
# link.print_all()
if __name__ == '__main__':
test_link()
最近看《漫画算法: 小灰的算法之旅 (python篇)》发现里面代码写的更清晰明了,分别是「在指定位置插入元素」和「删除指定位置的元素」,插入和删除的分析其实是和上面一致的。看代码~
# -*- coding: utf-8 -*-
class SinglyLinkedList:
def __init__(self) -> None:
self.size = 0
self.head = None
self.last = None
class Node:
def __init__(self, data) -> None:
self.data = data
self.next = None
def get_value(self,index):
if index < 0 or index >= self.size:
return Exception("查询位置超出链表的范围了!")
p = self.head
for i in range(index):
p = p.next
return p
def insert(self,index,value):
if index < 0 or index > self.size:
return Exception("插入位置超出链表的范围了!")
# 声明一个新节点
new_node = self.Node(value)
# 如果是空链表,则直接将该新节点为链表的头和尾
if self.size == 0:
self.head = new_node
self.last = new_node
# 如果插入在链表的头部
elif index == 0:
# 将新节点的next指针指向原先的头节
new_node.next = self.head
# 把新节点变为链表的头节点
self.head = new_node
# 如果插入在链表的尾部
elif self.size == index:
# 将原先的尾节点的next指向新节点
self.last.next = new_node
# 把新节点变为链表的尾节点
self.last = new_node
# 插入中间,则需要找到index的前驱节点
else:
prev_node = self.get_value(index-1)
# 将新节点的next指向前驱节点的next
new_node.next = prev_node.next
# 将前驱节点的next指向新节点
prev_node.next = new_node
self.size += 1
def delete(self,index):
if index < 0 or index >= self.size:
return Exception("删除位置超出链表的范围了!")
# 删除头节点
if index == 0:
delete_node = self.head
# 将原有头节点的下一个节点作为新的头节点
self.head = self.head.next
# 删除尾节点
elif self.size == index:
# 找到尾节点的前驱节点
prev_node = self.get_value(index-1)
# 删除节点为前驱节点的下一个节点
delete_node = prev_node.next
# 这个前驱节点作为新的尾节点,next指向None
prev_node.next = None
# 删除中间位置
else:
prev_node = self.get_value(index-1)
# 找到删除节点的下一个节点
next_node = prev_node.next.next
delete_node = prev_node.next
# 将前驱节点的next指向删除节点的下一个节点
prev_node.next = next_node
self.size -= 1
return delete_node
def print_all(self):
if self.head is None:
return Exception("该链表为空!")
q = self.head
while q is not None:
print(q.data)
q = q.next
def test_link():
link = SinglyLinkedList()
link.insert(0, 1)
link.insert(1, 2)
link.insert(2, 3)
link.insert(1, 5)
link.delete(0)
link.delete(1)
link.delete(1)
link.print_all()
if __name__ == '__main__':
test_link()
四、总结
1.链表和数组的比较
时间复杂度 | 查找 | 更新 | 插入 | 删除 |
---|---|---|---|---|
数组 | O(1) | O(1) | O(n) | O(n) |
链表 | O(n) | O(1) | O(1) | O(1) |
数组:优势在于能够快速定位元素,对于读操作多、写操作少的场景,适合用数组。
链表:优势在于能够灵活地进行插入和删除操作,对于频繁插入、删除元素,适合用链表。