文章目录
这里的二分模板来源于 《算法竞赛进阶指南》和Acwing,这里只是对模板的用法及Leetcode的例题讲解。 同时欢迎各位加入Acwing社区
所谓二分算法,就是不断的缩减区间的范围去寻找目标值
二分模板
整数二分
模板一
-
会将区间划分为[l,mid] 和[mid+1,r]两个区间,最终结果会落在左半区间
-
left指针和right指针最终都会落在相同的点上,可以通过两个指针指向的值判断是否是有解
int l = 0;
int r = nums.length-1;
while(l < r ){
int mid = l+r>>1;
if(check(mid)){
r = mid;
}else{
l = mid+1;
}
}
模板二
-
会将区间划分为[l,mid-1] 和[mid,r]两个区间,最终结果会落在右半区间
-
left指针和right指针最终都会落在相同的点上,可以通过两个指针指向的值判断是否是有解
int l = 0;
int r = nums.length-1;
while(ll < r){
int mid = l+r+1 >>1;
if(check(mid)){
l = mid;
}else{
r = mid-1;
}
}
浮点数二分
这个用的比较少,这里的k指的是题目的精度
double find(int left,int right){
double eps = le-k+2;
while(right - left > eps){
double mid = (right+left)/2;
if(check(mid)){
r = mid;
}else{
l = mid;
}
}
reutrn l;
}
写出正确的二分
-
判断题目是否可以使用二分,二分不仅仅适用于单调的序列,而且适用于有二段性的序列
-
通过判断答案所落在的区间,以及mid 归属于那一半区间
-
写出check函数,选用不同的模板
-
二分终止条件就是
l == r
, 可以通过l或者是r指向的值是否是预期值,判断有没有解
704. 二分查找
题目描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
思路
这道题就是二分查找的模板题。
首先分析题目,这是一个有序的数组,查找指定值,满足二分的基本条件
这里是一个升序的数组,如果说 num[mid] >= target
,那么最终答案会落在左半区间,因为这又是一个升序的数组,如果说mid指向的元素都比目标值大了,那么后面的只可能更大。
最终两个指针都会指向同一个位置,可以通过判断最后指向的位置是否为目标值,来判断是否有解
class Solution {
public int search(int[] nums, int target) {
int l = 0;
int r = nums.length-1;
while(l < r){
int mid = l+r >>1;
if(nums[mid]>= target){
r = mid;
}else{
l = mid+1;
}
}
if(nums[l] == target){
return l;
}
return -1;
}
}
34.在排序数组中查找元素的第一个和最后一个位置
题目描述
该题同样是可以用暴力解的,但是这里就不给出解法
在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
思路
一般来说,看到有序的数组,都要去想一想能不能用二分
-
如果说查找开始位置,在有解的情况之下,一定会落在候选区间的左半边,所以说我们选择
r = mid
的这个模板,那么check函数就顺理成章的写出来了,nums[mid]>=target
,这样答案就会落在左半区间,那么就得到的就是开始位置 -
如果说查找结束位置,与分析开始位置是一致的,在有解的情况之下,一定会落在候选区间的右半边,所以说我们选择
l=mid
的这个模板,那么check函数就是nums[mid]<=target
,这样答案就会落在右半区间,那么得到的就是结束位置
代码
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] ans = new int[]{-1,-1};
if(nums.length == 0){
return new int[]{-1,-1};
}
// >= target 中最小的那一个
int l = 0;
int r = nums.length-1;
while(l<r){
int mid = l+r>>1;
if(nums[mid]>=target){
r = mid;
}else{
l = mid+1;
}
}
if(nums[l] != target){
return ans;
}
// <= target 中最大的那一个
ans[0] = l;
l = 0;
r = nums.length-1;
while(l<r){
int mid = l+r+1>>1;
if(nums[mid]<=target){
l=mid;
}else{
r =mid-1;
}
}
if(nums[l]!=target){
return ans;
}
ans[1] =l;
return ans;
}
}
69.X的平方根
题目描述
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
思路
二分模板题
和上述两个题都是同样的分析方法
代码
class Solution {
public int mySqrt(int x) {
long l = 0;
long r = x;
while(l<r){
long mid = l+r+1l>>1;
if(mid <= x/mid){
l = mid;
}else{
r = mid-1;
}
}
return (int)l;
}
}
33.搜索旋转排序数组
题目描述
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
思路
从题目中进行分析,数组中没有重复元素,原来的数组升序的,旋转之
后,我们很容易发现,前半段都是大于新数组的第一个数,后半段都是小于新数组的第一个数,仍然具有二段性,可以使用二分。那么说现在就用两个问题,首先,就是如何去寻找这个分界点,其次,就是分界点确定之后,如果去运用我们的二分模板去寻找答案
-
分界点获取
- 遍历:分界点,后面的元素一定是比分界点出的元素小的
- 二分:整个序列具有二段性,前半段具有大于num[0] 的性质,后半段具有小于num[0]的性质
-
找到分界点之后,如果说target比第一个元素大,那么答案区间一定在多半段,反之,一定在右半段。不管是那一段,都是单调的,合理分析。既可以得到答案
代码
class Solution {
public int search(int[] nums, int target) {
if(nums.length <= 0 ){
return -1;
}
int l = 0 ;
int r = nums.length-1;
while(l < r){
int mid = l+r+1>>1;
if(nums[mid]>=nums[0]) l = mid;
else r = mid-1;
}
if(target >= nums[0]) l = 0 ;
else{
l =r+1;
r = nums.length-1;
}
while(l < r){
int mid = l + r >> 1;
if(nums[mid]>=target){
r = mid;
}else{
l = mid +1;
}
}
if(nums[r] == target ){
return r;
}
return -1;
}
}
81.搜索旋转排序数组II
题目描述
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
思路
相比于上题,数组中有了重复元素,使得不具有二段性,而没有二段性的原因就是旋转之后,新数组的头和尾中有了重复元素,如果说取出了这些重复元素,数组就重新具有了二段性,如果说数组尾和数组头是一致的,就压缩数组的右边界,然后就和上题一样的做法,之后就不在赘述
代码一
直接遍历,时间复杂度 O(n)
class Solution {
public boolean search(int[] nums, int target) {
for(int i = 0;i<nums.length;i++){
if(nums[i] == target){
return true;
}
}
return false;
}
}
代码二
二分,最坏情况下是O(N),相比之下,更加推荐使用代码一,代码量更加短,时间复杂度一致
class Solution {
public boolean search(int[] nums, int target) {
if(nums.length == 0){
return false;
}
int R = nums.length - 1;
while(R >= 0 && nums[R] == nums[0] ){
R--;
}
if(R < 0) return nums[0] == target;
int r = R;
int l = 0;
while(l < r){
int mid = l+r+1 >> 1;
if(nums[mid] >= nums[0]) l = mid;
else r = mid-1;
}
if(target >= nums[0]){
r = l;
l = 0;
}else{
l++;
r = R;
}
while(l < r){
int mid = l+r>>1;
if(nums[mid]>=target) r = mid;
else l = mid+1;
}
if(nums[r] == target) return true;
return false;
}
}
852.山脉数组的峰顶索引
题意描述
符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)
使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i] arr[i] > arr[i+1] > ... > arr[arr.length - 1]
.
给你由整数组成的山脉数组 arr ,返回任何满足arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
的下标 i 。
思路
这个数组实际上是具有二段性,前面一段是单调递增的,后面一段是单调递减的。我们需要找到分界点下标。如果说当前元素,比他的前一个元素要大的话,说明答案会在右半边
代码
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int n = arr.length;
int l = 0;
int r = n-2;
while(l<r){
int mid = l+r+1>>1;
if(arr[mid]>arr[mid-1]){
l = mid;
}else{
r = mid-1;
}
}
return l;
}
}
278.第一个错误的版本
题目描述
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数
思路
- 从题意中分析,整个序列具有二段性,前面一段是没有错误的版本,后面一段是错误的版本,所以可以用二分法去寻找中间的那个分割点
- 分析mid指针所指向的位置,如果说这个位置是错误的版本,那么答案最终回落至左半区间,选择r = mid,的这个二分版本
- 只需要将题目中提供的函数作为check函数即可
- l == r 就是答案
代码实现
时间复杂度 : O(logn)
空间复杂度 : O(l)
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int l = 1;
int r = n;
while(l<r){
long tem =(long) l+r>>1;
int mid = (int)tem;
if(isBadVersion(mid)){
r = mid;
}else{
l = mid+1;
}
}
return l;
}
}