一、经验总结
何时使用二分查找?
二分查找不只适用于有序数组,只要可以根据某种规律用数组中的某个数将数组划分成规则不同的两个部分,根据规律可以有选择性的舍去一部分,进而在另一个部分中继续查找,就可以使用二分查找。
二分查找的模型?如何选择?
二分查找算法具体分为三大模型:
- 朴素二分查找:所求的元素 t 可以精确找到,将数组划分为大于t,等于t,小于t
- 左端点二分查找:所求的元素 t 是区间的左端点(或是归于右区间),将数组划分为小于t,大于等于t
- 右端点二分查找:所求的元素 t 是区间的右端点(或是归于左区间),将数组划分为小于等于t,大于t
如何使用二分查找?
- 首先根据题意选用合适的模型
- 定义区间端点left = 0, right = n-1;
- 循环二分,注意循环条件
- 计算中点mid,注意偶数个的取中方法
- 判断中点mid处于哪个区间,根据规则移动区间端点,注意区间端点是否跳过mid
- 如果是朴素二分则当arr[mid]==t时,查找成功,返回结果
- 如果是左右端点二分,则循环结束时left==right,相遇位置就是最终结果
- 判断相遇位置的值是不是target,注意特殊情况特殊处理
朴素二分:
循环条件:循环条件是left<=right,因为[left, right]是一个左闭右闭区间,当left==right时,区间内还有一个元素没有判断。
区间端点的移动:无论是left还是right都必须跳过mid,否则可能会发生死循环
- 只剩两个元素要判断时,mid永远取较小的那个(不+1取中),如果恰好target是较大的那个(或者不存在),就会发生死循环。
- 如果target不存在,即使left == right也会陷入死循环。
求中点的操作:加1取中和不加1取中两个版本都可以取,不影响最终结果。
左右端点二分:
- 循环条件:left<right,当left==right时,就是最终结果,不需要再次进入循环判断,也是为了防止死循环。
- 求左端点时,以左端点L为界,将数组划分为左区间<L和右区间>=L。当mid>=target落到右区间时,right不能跳过mid,因为mid可能是左端点L,也正因为right不能跳过mid,mid应该不加1取中,防止死循环
- 循环结束判断相遇位置的值,如果等于target即为左端点,否则target不存在。
- 求右端点也是同样的道理。
二、相关编程题
2.1 二分查找
题目描述
给定⼀个n个元素有序的(升序)整型数组nums和⼀个⽬标值target,写⼀个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。
题目链接
算法原理
循环条件:循环条件是left<=right,因为[left, right]是一个左闭右闭区间,当left==right时,区间内还有一个元素没有判断。
区间端点的移动:无论是left还是right都必须跳过mid,否则可能会发生死循环
- 只剩两个元素要判断时,mid永远取较小的那个(不+1取中),如果恰好target是较大的那个(或者不存在),就会发生死循环。
- 如果target不存在,即使left == right也会陷入死循环。
求中点的操作:加1取中和不加1取中两个版本都可以取,不影响最终结果。
编写代码
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(target > nums[mid])
{
left = mid+1;
}
else if(target < nums[mid])
{
right = mid-1;
}
else
{
return mid;
}
}
return -1;
}
};
2.2 在排序数组中查找元素的第⼀个和最后⼀个位置
题目描述
给你⼀个按照⾮递减顺序排列的整数数组nums,和⼀个⽬标值target。请你找出给定⽬标值在数组中的开始位置和结束位置。
如果数组中不存在目标值target,返回[-1,-1]。
你必须设计并实现时间复杂度为O(log n)的算法解决此问题。
题目链接
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
算法原理
- 循环条件left<right,当left==right时,就是最终结果,不需要再次进入循环判断,也是为了防止死循环。
- 求左端点时,以左端点L为界,将数组划分为左区间<L和右区间>=L。当mid>=target落到右区间时,right不能跳过mid,因为mid可能是左端点L,也正因为right不能跳过mid,mid应该不加1取中,防止死循环
- 循环结束判断相遇位置的值,如果等于target即为左端点,否则target不存在。
- 求右端点也是同样的道理。
编写代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int begin = -1, end = -1;
//处理空数组
if(nums.size() == 0)
{
return {begin, end};
}
//先求左端点
int left = 0, right = nums.size()-1;
while(left < right)
{
// 求左端点right不跳过mid,所以mid不加1取中,防止死循环
int mid = left+(right-left)/2;
if(nums[mid] < target)
{
left = mid+1;
}
else
{
right = mid;
}
}
if(nums[left] != target)
{
return {begin, end};
}
begin = left;
//再求右端点
right = nums.size()-1;
while(left < right)
{
// 求右端点left不跳过mid,所以mid加1取中,防止死循环
int mid = left+(right-left+1)/2;
if(nums[mid] <= target)
{
left = mid;
}
else
{
right = mid-1;
}
}
end = left;
return {begin, end};
}
};
2.3 X的平方根
题目描述
给你⼀个⾮负整数 x ,计算并返回 x 的算术平⽅根。
由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。
注意:不允许使⽤任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
题目链接
算法原理
算法巧记:求mid时,如果下面出现-1(right =mid-1)就采用+1取中法。
编写代码
class Solution {
public:
int mySqrt(int x) {
if(x==0) 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 left;
}
};
2.4 搜索插入位置
题目描述
给定⼀个排序数组和⼀个⽬标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插⼊的位置。
请必须使⽤时间复杂度为 O(log n) 的算法。
题目链接
算法原理
编写代码
// 朴素二分
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)
{
right = mid-1;
}
else if(nums[mid] < target)
{
left = mid+1;
}
else
{
return mid;
}
}
return 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) ++left;
return left;
}
};
2.5 山峰数组的顶峰索引
题目描述
题目链接
算法原理
编写代码
//右端点二分
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 1, right = arr.size()-2; //峰顶不可能使第一个和最后一个位置
while(left < right)
{
int mid = left+(right-left+1)/2;
if(arr[mid] > arr[mid-1]) //和前一个位置进行比较
{
left = mid;
}
else
{
right = mid-1;
}
}
return left;
}
};
//左端点二分
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 left;
}
};
2.6 寻找峰值
题目描述
题目链接
算法原理
编写代码
//左端点二分
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])
{
right = mid;
}
else
{
left = mid+1;
}
}
return left;
}
};
//右端点二分
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0, right = nums.size()-1;
while(left < right)
{
int mid = left+(right-left+1)/2;
if(nums[mid] > nums[mid-1])
{
left = mid;
}
else
{
right = mid-1;
}
}
return left;
}
};
2.7 寻找旋转排序数组中的最小值
题目描述
题目链接
153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
算法原理
编写代码
// 选择数组末尾作为参照物
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size()-1;
int d = nums[right];
while(left < right)
{
int mid = left+(right-left)/2;
if(nums[mid] > d)
{
left = mid+1;
}
else
{
right = mid;
}
}
return nums[left];
}
};
//选择数组开头作为参照物
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size()-1;
int d = nums[left];
while(left < right)
{
int mid = left+(right-left)/2;
if(nums[mid] >= d)
{
left = mid+1;
}
else
{
right = mid;
}
}
if(nums[left]>d) return d; //需要做特殊处理
else return nums[left];
}
};
2.8 0〜n-1 中缺失的数字
题目描述
题目链接
算法原理
编写代码
//和差抵消
class Solution {
public:
int takeAttendance(vector<int>& records) {
int n = records.size();
int ret = n*(n+1)/2;
size_t i = 0;
for(; i < n; ++i)
{
ret -= records[i];
}
return ret;
}
};
//按位异或抵消
class Solution {
public:
int takeAttendance(vector<int>& records) {
int ret = 0;
size_t i = 0;
for(; i < records.size(); ++i)
{
ret ^= records[i];
ret ^= i;
}
ret ^= i;
return ret;
}
};
//二分查找
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;
}
}
if(records[left] == left) ++left;
return left;
}
};