本文主要针对 leetcode 中部分数组相关的简单、中等算法题进行总结分析,提供几个易于理解,便于记忆的解题范式,希望可以帮到大家。
其中涉及到的数组方法,以及相关数据结构,总结在前面,如有不熟悉,建议先看一遍这篇文章:前端常用数据结构一览
数组方法:sort()升降序、Array.from()
Map方法:has()、get()、set()
解题思路:求和转求差、双指针、快慢指针
1 两数之和
题目: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
思路: 此类问题必然是需要遍历数组的,关键在于想到一次遍历即可解决问题的方法。常规思路不好解决的时候,可以将求和问题转化为求差问题,再结合 Map 能保存键值对的特性,有如下解法:
1.新建一个Map
来记录已经遍历过的数值(key)
及其对应的索引值(value)
2.遍历数组,确认Map
里是否有值等于target - nums[i]
- 是,则返回
Map
中key
为target - nums[i]
所对用的value
以及此时的i
- 否,则将数值和索引值添加到
Map
中set(num[i], i)
js
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
const myMap = new Map()
for(let i = 0; i < nums.length; i++){
if(myMap.has(target - nums[i])){
return [myMap.get(target - nums[i]), i]
}else{
myMap.set(nums[i],i)
}
}
};
2 三数之和
题目: 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
思路: 因为是三数,不能只是简单遍历,考虑双指针法,即固定其中一个数,在剩下的数中寻找是否存在两个数和固定数求和为0的,具体解法如下:
1.数组升序排序
2.数组遍历,初始化对撞指针:let l = i + 1, r = nums.length - 1
3.对撞指针循环移动:while(l < r)
,不断计算sum
值大小
sum === 0
,将三元组添加到res
中,并且l++,r--
sum >= 0
,说明右侧的数偏大了,r--
sum <= 0
,说明左侧的数偏小了,l++
4.特殊情况处理:重复元素需要跳过
- 数组遍历时,从第二个开始,与前一个数值比较,相等即跳过此次遍历
sum === 0
时,循环判断左指针当前值与后一个位置数值是否相等(还需要确保l < r
),相等即l++
;循环判断右指针当前值与前一个位置数值是否相等,相等即r--
js
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
const res = []
if(nums == null || nums.length < 3) return res
nums.sort((a,b) => a - b)
for(let i = 0; i < nums.length; i++){
if(nums[i] > 0) break
if(i > 0 && nums[i] === nums[i - 1]) continue
let l = i + 1, r = nums.length - 1
while(l < r){
const sum = nums[i] + nums[l] + nums[r]
if(sum === 0){
res.push([nums[i], nums[l], nums[r]])
while(l < r && nums[l] == nums[l + 1]) l++
while(l < r && nums[r] == nums[r - 1]) r--
l++
r--
}else if(sum < 0){
l++
}else{
r--
}
}
}
return res
};
3 合并两个有序数组
题目: 给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
思路: 数组有序,直接用双指针法即可
1.两指针分别指向nums1
和nums2
最大值,依次比较,较大值赋给nums1
的尾部
2.遍历条件为while(m>0 && n>0)
3.特殊情况处理:当m
先为0的时,依次将nums2
赋值给nums1
即可
js
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
var merge = function(nums1, m, nums2, n) {
while(m > 0 && n > 0){
if(nums1[m-1] >= nums2[n-1]){
nums1[m+n-1] = nums1[m-1]
m--
}else{
nums1[m+n-1] = nums2[n-1]
n--
}
}
while(n > 0){
nums1[n-1] = nums2[n-1]
n--
}
};
4 最大子数组和
题目: 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
思路: 因为要求连续,直接遍历累加即可,但当出现前i
项和少于0即应舍弃掉这部分元素
1.遍历数组,更新前i
项最大和sum
2.更新数组最大和res
3.特殊情况处理:数组元素有可能均为负数,因此res
的初始值不能默认给0,而是数组的第一个元素
js
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
let res = nums[0]
let sum = 0
nums.forEach(item => {
sum += item
res = Math.max(sum,res)
if(sum < 0){
sum = 0
}
})
return res
};
5 最长连续序列
题目: 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
思路: 对时间复杂度有要求的往往都需要使用数组以外的数据结构,比如 Map。因为不要求序列元素在原数组连续,考虑用 key 存元素本身,value 存元素所在序列最大长度,解法如下:
1、遍历数组,判断 myMap 中是否已经有该元素,有则跳过,无则继续
2、新存元素时,分别查该元素左邻居和右邻居的 value(不存在时即为0),将其相加并 +1 即可得到该元素的 value。
3、还需要更新连续序列头尾数字的 value(解决了先存元素因为刚开始缺乏左右邻居导致的 value 偏小的问题),因为后续遍历找左右边时,只会找到现有连续序列的头或者尾
js
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
let res = 0
const myMap = new Map()
nums.forEach(num => {
if(!myMap.has(num)){
const left = myMap.get(num-1) || 0
const right = myMap.get(num+1) || 0
const cur = left + right + 1
res = Math.max(cur, res)
myMap.set(num, cur)
myMap.set(num - left, cur)
myMap.set(num + right, cur)
}
})
return res
};
6 长度最小的子数组
题目: 给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其和 ≥ target 的长度最小的连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度 。如果不存在符合条件的子数组,返回 0 。
思路: 因为要求是连续子数组,且均为正整数,考虑快慢指针,解法如下:
1.快指针遍历数组,累加元素,当满足条件后,进入while循环,遍历慢指针直至不满足条件,得到此时最短子数组
2.快指针继续遍历,重复步骤 1 直至遍历结束
3.返回步骤 1 中得到的最小值
4.特殊情况处理:若步骤 1 一次也未能满足条件,则返回 0
js
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
var minSubArrayLen = function(target, nums) {
let res = nums.length + 1
let sum = 0
let slow = 0
for(let i = 0; i < nums.length; i++){
sum += nums[i]
if(sum >= target){
while(sum >= target){
sum -= nums[slow]
slow++
}
res = Math.min(res, i - slow + 2)
}
}
return res === nums.length + 1 ? 0 : res
};
7 买卖股票的最佳时机
题目: 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路: 遍历时与前i
项最小值做差即可以得到最大利润,解法如下:
1.遍历数组,更新前i
项最小值min
2.更新最大利润res
js
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
let min = prices[0]
let res = 0
prices.forEach(price => {
min = Math.min(min, price)
res = Math.max(res, price - min)
})
return res
};
8 螺旋矩阵
题目: 给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。3 行 4 列的例子:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 思路: 依次以矩阵的四个角作为起点进行单向遍历,包括起点,但不包含终点,得到一个 m-1 行 n-1 列的矩阵,直至剩余单行或者单列,再单独遍历即可,解法如下:
1.确定四角
- 左边界
left : 0
- 上边界
top : 0
- 右边界
right : matrix[0].length - 1
- 下边界
bottom : matrix.length - 1
2.循环成环条件:top < bottom && left < right
3.每循环一次,需要收缩边界left++、right--、top++、bottom--
4.特殊情况处理:
- 只剩单行:
top === bottom && left <= right
- 只剩单列:
left == right && top <= bottom
js
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function(matrix) {
const res = []
let left = 0;
let top = 0;
let right = matrix[0].length - 1;
let bottom = matrix.length - 1;
while(left < right && top < bottom){
for(let i = left; i < right ; i++) {
res.push(matrix[top][i])
};
for(let i = top; i < bottom; i++){
res.push(matrix[i][right])
};
for(let i = right; i > left; i--){
res.push(matrix[bottom][i])
}
for(let i = bottom; i > top; i--){
res.push(matrix[i][left])
}
left++
top++
right--
bottom--
}
while(left === right && top <= bottom){
res.push(matrix[top][left])
top++
}
while(left <= right && top === bottom){
res.push(matrix[top][left])
left++
}
return res
};
9 合并区间
题目: 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
- 示例输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
- 示例输出: [[1,6],[8,10],[15,18]]
思路: 数组排序后,采用快慢指针遍历数组,有交集即合并,解法如下:
1.以子数组左边界升序排列
2.初始化慢指针为第一个子数组pre
3.快指针从第二个子数组cur
开始遍历
4.判断是否有重叠区间pre[1] >= cur[0]
- 是,合并区间并更新慢指针
pre
:pre[1] = Math.max(pre[1], cur[1])
- 否,将当前
pre
添加到res
,并更新慢指针指向当前快指针pre = cur
5.将最后一个子数组,此时的慢指针pre
添加到res
js
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
intervals.sort((a,b) => a[0]-b[0])
const res = []
let pre = intervals[0]
for(let i = 1; i < intervals.length; i++){
const cur = intervals[i]
if(pre[1] >= cur[0]){
pre[1] = Math.max(pre[1],cur[1])
}else{
res.push(pre)
pre = cur
}
}
res.push(pre)
return res
};
10 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
-
示例输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
-
示例输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
思考: 字符串转数组排序后再转回字符串,存为 Map 的 key,即可作为字母异位词判断依据,value 存符合条件的数组。
1.遍历数组
2.将元素转数组排序后重新转为字符串,作为 key 存入,元素作为数组的子元素存入 value
3.重复 2 步骤,遇到 Map 已存在的 key,即取出 value,push 后再重新存入
4.将 Map 的 value 转为数组
js
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
const myMap = new Map()
strs.forEach(str => {
const arr = str.split('')
arr.sort()
let key = arr.toString()
const list = myMap.get(key) || new Array()
list.push(str)
myMap.set(key,list)
})
return Array.from(myMap.values())
};