top of page

Word Ladder: A Step-by-Step Interview Walkthrough

  • Jun 5
  • 14 min read

Word Ladder is a shortest-path problem wearing a string-manipulation costume, and the entire challenge is seeing through the disguise. The problem talks about transforming words one letter at a time, which sounds like it might call for clever string algorithms or backtracking. But the moment you reframe words as nodes and one-letter transformations as edges, it becomes a textbook shortest-path-in-an-unweighted-graph problem — and that has one canonical answer: breadth-first search. Candidates who attack it as a string problem often reach for DFS or recursive backtracking and end up exploring far more paths than necessary, sometimes finding a path but not the shortest one. Candidates who recognize the graph structure reach for BFS immediately and get the shortest path by construction. The signal here is whether you connect "shortest path, equal-weight steps" to "BFS" automatically.


Shortest-transformation problems show up across many domains. Spell checkers and autocorrect find the minimum edits between words. Bioinformatics tools compute mutation distances between DNA sequences. Network routing finds minimum-hop paths between nodes. Puzzle solvers (Rubik's cube, sliding puzzles) find the fewest moves to a goal state — each state is a node, each move an edge, and BFS finds the optimal solution. Any time you're navigating between configurations where each step has equal cost and you want the fewest steps, this is the pattern.


Problem Statement

Given two words, beginWord and endWord, and a dictionary wordList, return the length of the shortest transformation sequence from beginWord to endWord.

Rules:

  • Only one letter can be changed at a time.

  • Each transformed word must exist in wordList.

  • beginWord does not need to be in wordList.

  • Return 0 if no such transformation exists.


Example: 

beginWord = "hit",

endWord = "cog",

wordList = ["hot","dot","dog","lot","log","cog"] 

result → 5 (the sequence hit → hot → dot → dog → cog has 5 words).


1. Clarify Requirements Before Jumping Into Code

Before writing a single line, we should restate the problem in our own words and ask the kinds of questions that would change our approach.


Input: two words beginWord and endWord, and a list of valid words.

Output: an integer — the length of the shortest transformation sequence (counting both endpoints), or 0 if impossible.


Details to clarify:

  • Does "one letter changed" mean exactly one position differs? Yes — adjacent words in the sequence differ at exactly one character position.

  • Are all words the same length? Yes — otherwise a single-letter substitution couldn't connect them.

  • Does the count include both beginWord and endWord? Yes — hit → hot is a result of length 2, not 1.

  • Must every intermediate word be in the dictionary? Yes — including the endpoints in the sequence (except beginWord, which doesn't need to be in the list but is still the start).

  • What if endWord isn't in the dictionary? Then no valid sequence can end at it. Return 0.

The thing to flag is "shortest." We're not asked whether a transformation exists, or to find any transformation — we need the shortest one. That word is the entire reason BFS, specifically, is the right tool. DFS could find a path but not guarantee it's the shortest. As we'll see, BFS's level-by-level exploration gives us shortest-path for free.


2. Identify the Category of the Question

A few signals jump out:

  • We move between states (words) via discrete steps (single-letter changes).

  • Every step has the same "cost" — one transformation.

  • We want the minimum number of steps between two states.

That combination — discrete states, equal-cost transitions, minimize the number of steps — is the fingerprint of shortest path in an unweighted graph. And shortest path in an unweighted graph has one canonical solution: BFS. BFS explores nodes in order of their distance from the source, so the first time it reaches the target, it's via the shortest path. Same family as any "minimum moves to reach a goal state" problem — sliding puzzles, Rubik's cube, minimum knight moves on a chessboard.


3. Brute Force Solution

Let's think about the naive approach to understand the problem. We could try every possible transformation sequence: from beginWord, try changing each letter to reach any valid word, then recurse from there, tracking the shortest complete sequence we find.

search(word, stepsSoFar):
    if word == endWord: record stepsSoFar
    for each valid one-letter transformation next:
        search(next, stepsSoFar + 1)

This solution is exponential. Each word can transform into many others, those branch further, and the same words get revisited along countless overlapping paths. Worse, a naive DFS like this doesn't even naturally find the shortest path — it finds a path and would have to explore everything to be sure it found the minimum.


The brute force teaches us two things. First, the paths overlap massively, so we need to avoid re-exploring words — a visited set. Second, and more importantly, we need an exploration order that finds the shortest path first, so we can stop as soon as we reach the target. That second requirement is exactly what BFS provides and DFS doesn't.


4. Brainstorm More Solutions

Step 1: Reframe words as a graph

The problem is phrased in terms of words and letter changes, but let's resist solving it at that surface level and ask what's structurally going on.

We have a starting point beginWord, a destination endWord, and a rule for getting from one word to another (change one letter, land on a valid dictionary word). We're asked for the fewest such moves. Stripped of the string-specific details, that's: a set of positions, a set of legal moves between positions, and a request for the fewest moves from start to finish.

That abstract shape should feel familiar — it's navigation. We have states we can be in, transitions between them, and we want the shortest route. Whenever a problem reduces to "states connected by transitions" the natural model is a graph: the states are nodes, and a transition between two states is an edge. So let's try drawing our problem that way and see if it fits.


Each word is a node. When does an edge connect two words? Exactly when one legal move takes us from one to the other — that is, when they differ at a single character position and both are valid dictionary words. There's nothing else; a move is a one-letter change, so an edge is a one-letter difference. The question "fewest transformations from beginWord to endWord" now becomes "fewest edges from the beginWord node to the endWord node" — the shortest path in a graph.


One more property is worth noticing before we pick an algorithm, because it determines which shortest-path algorithm we need. How "expensive" is each edge?

Every transformation is one step — no transformation costs more than any other. So every edge has the same weight. That's an unweighted graph, and unweighted shortest path is the specific case BFS was built for. BFS explores outward in rings of equal distance — all nodes one move away, then all nodes two moves away, and so on — so the first time it reaches endWord, it's via a shortest path, because any shorter route would have been found in an earlier ring. That "first arrival is the shortest arrival" guarantee is exactly what the problem's "shortest" requirement demands.


Contrast this with the other well-known algorithm for exploring a graph, DFS, which plunges deep down one path before backtracking. If it stumbles onto endWord via a long, winding route, it has no idea whether a shorter route exists — it would have to explore all paths and take the minimum, which is the exponential blowup from the brute force.


Step 2: How do we find a word's neighbors?

There's a practical wrinkle, though. The graph's edges aren't handed to us — we have a flat word list, not an adjacency list. So given a word, we need a way to compute which other words it's connected to.


The most literal interpretation of "differs at one position" suggests an obvious method: take our word, compare it against every other word in the dictionary, and keep the ones that differ in exactly one spot. Let's think about what that costs. For a single word, we'd compare against all n dictionary words, and each comparison walks L characters to count the differences — so O(n × L) to find one word's neighbors. But BFS will ask for the neighbors of potentially every word in the dictionary, so across the whole search that's O(n × n × L) = O(n² × L). When the dictionary has thousands of words, that  term hurts. Can we find a word's neighbors without scanning the entire dictionary every time?


What if instead of asking "which existing words differ from me by one letter?" — which forces us to look at every word — let's ask "what are all the words that could possibly differ from me by one letter, and which of those actually exist?" There aren't many. A one-letter-different word is just our word with a single position changed to some other letter. For a word of length L, there are L positions, and 25 other letters we could put at each — so only L × 25 possible neighbors, regardless of how big the dictionary is. We can enumerate every one of them directly.

neighbors(word):
    for each position j in word:
        for each letter c from 'a' to 'z':
            candidate = word with position j replaced by c
            if candidate in dictionary:      # O(1) hash-set lookup
                candidate is a neighbor

The crucial move is that "is this candidate a real word?" becomes an O(1) hash-set lookup instead of a scan. So finding one word's neighbors costs O(L × 26 × L) — we generate L × 26 candidates, each taking O(L) to build — which is O(L²) with the 26 folded in as a constant. That's independent of n. Across the whole BFS, processing all n words is O(n × L²), and since word length L is usually tiny compared to dictionary size n, this crushes the O(n² × L) pair-comparison approach.


Notice the general principle: when "search the whole collection for matches" is expensive, flip it into "generate the small set of possible matches and test each against a fast lookup." We don't enumerate the graph's edges by inspecting every pair — we derive each node's neighbors on demand, cheaply, the moment BFS asks for them.


Step 3: Track distance with the level-by-level BFS pattern

We need to return the length of the shortest sequence, which means tracking how many steps BFS has taken. We can use the standard level-snapshot pattern: at the start of each BFS "level," record how many words are in the queue. Those words are all at the current distance. Process exactly that many, enqueue their unvisited neighbors (which are at the next distance), and increment the step counter.


This is the same queue-size snapshot trick used for level-order tree traversal — snapshot the level size, process exactly that many, and everything added during processing belongs to the next level.

queue = [beginWord], visited = {beginWord}, steps = 1
while queue not empty:
    levelSize = queue.size()
    for i in 0..levelSize - 1:
        word = queue.dequeue()
        if word == endWord: return steps
        for each neighbor of word not in visited:
            mark neighbor visited
            enqueue neighbor
    steps++
return 0

Step 4: Mark visited at enqueue time, not dequeue time

A subtle but critical detail: we mark a word as visited when we add it to the queue, not when we later remove it.

Why does this matter? Suppose two different words at the current level both have the same neighbor X. If we only marked X visited when dequeuing it, both words would see X as unvisited and enqueue it — so X ends up in the queue twice. That's wasted work, and in larger graphs it can balloon. By marking X visited the moment we first enqueue it, the second word sees it's already claimed and skips it. Each word enters the queue at most once.

This is the same principle as marking nodes during traversal in Number of Islands and Clone Graph: claim the node the moment you first see it, not after you've gotten around to processing it.


Step 5: Walk through the example

Let's trace beginWord = "hit", endWord = "cog", dictionary {hot, dot, dog, lot, log, cog}.

  • Level 1 (steps = 1): queue = [hit]. Dequeue hit (not cog). Neighbors of hit in the dictionary: hot (change i→o). Enqueue hot. → queue = [hot]. steps = 2.

  • Level 2 (steps = 2): queue = [hot]. Dequeue hot (not cog). Neighbors: dot (h→d), lot (h→l). Enqueue both. → queue = [dot, lot]. steps = 3.

  • Level 3 (steps = 3): Dequeue dot → neighbors dog (t→g), lot (already visited). Enqueue dog. Dequeue lot → neighbors log (t→g), dot (visited). Enqueue log. → queue = [dog, log]. steps = 4.

  • Level 4 (steps = 4): Dequeue dog → neighbor cog (d→c), log (visited). Enqueue cog. Dequeue log → neighbor cog (already visited — and here's where enqueue-time marking pays off, since cog was just claimed by dog). → queue = [cog]. steps = 5.

  • Level 5 (steps = 5): Dequeue cog → it is endWord. Return 5.

The answer is 5, matching the expected hit → hot → dot → dog → cog. Notice how BFS found it level by level, and the steps counter directly gave us the sequence length.


Step 6: Complexity

Let n be the number of words in the dictionary and L the word length. For each word we process, we generate L × 26 candidate neighbors, and each candidate involves building a string of length LO(L) — plus an O(1) hash-set lookup. So processing one word is O(L² × 26). Across all n words (each processed at most once thanks to the visited set), total time is O(n × L² × 26), often written O(n × L²) since 26 is constant.

Space is O(n) for the dictionary set, the visited set, and the queue — each holds at most all the words.

This is dramatically better than the exponential brute force, and BFS guarantees the shortest path without exploring more than necessary.


Step 7: Can we do better? Searching from both ends

Standard BFS is a solid answer, but let's poke at where it spends its effort and see if there's room to improve. Picture the BFS frontier expanding outward from beginWord in rings: distance 1, distance 2, distance 3. Here's the thing about those rings — they get bigger fast. If each word has, say, b neighbors on average, the ring at distance d contains roughly b^d words. The work to reach distance d is dominated by that last, largest ring. So if beginWord and endWord are k steps apart, we explore on the order of b^k words to find the path.


That exponential growth is the weakness. The deeper we have to search, the more explosively the frontier widens. So let's ask: is there a way to avoid searching all k levels deep from one side?


Instead of one search expanding k levels from beginWord, what if we run two searches at once — one expanding outward from beginWord, one expanding outward from endWord — and stop the moment they touch. If the two frontiers meet in the middle, each only had to travel about k/2 steps. Now compare the work: one-directional explores about b^k words; two searches of depth k/2 explore about 2 × b^(k/2) words. Since b^(k/2) is the square root of b^k, that's a massive reduction for any non-trivial distance. We've turned one deep, wide search into two shallow ones that meet in the middle.


Why does meeting in the middle work at all? Because the graph is undirected — if there's a path from beginWord to endWord, the same edges form a path from endWord back to beginWord. So a search from each end is exploring the same connecting structure from opposite directions. The shortest path is found when a word shows up in both frontiers: that word is reachable in some steps from one end and some steps from the other, and the sum is a complete path.


The catch is implementation complexity. We now maintain two visited sets and two frontiers, and at each step we have to (a) decide which frontier to expand — the standard trick is to always expand the smaller one, since that keeps both frontiers balanced and the total work minimal — and (b) check after each expansion whether the frontier we just grew has collided with the other. Detecting the meeting point and computing the combined distance correctly is fiddly, and an off-by-one in the step count is easy to introduce.


Because of that complexity, I'd lead with standard BFS in an interview — it's correct, clear, and easy to explain. I'd bring up bidirectional BFS as the answer to "how would you scale this to a very large dictionary or very long words?" It shows I understand where the cost in BFS actually lives (the exponentially growing frontier) and that the fix is to attack the depth, not the breadth. That diagnosis — cost grows with search depth, so cut the depth in half by searching from both ends — is the kind of reasoning that signals real understanding beyond reciting the standard algorithm.


5. Discuss Trade-Offs Between Solutions

Approach

Time

Space

When I'd use it

Brute-force DFS over all sequences

Exponential

High

Never — and doesn't even find shortest naturally

BFS with neighbor generation

O(n × L² × 26)

O(n)

My default — finds the shortest path by construction

Bidirectional BFS

O(n × L² × 26), smaller constant

O(n)

Optimization for large dictionaries; search from both ends


6. Pseudocode

dict = set(wordList)
if endWord not in dict: return 0

queue = [beginWord]
visited = {beginWord}
steps = 1

while queue not empty:
    levelSize = queue.size()
    for i in 0..levelSize - 1:
        word = queue.dequeue()
        if word == endWord:
            return steps
        for each position j in word:
            for each letter c from 'a' to 'z':
                candidate = word with position j set to c
                if candidate in dict and candidate not in visited:
                    visited.add(candidate)        # mark at enqueue time
                    queue.enqueue(candidate)
    steps++

return 0

7. Edge Cases

Things to verify before claiming we're done:

  • endWord not in the dictionary → return 0 immediately, before any BFS. ✓

  • beginWord equals endWord → depends on the exact problem variant; in the standard version, if they're equal and endWord is valid, the first dequeue matches and returns steps = 1. Worth clarifying with the interviewer.

  • Empty dictionary → endWord can't be present, so return 0.

  • No valid path exists → BFS exhausts the queue without reaching endWord, returns 0.

  • A word that's one step from many others → the visited set prevents revisiting, keeping the work linear in the dictionary size.

The early endWord not in dict check is an important short-circuit — without it, we'd run a full BFS that can never succeed, wasting time before returning 0.


8. Full Code

import java.util.*;

public class WordLadder {

    public static int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> dict = new HashSet<>(wordList);
        // If endWord isn't reachable as a valid word, 
        // no sequence can end at it
        if (!dict.contains(endWord)) {
            return 0;
        }

        Queue<String> queue = new LinkedList<>();
        queue.add(beginWord);

        Set<String> visited = new HashSet<>();
        visited.add(beginWord);

        int steps = 1;

        while (!queue.isEmpty()) {
            // Snapshot the current level — these words are 
            // all at distance `steps`
            int size = queue.size();

            for (int i = 0; i < size; i++) {
                String word = queue.poll();

                if (word.equals(endWord)) {
                    return steps;
                }

                char[] chars = word.toCharArray();

                // Generate neighbors: change each position 
                // to each of 26 letters
                for (int j = 0; j < chars.length; j++) {
                    char original = chars[j];

                    for (char c = 'a'; c <= 'z'; c++) {
                        if (c == original) continue;

                        chars[j] = c;
                        String next = new String(chars);

                        if (dict.contains(next) && !visited.contains(next)) {
                            // mark at enqueue time, not dequeue
                            visited.add(next);   
                            queue.add(next);
                        }
                    }
                    // restore before moving to next position
                    chars[j] = original;  
                }
            }

            steps++;
        }

        return 0;  // queue exhausted without reaching endWord
    }
}

A couple of implementation details worth noting in the code: we restore chars[j] = original after trying all 26 letters at position j, so each position is varied independently rather than compounding changes. And we generate candidates by substitution rather than comparing every pair of dictionary words, which keeps neighbor-finding efficient for large dictionaries.


9. Test the Code

List<String> words = Arrays.asList("hot", "dot", "dog", "lot", "log", "cog");

System.out.println(ladderLength("hit", "cog", words));  // 5  (hit→hot→dot→dog→cog)
System.out.println(ladderLength("hit", "hot", words));  // 2  (hit→hot)
System.out.println(ladderLength("hit", "xyz", words));  // 0  (xyz not in dictionary)

// endWord missing from dictionary
System.out.println(ladderLength("hit", "cag", words));  // 0

// No path exists even though endWord is present
List<String> isolated = Arrays.asList("abc", "cog");
System.out.println(ladderLength("hit", "cog", isolated));  // 0 (hit can't reach cog)

// Single transformation
List<String> simple = Arrays.asList("hot");
System.out.println(ladderLength("hit", "hot", simple));  // 2

These hit the meaningful cases: a multi-step transformation, a short transformation, an endWord not in the dictionary, and an endWord that's present but unreachable. The "present but unreachable" case is the important one — it verifies that we don't wrongly return a count just because the target exists; there must be an actual path.


10. Key Lessons

  • When a problem asks for the shortest sequence of equal-cost steps between two states, it's a shortest-path-in-an-unweighted-graph problem, and BFS is the answer. The word "shortest" combined with "one step at a time" should trigger BFS automatically.

  • BFS finds the shortest path because it explores in order of distance — the first time it reaches the target, no shorter path could exist. DFS doesn't have this property, which is why it's the wrong choice here even though it would eventually find a path.

  • Graphs are often implicit. The nodes and edges aren't always handed to you as an adjacency list — sometimes you compute neighbors on the fly (here, by trying single-letter substitutions). Recognizing the implicit graph is the key reframing.

  • Mark nodes visited at enqueue time, not dequeue time. If you wait until dequeue, the same node can be added to the queue multiple times by different predecessors at the same level, wasting work. Claim it the moment you first see it.

  • The level-snapshot BFS pattern (record queue.size(), process exactly that many, then increment the counter) is how you track distance or depth in BFS. It's the same pattern used for level-order tree traversal — recognize it and reuse it.


The thing that makes Word Ladder click is the cognitive move of looking at "transform this word into that word, one letter at a time, in the fewest steps" and recognizing a shortest-path problem on an implicit graph. Once you train yourself to translate "states + equal-cost transitions + minimize steps" into "BFS on a graph," an entire category of transformation and puzzle problems collapses into the same template — build the implicit graph, run BFS, count the levels.


Good Luck and Happy Coding!

Recent Posts

See All

Comments


Drop Me a Line, Let Me Know What You Think

Thanks for submitting!

© 2026 by WhiteboardReady

bottom of page