C++算法:二分查找

二分查找

要求数据有序 或有一定规律 。解题时,需要找到二段性,即找到一个规律把数组分成两部分。

1.例题

暴力解法需要遍历整个数组,时间复杂度是O(N),二分查找算法则给一个值x,如果目标值target<x,那么x及x往后的值都可以不再考虑,这样就大大提高了效率。

一个数组,中间位置元素的下标为mid。left=0,right=nums.size()。即mid=(left+right)/2,为了防止left+right溢出,mid=left+(right-left)/2 。当nums[mid]比target大,left=mid+1,只考虑此时[left,right];当nums[mid]比target小,right=mid-1;当nums[mid]==target时,就找到了target,返回下标。当left>right,循环结束。

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target)
    {
        int left = 0;
        int right = nums.size() - 1;
        int mid = left + (right-left)/2 ;
        while (left <= right)
        {
            if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid - 1;
            else
                return mid;
            mid = (left + right) / 2;
        }
        return -1;
    }
};

设有N个数据,找1次后剩N/2个数据,找2次后剩N/4个数据,找3次后剩N/8个数据......一共找了x次,只剩1个数据,即1=N/2x ,即x = log2N,所以时间复杂度时O(logN) 。

普通二分模板:

cpp 复制代码
while(left<=right)
{
    int mid=left+(right-left)/2;
    if(...)
        left=mid+1;
    else if(...)
        right=mid-1;
    else
        return ...;
}

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

非递减顺序排列的整数数组即数组升序,可能有重复值,题目要求返回重复元素的起始和终止位置。二分查找算法中的"二分"可以理解为把数据按照某种规则分成两部分,即二段性 。本题可以把数组分为大于target和小于target两部分

查找区间左端点,把数组分为小于target和大于等于target两部分,mid=left+(right-left)/2。如果nums[mid]<target,left=mid+1,继续在[left,right]里查找;如果nums[mid]>=target,right=mid,继续在[left,right]里查找。right不能为mid-1,如果[left,right]中只有一个元素,即right==left,right=mid-1则会导致left>right。

在如图所示的场景下,最后mid=2,nums[mid]=3,left=right=2。如果此时再进入循环,mid为2,nums[mid]>=target,right=mid,再进入循环,这样就成死循环了,所以循环条件是left<right。

求mid,可以left+(right-left)/2,也可以left+(right-left+1)/2 。如果数组长度为奇数,两种方式无区别,如果为偶数,前者求出的是偏左的元素,后者求出的是偏右的元素。在查找区间左端点时,应该用left+(right-left)/2,left+(right-left+1)/2在如下场景会死循环。

查找区间右端点,同样地,把数组分为大于target和小于等于target两部分,如果nums[mid]>target,right=mid-1,继续在[left,right]里查找;如果nums[mid]<=target,left=mid,继续在[left,right]里查找。同理,循环条件是left<right,用left+(right-left+1)/2求mid,如果用left+(right-left)/2会死循环。

cpp 复制代码
class Solution12 {
public:
    vector<int> searchRange(vector<int>& nums, int target)
    {
        if (nums.size() == 0)
            return { -1,-1 };

        int left = 0;
        int right = nums.size() - 1;
        int begin = 0;
        while (left < right)//左端点
        {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target)
                left = mid + 1;
            else
                right = mid;
        }
        if (nums[left] != target)
            return { -1,-1 };
        else
            begin = left;

        left = 0;
        right = nums.size() - 1;
        while (left < right)//右端点
        {
            int mid = left + (right - left + 1) / 2;
            if (nums[mid] > target)
                right = mid - 1;
            else
                left = mid;
        }

        return { begin,right };
    }
};

使用模板前,先根据二段性把数组分成两部分,然后确定要得到哪个部分的哪个端点,套用对应模板。如果不能一次确定用哪个模板,可以两个都试一下

cpp 复制代码
//查找左端点模板
while (left < right)
{
    int mid = left + (right - left) / 2;
    if (...)
    	left = mid + 1;
    else
    	right = mid;
}

//查找右端点模板
while (left < right)
{
    int mid = left + (right - left + 1) / 2;
    if (...)
    	right = mid - 1;
    else
    	left = mid;
}

3.x的平方根

假设求17的算术平方根,12 = 1、22 = 4、32 = 9、42 = 16、52 = 25、62 = 36......17的算术平方根应该介于4到5之间,根据题意返回4。若求16的算术平方根,则直接返回4。在数字1~17中,定义left=1,right=17,如果要求1的算术平方根,则right为1。把1 ~17分为平方后大于17,和平方后小于等于17两部分。我们要在<=17的部分获得右端点,套用二分查找的右端点模板。

mid2 <=17,left=mid;mid2 >17,right=mid-1。应该返回right,因为题目要求相当于向下取整。按照模板,mid=left+(right-left+1)/2,第一次mid=9,right=8;第二次mid=5,right=4;接下来left移动,直到left==right。

cpp 复制代码
class Solution13 {
public:
    int mySqrt(int x)
    {
        if (x < 1)
            return 0;
        int left = 1;
        int right = x;
        while (left < right)
        {
            long long mid = left + (right - left + 1) / 2;
            if (mid * mid > x)
                right = mid - 1;
            else
                left = mid;
        }
        return right;
    }
};

4.搜索插入位置

把nums分为大于等于target,和小于target两部分,因为如果target在nums中,则nums中存在元素等于target;如果target不在nums中,若应该插在 i 位置,则必然nums[i]>target,所以选择大于等于target部分的左端点,套用左端点模板。当nums[mid]<target,left=mid+1;当nums[mid]>=target,right=mid。

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)
                left = mid + 1;
            else
                right = mid;
        }
        if (nums[left] < target)
            return left + 1;
        return left;
    }
};

5.山峰数组的峰顶

如果暴力枚举,那么一个元素比它前一个元素大,说明此时是递增的,当一个元素比它前一个元素小时,前一个元素就是峰值。

用二分查找,把数组分成递增到峰值和递减两部分,要得到的峰值是递增部分的右端点,套用右端点模板,所以mid=left+(right-left+1)/2。如果mid落在递增部分,则arr[mid]>arr[mid-1],left不能到递减部分,所以left=mid;如果mid落在递减部分,则arr[mid]<arr[mid-1],我们要靠right返回递增部分的右端点,所以right=mid-1。

cpp 复制代码
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr)
    {
        int left = 1;
        int right = arr.size() - 2;
        while (left < right)
        {
            int mid = left + (right - left + 1) / 2;
            if (arr[mid] > arr[mid - 1])
                left = mid;
            if (arr[mid] < arr[mid - 1])
                right = mid - 1;
        }
        return right;
    }
};

6.寻找峰值

数组一共有3种情况,一是一直递减,nums[-1]看作负无穷,所以nums[0]为峰值;二是一直递增,nums[nums.size()]看作负无穷,所以nums[nums.size()-1]为峰值;三是先增后减,如果有多个峰值,返回一个即可。

如果[mid,mid+1]是递增的,则mid之后必然存在峰值;如果[mid,mid+1]是递减的,则mid之前必然存在峰值。

把数组分成递增、递减两部分,要得到递减部分的左端点,即峰值,套用左端点模板。mid=left+(right-left)/2。mid在递增部分,left=mid+1;mid在递减部分,right=mid。

cpp 复制代码
class Solution {
public:
    int findPeakElement(vector<int>& nums)
    {
        int left = 0;
        int right = nums.size() - 1;
        while (left < right)
        {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[mid + 1])
                right = mid;
            if (nums[mid] < nums[mid + 1])
                left = mid + 1;
        }
        return left;
    }
};

7.搜索旋转排序数组中的最小值

根据题意,数组经过旋转后,会有两段递增部分,由于nums中元素互不相同,所以一个递增部分的所有元素会大于另一个递增部分的所有元素。如下图所示。

把数组分为这两段递增部分,显然我们要得到元素小的递增部分的左端点,套用左端点模板。mid=left+(right-left)/2。以nums[n-1]为参考点,mid在元素大的递增部分,则nums[mid]>nums[n-1],left=mid+1;mid在元素小的递增部分,则nums[mid]<nums[n-1],right=mid。

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums)
    {
        int left = 0;
        int right = nums.size() - 1;
        int n = nums.size();
        while (left < right)
        {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[n - 1])
                left = mid + 1;
            if (nums[mid] < nums[n - 1])
                right = mid;
        }
        return nums[left];
    }
};

以nums[0]为参考点,则要处理一下边界情况。

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums)
    {
        int left = 0;
        int right = nums.size() - 1;

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

8.0〜n-1 中缺失的数字

这道题可以用哈希表、直接遍历、异或^ 位运算(单身狗)、等差数列求和等方解决,但时间复杂度均为O(N)。

用二分查找,如果 0~i 没有缺席,则元素对应也为 0~i 。把数组分为与下标对应和不对应两部分

显然要得到下标不对应部分的左端点,套用左端点模板。mid=left+(right-left)/2。mid在下标对应部分,则records[mid]==mid,left=mid+1;mid在下标不对应部分,则records[mid] != mid,right=mid。

如果数组为[0,1,2,3],根据题意,缺席的是4,此时要返回left+1。

cpp 复制代码
class Solution {
public:
    int takeAttendance(vector<int>& records)
    {
        int left = 0;
        int right = records.size() - 1;
        while (left < right)
        {
            int mid = left + (right - left) / 2;
            if (records[mid] == mid)
                left = mid + 1;
            else
                right = mid;
        }
        if (records[left] == left)
            return left + 1;
        return left;
    }
};
相关推荐
浅念-1 天前
从LeetCode入门位运算:常见技巧与实战题目全解析
数据结构·数据库·c++·笔记·算法·leetcode·牛客
CoovallyAIHub1 天前
无人机拍叶片→AI找缺陷:CEA-DETR改进RT-DETR做风电叶片表面缺陷检测,mAP50达89.4%
算法·架构·github
CoovallyAIHub1 天前
混合训练反而更差?VLM Agent在训练前协调跨数据集标注,文档布局检测F-score从0.860提升至0.883
算法·架构·github
鸿途优学-UU教育1 天前
教材质量——法考培训的根基与底气
算法
_深海凉_1 天前
LeetCode热题100-最大数(179)
算法·leetcode·职场和发展
剑挑星河月1 天前
763.划分字母区间
数据结构·算法·leetcode
XY_墨莲伊1 天前
【编译原理】实验二:基于有穷自动机FA词法分析器设计与实现
c语言·开发语言·c++·python
小辉同志1 天前
74. 搜索二维矩阵
c++·leetcode·矩阵·二分查找
programhelp_1 天前
Snowflake OA 2026 面经|3道高频真题拆解 + 速通攻略
经验分享·算法·面试·职场和发展
Duang1 天前
AI 真能自己写出整个 Windows 系统吗?我做了一场无监督实验
算法·设计模式·架构