为了账号安全,请及时绑定邮箱和手机立即绑定

对数据结构和算法的总结和思考(四)--快速排序

快速排序,顾名思义就是快,多快呢?亲测了下,整体性能除了计数排序无人能比,不过计数排序有一个很大的劣势是只能对整数进行排序,这就大大限制了使用场景。所以,快速排序应该是目前表现最好的排序方法,各种解释器都有快速排序的实现,并且快速排序实现简单易于理解。实现原理:找出一个标杆元素,将比标杆元素大的放到右边,小的放到左边,然后递归合并。具体代码如下:

// 快速排序

// 找出基准值,取出基准值,遍历放入

function quick(arr) {
    if (arr.length < 2) return arr;

    let flag = parseInt(arr.length / 2);
    let flagValue = arr.splice(flag, 1);

    let leftArr = [], rightArr = [];
    for (let i = 0; i < arr.length; i ++) {
        if (arr[i] > flagValue[0]) {
            rightArr.push(arr[i]);
        } else {
            leftArr.push(arr[i]);
        }
    }

    return quick(leftArr).concat(flagValue).concat(rightArr);
}

let sortArr = [];
for (let i = 0;i < 1000; i ++) {
    sortArr.push(parseInt(Math.random() * 10000));
}

console.time('native sort ');
console.log(sortArr.sort(function(a, b) {
    return a- b;
}));
console.timeEnd('native sort ');

console.time('快速排序耗时 ');
console.log(quick(sortArr));
console.timeEnd('快速排序耗时 ');

这里用了js内置排序算法和快速排序比较,表现性能差不多。快速排序有个地方需要注意,一定要吧标杆元素取出来,然后再循环数组,不然很肯能出现死循环的情况。

既然快速排序是根据标杆元素来分开元素,那么标杆元素的选取就会严重的影响性能,如果不小心选到了最小的一个,那么恭喜你,你取到了快速排序最慢的情况,时间复杂度为O(n^2)和基本排序速度一样了。如果想要优化快速排序,那最好的优化方式就是优化中间元素的选取。

还好,先贤已经找到了相对较好的选取中值的方法,一种是元素个数不多的时候采用三分法,具体实现是选取一组共三个元素,取出三个元素的中的中值,这个值就有更大的几率接近于中值,起码不是最大或者最小值。一种是元素个数多的时候取三组每组三个共九个元素的九分法,具体实现是分别取出三组中的中值,然后再从三组中值中取出中值。

下面我们采用三分法对快速排序进行优化,引入一个区中值的文件getMiddle.js

let swap = require('./swap.js');
function getMiddle(arr, low, m, high) {
    let middleIndex = m;
    if (arr[low] > arr[high]) {
        swap(arr, low, high);
    }
    if (arr[high] < arr[m]) {
        swap(arr, high, m);
        middleIndex = high;
    }
    if (arr[low] > arr[m]) {
        swap(arr, low, m);
        middleIndex = low;
    }
    return middleIndex;
}

module.exports = getMiddle;

然后快速排序代码稍微改造下:

let count = 0;
let getMiddle = require('./getMiddle.js');
function quick(arr) {
   if (arr.length < 2) return arr;
   let middle = getMiddle(arr, 0, parseInt(arr.length / 2), arr.length - 1),
//    let middle = parseInt(arr.length / 2),
         left = [], 
         right = [];
   let middleValue = arr.splice(middle, 1);
   for (let i = 0; i < arr.length; i ++) {
       count ++;
       arr[i] > middleValue[0] ? right.push(arr[i]) : left.push(arr[i]);
   }
   return quick(left).concat(middleValue).concat(quick(right));
}

let arr = [2, 4,5, 1, 3, 6, 9, 4]

console.log(quick(arr));
console.log(count);

这样就对中值选取进行了改进,使快速排序进行了优化。还有一种思路是从快速排序的递归入手,递归会不停地吃内存,那么优化递归也就能优化在大数据量情况下的快速排序。怎么优化呢,尾递归。

下面是一个尾递归转化函数:

function tco(f) {
    let funcStack = [],
        active = false,
        value;
    return function() {
        funcStack.push(arguments);
        if (active) return;
        active = true;
        while (funcStack.length) {
            value = f.apply(this, funcStack.shift());
        }
        active = false;
        return value;
    }
}

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

所以优化后的实现为:

var count = 0;
var getMiddle = require('./getMiddle.js');

function tco(f) {
    let funcStack = [],
        active = false,
        value;
    return function() {
        funcStack.push(arguments);
        if (active) return;
        active = true;
        while (funcStack.length) {
            value = f.apply(this, funcStack.shift());
        }
        active = false;
        return value;
    }
}

function quick(arr) {
   if (arr.length < 2) return arr;
   let middle = getMiddle(arr, 0, parseInt(arr.length / 2), arr.length - 1),
//    let middle = parseInt(arr.length / 2),
         left = [], 
         right = [];
   let middleValue = arr.splice(middle, 1);
   for (let i = 0; i < arr.length; i ++) {
       count ++;
       arr[i] > middleValue[0] ? right.push(arr[i]) : left.push(arr[i]);
   }
   return quick(left).concat(middleValue).concat(quick(right));
}

var arr = [2, 4,5, 1, 3, 6, 9, 4]
var quick2 = tco(quick);
console.log(quick2(arr));
// console.log(count)

当然关于尾递归优化大家可以参看阮一峰的es6标准入门,地址为:
http://es6.ruanyifeng.com/#docs/function#
以上就是整个快速排序的内容,下一篇将分享堆排序,thx。

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消