选择排序
不断选择剩余数组中的最小值----找到未排序部分最小的元素,然后将它和未排序部分首个元素交换位置。
public class Selection {
public static void sort(Comparable[] a) {
int N = a.length;
for(int i = 0; i < N; i++) {
int min = i; //最小元素的索引
for(int j = i+1; j < N; j++)
if(less(a[j], a[min])) min = j; //寻找剩余部分的最小值
exch(a, i, min); //交换
}
}
}
对于长度为N的数组,选择排序需要大约N2/2次比较和N次交换。(N*N轨迹表)
特点: 运行时间与输入数组有序程度无关;数据移动最少;稳定排序。
插入排序
将待排序元素插入有序部分的适当位置。
public class Insertion {
public static void sort(Comparable[] a) {
int N = a.length;
for(int i = 1; i < N; i++) {
for(int j = i; j > 0 && less(a[j], a[j-1]); j--)
exch(a, j, j-1); //右移
}
}
}
对于长度为N且主键不重复的数组,平均情况下插入排序需要N2/4次比较和N2/4次交换;最坏情况下需要N2/2次比较和N2/2次交换;最好情况下需要N-1次比较和0次交换(完全有序)。
插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数组的大小再减一。
冒泡排序
从数组末尾开始依次比较相邻元素,大小关系相反则交换位置。
public class bubble {
public static void sort(Comparable[] a) {
int N = a.length;
boolean flag = true;
for(int i = 0; flag; i++) {
flag = false;
for(int j = N-1; j > i; j--) {
if(less(a[j], a[j-1])) {
exch(a, j, j-1);
flag = true;
}
}
}
}
}
对于长度为N的数组,冒泡排序在最坏情况下需要进行(N²-N)/2 次比较运算和(N²-N)/2 次交换运算。
算法复杂度O(n2);稳定排序
希尔排序
使数组中任意间隔为h的元素都是有序的。
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-h]); j -= h)
exch(a, j, j-h);
}
h /= 3;
}
}
}
当h满足序列1/2(3k-1)时,算法的时间复杂度基本维持在O(n1.25)。除此之外,只要使用终值为1的递减数列,基本都能高效地完成排序。但是,如果遇到2的幂指数等h=1之前几乎不需要排序的数列,希尔排序的效率会大大降低。
归并排序
采用分治的思想,自顶向下/自底向上将划分得到的数组排序并层层合并。
import java.util.Scanner;
public class Merge {
private static Comparable[] aux;
public static void sort(Comparable[] a) {
aux = new Comparable[a.length];
sort(a, 0, a.length - 1);
}
//自顶向下的归并排序
private static void sort(Comparable[] a, int lo, int hi) {
if(hi <= lo) return;
int mid = lo + (hi - lo)/2;
sort(a, lo, mid);
sort(a, mid+1, hi);
if(less(a[mid+1], a[mid]))
merge(a, lo, mid, hi); //数组有序时会跳过merge操作
}
private 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(int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
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) {
Scanner input = new Scanner(System.in);
String str = input.nextLine();
String[] a = str.split(" ");
sort(a);
assert isSorted(a);
show(a);
}
}
//自底向上
public static void sort(Comparable[] a) {
int N = a.length;
aux = new Comparable[a.length];
for(int sz = 1; sz < N; sz *= 2)
for(int lo = 0; lo < N-sz; lo += 2*sz)
merge(a, lo, lo+sz-1, Math.min(lo+2*sz-1, N-1));
}
对于长度为N的任意数组,归并排序需要1/2NlgN~NlgN次比较,最多需要访问数组6NlgN次;稳定排序。
自底向上的归并排序比较适合用链表组织的数据。
快速排序
选定切分元素,将数组分为小于该元素(左侧)、大于该元素(右侧)两部分…不断循环直至排序完成。
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
public class Quick {
public static void sort(Comparable[] a) {
Collections.shuffle(Arrays.asList(a)); //消除对输入的依赖
sort(a, 0, a.length - 1);
}
private 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;
} //寻找左侧大于v的下标
while(less(v, a[--j])){} //寻找右侧小于v的下标
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(int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
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) {
Scanner input = new Scanner(System.in);
String str = input.nextLine();
String[] a = str.split(" ");
sort(a);
assert isSorted(a);
show(a);
}
}
对于长度为N的无重复数组,快速排序平均需要2NlnN次比较(以及1/6的交换);最多需要N2/2次比较;不稳定排序。
内循环中数据移动少、切分不平衡时较为低效(随机打乱数组可以有效预防)
算法改进
- 对小数组使用插入排序
- 三取样切分:使用子数组的中位数作为切分元素
- 三向切分:划分得到小于、等于、大于三个部分
private static void sort(Comparable[] a, int lo, int hi) {
if(hi <= lo) return;
int lt = lo, i = lo+1, gt = hi;
Comparable v = a[lo];
while(i <= gt) {
int cmp = a[i].compareTo(v);
if(cmp < 0) exch(a, lt++, i++);
else if(cmp > 0) exch(a, i, gt--);
else i++;
} //a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]
sort(a, lo, lt-1);
sort(a, gt+1, hi);
}
对于大小为N的数组,三向切分的快速排序需要(2ln2)NH次比较(其中H为由主键值出现频率定义的香农信息量)。
堆排序
循环构造大根堆,然后利用下沉排序销毁堆。
import java.util.Scanner;
public class Heapsort {
public static void sort(Comparable[] a) {
int N = a.length - 1;
for(int k = N/2; k > -1; k--) {
sink(a, k, N);
}
while(N > 0) {
exch(a, 0, N--);
sink(a,0, N);
}
}
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 sink(Comparable[] a, int k, int N) {
while(2*k <= N) {
int j = 2*k;
if(j < N && less(a[j], a[j+1]))
j++;
if(!less(a[k], a[j]))
break;
exch(a, k, j);
k = j;
}
}
private static void show(Comparable[] a) {
for(int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
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) {
Scanner input = new Scanner(System.in);
String str = input.nextLine();
String[] a = str.split(" ");
sort(a);
assert isSorted(a);
show(a);
}
}
平均时间复杂度为 Ο(nlogn)
计数排序
设输入数组为A(0≤Ai≤K),输出数组为B。维护一个辅助数组C[k]:1. 遍历Ai,利用C[Ai]记录该元素出现次数;2. 遍历Cj,累加得到小于等于j的元素个数。然后倒序遍历Ai,根据C[k]得到输出数组B。
import java.util.Scanner;
public class countSort {
public static String[] b;
public static final int K = 10000;
private static void sort(String[] a) {
int[] c = new int[VMAX+1];
b = new String[a.length];
for(String i : a){
c[Integer.parseInt(i)]++;
}
for(int i = 1; i < K+1; i++){
c[i] += c[i-1];
}
for(int j = a.length-1; j > -1; j--){
b[c[Integer.parseInt(a[j])]-1] = a[j];
c[Integer.parseInt(a[j])]--;
} //倒序保证排序的稳定性
}
private static boolean less(String v, String w) {
return v.compareTo(w) < 0;
}
private static void show(String[] a) {
for (String s : a) {
System.out.print(s + " ");
}
System.out.println();
}
public static boolean isSorted(String[] 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) {
Scanner input = new Scanner(System.in);
String str = input.nextLine();
String[] a = str.split(" ");
sort(a);
assert isSorted(b);
show(b);
}
}
时间复杂度为O(n+K)
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
基数排序
根据键值的每位数字来分配桶