代码框架
核心的是寻找问题的"两段性",以及精准把控让无数人头疼的边界条件(while 里面到底带不带等号,right 到底等不等于 mid)。
cpp
// 基础二分查找框架 (左右闭区间 [left, right])
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
// 因为是闭区间,当 left == right 时区间依然有效,所以用 <=
while (left <= right) {
int mid = left + (right - left) / 2; // 防溢出写法
if (nums[mid] == target) {
return mid; // 找到目标(或者在这里根据题意继续收缩左右边界)
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右侧,更新左边界
} else if (nums[mid] > target) {
right = mid - 1; // 目标在左侧,更新右边界
}
}
return -1; // 没找到或者返回最终所需的边界
}
搜索插入位置
循环结束时,一定是 left = right + 1
此时的 left 正好指向第一个大于 target 的位置,即最终的插入位置
哪怕 target 比所有元素都大,left 最后也会越界停在 nums.size() 的位置
核心都是理解 while 循环结束后 left 和 right 指针到底停在了什么状态。
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
while(left <= right){
int mid = left+(right-left)/2;
if(nums[mid] == target){
return mid;
}else if(nums[mid]<target){
left = mid+1;
}else if(nums[mid]>target){
right = mid-1;
}
}
return left;
}
};
搜索二维矩阵
由于本题的数字大小特殊性
每行中的整数从左到右按非严格递增顺序排列。
每行的第一个整数大于前一行的最后一个整数。
本质上就是一个m*n的一维数组
难点就是找到重点了应该如何去变换成二维数组的坐标
行号:mid / n (看它前面完整排满了多少行)
列号:mid % n (看它在当前行排在第几个)
重点就是下面这一句
cpp
int midValue = matrix[mid / n][mid % n];
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if (matrix.empty() || matrix[0].empty()) return false;
int left = 0;
int m = matrix.size();
int n = matrix[0].size();
int right = m*n-1;
while(left<=right){
int mid = left +(right-left)/2;
int midValue = matrix[mid / n][mid % n];
if (midValue == target){
return true;
}else if(midValue<target){
left = mid+1;
}else if(midValue >target){
right = mid-1;
}
}
return false;
}
};
题外话判断空条件
警报一:想要访问具体索引时(如 nums[0]、matrix[0][0])
如果给你的是个空数组 [],你强行去拿 nums[0],直接越界。
警报二:想要获取嵌套/内部结构的属性时(如二维数组的列数)
就像上一题里的 matrix[0].size()。求矩阵的列数,前提是你得有"第一行"。如果 matrix 本身是空的 [],你试图去调取不存在的 matrix[0] 的大小,程序立刻就炸了。但如果你只是求最外层的 matrix.size(),通常是安全的(空数组会乖乖返回 0)。
警报三:想要解引用指针时(如树和链表问题中的 node->val)
只要你想用箭头 -> 或者点 . 去读取属性,左边的对象绝对不能是 null 或空指针。
在排序数组中查找元素的第一个和最后一个位置
当我们用基础框架找到 nums[mid] == target 时,我们不能再像以前那样直接 return mid 了,因为这个 mid 可能只是连续目标值中间的某一个,并不是我们要找的"开始位置"或"结束位置"。
寻找开始位置(左边界): 当我们发现 nums[mid] == target 时,说明当前找到了目标,但它左边可能还有目标。 先用一个变量把当前这个 mid 记下来,然后绝不手软地砍掉右半边(让 right = mid - 1),继续在左半区间寻找有没有更早出现的目标值。 直到循环结束,我们记录下来的就是最左边的位置。
寻找结束位置(右边界):同理,当我们发现 nums[mid] == target 时,说明当前找到了目标,但它右边可能还有。我们同样先记下当前的 mid,然后砍掉左半边(让 left = mid + 1),继续去右半区间探索有没有更晚出现的目标值。
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()){
return {-1,-1};
}
int first = firstlocal(nums,target);
int last = lastlocal(nums,target);
return {first,last};
}
int firstlocal(vector<int>& nums,int target){
int left = 0;
int right = nums.size()-1;
int firstpos = -1;
while(left<=right){
int mid = left+(right-left)/2;
if(nums[mid] == target){
firstpos = mid;
right = mid-1;
}else if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid+1;
}
}
return firstpos;
}
int lastlocal(vector<int>& nums,int target){
int left = 0;
int right = nums.size()-1;
int lastpos = -1;
while(left<=right){
int mid = left+(right-left)/2;
if(nums[mid] == target){
lastpos = mid;
left = mid+1;
}else if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid+1;
}
}
return lastpos;
}
};
搜索旋转排序数组
如果要去找到旋转点的话,时间复杂度过高,题目要求logn的时间复杂度
二分查找的高阶思维------不一定要全局有序,只要具备"局部有序性",照样能用二分!
核心思想是在修改基础框架的中的if判断操作这一部分
首先切割层两份,一份必定有序,一份可能有序
如果目标出现在有序部分中,则直接使用二分
如果目标出现无序部分中,则继续切割,继续查看目标是否在有序部分中
cpp
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(nums[mid] == target) return mid;
if(nums[left]<=nums[mid]){
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 -1;
}
};
寻找旋转排序数组中的最小值
结合了旋转和找起始位置(记录并收缩)的结合
优化条件,要是整体有序,则直接返回left即可
修改框架中的if判断的条件,现在是判断有序无序
如果左边部分有序,则最小值(断崖)在右半部分,继续判断
如右半部分有序,则最小值,在左半部分。
继续这样分为有序和无序,然后将nums[mid]和minval判断最终得到最小值
cpp
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size()-1;
int minval = INT_MAX;
while(left<=right){
if(nums[left] <= nums[right]){
minval = min(minval,nums[left]);
break;
}
int mid = left+(right-left)/2;
minval = min(minval,nums[mid]);
if(nums[left]<=nums[mid]){
left = mid+1;
}else{
right = mid-1;
}
}
return minval;
}
};
寻找两个正序数组的中位数
- 什么是中位数?中位数的本质是把一个集合划分为长度相等的两个部分,并且左半部分的最大值 ≤\le≤ 右半部分的最小值。
- 既然有两个数组 nums1 和 nums2,我们可以想象在这两个数组中各切一刀,把它们都分成左右两半。
- 把 nums1 的左半边和 nums2 的左半边拼起来,作为总体的"左半部分";把右半边拼起来,作为总体的"右半部分"。
- 核心规律(两段性):假设我们在较短的数组 nums1 中,切在第 iii 个位置(即 nums1 贡献了 iii 个元素给左半部分)。为了保证总体左右两部分长度相等(或者左边多一个),nums2 必须贡献的元素个数 jjj 也就随之确定了:j=m+n+12−ij = \frac{m + n + 1}{2} - ij=2m+n+1−i。
- 二分游走判断:我们拿基础框架去二分寻找这个正确的切口 iii:
- 切口周围有四个关键元素:nums1 切口左边的最大值 L1L_1L1,右边的最小值 R1R_1R1;nums2 切口左边的最大值 L2L_2L2,右边的最小值 R2R_2R2。
- 如果 L1>R2L_1 > R_2L1>R2: 说明我们在 nums1 里的切口太靠右了,给左半部分贡献了太多大元素,得向左边收缩:right = i - 1。
- 如果 L2>R1L_2 > R_1L2>R1: 说明我们在 nums1 里的切口太靠左了,得向右边收缩:left = i + 1。
- 如果交叉比较都满足(L1≤R2L_1 \le R_2L1≤R2 且 L2≤R1L_2 \le R_1L2≤R1): 恭喜你,找到了完美的黄金分割线!直接计算中位数即可。
cpp
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;
int right = m;
while(left<=right){
int i = left + (right-left)/2;
int j = (m+n+1)/2 -i;
int nums1Lmax = (i == 0)? INT_MIN : nums1[i-1];
int nums1Rmin = (i == m) ? INT_MAX : nums1[i];
int nums2Lmax = (j == 0) ? INT_MIN :nums2[j-1];
int nums2Rmin = (j==n)? INT_MAX : nums2[j];
if(nums1Lmax <= nums2Rmin && nums2Lmax <= nums1Rmin){
if((m+n)%2 == 0){
return (max(nums1Lmax,nums2Lmax)+min(nums2Rmin,nums1Rmin)) / 2.0;
}else{
return max(nums1Lmax,nums2Lmax);
}
}else if(nums1Lmax>nums2Rmin){
right = i-1;
}else if(nums2Lmax > nums1Rmin){
left = i+1;
}
}
return 0.0;
}
};