小G的算法刷题日记 - 双指针法(一)

目前整理几道可以使用双指针法解决的链表题目:19. 删除N,21. 合并2,23. 合并K,86. 分隔链表,141. 环形链表,142. 环形链表II,(160. 相交链表,876. 链表的中间结点暂时未整理),具体题目如下:

19. 删除链表的倒数第N个节点

题目重点:给定一个单链表和整数n,删除倒数第n个结点,返回新的链表头。

思路:想要去删除倒数第n个结点,我们需要获得它的前一个即倒数第n+1个的引用,此时使用双指针-快慢指针法可以完美解决,步骤如下:

  1. 创建一个虚拟头节点 dummy,它的 .next 指向原链表头,防止删除的是头结点不好处理;

  2. fast 先向前走 n 步;

  3. 然后 fastslow 一起走,直到 fast 到达链表尾部;

  4. 此时 slow 的位置就是待删除节点的前一个;

  5. 删除:slow.next = slow.next.next。

核心代码:

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(0, head)
        fast = slow = dummy

        # Step 1: fast 先走 n 步
        for _ in range(n):
            fast = fast.next

        # Step 2: fast 和 slow 同时移动
        while fast.next:
            fast = fast.next
            slow = slow.next

        # Step 3: 删除 slow 后面的节点
        slow.next = slow.next.next

        return dummy.next

21. 合并两个有序链表

题目重点:给定两个升序链表:list1,list2,需要将这两个链表拼接为一个新的升序链表并返回。

思路:两个指针分别遍历两个链表,比较大小,归并它们,具体步骤如下:

  1. 创建一个虚拟头节点 dummy,用一个指针 cur 追踪结果链表尾部;

  2. 比较 list1 list2 当前节点值,小的那个挂到 cur.next;

  3. 哪个链表节点用了,就把那个链表向前移动一步;

  4. 当某个链表为空了,直接把另一个链表剩余部分接上。

核心代码:

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(-1)
        cur = dummy

        while list1 and list2:
            if list1.val <= list2.val:
                cur.next = list1
                list1 = list1.next
            else:
                cur.next = list2
                list2 = list2.next
            cur = cur.next
        
        # 处理剩余链表(最多只剩一个非空)
        cur.next = list1 if list1 else list2
        return dummy.next

23. 合并K个有序链表

题目重点:输入:一个包含 k 个链表头节点的列表 lists(已按升序排列),目标:将这个 lists 合并成一个新的升序链表,并返回头节点。

思路:使用最小堆(优先队列)优化,即堆中每个元素是当前各链表的头节点,每次弹出堆顶的最小值,加入结果链表,并把该节点的 next 放入堆中。

核心代码:

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        # 初始化一个最小堆,可以使用它快速找到当前所有链表头中值最小的那个节点
        min_heap = []
        
        for i, node in enumerate(lists):
            if node:
                # 把元素放入堆中
                heappush(min_heap, (node.val, i, node))
 
        dummy = ListNode(-1)
        cur = dummy
        
        # 只要堆不空,就循环处理,说明还有节点没处理完。
        while min_heap:
            # 弹出堆中当前值最小的节点
            val, i, node = heappop(min_heap)
            cur.next = node
            cur = cur.next
            if node.next:
                heappush(min_heap, (node.next.val, i, node.next))
        return dummy.next

86. 分隔链表

题目重点:给定一个链表head和一个int值x,将 < x 的节点放到前面,>= x 的节点放到后面,需要保持每个节点的初始相对位置。

思路:上面的21题需要你将两个链表合二为一,这道题是将两个链表先一分为二,其中一个链表中的元素大小都小于 x, 另一个链表中的元素大小都大于等于 x,最后将这两条链表接到一起。注意最后拼接的时候需要将后半段链表(after链)尾节点的next断开!!!

核心代码:

class Solution:
    def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        before_dummy = ListNode(-1)
        after_dummy = ListNode(-1)

        before = before_dummy  # 尾指针
        after = after_dummy    # 尾指针

        while head:
            if head.val < x:
                before.next = head
                before = before.next
            else:
                after.next = head
                after = after.next
            head = head.next

        # 结束后拼接两个链表
        after.next = None           # 把后半段(after链)尾节点的 next 断开,防止出现链表“环”或者“多余尾巴”的问题。
        before.next = after_dummy.next
        return before_dummy.next

141. 环形链表

题目重点:判断链表中是否存在环,即某个节点再次被访问。

思路:经典双指针-快慢指针法,使用两个指针 slow 和 fast,slow 每次走一步,fast每次走两步。
如果链表中存在环,两个指针一定会在某个时刻相遇;
如果无环,fast 会先到达链表尾部,退出循环。

核心代码: 

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 slow == fast:
                return True

        return False

142. 环形链表II

题目重点:不仅要判断有无环,还要找出入环的第一个节点,

思路:第一步:判断链表是否有环,和上道题思路一致,使用快慢指针去判断;第二步(核心):找到环的起始节点,让一个指针从头开始走,一个从相遇点开始走,再次相遇点即环起点。

为什么从相遇点和头节点同时出发会在环入口相遇?
假设:

a:从头节点到环的入口节点的距离(非环部分的长度)
b:从环的入口节点到第一次相遇点的距离
r:整个环的长度
那么:第一次相遇时慢指针走了 a + b,快节点走了 2(a + b),二者的差 a + b 则是环长 r 的整数倍,即 a + b = n x r。此时让一个指针 ptr 从头开始走,一个指针 slow 从相遇点开始走,则会在入环点相遇,此时返回 ptr。

核心代码:

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = head
        fast = head

        # Step 1: 判断是否有环
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            # 如果找到了相遇点(slow == fast),说明有环 → 用 break 跳出 → 不执行 else
            if slow == fast:
                break
        # 只有在 while 循环“正常结束”时(即未被 break 打断)才执行 else 中的语句。
        else:
            return None  # 无环

        
        # Step 2: 找入环点
        ptr = head
        while ptr != slow:
            ptr = ptr.next
            slow = slow.next

        return ptr

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值