题目描述
给定一个含有正整数和负整数的环形数组 nums
。 如果某个索引中的数 k
为正数,则向前移动 k
个索引。相反,如果是负数 -k
,则向后移动 k
个索引。因为数组是环形的,所以可以假设最后一个元素的下一个元素是第一个元素,而第一个元素的前一个元素是最后一个元素。
确定 nums
中是否存在循环(或周期)。循环必须在相同的索引处开始和结束并且循环长度大于 1。此外,一个循环中的所有运动都必须沿着同一方向进行。换句话说,一个循环中不能同时包括向前的运动和向后的运动。
样例
输入:[2,-1,1,2,2]
输出:true
解释:存在循环,按索引 0 -> 2 -> 3 -> 0。循环长度为 3。
输入:[-1,2]
输出:false
解释:按索引 1 -> 1 -> 1 ... 的运动无法构成循环,因为循环的长度为 1。
根据定义,循环的长度必须大于 1。
输入:[-2,1,-1,-2,-2]
输出:false
解释:按索引 1 -> 2 -> 1 -> ... 的运动无法构成循环,
因为按索引 1 -> 2 的运动是向前的运动而按索引 2 -> 1 的运动是向后的运动。
一个循环中的所有运动都必须沿着同一方向进行。
限制
-1000 <= nums[i] <= 1000
nums[i] != 0
0 <= nums.length <= 5000
进阶
- 你能写出时间时间复杂度为 $O(n)$ 和额外空间复杂度为 $O(1)$ 的算法吗?
算法
(路径标记寻环) $O(n)$
- 首先将每个位置模
n
,去掉那些走到自己的点。 - 对于每个非 0 位置开始寻环
- 按照题目描述,如果当前位置大于 0,则寻前进环,访问到已经访问过得点或者值为负的点结束;否则寻倒退环,访问到已经访问过得点或者值为正的点结束。
- 重点是寻环过程需要记录已经访问过的点,此时可以做一个特殊标记(例如遍历过的位置加一个大的
offset
);如果又回到了特殊标记过的位置,则说明有环,返回true
。 - 如果没有走到特殊标记过的位置,说明不存在环;此时,需要再走一遍刚才走过的路径,把路径上的所有点请 0(防止再被访问)。
时间复杂度
- 每个点最多被访问常数,故总时间复杂度为 $O(n)$。
空间复杂度
- 在原数组上做标记,故仅需要常数的额外空间。
C++ 代码
class Solution {
private:
bool checkForward(int start, vector<int> &nums) {
const int offset = 10000;
const int n = nums.size();
int i = start;
do {
int j = (i + nums[i] + n) % n;
nums[i] += offset;
i = j;
} while (nums[i] > 0 && nums[i] <= 1000);
if (nums[i] > 1000)
return true;
i = start;
do {
int j = (i + nums[i] - offset + n) % n;
nums[i] = 0;
i = j;
} while (nums[i] > 0);
return false;
}
bool checkBackward(int start, vector<int> &nums) {
const int offset = 10000;
const int n = nums.size();
int i = start;
do {
int j = (i + nums[i] + n) % n;
nums[i] += offset;
i = j;
} while (nums[i] < 0);
if (nums[i] > 1000)
return true;
i = start;
do {
int j = (i + nums[i] - offset + n) % n;
nums[i] = 0;
i = j;
} while (nums[i] < 0);
return false;
}
public:
bool circularArrayLoop(vector<int>& nums) {
const int n = nums.size();
for (int i = 0; i < n; i++)
nums[i] %= n;
for (int i = 0; i < n; i++) {
if (nums[i] == 0)
continue;
if (nums[i] > 0 && checkForward(i, nums))
return true;
if (nums[i] < 0 && checkBackward(i, nums))
return true;
}
return false;
}
};
算法2
(快慢指针寻环) $O(n)$
- 和算法 1 类似,只不过采用了经典的快慢指针寻环。
时间复杂度
- 每个点最多被访问常数,故总时间复杂度为 $O(n)$。
空间复杂度
- 在原数组上做标记,故仅需要常数的额外空间。
C++ 代码
class Solution {
private:
bool checkForward(int start, vector<int> &nums) {
const int n = nums.size();
int slow = start, fast = (start + nums[start] + n) % n;
while (1) {
slow = (slow + nums[slow] + n) % n;
if (nums[slow] <= 0) break;
fast = (fast + nums[fast] + n) % n;
if (nums[fast] <= 0) break;
fast = (fast + nums[fast] + n) % n;
if (nums[fast] <= 0) break;
if (slow == fast)
return true;
};
int i = start;
while (nums[i] > 0) {
int j = (i + nums[i] + n) % n;
nums[i] = 0;
i = j;
}
return false;
}
bool checkBackward(int start, vector<int> &nums) {
const int n = nums.size();
int slow = start, fast = (start + nums[start] + n) % n;
while (1) {
slow = (slow + nums[slow] + n) % n;
if (nums[slow] >= 0) break;
fast = (fast + nums[fast] + n) % n;
if (nums[fast] >= 0) break;
fast = (fast + nums[fast] + n) % n;
if (nums[fast] >= 0) break;
if (slow == fast)
return true;
};
int i = start;
while (nums[i] < 0) {
int j = (i + nums[i] + n) % n;
nums[i] = 0;
i = j;
}
return false;
}
public:
bool circularArrayLoop(vector<int>& nums) {
const int n = nums.size();
for (int i = 0; i < n; i++)
nums[i] %= n;
for (int i = 0; i < n; i++) {
if (nums[i] == 0)
continue;
if (nums[i] > 0 && nums[(i + nums[i] + n) % n] > 0 && checkForward(i, nums))
return true;
if (nums[i] < 0 && nums[(i + nums[i] + n) % n] < 0 && checkBackward(i, nums))
return true;
}
return false;
}
};