稳定指的是什么?
稳定排序是指原来相等的两个元素前后相对位置在排序后依然不变。
常见的几种排序算法稳定性分析
本文讨论的排序算法有以下这几种:
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
说在最前面,本文分析出来的所谓的稳定性排序,前提是你对该排序算法的实现是正确的,(简单的举个例子,每种排序都少不了比较的过程,比较的时候运算符使用的是 " < " 还是 ” <= “,这就会对你实现的排序算法稳定性产生影响),另外本文给出的稳定排序的代码实现都是正确的,即能保证稳定性的实现。
所以本文在分析完给出的结论是这样的两种情况:
- 可以保证稳定性
- 非稳定
注意我的说法,我没有说稳定性的排序,一定是稳定的,这跟你代码实现息息相关!
想要具体了解这个内容的朋友,可以参考我这篇文章,里面有具体的阐述和代码示例。
1. 冒泡排序
在冒泡排序的算法中,每一次循环都是比较两个相邻的元素,前面比后面大的话,就把前面的往后移,后面的往前挪,说白了就是交换一下这俩元素,相等的话不会执行交换,所以相等的元素的前后相对位置不会发生改变,所以冒泡排序可以是稳定的。
代码实现如下:
/**
* 冒泡排序,每次都能将最大的元素干到最后,一共进行n-1趟冒泡
* 优点:每次放完都能减少一次比较
* @param arr
*/
public static <T extends Comparable<? super T>> void bubbleSort(T[] arr) {
//外层循环代表的是趟数
//内层循环代表的是比较两个元素
for(int i = 0; i < arr.length-1; i++) {
for(int j = 0; j < arr.length - i - 1; j++) {
if(arr[j].compareTo(arr[j+1]) > 0) {
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
2. 选择排序
每次考虑一个位置(从前到后,每次循环递增),找出从该位置(当前 index)开始到数组末尾中的最小元素,与该位置进行交换(将当前的最小值与当前 index 处的元素进行交换),可能破坏稳定性。
比如 [5,8,3,5,2],第一次交换把第一个 5 换到最后去了,第二个 5 此时在第一个5位置之前了。所以选择排序是不稳定的。
/**
* 选择排序
* @param arr 待排序数组
*/
public static <T extends Comparable<? super T>> void selectionSort(T[] arr) {
int n = arr.length;
for(int i = 0; i < n; i++) {
//寻找i到n区间内的最小值
int minIndex = i;
for(int j = i + 1; j < n; j++) {
if (arr[j].compareTo(arr[minIndex]) < 0) {
minIndex = j;
}
}
//交换下元素
swap(arr, i, minIndex);
}
}
3. 插入排序
从后往前插,一直找到不比它大的元素(说明这时候前面的元素小于等于当前考察的元素),该元素后面就是当前元素该插入的位置,所以并未改变相等元素的先后顺序,所以插入排序可以是稳定的。
/**
* 插入排序
* @param arr
*/
public static <T extends Comparable<? super T>> void insertSort(T[] arr) {
int n = arr.length;
for(int i = 1; i < n; i++) {
T e = arr[i];
int j;
for(j = i; j > 0 && arr[j - 1].compareTo(e) > 0; j--) {
//前面的往后挪,一直到j-1不再大于e了
arr[j] = arr[j-1];
}
//j-1不大于e了。那填入j位置吧
arr[j] = e;
}
}
4. 希尔排序
希尔排序是按照不同步长对元素进行插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
/**
* 希尔排序
* @param arr
*/
public static <T extends Comparable<? super T>> void shellSort(T[] arr) {
//gap即为增量,直到gap为1时,此时是对全序列进行一次直接插入排序
for(int gap = arr.length / 2; gap > 0 ; gap /= 2) {
for(int i = gap; i < arr.length; i++) {
T e = arr[i];
int j;
for(j = i;j >= gap && arr[j - gap].compareTo(e) > 0; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = e;
}
}
}
5. 归并排序
每次将当前数组一分为 2 ,将两个排好序的数组进行合并,小的放入,移动指针。
归并排序可以是稳定的。
// 归并排序,外部调用接口
public static <T extends Comparable<? super T>> void mergeSort(T[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
/**
* 递归使用归并排序,对arr[l,....r]的范围进行排序
* @param arr
* @param l 左边界
* @param r 右边界
*/
private static <T extends Comparable<? super T>> void mergeSort(T[] arr,int l,int r) {
// l >= r 代表着我们只需要处理一个元素或者一个元素都没有的情况了
if (l >= r) {
return;
}
// 二分正确写法,防止整型溢出
int mid = (l + r) >>> 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
// 一个优化方案,如果 arr[mid] 比 arr[mid+1] 小,说明已经有序了,不必 merge
if (arr[mid].compareTo(arr[mid + 1]) > 0) {
merge(arr, l, mid, r);
}
}
/**
* 将arr[l...mid]和arr[mid+1,.....r]两部分进行归并
* @param arr
* @param l
* @param mid
* @param r
*/
private static <T extends Comparable<? super T>> void merge(T[] arr,int l,int mid,int r) {
//声明一个归并过程需要使用的辅助空间
T[] aux = (T[] )new Comparable[r - l + 1];
//因为需要对原数组进行排序,所以用aux拷贝一份原数组,之后方便后序的归并排序过程
for (int i = l; i <= r; i++) {
aux[i - l] = arr[i];
}
//i,j为两个待合并的数组头部索引
int i = l;
int j = mid + 1;
//归并过程
for(int k = l; k <= r; k++) {
//如果i已经超过mid指针,说明前一个已经排完了,把第二个剩下的都接上就行了
if (i > mid) {
arr[k] = aux[j - l];
j++;
}
//如果j已经超过r右边界,说明后一个已经排完了,把第一个剩下的都接上就行了
else if (j > r) {
arr[k] = aux[i - l];
i++;
}
//接下来的两个分支就是对比哪个元素小就进到arr数组里面去
else if (aux[i-l].compareTo(aux[j-l]) <= 0) {
arr[k] = aux[i - l];
i++;
}
else {
arr[k] = aux[j - l];
j++;
}
}
}
6. 快速排序
快排的核心思想就是每次我选取一个枢纽元,将小于这个枢纽元的元素划分到一块儿,大于这个枢纽元的元素划分到另一块儿,然后将枢纽元放在这两块儿的中间。快排是不稳定的。看下面这个简单的例子:
假设,当前的枢纽元为 5 ,我已经标上红色,我们已经成功的把小于 5 的元素划分到左半部分(绿色的 3,3,4,3),大于 5 的元素划分到了右半部份(蓝色的 8,9,10,11)。
[ 5 3 3 4 3 8 9 10 11 ]
此时我们将枢纽元交换到两个部分的中间,方法就是将 5 与 左半部分最后一个元素 3 进行交换。到这里你就会发现,原来在后面的 3 现在跑到其它 3 的前面来了。
//三路快排代码实现(分成三个区间 小于 /等于 /大于 这样以来在重复元素比较多的情况下,效率显著的提高)
public static <T extends Comparable<? super T>> void quickSort3ways(T[] arr) {
quickSort3ways(arr,0,arr.length - 1);
}
private static <T extends Comparable<? super T>> void quickSort3ways(T[] arr,int l,int r) {
if (l >= r) {
return;
}
swap(arr, l, random.nextInt(r - l) + l);
T v = arr[l];
//lt小于v的最后一个元素索引
int lt = l;//arr[l+1..lt] < v (因为lt = l,所以初始时这个区间为空)
//gt为大于v的第一个元素索引
int gt = r + 1;//arr[gt...r] > v (因为gt = r + l,所以初始时这个区间为空)
//i为当前待观察的元素
int i = l + 1;//arr[lt + 1...i) == v (因为lt + 1 = l + l,所以初始时这个区间为空)
//开始正式的三路快排过程
while (i < gt) {
if (arr[i].compareTo(v) < 0) {
swap(arr, lt + 1, i);
lt++;
i++;
}else if (arr[i].compareTo(v) > 0) {
swap(arr, gt - 1, i);
gt--;
}else {
//arr[i] == v 啥也不干只移动i指针
i++;
}
}
//交换lt与l处的元素,自此原数组被分成了三部分
//[l...lt-1] < v; [lt...gt-1] == v ; [gt..r] > v
swap(arr, l, lt);
quickSort3ways(arr, l, lt-1);
quickSort3ways(arr, gt, r);
}
7. 堆排序
我们知道堆的结构是节点 i 的孩子为 2 * i 和 2 * i + 1 节点,大顶堆要求父节点大于等于其 2 个子节点,小顶堆要求父节点小于等于其 2 个子节点。在一个长为 n 的序列,堆排序的过程是从第 n / 2 开始和其子节点共 3 个值选择最大(大顶堆)或者最小(小顶堆),这 3 个元素之间的选择当然不会破坏稳定性。但当为 n / 2 - 1, n / 2 - 2, … 1 这些个父节点选择元素时,就会破坏稳定性。有可能第 n / 2 个父节点交换把后面一个元素交换过去了,而第 n / 2 - 1个父节点把后面一个相同的元素没有交换,那么这 2 个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法
package SeniorSort;
/**
* 堆排序,主要依赖于堆这种数据结构
* 1.第一种堆排序的方法:对数组进行Heapify,之后不停的出队就完事儿了
* 2.第二种,原地堆排序,不断的把根节点放到最后,然后对剩下的构建堆,再做同样的事
* @author xudaxia0610
*
*/
public class HeapSort<T extends Comparable<T>> {
/**
* 交换数组中的两个元素
* @param arr
* @param i
* @param j
*/
private static <T> void swap(T[] arr,int i,int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 第一种堆排序方法,需要辅助空间,因为需要把待排序的数组数据全部放入堆中
* @param arr
*/
public void heapSort1(T[] arr) {
MaxHeap<T> maxHeap = new MaxHeap<>(arr);
for(int i = arr.length - 1;i >= 0;i --) {
arr[i] = maxHeap.extractMax();
}
}
/**
* 第二种堆排序方法:原地堆排序,不需要辅助空间,空间复杂度为O(1)
* @param arr
*/
public static <T extends Comparable<? super T>> void heapSort2(T[] arr) {
int len = arr.length;
//对原数组heapify的过程(原地heapify)
for(int i = (len - 1)/2;i >= 0;i --) {
//下沉
shiftDown(arr,len,i);
}
//注意 i是大于0的,因为i等于0时就剩最后一个元素了,不必再去执行排序了
for(int i = len-1; i > 0; i--) {
//每次将堆顶与最后一个元素交换
swap(arr, i, 0);
//注意每次下沉都是在i个元素中进行(排除末尾已排好序的元素)
shiftDown(arr,i,0);
}
}
/**
* 下沉操作
* @param arr
* @param nums 数组元素个数
* @param index 待下沉的索引
*/
private static <T extends Comparable<? super T>> void shiftDown(T[] arr,int nums,int index) {
while ((2*index + 1) < nums) {
int j = 2*index + 1;
if (j + 1 < nums &&
arr[j].compareTo(arr[j + 1]) < 0) {
j++;
}
if (arr[index].compareTo(arr[j]) >= 0) {
break;
}
swap(arr, index, j);
index = j;
}
}
}
总结
- 可以保证稳定性的排序:冒泡、插入、归并
- 不稳定的排序:选择、希尔、快排、堆排
还是那句话,是可以保证,并非是一定是稳定,这跟你的排序算法代码实现正确与否有直接的关系。
具体的可以参考我的另一篇文章。