Skip to content

Diagonal matrices

A matrix that is zero off its diagonal needs only n numbers. A get function supplies the zeros, set refuses anything else off the diagonal, and a product with a vector takes n steps.

Helps to know first
This topic

First, the picture

A 4 × 4 matrix with numbers only on its diagonal. Watch the diagonal drop into a strip of 4 cells while the zeros stay behind.

Storing only the diagonal

A matrix of mostly zeros kept as a single strip

    int n;    double *d;}; bool diag_init(struct diag *m, int n){    m->n = n;    m->d = calloc((size_t)n, sizeof *m->d);    return m->d != NULL;} double get(const struct diag *m, int i, int j){    return i == j ? m->d[i] : 0.0;} bool set(struct diag *m, int i, int j, double x){    if (i != j)        return x == 0.0;    m->d[i] = x;    return true;} int main(void){    struct diag m;    if (!diag_init(&m, N))        return 1;    for (int i = 0; i < N; i++)        set(&m, i, i, i + 2);    for (int i = 0; i < N; i++) {        for (int j = 0; j < N; j++)            printf("%g ", get(&m, i, j));        printf("\n");    }    printf("stored %d of %d cells\n", N, N * N);    bool ok = set(&m, 0, 1, 9);    printf("set(0, 1, 9): %s\n", ok ? "done" : "refused");    double x[N], y[N];    for (int i = 0; i < N; i++)        x[i] = i + 1;    for (int i = 0; i < N; i++)        y[i] = m.d[i] * x[i];    printf("y =");    for (int i = 0; i < N; i++)        printf(" %g", y[i]);    printf("\n");    free(m.d);    return 0;}

A 4 × 4 matrix that is zero off its diagonal: make its store.

cells stored
4 of 16
4

Output

0.00 / 11.20 s
Describe this picture

The picture is a square grid of 4 rows and 4 columns with the row and column numbers beside it, and under it a strip of 4 boxes, the block of doubles d that the program asks calloc for. The 4 diagonal cells of the grid are drawn solid; the 12 cells off the diagonal are faint and dashed, each showing 0, because no memory holds them. Under the grid is a line of text naming the cell the program is at. Under the picture are the program diag.c, with the line about to run marked, one sentence on the current step, a readout “cells stored” reading “4 of 16”, the control, and the output. The clip plays once, about 11 seconds. The strip appears holding four zeros. Then set writes 2, 3, 4 and 5 down the diagonal, and each value flies from its grid cell down into its box. The program prints the matrix through get: the first row cell by cell, where (0, 0) reads d[0] and the other three cells are 0 with no memory read, then the other rows one at a time. Next set(0, 1, 9) lights cell (0, 1) as a warning and is refused. Last, y = Mx runs down the diagonal, y[i] = d[i] × x[i] with x = 1, 2, 3, 4, and the clip rests on the output y = 2 6 12 20. The control, “Size n”, is a slider from 2 to 8: the grid grows to n × n cells and the strip to n boxes, and the clip plays again. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as diagonal.step, and the size as diagonal.n.

Only n numbers

A diagonal matrix is zero everywhere off its diagonal: M(i, j) = 0 whenever i ≠ j. An n × n one has n² entries, but only n of them can be anything other than zero. So the program stores those n and nothing else:

struct diag {
    int n;
    double *d;
};

d points at a block of n doubles, one per diagonal entry: d[i] is M(i, i). The program asks calloc for it, so every cell starts at 0:

m->d = calloc((size_t)n, sizeof *m->d);

For n = 4 that is 4 cells of 8 bytes, 32 bytes, where the full grid would need 16 cells and 128 bytes. The other 12 entries are not stored anywhere.

The code and its cost

Reading an entry

The zeros come from the code, not from memory. get checks where the entry is:

double get(const struct diag *m, int i, int j)
{
    return i == j ? m->d[i] : 0.0;
}

On the diagonal it reads d[i]; anywhere else it returns 0 without reading memory at all. The program prints the whole matrix through get, so all 16 entries appear on screen, zeros included:

2 0 0 0
0 3 0 0
0 0 4 0
0 0 0 5
stored 4 of 16 cells

Writing an entry

set writes the diagonal and nothing else:

bool set(struct diag *m, int i, int j, double x)
{
    if (i != j)
        return x == 0.0;
    m->d[i] = x;
    return true;
}

Off the diagonal there is no cell to write. A zero is accepted, because the matrix already holds 0 there and nothing changes. Any other value is refused, and set returns false. That way the store can never disagree with the matrix it claims to be. In the clip, set(0, 1, 9) is refused and the program prints set(0, 1, 9): refused.

Multiplying by a vector

Row i of M has a single non-zero entry, d[i], in column i. So entry i of y = Mx is just d[i] × x[i]:

for (int i = 0; i < N; i++)
    y[i] = m.d[i] * x[i];

That is n multiplications, where a full matrix would take n². With x = (1, 2, 3, 4) the program prints y = 2 6 12 20. A diagonal matrix scales each coordinate by its own factor, which is what diagonalisation is looking for.

The cost

Storing a diagonal matrix takes n cells instead of n². Reading an entry and writing one take one step each. Multiplying by a vector takes n steps instead of n².

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic diag.c -o diag
./diag

The size is a macro, so gcc -DN=8 … builds the 8 × 8 version, which prints stored 8 of 64 cells and y = 2 6 12 20 30 42 56 72.

Worked example

A large matrix. For n = 1,000, the strip holds 1,000 doubles, 8,000 bytes. The full grid would hold 1,000,000 doubles, 8,000,000 bytes, of which all but 1,000 are zeros.

Two diagonal matrices multiplied. If D has diagonal d and E has diagonal e, then DE is diagonal too, with entries d[i] × e[i]. That is n multiplications, and the product needs only n cells.

Two reads. For n = 4, get(2, 2) returns d[2] = 4, and get(2, 3) returns 0 without reading memory.

Two checks. Why does set(&m, 0, 1, 0) succeed? (Entry (0, 1) is already 0, so nothing changes.) How many cells does an 8 × 8 diagonal matrix store? (8, against 64.)

Where it’s used

Gains on channels. Scaling each channel of a signal by its own gain is multiplying by a diagonal matrix, one multiplication per channel.

Weighted fits. A weighted least-squares fit gives each measurement a weight; the weights sit on the diagonal of a matrix that is never stored in full.

Matrix libraries. Numerical libraries treat diagonal matrices as a special case, storing only the diagonal and skipping the zeros in every product.

Reference card

Code or ideaMeaning
struct diag { int n; double *d; }n doubles for an n × n diagonal matrix
get(i, j)d[i] when i = j, 0 otherwise, with no memory read
set(i, j, x)writes d[i] on the diagonal; off it, accepts only 0
y[i] = d[i] × x[i]y = Mx in n multiplications
cellsn instead of n²

Next, triangular and symmetric matrices store half a grid with a formula for each entry’s place.

Platform: C11, gcc on x86-64 Linux: int 4 bytes, double 8, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look