341. Flatten Nested List Iterator


Problem Description

The problem presents a data structure that consists of a list where each element can either be a single integer or a nested list of integers. Such nested lists can have multiple levels of inner lists containing integers. The task is to create an iterator that traverses this complex structure sequentially, effectively flattening it, so that all the individual integers get accessed one by one in the order they appear, from left to right and from topmost level to the deepest level without any concern for the nested structure.

This requires designing a class, NestedIterator, that keeps track of the integers in these nested lists, allowing the user to repeatedly call next() to get the next integer and hasNext() to check if more integers are available for retrieval. To check for correctness, the iterator is used to extract all the integers into a flat list res, and if res matches the expected output (i.e., the list with the same integers in the same order, but without any nested structure), then the implementation is correct.

Intuition

The core of the solution lies in using Depth-First Search (DFS) to traverse the nested list structure before we start iterating. The reason for selecting DFS is that it naturally follows the order and depth in which the integers are stored within the nested lists. It can reach the deepest elements first and backtrack to explore other branches, which is perfect for capturing all elements in the required order.

We start by initializing the iterator with the nested list. During the initialization, we perform a DFS to traverse all elements within the nestedList. If the current element is an integer, we append it to the vals list, which is a flat list containing all the nested integers in the correct order. If the element is a nested list, we recursively apply the same DFS process to that list.

Once the DFS is complete, vals will contain all the integers in a flat structure, and we're ready to iterate over them using our next() and hasNext() methods. The next() method returns the next integer by accessing the current index in vals and increments the index for the next call. The hasNext() method simply checks if the current index is less than the length of vals, indicating that there are more elements to be iterated over.

Learn more about Stack, Tree, Depth-First Search and Queue patterns.

Solution Approach

The solution is implemented in Python and revolves around the concept of Depth-First Search (DFS) to traverse and flatten the nested list structure. Depth-First Search is an algorithm that starts at the root (in this case, the first list or integer in the nested list) and explores as far as possible along each branch before backtracking.

Data Structure

  • A list called self.vals is used to store all integers from the nested list in a flattened form after the DFS traversal, and self.cur keeps the current index of the next integer to return.

Algorithm

  1. A nested function dfs(nestedList) is defined within the __init__ method of the NestedIterator class to perform a depth-first traversal of the input nestedList. This is a recursive function that:
    • Checks if the current element is an integer by calling e.isInteger(). If it is, the integer is appended to self.vals.
    • If the element is another list, the function calls itself with e.getList(), continuing the DFS on the nested list.
  2. The __init__ method initializes the self.vals list to store the flattened integers and sets the self.cur index to 0. It then calls the dfs(nestedList) to fill self.vals using the DFS traversal explained above.
  3. The next() method is responsible for returning the next integer in the self.vals list. It stores the value at the current index (self.cur), increments self.cur to point to the next integer, and returns the stored value.
  4. The hasNext() method simply checks if self.cur is less than the length of self.vals, which determines if there are any more integers to iterate through.

Patterns

  • The main pattern used here is the iterator pattern, which provides a way to access the elements of a collection without exposing its underlying representation. The next() and hasNext() methods are classic examples of this pattern and allow users to iterate over the collection one element at a time.

By using recursive DFS, any nesting of lists within lists is handled elegantly, ensuring that integers are discovered in the correct order. Once the DFS is complete, the resulting self.vals becomes a simple flat list that the next() and hasNext() methods can easily navigate.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?

Example Walkthrough

Let's say we have the following nested list as an example to illustrate the solution approach:

1nestedList = [[1, 1], 2, [1, 1]]

Our goal is to flatten nestedList using the NestedIterator class so that we can iterate over all the integers sequentially.

  1. First, let's visualize the depth-first traversal:

    • Start with the first element, which is a nested list [1, 1].
    • The DFS will go into this nested list and append both 1s to self.vals.
    • Backtrack to the next element which is 2, append it to self.vals.
    • Move to the last element, which is another nested list [1, 1], and append both 1s to self.vals.
  2. After initialization and DFS traversal, our self.vals list inside the NestedIterator will look like this:

    1self.vals = [1, 1, 2, 1, 1]

    And the self.cur index used to keep track of the current position will be initialized to 0.

  3. The next() method is called. Since self.cur is 0, the first element in self.vals is returned which is 1, and self.cur is incremented to 1.

  4. The next() method is called again. Now self.cur is 1, the second element in self.vals is returned which is also 1, and self.cur is incremented to 2.

  5. This process continues each time next() is called. When next() is called for the third time, self.cur is at 2, and self.vals[2] which is 2, is returned, with self.cur incremented to 3.

  6. When the hasNext() method is called at any point, it checks if self.cur is less than the length of self.vals. As long as self.cur is less than 5 in this case, hasNext() will return true, indicating that there are more elements to be iterated over.

  7. The process continues until self.cur equals the length of self.vals (in this case, 5), at which point hasNext() will return false, signifying that we have reached the end of our flattened list.

By following this approach, the NestedIterator class effectively flattens the nested structure of the input list and allows for an easy sequential iteration over the integers using the DFS technique.

Solution Implementation

1class NestedIterator:
2    def __init__(self, nestedList: [NestedInteger]):
3        # A depth-first search function to flatten the nested list
4        def flatten(nested_list):
5            for element in nested_list:
6                if element.isInteger():
7                    # If the element is an integer, append it directly to flat_list
8                    self.flat_list.append(element.getInteger())
9                else:
10                    # If the element is a list, recursively call flatten on it
11                    flatten(element.getList())
12
13        self.flat_list = []  # A list to store the flattened elements
14        self.index = 0  # An index to track the current position in flat_list
15        flatten(nestedList)  # Initialize by starting the flattening process
16
17    def next(self) -> int:
18        # Returns the next integer in the flat_list and increments the index
19        value = self.flat_list[self.index]
20        self.index += 1
21        return value
22
23    def hasNext(self) -> bool:
24        # Check if there are more integers to iterate over
25        return self.index < len(self.flat_list)
26
27
28# Your NestedIterator object will be instantiated and called as such:
29# i, v = NestedIterator(nestedList), []
30# while i.hasNext(): v.append(i.next())
31
1import java.util.ArrayList;
2import java.util.Iterator;
3import java.util.List;
4
5public class NestedIterator implements Iterator<Integer> {
6
7    // A list to hold all integers gathered from the nested list.
8    private List<Integer> flattenedList;
9
10    // An iterator to iterate through the flattened list of integers.
11    private Iterator<Integer> flatListIterator;
12
13    /**
14     * Constructor which takes a list of NestedInteger objects and
15     * initializes the iterator after flattening the list.
16     * @param nestedList a list of NestedInteger objects to be flattened.
17     */
18    public NestedIterator(List<NestedInteger> nestedList) {
19        flattenedList = new ArrayList<>();
20        // Flatten the nested list using depth-first search.
21        flattenList(nestedList);
22        // Initialize iterator for the flattened list.
23        flatListIterator = flattenedList.iterator();
24    }
25
26    /**
27     * Returns the next integer in the nested list.
28     * @return the next integer.
29     */
30    @Override
31    public Integer next() {
32        return flatListIterator.next();
33    }
34
35    /**
36     * Determines if there are more integers to return from the nested list.
37     * @return true if there are more integers to return, false otherwise.
38     */
39    @Override
40    public boolean hasNext() {
41        return flatListIterator.hasNext();
42    }
43
44    /**
45     * Helper method to flatten a list of NestedInteger objects using a depth-first search approach.
46     * @param nestedList a list of NestedInteger to be flattened.
47     */
48    private void flattenList(List<NestedInteger> nestedList) {
49        for (NestedInteger element : nestedList) {
50            // Check if the NestedInteger is a single integer.
51            if (element.isInteger()) {
52                // Add integer to flattened list.
53                flattenedList.add(element.getInteger());
54            } else {
55                // If it is a nested list, then recur.
56                flattenList(element.getList());
57            }
58        }
59    }
60}
61
62/**
63 * Examples of how the NestedIterator class could be used:
64 * NestedIterator iterator = new NestedIterator(nestedList);
65 * while (iterator.hasNext()) {
66 *     v[f()] = iterator.next();
67 * }
68 */
69
1class NestedIterator {
2public:
3    // Constructor initializes the iterator with the given nested list
4    NestedIterator(vector<NestedInteger> &nestedList) {
5        flattenList(nestedList);
6    }
7
8    // Returns the next integer in the nested list
9    int next() {
10        return flattenedList[currentIndex++];
11    }
12
13    // Returns true if there are more integers to be iterated over
14    bool hasNext() const {
15        return currentIndex < flattenedList.size();
16    }
17
18private:
19    vector<int> flattenedList; // Flattened list of integers
20    size_t currentIndex = 0;   // Current index in the flattened list
21
22    // Helper function to flatten a nested list into a single list of integers
23    void flattenList(const vector<NestedInteger> &nestedList) {
24        for (const auto &element : nestedList) {
25            if (element.isInteger()) {
26                flattenedList.push_back(element.getInteger());
27            } else {
28                flattenList(element.getList());
29            }
30        }
31    }
32};
33
34// Usage:
35// NestedIterator i(nestedList);
36// while (i.hasNext()) cout << i.next();
37
1/** This is the given interface for NestedInteger with explanations */
2interface NestedInteger {
3    // Constructor may hold a single integer
4    constructor(value?: number): void;
5
6    // Returns true if this NestedInteger holds a single integer
7    isInteger(): boolean;
8
9    // Returns the single integer this NestedInteger holds, or null if it holds a nested list
10    getInteger(): number | null;
11
12    // Sets this NestedInteger to hold a single integer
13    setInteger(value: number): void;
14
15    // Sets this NestedInteger to hold a nested list and adds a nested integer to it
16    add(elem: NestedInteger): void;
17
18    // Returns the nested list this NestedInteger holds, or an empty list if it holds a single integer
19    getList(): NestedInteger[];
20}
21
22// Array to hold the flattened list of integers
23let flatList: number[] = [];
24
25// Index to track the current position in the flat list
26let currentIndex: number = 0;
27
28/**
29 * Constructor that takes a NestedInteger list and flattens it.
30 */
31function nestedIteratorConstructor(nestedList: NestedInteger[]): void {
32    currentIndex = 0;
33    flatList = [];
34    flattenList(nestedList);
35}
36
37/**
38 * Helper function to flatten a nested list.
39 * Recursively traverses the input and stores integers in flatList.
40 */
41function flattenList(nestedList: NestedInteger[]): void {
42    for (const item of nestedList) {
43        if (item.isInteger()) {
44            flatList.push(item.getInteger());
45        } else {
46            flattenList(item.getList());
47        }
48    }
49}
50
51/**
52 * Returns true if the iterator has more elements, false otherwise.
53 */
54function hasNext(): boolean {
55    return currentIndex < flatList.length;
56}
57
58/**
59 * Returns the next element in the iteration and advances the iterator.
60 */
61function next(): number {
62    return flatList[currentIndex++];
63}
64
65// Example usage:
66// nestedIteratorConstructor(nestedList);
67// const result: number[] = [];
68// while (hasNext()) result.push(next());
69

Time and Space Complexity

Time Complexity

The constructor of the NestedIterator class involves a Depth-First Search (DFS) through the entire nested list. The time complexity for this operation is O(N), where N is the total number of integers within the nested structure. This is because we need to visit every nested element and integer once to flatten the structure into the vals list.

The next() method has a time complexity of O(1) for each call, as it simply accesses the next element in the flattened vals list and increments the cur pointer.

The hasNext() method also works in O(1) time as it only checks if the cur pointer is less than the length of the vals list.

Therefore, considering all method calls, the time complexity is O(N) for the entire iteration over the nested structure due to the initial DFS.

Space Complexity

The space complexity for the DFS in the constructor is O(L), where L is the maximum depth of nesting in the input, due to the stack space used by the recursive calls of the dfs() function.

Additionally, the space complexity for storing the flattened list of integers is O(N), where N is the total number of integers.

Thus the overall space complexity of the NestedIterator is O(N + L). In the case where there is no nesting, L would be O(1), making the space complexity purely O(N).

Learn more about how to find time and space complexity quickly using problem constraints.


Fast Track Your Learning with Our Quick Skills Quiz:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings


Got a question? Ask the Monster Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

Coding Interview Strategies

Dive into our free, detailed pattern charts and company guides to understand what each company focuses on.

See Patterns

🪄