排序算法九:基数排序

排序算法九:基数排序


声明:引用请注明出处http://blog.csdn.net/lg1259156776/


引言

在我的博文《“主宰世界”的10种算法短评》中给出的首个算法就是高效的排序算法。本文将对排序算法做一个全面的梳理,从最简单的“冒泡”到高效的堆排序等。

系列博文的前八篇讲述了插入排序、交换排序、选择排序和归并排序等四种不同类型,本文将讲述第五大类的排序算法:基数排序


排序相关的的基本概念

  • 排序:将一组杂乱无章的数据按一定的规律顺次排列起来。
    • 数据表( data list): 它是待排序数据对象的有限集合。
    • 排序码(key):通常数据对象有多个属性域,即多个数据成员组成,其中有一个属性域可用来区分对象,作为排序依据。该域即为排序码。每个数据表用哪个属性域作为排序码,要视具体的应用需要而定。
  • 分类
    • 内排序:指在排序期间数据对象全部存放在内存的排序;
    • 外排序:指在排序期间全部对象个数太多,不能同时存放在内存,必须根据排序过程的要求,不断在内、外存之间移动的排序。

排序算法的分析

排序算法的稳定性

如果在对象序列中有两个对象r[i]r[j] ,它们的排序码k[i]==k[j] 。如果排序前后,对象r[i]r[j] 的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。

排序算法的评价

时间开销

  • 排序的时间开销可用算法执行中的数据比较次数与数据移动次数来衡量。
  • 算法运行时间代价的大略估算一般都按平均情况进行估算。对于那些受对象排序码序列初始排列及对象个数影响较大的,需要按最好情况和最坏情况进行估算

空间开销

算法执行时所需的附加存储。


基数排序

基本思想

不进行关键字的比较,而是利用”分配”和”收集”。

PS:以十进制为例,基数指的是数的位,如个位,十位百位等。而以十六进制为例,0xB2,就有两个radices(radix的复数)。

Least significant digit(LSD)

短的关键字被认为是小的,排在前面,然后相同长度的关键字再按照词典顺序或者数字大小等进行排序。比如1,2,3,4,5,6,7,8,9,10,11或者”b, c, d, e, f, g, h, i, j, ba” 。

Most significance digit(MSD)

直接按照字典的顺序进行排序,对于字符串、单词或者是长度固定的整数排序比较合适。比如:1, 10, 2, 3, 4, 5, 6, 7, 8, 9和 “b, ba, c, d, e, f, g, h, i, j”。

基数排序图示

这里写图片描述
这里写图片描述

从图示中可以看出基数排序(LSD)的基本流程为如下节。

基数排序流程

将根据整数的最右边数字将其扔进相应的0~9号的篮子里,对于相同的数字要保持其原来的相对顺序(确保排序算法的稳定性),然后将篮子里的数如图所示的串起来,然后再进行第二趟的收集(按照第二位的数字进行收集),就这样不断的反复,当没有更多的位时,串起来的数字就是排好序的数字。

算法分析

空间

采用顺序分配,显然不合适。由于每个口袋都有可能存放所有的待排序的整数。所以,额外空间的需求为10n,太大了。采用链接分配是合理的。额外空间的需求为n,通常再增加指向每个口袋的首尾指针就可以了。在一般情况下,设每个键字的取值范围为d, 首尾指针共计2×radix个,总的空间为O(n+2×radix)

时间

上图示中每个数计有2 位,因此执行2 次分配和收集就可以了。在一般情况下,每个结点有d 位关键字,必须执行d 次分配和收集操作。

  • 每次分配的代价:O(n)
  • 每次收集的代价:O(radix)
  • 总的代价为:O(d×(n+radix))

算法的c plus plus实现

基于LSD的基数排序算法:

#include <iostream>

using namespace std;
const int MAX = 10;

void print(int *a,int sz) {               
    for(int i = 0; i < sz; i++)
        cout << a[i] << " ";
    cout << endl;
}

void RadixSortLSD(int *a, int arraySize)
{
    int i, bucket[MAX], maxVal = 0, digitPosition =1 ;
    for(i = 0; i < arraySize; i++) {
        if(a[i] > maxVal) maxVal = a[i];
    }

    int pass = 1;  // used to show the progress
    /* maxVal: this variable decide the while-loop count 
               if maxVal is 3 digits, then we loop through 3 times */
    while(maxVal/digitPosition > 0) {
        /* reset counter */
        int digitCount[10] = {0};

        /* count pos-th digits (keys) */
        for(i = 0; i < arraySize; i++)
            digitCount[a[i]/digitPosition%10]++;

        /* accumulated count */
        for(i = 1; i < 10; i++)
            digitCount[i] += digitCount[i-1];

        /* To keep the order, start from back side */
        for(i = arraySize - 1; i >= 0; i--)
            bucket[--digitCount[a[i]/digitPosition%10]] = a[i];

        /* rearrange the original array using elements in the bucket */
        for(i = 0; i < arraySize; i++)
            a[i] = bucket[i];

        /* at this point, a array is sorted by digitPosition-th digit */
        cout << "pass #" << pass++ << ": ";
        print(a,arraySize);

        /* move up the digit position */
        digitPosition *= 10;
    }   
 }

int main()
{
    int a[] = {170, 45, 75, 90, 2, 24, 802, 66};
    const size_t sz = sizeof(a)/sizeof(a[0]);

    cout << "pass #0: ";
    print(a,sz);
    RadixSortLSD(&a[0],sz);
    return 0;
}

输出为:

pass #0: 170 45 75 90 2 24 802 66 
pass #1: 170 90 2 802 24 45 75 66 
pass #2: 2 802 24 45 66 170 75 90 
pass #3: 2 24 45 66 75 90 170 802 

这里写图片描述

首先统计10个篮子(或口袋)中各有多少个数字,然后从0~9数字的频次分布(而不是频次密度,有一个累加的过程),以确定“收集”整数时的位置下标所在。同时为了保证排序算法稳定,相同的数字保持原来相对位置不变,对原始数据表倒序遍历,逐个构成收集后的数据表。例如,上图所示,对于数字66,其所对应的频次分布为8,也就是应当排在第8位,在数组中下标应该为7。而如果对于数字2和802,对应的频次分布为4,那么对于数据表从后往前遍历的话,对应802的下标为3,而2的下标2,这样实际上就保证了排序算法的稳定性。


2015-9-28 艺少

原文地址:https://www.cnblogs.com/huty/p/8519091.html