代码随想录算法训练营第一天 | 704. 二分查找、27. 移除元素
704. 二分查找
文档讲解:704. 二分查找
视频讲解:《代码随想录》算法视频公开课:手把手带你撕出正确的二分法
本题考查基础的二分查找,和暴力解法不同,根据数组的有序性,折半地缩小查找空间,达到快速查找的目的。
二分查找的思想很简单,但是在处理边界上容易出问题,归根结底是对搜索空间的定义不明确,导致查找时漏掉或者多加了元素。
常用的查找空间有两种,一种是左闭右闭,一种是左闭右开,现在分别对这两种实现方法做详细的实现。
左闭右闭
即区间内的元素全是未确定的元素,确定的元素不会在这里面(如要目标元素和一定不是目标的元素)。
class Solution {
public int search(int[] nums, int target) {
// 因为数组是有序的,所以小于最小值或大于最大值的元素一定不在数组中
if (target < nums[0] || target > nums[nums.length - 1]) return -1;
// 注意:左闭右闭区间,所以 right 起始为数组的最后一个元素
int left = 0;
int right = nums.length - 1;
// 注意:这里是小于等于,因为 left == right 时,搜索区间只有一个元素,仍然是合法的搜索区间,可能这一个元素就是目标值
while (left <= right) {
// 获取中间值,避免大数相加溢出
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
// 目标值在右边,要舍弃另一半查找空间,因为 nums[mid] 已经确定不是目标值,所以 mid 不应该被包含在区间内,因此 left 直接取 mid 的下一个位置
left = mid + 1;
} else if (nums[mid] > target) {
// 目标值在左边,要舍弃另一半查找空间,因为 nums[mid] 已经确定不是目标值,所以 mid 不应该被包含在区间内,因此 right 直接取 mid 的前一个位置
right = mid - 1;
}
}
return -1;
}
}
左闭右开
区间的定义和左闭右闭是一样的,但是因为右边是开区间,所以对 right 指针的处理会有些不同。
class Solution {
public int search(int[] nums, int target) {
// 因为数组是有序的,所以小于最小值或大于最大值的元素一定不在数组中
if (target < nums[0] || target > nums[nums.length - 1]) return -1;
// 注意:左闭右开区间,right 指向的元素并不在区间内,因此 right 起始为 nums.length
int left = 0;
int right = nums.length;
// 注意:这里是小于,因为是左闭右开区间,当 left == right 时,搜索区间为空,不应该再去循环了
while (left < right) {
// 获取中间值,避免大数相加溢出
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
// 和左闭右闭一样
left = mid + 1;
} else if (nums[mid] > target) {
// 因为是左闭右开区间,right 指向的是不在区间内的元素,此时的 nums[mid] 刚确定完不是目标值,要从区间内排除,因此将 mid 赋值给 right
// 且 mid - 1 可能是目标值,因此需要在区间内
right = mid;
}
}
return -1;
}
}
27. 移除元素
文档讲解:27. 移除元素
视频讲解:《代码随想录》算法视频公开课:数组中移除元素并不容易
题目要求在原数组上修改,所以是在考察数组的基本操作,因此只能采用覆盖的方法,因为数组的长度是固定的。
暴力解法
双层 for 循环遍历元素,如果是目标值,就将目标值后面的元素挨个往前覆盖,要注意循环结束的条件。
class Solution {
public int removeElement(int[] nums, int val) {
int len = nums.length;
// 随着目标值不断的覆盖,遍历的终点在不断前移,因此要 "提前" 结束循环,不能再遍历到 nums.length
for (int i = 0; i < len; i++) {
if (nums[i] == val) {
// 找到目标值,后面的元素往前覆盖,注意内层循环的结束条件是 len - 1,防止访问 j + 1 时越界
for (int j = i; j < len - 1; j++) {
nums[j] = nums[j + 1];
}
// 重点:删掉一个元素,长度 - 1
len--;
// 重点:因为当前元素已经被后面的元素覆盖了,此时的 i 指向的是覆盖的那个元素,下一轮循环还要对它做判断,所以 i 回退一次
i--;
}
}
// 遍历结束,len 就是删除指定元素后剩余元素的数量
return len;
}
}
双指针
暴力解法的时间复杂度是 O(n^2),时间花费在了挨个移动元素上,所以优化的重点就在这里,能否一次遍历解决问题。
双指针的思路是定义两个指针,初始都指向数组的第一个元素,left 指针表示该指针之前的元素都是非目标值,right 指针用来遍历数组,如果当前遍历的元素不是目标值,则将nums[right] 赋值给 nums[left],且 left、right 同时后移,如果当前遍历的元素是目标值,则 left 停顿,right 继续后移,重复上述循环。
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0, right = 0;
while (right < nums.length) {
// 重申一下,left 表示该位置之前的元素都是要留下的元素,不包含要删除的元素
// 所以这里要处理不等于目标值的元素
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}
}
这个双指针的思想很巧妙,一个指针用来控场(存储),一个指针用来遍历,符合条件的元素就扔进存储中,存储不断扩大,left 不断后移。