一个忽视特殊情况和数学规律而引发的血案—自实现pow函数
Leetcode上遇到的一题。初学C语言的时候写过,但当时写得当然是最简单的了:
double pow(double x, int n) { double r=1.; while(n--) r*=x; return r; }
是的,虽然这代码是现在写的,但是初学C的我绝对写得出这样的代码。下面考虑特殊情况:
1 判断两个double型变量是否相等不能直接用==,而是应该这么写:
inline bool is0(double a) { return a<1E-10 && -a<1E-10; }
从而a=b即is0(a-b). 至于1E-10合适呢?从6到10的我倒是都见过。就这题来说,设成10就够通过OJ。
2 x=0时结果为0.
3 x=1时结果为1.
4 x=-1时结果为1或-1. 实际上笔者做题时漏掉了这一项 = = |||
5 n=0时结果为1或-1.
否则,老老实实算吧。但是注意:
6 对于小数的幂会越算越接近0,如果足够小了,就可以停止了。换而言之,如果收敛了就可以停止了,如何判断收敛呢?简单,看r*=x前后有没有明显不同。(其实笔者还漏掉了这个 囧|||)
于是:
1 double pow(double x, int n) { 2 if(is0(x)) return 0; 3 if(is0(x-1.)) return 1; 4 if(is0(x+1.)) 5 if(n & 1) return -1; 6 else return 1; 7 bool sgn = 1; 8 if(x<-1E-9 && (n & 1)) sgn = 0; 9 if(n==0) 10 if(sgn) return 1; 11 else return 0; 12 double r = 1.; 13 double t; 14 if(n>0) 15 { 16 while(n-- && !is0(t-r)){ 17 t = r; 18 r*=x; 19 } 20 return r; 21 } 22 else 23 { 24 while(n++ && !is0(t-r)){ 25 t = r; 26 r/=x; 27 } 28 return r; 29 } 30 }
提交,通过,看下runtime是128ms!大家的CPP程序都是10以内的成绩,128那是python跑出来需要的时间呢。点开tag发现,这题最终应该用二分的方法解:
n是偶数时:
a^n = a^{n/2} * a^{n/2};
n是奇数时:
a^n = a^{(n-1)/2} * a^{(n-1)/2} * a;
写成递归再简单不过:
double powd(double x, int n) { if(n==0) return 1; if(n==1) return x; double r = powd(x, n/2) * powd(x, n/2); if(n%2) return r * x; return r; }
递归毕竟太慢而且附带着空间上的开销,写成非递归吧。
考虑到n是整型,实际上也就是
a^n = a^{n/2} * a^{n/2} * a;
总而言之:
r = a^{n/2} * a^{n/2},如果n是奇数,则r*=a;
于是:
double powb(double x, int n) { double r = 1.; while(n) { if(n & 1) r*=x; x*=x; n/=2; } return r; } double pow(double x, int n) { if(is0(x)) return 0; if(is0(x-1.)) return 1; if(is0(x+1.)) if(n & 1) return -1; else return 1; bool sgn = 1; if(x<-1E-9 && (n & 1)) sgn = 0; if(n==0) if(sgn) return 1; else return 0; double r = powb(x, abs(n)); if(n>0) { return r; } else { return 1/r; } }
O(lgn). 提交,16ms走起!
是的~ 我就是对那段特殊情况的判断还恋恋不舍~ 毕竟那是血案得来的教训~ 前期对于特殊情况的考虑一个都不能少!
此外,对于算法问题:
能二分的尽力想如何二分;
能用非递归的绝不用递归,除非非递归实在太麻烦;
能不用回溯的尽量不用(比如八皇后问题,虽然回溯和非回溯两个算法都是O(n^3))
回溯能剪枝就剪枝(比如N-Sum问题)
用DP的时候,数组能有多小用多小。DP的每次迭代都不复杂,往往只需要用到少数几个数
... 以上!