查找
查找的基本概念
1、查找的定义
- “查找表” :同一类型的数据元素(或记录)构成的集合。 由于“集合”中的数据元素之间存在松散的关系,可用查找表进行灵活应用;
查找—指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素;
- 若表中存在这样的一个记录, 则称查找成功,查找的结果可给出整个记录的信息,或指 示该记录在查找表中的位置;
- 若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个 “空” 记录或 “空” 指针;
2、查找表的分类和评价指标
- 分类
静态查找表 仅作“查询”(检索)操作的查询表;
动态查询表 作“插入”和“删除”操作的查询表;
- 查找算法的评价指标
平均查找长度(ASL):关键字的平均比较次数;
线性表的查找
1、顺序查找
- 应用范围:
顺序表或线性链表表示的静态查找表
表内元素之间无序;
- 顺序表的表示:
typedef struct {
ElemType *R;//表基址
int length;//表长
}SSTable;
- 在顺序表中查找值为e的数据元素
int LocateELem(SqList L,ElemType e){
for (i=0;i< L.length;i++)
if (L.elem[i]==e) return i+1;
return 0;
}
改进:把待查关键字key存入表头(“哨兵”),从后向前逐个比较,可免去查找过程中每一步 都要检测是否查找完毕,加快速度;
- 在顺序表ST中查找值为Key的数据元素 查找13:找到,返回下标5;如果没找到返回0;
int Search_Seq( SSTable ST , KeyType key ){
ST.R[0].key =key; //若成功返回其位置信息,否则返回0
for( i=ST.length; ST.R[ i ].key!=key; -- i );//不用for(i= ST.length-1; i>0; --i) 或 for(i=1; i< ST.length; i++)
return i;
}
空间复杂度:一个辅助空间—O(1);
时间复杂度:
1、查找成功时的平均查找长度:ASLs(n)=(1+2+ … +n)/n =(n+1)/2;
2、查找不成功时的平均查找长度:ASLf =n+1;
-
优点:算法简单,逻辑次序无要求,适用于不同的存储结构;
-
缺点:ASL太长,时间效率太低;
-
改进措施:非等概率查找时,可按照查找概率进行排序。
2、折半查找
- 也称二分查找:首先使用排序算法对查找表进行排序,保证有序;
- 以升序数列为例,比较一个元素与数列中的中间位置的元素的大小: 如果比中间位置的元素大,则在后半部分的数列进行二分查找 如果比中间位置的元素小,则在前半部分数列的进行二分查找 如果相等,找到了元素的位置。
若k==R[mid].key,查找成功;
若k<R[mid].key,则high=mid-1;
若k>R[mid].key,则low=mid+1;
设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,k为给定值;
初始时,令low=1,high=n,mid= (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时,查找失败
int BinSearch(int r[],int low,int high,int k){
int mid;
if(low>high) return 0;
else{
mid=(low+high)/2;
if(k<r[mid]) return BinSearch(r,low,mid-1,k);
else if(k>r[mid]) return BinSearch(r,mid+1,high,k);
else return mid;
}
}
int BinSearch(int r[],int n,int k){
int mid,low=1,high=n;
while(low<=high){
mid=(low+high)/2;
if(k<r[mid]) high=mid-1;
else if(k>r[mid])
low=mid+1;
else
return mid;
}
return 0; //查找失败返回0
}
假定每个元素的查找概率相等,求查找成功时的平均查找长度。
ASL=(1x1+2x2+4x3+4x4 ) x1/11=33/11=3;
查找过程
每次将待查记录所在区间缩小一半,比顺序查找效率高,时间复杂度O(log2n);
适用条件
采用顺序存储结构的有序表,不宜用于链式结构;
3、分块查找
分块有序,即分成若干子表,要求每个子表中的数值都比后一块中数值小(但子表内部未必有序);
然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址(即头指针);
分块查找的平均查找长度为索引查找和块内查找的平均长度之和。设长度为n 的查找表均匀地分 为b 块,每块有s 个记录,在概率相等的情况下,如果对索引表进行顺序查找,则
如果对索引表进行折半查找,则
-
优点:插入和删除比较容易,无需进行大量移动;
-
缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算;
-
适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。
就平均查找长度而言,折半查找的平均查找长度最小,分块查找次之,顺序查找最大;
就表的结构而言,顺序查找对有序表、无序表均适用,折半查找仅适用于有序表,而分块查找要求表中元素至少是分块有序的。
二叉排序树
1、二叉排序树的定义
二叉排序树(Binary Sort Tree, BST),也称二叉查找树;
二叉排序树或者是一棵空树,或者是一棵具有下列特性的非空二叉树:
1、 若左子树非空,则左子树上所有结点关键字均小于根结点的关键字值;
2、 若右子树非空,则右子树上所有结点关键字均大于根结点的关键字值;
3、 左、右子树本身也分别是一棵二叉排序树。
二叉排序树是一个递归的数据结构 根据二叉树的定义,可得左子树结点值 < 根结点值 < 右子树结点值。 所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
2、二叉排序树(BST)的操作
- 创建
将8作为根节点
插入3,由于3小于8,作为8的左子树
插入10,由于10大于8,作为8的右子树
插入1,由于1小于8,进入左子树3,1又小于3,则1为3的左子树
插入6,由于6小于8,进入左子树3,6又大于3,则6为3的右子树
插入14,由于14大于8,进入右子树10,14又大于10,则14为10的右子树
插入4,由于4小于8,进入左子树3,4又大于3,进入右子树6,4还小于6,则4为6的左子树
插入7
插入13
- 查找
bool search(Node* root, int key){
while (root != NULL){
if(key == root->data)
return true;
else if(key < root->data)
root = root->left;
else
root = root->right;
}
return false;
}
- 删除
1、被删除结点为叶子结点
直接从二叉排序中删除即可,不会影响到其他结点。例如删去7:
2、被删除结点D仅有一个孩子 如果只有左孩子,没有右孩子,那么只需要把要删除结点的 左孩子连接到要删除结点的父亲结点,然后删除D结点;
如果只有右孩子,没有左孩子,那么只要将要删除结点D的 右孩子连接到要删除结点D的父亲结点,然后删除D结点;
3、被删除结点左右孩子都在
保证删除结点8后,再次中序遍历它,仍不改变其升 序的排列方式。 只有用7或者10来替换8原来的位置。
平均查找长度和二叉树的形态有关:
最好:log2n(形态匀称,与二分查找的判定树相似);
最坏:(n+1)/2(单支树);
第i层结点需比较i次,两图的平均查找长度为:
平衡二叉树
1、平衡二叉树(AVL)的定义
- 平衡二叉树(AVL):任意节点的子树的高度差都小于等于1的二叉排序树;
平衡因子 BF( Balance Factor ):左子树和右子树高度差 一般来说 BF 的绝对值大于 1,平衡树二叉树就失衡,需要「旋转」纠正 最小不平衡子树是距离插入节点最近的,并且 BF 的绝对值大于 1 的节点为根节点的子树。 旋转纠正只需要纠正最小不平衡子树即可;
不论用什么方法,操作以后,都是让是三个点之中大小为中间的数做根,比中间 数小的数,做根的左孩子,比中间数大的数,做根的右孩子,因此有一个简单的 方法:
1、不管什么结构,从最深的叶子节点向上递归查找第一个失衡的点,标号为1;
2、再选1下面的点为2;
3、再选2下面的点为3;
4、不管什么结构,大小为中间的数做根,比中间数小的数,做根的左孩子,比中 间数大的数,做根的右孩子;
5、然后按照分支和每个点之间的关系,补全,这样就完成了。
2、平衡二叉树(AVL)的操作
- 将下面序列构成一棵平衡二叉排序树( 13,24,37,90,53)
B树与B+树
1、B树的定义
-
B树即为B-树。因为B树的原英文名称为B-tree(Balance-Tree);
比如数据库里有一张表,有100万条记录,现在要查找其中的某条数据,如何快速地从100万条 记录中找到需要的那条记录呢?
-
平衡:所有叶子结点在同一层,左边和右边分布均匀;
-
有序:每个结点中有序,且左子树小于根,根小于右子树;
-
多路:多棵子树(如果最多子树个数为m,则称m阶B-tree )。
- 所有叶子结点都在同一层上(失败结点,指向叶子的指针为空);
- 结点中按关键字大小顺序排列,非叶子结点的结构如下:n为关键字,p为指针
(1)树中每个结点至多有m棵子树,即至多含有m-1个关键字;
(2)若根节点不是终端结点,则至少有两棵子树。 当根节点为叶子节点时,可以没有子树;不为叶子节点时,至少有一个关键字,也就是至少有两 棵子树;
(3)除根节点外的所有非叶结点至少有ceil(m/2)棵子树,即至少含有ceil(m/2)-1个关键字。 (为了减少不必要的层高,提高效率和资源利用率) ceil(x)函数用于求不小于 x 的最小整数,也即向上取整。
2、B树的查找
3、B树的插入
插入后确保至多m-1个关键字;
先查找,进入最下层结点;
有空位,直接插入;
该结点中关键字个数n<m-1,有序插入到该结点的合适位置;
无空位,分裂;
即原关键字个数n=m-1 。
无空位,插入85
无空位,插入7
4、B树的删除
删除后的结点中的关键字个数>= ceil(m/2)-1,因此将涉及结点的“合并”问题。由于删除的关键字位 置不同,可以分为关键字在终端结点和不在终端结点两种情况。 首先找到待删关键字key所在结点 (1)若key在最下层结点中
结点中关键字个数 > m/2-1, 则直接删除
结点中关键字个数== m/2-1, 且兄弟… > m/2-1,则借调
结点、其兄弟的关键字个数都== m/2-1,则合并
(2)若key不在最下层,为了“中序有序”
用key的左子树中的最大码或右子树中的最小码k取代key,然后从该子树中删除k(k一定在最下层)
删除关键字K=50
删除关键字K=53
首先找到待删关键字key所在结点
(1)若key在最下层结点中
结点中关键字个数 > m/2-1, 则直接删除;
结点中关键字个数==m/2-1, 且兄弟… > m/2-1,则借调;
结点、其兄弟的关键字个数都==m/2-1,则合并;
(2)若key不在最下层,为了“中序有序”
用key的左子树中的最大码或右子树中的最小码k取代key,然后从该子树中删除k(k一定在 最下层)
5、B+树
B+树是数据库和操作系统中的一种用于文件查找的数据结构,是一种高效的平衡查找树(效率比B-树高)
在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树。叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中,且叶结点的指针指向记录。在B-树中,具有n个关键字的结点含有(n+1)棵子树,叶结点和其他结点包含的关键字是不重复的。
在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树 的最大(或最小)关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
在B-树中每个关键字对应一个记录的存储地址;
在B+树中,有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个单链表。
- B+树比B-树更适合实际应用中操作系统的文件索引
红黑树
1、红黑树的性质
平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。 但是,插入节点之后,为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建 一颗平衡二叉树的成本其实不小;
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。可在 O(logN) 时间内完成查找、 增加、删除等操作;
红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念, 它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并 没有严格的卡定某个平衡因子来维持绝对平衡);
通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二 倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根 节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)。
- 红黑树的性质
节点是红色或黑色;
根是黑色;
叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点), 红色节点的子节点都是黑色,红色节点的父节点都是黑色,从根节点到叶子节点的所有路径上不 能有 2 个连续的红色节点;
从任一节点到叶子节点的所有路径都包含相同数目的黑色节点。
2、红黑树的插入
并查集
1、并查集的定义
用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元。
2、并查集的操作
定义一个数组:int fa[1000]; (数组长度依题意而定)。这个数组记录了每个人的上级是谁。
1、用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元;
2、一个集合内的所有元素组织成以代表元为根的树形结构;
3、对于每一个元素 x,fa[x] 存放 x 在树形结构中的父亲节点(如果 x 是根节点,则令fa[x] = x);
4、对于查找操作,假设需要确定 x 所在的的集合,也就是确定集合的代表元。可以沿着fa[x]不断 在树形结构中向上移动,直到到达根节点。
因此,基于这样的特性,并查集的主要用途有以下两点:
1、维护无向图的连通性(判断两个点是否在同一连通块内,或增加一条边后是否会产生环);
2、用在求解最小生成树的Kruskal算法里。将图中的每个edge按照权重大小进行排序,每次从边 集中取出权重最小且两个顶点都不在同一个集合的边加入生成树。如果这两个顶点都在同一集合内, 说明已经通过其他边相连,因此如果将这个边添加到生成树中,那么就会形成环。
散列表
1、散列表定义
- 基本思想
查找元素记录的存储位置与关键字之间存在对应关系;
以数据对象的关键字key为自变量,通过一个确定的函数关系h,计算出对应的函数值h(key),把这 个值解释为数据对象的存储地址,并按此存放,即“存储位置 = h(key)”。 优点:查找速度极快O(1),查找效率与元素个数n无关。
根据哈希函数H(k)=k%17;
查找key=42,则访问H(42)=42%17=8号地址,若内容为42则成功;
若查不到,则返回一个特殊值,如空指针或空记录。
可能将不同的关键字映射到同一个散列地址上,即h(keyi) = h(keyj),但是 keyi≠keyj,这 种现象称为“冲突(Collision)”,keyi和keyj称为“同义词(synonym)” 。 有6个元素的关键码分别为:(14,23,39,9,25,11)。
哈希函数:H(k)=k mod 7;
地址编码从0-6;
2、散列表的构造
- 在构造前考虑两个方面:
1、构造好的散列函数,避免浪费空间;
2、制定一个好的解决冲突的方案。
- 构造方法:
根据元素集合的特性构造:
1、地址空间尽量小;
2、均匀。
- 直接定址法
Hash(key) = a·key + b (或Hash(key)=key)(a、b为常数))
优点:以关键码key的某个线性函数值为哈希地址,不会产生冲突;
缺点:要占用连续地址空间,空间效率低。
- 除留余数法
3、散列表冲突的处理
- 开放定址法
基本思想:有冲突时就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找 到,并将数据元素存入。
优点:只要哈希表未被填满,保证能找到一个空地址单元存放有冲突的元素。
缺点:可能使第i个哈希地址的同义词存入第i+1个地址,这样本应存入第i+1个哈希地址 的元素变成了第i+2个哈希地址的同义词,……,产生“聚集”现象,降低查找效率。