← Back to DevBytes

ZigZag Conversion: Multiple Solutions and Complexity Analysis

Introduction to ZigZag Conversion

The ZigZag Conversion problem is a classic algorithmic challenge that appears frequently in coding interviews and competitive programming. Given a string and a number of rows, the task is to write the characters of the string in a zigzag pattern across those rows, then read the result row by row to produce the converted string.

While the problem itself may seem like a brainteaser, it teaches fundamental concepts about pattern recognition, index arithmetic, and simulation techniques. Mastering it sharpens your ability to translate visual patterns into efficient code — a skill that transfers directly to real-world problems involving matrix traversal, data reshaping, and stream processing.

What Is the ZigZag Pattern?

Imagine writing a string diagonally down and then up across a fixed number of rows, repeating until every character is placed. For example, with the input string "PAYPALISHIRING" and numRows = 3, the characters are arranged like this:

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

Reading row by row produces the output: "PAHNAPLSIIGYIR". With numRows = 4, the same string becomes:

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

The output is "PINALSIGYAHRPI". The key insight is that characters flow downward until they hit the bottom row, then flow upward until they hit the top row, and this cycle repeats.

Why It Matters

Beyond interview preparation, the ZigZag Conversion problem illustrates several important software engineering principles:

How to Use It: Multiple Solutions

Solution 1: Simulation with Row Buffers

The most intuitive approach is to simulate the zigzag writing process. Maintain a list of string builders, one per row, and a pointer that moves up and down as you iterate through the input. Append each character to the appropriate row, then concatenate all rows at the end.

function convertSimulation(s, numRows) {
    if (numRows === 1 || s.length <= numRows) {
        return s;
    }

    const rows = new Array(numRows).fill("");
    let currentRow = 0;
    let goingDown = false;

    for (const char of s) {
        rows[currentRow] += char;

        // Reverse direction at the top or bottom row
        if (currentRow === 0 || currentRow === numRows - 1) {
            goingDown = !goingDown;
        }

        currentRow += goingDown ? 1 : -1;
    }

    return rows.join("");
}

// Example usage
console.log(convertSimulation("PAYPALISHIRING", 3)); // "PAHNAPLSIIGYIR"
console.log(convertSimulation("PAYPALISHIRING", 4)); // "PINALSIGYAHRPI"
console.log(convertSimulation("A", 1));              // "A"

This solution is easy to understand and implement. The direction flag goingDown toggles whenever the pointer reaches either boundary, naturally producing the zigzag motion.

Solution 2: Visit by Row with Index Arithmetic

Instead of simulating the process, you can compute exactly which characters belong to each row. For a given numRows, the zigzag pattern has a cycle length of cycleLen = 2 * numRows - 2. Within each cycle, row i contains the character at position i and, if it is not the first or last row, an additional character at position cycleLen - i.

function convertByRow(s, numRows) {
    if (numRows === 1 || s.length <= numRows) {
        return s;
    }

    const n = s.length;
    const cycleLen = 2 * numRows - 2;
    let result = "";

    for (let row = 0; row < numRows; row++) {
        for (let j = 0; j + row < n; j += cycleLen) {
            // First character in the cycle for this row
            result += s[j + row];

            // Middle rows have a second character per cycle
            const secondIndex = j + cycleLen - row;
            if (row !== 0 && row !== numRows - 1 && secondIndex < n) {
                result += s[secondIndex];
            }
        }
    }

    return result;
}

// Example usage
console.log(convertByRow("PAYPALISHIRING", 3)); // "PAHNAPLSIIGYIR"
console.log(convertByRow("PAYPALISHIRING", 4)); // "PINALSIGYAHRPI"

This approach avoids the overhead of maintaining multiple buffers and a direction flag. It directly computes the indices, making it both elegant and efficient. The outer loop iterates over rows, and the inner loop jumps by cycleLen to visit each cycle.

Solution 3: Python Implementation with List of Characters

For Python developers, using a list of character lists and joining at the end is more efficient than repeated string concatenation, since strings in Python are immutable.

def convert(s: str, numRows: int) -> str:
    if numRows == 1 or len(s) <= numRows:
        return s

    rows = [[] for _ in range(numRows)]
    current_row = 0
    going_down = False

    for char in s:
        rows[current_row].append(char)
        if current_row == 0 or current_row == numRows - 1:
            going_down = not going_down
        current_row += 1 if going_down else -1

    return "".join("".join(row) for row in rows)

# Example usage
print(convert("PAYPALISHIRING", 3))  # PAHNAPLSIIGYIR
print(convert("PAYPALISHIRING", 4))  # PINALSIGYAHRPI
print(convert("A", 1))               # A

Solution 4: Java Implementation with StringBuilder

In Java, StringBuilder is the idiomatic choice for building strings incrementally. The simulation approach translates cleanly:

public class ZigZagConversion {
    public String convert(String s, int numRows) {
        if (numRows == 1 || s.length() <= numRows) {
            return s;
        }

        StringBuilder[] rows = new StringBuilder[numRows];
        for (int i = 0; i < numRows; i++) {
            rows[i] = new StringBuilder();
        }

        int currentRow = 0;
        boolean goingDown = false;

        for (char c : s.toCharArray()) {
            rows[currentRow].append(c);
            if (currentRow == 0 || currentRow == numRows - 1) {
                goingDown = !goingDown;
            }
            currentRow += goingDown ? 1 : -1;
        }

        StringBuilder result = new StringBuilder();
        for (StringBuilder row : rows) {
            result.append(row);
        }
        return result.toString();
    }

    public static void main(String[] args) {
        ZigZagConversion solver = new ZigZagConversion();
        System.out.println(solver.convert("PAYPALISHIRING", 3)); // PAHNAPLSIIGYIR
        System.out.println(solver.convert("PAYPALISHIRING", 4)); // PINALSIGYAHRPI
    }
}

Complexity Analysis

Simulation Approach

Time Complexity: O(n), where n is the length of the input string. Each character is visited exactly once and appended to a row buffer.

Space Complexity: O(n). The row buffers collectively hold all n characters. In languages with immutable strings, the final concatenation may require additional temporary space, but the asymptotic bound remains O(n).

Visit by Row Approach

Time Complexity: O(n). Although there are nested loops, the inner loop advances by cycleLen each iteration, so the total number of iterations across all rows is proportional to n. Each character is visited at most twice (once for the primary index, once for the secondary index in middle rows), but this is still O(n).

Space Complexity: O(1) extra space beyond the output string, since no auxiliary row buffers are needed. If you count the output string itself, the space is O(n).

Comparison Summary

Best Practices

Common Pitfalls

One frequent mistake is forgetting that the upward pass excludes the first and last rows. When moving upward, the top and bottom rows are only visited once per cycle, while middle rows are visited twice. The visit-by-row solution accounts for this with the condition row !== 0 && row !== numRows - 1 before adding the second character.

Another pitfall is off-by-one errors in the cycle index calculation. The second character in a cycle for row i is at index j + cycleLen - i, not j + cycleLen - i - 1. Always verify with a small example before trusting the formula.

Finally, be careful with the direction toggle in the simulation approach. The toggle must happen after appending the character but the check uses the current row. Reversing the order of operations can cause the pointer to move incorrectly and produce garbled output.

Conclusion

The ZigZag Conversion problem is a deceptively simple exercise that rewards both careful simulation and mathematical insight. By exploring multiple solutions — from the intuitive row-buffer simulation to the elegant index-arithmetic approach — you gain a deeper understanding of how visual patterns map to algorithmic logic. Both solutions achieve linear time complexity, but they differ in space usage and readability, giving you the flexibility to choose the right tool for each context. Whether you encounter this problem in an interview or in a real-world data transformation task, the principles of cycle detection, index computation, and efficient string building will serve you well across a wide range of algorithmic challenges.

— Ad —

Google AdSense will appear here after approval

← Back to all articles