龟兔算法思路
在做LeetCode 287. Find the Duplicate Number 时,接触到了这个算法,在网上搜的讲解感觉有一些不够详细,虽然讲出了思路,但是有些跳跃,所以自己用比较徐,但是比较详细的话总结了一遍,方便日后回顾
- 初始化:设定两个指针,乌龟
slow
,兔子fast
, 第一步:判断是否有环。
slow
和fast
同时从起点出发,slow
一次走一步,fast
一次走两步。如果存在环,slow
肯定会在一点相遇。而且相遇的点在环里。如果不存在环,fast
先会到结尾处。比如两个人在同时从教室往操场跑道上跑,到了跑道之后开始绕圈跑,跑的快的肯定会在跑道的某一点和跑得慢的相遇。跑道即是环,跑道入口就是环的起点。假设数组起点到环起点长度为
m
,第一次相遇点到环起点长度为k
(前面已经说过k肯定在环内),环长度为n
。当第一次相遇时,slow
走的总距离i=m+an+k
,fast
走的总距离2i=m+bn+k
,an
,bn
表示在环里绕了a
圈和b
圈。fast
总距离是2i
因为fast
一次走两步,所以总距离是slow
的两倍。- 两个指针走的距离只差
i=(b-a)*n
。也就是说fast
比slow
多走的距离是环长度的整数倍,slow
一共走的距离也是环长度的整数倍。
第二步:将其中一个指针拿回数组起点(
fast
或slow
都行,这里我们拿fast
),另一个(在这是slow
)留在第一次相遇的点。让两个指针同时以每次一步的速度往前走。当fast
走了m+xn
之后(即走了m
到达环起点又绕了x
圈),fast
距离数组起点的距离是m+xn
。此时slow
从第一次相遇点也走了m+xn
,再加上最开始走的i
,slow
此时一共走了i+m+xn
的距离,由于i=(b-a)*n
,即i
是n
的整数倍,也就是说,现在slow
距离数组起点也是m
加上一定的圈数。所以此时slow
和fast
第二次相遇,相遇在环起点,这样就知道环起点在哪了。- 时间复杂度:O(n) // 解释待补充
例题
例题一:
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.
Note:
1. You must not modify the array (assume the array is read only).
2. You must use only constant, O(1) extra space.
3. Your runtime complexity should be less than O(n2).
4. There is only one duplicate number in the array, but it could be repeated more than once.
分析:
表面上看是找重复元素,可以挨个比较,但是时间复杂度是O(n2),不满足题目要求。也可用二分法查找,时间复杂度O(nlogn)。但是仔细分析会发现如果数组中存在重复元素,其实可以看成数组形成了一个环。
1->2->3->4->5
4 |
/ |
8 6
\ /
7
所以可以用龟兔算法找到环的入口,即是重复的元素。题目说假设只有一个重复元素。
代码实现:
public int findDuplicate(int[] nums) {
if (nums.length > 1) {
// 初始化两个指针,由于第一次相遇的判断是fast和slow是否相等,
// 如果都初始化为nums[0],第一个while循环就直接跳过了
// 这里的走两步并不是fast=nums[1],可以将nums数组看做映射关系,走两步是指映射两次
int slow = nums[0];
int fast = nums[nums[0]];
// 快的指针走两步,慢的走一步
// 此时找到的是第一次相遇的点,在环内。而slow和fast的值是相遇的点的数组的下标,相当于知道了m+k
while (fast != slow) {
slow = nums[slow];
fast = nums[nums[fast]];
}
// 将fast移到数组起点。slow仍在第一次相遇的位置。现在两个指针以相同的一次一步的速度往前走
fast = 0; // 此处不能赋值为nums[0]
while (fast != slow) {
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
return -1;
}
例题二:
Given a linked list, determine if it has a cycle in it.
直接用上述算法即可。
代码实现
// 节点定义
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public ListNode hasCycle(ListNode head) {
//不用额外内存,可以用两个指针,一个快一个慢,快的每次走两步,慢的每次走一步,
//当他俩重合时时,则是环,快的到链表结尾时,不是环
//即使第一回合没重合,后面肯定能重合
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head.next; // 此时的走两步则是多往后一个next
// 判断是否存在环
while (slow != fast) {
if (fast == null || fast.next == null)
return null;
fast = fast.next.next;
slow = slow.next;
}
// 寻找环起点,将fast拿回list的起点,不能直接放到head,而要放到head的前一个节点
ListNode pre = new ListNode(0);
pre.next=head;
fast=pre;
while(slow!=fast){
System.out.println("2");
fast=fast.next;
slow=slow.next;
}
return fast;
}