最优 O(N)
最差 O(N^2)
平均 O(N^2)
空间 O(1)
稳定排序
插排的思路是保证遍历到每个元素时,该元素前面的序列都是有序的。基于这个前提,我们将遍历到的新元素和它前面的序列相比对,然后将这个元素插入到适当的位置,使得这个包含新元素的序列也是有序的。虽然外层遍历只要O(N)时间,但是因为找到新元素的适当位置后要将之后的所有元素后移,所以最差时间是O(N^2)。
public class InsertionSort<T extends Comparable> { public T[] sort(T[] a){ T[] array=a; T tmp; for(int i = 1; i<a.length; i++){ tmp=array[i]; int j; // 从后向前找到插入新元素的适当位置,比新元素大的都后移一位 for(j=i;j>0;j--){ if(array[j-1].compareTo(tmp)<0) break; array[j]=array[j-1]; } // 较大的元素都后移了一位,这里只要插入这个新元素,则新序列还是有序的 array[j]=tmp; } return array; } }
最优 O(N)
最差 O(N^2)
空间 O(1)
不稳定排序
插排的一个优化是希尔排序,在原本的插入排序中,我们选择以一个一个往后看等待插入的元素。然而插入排序有这么一个性质,即如果序列有序程度越高,则时间复杂度越低。所以希尔排序利用了这个性质,先通过较大的步长进行一次插入排序(不再是一个一个往后找待插元素,而是隔一个或者隔n/2个元素来找),这样每次子插排都能将序列变得更有序一点,而且因为步长一开始很大,所以要遍历的数少,时间复杂度也很低。后来步长变小了,要遍历的数变多了,但数组也更有序了,时间复杂度也降低了。当然n/2^i这个步长序列不是最好的,最好的步长序列能使最差复杂度降低至O(N(logN)^2)
public class Shellsort { public static <T extends Comparable<? super T>> T[] sort(T[] a) { int j = 0; // 从n/2开始选择步长,每轮插排后减少步长,直到步长为1是最后一次 for (int gap = a.length / 2; gap > 0; gap /= 2) { T tmp = null; // 步长为gap的插入排序 for(int i = gap; i<a.length; i++){ tmp = a[i]; // 找到合适的插入点 for(j=i;j>=gap&&tmp.compareTo(a[j-gap])<0;j-=gap){ a[j]=a[j-gap]; } a[j]=tmp; } } return a; } }
最优 O(N^2)
最差 O(N^2)
平均 O(N^2)
空间 O(1)
不稳定排序
选排的思路和插排相反,插排是搜索有序序列找到合适的位置,选排则是搜索剩余的无序序列选择出其中最大(或最小)的,并将这个被选择的数放到无序序列的首部。需要注意的是。将这个数放到首部时,用的是交换的方法。因为无论如何都要遍历完子序列,所以时间复杂度至少是O(N^2)
public class SelectionSort<T extends Comparable> { public T[] sort(T[] array) { T[] a = array; T tmp; int minIndex; for (int i = 0; i < a.length; i++) { minIndex = i; // 在剩余无序序列中找到一个最小的 for (int j = i; j < a.length; j++) { if (a[minIndex].compareTo(a[j]) > 0) minIndex = j; } // 将这个最小的交换到无序序列的首部 if (minIndex != i) { tmp = a[i]; a[i] = a[minIndex]; a[minIndex] = tmp; } } return a; } }
最优 O(NlogN)
最差 O(NlogN)
平均 O(NlogN)
空间 O(N)
稳定排序
归并排序是典型的分治法。基本思路是将一个较长数组一分为二,我们将它的左右两半部分分别排好序,然后将这有序的两部分合并起来,这样较长数组也是有序的了。因为两半部分都是有序的,所以合并只需要同时遍历一下两半部分,看谁较小(大)就把谁先塞进数组就可以了。根据这个思路,我们不停的二分、二分,这样二分到尽头肯定只有一个数的数组,那它肯定是有序的,然后我们将两个只有一个数的数组合并,只要比较下这两个数就行了。这样就返回了一个有两个数的有序数组,然后再一直合并、合并、直到合并出整个数组。不过,在合并两个子数组时,要先用一个临时数组存放,否则会修改两个子数组的数据(因为两个子数组实际上是原数组的两个子部分)。
public class Mergesort<T extends Comparable<? super T>> { private static <T extends Comparable<? super T>> T[] sort(T[] array, T[] tmp, int left, int right){ if(left<right){ // 计算二分中点位置 int center = (left+right)/2; // 将左半部分排序 sort(array,tmp,left,center); // 将右半部分排序 sort(array,tmp,center+1,right); // 将左右两半部分合并 merge(array,tmp,left,center+1,right); } return array; } public static <T extends Comparable<? super T>> T[] sort(T[] a){ T[] tmp = (T[]) new Comparable[a.length]; return sort(a,tmp,0,a.length-1); } private static <T extends Comparable<? super T>> void merge(T[] a, T[] tmpArray, int leftPos, int rightPos, int rightEnd){ int leftEnd=rightPos-1; int tmpPos = leftPos; int numElements = rightEnd - leftPos +1; // 将两个有序数组的元素比较大小后合并成一个数组,存入一个临时数组中 while(leftPos<=leftEnd && rightPos<=rightEnd){ if(a[leftPos].compareTo(a[rightPos])<0) tmpArray[tmpPos++]=a[leftPos++]; else tmpArray[tmpPos++]=a[rightPos++]; } // 如果左半部分没有合并完(说明右半部分的数较小),将其合并进去 while(leftPos<=leftEnd){ tmpArray[tmpPos++]=a[leftPos++]; } // 如果右半部分没有合并完(说明左半部分的数较小),将其合并进去 while(rightPos<=rightEnd){ tmpArray[tmpPos++]=a[rightPos++]; } // 将这个临时数组拷贝回原数组 for(int i = 0;i<numElements;i++,rightEnd--){ a[rightEnd]=tmpArray[rightEnd]; } } }
最优 O(NlogN)
最差 O(N^2)
平均 O(NlogN)
空间 O(logN)
不稳定排序
快速排序也是分治的思想,它先选取一个枢纽值pivot,将整个数组按照枢纽值的大小重新排列,比枢纽值小的放在左边,比枢纽值大的放在右边。然后,我们再分别对这左半部分和右半部分进行同样的操作,这里半个部分不一定有半个数组的元素,要看具体有多少元素大于或小于枢纽值。一般情况下我们都选择子数组的第一个值作为枢纽,简化实现。最好还是能写个随机选择枢纽值的函数。最差的情况下,每次选到的枢纽值都是最小(最大)的,所以每次分割出来的子数组都只比当前整个数组小1的长度,就会导致O(N^2)的复杂度。
public class Quicksort { public <T extends Comparable<? super T>> T[] sort(T[] a){ return sort(a,0,a.length); } private <T extends Comparable<? super T>> T[] sort(T[] a, int left, int right){ if(left==right-1) return null; //Partition into two part first int k=partition(a,left,right); //Recurse on front part sort(a,left,k); //Recurse on rear part sort(a,k,right); return a; } private <T extends Comparable<? super T>> int partition(T[] a, int left, int right){ T p = a[left]; int i = left, j =right; // 先找到右边第一个小于枢纽值的数 do{ j--; }while(a[j].compareTo(p)>0 && j>i); //Swap a[i] and a[j] to partition while(i<j){ // 将右边那个小于枢纽值的数和左边那个大于枢纽值的数交换(第一次交换的是枢纽值本身) swap(a,i,j); // 找到下一对可以交换的元素 do{ i++; }while(a[i].compareTo(p)<0); do{ j--; }while(a[j].compareTo(p)>0); } //Elements before j+1 are partitioned, so we need to partition next part start from j+1 return j+1; } private <T extends Comparable<? super T>> void swap(T[] a, int i, int j){ T tmp = a[j]; a[j] = a[i]; a[i] = tmp; } }
假设我们有1TB的数据需要排序,但是只有一台1GB内存的机器,请问如何排序?
外部排序基于归并排序的理念,以该题为例,为了计算方便我们假设TB/GB/MB是以1000换算的,而且内存也有1.001G空间:
将1TB数据分1000次读入内存中,每次对1GB的数据进行排序,每次排序完将其作为一个区块结果存入磁盘中。
在内存中划分1000个通道,每个通道读入每个区块前1MB的数据,可知每个1MB都是有序的。
对这1000个有序的1MB数据进行归并。将结果存入一个1MB的缓存当中,每当这1MB缓存满的时候,将这1MB缓存存入磁盘并清空。每当任何一个1MB通道空时,我们将对应区块的下1MB数据读入这个通道。
持续执行步骤3直到所有区块的数据都被读完,这时候磁盘中就是1TB有序的数据了。