IOS 中 sort 方法的兼容問題

快速排序(update)

在解決 Sarafi 中 sort 方法問題時,筆者沒有考慮時間複雜度的問題,使用 O(n2) 的排序算法進行重寫,在實際產品環境中引起不小的性能問題。git

閱讀 v8 array.js 源碼(Array.js)後發現,Chrome 在實現 sort 方法時對小數組(length <= 10)進行插入排序,對大數組進行快速排序 O(nlogn),來下降該方法的時間複雜度。github

快速排序的核心是不斷把原數組作切割,切割成小數組後再對小數組進行相同的處理,這是一種典型的分治的算法設計思路,選取數組中第一個元素做爲基準,可對其進行簡單實現以下:web

function QuickSort(arr, func) {
    if (!arr || !arr.length) return [];
    if (arr.length === 1) return arr;
    
    var pivot = arr[0];
    var smallSet = [];
    var bigSet = [];
    for (var i = 1; i < arr.length; i++) {
        if (func(arr[i], pivot) < 0) {
            smallSet.push(arr[i]);
        } else {
            bigSet.push(arr[i]);
        }
    }
    
    return basicSort(smallSet, func).concat([pivot]).concat(basicSort(bigSet, func));
}

上面的算法建立一個新的數組做爲計算結果,從空間使用的角度看是不經濟的,Javascript 的快速排序算法中並無像上面的代碼那樣建立一個新的數組,而是在原數組的基礎上,經過交換元素位置實現排序,故而相似於 push、 pop、 splice 這幾個方法,sort 方法也是會修改原數組對象的。算法

function swap(arr, from, to) {
    if (from === to) return;

    let temp = arr[from];
    arr[from] = arr[to];
    arr[to] = temp;
}
 
function QuickSortWithPartition(arr, fn, from, to) {
    from = from === void 0 ? 0 : from;
    to = to === void 0 ? arr.length : to;

    if (from >= to - 1) {
        return arr;
    }

    let pivot = arr[from];
    let smallIndex = from;
    let bigIndex = from + 1;

    for (; bigIndex < to; bigIndex++) {
        if (fn(arr[bigIndex], pivot) < 0) {
            smallIndex++;
            swap(arr, smallIndex, bigIndex);
        }
    }

    swap(arr, smallIndex, from);

    QuickSortWithPartition(arr, fn, from, smallIndex - 1);
    QuickSortWithPartition(arr, fn, smallIndex + 1, to);

    return arr;
}

其中,from 是起始索引,to 是終止索引,若是這兩個參數缺失,則表示處理整個數組。數組

由於上面的分區過程當中,大數分區和小數分區都是從左向右增加,其實咱們能夠考慮從兩側向中間遍歷,這樣能有效地減小交換元素的次數。舉個例子,假如咱們有一個數組 [2, 1, 3, 1, 3, 1, 3],採用上面的分區算法一共會碰到三次比基準元素小的狀況,因此會發生三次交換;而若是咱們換個思路,把從右往左找到小於基準的元素,和從左往右找到大於基準的元素交換,這個數組只須要交換一次便可完成排序(把第一個3和最後一個1交換)。瀏覽器

function QuickSortWithPartitionOp(arr, fn, from, to) {
    from = from === void 0 ? 0 : from;
    to = to === void 0 ? arr.length : to;

    if (from >= to - 1) {
        return arr;
    }

    let pivot = arr[from];
    let smallEnd = from;
    let bigBegin = to - 1;

    while (smallEnd < bigBegin) {
        while (fn(arr[bigBegin], pivot) >= 0 && smallEnd < bigBegin) {
            bigBegin--;
        }

        while (fn(arr[smallEnd], pivot) <= 0 && smallEnd < bigBegin) {
            smallEnd++;
        }

        if (smallEnd < bigBegin) {
            swap(arr, smallEnd, bigBegin);
        }
    }

    swap(arr, smallEnd, from);

    QuickSortWithPartitionOp(arr, fn, from, smallEnd - 1);
    QuickSortWithPartitionOp(arr, fn, smallEnd + 1, to);

    return arr;
}

快速排序算法平均時間複雜度是 O(nlogn),但它的最差狀況下時間複雜度會增大到 O(n2),其性能好壞的關鍵就在於分區是否合理:若是每次都能平均分紅相等的兩個分區,那麼只須要 logn 層遞歸;而若是每次分區都不合理,總有一個分區是空的,則須要 n 層迭代。app

對於一個內容隨機的數組而言,不太可能出現最差狀況,但平常處理的數組每每並非內容隨機的,一種很容易的解決方案是不要選取固定位置的元素做爲基準元素,而是隨機從數組裏挑出一個元素做爲基準元素,這樣能夠極大機率地避免最差狀況,然而這並不等於避免最差狀況,特別是在數組很大的時候,更要求咱們更謹慎地選取基準元素。性能

三數取中(median-of-three)

三數取中法是挑選基準元素的一種經常使用方法:即挑選基準元素時,先把第一個元素、最後一個元素和中間一個元素挑出來,這三個元素中大小在中間的那個元素就被認爲是基準元素。優化

function getPivot(arr, fn, from, to) {
    let mid = (from + to) >> 1;

    if (fn(arr[from], arr[mid]) < 0) {
        swap(arr, from, mid);
    }

    if (fn(arr[from], arr[to]) > 0) {
        swap(arr, from, to);
    }

    if (fn(arr[to], arr[mid]) < 0) {
        swap(arr, to, mid);
    }

    return arr[from];
}

其餘比較典型的取中值手段包括:ui

  • 一種是平均間隔取一個元素,多個元素取中位數(即多取幾個,增長可靠性)
  • 一種是對三數取中進行遞歸運算,先把大數組平均分紅三塊,對每一塊進行三數取中,會獲得三個中值,再對這三個中值取中位數

v8 源碼中的基準元素選取更爲複雜:若是數組長度不超過1000,則進行基本的三數取中;若是數組長度超過1000,那麼 v8 的處理是除去首尾的元素,對剩下的元素每隔200左右挑出一個元素,對這些元素排序,找出中間的那個,並用這個元素跟原數組首尾兩個元素一塊兒進行三數取中。

針對重複元素的處理(三路劃分)

設想一下,一個數組裏若是全部元素都相等,基準元素無論怎麼選都是同樣的,那麼在分區的時候,必然出現除基準元素外的其餘元素都被分到同一個分區的狀況,進入最差性能的 case。

那麼對於重複元素應該怎麼處理呢?
從性能的角度,若是發現一個元素與基準元素相同,那麼它應該被記錄下來,避免後續再進行沒必要要的比較。

function QuickSortWithPartitionDump(arr, fn, from, to) {
    from = from === void 0 ? 0 : from;
    to = to === void 0 ? arr.length - 1 : to;

    if (from >= to) {
        return arr;
    }

    let pivot = getPivot(arr, fn, from, to);
    let smallEnd = from;
    let bigBegin = to;
    let i = from + 1;

    while (i <= bigBegin) {
        let r = fn(arr[i], pivot);
        if (r < 0) {
            swap(arr, smallEnd++, i++);
        } else if (r > 0) {
            swap(arr, i, bigBegin--);
        } else {
            i += 1;
        }
    }

    QuickSortWithPartitionDump(arr, fn, from, smallEnd - 1);
    QuickSortWithPartitionDump(arr, fn, bigBegin + 1, to);

    return arr;
}

針對小數組的優化

對於小數組,可能使用快速排序的速度還不如平均複雜度更高的插入排序,故而出於減小遞歸深度的考慮,數組長度較小時,使用插入排序算法。

function insertSort(arr, fn, from, to) {
    for (let i = from; i < to; i++) {
        for (let j = i + 1; j < to; j++) {
            let t = fn(arr[i], arr[j]);
            let r = (typeof t === 'number' ? t : t ? 1 : 0) > 0;
            if (r) {
                let tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }
        }
    }

    return arr;
}

v8 引擎額外作的優化

快速排序若是遞歸太深的話很能夠出現「爆棧」,上面提到的對小數組採用插入排序算法,以及採用內省排序算法均可以減小遞歸深度,不過 v8 引擎中還作了一些不太常見的優化:每次分區後,v8 引擎會選擇元素少的分區進行遞歸,而將元素多的分區直接經過循環處理,無疑能夠大大減少遞歸深度。

v8 引擎沒有作的優化

因爲快速排序時間複雜度的不穩定性,David Musser 於1997設計了內省排序法(Introsort),這個算法在快速排序的基礎上,監控遞歸的深度:一旦長度爲 n 的數組通過 logn 層遞歸(快速排序算法最佳狀況下的遞歸層數)尚未結束的話,就認爲此次快速排序的效率可能不理想,轉而將剩餘部分換用其餘排序算法,一般使用堆排序算法(Heapsort,最差時間複雜度和最優時間複雜度均爲 O(nlogn))。

IOS 中 sort 方法的兼容問題

筆者發現 Safari 或者 iPhone 中 sort 方法不生效(不一樣瀏覽器實現機制差別),故判斷後進行該方法的重寫處理,代碼以下:

;(function(w){
    if(/msie|applewebkit.+safari/i.test(w.navigator.userAgent)){
        var _sort = Array.prototype.sort;
        Array.prototype.sort = function(fn){
            if(!!fn && typeof fn === 'function'){
                if(this.length < 2) return this;
                var i = 0, j = i + 1, l = this.length, tmp, r = false, t = 0;
                for(; i < l; i++){
                    for(j = i + 1; j < l; j++){
                        t = fn.call(this, this[i], this[j]);
                        r = (typeof t === 'number' ? t : !!t ? 1 : 0) > 0 ? true : false;
                        if(r){
                            tmp = this[i];
                            this[i] = this[j];
                            this[j] = tmp;
                        }
                    }
                }
                return this;
            } else {
                return _sort.call(this);
            }
        };
    }
})(window);
相關文章
相關標籤/搜索