快慢指针 C++| leetcode #19, #876, #141, #142, #1721

快慢指针的思想

快慢指针的思想并不难理解。正如它的名字一样,快慢指针就是指两个指针,一个跑得快,一个跑得慢。这里所谓的快和慢指的是移动的步长,比如每次让一个指针向前走两步,而让另一个指针向前走一步,那么前面那个指针就是快指针,后面的就是慢指针。

P.S. 在这篇blog中,快慢指针也可广义地指一个在前、一个在后的指针,下面将要讲到的leetcode #19就是使用了这种方法。

Leetcode #19

题目描述

在这里插入图片描述

思路1:哈希

一种比较简单的想法是建立一个指针数组,使得每个链表结点的序号与结点本身有一个映射关系。因为在该题中有可能删除头结点(如示例2),所以我们可以用一个指向head的prehead结点作为指针数组的第0项。随后,我们可以根据数学关系取倒数第n项。然后使前一个结点指向后一个结点即可。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* mp[32];   //题目最大size是30,多一个头结点,多一个nullptr
        int i=0;
        ListNode* curr = new ListNode(-1, head);
        while(curr){
            mp[i] = curr;
            i++;
            curr = curr->next;
        }
        mp[i] = nullptr;
        mp[i-n-1]->next = mp[i-n+1];
        return mp[0]->next;
    }
};

思路2:快慢指针

另外一种方法呢,就是本文介绍的快慢指针了。我们让快指针先起跑,我们说的倒数第n个,其实也就是和最后一个结点相差n-1个的结点。那么,一开始我们让fast和slow指针都指向头结点,然后让fast指针跑n-1步,此时fast指针和slow指针就相差n-1个结点了。接下来每次让fast和slow指针都前进一步,当fast到达最后一个结点时(即 fast->next 为nullptr时),slow 指针即为倒数第n个结点。因为我们要对slow进行删除操作,所以在每一次循环中,我们都要把slow前一个结点(即prev)记录下来,最后直接让 prev->next 指向 slow->next 即可。

P.S. 最后要注意删除头结点的特殊情况。当然,可以通过设置一个新的头结点指向head来解决这个问题,但是由于笔者较懒,就不改进这个代码了2333。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *fast, *slow;
        ListNode* ans = head;
        fast = slow = head;
        ListNode* prev = slow;
        for(int i=1; i<n; i++){
            fast = fast->next;
        }
        while(fast->next){
            fast = fast->next;
            prev = slow;
            slow = slow->next;
        }
        if(prev != slow)    prev->next = slow->next;
        else    ans = slow->next;  //特殊情况,删除头结点
        return ans;
    }
};

事实上,这种做法并没有在时间复杂度上比第一种做法好多少,不过是一个很好的理解快慢指针的例子。

Leetcode #876

题目描述

在这里插入图片描述

思路与代码

与#19非常相似,但有些许不同。这一次我们用的快慢指针是真正意义上的“快”和“慢”了,快指针一次走两步,而慢指针一次走一步。这种找中间元素的情况,经常需要分奇偶讨论。据题意可得,在偶数结点的情况下,我们会返回中间两个靠后的哪一个结点。那么我们来分析一下,对于奇数结点的情况,结点移动的过程如下图所示:
在这里插入图片描述
可见,总结点数为奇数时,当fast指向最后一个结点时(即 fast->next == nullptr),slow正好指向中间的结点。接下来我们看一下结点总数为偶数的情况:
在这里插入图片描述
可见,当总结点数为偶数时,fast指向nullptr时,slow正好指向题目要求的“中间结点”。根据这种思路,我们可以编写出如下代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        if(!head || !head->next)    return head;
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

Leetcode #141

题目描述

在这里插入图片描述

思路1:哈希

很容易想到,我们可以建立一个链表结点到bool型或int型的map,随后遍历整个链表,如果在遍历过程中,某一元素已经被访问过,那么这个链表必定是有环的。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(head == NULL)    return false;
        unordered_map<ListNode*, bool> mp;
        while(head != NULL){
            if(mp[head]) return true;
            mp[head] = true;
            head = head->next;
        }
        return false;
    }
};

思路二:快慢指针

这个方法非常有趣。先给出结论:对于存在环的链表,快慢指针一定会在某一时刻相遇
在这里插入图片描述
先给出比较通俗的理解:我们知道,快指针每一次会比慢指针多走一步,那么如果在一个环里,快指针一定可以追上慢指针(因为在一个环中,快指针与慢指针之间差的步数是有限的)。

笔者在这里尝试给出较为数学一点的证明。

要证明快慢指针会相遇,即证明: ∃ s ∈ Z \exist s\in Z sZ,使得slow指针走到第 s − 1 s-1 s1步时,与fast会相遇。

假设整个链表的结点数为 n n n,环中包含的结点数为 k k k s s s为slow结点的序号(绝对序号,即总共走了 s − 1 s-1 s1步), f f f为fast结点的绝对序号。那么,根据快指针每一次比慢指针多走一步,慢指针走了 s − 1 s-1 s1步后,快指针比慢指针多走了 s − 1 s-1 s1步,我们有:
f − s = s − 1 f-s = s-1 fs=s1
以环的入口为第一个结点,假设slow与fast在环的第 m m m个结点相遇。有以下关系:
[ f − ( n − k ) ] % k = m [ s − ( n − k ) ] % k = m [f-(n-k)] \%k = m\\ [s-(n-k)]\%k = m [f(nk)]%k=m[s(nk)]%k=m
假设fast走了 a a a圈,slow走了 b b b圈,那么:
f − ( n − k ) = a k + m s − ( n − k ) = b k + m f-(n-k) = ak+m\\ s-(n-k)=bk+m f(nk)=ak+ms(nk)=bk+m
联立以上式子,可得:
s = ( a − b ) k + 1 s=(a-b)k+1 s=(ab)k+1
显然,fast一定比slow先跑完一整圈,所以 a > b a>b a>b,则当slow走了 ( a − b ) k (a-b)k (ab)k步之后,fast和slow会相遇。

总而言之,fast会追上slow。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(head == NULL || head->next == NULL) return false;
        ListNode* fast = head->next;
        ListNode* slow = head;
        while(fast != slow){
            if(fast == NULL || fast->next == NULL)  return false;
            fast = fast->next->next;
            slow = slow->next;
        }
        return true;
    }
};

Leetcode #142

题目描述

在这里插入图片描述

思路1:哈希

建立链表结点到bool型变量的map,标记某结点是否已经访问过,出现过的第一个已访问过的结点即为所求。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_map<ListNode*, bool> mp;
        if(head == NULL || head->next == NULL)  return NULL;
        while(head != NULL){
            if(mp[head]) return head;
            mp[head] = true;
            head = head->next;
        }
        return NULL;
    }
};

思路2:快慢指针+双指针

这道题的思路基本都是从数学推导上得来的,这里leetcode官网给出的解答就挺不错的。示意图如下:
在这里插入图片描述
这里的 a , b , c a, b, c a,b,c指的是对应段的长度,即结点数。因为我们知道,对于总路程来讲,fast指针走过的路程是slow指针的两倍,那么有如下关系:
a + n ( b + c ) + b = 2 ( a + b ) a+n(b+c)+b = 2(a+b) a+n(b+c)+b=2(a+b)
化简可得: a = c + ( n − 1 ) ( b + c ) . a = c+(n-1)(b+c). a=c+(n1)(b+c).也就是说,如果让一个新结点从头结点出发,slow从相遇点出发,那么它们一定会在入环点相遇。(因为 c c c为从相遇点到入环点的距离,而 ( b + c ) (b+c) (b+c)就是整个环的长度,不管加多少个,并不会改变在环内的位置)

思路如上,代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head == NULL)    return NULL;
        ListNode *fast, *slow;
        fast = slow = head;
        while(fast != NULL){
            slow = slow->next;
            if(fast->next == NULL)  return NULL;
            fast = fast->next->next;
            if(fast == slow){
                ListNode* tmp = head;
                while(tmp != slow){
                    tmp = tmp->next;
                    slow = slow->next;
                }
                return slow;
            }
        }
        return NULL;
    }
};

Leetcode #1721

题目描述

在这里插入图片描述

思路与代码

找到正数第k个结点并不难,一路往下循环即可,找倒数第k个结点的过程依旧使用之前提到的快慢指针的方法。最后的交换过程需要注意,相邻两个结点的交换和不相邻结点的交换略有不同,此外,如果交换的结点是head结点,那么我们不能返回head结点。

代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapNodes(ListNode* head, int k) {
        ListNode *fast, *slow, *prev1, *prev2, *tmp1, *tmp2, *curr;
        fast = slow = tmp1 = tmp2 = curr = head;
        ListNode *tmps1, *tmps2;
        prev1 = new ListNode(-1, head);
        prev2 = new ListNode(-1, head);
        int cnt = 0, cnt2 = 0;
        //找到正数第k个结点
        while(curr != nullptr){
            cnt++;
            if(cnt == k){
                tmp1 = curr;
                break;
            }
            prev1 = prev1->next;
            curr = curr->next;
        }
        //快慢指针找倒数第k个结点
        for(int i=0; i<k; i++){
            fast = fast->next;
        }
        cnt2 = 1;
        while(fast){
            prev2 = slow;
            slow = slow->next;
            fast = fast->next;
            cnt2++;
        }
        tmp2 = slow;
        if(cnt2 < k){   //如果倒数第k个结点在整数第k个结点的前面,交换tmp1, tmp2 和 prev1, prev2
            tmp2 = tmp1;
            tmp1 = slow;
            curr = prev1;
            prev1 = prev2;
            prev2 = curr;
        }
        bool flag = false;  //标记发生改变的是否为head结点
        if(tmp1 == tmp2)    return head;
        else{
            if(prev1->next == head) flag = true;
            tmps1 = tmp1->next;
            tmps2 = tmp2->next;
            if(prev2 == tmp1){  //如果两个结点相邻
                prev2->next = tmps2;
                prev1->next = tmps1;
                tmp2->next = tmp1;
            }
            else{
                prev1->next = tmp2;
                prev2->next = tmp1;
                tmp2->next = tmps1;
                tmp1->next = tmps2;
            }
        }
        return flag ? prev1->next : head;
    }
};

总结

快慢指针可以用于寻找链表中点,寻找倒数第k个结点,判断是否有环等等,感觉贼有趣。如果上述内容有什么错误,还请各位看官直接留言,一定仔细检查勘正。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值