43. Dual spaces and dual bases
Every vector so far has been an input — something a matrix or transformation acts on. This lesson studies the outputs of a different, simpler kind of map: linear functionals, functions that turn a vector into a single number. Collecting all of them produces the dual space — a genuinely new vector space, built entirely from V itself, that quietly reappears as soon as §23.1's bilinear forms need a place to live.
Linear functionals
A linear functional on V is a linear transformation (§18.0) f:V\to\mathbb{R} — the special case of a linear map whose codomain is the scalars themselves (\mathbb{R}=\mathbb{R}^1, a 1-dimensional vector space). Concretely on \mathbb{R}^n, every linear functional has the form
f(\vec x)=a_1x_1+a_2x_2+\cdots+a_nx_n
for fixed constants a_1,\dots,a_n — by §18.1's standard-matrix correspondence, f(\vec x)=A\vec x for a 1\times n matrix A, i.e. a row vector. This is the identity worth holding onto for the whole lesson: a linear functional on \mathbb{R}^n is a row vector, dual to the column vectors V itself is made of.
Examples already used throughout this course, unnamed: \vec v\mapsto\vec v\cdot\vec u for fixed \vec u (§20.0's dot product, viewed as a function of one argument with the other held fixed) is a linear functional; \vec x\mapsto x_i (extracting a single coordinate) is a linear functional; A\mapsto\operatorname{tr}A on M_{n\times n} is a linear functional on the space of matrices.
The dual space
The dual space V^* is the set of all linear functionals on V, with pointwise addition and scaling: (f+g)(\vec v)=f(\vec v)+g(\vec v), (cf)(\vec v)=c\,f(\vec v). V^* is itself a vector space (§17.0's axioms check routinely, since addition/scaling of functions always satisfies them). For finite-dimensional V,
\dim V^*=\dim V
so V^*\cong V (§18.4's isomorphism theorem) — but the isomorphism is not canonical (it depends on a choice of basis, unlike, say, V\cong V via the identity), a subtlety that matters more in infinite dimensions than here, but worth flagging.
Dual basis
Given a basis \{\vec v_1,\dots,\vec v_n\} of V, its dual basis \{f_1,\dots,f_n\}\subseteq V^* is defined by
f_i(\vec v_j)=\delta_{ij}
(the Kronecker delta from §20.4 — 1 if i=j, else 0). Each f_i is the unique linear functional picking out the i-th coordinate relative to this basis (well-defined by §18.0's "determined by action on a basis" fact, applied to functionals). For \mathbb{R}^n with the standard basis, the dual basis is exactly \{f_1,\dots,f_n\} with f_i(\vec x)=x_i — the coordinate-extraction functionals mentioned above — and under the row/column identification, f_i is literally \vec e_i^T.
For an orthonormal basis specifically, §20.4's coordinate formula c_i=\vec v\cdot\vec q_i says the dual basis functional f_i is "dot with \vec q_i" — the dual basis and the original orthonormal basis coincide under the identification V\cong V^* via the dot product. This is the deeper reason §20.4's coordinate formula worked out so cleanly: orthonormality makes the distinction between V and V^* briefly invisible.
Doing it in Python
import numpy as np
v1 = np.array([1., 1., 0.])
v2 = np.array([0., 1., 1.])
v3 = np.array([1., 0., 1.])
V = np.column_stack([v1, v2, v3])
# The dual basis, as row vectors, is exactly V^{-1} -- solve f_i(v_j) = delta_ij
F = np.linalg.inv(V) # rows of F are f1, f2, f3
for i in range(3):
row = F[i, :]
values = [round(float(row @ v), 6) for v in (v1, v2, v3)]
print(f"f{i+1}(v1,v2,v3) =", values)
f1(v1,v2,v3) = [1.0, 0.0, 0.0]
f2(v1,v2,v3) = [0.0, 1.0, 0.0]
f3(v1,v2,v3) = [0.0, 0.0, 1.0]
Confirming the dual basis of an orthonormal basis coincides with itself:
import numpy as np
q1 = np.array([1., 1., 0.]) / np.sqrt(2)
q2 = np.array([1., -1., 0.]) / np.sqrt(2)
q3 = np.array([0., 0., 1.])
Q = np.column_stack([q1, q2, q3])
F = np.linalg.inv(Q)
print("Q^-1 == Q^T:", bool(np.allclose(F, Q.T)))
print("F's rows are exactly q1, q2, q3 (as rows):", bool(np.allclose(F, Q.T)))
Q^-1 == Q^T: True
F's rows are exactly q1, q2, q3 (as rows): True
Worked example
Find the dual basis functional f_1 for \{\vec v_1,\vec v_2\}=\{(2,1),(1,1)\}, i.e. find the row vector (a,b) with f_1(\vec v_1)=1, f_1(\vec v_2)=0.
f_1(x,y)=ax+by. Conditions: 2a+b=1 and a+b=0\Rightarrow b=-a. Substituting: 2a-a=1\Rightarrow a=1, so b=-1.
\boxed{f_1(x,y)=x-y}
Sanity check. f_1(\vec v_1)=f_1(2,1)=2-1=1 ✓. f_1(\vec v_2)=f_1(1,1)=1-1=0 ✓ — both defining conditions hold exactly, confirming this is the correct (and by uniqueness, the only) dual functional for \vec v_1 relative to this basis.
Your turn
1. Find f_2 (the other dual basis functional) for the same basis \{(2,1),(1,1)\} from the worked example.
2. For the standard basis of \mathbb{R}^3, write f_2 explicitly as a formula.
3. True or false: the dual basis depends only on V, not on which basis of V was chosen.
Solutions
1. f_2(x,y)=cx+dy with f_2(\vec v_1)=0, f_2(\vec v_2)=1: 2c+d=0 and c+d=1. From the first, d=-2c; substituting: c-2c=1\Rightarrow c=-1,d=2. f_2(x,y)=-x+2y. Check: f_2(2,1)=-2+2=0 ✓, f_2(1,1)=-1+2=1 ✓.
2. f_2(x,y,z)=y — the functional extracting the second coordinate directly, matching "the dual basis of the standard basis is coordinate extraction" stated above.
3. False. The dual basis is defined relative to a chosen basis of V (the \delta_{ij} conditions reference specific \vec v_j's) — a different basis of V produces a different dual basis of V^*, even though V^* itself doesn't depend on the choice. This mirrors §17.3's coordinate vectors: the space of coordinate vectors is basis-independent in the abstract, but the specific numbers always depend on which basis was picked.
Check yourself in code
Find the dual basis (as rows of V^{-1}) for \{\vec v_1,\vec v_2\}=\{(3,1),(2,1)\}, and verify f_1(\vec v_1)=1, f_1(\vec v_2)=0.
Print exactly this:
dual basis rows:
[1.0, -2.0]
[-1.0, 3.0]
f1(v1)=1.0, f1(v2)=0.0
import numpy as np
v1 = np.array([3., 1.])
v2 = np.array([2., 1.])
V = np.column_stack([v1, v2])
F = np.linalg.inv(V)
print("dual basis rows:")
for row in F.tolist():
print([round(v, 4) for v in row])
# print "f1(v1)=..., f1(v2)=..." using F's first row dotted with v1 and v2
import numpy as np
v1 = np.array([3., 1.])
v2 = np.array([2., 1.])
V = np.column_stack([v1, v2])
F = np.linalg.inv(V)
print("dual basis rows:")
for row in F.tolist():
print([round(v, 4) for v in row])
f1 = F[0, :]
print(f"f1(v1)={round(float(f1 @ v1), 4)}, f1(v2)={round(float(f1 @ v2), 4)}")
A linear functional on V is a linear map to scalars — on \mathbb{R}^n, exactly a row vector. Collecting all of them gives the dual space V^*, with \dim V^*=\dim V, and every basis of V determines a dual basis via f_i(\vec v_j)=\delta_{ij} — for an orthonormal basis, the dual basis coincides with the original, which is exactly why §20.4's coordinate formula needed nothing beyond a dot product.
Next: pairing two vector arguments instead of one — bilinear forms, of which both the dot product and every quadratic form (§21.3) have already been examples.