双指针算法之快慢指针

192 阅读1分钟

快慢指针的常见算法

快慢指针一般都是在初始化时,是快慢指针指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,或者使快慢指针在初始化时指向数组的起始位置,以此来巧妙解决一些链表和数组中的问题。

1、去除数组中指定值

链接:leetcode-cn.com/problems/re…

移除数组中出现的大小指定的某个值

不要使用额外的数组空间来移除数组中值为val的元素,给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变你不需要考虑数组中超出新长度后面的元素。

示例 1:

给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。即[2,2],你不需要考虑数组中超出新长度后面的元素。

    int length = array.length;

    int slowIndex = 0;

    for (int fastIndex = 0; fastIndex < length; fastIndex++) {

        if (target != array[fastIndex]) {

            array[slowIndex++] = array[fastIndex];

        }

    }

    return slowIndex;

}

思想:使用两个指针,一个快一个慢,快指针在遇到目标值时直接跳过目标值,索引+1,再将快指针对应的数字赋值给慢指针,从而实现删除

2、判定链表中是否含有环

单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。

如果链表中不包含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。

boolean hasCycle(ListNode head) {

    while (head != null)

        head = head.next;

    return false;

}

但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。

经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。

boolean hasCycle(ListNode head) {

    ListNode fast, slow;

    fast = slow = head;

    while(fast != null && fast.next != null) {

        fast = fast.next.next;

        slow = slow.next;

        

        if (fast == slow)

            return true;

    }

    return false;

}

3、已知链表中含有环,返回这个环的起始位置(有点奇技淫巧)

image.png 这个问题其实不困难,有点类似脑筋急转弯,先直接看代码:

ListNode detectCycle(ListNode head) {

    ListNode fast, slow;

    fast = slow = head;

    while (fast != null && fast.next != null) {

        fast = fast.next.next;

        slow = slow.next;

        if (fast == slow)

            break;

    }

    

    slow = head;

    while (slow != fast) {

        fast = fast.next;

        slow = slow.next;

    }

    return slow;

}

}

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

第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。

image.png 设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。

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

4、寻找链表的中点

类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。

ListNode slow, fast;

slow = fast = head;

while (fast != null && fast.next != null) {

    fast = fast.next.next;

    slow = slow.next;

}

// slow 就在中间位置

return slow;

当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右:

image.png 寻找链表中点的一个重要作用是对链表进行【归并排序】。

回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。

但是现在你学会了找到链表的中点,就能实现链表的二分了。

5、寻找链表的倒数第 k 个元素

我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度):

ListNode slow, fast;

slow = fast = head;

while (k-- > 0)

    fast = fast.next;

\


while (fast != null) {

    slow = slow.next;

    fast = fast.next;

}

return slow;

【参考】
【1】zhuanlan.zhihu.com/p/138273078

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。