C++算法学习篇5--二分查找法

C++算法刷题--二分查找法

摘要

这篇刷题日记整理二分查找法在 8 道 LeetCode 题目中的实际应用,涵盖从最基础的有序数组找值,到可重复数组找端点、旋转数组找最小值、山脉数组找峰值、缺失数定位等进阶场景。核心难度不在写二分本身,而在于为每道题建立正确的查找边界------即循环不变量与 mid 取值方向如何配合。每题附了解题思路,重点解释指针移动策略、特判条件及其原因。


二分查找法简介

二分查找的核心前提:序列具有有序性(或可证明的单调性)。每次根据中点值与目标的比较,将搜索区间折半,时间复杂度稳定在 O(log n),空间 O(1)。

三个基本要素:

  1. 循环不变量 :保证答案始终在 [left, right] 区间内,这是每次指针移动的依据
  2. mid 的取值方向 :
    • 偏左:mid = left + (right - left) / 2(向下取整)
    • 偏右:mid = left + (right - left + 1) / 2(向上取整)
    • 两者在区间长度为 2 时行为不同,必须与指针移动方式配套使用,否则死循环
  3. 收敛条件 :while(left < right) 收敛到单点 left == right;while(left <= right) 收敛到空区间 left > right

两类典型边界模板:

边界类型 含义 指针移动规则 mid 取值
最左边界 第一个 >= target 的位置 nums[mid] >= target → right = mid,否则 left = mid + 1 偏左
最右边界 最后一个 <= target 的位置 nums[mid] > target → right = mid - 1,否则 left = mid 偏右

关键易错点:

  • 没有精确匹配元素时(LC35、LC69),找的是"插入位置",循环终止后 [left, right] 的实际含义需结合具体不变量判断
  • 山脉数组(LC852):搜索区间取 [1, n-2] 是为了避免与 arr[0] 或 arr[n-1] 比较时越界,且两端不可能为峰顶
  • 旋转数组(LC153):以 nums[right] 为参照值,将"找旋转分界点"转化为普通的最左边界查找

力扣704.二分查找

题解:

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) 
    {
        int left=0,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 right=mid-1;
        }
        if(nums[left]==target)
        return left;
        else return -1;
    }
}

题目解析及解题思路:

题目本质:在有序无重复数组中找 target 的下标。二分查找利用的就是有序性:中点值小于 target,说明 target 只可能在右半边;大于 target 则只可能在左半边,每次把区间砍一半,最终收敛。本题解的查找边界是"找值"型:mid 取偏左,找到就立即 return mid;找不到时用 left=mid+1 / right=mid-1 把中点排除掉;循环结束 left==right 收敛后,还需要补一次判断 numsleft==target,成立返回 left,否则返回 -1------这个"收敛后再校验"的步骤是必要的,防止 target 恰好落在收敛点上被漏掉。时间复杂度 O(log n),空间 O(1)。

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

题解:

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

题目解析及解题思路:

题目要求有序可重复数组中 target 的左右端点。核心思路是两次二分,各找一个查找边界。第一次找"第一个值不小于 target 的位置"(最左边界):numsmid<target 时 left=mid+1,numsmid>=target 时 right=mid,区间收敛到左端点;收敛后若不相等于 target,说明数组里没有 target,直接返回 {-1,-1}。第二次从刚才的左端点出发,找"最后一个值不大于 target 的位置"(最右边界):此时 mid 要取偏右((right-left+1)/2),numsmid>target 时 right=mid-1,否则 left=mid。易错点:左右边界要套两个不同的模板,找左端点时若把 mid 取偏右,区间长度为 2 时左右指针会互跳导致死循环。

力扣35搜索插入位置

题解:

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

题目解析及解题思路:

题目本质是"找数组中第一个值不小于 target 的位置":该位置的值等于 target 就直接插在那,比 target 大就插在它左边一位。查找边界是"最左边界":numsmid<=target 时 left=mid(mid 本身可能就是边界,要保留在区间里),否则 right=mid-1,mid 需取偏右防止死循环。循环结束后 left 就是候选位置:若 numsleft>=target,说明是相等情况,返回 left;否则左边都比 target 小,返回 left+1。这个模式和 C++ STL 的 lower_bound 完全一致,凡是"看似二分、实际可能没有精确相等元素"的题都可以套这个边界。

力扣69.x的平方根

题解:

cpp 复制代码
class Solution {
public:
    int mySqrt(int x) 
    {
        
        if(x==0)
        return 0;

        if(x==1)
        return 1;
        int left=1,right=x;
        long long mid=0;
        while(left<right)
        {
            mid=left+(right-left+1)/2;
            if(mid*mid<=x)
            left=mid;
            else right=mid-1;
        }
        if(mid*mid<=x)
        return mid;
        else return mid-1;
    }
};

题目解析及解题思路:

题目要求找"平方不超过 x 的最大整数",即 midmid<=x 能成立的最大 mid,是典型的"满足条件的位置最右端点"型查找边界。mid 取偏右,满足条件时 left=mid,不满足时 right=mid-1,循环结束后 left 就是最大的满足条件候选。两个坑点:一是 x==0 和 x==1 要直接返回,尤其 x==1 时若不特判,区间为 1,1,循环体一次都不执行,mid 还是初始值 0,最后校验 midmid<=x 会错误地返回 0;二是 x 较大时 mid*mid 可能超出 int 范围,mid 要声明成 long long 防溢出。

力扣852山脉数组的峰顶索引

题解:

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

题目解析及解题思路:

山脉数组"先严格递增再严格递减",峰顶就是递增到递减的分界点。二分找分界:取 mid 看斜率,arrmid>arrmid-1 说明 mid 还在上坡,峰顶在 mid 处或右侧,left=mid;否则 mid 已到达或越过转折,峰顶在 mid 处或左侧,right=mid-1。搜索区间取 1, n-2:一方面要和 arrmid-1 比较,防止 mid=0 越界,另一方面两端保证不是峰顶,可以直接排除。区间收敛后 left 就是峰顶下标。这个"往上走就向右、往下走就向左"的思想也是 LC162 的基础。

力扣162寻找峰值

题解:

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

题目解析及解题思路:

与 LC852 思路相同,但数组不保证是山脉数组------只保证"存在峰值且相邻元素不相等"------而且两端也可能是峰值(题目假设越界处为负无穷)。所以先排除三种特殊情况:只有一个元素直接返回;首元素大于次元素,峰值在 0;末元素大于次末元素,峰值在末尾。剩下在中间区间二分,思想一致:numsmid>numsmid-1 说明还在上坡,峰值必在 mid 或其右侧,left=mid;否则 right=mid-1。区间收敛后 left 就是一个峰值。注意题目只要求找到"一个"峰值而非"最高"的,二分收敛到任意上坡点即可。

力扣153寻找旋转排序数组中的最小值

题解:

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

题目解析及解题思路:

旋转排序数组(无重复)本质上是两个单调区间拼接:某处...最大, 最小...某处。关键是找到一个可靠的参照值,把"找分界"转化为普通的边界查找:本解取 x=numsright(旋转时它是最小值,未旋转时它是最大值),问题变成找"第一个值不大于 x 的位置"。numsmid>x 说明 mid 在左半段,最小值在它右边,left=mid+1;否则 mid 在右半段,最小值在 mid 处或左边,right=mid。区间收敛后 numsleft 就是最小值。用"中点值与右端值比较"这个技巧,无论数组旋转还是没旋转都能正确定位分界点。

力扣LCR 173点名

题解:

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

题目解析及解题思路:

records 是 0,1,2,... 自然数序列缺了一个数,数组长度恰好比总个数少一。关键不变量:设缺失的数为 k,则 k 之前的位置都有 recordsi==i,k 及之后的位置都有 recordsi=i+1>i,问题就转化为找"第一个 recordsi>i 的位置"。查找边界:recordsmid>mid 说明缺失数在 mid 或更左,right=mid;否则缺失数在右边,left=mid+1。循环收敛到 left 后:若 recordsleft==left,说明缺失的是末尾那个数,返回 left+1;否则缺失的就是 left。这个思路和"找值不小于下标的第一个位置"是同一类边界查找。

任何的建议和心得欢迎交流,我们共同进步!

相关推荐
起个名字费劲死了1 小时前
基于灰度/形状的2D模板匹配
算法
杜 硕1 小时前
归并排序详解
数据结构·算法·排序算法
科技观察哨1 小时前
国产纳米定位平台与精密位移台量产能力图谱(2026):Z轴压电、XYθZ、六自由度并联与光刻/检测/装配选型指南
网络·人工智能·算法
蒸蒸yyyyzwd1 小时前
秋招学习笔记 Day 64 之准备系统软件开发工程师岗位笔记
c++·八股
鱼很腾apoc1 小时前
【MySQL】 第2期 查询与函数系统详解:从基础 CRUD 到复合多表查询
linux·数据库·学习·mysql·dml
奇树谦1 小时前
LRU 缓存原理与 C++ 实践示例
开发语言·c++·缓存
幼儿园蛋小黄2 小时前
嵌入式学习总结:从 I2C 中断驱动到 DHT11 温湿度采集实战
单片机·嵌入式硬件·学习
星恒随风2 小时前
Linux进程间通信(二):深入理解System V共享内存、消息队列、信号量与内核IPC资源管理
linux·运维·服务器·笔记·学习·状态模式
多弗朗皮卡丘2 小时前
C++ 红黑树
开发语言·c++·红黑树