非线性方程求根迭代法(C++)

2024-01-15 06:28

本文主要是介绍非线性方程求根迭代法(C++),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 问题描述
  • 算法描述
    • 不动点迭代法
      • 一维情形
      • 多维情形
    • 牛顿迭代法
      • 单根情形
      • 重根情形
    • 割线法
    • 抛物线法
    • 逆二次插值法
  • 算法实现
    • 准备工作
    • 一般迭代法
    • 割线法
    • 抛物线法
    • 逆二次插值法
  • 实例分析
    • 例1
    • 例2

迭代法是一种求解非线性方程根的方法, 它通过构造一个迭代过程, 将一个非线性方程转化为一个等价的不动点方程, 然后通过迭代逼近不动点, 从而得到非线性方程的根.

迭代法的基本思想是将隐式方程转化为显式的计算公式, 然后通过迭代, 求方程近似根.

问题描述

已知方程

f ( x ) = 0 \bm f(\bm x)=\bm 0 f(x)=0

R n \mathbb R^n Rn的某个区域 D D D上有根, 求方程的近似解 x \bm x x.

算法描述

迭代法要求将上述方程化为如下形式:

x n + 1 = φ ( x n ) \bm x_{n+1}=\bm\varphi(\bm x_n) xn+1=φ(xn)

则可取 φ \bm\varphi φ为迭代函数. 而迭代函数的构造方式并不是唯一的, 不同算法的构造方式不同, 下面将逐个进行介绍.

不动点迭代法

不动点迭代法是一种求解非线性方程的数值方法, 其基本思想是将原方程变形为关于 x x x的迭代方程, 然后通过不断迭代求解 x x x的值.

一维情形

假设有非线性方程

f ( x ) = 0 f(x)=0 f(x)=0

则可以通过恒等变形改写为

x = φ ( x ) x=\varphi(x) x=φ(x)

因此, 我们可以得到如下不动点迭代公式:

x n + 1 = φ ( x n ) x_{n+1}=\varphi(x_n) xn+1=φ(xn)

故求解原方程的解就转化为求解函数 φ ( x ) \varphi(x) φ(x)的不动点. 设 x 0 x_0 x0为方程的一个近似解, 则利用上述不动点迭代公式即可逼近原方程的解.

多维情形

在一维情形的基础下, 进一步考虑非线性方程组

f ( x ) = 0 \bm f(\bm x)=\bm 0 f(x)=0

和一维情形类似, 可将方程变形为

x = φ ( x ) \bm x=\bm\varphi(\bm x) x=φ(x)

故有高维不动点迭代公式:

x n + 1 = φ ( x n ) \bm x_{n+1}=\bm\varphi(\bm x_n) xn+1=φ(xn)

牛顿迭代法

牛顿迭代法(Newton-Raphson method)是求解方程近似根的一种方法, 其基本思想是利用泰勒公式将方程进行线性化, 然后通过不断迭代, 使得误差逐渐缩小, 最终得到近似解.

单根情形

首先, 将 f ( x ) f(x) f(x) x = x 0 x=x_0 x=x0处进行泰勒展开, 得到

f ( x ) = f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) + f ′ ′ ( x 0 ) 2 ( x − x 0 ) 2 + ⋯ f(x) = f(x_0) + f^{\prime}(x_0)(x-x_0) + \frac{f^{\prime\prime}(x_0)}{2}(x-x_0)^2 + \cdots f(x)=f(x0)+f(x0)(xx0)+2f′′(x0)(xx0)2+

其中 f ′ ( x ) f^{\prime}(x) f(x) f ′ ′ ( x ) f^{\prime\prime}(x) f′′(x)分别表示 f ( x ) f(x) f(x) x x x处的导数和二阶导数.

e ( x ) = f ( x ) f ′ ( x ) e(x) = \frac{f(x)}{f^{\prime}(x)} e(x)=f(x)f(x)

则有

e ( x ) = − f ( x 0 ) f ′ ( x 0 ) − ( x − x 0 ) − f ′ ′ ( x 0 ) 2 f ′ ( x 0 ) ( x − x 0 ) 2 − ⋯ e(x) = - \frac{f(x_0)}{f^{\prime}(x_0)} - (x-x_0) - \frac{f^{\prime\prime}(x_0)}{2f^{\prime}(x_0)}(x-x_0)^2 - \cdots e(x)=f(x0)f(x0)(xx0)2f(x0)f′′(x0)(xx0)2

因此, 可以通过迭代公式

x n + 1 = x n − f ( x n ) f ′ ( x n ) x_{n+1} = x_n - \frac{f(x_n)}{f^{\prime}(x_n)} xn+1=xnf(xn)f(xn)

不断迭代求解, 直到 e ( x ) e(x) e(x)的值足够小(即达到所需的精度), 即可得到方程的近似解.

值得注意的是, 牛顿迭代法的前提是方程的导数, 即 f ′ ( x ) f^{\prime}(x) f(x)在所求解的区间内不等于零, 否则会导致方法失效. 此外, 迭代过程中可能会出现震荡或收敛于非解的情况, 此时需要采取适当的策略进行优化和调整.

重根情形

x 0 x_0 x0 f ( x ) f(x) f(x)的根, 且其重数大于 1 1 1, 此时 f ( x 0 ) f(x_0) f(x0) f ′ ( x 0 ) f^\prime(x_0) f(x0)都为 0 0 0, 这给牛顿法和后面的割线法都带来了问题, 因为在它们的公式分母中都包含导数或其估算值, 当解收敛到离根非常近时, 可能会导致除零错误.

对此, 我们可以令

u ( x ) = f ( x ) f ′ ( x ) u(x)=\frac{f(x)}{f^\prime(x)} u(x)=f(x)f(x)

则可以证明: 若 x 0 x_0 x0为原方程的 m m m重根, 则其为方程

u ( x ) = 0 u(x)=0 u(x)=0

的单根, 从而对 u ( x ) = 0 u(x)=0 u(x)=0应用牛顿迭代法即可.

割线法

f : R → R f:\mathbb R\rightarrow\mathbb R f:RR, 但函数 f f f的性质不好, 可能在某些地方不可导或导数难以计算. 这时我们就可以利用割线近似切线, 即

f ′ ( x n ) ≈ f ( x n ) − f ( x n − 1 ) x n − x n − 1 f^\prime(x_n)\approx\frac{f(x_n)-f(x_{n-1})}{x_n-x_{n-1}} f(xn)xnxn1f(xn)f(xn1)

代入牛顿迭代公式, 就得到了割线法:

x k + 1 = x k − x n − x n − 1 f ( x n ) − f ( x n − 1 ) f ( x k ) x_{k+1}=x_k-\frac{x_n-x_{n-1}}{f(x_n)-f(x_{n-1})}f(x_k) xk+1=xkf(xn)f(xn1)xnxn1f(xk)

抛物线法

抛物线法又称为米勒法, 是割线法的推广. 割线法是利用前两项构造线性插值, 而抛物线法则是利用前三项构造二次插值, 则我们可以求得如下迭代公式:

x k + 1 = x k − 2 f k ω + s g n ( ω ) ω 2 − 4 f k f [ x k , x k − 1 , x k − 2 ] x_{k+1}=x_k-\frac{2f_k}{\omega+{\rm sgn}(\omega)\sqrt{\omega^2-4f_kf[x_k,x_{k-1},x_{k-2}]}} xk+1=xkω+sgn(ω)ω24fkf[xk,xk1,xk2] 2fk

其中,

ω = f k − f k − 1 x k − x k − 1 + x k − x k − 1 x k − x k − 2 ( f k − f k − 1 x k − x k − 1 − f k − 1 − f k − 2 x k − 1 − x k − 2 ) \omega=\frac{f_k-f_{k-1}}{x_k-x_{k-1}}+\frac{x_k-x_{k-1}}{x_k-x_{k-2}}\left(\frac{f_k-f_{k-1}}{x_k-x_{k-1}}-\frac{f_{k-1}-f_{k-2}}{x_{k-1}-x_{k-2}}\right) ω=xkxk1fkfk1+xkxk2xkxk1(xkxk1fkfk1xk1xk2fk1fk2)

逆二次插值法

在抛物线法中, 当构造的抛物线不与 x x x轴相交时, 可能会收敛到方程的复数根. 这一方面体现了抛物线法的一个优点: 实的初始值迭代求得方程的复数根; 但同时也有可能无法收敛到实数根. 使用逆二次插值法可以解决这个问题.

逆二次插值法又称为反抛物线法, 即使用以 y y y为自变量的抛物线 x = g ( y ) x=g(y) x=g(y)与横轴的交点逼近函数 f ( x ) f(x) f(x)的实根. 可计算得迭代公式如下:

x k + 1 = x k − y k x k − x k − 1 y k − y k − 1 + y k y k − 1 y k − y k − 2 ( x k − x k − 1 y k − y k − 1 − x k − 1 − x k − 2 y k − 1 − y k − 2 ) x_{k+1}=x_k-y_k\frac{x_k-x_{k-1}}{y_k-y_{k-1}}+\frac{y_ky_{k-1}}{y_k-y_{k-2}}\left(\frac{x_k-x_{k-1}}{y_k-y_{k-1}}-\frac{x_{k-1}-x_{k-2}}{y_{k-1}-y_{k-2}}\right) xk+1=xkykykyk1xkxk1+ykyk2ykyk1(ykyk1xkxk1yk1yk2xk1xk2)

算法实现

准备工作

由于牵涉到高维问题, 我们首先编写一个向量之间的度量函数:

double distance(const std::vector<double> &x, const std::vector<double> &y)
{size_t n(x.size());if (!n)return 0;if (n != y.size())return NAN;double r(0);auto i = x.cend(), j = y.cend();do{double t(*--i - *--j);r += t *= t;} while (--n);return sqrt(r);
}

由于抛物线法中涉及到符号函数, 另外编写符号函数sgn:

// 符号函数
template <class T>
inline int sgn(const T &x) noexcept
{if (x == 0)return 0;if (x > 0)return 1;return -1;
}

此外, 为了方便用户输入函数, 还要编写一个字符串替换的函数:

/** 替换子串* str   : 要替换的字符串* oldStr: 旧子串* newStr: 新子串* except: 排除的子串*/
std::string &substring_replace(std::string &str, const std::vector<std::string> &oldStr, const std::vector<std::string> &newStr, const std::vector<std::string> &except)
{if (oldStr.empty()){str.clear();return str;}// if(oldStr.size()!=newStr.size())//     if(oldStr.size()>newStr.size())//         oldStr.erase(oldStr.begin()+newStr.size(),oldStr.end());//     else//         newStr.erase(newStr.begin()+oldStr.size(),newStr.end());size_t n(oldStr.size() > newStr.size() ? newStr.size() : oldStr.size());QBitArray a(str.length()); // 这里用了Qt中的QBitArray, 使用bool数组也可以实现其功能for (const auto &i : except){size_t pos(0);while ((pos = str.find(i, pos)) != std::string::npos){a.fill(true, pos, pos + i.length());pos += i.length();}}size_t pos(0);for (size_t k(0); k < n; ++k){const std::string &pre = oldStr[k], &next = newStr[k];F:while ((pos = str.find(pre, pos)) != std::string::npos){size_t i(pre.length()), p(pos);doif (a.at(p++)){pos += pre.length();goto F;}while (--i);QBitArray t(a.size() + next.size() - pre.size());dot.setBit(i, a.at(i));while (++i < pos);p = pos + pre.length();str.replace(pos, pre.length(), next);if (i != (pos += next.length()))dot.setBit(i, false);while (++i != pos);while (p != a.size())t.setBit(i++, a.at(p++));a = t;}}return str;
}

以及浮点向量转字符串向量的函数:

// double向量转string向量
std::vector<std::string> vec_to_string(const std::vector<double> &x)
{std::vector<std::string> y(x.size());auto i = x.cbegin();auto k = y.begin();while (i < x.cend())*k++ = *i++;return y;
}

一般迭代法

为了方便用户输入, 我们首先引入字符串函数计算1:

extern double calStr(string);
extern double calStr_x(string, const double &);
vector<string> func{"sqrt", "sin", "log", "exp", "pow", "abs", "cos", "tan", "asin", "acos", "atan"};

接着实现一般迭代法的单步迭代:

/** 迭代法* current_vec: 迭代向量* func       : 迭代函数* x          : 未知数** 返回(bool):*  true : 迭代失败*  false: 迭代成功*/
bool iterative_method(std::vector<double> &current_vec, const std::vector<std::string> &f, const std::vector<std::string> &x)
{std::vector<double> x0(current_vec);unsigned n(current_vec.size());do{std::string toCal(f[--n]);substring_replace(toCal, x, vec_to_string(current_vec), func);if (isnan(current_vec[n] = calStr(toCal))){current_vec = x0;return true;}} while (n);return false;
}

最终, 实现一般迭代法如下:

/** 迭代法* current_vec: 迭代向量* func       : 迭代函数* x          : 未知数* epsilon    : 迭代终止条件* mid        : 迭代中间结果导出** 返回(bool):*  true : 迭代失败*  false: 迭代成功*/
bool iterative_method(std::vector<double> &current_vec, const std::vector<std::string> &func, const std::vector<std::string> &x, double epsilon, QString &mid) // mid使用了QString, 可以对应转换为std::string或者std::vector<vector<double>>
{for (auto &i : x)mid.append(QString::fromStdString(i)).append(',');*(mid.end() - 1) = '\n';for (auto &i : current_vec)mid.append(QString::number(i, 'g', 14)).append(',');std::vector<double> x0(current_vec), x1(x0);if (iterative_method(current_vec, func, x)){current_vec = x0;return true;}*(mid.end() - 1) = '\n';for (auto &i : current_vec)mid.append(QString::number(i, 'g', 14)).append(',');while (distance(current_vec, x1) >= epsilon){x1 = current_vec;if (iterative_method(current_vec, func, x)){current_vec = x0;return true;}*(mid.end() - 1) = '\n';for (auto &i : current_vec)mid.append(QString::number(i, 'g', 14)).append(',');}mid.chop(1);return false;
}

割线法

/** 割线法* f   : 原方程函数* x1  : 第1个值* x2  : 第2个值* e   : 精度* path: 迭代保存路径*/
void secant_method(const function<double(double)> &f, double x1, double x2, double e = 1e-6, const QString &path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation))
{QFile file(path);if (!(file.open(QIODevice::WriteOnly)))throw "文件保存失败!";double f1(f(x1)), f2;if (isnan(f1))throw "区间内存在奇点!";file.write((to_string(x1) + '\n' + to_string(x2)).c_str());while (abs(x1 - x2) >= e){if (isnan(f2 = f(x2))){file.close();throw "区间内存在奇点!";}double t = x2 - (x2 - x1) * f2 / (f2 - f1);file.write(('\n' + to_string(t)).c_str());x1 = x2;x2 = t;f1 = f2;}file.close();
}

抛物线法

/** 抛物线法* f   : 原方程函数* x1  : 第1个值* x2  : 第2个值* x3  : 第3个值* e   : 精度* path: 迭代保存路径*/
void parabolic_method(const function<double(double)> &f, double x1, double x2, double x3, double e = 1e-6, const QString &path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation))
{QFile file(path);if (!(file.open(QIODevice::WriteOnly)))throw "文件保存失败!";double f1(f(x1)), f2(f(x2)), f3, omega0((f2 - f1) / (x2 - x1));if (isnan(f1) || isnan(f2)){file.close();throw "区间内存在奇点!";}file.write((to_string(x1) + '\n' + to_string(x2) + '\n' + to_string(x3)).c_str());while (abs(x1 - x2) >= e){if (isnan(f3 = f(x3))){file.close();throw "区间内存在奇点!";}double omega1 = (f3 - f2) / (x3 - x2), d123 = (omega1 - omega0) / (x3 - x1), omega = omega1 + (x3 - x2) * d123, delta = omega * omega - 4 * f3 * d123;if (delta < 0){file.close();throw "迭代出现复根!";}double t = x3 - 2 * f3 / (omega + sgn(omega) * sqrt(delta));file.write(('\n' + to_string(t)).c_str());x1 = x2;x2 = x3;x3 = t;f1 = f2;f2 = f3;omega0 = omega1;}file.close();
}

逆二次插值法

/** 逆二次插值法* f   : 原方程函数* x1  : 第1个值* x2  : 第2个值* x3  : 第3个值* e   : 精度* path: 迭代保存路径*/
void inverse_quadratic_interpolation(const function<double(double)> &f, double x1, double x2, double x3, double e = 1e-6, const QString &path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation))
{QFile file(path);if (!(file.open(QIODevice::WriteOnly)))throw "文件保存失败!";double f1(f(x1)), f2(f(x2)), f3, omega0((x2 - x1) / (f2 - f1));if (isnan(f1) || isnan(f2)){file.close();throw "区间内存在奇点!";}file.write((to_string(x1) + '\n' + to_string(x2) + '\n' + to_string(x3)).c_str());while (abs(x1 - x2) >= e){if (isnan(f3 = f(x3))){file.close();throw "区间内存在奇点!";}double omega1 = (x3 - x2) / (f3 - f2), t = x3 - omega1 * f3 + (omega1 - omega0) * f3 * f2 / (f3 - f1);file.write(('\n' + to_string(t)).c_str());x1 = x2;x2 = x3;x3 = t;f1 = f2;f2 = f3;omega0 = omega1;}file.close();
}

实例分析

例1

设有方程 x 3 + 2 x 2 + 10 x − 20 = 0 x^3+2x^2+10x-20=0 x3+2x2+10x20=0.

首先计算出 f ′ ( x ) = 3 x 2 + 4 x + 10 f^\prime(x)=3x^2+4x+10 f(x)=3x2+4x+10, 下面我们讨论选取不同初值时Newton迭代法的收敛速度.

分别取 x 0 ∈ { x ∈ Z : − 128 ⩽ x ⩽ 127 } x_0\in\{x\in\mathbb Z:-128\leqslant x\leqslant 127\} x0{xZ:128x127}, 并取终止标准为 ∣ f ( x n ) ∣ < 1 0 − 6 |f(x_n)|<10^{-6} f(xn)<106, 计算得其误差与迭代次数如下所示:


观察可得, 在 x 0 = 1 x_0=1 x0=1附近时迭代次数较少, 收敛较快, 接着在 1 1 1附近取更密集的初值, 计算结果如下所示:


利用卡当公式, 我们可以求得方程有唯一实根

x = − 2 3 − 13 4 3 3 3 3930 + 176 3 + 1 3 6 3930 + 352 3 = 1.36880 ⋯ x=-\frac23-\frac{13\sqrt[3]{4}}{3\sqrt[3]{3\sqrt{3930}+176}}+\frac13\sqrt[3]{6\sqrt{3930}+352}=1.36880\cdots x=323333930 +176 1334 +31363930 +352 =1.36880

可见当初值越靠近根 x x x, 迭代次数越少, 收敛速度越快.

例2

取初值 x 0 = 4 , x 1 = 3.8 x_0=4,x_1=3.8 x0=4,x1=3.8, 求方程 x 3 − 2 x − 5 = 0 x^3-2x-5=0 x32x5=0 [ 1 , 4 ] [1,4] [1,4]上的根.

仍然取终止标准为 ∣ f ( x n ) ∣ < 1 0 − 6 |f(x_n)|<10^{-6} f(xn)<106, 使用Newton迭代法可得误差为 − 1.77636 × 1 0 − 5 -1.77636\times10^{-5} 1.77636×105, 迭代次数为6次; 取终止标准为 ∣ x n + 1 − x n ∣ < 1 0 − 6 |x_{n+1}-x_n|<10^{-6} xn+1xn<106, 使用割线法可得误差为 2.37144 × 1 0 − 13 2.37144\times10^{-13} 2.37144×1013, 迭代次数为8次.


  1. 参见C++实现简单计算器(字符串替换). ↩︎

这篇关于非线性方程求根迭代法(C++)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/607954

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

C++中detach的作用、使用场景及注意事项

《C++中detach的作用、使用场景及注意事项》关于C++中的detach,它主要涉及多线程编程中的线程管理,理解detach的作用、使用场景以及注意事项,对于写出高效、安全的多线程程序至关重要,下... 目录一、什么是join()?它的作用是什么?类比一下:二、join()的作用总结三、join()怎么

C++中全局变量和局部变量的区别

《C++中全局变量和局部变量的区别》本文主要介绍了C++中全局变量和局部变量的区别,全局变量和局部变量在作用域和生命周期上有显著的区别,下面就来介绍一下,感兴趣的可以了解一下... 目录一、全局变量定义生命周期存储位置代码示例输出二、局部变量定义生命周期存储位置代码示例输出三、全局变量和局部变量的区别作用域