Leetcode刷题笔记—链表篇
链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。
左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。
滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天
前言:文章有点长,这是博主本人的写作风格,大家无需一口气把文章从头到尾看完,只看自己有疑惑的地方即可
力扣上给的是伪代码,对链表的题型无法在自己的本地编辑器上调试,这里给大家一个利用列表创建链表的模板可以方便大家实际调试
def create_list(linklist):
"""
:param linklist:拿来构建链表的数组
:return: head-返回链表的头结点
"""
node_list = []
for i in range(len(linklist)):
node = ListNode()
node.val = linklist[i]
node_list.append(node)
head = node_list[0]
p = head
for node in node_list[1:]:
p.next = node
p = p.next
return head
一、快慢指针在链表中的应用
第一题:旋转链表
Leetcode61. 旋转链表:中等题 详情请点击链接看原题
给你一个链表的头节点
head
,旋转链表,将链表每个节点向右移动k
个位置
python代码解法:
class Solution:
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if not head:
return head
length = 0
p = head
while p:
length += 1
p = p.next
k %= length # 对长度取模
if k == 0 or length == 1:
return head
slow, fast = head, head
dummy_head = ListNode()
dummy_head.next = head
while fast.next:
if k <= 0:
slow = slow.next
k -= 1
fast = fast.next
dummy_head.next = slow.next
slow.next = fast.next
fast.next = head
return dummy_head.next
第二题:删除排序链表中的重复元素
给定一个已排序的链表的头
head
, 删除所有重复的元素,使每个元素只出现一次,返回已排序的链表
细心的同学会发现这道题和 Leetcode26-删除有序数组中的重复项 这道题非常类似,只不过这里是有序链表,解题思路完全一致,只不过对链表和数组的处理稍有不一致,这篇文章中有对删除有序数组中的重复项的题解
python题解代码1
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
dummy = ListNode()
dummy.next = head
slow, fast = head, head
while fast:
if slow.val != fast.val: # 快慢指针同时移动
slow.next = fast # 将中间重复的元素断链
slow = slow.next # slow指针后移一位
fast = fast.next
slow.next = fast # 防止最后一个元素重复
return dummy.next
python题解代码2
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
dummy_head = ListNode()
dummy_head.next = head
slow = fast = head
while fast:
while fast and fast.val == slow.val:
fast = fast.next
slow.next = fast
slow = fast
return dummy_head.next
第三题:删除排序链表中的重复元素 II
Leetcode82:删除排序链表中的重复元素 II:中等题
给定一个已排序的链表的头
head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表
和上一题不同的是这道题中只要存在重复元素,就删除所有包含重复元素的节点,上一题对重复元素的处理是保留一个
先创建一个虚拟头结点 dummy
,令dummy.next = head
,然后创建指针pre
指向dummy
,指针cur
指向head
,开始遍历链表
- 当
cur
指向的结点值与cur.next
指向的结点值相同时,我们就让cur
不断向后移动,直到cur
指向的结点值与cur.next
指向的结点值不相同时,停止移动 - 我们判断
pre.next
是否等于cur
- 如果相等说明
pre
与cur
之间没有重复结点,我们就让pre
移动到cur
的位置 - 如果不相等就让
pre.next = cur.next
,然后让cur
继续向后移动
- 继续上述操作直到
cur
为空,遍历结束
python题解代码
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
dummy_head = ListNode()
dummy_head.next = head
pre, cur = dummy_head, head
while cur:
while cur.next and cur.val == cur.next.val: # 1.如果cur指向的结点值与cur.next指向的结点值相同时,就让cur不断向后移动
cur = cur.next
if pre.next == cur: # 2.说明pre与cur之间没有重复结点
pre = cur
else: # 3.说明pre与cur之间有重复结点
pre.next = cur.next
cur = cur.next # 4.cur继续向后移动
return dummy_head.next
第四题:回文链表
给你一个单链表的头节点
head
,请你判断该链表是否为回文链表。如果是,返回true
;否则,返回false
解题思路:
我们可以将链表的后半部分反转【修改链表结构】,然后将前半部分和后半部分进行比较,比较完后将链表恢复
- 找到前半部分链表的尾结点(慢指针走一步,快指针一次走两步,快慢指针同时出发,快指针走到末尾慢指针走到中间)
- 反转后半部分链表
- 恢复链表
- 返回结果
python题解代码
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def reverse_link(self, head):
dummy_head = ListNode()
cur_node = head
while cur_node:
next_node = cur_node.next
cur_node.next = dummy_head.next
dummy_head.next = cur_node
cur_node = next_node
return dummy_head.next
def find_middle_node(self, head): # 找到中间结点
slow, fast = head, head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
return slow
def isPalindrome(self, head: Optional[ListNode]) -> bool:
if not head:
return True
mid_node = self.find_middle_node(head) # 找到前半部分链表的尾结点(用于还原链表)
reverse_head = self.reverse_link(mid_node.next) # 反转后半部分(保存反转后的头结点用于恢复链表)
i, j = head, reverse_head # 重新初始化i和j指针指向两个链表的头结点
flag = False
while not flag and j:
if i.val != j.val:
flag = True
i, j = i.next, j.next
mid_node.next = self.reverse_link(reverse_head) # 还原链表
if flag:
return False
return True
二、链表删除的相关题型
第一题:移除链表元素
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点
情形1对应解法:不借助于虚拟头结点,直接使用原来的链表来进行移除节点操作,那么删除头结点的操作需要另做考虑
python题解代码
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
while head and head.val == val: # [7, 7, 7, 7]对于要移除的节点为链表的头结点需要用另外的逻辑来处理
head = head.next
if not head: # 如果链表为空,直接返回
return head
prev = head # prev指向头结点(用来指向待移除节点的前一个节点)
while prev.next: # 注意这里的条件!!!
if prev.next.val == val:
prev.next = prev.next.next
else:
prev = prev.next
return head
情形2对应解法:设置一个虚拟头结点在进行删除操作(设置虚拟头结点后,对原链表中头结点则无需做特殊处理)
python题解代码
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
if not head:
return head
dummy = ListNode() # 创建一个虚拟头结点
dummy.next = head # 和原链表连接起来
prev = dummy # prev指针指向虚拟头结点
while prev.next:
if prev.next.val == val:
prev.next = prev.next.next
else:
prev = prev.next
return dummy.next
注意上面prev
指针为什么不直接指向待删除的节点,第二个while
循环的条件中为什么是while prev.next
而不是while prev
,别急,第二题中会有详细介绍为什么
第二题:删除链表的倒数第 N 个结点
Leetcode19:删除链表的倒数第 N 个结点:中等题 详情请点击链接看原题
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点
题目提示中:链表中结点的数目为sz
,1 <= sz <= 30
,1 <= n <= sz
,所以我们不用担心n
会超出链表的长度
快慢指针大显神威
step1:题目要求返回链表的头结点,所以这里我们创建一个虚拟头结点,让它的next
指针指向head
【常规操作】用于最后返回链表的头结点
step2:快慢指针同时指向头结点head
,快指针先行 n
步,然后快慢指针同时移动,当快指针指向最后一个节点(即 fast.next=None 的时候)
,慢指针指向节点的下一个节点(slow.next)
即我们要删除的倒数第n个节点
step3:快慢指针同时移动之前需要先判断快指针是否指向末尾,如果快指针为None
说明,n
正好等于链表的长度即要删除的倒数第 n
个节点正好是正数第1
个节点
python题解代码
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy_head = ListNode() # 创建一个虚拟头结点
dummy_head.next = head
slow, fast = head, head # 快慢指针同时指向链表头结点
for i in range(n):
fast = fast.next
if not fast: # 如果快指针指到头了,第一个节点就是待删除的节点
return slow.next
while fast.next: # 注意这里的条件 !!!
fast = fast.next
slow = slow.next
slow.next = slow.next.next # 删除 slow.next 节点操作
return dummy_head.next
注:请同学们思考,为什么快指针指向最后一个节点的时候,慢指针指向节点的下一个节点才是我们要删除的倒数第n
个节点呢?
假设链表的长度为
5
,我们要删除倒数第2
个节点,同学们可以在纸上画一下,我们先让快指针移动2
步,快慢指针同时移动,当快指针指向None
的时候,慢指针正好指向倒数第2
个节点(即我们要删除的节点)
But…
但由于链表的特性,对链表的删除操作而言,我们是无法直接删除某个指针所指向的节点的,但我们可以很轻松的删除该指针指向的节点的下一个节点,所以我们必须要让
slow
指针停留在待删除节点的前一个位置,所以我们让fast
指针指向最后一个节点,slow
指向待删除节点的前驱结点
的时候就跳出循环
第三题:删除链表中的节点
Leetcode237:删除链表中的节点:中等题 详情请点击链接看原题
有一个单链表的
head
,我们想删除它其中的一个节点node
,给你一个需要删除的节点node
。在本题中你将无法访问头结点head
题目的限制条件:链表的所有值都是唯一的,并且保证给定的节点node
不是链表中的最后一个节点(这里不做过多赘述,请看原题)
题目分析
我们删除链表中节点的操作一般是修改要删除节点的上一个节点的指针,将该指针指向要删除节点的下一个节点
相信同学们看到这道题的第一反应肯定很懵,要你删除链表中的节点却没有给出链表头结点head
,我们如何从头结点
遍历到待删除节点的上一个节点
其实不然,这道题无需你从头结点遍历到指定的节点,题目所给的node
参数已经指向了要删除的节点,but 我们也无法直接删除node
指针指向的节点
狸猫换太子…
从第二题
我们可以知道,既然我们无法直接删除给定的节点,那我们可以将所给节点的下一个节点的val
拿过来覆盖自己本来的val
,然后删除所给节点的下一个节点就可以达到同样的目的
python题解代码
【一共就两行代码】
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
三、链表合并
第一题:合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
python题解代码
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode() # 创建一个虚拟头结点
cur = dummy # 创建一个指针指向虚拟头结点
l1, l2 = list1, list2
while l1 and l2:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
cur.next = l1 if l1 else l2
return dummy.next
第二题:合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表
方法1:最小堆(需要具备堆的基础概念)
方法1需要大家对堆的基础知识有一定的了解,如果不熟悉的同学请先去了解一下
最大堆
,最小堆
的概念以及如何进行堆调整
,堆中节点的下沉
与上浮
,没搞清楚这些概念之前这个方法会很难理解
分析
- 合并后的第一个节点
first
一定是某个链表的头结点(因为链表已经升序排列), - 合并后的第二个节点可能是某个链表的头结点,也可能是
first
的下一个节点,那么每当我们找到一个节点值最小的节点x
,我们就把节点x.next
加入【可能是最小节点的集合】中因此我们可以利用最小堆
来充当这个可能是最小节点的集合
在了解了上述概念之后其实并不用大家动手写关于堆的代码,python
中有个标准库heapq
里面封装好了关于堆的操作方法,现在让我来给大家介绍一下
heapq.heappush(heap, num):
先创建一个空堆,然后将数据一个一个地添加到堆中。每添加一个数据后,heap
都满足小顶堆的特性
heapq.heappop(heap):
将堆顶的数据出堆,并将堆中剩余的数据构造成新的小顶堆
python题解代码
import heapq
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
dummy = ListNode(0)
p = dummy
heap = []
for i in range(len(lists)):
if lists[i]:
heapq.heappush(heap, (lists[i].val, i)) # 将所有链表的头结点入堆(这里根据lists[i].val构建最小堆)
lists[i] = lists[i].next # 指针后移
while heap:
val, idx = heapq.heappop(heap) # 将堆顶的最小元素弹出
p.next = ListNode(val)
p = p.next
if lists[idx]: # 如果弹出的最小节点的下个节点不为空(则有可能为最小)
heapq.heappush(heap, (lists[idx].val, idx)) # 继续入堆(重新自动构建最小堆)
lists[idx] = lists[idx].next
return dummy.next
def create_list(linklist):
"""
:param linklist:拿来构建链表的数组
:return: head-返回链表的头结点
"""
node_list = []
for i in range(len(linklist)):
node = ListNode()
node.val = linklist[i]
node_list.append(node)
head = node_list[0]
p = head
for node in node_list[1:]:
p.next = node
p = p.next
return head
if __name__ == '__main__':
s = Solution()
res = []
lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
for lis in lists:
res.append(create_list(lis))
after_head = s.mergeKLists(res)
while after_head:
print(after_head.val)
after_head = after_head.next
对了堆也是大厂面试中的高频考点,对于这道题而言,主要考察的是你对于链表的操作,在面试中你直接使用python
的heapq
封好好的功能也是没有问题的
方法2:未完待续
四、链表相关的其他题型
第一题:两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)
方法一:分析
初始时,cur
指向虚拟头结点
这里建议大家画图,能用纸笔画就用纸笔画,身边没纸笔用你的画图工具
注意,链表的索引只能根据上一个节点的next
来找到下一个节点的位置
cur.next = cur.next.next # 步骤一
所以执行完骤一(将头结点指向原本的第一个节点改为指向第二个节点)之后我们无法找到第一个节点
temp1 = cur.next # 先将第一个节点的位置暂存起来
同理执行完步骤二(将第二个节点和链表断开)后我们也无法找到第三个节点
temp2 = cur.next.next.next # 将第三个节点的位置暂存起来
方法1总结
因为要通过改变
cur节点
的next
和cur.next.next节点
的next
的指向来交换cur.next
和cur.next.next
节点
所以我们需要先将cur节点
和cur.next.next节点
这两个节点原来的指向暂存起来,即temp1 = cur.next, temp2 = cur.next.next.next
python题解代码
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode()
dummy.next = head
cur = dummy
while cur.next and cur.next.next:
temp1 = cur.next
temp2 = cur.next.next.next
cur.next = cur.next.next
cur.next.next = temp1
cur.next.next.next = temp2
cur = cur.next.next # cur移动两位,准备下一轮交换
return dummy.next
方法2分析:递归法
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
# 待翻转的两个node分别是pre和cur
pre = head
cur = head.next
next_node = head.next.next
cur.next = pre
pre.next = self.swapPairs(next_node)
return cur
第二题:相交链表
Leetcode160:相交链表:简单题 详情请点击链接看原题
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
分析
严格证明取自力扣K神大佬题解
设【第一个公共节点为】node
,【链表 headA】的节点数量为 a
,【链表 headB】的节点数量为 b
,【两链表的公共尾部】的节点数量为 c
,则有:
- 头节点
headA
到node
前共有a - c
个节点 - 头节点
headB
到node
前共有b - c
个节点 - 指针
A
先遍历完链表headA
,再开始遍历链表headB
,当走到node
时,共走步数为:a + (b − c)
- 指针
B
先遍历完链表headB
,再开始遍历链表headA
,当走到node
时,共走步数为:b + (a − c)
因为a + (b - c) = b + (a - c)
,此时指针A
, B
重合(两指针相遇即链表相交),并有两种情况:
case1:若两链表有公共尾部(
即c > 0
):指针A
,B
同时指向第一个公共节点node
case2:若两链表无公共尾部(即c=0
),指针A
,B
同时指向None
python题解代码
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
A, B = headA, headB
while A != B: # 当A==B时推出循环,两指针同时扫描到交点或同时指向 None
A = A.next if A else headB
B = B.next if B else headA
return A
换种更容易理解的思路,若两链表相交,交点为c
,链表A的长度为a + c
,链表B的长度为b + c
,两个头节点指针分别走过对方来时的路则有a + c + b + c = b + c + a + c
,则证明两链表相交与一点,若不相交,两头节点互相走过对方来时的路有a + b = b + a
,两指针刚好同时指向None
两个人走的路一样,走路的速度一样,两条路有交点必在交点相遇,没交点则在在终点(None)相遇
第三题:交换链表中的节点
Leetcode1721:交换链表中的节点:中等题 详情请点击链接看原题
给你链表的头节点
head
和一个整数k
。
交换 链表正数第k
个节点和倒数第k
个节点的值后,返回链表的头节点(链表 从1
开始索引
分析
本题有两种解法,值交换(偷天换日)和节点交换,力扣上博主的题解总结的很好,这里只介绍第一种解法,节点交换需要考虑的边界条件太复杂,不想写了
方法1:值交换-找到倒数第 k 个节点和第 k 个节点后进行值交换
找第
k
个节点非常简单,找倒数第k
个节点和前文中的删除倒数第k个节点
的方法是一样的
python题解代码
class Solution:
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
slow, fast = head, head
# 因为从头节点(第一个节点)开始遍历,循环k-1次即指向第k个节点
for _ in range(k - 1):
fast = fast.next
cur = fast
# 同理当cur指向最后一个节点(cur.next为空)的时候,slow正好指向倒数第k个节点
while cur.next:
slow = slow.next
cur = cur.next
fast.val, slow.val = slow.val, fast.val # 第k个节点和倒数第k个节点做值交换
return head
第四题:环形链表I
Leetcode141:环形链表I:简单题 详情请点击链接看原题
给你一个链表的头节点
head
,判断链表中是否有环
分析
设有两个指针 slow
和 fast
初始时均指向头结点,slow
向后走一次,fast
向后走两次,在每轮移动之后,fast
和 slow
的距离就会增加 1
,如果链表有环,fast
指针一定先进入环中(继续移动),等slow
指针进入环中后,二者继续移动,fast
指针和slow
指针的距离+1,可是在环中,距离+1也意味着距离-1(距离拉开的同时距离也正在缩小)
根据物理学上的相对运动来说,其实就相当于
slow
指针静止,fast
指针每次移动一个距离向slow
指针靠近,所以二者是一定会在环中相遇的
python题解代码
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if fast == slow:
return True
return False
第五题:环形链表II
Leetcode142:环形链表II:中等题 详情请点击链接看原题
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
分析
这道题的题解分析很多博主都给出非常严格的数学证明,在本文中,作者尽量用最少的文字简化分析让大家能够快速理解
从上一题可以知道,
slow
走一步,fast
走两步,链表有环则二者一定会相遇,假设从头结点到环形入口节点 的节点数为x
。 环形入口节点到相遇节点节点数为y
。 从相遇节点再到环形入口节点节点数为z
从上一题的分析中我们可以知道,fast
指针不可能跨过slow
指针而不与slow
指针相遇,所以slow
指针不存在绕环一圈的情况
则相遇时slow
指针走过的节点数为 x + y
, fast
指针走过的节点数:x + y + n (y + z)
,x
可能很长,环很短,fast
指针在环中走了n
圈才遇到slow
指针
fast
指针走过的节点数 =slow
指针走过的节点数 *2
(x + y) * 2 = x + y + n (y + z)
,利用初中所学的数学知识进行化简,x = (n - 1) (y + z) + z
注意这里 n
一定是大于等于 1
的,因为 fast
指针至少要多走一圈才能相遇 slow
指针,当 n
为 1
的时候,公式就化解为 x = z
x = z
:意味着从头结点
出发一个指针,从相遇节点
也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点
到此,我们可以在相遇节点处定义一个指针index1
,在头结点处定义一个指针index2
,index1
和 index2
同时移动,每次移动一个节点,那么它们相遇的地方就是环形的入口节点,如果 n > 1
则说明 index1
指针在环里多转了 (n - 1)
圈再遇到 index2
,相遇节点依然是环形的入口节点
python题解代码
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow, fast = head, head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
if fast == slow:
index1 = fast # 定义index1指向相遇节点
index2 = head # 定义index2指向头结点
while index2 != index1: # 同时移动index1和index2直到二者相遇
index1 = index1.next
index2 = index2.next
return index1
return None
第六题:反转链表
Leetcode206:反转链表:简单题 详情请点击链接看原题
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表
方法1: 头插法反转单链表
python题解代码
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode()
cur_node = head
while cur_node:
next_node = cur_node.next # 将头结点断链,保存剩余链表到 next_node
cur_node.next = dummy.next
dummy.next = cur_node
cur_node = next_node # cur_node 重新指向剩余链表的头结点
return dummy.next
方法2: 递归解法
使用递归法遍历链表,当越过尾节点后终止递归,在回溯时修改各节点的next
引用指向
recur(cur, pre)
递归函数
- 终止条件:当
cur
为空,则返回尾节点pre
(即反转链表的头结点) - 递归后继节点,记录返回值为
res
- 修改当前节点
cur
引用指向前驱节点pre
- 修改反转链表的头结点
res
第七题:反转链表 II
Leetcode92. 反转链表 II:中等题 详情请点击链接看原题
给你单链表的头指针
head
和两个整数left
和right
,其中left <= right
。请你反转从位置left
到位置right
的链表节点,返回 反转后的链表
比反转链表多了一步是我们要把抽取出来反转后的区间拼接回去,故我们需要知道四个定位节点:
reverseHead
:反转区间的前一个节点
reverseHead
:反转区间的头结点
reverseTail
:反转区间的尾结点
reverseNext
:反转区间的下一个节点
python题解代码
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
dummy_head = ListNode()
dummy_head.next = head
prev_head = dummy_head
count = 1
while count < left:
prev_head = prev_head.next
count += 1
reverse_head = prev_head.next
cur_node = reverse_head
next_node = None
while count <= right:
next_node = cur_node.next
cur_node.next = prev_head.next
prev_head.next = cur_node
cur_node = next_node
count += 1
p = head
i = 1
while i <= right:
p = p.next
i += 1
p.next = next_node
return dummy_head.next
第八题:分隔链表
Leetcode725. 分隔链表:中等题 详情请点击链接看原题
给你一个头结点为
head
的单链表和一个整数k
,请你设计一个算法将链表分隔为k
个连续的部分。
每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过1
。这可能会导致有些部分为null
解题思路
尽可能将链表平均分为 k
份, 无法均分时,应当使前面比后面多,我们可以先对链表进行一次扫描,得到总长度 cnt
,再结合需要将链表划分为 k
份,可知每一份的最小分配单位为 per = cnt // k
从前往后切分出 k
份链表,由于是在原链表的基础上进行,因此这里的切分只需要在合适的位置将结点的 next
指针置空即可
python题解代码
class Solution:
def splitListToParts(self, head: Optional[ListNode], k: int) -> List[Optional[ListNode]]:
if not head:
return [None] * k
p, length = head, 0
while p:
length, p = length + 1, p.next
res, cur = [], head
a, b = divmod(length, k)
while k:
if not cur:
break
res.append(cur)
cur_len = a + (1 if b > 0 else 0)
b -= 1
for _ in range(cur_len - 1):
cur = cur.next
cur.next, cur = None, cur.next
k -= 1
res += [None] * k
return res
第九题:分隔链表
Leetcode86. 分隔链表:中等题 详情请点击链接看原题
给你一个链表的头节点
head
和一个特定值x
,请你对链表进行分隔,使得所有 小于x
的节点都出现在 大于或等于 x 的节点之前
解题思路
- 新建两个链表
small_dummy
,big_dummy
,分别用于添加所有「节点值 <x
」和「节点值x≥x
」的节点 - 遍历链表
head
并依次比较各节点值head.val
和x
的大小,比x
小的放在small_dummy
后面,比x
大的放在big_dummy
后面 - 遍历完成后,拼接
small_dummy
和big_dummy
链表,最后返回small_dummy.next
即可
python题解代码
class Solution:
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
small_dummy = ListNode()
big_dummy = ListNode()
small, big = small_dummy, big_dummy
p = head
while p:
if p.val < x:
small.next = ListNode(p.val)
small = small.next
else:
big.next = ListNode(p.val)
big = big.next
p = p.next
small.next = big_dummy.next
return small_dummy.next
第十题:奇偶链表
Leetcode328. 奇偶链表:中等题 详情请点击链接看原题
给定单链表的头节点
head
,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表
注意看:该题的解题思路与上题几乎如初一致,只是换了个壳子而已
python题解代码
class Solution:
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
odd_dummy = ListNode()
even_dummy = ListNode()
odd, even = odd_dummy, even_dummy
p = head
i = 0
while p:
if i % 2 == 0:
odd.next = ListNode(p.val)
odd = odd.next
else:
even.next = ListNode(p.val)
even = even.next
p = p.next
i += 1
odd.next = even_dummy.next
return odd_dummy.next
第十一题:K 个一组翻转链表
Leetcode25. K 个一组翻转链表:困难题 (详情点击链接见原题)
给你链表的头节点
head
,每k
个节点一组进行翻转,请你返回修改后的链表
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换
解题步骤
- 链表分区为已翻转部分+待翻转部分+未翻转部分
- 每次翻转前,要确定翻转链表的范围,这个必须通过
k
此循环来定 - 需记录翻转链表的前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
- 初始需要两个变量
pre
和end
,pre
代表待翻转链表的前驱,end
代表待翻转链表的末尾 - 经过
k
次循环,end
到达末尾,记录待翻转链表的后继next = end.next
- 翻转链表,然后将三部分连接起来,然后重置
pre
和end
指针,然后进入下一次循环【特殊情况:当翻转部分长度不足k
时,在定位end
完成后,直接返回即可】
python题解代码(头插)
class Solution:
def reverse(self, head): # 头插法反转链表
dummy = ListNode()
cur_node = head
while cur_node:
next_node = cur_node.next
cur_node.next = dummy.next
dummy.next = cur_node
cur_node = next_node
return dummy.next
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy_head = ListNode()
dummy_head.next = head
pre, end = dummy_head, dummy_head
while end.next:
i = 0
while i < k and end:
end = end.next
i += 1
if not end: # 说明结点总数不是 k 的整数倍,将最后剩余的结点保持原有顺序
break
start = pre.next
next = end.next # 保留剩余的结点用于后面的赋值
end.next = None # 断链
pre.next = self.reverse(start) # 连接反转后的链表
start.next = next # start指向下一个范围的起始结点
pre = end = start
return dummy_head.next
python题解代码(尾插)
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy_head = ListNode()
dummy_head.next = head
pre = tail = dummy_head
while True:
count = k
while count and tail:
count -= 1
tail = tail.next
if not tail:
break
next_dummy = pre.next # 保存用来当作下一个虚拟头结点
while pre.next != tail: # 尾插法反转
cur = pre.next # 获取下一个元素
pre.next = cur.next # 连接当前结点的下一个结点
cur.next = tail.next # 和剩余的链表连接起来
tail.next = cur # 插在 tail 后面
pre = tail = next_dummy
return dummy_head.next
五、链表在位运算中的应用
第一题:两数相加
Leetcode2:两数相加:中等题 详情请点击链接看原题
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字0
之外,这两个数都不会以0
开头
分析(使用链表实现大数加法
)
- 将两个链表看成是相同长度进行遍历,如果一个数较短则在前面补
0
如987 + 23 = 987 + 023
数不以
0
开头,但是当某个数较短的时候需要在前面补0
,数字在链表中是按照 逆序 的方式存储的,所以当我们遍历到链表末尾的时候若其中一个链表不为空另一个链表为空,则需要在链表的后面补0
为了达到前面说的在数字前面补0
保证数字对齐
7——>8——>9
3——>2——>0
- 对每一位计算的同时需要考虑上一位的进位问题,并当前位计算结束后同样需要更新进位值
- 如果两个链表遍历完毕后进位值为
1
,则在新链表最前方添加结点1
python题解代码
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode()
cur_node = dummy
carry = 0 # 进位
while l1 or l2:
x = 0 if not l1 else l1.val # 如果一个链表为空另一个不为空则需要在后面补0
y = 0 if not l2 else l2.val
total = x + y + carry # 从两个数字的末尾(两个链表的头)开始计算
carry = total / 10 # 记录当前两个数之和的进位
total %= 10 # 取个位
cur_node.next = ListNode(total) # 将实例化的结点放在头结点dummy之后
cur_node = cur_node.next # cur_node指针右移准备计算下一位
if l1:
l1 = l1.next # l1不为空则l1右移
if l2:
l2 = l2.next # l2不为空则l2右移
if carry == 1: # 如果两个链表遍历完毕后进位值为 1,则在新链表最末尾(即数的最高位)添加结点 1
cur_node.next = ListNode(carry)
return dummy.next
第二题:两数相加 II
Leetcode445. 两数相加 II:中等题 详情请点击链接看原题
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表
解题思路:两数相加+反转链表
python题解代码
class Solution:
def reverse_link(self, head):
dummy_head = ListNode()
cur = head
while cur:
next_node = cur.next
cur.next = dummy_head.next
dummy_head.next = cur
cur = next_node
return dummy_head.next
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
p1 = self.reverse_link(l1)
p2 = self.reverse_link(l2)
dummy_head = ListNode()
p = dummy_head
carry = 0
while p1 or p2:
x = p1.val if p1 else 0
y = p2.val if p2 else 0
cur = x + y + carry
if cur > 9:
cur -= 10
carry = 1
else:
carry = 0
p.next = ListNode(cur)
p = p.next
if p1:
p1 = p1.next
if p2:
p2 = p2.next
if carry == 1:
p.next = ListNode(1)
ans = self.reverse_link(dummy_head.next)
return ans
六、链表在排序中的应用
第一题:排序链表
Leetcode148. 排序链表:中等题 详情请点击链接看原题
给你链表的头结点
head
,请将其按 升序 排列并返回 排序后的链表
python题解代码
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next: # 1.递归终止条件:空链表或者只有一个结点的链表,递归结束
return head
# 2.分割环节
slow, fast = head, head
while fast.next and fast.next.next:
fast, slow = fast.next.next, slow.next # 快指针走两步慢指针走一步,当快指针走到末尾,慢指针指向链表中点
mid, slow.next = slow.next, None # 断开
left = self.sortList(head)
right = self.sortList(mid)
# 3. 合并环节
p = dummy_head = ListNode()
while left and right:
if left.val < right.val:
p.next = left
left = left.next
else:
p.next = right
right = right.next
p = p.next
p.next = left if left else right
return dummy_head.next
七、链表与其他数据结构的综合应用
第一题:LRU缓存
Leetcode146:LRU缓存:中等题 详情请点击链接看原题
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构
博主水平有限,算法设计思路来源于力扣大佬题解,博主只在这里做个总结,方便大家和本人理解
要让put
和get
方法的时间复杂度为O(1)
,cache
这个数据结构的必要条件
1.
cache
中的元素必须有时序以区分最近使用和久未使用的数据,容量满了以后要删除最久未使用的那个元素
2.我们要在cache
中快速找某个key
是否已存在并得到对应的val
3.每次访问cache
中的某个key
,需要将这个元素变为最近使用的,也就是说cache
要支持在任意位置快速插入和删除元素
这个数据接口应具备如下节点:
1.支持快速查找
2.保存访问的先后顺序,在末尾加入一项,去除最前端一项
3.将队列中的某一项移到末尾
哈希表查找快,但数据无固定顺序,链表有顺序之分但是查找慢,所以二者结合一下形成一种新的数据结构,这就是大名鼎鼎的哈希链表 LinkedHashMap
1.默认
put
操作从链表尾部添加元素,那么显然越靠近尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的(因为是双链表,你也可以在链表头部添加元素,把头部元素当成是最近使用的元素)
2.对于某一个key
,我们可以通过哈希表快速定位到链表中的结点从而取得对应的val
3.链表虽然支持通过修改指针在任意位置插入和删除,只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助于哈希表,通过key
快速映射到任意一个链表结点,然后进行插入和删除
这里为啥用双链表而不是单链表?
从前几题的分析我们可以知道删除一个节点不光要得到该节点本身的指针,也需要操作其前驱结点的指针
,而我们用哈希表只能定位到需删除的节点,无法定位到其前驱结点,而双向链表就支持直接查找前驱,保证操作的时间复杂度为O(1)
python题解代码
class ListNode:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hash_map = {}
self.head, self.tail = ListNode(), ListNode()
self.head.next = self.tail
self.tail.prev = self.head
def move_to_head(self, key):
"""
即双链表的一个查找和插入操作
:param key:
:return:
"""
node = self.hash_map[key] # 找到 key 对应的结点
# 将该节点与原链表断链
node.prev.next = node.next
node.next.prev = node.prev
# 将最近访问的节点放到头结点后
node.next = self.head.next
node.prev = self.head.next.prev
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
if key in self.hash_map: # 如果关键字 key 已经存在
self.move_to_head(key) # 将该 key 对应的结点插入到头结点后更新为最近访问
return -1 if self.hash_map.get(key, -1) == -1 else self.hash_map[key].value # 返回关键字对应的值
def put(self, key: int, value: int) -> None:
if key in self.hash_map: # 如果要插入结点的 key 已存在
self.hash_map[key].value = value # 变更其数据值
self.move_to_head(key) # 将该 key 对应的结点插入到头结点后更新为最近访问
else:
if len(self.hash_map) == self.capacity: # 如果容量已满,去掉最久没有被访问的节点
self.hash_map.pop(self.tail.prev.key) # 去掉尾节点的前一个节点(最久未使用)
self.tail.prev.prev.next = self.tail
self.tail.prev = self.tail.prev.prev
new_node = ListNode(key, value) # 申请一个新结点
self.hash_map[key] = new_node # 将该结点与哈希表中的key建立映射关系
# 将新结点插入到头结点之后
new_node.next = self.head.next
new_node.prev = self.head.next.prev
self.head.next.prev = new_node
self.head.next = new_node
其实对于本题来说核心考察点就是双链表的插入和删除操作,需要注意细节和前后顺序
第二题:随机链表的复制
Leetcode138. 随机链表的复制:中等题 详情请点击链接看原题
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点
给定链表的头结点 head
,复制普通链表很简单,只需遍历链表,每轮建立新结点 + 构建前驱结点 pre
和当前结点 node
的引用指向即可,本题链表的结点新增了 random
指针指向链表中的任意结点或者 NULL
,这个random
指针意味着在复制过程中,除了构建前驱节点和当前结点的引用指向 pre.next
,还要构建前驱结点和其随机结点的引用指向
- 若头结点
head
为空结点,直接返回None
- 初始化哈希表
hash_map
,结点cur
指向原链表的头结点 - 复制链表:建立新结点,并向
hash_map
中添加键值对(原 cur 结点:利用原cur结点的val创建新结点)
,cur
遍历至原链表下一结点 - 构建新链表的引用指向:构建新结点的
next
和random
引用指向 - 返回值:返回新链表的头结点
hash_map[cur]
python题解代码
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return head
hash_map = {}
cur = head
while cur:
hash_map[cur] = Node(cur.val)
cur = cur.next
cur = head
while cur:
hash_map[cur].next = hash_map.get(cur.next)
hash_map[cur].random = hash_map.get(cur.random)
cur = cur.next
return hash_map[head]
总结
本文给大家总结了大厂面试中喜欢考察的关于链表的相关题型,对于链表题,双指针的快慢指针通常是最常用的,注意一下链表的插入和删除操作中的顺序要求,顺序乱了整个链表就断裂了,主要帮助大家总结了考点以及清晰明了的题解方便大家理解,如果你觉得本文对你有帮助的话,还请点赞收藏转发,最后,祝大家都能找到心仪的工作呀~