title: 数据结构 | 查找
date: 2019-12-8 11:39:58
tags: 数据结构
静态查找表:顺序查找和折半查找;
动态查找表:二叉排序树、二叉平衡树、B/B+树、哈希查找、哈希表
主要内容
- 9.1 静态查找表
9.1.1 顺序表的查找
9.1.2 有序表的查找 - 9.2 动态查找表
9.2.1 二叉排序树和二叉平衡树
9.2.2 B_树和B+树//本篇文章此处截止 - 9.3 哈希( Hashing )表(散列表)
参考资料:
B树部分大面积整理使用了这位文哥的资料,前辈总结的真的特别好,这里直接使用了……侵删。
作者:文哥的学习日记
链接:https://www.jianshu.com/p/7dedb7ebe033
概 述
- 查找表 (search table):
同一类型数据元素构成的集合。 - 查找操作:
(1)查询某个“特定的”数据元素是否在查找表中;
(2)检索某个“特定的”数据元素的各种属性;
(3)在查找表中插入一个数据元素;
(4)从查找表中删除某个数据元素.
静态查找表:对查找表只作(1)、(2)操作;
动态查找表:可以对查找表作(1)-(4)操作。 - 查找方法评价
1.查找速度
2.占用存储空间多少
3.算法本身复杂程度
4.平均查找长度ASL(Average Search Length):为确定记录在表中的位置,需和给定值进行比较的关键字的个数的期望值叫查找算法的ASL。
静态查找表
定义
只提供如下两种查找的查找表:
1) 查询某个“特定”元素是否在表中;
2) 检索某个“特定”元素的各种属性;
顺序表及其查找 —— 顺序查找法
查找表组织
查找表用线性表表示。
即将查找表的记录排成一个记录序列(无序)。
L1=(45,53,12,3,37,24,100,61,90,78)
typedef struct{ //静态查找表的顺序存储结构
ElemType *elem;
int length;
}SSTable;
顺序查找
查找过程:从表的一端开始逐个进行记录的关键字和给定值的比较
逐一比较,且设置0号位置为监视哨,0处值等于待查找值。
顺序查找的性能分析
平均查找长度ASLss=(n+1)/2
顺序查找的算法
int Search_Seq(SSTable ST, KeyType key){
//在顺序表ST中顺序查找其关键字等于key的数据元素。
//若找到,则函数值为该元素在表中的位置,否则为0。
ST.elem[0].key=key; // “哨兵”
for(i=ST.length;!EQ(key,ST.elem[mid].key);--i)
//从后向前找
return i; //若表中不存在待查元素,i=0
}//Search_Seq
有序表及其查找 —— 折半查找法
- 有序表
若线性表中的记录按关键字有序,则称为有序表
查找表组织
查找表用有序表表示。
即将查找表的记录排成按关键字有序的序列。
折半查找
- 查找过程:每次将待查记录所在区间缩小一半
- 适用条件:采用顺序存储结构的有序表
- 算法实现
设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,k为给定值
初始时,令low=1,high=n,mid=floor[(low+high)/2]
让k与mid指向的记录比较:
若k==r[mid].key,查找成功;
若k<r[mid].key,则high=mid-1;
若k>r[mid].key,则low=mid+1;
重复上述操作,直至low>high时,查找失败.
折半查找的性能分析
查找过程中所有元素可构成一颗判定二叉树,例如:
- 判定树上每个结点需要的查找次数刚好为该结点所在的层数.
- 无论查找成功或失败,次数不会超过判定树的深度
- n个结点的判定树的深度为[log2n]+1
- 折半查找的算法复杂度不超过[log2n]+1
查找成功时的平均查找长度
ASLbs= log2(n+1)-1
折半查找的算法
int Search_Bin ( SSTable ST, KeyType key ) {
// 在有序表ST中折半查找其关键字等于key的数据元素。
// 若找到,则函数值为该元素在表中的位置,否则为0。
low=1;high=ST.length; // 置区间初值
while (low<=high) {
mid=(low+high)/2;
if(EQ(key,ST.elem[mid].key))
return mid; // 找到待查元素
else if (LT(key,ST.elem[mid].key))
high = mid - 1; // 继续在前半区间进行查找
else
low = mid + 1; // 继续在后半区间进行查找
}
return 0; // 顺序表中不存在待查元素
} // Search_Bin
分块查找 —— 索引顺序查找
查找表组织
分块有序表
索引顺序查找
查找过程
将表分成几块,块内无序,块间有序;
先确定待查记录所在块,再在块内查找。
索引顺序查找的性能分析
索引顺序查找的算法实现
该表的构造过程是:把要查找的表分成长度相等的几个子表(称为块)。对每个子表建立一个索引项。
索引表中关键字递增有序,具体表块内部记录可以无序。但块之间一定有序(即后一块中的最小关键字都大于前一块中最大关键字)。
查找过程分两步:
1.首先在索引表中确定待查记录所在的块;
2.在块中按顺序查找。
注意,索引表有序且是顺序存储结构中可以用折半查找。块中记录无序只能用顺序查找。
查找方法比较
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序/无序 | 有序 | 分块有序 |
存储结构 | 顺序存储结构/线性链表 | 顺序存储 | 顺序存储结构/线性链表 |
动态查找表
定义
静态以外,额外提供查找后的插入和删除操作。
二叉排序树
定义
- 二叉排序树或是一棵空树,或是具有下列性质的二叉树:
1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
2.若它的右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值
3.它的左、右子树也分别为二叉排序树
二叉排序树的插入(生成)
- 插入原则
若二叉排序树为空,则插入结点应为新的根结点;否则,继续在其左、右子树上查找,直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子 - 二叉排序树生成
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树
查找算法一
//在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素,
//若查找成功,则返回指向该数据元素结点的指针,否则返回空指针。
BiTree SearchBST(BiTree T,KeyType key){
if(!T)||EQ(key,T->data.key))
return(T);
else if LT(key, T->data.key) //查找的值比当前节点小
return(SearchBST(T->lchild,key));
else
return(SearchBST(T->rchild.key));
}//SearchBST
查找算法二
//在根指针T所指二叉排序树中递归地查找其关键字等于key的数据元素,若查找
//成功,则指针p指向该数据元素结点,并返回TRUE,否则指针p指向查找路径上
//访问的最后一个结点并返回FALSE,指针f指向T的双亲,其初始调用值为NULL
Status SearchBST(BiTree T,KeyType key,BiTree f,BiTree &p){
if(!T)
{p=f;return FALSE;}
else if EQ(key,T->data.key)
{ p=T;return TRUE;}
else if LT(key,T->data.key)//每一步查找记录cur的双亲,即把T赋给下一个f
SearchBST(T->lchild,key,T,p);
else
SearchBST(T->rchild,key,T,p);
}//SearchBST
插入算法
// 当二叉排序树T中不存在关键字等于e.key的数据元素时,插入e并返回TRUE,
// 否则返回FALSE。
Status InsertBST(BiTree &T,ElemType e){
if(!SearchBST(T, e.key, NULL, p){
s=(BiTree)malloc(sizeof(BiTNode));
s->data=e;
s->lchild=s->rchild=NULL;
if(!p) //指针p指向了查找路径上访问的最后一个结点
T=s;//这里是判断若整个二叉树为空,则把s作为根节点
else if(LT(e.key,p->data.key)
p->lchild=s;
else
p->rchild=s;
return TRUE;
}
else return FALSE;
}//InsertBST
二叉排序树的生成
注意:若有重复元素,则不再插入。
二叉排序树的删除
要删除二叉排序树中的p结点,分三种情况:
- (1) p为叶子结点,只需修改p双亲f的指针
f->lchild=NULL f->rchild=NULL - (2) p只有左子树或右子树
p只有左子树,用p的左孩子代替p (1)(2)
p只有右子树,用p的右孩子代替p (3)(4) - (3) p左、右子树均非空
沿p左子树的根C的右子树分支找到S,满足S的右子树为空,将S的左子树成为S的双亲Q的右子树,用S取代p (5)
若C无右子树,用C取代p (6)
也即S替代要删的店,S的左子树替代S。
算法在二叉排序树中删除一个节点的算法
Status DeleteBST(BiTree &T,KeyType key){
if(!T)
return FALSE;
else{
if EQ(key,T->data.key)
Delete(T);
else if LT(key,T->data.key)
DeleteBST(T->lchild, key);
else
DeleteBST(T->rchild, key);
return TRUE;
}
}
void Delete(BiTree &p){
if(!p->rchild){q=p;p=p->lchild;free(q);}
//用p的左孩子代替p
else if(!p->lchild){q=p;p=p->rchild;free(q);}
//用p的右孩子代替p
else{
q=p;s=p->lchild;
while(s->rchild){q=s;s=s->rchild;}
//转左,然后向右到尽头
p->data=s->data; //s指向被删结点的"前驱"
if(q!=p)
q->rchild=s->lchild; //重接*q的右子树
else q->lchild=s->lchild; //重接*q的左子树
}
}//delete
二叉排序树性能分析
只有二叉排序树为平衡树时,其平均查找时间为O(log n),
反之,可能退化直到顺序查找O(n)。
平衡二叉树
定义
平衡二叉树又称AVL树。
它或者是一棵空树,或者是具有下列性质的二叉树:
它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
平衡因子
结点的平衡因子BF(Balance Factor)是左子树的深度减去右子树的深度,它只可能是 -1, 0, 1
二叉排序树转成平衡树
失去平衡后进行调整的四种情况
单向右旋平衡处理 LL型
当在左子树上插入左结点,使平衡因子由1增至2时
调整过程
将BA向右旋转90度,把B的右孩子变为A的左孩子,A变为B的右孩子,B替代A的位置。
单向左旋平衡处理 RR型
当在右子树上插入右结点,使平衡因子由-1增至-2时
调整过程
将BA向右旋转90度,把B的右孩子变为A的左孩子,A变为B的右孩子,B替代A的位置。
双向旋转(先左后右)平衡处理 LR型
当在左子树上插入右结点,使平衡因子由1增至2时
调整过程
1)将CB向左旋转90度,把CL变为B的右子树,把B变为C 的左孩子;
2)将BCA向右旋转90 度,把CR变为A的左孩子,A变为C的右孩子;最后,C带替A的位置。
双向旋转(先右后左)平衡处理 RL型
当在右子树上插入左结点,使平衡因子由-1增至-2时
调整过程
1)将CB向右旋转90度,把CR变为B的右子树,把B变为C的左孩子;
2)将BCA向左旋转90度,把C的左孩子变为A的右孩子,A变为C的左孩子;最后,C带替A的位置。
平衡二叉树的查找及性能分析
它的时间复杂度与二叉排序树的最好时间复杂相同,都为O(log2n)。
B-树
类似“多叉的”二叉查找树
定义
B-树中所有结点中孩子结点个数的最大值成为B-树的阶,通常用m表示,从查找效率考虑,一般要求m>=3。一棵m阶B-树或者是一棵空树,或者是满足以下条件的m叉树。
1)每个结点最多有m个分支(子树);而最少分支数要看是否为根结点,如果是根结点且不是叶子结点,则至少要有两个分支,非根非叶结点至少有ceil(m/2)个分支,这里ceil代表向上取整。
2)如果一个结点有n-1个关键字,那么该结点有n个分支。这n-1个关键字按照递增顺序排列。
3)每个结点的结构为:
n k1 k2 ... kn
p0 p1 p2 ... pn
其中,n为该结点中关键字的个数;ki为该结点的关键字且满足ki<ki+1;pi为该结点的孩子结点指针且满足pi所指结点上的关键字大于ki且小于ki+1,p0所指结点上的关键字小于k1,pn所指结点上的关键字大于kn。
4)结点内各关键字互不相等且按从小到大排列。
5)叶子结点处于同一层;可以用空指针表示,是查找失败到达的位置。
注:平衡m叉查找树是指每个关键字的左侧子树与右侧子树的高度差的绝对值不超过1的查找树,其结点结构与上面提到的B-树结点结构相同,由此可见,B-树是平衡m叉查找树,但限制更强,要求所有叶结点都在同一层。
摘录了大佬 文哥的学习日记 的实例,原文链接在开篇已经给出。
上面的图片显示了一棵B-树,最底层的叶子结点没有显示。我们对上面提到的5条特点进行逐条解释:
1)结点的分支数等于关键字数+1,最大的分支数就是B-树的阶数,因此m阶的B-树中结点最多有m个分支,所以可以看到,上面的一棵树是一个5-阶B-树。
2)因为上面是一棵5阶B-树,所以非根非叶结点至少要有ceil(5/2)=3个分支。根结点可以不满足这个条件,图中的根结点有两个分支。
3)如果根结点中没有关键字就没有分支,此时B-树是空树,如果根结点有关键字,则其分支数比大于或等于2,因为分支数等于关键字数+1.
4)上图中除根结点外,结点中的关键字个数至少为2,因为分支数至少为3,分支数比关键字数多1,还可以看出结点内关键字都是有序的,并且在同一层中,左边结点内所有关键字均小于右边结点内的关键字,例如,第二层上的两个结点,左边结点内的关键字为15,26,他们均小于右边结点内的关键字39和45.
B-树一个很重要的特征是,下层结点内的关键字取值总是落在由上层结点关键字所划分的区间内,具体落在哪个区间内可以由指向它的指针看出。例如,第二层最左边的结点内的关键字划分了三个区间,小于15,15到26,大于26,可以看出其下层中最左边结点内的关键字都小于15,中间结点的关键字在15和26之间,右边结点的关键字大于26.
5)上图中叶子结点都在第四层上,代表查找不成功的位置。
B-树上的查找
(1)将给定key值与根结点关键字k[i](k[]为结点内的关键字数组)比较,如果相等,那么查找成功。
(2)
- 若key<k[1],则到p[0]所指示的子树中进行继续查找(p[ ]为结点内的指针数组),这里要注意B-树中每个结点的内部结构。
- 若key>k[n],则道p[n]所指示的子树中继续查找。
- 若k[i]<key<k[i+1],则沿着指针p[I]所指示的子树继续查找。
(3)如果,查找到叶子结点,那么查找失败。
B-树上的插入
与二叉排序树一样,B-树的创建过程也是将关键字逐个插入到树中的过程。
在进行插入之前,要确定一下每个结点中关键字个数的范围,如果B-树的阶数为m,则结点中关键字个数的范围为 [ceil(m/2)-1,m-1] 个。
对于关键字的插入,需要找到插入位置。在B-树的查找过程中,当遇到空指针时,则证明查找不成功,同时也找到了插入位置,即根据空指针可以确定在最底层非叶结点中的插入位置,为了方便,我们称最底层的非叶结点为终端结点,由此可见,B-树结点的插入总是落在终端结点上。在插入过程中有可能破坏B-树的特征,如新关键字的插入使得结点中关键字的个数超过规定个数,这是要进行结点的拆分。
接下来,我们以关键字序列{1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15}创建一棵5阶B-树,我们将详细体会B-树的插入过程。
(1)确定结点中关键字个数范围
由于题目要求建立5阶B-树,因此关键字的个数范围为2~4
(2)根结点最多可以容纳4个关键字,依次插入关键字1、2、6、7后的B-树如下图所示:
(3)当插入关键字11的时候,发现此时结点中关键字的个数变为5,超出范围,需要拆分,去关键字数组中的中间位置,也就是k[3]=6,作为一个独立的结点,即新的根结点,将关键字6左、右关键字分别做成两个结点,作为新根结点的两个分支,此时树如下图所示:
(4)新关键字总是插在叶子结点上,插入关键字4、8、13之后树为:
(5)关键字10需要插入在关键字8和11之间,此时又会出现关键字个数超出范围的情况,因此需要拆分。拆分时需要将关键字10纳入根结点中,并将10左右的关键字做成两个新的结点连在根结点上。插入关键字10并经过拆分操作后的B-树如下图:
(6)插入关键字5、17、9、16之后的B-树如图所示:
(7)关键字20插入在关键字17以后,此时会造成结点关键字个数超出范围,需要拆分,方法同上,树为:
(8)按照上述步骤依次插入关键字3、12、14、18、19之后B-树如下图所示:
(9)插入最后一个关键字15,15应该插入在14之后,此时会出现关键字个数超出范围的情况,则需要进行拆分,将13并入根结点,13并入根结点之后,又使得根结点的关键字个数超出范围,需要再次进行拆分,将10作为新的根结点,并将10左、右关键字做成两个新结点连接到新根结点的指针上,这种插入一个关键字之后出现多次拆分的情况称为连锁反应,最终形成的B-树如下图所示:
B-树的删除
对于B-树关键字的删除,需要找到待删除的关键字,在结点中删除关键字的过程也有可能破坏B-树的特性,如旧关键字的删除可能使得结点中关键字的个数少于规定个数,这是可能需要向其兄弟结点借关键字或者和其孩子结点进行关键字的交换,也可能需要进行结点的合并,其中,和当前结点的孩子进行关键字交换的操作可以保证删除操作总是发生在终端结点上。
我们用刚刚生成的B-树作为例子,一次删除8、16、15、4这4个关键字。
(1)删除关键字8、16。关键字8在终端结点上,并且删除后其所在结点中关键字的个数不会少于2,因此可以直接删除。关键字16不在终端结点上,但是可以用17来覆盖16,然后将原来的17删除掉,这就是上面提到的和孩子结点进行关键字交换的操作。这里不能用15和16进行关键字交换,因为这样会导致15所在结点中关键字的个数小于2。因此,删除8和16之后B-树如下图所示:
(2)删除关键字15,15虽然也在终端结点上,但是不能直接删除,因为删除后当前结点中关键字的个数小于2。这是需要向其兄弟结点借关键字,显然应该向其右兄弟来借关键字,因为左兄弟的关键字个数已经是下限2.借关键字不能直接将18移到15所在的结点上,因为这样会使得15所在的结点上出现比17大的关键字,所以正确的借法应该是先用17覆盖15,在用18覆盖原来的17,最后删除原来的18,删除关键字15后的B-树如下图所示:
(3)删除关键字4,4在终端结点上,但是此时4所在的结点的关键字个数已经到下限,需要借关键字,不过可以看到其左右兄弟结点已经没有多余的关键字可借。所以就需要进行关键字的合并。可以先将关键字4删除,然后将关键字5、6、7、9进行合并作为一个结点链接在关键字3右边的指针上,也可以将关键字1、2、3、5合并作为一个结点链接在关键字6左边的指针上,如下图所示:
显然上述两种情况下都不满足B-树的规定,即出现了非根的双分支结点,需要继续进行合并,合并后的B-树如下图所示:
有时候删除的结点不在终端结点上,我们首先需要将其转化到终端结点上,然后再按上面的各种情况进行删除。在讲述这种情况下的删除方法之前,要引入一个相邻关键字的概念,对于不在终端结点的关键字a,它的相邻关键字为其左子树中值最大的关键字或者其右子树中值最小的关键字。找a的相邻关键字的方法为:沿着a的左指针来到其子树根结点,然后沿着根结点中最右端的关键字的右指针往下走,用同样的方法一直走到叶结点上,叶结点上的最右端的关键字即为a的相邻关键字(这里找的是a左边的相邻关键字,我们可以用同样的思路找到a右边的相邻关键字)。可以看到下图中a的相邻关键字是d和e,要删除关键字a,可以用d来取代a,然后按照上面的情况删除叶子结点上的d即可。
B-树的应用
为了将大型数据库文件存储在硬盘上,以减少访问硬盘次数为目的,在此提出了一种平衡多路查找树——B-树结构。由其性能分析可知它的检索效率是相当高的 为了提高 B-树性能’还有很多种B-树的变型,力图对B-树进行改进,比如B+树。
B+树
B+树是应文件系统所需而出的一种B-树的变型树。
一棵m阶的B+树和m阶的B-树的差异在于:
- 有n棵子树的结点中含有n个关键字。
- 所有的叶子结点中包含了全部关键字信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。
键树
又称数字查找树,它是一棵度≥2的树,树中的每个结点中不是包含一个或几个关键字,而是只含有组成关键字的符号。
例如,若关键字为数值,则结点中只包含一个数位;若关键字为单词,则结点中只包含一个字母字符。这种树会给某种类型关键字的表的查找带来方便。
哈希查找
基本思想
在记录的存储地址和它的关键字之间建立一个确定的对应关系;这样,不经过比较,一次存取就能得到所查元素的查找方法
定义
哈希函数
在记录的关键字与记录的存储地址之间建立的一种对应关系叫哈希函数。
addr(ai)=H(ki)
其中,ai是表中元素,addr()是取址函数,ki是ai的关键字,H()是哈希函数。
关键字集合——H( )——>存储地址集合
哈希表
应用哈希函数,由记录的关键字确定记录在表中的地址,并将记录放入此地址,这样构成的表叫哈希表。
哈希查找
又叫散列查找,利用哈希函数进行查找的过程。
哈希函数的构造方法
直接定址法
- 构造
取关键字或关键字的某个线性函数作哈希地址,即H(key)=key 或 H(key)=a·key+b - 特点
直接定址法所得地址集合与关键字集合大小相等,不会发生冲突 - 实际中能用这种哈希函数的情况很少
数字分析法
- 构造
对关键字进行分析,取关键字的若干位或其组合作哈希地址 - 适于关键字位数比哈希地址位数大,且可能出现的关键字事先知道的情况
平方取中法
- 构造
取关键字平方后中间几位作哈希地址 - 适于不知道全部关键字情况
折叠法
- 构造
将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)做哈希地址 - 种类
移位叠加:将分割后的几部分低位对齐相加
间界叠加:从一端沿分割界来回折送,然后对齐相加 - 适于关键字位数很多,且每一位上数字分布大致均匀情况
除留余数法
- 构造
取关键字被某个不大于哈希表表长m的数p除后所得余数作哈希地址,即H(key)=key MOD p,pm - 特点
简单、常用,可与上述几种方法结合使用
p的选取很重要;p选的不好,容易产生同义词
随机数法
- 构造
取关键字的随机函数值作哈希地址,即H(key)=random(key) - 适于关键字长度不等的情况
选取哈希函数的因素
- 计算哈希函数所需时间
- 关键字长度
- 哈希表长度(哈希地址范围)
- 关键字分布情况
- 记录的查找频率
处理冲突的方法
开放定址法
-
方法
当冲突发生时,形成一个探查序列;沿此序列逐个地址探查,直到找到一个空位置(开放的地址),将发生冲突的记录放到该地址中,
即hash(key) = (hash(key)+di)mod TableSize。
di为增量序列,TableSize为表长。 -
分类
线性探测再散列:di=1,2,3,……m-1
二次探测再散列:di=1²,-1²,2²,-2²,3²,……±k²(k <= m/2)
伪随机探测再散列:di=伪随机数序列
再哈希法
- 方法
构造若干个哈希函数,当发生冲突时,计算下一个哈希地址,即:Hi=Rhi(key) i=1,2,……k
其中:Rhi——不同的哈希函数 - 特点
计算时间增加
链地址法
- 方法
将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针。
哈希查找过程及分析
哈希查找过程
哈希查找分析
- 哈希查找过程仍是一个给定值与关键字进行比较的过程
- 评价哈希查找效率仍要用ASL
- 哈希查找过程与给定值进行比较的关键字的个数取决于:
- 哈希函数
- 处理冲突的方法
- 哈希表的填满因子=表中填入的记录数/哈希表长度
哈希表小结
-
散列方法的查找效率不依赖于n ,只依赖于负载因子
α=n/M n为散列表中关键字个数,M为散列表表长,散列表检索的平均检索长度只随α 的增大而增加。 -
散列方法的应用限制
散列方法一般不适用于重复关键码值的应用程序
散列方法一般不适用于范围检索