Skip to content

Sparse matrices as linked lists

An array of row heads, each with a sorted chain of (column, value) nodes, lets non-zeros be inserted, changed and removed without shifting anything.

Helps to know first
This topic

First, the picture

Four row heads, each with a chain of nodes sorted by column. Watch seven updates splice nodes in, change one, and unlink one, with nothing shifted.

A matrix made of chains

Seven updates to a 4 × 4 matrix kept as sorted row chains

struct entry {    int col;    double val;    struct entry *next;}; static struct entry *rows[N]; void set(int i, int j, double x){    struct entry **link = &rows[i];    while (*link != NULL && (*link)->col < j)        link = &(*link)->next;    if (*link != NULL && (*link)->col == j) {        if (x == 0.0) {            struct entry *dead = *link;            *link = dead->next;            free(dead);        } else {            (*link)->val = x;        }        return;    }    if (x == 0.0)        return;    struct entry *e = malloc(sizeof *e);    if (e == NULL)        exit(1);    e->col = j;    e->val = x;    e->next = *link;    *link = e;}int main(void){    set(0, 2, 5);    set(0, 0, 1);    set(1, 3, 2);    set(0, 1, 4);    set(0, 2, 0);    set(1, 3, 6);    set(3, 0, 7);    for (int i = 0; i < N; i++) {

Seven calls of set on an empty 4 × 4 matrix.

Output

0.00 / 19.20 s
Describe this picture

The picture has two parts, side by side on a wide screen and one above the other on a phone. On the left is a column of four heads, rows[0] to rows[3], under the name static struct entry *rows[4]; each head has its chain of nodes running to the right, joined by arrows, and each node shows its column and value, as 2:5. Over the heads is a lane with the current call. On the right is the 4 × 4 matrix the chains stand for, with every cell not in a chain showing 0. Under the picture are the lines of chains.c the clip runs, with the line about to run marked, one sentence on the current step, and the output. The clip plays once, about 19 seconds. Each call of set lights its row’s head and its cell in the matrix. set(0, 2, 5) puts a node at 0x1000 into row 0’s empty chain. set(0, 0, 1) puts its node, at 0x1020, in front of it, since column 0 comes first; set(1, 3, 2) starts row 1’s chain; set(0, 1, 4) walks past the column-0 node, lighting it, and splices its node, at 0x1060, between columns 0 and 2. Then set(0, 2, 0) walks past columns 0 and 1, finds column 2, unlinks that node, and frees it: row 0’s chain closes up and the matrix cell becomes 0. set(1, 3, 6) finds column 3 and changes its value in place, toned. Last, set(3, 0, 7) gets the block at 0x1000 again for its node. The chains print as row 0: 0:1 1:4 to row 3: 0:7, the matrix prints through get, and every node is freed and each head set back to NULL, leaving four empty heads. With reduced motion the clip rests on the printed matrix, before the nodes are freed. Watch only: there is no control. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as chains.step.

A chain per row

CSR packs the entries tightly, but a new entry in the middle means shifting every entry after it. When the pattern of non-zeros keeps changing, chains do better. Each row gets a head, a pointer to the first node of a linked chain, and each node holds one entry:

struct entry {
    int col;
    double val;
    struct entry *next;
};

static struct entry *rows[N];

A node is 24 bytes, and malloc gives it a 32-byte block. The heads are statics, so they start as NULL: an empty matrix. Each chain is kept sorted by column. A search can then stop at the first column larger than the one it wants, and the chain prints in column order.

The code and its cost

set: three cases

set(i, j, x) walks row i’s chain with a pointer to a link, as deleting from a list does: link points at the head or at the next field of a node, so changing *link changes whichever pointer leads to the next node.

struct entry **link = &rows[i];
while (*link != NULL && (*link)->col < j)
    link = &(*link)->next;

The walk stops at the first node whose column is j or more, or at the end. Then there are three cases.

The entry exists, and *link is the column-j node. If x is 0, the node is unlinked and freed. Setting an entry to zero does not store a zero; it removes the node:

if (x == 0.0) {
    struct entry *dead = *link;
    *link = dead->next;
    free(dead);
} else {
    (*link)->val = x;
}

Otherwise the value changes in place.

The entry is missing, and x is 0. The matrix already has 0 there, so nothing happens.

The entry is missing, and x is not 0. A new node is spliced in where the walk stopped, before the first larger column:

struct entry *e = malloc(sizeof *e);
e->col = j;
e->val = x;
e->next = *link;
*link = e;

Because link may point at the head or at a node’s next, the same two lines insert a new first entry and a middle one. No other node moves.

The run

The program makes seven calls on an empty 4 × 4 matrix:

CallWhat happensBlock
set(0, 2, 5)new node, row 0’s first0x1000
set(0, 0, 1)new node, in front of column 20x1020
set(1, 3, 2)new node, row 1’s first0x1040
set(0, 1, 4)new node, between columns 0 and 20x1060
set(0, 2, 0)unlinked and freed0x1000 freed
set(1, 3, 6)value changed in place
set(3, 0, 7)new node, row 3’s first0x1000 again

The last node gets the block that set(0, 2, 0) gave back: on this platform, glibc’s malloc hands out a recently freed block of the same size first. C does not promise this; another allocator may return any free block. The program prints the chains and then the matrix through get:

row 0: 0:1 1:4
row 1: 3:6
row 2:
row 3: 0:7
1 4 0 0
0 0 0 6
0 0 0 0
7 0 0 0

At the end, main frees each row’s nodes and then sets rows[i] = NULL. Without that line the heads of rows 0, 1 and 3 would still hold the addresses of freed blocks: dangling pointers, which any later get or set would follow into memory the program no longer owns.

get

get walks the chain and stops as soon as the columns pass j:

for (const struct entry *e = rows[i]; e != NULL && e->col <= j; e = e->next)
    if (e->col == j)
        return e->val;
return 0.0;

Against CSR

Chains never shift: an insertion or a removal changes one or two pointers once the place is found. But finding the place means following pointers from node to node, which is slower than reading neighbouring cells of an array, and each entry takes a 32-byte block, where CSR takes 12 bytes (a double and an int). Reaching an entry costs a walk along its row, as long as the row’s chain.

A fuller version gives each node a second pointer and links the columns too, with an array of column heads, so that a column can be walked as easily as a row.

Running it yourself

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

Worked example

Appending. On the final state, set(0, 3, 9) walks past columns 0 and 1 and reaches the end of row 0’s chain, so its node goes after the column-1 node.

Memory. Ten non-zeros in an 8 × 8 matrix take 10 blocks of 32 bytes, 320 bytes, plus 8 heads of 8 bytes, 64 bytes: 384 bytes. CSR takes 29 numbers, 10 doubles and 19 ints, 156 bytes.

Nothing to do. set(2, 1, 0) on the empty row 2 finds no entry and x is 0, so it returns without allocating.

Two checks. Why is each chain sorted? (A search can stop at the first larger column, and printing goes in column order.) Which block does (3, 0) get? (0x1000, freed when (0, 2) was set to zero, under glibc’s malloc.)

Where it’s used

Elimination. Solving a sparse system by elimination creates new non-zeros, called fill-in, as it goes; chains take them without shifting.

Assembly. Simulations often build a sparse matrix entry by entry, in no particular order, in a linked form, and convert it to CSR once it is complete, for fast products.

Reference card

Code or ideaMeaning
struct entry *rows[N]one head per row; NULL is an empty row
chain sorted by columnsearches stop early; printing in order
struct entry **linkpoints at the head or a next: one code path for every position
set to 0unlinks and frees the node
32-byte block per entryagainst 12 bytes in CSR, but no shifting

Next, polynomials: storing a polynomial keeps its terms the same two ways, in an array or in a chain.

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