【LeetCode数组#1二分法】二分查找、搜索插入、在排序数组中查找元素的第一个和最后一个位置
二分查找
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
初见思路
虽然这套题有明确的考察点,但是我习惯拿到题先用最笨的办法实现一次,然后再改进算法
这题整体还是比较简单的,如果用常规的思路也很容易做出来
例如:遍历数组,找到符合条件的值,然后返回其下标,如果条件均不满足,则返回默认下标值(-1)
class Solution {
public int search(int[] nums, int target) {
int index = -1;
for(int i = 0; i < nums.length; i++){
if(nums[i] == target){
index = i;
}
}
return index;
}
}
那么实际上,本题考察的主要是“查找”数组的过程
显然,遍历的方式对于二分法来说,是非常低效的,因此肯定是要用二分法来写的
常规思路
在升序数组nums中寻找目标值target,对于特定下标 i,比较nums[i]和target的大小
- 如果nums[i] = target,则下标 i 即为要寻找的下标;
- 如果nums[i] > target,则 target 只可能在下标 i 的左侧;
- 如果nums[i] < target,则target只可能在下标 i 的右侧。
基于上述事实,可以在有序数组中使用二分查找寻找目标值。【ps:如果数组是乱序,那么有你可能会找出多个值】
二分法的解题流程如下:
1、定义一个查找的范围,一般用left和right代表左右边界
2、在该范围内取中点middle,比较nums[mid]和target
3、根据比较结果进行返回或继续对某侧进行查找
解题模板
实际上二分法有两种写法,分别是左闭右闭和左闭右开【即是否包含边界点本身】,模板只记一种就行,另外的补充再说
c++版
class Solution {
public:
int search(vector<int>& nums, int target) {
//定义左右边界
int left = 0;
int right = nums.size() - 1;
while(left <= right){
//计算中间值
int mid = (right - left)/2 + left;//定位mid值所在位置
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
//移动左边界
left = mid + 1;
}else if(nums[mid] > target){
//移动右边界
right = mid - 1;
}
}
return -1;
}
};
二刷问题
移动边界时,逻辑没有处理好。忘了要跳过mid
要移动左边界的话,移动的目标位置应该是当前mid的后一位
而移动左边界的话,目标位置应该是当前mid的前一位
Java版
class Solution {
public int search(int[] nums, int target) {
//①定义左右边界
int left = 0, right = nums.length - 1;
//②写一个条件循环*
while (left <= right) {
//计算区间的中点mid
int mid = (right - left) / 2 + left;//用减是为了防止数据溢出,加left是为了定位到当前区间
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) { //此时,target只能在区间中点的左半部分,因此右边界改变,变为左半部分的右边界
right = mid - 1;
} else if(nums[mid] < target) {//target只能在区间中点的右半部分,因此左边界改变,变为右半部分的左边界
left = mid + 1;
}
}
return -1;//找不到返回-1
}
}
注释:
边界问题
在上述写法中有两处值得关注的边界,
一处是while的条件left <= right【什么时候要写<、什么时候写<=】,一处是边界改变时的mid - 1【什么时候写mid,什么时候写mid-1】
这两个边界问题都只取决于你决定采用哪种二分法的写法,在一开始我们就必须先确定要用左闭右闭还是左闭右开,然后后面再写的时候要贯彻执行,根据区间的定义去选择边界
以左闭右闭[left, right]为例
当忘了while里应该写<=还是<时,可以想一想一开始确定的区间
比如,如果我写left <= right,对于[left, right]来说是不是合法的呢?【[1,1],1到1的区间且包含1,虽然只有一个元素但至少不非法】
显然还是可以这么写的,所以在左闭右闭时应该写left <= right
类似的,left <= right对于[left, right)则不合法【[1,1),即包含1又不含1?】
那么mid呢?
假设目前你的nums[mid] > target,那么这个target一定不会出现在当前mid的右边区间了
所以下一步应该从左边区间再查找
这时候应该将当前的右边界调整到左边区间上来,并且需要排除掉当前的nums[mid]
因此下次查找的右边界的下标应该是right = mid - 1
如果不减1就相当于把一个不属于下次查找区间的数放到区间中了,对于区间来说是非法的,自然会出错
类似的,right = mid - 1对于[left, right)则不合法,因为[left, right)在下次查找中本来就不包含right,所以应该直接写right = mid【若nums[mid] < target,则[left, right)的left还是需要写成left = mid - 1,因为left在[left, right)中是被包括在内的】
Python版
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left)//2 + left
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
拓展练习
LeetCode35搜索插入位置
思路就是二分查找,这里还是选左闭右闭的方式去写
c++版
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//定义左右边界
int left = 0;
int right = nums.size() - 1;
while(left <= right){
//计算mid
int mid = (right - left)/2 + left;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
//移动左边界
left = mid + 1;
}else if(nums[mid] > target){
//移动右边界
right = mid - 1;
}
}
return right + 1;
}
};
基本上还是按照二分查找的思路去写的,这里的关键是遍历完成之后的返回值
为什么是返回right + 1?
遍历完之后可以有以下几种情况:(都是没找到target的情况)
1、遍历完了还没结束说明并没有找到target
此时需要返回插入位置,因为跳出while循环了,此时的 left 肯定大于 right
l↓ ↓r
[1,2,3,4]
r↓ ↓l
[1,2,3,4]
此时,要插入的位置是2和3之间,因为r已经跑到小于l的位置,所以要加回来然后返回(即right + 1),这样target才能插到2和3之间
2、遍历到了右边界还没找到找到target
l↓ ↓r
[1,2,3,4]
此时,需要插入的位置是right + 1(即数组末尾)
3、遍历到了左边界还没找到找到target
此时只能是 left 大于 right 才会发生的情况,因为缩右边界时,左边界是不变的,如果一直nums[mid] > target,那就会一直移动右边界,直到超过一直不动的left,循环结束,此时情况如下:
↓rl↓
[1,2,3,4]
右边界已经指向数组外面,所以要加回来,加到l在的位置再插入target
即right + 1
因此,在while循环结束后返回right + 1可以处理多种情况
Java版
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = (right - left)/2 + left;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1;
}else{
right = mid - 1;
}
}
//此处可以同时处理四种情况下的返回值
//1、目标值等于数组中某一个元素 return middle;
//2、目标值插入数组中的位置 [left, right],return right + 1
//3、目标值在数组所有元素之后的情况 [left, right],这是右闭区间,所以 return right + 1
//4、目标值在数组所有元素之前 [0, -1]【没想通】
return right+1;
}
}
Python版
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)-1
while left <= right:
mid = (right - left)//2 + left
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return right+1
LeetCode34在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
思路
从示例1来看,我们查找的元素可以是多个,然后需要返回的是多个数位于数组中的区间(暂且称为结果区间)
因此需要分别求结果区间的左边界和右边界
一种笨一点但比较有条理的方式是:使用二分法分别求出左边界和右边界,然后再根据条件判断
在此之前我们需要明确会出现的情况
情况1:target出现在数组范围的左边
nums = [5,7,7,8,8,10], target = 2
因为数组nums是进过排序的,因此2是在数组范围的左边
当然此时是不能从数组中找到terget的,因此要返回{-1,-1}
情况2:target出现在数组范围的右边
nums = [5,7,7,8,8,10], target = 16
同理,此时也无法再数组中找到target,因此要返回{-1,-1}
情况3:target在数组范围内
nums = [5,7,7,8,8,10], target = 8
因为nums是经过排序的整数数组,所以一旦target落在数组范围内,那必然会有匹配值
上面的情况返回值应该是{3,4}
下面来分别求左右边界
代码分析
寻找左右边界的代码实际上就是单独的二分法代码
但里面有一些需要注意的点,先看代码整体框架
class Solution {
private:
int getRightBorder(vector<int>& nums, int target) {
}
int getLeftBorder(vector<int>& nums, int target) {
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
}
};
因为要单独求左边界和右边界,所以单独定义两个函数负责做这件事
函数定义如下
class Solution {
private:
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2;
while(left <= right){
int mid = (right - left)/2 + left;
if(nums[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2;
while(left <= right){
int mid = (right - left)/2 + left;
if(nums[mid] >= target){
right = mid - 1;
leftBorder = right;
}else{
left = mid + 1;
}
}
return leftBorder;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
}
};
这里有几点需要注意的
1、左边界和求右边界的核心代码是一个逻辑
实际上,求左边界和求右边界的核心代码是一个逻辑,都是简单的二分法
但是,因为要求的是左右边界,所以加入了一些条件对查找范围进行了控制
2、为什么左右边界的初始值都是-2?
因为这是一个特殊值,方便在没找到的情况下返回题目要求的{-1,-1}
3、为什么求右边界时,循环中的条件是nums[mid] > target,而求左边界时是nums[mid] >= target?
在求解右边界和左边界时,循环中的条件不同的原因是为了确保获取到正确的边界值。
- 求解右边界时(
getRightBorder函数),我们需要找到第一个大于目标值的元素位置。因此,当nums[middle] > target时,我们将right更新为middle - 1,继续在左半部分查找(目标值在mid的左边)。而当nums[middle] == target时,我们需要更新左边界,因为目标值可能出现在右半部分。所以我们将left更新为middle + 1,同时记录当前的left值作为右边界的候选位置。 - 求解左边界时(
getLeftBorder函数),我们需要找到第一个大于或等于目标值的元素位置。因此,当nums[middle] >= target时,我们将right更新为middle - 1,继续在左半部分查找。同时,我们将当前的right值记录为左边界的候选位置。而当nums[middle] < target时,我们将left更新为middle + 1,继续在右半部分查找。
通过这样的条件设置,最终得到的左边界和右边界位置可以保证是符合要求的。在最坏情况下,左边界和右边界都是在数组的边界位置,即左边界是 -1,右边界是 nums.size()。在函数的返回值中,如果左边界和右边界的差值大于 1,则说明存在目标值,并且返回的结果为目标值的范围(左边界加 1,右边界减 1);如果差值为 1,则说明只有一个目标值,返回的结果为 -1, -1;如果左边界和右边界都是边界值,则说明目标值不存在,返回的结果为 -1, -1。
4、为什么使用left作为右边界的更新值,使用right作为左边界的更新值?
这个问题实际上在问题3中已经解释了
这里需要画个图:

完整代码
注意点:
1、求左右边界的函数核心实现是一样的,只是记录左右边界的位置不同
2、左右边界变量值初始化为-2
3、求左边界时记得right指针更新的条件是大于等于
class Solution {
private:
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2;
while(left <= right){
int mid = (right - left)/2 + left;
if(nums[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2;
while(left <= right){
int mid = (right - left)/2 + left;
if(nums[mid] >= target){
right = mid - 1;
leftBorder = right;
}else{
left = mid + 1;
}
}
return leftBorder;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
//计算左右边界
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
//根据计算得到的左右边界判断返回值
// 情况一
if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
// 情况二
return {-1, -1};
}
};
【LeetCode数组#1二分法】二分查找、搜索插入、在排序数组中查找元素的第一个和最后一个位置的更多相关文章
- 【LeetCode】34. 在排序数组中查找元素的第一个和最后一个位置
34. 在排序数组中查找元素的第一个和最后一个位置 知识点:数组,二分查找: 题目描述 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置 ...
- Java实现 LeetCode 34 在排序数组中查找元素的第一个和最后一个位置
在排序数组中查找元素的第一个和最后一个位置 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n ...
- LeetCode HOT 100:在排序数组中查找元素的第一个和最后一个位置
题目:34. 在排序数组中查找元素的第一个和最后一个位置 题目描述: 给你一个递增数组,和一个目标值target,最终返回数组中第一次出现target和最后一次出现target的下标.如果该数组中没有 ...
- 34、在排序数组中查找元素的第一个和最后一个位置 | 算法(leetode,附思维导图 + 全部解法)300题
零 标题:算法(leetode,附思维导图 + 全部解法)300题之(34)在排序数组中查找元素的第一个和最后一个位置 一 题目描述 二 解法总览(思维导图) 三 全部解法 1 方案1 1)代码: / ...
- [LeetCode] 34. Find First and Last Position of Element in Sorted Array 在有序数组中查找元素的第一个和最后一个位置
Given an array of integers nums sorted in ascending order, find the starting and ending position of ...
- 【LeetCode】34-在排序数组中查找元素的第一个和最后一个位置
题目描述 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n) 级别. 如果数组中不存在目标值 ...
- Leetcode题目34.在排序数组中查找元素的第一个和最后一个位置(中等)
题目描述: 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n) 级别. 如果数组中不存在目标 ...
- [Swift]LeetCode34. 在排序数组中查找元素的第一个和最后一个位置 | Find First and Last Position of Element in Sorted Array
Given an array of integers nums sorted in ascending order, find the starting and ending position of ...
- 【LeetCode】在排序数组中查找元素的第一个和最后一个位置【三次二分】
给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n) 级别. 如果数组中不存在目标值,返回 [ ...
- LeetCode 34 - 在排序数组中查找元素的第一个和最后一个位置 - [二分][lower_bound和upper_bound]
给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n) 级别. 如果数组中不存在目标值,返回 [ ...
随机推荐
- [转帖]012 Linux 搞懂用户权限升级 (sudo 和 su),包学会
https://my.oschina.net/u/3113381/blog/5431540 Linux 系统中 root 账号通常用于系统的管理和维护,对操作系统的所有资源具有访问控制权限,当一个普通 ...
- 一次JSF上线问题引发的MsgPack深入理解,保证对你有收获
作者: 京东零售 肖梦圆 前序 某一日晚上上线,测试同学在回归项目黄金流程时,有一个工单项目接口报JSF序列化错误,马上升级对应的client包版本,编译部署后错误消失. 线上问题是解决了,但是作为程 ...
- 一文搞懂Redis
作者: 京东物流 刘丽侠 姚再毅 康睿 刘斌 李振 一.Redis的特性 1.1 Redis为什么快? 基于内存操作,操作不需要跟磁盘交互,单次执行很快 命令执行是单线程,因为是基于内存操作,单次执行 ...
- 可插拔组件设计机制—SPI
作者:京东物流 孔祥东 1.SPI 是什么? SPI 的全称是Service Provider Interface,即提供服务接口:是一种服务发现机制,SPI 的本质是将接口实现类的全限定名配置在文件 ...
- 【JS 逆向百例】Ether Rock 空投接口 AES256 加密分析
关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...
- [postgres]配置主从异步流复制
前言 环境信息 IP 角色 操作系统 PostgreSQL版本 192.168.1.112 主库 Debian 12 15.3 192.168.1.113 从库 Debian 12 15.3 配置主从 ...
- Unity的SpriteAtlas实践
我的环境 Unity引擎版本:Unity2019.3.7f1 AssetBundles-Browser 于2021-1-14拉取,github上最后提交日期是2019-12-14,在本文简称:ABBr ...
- vim 从嫌弃到依赖(16)——宏
终于到了我第二喜欢的vim功能了(当然了,最喜欢的是.命令).我原本计划在介绍完.命令之后介绍宏,以便让各位小伙伴们能了解到vim对于重复操作进行的强大的优化.但是由于宏本身跟寄存器息息相关,所以还是 ...
- python快速入门【四】-----各类函数创建
python入门合集: python快速入门[一]-----基础语法 python快速入门[二]----常见的数据结构 python快速入门[三]-----For 循环.While 循环 python ...
- 6.1 Windows驱动开发:内核枚举SSDT表基址
SSDT表(System Service Descriptor Table)是Windows操作系统内核中的关键组成部分,负责存储系统服务调用的相关信息.具体而言,SSDT表包含了系统调用的函数地址以 ...