排序

来源:互联网 发布:cygwin linux 编辑:程序博客网 时间:2024/04/29 16:30

基于比较的排序。排序根据数据量的不同分内存排序和外部排序。但数据量大得不能放在内存的时候就需要用外部排序。内存排序有:冒泡排序、插入排序、希尔排序、堆排序、归并排序、快速排序。

冒泡排序

冒泡排序第一次保证第1位是最小元素,第二次保证第2位是次小元素。在不断交换过程中可能把较小的元素交换到后面的位置,在后面的排序中又交换到前面。时间复杂度O(N2)。

public static <T extends Comparable<? super T>> void bubbleSort(T[] a) {    for (int i = 0; i < a.length; i++) {        for (int j = i; j < a.length; j++) {            if(a[i].compareTo(a[j])>0){                T tmp = a[i];                a[i] = a[j];                a[j] = tmp;            }        }    }}

插入排序

插入排序由N-1趟排序组成。对于p=1到(N-1)趟。在第p趟排序的时候,保证从0到p的元素已经是排序好的。将位置p上的元素element与 [0,p-1]上的元素比较,比element大的元素右移,找到合适的位置插入element。 时间复杂度O(N2)。
这里写图片描述
令 nums = {34,8,64,51,32,21},做升序排序。
  p=1 nums[1] 在 0 和 1 之间给 nums[1]安排合适的位置。
  tmp = nums[1]=8,因为遍历过程中可能修改nums[p]的值,所以先记下来。
   idx = p-1=0,idx >=0,nums[idx] >tmp (34>8 ),移动nums[idx+1]=nums[idx];
   idx=p-2=-1,idx<0,退出;
   nums[idx+1]=tmp。完成此次循环。
  p=2 nums[2] 在0 和 2之间给nums[2]安排合适的位置。
   tmp = nums[p]=64;
   idx = p-1=1,idx>=0, nums[idx]<tmp (34<64),不交换;
   idx = p-2=0,idx>=0,nums[idx]<tmp (8<64),不交换。
   idx = p-3=-1,idx<0,退出。
   nums[idx+1]=tmp 就不对了。此时应该是nums[p]=tmp。如果idx=p-1的话,那么nums[idx+1]=tmp就没有问题了。所以在做出判断 idx=p-1,不交换之后就应该退出循环,因为 数组下标:p-1 ,p-2,p-3,….0 数组是递减的。当需要判断nums[2]的位置的时候应该认为从0到1是已经排序好的。这点很重要。那么对上面的排序再次推理。
   
  p=1 nums[1] 在 0 和 1 之间给 nums[1]安排合适的位置。
   tmp = nums[1]=8,因为遍历过程中可能修改nums[p]的值,所以先记下来。
   idx = p-1=0,idx >=0,nums[idx] >tmp (34>8 ),交换nums[idx+1]=nums[idx];
   idx=p-2=-1,idx<0,退出;
   nums[idx+1]=tmp。完成此次循环。
  p=2 nums[2] 在0 和 2之间给nums[2]安排合适的位置。
   tmp = nums[p]=64;
   idx = p-1=1,idx>=0, nums[idx]<tmp (34<64),不交换,退出;
   nums[idx+1]=tmp。完成此次循环。
  p=3, nums[3]在0和3之间安排合适的位置。
   tmp=nums[p] = 51;
   idx=p-1=2,nums[idx]>tmp, (64>51)交换;
   idx=p-2=1,nums[idx] <tmp, (34<51)不交换,退出;
   nums[idx+1]=tmp。完成此次循环。
  p=4
   ...
  p=5
   ...
  ...
  p=nums.length-1
  

public static <T extends Comparable<? super T>> void insertionSort(T[] a) {        for (int p = 1; p < a.length; p++) {            T tmp = a[p];            int idx = p - 1;            for (; idx >= 0 && tmp.compareTo(a[idx]) < 0; idx--) {                a[idx + 1] = a[idx];            }            a[idx + 1] = tmp;        }    }

希尔排序

希尔排序使用一个增量序列 h1,h2,...ht。选择合适的增量序列,会加快排序速度。例如使用Hibbard增量的希尔排序最快情形是 N3/2。希尔排序不再只是交换相邻的两个元素。

这里写图片描述
例如对数组nums={81,94,11,96,12,35,17,95,28,58,41,75,15} 升序排序。
 如果间隔=5,分别对下标 0,5,10; 1,6,11; 2,7,12; 3,8; 4,9 5个子数组分别进行插入排序。分别得到结果:
  下标 0,5,10对应的值:81,35,41—->35,41,81
  下标 1,6,11对应的值:94,17,75—->17,75,94
  下标 2,7,12对应的值:11,95,15—->11,15,95
  下标 3,8对应的值:96,28—->28,96
  下标 4,9 对应的值:12,58—->12,58
 对上面排序好的数组,再次间隔=3,分别对下标 0,3,6,9,12; 1,4,7,10; 2,5,8,11 3个子数组分别进行插入排序
  下标 0,3,6,9,12对应的值:35,28,75,58,95—->28,35,58,75,95
  下标 1,4,7,10对应的值:17,12,15,81—->12,15,17,18
  下标 2,5,8,11对应的值:11,41,96,94—->11,41,94,96
 对上面排序好的数组,再次间隔=1,进行插入排序,得到排序好的数组。
在一般实现中增量序列使用ht=[N/2]hk=[hk+1/2]。但绝对不是最好的序列。

public static <T extends Comparable<? super T>> void shellSort(T[] a) {for (int gap = a.length / 2; gap > 0; gap /= 2) {// gap = 6,3,1    // i =[0,1,2,...gap-1]    for (int i = 0; i < gap; i++) {        for (int p = gap + i; p < a.length; p += gap) {            T tmp = a[p];            int idx = p - gap;            for (; idx >= 0 && tmp.compareTo(a[idx]) < 0; idx = idx - gap) {                a[idx + gap] = a[idx];            }            a[idx + gap] = tmp;        }    }}}

看到插入排序那段是把上面代码中 +1 -1 部分修改为 +gap -gap。
上面有4层循环,可以进一步简写为:

public static <T extends Comparable<? super T>> void shellSort2(T[] a) {    for (int gap = a.length / 2; gap > 0; gap /= 2) {// gap = 6,3,1        // i =[0,1,2,...gap-1]        for (int p = gap; p < a.length; p++) {        //50比较;61比较... 105,0比较;116,1比较            T tmp = a[p];            int idx = p - gap;            for (; idx >= 0 && tmp.compareTo(a[idx]) < 0; idx = idx - gap) {                a[idx + gap] = a[idx];            }            a[idx + gap] = tmp;        }    }}

引用思路和代码:http://blog.csdn.net/mfcdestoryer/article/details/7025273

要记住的一些结论:希尔排序的最坏情形是O(n2)。使用Hibbard增量的希尔排序最快情形是 N3/2

堆排序

堆排序与前面优先队列中讲到的最小堆一样。如果要对数组从小到大排序,将N个数构建一个最大堆。删除根节点得到最大值,删除后堆size-1,使用最后一个位置存放最大元素。接着删除堆中的最大元素,得到N个数的次大元素,放入倒数第2的位置。整个堆size=0,也就从小到大排序好了。在排序中数组下标从0开始,所以左子结点是2*i+1。
堆排序的分析:第一阶段构建堆最多使用2N次比较。第二阶段,第i次deleteMax最多用2[logi]次比较,总数是2NlogN-O(N)次比较。最坏情形下堆排序最多2NlogN-O(N)次比较。

归并排序

算法描述

归并排序以O(NlogN)的最坏情况运行。比较次数也几乎是所有算法中最优的。归并是将两个已经排序好的表,合并成一个排序好的表。算法描述为:如果N=1,排序完成。否则,递归的将前半部分、后半部分排序,合并两部分。

public static <T extends Comparable<? super T>> void mergeSort(T[] a) {    Object[] tmpArray = new Object[a.length];    mergeSort(a, tmpArray, 0, a.length-1);}private static <T extends Comparable<? super T>> void mergeSort(T[] a, Object[] tmpArray, int left, int right) {    if (left < right) {        int center = (left + right) / 2;        mergeSort(a, tmpArray, left, center);        mergeSort(a, tmpArray, center + 1, right);        merge(a, tmpArray, left, center + 1, right);    }}private static <T extends Comparable<? super T>> void merge(T[] a, Object[] tmpArray, int leftPos, int rightPos, int rightEnd) {    int idx = leftPos;    int leftEnd = rightPos - 1;    int numsLength = rightEnd - leftPos + 1;    while (leftPos <= leftEnd && rightPos <= rightEnd) {        if (a[leftPos].compareTo(a[rightPos]) < 0) {            tmpArray[idx++] = a[leftPos];            leftPos++;        } else {            tmpArray[idx++] = a[rightPos];            rightPos++;        }    }    while (leftPos <= leftEnd) {        tmpArray[idx++] = a[leftPos++];    }    while (rightPos <= rightEnd) {        tmpArray[idx++] = a[rightPos++];    }    for (int i = 0; i < numsLength; i++, rightEnd--) {        a[rightEnd] = (T) tmpArray[rightEnd];    }}

分析

归并排序是用于分析递归例程技巧的经典实例:我们必须为递归程序写一个递归关系。假设N是2的幂,我们总可以将数组分成相等的两部分。
 对于N=1,使用时间记为1;
 对N>1个数排序用时等于两个大小为N/2的递归排序所有时间,加上合并的时间。合并时间是线性的。
  T(1)=1
  T(N) = 2T(N/2)+N
 用两种方法求解。
 1 用N除以递推式两边:
  T(N)N=2T(N/2)N+1=T(N/2)N/2+1
 可以将方程进一步推导为:
  T(N/2)N/2=T(N/4)N/4+1
  T(N/4)N/4=T(N/8)N/8+1
  …
  T(2)2=T(1)1+1
  所有方程左边相加、右边相加得到 T(N)N=T(1)1+1logN。logN是因为从T(N)NT(2)2有logN项。
 
 2另一种方式是在右边联系带入递推关系式
  T(N) = 2T(N/2)+N
  T(N/2) = 2T(N/4)+N 所以 T(N)=4T(N/4)+2N,连续带下去,得到T(N)=2kT(N/2k)+kN。利用k=logN,得到T(N)=N+NlogN
 

快速排序

快速排序的平均运行时间是O(NlogN),最坏情况是O(N2)。优点是非常精炼和高度优化的内部循环。一般情况下可以将快速排序和堆排序结合使用。与归并排序相同,快速也是一种分治的递归算法。只是分的方式不同。

算法描述

这里写图片描述
对数组S排序
1 如果S中元素个数为1或者0,返回;
2 取S中任一元素v为枢纽元(pivot);
3 S中其余元素分为两个不相交的集合:S1中的元素都小于pivot,S2中的集合都大于pivot;
4 S1 = 快速排序S1,S2=快速排序S2,S1 ,pivot,S2 为排序好的数组。

选取枢纽元pivot

通常选择第一个元素或者最后一个元素。如果输入是随机的,这样做没有问题。如果输入是预排序好的,或者正好逆序,那么这样的pivot将产生恶劣的分割,因为所有元素或者都被划入S1,或者S2。
安全一些的做法是随机选择。但是产生随机数开销也很大。
三数中值分割法。取数组左端、右端,中间位置的三个元素的中间值作为pivot。把pivot放入最后一位,右端元素放入中间位置。

分割策略

这里描述的分割策略是安全的、高效的。在分割阶段要做的是把所有小元素移动到数组左边,所有大元素移动到数组右边。小和大是相对于pivot元而言。i从左向右移动,当发现S[i]>pivot ,i停止移动;j从右向左移动,当发现S[j] < pivot,j停止移动。如果i < j,交换i和j位的元素,j+1。重复移动。如果j < i,停止移动,将i位和最后一位元素交换。

这里写图片描述

这里写图片描述

这里写图片描述

如果遇到和pivot相同的元素,则i和j都停止,进行交换,虽然会有无效的交换,但是可以产生相对平衡的两个数组。

外排序

当不能所有数据都加载到内存再排序的时候使用外排序。外排序就是归并排序的思想。

简单算法

假设有4个磁盘a1,a2,b1,b2。两个做输入,两个做输出。假设内存一次可以对M条记录做排序。
第一轮假设b1,b2做输出,数据存储在a1。
 从a1读取M个数,排序,写入b1;
 从a1读取M个数,排序,写入b2;
 交替直到完成a1数据的排序。
第二轮
 将b1的第1个顺串,与b2的第1个顺串,合并排序写入a1;
 将b1的第2个顺串,与b2的第2个顺串,合并排序写入a2;
 交替直到完成b1,b2数据的排序。合并已经排序好的顺串是不需要加载到内存中才能进行的。
直到排序完成。
每一轮排序完成都会增加顺串的长度。总共需要log(N/M)趟可以完成排序工作。外加一趟构造顺串的工作。
这里写图片描述
 
 如果M=3,第一轮完成后如下图。
这里写图片描述

这里写图片描述
  
这里写图片描述

多路合并

上面的算法基本是2路合并。k路合并中k>2。需要 logk(N/M)趟工作完成。

多相合并

当磁盘数量是奇数的时候使用。

替换选择

替换选择可以加快构造顺串的速度。

桶式排序

桶式排序是线性的。输入数据a1,a2,a3….an 每个数字都小于M,使用大小为M+1的数组,初始化为0,读到ai,则count[ai]增加1。所有数据输入完成,扫描大于0的下标得到排序。但是该模型不实用。首先要确定M值;第二如果数据和稀疏,M很大,则空间浪费非常严重。如果输入都是一些小整数,则非常实用。

总结

冒泡排序:O(N2)
插入排序:O(N2)
希尔排序:O(N7/6)?
堆排序:O(NlogN)
归并排序:O(NlogN)
快速排序:O(NlogN)
元素数量<=20,使用插入排序最快。
元素数量>20,使用堆排序或者快速排序。

0 0
原创粉丝点击