Back to Data Structures

Eigenvalue Methods

October 16, 2026 Wasil Zafar 40 min read

Power Iteration · Inverse Iteration · QR Algorithm

Table of Contents

  1. Why Eigenvalue Methods Matters
  2. Core Concepts and Terminology
  3. How Eigenvalue Methods Works, Step by Step
  4. Worked Example with a Full Trace
  5. Implementation in Python, C++, and Java
  6. Complexity, Correctness, and Invariants
  7. Variants, Pitfalls, and Optimizations
  8. Practice Problems and Real-World Use

Why Eigenvalue Methods Matters

A matrix describes how a system transforms a vector. Most vectors change direction, but some special directions are only stretched or reversed. These are eigenvectors, and their stretch factors are eigenvalues. They reveal dominant directions in data, vibration modes in mechanical models, and growth or decay in repeated linear transformations.

Different questions call for different methods. Power iteration seeks a dominant direction by repeated multiplication. Inverse iteration uses shifted linear solves to favor an eigenvalue near a chosen target. The QR algorithm changes coordinates repeatedly to expose a whole spectrum. Understanding their convergence conditions is as important as implementing their loops.

This lesson explains the broader ideas, then scopes its complete programs to real symmetric matrices. Such matrices have real eigenvalues and an orthonormal eigenbasis, making the geometry and residual checks especially clear. The implementations include iteration limits, pivoted solves for inverse iteration, and an unshifted Householder QR algorithm. They explicitly report nonconvergence rather than assuming every input will work.

Eigenvalue methods search for directions satisfying $Av=\lambda v$. Multiplication emphasizes large-magnitude eigenvalues, a shifted inverse emphasizes nearby eigenvalues, and orthogonal similarity transformations preserve the spectrum while making its structure easier to read.

Core Concepts and Terminology

An eigenpair consists of a scalar $\lambda$ and a nonzero vector $v$ with $Av=\lambda v$. Multiplying $v$ by any nonzero scalar gives the same eigendirection, so algorithms normalize their iterates. Negative eigenvalues can reverse a vector's sign on every multiplication without preventing convergence of its direction.

For a nonzero real vector $x$, the Rayleigh quotient is $\theta(x)=x^TAx/(x^Tx)$. It estimates an eigenvalue, but the estimate alone is insufficient. Always inspect the eigenpair residual $r=Ax-\theta(x)x$. Our nonzero-matrix stopping measure is

$$ \rho(x)=\frac{\lVert Ax-\theta(x)x\rVert_2} {\lVert A\rVert_F\lVert x\rVert_2}, \qquad \lVert A\rVert_F=\sqrt{\sum_{i,j}A_{ij}^2}. $$

The code evaluates the equivalent expression after uniformly scaling $A$, using hypot for norms. The zero matrix is treated separately through a reference value of $1$; every nonzero vector is then an exact eigenvector with eigenvalue zero.

A dominant eigenvalue means largest magnitude, not necessarily largest algebraic value. If $\lvert\lambda_1\rvert\gt\lvert\lambda_2\rvert$ and the start has a nonzero component in the first eigendirection, power iteration tends toward that direction. If this component is absent, another eigenpair can have zero residual while the intended dominant target is missed.

For symmetric $A$, write $A=V\Lambda V^T$, with orthogonal $V$. A general real nonsymmetric matrix can instead have complex eigenvalues, nonorthogonal eigenvectors, or no full eigenbasis. The code rejects nonsymmetric inputs; it is not a general real-Schur solver.

Eigenvectors, spectrum, and shift targetMatrix two,one;one,two has eigenvalue three in direction one,one and eigenvalue one in direction one,negative one. Shift one point five is distance one half from one and one point five from three. Power, inverse, and QR answer different target questions.Three questions about the same two eigendirectionsA stretches [1, 1] by 3 and leaves [1, −1] unchanged.A2112A × [1, 1] = [3, 3] = 3 × [1, 1]Power iteration targets this dominant magnitude.A × [1, −1] = [1, −1]Inverse iteration can favor this other direction.Spectrum and inverse shift, drawn to scale:λ = 1μ = 1.5λ = 3distance 0.5distance 1.5Power: largest magnitude. Inverse: nearest to shift.QR: expose both values through orthogonal changes of basis.
TermMeaningRole or example
Eigenpair $(\lambda,v)$$Av=\lambda v$ with $v\ne0$For rows $(2,1),(1,2)$, direction $(1,1)^T$ has eigenvalue $3$
Spectral radius$\max_i\lvert\lambda_i\rvert$Equals $3$ in the sample
Rayleigh quotient $\theta(x)$$x^TAx/(x^Tx)$First power update gives $14/5$
Residual $r$$Ax-\theta x$Tests an approximate eigenpair against original $A$
Relative residual $\rho$Residual normalized by $\lVert A\rVert_F\lVert x\rVert_2$The program stops when $\rho\le10^{-10}$
Magnitude gapSeparation between the largest eigenvalue magnitudesPower's sample direction error decreases at a factor near $1/3$ per step
Shift $\mu$Target used in $A-\mu I$$\mu=3/2$ favors eigenvalue $1$ over $3$
Inverse iterationRepeated solves with $A-\mu I$, followed by normalizationFactor the shifted matrix once for a fixed shift
Orthogonal similarity$T_{k+1}=Q_k^TT_kQ_k$Preserves all eigenvalues in exact arithmetic
Accumulated basis $Z_k$Product of QR orthogonal factorsIts columns approach eigenvectors when the symmetric iterate becomes diagonal
Iteration budgetMaximum number of update stepsReaching it without the residual test passing is reported as failure

How Eigenvalue Methods Works, Step by Step

Power iteration. Start with a nonzero vector and normalize it. At every check, compute its Rayleigh quotient and residual. If the residual passes, return the approximate pair. Otherwise calculate $y=Ax$, normalize $x\leftarrow y/\lVert y\rVert_2$, and repeat within the budget. An exactly zero product needs care: if the current vector lies in the nullspace, its zero eigenpair should already pass the residual test.

For symmetric $A$, expanding $x_0=\sum_i c_iv_i$ gives $A^kx_0=\sum_i c_i\lambda_i^kv_i$. Relative to a unique largest-magnitude eigenvalue, other components decay like $(\lambda_i/\lambda_1)^k$, provided $c_1\ne0$. This is a convergence condition, not a promise for every start or every spectrum.

Inverse iteration with a fixed shift. Choose finite $\mu$ such that $A-\mu I$ is nonsingular. Factor this matrix once with partial row pivoting. At each update, solve

$$ (A-\mu I)y=x, \qquad x\leftarrow y/\lVert y\rVert_2. $$

The transformed eigenvalues are $1/(\lambda_i-\mu)$. A uniquely nearest eigenvalue is therefore dominant in the transformed problem, assuming the starting component is present. The Netlib inverse-iteration reference explains this shift-and-solve construction and why an explicit inverse is unnecessary. Our code estimates the final eigenvalue with the Rayleigh quotient of original $A$, not by reporting a transformed inverse eigenvalue.

The packed LU helper swaps entire rows, including earlier stored multipliers, and records their final order. A shifted pivot at most $10^{-14}$ times the shifted matrix's initial maximum entry magnitude is rejected. A shift too close to an eigenvalue can trigger this threshold; changing the shift requires a new factorization.

Unshifted QR iteration. Initialize $T_0=A$ and $Z_0=I$. Repeatedly compute an unpivoted factorization and reverse its factors:

$$ T_k=Q_kR_k, \qquad T_{k+1}=R_kQ_k, \qquad Z_{k+1}=Z_kQ_k. $$

Since $R_k=Q_k^TT_k$, each update is an orthogonal similarity. For symmetric inputs, a diagonal limit exposes the eigenvalues, and accumulated columns of $Z_k$ supply their eigenvectors. Our code checks each accumulated column against original $A$ using its Rayleigh quotient, rather than relying on unchanged diagonal entries alone. On success, it sorts the eigenvalues and moves the corresponding eigenvector columns with them.

The QR helper uses Householder reflectors with an opposite-sign target to avoid cancellation. It skips an exactly zero tail, allowing a factorization even when the matrix is singular. Do not substitute column-pivoted least-squares QR and blindly use $RQ$: the additional permutation changes the similarity argument.

The Cornell numerical-analysis notes on QR iteration connect this process to orthogonal iteration and discuss the need for shifts and structural reduction in practical solvers. The simple unshifted version here can stagnate or cycle.

First complete power iteration stageStarting vector one,zero becomes raw product two,one and normalizes by square root five. Its Rayleigh quotient is fourteen fifths. Residual negative three,six divided by five square root five has norm three fifths, so the relative residual is about zero point one eight nine seven three seven.One complete power update and its residual checkBegin at x₀ = [1, 0]. Norm of A is √10 in Frobenius units.Current unit vectorx₀ = [1, 0]θ₀ = 2Multiply by Ay₁ = [2, 1]norm(y₁) = √5Normalizex₁ = [2, 1] / √5norm(x₁) = 1→→A x₁ = [5, 4] / √5; Rayleigh quotient θ₁ = 14/5 = 2.8.Residual = [−3, 6] / (5√5); absolute norm = 3/5.Relative residual = (3/5) / √10 ≈ 0.189737.This is above the hand-run tolerance 0.025: continue iterating.Normalize to control size; use the residual to decide when to stop.
Eigenvalue Methods control flow
flowchart TD
    A["Validate symmetric A, tolerance, and update budget"] --> B{"Method?"}
    B -->|Power| C["Normalize starting vector"]
    B -->|Inverse| D["Factor shifted matrix with partial row pivoting"]
    D --> C
    C --> E{"Original-matrix eigenpair residual passes?"}
    E -->|Yes| F["Return approximate eigenpair"]
    E -->|No| G{"Budget remains?"}
    G -->|No| H["Report nonconvergence"]
    G -->|Yes| I["Multiply or shifted-solve; normalize"]
    I --> E
    B -->|QR| J["Initialize T and accumulated orthogonal basis"]
    J --> K{"All basis-column residuals pass?"}
    K -->|Yes| L["Sort eigenpairs and verify orthogonality"]
    K -->|No| M{"Budget remains?"}
    M -->|No| H
    M -->|Yes| N["Unpivoted QR; T becomes R Q; accumulate Q"]
    N --> K

Worked Example with a Full Trace

Use

$$ A=\begin{pmatrix}2&1\\1&2\end{pmatrix}, \qquad x_0=(1,0)^T, \qquad \mu=3/2. $$

Direct multiplication verifies eigenpairs $\lambda_+=3$, $v_+=(1,1)^T/\sqrt2$ and $\lambda_-=1$, $v_-=(1,-1)^T/\sqrt2$. The Frobenius norm is $\sqrt{10}$.

For a complete short hand run, set relative tolerance $\tau=0.025$ and an update budget of $3$. The programs later use the tighter $10^{-10}$ tolerance. We show all three updates and all intermediate iterates for this hand run. Signs of eigenvectors are immaterial.

Power iteration. The first raw product is $(2,1)^T$, with norm $\sqrt5$, so $x_1=(2,1)^T/\sqrt5$. Then $Ax_1=(5,4)^T/\sqrt5$, giving $\theta_1=14/5$ and residual norm $3/5$. The next raw product normalizes to $x_2=(5,4)^T/\sqrt{41}$, and the third to $x_3=(14,13)^T/\sqrt{365}$.

Inverse iteration setup. The shifted matrix is

$$ B=A-(3/2)I=\begin{pmatrix}1/2&1\\1&1/2\end{pmatrix}. $$

Partial pivoting swaps its rows. Its packed factors represent lower rows $(1,0),(1/2,1)$ and upper rows $(1,1/2),(0,3/4)$. Solving $By=x_0$ gives $y_1=(-2/3,4/3)^T$, so the normalized vector is $u_1=(-1,2)^T/\sqrt5$. Reusing the same factors gives

$$ y_2=\frac{(10,-8)^T}{3\sqrt5}, \quad u_2=\frac{(5,-4)^T}{\sqrt{41}}, \qquad y_3=\frac{(-26,28)^T}{3\sqrt{41}}, \quad u_3=\frac{(-13,14)^T}{\sqrt{365}}. $$

The desired inverse eigenvalue of $B^{-1}$ is negative, so these vector signs alternate. Their directions converge toward the eigenspace for original eigenvalue $1$.

The following table records every vector state and check for the hand run. The absolute residual norms are the same for both methods; divide by $\sqrt{10}$ to obtain the last column.

Update $k$Power vector $x_k$Power $\theta_k$Inverse vector $u_k$Inverse $\theta_k$Absolute residual normRelative residual
$0$$(1,0)^T$$2$$(1,0)^T$$2$$1$$0.316228$
$1$$(2,1)^T/\sqrt5$$14/5$$(-1,2)^T/\sqrt5$$6/5$$3/5$$0.189737$
$2$$(5,4)^T/\sqrt{41}$$122/41$$(5,-4)^T/\sqrt{41}$$42/41$$9/41$$0.069416$
$3$$(14,13)^T/\sqrt{365}$$1094/365$$(-13,14)^T/\sqrt{365}$$366/365$$27/365$$0.023392$

Both vector methods first pass $\tau=0.025$ at update $3$. Their returned eigenvalues are approximations near $3$ and $1$, not exact answers merely because the check passed.

QR iteration. An unpivoted Householder factorization of $T_0=A$ gives

$$ Q_0=\frac1{\sqrt5}\begin{pmatrix}-2&1\\-1&-2\end{pmatrix}, \qquad R_0=\begin{pmatrix}-\sqrt5&-4/\sqrt5\\0&-3/\sqrt5\end{pmatrix}. $$

Reversing the factors and repeating produces all hand-run matrix states:

$$ T_1=\begin{pmatrix}14/5&3/5\\3/5&6/5\end{pmatrix}, \qquad T_2=\begin{pmatrix}122/41&9/41\\9/41&42/41\end{pmatrix}, \qquad T_3=\begin{pmatrix}1094/365&27/365\\27/365&366/365\end{pmatrix}. $$

Up to column signs, the accumulated basis is $Z_k=\begin{pmatrix}a_k&b_k\\b_k&-a_k\end{pmatrix}/\sqrt{a_k^2+b_k^2}$, with $(a_k,b_k)=(1,0),(2,1),(5,4),(14,13)$ for $k=0,1,2,3$. Thus all accumulated eigenvector states are specified too. Each column has the same relative residual listed in the table, so QR also passes the loose check after three updates. Every $T_k$ has trace $4$ and determinant $3$, preserving the spectrum.

Tighter program run. Let $q=3^k$. For these three runs in exact arithmetic, the residual norm is $2q/(q^2+1)$. At update $20$, the relative residual is about $1.81\times10^{-10}$; at update $21$, it is about $6.05\times10^{-11}$. Therefore the programs first pass $10^{-10}$ after $21$ updates. They scale $A$ by $2$ internally and scale the shift by the same amount; normalization and the relative residual are unchanged.

Every state in the three-update hand runPower directions are one,zero;two,one;five,four;fourteen,thirteen. Inverse directions are one,zero;negative one,two;five,negative four;negative thirteen,fourteen. QR diagonal pairs are two,two;fourteen fifths,six fifths;one hundred twenty two over forty one,forty two over forty one;one thousand ninety four over three hundred sixty five,three hundred sixty six over three hundred sixty five. Relative residual falls below zero point zero two five at step three.Complete hand trace: all three methods, three updatesVectors below are directions before dividing by their stated norms.kPower directionInverse directionQR diagonal0[1, 0][1, 0][2, 2]norm = 1relative residual ≈ 0.3162281[2, 1][−1, 2][14/5, 6/5]norm = √5relative residual ≈ 0.1897372[5, 4][5, −4][122/41, 42/41]norm = √41relative residual ≈ 0.0694163[14, 13][−13, 14][1094/365, 366/365]norm = √365relative residual ≈ 0.023392At tolerance 0.025, all three stop after update 3.At 10⁻¹⁰, the program instead requires 21 updates; residual ≈ 6.05 × 10⁻¹¹.
Worked Example

Trace It by Hand

Starting from $x_2=(5,4)^T/\sqrt{41}$, predict the next power direction before calculating its Rayleigh quotient. Reveal: the raw product is $(14,13)^T/\sqrt{41}$, so normalization gives $(14,13)^T/\sqrt{365}$ and the quotient is $1094/365$.

Now start power iteration with $(1,-1)^T$ instead. Check: the vector is already an eigenvector for eigenvalue $1$, with residual zero. The routine can return immediately even though $3$ is the dominant eigenvalue. Explain which convergence assumption the start violates.

Finally, change the inverse shift to $5/2$. Prediction: with a nonorthogonal start, inverse iteration now favors eigenvalue $3$, whose distance to the shift is $1/2$, rather than eigenvalue $1$, whose distance is $3/2$.

Implementation in Python, C++, and Java

Each panel is a complete program implementing power iteration, fixed-shift inverse iteration, and unshifted QR. Run Python 3 directly, compile C++14 or newer, or save Java as Main.java. Inputs are finite, nonempty, exactly symmetric square arrays. A nonzero start must have the matching length. Each call preserves its input arrays, validates the tolerance and iteration limit, and either returns a residual-checked approximation or raises an error.

prepare scales $A$ by its largest entry magnitude and scales the inverse shift consistently. The inverse routine factors once and reuses a packed, partially pivoted LU solve. Its pivot threshold is a separate fixed $10^{-14}$ policy; it is not the eigenpair stopping tolerance. The QR helper is deliberately unpivoted and supports exactly zero column tails. The QR eigenvalue routine returns ascending values and matching eigenvector columns. None of these choices guarantees convergence for every symmetric input.

The vector routines return an eigenvalue, normalized eigenvector, update count, and relative residual. QR returns all values, their vector matrix, its update count, and the largest relative residual. A zero update limit allows only an initial-state check. To make signs reproducible within a run, canonical makes the largest-magnitude component nonnegative, retaining the first on an exact tie. Different roundoff or sign conventions can still produce equivalent opposite eigenvectors on other inputs.

The sample prints a scientific residual rather than hiding it behind six decimal places. QR orthogonality is checked separately using the largest entry magnitude of $Z^TZ-I$; its printed zero is rounded, not a claim of exact floating-point orthogonality. Scaling reduces overflow risk but does not recover information lost through extreme dynamic range. These simple dense routines emphasize the algorithms and failure reporting rather than production performance.

import math


def dot(x, y):
    return sum(a * b for a, b in zip(x, y))


def norm(x):
    value = 0.0
    for v in x:
        value = math.hypot(value, v)
    return value


def unit(x):
    if not x or any(not math.isfinite(v) for v in x):
        raise ValueError("vector must be nonempty and finite")
    scale = max(abs(v) for v in x)
    if scale == 0.0:
        raise ValueError("cannot normalize a zero vector")
    y = [v / scale for v in x]
    length = norm(y)
    return [v / length for v in y]


def canonical(x):
    x = unit(x)
    k = max(range(len(x)), key=lambda i: abs(x[i]))
    return [-v for v in x] if x[k] < 0.0 else x


def mv(a, x):
    return [dot(row, x) for row in a]


def mm(a, b):
    n = len(a)
    return [[sum(a[i][k] * b[k][j] for k in range(n))
             for j in range(n)] for i in range(n)]


def prepare(a, tol, limit):
    n = len(a)
    if n == 0 or any(len(row) != n for row in a):
        raise ValueError("A must be nonempty and square")
    if any(not math.isfinite(v) for row in a for v in row):
        raise ValueError("A must be finite")
    if any(a[i][j] != a[j][i] for i in range(n) for j in range(i)):
        raise ValueError("this lesson requires exact symmetry")
    if not math.isfinite(tol) or not 0.0 < tol < 1.0:
        raise ValueError("require finite 0 < tol < 1")
    if not isinstance(limit, int) or limit < 0:
        raise ValueError("limit must be a nonnegative integer")
    scale = max(abs(v) for row in a for v in row) or 1.0
    b = [[v / scale for v in row] for row in a]
    reference = norm([v for row in b for v in row]) or 1.0
    return b, scale, reference


def assess(a, x, reference):
    ax = mv(a, x)
    lam = dot(x, ax) / dot(x, x)
    residual = norm([v - lam * w for v, w in zip(ax, x)])
    return lam, residual / (reference * norm(x))


def lu_factor(a):
    n = len(a)
    u = [row[:] for row in a]
    p = list(range(n))
    scale = max(abs(v) for row in u for v in row)
    for k in range(n):
        r = max(range(k, n), key=lambda i: abs(u[i][k]))
        if scale == 0.0 or abs(u[r][k]) / scale <= 1e-14:
            raise ValueError("shifted solve has a rejected pivot")
        u[k], u[r] = u[r], u[k]
        p[k], p[r] = p[r], p[k]
        for i in range(k + 1, n):
            u[i][k] /= u[k][k]
            for j in range(k + 1, n):
                u[i][j] -= u[i][k] * u[k][j]
                if not math.isfinite(u[i][j]):
                    raise ValueError("nonfinite shifted factorization")
    return u, p


def lu_solve(f, b):
    u, p = f
    n = len(p)
    x = [b[p[i]] for i in range(n)]
    for i in range(n):
        for j in range(i):
            x[i] -= u[i][j] * x[j]
    for i in range(n - 1, -1, -1):
        for j in range(i + 1, n):
            x[i] -= u[i][j] * x[j]
        x[i] /= u[i][i]
        if not math.isfinite(x[i]):
            raise ValueError("nonfinite shifted solve")
    return x


def iterate(a, start, inverse=False, shift=0.0, tol=1e-10, limit=1000):
    b, scale, reference = prepare(a, tol, limit)
    if len(start) != len(a) or not math.isfinite(shift):
        raise ValueError("invalid start length or shift")
    x = unit(start)
    if inverse:
        mu = shift / scale
        shifted = [[b[i][j] - (mu if i == j else 0.0)
                    for j in range(len(a))] for i in range(len(a))]
        if any(not math.isfinite(v) for row in shifted for v in row):
            raise ValueError("shift is not representable after scaling")
        factors = lu_factor(shifted)
    for step in range(limit + 1):
        lam, residual = assess(b, x, reference)
        if residual <= tol:
            value = lam * scale
            if not math.isfinite(value):
                raise ValueError("eigenvalue is not representable")
            return value, canonical(x), step, residual
        if step < limit:
            x = unit(lu_solve(factors, x) if inverse else mv(b, x))
    raise RuntimeError("vector iteration did not converge")


def qr_factor(a):
    # Unpivoted square Householder QR; zero tails need no reflector.
    n = len(a)
    r = [row[:] for row in a]
    q = [[float(i == j) for j in range(n)] for i in range(n)]
    for k in range(n):
        v = [r[i][k] for i in range(k, n)]
        length = norm(v)
        if length == 0.0:
            continue
        alpha = -length if v[0] >= 0.0 else length
        v[0] -= alpha
        v = unit(v)
        for j in range(k, n):
            d = sum(v[t] * r[k + t][j] for t in range(len(v)))
            for t in range(len(v)):
                r[k + t][j] -= 2.0 * v[t] * d
        r[k][k] = alpha
        for i in range(k + 1, n):
            r[i][k] = 0.0
        for i in range(n):
            d = sum(q[i][k + t] * v[t] for t in range(len(v)))
            for t in range(len(v)):
                q[i][k + t] -= 2.0 * d * v[t]
    return q, r


def qr_eigen(a, tol=1e-10, limit=1000):
    b, scale, reference = prepare(a, tol, limit)
    n = len(a)
    t = [row[:] for row in b]
    v = [[float(i == j) for j in range(n)] for i in range(n)]
    for step in range(limit + 1):
        pairs = [assess(b, [v[i][j] for i in range(n)], reference) for j in range(n)]
        error = max(rho for lam, rho in pairs)
        if error <= tol:
            order = sorted(range(n), key=lambda j: pairs[j][0])
            values = [pairs[j][0] * scale for j in order]
            if any(not math.isfinite(x) for x in values):
                raise ValueError("eigenvalue is not representable")
            columns = [canonical([v[i][j] for i in range(n)]) for j in order]
            vectors = [[col[i] for col in columns] for i in range(n)]
            return values, vectors, step, error
        if step < limit:
            q, r = qr_factor(t)
            t, v = mm(r, q), mm(v, q)
    raise RuntimeError("unshifted QR did not converge")


def vector_text(x):
    return "[" + ", ".join(f"{v:.6f}" for v in x) + "]"


def main():
    a = [[2.0, 1.0], [1.0, 2.0]]
    power = iterate(a, [1.0, 0.0])
    inverse = iterate(a, [1.0, 0.0], inverse=True, shift=1.5)
    values, vectors, steps, qr_error = qr_eigen(a)
    orth = max(abs(sum(vectors[k][i] * vectors[k][j] for k in range(2)) - float(i == j))
               for i in range(2) for j in range(2))
    error = max(power[3], inverse[3], qr_error)
    print(f"power lambda = {power[0]:.6f}")
    print("power vector = " + vector_text(power[1]))
    print(f"inverse lambda = {inverse[0]:.6f}")
    print("inverse vector = " + vector_text(inverse[1]))
    print("QR lambdas = " + vector_text(values))
    print(f"iterations = [{power[2]}, {inverse[2]}, {steps}]")
    print(f"max relative residual = {error:.2e}")
    print(f"QR orthogonality error = {orth:.6f}")
    print("check = " + ("PASS" if max(error, orth) < 1e-10 else "FAIL"))


if __name__ == "__main__":
    main()
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <vector>
#include <utility>

using Vec = std::vector<double>;
using Matrix = std::vector<Vec>;
struct Prepared { Matrix b; double scale, reference; };
struct Pair { double lambda, residual; };
struct LU { Matrix u; std::vector<std::size_t> p; };
struct IterResult { double lambda; Vec vector; int steps; double residual; };
struct QRResult { Vec values; Matrix vectors; int steps; double residual; };

double dot(const Vec& x, const Vec& y) {
    double s = 0.0;
    for (std::size_t i = 0; i < x.size(); ++i) s += x[i] * y[i];
    return s;
}
double norm(const Vec& x) {
    double s = 0.0;
    for (double v : x) s = std::hypot(s, v);
    return s;
}
Vec unit(const Vec& x) {
    if (x.empty()) throw std::invalid_argument("vector must be nonempty");
    double scale = 0.0;
    for (double v : x) {
        if (!std::isfinite(v)) throw std::invalid_argument("vector must be finite");
        scale = std::max(scale, std::abs(v));
    }
    if (scale == 0.0) throw std::runtime_error("cannot normalize zero vector");
    Vec y = x;
    for (auto& v : y) v /= scale;
    const double length = norm(y);
    for (auto& v : y) v /= length;
    return y;
}
Vec canonical(const Vec& x) {
    Vec y = unit(x);
    std::size_t k = 0;
    for (std::size_t i = 1; i < y.size(); ++i) if (std::abs(y[i]) > std::abs(y[k])) k = i;
    if (y[k] < 0.0) for (auto& v : y) v = -v;
    return y;
}
Vec mv(const Matrix& a, const Vec& x) {
    Vec y;
    for (const auto& row : a) y.push_back(dot(row, x));
    return y;
}
Matrix mm(const Matrix& a, const Matrix& b) {
    const std::size_t n = a.size();
    Matrix c(n, Vec(n, 0.0));
    for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j)
        for (std::size_t k = 0; k < n; ++k) c[i][j] += a[i][k] * b[k][j];
    return c;
}
Prepared prepare(const Matrix& a, double tol, int limit) {
    const std::size_t n = a.size();
    if (n == 0) throw std::invalid_argument("A must be nonempty");
    double scale = 0.0;
    for (const auto& row : a) {
        if (row.size() != n) throw std::invalid_argument("A must be square");
        for (double v : row) {
            if (!std::isfinite(v)) throw std::invalid_argument("A must be finite");
            scale = std::max(scale, std::abs(v));
        }
    }
    for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < i; ++j)
        if (a[i][j] != a[j][i]) throw std::invalid_argument("require exact symmetry");
    if (!std::isfinite(tol) || !(tol > 0.0 && tol < 1.0) || limit < 0)
        throw std::invalid_argument("invalid tolerance or iteration limit");
    if (scale == 0.0) scale = 1.0;
    Matrix b = a;
    double reference = 0.0;
    for (auto& row : b) for (auto& v : row) { v /= scale; reference = std::hypot(reference, v); }
    if (reference == 0.0) reference = 1.0;
    return {b, scale, reference};
}
Pair assess(const Matrix& a, const Vec& x, double reference) {
    Vec ax = mv(a, x);
    const double lambda = dot(x, ax) / dot(x, x);
    for (std::size_t i = 0; i < x.size(); ++i) ax[i] -= lambda * x[i];
    return {lambda, norm(ax) / (reference * norm(x))};
}
LU lu_factor(const Matrix& a) {
    const std::size_t n = a.size();
    Matrix u = a;
    std::vector<std::size_t> p(n);
    std::iota(p.begin(), p.end(), 0);
    double scale = 0.0;
    for (const auto& row : u) for (double v : row) scale = std::max(scale, std::abs(v));
    for (std::size_t k = 0; k < n; ++k) {
        std::size_t r = k;
        for (std::size_t i = k + 1; i < n; ++i) if (std::abs(u[i][k]) > std::abs(u[r][k])) r = i;
        if (scale == 0.0 || std::abs(u[r][k]) / scale <= 1e-14)
            throw std::runtime_error("shifted solve has a rejected pivot");
        std::swap(u[k], u[r]); std::swap(p[k], p[r]);
        for (std::size_t i = k + 1; i < n; ++i) {
            u[i][k] /= u[k][k];
            for (std::size_t j = k + 1; j < n; ++j) {
                u[i][j] -= u[i][k] * u[k][j];
                if (!std::isfinite(u[i][j])) throw std::runtime_error("nonfinite shifted factorization");
            }
        }
    }
    return {u, p};
}
Vec lu_solve(const LU& f, const Vec& b) {
    const std::size_t n = b.size();
    Vec x(n);
    for (std::size_t i = 0; i < n; ++i) x[i] = b[f.p[i]];
    for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < i; ++j) x[i] -= f.u[i][j] * x[j];
    for (std::size_t i = n; i-- > 0;) {
        for (std::size_t j = i + 1; j < n; ++j) x[i] -= f.u[i][j] * x[j];
        x[i] /= f.u[i][i];
        if (!std::isfinite(x[i])) throw std::runtime_error("nonfinite shifted solve");
    }
    return x;
}
IterResult iterate(const Matrix& a, const Vec& start, bool inverse = false,
                   double shift = 0.0, double tol = 1e-10, int limit = 1000) {
    const auto prep = prepare(a, tol, limit);
    if (start.size() != a.size() || !std::isfinite(shift)) throw std::invalid_argument("invalid start or shift");
    Vec x = unit(start);
    LU f;
    if (inverse) {
        Matrix shifted = prep.b;
        const double mu = shift / prep.scale;
        for (std::size_t i = 0; i < a.size(); ++i) {
            shifted[i][i] -= mu;
            if (!std::isfinite(shifted[i][i])) throw std::runtime_error("scaled shift is not finite");
        }
        f = lu_factor(shifted);
    }
    for (int step = 0; ; ++step) {
        const auto pair = assess(prep.b, x, prep.reference);
        if (pair.residual <= tol) {
            const double value = pair.lambda * prep.scale;
            if (!std::isfinite(value)) throw std::runtime_error("eigenvalue is not finite");
            return {value, canonical(x), step, pair.residual};
        }
        if (step == limit) break;
        x = unit(inverse ? lu_solve(f, x) : mv(prep.b, x));
    }
    throw std::runtime_error("vector iteration did not converge");
}
std::pair<Matrix, Matrix> qr_factor(const Matrix& a) {
    // Unpivoted square Householder QR; skip exactly zero tails.
    const std::size_t n = a.size();
    Matrix r = a, q(n, Vec(n, 0.0));
    for (std::size_t i = 0; i < n; ++i) q[i][i] = 1.0;
    for (std::size_t k = 0; k < n; ++k) {
        Vec v(n - k);
        for (std::size_t t = 0; t < v.size(); ++t) v[t] = r[k + t][k];
        const double length = norm(v);
        if (length == 0.0) continue;
        const double alpha = v[0] >= 0.0 ? -length : length;
        v[0] -= alpha; v = unit(v);
        for (std::size_t j = k; j < n; ++j) {
            double d = 0.0;
            for (std::size_t t = 0; t < v.size(); ++t) d += v[t] * r[k + t][j];
            for (std::size_t t = 0; t < v.size(); ++t) r[k + t][j] -= 2.0 * v[t] * d;
        }
        r[k][k] = alpha;
        for (std::size_t i = k + 1; i < n; ++i) r[i][k] = 0.0;
        for (std::size_t i = 0; i < n; ++i) {
            double d = 0.0;
            for (std::size_t t = 0; t < v.size(); ++t) d += q[i][k + t] * v[t];
            for (std::size_t t = 0; t < v.size(); ++t) q[i][k + t] -= 2.0 * d * v[t];
        }
    }
    return {q, r};
}
QRResult qr_eigen(const Matrix& a, double tol = 1e-10, int limit = 1000) {
    const auto prep = prepare(a, tol, limit);
    const std::size_t n = a.size();
    Matrix t = prep.b, v(n, Vec(n, 0.0));
    for (std::size_t i = 0; i < n; ++i) v[i][i] = 1.0;
    for (int step = 0; ; ++step) {
        std::vector<Pair> pairs;
        double error = 0.0;
        for (std::size_t j = 0; j < n; ++j) {
            Vec col(n); for (std::size_t i = 0; i < n; ++i) col[i] = v[i][j];
            pairs.push_back(assess(prep.b, col, prep.reference));
            error = std::max(error, pairs.back().residual);
        }
        if (error <= tol) {
            std::vector<std::size_t> order(n); std::iota(order.begin(), order.end(), 0);
            std::stable_sort(order.begin(), order.end(), [&](std::size_t i, std::size_t j) { return pairs[i].lambda < pairs[j].lambda; });
            Vec values(n); Matrix vectors(n, Vec(n));
            for (std::size_t j = 0; j < n; ++j) {
                values[j] = pairs[order[j]].lambda * prep.scale;
                if (!std::isfinite(values[j])) throw std::runtime_error("eigenvalue is not finite");
                Vec col(n); for (std::size_t i = 0; i < n; ++i) col[i] = v[i][order[j]];
                col = canonical(col); for (std::size_t i = 0; i < n; ++i) vectors[i][j] = col[i];
            }
            return {values, vectors, step, error};
        }
        if (step == limit) break;
        const auto qr = qr_factor(t);
        t = mm(qr.second, qr.first); v = mm(v, qr.first);
    }
    throw std::runtime_error("unshifted QR did not converge");
}
void print_vector(const Vec& x) {
    std::cout << '[';
    for (std::size_t i = 0; i < x.size(); ++i) { if (i) std::cout << ", "; std::cout << x[i]; }
    std::cout << ']';
}
int main() {
    const Matrix a = { {2.0, 1.0}, {1.0, 2.0} };
    const auto power = iterate(a, {1.0, 0.0}), inverse = iterate(a, {1.0, 0.0}, true, 1.5);
    const auto qr = qr_eigen(a);
    double orth = 0.0;
    for (std::size_t i = 0; i < 2; ++i) for (std::size_t j = 0; j < 2; ++j) {
        double value = 0.0; for (std::size_t k = 0; k < 2; ++k) value += qr.vectors[k][i] * qr.vectors[k][j];
        orth = std::max(orth, std::abs(value - (i == j ? 1.0 : 0.0)));
    }
    const double error = std::max({power.residual, inverse.residual, qr.residual});
    std::cout << std::fixed << std::setprecision(6) << "power lambda = " << power.lambda << "\npower vector = "; print_vector(power.vector);
    std::cout << "\ninverse lambda = " << inverse.lambda << "\ninverse vector = "; print_vector(inverse.vector);
    std::cout << "\nQR lambdas = "; print_vector(qr.values);
    std::cout << "\niterations = [" << power.steps << ", " << inverse.steps << ", " << qr.steps << ']'
              << "\nmax relative residual = " << std::scientific << std::setprecision(2) << error
              << "\nQR orthogonality error = " << std::fixed << std::setprecision(6) << orth
              << "\ncheck = " << (std::max(error, orth) < 1e-10 ? "PASS" : "FAIL") << '\n';
}
import java.util.Arrays;
import java.util.Locale;

public class Main {
    static class Prepared {
        final double[][] b; final double scale, reference;
        Prepared(double[][] b, double scale, double reference) { this.b=b; this.scale=scale; this.reference=reference; }
    }
    static class Pair {
        final double lambda, residual;
        Pair(double lambda, double residual) { this.lambda=lambda; this.residual=residual; }
    }
    static class LU {
        final double[][] u; final int[] p;
        LU(double[][] u, int[] p) { this.u=u; this.p=p; }
    }
    static class IterResult {
        final double lambda, residual; final double[] vector; final int steps;
        IterResult(double lambda, double[] vector, int steps, double residual) {
            this.lambda=lambda; this.vector=vector; this.steps=steps; this.residual=residual;
        }
    }
    static class QRResult {
        final double[] values; final double[][] vectors; final int steps; final double residual;
        QRResult(double[] values, double[][] vectors, int steps, double residual) {
            this.values=values; this.vectors=vectors; this.steps=steps; this.residual=residual;
        }
    }
    static double dot(double[] x, double[] y) {
        double s=0.0; for(int i=0;i<x.length;++i)s+=x[i]*y[i]; return s;
    }
    static double norm(double[] x) {
        double s=0.0; for(double v:x)s=Math.hypot(s,v); return s;
    }
    static double[] unit(double[] x) {
        if(x.length==0)throw new IllegalArgumentException("vector must be nonempty");
        double scale=0.0;
        for(double v:x) {
            if(!Double.isFinite(v))throw new IllegalArgumentException("vector must be finite");
            scale=Math.max(scale,Math.abs(v));
        }
        if(scale==0.0)throw new IllegalArgumentException("cannot normalize zero vector");
        double[] y=x.clone(); for(int i=0;i<y.length;++i)y[i]/=scale;
        double length=norm(y);for(int i=0;i<y.length;++i)y[i]/=length;return y;
    }
    static double[] canonical(double[] x) {
        double[] y=unit(x);int k=0;
        for(int i=1;i<y.length;++i)if(Math.abs(y[i])>Math.abs(y[k]))k=i;
        if(y[k]<0.0)for(int i=0;i<y.length;++i)y[i]=-y[i];return y;
    }
    static double[] mv(double[][] a,double[] x) {
        double[] y=new double[a.length];for(int i=0;i<a.length;++i)y[i]=dot(a[i],x);return y;
    }
    static double[][] mm(double[][] a,double[][] b) {
        int n=a.length;double[][] c=new double[n][n];
        for(int i=0;i<n;++i)for(int j=0;j<n;++j)for(int k=0;k<n;++k)c[i][j]+=a[i][k]*b[k][j];return c;
    }
    static double[][] copy(double[][] a) {
        double[][] b=new double[a.length][];for(int i=0;i<a.length;++i)b[i]=a[i].clone();return b;
    }
    static double[][] identity(int n) {
        double[][] a=new double[n][n];for(int i=0;i<n;++i)a[i][i]=1.0;return a;
    }
    static Prepared prepare(double[][] a,double tol,int limit) {
        int n=a.length;if(n==0)throw new IllegalArgumentException("A must be nonempty");
        double scale=0.0;
        for(double[] row:a) {
            if(row==null||row.length!=n)throw new IllegalArgumentException("A must be square");
            for(double v:row) {
                if(!Double.isFinite(v))throw new IllegalArgumentException("A must be finite");
                scale=Math.max(scale,Math.abs(v));
            }
        }
        for(int i=0;i<n;++i)for(int j=0;j<i;++j)
            if(a[i][j]!=a[j][i])throw new IllegalArgumentException("require exact symmetry");
        if(!Double.isFinite(tol)||!(tol>0.0&&tol<1.0)||limit<0)
            throw new IllegalArgumentException("invalid tolerance or iteration limit");
        if(scale==0.0)scale=1.0;
        double[][] b=copy(a);double reference=0.0;
        for(double[] row:b)for(int j=0;j<n;++j) {row[j]/=scale;reference=Math.hypot(reference,row[j]);}
        if(reference==0.0)reference=1.0;return new Prepared(b,scale,reference);
    }
    static Pair assess(double[][] a,double[] x,double reference) {
        double[] ax=mv(a,x);double lambda=dot(x,ax)/dot(x,x);
        for(int i=0;i<x.length;++i)ax[i]-=lambda*x[i];return new Pair(lambda,norm(ax)/(reference*norm(x)));
    }
    static LU luFactor(double[][] a) {
        int n=a.length;double[][] u=copy(a);int[] p=new int[n];double scale=0.0;
        for(int i=0;i<n;++i){p[i]=i;for(double v:u[i])scale=Math.max(scale,Math.abs(v));}
        for(int k=0;k<n;++k) {
            int r=k;for(int i=k+1;i<n;++i)if(Math.abs(u[i][k])>Math.abs(u[r][k]))r=i;
            if(scale==0.0||Math.abs(u[r][k])/scale<=1e-14)
                throw new IllegalArgumentException("shifted solve has a rejected pivot");
            double[] row=u[k];u[k]=u[r];u[r]=row;int index=p[k];p[k]=p[r];p[r]=index;
            for(int i=k+1;i<n;++i) {
                u[i][k]/=u[k][k];
                for(int j=k+1;j<n;++j) {
                    u[i][j]-=u[i][k]*u[k][j];
                    if(!Double.isFinite(u[i][j]))throw new IllegalArgumentException("nonfinite shifted factorization");
                }
            }
        }
        return new LU(u,p);
    }
    static double[] luSolve(LU f,double[] b) {
        int n=b.length;double[] x=new double[n];for(int i=0;i<n;++i)x[i]=b[f.p[i]];
        for(int i=0;i<n;++i)for(int j=0;j<i;++j)x[i]-=f.u[i][j]*x[j];
        for(int i=n-1;i>=0;--i) {
            for(int j=i+1;j<n;++j)x[i]-=f.u[i][j]*x[j];x[i]/=f.u[i][i];
            if(!Double.isFinite(x[i]))throw new IllegalArgumentException("nonfinite shifted solve");
        }
        return x;
    }
    static IterResult iterate(double[][] a,double[] start,boolean inverse,double shift,double tol,int limit) {
        Prepared prep=prepare(a,tol,limit);
        if(start.length!=a.length||!Double.isFinite(shift))throw new IllegalArgumentException("invalid start or shift");
        double[] x=unit(start);LU f=null;
        if(inverse) {
            double[][] shifted=copy(prep.b);double mu=shift/prep.scale;
            for(int i=0;i<a.length;++i) {
                shifted[i][i]-=mu;
                if(!Double.isFinite(shifted[i][i]))throw new IllegalArgumentException("scaled shift is not finite");
            }
            f=luFactor(shifted);
        }
        for(int step=0;;++step) {
            Pair pair=assess(prep.b,x,prep.reference);
            if(pair.residual<=tol) {
                double value=pair.lambda*prep.scale;
                if(!Double.isFinite(value))throw new IllegalArgumentException("eigenvalue is not finite");
                return new IterResult(value,canonical(x),step,pair.residual);
            }
            if(step==limit)break;x=unit(inverse?luSolve(f,x):mv(prep.b,x));
        }
        throw new IllegalStateException("vector iteration did not converge");
    }
    static double[][][] qrFactor(double[][] a) {
        // Unpivoted square Householder QR; skip exactly zero tails.
        int n=a.length;double[][] r=copy(a),q=identity(n);
        for(int k=0;k<n;++k) {
            double[] v=new double[n-k];for(int t=0;t<v.length;++t)v[t]=r[k+t][k];
            double length=norm(v);if(length==0.0)continue;
            double alpha=v[0]>=0.0?-length:length;v[0]-=alpha;v=unit(v);
            for(int j=k;j<n;++j) {
                double d=0.0;for(int t=0;t<v.length;++t)d+=v[t]*r[k+t][j];
                for(int t=0;t<v.length;++t)r[k+t][j]-=2.0*v[t]*d;
            }
            r[k][k]=alpha;for(int i=k+1;i<n;++i)r[i][k]=0.0;
            for(int i=0;i<n;++i) {
                double d=0.0;for(int t=0;t<v.length;++t)d+=q[i][k+t]*v[t];
                for(int t=0;t<v.length;++t)q[i][k+t]-=2.0*d*v[t];
            }
        }
        return new double[][][]{q,r};
    }
    static QRResult qrEigen(double[][] a,double tol,int limit) {
        Prepared prep=prepare(a,tol,limit);int n=a.length;double[][] t=copy(prep.b),v=identity(n);
        for(int step=0;;++step) {
            Pair[] pairs=new Pair[n];double error=0.0;
            for(int j=0;j<n;++j) {
                double[] col=new double[n];for(int i=0;i<n;++i)col[i]=v[i][j];
                pairs[j]=assess(prep.b,col,prep.reference);error=Math.max(error,pairs[j].residual);
            }
            if(error<=tol) {
                Integer[] order=new Integer[n];for(int j=0;j<n;++j)order[j]=j;
                Arrays.sort(order,(i,j)->Double.compare(pairs[i].lambda,pairs[j].lambda));
                double[] values=new double[n];double[][] vectors=new double[n][n];
                for(int j=0;j<n;++j) {
                    values[j]=pairs[order[j]].lambda*prep.scale;
                    if(!Double.isFinite(values[j]))throw new IllegalArgumentException("eigenvalue is not finite");
                    double[] col=new double[n];for(int i=0;i<n;++i)col[i]=v[i][order[j]];
                    col=canonical(col);for(int i=0;i<n;++i)vectors[i][j]=col[i];
                }
                return new QRResult(values,vectors,step,error);
            }
            if(step==limit)break;double[][][] qr=qrFactor(t);t=mm(qr[1],qr[0]);v=mm(v,qr[0]);
        }
        throw new IllegalStateException("unshifted QR did not converge");
    }
    static String vectorText(double[] x) {
        StringBuilder s=new StringBuilder("[");
        for(int i=0;i<x.length;++i){if(i>0)s.append(", ");s.append(String.format(Locale.ROOT,"%.6f",x[i]));}
        return s.append(']').toString();
    }
    public static void main(String[] args) {
        double[][] a={ {2.0,1.0},{1.0,2.0} };double[] start={1.0,0.0};
        IterResult power=iterate(a,start,false,0.0,1e-10,1000),inverse=iterate(a,start,true,1.5,1e-10,1000);
        QRResult qr=qrEigen(a,1e-10,1000);double orth=0.0;
        for(int i=0;i<2;++i)for(int j=0;j<2;++j) {
            double value=0.0;for(int k=0;k<2;++k)value+=qr.vectors[k][i]*qr.vectors[k][j];
            orth=Math.max(orth,Math.abs(value-(i==j?1.0:0.0)));
        }
        double error=Math.max(power.residual,Math.max(inverse.residual,qr.residual));
        System.out.printf(Locale.ROOT,"power lambda = %.6f%n",power.lambda);
        System.out.println("power vector = "+vectorText(power.vector));
        System.out.printf(Locale.ROOT,"inverse lambda = %.6f%n",inverse.lambda);
        System.out.println("inverse vector = "+vectorText(inverse.vector));
        System.out.println("QR lambdas = "+vectorText(qr.values));
        System.out.printf(Locale.ROOT,"iterations = [%d, %d, %d]%nmax relative residual = %.2e%nQR orthogonality error = %.6f%n",power.steps,inverse.steps,qr.steps,error,orth);
        System.out.println("check = "+(Math.max(error,orth)<1e-10?"PASS":"FAIL"));
    }
}

All three programs print the same nine lines:

  • power lambda = 3.000000
  • power vector = [0.707107, 0.707107]
  • inverse lambda = 1.000000
  • inverse vector = [-0.707107, 0.707107]
  • QR lambdas = [1.000000, 3.000000]
  • iterations = [21, 21, 21]
  • max relative residual = 6.05e-11
  • QR orthogonality error = 0.000000
  • check = PASS

Complexity, Correctness, and Invariants

The number of iterations is part of the cost. There is no matrix-size-only worst-case convergence bound for these unrestricted elementary methods: a gap may be tiny, a target component may be missing, or an unshifted QR sequence may cycle. With a fixed budget $K$, the code terminates by returning a checked result or reporting failure.

Dense power iteration costs $\Theta(n^2)$ per update and residual check, with $O(n)$ vector workspace beyond its matrix copy. A fixed-shift inverse run has $\Theta(n^3)$ LU setup and $\Theta(n^2)$ work per solve/check. The simple QR implementation performs dense Householder factorization, dense products, and checks all $n$ candidate eigenvectors, giving $\Theta(n^3)$ per sweep and $\Theta(n^2)$ storage. For a fixed number of updates, existing zeros do not remove the dense matrix-product costs.

An initial exact eigenpair can return before any vector update; matrix validation and copying still cost $\Theta(n^2)$. Inverse iteration also prepares its shifted factorization before that initial check. QR checks an entire initial basis and therefore does cubic checking work even when it returns at update zero. For a successful run using $k$ updates, the bounds below include setup and checks.

MethodDense total time for $k$ updatesStored data and workspaceTarget and convergence conditions
Power iteration$\Theta((k+1)n^2)$$\Theta(n^2)$ matrix copy plus $O(n)$ vectorsOne eigenpair; a unique largest magnitude and nonzero target component give the standard convergence argument
Fixed-shift inverse iteration$\Theta(n^3+(k+1)n^2)$$\Theta(n^2)$ matrix and LU factorsOne nearby eigenpair; shifted solve must pass pivots and the nearest eigenspace must be represented
Unshifted dense QR here$\Theta((k+1)n^3)$$\Theta(n^2)$ matricesAll symmetric eigenpairs if convergence occurs; equal magnitudes can obstruct it
Structured QR with shifts and deflationTypically cubic dense reduction plus cheaper structured sweeps$O(n^2)$ if eigenvectors are retainedPractical eigensolvers exploit Hessenberg or symmetric tridiagonal form
Sparse Lanczos for symmetric matricesDepends on matrix-vector products, subspace size, and reorthogonalizationSparse matrix plus a retained basisSelected eigenpairs or invariant subspaces of large sparse problems

For power iteration, the invariant is $x_k$ proportional to $A^kx_0$, as long as normalization is defined. Orthogonal eigenbasis expansion then proves component suppression under the stated gap and starting-vector assumptions. The Rayleigh quotient of a normalized vector gives a scalar to test, and the residual verifies the resulting pair.

For inverse iteration, the corresponding invariant is $u_k$ proportional to $(A-\mu I)^{-k}u_0$. Its eigenvectors are unchanged, and the transformed eigenvalues are reciprocal distances with sign. Under a unique nearest eigenvalue and a nonzero component in that direction, unwanted components decay at a rate governed by $\lvert(\lambda_{\mathrm{target}}-\mu)/(\lambda_{\mathrm{next}}-\mu)\rvert$.

For QR, induction gives

$$ T_k=Z_k^TAZ_k, \qquad Z_k^TZ_k=I. $$

The update preserves this identity because $T_{k+1}=Q_k^TT_kQ_k$ and $Z_{k+1}=Z_kQ_k$. If $T_k$ becomes diagonal, its diagonal entries and columns of $Z_k$ form exact eigenpairs of original $A$. The scaled implementation obeys the corresponding identity with $A/s$, then restores eigenvalue units.

For a real symmetric matrix, any approximate pair satisfies

$$ \min_i\lvert\theta-\lambda_i\rvert \le\frac{\lVert Ax-\theta x\rVert_2}{\lVert x\rVert_2}. $$

This follows by expanding the residual in the orthonormal eigenbasis. It locates some nearby eigenvalue, but does not certify which eigenvalue was found or a unique eigenvector when eigenvalues cluster. Floating-point orthogonality and similarity drift are additional reasons to check residuals against the original input.

Variants, Pitfalls, and Optimizations

A valid eigenpair may be the wrong target. Starting the sample power routine in direction $(1,-1)^T$ returns eigenvalue $1$ with zero residual. That is a correct eigenpair but not the dominant one. Multiple starts or subspace methods help when a single starting direction is unreliable. Residuals certify the equation, not the requested ordering of the spectrum.

Sign changes and equal magnitudes. A negative dominant eigenvalue can make successive vectors alternate sign, so $\lVert x_{k+1}-x_k\rVert$ is a poor sole stopping test. With $A=\begin{pmatrix}0&1\\1&0\end{pmatrix}$ and start $(1,0)^T$, power alternates coordinate axes. The Rayleigh quotient remains $0$ while the residual norm remains $1$. Unshifted QR also fails to diagonalize this example; an unchanged diagonal is not convergence. The budget must report failure.

A shift changes the problem being solved internally. Do not choose an exact eigenvalue as a fixed inverse shift: the shifted matrix is singular. Near-eigenvalue shifts can speed direction convergence but make the solves delicate or trigger pivot rejection. Rayleigh quotient iteration updates the shift and can converge rapidly near a simple symmetric eigenpair, but generally needs new shifted factorizations. An inverse estimate is not itself an eigenvalue of original $A$.

QR needs similarity, shifts, and deflation. A shifted sweep factors $T_k-\mu_kI=Q_kR_k$, then forms $T_{k+1}=R_kQ_k+\mu_kI$. Practical methods choose shifts and split converged blocks using deflation. Symmetric solvers usually reduce to tridiagonal form first; general real solvers work toward a real Schur form that may contain $2\times2$ blocks for complex conjugate pairs. Columns of an accumulated nonsymmetric Schur basis are not generally individual eigenvectors. The code's symmetric restriction avoids that false interpretation.

Repeated and clustered eigenvalues. Eigenvectors within a repeated eigenspace are not unique. Even a very small residual may leave the individual direction poorly determined when nearby eigenvalues are tightly clustered. Compare invariant subspaces, residuals, and orthogonality rather than requiring one particular signed vector. Arnoldi, Lanczos, block iteration, and restart strategies extend the basic repeated-multiplication idea for larger problems.

Exploit structure without changing the target. Sparse power iteration can use a sparse matrix-vector product rather than dense arrays. Sparse shift-invert methods reuse sparse factors or suitable solves, while fill-in affects memory. For covariance directions, applying an SVD to centered data can avoid explicitly forming a covariance matrix. For graph Laplacians, the smallest eigenvalue may be a known zero; the next-smallest direction requires excluding that nullspace or using an appropriate shifted/subspace method.

Wrong target despite zero residual and a cycling exampleThe sample start one,negative one returns exact eigenvalue one rather than dominant three. A different matrix zero,one;one,zero causes power iteration to alternate coordinate axes with quotient zero and residual norm one. A budget must report nonconvergence.A small residual verifies a pair, not its target labelUse A = [[2, 1], [1, 2]], whose dominant eigenvalue is 3.Start [1, 0]: both eigendirections are represented.Power iteration approaches eigenvalue 3 and direction [1, 1].The dominant component grows relative to the other component.Start [1, −1]: the dominant component is absent.Returned eigenvalue 1; residual exactly 0; dominant target missed.The eigenpair equation is correct. The starting-vector assumption fails.A separate cycling case: A = [[0, 1], [1, 0]].[1, 0] ↔ [0, 1]; Rayleigh quotient stays 0, residual norm stays 1.Stop with nonconvergence when the update budget is exhausted.Use residuals, target assumptions, and a budget together.

Always check original-matrix residuals and enforce an update limit; unchanged Rayleigh quotients or diagonals can hide cycling. Track eigenvector signs and eigenvalue targets separately, and use unpivoted QR in the similarity iteration rather than blindly reusing column-pivoted least-squares QR.

Practice Problems and Real-World Use

Validate the methods on matrices whose spectrum is known independently. For example, construct $A=V\Lambda V^T$ from an orthogonal matrix and a chosen diagonal spectrum, then verify returned eigenvalues, residuals, and QR orthogonality. For a simple symmetric eigenvalue, compare vectors up to sign. For repeated eigenvalues, compare their spanned subspace instead.

Include negative eigenvalues, a zero matrix, a singular but valid QR input, a start orthogonal to the dominant direction, an exact singular shift, a shift needing an LU row swap, and the equal-magnitude cycling example. Repeat tests after uniform scaling and preserve the original input for checks. A nonconvergence test is a required outcome for a limited method, not automatically an implementation defect.

Exercise or applicationConcrete task or checkpointIdea testedDifficulty
Hand-trace comparisonReproduce all three sample methods through update $3$ at $\tau=0.025$Different operators and the same residual criterionBeginner
Negative dominant eigenvalueUse $\operatorname{diag}(-4,1)$ with both starting components nonzeroMagnitude dominance and sign alternationIntermediate
Wrong-target regressionStart the sample with $(1,-1)^T$ and obtain eigenvalue $1$ immediatelyA zero residual does not certify dominanceIntermediate
Cycling regressionUse rows $(0,1),(1,0)$ with start $(1,0)^T$ and expect a budget failureEqual magnitudes and misleading unchanged quotientsIntermediate
Shift targetingChange the sample shift from $3/2$ to $5/2$; switch from target $1$ to $3$Closest-eigenvalue selection and factor reuseIntermediate
Principal component directionFind a largest covariance eigendirection, or compare with an SVD of centered dataDominant variance directionApplied
Graph spectral partitioningFind an appropriate small Laplacian eigenspace after handling the constant nullspaceTargeting and structural zero modesAdvanced
Vibration and stability modelsAnalyze real symmetric mode matrices or the appropriate generalized problemEigenmodes, units, and modeling assumptionsApplied