一、插入排序
插入是稳定排序,它的最差运行时间和平均运行时间都为O(N^2)。以反序输入可以达到最差运行时间。插入排序执行的交换次数等于逆序数。空间复杂度O(N)。
//插入排序
public static void insertionSort(int[] a){
int k;
for(int i = 1; i < a.length; i++){
int tmp = a[i];
for(k = i; k > 0 && a[k-1] > tmp;k--)
a[k] = a[k-1];
a[k] = tmp;
}
二、希尔排序
希尔排序通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。所以希尔排序也叫做缩减增量排序。它使用一个序列h1、h2…ht,叫做增量序列。只要h1=1,任何增量序列都是可行的。
使用希尔增量时希尔排序的最坏情形运行时间为Θ(N^2)。对每个独立的子数组执行插入排序。使用Hibbard增量的希尔排序的最坏情形运行时间为Θ(N^(3/2))。
//希尔排序
public static void shellSort(int[] a){
int j;
for(int gap = a.length/2; gap > 0; gap/=2)
for(int i = gap; i < a.length; i++){
int temp = a[i];
for(j = i; j >= gap && a[j-gap] > temp; j-=gap)
a[j] = a[j-gap];
a[j] = temp;
}
}
三、堆排序
执行N次deleteMin操作,最小的元素先离开堆。通过将这些元素记录到第二个数组然后再将数组拷贝回来,得到N个元素的排序。由于每个deleteMin花费时间O(log N),因此总的运行时间是O(N logN)。问题在于它使用了一个附加的数组,存储需求增加一倍。解决办法是每次deleteMin后,堆缩小1,位于堆中最后的单元可以用来存放刚刚删去的元素。
最小堆——-递减顺序 最大堆——递增顺序
堆排序不是稳定排序。
//堆排序
public static int leftChild(int i){
return 2 * i +1;
}
public static void percDown(int[] a, int i, int n){
int tmp = a[i];
int child=0;
for(; leftChild(i) < n; i = child){
child = leftChild(i);
if(child != n-1 && a[child+1] > a[child])
child++;
if(tmp < a[child])
a[i] = a[child];
else
break;
}
a[i] = tmp;
}
public static void swap(int[] a, int i, int j){
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
public static void heapSort(int[] a){
for(int i = a.length/2; i >= 0; i--)
percDown(a,i,a.length); //建堆
for(int i = a.length - 1; i > 0; i--){
swap(a,0,i);
percDown(a,0,i);
}
四、归并排序
归并排序以O(N log N)最坏情形时间运行,而所使用的比较次数几乎是最优的。归并排序采用分治策略。合并两个已排序的表的时间是线性的,因为最多进行N-1次比较。
虽然归并排序的运行时间是O(N logN),但是它有个明显的问题,即合并两个已排序的表用到线性附加内存。算法需要花费将数据拷贝到临时数组再拷贝回来这样一些附加的工作,减慢了排序的速度。归并排序的运行时间严重依赖于比较元素和在数组中移动元素的相对开销。这些开销是与语言相关的。
Java中,执行一次泛型排序时,进行一次元素比较可能是昂贵的,因为比较可能不容易被内嵌,从而动态调度的开销可能会减慢执行的速度,但是移动元素则是省时的,因为它们是引用的赋值,而不是庞大对象的拷贝。归并排序使用流行的排序算法中最少的比较次数,所以标准java类库中泛型排序使用归并算法。c++的泛型排序中,如果对象庞大,拷贝对象可能需要很大开销,而由于编译器具有主动执行内嵌优化的能力,比较对象常常是相对省时的。这种情形下,如果能够使用更少的数据移动,有理由让算法多使用一些比较。快排能达到这种权衡,是C++类库中通常使用的排序例程。
//私有函数,实现分治策略
private static void mergeSort(int[] a,int[] tmpArr, int left, int right ){
if(left < right){//这个比较不能忘
int center = (right + left)/2;
mergeSort(a, tmpArr, left, center);
mergeSort(a,tmpArr, center+1, right);
merge(a, tmpArr, left, center+1, right);
}
}
public static void mergeSort(int[] a){
int[] tmpArr = new int[a.length];
mergeSort(a, tmpArr, 0, a.length-1);
}
//合并操作,需要一个临时数组,增加线性空间,数组可以在公有的mergeSort函数中创建
public static void merge(int[] a, int[] tmpArr, int left, int rightStart, int right){
int leftEnd = rightStart - 1;
int tmpPos = left;
int num = right - left + 1;
while(left <= leftEnd && rightStart <= right ){
if(a[left] <= a[rightStart])
tmpArr[tmpPos++] = a[left++];
else
tmpArr[tmpPos++] = a[rightStart++];
}
while(left <= leftEnd) //剩下的都是左边数组
tmpArr[tmpPos++] = a[left++];
while(rightStart <= right) //剩下的都是右边数组
tmpArr[tmpPos++] = a[rightStart++];
for(int i = 0; i < num; i++,right--) //拷贝回来
a[right] = tmpArr[right];
}
五、快速排序
快排是在实践中一种快速的排序算法,在C++或对Java基本类型的排序中特别有用。它的平均运行时间是O(N log N)。它之所以特别快,是因为非常精炼和高度优化的内部循环。它的最坏情况性能为O(N^2),但经过少许努力可使这种情形很难出现。
快速排序也是一种分治的、递归算法。将数组S排序的基本算法由下列简单的四步组成:
- 如果S中元素个数是0或1,则返回
- 取S中任一元素v,称之为枢纽元
- 将S中其余算法划分成两个不相交的集合:S1和S2,其中S1的元素≤v,S2中的元素≥v。
- 返回quicksort(S1)后跟v,继而返回quicksort(S2)
快速排序递归地解决两个子问题并需要线性的附加工作。但是这两个子问题并不保证具有相等的大小,四个潜在的隐患。快排更快的原因在于,第3步分割成两组实际上是在适当的位置进行并且非常有效,它的高效不仅弥补大小不等的递归调用的不足而且还能有所超出。
选取枢纽元的方法
- 一种错误的方法–将第一个元素用作枢纽元。反序的时候会使得每次都产生最坏划分
- 安全的做法—随机选取枢纽元。一般来说是安全的,但是随机数的生成开销一般很大,减少不了算法其余部分的平均运行时间
- 三数中值分割法–使用左端、右端和中心位置上的三个元素的中值作为枢纽元。这种方法还有额外的好处,即三者的最小者放在了a[left],这正是分割阶段应该将它放到的位置。三者的最大者放在a[right]也是正确的位置。可以把枢纽元放在a[right-1]并在分割阶段将i和j初始化为left+1和right-2。a[left]比枢纽元小,所以将它用作j的警戒标记。不必担心j跑过端点。i将停在那些等于枢纽元的关键字处,所以枢纽元存储在a[right-1]也提供了一个警戒标记。
小数组
对于很小的数组,N≤20,快速排序不如插入排序。通常的解决方法是对于小的数组不使用递归的快速排序,而是用插入排序这样对小数组有效的排序算法。
//快速排序与插入排序的结合
public class QuickSort {
private static final int CUTOFF = 10;//这里选择截止范围是10,当数组大小小于10的时候不用快排,用插入
//覆盖的公用函数
public static void quicksort(int[] a){
quicksort(a,0,a.length-1);
}
public static void swap(int[] a, int i, int j){
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
//采用三数中值分割法
public static int median(int[] a, int left, int right){
int center = (left + right)/2;
if(a[center] < a[left]) swap(a, center, left);
if(a[right] < a[left]) swap(a, right, left);
if(a[center] > a[right]) swap(a, center, left);
swap(a,center, right-1);//把枢纽元放在right-1的位置
return a[right-1];//返回枢纽元的值
}
//快排核心函数
private static void quicksort(int[] a, int left, int right){
if(left + CUTOFF <= right){
int pivot = median(a,left,right);
int i = left;
int j = right -1;
for(;;){
while(a[++i] < pivot){}
while(a[++j] > pivot){}
if(i < j ) swap(a,i,j);
else
break;
}
swap(a,i,right-1); //将主元放到位置i
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
else
insertionSort(a,left,right);//在规模小于等于10的子数组上进行插入排序
}
//插入排序函数
public static void insertionSort(int[] a, int left, int right){
int k;
for(int i = left+1; i <= right; i++){
int tmp = a[i];
for(k = i; k > 0 && a[k-1] > tmp;k--)
a[k] = a[k-1];
a[k] = tmp;
}
}
public static void main(String[] args){
int[] a = {6,5,7,3,1,9,8};
quicksort(a);
for(int i :a)
System.out.print(i + " ");
}
}
(6)桶排序
(7)选择排序
(8)计数排序
(9)基数排序
(10)冒泡排序
(11)外部排序
总结
稳定排序:冒泡、插入、归并、基数、计数、桶
不稳定排序:选择、快排、希尔、堆
排序算法的一般下界
对只用到比较的任意排序算法都需要Ω(NlogN)次比较,并平均需要log(N!)次比较。