【选择排序】
a[i++] —> a[n],从前往后看、选择最小值、一次交换到位
1,完整循环找到数组中最小的元素;
2,把这个最小的元素与a[0]交换;
3,在a[i]-an的子数组中重复1-2步骤;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
for(int i = 0; i < n; i++) {
int min = i;
for(int j = i + 1; j < n; j++) {
if(a[j] < min)
min = j;
}
swap(i, j, a);
}
|
简写:
1 2 3 4 5 |
for(int i =0; i < n; i++) {
min = //本次循环的i...n中的最小的元素的index;
//将min和本次循环的i两个元素交换;
}
|
特点:
选择排序的扫描路线:a[i++] —> a[n]
选择排序是在每个大循环下,通过完整子循环找到最小值后,退出子循环再进行交换,而不是一找到小于关系就交换,每次交换后,左侧的有序数组的位置是最终的;
运行时间和输入无关,扫描数组的次数是固定的,因为大循环和子循环的次数是固定的,前一次扫描并不为下一次扫描提供信息;
交换次数最少,因为是子循环完全结束后才进行一次交换,每次交换的结果都是最后的排序子结果,元素不用做二次挪动;
选择排序是一截一截往后看,将子数组中的最小元素交换到最前头的位置;
选择排序没有最好情况和最坏情况,扫描的次数是固定的,交换的次数已经是所有排序算法中最少的了;
【插入排序】
a[j—] —> a[0],从半路往前看、让元素尝试往前走不动为止
插入排序是一截一截往前看,将倒置的元素交换;对于一个元素,总是尝试往前走,走到不能走为止,因为前面的元素已然有序了,所以走不动的时候左侧元素也是刚刚重新有序;总是相邻元素交换,所以交换次数频繁,一次交换的位置未必是最终位置;
插入排序的扫描路线:a[j—] —> a[0]
1 2 3 4 5 6 |
for(int i = 1; i < n; i++) {
for(int j = i; j > 0 && a[j] < a[j-1]; j--)
swap(j, j -1, a);
}
|
每次子循环的结果,左侧的元素肯定是在已知(已经扫描)的数组中有序的,所以每次子循环发生的条件是,如果a[j]小于a[j-1],则交换,然后继续进行j—之后的扫描;但如果a[j]不小于a[j-1],因为左侧在已知数组中是有序的,这个时候该子循环就不会发生了;
对已然有序的数组排序,插入排序是线性扫描,0次交换;
插入排序就是解决倒置的两个元素,交换的次数就是倒置的元素个数;
插入排序和选择排序都是平方级的运行时间,但插入排序通常比选择排序快一个常数,因为对于随机的数组来说,插入排序扫描的次数会由于数组中的部分有序而减少,但选择排序不会,必须全扫描;交换,则是插入排序比选择排序次数要多,选择排序交换次数最多不超过n;
【归并排序】
先2分排序子数组,再归并成原数组
原地归并算法:
1,先将所有元素赋值到一个新的数组中,此时数组是两截有序的数组;
2, 在原数组上讲新数组中的数本地归并回来:左边用尽取右边;右边用尽取左边;右边当前元素比左边小取右边;右边当前元素大于等于左边取左边;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void merge(Comparable[] a, int low, int mid, int hign) {
int i = low, j = mid + 1;
for(int k = low; k <= hign; k++)
aux[k] = a[k];
for(int k = low; k <= hign; k++)
if(i > mid) a[k] = aux[j++];
else if(j > hign) a[k] = aux[i++];
else if(aux[j] < aux[i]) a[k] = aux[j++);
else a[k] = aux[i++];
}
|
自顶向下的归并排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
sort(a, 0, a.length - 1);
void sort(Comparable[] a, int low, int hign) {
if(hign < = low) return;
int mid = low + (hign - low) / 2;
sort(a, low, mid);
sort(a, mid + 1, hign);
merge(a, low, mid, hign);
}
|
归并算法的思路就是:两个单元素的数组是分别有序的,可以通过merge方法将其归并为一个2元素的有序数组,依此类推,两个a[low]到a[mid]和a[mid+1]到a[hign]的数组分别有序,可以通过merge方法将其归并为一个有序数组a[low]到a[hign];
归并自上而下,先分拆(sort)直至单元素数组,再归并(merge)回到原数组;
归并算法的几个优化策略:
1,对小规模数组改用插入排序,比如sort方法中发现hign - low <= 10,则用插入排序;
2, 测试数组是否已经有序,就是看a[mid]如果小于等于a[mid+1],就不用归并了;
归并排序的时间总是NlogN;
跟普通排序不需要额外空间不一样,归并排序是需要额外空间的,跟N成正比;
归并排序是某种程度上的空间换时间算法;
自底向上的归并:
1 2 3 4 5 6 7 8 9 10 |
void sort(Comparable[] a) {
aux = new Comparable[a.length];
for(int sz = 1; sz < a.length; sz = sz + sz)
for(int low = 0; low < N - sz; low += sz + sz)
merge(a, low, low + sz - 1, Math.min(low + sz + sz - 1, N -1);
}
|
自顶向下是 化整为零,自底向上是循序渐进;
归并排序用了aux辅助数组,是空间复杂度不是最优的;
任何比较排序算法的复杂度都不会低于lg(N!)~NlgN;
【快速排序】
每一次切分都使数组左右两边趋于有序
特点:
1, 快速排序是原地排序,只需要一个很小的辅助栈;归并排序无法做到;
2, 复杂度是NlgN;插入、选择等交换排序无法做到;
3, 快速排序的原理:将一个数组分成两个子数组,将两部分独立地排序。
快速排序和归并排序是互补的:
1, 归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;快速排序是当两个子数组都有序时整个数组也就自然有序了。
2, 归并排序中,递归调用发生在处理整个数组之前;快速排序中,递归调用发生在处理整个数组之后;
3, 归并排序是数组被等分为两半;快速排序中,切分的位置取决于数组的内容;
算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void sort(Comparable[] a) {
random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素
sort(a, 0, a.length - 1);
}
void sort(Comparable[] a, int low, int hign) {
if(hign <= low)
return;
int j = partition(a, low, hign);
sort(a, low, j -1);
sort(a, j + 1, hign);
}
|
递归调用切分实现排序的思路:
如果左子数组和右子数组都是有序的,那么由左子数组(有序且所有元素都小于等于切分元素+切分元素+右子数组(有序且所有元素大于等于切分元素)组成的结果数组也一定是有序的;
切分找j的条件
1, 对于某个j,a[j]已经排定;
2, a[low]到a[j - 1]中的所有元素都不大于a[j];
3, a[j+1]到a[hign]中的所有元素都不小于a[j];
注意,上面的2, 3说的都是j的左边和右边的元素分别不大于和不小于切分元素,但此时左右两边并不是有序的;
切分算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
int partition(Comparable[] a, int low, int hign) {
int i = low, j = hign + 1;
Comparable v = a[0];
while(true) {
while(a[++i] < v)
if(i == hign)
break;
while(a[--j] > v)
if(j == low)
break;
if(i >= j)
break;
swap(a, i, j);
}
swap(a, low, j);
return j;
}
|
两个小while分别做从左、从右往中间走的动作;
从左边走遇见的比切分元素大的元素,跟从右边走遇见的比切分元素小的元素,交换之;
因为一次大循环中用的都是a[lo],同一个参考值,那么通过这样的交换,左侧都小,右侧都大;
i, j相遇的最后一次交换,是a[j]被认为小小于切分元素,a[i]被认为大于切分元素,然后他们交换了位置,这一次是相邻交换,—j和—i使得i和j的索引也交换了,所以此时j就是上一次的i的位置,已经被小于切分元素a[lo]的上一次的a[j]给占用了;同时a[i]则是 大于a[lo]的元素;
所以,最后swap(lo, j),跟开始的切分元素=a[lo]相呼应;
切分的轨迹是这样的 :
lo….i….j…hi
lo….ij…….hi
lo….ji…….hi
j…..loi…….hi
归并排序是从底往上一层层合并有有序子数组;
快速排序是从上往下,循序渐进,一层层切分下去,每一次切分都使得数组呈两边大小合适状态,切到单元素数组的时候,整个数组基于n多个两边大小合适的小数组,而有序了;
切分元素不一定要选择a[low],随机选都行;
打乱数组顺序的意义:random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素。
如果每次的切分元素总是最小的元素,那么每一次切分都只是分离成一个元素的数组和一个N-1长度的子数组,这使得数组会被切分N多次;
对于小数组,切换到插入排序能提高效率;
partition方法还可以用来找一个数组中【第k大的元素】:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int lo = 0, hi = a.length - 1;
while(hi > lo) {
int j = partition(a, lo, hi);
if(j == k)
return a[k];
if(j > k)
hi = j - 1;
if(j < k)
lo = j + 1;
return a[k];
|
【堆排序】
先使堆有序,再一个个删除最大值,删除即把第一个最大元素往尾部交换以推出堆
上浮和下沉算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
void swim(int k) {
while(k > 1 && pq[k/2] < pq[k]) {
swap(pq, k/2, k);
k = k / 2;
}
void sink(int k) {
while(2 * k <= N) {
int j = 2 * k;
if(j < N && pq[j] < pq[j+1]) //选取两个子节点中较大的一个往上交换
j++;
if(pq[k] >= pq[j]) //结束下沉,已经比字节点大了
break;
swap(pq, k, j); //下沉
k = j;
}
public static void sort(Comparable[] a) {
int N = a.length;
//先使得堆有序
for(int k = N/2; k >= 1; k--) //从右到左扫,因为最终堆有序是右边总比左边小;
sink(a, k, N); //只扫描一半,因为一半之后的元素都是叶节点,就是为1的堆
//再进行下沉排序,销毁堆有序
while(N > 1) { //把最大元素删除,然后放入堆缩小后数组中空出的位置
swap(a, 1, N--); //a[1]就是最大元素,把其交换到最后一个,就是从堆有序堆中删除它
sink(a, 1, N); //被交换到a[1]的元素,通过在子堆中下沉,使得子堆再次有序
} //重复这个过程,最大元素总是往后一个一个走,最后整个数组就有序了
|
堆排序的复杂度:N*lgN,而且是原地排序,无额外空间消耗;
【SpaceToTime排序 空间换时间排序法】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
int i = 0;
int max = array[0];
int len = array.length;
for(int i = 1; i < len; i++) //找出最大值,做为空间数组的length
if(array[i] > max)
max = array[i];
int[] temp = new int[max + 1];
for(int i = 0; i < len; i++)
temp[array[i]] = array[i];
int j = 0;
int max1 = max + 1;
for(i = 0; i < max1; i++) {
if(temp[i] > 0]
array[j++] = temp[i];
}
不计空间成本,把数组值映射到一个临时数组的下标,然后遍历临时数组,把大于0的数顺序放回原数组;
注:temp[i] = i;
array[i] > 0;
|
【Java.util.Arrays.sort】
对原始类型用三向切分的快速排序;
对引用类型用归并排序;
【Comparable Comparator】
一个类实现了Comparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
1、类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
2、可以使用多种排序标准,比如升序、降序等
多键排序,用Comparator;