每日算法 — 使用java实现割线法:免导数方程求根与超线性收敛

引言

割线法(Secant Method)是数值分析中求解非线性方程f(x)=0的经典迭代方法。作为牛顿迭代法的免导数替代方案,割线法通过两点连线的斜率近似导数,避免了牛顿法中计算导数的开销。该方法具有超线性收敛速度(收敛阶约为1.618),在实际工程计算中应用广泛。本文将用Java实现割线法,分析其收敛性,并与牛顿法进行对比。

算法原理

从牛顿法到割线法

牛顿迭代公式为:x_{n+1} = x_n – f(x_n) / f'(x_n)

当导数f'(x)难以计算或计算成本较高时,割线法用差商代替导数:

f'(x_n) ≈ [f(x_n) – f(x_{n-1})] / [x_n – x_{n-1}]

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

x_{n+1} = x_n – f(x_n) * (x_n – x_{n-1}) / [f(x_n) – f(x_{n-1})]

几何解释

割线法每次迭代用通过(x_{n-1}, f(x_{n-1}))和(x_n, f(x_n))两点的割线与x轴的交点作为下一个近似解,而牛顿法使用切线。

收敛阶分析

割线法的收敛阶p满足方程 p² = p + 1,解得 p = (1+√5)/2 ≈ 1.618(黄金比例)。这意味着误差满足 |e_{n+1}| ≈ C|e_n|^1.618,属于超线性收敛。

Java实现

import java.util.function.DoubleFunction;

/**
 * 割线法(Secant Method)求根实现
 * 免导数的非线性方程数值解法
 */
public class SecantMethod {

    /**
     * 割线法求根
     *
     * @param f        目标函数 f(x)
     * @param x0       初始近似值1
     * @param x1       初始近似值2
     * @param epsilon  收敛精度阈值
     * @param maxIter  最大迭代次数
     * @return 方程的根
     * @throws RuntimeException 若迭代不收敛
     */
    public static double solve(DoubleFunction<Double> f,
                                double x0, double x1,
                                double epsilon, int maxIter) {
        double fx0 = f.apply(x0);
        double fx1 = f.apply(x1);

        System.out.printf("割线法求根,初始值: x0=%.6f, x1=%.6f%n", x0, x1);
        System.out.printf("iter 0: x = %.10f, f(x) = %.10f%n", x0, fx0);
        System.out.printf("iter 1: x = %.10f, f(x) = %.10f%n", x1, fx1);

        for (int i = 2; i <= maxIter; i++) {
            // 割线法核心迭代公式
            // x_{n+1} = x_n - f(x_n) * (x_n - x_{n-1}) / (f(x_n) - f(x_{n-1}))
            double denominator = fx1 - fx0;

            if (Math.abs(denominator) < 1e-15) {
                throw new RuntimeException("分母接近零,割线法失效,请更换初始值");
            }

            double x2 = x1 - fx1 * (x1 - x0) / denominator;
            double fx2 = f.apply(x2);

            System.out.printf("iter %d: x = %.10f, f(x) = %.2e%n", i, x2, fx2);

            // 收敛判断
            if (Math.abs(fx2) < epsilon || Math.abs(x2 - x1) < epsilon) {
                System.out.printf("收敛!共迭代 %d 次%n", i);
                return x2;
            }

            // 更新前两个点
            x0 = x1;
            fx0 = fx1;
            x1 = x2;
            fx1 = fx2;
        }

        throw new RuntimeException("迭代次数超过上限,可能不收敛");
    }

    /**
     * 带完整收敛历史记录的割线法
     */
    public static SecantResult solveWithHistory(DoubleFunction<Double> f,
                                                  double x0, double x1,
                                                  double epsilon, int maxIter) {
        double[] history = new double[maxIter + 2];
        double[] errors = new double[maxIter + 2];
        double fx0 = f.apply(x0);
        double fx1 = f.apply(x1);

        history[0] = x0;
        history[1] = x1;
        errors[0] = Math.abs(fx0);
        errors[1] = Math.abs(fx1);

        int iter = 1;
        for (int i = 2; i <= maxIter; i++) {
            double denom = fx1 - fx0;
            if (Math.abs(denom) < 1e-15) break;

            double x2 = x1 - fx1 * (x1 - x0) / denom;
            double fx2 = f.apply(x2);

            history[i] = x2;
            errors[i] = Math.abs(fx2);
            iter = i;

            if (Math.abs(fx2) < epsilon || Math.abs(x2 - x1) < epsilon) {
                return new SecantResult(x2, iter, trimArray(history, iter + 1),
                                         trimArray(errors, iter + 1), true);
            }

            x0 = x1; fx0 = fx1;
            x1 = x2; fx1 = fx2;
        }

        return new SecantResult(history[iter], iter, trimArray(history, iter + 1),
                                 trimArray(errors, iter + 1), false);
    }

    private static double[] trimArray(double[] arr, int len) {
        double[] res = new double[len];
        System.arraycopy(arr, 0, res, 0, len);
        return res;
    }

    static class SecantResult {
        double root;
        int iterations;
        double[] history;
        double[] errors;
        boolean converged;

        SecantResult(double root, int iterations, double[] history,
                     double[] errors, boolean converged) {
            this.root = root;
            this.iterations = iterations;
            this.history = history;
            this.errors = errors;
            this.converged = converged;
        }
    }

    // ==================== 测试与演示 ====================

    public static void main(String[] args) {
        System.out.println("=== 割线法求根演示 ===\n");

        // 示例1: f(x) = x^2 - 2 = 0, 根为 sqrt(2)
        System.out.println("【示例1】f(x) = x^2 - 2 = 0,求 sqrt(2)");
        DoubleFunction<Double> f1 = x -> x * x - 2;
        double root1 = solve(f1, 1.0, 2.0, 1e-10, 50);
        System.out.printf("结果: %.10f, 真实值: %.10f%n%n", root1, Math.sqrt(2));

        // 示例2: f(x) = e^x + x - 3 = 0
        System.out.println("【示例2】f(x) = e^x + x - 3 = 0");
        DoubleFunction<Double> f2 = x -> Math.exp(x) + x - 3;
        double root2 = solve(f2, 0.0, 1.0, 1e-10, 50);
        System.out.printf("结果: %.10f%n%n", root2);

        // 示例3: f(x) = cos(x) - x = 0
        System.out.println("【示例3】f(x) = cos(x) - x = 0");
        DoubleFunction<Double> f3 = x -> Math.cos(x) - x;
        double root3 = solve(f3, 0.0, 1.0, 1e-10, 50);
        System.out.printf("结果: %.10f%n%n", root3);

        // 示例4: 收敛历史分析
        System.out.println("【示例4】收敛速度分析 f(x)=x^3-x-2");
        DoubleFunction<Double> f4 = x -> x * x * x - x - 2;
        SecantResult result = solveWithHistory(f4, 1.0, 2.0, 1e-12, 50);
        System.out.println("迭代历史:");
        for (int i = 0; i < result.history.length; i++) {
            System.out.printf("  iter %d: x=%.12f, |f(x)|=%.2e%n",
                              i, result.history[i], result.errors[i]);
        }
        System.out.printf("最终根: %.12f, 总迭代: %d%n", result.root, result.iterations);
    }
}

割线法 vs 牛顿法

特性 割线法 牛顿法
是否需要导数 否(用差商近似) 是
每步函数求值次数 1次(复用上步值) 2次(f和f’)
收敛阶 ≈1.618(超线性) 2(二次收敛)
初始值需求 2个初始近似值 1个初始近似值
实现复杂度 简单 需要实现导数计算

注意事项与改进

  1. 分母接近零:当f(x_n) ≈ f(x_{n-1})时,迭代会失效。应加入分母检查并抛出异常或切换策略。
  2. 初始值选择:割线法需要两个不同的初始值,且都应在根的邻域内。
  3. 假位置法(Regula Falsi):若需要保证收敛,可改用假位置法,它保持根始终在两点之间。

总结

割线法是牛顿法的优雅替代,通过牺牲部分收敛速度换取免导数的便利性。其1.618阶的收敛速度在实际应用中往往已足够高效,特别适合导数难以解析求得的场景。掌握割线法有助于理解数值方法中”近似”与”效率”之间的权衡艺术。