力扣hot100 | 链表 | 160. 相交链表、206. 反转链表、234. 回文链表、141. 环形链表、142. 环形链表 II、21. 合并两个有序链表、2. 两数相加

160. 相交链表

力扣题目链接
给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交:
在这里插入图片描述
题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构。

示例 1:
在这里插入图片描述
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。

示例 2:
在这里插入图片描述
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at ‘2’
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:
在这里插入图片描述
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:No intersection
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

一、双指针法【推荐】

此解法参考:灵茶山艾府

  • 【思路】让两个指针分别遍历两个链表,当一个指针到达链表末尾时,让它从另一个链表的头部开始继续遍历。这样两个指针最终会在相交点相遇,或者同时到达末尾(都为None)。
  • 【步骤】
    1. 初始化两个指针 p=headA, q=headB
    2. 不断循环,直到 p=q
    3. 每次循环,pq 各向后走一步。具体来说,如果 p 不是空节点,那么更新 pp.next,否则更新 pheadB;如果 q 不是空节点,那么更新 qq.next,否则更新 qheadA
    4. 循环结束时,如果两条链表相交,那么此时 pq 都在相交的起始节点处,返回 p;如果两条链表不相交,那么 pq 都走到空节点,所以也可以返回 p,即空节点。
  • 【为什么相交的话pq一定相遇?】
    在这里插入图片描述
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
		p, q = headA, headB
        
        while p != q:
			p = p.next if p else headB
            q = q.next if q else headA
 
        return p  # 返回相交节点(如果不相交,两个指针都会是None)
  • 时间复杂度 O(m + n):其中 m 是第一条链表的长度,n 是第二条链表的长度。除了交点,每个节点会被指针 p 访问至多一次,每个节点会被指针 q 访问至多一次。
  • 空间复杂度 O(1)

二、哈希法

  • 【思路】先遍历一个链表,将所有节点存入哈希表,然后遍历另一个链表,检查每个节点是否在哈希表中。(用额外空间换取直观的解题思路)
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        # 将链表A的所有节点存入集合
        visited = set()
        cur = headA
        while cur:
            visited.add(cur)
            cur = cur.next
        
        # 遍历链表B,查找第一个在集合中的节点
        cur = headB
        while cur:
            if cur in visited:
                return cur
            cur = cur.next
        
        return None       
  • 时间复杂度 O(m + n)
  • 空间复杂度 O(m)O(n)

206. 反转链表

力扣题目链接
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

“鱼咬尾”(三指针迭代法)【推荐】

  • 【思路】使用三个指针 prevcurnext 来逐个反转链表中每个节点的指向。
  • 代码如下,可以发现整个迭代的循环体(while)中是一段漂亮的“鱼咬尾”,非常好记!
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        cur = head
        prev = None
        while cur:
            nxt = cur.next
            cur.next = prev  # 把next指回上一个节点
            prev = cur  # 再把prev向前移动一位
            cur = nxt   # 再把cur向前移动一位
        return prev  # 注意最后一轮过后cur = None, 所以新的头节点是prev!!
  • 时间复杂度 O(n)
  • 空间复杂度 O(1)
  • 【记住一个性质(反复用于下一题和25题)】反转结束后,从原来的链表上看
    • pre指向反转这一段的末尾
    • cur指向反转这一段后面的下一个节点(对本题来说是None)

此性质参考自:灵茶山艾府

*92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点 (1-based),返回 反转后的链表

示例 1:
在这里插入图片描述
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

  • 【思路】同上题,使用三个指针 prevcurnxt 来逐个反转链表中每个节点的指向。
  • 【注意】与上题不同的是:
    1. 要找到p0,也就是left位置(需要反转的开头位置)的上一位。——从dummyleft - 1步。

    2. 注意pre初始值与上一题一样也是None而不是p0

    3. curp0.next开始迭代(向前走),但结束条件不是走到结尾None(while),而是走一定的步数(right-left+1),不过还是满足上题说到的性质 【一段“鱼咬尾”迭代后】

      • pre指向反转这一段的末尾
      • cur指向反转这一段后面的下一个节点
        在这里插入图片描述
    4. 最后还需要:
      ① 把p0.next.next(反转这一段的开头)连到cur(反转这一段后面的下一个节点)上;
      ② 把p0.next连到pre(反转这一段的末尾)上。

【注意】这两步的顺序一定不能换!! 不然第二步就不能通过p0.next来访问 反转这一段的开头 的节点了
在这里插入图片描述
图与此性质源自:灵茶山艾府

class Solution:
    def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
        p0 = dummy = ListNode(next = head)
        for _ in range(left - 1):  # 先移到ROI的前一位
            p0 = p0.next
        
        cur = p0.next
        pre = None   # 别混淆pre和p0,pre的初始值始终为none
        for _ in range(right - left + 1):  # 鱼咬尾循环,指定步数
            nxt = cur.next
            cur.next = pre
            pre = cur
            cur = nxt

        p0.next.next = cur  # 【注意】这两行顺序一定一定不能换!!
        p0.next = pre
        return dummy.next
  • 时间复杂度 O(right)
  • 空间复杂度 O(1)

234. 回文链表

力扣题目链接
给你一个单链表的头节点head ,请你判断该链表是否为回文链表 (向前和向后读都相同的序列)。如果是,返回true;否则,返回 false

示例 1:
在这里插入图片描述
输入:head = [1,2,2,1]
输出:true

示例 2:
在这里插入图片描述
输入:head = [1,2]
输出:false

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

一、转换为数组

  • 【思路】将链表转换为数组,然后使用双指针判断是否回文。
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
    # 将链表转换为数组
    values = []
    current = head
    while current:
        values.append(current.val)
        current = current.next
    
    # 使用双指针判断回文
    left, right = 0, len(values) - 1
    while left < right:
        if values[left] != values[right]:
            return False
        left += 1
        right -= 1
    
    return True
  • 时间复杂度 O(n)
  • 空间复杂度 O(n)

二、双指针+反转链表【推荐】

此方法参考:灵茶山艾府

  • 【步骤】
    1. 用快慢指针找到链表中点 (876. 链表的中间结点)
    2. 反转后半部分链表
    3. 比较前半部分反转后的后半部分
      在这里插入图片描述
      在这里插入图片描述
  • 【注意】循环条件要判断 head2 是否为空而不是 head 是否为空。若判断 head 是否为空,会错误地多循环一次(下图中2—>3并没有断开,继续2—>3—>None),导致访问 head2.val 出现空指针异常。
    在这里插入图片描述
class Solution:
    # 206. 反转链表
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        curr = head
        prev = None
        while curr:
            tmp = curr.next
            curr.next = prev  
            prev = curr  
            curr = tmp   
        return prev  

    # 876. 链表的中间结点
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head
        while fast and fast.next:  # 奇数长度,fast停在最后一个节点,slow停在正中心
            slow = slow.next       # 偶数长度,fast停在None节点,slow停在正中心(偏右)
            fast = fast.next.next
        return slow

    # 234.本题
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        mid = self.middleNode(head)
        head2 = self.reverseList(mid)
        while head2:
            if head.val != head2.val:
                return False
            head = head.next
            head2 = head2.next
        return True
  • 时间复杂度 O(n)
  • 空间复杂度 O(1)

141. 环形链表

力扣题目链接
给你一个链表的头节点 head,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:
在这里插入图片描述
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
在这里插入图片描述
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
在这里插入图片描述
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

快慢指针

此方法参考:灵茶山艾府

  • 【思路】想象兔子和乌龟在同一环形跑道上,一个速度快、另一个速度慢。那么兔子必然在一段时间后追上乌龟(套圈)。对于链表来说,如果在链表中引入两个以不同速度(一个比另一个快一倍)前进的指针,在链表存在环的情况下,这两个指针必定会相遇。
  • 【问:兔子会不会「跳过」乌龟,不和乌龟相遇?】
    答:这是不可能的。如果有环的话,那么兔子和乌龟都会进入环中。进入环后用「相对速度」思考,乌龟不动,兔子相对乌龟每次只走一步,这样就可以看出兔子一定会和乌龟相遇了。
  • 【问:为什么代码的 while 循环没有判断 slow 是否为空?】
    答:slow 在 fast 后面,如果 fast 不是空,那么 slow 也肯定不是空。好比快人先去探路,慢人走的都是快人走过的路。
class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        slow = fast = head    # 同时从起点出发
        while fast and fast.next:
            slow = slow.next  # 乌龟走一步
            fast = fast.next.next # 兔子走两步
            if fast == slow:  # 若相遇(套圈)
                return True
        return False
  • 时间复杂度 O(n),其中 n 为链表的长度。
  • 空间复杂度 O(1),仅用到若干额外变量。

142. 环形链表 II

力扣题目链接
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

不允许修改 链表。

示例1:
在这里插入图片描述
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

Floyd 判圈算法(快慢指针)

此题解参考:灵茶山艾府

  • 【思路】与上一题一样,也是快慢指针(两倍速):
  • 【关键结论】慢指针从相遇点开始,移动 a 步后恰好走到入环口(虽然可能会多次经过入环口)
  • ==>于是此时,同速度移动head(或另一个从head出发的指针)和慢指针,它们相遇时就是入环口!
  • 【推导如下】
    在这里插入图片描述
class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        fast = slow = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if fast == slow:  # 快慢指针相遇点
                while slow != head:   # 直到head和slow相遇(a步)
                    slow = slow.next
                    head = head.next
                return slow
        return None
  • 时间复杂度 O(n)
  • 空间复杂度 O(1)

21. 合并两个有序链表

力扣题目链接
将两个升序链表合并为一个新的 升序(非递减顺序排列) 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:
在这里插入图片描述
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

一、迭代法【推荐】

此题解搬运自:灵茶山艾府

  • 【前置知识】
  • 【思路】
  • 【步骤】
    1. 创建一个哨兵节点(dummy head),作为合并后的新链表头节点的前一个节点。(这样可以避免单独处理头节点,也无需特判链表为空的情况,从而简化代码。)
    2. 比较 list1list2的节点值,如果 list1的节点值小,则把 list1加到新链表的末尾,然后把 list1前移。如果 list2 的节点值小则同理 【哪个小放/移哪个】;如果两个节点值一样,那么把谁加到新链表的末尾都是一样的,不妨规定把 list2 加到新链表末尾。
    3. 重复上述过程,直到其中一个链表为空。
    4. 循环结束后,其中一个链表可能还有剩余的节点,将剩余部分直接全部加到新链表的末尾。
    5. 返回新头节点。
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        cur = dummy = ListNode()      # 用dummy head 简化逻辑
        while list1 and list2:
            if list1.val < list2.val: # 哪个小加哪个
                cur.next = list1      # 加到新链表中
                list1 = list1.next    # 前移1
            else:   # 注:相等的情况加哪个节点都是可以的
                cur.next = list2
                list2 = list2.next
            cur = cur.next            # 前移,指到本轮新加的节点上
        cur.next = list1 or list2  # 或者cur.next = list1 if list1 else list2
        return dummy.next             # 返回新节点的头
  • 时间复杂度 O(m + n)
  • 空间复杂度 O(1)
  • 【补充res = list1 or list2的用法】等价于res = list1 if list1 else list2
    • or操作符:
    • 如果第一个操作数为"真值",直接返回第一个操作数
    • 如果第一个操作数为"假值",返回第二个操作数

二、递归法

此题解搬运自:灵茶山艾府

  • 【前置知识】如何理解递归?计算机是怎么执行递归的?【基础算法精讲 09】
  • 【思路】直接用 mergeTwoLists 当作递归函数:
    • 递归边界:如果其中一个链表为空,直接返回另一个链表作为合并后的结果。
    • 如果两个链表都不为空,则比较两个链表当前节点的值,并选择较小的节点作为新链表的当前节点。例如list1的节点值更小,那么递归调用 mergeTwoLists(list1.next, list2),将递归返回的链表接在 list1的末尾。
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        if list1 is None: return list2  # 注:如果都为空则返回空
        if list2 is None: return list1
        if list1.val < list2.val:
            list1.next = self.mergeTwoLists(list1.next, list2)
            return list1
        list2.next = self.mergeTwoLists(list1, list2.next)
        return list2
  • 时间复杂度 O(m + n),其中 n 为 list1的长度,m 为 list2 的长度。
  • 空间复杂度 O(m + n),递归栈空间

2. 两数相加

力扣题目链接
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:
在这里插入图片描述
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]

一、迭代法

  • 【思路】模拟加法运算。由于链表是逆序存储的(低位在前,高位在后),这正好符合我们从个位开始相加的习惯(从头节点开始加相当于从个位开始加):
    • 逐位相加:从两个链表的头部开始,逐个节点相加
    • 处理进位:当相加结果≥10时,需要向下一位进位
    • 处理长度不一致:两个链表可能长度不同,短的链表结束后按0处理
    • 处理最后的进位:所有位都处理完后,如果还有进位,需要新增一个节点
  • 【步骤】
    1. 初始化:创建虚拟头节点和当前指针,设置进位标志为0
    2. 主循环:当任一链表未结束存在进位时继续循环
      • 获取当前位的值(链表为空则不用加(取为0))
      • 计算当前位相加结果(包括进位)
      • 创建新节点存储结果的该位数(对10的余数
      • 更新进位值(对10的整除数
      • 移动指针
    3. 返回结果:返回虚拟头节点的下一个节点(头节点)
class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        cur = dummy = ListNode() 
        carry = 0  # 进位
        while l1 or l2 or carry:  # 有一个不是空节点,或者还有进位,就继续
            if l1:
                carry += l1.val  # 加入节点值(这里直接用carry来承载本轮总数)
                l1 = l1.next  # 前移1
            if l2:
                carry += l2.val  
                l2 = l2.next  
            cur.next = ListNode(carry % 10)  # 要先添加节点
            cur = cur.next  				 # 再前移cur(不然前面没东西啊)
            carry //= 10  # 新的进位
        return dummy.next  # 返回头节点
  • 时间复杂度 O(n),其中 n 为l1长度和l2长度的最大值。
  • 空间复杂度 O(1),返回值不算的话。

二、递归法

写法1. 创建新节点

class Solution:
    # l1 和 l2 为当前遍历的节点,carry 为进位
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode], carry=0) -> Optional[ListNode]:
        if l1 is None and l2 is None and carry == 0:  # 递归边界
            return None

        s = carry
        if l1:
            s += l1.val  # 累加进位与节点值
            l1 = l1.next
        if l2:
            s += l2.val
            l2 = l2.next

        # s 除以 10 的余数为当前节点值,商为进位
        return ListNode(s % 10, self.addTwoNumbers(l1, l2, s // 10))

写法2. 原地修改

class Solution:
    # l1 和 l2 为当前遍历的节点,carry 为进位
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode], carry=0) -> Optional[ListNode]:
        if l1 is None and l2 is None:  # 递归边界
            return ListNode(carry) if carry else None  # 如果进位了,就额外创建一个节点
        if l1 is None:  # 如果 l1 是空的,那么此时 l2 一定不是空节点
            l1, l2 = l2, l1  # 交换 l1 与 l2,保证 l1 非空,从而简化代码
        s = carry + l1.val + (l2.val if l2 else 0)  # 节点值和进位加在一起
        l1.val = s % 10  # 每个节点保存一个数位(直接修改原链表)
        l1.next = self.addTwoNumbers(l1.next, l2.next if l2 else None, s // 10)  # 进位
        return l1
  • 时间复杂度 O(n)
  • 空间复杂度 O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值