查找表
一、认识查找表
- 查找表:给定一个由同一类型的数据元素(或记录)构成的集合,从中查找指定数据项(或数据元素某个特征)的数据元素或记录
- 查找表的主要操作
- 查询某个“特定的”数据元素是否在查找表中
- 检索某个“特定的”数据元素的各种属性
- 在查找表中插入一个数据元素
- 从查找表中删去某个数据元素
- 查找表的分类
- 静态查找表:仅作查询和检索操作的查找表
- 动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已存在的某个数据元素
- 关键字/Key
- 是数据元素中某个数据项的值,用以表示一个数据元素
- 若此关键字可以识别惟一的一个记录,则称之为“主关键字”
- 若此关键字可以识别若干记录,则称之为“次关键字”
- 查找/Searching
- 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素
- 若查找表中存在这样一个记录,则称“查找成功”,查找结果:给出整个记录信息,或指示该记录在查找表中的位置;否则称“查找不成功”,查找结果:给出“空记录”或“空指针”
- 查找方法
- 查找的方法取决于查找表的结构,所谓查找表的结构来自于对集合中的元素间人为添加的“关系”。
- 查找性能的评价
- 查找速度、占用存储空间多少、算法本身复杂程度
- 平均查找长度ASL(Average Search Length)
- 为确定记录在表中的位置,需和给定值进行比较的关键字的个数的期望值叫查找表算法的ASL
- 对于包括 n n n个记录的表,查找成功时 A S L = ∑ i = 1 n p i c i ASL=\sum_{i=1}^np_ic_i ASL=∑i=1npici
- 其中 p i p_i pi为查找表中第 i i i个记录的概率,且 ∑ i = 1 n p i = 1 \sum_{i=1}^np_i=1 ∑i=1npi=1, c i c_i ci为找到查找表中第 i i i个记录需要比较关键字的次数
二、静态查找表
- ADT
ADT StaticSearchTable{ 数据对象D:D时具有相同天热行的数据元素的集合 数据关系R:数据元素同属一个集合 基本操作: Create(&ST,n); //构造一个含n个数据元素的静态查找表ST Destroy(&ST); //销毁表ST Search(ST,key); //查找ST中其关键字等于key的元素 Traverse(ST,Visit());//按某种次序对ST的每个元素调用Visit函数一次且仅一次 }ADT StaticSearchTable
- 静态查找表的类型
- 顺序查找表:集合元素间添加序偶关系,构成顺序表
- 有序查找表:集合元素间依据大小添加序偶关系,构成有序表
- 索引顺寻标:集合元素分块,块间有序,块内无序
- 顺序查找表
int Search_Seq(SSTable ST, KeyType key) { ST.elem[0].key=key; //设置哨兵 for(i=ST.length;ST.elem[i].key!=key;--i);//从后往前找 return i; //找不到时,i为0 }//Search_Seq
- 性能分析
- 平均查找长度
- 等概率查找: p i = 1 n , c i = n − i + 1 , A S L = ∑ i = 1 n p i c i = n + 1 2 p_i=\frac{1}{n},c_i=n-i+1,ASL=\sum_{i=1}^np_ic_i=\frac{n+1}{2} pi=n1,ci=n−i+1,ASL=∑i=1npici=2n+1
- 不等概率查找的情况下, A S L ASL ASL在查找概率满足 p n ≥ p n − 1 ≥ ⋯ ≥ p 1 p_n\geq p_{n-1}\geq \dots\geq p_1 pn≥pn−1≥⋯≥p1时取最小值
- 若查找概率无法实现测定,则查找过程采取的改进办法是将访问频度大的记录后移。或每次查找之后,将刚刚查找到的记录直接移至表尾的位置上
- 查找不成功时
- 查找算法的 A S L ASL ASL应是查找成功时的 A S L ASL ASL和查找不成功时的 A S L ASL ASL的期望
- 对顺序查找,查找不成功时和给定值进行比较的次数都是 n + 1 n+1 n+1,若设查找成功和不成功可能性相同,对每个记录的查找概率也相等,则 p i = 1 2 n p_i=\frac{1}{2n} pi=2n1,此时 A S L = 1 2 n + 1 2 + 1 2 ( n + 1 ) = 3 4 ( n + 1 ) ASL=\frac{1}{2}\frac{n+1}{2}+\frac{1}{2}(n+1)=\frac{3}{4}(n+1) ASL=212n+1+21(n+1)=43(n+1)
- 平均查找长度
- 性能分析
- 有序查找表
- 将逻辑结构相邻的元素按关键字排好序,则得到有序查找表,可采用“折半”查找
- 折半查找/Binary Search
- 查找过程:每次将待查记录所在区间缩小一半
- 适用条件:采用顺序存储结构的有序表
int Search_Bin(SSTable ST, KeyType key) { low=1; high=ST.length; while(low <= high) { mid=(low+high)/2; if(key==ST.elem[mid].key) return mid; //找到待查元素 else if(key < ST.elem[mid].key) high=mid-1; //继续在前半区间进行查找 else low=mid+1; //继续在后半区间进行查找 } return 0; //顺序表中不存在待查元素 }
- 性能分析
- 关键字比较次数不超过 └ l o g 2 n ┘ + 1 \llcorner log_2n\lrcorner+1 └log2n┘+1,时间复杂度为 O ( l o g n ) O(logn) O(logn)
- 特别的,当 n > 50 n>50 n>50,查找成功时的 A S L ≈ l o g 2 ( n + 1 ) − 1 ASL\approx log_2(n+1)-1 ASL≈log2(n+1)−1
- 折半查找适用条件:有序的顺序表
- 斐波那契查找
- 适用条件:有序静态查找表,顺序表的实现方式
- 将折半查找的“砍一半”搜索空间,改成不平衡地近似砍一半,用斐波那契数做分割点 F n + 1 = F n + F n − 1 F_{n+1}=F_n+F_{n-1} Fn+1=Fn+Fn−1
- 假设有序查找表长 m = F n + 1 − 1 m=F_{n+1}-1 m=Fn+1−1,则比较查找关键字 k e y key key和 S T . e l e m [ F n ] . k e y ST.elem[F_n].key ST.elem[Fn].key,前一般有 F n − 1 F_n-1 Fn−1个记录,后一半有 F n − 1 − 1 F_{n-1}-1 Fn−1−1个记录,采用类似折半查找的方法,递归调用
- 若有序查找表的长度 m ≠ F n − 1 m\neq F_n-1 m=Fn−1,则取表的前 F n − 1 F_n-1 Fn−1项,使得 n n n尽可能大;查找表后面的项采用递归调用斐波那契查找
- 性能分析:平均优于折半,但最坏比折半差,分割时只需加减运算
- 插值查找
- 直接依据给定的 k e y key key值,来估计记录应该在的位置,估计位置的公式 i = k e y − S T . e l e m [ l ] . k e y S T . e l e m [ h ] . k e y − S T . e l e m [ l ] . k e y ( h − l + 1 ) i=\frac{key-ST.elem[l].key}{ST.elem[h].key-ST.elem[l].key}(h-l+1) i=ST.elem[h].key−ST.elem[l].keykey−ST.elem[l].key(h−l+1)
- 适用条件:关键字在查找表中“均匀分布”
- 性能:表大时,平均性能由于折半查找
顺序表 | 有序表 | |
---|---|---|
表的特性 | 无序 | 有序 |
存储结构 | 顺序或链式 | 顺序 |
插删操作 | 易于进行 | 需移动元素 |
ASL的值 | 大 | 小 |
- 索引顺序表
- 在建立顺序表的同时,建立一个索引项,包括两项:关键字项(块内最大关键字)和指针项(块在顺序表的起始位置)
- 索引表按关键字有序,表则为分块有序
- 又称分块查找
- 查找过程:将表分成几块,块内无需,块间有序,先确定待查记录所在快,再在块内查找
- 适用条件:分块有序表
- 算法实现
- 用数组存放待查记录,每个数据元素至少含有关键字域
- 建立索引表,每个索引表节点含有最大关键字域和指向本块第一个节点的指针
- 利用索引表,确定块,然后再块内顺序查找
typedef struct IndexType { keyType maxkey;//块中最大的关键字 int startpos;//块的起始位置指针 }Index; int Block_search(RecType ST[],Index ind[],KeyType key,int n,int b) { //在分块索引表中查找关键字为key的记录,表长为n,块数为b //LT小于, LQ小于等于,EQ等于 int i=0,j; while((i<b)&<(ind[i].maxkey,key)) i++; if (i>b) { printf("\nNot found"); return(0); } j=ind[i].startpos; while((j<n)&&LQ(ST[j].key,ind[i].maxkey)) { if ( EQ(ST[j].key, key) ) break; j++; } //在块内查找 if (j>=n||!EQ(ST[j].key,key)) { j=0; printf("\nNot found"); } return(j); }
- 性能分析
- A L S = L b + L w ALS=L_b+L_w ALS=Lb+Lw, L b L_b Lb,查找索引表,确定所在块的平均查找长度, L w L_w Lw,块中查找元素的平均查找长度
- 若长为
n
n
n的表被平均分为
b
b
b块,则每块包含
s
=
n
b
s=\frac{n}{b}
s=bn个记录,假设每个记录的查找概率相等
- 若用顺序查找确定块位置 A S L = 1 b ∑ i = 1 b i + 1 s ∑ j = 1 s j = b + 1 2 + s + 1 2 = + f r a c 12 ( n s + s ) + 1 ASL=\frac{1}{b}\sum_{i=1}^bi+\frac{1}{s}\sum_{j=1}^sj=\frac{b+1}{2}+\frac{s+1}{2}=+frac{1}{2}(\frac{n}{s}+s)+1 ASL=b1∑i=1bi+s1∑j=1sj=2b+1+2s+1=+frac12(sn+s)+1
- 若用折半查找确定块位置 A S L ≈ l o g 2 ( n s + 1 ) + s 2 ASL\approx log_2(\frac{n}{s}+1)+\frac{s}{2} ASL≈log2(sn+1)+2s
顺序查找 | 有序查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 两者之间 |
表结构 | 有序表、无序表 | 有序表 | 分块有序表 |
存储结构 | 顺序存储结构/线性链表 | 顺序存储结构 | 顺序存储结构/线性链表 |
- 几种查找表的对比
查找 | 插入 | 删除 | |
---|---|---|---|
无序顺序表 | O(n) | O(1) | O(n) |
无序线性链表 | O(n) | O(1) | O(1) |
有序顺序表 | O(logn) | O(n) | O(n) |
有序线性链表 | O(n) | O(n) | O(n) |
静态查找树表 | O(nlogn) | O(nlogn) | O(nlogn) |
- 结论
- 从查找性能看,最好情况能达 O ( l o g n ) O(logn) O(logn),要求表有序
- 从插入和删除的性能看,最好情况能达 O ( 1 ) O(1) O(1),要求存储结构时链表
三、动态查找表
- ADT
ADT DynamicSearchTable{ 数据对象D:D是具有相同特性的数据元素的集合 数据关系R:数据元素同属一个集合 基本操作: InitDSTable(&DT); DestroyDSTable(&DT); SearchDSTable(DT,key); InsertDSTable(&DT,e); DeleteDSTable(&DT,key); TraverseDSTable(DT, Visit()); }ADT DynamicSearchTable
- 动态查找表的分类
- 二叉排序树:待查记录集合的元素构建二叉树式的逻辑结构
- B − B^- B−树和 B + B^+ B+树:待查记录集合的元素构成多叉树的逻辑结构
- 键树
- 二叉排序树
- 二叉排序树或者是一棵二叉树,或者是具有如下特性的树
- 若左子树不空,左子树所有节点值均小于根节点值
- 若右子树不空,右子树所有节点值均大于根节点值
- 它的左右子树也都是二叉排序树
- 查找算法思想
- 若二叉排序树为空,则查找不成功
- 若给定值等于根节点的关键字,查找成功
- 若给定值小于根节点的关键字,继续在左子树上查找
- 若给定值大于根节点的关键字,继续在右子树上查找
Status SearchBST(BiTree T, KeyType key,BitNode* &f,BitNode* &p) { if (!T) { p=f; //p指向查找路径上访问的最后一个结点并返回FALSE return FALSE; } if EQ(key, T->data.key) { p=T; //若查找成功,p指向该结点,并返回TRUE return TRUE; } if LT(key,T->data.key) { f=T; return(SearchBST(T->lchild,key,f,p)); // 在左子树中继续查找 } else { f=T; return(SearchBST(T->rchild, key,f,p)); // 在右子树中继续查找 } }//SearchBST
- 二叉排序树的中序遍历为一个关键字的有序序列
- 二叉排序树的插入算法
- 若二叉树为空树,则新插入的结点为新的根节点,否则,新插入的结点必为一个新的叶子结点,其插入位置由查找过程得到
Status InsertBST(BiTree &T, ElemType e) { if (!SearchBST(T,e.key,NULL,p)) { //查找不成功 BiTree* s = (BiTree)malloc(sizeof(BiTNode)); s->data=e; s->lchild=s->rchild=NULL; if (!p) T=s; //插入s为新的根结点 else if LT(e.key,p->data.key) p->lchild=s;//插入*s为*p的左孩子 else p->rchild=s; //插入*s为*p的右孩子 return TRUE; //插入成功 } else return FALSE; } //Insert BST
- 二叉排序树的删除算法
- 被删除的结点只有左子树或只有右子树,其双亲结点的指针域的值改为“指向被删除节点的左子树或右子树”
- 被删除的结点既有左子树,也有右子树,用中序遍历二叉树输出结构序列中被删除节点的前驱替代之,然后在删除该前驱节点
- 被删除的结点是叶子,删去即可
Status DeleteBST(BiTree &T, KeyType key) { BitNode *f=NULL, *p; if (!SearchBST(T,key,f,p)) //查找不成功 return FALSE; //不存在关键字等于key的数据元素 Delete(p,f); }//DeleteBST void Delete(BiTree &p, BiTree f) {//删除过程 //从二叉排序树中删除结点p,并重接它的左子树或右子树 if (!p->rchild && !p->lchild) {......;return ..}//删除叶子节点 if (!p->rchild) {......} //左孩子为空 else if (!p->lchild) {......} //右孩子为空 else {......}//左右孩子都非空 }//Delete
- 二叉排序树操作的性能
- 插入和删除的开销都集中在查找操作
- n个关键字,可构造不同形态的二叉排序树,其平均查找长度可能会不同,甚至可能差别很大
- 共有 n ! n! n!种不同的输入序列,每个输入序列对应一棵二叉排序树,假设每棵二叉排序树种每个关键字被等概查找,那么得到平均查找长度为 2 n + 1 n l o g 2 n + C = O ( l o g n ) 2\frac{n+1}{n}log_2n+C=O(logn) 2nn+1log2n+C=O(logn)
- 二叉排序树或者是一棵二叉树,或者是具有如下特性的树
- 平衡二叉排序树
- 引入的原因
- 二叉排序树平均查找长度受树的形态影响较大,形态比较均匀时查找效率较好,因此希望有形态总是均衡的二叉排序树,查找时有最好的效率,这就是平衡二叉排序树
- AVLTree或者是空树,或者是满足下列性质的二叉树
- 左子树和右子树深度之差的绝对值不大于1
- 左子树和右子树也都是平衡二叉树
- 平衡因子
- 称二叉树上结点左子树的深度减去右子树深度为结点的平衡因子
- 平衡二叉树上的每个结点的平衡因子只可能是-1,0,1,否则,只要一个结点的平衡因子的绝对值大于1,该二叉树就不是平衡二叉树
- 性质
- 在平衡二叉排序树上执行查找的过程与二叉排序上的查找过程一样,和给定 k e y key key值比较的次数不超过树的深度
- 设深度为 h h h的平衡二叉排序树所具有的最少结点数 N h N_h Nh,则 N h N_h Nh满足递推关系: N 0 = 0. N 1 = 1 , N 2 = 2 , ⋯ , N h = N h − 1 + N h − 2 N_0=0.N_1=1,N_2=2,\cdots,N_h=N_{h-1}+N_{h-2} N0=0.N1=1,N2=2,⋯,Nh=Nh−1+Nh−2,类似斐波那契数,可解的 h = ≈ l o g Φ ( 5 ( n + 1 ) ) − 2 h=\approx log_{\Phi}(\sqrt{5}(n+1))-2 h=≈logΦ(5(n+1))−2,其中 Φ = 5 + 1 2 \Phi=\frac{\sqrt{5}+1}{2} Φ=25+1
- 平均查找长度和 l o g 2 n log_2n log2n是一个量级的,也是 O ( l o g n ) O(logn) O(logn)
- 平衡化旋转(LL,LR,RL,RR)
- 失衡结点:沿着插入或删除结点上行到根节点就能找到受到影响的结点,这些结点的平衡因子和子树深度都可能会发生变化,平衡因子绝对值大于1的结点称为失衡结点
-
L
L
LL
LL型平衡化旋转
- 失衡节点a初始时刻平衡因子为1,在结点a的左孩子b的左子树上进行插入x,使得a的平衡因子变成2
- 插入前: a = 1 , b = 0 a=1,b=0 a=1,b=0,插入后: b = 1 , a = 2 b=1,a=2 b=1,a=2,旋转后 a = 0 , b = 0 a=0,b=0 a=0,b=0
- 顺时针旋转以a为根的子树
typedef struct BNode { KeyType key; //关键字域 int Bfactor; //平衡因子域 ... //其它数据域 struct BNode *Lchild, *Rchild; }BSTNode; void LL_rorate(BSTNode *a) { BSTNode* b; b=a->Lchild; a->Lchild=b->Rchild; b->Rchild=a; a->Bfactor=b->factor=0; a=b; }
-
L
R
LR
LR型平衡化旋转
- 失衡节点a初始时刻平衡因子为1,在结点a的左孩子b的右子树上插入x,使得a的平衡因子变成2
- 插入前: a = 1 , b , c = 0 a=1,b,c=0 a=1,b,c=0,插入后: c = 0 , 1 , − 1 , b = − 1 , a = 2 c=0,1,-1,b=-1,a=2 c=0,1,−1,b=−1,a=2,旋转后 ( − 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 0 ) (-1,0,0),(0,1,0),(0,0,0) (−1,0,0),(0,1,0),(0,0,0)
- 先逆时针旋转a的左子树,再顺时针旋转a为根的子树
void LR_rotate(BSTNode *a) { BSTNode *b,*c; b=a->Lchild; c=b->Rchild; a->Lchild=c->Rchild; b->Rchild=c->Lchild; c->Lchild=b; c->Rchild=a; if(c->Bfactor==1) { a->Bfactor=-1; b->Bfactor=0; c->Bfactor=0; } else if(c->Bfactor==0) a->Bfactor=b->Bfactor=0; else { a->Bfactor=0; b->Bfactor=1; c->Bfactor=0; } }
-
R
L
RL
RL型平衡化旋转
- 失衡结点a初始时平衡因子为-1,在结点a的右孩子b的左子树上插入x,使得a的平衡因子变成-2
- 插入前: a = − 1 , b , c = 0 a=-1,b,c=0 a=−1,b,c=0,插入后: c = 0 , − 1 , 1 , b = 1 , a = − 2 c=0,-1,1,b=1,a=-2 c=0,−1,1,b=1,a=−2,旋转后: ( 1 , 0 , 0 ) , ( 0 , − 1 , 0 ) , ( 0 , 0 , 0 ) (1,0,0),(0,-1,0),(0,0,0) (1,0,0),(0,−1,0),(0,0,0)
- 先顺时针旋转a的右子树,再逆时针旋转a为根的子树
void RL_rotate(BSTNode *a) { BSTNode *b,*c; b=a->Rchild; c=b->Lchild; a->Rchild=c->Lchild; b->Lchild=c->Rchild; c->Lchild=a; c->Rchild=b; if(c->Bfactor==1) { a->Bfactor=0; b->Bfactor=-1; c->Bfactor=0; } else if(c->Bfactor==0) a->Bfactor=b->Bfactor=0; else { a->Bfactor=1; a->Bfactor=0; a->Bfactor=0; } }
-
R
R
RR
RR型平衡化旋转
- 失衡节点a初始时刻平衡因子为-1,在结点a的右孩子b的右子树上进行插入x,使得a的平衡因子变为-2.
- 插入前: a = − 1 , b = 0 a=-1,b=0 a=−1,b=0,插入后 b = − 1 , a = − 2 b=-1,a=-2 b=−1,a=−2,旋转后: a = 0 , b = 0 a=0,b=0 a=0,b=0
- 逆时针旋转以a为根的子树
BSTNode* RR_rotate(BSTNode *a) { BSTNode *b; b=a->Rchild; a->Rchild=b->Lchild; b->Lchild=a; a->Bfactor=b->Bfactor=0; a=b; }
- 引入的原因
四、改进索引技术
- 索引顺序表:将数据表分块,块间有序,块内无序,块有索引
- 如果每个记录都建立索引,即块的大小为1,这就给数据建立了稠密的索引表
- 索引表有序,可以用折半查找或平衡二叉树来构造索引表
- 适用场景:
- 记录大小不固定,或者每个记录都很大
- 记录存储在外存,内存放不下,比如数据库文件
- 增删记录的操作非常频繁时,不移动数据,仅更新索引表
- 树形索引表
- 用平衡二叉树来组织索引表可行,但大型数据库不可行
- 因此,必须选择一种尽可能降低磁盘I/O次数的索引组织方式,树结点的大小尽可能地接近页的大小,由此提出了多路平衡查找树,称为 B − B^- B−树
- 磁盘结构
- 磁盘是很多块构成的集合
- 每个块有自己的“地址”,用于查找给定地址的块
- 每个磁盘块的大小都是固定的,格式化的时候设定
- 磁盘块是磁盘读取的基本单位
-
B
−
B^-
B−树
- B − B^- B−树主要用于文件系统种,在 B − B^- B−树中,每个结点的大小为一个磁盘页,结点中所包含的关键字及其孩子的数目取决于页的大小
- 一棵
m
m
m阶
B
−
B^-
B−树,或者是空树,或者是满足以下性质的
m
m
m叉树
- 根结点或者是叶子,或者至少有两棵子树,至多有 m m m棵子树
- 除根结点外,所有非终端结点至少有 ┌ m 2 ┐ \ulcorner\frac{m}{2}\urcorner ┌2m┐棵子树,至多 m m m棵子树
- 所有叶子结点都在树的同一层上
- 每个结点应包含如下信息: ( n , A 0 , K 1 , A 1 , K 2 , A 2 , … , K n , A n ) (n,A_0,K_1,A_1,K_2,A_2,\dots,K_n,A_n) (n,A0,K1,A1,K2,A2,…,Kn,An),其中 K i K_i Ki是关键字,且 K i < K i + 1 K_i<K_{i+1} Ki<Ki+1, A i A_i Ai是指向孩子结点的指针,且 A i − 1 A_{i-1} Ai−1所指向的子树中所有的结点的关键字都小于 K i K_i Ki, A i A_i Ai所指向的子树中所有结点的关键字都大于 K i K_i Ki, n n n是结点中关键字的个数,且 ┌ m 2 ┐ − 1 ≤ n ≤ m − 1 \ulcorner\frac{m}{2}\urcorner-1\leq n\leq m-1 ┌2m┐−1≤n≤m−1
- 存储结构
#define M 5 //根据实际需要定义B-树的阶数 typedef struct BTNode{ int keynum; //结点中关键字数目 struct BTNode *parent; //指向父节点的指针 KeyType key[M]; //关键字向量,key[0]未用 struct BTNode *ptr[M]; //子树指针向量 RecType *recptr[M]; //记录指针向量,recptr[0]未用 }BTNode;
-
B
−
B^-
B−树的查找
- 从树的根节点
T
T
T开始,在
T
T
T所指向的结点的关键字向量
k
e
y
[
1...
k
e
y
n
u
m
]
key[1...keynum]
key[1...keynum]中查找给定值
K
K
K(折半查找),若没找到,则将
K
K
K与各个分量的值进行比较
- 若 K < k e y [ 1 ] : T = T − > p t r [ 0 ] K<key[1]:T=T->ptr[0] K<key[1]:T=T−>ptr[0]
- 若 k e y [ i ] < K < k e y [ i + 1 ] , T = T − > p t r [ i ] key[i]<K<key[i+1],T=T->ptr[i] key[i]<K<key[i+1],T=T−>ptr[i]
- 若 K > k e y [ k e y n u m ] : T − > p t r [ k e y n u m ] K>key[keynum]:T->ptr[keynum] K>key[keynum]:T−>ptr[keynum]
- 直到 T T T是叶子结点且未找到相等关键字,查找失败
int BT_search(BTNode* T, KeyType K, BTNode *p) { //在B-树种查找关键字K,查找成功返回在结点中的位置 //及结点指针p;否则返回0及最后一个指针 BTNode *q; int n; p = q = T; while(q!=NULL) { p=q; q->key[0]=K; //设置查找哨兵 for(n=q->keynum;K<=q->key[n];n--) { if(n>0&&EQ(q->key[n],K)); return n; } q=q->ptr[n]; } }
- 性能分析
- 分为找节点和在节点中找关键字,在节点中找关键字是堆内存操作,找节点是对外存数据处理,找节点是影响性能的主要因素
- 一个节点通常是一个磁盘块,访问节点数越少越好
- 关键在于关键字所在节点的深度
- 设被查询关键字在 B − B^- B−树的深度为 h h h
- 第h层上至少有 ┌ m 2 ┐ h − 2 \ulcorner\frac{m}{2}\urcorner^{h-2} ┌2m┐h−2个节点
- 根节点至少包括一个关键字,其它节点至少包括 ┌ m 2 ┐ \ulcorner\frac{m}{2}\urcorner ┌2m┐个关键字,可推的 n ≥ 1 + ( ┌ m 2 ┐ − 1 ) ∑ i = 2 h ( ┌ m 2 ┐ − 1 ) i = 2 ( ┌ m 2 ┐ − 1 ) h − 1 − 1 n\geq1+(\ulcorner\frac{m}{2}\urcorner-1)\sum_{i=2}^h(\ulcorner\frac{m}{2}\urcorner-1)^i=2(\ulcorner\frac{m}{2}\urcorner-1)^{h-1}-1 n≥1+(┌2m┐−1)∑i=2h(┌2m┐−1)i=2(┌2m┐−1)h−1−1
- 推得 h ≤ l o g ┌ m 2 ┐ − 1 ( n + 1 2 ) + 1 h\leq log_{\ulcorner\frac{m}{2}\urcorner-1}(\frac{n+1}{2})+1 h≤log┌2m┐−1(2n+1)+1
- 从树的根节点
T
T
T开始,在
T
T
T所指向的结点的关键字向量
k
e
y
[
1...
k
e
y
n
u
m
]
key[1...keynum]
key[1...keynum]中查找给定值
K
K
K(折半查找),若没找到,则将
K
K
K与各个分量的值进行比较
-
B
−
B^-
B−树的插入
- B − B^- B−树的种查找关键字 K K K,若找到,表名关键字已存在,返回;否则, K K K的查找操作失败于某个叶子节点,转下一步
- 将
K
K
K插入到该叶子节点中,插入时,若
- 叶子结点的关键字数 < m − 1 <m-1 <m−1:直接插入
- 叶子节点的关键字树 = m − 1 =m-1 =m−1:将节点“分裂”
- 分裂的方法
- 从中间分成两个节点,将最中间关键字(上界)插入父节点,并以分裂后的两个节点作为其左右子节点,若父节点被插入新节点后,不满足 m m m阶 B − B^- B−树定义,继续分裂
-
B
−
B^-
B−树的删除
- 在 B − B^- B−树上删除一个关键字 K K K,首先找到关键字所在的节点 N N N,然后在 N N N中进行关键字 K K K的删除操作
- 若 N N N不是叶子节点,设 K K K是 N N N中的第 i i i个关键字,则将指针 A i − 1 A_{i-1} Ai−1所指子树中的最大关键字 K ′ K' K′放在 ( K ) (K) (K)的位置,然后删除 K ′ K' K′,而 K ′ K' K′一定在叶子节点上
- 从叶子节点删除一个关键字
- 若节点 N N N中关键字个数 > ┌ m 2 ┐ − 1 >\ulcorner\frac{m}{2}\urcorner-1 >┌2m┐−1,则直接删除关键字
- 若节点 N N N中关键字个数 = > ┌ m 2 ┐ − 1 =>\ulcorner\frac{m}{2}\urcorner-1 =>┌2m┐−1,若节点 N N N的左(右)兄弟节点中关键字个数 > ┌ m 2 ┐ − 1 >\ulcorner\frac{m}{2}\urcorner-1 >┌2m┐−1,则将节点 N N N的左(右)兄弟节点中最大(最小)关键字上移到其父节点中,而父节点中大于(小于)且紧靠上移关键字的关键字下移到节点 N N N
- 若结点 N N N和其兄弟结点中的关键字数 = ⌈ m / 2 ⌉ − 1 =⌈m/2⌉−1 =⌈m/2⌉−1,删除结点 N N N中的关键字,再将结点 N N N中的关键字、指针与其兄弟结点以及分割二者的父结点中的某个关键字 K i K_i Ki,合并为一个结点,若因此使父结点中的关键字个数 < ⌈ m / 2 ⌉ − 1 <⌈m/2⌉−1 <⌈m/2⌉−1,则依此类推
-
B
+
B^+
B+树
- 与 B − B^- B−树不同,它只用叶子结点存储记录,所有非叶子结点可看成索引
- 一棵
m
m
m阶
B
+
B^+
B+树
- 若一个结点有 m m m棵子树,则必含有 m m m个关键字
- 所有叶子结点中包含了全部记录的关键字信息以及这些关键字记录的指针,而且叶子结点按关键字的大小从小到大顺序链表
- 所有非叶子结点可以看成是索引的部分,结点中只含有其子树的根节点中的最大(或最小)关键字
-
B
+
B^+
B+树的操作类似
B
−
B^-
B−树
- 查询区别:在非叶节点上找到关键字,不停止,沿着路径找到叶节点
- 插入区别:仅在叶节点进行,当叶节点包含超过 m m m个关键字,则分裂,并在双亲结点中包含分裂后两个节点的最大关键字及指针
- 删除区别:仅在叶节点进行,当叶节点的最大关键字被删除,其双亲的最大关键字可当成是“分界关键字”依然存在;叶节点过小时,需要类似 B − B^- B−树进行结点合并
五、哈希表与哈希查找
- 哈希函数/Hash:哈希函数是一种从关键字空间到存储地址空间的一种映射
- 哈希表:根据设定的哈希函数 H ( k e y ) H(key) H(key),将一组关键字映射到一个有限的、地址连续的地址集(区间)上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查中表称之为“哈希表”
- 哈希查找:也叫散列查找,依据给定的关键字 k e y key key和哈希函数,直接算出记录存储的地址,不经过比较,一次存取就能得到想要查找的记录
- 存在的问题:
- 两个不同关键字计算出相同的哈希地址
- 这称之为“冲突”,需要设计一个函数,将所有不同 k e y key key映射到不同的地址
- 若设计不出,就需要设计冲突解决办法
- 哈希表存在很多空白的空间没有存放记录
- 空间太大,可能很多没存储记录,浪费了;空间太小,可能产生大量冲突
- 两个不同关键字计算出相同的哈希地址
- 常见哈希函数
- 直接定址法
- H ( k e y ) = k e y H(key)=key H(key)=key或者 H ( k e y ) = a × k e y + b H(key)=a\times key+b H(key)=a×key+b
- 此方法仅适合于:地址集合的大小等于关键字集合的大小
- 平方取中法
- 以关键字的平方值得中间几位作为存储地址
- 此方法适合于:关键字中的每一位都有某些数字重复出现频度很高的现象,或不知道关键字频率分布情况时
- 随机数法
- 设定哈希函数为: H ( k e y ) = R a n d o m ( k e y ) H(key)=Random(key) H(key)=Random(key),其中 R a n d o m Random Random为以 k e y key key为随机数种子的伪随机数
- 此方法适合于:对长度不等的关键字构造哈希函数
- 数字分析法
- 分析所有关键字,从中提取分布均匀的若干位或它们的组合作为地址
- 此法适用于能预先估计出全体关键字的每一位上各种数字出现的频度
- 折叠法
- 将关键字分割称若干部分,然后取它们的叠加和伪哈希地址
- 两种叠加处理方法
- 移位叠加:将分割后的及部分低位对齐相加
- 间界叠加:从一段沿分割界来回折送,然后对齐相加,适于数字维数多
- 除留余数法
- 设定哈希函数 H ( k e y ) = k e y M O D p ( p ≤ m ) H(key)=keyMODp(p\leq m) H(key)=keyMODp(p≤m)
- 其中, m m m为表长, p p p为不大于 m m m的素数或是不含20以下的质因子
- 直接定址法
- 选取哈希函数
- 计算哈希函数所需时间
- 关键字长度
- 哈希表长度
- 关键字分布情况
- 记录的查找频率
- 冲突处理方法
- 开放定址法
- 为冲突的地址准备好一系列备选地址
- 备选地址的计算公式 H i = ( H ( k e y ) + d i ) M O D m H_i=(H(key)+d_i)MOD m Hi=(H(key)+di)MODm
- 其中
d
i
d_i
di为增量序列,常见有三种取法
- 线性探测再散列: d i = c × i , c = 1 d_i=c\times i,c=1 di=c×i,c=1
- 平方探测再散列: d i = 1 2 , − 1 2 , 2 2 , − 2 2 . . . d_i=1^2,-1^2,2^2,-2^2... di=12,−12,22,−22...
- 伪随机探测再散列: d i d_i di是一组伪随机数列
- 平方探测时,表长必为形如 4 j + 3 4j+3 4j+3的素数
- 伪随机探测时取决于伪随机数列( m , d i m,d_i m,di没有公因子)
- 再哈希法
- 当发生冲突时,计算下一个哈希地址,新的哈希函数计算
- 链地址法
- 将所有哈希地址相同的记录都链接在同一链表中
- 建立一个公共溢出区
- 在基本散列表之外,另设一个溢出表保存与基本表中记录冲突的所有记录
- 设散列表长为 m m m,设立基本散列表 h a s h t a b l e [ m ] hashtable[m] hashtable[m],每个分量保存一个记录;溢出表 o v e r t a b l e [ m ] overtable[m] overtable[m],一旦某个记录的散列地址有冲突,都填入溢出表中
- 开放定址法
- 哈希表的查找
- 对于给定值
K
K
K,计算哈希地址
i
=
H
(
K
)
i=H(K)
i=H(K)
- 若 r [ i ] = N U L L r[i]=NULL r[i]=NULL,则查找不成功
- 若 r [ i ] . k e y = K r[i].key=K r[i].key=K,则查找成功
- 否则,求下一地址
H
i
H_i
Hi,直至
- r [ H i ] = N U L L r[H_i]=NULL r[Hi]=NULL,查找不成功
- 或 r [ H i ] . k e y = K r[H_i].key=K r[Hi].key=K,查找成功
#define M 15 typedef struct node { KeyType key; struct node* link; }HNode; HNode *hash_search(HNode *t[], KeyType k) { HNode *p; int i; i=h(k); if(t[i]==NULL) { return NULL; } p=t[i]; while(p!=NULL) { if(EQ(p->key,k)) { return p; } else { p=p->link; } } return NULL; }//查找散列表HT中的关键字K,用链地址法解决冲突
- 哈希表查找的分析
- 决定哈希查找ASL的因素
- 选用的哈希函数
- 选用的处理冲突的方法
- 哈希表饱和的程度:装载因子 α = n m \alpha=\frac{n}{m} α=mn值得大小,其中 m , n m,n m,n分别表示表长和记录个数, α \alpha α越小,冲突可能性越小
- 一般情况下,认为选用的哈希函数时“均匀的”,讨论ASL可不考虑
- 当哈希表得冲突处理方法相同时,ASL时装载因子
α
\alpha
α的函数
- 线性探测再散列的哈希查找: A S L ≈ 1 2 ( 1 + 1 1 − α ) ASL\approx \frac{1}{2}(1+\frac{1}{1-\alpha}) ASL≈21(1+1−α1)
- 平方探测再散列、随机探测再散列和再哈希的哈希查找 A S L ≈ − 1 α l n ( 1 − α ) ASL\approx-\frac{1}{\alpha}ln(1-\alpha) ASL≈−α1ln(1−α)
- 链地址法的哈希查找: A S L ≈ 1 + α 2 ASL\approx1+\frac{\alpha}{2} ASL≈1+2α
- 决定哈希查找ASL的因素
- 哈希表的平均查找长度是 α \alpha α的函数,不是 n n n的函数,在用哈希表构造查找表时,可选择一个适当的装填因子 α \alpha α,使得平均查找长度限定在某个范围内
- 对于给定值
K
K
K,计算哈希地址
i
=
H
(
K
)
i=H(K)
i=H(K)