Pattern: Fast & Slow pointers, 快慢指针类型
介绍部分来自:链接:https://www.zhihu.com/question/36738189/answer/908664455 作者:穷码农
这种模式,有一个非常出门的名字,叫龟兔赛跑。 咱们肯定都知道龟兔赛跑啦。但还是再解释一下快慢指针:这种算法的两个指针的在数组上(或是链表上,序列上)的移动速度不一样。还别说,这种方法在解决有环的链表和数组时特别有用。
通过控制指针不同的移动速度(比如在环形链表上),这种算法证明了他们肯定会相遇的。快的一个指针肯定会追上慢的一个(可以想象成跑道上面跑得快的人套圈跑得慢的人)。
上面这个图演示了快慢两个指针最终在5相遇了
咋知道需要用快慢指针模式勒?
- 问题需要处理环上的问题,比如环形链表和环形数组
- 当你需要知道链表的长度或是某个特别位置的信息的时候
那啥时候用快慢指针而不是上面的双指针呢?
- 有些情形下,咱们不应该用双指针,比如我们在单链表上不能往回移动的时候。一个典型的需要用到快慢指针的模式的是当你需要去判断一个链表是否是回文的时候。
总结: 慢指针执行一个步骤,快指针执行另两个步骤。快慢指针一般用于两者最终可以相遇(相等)的情况下使用。
经典题目:
链表的数据结构:
// Definition for singly-linked list.
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
1、LinkedList Cycle (easy)
描述:
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例:
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
使用快慢指针:
public boolean hasCycle(ListNode head) {
if (head == null)
return false;
ListNode slow = head; // 慢指针
ListNode fast = head; // 快指针
while (fast != null && fast.next != null){
fast = fast.next.next; // 快指针走两步
slow = slow.next; // 慢指针走一步
if (fast == slow) // 最终相遇则有环
return true;
}
return false;
}
使用破坏链表法:
public boolean hasCycle(ListNode head) {
if (head == null)
return false;
ListNode p = head;
while (p != null){
ListNode next = p.next; // 下一个节点
if (next == head) return true; // 等于头节点则返回
p.next = head; // 破坏链表
p = next; // 下一个节点继续遍历
}
return false;
}
2、Start of LinkedList Cycle (medium)
注意: 慢指针一定是走不到一圈就相遇了,因为如果在环的入口点没有相遇的话,快指针的速度是慢指针的两倍,慢指针在入口点时快指针已经进入环内,在慢指针走完一圈之前,快指针一定会追上它。最差的情况就是在入口点相遇,这是快指针走了两圈,慢指针刚好走了一圈。
快指针每次走两步:a + nc + b
慢指针每次走一步:a + b
距离关系有 a + nc + b = 2(a + b)
得:a = b - nc
当 n = 1
时 a = c - b
。也就是 相遇点到环起点(顺时针) = 起始点到环起点。故使用一个指针从头结点开始遍历,慢指针从相遇点开始遍历,两者就会在环起点相遇。
当 n = 0
时,a = b
。也就是表头就是环入口点。
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null)
return null;
// 找出相遇点
ListNode slow = head; // 慢指针,每次走一步
ListNode fast = head; // 快指针,每次走两步
boolean isCycle = false; // 是否有环标志
while (fast != null && fast.next != null){
fast = fast.next.next; // 走两步
slow = slow.next; // 走一步
if (fast == slow){
isCycle = true; // 有环标志
break; // 退出循环
}
}
// 有环则找出起始点
if (isCycle){
ListNode first = head; // 从头结点开始遍历,最终和慢指针在环入口点相遇
while (first != slow){
first = first.next;
slow = slow.next;
}
return slow;
}else {
return null;
}
}
}
3、Happy Number (medium)
描述
写一个算法来判断一个数是不是"快乐数"。
一个数是不是快乐是这么定义的:对于一个正整数,每一次将该数替换为他每个位置上的数字的平方和,然后重复这个过程直到这个数变为1,或是无限循环但始 终变不到1。如果可以变为1,那么这个数就是快乐数。
样例
例1:
输入:19
输出:true
说明:
19是一个快乐的数字
1 ^ 2 + 9 ^ 2 = 82
8 ^ 2 + 2 ^ 2 = 68
6 ^ 2 + 8 ^ 2 = 100
1 ^ 2 + 0 ^ 2 + 0 ^ 2 = 1
例2:
输入:5
输出:false
说明:
5不是一个快乐的数字
25->29->85->89->145->42->20->4->16->37->58->89
再次出现89。
如果给定的数字会一直循环,则可以使用快慢指针判断是否重复。
slow
指针进行一次替换操作
fast
指针进行两次替换操作
他们最终会在重复的数字相遇,即slow==fast
。
如果他们最终都为 slow == fast == 1
,即快乐数。否则,为不快乐数。
class Solution {
public boolean isHappy(int n) {
if (n == 0)
return false;
if (n == 1)
return true;
int slow = n; // 慢指针,进行一次替换操作
int fast = n; // 快指针,进行两次替换操作
// 当 slow != fast 时一直循环
do {
slow = squareSum(slow);
fast = squareSum(fast);
fast = squareSum(fast);
}while (slow != fast);
// 在 1 相遇 即 slow == fast == 1 即快乐数
if (fast == 1){
return true;
}
return false;
}
public int squareSum(int n){
int squareSum = 0;
while (n != 0){
squareSum += (n % 10) * (n % 10); // 取个位数
n /= 10; // 缩小十位
}
return squareSum;
}
}
4、Middle of the LinkedList (easy)
描述
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
The number of nodes in the given list will be between 1 and 100.
样例
样例 1:
输入:1->2->3->4->5->null
输出:3->4->5->null
样例 2:
输入:1->2->3->4->5->6->null
输出:4->5->6->null
解题思路:
使用快慢指针,快指针每次走两步,慢指针每次走一步。当快指针到达链表尾部的时候,慢指针就到达链表的中部。
public class Solution {
/**
* @param head: the head node
* @return: the middle node
*/
public ListNode middleNode(ListNode head) {
// write your code here.
if (head == null)
return null;
ListNode slow = head; // 慢指针,每次走一步
ListNode fast = head; // 快指针,每次走两步
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}