@author LemonCat
@time 2023/7/26
@target 第一章 数组 part01 (qq.com)
今日任务
- 数组理论基础,704. 二分查找,27. 移除元素
数组理论基础
-
定义:数组是存放在连续内存空间上的相同类型数据的集合
- 数组下标都是从 0 开始的。
- 数组内存空间的地址是连续的
- 数组中不支持直接删除元素,只能进行覆盖操作
-
二维数组
- int [first][second] first 指的是行,second 指的是列
- 可以想成是行列式
704. 二分查找
题目建议:大家能把 704 掌握就可以,35.搜索插入位置 和 34. 在排序数组中查找元素的第一个和最后一个位置 ,如果有时间就去看一下,没时间可以先不看,二刷的时候在看。
先把 704 写熟练,要熟悉 根据 左闭右开,左闭右闭 两种区间规则 写出来的二分法。
思想
二分法:
-
思想 - 不断缩小目标范围去寻找目标
-
前提 - 顺序数组
-
使用方法
- 寻找区间 - 左闭右闭
- 寻找区间 - 左闭右开
- 也可以有其他方法 - 左开右闭(不常用)
方法一:左闭右闭
-
int right = nums.length - 1
- 左闭右闭 因此右边在寻找范围内 即为 nums.length - 1
-
left <= right 是因为 left 和 right 相等的时候是合法的
- 因为区间是 左闭右闭 -> [left, right] => 例如:[1, 1] - 合法的
-
当 nums[mid]小于目标值的时候 - 目标值在右边 left = mid + 1
- 因为搜索区间是 [left, right],middle 已经不等于目标 -> 这样可以减少重复寻找
-
当 nums[mid]大于目标值的时候 - 目标值在左边 right = mid - 1
- 因为搜索区间是 [left, right],middle 已经不等于目标
-
若 nums[mid]与目标值相等时,就直接返回中间值
/**
* 方法一 - 左闭右闭
*/
class Solution {
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]) { // 若目标值不再数组范围内 直接返回 减少循环
return -1;
}
int left = 0;
int right = nums.length - 1; // 左闭右闭 因此右边在寻找范围内 即为 nums.length - 1
while (left <= right) { // [left, right]
int mid = left + ((right - left) >> 1); // 1. 防止溢出 2. 位运算计算速度更快
if (target < nums[mid]) {
right = mid - 1;
} else if (target > nums[mid]) {
left = mid + 1;
} else if (target == nums[mid]) {
return mid;
}
}
return -1;
}
}
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
方法二:左闭右开
-
int right = nums.length
- 左闭右开 因此右边不在寻找范围内 即为 nums.length
-
left < right left 和 right 相等的时候是不合法的
- 因为区间是 左闭右开 -> [left, right) => 例如:[1, 1) - 不合法的
-
当 nums[mid]小于目标值的时候 - 目标值在右边 left = mid
- 因为搜索区间是 [left, right),middle 虽然不等于目标 但由于右边开 本质上搜索区间不包含右侧
-
当 nums[mid]大于目标值的时候 - 目标值在左边 right = mid - 1
- 因为搜索区间是 [left, right]),middle 已经不等于目标
-
若 nums[mid]与目标值相等时,就直接返回中间值
class Solution {
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]) { // 若目标值不再数组范围内 直接返回 减少循环
return -1;
}
int left = 0;
int right = nums.length; // 左闭右开 因此右边不在寻找范围内 即为 nums.length
while (left < right) { // [left, right)
int mid = left + ((right - left) >> 1); // 1. 防止溢出 2. 位运算计算速度更快
if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
} else if (target == nums[mid]) {
return mid;
}
}
return -1;
}
}
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
27. 移除元素
题目建议: 暴力的解法,可以锻炼一下我们的代码实现能力,建议先把暴力写法写一遍。 双指针法 是本题的精髓,今日需要掌握,至于拓展题目可以先不看。
方法一 - 暴力解法
-
双层 for 循环
- 第一个 遍历数组元素
- 第二个 更新数组 - 将后面的值移动到前面
/**
* 方法一 - 暴力解法
*/
class Solution {
public int removeElement(int[] nums, int val) {
int size = nums.length; // 记录原数组长度(返回的)
for (int i = 0; i < size; i++) { // 发现需要移除的元素,就将数组集体向前移动一位
if (nums[i] == val) {
// 第二个for循环,将后续数组集体前移 - nums[j] = nums[j + 1]
// 即当 j 初始为 i - 注意 j < size - 1 否则会数组下标越界 OOE
for (int j = i; j < size - 1; j++) {
nums[j] = nums[j + 1];
}
i--; // 因为下标 i 以后的数值都向前移动了一位,所以 i 也向前移动一位 否则则会跳过一个值
size--; // 此时数组的大小-1 - 因为向前移动了
}
}
return size;
}
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
方法二 - 双指针法
-
双指针思路:
- 快指针 遍历数组
- 慢指针 只有遍历到不是目标值时向前移动 并将我们需要的数组 快指针的值 替换 慢指针的值
-
双指针法(快慢指针法): 通过一个快指针和慢指针在一个 for 循环下完成两个 for 循环的工作
定义快慢指针
- 快指针:寻找新数组的元素 - 新数组就是不含有目标元素的数组
- 慢指针:指向要更新的元素 - 新数组下标的位置
/**
* 方法二 - 双指针
*/
class Solution {
public int removeElement(int[] nums, int val) {
int fast = 0; // 快指针
int slow = 0; // 慢指针
for (int i = 0; i < nums.length; i++) {
if (nums[i] != val) { // 若果不是要移出的值 - 快指针值 赋给 慢指针值 慢指针++
nums[slow] = nums[fast];
slow++;
}
fast++; // 无论什么条件 快指针永远向前进
}
return slow;
}
}
-
我们发现 上面的代码有很多逻辑重复的地方 -> 对上面代码进行优化
- 遍历循环的索引 i 本质就是 fast 快指针
- 慢指针 ++ 的过程 可以放到数组中 -> 代码更加简洁
/**
* 方法二 - 双指针 PLUS
*/
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0; // 慢指针
for (int fast = 0; fast < nums.length; fast++) { // 快指针
if (nums[fast] != val) { // 若果不是要移出的值 - 快指针值 赋给 慢指针值 慢指针++
nums[slow++] = nums[fast];
}
}
return slow;
}
}
- 结束 - 门前大桥下 游过一群鸭 快来快来数一数 全是柯尔鸭 ~ 明天继续加油!