Spiral Matrix in Cpp: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Mastering the Spiral Matrix Algorithm in C++: A Complete Guide

Generating a spiral matrix in C++ is a classic algorithm that tests your ability to manipulate 2D arrays and manage state. It involves creating a square grid and filling it with numbers starting from 1, spiraling inwards clockwise until the entire matrix is complete.


Have you ever stared at a coding problem that seems simple on the surface but quickly unravels into a mess of confusing boundary checks and off-by-one errors? Many developers, both new and experienced, hit this wall when first encountering the spiral matrix challenge. It's a puzzle that looks like a straightforward loop but demands a surprisingly elegant and precise solution.

This isn't just about filling a grid with numbers; it's about mastering control flow, state management, and algorithmic thinking in C++. This guide will transform that confusion into confidence. We'll deconstruct the spiral matrix problem from the ground up, providing a crystal-clear logical walkthrough, a detailed C++ code implementation, and insights into why this algorithm is a cornerstone of technical interviews.


What Exactly is a Spiral Matrix?

A spiral matrix is a square 2D array (or matrix) of size N x N where the elements are filled with sequential numbers, starting from 1, in a clockwise, inward-spiraling pattern. The process begins at the top-left corner (position [0][0]), moves right to the top-right corner, then down, then left, then up, and continues this pattern inwards until all N*N cells are filled.

Think of it like drawing a square spiral on a piece of graph paper. You start at an outer corner, trace the perimeter, and just before you complete the square, you turn inwards to trace the perimeter of the next, smaller square inside.

For example, a spiral matrix of size 3 looks like this:


1 2 3
8 9 4
7 6 5

And a spiral matrix of size 4 demonstrates the pattern on a larger scale:


 1  2  3  4
12 13 14  5
11 16 15  6
10  9  8  7

The core challenge lies in programming the logic that correctly "turns the corner" and shrinks the boundaries of the drawing area at the right moment.


Why is This Algorithm a Must-Know for Developers?

The spiral matrix problem is far more than an academic exercise. It's a frequent guest in technical interviews for software engineering roles at major tech companies. But why is it so popular?

  • Tests Core Algorithmic Skills: It effectively assesses a candidate's ability to translate a visual pattern into concrete logic. It requires breaking down a complex problem into manageable steps.
  • Demonstrates State Management: A successful solution requires carefully tracking the current position (row, column), the current direction of movement (right, down, left, up), and the boundaries of the current layer of the spiral.
  • Highlights 2D Array Manipulation: It's a perfect test of your comfort level with navigating and modifying two-dimensional data structures, a common task in many software domains.
  • Reveals Attention to Detail: The logic is ripe for off-by-one errors. A clean, correct implementation shows that you are a careful and precise programmer who considers edge cases (like a 1x1 or 0x0 matrix).

Mastering this algorithm signals that you have a solid foundation in problem-solving, which is transferable to countless real-world programming challenges, from image processing to game development pathfinding.


How to Construct a Spiral Matrix: The Algorithmic Blueprint

There are a few ways to approach this problem, but one of the most intuitive and robust methods is to simulate the drawing process directly. This involves moving in one direction until a boundary is hit, then changing direction and shrinking that boundary.

Let's break down the logic into a step-by-step process.

The Four-Pointer Boundary Approach

The core idea is to maintain four variables that represent the boundaries of the layer we are currently filling:

  • top_row: The index of the topmost row of the current layer.
  • bottom_row: The index of the bottommost row of the current layer.
  • left_col: The index of the leftmost column of the current layer.
  • right_col: The index of the rightmost column of the current layer.

We fill the matrix one layer at a time, in four distinct movements per layer:

  1. Move Right: Traverse from left_col to right_col along the top_row. After this, the top row is complete, so we increment top_row to shrink the boundary.
  2. Move Down: Traverse from the new top_row to bottom_row along the right_col. Now the rightmost column is done, so we decrement right_col.
  3. Move Left: Traverse from the new right_col to left_col along the bottom_row. The bottom row is now filled, so we decrement bottom_row.
  4. Move Up: Traverse from the new bottom_row to top_row along the left_col. The leftmost column is complete, so we increment left_col.

We repeat this four-step process in a loop, and with each full iteration, the boundaries shrink inwards. The loop continues as long as top_row <= bottom_row and left_col <= right_col.

Visualization with an ASCII Flow Diagram

Here’s a visual representation of the algorithm's flow for filling one complete outer layer.

    ● Start Layer (e.g., top=0, bottom=N-1, left=0, right=N-1)
    │
    ▼
  ┌───────────────────────────┐
  │ 1. Fill Top Row (L ⟶ R)   │
  │   for col from left to right:
  │     matrix[top][col] = num++
  └────────────┬──────────────┘
               │
               ▼
          ┌ Increment top_row ┐
          └─────────┬─────────┘
                    │
                    ▼
  ┌───────────────────────────┐
  │ 2. Fill Right Col (T ⟶ B) │
  │   for row from top to bottom:
  │     matrix[row][right] = num++
  └────────────┬──────────────┘
               │
               ▼
          ┌ Decrement right_col ┐
          └─────────┬───────────┘
                    │
                    ▼
  ┌───────────────────────────┐
  │ 3. Fill Bottom Row (R ⟶ L)│
  │   for col from right to left:
  │     matrix[bottom][col] = num++
  └────────────┬──────────────┘
               │
               ▼
          ┌ Decrement bottom_row ┐
          └──────────┬───────────┘
                     │
                     ▼
  ┌───────────────────────────┐
  │ 4. Fill Left Col (B ⟶ T)  │
  │   for row from bottom to top:
  │     matrix[row][left] = num++
  └────────────┬──────────────┘
               │
               ▼
          ┌ Increment left_col ┐
          └──────────┬─────────┘
                     │
                     ▼
    ◆ Boundaries still valid? (top <= bottom)
   ╱           ╲
  Yes           No
  │              │
  ▼              ▼
[Loop to Start]  ● End

Deep Dive: C++ Solution Code Walkthrough

Now, let's analyze the elegant C++ solution provided in the kodikra learning path. This implementation uses a slightly different but equally effective simulation method: it tracks the current position and direction, changing direction only when it hits a boundary or an already-filled cell.

The Full C++ Code

This code is taken directly from the exclusive kodikra.com C++ curriculum.


#include <vector>
#include <tuple>

// This namespace is part of the kodikra module structure.
namespace spiral_matrix {

// Anonymous namespace to keep helper types and functions local to this file.
namespace {

// Type alias for coordinates using std::tuple for row and column.
using Coords = std::tuple<int32_t, int32_t>;

// Type alias for direction vectors (delta row, delta col).
using Dir = std::tuple<int32_t, int32_t>;

// Overload the '+' operator to easily add a direction to coordinates.
// [[nodiscard]] suggests the compiler should warn if the return value is ignored.
[[nodiscard]] Coords operator+(Coords const coords, Dir const dir) {
    auto const [row, col] = coords;
    auto const [dr, dc] = dir;
    return {row + dr, col + dc};
}

} // namespace

std::vector<std::vector<uint32_t>> create_spiral(uint32_t const size) {
    if (size == 0) {
        return {};
    }

    // Initialize the matrix with zeros.
    std::vector<std::vector<uint32_t>> matrix(size, std::vector<uint32_t>(size, 0));

    // Directions: 0:Right, 1:Down, 2:Left, 3:Up
    const std::vector<Dir> directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    uint32_t current_dir_idx = 0;

    Coords current_pos = {0, 0};
    uint32_t number_to_place = 1;

    while (number_to_place <= size * size) {
        auto const [row, col] = current_pos;
        matrix[row][col] = number_to_place++;

        Coords next_pos = current_pos + directions[current_dir_idx];
        auto const [next_row, next_col] = next_pos;

        // Check if we need to change direction.
        // Condition: next position is out of bounds OR the next cell is already filled.
        if (next_row < 0 || next_row >= size || next_col < 0 || next_col >= size || matrix[next_row][next_col] != 0) {
            // Change direction (clockwise).
            current_dir_idx = (current_dir_idx + 1) % 4;
            next_pos = current_pos + directions[current_dir_idx];
        }

        current_pos = next_pos;
    }

    return matrix;
}

} // namespace spiral_matrix

Line-by-Line Explanation

1. Headers and Namespaces


#include <vector>
#include <tuple>

namespace spiral_matrix {
namespace {

We include <vector> for std::vector and <tuple> for std::tuple. The code is neatly organized within the spiral_matrix namespace, following the structure of the kodikra module. An anonymous namespace is used to limit the scope of helper utilities like type aliases and operator overloads to this translation unit, preventing potential name clashes.

2. Type Aliases: Coords and Dir


using Coords = std::tuple<int32_t, int32_t>;
using Dir = std::tuple<int32_t, int32_t>;

Using type aliases makes the code much more readable. Coords represents a point (row, col) in the matrix. Dir represents a direction vector (delta_row, delta_col). For example, moving right is (0, 1), and moving down is (1, 0).

3. Operator Overloading for Readability


[[nodiscard]] Coords operator+(Coords const coords, Dir const dir) {
    auto const [row, col] = coords;
    auto const [dr, dc] = dir;
    return {row + dr, col + dc};
}

This is a fantastic C++ feature that enhances clarity. Instead of writing new_row = old_row + delta_row; new_col = old_col + delta_col; everywhere, we can simply write next_pos = current_pos + direction. The structured bindings (auto const [row, col]) make unpacking the tuple elements clean and simple.

4. Function Definition and Initialization


std::vector<std::vector<uint32_t>> create_spiral(uint32_t const size) {
    if (size == 0) {
        return {};
    }
    std::vector<std::vector<uint32_t>> matrix(size, std::vector<uint32_t>(size, 0));

The function create_spiral takes the desired size and returns the completed 2D vector. It first handles the edge case of size = 0. Then, it creates an N x N matrix and initializes all its cells to 0. This is crucial because a value of 0 will signify an unvisited cell.

5. State Management Variables


const std::vector<Dir> directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
uint32_t current_dir_idx = 0;
Coords current_pos = {0, 0};
uint32_t number_to_place = 1;
  • directions: A constant vector storing our four direction vectors in clockwise order: Right, Down, Left, Up.
  • current_dir_idx: An index into the directions vector. It starts at 0 (Right).
  • current_pos: The current (row, col) coordinates, starting at the top-left corner (0, 0).
  • number_to_place: The integer we are about to place in the matrix, starting from 1.

6. The Main Simulation Loop


while (number_to_place <= size * size) {
    // ... loop body ...
}

The loop continues until all size * size cells have been filled. This is a robust condition that ensures the algorithm terminates correctly.

7. Inside the Loop: Fill, Predict, and Turn


auto const [row, col] = current_pos;
matrix[row][col] = number_to_place++;

Coords next_pos = current_pos + directions[current_dir_idx];
auto const [next_row, next_col] = next_pos;

if (next_row < 0 || next_row >= size || next_col < 0 || next_col >= size || matrix[next_row][next_col] != 0) {
    current_dir_idx = (current_dir_idx + 1) % 4;
    next_pos = current_pos + directions[current_dir_idx];
}

current_pos = next_pos;
  1. Fill: The current cell at current_pos is filled with number_to_place, and the number is then incremented.
  2. Predict: We calculate the next_pos by adding the current direction vector to our current position.
  3. Check & Turn: This is the most critical part. The if statement checks for two conditions:
    • Is the predicted next_pos outside the matrix boundaries?
    • OR, is the cell at next_pos already filled (i.e., not equal to 0)?
  4. Execute Turn: If either condition is true, it's time to turn. We update current_dir_idx by taking the next index in our directions array, using the modulo operator % 4 to wrap around from Up (index 3) back to Right (index 0). We then recalculate next_pos using this new direction.
  5. Update: Finally, we update current_pos to the calculated (and validated) next_pos for the next iteration.

This approach is powerful because it doesn't need to manage four separate boundary pointers. It simply reacts to its environment (the matrix boundaries and filled cells) to decide when to turn.


Compiling and Running the Code

To test this solution, you need a main function to call it and print the result. Create a file named main.cpp.


#include <iostream>
#include <vector>
#include <iomanip> // For std::setw

// Assume the spiral_matrix code is in a header or the same file
namespace spiral_matrix {
    // ... paste the create_spiral function and its helpers here ...
}

void print_matrix(const std::vector<std::vector<uint32_t>>& matrix) {
    if (matrix.empty()) {
        std::cout << "Matrix is empty." << std::endl;
        return;
    }
    for (const auto& row : matrix) {
        for (const auto& cell : row) {
            std::cout << std::setw(4) << cell;
        }
        std::cout << std::endl;
    }
}

int main() {
    uint32_t size = 5;
    std::cout << "Generating a spiral matrix of size " << size << "x" << size << ":" << std::endl;
    
    auto my_matrix = spiral_matrix::create_spiral(size);
    print_matrix(my_matrix);

    return 0;
}

Now, open your terminal and compile the code using a modern C++ compiler like g++.


$ g++ -std=c++17 -o spiral_app main.cpp

This command compiles main.cpp using the C++17 standard and creates an executable file named spiral_app. Now, run it:


$ ./spiral_app

You should see the following output, beautifully formatted:


Generating a spiral matrix of size 5x5:
   1   2   3   4   5
  16  17  18  19   6
  15  24  25  20   7
  14  23  22  21   8
  13  12  11  10   9

An Alternative: The Layer-by-Layer Approach

While the direction-simulation approach is very efficient, some find the layer-by-layer logic (as described in our initial blueprint) easier to reason about. Let's implement that for comparison. This approach explicitly fills the four sides of the current outer layer before moving to the next layer inwards.

Layer-by-Layer C++ Implementation


#include <vector>

std::vector<std::vector<uint32_t>> create_spiral_layers(uint32_t size) {
    if (size == 0) return {};

    std::vector<std::vector<uint32_t>> matrix(size, std::vector<uint32_t>(size));
    uint32_t num = 1;
    int32_t top = 0, bottom = size - 1;
    int32_t left = 0, right = size - 1;

    while (top <= bottom && left <= right) {
        // 1. Fill top row (left to right)
        for (int i = left; i <= right; ++i) {
            matrix[top][i] = num++;
        }
        top++;

        // 2. Fill right column (top to bottom)
        for (int i = top; i <= bottom; ++i) {
            matrix[i][right] = num++;
        }
        right--;

        // 3. Fill bottom row (right to left)
        if (top <= bottom) { // Prevent single-row overwrite
            for (int i = right; i >= left; --i) {
                matrix[bottom][i] = num++;
            }
            bottom--;
        }

        // 4. Fill left column (bottom to top)
        if (left <= right) { // Prevent single-column overwrite
            for (int i = bottom; i >= top; --i) {
                matrix[i][left] = num++;
            }
            left++;
        }
    }
    return matrix;
}

Logic Visualization for the Layer Approach

This diagram shows how the boundaries shrink after each complete layer is processed.

    ● Start with Initial Boundaries (T, B, L, R)
    │
    ▼
  ┌─────────────────┐
  │ Fill Outer Layer│
  │ (4-step process)│
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │ Shrink Boundaries:
  │  T++, B--, L++, R--
  └────────┬────────┘
           │
           ▼
    ◆ Inner Layer Exists?
   ╱   (T <= B && L <= R)   ╲
  Yes                       No
  │                          │
  └─────────► [Loop]         ▼
                           ● Done

Pros and Cons of Each Approach

Both methods achieve the same result, but they have different characteristics that might make one preferable over the other depending on the context.

Approach Pros Cons
Direction Simulation - Very concise and elegant loop body.
- State is simple (position, direction).
- Easily adaptable to other pathing problems.
- The logic for changing direction can feel less direct.
- Relies on pre-filling the matrix with a sentinel value (like 0).
Layer-by-Layer - Logic is very explicit and easy to trace on paper.
- Directly corresponds to the visual idea of concentric squares.
- Doesn't require a pre-filled matrix.
- Code is more verbose with four separate loops per layer.
- Requires extra checks (if (top <= bottom)) to handle non-square or odd-sized matrices correctly.

For a typical interview setting, both are valid solutions. The direction simulation method often impresses more due to its compact and clever logic, but a clean, working layer-by-layer implementation is also a strong signal of competence.


Frequently Asked Questions (FAQ)

What is the time and space complexity of the spiral matrix algorithm?

The complexity is straightforward. For an N x N matrix, we must visit and write to each of the N*N cells exactly once. Therefore, the time complexity is O(N²). The space required is for the matrix itself, which is also O(N²). However, if we consider the extra space used by the algorithm (for variables like position, direction, or boundaries), it is constant. Thus, the auxiliary space complexity is O(1).

Can this algorithm be adapted for a non-square (M x N) matrix?

Yes, absolutely. The layer-by-layer approach adapts very naturally. The while (top <= bottom && left <= right) condition handles rectangular matrices perfectly, as one pair of boundaries will meet before the other, gracefully terminating the loop. The direction simulation method also works, as its boundary checks (e.g., next_row >= M or next_col >= N) will correctly trigger turns at the rectangular edges.

How should edge cases like a 1x1 or 0x0 matrix be handled?

A robust implementation must handle these. For a size of 0, the function should return an empty vector, which both of our example solutions do. For a size of 1, the loop should execute once to place the number '1' in the single cell and then terminate correctly. The conditions in both algorithms are set up to handle this without special casing.

What are the most common mistakes when implementing this algorithm?

The most frequent errors are off-by-one mistakes in loop bounds or boundary updates. Forgetting to check if a layer has already been fully traversed in one dimension (the if (top <= bottom) checks in the layer approach) can cause numbers to be overwritten. In the simulation approach, an incorrect direction change condition or a faulty modulo operation can send the spiral off course.

Is recursion a good way to solve the spiral matrix problem?

While it's possible to frame this problem recursively (e.g., a function that fills the outer layer and then calls itself for the inner sub-matrix), it's generally not recommended. An iterative solution is more straightforward, more memory-efficient (avoiding deep recursion stacks), and less prone to stack overflow errors for large matrices. Iteration is the standard and expected approach for this problem.

Why use std::tuple for coordinates instead of a struct or std::pair?

This is largely a style choice in modern C++. A struct Coords { int r, c; }; would be the most descriptive. std::pair is limited to exactly two elements. std::tuple is a generic container for a fixed-size collection of heterogeneous values. Using it here, combined with structured bindings (auto [r, c] = pos;), offers a concise and functional style without the boilerplate of defining a new struct, making it a popular choice in modern C++ codebases.

How can I easily print the matrix to the console for verification?

You can use a simple nested loop. For better alignment, especially with multi-digit numbers, include the <iomanip> header and use std::setw() to set a fixed width for each number. The print_matrix function in our "Compiling and Running" section is a perfect example of how to do this cleanly.


Conclusion: From Confusion to Confidence

The spiral matrix problem is a journey in itself. It starts as a simple visual idea and evolves into a fascinating challenge of logic, state, and boundary management. By breaking it down into methodical steps—either through direct simulation or a layer-by-layer traversal—we can transform this intimidating puzzle into a manageable and even enjoyable exercise.

You've now explored two robust C++ implementations, understood the critical logic behind them, and seen how to compile and test your results. Mastering this algorithm not only prepares you for technical interviews but also sharpens the fundamental programming skills you'll use daily. It's a testament to how elegant code can solve a visually complex problem.

Disclaimer: The C++ code in this article is written to modern standards, specifically C++17. It has been tested with g++ (version 11 or newer) and Clang. Minor modifications may be needed for older compilers.

Ready to tackle the next challenge? Continue your journey on the kodikra Cpp 5 learning path or explore our complete C++ curriculum to build an even stronger foundation.


Published by Kodikra — Your trusted Cpp learning resource.