选择排序与插入排序的实现与对比(Java 实现)
选择排序
基本概念
首先,找到需要进行排序的数组中最小的那个元素,接着,将它和数组的第一个元素交换位置(如果第一个元素就是最小的元素那么将它和它自己进行交换)。接着,再在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。以此类推,直到最后将整个数组排序。这就是选择排序的排序思想,它总是在不断地选择剩余元素之中的最小者。
其排序思想如下图所示(这里以排序 [8, 5, 2, 4] 为例)
选择排序实现代码如下
/**
* 选择排序的算法实现,将 array 按升序排列
* 使用 Comparable 数组确保实现 Comparable 接口的类对象都可以用此方法进行选择排序
*
* @param array 进行选择排序的数组
*/
public static void sort(Comparable[] array) {
// 取得数组长度
int n = array.length;
// 将 array[i] 和 array[i+1...N] 中最小的元素交换
for (int i = 0; i < n; i++) {
// 用于标记 array[i+1...N] 中最小元素的索引
int minIndex = i;
// 寻找当前 array[i+1...N] 中最小元素的索引
for (int j = i + 1; j < n; j++) {
// 判断当前 array[j] 是否比 array[minIndex] 还要小
if (less(array[j], array[minIndex])) {
// 还要小的话将最小元素的索引标记为 j
minIndex = j;
}
}
// 找到后将 array[i] 和 array[i+1...N] 中最小的元素交换
swap(array, i, minIndex);
}
}
/**
* 判断元素 v 是否比元素 w 小
*
* @return 返回 true,则 v < w;反之 v >= w
*/
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
/**
* 交换数组 array 指定索引位置 i 和 j 的元素
*
* @param array 需要进行元素互换的数组
* @param i 需要交换的元素的索引位置
* @param j 需要交换的元素的索引位置
*/
private static void swap(Comparable[] array, int i, int j) {
Comparable temp = array[i];
array[i] = array[j];
array[j] = temp;
}
从以上代码可知,选择排序的内部循环只是在比较当前元素与目前已知的最小元素谁大谁小,而且需要注意的是如果最小元素在数组中处于靠前的位置,那么它也不会提前停止,还是会继续比较剩余的元素。对于处于外部循环中的交换函数,它每次交换都能排定一个元素,比如一个数组第一次查找最小元素时最小元素在第一个位置,那么 i 和 minIndex 都会是同一个值,它不会说最小元素还是自己就不会进行交换,还是会执行交换函数将它和它自己进行互换。所以对于交换函数来说,交换的总次数总是 n。所以,算法的时间效率就取决于内部循环中比较的次数。
此处引入一个命题:对于长度为 N 的数组,选择排序需要大约 N2/2 次比较和 N 次交换。
选择排序的特点
-
运行时间和输入无关。
对于选择排序来说,排序的数组不管是已经有序的还是元素全都是一样的又或者是元素全部随机分布的,它所耗费的时间都是相差不大的。此处引入一个我用于测试排序算法的测试类
import java.lang.reflect.Method; import java.util.Arrays; import java.util.Random; /** * 测试各排序算法的工具类 * * @author 踏雪彡寻梅 * @version 1.0 * @date 2019-05-14 23:57 */ public class SortCompare { /** * 该测试类不产生任何实例 */ private SortCompare(){} /** * 判断元素 v 是否比元素 w 小 * * @return 返回 true,则 v < w;反之 v >= w */ private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } /** * 检测 array 是否排序成功 * @param array 进行检测的数组 * @return 返回 true 表示排序成功,返回 false 表示排序失败 */ private static boolean isSorted(Comparable[] array){ int n = array.length; for (int i = 1; i < n; i++){ // 如果某个元素比它前面的一个元素还要小,说明排序失败 if(less(array[i], array[i - 1])){ return false; } } return true; } /** * 返回运行 sortName 排序算法所需要的时间,单位:秒 * @param sortName 进行测试的排序算法的类名称 * @param array 进行排序的数组 * @return 返回排序算法运行的时间 */ public static double time(String sortName, Comparable[] array){ // 排序总用时,单位:秒 double totalTime = 0.0; // 通过 Java 的反射机制,通过排序算法的类名,运行排序函数 try { // 通过 sortName 获得排序函数的 Class 对象 Class sortClass = Class.forName(sortName); // 通过排序函数的 Class 对象获得排序方法 Method sortMethod = sortClass.getMethod("sort", new Class[]{ Comparable[].class }); // 使用 array 数组生成排序函数的排序参数,各排序函数的参数只有一个,即 array 数组 Object[] params = new Object[]{array}; // 算法开始运行时的时间 long startTime = System.currentTimeMillis(); // 调用排序函数 sortMethod.invoke(null, params); // 算法结束时的时间 long endTime = System.currentTimeMillis(); // 检测排序是否成功 assert isSorted(array); // 计算排序用时,单位:秒 totalTime = (endTime - startTime) / 1000.0; } catch (Exception e){ System.err.println("计算排序算法运行时间失败!"); e.printStackTrace(); } return totalTime; } /** * 使用排序算法 sortName 对 T 个长度为 N 的随机数组进行排序测试,计算 T 次测试的总时间 * @param sortName 进行测试的排序算法的类名称 * @param N 测试数组的元素个数 * @param T 测试次数 * @return 返回测试的总时间,单位:秒 */ public static double timeRandomInput(String sortName, int N, int T){ // 随机数种子 long seed = System.currentTimeMillis(); Random random = new Random(seed); // 总时间 double totalTime = 0.0; // 进行测试的随机数组,这里使用 Double 类型的数组 // 各元素的范围为 [0.0, 1.0) // 这样几乎不可能产生相等的主键值 Double[] array = new Double[N]; for (int t = 0; t < T; t++){ // 进行一次测试(生成一个随机数组并排序) for (int i = 0; i < N; i++) { // 范围: [0.0, 1.0) array[i] = random.nextDouble(); } // 对生成的随机数组进行时间计算,并同时记录到总时间中 totalTime += time(sortName, array); } return totalTime; } /** * 生成有 n 个元素的随机数组,每个元素的随机范围为 [0.0, 1.0) * 这里的测试用例只生成 Double 型的测试用例 * 各元素的范围为 [0.0, 1.0) * 这样几乎不可能产生相等的主键值 * @param n 数组的元素个数 * @return 返回一个测试用例数组 */ public static Double[] generateRandomArray(int n) { // 随机数种子 long seed = System.currentTimeMillis(); Random random = new Random(seed); // 测试用例数组 Double[] array = new Double[n]; for (int i = 0; i < n; i++) { // 范围: [0.0, 1.0) array[i] = random.nextDouble(); } // 返回测试用例数组 return array; } /** * 生成有 n 个元素的 Integer 型随机数组,每个元素的随机范围为 [rangeL, rangeR] * @param n 数组的元素个数 * @param rangeL 元素的最小值 * @param rangeR 元素的最大值 * @return 返回一个测试用例数组 */ public static Integer[] generateRandomArray(int n, int rangeL, int rangeR) { // 检查元素的左范围是否小于等于右范围 assert rangeL <= rangeR; // 用于存储随机生成的测试用例 Integer[] array = new Integer[n]; for (int i = 0; i < n; i++) { // 范围:[rangeL, rangeR] array[i] = (int) (Math.random() * (rangeR - rangeL + 1) + rangeL); } // 返回测试用例数组 return array; } /** * 生成一个近乎有序的 Integer 型测试用例数组 * 首先生成一个含有 [0...n-1] 的完全有序数组, 之后随机交换 swapTimes 对数据 * swapTimes 定义了数组的无序程度: * swapTimes == 0 时, 数组完全有序 * swapTimes 越大, 数组越趋向于无序 * @param n 希望生成数组的元素个数 * @param swapTimes 交换的元素对数 * @return 返回一个近乎有序的测试用例数组 */ public static Integer[] generateNearlyOrderedArray(int n, int swapTimes){ Integer[] array = new Integer[n]; for( int i = 0 ; i < n ; i ++ ) { array[i] = i; } for( int i = 0 ; i < swapTimes ; i ++ ){ // 随机生成一对交换索引 int a = (int)(Math.random() * n); int b = (int)(Math.random() * n); // 交换 int temp = array[a]; array[a] = array[b]; array[b] = temp; } return array; } /** * 生成一个有 n 个元素的 Integer 型逆序(递减)测试用例数组 * @param n 数组元素个数 * @return 返回一个逆序的测试用例数组 */ public static Integer[] generateInvertedArray(int n){ Integer[] array = new Integer[n]; int index = 0; for (int i = n - 1; i >= 0; i--) { array[index++] = i; } return array; } /** * 生成一个数量级为 n 的具有大量相同元素的 Integer 型测试用例数组 * n 必须为 100 的倍数 * @param n 数组元素个数 * @return 返回一个具有大量相同元素的测试用例数组 */ public static Integer[] generateManySameElementsArray(int n){ assert n % 100 == 0; Integer[] array = new Integer[n]; int index = 0; for (int i = 0; i < n / 100; i++) { // range: [0, 5] int element = (int) (Math.random() * (5 + 1) + 0); for (int j = 0; j < 100; j++){ array[index++] = element; } } return array; } /** * 打印 array 数组的所有内容 * @param array 要进行打印的数组 */ public static void printArray(Object[] array) { System.out.print("当前数组中元素: ["); for (int i = 0; i < array.length; i++){ if(i != array.length - 1) { System.out.print(array[i]); System.out.print(", "); } else { System.out.println(array[i] + "]"); } } } /** * 使用排序算法 sortName 对数组 array 进行 T 次排序测试,取平均时间,单位:秒 * @param sortName 进行测试的排序算法类名称 * @param array 进行测试的数组 * @param T 测试次数 */ public static void test(String sortName, Comparable[] array, int T){ // 总时间 double totalTime = 0.0; // 临时数组,用于重复测试 array 所用 Comparable[] tempArray = Arrays.copyOf(array, array.length); for (int t = 0; t < T; t++){ totalTime += time(sortName, array); // 避免重复排序已经有序的 array array = Arrays.copyOf(tempArray, tempArray.length); } // 平均时间 final double avg = totalTime / T; // 输出结果 System.out.printf("排序算法:%s, 排序数量级:%d, 测试 %d 次平均耗时:%.7f s\n", sortName, array.length, T, avg); } /** * 显示两个排序算法 sortName1 和 sortName2 的对比结果 * @param sortName1 进行对比的排序算法的名称 * @param sortName2 进行对比的排序算法的名称 * @param N 两种排序算法测试的数量级 * @param time1 sortName1 排序所需的时间 * @param time2 sortName2 排序所需的时间 */ public static void showCompareResult(String sortName1, String sortName2, int N, double time1, double time2){ // 两者之比 final double ratio; // if(time1 > time2){ ratio = time1 / time2; System.out.println("For " + N + " elements"); System.out.println(sortName1 + " ---> time1: " + time1 + " s"); System.out.println(sortName2 + " ---> time2: " + time2 + " s"); System.out.printf("\t%s is %.7f times faster than %s.\n", sortName2, ratio, sortName1); } else if(time1 <= time2) { ratio = time2 / time1; System.out.println("For " + N + " elements"); System.out.println(sortName1 + " ---> time1: " + time1 + " s"); System.out.println(sortName2 + " ---> time2: " + time2 + " s"); System.out.printf("\t%s is %.7f times faster than %s.\n", sortName1, ratio, sortName2); } } /** * 测试工具类 */ public static void main(String[] args) { final int N = 2_0000; final int T = 20; double time1 = timeRandomInput("selectionsort.SelectionSort", N, T); double time2 = timeRandomInput("insertionsort.InsertionSort", N, T); showCompareResult("SelectionSort", "InsertionSort", N, time1, time2); System.out.println("\n=====================\n"); Double[] array1 = generateRandomArray(N); Double[] array2 = Arrays.copyOf(array1, array1.length); test("selectionsort.SelectionSort", array1, T); test("insertionsort.InsertionSort", array2, T); } }
测试代码
/** * 测试用例 */ public static void main(String[] args) { final int N = 2_0000; final int T = 20; System.out.println("测试对各元素随机分布的数组进行排序: "); Double[] array1 = SortCompare.generateRandomArray(N); // selectionsort.SelectionSort: 我的选择排序写在了 SelectionSort 类中,这个类我放在在包 selectionsort 中,所以这里这样传参 SortCompare.test("selectionsort.SelectionSort", array1, T); System.out.println("\n=============================\n"); System.out.println("测试对各元素都相等的数组进行排序: "); Integer[] array2 = SortCompare.generateRandomArray(N, 0, 0); SortCompare.test("selectionsort.SelectionSort", array2, T); System.out.println("\n=============================\n"); System.out.println("测试对近乎有序的数组进行排序:"); int swapTimes = 100; Integer[] array3 = SortCompare.generateNearlyOrderedArray(N, swapTimes); SortCompare.test("selectionsort.SelectionSort", array3, T); System.out.println("\n=============================\n"); System.out.println("测试对完全有序的数组进行排序: "); swapTimes = 0; Integer[] array4 = SortCompare.generateNearlyOrderedArray(N, swapTimes); SortCompare.test("selectionsort.SelectionSort", array4, T); System.out.println("\n=============================\n"); System.out.println("测试对有大量相同元素的数组进行排序:"); Integer[] array5 = SortCompare.generateManySameElementsArray(N); SortCompare.test("selectionsort.SelectionSort", array5, T); System.out.println("\n=============================\n"); System.out.println("测试对元素呈降序排列的数组进行排序:"); Integer[] array6 = SortCompare.generateInvertedArray(N); SortCompare.test("selectionsort.SelectionSort", array6, T); }
测试结果
从结果可以看出,排序的数组不管是已经有序的还是元素全都是一样的又或者是元素全部随机分布的再或者其他情况的,它进行排序所耗费的时间都是相差不大的。 -
数据移动是最少的。
在选择排序中,交换次数和数组的大小是有关系的,对于长度为 N 的数组,选择排序需要进行 N 次交换,所以它的交换次数和数组的大小是线性关系。对于其他算法来说,大部分的增长数量级都是线性对数或是平方级别。
插入排序
基本概念
在我们平时打扑克牌时,整理时总是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。插入排序也是大致的道理,为了给需要插入的元素腾出空间,需要将其余所有元素在插入之前都向右移动一位。
算法思想如下图所示(这里以排序 [8, 5, 2, 4] 为例)
和选择排序的异同
- 和选择排序一样,当前索引左边的所有元素都是有序的,但它们的最终位置还不确定,为了给后续元素中更小的元素腾出空间,它们可能会被移动。当索引到达数组的右端时,数组排序就完成了。
- 和选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中元素已经有序(或者近乎有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
此处引入一个命题:对于随机排列的长度为 N 且各元素不重复的数组,平均情况下插入排序需要大约 N2/4 次比较及大约 N2/4 次交换。最坏情况(逆序数组)需要大约 N2/2 次比较和大约 N2/2 次交换,最好情况(数组已经有序)需要 N - 1 次比较和 0 次交换。
插入排序的特性
- 插入排序对于实际应用中常见的某些类型的非随机数组很有效。例如当用插入排序对一个有序数组进行排序时,插入排序能够立即发现每个元素都已经在合适的位置之上,它的运行时间也是线性的(对于这种数组,选择排序的运行时间是平方级别的)。
- 同理,插入排序对近乎有序的数组进行排序时也是很有效的。当数组中不在正确位置(数组有序时元素所在的位置)上的元素越少的时候,插入排序排序的速度就会越快。
插入排序实现代码如下
/**
* 插入排序未优化写法
* 将 array 按升序排列
*
* @param array 进行插入排序的数组
*/
public static void sort(Comparable[] array) {
// 取得数组长度
int n = array.length;
// 将 array[i] 插入到 array[i-1], array[i-2], array[i-3]...之中
for (int i = 1; i < n; i++) {
for (int j = i; j > 0 && less(array[j], array[j - 1]); j--) {
swap(array, j, j - 1);
}
}
}
/**
* 判断元素 v 是否比元素 w 小
*
* @return 返回 true,则 v < w;反之 v >= w
*/
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
/**
* 交换数组 array 指定索引位置 i 和 j 的元素
*
* @param array 需要进行元素互换的数组
* @param i 需要交换的元素的索引位置
* @param j 需要交换的元素的索引位置
*/
private static void swap(Comparable[] array, int i, int j) {
Comparable temp = array[i];
array[i] = array[j];
array[j] = temp;
}
从以上代码可以看出,对于 1 到 N - 1 之间的每一个 i,将 array[i] 与 array[0] 到 array[i - 1] 中比它小的所有元素依次有序地交换。在索引 i 由左向右变化的过程中,它左侧的元素总是有序的,所以当 i 到达数组的右端时排序就完成了。
也可以看出对于每次与前面的元素进行交换时都需要进行三次赋值操作,这显然是会耗费一些时间的,所以以上算法可改进如下:
/**
* 插入排序优化写法
* 此写法访问数组的次数比使用交换函数的写法少了一半左右,可以提升一些效率
* 将 array 按升序排列
*
* @param array 进行插入排序的数组
*/
public static void sort(Comparable[] array) {
// 取得数组长度
int n = array.length;
// 将 array[i] 插入到 array[i-1], array[i-2], array[i-3]...之中
for (int i = 1; i < n; i++) {
// 先将 array[i] 复制一份
Comparable e = array[i];
int j = i;
for (; j > 0 && less(e, array[j - 1]); j--) {
// 将较大的元素往右移
array[j] = array[j - 1];
}
// 最后将 e 赋值回来,确保插入排序成功
array[j] = e;
}
}
以上代码的思路如下图所示(这里以排序 [8, 5, 2, 4] 为例):
现在对两种写法分别进行测试一遍,以观察这两种写法的效率。
测试代码
/**
* 测试插入排序
*/
public static void main(String[] args) {
final int N = 2_0000;
final int T = 20;
System.out.println("测试对各元素随机分布的数组进行排序, 各元素范围:[0.0, 1.0)。");
Double[] array1 = SortCompare.generateRandomArray(N);
// insertionsort.InsertionSort: 我的插入排序写在了 InsertionSort 类中,这个类我放在在包 insertionsort 中,所以这里这样传参
SortCompare.test("insertionsort.InsertionSort", array1, T);
System.out.println("\n=============================\n");
System.out.println("测试对各元素都相等的数组进行排序, 各元素值:0。");
Integer[] array2 = SortCompare.generateRandomArray(N, 0, 0);
SortCompare.test("insertionsort.InsertionSort", array2, T);
System.out.println("\n=============================\n");
System.out.println("测试对完全有序的数组进行排序: ");
int swapTimes = 0;
Integer[] array3 = SortCompare.generateNearlyOrderedArray(N, swapTimes);
SortCompare.test("insertionsort.InsertionSort", array3, T);
System.out.println("\n=============================\n");
System.out.println("测试对近乎有序的数组进行排序: ");
swapTimes = 100;
Integer[] array4 = SortCompare.generateNearlyOrderedArray(N, swapTimes);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
System.out.println("测试对有大量相同元素的数组进行排序:");
Integer[] array5 = SortCompare.generateManySameElementsArray(N);
SortCompare.test("insertionsort.InsertionSort", array5, T);
System.out.println("\n=============================\n");
System.out.println("测试对元素呈降序排列的数组进行排序:");
Integer[] array6 = SortCompare.generateInvertedArray(N);
SortCompare.test("insertionsort.InsertionSort", array6, T);
}
使用未优化写法时的测试结果
使用优化写法时的测试结果
可以看出,使用优化的写法时在效率上比未优化的写法快速了一些。这是因为使用优化的写法时访问数组的次数会比使用未优化的写法少了一半左右,可以提升一些效率。
总的来说,插入排序对于近乎有序的数组十分高效,也很适合小规模数组。
选择排序和插入排序的效率对比
最后,在通过一组测试用例来进行选择排序和插入排序的效率对比,来观察在各情况下两种算法的效率。
测试代码
public static void main(String[] args) {
final int N = 2_0000;
final int T = 20;
System.out.println("测试对各元素随机分布的数组进行排序, 各元素范围:[0.0, 1.0)。");
Double[] array1 = SortCompare.generateRandomArray(N);
Double[] array2 = Arrays.copyOf(array1, array1.length);
SortCompare.test("selectionsort.SelectionSort", array1, T);
SortCompare.test("insertionsort.InsertionSort", array2, T);
System.out.println("\n=============================\n");
System.out.println("测试对有大量相同元素的数组进行排序:");
Integer[] array3 = SortCompare.generateManySameElementsArray(N);
Integer[] array4 = Arrays.copyOf(array3, array3.length);
SortCompare.test("selectionsort.SelectionSort", array3, T);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
int swapTimes = 100;
System.out.println("测试对近乎有序的数组进行排序: ");
array3 = SortCompare.generateNearlyOrderedArray(N, swapTimes);
array4 = Arrays.copyOf(array3, array3.length);
SortCompare.test("selectionsort.SelectionSort", array3, T);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
System.out.println("测试对各元素都相等的数组进行排序, 各元素值:0。");
array3 = SortCompare.generateRandomArray(N, 0, 0);
array4 = Arrays.copyOf(array3, array3.length);
SortCompare.test("selectionsort.SelectionSort", array3, T);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
System.out.println("测试对完全有序的数组进行排序: ");
swapTimes = 0;
array3 = SortCompare.generateNearlyOrderedArray(N, swapTimes);
array4 = Arrays.copyOf(array3, array3.length);
SortCompare.test("selectionsort.SelectionSort", array3, T);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
System.out.println("测试逆序排列的数组:");
array3 = SortCompare.generateInvertedArray(N);
array4 = Arrays.copyOf(array3, array3.length);
SortCompare.test("selectionsort.SelectionSort", array3, T);
SortCompare.test("insertionsort.InsertionSort", array4, T);
System.out.println("\n=============================\n");
System.out.println("测试 " + T + " 种 " + N + " 数量级的随机数组( random range: [0.0, 1.0) )两种排序算法的效率之比:");
double time1 = SortCompare.timeRandomInput("selectionsort.SelectionSort", N, T);
double time2 = SortCompare.timeRandomInput("insertionsort.InsertionSort", N, T);
SortCompare.showCompareResult("SelectionSort", "InsertionSort", N, time1, time2);
}
测试结果
观察前 6 个测试,可以看出在各元素随机分布的数组中,选择排序和插入排序的效率都差不多。而在逆序数组中,选择排序的速度要快过插入排序。对于其他四种情况,插入排序都要比选择排序更快。对于最后一个测试,是用这两种排序算法对 20 个不同的随机数组进行排序,统计出它们各自的总时间,然后进行对比。可以看出在此测试中,选择排序要稍稍快过插入排序一些,它们的时间之比是一个较小的常数,有时候可能会是插入排序快过选择排序一些。
小结
- 对于随机排序的无重复元素的数组,插入排序和选择排序的运行时间是平方级别的,两者之比应该是一个较小的常数。
- 对于逆序数组,选择排序的速度要快于插入排序。
- 对于具有大量相同元素的数组、近乎有序的数组、各元素都相等的数组和完全有序的数组,插入排序的速度都快于选择排序。
- 插入排序对于近乎有序的数组十分高效,也很适合小规模数组。
如有写的不足的,请见谅,请大家多多指教。
我的个人博客网站: www.xilikeli.cn 欢迎大家来访(#.#)