数据结构(七)–查找
文章目录
查找
查找的基本概念
基本概念
-
查找表: 同一类型的数据元素构成的集合; 包括线性表、树表、散列表
由于"集合"中的数据元素之间存在着松散的关系, 因此查找表示一种应用灵便的结构
-
关键字: 是数据元素中某个数据项的值, 用以标识数据元素(或记录), 分主关键字和次关键字
主关键字: 可唯一地标识一个记录的关键字是主关键字;
次关键字: 反之, 用以识别若干记录的关键字是次关键字.
-
查找 (是否成功)
- 查询某个"特定的"数据元素是否在查找表中
- 检索某个"特定的"数据元素的各种属性
- 在查找表中插入一个数据元素
- 从查找表中删去某个数据元素
-
查找表分类
- 静态查找表
仅作"查询"(检索)操作的查找表 - 动态查找表
- 作"插入"和"删除"操作的查找表
- 查找表示在查找过程中动态生成的.
- 伴随着插入不存在元素或删除已存在的元素
- 平均查找长度 (评价查找算法的优劣,这里就不用算法的通用指标:时间复杂度,而是用平均查找长度)
- 关键字的平均比较次数,也称平均查找长度 (ASL : Average Search Length)
- ASL =
∑
i
=
1
n
(
p
i
c
i
)
\sum_{i=1}^n ({p_i c_i})
∑i=1n(pici) (关键字比较次数的期望值)
n : 记录的个数
pi :查找第i个记录的概率(通常认为pi = 1 / n)
ci : 找到第i个记录所需的比较次数
- 静态查找表
线性表的查找
应用范围:
- 顺序表或线性表表示的静态查找表
- 表内元素之间无序
顺序表的表示:
数据元素类型的定义:
typedef struct{
KeyType key; //关键字域
...... //其他域
}ElemType;
typedef struct{//顺序表及结构类型定义
ElemType *R; //表基址
int length; //表长
}SSTable;//Sequential Search Table
SSTable ST; //定义顺序表ST
一、顺序查找法:
算法:
下面有三种算法:
-
int Search_Seq(SSTable ST, KeyType key){ //若查找成功返回其位置信息,否则返回0 for(i = ST.Length;i >= 1; --i) //下标从1开始, 0位置不放元素 if(ST.R[i].key == key) return i; return 0; }//Search_Seq
-
int Search_Seq(SSTable ST, KeyType key){ for(i = ST.Length;ST.R[i].key != key; --i) if(i<=0) break; if(i>0)return i; else return 0; }
-
int Search_Seq(SSTable ST, KeyType key){ for(i = ST.Length;ST.R[i].key != key && i>0 ; --i); if(i>0)return i; else return 0; }
改进算法
int Search_Seq(SSTable ST, KeyType key)
{ ST.R[0].key = key;
for(i = ST.length; ST.R[i].key != key; --i);
return i;
}//Search_Seq
//设置哨兵,避免每次比较i>=1, 当n比较大时, 时间可减少一半
- 可用于顺序或链式存储结构
- 从后往前,依次比较. 查找成功, 返回位序, 否则返回0(不存在的位序)
- 设置"哨兵",避免位序控制
性能分析
在等概率查找的情况下,顺序表查找的平均查找长度为:
-
ASL = 1 n \frac{1}{n} n1 ∑ i = 1 n ( n − i + 1 ) \sum_{i=1}^n ({n-i+1}) ∑i=1n(n−i+1) = n + 1 2 \frac{n+1}{2} 2n+1
-
时间复杂度: T(n) = O(n)
二、折半查找
基本要求: 1. 只能用顺序存储结构 2. 表中记录按关键字有序排列
指针low和high分别表示当前查找区间的下界和上界, mid为区间的中间位置
mid = ⌊ \lfloor ⌊ (low + high)/2 ⌋ \rfloor ⌋ (向下取整)
如果关键字key 比中间位置记录的关键字小,则high = mid - 1,否则low = mid + 1
算法描述:
int Search_Bin(SSTable ST, KeyType key)
{ low = 1; high = ST.length;
while(low<=high)
{mid = (low<=high)/2;
if(key == ST.R[mid].key) return mid;
else if(key<ST.R[mid].key)high=mid-1;
else low = mid + 1;
return 0;
}//Search_Bin
举个栗子:
判定树 : 把当前查找区间的中间位置作为根,左子表和右子表分别作为根的左子树和右子树,得到的二叉树称为折半查找的判定树
-
折半查找在查找成功时进行比较的关键字个数最多不超过树的深度.
-
判定树的形态只与表记录个数n相关,而关键字的取值无关
-
具有n个结点的判定树的深度为 ⌊ \lfloor ⌊ log 2 n \log_2n log2n ⌋ \rfloor ⌋ +1
-
ASL = ∑ i = 1 n ( P i C i ) \sum_{i=1}^n ({P_i C_i}) ∑i=1n(PiCi) = n + 1 n \frac{n+1}{n} nn+1 log 2 ( n + 1 ) \log_2 (n+1) log2(n+1)-1
- 当n较大时,近似结果 ASL = log 2 ( n + 1 ) \log_2 (n+1) log2(n+1)-1
- 折半查找时间复杂度为O( log 2 n \log_2n log2n)
-
优点是折半查找效率比顺序查找高,比较次数少
-
缺点是只能用于顺序表,而且是有序表,不是适合用于数据元素经常变动的线性表
折半查找的判定树中,若mid = ⌊ \lfloor ⌊(low+high)/2 ⌋ \rfloor ⌋,则对于任何一个结点,必有:右子树结点数-左子树结点数 = 0或1 ;即折半查找的判定树一定是平衡二叉树,判定树中只有最下面一层是不满的,因此元素个数为n时,树高h = ⌈ \lceil ⌈ l o g 2 ( n + 1 ) log_2(n+1) log2(n+1) ⌉ \rceil ⌉ 【计算方法和“完全二叉树”相同】
判定树结点关键字:左<中<右,满足二叉排序树的定义,失败结点:n+1个(等于成功结点的空链域数量)
三、分块查找
也叫索引查找,是顺序查找的一种改进
- 为查找表建立索引表:关键字+指针
- 索引表按关键字有序,查找表按关键字有序或分块有序
- 查找过程:
- 由索引表确定待查找记录所在分块: 折半查找,或者顺序查找
- 在对应的分块中查找记录: 折半查找或顺序查找
树表的查找
一、二叉排序树
- 1.定义:二叉排序树或者是一棵空树,或者是具有下列性质的二叉树
- 左子树上所有结点的值都小于根结点的值
- 右子树上所有结点的值都大于根结点的值
- 左子树和右子树也是二叉排序树
-
二叉排序树的二叉链表存储:
-
typedef stuct {KeyType key; InfoType otherinfo; }ElemType; typedef stuct BSTNode {ElemType data; stuct BSTNode *lchild,*rchild; }BSTNode,*BSTree;
-
2.查找: 二叉排序树又称二叉查找树,其查找过程是一个从根结点开始,沿某一个分支逐层向下进行比较判等的过程
- 算法思想:
-
- 从根结点开始,如果根指针为NULL,则查找不成功
- 否则用给定值key与根结点的关键字值
T->data.key
进行比较- 如果
key == T->data.key
,则查找成功返回根结点地址 - 如果
key < T->data.key
, 则递归查找根结点的左子树 - 如果
key > T->data.key
, 则递归查找根结点的右子树
- 如果
-
BSTree SearchBST(BSTree T,KeyType key) {BSTree p=T; if ((!T)|| key==T->data.key) return T; else if (key < T->data.key) return SearchBST(T->lchild,key); else return SearchBST(T->rchild,key); }//SearchBST
-
其中时间复杂度:T(n)=O( log 2 n \log_2n log2n)
-
3.插入
-
(1)若
BST
为空, 则待插入结点*S
作为根结点插入空树 -
(2)若
BST
非空,则将key与T->data.key
比较:- ①若
key==T->data.key
:停止插入 - ②若
key < T->data.key
:将*S
插入左子树 - ③若
key > T->data.key
:将*S
插入右子树
- ①若
-
-
(3)新插入的结点一定是不成功结点的左孩子或右孩子
void InsertBST(BSTree &T.ElemType e) { if (!T) {s=new BSTNode; s->data=e; s->lchild = s->rchild = NULL: T=S;} else if (e.key < T->data.key) InsertBST(T->lchild,e); else InsertBST(T->rchild,e); }//InsertBST
-
时间复杂度: T(n) = O( log 2 n \log_2n log2n)
-
-
4.创建:
- 1.将BST初始化为空树
- 2.读入一个关键字为key的结点,插入BST中
- 3.重复执行,直到结束标志
-
void CreatBST(BSTree &T) {T = NULL; cin>>e; while(e.key!=ENDFLAG) {InsertBST(T,e); cin>>e;}}
-
时间复杂度: T(n) = O( n l o g 2 n nlog_2n nlog2n)
-
小结: 1) 构造BST的过程为对无序序列进行排序的过程
2) 利用BST插入结点不必移动已排好序的结点,只需添加一个叶结点即可
3) 初始序列不同,BST的形态也不同,查找性能ASL也不同,最短为 l o g 2 n log_2n log2n ,最长可能为n
4) BST 既有类似于折半查找的特性, 又可采用链表作为存储结构, 因此是动态查找表的适宜 表示
-
5.删除:
- 在BST中删除一个结点时, 必须将断开的二叉链表重新链接起来,并确保BST的性质不会丢失
2)为了保证在执行删除后,其搜索性能不至于降低吗还需要防止重新链接后树的高度增加
- 假设BST中被删的结点是
*p
(PL和PR分别表示其左子树和右子树),其双亲结点是*f
, 并设*p
是*f
的左孩子. 分三种情况:
-
第一种:
-
第二种:
-
第三种:
二、平衡二叉树——AVL树
1.定义: 一颗AVL树或者是空树,或者是具有下列性质的二叉树;它的左右子树都是AVL树,且左子树和右子树的高度之差的绝对值不超过1。
结点的平衡因子(BF)定义为该结点的左子树的深度减去右子树的深度:
所有结点的BF只能是-1、0、1
若有结点BF的绝对值大于1,则是非AVL树
2.结论
1)使二叉树经过处理成为平衡二叉树(AVL)的过程称作平衡化
2)对二叉排序树(BST)进行平衡化得到平衡二叉排序树(BBST)
3)平衡二叉排序树(BBST)具有最佳ASL
3.创建
1)建立平衡二叉排序树的算法从一颗空树开始,通过输入一系列的关键字,逐步建立AVL树
2)在插入新结点时进行平衡旋转,分四种情形:
- LL型->单向右旋
- LR型->先左后右
- RR型->单向左旋
- RL型->先右后左
3)基准点:距离叶结点最近的不平衡结点;旋转点:从基准点出发,按照平衡化处理方法寻找旋转点
三、B-树
1.定义:m阶的B树或为空,或满足:
①每个结点至多有m棵树
②若根结点不是叶子结点,则至少有两棵树
③除根结点之外的所有非终端结点至少有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉ 棵子树
④非终端结点包含以下信息:(n, A 0 A_0 A0, K 1 K_1 K1, A 1 A_1 A1, K 2 K_2 K2, A 2 A_2 A2, K 3 K_3 K3, A 3 A_3 A3,…, K n K_n Kn, A n A_n An) K 1 K_1 K1( ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1≤i≤n)为关键字,且 K 1 K_1 K1< K 2 K_2 K2<…< K n K_n Kn; A i A_i Ai为所指向子树上的指针(0≤i≤n),且 A i − 1 A_{i-1} Ai−1所指子树上的所有关键字均小于 K i K_i Ki, A i A_i Ai所指子树均大于 K i K_i Ki
⑤树中所以叶子结点出现在同一层次上且不带信息: 指针为空,看作失败结点
2.查找
两个过程交替进行:
- 沿指针查找结点
- 在结点的关键字中查找
查找处理结果:
- 若查找成功,则返回指向被查找关键字所在结点的指针和关键字在结点中的位置
- 若查找不成功,则返回插入位置
四、B+树
1.定义
Ⅰ.树中每个非叶结点最多有m颗子树,至少有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉颗子树
Ⅱ.根结点至少有2颗子树
Ⅲ.所有的叶子结点都处在同一个层次上,包含了全部关键字及指向相应数据元素的指针
Ⅳ.叶结点本身按关键字从小到大顺序排列
2.m阶的B+树和B-树的差异
1)有n颗子树的结点中含有n个关键字
2)所有的叶子结点中包含了全部关键字的信息
3)所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中最大(或最小)关键字
3.查找
1)可从根结点索引查找
2)也可从头指针顺序查找
散列表的查找
一、哈希表(散列表也叫哈希表)
1.定义: 一般情况下,记录和存储的位置不存在对应关系,查找依靠关键字值的比较:①顺序查找:等于、不等于 。②折半查找、二叉排序树查找:等于、小于、大于查找的效率取决于和给定值进行比较的关键字个数。
为了避免关键字的比较有一个办法:预先知道所查关键字在表中的位置。即:Loc = f(key)
对应概念: 哈希(hash)——杂凑;哈希函数;哈希表;哈希查找;哈希造表/散列;哈希地址
二、构造哈希函数
- 散列表的长度
- 关键字的长度
- 关键字的分布情况
- 计算散列函数的时间
- 记录的查找频率
遵循原则:
- 哈希函数时压缩映像,不同关键字可能对应同一地址,称之为冲突
- 哈希函数的构造方法很灵活,尽量构造一个“均匀的”函数, 使冲突尽可能少地产生
- 必须构造配套的冲突处理方法
六种构造哈希函数方法:①直接定址法 ②数学分析法 ③平方取中法 ④折叠法 ⑤除留余数法 ⑥随机数法
注:若非数字关键字,则需先对其进行数字化处理
1.直接定址法
哈希函数为关键字的线性函数(这种哈希函数叫做自身函数)
H(key) = key 或者 H(key) = a × key + b (其中a和b为常数)
2.数学分析法
预先知道所有关键字的前提下,选取关键字分布均匀的若干位直接作哈希地址,或迭加后的值作哈希地址(注:此方法仅适合于能预先估计出全体关键字的每一位上各种数字出现的频度。)
3.平方取中法
(1)以关键字的平方值的中间几位作为存储地址
(2)适用于不知全部关键字或取值集中在局部的情形
注: ❶求"关键字的平方值"的目的是"扩大差别" ❷平方值的中间各位又与整个关键字中各位相关
4.折叠法
将关键字等分为几部分,取这几部分的迭加和作哈希地址,有:移位迭加和间界迭加
(注:适用于关键字位数很多且每一位上的数字分布大致均匀的情况)
5.除留余数法
取关键字被某个不大于表长m的整数p除后所得余数作为哈希地址
设哈希函数为:H(key) = key%p (p应为不大于m的质数或是不含20以下的质因子的合数)
注:该方法简单、实用。
6.随机数法
取关键字的随机函数值为其哈希地址
设哈希函数为: H(key) = Random(key) 其中,Random为随机函数
注:适用于关键字长度不等时构造哈希函数
三、处理冲突的方法
(1) "冲突"是指由关键字得到的哈希地址上已存有记录,则“处理冲突”就是为该关键字的记录找到另一个为”空“的哈希地址
(2)在处理冲突的过程中,可能再次发生冲突,由此得到一系列哈希地址 H 1 H_1 H1, H 2 H_2 H2, …, H n H_n Hn
1.开放定址法
三种冲突处理方式:
①线性探测再散列
d i d_i di = 1, 2, … , m-1
注:只要哈希表未满,总能找到一个不冲突的哈希地址
②二次探测再散列
d i d_i di = 1 2 1^2 12, − 1 2 -1^2 −12, 2 2 2^2 22, − 2 2 -2^2 −22, …, − k 2 -k^2 −k2
注:只有在m为形如4j+3的素数时可行。
③随机探测再散列
d i d_i di = 伪随机数序列
2.链地址法
将所有哈希地址相同的记录都链接在同一链表中
四、哈希表的查找
(1)给定K值,由哈希函数求得哈希地址。若该位置为空,则查找失败
(2)若该位置不为空,且关键字值与给定值K相等,则查找成功
(3)若关键字值与给定值K不相等,则根据冲突处理函数找“下一个”位置,直到——为空,或相等。
从查找过程得知,哈希表查找的平均查找长度ASL实际上并不等于零。
五、哈希表的删除
从哈希表中删除记录时要做特殊处理,相应地需要修改查找的算法
总结
(1)线性表的查找
(2)树表的查找
①二叉排序树的查找过程与折半查找类似
②二叉排序树经平衡化处理成为平衡二叉排序树,可使查找性能最好。平衡处理方法有LL型、LR型、RR型和RL型四种
③B-树是一种平衡的多叉查找树,是一种在文件系统中常使用的动态索引技术
④B+树是B-树的变形树,更适合做文件系统的索引
(3)散列表通过关键字值和存储地址之间的联系建立散列表
①散列表的查找长度和记录总数无关,可通过调节填装因子,把平均查找长度控制在所需的范围内
②除留余数法时最常用的构造散列函数的方法
③处理冲突通常有开放地址法和链地址法