【前端算法系列】数组

数组

  • 查找:通过下标找值 O(1)复杂度
  • 插入:插入后,后面的元素要往后移动 O(n)几重循环,如果插到最后一位是O(1)
  • 删除:删除后,后面的元素往前移动(复杂度同插入)

插入和删除非多,可以用链表来改善

常用:

  1. 双指针方法(用来缩小范围):用在涉及求和、比大小类的数组题目(此类题目前提是该数组必须有序。否则双指针根本无法帮助我们缩小定位的范围,压根没有意义,所以要提前排序)

在这里插入图片描述

// 数组技巧:

// 为什么不直接arr.length<l,因为防止数组在遍历过程中有长度变化
for(let i=0,l=arr.length;i<l;i++){} 

★ 移动零

function solution(arr){
    let j=0
    for(let i=0;i<arr.length;i++){
        if(arr[i]!==0){
            arr[j] = arr[i]
            if(i!=j){
                arr[i] = 0  // [1, 0, 0, 3, 12]  [1, 3, 0, 0, 12] ...
            }
            j++
        }
    }
    return arr
}
console.log(  solution([0,1,0,3,12])) // [1, 3, 12, 0, 0]

合并两个有序数组

1)因为是有序数组,所以用两个指针,各指向两个数组生效部分的尾部
2)每次只对指针所指向的元素进行比较,取最大的,push到nums1尾部
3)边界判断:

  • 如果遍历完nums1,剩下nums2,即nums2的都比nums1的小,直接不到nums1前面去
  • 如果遍历完nums2,剩下nums1,由于容器本身是nums1,所以不必做任何额外操作
let nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
const merge = function(nums1, m, nums2, n) {
  let i = m - 1, j = n - 1, k = m + n - 1
  while(i>=0&& j>=0){
    if(nums1[i]>=nums2[j]){
      nums1[k]=nums1[i]
      i--
      k--
    } else {
      nums1[k] = nums2[j]
      j--
      k--
    }
  }

  // nums2剩余的情况,特殊处理一下 
  while(j>=0){
    nums1[k] = nums2[j]
    k--
    j--
  }
}

merge(nums1, m, nums2, n)
console.log(nums1)

盛水最多的容器

思路:
方法一:定义一个max变量,先让第一根柱子遍历,去跟后面的柱子组成面积,把组成最大的面积记录到max里,然后遍历完成后第二根去遍历后面的柱子,也是组成面积,求最大面积的,最后会得到一个最大面积

方法二:左右边界向中间收敛,取第一根柱子和最后一根柱子,让他们往里面走,谁的高度小,就往里面走(左右夹逼方法)

function maxArea(arr) {
    let max = 0
    for (let i = 0, j=arr.length-1; i<j;) {
        let minHeight = arr[i]<arr[j]? arr[i++]:arr[j--]
        max = Math.max(max, (j-i+1)*minHeight)
    }
    return max
}
console.log(maxArea([1,8,6,2,5,4,8,3,7]))

两数之和(求和转求差,空间换时间,使用map)

a+b = target 且不重复

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

  • 方法一:暴力求解,两层循环遍历,枚举a+枚举b=target
  • 方法二:用哈希表存储,后面查询时间复杂度为O(1):比如9-2等于7,再遍历循环有没有跟7相等的
function twoSum(nums, target){
    // 1. 定义哈希和空数组,遍历目标数组,把值、索引放到map作为key、val
    let map = new Map()
    let arr = []
    for(let i=0;i<nums.length;i++){
        map.set(nums[i], i)
    }
    /* 2. 循环数组,找出可以被target相减的,且相减完的数组不等于循环j变量的,
        说明map.get(nums[j])+map.get(target - nums[j]) = target
    */
    for(let j=0;j<nums.length;j++){
        if(map.has(target-nums[j]) && map.get(target - nums[j])!=j )
        arr.push(j, map.get(target - nums[j]))
        return arr
    }
}
console.log(twoSum([2, 7, 11, 15], 9))

这种不用设置map,用对象表示

const twoSum = function(nums, target) {
    // 这里我用对象来模拟 map 的能力
    const map = {}
    // 缓存数组长度
    const len = nums.length
    // 遍历数组
    for(let i=0;i<len;i++) {
        // 判断当前值对应的 target 差值是否存在(是否已遍历过)
        if(map[target-nums[i]]!==undefined) {
            // 若有对应差值,那么答案get!
            return [map[target - nums[i]], i]
        }
        // 若没有对应差值,则记录当前值
        map[nums[i]]=i
    }
};

三数之和

使得a + b + c = 0 且不重复的三元组

给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[[-1, 0, 1], [-1, -1, 2] ]

方法一:暴力求解,三重循环 O(n^3)
方法二:hash表来记录 a b, a+b=-c
方法三:左右下标往中间推进
双指针要先排序,才能缩短查找范围

1)对数组进行遍历,每次遍历到哪个数字,就固定哪个数字(固定指针)
左指针指向固定指针后面,把右指针指向数组末尾,让左右指针从起点开始,向中间前进
2)每次指针移动一次位置,就计算三个指针之和是否等于0,等于0就得到一个目标组合
3)否则

  • 相加之和大于0,说明右侧的数偏大了,右指针左移
  • 相加之和小于0,说明左侧的数偏小了,左指针右移

Image text

const threeSum = function(nums){
  let res=[]
  nums = nums.sort((a, b)=>a-b)
  const len=nums.length
  // 注意:遍历到倒数第三个就够了,因为左右指针会遍历后面两个数
  for(let i=0;i<len-2;i++){
    // 左指针
    let j=i+1
    // 右指针
    let k=len-1
    // 如果遇到重复数字,则跳过
    if(i>0&&nums[i]===nums[i-1]){
      continue
    }
    while(j<k){
      // 三数之和小于0,左指针前进
      if(nums[i]+nums[j]+nums[k]<0){
        j++
        // 处理左指针重复情况
        while(j<k&&nums[j]===nums[j-1]){
          j++
        }
      }else if(nums[i]+nums[j]+nums[k]>0){
        // 三数之和大于0,右指针后退
        k--
        // 处理右指针元素重复情况
        while(j<k&&nums[k]===nums[k+1]){
          k--
        }
      }else{
        // 三数之和等于0
        res.push([nums[i], nums[j], nums[k]])
        // 左右指针一起前进
        j++
        k--

        // 若左指针重复,跳过
        while(j<k&&nums[j]===nums[j-1]){
          j++
        }

        // 若右指针重复,跳过
        while(j<k&&nums[k]===nums[k+1]){
          k++
        }
      }
    }
  }
  return res // 返回结果
}

let nums = [-1, 0, 1, 2, -1, -4]
console.log(threeSum(nums)) // [[-1, -1, 2], [-1, 0, 1]]

卡牌分组(归类)

给定一副牌,每张牌上都写着一个整数。
此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:

每组都有 X 张牌。
组内所有的牌上都写着相同的整数。
仅当你可选的 X >= 2 时返回 true。

输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]

最大公约数:a能被数b整除,a就叫做b的倍数,b就叫做a的约数;几个整数中公有的约数,叫做这几个数的公约数;
比如12、16的公约数有1、2、4,其中最大的一个是4,4就是最大公约数

思路:
1)用map或object统计出每张牌的个数: {1: 2, 2: 2, 3: 2, 4: 2}
2)求出相邻两个数的最大公约数: [2,2,2,2] 遍历求出它们的最大公约数

var hasGroupsSizeX = function (arr) {
    // 存储每张卡牌的总数
    // 修改排序的方式修改为直接统计每个相同字符的数量,思路不变
    let group = []
    let tmp = {}
    // 也可以用map,但是比较耗性能
    arr.forEach(item => {
        tmp[item] = tmp[item] ? tmp[item] + 1 : 1 
    })
    console.log(tmp)
    for (let v of Object.values(tmp)) {
        group.push(v)  // group ==> [2, 2, 2, 2]
    }
    // 此时group已经存放的是每张牌的总数了(数组只遍历一遍,避免了排序和正则的耗时)
    // 求两个数的最大公约数
    let gcd = (a, b) => {
        return b === 0 ? a : gcd(b, a % b)
    }
    while (group.length > 1) {
        let a = group.shift()
        let b = group.shift()
        let v = gcd(a, b)
        if (v === 1) {
            return false
        } else {
            group.unshift(v)
        }
    }
    return group.length ? group[0] > 1 : false
}
console.log(hasGroupsSizeX([1, 2, 3, 4, 4, 3, 2, 1]))

种花问题(筛选)

假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。

输入: flowerbed = [1,0,0,0,1], n = 1
输出: True

const canPlaceFlowers = (arr, n) => {
    // 计数器,可以种多少朵花
    let max = 0
    // 右边界补充[0,0,0],最后一块地能不能种只取决于前面的是不是1,所以默认最后一块地的右侧是0(无须考虑右侧边界有阻碍)
    arr.push(0)
    for (let i = 0, len = arr.length - 1; i < len; i++) {
        // 0表示可以种花
        if (arr[i] === 0) {
            if (i === 0 && arr[1] === 0) {
                // 种一颗花
                max++
                i++
            } else if (arr[i - 1] === 0 && arr[i + 1] === 0) {
                max++
                i++
            }
        }
    }
    return max >= n
}
console.log(canPlaceFlowers([1,0,0,0,1], 1))


// 简洁版
var canPlaceFlowers = function(flowerbed, n) {
  	return n <= ('0' + flowerbed.join('') + '0').split(/1+/) // 以1为分割点
        .reduce((a, c) =>
            a + Math.floor((c.length - 1) / 2)
        , 0)
};

格雷编码(二进制)

格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。即使有多个不同答案,你也只需要返回其中一种。
格雷编码序列必须以 0 开头
在这里插入图片描述

const grayCode = (n) => {
  // 递归函数,用来算输入为n的格雷编码序列
  let make = (n) => {
      if (n === 1) {
          return ['0', '1']
      } else {
          let prev = make(n - 1)
          let result = []
          let max = Math.pow(2, n) - 1
          for (let i = 0, len = prev.length; i < len; i++) {
              result[i] = `0${prev[i]}`
              result[max - i] = `1${prev[i]}`
          }
          return result
      }
  }
  return make(n)
}

/* const grayCode = (n) => {
  let res = []
  //  i<pow(2, n) i小于2的n次方
  //  左移n位就是2的n次方
  for (var i = 0; i < (1 << n); i++) {
      res.push(i ^ (i >> 1))
  }
  return res
}*/

console.log(grayCode(2))

按奇偶排序数组 II

给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。

对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。

输入:[4,2,5,7]
输出:[4,5,2,7]
解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。

思路:定义一个空数组,偶数位放2、4,奇数放5、7

const sortArrayByParityII = (arr) => {
  // 进行升序排序
  arr.sort((a, b) => a - b)
  // 声明一个空数组用来存储奇偶排序后的数组
  let r = []
  // 记录奇数、偶数位下标
  let odd = 1
  let even = 0
  // 对数组进行遍历
  arr.forEach(item => {
    if (item % 2 === 1) {
      r[odd] = item
      // 向后移动两位
      odd += 2
    } else {
      r[even] = item
      // 向后移动两位
      even += 2
    }
  })
  return r
}

最大间距

给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
如果数组元素个数小于 2,则返回 0。

输入: [3,6,9,1]
输出: 3
解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3

思路:

1)数组排序

2)每次排序完就判断当前值跟它右边值的差距

3)获取新的数组中的最大值

const maximumGap = (arr) => {
  if (arr.length < 2) {
    return 0
  }
  let max = 0
  let len = arr.length - 1
  let space
  for (let i = len, tmp; i > 0; i--) {
    // 排序
    for (let j = 0; j < i; j++) {
      tmp = arr[j]
      if (tmp > arr[j + 1]) {
        arr[j] = arr[j + 1]
        arr[j + 1] = tmp
      }
    }
    if (i < len) {
      // 等于最大值减当前值,最大值在右侧
      space = arr[i + 1] - arr[i]
      if (space > max) {
        max = space
      }
    }
  }
  // 处理边界 [1, 13, 16, 19]  只处理到space=16-13,没有处理 13-1
  return Math.max(max, arr[1] - arr[0])
}
console.log(maximumGap([3,6,9,1]))

数组中得第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

思路:
1)排序
2)遍历

const findKthLargest = (arr, k) => {
  let len = arr.length - 1
  for (let i = len, tmp; i > len - k; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        tmp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = tmp
      }
    }
  }
  // arr[len+1-k]
  return arr[len - (k - 1)]
}

// const findKthLargest = (arr, k) => {
//   return arr.sort((a, b) => b - a)[k - 1]
// }

缺失的第一个正数

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数

输入: [1,2,0] // 123,3没有所以是3
输出: 3

输入: [3,4,-1,1] // 2没有,所以是2
输出: 2

输入: [7,8,9,11,12] // 正整数从1开始,所以是1
输出: 2

注:0、-1不是正整数

思路:未排序、整数数组、最小的正整数
1)过滤非正整数的
2)排序,每排序一次就判断一次(sort()是全部排序,效率低,没必要把数组全部排序一遍)

  • 最小元素的索引i>0时,当前元素跟前面的元素相减大于1,表示不相邻,返回当前元素前面的一位元素
  • 最小元素的索引i==0时,最小的等于1
const firstMissingPositive = (arr) => {
  // 过滤掉非正整数
  arr = arr.filter(item => item > 0)
  // 实现选择排序,先拿到最小值,如果第一个元素不是1直接返回1,如果是1,就要比相邻元素差值
  for (let i = 0, len = arr.length, min; i < len; i++) {
    min = arr[i]
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < min) {
        let c = min
        min = arr[j]
        arr[j] = c
      }
    }
    arr[i] = min
    if (i > 0) {
      // 大于一,表示两个数字不相邻
      if (arr[i] - arr[i - 1] > 1) {
        return arr[i - 1] + 1
      }
    } else {
      if (min !== 1) {
        // 正整数从1开始,所以没有1就为1
        return 1
      }
    }
  }
  // 如果都每返回值,返回 最后的数+1
  return arr.length ? arr.pop() + 1 : 1
}

删除排序数组中的重复项

给定一个排序数组,你需要在 原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

const removeDuplicates = array => {
    const length = array.length
    let slowPointer = 0
    for (let fastPointer = 0; fastPointer < length; fastPointer ++) {
        if (array[slowPointer] !== array[fastPointer]) {
            slowPointer++
            array[slowPointer] = array[fastPointer]
        }
    }
    return slowPointer+1
}
console.log(removeDuplicates([0,0,1,1,1,2,2,3,3,4]))

众数

给定一个大小为 n 的数组,找出其中所有出现超过 ⌊ n/2 ⌋ 次的元素。
说明: 要求算法的时间复杂度为 O(n),空间复杂度为 O(1)。

摩尔投票法:设置变量num存放数组元素,cout计算数量,遍历遇到相同的就+1,遇到不一样的-1,打平了count=0的时候,num更换成其他元素
遍历这个元素,对这个元素进行计数,再判断是否大于数组的一半 O(n)

const find = array => {
    let count = 1
    let result = array[0]
    for (let i = 0; i < array.lenght; i++) {
        if (count === 0) result = array[i]
        if (array[i] === result) {
            count++
        }else {
            count--
        }
    }
    return result
}
console.log(find([2,2,1,1,1,1,2,2]))

组合总和(回溯)

给定一组不含重复数字的非负数组和一个非负目标数字,在数组中找出所有数加起来等于给定的目标数字的组合。

示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[[7], [2,2,3]]

遍历所有的情况来找出问题的解,在这个遍历过程当中,以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索

一个临时数组tmpArray,进入递归前push一个结果

const find = (array, target) => {
    // 最外层数组
    let result = []
    // 深度遍历
    const dfs = (index, sum, tmpArray) => { //  tmpArray  --> [2, 2]
        if (sum === target) { // 正好等于7,就把参数转数组push到result里
            result.push(tmpArray.slice())
        }
        if (sum > target) {  // 2 3 3 > 7
            return
        }
        // 递归主体
        for (let i = index; i < array.length; i++) {
            //加入数组,递归,看是否能被组合成7    [2, 2, 2]   ---- > 下一轮 [2, 2, 3]
            tmpArray.push(array[i])  
            dfs(i, sum + array[i], tmpArray)
            // 递归完把最后的拿掉,用完就丢弃,下轮循环,再加进去递归,看是否组合成7
            tmpArray.pop()
            // console.log(tmpArray) // tmpArray只是一个临时数组    [2, 2]
        }
    }
    dfs(0, 0, [])
    return result
}
console.log(find([2, 3, 6, 7], 7))

旋转数组的最小数字 (二分查找)

  • 方法1:暴力求解 时间复杂度 O(N)
var minArray = function(arr) {
    if(arr.length==0) return
    let minArr=arr[0]
    for(let i=1; i<arr.length; i++){
        minArr = Math.min(minArr, arr[i])
    }
    return minArr
};
  • 方法2:暴力求解 es6扩展运算符
var minArray = function(arr) {
    return Math.min(...arr)
}
  • 方法3:循环二分,不断去更新存在于两个子数组(两个非递减排序子数组)中的下标。时间复杂度是O(log(n))
    在这里插入图片描述

设置i, j指针分别指向numbers 数组左右两端,m=(i + j)/2为每次二分的中点( m<=m<j ),可分为以下三种情况:
当 numbers[m] > numbers[j]时:m一定在左排序数组中,即旋转点x一定在[m + 1, j]闭区间内,因此执行 i = m + 1
当 numbers[m] < numbers[j] 时:m一定在右排序数组中,即旋转点x一定在[i, m]闭区间内,因此执行 j = m
当 numbers[m] == numbers[j] 时: 无法判断m在哪个排序数组中,即无法判断旋转点x在[i, m]还是[m + 1, j]区间中。解决方案: 执行 j = j - 1缩小判断范围
返回值:当i=j时跳出二分循环,并返回numbers[i]即可。

var minArray = (nums) => {
    let l = 0;
    let r = nums.length - 1;
    while (l < r) {
        let mid = (l + r) >> 1;
        if (nums[mid] > nums[r]) {
            l = mid + 1;
        } else if (nums[mid] == nums[r]) {
            r--;
        } else {
            r = mid;
        }
    }
    return nums[l];
};
console.log(minArray([3,4,5,1,2]))
  • 方法4:sort
var minArray = (nums) => {
  return nums.sort((a,b)=>(a-b))[0]
}

调整数组使奇数全部都位于偶数前面

题目: 输入一个整数数组,实现一个函数, 来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分, 所有偶数位于数组的后半部分。

var sortArrayByParity = function (arr) {
  let left = 0;
  let right = arr.length - 1;
  while (left < right) {
      let temp = 0;
      if (arr[left] % 2 != 0) {
          left++;
      }
      if (arr[right] % 2 == 0) {
          right--;
      }
      temp = arr[left];
      arr[left] = arr[right];
      arr[right] = temp;
  }
  return arr
};
console.log(sortArrayByParity([1, 3, 3, 4, 5, 6, 7, 8, 9, 10]))

打印从1到最大的n位数

最大的n位十进制数,比如2的最大2位十进制=99 ,3=999,得出规律等于end=10^n-1

var printNumbers = function(n) {
  return Array.from({length: 10**n-1}, (item, index)=> index+1 )   // Math.pow(10, n)
}

/* 或者这种求数值的整次方
  let max = 1;
  let x = 10;
  while (n) {
      if (n & 1) {
          max = max * x;
      }
      x = x * x;
      n = n >> 1;
  }
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值