版权声明:欢迎转载,但请注明出处,若有什么不对的地方,欢迎指正,https://blog.youkuaiyun.com/wutenglong123/article/details/82746650
提起二分查找,许多人都会想:这是一个简单问题,我只用一个while或者递归就能解决,三行代码最多。再把上下限背过,就没有什么能难得住的二分查找问题了。但事实究竟是这样吗?
我们一起从头看一遍二分查找,其中穿插各个OJ的二分查找问题,同时时不时的我们会加入算法导论中对于这个问题的引申。
##什么是二分查找?
二进制搜索是计算机科学中最基本和最有用的算法之一。 它描述了在有序集合中搜索特定值的过程。我们需要对二进制搜索中的名词赋予一个简单的定义:
target - 要搜索的值
index - 搜索的当前位置
left ,right - 我们用来维持搜索空间的指标
mid- 用来应用条件来确定我们是应该向左还是向右搜索的索引
##二分查找的过程
在二分查找的基本形式中,二分搜索在具有指定左右索引的连续序列(搜索空间)上运行。维护搜索空间的左,右和中间标记,并比较搜索目标; 如果条件不满足或者值不相等,则消除目标不可能存在的一半(有序序列),并继续搜索剩下的一半,直到成功为止。 如果搜索以空的数字集合部分结束,则无法满足条件并且未找到目标。
给定n个元素的排序(按升序排列)整数数组nums和目标值,编写一个函数来搜索nums中的目标。 如果target存在,则返回其索引,否则返回-1
Leetcode Binary Search
此代码只有 34.4% 为AC,甚至低于许多高级算法!
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.size() == 0)return -1;
int left =0;
int right = nums.size()-1;
while(left<=right){
int mid = (left+right) >>1;
if(nums[mid]<target)left = mid+1;
else if(nums[mid]>target)right =mid-1;
else return mid;
}return -1;
}
};
算法的应用
由于二进制搜索是一种算法,在每次比较后将搜索空间划分为2。 所以每次需要搜索集合中的索引或元素时,都应考虑二进制搜索。 如果集合是无序的,我们可以在应用二进制搜索之前对其进行排序1。
总的来说,我们可以将此部分分为:
Markdown画图,冒号后面必须空一格,不然报错。我的天呐。对萌新一点都不友好。
我们总希望能够研究的二分查找问题越多越好,虽然我们拥有的思路相同,但每次我们查看不同大佬代码时,它的实现似乎都略有不同。 尽管每个实现在每个步骤中将问题空间划分为1/2,但其中一个有许多问题:
- 为什么它的实现略有不同?
- 大佬在想什么?
- 哪种方式更容易?
- 哪种方式更好?
###经过多次失败的尝试,和资料的阅读查找,附上几个二分查找的主要模板,并且附上几个相似的例子。
1、同上面的基本算法相同的写法,我们的谭浩强、严蔚敏老师就是这么教我们的
int binarySearch(vector<int>& nums, int target){
if(nums.size() == 0)
return -1;
int left = 0;
int right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){ return mid; }
else if(nums[mid] < target) { left = mid + 1; }
else { right = mid - 1; }
}
return -1;
}
- 初始条件: left = 0, right = length-1(数组终点下标)
- 终止条件: left > right
- 查找左搜索区间:right = mid -1
- 查找右搜索区间:left = mid+1
Ex1:计算并返回x的平方根,其中x保证为非负整数。
由于返回类型是整数,因此将截断十进制数字,并仅返回结果的整数部分。
循环体中的判断条件
if(x/mid >= mid)
若为乘法if(mid*mid <=x)
时会**TLE
**,感兴趣的话可以查阅相关资料。
class Solution {
public:
int mySqrt(int x) {
if (x <2)return x;
int left = 0;
int right = x ;
while(left< right){
int mid = (right + left) /2;
if (x/mid >= mid) left = mid+1;
else right = mid;
}return right-1;
}
};
当然我写完了我的代码后也会出来看神仙:
class Solution {
public:
int mySqrt(int x) {
long r = x;
while (r*r > x)
r = (r + x/r) / 2;
return r;
啧啧啧。由于此思路与本文主体无关,但既然看到了神仙,我们刚还在说要揣摩大佬的思路,所以特放在附录里2。
值得注意的是,上述方法当输入为
但是一个题目的办法多种多样,除了上面的两个,比如还有Shifting nth root algorithm
3
class Solution:
def mySqrt(self, x):
res = 0
bit = 1 << 30
while bit > x:
bit >>= 2
while bit != 0:
if x >= res + bit:
x -= res + bit
res += bit << 1
res >>= 1
bit >>= 2
return res
言归正传,我们能够看到二分查找用途广且方便计算。现在我们来分析一下二分查找的时间复杂度(讨论非递归情况,递归方式见4)。
(原谅我递归方式 C语言版原谅我在wiki上直接粘了一个,就是这个↓)
int binary_search(const int arr[], int start, int end, int khey) {
if (start > end)
return -1;
int mid = start + (end - start) / 2; //直接平均可能會溢位,所以用此算法
if (arr[mid] > khey)
return binary_search(arr, start, mid - 1, khey);
else if (arr[mid] < khey)
return binary_search(arr, mid + 1, end, khey);
else
return mid; //最後檢測相等是因為多數搜尋狀況不是大於要不就小於
}
易得 T ( n ) = ( n / 2 ) + 1 T(n) =(n/2) +1 T(n)=(n/2)+1 由主方法知 n l o g 2 1 = 1 n^{log_21}=1 nlog21=1,由于 h ( n ) = Θ ( n l o g 2 1 ) = Θ ( 1 ) h(n)=\Theta(n^{log_21}) = \Theta(1) h(n)=Θ(nlog21)=Θ(1) 考虑第二种情况: T ( n ) = Θ ( n l o g b a l g n ) T(n)=\Theta(n^{log_ba}lgn) T(n)=Θ(nlogbalgn),则 T ( n ) = Θ ( l o g 2 n ) T(n) = \Theta(log_2n) T(n)=Θ(log2n) 5
Ex2:Guess Number Higher or Lower
我们正在玩猜数字游戏。 游戏如下:
我从1到n中选择一个数字。 你猜测我选择了哪个号码。每次你猜错了,我都会告诉你这个数字是高还是低。可调用预定义的API guess(int num),它返回3个可能的结果(-1,1或0)
class Solution {
public:
int guessNumber(int n) {
int left = 0;
int right = n;
if( n<2) return n;
while(left<=right){
int mid = left+(right-left)/2;//否则overflow会导致超时,其他需转换。API传入值为int。
if (guess(mid) == 0){
return mid;
}else if (guess(mid)==-1){
right = mid-1;
}
else left = mid +1;
}return -1;
}
};
没有工作量,多调一个API而已。
有关排序的算法见(暂空)。 ↩︎
-
此为牛顿法:
设 r r r是 f ( x ) = 0 f(x) = 0 f(x)=0的根,选取 x 0 x_0 x0作为 r r r初始近似值,过点 ( x 0 , f ( x 0 ) ) (x_0,f(x_0)) (x0,f(x0))做曲线 y = f ( x ) y = f(x) y=f(x)的切线 L L L, L L L的方程为 y = f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) y = f(x_0)+f'(x_0)(x-x_0) y=f(x0)+f′(x0)(x−x0),求出 L L L与 x x x轴交点的横坐标 x 1 = x 0 − f ( x 0 ) f ′ ( x 0 ) x_1 = \frac{x_0-f(x_0)}{f'(x_0)}\qquad x1=f′(x0)x0−f(x0),称 x 1 x_1 x1为 r r r的一次近似值。
过点 ( x 1 , f ( x 1 ) ) (x1,f(x1)) (x1,f(x1))做曲线 y = f ( x ) y = f(x) y=f(x)的切线,并求该切线与x轴交点的横坐标 x 2 = x 1 − f ( x 1 ) f ′ ( x 1 ) x_2 = \frac{x_1-f(x1)}{f'(x1)}\qquad x2=f′(x1)x1−f(x1),称 x 2 x_2 x2为 r r r的二次近似值。重复以上过程,得 r r r的近似值序列,其中 x ( n + 1 ) = x ( n ) - f ( x ( n ) ) f ′ ( x ( n ) ) x(n+1)=x(n)-\frac{f(x(n))}{f'(x(n))}\qquad x(n+1)=x(n)-f′(x(n))f(x(n)), 称为 r r r的 n + 1 n+1 n+1次近似值,上式称为牛顿迭代公式。 -
Shifting nth root algorithm:
除了en_wiki,好像没有太细致的中文讲解,见我的另一篇blog吧。 复杂度分析见. ↩︎
有关于如何求算法的 Ω , Θ , O , ω , o \Omega,\Theta,O,\omega,o Ω,Θ,O,ω,o请参考另一篇blog。 ↩︎