实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 :
输入: 4
输出: 2
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
class Solution {
public:
int mySqrt(int x) {
int l = 0, r = x;
while(l < r){
int mid = l + (long long)r + 1>> 1;
if(mid <= x/mid) l = mid; // 查找t^2 <= x
else r = mid - 1;
}
return l;
}
};
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 :
输入: [1,3,5,6], 5
输出: 2
输入: [1,3,5,6], 2
输出: 1
输入: [1,3,5,6], 7
输出: 4
输入: [1,3,5,6], 0
输出: 0
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if(nums.empty() || target > nums.back()) return nums.size();
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid; // 查找t>=x的第一个数
else l = mid + 1;
}
return l;
}
};
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 :
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1, -1};
return {getFirst(nums, target), getLast(nums, target)};
}
int getFirst(vector<int>& nums, int target){
int l = 0, r = nums.size()-1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
return nums[l] != target ? -1 : l;
}
int getLast(vector<int>& nums, int target){
int l = 0, r = nums.size()-1;
while(l < r){
int mid = l + r + 1 >> 1;
if(nums[mid] <= target) l = mid;
else r = mid - 1;
}
return nums[l] != target ? -1 : l;
}
};
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
示例 :
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3
输出:true
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 13
输出:false
输入:matrix = [], target = 0
输出:false
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty() || matrix[0].empty()) return false;
int n = matrix.size(), m = matrix[0].size();
int l = 0, r = n*m-1;
while(l < r){
int mid = l + r >> 1;
if(matrix[mid/m][mid%m] >= target) r = mid; // t>=x
else l = mid + 1;
}
return matrix[l/m][l%m] == target ? true : false;
}
};
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
示例 :
输入: [3,4,5,1,2]
输出: 1
输入: [4,5,6,7,0,1,2]
输出: 0
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] <= nums.back()) r = mid; // nums[t]<=nums.back
else l = mid + 1;
}
return nums[l];
}
};
给你一个升序排列的整数数组 nums ,和一个整数 target 。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
示例 :
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
输入:nums = [1], target = 0
输出:-1
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
// 找到最小值
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] <= nums.back()) r = mid; // nums[t] <= nums.back()
else l = mid + 1;
}
if(target <= nums.back()) r = nums.size() - 1;
else l = 0, r--;
// 单调二分
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid; // t >= x
else l = mid + 1;
}
return nums[l] == target ? l : -1;
}
};
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
int l = 1, r = n;
while(l < r){
int mid = l + (long long)r >> 1; // n为最大整数,溢出
if(isBadVersion(mid)) r = mid;
else l = mid + 1;
}
return l;
}
};
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。
示例 :
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
说明:
你的解法应该是 O(logN) 时间复杂度的。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
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;
}
};
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 :
输入: [1,3,4,2,2]
输出: 2
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size()-1;
int l = 1, r = n;
while(l < r){
int cnt = 0;
int mid = l + r >> 1;
for(int x : nums)
if(x <= mid) cnt++; // 统计中位数及左边数字的个数
if(cnt > mid) r = mid;
else l = mid+1;
}
return l;
}
};
给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照 升序排列 。编写一个方法,计算出研究者的 h 指数。
h 指数的定义: “h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (N 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。(其余的 N - h 篇论文每篇被引用次数不多于 h 次。)"
示例:
输入: citations = [0,1,3,5,6]
输出: 3
解释: 给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
由于研究者有 3 篇论文每篇至少被引用了 3 次,其余两篇论文每篇被引用不多于 3 次,所以她的 h 指数是 3。
说明:
如果 h 有多有种可能的值 ,h 指数是其中最大的那个。
思路:
class Solution {
public:
int hIndex(vector<int>& nums) {
int l = 0, r = nums.size();
while(l < r){
int mid = l + r + 1>> 1;
if(nums[nums.size() - mid] >= mid) l = mid; // 左边都满足nums[h]>=h,判断倒数第h个数是否≥h
else r = mid - 1;
}
return l;
}
};
参考链接:
扩展分类
练习题
题型一:在数组中查找符合条件的元素的下标
一般而言这个数组是有序的,也可能是半有序的(旋转有序数组或者山脉数组)。
题目 | 提示与题解 |
---|---|
704. 二分查找(简单) | 二分查找的最原始问题,使用本题解介绍的方法就要注意,需要后处理。 |
34. 在排序数组中查找元素的第一个和最后一个位置(中等) | 查找边界问题,题解(有视频讲解)。 |
33. 搜索旋转排序数组(中等) | 题解,利用局部单调性,逐步缩小搜索区间(其它问题类似) |
81. 搜索旋转排序数组 II(中等) | 题解 |
153. 寻找旋转排序数组中的最小值(中等) | 题解 |
154. 寻找旋转排序数组中的最小值 II(中等) | 题解 |
300. 最长上升子序列(中等) | 特别经典的一道「动态规划」,二分查找的思路是基于「动态规划」的状态定义得到,代码很像第 35 题,题解。 |
275. H 指数 II(中等) | 题解 |
852. 山脉数组的峰顶索引(简单) | 利用局部单调性,逐步缩小搜索区间。 |
1095. 山脉数组中查找目标值(中等) | 官方题解(有视频讲解),题解 |
4. 寻找两个正序数组的中位数(困难)) | 官方题解(有视频讲解),题解 |
658. 找到 K 个最接近的元素(中等) | 题解,这个问题二分的写法需要做复杂的分类讨论,可以放在以后做 |
题型二:在一个有范围的区间里搜索一个整数
定位一个有范围的整数,这件事情也叫「二分答案」或者叫「二分结果」。如果题目要求的是一个整数,这个整数有明确的范围,可以考虑使用二分查找。
题目 | 提示与题解 |
---|---|
69. 平方根(简单) | 题解,在一个整数范围里查找一个整数,也是二分查找法的应用场景。 |
287. 寻找重复数(中等) | 题解,在一个整数范围里查找一个整数。这个问题二分查找的解法很反常规,知道即可。 |
374. 猜数字大小(简单) | 题解 |
1300. 转变数组后最接近目标值的数组和 | 题解 |
题型三:复杂的二分查找问题(判别条件需要遍历数组)
「力扣」上还有这样一类问题:目标变量和另一个变量有相关关系(一般而言是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测(满足某种意义上的单调性)。这样的问题的判别函数通常会写成一个函数的形式。
这一类问题可以统称为「 最大值极小化 」问题,最原始的问题场景是木棍切割问题,这道题的原始问题是「力扣」第 410 题。
解题的思路是这样的:
以下给出的问题无一例外。
题目 | 提示与题解 |
---|---|
410. 分割数组的最大值(困难) | 题解 |
875. 爱吃香蕉的珂珂(中等) | 题解 |
LCP 12. 小张刷题计划(中等) | (题解在第 410 题题解里) |
1482. 制作 m 束花所需的最少天数(中等) | (题解在第 1300 题题解里) |
1552. 两球之间的磁力(中等) |
LeetBook总结
704. 二分查找(简单)
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
return nums[l] == target ? l : -1;
}
};
374. 猜数字大小
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is lower than the guess number
* 1 if num is higher than the guess number
* otherwise return 0
* int guess(int num);
*/
class Solution {
public:
int guessNumber(int n) {
int l = 1, r = n;
while(l < r){
int mid = l + (long long)r >> 1;
int res = guess(mid);
if(res <= 0) r = mid;
else l = mid + 1;
}
return l;
}
};
81. 搜索旋转排序数组 II(中等)
【分析】与lc33类似,多了重复数字的情况,只需去掉旋转数组结尾和开头相同的部分,剩下的与lc33完全相同。
class Solution {
public:
bool search(vector<int>& nums, int target) {
if(nums.empty()) return false;
// 去掉结尾和开头相同的部分
int t = nums.size() - 1;
while(t > 0 && nums[t] == nums[0]) t--;
// 找最小值
int l = 0, r = t;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] <= nums[t]) r = mid;
else l = mid + 1;
}
if(target <= nums[t]) r = t; // 答案在右边
else l = 0, r--; // 答案在左边
// 单调二分
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
return nums[l] == target;
}
};
154. 寻找旋转排序数组中的最小值 II(中等)
【分析】与lc153类似,多了重复数字的情况,只需去掉旋转数组结尾和开头相同的部分,剩下的与lc153完全相同。
class Solution {
public:
int findMin(vector<int>& nums) {
int t = nums.size()-1;
while(t > 0 && nums[t] == nums[0]) t--; // 去掉结尾与开头相同的部分
int l = 0, r = t;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] <= nums[t]) r = mid;
else l = mid + 1;
}
return nums[l];
}
};
852. 山脉数组的峰顶索引(简单)
【思路】找峰顶,判断是否有 M ≥ M+1
class Solution {
public:
int peakIndexInMountainArray(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= nums[mid+1]) r = mid; // M >= M+1
else l = mid + 1;
}
return l;
}
};
1095. 山脉数组中查找目标值(中等)
【思路】先找到峰顶,左右区间找target
/**
* // This is the MountainArray's API interface.
* // You should not implement it, or speculate about its implementation
* class MountainArray {
* public:
* int get(int index);
* int length();
* };
*/
class Solution {
public:
int findInMountainArray(int target, MountainArray &nums) {
// 找到峰顶,最大值
int l = 0, r = nums.length() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums.get(mid) >= nums.get(mid+1)) r = mid;
else l = mid + 1;
}
int t = l; // t为峰顶
// 在左侧递增区间找target
l = 0, r = t;
while(l < r){
int mid = l + r >> 1;
if(nums.get(mid) >= target) r = mid;
else l = mid + 1;
}
if(nums.get(l) == target) return l;
// 在右侧递减区间找target
l = t + 1, r = nums.length() - 1;
while(l < r){
int mid = l + r >> 1;
if(nums.get(mid) <= target) r = mid;
else l = mid + 1;
}
if(nums.get(l) == target) return l;
else return -1;
}
};
658. 找到 K 个最接近的元素(中等)
【思路】只寻找左边界即可。
假设 mid 是左边界,则当前区间覆盖的范围是 [mid, mid + k -1]. 如果发现 a[mid] 与 x 距离比 a[mid + k] 与 x 的距离要大,说明解一定在右侧,否则在左侧。
class Solution {
public:
vector<int> findClosestElements(vector<int>& nums, int k, int x) {
int l = 0, r = nums.size() - k; // 保证mid+k不超出范围
while(l < r){
int mid = l + r >> 1;
if(x - nums[mid] <= nums[mid+k] - x) r = mid;
else l = mid + 1;
}
return vector<int>(nums.begin()+l, nums.begin()+l+k);
}
};
1300. 转变数组后最接近目标值的数组和
【思路】二分查找确定这个整数值(阈值越大,转变数组和越大,具有单调性,可用二分,对比lc658)
如果选择一个阈值 value
,使得它对应的 sum
是第 1 个大于等于 target
的,那么目标值可能在 value
也可能在 value - 1
class Solution {
public:
int findBestValue(vector<int>& arr, int target) {
int l = 0, r = 0;
for(int num : arr)
r = max(r, num); // 找数组中的最大值作为右边界
// 二分找到第一个大于等于target的阈值val
while(l < r){
int mid = l + r >> 1;
int sum = calSum(arr, mid);
if(sum >= target) r = mid;
else l = mid + 1;
}
// 找最接近target左右两边最小的数
int sum1 = calSum(arr, l - 1);
int sum2 = calSum(arr, l);
if(target - sum1 <= sum2 - target) // k-x1 <= x2-k
return l - 1;
return l;
}
int calSum(vector<int>& arr, int val){ // 计算转变数组的和
int sum = 0;
for(int num : arr)
sum += min(num, val);
return sum;
}
};