【数据结构与算法】快速排序、随机基准值、双路快排、三路快排

在 https://visualgo.net/zh/sorting 的 QUI 标签中可以看到快排序动画演示。

快速排序

平均时间复杂度:O(nlogn),最坏情况下 O(n^2)
空间复杂度:O(1)

基本思想

分治。

找一个基准元素,以基准元素分解,左边是比基准元素小的,右边是比基准元素大的。

这样就把一个待排序数组分成了左右两部分。
对左、右分别进行上面的步骤。

4 3 5 6 2 1
我们选取 4 作为基准值。
1 3 2 4 5 6
在对 [1 3 2] 和 [5 6] 进行操作。

快排1

基础实现

// 返回 p, arr[l, p - 1] < arr[p]; arr[p + 1, r] > arr[p];
template<typename T>
int __partition(T arr[], int l, int r) {
    T v = arr[l];  // 选取第一个元素为基准值

    int j = l;
    for (int i = l + 1; i <= r; ++i) {
        if (arr[i] < v) {
            swap(arr[++j], arr[i]);
        }
    }

    swap(arr[l], arr[j]);
    return j;
}

template<typename T>
void __quickSort(T arr[], int l, int r) {
    if (l >= r) return;

	// 经过 __partition 后,[l, p-1] < arr[p],[p + 1, r] > arr[p]
    T p = __partition(arr, l, r);      
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

template<typename T>
void quickSort(T arr[], int n) {
    __quickSort(arr, 0, n - 1);
}

10k 个随机数 [0, 10k] 排序:

归并排序        :       0.001776 s
快速排序        :       0.001609 s

归并排序、快排均属于 O(nlogn) 数量级的算法。

优化

随机基准值

上面的这种实现是有问题的,如果待排序数组已经接近有序了,我们选取第一个值作为基准值,将其分成两部分,就会造成一部分特别大。通过下面的测试可以看出:

10k 个接近有序的数 [0, 10k] 排序:

归并排序        :       0.004252 s
快速排序        :       2.7637 s

这个时候快排就退化为了 O(n^2) 级别。
快排2
究其原因,发现是选择基准数字的问题。
那么这个基准数字应该如何选择呢?

随机选一个数做基准值

第一次随机选取一个数,选取到刚好是最小的概率是 1/n;
第二次是 1/(n-1);

这样下来,每次都选取到的是一个很小的数字 1/n * 1/(n-1) * 1/(n-2) * ...。是一个很小的概率。

template<typename T>
int __partition(T arr[], int l, int r) {
    swap(arr[l], arr[rand() % (r-l+1) + l]);
    T v = arr[l];

    // arr[l + 1, j] < v; arr[j + 1, i] > v;
    int j = l;
    for (int i = l + 1; i <= r; ++i) {
        if (arr[i] < v) {
            swap(arr[++j], arr[i]);
        }
    }

    swap(arr[l], arr[j]);
    return j;
}

// arr[l, r]
template<typename T>
void __quickSort(T arr[], int l, int r) {
    if (l >= r) return;
    srand(time(NULL));

    T p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

双路快排

经过上面的 优化1,快速排序已经可以胜任一些场景下的排序了。
但是,考虑问题需要全面。
在上面的 优化1 中,将数据分成的两部分,默认把等于基准值的分在了有半部分,如果要排序的数组中和基准值相等的很多,比如 100k 个数都是 [0, 5] 的数,那就会出现左右不均,导致一边很多一边很少,这样快排的速度也会退化。

100k 个随机数,范围 [0, 5]:

归并排序	:	0.015571 s
快速排序	:	2.00576 s

分配不均

如何解决呢?
双路快排!

将等于基准值的数平均分配给两边。

template<typename T>
int __partition2(T arr[], int l, int r) {
    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    int i = l + 1, j = r;
    while (true) {
        while (i <= r && arr[i] < v) i++;
        while (j >= l + 1 && arr[j] > v) j--;
        if (i > j) break;
        swap(arr[i++], arr[j--]);
    }

    swap(arr[l], arr[j]);
    return j;
}

双路快排,100k 个随机数,范围 [0, 5]:

归并排序	:	0.015794 s
快速排序	:	0.026956 s

可以看到,双路快排和归并已经在一个数量级了。

三路快排

通过上面的随机选基准值、双路快排,已经解决了一部分问题,但现在还不是最优。

100k 个随机数,范围 [0, 5],虽然双路快排基本平均的分成了两部分进行,但是对于大量的和基准值相同的数,做了很多重复操作。

三路快排就是将待排序数组分成【小于基准值,等于基准值,大于基准值】三部分,下一轮排序只对小于基准值和大于基准值的两个部分进行排序即可,这样就省去了很大一部分操作。

template <typename T>
void __quickSort3Ways(T arr[], int l, int r) {
    if (l >= r) return ;

    srand(time(NULL));
    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // partition
    int i = l + 1;
    int lt = l;
    int gt = r + 1;
    while (i < gt) {
        if (arr[i] < v) {
            swap(arr[++lt], arr[i++]);
        } else if (arr[i] > v) {
            swap(arr[i], arr[--gt]);
        } else {
            ++i;
        }
    }
    swap(arr[l], arr[lt]);

    __quickSort3Ways(arr, l, lt - 1);
    __quickSort3Ways(arr, gt, r);
}

template<typename T>
void quickSort3Ways(T arr[], int n) {
    __quickSort3Ways(arr, 0, n - 1);
}

三路快排,100k 个随机数,范围 [0, 5]:

归并排序	:	0.015375 s
双路快速排序	:	0.026594 s
三路快速排序	:	0.00316 s

可以看到三路快排性能有一个明显的提升!

性能测试

100k 个随机数,范围 [0, 100k]:

归并排序	:	0.022674 s
双路快速排序	:	0.03437 s
三路快速排序	:	0.037351 s

100k 个随机数,范围 [0, 5]:

归并排序	:	0.015524 s
双路快速排序	:	0.026816 s
三路快速排序	:	0.002967 s

100k 个接近有序的数,范围 [0, 100k]:

归并排序	:	0.003909 s
双路快速排序	:	0.020788 s
三路快速排序	:	0.036348 s

EOF

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值