冒泡排序
冒泡排序的操作流程如下:从0到N-1遍历一遍数组,a[0]与a[1]比较,如果a[0]比a[1]大,a[0]与a[1]交换位置,然后a[1]与a[2]比较,如果a[1]比a[2]大,a[1]与a[2]交换位置,直到a[N-1]最大。
再从剩下的0到N-2遍历一遍数组,直到a[N-2]最大,再从剩下的0到N-3遍历一遍数组,直到a[N-3]最大,这样一直进行下去,从N-1到0就按降序排列了。
代码如下
public static void sort(Comparable[] a) {
int n = a.length;
for(int i = 0; i < n-1; i++) {
for(int j = 1; j < n-i; j++) {
if (less(a[j], a[j-1])) exch(a, j , j-1);
}
assert isSorted(a, n-1-i, n-1);
}
assert isSorted(a);
}
两个for循环所以时间复杂度O(),具体点i=0时,if比较n-1次,i=1时,if比较n-2次...i=n-3时,if比较2次,i=n-2时,if比较1次,
所以总次数1+2+3+4+5+6+7+8+9+...+.n=(1+n)n/2=O()
例如数组{7,3,1,78,2}的运行轨迹如下:
i=0时,if比较4次
3,7,1,78,2
3,1,7,78,2
3,1,7,78,2
3,1,7,2,78
i=1时,if比较3次
1,3,7,2,78
1,3,7,2,78
1,3,2,7,78
i=2时,if比较2次
1,3,2,7,78
1,2,3,7,78
i=3时,if比较1次
1,2,3,7,78
选择排序
选择排序的操作流程如下:从0到N-1遍历一遍数组,两两比较找出最小元素min,a[0]与a[min]交换位置,
再从剩下的1到N-1遍历一遍数组,找出最小元素min,a[1]与a[min]交换位置,
再从剩下的2到N-1遍历一遍数组,找出最小元素min,a[2]与a[min]交换位置,
这样一直进行下去,从0到N-1就有序了。
代码如下
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;
}
if (min != i) exch(a, i , min);
assert isSorted(a, 0, i);
}
assert isSorted(a);
}
两个for循环,所以时间复杂度O()
例如数组{7,3,1,78,2}的运行轨迹如下:
i=0时,找到最小元素1,1与7交换位置
1,3,7,78,2
i=1时,找到最小元素2,2与3交换位置
1,2,7,78,3
i=2时,找到最小元素3,3与7交换位置
1,2,3,78,7
i=3时,找到最小元素7,7与78交换位置
1,2,3,7,78
冒泡排序会在每次比较后就交换位置,而选择排序是在所有元素比较完找到最小元素位置后才交换位置,所以选择排序比冒泡排序交换元素的次数少一些,运行比冒泡排序快一些,通过运行SortCompare可以看出选择排序比冒泡排序快1~2倍。
# java SortCompare Selection Bubble 3000 100
For 3000 random Doubles
Selection is 1.8 times faster than Bubble
插入排序
插入排序的操作流程如下:
先看a[0]与a[1],如果a[1]比a[0]小,a[1]与a[0]交换位置,再看a[0]/a[1]/a[2],a[2]与a[1]比较,如果a[2]比a[1]小,a[2]与a[1]交换位置,否则停止比较,因为a[0],a[1]已经有序了,a[2]比a[1]大自然也比a[0]大
代码如下
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);
}
assert isSorted(a, 0, i);
}
assert isSorted(a);
}
两个for循环所以时间复杂度O(),但是如果数组已经部分有序,很多时候第2个for循环会马上结束,这样算法会快很多达到O(N)级别,所以插入排序适合本来就部分有序的数组。
# java SortCompare Insertion Selection 3000 100
For 3000 random Doubles
Insertion is 1.0 times faster than Selection
# java SortCompare Insertion Selection 3000 100 sorted
For 3000 sorted Doubles
Insertion is 73.4 times faster than Selection
通过运行SortCompare比较插入和选择排序,对于随机数组两者速度差不多,但对于有序的数组,插入排序是选择排序的73倍
例如数组{7,3,1,78,2}的运行轨迹如下:
i=1时,前2个元素比较
3,7,1,78,2
i=2时,前3个元素比较
3,1,7,78,2 //1插入到7前面
1,3,7,78,2 //1插入到3前面
i=3时,前4个元素比较
1,3,7,78,2 //78比7大,停止第2个for循环
i=4时,前5个元素比较
1,3,7,2,78 //2插入到78前面
1,3,2,7,78 //2插入到7前面
1,2,3,7,78 //2插入到3前面
1,2,3,7,78 //2比1大,停止第2个for循环
希尔排序
希尔排序对插入排序进行了改进,对于随机数组效率比插入排序要高,基本思路如下:
取一个小于数组长度n的增量h1,把数组a分成h1个子数组,任意相隔h1的元素在一个数组里,对这h1个数组分别运用插入排序,使数组a中任意间隔h1的元素是有序的。
再取一个小于h1的增量h2进行上面同样的操作,最后取1做为增量把数组a再做一次插入排序。
增量序列h1,h2,hn之间满足一定的关系,比如下面的实现h1 = 3*h2 + 1;可以有不同的增量序列,但最后一个增量都是1,不同的增量序列算法的运行时间也会不同。
在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内进行插入排序较快,后来增量h逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于前面已经按较大增量作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
代码如下
public static void sort(Comparable[] a) {
int n = a.length;
// 3x+1 increment sequence: 1, 4, 13, 40, 121, 364, 1093, ...
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);
}
}
assert isHSorted(a, h);
h /= 3;
}
assert isSorted(a);
}
上面的代码运用插入排序的思想先处理每个h子数组的a[0]/a[1],再处理每个h子数组的a[0]/a[1]/a[2],,再处理每个h子数组的a[0]/a[1]/a[2].../a[n]。
也可以下面这样写,先处理第1个子数组的a[0]/a[1],a[0]/a[1]/a[2],a[0]/a[1]/a[2].../a[n]对第1个数组进行完整的插入排序,
再处理第2到n个子数组,对每个子数组进行完整的插入排序。这种写法比较好理解,但代码写起来要麻烦些
public static void sort2(Comparable[] a) {
int n = a.length;
// 3x+1 increment sequence: 1, 4, 13, 40, 121, 364, 1093, ...
int h = 1;
while(h < n/3) h = 3*h + 1;
while(h >= 1) {
for(int i = 0; i < h; i++) {
for(int j = h+i; j < n; j+=h) {
for(int k =j; k >= h && less(a[k], a[k-h]); k-=h) {
exch(a,k,k-h);
}
}
}
assert isHSorted(a, h);
h /= 3;
}
assert isSorted(a);
}
不同的增量序列算法的运行时间不同,希尔排序时间复杂度大概是O(,),运行SortCompare可以看出对于随机数组希尔排序比插入和选择排序快
# java SortCompare Shell Selection 3000 100
For 3000 random Doubles
Shell is 13.6 times faster than Selection
# java SortCompare Shell Insertion 3000 100
For 3000 random Doubles
Shell is 12.5 times faster than Insertion
# java SortCompare Shell Insertion 3000 100 sorted
For 3000 sorted Doubles
Shell is 0.5 times faster than Insertion
例如数组{7,3,1,78,2,8}的运行轨迹如下:h=4,1,
h=4时,分4组7,2一组,3,8一组,1一组,78一组,分别进行插入排序后如下
2,3,1,78,7,8
h=1时,在进行一次插入排序
1,2,3,7,8,78
归并排序
归并排序运用递归分治的思想,把数组a平分成2个数组a1和a2,分别对a1,a2排序后再把a1,a2归并成一个有序数组。
对a1排序也采用分治的思想,把a1平分成2个数组a3和a4,分别对a3,a4排序后再把a3,a4归并成一个有序的数组a1。
如此递归下去直到平分的2个数组都只有一个元素
代码如下
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
assert isSorted(a, lo, mid);
assert isSorted(a, mid+1, hi);
for(int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
int i = lo;
int j = mid+1;
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++];
}
assert isSorted(a, lo, hi);
}
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = (lo+hi)/2;
sort(a, aux, lo, mid);
sort(a, aux, mid+1, hi);
merge(a, aux, lo, mid, hi);
}
public static void sort(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
sort(a, aux, 0, a.length-1);
assert isSorted(a);
}
归并排序时间复杂度是O(),空间复杂度是O(N),如上面代码merge时需要一个辅助数组Comparable[] aux
我们可以通过上图这个依赖树来理解归并排序的时间复杂度,从图中可以看出树有lgN层,每层所有子数组在归并时的比较次数加起来正好是N,所以时间复杂度是O()
# java SortCompare Merge Shell 3000 100
For 3000 random Doubles
Merge is 0.7 times faster than Shell
# java SortCompare Merge Selection 3000 100
For 3000 random Doubles
Merge is 8.1 times faster than Selection
通过SortCompare对比可以看出归并排序比选择排序快,比希尔排序慢
上面这种归并排序算法运用的是深度优先的策略,如数组{1,2,3,4,5,6,7,8},先把1,2归并成数组a1,再把3,4归并成数组a2,再把a1和a2归并成a3,再把5,6归并成数组b1,再把7,8归并成数组b2,再把b1和b2归并成b3,最后把a3和b3归并成数组a完成排序
这种方式也叫做自顶向下的归并排序,还有一种自底向上的归并方式,采用广度优先的的策略,如数组{1,2,3,4,5,6,7,8},先两两归并把1,2归并成数组a1,再把3,4归并成数组a2,再把5,6归并成数组a3,再把7,8归并成数组a4,然后四四归并把a1和a2归并成b1,把a3和a4归并成b2,最后八八归并把b1和b2归并成数组a完成排序。
这种方式时间复杂度是O(),空间复杂度是O(N)
代码如下:
public static void sort(Comparable[] a) {
int n = a.length;
Comparable[] aux = new Comparable[a.length];
for(int len = 1; len < n; len = len + len) {
for(int lo = 0; lo < n-len; lo += len+len) {
int mid = lo + len -1;
int hi = Math.min(lo + len + len -1, n -1);
merge(a, aux, lo, mid, hi);
}
}
assert isSorted(a);
}
Ex 2.1.24,2.1.25 InsertionX 插入排序的改进
/**
* Rearranges the array in ascending order, using the natural order.
* @param a the array to be sorted
*/
public static void sort(Comparable[] a) {
int n = a.length;
// part 1
// put smallest element in position to serve as sentinel
int exchanges = 0;
for (int i = n-1; i > 0; i--) {
if (less(a[i], a[i-1])) {
exch(a, i, i-1);
exchanges++;
}
}
if (exchanges == 0) return;
// part 2
// insertion sort with half-exchanges
for (int i = 2; i < n; i++) {
Comparable v = a[i];
int j = i;
while (less(v, a[j-1])) {
a[j] = a[j-1];
j--;
}
a[j] = v;
}
assert isSorted(a);
}
我们可以先把最小元素找到移到a[0]去,这样当j=1时,a[j]肯定大于a[j-1],j就不会执行j--,就不用每次判断j > 1了,如上面par1部分.
每次比较如果a[j]比a[j-1]小,也可能比a[j-2],a[j-3]也小,这样a[j]与a[j-1]交换了,a[j-1]比a[j-2]还是要交换,可以只把大值往后移动一位,记住小值的位置,最后再把小值放到它的位置去,这样就可以减少交换的次数,如上面part2
BinaryInsertion 插入排序的改进
因为每次插入后,0到i已经有序了,所以我们可以通过二分查找下次的插入位置,从而减少比较的次数,代码如下:
public static void sort(Comparable[] a) {
int n = a.length;
for (int i = 1; i < n; i++) {
if (!less(a[i], a[i-1])) continue;
// binary search to determine index j at which to insert a[i]
Comparable v = a[i];
int lo = 0, hi = i;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (less(v, a[mid])) hi = mid;
else lo = mid + 1;
}
// insetion sort with "half exchanges"
// (insert a[i] at index j and shift a[j], ..., a[i-1] to right)
for (int j = i; j > lo; --j)
a[j] = a[j-1];
a[lo] = v;
}
assert isSorted(a);
}
Ex 2.2.11 归并排序的改进,实现 2.2.2 节所述的对归并排序的三项改进:
private static final int CUTOFF = 7; // cutoff to insertion sort
// This class should not be instantiated.
private MergeX() { }
private static void merge(Comparable[] src, Comparable[] dst, int lo, int mid, int hi) {
// precondition: src[lo .. mid] and src[mid+1 .. hi] are sorted subarrays
assert isSorted(src, lo, mid);
assert isSorted(src, mid+1, hi);
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) dst[k] = src[j++];
else if (j > hi) dst[k] = src[i++];
else if (less(src[j], src[i])) dst[k] = src[j++]; // to ensure stability
else dst[k] = src[i++];
}
// postcondition: dst[lo .. hi] is sorted subarray
assert isSorted(dst, lo, hi);
}
private static void sort(Comparable[] src, Comparable[] dst, int lo, int hi) {
// if (hi <= lo) return;
//改进1
if (hi <= lo + CUTOFF) {
insertionSort(dst, lo, hi);
return;
}
int mid = lo + (hi - lo) / 2;
sort(dst, src, lo, mid);
sort(dst, src, mid+1, hi);
// if (!less(src[mid+1], src[mid])) {
// for (int i = lo; i <= hi; i++) dst[i] = src[i];
// return;
// }
//改进2
// using System.arraycopy() is a bit faster than the above loop
if (!less(src[mid+1], src[mid])) {
System.arraycopy(src, lo, dst, lo, hi - lo + 1);
return;
}
merge(src, dst, lo, mid, hi);
}
/**
* Rearranges the array in ascending order, using the natural order.
* @param a the array to be sorted
*/
public static void sort(Comparable[] a) {
改进3
Comparable[] aux = a.clone();
sort(aux, a, 0, a.length-1);
assert isSorted(a);
}
// sort from a[lo] to a[hi] using insertion sort
private static void insertionSort(Comparable[] a, int lo, int hi) {
for (int i = lo; i <= hi; i++)
for (int j = i; j > lo && less(a[j], a[j-1]); j--)
exch(a, j, j-1);
}
改进1:加快小数组的排序速度,小数组用插入排序可以减少递归开销
改进2:检测数组是否已经有序,如果要归并的两个子数组已经有序可以直接copy了,开销比merge操作小
改进3:通过在递归中交换参数来避免在merge操作中复制元素到辅助数组。可以通过观察例子的运行轨迹来说明这种操作不会影响已经排序的子数组。
Ex 2.2.20 Index mergesort,不改变数组中元素位置,返回一个int[]数组perm,其中perm[i]的值是原数组中第i小的元素的位置。
/***************************************************************************
* Index mergesort. Ex 2.2.20
***************************************************************************/
// stably merge a[lo .. mid] with a[mid+1 .. hi] using aux[lo .. hi]
private static void merge(Comparable[] a, int[] index, int[] aux, int lo, int mid, int hi) {
// copy to aux[]
for (int k = lo; k <= hi; k++) {
aux[k] = index[k];
}
// merge back to a[]
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) index[k] = aux[j++];
else if (j > hi) index[k] = aux[i++];
else if (less(a[aux[j]], a[aux[i]])) index[k] = aux[j++];
else index[k] = aux[i++];
}
}
/**
* Returns a permutation that gives the elements in the array in ascending order.
* @param a the array
* @return a permutation {@code p[]} such that {@code a[p[0]]}, {@code a[p[1]]},
* ..., {@code a[p[N-1]]} are in ascending order
*/
public static int[] indexSort(Comparable[] a) {
int n = a.length;
int[] index = new int[n];
for (int i = 0; i < n; i++)
index[i] = i;
int[] aux = new int[n];
sort(a, index, aux, 0, n-1);
return index;
}
// mergesort a[lo..hi] using auxiliary array aux[lo..hi]
private static void sort(Comparable[] a, int[] index, int[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, index, aux, lo, mid);
sort(a, index, aux, mid + 1, hi);
merge(a, index, aux, lo, mid, hi);
}
Ex 2.2.10 快速归并
实现一个 merge() 方法,按降序将 a[] 的后半部分复制到 aux[],然后将其归并回 a[] 中。这样就可以去掉内循环中检测某半边是否用尽的代码。注意:这样的排序产生的结果是不稳定的(请见 2.5.1.8 节)。
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
for (int i = lo; i <= mid; i++)
aux[i] = a[i];
for (int j = mid+1; j <= hi; j++)
aux[j] = a[hi-j+mid+1];
int i = lo, j = hi;
for (int k = lo; k <= hi; k++)
if (less(aux[j], aux[i])) a[k] = aux[j--];
else a[k] = aux[i++];
}