【选择排序】
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;