常用的内部排序

1 排序概述

       一旦将一个杂乱无章的记录重排成一组有序记录,就能够快速地从这组记录中找到目标记录。因此通常来说,排序的目的就是快速查找。
       对于一个排序算法来说,一般从如下三个方面来衡量算法的优劣。
  1. 时间复杂度:主要是分析关键字的比较次数和记录的移动次数。
  2. 空间复杂度:分析排序算法中需要多少辅助内存。
  3. 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则成这种排序算法是稳定的;反之,就是不稳定的。
       如果整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成,这种排序就称为内部排序。如果参与排序的数据元素非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘等),这种排序就被称为外部排序

2 选择排序法

       常用的选择排序方法有两种:直接选择排序和堆排序。直接选择排序简单直观,但性能略差;堆排序是一种较为高效的选择排序方法,但实现起来略微复杂。

2、1 直接选择排序

       直接选择排序的思路很简单,它需要经过n-1趟排序。
       第1趟比较: 程序将记录定位在第1个数据上,拿第1个数据依次和它后面的每个数据进行比较,如果第1个数据大于后面某个数据,就交换它们······以此类推。经过第1趟比较,这样数据中最小的数据将被选出,它被排在第1位。
       ······
       按照此规则一共进行n-1趟比较,这组数据中第n-1小(第2大)的数据被选出,被排在第n-1位,剩下的就是最大的数据排在最后。
       直接选择排序的优点是算法简单,容易实现。
       直接选择排序的缺点是每趟排序只能确定一个元素,n个数据需要进行n-1趟排序。
import java.util.Arrays;


class DataWrap implements Comparable<DataWrap> {
	int data;
	String flag;
	
	public DataWrap(int data , String flag) {
		this.data = data;
		this.flag = flag;
	}
	
	public String toString() {
		return data + flag;
	}

	@Override
	public int compareTo(DataWrap dw) {
		return this.data > dw.data ? 1 : (this.data == dw.data ? 0 : -1);
	}
}


public class SelectSort {
	public static void selectSort(DataWrap[] data) {
		System.out.println("开始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			for ( int j = i ; j < len ; j ++ ) {
				if ( data[i].compareTo(data[j]) > 0 ) {
					DataWrap tmp = data[i];
					data[i] = data[j];
					data[j] = tmp;
				}
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}

	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(21, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(30, "*"),
			new DataWrap(16, ""),
			new DataWrap(9, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		selectSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[21, 30, 49, 30*, 16, 9]
开始排序
第1趟排序:[9, 30, 49, 30*, 21, 16]
第2趟排序:[9, 16, 49, 30*, 30, 21]
第3趟排序:[9, 16, 21, 49, 30, 30*]
第4趟排序:[9, 16, 21, 30, 49, 30*]
第5趟排序:[9, 16, 21, 30, 30*, 49]
排序之后:[9, 16, 21, 30, 30*, 49]
       其实从程序和结果中不难看出,直接选择排序每趟比较最多只需要交换一次就够:主要找到本趟比较重最小的数据,然后拿它和本趟比较中第1位的数据交换。针对上面发现的问题,对直接选择排序进行改进,改进后的算法如下所示。
import java.util.Arrays;

public class SelectSort2 {
	public static void selectSort(DataWrap[] data) {
		System.out.println("开始排序:");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			int minIndex = i;
			for ( int j = i + 1 ; j < len ; j ++ ) {
				if ( data[minIndex].compareTo(data[j]) > 0 ) {
					minIndex = j;
				}
			}
			if (minIndex != i) {
				DataWrap tmp = data[i];
				data[i] = data[minIndex];
				data[minIndex] = tmp;
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(21, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(30, "*"),
			new DataWrap(16, ""),
			new DataWrap(9, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		selectSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[21, 30, 49, 30*, 16, 9]
开始排序:
第1趟排序:[9, 30, 49, 30*, 16, 21]
第2趟排序:[9, 16, 49, 30*, 30, 21]
第3趟排序:[9, 16, 21, 30*, 30, 49]
排序之后:[9, 16, 21, 30*, 30, 49]

总结:对于直接选择排序算法而言,假设有n个数据,数据交换次数最多有n-1次,但程序比较的次数较多。总体来说,其时间效率为O(n²)。直接选择排序算法的空间效率为O(1)。从程序运行的结果来看,直接选择排序是不稳定的。

2、2 堆排序

       假设有n个数据元素的序列k0, k1,…, kn-1,当且仅当满足如下关系时,可以将这组数据称为小顶堆(小根堆)。
ki≤k2i+1且ki≤k2i+2(其中i=0,2,…,(n-1)/2)
       或者,满足如下关系,可以将这组数据称为大顶堆(大根堆)。
ki≥k2i+1且ki≥k2i+2(其中i=0,2,…,(n-1)/2)
       对于满足小根堆的数据序列k0, k1,…, kn-1,如果将它们顺序排成一棵完全二叉树,则此树的特点是,书中所有节点的值都是小于其左、右子节点的值,此树的根节点的值必然最小。反之,大根堆的根节点的值必然最大。同时,通过定义可以得知,小根堆的任意子树也是小根堆,大根堆的任意子树也是大根堆。
       
       对于堆排序的关键在于建堆,它按照如下步骤完成排序。
  1. 将索引0~n-1处的全部数据建成大根(或小根)堆,就可以选择出这组数据中的最大(或最小)值。将所建的大根(或小根)堆的根节点与这组数据的倒数第1个节点交换,就使得这组数据中的最大(或最小)值排在最后。
  2. 将索引0~n-2处的全部数据建成大根(或小根)堆,就可以选择出这组数据中的最大(或最小)值。将所建的大根(或小根)堆的根节点与这组数据的倒数第2个节点交换,就使得这组数据中的最大(或最小)值排在最后。
      ......
       通过上面的介绍可以发现,堆排序的步骤就是重复执行以下两步。
  1. 建堆。
  2. 拿堆的根节点和最后一个节点交换。
       由此可见,对于包含n个数据元素的数据组来说,堆排序需要经过n-1次建堆,每次建堆的作用就是选出该堆的最大值或最小值。堆排序本质上依然是一种选择排序。堆排序与直接选择排序的差别在于,堆排序可以通过树形结构保存部分比较结果,可减少比较次数,从而提高效率。
       接下来的关键就是建堆的过程。建堆其实比较简单,不断重复如下步骤即可(以建大根堆为例)。
  1. 从最后一个非叶子节点开始,比较该节点和它两个子节点的值;如果某个子节点的值大于父节点的值,就把父节点和较大的子节点交换。
  2. 向前逐步调整直到根节点,即保证每个父节点的值都大于等于其左、右子节点的值,建堆完成。
       例如,有如下数组数据:9,79,46,30,58,49。下面介绍完整的建堆过程。
       1、先将其转换为完成二叉树,转换得到的完全二叉树如下所示。

       2、完全二叉树的最后一个非叶子节点,也就是最后一个父节点。最后一个节点的索引为len-1,那么最后一个非叶子节点的索引应该为(len-2)/2。也就是从索引为2的节点开始,如果其子节点的值大于它本身的值,则把它和较大的子节点进行交换,即将索引为2的节点和索引为5的元素交换,交换后的结果如下所示。

       3、向前处理前一个非叶子节点(索引为(len-2)/2-1),也就是处理索引为1的节点,此时79>30、79>58,因此无需交换。
       4、向前处理前一个非叶子节点,也就是处理索引为0的节点,此时9<79,因此需要交换,交换后的结果如下所示。

       5、如果某个节点和它的某个子节点交换后,该子节点又有自己点,那么系统还需要再次对该子节点进行判断。例如,索引为0的节点和索引为1的节点交换后,索引为1的节点还有子节点,因此程序必须再次保证索引为1的节点的值大于等于其左、右子节点的值,因此还需要交换一次,交换后的结果如下所示。

       下面程序实现堆排序
import java.util.Arrays;

public class HeapSort {
	public static void heapSort(DataWrap[] data) {
		System.out.println("开始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			buildMaxHeap(data , len -1 - i);  //建大根堆
			swap(data , 0 , len -1 - i);  //交换堆顶和最后一个元素
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}
	
	private static void buildMaxHeap(DataWrap[] data , int lastIndex) {
		for ( int i = (lastIndex - 1) / 2 ; i >= 0 ; i -- ) {  //从最后一个节点的父节点开始
			int k = i;
			while (k * 2 + 1 <= lastIndex) {  //如果当前k节点的子节点存在
				int biggerIndex = k * 2 + 1;
				if(biggerIndex < lastIndex){  //如果当前k节点的右节点存在
					if(data[biggerIndex].compareTo(data[biggerIndex + 1]) < 0) {
						biggerIndex++;
					}
				}
				if(data[k].compareTo(data[biggerIndex]) < 0 ) {
					swap(data , k , biggerIndex);
					k = biggerIndex;  //重新保证k节点的值大于等于其左、右子节点的值
				}
				else {
					break;
				}
			}
		}
	}
	
	private static void swap(DataWrap[] data , int i , int j) {
		DataWrap tmp = data[i];
		data[i] = data[j];
		data[j] = tmp;
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(21, ""),
				new DataWrap(30, ""),
				new DataWrap(49, ""),
				new DataWrap(30, "*"),
				new DataWrap(21, "*"),
				new DataWrap(16, ""),
				new DataWrap(9, ""),
			};
		System.out.println("排序之前:" + Arrays.toString(data));
		heapSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[21, 30, 49, 30*, 21*, 16, 9]
开始排序
第1趟排序:[9, 30, 21, 30*, 21*, 16, 49]
第2趟排序:[16, 30*, 21, 9, 21*, 30, 49]
第3趟排序:[16, 21*, 21, 9, 30*, 30, 49]
第4趟排序:[9, 16, 21, 21*, 30*, 30, 49]
第5趟排序:[9, 16, 21, 21*, 30*, 30, 49]
第6趟排序:[9, 16, 21, 21*, 30*, 30, 49]
排序之后:[9, 16, 21, 21*, 30*, 30, 49]

总结 :对于堆排序算法而言,假设有n个数据,需要进行n-1次建堆,每次建堆时间耗时log 2 n,则其时间效率为O(n*log 2 n)。堆排序算法的空间效率为O(1)。从程序运行的结果来看,堆排序是不稳定的。

3 交换排序法

       交换排序的主要操作是对数组中的数据不断地进行交换操作。交换排序主要有冒泡排序和快速排序,这两种排序都是应用极广的排序算法。

3、1 冒泡排序

       对于包含n个数据的一组记录,在最坏的情况下,冒泡排序需要进行n-1趟排序。
       第一趟:依次比较0和1、1和2、...、n-2和n-1索引处的元素,如果发现前一个数据大于后一个数据,则交换它们。经过第一趟的比较,最大的元素排到了最后。
       第二趟:依次比较0和1、1和2、...、n-3和n-2索引处的元素,如果发现前一个数据大于后一个数据,则交换它们。经过第二趟的比较,第二大的元素排到了最后。
       ......
       第n-1趟:依次比较0和1索引出的元素,如果发现前一个数据大于后一个数据,则交换它们。经过n-1趟的比较,第n-1大的元素排到了第二位。
       实际上,冒泡排序的每趟交换后,不经能将当前最大值排到最后的位置,还能部分理顺前面的其他元素;一旦某趟没有交换发生,即可提前结束排序。
import java.util.Arrays;

public class BubbleSort {
	public static void bubbleSort(DataWrap[] data) {
		System.out.println("开始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			boolean flag = false;
			for ( int j = 0 ; j < len - 1 - i ; j ++ ) {
				if (data[j].compareTo(data[j + 1]) > 0 ) {
					DataWrap tmp = data[j];
					data[j] = data[j + 1];
					data[j + 1] = tmp;
					flag = true;
				}
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
			if (!flag) {
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*")
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		bubbleSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, 16, 21*, 23, 30, 49, 21, 30*]
开始排序
第1趟排序:[9, 16, 21*, 23, 30, 21, 30*, 49]
第2趟排序:[9, 16, 21*, 23, 21, 30, 30*, 49]
第3趟排序:[9, 16, 21*, 21, 23, 30, 30*, 49]
第4趟排序:[9, 16, 21*, 21, 23, 30, 30*, 49]
排序之后:[9, 16, 21*, 21, 23, 30, 30*, 49]

总结:对于冒泡排序算法而言,其时间效率是不确定的,在最好的情况下,初始数据序列已经处于有序状态,执行1趟冒泡即可,做n-1趟比较,无须进行任何交换;但在最坏的情况下,初始数据序列处于完全逆序状态,算法要执行n-1趟冒泡,第i趟(1<i<n)做了n-i次比较,执行n-i-1次对象交换。此时的比较总次数为n*(n-1)/2,记录移动总次数为n*(n-1)*3/2。冒泡排序算法的空间效率为O(1)。从程序运行的结果来看,冒泡排序是稳定的。

3、2 快速排序

       快速排序是一个速度非常快的交换排序算法,它的基本思路很简单:从待排序的数据序列中任取一个数据(如第一个数据)作为分界值,所有比它小的数据元素一律放在左边,所有比它大的数据元素一律放在右边。经过这样一趟下来,该序列形成左、右两个子序列,左边序列中的数据元素都比分界值小,右边序列中的数据元素都比分界值大。接下来对左、右两个子序列进行递归,对两个子序列重新选择中心元素并依照此规则调整,直到每个子序列的元素只剩一个,排序完成。
       从上面的算法分析可以看出,实现快速排序的关键在于第一趟要做的事情,如下所示。
  1. 选出指定的分界值。
  2. 将所有比分界值小的数据元素放在左边。
  3. 将所有比分界值大的数据元素放在右边。
       问题在于,如何实现上面的第2和第3步?这是就要用到交换的思想了,其思路如下。
  1. 定义一个i变量,i变量从左边的第一个索引开始,找大于分界值的元素的索引,并用i来记录它。
  2. 定义一个j变量,j变量从右边的第一个索引开始,找小于分界值的元素的索引,并用j来记录它。
  3. 如果i<j,则交换i,j两个索引处的元素。
       重复执行以上1~3步,直到i≥j,可以判断j左边的数据元素都小于分界值,j右边的数据元素都大于分界值,最后将分界值和j索引处的元素交换即可。
import java.util.Arrays;

public class QuickSort {
	private static void swap(DataWrap[] data , int i , int j ) {
		DataWrap tmp;
		tmp = data[i];
		data[i] = data[j];
		data[j] = tmp;
	}
	
	private static void subSort(DataWrap[] data , int start , int end) {
		if (start < end) {
			DataWrap base = data[start];
			int i = start;
			int j = end  + 1;
			while (true) {
				while (i < end && data[++i].compareTo(base) <= 0);
				while (j > start && data[--j].compareTo(base) >= 0);
				if (i < j) {
					swap(data , i , j);
				}
				else {
					break;
				}
			}
			swap(data , start , j);
			subSort(data, start, j - 1);
			subSort(data, j + 1, end);
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, ""),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, "*"),
			new DataWrap(30, ""),
			new DataWrap(13, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		subSort(data, 0, data.length - 1);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, -16, 21, 23, -30, -49, 21*, 30, 13]
排序之后:[-49, -30, -16, 9, 13, 21, 21*, 23, 30]

总结:快速排序的时间效率很好,因为它每趟能确定的元素呈指数增长。快速排序需要使用递归,而递归使用栈,因此它的空间效率为O(n*log2n)。快速排序中包含跳跃式交换,因此是不稳定的排序。

4 插入排序法

       插入排序也是一类非常常见的排序方法,它主要包含直接插入排序、shell排序和折半插入排序等几种常见的排序方法。

4、1 直接插入排序

       直接插入排序的思路非常简单:依次将待排序的数据元素按其关键字值的大小插入前面的有序序列。
       细化来说,对于一个有n个元素的数据序列,排序需要进行n-1趟插入操作,如下所示。
       第一趟:将第2个元素插入前面的有序子序列中,此时前面只有一个元素,当然是有序的。
       第二趟:将第3个 元素插入前面的有序子序列中,前面两个元素时有序的。
       ......
       第n-1趟:将第n个元素插入前面的有序子序列中,前面n-1个元素时有序的。
import java.util.Arrays;

public class InsertSort {
	public static void insertSort(DataWrap[] data) {
		int len = data.length;
		for ( int i = 1 ; i < len ; i ++ ) {
			DataWrap tmp = data[i];
			if (data[i].compareTo(data[i - 1]) < 0) {
				int j = i - 1;
				for ( ; j >= 0 && data[j].compareTo(tmp) > 0 ; j -- ) {
					data[j + 1] = data[j];
				}
				data[j + 1] = tmp;
			}
			System.out.println("第" + i + "趟排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*"),
			new DataWrap(30, "")
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		insertSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
第1趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第2趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第3趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第4趟排序:[-30, -16, 9, 21*, 23, -49, 21, 30*, 30]
第5趟排序:[-49, -30, -16, 9, 21*, 23, 21, 30*, 30]
第6趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
第7趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
第8趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
排序之后:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
 
总结:直接插入排序的时间效率不高,在最坏的情况下,所有元素的比较次数总和为(0+1+...+n-1)=O(n²);在其他情况下,也要考虑移动元素的次数,故时间复杂度为O(n²)。 直接插入排序算法的空间效率为O(1)。从程序运行的结果来看, 直接插入排序是稳定的。

4、2 折半插入排序

       折半插入排序是对直接插入排序的简单改进。对于直接插入排序而言,当第i-1趟需要将第i个元素插入前面的0~i-1个元素序列中时,它总是从i-1个元素开始,逐个比较每个元素,直到找到它的位置。这显然没有利用前面 0~i-1个元素已经有序的特点,而折半插入排序则改进了这一点。
       折半插入排序的做法如下所示。
  1. 计算0~i-1索引的中间点,也就是用i索引处的元素和(0+i-1)/2索引处的元素进行比较,如果i索引处的元素大,就直接在(0+i-1)/2~i-1半个范围内搜索;反之,就在0~(0+i-1)/2半个范围内搜索,这就是所谓的折半。
  2. 在半个范围内搜索时,再按第1步方法进行折半搜索。总是不断地折半,这样就可以将搜索范围不断缩小,从而快速确定第i个元素的插入位置。
注意:此处介绍的折半插入,其实就是通过不断地这般来快速确定第i个元素的插入位置,这实际上就是一种查找算法:折半查找。Java的Arrays类里有一个binarySearch()方法,它就是折半查找的实现,用于从指定数组(或数组的一部分)中查找指定元素,前提是该数组(或数组的一部分)已经处于有序状态。
     3.一旦确定了第i个元素的插入位置,程序将该位置以后的元素整体后移一位,然后将第i个元素放入该位置。
import java.util.Arrays;

public class BinaryInsertSort {
	public static void binaryInsertSort(DataWrap[] data) {
		System.out.println("开始排序");
		int len = data.length;
		for ( int i = 1 ; i < len ; i ++ ) {
			DataWrap tmp = data[i];
			int low = 0;
			int high = i - 1;
			while (low <= high) {
				int mid = (low + high) / 2;
				if (tmp.compareTo(data[mid]) > 0){
					low = mid + 1;
				}
				else {
					high = mid - 1;
				}
			}
			for (int j = i ; j > low ; j --) {
				data[j] = data[j - 1];
			}
			data[low] = tmp;
			System.out.println("第" + i + "次排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(9, ""),
				new DataWrap(-16, ""),
				new DataWrap(21, "*"),
				new DataWrap(23, ""),
				new DataWrap(-30, ""),
				new DataWrap(-49, ""),
				new DataWrap(21, ""),
				new DataWrap(30, "*"),
				new DataWrap(30, "")
			};
			System.out.println("排序之前:" + Arrays.toString(data));
			binaryInsertSort(data);
			System.out.println("排序之后:" + Arrays.toString(data));
	}
}
总结:折半插入排序与直接插入排序的效果基本相同,只是更快一些,因此折半插入排序可以更快地准确第i个元素的插入位置。

4、3 Shell排序

       Shell排序对直接插入排序进行了简单改进:它通过加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项大跨度的移动。当这些数据项排过一趟序列,Shell排序算法减小数据项的间隔再进行排序,依此进行下去。这些进行排序的数据项之间的间隔被称为增量,习惯上用h来表示这个增量。
       假设本次Shell排序的h为4,其插入操作如下。
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
       注意上面红色的数据。
       当h增量为4时,第1趟将保证做引为0,4,8的数据元素已经有序。第1趟完成后,算法向右移一步,对索引为1,5的数据元素进行排序。这个排序过程持续进行,直到所有的数据项都已经完成了以4为增量的排序。也就是说,所有间隔为4的数据项之间都已经排列有序。当完成以4为增量的Shell排序后,所有元素离它最终有序序列中的位置相差不到两个单元,这就是数组“基本有序”的含义。通过创建这种交错的内部有序的数据项集合,就可以减少直接插入排序中数据项“整体搬家”的工作量。
       上面已经演示了以4为增量的Shell排序,接下来应该减少增量,直到完成以1位增量的Shell排序,此时数据序列将会变为有序序列。
注意:通过上面的介绍可以发现,可以认为直接插入排序是Shell排序的一种特例——直接使用增量为1的Shell排序就是直接插入排序。
       最终确定Shell排序算法的关键在于确定h序列的值。常用的h序列有Knuth提出,该序列从1开始,通过h=3*h+1产生。前面公式用于从1开始计算这个序列,可以看到h序列为1,4,13,40......反过来,程序中还需要反向计算h序列,那应该使用公式:h=(h-1)/3。
import java.util.Arrays;

public class ShellSort {
	public static void shellSort(DataWrap[] data) {
		System.out.println("开始排序");
		int len = data.length;
		int h = 1;  //h变量保存可变增量
		while (h <= len / 3) {
			h = h * 3 + 1;
		}
		while (h > 0) {
			System.out.println("===h的值:" + h + "===");
			for ( int i = h ; i < len ; i ++) {
				DataWrap tmp = data[i];
				if (data[i].compareTo(data[i - h]) < 0) {
					int j = i - h;
					for ( ; j >=0 && data[j].compareTo(tmp) > 0 ; j -= h ) {
						data[j + h] = data[j];
					}
					data[j + h] = tmp;
				}
			}
			h = (h - 1) / 3;
		}
	}
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(9, ""),
				new DataWrap(-16, ""),
				new DataWrap(21, "*"),
				new DataWrap(23, ""),
				new DataWrap(-30, ""),
				new DataWrap(-49, ""),
				new DataWrap(21, ""),
				new DataWrap(30, "*"),
				new DataWrap(30, "")
			};
			System.out.println("排序之前:" + Arrays.toString(data));
			shellSort(data);
			System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
开始排序
===h的值:4===
===h的值:1===
排序之后:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
       Shell排序比插入排序快很多,因为当h值大的时候,数据项每一趟排序需要移动元素的个数很少,但数据项移动的距离很长,这是非常有效率的。当h减小时,每一趟排序需要移动的元素的个数增多,但是此时数据项已经接近于它们排序最终的位置,这对插入排序可以更有效率。正是这两种情况的结合才使得Shell排序效率那么高。但是Shell排序是不稳定的排序。

4 归并排序法

       归并的基本思想就是将两个(或以上)有序的序列合并成一个新的有序序列。西华来说,归并排序先将长度为n的无序序列看成是n个长度为1的有序子序列,首先做两两合并,得到n/2个长度为2的有序子序列,再做两两合并......不断地重复这个过程,最终可以得到一个长度为n的有序序列。

       上图是归并排序的示意过程,总结来说,对于长度为n的数据序列,只需经过log2n次合并。
       对于归并排序而言,其算法关键就在于“合并”。合并算法的具体步骤如下。
  1. 定义变量i,i从0开始,依次等于A序列的每个元素的索引。
  2. 定义变量j,j从0开始,依次等于B序列的每个元素的索引。
  3. 拿A序列中i索引处的元素和B序列中j索引处的元素进行比较,将较小的复制到一个临时数组中。
  4. 如果i索引处的元素小,则i++;如果j索引处的元素小,则j++。
       不断地重复上面四个步骤,即可将A、B两个序列中的数据元素复制到临时数组中,直到其中一个数组中的所有元素都被复制到临时数组中。最后,将另一个数组多出来的元素全部复制到临时数组中,合并即完成,再将临时数组中的元素复制回去即可。
import java.util.Arrays;

public class MergeSort {
	public static void mergeSort(DataWrap[] data) {
		sort(data , 0 , data.length - 1);
	}
	
	/**
	 * 将索引从left到right范围的数组元素进行归并排序
	 * @param data 待排序的数组
	 * @param left 待排序的数组的第一个元素的索引
	 * @param right 待排序的数组的最后一个元素的索引
	 */
	private static void sort(DataWrap[] data , int left , int right) {
		if (left < right) {
			int center = (left + right) / 2;
			sort(data , left , center);  //对左边数组进行递归
			sort(data , center + 1 , right);  //对右边数组进行递归
			merge(data , left , center , right);
		}
	}
	
	/**
	 * 将两个数组进行归并,归并前两个数组已经有序,归并后依然有序
	 * @param data 数组对象
	 * @param left 左数组的第一个元素的索引
	 * @param center 左数组的最后一个元素的索引,center + 1是右数组的第一个元素的索引
	 * @param right 右数组的最后一个元素的索引
	 */
	private static void merge(DataWrap[] data , int left , int center , int right) {
		DataWrap[] tmpArr = new DataWrap[data.length];
		int mid = center + 1;
		int third = left;  //用于记录中间数组的索引
		int tmp = left;
		while (left <= center && mid <= right) {  //从两个数组中取出小的放入中间数组
			if (data[left].compareTo(data[mid]) <= 0) {
				tmpArr[third++] = data[left++];
			}
			else {
				tmpArr[third++] = data[mid++];
			}
		}
		while (mid <= right) {  //剩余部分依次放入中间数组
			tmpArr[third++] = data[mid++];
		}
		while (left <= center) {
			tmpArr[third++] = data[left++];
		}
		while(tmp <= right) {  //将中间数组中的内容复制回原数组
			data[tmp] = tmpArr[tmp++];
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*"),
			new DataWrap(30, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		mergeSort(data);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
排序之后:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]

总结:归并算法需要递归地进行分解、合并,每进行一趟归并排序需要调用一次merge()方法一次,每次执行merge()方法需要比较n次,因此归并排序算法的时间复杂度为O(n*log2n)。但是归并算法的空间效率较差,它需要一个与原始序列同样大小的辅助序列。归并排序是稳定的。

5 桶式排序法

       桶式排序不再是一种基于比较的排序方法,它是一种非常巧妙的排序方式,但这种排序方式需要待排序序列满足如下特征:
  1. 待排序列的所有值处于一个可枚举范围内。
  2. 待排序列所在的这个可枚举范围不应该太大,否则排序开销太大。
       下面介绍桶式排序的详细过程,以如下待排序列为例:5,4,2,4,1。这个待排序列处于0~5这个可枚举范围之内,而且这个范围很小,正是桶式排序大有用处的地方。
       具体步骤如下:
       1、对这个可枚举范围构建一个buckets数组,用于记录“落入”每个桶中的元素的个数。

       2、按如下公式对上图所示的buckets数组的元素进行重新计算。
             buckets[i]=buckets[i]+buckets[i-1](其中1≤i≤buckets.length)

       重新计算后的buckets数组元素保存了“落入”当前桶和“落入”前面所有桶中元素的总数组,而且定义的桶本身就是从小到大排列的,也就是说,“落入”前面桶中的元素肯定小于“落入”当前桶中的元素。综合上面两点,可以得到一个结论:每个buckets数组元素的值小于、等于“落入”当前桶中元素的个数。也就是说,“落入”当前桶中的元素在有序序列中应该排在buckets数组元素值所确定的位置。
       上面的理论还有些抽象。以待排序列中的最后一个元素1为例,找到新buckets数组中元素1对应桶的值,这表明元素1就应该排在第1位;再以待排序列中倒数第2个元素4为例,找到新的buckets数组中元素4对应的值,这表明元素4就应该排在第4位,依次类推。
import java.util.Arrays;

public class BucketSort {
	public static void bucketSort(DataWrap[] data , int min , int max) {
		System.out.println("开始排序:");
		int arrayLength = data.length;
		DataWrap[] tmp = new DataWrap[arrayLength];
		int[] buckets = new int[max - min];  //buckets数组相当于定义了max-min个桶,并用于记录待排序元素的信息
		for ( int i = 0 ; i < arrayLength ; i ++) {  //计算原buckets数组
			buckets[data[i].data - min]++;
		}
		System.out.println(Arrays.toString(buckets));
		for ( int i = 1 ; i < max - min ; i ++ ) {  //计算“落入”各桶内的元素在有序序列中的位置
			buckets[i] = buckets[i] + buckets[i - 1];
		}
		System.out.println(Arrays.toString(buckets));
		System.arraycopy(data, 0, tmp, 0, arrayLength);  //将data数组中数据完全复制,进行缓存
		for ( int k = arrayLength - 1 ; k >= 0 ; k -- ) {
			data[--buckets[tmp[k].data - min]] = tmp[k];
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(5, ""),
			new DataWrap(-1, ""),
			new DataWrap(8, ""),
			new DataWrap(5, "*"),
			new DataWrap(7, ""),
			new DataWrap(3, ""),
			new DataWrap(-3, ""),
			new DataWrap(1, ""),
			new DataWrap(3, "*"),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		bucketSort(data , -3 , 10);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[9, 5, -1, 8, 5*, 7, 3, -3, 1, 3*]
开始排序:
[1, 0, 1, 0, 1, 0, 2, 0, 2, 0, 1, 1, 1]
[1, 1, 2, 2, 3, 3, 5, 5, 7, 7, 8, 9, 10]
排序之后:[-3, -1, 1, 3, 3*, 5, 5*, 7, 8, 9]


总结:桶式排序的时间效率极高,只需要经过两轮遍历就可以得到每个待排数据在有序序列中的位置。但是桶式排序的空间开销比较大,它需要两个数组:第1个buckets数组用于记录“落入”各桶中元素的个数,进入保存各元素在有序序列中的位置;第2个数组用于缓存待排数据。桶式排序是稳定的。

6 基数排序法

       基数排序法不再是一种常规的排序方法,它必须依赖于另外的排序方法。基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序。多关键字排序的思路就是将待排序数据里的排序关键字拆分成多个排序关键字,然后,根据子关键字对待排数据进行排序。
       在进行多关键字排序时有两种解决方案。
  1. 最高位优先法MSD(Most Significant Digit first)
  2. 最低位优先法LSD(Least Significant Digit first)
       例如,对192,221,13,23进行排序,可以观察到它每个数据之多只有3位,因此可以将每个数据拆分成3个关键字:百位(最高位)、十位、个位(最低位)。
       如果按照习惯思维,会先比较百位,百位大的数据大;百位相同再比较十位,十位大的数据大;最后再比较个位。人的习惯思维是最高位优先方式。但是这种方式计算机实现起来有一定的难度,当开始比较十位时,还需要判断它们百位是否相同。计算机通常会选择最低位优先法,如下所示。
       第1轮比较个位,对个位关键字排序后得到序列为:221,192,13,23。
       第2轮比较十位,对十位关键字排序后得到序列为: 13,23, 221,192。
       第3轮比较百位,对百位关键字排序后得到序列为:13,23,221,192。
       从上面介绍可以看出,基数排序方法对任一个关键字排序时必须借助另一种排序方法,而且这种排序方法必须是稳定的。根据桶式排序的特点,一般采用桶式排序的方式。
import java.util.Arrays;

public class MultiKeyRadixSort {
	/**
	 * @param data 待排序数组
	 * @param radix 指定关键字拆分的进制。如radix=10,表明按十进制拆分。
	 * @param d 指定将关键字拆分成几个关键字
	 */
	public static void radixSort(int[] data , int radix , int d) {
		System.out.println("开始排序");
		int arrayLength = data.length;
		int[] tmp = new int[arrayLength];
		int[] buckets = new int[radix];
		for ( int i = 0 , rate = 1 ; i < d ; i ++ ) { //rate用于保存当前计算的位
			Arrays.fill(buckets, 0);  //重置count数组,开始统计第二个关键字
			System.arraycopy(data, 0, tmp, 0, arrayLength);
			for ( int j = 0 ; j < arrayLength ; 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 = arrayLength - 1 ; m >=0 ; m -- ) {  //按子关键字对指定数据进行排序
				int subKey = (tmp[m] / rate) % radix;
				data[--buckets[subKey]] = tmp[m];
			}
			System.out.println("对" + rate + "位上子关键字排序:" + Arrays.toString(data));
			rate *= radix;
		}
	}
	
	public static void main(String[] args) {
		int[] data = {1100 , 192 , 211 , 12 , 14};
		System.out.println("排序之前:" + Arrays.toString(data));
		radixSort(data , 10 , 4);
		System.out.println("排序之后:" + Arrays.toString(data));
	}
}
输出结果为:
排序之前:[1100, 192, 211, 12, 14]
开始排序
对1位上子关键字排序:[1100, 211, 192, 12, 14]
对10位上子关键字排序:[1100, 211, 12, 14, 192]
对100位上子关键字排序:[12, 14, 1100, 192, 211]
对1000位上子关键字排序:[12, 14, 192, 211, 1100]
排序之后:[12, 14, 192, 211, 1100]

提示:对于多关键字来说,程序将待排数据拆分成多个子关键字后,既可以使用桶式排序,也可使用任何一种稳定的排序方法。
















  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值