【js - 算法】数组篇

220 阅读2分钟

数组

本文对于数组中常见的题型给出了常见的解法

前言

对于时间空间复杂度不了解的同学可以看下:时间复杂度、空间复杂度快速入门

本文涉及算法:二分、双指针、滑动窗口

二分

  • 二分:一般是有序的不重复的数组

题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

leetcode 链接

区间问题:

  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;
};
  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) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

leetcode 链接

  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;
};
  1. 双指针 快慢指针,如果快指针等于 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,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

leetcode 链接

  1. 暴力解题:先进行平方,再排序,时间复杂度: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);
};
  1. 双指针:需要开辟额外空间,时间复杂度 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 。

leetcode 链接

  1. 暴力解题:双重循环,时间复杂度 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
};
  1. 滑动窗口:在我们的双重循环上面改造一下下,进行优化,使得时间复杂度变成 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
};