几种排序算法的稳定性分析

稳定指的是什么?
 
稳定排序是指原来相等的两个元素前后相对位置在排序后依然不变。

 
为什么要对排序算法提出稳定性的要求?
 

简单的举个小例子:比如我们班一次期末考试,分高的在前面,对于分数相同的学生的排名需要借助上次考试结果,上次分数高的在前面,那么这个时候就要使用稳定排序。

又比如,我们需要对学生先进行年龄排名,然后再根据身高排名,身高相同的,要求年龄大的在前面,这个时候必须采用稳定排序。

抽象来说就是,有两个排序关键字的时候(而且往往需要不止一次的排序),稳定排序可以让第一个关键字排序的结果服务于第二个关键字排序中数值相等的那些数

 
常见的几种排序算法稳定性分析
 

本文讨论的排序算法有以下这几种:

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 归并排序
  • 快速排序
  • 堆排序

说在最前面,本文分析出来的所谓的稳定性排序,前提是你对该排序算法的实现是正确的,(简单的举个例子,每种排序都少不了比较的过程,比较的时候运算符使用的是 " < " 还是 ” <= “,这就会对你实现的排序算法稳定性产生影响),另外本文给出的稳定排序的代码实现都是正确的,即能保证稳定性的实现。

所以本文在分析完给出的结论是这样的两种情况:

  • 可以保证稳定性
  • 非稳定

注意我的说法,我没有说稳定性的排序,一定是稳定的,这跟你代码实现息息相关!

想要具体了解这个内容的朋友,可以参考我这篇文章,里面有具体的阐述和代码示例。

扫描二维码关注公众号,回复: 6680750 查看本文章

稳定排序的代码你写对了吗?
 

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;
		}
	}

}

 
总结
 

  • 可以保证稳定性的排序:冒泡、插入、归并
  • 不稳定的排序:选择、希尔、快排、堆排

还是那句话,是可以保证,并非是一定是稳定,这跟你的排序算法代码实现正确与否有直接的关系。

具体的可以参考我的另一篇文章。

稳定排序的代码你写对了吗?

猜你喜欢

转载自blog.csdn.net/u013568373/article/details/93600427