您现在的位置是:首页 >技术教程 >数据结构与算法·第9章【查找】网站首页技术教程
数据结构与算法·第9章【查找】
概念
关键字:
- 是数据元素(或记录)中某个数据项的值,用以标识(识别)一个数据元素(或记录)。
- 若此关键字可以识别唯一的一个记录,则称之谓“主关键字”。
- 若此关键字能识别若干记录,则称之谓“次关键字”。
查找:
- 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或(记录)。
- 若查找表中存在这样一个记录,则称“查找成功”。查找结果给出整个记录的信息,或指示该记录在查找表中的位置;否则称“查找不成功”。查找结果给出“空记录”或“空指针”。
静态查找表
结构定义:
typedef struct {
ElemType *elem; // 数据元素存储空间基址,建表时按实际长度分配,0号单元留空
int length; // 表的长度
} SSTable;
基本操作:
- Create(&ST,n)
构造一个含n个数据元素的静态查找表ST
- Destroy(&ST)
静态查找表存在,则销毁表ST
- Search(ST,key)
静态查找表ST存在,key为和查找表中元素的关键字类型相同的给定值;若ST中存在其关键字等于key的数据元素,则函数值为该元素的值或在表中的位置,否则为“空”
- Traverse(ST,Visit())
按照某种次序对ST的每个元素调用函数Visit()一次且仅一次
A
S
L
ASL
ASL平均查找长度:
每个关键字的被查找概率(通常都是相等的:
1
/
n
1/n
1/n)乘以查找次数
顺序表的查找
int Search_Seq(SSTable ST, KeyType key) {
// 在顺序表ST中顺序查找其关键字等于key的数据元素。
// 若找到,则函数值为该元素在表中的位置,否则为0。
ST.elem[0].key = key; // “哨兵”,用于简化边界判断
int i;
for (i = ST.length; ST.elem[i].key != key; --i) {
// 从后往前遍历顺序表ST,找到第一个关键字等于key的元素
}
return i; // 返回找到的元素在表中的位置,找不到时返回0
}
A
S
L
=
(
n
+
1
)
/
2
ASL=(n+1)/2
ASL=(n+1)/2
优点:
- 算法简单,且对表的结构无任何要求,无论是用顺序表还是用链表来存放结点,也无论结点之间是否按关键字有序,它同样适用。
缺点:
- 查找效率低,因此,当n较大时不宜采用顺序查找。
有序表的查找
int Search_Bin(SSTable ST, KeyType key) {
// 在有序表ST中折半查找其关键字等于key的数据元素。
// 若找到,则函数值为该元素在表中的位置,否则为0。
int low = 1, high = ST.length, mid;
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; // 顺序表中不存在待查元素
}
二分的思想
二叉排序树
二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
- 它的左、右子树也都分别是二叉排序树。
typedef struct BiTNode {
TElemType data; // 结点数据
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
若二叉排序树为空,则查找不成功;
否则,进行以下步骤:
- 若给定值等于根结点的关键字,则查找成功;
- 若给定值小于根结点的关键字,则继续在左子树上进行查找;
- 若给定值大于根结点的关键字,则继续在右子树上进行查找。
插入算法:
Status InsertBST(BiTree &T, ElemType e) {
// 当二叉排序树中不存在关键字等于 e.key 的数据元素时,
// 插入元素值为 e 的结点,并返回 TRUE;
// 否则,不进行插入并返回 FALSE
BiTree p, s;
if (!SearchBST(T, e.key, NULL, p)) {
// 为新结点分配空间,并初始化结点数据和左右孩子指针
s = (BiTree) malloc(sizeof(BiTNode));
s->data = e;
s->lchild = s->rchild = NULL;
if (!p) {
// 如果找到的位置是空,则插入 s 为新的根结点
T = s;
} else if (LT(e.key, p->data.key)) {
// 如果找到的位置比 e 小,则插入 s 为该位置的左孩子
p->lchild = s;
} else {
// 如果找到的位置比 e 大,则插入 s 为该位置的右孩子
p->rchild = s;
}
return TRUE; // 插入成功
} else {
return FALSE; // 插入失败,已存在相同关键字的结点
}
} // InsertBST
删除算法:
好的,以下是格式更美观的三种删除操作:
1. 删除叶子结点
当要删除的结点是叶子结点时,直接将该结点从二叉排序树中删除即可。
2. 删除只有左子树或只有右子树的结点
当要删除的结点只有左子树或者只有右子树时,让其子树代替它的位置,即将子树与其父节点相连,然后释放被删除结点的内存空间。
3. 删除既有左子树,也有右子树的结点
当要删除的结点既有左子树,也有右子树时,可以选择用其前驱或后继结点代替该结点的位置,然后将被选中的前驱或后继结点从原来的位置移动到要删除的结点位置上,并删除原结点。具体步骤如下:
- 通常选择右子树中最小的结点作为后继结点,或左子树中最大的结点作为前驱结点。以选取前驱结点为例(中序遍历的前驱),具体步骤如下:
- 从被删除结点的左子树开始,沿着右子树一直往下查找,找到最小的结点,并记为p。
在二叉排序树中,一个结点的前驱结点是其左子树上的最右侧结点,也就是比该结点小的所有结点中,键值最大的结点。
- 将p的值赋给被删除结点。
- 删除p所在的结点。
- 从被删除结点的左子树开始,沿着右子树一直往下查找,找到最小的结点,并记为p。
原图
删除后
其中,对于
40
40
40这个节点,如果:
- 右子树为空树则只需重接它的左子树
- 左子树为空树只需重接它的右子树
- 左右子树均不空,仍然找它的前驱节点
平衡二叉树
平衡二叉树又称AVL树,是二叉排序树仍然满足二叉树左子树小,右子树大的特性
的另一种形式。它或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1
结点的平衡因子:
该结点的左子树高度减去它的右子树高度。即:
∣
h
L
−
h
R
∣
|h_L-h_R|
∣hL−hR∣
插入
只用掌握算法思想即可,具体代码考试不作要求
普遍的思路:选取大小在中间的那个点作为根节点
B-树、B+树
B-树
其每个节点可以包含多个关键字和对应的指针。一棵
m
m
m阶的B-树,或为空树,或满足以下特性:
-
所有非叶子结点均至少含有 ⌈ m 2 ⌉ leftlceil frac{m}{2} ight ceil ⌈2m⌉棵子树,至多含有 m m m棵子树;
-
根结点或为叶子结点,或至少含有两棵子树;
-
所有非终端结点
叶子节点
含有下列信息数据: ( n , A 0 , K 1 , A 1 , K 2 , A 2 , … , K n , A n ) (n, A_0, K_1, A_1, K_2, A_2, ldots, K_n, A_n) (n,A0,K1,A1,K2,A2,…,Kn,An),其中:- n n n为关键字的数量;
- 关键字 k i k_i ki( 1 ≤ i ≤ n 1 leq i leq n 1≤i≤n)按照非降序排列, A i A_i Ai为指向子树的指针。
- A i A_i Ai为指向子树根结点的指针,并且指针 A i − 1 A_{i-1} Ai−1所指子树上所有关键字均小于 k i k_i ki,指针 A n A_n An所指子树上所有关键字均大于 k n k_n kn;
-
树中所有叶子结点均不带信息,并且在树中的同一层次上。
查找过程:
在B-树中,搜索操作涉及到从根结点出发,沿着指针搜索结点并在结点内进行顺序(或折半)查找的过程。具体而言,搜索过程如下:
- 从根结点开始,对于每个非终端结点,采用类似二分查找的方式,在结点内部查找关键字所在的位置,并沿着该位置对应的子树继续搜索;
- 重复上述步骤,直至搜索到叶子结点。在叶子结点内部进行顺序(或折半)查找,找到目标关键字时返回指向该结点的指针以及关键字在结点中的位置;
- 如果在叶子结点中没有找到目标关键字,则返回插入位置。
插入过程:
当在B-树中进行查找时,如果未找到目标关键字,则需要进行插入操作。此时,插入位置必定在B-树的最下层非叶子结点上,并可能会引起结点的分裂。具体来说,插入时可能出现以下几种情况:
- 如果插入后,该结点的关键字个数 n < m n<m n<m,则不需要进行结点的分裂。直接将新关键字插入到该结点中;(在B-树中,每个结点都有一个固定的最大关键字数 m m m和一个可变的关键字数 n n n。)
- 如果插入后,该结点的关键字个数 n = m n=m n=m,则需要进行结点的分裂。具体而言,首先令 s = ⌈ m 2 ⌉ s=leftlceilfrac{m}{2} ight ceil s=⌈2m⌉,在原结点中保留 ( A 0 , K 1 , … , K s − 1 , A s − 1 ) (A_0,K_1,ldots,K_{s-1},A_{s-1}) (A0,K1,…,Ks−1,As−1),新建一个结点 ( A s , K s + 1 , … , K n , A n ) (A_s,K_{s+1},ldots,K_n,A_n) (As,Ks+1,…,Kn,An),然后将 ( K s , p ) (K_s,p) (Ks,p)插入到该结点的双亲结点中;
- 如果在插入时,发现双亲结点为空,则需要新建一个根结点,并将原结点和新结点作为该根结点的两个子结点。
光看理论有些抽象,看看具体例子:
首先插入60,没什么好说的
插入90后,节点变成60、80、90,满足第2点。所以拆分,将
K
S
:
80
K_S:80
KS:80插入双亲节点
插入30后,同理
但是,此时根节点n=m,同样需要拆分
最终结果遂如是
删除
当需要从B-树中删除关键字时,需要考虑以下几种情况:
-
如果待删关键字在一个叶子结点中,直接删除即可,并且要保证删除之后该叶子结点中的关键字个数不小于 ⌈ m / 2 ⌉ − 1 lceil m/2 ceil - 1 ⌈m/2⌉−1。
-
如果待删关键字在一个非叶子结点中,可以使用该结点的左子树或右子树中的最大(或最小)关键字替代该关键字。具体而言,如果该关键字在结点 A i A_i Ai中,且其左子树 A i − 1 A_{i-1} Ai−1中的关键字个数不小于 ⌈ m / 2 ⌉ lceil m/2 ceil ⌈m/2⌉,则在 A i A_i Ai中找到比该关键字小的最大关键字 Y Y Y,并将其替代待删关键字 K i K_i Ki,然后在子树 A i − 1 A_{i-1} Ai−1中删除关键字 Y Y Y。如果 A i − 1 A_{i-1} Ai−1中的关键字数小于 ⌈ m / 2 ⌉ − 1 lceil m/2 ceil - 1 ⌈m/2⌉−1,则需要考虑右子树 A i + 1 A_{i+1} Ai+1中的关键字个数。如果 A i + 1 A_{i+1} Ai+1中的关键字数大于 ⌈ m / 2 ⌉ − 1 lceil m/2 ceil -1 ⌈m/2⌉−1,则在 A i A_i Ai中找到比该关键字大的最小关键字 Z Z Z,并将其替代待删关键字 K i K_i Ki,然后在子树 A i + 1 A_{i+1} Ai+1中删除关键字 Z Z Z。如果 A i + 1 A_{i+1} Ai+1中的关键字数也小于 ⌈ m / 2 ⌉ − 1 lceil m/2 ceil -1 ⌈m/2⌉−1,则将 A i A_i Ai与左(或右)兄弟结点合并,并将 A i A_i Ai和父结点中相应的关键字删除。
左子树找个最大的。如果不行,右子树找个最小的
-
每次删除关键字后,需要检查其父结点中的关键字个数是否小于 ⌈ m / 2 ⌉ − 1 lceil m/2 ceil - 1 ⌈m/2⌉−1。如果是,则需要进行结点的合并操作,具体而言,将该结点与其相邻的兄弟结点以及父结点中的关键字进行合并,直到满足B-树的要求为止。特别的,如果需要进行结点的合并操作时,根节点只有一个子节点,则可以将该子节点作为新的根结点。
哈希表
当使用哈希表存储数据时,需要借助哈希函数将关键字映射到一个地址集合上。这个哈希函数的设置是比较灵活的,只要保证地址集合的大小不超出允许范围即可。
然而,由于哈希函数本质上是一种压缩映射,因此在一般情况下容易产生“冲突”现象。也就是说,对于两个不同的关键字 key1 和 key2,它们通过哈希函数 f 映射后可能会得到相同的函数值,即 f(key1) = f(key2),这种情况称为哈希冲突。具有相同函数值的关键字对于该哈希函数来说被称为同义词。
很难不冲突,需要找到解决冲突的方法
哈希表的定义:
在哈希表中,每个关键字值会被映射到唯一的哈希地址,这个哈希地址可以作为该关键字在哈希表中的索引。通过哈希函数的设计和优化,我们可以使得哈希地址的分布更加均匀,从而进一步提高哈希表的查找效率。
除留余数法
也有其他的方法,但是考试主要考察这个方法
H(key) = key MOD p,其中 p 是不大于哈希表长度 m 的素数或不含 20 以下的质因子。
在选择 p 的时候,我们要尽可能地选取与关键字无关的素数,以保证哈希函数的均匀性。同时,为了避免出现哈希冲突,p 的选择也需要注意,通常情况下选取一个大素数会更好。
处理冲突
开放寻址法
为产生冲突的地址 H(key) 求得一个地址序列:
H
0
,
H
1
,
H
2
,
…
,
H
s
。
1
≤
s
≤
m
−
1
H0, H1, H2, …, Hs 。 1 ≤ s ≤ m-1
H0,H1,H2,…,Hs。1≤s≤m−1
其中:
H
0
=
H
(
k
e
y
)
H0 = H(key)
H0=H(key)
H
i
=
(
H
(
k
e
y
)
+
d
i
)
M
O
D
m
,
i
=
1
,
2
,
…
,
s
,
m
Hi = (H(key) + di) MOD m,i = 1, 2, …, s,m
Hi=(H(key)+di)MODm,i=1,2,…,s,m为哈希表表长。
增量 d i di di 有三种取法:
- 线性探测再散列: d i = 1 , 2 , 3 , … , m − 1 di = 1,2,3,…,m-1 di=1,2,3,…,m−1。
- 二次探测再散列: d i = 1 2 , − 1 2 , 2 2 , − 2 2 , … di = 1^2,-1^2,2^2,-2^2,… di=12,−12,22,−22,…。
- 伪随机探测再散列: d i di di 是一组伪随机数列或者 d i = i × H 2 ( k e y ) di=i×H2(key) di=i×H2(key),又称双散列函数探测。
在平方探测时,表长 m 必为形如 4j+3 的素数,例如7、11、19、23等,以避免出现二次聚集现象。在随机探测时,表长 m 和增量 di 之间不应有公因数,以确保产生的地址序列具有完备性。
下方数字代表查找次数
再哈希法
H
i
=
R
H
i
(
k
e
y
)
i
=
1
,
2
,
…
,
k
Hi = RHi (key) i=1, 2, …, k
Hi=RHi(key)i=1,2,…,k
R
H
i
RHi
RHi 均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。这种方法不易产生“聚集”,但增加了计算的时间。
链地址法
查找
如果采用开放定址处理冲突,查找过程如下:
- 对于给定的值K,计算出哈希地址i=H(K)。
- 如果r[i]为NULL,则表示查找不成功。
- 如果r[i].key等于K,则表示查找成功。
- 否则按照所选解决冲突方法,求下一个地址Hi,重复执行步骤3和4,直至r[Hi]为NULL(查找不成功)或者r[Hi].key等于K(查找成功)。
数据结构定义:
int hashsize[] = { 997, ... }; // 哈希表容量递增表,一个合适的素数序列
typedef struct {
ElemType *elem; // 数据元素存储基址,动态分配数组
int count; // 当前数据元素个数
int sizeindex; // hashsize[sizeindex]为当前容量
} HashTable;
#define SUCCESS 1
#define UNSUCCESS 0
#define DUPLICATE -1
查找:
Status SearchHash(HashTable H, KeyType K, int& p, int& c) {
// 在开放定址哈希表H中查找关键码为K的记录
p = Hash(K); // 求得哈希地址
while (H.elem[p].key != NULLKEY && !EQ(K, H.elem[p].key)) { // 该位置有记录并且关键字不相等
collision(p, ++c); // 求得下一探查地址p
}
if (EQ(K, H.elem[p].key)) { // 查找成功,返回待查数据元素位置p
return SUCCESS;
} else { // 查找不成功
return UNSUCCESS;
}
}
插入:
Status InsertHash(HashTable& H, Elemtype e) {
int c = 0;
int p;
if (HashSearch(H, e.key, p, c) == SUCCESS) { // 表中已有与e有相同关键字的元素
return DUPLICATE;
} else {
if (c < hashsize[H.sizeindex] / 2) { // 冲突次数c未达到上限(阀值c可调)
H.elem[p] = e; // 将e插入到哈希表中
++H.count; // 当前数据元素个数加1
return SUCCESS;
} else {
RecreateHashTable(H); // 冲突次数c已达到上限,重建哈希表
}
}
} // InsertHash
习题
判定树
画出对长度为10的有序表进行折半查找的判定树,并求其等概率是查找成功的平均查找长度
判定树的理解
题目简单,就不放答案了,主要是知道判定树的概念
二叉排序树
- 比较简单,按照表中顺序插就行了
(Jan,Feb,……)
- 注意一下,当区间只剩两个数时:类似 [ 1 , 2 ] [1,2] [1,2],第一个数的查找次数是 i i i,第2个数是 i + 1 i+1 i+1(很好理解的)
- 好好看一下,主要是这个平衡二叉树生成
删除
主要是看看删除68后的情况