exercism

Exercism - Spiral Matrix

This post shows you how to get Spiral Matrix exercise of Exercism.

Stevinator Stevinator
13 min read
SHARE
exercism dart flutter spiral-matrix

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

Spiral Matrix Exercise

So we need to use the following concepts.

2D Lists (Matrices)

2D lists are lists of lists, creating a matrix structure. They’re perfect for representing grids, boards, or matrices.

void main() {
  // Create 2D list (matrix)
  List<List<int>> matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
  ];
  
  // Access elements
  int value = matrix[0][0]; // 1 (row 0, column 0)
  int value2 = matrix[1][2]; // 6 (row 1, column 2)
  
  // Create empty matrix
  int size = 3;
  List<List<int>> empty = List.generate(size, (_) => List<int>.filled(size, 0));
  print(empty); // [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
}

List generate() Method

The List.generate() method creates a list by calling a function for each index. It’s perfect for creating matrices.

void main() {
  int size = 3;
  
  // Generate matrix with List.generate
  List<List<int>> matrix = List.generate(size, (_) => List<int>.filled(size, 0));
  print(matrix); // [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
  
  // Generate with index
  List<List<int>> numbered = List.generate(size, (i) => List.generate(size, (j) => i * size + j));
  print(numbered); // [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
}

List filled() Constructor

The List.filled() constructor creates a list with a specified length and initial value. It’s perfect for initializing matrix rows.

void main() {
  // Create list filled with zeros
  List<int> row = List<int>.filled(3, 0);
  print(row); // [0, 0, 0]
  
  // Use in matrix generation
  int size = 4;
  List<List<int>> matrix = List.generate(size, (_) => List<int>.filled(size, 0));
  print(matrix); // [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
}

Records (Tuples) for Directions

Records allow you to group multiple values together. They’re perfect for representing direction vectors (row delta, column delta).

void main() {
  // Direction vectors as records
  List<(int, int)> directions = [
    (0, 1),   // Right (row +0, col +1)
    (1, 0),   // Down (row +1, col +0)
    (0, -1),  // Left (row +0, col -1)
    (-1, 0),  // Up (row -1, col +0)
  ];
  
  // Destructure records
  var (dr, dc) = directions[0];
  print(dr); // 0
  print(dc); // 1
  
  // Use in movement
  int row = 0, col = 0;
  var (deltaR, deltaC) = directions[0];
  int newRow = row + deltaR;
  int newCol = col + deltaC;
  print('($newRow, $newCol)'); // (0, 1)
}

Modulo Operator (%)

The modulo operator (%) returns the remainder after division. It’s essential for cycling through directions in a circular pattern.

void main() {
  int dir = 0;
  int numDirections = 4;
  
  // Cycle through directions
  dir = (dir + 1) % numDirections;
  print(dir); // 1
  
  dir = (dir + 1) % numDirections;
  print(dir); // 2
  
  dir = (dir + 1) % numDirections;
  print(dir); // 3
  
  dir = (dir + 1) % numDirections;
  print(dir); // 0 (wraps around)
  
  // Use in spiral: when hit boundary, change direction
  if (hitBoundary) {
    dir = (dir + 1) % 4; // Next direction
  }
}

Bounds Checking

Bounds checking ensures that array indices are within valid ranges. It’s essential for preventing out-of-bounds errors when traversing matrices.

void main() {
  int size = 3;
  int row = 2;
  int col = 3;
  
  // Check bounds
  bool validRow = row >= 0 && row < size;
  bool validCol = col >= 0 && col < size;
  bool inBounds = validRow && validCol;
  
  // Combined check
  bool inBounds2 = row >= 0 && row < size && col >= 0 && col < size;
  
  // Use in spiral
  int newRow = row + 1;
  int newCol = col + 1;
  if (newRow < 0 || newRow >= size || newCol < 0 || newCol >= size) {
    // Out of bounds - change direction
  }
}

Matrix Element Access

Matrix elements are accessed using two indices: matrix[row][column]. The first index is the row, the second is the column.

void main() {
  List<List<int>> matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
  ];
  
  // Access element
  int value = matrix[1][2]; // 6 (row 1, column 2)
  
  // Set element
  matrix[0][0] = 10;
  print(matrix[0][0]); // 10
  
  // Check if element is set (not zero/default)
  if (matrix[1][1] != 0) {
    print('Already filled');
  }
}

Variable Declaration and Assignment

Variables can be declared and assigned in a single statement. Multiple variables can be declared together.

void main() {
  // Single variable
  int row = 0;
  int col = 0;
  int dir = 0;
  
  // Multiple variables in one line
  int r = 0, c = 0, d = 0;
  
  // Update variables
  r = 1;
  c = 2;
  d = (d + 1) % 4;
}

For Loops

For loops allow you to iterate a specific number of times. They’re perfect for filling a matrix with sequential numbers.

void main() {
  int total = 9; // 3x3 matrix = 9 elements
  
  // Fill matrix with numbers 1 to 9
  for (int n = 1; n <= total; n++) {
    print(n); // 1, 2, 3, ..., 9
  }
  
  // Use in spiral
  int size = 3;
  for (int n = 1; n <= size * size; n++) {
    // Fill matrix with n
  }
}

Conditional Logic (if-else)

Conditional statements allow you to execute different code based on conditions. They’re essential for checking bounds and filled cells.

void main() {
  int newRow = 2;
  int newCol = 3;
  int size = 3;
  List<List<int>> matrix = List.generate(size, (_) => List<int>.filled(size, 0));
  
  // Check if out of bounds or already filled
  if (newRow < 0 || newRow >= size || 
      newCol < 0 || newCol >= size || 
      matrix[newRow][newCol] != 0) {
    // Change direction
    dir = (dir + 1) % 4;
  } else {
    // Move to new position
    row = newRow;
    col = newCol;
  }
}

Spiral Traversal Algorithm

A spiral traversal algorithm moves through a matrix in a circular pattern, changing direction when hitting boundaries or already-filled cells.

void main() {
  // Algorithm steps:
  // 1. Start at (0, 0) with direction right
  // 2. Fill current cell with next number
  // 3. Calculate next position
  // 4. If next position is invalid (out of bounds or filled):
  //    - Change direction
  //    - Recalculate next position
  // 5. Move to next position
  // 6. Repeat until all cells filled
  
  int size = 3;
  List<List<int>> matrix = List.generate(size, (_) => List<int>.filled(size, 0));
  List<(int, int)> directions = [(0, 1), (1, 0), (0, -1), (-1, 0)];
  int row = 0, col = 0, dir = 0;
  
  for (int n = 1; n <= size * size; n++) {
    matrix[row][col] = n;
    
    // Calculate next position
    var (dr, dc) = directions[dir];
    int nextRow = row + dr;
    int nextCol = col + dc;
    
    // Check if need to change direction
    if (nextRow < 0 || nextRow >= size || 
        nextCol < 0 || nextCol >= size || 
        matrix[nextRow][nextCol] != 0) {
      dir = (dir + 1) % 4;
      var (newDr, newDc) = directions[dir];
      row += newDr;
      col += newDc;
    } else {
      row = nextRow;
      col = nextCol;
    }
  }
}

Introduction

In a small village near an ancient forest, there was a legend of a hidden treasure buried deep within the woods. Despite numerous attempts, no one had ever succeeded in finding it. This was about to change, however, thanks to a young explorer named Elara. She had discovered an old document containing instructions on how to locate the treasure. Using these instructions, Elara was able to draw a map that revealed the path to the treasure.

To her surprise, the path followed a peculiar clockwise spiral. It was no wonder no one had been able to find the treasure before! With the map in hand, Elara embarks on her journey to uncover the hidden treasure.

Instructions

Your task is to return a square matrix of a given size.

The matrix should be filled with natural numbers, starting from 1 in the top-left corner, increasing in an inward, clockwise spiral order, like these examples:

Examples

Spiral matrix of size 3:

1 2 3
8 9 4
7 6 5

Spiral matrix of size 4:

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

How do we create a spiral matrix?

To create a spiral matrix:

  1. Initialize matrix: Create a size×size matrix filled with zeros
  2. Set starting position: Start at (0, 0) with direction right
  3. Define directions: Four direction vectors: right (0,1), down (1,0), left (0,-1), up (-1,0)
  4. Fill matrix: For each number from 1 to size²:
    • Fill current cell with the number
    • Calculate next position using current direction
    • If next position is invalid (out of bounds or already filled):
      • Change direction (cycle to next)
      • Recalculate next position with new direction
    • Move to next position
  5. Return matrix: Return the filled matrix

The key insight is using direction vectors to move through the matrix, changing direction when hitting boundaries or already-filled cells. The directions cycle: right → down → left → up → right…

For example, size 3:

  • Start at (0,0), direction right → fill 1, move to (0,1)
  • Fill 2, move to (0,2)
  • Fill 3, move to (1,2) [hit right boundary, change to down]
  • Fill 4, move to (2,2)
  • Fill 5, move to (2,1) [hit bottom, change to left]
  • Continue until all 9 cells filled

Solution

class SpiralMatrix {
  final int size;
  SpiralMatrix(this.size);

  List<List<int>> toList() {
    if (size == 0) return [];

    final m = List.generate(size, (_) => List<int>.filled(size, 0));
    final d = [(0, 1), (1, 0), (0, -1), (-1, 0)];
    var r = 0, c = 0, dir = 0;
    
    for (var n = 1; n <= size * size; n++) {
      m[r][c] = n;
      final (dr, dc) = d[dir];
      final nr = r + dr;
      final nc = c + dc;
      
      if (nr < 0 || nr >= size || nc < 0 || nc >= size || m[nr][nc] != 0) {
        dir = (dir + 1) % 4;
        final (newDr, newDc) = d[dir];
        r += newDr;
        c += newDc;
      } else {
        r = nr;
        c = nc;
      }
    }
    return m;
  }
}

Let’s break down the solution:

  1. class SpiralMatrix - Main class:

    • Encapsulates the spiral matrix generation
    • Stores the size of the matrix
  2. final int size - Matrix size:

    • Size of the square matrix (size × size)
    • Determines how many numbers to fill (size²)
  3. SpiralMatrix(this.size) - Constructor:

    • Initializes the size field
    • Simple constructor
  4. List<List<int>> toList() - Generate matrix:

    • Returns the spiral matrix as a 2D list
    • Main method that creates the spiral
  5. if (size == 0) return [] - Edge case:

    • Returns empty list for size 0
    • Prevents errors with empty matrix
  6. final m = List.generate(size, (_) => List<int>.filled(size, 0)) - Initialize matrix:

    • Creates size×size matrix filled with zeros
    • List.generate(size, ...) creates size rows
    • Each row is List<int>.filled(size, 0) (size zeros)
    • Example: size=3 → [[0,0,0], [0,0,0], [0,0,0]]
  7. final d = [(0, 1), (1, 0), (0, -1), (-1, 0)] - Direction vectors:

    • List of direction records (row delta, column delta)
    • (0, 1): Right (no row change, +1 column)
    • (1, 0): Down (+1 row, no column change)
    • (0, -1): Left (no row change, -1 column)
    • (-1, 0): Up (-1 row, no column change)
  8. var r = 0, c = 0, dir = 0 - Current state:

    • r: Current row position
    • c: Current column position
    • dir: Current direction index (0=right, 1=down, 2=left, 3=up)
    • All start at 0
  9. for (var n = 1; n <= size * size; n++) - Fill loop:

    • Iterates from 1 to size² (total number of cells)
    • Each iteration fills one cell with number n
  10. m[r][c] = n - Fill current cell:

    • Sets matrix element at current position to n
    • Example: first iteration sets m[0][0] = 1
  11. final (dr, dc) = d[dir] - Get direction vector:

    • Destructures current direction record
    • dr: Row delta (change in row)
    • dc: Column delta (change in column)
    • Example: dir=0 → (0, 1) → dr=0, dc=1
  12. final nr = r + dr and final nc = c + dc - Calculate next position:

    • Computes where we would move next
    • Example: r=0, c=0, dr=0, dc=1 → nr=0, nc=1
  13. if (nr < 0 || nr >= size || nc < 0 || nc >= size || m[nr][nc] != 0) - Check validity:

    • Checks if next position is invalid:
      • nr < 0: Out of bounds (above top)
      • nr >= size: Out of bounds (below bottom)
      • nc < 0: Out of bounds (left of left edge)
      • nc >= size: Out of bounds (right of right edge)
      • m[nr][nc] != 0: Already filled (not zero)
  14. dir = (dir + 1) % 4 - Change direction:

    • Cycles to next direction
    • Modulo 4 wraps around: 0→1→2→3→0
    • Example: dir=0 (right) → dir=1 (down)
  15. final (newDr, newDc) = d[dir] - Get new direction:

    • Gets direction vector for new direction
    • After changing direction, recalculate deltas
  16. r += newDr and c += newDc - Move with new direction:

    • Updates position using new direction
    • Moves one step in the new direction
    • Example: r=0, c=2, newDr=1, newDc=0 → r=1, c=2
  17. else { r = nr; c = nc; } - Move normally:

    • If next position is valid, move there
    • Updates position to calculated next position
    • Continues in same direction
  18. return m - Return matrix:

    • Returns the filled spiral matrix
    • All cells now contain numbers 1 to size² in spiral order

The solution efficiently creates a spiral matrix by using direction vectors to traverse the matrix, changing direction when hitting boundaries or already-filled cells. The modulo operator cycles through the four directions, creating the spiral pattern naturally.


A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉

Visit My YouTube Channel
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.