原地排序算法
原地排序是针对排序的空间复杂度而言的。从前边的若干排序算法中可以看出,排序过程中需要借助于一些局部变量暂存数据,或用于比较,或用于交换,或用于存储当前排序结果集。
- 比如选择排序,每轮需要一个局部变量暂存最大/小值,一个局部变量暂存最大/小值的索引;
- 比如冒泡排序,每轮需要一个局部变量用于交换(当然针对
int
的交换也可以用比较骚气的操作不使用额外变量); - 比如插入排序,每轮需要一个局部变量暂存固定元素的值;
- 比如归并排序,每次合并不仅需要固定数量局部变量,还需要新开与当前合并任务相关的临时数组;
- 比如快速排序,每次分区也是需要几个局部变量;
- 比如桶排序,每个桶的设定都是要开辟新的内存空间;
- 比如计数排序,存储统计个数的数组需要开辟新空间;
- 比如基数排序,需要用到桶排序或计数排序的一种,相应就需要开辟新的内存空间。
可以看出,有些算法需要用到的额外内存有限,但有些用到的内存和待排序数据量有关。其中使用有限内存空间复杂度为O(1)
的排序称为原地排序
不难看出,之前使用的排序中,大部分都是原地排序,只有少数几个不是
原地排序列表:
选择排序、冒泡排序、插入排序、快速排序
稳定排序算法
稳定排序是针对待排数据集中的重复元素而言的。
比如一个人员待排数据集[张三,李四,王五,马六]
,其中张三7
岁,李四、王五、马六都是5
岁,现在按年龄升序排列,得到的结果可以是[李四,王五,马六,张三]
,也可以是[王五,马六,李四,张三]
甚至其他,但对比发现,第一种排序结果中,年龄相同的三人先后顺序没变,而其他的都改变了。
针对这种值相等的情况,经过排序后,其先后顺序不改变,相应的排序算法称为稳定排序算法。
大部分基于数值比较的都是稳定的排序算法,当然一不小心可能就将代码写成了不稳定排序。
- 比如选择排序,之前我以为选择排序是稳定排序,但其实想一个反例就可证明其不是稳定排序。例如
[2,2,1]
,第一轮找最小值找到了1
,那么1
就要和index=0
的元素做交换了,这一交换就打破了原本两个2
元素的顺序。所以选择排序不是稳定排序; - 比如冒泡排序,在前后元素比较时要格外注意,
array[j]
和array[j+1]
比较,如果相等,不做交换,就是稳定排序;如果不小心做了交换,就不稳定了。所以冒泡排序是稳定排序; - 比如插入排序,和冒泡排序一样,
temp
和array[j]
比较,因为temp
所在索引原本就比j
大,在相等情况下,就不要做交换了,可以保证稳定。所以插入排序是稳定排序; - 比如归并排序,在合并两个有序数组时,前半部分子数组天然的在前边,所以如果两边有相同元素,保持前半部分的元素优先就能保证稳定。所以归并排序是稳定排序;
- 比如快速排序,之前我也以为这是个稳定排序(其实之前以为有比较的都是稳定的),找个反例,例如某个子数组
[1,4,4,2]
,选取了2
为分区点,执行到最后时,要把子数组相对位置为index=1
的元素4
与2
做交换,这就破坏了两个4
的顺序。所以快速排序不是稳定排序; - 比如桶排序,单个桶要用到归并排序或者快速排序,天然的一致。使用了归并排序就是稳定的,使用了快速排序就不是稳定的;
- 比如计数排序(这里曾给自己挖了个坑,坑的地址在时间复杂度为O(n)的排序(JAVA)),因为计数排序独特而巧妙的赋值方式是从后往前的,所以搭配着也要从后往前去遍历,只要二者方向一致,就能保证稳定。所以计数排序是稳定排序。
- 比如基数排序,基数排序要用到稳定的桶排序(也就是单个桶中使用归并排序),或者使用稳定的计数排序,所以天然就是稳定的。
稳定排序列表:
冒泡排序、插入排序、归并排序、桶排序(使用归并)、计数排序、基数排序
总结
是否原地排序 | 是否稳定排序 | |
---|---|---|
选择排序 | 是 | 否 |
冒泡排序 | 是 | 是 |
插入排序 | 是 | 是 |
归并排序 | 否 | 是 |
快速排序 | 是 | 否 |
桶排序 | 否 | 是(归并) |
计数排序 | 否 | 是 |
基数排序 | 否 | 是 |