序列(两)密钥索引、桶排序、位图、失败者树(照片详细解释--失败者树)

序列(两)

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

不论什么比較排序的时间复杂度的下界是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[]转化为一张用于排序的索引表是非常easy的。

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个keyword时,虽然基数排序运行的循环轮数会比高速排序要少。但每一轮它所耗费的时间要长得多。且高速排序通常能够比基数排序更有效地使用硬件的缓存。

此外,利用计数排序作为中间稳定排序的基数排序不是原址排序。而非常多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个记录。然后对它们作两两归并。直至得到一个有序文件为止。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

每一趟归并从m个归并段得到m/2个归并段。

这样的归并方法称为2-路平衡归并。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

普通情况下,对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个记录中选出keyword最小的记录时仅需进行log2k次比較

则总的归并时间变为log2m (u-1)tmg此式与k无关。它不再随k的增长而增长。

败者树

它是树形选择排序的一种变型。

每一个非终端结点均表示其左、右孩子结点中的“败者”。

而让胜者去參加更高一层的比赛。便可得到一颗“败者树”(所谓“胜者”就是你想选出来的元素)。

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

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

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

那表示叶子结点的b[]该怎样与之相应?

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

败者树的建立:

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

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

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

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

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

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

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWFuZ195dWxlaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

//同理,对于叶结点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-29 19:09:48

序列(两)密钥索引、桶排序、位图、失败者树(照片详细解释--失败者树)的相关文章

[POJ] #1003# 487-3279 : 桶排序/字典树(Trie树)/快速排序

一. 题目 487-3279 Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 274040   Accepted: 48891 Description Businesses like to have memorable telephone numbers. One way to make a telephone number memorable is to have it spell a memorable word or

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

排序(二) 以上排序算法都有一个性质:在排序的最终结果中,各元素的次序依赖于它们之间的比较.我们把这类排序算法称为比较排序. 任何比较排序的时间复杂度的下界是nlgn. 以下排序算法是用运算而不是比较来确定排序顺序的.因此下界nlgn对它们是不适用的. 键索引计数法(计数排序) 计数排序假设n个输入元素中的每一个都是在0到k区间的一个整数,其中k为某个整数. 思想:对每一个输入元素x,确定小于x的元素个数.利用这一信息,就可以直接把x放到它在输出数组中的位置了. 例如: 学生被分为若干组,标号为

对聚集表查询的时候,未显式指定排序列的时候,默认查询结果的顺序一定是按照聚集索引顺序排序的吗

在sql server 中,如果一张表存在聚集索引的时候,大多数情况下,如果进行select * from TableName查询,默认的返回顺序是按照聚集所在列的顺序返回的 但是,在一张表存在聚集索引的时候,并不一定所有的情况都是按照聚集索引列的顺序排列的, 下面开始测试 create table TestDefaultOrder ( Id int identity(1,1) primary key,--主键上默认会建立聚集索引 Col2 char(5), COL3 char(5) ) --写

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

01.计数排序.桶排序与基数排序 并不是所有的排序 都是基于比较的,计数排序和基数排序就不是.基于比较排序的排序方法,其复杂度无法突破\(n\log{n}\) 的下限,但是 计数排序 桶排序 和基数排序是分布排序,他们是可以突破这个下限达到O(n)的的复杂度的. 1. 计数排序 概念 计数排序是一种稳定的线性时间排序算法.计数排序使用一个额外的数组C,使用 C[i] 来计算 i 出现的次数.然后根据数C来将原数组A中的元素排到正确的位置. 复杂度 计数排序的最坏时间复杂度.最好时间复杂度.平均时

桶排序

假设现在有一组小于M的正整数 a1. a2 ,-- ,an ,对它们排序可以采用以下的思路:使用一个大小为M的数组buckets,这个数组的每一个单元称为一个个的bucket,桶,初始化全部为0.扫描数组a,当扫描到ai的时候,buckets[ai] 加1.这样当a扫描完之后,扫描buckets,打印非零单元的下标,它的值是几就打印几次.打印出来的值实际上就是排好序之后的数组a了.我们可以依次把它们赋值给a,使得a有序. 代码如下: #include<iostream> using names

leetcode算法题3:分组,让每个组的最小者,相加之后和最大。想知道桶排序是怎么样的吗?

/* Given an array of 2n integers, your task is to group these integers into n pairs of integer, say (a1, b1), (a2, b2), -, (an, bn) which makes sum of min(ai, bi) for all i from 1 to n as large as possible. Example 1: Input: [1,4,3,2] Output: 4 Expla

Oracle索引总结(四)- Oracle索引种类之位图索引

位图索引 1.1 位图索引概述 位图索引通过位图向量,表示索引键值在表中的分布. 适用于没有大量更新操作的对象,如:OLAP数据库. 对于存在大量更新操作的索引列,不适用位图索引.因此对于OLTP并不适用. 更新位图向量时,相应位图涉及的所有数据行会被锁定,无法针对这些数据行的该索引列进行DML操作. 1.2 位图索引结构的说明 与B-tree索引的联系及区别如下: 与B-tree索引的联系:位图索引使用B-tree形式组成. 与B-tree索引的区别:位图索引的一个索引键值对应一个叶子节点.(

8-4.桶排序算法详解

1. 桶排序介绍 桶排序(Bucket sort)是一种基于计数的排序算法,工作的原理是将数据分到有限数量的桶子里,然后每个桶再分别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序).当要被排序的数据内的数值是均匀分配的时候,桶排序时间复杂度为Θ(n).桶排序不同于快速排序,并不是比较排序,不受到时间复杂度 O(nlogn) 下限的影响. 桶排序按下面4步进行: 1. 设置固定数量的空桶. 2. 把数据放到对应的桶中. 3. 对每个不为空的桶中数据进行排序. 4. 拼接从不为空

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

说基数排序之前,我们先说桶排序: 基本思想:是将阵列分到有限数量的桶子里.每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序).桶排序是鸽巢排序的一种归纳结果.当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n)).但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响.          简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序. 例如要对大小为[1..1000]范围内的n个整数A[1..n]排序 首