Since the array is circular, there is always a cycle. A loop is:
- A cycle with at least two indices.
- Within the cycle, the traversal direction does not change.
For example, below, if we start from index 0, the direction changes at the hop . But within the cycle:
, the direction does not change. So, we declare it has a loop.

Starting from each index we can check the above two conditions using slow and fast pointers.
Time: , space:
.
class Solution:
def has_loop_from(self, begin, nums):
def next(i):
return (i+nums[i]) % len(nums)
slow, fast = begin, next(begin)
while slow != fast:
slow = next(slow)
fast = next(next(fast))
head = slow
dir = nums[head]
while True:
slow = next(slow)
# Direction changed
if nums[slow] * dir < 0:
return False
if slow == head:
break
# Cycle length > 1
return slow != next(slow)
def circularArrayLoop(self, nums: List[int]) -> bool:
for i in range(len(nums)):
if self.has_loop_from(i, nums):
return True
return False
If a cycle is not a loop, starting from different indices of that cycle is not going to find anything new. We can skip this wasted work by zeroing out (making self-loop) the indices of a not-loop cycle.
Time: , space:
.
class Solution:
def zero_out_cycle(self, begin, nums):
def next(i):
return (i+nums[i]) % len(nums)
while nums[begin] != 0:
begin_next = next(begin)
nums[begin] = 0
begin = begin_next
def has_loop_from(self, begin, nums):
def next(i):
return (i+nums[i]) % len(nums)
slow, fast = begin, next(begin)
while slow != fast:
slow = next(slow)
fast = next(next(fast))
head = slow
dir = nums[head]
while True:
slow = next(slow)
# Direction changed
if nums[slow] * dir < 0:
return False
if slow == head:
break
# Cycle length > 1
return slow != next(slow)
def circularArrayLoop(self, nums: List[int]) -> bool:
for i in range(len(nums)):
if self.has_loop_from(i, nums):
return True
else:
self.zero_out_cycle(i, nums)
return False
Leave a comment