最近看一些面試題,很多都提到了數組去重,用的最多的不外乎就是下面這個例子
arr.filter(function(value,index,arr){
return arr.indexOf(value,index+1) === -1
})
如果忽略其他因素,只從代碼簡潔度和易讀性來說,這個代碼確實是很好的,也確實體現了對js的掌握程度。
但是,從其他方面來說,這個代碼是否就真的是很好的呢?作為一個曾經的半吊子acmer,效率恐怕是我最在意的東西了。那我們就來看下效率吧。
以下所有實驗均基於nodejs環境。
首先,我們需要隨機生成一個足夠大的數組。這里就來個10萬吧。然后跨度是1-10000。(為什么要提跨度,這個后面再說),然后記錄下開始時間,並使用上面的代碼跑一下,最后記錄結束時間,並相減就可以了。
下面是代碼
1 var arr = []; 2 //生成隨機數組 3 for (var i = 100000; i >= 0; i--) { 4 arr.push(Math.ceil(Math.random()*10000)); 5 } 6 7 //開始時間 8 var now1 = new Date(); 9 var arr1 = arr.filter(function(value,index,arr){ 10 return arr.indexOf(value,index+1) === -1 11 }) 12 // console.log(arr1.sort(function(a,b){ 13 // return a-b; 14 // })); 15 //結束時間 16 var now2 = new Date(); 17 console.log(now2-now1);
那么10萬個跨度是10000的數組,效率是多少呢?
執行結果: 2000毫秒左右,3000毫秒以內(其他瀏覽器環境測試結果可能不盡相同)
這個效率,恩。。很一般。
我們看下代碼也不難得出這個結論。filter效率為n,indexOf效率為n,又是嵌套的,那么效率就是n²了。(這里有小問題,下面會解答)
接下來是我自己想的兩個去重的方法,對應不同的場景吧。
一:如果數組里面的值都是數字的話。那么,我們可以使用數字對應下標的方法來進行去重。比如說,我的數組是arr1=[1,3,3,3,5,6],那么我就新開一個數組arr2,arr2的值就是[1,,1,,1,1]
也就是說,只要是arr1中出現過的值,我就在arr2中找到對應的下標,並且賦值為1。
1 var now1 = new Date(); 2 3 //中間數組,用來記錄arr出現過的值 4 var tmp = []; 5 arr.forEach(function(value){ 6 tmp[value] = 1; 7 }); 8 9 var arr2 = []; 10 tmp.filter(function(value,index){ 11 if(value === 1){ 12 arr2.push(index); 13 } 14 }) 15 16 var now2 = new Date(); 17 console.log(now2-now1);
執行結果: 5毫秒左右,10毫秒以內
那么這個結果就很明顯,效率之間的差距不是一般的大。。。
從代碼也可以看出來,都只有一層循環,那么效率就是O(n)了。還是非常快的。
二:使用sort進行排序。並使用filter過濾數組即可
1 var now1 = new Date(); 2 //使用sort從小到大排序並過濾掉前后相同的元素 3 var arr3 = arr.sort(function(a,b){ 4 return a-b; 5 }).filter(function(value,index){ 6 return arr[index] !== arr[index-1]; 7 }); 8 var now2 = new Date(); 9 console.log(now2-now1);
執行結果: 80毫秒左右,100毫秒以內
可以看出來這個結果也還算可以(對比第一種)
不難看出來,node中sort的效率應該在nlogn(-。-廢話么這不是)
並且2次遍歷數組的消耗基本可以忽略不計(從第二種可以看出來)
那么為什么既然比第二個的效率低,又沒第一個簡潔,為什么要用這種方法呢,第一是可以擴寬思路,第二是因為這種方法的適用范圍比第二種要高。
比如說,我要去重的數組不是數字,而是字符。那么第二種方法就無法使用(可以使用對象來實現,不過不知道對象遍歷的效率高不高,留作以后測試)。並且, 第一種方法如果其中有一個數字為10E,那么空間消耗就很大(js中不確定,大部分語言是這樣)。因為要開一個10E的數組。。。以前比賽的時候用c開個1000萬的數組就超了。
三個方法放一次看下:
1 var arr = []; 2 //生成隨機數組 3 for (var i = 100000; i >= 0; i--) { 4 arr.push(Math.ceil(Math.random()*10000)); 5 } 6 7 //開始時間 8 var now1 = new Date(); 9 var arr1 = arr.filter(function(value,index,arr){ 10 return arr.indexOf(value,index+1) === -1 11 }) 12 13 //結束時間 14 var now2 = new Date(); 15 console.log(now2-now1); 16 17 console.log("------------------------"); 18 19 now1 = new Date(); 20 21 //中間數組,用來記錄arr出現過的值 22 var tmp = []; 23 arr.forEach(function(value){ 24 tmp[value] = 1; 25 }); 26 27 var arr2 = []; 28 tmp.filter(function(value,index){ 29 if(value === 1){ 30 arr2.push(index); 31 } 32 }) 33 34 now2 = new Date(); 35 console.log(now2-now1); 36 37 console.log("------------------------"); 38 39 now1 = new Date(); 40 //使用sort從小到大排序並過濾掉前后相同的元素 41 arr3 = arr.sort(function(a,b){ 42 return a-b; 43 }).filter(function(value,index){ 44 return arr[index] !== arr[index-1]; 45 }); 46 //過濾數組 47 now2 = new Date(); 48 console.log(now2-now1);
效率對比就非常明顯了。
下面來說一下之前留下的坑:
1、為什么要用跨度呢,因為實驗中發現(一開始並沒有細想),如果數組跨度越下,那么第一種方法的速度就越快,但是其他兩種,尤其是第二種的變化幅度就沒那么劇烈,這是為什么呢?
其實是因為,indexOf的機制造成的。indexOf的實現是從你規定的位置開始往后找,找到第一個就停下來。所以很明顯的,如果跨度越小,那么出現重復數字的幾率就越高,那么就越有可能很快有返回結果,所以跨度越小就會越接近n的效率。
反觀其他兩個方法,不管你跨度多少,影響的都是數組的長度,那么影響就只在遍歷一遍數組的效率這方面,所以就很小。
2、其實第一種方法的2000毫秒左右的效率是基於是隨機數組的情況下,那么如果我們把數組改成順序數組,也就是沒有重復的數組呢?
我們來試驗一下
var arr = []; //生成順序數組 for (var i = 100000; i >= 0; i--) { arr.push(i); }
看到了吧,這才是真正的n²的效率,之前的2000毫秒和這個還是有常數級別的區別的。
我們還發現,第三種方法的速度也變久了。將近翻了一倍,這個的話應該是sort內部實現的問題,我也不知道node內部sort使用的是什么排序,不過排序方法一般都是對順序或者倒序和隨機之間有一定的差距,這個很正常。
實驗做完了,我們來總結一下:
第一種:
優點:簡潔,明了,一定程度上可以看出對js的掌握程度,適用范圍很廣,並且不會改變數組元素的相對位置(分為對后去重和對前去重兩種)。
缺點:效率非常慢,尤其是在重復數非常少的情況下。
第二種:
優點:速度非常快,受其他因素影響小(如果數量少,跨度大的話,也就是稀疏數組,反而會比其他的慢)。
缺點:適用范圍比較小,只能適合數字數組,並且數組不能過大。如果為其他數組,可以使用對象作為數組。遍歷使用for in 即可,效率和第三種差距不大。代碼如下,可自行實驗。
1 var now1 = new Date(); 2 3 //中間對象 4 var tmp = {}; 5 arr.forEach(function(value){ 6 tmp[value+'we'] = 1; 7 }) 8 9 var arr3 = []; 10 for(index in tmp){ 11 if(tmp[index] === 1){ 12 arr3.push(index); 13 } 14 } 15 16 var now2 = new Date(); 17 console.log(now2-now1);
第三種:
優點:速度相對較快,受其他因素影響不大,適用范圍也相對較廣。
缺點:中軌終於,適用范圍不如第一種大,速度不如第三種快。會受其他因素一定的影響。