從一道編程題看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語法。

好,目前想到就這些,之後再補充吧。

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