对数据结构和算法的总结和思考(四)--快速排序
快速排序,顾名思义就是快,多快呢?亲测了下,整体性能除了计数排序无人能比,不过计数排序有一个很大的劣势是只能对整数进行排序,这就大大限制了使用场景。所以,快速排序应该是目前表现最好的排序方法,各种解释器都有快速排序的实现,并且快速排序实现简单易于理解。实现原理:找出一个标杆元素,将比标杆元素大的放到右边,小的放到左边,然后递归合并。具体代码如下:
// 快速排序
// 找出基准值,取出基准值,遍历放入
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。
共同学习,写下你的评论
评论加载中...
作者其他优质文章