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.

NavigationLink
PreviousChapter 5 Part 1: Arrays and Multi-Dimensional Arrays
NextChapter 6 Part 1: Singly and Doubly Linked Lists

About

About BookGrill.net

BookGrill.net is a technology book review site for developers, engineers, and anyone who builds things with code. We cover books on software engineering, AI and machine learning, cybersecurity, systems design, and the culture of technology.

Know More