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 提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
经典的二分查找题,通常我们遇到二分查找题的时候会有以下基本思路:
- 初始化两个指针:left 和 right,分别指向数组的第一个元素和最后一个元素。
- 计算中间位置:mid = (left + right) / 2。
- 比较中间元素与目标值:
- 如果 nums[mid] 等于 target,则返回 mid。
- 如果 nums[mid] 小于 target,说明目标值在 mid 的右侧,因此将 left 设置为 mid + 1。
- 如果 nums[mid] 大于 target,说明目标值在 mid 的左侧,因此将 right 设置为 mid - 1。
- 重复上述步骤,直到 left 大于 right,这意味着目标值不存在于数组中,返回-1。
但此时需要明确一个问题:区间的开闭如何选择,这决定我们在处理边界的时候要做什么样的操作,一旦确定不做更改。我采用了全闭区间的方式
以下是题解
class Solution {
public:
int search(vector<int>& nums, int target) {
// 初始化左右边界
int left=0,right=nums.size()-1;
// 计算初始中间位置,使用 (right - left) / 2 避免整数溢出
int mid=left + ((right - left) / 2);
// 当左边界不大于右边界时,继续查找
while(left<=right){
// 如果目标值小于中间值,更新右边界并重新计算中间位置
if(target<nums[mid]){
right=mid-1;
mid=left + ((right - left) / 2);
}
// 如果目标值大于中间值,更新左边界并重新计算中间位置
else if(target>nums[mid]){
left=mid+1;
mid=left + ((right - left) / 2);
}
// 如果目标值等于中间值,返回中间位置
else
return mid;
}
// 如果循环结束还没有找到目标值,返回-1
return -1;
}
};
我使用mid=left + ((right - left) / 2)代替了mid=(right + left) / 2,有时候可能不太容易想到这个等价的式子,下面是思路:
为什么这样做?
当 left 和 right 都是大整数时,直接相加可能会导致整数溢出。例如,在32位整数环境中,如果 left 和 right 都接近 INT_MAX,那么 left + right 就会溢出。
通过使用 left + (right - left) / 2,我们避免了这种直接相加的情况,从而避免了整数溢出的风险。
让我们从数学上深入理解这个表达式:
- 传统的平均值计算:
- 展开这个平均值:
这就是我们的表达式
left + (right - left) / 2。
27.移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。 示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 你不需要考虑数组中超出新长度后面的元素。
这个问题可以通过双指针技术来解决。我们可以使用一个慢指针来跟踪新数组的长度,和一个快指针来遍历原数组。以下是基本思路:
- 初始化两个指针:i = 0(慢指针)和 j = 0(快指针),其中 i 用于跟踪新数组的长度,j 用于遍历原数组。
- 遍历数组:使用快指针 j 遍历数组,检查每个元素是否等于给定的值 val。
- 元素不等于给定值:如果 nums[j] 不等于 val,我们将 nums[j] 复制到 nums[i] 的位置,并将慢指针 i 增加 1。
- 元素等于给定值:如果 nums[j] 等于 val,我们什么都不做,只是将快指针 j 增加 1,以检查下一个元素。
- 返回新数组的长度:遍历完成后,i 将表示新数组的长度。我们返回 i 作为结果。
题解如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 初始化慢指针 slow,用于跟踪新数组的长度
int slow = 0;
// 使用快指针 fast 遍历数组
for (int fast = 0; fast < nums.size(); fast++) {
// 如果当前元素不等于给定值
if (nums[fast] != val) {
// 将当前元素复制到慢指针的位置
nums[slow] = nums[fast];
// 增加慢指针的值,表示新数组的长度增加了
slow++;
}
// 如果当前元素等于给定值,快指针继续前进,不做任何操作
}
// 返回新数组的长度
return slow;
}
};
同样可以使用快慢指针法的题型:
- 判断链表是否有环:
- 使用两个指针,一个快指针每次移动两步,一个慢指针每次移动一步。如果链表中存在环,那么快指针最终会追上慢指针。
- 找到链表的中点:
- 同样使用快慢指针,当快指针到达链表的末尾时,慢指针恰好在链表的中点。
- 找到链表的倒数第 k 个节点:
- 使用两个指针,先让快指针移动 k 步,然后快慢指针同时移动,当快指针到达链表末尾时,慢指针指向的就是倒数第 k 个节点。
- 数组中找到两个数的和为给定值:
- 对于有序数组,使用两个指针从数组的两端开始,根据两指针指向的数字之和与目标值的大小关系来移动指针。
- 移除数组中的重复元素:
- 使用两个指针,一个指针用于遍历数组,另一个指针用于指向下一个非重复元素应该插入的位置。
- 找到数组中的最大/最小子数组/子序列:
- 使用两个指针来定义子数组/子序列的开始和结束位置。
- 滑动窗口问题:
- 例如,找到数组中和为给定值的连续子数组,或找到数组中的最小子数组长度等问题。