1:二分查找
思路:要實現一個sqrt函數,可以使用二分法,首先確定一個範圍[begin, end],這個範圍的中間數mid,看mid的平方是否等於x,如果相等,則返回mid,如果不等則縮小[begin,end]的範圍,爲原來的一半。這裏的初始範圍可以是[1, x],也可以是更精確一些的[1, (x/2) + 1]。(因 (x/2) + 1 的平方等於 x+1+(x^2/4),它一定大於x,所以,x的平方根一定在[1, (x/2) + 1]範圍內)
題目中給出的函數原型是int mySqrt(int x)。參數和返回值都是整數。這裏稍微擴展一下,將函數原型改爲double mySqrt(int x)。解題思路還是一樣的,但是浮點數因精度的原因,無法判斷兩個浮點數是否完全相等,只能說兩者的差值絕對值小於某個精度,所以在二分查找時,需要一定的技巧,具體的代碼如下:
double mySqrt_binarysearch(int x)
{
if(x <= 0) return 0;
double begin = 1;
double end = x/2+1;
double mid, lastmid;
mid = begin + (end-begin)/2;
do{
if(mid < x/mid) begin = mid;
else end = mid;
lastmid = mid;
mid = begin + (end-begin)/2;
}
while(ABS(lastmid-mid) > FLT_MIN);
return mid;
}
上面的代碼中,逐步縮小[begin,end]的範圍,通過判斷上次的lastmid與本次的mid的差值絕對值是否在精度之內,來決定是否繼續尋找下去。
2:牛頓迭代法
上面的實現方法只能說是中規中矩,但是實現sqrt有更牛逼的方法,就是牛頓迭代法。該方法就是由我們熟知的牛頓提出的。具體思想可以自行搜索。簡而言之,如下圖:
x^2 = a的解,也就是函數f(x) = x^2 – a與x軸的交點。可以在x軸上先任選一點x0,則點(x0, f(x0))在f(x)上的切線,與x軸的交點爲x1,它們滿足切線的方程:f(x0)=(x0-x1)f’(x0),可得x1更接近最終的結果,解方程得到:
x1 = (x0 + (a/x0))/2。以x1爲新的x0,按照切線的方法依次迭代下去,最終求得符合精確度要求的結果值。它的實現代碼如下:
double mySqrt_newton(int x)
{
if(x <= 0) return 0;
double res, lastres;
res = x; //初始值,可以爲任意非0的值
do{
lastres = res;
res = (res + x/res)/2;
}
while(ABS(lastres-res) > FLT_MIN);
return res;
}
使用牛頓法解決sqrt的效率非常高,關於效率比較可參見本文最後一節。牛頓法的效率很大程度上取決於初始值的選取,這就引出了下一節。
3:神蹟
下面這段代碼出自《雷神之錘》,至今尚未找到該代碼的真正作者,代碼如下:
float InvSqrt(float x)
{
float xhalf = 0.5f * x;
int i = *(int*)&x;
i = 0x5f375a86 - (i>>1);
x = *(float*)&i;
x = x*(1.5f-xhalf*x*x);
x = x*(1.5f-xhalf*x*x);
x = x*(1.5f-xhalf*x*x);
return 1/x;
}
它本質上還是使用的牛頓迭代法,真正牛逼的地方在於它初始值的選擇,0x5f375a86這個魔法數字的由來尚不可知,該算法的具體原理及其背景可以參見維基百科,不再贅述。
要注意的是,上面算法使用的是float和int類型,實驗可知他們不能替換爲double和long。
4:效率
使用下面的代碼,測試上述三種方法,以及系統默認方法的效率:
int main(int argc, char **argv)
{
clock_t begin, end;
int num = atoi(argv[1]);
double res;
int i;
int loopcnts = 1000000;
begin = clock();
for(i = 0; i < loopcnts; i++)
res = mySqrt_binarysearch(num);
end = clock();
printf("mySqrt_binarysearch(%d) = %f, spent time is %f\n", num, res, (double)(end-begin));
begin = clock();
for(i = 0; i < loopcnts; i++)
res = mySqrt_newton(num);
end = clock();
printf("mySqrt_newton(%d) = %f, spent time is %f\n", num, res, (double)(end-begin));
begin = clock();
for(i = 0; i < loopcnts; i++)
res = InvSqrt(num);
end = clock();
printf("InvSqrt(%d) = %f, spent time is %f\n", num, res, (double)(end-begin));
begin = clock();
for(i = 0; i < loopcnts; i++)
res = sqrt(num);
end = clock();
printf("system sqrt(%d) = %f, spent time is %f\n", num, res, (double)(end-begin));
}
測試結果如下:
mySqrt_binarysearch(65535) = 255.998047, spent time is 3437535.000000
mySqrt_newton(65535) = 255.998047, spent time is 659694.000000
InvSqrt(65535) = 255.998047, spent time is 65902.000000
system sqrt(65535) = 255.998047, spent time is 82605.000000
可見,二分法最慢,普通的牛頓迭代法次之,神蹟代碼要比系統庫的還要快一些。
Ps:謹以此文,給予那些不知天高地厚的程序員們,當頭棒喝!
參考: