Skip to content

Commit 38df36e

Browse files
committed
feat(algorithms): circular array loop
1 parent ea97258 commit 38df36e

File tree

7 files changed

+174
-0
lines changed

7 files changed

+174
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Circular Array Loop
2+
3+
We are given a circular array of non-zero integers, nums, where each integer represents the number of steps to be taken
4+
either forward or backward from its current index. Positive values indicate forward movement, while negative values
5+
imply backward movement. When reaching either end of the array, the traversal wraps around to the opposite end.
6+
7+
> A circular array is a type of array where the last element is followed by the first element, forming a loop or circle.
8+
9+
The input array may contain a cycle, which is a sequence of indexes characterized by the following:
10+
11+
The sequence starts and ends at the same index.
12+
The length of the sequence is at least two.
13+
The loop must be in a single direction, forward or backward.
14+
Note: A cycle in the array does not have to originate at the beginning. It may begin from any point in the array.
15+
16+
Your task is to determine if nums has a cycle. Return TRUE if there is a cycle. Otherwise return FALSE.
17+
18+
## Examples
19+
20+
![Example 1](circular_array_loop_example1.png)
21+
![Example 2](./circular_array_loop_example_2.png)
22+
![Example 3](./circular_array_loop_example_3.png)
23+
![Example 4](./circular_array_loop_example_4.png)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import List
2+
3+
4+
def circular_array_loop(nums: List[int]) -> bool:
5+
"""
6+
Checks if there is a cycle in the provided list of non-zero integers
7+
8+
Problem Explanation:
9+
A cycle exists if we can start at an index and follow the direction of the array (either forward or backward) and
10+
return to the starting index without getting stuck in a loop that doesn't include all elements.
11+
12+
The array is circular, meaning that if you reach the end of the array, you wrap around to the beginning, and vice
13+
versa.
14+
15+
Algorithm:
16+
- Direction Check: Determine the direction of movement (forward or backward) based on the sign of the current
17+
element.
18+
- Cycle Detection: Use a slow and fast pointer approach (similar to Floyd's Tortoise and Hare algorithm) to detect
19+
a cycle.
20+
- Cycle Validation: Ensure that the cycle is valid (i.e., it covers all elements and is not a subset of the array).
21+
22+
Time Complexity:
23+
The time complexity is O(n), where n is the length of the array. This is because each element is processed at
24+
most once.
25+
26+
Space Complexity:
27+
The space complexity is O(1) since we are using a constant amount of extra space.
28+
29+
Args:
30+
nums (list): list of non-zero integers
31+
Returns:
32+
bool: True if there is a circular array loop, False otherwise
33+
"""
34+
length = len(nums)
35+
36+
for i in range(length):
37+
# Skip if we've already visited this index
38+
if nums[i] == 0:
39+
continue
40+
41+
# Determine the direction of the cycle based on the sign of the current element
42+
direction = 1 if nums[i] > 0 else -1
43+
44+
# set the slow and the fast pointers to the current index
45+
slow, fast = i, i
46+
47+
while True:
48+
# Move slow pointer
49+
slow = (slow + nums[slow]) % length
50+
51+
if nums[slow] * direction <= 0:
52+
break # Invalid direction
53+
54+
# Move fast pointer twice
55+
56+
# first time
57+
fast = (fast + nums[fast]) % length
58+
if nums[fast] * direction <= 0:
59+
break # Invalid direction
60+
61+
# second time
62+
fast = (fast + nums[fast]) % length
63+
if nums[fast] * direction <= 0:
64+
break # Invalid direction
65+
66+
# If slow and fast meet, a cycle is detected
67+
if slow == fast:
68+
# Check if the cycle length is greater than 1
69+
if slow == (slow + nums[slow]) % length:
70+
break # Cycle length is 1, invalid
71+
return True
72+
73+
# Mark all visited indices in this path to avoid reprocessing
74+
# Note that this modifies the input list of elements and may not be desirable. This however, has a space
75+
# complexity of O(1) as no extra space is allocated. An alternative solution is to use a list to keep track of
76+
# visited indices and avoid reprocessing, this list would be initialized the the length of the input list such
77+
# as visited = [0 for _ in range(length)] or visited = [False for _ in range(length)] or
78+
# visited = [False] * length
79+
# and then that is used to avoid reprocessing like below:
80+
# while nums[slow] * direction > 0 and not visited[slow]:
81+
# visited[slow] = True
82+
# slow = (slow + nums[slow]) % length
83+
slow = i
84+
while nums[slow] * direction > 0:
85+
next_slow = (slow + nums[slow]) % length
86+
nums[slow] = 0
87+
slow = next_slow
88+
89+
return False
58.1 KB
Loading
56 KB
Loading
60.3 KB
Loading
42.7 KB
Loading
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import unittest
2+
from . import circular_array_loop
3+
4+
5+
class CircularArrayLoopTestCase(unittest.TestCase):
6+
def test_1(self):
7+
"""should return True for [3,1,2]"""
8+
nums = [3,1,2]
9+
actual = circular_array_loop(nums)
10+
self.assertTrue(actual)
11+
12+
def test_2(self):
13+
"""should return True for [-2, -1, -3]"""
14+
nums = [-2, -1, -3]
15+
actual = circular_array_loop(nums)
16+
self.assertTrue(actual)
17+
18+
def test_3(self):
19+
"""should return False for [2,1,-1,-2]"""
20+
nums = [2,1,-1,-2]
21+
actual = circular_array_loop(nums)
22+
self.assertFalse(actual)
23+
24+
def test_4(self):
25+
"""should return True for [3,-3,1,1]"""
26+
nums = [3,-3,1,1]
27+
actual = circular_array_loop(nums)
28+
self.assertTrue(actual)
29+
30+
def test_5(self):
31+
"""should return True for [1,3,-2,-4,1]"""
32+
nums = [1,3,-2,-4,1]
33+
actual = circular_array_loop(nums)
34+
self.assertTrue(actual)
35+
36+
def test_6(self):
37+
"""should return True for [2,1,-1,-2]"""
38+
nums = [2,1,-1,-2]
39+
actual = circular_array_loop(nums)
40+
self.assertFalse(actual)
41+
42+
def test_7(self):
43+
"""should return True for [5,4,-2,-1,3]"""
44+
nums = [5,4,-2,-1,3]
45+
actual = circular_array_loop(nums)
46+
self.assertFalse(actual)
47+
48+
def test_8(self):
49+
"""should return True for [1,2,-3,3,4,7,1]"""
50+
nums = [1,2,-3,3,4,7,1]
51+
actual = circular_array_loop(nums)
52+
self.assertTrue(actual)
53+
54+
def test_9(self):
55+
"""should return True for [3,3,1,-1,2]"""
56+
nums = [3,3,1,-1,2]
57+
actual = circular_array_loop(nums)
58+
self.assertTrue(actual)
59+
60+
61+
if __name__ == '__main__':
62+
unittest.main()

0 commit comments

Comments
 (0)