【二分算法 深度解析】二段性思维与经典题型全通关

文章目录

    • [1. 二分查找](#1. 二分查找)
    • [2. 在排序数组中查找元素的第一个和最后一个位置](#2. 在排序数组中查找元素的第一个和最后一个位置)
    • [3. x的平方根](#3. x的平方根)
    • [4. 搜索插入位置](#4. 搜索插入位置)
    • [5. 山脉数组的峰顶索引](#5. 山脉数组的峰顶索引)
    • [6. 寻找峰值](#6. 寻找峰值)
    • [7. 寻找旋转排序数组中的最小值](#7. 寻找旋转排序数组中的最小值)
    • [8. 0 ~ n-1 中缺失的数字](#8. 0 ~ n-1 中缺失的数字)

1. 二分查找

题目链接:704. 二分查找

算法原理:

解法一:暴力解法(不考虑题目复杂度要求)

遍历数组的每一个数判断是否相等:

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            if(nums[i] == target) return i;
        }
        return -1;
    }
}

解法二:二分查找算法

利用数组有序性,在数组中间找一个数,若该数大于 target ,则只需判断该数左边的数;若该数小于 target ,则只需判断该数右边的数。也就是将整个数组分为了两段,且只需对其中一段进行判断。然后进行这个循环判断。

那么此处的细节问题:

  1. 循环结束的条件
    因为每次需要判断的区间数都是未知的,因此即使到 left == right 时,该数也是未知需要判断的,所以结束条件应为 left > right。所以整个核心的算法步骤如图:
  1. 为什么是正确的?

    此方法是在暴力的基础上进行优化的,因此暴力解法既然正确,那么该优化解法也必然是正确的。

  2. 时间复杂度

当取一次中间值可以减少一半的数,所以根据规律可以得出时间复杂度为 O(logN)。

算法代码:

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

此处还有一个问题,当数组范围非常大的时候,mid 会出现溢出的问题,所以此处可以使用 int mid = left + ( right - left ) / 2 来解决溢出问题。

  • 朴素二分算法模板
  • 二分算法不仅适用于数组有序的情况,只有符合" 二段性 ",可以根据规律将数组分为两段且分段后依旧满足这个规律即可使用二分算法进行解决。

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

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

题目描述:

算法原理:

解法一:暴力解法

遍历数组找到满足的元素:

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 初始化起始和结束索引为 -1(表示未找到)
        int start = -1;
        int end = -1;
        
        // 暴力遍历整个数组
        for (int i = 0; i < nums.length; i++) {
            // 找到第一个匹配的元素,赋值给start
            if (nums[i] == target && start == -1) {
                start = i;
            }
            // 只要匹配,就更新end(最终会是最后一个匹配的索引)
            if (nums[i] == target) {
                end = i;
            }
        }
        
        // 返回结果数组
        return new int[]{start, end};
    }
}

解法二:二分查找算法

在朴素二分查找的基础上做优化。当在数组中间随便找一个值可能为目标值,但是却不能判断是否为左右端点,因此使用查找左右端点的二分来解决。

查找区间的左端点:

  • 取中间mid 下标值为 x,当x 小于 target 时,x 左边全部小于 target 没有满足条件的值,因此使left = mid + 1;当 x 大于等于 target 时,x 右边也全部大于等于 target,因为查找的是左端点,因此可以将 right 指针指向mid ,为什么不是 mid -1?因为当x恰好为左端点时,将right = mid - 1,那么区间内将再没有满足条件的数,因此必须为mid。

细节处理:

  1. 循环条件:循环条件应为 left < right,如果left == right 也作为循环条件,那么会出现死循环问题,x 始终会大于等于 target,right也始终会指向mid下标,无法跳出循环。当 left 等于 right 时,单独进行判断处理即可,若为目标值则更新边界,若不满足则直接返回即可。
  2. 求中点的操作:求中点有两种方式,left + ( right - left ) / 2 left + (right - left + 1) / 2这两种方法。在求左端点只能使用第一种不加1的方法,使用第二种加1会出现死循环问题。
    假设当前区间是 left=2, right=3,且 nums[mid] >= target(需要执行 right = mid)。
    用方式 2 计算 mid = 2 + (3-2+1)/2 = 3
    执行 right = mid → right = 3
    区间还是 left=2, right=3,没有任何变化
    下一次循环会重复计算相同的 mid,陷入死循环。

查找区间的右端点:

  • 取右端点和左端点处理起来是一样的,只不过细节上不一样。当x 小于等于 target 时,left 指向 mid;当x 大于target时,right = mid - 1。

细节处理:

  1. 循环条件:和左端点一样。
  2. 求中点操作:处理右端点必须使用 left + (right - left + 1) / 2的方法,使用left + ( right - left ) / 2会出现死循环问题。

算法代码:

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] ret = new int[]{-1,-1};
        //处理边界情况
        if(nums.length == 0) return ret;

        //1. 处理左端点
        int left = 0,right = nums.length-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 ret;
        else ret[0] = left;
        
        //2. 处理右端点
        left = 0;
        right = nums.length-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) ret[1] = right;
        
        return ret;
    }
}

代码中处理右端点时的 left = 0可以省略,因为left 已经指向了左端点,可以不从 0 开始处理,直接处理左端点到右边界的数即可。

3. x的平方根

题目链接:69. x的平方根

题目描述:

算法原理:

解法:二分查找算法

由题中只保留整数部分,随便找一个数mid平方可以得到将小于等于 x 作为一个区间,大于作为一个区间,由此可根据二段性使用二分。当小于等于时,将left指针指向mid处;大于时,right 指向 mid-1处。

细节处理:

循环条件为left < right,为二分模板的条件。求中点操作和处理右端点一样,要使用 left + ( right - left ) / 2 ,否则将会出现死循环。

算法代码:

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

4. 搜索插入位置

题目链接:35. 搜索插入位置

题目描述:

算法原理;

该题就是在朴素二分上添加了一个如果不存在返回插入位置的条件。观察规律发现,插入位置一定是在出现第一个比目标值大的数之前,因此区间可划分为小于和大于等于目标值。使用查找左右端点模板,此处使用查找左端点模板,当 mid 小于 目标值时,left 指向mid + 1,当mid 大于等于目标值,right 指向 mid。

细节处理:

求中点操作,因为查找左端点,所以中点使用left = ( right - left ) / 2操作,使用 +1 会出现死循环。此外,该题中left和right 都是在数组中的位置,当出现插入位置在数组外时应该进行单独处理。

算法代码:

java 复制代码
class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0,right = nums.length-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. 山脉数组的峰顶索引

题目链接:852. 山脉数组的峰顶索引

题目描述:

算法原理:

虽然该题中数组不是有序的,但是根据题意数组呈现倒三角形状,一定具有 " 二段性 ",因此也可以使用二分算法解决。

选取一个下标 i ,当 i 下标值大于 i-1时,说明峰值在 i 或i的右边,将left 指向i下标,因为 峰值可能为 i 下标,所以不是 i+1;当i 下标 小于 i-1 时,说明峰值在 i下标的左边,将right 指向 i - 1的位置。

细节处理:求中点操作使用 +1的方法,避免出现死循环。

算法代码:

java 复制代码
class Solution {
    public int peakIndexInMountainArray(int[] nums) {
        int left = 1,right = nums.length-2;
        while (left < right){
            int mid = left + (right - left + 1) / 2;
            if(nums[mid] > nums[mid-1]) left = mid;
            else right = mid - 1;
        }
        return left;
    }
}

6. 寻找峰值

题目链接:162. 寻找峰值

题目描述:

算法原理:

该题为上一题的进阶,只是峰值不是唯一的。算法思路基本一样。

算法代码:

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

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

题目链接:153. 寻找旋转排序数组中的最小值

题目描述:

算法原理:

根据题中可知数组要么刚好旋转一圈为原严格递增数组,要么为下图所示二段性数组:

发现左侧数组所有值都大于D点,因此当在数组中随便取一个值大于 D点值时,最小值应该在该下标到D点之间,将left 指向 该下标加一处;而右侧数组所有值都小于等于D点值,当取一个数小于等于D点值时,最小值应该在该下标或该下标左侧,将right 指向该下标。细节处理同上。且选用D点处理不需要处理严格递增时的边界问题,选取A点时需要处理边界问题。

算法代码:

java 复制代码
class Solution {
    public int findMin(int[] nums) {
        int left = 0,right = nums.length-1;
        int 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];
    }
}

8. 0 ~ n-1 中缺失的数字

题目链接:173. 0 ~ n-1 中缺失的数字

题目描述:

算法原理:

该题也具有二段性,将数组下标写出与数组进行比较发现左边区间数组值和下标一 一对应,而右边区间数组值和下标不对应,且要寻找的确实数字就是右边区间的左端点下标。因此可以利用该二段性使用二分算法解决。

当取一个数mid和下标相等时,目标值在另一个区间,将left 指向 mid + 1处,当mid 和下标不对应时,说明目标值为mid 或者mid左侧区间,将right 指向mid 处。细节处理循环条件和求中点操作同上。

算法代码:

java 复制代码
class Solution {
    public int takeAttendance(int[] nums) {
        int left = 0,right = nums.length-1;
        while (left < right){
            int mid = left + (right-left)/2;
            if(nums[mid] == mid) left = mid + 1;
            else right = mid;
        }
        if(nums[left] == left) return left + 1;
        return left;
    }
}
相关推荐
一起努力啊~2 小时前
算法刷题--字符串
算法
摇滚侠2 小时前
尚硅谷 Nginx 教程(亿级流量 Nginx 架构设计),基本使用,笔记 6-42
java·笔记·nginx
啊阿狸不会拉杆2 小时前
《数字图像处理》第 10 章 - 图像分割
图像处理·人工智能·深度学习·算法·计算机视觉·数字图像处理
SenChien2 小时前
Java大模型应用开发day06-天机ai-学习笔记
java·spring boot·笔记·学习·大模型应用开发·springai
早川9192 小时前
9种常用排序算法总结
数据结构·算法·排序算法
大只鹅2 小时前
Stream使用
java·开发语言
青衫码上行2 小时前
maven依赖管理和生命周期
java·学习·maven
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-离散化
c语言·数据结构·c++·算法·visual studio
散峰而望2 小时前
OJ 题目的做题模式和相关报错情况
java·c语言·数据结构·c++·vscode·算法·visual studio code