The Crank–Nicolson Method
The
explicit
scheme for the
heat
equation is a delight to program — one line of arithmetic per grid point — but it comes
with a leash. As
the
stability analysis shows, it is only conditionally stable: the mesh
ratio must obey r = \alpha\,\Delta t/\Delta x^2 \le \tfrac12. Refine the
spatial grid to chase some fine detail — halve \Delta x — and the largest
legal time step drops by a factor of four, because the bound scales with
\Delta x^2. On a genuinely fine grid you can be forced into millions of
absurdly tiny steps just to keep the arithmetic from exploding.
The cure is to stop computing the new values one at a time and instead solve for the whole new time
level at once. Schemes that do this are implicit, and the most celebrated
member of the family — the workhorse of practical diffusion solvers since the 1940s — is the
Crank–Nicolson method. It buys something remarkable: it is
unconditionally stable (no cap on \Delta t at all) and,
at the same time, second-order accurate in both space and time. The price is a small
linear system solved once per step — and, as we will see, that system is
tridiagonal, so the cost is almost as cheap as the explicit rule anyway.
One dial: the θ-method
The explicit and implicit schemes are not really two separate ideas — they are the two ends of a
single dial. Start from the heat equation u_t = \alpha\,u_{xx} and, as
always, replace the time derivative by a forward difference between level
n and level n+1. The only question is
where to evaluate the spatial second difference
\delta^2 u_i = u_{i+1} - 2u_i + u_{i-1}
— at the old level, the new level, or somewhere in between? The θ-method refuses to
choose and takes a weighted average, with a single knob
\theta \in [0,1]:
\frac{u_i^{n+1} - u_i^n}{\Delta t}
= \frac{\alpha}{\Delta x^2}\Big[\,\theta\,\delta^2 u_i^{n+1}
\; + \; (1-\theta)\,\delta^2 u_i^{n}\,\Big].
Turning the dial recovers three classic schemes:
-
\theta = 0 — the whole spatial term sits at the old level: the
familiar explicit (forward-time) scheme, conditionally stable.
-
\theta = 1 — the whole term sits at the new level: the
fully implicit (backward-time) scheme, unconditionally stable but only
first-order accurate in time.
-
\theta = \tfrac12 — an even split between old and new:
Crank–Nicolson. Averaging the two one-sided differences is exactly the
trapezoidal rule in time, and that symmetry is what lifts the time accuracy to second
order.
So Crank–Nicolson is not an exotic new construction — it is simply the θ-method with the dial set
dead-centre, averaging the old and new spatial curvatures.
The update: a tridiagonal system
Set \theta = \tfrac12 and write the mesh ratio
r = \alpha\,\Delta t/\Delta x^2. Multiplying through by
\Delta t and gathering every new-level value on the left,
every old-level value on the right, gives the Crank–Nicolson update at each interior
point i:
-\tfrac{r}{2}\,u_{i-1}^{n+1} + (1+r)\,u_i^{n+1} - \tfrac{r}{2}\,u_{i+1}^{n+1}
\;=\; \tfrac{r}{2}\,u_{i-1}^{n} + (1-r)\,u_i^{n} + \tfrac{r}{2}\,u_{i+1}^{n}.
The right-hand side is entirely known — just old numbers combined with arithmetic, exactly like the
explicit scheme. The left-hand side is where the difference lives: three unknown new values
are tied together in one equation. Writing this equation at every interior grid point stacks up into a
linear system
A\,\mathbf{u}^{n+1} = \mathbf{d},
where \mathbf{d} collects the known right-hand sides (plus the boundary
values) and A is tridiagonal: the constant
1+r runs down the main diagonal, and
-\tfrac{r}{2} sits on the two neighbouring diagonals — nothing anywhere
else. That special shape matters enormously: a tridiagonal system of
M unknowns is solved by the Thomas algorithm in
O(M) work — one forward sweep and one back-substitution — so an implicit
step costs only a small constant multiple of an explicit one, not the
O(M^3) of a general matrix solve.
-
Average the spatial second difference over the old and new time levels:
-\tfrac{r}{2}u_{i-1}^{n+1} + (1+r)u_i^{n+1} - \tfrac{r}{2}u_{i+1}^{n+1}
= \tfrac{r}{2}u_{i-1}^{n} + (1-r)u_i^{n} + \tfrac{r}{2}u_{i+1}^{n},
with r = \alpha\,\Delta t/\Delta x^2.
-
Each step solves one tridiagonal system
A\mathbf{u}^{n+1}=\mathbf{d},
A = \operatorname{tridiag}(-\tfrac{r}{2},\,1+r,\,-\tfrac{r}{2}),
in O(M) work via the Thomas algorithm.
-
It is second-order accurate in both space and time:
error = O(\Delta x^2) + O(\Delta t^2).
-
It is unconditionally stable: the von Neumann amplification factor satisfies
|G| \le 1 for every mode and every choice of
\Delta t and \Delta x.
Why it is unconditionally stable
Run the same von
Neumann test as before: feed in a single Fourier mode
u_i^n = G^n e^{\mathrm{i} k x_i} and ask by how much one step
multiplies it. Substituting into the Crank–Nicolson update and writing
s = \sin^2\!\big(\tfrac{k\,\Delta x}{2}\big), the exponentials cancel and
leave the amplification factor
G = \frac{1 - 2r\,s}{1 + 2r\,s}.
Here is the whole argument in one line. Because r > 0 and
s \ge 0, the quantity 2rs is never negative, so
the denominator 1 + 2rs is always at least as large in size as the
numerator 1 - 2rs:
|1 - 2r s| \le 1 + 2r s \quad\Longrightarrow\quad |G| \le 1 \quad\text{for every mode } k.
No condition on r appears anywhere — the inequality holds for
r = 0.5, for r = 50, for
r = 5000. Contrast the explicit scheme, whose factor
G = 1 - 4r s sails past -1 the moment
r > \tfrac12. Crank–Nicolson simply cannot blow up. (Compare the fully
implicit scheme, G = 1/(1+4rs), which is also unconditionally stable but
loses the second-order time accuracy.)
One step, in code
The entire method is: build the known right-hand side from the old row, then solve the tridiagonal
system for the new row. Because the matrix is tridiagonal, the solve is the classic
Thomas algorithm — a forward sweep to eliminate the sub-diagonal, then a
back-substitution. Here is one Crank–Nicolson step on the interior points, with the ends held at fixed
(Dirichlet) boundary values:
// One Crank–Nicolson step on interior points 1 .. N-1 (Dirichlet ends held fixed).
// Solve A u^{n+1} = d, where A = tridiag(-r/2, 1+r, -r/2).
function crankNicolsonStep(u: number[], r: number): number[] {
const N = u.length - 1;
const a = -r / 2, b = 1 + r, c = -r / 2; // sub-, main-, super-diagonal
// Right-hand side d[i], built entirely from the OLD level u.
const d: number[] = [];
for (let i = 1; i < N; i++)
d[i] = (r / 2) * u[i - 1] + (1 - r) * u[i] + (r / 2) * u[i + 1];
// Thomas algorithm — forward sweep (modified super-diagonal cp, RHS dp).
const cp: number[] = [], dp: number[] = [];
cp[1] = c / b;
dp[1] = d[1] / b;
for (let i = 2; i < N; i++) {
const m = b - a * cp[i - 1];
cp[i] = c / m;
dp[i] = (d[i] - a * dp[i - 1]) / m;
}
// Back-substitution into the new row v (boundaries v[0], v[N] unchanged).
const v = u.slice();
v[N - 1] = dp[N - 1];
for (let i = N - 2; i >= 1; i--) v[i] = dp[i] - cp[i] * v[i + 1];
return v;
}
Notice there is no stability check anywhere — no test that r \le \tfrac12,
because none is needed. You are free to pick \Delta t purely for
accuracy, which is exactly the freedom the explicit scheme denies you.
Watch the amplification factor
This is the whole stability story on one picture. The horizontal axis is
k\,\Delta x, sweeping from the smoothest mode (left) to the jagged
highest-frequency mode k\,\Delta x = \pi (right); the vertical axis is the
size |G| of the amplification factor. Anything above the dashed
|G| = 1 line grows each step — an instability. Drag the mesh ratio
r upward: the explicit curve
|1 - 4r\,s| lifts its right end clean over the line the instant
r passes 0.5, while the Crank–Nicolson curve
\big|(1-2r s)/(1+2r s)\big| stays pinned at or below
1 no matter how far you push — visibly, permanently safe.
The method is named for two British mathematicians, John Crank (1916–2006) and
Phyllis Nicolson (1917–1968), who published it in 1947. Their goal
was thoroughly practical: solving the heat-conduction equation numerically at a time when a
"computer" was often still a person with a mechanical desk calculator, and the earliest
electronic machines were rare, temperamental, and desperately slow.
The explicit scheme's tiny stability-limited steps made such calculations agonisingly long by hand.
Crank and Nicolson's insight — accept a bit more work per step (a tridiagonal solve) in exchange for
removing the step-size limit entirely — turned an impractical multi-day computation into a feasible
one. Nicolson, notably, did much of the numerical testing that showed the scheme actually behaved as
the algebra promised. Nearly eighty years later their averaging trick is still the default
time-stepper in countless diffusion, option-pricing, and reaction–diffusion codes.
Unconditional stability is not the same as accuracy. "Stable" only promises that
errors will not explode — it says nothing about whether the answer is any good. Because
Crank–Nicolson lets you take an arbitrarily large \Delta t without blowing
up, it is tempting to crank the step way up to save time. But the time-truncation error is still
O(\Delta t^2): too large a step quietly smears out sharp features
and lags fast transients, giving a stable-but-wrong result. Stability keeps the numbers finite;
accuracy is a separate budget you still have to pay.
There is a second, subtler trap. For the roughest mode, the amplification factor tends to
G \to -1 as r grows large. A factor near
-1 is stable (its size is 1) but it
barely damps and it flips sign every step. So on discontinuous or
sharp-cornered initial data — a step profile, a hot spot switched on instantly — Crank–Nicolson with a
big \Delta t can show persistent, slowly-decaying
oscillations (ringing) near the jump, even though nothing is technically unstable. If
you see that, shrink \Delta t, or start with a couple of strongly-damping
fully-implicit steps (the "Rannacher smoothing" trick) before switching to Crank–Nicolson.