非线性一元方程求解——弦截法、牛顿迭代法(C++)

一元方程

对于一元方程,如果要求f(x)=0的解,其过程大致包括如下三个问题:

  1. 根的存在性:是否有根,如果有,有几个?
  2. 根的分布:根分布区间;
  3. 求根的公式:如何从根的近似值逐步精确化?

求解方法

首先,对于某些特殊放入函数 f ( x ) f(x) f(x),如果满足以下定理: f ( x ) f(x) f(x)在区间 [ a , b ] [a,b] [a,b]上连续, f ( a ) ⋅ f ( b ) < 0 f(a)\cdot f(b)<0 f(a)f(b)<0,则 f ( x ) = 0 f(x)=0 f(x)=0在区间 [ a , b ] [a,b] [a,b]上至少存在一个根。
然后就需要确定根的分布,此处有两种方法:

  1. 搜索法:将区间 [ a , b ] [a,b] [a,b]分割为n个子区间,子区间长度为 Δ x \Delta x Δx,如果 f ( x i ) ⋅ f ( x i + 1 ) < 0 ( i = 0 , ⋯   , n ) f(x_i)\cdot f(x_{i+1})<0(i=0,\cdots,n) f(xi)f(xi+1)<0(i=0,,n),则根在区间 [ x i , x i + 1 ] [x_i,x_{i+1}] [xi,xi+1]内。
  2. 对分法:将区间 [ a , b ] [a,b] [a,b]从中点分开,分别计算 f ( a ) ⋅ f ( ( a + b ) / 2 ) f(a)\cdot f((a+b)/2) f(a)f((a+b)/2) f ( b ) ⋅ f ( ( a + b ) / 2 ) f(b)\cdot f((a+b)/2) f(b)f((a+b)/2),判断哪个乘积小于0,继续对分对应区间,直至满足精度。

对于一般的情况,对分法的搜索速度较快,但是如果存在根与某个区间端点非常靠近的情况,搜索法反而速度更快。对分法还有一个问题,若区间内部存在多个根,可能会出现遗漏和算法报错等原因,因此需要根据实际需要选取对应的方法确定根的分布。
当初始区间较大时,对于上述两种方法其收敛速度都不太理想,因此引入下面的方法:

  1. 牛顿迭代法:将原方程进行泰勒展开,可以发现 f ( x ) = f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) + f ′ ′ ( ξ ) 2 ! ( x − x 0 ) 2 f(x)=f(x_0)+f'(x_0)(x-x_0)+\frac{f''(\xi)}{2!}(x-x_0)^2 f(x)=f(x0)+f(x0)(xx0)+2!f′′(ξ)(xx0)2,因此有 f ( x ) ≈ f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) f(x)\approx f(x_0)+f'(x_0)(x-x_0) f(x)f(x0)+f(x0)(xx0),由此得到牛顿迭代式: x k + 1 = x k − f ( x k ) f ′ ( x k ) x_{k+1}=x_k-\frac{f(x_k)}{f'(x_k)} xk+1=xkf(xk)f(xk)。可是牛顿迭代法的收敛性取决于 x 0 x_0 x0的选取,因此使用牛顿迭代法之前还需要满足如下条件:
    • f ( a ) ⋅ f ( b ) < 0 f(a)\cdot f(b)<0 f(a)f(b)<0;
    • f ′ ( x ) ≠ 0 f'(x)\neq 0 f(x)=0;
    • f ′ ′ ( x ) f''(x) f′′(x)存在且不变号;
    • x 0 ∈ [ a , b ] x_0\in [a,b] x0[a,b],使得 f ′ ′ ( x 0 ) ⋅ f ( x 0 ) > 0 f''(x_0)\cdot f(x_0)> 0 f′′(x0)f(x0)>0;
  2. 弦截法:相比于牛顿迭代法,弦截法不需要使用导数,使用差商进行计算,如果 f ( x ) f(x) f(x)比较复杂,即使是近似计算导数也非常不方便。根据这个思路可以得到弦截法的迭代公式: x k + 1 = x k − ( x k − x k − 1 ) f ( x k ) f ( x k ) − f ( x k − 1 ) x_{k+1}=x_k-(x_k-x_{k-1})\frac{f(x_k)}{f(x_k)-f(x_{k-1})} xk+1=xk(xkxk1)f(xk)f(xk1)f(xk)。弦截法的使用条件与牛顿迭代法相同,收敛速度也较牛顿法慢,并且需要两个初值。

代码实现

首先是牛顿法的实现:

#include<iostream>
#include<cmath>
#include <chrono>//计时模块
#include <iomanip>
#define DerivativeIntervalAccuracy 1e-6
//近似求导,传递函数进来
double f_derivative(double (*fx)(double),double x)
{
    return (fx(x + DerivativeIntervalAccuracy) - fx(x))/DerivativeIntervalAccuracy;
}

//指定函数
double fx(double x)
{
    //计算一个复杂函数
    return pow(x,4)-11.7*log10(x+3)+exp(-x)-5;
}
double Newton_iterative(double (*fx)(double x),double x_0,double error)
{
    double x_1=0.0,x_2=x_0;//x_1表示x_k-1,x_2表示x_k
    double d_fx;
    int i = 0;
    do{
        x_1=x_2;
        d_fx=f_derivative(fx, x_1);
        x_2=x_1-fx(x_1)/d_fx;
        if(i++>10000)
        {
            std::cout<<"迭代次数过多"<<std::endl;
            return 0.0;
        }
        //std::cout<<"x(k)="<<x_1<<" "<<"x(k+1)="<<x_2<<" "<<std::endl;
    }while(fabs(x_2-x_1)>error);
    //std::cout<<"迭代次数"<<i<<std::endl;
    return x_2;
}
int  main()
{
    double error=1e-6;
    double x;
    double x_0=100.0;//初始值
    double d_fx = 0.0;//导数
    auto start = std::chrono::steady_clock::now();
    x = Newton_iterative(fx, x_0, error);
    // 结束计时
    auto end = std::chrono::steady_clock::now();

    // 计算时间差
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    // 输出时间差(以毫秒为单位)
    std::cout << "Elapsed time: " << duration.count() << " microseconds" << std::endl;
    
    std::cout<<"x="<<std::setprecision(10)<<x<<std::endl;
    return 0;
}

其次是弦截法的实现:

#include<iostream>
#include<cmath>
#include <iomanip>
#include <chrono>//计时模块
using namespace std;
//指定函数
double fx(double x)
{
    //计算一个复杂函数
    return pow(x,4)-11.7*log10(x+3)+exp(-x)-5;
}
double chord_truncation(double (*fx)(double x),double x_0,double x_1,double error)
{
    int i = 0;
    double x_2,x_3=x_0,x_4=x_1;
    do{
        x_2 = x_3;
        x_3 = x_4;
        x_4=x_3-(x_3-x_2)*(fx(x_3)/(fx(x_3)-fx(x_2)));
        if(i++>10000)
        {
            std::cout<<"迭代次数过多"<<std::endl;
            return 0.0;
        }
        cout<<"x(k-1)="<<x_2<<" "<<"x(k)="<<x_3<<" "<<"x(k+1)="<<x_4<<endl;
    }while(fabs(x_3-x_2)>error);
    //std::cout<<"迭代次数"<<i<<std::endl;
    return x_4;
}
int  main()
{
    double error=1e-6;
    double x_1=10.0,x_2=0.0;//x_1表示x_k-1,x_2表示x_k
    auto start = std::chrono::steady_clock::now();
    double x=chord_truncation(fx,x_1,x_2,error);
      // 结束计时
    auto end = std::chrono::steady_clock::now();

    // 计算时间差
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    // 输出时间差(以毫秒为单位)
    std::cout << "Elapsed time: " << duration.count() << " microseconds" << std::endl;
    
    cout<<"x="<<std::setprecision(15)<<x<<endl;
    return 0;
}

结果分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对于牛顿法,为了避免cout输出的输出影响测试,第一次测试了算法的执行时间,耗时9ms,总共迭代了18次,最终结果在matlab中可以看出非常接近0,使用牛顿迭代法时需要注意初始点的选择,这决定了算法的迭代速度。
在这里插入图片描述
在这里插入图片描述

对于弦截法,如果将初始的 x 0 、 x 1 x_0、x_1 x0x1换为10和100,运行结果如下:
在这里插入图片描述

对于弦截法,其迭代次数与两个初始点选择都相关。在与matlab中测试结果发现,其结果精度更高,更加接近0。
在这里插入图片描述
另外地,如果错误地选择某些点,迭代结果会出现错误,例如在牛顿迭代法和弦截法中初始点包括0,就会出现下面报错:
在这里插入图片描述

这是因为在迭代过程中产生了某个时刻的x<-3,而函数里面包括了 log ⁡ 10 ( x + 3 ) \log_{10}(x+3) log10(x+3),但是将初始点换为-2时,又能求出此函数的另外一个解:
在这里插入图片描述

那为什么会迭代发散呢?使用matlab将函数图像绘制出来:
在这里插入图片描述

结合上面提到的牛顿迭代法的使用条件,当初始值为0时,显然不满足第四条。
综上所述可以得出如下结论:无论是牛顿迭代法还是弦截法都需要注意初始点的选择,这不仅关系到算法速度,还关系到能否得到正确的结果。

  • 42
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值