基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- GitHub
视频:灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频
反转链表
06 反转链表 K个一组翻转
课程讲解
- 链表的第一个节点叫头节点(head)
- 链表的每个节点都包含节点值和指向下一个节点的next指针
- 链表的最后一个节点指向空
206. 反转链表
如果要反转一个链表
- 链表的第一个节点,它的next会指向空
- 链表的其余节点的next,应该反过来指向它的上一个节点
需要用变量 cur 表示当前遍历到的节点,变量 pre 表示上一个节点。两个变量不够,因为一旦修改了当前节点的next,就无法知道它原本的next是谁了。所以在修改之前,还需要用一个变量 nxt 记录它原来的next
这样修改完之后,就可以修改下一个节点了,做法是把 pre 更新成 cur,把 cur 更新成 next。如此循环,直到把所有节点都修改完,此时 cur 等于空,pre 等于原来链表的最后一个节点(也就是反转后链表的头节点),最后返回 pre
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre = None
cur = head
while cur:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
return pre
- 时间复杂度取决于循环次数,也就是节点个数,O(n)
- 空间O(1)
反转结束后,从原本的链表上看:
- pre 指向反转这一段的末尾
- cur 指向反转这一段后续的下一个节点
下面两题将会用到这个性质
92. 反转链表 II
反转中间一部分链表

根据性质,反转中间这一段之后,2应该指向cur,1应该指向pre,这样就能组成14325了
把反转这一段的上一个节点叫做p0,那么就是把 p0.next 指向cur,p0指向pre

还有一个特殊情况:当 left=1 时,是没有p0的。可以在 head 的前面再加上一个哨兵节点作为p0
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
p0 = dummy
for _ in range(left-1): # 因为left从1开始
p0 = p0.next # 最后到达反转这一段的上一个节点
# 下面跟反转链表做法一样
pre = None
cur = p0.next
for _ in range(right-left+1): # 反转这一段一共有 right-left+1 个元素
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
p0.next.next = cur
p0.next = pre
return dummy.next
25. K 个一组翻转链表
每K个节点一组翻转,不足K个的时候不能反转
先把链表的长度求出来,翻转之前先判断下剩余节点个数,如果 ≥ k 就可以翻转,否则就无法翻转,直接退出循环
每一组翻转的过程和上一题是一样的。额外要做的事情就是在翻转之后,把 p0 更新成下一段要翻转的链表的上一个节点(其实就是 p0.next)

由于也会修改 p0.next,所以在修改之前,可以先用一个临时变量 nxt 把它存起来。最后修改完了,再把 p0 更新成 nxt,开启下一次循环

不断循环得到答案,最后返回哨兵节点的next作为头节点
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 先求链表长度
n = 0
cur = head
while cur:
n += 1
cur = cur.next
dummy = ListNode(next=head)
p0 = dummy
pre = None
cur = p0.next
# 剩余节点个数 >= k,就可以循环
while n >= k:
n -= k
for _ in range(k):
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
nxt = p0.next # 提前存
p0.next.next = cur
p0.next = pre
# K个一组翻转完了,额外要做的
p0 = nxt # 把p0更新成下一段要翻转的链表的上一个节点(其实就是p0.next)
return dummy.next
课后作业
快慢指针
07 环形链表 重排链表
课程讲解
876. 链表的中间结点
如果链表长度是奇数,那么返回中间节点的值;如果链表长度为偶数,那么返回第二个中间节点的值
- 可以用两个指针指向链表的头节点,一个fast一个slow,每次循环slow走一步,fast走两步
- 可以证明得到,如果链表长度是奇数,fast在最后一个节点(即它的下一个节点指向空),slow一定在中间节点;如果链表长度是偶数,fast指向空,slow一定在中间节点
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
- 时间复杂度:O(n)。当fast走到链表末尾的时候,循环结束
- 空间复杂度:O(1)
141. 环形链表
如果图中有环,slow一定会走到环里。用相对速度思考,fast相对于slow的相对速度是每次循环走一步,因此两者肯定会相遇
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if fast is slow:
return True
return False
- 时间复杂度:O(n)。slow进入环后,循环次数是小于环的长度的
- 空间复杂度:O(1)
142. 环形链表 II
不仅要判断是否有环,还需要找到环的入口
结论:当快慢指针相遇时,slow还没有走完一整圈
- 考虑一个最坏情况,当slow进入环的时候,fast刚好在slow的前面
- 那用相对速度分析,fast需要走(环长-1)步才能跟slow相遇。对于其余任何情况,fast走的距离都不会超过(环长-1)步
- 所以slow移动的距离是小于环长的


a-c 是环长的倍数,head和slow均走了c步,此时head到入口的距离就是a-c。因此它两继续走,最终一定会在入口相遇
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if fast is slow: # 当快慢指针相遇时
# 如果此时慢指针和头节点没有相遇
while slow is not head:
# 各走一步
slow = slow.next
head = head.next
return slow
return None # 没有环
- 时间复杂度:O(n)。slow在相遇之前和相遇之后都走了O(n)步
- 空间复杂度:O(1)
143. 重排链表
先理解题意,最终结果就是把12和543交错合并起来

首先需要找到3,把3后面这段链表反转得到543
- 结合之前讲的【876. 链表的中间节点】以及【206. 反转链表】完成
这样就得到两段链表,把右边这段的头节点叫head2。每次循环时,先把head指向head2,然后head2指向head.next。再把它两(head和head2)移到它们的下一个节点上

由于更新了head的next,需要提前记录这两个next,最后更新到head上

如此不断循环,直到head2指向3,或者它的next是空,就退出循环

python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
"""
Do not return anything, modify head in-place instead.
"""
mid = self.middleNode(head) # 首先拿到中间节点
head2 = self.reverseList(mid) # 反转后半段
# 不断循环,直到head2的next为空
while head2.next: # 或写成 while head2 != mid:
nxt = head.next
nxt2 = head2.next
head.next = head2
head2.next = nxt
# 移到下一个节点
head = nxt
head2 = nxt2
# 876. 链表的中间节点
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# 206. 反转链表
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre = None
cur = head
while cur:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
return pre
- 时间复杂度:O(n)
- 空间复杂度:O(1)
课后作业
前后指针
08 链表删除 链表去重
课程讲解
对于单链表来说,要想删除一个节点,需要利用它的上一个节点,即把上一个节点指向要删除节点的下一个节点
- 注意,此时这个节点还是存在的,如果语言自带垃圾回收,那会帮你回收这部分内存
- C++需要手动回收,避免内存泄漏
对于删除节点来说,什么时候需要创建一个 dummy_node 呢?
- 一般来说,如果需要删除头节点的话,创建一个 dummy_node 是比较合适的
237. 删除链表中的节点
题目只给你要删除的节点,不知道它的上一个节点
保证给定的节点 node 不是链表中的最后一个节点。因此可以把下一个节点的值copy过来,然后把下一个节点删除
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def deleteNode(self, node):
"""
:type node: ListNode
:rtype: void Do not return anything, modify node in-place instead.
"""
node.val = node.next.val
node.next = node.next.next
- 时间和空间复杂度都是O(1)
19. 删除链表的倒数第 N 个结点
如果 n = 链表长度,那么头节点是会被删除的,需要创建 dummy_node
需要找到倒数第 n+1 个节点。怎么找?(快慢指针)
- 初始化右指针指向 dummy_node,先让右指针走n步
- 然后初始化左指针指向 dummy_node,左右指针一起向右走
- 那么左右指针的距离始终为n,当右指针走到最后一个节点(即倒数第一个节点),那么左指针就恰好走到了倒数第 n+1 个节点
- 这样就可以做删除操作了
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
right = dummy
for _ in range(n):
right = right.next
left = dummy
while right.next:
left = left.next
right = right.next
left.next = left.next.next
return dummy.next
- 时间复杂度:两个for循环加起来是 O(链表长度) 的
- 空间复杂度:O(1)
83. 删除排序链表中的重复元素
如果有重复节点,只保留一个
- 不需要 dummy_node,因为可以把头节点保留下来
初始化 cur 指向头节点,
- 如果下一个节点存在,且它的值和cur指向的节点值相同,那就删除下一个节点
- 否则就移到下一个节点
- 如此循环,直到cur后面没有节点为止
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 链表长度可能为0
if head is None:
return head
cur = head
while cur.next:
if cur.next.val == cur.val:
cur.next = cur.next.next
else:
cur = cur.next
return head
- 时间复杂度:每次循环要么删除一个节点,要么cur向右移动(也相当于处理了一个节点),所以是O(n)的
- 空间复杂度:O(1)
82. 删除排序链表中的重复元素 II
如果有重复节点,需要把重复的全部删除
- 需要dummy_node,因为如果开头就有几个重复节点,头节点是可能会被删除的
初始化 cur 指向 dummy_node,每次循环时看下一个节点和下下一个节点的值是否一样
- 如果一样,再套一个循环,不断删除节点,直到没有节点或者遇到的节点值不一样
- 如果不一样,cur就移到下一个节点,直到后面不足两个节点为止
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(next=head)
cur = dummy
while cur.next and cur.next.next:
val = cur.next.val
if cur.next.next.val == val:
while cur.next and cur.next.val == val:
cur.next = cur.next.next
else:
cur = cur.next
return dummy.next
- 时间复杂度:虽然写了一个循环套循环,但每次循环要么删除一个节点,要么cur向右移动(也相当于处理了一个节点),所以是O(n)的
- 空间复杂度:O(1)