permutations
lazy evaluation
algorithms
combinatorics
programming

Generating permutations lazily

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Generating permutations lazily means producing one arrangement at a time instead of materializing the full result set in memory. That distinction matters because permutation counts grow factorially. Even moderate input sizes become impractical if you try to build the whole list up front.

Lazy generation is the right default when you want to iterate, filter, or stop early. It keeps memory use small and lets the caller consume only as many permutations as needed.

Why Eager Generation Breaks Down

For n distinct items there are n! permutations. That grows quickly:

  • '5! = 120'
  • '8! = 40,320'
  • '10! = 3,628,800'

If you eagerly build a list of all permutations, both memory and startup time increase sharply. In many real tasks, such as search or constraint solving, you only need the first few valid candidates. Lazy generation avoids paying for results you never inspect.

Python Already Has a Lazy Iterator

In Python, itertools.permutations already yields permutations lazily:

python
1from itertools import permutations, islice
2
3items = ["A", "B", "C", "D"]
4
5for perm in islice(permutations(items), 5):
6    print(perm)

This does not create all 24 permutations at once. It produces them on demand as the loop asks for them.

That is often the whole answer. If the standard library already matches your needs, use it.

Writing a Custom Lazy Generator

Sometimes you need custom pruning logic, extra metadata, or a different order. In that case, write a generator with yield:

python
1def lazy_permutations(items):
2    items = list(items)
3
4    if len(items) <= 1:
5        yield tuple(items)
6        return
7
8    for i, item in enumerate(items):
9        rest = items[:i] + items[i + 1:]
10        for perm in lazy_permutations(rest):
11            yield (item,) + perm
12
13for perm in lazy_permutations([1, 2, 3]):
14    print(perm)

This is recursive and easy to understand. Each call fixes one leading element and lazily delegates the remainder to smaller subproblems.

Why Laziness Is Useful

The biggest advantage is early termination. You can stop as soon as you find a result that satisfies a condition:

python
1from itertools import permutations
2
3digits = [1, 2, 3, 4]
4
5for perm in permutations(digits):
6    value = int("".join(map(str, perm)))
7    if value % 7 == 0:
8        print("first match:", perm, value)
9        break

An eager approach would generate every permutation before the search even began.

Laziness also works well in pipelines. You can chain filtering, mapping, and search steps without building huge intermediate collections.

Handling Duplicate Elements

If the input contains duplicates, a naive generator returns duplicate permutations. For example, [1, 1, 2] has repeated outputs unless you deduplicate during generation.

One simple approach is to track which values were used at the current recursion depth:

python
1def unique_lazy_permutations(items):
2    items = list(items)
3
4    if len(items) <= 1:
5        yield tuple(items)
6        return
7
8    used = set()
9    for i, item in enumerate(items):
10        if item in used:
11            continue
12        used.add(item)
13
14        rest = items[:i] + items[i + 1:]
15        for perm in unique_lazy_permutations(rest):
16            yield (item,) + perm
17
18for perm in unique_lazy_permutations([1, 1, 2]):
19    print(perm)

This keeps the generation lazy while avoiding repeated work for duplicate values.

Complexity Still Matters

Laziness does not change the total number of permutations. If you iterate through all of them, the work is still factorial. What changes is memory usage and the ability to stop early.

That distinction matters when explaining performance. A lazy permutation generator is not magically fast; it is incremental. That is a major improvement for many workloads, but it does not remove the combinatorial explosion.

Common Pitfalls

The most common mistake is immediately converting the lazy iterator back into a list with list(permutations(items)). That defeats the whole point and brings back the memory cost.

Another mistake is assuming lazy generation reduces time even when you consume every permutation. If you eventually iterate over the full set, laziness mostly helps memory, not asymptotic runtime.

Recursive generators also make duplicate handling easy to overlook. If the input can repeat values, add deduplication logic or you may get repeated permutations.

Finally, be mindful of recursion depth for larger inputs in custom implementations. The standard library version is usually more robust than a handwritten recursive generator.

Summary

  • Lazy generation yields permutations one at a time instead of storing all of them.
  • 'itertools.permutations already provides a lazy iterator in Python.'
  • Custom generators are useful when you need pruning, custom ordering, or duplicate handling.
  • Laziness saves memory and enables early termination, but it does not remove factorial growth.
  • Avoid converting a lazy iterator into a full list unless you truly need every permutation at once.

Course illustration
Course illustration

All Rights Reserved.