关键字:
查找:
结构定义:
typedef struct {
ElemType *elem; // 数据元素存储空间基址,建表时按实际长度分配,0号单元留空
int length; // 表的长度
} SSTable;
基本操作:
构造一个含n个数据元素的静态查找表ST
静态查找表存在,则销毁表ST
静态查找表ST存在,key为和查找表中元素的关键字类型相同的给定值;若ST中存在其关键字等于key的数据元素,则函数值为该元素的值或在表中的位置,否则为“空”
按照某种次序对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
优点:
缺点:
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
好的,以下是格式更美观的三种删除操作:
当要删除的结点是叶子结点时,直接将该结点从二叉排序树中删除即可。
当要删除的结点只有左子树或者只有右子树时,让其子树代替它的位置,即将子树与其父节点相连,然后释放被删除结点的内存空间。
当要删除的结点既有左子树,也有右子树时,可以选择用其前驱或后继结点代替该结点的位置,然后将被选中的前驱或后继结点从原来的位置移动到要删除的结点位置上,并删除原结点。具体步骤如下:
在二叉排序树中,一个结点的前驱结点是其左子树上的最右侧结点,也就是比该结点小的所有结点中,键值最大的结点。
平衡二叉树又称AVL树,是二叉排序树仍然满足二叉树左子树小,右子树大的特性
的另一种形式。它或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1
结点的平衡因子:
该结点的左子树高度减去它的右子树高度。即: ∣ h L − h R ∣ |h_L-h_R| ∣hL−hR∣
只用掌握算法思想即可,具体代码考试不作要求
其每个节点可以包含多个关键字和对应的指针。一棵 m m m阶的B-树,或为空树,或满足以下特性:
所有非叶子结点均至少含有 ⌈ m 2 ⌉ \left\lceil \frac{m}{2} \right\rceil ⌈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),其中:
树中所有叶子结点均不带信息,并且在树中的同一层次上。
在B-树中,搜索操作涉及到从根结点出发,沿着指针搜索结点并在结点内进行顺序(或折半)查找的过程。具体而言,搜索过程如下:
插入过程:
当在B-树中进行查找时,如果未找到目标关键字,则需要进行插入操作。此时,插入位置必定在B-树的最下层非叶子结点上,并可能会引起结点的分裂。具体来说,插入时可能出现以下几种情况:
光看理论有些抽象,看看具体例子:
首先插入60,没什么好说的
插入90后,节点变成60、80、90,满足第2点。所以拆分,将 K S : 80 K_S:80 KS:80插入双亲节点
插入30后,同理
但是,此时根节点n=m,同样需要拆分
最终结果遂如是
当需要从B-树中删除关键字时,需要考虑以下几种情况:
如果待删关键字在一个叶子结点中,直接删除即可,并且要保证删除之后该叶子结点中的关键字个数不小于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil - 1 ⌈m/2⌉−1。
如果待删关键字在一个非叶子结点中,可以使用该结点的左子树或右子树中的最大(或最小)关键字替代该关键字。具体而言,如果该关键字在结点 A i A_i Ai中,且其左子树 A i − 1 A_{i-1} Ai−1中的关键字个数不小于 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈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\rceil - 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\rceil -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\rceil -1 ⌈m/2⌉−1,则将 A i A_i Ai与左(或右)兄弟结点合并,并将 A i A_i Ai和父结点中相应的关键字删除。左子树找个最大的。如果不行,右子树找个最小的
每次删除关键字后,需要检查其父结点中的关键字个数是否小于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil - 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 有三种取法:
在平方探测时,表长 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 均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。这种方法不易产生“聚集”,但增加了计算的时间。
如果采用开放定址处理冲突,查找过程如下:
数据结构定义:
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,……)