当前位置: 首页 > news >正文

L53.【LeetCode题解】二分法习题集2

目录

1.162. 寻找峰值

分析

代码

提交结果

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

分析

图像的增长趋势可以分这样几类

逐个击破

比较明显的

先增后减再增

用二段性给出left和right的更新算法

代码

提交结果

其他做法

提交结果

3.LCR 173. 点名(同剑指offer 53:0~n-1中缺失的数字)(一题多解)

分析

代码

提交结果

二分法的其他解法

其他解法1:异或运算

其他解法2:高斯求和


1.162. 寻找峰值

https://leetcode.cn/problems/find-peak-element/description/

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

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

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

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:

输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:

输入:nums = [1,2,1,3,5,6,4]输出:1 或 5 
解释:你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。

提示:

  • 1 <= nums.length <= 1000
  • -2^31 <= nums[i] <= 2^31 - 1
  • 对于所有有效的 i 都有 nums[i] != nums[i + 1]

分析

假设 nums[-1] = nums[n] = -∞(这个是关键条件,后面会用到),例如nums=[1,2,3,1],其峰值元素为3,从图中看,类似于极大值点

又如nums=[1,2,1,3,5,6,4], 其峰值元素为2或6

暴力查找需要从左往右一个一个筛选,时间复杂度为O(N),运行速度慢,利用二段性,采用二分法,比较nums[i]和nums[i+1]的大小,有且仅有两种局部单调情况(nums[i]!=nums[i+1])

目标:寻找峰值一定存在的位置

1. nums[i]<nums[i+1]

i+1及i+1之后一定存在峰值元素,i之前可能没有峰值

2.nums[i]>nums[i+1]

i及i之前一定存在峰值元素,i+1之后可能没有峰值

显然只考虑"一定存在"的情况,根据nums[i]和nums[i+1]之间的大小关系来更新查找的区间:

当nums[mid]<nums[mid+1]时,更新区间为:left=mid+1

当nums[mid]>nums[mid+1]时,更新区间为:right=mid

代码

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

注意:使用的是CC54.【C++ Cont】二分查找的左、右边界模版文章的查找左边界的模版, mid=left+(right-left)/2;不要写成 mid=left+(right-left+1)/2;,会死循环

提交结果

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1n 次旋转

分析

以[3,4,5,1,2]为例分析所有可能的旋转数组,分别画出它们的增长图:

[1,2,3,4,5]:

[5,1,2,3,4]:

[4,5,1,2,3]:

[3,4,5,1,2]:

[2,3,4,5,1]:

图像的增长趋势可以分这样几类

1.一直增: [1,2,3,4,5]

2.先减后增: [5,1,2,3,4]

3.先增后减再增: [4,5,1,2,3]、[3,4,5,1,2]

4.先增后减: [2,3,4,5,1]

逐个击破

比较明显的

先减后增特征明显:只有它上来先减,最小值在nums[1](前提:n>=2)

先增后减特征明显:只有它是最后减,最小值在nums[nums.size()-1](前提:n>=2)

先增后减再增

可以看出:最小值在左单增区间和右单增区间之间

给定初始区间[left,right],设计二分法让循环结束时,left==right==最小值所处的位置,

用二段性给出left和right的更新算法

观察可知:

如果i在左单增区间,那么nums[i]>nums[nums.size()-1](前提:n>=2)

如果i在右单增区间,那么nums[i]<=nums[nums.size()-1](前提:n>=2)

那么有:

当nums[mid]>nums[nums.size()-1]时,left=mid+1

当nums[mid]<=nums[nums.size()-1]时,right=mid

以上算法对于一直增的情况同样适用,因为一直满足nums[mid]<=nums[nums.size()-1],right=mid,区间右边界向左侧移动,最终left=right=0

*注:使用nums[0]来分左右单增区间见下面的其他方法

代码

class Solution {
public:int findMin(vector<int>& nums) {if (nums.size()==1)return nums[0];//先减后增if (nums[1]<nums[0])return nums[1];//先增后减if (nums[nums.size()-1]<nums[nums.size()-2])return nums[nums.size()-1];//一直增和先增后减再增int left=0;int right=nums.size()-1;while (left<right){int mid=left+(right-left)/2;if (nums[mid]>nums[nums.size()-1])//是否为左单增区间left=mid+1;else //否则为右单增区间: nums[mid]<=nums[nums.size()-1]right=mid;}return nums[left];}
};

提交结果

其他做法

如果用nums[0]来界定i在左单增区间还是右单增区间,写法是怎样的?

如果i在左单增区间,那么nums[i]>=nums[0](前提:n>=2)

如果i在右单增区间,那么nums[i]<nums[0](前提:n>=2)

该算法对一直增的情况是不能得出最小值的,left指针会一直向右移动,导致循环退出时,left和right都指向最大值,left==right==nums.size()-1(对于先增后减再增,循环结束时left和right是不可能等于nums.size()-1),此时只需要返回nums[0]即可

class Solution {
public:int findMin(vector<int>& nums) {if (nums.size()==1)return nums[0];//先减后增if (nums[1]<nums[0])return nums[1];//先增后减if (nums[nums.size()-1]<nums[nums.size()-2])return nums[nums.size()-1];//一直增和先增后减再增int left=0;int right=nums.size()-1;while (left<right){int mid=left+(right-left)/2;if (nums[mid]>=nums[0])//是否为左单增区间left=mid+1;else //否则为右单增区间: nums[mid]<nums[0]right=mid;}if (left==nums.size()-1)return nums[0];//一直增返回的结果elsereturn nums[left];//先增后减再增返回的结果}
};

提交结果

3.LCR 173. 点名(同剑指offer 53:0~n-1中缺失的数字)(一题多解)

https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/description/

某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。

示例 1:

输入:records = [0,1,2,3,5]
输出:4

示例 2:

输入:records = [0, 1, 2, 3, 4, 5, 6, 8]
输出:7

提示:

1 <= records.length <= 10000

分析

暴力解法显然是方法1:从左向右或者从右向左遍历,时间复杂度为O(N),可以使用二分法,观察是否具有二段性 方法2:建哈希表,之后一个一个查

以0~(5-1)为例,分类讨论各个情况:

缺0: [1,2,3,4,5]

缺1: [0,2,3,4,5]

缺2: [0,1,3,4,5]

缺3: [0,1,2,4,5]

缺4: [0,1,2,3,5]

缺5: [0,1,2,3,4]

 除了"缺0"和"缺5"的情况,其他情况均可以使用二段性进行分两段:

第一段:左单增段 第二段:右单增段

以缺3: [0,1,2,4,5]的为例:

给一个指针i,通过斜率来看在哪个区间

1.如果i在左单增区间,records[i]和records[records.size()-1]的连线的斜率是大于1的,即records[records.size() - 1] - records[mid] > records.size() - 1 - mid

此时更新left = mid

2.如果i在右单增区间,records[i]和records[0]的连线的斜率是大于1的,即records[mid] - records[0] > mid - 0

此时更新right = mid - 1

如果是"缺0"或者"缺5"的情况,那么情况1和2的斜率是一样的,这个特殊情况应该在首先就判断或者在最后返回的时候判断

代码

class Solution {
public:int takeAttendance(vector<int>& records){//mid==0if (records[records.size() - 1] - records[0] == records.size() - 1 - 0){if (records[0]==1)return 0;elsereturn records[records.size() - 1]+1;}int left = 0;int right = records.size() - 1;while (left < right){int mid = left + (right - left + 1) / 2;//左斜率:(nums[mid]-nums[0])/(mid-0)if (records[mid] - records[0] > mid - 0)//左斜率>1right = mid - 1;if (records[records.size() - 1] - records[mid] > records.size() - 1 - mid)left = mid;}return records[left] + 1;}
};

提交结果

二分法的其他解法

观察下标和元素之间的大小关系,以[0,1,3,4,5]为例

具有明显的二段性:

(最终返回下标2)

 绿框中元素的值==下标,蓝框中元素的值==下标+1

分情况更新left和right即可,循环退出时返回下标即可

注意讨论特殊情况:缺5: [0,1,2,3,4],返回records.size(),这个必须一开始就判断或者在最后返回的时候判断

class Solution {
public:int takeAttendance(vector<int>& records){if (records[records.size()-1]==records.size()-1)return records.size();int left = 0;int right = records.size() - 1;while (left < right){int mid = left + (right - left) / 2;if (records[mid]==mid)//左斜率>1left = mid+1;elseright=mid;          }return left;}
};

提交结果:

其他解法1:异或运算

构造完整的数组与records数组异或,直接得出结果(这个解法之前在E24.【C语言】练习:求一个整数存储在内存中的二进制中1的个数(两种方法)文章中提到过)

class Solution {
public:int takeAttendance( vector<int>& records){int ret=0;for (int i=0;i<records.size();i++)ret^=records[i];for (int j=0;j<=records.size();ret^=j,j++);return ret;}
};

时间复杂度为O(N) 

提交结果:

其他解法2:高斯求和

class Solution {
public:int takeAttendance(vector<int>& records){int sum=0;for (int i=0;i<records.size();i++)sum+=records[i];return (0+records.size())*(records.size()+1)/2-sum;}
};

时间复杂度为O(N) 

提交结果:

相关文章:

  • LangGraph(五)——自定义状态
  • AI练习:混合圆
  • [论文精读]Ward: Provable RAG Dataset Inference via LLM Watermarks
  • 2022年下半年信息系统项目管理师——综合知识真题及答案(5)
  • redis--redisJava客户端:Jedis详解
  • 《MQTT 从 0 到 1:原理、实战与面试指南全解》
  • 学习 Android(十一)Service
  • 一文详解并查集:从基础原理到高级应用
  • ctfhub技能书http协议
  • 人工智能小白转型学习指南
  • AI预测3D新模型百十个定位预测+胆码预测+去和尾2025年5月21日第84弹
  • java:找不到符号 java.util.0ptional<com.auth0.jwt.interfaces.DecodedJWT>
  • P1090 [NOIP 2004 提高组] 合并果子
  • SpringAI开发SSE传输协议的MCP Server
  • 信息系统项目管理师考前练习1
  • 【TTS回顾】StyleTTS 深度剖析:TTS+风格迁移
  • FTP与NFS服务详解
  • 五、【API 开发篇(下)】:使用 Django REST Framework构建测试用例模型的 CRUD API
  • 使用 Shadcn UI 构建 Java 桌面应用
  • 目标检测 LW-DETR(2024)详细解读
  • 知道抖音视频是怎么做的网站嘛/培训机构哪家好