排序算法模板
public class Example {
public static void sort(Comparable[] a){
//此处为排序算法
//默认由小到大排序
}
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 isSored(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){
Integer[] a = {1, 4, 2, 5, 2, 8};
sort(a);
assert isSored(a);
show(a);
}
}
选择排序
选择排序不断选择剩余元素之中的最小者,并交换它到对应的位置。它是第几小的元素,就把它交换到数组的第几个位置。
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(N-1)/2次比较,N次交换。
插入排序
插入排序将每个元素插入到左侧已经有序的元素中。
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(N-1)/2次比较,N(N-1)/2次交换。
平均情况下(随机排列),需要~N^2/4次比较,~N^2/4次交换。
最好情况下(已经有序),需要N-1次比较,0次交换。
希尔排序
希尔排序使数组中任意间隔为h(步长)的元素都是有序的,如果h很大,就能将元素移动到很远的地方,为实现更小的h有序创造方便。
排序之初,各个子数组都很短,排序之后,子数组都是部分有序的。这两种情况都很适合插入排序。
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 = h/3;
}
}
//也可以这样写
public static void sort(Comparable[] a){
int N = a.length;
int h = N;
while (h > 1){
h = h / 3 + 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);
}
}
}
}
步长的选择是希尔排序中的重要部分:
- 初始步长要小于数组长度。
- 最终步长要为1,这时就变为了插入排序,确保数组一定会被完全排序。
步长序列不同,希尔排序的效率也不同,但是效率都优于N^2。
归并排序
将数组分成两半排序,然后将结果归并起来。采用了分治思想。
实现方式有很多,可以采取自顶向下(递归)的方式排序,也可以采取自底向上(非递归)的方式排序。可以使用额外空间来辅助归并,也可以不使用额外空间(局部利用插入排序)来归并。
//自顶向下
public static void sortUpTodDown(Comparable[] a){
aux = new Comparable[a.length];
sortUpToDown(a, 0, a.length - 1);
}
public static void sortUpToDown(Comparable[] a, int lo, int hi){
if (hi <= lo){
return;
}
int mid = lo + (hi - lo)/2;
sortUpToDown(a, lo, mid);
sortUpToDown(a, mid + 1, hi);
merge(a, lo, mid, hi);
}
//自底向上
public static void sortDownToUp(Comparable[] a){
int N = a.length;
aux = new Comparable[N];
//sz是子数组大小
for (int sz = 1; sz < N; sz *= 2){
for (int lo = 0; lo < N - sz; lo += sz * 2){
merge(a, lo, lo + sz - 1, Math.min(lo + sz * 2 - 1, N -1));
}
}
}
//归并(使用额外空间)
//辅助数组
private static Comparable[] aux;
public static void merge(Comparable[] a, int lo, int mid, int hi){
//优化,证明数组已经有序
if (less(a[mid], a[mid + 1])){
return;
}
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[i], aux[j])){
a[k] = aux[i++];
}
else {
a[k] = aux[j++];
}
}
}
//归并(不使用额外空间,从mid+1元素开始使用插入排序,将第二个数组的元素不断插入到第一个数组中)
public static void merge2(Comparable[] a, int lo, int mid, int hi){
if (less(a[mid], a[mid + 1])){
return;
}
for (int i = mid + 1;i <= hi; i++){
for (int j = i; j > 0 && less(a[j], a[j - 1]); j--){
exch(a, j, j - 1);
}
}
}
归并排序的效率,与实现方式有一定的关系。
总运行时间为划分时间+归并时间。
两种划分方式,时间复杂度都为N,并且都需要进行logN次归并。
两种归并方式,时间复杂度都为N,但优化后的最好情况下时间复杂度为1。
总时间复杂度为N+NlogN(视为NlogN)
最坏情况下(所有元素逆序),时间复杂度NlogN
平均情况下(随机排列),时间复杂度NlogN(未优化),N~NlogN(优化)
最好情况下(已经有序),时间复杂度NlogN(未优化),N(优化)
快速排序
快速排序和归并排序时互补的:
归并排序先使子数组(单个元素开始)有序,不断两两归并变为更大的有序子数组,最终使整个数组有序。
快速排序使整个数组进行切分,保证最低程度的有序(以切分点为界,前一部分都小于后一部分),再对切分得到的两个子数组继续切分,使有序性进一步加大。最终当不可再切分时,完全有序。整个过程相当于不断改变元素位置使之始终处在正确的区域,当划定的区域只能容纳一个元素时,每个元素都在正确的位置,整体有序。
都采用了分治的思想。
public static void sort(Comparable[] a){
sort3part(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi){
if (hi <= lo){
return;
}
int index = partition(a, lo, hi);
sort(a, lo, index - 1);
sort(a, index + 1, hi);
}
//切分点左侧的元素都小于等于切分元素,右侧都大于切分元素
private static int partition(Comparable[] a, int lo, int hi) {
Comparable v = a[lo];
int i = lo, j = hi;
while (true) {
//从左向右扫描,直到找到一个大于v的元素
while (i <= hi && !less(v, a[i])) i++;
//从右向左扫描,直到找到一个小于等于v的元素
while (j >= lo && less(v, a[j])) j--;
if (i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
快速排序的效率与切分点的选择有很大关系
最坏情况下(每次切分后的两子数组之一为空),时间复杂度N^2
平均情况下(随机数据),时间复杂度NlogN
最好情况下(切分点每次都正好将数组对半分),时间复杂度NlogN
三向切分快速排序
当有大量重复数据时,可以使用三向切分的快速排序提高效率。
维护一个指针lt,使得a[lo..lt-1]中的元素都小于v
维护一个指针gt,使得a[gt+1..hi]中的元素都大于v
维护一个指针i,使得a[lt..i-1]中的元素都等于v,a[i..gt]中的元素未确定
private static void sort3part(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){
if (less(a[i], v)) exch(a, lt++, i++);
else if (less(v, a[i])) exch(a, i, gt--);
else i++;
}
sort3part(a, lo, lt - 1);
sort3part(a, gt + 1, hi);
}
当有大量重复数据时,这种方法效率很高。
但是若数据重复率低时,这种方法比标准的快速排序多了许多不必要的交换操作,效率要低不少。