【灵神高频面试题合集06-08】反转链表、快慢指针(环形链表/重排链表)、前后指针(删除链表/链表去重)

基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- 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

课后作业

24. 两两交换链表中的节点

445. 两数相加 II

2816. 翻倍以链表形式表示的数字


快慢指针

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)

课后作业

234. 回文链表

2130. 链表最大孪生和


前后指针

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)

课后作业

203. 移除链表元素

3217. 从链表中移除在数组中存在的节点

2487. 从链表中移除节点

1669. 合并两个链表

相关推荐
平行侠1 小时前
037插入排序 - 整理扑克牌的算法
数据结构·算法
deephub1 小时前
2026 RAG 选型指南:Vector、Graph、Vectorless 该怎么挑
人工智能·python·大语言模型·rag
ECT-OS-JiuHuaShan1 小时前
彻底定理化:从量子纠缠到量子代谢
数据库·人工智能·学习·算法·生活·量子计算
爱喝雪碧的可乐2 小时前
2026 腾讯广告算法大赛优秀方案启示:行为条件化多模态自回归生成推荐摘要
算法·数据挖掘·回归·推荐系统·推荐算法
碧海银沙音频科技研究院2 小时前
音箱在加入 NN AEC(神经网络声学回声消除) 后出现反复重启问题解决
人工智能·深度学习·算法
狐狐生风3 小时前
使用 UV 创建并运行 Python 项目(完整步骤)
python·uv
叼烟扛炮3 小时前
C++ 知识点18 内部类
开发语言·c++·算法·内部类
YOGOD有神3 小时前
用AI自动从谷歌地图抓取海外客户,我跑了一次7小时的任务,结果出乎意料
算法
噜噜噜阿鲁~3 小时前
python学习笔记 | 9.2、模块-安装第三方模块
笔记·python·学习