對於JavaScript實現排序演算法的一些其他測試

Russ_Zhong發表於2019-02-25

在我的上一篇文章中,總結了六種排序演算法的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日,真當人家不懂嗎?

相關文章