实现 sqrt(x):二分查找法和牛顿法
最近忙里偷闲,每天刷一道 LeetCode 的简单题保持手感,发现简单题虽然很容易 AC,但若去了解其所有的解法,也可学习到不少新的知识点,扩展知识的广度。
创作本文的思路来源于:LeetCode Problem 69. x 的平方根
简述题目大意(不想跳转链接,可以看这里):给定一个非负整数 x,要求计算并返回 x 的平方根(取整)。例如,输入 4,则输出 2;输入 8,则输出 2(8 的平方根是 2.82842……,由于返回类型是整数,因此小数部分被舍去)。即给定一个 \(x\),我们要计算出 \(\lfloor \sqrt{x} \rfloor\)。
最简单最直觉的方法自然是从 0 开始遍历,直到找到第一个其平方值大于 \(x\) 的数 \(n\),则 \(n-1\) 即是答案。对于任意的 \(x\),其取整后平方根一定在 \([0, x]\) 区间上,代码如下:
int sqrt(int x)
{
if (x == 0)
return x;
int ans = 1;
while (ans <= x / ans)
ans++;
return ans - 1;
}
这里需要注意的有两点:
- 第 6 行代码中,
while的判断条件可以避免溢出。很大概率上,你可能会写成while (ans * ans <= x),这更自然、更直观,但当ans的值很大时,ans * ans的结果可能会超过int类型的最大表示范围。举个例子,比如我们要计算 \(x\) 的取整平方根(其值为 \(n\),即 \(\lfloor \sqrt{x} \rfloor = n\)),算法会将ans遍历到第一个平方超过 \(x\) 的值,即 \(n+1\) 后停止。如果 \(x\) 的值就是int类型能够表示的最大值,那么当ans遍历到 \(n+1\) 时,计算ans * ans的结果就超出了int类型的表示范围。 - 由于在
while的循环判断中,我们用除法代替了乘法,因此ans便不能再从 0 开始遍历(否则会导致除零错误)。为此,我们可以在算法开始单独处理 \(x = 0\) 的情况,然后让ans从 1 开始遍历。
作为一道简单题,这种暴力朴素的算法自然是可以 AC 的。但其效率极低(需要遍历 \(O(\sqrt{n})\) 次),在 LeetCode 上的时间效率只能快过约 5% 的用户,使用 C++ 语言的运行时间平均要 90ms 以上。因此,本文提供了两种更加高效的算法:二分查找法和牛顿法。
1. 二分查找法
如果你在暴力求解的基础上继续思考,很大概率会想到用二分搜索求解。
没错,思考暴力求解的策略,我们在区间 \([0, x]\) 上搜索解,而搜索区间 \([0, x]\) 天然是有序的,自然可以用二分搜索代替线性搜索,以大大提高搜索效率。
更进一步的,我们还可以缩小我们的搜索区间。直觉告诉我们,对于一个非负整数 \(x\),其 \(\sqrt{x}\) 应该不会大于 \(x / 2\)(例如,\(\sqrt{25} = 5\),小于 \(25 / 2 = 12.5\))。我们可以证明:
&\text{设 } y = \frac{x}{2} - \sqrt{x},\text{ 则 } y^\prime = \frac{1}{2} - \frac{1}{2\sqrt{x}}, \\[2ex]
&\text{令 } y^\prime = 0, \text{ 可得驻点 } x = 1, \\[2ex]
&\text{当 } x > 1 \text{ 时}, y^\prime > 0, \text{ 即当 } x > 1 \text{ 时 }, y = \frac{x}{2} - \sqrt{x} \text{ 的值单调递增}, \\[2ex]
&\text{可推出, 当 } x > 1 \text{ 时}, \lfloor \frac{x}{2} \rfloor - \lfloor \sqrt{x} \rfloor \text{ 的值单调递增}, \\[2ex]
&\text{又因为当 } x = 2 \text{ 时}, \lfloor \frac{x}{2} \rfloor - \lfloor \sqrt{x} \rfloor = 0, \\[2ex]
&\text{所以当 } x \geq 2 \text{ 时}, \lfloor \frac{x}{2} \rfloor - \lfloor \sqrt{x} \rfloor \geq 0, \text{ 即 } x \geq 2 \text{ 时},\lfloor \frac{x}{2} \rfloor \geq \lfloor \sqrt{x} \rfloor
&\text{(证毕)}
\end{aligned}
\]
通过证明我们可以得知,当所求的 \(x\) 大于等于 \(2\) 时,sqrt(x) 的搜索空间为 \([1, x / 2]\),对于 \(x < 2\) 的情况,我们只要特殊处理即可(这里我们也可以得到结论:当 \(x \geq 1\) 时,\(\lfloor \frac{x}{2} \rfloor + 1 \geq \lfloor \sqrt{x} \rfloor\),然后单独处理 \(x < 1\) 的情况)。代码:
int sqrt(int x)
{
if (x < 2) // 处理特殊情况
return x;
int left = 1, right = x / 2;
while (left <= right) {
# 避免溢出,相当于 mid = (left + right) / 2
int mid = left + ((right - left) >> 1);
if (mid == x / mid)
return mid;
else if (mid > x / mid)
right = mid - 1;
else
left = mid + 1;
}
return right;
}
在这里解释一下最后的返回值为什么是 right。对于二分查找,其搜索空间会不断收缩到 left == right(关于二分查找,这里不多赘述,自行手动模拟即可),此时 mid、left和 right 三者的值相等(mid = (left + right) / 2)。根据二分查找的搜索范围的收缩条件可知,left(或 mid)左侧的值都小于等于 \(\lfloor \sqrt{x} \rfloor\),right(或 mid)右侧的值都大于 \(\lfloor \sqrt{x} \rfloor\),此时(while 的最后一次循环),判断 mid 的平方与 x 的大小,有三种情况:
mid == x / mid。则在循环内直接返回mid的值。mid > x / mid。这种情况下,因为mid左侧的值都小于等于 \(\lfloor \sqrt{x} \rfloor\),而mid的值大于 \(x\),则mid-1即是答案。而按照分支条件,执行right = mid - 1,可知right的值正是应返回的值。此时,循环结束,应返回right。mid <= x / mid。这种情况下,mid、left和right正是计算答案(右边的值都大于 \(\lfloor \sqrt{x} \rfloor\))。按照分支条件,执行left = mid + 1,循环结束。此时,mid和right的值为计算结果。
综合上面三点可知,如果 while 循环结束,则 right 保存的值一定是计算结果。
和之前的暴力法相比,使用二分查找的思想求解 sqrt(x),只需要循环遍历 \(O(\lg{\frac{x}{2}})\) 次;空间复杂度为 \(O(1)\)。
2. 牛顿—拉弗森迭代法
牛顿—拉弗森迭代法(简称牛顿法)使用以直代曲的思想,是一种求解函数的方法,不仅仅适用于求解开方计算。当然使用牛顿法求解函数也存在很多坑,但对于求解开方而言,牛顿法是安全的。关于这一方法,你需要掌握一定的高等数学知识,想了解详细的内容,可以参考链接:如何通俗易懂地讲解牛顿迭代法求开方?数值分析?—马同学的回答
简单的理解,可以参考图片:

图片来源:牛顿法与拟牛顿法
给定任意一个非负整数 \(n\),我们想要找到一个 \(x = \lfloor \sqrt{n} \rfloor\),这相当于我们要计算函数 \(f(x) = x^2 - n\) 的根。我们首先需要先给出一个猜测值 \(x_0\),不妨令 \(x_0 = \frac{x}{2} + 1\)(证明见第一小节),然后在 \(f(x_0)\) 处作函数的切线,切线与 \(x\) 轴的交点,即为一次迭代后的值 \(x_1\)。若 \(x_1\) 不是要得到的结果,则继续迭代,在 \(f(x_1)\) 处作函数的切线,切线与 \(x\) 轴的交点,即为第二次迭代后的值 \(x_2\)。以此类推,直到得到 \(x_n = \lfloor \sqrt{n} \rfloor\)。
现在我们来推导迭代式。对于 \(x_i\),其函数值为 \(f(x_i)\),则对于点 \((x_i, f(x_i))\),可得其切线方程:
&y - f(x_i) = f(x_i)^\prime(x - x_i) \\[2ex]
\implies &y - (x_i^2 - n) = 2x_i(x - x_i) \\[2ex]
\implies &y + x_i^2 + n = 2x_ix
\end{align}
\]
又因为 \(x_{i+1}\) 为切线与 \(x\) 轴的交点,所以令 \(y=0\),可得:
\]
现在,我们就可以根据迭代式编写代码了:
int sqrt(int x)
{
// 避免除零错误,单独处理 x = 0 的情况
if (x == 0)
return x;
int t = x / 2 + 1;
while (t > x / t)
t = (t + x / t) / 2;
return t;
}
为了确保算法是正确的,我们还需要一些额外的证明。首先,证明迭代式是单调递减的:
\]
可知,在区间 \([\sqrt{x}, +\infty)\) 上,\(x_{i+1} - x_i < 0\)。
然后,我们还要证明迭代式是可以收敛到 \(\lfloor \sqrt{n} \rfloor\) 的:
\]
因此,当 while 循环结束时,我们可以得到正确的答案。
关于牛顿法求 sqrt(x) 的时间复杂度,笔者目前也没有搞清楚,有了解的童鞋欢迎交流~。不过通过查询资料,以及实际测试,可知牛顿法的时间效率优于二分搜索法。
实现 sqrt(x):二分查找法和牛顿法的更多相关文章
- Leetcode 69 Sqrt(x) 二分查找(二分答案)
可怕的同时考数值溢出和二分的酱油题之一,常在各种小公司的笔试中充当大题来给你好看... 题意很简单,在<二分查找综述>中有描述. 重点:使用简单粗暴的long long来避免溢出,二分均方 ...
- 69. Sqrt(x)(二分查找)
Implement int sqrt(int x). Compute and return the square root of x, where x is guaranteed to be a no ...
- (二分查找 拓展) leetcode 69. Sqrt(x)
Implement int sqrt(int x). Compute and return the square root of x, where x is guaranteed to be a no ...
- 二分查找方法和printk打印级别
人生就是一个茶几,上面摆满了杯具.内核也是一个大茶几,不过它上面的杯具是一个个的bug.确定bug什么时候被引入是一个很关键的步骤,在这个定位bug的过程中,不论有意或无意,都会很自然地用到二分查找的 ...
- find the nth digit(二分查找)
题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=1597 find the nth digit Time Limit: 1000/1000 MS (Jav ...
- BZOJ_3343_教主的魔法_分块+二分查找
BZOJ_3343_教主的魔法_分块+二分查找 题意:教主最近学会了一种神奇的魔法,能够使人长高.于是他准备演示给XMYZ信息组每个英雄看.于是N个英雄们又一次聚集在了一起,这次他们排成了一列被编号为 ...
- 算法之二分查找PK线性查找
列表查找(线性查找) 本质就是列表的index() 顺序查找 也叫线性查找,从列表第一个元素开始,顺序进行搜索,知道找到元素或搜索到列表最后一个元素为止. 以下是示例代码: def line_sear ...
- POJ2104 (平方分割)二分查找理解。
题意:任意区间求第k大数 思路: 预处理:利用平方分割(分桶法)把区间切割成B = sqrt(n)大小的一块块,然后每个各自排序. 二分第k大数x,接着就需要求[l,r]区间中x的排名,与k比较,将两 ...
- 【bzoj2957】楼房重建 分块+二分查找
题目描述 小A的楼房外有一大片施工工地,工地上有N栋待建的楼房.每天,这片工地上的房子拆了又建.建了又拆.他经常无聊地看着窗外发呆,数自己能够看到多少栋房子.为了简化问题,我们考虑这些事件发生在一个二 ...
随机推荐
- Linux 命令之 mkdir
mkdir的作用是创建一个目录,可以理解为 make directory 的缩写. 创建目录 mkdir dir_name 在当前目录创建一个名为 dir_name 的目录. 同时创建多级目录 假设现 ...
- Hyper-V 下linux虚拟机静态IP上网配置的两种方式(1)
工作需要,搭建linux环境,网上搜了两种Hyper-V配置linux静态IP及上网的方式,记录一下,方便查阅,如下是桥接方式的配置: 本实例所用的各项资源说明,系统是windows10企业版64bi ...
- Python接口测试框架实战与自动化进阶☝☝☝
Python接口测试框架实战与自动化进阶☝☝☝ 一.fiddler在工作中的运用 1.如何抓接口 抓紧手机端接口 ①.在电脑终端输入:ipconfig ,找到电脑ip ②.打开手机,连接WiFi, ...
- 【Python秘籍】ASCII码与字符的转换
如何在python中显示ASCII码呢?其实你只需要记住两个函数即可:ord()和 chr(),这两个函数都是python内置的函数,不需要引入任何的包,直接就可以使用. 一.显示ASCII码 显示A ...
- linux下mqtt-client
CPATH += ../embe_mqtt/MQTTClient/srcPSRTPATH = ../embe_mqtt/MQTTPacket/src LOADPATH += -I$(CPATH)LOA ...
- 简单认识JVM
准备: 在具体聊JVM之前,我们先看两张图,通过分析图,咱们慢慢来聊聊JVM. JVM内存结构图 JVM内存结构脑图 上面两张图中,第二张图相对来说比较直观,就是JVM内存结构都划分成了哪些模块,各个 ...
- electron打包分发
原始的方式打包 下载对应的版本号的Release Electron 然后把对应的项目方便整理成这样的目录结构(Windows下) node_modules重新安装,不然可能启动失败 把整文件夹给别人就 ...
- qt读取文本
直接 代码: // lyy : 2016/8/26 16:40:11 说明:读取文本 bool FileOpeartion:: GetTheTextContent (const QString str ...
- Spring(四)Spring与数据库编程
Spring最重要的功能毫无疑问就是操作数据.数据库的百年城是互联网编程的基础,Spring为开发者提供了JDBC模板模式,那就是它自身的JdbcTemplate.Spring还提供了Transact ...
- Java多线程编程(一)Java多线程技能
一.进程和多线程的概念以及线程的优点 打开Windo任务管理器可以看到很多正在运行着的exe程序,完全可以将运行在内存中的exe文件理解成进程,进程是受操作系统管理的基本运行单元. 线程可以理解成在进 ...