概述:
本文主要讨论的是在内存中完成的排序工作 即我们所说的内排序,本文不涉及具体算法的讲解。
基于比较的排序的最坏时间不小于nlogn,可用决策树证明。
1.简单选择排序
基本思想:
找到数组中最小的元素,其次将它和数组的第一个元素交换,在剩下的元素找到最小的元素,将它与数组的第二个元素交换位置,如此往复,直到整个数组排序。
命题:
对于长度为N的数组,选择排序大约需要N^2/2次比较和N次交换。
特点:
1.运行时间与输入无关。一个已经有序的数组和一个元素随机排列的数组所用的时间一样长。
2.数据移动是最少的。
效率:
最好时间 O(n^2)
最坏时间 O(n^2)
空间 O(1)
不稳定
代码:
public class SelectSort {
public static void sort(int[] array){
for(int i=0;i<array.length;i++){
int selected=i;
for(int j=i+1;j<array.length;j++){
if(array[j]<array[selected])
selected=j;
}
int temp=array[i];
array[i]=array[selected];
array[selected]=temp;
}
}
}
2.堆排序
基本思想:
二叉堆:一棵二叉树的每个节点都大于(或小于)等于它的两个子节点。二叉堆是一棵完全二叉树,我们可以用数组完美表示。
由下而上的堆有序化(上浮):
时间:logN
如果堆的有序状态因为某个节点变得比它的父节点更大二被打破,那么我们就需要通过交换它和它的父节点来修复堆。
private void swim(int k){
while(k>1&&less(k/2,k)){//less()比较数组中两个数的大小
exch(k/2,k);//交换数组中的两个数
k=k/2;
}
}
由上而下的有序化(下沉):
时间:logN
如果堆的有序状态因为某个节点变得比它的两个子节点或者其中之一更小而打破了,那么我们可以通过将它和它的两个子节点中较大的节点交换来恢复堆。
private void sink(int k){
while(2*k<=N){//N为数组可用长度
int j=2*k;
if(j!=N-1&&less(j,j+1)) j++;
if(!less(k,j)) break;
exch(k,j);
k=j;
}
}
插入元素:
我们将元素加到数组末尾,增加堆的大小并让新元素上浮到合适位置。
删除元素:
我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。
命题:
一棵大小为N的完全二叉树的高度为[logN]。
用下沉操作由N个元素的构造堆只需少于2N次比较以及少于N次比较。
效率:
最好时间 O(n*logn)
最坏时间 O(n*logn)
空间 O(1)
不稳定
代码:
public class HeapSort {//最好最坏平均都是O(n*logn) 空间复杂度O(1) 不稳定 例子: 10 12 24 12;10输出后12代替10的位置
public static void sort(int [] array){
for(int i=array.length/2;i>=0;i--){//构建堆
down(array,i,array.length);
}
for(int i=array.length-1;i>0;i--)
{
swap(array,0,i);
down(array,0,i);
}
}
public static void swap(int array[],int m,int n){
int temp=array[m];
array[m]=array[n];
array[n]=temp;
}
public static void down1(int array[],int i,int n){
int child;
int temp;
for(temp=array[i];2*i+1<n;i=child){
child=2*i+1;
if(child!=n-1&&array[child]<array[child+1])
child++;
if(array[child]>temp)
array[i]=array[child];
else
break;
}
array[i]=temp;
}
public static void down(int[] array,int i,int n){
while(2*i+1<n){
int child=2*i+1;
if(child!=n-1&&array[child]<array[child+1])
child++;
if(array[child]<=array[i])
break;
swap(array,child,i);
i=child;
}
}
}
直接插入排序
基本思想
将当前元素插入到已经有序的数组中适当位置处,其后面的元素都要向后移动一位。
命题:
对于随机排列长度为且主键不重复的数组,平均情况下插入排序需要~N^2/4比较和~N^2/4次交换,最坏情况下需要~N^2/2比较和~N^2/2次交换,最好情况需要N-1次比较和0次交换。
插入排序需要的交换操作和数组中的逆序对的数量相同。
特点:
插入排序对于某些类型的非随机数组很有效哦,即对部分有序的数组,
效率:
最坏时间 O(N^2)
最好时间 O(N)
空间 O(1)
稳定
代码:
public class InsertionSort {
public static void sort(int[] array){
for(int i=0;i<=array.length-1;i++){
int j;
int temp=array[i];
for(j=i-1;j>=0&&temp<array[j];j--)
array[j+1]=array[j];//后移一位
array[j+1]=temp;
}
}
}
4.希尔排序
基本思想:
传统数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移到另一端。
希尔排序的基本思想是使数组中任意间隔为h的数组都是有序的。在进行排序时,如果h很大,我们就能将元素移动到很远的地方。实现希尔排序的一种方法就是对于每个h,用插入排序将h个子数组独立的排序,然后不断的减小间隔,重复排序直到数组有序。
递增序列的选择:
使用Hibbard增量:1,4,13,40,121,344... 最坏的运行时间为O(N^(3/2));
代码:
public static void sort1(int[] array){
int increment=1;//设置增量
while(increment<array.length/3) increment=increment*3+1;//1,4,13,40,121,364..
for(;increment>0;increment/=3){
for(int i=increment;i<array.length;i++){
int j;
int temp=array[i];
for(j=i;j>=increment&&temp<array[j-increment];j-=increment){
array[j]=array[j-increment];
}
array[j]=temp;
}
}
}
特点:
对于中大型数据都有不错的效率。
效率:
最好时间:N*logN
最坏时间:N^2
空间 O(1)
并不稳定
代码:
public class ShellSort {
public static void sort(int array[]){
for(int increment=array.length/2;increment>0;increment/=2){//增量递减
for(int i=increment;i<array.length;i++){//遍历数组
int j;
int temp=array[i];
for(j=i;j>=increment&&temp<array[j-increment];j-=increment){//对每个增量组内的元素进行插入排序;
array[j]=array[j-increment];
}
array[j]=temp;
}
}
}
}
5.冒泡排序
基本思想:
临近的数字进行两两比较,从头到尾按照从大到小或者从小到大的顺序交换,每一趟交换之后最大或最小的元素放在了最后。然后从头开始重复以上操作,直至数组有序。
效率:
最好时间O(n)
最坏时间O(N^2)
空间O(1)
稳定
代码:
public class BubbleSort {
public static void sort(int[] array){//非改进算法,最好最坏都是O(n^2),只是每次认为有一个最大数排到最高位。
for(int i=0;i<array.length-1;i++){//遍历次数
for(int j=0;j<array.length-1-i;j++){
if(array[j]>array[j+1]){
int temp=array[j+1];
array[j+1]=array[j];
array[j]=temp;
}
}
}
}
public static void envoleSort(int[] array){//改进后的算法,最坏O(n^2),最好O(n),每次遍历要记录最后交换的位置
int place=array.length-1;
for(int i=0;i<array.length-1;i++){
int index=0;
for(int j=0;j<place;j++){
if(array[j]>array[j+1]){
int temp=array[j+1];
array[j+1]=array[j];
array[j]=temp;
index=j;
}
}
place=index;
if(index==0)
break;
}
}
}
6.快速排序
基本思想:
快速排序是一种分治的排序算法,它将一个数组分成两个子数组,然后独立的排序,切分的位置影响着排序的效率。
策略:
随机的选取a[lo]作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描找到一个大于等于它的元素,再从数组的右端开始向左扫描找到一个小于等于它的元素,交换他们的位置,如此继续。这样当两个指针相遇时,左指针左侧的元素都不大于切分元素,右指针右侧的元素都不小于切分元素。
注意:
左侧扫描最好是在遇到大于等于切分元素时停下,右侧扫描最好是在小于等于切分元素时停下,尽管这样可能会造成不必要的值的交换,但在某些应用中,我们能避免算法的运行时间变成平方级别。
快速排序在切分不均匀的时候这个程序极为低效。
特点:
快排是通用的排序算法,使用大型随机数据。
效率:
最好时间:N*logN
最坏时间:N^2
平均时间:N*logN
最好空间:O(logN)
最坏空间:O(N)
不稳定
步骤:
1) 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot);
2) 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大;
3) 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素;
基准元的选择:
1)固定基准元,只选取区域的第一个作为基准元。最好O(nlogn) 最坏O(n^2)public class Sort {
public void sort(int[] arr,int low,int high){
if(low>=high)
return;
int k=partition(arr,low,high);
sort(arr,low,k-1);
sort(arr,k+1,high);
}
public int partition(int[] arr,int low,int high){//分割函数
/*
* 1.递归的基准情形:i=L;j=R; a[i]为key
* 2.j--向前寻找比他小的数,找到后将次数填到a[i]
* 3.i++向后找比他大的数,找到后将数填到a[j]
* 4.当i==j时退出
*/
int i=low;
int j=high+1;
int key=arr[low];
while(true){
while(arr[++i]<key)if(i==high)break;
while(arr[--j]>key)if(j==low)break;
if(i>=j)break;
swap(arr,i,j);
}
swap(arr,low,j);
return j;
}
public void swap(int[] arr,int x,int y){
int temp=arr[x];arr[x]=arr[y];arr[y]=temp;
}
}
2)随机基准元:取待排序列中任意一个元素作为基准元。随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度
public void sort(int[] arr,int low,int high){
if(low>=high)
return;
random(arr,low,high);
int k=partition(arr,low,high);
sort(arr,low,k-1);
sort(arr,k+1,high);
}
public void random(int[] arr,int low,int high){
Random random=new Random();
int ranindex=random.nextInt(high-low)+low;
swap(arr,ranindex,low);
}
3)三元取中:一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。
public void sort(int[] arr,int low,int high){
if(low>=high)
return;
medianOfThree(arr,low,high);
int k=partition(arr,low,high);
sort(arr,low,k-1);
sort(arr,k+1,high);
}
public void medianOfThree(int[]arr,int low,int high){
int mid=(low+high)/2;
if(arr[mid]>arr[high]){
swap(arr,mid,high);
}
if(arr[low]>arr[high]){
swap(arr,low,high);
}
if(arr[mid]>arr[low]){
swap(arr,mid,low);
}
}
两种优化方式:
优化一:当待排序序列的长度分割到一定大小后,使用插入排序
public void sort(int[] arr,int low,int high){
if(high-low+1<10){
insert(arr,low,high);
return;
}
medianOfThree(arr,low,high);
int k=partition(arr,low,high);
sort(arr,low,k-1);
sort(arr,k+1,high);
}
优化二:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
public void sortThird(int[] array,int first,int last){
if(first>=last)
return;
int lt=first;
int i=lt+1;
int gt=last;
int key=array[first];
while(i<=gt){
if(array[i]>key)
swap(array,i,gt--);
else if(array[i]<key)
swap(array,lt++,i++);
else
i++;
}
sortThird(array,first,lt-1);
sortThird(array,gt+1,last);
}
非递归的快速排序:
public void sort(int[] arr,int low,int high){
if(low>=high)return;
Stack<Integer> stack=new Stack<>();
stack.push(low);
stack.push(high);
while(!stack.isEmpty()){
int right=stack.pop();
int left=stack.pop();
int p=partition(arr,left,right);
if(p-1>left){
stack.push(left);
stack.push(p-1);
}
if(p+1<right){
stack.push(p+1);
stack.push(right);
}
}
}
7.归并排序
基本思想:
归并排序是典型的分而治之的思想,递归地将数组切分成两部分,将切分后的数组排序,然后合并两个排序数组。
命题:
对于长度为N的任意数组,自顶向下的归并排序需要1/2N*lgN~N*logN次比较
递推公式:
C(n)=2C(n/2)+n ---->C(n)=n*logn+n
特点:
归并排序以O(NlogN)的最坏时间运行,所使用的比较次数几乎是最优的,也是稳定的算法。
效率:
时间 O(n*logn)
空间 O(n)
稳定
代码:
public class MergeSort {//n*logn 额外空间 n
public static void sort(int array[]){
int arr[]=new int[array.length];//临时数组
mergeSort(array,arr,0,array.length-1);
}
public static void mergeSort(int array[],int tempArray[],int low,int high){//切分
if(low<high){
int mid=(low+high)/2;
mergeSort(array,tempArray,low,mid);
mergeSort(array,tempArray,mid+1,high);
merge(array,tempArray,low,high);
}
}
public static void merge(int array[],int tempArray[],int left,int right){//归并
int leftend=(left+right)/2;
int rightpos=leftend+1;
int temppos=left;
int numberelements=right-left+1;
while(left<=leftend&&rightpos<=right){//
if(array[left]<array[rightpos])
tempArray[temppos++]=array[left++];
else
tempArray[temppos++]=array[rightpos++];
}
while(left<=leftend)//拷贝剩下的
tempArray[temppos++]=array[left++];
while(rightpos<=right)//拷贝剩下的
tempArray[temppos++]=array[rightpos++];
for(int i=0;i<numberelements;i++,right--){
array[right]=tempArray[right];
}
}
线性排序
计数排序:
基本思想:
计数排序适用于元素大小在一定范围内的数组,它创建一个长度为这个数据范围的数组C,C中每个元素记录要排序数组中对应记录的出现个数。
效率:
时间O(n)
空间O(n)
稳定
代码:
public class CountSort {//通过count数组找到元素的真正位置
public static void sort(int[] array,int range){
int[] count=new int[range];
int[] newArray=new int[array.length];
for(int i=0;i<array.length;i++){//统计每个元素出现的次数
count[array[i]]++;
}
for(int i=1;i<count.length;i++){//统计某个元素之前的元素的个数,即找到元素的正确位置
count[i]+=count[i-1];
}
for(int i=newArray.length-1;i>=0;i--){
count[array[i]]--;
newArray[count[array[i]]]=array[i];//将数组放在正确的位置上
}
for(int i=0;i<array.length;i++){//拷贝回原数组
array[i]=newArray[i];
}
}
}
桶排序
基本思想:
假设输入数据服从均匀分布,平均情况下它的时间代价为O(n)。桶排序将[0,1)区间划分为n个大小相同的子区间,或称为桶。
将n个输入分别放到各个桶中,因为输入是均匀,独立地分布在[0,1]区间上,所以一般不会出现很多数落在同一个桶的情况。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个每个桶,按照次序把每个桶中的元素列出来即可。
效率:
对于N个待排数据,M个桶,平均每个桶[N/M]个数据
桶排序平均时间复杂度为:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM) M越大,效率越高,空间复杂度越高
空间:O(N+M)
稳定
代码:
左图展示了在一个包含10个元素的输入数组上的桶排序过程。
public class BucketSort {
public static void sort(int[] array){//假定输入为[0,100)的随机数
List[] bucket=new ArrayList[array.length];//创建一个等长的桶
for(int i=0;i<array.length;i++){//遍历数组,把数据放进桶里
int temp=array[i]/10;
if(bucket[temp]==null)
bucket[temp]=new ArrayList<Integer>();
bucket[temp].add(array[i]);
}
for(int i=0;i<bucket.length;i++) {//对每个桶进行排序
if (bucket[i] != null)
/*
Collections.sort()的内部实现是Arrays.sort();
Arrays.sort()实现由两种组成: 1.归并排序 2.二分插入排序
*/
Collections.sort(bucket[i]);
}
int index=0;
for(int i=0;i<bucket.length;i++){//遍历每个桶,得到排序后的数组
if(bucket[i]!=null){
for(Object num:bucket[i]){
array[index++]=(Integer)num;
}
}
}
}
}
基数排序
基本思想:
基数排序是一个“分配”和“收集”的算法。
基数排序分为LSD(Least significant digital)或MSD(Most significant digital),
LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。
效率:
设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的
空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
例子:
第一步
第二步
第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
代码:
public class BaseSort {
public static void sort(int[] number, int d) //d表示最大的数有多少位
{
int k = 0;
int n = 1;
int m = 1; //控制键值排序依据在哪一位
int[][]temp = new int[10][number.length]; //数组的第一维表示可能的余数0-9
int[]order = new int[10]; //数组orderp[i]用来表示该位是i的数的个数
while(m <= d)
{
for(int i = 0; i < number.length; i++)
{
int lsd = ((number[i] / n) % 10);
temp[lsd][order[lsd]] = number[i];
order[lsd]++;
}
for(int i = 0; i < 10; i++)
{
if(order[i] != 0)
for(int j = 0; j < order[i]; j++)
{
number[k] = temp[i][j];
k++;
}
order[i] = 0;
}
n *= 10;
k = 0;
m++;
}
}
}
用哪种算法
(转)
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的,
归并排序:内存空间允许,且要求稳定性。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或二分插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
4)一般不使用或不直接使用传统的冒泡排序。