一、在数组中查找
1、二分查找
力扣题单 704
给定一个 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
class Solution {
public:
int search(vector<int>& nums, int target) {
int right=nums.size()-1; //右边边界
int left=0; //左边界
while(left<=right){
int mid=left+((right-left)>>1); //相当于(left+right)/2,但是这样可以防止溢出
if(nums[mid]==target){
return mid; //找到target就返回 结束循环
}else if(nums[mid]>target){
right=mid-1; //target在区间[left,mid-1]
}else{
left=mid+1; //target在区间[mid+1,right]
}
}
return -1; //没有找到
}
};
2、搜索插入位置
力扣题单 35
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
class Solution {
public:
//实际上就是找到一个大于等于这个target的位置
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]<target){
left=mid+1;
//只要nums[mid]<target 那么就会更新left
//所以这一步保证了nums[left]>=target
//如果nums[left]==target 那么就返回left 没有任何问题
//如果nums[left]>target 那么这个位置就是targte应该插入的位置 因为nums[left-1]=nums[mid]<target
}else{
right=mid-1;
}
}
return left;
}
};
3、寻找比目标字母大的最小字母
力扣题单 744
给你一个字符数组 letters,该数组按非递减顺序排序,以及一个字符 target。letters 里至少有两个不同的字符。
返回 letters 中大于 target 的最小的字符。如果不存在这样的字符,则返回 letters 的第一个字符。
示例 1:
输入: letters = ["c", "f", "j"],target = "a"
输出: "c"
解释:letters 中字典上比 'a' 大的最小字符是 'c'。
示例 2:
输入: letters = ["c","f","j"], target = "c"
输出: "f"
解释:letters 中字典顺序上大于 'c' 的最小字符是 'f'。
示例 3:
输入: letters = ["x","x","y","y"], target = "z"
输出: "x"
解释:letters 中没有一个字符在字典上大于 'z',所以我们返回 letters[0]。
class Solution {
public:
char nextGreatestLetter(vector<char>& letters, char target) {
int right=letters.size()-1;
int left=0;
while(left<=right){
int mid=left+((right-left)>>1);
if(letters[mid]-'0'<=target-'0'){ //这里的-'0'操作是将字符转成数字进行比较 当然也可以不转
left=mid+1;
//这里和上一题一样 保证了letters[left]>target
}else{
right=mid-1;
}
}
return left==letters.size()?letters[0]:letters[left];
//三目操作符 如果left=letters,size() 说明没有找到比这个字母大的字母 那么返回第一个字母
//否则 找到就返回对应字母
}
};
4、统计有序数组中的负数
力扣题单 1351
给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非严格递减顺序排列。 请你统计并返回 grid 中 负数 的数目。
示例 1:
输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
输出:8
解释:矩阵中共有 8 个负数。
示例 2:
输入:grid = [[3,2],[1,0]]
输出:0
class Solution {
public:
int countNegatives(vector<vector<int>>& grid) {
int m=grid.size()-1; //数组行数
int n=grid[0].size()-1; //数组列数
int left=0;
int right=n;
int res=0; //数组中负数的个数
for(int i=0;i<=m;i++){ //循环就是对每一行进行二分
left=0;
right=n;
while(left<=right){
int mid=left+((right-left)>>1);
if(grid[i][mid]>=0){
left=mid+1;
}else{
right=mid-1;
}
}
if(left<=n){
//如果left>n 说明在这一行没有找到负数
res+=(n-left+1);
}
}
return res;
}
};
5、在排序数组中查找元素的第一个位置和最后一个位置
力扣题单 34
给你一个按照非递减顺序排列的整数数组 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]
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int first=-1;
int last=-1;
vector<int> res;
//第一次二分找到第一个元素
//举个例子1 2 3 3 3 3 3 3 5 9
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
first=mid;
right=mid-1; //关键所在
//这一步的操作保证nums[first]==target
//并且更新之后的区间[left,mid-1]如果还存在target 那么会一直更新first
//保证target指向这个第一个元素
}else if(nums[mid]<target){
left=mid+1;
}else{
right=mid-1;
}
}
//第二次二分找到最后一个元素元素
left=0;
right=nums.size()-1;
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
last=mid;
left=mid+1;
}else if(nums[mid]<target){
left=mid+1;
}else{
right=mid-1;
}
}
res.push_back(first);
res.push_back(last);
return res;
}
};
二、旋转数组
1、搜索旋转排序数组
力扣题单 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 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
class Solution {
public:
//关键是找到轴点 也就是将数组一分为二的点 这样数组左右两侧都是有序的
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int temp=0;
//第一次二分找到轴点 比如 4 5 6 7 0 1 2这个数组中7的下标 这样就可以将数组一分为二
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[0]<=nums[mid]){
left=mid+1;
temp=mid;
}else{
right=mid-1;
}
}
//如果target小于第一个元素 说明其应该在轴点右侧
if(target<nums[0]){
left=temp+1;
right=nums.size()-1;
}else{
left=0;
right=temp;
}
//二分查找就行
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
right=mid-1;
}else{
left=mid+1;
}
}
return -1;
}
};
2、寻找排序数组中的最小值
力扣题单 153
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 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] ,旋转 3 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。 s
class Solution {
public:
//这题的关键还是找到轴点
int findMin(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
int res=0;
while(left<=right){
int mid=left+((right-left)>>1);
if(nums[0]<=nums[mid]){
res=mid;
left=mid+1;
}else{
right=mid-1;
}
}
//如果这个轴点元素是最后一个 说明整个数组就是递增的 那么返回第一个元素就可以
//不然就返回相应的下标元素
return (res+1==nums.size())?nums[0]:nums[res+1];
}
};
三、标准二分查找
1、猜数字大小
力扣题单 374
我们正在玩猜数字游戏。猜数字游戏的规则如下:
我会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。如果你猜错了,我会告诉你,我选出的数字比你猜测的数字大了还是小了。你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有三种可能的情况:
-1:你猜的数字比我选出的数字大 (即 num > pick)。
1:你猜的数字比我选出的数字小 (即 num < pick)。
0:你猜的数字与我选出的数字相等。(即 num == pick)。
返回我选出的数字。
示例 1:
输入:n = 10, pick = 6
输出:6
示例 2:
输入:n = 1, pick = 1
输出:1
示例 3:
输入:n = 2, pick = 1
输出:1
class Solution {
public:
int guessNumber(int n) {
int left=0;
int right=n;
while(left<=right){
int mid=left+((right-left)>>1);
if(guess[mid]==-1){
right=mid-1;
}else if(guess[mid]==1){
left=mid+1;
}else{
return mid;
}
}
return -1;
}
};
2、第一个错误的版本
力扣题单 278
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例 1:
输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
示例 2:
输入:n = 1, bad = 1
输出:1
class Solution {
public:
int firstBadVersion(int n) {
//这道题就是要找到第一个使得函数isBadVersion()==true的下标
int left=0;
int right=n;
int res=0;
while(left<=right){
int mid=left+((right-left)>>1);
if(isBadVersion(mid)){
res=mid;
right=mid-1;
//这一步和前面查找有序数组中的第一个元素和最后一个元素是一样的操作 异曲同工
//第一节的第五题
}else{
left=mid+1;
}
}
return res;
}
};
3、搜索二维矩阵
力扣题单 74
给你一个满足下述两条属性的 m x n 整数矩阵:
每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
示例一
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例二
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false 数组中不存在13
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m=matrix.size()-1;
int n=matrix[0].size()-1;
//首先二分找到对应的行
int left=0;
int right=m;
int temp=0;
//首先要找到第一个大于target的行 target应该就在这一个行的下一行
while(left<=right){
int mid=left+((right-left)>>1);
if(matrix[mid][0]<=target){
temp=mid;
left=mid+1;
}else{
right=mid-1;
}
}
//在该行内进行二分查找
left=0;
right=n;
while(left<=right){
int mid=left+((right-left)>>1);
if(matrix[temp][mid]==target){
return true;
}else if(matrix[temp][mid]>target){
right=mid-1;
}else{
left=mid+1;
}
}
return false;
}
};
四、数学
1、有效的完全平方数
力扣题单 367
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。不能使用任何内置的库函数,如 sqrt 。
示例 1:
输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 16 且 4 是一个整数。
示例 2:
输入:num = 14
输出:false
解释:返回 false ,因为 3.742 * 3.742 = 14 但 3.742 不是一个整数。
class Solution {
public:
bool isPerfectSquare(int num) {
int left=1;
int right=num;
while(left<=right){
int mid=left+((right-left)>>1);
long long int temp=(long long int)mid*mid; //防止溢出
if(temp==num){
return true;
}else if(temp>num){
right=mid-1;
}else{
left=mid+1;
}
}
return false;
}
};
2、x的平方根
力扣题单 69
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去.
class Solution {
public:
int mySqrt(int x) {
int left=1;
int right=x;
int res=0;
while(left<=right){
int mid=left+((right-left)>>1);
long long int temp=(long long int)mid*mid;
if(temp<=x){
res=mid;
left=mid+1;
}else{
right=mid-1;
}
}
return res;
}
};
3、排列硬币
力扣题单 441
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。
class Solution {
public:
int arrangeCoins(int n) {
int left=1;
int right=n;
int res=1;
while(left<=right){
int mid=left+((right-left)>>1);
long long int temp=(long long int)mid*(1+mid)/2;
if(temp<=n){
res=mid;
left=mid+1;
}else{
right=mid-1;
}
}
return res;
}
};
五、巧妙的不变量
1、H指数
力扣题单 275
给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。
h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少 有 h 篇论文分别被引用了至少 h 次。
请你设计并实现对数时间复杂度的算法解决此问题。
示例 1:
输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
由于研究者有3篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3 。
示例 2:
输入:citations = [1,2,100]
输出:2
class Solution {
public:
int hIndex(vector<int>& citations) {
int left=0;
int right=citations.size()-1;
int n=right;
int res=0;
while(left<=right){
int mid=left+((right-left)>>1);
if(citations[mid]>=(n-mid+1)){
res=n-mid+1;
right=mid-1;
}else{
left=mid+1;
}
}
return res;
}
};
2、有序数组中的单一元素
力扣题单 540
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。请你找出并返回只出现一次的那个数。你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
if(nums.size()==1){
return nums[0];
}
//这个数出现的下标一定是偶数位置
int left=0;
int right=nums.size()-1;
while(left<=right){
int mid=left+((right-left)>>1);
mid=mid-(mid&1);
//如果mid是偶数 那么mid&1就是0
//如果mid是奇数 那么mid&1就是1
if(nums[mid]==nums[mid+1]){
left=mid+2;
}else{
right=mid-1;
}
}
return nums[left];
}
};
/*由于只出现一次的元素所在下标 x 的左边有偶数个元素,因此下标 x 一定是偶数,可以在偶数下标范围内二分查找。二分查找的目标是找到满足 nums[x]!=nums[x+1] 的最小的偶数下标 x,则下标 x 处的元素是只出现一次的元素。*/
3、山脉数组的峰顶索引
力扣题单 852
给定一个长度为 n 的整数 山脉 数组 arr ,其中的值递增到一个 峰值元素 然后递减。返回峰值元素的下标。你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。
示例 1:
输入:arr = [0,1,0]
输出:1
示例 2:
输入:arr = [0,2,1,0]
输出:1
示例 3:
输入:arr = [0,10,5,2]
输出:1
这题是因为原题保证了数组是一个山脉数组 所以arr[mid+1]不会发生溢出
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
//这题就是找到最大的元素就可以了
int left=0;
int right=arr.size()-1;
int res=arr[0];
while(left<=right){
int mid=left+((right-left)>>1);
if(arr[mid]<arr[mid+1]){
res=mid+1;
left=mid+1;
}else{
right=mid-1;
}
}
return res;
}
};
2024.8.20