线性表的查找
顺序查找
技巧:设置哨兵,放在下标为0的位置。
int Search_Seq(SSTable ST, KeyType key) {
ST.R[0].key = key;
for(int i = ST.length; ST.R[i].key != key; i--);
return i;
}
算法分析
适用于顺序结构和链式结构,不要求记录按关键字有序。
平均查找长度为(n + 1) / 2,查找效率低。
折半(二分)查找
int Search_Bin(SSTable ST, KeyType key) {
low = 1; high = ST.length;
while(low <= high) {
mid = (low + right) / 2;
if(key == ST.R[mid].key) return mid;
else if(key < ST.R[mid].key) high = mid - 1;
else low = mid + 1;
}
return 0;
}
二叉判定树
当前查找区间的中间位置是根,左子树和右子树分别是左子表和右子表。
则查找失败就是走了一条从根结点到外部结点的路径,比较的关键字个数等于路径上的内部结点个数。
当n较大时,ASL = log2(n + 1) - 1
算法分析
要求必须是顺序存储结构,且按关键字有序排列。
对于长度为n的有序表,需要比较的关键字个数最多是floor(log2(n)) + 1。时间复杂度为O(log2(n)).
分块查找(索引顺序查找)
建立一个索引表,按关键字有序,整个待查找表有序或者分块有序。
确定块的查找可以用顺序查找或者折半查找,块中的查找只能用顺序查找(因为不一定关键字有序)。
算法分析
顺序查找:ASL = (1 / 2) * (n / s + s) + 1
折半查找:ASL = log2(n / s + 1) + (s / 2)
便于插入和删除,可以实现在快速查找的同时经常动态变化。
散列表(哈希)
相关定义
- 散列表:有限连续的地址空间。
- 冲突:不同关键字对应同一个散列地址。
冲突是不可避免的。 - 同义词:发生冲突的不同关键字。
构造散列函数
原则
- 减少冲突。
- 散列地址分布均匀。
常用方法
- 直接定址
1)条件:已知关键字每一位的数字分布情况。
2)操作:从关键字中提取数字分布比较随机的若干位作为散列地址。 - 平方取中
取关键字平方后的中间几位,具体位数由表长决定。 - 折叠
将关键字分割成位数相同的多个部分,叠加求和,舍去进位,取与前面相同的位数。
适用于散列地址位数少,关键字位数多。
1)移位叠加:最低位对齐
2)边界叠加:按照折叠绳子的方式叠加 - 除留余数
选择一个比表长小的最大素数取模。
处理冲突
- 开放地址
H0发生冲突时,基于H0用某方式计算得到下一个H,直到不发生冲突为止。
- 线性探测法
假想成一个循环表,先从冲突地址下一个位置进行寻找,如果到表尾也没找到位置,就从表头开始再找,如果还是找不到,做溢出处理。 - 二次探测法
每次从冲突位置的前后k²位置查找。 - 伪随机探测法
增量序列等于一个伪随机数序列。
- 链地址
同义词链表:散列地址相同的关键词放进一个单链表。
一个数组存放头指针。
分析
- 线性探测法
- 优点:只要表没满,总能找到合适的位置。
- 缺点:二次聚集(第一个散列地址不同的关键字在寻找后续散列地址的过程中再次冲突)。
- 二次探测法/伪随机探测法
- 优点:没有二次聚集。
- 缺点:不一定找得到位置。
- 链地址
没有二次聚集。
散列表查找
算法分析
- 装填因子α
α = 表中填入记录数 / 散列表长度
表示散列表的装满程度。α越大,冲突可能性越大,需要比较的关键字个数越多。 - 影响平均查找长度的因素:处理冲突的方法和装填因子。
平均查找长度与α有关,与n无关。
线性探测法 成功(1/2)(1+1/(1-α)) 失败(1/2)(1+(1-α)²)
二次探测法/伪随机探测法 成功(-ln(1-α)/α) 失败(1/(1-α))
链地址法 成功(1+α/2) 失败(α+exp(-α))
二叉排序树
性质:中序遍历是递增的
查找
算法实现
BSTree SearchBST(BSTree T, KeyType key) {
if(!T || key == T->data) return T;
else if(key < T->data) return SearchBST(T->lchild, key);
else return SearchBST(T->rchild, key);
}
算法分析
最坏情况:单支树 ASL=(n+1)/2
平均情况:ASL=log2(n)
插入
时间复杂度O(log2(n))
创建
时间复杂度O(nlog2(n))
删除
在不破坏二叉排序树性质的情况下,选择合理的被删结点的左/右孩子对其进行替代。
时间复杂度O(log2(n))
平衡二叉树(AVL树)
平衡因子(BF)
节点左右子树的深度差。
AVL树的所有节点的BF为0/1/-1.
查找的时间复杂度是O(log2(n))
平衡调整方法
调整最小不平衡子树。
书上总结了4中旋转方式(LL,RR,RL,LR),但是完全不需要记,因为很抽象。确定好调整范围后直接进行调整。
B-树
定义
B-树也叫B树、B_树(“-”是个连字符,不是“减”),是适用于外查找(存在外存里的)的平衡多叉查找树。
适用于磁盘目录管理、数据库系统索引等。
- 每个结点至多有m棵子树(m称为阶,m等于2时B-树就是二叉搜索树)。阶数通常非常大,以保证在存了大量数据的情况下,树的高度不会过于大。
- 如果根不是叶子,则至少有两棵子树。(可以理解成 根不是叶子节点 说明根必有子树 而B-树的节点必须先有关键字才能有子树 且紫薯数量等于关键字数量加1)
- 除根之外的所有非叶子结点至少有
ceil(m/2)
棵子树。 - 失败结点:叶子结点。所有叶子结点都位于同一层。
- 非叶子结点最多有(m-1)个关键字。(之所以这样要求,是因为B-树的每个结点其实可以理解成关键字“填补了”子树的缝隙,子树存在于两个相邻关键字之间(自己瞎理解的,如果让你感到迷惑就忽略吧))
特点
- 平衡:所有叶子都在同一层。
- 有序:左小右大。
- 多路:非叶子节点有多个关键字和子树。
查找
存储结构
#define m 3
typedef struct BTNode {
int keynum;
struct BTNode *parent;
int k[m + 1]; // 关键字
struct BTNode *ptr[m + 1]; // 子树指针
Record *recptr[m + 1];
} BTNode, *BTree;
typedef struct {
BTNode *pt;
int i; // 关键字序号
int tag; // 查找是否成功
} Result;
算法描述
顺序/折半查找,将待查值与根节点各个关键字比较。
key == Ki
查找成功。key < K1
沿第一个子树向下找。Ki < key < K(i+1)
沿着第i个子树向下找。key > Kj
沿着随后一个子树指针向下找。
如果直到叶子节点也没有找到,则查找失败。
PS:“关键字”指的并不是存储的数据,实际上一个结点中存储的应该是很多个<key, value>键值对,只是为了方便而用关键字来代指键值对。所以在查找、插入、删除的过程中,真正操作的对象其实是“记录”,也就是键值对。
算法实现
Result SearchBTree(BTree T, int key) {
p = T, q = NULL, found = false, i = 0;
while(p && !found) {
i = Search(p, key);
if(i > 0 && p->k[i] == key) found = true;
else q = p, p = p->ptr[i];
}
return (found) ? (p, i, 1) : (q, i, 0);
}
算法分析
B-树查找的两种基本操作是在树上找结点和在结点中找关键字。由于B-树通常存在磁盘上,所以前者是在磁盘上执行的,后者是在内存中执行的。所以磁盘中的查找次数,也就是待查关键字在B-树上的层数(深度)决定了查找效率。
查找可能在非叶子结点结束,整棵树中查找一次的效率逼近二分查找。
插入
算法描述
- 如果待插入key存在,就用新的value替换旧的value。
- 如果待插入key不存在,就在叶子结点中插入;插入之后判断当前结点key数是否小于等于m-1,如果不满足,则取当前结点的中间结点(如果是偶数就取中间左侧或右侧任意一个结点)放到其上一层结点的正确位置,也就是“结点分裂”。
算法分析
如果所有关键字都事先排序后再插入,会导致结点空间利用率很低,最差只有50%(因为父结点对应关键字与其右侧关键字之间的整数个数可能小于子结点的空间,比如父节点27与30之间的子树,最多只能有28和29两个结点,而空间很可能大于2)。
删除
算法描述
- 如果待删结点位于非叶子结点,先将其替换到叶子中再进行删除。
- 删除之后判断当前结点中关键字个数是否大于等于
ceil(m/2)-1
,如果是就结束操作。 - 如果不是,先看其兄弟结点是否可以对其进行支援(挪过一个关键字并不影响兄弟结点自身的合法性),如果可以就做,如果不可以,从其父结点中取出关键字并与其及其兄弟结点进行结点合并。
B+树
B+树与B树的区别
- B+树的非叶子结点只存储关键字,不存储数据,可以减少结点数从而减小树的高度,进而查找过程中需要进行的磁盘操作次数就更少。
- B+树的叶子结点按照从小到大的顺序依次连接。
- B+树除根之外的所有非叶子结点至少有
ceil(m/2)
个关键字(注意B树除根之外的所有非叶子结点至少有ceil(m/2)
棵子树)。
查找
与B-树类似,只是如果非叶子结点上的关键字等于待查关键字,不会终止查找,而是继续向下直到找到对应的叶子结点。也就是说,不会像B-树一样时间效率不稳定。由于B+树所有叶子都位于同一高度,所以查找的时间复杂度稳定在O(logn)。
插入和删除
都只在叶子结点中进行。如果插入/删除之后导致出现溢出/关键字不够,就操作对应结点及其兄弟/父结点进行结点分裂/合并。
为什么用B+树而不用B树?
- IO次数更少。每个结点能存的关键字数量更多,所以高度更低,需要的磁盘IO次数更少。
- 便于范围查询。只需要扫描一遍所有叶子就可以完成给定上下限的范围查询,而B树需要进行中序遍历。
- 查询效率更稳定。由于每次查询都是从根到叶子,所以查询复杂度稳定为树的高度。