【算法】计数排序、桶排序和基数排序详解

2021-06-26 20:05

阅读:634

标签:注意   har   时间   ack   概念   name   bucket   空间复杂度   地方   

01.计数排序、桶排序与基数排序

并不是所有的排序 都是基于比较的,计数排序和基数排序就不是。基于比较排序的排序方法,其复杂度无法突破\(n\log{n}\) 的下限,但是 计数排序 桶排序 和基数排序是分布排序,他们是可以突破这个下限达到O(n)的的复杂度的

1. 计数排序

概念

计数排序是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,使用 C[i] 来计算 i 出现的次数。然后根据数C来将原数组A中的元素排到正确的位置。

复杂度

计数排序的最坏时间复杂度、最好时间复杂度、平均时间复杂度、最坏空间复杂度都是O(n+k) 。n为元素个数,k为待排序数的最大值。

优缺点

计数排序不是比较排序,排序的速度优于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加1),这使得对于数组中数据范围很大的数组,需要大量的时间和内存。(简言之,不适于大范围数组)

通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。算法的步骤如下:

  1. 找出待排序数组中的最大元素和最小元素。
  2. 统计数组中值为 i 的元素出现的次数,存入数组C的第 i 项。
  3. 对所有的计数累加(从C中第一个元素开始,每一项和前一项累加)。
  4. 反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项, 每放一个元素就将 C[i] 减去1.

C语言实现

#include 
#include 
#include 

void print_arr(int *arr, int n)
{
    int i;
    printf("%d", arr[0]);
    for (i = 1; i  0; j--)
    {
        int elem = ini_arr[j - 1];          // 取待排序元素
        int index = count_arr[elem] - 1;    // 取待排序元素在有序数组中的序号
        sortrd_arr[index] = elem;           // 将待排序数组存入结果数组中
        count_arr[elem]--;                  // 修正排序结果,保证sorted_arr数组中元素的稳定性
    }
    /*
     * 上述句子也可以写为: 
     *      sortrd_arr[--count_arr[ini_arr[j -1]]] = ini_arr[j -1];
     *
     */
        free(count_arr);
}

int main(int argc, char **argv) 
{
    int n =10;
    int i;
    int *arr = (int *)malloc(sizeof(int) * n);
    int *sorted_arr = (int *)malloc(sizeof(int) * n);
    srand(time(0));
    for (i = 0; i

2. 桶排序

概念

桶排序(Bucket sort)或所谓的箱排序,其工作原理是将阵列分到有限数量的桶里。每个桶再分别排序。桶排序是分布排序,不是比较排序,因而不受到比较排序 O(\(n\log{n}\))下限的影响。

复杂度

最坏时间复杂度是O(n^2), 平均复杂度是O(n+k),最坏空间复杂度是O(n*k)。

关键步骤

  1. 设置一个定量的阵列当作空桶子。
  2. 寻访序列,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶进行排序。可以在放入元素的时候进行插入排序,也可以在写回的时候进行快速排序。
  4. 从不是空的桶子里把项目放回到原来的序列中。

算法实现

假设数据分布在[0, 100]之间,每个桶内部用链表表示,在数据入桶的同时插入排序。然后把各个桶中的数据合并。


#include 
#include 
#include 

using namespace std;

const int BUCKET_NUM = 10;

// 数据结构的定义,explicit表示不允许构造函数发生隐式转换
struct ListNode {
    explicit ListNode(int i = 0): mData(i), mNext(NULL){};
    ListNode *mNext;
    int mData;
};

ListNode *insert(ListNode *head, int val)
{
    ListNode dummyNode;                         // 节点指针
    ListNode *newNode = new ListNode(val);      //待插入的新节点
    ListNode *pre , *curr;
    dummyNode.mNext = head;                     // 将指针指向链表头部
    pre = &dummyNode;                           // 设置临时指针。pre是当前检查元素的上一个元素
    curr = head;

    while (NULL != curr && curr->mData  mNext;       
    }

    newNode->mNext = curr;                      // 改变指针指向
    pre ->mNext =  newNode;
    return dummyNode.mNext;                     // 返回链表头节点
}

ListNode *Merge(ListNode *head1, ListNode *head2)   // 将head2合并到head1
{
    ListNode dummyNode;
    ListNode *dummy = &dummyNode;               // 临时指针
    while(NULL != head1 && NULL != head2)       // 循环直到末尾
    {
        if (head1->mData mData)      // 类似于归并排序
        {
            dummy->mNext = head1;
            head1 = head1 -> mNext;
        }else{
            dummy->mNext = head2;
            head2 = head2->mNext;
        }
        dummy = dummy->mNext;
    }
    if (NULL != head1) dummy->mNext = head1;
    if(NULL!=head2) dummy->mNext = head2;
    return dummyNode.mNext;
}

void BucketSort(int n,int arr[]){
    vector buckets(BUCKET_NUM,(ListNode*)(0));

    // 将元素分配到桶
    for(int i=0;imData;
        head = head->mNext;
    }
}

3. 基数排序

定义

基数排序是桶排序的扩充,也是一种分布排序。其原理是将整数按位数切割为不同的数字,然后按每个数分别比较。根据比较的方向,基数排序又可以分为MSD(从左到右)和LSD(从右向左)

LSD原理 将所有带比较数值统一为同样的数位长度,数位较短的前面补0,。然后,从最低位开始,进行一次排序,一直到最高位排序完成以后,数列就变成一个有序数列。其思想就是,将待排序数据中的每组关键字依次进行桶分配。

MSD原理
msd算法从左向右遍历字符。其核心思想是分治,我们采用递归的方法来实现。原理如下:

  • 首先,使用键索引排序的方法对首字母进行排序,此时排好序的数组已经是首字母有序的数组,并且已经按照首字字母分好了组。
  • 按照分好的组,递归的对每个首字母对应的子数组进行排序。
  • 重复步骤二。

复杂度

最坏时间复杂度是O(kN),最坏空间复杂度是O(k+N);

LSD实现

/**
 * 基数排序:C 语言
 *
 * 
 */

#include 

// 数组长度
#define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) )

/*
 * 获取数组a中最大值
 *
 * 参数说明:
 *     a -- 数组
 *     n -- 数组长度
 */
int get_max(int a[], int n)
{
    int i, max;

    max = a[0];
    for (i = 1; i  max)
            max = a[i];
    return max;
}

/*
 * 对数组按照"某个位数"进行排序(桶排序)
 *
 * 参数说明:
 *     a -- 数组
 *     n -- 数组长度
x*     exp -- 指数。对数组a按照该指数进行排序。
 *
 * 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
 *    (01) 当exp=1表示按照"个位"对数组a进行排序
 *    (02) 当exp=10表示按照"十位"对数组a进行排序
 *    (03) 当exp=100表示按照"百位"对数组a进行排序
 *    ...
 */
void count_sort(int a[], int n, int exp)
{
    // 存储"被排序数据"的临时数组
    int *output = (int *)malloc(sizeof(int)*n);            
    int i, buckets[10] = {0};

    // 将数据出现的次数存储在buckets[]中
    for (i = 0; i = 0; i--)
    {
        output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
        buckets[ (a[i]/exp)%10 ]--;
    }

    // 将排序好的数据赋值给a[]
    for (i = 0; i  0; exp *= 10)
        count_sort(a, n, exp);
}

void main()
{
    int i;
    int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
    int ilen = LENGTH(a);

    printf("before sort:");
    for (i=0; i

msd实现


  /*
  MSD(Most Significant Digit First) 高位优先的字符串排序
 
  该算法基于键索引计数法的思想,进行了扩展,使得该算法可以
  处理不等长的字符串排序,其中涉及两个关键点。
  1、采用分治法,从高位向低位的方向,依次选取关键字做为排序
     的键字进行排序,每一轮排序后,将字符串组进行拆分,对拆
     分后的每个子组分别进行排序,这里子组拆分的依据就是本轮
     键索引计数法排序之后的分类组。
  
     如下示例,最初只有一个字符串组,其中组成员数为5个字符串
     首先,选择第0列做为排序的链字进行键索引计数法排序,排序
     完成后,按分类组划分,此时分为了两组(见第一次后的情况),
     这时候对这两组分别进行链索引计数法排序,注意这时每组第
     0列已经为相同字符,所以此时选择第1做为排序的链字进行键
     索引计数法排序,在第二次排序后,此时已经分为了4组lp字符串
     组,依次类推,直到所有子组仅含有一个成员,所有子组排序
     处理完后,即整个字符串排序算法完成。
  
     原始:    第一次后:    第二次后:
     abcd      abcd          abcd
     ddba      acca              
     daca                    acca 
     acca      ddba              
     daab      daca          daca
               daab          daab
                                 
                             ddba
  
  2、刚才提到了该算法可以处理不等长的字符串排序,该算法采用一
     种比较巧妙的方法,将短字符串长度已经满足不了排序的处理也
     做为键值比较处理了,同时如果短字符串长度满足不了排序处理
     时,该键值优先级最高,所以就会出现排在最上方。
  
     如下示例,当第2列(字符c的位置)处理完后,开始进行第3列比较
     处理,此时第一个条目abcd的第3列键值为d、第二个条目abc的第
     3列键值已经不存在,长度已经满足不了排序,但此时键值优先级
     为最高,第三个条目abcde的第3列键值为d,所以本轮最终将第二
     个条目abc排在了最上面,相同原理,abcd条目就会比abcde条目的
     优先级高。
  
     原始:    排序后:
     abcd      abc
     abc       abcd
     abcde     abcde
  */
 

#include 
#include 
#include 
#include 


const int R = 256;  // 基数
const int M = 15;   // 小数组使用插入排序的阈值
using namespace std;

int charAt(const string& str, int d)
{
    if ( d & sVec, int lo, int hi, int d, vector& aux)
{
    int i,r;

    /*
     * 这里存在一个优化:当数组数量较少的时候,我们可以使用插入排序优化算法
     *  
     * 故而下列return语句可以改写为:
     *      if (hi  sVec;
    ifstream infile("data.txt");
    cout>str)
    {
        cout aux(n);
    MSD_sort(sVec, 0, n-1, 0, aux);

    cout

msd算法的性能及其改进

我们从三个方面衡量算法的性能:需要检查的字符数量,统计字符出现频率时所需的时间和空间,将频率转化为索引所需的时间和空间。

msd算法的性能取决于数据,因为他并非是比较排序,键的顺序并不重要,所需关注的只是键所对应的值:

  • 对于随机输入,msd只检查足以区分字符串的(字符),因为其运行时间是亚线性的。

  • 对于非随机输入,他的运行时间仍然可能是亚线性的。特别是当存在大量等值键的情况下,msd会检查比随机输入更多的键,因而可能耗费更多的时间,其时间接近线性。
  • 最坏情况下,即为所有字符都相同并且所有字符长度都一致,那么其运行时间是线性的。

用msd算法对基于大型字母表的字符串排序时,msd算法可能消耗大量的时间和空间,特别是在有大量重复字符串的情况下。

msd算法可以注意的地方:

  • 小数组时可以使用改进后的插入排序。
  • 对于含有大量等值键的子数组排序会比较慢。msd的最坏情况就是所有的键均相同。
  • 额外空间的占用。为了进行切分,msd算法使用了两个辅助数组,一个是aux, 一个是count。本质上,使用aux只是为了保证稳定性。aux当然可以舍弃,但是这样就不会有稳定性了。

4.参考链接

  1. https://segmentfault.com/a/1190000012923917
  2. https://zh.wikipedia.org/wiki/%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F
  3. https://zh.wikipedia.org/wiki/%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F
  4. https://blog.csdn.net/liujianfeng1984/article/details/48488597
  5. https://blog.csdn.net/xuelabizp/article/details/50781616

【算法】计数排序、桶排序和基数排序详解

标签:注意   har   时间   ack   概念   name   bucket   空间复杂度   地方   

原文地址:https://www.cnblogs.com/imilano/p/9655754.html


评论


亲,登录后才可以留言!