基于比较的排序。排序根据数据量的不同分内存排序和外部排序。但数据量大得不能放在内存的时候就需要用外部排序。内存排序有:冒泡排序、插入排序、希尔排序、堆排序、归并排序、快速排序。
冒泡排序
冒泡排序第一次保证第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++) {
//5与0比较;6与1比较... 10与5,0比较;11与6,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+1∗logN
。logN是因为从
T(N)N
到
T(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)+k∗N
。利用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,使用堆排序或者快速排序。