前两天逛github看到一道很简略的面试题——如何不必库函数疾速求出$\sqrt2$的值,准确到小数点后10位! 第一反馈这不很简略嘛,大学数据结构课讲二分查找的时候老师还用这个做过示例。但转念一想,能作为大厂的面试题,背地相对没有那么简略,于是我google了下,后果找到了更奇妙的数学方法,甚至发现了一件奇闻趣事…… 一道简简单单的面试题,不仅能考查到候选人的编程能力,还能间接考查到候选人的数学素养,难怪很多大厂都会问这个。。。
回到正题,求$\sqrt2$到底有多少种解法,咱们由简入难一步步来看下咱们是如何让计算机更快计算sqrt的。
二分查找
首先是略微具备点编程和数据结构根底的人都能想到的二分查找,这里我不再具体解说思路,但还是要编码测试下,次要是测试须要迭代多少次能力达到小数点后10位的精度。
public class BSearch { static double sqrt2 = 1.4142135624; static double delta = 0.0000000001; public static void main(String[] args) { double l = 1.0; double r = 2.0; int cnt = 0; while (l < r) { double mid = (l + r) / 2; if (Math.abs(l - sqrt2) < delta) { break; } if (mid * mid > 2.0) { r = mid; } else { l = mid; } cnt++; } System.out.println("通过" + cnt + "次迭代后得" + l); }}
通过34次迭代后得1.4142135623260401
记住这个迭代次数34。
牛顿迭代
数学学得好的同学必定晓得牛顿迭代法,它是求解线性方程近似解的办法,因为有些方程无奈疾速求出准确解,只能尽可能去迫近。
回到咱们求解$\sqrt2$上,咱们能够结构出多项式方程f(x),使得$f(x)=0$的一个正根是$\sqrt2$,最简略的就是$f(x) = x^2 - 2= 0$,而后咱们就能够使用牛顿迭代求解它的根了。
$$x_{1}=x_{0}-\frac{f\left(x_{0}\right)}{f^{\prime}\left(x_{0}\right)} = x_0 - \frac{x_0^2-2}{2x_0}$$
为了更直观些,咱们图例展现下迭代两次的过程。
上图中彩色的曲线是$f(x)=x^2-2$,咱们最终想要的是它和x轴的交点X,也就是$\sqrt2$的具体值。A点的坐标是(2,2),绿色的线是$f(x)=x^2-2$在点A处的切线,洋红色线是过A点的垂线。咱们选的牛顿迭代初始点就是B,设B点的横坐标是$x_0$,按求导的定义,AC点的斜率就是$f^{\prime}(x)$,AB的长度就是$f(x)$,晓得了AB的长度,AC的斜率,BC的长度也就很好求了,就是$\frac{f\left(x_{0}\right)}{f^{\prime}\left(x_{0}\right)}$,所以C点的横坐标就是$x_1 = x_0 - \frac{f\left(x_{0}\right)}{f^{\prime}\left(x_{0}\right)}$。
怎么样,迭代一次之后就很迫近咱们真正想要的X了,当初咱们在C点把同样的事件再做一遍,如下图。
再一次迭代后失去了点E,点E比点C更加迫近X点,我把上图中点E部分放大如下。
能够看到点E曾经相当靠近$\sqrt2$了,这只是两次迭代的成果,写个代码来看下到底须要迭代多少次就能达到准确到小数点后10位的精度。
具体代码如下,这里我取x的初始值为2.0 因为$\sqrt2$不可能大于2,咱们晓得这点就能够取个近似值,缩小迭代次数。
public class NewtonMethod { static double sqrt2 = 1.4142135624; static double delta = 0.0000000001; public static void main(String[] args) { double x = 2.0; int cnt = 0; while (Math.abs(x - sqrt2) > delta){ x = x - (x*x - 2)/(2*x); cnt++; } System.out.println("通过" + cnt + "次迭代后得" + x); }}
通过4次迭代后得1.4142135623746899
只须要4次迭代就能达到二分34次迭代的成果了,的确显著快多了。这里再补充一点,实际上x的初始值能够取任意负数,然而会影响到性能,我尝试取1亿,最终须要30次迭代,不过还是比二分快。
为了主观比照下牛顿迭代和二分的性能差别,这里我还是用JMH压测下,后果如下,压测后果仅供参考。
Benchmark Mode Cnt Score Error UnitsTest.NewtonMethod thrpt 2 96292165.813 ops/sTest.bSearch thrpt 2 11856462.059 ops/s
到这里文章进度条只有一半,你是不是感觉我上面要讲比牛顿迭代更好更快的办法?理论我目前没有找到比牛顿迭代又好又快的算法了,然而我找到一个相干的故事,以及它引出的以就义精度换取速度求$\frac{1}{\sqrt{x}}$的神奇算法,当然它也能够用来求$\sqrt2$。
神奇的数字0x5f3759df
这首先要从一个诡异的常数说起—— __0x5f3759df__,提到这个常数还得提到一个游戏,Quake-III Arena (雷神之锤3)。
这是90年代的经典游戏之一。该系列的游戏岂但画面和内容不错,而且即便计算机配置低,也能极其流畅地运行。这款游戏起初在GPL协定下开源,github地址 https://github.com/id-Software/Quake-III-Arena,大家从中发现其中一个可能疾速求解$\frac{1}{\sqrt{x}}$的函数,代码如下。
float InvSqrt(float x){ float xhalf = 0.5f*x; int i = *(int*)&x; i = 0x5f3759df - (i >> 1); x = *(float*)&i; x = x*(1.5f - xhalf*x*x); return x;}
看完代码的我
其实这个算法就是牛顿迭代单次的近似解法,具体证实请看卡马克疾速平方根倒数算法,它能以几十倍的速度劣势秒杀其余算法,要晓得几十年前的CPU速度可远不迭当初的,速度就是绝对优势。这段代码看起来神奇,它的起源也很离奇。
开始大家都认为这个算法是游戏的开发者Carmack发现的,但起初考察发现,该算法在这之前就在计算机图形学的硬件与软件畛域中有所利用,如SGI和3dfx就曾在产品中利用此算法,所以至今都无人知晓这个算法是谁创造的。
但传奇并没有完结。Lomont计算出后果当前十分称心,于是拿本人计算出的起始值和卡马克的神秘数字做较量,看看谁的数字可能更快更准确求得平方根。后果是卡马克赢了... 谁也不晓得卡马克是怎么找到这个数字的。最初Lomont怒了,采纳暴力办法一个数字一个数字试过去,终于找到一个比卡马克数字要好上那么一丁点的数字,尽管实际上这两个数字所产生的后果十分近似,这个暴力得出的数字是0x5f375a86。Lomont为此写下一篇论文 Fast Inverse Square Root。 From 百度百科
来看看这个算法的理论运行成果怎么样吧,上面是我批改后的java代码,下面的C代码中有些操作java中并不反对,所以须要做些改变。
public class CarmackMethod { public static void main(String[] args) { float x = 2.0f; float xhalf = 0.5f*x;// int i = *(int*)&x; int i= Float.floatToRawIntBits(x); i = 0x5f3759df - (i >> 1);// x = *(float*)&i; x = Float.intBitsToFloat(i); x = x*(1.5f - xhalf*x*x); System.out.println(1.0/x); }}
1.4145671304934657
当然这里精度就不行了,只准确到了0.001。
这里我把下面3种算法的精度都升高到0.001而后用JMH做个不严格的性能测试。这里说不严格是因为我只做$\sqrt2$的测试,而且用的是java实现的,而且像是CarmackMethod的实现,可能因为java和c的运行机制的不同,性能会受很大影响,上面这个后果 __仅供娱乐__,看看就好。
Benchmark Mode Cnt Score Error UnitsTest.CarmackMethod thrpt 2 111286742.330 ops/sTest.bSearch thrpt 2 58705907.393 ops/sTest.NewtonMethod thrpt 2 110232331.339 ops/s
各种编程语言是如何实现sqrt?
下面说了3个解法,那你是不是也很好奇目前各种编程语言库函数里对sqrt是如何实现的? 我也很好奇,于是咱们帮你们翻了jdk源码,发现它基本就不须要本人实现sqrt,毕竟sqrt在计算机领域是有个比拟罕用的计算,所以支流的CPU架构都提供了对sqrt的硬件反对,只须要一条汇编指令就能够了,在x86架构下sqrt能够间接用上面这条汇编指令。
sqrtsd %1, %0" : "=x" (res) : "xm" (x)
在Risc-v中能够能够用fsqrt.s
或fsqrt.d
指令,Rics-v中文手册
硬件能够在一个指令周期内实现一个数的开方,相比那些须要几十甚至成千盈百个CPU指令实现的各种算法而言,这速度差别不言而喻。 实际上上,古代CPU在多外围、流水线、多发射、超标量……等技术的加持下,一般家用CPU都能够做到每秒百亿次的浮点运算。
__生存处处有惊喜__,当我关上python math模块的源码时,没有发现浮点数的求根(预计也是间接用的CPU级指令),但我发现了一个更骚的对64位整数求根的操作,所以这里再补充介绍一个python的近似求根算法。
python中的_approximate_isqrt()
上面这段代码能够返回输出值求根后的整数局部,但齐全不晓得是什么原理。
_approximate_isqrt(uint64_t n){ uint32_t u = 1U + (n >> 62); u = (u << 1) + (n >> 59) / u; u = (u << 3) + (n >> 53) / u; u = (u << 7) + (n >> 41) / u; return (u << 15) + (n >> 17) / u;}
源码中有正文,这段C代码能够翻译为以下python代码,不过我仍旧看不懂。
def isqrt(n): """ Return the integer part of the square root of the input. """ n = operator.index(n) if n < 0: raise ValueError("isqrt() argument must be nonnegative") if n == 0: return 0 c = (n.bit_length() - 1) // 2 a = 1 d = 0 for s in reversed(range(c.bit_length())): # Loop invariant: (a-1)**2 < (n >> 2*(c - d)) < (a+1)**2 e = d d = c >> s a = (a << d - e - 1) + (n >> 2*c - e - d + 1) // a return a - (a*a > n)
结语
这篇博客从立题到实现经验了好几天的工夫,期间整顿思路、编码、绘图、查阅材料、批改欠缺总累计耗时近8h。写作不易,如果文章对你有用欢送素质三连(点赞、珍藏加关注) 。
援用链接
[1] 牛顿迭代法: https://baike.baidu.com/item/...
[2] 卡马克疾速平方根倒数算法: http://jcf94.com/2016/01/14/2...
[3] Fast Inverse Square Root: http://read.pudn.com/download...
[4] From 百度百科: https://baike.baidu.com/item/...
[5] 这条汇编指令: https://code.woboq.org/usersp...
[6] Rics-v中文手册: http://crva.ict.ac.cn/documen... Chinese-v2p1.pdf
[7] python math模块: https://github.com/python/cpy...