zoukankan      html  css  js  c++  java
  • 【经典算法】线性时间排序

      在计算机科学中,排序是一门基础的算法技术,许多算法都要以此作为基础,不同的排序算法有着不同的时间开销和空间开销。排序算法有非常多种,如我们最常用的快速排序和堆排序等算法,这些算法需要对序列中的数据进行比较,因为被称为基于比较的排序

    基于比较的排序算法是不能突破O(NlogN)的。简单证明如下:

      N个数有N!个可能的排列情况,也就是说基于比较的排序算法的判定树有N!个叶子结点,比较次数至少为log(N!)=O(NlogN)(斯特林公式)。

      而非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。

      本文着重介绍三种线性的非基于比较的排序算法:计数排序、桶排序与基数排序。

    1.计数排序 

      假设我们有一个待排序的整数序列A,其中元素的最小值不小于0,最大值不超过K。建立一个长度为K的线性表C,用来记录不大于每个值的元素的个数。这个算法伪代码如下:

      假设输入数组A[1..n],A.length=n,我们还需要两个数组:B[1..n]存放排序的输出,C[0..k]提供临时存储空间。

    COUNTING-SORT(A,B,k)
    1 let C[0..k]be a new array
    2 for i=0 to k
    3     C[i] = 0
    4 for j = 1 to A.length
    5     C[A[j]] = C[A[j]] + 1;
    6 //C[i] now contains the number of elements equal to i.
    7 for i = 1 to k
    8     C[i] = C[i] + C[i-1]
    9 //C[i]now contains the number of elements less than or equal to i.
    10 for j = A.length downto 1
    11     B[C[A[j]]] = A[J]
    12     C[A[J]] = C[A[j]] - 1;

      上述代码在第2~3行for循环的初始化操作之后,数组C的值全为0;第4~5行的for循环遍历每一个输入元素。如果一个输入元素的值为i,就将C[i]值加1.于是,在第5行执行完成后,C[i]中保存的就是等于i的元素的个数,其中i=0,1,...,k。第7~8行通过加总计算确定对每一个i=0,1,...,k,就有多少输入元素是小于或等于i的。

      最后,在第10~12行的for循环部分,把每个元素A[j]放到它在输出数组B中的正确位置上。如果所有n个元素都是互异的,那么当第一次执行第10行时,对每个A[j]值来说,C[A[j]]就是A[j]在输出数组中的最终正确的位置。这是因为共有C[A[j]]个元素小于等于A[j]。因为所有的元素可能并不都是互异的,所以,我们每将一个值A[j]放入数组B中一后,都要将C[A[j]]的值减1.这样,当遇到下一个值等于A[j]的输入元素(如果存在)时,该元素可以直接被放在输出数组A[j]的前一个位置上。

      例如,输入数组A为{3, 4, 3, 2, 1},最大是4,数组长度是5。

      建立计数数组C{0, 0, 0, 0}。

      遍历输入数组:

      A{3, 4, 3, 2, 1} -> C{0, 0, 1, 0}
      A{3, 4, 3, 2, 1} -> C{0, 0, 1, 1}
      A{3, 4, 3, 2, 1} -> C{0, 0, 2, 1}
      A{3, 4, 3, 2, 1} -> C{0, 1, 2, 1}
      A{3, 4, 3, 2, 1} -> C{1, 1, 2, 1}

      计数数组现在是{1, 1, 2, 1},我们现在把它写回到输入数组里:

      C{0, 1, 2, 1} -> A{1, 4, 3, 2, 1}
      C{o, o, 2, 1} -> A{1, 2, 3, 2, 1}
      C{o, o, 1, 1} -> A{1, 2, 3, 2, 1}
      C{o, o, o, 1} -> A{1, 2, 3, 3, 1}
      C{o, o, o, o} -> A{1, 2, 3, 3, 4}

      这样就排好序了。

      时间:O(n + k),n是输入数组长度,k是最大的数的大小。

      空间:O(n + k),n是输入数组长度,k是最大的数的大小。

      C++代码如下:

      

     1 void CountingSort(int A[], int len, int k) {
     2     if (A == NULL || len <= 0 || k <= 0)
     3         return;
     4     
     5     int *B = new int[len]();
     6     int *C = new int[k+1]();
     7     for (int i = 0; i < len; ++i)
     8         C[A[i]]++;
     9     for (int i = 1; i <= k; ++i)
    10         C[i] += C[i - 1];
    11 
    12     for (int i = len - 1; i >= 0; --i) {
    13         B[C[A[i]] - 1] = A[i];
    14         C[A[i]]--;
    15     }
    16 
    17     for (int i = 0; i < len; ++i)
    18         A[i] = B[i];
    19 
    20     delete []C;
    21     delete []B;
    22 }

     2. 桶排序  

      这种特殊实现的方式时间复杂度为O(N+K),空间复杂度也为O(N+K),同样要求每个元素都要在K的范围内。更一般的,如果我们的K很大,无法直接开出O(K)的空间该如何呢?

      首先定义桶,桶为一个数据容器,每个桶存储一个区间内的数。依然有一个待排序的整数序列A,元素的最小值不小于0,最大值不超过K。假设我们有M个桶,第i个桶Bucket[i]存储iK/M至(i+1)K/M之间的数,有如下桶排序的一般方法:

    1. 扫描序列A,根据每个元素的值所属的区间,放入指定的桶中(顺序放置)。
    2. 对每个桶中的元素进行排序,什么排序算法都可以,例如快速排序。
    3. 依次收集每个桶中的元素,顺序放置到输出序列中。

      对该算法简单分析,如果数据是期望平均分布的,则每个桶中的元素平均个数为N/M。如果对每个桶中的元素排序使用的算法是快速排序,每次排序的时间复杂度为O(N/Mlog(N/M))。则总的时间复杂度为O(N)+O(M)O(N/Mlog(N/M)) = O(N+ Nlog(N/M)) =O(N + NlogN - NlogM)。当M接近于N是,桶排序的时间复杂度就可以近似认为是O(N)的。就是桶越多,时间效率就越高,而桶越多,空间却就越大,由此可见时间和空间是一个矛盾的两个方面。

      桶中元素的顺序放入和顺序取出是有必要的,因为这样可以确定桶排序是一种稳定排序算法,配合基数排序是很好用的

      代码如下,使用的例子是算法导论上的例子(区间为[0,1)):

      

     1 struct Node {
     2     float value;
     3     Node* next;
     4 
     5     Node(float v = 0) :value(v), next(NULL) {}
     6 };
     7 
     8 void Destruct(Node *p) {
     9     if (p == NULL) {
    10         return;
    11     }else {
    12         Destruct(p->next);
    13         delete p;
    14     }
    15 }
    16 
    17 void BucketSort(float A[],  int n) {
    18     Node **B = new Node*[n];
    19     memset(B, NULL, sizeof(Node*)*n);
    20     
    21     int i = 0, j;
    22     for (i = 0; i < n; i++) {
    23         int bi = n * A[i] ;
    24         Node* q = new Node(A[i]);
    25         Node *p = B[bi];
    26         if (p == NULL) {
    27             B[bi] = q;
    28         } else if (p->next == NULL) {
    29             if (A[i] < p->value) {
    30                 B[bi] = q;
    31                 q->next = p;
    32             } else {
    33                 p->next = q;
    34             }
    35         } else {
    36             while ( p->next != NULL && A[i] > p->next->value)
    37                 p = p->next;
    38             q->next = p->next;
    39             p->next = q;
    40         }
    41     }
    42 
    43     for (i = j = 0; i < n; i++) {
    44         Node *p = B[i];
    45         while (p != NULL) {
    46             A[j++] = p->value;
    47             p = p->next;
    48         }
    49     }
    50 
    51     for (i = 0; i < n; i++)
    52         Destruct(B[i]);
    53 
    54     delete []B;
    55 }

    3.基数排序

      下面说到我们的重头戏,基数排序(Radix Sort)。上述的基数排序和桶排序都只是在研究一个关键字的排序,现在我们来讨论有多个关键字的排序问题。

      假设我们有一些二元组(a,b),要对它们进行以a为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为MSD(Most Significant Dight)排序。

      第二种方式是从最低有效关键字开始排序,称为LSD(Least Significant Dight)排序。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD方法往往比MSD简单而开销小。下文介绍的方法全部是基于LSD的。

      代码如下:

      

     1 #include<iostream>
     2 #include<vector>
     3 #include<cmath>
     4 using namespace std;
     5 
     6 int getMax(int arr[], int n) {
     7     int mx = arr[0];
     8     for (int i = 1; i < n; i++)
     9         if (arr[i] > mx)
    10         mx = arr[i];
    11 
    12     return mx;
    13 }
    14 
    15 void CountSort(int arr[], int n, int exp) {
    16     int *output = new int[n];
    17     int i, count[10] = { 0 };
    18 
    19     //Store count of occurrences in count[]
    20     for (i = 0; i < n; i++)
    21         count[(arr[i] / exp) % 10]++;
    22 
    23     //Change count[i] so that count[i] now contains actual position of
    24     //this digit in output[]
    25     for (i = 1; i < 10; i++)
    26         count[i] += count[i - 1];
    27 
    28     //Build the output array
    29     for (i = n - 1; i >= 0; i--) {
    30         output[count[(arr[i] / exp) % 10] - 1] = arr[i];
    31         count[(arr[i] / exp) % 10]--;
    32     }
    33 
    34     //Copy the output array to arr[], so that arr[] now 
    35     //contains sorted numbers according to current digit
    36     for (i = 0; i < n; i++)
    37         arr[i] = output[i];
    38 
    39     delete []output;
    40 }
    41 
    42 void RadixSort(int arr[], int n) {
    43     //Find the maximum number fo know number of digits
    44     int m = getMax(arr, n);
    45 
    46     //Do counting sort for every digit. Note that instead of passing digit
    47     //number, exp is passed. exp is 10^i where i is current digit number
    48     for (int exp = 1; m / exp > 0; exp *= 10)
    49         CountSort(arr, n, exp);
    50 }
    51 
    52 //A utility function to print an array
    53 void Print(int arr[], int n) {
    54     for (int i = 0; i < n; i++)
    55         cout << arr[i] << " ";
    56 }
    57 
    58 int main()
    59 {
    60     int arr[] = { 170, 45, 90, 802, 24, 2, 66 };
    61     int n = sizeof(arr) / sizeof(arr[0]);
    62     RadixSort(arr, n);
    63     Print(arr, n);
    64 
    65     system("pause");
    66     return 0;
    67 
    68 }

    三种排序算法的比较:   

      从整体上来说,计数排序,桶排序都是非基于比较的排序算法,而其时间复杂度依赖于数据的范围,桶排序还依赖于空间的开销和数据的分布。而基数排序是一种对多元组排序的有效方法,具体实现要用到计数排序或桶排序。

    相对于快速排序、堆排序等基于比较的排序算法,计数排序、桶排序和基数排序限制较多,不如快速排序、堆排序等算法灵活性好。但反过来讲,这三种线性排序算法之所以能够达到线性时间,是因为充分利用了待排序数据的特性,如果生硬得使用快速排序、堆排序等算法,就相当于浪费了这些特性,因而达不到更高的效率。

      在实际应用中,基数排序可以用于后缀数组的倍增算法,使时间复杂度从O(NlogNlogN)降到O(N*logN)。线性排序算法使用最重要的是,充分利用数据特殊的性质,以达到最佳效果

    参考文献:

      1.http://www.geeksforgeeks.org/radix-sort/

      2.http://www.geeksforgeeks.org/counting-sort/

      3.https://www.byvoid.com/blog/sort-radix

  • 相关阅读:
    java获取文件夹下所有目录
    java下载zip文件
    oracle 递归查询数据
    easyUi刷新 tabs
    jsp引入本地图片
    zabbix web监测设置
    jenkins部署
    ss 异常活动端口查询-std
    logrotate 日志分割
    rsync删除大量小文件
  • 原文地址:https://www.cnblogs.com/vincently/p/4525858.html
Copyright © 2011-2022 走看看