王卓数据结构与算法之查找算法
Posted 生命是有光的
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了王卓数据结构与算法之查找算法相关的知识,希望对你有一定的参考价值。
✍、目录脑图
1、查找
1.1、查找的分类
静态查找表:仅作"查询"操作的查找表。
动态查找表:作"插入"和"删除"操作的查找表。
1.2、查找算法的评价指标
查找算法的评价指标是关键字的平均比较次数,也称平均查找长度ASL(Average Search Length)。
我们也可以用下面的关键字比较次数的期望值。
A
S
L
=
∑
i
=
1
n
p
i
c
i
ASL = \\sum_{i=1}^{n}{p_ic_i}
ASL=i=1∑npici
n表示记录的个数,pi表示查找第i个记录的概率(通常认为pi=1/n),ci表示找到第i个记录所需的比较次数。
1.3、线性表的查找
1.3.1、顺序查找(线性查找)
应用范围:
- 顺序表或线性链表表示的静态查找表
- 表内元素之间无序
数据元素类型定义:
typedef struct{
KeyType key; //关键字域
.... // 其他域
}ElemType;
// 顺序表结构类型定义
typedef struct{
ElemType *R; // 表基址
int length; // 表长
}SSTable;
SSTable ST; // 定义顺序表ST
// 方式一
int Search_Seq(SSTable ST,KeyType key){
// 若成功返回其位置信息,否则返回0
for(i=ST.length;i≥1;--i){
if(ST.R[i].key == key){
return i'
}
}
return 0;
}
// 方式二
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;
}
}
上述每执行一次循环都要进行两次比较,我们还可以改进:把待查关键字key存入表头(“哨兵”),从后向前挨个比较,可免去查找过程中每一步都要检测是否查找完毕,加快速度。
int Search_Seq(SSTable ST,KeyType key){
ST.R[0].key = key;
for(i=ST.length;ST.R[i].key!=key;--i);
return i;
}
当ST.Length较大时,此改进能使进行一次查找所需的平均时间几乎减少一半。
1.3.1.1、顺序查找性能分析
顺序查找比较次数与key位置有关:
- 查找第i个元素,需要比较
n-i+1
次 - 查找失败,需要比较
n+1
次
时间复杂度:查找成功时的平均查找长度,设表中各记录查找概率相等:ASL = (1+2+....+n)/n = (n+1)/2
空间复杂度:一个辅助空间------O(1)
1.3.1.2、顺序查找的特点
优点:算法简单,逻辑次序无要求,且不同存储结构均适用
缺点:ASL太长,时间效率太低
1.3.2、折半查找(二分查找)
折半查找:每次将待查记录所在区间缩小一半。
-
mid = (low+high)/2
-
key < mid 则 high = mid - 1
-
key > mid 则 low = mid + 1
-
key == mid,找到
例如上述要查找21,
mid = (low+high)/2 = (1+11)/2 = 6, 21 < 56,high = mid - 1 = 5
mid = (1+5)/2 = 3,21 > 19, low = mid + 1 = 4
mid = (4+5)/2 = 4,21 = 21
我们再来看一个例子:
例如上述要查找63,
mid = (low+high)/2 = (1+11)/2 = 6, 63 > 56,low = mid + 1 = 7
mid = (7+11)/2 = 9,63 < 80, high = mid - 1 = 8
mid = (7+8)/2 = 7, 63 < 64,high = mid - 1 = 6
high < low,结束,查找失败。
折半查找算法实现:
非递归算法:
int Search_Bin(SSTable ST,KeyType key){
low = 1;high = ST.length; //置区间初值
while(low <= high){
mid = (low+high)/2;
if(ST.R[mid].key == key){
return mid; // 找到待查元素
}else if(key < ST.R[mid].key){ // 缩小查找区间
high = mid - 1; //继续在前半区间进行查找
}else{
low = mid + 1; //继续在后半区间进行查找
}
}
return 0 ; //顺序表中不存在待查元素
}
递归算法:
int Search_Bin(SSTable ST,KeyType key,int low,int high){
if(low>high){
return 0 ; // 查找不到时返回0
}
mid = (low+high)/2;
if(key == ST.elem[mid].key){
return mid;
}else if(key<ST.elem[mid].key){
// 递归,在前半区间进行查找
}else{
// 递归,在后半区间进行查找
}
}
1.3.2.1、折半查找性能分析
我们将其画成判定树,比较次数为1次的在第一层,比较次数为2次的在第二层…
查找成功:
- 比较次数 = 路径上的结点数
- 例如 4 号位置,比较此为为路径上的结点数为3
- 比较次数 = 结点的层数
- 比较次数 <= 树的深度 = [log2n] + 1
查找失败:
- 比较次数 = 路径上内部结点数
我们来看一个例子:
平均查找长度ASL(成功时):
设表长 n=2h-1,则 h=log2(n+1)(此时,判定树为深度 = h 的满二叉树),且表中每个记录的查找概率相等: Pi = 1/n
ASL ≈ log2(n+1) - 1 (n>50)
1.3.2.2、折半查找的优缺点
折半查找优点:效率比顺序查找高
折半查找缺点:只适用于有序表,且限于顺序存储结构(对线性链表无效)
1.3.3、分块查找(索引顺序查找)
条件:
- 将表分成几块,且表或者有序,或者分块有序。若 i < j,则第 j 块中所有记录的关键字均大于第 i 块中的最大关键字
- 建立"索引表"(每个结点含有最大关键字域和指向本块第一个结点的指针,且按关键字有序)
查找过程:先确定待查记录所在块(顺序或折半查找),再在块内查找(顺序查找)。
1.3.3.1、分块查找性能分析
查找效率 ASL = Lb(对索引表查找的ASL) + Lw(对块内查找的ASL)
ASL ≈ log2(n/s+1) + s/2 [S为每块内部的记录个数,n/s 即块的数目]
1.3.3.2、分块查找优缺点
优点:插入和删除都比较容易,无需进行大量移动
缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。
适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。
1.3.4、查找方法比较
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序表、无序表 | 有序表 | 分块有序 |
存储结构 | 顺序表、线性链表 | 顺序表 | 顺序表、线性链表 |
1.4、树表的查找
当表插入、删除操作频繁时,为维护表的有序性,需要移动表中很多记录。我们改用动态查找表。
对于给定值key,若表中存在,则成功返回;否则,插入关键字key的记录
1.4.1、二叉排序树
二叉排序树又称为二叉搜索树、二叉查找树。
定义:
二叉排序树或是空树,或是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的值均小于根节点的值。
- 若其右子树非空,则右子树上所有结点的值大于等于根节点的值。
- 其左右子树本身又是一棵二叉排序树。
1.4.2、二叉排序树的性质
中序遍历非空的二叉排序树所得到的数据元素序列是一个按关键字排列的递增有序序列。
1.4.3、二叉排序树的存储结构
typedef struct{
KeyType key; //关键字项
InfoType otherinfo; //其他数据域
}ElemType;
typedef struct BSTNode{
ElemType data; // 数据域
struct BSTNode *lchild,*rchild; // 左右孩子指针
}
BSTree T; // 定义二叉排序树T
1.4.3、二叉排序树的操作
1.4.3.1、查找
- 若查找的关键字等于根节点,成功
- 否则
- 若小于根节点,查其左子树
- 若大于根节点,查其左子树
- 在左右子树上的操作类似
算法思想:
- 若二叉排序树为空,则查找失败,返回空指针
- 若二叉排序树非空,将给定值key与根节点的关键字 T->data.key 进行比较:
- 若 key 等于 T->data.key,则查找成功,返回根节点地址
- 若 key 小于 T->data.key,则进一步查找左子树。
- 若 key 大于 T->data.key,则进一步查找右子树。
BSTree SearchBST(BSTree T,KeyType key){
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); //在右子树中继续查找
}
}
1.4.3.2、查找分析
二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径。
比较的关键字次序 = 此结点所在层次数
最多的比较次数 = 树的深度
二叉排序树的平均查找长度:
含有n个结点的二叉排序树的平均查找长度和树的形态有关。
问题:如何提高形态不均衡的二叉排序树的查找效率?
解决办法:做"平衡化"处理,即尽量让二叉树的形态均衡!
1.4.3.3、插入
若二叉排序树为空,则插入结点作为根结点插入到空树中。
否则,继续在其左、右子树上查找
- 树中已有,不再插入
- 树中没有
- 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子
插入的元素一定在叶结点上
1.4.3.4、生成
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树。
一个无序序列可通过构造二叉排序树而变成一个有序序列,构造树的过程就是对无序序列进行排序的过程。
插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录。
关键字的输入顺序不同,建立的二叉排序树不同
1.4.3.5、删除
从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。
由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。
- 将因删除结点而断开的二叉链表重新链接起来
- 防止重新链接后树的高度增加
- 被删除的结点是叶子结点:直接删去该结点
- 被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换它(结点替换)
- 被删除的结点既有左子树,也有右子树
一种方法是以其序前驱替换之(值替换),然后再删除该前驱结点。(前驱是左子树中最大的结点)
也可以用其后继替换之,然后再删除该后继结点。(后继是右子树中最小的结点)
1.4.4、平衡二叉树
平衡二叉树
- 又称AVL树
- 一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:
- 左子树和右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉排序树
为了方便起见,给每个结点附加一个数字,给出该结点左子树与右子树的高度差。这个数字称为结点的平衡因子
平衡因子 = 结点左子树的高度 - 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是 -1、0、1
对于一棵有 n 个结点的AVL树,其高度保持在 O(log2n)数量级,ASL也保持在 O(log2n)量级
1.4.4.1、平衡调整方法
- LL型:C<B<A,所以我们调整之后也得保持C<B<A
- LR型:B<C<A,所以我们调整之后也得保持B<C<A
1.4.4.2、LL型
- B结点带左子树α一起上升
- A结点成为B的右孩子
- 原来B结点的右子树β作为A的左子树
1.4.4.2、RR型
- B结点带右子树β一起上升
- A结点成为B的左孩子
- 原来B结点的左子树α作为A的右子树
例如:
1.4.4.3、LR型
- C结点穿过A、B结点上升
- B结点成为C的左孩子,A结点成为C的右孩子
- 原来C结点的左子树β作为B的右子树,原来C结点的右子树γ作为A的左子树
例如:
1.4.4.4、RL型
1.5、散列表的查找
基本思想:记录的存储位置与关键字之间存在对应关系,对应关系-hash函数
优点:查找效率高
缺点:空间效率低
散列方法:选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放。查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键码进行对比,确定查找是否成功。
散列函数:散列方法中使用的转换函数
冲突:不同的关键码映射到同一个散列地址:key1 ≠ key2,但是H(key1) = H(key2)
1.5.1、直接定址法
H a s h ( k e y ) = a × k e y + b ( a 、 b 为 常 数 ) Hash(key) = a×key + b (a、b为常数) Hash(key)=a×key+b(a、b为常数)
优点:以关键码key的某个线性函数值为散列地址,不会产生冲突。
缺点:要占用连续地址空间,空间效率低
1.5.2、除留余数法
H a s h ( k e y ) = k e y m o d p ( p 是 一 个 整 数 ) Hash(key) = key mod p (p是一个整数) Hash(key)=keymodp(p是一个整数)
关键:如何选取合适的p?
技巧:设表长为 m, 取 p≤m 且为质数
1.5.3、处理冲突的方法
1.5.3.1、开放定址法(开地址法)
基本思想:有冲突时就去寻找下一个空的单列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入。
例如:除留余数法: Hi =(Hash(key)+di) mod m di为增量序列
常用方法:
- 线性探测法 di 为 1、2、…3、m-1 线性序列
- 二次探测法 di为 12 、 -12、22、-22、…,q2 二次序列
- 伪随机探测法 di为伪随机数序列
- 线性探测法
例:关键码集为{47、7、29、11、16、92、22、8、3},散列表表长为 11,散列函数为 Hash(key)=key mod 11
,请用线性探测法解决冲突。
Hash(key) | 运算次数 |
---|---|
47%11 = 3 | 1 |
7%11 = 7 | 1 |
29%11 = 7,发生冲突,(7+1)%11 = 8 | 2 |
11%11 = 0 | 1 |
16%11 = 5 | 1 |
92%11 = 4 | 1 |
22%11 = 0,发生冲突,(0+1)%11 = 1 | 2 |
8%11 = 8,发生冲突,(8+1)%11 = 9 | 1 |
3%11 = 3,发生冲突。(3+1)%11 = 4,发生冲突。(3+2)%11 = 5,发生冲突。(3+3)%11 = 6 | 4 |
平均查找长度ASL =(1+2+1+1+1+4+1+2+2)/9 = 1.67
- 二次探测法
例:关键码集为{47、7、29、11、16、92、22、8、3},设散列函数为 Hash(key)=key mod 11
,Hi=(Hash(key)+di) mod m。其中:m为散列表长度,m要求是某个 4k+3 的质数,di为增量序列 12 、 -12、22、-22、…,q2
Hash(3)=3,散列地址冲突,由 H1 = (Hash(3)+12) mod 11 = 4,依然冲突,H2 = (Hash(3)-12) mod 11 = 2,找到空的散列地址,存入。
1.5.3.2、链地址法(拉链法)
基本思想:相同散列地址的记录链成一单链表,m个散列地址就设 m 个单链表,然后用数组将 m 个单链表的表头指针存储起来,形成一个动态的结构。
19%13 = 7
14%13 = 1
23%13 = 10
1%13 = 1
68%13 = 3
…
我们计算散列地址,地址相同的放在同样的单链表上,散列表的个数由 key mod m 中的m来决定,上图中m为13,得到的余数为 0~12 ,所有有13个单链表。
链地址法建立散列表步骤:
- 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行步骤2解决冲突
- 根据选择的冲突处理方法,计算关键字key的下一个存储地址,若该地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入此链表。
链地址法优点:
- 非同义词不会冲突,无"聚集"现象
- 链表上结点空间动态申请,更适合于表长不确定的情况。
1.5.3.3、散列表查找性能分析
例题:已知一组关键字(19,14,23,1,68,20,84,27,55,11,10,79),散列函数为:H(key)=key mod 13,散列表长为 m=16,设每个记录的查找概率相等。
散列表长为16
- 使用线性探测再散列处理冲突
H(key) | 比较次数 |
---|---|
H(19) = 19%13 = 6 | 1 |
H(14) = 14%13 = 1 | 1 |
H(23) = 23%13 = 10 | 1 |
H(1) = 1%13 = 1,冲突。H(1) = (1+1)%13 = 2 | 2 |
H(68) = 68%13 = 3 | 1 |
H(20) = 20%13 = 7 | 1 |
H(84) = 84%13 = 6,冲突。H(84) = (6+1)%13 = 7,冲突。H(84) = (6+2)%13 = 8 | 3 |
H(27) = 27%13 = 1,冲突。H(27) = (1+1)%13 = 2,冲突。H(27) = (1+2)%13 = 3,冲突。 H(27) = (1+3)%13 = 4,冲突。 | 4 |
平均查找长度ASL = (1*6+2+ 3 * 3+4+9)/12 = 2.5
- 使用链地址法处理冲突
使用平均查找长度ASL来衡量查找算法,ASL取决于:
- 散列函数
- 处理冲突的方法
- 散列表的装填因子α
α = 表 中 填 入 的 记 录 数 哈 希 表 的 长 度 α = \\frac{表中填入的记录数}{哈希表的长度} α=哈希表的长度表中填入的记录数
α越大,表中记录越多,说明表装的越满,发生冲突的可能性就越大,查找时比较次数就越多
1.5.3.4、几点结论
- 散列表技术具有很好的平均性能,优于一些传统的技术
- 链地址法优于开地址法
- 除留余数法做散列函数优于其他类型函数
以上是关于王卓数据结构与算法之查找算法的主要内容,如果未能解决你的问题,请参考以下文章
《数据结构与算法》---(哔哩哔哩--王卓老师笔记)史上最详细笔记
《数据结构与算法》---(哔哩哔哩-王卓老师笔记)--数据结构
《数据结构与算法》---(哔哩哔哩-王卓老师笔记)---线性表类型定义