基本的二分法的架构如下代码块所示:
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left <= right) {
int mid = left + (right - left) / 2;//这与(left + right) / 2 的结果相同,只是为了防止太大导致溢出
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = mid + 1;//注意,middle已经寻找过了,为此要避开
} else if (nums[mid] > target) {
right = mid - 1;//注意,middle已经寻找过了,为此要避开
}
}
return ...;
}
1.寻找一个数
最直接的解法用find函数,时间复杂度应该是O(N),空间复杂度为O(1)
class Solution {
public:
int search(vector<int>& nums, int target) {
auto result=find(nums.begin(),nums.end(),target);
if (result<nums.end())
return result-nums.begin();
return -1;
}
};
二分法:时间复杂度为O(logn),空间复杂度为O(1)
class Solution {
public:
int search(vector<int>& nums, int target) {
//已经排序好的了
int left=0, right=nums.size()-1;
while(left<=right)//注意有等号,由于right的初始化,决定了采用的实际上是闭区间
{
int middle=(left+right)/2;
if(nums[middle]==target)
return middle;
else if(nums[middle]>target)//证明target在左半区
right=middle-1;//注意:由于搜索区间是闭区间,[left, mid-1]
else if(nums[middle]<target)
left=middle+1;//注意,[mid+1, right]
// mid已经搜索过了,不避开会导致超时~
}
return -1;
}
};
若上题while(left<=right)改为while(left<right)则应该是:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0, right =nums.size()-1;
while(left<right)
{
int middle=(left+right)/2;
if(nums[middle]<target)
{
left=middle+1;
}
else if(nums[middle]>target)
{
right=middle-1;
}
else
return middle;
}
return nums[left]==target? left:-1;
}
};
while(left <= right) 的终止条件是 left == right + 1(也就是left>right),写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2], 可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
while(left < right) 的终止条件是 left == right,写成区间的形式就是 [right, right],或者带个具体的数字进去 [2, 2], 这时候区间非空,还有一个数 2,但此时 while 循环终止了。也就是说区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。必须再次检查最后一个索引!
2. 左侧边界与右侧边界的二分法查找
要解决这道题,明确以下三种二分法的思路(这个所谓的思路感觉有点问题~按着做容易出错,还是以上题为架构看如何逼近边界较好)
,时间复杂度: O(logn),空间复杂度:O(1)
class Solution {
public:
int left_bound(vector<int>& nums, int target)
{
int left=0, right=nums.size()-1;
while(left<=right)
{
int mid=(left+right)/2;
int value=nums[mid];
if(value<target)//target应该在右半区
left=mid+1;//中间已经算过了
else if(value>target)//应该在左半区
right=mid-1;//中间已经算过了
else if(value==target)
right=mid-1;//一直递推收紧右侧的边界以锁定左侧边界,
// 下一个中间值必然是大于或等于,因此可以做到锁定一直更新right
}
// 注意要先判断是否有数据越界的情况(由于上面right在找到目标后递推,故此有可能left越界)
if(left<0 || left>=nums.size())
return -1;
// 判断一下 nums[left] 是不是 target(由于上面right在找到目标后递推,故此有可能left不是目标值但是结束了~)
return nums[left] == target ? left : -1;
}
int right_bound(vector<int>& nums, int target)
{
int left=0, right=nums.size()-1;
while(left<=right)
{
int mid=(left+right)/2;
int value=nums[mid];
if(value<target)//target应该在右半区
left=mid+1;//中间已经算过了
else if(value>target)//应该在左半区
right=mid-1;//中间已经算过了
else if(value==target)
left=mid+1;//一直地推左侧以锁定右侧的边界
// 下一个中间值,必然是小于等于,因此一直推直到锁定右边
}
// 确保不要越界
if(right<0 || right>=nums.size())
return -1;
// 判断一下 nums[right] 是不是 target
return nums[right] == target ? right : -1;
}
vector<int> searchRange(vector<int>& nums, int target) {
// 左侧边界与右侧边界的二分法查找
int left=left_bound(nums,target);
if(left==-1)//左边界若不合法,就不用判断右边了~
return vector<int>{-1,-1};
int right=right_bound(nums,target);
return vector<int>{left,right};
}
};
求解立方根
关键点在于要考虑负号以及小数的情况。且浮点数left<=right会无限运行,需要给个小范围的阈值
#include <iomanip>
#include <iostream>
#include <cmath>
using namespace std;
int main() {
double value;
cin>>value;
//二分法
//由于是开立方根,所以存在为负号的情况
//同时需要考虑小数的情况
double left=min(-1.0, value);
double right=max(1.0, value);
double middle;//就是结果
// while(left<=right)//由于是浮点数,为此永远都很接近不会相等
while(abs(right-left)>0.01)//因此给0.01。毕竟小数点保留1位而已(注意是相减大于某个小数)
{
middle=(left+right)/2.0;
double result=pow(middle,3);
if(result<value)
{
left=middle;
}
else if(result>value)
{
right=middle;
}
else//相等
break;
}
std::cout<<fixed<<setprecision(1)<<middle;
}
寻找两个正序数组的中位数
最直接的做法其实就是合并一个数组,然后排序,但是时间复杂度为:O((M+N)Log(M+N))其实并不满足题目要求(虽然可以通过所有测试样例)
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
vector<int> new_group;
for(int i=0;i<nums1.size();i++)
{
new_group.push_back(nums1[i]);
}
for(int i=0;i<nums2.size();i++)
{
new_group.push_back(nums2[i]);
}
// 时间复杂度为:O((M+N)Log(M+N))
sort(new_group.begin(),new_group.end());
if((nums1.size()+nums2.size())%2==0)//能被偶数整除(余数)
return (double)(
new_group[(nums1.size()+nums2.size())/2]+
new_group[(nums1.size()+nums2.size())/2-1]
)/2;
return new_group[(nums1.size()+nums2.size())/2];//奇数的情况返回原值
}
};
,下面的双指针法可以让时间复杂度进一步降低为O(m+n)
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
vector<int> merged;
int i = 0, j = 0;
// 合并两个有序数组
while (i < nums1.size() && j < nums2.size()) {
if (nums1[i] < nums2[j]) {
merged.push_back(nums1[i]);
i++;
} else {
merged.push_back(nums2[j]);
j++;
}
}
// 将剩余元素添加到合并数组
while (i < nums1.size()) {
merged.push_back(nums1[i]);
i++;
}
while (j < nums2.size()) {
merged.push_back(nums2[j]);
j++;
}
// 计算中位数
int n = merged.size();
if (n % 2 == 1) {//为奇数
return merged[n / 2];
} else {//为偶数
return (merged[n / 2 - 1] + merged[n / 2]) / 2.0;
}
}
};
而题目要求O(Log(M+N))实际上就是典型的二分法的时间复杂度。
思路就是在两个有序数组中找到第k 小的元素。其中k就是中位数。(理解得不是很透彻~)
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 确保nums1是较短的数组,以减少二分查找的范围
if (nums1.size() > nums2.size())
swap(nums2, nums1);
int m = nums1.size();
int n = nums2.size();
int k = (m + n + 1) / 2;//第k小的值
int left = 0, right = m;
while (left < right) {
//i和j为nums1 和 nums2 中的分割点
// k为中位数左侧元素的总数
// num1中的分割点
int i = (right +left) / 2;
//num2中的分割点
int j = k - i;//第k小的值,对应1中的i个,那么2中就是k-2个
if (nums1[i] < nums2[j - 1]) {
left = i + 1; // i太小了,需要右移
} else {
right = i; // i太大了,需要左移
}
}
int i = left;
int j = k - i;
int nums1LeftMax = (i == 0 ? INT_MIN : nums1[i - 1]);
int nums1RightMin = (i == m ? INT_MAX : nums1[i]);
int nums2LeftMax = (j == 0 ? INT_MIN : nums2[j - 1]);
int nums2RightMin = (j == n ? INT_MAX : nums2[j]);
// 如果总长度是奇数
if ((m + n) % 2 == 1) {
return std::max(nums1LeftMax, nums2LeftMax);
} else {
// 如果总长度是偶数
return (std::max(nums1LeftMax, nums2LeftMax) + std::min(nums1RightMin, nums2RightMin)) / 2.0;
}
}
};