Episode 1 — Fundamentals / 1.21 — Arrays

1.21.g -- Multidimensional Arrays

In one sentence: A multidimensional array is an array whose elements are themselves arrays -- commonly used for matrices, grids, and tables -- accessed with chained brackets like matrix[row][col] and iterated with nested loops.

Navigation: <- 1.21.f -- Practice Problems . 1.21 Overview


1. What is a multidimensional array?

A multidimensional array is simply an array containing other arrays. The most common form is a 2D array (array of arrays), which models a grid with rows and columns.

// 1D array -- a list
const list = [1, 2, 3, 4, 5];

// 2D array -- a grid (matrix)
const grid = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// 3D array -- a cube (rarely needed)
const cube = [
  [[1, 2], [3, 4]],
  [[5, 6], [7, 8]]
];

Visualization of a 2D array:

          col 0   col 1   col 2
        ┌───────┬───────┬───────┐
row 0   │   1   │   2   │   3   │
        ├───────┼───────┼───────┤
row 1   │   4   │   5   │   6   │
        ├───────┼───────┼───────┤
row 2   │   7   │   8   │   9   │
        └───────┴───────┴───────┘

2. Creating a 2D array (matrix)

Literal syntax

// 3x3 matrix
const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// 2x4 matrix (2 rows, 4 columns)
const table = [
  [10, 20, 30, 40],
  [50, 60, 70, 80]
];

// Matrix with mixed data (spreadsheet-like)
const spreadsheet = [
  ["Name",    "Age", "Score"],
  ["Alice",   25,    95],
  ["Bob",     30,    82],
  ["Carol",   28,    91]
];

Programmatic creation

// Create a 3x4 matrix filled with zeros
const rows = 3;
const cols = 4;

// Correct way: Array.from with a factory function
const matrix = Array.from({ length: rows }, () =>
  new Array(cols).fill(0)
);
console.log(matrix);
// [[0, 0, 0, 0],
//  [0, 0, 0, 0],
//  [0, 0, 0, 0]]

// WRONG way: fill with the same array reference
const bad = new Array(3).fill(new Array(4).fill(0));
bad[0][0] = 99;
console.log(bad);
// [[99, 0, 0, 0],
//  [99, 0, 0, 0],   <-- all rows share the same inner array!
//  [99, 0, 0, 0]]

Why the "wrong way" fails: fill() uses the same reference for every slot. All three rows point to the same inner array, so modifying one modifies all.

Create with nested for loops

function createMatrix(rows, cols, defaultValue = 0) {
  const matrix = [];
  for (let r = 0; r < rows; r++) {
    const row = [];
    for (let c = 0; c < cols; c++) {
      row.push(defaultValue);
    }
    matrix.push(row);
  }
  return matrix;
}

const m = createMatrix(3, 3, 0);
console.log(m);  // [[0,0,0], [0,0,0], [0,0,0]]

Identity matrix

function identityMatrix(n) {
  return Array.from({ length: n }, (_, r) =>
    Array.from({ length: n }, (_, c) => (r === c ? 1 : 0))
  );
}

console.log(identityMatrix(3));
// [[1, 0, 0],
//  [0, 1, 0],
//  [0, 0, 1]]

3. Accessing elements: matrix[row][col]

Access is done by chaining bracket notation: first the row, then the column.

const matrix = [
  [10, 20, 30],
  [40, 50, 60],
  [70, 80, 90]
];

// Access row 0, col 2
console.log(matrix[0][2]);  // 30

// Access row 2, col 0
console.log(matrix[2][0]);  // 70

// Access the center element
console.log(matrix[1][1]);  // 50

// Modify an element
matrix[1][2] = 99;
console.log(matrix[1]);     // [40, 50, 99]

Breaking down matrix[r][c]:

// Step 1: matrix[1] returns the inner array [40, 50, 60]
const row = matrix[1];
console.log(row);          // [40, 50, 60]

// Step 2: row[2] returns the element at column 2
const element = row[2];
console.log(element);      // 60 (or 99 after modification above)

// Combined: matrix[1][2]

Getting dimensions:

const rows = matrix.length;       // 3 (number of rows)
const cols = matrix[0].length;    // 3 (number of columns in first row)

console.log(`Matrix is ${rows}x${cols}`);  // Matrix is 3x3

4. Iterating with nested loops

Standard nested for loop

const matrix = [
  [1,  2,  3,  4],
  [5,  6,  7,  8],
  [9, 10, 11, 12]
];

// Print every element
for (let r = 0; r < matrix.length; r++) {
  for (let c = 0; c < matrix[r].length; c++) {
    process.stdout.write(String(matrix[r][c]).padStart(3));
  }
  console.log();  // newline after each row
}
//   1  2  3  4
//   5  6  7  8
//   9 10 11 12

Using for...of

const grid = [
  ["a", "b", "c"],
  ["d", "e", "f"],
  ["g", "h", "i"]
];

for (const row of grid) {
  for (const cell of row) {
    process.stdout.write(cell + " ");
  }
  console.log();
}
// a b c
// d e f
// g h i

With indices using entries()

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

for (const [r, row] of matrix.entries()) {
  for (const [c, val] of row.entries()) {
    console.log(`matrix[${r}][${c}] = ${val}`);
  }
}
// matrix[0][0] = 1
// matrix[0][1] = 2
// ... etc.

Iterating column-first

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// Column by column (outer loop = columns, inner = rows)
for (let c = 0; c < matrix[0].length; c++) {
  const column = [];
  for (let r = 0; r < matrix.length; r++) {
    column.push(matrix[r][c]);
  }
  console.log(`Column ${c}:`, column);
}
// Column 0: [1, 4, 7]
// Column 1: [2, 5, 8]
// Column 2: [3, 6, 9]

5. 3D arrays (briefly)

A 3D array adds another nesting level -- think of it as multiple layers of 2D grids.

// 2x2x2 cube
const cube = [
  [  // layer 0
    [1, 2],
    [3, 4]
  ],
  [  // layer 1
    [5, 6],
    [7, 8]
  ]
];

// Access: cube[layer][row][col]
console.log(cube[0][1][0]);  // 3 (layer 0, row 1, col 0)
console.log(cube[1][0][1]);  // 6 (layer 1, row 0, col 1)

// Iterate with triple nested loop
for (let l = 0; l < cube.length; l++) {
  for (let r = 0; r < cube[l].length; r++) {
    for (let c = 0; c < cube[l][r].length; c++) {
      console.log(`cube[${l}][${r}][${c}] = ${cube[l][r][c]}`);
    }
  }
}

3D arrays are rare in everyday web development but appear in game engines (voxels), scientific computing, and color image data (height x width x RGB channels).


6. Common operations

Sum all elements in a matrix

function matrixSum(matrix) {
  let total = 0;
  for (const row of matrix) {
    for (const val of row) {
      total += val;
    }
  }
  return total;
}

const m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(matrixSum(m));  // 45

Transpose a matrix (rows become columns)

function transpose(matrix) {
  const rows = matrix.length;
  const cols = matrix[0].length;

  const result = Array.from({ length: cols }, () => []);

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      result[c].push(matrix[r][c]);
    }
  }

  return result;
}

const original = [
  [1, 2, 3],
  [4, 5, 6]
];

console.log(transpose(original));
// [[1, 4],
//  [2, 5],
//  [3, 6]]

Visualized:

Original (2x3):         Transposed (3x2):
┌───┬───┬───┐           ┌───┬───┐
│ 1 │ 2 │ 3 │           │ 1 │ 4 │
├───┼───┼───┤    →      ├───┼───┤
│ 4 │ 5 │ 6 │           │ 2 │ 5 │
└───┴───┴───┘           ├───┼───┤
                        │ 3 │ 6 │
                        └───┴───┘

Matrix addition

function matrixAdd(a, b) {
  const result = [];
  for (let r = 0; r < a.length; r++) {
    const row = [];
    for (let c = 0; c < a[r].length; c++) {
      row.push(a[r][c] + b[r][c]);
    }
    result.push(row);
  }
  return result;
}

const A = [[1, 2], [3, 4]];
const B = [[5, 6], [7, 8]];
console.log(matrixAdd(A, B));
// [[6, 8], [10, 12]]

Search in a matrix

function findInMatrix(matrix, target) {
  for (let r = 0; r < matrix.length; r++) {
    for (let c = 0; c < matrix[r].length; c++) {
      if (matrix[r][c] === target) {
        return { row: r, col: c };
      }
    }
  }
  return null;  // not found
}

const grid = [
  [10, 20, 30],
  [40, 50, 60],
  [70, 80, 90]
];

console.log(findInMatrix(grid, 50));  // { row: 1, col: 1 }
console.log(findInMatrix(grid, 99));  // null

Rotate a matrix 90 degrees clockwise

function rotate90(matrix) {
  const n = matrix.length;
  const result = Array.from({ length: n }, () => new Array(n));

  for (let r = 0; r < n; r++) {
    for (let c = 0; c < n; c++) {
      result[c][n - 1 - r] = matrix[r][c];
    }
  }

  return result;
}

const m = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

console.log(rotate90(m));
// [[7, 4, 1],
//  [8, 5, 2],
//  [9, 6, 3]]

7. Jagged arrays (rows of different lengths)

Unlike matrices (rectangular), jagged arrays have rows of different lengths.

const jagged = [
  [1, 2, 3],
  [4, 5],
  [6, 7, 8, 9],
  [10]
];

console.log(jagged.length);      // 4 (rows)
console.log(jagged[0].length);   // 3
console.log(jagged[1].length);   // 2
console.log(jagged[2].length);   // 4
console.log(jagged[3].length);   // 1

Iterating jagged arrays safely:

for (let r = 0; r < jagged.length; r++) {
  for (let c = 0; c < jagged[r].length; c++) {  // use jagged[r].length, not a fixed col count
    process.stdout.write(String(jagged[r][c]).padStart(3));
  }
  console.log();
}
//   1  2  3
//   4  5
//   6  7  8  9
//  10

Real-world uses of jagged arrays:

  • Student grades (different number of assignments per student)
  • File system (directories with different numbers of files)
  • Social network (friends lists of different lengths)
const studentGrades = [
  { name: "Alice", grades: [95, 82, 91, 88, 97] },
  { name: "Bob",   grades: [78, 85, 72] },
  { name: "Carol", grades: [90, 93, 87, 91] }
];

for (const student of studentGrades) {
  const avg = student.grades.reduce((a, b) => a + b, 0) / student.grades.length;
  console.log(`${student.name}: ${student.grades.length} grades, avg = ${avg.toFixed(1)}`);
}
// Alice: 5 grades, avg = 90.6
// Bob: 3 grades, avg = 78.3
// Carol: 4 grades, avg = 90.3

8. Real examples

Tic-Tac-Toe board

// Initialize empty board
function createBoard() {
  return [
    [" ", " ", " "],
    [" ", " ", " "],
    [" ", " ", " "]
  ];
}

// Display the board
function displayBoard(board) {
  console.log("  0   1   2");
  for (let r = 0; r < board.length; r++) {
    console.log(`${r} ${board[r].join(" | ")}`);
    if (r < board.length - 1) {
      console.log("  ---------");
    }
  }
}

// Place a mark
function placeMark(board, row, col, mark) {
  if (board[row][col] !== " ") {
    console.log("Cell already taken!");
    return false;
  }
  board[row][col] = mark;
  return true;
}

// Check for a winner
function checkWinner(board) {
  // Check rows
  for (let r = 0; r < 3; r++) {
    if (board[r][0] !== " " && board[r][0] === board[r][1] && board[r][1] === board[r][2]) {
      return board[r][0];
    }
  }
  // Check columns
  for (let c = 0; c < 3; c++) {
    if (board[0][c] !== " " && board[0][c] === board[1][c] && board[1][c] === board[2][c]) {
      return board[0][c];
    }
  }
  // Check diagonals
  if (board[0][0] !== " " && board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
    return board[0][0];
  }
  if (board[0][2] !== " " && board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
    return board[0][2];
  }
  return null;
}

// Demo game
const board = createBoard();
placeMark(board, 0, 0, "X");
placeMark(board, 1, 1, "O");
placeMark(board, 0, 1, "X");
placeMark(board, 2, 0, "O");
placeMark(board, 0, 2, "X");  // X wins row 0

displayBoard(board);
//   0   1   2
// 0 X | X | X
//   ---------
// 1   | O |
//   ---------
// 2 O |   |

const winner = checkWinner(board);
console.log(winner ? `Winner: ${winner}` : "No winner yet");
// Winner: X

Spreadsheet data

const sheet = [
  ["Product",  "Q1",  "Q2",  "Q3",  "Q4"],
  ["Widgets",  1200,  1500,  1100,  1800],
  ["Gadgets",  800,   950,   1200,  1050],
  ["Gizmos",   450,   520,   480,   610]
];

// Calculate row totals
console.log("Sales Report:");
console.log("=".repeat(50));

for (let r = 1; r < sheet.length; r++) {
  const product = sheet[r][0];
  let total = 0;
  for (let c = 1; c < sheet[r].length; c++) {
    total += sheet[r][c];
  }
  console.log(`${product}: ${total} units (avg ${(total / 4).toFixed(0)}/quarter)`);
}
// Widgets: 5600 units (avg 1400/quarter)
// Gadgets: 4000 units (avg 1000/quarter)
// Gizmos: 2060 units (avg 515/quarter)

// Calculate column totals
console.log("\nQuarterly totals:");
for (let c = 1; c < sheet[0].length; c++) {
  let colTotal = 0;
  for (let r = 1; r < sheet.length; r++) {
    colTotal += sheet[r][c];
  }
  console.log(`${sheet[0][c]}: ${colTotal}`);
}
// Q1: 2450
// Q2: 2970
// Q3: 2780
// Q4: 3460

Pixel grid (simplified)

// A 5x5 pixel grid using grayscale values (0-255)
const pixels = [
  [  0,   0,   0,   0,   0],
  [  0, 255, 255, 255,   0],
  [  0, 255,   0, 255,   0],
  [  0, 255, 255, 255,   0],
  [  0,   0,   0,   0,   0]
];

// Visualize as ASCII art
function renderPixels(grid) {
  for (const row of grid) {
    let line = "";
    for (const pixel of row) {
      line += pixel > 128 ? "##" : "  ";
    }
    console.log("|" + line + "|");
  }
}

renderPixels(pixels);
// |          |
// |  ######  |
// |  ##  ##  |
// |  ######  |
// |          |

// Invert the image
function invertPixels(grid) {
  return grid.map(row => row.map(pixel => 255 - pixel));
}

console.log("\nInverted:");
renderPixels(invertPixels(pixels));
// |##########|
// |##      ##|
// |##  ##  ##|
// |##      ##|
// |##########|

9. Practice: Tic-Tac-Toe board + Matrix addition

Exercise 1: Check if a tic-tac-toe board is full

function isBoardFull(board) {
  for (const row of board) {
    for (const cell of row) {
      if (cell === " ") return false;
    }
  }
  return true;
}

// Test
const fullBoard = [["X","O","X"],["O","X","O"],["O","X","O"]];
const partialBoard = [["X"," ","X"],["O","X","O"],["O","X","O"]];

console.log(isBoardFull(fullBoard));     // true
console.log(isBoardFull(partialBoard));  // false

Exercise 2: Matrix addition (try it yourself first!)

// Given:
const M1 = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

const M2 = [
  [9, 8, 7],
  [6, 5, 4],
  [3, 2, 1]
];

// Expected result of M1 + M2:
// [[10, 10, 10],
//  [10, 10, 10],
//  [10, 10, 10]]

function addMatrices(a, b) {
  const result = [];
  for (let r = 0; r < a.length; r++) {
    const row = [];
    for (let c = 0; c < a[r].length; c++) {
      row.push(a[r][c] + b[r][c]);
    }
    result.push(row);
  }
  return result;
}

console.log(addMatrices(M1, M2));
// [[10, 10, 10], [10, 10, 10], [10, 10, 10]]

Key takeaways

  1. A 2D array is an array of arrays -- access with matrix[row][col].
  2. Create matrices with Array.from() and a factory function -- never fill() with a shared array reference.
  3. Iterate 2D arrays with nested loops -- outer loop for rows, inner for columns.
  4. Jagged arrays have rows of different lengths -- always use matrix[r].length for the inner bound.
  5. Transpose swaps rows and columns; rotation maps [r][c] to [c][n-1-r].
  6. 2D arrays model real-world grids: game boards, spreadsheets, images, calendars.
  7. 3D arrays add another layer of nesting and are used in specialized domains.

Explain-It Challenge

Explain without notes:

  1. Why does new Array(3).fill([]) create a broken 2D array, and how do you fix it?
  2. How do you access the element in the second row, third column of a matrix?
  3. To iterate a matrix column-by-column instead of row-by-row, which index becomes the outer loop?
  4. Draw a 3x3 tic-tac-toe board as a 2D array and show how you would check if the middle row is all "X".

Navigation: <- 1.21.f -- Practice Problems . 1.21 Overview