04二分查找(浮点二分)

# 🔍 二分查找的“陷阱”:为什么我的平方根算法对小于1的数失效?

> **作者**:一位正在深入理解算法细节的开发者  
> **关键词**:二分查找、平方根、浮点数、算法边界、编程陷阱  
> **发布时间**:2025年8月28日

---

在学习和实现算法的过程中,我们常常会遇到一些“看似正确,却在边界上出错”的问题。今天,我们就来剖析一个经典的例子:**用二分查找求平方根**。

你可能写过这样的代码:

```cpp
double l = 0, r = x;
while (r - l > 1e-8) {
    double mid = (l + r) / 2;
    if (mid * mid >= x) r = mid;
    else l = mid;
}

逻辑清晰,代码简洁,测试 x = 4x = 9 都没问题。但当你输入 x = 0.25 时,结果却是:

输入:0.25
输出:0.25(错误!应为 0.5)

这是为什么?难道二分查找失效了?其实不是算法错了,而是我们忽略了一个关键的数学事实


🧩 问题重现:小于1的数“不生效”?

我们来手动分析一下 x = 0.25 的情况:

  • 初始区间:l = 0, r = 0.25
  • 真实平方根:sqrt(0.25) = 0.5

0.5 > 0.25,也就是说:真实解根本不在区间 [0, 0.25] 内!

而我们的二分查找是在这个“错误的区间”内进行的,无论怎么迭代,都无法跳出 [0, 0.25],最终只能收敛到 0.25 本身。

根本问题:初始搜索区间选择错误


📐 数学真相:平方根的大小关系

我们来观察一个有趣的数学规律:

x sqrt(x) 比较结果
4 2 sqrt(x) < x
1 1 sqrt(x) = x
0.25 0.5 sqrt(x) > x

👉 结论

  • x > 1 时,sqrt(x) < x
  • x = 1 时,sqrt(x) = x
  • 0 < x < 1 时,sqrt(x) > x

所以,如果我们统一设置 r = x,那么当 x < 1 时,真实解就被排除在搜索区间之外了


✅ 正确解法:动态设置搜索区间

为了让二分查找适用于所有 x ≥ 0 的情况,我们必须确保搜索区间包含真实解

✅ 解决方案:取 r = max(x, 1.0)

  • 如果 x ≥ 1sqrt(x) ≤ x → 区间 [0, x] 合理
  • 如果 x < 1sqrt(x) < 1 → 区间 [0, 1] 包含解

这样,无论 x 多大或多小,真实平方根都在 [0, max(x, 1)] 内。


✅ 修正后的完整代码

#include <iostream>
#include <iomanip>  // 用于控制输出精度
using namespace std;

int main() {
    cout << fixed << setprecision(8);  // 输出保留8位小数

    double x;
    while (cin >> x) {
        if (x < 0) {
            cout << "负数没有实数平方根" << endl;
            continue;
        }
        if (x == 0) {
            cout << 0 << endl;
            continue;
        }

        // 关键修正:右边界取 max(x, 1.0)
        double l = 0, r = (x >= 1) ? x : 1;

        // 二分查找
        while (r - l > 1e-8) {
            double mid = (l + r) / 2;
            if (mid * mid >= x)
                r = mid;
            else
                l = mid;
        }

        cout << "sqrt(" << x << ") ≈ " << l << endl;
    }
    return 0;
}

🧪 测试验证

输入 输出 是否正确
4 2.00000000
0.25 0.50000000
0.01 0.10000000
2 1.41421356

全部通过!


🎯 二分查找的核心思想回顾

这个“小问题”其实揭示了二分查找的几个关键原则:

  1. 搜索空间必须包含解
    → 无论算法多精妙,如果初始区间错了,结果一定错。

  2. 边界条件决定成败
    x = 0x = 1x < 1 都是需要单独考虑的边界。

  3. 单调性是二分的前提
    f(mid) = mid^2 是单调递增的,所以可以二分。

  4. 浮点数二分需控制精度
    → 用 1e-8 作为收敛条件,避免无限循环。


💡 更进一步:牛顿迭代法(更快!)

虽然二分查找稳定可靠,但收敛速度是线性的(O(log(1/ε)))。
更高效的方法是牛顿迭代法

double sqrt_newton(double x) {
    if (x == 0) return 0;
    double r = x;
    while (abs(r * r - x) > 1e-8) {
        r = (r + x / r) / 2;
    }
    return r;
}

牛顿法收敛速度是平方级的,更快!


✅ 总结

问题 解决方案
小于1的数失效 r = xr = max(x, 1.0)
负数输入 增加判断和提示
精度不足 使用 1e-8 并控制输出格式
收敛慢 可改用牛顿迭代法

编程不是写完就对,而是不断理解边界、修正假设的过程。

这个小小的“平方根”问题,教会我们:

算法的正确性,不仅取决于逻辑,更取决于对数学本质的理解。


📚 延伸阅读

  • 《算法导论》—— 二分查找与数值计算
  • LeetCode 69. Sqrt(x)
  • 牛顿法求根:从几何直观理解迭代收敛
posted @ 2025-08-28 15:07  寻龙诀  阅读(12)  评论(0)    收藏  举报
//页脚烟花效果 //雪花飘落