双指针技巧秒杀七道链表题目(完)

双指针技巧秒杀七道链表题目(2)

单链表的倒数第 k 个节点

从前往后寻找单链表的第 k 个节点很简单,一个 for 循环遍历过去就找到了,但是如何寻找从后往前数的第 k 个节点呢?

这个解法就比较巧妙了,假设 k = 2,思路如下:

首先,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步:

img

现在的 p1,只要再走 n - k 步,就能走到链表末尾的空指针了对吧?

趁这个时候,再用一个指针 p2 指向链表头节点 head

img

接下来就很显然了,让 p1p2 同时向前走,p1 走到链表末尾的空指针时前进了 n - k 步,p2 也从 head 开始前进了 n - k 步,停留在第 n - k + 1 个节点上,即恰好停链表的倒数第 k 个节点上:

img

这样,只遍历了一次链表,就获得了倒数第 k 个节点 p2

例如:

力扣第 19 题「 删除链表的倒数第 N 个结点

img

我的代码:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        //虚拟头节点
        ListNode* dummy=new ListNode(-1);
        dummy->next=head;

        ListNode* fast=dummy;
        ListNode* slow=dummy;
        //要删除第n个,就要先找到第n+1个节点,所以fast先走n+1步
        for(int i=0;i<=n;i++){
            fast=fast->next;
        }
        while(fast!=nullptr){
            fast=fast->next;
            slow=slow->next;
        }
        slow->next=slow->next->next;
        return dummy->next;
    }
};

注意:

​ 要使用虚拟头节点,这样可以省去讨论许多特殊情况,返回dummy->next,head可以为空。

单链表的中点

力扣第 876 题「 链表的中间结点」就是这个题目,问题的关键也在于我们无法直接得到单链表的长度 n,常规方法也是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。

我们让两个指针 slowfast 分别指向链表头结点 head

每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点

我的代码:

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        //返回中间节点,利用快慢指针即可,快的指针每次走2步,慢的每次走1步
        ListNode* fast=head;
        ListNode* slow=head;
        while(fast->next!=nullptr){
            fast=fast->next->next;
            slow=slow->next;
            //此时有两个中间节点,slow刚好指向第二个
            if(fast==nullptr){
                return slow;
            }
        }
        //只有一个中间节点
        return slow;
    }
};

判断链表是否包含环

判断链表是否包含环属于经典问题了,解决方案也是用快慢指针:

每当慢指针 slow 前进一步,快指针 fast 就前进两步。

如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

当然,这个问题还有进阶版:如果链表中含有环,如何计算这个环的起点?

可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。

为什么要这样呢?这里简单说一下其中的原理。

我们假设快慢指针相遇时,慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步:

img

fast 一定比 slow 多走了 k 步,这多走的 k 步其实就是 fast 指针在环里转圈圈,所以 k 的值就是环长度的「整数倍」。

假设相遇点距环的起点的距离为 m,那么结合上图的 slow 指针,环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始走k步可以转回到相遇点,那走 k - m 步肯定就走到环起点了:

img

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后一定会相遇,相遇之处就是环的起点了。

两个链表是否相交

力扣第 160 题「 相交链表

我的代码:

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* a=headA;
        ListNode* b=headB;
        while(a!=b){
            a==nullptr?(a=headB):(a=a->next);
            b==nullptr?(b=headA):(b=b->next);
        }
        return a;
    }
};

思路:

​ 本题书中思路比较清晰,无需总结

给你输入两个链表的头结点 headAheadB,这两个链表可能存在相交。

如果相交,你的算法应该返回相交的那个节点;如果没相交,则返回 null。

比如题目给我们举的例子,如果输入的两个链表如下图:

img

那么我们的算法应该返回 c1 这个节点。

这个题直接的想法可能是用 HashSet 记录一个链表的所有节点,然后和另一条链表对比,但这就需要额外的空间。

如果不用额外的空间,只使用两个指针,你如何做呢?

难点在于,由于两条链表的长度可能不同,两条链表之间的节点无法对应:

img

如果用两个指针 p1p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1

解决这个问题的关键是,通过某些方式,让 p1p2 能够同时到达相交节点 c1

所以,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。

如果这样进行拼接,就可以让 p1p2 同时进入公共部分,也就是同时到达相交节点 c1

img

那你可能会问,如果说两个链表没有相交点,是否能够正确的返回 null 呢?

这个逻辑可以覆盖这种情况的,相当于 c1 节点是 null 空指针嘛,可以正确返回 null。

按照这个思路,可以写出如下代码:

ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    // p1 指向 A 链表头结点,p2 指向 B 链表头结点
    ListNode p1 = headA, p2 = headB;
    while (p1 != p2) {
        // p1 走一步,如果走到 A 链表末尾,转到 B 链表
        if (p1 == null) p1 = headB;
        else            p1 = p1.next;
        // p2 走一步,如果走到 B 链表末尾,转到 A 链表
        if (p2 == null) p2 = headA;
        else            p2 = p2.next;
    }
    return p1;
}

这样,这道题就解决了,空间复杂度为 O(1),时间复杂度为 O(N)

posted @ 2022-04-04 11:15  BailanZ  阅读(49)  评论(0编辑  收藏  举报