参考:
目录
题目:704.二分查找(力扣)
零.描述
思路不难,难于细节
1. 最常用的二分查找场景
寻找一个数、寻找左侧边界、寻找右侧边界。
细节:while (不等号) 是否应该带等号; mid 是否应该加一
2. 二分法框架
int binarySearch(int[] nums, int target) {
int left = 0, right = ...; //注意1: right
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) { ...}
else if (nums[mid] < target) { left = ...} //注意2: 下一次left取值
else if (nums[mid] > target) { right = ...}
}
return ...;
}
区别:左闭右闭 or 左闭右开
3. 左闭右闭 [left,right]
关键:当left ==right 区间左闭右闭,仍然有效 ,因此需要while( <= )
举例:{1,3,4 }寻找4,如果缺少等于,就找不到
L=0,R=2,M=1 ,nums[M]=3<4
L=2,R=2,缺少等于,不进入while循环,错误输出-1
//搜索一个数,如果存在,返回其索引,否则返回 -1。
int Search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意[l,r]
while(left <= right) { // 注意while(l<=r) "<="
// 当left==right,区间[left, right]依然有效,所以用 <=
int mid = left + (right - left) / 2;
if(nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid + 1; // 注意
// target 在右区间,所以[middle + 1, right]
else if (nums[mid] > target) right = mid - 1; // 注意
// target 在左区间,所以[left, middle - 1]
}
return -1;
}
// 同理
int Search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
// 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else{
return middle; // nums[middle] == target
} // 数组中找到目标值,直接返回下标
}
// 未找到目标值
return -1;
}
3. 左闭右开 [left,right)
关键: 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
举例:{1,3,4 }寻找4
L=0,R=3,M=1 ,nums[M]=3<4
L=2,R=3,M=2,nums[M]=4 正确输出
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) {
// 因为left == right的时候,在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
一、七种情况(原文见参考2)
总结:
- 查找数组中等于key的数字
基础模板,常用[left,right] - 查找数组中第一个等于key的数字
区别:去arr[mid]== key分支,改arr[mid]>key 为 >=
加判断 if(L < arr.size() && arr[L] == key)
输出L - 查找数组中最后一个等于key的数字
区别:去arr[mid]== key分支,改arr[mid]<key 为 <=
加判断 if(R >= 0 && arr[R] == key)
输出R - 查找数组中第一个大于等于key的数字
区别:去arr[mid]== key分支,改arr[mid]>key 为 >=
加判断 if(L < arr.size())
输出L - 查找数组中最后一个小于等于key的数字
区别:去arr[mid]== key分支,改arr[mid]<key 为 <=
不要加判断 if(R >= 0)不要return -1
输出R - 查找数组中第一个大于key的数字
区别:去arr[mid]== key分支,依旧arr[mid]>key
不要加判断 if(L < arr.size()) 不要return -1
输出L - 查找数组中最后一个小于key的数字
区别:去arr[mid]== key分支,依旧arr[mid]<key
不要加判断 if(R >= 0)不要return -1
输出R
1、查找数组中等于key的数字
如上文所述。
2、查找数组中第一个等于key的数字
问题:数组中可能有重复的key,要找的是第一个key的位置
分析:找到一个等于key的数,依然还要继续寻找它的前面有没有等于key的数。
- 修改之前的arr[mid]>key修改为arr[mid]>=key,并且去掉arr[mid]== key这条分支,
这样就算遇到了等于key的答案,依然不会返回 - 程序继续往前寻找是否有其它等于key的值
- 最后跳出循环之后,如果L满足L<arr.size()&&arr[L]==key,返回L的值作为结果,
- 否则输出-1,代表没有找到。
代码:
/**查找第一个与key相等的元素的下标, 如果不存在返回-1 */
int erfenfirstEqual(vector<int> arr, int key){
int L = 0, R = arr.size() - 1; //在[L,R]查找第一个>=key的
int mid;
while( L <= R){
// 采用[left,right]
mid = L + ((R - L) >> 1);
if(arr[mid] >= key)//第一次key出现在[L,mid]中
R = mid - 1; //下一次搜索范围[L,mid-1]
//意义:想使得arr[R+1]==key,即,R是key索引之前的倒数第一个数
else
L = mid + 1; //下一次搜索范围[mid+1,R]
//意义:上面固定了arr[R+1]==key,再使得L不断递增,使得L=R+1跳出
}
//此时,L>R
if(L < arr.size() && arr[L] == key) //
return L;
return -1;
}
举例:
- 找到一个等于key的数,并且它的前面没有等于key的数了
1,2,3,5,6,6,6,6,6,6(key=6)【正确解为4】
L=0,R=9,M=4,arr[M]=key 但是去掉arr[mid]== key分支,因此继续循环
L=0,R=4-1=3,M=1,arr[M]<key L=mid+1
L=2,R=3,M=2,arr[M]<key L=mid+1
L=3,R=3,M=3,arr[M]<key L=mid+1
L=4,跳出循环,判断arr[L]==key - 找到一个等于key的数,但是它的前面还有等于key的数。
1,2,3,5,6,6,6,6,6,6,6(key=6)【正确解为4】
L=0,R=10,M=5,arr[M]=key 但是去掉arr[mid]== key分支,因此继续循环
L=0,R=5-1=4,M=2,arr[M]<key L=mid+1
L=3,R=4,M=3,arr[M]<key L=mid+1
L=4,R=4,M=4,arr[M]=key 但是去掉arr[mid]== key分支,因此继续循环
L=4,R=4-1=3,M=3,arr[M]<key L=mid+1
L=M+1=4, 此时因为L>R,跳出循环 - 数组中不存在key,那么输出条件的arr[L] == key就不会满足,
输出的依然是-1;
记忆:
修改之前的arr[mid]>key修改为arr[mid]>=key,并且去掉arr[mid]== key这条分支
想法在于向使得R为最后一个小于key的数,然后不断增加L,使得L>R,跳出while
3、查找数组中最后一个等于key的数字
问题:数组中可能有重复的key,要找的是最后一个key的位置
分析:找到一个等于key的数,依然还要继续寻找它的后面有没有等于key的数。
- 修改之前的arr[mid]>=key修改为arr[mid]<=key
- 将R=mid-1改成了L=mid+1,表示向后移动
代码:
/**查找第一个大于等于key的元素的下标*/
int erfenlasteuual(vector<int> arr,int key){
int L = 0, R = arr.size() - 1;
int mid;
while( L <= R){
mid = L + ((R - L) >> 1);
if(arr[mid] <= key) //注意:区别1
L = mid + 1;
else
R = mid - 1;
}
if(R >= 0 && arr[R] == key) //注意:区别2
return R;
return -1;
}
记忆:
跟找第一个等于key类似,区别在于:先if的是arr[mid]<=key
输出R
4、查找数组中第一个大于等于key的数字
问题:返回第一个等于key的数,如果没有等于key的数,返回第一个大于key的数,
如果没有大于key的数,那么返回-1
分析:
- 在第二种情况的代码下修改,修改if(L < arr.size()&& arr[L]== key) 为if(L <arr.size()),
因为如果没有等于key的数,那么就直接返回L就可以 - L有几种情况:
- 第一个是L没有超过数组的范围,那么返回的就是正确的结果,
如果L>=arr.size(),那么说明整个数组都小于key,直接返回-1.
如果数组中有key的情况下,那么最后按照寻找第一个key的思想,会返回正确的答案 - 没有key的情况
1、所有元素大于key:
L一直不会变化的,因为不会运行到L=mid+1这一条分支,最后返回0,答案正确
2、所有元素小于key
R一直不会变化的,因为不会运行到R=mid-1,最后一次循环肯定就是当L等于R的时候
依然还是走了L=mid+1着一个分支,此时的mid为arr.size()-1,
因为R是不会变的,R最开始是arr.size()-1,最后就有L=R+1=mid+1=arr.size().
返回 arr.size().答案正确.
3、不存在key,但是数组元素有比key大的,有比key小的
R可能有两种指向:
指向最后一个小于key的数
指向第一个大于key的数
(因为二分法的性质决定的),不可能再往前或者往后指了,但是最后跳出循环的条件必 然是L等于R,那么如果指向小于key的数,跳出循环前做L=mid+1,或者指向大于key,
跳出循环前做R=mid-1,结果都是正确的。
代码:
/*在[L,R]查找第一个>=key的数字*/
int erfenfirstlargeEqual(vector<int> arr,int key){
int L = 0, R = arr.size() - 1;
int mid;
while( L <= R){
mid = L + ((R - L) >> 1);
if(arr[mid] >= key)
R = mid - 1;
else
L = mid + 1;
}
if(L < arr.size()) // 与寻找第一个等于key问题 区别if条件 不需要 arr[L] == key
return L;
return -1;
}
记忆:
跟找第一个等于key类似,区别在于:最后输出判断时,if条件不需要arr[L]==key
5、查找数组中最后一个小于等于key的数字
因为R只有等于-1这一种情况超出数组范围,而能造成这种情况的前提就是数组中确实没有小于等于key的数字,所以直接输出R就行了
代码:
int erfenlastSmallEqual(vector<int> arr,int key){
int L = 0, R = arr.size() - 1;
int mid;
while( L <= R){
mid = L + ((R - L) >> 1);
if(arr[mid] <= key)
L = mid + 1;
else
R = mid - 1;
}
//if(R >= 0 && arr[R] == key) //注意:区别于找第最后一个等于key的值
return R;
//也不需要return -1
}
6、查找数组中第一个大于key的数字
问题:
和第二种和第四种情况的不同在于:if(arr[mid] >= key)改成了if(arr[mid] > key),因为不是要寻找等key的;而是要寻找大于key的,
但是因为是寻找第一个大于的,所以依然找到后不能直接返回,和普通的二分法有点像,但是没有等于key跳出这条分支,最后依然是返回L
分析:
- 全部小于key,那么R不会变,直到最后都不会变,
所以最后返回的L=mid+1= R+1= arr.size()-1+1 = arr.size() - 全部大于key,那么L不会变,最后返回L,L=0
- 大于的,有小于的
因为在等于key的时候是选择做L=mid-1,所以R最小只能指向最后一个等于key或者最后一个小于key的值(无key的情况下),那么最后的结果是返回L=R+1,依然是返回了第一个大于key的数
代码:
/**查找第一个大于key的元素的下标 */
int erfenfirstLarge(vector<int> arr,int key){
int L = 0,R = arr.size() - 1;
int mid;
while(L <= R){
mid = L + ((R - L) >> 1);
if(arr[mid] > key)
R = mid - 1;
else
L = mid + 1;
}
return L; //区别在此,不需要判断
}
7、查找数组中最后一个小于key的数字
int erfenlastSmall(vector<int> arr,int key){
int L = 0, R = arr.size() - 1;
int mid;
while(L <= R){
mid = L + ((R - L) >> 1);
if(arr[mid] < key)
L = mid + 1;
else
R = mid - 1;
}
return R;
}