我们在之前的排序算法文章中,介绍了六种基于比较的排序算法:选择排序、冒泡排序、插入排序、归并排序、快速排序和堆排序,现在我们来总结一下每个算法对应的时间复杂度、空间复杂度以及稳定性
稳定性的意思就是对于两个值相等的元素,在排完序后,它们的相对位置没有发生改变,这样的算法就是稳定的
我们先说说为什么要研究算法的稳定性:对于简单类型的比较,譬如比较数组中的数,那么每个数值都是等效的,稳定性就没有什么用;不过对于复杂类型,稳定性就很有用了。举个栗子:比如一群学生,他们有两个属性,一个是成绩,一个是班级,我想先按照他们的成绩进行降序排序,之后再根据班级进行升序排序;那么排完成绩后,我们再根据班级进行排序时,如果排序算法是不稳定的,会导致之前排好的成绩可能会因为班级排序而打乱;而对于稳定的排序算法,同一个班级的数据的相对位置就不会发生改变,也就不会影响到之前已经排好的成绩顺序
讲完了稳定性的用处后,我们再来逐个分析一下上述六大排序的稳定性
对于选择排序,它是不稳定的排序,举例如下:
当找到数组中的最小值 2,并与数组第一个元素进行交换后,两个数字 3 的相对顺序就发生了改变,因此选择排序是不稳定的排序
对于冒泡排序,它是稳定的排序,因为当冒泡的元素只有遇到比它小的元素时,它才交换位置;遇到相同元素时并不交换位置,所以不会改变相同元素之间的相对位置
对于插入排序,它是稳定的排序,因为插入的元素只有遇到比它大的元素时,它才会继续往前比较,直到找到合适的插入位置;遇到相同元素时,它就停止了比较,所以不会改变相同元素之间的相对位置
对于归并排序,它是稳定的排序,我们回想一下归并排序里的归并过程,当左右指针所指的元素相同时,左指针所指的数先放入数组中,所以不会改变它们的相对位置,所以归并排序也是稳定的排序(之前我们讲归并排序的应用时提到了小和问题,小和问题中当左右指针所指的元素相同时,是右指针所指的数先放入数组,所以小和问题中的排序并不是稳定的排序)
对于快速排序,它是不稳定的排序,举例如下:
如下所示的排序过程中,若 5 为基准元素,左边为小于 5 的元素,右边为大于 5 的元素,那么当指针 i 指向第三个元素时,因为 6 比基准值 5 大,因此 6 与 5 要交换位置,两个数字 6 的相对位置就发生了改变,因此快速排序是不稳定的排序
对于堆排序,它是不稳定的排序,因为堆排序使用了堆这个数据结构,它在比较的时候只和自己的父结点或孩子结点进行比较,压根不关心稳定性的问题,不过为了理解,我们还是举个栗子,沿用之前介绍堆排序稳定性使用的栗子
对于这样的大根堆,如果我们使堆上浮,去掉里面最大的元素,那么最后一个元素 3 就会跑到根结点的位置,于是两个 3 的相对位置就发生了变化,因此堆排序是不稳定的排序
根据以上的分析,我们得出一个结论:
不具备稳定性的排序有选择排序、快速排序和堆排序;它们的特点是元素交换时会进行跳跃,这种跳跃交换就很可能导致相对位置发生变化
有稳定性的排序有冒泡排序、插入排序、归并排序(tips: 计数排序和基数排序这两个不基于比较的排序也是稳定的排序)
我们现在来分析一下他们的时间复杂度和空间复杂度
选择排序算是里面最 low 的一个了,它的时间复杂度为 O(N²)这样高的级别,却还不能保证稳定性
现在我们来看看下面这三个排序
目前还没有找到时间复杂度小于 O(NlogN) 的排序算法
目前还没有发现时间复杂度为 O(NlogN),空间复杂度在 O(N) 以下,同时又有稳定性的算法
所以我们选择排序算法时,不可能时间复杂度、空间复杂度、稳定性三者好处都占完,目前还没找到这样的算法,我们只能根据我们的实际情况选择要么最快、要么额外空间使用最少、要么稳定的算法
工程上对排序的改进:我们可能在某些快排的代码中,看到这一段代码
if(l > r - 60) { // 在 arr[l...r] 插入排序 // O(N²) 小样本量的时候跑得快 return; }
这正是一种综合排序,充分利用了 O(NlogN) 和 O(N²) 各自的优势,对于小样本而言,虽然时间复杂度是 O(N²),但是它此时是常数时间,瓶颈并不明显;这样它就能在大样本中运用快排的时间复杂度优势,小样本中利用插入排序常数时间低的优势
欢迎来逛逛我的博客 mmimo技术小栈