目录
前言
相信大家都已经对二分查找的思路很了解。但是知道怎么做不等于能做出来,尤其是将思路转化为代码时往往会遇到各种细节上的问题。在初学二分查找时尤为明显。本章将介绍二分查找的基本思路及不容易记错记混的二分查找模板。
二分查找定义
在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)[1]、对数搜索算法(英语:logarithmic search algorithm)[2],是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
二分查找算法在最坏情况下是对数时间复杂度的,需要进行次比较操作。二分查找算法使用常数空间,对于任何大小的输入数据,算法使用的空间都是一样的。除非输入数据数量很少,否则二分查找算法比线性搜索更快,但数组必须事先被排序。尽管一些特定的、为了快速搜索而设计的数据结构更有效(比如哈希表),二分查找算法应用面更广。
二分查找算法有许多种变种。比如分散层叠可以提升在多个数组中对同一个数值的搜索的速度。分散层叠有效的解决了计算几何学和其他领域的许多搜索问题。指数搜索将二分查找算法拓宽到无边界的列表。二叉搜索树和B树数据结构就是基于二分查找算法的。
————copy自维基百科
二分查找思路
二分搜索只对有序数组有效。二分搜索先比较数组中位元素和目标值。如果目标值与中位元素相等,则返回其在数组中的位置;如果目标值小于中位元素,则搜索继续在前半部分的数组中进行。如果目标值大于中位元素,则搜索继续在数组上部分进行。由此,算法每次排除掉至少一半的待查数组。
代码实现(一般的版本)
二分查找的代码实现一般有很多种,这里举例一部分:
模板1
int search(int nums[], int size, int target) {
int left = 0;
int right = size - 1;
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
模板2
int search(int nums[], int size, int target){
int left = 0;
int right = size;
while (left < right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
模板3
int search(int nums[], int size, int target) {
int left = 0;
int right = size - 1;
while (left < right)
{
int mid = left + right + 1 >> 1;
if (num[mid]>target) {
left = mid;
}
else if(num[mid]<target){
right = mid - 1;
}
else if(num[mid]==target){
return mid;
}
return -1;
}
具体可以参考以下文章:
二分查找 & 二分答案 万字详解,超多例题,带你学透二分。_c++二分答案怎么确定是l<r还是l<=r-优快云博客
新的方法
我们可以看一下之前的代码,不难发现,上述代码while跳出循环条件,middle的取值,left等于middle+/-1,right等于middle+/-1都不一样。他们都有一个共同点,那就是:都是对二分循环体根据题目条件的不同而改变。但是或许我们可以换一个思路:不对循环体改变,而是改变check(middle)函数以及输出的数值,这样我们的循环体就可以不用改变了,也方便好记。
代码模板
bool check(int x) {
if (condition) return 1;
else return 0;
}
int find(int* arr, int left, int right) {
left--;
right++;
while (right - left > 1) {
int middle = left + right >> 1;
if (check(middle)) left = middle;
else right = middle;
}
return left / right;
}
注意到,循环体是无论什么题都可以套用这个模板的,关键是要改变check函数和return的值。
做题思路和细节
第一步,将我们的数组分为target及左侧,和target右侧两部分,为了方便讲述,我们分别标成1(满足)和0(不满足)。我们要找到合适的check函数,是的target及左边均为1,右边均为0。L和R一开时在外面是因为数组有可能是全为1或者全为0的。需要注意,无论什么时候:L都在1区,R都在0区。
L,R位置 | L | M | R | ||||||
是否满足 | 1 | 1 | 1 | 1 | 1...1(tar)0... | 0 | 0 | ||
下标 | -1 | 0 | 1 | 2 | 3 | ... target... | n-2 | n-1 | n |
第二步,如果check(mid)为1,证明M左侧的下标都满足,于是我们可以把M换成L,反之换成R
L,R位置 | L | R | |||||||
是否满足 | 1 | 1 | 1 | 1 | 1...1(tar)0... | 0 | 0 | ||
下标 | -1 | 0 | 1 | 2 | 3 | ... target... | n-2 | n-1 | n |
重复第二步的过程,直到L在R旁边,跳出循环
L,R位置 | L | R | |||||||
是否满足 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | ||
下标 | -1 | 0 | 1 | ... | target | target+1 | ... | n-1 | n |
注意到L始终在满足区,R始终在不满足区,故跳出循环时,L必为target,R必为target+1,接下来根据题目需要输出L或者R。
关于新方法的常见问题
一,会不会陷入死循环?
事实上是不会的。因为mid=(left+right)/2。我们可以证明:当right-left>=2时,经过mid=left或mid=right操作后,right-left会严格单调递减。而当right-left==1时不满足循环条件,会跳出循环。
二,这个方法能不能遍历所有元素
答案是能,因为无论何时,L左边的元素都是满足的,及已被遍历的,而R右边的元素同理。当跳出循环时,L就在R左边,L和R之间已经没有其他元素了,故能遍历所有元素。
一道例题
可能光说理论大家无法理解,现在结合一道例题方便大家理解:
题目描述
在一个单调递增的数组中,有一个target,你需要返回target的下标。输入共两行,第一行为数组大小和target。第二行为数组。输出共一行,及target的下标。数据范围:N<10000。
思路
首先,我们要定义check,我们令check为:当小于等于target输出1,其余情况则输出0。这样target也在1区,我们最后输出时应该输出L。接下来就是套用模板了。
代码
#include<iostream>
const int N = 10010;
int arr[N];
int n,t;
bool check(int mid) {
if (arr[mid] <= t) {
return 1;
}
return 0;
}
int find(){
int l = -1;
int r = n;
while (r - l > 1) {
int mid = r + l >> 1;
if (check(mid)) l=mid;
else r=mid;
}
return l;
}
int main() {
std::cin >> n;
std::cin >> t;
for (int i = 0; i < n; i++) std::cin >> arr[i];
std::cout << find();
}
提交结果:
输入 8 4
1 1 1 4 6 7 8 9
输出 3