LeetCode #6 — Medium

Zigzag
Conversion

A complete visual walkthrough of the zigzag pattern — from brute force simulation to the elegant row-builder approach.

Solve on LeetCode
The Problem

Problem Statement

The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)

P   A   H   N
A P L S I I G
Y   I   R

And then read line by line: "PAHNAPLSIIGYIR"

Write the code that will take a string and make this conversion given a number of rows:

string convert(string s, int numRows);

Example 1:

Input: s = "PAYPALISHIRING", numRows = 3
Output: "PAHNAPLSIIGYIR"

Example 2:

Input: s = "PAYPALISHIRING", numRows = 4
Output: "PINALSIGYAHRPI"
Explanation:
P     I    N
A   L S  I G
Y A   H R
P     I

Example 3:

Input: s = "A", numRows = 1
Output: "A"

Constraints:

  • 1 <= s.length <= 1000
  • s consists of English letters (lower-case and upper-case), ',' and '.'.
  • 1 <= numRows <= 1000

Roadmap

  1. The Brute Force — Simulate the Grid
  2. The Core Insight — Row Builders
  3. Algorithm Walkthrough
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

The Brute Force — Simulate the Grid

The most intuitive approach: build a 2D grid and actually place each character at its zigzag position. Then read the grid row by row, left to right.

Example: "PAYPALISHIRING", numRows = 3

Place characters in a zigzag pattern across 3 rows, then read each row left to right.

P
A
H
A
P
L
S
Y
I
Row 0: P A H N
Row 1: A P L S I I G
Row 2: Y I R
Result: "PAHNAPLSIIGYIR"

Example: "PAYPALISHIRING", numRows = 4

With 4 rows, the zigzag creates a deeper pattern with diagonal characters between the top and bottom.

P
I
N
A
L
S
I
Y
A
H
P
I
Row 0: P I N
Row 1: A L S I G
Row 2: Y A H R
Row 3: P I
Result: "PINALSIGYAHRPI"

This approach works but wastes space. The 2D grid has many empty cells. For a string of length n with numRows rows, the grid can be up to O(n * numRows) in size, even though most cells are empty.

💡

The waste: We allocate an entire 2D grid just to figure out which characters end up in which row. But we never actually need the column positions — we just need the row assignments. Can we skip the grid entirely?

Step 02

The Core Insight — Row Builders

You don't need a 2D array at all. The zigzag pattern is just a bouncing counter: start at row 0, go down to row numRows-1, then bounce back up to row 0, and repeat.

The Trick: Track the Row, Not the Position

Use an array of numRows StringBuilder objects. For each character, append it to the StringBuilder for its current row. Flip direction when you hit the top or bottom row.

curRow bounces
0 → 1 → 2 → 1 → 0 → 1 → 2 …
(for numRows = 3)
goingDown flag
Flips at row 0 and row numRows-1
Controls +1 or -1 movement

This is the key optimization: Instead of a 2D grid with empty cells, we use just numRows strings. Each character goes directly into its row's string. No wasted space, no column tracking, no empty cells. Same O(n) time, but now O(n) space instead of O(n * numRows).

Step 03

Algorithm Walkthrough

Let's trace through "PAYPALISHIRING" with numRows = 3, watching the direction bounce and each row's StringBuilder grow character by character.

Full Trace: Row-by-Row Construction

CHAR ROW DIR ROW 0 ROW 1 ROW 2
P0 P
A1 PA
Y2 PAY
P1 PAPY
A0 PAAPY
L1 PAAPLY
I2 PAAPLYI
S1 PAAPLSYI
H0 PAHAPLSYI
I1 PAHAPLSIYI
R2 PAHAPLSIYIR
I1 PAHAPLSIIYIR
N0 PAHNAPLSIIYIR
G1 PAHNAPLSIIGYIR
Concatenate: PAHN + APLSIIG + YIR = PAHNAPLSIIGYIR
🎯

Notice the bounce: The direction flips whenever curRow hits 0 or numRows-1. This creates the zigzag pattern without ever needing to track columns. Each character lands in exactly the right row.

Step 04

Edge Cases

Several edge cases can short-circuit the algorithm entirely. Handle them upfront for clean code and early returns.

Cases to Handle

numRows == 1
No zigzag possible with a single row. Return the string as-is. The bouncing direction would never change.
numRows >= s.length()
Every character lands in its own row, so reading top-to-bottom produces the original string. Return as-is.
Empty string
Nothing to convert — both conditions above cover this, but it's worth noting explicitly.
numRows == 2
The simplest zigzag: characters alternate between row 0 and row 1. "ABCDEF" becomes "ACE" + "BDF" = "ACEBDF". The algorithm handles this correctly — no special code needed.

numRows = 2 Example

A
C
E
B
D
F
Result: "ACEBDF"
Step 05

Full Annotated Code

class Solution {
    public String convert(String s, int numRows) {

        // Edge cases: no zigzag needed
        if (numRows == 1 || numRows >= s.length())
            return s;

        // Create one StringBuilder per row
        StringBuilder[] rows = new StringBuilder[numRows];
        for (int i = 0; i < numRows; i++)
            rows[i] = new StringBuilder();

        int curRow = 0;
        boolean goingDown = false;   // starts false, flips to true on row 0

        for (char c : s.toCharArray()) {
            rows[curRow].append(c);      // place char in current row

            // Bounce: flip direction at top and bottom
            if (curRow == 0 || curRow == numRows - 1)
                goingDown = !goingDown;

            // Move to next row
            curRow += goingDown ? 1 : -1;
        }

        // Concatenate all rows
        StringBuilder result = new StringBuilder();
        for (StringBuilder row : rows)
            result.append(row);

        return result.toString();
    }
}
📝

Why does goingDown start as false? Because the first character is at row 0 (a boundary). The if check immediately flips it to true, so the second character goes to row 1. This avoids an off-by-one error on the first iteration.

Step 06

Interactive Demo

Enter a string and number of rows, then step through the algorithm watching the zigzag build character by character.

⚙ Zigzag Visualizer



Step 07

Complexity Analysis

Time
O(n)
Space
O(n)

We iterate through the string exactly once, and each character is appended to its row's StringBuilder in O(1) amortized time. The final concatenation traverses all n characters once more, giving O(n) total time. The total space across all StringBuilder objects is exactly n characters (every character appears in exactly one row), plus O(numRows) overhead for the array of builders.

Approach Breakdown

2D GRID
O(n) time
O(n * numRows) space

Each character is placed into a 2D matrix at its zigzag row and column, then the matrix is read row by row. The grid has numRows rows and up to n columns, but most cells are empty -- wasting O(n * numRows) memory for only n filled positions.

ROW BUILDERS
O(n) time
O(n) space

A single pass appends each character to its row's StringBuilder using a bouncing row counter. The direction flips at row 0 and row numRows-1, so each append is O(1) amortized. Total storage across all builders is exactly n characters -- no wasted empty cells.

🎯

The zigzag pattern repeats every 2 * (numRows - 1) characters, which means a formula-based approach can read characters directly by index without the bouncing loop. Both approaches are O(n) time, but the row builder avoids the wasted empty cells of a 2D grid. Since every character must be read and written at least once, O(n) time and O(n) space are both tight lower bounds.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Off-by-one on range boundaries

Wrong move: Loop endpoints miss first/last candidate.

Usually fails on: Fails on minimal arrays and exact-boundary answers.

Fix: Re-derive loops from inclusive/exclusive ranges before coding.