AiTechWorlds
AiTechWorlds
Open any spreadsheet — Excel, Google Sheets, a budget planner. The data does not live in a single column. It lives on a grid: rows going across, columns going down. Cell B3 means "column B, row 3." You navigate to that exact intersection without scanning the whole document.
That grid is a 2D array. Instead of one index to locate an element, you need two: a row index and a column index. Everything else — the rules, the performance characteristics, the operations — follows directly from the 1D array you already understand.
The moment your data has a naturally two-dimensional structure — a game board, a distance table, a grayscale image, a seating chart — a 2D array is often the most natural and efficient representation.
Python does not have a native 2D array type, but a list of lists serves the same purpose:
# A 3×4 matrix (3 rows, 4 columns)
matrix = [
[1, 2, 3, 4], # row 0
[5, 6, 7, 8], # row 1
[9, 10, 11, 12] # row 2
]
# Access element at row 1, column 2
print(matrix[1][2]) # Output: 7
# Access entire row 0
print(matrix[0]) # Output: [1, 2, 3, 4]
The notation matrix[row][col] follows directly from nested list access. The outer index selects a row (a list), and the inner index selects a column (an element within that list).
A common interview pitfall is this incorrect approach:
# WRONG — all rows share the same list object
rows, cols = 3, 4
bad_matrix = [[0] * cols] * rows
bad_matrix[0][0] = 99
print(bad_matrix)
# Output: [[99, 0, 0, 0], [99, 0, 0, 0], [99, 0, 0, 0]]
# Changing row 0 changed ALL rows!
The correct approach uses a list comprehension to create independent rows:
# CORRECT — each row is a separate list object
rows, cols = 3, 4
matrix = [[0] * cols for _ in range(rows)]
matrix[0][0] = 99
print(matrix)
# Output: [[99, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
In memory, 2D arrays are stored as a flat sequence of bytes. The order matters for performance.
Row-major order (used by C, Python, NumPy default) stores all elements of row 0 first, then row 1, then row 2:
Matrix: Memory layout (row-major):
[1, 2, 3] [1, 2, 3, 4, 5, 6, 7, 8, 9]
[4, 5, 6] ↑ row 0 ↑ row 1 ↑ row 2
[7, 8, 9]
Column-major order (used by Fortran, MATLAB, Julia) stores all elements of column 0 first, then column 1:
Memory layout (column-major):
[1, 4, 7, 2, 5, 8, 3, 6, 9]
↑ col 0 ↑ col 1 ↑ col 2
Why it matters: Modern CPUs cache memory in lines. If you iterate in the same order as storage, you get cache hits (fast). If you iterate against the grain, you get cache misses (slow). For Python list-of-lists matrices, iterating row by row is faster than column by column.
Transpose flips rows and columns: element [i][j] moves to [j][i].
def transpose(matrix):
rows = len(matrix)
cols = len(matrix[0])
# Create new matrix with flipped dimensions
result = [[0] * rows for _ in range(cols)]
for i in range(rows):
for j in range(cols):
result[j][i] = matrix[i][j]
return result
original = [
[1, 2, 3],
[4, 5, 6]
]
print(transpose(original))
# Output: [[1, 4], [2, 5], [3, 6]]
Python one-liner using zip:
transposed = [list(row) for row in zip(*original)]
print(transposed) # Output: [[1, 4], [2, 5], [3, 6]]
Two matrices of the same dimensions can be added element-by-element:
def matrix_add(A, B):
rows, cols = len(A), len(A[0])
return [[A[i][j] + B[i][j] for j in range(cols)] for i in range(rows)]
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
print(matrix_add(A, B)) # Output: [[6, 8], [10, 12]]
Matrix multiplication is not element-by-element. Element C[i][j] is the dot product of row i of A and column j of B. For this to work, A must have as many columns as B has rows.
def matrix_multiply(A, B):
rows_A = len(A)
cols_A = len(A[0]) # Must equal len(B)
cols_B = len(B[0])
C = [[0] * cols_B for _ in range(rows_A)]
for i in range(rows_A):
for j in range(cols_B):
for k in range(cols_A):
C[i][j] += A[i][k] * B[k][j]
return C
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
print(matrix_multiply(A, B))
# Output: [[19, 22], [43, 50]]
# C[0][0] = 1×5 + 2×7 = 5 + 14 = 19
# C[0][1] = 1×6 + 2×8 = 6 + 16 = 22
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
Access element [i][j] | O(1) | O(1) | Direct index |
| Traverse entire matrix | O(m × n) | O(1) | Visit every cell once |
| Matrix addition (m×n) | O(m × n) | O(m × n) | New result matrix |
| Transpose (m×n) | O(m × n) | O(n × m) | Flip dimensions |
| Matrix multiply (n×n) | O(n³) | O(n²) | Classic algorithm |
| Matrix multiply (n×n) | O(n^2.37) | O(n²) | Strassen's algorithm |
| Row search | O(n) | O(1) | Scan a single row |
| Search sorted matrix | O(m + n) | O(1) | Start top-right corner |
A grayscale image is literally a 2D array. Each cell holds a value from 0 (black) to 255 (white):
# 4×4 grayscale image (brightness values)
image = [
[ 10, 50, 80, 120],
[ 30, 90, 150, 200],
[ 60, 130, 170, 220],
[100, 160, 210, 250]
]
# Invert the image: new brightness = 255 - old brightness
inverted = [[255 - pixel for pixel in row] for row in image]
print(inverted[0]) # Output: [245, 205, 175, 135]
A 1080p image is a 1080×1920 matrix — about 2 million cells. Efficient matrix operations are critical in any image processing pipeline.
board = [
['X', 'O', 'X'],
['O', 'X', 'O'],
['O', 'X', 'X']
]
def check_winner(board):
n = len(board)
for i in range(n):
if len(set(board[i])) == 1: # Check row i
return board[i][0]
if len(set(board[j][i] for j in range(n))) == 1: # Check col i
return board[0][i]
if len(set(board[i][i] for i in range(n))) == 1: # Main diagonal
return board[0][0]
if len(set(board[i][n-1-i] for i in range(n))) == 1: # Anti-diagonal
return board[0][n-1]
return None
print(check_winner(board)) # Output: X
Given a matrix, return all elements in clockwise spiral order starting from the top-left.
def spiral_order(matrix):
result = []
while matrix:
# Take the first row (left to right)
result += matrix.pop(0)
# Rotate remaining matrix 90 degrees counter-clockwise
# zip(*matrix) transposes it, [::-1] reverses rows
matrix = list(zip(*matrix))[::-1]
return result
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(spiral_order(grid))
# Output: [1, 2, 3, 6, 9, 8, 7, 4, 5]
Step-by-step trace:
| Step | Action | Collected | Remaining Matrix |
|---|---|---|---|
| 1 | Pop row [1,2,3] | [1,2,3] | [[4,5,6],[7,8,9]] |
| 2 | Rotate CCW | — | [[6,9],[5,8],[4,7]] → actually [(6,9),(5,8),(4,7)] reversed = [(4,7),(5,8),(6,9)] |
| 3 | Pop row [4,7] | [1,2,3,6,9] | ... |
The elegant trick: after taking the top row, rotating the remaining matrix counter-clockwise means the next top row is what was previously the right column — exactly the next direction in a clockwise spiral.
For production numerical computing, Python's list-of-lists is too slow. NumPy provides true 2D arrays backed by optimized C code:
import numpy as np
# Create matrices
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# Element-wise operations — no loops needed
print(A + B) # [[6,8],[10,12]]
print(A * 3) # [[3,6],[9,12]]
# True matrix multiplication
print(A @ B) # [[19,22],[43,50]]
# Transpose
print(A.T) # [[1,3],[2,4]]
# Shape information
print(A.shape) # (2, 2)
print(A.dtype) # int64
NumPy operations run in C, making them 10–100× faster than equivalent Python loops — essential for machine learning, signal processing, and scientific computing.
[row][col] — two indices instead of one[[0] * cols for _ in range(rows)] to create an empty matrix — never use [[0] * cols] * rowsNext, you will leave contiguous memory behind entirely and explore linked lists — structures where elements can live anywhere in memory, connected only by pointers.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises