16.1. Matrix Storage and Design#
16.1.1. Sparse, Structured, and Dense Matrices#
In numerical computation, we often encounter matrices that have special properties. We can broadly classify them into three types:
A sparse matrix is a matrix with enough zero-valued entries that it is computationally efficient to store and manipulate the matrix by taking advantage of these zeros. [Wilkinson]
A structured matrix has entries that follow a predictable mathematical pattern, which can be exploited for storage and computation (e.g., Toeplitz, Circulant, or Diagonal matrices).
A dense matrix is neither sparse nor structured, meaning most of its entries are non-zero and do not follow a simple pattern. These are stored as standard 2D arrays.
16.1.2. Design Goals for Sparse Matrices#
When designing sparse matrix formats and libraries, we have several (sometimes conflicting) goals:
Usability: Most operations (
*,\,+, etc.) should give the same results for sparse and dense matrices. The type should propagate automatically (e.g., sparse + dense = dense).Simplicity & Robustness: The interface should be easy to use and handle edge cases (like all-zero matrices) gracefully.
Storage Cost: The memory required should be proportional to the number of non-zeros, \(\text{nnz}\). We write this as \(O(\text{nnz})\).
Performance: The time for sparse operations (like matrix-vector multiply) should be proportional to the number of non-zeros, \(O(\text{nnz})\), not the dense size, \(O(n^2)\).
16.1.3. Common Data Structures for Sparse Matrices#
When we move from a dense Array to a sparse format, we must decide how to store only the non-zero values. As a running example, let’s consider how to store the following 5x5 matrix, which has 10 non-zero entries (nnz=10).
16.1.3.1. 1. Full (Dense) Storage#
Used in the standard
Arraytype.Stores all matrix entries, including the zeros.
Memory Usage: \(O(m \cdot n)\) for an \(m \times n\) matrix.
16.1.3.2. 2. List of Lists (LIL)#
Store only the non-zero values, organized by row or by column.
This example shows a column-oriented LIL:
Column 1 -> Rows [1,3,4], Values [5,-2,-4]
Column 2 -> Rows [2], Values [5]
Column 3 -> Rows [1,3], Values [-3,-1]
Column 4 -> Rows [1,4], Values [-2,-10]
Column 5 -> Rows [1,5], Values [7,9]
Pro: Very easy to insert new elements (good for incremental matrix construction).
Pro: This is essentially the adjacency list format for the graph represented by the matrix.
Con: Can be inefficient for matrix-vector products compared to other formats, as it involves many small, separate lists.
Memory Usage: \(O(\text{nnz} + n)\) for a column-oriented format with \(n\) columns (one list per column, plus storage for the non-zeros).
16.1.3.3. 3. Coordinate List (COO)#
Store three lists of length \(\text{nnz}\): one for row indices (
I), one for column indices (J), and one for the values (V).
(Row, Col, Val)
[1, 1, 5]
[3, 1, -2]
[4, 1, -4]
[2, 2, 5]
[1, 3, -3]
[3, 3, -1]
[1, 4, -2]
[4, 4, -10]
[1, 5, 7]
[5, 5, 9]
Pro: Also very simple and easy to construct incrementally (just append to the three lists).
Con: Must be sorted or converted to another format (like CSC) for efficient arithmetic or look-ups. Operations on unsorted COO are very slow.
Memory Usage: \(O(\text{nnz})\) (specifically, \(3 \times \text{nnz}\) storage).
16.1.3.4. 4. Compressed Sparse Column (CSC)#
This is the most common format for high-performance computing. It is essentially the COO format, but with the column indices compressed.
Instead of storing the column index for every non-zero, we store three arrays:
nzval: A list of all non-zero values, ordered column by column.rowval: A list of the row indices for each corresponding value innzval.colptr: An array of \(n+1\) pointers (indices) that tell us where each column starts in thenzvalandrowvalarrays.
nzval = [5,-2,-4, 5, -3,-1, -2,-10, 7, 9] # Non-zero values
rowval = [1, 3, 4, 2, 1, 3, 1, 4, 1, 5] # Row indices for each value
colptr = [1, 4, 5, 7, 9, 11] # Pointers to the start of each column
The
colptrarray contains \(n+1\) entries. The non-zeros for columnkare stored innzvalandrowvalfrom indexcolptr[k]up tocolptr[k+1]-1.This design means
colptr[1]is always1, andcolptr[n+1]is alwaysnnz+1. The number of elements in columnkis simplycolptr[k+1] - colptr[k].Pro: Highly efficient for arithmetic operations, column slicing, and matrix-vector products.
Con: Very inefficient for incremental construction (inserting a new non-zero requires rebuilding large parts of the arrays).
Con: Element look-up \(A[i,j]\) is relatively slow, \(O(\log(\text{nnz}_j))\) (where \(\text{nnz}_j\) is the number of non-zeros in column \(j\)), as you must search the column.
Memory Usage: \(O(\text{nnz} + n)\).
Note: This is the format used by Julia’s
SparseMatrixCSCtype and many other libraries (e.g., in Python’sscipy.sparse). A similar Compressed Sparse Row (CSR) format is also common.