Skip to content

Commit c0c14fc

Browse files
committed
feat(datastructures, nested iterator): nested iterator data structure
1 parent 71ffd3a commit c0c14fc

3 files changed

Lines changed: 394 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Flatten Nested List Iterator
2+
3+
You are given a nested list of integers nestedList. Each element is either an integer or a list whose elements may also
4+
be integers or other lists. Implement an iterator to flatten it.
5+
6+
Implement the NestedIterator class:
7+
8+
- NestedIterator(List<NestedInteger> nestedList) Initializes the iterator with the nested list nestedList.
9+
- int next() Returns the next integer in the nested list.
10+
- boolean hasNext() Returns true if there are still some integers in the nested list and false otherwise.
11+
12+
Your code will be tested with the following pseudocode:
13+
14+
> initialize iterator with nestedList
15+
> res = []
16+
> while iterator.hasNext()
17+
append iterator.next() to the end of res
18+
> return res
19+
20+
If `res` matches the expected flattened list, then your code will be judged as correct.
21+
22+
## Examples
23+
24+
Example 1:
25+
26+
```text
27+
Input: nestedList = [[1,1],2,[1,1]]
28+
Output: [1,1,2,1,1]
29+
Explanation: By calling next repeatedly until hasNext returns false, the order of elements returned by next should be:
30+
[1,1,2,1,1].
31+
```
32+
33+
Example 2:
34+
```text
35+
Input: nestedList = [1,[4,[6]]]
36+
Output: [1,4,6]
37+
Explanation: By calling next repeatedly until hasNext returns false, the order of elements returned by next should be:
38+
[1,4,6].
39+
```
40+
41+
## Constraints
42+
43+
- 1 <= nestedList.length <= 500
44+
- The values of the integers in the nested list is in the range [-10^6, 10^6].
45+
46+
## Topics
47+
48+
- Stack
49+
- Tree
50+
- Depth-First Search
51+
- Design
52+
- Queue
53+
- Iterator
54+
55+
## Solution(s)
56+
57+
1. [Depth-First Search Approach](#depth-first-search-approach)
58+
2. [Stack Approach](#stack-approach)
59+
60+
### Depth-first Search Approach
61+
62+
When we look at this nested list structure, we're essentially dealing with a tree where each node can either be a leaf
63+
(integer) or a branch (nested list). The challenge is to visit every integer in the correct order, just like reading a
64+
book - we read each word sequentially, even though the book has a hierarchical structure of chapters, sections, and
65+
paragraphs.
66+
67+
The key insight is recognizing that we need to "flatten" this hierarchical structure into a linear sequence. Think of it
68+
like unpacking nested boxes - we open the main box, and for each item inside, if it's another box, we open that too,
69+
continuing until we find all the actual items (integers).
70+
71+
Since we need to access elements one by one through next() calls, we have two main strategies:
72+
73+
- Lazy evaluation: Only flatten elements when needed during next() or hasNext() calls
74+
- Eager evaluation: Flatten everything upfront during initialization
75+
76+
The eager approach is simpler and more intuitive here. Why? Because:
77+
78+
- We're going to visit all elements anyway (the iterator will traverse the entire structure)
79+
- Pre-flattening makes next() and hasNext() operations trivial - just array indexing
80+
- We avoid the complexity of maintaining state about partially explored nested lists
81+
82+
The DFS pattern naturally fits this problem because it mirrors how we'd manually flatten the list: when we encounter a
83+
nested list, we immediately dive into it, process all its contents, then continue with the next element at the current
84+
level. This is exactly what DFS does - it goes as deep as possible before backtracking.
85+
86+
The recursive nature of DFS perfectly matches the recursive structure of nested lists. Each recursive call handles one
87+
level of nesting, and the base case (finding an integer) is where we actually collect the values into our flattened
88+
result.
89+
90+
#### Complexity Analysis
91+
92+
##### Time Complexity
93+
94+
- Constructor: `O(n)` where n is the total number of integers in the nested list structure. The DFS traversal visits
95+
each element exactly once, whether it's an integer or a nested list. For each integer element, we perform an `O(1)`
96+
append operation.
97+
- next(): `O(1)` - Simply increments the index and returns the element at that position in the flattened list.
98+
- hasNext(): `O(1) `- Just compares the current index with the length of the list.
99+
100+
##### Space Complexity
101+
102+
- `O(n + d)` where n is the total number of integers in the nested structure and d is the maximum depth of nesting.
103+
- `O(n)` for storing all integers in `self.flattened_list` list after flattening.
104+
- `O(d)` for the recursive call stack during DFS traversal, where d represents the maximum nesting depth.
105+
- In the worst case where the structure is deeply nested (like `[[[[...[[1]]...]]]`), the space complexity would be
106+
`O(n)` for both the storage and call stack combined.
107+
108+
Analysis: The approach flattens the entire nested structure upfront during initialization using DFS. This trades
109+
initialization time for constant-time iteration operations. All integers are extracted and stored in a simple list,
110+
making subsequent next() and hasNext() operations very efficient at O(1) each.
111+
112+
---
113+
114+
### Stack Approach
115+
116+
We’ll use a stack to solve this problem. The stack will be used to store the integer and list of integers on the iterator
117+
object. We’ll push all the nested list data in the stack in reverse order in the constructor. The elements are pushed in
118+
reverse order because the iterator is implemented using a stack. In order to process the nested list correctly, the
119+
elements need to be accessed in the order they appear in the original nested list.
120+
121+
Here is how we implement the NestedIterator class methods to solve the above problem:
122+
123+
**Constructor**
124+
125+
1. The constructor initializes an empty stack of NestedInteger objects.
126+
2. The constructor iterates through the input nestedList, starting from the last element to the first element
127+
(reverse order). It pushes each element onto the stack using stack.push().
128+
129+
**`hasNext()` method**
130+
131+
The hasNext() method checks if there is a next integer to return from the stack, iterating through nested lists if
132+
necessary.
133+
134+
1. The top element of the stack is retrieved using stack.peek().
135+
2. The top element is checked using the method top.isInteger():
136+
- If it is an integer, the method returns TRUE because the next element is an integer.
137+
- The top element must be a nested list if it is not an integer. In this case:
138+
- The top element is popped from the stack using stack.pop().
139+
- The list is retrieved using top.getList().
140+
- The elements of this nested list are pushed onto the stack in reverse order using stack.push(). This ensures that
141+
the first element of the nested list will be processed first.
142+
3. If the stack is empty, the method returns FALSE.
143+
144+
**`next()` method**
145+
146+
The next() method returns the next integer from the stack.
147+
1. The method calls hasNext() to check if more integers are available. If hasNext() returns true, the method pops the
148+
top element of the stack using stack.pop(), which will be an integer because hasNext() ensures that any nested lists
149+
are flattened and only integers remain in the stack.
150+
2. If hasNext() returns false, the method returns 0.
151+
152+
#### Complexity Analysis
153+
154+
##### Time Complexity
155+
156+
Assume
157+
- n is the number of elements,
158+
- l is the number of nested lists, and
159+
- d is the maximum nesting depth (maximum number of lists inside each other).
160+
161+
**Constructor**: Since the constructor pushes all of the elements from the nested list into the stack, the total time
162+
will be the size of that list. Therefore, the time complexity will be `O(n+l)`.
163+
164+
**Has Next ()**: This function will be called several times. During all of these calls, the function will iterate over
165+
all the lists exactly once (the while loop) and process every integer exactly once. Thus, a total of `O(n+l)` effort is
166+
spent. The iterator will be progressed for every integer in the nested list. Thus, there will be a total of `O(n)` calls.
167+
Accordingly, the running time for one call to Has Next is `O((n+l)/n)=O(l/n)`.
168+
169+
Next (): This function calls the Has Next function every time. So, its complexity will be the same as that of the Has
170+
Next function, that is, `O(l/n)`.
171+
172+
##### Space Complexity
173+
174+
The space complexity will be `O(n+l)`
175+
176+
In the worst-case scenario, whereby the outermost list contains
177+
n integers or l empty sub-lists, it will cost `O(n+l)` space. Other expensive cases occur when the nesting is very deep.
178+
It’s useful to remember that d≤l (because each layer of nesting requires another list), but we don’t need to consider
179+
this for our case.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from typing import List, Optional
2+
3+
4+
class NestedInteger:
5+
# Constructor initializes a single integer if a value has been passed
6+
# else it initializes an empty list
7+
def __init__(self, integer=None):
8+
if integer:
9+
self.integer = integer
10+
else:
11+
self.n_list = []
12+
self.integer = 0
13+
14+
# If this NestedIntegers holds a single integer rather
15+
# than a nested list, returns TRUE, else, returns FALSE
16+
def is_integer(self):
17+
if self.integer:
18+
return True
19+
return False
20+
21+
# Returns the single integer, if this NestedIntegers holds a single integer
22+
# Returns null if this NestedIntegers holds a nested list
23+
def get_integer(self):
24+
return self.integer
25+
26+
# Sets this NestedIntegers to hold a single integer.
27+
def set_integer(self, value):
28+
self.n_list = None
29+
self.integer = value
30+
31+
# Sets this NestedIntegers to hold a nested list and adds a nested
32+
# integer to it.
33+
def add(self, ni):
34+
if self.integer:
35+
self.n_list = []
36+
self.n_list.append(NestedInteger(self.integer))
37+
self.integer = None
38+
self.n_list.append(ni)
39+
40+
# Returns the nested list, if this NestedIntegers holds a nested list
41+
# Returns null if this NestedIntegers holds a single integer
42+
def get_list(self):
43+
return self.n_list
44+
45+
46+
class NestedIterator:
47+
def __init__(self, nested_list: List[NestedInteger]):
48+
"""
49+
Initialize the iterator with a nested list
50+
Flatten the nested structure into a simple list of integers
51+
Args:
52+
nested_list (List[NestedInteger]): A list of NestedInteger objects that may contain integers or nested lists
53+
"""
54+
55+
def flatten_nested_list(nested: List[NestedInteger]) -> None:
56+
"""
57+
Recursively flatten a nested list structure using depth first search. This is the core of the solution
58+
Args:
59+
nested: List[NestedInteger]: A list of nested integer objects to flatten
60+
"""
61+
for nested_integer in nested:
62+
if nested_integer.is_integer():
63+
# If it is a single integer, add it to the flattened list
64+
self.flattened_list.append(nested_integer.get_integer())
65+
else:
66+
flatten_nested_list(nested_integer.get_list())
67+
68+
# Initialize the flattened list to store all integers
69+
self.flattened_list = []
70+
# Initialize the current index pointer (starts at -1, will be incremented before first access
71+
self.current_index = -1
72+
# Flatten the entire nested list structure
73+
flatten_nested_list(nested_list)
74+
75+
def next(self) -> int:
76+
"""
77+
Return the next integer in the iteration
78+
Returns:
79+
int: The next integer in the iteration
80+
"""
81+
# Move to the next position
82+
self.current_index += 1
83+
# Return the integer at the current position
84+
return self.flattened_list[self.current_index]
85+
86+
def has_next(self) -> bool:
87+
"""
88+
Check if there are more integers to iterate over.
89+
Returns:
90+
bool: True if there are more integers to iterate over
91+
"""
92+
# Check if the next position is within bounds
93+
return self.current_index + 1 < len(self.flattened_list)
94+
95+
96+
class NestedIteratorV2:
97+
"""
98+
This version uses a stack and iterator approach to flatten the list of NestedInteger objects
99+
"""
100+
101+
def __init__(self, nested_list: List[NestedInteger]):
102+
"""
103+
Initialize the iterator with a nested list.
104+
Flatten the nested structure into a simple list of integers
105+
Args:
106+
nested_list (List[NestedInteger]): A list of NestedInteger objects that may contain integers or nested lists
107+
"""
108+
self.nested_list_stack = list(reversed(nested_list))
109+
110+
def next(self) -> Optional[int]:
111+
"""
112+
Return the next integer in the iteration
113+
Returns:
114+
int: The next integer in the iteration
115+
"""
116+
if self.has_next():
117+
return self.nested_list_stack.pop().get_integer()
118+
return None
119+
120+
def has_next(self) -> bool:
121+
"""
122+
Check if there are more integers to iterate over.
123+
Returns:
124+
bool: True if there are more integers to iterate over
125+
"""
126+
# Iterate through the stack while the stack is not empty
127+
while len(self.nested_list_stack) > 0:
128+
# Save the top value of the stack
129+
top = self.nested_list_stack[-1]
130+
# If the top value is an integer, if true, return True. If not continue
131+
if top.is_integer():
132+
return True
133+
134+
# if the top is not an integer, it must be the list of integers. Ppop the list from the stack and save it in
135+
# the top_list
136+
top_list = self.nested_list_stack.pop().get_list()
137+
# Save the length of the top_list in i and iterate in the list
138+
i = len(top_list) - 1
139+
while i >= 0:
140+
# Append the values of the nested list into the stack
141+
self.nested_list_stack.append(top_list[i])
142+
i -= 1
143+
return False
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from datastructures.nested_iterator import (
5+
NestedIterator,
6+
NestedInteger,
7+
NestedIteratorV2,
8+
)
9+
from utils.test_utils import custom_test_name_func
10+
11+
12+
NESTED_ITERATOR_TEST_CASES = [
13+
([1, [2, 3], 4], [1, 2, 3, 4]),
14+
([3, [2, 3, 4], 4, [2, 3]], [3, 2, 3, 4, 4, 2, 3]),
15+
([[2, 3], 3, [2, 3], 4, [2, 3, 4, 5]], [2, 3, 3, 2, 3, 4, 2, 3, 4, 5]),
16+
([1, [3, [4, [5, 6], 7], 8], 9], [1, 3, 4, 5, 6, 7, 8, 9]),
17+
([[2, 3, [2, 3]]], [2, 3, 2, 3]),
18+
]
19+
20+
21+
def create_nested_iterator_structure(input_list):
22+
def parse_input(nested, input_list):
23+
if isinstance(input_list, int):
24+
nested.set_integer(input_list)
25+
else:
26+
for item in input_list:
27+
child = NestedInteger()
28+
nested.add(child)
29+
parse_input(child, item)
30+
31+
nested_structure = NestedInteger()
32+
parse_input(nested_structure, input_list)
33+
return nested_structure
34+
35+
36+
def create_nested_iterator_from_structure(nested_structure):
37+
return NestedIterator(nested_structure.get_list())
38+
39+
40+
def create_nested_iterator_v2_from_structure(nested_structure):
41+
return NestedIteratorV2(nested_structure.get_list())
42+
43+
44+
def flatten_list(nested_iterator_object):
45+
result = []
46+
while nested_iterator_object.has_next():
47+
result.append(nested_iterator_object.next())
48+
return result
49+
50+
51+
class NestedIteratorTestCase(unittest.TestCase):
52+
@parameterized.expand(NESTED_ITERATOR_TEST_CASES, name_func=custom_test_name_func)
53+
def test_nested_iterator_dfs(
54+
self, nested_list: List[List[int]], expected: List[int]
55+
):
56+
nested_integer = create_nested_iterator_structure(nested_list)
57+
nested_iterator = create_nested_iterator_from_structure(nested_integer)
58+
actual = flatten_list(nested_iterator)
59+
self.assertEqual(expected, actual)
60+
61+
@parameterized.expand(NESTED_ITERATOR_TEST_CASES, name_func=custom_test_name_func)
62+
def test_nested_iterator_stack(
63+
self, nested_list: List[List[int]], expected: List[int]
64+
):
65+
nested_integer = create_nested_iterator_structure(nested_list)
66+
nested_iterator = create_nested_iterator_v2_from_structure(nested_integer)
67+
actual = flatten_list(nested_iterator)
68+
self.assertEqual(expected, actual)
69+
70+
71+
if __name__ == "__main__":
72+
unittest.main()

0 commit comments

Comments
 (0)