文章目录
- 一 链表简介
- 1.1链表定义
- 1.2 双向链表
- 1.3 循环链表
- 二、链表的基本操作
- 2.1 链表的结构定义
- 2.2 建立一个线性链表
- 2.3 求线性链表的长度
- 2.4 查找元素
- 2.5 插入元素
- 2.5.1 链表头部插入元素
- 2.5.2 链表尾部插入元素
- 2.5.3 链表中间插入元素
- 2.6 改变元素
- 2.7 删除元素
- 2.7.1 链表头部删除元素
- 2.7.2 链表尾部删除元素
- 2.7.3 链表中间删除元素
- 2.8 链表题目
- 删除排序链表中的重复元素
- 三、链表排序
- 3.1 链表冒泡排序
- 3.2 链表选择排序
- 四、链表双指针
- 4.1 起点不一致的快慢指针
- 4.2 步长不一致的快慢指针
- 4.3 分离双指针
一 链表简介
1.1链表定义
链表(Linked List):一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
简单来说,「链表」 是实现线性表链式存储结构的基础。
以单链表为例,链表的存储方式如下图所示。
如上图所示,链表通过将一组任意的存储单元串联在一起。其中,每个数据元素占用若干存储单元的组合称为一个「链节点」。为了将所有的节点串起来,每个链节点不仅要存放一个数据元素的值,还要存放一个指出这个数据元素在逻辑关系上的直接后继元素所在链节点的地址,该地址被称为「后继指针 next
在链表中,数据元素之间的逻辑关系是通过指针来间接反映的。逻辑上相邻的数据元素在物理地址上可能相邻,可也能不相邻。其在物理地址上的表现是随机的。
我们先来简单介绍一下链表结构的优缺点:
- 优点:存储空间不必事先分配,在需要存储空间的时候可以临时申请,不会造成空间的浪费;一些操作的时间效率远比数组高(插入、移动、删除元素等)。
- 缺点:不仅数据元素本身的数据信息要占用存储空间,指针也需要占用存储空间,链表结构比数组结构的空间开销大。
1.2 双向链表
双向链表(Doubly Linked List):链表的一种,也叫做双链表。它的每个链节点中有两个指针,分别指向直接后继和直接前驱。
双向链表特点:从双链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点。
1.3 循环链表
循环链表(Circular linked list):链表的一种。它的最后一个链节点指向头节点,形成一个环。
循环链表特点:从循环链表的任何一个节点出发都能找到任何其他节点。
二、链表的基本操作
数据结构的操作一般涉及到增、删、改、查 4 种情况,链表的操作也基本上是这 4 种情况。我们一起来看一下链表的基本操作。
2.1 链表的结构定义
链表是由链节点通过 next 链接而构成的,我们可以先定义一个简单的「链节点类」,再来定义完整的「链表类」。
- 链节点类(即 ListNode 类):使用成员变量 val 表示数据元素的值,使用指针变量 next 表示后继指针。
- 链表类(即 LinkedList 类):使用一个链节点变量 head 来表示链表的头节点。
我们在创建空链表时,只需要把相应的链表头节点变量设置为空链接即可。在 Python 里可以将其设置为 None,其他语言也有类似的惯用值,比如 NULL、0 等。
# 链节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 链表类
class LinkedList:
def __init__(self):
self.head = None
2.2 建立一个线性链表
建立一个线性链表:根据线性表的数据元素动态生成链节点,并依次将其连接到链表中。
1、从所给线性表中取出第 1个数据元素,建立链表头节点。然后依次获取表中的数据元素。
2、每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。
3、插入完毕之后返回第 1 个链节点(即头节点)的地址。
# 根据 data 初始化一个新链表
def create(self, data):
if not data:
return
self.head = ListNode(data[0])
cur = self.head
for i in range(1, len(data)):
node = ListNode(data[i])
cur.next = node
cur = cur.next
建立一个线性链表」的操作依赖于线性表的数据元素个数,因此,「建立一个线性链表」的时间复杂度为 O(n),n 为线性表长度。
2.3 求线性链表的长度
求线性链表长度:使用指针变量 cur 顺着链表 next 指针进行移动,并使用计数器 count 记录元素个数。
1、让指针变量 cur 指向链表的第 1 个链节点。
2、顺着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
3、等 cur 指向为空时结束遍历,此时计数器的数值就是链表的长度,将其返回即可。
# 获取线性链表长度
def length(self):
count = 0
cur = self.head
while cur:
count += 1
cur = cur.next
return count
2.4 查找元素
在链表中查找值为 val 的元素:从头节点 head 开始,沿着链表节点逐一进行查找。如果查找成功,返回被查找节点的地址;否则返回 None。
1、让指针变量 cur 指向链表的第 1 个链节点。
2、顺着链节点的 next 指针遍历链表,如果遇到 cur.val==val,则返回当前指针变量 cur。
3、如果 cur 指向为空时也未找到,则该链表中没有值为 val 的元素,则返回 None。
# 查找元素:在链表中查找值为 val 的元素
def find(self, val):
cur = self.head
while cur:
if val == cur.val:
return cur
cur = cur.next
return None
2.5 插入元素
2.5.1 链表头部插入元素
# 链表头部插入元素
def insertFront(self, val):
node = ListNode(val)
node.next = self.head
self.head = node
2.5.2 链表尾部插入元素
# 链表尾部插入元素
def insertRear(self, val):
node = ListNode(val)
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
2.5.3 链表中间插入元素
# 链表中间插入元素
def insertInside(self, index, val):
count = 0
cur = self.head
while cur and count < index - 1:
count += 1
cur = cur.next
if not cur:
return 'Error'
node = ListNode(val)
node.next = cur.next
cur.next = node
2.6 改变元素
「将链表中第 i 个元素值改为 val」 的代码如下:
# 改变元素:将链表中第 i 个元素值改为 val
def change(self, index, val):
count = 0
cur = self.head
while cur and count < index:
count += 1
cur = cur.next
if not cur:
return 'Error'
cur.val = val
2.7 删除元素
2.7.1 链表头部删除元素
# 链表头部删除元素
def removeFront(self):
if self.head:
self.head = self.head.next
2.7.2 链表尾部删除元素
# 链表尾部删除元素
def removeRear(self):
if not self.head or not self.head.next:
return 'Error'
cur = self.head
while cur.next.next:
cur = cur.next
cur.next = None
2.7.3 链表中间删除元素
链表中间删除元素:删除链表第 i 个链节点。
1、先使用指针变量 cur 移动到第 i−1 个位置的链节点。
2、然后将 cur 的 next 指针,指向要第 i 个元素的下一个节点即可。
# 链表中间删除元素
def removeInside(self, index):
count = 0
cur = self.head
while cur.next and count < index - 1:
count += 1
cur = cur.next
if not cur:
return 'Error'
del_node = cur.next
cur.next = del_node.next
2.8 链表题目
删除排序链表中的重复元素
描述:给定一个已排序的链表的头 head。
要求:删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
示例输入:head = [1,1,2,3,3]
输出:[1,2,3]
思路 1:遍历
1、使用指针 curr 遍历链表,先将 head 保存到 curr 指针。
2、判断当前元素的值和当前元素下一个节点元素值是否相等。
3、如果相等,则让当前指针指向当前指针下两个节点。
4、否则,让 curr 继续向后遍历。
5、遍历完之后返回头节点 head
class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if head == None:
return head
curr = head
while curr.next:
if curr.val == curr.next.val:
curr.next = curr.next.next
else:
curr = curr.next
return head
三、链表排序
适合链表的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序、基数排序。
不适合链表的排序算法:希尔排序。
可以用于链表排序但不建议使用的排序算法:堆排序。
3.1 链表冒泡排序
1、使用三个指针 node_i、node_j 和 tail。其中 node_i 用于控制外循环次数,循环次数为链节点个数(链表长度)。node_j 和 tail 用于控制内循环次数和循环结束位置。
2、排序开始前,将 node_i 、node_j 置于头节点位置。tail 指向链表末尾,即 None。
3、比较链表中相邻两个元素 node_j.val 与 node_j.next.val 的值大小,如果 node_j.val > node_j.next.val,则值相互交换。否则不发生交换。然后向右移动 node_j 指针,直到 node_j.next == tail 时停止。
4、一次循环之后,将 tail 移动到 node_j 所在位置。相当于 tail 向左移动了一位。此时 tail 节点右侧为链表中最大的链节点。
5、然后移动 node_i 节点,并将 node_j 置于头节点位置。然后重复第 3、4 步操作。
6、直到 node_i 节点移动到链表末尾停止,排序结束。
7、返回链表的头节点 head。
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 链表类
class LinkedList:
def __init__(self):
self.head = None
# 根据 data 初始化一个新链表
def create(self, data):
if not data:
return
self.head = ListNode(data[0])
cur = self.head
for i in range(1, len(data)):
node = ListNode(data[i])
cur.next = node
cur = cur.next
#冒泡排序
class Solution:
def sectionSort(self, head: ListNode):
node_i = head
# node_i 为当前未排序链表的第一个链节点
while node_i and node_i.next:
# min_node 为未排序链表中的值最小节点
min_node = node_i
node_j = node_i.next
while node_j:
if node_j.val < min_node.val:
min_node = node_j
node_j = node_j.next
# 交换值最小节点与未排序链表中第一个节点的值
if node_i != min_node:
node_i.val, min_node.val = min_node.val, node_i.val
node_i = node_i.next
return head
def sortList(self, head):
"""
:param head:[ListNode]的head
:return:
"""
return self.sectionSort(head)
if __name__ =="__main__":
data=[3,1,5,7,2]
C=LinkedList()
C.create(data) #创建链表
D=Solution()
D.sortList(C.head)#排序
#打印
result=[]
node_i = C.head
while node_i:
result.append(node_i.val)
node_i = node_i.next
print("结果:",result)
3.2 链表选择排序
1、使用两个指针 node_i、node_j。node_i 既可以用于控制外循环次数,又可以作为当前未排序链表的第一个链节点位置。
2、使用 min_node 记录当前未排序链表中值最小的链节点。
3、每一趟排序开始时,先令 min_node = node_i(即暂时假设链表中 node_i 节点为值最小的节点,经过比较后再确定最小值节点位置)。
4、然后依次比较未排序链表中 node_j.val 与 min_node.val 的值大小。如果 node_j.val < min_node.val,则更新 min_node 为 node_j。
5、这一趟排序结束时,未排序链表中最小值节点为 min_node,如果 node_i != min_node,则将 node_i 与 min_node 值进行交换。如果 node_i == min_node,则不用交换。
6、排序结束后,继续向右移动 node_i,重复上述步骤,在剩余未排序链表中寻找最小的链节点,并与 node_i 进行比较和交换,直到 node_i == None 或者 node_i.next == None 时,停止排序。
7、返回链表的头节点 head。
class Solution:
def sectionSort(self, head: ListNode):
node_i = head
# node_i 为当前未排序链表的第一个链节点
while node_i and node_i.next:
# min_node 为未排序链表中的值最小节点
min_node = node_i
node_j = node_i.next
while node_j:
if node_j.val < min_node.val:
min_node = node_j
node_j = node_j.next
# 交换值最小节点与未排序链表中第一个节点的值
if node_i != min_node:
node_i.val, min_node.val = min_node.val, node_i.val
node_i = node_i.next
return head
def sortList(self, head):
"""
:param head:[ListNode]的head
:return:
"""
return self.sectionSort(head)
四、链表双指针
双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞时针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。
而在单链表中,因为遍历节点只能顺着 next 指针方向进行,所以对于链表而言,一般只会用到「快慢指针」和「分离双指针」。其中链表的「快慢指针」又分为「起点不一致的快慢指针」和「步长不一致的快慢指针」。
4.1 起点不一致的快慢指针
起点不一致的快慢指针:指的是两个指针从同一侧开始遍历链表,但是两个指针的起点不一样。 快指针 fast 比慢指针 slow 先走 n 步,直到快指针移动到链表尾端时为止。
1、使用两个指针 slow、fast。slow、fast 都指向链表的头节点,即:slow = head,fast = head。
2、先将快指针向右移动 n 步。然后再同时向右移动快、慢指针。
3、等到快指针移动到链表尾部(即 fast == None)时跳出循环体。
伪代码模板
slow = head
fast = head
while n:
fast = fast.next
n -= 1
while fast:
fast = fast.next
slow = slow.next
适用范围
起点不一致的快慢指针主要用于找到链表中倒数第 k 个节点、删除链表倒数第 N 个节点等。
删除链表的倒数第 N 个结点
描述:给定一个链表的头节点 head。
要求:删除链表的倒数第 n 个节点,并且返回链表的头节点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
输入:head = [1], n = 1
输出:[]
如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 n 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 n 个节点位置。将该位置上的节点删除即可。
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
newHead = ListNode(0, head) #插入一个0
fast = head
slow = newHead
while n:
fast = fast.next
n -= 1
while fast:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return newHead.next
4.2 步长不一致的快慢指针
步长不一致的快慢指针:指的是两个指针从同一侧开始遍历链表,两个指针的起点一样,但是步长不一致。例如,慢指针 slow 每次走 1 步,快指针 fast 每次走两步。直到快指针移动到链表尾端时为止。
1、使用两个指针 slow、fast。slow、fast 都指向链表的头节点。
2、在循环体中将快、慢指针同时向右移动,但是快、慢指针的移动步长不一致。比如将慢指针每次移动 1 步,即 slow = slow.next。快指针每次移动 2 步,即 fast = fast.next.next。
3、等到快指针移动到链表尾部(即 fast == None)时跳出循环体。
伪代码
fast = head
slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
适用范围
步长不一致的快慢指针适合寻找链表的中点、判断和检测链表是否有环、找到两个链表的交点等问题。
链表的中间结点
描述:给定一个单链表的头节点 head。
要求:返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。
class Solution:
def middleNode(self, head: ListNode) -> ListNode:
fast = head
slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
4.3 分离双指针
分离双指针:两个指针分别属于不同的链表,两个指针分别在两个链表中移动。
1、使用两个指针 left_1、left_2。left_1 指向第一个链表头节点,即:left_1 = list1,left_2 指向第二个链表头节点,即:left_2 = list2。
2、当满足一定条件时,两个指针同时右移,即 left_1 = left_1.next、left_2 = left_2.next。
3、当满足另外一定条件时,将 left_1 指针右移,即 left_1 = left_1.next。
4、当满足其他一定条件时,将 left_2 指针右移,即 left_2 = left_2.next。
5、当其中一个链表遍历完时或者满足其他特殊条件时跳出循环体。
伪代码
left_1 = list1
left_2 = list2
while left_1 and left_2:
if 一定条件 1:
left_1 = left_1.next
left_2 = left_2.next
elif 一定条件 2:
left_1 = left_1.next
elif 一定条件 3:
left_2 = left_2.next
适用范围
分离双指针一般用于有序链表合并等问题。
合并两个有序链表
描述:给定两个升序链表的头节点 list1 和 list2。
要求:将其合并为一个升序链表。
输入:list1 = [1,2,4], list2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:list1 = [], list2 = []
输出:[]
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(-1)
curr = dummy_head
while list1 and list2:
if list1.val <= list2.val:
curr.next = list1
list1 = list1.next
else:
curr.next = list2
list2 = list2.next
curr = curr.next
curr.next = list1 if list1 is not None else list2
return dummy_head.next