Solving ZigZag Conversion in JavaScript: Step-by-Step Guide
The ZigZag Conversion problem is a classic algorithmic challenge that frequently appears in coding interviews and competitive programming platforms. It asks you to take a string and a number of rows, then rearrange the characters as if they were written in a zigzag pattern across those rows, reading row by row to produce the output. While the problem sounds deceptively simple, it teaches fundamental concepts about string manipulation, index tracking, and pattern recognition.
What Is the ZigZag Conversion Problem?
Given a string s and an integer numRows, you write the characters of s in a zigzag pattern across numRows rows. Once the entire string is laid out, you read the characters row by row to form the converted string.
For example, with s = "PAYPALISHIRING" and numRows = 3, the layout looks like this:
P A H N
A P L S I I G
Y I R
Reading row by row produces "PAHNAPLSIIGYIR". With numRows = 4, the same string becomes "PINALSIGYAHRPI". The key insight is that characters travel downward until they hit the bottom row, then travel diagonally upward until they hit the top row, and the cycle repeats.
Why It Matters
Beyond being a popular interview question, the ZigZag Conversion problem sharpens several practical developer skills:
- Index arithmetic: You learn to compute positions using modular arithmetic and cycle lengths.
- State machines: Tracking direction (down vs. up) mirrors real-world parsing logic.
- Memory efficiency: Comparing simulation versus formula-based approaches teaches tradeoffs between clarity and performance.
- Edge case handling: Cases like
numRows = 1or strings shorter than the row count force defensive thinking.
Approach 1: Simulating the Zigzag With an Array of Rows
The most intuitive approach is to simulate the writing process. You maintain an array of strings, one per row, and a pointer that moves down and up while appending characters. This mirrors exactly how a human would solve the problem on paper.
function convert(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("");
}
console.log(convert("PAYPALISHIRING", 3)); // "PAHNAPLSIIGYIR"
console.log(convert("PAYPALISHIRING", 4)); // "PINALSIGYAHRPI"
console.log(convert("A", 1)); // "A"
Here, goingDown flips whenever we reach either boundary. The time complexity is O(n) where n is the length of the string, and space complexity is O(n) for storing the rows. This solution is easy to explain in an interview and hard to get wrong.
Approach 2: Cycle-Based Index Calculation
If you want to avoid simulation entirely, you can compute which characters belong to each row using the cycle length. For numRows rows, a full zigzag cycle contains cycleLen = 2 * numRows - 2 characters. For each row, you jump through the string by cycleLen and, for middle rows, also grab the diagonal character between cycles.
function convertByCycle(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 i = 0; i + row < n; i += cycleLen) {
// Vertical character in this cycle
result += s[i + row];
// Diagonal character (only for middle rows)
const diagonal = i + cycleLen - row;
if (row !== 0 && row !== numRows - 1 && diagonal < n) {
result += s[diagonal];
}
}
}
return result;
}
console.log(convertByCycle("PAYPALISHIRING", 3)); // "PAHNAPLSIIGYIR"
This approach also runs in O(n) time but uses O(1) extra space (excluding the output string). It is more elegant for environments where memory is constrained, though the index math requires careful reasoning.
How to Use It in Practice
While you rarely need to zigzag-encode strings in production, the pattern appears in scenarios like visual text formatting, simple obfuscation schemes, and educational tooling. Here is a small utility module that exposes both strategies and lets callers pick one:
// zigzag.js
export function convert(s, numRows, strategy = "simulate") {
if (numRows <= 1 || s.length <= numRows) return s;
if (strategy === "cycle") {
return convertByCycle(s, numRows);
}
return convertBySimulation(s, numRows);
}
function convertBySimulation(s, numRows) {
const rows = new Array(numRows).fill("");
let row = 0, down = false;
for (const ch of s) {
rows[row] += ch;
if (row === 0 || row === numRows - 1) down = !down;
row += down ? 1 : -1;
}
return rows.join("");
}
function convertByCycle(s, numRows) {
const cycleLen = 2 * numRows - 2;
let out = "";
for (let r = 0; r < numRows; r++) {
for (let i = 0; i + r < s.length; i += cycleLen) {
out += s[i + r];
const d = i + cycleLen - r;
if (r !== 0 && r !== numRows - 1 && d < s.length) out += s[d];
}
}
return out;
}
You can then import and use it in any JavaScript project, choosing the cycle strategy when memory matters most and the simulation strategy when readability is the priority.
Best Practices
- Handle edge cases first: Always check
numRows === 1and strings shorter thannumRowsbefore doing any work. These cases should return the original string unchanged. - Prefer simulation for interviews: It is easier to explain, easier to debug, and less prone to off-by-one errors under pressure.
- Use the cycle approach for performance: When processing very large strings or running in constrained environments, the
O(1)auxiliary space of the cycle method is a meaningful win. - Write tests for boundary values: Cover
numRowsof 1, 2, and values larger than the string length, plus empty strings and single-character inputs. - Avoid repeated string concatenation in hot paths: In JavaScript, building strings with
+=inside tight loops can be slow for huge inputs. Consider pushing to an array and callingjoin("")if profiling shows it matters. - Name variables clearly: Use
currentRow,goingDown, andcycleLenrather than single letters so the logic stays self-documenting.
Common Pitfalls
One frequent mistake is forgetting that the diagonal characters only exist for middle rows. In the cycle approach, adding a diagonal character for the first or last row duplicates the vertical character and corrupts the output. Another common bug is allowing currentRow to go out of bounds by flipping direction at the wrong time โ always flip after checking the boundary, or check the boundary before moving.
Developers also sometimes assume numRows is always greater than 1. When numRows = 1, the cycle length formula 2 * numRows - 2 evaluates to 0, causing an infinite loop in the cycle approach. Guarding against this case up front prevents the bug entirely.
Conclusion
The ZigZag Conversion problem is a compact exercise that rewards clear thinking about direction, cycles, and index arithmetic. The simulation approach gives you a readable, interview-friendly solution that mirrors the problem description, while the cycle-based approach offers a memory-efficient alternative rooted in mathematical pattern recognition. By understanding both, practicing the edge cases, and following the best practices above, you will be well equipped to solve this problem confidently in any JavaScript codebase or technical interview.