二分专题

二分的题目类型

  • 75%的题目与单调性相关
  • 95%的题目与一种能分为两端,或者是说区间中能有一个临界的分界关系有关(比如某种性质在左区间成立,右区间不成立)

对于满足二段性的题目的两套模板

模板一

对于区间[L,R],我们想要确定一个M,并且我们知道在区间[L,M)中任意一点mid,满足check(mid)=true,而[M,R]满足check(mid)=false

模板如下

if check(mid)==false, [L,R]->[L,mid] R=mid

else check(mid)==true, [L,R]->[mid+1,R] L=mid+1

此时计算mid的方式是(L+R)/2下取整

int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}

模板二

对于区间[L,R],我们想要确定一个M,并且我们知道在区间[L,M]中任意一点mid,满足check(mid)=true,而(M,R]满足check(mid)=false

模板如下

if check(mid)==false, [L,R]->[L,mid-1] R=mid-1

else check(mid)==true, [L,R]->[mid,R] L=mid

此时计算mid的方式是(L+R)/2上取整,即(L+R+1)/2

int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;//注意l=mid,r=mid-1时求mid要加1
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}

解决二分题目的一般流程

  1. 确定二分的边界
  2. 编写一个二分的代码框架
  3. 设计一个check函数(用来检查性质是否满足,以便更新L,R)
  4. 判断区间如何更新
  5. 如果更新方式写的是L=mid,R=mid-1,那么在算mid的时候记得+1

LeeCode实战

LC69.x的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

解法思路

  1. 确定二分边界;本题的要求是求非负整数的平方根,所以边界应该界定为[0,x],由此可以确定,L=0,R=x;
  2. 编写代码框架;
  3. 设计check()函数;本例的check函数的确定要确保任何情况(指无论x是平方数或者非平方数)都能确保check两段性,且端点不变的情况,这里我们选取check函数为mid^2<=x;
  4. 判断区间更新方式;由我们的check方式很容易判断我们应该套用模板一;
  5. 判断mid的确定方式;模板一不需要进行+1操作;

代码

class Solution {
public:
int mySqrt(int x) {
int l=0,r=x;
while(l<r)
{
int mid=l+(long long)r+1>>1;//这个地方需要注意,我们要注意到x可能会产生溢出,所以把r改为long long避免这个问题
if(mid<=x/mid) l=mid;
else r=mid-1;
}
return r;
}
};

LC35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。

如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

解法思路

  1. 确定二分边界;本例给定的是一个有序数组,我们要做的是找到并返回目标值的索引,所以本例的边界应该是数组的索引范围,即L=0,R=nums.size()-1;
  2. 编写代码框架;
  3. 设计check()函数;本例给定的是一个有序的数组,我们如果想将整个区间划分为两端,只需要将nums[mid]与target进行比较就行,我们可以设计nums[mid]<target或者nums[mid]<=target,不同的设计方式会影响我们接下来的模板选取;
  4. 判断区间更新方式;
  5. 判断mid的确定方式;

代码

class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if(nums.empty() || nums.back()<target) return nums.size(); int l=0,r=nums.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]<target) l=mid+1;
else r=mid;
}
return l;
}
};

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

  • 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

解法思路一

本题因为是已经知道给定的整数数组是按照升序排列的,那么我们只需要遍历一遍数组内的所有元素,并进行记录target的索引位置,如果未查询到target那么便返回一个[-1,-1],查询到返回相应的[l,r]即可;但是这样做的时间复杂度是O(n),我们可以有更好的二分选择,从而将时间复杂度降低到O(log n);直接遍历的方法过于简单,这里就不列举代码;

解法思路二

给定的数组是一个升序的整数数组,由此我们可以很容易的想到采用二分法;

  1. 确定二分边界;查找一个数组的索引,我们的二分边界就是这个数组的索引范围,所以L=0,R=nums.size()-1;
  2. 编写代码框架;这个题目由于是找两个临界点,并且两个临界点的check设计并非相同,所以我们要二分查找两次,并且要用到两套模板;
  3. 设计check函数;对于开始位置的查找我们的check应该设计为nums[mid]<target,并且要应用模板一;对于结束位置的查找,我们应该设计check函数为nums[mid]<=target,并且要应用模板二;
  4. 判断区间更新方式;区间更新方式与之前讲的规则相同;
  5. 判断mid的确定方式;模板一不加1,模板二加1;

代码

class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans{-1,-1};
if(nums.empty()) return ans;
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r)/2;
if(nums[mid]<target) l=mid+1;
else r=mid;
}
ans[0]=l;
l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r+1)/2;
if(nums[mid]<=target) l=mid;
else r=mid-1;
}
ans[1]=l;
if(nums[ans[0]]!=target || nums[ans[1]]!=target)
{
ans[0]=-1;
ans[1]=-1;
return ans;
}
else return ans;
}
};

LC74.搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。

每行的第一个整数大于前一行的最后一个整数。

示例1

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3

输出:true

解法思路

本例其实是一个有序数组划分成了二维的数组,我们将每行首位相连,就会得到一个一维的升序数组,那么思路就有了,我们在不计较空间复杂度的情况下,我们可以新构造一个一维数组,然后讲二维数组按序添加到一维数组里面,那么这个题目就变成了34题,但是这样的空间消耗很大,所以我们采用了一种只在原二维矩阵上进行查找的方式;下面进行详细的讲解;

  1. 我们进行一下分析,我们其实可以将整个查找过程分为两步,我们并不是要一步做到精准定位,我们可以先确定target在哪一行(或者说可能在哪一行),然后再在确定的行,进行精准的查找。
  2. 第一步,我们只需要比较每一行的第一个元素与target的差别,如果target比matrix[i][0]小,则target一定在matrix的0i-1行,否则在matrix的in-1行,具此二段性,我们可以进行二分找到target的行位置row
  3. 第二步,在确定了行位置row之后,因为我们的每一行是升序排列的,所以也可以继续用二分进行列位置col的确定
  4. 之后我们需要做的就是判断matrix[row][col]是不是与target相等,如果相等,则返回{row,col},否则返回{-1,-1}

代码

class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m=matrix.size(),n=matrix[0].size();
if(target<matrix[0][0] || target>matrix[m-1][n-1]) return false;
int l=0,r=m-1;
while(l<r)
{
int mid=l+r+1>>1;
if(matrix[mid][0]<=target) l=mid;
else r=mid-1;
}
int j=l;
l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(matrix[j][mid]<=target) l=mid;
else r=mid-1;
}
if(matrix[j][l]==target) return true;
return false;
}
};

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

假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。

请找出其中最小的元素。

解法思路

本题也是极具代表性的一个二分题目,我们可以注意到,nums[0]或者是nums.back()的值都是二分的两端临界值,所以根据这个我们可以很容易的进行二分算法,然后确定索引i,返回nums[i]即可;

代码

class Solution {
public:
int findMin(vector<int>& nums) {
if(nums.size()==1) return nums[0];
int l=0,r=nums.size()-1,target=nums[r];
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]>target) l=mid+1;
else r=mid;
}
return nums[r];
}
};

LC33.搜索旋转排序数组

整数数组 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 。

解法思路

做完153题,在看这个问题其实很明了,153题让找一个最小值,或者让找一个最大值,其本质都是找出了一个旋转过后的分界点,那么根据这个分界点我们可以重构出一个有序数组。所以对于这一题,我们可以先找出分界点,然后判断target在哪一个升序区间,然后再用二分进行查找;

代码

class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int l=0,r=nums.size()-1,b=nums.back();
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]>b) l=mid+1;
else r=mid;
}
if(target<=b) r=nums.size()-1;
else l=0,r-=1;
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]<target) l=mid+1;
else r=mid;
}
if(nums[l]==target) return l;
return -1;
}
};

LC162.寻找峰值

峰值元素是指其值大于左右相邻值的元素。

给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

解法思路

本题就是二分题目中那种5%的题目,我们并没有一个很明确的二段性的性质来进行很容易的更新区间。但是这个题目,我们最暴力的解法就是进行一次遍历,根据题目假设和峰值的定义,我们完全可以一次遍历找到我们想要答案,但是这样的时间复杂度是O(n)。我们其实更像要一个时间复杂度为O(log n)的解法,这样我们就要进行一些细微的分析。

  1. 由于假设了nums[-1]=nums[n]=-∞,所以我们知道在nums中必定有一个峰值,并且很容易看出,当nums升序则答案为n-1,当nums降序时答案为0;
  2. 根据这个我们可以得到,如果nums[mid]<nums[mid+1],则在mid+1~n-1之间一定有一个峰值,相反在0-mid之间一定有一个峰值,具此我们可以写二分算法了;

    代码
class Solution {
public:
int findPeakElement(vector<int>& nums) {
if(nums.size()==1) return 0;
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]<nums[mid+1]) l=mid+1;
else r=mid;
}
return l;
}
};

LeetCode刷题 二分专题的更多相关文章

  1. C#LeetCode刷题-二分查找​​​​​​​

    二分查找篇 # 题名 刷题 通过率 难度 4 两个排序数组的中位数 C#LeetCode刷题之#4-两个排序数组的中位数(Median of Two Sorted Arrays)-该题未达最优解 30 ...

  2. LeetCode刷题 树专题

    树专题 关于树的几个基本概念 1 树的节点定义 2 关于二叉树的遍历方法 2.1 前序遍历 2.2 中序遍历 2.3 后序遍历 2.4 层序遍历 3 几种常见的树介绍 3.1 完全二叉树 3.2 二叉 ...

  3. LeetCode刷题 链表专题

    链表专题 链表题目的一般做法 单链表的结构类型 删除节点 方法一 方法二 增加节点 LeedCode实战 LC19.删除链表的倒数第N个结点 解法思路 LC24.两两交换链表中的节点 解法思路 LC6 ...

  4. LeetCode刷题专栏第一篇--思维导图&时间安排

    昨天是元宵节,过完元宵节相当于这个年正式过完了.不知道大家有没有投入继续投入紧张的学习工作中.年前我想开一个Leetcode刷题专栏,于是发了一个投票想了解大家的需求征集意见.投票于2019年2月1日 ...

  5. LeetCode刷题总结之双指针法

    Leetcode刷题总结 目前已经刷了50道题,从零开始刷题学到了很多精妙的解法和深刻的思想,因此想按方法对写过的题做一个总结 双指针法 双指针法有时也叫快慢指针,在数组里是用两个整型值代表下标,在链 ...

  6. LeetCode刷题总结-数组篇(上)

    数组是算法中最常用的一种数据结构,也是面试中最常考的考点.在LeetCode题库中,标记为数组类型的习题到目前为止,已累计到了202题.然而,这202道习题并不是每道题只标记为数组一个考点,大部分习题 ...

  7. LeetCode刷题总结-数组篇(中)

    本文接着上一篇文章<LeetCode刷题总结-数组篇(上)>,继续讲第二个常考问题:矩阵问题. 矩阵也可以称为二维数组.在LeetCode相关习题中,作者总结发现主要考点有:矩阵元素的遍历 ...

  8. LeetCode刷题总结-树篇(上)

          引子:刷题的过程可能是枯燥的,但程序员们的日常确不乏趣味.分享一则LeetCode上名为<打家劫舍 |||>题目的评论: 如有兴趣可以从此题为起点,去LeetCode开启刷题之 ...

  9. LeetCode刷题笔记和想法(C++)

    主要用于记录在LeetCode刷题的过程中学习到的一些思想和自己的想法,希望通过leetcode提升自己的编程素养 :p 高效leetcode刷题小诀窍(这只是目前对我自己而言的小方法,之后会根据自己 ...

随机推荐

  1. 简述编写Django应用的基本步骤

    (1)创建项目,cd到一个你想要放置你代码的目录.Django -admin startproject mysite. Django project即一个Django项目实例需要的设置项的集合,包括数 ...

  2. GDOI 2021 退役记

    Day -n 时常想自己不学OI会怎样,经常畏惧自己其实没有心里想的那样有能力,去机房来麻痹自己 从 3.21 始加大频率刷题,复习以前都学会,而现在都被抛在脑后的算法 反正都要退役了,成绩也得鲜亮点 ...

  3. mybatis中#{}与${}取值的区别

    1. 首先对于一个接口 Employee getEmpByIdAndName(@Param("id") Integer id,@Param("empName") ...

  4. 分组密码(五)AES算法② — 密码学复习(八)

    在上一篇简单复习了AES的历史时间节点.产生背景.与DES的对比.算法框图(粗略)以及一些数学基础,如果不记得的话点击这里回顾.下面将介绍AES算法的细节. 下面给出AES算法的流程,图片来源:密码算 ...

  5. 【原创】SystemVerilog中的typedef前置声明方式

    SystemVerilog中,为了是代码简洁.易记,允许用户根据个人需要使用typedef自定义数据类型名,常用的使用方法可参见"define和typedef区别".但是在Syst ...

  6. mysql从零开始之MySQL 管理

    MySQL 管理 启动及关闭 MySQL 服务器 Windows 系统下 在 Windows 系统下,打开命令窗口(cmd),进入 MySQL 安装目录的 bin 目录. 启动: cd c:/mysq ...

  7. MyBatis 批量插入数据的 3 种方法!

    批量插入功能是我们日常工作中比较常见的业务功能之一,之前我也写过一篇关于<MyBatis Plus 批量数据插入功能,yyds!>的文章,但评论区的反馈不是很好,主要有两个问题:第一,对 ...

  8. 反调试--CRC检测

    #include"CRC32.h" #include<Windows.h> #include<iostream> using namespace std; ...

  9. 通过Swagger文档生成前端service文件,提升前端开发效率

    在企业级的项目开发过程中,一般会采用前后端分离的开发方式,前后端通过api接口进行通信,所以接口文档就显得十分的重要. 目前大多数的公司都会引入Swagger来自动生成文档,大大提高了前后端分离开发的 ...

  10. Java基础之(八):顺序结构与选择结构

    顺序结构 Java的基本结构就是顺序结构 顺序结构是最简单的算法结构 它是任何一个算法都离不开的一种基本算法结构. 例子: public static void main(String[] args) ...