二分查找
- 原理
- 经典例题
-
- [[704. 二分查找](https://leetcode.cn/problems/binary-search/)](#704. 二分查找)
- [[34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/)](#34. 在排序数组中查找元素的第一个和最后一个位置)
- [[35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/)](#35. 搜索插入位置)
- [[69. x 的平方根 ](https://leetcode.cn/problems/sqrtx/)](#69. x 的平方根 )
- [[852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/)](#852. 山脉数组的峰顶索引)
- [[162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/)](#162. 寻找峰值)
- [[153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/)](#153. 寻找旋转排序数组中的最小值)
- [[LCR 173. 点名](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/)](#LCR 173. 点名)
原理
二分查找一般用于数组有序的情况,但不仅仅限于这种情形,更加普遍地来说,它适用于可以将一个整体切分为两个具有不一样特征的部分的情况,即问题具有二段性。它的思路很简单,就是将数组分为两部分,一部分是不存在目标的部分,另一部分是目标可能存在的部分,按照这个思路,其实遍历查找也是二分查找,只不过它每次只能排除一个数据,如果是这样的话,那么我们为什么不进行类似于1/4切分或者2/3这种切分,而是以1/2进行切分呢?可以这样想,数据是不确定的,如果进行1/4切分的话,有1/2的概率一次就排除3/4的数据,但也有1/2的概率一次只排除1/4的数据,从整体概率考虑,进行1/2切分是最优的。
二分问题可以细分为:一般的二分切分、寻找左边界的二分切分、寻找右边界的二分切分
在实现时需要特别注意:
- 循环结束条件
- 中间值mid的求取方式
- 左右指针每一次的步长
一、一般的二分切分
即在有序数组中寻找目标值
cpp
int BinarySearch(vector<int>& num, int target){
int left = 0;
int right = num.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
//也可以是int mid = left + (right - left) / 2;
if (num[mid] == target) {
return mid;
}
else if (num[mid] < target) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1;
}
二、寻找左边界的二分切分
cpp
int BinarySearch(vector<int>& num, int target){
int left = 0;
int right = num.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (num[mid] >= target) {
right = mid;
}
else {
left = mid +1;
}
}
if (num[left] == target) {
return left;
}
return -1;
}
- 步长:在num[mid] == target时,不能让right = mid-1,因为我们现在在寻找左边界,如果mid位置已经是左边界了,此时就会错过该位置,为了简便,直接在num[mid] >= target时设置---right = mid;
- 循环条件:如果循环结束条件为left <= right,最后ledt==right时如果走了right=mid就会导致死循环,因此循环条件为----:left <= right
- mid取值方式: 当只剩两个元素时,由于mid取较左边的值,无论走哪个条件都能出循环
三、寻找右边界的二分切分
cpp
int BinarySearch(vector<int>& num, int target){
int left = 0;
int right = num.size() - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (num[mid] <= target) {
left = mid;
}
else {
right = mid - 1;
}
}
if (num[left] == target) {
return left;
}
return -1;
}
求解二分问题的一般思路为:
- 根据问题分析出二段性
- 选择合适的切分模板
- 根据问题分析处理细节
经典例题
704. 二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
cpp
class Solution {
public:
int search(vector<int>& nums, int target) {
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;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans(2,-1);
if(0==nums.size()){
return ans;
}
int left=0;
int right=nums.size()-1;
//寻找左端点
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target){
left=mid+1;
}else{
right=mid;
}
}
if(target!=nums[left]){
return ans;
}
ans[0]=left;
//寻找右端点
left=0;
right=nums.size()-1;
while(left<right){
int mid=left+(right-left+1)/2;
if(nums[mid]>target){
right=mid-1;
}else{
left=mid;
}
}
ans[1]=right;
return ans;
}
};
35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid=0;
while(left<=right){
mid=left+(right-left)/2;
if(nums[mid]==target){
return mid;
}
else if(nums[mid]>target){
right=mid-1;
}else{
left=mid+1;
}
}
if(nums[mid]<target){
return mid+1;
}
return mid;
}
};
69. x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
cpp
class Solution {
public:
int mySqrt(int x) {
//确定量级
int i=1;
long long int j=10;
while(x/j){
j*=10;
i+=1;
}
int left=0;
int right=1;
i=(i+1)/2;
while(i--){
right*=10;
}
while(left<=right){
unsigned long long mid=left+(right-left)/2;
unsigned long long tmp=mid*mid;
if(tmp==x||(tmp<x&&(mid+1)*(mid+1)>x)){
return mid;
}else if(tmp<x){
left=mid+1;
}else{
right=mid-1;
}
}
return 0;
}
};
852. 山脉数组的峰顶索引
给定一个长度为 n 的整数 山脉 数组 arr ,其中的值递增到一个 峰值元素 然后递减。
返回峰值元素的下标。
你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。
cpp
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 0;
int right = arr.size() - 1;
int mid = 0;
while (left < right) {
if (left + 1 == right) {
return arr[left] > arr[right] ? left : right;
}
mid = left + (right - left) / 2;
int lmid = left + (mid - left + 1) / 2;
if (arr[left] < arr[lmid]) {
if (arr[lmid] <= arr[mid]) {
left = lmid;
} else {
right = mid;
}
} else {
right = lmid;
}
}
return mid;
}
};
162. 寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
cpp
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left + 1) / 2;
if ((0 == mid || nums[mid - 1] < nums[mid]) &&
(mid + 1 == nums.size() || nums[mid] > nums[mid + 1])) {
return mid;
}
if (nums[mid - 1] > nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
};
153. 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
cpp
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[left] <= nums[mid]) {
if (nums[mid] < nums[right]) {
return nums[left];
}
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
};
LCR 173. 点名
某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。
- 根据问题分析出二段性
取中间点mid,如果records[mid]==mid,说明[left,mid]区间内的学生一定没有缺席,可以让left=mid+1;如果records[mid]!=mid,让right=mid,此时不能让right=mid-1,因为rnid的位置可能就是缺席学生的位置,让right=mid-1会直接错过该位置
- 选择合适的切分模板
根据前面的分析可以得知应该选用寻找左边界的切分模板
- 根据问题分析处理细节
有可能是最后一名学生缺席,在最后需要特别处理这种情况
cpp
class Solution {
public:
int takeAttendance(vector<int>& records) {
int left=0;
int right=records.size()-1;
while(left<right){
int mid=left+(right-left)/2;
if(records[mid]!=mid){
right=mid;
}else{
left=mid+1;
}
}
if(left+1==records.size()&&left==records[left]){
return left+1;
}
return left;
}
};