二分查找的提示
- 数组有序、或部分有序
- 查找指定元素、或查找某个满足条件的元素
- 时间复杂度要求 O ( l o g n ) O(logn) O(logn)
class Solution {
public:
int mySqrt(int x) {
// 方法:二分查找
// /* 我的迭代实现V1(不work)
// 思路:二分查找的变体问题,在有序数组中找最后一个mid * mid <= x的mid
// 问题:平方会上溢;
// */
// int low = 0, high = x; // high这里不能是x/2,得是x,不然1会无法通过
// while(low <= high)
// {
// int mid = low + (high - low) / 2; // C++里整除是/而不是//,除以2这样写不会出错
// if(mid * mid <= x) // runtime error: signed integer overflow: 1073697799 * 1073697799 cannot be represented in type 'int'
// {
// if((mid == high) || (mid + 1) * (mid + 1) > x) return mid;
// low = mid + 1;
// }
// else
// {
// high = mid - 1;
// }
// }
// return -1;
/* 我的迭代实现V2(推荐写法1,不容易写错)
思路:二分查找的变体问题,在有序数组中找最后一个mid <= x/mid 的mid
Time:O(logn),执行用时:4 ms, 在所有 C++ 提交中击败了48.33%的用户
Space:O(1),内存消耗:5.8 MB, 在所有 C++ 提交中击败了64.62%的用户
*/
if(x == 0) return 0;
int low = 1, high = x; // high这里不能是x/2,得是x,不然1会无法通过
while(low <= high)
{
int mid = low + (high - low) / 2; // 除以2这样写不会出错
if(mid <= x / mid)
{
if(mid == high || (mid + 1) > x / (mid + 1)) return mid; // mid是最后一个值,或者(mid+1) > x/(mid+1)了
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return -1;
/* leetcode101标准答案(推荐写法2,更快一些):
1. 将求平方,转换为求整除
2. 通过整除将二分查找的变体问题,转换为标准的二分查找问题:找的是mid == x/mid的mid;但若无解,返回的不是-1,而是较小值(退出循环后,较小值是high)!!!
3. 考虑边界条件,把0单独处理,避免除以0
*/
if(x == 0) return 0;
int low = 1, high = x;
while(low <= high)
{
int mid = low + (high - low) / 2;
int target = x / mid;
if(mid == target) return mid;
else if(mid > target) high = mid - 1; // 注意这里是>,就单纯把x/mid当作一个目标值target,不要考虑target里的mid
else low = mid + 1;
}
return high; // 不是-1,因为这道题永远有解
/* 牛顿迭代法
牛顿迭代法是找f(x)=0的近似最优解x的方法,方法是迭代求解x(n+1) = x(n) - f(x(n))/f'(x(n));
这道题对应的f(x) = x^2 - C,递推公式x(n+1) = (x(n) + C/x(n)) / 2。
Time: O(logn)
Space: O(1)
牛顿迭代法的详细介绍见本题官方题解:https://leetcode.cn/problems/sqrtx/solution/x-de-ping-fang-gen-by-leetcode-solution/
*/
if(x == 0) return 0;
long res = x; // 看详细介绍,初始化为C
// unsigned int表示范围是[0, 2^32-1]
// int表示范围是[-2^31, 2^31-1]
// 因为下面有res + xxx的操作,为了防止res超出int的表示范围,所以得定义为long类型
while(res > x / res)
{
res = (res + x / res) / 2; // 看详细介绍,是x(n+1)是在x(n)的左边
}
return res;
}
};
class Solution {
public:
/* 我的实现V1,不work,问题:把找left和找right写在一起、没有办法return中断while,导致nums={1} target=1 时,改不了low和high,跳不出循环 */
// vector<int> searchRange(vector<int>& nums, int target) {
// // 方法:二分查找
// /* 我的实现:
// 1. 二分查找的变体问题:在有序数组中找出第一个和最后一个等于指定值的位置
// 2. 要留意的错误:low、high是下标,比较大小时是nums[low]和nums[high]
// */
// int low = 0, high = nums.size() - 1, mid;
// int left = -1, right = -1;
// while(low <= high)
// {
// mid = low + (high - low) / 2;
// if(nums[mid] == target)
// {
// if(mid == 0 || nums[mid - 1] < target) left = mid;
// else high = mid - 1; // 从左数不是第一个
// if(mid == nums.size() - 1 || nums[mid + 1] > target) right = mid;
// else low = mid + 1; // 从右数不是第一个
// }
// else if(nums[mid] > target)
// {
// high = mid - 1;
// }
// else
// {
// low = mid + 1;
// }
// }
// return vector<int> {left, right};
// }
int searchRangeLeft(vector<int>& nums, int target)
{
int low = 0, high = nums.size() - 1, mid, left = -1;
while(low <= high)
{
mid = low + (high - low) / 2;
if(nums[mid] == target)
{
if(mid == 0 || nums[mid - 1] < target)
{
left = mid;
return left;
}
else high = mid - 1; // 从左数不是第一个
}
else if(nums[mid] > target)
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
int searchRangeRight(vector<int>& nums, int target) {
int low = 0, high = nums.size() - 1, mid, right = -1;
while(low <= high)
{
mid = low + (high - low) / 2;
if(nums[mid] == target)
{
if(mid == nums.size() - 1 || nums[mid + 1] > target)
{
right = mid;
return right;
}
else low = mid + 1; // 从右数不是第一个
}
else if(nums[mid] > target)
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
vector<int> searchRange(vector<int>& nums, int target) {
// 方法:二分查找
/* 我的实现:
1. 二分查找的变体问题:在有序数组中找出第一个和最后一个等于指定值的位置
2. 要留意的错误:low、high是下标,比较大小时是nums[low]和nums[high]
Time: O(logn)
Space: O(1)
*/
int left = searchRangeLeft(nums, target);
int right = searchRangeRight(nums, target);
return vector<int> {left, right};
}
};
class Solution {
public:
int search(vector<int>& nums, int target) {
// 二分查找
/* 旋转数组
1. 旋转数组的特性:
- 以数组中间点为分区,会将数组分成一个有序数组和一个旋转数组
- 如果nums[mid]>=nums[left],说明前半部分是有序的,后半部分是旋转数组;如果nums[mid]<nums[left],说明后半部分是有序的,前半部分是旋转数组
- 判断目标元素是否有序数组的两端范围内,在的话收缩边界到有序数组,不在的话收缩边界到另一半旋转数组范围内
*/
// 第二遍写
if(nums.size() == 0) return -1;
int l = 0, r = nums.size() - 1, mid;
while(l <= r)
{
mid = l + (r - l) / 2;
if(nums[mid] == target) return mid;
if(nums[mid] >= nums[l]) // 等号在判断左侧有序这边,因为整除的原因mid有可能=0
{
if(nums[l] <= target && target < nums[mid]) // 在有序范围内
{
r = mid - 1;
}
else
{
l = mid + 1;
}
}
else // 右侧有序
{
if(nums[mid] < target && target <= nums[r]) // 在有序范围内
{
l = mid + 1;
}
else // 不在有序范围内
{
r = mid - 1;
}
}
}
return -1;
// // 第一遍写
// if(nums.size() == 0) return -1;
// int left = 0, right = nums.size() - 1, mid;
// while(left <= right)
// {
// mid = left + (right - left) / 2;
// if(nums[mid] == target) return mid;
// // 判断左半有序还是右半有序,如果nums[0] <= nums[mid],则左半有序、右半旋转
// if(nums[0] <= nums[mid]) // 左半有序,虽然无重复元素也得是<=,不能是<,因为有可能mid = 0
// {
// // 确认target是否在有序区间的数值范围内
// if(nums[left] <= target && target < nums[mid]) // 注意是<=,不是<;nums[left]也可以换nums[0]
// {
// // 可能在有序区间
// right = mid - 1;
// }
// else
// {
// // 可能在无序区间
// left = mid + 1;
// }
// }
// else // 右半有序
// {
// // 确认target是否在有序区间的数值范围内
// if(nums[mid] < target && target <= nums[right]) // // 注意是>=,不是>;nums[right]也可以换nums[nums.size()-1]
// {
// // 可能在有序区间
// left = mid + 1;
// }
// else
// {
// // 可能在无序区间
// right = mid - 1;
// }
// }
// }
// return -1;
}
};
class Solution {
public:
bool search(vector<int>& nums, int target) {
// 二分查找
/* 旋转数组
1. 无重复元素的旋转数组查找元素见leetcode 33
2. 有重复元素的旋转数组查找元素,不同的是:nums[mid] >= nums[left]无法保证左侧有序,这时需要将left++
*/
if(nums.size() == 0) return false;
int left = 0, right = nums.size() - 1, mid;
while(left <= right)
{
mid = left + (right - left) / 2;
if(nums[mid] == target) return true;
if(nums[mid] == nums[left]) ++left; // 旋转数组中存在重复元素时,无法判断左侧有序还是右侧有序,将左端点右移一位再来判断
else if(nums[mid] > nums[left]) // 左侧有序
{
if(nums[left] <= target && target < nums[mid])
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
else // 右侧有序
{
if(nums[mid] < target && target <= nums[right])
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
}
return false;
}
};
class Solution {
public:
int findMin(vector<int>& nums) {
// 二分查找
/* 无重复元素的旋转数组查找最小值
1. 如何确定哪半是排序数组、哪半是旋转数组,方法同leetcode 33
2. 如何找最小值:先将最小值定为有序区间的左端点,再继续搜旋转数组区间看有没有更小的
*/
int l = 0, r = nums.size() - 1, mid, res = 5001;
while(l <= r)
{
mid = l + (r - l) / 2;
if(nums[mid] >= nums[l]) // 左侧有序
{
res = min(res, nums[l]); // 把最小值设为有序区间的最小值
// 继续搜旋转区间
l = mid + 1;
}
else // 右侧有序
{
res = min(res, nums[mid]); // 把最小值设为有序区间的最小值
// 继续搜旋转区间
r = mid - 1;
}
}
return res;
}
};
class Solution {
public:
int findMin(vector<int>& nums) {
// 二分查找
/* 有重复元素的旋转数组查找最小值
注意:要给res赋初始值、nums[mid] == nums[l]时也要给res赋值
*/
int l = 0, r = nums.size() - 1, mid, res = 5001; // res要预先给一个较大值
while(l <= r)
{
mid = l + (r - l) / 2;
if(nums[mid] == nums[l])
{
res = min(res, nums[l]); // 这种情况下也要给res赋值!!!不然输入[1]时输出5001
++l;
}
else if(nums[mid] > nums[l]) // 左侧是有序数组
{
res = min(res, nums[l]);
l = mid + 1;
}
else // 右侧是有序数组
{
res = min(res, nums[mid]);
r = mid - 1;
}
}
return res;
}
};
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
// 二分查找
// // 第一次自己实现,不work,会逐渐细分条件,其实是没想清楚
// if(nums.size() == 1) return nums[0]; // 边界条件
// int l = 0, r = nums.size() - 1, mid;
// while(l <= r)
// {
// mid = l + (r - l) / 2;
// if (mid % 2 == 0 && (mid == 0 || nums[mid] == nums[mid - 1])) r = mid - 2; // 在左侧
// else if (mid % 2 == 1 && (mid == nums.size() - 1 || nums[mid] == nums[mid + 1])) r = mid - 1; // 在左侧
// else if (mid % 2 == 1 && (mid == 0 || nums[mid] == nums[mid - 1])) l = mid + 1; // 在右侧
// else l = mid + 2;
// }
// return -1;
/*
如果每个数字都出现两次,就一定会满足对于所有的(偶数下标,奇数下标)这样的小单元,值都相等。
二分查找确定区间的方法是:对于mid判断(偶数下标,奇数下标)这样的小单元的值是否相等,如果相等,说明单元素不会在左半段,只会在右半段;如果不相等,说明单元素在左半段
*/
int l = 0, r = nums.size() - 1, mid; // 注意在二分查找时,r是要-1的!!!
while(l < r) // r和l缩到同一个位置上的时候,就是那个单元素了
{
mid = l + (r - l) / 2;
if(mid % 2 == 0) // mid是偶数
{
if(nums[mid] == nums[mid + 1]) // 单元素在右侧
{
l = mid + 2; // 跳过双元素的部分,依旧保持所搜索数组满足只有一个单元素的特性
}
else // 单元素在左侧
{
r = mid;
}
}
else // mid是奇数
{
if(nums[mid - 1] == nums[mid]) // 单元素在右侧
{
l = mid + 1;
}
else // 单元素在左侧
{
r = mid;
}
}
}
return nums[r]; // l和r都可
}
};
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 二分查找 官方答案 看懂阶段
// 选较短的数组来分割,另一个数组的分割由此决定
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.size();
int n = nums2.size();
int left = 0, right = m;
// median1:前一部分的最大值
// median2:后一部分的最小值
int median1 = 0, median2 = 0;
while (left <= right) {
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i = (left + right) / 2;
int j = (m + n + 1) / 2 - i; // 两个数组的中位数是两个数组从小到大排序后的第k = (m + n + 1) / 2 个数的位置上,这对奇偶长度的都成立???
// nums_im1, nums_i, nums_jm1, nums_j 分别表示nums1左段最大值nums1[i-1], nums1右段最小值nums1[i], nums2左段最大值nums2[j-1], nums2右段最小值nums2[j]
int nums_im1 = (i == 0 ? INT_MIN : nums1[i - 1]);
int nums_i = (i == m ? INT_MAX : nums1[i]);
int nums_jm1 = (j == 0 ? INT_MIN : nums2[j - 1]);
int nums_j = (j == n ? INT_MAX : nums2[j]);
/* 找到中位数的条件是:nums_im1 <= nums_j && nums_jm1 <= nums_i
所以:
1) 如果nums_im1 <= nums_j,可以对于nums1继续向右找一下
2) 如果nums_im1 > nums_j,可以对于nums继续向左找一下
直到left > right
*/
if (nums_im1 <= nums_j) {
median1 = max(nums_im1, nums_jm1);
median2 = min(nums_i, nums_j);
left = i + 1;
} else {
right = i - 1;
}
}
// 中位数是什么要看nums长度的奇偶:如果是奇数,median就是左侧最大;如果是偶数,就是左侧最大和右侧最小的平均?
return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
}
};
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
/* 二分查找(的思想)
1. 先找到中间的数,通过跟target比对,排除一部分不可能的解,缩小查找范围。然而在本题中,如果同时找行、列中间位置,是不好排除的。所以要思考中间位置是什么,能排除的是什么
2. 可以看出,矩阵的左下角or右上角元素是中间元素,通过与target比对,可以排除一行或一列。例如对于左下角元素x:
(1)如果x==target, return true
(2)如果x<target, 列+1
(3)如果x>target, 行-1
*/
if(matrix.empty() || matrix[0].empty()) return false;
int i = matrix.size() - 1, j = 0; // 从左下角元素开始找
while(i >= 0 && j < matrix[0].size())
{
if(matrix[i][j] == target) return true;
if(matrix[i][j] < target) ++j;
else --i;
}
return false;
}
};