You are given a circular array nums
of positive and negative integers. If a number k at an index is positive, then move forward k steps. Conversely, if it's negative (-k), move backward k steps. Since the array is circular, you may assume that the last element's next element is the first element, and the first element's previous element is the last element.
Determine if there is a loop (or a cycle) in nums
. A cycle must start and end at the same index and the cycle's length > 1. Furthermore, movements in a cycle must all follow a single direction. In other words, a cycle must not consist of both forward and backward movements.
Example 1:
Input: [2,-1,1,2,2] Output: true Explanation: There is a cycle, from index 0 -> 2 -> 3 -> 0. The cycle's length is 3.
Example 2:
Input: [-1,2] Output: false Explanation: The movement from index 1 -> 1 -> 1 ... is not a cycle, because the cycle's length is 1. By definition the cycle's length must be greater than 1.
Example 3:
Input: [-2,1,-1,-2,-2] Output: false Explanation: The movement from index 1 -> 2 -> 1 -> ... is not a cycle, because movement from index 1 -> 2 is a forward movement, but movement from index 2 -> 1 is a backward movement. All movements in a cycle must follow a single direction.
Note:
- -1000 ≤ nums[i] ≤ 1000
- nums[i] ≠ 0
- 1 ≤ nums.length ≤ 5000
Follow up:
Could you solve it in O(n) time complexity and O(1) extra space complexity?
說實話,這道題描述的並不是很清楚,比如題目中有句話說循環必須是 forward 或是 backward 的,如果不給例子說明,不太容易能 get 到 point。所謂的循環必須是一個方向的就是說不能跳到一個數,再反方向跳回來,這不算一個 loop。比如 [1, -1] 就不是一個 loop,而 [1, 1] 是一個正確的 loop。看到論壇中一半的帖子都是各種需要 clarify 和不理解 test case 就感覺很好笑~ 當然博主也成功踩坑了。弄清楚了題意后來考慮如何做,由於從一個位置只能跳到一個別的位置,而不是像圖那樣一個點可以到多個位置,所以這里我們就可以用個 HashMap 根據坐標建立一對一的映射,一旦某個達到的坐標已經有映射了,說明環存在,當然我們還需要進行一系列條件判斷。首先我們需要一個 visited 數組,來記錄訪問過的數字,然后我們遍歷原數組,如果當前數字已經訪問過了,直接跳過,否則就以當前位置坐標為起始點開始查找,進行 while 循環,計算下一個位置,計算方法是當前位置坐標加上對應的數字,由於是循環數組,所以結果可能會超出數組的長度,所以我們要對數組長度取余。當然上面的數字也可能是負數,加完以后可能也是負數,所以在取余之前還得再補上一個n,使其變為正數,但是 若這個負數遠大於n的話,取余之前只加上一個n,可能是不夠的,所以正確的方法是應該先對n取余,再加上n。為了同時把正數的情況也包含進來,最終我們的處理方法是先對n取余,再加上n,再對n取余,這樣不管正數還是負數,大小如何,都可以成功的旋轉跳躍了。此時我們判斷,如果 next 和 cur 相等,說明此時是一個數字的循環,不符合題意,再有就是檢查二者的方向,數字是正數表示 forward,若是負數表示 backward,在一個 loop 中必須同正或同負,我們只要讓二者相乘,如果結果是負數的話,說明方向不同,直接 break 掉。此時如果 next 已經有映射了,說明我們找到了合法的 loop,返回 true,否則建立一個這樣的映射,將 next 位置在 visited 數組中標記 true,繼續循環,參見代碼如下:
解法一:
class Solution { public: bool circularArrayLoop(vector<int>& nums) { int n = nums.size(); vector<bool> visited(n); for (int i = 0; i < n; ++i) { if (visited[i]) continue; visited[i] = true; unordered_map<int, int> m; int cur = i; while (true) { int next = ((cur + nums[cur]) % n + n) % n; if (next == cur || nums[next] * nums[cur] < 0) break; if (m.count(next)) return true; m[cur] = next; cur = next; visited[next] = true; } } return false; } };
我們也可以不用 visited 數組,直接在 nums 中標記,由於題目中說了 nums 中不會有0,所以可以把訪問過的位置標記為0。然后計算 next 位置,對於會超出數組的長度的正數,我們可以通過對n取余,但是對於負數,若這個負數遠大於n的話,取余之前只加上一個n,可能是不夠的,所以正確的方法是應該先對n取余,再加上n。為了同時把正數的情況也包含進來,最終我們的處理方法是先對n取余,再加上n,再對n取余,這樣不管正數還是負數,大小如何,都可以成功的旋轉跳躍了。接下來看,如果 next 和i相等,直接跳過,因為這表示循環只有一個數字,不合題意。然后開始循環,當 cur 和 nums[next] 的乘積為正時,說明此時是一個方向的,我們將 cur 賦值為 nums[next],將 nums[next] 賦值為0,表示已經訪問過,然后再計算新的 next 位置。直到循環被打破,若此時 next 和i相等,說明有大於1個數字的環存在,返回 true。最終 for 循環退出后,返回 false 即可,代碼參見評論區11樓。想法倒是不錯,但是存在一個邏輯上的錯誤。
對於 [1, 1, 2] 這個例子,當 i=0 時,最后跳出 while 循環時 next 是等於1的,和 i 不相等,所以沒法返回 true。但這個例子其實是有正確的循環的,是后兩個數字1和2可以循環,那為什么我們沒 catch 到呢?因為我們前面的思路默認為循環開始的位置就是i,但明顯不一定成立的,就像在鏈表中找環一樣,環的起點可以是任意位置啊,不一定是表頭結點啊。那為啥上面的解法就可以呢?這是因為上面使用的是 HashMap,而且對於每個i,都使用了一個新的 HashMap,跟之前的並沒有聯系,而且把以i位置為起點,經過的位置都存到了 HashMap 中,這樣是可以找出環的。而這里我們直接跟i相比肯定是不對的。那么既然說到了鏈表中找環,刷題老司機們大聲告訴我該用什么?對了,就是快慢指針了。那么對於每個i位置,慢指針指向i,快指針指向下一個位置,這里調用子函數來計算下一個位置。此時慢指針指向的數要和快指針指向的數正負相同,這個不難理解。並且慢指針指向的數還要跟快指針的下一個位置上的數符號相同,這個原因后面再講。上面這兩個就是 while 循環的條件,我們直到當快慢指針相遇的時候,就是環出現的時候,但是這里有個坑,即便快慢指針相遇了,也不同立馬返回 true,因為題目中說了了環的長度必須大於1,所以我們要用慢指針指向的數和慢指針下一個位置上的數比較,若相同,則說明環的長度為1,此時並不返回 false,而且 break 掉 while 循環。因為這只能說以i位置開始的鏈表無符合要求的環而已,后面可能還會出現符合要求的環。但是若二者不相同的話,則已經找到了符合要求的環,直接返回 true。若快慢指針還不相同的,則分別更新,慢指針走一步,快指針走兩步。當 while 循環退出后,我們需要標記已經走過的結點,從而提高運算效率,方法就是將慢指針重置為i,再用一個 while 循環,條件是 nums[i] 和 慢指針指的數正負相同,然后計算下一個位置,並且 nums[slow] 標記為0,並且慢指針移動到 next 位置。最終 for 循環退出后,返回 false 即可,參見代碼如下:
解法二:
class Solution { public: bool circularArrayLoop(vector<int>& nums) { int n = nums.size(); for (int i = 0; i < n; ++i) { if (nums[i] == 0) continue; int slow = i, fast = getNext(nums, i), val = nums[i]; while (val * nums[fast] > 0 && val * nums[getNext(nums, fast)] > 0) { if (slow == fast) { if (slow == getNext(nums, slow)) break; return true; } slow = getNext(nums, slow); fast = getNext(nums, getNext(nums, fast)); } slow = i; while (val * nums[slow] > 0) { int next = getNext(nums, slow); nums[slow] = 0; slow = next; } } return false; } int getNext(vector<int>& nums, int i) { int n = nums.size(); return (((nums[i] + i) % n) + n) % n; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/457
參考資料:
https://leetcode.com/problems/circular-array-loop/
https://leetcode.com/problems/circular-array-loop/discuss/94148/Java-SlowFast-Pointer-Solution