在介绍所有算法之前,先引入一下排序算法稳定性的概念。若Ai=Aj,在排序之前的乱序数组中,Ai在Aj之前,排序后,Ai仍在Aj之前。则说这个排序算法具有稳定性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
分析算法稳定与否的意义在于:当要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。
冒泡排序:
假设实现递增排序,将元素与其后的元素比较,若ai>ai+1,则调换位置。当一次内部排序完成,最大元素将会到达数组的最尾部。就像小鱼吐泡泡一样最大的将会浮到上面。JAVA实现如下:
public static void bubbleSort(int[] nums){
int temp;
for(int i=0;i<nums.length-1;i++){
for(int j=0;j<nums.length-i-1;j++){
if(nums[j]>nums[j+1]){
temp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
}
}
}
}
冒泡排序是具有稳定性的算法,最好情况下若配置一个标志位,在第一次内部排序时标志没有任何一次数据交换,则时间复杂度可以达到O(n)。最坏情况下的时间复杂度为O(n^2)。
直接插入排序:
直接插入排序像打扑克时抽牌,手中抽到第一张是默认是有序的,接下来抽取的时候按照自己的排序规则插入,抽一张插一张,手上的牌默认是已经有序排列的。Java实现如下:
public static void Insertion(int[] nums){
int temp,index;
for(int i=0;i<nums.length;i++){
temp=nums[i];
for(index=i;index>0&&temp<nums[index-1];index--){
nums[index]=nums[index-1];
}
nums[index]=temp;
}
}
直接插入排序算法具有稳定性,最好情况的时间复杂度为O(n),最坏情况的时间复杂度为O(n^2)。
希尔插入排序
希尔插入排序是在直接插入排序算法的基础上做了改进。设置了一个将逐渐递减的增长因子,以间隔为增长因子的两个元素进行比较执行插入算法的操作。当增长因子变为1时,排序完成。JAVA实现如下:
public static void Shell(int[] nums){
int index=0;
int temp=0;
for(int increment=nums.length/2;increment>0;increment/=2){
for(int i=increment;i<nums.length;i++){
temp=nums[i];
for(index=i-increment;index>=0;index-=increment){
if(temp<nums[index]){
nums[index+increment] = nums[index];
}else{
break;
}
}
nums[index+increment]=temp;
}
}
}
希尔排序是非稳定的排序算法,最好情况的时间复杂度为O(nlgn),最坏情况的时间复杂度为O(n^2)。
简单选择排序
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
public static void Select(int[] nums){
int position=0;
for(int i=0;i<nums.length;i++){
position=i;
int temp=nums[i];
for(int j=i+1;j<nums.length;j++){
if(nums[j]<temp){
temp=nums[j];
position=j;
}
}
nums[position]=nums[i];
nums[i]=temp;
}
}
简单选择排序是不稳定的,假设1,2,1为等待排序的序列,算法执行后,第一个1会到第二个1后面。最好最坏情况的时间复杂度均为O(n^2)。
快速排序
选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。此时基准元素在其排好序后的正确位置。以后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。Java实现如下:
public static void Partition(int[] nums, int left, int right) {
int key = nums[left];//作为旗标元素
int start=left+1,end=right;
for(int i=start;left!=right;i++){
if(nums[i]<key){
int temp = nums[i];
nums[i]=nums[left];
nums[left]=temp;
left++;
}else{
int temp = nums[right];
nums[right]=nums[i];
nums[i]=temp;
right--;
i--;
}
}
if(start<left)Partition(nums,start,left-1);
if(end>right)Partition(nums,right+1,end);
}
快速排序不稳定,最好情况时间复杂度O(nlog2n),最坏情况时间复杂度O(n^2)。
快速排序算法的改进:
在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
归并排序
归并排序就是利用归并的思想实现的排序方法。而且充分利用了完全二叉树的深度是log2n+1的特性,因此效率比较高。其基本原理如下:对于给定的一组记录,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序,最后再用递归方法将排好序的半子表合并成为越来越大的有序序列。Java实现:
public static int[] sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
sort(nums, low, mid);
// 右边
sort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
归并排序稳定,最好最坏情况下时间复杂度都是O(nlog2n)。
基数排序
基数排序已经不再是一种常规的排序方式,它更多地像一种排序方法的应用,基数排序必须依赖于另外的排序方法。基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。
多关键字排序的思路是将待排数据里德排序关键字拆分成多个排序关键字;第1个排序关键字,第2个排序关键字,第3个排序关键字……然后,根据子关键字对待排序数据进行排序。
多关键字排序时有两种解决方案:
最高位优先法(MSD)(Most Significant Digit first)
最低位优先法(LSD)(Least Significant Digit first)
例如,对如下数据序列进行排序。
192,221,12,23
可以观察到它的每个数据至多只有3位,因此可以将每个数据拆分成3个关键字:百位(高位)、十位、个位(低位)。
如果按照习惯思维,会先比较百位,百位大的数据大,百位相同的再比较十位,十位大的数据大;最后再比较个位。人得习惯思维是最高位优先方式。
如果按照人得思维方式,计算机实现起来有一定的困难,当开始比较十位时,程序还需要判断它们的百位是否相同–这就认为地增加了难度,计算机通常会选择最低位优先法。
基数排序方法对任一子关键字排序时必须借助于另一种排序方法,而且这种排序方法必须是稳定的。
对于多关键字拆分出来的子关键字,它们一定位于0-9这个可枚举的范围内,这个范围不大,因此用桶式排序效率非常好。
对于多关键字排序来说,程序将待排数据拆分成多个子关键字后,对子关键字排序既可以使用桶式排序,也可以使用任何一种稳定的排序方法。
public static void radixSort(int[] data, int radix, int d) {
// 缓存数组
int[] tmp = new int[data.length];
// buckets用于记录待排序元素的信息
// buckets数组定义了max-min个桶
int[] buckets = new int[radix];
for (int i = 0, rate = 1; i < d; i++) {
// 重置count数组,开始统计下一个关键字
Arrays.fill(buckets, 0);
// 将data中的元素完全复制到tmp数组中
System.arraycopy(data, 0, tmp, 0, data.length);
// 计算每个待排序数据的子关键字
for (int j = 0; j < data.length; j++) {
int subKey = (tmp[j] / rate) % radix;
buckets[subKey]++;
}
for (int j = 1; j < radix; j++) {
buckets[j] = buckets[j] + buckets[j - 1];
}
// 按子关键字对指定的数据进行排序
for (int m = data.length - 1; m >= 0; m--) {
int subKey = (tmp[m] / rate) % radix;
data[--buckets[subKey]] = tmp[m];
}
rate *= radix;
}
}
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大顶堆和小顶堆,是完全二叉树。大顶堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大顶堆,因为根据大顶堆的要求可知,最大的值一定在堆顶。
Java实现:
数组工具类:
public static void printArray(int[] array) {
System.out.print("{");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if (i < array.length - 1) {
System.out.print(", ");
}
}
System.out.println("}");
}
public static void exchangeElements(int[] array, int index1, int index2) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
具体排序实现:
public static void heapSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
buildMaxHeap(array);
for (int i = array.length - 1; i >= 1; i--) {
ArrayUtils.exchangeElements(array, 0, i);
maxHeap(array, i, 0);
}
}
private static void buildMaxHeap(int[] array) {
if (array == null || array.length <= 1) {
return;
}
int half = array.length / 2;
for (int i = half; i >= 0; i--) {
maxHeap(array, array.length, i);
}
}
private static void maxHeap(int[] array, int heapSize, int index) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = index;
if (left < heapSize && array[left] > array[index]) {
largest = left;
}
if (right < heapSize && array[right] > array[largest]) {
largest = right;
}
if (index != largest) {
ArrayUtils.exchangeElements(array, index, largest);
maxHeap(array, heapSize, largest);
}
}