二叉排序树 Binary Sort Tree
定义
- 二叉排序树或是一棵空树,或是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于等于它的根结点的值
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 左、右子树本身又各是一棵二叉排序树
- 因此,中序遍历二叉排序树可得到关键字的升序序列
查找过程
- 查找过程:若二叉排序树为空,则查找失败;否则,
- (1)若给定值等于根结点的关键字,则查找成功;
- (2)若给定值小于根结点的关键字,则继续在左子树上进行查找;
- (3)若给定值大于根结点的关键字,则继续在右子树上进行查找
// 在二叉搜索树中查找元素
pBiTree_t BST_find(pBiTree_t root, TreeElementType_t data)
{
pBiTree_t p = root;
while (p)
{
if (p->data > data)
{
p = p->L;
}
else if (p->data < data)
{
p = p->R;
}
else {
break;
}
}
return p;
}
查找最大、最小值
pBiTree_t BST_findMin(pBiTree_t root)
{
pBiTree_t p = root;
if (!root)
{
return NULL;
}
while (p->L)
{
p = p->L;
}
return p;
}
pBiTree_t BST_findMax(pBiTree_t root)
{
pBiTree_t p = root;
if (!root)
{
return NULL;
}
while (p->R)
{
p = p->R;
}
return p;
}
插入元素
- 若二叉排序树为空,则插入结点应为新的根结点;否则,从根结点开始查找,直到某结点的左子树或右子树为空为止,将新结点作为叶子结点插入
- 二叉排序树生成:从空树出发,反复进行插入操作
pBiTree_t BST_insert(pBiTree_t root, TreeElementType_t data)
{
if (!root)
{
root = (pBiTree_t)malloc(sizeof(BiTree_t));
if (!root)
{
return NULL;
}
root->data = data;
root->L = NULL;
root->R = NULL;
}
else {
if (root->data < data)
{
root->L = BST_insert(root->L, data);
}
else if (root->data > data)
{
root->R = BST_insert(root->R, data);
}
}
return root;
}
删除元素
- 删除后保持中序遍历序列不变即可,因此有多种删除方法
- 要删除二叉排序树中的
p
结点,分三种情况:- (1)
p
为叶子结点:直接删除,再将父结点指针置空 - (2)
p
只有左子树或右子树p
只有左子树,用p
的左孩子代替p
p
只有右子树,用p
的右孩子代替p
- (3)
p
左、右子树均非空:用左子树最大元素(前驱)或右子树最小元素(后继)来代替删除的结点 (这两个结点最多都只有1个子结点,因此可用上一种方案来删除这个被选择的结点)
- (1)
pBiTree_t BST_delete(pBiTree_t root, TreeElementType_t data)
{
pBiTree_t p;
if (!root)
{
return root;
}
if (data < root->data)
{
root->L = BST_delete(root->L, data);
}
else if (data > root->data)
{
root->R = BST_delete(root->R, data);
}
else {
if (root->L && root->R)
{
p = BST_findMax(root->L);
root->data = p->data;
root->L = BST_delete(root->L, root->data);
}
else {
if (!(root->L))
{
p = root;
root = root->R;
}
else {
p = root;
root = root->L;
}
free(p);
}
}
return root;
}
- 总是优先删除前驱(或者后继)容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。解决这一问题的办法有两种: 一种是每次交替删除前驱或后继; 另一种是记录子树高度, 总是优先在高度较高的一棵子树里删除结点
性能分析
- 同一组关键字能够构造出多棵不同形态的二叉排序树,其平均查找长度的值各不相同,甚至可能差别很大
- 最好情况是:二叉排序树形态与折半查找判定树相同,此时 A S L ASLASL 与 log 2 n \log_2nlog2n 成正比
A S L = ( 1 + 2 × 2 + 3 × 2 ) / 5 = 2.2 → O ( l o g n ) \begin{aligned}ASL&=(1+2\times2+3\times2)/5=2.2\\&\rightarrow O(logn)\end{aligned}ASL=(1+2×2+3×2)/5=2.2→O(logn)
A S L = ( 1 + 2 + 3 + 4 + 5 ) / 5 = 3 → O ( n ) \begin{aligned}ASL&=(1+2+3+4+5)/5=3\\&\rightarrow O(n)\end{aligned}ASL=(1+2+3+4+5)/5=3→O(n)
平衡二叉树 Balanced Binary Tree (AVL树)
定义
- 二叉排序树或是一棵空树,或是具有下列性质的二叉树:
- 它的左、右子树深度之差的绝对值不大于1
- 它的左、右子树都是平衡二叉树
- 平衡因子 BF (Balance Factor):
B F = h L − h R BF=h_L-h_RBF=hL−hR
平衡二叉树的插入
- 每当 AVL 树中插入一个新结点,插入路径上各结点的 BF 都要自底向上重新计算以判断树是否平衡。因此 AVL 树结点还要附加信息:以该结点为根的子树高度
struct node {
int v, height; // height 为当前子树高度
node *lchild, *rchild;
};
// 获取以 root 为根结点的子树的当前 height
int getHeight(node* root} {
if(root == NULL)
return 0;
return root->height;
}
// 计算结点 root 的平衡因子
int getBalanceFactor(node* root) {
return getHeight(root->lchild) - getHeight(root->rchild);
}
// 更新结点 root 的 height
void updateHeight(node* root) {
root->height = max(getHeight(root->lchild), getHeight(root->rchild)) + 1;
}
RR 型不平衡 (破坏者在被破坏者的右子树的右子树上)
左单旋
- A AA 不一定是根结点,它是距离产生问题结点最近的且 ∣ B F ∣ > 1 |BF|>1∣BF∣>1 的结点
void Single_left_rotation(node* &root)
{
node* temp = root->rchild;
root->rchild = temp->lchild;
temp->lchild = root;
updateHeight(root); // 更新结点 A 的高度
updateHeight(temp); // 更新结点 B 的高度
root = temp;
}
LL 型不平衡 (破坏者在被破坏者的左子树的左子树上)
右单旋
void Single_right_rotation(node* &root)
{
node* temp = root->lchild;
root->lchild = temp->rchild;
temp->rchild = root;
updateHeight(root); // 更新结点 A 的高度
updateHeight(temp); // 更新结点 B 的高度
root = temp;
}
LR 型不平衡
先左旋后右旋
- 先对以 B BB 为根结点的子树左单旋,再对以 A AA 为根结点的子树右单旋
void Double_left_right_rotation(node* &root)
{
Single_left_rotation(root->L);
Single_right_rotation(root);
}
RL 型不平衡
先右旋后左旋
- 先对以 B BB 为根结点的子树右单旋,再对以 A AA 为根结点的子树左单旋
void Double_right_left_rotation(node* &root)
{
Single_right_rotation(root->R);
Single_left_rotation(root);
}
插入元素
// 需要从插入的结点开始从下往上判断结点是否失衡,因此每个 insert 函数后
// 都要更新当前子树的高度,并判断是哪种不平衡类型
void insert(node* &root, int v)
{
if(!root)
{
root = new node;
root->v = v;
root->height = 1;
root->lchild = root->rchild = nullptr;
return;
}
if(v < root->v)
{
insert(root->lchild, v);
updateHeight(root); // 更新树高
if(getBalanceFactor(root) == 2) {
if(getBalanceFactor(root->lchild) == 1) (
Single_right_rotation(root); // LL 型
} else if(getBalanceFactor(root->lchild) == -1) (
Double_left_right_rotation(root); // LR 型
}
}
} else {
insert(root->rchild, v);
updateHeight(root);
if(getBalanceFactor(root) == -2) {
if(getBalanceFactor(root->rchild) == -1)
Single_left_rotation(root); // RR 型
} else if(getBalanceFactor(root->rchild) == 1) {
Double_right_left_rotation(root); // RL 型
}
}
}
}
性能分析
- AVL 树的插入、删除、查找均可在 O ( log n ) O(\log n)O(logn) 时间内完成
平衡二叉树的 ASL 总能保持 O ( log 2 n ) O(\log_2n)O(log2n)
- 在平衡树上进行查找,和给定值进行比较的关键字的个数不超过平衡树的深度。因此要求得含 n nn 个关键字的二叉平衡树可能达到的最大深度
- 为了解答上述问题,可以先分析深度为 h hh 的二叉平衡树中所含结点的最小值 N h N_hNh
N 0 = 0 , N 1 = 1 , N 2 = 2 , N 3 = 4 , N 4 = 7... N_0=0,N_1=1,N_2=2,N_3=4,N_4=7...N0=0,N1=1,N2=2,N3=4,N4=7...一般情况下,N h = N h − 1 + N h − 2 + 1 N_h=N_{h-1}+N_{h-2}+1Nh=Nh−1+Nh−2+1,这个关系与斐波那契数列相似. 利用归纳法可证:N h = F h + 2 − 1 N_h=F_{h+2}-1Nh=Fh+2−1。而 F h ≈ ϕ h / 5 F_h\approx \phi^h/\sqrt 5Fh≈ϕh/5,其中 ϕ = 1 + 5 2 \phi=\frac{1+\sqrt 5}{2}ϕ=21+5, 因此 N h ≈ ϕ h + 2 / 5 − 1 N_h\approx \phi^{h+2}/\sqrt 5-1Nh≈ϕh+2/5−1 - 由此可得,含 n nn 个关键字的二叉平衡树可能达到的最大深度为 l o g ϕ ( 5 ( n + 1 ) ) − 2 log_{\phi}(\sqrt 5(n+1))-2logϕ(5(n+1))−2,在平衡树上进行查找的时间复杂度为 O ( l o g n ) O(logn)O(logn)
B-树
定义
B-树是一种 平衡 的 多路 查找 树
m mm阶B-树,或为空树,或为满足下列特性的m mm叉树:
- 每个结点至多有m mm棵子树( 每个结点至多有m − 1 m-1m−1个关键字)
- 若根结点不是叶结点,则至少有2棵子树
- 除根之外的所有非叶结点至少有⌈ m / 2 ⌉ \lceil m/2\rceil⌈m/2⌉棵子树
- 所有非叶子结点包含信息:n , p 0 , k 1 , p 1 , k 2 , … , k n , p n n,p_0,k_1,p_1,k_2,…,k_n,p_nn,p0,k1,p1,k2,…,kn,pn(n nn为关键字个数,p pp为指向子树的指针,k kk为关键字)
特性:
- 树中所有叶子结点均不带信息,且在树中的同一层次上
- 非叶结点中的多个关键字均自小至大有序排列,即:K 1 < K 2 < … < K n K_1< K_2 < … < K_nK1<K2<…<Kn
- p i − 1 p_{i-1}pi−1 所指子树上所有关键字均小于k i k_iki
- p i p_ipi 所指子树上所有关键字均大于k i k_iki
实际上在B-树中的每个结点上还应包含n个指向关键字的记录的指针。
比如存储学生信息:B-树上的关键字为学生学号,而另外存的关键字记录的指针就指向每个学生的信息
查找过程
B-树的查找是沿指针搜索结点和在结点内部顺序(或折半)查找两个过程交替进行。
- 查找成功,返回结点指针及在结点中的位置
- 查找失败,则返回插入位置
*C语言描述
#define m 3 //B-树的阶数
typedef struct BTNode {
int keynum; // 结点中关键字个数,结点大小 n
struct BTNode *parent; // 指向双亲结点的指针
KeyType key[m+1]; // 关键字(0号单元不用) 多定义一个单元是为了之后的插入 k1,k2...
struct BTNode *ptr[m+1]; // 子树指针向量(0号单元不用)p0,p1...
Record *recptr[m+1]; // 记录指针向量(0号单元不用)
} BTNode, *BTree;
typedef struct {
BTNode *pt; // 指向找到的结点的指针
int i; // 1..m-1,在结点中的关键字序号
int tag; // 标志查找成功(=1)或失败(=0)
} Result; // 在B树的查找结果类型
Result SearchBTree(BTree T, KeyType K) {
// 在m 阶的B-树 T 中查找关键字 K, 返回查找结果 (pt, i, tag)。若查找成功,则
// 特征值 tag=1, 指针 pt 所指结点中第 i 个关键字等于 K; 否则特征值 tag=0, 等于
// K 的关键字应插入在指针 pt 所指结点 中第 i 个关键字和第 i+1个关键字之间
p=T; q=NULL; found=FALSE; i=0;
while (p && !found) {
n=p->keynum; i=Search(p, K); // 在p->key[1..keynum]中查找 i , p->key[i]<=K<p->key[i+1]
if (i>0 && p->key[i]==K) found=TRUE;
else { q=p; p=p->ptr[i]; } // q 指示 p 的双亲
}
if (found) return (p,i,1); // 查找成功
else return (q,i,0); // 查找不成功
}
查找性能的分析
B-树一般存储在磁盘上,查找的过程为在磁盘上找到要搜索的结点后,将结点中的信息读入内存,再利用顺序/折半查找查询关键字。因此查找时间主要花费在搜索结点(访问外存)上,即主要取决于B-树的深度
考虑最坏情况:含 N NN 个关键字的 m mm 阶 B-树可能达到的最大深度 H HH 为多少?
反过来问: 深度为H HH的B-树中,至少含有多少个结点?
- 第1层:1 11
- 第2层:2 22
- 第3层:2 × ⌈ m / 2 ⌉ 2\times \lceil m/2\rceil2×⌈m/2⌉
- 第4层:2 × ⌈ m / 2 ⌉ 2 2\times \lceil m/2\rceil^22×⌈m/2⌉2
… - 第H+1层:2 × ⌈ m / 2 ⌉ H − 1 2\times \lceil m/2\rceil^{H-1}2×⌈m/2⌉H−1
第 H + 1 H+1H+1 层为叶子结点,而当前树中含有 N NN 个关键字,则叶子结点必为 N + 1 N+1N+1 个
∴ N + 1 ≥ 2 × ⌈ m / 2 ⌉ H − 1 \therefore N+1 \geq 2\times \lceil m/2\rceil^{H-1}∴N+1≥2×⌈m/2⌉H−1∴ H ≤ l o g ⌈ m / 2 ⌉ ( N + 1 2 ) + 1 \therefore H \leq log_{\lceil m/2\rceil}(\frac{N+1}{2})+1∴H≤log⌈m/2⌉(2N+1)+1
因此,在含 N 个关键字的 B-树上进行一次查找,需访问的结点个数不超过
l o g ⌈ m / 2 ⌉ ( N + 1 2 ) + 1 log_{\lceil m/2\rceil}(\frac{N+1}{2})+1log⌈m/2⌉(2N+1)+1
插入
在查找不成功之后,需进行插入。在最下层的非叶结点处插入
- 插入后,该结点的关键字个数n < m n<mn<m,不修改指针
- 插入后,该结点的关键字个数 n = m n=mn=m,则需进行“结点分裂”,令 s = ⌈ m / 2 ⌉ s = \lceil m/2\rceils=⌈m/2⌉,在原结点中保留( A 0 , K 1 , … … , K s − 1 , A s − 1 ) (A0,K1,…… , Ks-1,As-1)(A0,K1,……,Ks−1,As−1);建新结点( A s , K s + 1 , … … , K n , A n ) (As,Ks+1,…… ,Kn,An)(As,Ks+1,……,Kn,An);将( K s , p ) (Ks,p)(Ks,p)插入双亲结点
- 若双亲为空,则建新的根结点
3阶B-树:
插入60:
插入90:
插入30:
删除
- 假如删除非终端结点中的k i k_iki,则可以用指针p i p_ipi所指子树中的最小关键字Y YY代替k i k_iki,然后在相应结点中删除Y YY,例如在下图中删除45,可以用50代替45,然后删除50.因此下面可以只讨论删除最下层非叶结点中关键字的情形
- 删除之后,结点中关键字的个数≥ ⌈ m / 2 ⌉ − 1 \geq\lceil m/2\rceil-1≥⌈m/2⌉−1,则只需删去该关键字和相应指针
- 否则,要从其左/右兄弟结点“借调”关键字,将兄弟结点中最小/最大的关键字移至父结点,而将父结点中大于/小于且仅靠该上移关键字的关键字下移至被删关键字所在结点中。例如下图所示为删除50之后的B-树
- 若其左和右兄弟结点均无关键字可借(结点中只有⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1⌈m/2⌉−1个关键字),则必须进行结点的“合并”。假设该结点有右兄弟,且其右兄弟结点地址由父结点中的指针p i p_ipi所指,则在删去关键字之后,它所在结点中剩余的关键字和指针,加上双亲结点中的关键字k i k_iki一起,合并到p i p_ipi所指兄弟结点中(若无右兄弟,则合并至左兄弟中)。如果因此使父结点中的关键字数目不足,则依次类推做相应处理。例如下图所示为删除53、37之后的B-树
B+树
应文件系统所需而出的一种B-树的变型树
定义
- 每个叶子结点中含有 n nn 个关键字和 n nn 个指向记录的指针;并且,所有叶子结点彼此相链接构成一个有序链表,其头指针指向含最小关键字的结点
- 每个非叶结点中的关键字 k i k_iki 即为其相应指针 p i p_ipi 所指子树中关键字的最大值
- 所有叶子结点都处在同一层次上,每个叶子结点中关键字的个数均介于 ⌈ m / 2 ⌉ \lceil m/2\rceil⌈m/2⌉和 m mm 之间
可通过单链表或多路查找树来查找记录
插入
仅在叶结点中进行,当结点中关键字个数> m >m>m时将它分裂成两个结点,含关键字的个数分别为⌈ m + 1 2 ⌉ \lceil \frac{m+1}{2}\rceil⌈2m+1⌉和⌈ m + 1 2 ⌉ \lceil \frac{m+1}{2}\rceil⌈2m+1⌉。并且它们的父结点应同时包含这两个结点中的最大关键字
删除
仅在叶结点中进行。当叶结点中的最大关键字被删除时,其在非叶结点中的值可以作为分解关键字存在。若删除使结点中关键字个数< ⌈ m 2 ⌉ <\lceil \frac{m}{2}\rceil<⌈2m⌉,则与兄弟结点合并,过程类似于B-树
哈希查找 / 散列查找
通常查找方法的共同特点:
- 记录在表中的位置和它的关键字之间不存在一个确定的关系
- 查找过程为给定值依次与各个关键字进行比较
- 查找效率取决于比较的次数
对于频繁使用的查找表,希望 A S L = 0 ASL=0ASL=0
- 必须在记录的关键字和它的存储位置之间建立一个双射 f ff,将 f ff 称为 哈希函数
- 哈希函数只是一种映象,所以哈希函数的设定很灵活,只要使任何关键字的哈希函数值都落在哈希表长允许的范围之内即可
- 哈希表 (Hash Table) 即为 “关键字 → \rightarrow→ 地址” 的表
基本概念
- 冲突 (collision):k e y 1 ≠ k e y 2 key1\neq key2key1=key2,但 H ( k e y 1 ) = H ( k e y 2 ) H(key1)=H(key2)H(key1)=H(key2)
- 同义词:哈希函数值相同的两个关键字
- 哈希函数通常是一种压缩映象,所以冲突不可避免,只能选择一个好的哈希函数,尽量减少产生冲突的机率;同时,冲突发生后,应该有处理冲突的方法
- 选择一个好的哈希函数的标准:能将关键字均匀的分布在存储空间中
哈希函数构造方法
选取哈希函数,考虑以下因素:
- 计算哈希函数所需时间 (哈希函数不应太复杂)
- 关键字长度
- 哈希表长度(哈希地址范围)
- 关键字分布情况
- 记录的查找频率 (查找频率高的记录应尽量避免冲突)
直接定址法
- 构造:取关键字或关键字的某个线性函数作哈希地址
H ( k e y ) = k e y 或 H ( k e y ) = a ⋅ k e y + b H(key)=key\\ 或 \ H(key)=a·key+bH(key)=key或 H(key)=a⋅key+b - 这样构造的哈希函数不会有冲突
数字分析法
- 构造:对关键字进行分析,取关键字的若干位或其组合作哈希地址
- 适合关键字位数比哈希地址位数大,且可能出现的关键字事先知道的情况
平方取中法
- 构造:取关键字平方后中间几位作哈希地址。目的是“扩大差别” ,平方值的中间位能受到整个关键字中各位的影响 (很少用)
折叠法
- 构造:将关键字分割成位数相同的几部分 (最后一部分可以不同),取这几部分的叠加和 (舍去进位) 作为哈希地址
- 移位叠加:将分割后的几部分低位对齐相加
- 间界叠加:从一端沿分割界来回折送,并对齐相加
- 适合关键字位数很多,且每一位上数字分布大致均匀的情况
除留余数法
- 构造:取关键字被 p pp 除后所得余数作哈希地址(p ≤ p\leqp≤ 哈希表表长 m mm)
H ( k e y ) = k e y M O D p , p ≤ m H(key)=key\ \ MOD\ \ p,\ \ \ \ \ \ \ \ p\leq mH(key)=key MOD p, p≤m不仅可以对关键字直接取模,也可以在折叠、平方取中等运算后取模
- 特点
- 简单、常用
- p pp 的选取很重要; p pp 选的不好,容易发生冲突. p pp 一般取小于等于表长 m mm 的最大素数
冲突处理方法
- 处理冲突:为产生冲突的地址寻找下一个空的哈希地址
开放定址法
- 方法:当冲突发生时,形成一个探查序列 d i d_idi;沿此序列逐个地址探查,直到找到一个空位置(开放的地址),将发生冲突的记录放到该地址中
- 令:H i H_iHi——新的哈希地址;H ( k e y ) H(key)H(key)——哈希函数;d i d_idi——增量序列;m mm——哈希表表长
则:
H i = ( H ( k e y ) + d i + m ) M O D m H_i=(H(key)+d_i+m)\ MOD\ mHi=(H(key)+di+m) MOD m
- 令:H i H_iHi——新的哈希地址;H ( k e y ) H(key)H(key)——哈希函数;d i d_idi——增量序列;m mm——哈希表表长
- 分类:
- 线性探测再散列:d i = 1 , 2 , 3 , … … m − 1 d_i=1,2,3,……m-1di=1,2,3,……m−1
- 线性探测再散列可以保证只要哈希表未满,就能找到空地址
- 二次探测再散列:d i = 1 2 , − 1 2 , 2 2 , … , ± k 2 ( k < m / 2 ) d_i=1^2,-1^2,2^2,…,±k^2\ \ (k<m/2)di=12,−12,22,…,±k2 (k<m/2)
- 二次探测再散列只有在 m mm 为形如 4 j + 3 4j+34j+3 (j jj 为整数) 的素数时才一定能找到空地址
- 伪随机探测再散列:d i = 伪 随 机 数 序 列 d_i=伪随机数序列di=伪随机数序列
A S L = ( 1 ∗ 4 + 2 ∗ 2 + 3 ∗ 1 + 5 + 6 ) / 9 = 22 / 9 ASL=(1*4+2*2+3*1+5+6)/9 =22/9ASL=(1∗4+2∗2+3∗1+5+6)/9=22/9
- 线性探测再散列:d i = 1 , 2 , 3 , … … m − 1 d_i=1,2,3,……m-1di=1,2,3,……m−1
再哈希法
- 方法:构造若干个哈希函数,当发生冲突时,使用另外一个哈希函数计算哈希地址,即:H i = R h i ( k e y ) , i = 1 , 2 , … … k H_i=Rh_i(key) ,i=1,2,……kHi=Rhi(key),i=1,2,……k,直到不发生冲突为止。其中:R h i Rh_iRhi——不同的哈希函数
链地址法
- 方法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放单链表的头指针
A S L = 6 × 1 + 2 × 2 + 3 × 1 9 ASL=\frac{6\times 1+ 2 \times 2+3\times1}{9}ASL=96×1+2×2+3×1
哈希查找过程
哈希查找分析
- 由于冲突的存在,哈希查找过程仍是一个给定值与关键字进行比较的过程。评价哈希查找效率仍要用 A S L ASLASL
- 哈希查找过程中,与给定值进行比较的关键字的个数取决于:
- 哈希函数
- 处理冲突的方法
- 哈希表饱和的程度,装载因子 α = n / m \alpha=n/mα=n/m(n nn—记录数,m mm—表的长度)
- 一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论 A S L ASLASL 时,可以不考虑它的因素. 因此,哈希表的 A S L ASLASL 是处理冲突方法和装载因子的函数. 查找成功时,
- 线性探测再散列的平均查找长度为
S n l ≈ 1 2 ( 1 + 1 1 − α ) S_{nl}\approx \frac{1}{2}(1+\frac{1}{1-\alpha})Snl≈21(1+1−α1) - 随机探测再散列、二次探测再散列、再哈希的平均查找长度为
S n r ≈ − 1 α l n ( 1 − α ) S_{nr}\approx -\frac{1}{\alpha}ln(1-\alpha)Snr≈−α1ln(1−α) - 链地址法的平均查找长度为
S n c ≈ 1 + α 2 S_{nc}\approx1+ \frac{\alpha}{2}Snc≈1+2α可见哈希表的平均查找长度是α \alphaα 的函数,而不是 n nn 的函数。可以选择一个适当的装填因子 α \alphaα,使得平均查找长度限定在某个范围内
- 线性探测再散列的平均查找长度为
字符串 hash
参考 《算法笔记》
- 字符串 hash 是指将一个字符串 S SS 映射为一个整数, 使得该整数可以尽可能唯一地代表字符串 S SS
- 先假设字符串均由字母 A ∼ Z A\sim ZA∼Z 构成。在这个基础上,不妨把 A ∼ Z A\sim ZA∼Z 视为 0 ∼ 25 0\sim 250∼25, a ∼ z a\sim za∼z 视为 26 ∼ 51 26\sim5126∼51, 这样就把 52 个字母对应到了五十二进制中。接着, 按照将五十二进制转换为十进制的思路, 由进制转换的结论可知, 在进制转换过程中, 得到的十进制肯定是唯一的, 由此便可实现将字符串映射为整数的需求(注意:转换成的整数最大为是 5 2 l e n − 1 52^{len}-152len−1, 其中 l e n lenlen 为字符串长度). 显然,如果字符串 S SS 的长度比较长, 那么转换成的整数也会很大, 因此需要注意使用时 l e n lenlen 不能太长
int hashFunc(char S[], int len) {
int id = 0;
for (int i = 0; i < len; i++) {
if (S[i] >= 'A' && S[i] <= 'Z') {
id = id * 52 + (S[i] - 'A');
} else if(S[i] >= 'a' && S[i] <= 'z') {
id = id * 52 + (S[i] - 'a') + 26;
}
}
return id;
}
- 而如果出现了数字, 一般有两种处理方法:
- (1) 按照字母的处理方法, 增大进制数至 62
- (2) 如果保证在字符串的末尾是确定个数的数字,那么就可以把前面英文字母的部分按上面的思路转换成整数, 再将末尾的数字直接拼接上去
- 例如对由三个字符加一位数字组成的字符串 “BCD4” 来说, 就可以先将前面的 “BCD” 转换为整数 731, 然后直接拼接上末位的 4 变为 7314
大集合数据查找
布隆过滤器 (Bloom Filter)
- 布隆过滤器时一个二进制向量数据结构,具有很好的时间、空间效率,尤其是空间效率极高,常常被用来检测某个元素是否是巨量数据集合中的成员
原理
- 它实际上是一个很长的二进制向量和 K KK 个随机映射函数。当一个元素被加入集合时,通过 K KK 个随即映射函数将这个元素映射成二进制向量中的 K KK 个点,把它们置为 1。检索时,只要看查询点映射后对应的位置是不是都是 1 就 (大约) 知道集合中有没有它了:如果所对应位置有任何一个 0,则被检元素一定不在 (可能会产生误判,但一定不会产生漏判)
计数 BF (Counting Bloom Filter)
- 基本的 BF 无法删除集合成员,只能增加成员并对其进行查询;计数 BF 对此进行了改进
- 计数 BF 的位数组中每个元素用多位表示
跳表
- 跳表核心思想:基于有序链表,在一个节点上添加更多的指针,令其指向更远的后方结点,进一步提高查询效率
- 如果一个节点存在 k kk 个向前的指针,那么该节点是第 k kk 层的节点。一个跳表的最大层 MaxLevel 为跳表中所有节点中最大的层数
- 基于跳表的查找过程:先通过每个节点的最上层指针进行查找,这样就能跳过大部分节点。然后再缩小范围,对下面一层指针进行查找,若仍未找到,缩小范围继续查找