专题三 之 【二分查找】
目录
1.二分查找的简介
2.例题
2.1 704. 二分查找 - 力扣(LeetCode)
2.2 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
2.3 LCR 068. 搜索插入位置 - 力扣(LeetCode)
2.4 69. x 的平方根 - 力扣(LeetCode)
2.5 852. 山脉数组的峰顶索引 - 力扣(LeetCode)
2.6 162. 寻找峰值 - 力扣(LeetCode)
2.7 153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
编辑
2.8 LCR 173. 点名 - 力扣(LeetCode)
1.二分查找的简介
当查找的数据将整堆数据划分为两部分,即数据具有 二段性 时,
二分查找就可以很好的解决问题,并不只有有序数组才能使用二分查找算法
- 朴素二分查找模板
        //朴素二分查找int left = 0, right = nums.size() - 1;while(left <= right){//中间点的取值,向上或向下取整均可int midi = left + (right - left + 1) / 2;//核心移动逻辑if(......) right = midi - 1;else if(.....) left = midi + 1;else return .....;}- 二分查找左边界模板
        int left = 0, right = nums.size() - 1;while(left < right)//结束条件,分情况证明{int midi = left + (right - left) / 2;if(....) right = midi;else left = midi + 1;} - 二分查找右边界模板
        int left = 0, right = nums.size() - 1;//左端点可以不动while(left < right){int midi = left + (right - left + 1) / 2;if(.....) right = midi - 1;else left = midi;}根据二段性理解记忆上述模板,下面通过例题来初步感受二分查找算法
2.例题
2.1 704. 二分查找 - 力扣(LeetCode)

方法一:暴力查找,时间复杂度为O(N)
方法二:根据二段性,使用二分查找算法

二段性:要查找的值target将数组分为了三部分
(1)横线代表数组nums,目标值为target
(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点
(1)
- 当nums[ mid ] > target 时,由于数组是升序数组,target只可能存在于 [left,mid-1]中,此时更新 right = mid - 1,继续比较nums[ mid ] 和 target 的大小关系
- 当nums[ mid ] < target 时,由于数组是升序数组,target只可能存在于 [mid+1,right]中,此时更新 left = mid + 1,继续比较nums[ mid ] 和 target 的大小关系
- 当nums[ mid ] == target 时,返回 mid 即可
- 区间不断缩小,当区间还剩下一个数时,仍需继续比较,直到left > right 时,查找结束
class Solution
{
public:int search(vector<int>& nums, int target){//使用二分查找的前提是:找到二段性//根据某种条件可以将数组分为两部分,舍弃一部分,再在另一部分中查找int left = 0, right = nums.size() - 1;while(left <= right)//只有一个数也需要进行判断{//中间点的取值,左右均可int midi = left + (right - left + 1) / 2;//核心移动逻辑if(nums[midi] > target) right = midi - 1;else if(nums[midi] < target) left = midi + 1;else return midi;}return -1;}
};细节问题
(1) 中间值的计算方法
midi = (left + right) / 2 --------------------1
midi = (left + right + 1) / 2 ----------------2
midi = left + (right - left) / 2; --------------3
midi = left + (right - left + 1) / 2; ---------4
当数据个数为奇数时,上述计算方法得到的值相同
当数据个数为偶数时,1、3两种方法向下取整,2、4两种方法向上取整。例如1 2 3 4 四个数,left 指向1,下标为0,right 指向4,下标为3,此时 1、3两种方法向下取整 midi指向2,2、4两种方法向上取整,midi指向3
当数据两较大时,3、4两种方法还具有防溢出的好处
(2) 二分查找需要不断比较nums[ mid ] 和 target 的大小关系,所以是一个循环,同时循环结束条件应当是left <= right,因为当区间还剩下一个数时,我们还能进行一次查找判断
(3)朴素的二分查找,向上取整、向下取整均可,因为当nums[ mid ] != target时,left、right不会固定不动(可以根据下面两种模板再来理解这句话)
查找一次区间长度缩小一半,设查找了m次,一共有N个数据,则时间复杂度为O(m),又因为2^m = N, m = logN,所以二分查找时间复杂度为O(logN)
通过严格的数学证明,二分查找效率优于n分查找(n > 2)感兴趣的朋友可以参考算法导论
2.2 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

方法一:暴力求解,直接遍历查找,时间复杂度O(N)
方法二:根据二段性使用二分查找算法,时间复杂度正好O(logN)

查找左边界
二段性:要查找的值target将数组分为了两部分
(1)横线代表数组nums,目标值为target
(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点
(1)
- 当nums[ mid ] >= target 时,由于数组是非递减数组,target只可能存在于 [left,mid]中,此时更新 right = mid,继续比较nums[ mid ] 和 target 的大小关系
- 当nums[ mid ] < target 时,由于数组是非递减数组,target只可能存在于 [mid+1,right]中,此时更新 left = mid + 1,继续比较nums[ mid ] 和 target 的大小关系
- 区间不断缩小,当区间还剩下一个数时,跳出循环进行判断
细节问题
(1)中点midi的求法:midi = left + (right - left) / 2 向下取整,这是因为若向上取整
当区间个数为2,且nums[ mid ] >= target,right = mid的移动规则导致right根本没移动,最终进入死循环。例如3 4两个数,target = 0,left = 0,指向3,right =1,指向4,向上取整,midi = 1,指向4, 根据移动规则right = midi = 1,这不就进入死循环了吗。反之向下取整,midi = 0,不管命中哪个判断条件,区间长度都在缩小,最终达到循环结束条件
(2) 循环结束条件
整合上述情况,最终当区间长度为1时,选择跳出循环判断查找结果
查找右边界
二段性:要查找的值target将数组分为了两部分
(1)横线代表数组nums,目标值为target
(1) left、right 分别指向数组的两端,mid 为 left 和 right 的中点
(1)
- 当nums[ mid ] <= target 时,由于数组是非递减数组,target只可能存在于 [mid,right]中,此时更新 left= mid,继续比较nums[ mid ] 和 target 的大小关系
- 当nums[ mid ] > target 时,由于数组是非递减数组,target只可能存在于 [left,mid-1]中,此时更新 right = mid - 1,继续比较nums[ mid ] 和 target 的大小关系
- 区间不断缩小,当区间还剩下一个数时,跳出循环进行判断
细节问题
(1)中点midi的求法:midi = left + (right - left) / 2 向上取整,这是因为若向下取整
当区间个数为2,且nums[ mid ] <= target,left = mid的移动规则导致left根本没移动,最终进入死循环。例如3 4两个数,target = 4,left = 0,指向3,right =1,指向4,向下取整,midi = 0,指向3, 根据移动规则left = midi = 0,这不就进入死循环了吗。反之向上取整,midi = 1,不管命中哪个判断条件,区间长度都在缩小,最终达到循环结束条件
(2) 循环结束条件
整合上述情况,最终当区间长度为1时,选择跳出循环判断查找结果
class Solution
{
public:vector<int> searchRange(vector<int>& nums, int target){//处理边界情况if(nums.size() == 0) return {-1, -1};int begin = 0;int left = 0, right = nums.size() - 1;//左端点  二段性    <target    >=target//细节问题:1.循环结束条件   无需判断、有可能进入死循环//2.求中点   区间可能不动while(left < right)//结束条件,分情况证明{int midi = left + (right - left) / 2;//右边界不动,向左靠if(nums[midi] >= target) right = midi;else left = midi + 1;}    if(nums[left] != target) return {-1, -1};else begin = left;//右端点  二段性    >=target    <targetleft = 0, right = nums.size() - 1;//左端点可以不动while(left < right){int midi = left + (right - left + 1) / 2;if(nums[midi] > target) right = midi - 1;else left = midi;}//有左端点,一定就有右端点return {begin, right};}
};
right处于向上取整位置,left处于向下取整位置
在二分查找左右边界的模板中,midi的求法可以根据“谁不变,取相反”的口诀进行记忆
2.3 LCR 068. 搜索插入位置 - 力扣(LeetCode)

方法一:遍历查找,时间复杂度O(N)
方法二:根据二段性使用二分查找算法
仔细分析,target将数组分为两部分,即二分查找左边界
细节问题:
三种情况中,前两种情况跳出循环后,下标就是插入或存在下标,当数组元素全小于target时,插入下标应该是 left + 1
class Solution {
public:int searchInsert(vector<int>& nums, int target) {//二分查找左端点int left = 0, right = nums.size() - 1;while(left < right){int midi = left + (right - left) / 2;if(nums[midi] < target) left = midi + 1;else right = midi;}//全小,来到数组的最后一个位置,但是应该存放在最后一个位置的下一个位置//其余情况,找得到,就返回下标,找不到,正好是比target大的第一个数的位置下标if(nums[left] < target) return left + 1;else return left;}
};2.4 69. x 的平方根 - 力扣(LeetCode)

方法一:从前到后暴力比较每一个中,时间复杂度O(N)
方法二:根据二段性使用二分查找
仔细分析,这里算术平方根是向下取整,查找的数将[0, x]区间分为两部分,
查找平方等于x或者平方小于x的数中的最大的数,即二分查找右边界
细节问题:int类型的数进行平方可能会溢出
class Solution 
{
public:int mySqrt(int x){//所找的数的平方小于等于x//二分查找,右端点   <=  >//越界问题long long left = 0, right = x;while(left < right){long long midi = left + (right - left + 1) / 2;if(midi * midi <= x) left = midi;else right = midi -1;}return left;}
};2.5 852. 山脉数组的峰顶索引 - 力扣(LeetCode)

方法一:遍历查找,时间复杂度O(N)
方法二:根据二段性使用二分查找算法

上下坡不就是典型的二段性吗
山峰的左边,nums[i] < nums[i+1]; 山峰的右边,nums[i] > nums[i+1];

class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {//二分查找右边界int left = 0, right = arr.size() - 1;while(left < right){int midi = left + (right - left + 1) / 2;if(arr[midi] < arr[midi - 1]) right = midi - 1;else left = midi;}return left;}
};
class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {//二分查找左边界int left = 0, right = arr.size() - 1;while(left < right){int midi = left + (right - left) / 2;if(arr[midi] < arr[midi + 1]) left = midi + 1;else right = midi;}return left;}
};2.6 162. 寻找峰值 - 力扣(LeetCode)

方法一:根据峰值特点,分下面三种情况,直接遍历查找,时间复杂度O(N)

上下坡正好具有二段性,二分查找正好适用

class Solution {
public:int findPeakElement(vector<int>& nums) {//左边界int left = 0, right = nums.size() - 1;while(left < right){int midi = left + (right - left) / 2;if(nums[midi] > nums[midi + 1]) right = midi;else left = midi + 1;}return left;}
};
class Solution {
public:int findPeakElement(vector<int>& nums) {//右边界int left = 0, right = nums.size() - 1;while(left < right){int midi = left + (right - left + 1) / 2;if(nums[midi] > nums[midi - 1]) left = midi;else right = midi - 1;}return left;}
};2.7 153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
方法一:直接遍历查找最小值,时间复杂度O(N)

可以将旋转数组抽象为上述两种图形,要么升序排列,要么间断升序排列
通过判断首尾元素的大小就可以区分两种情况,升序排列直接返回首元素,避免下面的判断
间断升序排列时,1区域元素恒大于2区域元素,这不就是二段性吗
- 以B为target,最小值左边元素恒大于B,包含最小值在内的右边元素恒小于等于B
nums[midi] > target,即midi落在1区域时,应当更新 left = midi + 1
nums[midi] <= target,即midi落在2区域时,应当更新 right = midi
- 以A为target,最小值左边元素恒大于等于A,包含最小值在内的右边元素恒小于A
nums[midi] >= target,即midi落在1区域时,应当更新 left = midi + 1
nums[midi] < target,即midi落在2区域时,应当更新 right = midi
class Solution {
public:int findMin(vector<int>& nums) {//经过旋转之后的数组,首大于尾int n = nums.size();if(nums[0] < nums[n - 1]) return nums[0];//二段性int left = 0, right = n - 1, target = nums[0];while(left < right){int midi = left + (right - left) / 2;if(nums[midi] >= target) left = midi + 1;else right = midi;}return nums[left];}
};2.8 LCR 173. 点名 - 力扣(LeetCode)

方法一:哈希表存入0~n-1个值,遍历数组,存在即删除,最后剩余的数就是缺失的数
方法二:从0开始遍历,序号与数组元素对不上时,序号就是缺失的数
方法三:将数组元素和0~n-1异或在一起,异或的结果就是缺失的数
方法四:高斯求和,再减去数组元素,剩下的值就是缺失的数
方法五:根据方法二下标与数组元素的对应关系可知,要查找的数的左边的所有元素与其下标一一对应,右边的所有元素比其下标大一

class Solution {
public:int takeAttendance(vector<int>& records) {//下标划分二段性int left = 0, right = records.size() - 1;while(left < right)//二分查找右端点{int midi = left + (right - left + 1) / 2;if(records[midi] == midi) left = midi;else right = midi - 1;}if(records[left] == left)//找到了端点return left + 1;else return left;}
};
class Solution {
public:int takeAttendance(vector<int>& records) {//下标划分二段性int n = records.size();int left = 0, right = n - 1;while(left < right)//二分查找左端点{int midi = left + (right - left) / 2;if(records[midi] != midi) right = midi;else left = midi + 1;}if(records[left] == left)//找到了端点return left + 1;else return left;}
};二分查找右端点的最终结果
(1)数组元素与下标一致,缺失的数等于left+1
(2)数组元素与下标不一致,缺失的数等于left
二分查找左端点的最终结果
(1)数组元素与下标一致,缺失的数等于left+1
(2)数组元素与下标不一致,缺失的数等于left









