Golang DSA Chapter 5 Part 2: Matrix Operations in Go
Welcome back to Part 2 of Chapter 5 from “Learn Data Structures and Algorithms with Golang” by Bhagvan Kommadi. In Part 1 we looked at arrays and multi-dimensional arrays. Now we get into the fun stuff: matrix operations, special matrix types, and tensors.
If you’ve ever taken a linear algebra class, most of this will feel familiar. If you haven’t, don’t worry. The operations themselves are pretty straightforward once you see the code.
Setting Up Two Matrices
Before we can do anything with matrices, we need some to work with. The book starts with two simple 2x2 matrices:
var matrix1 = [2][2]int{
{4, 5},
{1, 2},
}
var matrix2 = [2][2]int{
{6, 7},
{3, 4},
}
Nothing fancy. Two grids of numbers. Now let’s do things with them.
Matrix Addition
Adding two matrices is about as simple as it gets. You just add each element in the same position. Row 0, column 0 of matrix1 plus row 0, column 0 of matrix2, and so on.
func add(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
var m int
var l int
var sum [2][2]int
for l = 0; l < 2; l++ {
for m = 0; m < 2; m++ {
sum[l][m] = matrix1[l][m] + matrix2[l][m]
}
}
return sum
}
So for our two matrices, position [0][0] would be 4 + 6 = 10, position [0][1] would be 5 + 7 = 12, and so on. You get a new matrix where every cell is the sum of the corresponding cells from the two inputs.
You call it like this:
var sum [2][2]int
sum = add(matrix1, matrix2)
Matrix Subtraction
Subtraction is the exact same pattern, just with a minus sign instead of plus:
func subtract(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
var m int
var l int
var difference [2][2]int
for l = 0; l < 2; l++ {
for m = 0; m < 2; m++ {
difference[l][m] = matrix1[l][m] - matrix2[l][m]
}
}
return difference
}
Position [0][0] becomes 4 - 6 = -2, position [0][1] becomes 5 - 7 = -2. You get the idea.
Matrix Multiplication
Here’s where it gets slightly more interesting. Matrix multiplication is not just multiplying each position like addition was. Instead, for each cell in the result, you take a row from the first matrix and a column from the second matrix, multiply the corresponding elements, and add them all up.
func multiply(matrix1 [2][2]int, matrix2 [2][2]int) [2][2]int {
var m int
var l int
var n int
var product [2][2]int
for l = 0; l < 2; l++ {
for m = 0; m < 2; m++ {
var productSum int = 0
for n = 0; n < 2; n++ {
productSum = productSum + matrix1[l][n]*matrix2[n][m]
}
product[l][m] = productSum
}
}
return product
}
Notice the three nested loops. That’s the classic O(n^3) matrix multiplication. For a 2x2 matrix, no big deal. For huge matrices, this gets slow fast. There are smarter algorithms out there (like Strassen’s), but the book keeps it simple here.
To get position [0][0] of the product: (4*6) + (5*3) = 24 + 15 = 39. You’re walking across row 0 of matrix1 and down column 0 of matrix2 at the same time.
Matrix Transpose
Transposing a matrix means flipping it along its diagonal. Rows become columns, columns become rows. Position [0][1] swaps with [1][0], and so on.
func transpose(matrix1 [2][2]int) [2][2]int {
var m int
var l int
var transMatrix [2][2]int
for l = 0; l < 2; l++ {
for m = 0; m < 2; m++ {
transMatrix[l][m] = matrix1[m][l]
}
}
return transMatrix
}
The key line is transMatrix[l][m] = matrix1[m][l]. You’re just swapping the indices. If your matrix was {{4, 5}, {1, 2}}, the transpose is {{4, 1}, {5, 2}}.
Determinant
The determinant of a 2x2 matrix is a single number calculated from its elements. For a matrix {{a, b}, {c, d}}, the determinant is a*d - b*c. It tells you things like whether the matrix is invertible (if the determinant is zero, it’s not).
func determinant(matrix1 [2][2]int) float64 {
var det float64
det = float64((matrix1[0][0] * matrix1[1][1]) - (matrix1[0][1] * matrix1[1][0]))
return det
}
For our matrix1 {{4, 5}, {1, 2}}, the determinant is (4*2) - (5*1) = 8 - 5 = 3.
Matrix Inversion
The inverse of a matrix is basically the “undo” matrix. If you multiply a matrix by its inverse, you get the identity matrix (the matrix equivalent of the number 1). Not every matrix has an inverse, only the ones with a non-zero determinant.
For a 2x2 matrix, the inverse formula uses the determinant:
func inverse(matrix [2][2]int) [][]float64 {
var det float64
det = determinant(matrix)
var invmatrix [][]float64
invmatrix = make([][]float64, 2)
invmatrix[0] = make([]float64, 2)
invmatrix[1] = make([]float64, 2)
invmatrix[0][0] = float64(matrix[1][1]) / det
invmatrix[0][1] = -1 * float64(matrix[0][1]) / det
invmatrix[1][0] = -1 * float64(matrix[1][0]) / det
invmatrix[1][1] = float64(matrix[0][0]) / det
return invmatrix
}
You swap the diagonal elements, negate the off-diagonal elements, and divide everything by the determinant. The result is a float64 matrix because division usually produces non-integer values.
Zig-Zag Matrix
Now we get into some special matrix types. A zig-zag matrix is a square n x n grid where integers are arranged along anti-diagonals in order. It fills in the numbers going back and forth diagonally, like a zigzag pattern.
func PrintZigZag(n int) []int {
var zigzag []int
zigzag = make([]int, n*n)
var i int
i = 0
var m int
m = n * 2
var p int
for p = 1; p <= m; p++ {
var x int
x = p - n
if x < 0 {
x = 0
}
var y int
y = p - 1
if y > n-1 {
y = n - 1
}
var j int
j = m - p
if j > p {
j = p
}
var k int
for k = 0; k < j; k++ {
if p&1 == 0 {
zigzag[(x+k)*n+y-k] = i
} else {
zigzag[(y-k)*n+x+k] = i
}
i++
}
}
return zigzag
}
The algorithm uses the bitwise AND (p&1) to check if the current diagonal number is even or odd, which determines the direction of traversal. Even diagonals go one way, odd diagonals go the other. That’s what creates the zig-zag pattern.
For a 5x5 matrix, the output looks like a grid where 0 starts at the top-left corner and the numbers snake diagonally through the matrix.
Spiral Matrix
A spiral matrix fills in numbers starting from the top-left, going right along the top row, then down the right column, then left along the bottom row, then up the left column, and repeating inward like a spiral.
func PrintSpiral(n int) []int {
var left, top, right, bottom int
left = 0
top = 0
right = n - 1
bottom = n - 1
var size int
size = n * n
var s []int
s = make([]int, size)
var i int
i = 0
for left < right {
var c int
for c = left; c <= right; c++ {
s[top*n+c] = i
i++
}
top++
var r int
for r = top; r <= bottom; r++ {
s[r*n+right] = i
i++
}
right--
if top == bottom {
break
}
for c = right; c >= left; c-- {
s[bottom*n+c] = i
i++
}
bottom--
for r = bottom; r >= top; r-- {
s[r*n+left] = i
i++
}
left++
}
s[top*n+left] = i
return s
}
The trick here is four boundary variables: left, right, top, bottom. Each time you finish a side of the spiral, you shrink the boundary inward. So top++ after filling the top row, right-- after filling the right column, and so on. The boundaries keep closing in until you reach the center.
This is actually a pretty common interview question, so worth understanding.
Boolean Matrix
A Boolean matrix transformation takes any matrix and, wherever it finds a 1, sets the entire row and column of that cell to 1. Think of it like a cross pattern expanding from every cell that has a 1 in it.
func changeMatrix(matrix [3][3]int) [3][3]int {
var i, j int
var Rows [3]int
var Columns [3]int
var matrixChanged [3][3]int
for i = 0; i < 3; i++ {
for j = 0; j < 3; j++ {
if matrix[i][j] == 1 {
Rows[i] = 1
Columns[j] = 1
}
}
}
for i = 0; i < 3; i++ {
for j = 0; j < 3; j++ {
if Rows[i] == 1 || Columns[j] == 1 {
matrixChanged[i][j] = 1
}
}
}
return matrixChanged
}
The approach is clean: first pass marks which rows and columns contain a 1. Second pass builds the result matrix, setting any cell to 1 if its row or column was marked. Two passes, no surprises.
If you start with {{1,0,0},{0,0,0},{0,0,0}}, the 1 at position [0][0] means all of row 0 and all of column 0 become 1. The result is {{1,1,1},{1,0,0},{1,0,0}}.
Multi-Dimensional Arrays
Going beyond two dimensions, Go handles multi-dimensional arrays just fine. Here’s a 3D array filled with random integers:
var threedarray [2][2][2]int
var i, j, k int
for i = 0; i < 2; i++ {
for j = 0; j < 2; j++ {
for k = 0; k < 2; k++ {
threedarray[i][j][k] = rand.Intn(3)
}
}
}
fmt.Println(threedarray)
Think of it as a cube of numbers. First index picks the layer, second picks the row, third picks the column. Not something you use every day, but useful when your data genuinely has three or more dimensions.
Tensors
Tensors are multi-dimensional arrays used in physics, biology, and machine learning. The name sounds intimidating, but in Go they’re just arrays with more dimensions. William Rowan Hamilton first coined the term, and they show up everywhere in fields like electromagnetism and diffusion imaging.
The tensor’s order is defined by the sum of its argument orders plus the result tensor’s order. An inertia matrix, for example, is a second-order tensor.
Here’s a 3x3x3 tensor:
var array [3][3][3]int
var i, j, k int
for i = 0; i < 3; i++ {
for j = 0; j < 3; j++ {
for k = 0; k < 3; k++ {
array[i][j][k] = rand.Intn(3)
}
}
}
One key operation on tensors is unfolding (sometimes called matricization). You take a 3D tensor and flatten one dimension to get a 2D matrix. The book shows 0-mode, 1-mode, and 2-mode unfolding, which just means you fix the first index to 0, 1, or 2 and print the resulting 2D slice:
// 0-mode unfolding
for j = 0; j < 3; j++ {
for k = 0; k < 3; k++ {
fmt.Printf("%d ", array[0][j][k])
}
fmt.Printf("\n")
}
For 1-mode, you change array[0] to array[1]. For 2-mode, array[2]. Each mode gives you a different “view” of the same data. If you’re getting into machine learning or scientific computing, tensor operations become really important.
Performance Notes
A quick note on the performance of these operations:
- Addition and subtraction are O(n^2) for an n x n matrix. You visit each cell exactly once.
- Multiplication is O(n^3) with the basic algorithm. Each cell in the result requires summing n products.
- Transpose is O(n^2), just swapping positions.
- Determinant for a 2x2 is O(1), but for larger matrices it scales up fast.
- Inversion depends on the determinant calculation and is also expensive for large matrices.
For the special matrices (zig-zag, spiral, Boolean), they’re all O(n^2) since you’re filling or scanning every cell in an n x n grid.
Chapter 5 Summary
This chapter covered a lot of ground with homogeneous data structures:
- Matrix operations: addition, subtraction, multiplication, transpose, determinant, and inversion
- Special matrices: zig-zag patterns, spiral patterns, Boolean matrix transformations
- Multi-dimensional arrays: extending beyond 2D into 3D and beyond
- Tensors: multi-dimensional arrays with unfolding operations
All of these build on the array fundamentals from Part 1. The matrix operations are bread-and-butter linear algebra, and the special matrix types are the kind of thing that pops up in coding interviews and competitive programming.
Next up is Chapter 6, where we move from homogeneous structures to heterogeneous ones: linked lists.
This post is part of a series retelling “Learn Data Structures and Algorithms with Golang” by Bhagvan Kommadi (Packt, 2019, ISBN: 978-1-78961-850-1). You can find all posts in the series introduction.
| Navigation | Link |
|---|---|
| Previous | Chapter 5 Part 1: Arrays and Multi-Dimensional Arrays |
| Next | Chapter 6 Part 1: Singly and Doubly Linked Lists |