大家好,我是柒八九
。这篇文章是我们算法探险系列的第三篇文章。是针对数据结构方面的第二篇。上一篇JS算法探险之整数中我们介绍了关于JS整数的一些基础知识和相关算法题。我们做一个简单的「前情回顾」。
例如
IEEE 754
格式)(number).toString(2)
i>>1
来计算i/2
,而且还是下取整i&1
来计算 i%2
而今天,我们继续介绍一类,很基础但是无论在数据结构方向还是算法解题中都占有很大比重的数据结构 ---「数组」。
闲话少叙,那就开始咯。
还是老样子,附赠一首打油诗:
sum
套路就那般right
,根据条件移动left
sum
/mult
< target
: 右移 (right++
)sum
/mult
> target
: 右移 (left++
)left
永远不会走到指针right
的右边」 (left<=right
)sum
VStarget
, sum
小,left++
;sum
大,right--
left
为首right
为尾(求两数之和)i
个数据和)Map(sum,count/index)
来辅助❝奇怪的知识点: 1. 数组的本质是「对象」且为「异质对象」 2. JS 只支持一维数组,并不支持矩阵 ❞
JS数组本质上是「对象」
❝根据
EMMAScript
规范,在JS中有两种对象 1. 常规对象Ordinary Object 2. 异质对象Exotic Object ❞
这两种对象包含了JS世界中所有的对象,「任何不属于常规对象的对象都是异质对象」。
而数组就是异质对象,即
❝数组的本质是「对象」且为「异质对象」 ❞
调用Array函数生成数组实例
ArrayCreate返回值
在JS中,我们可以通过很多方式来构建一维数组。
let array = Array('柒','八','九'); // new Array / []等
而构建多维数组,就需要借助函数来构建。
const matrix= (x,y) =>
new Array(x).fill(0)
.map(
()=>new Array(y).fill(0)
)
通过matrix
我们构建了一个,x
行,y
列 且数组中值都为0
的二维数组(矩阵)。
matrix(5,4)
当然,我们可以在函数内部执行其他的初始化条件。然后生成满足条件的二维数组。
多维数组的话,可以套用上面的代码。
const matrixN = (i,j,z) =>
new Array(i).fill(0)
.map(
()=>new Array(j).fill(0)
.map(
()=>new Array(z).fill(0)
)
)
用类似的方式,我们构建了一个i
(行)、j
(列)及 z
(深度)的矩阵。
「双指针」是一种常见的解题思路,使用两个「相反」方向或者「相同」方向的指针扫描数组从而达到解题目的。
有两种解题思路: 「反向双指针」/「同向双指针」
P1
(left
)指向数组的「第一个」数字,另一个指针P2
(right
)指向数组的「最后一个」数字,然后比较两个指针指向的「数字之和」(sum)与一个「目标值」(target)直接的大小关系。
「left/right
的移动方向是相反的」sum
< target
: 右移 left
(left++
)sum
> target
: 左移 right
(right--
)sum
)或者「乘积」(mult
)。
「初始化」的时候两个指针left
/right
都指向数组的「第一个」数字。
如果两个指针之间的「子数组」的sum
或者mult
与target
之间关系如下:
「left/right
的移动方向是相同的」sum
/mult
< target
: 右移 right
(right++
),在「子数组右边」增加新的数字sum
/mult
> target
: 右移left
(left++
),删除「子数组最左边」的数字题目描述:
❝输入一个递增排序的数组和一个值
target
,在数组中找出两个和为target
的数字并返回它们的下标 提示: 数组中有且只有一对符合要求 同时一个数字不能使用两次 示例:输入数组:[1,2,4,6,10],k的值为8 输出[1,3] ❞
left
指向首元素,right
指向尾元素sum
VS target
移动对应的指针function twoSum4SortedArray(nums,target){
let left=0,right= nums.length-1; // 初始化指针left,right
while(left<right && nums[left] + nums[right] != target){
if(nums[left] + nums[right]<target){
left++;
}else{
right--;
}
}
return [left,right]
}
注意:如果大家在刷leetcode
等算法网站中,肯定会见过关于数组的两数之和的题。但是,这里的题干和常规的两数之和还有点区别。首先,该题干中,天生有序,所以,可以「套用」反向双指针的套路。
为了做区分,我们将twoSum
的解题代码也直接写出来。它的解题思路是,利用了Map
对数据和下标做了转换。
function twoSum(nums,target){
let map = new Map(); // 用于,存储[nums[i],i]之间的关系
for(let i =0;i<nums.length;i++){
let expectValue = target - nums[i];
// 先从map中找,是否存在指定值
if(map.has(expectValue)){
// 如果有,直接返回与值相对于的下标
return [map.get(expectValue),i]
}
// 存储[nums[i],i]之间的关系
map.set(nums[i],i);
}
return null;
}
题目描述:
❝输入一个数组,找出数组中所有和为
target
的3个数字的三元组 提示: 返回值不得包含「重复」的三元组 示例:输入数组:[-1,0,1,2,-1,-4]
,target的值为0 输出[[-1,0,1],[-1,-1,2]]
❞
left
指向固定元素的「后一个元素」,right
指向「尾元素」sum
VS target
移动对应的指针sort
但是需要指定排序函数nums.sort((a,b)=>a-b)
function threeSum(nums,target){
let result = [];
if(nums.length<3) return [];
// 人工对数据进行排序处理
nums.sort((a,b)=>a-b);
let i =0;
while(i<nums.length-2){
twoSum(nums,i,target,result);
let temp = nums[i];
// 剔除,重复元祖中第一个数值
while(i<nums.length && nums[i]==temp) i++;
}
return result;
}
我们把「排序数组中的两个数字之和」的算法,做了一个改造,因为left
不在从0
开始,所有需要将left
的前一个位置i
传入,right
的逻辑不变,还是「数组尾部」
left = i + 1
right = nums.length - 1
function twoSum(nums,i,target,result){
// 初始化指针left,right
let left = i + 1, right = nums.length -1;
while(left<right){
// 求和
let sum = nums[i] + nums[left] + nums[right];
// 指针移动过程 (if/else)
if(sum === target){
result.push([nums[i],num[left],nums[right]]);
let temp = nums[left];
// 剔除,重复元祖第二个数值
while(nums[left] === temp && left < right) left++;
}else if(sum < 0) {
left++;
}else{
right--;
}
}
}
我们来对比下,两个twoSum
的共同和不同点。
它们的核心框架是相似的。都是利用「方向双指针」进行sum
与target
之间的数据对比。
题目描述:
❝输入一个「正整数」组成的数组和一个正整数
target
,找出数组中「和」大于或等于target
的「连续子数组」的「最短」长度 提示: 如果不存在满足条件的子数组,返回0 示例:输入数组:[5,1,4,3]
,target
的值为7 输出2
(和大于或等于7的最短连续子数组是[4,3]
) ❞
left
指向子数组的第一个数字right
指向子数组的最后一个数字left
/right
两指针之间的所有数字的组成left
永远不会走到指针right
的右边」left
/right
「初始化」的时候都指向数组的第一个元素,套用上面的公式sum
>= target
: 右移left
(left++
),删除「子数组最左边」的数字,子数组长度-1sum
< target
: 右移 right
(right++
),在「子数组右边」增加新的数字,子数组长度+1sum
>=target
时,为了求最短子数组,我们需要移动left
进行子数组的「瘦身」处理 (left++
)function minSubArrayLen(nums,target){
let left=0,right=0; // 同向双指针,默认值
let sum=0;
// 最小的长度
let minLength = Number.MAX_SAFE_INTEGER;
for(right=0;right<nums.length;right++){
sum+=nums[right];
while(left<=right && sum>=target){
// 更新最小长度
minLength = Math.min(minLength,right - left + 1);
// 子数组**瘦身**处理
sum-= nums[left++]
}
}
return minLength == Number.MAX_SAFE_INTEGER?0:minLength;
}
有几点需要唠叨一下:
Number.MAX_SAFE_INTEGER
) 如果已知最大范围,我们也可以给一个定值。例如,100/1000
等;那最大值,就是用0
等替换right
先动,left
视情况而动题目描述:
❝输入一个「正整数」组成的数组和一个正整数
target
,找出数组中「乘积」小于target
的「连续子数组」的所有组合的个数 示例:输入数组:[10,5,2,6]
,target
的值为100
输出8
([10],[5],[2],[6],[10,5],[5,2],[2,6],[5,2,6]) ❞
left
永远不会走到指针right
的右边」 (left<=right
)mult
>= target
: 右移left
(left++
),删除「子数组最左边」的数字,子数组长度-1(mult变小)mult
< target
: 右移 right
(right++
),在「子数组右边」增加新的数字,子数组长度+1 (mult变大)left
到某个位置时子数组的「乘积」小于target
,就不需要移动left
。只要right
「保持不动」,[left
,right
]区间的「所有」的子数组的乘积都一定小于target
。也就说说,两个指针之间有多少个数字,就有多少个满足条件的子数组function numSubarrayMultLessThanTarget(nums,target){
let mult = 1;
let left =0,right=0; // 初始化指针
let count = 0;
for(right=0;right<nums.length;right++){
mult *=nums[right];
// 指针left永远不会走到指针right的右边
while(left<=right && mult >= target){
mult /=nums[left++];
}
count += right>=left?right - left +1 :0;
}
return count;
}
虽然,在求正整数数组「和」或者「乘积」之间,有些具体逻辑不大相同,但是总体的思路都是利用「同向双指针」对数据进行处理。
使用「双指针解决子数组之和」有一个前提条件:数组中的「所有」数字都是「正数」。所有,双指针的在解决非正数的数组时,是不满足条件的。
针对非正数的数组,我们换一个思路来求子数组之和。
假设整个数组的长度为n
,它的某个「子数组」的第一个数字的下标是i
;最后一个数字的下标是j
。我们做一个「预处理」,计算从数组下标为0的数字开始到以「每个数字」为结尾的「子数组之和」。预处理只需要从头到尾扫描一次,就能求出从下标0开始到下标0结束的子数组之和 「S0」 ,以此类推,S1,S2,Sn-1因此,从下标为i
开始到下标为j
结束的子数组的和就是 「Sj-Si-1」
例如:在数组[1,2,3,4,5]
中,从S2的子数组[1,2,3]
之和是6,S4的子数组[1,2,3,4,5]
之和是15,那么从下标3开始到下标4结束的子数组之和[4,5]
之和是9,也就是 S4 - S2 即:15 - 9 = 6
具体的思路如上,「做事讲究,师出有名,既然思想有了,那就需要见真章的时候了」。
题目描述:
❝输入一个「整数」组成的数组和一个整数
target
,找出数组中数字之和等于target
的「连续子数组」的个数 示例:输入数组:[1,1,1]
,target
的值为2
输出2
❞
i
个数字之和」,并将和「保存」下来i
个数字之和记为x
j
(j<i
) 即,j
在x
前面,且数组的前j
个数字之和为x-target
(「很重要」)j+1
个数字开始到第i
个数字结束的子数组之和为target
target
的子数组的个数。当扫描到数组第i
个数字并求得前i
个数字之和是x
时,需要知道在i
之前存在「多少」个j
并且前j
个数字之和等于x-target
i
,不但要保存前i
个数字之和,还要保存每个和出现的次数Map
(哈希表)的「键」(key)保存前i
个数字之和,「值」(value)保存每个和出现的次数function subarraySum(nums,target){
// 构建哈希表,并初始化[0,1]
let sumToCount = new Map();
sumToCount.set(0,1);
let sum = 0;
let count =0;
for(let num of nums){
sum += num;
// 统计 在当前数值下的 x-target的值
count += sumToCount.get(sum - target) // 前面已经存在
|| 0; // 首次出现
sumToCount.set(
sum,
(sumToCount.get(sum)||0)+1
)
}
return count;
}
题目描述:
❝输入一个只包含0和1的数组,求0和1的个数相同的「最长连续子数组」的长度 示例:输入数组:
[0,1,0]
输出2
(两个子数组分别是[0,1]和[1,0]) ❞
i
个数字之和」i
个数字之和为m,前j
个数字(j<i
)之和也为m,那么从j+1
到第i个数字的子数组之和为0,长度为i - j
function findMaxLength(nums){
let sumToIndex = new Map();
sumtoIndex.set(0,-1);
let sum = 0;
let maxLength =0;
for(let i=0;i<nums.length;i++){
// 数据转化
sum+=nums[i]==0?-1:1;
if(sumToIndex.has(sum)){
maxLength = Math.max(maxLength,i-sumToIndex.get(sum));
}else{
// 存储关系
sumToIndex.set(sum,i)
}
}
return maxLength;
}
我们可以看到,在「和为target的子数组」和「0和1个数相同的子数组」中,虽然有些细节是不一样的,但是总体框架还是一致的。
i
个数字之和 求出sum
Map
来存储sum
与对应所求的(count/index
)直接的关系题目描述:
❝输入一个整数数组,如果一个数字左边的子数组的数字之和等于右边子数组的数字之和,返回该数字的下标 示例:输入数组:
[1,7,3,6,2,9]
输出3
(对应的值为6) ,下标为3的数字(值为6)的左边3个数1,7,3的和与右边两个数字2,9的和相等 ❞
i
个数字时i-1
个数字的和i+1
个数字开始累加到最后一个数字的和,这个和等于数组中「所有数字」之和减去从第一个数字累加到第i
个数字的和function pivotIndex(nums){
let total =0;
total =nums.reduce((pre,cur)=>pre+cur);
let sum = 0;
for(let i=0;i<nums.length;i++){
sum+=nums[i];
if(sum - nums[i] == total - sum){
return i
}
}
return -1;
}