数组
本文对于数组中常见的题型给出了常见的解法
前言
对于时间空间复杂度不了解的同学可以看下:时间复杂度、空间复杂度快速入门
本文涉及算法:二分、双指针、滑动窗口
二分
- 二分:一般是有序的不重复的数组
题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
区间问题:
- 左闭右闭:时间复杂度 O(logn)
var search = function (nums, target) {
let left = 0;
let right = nums.length - 1; // 右闭
// 等号有意义,left === right
while (left <= right) {
let mid = left + ((right - left) >> 1); // 转为二进制,右移一位,再转为十进制
if (nums[mid] < target) {
left = mid + 1;
}
if (nums[mid] > target) {
right = mid - 1;
}
if (nums[mid] === target) {
return mid;
}
}
return -1;
};
- 左闭右开:时间复杂度 O(logn)
var search = function (nums, target) {
let left = 0;
let right = nums.length; // 右开
// 等号没意义
while (left < right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] < target) {
left = mid + 1;
}
if (nums[mid] > target) {
right = mid; // 保持右开
}
if (nums[mid] === target) {
return mid;
}
}
return -1;
};
移除元素
- 不使用额外空间,原地移除数组
题目:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
- 暴力解题 - 双层循环(时间复杂度为 O(N^2))(空间复杂度 O(1))
const remove = (nums, target) => {
for (let i = 0; i < nums.length; i++) {
if (nums[i] === target) {
nums.splice(i, 1);
i--; // 下标需要往后移一位
}
}
return nums.length;
};
- 双指针 快慢指针,如果快指针等于 target,慢指针不动,快指针前移; 如果快指针不等于 target,快慢指针交换位置的值,慢指针前移(重点在于这一步,相当于把 target 的值移除);
const remove = (nums, target) => {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (target !== nums[fast]) {
nums[slow] = nums[fast];
slow++; // 前移一位
}
}
return nums;
};
有序数组的平方
题目:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
- 暴力解题:先进行平方,再排序,时间复杂度:O(n + nlogn)
const sortedSquares = (nums) => {
for (let i = 0; i < nums?.length; i++) {
nums[i] = nums[i] * nums[i];
}
return nums.sort((a, b) => a - b);
};
- 双指针:需要开辟额外空间,时间复杂度 O(n),空间复杂度 O(n)
const sortedSquares = (nums) => {
let arr = new Array(nums.length).fill(0);
let k = nums.length - 1;
for (let i = 0, j = nums.length - 1; i <= j; ) {
if (nums[i] * nums[i] < nums[j] * nums[j]) {
arr[k--] = nums[j] * nums[j];
j--;
} else {
arr[k--] = nums[i] * nums[i];
i++;
}
}
return arr;
};
长度最小的子数组
题目:给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
- 暴力解题:双重循环,时间复杂度 O(n^2)
const minSubArrayLen = (nums, target) => {
let sum = 0;
let subLen = 0;
let result = Number.MAX_VALUE; // 初始赋值为 js 中的最大数
for (let i = 0; i < nums.length; i++) {
sum = 0; // 外层循环重置 sum,重新开始计算
for (let j = i; j < nums.length; j++) {
// 内层循环从 i 开始
sum = sum + nums[j];
if (sum >= target) {
// 大于目标值结束
subLen = j - i + 1;
result = result < subLen ? result : subLen; // 对比最小连续子数组长度
break;
}
}
}
return result === Number.MAX_VALUE ? 0 : result; // 如果 result 未进行赋值,直接返回 0
};
- 滑动窗口:在我们的双重循环上面改造一下下,进行优化,使得时间复杂度变成 O(n) 滑动有起始位置(其实也可以叫做双指针,但是它更类似于滑动的窗口,so...),只需要一个循环,终点位置就是遍历的下标,起点位置从 0 开始
const minSubArrayLen = (nums, target) => {
let sum = 0;
let subLen = 0;
let result = Number.MAX_VALUE; // 初始赋值为 js 中的最大数
let i = 0; // 起点位置
// j 为终点位置
for (let j = 0; j < nums.length; j++) {
sum = sum + nums[j];
// 滑动窗口的核心代码,
// 1.首先外面终点位置进行右移,sum 进行累加,当 >= target 时候,while 里面进行起点位置的右移,当不再 >= target 出 while 循环
// 2.出了 while 终点位置继续右移,重复第一步
// 注意:这边的 while 对每个元素只有一进一出两步操作,所以时间复杂度为 O(2n)
while (sum >= target) {
subLen = j - i + 1;
result = result < subLen ? result : subLen; // 对比最小连续子数组长度
sum = sum - nums[i++]; // 滑动核心代码,起点位置前进
}
}
return result === Number.MAX_VALUE ? 0 : result; // 如果 result 未进行赋值,直接返回 0
};