快速排序的几个优化

快排代码的第一句便是选取基准点,此后数据的移动根据这个基准点的大小进行调整,如果基准点选取的不好,将会导致快排的效率低下,经过测试,普通的快排算法针对(1)近乎有序的数列;(2)含有大量重复数据的数列;这两种情况时效率将会变得非常低,针对这些情况,经过适当的优化可以使快排达到很高的效率。

1.三数取中法和随机交换法

快排将选取的基准点经过调整放到合适的位置,之后将这个基准点左右两边的区间分别递归的进行快排,如果基准点的数据比较小,将会导致调整后基准点处于靠近两侧的位置,那么两边的区间长度将会严重失去平衡


对于一个近乎有序的数列,当直接使用第一个元素作为基准点的时候,将会导致下图的情况


如果数列非常接近有序


可以看到此时快排的递归过程生成的递归树的平衡性非常差,不能保证高度是logn,进而导致了效率变低,针对这种情况,只需改变选取基准点的方式就可以提高算法的效率。

三数取中法:指的是选取基准点之前我们可以拿出数列中间位置元素的值,将它和首尾的元素进行比较,之后将这三个数中的中间大的数交换到数列首位的位置,之后将这个数作为基准点,尽量减小之后的分区后左右两边的区间长度之差。

随机交换法:指的是选取基准点之前设计随机种子,通过随机函数得到一个在数列长度内的数,将这个随机数作为索引所指的数和第一个元素进行交换,之后将首位元素作为基准点。即随机选一个数放到首位的地方。这样一来,第一次就将最小的数交换到首位的概率是非常小的,第二次将次小的数交换到首位的概率依然非常的小。

2.针对数列含有大量重复元素的情况

比如随机生成一个含有15万个数据的数组,范围是从0~10,那么数组中将含有非常多的重复数据,对这个数组使用上述的快排排序时,时间几乎又是回到了O(n^2)的级别,原因如下


在之前的分区算法中,由于含有大量的重复数据,将会导致分出来的左右区间如图所示,两个区间的长度比例完全失衡,导致了算法的时间复杂度上升,针对这种情况,需要对分区操作做适当的修改,思路是将小于基准点的数全部放到左边,大于基准点的数全部放到右边,

具体操作:从右向左扫描时,如果元素值大于基准点,则继续,否则停止,如图j所指的位置,从左向右扫面时,当元素值小于基准点,则继续,否则停止,如图i所指的位置

经过上述扫描后,i和j停在了相应的位置,此时要做的是,交换i和j分别所指的元素即可,这样一来,左边的全部都是小于等于基准点的,右边的全部都是大于等于基准点的,这样做的好处是,对于等于基准点的元素,将他们分别放到了左右的两个区间,而不是像之前那样完全放在同一边,导致区间长度不平衡。因此,通过将等于基准点的元素分配到不同区间,保证了左右区间长度尽可能平衡,提升了算法的效率


代码

template<typename T>
int partion2(T k[], int l, int r) {
    swap(k[l], k[rand() % (r - l + 1) + l]);
    T temp = k[l];
    int i = l + 1, j = r;
    while (true) {
        while (i <= r && k[i] < temp) i++;
        while (j >= l && k[j] > temp) j--;
        if (i > j)
            break;
        swap(k[i], k[j]);
        i++;
        j--;
    }
    swap(k[l], k[j]);

    return j;
}

template<typename T>
void QSort2(T k[], int l, int r) {
    if (l < r) {
        int p = partion2(k, l, r);
        QSort2(k, l, p - 1);
        QSort2(k, p + 1, r);
    }
}

template<typename T>
void QuickSort2(T k[], int len) {
    srand(time(NULL));
    QSort2(k, 0, len - 1);
}

经过测试,对于含有大量重复数据的数组,算法的效率基本回到了O(nlogn)的级别。

3.3路法

3路法同样是针对含有大量重复数列的优化,不同于之前的快排方法,3路法的思想是将数列分成3个区间,分别是小于、等于和大于基准点的区间,那么分区之后,对于等于基准点的区间内的元素,我们就不需要对其做任何处理了,只需要递归的处理小于和大于基准点的元素即可。


如图,需要多设置几个索引,橙色部分全部是小于v的数据,紫色为大于v的数据,i所指的为待处理的数据。

(1)当待处理的数据小于v时,只需将i所指的元素和等于v的区间的第一个元素交换即可,即将e换到lt之后的位置,lt++,i++


(2)i所指元素大于v时,将其换到大于基准点的区间的第一个位置即可,即交换i和gt-1的元素,gt--,i不变


(3)i所指元素等于v时,直接i++


代码

template<typename T>
void QSort3Ways(T k[], int l, int r) {
    if (r - l <= 15) {
        InsertSort(k, l, r);
        return;
    }

    swap(k[l], k[rand() % (r - l + 1) + l]);
    T temp = k[l];
    int lt = l;                 // k[l+1...lt] < v
    int gt = r + 1;             // k[gt...r] > v
    int i = l + 1;              // k[lt+1...i) = v
    while (i < gt) {
        if (k[i] < temp) {
            swap(k[i], k[lt + 1]);
            i++;
            lt++;
        } else if (k[i] > temp) {
            swap(k[i], k[gt - 1]);
            gt--;
        } else {
            i++;
        }
    }
    swap(k[l], k[lt]);
    QSort3Ways(k, l, lt - 1);
    QSort3Ways(k, gt, r);

}

template<typename T>
void QuickSort3Ways(T k[], int len) {
    srand(time(NULL));
    QSort3Ways(k, 0, len - 1);
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值