有关二分查找的讲解请前往:【算法】二分查找
704. 二分查找
[LeetCode]
简 单 \color{#00AF9B}{简单} 简单
给定一个
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:二分查找
由于数组已按照升序排列,则本题即为一道基础的二分查找题。
设left
和right
分别为数组的开始和起始位置,每次判断mid
位置的数是否等于target
。若等于,则返回mid
;若target
小于该数,则前往数组的左边进行寻找;若target
大于该数,则前往数组的右边进行寻找。
过程演示
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
#include <vector>
using namespace std;
class Solution
{
public:
int search(vector<int> &nums, int target)
{
for (int left = 0, right = nums.size() - 1; left <= right;)
{
int mid = ((right - left) >> 1) + left;
if (target == nums[mid])
return mid;
else if (target < nums[mid])
right = mid - 1;
else
left = mid + 1;
}
return -1;
}
};
复杂度分析
时间复杂度:O(logn)
空间复杂度:O(1),只需要常量空间保存若干变量
参考结果
Accepted
47/47 cases passed (28 ms)
Your runtime beats 84.69 % of cpp submissions
Your memory usage beats 54.85 % of cpp submissions (26.9 MB)
278. 第一个错误的版本
[LeetCode]
简 单 \color{#00AF9B}{简单} 简单
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有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
提示:
- 1 <= bad <= n <= 231 - 1
注意:
数组长度最高为231-1,使用int整型就足够表示。
方法1:二分查找
这一题不是查找确切的值,而是查找某个临界位置。我们可以改变left和right的改变时机,使得left和right最终重叠在我们所查找的临界点处。
本题可以抽象为如下的数组:
[
f
a
l
s
e
,
f
a
l
s
e
,
.
.
.
,
t
r
u
e
,
t
r
u
e
]
[false, false, ..., true, true]
[false,false,...,true,true]
我们的目标是运用二分查找找到第一个真值。首先,对中间项进行判断:
- 若[mid]为真,则代表中间项是错误版本,第一个错误版本要么在该项之前,要么就是它自身,因此我们将
right
置为mid
,而不是mid-1
,因为要从它自身开始往左边寻找。 - 若[mid]为假,则代表中间项是正确版本,第一个错误版本必定在它之后,因此
left
置为mid+1
。
最终,left
和right
必定会指向同一个元素,而该元素就是值为true的临界点,因为我们保留了true以进行二次判断,而直接舍弃了false的所有可能。由此可得出,循环的有效条件为left<right
。
过程演示
示例 1:
输入:n = 5, bad = 4
输出:4
bool isBadVersion(int version);
class Solution
{
public:
int firstBadVersion(int n)
{
int left = 1, right = n;
while (left < right)
{
int mid = ((right - left) >> 1) + left;
if (isBadVersion(mid))
right = mid;
else
left = mid + 1;
}
return left;
}
};
复杂度分析
时间复杂度:O(logn)
空间复杂度:O(1),只需要常量空间保存若干变量
参考结果
Accepted
22/22 cases passed (0 ms)
Your runtime beats 100 % of cpp submissions
Your memory usage beats 20.76 % of cpp submissions (5.9 MB)
35. 插入的位置
[LeetCode]
简 单 \color{#00AF9B}{简单} 简单
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为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
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:
输入: nums = [1], target = 0
输出: 0
提示:
- 1 <= nums.length <= 104
- -104 <= nums[i] <= 104
nums
为无重复元素的升序排列数组- -104 <= target <= 104
注意:
这一题和上一题类似,也是使用二分查找去寻找某个临界位置。
方法1:二分查找
这道题乍一看有两个条件:
- 若
target
在数组中则返回其索引; - 否则返回它将会被插入的位置。
但仔细思考一下可以发现这其实是一个问题。
我们举两个最简单的例子来说明一下。
- 在[1,5,7]中查找3:结果得出插入的位置为2
- 在[1,3,7]中查找3:结果得出插入的位置为2
对于例1,我们寻找到第一个比3大的数5
即可找到3要插入的位置,即寻找到5
后,需要将3插入当前5的位置。概括起来就是要找5的下标。
对于例2,我们找到等于3的数。即要找3的下标。
可以得到,我们只要找到第一个大于等于target
的位置,即是答案。
从另一个角度来说,我们找到最后一个小于target
的位置+1也能得到答案。
因此,按照上述思路,能够确定答案的方法有两个:
- 找到第一个大于等于
target
的位置 - 找到最后一个小于
target
的位置+1(因为要在这个位置的后一个位置上插入),若target在数组中则不需要+1
此处,我们采取更便捷的第一个方法。
我们要使用二分法不断逼近临界位置。在这过程中,每当nums[mid]
大于等于target
,它都有可能是最后的答案。紧接着各变量都要发生变化以进行下一次循环,所以,我们需要额外的一个变量ans
来存放当时的mid
。
还剩下一个问题就是ans
变量的初值。ans
变量在循环中是可以遍历到nums[0]
的。因此可得,我们只需要预判数组的右边界即可,即ans
变量的初值为数组长度,这代表着循环中每次判断结果都是target>nums[mid]
,即插入值大于数组中所有的值,需要插入到最后的位置。
过程演示
示例 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
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
#include <vector>
using namespace std;
class Solution
{
public:
int searchInsert(vector<int> &nums, int target)
{
int n = nums.size();
int left = 0, right = n - 1;
int ans = n;
while (left <= right)
{
int mid = ((right - left) >> 1) + left;
if (target <= nums[mid])
{
ans = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return ans;
}
};
复杂度分析
时间复杂度:O(logn)
空间复杂度:O(1),只需要常量空间保存若干变量
参考结果
Accepted
64/64 cases passed (0 ms)
Your runtime beats 100 % of cpp submissions
Your memory usage beats 95.81 % of cpp submissions (9.3 MB)
Animation powered by ManimCommunity/manim