Skip to content

Iterator

An iterator is an object that enables traversal through a container, such as a list or a tree, without exposing its underlying representation. It provides a way to access elements sequentially one at a time, without needing to know the container’s internal structure. Instead of directly indexing into the container (e.g., my_list[i]), an iterator abstracts away the details of accessing the next element. This separation of concerns promotes cleaner code, better abstraction, and easier modification of the underlying data structure without affecting the iteration logic.

Why are iterators important?

  • Abstraction: Hides the internal workings of the container, allowing for changes in the container’s implementation without impacting code that uses the iterator.
  • Flexibility: Enables different traversal methods (e.g., forward, backward, random access) depending on the iterator’s design.
  • Efficiency: Can optimize traversal for specific data structures, potentially avoiding unnecessary computations.
  • Composability: Iterators can be chained together or combined to create more complex iteration patterns.
  • Lazy Evaluation: Iterators often support lazy evaluation, meaning elements are generated only when needed, improving performance, especially for large datasets.

Core Concepts and Terminology:

  • Iterator Object: The object that manages the traversal.
  • Container: The data structure being traversed (e.g., list, tree, graph).
  • begin(): Method (or equivalent) to obtain an iterator pointing to the first element.
  • end(): Method (or equivalent) to obtain an iterator signaling the end of traversal.
  • next(): Method (or equivalent) to move the iterator to the next element and return its value.
  • hasNext(): Method (or equivalent) to check if there’s a next element.

2. When to Use Iterators (and When Not To)

Section titled “2. When to Use Iterators (and When Not To)”

Use iterators when:

  • You need to traverse a collection sequentially.
  • You want to abstract away the internal details of the collection.
  • You need to support different traversal strategies.
  • You’re working with potentially large datasets where lazy evaluation is beneficial.
  • You need to easily compose iteration logic.

Don’t use iterators when:

  • Random access to elements is crucial and efficient random access is provided by the container (e.g., using an array’s index).
  • The data structure is extremely simple and direct iteration is straightforward.
  • Performance is critical and the overhead of iterator management outweighs the benefits.

A generic iterator implementation typically involves:

  1. Constructor: Initializes the iterator to a starting position (e.g., beginning of a list).
  2. hasNext(): Checks if there are more elements to be iterated.
  3. next(): Advances the iterator to the next element and returns its value. Handles the end-of-iteration condition.
  4. (Optional) remove(): Removes the element at the current iterator position.
class ListIterator:
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.data):
value = self.data[self.index]
self.index += 1
return value
else:
raise StopIteration
my_list = [1, 2, 3, 4, 5]
my_iterator = ListIterator(my_list)
for item in my_iterator:
print(item)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class MyIterator implements Iterator<Integer> {
private List<Integer> data;
private int index;
public MyIterator(List<Integer> data) {
this.data = data;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < data.size();
}
@Override
public Integer next() {
if (hasNext()) {
return data.get(index++);
} else {
throw new java.util.NoSuchElementException();
}
}
}
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
MyIterator iterator = new MyIterator(list);
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
#include <iostream>
#include <vector>
template <typename T>
class Iterator {
private:
std::vector<T> data;
size_t index;
public:
Iterator(const std::vector<T>& data) : data(data), index(0) {}
bool hasNext() const { return index < data.size(); }
T next() {
if (hasNext()) {
return data[index++];
} else {
throw std::out_of_range("Iterator out of bounds");
}
}
};
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
Iterator<int> it(vec);
while (it.hasNext()) {
std::cout << it.next() << std::endl;
}
return 0;
}

The complexity of iterator operations depends heavily on the underlying data structure.

OperationTime Complexity (Best/Average/Worst)Space Complexity
hasNext()O(1) / O(1) / O(1)O(1)
next()O(1) / O(1) / O(1)O(1)
begin()O(1)O(1)
end()O(1)O(1)

Note: For more complex data structures (like trees), the complexity of next() might be higher (e.g., O(log n) for a balanced binary search tree if the iterator performs in-order traversal).

  • Exception Handling: Always handle the end-of-iteration condition gracefully (e.g., using StopIteration in Python or NoSuchElementException in Java). Failing to do so will result in runtime errors.
  • Const Correctness (C++): When designing iterators in C++, use const correctly to indicate when methods don’t modify the underlying data structure.
  • Custom Iterators: Don’t be afraid to create custom iterators tailored to your specific data structures and traversal needs.
  • Debugging: When debugging iterator-related issues, carefully step through the code to track the iterator’s position and the state of the underlying data structure.
  • Infinite Loops: Ensure your hasNext() method accurately reflects the end of the iteration to avoid infinite loops.

Description: Implement the BSTIterator class that represents an iterator over the in-order traversal of a binary search tree (BST).

High-Level Approach:

A BST iterator typically uses a stack to simulate the recursive in-order traversal. The next() method pops the leftmost node from the stack (the smallest element not yet visited). Then, it pushes the right subtree of the popped node onto the stack to prepare for future iterations. The hasNext() method checks if the stack is empty. This avoids explicit recursion, managing memory efficiently for potentially deep trees. The Python implementation would leverage the stack implicitly using recursion. The Java and C++ implementations would explicitly use a stack data structure.

(Note: Full implementations for BSTIterator are beyond the scope of this cheatsheet, but the high-level strategy is detailed above.)