704. 二分查找
二分查找针对有序数组(示例中为升序,假设无重复元素),使用左右双指针指定查找区间,middle指针将区间分为左右区间,通过比较nums[middle]的值和target值的相对大小,定位target在左区间、middle或右区间,因此称为“二分查找”
给定一个 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],因此初始化时right=nums.size()-1,而循环条件为left<=right,当区间中点值大于target值时,middle值必然不在有效搜索区间,故right=middle-1
// 时间复杂度:O(log n)
// 空间复杂度:O(1)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭区间里
while (left <= right) { // 当left==right, 区间不为空集,故用<=
int middle = left + (right - left) / 2; // 防止溢出,等同于(left + right) / 2
if (nums[middle] > target) {
right = middle - 1; // target在左区间,即[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target在右区间,即[middle + 1, right]
} else { // num[middle] == target
return middle;
}
}
// 未找到目标值
return -1;
}
};
2. 左闭右开区间法
有效搜索区间不包含右端点,即[left, right),因此初始化时right=nums.size(),而循环条件为left<right,当区间中点值大于target值时,middle值必然不在有效搜索区间,而right也不在,故right=middle。试想当right=left+2,也就是左右指针和middle指针指向的位置为三个连续的元素,若此时若nums[middle]>target,而令right=middle-1=left,此时跳出循环,返回-1,而num[left]==target是有可能的,这种情况就被漏掉了,这就是right为什么更新为middle而不是middle-1的原因
// 时间复杂度:O(log n)
// 空间复杂度:O(1)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() ; // 定义target在左闭右开区间里
while (left < right) { // 当left==right, 区间为空集,故用<
int middle = left + (right - left) / 2; // 防止溢出,等同于(left + right) / 2
if (nums[middle] > target) {
right = middle; // target在左区间,即[left, middle)
} else if (nums[middle] < target) {
left = middle + 1; // target在右区间,即[middle + 1, right]
} else { // num[middle] == target
return middle;
}
}
// 未找到目标值
return -1;
}
};
3. 相似题目
(1) 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
这道题和二分查找是一样的,区别只是当为搜索到target时,需要返回合适的插入位置,而不是-1,于是就要分析当搜索失败时,left和right指针的相对关系,
1)左闭右闭区间
区间随着搜索不断缩小,直到left=right(可以从剩余三个元素或两个元素开始分析,结论是一样的),此时middle=left=right, 于是当nums[middle] > target时, right=middle-1, 应该在left位置插入target值;当nums[middle] < target时,left = middle + 1, 也是在left位置插入(两种情况都会导致left>right,跳出循环)。综上,将最后的return -1修改为return left即可。
2)左闭右开区间
区间随着搜索不断缩小,直到right=left+1。此时middle=left,于是当nums[middle] > target时,更新right,令 right=middle, 此时三指针重合,插入位置选left/right都可以;当nums[middle] < target时,令left = middle + 1, 此时left、right重合,插入位置选left/right都可以(两种情况都会导致left=right,跳出循环)。综上,将最后的return -1修改为return left或right均可。
(2) 34.在排序数组中查找元素的第一个和最后一个位置
注意这道题的预设条件发生了变化,数组虽然仍是升序但存在重复元素,因此第一个和最后一个位置分开查找逻辑比较清晰。以左闭有闭区间二分查找为例
第一个位置:类似无重复元素的二分查找,只是右边界的更新条件修改为nums[middle]>=target,也就是middle值大于等于target时,都要让right=middle-1。target存在时,nums[middle]=target成立,但此时仍然执行了right=middle-1,直到循环结束后right+1指向了第一个位置。于是选择first_index=right+1。
最后一个位置:左边界的更新条件修改为nums[middle]<=target,也就是middle值小于等于target时,都要让left=middle+1。target存在时,nums[middle]=target成立,但此时仍然执行了left=middle+1,直到循环结束后left-1指向了最后一个位置。于是选择final_index=left-1
三种情况
情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时first_index或final_index没有被赋值过,应该返回{-1, -1}
情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
// 两次二分查找分别寻找第一个最后一个位置
// 时间复杂度O(log n)
// 空间复杂度O(1)
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int first_index = getFirstIndex(nums, target);
int final_index = getFinalIndex(nums, target);
// 情况一
if (first_index == -2 || final_index == -2) return {-1, -1};
// 情况三
if (final_index - first_index > 1 ) return {first_index + 1, final_index - 1};
// 情况二
return {-1, -1};
}
private:
int getFirstIndex(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 左闭右闭区间二分查找
int first_index = -2; // 记录一下没有被赋值的情况,这是为了区分情况一和情况二
// 这两种情况都对应了target不存在,最终final_index-first_index值是一样的;
while (left <= right) {
int middle = left + (right - left) / 2;
if (nums[middle] >= target) { //中间值大于等于target都更新right
right = middle - 1;
first_index = right; // target存在时,right会一直左移到第一个target之前
// 上边如赋值为right+1, getFinalIndex中赋值为left-1
// 则target存在时,final_index-first_index>=0
// 而情况二下,final_index-first_index = 1,即无法区分情况二三
} else if (nums[middle] < target)
{
left = middle + 1;
}
}
return first_index;
}
int getFinalIndex(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 左闭右闭区间二分查找
int final_index = -2; // 记录一下没有被赋值的情况,区分情况一二
while (left <= right) {
int middle = left + (right - left) / 2;
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] <= target) { // //中间值小于等于target都更新left
left = middle + 1;
final_index = left; // target存在时,left会一直左移到最后一个target之后
// 注意上边final_index没有赋值为left - 1, 是为了区分情况二三
}
}
return final_index;
}
};
27. 移除元素
给你一个数组
nums
和一个值val
,你需要 原地 移除所有数值等于val
的元素。元素的顺序可能发生改变。然后返回nums
中与val
不同的元素的数量。假设
nums
中不等于val
的元素数量为k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。- 返回
k
。
1. 暴力法
使用两个for循环,第一个for循环 遍历每个数组元素值,判断是否等于target。当相等时,第二个for循环用于将该元素之后的所有元素位置向前移一位,覆盖要移除的元素
// 暴力法:双for循环
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size(); // 这里的size不只是记录初始大小,更重要的是随着元素的移除,size大小是会变化的,但nums.size()不会变
for (int i = 0; i < size; i++) {
if (nums[i] == val) {
for (int j = i + 1; j < nums.size(); j++) {
nums[j - 1] = nums[j]; // 只要求前边的元素不包含val,直接覆盖前边的元素,而不用处理最后部分的元素,这也是前文说nums.size()不会变的原因
}
i--; // 下标i以后的数值都向前移动了一位,所以i也前移一位
size--; // 此时数组的大小减一
}
}
return size;
}
};
2. 快慢指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:只记录不等于val的元素,因此当nums[fast_index] != val时,令nums[slow_index] = nums[fast_index],并且slow_index + 1
// 快慢指针法:单for循环
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow_index = 0;
for (int fast_index = 0; fast_index < nums.size(); fast_index++) {
if (nums[fast_index] != val) {
nums[slow_index++] = nums[fast_index]; //只记录不等于val的数值
}
}
return slow_index;
}
};