排序(二)键索引、桶排序、位示图、败者树等

排序(二)

以上排序算法都有一个性质:在排序的最终结果中,各元素的次序依赖于它们之间的比较。我们把这类排序算法称为比较排序

任何比较排序的时间复杂度的下界是nlgn。

以下排序算法是用运算而不是比较来确定排序顺序的。因此下界nlgn对它们是不适用的。

键索引计数法(计数排序)

计数排序假设n个输入元素中的每一个都是在0到k区间的一个整数,其中k为某个整数。

思想:对每一个输入元素x,确定小于x的元素个数。利用这一信息,就可以直接把x放到它在输出数组中的位置了。

例如:

学生被分为若干组,标号为1,、2、3、4等,在某些情况下我们希望将全班同学按组序号排序分类。

1.频率统计:

第一步就是使用int数组cout[]计算每个键出现的频率。

对于数组中的每个元素,都使用它的键访问count[]中的相应元素并将其加1。(即把键值作为cout[]的索引)如果键值为r,则将count[r+1]加1.(为什么需要加1?稍后解释)

for (i=0; i<N; i++)

count[a[i].key()+1]++ ;

count[0~5]:0 0 3 5 6 6

2.将频率转换为索引:

接下来,我们会使用count[]来计算每个键在排序结果中的起始索引位置。在这个示例中,因为第一组中有3个人,第二组中有5个人,因此第三组中的同学在排序结果数组中的起始位置为8。

对于每个键值r,小于r+1的键的频率之和为小于r的键的频率之和加上count[r],因此从左向右将count[]转化为一张用于排序的索引表是很容易的。

for (int r=0; r<R; r++)

count[r+1] += count[r] ;

count[0~5]:0 0 3 8 14 20

3. 数据分类:

在将count[]数组转换为一张索引表之后,将所有元素(学生)移动到一个辅助数组aux[]中以进行排序。每个元素在aux[]中的位置是由它的键(组别)对应的count[]值决定的,在移动之后将count[]中对应元素的值加1,以保证count[r]总是下一个键为r的元素在aux[]中的索引位置。这个过程只需遍历一遍数据即可产生排序结果

(这种实现方式的稳定性是很关键的——键相同的元素在排序后会被聚集到一起,但相对顺序没有变化。)

for (int i=0; i<N; i++)

aux[count[a[i].key()]++] = a[i] ;

4. 回写:

因此我们在将元素移动到辅助数组的过程中完成了排序,所以最后一步就是将排序的结果复制回原数组中。

for (int i=0; i<N; i++)

a[i] = aux[i] ;

特点:键索引计数法是一种对于小整数键排序非常有效却常常被忽略的排序方法。

键索引计数法不需要比较,只要当范围R在N的一个常数因子范围之内,它都是一个线性时间级别的排序方法。

基数排序

有时候,我们需要对长度都相同的字符串进行排序。这种情况在排序应用中很常见——比如电话号码、银行账号、IP地址等都是典型的定长字符串。

将此类字符串排序可以通过低位优先的字符串排序来完成。如果字符串的长度均为W,那就从右向左以每个位置的字符作为键,用键索引计数法(或插入排序)将字符串排序W遍。

(为了确保基数排序的正确性,一位数排序算法必须是稳定的。例如:计数排序、插入排序)

特点:基数排序是否比基于比较的排序算法(如快速排序)更好呢?

基数排序的时间复杂度为线性级(n),这一结果看上去要比快速排序的期望运行时间代价(nlgn)更好一些。但是,在这两个表达式中隐含在背后的常数项因子是不同的。

在处理的n个关键字时,尽管基数排序执行的循环轮数会比快速排序要少,但每一轮它所耗费的时间要长得多。且快速排序通常可以比基数排序更有效地使用硬件的缓存。

此外,利用计数排序作为中间稳定排序的基数排序不是原址排序,而很多nlgn时间的比较排序是原址排序。因此,当主存的容量比较宝贵时,我们可能会更倾向于像快速排序这样的原址排序。

桶排序

桶排序(bucket sort)假设输入数据服从均匀分布,其独立分布在[0,M)区间上。平均情况下它的时间代价为O(n)。

思想:桶排序将[0,M)区间划分为n个相同大小的子区间,或称为

然后,将n个输入数分别放到各个桶中。因为输入数据时均匀、独立地分布在[0,M)区间上,所以一般不会出现很多数落在同一个桶中的情况。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。

(桶平排序算法还需要一个临时数组B[0..n-1]来存放链表(即桶),并假设存在一种用于维护这些链表的机制)

(有点像哈希表的拉链法的处理方式。)

位示图

思想:用比特位的相对位置(索引)来表示一个数值。

即就像用数组的下标来表示一个数值那样,只不过为了节省内存我们用一个bit的位置来标记一个数。

例如:我们可以将集合{1, 2, 3, 5,8, 13}存储在下面这个字符串中:0 1 1 1 0 10 0 1 0 0 0 0 1 0 0 0 0 0 0 集合中代表数字的各个位设置为1,而其他的位全部都设为0。

特点:位示图法适用的问题是(该情况在排序问题中不太常见):

输入的范围相对要小些,并且还不包含重复数据,且没有数据与记录相关联。

【应用举例】

考虑这样一个问题:给一个磁盘文件排序。(具体描述如下)

输入

所输入的是一个文件,至多包含n个不重复的正整数,每个正整数都要小于n,这里n=10^7. 这些整数没有与之对应的记录相关联。(即仅对这些整数排序)

输出

以增序形式输出经过排序的整数列表。

约束

至多只有1MB的可用主存,但是可用磁盘空间非常充足。10秒钟是最适宜的运行时间。

看到磁盘文件排序,我们首先想到经典的多路归并排序。(后面会讲到)

一个整数为32位,我们可以在1MB空间中存储250000个数。因此,我们将使用一个在输入文件中带有40个通道的程序。在第一个通道中它将249999之间的任意整数读到内存中,并(至多)对250000个整数进行排序,然后将它们写到输出文件中。第二个通道对250000到499999之间的整数进行排序,依此类推,直到第40个通道,它将排序9750000到9999999之间的整数。在内存中,我们用快速排序,然后把排序的有序序列进行归并,最终得到整体有序。

但是,此方式的效率较低,光是读取输入文件就需要40次,还有外部归并的IO开销。

怎样降低IO操作的次数,来提高程序的效率?一次把这一千万个数字全部读入内存?

用位图的方式,我们将使用一个具有一千万个bit位来表示该文件,在该bit位串中,当且仅当整数i在该文件中时,第i位才打开(设为1)。

给定了表示文件中整数集合的位图数据结构后,我们可以将编写该程序的过程分为三个自然阶段,第一个阶段关闭所有的位,将集合初始化为空集。第二个阶段读取文件中的每个整数,并打开相应的位,建立该集合。第三个阶段检查每个位,如果某个位是1,就写出相应的整数,从而创建已排序的输出文件。

内部排序方法总结

稳定性

如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的

这个性质在许多情况下很重要。

例如

考虑一个需要处理大量含有地理位置和时间戳的事件的互联网商业程序。

首先,我们在事件发生时将它们挨个存储在一个数组中,这样在数组中它们已经是按时间排序好了的。现在再按照地理位置切分,如果排序算法不是稳定的,排序后的每个城市的交易可能不会再是按照时间顺序排序的了。

算法         是否稳定

选择排序         否

插入排序     是

希尔排序         否

快速排序         否

三向快速排序 否

归并排序         是

堆排序             否

键索引计数   是

基数排序         是

快速排序是最快的通用排序算法。

快速排序之所以快是因为它的内循环中的指令很少(而且它还能利用缓存,因为它总是顺序地访问数据),所以它的运行时间的增长数量级为~cNlgN,而这里的c比其他线性对数级别的排序算法的相应常数都要小。

且,在使用三向切分之后,快速排序对于实际应用中可能出现的某些分布的输入变成线性级别的了,而其他的排序算法仍然需要线性对数时间。

如果稳定性很重要而空间又不是问题,归并排序可能是最好的。

----------------------------------------------------------------------外部排序---------------------------------------------------------------------

(我们为什么要进行外部排序?为什么不在插入数据时就按照某种数据结构组织,方便查找且有序。这就像静态查找树那样,没什么实用功能)

外部排序基本上由两个相对独立的阶段组成。

首先,按可用内存大小,将外存上含有n个记录的文件分成若干长度为l的子文件,依次读入内存并利用有效的内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写入外存,通常称这些有序子文件为归并段

然后,对这些归并段进行逐趟归并,使归并段逐渐由小至大,直到得到整个有序文件为止。

【例】假设有一个含有10000个记录的文件,首先通过10次内部排序得到10个初始归并段R1~R10,其中每一段都含有1000个记录。然后对它们作两两归并,直至得到一个有序文件为止。

每一趟归并从m个归并段得到m/2个归并段。这种归并方法称为2-路平衡归并。

若对上例中所得的10个初始归并段进行5-路平衡归并,则从下图可见,仅需进行二趟归并,外排时总的IO读/写次数显著减少。

一般情况下,对m个初始归并段进行k-路平衡归并时,归并的趟数s = logkm

可见,若增加k或减少m便能减少s。

一般的归并merge,每得到归并后的有序段中的一个记录,都要进行k-1次比较。显然,为得到含u个记录的归并段需进行(u-1)(k-1)次比较。

内部归并过程中总的比较次数为:

logkm (k-1) (u-1)tmg  =( log2m/ log2k)(k-1) (u-1)tmg

所以,要单纯地增加k将导致内部归并的时间,这将抵消由于增大k而减少外存信息读写时间所得效益。

然而,若在进行k-路归并时利用败者树(Tree of Loser),则可使在k个记录中选出关键字最小的记录时仅需进行log2k次比较。则总的归并时间变为log2m (u-1)tmg此式与k无关,它不再随k的增长而增长。

败者树

它是树形选择排序的一种变型。每个非终端结点均表示其左、右孩子结点中的“败者”。而让胜者去参加更高一层的比赛,便可得到一颗“败者树”(所谓“胜者”就是你想选出来的元素)。

以一颗实现5-路(k=5)归并的败者树为例:

数组ls[0…k-1]表示败者树中的非终端结点。败者树中根结点ls[1]的双亲结点ls[0]为“冠军”,其他结点记录的是其左右子树中的“败者”的索引值。b[0…k-1]是待比较的各路归并序列的首元素。

ls[]中除首元素外,其他元素表示为完全二叉树。

那表示叶子结点的b[]该如何与之对应?

叶结点b[x]的父结点是ls[(x+k)/2]。

败者树的建立:

1、  初始化败者树:把ls[0..k-1]中全设置为MINKEY(可能的最小值,即“绝对的胜者”)

//我们设一个b[k]= MINKEY,ls[]中记录的是b数组中的索引值。故初始为5.

2、从各叶子结点溯流而上,调整败者树中的值。

拿胜者s(初始为叶结点值)与其父结点中值比较,谁败(较大的)谁上位(留着父结点中),胜者被记录在s中。(决出胜者,记录败者,胜者向上走)

//对于叶结点b[4],调整的结果如下:

//对于叶结点b[3],调整的结果如下

//同理,对于叶结点b[2],调整的结果如下

//同理,对于叶结点b[1],调整的结果如下

//同理,对于叶结点b[0],调整的结果如下

void CreateLoserTree(LoserTree &ls)
{
    b[k].key = MINKEY ;

    //设置ls中“败者”的初值
    for (i=0; i<k; ++i)
        ls[i] = k ;

    //依次从b[k-1]...b[0]出发调整败者
    for (i=k-1; i>=0; --i)
        Adjust(ls, i) ;
}

void Adjust(LoserTree &ls, int m)
{
    //沿从叶节点b[m]到根结点ls[0]的路径 调整败者树
    for (i = (m + k)/2; i>0; i=i/2)  //ls[i]是b[m]的双亲结点
    {
        if (b[m].key > b[ls[i]].key)
            exch(m, ls[i]) ;         //m保存新的胜者的索引
    }
    ls[0] = m ;
}

【后记】

从n个数中选出最小的,我们为什么要用败者树?

首先,我们想到用优先队列,但其应对这种多路归并的情况,效率并不高。

堆结构:其待处理的元素都在树结点中(在叶节点和非叶子节点中)

败者树:其待处理的元素都在树的叶子结点上,其非叶子结点上记录上次其子结点比较的结果。

这样的话,堆结构的某个叶子结点不是对应固定的某个待归并序列。一次选出最值之后,还得取出各归并序列的首元素,重建堆再调整,不能利用之前比较的结果。

而败者树,一个叶结点固定地对应一个归并序列,这样,若其序列的首元素被选出,则序列的下个元素可以直接增补进入结点,然后沿树的路径向上比较。

总结:堆结构适用于插入是无规则的,选出最值。

败者树适用于多路序列的插入,选出最值。

排序(二)键索引、桶排序、位示图、败者树等

时间: 2024-10-13 20:57:57

排序(二)键索引、桶排序、位示图、败者树等的相关文章

排序算法(七)非比较排序:计数排序、基数排序、桶排序

前面讲的是比较排序算法,主要有冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等. 非比较排序算法:计数排序,基数排序,桶排序.在一定条件下,它们的时间复杂度可以达到O(n). 一,计数排序(Counting Sort) (1)算法简介 计数排序(Counting sort)是一种稳定的排序算法.计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数.然后根据数组C来将A中的元素排到正确的位置.它只能对整数进行排序. (2)算法描述和实现 得到待排序数的范围(在

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

一.计数排序 稳定. 当输入的元素是n 个小区间(0到k)内整数时,它的运行时间是 O(n + k),空间复杂度是O(n). const int K = 100; //计数排序:假设输入数据都属于一个小区间内的整数,可用于解决如年龄排序类的问题 //Input:A[0, ..., n-1], 0 <= A[i] < K //Output:B[0, ..., n-1], sorting of A //Aux storage C[0, ..., K) void CountSort(int A[],

线性排序算法---- 计数排序, 基数排序, 桶排序

排序算法里,除了比较排序算法(堆排序,归并排序,快速排序),还有一类经典的排序算法-------线性时间排序算法.听名字就让人兴奋! 线性时间排序,顾名思义,算法复杂度为线性时间O(n) , 非常快,比快速排序还要快的存在,简直逆天.下面我们来仔细看看三种逆天的线性排序算法, 计数排序,基数排序和桶排序. 1计数排序  counting Sort 计数排序 假设 n个 输入元素中的每一个都是在 0 到 k 区间内的一个整数,当 k= O(N) 时,排序运行的时间为 O(N). 计数排序的基本思想

经典算法之排序问题(二):桶排序、鸽巢排序

鸽巢排序: 鸽巢排序, 也被称作基数分类, 是一种时间复杂度为(Θ(n))且在不可避免遍历每一个元素并且排序的情况下效率最好的一种排序算法. 但它只有在差值(或者可被映射在差值)很小的范围内的数值排序的情况下实用. 当涉及到多个不相等的元素, 且将这些元素放在同一个"鸽巢"的时候, 算法的效率会有所降低.为了简便和保持鸽巢排序在适应不同的情况, 比如两个在同一个存储桶中结束的元素必然相等 我们一般很少使用鸽巢排序, 因为它很少可以在灵活性, 简便性, 尤是速度上超过其他排序算法. 事实

线性排序之基数排序,桶排序,计数排序

基数排序 计数排序 桶排序 基数排序,桶排序,计数排序是三种线性排序方法,突破了比较排序的O(nlogn)的限制.但是只适用于特定的情况. 基数排序 以下为维基百科的描述: 基数排序 : 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零.然后,从最低位开始,依次进行一次排序.这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列. 基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digit

算法导论-- 线性时间排序(计数排序、基数排序、桶排序)

线性时间排序 前面介绍的几种排序,都是能够在复杂度nlg(n)时间内排序n个数的算法,这些算法都是通过比较来决定它们的顺序,这类算法叫做比较排序 .下面介绍的几种算法用运算去排序,且它们的复杂度是线性时间. -------------------------------------- 1.计数排序 计数排序采用的方法是:对每个元素x,去确定小于x的元素的个数,从而就可以知道元素x在输出数组中的哪个位置了. 计数排序的一个重要性质是它是稳定的,即对于相同的两个数,排序后,还会保持它们在输入数组中的

计数排序、基数排序及桶排序

计数排序思想: 对每一个元素,确定小于其的元素个数,利用这一信息即可将其放入正确的位置. 计数排序时间复杂度:Θ(n) 计数排序示例: from random import randint def counting_sort(a,b,max_number): #用于记录各元素出现次数 c = [] for i in range(max_number+1): c.append(0) for number in a: c[number] += 1 for i in range(1,max_numbe

排序算法四(桶排序)

一.桶排序算法的引入. 之前我们已经说过了计数排序的算法. 这个时候我们如果有这样的一个待排序数据序列: int x[14]={-10, 2, 3, 7, 20, 23, 25, 40, 41, 43,60, 80, 90, 100}; 我们如果按照计数排序的算法,那么待排序数据的范围是:-10 到 100 我们为了实现对于这个数据集的遍历,这个时候需要额外的开辟一个大小为(100- (-10)+ 1)的空间,这个时候的空间复杂度达远远超过我们的预计,也就是这个造成了空间的浪费. 所以我们可以说

常见排序算法导读(11)[桶排序]

上一节讲了基数排序(Radix Sort),这一节介绍桶排序(Bucket Sort or Bin Sort).和基数排序一样,桶排序也是一种分布式排序. 桶排序(Bucket Sort)的基本思想 将待排对象序列按照一定hash算法分发到N个桶中 对每一个桶的待排对象进行排序 从头到尾遍历N个桶,收集所有非空的桶里的有序对象(子序列)组成一个统一的有序对象序列 在每一个桶中,如果采用链式存储的话,1.和2.可以合并在一起操作,也就是在分发的过程中保证每一个桶的对象桶内有序. 例如: 设有5个桶