1. 定义
插值问题的本质其实就是:
- 给定一堆采样点,然后构造一个函数来对这堆采样点背后的真实函数表达进行拟合。
即是说,找一条经过这一堆采样点的曲线来对这些采样点背后的真实函数曲线进行描述。
我们给出书中的定义如下:
f ( x ) f(x) f(x)为定义在区间 [ a , b ] [a,b] [a,b]上的函数, x 0 , . . . , x n x_0, ..., x_n x0,...,xn为 [ a , b ] [a,b] [a,b]上的 n + 1 n+1 n+1个互不相同的点, Φ \Phi Φ为给定的某一函数类,若 Φ \Phi Φ上有函数 ϕ x \phi{x} ϕx,满足
ϕ ( x i ) = f ( x i ) , i = 0 , 1 , . . . , n \phi(x_i) = f(x_i), \ i=0,1,...,n ϕ(xi)=f(xi), i=0,1,...,n
则称 ϕ ( x ) \phi(x) ϕ(x)为 f ( x ) f(x) f(x)关于节点 x 0 , . . . , x n x_0, ..., x_n x0,...,xn在 Φ \Phi Φ上的插值函数。称点 x 0 , . . . , x n x_0, ..., x_n x0,...,xn为插值节点;称 ( x i , f ( x i ) ) , i = 0 , 1 , . . . , n (x_i, f(x_i)), \ i=0,1,...,n (xi,f(xi)), i=0,1,...,n为插值型值点,简称型值点或者插值点; f ( x ) f(x) f(x)称为被插函数。
2. Lagrange插值
1. 定义 & 实现
Lagrange插值公式本质上就是用一个 n n n阶函数来拟合这些采样点,因此,我们事实上就是要解如下方程组:
{ y 0 = a 0 + a 1 x 0 + a 2 x 0 2 + . . . + a n x 0 n y 1 = a 0 + a 1 x 1 + a 2 x 1 2 + . . . + a n x 1 n . . . y n = a 0 + a 1 x n + a 2 x n 2 + . . . + a n x n n \left\{ \begin{aligned} y_0 &= a_0 + a_1 x_0 + a_2 x_0^2 + ... + a_n x_0^n \\ y_1 &= a_0 + a_1 x_1 + a_2 x_1^2 + ... + a_n x_1^n \\ ... \\ y_n &= a_0 + a_1 x_n + a_2 x_n^2 + ... + a_n x_n^n \end{aligned} \right. ⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧y0y1...yn=a0+a1x0+a2x02+...+anx0n=a0+a1x1+a2x12+...+anx1n=a0+a1xn+a2xn2+...+anxnn
当满足 x 0 , . . . , x n x_0, ..., x_n x0,...,xn互异时,上述方程的解是唯一的。
而Lagrange直接给出了一个插值函数,令其满足上述 n + 1 n+1 n+1个方程,亦即Lagrange插值函数:
L n ( x ) = ∑ i = 0 n l i ( x ) y i L_n(x) = \sum_{i=0}^{n} l_i(x) y_i Ln(x)=i=0∑nli(x)yi
其中, l i ( x ) l_i(x) li(x)满足条件:
{ l i = 1 if x = x i l i = 0 if x = x j , j ≠ i \left\{ \begin{aligned} l_i &= 1 \text{ if } x = x_i \\ l_i &= 0 \text{ if } x = x_j, \ j \neq i \end{aligned} \right. {lili=1 if x=xi=0 if x=xj, j=i
由是,可以直观地给出 l i ( x ) l_i(x) li(x)的一种实现方式如下:
l i ( x ) = Π j ≠ i x − x j x i − x j l_i(x) = \Pi_{j \neq i} \frac{x - x_j}{x_i - x_j} li(x)=Πj=ixi−xjx−xj
2. 伪代码实现
我们在python当中给出Lagrange插值函数的具体代码实现如下:
def lagrange_fn(xlist, ylist):
assert(len(xlist) == len(ylist))
assert(len(set(xlist)) == len(xlist))
n = len(xlist)
llist = []
for i in range(n):
li = ylist[i]
for j in range(n):
if j == i:
continue
li /= (xlist[i] - xlist[j])
llist.append(li)
def fn(x):
s = 1
for xi, yi in zip(xlist, ylist):
if x == xi:
return yi
s *= (x - xi)
res = 0
for xi, li in zip(xlist, llist):
res += li * s / (x - xi)
return res
return fn
3. 误差分析
对于n次插值多项式的误差,有如下定理:
设 L n ( x ) L_n(x) Ln(x)是 [ a , b ] [a,b] [a,b]上过 ( x i , f ( x i ) ) , i = 0 , 1 , . . . , n (x_i, f(x_i)), i=0,1,...,n (xi,f(xi)),i=0,1,...,n的 n n n次插值多项式, x i ∈ [ a , b ] x_i \in [a, b] xi∈[a,b], x i x_i xi互不相同,当 f ∈ C n + 1 [ a , b ] f\in C^{n+1}[a,b] f∈Cn+1[a,b]时,则存在 ξ ∈ [ a , b ] \xi \in [a,b] ξ∈[a,b]使得插值多项式的误差为:
R n ( x ) = f n + 1 ( ξ ) ( n + 1 ) ! Π i = 0 n ( x − x i ) R_n(x) = \frac{f^{n+1}(\xi)}{(n+1)!}\Pi_{i=0}^{n}(x-x_i) Rn(x)=(n+1)!fn+1(ξ)Πi=0n(x−xi)
3. Newton插值
1. 定义 & 实现
Newton插值公式和Lagrange插值公式本质上来说是一样的,都是用一个 n n n阶的多项式来对采样点进行拟合。
由前所述,由于n阶函数的解是唯一的,所以Newton插值公式本质上来说和Lagrange插值公式是完全等价的。
他们的区别在于具体的实现思路,Lagrange插值是平权地对每一个点进行描述,而Newton插值的思路则是通过残差的方式进行实现。
即是说,要对第 k k k个点进行拟合,只需要先拟合前 k − 1 k-1 k−1个点进行拟合,得到一个 k − 1 k-1 k−1维的函数,然后对于第k个点,就会有残差 y k − f k + 1 ( x k ) y_k - f_{k+1}(x_k) yk−fk+1(xk),然后我们用一个k维的多项式来填补这个差距即可。
综上,我们可以写出Newton插值公式的表达式如下:
N ( x ) = f ( x 0 ) + ∑ i = 1 n f [ x 0 , x 1 , . . . , x i − 1 ] Π j = 0 i − 1 ( x − x j ) N(x) = f(x_0) + \sum_{i=1}^{n} f[x_0, x_1, ..., x_{i-1}]\Pi_{j=0}^{i-1}(x-x_{j}) N(x)=f(x0)+i=1∑nf[x0,x1,...,xi−1]Πj=0i−1(x−xj)
其中, f [ x 0 , x 1 , . . . , x k ] f[x_0, x_1, ..., x_k] f[x0,x1,...,xk]就是 k k k阶差商,其定义可以通过迭代关系得到:
{ f [ x k ] = f ( x k ) . . . f [ x 0 , . . . , x k ] = f [ x 1 , . . . , x k ] − f [ x 0 , . . . , x k − 1 ] x k − x 0 \left\{ \begin{aligned} &f[x_k] = f(x_k) \\ &... \\ &f[x_0, ..., x_k] = \frac{f[x_1, ..., x_k] - f[x_0, ..., x_{k-1}]}{x_k - x_0} \end{aligned} \right. ⎩⎪⎪⎪⎨⎪⎪⎪⎧f[xk]=f(xk)...f[x0,...,xk]=xk−x0f[x1,...,xk]−f[x0,...,xk−1]
更进一步的,我们可以给出书中列举出的关于差商的几个性质如下:
性质1.1: k k k阶差商 f [ x 0 , . . . , x k ] f[x_0, ..., x_k] f[x0,...,xk]是由函数值 f ( x 0 ) , f ( x 1 ) , . . . , f ( x k ) f(x_0), f(x_1), ..., f(x_k) f(x0),f(x1),...,f(xk)的线性组合而成,即:
f [ x 0 , x 1 , . . . , x k ] = ∑ i = 0 k 1 Π j ≠ i ( x i − x j ) f ( x i ) f[x_0, x_1, ... ,x_k] = \sum_{i=0}^{k} \frac{1}{\Pi_{j \neq i} (x_i - x_j)} f(x_i) f[x0,x1,...,xk]=i=0∑kΠj=i(xi−xj)1f(xi)
性质1.2:若 i 0 , i 1 , . . . , i k i_0, i_1, ..., i_k i0,i1,...,ik为 0 , 1 , . . . , k 0, 1, ..., k 0,1,...,k的任一排列,则有:
f [ x 0 , x 1 , . . . , x k ] = f [ x i 0 , x i 1 , . . . , x i k ] f[x_0, x_1, ..., x_k] = f[x_{i_0}, x_{i_1}, ..., x_{i_k}] f[x0,x1,...,xk]=f[xi0,xi1,...,xik]
性质1.3:若 f ( x ) f(x) f(x)为 m m m次多项式,则 f [ x 0 , x 1 , . . . , x k − 1 , x ] f[x_0, x_1, ..., x_{k-1}, x] f[x0,x1,...,xk−1,x]为 m − k m-k m−k次多项式。
2. 伪代码实现
同样的,我们给出python代码演示如下:
def newton_fn(xlist, ylist):
assert(len(xlist) == len(ylist))
assert(len(set(xlist)) == len(xlist))
n = len(xlist)
nmatrix = [[0 for _ in range(n)] for _ in range(n)]
for i in range(n):
nmatrix[i][0] = ylist[i]
for j in range(1, i+1):
nmatrix[i][j] = (nmatrix[i][j-1] - nmatrix[i-1][j-1]) / (xlist[i] - xlist[i-j])
nlist = [nmatrix[i][i] for i in range(n)]
def fn(x):
s = 1
res = 0
for i in range(n):
res += nlist[i] * s
s *= (x-xlist[i])
return res
return fn
3. 误差分析
如前所述,由于Newton插值公式和Lagrange插值公式本质上是同一个多项式的两种构造方法,所以他们的误差分析是完全相同的,这里也就不再多做赘述了。
4. 分段插值
1. 定义 & 实现
上述Lagrange插值和Newton插值本质上都是用一个n阶方程来对 n + 1 n+1 n+1个点进行描述,而高阶方程存在一个极大的问题就是过拟合问题,也就是说,可能为了对某些点进行拟合而导致函数去现在插值区间内剧烈震荡。
上述现象又称为Runge现象,一个典型的例子就是 f ( x ) = 1 1 + 25 x 2 , x ∈ [ − 1 , 1 ] f(x) = \frac{1}{1+25x^2}, x \in [-1, 1] f(x)=1+25x21,x∈[−1,1]。给出的拟合点越多,函数在边界附近的震荡越厉害。
因此,这里给出另外一种插值方法,即分段插值方法,其思路极其暴力,即首先对点进行排序处理,然后对每两个邻接点之间线性都采用线性连接。
此时,对于任何一个待求的点,我们只要找到其邻接的两个点 x i , x i + 1 x_i, x_{i+1} xi,xi+1,就可以快速地给出其函数估计值:
f ( x ) = x − x i + 1 x i − x i + 1 f ( x i ) + x − x i x i + 1 − x i f ( x i + 1 ) f(x) = \frac{x - x_{i+1}}{x_i - x_{i+1}}f(x_i) + \frac{x - x_{i}}{x_{i+1} - x_{i}}f(x_{i+1}) f(x)=xi−xi+1x−xi+1f(xi)+xi+1−xix−xif(xi+1)
2. 伪代码实现
同样的,我们给出分段插值的python代码示例如下:
import bisect
def segment_fn(xlist, ylist):
assert(len(xlist) == len(ylist))
assert(len(set(xlist)) == len(xlist))
xy = sorted([(x, y) for x, y in zip(xlist, ylist)])
xlist = [x[0] for x in xy]
ylist = [x[1] for x in xy]
def fn(x):
assert(xlist[0] <= x <= xlist[-1])
i = bisect.bisect_left(xlist, x)
y = (x - xlist[i])/(xlist[i+1]-xlist[i]) * ylist[i+1] + (x - xlist[i+1])/(xlist[i]-xlist[i+1]) * ylist[i]
return y
return fn
3. 误差分析
分段插值本质上来说就是在每一个小的区间上都采用线性拟合,因此,根据上述Lagrange插值的误差分析,其误差总可以表示为:
δ ( x ) = f ( 2 ) ( ξ ) 2 ! ( x − x i ) ( x − x i + 1 ) ≤ M 2 2 ∣ ( x − x i ) ( x − x i + 1 ) ∣ ≤ M 2 2 ⋅ 1 4 ( x i + 1 − x i ) 2 = M 2 8 ( x i + 1 − x i ) 2 \begin{aligned} \delta(x) &= \frac{f^{(2)}(\xi)}{2!}(x-x_i)(x-x_{i+1}) \\ &\leq \frac{M_2}{2}|(x-x_i)(x-x_{i+1})| \\ &\leq \frac{M_2}{2} \cdot \frac{1}{4}(x_{i+1}-x_{i})^2 \\ &= \frac{M_2}{8}(x_{i+1}-x_{i})^2 \end{aligned} δ(x)=2!f(2)(ξ)(x−xi)(x−xi+1)≤2M2∣(x−xi)(x−xi+1)∣≤2M2⋅41(xi+1−xi)2=8M2(xi+1−xi)2
其中, M 2 = m a x ∣ f ( 2 ) ( x ) ∣ , x ∈ [ a , b ] M_2 = max|f^{(2)}(x)|, x \in [a, b] M2=max∣f(2)(x)∣,x∈[a,b]。
5. 三次样条插值
1. 定义
如前所述,Lagrange插值和Newton插值平滑,但是容易过拟合,反之分段插值可以有效的防止过拟合,但是在连接处不够平滑,如果采样点不够充分,则拟合效果可能不太好。
而三次样条函数则是结合了上述几种方式的优点,它依然采用的是分段插值的方式,从而避免过拟合,但是,为了增加平滑性,他在两点之间不再使用线性连接,而是采用一个三次函数,然后限制连接处位置的一阶导数和二阶导数连续,从而保证了拟合曲线整体的平滑性。
给出书中的定义如下:
给定区间 [ a , b ] [a,b] [a,b]上的 n + 1 n+1 n+1个节点 a = x 0 < x 1 < . . . < x n = b a=x_0 < x_1 < ... < x_n = b a=x0<x1<...<xn=b和这些点上的函数值 f ( x i ) = y i , i = 0 , 1 , . . . , n f(x_i) = y_i, i = 0, 1, ..., n f(xi)=yi,i=0,1,...,n。若 S ( x ) S(x) S(x)满足
S ( x i ) = y i , i = 0 , 1 , . . . , n S(x_i) = y_i, i = 0,1,...,n S(xi)=yi,i=0,1,...,n
S ( x ) S(x) S(x)在每个小区间 [ x i , x i + 1 ] [x_i, x_{i+1}] [xi,xi+1]上至多是一个三次多项式, S ( x ) S(x) S(x)在 [ a , b ] [a,b] [a,b]上有连续的二阶导数,则称 S ( x ) S(x) S(x)为 f ( x ) f(x) f(x)关于剖分 a = x 0 < x 1 < . . . < x n = b a=x_0 < x_1 < ... < x_n = b a=x0<x1<...<xn=b的三次样条插值函数,称 x 0 , x 1 , . . . , x n x_0, x_1, ..., x_n x0,x1,...,xn为样条节点。
但是,这里比较特殊的是,这里一共会有 n n n个区间, 4 n 4n 4n个待定参数,而通过 n + 1 n+1 n+1个节点的值以及邻接点上的一阶导和二阶导连续条件,我们一共只有 2 + 2 ( n − 1 ) + 2 ( n − 1 ) = 4 n − 2 2 + 2(n-1) + 2(n-1) = 4n-2 2+2(n−1)+2(n−1)=4n−2个限制条件,不足以对全部的 4 n 4n 4n个参数进行完全求解,因此,我们还需要额外提供两个边界条件,才能够完成所有的 4 n 4n 4n个参数的求解。
这里的参数求解方式需要联合矩阵的求解,因此这里就不给出代码示例了。