基数排序和前几篇博客中写到的排序方法完全不同。前面几种排序方法主要是通过关键字间的比较和移动记录这两种操作来实现排序的,而实现基数排序不需要进行记录项间的比较。而是把关键字按一定规则分布在不同的区域,然后再重新整合,使之有序,属于分布排序的一种。

基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。基数排序是借助“分配”和“收集”两种操作对单逻辑关键字进行排序的一种内部排序方法。

下面举一个书上的例子,扑克牌的排序。对于扑克牌,在比较任意两张牌的大小时(暂不考虑大王和小王),首先要比较花色,因为花色的“地位”高于面值,比如黑桃>红心>方片>梅花;然后是面值的大小A>13>12>...>2,所以这里扑克牌的排序就牵涉到两种关键字。那么怎么来排序呢?这里有两种:

1. 遍历52张牌,根据花色分成4堆(分配操作)。然后对每一堆按面值大小整理有序,即每一花色堆中的牌再分成13堆(分配操作),这已经比较到了最后一个关键字,不能再往下分了,这时每堆只有一张。到最后一共有52堆(可以称之为52个子序列),最后将这些子序列依次联接在一起(收集操作)成为一个有序序列。

2.遍历52张牌,根据面值分成13堆(分配操作)。然后把这13堆摞起来(收集操作),按这样的顺序:面值最小(即面值为2)的堆放最下面,然后依次往上放,面值最大(即面值为A)的堆放最上面。这样13堆就又变成了一堆,再次遍历这一堆52张牌,根据花色不同分成4堆(分配操作)。这时候每个花色堆中的牌都是按面值有序的,比如黑桃堆,黑桃A在最下面,黑桃2在最上面。这时再把这四个花色堆按花色顺序堆在一起(收集操作),那么所有52张牌就都是有序的了。如果把黑桃堆放最下面,依次往上,梅花堆放最上面,那么牌的顺序就是递增的,反之则是递减的。

由此可见,基数排序根据多关键字,借助“分配”和“收集”的操作,并未有记录间的比较,就能使待排序列有序。

上面两种不同的方法其实引出了两种不同的基数排序:
最高位优先(Most Significant Digit first)法,简称MSD法。
最低位优先(Least Significant Digit first)法,简称LSD法。

以下是摘自数据结构严蔚敏版中的定义:

一般情况下,假设有n个记录的序列

{R1,R2,...,Rn}

且每个记录Ri 中含有d个关键字(Ki2, Ki1, ..., Kid-1 ),则称R序列对关键字(Ki2, Ki1, ..., Kid-1 )有序是指:对于序列中任意两个记录Ri 和 Rj (1<=i<=j<=n)都满足下列有序关系:

(Ki2, Ki1, ..., Kid-1 ) < (Kj2, Kj1, ..., Kjd-1 )

其中K0称为最主位关键字,Kd-1称为最次位关键字。

为实现多关键字排序,通常有两种方法:

第一种方法是:先对最主位关键字K0进行排序,将序列分成若干子序列,每个子序列中的记录都有相同的K0值,然后分别就每个子序列对关键字K1进行排序,按K1值不同再分成若干更小的子序列,依次重复,直到对Kd-2进行排序之后得到的每一子序列中的记录都有相同的关键字(K0, K1, ... , Kd-2),而后分别每个子序列对Kd-1进行排序,最后将所有子序列依次联接在一起成为一个有序序列,这种方法称之为最高位优先法,简称MSD法

第二种方法是从最次位关键字Kd-1起进行排序。然后再对高一位的关键字Kd-2进行排序,依次重复,直至对K0进行排序后便成为一个有序序列。这种方法称之为最低优先法,简称LSD法

从上所述,可以看出两种排序方法的不同特点:

若按MSD进行排序,必须将序列逐层分割成若干子序列,然后对各子序列分别进行排序,到最后一个关键字时,一次收集所有子序列。这样貌似要更多的辅助存储空间。

若按LSD进行排序,每按关键字分成子序列后(一次分配),就进行一次收集操作。然后再遍历所有记录,按更主关键字再分配,然后再收集,... ,,所有针对每个关键字都是针对整个序列参加排序。用分配和收集操作可以实现排序,当然用前几篇博客中提到普通排序方法也可以,但这些排序方法必须是稳定的,这是显而易见的,比如,二位数集全中36和35的比较,这里其实有2个关键字,个位,十位。按LSD来,先比较个位,这时36在前,35在后,然后比较十位,由于都是3,如果是不稳定排序的话,那么他们两个的位置就有可能互换。从而导致非完全有序的序列。

好了,上面讲了好多理论知识,下面是一个链式基数排序的算法:

这里我们就拿整数的比较来讲,比较简单。每一位(个,十,百...)都是一个关键字,每个关键字的范围都是0~9。但其实,许多情况下针对不同的关键字可能有不同的取值范围,可能就要做一些相应的调整。

按所有记录存储在一个链表结构中,每个记录是链表的一个结点中的value。然后根据关键字的取值范围建立(0~9,共10个)10个子链表。遍历链表中的元素,比较个位,按个位的值,把结点添加相应的子链表的尾部。比如365的个位5,那么就放在第5个子链表的尾部。这样就完成了一次分配。

然后将子链表从第0个到第9个首尾相连起来:header0...tail0->header1...tail1->... ... ->header9...tail9。这样就完成了一次收集。

然后再根据十位来比较,根据十位的值装入不同子链表中(此时的子链表是新的,可以是新建的,可以是处理清空过的,代码处理一下即可)。这样又完成了一次分配。

然后再首尾相连,完成一次收集。

。。。这要一直下去,比较完所有关键字,位数。即完成序列的排序。

下面是代码:

先要建立自定义的链表结点和链表的数据结构,我这里简单写了一下:

    // 单链表
class SingleLinkedList<T>
{
public MyLLNode<T> First
{
get;
set;
} public MyLLNode<T> Last
{
get;
set;
} public void AddLast(MyLLNode<T> node)
{
if (First == null)
{
First = node;
Last = node;
node.Next = null;
}
else
{
Last.Next = node;
Last = node;
node.Next = null;
}
}
} // 单链表结点
class MyLLNode<T>
{
public T Value
{
get;
set;
} public MyLLNode<T> Next
{
get;
set;
}
}

下面是排序的代码:

        private static void RandixSort(int[] myArray,int keyNum)
{
SingleLinkedList<int> listArray = new SingleLinkedList<int>();
foreach (int i in myArray)
{
listArray.AddLast(new MyLLNode<int>() { Value = i });
} for (int i = ; i < keyNum; i++)
{
// 对每个关键字执行分配和收集操作
DistributeAndCollect(listArray, i);
} int j = ;
while (listArray.First != null)
{
myArray[j++] = listArray.First.Value;
listArray.First = listArray.First.Next;
}
} // 分配和收集
private static void DistributeAndCollect(SingleLinkedList<int> listArray, int i)
{
int randix = ; //关键字取值范围
int divider = (int)Math.Pow(, i);
List<SingleLinkedList<int>> subLists = new List<SingleLinkedList<int>>(); //建立子序列
for (int j = ; j < randix; j++)
{
subLists.Add(new SingleLinkedList<int>());
} // 开始一次分配
while (listArray.First != null)
{
int index = (listArray.First.Value / divider) % ; MyLLNode<int> tempNode=listArray.First.Next;
subLists[index].AddLast(listArray.First);
listArray.First = tempNode;
} // 开始一次收集
int k = ;
for (; k < randix; k++)
{
if (subLists[k].First != null)
{
// 找到第一个非空子序列以设置总序列的First值
listArray.First = subLists[k].First;
listArray.Last = subLists[k].Last;
break;
}
} // 找好子序列设置好listArray.First后,开始处理非空子序列的首尾相连
for (; k < randix; k++)
{
if (subLists[k].First != null)
{
listArray.Last.Next = subLists[k].First;
listArray.Last = subLists[k].Last;
}
}
}

再次强调一点,只是这个示例中的每个关键字的处理情况一样,所以DistributeAndCollect方法中的处理比较简单,有时候需要针对不同的关键字作相应的调整,比如randix取值范围不一样的情况。

下面的调用:

     static void Main(string[] args)
{
int[] numbers = { , , , , , , , , , , , , };
RandixSort(numbers, ); foreach (int i in numbers)
{
Console.Write(i.ToString() + " ");
}
Console.Read();
}

最后,针对这个例子能看出,对于n个记录(假设每个记录含d个关键字,每个关键字的取值范围是randix个值)进行链式基数排序的时间复杂度为O(d(n+randix)),其中每一趟分配的时间复杂度为O(n),第一趟收集的时间复杂度为O(randix)[因为有randix个子链表],整个排序需进行d趟分配和收集。所需辅助空间为2*randix个引用,其实每个子序列也就保存了首尾两个引用,结点还是那么些个结点,只不过通过设置next指针把其分开了而已。

好了,这就是基数排序的全部内容。

小小c#算法题 - 9 - 基数排序 (Radix Sort)的更多相关文章

  1. 小小c#算法题 - 8 - 归并排序 (Merging Sort)

    “归并”的含义是将两个或两个以上的有序序列组合成一个新的有序序列.这个“归并”可以在O(n+m)的数量级上实现,但这同时也需要O(n+m)的空间复杂度.具体为:首先分配一个新的长度为n+m的空序列,然 ...

  2. 小小c#算法题 - 7 - 堆排序 (Heap Sort)

    在讨论堆排序之前,我们先来讨论一下另外一种排序算法——插入排序.插入排序的逻辑相当简单,先遍历一遍数组找到最小值,然后将这个最小值跟第一个元素交换.然后遍历第一个元素之后的n-1个元素,得到这n-1个 ...

  3. 经典排序算法 - 基数排序Radix sort

    经典排序算法 - 基数排序Radix sort 原理类似桶排序,这里总是须要10个桶,多次使用 首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,临时忽视十位数 比如 待排序数组[ ...

  4. 小小c#算法题 - 11 - 二叉树的构造及先序遍历、中序遍历、后序遍历

    在上一篇文章 小小c#算法题 - 10 - 求树的深度中,用到了树的数据结构,树型结构是一类重要的非线性数据结构,树是以分支关系定义的层次结构,是n(n>=0)个结点的有限集.但在那篇文章中,只 ...

  5. 基数排序(radix sort)

    #include<iostream> #include<ctime> #include <stdio.h> #include<cstring> #inc ...

  6. 学习算法-基数排序(radix sort)卡片分类(card sort) C++数组实现

    基数排序称为卡片分类,这是一个比较早的时间越多,排名方法. 现代计算机出现之前,它已被用于排序老式打孔卡. 说下基数排序的思想.前面我有写一个桶式排序,基数排序的思想是桶式排序的推广. 桶式排序:ht ...

  7. [转] 经典排序算法 - 基数排序Radix sort

    原理类似桶排序,这里总是需要10个桶,多次使用 首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,暂时忽视十位数 例如 待排序数组[62,14,59,88,16]简单点五个数字 分 ...

  8. 桶排序/基数排序(Radix Sort)

    说基数排序之前,我们先说桶排序: 基本思想:是将阵列分到有限数量的桶子里.每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序).桶排序是鸽巢排序的一种归纳结果.当要被排序 ...

  9. 小小c#算法题 - 6 - 快速排序 (QuickSort)

    快速排序是排序算法中效率比较高的一种,也是面试常被问到的问题. 快速排序(Quick Sort)是对冒泡排序的一种改进.它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字 ...

随机推荐

  1. rabbitmq的vhost与用户管理

    当我们在创建用户时,会指定用户能访问一个虚拟机,并且该用户只能访问该虚拟机下的队列和交换机,如果没有指定,默认的是”/”;一个rabbitmq服务器上可以运行多个vhost,以便于适用不同的业务需要, ...

  2. C++语言对C的增强(1)——实用性、变量检测、struct类型、C++中所有变量和函数都必须有类型、bool类型、三目运算符

    1.“实用性”增强 C语言中的变量都必须在作用域开始的位置定义,C++中更强调语言的“实用性”,所有的变量都可以在需要使用时再定义. 2.变量检测加强 在C语言中,重复定义多个同名的全局变量是合法的: ...

  3. ping错误详解

    在网络中Ping 是一个十分好用的TCP/IP工具,它主要的功能是用来检测网络的连通情况和分析网络速度. 输入 ping /? 例出ping的参数 使用Ping检查连通性有五个步骤 1. 使用ipco ...

  4. 浅析TCP /UDP/ IP协议

    互连网早期的时候,主机间的互连使用的是NCP协议.这种协议本身有很多缺陷,如:不能互连不同的主机,不能互连不同的操作系统,没有纠错功能.为了改善这种缺点,大牛弄出了TCP/IP协议.现在几乎所有的操作 ...

  5. java数组复制的几种常见用法

    1.1数组复制的几种常见用法 1.1.1System.arraycopy的用法 int[] src = {1,3,5,7,9,11,13,15,17}; int[] dest = {2,4,6,8,1 ...

  6. Oracle 静默安装oracle client

    静默安装oracle clint比较简单,修改instantclient.crsp文件的几个位置即可 [root@localhost ~]# vi /etc/oralnstloc inventory_ ...

  7. List<T> JIT 分配策略

    参考 http://www.cnblogs.com/brookshi/p/5353021.html defaultCapacity意思是new List<T>时默认大小是4. _items ...

  8. Git学习笔记(二)分支管理与合并及Bug分支

    一.分支管理 1.什么是分支 分支就相当于我们看科幻片里的平行宇宙,如果两个平行宇宙互不干扰,那铁定是啥事儿没有.不过,在某个时间点,两个平行宇宙合并了呢?假如两个宇宙中都有你的影子, 合并之后相当于 ...

  9. python的raw_input()函数。 函数的可变对象和不可变对象作为参数传递。

    python的raw_input()函数, 接受键盘输入, 其返回值是字符串类型, 所以当输入的是数字时, 如果是想参与算术运算, 必须要对其进行类型转换. python的参数传递, 对于可变对象和不 ...

  10. 2015.12.12 DataGridveiw中添加checkbox列

    最简单的办法是通过DataTable来添加 DataTable中添加bool类型的列 dtpdf.Columns.Add("入库", typeof(bool)); DataRow ...