算法题总结之二分搜索

本文详细介绍了二分查找的三种类型:查找完全相等的数、查找第一个不小于目标值的数及其变形,以及查找第一个大于目标值的数及其变形。通过具体的例子和代码演示,阐述了不同场景下的二分查找实现,并提供了若干相关的编程题目作为实践练习。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分搜索小记

二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,博主这里对二分搜索进行了归类:

第一类: 需查找和目标值完全相等的数

这是最简单的一类,也是我们最开始学二分查找法需要解决的问题,比如我们有数组[2, 4, 5, 6, 9],target = 6,那么我们可以写出二分查找法的代码如下:

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) return mid;
        else if (nums[mid] < target) left = mid + 1;
        else right = mid;
    }
    return -1;
}

会返回3,也就是target的在数组中的位置。注意二分查找法的写法并不唯一,主要可以变动地方有四处:

第一处是right的初始化,可以写成 nums.size() 或者 nums.size() - 1

第二处是left和right的关系,可以写成 left < right 或者 left <= right

第三处是更新right的赋值,可以写成 right = mid 或者 right = mid - 1

第四处是最后返回值,可以返回left,right,或right - 1

但是这些不同的写法并不能随机的组合,像博主的那种写法,若right初始化为了nums.size(),那么就必须用left < right,而最后的right的赋值必须用 right = mid。但是如果我们right初始化为 nums.size() - 1,那么就必须用 left <= right,并且right的赋值要写成 right = mid - 1,不然就会出错。所以博主的建议是选择一套自己喜欢的写法,并且记住,实在不行就带简单的例子来一步一步执行,确定正确的写法也行。

第二类: 查找第一个不小于目标值的数,可变形为查找最后一个小于目标值的数

这是比较常见的一类,因为我们要查找的目标值不一定会在数组中出现,也有可能是跟目标值相等的数在数组中并不唯一,而是有多个,那么这种情况下nums[mid] == target这条判断语句就没有必要存在。比如在数组[2, 4, 5, 6, 9]中查找数字3,就会返回数字4的位置;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回第一个数字1的位置。我们可以使用如下代码:

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) left = mid + 1;
        else right = mid;
    }
    return right;
}

最后我们需要返回的位置就是right指针指向的地方。在C++的STL中有专门的查找第一个不小于目标值的数的函数lower_bound,在博主的解法中也会时不时的用到这个函数。但是如果面试的时候人家不让使用内置函数,那么我们只能老老实实写上面这段二分查找的函数。

这一类可以轻松的变形为查找最后一个小于目标值的数,怎么变呢。我们已经找到了第一个不小于目标值的数,那么再往前退一位,返回right - 1,就是最后一个小于目标值的数。

值得一提的是,在数组中没有符合要求的项时,该代码返回num.size()。

第三类: 查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数

这一类也比较常见,尤其是查找第一个大于目标值的数,在C++的STL也有专门的函数upper_bound,这里跟上面的那种情况的写法上很相似,只需要添加一个等号,将之前的 nums[mid] < target 变成 nums[mid] <= target,就这一个小小的变化,其实直接就改变了搜索的方向,使得在数组中有很多跟目标值相同的数字存在的情况下,返回最后一个相同的数字的下一个位置。比如在数组[2, 4, 5, 6, 9]中查找数字3,还是返回数字4的位置,这跟上面那查找方式返回的结果相同,因为数字4在此数组中既是第一个不小于目标值3的数,也是第一个大于目标值3的数,所以make sense;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回坐标5,**通过对比返回的坐标和数组的长度,我们就知道是否存在这样一个大于目标值的数。**参见下面的代码:

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] <= target) left = mid + 1;
        else right = mid;
    }
    return right;
}

这一类可以轻松的变形为查找最后一个不大于目标值的数,怎么变呢。我们已经找到了第一个大于目标值的数,那么再往前退一位,返回right - 1,就是最后一个不大于目标值的数。比如在数组[0, 1, 1, 1, 1]中查找数字1,就会返回最后一个数字1的位置4,这在有些情况下是需要这么做的。

题目练习

求开方

69. Sqrt(x) (Easy)

该题目可以转变为求数组的中最后一个小于等于(不大于)目标值的数,即第三类的变种。

class Solution {
public:
    int mySqrt(int x) {
        if (x <= 1) return x;
        int left = 0, right = x;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (x / mid >= mid) left = mid + 1;
            else right = mid;
        }
        return right - 1;
    }
};
大于给定元素的最小元素

744. Find Smallest Letter Greater Than Target (Easy)

Input:
letters = ["c", "f", "j"]
target = "d"
Output: "f"

Input:
letters = ["c", "f", "j"]
target = "k"
Output: "c"

题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。

第三类问题。

class Solution {
public:
    char nextGreatestLetter(vector<char>& letters, char target) {
        if(target>=letters.back())
            return letters.front();
        int left=0,right=letters.size();
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(letters[mid]<=target)
                left=mid+1;
            else
                right=mid;  
        }
        return letters[right];
    }
};
有序数组的 Single Element

540. Single Element in a Sorted Array (Medium)

Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
Output: 2

题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。

观察题目可知偶数情况下:在index>=target时,index和index+1的数不相等。在index<target时,index和index+1的数相等。

class Solution {
public:
    int singleNonDuplicate(vector<int>& nums) {
        int left=0,right=nums.size()-1;
        int n=nums.size();
        while(left<right)    //left<right确保了mid+1不会越界,因为mid一定小于right
        {
            int mid=left+(right-left)/2;
            if(mid%2) --mid; //++mid的话下一行的mid+1可能出界。
            if(nums[mid]==nums[mid+1])
                left=mid+2;
            else
                right=mid;
        }
        return nums[right];
    }
};
第一个错误的版本

278. First Bad Version (Easy)

题目描述:给定一个元素 n 代表有 [1, 2, …, n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。

// Forward declaration of isBadVersion API.
bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int left=1,right=n;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(isBadVersion(mid))
                right=mid;
            else
                left=mid+1;
        }
        return right;        
    }
};
旋转数组的最小数字

153. Find Minimum in Rotated Sorted Array (Medium)

Input: [3,4,5,1,2],
Output: 1
class Solution {
public:
    int findMin(vector<int>& nums) {
        int left=0,right=nums.size()-1;
        if(nums[left]<=nums[right])
            return nums[left];
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<nums[right])
                right=mid;
            else
                left=mid+1;       
        }
        return nums[right];
    }
    
};

旋转数组的最小数字II

154. Find Minimum in Rotated Sorted Array II

和前一题相比,数字可重复。在碰到mid和要比较的数字相等时,去掉一个重复数字,对结果无影响,最坏情况时间复杂度为O(n),比如数组所有元素相同。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left=0,right=nums.size()-1;
        if(nums[0]<nums.back())
            return nums[0];
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<nums[right])
                right=mid;
            else if(nums[mid]>nums[right])
                left=mid+1;
            else
                --right;
        }
        return nums[right];
    }
};
查找区间

34. Search for a Range (Medium)

Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]

Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]

先用二分查找找第一个数,再找最后一个数。第一个二分查找找数组中第一个大于等于目标的数,第二个二分查找找数组中最后一个不大于目标的数(也即第一个大于目标的数减一)。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> res(2, -1);
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        if (right==nums.size()||nums[right] != target) return res;//条件一判断是否这个数组里所有的数字都比目标大,第二个条件判断是否数组中第一个大于等于目标的数不等于目标。
        res[0] = right;
        left=0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) left = mid + 1;
            else right= mid;
        }//由于上面的if已经保证了一定有目标值在给定数组中,这里不用判断了,值得一提的事。
        res[1] = right - 1;
        return res;
    }
};
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值