力扣(leetcode)hot 100二分查找看这一篇就够了

欢迎阅读阳明Coding的博客,这一章带来了力扣 hot 100关于二分查找问题的解决方法。这期在代码上加上了详细注释,看讲解很迷糊的朋友可以直接copy代码结合注释看更好理解。

目录

二分查找算法

搜索插入的位置

搜索二维矩阵

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

搜索旋转排序数组

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

寻找两个正序数组的中位数


二分查找算法

在开始题目教程之前,先讲解一下二分查找算法是什么。

二分查找是一种在有序数组中快速查找目标值的算法 ,核心是每次将搜索的范围缩小一半。时间复杂度为 O(log n),效率远远高于线性查找的 O(n)

基本前提:

  1. 数组必须是有序的(升序 或者降序)
  • 用于静态查找表(没有频繁插入删除的场景)

算法步骤

基于上面的分析,当我们看到有序数组这个字眼,和寻找具体和某个值相关的问题时,可以考虑套用二分查找算法。


搜索插入的位置

35. 搜索插入位置

为什么选择二分查找?

从条件上看:

  • 必须使用 O(log n) 算法 → 直接指向二分查找

  • 数组是有序的 → 二分查找的前提条件

从题目看:

  • 找目标值位置 → 标准二分查找

  • 找不到时找插入位置 → 其实就是找第一个大于等于 target 的位置

  • 这是二分查找的变种(lower_bound)

代码流程

  1. 初始化左边界 left = 0,右边界 right = nums.length - 1

  2. 进入循环,条件:left <= right

  3. 计算中间位置 mid = left + (right - left) / 2

  4. 判断:

    • 如果 nums[mid] == target:找到目标,直接返回 mid

    • 如果 nums[mid] < target:目标在右侧,left = mid + 1

    • 如果 nums[mid] > target:目标在左侧,right = mid - 1

  5. 循环结束后,left 指向的就是插入位置
    原因:循环结束时 left 是第一个大于 target 的元素位置

代码

class Solution {
    public int searchInsert(int[] nums, int target) {
        // 定义二分查找的左右边界
        int left = 0;
        int right = nums.length - 1;
        
        // 标准二分查找循环条件
        while (left <= right) {
            // 计算中间位置,避免整数溢出
            int mid = left + (right - left) / 2;
            
            // 找到目标值,直接返回索引
            if (nums[mid] == target) {
                return mid;
            } 
            // 中间值大于目标值,目标值在左半部分
            else if (nums[mid] > target) {
                right = mid - 1;
            } 
            // 中间值小于目标值,目标值在右半部分
            else {
                left = mid + 1;
            }
        }
        
        // 循环结束时,left指向目标值应该插入的位置
        // 这是因为:
        // 1. 如果target不存在,left会停在第一个大于target的元素位置
        // 2. 或者停在数组末尾(当target大于所有元素时)
        return left;
    }
}

以  nums = [1,3,5,6], target = 2  手绘代码执行图如下


搜索二维矩阵

74. 搜索二维矩阵

思路分析

为什么用二分查找?

1. 数据特性利用:
   每行递增,且下一行首元素大于上一行末元素
   整个矩阵可以展开成一个有序的一维数组

2. 降维转换:
   二维坐标 [i][j] ↔ 一维索引 idx
   公式:行号 = idx / n,列号 = idx % n(n为列数)

代码流程

  1. 获取矩阵的行数 m 和列数 n

  2. 将二维矩阵虚拟成一维数组,总长度 = m * n

  3. 初始化 left = 0,right = m*n - 1

  4. 进入二分循环:

    • 计算 mid 索引

    • 将一维索引转换为二维坐标:
      行号 = mid / n
      列号 = mid % n

    • 比较 matrix[行号][列号] 与 target:
      相等:返回 true
      小于:left = mid + 1
      大于:right = mid - 1

  5. 循环结束未找到,返回 false

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 获取矩阵的行数和列数
        int m = matrix.length;
        int n = matrix[0].length;
        
        // 将二维矩阵视为一维有序数组进行二分查找
        // 总元素个数为 m*n
        int left = 0, right = m * n - 1;
        
        // 标准二分查找循环
        while (left <= right) {
            // 计算中间位置,避免整数溢出
            int mid = left + (right - left) / 2;
            
            // 将一维索引 mid 转换为二维矩阵的行列索引
            // 行索引 = mid / 列数n (整除得到行号)
            // 列索引 = mid % 列数n (取余得到列号)
            int i = mid / n;
            int j = mid % n;
            
            // 找到目标值,返回 true
            if (target == matrix[i][j]) {
                return true;
            }
            // 目标值大于中间值,在右半部分继续查找
            else if (target > matrix[i][j]) {
                left = mid + 1;
            }
            // 目标值小于中间值,在左半部分继续查找
            else if (target < matrix[i][j]) {
                right = mid - 1;
            }
        }
        
        // 循环结束未找到目标值,返回 false
        return false;
    }
}

以示例一画一下代码执行流程图

具体自己可以推算一遍


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

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

为什么用二分查找?

  • O(log n) 时间复杂度要求 → 必须二分

  • 有序数组(非递减) → 满足二分前提

代码执行流程

查找起始位置:

  1. 初始化 left=0, right=n-1, res=-1

  2. 循环条件:left <= right

  3. 计算 mid

  4. 判断:

    • 如果 nums[mid] < target:left = mid + 1

    • 否则(>= target):
      如果等于 target:记录 res = mid
      继续向左搜索:right = mid - 1

  5. 循环结束,res 就是起始位置

查找结束位置:

  1. 重新初始化 left=0, right=n-1, res=-1

  2. 循环条件:left <= right

  3. 计算 mid

  4. 判断:

    • 如果 nums[mid] > target:right = mid - 1

    • 否则(<= target):
      如果等于 target:记录 res = mid
      继续向右搜索:left = mid + 1

  5. 循环结束,res 就是结束位置

class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 初始化结果数组,默认值为[-1, -1]
        int[] res = new int[2];
        // 分别查找起始位置和结束位置
        res[0] = findFirst(nums, target);
        res[1] = findEnd(nums, target);
        return res;
    }

    // 查找目标值在数组中的起始位置(第一次出现的位置)
    public int findFirst(int[] nums, int target) {
        int res = -1; // 默认未找到
        int left = 0;
        int right = nums.length - 1;
        
        // 二分查找循环
        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止溢出
            
            if (nums[mid] < target) {
                // 中间值小于目标值,目标值在右半部分
                left = mid + 1;
            } else {
                // 中间值大于等于目标值
                if (nums[mid] == target) {
                    // 记录当前位置,可能是起始位置
                    res = mid;
                }
                // 继续向左查找,尝试找到更早出现的目标值
                right = mid - 1;
            }
        }
        return res; // 返回起始位置,未找到返回-1
    }

    // 查找目标值在数组中的结束位置(最后一次出现的位置)
    public int findEnd(int[] nums, int target) {
        int res = -1; // 默认未找到
        int left = 0;
        int right = nums.length - 1;
        
        // 二分查找循环
        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止溢出
            
            if (nums[mid] > target) {
                // 中间值大于目标值,目标值在左半部分
                right = mid - 1;
            } else {
                // 中间值小于等于目标值
                if (nums[mid] == target) {
                    // 记录当前位置,可能是结束位置
                    res = mid;
                }
                // 继续向右查找,尝试找到更晚出现的目标值
                left = mid + 1;
            }
        }
        return res; // 返回结束位置,未找到返回-1
    }
}


搜索旋转排序数组

33. 搜索旋转排序数组

为什么还能用二分查找?

  • O(log n) 要求 → 只能二分

  • 虽然是旋转数组,但局部有序 → 可以利用二分

  1. 初始化 left=0, right=n-1

  2. 循环条件:left <= right

  3. 计算 mid

  4. 如果 nums[mid] == target:直接返回 mid

  5. 判断哪部分是有序的:

    • 如果 nums[left] <= nums[mid]:左半部分有序
      判断 target 是否在 [nums[left], nums[mid]] 范围内:
      是:right = mid - 1(在左侧搜索)
      否:left = mid + 1(在右侧搜索)

    • 否则:右半部分有序
      判断 target 是否在 [nums[mid], nums[right]] 范围内:
      是:left = mid + 1(在右侧搜索)
      否:right = mid - 1(在左侧搜索)

  6. 循环结束未找到,返回 -1

class Solution {
    public int search(int[] nums, int target) {
        // 初始化二分查找的左右边界
        int left = 0, right = nums.length - 1;
        
        // 二分查找循环
        while (left <= right) {
            // 计算中间位置,防止整数溢出
            int mid = left + (right - left) / 2;
            
            // 找到目标值,直接返回索引
            if (nums[mid] == target) {
                return mid;
            }
            
            // 情况1:左半部分有序(即旋转点不在左半部分)
            // 注意:这里使用 <= 是为了处理 left 和 mid 相等的情况
            if (nums[left] <= nums[mid]) {
                // 目标值在有序的左半部分范围内
                if (nums[left] <= target && target <= nums[mid]) {
                    // 在左半部分继续搜索
                    right = mid - 1;
                } else {
                    // 目标值不在左半部分,转向右半部分
                    left = mid + 1;
                }
            }
            
            // 情况2:右半部分有序(即旋转点不在右半部分)
            // 注意:这里使用 <= 是为了处理 mid 和 right 相等的情况
            if (nums[mid] <= nums[right]) {
                // 目标值在有序的右半部分范围内
                if (nums[mid] < target && target <= nums[right]) {
                    // 在右半部分继续搜索
                    left = mid + 1;
                } else {
                    // 目标值不在右半部分,转向左半部分
                    right = mid - 1;
                }
            }
        }
        
        // 循环结束未找到目标值,返回 -1
        return -1;
    }
}


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

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

为什么用二分查找?

  • O(log n) 要求 → 只能二分

  • 旋转数组的局部有序性 → 可以通过比较缩小搜索范围

代码流程

  1. 初始化 left=0, right=n-1

  2. 循环条件:left < right(注意不是<=)

  3. 计算 mid

  4. 比较 nums[mid] 和 nums[right]:

    • 如果 nums[mid] > nums[right]:
      最小值在右侧,left = mid + 1

    • 如果 nums[mid] <= nums[right]:
      最小值在左侧或就是 mid,right = mid

  5. 循环结束时 left == right,指向最小值

class Solution {
    public int findMin(int[] nums) {
        // 初始化左右指针,覆盖整个数组
        int left = 0;
        int right = nums.length - 1;
        
        // 使用二分查找,循环条件是 left < right,不是 <=
        // 当 left == right 时,就找到了最小值的位置
        while (left < right) {
            // 计算中间位置,防止整数溢出
            int mid = left + (right - left) / 2;
            
            // 情况1:中间值大于右端值
            // 说明最小值在 mid 的右侧(因为旋转点在右侧)
            // 例如: [4,5,6,7,0,1,2] 中,nums[mid]=7 > nums[right]=2
            if (nums[mid] > nums[right]) {
                // 最小值在 mid 右侧(不包括 mid)
                left = mid + 1;
            }
            // 情况2:中间值小于等于右端值
            // 说明最小值在 mid 的左侧或者就是 mid 本身
            // 例如: [4,5,6,0,1,2,3] 中,nums[mid]=0 <= nums[right]=3
            else {
                // 最小值在 mid 左侧(包括 mid)
                right = mid;
            }
        }
        
        // 循环结束时,left == right,指向最小值的位置
        return nums[left];
    }
}


寻找两个正序数组的中位数

4. 寻找两个正序数组的中位数

为什么用二分查找?

  • O(log(m+n)) 要求 → 必须二分

  • 两个数组都有序 → 可以利用有序性快速排除元素

代码流程

  1. 确保 nums1 是较短的数组(方便处理)

  2. 初始化 left=0, right=m(nums1的长度)

  3. 循环条件:left <= right

  4. 计算分割点:
    i = (left+right)/2(nums1左半部分元素个数)
    j = (m+n+1)/2 - i(nums2左半部分元素个数)

  5. 处理边界:
    nums1左最大 = i==0 ? 最小整数 : nums1[i-1]
    nums1右最小 = i==m ? 最大整数 : nums1[i]
    nums2左最大 = j==0 ? 最小整数 : nums2[j-1]
    nums2右最小 = j==n ? 最大整数 : nums2[j]

  6. 检查分割是否有效:

    • 如果 nums1左最大 <= nums2右最小 且 nums2左最大 <= nums1右最小:
      分割有效,计算中位数候选值

    • 如果 nums1左最大 > nums2右最小:
      分割点太靠右,right = i-1

    • 否则:
      分割点太靠左,left = i+1

  7. 根据总长度奇偶性返回中位数:
    奇数:返回左半部分最大值
    偶数:返回(左半部分最大值+右半部分最小值)/2

二分查找算法

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 确保 nums1 是较短的数组,这样可以减少二分查找的范围
        // 这是为了优化性能,确保在较短的数组上进行二分查找
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.length; // 较短数组的长度
        int n = nums2.length; // 较长数组的长度
        int left = 0, right = m; // 在较短数组上进行二分查找
        
        // median1: 中位数的第一个值(奇数时为中位数,偶数时为左中位数)
        // median2: 中位数的第二个值(偶数时为右中位数)
        int median1 = 0, median2 = 0;

        // 二分查找较短数组的分割点
        while (left <= right) {
            // i: nums1 中左半部分的元素个数(分割点左边有 i 个元素)
            // j: nums2 中左半部分的元素个数(分割点左边有 j 个元素)
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i; // 总左半部分应有 (m+n+1)/2 个元素

            // 处理边界情况:
            // nums1 分割点左边的最大值,如果 i==0 则为负无穷(表示 nums1 左半部分为空)
            int nums_im = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            // nums1 分割点右边的最小值,如果 i==m 则为正无穷(表示 nums1 右半部分为空)
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            // nums2 分割点左边的最大值,如果 j==0 则为负无穷(表示 nums2 左半部分为空)
            int nums_jm = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            // nums2 分割点右边的最小值,如果 j==n 则为正无穷(表示 nums2 右半部分为空)
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            // 检查分割点是否满足中位数的条件:
            // 左半部分的最大值应小于等于右半部分的最小值
            if (nums_im <= nums_j) {
                // 当前分割点有效,记录候选的中位数
                median1 = Math.max(nums_jm, nums_im); // 左半部分的最大值
                median2 = Math.min(nums_i, nums_j);   // 右半部分的最小值
                // 尝试将分割点向右移动,寻找更优的分割
                left = i + 1;
            } else {
                // 当前分割点无效(nums1 左半部分的最大值太大)
                // 需要将分割点向左移动
                right = i - 1;
            }
        }
        
        // 根据总元素个数的奇偶性返回中位数
        // 偶数个元素:返回两个中位数的平均值
        // 奇数个元素:返回左中位数(median1)
        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }
}

其他算法

同时遍历两个有序数组,每次取最小值前进,记录中间位置的两个数求中位数。

public double findMedianSortedArrays(int[] A, int[] B) {
    int m = A.length;
    int n = B.length;
    // n 代表总长度,不是代码开头定义的 B 数组长度
    int totalLen = m + n;
    // left 和 right 用于记录中间的两个数
    int left = -1, right = -1;
    // aStart 和 bStart 是两个数组的指针
    int aStart = 0, bStart = 0;
    // 遍历到总长度的一半即可找到中位数
    for (int i = 0; i <= totalLen / 2; i++) {
        // 将上一轮的 right 赋给 left
        left = right;
        // 比较两个数组当前指针的值,选择较小的那个
        // 条件:A 数组未越界 且 (B 数组已越界 或 A 当前值较小)
        if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) {
            right = A[aStart++];
        } else {
            right = B[bStart++];
        }
    }
    // 根据总长度的奇偶性返回中位数
    if ((totalLen & 1) == 0) {
        // 偶数长度,中位数为中间两个数的平均值
        return (left + right) / 2.0;
    } else {
        // 奇数长度,中位数就是 right
        return right;
    }
}


总结起来就是,如果算看到有序数组O log(n)这一个 关键字眼,可以优先考虑使用二分查找解决。当然也要结合题目实际。有疑问或者改进建议的友友可以评论区留言一下。你们的关注,点赞,收藏就是我更新的最大动力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值