排序算法总览
排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。内排序有可以分为以下几类:
(1)、插入排序:直接插入排序、二分法插入排序、希尔排序。
(2)、选择排序:简单选择排序、堆排序。
(3)、交换排序:冒泡排序、快速排序。
(4)、归并排序
(5)、线性时间排序:计数排序、基数排序、桶排序
算法复杂度以及稳定性分析
外排序
外排序(External
sorting)是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依次将待排序数据组织为多个有序的临时文件。然后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
内排序
插入排序
直接插入排序
基本思想:每步将一个待排序的纪录,按其关键字的大小插入到已经排好序的有序数据中,直到全部插入完为止。
public static void insertSort(int[] arr){ if(arr == null || arr.length <= 1) return; for(int i=1;i<arr.length ;i++){ int p=i; int temp=arr[p]; while(p>0&&arr[p-1]>temp){ arr[p]=arr[p-1]; p--; } arr[p]=temp; } }
特征分析:
1、空间复杂度O(1)。最好时间复杂度O(n),最坏时间复杂度O(n^2),平均时间复杂度为O(n^2)。最好情况下(已有序),比较次数n-1,移动次数0,最坏情况,比较次数O(n^2),移动次数O(n^2)。
2、直接插入排序是稳定的。
使用场景:
适合少量数据的排序,或者数据基本已经有序的情况。
二分插入排序(折半插入)
直接插入排序算法在寻找插入位置是采用线性查找的方式,我们可以采用二分查找来确定插入位置从而减少比较次数。
public static void binaryInsertSort(int[] arr){ if(arr == null || arr.length <= 1) return; for(int i=1;i<arr.length ;i++){ int temp=arr[i]; int start=0,end=i; while(start<end){ int mid=start+(end-start)/2; if(arr[mid]<=temp){ start=mid+1; }else{ end=mid; } } int p=i; while(p>end){ arr[p]=arr[p-1]; p--; } arr[p]=temp; } }
特征分析:
1、折半插入排序仅减少了关键字间的比较次数,而记录的移动次数不变。
使用场景:
1、如果元素的比较的操作比较耗时,可以对直接插入排序的性能进行提升。
链表的插入排序
由于链表不具有随机访问特性,因此不适用于折半插入排序,只适用于直接插入排序,每次从已排序数据表头开始查找插入位置。
public class ListNode{ private int val; private ListNode next; public ListNode(int val){ this.val=val; } } public static ListNode listInsertSort(ListNode head){ ListNode myHead=new ListNode(-1); ListNode p=head,t,pre,post; while(p!=null){ t=p; p=p.next; pre=myHead; post=myHead.next; while(post!=null&&post.val<=t.val){ pre=pre.next; post=post.next; } t.next=post; pre.next=t; } return myHead.next; }
希尔排序
基本思想:
直接插入排序中,如果元素X初始化时所处的位置为i,排序后所处位置为j,那么元素x必须要经过j-i-1(j>=i)次移动。而希尔排序中,我们先取一个小于n的整数d1作为第一个增量,把数据的全部记录分组。所有距离为d1的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量
di
=1(
di
<
di-1
…<d2<d1),即所有记录放在同一组中进行直接插入排序为止(这时候数组已经基本有序)。平均情况下,因为分组的增量>=1,元素X从初始化位置移动到最终位置所需要的次数比原来要小。
public static void shellSort(int[] arr){ if(arr == null || arr.length <= 1) return; int gap=arr.length/2; while(gap>0){ for(int i=1;i<arr.length ;i++){ int temp=arr[i]; int p=i; while(p>=gap&&arr[p-gap]>temp){ arr[p]=arr[p-gap]; p-=gap; } arr[p]=temp; } gap/=2; } }
特征分析:
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法,
在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快
O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(
n^2
)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.
本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。
原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。
正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。
选择排序
简单选择排序
它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
public static void simpleSelectSort(int[] arr){ if(arr==null||arr.length<=1) return; for(int i=0;i<arr.length;i++){ int start=i,minIndex=i; for(int j=i+1;j<arr.length;j++){ if(arr[j]<arr[minIndex]) minIndex=j; } int temp=arr[start];arr[start]=arr[minIndex];arr[minIndex]=temp; } }
特征分析:
1、空间复杂度O(1),最好/最坏/平均时间复杂度都是O(n^2),比较次数O(n^2),移动次数O(n)。
2、选择排序是不稳定的排序方法(比如序列[5,
5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
堆排序
堆
堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]]
>= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。最小堆通常在构造优先队列时使用。
堆排序
基本思想:
首先通过自底向上的调整堆来建立大根对,然后依次将最大值a[0]与末尾元素交换并调整交换后的堆(大小减1)。
//调整堆 public static void adjustHeap(int[] arr,int size,int top){ while(top*2+1<=size-1){ int change=0; if(top*2+1==size-1){//只有左子节点 change=top*2+1; if(arr[top]>=arr[top*2+1]){ break; } }else{ change=arr[top*2+1]>arr[top*2+2]?top*2+1:top*2+2; if(arr[top]>=arr[change]){ break; } } int t=arr[top];arr[top]=arr[change];arr[change]=t; top=change; } } //建堆 public static void buildHeap(int[] arr){ for(int top=(arr.length)/2;top>=0;top--){ adjustHeap(arr,arr.length,top); } } //堆排序 public static void heapSort(int[] arr){ buildHeap(arr); for(int i:arr){ System.out.print(i+" "); } System.out.println(); for(int size=arr.length;size>=2;size--){ int t=arr[0];arr[0]=arr[size-1];arr[size-1]=t; adjustHeap(arr,size-1,0); } }
特征分析:
1、空间复杂度O(1),平均时间复杂度O(n log n),依次堆调整O(log n)——即堆的高度。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数量较少的情况。
2、堆排序时不稳定的。
交换排序
冒泡排序
基本思想:
比较相邻的元素,如果前者比后者大,就交换他们两个,对从前往后对每一对相邻元素作同样的工作,这样一趟冒泡后,最后的元素就是最大的数。这样每一趟冒泡确定一个元素的位置,n趟后数组即有序。同样也可以从后往前冒泡,依次确定最小值。
public static void bubbleSort(int[] arr){ for(int end=arr.length-1;end>0;end--){ boolean changed=false; for(int index=0;index<end;index++){ if(arr[index]>arr[index+1]){ int t=arr[index+1]; arr[index+1]=arr[index]; arr[index]=t; changed=true; } } if(changed==false) return; } }
特征分析:
1、空间复杂度O(1),最好时间复杂度O(n),最坏时间复杂度O(n^2),平均时间复杂度O(n^2)。
2、冒泡排序时稳定的。
快速排序
基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
递归版
public static int partition(int[] arr,int start,int end){ int part=arr[start]; int locate=start; while(start<end){ while(end>start&&arr[end]>part)end--; if(end<=start){ break; } arr[locate]=arr[end]; locate=end; while(start<end&&arr[start]<=part) start++; if(start>=end){ break; } arr[locate]=arr[start]; locate=start; } arr[locate]=part; return locate; } public static void quickSort(int[] arr,int start,int end){ if(start>=end) return; int mid=partition(arr,start,end); quickSort(arr,start,mid-1); quickSort(arr,mid+1,end); }
跌代版
public static void quickSort(int[] arr){ Stack<Integer> stack=new Stack<Integer>(); stack.push(0);stack.push(arr.length-1); while(stack.empty()==false){ int end=stack.pop(),start=stack.pop(); if(start>=end) continue; int part=arr[start]; int locate =start; int pre=start,post=end; while(pre<post){ while(pre<post&&arr[post]>part)post--; if(pre>post) break; arr[locate]=arr[post]; locate=post; while(pre<post&&arr[pre]<=part)pre++; if(pre>post) break; arr[locate]=arr[pre]; locate=pre; } arr[locate]=part; stack.push(start);stack.push(locate-1); stack.push(locate+1);stack.push(end); } }
特征分析:
1、最好空间复杂度O(log(n)),最坏空间复杂度O(n),平均空间复杂度O(log
n);最好时间复杂度O(n log n),最坏时间复杂度O(n^2),平均时间复杂度O(n log n)。
2、快速排序时不稳定的。
改进
在最坏的情况下,每次划分都将n个元素划分为n-1个元素和1个元素,我们可以通过采取措施尽量较少这种不对称划分来优化算法。例如,我们在选取划分元素时,不是选取特定位置的元素,而是采取随机选取的方式或者随机取三个元素并选定其中值进行划分。
归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide
and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
递归版
public static void merge(int[] arr,int start1,int start2,int end2,int[] temp){ int end1=start2-1,index=0; int p=start1,q=start2; while(p<=end1||q<=end2){ if(q>end2||(p<=end1&&arr[p]<arr[q])){ temp[index++]=arr[p++]; }else{ temp[index++]=arr[q++]; } } for(int i=0;i<index;i++){ arr[start1+i]=temp[i]; } } public static void mergeSort(int[] arr,int start,int end,int[] temp){ if(start>=end) return; int mid=start+(end-start)/2; mergeSort(arr,start,mid,temp); mergeSort(arr,mid+1,end,temp); merge(arr,start,mid+1,end,temp); }
迭代版
public static void mergeSort(int[] arr,int[] temp){ int size=1,low,mid,high,n=arr.length; while(size<=n-1) { low=0; while(low+size<=n-1) { mid=low+size-1; high=mid+size; if(high>n-1)//第二个序列个数不足size high=n-1; merge(arr,low,mid+1,high,temp);//调用归并子函数 System.out.println("low:"+low+" mid:"+mid+" high:"+high); low=high+1;//下一次归并时第一关序列的下界 } size*=2;//范围扩大一倍 } }
特征分析:
1、空间复杂度O(n),时间复杂度O(1)。
2、归并排序时稳定的排序。
线性时间排序
计数排序
基本思想:
是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
public static int[] countSort(int[] a){ int b[] = new int[a.length]; int max = a[0],min = a[0]; for(int i:a){ if(i>max) max=i; if(i<min) min=i; }//这里k的大小是要排序的数组中,元素大小的极值差+1 int k=max-min+1; int c[]=new int[k]; for(int i=0;i<a.length;++i){ c[a[i]-min]+=1;//优化过的地方,减小了数组c的大小 } for(int i=1;i<c.length;++i){ c[i]=c[i]+c[i-1]; } for(int i=a.length-1;i>=0;--i){ b[--c[a[i]-min]]=a[i];//按存取的方式取出c的元素 } return b; }
特征分析:
1、空间复杂度O(k+n)(k为数据范围),时间复杂度O(n+k)。
2、计数排序时稳定的。
变形:
出来了通过统计出现次数之外,我们还可以用链表记录数值相同的元素。
适用场景:
1、当k=O(n)时。
基数排序
基本思想:
基数排序(radix
sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。
// d为数据长度 private static void radixSorting(int[] arr, int d) { for (int i = 1; i <=d; i++) { int[] res = countingSort(arr, i); // 依次对各位数字排序(直接用计数排序的变体) for(int j=0;j<arr.length;j++){ arr[j]=res[j]; } } } // 利用计数排序对元素的每一位进行排序 private static int[] countingSort(int[] arr, int expIndex) { int k = 9; int[] b = new int[arr.length]; int[] c = new int[k + 1]; // 这里比较特殊:数的每一位最大数为9 for (int i = 0; i < k; i++) { c[i] = 0; } for (int i = 0; i < arr.length; i++) { int d = getBitData(arr[i], expIndex); c[d]++; } for (int i = 1; i <= k; i++) { c[i] += c[i - 1]; } for (int i = arr.length - 1; i >= 0; i--) { int d = getBitData(arr[i], expIndex); b[c[d] - 1] = arr[i];// C[d]-1 就代表小于等于元素d的元素个数,就是d在B的位置 c[d]--; } return b; } // 获取data指定位的数 private static int getBitData(int data, int expIndex) { while (data != 0 && expIndex > 0) { data /= 10; expIndex--; } return data % 10; } public static int[] countSort(int[] a){ int b[] = new int[a.length]; int max = a[0],min = a[0]; for(int i:a){ if(i>max) max=i; if(i<min) min=i; }//这里k的大小是要排序的数组中,元素大小的极值差+1 int k=max-min+1; int c[]=new int[k]; for(int i=0;i<a.length;++i){ c[a[i]-min]+=1;//优化过的地方,减小了数组c的大小 } for(int i=1;i<c.length;++i){ c[i]=c[i]+c[i-1]; } for(int i=a.length-1;i>=0;--i){ b[--c[a[i]-min]]=a[i];//按存取的方式取出c的元素 } return b; }
特征分析:
1、基数排序法是属于稳定性的排序。
2、时间复杂度为O
(nlog(k)d),其中d为所采取的基数,而k为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
桶排序
基本思想:
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
简单桶排序实现
//假如排序的数据范围为(0-100),学生成绩 public static void bucketSort(int[] arr){ List<List<Integer>> buckets=new ArrayList<List<Integer>>(); buckets.add(new ArrayList<Integer>());//成绩0-59的桶 buckets.add(new ArrayList<Integer>());//成绩60-69 buckets.add(new ArrayList<Integer>());//成绩70-79 buckets.add(new ArrayList<Integer>());//成绩80-100 for(int i:arr){ if(i>=0&&i<=59) buckets.get(0).add(i); if(i>=60&&i<=69) buckets.get(1).add(i); if(i>=70&&i<=79) buckets.get(2).add(i); if(i>=80&&i<=100) buckets.get(3).add(i); } for(List<Integer> bucket:buckets){ Collections.sort(bucket); } int index=0; for(List<Integer> bucket:buckets){ for(int i:bucket){ arr[index++]=i; } } }
特征分析:
1、对于N个待排数据,M个桶,如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。
2、桶排序是稳定的(如果桶内数据的排序算法选择的不是稳定的,桶排序就变成不稳定的)。
适用场景:
1、输入数据符合均匀分布。
2、在面试的海量数据处理题目中,如对每天数以亿计的数据进行排序,直接排序即使采用nlgn的算法,依然是一件很恐怖的事情,内存也无法容纳如此多的数据,这时桶排序就可以有效地降低数据的数量级,再对降低了数量级的数据进行排序,可以得到比较良好的效果。
面试题
public class Solution { public boolean isAnagram(String s, String t) { if(s.length()!=t.length()) return false; char[] a=s.toCharArray(); char[] b=t.toCharArray(); Arrays.sort(a); Arrays.sort(b); boolean res; for(int i=0;i<a.length;i++){ if(a[i]!=b[i]){ return false; } } return true; } }
分析
题目中指明不能利用类库中的排序函数,即使允许效率也是低下的。
由于元素的值是确切的三种类型,0、1、2。我们可以利用快速排序的划分算法,每次划分就可以解决一个数值的元素的排序。
public class Solution { public int partition(int[] nums,int value,int begin,int end){ int p=begin,q=end; while(p<q){ while(p<=end&&nums[p]==value)p++; while(q>=begin&&nums[q]>value)q--; if(p<q){ int t=nums[p];nums[p]=nums[q];nums[q]=t; } } return p; } public void sortColors(int[] nums) { if(nums.length<=1) return; int nextBegin=partition(nums,0,0,nums.length-1); if(nextBegin<nums.length){ partition(nums,1,nextBegin,nums.length-1); } } }
分析
链表只具有顺序存取特性,只能使用简单插入排序。
public class Solution { public ListNode insertionSortList(ListNode head) { ListNode myHead=new ListNode(-1),p; while(head!=null){ p=head; head=head.next; insertElement(myHead,p); } return myHead.next; } private void insertElement(ListNode myHead,ListNode p){ ListNode pre=myHead,post=myHead.next; while(post!=null&&post.val<=p.val){ pre=post; post=post.next; } p.next=post; pre.next=p; } }
分析
由于要求时间复杂度为O(n log n),空间复杂度O(1)。那么我们想到符合这样的复杂度的排序算法只可能为堆排序、归并排序。
由于空间复杂度为O(1)的堆排序,要求随机存取特性,而链表显然不具有这样的特性,因此只能使用归并排序。
public static ListNode sortList(ListNode head) { int length=0; ListNode p=head; while(p!=null){ length++; p=p.next; } if(length<=1) return head; ListNode myHead=new ListNode(-1); myHead.next=head; mergerSort(myHead,length); return myHead.next; } private static void mergerSort(ListNode myHead,int length){ ListNode pre,start,end; int size=1; while(size<length){ pre=myHead; while(pre.next!=null){ start=pre.next; pre.next=merge(start,size); int count=size*2; while(count>0&&pre!=null){ pre=pre.next; count--; } if(count>0||pre==null||pre.next==null) break; } size*=2; } } private static ListNode merge(ListNode start,int size){ ListNode myHead=new ListNode(-1),end=myHead,second,last; int count=size; second=start; while(count>0&&second!=null){ second=second.next; count--; } if(count>0) return start;//长度不够一个size count=size; last=second; while(count>0&&last!=null){ last=last.next; count--; } ListNode p=start,q=second,t; while(p!=second||q!=last){ if(q==last||(p!=second&&p.val<=q.val)){ t=p; p=p.next; t.next=null; end.next=t; end=end.next; }else{ t=q; q=q.next; t.next=null; end.next=t; end=end.next; } } end.next=last; return myHead.next; }
public class Solution { public String largestNumber(int[] nums) { if (nums == null || nums.length == 0) return ""; String[] strs = new String[nums.length]; for (int i = 0; i < nums.length; i++) { strs[i] = nums[i]+""; } Arrays.sort(strs, new Comparator<String>() { @Override public int compare(String i, String j) { String s1 = i+j; String s2 = j+i; return s2.compareTo(s1); } }); //降序,直接比较i+j和j+i,就可以判断哪个应该放在前面 if (strs[0].charAt(0) == '0') return "0";//最大为0,结果必为0 StringBuilder res = new StringBuilder(); for (String s:strs) { res.append(s); } return res.toString(); } }