各种排序算法的实现及其比较

来源:互联网 发布:正规淘宝代刷官方网 编辑:程序博客网 时间:2024/04/28 05:14

常用的内部排序算法主要分为五类:插入、交换、选择、归并、基数排序。

文章的最后可能还会稍微分析一下外部排序。。。内/外部排序的区别就是 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,在排序过程中需要多次的内/外存之间的交换。

下面一个一个分析。

 

(注意,本篇中讲的 lg(n) 都是以2为底的)

 

一、插入排序

下面讲到的这些插入排序的时间复杂度,除希尔排序是O(n的3/2次方)外,其它的都是O(n平方)。另一方面,除希尔排序外,其它的排序都是稳定的。(稳定是指相同的两个数在排序之后它们的相对位置不变。)

1、直接插入排序。

这是最简单的排序方法,它的基本基本操作是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增加1的有序表。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int main()  
  5. {  
  6.     int src[] = {49,38,65,97,76,13,27,49};  
  7.   
  8.     int i,j,cnt = sizeof(src)/4;  
  9.     int guard = src[0]; //设置一个哨兵,用来记录当前值  
  10.     for(i = 1; i < cnt; i ++)  
  11.     {  
  12.         if(src[i] < src[i-1])  
  13.         {  
  14.             guard = src[i];  
  15.             src[i] = src[i-1];  
  16.             for(j = i - 2; src[j]>guard; j --) src[j+1] = src[j];  
  17.             src[j+1] = guard;  
  18.         }  
  19.     }  
  20.     for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;  
  21.     //getchar();  
  22.     return 0;  
  23. }  

 

 2、折半插入排序

这种插入排序是上一种排序的改进,就是在查找的时候不用一个一个对比,因为前面已经有序,所以可以用折半法。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int main()  
  5. {  
  6.     int src[] = {49,38,65,97,76,13,27,49};  
  7.   
  8.     int i,j,low,high,mid,cnt = sizeof(src)/4;  
  9.     int guard = src[0];   
  10.     for(i = 1; i < cnt; i ++)  
  11.     {  
  12.         if(src[i] < src[i-1])  
  13.         {  
  14.             guard = src[i];  
  15.             low = 0, high = i - 1;   
  16.             while(low <= high)  
  17.             {  
  18.                 mid = (low+high)/2;  
  19.                 if(guard < src[mid]) high = mid - 1;  
  20.                 else low = mid+1;  
  21.             }  
  22.             for(j = i - 1; j >= high +1; j --) src[j+1] = src[j]; //后移  
  23.             src[j+1] = guard;  
  24.         }  
  25.     }  
  26.     for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;  
  27.     //getchar();  
  28.     return 0;  
  29. }  

 

 3、2-路插入排序

先读入前面两个数,大的放队首,小的放队尾,并分别用final和first作为指针指向它们,两个都向中间延伸,first向右增长,final向左减小。

如对 int src[] = {49,38,65,97,76,13,27,49_} 进行排序(这里为了区分两个49,

我给第二个49加了一个下划线。。。)

第一步得:

 

final               first 49xxxxxx38

放入第三个数时,和队首first和队尾final分别进行比较,如果比first大则放它右边,并用first指向它。如果比final小,则放final的左边,并用final指向它。如果大小final,小于first,则插入到当前的first或final的位置。

第二步得:

 


 final           first 49  65xxxxx38

第三步:

 


  final          first 49 65  97xxxx38

第四步:

 


   final      first 49 65  76   97xx x38

第五步:

 


   final      first  
49 65  76   97xx  1338

第六步:

 


   final   first  
49 65  76   97x 13 2738

第七步:

 


    final  first  
49 49_ 65 76   97 13 2738

这样做的好处是可以减少移动的次数。。。具体的程序实现留给读者去实现。。。

 

4、希尔排序

又称为 缩小增量排序,它的基本思想是,先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本”有序时,再对全体记录进行一次直接插入排序。

 int src[] = {49,38,65,97,76,13,27,49_,55,4} 为例,共有10个数,

我们先以10/2=5为跨度,进行第一趟排序。

第一趟:

49和13比较,38和27比较,65和49_比较,97和55比较,76和4比较,得:

13,27,49_,55,4,49,38,65,97,76 (从这里可以看出,希尔排序是不稳定的,当然下结论前还要看最后的结果。)

第二趟,我们以5-2=3为跨度,进行排序,可得:

13,4,49_,38,27,49,55,65,97,76 (到这时一看,有序多了!)

第三趟,心3-2=1为跨度,进行排序,得:

4,13,27,38,49_,49,55,65,76,97

当跨度减小到1时,就算是排序结束了。由结果可以断定,希尔排序是不稳定的排序。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int main()  
  5. {  
  6.     int src[] = {49,38,65,97,76,13,27,49,55,4}; //大家可试一试奇数个的情况  
  7.   
  8.     int i,j,tmp,cnt = sizeof(src)/4,dk = (cnt+1)/2,c=1;  
  9.   
  10.     while(dk > 0)  
  11.     {  
  12.         cout<<c++<<endl;  
  13.         for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  14.         cout<<endl;  
  15.         for(i = dk; i < cnt; i ++)  
  16.         {  
  17.             if(src[i] < src[i-dk])  
  18.             {  
  19.                 tmp = src[i];  
  20.                 src[i] = src[i-dk];  
  21.                 src[i-dk] = tmp;  
  22.             }  
  23.         }  
  24.         dk = dk - 2 == 0 ? 1:dk-2;   
  25.     }  
  26.     cout<<c<<endl;  
  27.     for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  28.     cout<<endl;  
  29.   
  30.     getchar();  
  31.     return 0;  
  32. }  

 

 

二、交换排序

1、冒泡排序

很简单,就是相邻的两个数,两两相互比较,其特点是:每比较一次就可以得出最小/大的一个数 放到队首或队尾。它的时间复杂度也比较大:O(nlgn),冒泡排序算法是稳定的排序方式。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int main()  
  5. {  
  6.     int src[] = {49,38,65,97,76,13,27,49};  
  7.   
  8.     int i,j,cnt = sizeof(src)/4;  
  9.   
  10.     for(i = 0; i < cnt; i ++)  
  11.         for(j = cnt-1; j > i; j --)  
  12.             if(src[j] < src[j-1]) swap(src[j],src[j-1]);  
  13.   
  14.     //输出  
  15.     for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  16.     cout<<endl;  
  17.   
  18.     getchar();  
  19.     return 0;  
  20. }  
 

2、快速排序

快速排序的时间复杂度达到O(nlgn),被公认为最快的排序方法之一。在所有同数量级(O(nlgn))的排序当中,其平均性能最好。它其实是冒泡排序的改进,当一列数据基本有序的时候,快速排序将为蜕化为冒泡排序,时间复杂度为O(n平方)。基本思想是 取一个数作为中间数,比它小的都排左边,比它大的都排右边(如果是从大到小排序的话,就反过来),再对每一边用同样的思路进行递归求解。快速排序是不稳定的排序方式。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. void quickSort(int *src, int low,int high)  
  5. {  
  6.     if(src == NULL) return ;  
  7.     int i,j,pivot;  
  8.     if(low < high)  
  9.     {  
  10.         pivot = src[low];  
  11.         i = low,j = high;  
  12.         while(i < j)  
  13.         {  
  14.             while(i<j && src[j] >= pivot) j--;  
  15.             src[i] = src[j];  
  16.   
  17.             while(i<j && src[i] <= pivot) i++;  
  18.             src[j] = src[i];  
  19.         }  
  20.   
  21.         src[i] = pivot;  
  22.   
  23.         quickSort(src,low,i-1);  
  24.         quickSort(src,i+1,high);  
  25.     }  
  26. }  
  27.   
  28. int main()  
  29. {  
  30.     int src[] = {49,38,65,97,76,13,27,49};  
  31.   
  32.     int i,low,high,cnt = sizeof(src)/4;  
  33.   
  34.     quickSort(src,0,cnt-1);  
  35.   
  36.     //输出  
  37.     for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  38.     cout<<endl;  
  39.   
  40.     getchar();  
  41.     return 0;  
  42. }  

 

三、选择排序

1、简单选择排序

思路很简单,就是每次选出最小/大的数,和前面的第i个数交换。时间复杂度也是 O(n平方),是不稳定的排序方式,因为在交换的过程中,相同的两个数,前者有可能被交换到后面去,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int main()  
  5. {  
  6.     int src[] = {49,38,65,97,76,13,27,49};  
  7.   
  8.     int i,j,cnt = sizeof(src)/4,min ,index;  
  9.   
  10.     for(i = 0; i < cnt; i ++)  
  11.     {  
  12.         min = INT_MAX,index = -1;  
  13.         for(j = i; j < cnt; j ++)  
  14.         {  
  15.             if(min > src[j])  
  16.             {  
  17.                 min = src[j];  
  18.                 index = j;  
  19.             }  
  20.         }  
  21.         if(index != i)  
  22.         {  
  23.             src[index] = src[i];  
  24.             src[i] = min;  
  25.         }  
  26.     }  
  27.   
  28.     //输出  
  29.     for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  30.     cout<<endl;  
  31.   
  32.     getchar();  
  33.     return 0;  
  34. }  
 

 

2、堆排序

先用给定的数构造一棵完全二叉树,然后从下标为cnt/2(cnt指给定的数的个数)开始一个一个和它的孩子结点比较,小的就往上挪,最后得到一个小顶堆,取出堆顶,并把最后一个数放入堆顶,进行同样的操作,直到所有的数都已取完为止。我们可以用一个数组来顺序地表示这棵树,左孩子可以通过2*n来找到,右孩子可以通过2*n+1来找到。 下面给一个例子:

int src[] = {49,38,65,97,76,13,27,49};



 堆排序的时间复杂度是O(nlgn),也是最快的排序方法之一,在最坏的情况下,其时间复杂度还是O(nlgn),相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需要一个记录大小供交换用的辅助存储空间。堆排序也是不稳定的排序。

 

Cpp代码  收藏代码
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. void heapAdjust(int *src, int s,int m)  
  5. {   //从s开始进行一次调整,其中m指向需要进行排序的数据中最大的下标  
  6.     if(src == NULL) return;  
  7.     int i,rc = src[s];  
  8.     for(i = 2*s+1; i <= m; i = 2*i+1) //一层一层往下走  
  9.     {  
  10.         if(i<m && src[i] < src[i+1]) i++; //让i指向最大的那个孩子  
  11.         if(rc >= src[i]) break;  
  12.   
  13.         src[s] = src[i];  
  14.         s = i;  
  15.     }  
  16.     src[s] = rc;  
  17. }  
  18.   
  19. int main()  
  20. {  
  21.     int src[] = {49,38,65,97,76,13,27,49,1};  
  22.   
  23.     int i,j,cnt = sizeof(src)/4;  
  24.   
  25.     //先构建一个小顶堆  
  26.     for(i = cnt/2-1; i >-1; i --)  
  27.         heapAdjust(src,i,cnt-1);  
  28.   
  29.     //每次取出顶部最大的那个数放后面,再进行一次顶点调整  
  30.     for(i = cnt-1; i > 0; i --)  
  31.     {  
  32.         swap(src[0],src[i]);  
  33.         heapAdjust(src,0,i-1);  
  34.     }  
  35.   
  36.     //输出  
  37.     for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";  
  38.     cout<<endl;  
  39.   
  40.     getchar();  
  41.     return 0;  
  42. }  

 

四、归并排序

归并的意思就是两个或两个以上的有序表组合成一个新的有序表。整个归并排序需要进行【lgn取上限】次,总的时间复杂度为O(nlgn)。与快速排序相比,归并排序的最大特点是:它是一种稳定的排序方法。

如上图,这是一种2-路归并排序,通常用递归方法来实现,递归的形式的算法在形式上较简洁,但实用性很差。

[cpp] view plaincopy
  1. void Mergesort(int *p, int n)  
  2. {  
  3.     void Msort(int *p, int *temp, int left, int right);  
  4.     int *temp;  
  5.     if(n <= 0 || p == NULL)  
  6.         return;  
  7.     temp = (int *)malloc(sizeof(int) * n);  
  8.     if(temp == NULL)  
  9.         return;  
  10.     Msort(p, temp, 0, n-1);   
  11.     free(temp);  
  12. }  
  13.   
  14. void Msort(int *p, int *temp, int left, int right)  
  15. {  
  16.     void Merge(int *p, int *temp, int left, int rightbegin, int right);  
  17.     int leftend = (right + left)/2;  
  18.     int rightbegin = leftend+1;  
  19.     if(left < right)  
  20.     {  
  21.         Msort(p, temp, left, leftend);  
  22.         Msort(p, temp, rightbegin, right);  
  23.         Merge(p, temp, left, rightbegin, right);  
  24.     }  
  25. }  
  26.   
  27. void Merge(int *p, int *temp, int left, int rightbegin, int right)  
  28. {  
  29.     int TempArray = rightbegin;  
  30.     int pos = left;  
  31.     int begin = left;  
  32.     while(left < TempArray && rightbegin <= right)  
  33.     {  
  34.         if(p[left] <= p[rightbegin])  
  35.         {  
  36.             temp[pos++] = p[left++];  
  37.         }  
  38.         else if(p[left] > p[rightbegin])  
  39.         {  
  40.             temp[pos++] = p[rightbegin++];  
  41.         }  
  42.     }  
  43.     while(left < TempArray)  
  44.         temp[pos++] = p[left++];  
  45.     while(rightbegin <= right)  
  46.         temp[pos++] = p[rightbegin++];  
  47.     while(pos-- >= begin)  
  48.     {  
  49.         p[pos] = temp[pos];  
  50.     }  
  51. }  

 

五、基数排序

所谓的基数排序 其实就是一种多关键字的排序,最经典的例子就是英语字典的编排,先按第一个字母排列,分成26堆,再按第二个字母排列,……以此类推。。。更复杂的基本排序作者本人也没有研究过,有兴趣的朋友可以去网上找相关的资料。

     基数排序的主要思路是,将所有待比较数值(注意,必须是正整数)统一为同样的数位长度,数位较短的数前面补零. 然后, 从最低位开始, 依次进行一次稳定排序(我们常用上一篇blog介绍的计数排序算法, 因为每个位可能的取值范围是固定的从0到9).这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列. 


编码详细:http://www.cnblogs.com/dlutxm/archive/2011/10/20/2219321.html



六、各种内部排序的比较

1、时间复杂度达到O(nlgn) 的排序算法有:快速排序、堆排序、归并排序。

2、上面前四大类排序中,不稳定的排序有:希尔排序、快速排序、堆排序、简单的选择排序。

稳定的排序有:插入排序(除希尔外)、冒泡排序、归并排序。

3、从平均时间性能而言,快速排序最佳,其所需要的时间最少,但快速排序在最坏的情况下,时间性能还不如堆排序和归并排序。

 

七、外部排序

外部排序指的是大文件的排序,面试的时候,面试官喜欢问,给你一个非常非常大的文件(比如1T),一行一个数(或者一个单词),内存最多只有8G,硬盘足够大,CPU很高级……然后要你给这个文件里面的数据排序。你要怎么办?

这其实就要用到外部排序。就是说要借助外存储器进行多次的内/外存数据的交换,因为内存不足以加载所有的数据,所以只能一部分一部分地加载。

所以外部排序的思想就是:分两个独立的阶段。

首先,可按内存的大小,将外存上含n个记录的文件分成若干长度为 的子文件或段,依次读入内存,并利用有效的内部排序方法对它们进行排序,并将排序后得到的有序子文件 重新写入外存,通常称这些有序的子文件为归并段或顺串。然后,对这些归并段进行逐趟归并,使归并段逐渐由小到大,直至得到整个有序文件为止。

因此现在的问题就转化为如何归并两个大文件。这个读者朋友们想一下就明白了。就是把这两个文件按内存的大小,一部分一部分从小到大加载出来并,再写回外存。

当然归并的方法有很多种,作者本人也没怎么去研究。。。

 

出处:http://lingyibin.iteye.com/blog/1043886

原创粉丝点击