【算法题】二分

二分查找是高效解决有序/局部有序数组问题 的经典算法,核心思想是通过不断缩小"可能包含目标的区间",将时间复杂度从暴力遍历的 O(n)O(n)O(n) 优化到 O(log⁡n)O(\log n)O(logn)。

它的适用场景非常广泛:不仅能解决"查找目标值"这类基础问题,还能处理"找边界""找极值""旋转数组"等复杂场景。本文将通过7道经典题目,拆解二分查找在不同场景下的解题思路与代码实现。

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

题目描述:

给定非递减排序的整数数组 nums 和目标值 target,找出 target 在数组中的开始位置和结束位置;若不存在则返回 [-1, -1]。要求时间复杂度为 O(log⁡n)O(\log n)O(logn)。

示例

  • 输入:nums = [5,7,7,8,8,10], target = 8,输出:[3,4]
  • 输入:nums = [5,7,7,8,8,10], target = 6,输出:[-1,-1]

解题思路:

通过两次二分查找分别确定左边界和右边界:

  1. 找左边界 :二分找第一个等于 target 的位置。若 nums[mid] < target,左指针右移;否则右指针左移,最终左指针即为左边界。
  2. 找右边界 :二分找最后一个等于 target 的位置。若 nums[mid] <= target,左指针右移;否则右指针左移,最终右指针即为右边界。
  3. 若左边界对应的元素不是 target,直接返回 [-1,-1]

完整代码:

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

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),两次二分查找各占 O(log⁡n)O(\log n)O(logn)。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

二、x 的平方根

题目描述:

给定非负整数 x,计算并返回其算术平方根的整数部分(舍去小数部分),不允许使用内置指数函数或运算符。

示例

  • 输入:x = 4,输出:2
  • 输入:x = 8,输出:2(8的平方根是2.828...,取整数部分)

解题思路:

通过二分查找找最大的整数 mid ,使得 mid² <= x

  1. 边界条件:若 x < 1,直接返回 0;否则二分区间为 [1, x]
  2. 二分过程:计算 mid = left + (right - left + 1) / 2(避免死循环),用 long long 存储 mid * mid 防止整数溢出;若 mid * mid <= x,左指针右移(保留当前候选值),否则右指针左移。

完整代码:

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

复杂度分析:

  • 时间复杂度:O(log⁡x)O(\log x)O(logx),二分区间大小为 x,每次缩小一半。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

三、搜索插入位置

题目描述:

给定排序数组 nums 和目标值 target,找到 target 在数组中的索引;若不存在,则返回其按顺序插入的位置。要求时间复杂度为 O(log⁡n)O(\log n)O(logn)。

示例

  • 输入:nums = [1,3,5,6], target = 5,输出:2
  • 输入:nums = [1,3,5,6], target = 2,输出:1

解题思路:

通过二分查找找第一个大于等于 target 的位置

  • nums[mid] < target,说明 target 在右边,左指针右移;否则右指针左移。
  • 最终左指针即为目标位置(若 nums[left] < target,则插入到 left+1,否则插入到 left)。

完整代码:

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

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),二分遍历数组。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

四、山脉数组的峰顶索引

题目描述:

给定山脉数组 arr(先递增后递减),返回峰值元素的下标,要求时间复杂度为 O(log⁡n)O(\log n)O(logn)。

示例

  • 输入:arr = [0,1,0],输出:1
  • 输入:arr = [0,2,1,0],输出:1

解题思路:

山脉数组的峰值满足 arr[mid] > arr[mid-1]arr[mid] > arr[mid+1],通过二分缩小范围:

  • 二分区间为 [1, arr.size()-2](避免越界),比较 arr[mid]arr[mid-1]
    • arr[mid] > arr[mid-1],说明峰值在右边,左指针右移。
    • 否则说明峰值在左边,右指针左移。
  • 最终左指针即为峰值下标。

完整代码:

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;
    }
};

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),二分遍历数组。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

五、寻找峰值

题目描述:

峰值元素是指严格大于左右相邻值的元素,给定数组 nums,返回任意一个峰值的下标。假设 nums[-1] = nums[n] = -∞,要求时间复杂度为 O(log⁡n)O(\log n)O(logn)。

示例

  • 输入:nums = [1,2,3,1],输出:2
  • 输入:nums = [1,2,1,3,5,6,4],输出:15

解题思路:

利用"边界为负无穷"的假设,通过二分找峰值:

  • 二分区间为 [0, nums.size()-1],比较 nums[mid]nums[mid+1]
    • nums[mid] > nums[mid+1],说明峰值在左边(包括 mid),右指针左移。
    • 否则说明峰值在右边,左指针右移。
  • 最终左指针即为峰值下标(必然存在峰值)。

完整代码:

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

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),二分遍历数组。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

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

题目描述:

给定升序旋转后的数组 nums(元素互不相同),返回数组中的最小元素,要求时间复杂度为 O(log⁡n)O(\log n)O(logn)。

示例

  • 输入:nums = [3,4,5,1,2],输出:1
  • 输入:nums = [4,5,6,7,0,1,2],输出:0

解题思路:

旋转后的数组分为"左升序段"和"右升序段",最小值是右段的第一个元素:

  • 二分区间为 [0, nums.size()-1],比较 nums[mid]nums.back()(最后一个元素):
    • nums[mid] > nums.back(),说明 mid 在左段,最小值在右边,左指针右移。
    • 否则说明 mid 在右段,最小值在左边(包括 mid),右指针左移。
  • 最终左指针即为最小值的下标。

完整代码:

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

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),二分遍历数组。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。

七、点名

题目描述:

班级 n 位同学的学号为 0~n-1,点名结果记录于升序数组 records,仅一位同学缺席,返回其学号。

示例

  • 输入:records = [0,1,2,3,5],输出:4
  • 输入:records = [0,1,2,3,4,5,6,8],输出:7

解题思路:

正常情况下 records[mid] == mid,缺席的学号会打破该关系:

  • 二分区间为 [0, records.size()-1],比较 records[mid]mid
    • records[mid] == mid,说明缺席在右边,左指针右移。
    • 否则说明缺席在左边(包括 mid),右指针左移。
  • 最终若 records[left] == left,缺席学号为 left+1;否则为 left

完整代码:

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) left = mid + 1;
            else right = mid;
        }
        return records[left] == left ? left + 1 : left;
    }
};

复杂度分析:

  • 时间复杂度:O(log⁡n)O(\log n)O(logn),二分遍历数组。
  • 空间复杂度:O(1)O(1)O(1),仅用常数级额外变量。
相关推荐
想唱rap2 小时前
哈希(C++)
服务器·开发语言·c++·算法·哈希算法
资深web全栈开发2 小时前
LeetCode 2054:两个最好的不重叠活动 —— 从暴力到优化的完整思路
算法·leetcode
IT方大同2 小时前
数组的初始化与使用
c语言·数据结构·算法
im_AMBER2 小时前
Leetcode 84 水果成篮 | 删除子数组的最大得分
数据结构·c++·笔记·学习·算法·leetcode·哈希算法
AAA阿giao2 小时前
从树到楼梯:数据结构与算法的奇妙旅程
前端·javascript·数据结构·学习·算法·力扣·
Salt_07282 小时前
DAY 41 Dataset 和 Dataloader 类
python·算法·机器学习
长安er2 小时前
LeetCode 124/543 树形DP
算法·leetcode·二叉树·动态规划·回溯
Sheep Shaun2 小时前
STL:list,stack和queue
数据结构·c++·算法·链表·list
杜子不疼.2 小时前
【LeetCode 153 & 173_二分查找】寻找旋转排序数组中的最小值 & 缺失的数字
算法·leetcode·职场和发展