二分查找算法(一)
一、力扣704——二分查找
1.暴力解法:直接for遍历一遍数组,找到值就返回下标,未找到,返回-1。O(N)
2.二分查找: 具有二段性,根据某规律,能将数组分成两部分,能够砍掉数组的一部分
暴力解法的缺陷,每次查找时,只能消除掉其中的一个数,利用二分查找可以砍掉一部分。
代码如下:
class Solution {
public:int search(vector<int>& nums, int target) {int n = nums.size();int left = 0, right = n - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else {return mid;}}return -1;}
};
二、力扣34——在排序数组中查找元素的第一个和最后一个位置
题目解析
非递减顺序:表示要么是递增的,要么是平衡的(数值相同),找到就返回值所在的区间,找不到就返回[-1, -1]。空数组也返回[-1, -1]
算法原理
1.暴力解法:直接遍历数组,找到数值第一次出现的位置begin,保存下来,继续寻找,找到最后一个出现的位置end,再保存,得到区间[begin, end],O(N)
2.二分查找
如果用朴素二分,会导致,最后的时间复杂度变为暴力解法的时间复杂度
1.查找区间的左端点
注意:当mid所处位置的值>=target,right 不能再等于mid的左边值了,而是直接 == mid,因为有可能mid所处的位置就是target
难点:细节处理
1.循环条件
left <= right 错误
left < right
1.left == right的时候,就是最终结果,无需判断
2.如果判断,就会死循环
三种情况讨论:
- 有结果,right一定不会越过ret ,当left == right 时,就是最终的结果,无序判断
- 全大于target
只会命中第二个条件,left不会动,right一直向右边移动,直到left和right相遇为止就结束,最后判断left处的值是否等于target。不等于 返回 -1
- 全小于target,只会命中第一个条件,right不会动,left一直向右边移动,直到left和right相遇为止就结束。不能判断,判断就会死循环。
2.求中点的操作,如何求mid
left + (right - left)/2
left + (right - left + 1)/2 ---> 会陷入死循环
2.查找区间的右端点
类似查找左端点
1.循环条件 -> 同查找左端点: left < right
2.求中点的方式:
left + (right - left) / 2
left + (right - left + 1) / 2
注意此时只剩两个元素,left指向mid,如果命中第一个条件,left就一直指向mid,
使用第一种方式: left + (right - left) / 2 = left 就会导致陷入死循环.
选择第二种方式 :mid = left + (right - left + 1) / 2 = left + 1 ,left = mid = left + 1,就会和right指向同一个元素,就能终止循环
代码如下:
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {if(nums.size() == 0) return {-1, -1};int begin = 0, end = 0;// 1.查找二分左端点int left = 0, right = nums.size() - 1;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else {right = mid;}}if (nums[left] != target)return {-1, -1};elsebegin = left;// 2.二分右端点left = 0, right = nums.size() - 1;while (left < right) {int mid = left + (right - left + 1) / 2;if (nums[mid] <= target) {left = mid;} else {right = mid - 1;}}end = right;return {begin, end};}
};
总结二分模版(查找区间左端点的模版,和查找区间右端点的模版)
左端点
while (left < right) {
int mid = left + (right - left) / 2;
if (......) {
left = mid + 1; //记这里
} else {
right = mid;
}
}
右端点
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (......) {
left = mid;
} else {
right = mid - 1; //记这里
}
}
如何记忆:当下面出现 - 1的时候上面就 +1
三、力扣69——x的平方根
题目解析
返回一个比真实值小的最近整数。
算法原理
1.暴力解法:
依次从1开始算他们的平方数,如果没有刚好等于的,就返回刚大于x的前一个数的原始值,也就是找到这个刚大于x的数后-1
2.二分查找:
具有二段性,不朴素的二分查找:
准备left ,right, mid:left从1开始,right就为x(因为可能x = 1):
1.mid * mid <= x,left = mid
2.mid * mid > x , right = mid - 1
代码编写
注意long long防止溢出
class Solution {
public:int mySqrt(int x) {if (x < 1)return 0; // 处理边界情况long long left = 1, right = x;while (left < right) {long long mid = left + (right - left + 1) / 2;if (mid * mid <= x) {left = mid;} else {right = mid - 1;}}return left;}
};
四、力扣35——搜索插入位置
算法原理
二分查找:
1.恰好能找到这个值
2.找插入位置
在中间:左边是小于 t ,右边都是大于 t 的
在边缘:全部大于t ,直接返回 0 ,全部小于t,直接返回nums.end()
所要找的值,如果是要刚好等于,返回mid,插入的位置一定是在刚好大于target的这个位置,因此我们需要分的是小于和大于等于这二段
理解:
如果我们的nums[mid]值小于target,那么我们需要找的下标一定不会再这个区间内,因此是left
= left + 1 ,如果nums[mid]值大于等于target,那么需要找的值就一定在这个区间内right = mid。
代码如下:
class Solution {
public:int searchInsert(vector<int>& nums, int target) {int left = 0,right = nums.size() - 1;int max = nums[right];while(left < right){int mid = left +(right - left) / 2;if(nums[mid] < target) left = mid + 1;else right = mid;}if(nums[left] < target) return left + 1;else return left;}
};
五、力扣852——山脉数组的峰顶索引
算法原理
二分查找:
当arr[mid] > arr[mid - 1]的时候,说明这是上坡,不一定到转折点
arr[mid] < arr[mid - 1]的时候,说明这是下坡,前面一定有一个转折点也就是山坡
这就是这道题的二义性
代码如下:
class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {int left = 0,right = arr.size() - 1;while(left < right){int mid = left + (right - left + 1) / 2;if(arr[mid] > arr[mid - 1]) left = mid;else right = mid - 1;}return left;}
};
六、力扣162——寻找峰值
题目解析
遇上题不同的是,有多个峰值,最左和最右都是比所有峰值小的
算法原理
1.暴力解法:
从第一个位置开始,一直向后走,分情况讨论,O(N),这道题的数据范围比较小,直接暴力解法也是可以的,峰值有可能在最后
2.二分查找:优化暴力解法
这里还是类似于上一道题 这道题特殊在
上图即为我们会遇到的山峰
- 1是遇到递减,说明之前一定有个山峰。
- 2是递增在递减,中间一定有个山峰。
- 3是一直递增,但因为nums[n] = 负无穷,所以在后面一定会的碰到一个递减,在此之间有一个山峰。
代码如下:
class Solution {
public:int findPeakElement(vector<int>& nums) {int left = 0,right = nums.size() - 1;while(left < right){int mid = left +(right - left) / 2;if(nums[mid] < nums[mid + 1]) left = mid + 1;else right = mid;}return left;}
};
七、力扣153寻找旋转排序数组中的最小值
如何旋转:
旋转n次后,nums原来就是一个升序排列的数组,传给我们的是经过n次旋转的,我们需要得到原始的升序排列的数组 ,并且返回数组中最小的元素,注意数组元素互不相同。
算法原理
1.暴力查找最小值:从前往后遍历,找最小值。慢在,没有使用这个数组的特性
2.二分查找:
这个数组一定存在两段数值上升的递增区间。
二段性:以D为参照点,会发现,左边的AB段的数组一定都是大于D的值的,而CD这个区域的所有值都是小于等于D的,而我们最终要找的值,就是C点。
如果mid落在A~B这个区间:nums[mid] < nums[n - 1](C ~ D)
如果mid落在C~D这个区间:nums[mid] >= nums[n - 1](C ~ D)
代码如下:
class Solution {
public:int findMin(vector<int>& nums) {int left = 0,right = nums.size() - 1;int x = nums[right];while(left < right){int mid = left + (right - left) / 2;if(nums[mid] > x) left = mid + 1;else right = mid;}return nums[left];}
};
八、力扣LCR 173——点名
题目解析
就是一共有9个人,但是有一个人的学号不在,因此数组长度只会有8个,就要找到这个不在的人
算法原理
- 解法一:高斯求和公式:原值求和 - 现有值的和 = 缺失的数
- 解法二:哈希表 ,建立一个 n+1大小的哈希表,遍历原始数组往里填,最后查看哈希表哪一个位置没有填,就是缺失的数字
- 解法三:直接遍历,for循环找,不等于则返回这个值
- 解法四:位运算,将题目所给数组,和一个完整数组的元素进行位运算异或。最后异或的结果就是位运算的结果。
前四种的时间复杂度都是O(N);
解法五:二分查找:
发现二段性:
从缺失值处开始划分,左边的值和下标都是一一对应的,当遇到不对应的值的时候就返回下标
细节问题:如果数组是【0,1,2,3】,缺失的值是4,但是在我们的二分查找里面并没有得到这个值
因此在最后还需要判断一下records[left] == records[right] == records.size() - 1,的话,就返回left + 1
代码编写
class Solution {
public:int takeAttendance(vector<int>& records) {//二分int left = 0,right = records.size() - 1;int cnt = records[right];while(left < right){int mid = left + (right - left + 1) / 2;if(records[mid] == mid) left = mid;else right = mid - 1;}if(records[left] == left) return left + 1;else return left;}
};