资讯专栏INFORMATION COLUMN

IOS 中 sort 方法的兼容问题

yeyan1996 / 997人阅读

摘要:快速排序在解决中方法问题时,笔者没有考虑时间复杂度的问题,使用的排序算法进行重写,在实际产品环境中引发不小的性能问题。中方法的兼容问题笔者发现或者中方法不生效不同浏览器实现机制差异,故判断后进行该方法的重写处理,代码如下

快速排序(update)

在解决 Sarafi 中 sort 方法问题时,笔者没有考虑时间复杂度的问题,使用 O(n2) 的排序算法进行重写,在实际产品环境中引发不小的性能问题。

阅读 v8 array.js 源码(Array.js)后发现,Chrome 在实现 sort 方法时对小数组(length <= 10)进行插入排序,对大数组进行快速排序 O(nlogn),来降低该方法的时间复杂度。

快速排序的核心是不断把原数组做切割,切割成小数组后再对小数组进行相同的处理,这是一种典型的分治的算法设计思路,选取数组中第一个元素作为基准,可对其进行简单实现如下:

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 层迭代。

对于一个内容随机的数组而言,不太可能出现最差情况,但日常处理的数组往往并不是内容随机的,一种很容易的解决方案是不要选取固定位置的元素作为基准元素,而是随机从数组里挑出一个元素作为基准元素,这样可以极大概率地避免最差情况,然而这并不等于避免最差情况,特别是在数组很大的时候,更要求我们更谨慎地选取基准元素。

三数取中(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];
}

其他比较典型的取中值手段包括:

一种是平均间隔取一个元素,多个元素取中位数(即多取几个,增加可靠性)

一种是对三数取中进行递归运算,先把大数组平均分成三块,对每一块进行三数取中,会得到三个中值,再对这三个中值取中位数

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);

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83446.html

相关文章

  • 前端开发:那些我遇到bug (持续更新)

    摘要:暂未找到完美的解决方法,各位看官发现了记得评论提醒一下安卓移动端浏览器设置无效,无法多选图片问题该问题同样暂未找到完美的解决方案别的现在一下子想不起来了。。。 从事前端开发将满一年了,期间遇到不少问题,最坑的是一些自己不知道的坑。所以写出来警示后人。 1. ios端的sort方法无效描述:之前做一个小程序的聊天列表的时候需要用到sort进行列表排序。嗯,后来有用户反应最新回复不置顶。。...

    曹金海 评论0 收藏0
  • 前端开发:那些我遇到bug (持续更新)

    摘要:暂未找到完美的解决方法,各位看官发现了记得评论提醒一下安卓移动端浏览器设置无效,无法多选图片问题该问题同样暂未找到完美的解决方案别的现在一下子想不起来了。。。 从事前端开发将满一年了,期间遇到不少问题,最坑的是一些自己不知道的坑。所以写出来警示后人。 1. ios端的sort方法无效描述:之前做一个小程序的聊天列表的时候需要用到sort进行列表排序。嗯,后来有用户反应最新回复不置顶。。...

    PAMPANG 评论0 收藏0
  • JavaScript sort() 排序坑详解

    摘要:前言做项目的时候发现使用排序后的代码,在和平台解析的结果不一样。而根据规范,通过可以推测出,显然这里互相矛盾反之亦然的情况。 前言:做项目的时候发现使用sort排序后的代码,在android和ios平台解析的结果不一样。showImg(https://segmentfault.com/img/bVbn0y2?w=150&h=150); 1、先从简单的开始,大家都知道sort()函数比较...

    ispring 评论0 收藏0
  • 解决PHP获取不了 React Native Fecth参数问题

    摘要:使用进行网络请求推荐的形式进行数据处理。这个时候自己去搜索了下,提出了两种解决方案构建表单数据参考这里但是这个在自己的机器上并不生效。服务端解决方案获取里面的内容,在中可以这样写这个时候就可以打印出数据了。 React Native 使用 fetch 进行网络请求,推荐Promise的形式进行数据处理。官方的 Demo 如下: fetch(https://mywebsite.com/e...

    edagarli 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<