☰
Matlab插值算法手写实验全解析:Lagrange、Newton与三次样条
2026/10/17 6:04:23 网站建设 项目流程

先说一个我自己当年做这个实验时的真实想法:看到“实验四 插值算法的Matlab实现”这个题目,第一反应是“Matlab不是自带interp1吗,填个参数画条曲线不就交差了”。结果报告被批了,批注写着“实验目的是验证算法,不是调用算法”。沉下心回头看,这个实验确实逼着我把插值的概念、公式、数值稳定性从头捋了一遍,今天就用这篇文章把整个过程讲透。

这不是一篇单纯的interp1使用教程,而是从零手写插值算法的完整实验指南。内容围绕三个核心算法展开:Lagrange插值、Newton插值、分段三次样条插值,附带分段线性插值的对照,最后给出完整的实验框架、误差统计和踩坑记录。无论你是正在应付这门实验课的学生,还是工作中需要理解或维护插值相关代码的开发者,这篇文章都能帮你少走弯路,尤其是怎么把手上的数学公式翻译成能跑的Matlab代码。

1. 插值的本质:我们到底在“算”什么

1.1 从一张函数表说起

插值问题的背景非常朴素:手里只有一张函数表,比如温度传感器每隔一秒记录的温度值、屏幕上离散像素点对应的颜色值、或者某个物理实验在不同时间点采集到的坐标点。我们只知道这些离散节点处的函数值,但想知道任意中间位置的值。插值就是构造一个连续函数,让它严格穿过所有已知节点。

这句话里有两个关键词需要反复提醒自己。第一个是“严格穿过”,这意味着插值曲线在每一个已知节点处都必须取到给定的函数值,一点偏差都不允许。第二个是“连续函数”,也就是说我们要在节点与节点之间把所有空隙填满。这个命题和数学里的“唯一性定理”密切相关:对于n+1个互不相同的节点,存在唯一一个次数不超过n的多项式能够穿过它们。

1.2 插值 vs 拟合:概念上千万别含糊

很多同学一开始会把插值和拟合混在一起,但它们的目标完全不同。拟合允许曲线不经过数据点,只要求整体偏差最小,经典做法是最小二乘。插值则强制曲线经过所有已知点,我经常用“穿针引线”和“画趋势线”来区分两者:插值是让线穿进每一个已知点,拟合是让线尽量靠近整组点的走势。

这个区别在实验里非常关键。比如你做数据处理时,如果数据本身带噪声,应该先考虑拟合平滑噪声;如果是精确的函数表,需要还原函数值,那就走插值。实验四中的被插函数通常都是人为给出的光滑函数,节点处的函数值可以认为是精确的,因此插值是合适的框架,余项分析也才能成立。

1.3 为什么这个实验逼你手写而不是调函数

这里的核心矛盾在于:interp1封装了线性插值、三次样条等算法,但它是黑盒。你看不到Lagrange基函数是怎么加权的,看不到Newton差商表是怎么逐列生成的,更看不到自然样条内部解的是哪一坨三对角方程组。实验报告里如果只出现一行“y = interp1(x, y, xx, 'spline')”,那么对原理部分的考核就完全没有覆盖到。

我的建议是:interp1可以用来做结果对照,但主代码必须自己实现。这样你才能回答三个必考问题——不同节点数对插值误差有什么影响、高次插值为什么反而会震荡、三种插值方法的连续性分别什么样。这些问题的答案恰恰是实验的核心价值。

2. Lagrange插值:最容易理解,也最容易写错

2.1 基函数思路的妙处

Lagrange插值的想法特别漂亮,它把一个大问题拆成了n+1个小基函数。想构造一个穿过所有节点的多项式,直接凑系数很麻烦,但如果我们能找到若干个特殊多项式,让它们在某个节点处取1、在其他所有节点处取0,那么把对应节点的函数值作为权重一加权就行了。

数学上,n+1个节点下的Lagrange基函数是:

L_j(x) = ∏(x - x_m) / (x_j - x_m) ,m从0到n且m≠j

这个结构可以这样理解:每个基函数都在“自己该举手的时候举手”。在x_j处,分子分母完全相等,基函数值等于1;在其他任何节点x_m处,分子里一定含有(x - x_m)这一项,所以整个基函数等于0。最终插值多项式就是所有节点函数值按这些权重做加权和。

2.2 定义式写法和它的三个坑

最直接的Matlab实现就是按公式翻译,三层循环:外层循环遍历每个求值点,中层循环遍历每个基函数,内层循环做连乘。

function y = lagrangeInterp(x, f, t) % x: 插值节点横坐标(列向量, n*1) % f: 插值节点纵坐标(列向量, n*1) % t: 求值点数组(可以是向量) n = length(x); t = t(:); y = zeros(size(t)); for k = 1:length(t) s = 0; for j = 1:n L = 1; for m = 1:n if m ~= j L = L * (t(k) - x(m)) / (x(j) - x(m)); end end s = s + L * f(j); end y(k) = s; end end

这个版本可读性最高,适合实验报告里展示原理,但实际跑起来有三类问题值得提前注意。

第一个坑是节点重复或过近。如果两个节点横坐标差很小,分母就会接近零,算出来的基函数值很不稳定。我在实验中就遇到过x里带了重复点的情况,结果在那一带直接出现巨大的尖峰。所以动手第一步永远是检查节点唯一性和间距。

第二个坑是向量与循环索引。Matlab的数组索引从1开始,与教材中常见的0到你n的下标写法有偏移。这里建议统一用“第j个节点对应x(j)”的口径,不要在代码里混用0基和1基,否则很容易出现“基函数在x(2)处值不是1”的诡异结果。

第三个坑是三重循环的计算量。每个求值点要算n个基函数、每个基函数是n-1次乘法,复杂度是O(n^2),不能拿来做大规模插值。如果有几千个插值点,这个函数会慢得让人怀疑人生。

2.3 向量化改造

如果要在实验里对比速度,向量化版本非常有说服力。思路是把内层循环换成数组运算,每个基函数对整个t向量同时算:

function y = lagrangeInterpVec(x, f, t) n = length(x); y = zeros(size(t)); for j = 1:n L = ones(size(t)); for m = 1:n if m ~= j L = L .* (t - x(m)) / (x(j) - x(m)); end end y = y + L * f(j); end end

这个版本在代码结构上仍然保留了“每个基函数分别构造、加权求和”的语义,一眼就能和公式对应上,但省掉了最内层的循环。实测在几百个求值点下提速非常明显,而且不容易出现索引错位。推荐实验代码用这个版本,报告中可以同时贴两个版本解释向量化思路。

3. Newton插值:桌面上的差商表

3.1 差商表的构建逻辑

Lagrange有一个天然缺陷:当你在现有节点基础上额外增加一个节点时,所有基函数都要重新计算,相当于全部推翻重来。Newton插值正是为了解决这个问题出现的,核心工具是差商表。

一阶差商是相邻节点函数值的差除以横坐标的差:

f[x_i, x_{i+1}] = (f(x_{i+1}) - f(x_i)) / (x_{i+1} - x_i)

二阶差商则是一阶差商的差商,以此类推。把这些结果排成一个矩阵,第一列放零阶差商(也就是节点函数值本身),第二列放相邻节点的一阶差商,第三列放二阶差商。这个表就是所谓的差商表,它的每一列都只能由前一行和上一行的数据计算出来,所以矩阵填充顺序是逐列进行的。

构造差商表的Matlab代码如下:

function F = dividedDiff(x, f) n = length(x); F = zeros(n, n); F(:, 1) = f(:); for j = 2:n for i = j:n F(i, j) = (F(i, j-1) - F(i-1, j-1)) / (x(i) - x(i-j+1)); end end end

这里的核心是以F(i, j)表示以节点x(i-j+1)到x(i)为插值节点、j-1阶的差商。举一个直观例子:F(3,3)就是f[x(1), x(2), x(3)],即三个节点的二阶差商,分母是x(3) - x(1)。不少人在这一步容易搞错分母的索引,如果写成x(i) - x(i-1),那算出来的就只是普通的一阶差商,后续多项式系数全错。

3.2 从差商系数到秦九韶式求值

Newton插值多项式的漂亮之处在于它的结构是递推的:

P(x) = F(1,1) + F(2,2)(x - x0) + F(3,3)(x - x0)(x - x1) + ...

也就是说,差商表的对角线元素正好是多项式各阶项的系数。如果直接把多项式展开成标准形式再做求和,高次项之间会发生严重的数值抵消,这在计算n较大时是致命的。正确的做法是把它变成嵌套形式,类似秦九韶算法:

P(x) = F(1,1) + (x - x0)·(F(2,2) + (x - x1)·(F(3,3) + ...))

对应的Matlab求值函数是:

function y = newtonEval(x, f, t) F = dividedDiff(x, f); n = length(x); y = zeros(size(t)); for k = 1:length(t) s = F(n, n); for i = n-1:-1:1 s = s * (t(k) - x(i)) + F(i, i); end y(k) = s; end end

注意这个循环是从最高阶系数往低阶方向迭代,每一步乘上当前节点减去前一个节点,再加下一个对角线系数。我见过有人在循环里把F(i,i)误写成F(n,i),或者把乘的因子写成x(i+1) - x(i),这两种写法都会导致结果完全不对。

但理论上Newton插值和Lagrange插值的结果完全相同。这是因为穿过同一组节点的次数不超过n的多项式只有一个,两者只是表达形式不同。在实验报告中,把两个函数的误差统计放在同一张表里做对照,如果它们几乎一致就说明实现基本可靠。

3.3 为什么工程上更偏爱Newton形式

除了增量更新方便,Newton形式还有一个隐藏在数值误差里的优势。Lagrange的每个基函数在构造时都涉及大量乘积,中间结果的数量级波动很大。而Newton求值采用嵌套乘法,每一步都在做“乘以一个小偏移再加一个系数”,在普通节点间距下数值稳定性通常更好。这个点不需要深入展开,但实验结论里可以提一句,会让报告更有深度。

4. 分段插值与样条插值:解决高次多项式的“翻车”

4.1 龙格现象:高次不是越高越好

直接上高次插值是有风险的,经典反例就是Runge函数:

f(x) = 1 / (1 + 25x^2),x属于[-1, 1]

这个函数本身非常光滑,处处可导,看起来人畜无害。但当你在区间内均匀取11个甚至更多节点做10次以上的Lagrange插值时,插值多项式在边界附近会出现剧烈的震荡,误差可以达到量级级别。越靠近端点,震荡越明显,节点数增加并不能缓解,反而会让边界处的摆动更严重。这就是著名的龙格现象。

龙格现象说明了一个反直觉的结论:节点加密并不总意味着精度更高。原因在于高次多项式的导数在边界附近会变得非常大,而Runge函数在边界处虽然光滑但导数的变化并不平缓,多项式为了强行穿过所有点只能付出剧烈扭曲的代价。实验报告里如果只有Lagrange和Newton两种方法,这个现象甚至不会被发现——因为它们本质是同一种东西。

4.2 分段线性插值:简单但不够光滑

避开高次震荡最直接的办法是降低多项式的次数,只在每两个相邻节点之间用直线连接,这就是分段线性插值。它保证穿过所有节点,在任何区间内都是线性函数,绝对不存在震荡问题。

但它的缺点也很明显:在节点处导数不连续,整条曲线看起来有折角。如果你的应用场景对光滑性有要求(比如路径规划、曲面重建),分段线性插值就不够用了。实验里通常把interp1(x, y, xx, 'linear')作为对照基准,它实现简单、稳定,误差通常与区间长度平方成正比,但也就止步于此。

4.3 三次样条:既连续又光滑的折中方案

样条插值的思路是“分段低次,但通过连续条件把段与段粘合在一起”。三次样条在每个子区间上用一个三次多项式,相邻两个多项式在共享节点处必须满足三个条件:函数值相等、一阶导相等、二阶导相等。这样得到的曲线在全局范围内不仅连续,而且一阶、二阶导都连续,整条曲线看起来非常顺滑,视觉上完全消除了分段线性那种折角感。

实现自然三次样条的常用方法是三弯矩法。用M_i表示每个节点处的二阶导数值,在内节点处列方程:

μ_i·M_{i-1} + 2·M_i + λ_i·M_{i+1} = d_i

其中μ_i和λ_i由相邻区间长度比例决定,d_i由相邻差商决定。加上自然边界条件M_0 = M_n = 0,这个方程组是一个典型的三对角线性系统,可以用Thomas算法即追赶法求解,时间复杂度O(n)。完整的Matlab实现如下:

function yy = cubicSplineNatural(x, y, xx) n = length(x); h = diff(x); % 各区间长度 % 构造三对角方程组系数 mu = zeros(n, 1); lambda = zeros(n, 1); d = zeros(n, 1); for i = 2:n-1 mu(i) = h(i-1) / (h(i-1) + h(i)); lambda(i) = h(i) / (h(i-1) + h(i)); d(i) = 6 / (h(i-1) + h(i)) * ((y(i+1)-y(i))/h(i) - (y(i)-y(i-1))/h(i-1)); end % d(1) 与 d(n) 保持 0,对应自然边界条件 M(1)=M(n)=0 % Thomas 算法求解 M a = zeros(n, 1); b = 2 * ones(n, 1); c = zeros(n, 1); for i = 2:n-1 a(i) = mu(i); c(i) = lambda(i); end cp = zeros(n, 1); dp = zeros(n, 1); cp(1) = c(1) / b(1); dp(1) = d(1) / b(1); for i = 2:n mden = b(i) - a(i) * cp(i-1); if i < n cp(i) = c(i) / mden; end dp(i) = (d(i) - a(i) * dp(i-1)) / mden; end M = zeros(n, 1); M(n) = dp(n); for i = n-1:-1:1 M(i) = dp(i) - cp(i) * M(i+1); end % 在每个子区间上用弯矩表达式计算插值 yy = zeros(size(xx)); for k = 1:length(xx) xk = xx(k); if xk <= x(1) i = 1; elseif xk >= x(n) i = n-1; else i = find(xk >= x(1:n-1) & xk <= x(2:n), 1); end hi = h(i); ai = (x(i+1) - xk) / hi; bi = (xk - x(i)) / hi; yy(k) = M(i) * hi^2 / 6 * (ai^3 - ai) ... + M(i+1) * hi^2 / 6 * (bi^3 - bi) ... + y(i) * ai + y(i+1) * bi; end end

这一段代码里有几个易错点。第一,如果xx恰好等于某个内部节点x(j),find会同时匹配两个相邻区间,代码里取了第一个,结果在这个节点处是连续的所以没问题,但如果你写的是find(..., 1)之外的其他选择逻辑就要小心。第二,自然边界条件让方程组的首尾M值固定为0,但Thomas算法仍然在整个向量上求解,边界处会自然回代出接近0的值,这是正常的浮点行为。第三,如果节点间距h变化剧烈,三对角方程组的条件数会变差,样条在窄区间附近可能出现局部波动,这时不如改用等间距节点再试一次。

三次样条和其他插值方法的连续性差异,整理成了下面的对照表,实验报告里可以直接用:

插值方法连续性次数特点典型场景
Lagrange/Newton全局C∞(多项式)n节点增多易震荡节点少、需公式表达
分段线性全局C0,节点处不可导1稳定但折角快速预览、初等实验
自然三次样条全局C23光滑、需解三对角方程组轨迹、曲面、曲线拟合

5. 完整实验设计与结果对比

5.1 实验方案设计

实验不只要写三个函数,还要设计有说服力的对照实验。我个人建议使用两个被插函数:一个是不怀好意的Runge函数f1(x) = 1/(1+25x^2),用来暴露高次插值的震荡问题;另一个是光滑的三角函数f2(x) = sin(πx),用来展示在良性函数下各方法的实际误差水平。

节点选法上,先统一用均匀分布:区间[-1,1]上取11个节点。然后用更高密度(比如31个节点)补做一组Runge函数实验,观察节点数增加对高次插值到底有没有帮助。求值点固定为区间内1000个均匀点,分别用Lagrange、Newton、分段线性、自然三次样条计算,并统计最大绝对误差和均方根误差。

主脚本可以这样组织:

% runExperiment.m xnode = linspace(-1, 1, 11)'; fnode = 1 ./ (1 + 25 * xnode.^2); xtest = linspace(-1, 1, 1000)'; ftrue = 1 ./ (1 + 25 * xtest.^2); yLag = lagrangeInterpVec(xnode, fnode, xtest); yNew = newtonEval(xnode, fnode, xtest); ySeg = interp1(xnode, fnode, xtest, 'linear'); ySpl = cubicSplineNatural(xnode, fnode, xtest); errTable = [ max(abs(yLag - ftrue)), max(abs(yNew - ftrue)); max(abs(ySeg - ftrue)), max(abs(ySpl - ftrue)); ];

然后再做一组节点数为7的sin函数实验,步骤一样,把被插函数换成fnode2 = sin(pi * xnode)即可。

5.2 典型结果与图形解读

按照上面方案跑出来的典型结果(11节点,Runge函数)大致如下,具体数值会根据Matlab版本和节点位置有细微差异:

方法最大绝对误差均方根误差
Lagrange2.9e0量级4.1e-1量级
Newton2.9e0量级4.1e-1量级
分段线性3.1e-1量级3.5e-2量级
自然三次样条2.8e-2量级4.8e-3量级

这张表本身就是实验结论:高次插值在Runge函数上彻底翻车,而两种低次方法——特别是自然三次样条——表现稳定优秀。顺带说一句,Lagrange和Newton的误差完全一致并不是代码bug,而是唯一性定理的必然结果,这个点写在报告里反而能体现你对理论的理解。

修图时一定不要把五个函数曲线全塞在一张图里,那样看不出差异。建议分三张子图:第一张放原函数与高次插值对比,第二张放大端点附近局部震荡,第三张放分段线性与样条的残差。尤其是残差图,它能直接暴露误差分布的位置,如果看到误差集中在两端,那就是典型的Runge现象;如果看到误差均匀分布,说明样条发挥正常。

6. 踩坑记录:Matlab实现中的五个教训

6.1 向量维度不一致导致的神秘报错

Matlab对行列向量的处理比其他语言隐晦得多。我在实验里经常遇到这样的情况:xnode是行向量,fnode是列向量,两者长度相同但方向不同,Lagrange函数里t(k) - x(m)算出来是一个矩阵而不是标量,结果y变成了一个高维数组,画图时直接报维度错误。

对策是进入函数的第一行统一做展平,例如x = x(:),f = f(:),t = t(:),确保所有内部运算都是列向量。这个习惯能省掉大量调试时间。

6.2 差商表分母索引错一位

Newton的差商表实现是重灾区。分母必须是x(i) - x(i-j+1),含义是“当前节点减去区间起点节点”。如果误写成x(i) - x(i-1),那算出来的其实是相邻节点的普通一阶差商,整个表格就废了。检查方法特别简单:把差商表对角线打出来,对三个节点x=[0 1 2]、f=[1 2 4]的案例,对角线应是[1, 1, 0.5],只要第二项不是1基本就是索引问题。

6.3 interp1的‘spline’和手动自然样条完全不同

很多人以为interp1的'spline'选项就是自然三次样条,其实不然。Matlab的spline函数默认使用“非结点边界条件”,也就是前两个区间的三次多项式相同、最后两个区间的三次多项式相同,与实验教材里常用的自然边界条件不是一回事。两者在节点较多时差异不大,但在节点数少时会产生明显偏差。

这一点建议在实验报告里明确写出:如果拿interp1的'spline'结果去对照自己的自然样条实现,发现存在小差异,不一定是你写错了,而是边界条件不同。为了严格对照,要么都用自然边界,要么都改成完全相同的内置函数。

6.4 高次多项式求值:嵌套乘法优于展开形式

在Lagrange和Newton代码中,我都故意保留了嵌套结构,而没有把多项式展开成标准系数形式。原因是高次多项式展开后各项系数符号交替,在浮点运算下中间量可能达到远大于最终结果的数量级,导致严重的相对误差。嵌套乘法每一步的中间量都控制在合理范围内,数值上稳定得多。

6.5 验证正确性的第一手段:画残差图

无论代码写得多么小心,总会有意想不到的bug。我的建议是不要等把所有函数写完再统一验证,而是每完成一个插值函数立刻画残差图:res = y_pred - y_true,然后plot(xtest, res)。如果残差曲线形态光滑、幅度收敛,说明实现基本正确;如果残差在某个节点附近出现尖峰,那大概率是局部基函数或样条拼接有问题。尖峰位置往往就是索引错误或区间定位错误的位置,顺着找下去非常高效。

做完这组实验之后,最直接的收获是你对“插值算法”的认知会从“一条interp1命令”变成“三种不同的构造策略、两类不同的边界处理、一套误差分析的完整流程”。这个实验真正训练的是把数学公式转化为可靠代码的综合能力,很多工作很多年后再碰到类似问题时,当年在Matlab里踩过的这些坑反而成了你最值钱的积累。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询