二分专题

二分的题目类型

  • 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. 12306抢票算法居然被曝光了!!!居然是redis实现的

    导读 相信大家应该都有抢火车票的经验,每年年底,这都是一场盛宴.然而你有没有想过抢火车票这个算法是怎么实现的呢? 应该没有吧,咱们今天就来一一探讨.其实并没有你想的那么难 bitmap与位运算 red ...

  2. P6624-[省选联考2020A卷]作业题【矩阵树定理,欧拉反演】

    正题 题目链接:https://www.luogu.com.cn/problem/P6624 题目大意 \(n\)个点的一张图,每条边有权值,一棵生成树的权值是所有边权和乘上边权的\(gcd\),即 ...

  3. CF618F-Double Knapsack【结论】

    正题 题目链接:https://www.luogu.com.cn/problem/CF618F 题目大意 给出大小为\(n\),值域为\([1,n]\)的两个可重集合\(A,B\) 需要你对它们各求出 ...

  4. 《HelloGitHub》第 66 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这里有实战项目.入门教程.黑科技.开源书籍.大厂开源项目等,涵盖多种编程语言 Pyt ...

  5. Decorator装饰器模式个人理解

    对于装饰器模式,其主要是为了:在不改变本体特征的情况下,对其进行包装.装饰,目的是为了补充.扩展.增强其功能. 有三个原则: 不能改变本体的特征 要对本体的功能进行扩展 装饰器脱离了本体则没有任何含义 ...

  6. UTF-8和Unicode编码

    常用的能够保存汉字的编码表有UTF-8.GBK等.需要注意,无论文件使用的是什么编码格式,读取到Java程序中,所有的字符都是用Unicode编码表示(Java中所有的字符内容都使用char类型表示, ...

  7. ECMA 2022 (es13) 新特性

    本文主要整理了截至到 2021年10月12日 为止的且处于 Stage 3->Stage 4 阶段的ECMA提案. 主要包括: Class Fields RegExp Match Indices ...

  8. 基于linux在线预览

    1.Libreoffice安装 在服务器上安装Libreoffice,在这里就不多说了, import os import sys import subprocess import re def co ...

  9. PAT (Basic Level) Practice (中文)1086 就不告诉你 (15分)

    1086 就不告诉你 (15分) 做作业的时候,邻座的小盆友问你:"五乘以七等于多少?"你应该不失礼貌地围笑着告诉他:"五十三."本题就要求你,对任何一对给定的 ...

  10. Java(24)常用API三

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15228417.html 博客主页:https://www.cnblogs.com/testero ...