2D Arrays & Matrices: Grids, Rows, and Columns
A 2D array (or matrix) is a grid of values addressed by two indices — a row and a column — written m[row][col]. It’s the natural shape for tables, images, game boards, and adjacency matrices. Conceptually it’s a rectangle of rows × cols cells; physically most languages store it as an “array of arrays” or one flat block, but you index it the same way. This page covers creating a matrix and traversing it by row and by column.
Creating a matrix
Here is a 3×4 grid (3 rows, 4 columns) initialized to zero:
#include <vector>
int rows = 3, cols = 4;
std::vector<std::vector<int>> m(rows, std::vector<int>(cols, 0));
m[0][1] = 7; // row 0, column 1
int rows = 3, cols = 4;
int[][] m = new int[rows][cols]; // defaults to 0
m[0][1] = 7; // row 0, column 1
const rows = 3, cols = 4;
const m = Array.from({ length: rows }, () => new Array(cols).fill(0));
m[0][1] = 7; // row 0, column 1
rows, cols = 3, 4
m = [[0] * cols for _ in range(rows)]
m[0][1] = 7 # row 0, column 1
Pitfall: In Python,
[[0] * cols] * rowscreatesrowsreferences to the same inner list, so writingm[0][1]changes every row. Always build rows with a comprehension as above. The same aliasing trap exists in JavaScript if you reuse one inner array reference.
How a matrix is stored
Most languages use row-major order: each row is laid out contiguously, one after another. So m[r][c] lives at flat offset r * cols + c. Because rows are contiguous, scanning row by row is cache-friendly and fast; jumping column by column skips across memory and can be slower on large matrices.
Going deeper: Some languages and libraries (Fortran, some BLAS routines) use column-major order, where columns are contiguous instead. The access pattern that’s cheap simply flips. For everyday DSA in these four languages, assume row-major.
Row traversal
Visit every cell, row by row — the natural, cache-friendly order. This sums all elements:
long sumByRows(const std::vector<std::vector<int>>& m) {
long total = 0;
for (size_t r = 0; r < m.size(); r++)
for (size_t c = 0; c < m[r].size(); c++)
total += m[r][c];
return total;
}
long sumByRows(int[][] m) {
long total = 0;
for (int r = 0; r < m.length; r++)
for (int c = 0; c < m[r].length; c++)
total += m[r][c];
return total;
}
function sumByRows(m) {
let total = 0;
for (let r = 0; r < m.length; r++)
for (let c = 0; c < m[r].length; c++)
total += m[r][c];
return total;
}
def sum_by_rows(m):
total = 0
for row in m:
for x in row:
total += x
return total
Column traversal
Sometimes you need to walk down each column — swap the loop order so the column index is the outer loop:
long sumByCols(const std::vector<std::vector<int>>& m) {
long total = 0;
int rows = m.size(), cols = rows ? m[0].size() : 0;
for (int c = 0; c < cols; c++)
for (int r = 0; r < rows; r++)
total += m[r][c];
return total;
}
long sumByCols(int[][] m) {
long total = 0;
int rows = m.length, cols = rows > 0 ? m[0].length : 0;
for (int c = 0; c < cols; c++)
for (int r = 0; r < rows; r++)
total += m[r][c];
return total;
}
function sumByCols(m) {
let total = 0;
const rows = m.length, cols = rows ? m[0].length : 0;
for (let c = 0; c < cols; c++)
for (let r = 0; r < rows; r++)
total += m[r][c];
return total;
}
def sum_by_cols(m):
total = 0
rows = len(m)
cols = len(m[0]) if rows else 0
for c in range(cols):
for r in range(rows):
total += m[r][c]
return total
Complexity
For an r × c matrix with n = r * c cells:
- Access by
m[i][j]: O(1) - Full traversal (any order): O(r · c) = O(n)
- Space: O(r · c) to store the grid
Row and column traversal have the same Big-O; the difference is constant-factor cache performance, which favors the row-major order.
Where 2D arrays show up
Matrices power image grids, dynamic-programming tables (see DP on grids), and dense graph representations via adjacency matrices. Up next: array rotation and reversal, then practice on the array exercises.