马上就要秋招了,又进入了刷题的季节。在刷题中进步,在刷题中成长。今天就讲一道刷题的趣事。文章结尾给出一些用JS做编程题的小技巧。
在讲之前呢,先说一些题外话。之前感觉主流算法编程语言是C、C++、Java,作为前端是很不服气的。但最近题做多了,发现这样不是没有道理的。现在大部分OJ都使用的是Chrome V8,毕竟性能是几大JS引擎中最好的。然后所有OJ封装的输入方法都是readline(牛客网readline()
,赛码网read_line()
,值得一提的是赛码网因为用的V8版本较低,所以有一个每次读入超过1024大小的字符串时要读多次的bug),输出方法是print()
。这就导致了一个问题:输入只能一下读完,有需要再做分割;输出只有一行时,只能把全部结果都封装好输出,因为print()
每次输出都有换行。不像上面三种语言,可以边输入边做处理,输出也能分批输出。这就导致,JS从一开始就输了,虽然时间、空间给编译型语言的要求要低,但感觉还是很不爽的。这就要求程序必须写得要讲究,能少做一些处理就少做一些,以减少时间的浪费。
好了,牢骚发完了,生活还是要继续,不是吗!言归正传,首先看一道牛客网上的今年网易内推的编程题操作序列:
小易有一个长度为n的整数序列,a_1,…,a_n。然后考虑在一个空序列b上进行n次以下操作:
1、将a_i放入b序列的末尾
2、逆置b序列
小易需要你计算输出操作n次之后的b序列。
输入描述:
输入包括两行,第一行包括一个整数n(2 ≤ n ≤ 2*10^5),即序列的长度。
第二行包括n个整数a_i(1 ≤ a_i ≤ 10^9),即序列a中的每个整数,以空格分割。
输出描述:
在一行中输出操作n次之后的b序列,以空格分割,行末无空格。
示例1
输入
4
1 2 3 4
输出
4 2 1 3
这道题并不难,写几个例子,不难发现规律。如输入1234,结果为4213,;输入12345,输出为53124。可以看出对于数字序列,每次都是从末端隔一个输出,到头之后再继续隔一个输出。好了,可以写代码了:
var n=+readline();
var arr=readline().split(' ').map(item=>+item);
var result='';
for(var i=n-1;i>=0;i-=2){
result+=arr[i]+' ';
}
if(i==-1)i=0;
else i=1;
for(;i<n;i+=2){
result+=arr[i]+' ';
}
print(result.trim());//去掉末尾的空格
程序没有写错,我兴致冲冲的运行却发现,OJ提示“内存超限:您的程序使用了超过限制的内存。case通过率为50.00%”,这就有些尴尬了。简单的题拿不到满分,笔试过不过就很难说了。这时候必须要想想代码哪个地方可以优化了。首先一个优化点在于map()
。因为从头到尾操作的是字符串,并没有用到数字的操作,所以用map()
操作来完成字符串到数字的转变就没有必要了。但是提交之后没有效果。那就再想想吧。这时候的有效代码只有字符串拼接了。突然想起JS高程中一段话:
针对
var lang='java';lang=lang+'Script'
。实现这个过程的过程如下:首先创建一个能容纳10个字符的新字符串,然后在这个字符串中填充“Java”和“Script”,最后一步是销毁原来的字符串“Java”和字符串“Script”,因为这两个字符串已经没用了。
当时我确实是这么想的,认为这是性能瓶颈所在。但忘了后面的一句话,“这也是在某些旧版本浏览器(丽日版本低于1.0的Firefox、IE6等)中拼接字符串时速度很慢的原因所在。但这些浏览器后来的版本已经解决了这个低效率问题。”按理说,V8应该不会发生这种问题。但经过测试,还是发现对于大字符串的拼接,+=
效率仍然很低。后面测试可以看到。
既然不用+=
,自然想到了数组的join()
方法进行拼接。事实上该题也适用该方法,毕竟最开始字符串都保存在数组中。于是有了下面的代码:
var n=+readline();
var arr=readline().split(' ');
var arr1=Array(Math.ceil(n/2)),arr2=Array(Math.floor(n/2)),i=0,j=0;
arr.forEach((item,index)=>index%2==0?arr1[i++]=item:arr2[j++]=item);
if(n%2==0){
print(arr2.reverse().join(' ').concat(' ',arr1.join(' ')));
}else{
print(arr1.reverse().join(' ').concat(' ',arr2.join(' ')));
}
该代码多用到了两个数组,还用了reverse()操作竟然还没有超限,说明数组join()
性能确实要比+=
好。另外注意到案例在50%的时候n=200000,每个数字又至少一个字符,所以整个字符串长度至少为200000,频繁的+=
的性能在此时已经较低了。
在评论区看了很多代码,发现其实本题一个好的做法应该是双向数组。
var n = +readline();
var a = readline().split(" ");
var num = Math.floor(n/2);
var b = Array(n);
var nex = num;
var pre = num - 1;
for( var i=0; i<n-1; i+=2){
b[nex++]= a[i];
b[pre--]= a[i+1];
}
if(a[i]){
b[nex] = a[i];
b.reverse();
}
print(b.join(" "));
下面测试一下用join和+=两种方法完成字符串连接的性能,测试使用Chrome内核版本为50.0的360极速浏览器(原谅我经常用这个浏览器),使用console.time()
和console.timeEnd()
查看时间花费。相关代码如下:
function testArray(n){
var arr=Array(n);
var code='0123456789';
for(var i=0;i<n;i++){
arr[i]=code[Math.floor(Math.random()*n)];
}
for(var i=0;i<5;i++){
(function(){
console.log('-----------------------')
var l=arr.length;
console.time('join');
var tmp=Array(l);
for(var j=0;j<l;j++){
tmp[j]=arr[j];
}
var str1=tmp.join(' ');
console.timeEnd('join')
console.time('+=');
var str2='';
for(var j=0;j<l;j++){
str2+=arr[j]+' ';
}
console.timeEnd('+=');
})()
}
for(var i=0;i<5;i++){
(function(){
console.log('-----------------------')
var l=arr.length;
console.time('+=')
var str2='';
for(var j=0;j<l;j++){
str2+=arr[j]+' ';
}
console.timeEnd('+=')
console.time('join')
var tmp=Array(l);
for(var j=0;j<l;j++){
tmp[j]=arr[j];
}
var str1=tmp.join(' ');
console.timeEnd('join')
})()
}
}
function testCase(userCase){
for(var i=0;i<userCase.length;i++){
console.log('-----------------------')
console.log('字符串长度:',userCase[i]);
testArray(userCase[i]);
}
}
testCase([1e3,1e4,1e5,1e6,1e7])
我们测试分别测试1000/10000/100000/1000000/10000000个字符时两种方法的性能,每种测试10次,最终得到结果如下图所示:
从中可以看出在大数据时,join()
性能要好于+=
性能。
好了,这就是本文的内容。
最后,给出几条关于笔试JS编程的建议。
1. 遍历用缓存数组长度的原生for循环,要好于forEach、map等,但后两者可以使代码简单,请酌情使用。
2. 对字符串用+操作符转化为整数性能较高,遇到字符串转整数可优先考虑。若输入第一行为个数时,最好直接转为整数,如var n=+readline();
,以免之后用到var arr=Array(n+1)
时,个数被转为数组项的尴尬。
3. Math.floor()
性能由于parseInt()
、等,不过用两个取反符将小数转为整数还是挺高端的,说不定给考官留下好印象。
4. OJ如牛客网,支持ES6语法,可以使用ES6语法,显得高端。很可惜,赛码网目前不支持ES6语法。
好,目前想到就这些,之后再补充吧。