由于硬盘读取速度远远慢于指令执行速度,排序大的分为内部排序和外部排序。具体分类如下:
插入排序
直接插入排序
基本思想
直接插入排序的基本思想是将一个记录插入已经排好序的有序表中,得到一个长度+1的新的有序表。插入排序由N-1趟排序组成。对于p=1到N-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。下表显示一个数组样例在每一趟插入排序后的情况。
原属数据 | 15 | 96 | 5 | 76 | 25 | 38 | 移动的位置 |
---|---|---|---|---|---|---|---|
p=1趟之后 | 15 | 96 | 5 | 76 | 25 | 38 | 0 |
p=2趟之后 | 5 | 15 | 96 | 76 | 25 | 38 | 2 |
p=3趟之后 | 5 | 15 | 76 | 96 | 25 | 38 | 1 |
p=4趟之后 | 5 | 15 | 25 | 76 | 96 | 38 | 2 |
p=5趟之后 | 5 | 15 | 25 | 38 | 76 | 96 | 2 |
算法的实现
public void insertSort(int[] a) {
int length = a.length;
if(length == 1) {
return;
}
for(int i = 1 ; i < length ; i++ ) {
int tmp = a[i];
for(int j = i-1 ; j >= 0 && a[j] >tmp ;j--) {
a[j+1]=a[j];
a[j]=tmp;
}
}
}
算法分析
由于嵌套循环的每一次都花费N次迭代,因此算法时间复杂度为O(N^2),而且这个界是精确的,因为以反序的输入可以达到该界。
希尔排序(缩减增量排序)
希尔排序是1959 年由Donald Shell 提出来的,该算法是冲破二次时间屏障的第一批算法之一。
操作方法:
1.选择一个增量序列h1,h2,…,hk,其中hi>hj,hk=1;
2.按增量序列个数k,对序列进行k 趟直接插入排序;
3.每趟排序,根据对应的增量hi,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
增量序列的一个流行(但是不好)的选择是使用Shell建议的序列:ht=[N/2]和hk=[hk+1/2]。
基本思想
先将整个序列分成若干子序列,每个子序列进行直接插入排序。待整个序列基本有序再进行直接插入排序。
算法的实现
public void shellSort(int[] a) {
int t = a.length;
if(t==1) {
return;
}
do {
t=t/2;
for(int i = t ; i < a.length ; i++ ) {
int tmp = a[i];
for(int j = i-t ; j >= 0 && a[j] >tmp ;j=j-t) {
a[j+t]=a[j];
a[j]=tmp;
}
}
}
while(t>1);
}
算法分析
使用希尔增量时希尔排序的时间复杂度为O(N^2)。
如果选择Hibbard增量序列(1,3,7,15,..2^k-1)时间复杂度将降到O(N^(3/2))。
选择排序
简单选择排序
基本思想
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
算法的实现
public void selectSort(int[] a , int high) {
if(high>0) {
int max = 0;
for(int i = 1 ; i<=high ;i++) {
if(a[i] > a[max]) {
max=i;
}
}
int tmp = a[high];
a[high] = a[max];
a[max]=tmp;
high--;
print(a);
selectSort(a, high);
}
}
算法分析
时间复杂度为O(N^2)。
堆排序
基本思想
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
算法的实现
public void heapSort(int[] a,int high) {
if(high<1) {
return;
}
for(int i = 1 ; i <=high;i++) {
upFloat(a,i);
}
a[0]=a[0]+a[high];
a[high]=a[0]-a[high];
a[0]=a[0]-a[high];
high--;
print(a);
heapSort(a,high);
}
public void upFloat(int[] b ,int i) {
if(i>0) {
if(b[i] > b[(i-1)/2]) {
b[i]=b[i]+b[(i-1)/2];
b[(i-1)/2]=b[i]-b[(i-1)/2];
b[i]=b[i]-b[(i-1)/2];
i=(i-1)/2;
upFloat(b,i);
}
}
}
算法分析
第一阶段构建堆最多用到2N次比较。在第二阶段,第i次输出堆顶元素最多用到2[log i]次比较,总数最多2Nlog N-O(N)次比较。因此在最坏情形下堆排序最多使用2Nlog N-O(N)次比较,算法时间复杂度为O(Nlog N)。
交换排序
冒泡排序
基本思想
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
算法的实现
public void bubbleSort(int[] a) {
for(int i = 0 ; i < a.length-1;i++) {
for(int j=0;j<a.length-1;j++) {
if(a[j]>a[j+1]) {
int num = a[j];
a[j] = a[j+1];
a[j+1]=num;
}
}
}
}
算法分析
时间复杂度为O(N^2)。
快速排序
基本思想
1.选择一个基准元素,通常选择第一个元素或者最后一个元素,
2.通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。
3.此时基准元素在其排好序后的正确位置
4.然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
算法的实现
public void quickSort(int[] a , int low ,int high) {
if(low<high) {
int middle= getMiddle(a,low,high);
quickSort(a,low,middle-1);
quickSort(a,middle+1,high);
}
}
public int getMiddle(int[] a , int low ,int high) {
int key = a[low];
while(low<high) {
while(low < high && a[high] >= key) {
high--;
}
a[low] = a[high];
while(low < high && a[low] <= key) {
low++;
}
a[high] = a[low];
}
a[low] = key;
return low;
}
算法分析
快速排序是递归的,算法时间复杂度为O(Nlog N)。
归并排序
基本思想
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
算法的实现
public int[] mergeSort(int[] a,int[] b) {
int[] array = new int[a.length+b.length];
int pointA = 0;
int pointB = 0;
for(int i= 0 ; i <array.length ; i++) {
if(pointA >= a.length && pointB < b.length) {
array[i] = b[pointB];
pointB++;
}
if(pointB >= b.length && pointA < a.length){
array[i] = a[pointA];
pointA++;
}
if(pointA < a.length && pointB < b.length) {
if(a[pointA] <= b[pointB]) {
array[i] = a[pointA];
pointA++;
}
else {
array[i] = b[pointB];
pointB++;
}
}
}
return array;
}
算法分析
归并排序最坏时间复杂度为O(Nlog N),但是它有一个明显的问题,即合并两个已排序的表到线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些附加的工作,它明显减慢了排序的速度。
基数排序
桶排序
基本思想
是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。
然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
算法分析
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果 对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn - nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
桶式排序是一种分配排序。分配排序的特定是不需要进行关键码的比较,但前提是要知道待排序列的一些具体情况。
基数排序
基本思想
是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
算法分析
基数排序的运行时间关于所有字符串中字符总个数是线性的。当串中的字符是从一个合理的小的字母集合取得,而且字符串或者是比较短、或者是非常相似时,则针对字符串的基数排序会表现非常好。因为O(Nlog N)的基于比较的排序算法在每次字符串比较中一般只查看少量的字符,所以一旦字符串的平均长度开始变大,基数排序的优势就会减小甚至完全丧失。