18.5 Flatten Nested List Iterator
Source:
src/main/kotlin/stack/FlattenNestedListIterator.ktPattern: stack of nested lists · Core page
The Problem
Design an iterator that flattens a nested list of integers lazily: NestedInteger is either an Int or a List<NestedInteger>. next() must return the next integer, hasNext() report whether one remains.
- Constraints: the nesting can be arbitrarily deep; total elements ≤ $5 \times 10^4$.
Examples
Input: [[1,1],2,[1,1]] -> next(): 1,1,2,1,1
Input: [1,[4,[6]]] -> next(): 1,4,6
Intuition — a stack of not-yet-flattened sublists; expand on demand
The lazy flattening keeps a stack of NestedIntegers, with hasNext() doing the work: as long as the top is a list, pop it and push its elements (in reverse, so the first is on top). When the top is an integer, we’re ready. next() pops that integer.
init: push all items in reverse order (so item[0] is on top)
hasNext():
while stack not empty and top is a list:
pop the list; push its elements in reverse
return stack not empty # the top is now an integer (or stack is empty)
next(): return stack.pop() as Int # hasNext guaranteed the top is an integer
Why does pushing in reverse work? A stack pops top-first; pushing [a,b,c] as c,b,a makes a come out first — preserving the original order. The “reverse the sublist, then push” step is the standard stack-as-queue trick.
Why is hasNext doing the flattening (not the constructor)? Lazy evaluation: a huge nested structure is not materialized until the iterator is actually consumed. hasNext expands only the top of the stack, one level at a time — the amortized cost per element is O(1) because each nested item is pushed and popped once.
Why not flatten eagerly? Eager flattening (recursively collect all ints into a list) is simpler but O(total) upfront memory and breaks laziness — for streaming or huge inputs, the stack version wins. The repo’s array.dfs.NestedInteger is the input type; the iterator wraps it.
Approach 1 — Eager flatten with recursion
Collect all integers into a list in the constructor, then index through: correct, but O(n) upfront and not lazy.
Approach 2 — Stack with on-demand expansion (the repo’s version, optimal)
class NestedIterator(nestedList: List<NestedInteger>) {
private val stack = ArrayDeque<NestedInteger>()
init {
// Initialize the stack with the nested list, reversed so we can pop from the top
for (item in nestedList.reversed()) {
stack.addLast(item)
}
}
/** @return the next integer (hasNext guarantees the top is an integer) */
fun next(): Int {
return stack.removeLast().getInteger()!!
}
/** @return true iff another integer remains; expands nested lists on demand */
fun hasNext(): Boolean {
// Make sure the top of the stack is an integer; if not, pop the nested list
while (stack.isNotEmpty() && !stack.last().isInteger()) {
val current = stack.removeLast()
val nestedList = current.getList()
// Push the elements of the nested list in reverse order
for (item in nestedList!!.reversed()) {
stack.addLast(item)
}
}
return stack.isNotEmpty() && stack.last().isInteger()
}
}
import java.util.*;
public class NestedIterator implements Iterator<Integer> {
private final Deque<NestedInteger> stack = new ArrayDeque<>();
/** @param nestedList the nested list to flatten */
public NestedIterator(List<NestedInteger> nestedList) {
for (int i = nestedList.size() - 1; i >= 0; i--) stack.push(nestedList.get(i));
}
/** @return the next integer (hasNext guarantees the top is an integer) */
@Override
public Integer next() {
return stack.pop().getInteger();
}
/** @return true iff another integer remains; expands nested lists on demand */
@Override
public boolean hasNext() {
while (!stack.isEmpty() && !stack.peek().isInteger()) {
List<NestedInteger> list = stack.pop().getList();
for (int i = list.size() - 1; i >= 0; i--) stack.push(list.get(i));
}
return !stack.isEmpty();
}
}
#include <vector>
// NestedInteger interface (problem-defined):
// bool isInteger(); int getInteger(); vector<NestedInteger>& getList();
class NestedIterator {
std::vector<NestedInteger> stack;
void pushReverse(const std::vector<NestedInteger>& list) {
for (auto it = list.rbegin(); it != list.rend(); ++it) stack.push_back(*it);
}
public:
/** @param nestedList the nested list to flatten */
NestedIterator(std::vector<NestedInteger>& nestedList) { pushReverse(nestedList); }
/** @return the next integer (hasNext guarantees the top is an integer) */
int next() {
int v = stack.back().getInteger();
stack.pop_back();
return v;
}
/** @return true iff another integer remains; expands nested lists on demand */
bool hasNext() {
while (!stack.empty() && !stack.back().isInteger()) {
auto list = stack.back().getList();
stack.pop_back();
pushReverse(list);
}
return !stack.empty();
}
};
class NestedIterator:
"""@param nested_list: the nested list to flatten"""
def __init__(self, nested_list):
self.stack = list(reversed(nested_list)) # reversed so the first item is on top
def next(self) -> int:
"""@return: the next integer (hasNext guarantees the top is an integer)"""
return self.stack.pop().get_integer()
def has_next(self) -> bool:
"""@return: true iff another integer remains; expands nested lists on demand"""
while self.stack and not self.stack[-1].is_integer():
nested = self.stack.pop().get_list()
self.stack.extend(reversed(nested)) # push in reverse to preserve order
return bool(self.stack)
#![allow(unused)]
fn main() {
// Illustrative: the NestedInteger enum is problem-defined; the iterator's
// stack-with-reverse-push logic is identical to the Kotlin/Java/C++ blocks.
impl Solution {
// (No standalone type; see the Kotlin/Java/C++ implementations for the pattern.)
}
}
Dry run
Input: [[1,1],2,[1,1]] — items: L[1,1], 2, L[1,1].
init: stack = [L[1,1], 2, L[1,1]] (top = L[1,1])
hasNext(): top is a list -> pop L[1,1], push 1,1 (reversed). stack = [1, 1, 2, L[1,1]].
top is integer -> true. next() -> 1. stack = [1, 2, L[1,1]]
hasNext(): top is integer -> true. next() -> 1. stack = [2, L[1,1]]
hasNext(): true. next() -> 2. stack = [L[1,1]]
hasNext(): top is a list -> pop, push 1,1. stack = [1, 1]. true. next() -> 1. stack = [1]
hasNext(): true. next() -> 1. stack = []
hasNext(): false ✓
The expansion is strictly on-demand: the second L[1,1] stays unexpanded until the iterator reaches it — nothing is flattened upfront. Each nested item is popped once and pushed once, which is the O(1)-amortized claim.
Complexity
Time. Each item pushed and popped once:
$$ T(n) = O(1) \text{ amortized per operation} $$
Space. The stack (worst case holds all items):
$$ S(n) = O(n) $$
Variants & follow-ups
- Peeking Iterator (18.4) — the one-element-buffer member of the lazy-iterator family.
- Mini Parser / Nested List Weight Sum — the same nesting processed recursively (DFS) instead of lazily.
- Interview follow-up: “Why is
hasNextthe right place to expand?”next()must be O(1) and the API guaranteesnext()is only called afterhasNext()returned true — so the flattening work belongs inhasNext, where it’s amortized over the traversal. Expanding in the constructor would trade laziness (and memory) for nothing.