链表中相关算法探讨
- 前言
- 一、移除链表元素
- 1.1 思路分析
- 1.2 解法探讨
- 1.2.1 直接删除
- 1.2.2 创建虚拟头节点来删除
- 1.2.3 递归版删除
- 二、反转列表
- 2.1 思路分析
- 2.2 做法
- 2.2.1 创建新链表方式
- 2.2.2 双指针法
- 2.2.3 递归法
- 三、两两交换链表中的节点
- 3.1 思路分析
- 3.2 解法探讨
- 3.2.1 不使用虚拟头节点(递归法)
- 3.2.2 使用虚拟头节点
- 四、删除链表的倒数第N个节点
- 4.1 思路分析
- 4.2 解法探讨
- 4.2.1 两次遍历
- 4.2.2 双指针法
- 4.2.2.1 一次遍历(不使用虚拟头节点)
- 4.2.2.2 一次遍历(使用虚拟头节点)
- 4.2.3 递归法
- 五、链表相交
- 5.1 思路分析
- 5.2 解法探讨
- 5.2.1
- 六、环形链表II
- 6.1 思路分析
- 6.2 解法探讨
- 6.2.1
- 总结
前言
- 接下来,我们一起学习力扣中跟链表有关的算法题。
一、移除链表元素
- 力扣算法题目第 203 题:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
- 示例 1:
输入: head = [1,2,6,3,4,5,6], val = 6
输出: [1,2,3,4,5]
- 示例 2:
输入: head = [ ], val = 1
输出: [ ]
- 前提:
- 列表中的节点数目在范围 [ 0, 1 0 4 10^4 104] 内
- 1 <= Node.val <= 50
- 0 <= val <= 50
1.1 思路分析
- 将要删除节点的前一个节点的next指向删除节点的下一个节点的地址,就可以删除这个节点。
1.2 解法探讨
1.2.1 直接删除
- 我们首先检查头节点head是否是要删除的节点,如果是,则不断前进直到找到不是要删除的节点或者到达链表末尾。
- 如果整个链表都是要删除的节点,则最终head会变成None。
- 接下来进入常规的遍历过程,从head开始遍历,对于每一个节点current,如果它的下一个节点是需要删除的节点,则跳过这个节点,否则就继续前进。
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
# 先定义节点类,定义节点的初始化方法
class ListNode:
def __init__(self, val=0, next=None):
# 定义属性 val 存储节点的值 next用来 指向下一个节点的地址
self.val = val
self.next = next
class Solution:
def removeElements(self, head: Optional[ListNode], val: int):
# 如果第一个节点就是空的,说明链表为空,此时 直接返回空
if head is None:
return None
# 当第一个节点不是空的 并且就是我们要删除的元素
while head is not None and head.val == val:
# 将头节点直接赋予 原链表的第二个元素,这样就删除了第一个节点
head = head.next
# 除了上述特殊情况外,我们需要定义一个指针cur 指向原链表中的第一个节点
cur = head
# 当cur指向的节点不为空,并且他的下一个节点也不为空,我们就进行循环遍历,
# 因为我们判断的就是cur指向的节点的下一个节点的状态 这样能保证我们将cur的next赋值的时候能有值
while cur is not None and cur.next is not None:
# 如果cur指向的节点的下一个节点是我们要删除的
if cur.next.val == val:
# 走到这 说明找到要删除的节点,那就将 cur的next指针 指向 下下个元素
cur.next = cur.next.next
# 走到这说明,cur指向的节点的下一个节点不是我们要删除的节点, 那就让cur指向原链表的下一个节点
else:
cur = cur.next
# 返回 头节点 也就是 链表的头
return head
1.2.2 创建虚拟头节点来删除
- 在涉及到 对链表的增删操作的时候,通常设置虚拟头节点会非常便于理解,同时操作也变得非常方便。
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
# 创建一个虚拟头结点,指向原链表的第一个节点,便于处理头结点可能需要删除的情况
dummy_head = ListNode(0, head)
# 使用 cur 指向虚拟头节点
cur = dummy_head
# 开始遍历链表寻找要删除的节点
# 当cur 指向的节点的下一个元素不为空。也就是不是尾节点
while cur.next is not None:
# 如果 cur 指针指向的 当前节点的下一个节点就是我们要删除的节点
if cur.next.val == val:
# 那么就让当前cur 指向的节点的 next 指向 cur 的下下个节点,
# 这样就跳过了cur的下一个节点,也就是删除了
cur.next = cur.next.next
else:
# 否则,移动到下一个节点
cur = cur.next
# 返回新的头结点
return dummy_head.next
1.2.3 递归版删除
- 因为每个节点要进行的操作都一样,都是检查是否是要删除的节点,如果是,那么就删除,不是的话就判断下一个节点。
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
# 如果链表为空,直接返回None
if head is None:
return None
# 处理当前节点
if head.val == val:
# 如果当前节点需要删除,则递归调用removeElements并跳过当前节点
return self.removeElements(head.next, val)
else:
# 当前节点不需要删除,则处理下一个节点
head.next = self.removeElements(head.next, val)
return head
二、反转列表
- 力扣算法题目第 203 题:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
- 示例 1:
输入: head = [ 1, 2, 3, 4, 5 ]
输出: [ 5, 4, 3, 2, 1 ]
- 示例 2:
输入: head = [ 1, 2 ]
输出: [ 2, 1 ]
- 示例 3:
输入: head = [ ]
输出: [ ]
- 前提:
- 链表中节点的数目范围是 [ 0, 5000 ]
- -5000 <= Node.val <= 5000
2.1 思路分析
- 如果再定义一个新的链表,然后遍历原来的链表,再依次将原来链表的值插进新链表,这会浪费内存空间,不过我们下边也会介绍这种做法,其实只要改变头节点跟next的指针指向即可。
2.2 做法
2.2.1 创建新链表方式
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n)
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]:
# 初始化一个空的新链表
reversed_head = None
# 遍历原链表
current = head
while current:
# 创建一个新节点,值为当前节点的值
new_node = ListNode(current.val)
# 将新节点连接到新链表的头部
new_node.next = reversed_head
reversed_head = new_node
# 移动到原链表的下一个节点
current = current.next
return reversed_head
2.2.2 双指针法
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
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]:
# 设置 双指针 cur指向头节点 pre 先指向 头节点前面 也就是None
cur = head
pre = None
# 从头节点开始遍历链表
while cur:
# 设置tmp存储 原链表的头节点的下一个节点的,因为下边改变指向方向后,会丢失原链表
tmp = cur.next
# 这一步就是改变链表指向方向的
cur.next = pre
# 改变指示方向后要更新 cur跟pre的值
pre = cur
cur = tmp
# 返回的是pre 因为遍历完pre刚好指向原链表的最后一个节点, 也就是新链表的头节点
return pre
2.2.3 递归法
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
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]:
# 调用翻转函数, 传入头结点跟None
return self.reverse(head, None)
# 定义 翻转函数
def reverse(self, cur: ListNode, pre: ListNode) ->ListNode:
# 如果cur空的话,也就是头结点是空的,那么就返回None
if cur == None:
# 递归出口
return pre
# 用 tmp 用来保存 cur 后边的那个节点
tmp = cur.next
# 翻转 链表中的指针方向
cur.next = pre
# 递归调用 reverse 传入改变 后的参数
return self.reverse(tmp, cur)
三、两两交换链表中的节点
- 力扣算法题目第 24 题:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
- 示例 1:
输入: head = [ 1, 2, 3, 4 ]
输出: [ 2, 1, 4, 3 ]
- 示例 2:
输入: head = [ 1 ]
输出: [ 1 ]
- 示例 3:
输入: head = [ ]
输出: [ ]
- 前提:
- 链表中节点的数目在范围 [ 0, 100 ] 内
- 0 <= Node.val <= 100
3.1 思路分析
- 两两交换链表中的节点,我们就需要两个指针指向要交换的两个节点,还需要保存第二个节点的next 防止丢失,循环遍历链表中的节点,依次完成这个动作。
- 也可以使用虚拟头节点,这样更好理解一点
3.2 解法探讨
3.2.1 不使用虚拟头节点(递归法)
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None or head.next is None:
return head
# 先定义指针指向前两个节点
pre = head
cur = head.next
# 重置头节点
head = head.next.next
# 交换两个指针的指向
cur.next = pre
# 因为 swapPairs 返回值就是上一次的头节点的指向
pre.next = self.swapPairs(head)
# 返回cur 因为cur指向的就是交换后的第一个节点
return cur
3.2.2 使用虚拟头节点
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 创建一个哑节点并将其next指向head
dummy = ListNode(0)
dummy.next = head
# 初始化一个当前节点指针
current = dummy
while current.next and current.next.next:
# 初始化A、B两个节点
A = current.next
B = current.next.next
# 交换A和B
current.next = B
A.next = B.next
B.next = A
# 移动current指针到交换后的B节点后面
current = A
# 返回哑节点之后的节点
return dummy.next
四、删除链表的倒数第N个节点
- 力扣算法题目第 19 题:给你一个链表,删除链表的倒数第 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
4.1 思路分析
- 因为我们不知道倒数的第 n 个节点是正着数的第几个节点, 所以我们需要遍历两次,第一次拿到链表长度,第二次根据长度跟倒数的 n 个节点,就能推出来要删除正数第几个节点
- 还可以用双指针,双指针之间有n个节点, 那么当其中一个指针指到末尾的时候,另一个指针就是倒数的第 n 个节点
- 还可以使用递归来做
4.2 解法探讨
4.2.1 两次遍历
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
# 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]:
# 定义一个length 用来存放 链表长度
length = 0
# 定义指针指向头节点
tmp = head
# 循环链表
while tmp:
# 获取链表长度
length += 1
tmp = tmp.next
# 获取到链表长度就要处理 特殊值, 如果倒数第 n 个节点刚好是头节点
if length == n:
# 那就返回头节点的 下一个节点当头节点
return head.next
# 获取到链表长度后 再次进行遍历 这次根据链表长度以及 n
# 我们能推断出要删除的节点是正着数的第几个节点
i = 1
pre = head
# 遍历找到要删除的节点的前一个节点
while i < length - n:
pre = pre.next
i = i +1
# 使用要删除节点的前一个节点 链接到 要删除节点的后一个节点
pre.next = pre.next.next
# 返回 头节点
return head
4.2.2 双指针法
4.2.2.1 一次遍历(不使用虚拟头节点)
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
# 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]:
# 定义 pre 指向 头节点 定义 cur指向 头节点的后边那个节点
pre = head
cur = head.next
# 先让cur走 n - 1 步 这样 当pre跟cur一起移动的时候
# 当 cur走到最后一个节点的时候,这样 pre 刚好就是要删除的节点的前一个节点
i = 1
while i < n:
cur = cur.next
i += 1
# 处理特殊值,不然cur越界 会报错
# 如果 移动了 n 步以后 cur 刚好为None 那么说明链表长度为 n
# 此时需要删除头节点, 头节点的下一个节点充当头节点
if cur is None:
return head.next
# 此时 pre 跟 cur 一起往后移动 当 cur指向最后一个节点的时候停止
while cur.next:
pre = pre.next
cur = cur.next
# 使用 要删除节点的前一个节点 指向 删除节点的后一个节点
pre.next = pre.next.next
# 返回 头结点
return head
4.2.2.2 一次遍历(使用虚拟头节点)
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
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 来指向 头节点
dummy = ListNode(0, head)
# 初始化两个指针 刚开始都指向 虚拟头节点
first = second = dummy
# 拉开距离, 让 first指针移动 n 个位置 这样 first 跟 second 就会相差n个位置
for _ in range(n + 1):
first = first.next
# 同步移动, 当first指针移动到最后的时候,此时 second指向的就是 倒数第 n 个节点
while first:
first = first.next
second = second.next
# 删除节点
second.next = second.next.next
# 返回头节点
return dummy.next
4.2.3 递归法
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
# 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]:
def helper(node):
if not node:
# 如果到达链表末尾,则返回当前节点的位置(距离末尾的距离)
return 0
# 递归调用,返回下一个节点的位置
pos = helper(node.next)
# 如果当前位置正好是要删除的节点的前一个位置
if pos == n :
node.next = node.next.next
return pos + 1
# 创建虚拟头节点
dummy = ListNode(0, head)
# 调用递归函数
helper(dummy)
# 返回新的头节点
return dummy.next
五、链表相交
5.1 思路分析
5.2 解法探讨
5.2.1
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
六、环形链表II
6.1 思路分析
6.2 解法探讨
6.2.1
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
总结
- 以上就是力扣中有关链表的题目的解题思路跟代码,我只是列举出来 我能想到的几种办法,如有其他解法,可以后台私信我。