一文道破快速排序从理解到优化

本小妞迷上赌 提交于 2020-02-18 20:12:33

快速排序

快速排序(QuickSort)是对冒泡排序(BubbleSort)的一种改进。

快速排序由C. A. R. Hoare在1960年提出,这是这位图灵奖得主在很年轻的时候想出来的,XMSL。

它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

其基本的编程实现,我已经在这篇文章中展示过了,各种排序算法都有——《基础排序算法大全(Java语言描述)》

快排的效率很高,对随机序列是很好使的,但也可能遇到O(N2)的糟糕情况。不过只要加以优化这种情况基本不可能出现。

快速排序是数据结构与算法的重要知识,是程序员和计算机专业学生必知必会的重要算法,是面试的常考重点,其性能优化也是需要思考的问题……

总之,一定要会!很会很会!

快速排序的流程

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

动图演示

在这里插入图片描述

优化策略

其实快排是一个天才般的算法,对其优化主要从两个角度展开:
一是使得选取的中轴更合理,尽量使得划分均匀一些。
二是试图在快排效率不好的时候更换算法或与其他算法组合,使之更高效。
下面给一些说明。

1.二分

运用二分思想,每次取划分出的区间的中值作为新的一次划分的中轴。
一般来说,快排对随机序列很好使,所以说我们可以认为,平均来讲去取中值更可能划分的比较均匀。
二分取中值可能优于取一端的值(事实上确实如此,比如洛谷P1177这个题,哪怕你用C++,你写普通版快排还是会TLE掉3个点)……

2.随机化

既然快排本身面对的就是随机序列,其实我们可以认为随机化地取枢纽值会比任意取好一些,至少比直接取一侧的值好一些。
随机化本身就可能是一种算法优化的策略,至少也是一种参考指标。

3.与其他排序结合

虽说快排确实是快,但有些情况下,比如遇到不利情况会变成O(N2),比如说在分治到小区间时甚至可能不如简单的排序算法……
所以说可以与其他排序算法结合,在合适的时候运用合适的算法进行高效协作。
而这部分可以参考我在文末附的一位大佬的博文。
想体会到这种策略是多么强,可以参看伟大的TimSort~

C++编程实现

这个版本是利用了二分的思想对算法进行了优化,使得原先的算法性能得到了一定程度的提高。

#include<iostream>
using namespace std;
int num, array[100001];

/**
 * 应用二分思想
 * @param left 左索引
 * @param right 右索引
 */
void quickSort(int left, int right) {
    int mid = array[(left+right)/2];//中间数
    int i = left, j = right;
    do {
        while (array[i] < mid) {
            i++;//查找左半部分比中间数大的数
        }
        while (array[j] > mid) {
            j--;//查找右半部分比中间数小的数
        }
        if (i <= j) {
            swap(array[i], array[j]);//交换
            i++;
            j--;
        }
    } while (i <= j);
    if (left < j) {
        quickSort(left, j);//递归搜索左半部分
    }
    if (i < right) {
        quickSort(i, right);//递归搜索右半部分
    }
}

int main() {
    cin >> num;
    for (int i = 1; i <= num; i++) {
        cin >> array[i];
    }
    quickSort(1, num);
    for (int i = 1; i <= num; i++) {
        cout << array[i] << " ";
    }
}

另一个版本

这位大佬写的比较麻烦,但讲得不错,值得学习……

谈谈快速排序与二路归并排序的思想

有关于这二者的时空复杂度啊、稳定性啊这些问题我就不想赘述了,我的博客也有——《排序综述》,很详细。

这里说的是二者的思想。
我在这篇文章——《算法分析与设计中的几种思想/策略》中也提到了,归并排序和快速排序同样都使用了分治的思想,可以通过递归很好的完成排序工作。

但快速排序的核心算法是划分,二路归并排序的核心算法是合并。

大家有没有想过这样一件事:快速排序和二路归并排序分别是在什么时候完成的排序?
其实是这样的:
快速排序在划分的时候,将大的放到轴右侧、小的换到轴的左侧,在这个划分的时候完成的排序啊,它的“分治”,“分”是核心,所以“划分”是核心算法,等到分无可分就完成了子区间的排序,子区间都排完,整体区间就完成了排序。
而二路归并排序的“分”只是为了二分地分解大区间为小的子区间,它是在“合”的时候完成的排序啊,它的“分治”,“并”是核心,所以“合并”是核心算法,等到分无可分就可以逐步合并区间了,最终就完成了整体区间的排序。

总结

快速排序在划分的时候完成排序,

重点在“分”;

归并排序在“并”的时候完成排序,

重点在“并”。

此外,快速排序属于交换排序,与归并排序有很大的不同,快排的“分”也是通过交换完成的。

就说这么多……

总结

看了这么多,你掌握快速排序了吗?看在我这么辛苦的份上,是不是应该用一个一个赞向我砸来呢?
OrzOrzOrz……

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!