比二分更快的方法
如果要求一個高次方程的根,我們可以用二分法來做,這是最基礎的方法了。但是有沒有更好更快的方法呢?
我們先來考察一個方程f(x)的在點a的泰勒展開,展開到一階就可以了(假設f(x)在點a可以泰勒展開,也就是泰勒展開的那個余項在n趨於無窮時趨於0)
f(x) -> f(a) + (x - a)f'(a)
現在我們令這個一階展開為0,當f'(a)是非0值,移項一下就有
x = a - f(a)/f'(a)
實際上當我們把f(a)改成f(x) - f(a),這就是一個過了f(a)的關於f(x)的切線方程,如果我們令f(x) = 0就可以得到這條切線在x軸上的交點了。
重復這個過程,我們就可以得到逼近我們所想要的答案的解了。這就是牛頓迭代法的原理。
如上圖,當我們求f[x] = x^2 - 2這個方程的根時,我們可以先猜這個解是4,然后在(4,f(4))這個點上做切線交x軸零點9/4,然后我們繼續在9/4這個點做切線,繼續逼近。
當然代碼寫起來是很簡單的了,比如我們要算簡單的y = sqrt(b),變形一下就是y^2 - b == 0的解,比如在leetcode寫一下
#include <iostream> #include <algorithm> class Solution { public: int mySqrt(int x) { double result = x;//隨便猜一個 double precision = 1e-6; double tmpX; if (x == 0) return 0; do { tmpX = result; result = result / 2.0 + x / 2.0 / result; } while (std::abs(tmpX - result) >= precision); return result; } };
寫了一個matlab來和二分法的速度進行了一下對比:
測試代碼:
clear
sum = 100000000;
rb = zeros(sum,2);
rn = zeros(sum,2);
index = 1;
for n = 1:sum
s =tic;
Newton(n);
elasped= toc(s);
rb(index,1) = n;
rb(index,2) = elasped*1e4;
s =tic;
Binary(n);
elasped= toc(s);
rn(index,1) = n;
rn(index,2) = elasped*1e4;
index = index + 1;
end
x1 =zeros(sum,1);
y1 =zeros(sum,1);
x2 = zeros(sum,1);
y2 = zeros(sum,1);
for n = 1: size(rb)
x1(n) = rb(n,1);
y1(n) = rb(n,2);
end
for n = 1: size(rn)
x2(n) = rn(n,1);
y2(n) = rn(n,2);
end
plot(x1, y1,'*', x2, y2,'+')
set(gca,'YTick')
title('Newton Method (+)和 Binary Search (*) 算sqrt(n)(n->1~100000000)的時間比較')
function result = Newton(x)
result = x;
while 1
tmpX = result;
result = result / 2.0 + x / 2.0 / result;
if abs(tmpX - result) < 1e-6
break
end
end
end
function result = Binary(x)
left = 0;
right = x;
while 1
result = left + (right - left)/2;
if result*result - x > 0
right = result;
elseif result*result - x < 0
left = result;
else
return
end
if abs(right - left) > 1e-6
break
end
end
end
測試結果:

不過由於是在matlab環境下,速度上感覺其實沒什么太大差別。
更快速的開方
QUAKE-III的源碼有一段非常厲害的開方代碼:
float Q_rsqrt(float number) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = *(long *)&y; // evil floating point bit level hacking i = 0x5f3759df - (i >> 1); // what the fuck? y = *(float *)&i; y = y * (threehalfs - (x2 * y * y)); // 1st iteration // y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed return y; }
這段代碼驚為天人,開方連迭代都不用,直接出結果,那個神奇的0x5f3759df是線性擬合出來的,如果想結果更准,把y = y * ( threehalfs - ( x2 * y * y ) );這句話多寫幾下就好了。
不過,這一段代碼是過不了leetcode的某個case,不過這已經不重要了。
Reference:
