从一道编程题看JS字符串连接性能

马上就要秋招了,又进入了刷题的季节。在刷题中进步,在刷题中成长。今天就讲一道刷题的趣事。文章结尾给出一些用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语法。

好,目前想到就这些,之后再补充吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章