二分算法详解

简介: 本文介绍了二分查找及其相关问题的解决方法,包括基本的二分查找、查找元素的第一个和最后一个位置、求平方根、搜索插入位置、寻找峰值和旋转数组中的最小值等问题。通过详细分析每种情况下的二分查找策略,如循环条件、区间划分及特殊情况处理,提供了清晰的代码实现。适用于算法初学者和需要巩固二分查找技巧的开发者。

1. 二分查找

704. 二分查找

这是一道单纯的朴素二分模版题,当 left == right 时的这种情况也是需要考虑的,因为不排除数组中只有一个数的情况,或者是二分到数组中只剩一个数的情况,所以循环条件要写 left <= right

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

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

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

求区间左端点 :

可以把区间分为两部分,一部分是 x < t 的情况,另一部分就是 x >= t 的情况,由于 x < t 中不包含答案,所以可以把 left = mid + 1,跳出这部分,同理,由于答案在右半部分,所以 right = mid ,就不能跳出这个区间了,又因为这个区间都是 x >= t 的,所以最后也一定会找到左端点

关于循环条件:

当最后需要执行更新 right 的操作时,如果说此时 left 和 right 指向了同一个位置,那么算出来的 mid 还是这个位置,更新的 right 还是此时的位置,会一直重复这个操作,导致死循环,同时无论是有结果的情况还是没有结果的情况,当 left 和 right 指向了同一个位置这个时候都需要退出循环,所以说循环条件就不能包含等于了

求中点的操作:

当数组是偶数的时候,中点的位置会有两个,如果加一就是右边的中点,不加就是左边的中点,因为上面 right 是移动到 mid 的位置的,如果这里按照右边的中点就会死循环,按照左边的中点就可以正常的退出循环

求区间右端点:

和求左端点相反,这里是把小于等于目标值分为一个区间,大于目标值分为一个区间,所以说 left 就只能移动到 mid 的位置了,如果移动到 mid + 1 就会错过答案,同理 right 是需要移动到 mid - 1 的位置去寻找答案的

关于循环条件:

这里的条件和区间左端点是一样的,都是不能写等号

求中点的操作:

由于这一次需要将 left 移动到 mid 的位置,所以说就不能求左边的中点了,需要求右边的中点才能退出循环

之后再处理一下特殊情况就可以了:当数组为空时或者循环结束后没有找到目标值的情况

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] ans = new int[2];
        if (nums.length == 0) {
            ans[0] = -1;
            ans[1] = -1;
            return ans;
        }
        int left = 0, right = nums.length - 1, mid = 0;
        //左端点
        while (left < right) {
            mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        if (nums[right] == target) {
            ans[0] = right;
        } else {
            ans[0] = -1;
        }
        //left = 0;
        right = nums.length - 1;
        while (left < right) {
            mid = left + (right - left + 1) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid;
            }
        }
        if (nums[right] == target) {
            ans[1] = right;
        } else {
            ans[1] = -1;
        }
        return ans;
    }
}

3. x 的平方根

69. x 的平方根

题目要求小数部分要舍去,要求的是算术平方根,平方之后是小于等于 x 的,也就是要求区间的左端点的问题

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

4. 搜索插入位置

35. 搜索插入位置

无论是要插入的位置还是找到目标值,所在的区间都是 >= 目标值的区间,也就是查找区间左端点的情况,最后如果找到了目标值直接返回下标,找不到就返回下标 + 1,也就是要插入的位置

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

5. 山脉数组的峰顶索引

852. 山脉数组的峰顶索引

这道题并不像上面的题一样,要查找的区间是有序的,这道题虽然不是有序的但是具有二段性,所以也可以使用二分来解决

关于峰值:第一个前一个元素大于后一个元素的位置

根据上面的分法就是求区间的右端点,如果把峰值分到右区间就是求区间左端点,这里以右端点为例:

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        if (arr.length == 1) return 0;
        int left = 0, right = arr.length - 1, mid = 0;
        while (left < right) {
            mid = left + (right - left + 1) / 2;
            if (arr[mid] > arr[mid - 1]) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
}

6. 162. 寻找峰值

162. 寻找峰值

这道题和上一道题不同的是会有多个峰值,需要找到其中的一个峰值,虽然有很多峰值,但是经过分析,还是可以发现存在二段性:

首先是可能会有三种情况的

无论是哪种情况,都可以找到以下的状态

也就可以有以下的结论:

nums[mid] > nums[mid + 1] :  right = mid

nums[mid] < nums[mid + 1] :  left = mid + 1

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

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

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

每一次旋转就是把最后一位的元素移动到第一位,题目就是求旋转后的数组中的最小值,这道题也可以分析出二段性,由于题目中已经明确指出了是互不相同的数组,所以可以得出以下结论:

将数组可以分为两段,左边的半段都是比右边的大的,所以答案也就在右边,就需要让 left = mid + 1,同理,如果 mid 落到右边,那么就是 right = mid ,从而不错过答案

class Solution {
    public int findMin(int[] nums) {
        int n = nums.length;
        int left = 0, right = n - 1, mid = 0;
        while (left < right) {
            mid = left + (right - left) / 2;
            if (nums[mid] > nums[n - 1]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return nums[right];
    }
}

上面是以 nums[n - 1] 作为对照,也可以把第一个元素作为对照,不过此时就需要考虑翻转之后的数组如果一直都是递增的特殊情况

class Solution {
    public int findMin(int[] nums) {
        int n = nums.length;
        if (nums[n - 1] > nums[0]) {
            return nums[0];
        }
        int left = 0, right = n - 1, mid = 0;
        while (left < right) {
            mid = left + (right - left) / 2;
            if (nums[mid] < nums[0]) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return nums[right];
    }
}

8. LCR 173. 点名

LCR 173. 点名

这道题是有多种解法的:

第一种:哈希表,把 0 ~ n - 1 的数丢到哈希表中,然后遍历求解

第二种:直接遍历

第三种:位运算

第四种:高斯求和,相减

上面的解法时间复杂度都是 O (n) 的,用二分的话就需要去找它的二段性

可以通过下标入手,左边区间下标是对应的,右边的是数组元素比下标大的

此外,还有一种特殊情况,就是数组中的元素和下标都是对应的,此时缺少的数字就是 n

class Solution {
    public int takeAttendance(int[] records) {
        int left = 0, right = records.length - 1, mid = 0;
        if(records[right] == right) return right+1;
        while (left < right) {
            mid = left + (right - left) / 2;
            if (records[mid] == mid) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
相关文章
|
17天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
14天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2553 19
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
14天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1545 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
10天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
12天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
16天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
724 14
|
11天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
546 6
|
4天前
|
Docker 容器
Docker操作 (五)
Docker操作 (五)
148 68
|
4天前
|
Docker 容器
Docker操作 (三)
Docker操作 (三)
133 69
|
16天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
575 49
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界