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:
- You must not modify the array (assume the array is read only).
- You must use only constant, O(1) extra space.
- Your runtime complexity should be less than
O(n2)
. - There is only one duplicate number in the array, but it could be repeated more than once.
題目標簽:Array, Binary Search, Two Pointers
題目給了我們一個nums array, 讓我們找到其中的重復數字。因為這一題有4個條件,所以有難度。1. 要求我們不能改動array;2. 只能用O(1)空間;3. 時間要小於O(n^2);4. 只有一個重復的數字,但是它可以出現最少1次。
方法1:O(logn * n)
利用binary search。
題目給的數字是在 [1, n] 之間, array 的size 是 n+1。所以肯定有一個數字會至少出現2次。
分析一下:
如果n 是5,那么就會有1 2 3 4 5 一共5個數字的可能,而array size 是6,那么其中一個數字肯定會至少出現兩次。
如果沒有重復的數字,小於等於1的數字 出現的次數 等於 1;
小於等於2的數字 出現的次數 等於 2;
... 同理3;4;5。
如果有重復的數字,如果重復的是1,那么 小於等於1的數字 出現的次數 肯定大於1;
基於這個理論,我們可以在1 2 3 4 5 選出一個 mid, 遍歷array來count 小於等於mid 的數字個數 小於等於 它自己mid 還是 大於 mid?
如果count 小於等於mid, 說明 1 到 mid 這些數字 沒有重復項, 重復項在 右半邊 mid 到n, 所以縮小到右半邊繼續搜索;
如果count 大於mid, 說明 1 到 mid 這些數字中 有重復項,縮小到 左半邊繼續搜索。
Java Solution:
Runtime beats 28.13%
完成日期:09/13/2017
關鍵詞:Array, Binary Search
關鍵點:如果從1到n中 有重復項,那么必然有一個數字出現的次數至少是2次
1 class Solution 2 { 3 public int findDuplicate(int[] nums) 4 { 5 /* Solution 1: binary search */ 6 int low = 1, high = nums.length - 1; 7 8 while(low <= high) 9 { 10 int mid = low + (high - low) / 2; 11 int cnt = 0; 12 13 for(int a: nums) 14 { 15 if(a <= mid) 16 cnt++; 17 } 18 19 if(cnt <= mid) // meaning duplicate is on the right side 20 low = mid + 1; 21 else // if cnt > mid, meaning duplicate is on the left side 22 high = mid - 1; 23 24 } 25 26 return low; 27 } 28 }
參考資料:
http://bookshadow.com/weblog/2015/09/28/leetcode-find-duplicate-number/
方法2:O(n),利用slow 和fast 雙指針,這個方法太巧妙了,有好幾個點比較難理解。
利用slow 和 fast 指針,經歷兩次 相遇,來找到重復的數字。
相遇1: slow 一次走一步;fast 一次走兩步。
當它們相遇的時候,讓slow 保留在相遇的點上。
相遇2:讓fast 從起點開始走,一次走一步;slow 在上一次相遇的點上開始走,一次走一步。
當它們相遇的時候,相遇的點就是重復的數字。
如何讓slow fast 走步呢,利用slow = nums[slow]; fast = nums[nums[fast]]. 把index 所指的數字值 替換index 來模仿走步數。
來結合圖片分析幾個關鍵點:
S 為起始點,O 為圓的入口,M1 為第一次相遇的地方,x 為S到O的長度, y 為 O到M1的長度, z 為 M1到O的長度。
1. 第一次相遇是為了找到它們的相遇點, 第二次相遇是為了找到圓的入口 O。
2. 路程會有一段path 接着和一個circle:
舉例子來看一下:
0 1 2 3 4 5 6 7
[4,6,2,1,7,1,3,5]
n = 7, 數字都是 1 ~ 7 中的選擇, array size = 8, 肯定會有一個數字,至少出現2次。
按照我們的方法來走一下:0 -> 4 -> 7 -> 5 -> 1 -> 6 -> 3 -> 1 -> 6 -> 3 -> 1 -> 6 -> 3 -> 1 ... 一直重復 1 6 3
紅色部分可以看作為S - O path; 綠色部分可以看作為 O點; 藍色部分看作為 圓。
3. 為什么會有一段path:
因為數字只能是 1 到 n, 所以index 0 肯定只能走過一次, 那么肯定有一段path, path 長度得看情況。
4. 為什么會有一個circle:
因為index 除了0, 有1到n(而且唯一不重復); 數字也是1到n, 所以每一次 index 指向數字 時候, 那個數字肯定能成為新的和唯一的 index,一直循環。
5. 為什么O 點一定是 重復的那個數字?
我們來看之前的例子: index array 里,紅色的兩列是重復的數字。3 能走到 1, 5 也能走到1, 3和5 代表着兩條路,所以當兩條路匯合的地方,就是重復的數字1。
6. 為什么x = z?
換句話說,為什么第二次它們能夠相遇。這應該是最麻煩的地方。
方法1: 驗證一下
第一次相遇后: slow 走的路程 = x + y;fast 走的路程 = x + y + z + y;fast = x + 2y + z。
因為第一次是slow 走一步,fast 走兩步,所以 2slow = fast。
代入公式:2x + 2y = x + 2y + z
x = z
方法2:我們想象一下,
情況1:如果slow 和fast 都從O點出發,slow 一次走一步,fast 一次走兩步,那么當它們相遇的時候,slow 走了一圈,fast 走了兩圈,而且它們一定相遇在O點;(可以在圖上畫刻度,自己走走看)
情況2:如果我們多加一段path, 讓它們從S點出發,slow 一次走一步,fast 一次走兩步, 那么它們什么時候能相遇呢? 情況1中它們相遇在O點,那么多加一段path(x 是path的長度),它們會在O點的基礎上,提早x 的距離相遇,因為它們多走了一段x的path,所以fast 會在O之前就趕上slow。所以 x 肯定等於 z。
7. 如果不太明白的話,可以畫一個類似的圖,再畫上刻度,自己多走幾次,感受一下。
Java Solution:
Runtime beats 51.39%
完成日期:09/14/2017
關鍵詞:Array, Two Pointers
關鍵點:快慢指針相遇的點就是重復的數字
1 class Solution 2 { 3 public int findDuplicate(int[] nums) 4 { 5 /* Solution 2: */ 6 int slow = 0, fast = 0; 7 // fist meeting 8 do 9 { 10 slow = nums[slow]; // slow takes one step each time 11 fast = nums[nums[fast]]; // fast takes two steps each time 12 13 }while(slow != fast); 14 15 // second meeting 16 fast = 0; 17 18 while(slow != fast) 19 { 20 slow = nums[slow]; // both slow and fast take one step each time 21 fast = nums[fast]; 22 } 23 // when they meet, the place must be entry of circle, and must be duplicate 24 return slow; 25 } 26 }
參考資料:
http://blog.csdn.net/monkeyduck/article/details/50439840
LeetCode 題目列表 - LeetCode Questions List