在我的上一篇文章中,總結了六種排序演算法的JavaScript實現,以及每種演算法的思想,掘金上的許多盆友提出了一些好的想法或者優化的方法,這裡針對這些方法做一些新的測試,以驗證盆友們的說法。此外,非常感謝大家仔細閱讀我的文章,你們的意見讓我進步很大,同時意識到自身的許多不足,我還是會繼續努力的。
首先還是說明一下,為了方便測試,專門寫了一個隨機陣列生成函式,程式碼如下:
exports.generateArray = function(length) {
let arr = Array(length);
for(let i=0; i<length; i++) {
arr[i] = Math.random();
}
return arr;
};
複製程式碼
只要輸入陣列的長度,就可以得到滿足要求的隨機陣列供你測試。
一、和原生介面比哪個快?
這是由@JHanLu提出的想法,確實是個很不錯的想法,在這裡進行一下測試。比較物件是Array.prototype.sort
方法和快速排序。
測試原生方法:
console.time(`RawSort`);
const arr = generateArray(10000000);
arr.sort((a, b) => a - b);
console.timeEnd(`RawSort`);
複製程式碼
針對10000000(一千萬)條資料進行從小到大的排序,耗時為12.7s:
RawSort: 12653.942ms
複製程式碼
針對10000(一萬)條資料進行排序,耗時0.023秒:
RawSort: 23.382ms
複製程式碼
測試快排:
程式碼如下:
function quickSort(arr) {
let left = 0,
right = arr.length - 1;
console.time(`QuickSort`);
main(arr, left, right);
console.timeEnd(`QuickSort`);
return arr;
function main(arr, left, right) {
if(arr.length === 1) {
return;
}
let index = partition(arr, left, right);
if(left < index - 1) {
main(arr, left, index - 1);
}
if(index < right) {
main(arr, index, right);
}
}
function partition(arr, left, right) {
let pivot = arr[Math.floor((left + right) / 2)];
while(left <= right) {
while(arr[left] < pivot) {
left++;
}
while(arr[right] > pivot) {
right--;
}
if(left <= right) {
[arr[left], arr[right]] = [arr[right], arr[left]];
left++;
right--;
}
}
return left;
}
}
console.log(quickSort(generateArray(10000000)));
複製程式碼
針對10000000(一千萬)條資料從小到大進行排序,耗時3.6s:
QuickSort: 3632.852ms
複製程式碼
針對10000(一萬)條資料進行排序,耗時0.012s:
QuickSort: 12.482ms
複製程式碼
可以看出來不管是大資料量還是小資料量,都是手動實現的快排更加快速,但是優勢不是很大。猜測原因可能是原生方法實際上還是要呼叫底層的快排來實現排序,比手動實現的快排多了一層封裝導致速度有稍微下降。
針對原生方法,還去了解了一下,發現V8引擎是不穩定排序,它根據陣列長度來選擇排序演算法的。當陣列長度在10以下時,會採用插入排序;陣列長度在10以上時,會採用快速排序,詳情參考:Array.prototype.sort()方法到底是如何排序的?。
由以上結論可以發現,當前端要排序的資料量比較大(千萬級,當然基本不太可能)時,最好還是使用手動實現的快排,速度會比較快。資料量不大時,完全可以使用Array.prototype.sort
進行排序,畢竟一千萬條資料的時間差也不超過兩秒。雖然原生方法可以滿足我們的要求,但是這也絕不是前端工程師不學習排序和演算法的理由。
二、氣泡排序寫錯了?
上一篇文章中,我在第二層迴圈內也是寫的從頭到尾的遍歷,但是@雪之祈舞
提出來,可以進行優化,因為沒遍歷一次資料,符合要求的資料就自動排序到末尾了(比如第一輪排序最大值就到了最後一個位置),所以在之後的遍歷中,都可以忽略陣列的後i項,這是正確的做法。接下來進行對比:
上一篇文章中的寫法:
function bubbleSort(arr) {
console.time(`BubbleSort`);
let len = arr.length;
let count = 0;
arr.forEach(() => {
// 這裡的i<len - 1實際上應該是i<len - 1 - i。
for(let i=0; i<len-1; i++) {
if(arr[i] > arr[i+1]) {
let tmp = arr[i+1];
arr[i] = tmp;
arr[i+1] = arr[i]
}
count++;
}
});
console.timeEnd(`BubbleSort`);
return count;
}
console.log(bubbleSort(generateArray(20000)));
複製程式碼
測試排序20000(兩萬)條資料,耗時1.27s:
BubbleSort: 1277.692ms
複製程式碼
而改進程式碼後:
function bubbleSort(arr) {
console.time(`BubbleSort`);
let len = arr.length;
let count = 0;
arr.forEach(() => {
for(let i=0; i<len-1-i; i++) {
if(arr[i] > arr[i+1]) {
let tmp = arr[i+1];
arr[i] = tmp;
arr[i+1] = arr[i]
}
count++;
}
});
console.timeEnd(`BubbleSort`);
return count;
}
console.log(bubbleSort(generateArray(20000)));
複製程式碼
測試20000(兩萬)條資料,耗時0.64s:
BubbleSort: 638.900ms
複製程式碼
可以發現幾乎縮短了一半的時間。
三、選擇排序不夠優秀!
這裡確實是我懶的緣故,最優秀的做法是每次遍歷找出最小值(或者最大值)儲存到一個變數中,而我是每次發現小於(或大於)參考值的元素就會將它們進行對調,這樣雖然時間複雜度沒變,但是變數交換花費的時間較多,造成了效能下降,感謝使用者@ly578269725的指導,由於我的懶惰可能會導致別人的誤會,我表示抱歉。下面是實測結果:
懶惰的寫法:
function selectionSort(arr) {
console.time(`SelectionSort`);
let len = arr.length;
let count = 0;
arr.forEach((item, index) => {
for(let i=index; i<len; i++) {
if(arr[i] > arr[index]) {
[arr[index], arr[i]] = [arr[i], arr[index]];
}
count++;
}
});
console.log(arr);
console.timeEnd(`SelectionSort`);
return count;
}
console.log(selectionSort(generateArray(30000)));
複製程式碼
對20000(兩萬)條資料進行排序,耗時5.2s:
SelectionSort: 5227.515ms
複製程式碼
而改良後:
function selectionSort(arr) {
console.time(`SelectionSort`);
let len = arr.length;
let count = 0;
arr.forEach((item, index) => {
let min = index;
for(let i=index; i<len; i++) {
if(arr[i] < arr[index]) {
min = i;
}
count++;
}
if(min !== index) {
[arr[min], arr[index]] = [arr[index], arr[min]];
}
});
console.log(arr);
console.timeEnd(`SelectionSort`);
return count;
}
console.log(selectionSort(generateArray(20000)));
複製程式碼
對20000(兩萬)條資料進行排序,耗時1.25s:
SelectionSort: 1250.662ms
複製程式碼
可以發現確實效能有了提升,此處也可以發現交換數值比賦值消耗更大。
四、阮老師的快排錯了嗎?
前段時間很火的一篇文章,說阮老師的快速排序是完全錯誤的,指的就是這篇文章:快速排序(Quicksort)的Javascript實現。關於阮老師這篇文章錯了沒有,知乎早就有了答案:如何看待文章《面試官:阮一峰版的快速排序完全是錯的》?,包括廖雪峰等大佬都表示阮老師的文章沒有什麼大問題,至少不能說是完全錯誤!阮老師的這篇文章寫於2011年,當時根本就不是程式設計師,而且他的這篇文章本來就是講思想多於程式碼,通過阮老師的講解,你很容易就能明白快排是怎麼回事,要說他哪裡不對,無非就是使用了Array.prototype.splice
方法來刪除原陣列中的pivot元素並獲取這個pivot,這個方法會增加演算法的複雜度,另外阮老師增加了空間複雜度,實現的程式碼不是最優方案。但是這不是說他的程式碼完全錯誤的理由,演算法本來就是思想多於程式碼的東西,你可以說他的程式碼不夠優秀(實際上本來就是面向初學者的,那樣的程式碼更好理解),但是不可以說他的程式碼完全錯誤,因為他完完全全遵守了快速排序的核心思想:分治。就像廖雪峰老師所說的一樣:人家教程主要是講思路,快排的基本思想是分治。非要用生產,Array.sort()瞭解一下?
實際上,阮老師在他的網站JavaScript 標準參考教程(alpha)上,早就實現了優化的快速排序,部落格的時間是2013年1月30日,真當人家不懂嗎?