希尔排序
插入排序在对几乎已经排好序的数组进行排序时,效率高,可以达到线性排序的效率。但对于大规模乱序数组(特别是逆序数组),插入排序很慢。因为他只会交换相邻的元素,元素只能一点点从数组一端移动到另一端。
希尔排序的思想:
交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。使数组中任意间隔为h的元素都是有序的。其中h为任意以1结尾的整数序列。
增量序列h:
希尔排序的执行时间依赖于增量序列,好的增量序列的共同特征:
1. 最后一个增量必须为1;
2. 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
排序过程:
程序代码:
public class ShellSort<T extends Comparable<T>> implements Sort<T> {
//核心算法,增量序列 1 4 13 ....(3*h+1)
public void sort(T[] a) {
int N = a.length;
int h = 1;
while(h < N/3) h = 3*h + 1;
while(h > 0 ) {
for(int i=h; i<N; i++) {
for(int j=i; j>=h && less(a[j], a[j-h]); j -= h) {
swap(a, j, j-h);
}
}
h = h/3;
}
}
//比较两个元素大小
public boolean less(T a,T a2) {
return a.compareTo(a2)<0;
}
//交换两个元素
public void swap(T[] a,int i,int j) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public boolean isSorted(T[] a) {
int N = a.length;
for(int i=1; i<N; i++) {
if(less(a[i],a[i-1]))
return false;
}
return true;
}
//测试
public static void main(String[] args) {
Integer[] a = {3,8,2,5,9,1,4,6,0,7};
ShellSort<Integer> ss = new ShellSort<Integer>();
ss.sort(a);
System.out.println(ss.isSorted(a));
}
}
分析:
希尔排序的运行时间最坏情况达不到平方级别
希尔排序是不稳定的排序,代码量小,原地排序。
- 希尔排序优于插入排序,但没有快速排序快(快速排序最坏情况下除外)。
测试
代码如下:
public class Test {
public static final int NUM = 10000;
public static <T> double testSort(T[] data, Sort<T> sort) {
StopWatch timer = new StopWatch();
sort.sort(data);
return timer.elapsedTime();
}
public static void main(String[] args) {
Double[] data1 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data1[i] = Math.random();
}
Double[] data2 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data2[i] = data1[i];
}
Double[] data3 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data3[i] = data1[i];
}
InsertionSort<Double> its = new InsertionSort<Double>();
SelectionSort<Double> sts = new SelectionSort<Double>();
ShellSort<Double> sls = new ShellSort<Double>();
double time1 = Test.testSort(data1,sts);
double time2 = Test.testSort(data2,its);
double time3 = Test.testSort(data3,sls);
System.out.println("SelectionSort---->" + time1);
System.out.println("InsertionSort---->" + time2);
System.out.println("ShellSort---->" + time3);
}
}
class StopWatch {
public final long start;
public StopWatch() {
start = System.currentTimeMillis();
}
public double elapsedTime() {
long now = System.currentTimeMillis();
return (now-start)/1000.0;
}
}
结果如下:
希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。
归并排序
排序思想:
归并排序是基于归并操作实现的,即将两个有序的数组归并成一个更大的有序数组。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
归并排序可分为
- 自顶向下的归并排序
- 自底向上的归并排序
自顶向下的归并排序
自顶向下的归并排序过程:
代码如下:
/**
1. 自顶向下的归并排序
*/
public class MergeSort<T extends Comparable<T>> implements Sort<T> {
private T[] array; //辅助数组
public void sort(T[] a) {
array = (T[]) new Comparable[a.length];
mergeSort(a, 0, a.length - 1);
}
//核心算法
public void mergeSort(T[] a, int down, int up) {
if (up <= down) return; //结束条件
int mid = (up - down) / 2 + down;
mergeSort(a, down, mid); //左半边排序
mergeSort(a, mid + 1, up); //右半边排序
merge(a, down, mid, up);
}
//一个数组左右半边分别有序,归并
public void merge(T[] a, int down, int mid, int up) {
int i = down, j = mid + 1;
//复制数组中元素
for (int k = down; k <= up; k++) {
array[k] = a[k];
}
for (int k = down; k <= up; k++) {
if (i > mid) a[k] = array[j++]; //左半边用尽,取右半边元素
else if (j > up) a[k] = array[i++];
else if (less(array[i], array[j])) //左半边元素比右半边小
a[k] = array[i++];
else a[k] = array[j++];
}
}
//比较两个元素大小
public boolean less(T a, T a2) {
return a.compareTo(a2) < 0;
}
public boolean isSorted(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
if (less(a[i], a[i - 1]))
return false;
}
return true;
}
//测试
public static void main(String[] args) {
Double[] a = new Double[100];
for (int i = 0; i < 100; i++) {
a[i] = Math.random();
}
MergeSort<Double> ss = new MergeSort<Double>();
ss.sort(a);
System.out.println(ss.isSorted(a));
}
}
分析:
比较次数0.5*NlogN到NlogN,访问数组最多6NlogN
时间复杂度O(NlogN),空间复杂度O(N)
优点:所需时间短,速度快。 缺点:所需空间和数组大小成正比
自底向上的归并排序
排序思想:
先两两归并(把每个元素当成一个长度为1的数组),在四四归并,然后八八归并,一直下去。每轮归并中最后一次归并的第二个数组可能比第一个小(注意不要越界)。
代码如下:
public class MergeUpSort<T extends Comparable<T>> implements Sort<T> {
private T[] array; //辅助数组
public void sort(T[] a) {
array = (T[]) new Comparable[a.length];
mergeSort(a);
}
//核心算法
public void mergeSort(T[] a) {
int N = a.length;
for (int i = 1; i < N; i = 2 * i) {
for (int j = 0; j < N - i; j += 2 * i)
merge(a, j, j + i - 1, Math.min(j + 2 * i - 1, N - 1));
}
}
//一个数组左右半边分别有序,归并
public void merge(T[] a, int down, int mid, int up) {
int i = down, j = mid + 1;
//复制数组中元素
for (int k = down; k <= up; k++) {
array[k] = a[k];
}
for (int k = down; k <= up; k++) {
if (i > mid) a[k] = array[j++]; //左半边用尽,取右半边元素
else if (j > up) a[k] = array[i++];
else if (less(array[i], array[j])) //左半边元素比右半边小
a[k] = array[i++];
else a[k] = array[j++];
}
}
//比较两个元素大小
public boolean less(T a, T a2) {
return a.compareTo(a2) < 0;
}
public boolean isSorted(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
if (less(a[i], a[i - 1]))
return false;
}
return true;
}
//测试
public static void main(String[] args) {
Double[] a = new Double[1050];
for (int i = 0; i < 1050; i++) {
a[i] = Math.random();
}
MergeUpSort<Double> ss = new MergeUpSort<Double>();
ss.sort(a);
System.out.println(ss.isSorted(a));
}
}
改进
对于自顶向下的归并排序:
- 归并排序在处理小规模问题时,由于方法的调用过于频繁,会产生过多的额外开销。插入排序在处理小数组上比归并排序更快。在用归并排序处理大规模数据时,,使用插入排序来处理小规模的子数组,一般可使归并排序的运行时间缩短10%-15%
- 添加一个判断,如果a[mid]小于a[mid+1],则数组已经有序,不需要进行归并操作。这样可大大减小有序子数组的运行时间。
- 通过在递归调用的每个层次交换输入数组和辅助数组的角色,节省元素复制到辅助数组中的时间(空间不行)。即在递归中,数据从输入数组排序到辅助数组和从辅助数组排序到输入数组交替使用。
改进后代码如下:
package com.gain.sort;
/**
* 改进自顶向下的归并排序
* 1. 对小规模数组使用插入排序
* 2. 加入数组是否有序的判断,减少归并次数
* 3. 通过在递归中交换参数,避免数组复制
* Created by gain on 2016/1/18.
*/
public class MergeInsSort<T extends Comparable<T>> implements Sort<T> {
public static final int CUTOFF = 10; //插入排序处理数组长度
private T[] array;
public void sort(T[] a) {
array = a.clone();
mergeSort(array, a, 0, a.length - 1);
}
//核心算法, 对dst进行排序
public void mergeSort(T[] src, T[] dst, int down, int up) {
//if (up <= down) return; //结束条件
//改进,小规模用插入排序
if (up - down <= CUTOFF) {
insertionSort(dst, down, up);
return;
}
int mid = (up - down) / 2 + down;
mergeSort(dst, src, down, mid); //左半边排序,交换输入数组和辅助数组角色
mergeSort(dst, src, mid + 1, up); //右半边排序,结果:src中有序
/*if(less(src[mid], src[mid + 1])) {
for(int i=down; i<=up;i++) {
dst[i] = src[i];
}
return;
}*/
//比上面循环更快
if(less(src[mid], src[mid + 1])) {
System.arraycopy(src, down, dst, down, up-down+1);
return;
}
merge(src, dst, down, mid, up);
}
//一个数组左右半边分别有序,src归并到dst
public void merge(T[] src, T[] dst, int down, int mid, int up) {
assert isSorted(src, down, mid); //断言,左右半边均有序
assert isSorted(src, mid+1,up);
int i = down, j = mid + 1;
for (int k = down; k <= up; k++) {
if (i > mid) dst[k] = src[j++]; //左半边用尽,取右半边元素
else if (j > up) dst[k] = src[i++];
else if (less(src[i], src[j])) //左半边元素比右半边小
dst[k] = src[i++];
else dst[k] = src[j++];
}
assert isSorted(dst, down, up);
}
public void insertionSort(T[] a, int down, int up) {
for (int i = down+1; i <= up; i++) {
for (int j = i; j >= down+1 && less(a[j], a[j-1]); j--) {
swap(a, j, j-1);
}
}
}
/*******************************************************************************/
//交换两个元素
public void swap(T[] a,int i,int j) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//比较两个元素大小
public boolean less(T a, T a2) {
return a.compareTo(a2) < 0;
}
public boolean isSorted(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
if (less(a[i], a[i - 1]))
return false;
}
return true;
}
public boolean isSorted(T[] a, int down, int up) {
for (int i = down+1; i <= up; i++) {
if (less(a[i], a[i - 1]))
return false;
}
return true;
}
//测试
public static void main(String[] args) {
Double[] a = new Double[1500];
for (int i = 0; i < 1500; i++) {
a[i] = Math.random();
}
MergeInsSort<Double> mis = new MergeInsSort<Double>();
mis.sort(a);
System.out.println(mis.isSorted(a));
System.out.println(mis.isSorted(mis.array));
}
}
测试
代码如下:
public class Test {
//测试数据个数
public static final int NUM = 1000000;
public static <T> double testSort(T[] data, Sort<T> sort) {
StopWatch timer = new StopWatch();
sort.sort(data);
return timer.elapsedTime();
}
public static void main(String[] args) {
Double[] data1 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data1[i] = Math.random();
}
Double[] data2 = data1.clone();
Double[] data3 = data1.clone();
Double[] data4 = data1.clone();
//InsertionSort<Double> its = new InsertionSort<Double>();
//SelectionSort<Double> sts = new SelectionSort<Double>();
ShellSort<Double> sls = new ShellSort<Double>();
MergeSort<Double> mgs = new MergeSort<Double>();
MergeInsSort<Double> mis = new MergeInsSort<Double>();
MergeUpSort<Double> mus = new MergeUpSort<Double>();
System.out.println("the number of data: " + Test.NUM);
System.out.println("MergeInsSort---->" + Test.testSort(data1,mis));
System.out.println("MergeUpSort---->" + Test.testSort(data2,mus));
System.out.println("MergeSort---->" + Test.testSort(data3,mgs));
System.out.println("ShellSort---->" + Test.testSort(data4,sls));
}
}
class StopWatch {
public final long start;
public StopWatch() {
start = System.currentTimeMillis();
}
public double elapsedTime() {
long now = System.currentTimeMillis();
return (now-start)/1000.0;
}
}
结果: