二分查找(Binary Search)是一种在有序数组中查找特定元素的算法。该算法的基本思想是通过每一次比较,将查找范围缩小一半,最终找到目标元素或者确定目标元素不存在。
二分查找的步骤:
left
和 right
,分别指向数组的起始和结束位置。left <= right
的条件下,执行以下步骤。mid
,可以使用 mid = (left + right) / 2
或者 mid = left + (right - left) / 2
。这样可以防止整数溢出。right = mid - 1
。left = mid + 1
。left
大于 right
,此时查找范围为空,表示目标元素不存在。二分查找的优势:
二分查找的局限性:
二分查找的应用场景:
二分查找基本模板
朴素版(适用于最基础的二分)
while(left<=right){
int mid=left+(right-left)/2;
if(条件)
left=mid+1;
else if(条件)
right=mid-1;
else
return ...;
}
全能版(均可适用)
①查找左边界
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再除,是因为最下面的条件mid-1,如果没有-1,那么上面也不需要+1,这样做是为了防止进入死循环,这个模板仅供参考,切勿死记硬背,实践出真知,适合自己的才最重要。
题目链接:https://leetcode.cn/problems/binary-search/
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
思路
这里就是最基础的朴素二分,我们直接写上去就可以了,不熟悉二分最好自己动手写一遍,对后面的二分题就会有更深入的了解,打牢基础。
left
和 right
分别指向数组的起始位置和结束位置。while
循环中,通过计算中间位置 mid
(避免整数溢出的写法),对比中间元素与目标元素的大小关系,从而缩小查找范围。
nums[mid] > target
,说明目标元素可能在左半部分,更新 right = mid - 1
。nums[mid] < target
,说明目标元素可能在右半部分,更新 left = mid + 1
。nums[mid] == target
,找到目标元素,返回 mid
。left
大于 right
,此时查找范围为空,返回 -1 表示目标元素不存在。代码
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0,right=nums.size()-1;
while(left<=right){
int mid = left+(right-left)/2;
if(nums[mid]>target) right=mid-1;
else if(nums[mid]<target) left=mid+1;
else return mid;
}
return -1;
}
};
题目链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
思路
这里题目指定时间复杂度为 O(log n)
的算法,说明就是要让我们使用二分的算法,这里需要注意两个问题,第一个是边界问题,避免死循环,第二就是这里找两个值,我们可以分成两个二分来找,这样我们可以将逻辑捋清楚,这里模板中左边界与有边界两个版本都能用上。
target
的元素的位置。这个过程通过维护两个指针 left
和 right
,在循环中不断缩小查找范围,最终 left
的位置就是目标元素的起始位置。nums[left]
不等于 target
,说明目标元素不存在,返回 {-1, -1}。begin
,则重置 left
和 right
,利用二分查找找到目标元素的结束位置。在这一步中,找到第一个大于 target
的位置的前一个位置,即 right
。代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(!nums.size()) return {-1,-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};
int begin=left;
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;
}
return {begin,right};
}
};
题目链接:https://leetcode.cn/problems/search-insert-position/
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
思路
和上一题一样,题目指定时间复杂度为 O(log n)
的算法,说明就是要让我们使用二分的算法,这里我们直接使用左边界二分即可,注意边界问题,比如target值大于最后一个值,需要考虑。
left
和 right
分别指向数组的起始位置和结束位置。while
循环中,通过计算中间位置 mid
(避免整数溢出的写法),对比中间元素与目标元素的大小关系,从而缩小查找范围。
nums[mid] < target
,说明目标元素可能在右半部分,更新 left = mid + 1
。nums[mid] >= target
,说明目标元素可能在左半部分,更新 right = mid
。left >= right
,此时 left
就是插入位置,因为 left
所指的元素是第一个大于或等于 target
的元素。nums[left]
与 target
的关系:
nums[left] < target
,则插入位置在 left + 1
。left
。代码
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
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 left+1;
return left;
}
};
题目链接:https://leetcode.cn/problems/sqrtx/
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
思路
这里我们使用二分是在暴力循环的基础上改进而来的,需要注意的是边界问题和溢出问题
x
的值,如果小于 1,则直接返回 0,因为在非负整数范围内不存在小于 1 的平方根。left
和 right
,分别指向 1 和 x
。while
循环中,通过计算中间位置 mid
(使用 (right - left + 1) / 2
避免整数溢出),对比中间元素的平方与 x
的大小关系,从而缩小查找范围。
mid * mid <= x
,说明目标平方根可能在右半部分,更新 left = mid
。mid * mid > x
,说明目标平方根可能在左半部分,更新 right = mid - 1
。left >= right
,此时 left
或 right
就是平方根的整数部分。right
。代码
class Solution {
public:
int mySqrt(int x) {
if(x<1) return 0;
int 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 right;
}
};
题目链接:https://leetcode.cn/problems/peak-index-in-a-mountain-array/
符合下列属性的数组 arr
称为 山脉数组 :
arr.length >= 3
i(0 < i < arr.length - 1)
使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给你由整数组成的山脉数组 arr
,返回满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
的下标 i
。
你必须设计并实现时间复杂度为 O(log(n))
的解决方案。
示例 1:
输入:arr = [0,1,0]
输出:1
示例 2:
输入:arr = [0,2,1,0]
输出:1
示例 3:
输入:arr = [0,10,5,2]
输出:1
思路
看到这题很多并不会想到使用二分去做,因为概念里写的使用二分一定要有序,但是这里是可以使用二分的思想来做这道题的,所以我们要将思路打开,根据题意这里山顶不会是左右的边界值,所以这里我们只要使用二分找出数组的最大值就行了
left
和 right
,分别指向二分的起始位置和结束位置(边界不需要考虑)。while
循环中,通过计算中间位置 mid
(使用 (right - left) / 2
避免整数溢出),对比中间元素与其右侧元素的大小关系,从而缩小查找范围。
arr[mid] < arr[mid + 1]
,说明峰值可能在右半部分,更新 left = mid + 1
。arr[mid] >= arr[mid + 1]
,说明峰值可能在左半部分,更新 right = mid
。left >= right
,此时 left
或 right
就是山脉数组的峰值。right
。这个算法的核心思想是通过二分查找,在山脉数组中找到峰值的索引。在山脉数组中,峰值是指一个位置左侧的元素严格单调递增,右侧的元素严格单调递减的位置。因此,通过比较中间元素与其右侧元素的大小关系,可以缩小查找范围,最终找到峰值的位置。
代码
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left=1,right=arr.size()-2;
while(left<right){
int mid = left+(right-left)/2;
if(arr[mid]<arr[mid+1]) left=mid+1;
else right=mid;
}
return right;
}
};
题目链接:https://leetcode.cn/problems/find-peak-element/
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞
。
你必须实现时间复杂度为 O(log n)
的算法来解决此问题。
示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
思路
基本和上一题的二分思想是一致的,但是这里需要考虑边界问题,因此不能直接去除首尾。
left
和 right
,分别指向数组的起始位置和结束位置。while
循环中,通过计算中间位置 mid
(使用 (right - left) / 2
避免整数溢出),对比中间元素与其右侧元素的大小关系,从而缩小查找范围。
nums[mid] < nums[mid + 1]
,说明峰值可能在右半部分,更新 left = mid + 1
。nums[mid] >= nums[mid + 1]
,说明峰值可能在左半部分,更新 right = mid
。left >= right
,此时 left
或 right
就是无序数组的峰值。left
。代码
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;
}
};
题目链接:https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/
已知一个长度为 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)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
思路
这里我们使用二分法使用中间值与最右边的值相比较,若中间值大,说明最小值一定在右边,反之在左边,这里使用二分法不用考虑数组旋转了几次。
left
和 right
,分别指向数组的起始位置和结束位置。x
,作为旋转的轴。这个轴元素是数组的最小元素。while
循环中,通过计算中间位置 mid
(使用 (right - left) / 2
避免整数溢出),对比中间元素与 x
的大小关系,从而缩小查找范围。
nums[mid] > x
,说明最小元素可能在右半部分,更新 left = mid + 1
。nums[mid] <= x
,说明最小元素可能在左半部分或就是 mid
位置,更新 right = mid
。left >= right
,此时 left
或 right
就是数组中的最小元素所在的位置。nums[left]
。代码
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];
}
};
题目链接:https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/
某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records
。假定仅有一位同学缺席,请返回他的学号。
示例 1:
输入: records = [0,1,2,3,5]
输出: 4
示例 2:
输入: records = [0, 1, 2, 3, 4, 5, 6, 8]
输出: 7
思路
这道题其实有多种解法,我们使用二分解决可以效率非常高,这里只需要注意边界问题,即最大学号缺席。
left
和 right
,分别指向数组的起始位置和结束位置。while
循环中,通过计算中间位置 mid
(使用 (right - left) / 2
避免整数溢出),对比中间元素 records[mid]
与 mid
的大小关系,从而缩小查找范围。
records[mid] == mid
,说明漏考勤的学生可能在右半部分,更新 left = mid + 1
。records[mid] != mid
,说明漏考勤的学生可能在左半部分或就是 mid
位置,更新 right = mid
。left >= right
,此时 left
或 right
就是漏考勤的学生的位置。right == records[right] ? right + 1 : right
。代码
class Solution {
public:
int takeAttendance(vector<int>& records) {
int left=0,right=records.size()-1;
while(left<right){
int mid = left + (right-left)/2;
if(records[mid]==mid) left=mid+1;
else right=mid;
}
return right==records[right]?right+1:right;
}
};