390 likes | 512 Views
第十章 索引与散列 静态索引结构 动态索引结构 散列. 10.1 静态索引结构 10.1.1 线形索引 索引表:由一组关键码和对象地址组成的索引项构成。 线形索引:在一个线形表中存放对象的索引项。. 一个索引项对应数据表中一个对象的索引结构叫做稠密索引。 数据表对象在外存中按加入的顺序存放而不是按关键码有序存放的索引结构。称为索引非顺序结构。 如果对象在外存中有序存放 , 可以采用子表索引 . 子表索引要求做到分块有序。即后一个子表中所有的关键码均大于前一个子表中所有对象的关键码。.
E N D
第十章 索引与散列 • 静态索引结构 • 动态索引结构 • 散列
10.1 静态索引结构 • 10.1.1 线形索引 • 索引表:由一组关键码和对象地址组成的索引项构成。 • 线形索引:在一个线形表中存放对象的索引项。
一个索引项对应数据表中一个对象的索引结构叫做稠密索引。一个索引项对应数据表中一个对象的索引结构叫做稠密索引。 • 数据表对象在外存中按加入的顺序存放而不是按关键码有序存放的索引结构。称为索引非顺序结构。 • 如果对象在外存中有序存放,可以采用子表索引.子表索引要求做到分块有序。即后一个子表中所有的关键码均大于前一个子表中所有对象的关键码。
图10.2所示的结构是索引顺序结构。它由索引表和子表构成。索引时首先在索引表中搜索给定值K,然后根据ID[i-1].max_key<K<=ID[i].max_key。找到待查子表序号i,再在第 i 个子表中查找。 索引顺序搜索成功的平均搜索长度为:ASLIndexSeq=ASLIndex+ASLSublist设把长度为n的表分成均等的b个子表,每个子表有S个对象,则b=n/s.又设表中每个对象的搜索概率相等。子表为1/b,子表内各对象为1/s,则顺序搜索的平均搜索长度为:ASLIndexSeq=(b+1)/2+(s+1)/2=(b+s)/2+1子表内折半搜索成功平均搜索程度为:ASLIndexSeq=log2(b+1)-1+(s+1)/2=log(1+n/s)+s/2可见,索引搜索的平均搜索长度与表的长度n和子表内对象的个数S有关。
10.1.2 倒排表 主关键码建立的索引为主搜索。非主关键码建立的索引叫次索引。次索引由次关键码、链表长度和链表本身组成。
倒排表是次索引的一种实现。在倒排表中所有次关键码的链都保存在次索引中。倒排表一般的搜索步骤是:首先通过次索引找到主索引的主关键码,再通过主关键码找到相应的对象。倒排表是次索引的一种实现。在倒排表中所有次关键码的链都保存在次索引中。倒排表一般的搜索步骤是:首先通过次索引找到主索引的主关键码,再通过主关键码找到相应的对象。 • 10.1.3 m路静态搜索树 • 当数据对象数目特别大,索引表本身也很大,在内存中放不下,可以建立索引的索引,称为二级索引。
如果二级索引内存中也放不下,可以建立多级索引,这种多级索引结构形成一种m叉树。如果二级索引内存中也放不下,可以建立多级索引,这种多级索引结构形成一种m叉树。 每一个分支结点表示一个索引块,最多有m个索引块,每个索引块分别给出各子树结点最大关键码和结点地址 树的叶结点中各索引项给出在数据表中存放的对象的关键码和存放地址。 这种m叉树用来作为多级索引,就是m路搜索树.
10.2 动态索引结构 • 动态的m路搜索树的定义为:一个空树或满足以下条件(1)根结点最多有m棵子树,并具有如下的结构:n,P0,(K1,P1),(K2,P2),…,(Kn,Pn)其中,Pi是指向子树的指针,0≤i≤n≤m;Ki是关键码 • (2)Ki<Ki+1,1≤i≤n.(3)在子树Pi中所有的关键码都小于Ki+1,且大于Ki,0≤i≤n
(4)在子树Pn中所有的关键码都大于Kn,而子树P0中的所有关键码都小于K1.(5)子树Pi也是m路搜索树,0≤i≤n.(4)在子树Pn中所有的关键码都大于Kn,而子树P0中的所有关键码都小于K1.(5)子树Pi也是m路搜索树,0≤i≤n. m路搜索树的C++描述:template<class Type>class Mtree{ protected: Mnode<Type>root; int m; public: Triple<Type>&search(const Type&); AVL树是2路搜索树。
已知m路搜索树的度为m和它的高度为h,则树的最大结点数为:已知m路搜索树的度为m和它的高度为h,则树的最大结点数为: Triple resulegetnode(root);mnode<Type>*p=root,*p=NULL;while (p!=NULL){ int I=0;p->key[(p->n)+1]=MAXKEY; while(p->key[I+1]<x)I++; if (p->key[I+1]==x){ result.r=p;result.I=I+1;result.tag=0; return result; } q=p;p=p->ptr[I];getnode(p); }result.r=p;result.I=I+1;result.tag=1;return result;}
对于给定的关键码数n,如果搜索树是平衡的,可以使m路搜索树的性能接近最佳.对于给定的关键码数n,如果搜索树是平衡的,可以使m路搜索树的性能接近最佳.
10.2.2 B树一棵m阶B树是一棵平衡的m路搜索树,它或者是空树,或满足下列条件。(1)根结点至少有两个子女(2)除根结点以外的所有结点(不包括失败结点)至少有m/2个子女。(3)所有的失败结点都位于同一层。
B树的类声明和B树结点类的声明如下:template<Type>class Btree;public Mtree<Type>{public: int insert(const Type&x); int remove(const Type &x);};template<class Type>class Mnode;private; int n; Mnode<Type>*parent; Type key[m+1]; Mnode<Type>*ptr[m+1];};struct Triple{ Mnode<Type>*r; int I;int tag;};
B树的搜索过程是一个在结点内搜索和循某一条路径向下一层搜索交替进行的过程。因此,B树的搜索时间与B树的阶层m和B树的高度h直接有关。B树的高度h与关键码个数N的关系为:h≤logm/2((N+1)/2)+1
10.2.3 B树的插入B树是从空树起,逐个插入关键码而生成的。但在插入关键码时,如果结点中关键码的个数超过m-1,则结点要发生“分裂” .否则直接插入.实现结点“分裂”的原则是:设结点P已经有m-1个关键码,当再插入一个关键码后结点中的状态为(m,P0,K1,P1,K2,P2,…,Km,Pm),其中Ki<K i+1,1≤I<m这是必须把结点分成p和q两个结点. 结点p:(m/2-1,P0,K1,P1,…,Km/2-1,Pm/2-1)结点q:(m-m/2,Pm/2,Km/2+1,Pm/2+1,…,Km,Pm)位于中间的关键码Km/2与指向新结点q的指针形成一个二元组(Km/2,q)插入到这两个结点的双亲结点中.
10.2.4 B树的删除 删除非叶结点的关键码.将被删关键码Ki删除,并从Pi所指示的子树中的最小关键码x代替Ki,然后将x所在的叶结点中将x删除.在叶结点上删除有四种情况:(1)若被删关键码所在的叶结点同时又是根结点,且该结点中关键码的个数n≥2,可直接删除.(2)若被删关键码所在的叶结点不是根结点,且该结点中关键码的个数n≥m/2,可直接删除.
(3)被删关键码所在叶结点删除前关键码有n=m/2-1,若这是与该结点相邻的右兄弟(或左兄弟)结点的关键码个数n≥m/2,则可按以下的步骤调整左、右兄弟和双亲结点。a. 将双亲结点中刚刚大于(或小于)被删关键码Ki下移到被删关键码所在的结点中。b. 将右兄弟(或左兄弟)结点中最小(或最大)关键码上移到双亲结点Ki位置。c. 将右兄弟(或左兄弟)结点中最左(或最右)子树指针平移到被删关键码所在结点最后(或最前)子树指针位置。d. 将右兄弟(或左兄弟)结点中,将被移走的关键码和指针位置用剩余的关键码和指针填补、调整,再将结点中的关键码个数减一。
(4)被删关键码所在的叶结点关键码的个数为n=m/2-1,若这时右兄弟(或左兄弟)结点的关键码个数n=m/2-1,则(4)被删关键码所在的叶结点关键码的个数为n=m/2-1,若这时右兄弟(或左兄弟)结点的关键码个数n=m/2-1,则 必须按以下步骤合并这两个结点。a. 将双亲结点p中相应关键码下移到选定保留的结点中,若要合并p中的子树指针Pi和Pi+1所指的结点,且保留Pi所指的结点,则把p中的关键码Ki+1下移到Pi所指的结点中。b. 把p中子树指针Pi+1所指结点中的全部指针和关键码都照搬到Pi所指结点的后面。删去P i+1所指的结点。c. 在双亲结点p中用后面剩余的关键码和指针填补关键码Ki+1和指针Pi+1。d. 修改双亲结点p和选定保留结点的关键码个数.
10.2.5 B+数B+树的定义如下: (1)树中每个结点最多有m棵树(2)根结点(非叶结点)至少有2棵树,除根结点外,其他的非叶结点至少有m/2棵子树,有n棵子树的非叶结点有n-1个关键码。(3)所有的叶结点都处于同一层次上,包含了全部关键码及指向相应数据对象存放地址的指针,且叶结点本身按关键码从小到大顺序链接。(4)每个叶结点中子树棵数可以多于m,可以少于m,视关键码字节及对象地址指针字节数而定。若设结点可容纳最大关键码数为m1,则指向对象地址指针也有m1个,因此,结点中的子树棵数应满足n∈[m1/2,m1].(5)所有的非叶结点可以看成是索引部分。结点格式同b树
在图10.15中所有非叶结点中子树的棵数n∈[2,4],其所有的关键码都出现在叶结点中。上层结点中的关键码都是其右子树上最小关键码的副本。B+树有两个指针,一个指向树的根结点,一个指向关键码最小的结点。因此,可以对B+树进行两种搜索,一种是从根结点开始,另一种是循叶结点自己拉起的链表顺序搜索。 在B+树上搜索在非叶结点上找到给定值,搜索并不停止,一直要找到叶结点上的给定值,搜索才结束。因此,每次搜索都从根结点走到叶结点。
在B+树中删除叶结点中的最小关键码,并不需要删除上层结点中的副本。因为其上层的副本只起引导搜索的“分界关键码”的作用。在B+树中删除叶结点中的最小关键码,并不需要删除上层结点中的副本。因为其上层的副本只起引导搜索的“分界关键码”的作用。
10.3 散列(Hashing)10.3.1 词典的抽象数据类型 在计算机科学中,词典也当作一种抽象数据类型。在讨论词典抽象数据类型时,把词典定义为<名字-属性>对的集合。通常用文件或表格来表示实际的对象集合,用文件中的记录或表格中的表项来表示单个对象。<名字-属性>被存于记录(或表项)中,通过表项的关键码来标识该表项。表项的存放位置及其关键码之间的对应关系可以用一个二元组表示:(关键码key,表项位置指针)这个二元组构成了搜索某一指定表项的索引项。可以使用多种方式搜索索引项。但是使用散列结构是一种搜索效率很高的词典的组织方法。10.3.2 散列表与散列方法 散列方法在表项的存储位置与它的关键码之间建立一个确定的对应函数关系Hash(),使每个关键码与结构中的唯一存储位置相对应:Address=Hash(Rec.key)
散列方法:在存放表项时,通过相同函数计算存储位置,并按此位置存放,这种方法称为散列方法。散列函数:在散列方法中使用的函数。散列表:按上述方法构造出来的表称为散列表。 通常关键码集合比散列表地址集合大的多,因此有可能经过散列函数的计算,把不同的关键码映射到同一个散列地址上。称这些散列地址相同的不同关键码为同义词。 对于散列方法需要讨论两个问题(1)对于给定的一个关键码集合,选择一个计算简单且地址分布比较均匀的散列函数,避免或尽量减少冲突。(2)拟订解决冲突的方案。10.3.3散列函数构造散列函数应注意以下几个问题:(1)散列函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址,其值域必须在1~m-1之间。(2)散列函数计算出来的地址应能均匀分布在整个地址空间中。(3)散列函数应是简单的。
1.直接定址法 此类方法取关键码的某个线形函数值作为散列地址:Hash(key)=a*key+b {其中a,b为常数}这类散列函数是一对一的映射,一般不会产生冲突。但是,它要求散列地址空间的大小与关键码集合的大小相同,这种要求一般很难实现。2.数字分析法 设有n个d位数,每一位可能有r种不同的符号。这r种不同的符号在各位上出现的频率不一定相同。可根据散列表的大小,选取其中各种符号分布均匀的若干位作为散列地址。计算各位数字中符号分布均匀度 其中, 表示第I个符号在第k位上出现的次数,n/r表示各种符号在n个数中均匀出现的期望值。计算 值越小,表明在该位各种符号分布的越均匀。
3.除留余数法 设散列表中允许的地址数为m,取一个不大于m,但最接近或等于m的质数p,或选取一个不含有小于20的质因子的合数作为除数。这样的散列函数为:hash(key)=key%p p≤m其中,“%”是整数除法取余的运算,要求这时的质数p不是接近2的幂。
4.平方取中法 先计算构成关键码的表示符的内码的平方,然后按照散列表的大小取中间的若干位作为散列地址。在平方取中法中,一般取散列地址为2的某次幂。
5.折叠法 有两种方法:(1)移位法----把各部分的最后一位对齐相加。(2)分界法----各部分不折断,沿各部分的分界来回折叠,然后对齐相加,将相加的结果当作散列地址。
10.3.4 处理冲突的闭散列方法 若设散列表有m个地址,将其改为m个桶。其桶号与散列地址一一对应。每个桶可存放s个表项。如果对不同的关键码用散列函数计算得到同一个散列地址,就产生了冲突,它们可以放到桶内的不同位置,只有当桶内所s个表项都放满后才会产生冲突。 处理冲突的一种常用的方法就是闭散列,也叫开地址法。所有的桶都直接放在散列表数组中。因此,每一个桶只有一个表项。如果在存放表项时发现,该位置已被别的表项占据,则在整个表中查找新的位置,如果表未被装满,则在允许的范围内必定有新的位置。查找的主要方法有3种。1.线形探测法 方法为:一旦发生冲突,在表中顺序向后寻找“下一个”空桶Hi的递推公式为:Hi=(Hi-1+1)%m i=1,2,….,m-1或Hi=(H0+I)%m I=1,2,….,m-1
实例:已知关键码Burke,Ekers,Broad,Blum,Attlee,Alton,Hecht, Ederly散列函数为:Hash(x)=ord(x)-ord(‘a’)可得:Hash(Burke)=1,Hash(Ekers)=4,Hash(Broad)=1, Hash(Blum)=1,Hash(Attlee)=0,Hash(Alton)=0, Hash(Hecht)=7, Hash(Ederly)=4 0 1 2 3 4 5 6 7 2.二次探查法 线形探查法容易产生“堆积”的问题,即不同探查序列的关键码占据可利用的空桶,使得为寻找某一关键码需要经历土同的探查序列的元素,导致搜索时间增加.使用二次探查法,在表中寻找“下一个”空桶的公式为: Hi=(H0+i2)%m,Hi=(H0-i2)%m,I=1,2,3,…,(m-1)/2式中H0=hash(x)是通过某一个散列函数hash()对表项的关键码x进行计算得到的桶号,它是一个非负整数.m是表的大小,它应是
一个值为4k+3的质数,其中k是一个整数.实例:若设表的长度为Tablesize=31,根据线形探查结果利用二次探查法所得到的结果为: 0 1 2 3 4 5 6 7 21 22 23 24 25 26 27 30 设散列表的桶数为m,待查表项的关键码为x,第一次通过散列函数计算出来的桶号为H0=hash(x).当发生冲突时,第i-1次和第i次次计算出来的“下一个”桶号为:
将上述两式相加可以得到: 从上述式子可以知道,只要知道上一次桶号Hi-1就可以知道下一个桶号Hi. 3.双散列法 使用散列方法时,需要使用两个散列函数.第一个散列函数Hash()按表项的关键码key 计算表项所在的桶号.一旦发生冲突,利用第二个散列函数ReHash()计算该表项“下一个”桶的移位量. 10.3.5 处理冲突的开散列方法----链地址法 链地址法处理冲突的方法是,将通过散列函数计算出来地址相同的关键码通过链表链接起来,各链表表头结点组成一个向量.向量的元素个数与桶数一致表头.