【刷题(13)】二分查找

一、二分查找基础

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

(1)int mid = ((right - left) >> 1) + left;
(2)lower_bound的底层实现

int lower_bound(vector<int>& nums, int x) 
{
	int left = 0;
	int right = nums.size() - 1;
    // 区间为 左闭右闭
	while (left <= right) {
		int mid = left +(right - left) / 2;
		if (x > nums[mid]) {
			left = mid + 1;
		}
		else {
			right = mid - 1;	
		}
	}
	return left;
}

upper_bound用法和上面类似。只是把lower_bound的 【大于等于】 换成 【大于】 。仿函数等等全是相同的用法
底层实现

int upper_bound(vector<int>& nums, int x) {
	int left = 0;
	int right = nums.size() - 1;
 
	while (left <= right) {
		int mid = left +(right - left) / 2;
		if (x >= nums[mid]) {       //这里是大于等于
			left = mid + 1;
		}
		else {
			right = mid - 1;	
		}
	}
	return left;
}

(3)binary_search()实现

template <class ForwardIterator, class T>
bool binary_search (ForwardIterator first, ForwardIterator last, const T& val)
{
    first = std::lower_bound(first,last,val);
    return (first!=last && !(val<*first));
}

(5)二分查找

int search(vector<int>& nums, int target){
        // 二分查找区间[left, right),初始为整个区间
        int left = 0;   
        int right = nums.size();
        // 找到首个大于target的值
        while(left < right){
            int mid = left + ((right - left) >> 1);
            if(nums[mid] > target){
                right = mid;    // 找到一个大于target的值,暂存并在左半区间继续查找
            }else{
                left = mid + 1; // 没有找到大于target的值,在右半区间继续查找
            }
        }
        return right;
    }

二、35. 搜索插入位置

1 题目

在这里插入图片描述

2 解题思路

(1)由于是排序数组,所以可以使用二分法来进行目标值查找
(2)假设题意是叫你在排序数组中寻找是否存在一个目标值,那么训练有素的读者肯定立马就能想到利用二分法在 O(log⁡n)的时间内找到是否存在目标值。但这题还多了个额外的条件,即如果不存在数组中的时候需要返回按顺序插入的位置,那我们还能用二分法么?答案是可以的,我们只需要稍作修改即可。

考虑这个插入的位置 pos,它成立的条件为:

nums[pos−1]<target≤nums[pos]
其中 nums 代表排序数组。由于如果存在这个目标值,我们返回的索引也是 pos,因此我们可以将两个条件合并得出最后的目标:「在一个有序数组中找第一个大于等于 target的下标」。

问题转化到这里,直接套用二分法即可,即不断用二分法逼近查找第一个大于等于 target的下标 。下文给出的代码是笔者习惯的二分写法,ans 初值设置为数组长度可以省略边界条件的判断,因为存在一种情况是 target 大于数组中的所有数,此时需要插入到数组长度的位置。

3 code

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n=nums.size();
        int left=0;
        int right=n-1;
        int ans=n;

        while(left<=right)
        {
            // >>1相当于/2
            int mid= left+((right-left)>>1);

            //移动逻辑
            if(target<=nums[mid])
            {
                ans=mid;
                right=mid-1;
            }
            else
            {
                left=mid+1;
            }
        }
        return ans;

    }
};

三、74. 搜索二维矩阵

1 题目

在这里插入图片描述

2 解题思路

(1)由于每行的第一个元素大于前一行的最后一个元素,且每行元素是升序的,所以每行的第一个元素大于前一行的第一个元素,因此矩阵第一列的元素是升序的。
(2)我们可以对矩阵的第一列的元素二分查找,找到最后一个不大于目标值的元素,然后在该元素所在行中二分查找目标值是否存在。

3 code

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) 
    {
        // 我们可以对矩阵的第一列的元素二分查找,找到最后一个不大于目标值的元素
        auto row=upper_bound(matrix.begin(),matrix.end(),target,[](const int b, const vector<int>&a)
        {
            return b<a[0];
        });

        if(row==matrix.begin())
        {
            return false;
        }

        --row;
        
        //然后在该元素所在行中二分查找目标值是否存在。
        return binary_search(row->begin(),row->end(),target);

    }
};

四、34. 在排序数组中查找元素的第一个和最后一个位置

1 题目

在这里插入图片描述

2 解题思路

在这里插入图片描述

3 code

class Solution {
private:
    /**
     * @brief 返回首个大于target的元素索引,如果不存在,返回数组长度n
     * @param nums: 输入数组
     * @param target: 目标值
     * @return: 目标值索引
    */
    int search(vector<int>& nums, int target){
        // 二分查找区间[left, right),初始为整个区间
        int left = 0;   
        int right = nums.size();
        // 找到首个大于target的值
        while(left < right){
            int mid = left + ((right - left) >> 1);
            if(nums[mid] > target){
                right = mid;    // 找到一个大于target的值,暂存并在左半区间继续查找
            }else{
                left = mid + 1; // 没有找到大于target的值,在右半区间继续查找
            }
        }
        return right;
    }
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        // 首个target如果存在,一定是首个大于target-1的元素
        int start = search(nums, target - 1);
        if(start == nums.size() || nums[start] != target){
            return {-1, -1};    // 首个target不存在,即数组中不包含target
        }
        // 找到首个大于target的元素,最后一个target一定是其前一位
        int end = search(nums, target);
        return {start, end - 1};
    }
};

五、33. 搜索旋转排序数组

1 题目

在这里插入图片描述

2 解题思路

这道题要在一个旋转了的有序数组中搜索目标值,要求时间复杂度为 O(logn)。这个时间复杂度只能首先尝试二分查找,但是二分查找的前提是数组有序,这个数组并不满足,还可以用吗?

先别急,虽然这个数组不是整体有序,但它是局部有序的,我们尝试二分着去做看看会发生什么。

二分每次都需要取中点 mid,对于这个旋转的有序数组:

  • 如果当前区间 [left, right) 分别在两端有序区间之内,那么就按二分查找去做即可。
  • 如果当前区间 [left, right) 是跨越了两端有序子区间的,那么中间点 mid 总会把当前区间 [left, right] 分成两段,一段是有序的,一段是无序的:

(1)如果 nums[mid] > nums[left],肯定是左半区间有序;

(2)如果 nums[mid] < nums[right-1],肯定是右半区间有序;【之所以 -1,是因为 right 初始为数组长度 n,直接取 right 会导致越界】
在这里插入图片描述
二分的策略还是一样的,二分的关键是要判断 target 落在哪个区间。我们只能取有序的那个区间来比较,因为只有区间有序,才能 通过端点值的大小比较判断是否落入对应的区间

在这里插入图片描述
因此我们只要能够每次判断目标值落到哪个区间,就可以通过二分排除另一半的区间,并不一定要求必须整个数组有序。
在这里插入图片描述

3 code

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;              // 二分查找左边界(左闭)
        int right = nums.size();    // 二分查找右边界(右开)
        while(left < right){
            int mid = left + ((right - left) >> 1);   
            if(nums[mid] == target){
                // 找到目标值,直接返回索引
                return mid;
            }
            if(nums[left] < nums[mid]){
                // 左半区间有序
                if(nums[left] <= target && target < nums[mid]){
                    right = mid;    // 目标值落入左半区间,更新右边界
                }else{  
                    left = mid + 1; // 否则在右半区间查找
                }
            }else{
                // 右半区间有序
                if(nums[mid] < target && target <= nums[right -1]){
                    left = mid + 1; // 目标值落入右半区间,更新左边界
                }else{
                    right = mid;    // 否则在左半区间查找
                }
            }
        }
        return -1;  // 如果退出循环,说明没有找到目标值,返回-1
    }
};

六、153. 寻找旋转排序数组中的最小值

1 题目

在这里插入图片描述

2 解题思路

这道题就是要在旋转后的数组找到最小值。不管旋转了多少次【除了旋转变回原数组】,都可以是分成两段有序数组,并且满足 nums[0] > nums[n - 1]。

我们要找到最小值,并且对数级别的时间复杂度,考虑用二分查找。二分查找的关键是 每次应该选择哪半个区间继续查找,或者说目标值落入了哪半个区间。
在这里插入图片描述
即使数组旋转回原数组的形式也一样适用:
在这里插入图片描述
根据 二分查找,每次找到一个更小的数就记录下来,当区间不断逼近最小值时,记录的结果就是最小值。
在这里插入图片描述

3 code

class Solution 
{
public:
    int findMin(vector<int>& nums) 
    {
        //初始化二分查找区间[0,n)
        int n = nums.size();
        int left=0;
        int right=n;

        //二分查找,根据nums[mid]的情况判断最小值在哪个半区间
        while(left<right)
        {
            int mid=left+((right-left)>>1);

            if(nums[mid]>nums[n-1])
            {
                //nums[mid]大于最后一个数,最小值在其右半区间
                left=mid+1;
            }
            else
            {
                //nums[mid]小于最后一个数,最小值在其左半区间
                //可以任务找到一个较小数暂存到ans=mid,ans和right是同步更新的
                right=mid;
            }
        }
        //结果存储在right中
        return nums[right];

    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BILLY BILLY

你的奖励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值