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
Output
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 idea | Meaning |
|---|---|
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 |
| cells | n 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.