Determinants

Concept

The determinant is a single number attached to a square matrix that measures how the matrix scales signed area (2Γ—2), volume (3Γ—3), or hypervolume (nΓ—n) β€” and it is zero exactly when the matrix is singular. In ch06 you saw the 2Γ—2 inverse formula $\mathbf{A}^{-1} = \frac{1}{ad - bc} \begin{pmatrix} d & -b \\ -c & a \end{pmatrix}$, where the denominator $ad - bc$ is the determinant. This chapter formalizes that number, gives two ways to compute it, and pins down the properties that make it the single most useful singularity test in linear algebra.

Why This Matters

The determinant is the bridge between the computational linear algebra of ch03–ch06 and the structural theory that comes next. It tells you whether a transformation collapses a direction (det = 0) without ever running Gauss-Jordan β€” which is how you'll recognize singular matrices, degenerate data, and rank-deficient systems at a glance. It feeds directly into ch08 (rank and vector spaces: a matrix is singular exactly when its rows/columns are linearly dependent), ch11 (eigenvalues: $\det(\mathbf{A} - \lambda\mathbf{I}) = 0$ is the characteristic equation), and ch24 (Jacobians: the determinant of the Jacobian is the factor that converts areas/volumes under a change of variables β€” the reason probability densities need Jacobian terms in transformed distributions).


Mathematical Notation

The determinant

For a square matrix $\mathbf{A} \in \mathbb{R}^{n \times n}$, the determinant is written $\det(\mathbf{A})$ or $|\mathbf{A}|$ (read "determinant of A" β€” the bars are not absolute value). It is a single real number defined by three properties:

These three rules determine the determinant uniquely, and everything else follows from them.

For $2 \times 2$ and $3 \times 3$ matrices there are closed forms:

$$ \det\begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc $$
$$ \det\begin{pmatrix} a & b & c \\ d & e & f \\ g & h & i \end{pmatrix} = aei + bfg + cdh - ceg - bdi - afh $$

(The $3 \times 3$ pattern is Sarrus' rule β€” products down-right minus products down-left; it works only for 3Γ—3.)

Motivation: the determinant's job is to answer one yes/no question β€” is this matrix invertible? β€” plus the quantitative version β€” by what factor does it stretch space? Both answers fall out of the same number.

Minors and cofactors

The minor $M_{ij}$ of $\mathbf{A}$ is the $(n-1) \times (n-1)$ matrix you get by deleting row $i$ and column $j$. The cofactor is the signed minor:

$$ C_{ij} = (-1)^{i+j} \det(M_{ij}) $$

The sign $(-1)^{i+j}$ alternates in a checkerboard pattern, starting with $+$ in the top-left corner:

$$ \begin{pmatrix} + & - & + & \cdots \\ - & + & - & \cdots \\ + & - & + & \cdots \\ \vdots & \vdots & \vdots & \ddots \end{pmatrix} $$

Motivation: minors and cofactors are the machinery of the expansion formula below β€” they break an $n \times n$ determinant into a sum of $(n-1) \times (n-1)$ ones, recursively down to $1 \times 1$.

Cofactor expansion

The determinant can be computed by expanding along any row or column. Along the first row:

$$ \det(\mathbf{A}) = \sum_{j=1}^{n} a_{1j} \, C_{1j} = a_{11}C_{11} + a_{12}C_{12} + \cdots + a_{1n}C_{1n} $$

Motivation: cofactor expansion is the definitional algorithm β€” the one that proves the determinant is well-defined and that textbooks use to derive every property. It is not how you compute determinants in practice: expanding an $n \times n$ determinant this way takes $n!$ operations, which is why our practical implementation (below) uses elimination instead.

Key properties

Every property below is used somewhere in this course, so each gets its motivation:

PropertyStatementMotivation
Singularity test$\det(\mathbf{A}) = 0 \iff \mathbf{A}$ is singularThe inverse exists iff $\det \neq 0$ β€” the ch06 connection
Row swapSwapping two rows flips the signThis is why elimination tracks sign (below); also the orientation flip in geometry
Row additionAdding a multiple of one row to another leaves $\det$ unchangedThe second rule that makes elimination legal
Row scalingMultiplying a row by $c$ multiplies $\det$ by $c$We avoid this operation in elimination precisely because it changes the answer
Triangular matrices$\det$ = product of diagonal entriesThe payoff of elimination: an upper-triangular matrix's determinant is free
Multiplicativity$\det(\mathbf{A}\mathbf{B}) = \det(\mathbf{A})\det(\mathbf{B})$How scaling factors compose; used in ch11+ whenever a matrix is factored
Transpose$\det(\mathbf{A}^T) = \det(\mathbf{A})$Rows and columns are interchangeable for the determinant
Singular rowsTwo identical (or proportional) rows $\Rightarrow \det = 0$Instant singularity detection, no computation needed

Intuition

Determinant as signed area

Take the columns of a $2 \times 2$ matrix $\mathbf{A}$ as two vectors $\mathbf{u}, \mathbf{v}$ drawn from the origin. They span a parallelogram, and $|\det(\mathbf{A})|$ is exactly its area:

        v = (c, d)  ●
                   / \
                  /   \        area of parallelogram
                 /     \       = |ad βˆ’ bc|
        ●───────●
     u = (a, b)

The sign of the determinant records orientation: if $\mathbf{v}$ is counterclockwise from $\mathbf{u}$ the determinant is positive, clockwise gives negative. Swapping the two columns (or rows) flips the sign β€” that's the row-swap property, now with a picture. In 3D the same story holds: the columns span a parallelepiped and $|\det|$ is its volume.

Singular = collapse

When the two columns point in the same direction (the parallelogram degenerates to a line), the area is 0 β€” and the matrix has no inverse, exactly as ch06 taught: the transformation squashes 2D space down to a line, destroying information irreversibly. The determinant is the algebraic version of "how much space survives the transformation": full space (det β‰  0) or collapsed (det = 0).

Why elimination can compute it

Gaussian elimination turns a general matrix into an upper-triangular one using exactly two moves β€” adding a multiple of one row to another and swapping rows. Both moves have known effects on the determinant (none, and sign-flip), and a triangular matrix's determinant is just the diagonal product. So: eliminate, count the swaps, multiply the diagonal. This is the O(nΒ³) algorithm, and it's the one real libraries use.


Worked Examples

Example 1: a 2Γ—2 β€” signed area

$$ \mathbf{A} = \begin{pmatrix} 4 & 7 \\ 2 & 6 \end{pmatrix}, \qquad \det(\mathbf{A}) = 4 \cdot 6 - 7 \cdot 2 = 24 - 14 = 10 $$

The columns $(4, 2)$ and $(7, 6)$ span a parallelogram of area 10, and the sign is positive: the second column is counterclockwise from the first.

Example 2: a 3Γ—3 by cofactor expansion

$$ \mathbf{A} = \begin{pmatrix} 2 & 1 & 0 \\ 1 & 2 & 1 \\ 0 & 1 & 2 \end{pmatrix} $$

Expand along the first row ($+a_{11}C_{11} - a_{12}C_{12} + a_{13}C_{13}$, with the checkerboard starting at $+$):

$$ \det(\mathbf{A}) = 2 \cdot \det\begin{pmatrix} 2 & 1 \\ 1 & 2 \end{pmatrix}

  • 1 \cdot \det\begin{pmatrix} 1 & 1 \\ 0 & 2 \end{pmatrix}
  • 0 \cdot \det\begin{pmatrix} 1 & 2 \\ 0 & 1 \end{pmatrix} $$

The zero entry kills the third term β€” a good reason to expand along the row or column with the most zeros. Then:

$$ = 2(4 - 1) - 1(2 - 0) + 0 = 6 - 2 = 4 $$

Example 3: elimination with a row swap

$$ \mathbf{A} = \begin{pmatrix} 1 & 1 & 1 \\ 1 & 1 & 2 \\ 1 & 2 & 1 \end{pmatrix} $$

Subtract row 1 from rows 2 and 3:

$$ \begin{pmatrix} 1 & 1 & 1 \\ 0 & 0 & 1 \\ 0 & 1 & 0 \end{pmatrix} $$

The column-2 pivot position holds a 0, so we swap rows 2 and 3 β€” one swap, sign flips to $-1$:

$$ \begin{pmatrix} 1 & 1 & 1 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix} \qquad \Rightarrow \qquad \det(\mathbf{A}) = (-1) \cdot (1 \cdot 1 \cdot 1) = -1 $$

Example 4: a singular matrix

$$ \mathbf{A} = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 1 & 2 & 3 \end{pmatrix} $$

Rows 1 and 3 are identical β€” the parallelogram (here, parallelepiped) is degenerate, so no computation is needed: $\det(\mathbf{A}) = 0$, and $\mathbf{A}$ is singular.


Rust Implementation

Add a new crate to your workspace:

cd code && cargo new --lib --edition 2024 ch07-determinants

Register it in code/Cargo.toml:

members = [..., "ch07-determinants"]

This crate builds on the Matrix struct from ch03. Open code/ch07-determinants/src/lib.rs and start by copying the full Matrix impl from ch03-linear-algebra-matrices/src/lib.rs (as you did in ch04 and ch06). Then add the two determinant methods plus their helpers:

/// A matrix is stored in row-major order as a flat Vec<f64>.
///
/// Row-major means the first row occupies indices 0..cols,
/// the second row occupies indices cols..2*cols, and so on.
/// The element at (row, col) is at index row * cols + col.
#[derive(Debug, Clone, PartialEq)]
pub struct Matrix {
    data: Vec<f64>,
    rows: usize,
    cols: usize,
}

impl Matrix {
    /// Create a new matrix from a flat Vec<f64> in row-major order.
    ///
    /// # Panics
    /// Panics if data.len() != rows * cols.
    pub fn new(data: Vec<f64>, rows: usize, cols: usize) -> Self {
        assert_eq!(
            data.len(),
            rows * cols,
            "Data length {} does not match dimensions {}Γ—{}",
            data.len(),
            rows,
            cols
        );
        Matrix { data, rows, cols }
    }

    /// Number of rows.
    pub fn rows(&self) -> usize {
        self.rows
    }

    /// Number of columns.
    pub fn cols(&self) -> usize {
        self.cols
    }

    /// Shape as (rows, cols).
    pub fn shape(&self) -> (usize, usize) {
        (self.rows, self.cols)
    }

    /// Access the element at (row, col) β€” 0-indexed.
    ///
    /// # Panics
    /// Panics if row or col is out of bounds.
    pub fn get(&self, row: usize, col: usize) -> f64 {
        assert!(row < self.rows, "Row {} out of bounds (rows={})", row, self.rows);
        assert!(col < self.cols, "Col {} out of bounds (cols={})", col, self.cols);
        self.data[row * self.cols + col]
    }

    /// The nΓ—n identity matrix: 1 on the diagonal, 0 elsewhere.
    pub fn identity(n: usize) -> Matrix {
        let mut data = vec![0.0; n * n];
        for i in 0..n {
            data[i * n + i] = 1.0;
        }
        Matrix {
            data,
            rows: n,
            cols: n,
        }
    }

    /// Matrix multiplication: self * other.
    ///
    /// For A ∈ ℝ^{mΓ—n} and B ∈ ℝ^{nΓ—p}, the result C ∈ ℝ^{mΓ—p}.
    /// C_{ij} = Ξ£_{k=0}^{n-1} A_{ik} * B_{kj}
    ///
    /// # Panics
    /// Panics if self.cols != other.rows (inner dimensions must match).
    pub fn multiply(&self, other: &Matrix) -> Matrix {
        assert_eq!(
            self.cols, other.rows,
            "Cannot multiply {}Γ—{} by {}Γ—{}",
            self.rows, self.cols, other.rows, other.cols
        );

        let mut data = vec![0.0; self.rows * other.cols];
        for i in 0..self.rows {
            for k in 0..self.cols {
                let aik = self.get(i, k);
                for j in 0..other.cols {
                    data[i * other.cols + j] += aik * other.get(k, j);
                }
            }
        }
        Matrix {
            data,
            rows: self.rows,
            cols: other.cols,
        }
    }

    /// ── Determinants ──

    /// Compute the determinant by **cofactor expansion** along the first row.
    ///
    /// For a 1Γ—1 matrix the determinant is the single entry. For n > 1 it is
    ///
    ///   det(A) = Ξ£β±Ό (-1)^j Β· a_{0j} Β· det(M_{0j})
    ///
    /// where M_{0j} is the (nβˆ’1)Γ—(nβˆ’1) **minor** obtained by deleting row 0
    /// and column j.
    ///
    /// This is the definitional algorithm β€” it is O(n!), so it is only
    /// practical for small matrices (up to about 4Γ—4).
    ///
    /// # Panics
    /// Panics if the matrix is not square.
    pub fn determinant_cofactor(&self) -> f64 {
        assert_eq!(
            self.rows, self.cols,
            "Determinant requires a square matrix (got {}Γ—{})",
            self.rows, self.cols
        );

        let n = self.rows;
        match n {
            // Empty-product convention: the determinant of a 0Γ—0 matrix is 1.
            0 => 1.0,
            1 => self.get(0, 0),
            _ => {
                let mut det = 0.0;
                for j in 0..n {
                    let sign = if j % 2 == 0 { 1.0 } else { -1.0 };
                    det += sign * self.get(0, j) * self.minor(0, j).determinant_cofactor();
                }
                det
            }
        }
    }

    /// Compute the determinant by **Gaussian elimination**.
    ///
    /// The matrix is reduced to upper-triangular form in place (on a copy)
    /// using only two row operations that leave the determinant unchanged or
    /// track it exactly:
    ///
    /// - adding a multiple of one row to another β€” determinant unchanged;
    /// - swapping two rows β€” determinant changes sign.
    ///
    /// The determinant is then the product of the diagonal entries, with the
    /// sign applied. This is O(nΒ³) and is the algorithm used in practice.
    ///
    /// A singular matrix has determinant 0, returned as exactly 0.0.
    ///
    /// # Panics
    /// Panics if the matrix is not square.
    pub fn determinant(&self) -> f64 {
        assert_eq!(
            self.rows, self.cols,
            "Determinant requires a square matrix (got {}Γ—{})",
            self.rows, self.cols
        );

        let n = self.rows;
        let mut m = self.clone();
        let mut sign = 1.0;

        for col in 0..n {
            // Partial pivoting: among rows col..n, pick the one with the
            // largest |value| in this column and swap it into the pivot row.
            let mut pivot = col;
            for row in (col + 1)..n {
                if m.get(row, col).abs() > m.get(pivot, col).abs() {
                    pivot = row;
                }
            }

            // If every candidate pivot is ~0, the column is all zeros below
            // the diagonal: the matrix is singular and det = 0. The 1e-10
            // threshold is an absolute one that assumes a well-scaled matrix
            // (entries of order 1), the convention used throughout this course.
            if m.get(pivot, col).abs() < 1e-10 {
                return 0.0;
            }

            if pivot != col {
                m.swap_rows(col, pivot);
                sign = -sign;
            }

            // Zero out the entries below the pivot. Adding a multiple of the
            // pivot row to a lower row never changes the determinant.
            for row in (col + 1)..n {
                let factor = m.get(row, col) / m.get(col, col);
                if factor != 0.0 {
                    m.add_scaled_row(row, col, -factor);
                }
            }
        }

        // Upper-triangular: det = sign · ∏ᡒ a_ii.
        let mut det = sign;
        for i in 0..n {
            det *= m.get(i, i);
        }
        det
    }

    /// ── Private helpers ──

    /// The (nβˆ’1)Γ—(nβˆ’1) **minor** M_{ij}: the matrix obtained by deleting
    /// row `row` and column `col` from `self`.
    ///
    /// # Panics
    /// Panics if row or col is out of bounds, or if the matrix is 1Γ—1 or
    /// smaller (there is nothing left after deleting).
    fn minor(&self, row: usize, col: usize) -> Matrix {
        assert!(row < self.rows, "Row {} out of bounds (rows={})", row, self.rows);
        assert!(col < self.cols, "Col {} out of bounds (cols={})", col, self.cols);
        assert!(self.rows > 1 && self.cols > 1, "Minor requires at least a 2Γ—2 matrix");

        let mut data = Vec::with_capacity((self.rows - 1) * (self.cols - 1));
        for i in 0..self.rows {
            if i == row {
                continue;
            }
            for j in 0..self.cols {
                if j == col {
                    continue;
                }
                data.push(self.get(i, j));
            }
        }
        Matrix {
            data,
            rows: self.rows - 1,
            cols: self.cols - 1,
        }
    }

    /// Swap rows `a` and `b` in place.
    fn swap_rows(&mut self, a: usize, b: usize) {
        if a == b {
            return;
        }
        for j in 0..self.cols {
            let tmp = self.data[a * self.cols + j];
            self.data[a * self.cols + j] = self.data[b * self.cols + j];
            self.data[b * self.cols + j] = tmp;
        }
    }

    /// Add `factor` times row `src` to row `dst` in place:
    /// row_dst ← row_dst + factor * row_src.
    fn add_scaled_row(&mut self, dst: usize, src: usize, factor: f64) {
        for j in 0..self.cols {
            self.data[dst * self.cols + j] += factor * self.data[src * self.cols + j];
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn approx_eq(a: f64, b: f64) -> bool {
        (a - b).abs() < 1e-10
    }

    #[test]
    fn test_1x1_det_is_the_entry() {
        assert!(approx_eq(Matrix::new(vec![5.0], 1, 1).determinant(), 5.0));
        assert!(approx_eq(Matrix::new(vec![-3.0], 1, 1).determinant_cofactor(), -3.0));
    }

    #[test]
    fn test_2x2_ad_minus_bc() {
        // det([[4,7],[2,6]]) = 4Β·6 βˆ’ 7Β·2 = 10
        let a = Matrix::new(vec![4.0, 7.0, 2.0, 6.0], 2, 2);
        assert!(approx_eq(a.determinant(), 10.0));
        assert!(approx_eq(a.determinant_cofactor(), 10.0));
    }

    #[test]
    fn test_2x2_row_swap_flips_sign() {
        // det([[0,1],[1,0]]) = βˆ’1
        let a = Matrix::new(vec![0.0, 1.0, 1.0, 0.0], 2, 2);
        assert!(approx_eq(a.determinant(), -1.0));
        assert!(approx_eq(a.determinant_cofactor(), -1.0));
    }

    #[test]
    fn test_3x3_known_value() {
        // det([[2,1,0],[1,2,1],[0,1,2]]) = 4
        let a = Matrix::new(vec![2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 2.0], 3, 3);
        assert!(approx_eq(a.determinant(), 4.0));
        assert!(approx_eq(a.determinant_cofactor(), 4.0));
    }

    #[test]
    fn test_3x3_zero_pivot_requires_swap() {
        // det([[1,1,1],[1,1,2],[1,2,1]]) = βˆ’1; the first elimination step
        // leaves a zero pivot in column 1 that needs a row swap.
        let a = Matrix::new(vec![1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 2.0, 1.0], 3, 3);
        assert!(approx_eq(a.determinant(), -1.0));
        assert!(approx_eq(a.determinant_cofactor(), -1.0));
    }

    #[test]
    fn test_triangular_det_is_diagonal_product() {
        // Upper triangular: det = 2Β·3Β·4 = 24
        let a = Matrix::new(vec![2.0, 5.0, 7.0, 0.0, 3.0, 1.0, 0.0, 0.0, 4.0], 3, 3);
        assert!(approx_eq(a.determinant(), 24.0));
        assert!(approx_eq(a.determinant_cofactor(), 24.0));
    }

    #[test]
    fn test_identity_det_is_one() {
        for n in 1..=5 {
            assert!(approx_eq(Matrix::identity(n).determinant(), 1.0));
            assert!(approx_eq(Matrix::identity(n).determinant_cofactor(), 1.0));
        }
    }

    #[test]
    fn test_singular_det_is_zero() {
        // Two identical rows β†’ singular β†’ det = 0.
        let a = Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 2.0, 3.0], 3, 3);
        assert!(approx_eq(a.determinant(), 0.0));
        assert!(approx_eq(a.determinant_cofactor(), 0.0));
    }

    #[test]
    fn test_4x4_cofactor_matches_elimination() {
        let a = Matrix::new(
            vec![1.0, 2.0, 0.0, 1.0, 0.0, 1.0, 3.0, 0.0, 2.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 2.0],
            4,
            4,
        );
        let det_elim = a.determinant();
        let det_cof = a.determinant_cofactor();
        assert!(
            approx_eq(det_elim, det_cof),
            "elimination {} vs cofactor {}",
            det_elim,
            det_cof
        );
        // Verified with exact (rational) arithmetic: det = 16.
        assert!(approx_eq(det_elim, 16.0));
    }

    #[test]
    fn test_4x4_band_matrix_matches_elimination() {
        let a = Matrix::new(
            vec![2.0, 1.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0, 0.0, 1.0, 2.0],
            4,
            4,
        );
        let det_elim = a.determinant();
        let det_cof = a.determinant_cofactor();
        assert!(approx_eq(det_elim, det_cof));
        // det = 5 for this tridiagonal matrix (verified by cofactor).
        assert!(approx_eq(det_elim, 5.0));
    }

    #[test]
    fn test_det_of_product_equals_product_of_dets() {
        // det(AB) = det(A)Β·det(B) β€” a key property worth pinning down.
        let a = Matrix::new(vec![1.0, 2.0, 3.0, 4.0], 2, 2);
        let b = Matrix::new(vec![0.0, 1.0, 1.0, 0.0], 2, 2);
        let ab = a.multiply(&b);
        assert!(approx_eq(ab.determinant(), a.determinant() * b.determinant()));
        assert!(approx_eq(
            ab.determinant_cofactor(),
            a.determinant_cofactor() * b.determinant_cofactor()
        ));
    }

    #[test]
    #[should_panic(expected = "square")]
    fn test_non_square_elimination_panics() {
        Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).determinant();
    }

    #[test]
    #[should_panic(expected = "square")]
    fn test_non_square_cofactor_panics() {
        Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).determinant_cofactor();
    }
}

Run the tests:

cd code && cargo test -p ch07-determinants
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Walkthrough


Verification

TestWhat it checksMathematical invariant
test_1x1_det_is_the_entry1Γ—1 base case, both algorithms$\det([a]) = a$
test_2x2_ad_minus_bcKnown 2Γ—2 value$\det\begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc$
test_2x2_row_swap_flips_signSwap flips signRow-swap property
test_3x3_known_valueHand-traced 3Γ—3 cofactor expansionCofactor expansion correctness
test_3x3_zero_pivot_requires_swapZero pivot rescued by row swapPartial pivoting + sign tracking
test_triangular_det_is_diagonal_productUpper-triangular matrix$\det$ = diagonal product
test_identity_det_is_one$\mathbf{I}_n$ for n = 1..5$\det(\mathbf{I}_n) = 1$
test_singular_det_is_zeroDuplicate row β†’ 0$\det = 0 \iff$ singular
test_4x4_cofactor_matches_eliminationBoth algorithms agree; det = 16 (exact rational check)Cofactor ≑ elimination
test_4x4_band_matrix_matches_eliminationTridiagonal 4Γ—4; det = 5Cofactor ≑ elimination
test_det_of_product_equals_product_of_dets$\det(\mathbf{A}\mathbf{B})$ vs $\det(\mathbf{A})\det(\mathbf{B})$Multiplicativity
test_non_square_elimination_panicsNon-square input panicsDeterminants require square matrices
test_non_square_cofactor_panicsNon-square input panicsDeterminants require square matrices

Key Takeaways