在线可视化算法演示:VisuAlgo - 排序
对插入排序、选择排序、希尔排序、归并排序、快速排序这五种排序算法的总结
选择排序
- 选定数组第一个元素为最小,遍历整个数组,如果有比第一个元素小且是最小的,将二者交换
- 选定第二个元素为最小,对余下的数组遍历,如果有更小的,将其与第二个元素交换
- 重复2,直至到达最后一个元素
Java 代码实现:
/**
* 选择排序
*
* @author: wjl
* @date: 2021/9/2 20:28
* @version: v1.0
*/
public class Selection {
public static void sort(Comparable[] a) {
int N = a.length;
sort(a, 0, N - 1);
}
public static void sort(Comparable[] a, int lo, int hi) {
for (int i = lo; i < hi + 1; i++) {
int min = i;
for (int j = i + 1; j < hi + 1; j++) {
if (less(a[j], a[min])) {
min = j;
}
}
exchange(a, i, min);
}
}
private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }
private static void exchange(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (Comparable comparable : a) {
System.out.print(comparable + " ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a[i], a[i - 1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
sort(args);
assert isSorted(args);
show(args);
}
}
插入排序
- 将第一个元素标记为已排序,从第二个元素开始遍历,如果所选中的元素小于第一个元素,将其移动到第一个元素位置,第一个元素到该元素前面的都向后移动一格
- 标记前两个元素为已排序,遍历未排序的元素,如果选中的元素比已排序的元素中的最大的小,则逆序遍历已排序的元素,若比已排序的元素小,将其插入到该元素前面
- 重复2,直至所有元素标记为已排序
Java 代码实现:
/**
* 插入排序
*
* @author: wjl
* @date: 2021/9/2 20:44
* @version: v1.0
*/
public class Insertion {
public static void sort(Comparable[] a) {
int N = a.length;
sort(a, 0, N - 1);
}
public static void sort(Comparable[] a, int lo, int hi) {
for (int i = lo + 1; i < hi + 1; i++) {
for (int j = i; j > lo && less(a[j], a[j - 1]); j--) {
exchange(a, j, j - 1);
}
}
}
private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }
private static void exchange(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (Comparable comparable : a) {
System.out.print(comparable + " ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a[i], a[i - 1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
sort(args);
assert isSorted(args);
show(args);
}
}
希尔排序
希尔排序是基于插入排序的一种算法
希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的,这样的数组被称为 h 有序数组。
实现希尔排序的一种方法是对于每个 h ,用插入排序将 h 个子数组独立地排序。
Java 代码实现:
/**
* 希尔排序
*
* @author: wjl
* @date: 2021/9/2 21:10
* @version: v1.0
*/
public class Shell {
public static void sort(Comparable[] a) {
int N = a.length;
int h = 1;
while (h < N / 3) { h = 3 * h + 1; }
while (h >= 1) {
for (int i = h; i < N; i++) {
for (int j = i; j >= h && less(a[j], a[j - 1]); j -= h) {
exchange(a, j, j - h);
}
}
h /= 3;
}
}
private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }
private static void exchange(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (Comparable comparable : a) {
System.out.print(comparable + " ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a[i], a[i - 1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
sort(args);
assert isSorted(args);
show(args);
}
}
归并排序
将一个数组先(递归地)将它分成两半分别排序,然后再将结果归并起来
归并算法:
- 对于已经排序好的一个数组的两半,复制该数组
- 分别从两个子数组的第一个元素开始索引,如果左大于右,将复制数组中的右元素赋值给做左元素所在位置,右数组索引向右移动一格
- 重复2,直至全部排序完成
-
对于元素较少的数组,多次递归调用反而会降低效率,一般在数组长度小于15时,使用插入排序
-
在归并操作时,因为两边的元素都是有序的,若左数组最后一个元素小于右数组第一个元素,则可以直接跳过归并,该子数组排序完成
/**
* 归并排序
*
* @author: wjl
* @date: 2021/9/3 19:17
* @version: v1.0
*/
public class Merge {
private static Comparable[] aux;
private static void sort(Comparable[] a) {
aux = new Comparable[a.length];
sort(a, 0, a.length - 1);
}
// 对于小规模(长度小于15),使用插入排序
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) return;
if (hi - lo <= 14) {
Insertion.sort(a, lo, hi);
} else {
int mid = lo + (hi - lo) / 2;
sort(a, lo, mid);
sort(a, mid + 1, hi);
// 如果 a[mid] <= a[mid+1],就跳过 merge()
if (!less(a[mid], a[mid + 1])) {
merge(a, lo, mid, hi);
}
}
}
public static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if (less(aux[j], aux[i])) a[k] = aux[j++];
else a[k] = aux[i++];
}
}
private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }
private static void show(Comparable[] a) {
for (Comparable comparable : a) {
System.out.print(comparable + " ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a[i], a[i - 1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
sort(args);
assert isSorted(args);
show(args);
}
}
快速排序
先打乱元素,再(递归地)随机选取一个元素作为中轴,将数组切分为两部分,将不大于中轴地元素移动到中轴左侧,不小于中轴的元素移动到中轴右侧,对左右部分分别排序
切分算法:
- 一般选取第一个元素为中轴
- 使用两个指针分别从数组两边向中间扫描,左边扫描到的元素若比中轴大,且右边扫描到的元素比中轴小,则将两者交换,指针移向下一位
- 重复2,直至两指针相遇
/**
* 快速排序
*
* @author: wjl
* @date: 2021/9/3 21:06
* @version: v1.0
*/
public class Quick {
public static void sort(Comparable[] a) {
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
public static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) { return; }
int j = partition(a, lo, hi);
sort(a, lo, j - 1);
sort(a, j + 1, hi);
}
private static int partition(Comparable[] a, int lo, int hi) {
int i = lo, j = hi + 1;
Comparable v = a[lo];
while (true) {
while (less(a[++i], v)) {
if (i == hi) break;
}
while (less(v, a[--j])) {
if (j == lo) break;
}
if (i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (Comparable comparable : a) {
System.out.print(comparable + " ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a[i], a[i - 1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
sort(args);
assert isSorted(args);
show(args);
}
}
排序比较类
为了比较这五种排序算法的速度,写一个比较类,比较任意两种算法的速度
/**
* 排序速度比较类
*
* @author: wjl
* @date: 2021/9/2 20:57
* @version: v1.0
*/
public class SortCompare {
/**
* 计时
*
* @param alg 传入的算法对应的字符串
* @param a 待排序的数组
* @return 运行时间
*/
public static double time(String alg, Comparable[] a) {
long start;
start = System.currentTimeMillis();
if (alg.equals("Selection")) Selection.sort(a);
if (alg.equals("Insertion")) Insertion.sort(a);
if (alg.equals("Shell")) Shell.sort(a);
if (alg.equals("Merge")) Merge.sort(a);
if (alg.equals("Quick")) Quick.sort(a);
long now = System.currentTimeMillis();
return (now - start) / 1000.0;
}
/**
* 生成随机数并排序
*
* @param alg 传入的算法对应的字符串
* @param N 随机数组长度
* @param T 重复统计次数
* @return 平均运行时间
*/
public static double timeRandomInput(String alg, int N, int T) {
double total = 0.0;
Double[] a = new Double[N];
for (int t = 0; t < T; t++) {
for (int i = 0; i < N; i++) {
a[i] = Math.random();
}
total += time(alg, a);
}
return total;
}
public static void main(String[] args) {
String alg1 = "Selection"; /* 第一种算法 */
String alg2 = "Insertion"; /* 第二种算法 */
String alg3 = "Shell"; /* 第三种算法 */
String alg4 = "Merge"; /* 第四种算法 */
String alg5 = "Quick"; /* 第五种算法 */
int N = Integer.parseInt(args[0]); /* 数组大小 */
int T = Integer.parseInt(args[1]); /* 重复次数 */
double t1 = timeRandomInput(alg1, N, T);
double t2 = timeRandomInput(alg2, N, T);
double t3 = timeRandomInput(alg3, N, T);
double t4 = timeRandomInput(alg4, N, T);
double t5 = timeRandomInput(alg5, N, T);
System.out.printf("%s : %3.5fs\n", alg1, t1);
System.out.printf("%s : %3.5fs\n", alg2, t2);
System.out.printf("%s : %3.5fs\n", alg3, t3);
System.out.printf("%s : %3.5fs\n", alg4, t4);
System.out.printf("%s : %3.5fs\n", alg5, t5);
}
}
测试不同规模的数组五种算法的速度:
$ java SortCompare 100 100
Selection : 0.01000s
Insertion : 0.01200s
Shell : 0.00800s
Merge : 0.00200s
Quick : 0.00700s
$ java SortCompare 500 100
Selection : 0.05000s
Insertion : 0.04500s
Shell : 0.02600s
Merge : 0.01100s
Quick : 0.01500s
$ java SortCompare 1000 100
Selection : 0.12200s
Insertion : 0.15400s
Shell : 0.04700s
Merge : 0.02400s
Quick : 0.04900s
$ java SortCompare 5000 100
Selection : 2.16100s
Insertion : 3.64100s
Shell : 0.37800s
Merge : 0.32800s
Quick : 0.08300s
$ java SortCompare 10000 100
Selection : 8.92300s
Insertion : 15.15300s
Shell : 1.13300s
Merge : 0.30400s
Quick : 0.22600s
五种算法的比较
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 不稳定 |
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n s ) O(ns) O(ns) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 不稳定 |
归并排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n \log n) O(nlogn) | O ( n log n ) O(n \log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
快速排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | 不稳定 |
以上。