个人认为这里面最繁琐的是数组索引因为不同的基数而引起的转换问题。比如里面的i和j,很显然i+j == k-1。而实际上,数组A中0-i的元素加上数组B中0-j的元素一共有(i+1)+(j+1) == k+1个数。
因为数组A和B都是有序的,所以我们知道A[i] > A[0…i-1]都大,B[j] > B[0…j-1]。
进一步,如果B[j-1] < A[i] < B[j],那么A[j]就正好大于 A中前i个数B中的前j个数也就是总共k-1个数,于是A[j]就是我们要找的目标数;
反之如果A[i-1] < B [j] < A[i],那么B[j]就成立我们要找的数。
万一A[i]和B[j]都不是我们要找的数,要么A[i]比B[j-1]小,要么B[j]比A[i-1]小。
假如是A[i]比B[j-1]小,那么我们可以分析推测出来,A[0…i]都太小而不能成为我们要找的目标,而B[j…n-1]又太大,也不可能是我们要找的目标。所以我们就可以开始二分查找的第二步操作——剪枝,让我们的范围缩小。而因为我们去除了A中较小的部分,所以我们要查找的数也从第k个变成了第(k-i-1)个。
对于B[j]比A[i-1]小的情况也是一样的。
从递归调用的地方,我们看出k总是在不断减小的,简单分析更可以知道,如果k是1的话就会停止递归。(这也是为什么我会认为总的时间复杂度是O(log(m+n))的地方。)所以位于i和j的值选取就变得比较关键。
一开始,我设定 int i = k > m? m-1: k-1;也就是让i相对比较大。虽然平均效率上差不多,但是如果剪枝时总是去除B的前段的话,k减小的速度就比较慢。例如最坏的情况:A中所有的数比B中都要大,而我们正好要找第n个数(也就是B中最后一个数),于是每次递归k都只减小了1。此时的复杂度就成了O(n)。
按数组大小来分配i和j可以做到对于任意的案例k的减少都是比较平均的。
对于那些边界检查,在之前的假设之下其实只是会有相等的情况出现,不过检查区域并不会比检查点糟糕。
进一步,其实剪枝时的(0 < j && A[i] < B[j-1]) 并不需要去检查 0 < j,因为j为0的情况只可能出现在n为0 (即B是一个空数组)。而此时,A[i]已是我们要找的目标而被返回了。写上(0 < j && A[i] < B[j-1]) 只是暗示它其实是((j <= 0 || B[j-1] < A[i])的取反。
这个其实也是毕竟常见的面试问题,要求不调用math库,实现对整数的sqrt方法,返回值只需要是整数。二分查找
其实这个问题用数学的表达方式就是:对于非负整数x,找出另一个非负整数n,其中n满足 n2 ≤ x < (n+1)2。
所以最直接的方法就是从0到X遍历过去直到找到满足上述条件的n。这个算法的复杂度自然是O(n)。
仔细想想,其实我们要找的数是在0和X之间,而他正巧可以视为一个有序的数组。似乎有可以运用二分查找法的可能。再回想二分查找法是要找到满足“与目标数相等”这一条件的数,而这里同样也是要找满足一定条件的数。所以我们就可以用二分法来解这个问题了,让复杂度降为O(log n)。
为方便起见,我假设传入的参数是非负的,因此使用unsigned int。
unsigned int sqrt(unsigned int x)
{
//no value should larger than max*max, otherwise it would be overflow
unsigned int max = (1 << (sizeof(x)/2*8))-1; //65535
if (max*max < x)return max;
unsigned int low = 0;
unsigned int high = max-1;
unsigned int mid = 0;
while (1) {
mid = (low+high)/2;
if (x < mid * mid)
high = mid-1;
else if((mid+1)*(mid+1) <= x)
low = mid+1;
else //if(mid * mid <= x && x < (mid+1)*(mid+1))
break;
}
return mid;
}
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-30980-2.html