zoukankan      html  css  js  c++  java
  • 排序

    在处理数据时,经常需要将一组无序序列变得有序,这里就需要用到排序算法。

    排序算法

    经过几十年的发展,排序算法也有很多种,这里记录一下入门的排序算法,有:冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、堆排序、桶排序、基数排序等。

    时间复杂度(T(n)):指算法执行时耗费的时间长度。

    空间复杂度(S(n)):   指算法运行时额外占用的空间。一般不算输入,有时也不算必然需要的输出(如排序算法中数组参数被指定const)。

    稳定性:当i,j ∈[ 0, n),i<j,有A[i] = A[j],排序之后,A[i]还在A[j]前面,则算法稳定;否则就不稳定。

    在分析复杂度时,使用渐进法去分析,并需要得到精确值,只要得到相同数量级的一个值即可。也称大O记法。

    若无额外说明,默认使用数组,非降序排序;且无额外说明,使用10万的随机数据做测试。


    统一使用的框架

     1 #include <stdio.h>
     2 #include <time.h>
     3 
     4 void Swap(int *a, int *b){
     5     int tmp = *a;
     6     *a = *b;
     7     *b = tmp;
     8 }
     9 
    10 int RandomNumber(int max){
    11     
    12     return rand() % max;
    13 }
    14 
    15 void GetRandom(int* A, int N){
    16     int i;
    17     
    18     for( i=0; i<N; i++)
    19         A[i] = RandomNumber(N*10);
    20 }
    21 
    22 void Print(int* A, int N){
    23     int i;
    24     
    25     for( i=0; i<N; i++)
    26       printf("%d ", A[i]);
    27     printf("
    ");
    28 }
    29 
    30 int main(void){
    31     int N = 10000;
    32     int A[N];
    33     double start, finish;
    34     
    35     srand((int)time(NULL));
    36     GetRandom( A, N);
    37     Print( A, N);
    38     
    39     start = clock();//取开始时间
    40     //某个排序算法
    41     finish = clock();//取结束时间
    42     
    43     Print( A, N);
    44     
    45     printf( "%f seconds
    ",(finish - start) / CLOCKS_PER_SEC);//以秒为单位显示之
    46     
    47     return 0;
    48 }
    C语言实现

    以后仅展示算法相关。


     冒泡排序

    最早学习的一种算法。

    思路:

    将每两个相邻的元素比较,后面比前面小则交换,否则继续向后比较;每一轮比较后就会使最大的一个元素放至最后,此时将规模-1,再次循环比较。

    版本A:

    1 void bubbleSort_A(int* A, int N){
    2     int i, j;
    3     
    4     for( i=0; i<N; i++)
    5       for( j=0; j<N-i-1; j++)
    6         if( A[j+1] < A[j] )
    7             Swap( &A[j+1], &A[j]);
    8 }

    这里的复杂度很好分析,两个嵌套的循环,且无任何提前退出,所有最好、最坏都是O(n2)。

    我这里测试10万数量级的排序平均耗时:38.144s

    反思:

    我刚刚提到了提前退出,那什么时候能提前退出呢?比如,如果进来的就是一个有序的序列,版本A还是要扫描n2。我们可以意识到,当扫描一轮之后,如果未做过交换操作,那么其实该序列已经有序且可以退出了。

    版本B:

     1 void bubbleSort_B(int* A, int N){
     2     int i, j, sorted=1;
     3     
     4     for( i=0; i<N; i++){
     5       sorted = 1;
     6       for( j=0; j<N-i-1; j++)
     7         if( A[j+1] < A[j] ){
     8             Swap( &A[j+1], &A[j]);
     9             sorted = 0;
    10         }
    11       if( sorted )
    12         break;    
    13     }
    14 }

    可以写得更简洁,但这样写一目了然。理论上说是T(n)应该是得到提高的,但实测O(n)还是在35~40s之间,为什么?因为这样写其实提高也只是比较好(某个时刻,前缀序列已有序)的情况。

    版本C:

    版本B改进的是整个前缀已有序的情况,还有一种情况,即序列部分有序,如10,9,1,2,3,4,7,6中1~4已有序,再去扫描是浪费时间,如何改进?看版本C。

     1 void bubbleSort_C(int* A, int N){
     2     int i, j, last;
     3     
     4     last = N-1;
     5     for( i=0; i<N; i++){
     6       for( j=0; j<last; j++)
     7         if( A[j+1] < A[j] ){
     8             Swap( &A[j+1], &A[j]);
     9             last = j;
    10         }   
    11     }
    12 }

    改进办法就是用一个指针last指向最后一次发生交换的地方,last后面的序列必然已有序。但整体时间复杂度还是O(n2)。

    总结:

    冒泡排序几十年前已被发明,简单有效。


    选择排序

    思路:

    在未排序的序列中选择一个最大值,放至已排序序列的最前方。

     1 void selectionSort(int* A, int N){
     2     int i, j, max;
     3     
     4     for( i=N-1; i>=0; i--){
     5         max = i;
     6         for( j=0; j<=i; j++)
     7           if( A[max] < A[j] ) 
     8             max = j;
     9         Swap( &A[max], &A[i]);
    10     }
    11 }

    总结:

    选择排序依然是O(n2),但是比冒泡排序改进的是,每次循环只要做一次交换,这种交换操作还是比较耗时的,比如我这边选择排序10万的随机数据耗时仅11.952s,但选择排序不管好、坏,都是O(n2)。


     插入排序

    思路:

    插入排序就像生活中抓拍的过程:一开始手上没牌,直接插入;手上有牌后再抓牌,可以从后向前找一个合适的地方插入该牌。

    插入排序就是这样,有序序列规模为k,无序序列规模为n-k,为第k+1个元素寻找位置时,从k到0依次扫描,不合适依次向后依,合适中断并插入。

     1 void insertionSort(int* A, int N){
     2     int i, j, X;
     3     
     4     for( i=0; i<N; i++){
     5         X = A[i];
     6         for( j=i; j>0; j--)
     7           if( X < A[j-1] )
     8             A[j] = A[j-1];
     9           else
    10             break;
    11         A[j] = X;
    12     }
    13 }

    总结:

    理论时间复杂度O(n2),但每次仅需移动元素而不需要进行交换。10万随机数据排序耗时7.711s


    希尔排序:

    说到希尔排序就要先说明一个概念:逆序对,当i,j ∈[ 0, n),i<j,有A[i] > A[j],则称i、j是一对逆序对。

    思想:

    冒泡排序因为每次只比较相邻的两个元素,因此一次最多消除一对逆序对。而希尔排序是通过先消除间隔较远的两个逆序对,而此时可能也会同时消除更多的逆序对,通过不断缩小间隔,使得序列逐渐有序。

     1 void shellSort(int* A, int N){
     2     int i, j, D, si, X;
     3     int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0};
     4     
     5     for( si=0; Sedgewick[si]>=N; si++);
     6     for( D=Sedgewick[si]; D>0; D=Sedgewick[++si]){
     7         for( i=D; i<N; i+=D){
     8             X = A[i];
     9             for( j=i; j>=D&&A[j-D]>X; j-=D)
    10                 A[j] = A[j-D];
    11             A[j] = X;
    12         }
    13     }
    14 }

    总结:

    希尔排序算法的整体时间复杂度与增量序列的选取有关,目前没有最优的增量序列。在代码中我使用的叫Sedgewick增量序列,形式如4i-3*2i+1,对其复杂度的猜想为平均O(N7/6),最差O(N4/3)。我这里10万的随机数据平均耗时6.127s


    快速排序:

    真正突破O(n2)的算法,使用了递归的思想。

    思想:

    先选择一个基准,然后开始扫描序列,比基准大的数放在序列右边,比基准小的值放在基准左边,然后再递归解决左右两个子序列。

    版本A:

    根据该思想,可以写出第一个版本:左右指针法

     1 int PartSort1(int* A, int left, int right){
     2     int pivot = right;
     3     
     4     while( left < right ){
     5         while( left<right && A[left] <= A[pivot] )
     6           left++;
     7         while( left<right && A[pivot] <= A[right] )
     8           right--;
     9         Swap( &A[left], &A[right]);
    10     } 
    11     Swap( &A[left], &A[pivot]);
    12     return left;
    13 }
    14 
    15 void QSort(int* A, int left, int right){
    16     if( right <= left )
    17       return;
    18     
    19     int pos = PartSort1( A, left, right);
    20     QSort( A, left, pos-1);
    21     QSort( A, pos+1, right);
    22 }

    使用两个指针,分别指向最左边、最右边,当左边向右边线性扫描,遇到大于基准的值停止;右边指针向左边线性扫描,遇到基于基准的值停止;然后交换两个值的位置,循环做这件事,直到左右指针不合法:左指针到了右指针右边。

    版本B:

    挖坑法:先保存基准值,然后由左指针向右扫描,遇到大于基准值后停止,将该左指针值放进右指针位置;同理,右指针向左扫描,遇到小于基准值后停止,将该值放进左指针位置;最后将保存的基准值放进左指针位置。

     1 //挖坑法
     2 int PartSort2(int* A, int left, int right){
     3     int pivot = A[right];
     4     while( left < right ){
     5         while( left<right && A[left]<=pivot )
     6           left++;
     7         A[right] = A[left];
     8         while( left<right && pivot<=A[right] )
     9           right--;
    10         A[left] = A[right];
    11     }
    12     A[left] = pivot;
    13     
    14     return left;
    15 } 

    版本C:

    递归比较消耗内存---栈帧,因此可以设定一个阀值,小于该阀值时调用简单排序。

    且基准的选择也可以优化快速排序,其中一种选择方式就是在左、中、右三个数中选一个中位数做基准。

     1 int FindPivot(int* A, int lo, int hi){
     2     int mi = (lo+hi)/2;
     3     if( A[mi] < A[lo] )
     4       Swap( &A[lo], &A[mi]);
     5     if( A[hi] < A[lo] )
     6       Swap( &A[hi], &A[lo]);
     7     if( A[hi] < A[mi] )
     8       Swap( &A[hi], &A[mi]);
     9     Swap( &A[mi], &A[hi-1]);
    10     
    11     return A[hi-1]; 
    12 }
    13 
    14 //挖坑法
    15 int PartSort2(int* A, int left, int right){
    16 //    int pivot = A[right];
    17     int pivot = FindPivot( A, left, right);
    18     right--;
    19     while( left < right ){
    20         while( left<right && A[left]<=pivot )
    21           left++;
    22         A[right] = A[left];
    23         while( left<right && pivot<=A[right] )
    24           right--;
    25         A[left] = A[right];
    26     }
    27     A[left] = pivot;
    28     
    29     return left;
    30 } 
    31 
    32 void QSort(int* A, int left, int right){
    33 //    if( right <= left )
    34 //      return;
    35     
    36     if( 1000 < (right-left+1) ){
    37         int pos = PartSort1( A, left, right);
    38         QSort( A, left, pos-1);
    39         QSort( A, pos+1, right);
    40     }
    41     else
    42         insertionSort( A+left, right-left+1);
    43 }

    总结:

    快速排序时间复杂度仅为O(NlogN),对快速排序进行10万随机数据测试,仅需不到0.01s时间。但快排比较占用空间,可以简单看做空间复杂度为O(logN)。


    归并排序:

    归并排序也是使用递归实现的,但与快速排序相反,快速排序是选基准、划分左右子序列,归并是一步步分解,分解到最小规模时,再依次向上合并,最后形成有序序列。

    思路:

    当序列规模较大时,以中间点将序列一分为二,重复此步骤,直到序列可以直接比较得出有序序列(规模为2的时候),然后再逐步归并,将两个规模为2的子序列依序归并为规模为4的更大子序列,最后形成一个规模为n的有序序列。

     1 void merge(int* E, int lo, int mi, int hi){
     2     int LLen, RLen, i, j, k;
     3     LLen = mi - lo + 1; RLen = hi - mi;
     4     int *left, *right = E + mi + 1, *A = E + lo;
     5     left = (int *)malloc(sizeof(int)*LLen);
     6     
     7     for( i=0; i<LLen; i++)
     8       left[i] = A[i];
     9     for( i=0, j=0, k=0; j<LLen && k<RLen; ){
    10         if( left[j] <= right[k] )
    11           A[i++] = left[j++];
    12         else
    13           A[i++] = right[k++];
    14     }
    15     while( j < LLen )
    16       A[i++] = left[j++];
    17     free(left);
    18 }
    19 
    20 void merge_Sort(int* A, int lo, int hi){
    21     if( hi <= lo )
    22       return;
    23     
    24     int mi = (lo+hi) >> 1;
    25     merge_Sort( A, lo, mi);
    26     merge_Sort( A, mi+1, hi);
    27     merge( A, lo, mi, hi);
    28 }
    29 
    30 void mergeSort(int* A, int N){
    31     merge_Sort( A, 0, N-1);
    32 }

    总结:

    归并排序带来的时间复杂度也是O(NlogN),但是归并需要一个O(N)的空间复杂度,当数据量极大的时候不能被接受,所以一般内部排序不会使用归并,但是归并在外部排序场合使用很多。10万随机数据排序平均为0.03s


    推荐一个带有动画演示的排序算法传送门:https://www.cnblogs.com/onepixel/articles/7674659.html

  • 相关阅读:
    json
    封装PDO
    PDO
    jquery练习
    jquery包
    jquery
    租房子 多条件查询
    查询
    新闻修改处理页面
    新闻添加数据
  • 原文地址:https://www.cnblogs.com/EasonDongH/p/9573116.html
Copyright © 2011-2022 走看看