nlogn级别的排序算法(2)快速排序

nlogn级别的排序算法(2)快速排序

1. 基本思路

通过一趟排序,要将数据分成独立的两部分,其中一部分的所有数据都比另一部分中的所有数据大。为此,每次取第一个数据作为中间值,每一趟排序的目的就是使中间值位于正确的位置,所有该位置左边的数据都比中间值小,所有该位置右边的数据都比中间值大。然后以中间值为界,只要左右两边的数据量大于1,即可分别进行相同的排序操作,递归直到完成整个数组的排序。

2. javaScript实现

/**
 * 快速排序迭代过程
 * @param  {Array} arr    排序数组
 * @param  {Number} lIndex 待排序部分的左边界下标
 * @param  {Number} rIndex 待排序部分的右边界下标
 */
function quickSort(arr,lIndex,rIndex){
    if (lIndex<rIndex) {
        var flag = partition(arr,lIndex,rIndex);        
        quickSort(arr,lIndex,flag-1);
        quickSort(arr,flag+1,rIndex);
    }
}

/**
 * 将排序部分的第一个元素v放到正确位置的过程,
 * v放置正确后,v左边的元素全部都小于或等于v,v右边的元素全部都大于或等于v
 * @param  {[Array]} arr    待排序的数组
 * @param  {[Number]} lIndex 待排序部分的左边界下标
 * @param  {[Number]} rIndex 待排序部分的右边界下标
 * @return {[Number]}       返回调整之后,v所在位置的下标
 */
function partition(arr,lIndex,rIndex){
    var j = lIndex;
    var v = arr[lIndex];
    for (var i = lIndex + 1; i <= rIndex; i++) {
        if (arr[i] < v) {
            arr.changeData(i,j+1);
            j++;
        }
    }
    arr.changeData(j,lIndex);
    return j;
}
//交换数组元素
Array.prototype.changeData = function(indexOne,indexTwo){
    var temp = this[indexOne];
    this[indexOne] = this[indexTwo];
    this[indexTwo] = temp;
};

var arr = [5,3,1,6,2,7,4,9];
quickSort(arr,0,arr.length);
console.log(arr);

3. 快速排序与归并排序的效率比较

使用100,0000数据量的随机数组进行测试,改进后的归并排序和上述快速排序算法所需要的时间分别为:

排序方法归并排序(改进)快速排序(基本算法)
用时0.453s0.304s

使用1000数据量的近乎有序的数组进行测试,改进后的归并排序和上述快速排序算法所需要的时间分别为:
(此处无法使用1000以上的数据量进行测试,由于递归调用过多,会报出调用栈溢出的错误)

排序方法归并排序(改进)快速排序(基本算法)
用时0.001s0.005s

由此可见,当待排序的数组为随机数据时,快速排序算法较之归并排序的算法效率要高很多。但是,当待排序的数组为近乎有序的数组时,快速排序的效率则会大幅降低。这是因为,上述快速排序每次都使用第一个数组作为分界点,当数组完全有序时,每次以分界点为界,会将数组划分非常不平衡的两部分,左边部分数据量只有1,而右边部分的数据量只在原来的基础上减少了1,如此,其排序效率会降低到n^2级别。

4. 快速排序的改进:随机化快速排序

综合以上分析,要改进快速排序算法遇见近乎有序数组排序效率低的问题,只需要改变分界点的选择方法即可。即由每次选择数组第一个元素变为每次随机选择数组元素。改进代码如下(仅在原来的基础上添加了依一句交换位置的代码):

function partition(arr,lIndex,rIndex){
    //交换数组lIndex位置与[lIndex,rIndex]范围内任意元素的位置
    arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
    var j = lIndex;
    var v = arr[lIndex];
    for (var i = lIndex + 1; i <= rIndex; i++) {
        if (arr[i] < v) {
            arr.changeData(i,j+1);
            j++;
        }
    }
    arr.changeData(j,lIndex);
    return j;
}

5. 快速排序的改进:双路快速排序

替换测试数组用例,当数组范围为[1,10],数据量为10000时(即数组中存在大部分重复数据且每一个数组的重复量都很大),归并排序和改进后的快速排序,其排序用时分别如下,此时,快速排序算法的效率再次降低。这是因为,数组中的元素每次实际上是被分为了小于v的部分大于或等于v的部分。对于每一个”基准”元素来说,重复的元素太多了,如果我们选的”基准”元素稍微有一点的不平衡,那么就会导致两部分的差距非常大;即时我们的”基准”元素选在了一个平衡的位置,但是由于等于”基准”元素的元素也非常多,也会使得序列被分成两个及其不平衡的部分,那么在这种情况下快速排序就又会退化成O(n^2)级别的排序算法。

排序方法归并排序快速排序
排序时间0.006s0.049s

由此产生了双路快速排序算法,其基本思路为,设置两个标记i,j,将小于v的元素放在i标记的左侧,大于v的元素放在j标记的右侧。i标记从左到右开始遍历,j元素从右到左开始遍历,当i元素遇到大于或等于v的元素,j元素遇到小于或等于v的元素时,则将i和j调换位置。当i>j时,一次partition结束,此时,j已经指向了最右侧的小于v的元素,只需要将第一个元素与j指向的元素调换位置即可。实现代码如下:

 * 双路快速排序算法
 * @param  {[Array]} arr    待排序的数组
 * @param  {[Number]} lIndex 待排序部分的左边界下标
 * @param  {[Number]} rIndex 待排序部分的右边界下标
 */
function quickSort2(arr,lIndex,rIndex){
    if (lIndex >= rIndex) {
        return;
    }
    var flag = partition2(arr,lIndex,rIndex);       
    quickSort2(arr,lIndex,flag-1);
    quickSort2(arr,flag+1,rIndex);      
    return arr;
}
function partition2(arr,lIndex,rIndex){
    arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
    var v = arr[lIndex];
    var i = lIndex + 1;
    var j = rIndex;
    while(true){
        //此处如果将<改为<=,则效率又会降低到普通快速排序一般,为什么呢??
        while(i <= rIndex && arr[i] < v) i++;
        while(j >= lIndex+1 && arr[j] > v) j--;
        if (i>j) {
            break;
        }
        arr.changeData(i,j);
        i++;
        j--;
    }
    arr.changeData(lIndex,j);
    return j;
}

改进后,对数组范围为[1,10],数据量为100000的大量重复数组进行排序,用时如下:

排序方法归并排序随机快速排序双路快速排序
用时0.0420.6510.008

真棒,双路快速排序甚至超过了归并排序。

6. 快速排序改进:三路快速排序

双路快速排序将数组分为了大于等于v小于等于v两部分,而实际上,等于v的部分也占据了很大比例。由此衍生出了三路快速排序算法,即将数组分为大于v等于v小于v三个部分。其代码如下:

function quickSort3Ways(arr,lIndex,rIndex){
    if (lIndex>=rIndex) {
        return;
    }
    var leftAndRightObj = partition3(arr,lIndex,rIndex);
    quickSort3Ways(arr,lIndex,leftAndRightObj[0]);
    quickSort3Ways(arr,leftAndRightObj[1],rIndex);  
    return arr;
}

function partition3(arr,lIndex,rIndex){
    arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
    var v = arr[lIndex];
    var lt = lIndex;
    var i = lIndex+1;
    var rt = rIndex+1;
    while(i < rt){
        if (arr[i] < v) {
            arr.changeData(i,lt+1);
            i++;
            lt++;
        }else if (arr[i] === v){
            i++;
        }else {
            arr.changeData(i,rt-1);
            rt--;
        }
    }
    arr.changeData(lIndex,lt);
    return [lt,rt];
}

三路快速排序在针对含有大量重复元素的数组时,比双路快速排序具有更好的性能。以下是数组长度为1,000,000,数据范围为[1-10]的数组,用不同的方法排序的耗时:

排序方法归并排序双路快速排序三路快速排序
用时0.3850.130.042
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值