注:以下内容均为个人复习使用,不保证所述内容绝对严谨;
注:考研知识点相对基础,因此这里只做知识点合集,不保证内容详细。
Pre:
查找表、关键字、查找(检索)
静态查找:不更改查找表,仅进行访问操作
动态查找:支持插入、删除、访问
顺序表查找、链表查找、散列表查找、索引查找(B+树,B-树)
一、查找的性能分析:
ASL平均查找长度 = Σ Pi * Ci (查找频率*查找次数)
ASL成功= 总比较次数 / 元素个数 (当元素在数据结构中存在时考虑)
ASL失败= 总比较次数 / 失败情形个数 (当元素在数据结构中不存在时考虑)
二、静态查找算法(顺序查找、二分查找)
1. 顺序查找:
从一端到另一端寻找。(遍历)
ASL =(n+1)/2
单词查找的时间复杂度 O(n)
2. 折半查找:
有关定义
即:二分查找。
根据二分查找划出的查找流程图,称为 二叉搜索树 ,又称判定树。
每次查询搜索深度不超过log n 。 二分查找一定没有回溯。
默认边界 : 向下取整 与 严格区间 , 即 mid=(l+r)/2, dfs(mid+1,r)与dfs(l,mid-1)。
使用前提:
顺序表存储结构、存储序列有序排列
时间复杂度 与 ASL
单次查询的 时间复杂度O(logn)
limit n->+∞ 时, ASL = height - 1
ASL成功 : n为具体数字时,暴力计算;注:若 n = 2h-1, 即为满二叉,也可以直接用结论, ASL= height - 1。
ASL失败: n为具体数字时,暴力计算;注:额外画出表示查询失败的外延节点,题目如果说假定概率相同,即指到达每个外延节点出现的概率相同。
失败的外部节点的个数 = n+1 。 【逻辑推证:每两个数字之间、序列两端,各有一个失败情况,所以是n+1。不需要再从二叉树节点个数计算】
Code (二分)
递归写法
int Q(int l,int r,int va){//区间[l,r]查找va
if(l>r) return false;//查询失败
int mid=(l+r)>>1;
if(va==a[mid]) return mid;
else if(va>a[mid]) return Q(mid+1,r,va);
else return Q(l,mid-1,va);
非递归写法
int Q(int va){
int l=1,r=n;
while(l<=r){
int mid=(l+r)>>1;
if(va<a[mid]) l=mid+1;
else if(va<a[mid]) r=mid-1;
else return mid;
}return false;//查询失败
}
3. 分块查找
有关定义
即块间有序,使用二分查找法寻找所属块;块内不一定有序,使用顺序查找法查找块内位置。
假定块大小为 k, 则 实际时间复杂度 为 O( k + log2(n/k) ),其中k为常数。(考研考试写的时候记得把常数抹了)
考古一个高中学的分块:思想理解:中和时间和空间(定期重构、分块)
分块大小的选择,根号n
为了最大程度的折中时空复杂度,最佳的选择为 每块大小:k = (√n),块数:k = (√n)
时间复杂度 与 ASL
单次查询的时间复杂度为 O( (√n) + log2(√n) )
ASL = 【(k+1) / 2 】+【log2(k+1)】。 (即:块内顺序的ASL为 (k+1)/2, 块间二分的ASL为 log2k)
三、散列查找(Hash哈希、杂凑)
1. 术语 :
Hash函数:就是映射,令存储位置=Hash ( key ) 。
同义词:存在不同的 key 导致 Hash( key ) 相同,这些key称为同义词。
装填因子:a= n/len , 总关键字个数除以表长。
2. 哈希函数好坏的评判
可操作性 : 操作简单、易于实现
冲突较小: Hash查找的ASL与时间复杂度 均为 O(冲突深度) 或 O(平均冲突次数),所以冲突越小越好
散列均匀: 即 在对哈希冲突进行处理的时候,要尽可能减少堆积现象,保证散列分布相对均匀。
理解堆积:
eg. 如果采用线性探测法解决冲突,我们会发现存在这样一种情况:我们是通过占用其他位置的方法解决冲突,这样就会导致让其他原本没有冲突碰撞的元素反而因此发生碰撞,从而增加了冲突次数/平均冲突深度。
3. 哈希函数的构造
直接定址法: H(key)= a* key + b
数字分析法:取关键字的若干位 或 组合 作为地址
平方取中法:将关键字平方后 取中间几位 作为地址
折叠法: 将关键字分割成几段,将几段的重合部分作为地址
推荐使用:
除留余数法构造Hash函数:
取p = 不大于表长的最大质数
Hash(key) = key % p
4. 哈希冲突的解决
(1) 开放定址法(探测空位法)
①线性探测法:从初次发生冲突的位置,依次向后探测(循环列表遍历一圈)寻找空位置。
优点:只要表未满,总能找到不冲突的散列地址;
缺点:每次冲突都是在冲突最近的空地址放置,带来新的冲突的可能,并且冲突“聚集”。
②二次探测法(平方探测法):从初次发生冲突的位置,依次探测 [pos ± i2]的位置,(即每次+1,-1,+4,-4,+9,-9…).
优点:避免冲突聚集。
缺点:跨度太大
注1:该哈希冲突解决方法下,如果要从哈希表中进行某个元素删除,需要采用的是标记法。
原因是:如果直接进行物理删除,则有可能出现,查找通路连通性被损坏的情况。
查找失败的标志:全表被搜索过 or 搜索到空位置。
(2) 链地址法:(链表存冲突)
冲突发生时,直接以该节点为头创建链表,将新key放入链表。
开散列: 处理冲突的方式为 将冲突元素放置hash表外的空间
闭散列: 处理冲突的方式为 将冲突元素在hash表内的空间寻找位置
查找失败的标志: 链表被搜索完依然没找到。
(3) 再哈希法
冲突发生时,令H[i]=ReHash( H[i] )
(4) 公共溢出区
另外开一个数组,冲突的元素都扔这里
5. Hash的ASL计算(平均查找长度)
ASL成功:按照hash函数查找,查到即成功
ASL失败:按照hash函数查找,遇到空位置即失败
6. 时间复杂度
哈希查找的时间复杂度,与节点个数n无关,只与 负载因子 有关,即冲突次数。
7. 装填因子 / 负载因子 α
装填因子/负载因子 = (元素个数) / (哈希表长)
四、动态查找算法(平衡树)
BST树 和 AVL树
二叉排序树 用树表示序列,左子树节点均小于root,右子树节点均大于root
下文中,查询、插入、删除、平衡,均为O( 树深度 ),平均复杂度为O(log n)。
注:下文中代码均为盲写,只用于帮助我自己理解自己的文字内容,不保证运行。
附:SBT平衡树模板链接
1> BST树 Binary Search Tree 二叉查找树
1. 查询操作:
查询值为va的点,每次判断va与root的关系,如果va<root则进入左子树,否则进入右子树
inline int query(int va,int root){
if(root==0) return -1;//无该数字
if(va==root) return root;
if(va<root) return query(va,root.lchild);
if(va>root) return query(va,root.rchild);
}
2. 插入操作:
插入一个值为va的点,每次判断va与root的关系,如果va<root则进入左子树,否则进入右子树
直到所要进入的左子树/右子树不存在,则直接将该点作为左子树/右子树
如果值为va的点已经存在,则不进行插入
3. 修改操作:
=查询操作+更改值(不改树形)
4. 删除操作:
本质思路:类比线性序列,当我们删除一个点时,应当将比他小的最大数or比他大的最小数拿来替代自己的位置。
如果所删除点没有左右子树,则直接删除
如果所删除点只有左子树/只有右子树,则直接用左子树/右子树代替它的位置
如果所删除点既有左子树又有右子树,则用中序直接前驱/后继 代替它的位置,于是问题转化为,删除中序直接前驱/后继。(可以推证的是,中序直接前驱/后继,一定是没有其右子树/左子树)。所以两步实现。
注:这是一种时间执行效率较高的树上删除的方法
inline void erase(int va){
int pos=query(va);
int alter_pos= L_link[pos];//中序前置
tree[pos].va=tree[alter_pos].va;//自己变祖先
fa[alter_pos].r_child = alter_pos.lchild;//孩子给祖先
}
5. 建树操作:
=对空树进行插入操作。
2>平衡二叉树 AVL
pre: 一个二叉查找树的查询效率与树形深度有关,O(log depth)
目的:通过一些实时维护的操作,以保证每次查询时间相对稳定。
二叉树性质:总点数相同时,树形越接近完全二叉树,则深度越小。
维护操作:时刻保持左右子树的深度差值不超过1。
分类讨论需要维护操作的情况:
- 可能导致不平衡的情况:
插入操作之后: 自己所在子树的高度变高。(根据前文,插入操作只会将 被插入点 作为 叶子节点)
删除操作之后: 自己所在子树的兄弟子树变高。
注:该节点所在子树指,包含该节点的所有子树。因此插入删除操作之后,需要将该节点的所有祖先节点都进行维护。 - 不平衡的种类有一下四种可能:
(a) LL形 (b) LR形 © RR形 (d) RL形 - 平衡操作:
- LL形调整:
对于root点,其右子树中有节点被删除or其左子树中被插入了新元素,则可能出现LL形。
浅证一下:
如图所示变形。其中L、root、fa表示点,其余已经平衡后的子树。
分析可知,先前 depth[L] = max( depth[LL], depth[LR] ) +1 = depth[R] +2, 其中LL,LR,R的深度没有发生改变
根据平衡性质可知,LL与LR的深度相差 <= 1,R与LR或LL的深度差1
⇒ 当前depth[root]= max(depth[LR],depth[R]) +1= depth[LR]+1 <= depth[LL]+2
⇒ depth[LL]>=depth[LR]情况下,该操作恒成立。
LL形的含义就是 depth[LL] >= depth[LR]
会带来depth[LL]<=depth[LR]的情况,我们称之为LR形
#if 0
思路:
root->Lchild 作为 root
root 及其右子树 整体作为 root->Lchild 的右子树
root->Lchild 的右子树被挤掉,root的左子树空缺 => 把r->L->R 送给r
#endif
inline void LL_reverse(int root){
int LL=root->Lchild;
root->Lchild = LL->Rchild;// 右儿子给root当左儿子
//root->Rchild 依然为 root->Rchild
LL->Rchild=root;//root变儿子
if (Fa[root]->Rchild == root) Fa[root]->Rchild = LL;//左儿子继位
else (Fa[root]->Lchild == root) Fa[root]->Lchild = LL;//左儿子继位
}
- LR形调整
#if 0
思路:
root->Lchild->Rchild 作为 root
root 及其右子树 整体作为 root->Lchild->Rchild 的右子树
root-Lchild及其左子树 整体作为root->Lchild->Rchild 的左子树
root->Lchild->Rchild 的右子树被挤掉,root的左子树空缺 => 把root->LR->R 送给root当Lchid
root->Lchild->Rchild 的左子树被挤掉,root->Lchild的右子树空缺 => 把root->LR->L 送给root->Lchild当Rchild
#endif
inline void LR_reverse(int root){
int LR=root->Lchild->Rchild,L=root->Lchild;
L->Rchild = LR->Lchild //左儿子给自己的父亲当右儿子
root->Lchild = LR->Rchild;// 右儿子给root当左儿子
//root->Rchild 依然为 root->RchildR
LR->Rchild=root;//root变右儿子
LR->Lchild=L;//L做左儿子
if (Fa[root]->Rchild == root) Fa[root]->Rchild = LR;//LR儿子继位
else (Fa[root]->Lchild == root) Fa[root]->Lchild = LR;//LR儿子继位
}
- RR形调整
类似LL调整, 根据对称性,Ctrl CV之后, 把LL调整中的左右全部互换就可以了
(如果竞赛赛场上遇到,推荐使用word文档进行查找修改,不容易出错)
inline void RR_reverse(int root){
int RR=root->Rchild;
root->Rchild = RR->Rchild;// L儿子给root当R儿子
//root->Lchild 依然为 root->Lchild
RR->Lchild=root;//root变儿子
if (Fa[root]->Rchild == root) Fa[root]->Rchild = RR;//R儿子继位
else (Fa[root]->Lchild == root) Fa[root]->Lchild = RR;//R儿子继位
}
- RL形调整
类似LR调整,根据对称性,Ctrl CV之后,把LR调整中的左右全部呼唤即可
(如果竞赛赛场上遇到,推荐使用word文档进行查找修改)
inline void RL_reverse(int root){
int RL=root->Rchild->Lchild,R=root->Rchild;
R->Lchild = RL->Rchild //左儿子给自己的父亲当右儿子
root->Rchild = RL->Lchild;// 右儿子给root当左儿子
//root->Lchild 依然为 root->Lchild
RL->Lchild=root;//root变右儿子
RL->Rchild=R;//L做左儿子
if (Fa[root]->Lchild == root) Fa[root]->Lchild = RL;//LR儿子继位
else (Fa[root]->Rchild == root) Fa[root]->Rchild = RL;//LR儿子继位
}
3> 红黑树(RB-Tree)
【略记】
考虑到每次mantian(平衡)操作都要较高的复杂度,所以希望适度平衡即可。
目的:保证左右子树的高度差不超过2倍
定义:
- 每个节点染色红色或者黑色两种颜色。
- 根节点与叶子节点为黑色
- 如果一点的节点为红色,其子节点一定为黑色;如果一个节点为黑色,其子节点颜色不做要求。
- 将 root-> leaf 路径中的黑色节点数,定义为“黑高”。
根据上述定义,**“高度差不超过两倍” ** transfer to “黑高相同即可”
Operations平衡操作:
首先理解,
插入操作时,考虑到如果插入黑色节点,黑高一定不同,所以插入节点一定为红。
如果其合适位置的父亲节点是黑,即可直接插入;否则,考虑变色、左旋、右旋操作。
Operation 1 : 变色
叔红要变色,父、叔变黑,爷爷变红。(如果爷爷为根节点,则改为黑)
叔黑看左右,右子树左旋,左子树右旋。
Operation 2 : 左旋
Operation 3 : 右旋
4> 索引查找
目的:提高磁盘中文件查找的效率
B树(B-树)
定义:
阶: 每个节点的子树个数<=阶(树的度)
要求:根的子树个数至少有两个;其余节点的子树个数,至少有 阶数/2 个子树;所有叶子节点必须在树的同一层上 ***
(相当于多路径平衡树)
节点存储信息:数字n,n个value值V [i], n个指针 cnt[i] 。 cnt[i]指向
(每个节点中存储的相当于一个顺序序列)
效果:保证左子树们* 中所有节点的所有value值都小于root中的任意value值;右子树们 均大于root。
查询:
在当前节点中,先进行二分查找,找到对应value直接返回,否则在对应位置进入子树。
时间复杂度 O( log阶数 * 高度 )
插入
查询+裂开 (裂开?gape、crack,刚背的考研词汇,嗯)
查询应当插入的位置,根据B-树的定义,考虑是否将当前点裂开
裂开的操作:将当前点的mid位置的value给父亲,左右两段分成两个点;判断父亲长度是否符合要求,如果超出长度,则继续向上裂变。
【⇒ 性质:只有根发生裂变,树的高度才会变高】
删除
将左儿子的最大值或右儿子的最小值代替自己,然后问题转化为删除左儿子的最大值(右儿子的最小值)
对于叶子节点上某点的删除:直接删除。
删除之后可能出现的情况:当前节点的序列长度过短,则向兄弟借(把兄弟的value给父亲,父亲的 value给自己)
B+树
把所有非叶子都做成索引,只有叶子做value值记录。
※ 常见考点:
二叉排序树/折半查找 的 “比较路径”/ “比较序列” 的合法性判断
基于性质:二分排序树的查找过程,一定是一直向下查找,一条路走下去的方式。即 当前节点的下一个节点一定是自己的儿子。
根据上述性质绘制查找树的该路径,判断是否可以成功绘制。
二分查找树的高度
二叉查找树为平衡二叉树,因此
总结点数n符合不等式 : 2h-1 <= n <= 2h-1
⇒ height = log(n+1) 向上取整
易错:区分二叉排序树、二叉查找树
二叉排序树: 是在对无序序列进行快速排序的过程中构造的树形结构,其 不一定是平衡树形, 有可能是一条链,其单次查询的时间复杂度 范围为 ( log2n , n)
二叉查找树:实在对有序序列进行二分查找的过程中构造的树形结构,其一定是平衡树形,其查询的时间复杂度 一定为 O ( log2n )。