【LeetCode刷题笔记】链表与快慢指针

【LeetCode刷题笔记】链表与快慢指针

专门开一篇专栏记录一下涉及到链表问题时,快慢指针的应用。

参考资料:

  1. 快慢指针判断单向链表是否有环及找环入口
  2. 看图理解单链表的反转
  3. [LeetCode] Linked List Cycle 单链表中的环
  4. [LeetCode] Linked List Cycle II 单链表中的环之二

一. 使用快慢指针来找到链表的中点

当题目中的数据以链表的形式进行存储时,与字符串的形式相比,由于链表不存在下标,也就意味着找到数据的中点就变得相对困难。因此,使用快慢指针在这里就显得格外的便捷:

  1. 首先我们设置两个指针slow和fast,slow指针每次移动一步,fast指针每次移动两步;
  2. 如果链表中节点个数为偶数时,当快指针无法继续移动时,慢指针刚好指向中点;如果链表中节点个数为奇数时,当快指针走完,慢指针指向中点前一个节点。
ListNode *slow=head,*fast=head;
while(fast->next&&fast->next->next)
{
slow=slow->next;//slow指针每次走一步
fast=fast->next->next;//fast指针每次走两步
}

看一个简单的应用:

Palindrome Linked List - LeetCode

234.Palindrome Linked List

Given a singly linked list, determine if it is a palindrome.

Example 1:

Input: 1->2
Output: false

Example 2:

Input: 1->2->2->1
Output: true

Follow up:

Could you do it in O(n) time and O(1) space?


Note:题目要求我判断一个链表形式的数组是否为回文数组。想要判断回文数组,最重要的一步就是首先找到链表的中点,然后再一一进行比较,这就要利用到之前提到的方法:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if(!head||!head->next)
            return true;
        ListNode *slow=head,*fast=head;
        stack<int> s;
        s.push(head->val);
        while(fast->next&&fast->next->next)
        {
            slow=slow->next;
            fast=fast->next->next;
            s.push(slow->val);//将慢指针指向的数据依次压入栈中
        }
        if(!fast->next)//说明快指针走到头了,节点个数为奇数,此时慢指针指向中点数字
            s.pop();//将中点pop掉
        while(slow->next)//依次将中点两侧数据进行比较
        {
            slow=slow->next;
            int tmp=s.top();
            s.pop();
            if(tmp!=slow->val)
                return false;
        }
        return true;
    }
};


二.链表的翻转

链表的翻转有很多种方法,例如使用额外空间的方法,递归的方法,利用指针交换的方法等等,具体可以参见文章片头参考资料里面的链接。这里只介绍一种最容易理解的,利用指针实现链表翻转的方法:

1.设置三个指针p,q,r,将p指向链表开头,q指向第二个结点;

p=head;
q=head->next;

2.将头结点与第二个结点断开;

head->next = NULL;

3.循环:

  • 将q结点的当前下一个结点设为r;
  • 将q指向前一个节点p;
  • p,q,r依次向后移动一个节点
r = q->next;
q->next = p;
p = q;
q =r;
r = q->next


来看一个例子:

Reorder List - LeetCode

143. Reorder List

Given a singly linked list L: L0→L1→…→Ln-1→Ln,
reorder it to: L0→LnL1→Ln-1→L2→Ln-2→…

You may not modify the values in the list's nodes, only nodes itself may be changed.

Example 1:

Given 1->2->3->4, reorder it to 1->4->2->3.

Example 2:

Given 1->2->3->4->5, reorder it to 1->5->2->4->3.

这题我们可以采用三个步骤来解决:

  1. 寻找链表的中点,将原链表断开为链表1和链表2;
  2. 将链表2翻转;
  3. 将链表2的结点交错的添加入链表1中。
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void reorderList(ListNode* head) {
        if(!head||!head->next||!head->next->next)
            return;
        ListNode *slow=head,*fast=head;
        while(fast->next&&fast->next->next)//寻找链表的中点
        {
            slow=slow->next;
            fast=fast->next->next;
        }
        ListNode *p,*q,*r;
        p=slow->next;//无论奇偶,slow指针所在位置都是链表1的末结点
        slow->next=NULL;
        q=p->next;
        p->next=NULL;
        while(q)//翻转链表的后半部分
        {
            r=q->next;
            q->next=p;
            p=q;
            q=r;
        }
        while(head&&p)//将第二个链表依次插入第一个链表中
        {
            q=head->next;
            head->next=p;
            p=p->next;
            head->next->next=q;
            head=q;
        }
        
    }
};

三.利用快慢指针来判断链表中是否有环(并找出环的入口)

首先,使用快慢指针判断链表中是否有环就好比是郭小明和博尔特赛跑,如果两人在笔直的道路上赛跑(链表不含有环),那么从同一起点出发后,郭小明永远追不上(遇不到)博尔特;如果是类似马拉松比赛,最后是在体育馆中的环形跑道跑完(假设无限跑),那么博尔特总会在环形跑道中“追上”郭小明。

有人可能会问,与赛跑不同,快指针每次走两步,慢指针每次走一步,那么存不存在一种情况,快指针直接“越过了”慢指针,到达了慢指针的下一个格子呢?

答案是不可能的,我们假设这种状态存在,也就是快指针越过了慢指针,领先慢指针一个格子,逆推一步,慢指针倒退一格,快指针倒退两格,那么快慢指针依然相遇,也就是说,快指针永远不可能直接越过慢指针,到达后一格。

所以利用快慢指针来判断链表中是否有环的思路就是:

  1. 设置两个指针,慢指针每次走一步,快指针每次走两步;
  2. 如果快慢指针相遇,那么链表含有环;
  3. 如果快指针到达了链表尾部且没有与慢指针相遇,那么链表不含有环。

我们来看一下具体实现:

LeetCode 141:Linked List Cycle

141. Linked List Cycle

Given a linked list, determine if it has a cycle in it.

Follow up:
Can you solve it without using extra space?

/**
 * 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) {
        ListNode *slow=head,*fast=head;
        while(fast&&fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;//设置快慢指针步长
            if(slow==fast)
                return true;//相遇则表示存在环
        }
        return false;
    }
};

这个判断的方法很巧妙也很实用,接下来我们要考虑的是,判断出链表存在环之后,我们如何寻找环的入口呢?

我们考虑如下情形:

慢指针:A——>B----->C(共走了N步)

快指针:A——>B----->C(共走了2N步)

快指针fast比慢指slow针多走了N步,因此,慢指针slow如果继续再走N步,会从C----->C,依然回到C点相遇点;

现在我们再设一慢指针slow2(方便起见可以重新定义之前的快指针)从A点出发,和原慢指针slow以相同步长移动,原来的慢指针slow继续从相遇点C往前走;

slow2经过N步之后到达C,slow经过N步之后也到达C,我们考察一下两个指针的轨迹:

slow:C----->B——>C

slow2:A----->B——>C

由此可见,两个指针若想要在C点相遇,其必要条件就是在B点(环入口)就相遇,然后“携手”走完B——>C的路程。


这样我们就获得了求解换入口的方法,看具体实现:

Linked List Cycle II - LeetCode

142. Linked List Cycle II

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

Note: Do not modify the linked list.

Follow up:
Can you solve it without using extra space?

/**
 * 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) {
        ListNode *slow=head,*fast=head;
        while(fast&&fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
            if(slow==fast)
                break;
        }
        if(!fast||!fast->next)
            return NULL;
        fast=head;//重新定义快指针,并将其指向起点
        while(slow!=fast)
        {
            fast=fast->next;
            slow=slow->next;
        }
        return fast;
    }
};

(未完待续)

编辑于 2018-07-02

文章被以下专栏收录