提示:100道LeetCode热题-7.3主要是链表相关,包括四题:删除链表的倒数第 N 个结点、两两交换链表中的节点、K 个一组翻转链表、随机链表的复制。由于初学,所以我的代码部分仅供参考。
目录
前言
截取的这四题有助于对链表节点等问题又更深入的认识,后面还会更新有关链表排序问题相关题目,大家有空可以看看o(* ̄▽ ̄*)ブ
提示:以下是本篇文章正文内容,下面结果代码仅供参考
题目1:删除链表的倒数第 N 个结点
1.题目要求:
题目如下:
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]示例 2:
输入:head = [1], n = 1 输出:[]示例 3:
输入:head = [1,2], n = 1 输出:[1]提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
代码框架已经提供如下:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def removeNthFromEnd(self, head, n):
"""
:type head: Optional[ListNode]
:type n: int
:rtype: Optional[ListNode]
"""
2.结果代码:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def removeNthFromEnd(self, head, n):
"""
:type head: Optional[ListNode]
:type n: int
:rtype: Optional[ListNode]
"""
# 创建一个虚拟头节点,方便处理删除头节点的情况
dummy = ListNode(0)
dummy.next = head
# 初始化两个指针
p1 = dummy
p2 = dummy
# 让 p1 先移动 n 步
for _ in range(n):
p1 = p1.next
# 同时移动 p1 和 p2,直到 p1 到达链表末尾
while p1.next:
p1 = p1.next
p2 = p2.next
# 删除 p2 的下一个节点
p2.next = p2.next.next
# 返回新的头节点
return dummy.next
说明:
1)这题使用双指针法。具体思路是:
-
使用两个指针 p1 和 p2,初始时都指向头节点。
-
先让 p1 向前移动 n 步,这样 p1 和 p2 之间的距离就是 n。
-
然后同时移动 p1 和 p2,当 p1 到达链表末尾时,p2 的下一个节点就是需要删除的节点。
-
修改指针,跳过需要删除的节点。
2)这题最大的提升点是方法只需要一次遍历链表,时间复杂度为 O(L),其中 L 是链表的长度。
题目2:两两交换链表中的节点
1.题目要求:
题目如下:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]示例 2:
输入:head = [] 输出:[]示例 3:
输入:head = [1] 输出:[1]提示:
- 链表中节点的数目在范围
[0, 100]
内0 <= Node.val <= 100
代码框架已经提供如下:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def swapPairs(self, head):
"""
:type head: Optional[ListNode]
:rtype: Optional[ListNode]
"""
2.结果代码:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def swapPairs(self, head):
"""
:type head: Optional[ListNode]
:rtype: Optional[ListNode]
"""
# 创建一个虚拟头节点,方便处理头节点的交换
dummy = ListNode(0)
dummy.next = head
prev = dummy
# 当链表中还有至少两个节点时进行交换
while prev.next and prev.next.next:
# 获取要交换的两个节点
first = prev.next
second = prev.next.next
# 交换两个节点
prev.next = second
first.next = second.next
second.next = first
# 更新 prev 指针,继续处理下一对节点
prev = first
# 返回新的头节点
return dummy.next
说明:我的代码解题思路
-
特殊情况处理:
-
如果链表为空(
head
为None
),直接返回None
。 -
如果链表只有一个节点,直接返回该节点。
-
-
两两交换节点:
-
使用一个虚拟头节点(
dummy
),它的next
指向链表的头节点。这样可以方便处理头节点的交换。 -
使用一个指针(
prev
)来记录当前交换的前一个节点。 -
每次交换两个节点后,更新指针,继续处理下一对节点。
-
-
终止条件:
-
当链表中剩余节点不足两个时,停止交换。
-
-
遍历链表一次,时间复杂度为 O(L),其中 L 是链表的长度。
-
空间复杂度为 O(1),因为只使用了常数级别的额外空间。
题目3:K 个一组翻转链表
1.题目要求:
题目如下:
给你链表的头节点
head
,每k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是k
的整数倍,那么请将最后剩余的节点保持原有顺序。你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5]示例 2:
输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]提示:
- 链表中的节点数目为
n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
进阶:你可以设计一个只用
O(1)
额外内存空间的算法解决此问题吗?
代码框架已经提供如下:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def reverseKGroup(self, head, k):
"""
:type head: Optional[ListNode]
:type k: int
:rtype: Optional[ListNode]
"""
2.结果代码:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def reverseKGroup(self, head, k):
"""
:type head: Optional[ListNode]
:type k: int
:rtype: Optional[ListNode]
"""
# 统计链表的总节点数
n = 0
cur = head
while cur: # 遍历链表,统计节点个数
n += 1
cur = cur.next
# 创建哨兵节点,方便处理链表头节点的反转
dummy = ListNode(next=head)
p0 = dummy # 初始化指针 p0,指向哨兵节点
# 当链表剩余节点数不少于 k 时,进行反转操作
while n >= k:
n -= k # 每次处理 k 个节点后,剩余节点数减 k
# 反转当前组的 k 个节点
pre = None # 初始化 pre 指针,用于反转链表
cur = p0.next # cur 指向当前组的第一个节点
for _ in range(k): # 反转 k 个节点
t = cur.next # 保存 cur 的下一个节点
cur.next = pre # 将 cur 的 next 指向 pre,实现反转
pre = cur # 更新 pre 指针
cur = t # 更新 cur 指针
# 重新连接链表
t = p0.next # t 指向当前组的第一个节点(反转后的最后一个节点)
p0.next.next = cur # 将反转后的最后一个节点的 next 指向下一组的第一个节点
p0.next = pre # 将 p0 的 next 指向反转后的第一个节点
p0 = t # 更新 p0 指针,指向下一组的第一个节点
# 返回反转后的链表头节点
return dummy.next
说明:
1)这题最大的难点有特殊情况的处理:
-
链表长度不足 k 个节点:如果链表的长度小于 k,则直接返回原链表,不做任何翻转。
-
链表长度不是 k 的整数倍:最后剩余的节点不足 k 个时,需要保持原有顺序,不能进行翻转。
-
翻转头节点:当翻转操作涉及头节点时,需要特别处理,因为头节点的翻转会改变链表的头指针。
2)这题最大的提升点:
-
时间复杂度:O(L),其中 L 是链表的长度。虽然代码中有两层循环,但每个节点只被访问一次。
-
空间复杂度:O(1),只使用了常数级别的额外空间。
题目4:随机链表的复制
1.题目要求:
题目如下:
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由
n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。例如,如果原链表中有
X
和Y
两个节点,其中X.random --> Y
。那么在复制链表中对应的两个节点x
和y
,同样有x.random --> y
。返回复制链表的头节点。
用一个由
n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。你的代码 只 接受原链表的头节点
head
作为传入参数。示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]示例 2:
输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]]示例 3:
输入:head = [[3,null],[3,0],[3,null]] 输出:[[3,null],[3,0],[3,null]]提示:
0 <= n <= 1000
-104 <= Node.val <= 104
Node.random
为null
或指向链表中的节点。
代码框架已经提供如下:
"""
# Definition for a Node.
class Node:
def __init__(self, x, next=None, random=None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution(object):
def copyRandomList(self, head):
"""
:type head: Node
:rtype: Node
"""
2.结果代码:
"""
# Definition for a Node.
class Node:
def __init__(self, x, next=None, random=None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head):
if not head:
return None
# 第一步:复制每个节点并插入到原节点之后
current = head
while current:
new_node = Node(current.val, current.next, None)
current.next = new_node
current = new_node.next
# 第二步:设置随机指针
current = head
while current:
if current.random:
current.next.random = current.random.next
current = current.next.next
# 第三步:拆分链表
old_list = head
new_list = head.next
new_head = head.next
while old_list:
old_list.next = old_list.next.next
if new_list.next:
new_list.next = new_list.next.next
old_list = old_list.next
new_list = new_list.next
return new_head
说明:
1)这题的难点:
-
随机指针的处理:
-
随机指针可能指向链表中的任意节点,甚至可能形成环。直接复制节点时,无法直接确定随机指针的目标节点。
-
需要一种方法来记录原节点与新节点之间的映射关系,以便正确设置随机指针。
-
-
避免重复创建节点:
-
如果直接遍历链表并创建新节点,可能会因为随机指针的存在而多次创建同一个节点,导致错误。
-
-
高效实现:
-
需要一种高效的方法来完成深拷贝,避免多次遍历链表。
-
因此,可以使用 三步法 来解决这个问题,这种方法的时间复杂度为 O(n),空间复杂度为 O(1):
第一步:复制每个节点并插入到原节点之后
-
遍历链表,对于每个节点 Node,创建一个新节点 Node’,并将 Node’ 插入到 Node 和 Node.next 之间。
-
例如,原链表为 A→B→C,复制后变为 A→A′→B→B′→C→C′。
第二步:设置随机指针
-
再次遍历链表,对于每个原节点 Node,其新节点 Node’ 的随机指针可以通过 Node.random.next 来设置。
-
例如,如果 Node.random 指向 Node2,那么 Node’.random 应指向 Node2.next。
第三步:拆分链表
-
最后,将链表拆分为两个独立的链表:原链表和新链表。
-
遍历链表,恢复原链表的
next
指针,并提取新链表的节点。
2)这题最大的提升点:
-
时间复杂度:O(n),其中 n 是链表的长度。每个节点只被访问三次(复制、设置随机指针、拆分)。
-
空间复杂度:O(1),除了新链表外,只使用了常数级别的额外空间。
总结
针对链表的四种题型进行了学习,了解了部分有关链表与python的相关知识,大家加油!