补充说明
随着刷题的量增加,愈发觉得leetcode给出的执行时间存在问题,故去论坛逛了逛,果然不出所料,因此说明一下:
leetCode给出的执行时间会有偏差,甚至同样的代码,提交两次得到的时间都不一样,同等时间复杂度会有几十ms的差别。所以leetcode反馈的运行时间如果差别在一个数量级以上,这时就要考虑优化代码。具体大家可以参考:Leetcode不同语言有哪些减少runtime的方法?。因此这个执行时间大家不必过于关注。若大家看到我关于leetcode练习的博客中存在这方面问题的欢迎指出,十分感谢。
补充时间:2018-05-20
闲聊(可以略过)
算法一直以来是我比较劣势的技能,在上大学接触过,然后就是考研的时候埋头啃过相关书籍,之后基本没怎么碰过了。但是对于大厂来说,算法是必备的基础技能,近段时间比较火的一个app抖音,它的公司-北京字节跳动科技有限公司,是一家纯靠算法起家的公司,我使用过抖音,在我接触过使用抖音的人的反馈就是:内容很有趣。这背后必然是通过算法处理用户数据,分析用户行为然后智能推荐,至少用户的反馈证明这家公司很强。而它旗下还有火山小视频、内涵段子(已凉)、今日头条等用户量很高的app。闲扯了半天,下面直接进入正题,这篇博客主要记录我在LeetCode做的一些题及思考过程(持续更新)。
参考
对于js数组一些方法的实现,这里针对V8引擎,大家可以阅读一下GitHub上开源的V8源码。
删除排序数组中的重复项
分析:题目大概意思是,给定一个已排序数组,你需要在该数组上进行操作,去除重复项,且不能使用额外数组。我们可以这样做:
- 维护两个指针,一个用于保存当前有效长度(不重复元素的长度)
- 由于数组是有序的,所以重复元素一定相邻
代码:
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function(nums) {
if (0 == nums.length) {
return 0;
}
// 创建一个指针保存当前有效长度,初始值为1(至少1个)
var index = 1;
// 遍历数组
for (var i = 1; i < nums.length; i++) {
// 若相邻两个值不同
if (nums[i] != nums[i-1]) {
// 存储不重复元素
nums[index] = nums[i];
// 有效长度加1
index++;
}
}
// 截取有效长度
nums.length = index;
// 返回有效长度
return index;
};
执行时间:104ms
完成日期:2018-04-26
leetcode案例
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function(nums) {
if(nums.length==0)return 0;
var i=0;
for(var j=1;j<nums.length;j++){
if(nums[j]!==nums[i]){
i++;
nums[i]=nums[j];
}
}
return i+1;
};
执行时间:72ms
买卖股票的最佳时II
分析:第二个例子其实有点误导性,我们只要知道要获取最大利润,那么肯定是有利润就卖,所以只要后一天比前一天价格高,就计算收益。例如,[1,5,4,9]
利润最大方式:5-1 + 9-4 = 9。
其他方式:4-1 + 9-5 = 7;
9-1 = 8;
代码:
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
// 声明利润变量
var profit = 0;
// 遍历数组
for (var i = 0; i < prices.length; i++) {
// 若前一项 < 后一项
if (prices[i] < prices[i+1]) {
// 计算差值,并累加利润
profit += prices[i + 1] - prices[i];
}
}
return profit;
};
执行时间:76ms
完成日期:2018-04-27
leetcode案例
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function (prices) {
var profit=0,
len=prices.length;
for(let i=0;i<len-1;++i){
let now=prices[i],
next=prices[i+1];
if(next>now){
profit+=next-now;
}
}
return profit;
};
执行时间:64ms
旋转数组
分析一:最笨的办法,循环移动,每次将数组最后一个元素先保存到一个中间变量,所有元素依次向后移动一位,移动完k次后,将中间遍历的值赋到数组第一个元素。
代码:
var arrLen = nums.length;
for (var i = 0; i < k; i ++){
var lastVal = nums[nums.length - 1]; // 最后一个值存储到临时变量
for (var j = nums.length - 2; j >= 0 ; j --){ // 所有元素依次后移一位
nums[j + 1] = nums[j];
}
nums[0] = lastVal; // 将临时变量的值赋给第一个元素
执行用时:388ms
分析二:分析一听上去就明显知道时间会很长,那么是否还有更好的办法,这就可以利用js数组两个动态添加删除的方法,直接移出数组末位,并添加到数组首位。而k相当于移动的次数。
代码:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
for (var i = 0; i < k; i++) {
nums.unshift(nums.pop());
}
};
执行用时:164ms
分析三:在测试用例的过程中,我们会发现,如果k等于数组长度,是不需要移动的,这可以减少程序执行次数。所以我们应先判断k与数组长度关系,大于等于数组长度时稍微复杂一些,所以先讨论简单的k < 数组长度的情况。
k < 数组长度
1.1 计算移动完成后,数组末位索引
1.2 使用splice从0开始到末位(包含末位)切割数组,并将切割得到的元素插入数组。k > 数组长度
2.1 k 如果是数组长度的整数倍,则无需移动数组。
2.2 k 如果不是数组长度的整数倍,移动次数为k/数组长度的余数即可。使用splice从0开始到末位(包含末位)切割数组,并将切割得到的元素插入数组。
代码:
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
var movedLastIndex;
if (k < nums.length) {
movedLastIndex = nums.length - k; // 此处为方便,故省略 - 1
} else if (k >= nums.length && k % nums.length == 0) {
// 不变
return;
} else if (k >= nums.length && k % nums.length != 0) {
k = k % nums.length;
movedLastIndex = nums.length - k;
}
// 利用apply方法第二个形参可以传入数组的特性,将形参数组中的数值遍历传给push。
nums.push.apply(nums,nums.splice(0, movedLastIndex));
};
执行用时:96ms
完成日期:2018-04-28
leetcode案例
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
nums.unshift(...nums.splice(-k % nums.length));
};
执行用时:68ms
解析:移动次数/数组长度的余数就是向右移动次数的最小值,因为unshift方法可以接受多个参数,splice方法返回的是一个数组,通过扩展运算符 … 对应传到unshift方法,从而实现旋转。
存在重复
题目:给定一个整数数组,判断是否存在重复元素。如果任何值在数组中出现至少两次,函数应该返回 true。如果每个元素都不相同,则返回 false。
分析:ES6中引入了新的数据结构Set,类似数组,但是成员没有重复值,所以我们只需要将传入的数组传入Set中,对比Set的size和原数组的length是否一致就能判断数组是否存在重复值
代码:
/**
* @param {number[]} nums
* @return {boolean}
*/
var containsDuplicate = function(nums) {
let oldLen = nums.length;
let setArr = new Set(nums);
if (oldLen === setArr.size) {
return false;
} else {
return true;
}
};
执行时间:108ms
完成日期:2018-05-01
leetcode案例
/**
* @param {number[]} nums
* @return {boolean}
*/
var containsDuplicate = function(nums) {
let length = nums.length? nums.length : 0
let set = new Set()
nums.forEach((el) => {
set.add(el)
})
if (set.size === length) {
return false
} else {
return true
}
};
执行时间:76ms
解析:显然,直接将数组以参数形式传入构造方法消耗的时间 > 通过遍历数组向Set集合添加元素的时间。
只出现一次的数字
分析一:先排序,数组会变成这些情况:[1,22,33,44],[11,22,3,44],[11,22,33,4],然后遍历数组:
- 若为首个,与后一个元素不同
- 若为末位,与前一个元素不同
- 其他,与前后元素都不一直
代码:
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
nums.sort();
for (var i = 0; i < nums.length; i++) {
switch (i) {
case 0:
if (nums[i] != nums[i+1]) {
return nums[i];
}
break;
case nums.length - 1:
if (nums[i] != nums[i-1]) {
return nums[i];
}
break;
default:
if (nums[i] != nums[i-1] && nums[i] != nums[i+1]) {
return nums[i];
}
}
}
};
执行用时: 144ms
分析二:由于执行用时很慢,有强迫症的我便重新思考了一下其他的方法,执行时间长的很大原因是在排序上。是否不用排序能够实现呢。去查了一下,发现一个很巧妙的办法,就是用位运算中的按位亦或,一个整数和它本身异或之后得到值是0,0与其他整数异或得到的是这个整数本身,即:1 ^ 1 = 0, 1 ^ 0 = 1,2 ^ 0 = 2。
代码:
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
var result = nums[0];
for (var i = 1; i < nums.length; i++) {
result ^= nums[i];
}
return result;
};
执行用时: 72ms
完成日期:2018-05-01
leetcode案例
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
let result = 0;
nums.forEach(el => (result ^= el));
return result;
};
执行用时: 56ms
两个数组的交集II
分析一:先来最简单的,要找交集,分别遍历两个数组,进行对比,相同的即从数组中取出放入另一个数组即可。
代码:
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
var result = [];
for (var i = 0; i < nums1.length; i++) {
for (var j = 0; j < nums2.length; j++) {
if (nums1[i] == nums2[j]) {
// 放入结果数组
result.push(nums2[j]);
// 从原数组中移除
nums2.splice(j,1);
break;
}
}
}
return result;
};
执行用时:96ms
分析二:很明显,这种办法消耗的时间比较长,时间复杂度O(n^2)。所以换一种思路,先遍历数组2,利用includes判断数组1中是否包含数组2的值,若包含,先将值添加到新数组中,然后把数组1中对应的值删除。
代码:
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
/*
遍历其中一个数组,并判断另一个数组中是否包含该数组的某元素
*/
var result = [];
for (var i = 0; i < nums2.length; i++) {
if (nums1.includes(nums2[i])) {
result.push(nums2[i]);
nums1.splice(nums1.indexOf(nums2[i]),1);
}
}
return result;
};
执行用时:84ms
完成日期:2018-05-02
leetcode案例
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
const map={};
const res=[];
nums1.forEach(function (v) {
map[v]?map[v]++:map[v]=1;
});
nums2.forEach(function (v) {
if(map[v]){
res.push(v);
map[v]--;
}
});
return res;
};
执行用时:56ms
分析:通过forEach遍历数组1并将数组1的元素作为map对象的key,而map对象的value其实是一个计数器,代表该元素存在的个数,存放到对象map中。接着遍历数组2,通过map[key]获取元素个数(其中key是数组2的元素)的方式,判断数组2的元素是否存在,存在则放入结果数组,并将计数器减1。
加一
分析:末位加一,显然末位为9时需要进1,所以分小于9和大于等于9两种情况讨论,我们直接遍历数组,如果小于9直接加1返回数组即可,如果大于等于9则将该元素置为0,待遍历结束后,为数组首位加一个元素1即可。
代码:
/**
* @param {number[]} digits
* @return {number[]}
*/
var plusOne = function(digits) {
for (var i = digits.length - 1; i >=0; i--) {
if (digits[i] < 9) {
digits[i]++;
return digits;
} else {
digits[i] = 0;
}
}
// 若全为9
digits.unshift(1);
return digits;
};
执行用时:76ms
完成日期:2018-05-03
leetcode案例
/**
* @param {number[]} digits
* @return {number[]}
*/
var plusOne = function(digits) {
var len = digits.length;
for (var i = len - 1; i >= 0; i--) {
if (digits[i] < 9) {
digits[i]++;
return digits;
}
digits[i] = 0;
}
return [1, ...digits];
};
执行用时:52ms
移动零
题目:
给定一个数组 nums, 编写一个函数将所有 0 移动到它的末尾,同时保持非零元素的相对顺序。
例如, 定义 nums = [0, 1, 0, 3, 12],调用函数之后, nums 应为 [1, 3, 12, 0, 0]。
注意事项:
1.必须在原数组上操作,不要为一个新数组分配额外空间。
2.尽量减少操作总数
分析:由于要在同一个数组上操作,我们可以考虑多用一个指针index,index指向的元素都是不为0的,遍历数组,将不为0的数全放到数组前边,最后index之后的肯定都是0,只要在nums.length - index这长度遍历添加0即可。
代码:
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
var index = 0;
for (var i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[index] = nums[i];
index++;
}
}
while(index < nums.length){
nums[index] = 0;
index++;
}
};
执行用时:96ms
完成日期:2018-05-04
leetcode案例
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let pos = 0;
for(let i = 0; i < nums.length ; i++) {
if(nums[i] !== 0) {
nums[pos] = nums[i]
pos++
}
}
for(; pos < nums.length; pos++) {
nums[pos] = 0
}
};
执行用时:64ms
两数之和
分析一:直观的想法(暴力搜索)就是从第一个数开始,逐一分别与后边元素相加,存在直接返回,不存在则从第二个数开始,逐一分别与后边元素相加,存在直接返回,不存在则继续之前操作,直到倒数第二个数与数组末位相加。可以看出,最坏情况则是遍历到倒数第二个数与数组末位相加。
代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
for (var i = 0; i < nums.length - 1; i++) {
for (var j = i + 1; j < nums.length; j++) {
if (target == nums[i] + nums[j]) {
return [i, j];
}
}
}
};
执行用时:160ms
分析二:明显分析一的做法耗时很长。于是乎,我想到了indexOf检索数组中元素,但是由于该方法只返回指定的字符串值在数组中首次出现的位置,故不合符需求。所以采用另一个办法,lastIndexOf方法,可返回一个指定的字符串值最后出现的位置。
代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
for (var i = 0; i < nums.length - 1; i++) {
var searchVal = target - nums[i]; // 目标值
var index = nums.lastIndexOf(searchVal); // 目标索引
if (-1 != index && i != index) { // 存在
return [i, index];
}
}
};
执行用时:212ms
一口老血喷出来。。。没想到用时更加长了,我查了一下lastIndexOf的源码,发现其实它用了while循环,比分析一慢的原因是它每次检索都是全量检索的。
分析三:利用Js对象的特性,先声明一个temObj对象,key是数组值,value是数组下标。通过temObj[key]是否存在就可以判断数组是否存在该值。做法如下:
代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
var temObj = {};
for (var i = 0; i < nums.length; i++) {
var searchVal = target - nums[i]; // 要查找的值
if (undefined != temObj[searchVal]) {
return [temObj[searchVal], i]
}
// 将未通过判断的键值添加到temObj中
temObj[nums[i]] = i;
}
};
执行用时:76ms
完成日期:2018-05-06
leetcode案例
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
let hashTable = {};
for(let i = 0;i < nums.length;i ++){
let complement = target - nums[i];
if(hashTable.hasOwnProperty(complement)){
return [hashTable[complement],i];
}else{
hashTable[nums[i]] = i;
}
}
throw new Error('No two sum solutions');
};
执行用时:52ms
分析:此处思路通分析三类似,只不过用的检索方法是hasOwnProperty,这个方法可以用来检测一个对象是否含有特定的自身属性,该方法会忽略掉那些从原型链上继承到的属性。大家不要被执行时间迷惑便觉得hasOwnProperty比直接访问属性快,事实上是直接访问属性要比hasOwnProperty快。大家可以参考这个测试结果:Object hasOwnProperty vs direct 关于Jsperf测试Js代码性能的使用方法,我会另外发表一篇博客进行详细说明。
有效的数独
分析:这是我觉得稍微有点难的算法,写出来的感觉就像做数学题,其实就是找规律。首先,根据题目要求,提了三个条件,行和列都好计算,我们使用双重for循环来解决,行:board[i][j], 列:board[j][i]。而九宫格稍难,我们可将外层循环作为九宫格序号,内层循环作为格点序号,难点在于表示第i个九宫格第j个格点的坐标。
行号规律:
第0个九宫格:000111222; 第1个九宫格:000111222; 第2个九宫格:000111222;
第3个九宫格:333444555; 第4个九宫格:333444555; 第5个九宫格:333444555;
第6个九宫格:666777888; 第7个九宫格:666777888; 第8个九宫格:666777888;
每3个九宫格行号增3;对于1个九宫格,每3个格点行号增1。
所以第i个九宫格第j个格点的行号为:parseInt(i/3) * 3 + parseInt(j/3)
parseInt(i/3) * 3 对应 每3个九宫格增加3,不足三个算0个(向下取整)
parseInt(j/3) 对应 每3个格点加1,不足三个算0个(向下取整)
列号规律:
第0个九宫格:012012012; 第1个九宫格:345345345; 第2个九宫格:678678678;
第3个九宫格:012012012; 第4个九宫格:345345345; 第5个九宫格:678678678;
第6个九宫格:012012012; 第7个九宫格:345345345; 第8个九宫格:678678678;
对于下一个九宫格列号增3,循环周期为3;对于1个九宫格,每下一个格点列号增1,周期也为3。
所以第i个九宫格第j个格点的列号为:(i % 3) * 3 + (j % 3) * 1
(i % 3) * 3 = 0,3,6,(j % 3) * 1 = 0,1,2(数组下标从0开始)
代码:
/**
* @param {character[][]} board
* @return {boolean}
*/
var isValidSudoku = function(board) {
/*
ES6新增数据结构Set,类似于数组,但是成员的值都是唯一的,没有重复的值。
它的一些api在此处符合使用场景。
我们可以这样表示 行:board[i][j], 列:board[j][i]
*/
let rolSet = new Set(); // 行
let colSet = new Set(); // 列
let boardSet = new Set(); // 块
for (let i = 0; i < 9; i++) {
rolSet.clear();
colSet.clear();
boardSet.clear();
for (let j = 0; j < 9; j++) {
if ("." != board[i][j]) {
// 如果行中包含相同元素
if (rolSet.has(board[i][j])) {
return false
} else {
rolSet.add(board[i][j]);
}
}
if ("." != board[j][i]) {
// 如果列中包含相同元素
if (colSet.has(board[j][i])) {
return false
} else {
colSet.add(board[j][i]);
}
}
// 行号+偏移量
var RowIndex = 3 * parseInt(i / 3) + parseInt(j / 3);
// 列号+偏移量
var ColIndex = 3 * (i % 3) + j % 3;
if ("." != board[RowIndex][ColIndex]) {
if (boardSet.has(board[RowIndex][ColIndex])) {
return false
} else {
boardSet.add(board[RowIndex][ColIndex]);
}
}
}
}
return true;
};
执行用时:124ms
完成日期:2018-05-07
leetcode案例
/**
* @param {character[][]} board
* @return {boolean}
*/
var isValidSudoku = function(board) {
var a = new Array(9),
b = new Array(9),
c = new Array(9);
for (var k = 0; k < 9; k++) {
a[k] = new Array(9);
b[k] = new Array(9);
c[k] = new Array(9);
}
for (var i = 0; i < 9; i++) {
for (var j = 0; j < 9; j++) {
var temp = board[i][j];
if (temp === ".") {
continue;
}
if (a[i][temp - 1] || b[temp - 1][j] || c[Math.floor(i / 3) * 3 + Math.floor(j / 3)][temp - 1]) {
return false;
}
a[i][temp - 1] = b[temp - 1][j] = c[Math.floor(i / 3) * 3 + Math.floor(j / 3)][temp - 1] = true;
}
}
return true;
};
执行用时:76ms
分析:查了相关资料,该案例使用的是位图法,我们根据题目不难发现,在数独中可能出现的数字范围是1-9,所以它可以对应数组下标0-8,因此可以通过数组下标来存储元素。因此,一开始便创建了三个数组分别对应行、列、九宫格,数组结构如下:
可以看到,每个数组有9个元素,每个元素是一个长度为9的空数组。a代表行,b代表列,c代表九宫格。接着双重循环,将board中的值存储于数组下标,并将值置为true。例:
a[0][n]表示board第一行所有元素(除.外),board第一行所有元素的值(除.外)为 n+1 (0 < n < 9)。a[0][n] = true 则表示 board 第一行的值中已存在重复的 n;
b数组同理。
c,之前我们提到过 Math.floor(i / 3) * 3 + Math.floor(j / 3) 表示第i个九宫格的第j个元素格点的行号,第0个九宫格行号:000 111 222 共9个,所以c[0][n1],c[0][n2],c[0][n3],c[1][m1],c[1][m2],c[1][m3],c[2][p1],c[2][p2],c[2][p3],代表第0个九宫格的元素。
旋转图像
分析:线性代数里有个矩阵叫转置矩阵,例:
1 2 3 1 4 7
4 5 6 => 2 5 8
7 8 9 3 6 9
其实就是将一个矩阵行列互换。之所以提这个概念,是因为若我们将这个转置矩阵每行逆序会变成这样:
7 4 1
8 5 2
9 6 3
这就是我们希望得到的顺时针旋转90°的矩阵。求转置矩阵有个特点,对角线上的数不变。
2阶矩阵:第1行交换1次(列:2),第2行交换0次;
3阶矩阵:第1行交换2次(列:2,3),第2行交换1次(列:3);第3行交换0次;
4阶矩阵:第1行交换3次(列:2,3,4),第2行交换2次(列:3,4);第3行交换1次;第4行交换0次。
n阶矩阵:第1行交换n-1次(列:2,3,4 … n),第2行交换n-2次(列:3,4 … n) … 第n行交换0次;
外层循环表示行,内层循环表示列。j的初始值 = i + 1;
代码:
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var rotate = function(matrix) {
// 行:matrix[i][j] 列:matrix[j][i]
// 先求转置矩阵 matrix
for (let i = 0; i < matrix.length; i++) {
for (let j = i + 1; j < matrix.length; j++) {
var temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
matrix[i].reverse();
}
};
执行用时:68ms
求旋转矩阵的方法不止这一个,有兴趣的同学可以去翻翻同济版的线性代数,深入研究可以看高等代数。大学曾经深入学过,奈何如今只记得书本名。(- -||)
完成日期:2018-05-08
leetcode案例
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var rotate = function(matrix) {
var n = matrix.length;
var i, j;
for (i = 0; i < Math.floor(n / 2); i++) {
for (j = 0; j < n; j++) {
[matrix[i][j], matrix[n - 1 - i][j]] = [matrix[n - 1 - i][j], matrix[i][j]];
}
}
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (i >= j) continue;
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
};
执行用时:64ms
分析:作者思路是先首行和尾行互换,再对矩阵进行转置。例:
1 2 3 7 8 9 7 4 1
4 5 6 => 4 5 6 => 8 5 2
7 8 9 1 2 3 9 6 3
总结
不知不觉已经把初级数组的题刷完了,收获挺大的,有时候写算法题更像是在写数学题,答案不唯一,但是总会有更巧妙的解法,在寻找答案思考过程中,又不断地熟悉了一遍Js数组的api,也用了一些ES6里的东西。这篇博客相当于个人的读书笔记,只为在遇到问题的时候,能快速的将忘却的知识迅速捡起来,而不会像刚才一样,只记得概念,具体的东西还要查半天资料,再通过一系列头脑风暴,然后回忆起来。