排序算法基本每个学语言的人都会写好几次。。在回顾排序算法的时候发现了上课划水时的一些错误理解。
1.旧的错误:在高效排序中局部使用了低效的‘冒泡’
2.新的发现:希尔排序与堆排序的‘相似’之处
3.旧的错误:堆排序的initHeap()的错误思路与正确思路
4.旧的错误:错误的并归排序写法——为了节省空间,让并归排序在同一个表中进行——结果每个小数组仍然要使用简单排序
5.旧的错误:错误的快排算法写法——取值思路错误,导致又用了简单排序
6.稳定性的作用:第一次理解到稳定性到底有什么意义
基础:
冒泡:
复杂度O(n^2),稳定,
冒泡排序很简单,思路也很有趣,但是性能比较低,所以不要写冒泡排序。
无序数组:985211369
冒泡1次: 89 5211369-->859 211369-->8529 11369-->~~~~852113699
冒泡2次: 58 2113699-->528 113699-->5218 13699-->~~~~521136899
这里说一下冒泡是因为后面会多次提到,很可能在其他高效排序中一不小心就用到了低性能的冒泡
选择:
复杂度O(n^2),不稳定
选择排序是大部分小伙计上手就会用的排序,遍历找min,交换,思路清晰,代码简洁 省略
直接插入排序:
复杂度O(n^2),稳定
直接插入是理解希尔排序的基础。也是三个基础排序中性能最高的,所以即便写不出快排等,也要用直接插入排序
无序数组: int[] arr=new int[] {5,1,0,7,2,14,17,10,11,13}
数组看成两个表,前有序,后无序
排序第一趟:1和5交换 得{1,5,0,7,2,14,17,10,11,13}
排序第二趟:0和5交换,再0和1交换,得{0,1,5,7,2,14,17,10,11,13}
嗯,上述第二趟比较的说明是错误的,这也是我最开始写直接插入排序时候的错误。。就是——“写成了冒泡”
按照上面的说法,代码是:
static void fakeISort(int[] arr) {
for(int i=1;i<arr.length;i++) {
int j=i; //j初始指向无序表表头
while(j>0&&arr[j]<arr[j-1]) {
int t=arr[j];
arr[j]=arr[j-1];
arr[j-1]=t;
--j;
}
}
}
看其中交换的操作,比如 453,交换一次 435,交换两次345,,实际把这部分写成了冒泡算法
实际上应该写出的代码是:
static void ISort(int[] arr) {
for(int i=1;i<arr.length;i++) {
int val=arr[i];
int j=i;
while(j>0&&val<arr[j-1]){
arr[j]=arr[j-1];
j--;
}arr[j]=val;
}
}
由于在一个数往前移动的过程中,冒泡的思路深入人心,,总是在无意间会用到,甚至包括后面会出现的希尔排序,堆排序。切记不要再写出‘冒泡’风格的直接插入排序算法了、
进阶:
SheelSort 希尔排序 第一个复杂度超越n^2的排序算法
复杂度:希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。 [百度百科]
分析一波比较次数
凭空分析:
直接插入排序从arr[1]开始到arr[n-1]结束,对于每一个值,向前对比直到前者小于自身为止
希尔排序共K*increment+Y趟 (具体看取值)
第1趟从arr[increment]到arr[k*increment],对于每一个值,向前跳跃式对比直到前者小于自身为止
~~~~
第K*increment趟(此时increment=1),从arr[1]到arr[n-1],对于每一个值,向前对比直到前者小于自身为止
凭空分析结论:数学不好,,不好说 不过可以发现其实希尔排序的最后一次对比等于直接插入排序,但此时数组已经基本有序!————(所以还可以得出结论,数组基本有序的情况有利于直接插入排序效率)
实例分析:假设有数组 5 1 17 7 2 14 17 10 11 13
(希尔排序的增量值取1/3)所以外层排序了3次
希尔排序代码如下:
static void fakeSheelSort(int[] arr) {
int inc=arr.length;
do {
inc=inc/3+1;
//增量inc意味着待排序的arr[index]将与arr[index-inc*k]对比,而不是对比arr[index-1*k]
for(int i=inc;i<arr.length;i++) {
int j=i-inc,I=i;
while(j>=0&&arr[j]>arr[i]) { //以下是错误的写法,即冒泡
int t=arr[j];
arr[j]=arr[i];
arr[i]=t;
j-=inc;i-=inc;
}i=I;
}
}while(inc>1);
}
//正确的写法只是将里面的for循环改动一下: 即冒泡改成直接插入
for(int i=inc;i<arr.length;i++) {
int temp=arr[i];
int j=i-inc;
while(j>=0&&temp<arr[j]) {
arr[j+inc]=arr[j];
j-=inc;
}arr[j+inc]=temp;
}
堆排序
说道堆排序不得不说一下二叉树。。我本来打算先复习一下二叉树再来看堆排序的,后来觉得有了堆排序的引入,写二叉树可能会更简单。
不过开始之前,对于二叉树至少要有这些了解:
根节点、子节点的定义。层序遍历的概念。
当对二叉树层序遍历,设根节点为arr[1],则有对于节点arr[i],其左孩子节点为arr[2*i],右孩子为arr[2*i+1]
如图所示
算法思想:
如图所示的两棵二叉树被称为大顶堆/小顶堆
容易看出,大顶堆根节点大于子节点,小顶堆反之
如果说我们可以把待排数组构建成大顶堆(这里一直用大顶堆举例),然后将根节点去掉(因为根节点最大,直接放在数组最后一位,对最大值的排序结束),剩余部分再次构建大顶堆,重复上述动作直到最后一个根节点被移除
(。天晓得这种结构|思路是怎么想出来的~~)
现在过程比较明确:
1.构建大顶堆 initHeap()
2.将大顶堆根节点移出作为最大值,重建(修复)大顶堆
难点在于如何构建大顶堆
这里的理性推理就省略了。。我当时没看书的时候自己想出了一种正确但绕了弯子的思路。。这种东西看一遍书基本不会忘的
从二叉树最后一个(按层序遍历)非叶子结点向前循环
对于每一个节点node,将以该节点为根节点的子树,调整至大顶堆
调整方法为,如果node的子节点中存在比note更大的值,让node与更大那一个子节点‘交换’,以此类推
图示: 假设有数组 5 1 17 7 2 14 17 10 11 13
首先如上左图,层序倒数第一个分支节点为第三层第二个节点②,调整以②为根的树为大顶堆。接着调整以⑦为根的树
然后如上右图,二层第二个节点(17)为根的树无需调整。接着调整(1)为根的树,(1)与右孩子(13)交换,然后再调整其子树
后续步骤省略
准备工作完成,下面是某一种代码实现——代码没有这么长,这中间另外写了错误的冒泡写法
public class HeapSort {
public static void heapSort(int[] arr) {
//数组arr没有做特殊处理,所以arr[0]左孩子arr[1],右孩子arr[2]
//一般的,节点arr[i]的左孩子为arr[2*i+1],右孩子为arr[2*i+2]
int last=(arr.length-1)/2; //寻找层序倒数第一个分支节点
initHeap(arr,last,arr.length); //初始化堆
//排序--重建堆--排序~~
int len=arr.length-1;
while(len>0) {
int max=arr[0];
arr[0]=arr[len];
arr[len--]=max;
initHeap2(arr,0,len+1);
}
}
//构建大顶堆。参数:数组,最后一个待排序的分支节点下标,数组长度
//初次新建大顶堆所有分支节点都需要排序。后续还原大顶堆仅仅根节点需要排序
private static void initHeap(int[] arr, int last,int len) {
int i=last;
while(last>=0) {
i=last;
while(i*2+2<len) { //‘冒泡’排序写法——疯狂交换
int left=arr[2*i+1],right=arr[2*i+2];
if(arr[i]>=left&&arr[i]>=right)
break;
else {
if(left>right) {
arr[2*i+1]=arr[i];
arr[i]=left;
i=i*2+1;
}else {
arr[2*i+2]=arr[i];
arr[i]=right;
i=i*2+2;
}
}
}
if(i*2+1<len&&arr[i*2+1]>arr[i]) {
int t=arr[i*2+1];
arr[i*2+1]=arr[i];
arr[i]=t;
}--last;
}
}
private static void initHeap2(int[] arr, int last,int len) {
int i=last;
while(last>=0) {
i=last;
int key=arr[i]; //正确写法,类似于希尔排序(或直接插入)
while(i*2+2<len) {
int left=arr[2*i+1],right=arr[2*i+2];
if(key>=left&&key>=right) {
arr[i]=key;
break;
}else { //将孩子中较大者向上转,i指向该较大值
int bigger=right>left?i*2+2:i*2+1;
arr[i]=arr[bigger];
i=bigger;
}
}
if(i*2+1<len&&arr[i*2+1]>key) {
arr[i]=arr[i*2+1];
i=i*2+1;
}arr[i]=key;
--last;
}
}
public static void main(String[] args) { //测试
int[] arr=Helper.randomArr(10, 100);
Helper.show(arr);
heapSort(arr);
Helper.show(arr);
}
}
想没想过堆排序其实也有希尔排序的影子 (只是个人理解。)
希尔排序中,每个元素arr[index]会与前面的0个或多个arr[index-inc*k]进行比较。堆排序中,每个arr[index]会与其左右孩子进行比较,即与arr[index*2+1]/arr[index*2+2]比较,通过比较结果的不同进行下一步。
它们都是将低效排序的相邻对比转换成了高效的间隔对比,不过希尔排序是线性增加的间隔,堆排序是平方级增长
归并排序(仅二路)
堆排序借助了二叉树的性质,很高效,但是这种结构在未产生之前是很难想到的。下面要说的归并排序就是一种思路比较清晰的算法
假设待排数组arr有n个值,那我们可以将待排数组看成n个有序子集合,如下所示
# # # # # # # # # # # # # # # # //有序子集合,每个集合长度不超过1
# # # # # # # # # # # # # # # # //相邻两个子集合进行归并排序得到新的子集合
# # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # //直至最后排序结束
综上,归并排序的核心在于:将两个有序子集合合并成一个有序子集合
原始的归并排序吧上述集合看成真的集合,,每次合并都要耗费新的空间(空间换时间),主要原因是局部排序也会耗费时间,举个例子:
子集A:1 4 7 子集B:2 5 8
如果采用真的数组C来保存,写法是 C[k++]=Max(A[i],B[j]);
如果在同一张表上处理,则 序列 1 4 7——2 5 8 还要进行排序,又会用到简单排序算法。。那就不是归并排序了
下面是错误的代码实现——错误的原因在于我们在局部排序中使用了直接插入排序,而不是归并排序要求的空间换时间
public class MSort {
static void merge(int[] A,int art,int aend,int bend) {
//说明,A为待合并数组,其中A[art,aend]有序且A[aend+1,bend]有序
if(bend>=A.length)bend=A.length-1;
for(int i=aend+1;i<=bend;i++) {
while(A[i]<A[i-1]) {
int key=A[i];
int j=i-1;
while(j>=0&&A[j]>key)
A[j+1]=A[j--];
A[++j]=key;
}
}
}
static void mSort(int[] arr) {
for(int i=1;i<arr.length;i*=2) {//i代表子数组长度
for(int j=0;j<arr.length;j+=2*i) {//将数组分成多份,每份长度为i(除去最后一份)
System.out.print("排序arr["+j+","+(j+i-1)+"],arr["+(j+i)+","+(j+2*i-1)+"]");
merge(arr,j,j+i-1,j+2*i-1);
}System.out.println();
}
}
public static void main(String[] args) {
int[] arr=Helper.randomArr(10, 100);
Helper.show(arr);
mSort(arr);
Helper.show(arr);
}
}
一种正确思路的代码如下:
static int[] sort(int[] arr, int len) {
if(len==1)
return arr;
if(len==2) {
if(arr[0]>arr[1]) {
int t=arr[0];arr[0]=arr[1];arr[1]=t;
}return arr;
}
int[] a=new int[len/2],b=new int[len-len/2];
int ac=0,bc=0;
for(int i=0;i<len;i++)
if(i<len/2)
a[ac++]=arr[i];
else
b[bc++]=arr[i];
a=sort(a,len/2);b=sort(b,len-len/2);
System.out.print("A:");Helper.show(a);
System.out.print("B:");Helper.show(b);
int[]c=new int[len];ac=0;bc=0;
for(int i=0;i<len;i++)
if(bc==b.length||ac<a.length&&a[ac]<b[bc])
c[i]=a[ac++];
else c[i]=b[bc++];
return c;
}
Main: arr=sort(arr,arr.length);
快排:
快排其实对比堆排序更好实现,主要是要理解思想
思路:通过一趟排序,将待排记录分割成独立的两部分,其中一部分关键字均比另一部分小,然后分别对每一部分继续分割,直至有序。
更通俗的解释:从待排序列中找到这样一个x,通过调整使得他比左边的数都要大,同时比右边的数都要小。此时x左边的整个部分与x右边的整个部分已经有序,再对左右两部分进行同样的操作即可。
static int partSort(int[] arr,int start,int end) {
//对[start,end]整体排序,并返回关键字坐标(下次排序的分界)
int key=arr[start]; //取第一个作为关键字
int low=start,high=end;
while(low<high) {
while(high>low&&arr[high]>=key) //Q1 Q2
high--; //如果右边存在不满足平衡条件的值,让他交换到另一边
swap(arr,low,high);
while(low<high&&arr[low]<=key)
low++;
swap(arr,low,high);
}return low;
}
static void fSort(int[] arr,int start,int end) {
int key1=partSort(arr,start,end);
if(key1<end)
fSort(arr,key1+1,end);
if(key1>start)
fSort(arr,start,key1-1);
}
//上述写法也可以合并.理解起来更费劲
public static void fastSort(int[] arr,int a,int b){
if(a>=b)
return;
int oa=a,ob=b;
while(a<b){
while(a<b&&arr[b]>=arr[a])
--b;
int t=arr[a];arr[a]=arr[b];arr[b]=t;
while(a<b&&arr[a]<=arr[b])
++a;
t=arr[b];arr[b]=arr[a];arr[a]=t;
}
if(oa<a-1)
fastSort(arr,oa,a-1);
if(ob>a+1)
fastSort(arr,a+1,ob);
}
Main: fastSort(arr,0,arr.length-1);
如代码注释所示,两个问题
Q1:为什么要先while(high)后while(low)?
A:因为我们所取的关键字是第一个,对比的方式是当 相等于关键字 则不排序。如果这里写成先while(low)那么我们数组靠前的 那几个等于关键字key的数字 将不会被排序——比如说数组 5 1 2 4 9. key=5 由于arr[low(0)]==5,low++,直接跳过了第一个数字5的排序。。 相应的,如果我们所取的关键字是最后一个,那么就需要先while(low)后while(high)
Q2:为什么是arr[high]>=key的时候 high-- (为什么arr[low]<=key的时候 low++) ——为什么要取等
A:如果不取等号,假设有数组 5 5,那么arr[0]和arr[1]会一直交换,直到超时。。
稳定性
假设现在有一个二维数组arr[k][2] 要求按照arr[i][0]降序排列,如果arr[i][0]相等则保持原顺序不变
实例: Arrays.sort(arr,(a,b)->b[0]-a[0]); //如果不了解lambda或者不了解comparator自己百度先
这个排序的结果会是错误的,他改变了arr[i][0]相等的元素的先后顺序
另外——mysql中order by采用的算法也是不稳定排序,这会导致出现问题——比如在使用limit做的分页里面出现当前页最后一个数据和下一页第一个数据重复(比较的关键字一样,在两次排序中前后的相对位置变化了),这个是我在其他博文上看到的,不过忘记了是哪一个网址。。