LeetCode 2757 - Generate Circular Array Values
The problem asks us to create a generator for a circular array, arr, starting from a given index startIndex. A generator is a construct that yields a value each time it is called. The first call to the generator should return the element at startIndex.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
The problem asks us to create a generator for a circular array, arr, starting from a given index startIndex. A generator is a construct that yields a value each time it is called. The first call to the generator should return the element at startIndex. Subsequent calls pass an integer jump which specifies how many positions to move in the array. If jump is positive, the index moves forward by that amount, wrapping around to the start if necessary. If jump is negative, the index moves backward, wrapping to the end if it moves past the beginning.
The input arr is a list of integers of length up to 10,000, which means we must handle moderate-size arrays efficiently. The steps list specifies the jumps and contains up to 100 values, meaning the generator will be called a limited number of times. Each jump can range from -10,000 to 10,000, requiring careful handling of circular indexing. The constraints guarantee a valid starting index within array bounds.
Key edge cases include large positive or negative jumps that exceed the array length, a jump of zero, and arrays of length one where all moves will point to the same element.
Approaches
The brute-force approach would maintain a current index and manually iterate through the array for each jump, adjusting the index by 1 repeatedly and wrapping around when needed. While simple, this approach is inefficient for large jumps, as it could require up to O(|jump|) operations for each gen.next() call.
The optimal approach leverages modulo arithmetic. Since the array is circular, any jump can be reduced modulo the array length. This allows direct computation of the next index in constant time without repeated iteration. The key insight is that for a jump k in an array of length n, the new index is (current_index + k) % n if k is positive, or (current_index + k + n) % n if k is negative to ensure a positive index.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O( | jump | per call) |
| Optimal | O(1) per call | O(1) | Uses modulo arithmetic to compute new index directly, constant time |
Algorithm Walkthrough
- Initialize the generator with the array
arrandstartIndex. Store the current index in a variablecurrent. - On the first call to
next(), yieldarr[current]without changing the index. - On subsequent calls, accept a
jumpvalue. - Compute the new index using modulo arithmetic to handle circular behavior:
current = (current + jump) % len(arr). If the result is negative, addlen(arr)to wrap correctly. - Yield
arr[current]. - Repeat steps 3-5 for every call to
next().
Why it works: Modulo arithmetic ensures the index always stays within [0, len(arr)-1], automatically implementing the circular array behavior. This invariant guarantees that every gen.next() call yields the correct array element after the specified jump.
Python Solution
from typing import Generator
def cycleGenerator(arr: list[int], startIndex: int) -> Generator[int, int, None]:
n = len(arr)
current = startIndex
jump = yield arr[current] # First yield returns startIndex element
while True:
if jump is None:
jump = 0
current = (current + jump) % n
jump = yield arr[current]
This implementation first yields the element at startIndex. Then it enters a loop where it waits for the next jump value. Using modulo arithmetic ensures the index stays within bounds, wrapping forward or backward as needed. The generator continues indefinitely, producing elements according to circular jumps.
Go Solution
package main
type Generator struct {
arr []int
current int
}
func cycleGenerator(arr []int, startIndex int) *Generator {
return &Generator{arr: arr, current: startIndex}
}
func (g *Generator) Next(jump ...int) int {
n := len(g.arr)
move := 0
if len(jump) > 0 {
move = jump[0]
}
g.current = (g.current + move) % n
if g.current < 0 {
g.current += n
}
return g.arr[g.current]
}
In Go, we use a struct to maintain generator state. The Next method takes an optional jump parameter. Modulo arithmetic is used to ensure the index wraps around correctly. A negative index is corrected by adding the array length.
Worked Examples
Example 1: arr = [1,2,3,4,5], steps = [1,2,6], startIndex = 0
| Step | Current Index | Jump | Result |
|---|---|---|---|
| first call | 0 | - | 1 |
| next(1) | 0 -> 1 | 1 | 2 |
| next(2) | 1 -> 3 | 2 | 4 |
| next(6) | 3 -> 4 | 6 | 5 |
Example 2: arr = [10,11,12,13,14,15], steps = [1,4,0,-1,-3], startIndex = 1
| Step | Current Index | Jump | Result |
|---|---|---|---|
| first call | 1 | - | 11 |
| next(1) | 1 -> 2 | 1 | 12 |
| next(4) | 2 -> 0 | 4 | 10 |
| next(0) | 0 -> 0 | 0 | 10 |
| next(-1) | 0 -> 5 | -1 | 15 |
| next(-3) | 5 -> 2 | -3 | 12 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per call | Each next() call computes the index using arithmetic, no loops |
| Space | O(1) | Only a few variables are stored; generator itself uses constant memory |
Modulo arithmetic avoids iterating through elements, so each jump is handled in constant time regardless of jump size.
Test Cases
arr = [1,2,3,4,5]
gen = cycleGenerator(arr, 0)
assert next(gen) == 1 # startIndex
assert gen.send(1) == 2
assert gen.send(2) == 4
assert gen.send(6) == 5
arr = [10,11,12,13,14,15]
gen = cycleGenerator(arr, 1)
assert next(gen) == 11
assert gen.send(1) == 12
assert gen.send(4) == 10
assert gen.send(0) == 10
assert gen.send(-1) == 15
assert gen.send(-3) == 12
arr = [2,4,6,7,8,10]
gen = cycleGenerator(arr, 3)
assert next(gen) == 7
assert gen.send(-4) == 10
assert gen.send(5) == 8
assert gen.send(-3) == 4
assert gen.send(10) == 10
# edge cases
arr = [1]
gen = cycleGenerator(arr, 0)
assert next(gen) == 1
assert gen.send(1000) == 1
assert gen.send(-1000) == 1
| Test | Why |
|---|---|
| simple jumps | verifies basic forward/backward movement |
| jumps exceeding array length | tests modulo arithmetic wraparound |
| jump zero | ensures staying on the same index works |
| single-element array | ensures circular wrap works for length 1 |
Edge Cases
A common edge case occurs when the jump magnitude exceeds the array length. Without modulo arithmetic, a naive implementation could index out of bounds. By using (current + jump) % len(arr), the solution handles these large jumps correctly.
Another edge case is when the jump is negative. Python’s modulo operator handles negative numbers differently than some languages, so we must explicitly add the array length to ensure a positive index. The Go implementation addresses this similarly.
A third edge case is an array of length one. All jumps, positive or negative, should return the same element. The modulo operation ensures that (0 + jump) % 1 == 0, which correctly yields the only element in the array.