查找
前言
查找表也是一种常见的数据结构,是由同一类类型的数据元素构成的集合。查找表中有两个重要的概念:
- 关键字:数据元素中某个数据项的值,用它可以标识一个数据元素。关键字唯一标识称为主关键字;识别若干记录的关键字称为次关键字。
- 查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。
在这一个章节中,查找表有两种:静态查找表和动态查找表。
静态查找表
静态查找表有不同的表示方法,实现查找操作的方法也不同。
顺序表的查找
以顺序表或线性链表彪识静态查找表,可用顺序查找来实现,也就是从头开始遍历,直到遍历全部返回查找失败,或者中途返回查找成功。
顺序表的平均查找长度为:
A
S
L
S
S
=
n
+
1
2
ASL_{SS}=\frac{n+1}{2}
ASLSS=2n+1 查找失败的情况可以忽略;
或
A
S
L
S
S
=
3
(
n
+
1
)
4
ASL_{SS}=\frac{3(n+1)}{4}
ASLSS=43(n+1) 查找失败的情况不可忽略;
有序表的查找
折半查找
- 查找过程:先确定待查记录所在的范围区间,然后逐步缩小范围直到找到或找不到记录为止。
- 平均查找长度: A S L s s = n + 1 n l o g 2 ( n + 1 ) − 1 ASL_{ss}=\frac{n+1}{n}log_2(n+1)-1 ASLss=nn+1log2(n+1)−1
其他类似的查找还有斐波那契查找和插值查找。
- 斐波那契查找:根据斐波那契序列的特点对表进行分割。该查找方式的平均性能比折半查找好,但是最坏结构比折半查找差。优点是分割时只需进行加减运算。
- 插值查找是根据给定值key来确定进行比较的关键字ST.elem[i].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 i=\frac{key-ST.elem[l].key}{ST.elem[h].key-ST.elem[l].key} i=ST.elem[h].key−ST.elem[l].keykey−ST.elem[l].key,其中l和h是有序表中最大关键字和最小关键字的记录。插值查找只适于关键字均匀分布的表,对表常较大的顺序表,平均性能比这般查找好。
静态树的查找
- 最优查找树: 对于一个已经有序的序列 k 0 , k 1 , k − 2 , . . . , k n − 1 { k_0 , k_1 , k-2 , . . . , k_{n − 1 }} k0,k1,k−2,...,kn−1。各关键字对应的权值分别为 w 0 , w 1 , w 2 , . . . , w n − 1 {w_0,w_1,w_2,...,w_{n-1}} w0,w1,w2,...,wn−1,需要构建一颗二叉树使得其带权路径长度 ∑ i = 0 n − 1 w i × h i \sum_{i=0}^{n-1} w_i \times h_i ∑i=0n−1wi×hi最小,其中 h i h_i hi为关键字 k i k_i ki对应的节点在二叉树中所在的层数。该二叉树称为最优查找树。狗仔静态最优查找树花费时间代价搞,因此常使用构造近似最优查找树作为替代。
- 对于一个已经有序的序列
k
0
,
k
1
,
k
−
2
,
.
.
.
,
k
n
−
1
{ k_0 , k_1 , k-2 , . . . , k_{n − 1 }}
k0,k1,k−2,...,kn−1。各关键字对应的权值分别为
w
0
,
w
1
,
w
2
,
.
.
.
,
w
n
−
1
{w_0,w_1,w_2,...,w_{n-1}}
w0,w1,w2,...,wn−1。现在取第i( l < = i < = h ) (l<=i<=h)(l<=i<=h)个记录作为构建的次优二叉查找树的根节点,使得
Δ P = ∣ ∑ j = i + 1 h w j − ∑ j = l i − 1 w j ∣ \Delta P= |\sum_{j=i+1}^{h}w_j-\sum_{j=l}^{i-1}w_j| ΔP=∣∑j=i+1hwj−∑j=li−1wj∣最小,
然后分别对子序列 k l , k l + 1 , k l + 2 , . . . , k i − 1 { k_l , k_{l + 1} , k_{l + 2} , . . . , k_{i − 1} } kl,kl+1,kl+2,...,ki−1和 k i + 1 , k i + 2 , . . . , k h { k_{i+1} , k_{i+ 2} , . . . , k_{h} } ki+1,ki+2,...,kh构造两颗次优查找树并作为根节点的左右子树,递归以上操作就可以得到一颗次优查找树。
为了便于计算 Δ P \Delta P ΔP,引入累计权值和 s w i = ∑ j = l i w j sw_i=\sum_{j=l}^{i}w_j swi=∑j=liwj, 并假设 w l − 1 = 0 w_{l-1}=0 wl−1=0 s w l − 1 = 0 sw_{l-1}=0 swl−1=0其实w ww和s w swsw的索引范围是[ l , h ] [l,h][l,h],加入这额外的两项是为了以下定义的完整性)则有:
两种树的查找源码可参考这里
索引顺序表的查找
可使用分块查找,除表之外,还有一个索引表指出每一块的最大值和起始位置。分块查找分两步进行,先确定待查记录所在的块,然后在块中顺序查找。
分块查找的平均长度为:
A
S
L
b
s
=
1
2
(
n
s
+
s
)
+
1
ASL_{bs}=\frac{1}{2}(\frac{n}{s}+s)+1
ASLbs=21(sn+s)+1。n表示表的长度,s表示每一块的记录
动态查找表
二叉排序树和平衡二叉树
二叉排序树
- 性质:若左子树不空,则左子树上所有的结点的值均小于根节点的值。若右子树不空,则右子树上所有结点的值均大于根节点的值。
- 中序遍历二叉排序树可以得到一个关键字的有序序列。
- 二叉排序树的删除:
如果删除的结点是叶子结点:只要将其双亲结点f原来指向p的指针改为指向空即可
如果删除的结点左子树或者右子树为空,令该结点的子树直接替代删除的结点即可。
如果结点左右子树均不空,则:
第一种方法:令p的左子树为f的左子树,p的右子树为s的左子树,如下图:
第二种方法:令p的直接前驱(或者后驱)替代p,然后再从二叉排列数中山区其直接前驱
平衡二叉树
平衡二叉树是指左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差不超过1.
有关平衡二叉树比较难懂的就是如何将一棵不平衡的树调整为平衡二叉树的过程。调整的类别主要有以下几种:
-
LL型:被破坏节点的左子树的左子树插入破坏节点
直接右旋即可
-
RR型:被破坏节点的右子树的右子树插入破坏节点
直接左旋即可
-
LR型:为被破坏节点的左子树的右子树插入破坏节点
先转为LL型,然后按照LL型的解决方法即可。
-
RL型:被破坏节点的右子树的左子树插入破坏节点
先转为RR型,然后按照RR型的解决方式即可。
B树和B+树
B树
B树和平衡二叉树从根本上说可以是一样的,只不过B树是多路查询,且每一个结点都有个数的要求。B树的定义如下:
- m阶的B树,每个结点最多有m棵子树
- 除根之外的所有非终端结点至少有[m/2]【向上取整】棵子树。
B树中的加入:
首先往叶子节点增加关键字。如果叶子节点超过m,则取中间值放入父节点后,叶子节点裂开,依次类推。
B树中的删除:
如果删除之后不影响结点,则直接删除。如果删除后小于,而右兄弟(左兄弟)大于,则兄弟中最小(最大)的关键字上移到父节点,父节点小于(大于)上移关键字的数值下移合并。
如果左右兄弟都小于,则删除关键字后,所在结点剩余的关键字和双亲结点的关键字一起合并到左(右)兄弟上。
B+树
B+树是B树的一种变形,差异在于有n棵子树的结点中含有n个关键字。所有的叶子结点中包含了全部关键字的信息,所有的非终端结点可以看成是索引部分。
哈希表
哈希表的理解比较简单,难的地方在于哈希函数的构造和冲突的处理。在记录的存储位置和关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置对应。在查找时直接根据f找到对应的记录,这种方法就成为哈希表。
哈希函数的构造方法
直接定址法
取关键字或关键字的某个线性函数值为哈希地址。该方法对于不同的关键字不会发生冲突,但实际上能使用该方法的情况很少。
数字分析法
假设关键字是以r为基的数,且哈希表中可能出现的关键字都是已知的,则可取关键字的若干数位组成哈希地址。
平方取中法
取关键字平方后的中间几位为哈希地址。
折叠法
将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为哈希地址。
除留余数法
取余数。是最简单、也是最常用的构造哈希函数的方法,对模的选取很重要。
随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址。
处理冲突的方法
开放地址法
在原来的哈希函数的值上增加增量,一共有三种增量取法:
线性探测再散列、二次探测再散列和伪随机数序列。
再哈希法
对原来的哈希函数值再重新使用新的哈希函数计算另一个地址,直到冲突不再产生。
链地址法
将所有关键字为同义词的记录存储再同一线性链表中。
建立一个公共溢出区
所有地址冲突的记录都存储到溢出表中。
总结
查找这一个章节有比较多的数据结构是比较复杂的,例如平衡二叉树等。总体来说理解比较难,但是上机题一般集中在哈希表、顺序查找和二叉平衡树中。B树主要用于操作系统的文件中,上机题比较少见。