Index
索引是一种数据结构,支持高效检索&动态调整(数据库技术)。
- 主码(Primary Key):每条记录的唯一标识。
- 辅码:可重复的属性
-
索引:把关键码与它对应的记录未知关联起来的过程(关键码,指针)
-
稠密索引:每个记录建立一个索引项。
主文件无需按照关键码排序。
-
稀疏索引:一组记录建立一个索引项。
主文件必须按照关键码次序存放。
可以把记录分块,索引指针指向每块的起始位置。
-
- 索引文件:记录这种联系的文件。
-
主文件:原始数据记录
一个主文件可能有多个相关索引文件,每个索引文件支持一个索引字段。
线性检索
按照索引码值的顺序进行排序的文件。
-
特点
可以访问变长数据库记录,支持高效二分检索。
体积太大,只能存储在磁盘中,影响效率。
-
二级线性索引
一个磁盘可以存放M个记录。
原始数据有N个记录,一级索引也有N个记录,则一级索引占用\(N/M\)个磁盘块。
再对一级索引按照磁盘块建立二级索引,只需要\(N/M^2\)个磁盘块。
每个二级索引存放对应的一级索引块中的第一个索引。
检索过程使用两次二分搜索即可。
- 假定一个计算机系统有4096字节的磁盘块,每个磁盘的磁盘号可以用一个4字节的整数表示。要存储的每一条记录中4个字节是关键码,64个字节是数据字段。记录已经排序,顺序地存储在磁盘文件中。我们建立一个稠密索引,该线性索引的结构为:(每个文件磁盘块的最小关键码,该块磁盘的磁盘号),通过线性索引访问磁盘文件中的记录。
如果线性索引的大小是2MB。最多可以在磁盘文件中存储 15M 条记录。
2MB/8B * (4096B/68B) = 0.25M * 60 = 15M
如果线性索引也存储在磁盘中(这样它的大小仅受二级索引的限制),而且使用4096个字节的二级索引,二级索引中的每个单元引用线性索引的磁盘块中最小的关键码值。文件中最多可以存储 15M 条记录。
4096/8 (二级索引个数) * 4096/8 (每个二级索引指向一个存一级索引的块,每个块中一级索引的个数) * 4096/68 (每个一级索引指向一个块,每个块存放记录的个数) = 2^18 * 60 = 15M
静态检索(Deprecated)
文件创建完成时索引就不能再被修改了。
几乎没有插入/删除,或者允许定期重新组织结构的话,效率还是很高的。
倒排检索
由属性值来确定记录的位置,称为倒排检索(基于属性/基于正文)。
-
基于属性的倒排
倒排表:
(attr, ptrList)
属性(不唯一)
-->
若干记录指针(唯一,如关键码或主文件地址) 可以对不同属性建立多个倒排表。
优点:基于属性的高效检索。
缺点:倒排表空间代价,更新运算效率降低。
-
对正文文件的倒排
支持文本内容的快速检索
-
词索引
提取关键词(过滤停用词),对关键词建立倒排表:
(keyword, [(text, position), ...])
检索时,首先检索关键词,之后提取关键词对应的记录指针列表,获取记录。
通常用另一个索引结构对关键词进行检索,如Trie图,散列。
- 全文索引
优点:高效检索文本数据库。
缺点:支持的检索类型有限,检索词有限,空间代价极高。
-
动态检索
动态更新删除,并保持最佳检索性能。
B树
-
定义
m阶B树为m路平衡查找树,或者为空,或者:
- 每个节点至多m个子树(m-1个关键码)
- 根节点至少2个子树,其他非叶节点至少
ceil(m/2)
个子树。
-
**节点的一般形态 **
每个节点都存放在一个单独的磁盘块中,所以每读入一个节点需要磁盘IO一次。
[ P0 K1 P1 K2 P2 ... P(j-1) Kj Pj ]
包含j个递增关键码K,j+1个指向两个关键码之间的子节点的指针。
注意关键码从1开始编号。
-
性质
- 有k个子树(指针P)的节点有k-1个关键码K
- 树高平衡,叶节点位于同一层,关键码不重复
- 父节点关键码是子节点的分界
- 访问局部性原理:值相近的记录放在相近的磁盘页中。
- 节点关键码至少一定比例是满的(保持效率)
-
查找
-
两步交替
- 读取根节点,在其K个关键码中检索目标关键码T。找到则检索结束。
- 否则,T落在两个K之间,取对应的P子树继续检索。如果P为空指针,检索失败。
-
检索长度
H为树高,则最多H次读盘上的节点。
考虑找到后要再读取主数据,则最多需要访外H+1次。
-
-
插入
保持等高(尽量)、等阶的性质。
找到最底层合适位置插入,溢出则节点分裂为左右两个节点,中间关键码连同新指针提升进入父节点,直到不溢出(根节点也溢出时,树高增加一层)。
节点分裂:插入后若节点数为m(溢出),则
[K1 K2 ... Km] -> [K1 ... K(ceil(m/2)-1)] + [K(ceil(m/2))] + [K(ceil(m/2)+1) ... Km]
查找:失败需要读盘H次。
插入:不分裂需要1次写盘,分裂的最后一个节点需要3次写盘,分裂中间的节点需要2次写盘。
故插入最多IO次数为:\(H+3+(H-1)*2 = 3H+1\)
-
删除
-
叶节点
- 删除后节点数不小于ceil(m/2)-1:直接删除
-
删除后节点数小于ceil(m/2)-1
-
相邻兄弟节点关键码大于ceil(m/2)-1
借若干关键码,调整父节点(实际结果是把父节点分界拉下来,兄弟的推上去)
-
相邻兄弟节点关键码等于ceil(m/2)-1
合并两个节点,并把父节点分界拉下来(有可能使树高减一)
-
## rank = 3 # ====== init ====== [55,65] / | \ [47] [60,62] [78] # ====== delete 78 ====== (borrow from brother) [55,62] / | \ [47] [60] [65]
-
非叶节点
把此关键码与后继交换位置,成为叶节点再删除。
-
-
性能分析
共有N个关键码的B树有N+1个叶节点(外部空指针)。
证明:设有l个空指针,n个节点,第i节点有x_i个关键码,x总和即关键码总数N,则边的总数m为:
设树m阶k层(根节点在第0层),则通过另一种方法计算最后一层的节点数,
最大高度公式:(也是最大检索次数)
最小高度公式:
设p为内部节点数,s为每插入一个关键码的平均分裂节点数:
- 高度为2的5阶B树,所含关键字的个数最少是5。
第一层最多有4个关键字,插入第五个后分裂为两层。
- B树不支持顺序查找。B+树支持顺序查找。
B+有链表,但B树没有这种方便的结构。
- 3阶B树含2047个关键字,则最高11层,最矮7层。
若考虑叶节点也是一层,则最高12层。
B+树
B树变体,只在叶节点存储信息。
MySQL索引采用的就是B+树。
-
定义
m阶B+树或者为空,或者:
- 每个节点至多m个子节点
- 根节点至少2个子节点,非根节点至少
ceil(m/2)
个子节点,
-
性质
-
有k个子节点的节点有k个关键码(从1开始编号!)
[K1 P1 ... Km Pm]
每个Pi指针指向一个子节点,对应的Ki为子节点中关键码的最大值(或最小值)。
- 叶节点一般用双链表连接起来
-
-
查找
读取根节点,二分检索范围,取对应子树继续查找。
直到查找到叶节点层。
-
插入
-
分裂:
[K1 ... Km Km+1] --> [K1 ... K(floor(m/2))] + [K(ceil(m/2)) ... Km Km+1]
之后提升
K(floor(m/2))
到父节点,递归检查是否需要分裂。
-
-
删除
- 删除关键码后个数大于
ceil(m/2)
,结束。 -
删除关键码后个数小于
ceil(m/2):
-
兄弟可以借节点:借一个,调整父节点中的关键码,结束。
需要把兄弟的一个关键码与对应子树挪过来,并修改父节点分界。
- 兄弟也借不了,和兄弟合并。删除一个父节点关键码,递归向上检查父节点关键码数量。
-
- 删除关键码后个数大于
红黑树
定义
满足如下约束的BST树:
- 黑红二色:每个节点被染色为红色或黑色。
- 首尾皆黑:根节点与叶节点(NULL)均为黑色。
- 红红不连:红色节点不相连。
- 黑点同阶:任意节点到其叶节点的路径包含相同数目的黑色节点。
节点X的阶(黑色高度,Rank):该节点到对应子树的任一外部节点的路径上黑色节点的个数。不包括自身,包括叶节点。叶节点阶0,根节点的阶称为树的阶。
性质
- 满二叉树(扩充二叉树,树叶均为黑色NULL)
-
k阶红黑树根到叶的简单路径长度介于
[k, 2k]
间均为黑节点最短,黑红相间最长。此性质限制了树高,允许红黑树在最坏情况下相比普通的BST都是高效的。
- 内部节点数最少是高为k的完全满二叉树,即至少有\(2^k-1\)个内部节点。
-
n个内部节点红黑树的最大高度:\(2log_2(n+1)+1\)
设阶为k,树高为h。
红黑树之插入
# TL;DR
首先插入目标位置,标记为红色。
* P不存在:变黑。
* P黑色:结束。
* P红色:双红调整。
* U黑色(eg.P左U右):
* X与U同侧(eg.为左子节点):旋转GP,换色(P变黑,XG变红)。
* X与U异侧(eg.为右子节点):旋转PX(退化到上一种情况),旋转GX,换色(X变黑,GP变红)。
# 换色规律:当前的父节点变黑,两个子节点变红
* U红色:
换色(G非根节点时变红;PU变黑),递归向上红红检查。
红黑树本身仍是BST树,插入元素的位置首先根据BST的二分规则找到,一定是某一个NULL叶节点处。
新插入的节点X标记为红色。设父节点P,祖父节点G,叔节点U。
- X无父节点(X是根节点):换为黑色,结束。
- 父节点P黑色:结束(不破坏红黑树的约束)。
-
父节点P红色:进行双红调整:
此时已知P为红色,故G为黑色且阶为1。
-
U为黑色:旋转
由于G阶为1,U只能是NULL叶节点。
调整方法:以G为轴,旋转P。
### BST Rotation ### 旋转操作本质上是在**不破坏BST树的二分性质**的情况下转移节点的方法! ## Right Rotation # axis X, target Y. Note B is transfered. \ \ [X] [Y] / \ / \ [Y] C --> A [X] / \ / \ A *B* *B* C ## Left Rotation # axis Y, target X. Reversion of Right Rotation. \ \ [X] [Y] / \ / \ [Y] C <-- A [X] / \ / \ A *B* *B* C
### RB-Tree RR rotation ### 以下讨论针对P在左,U在右的情况。P右U左时,两种case要反过来。 ## X is left child # ====== insert X ====== \ [G] / \ (P) []U / \ (X) [] / \ [] [] # ====== right rotate G, P ====== # Note the color is also changed! \ [P] / \ (X) (G) / \ / \ [] [] [] []U ## X is right child # ====== insert X ====== \ [G] / \ (P) []U / \ [] (X) / \ [] [] # ====== left rotate P, X ====== # degenerate to case [X is left child] \ [G] / \ (X) []U / \ (P) [] / \ [] [] # ===== right rotate G,X ====== \ [X] / \ (P) (G) / \ / \ [] [] [] []U
-
U为红色:换色
G阶为1,故红色U节点下只能挂两个叶节点。
### RB-Tree RR change color ## for simplicity, we omit [] NULL leaves. # ====== insert X ====== \ [G] / \ (P) (U) / (X) # ====== change color ====== # of G, P, U \ (G) / \ [P] [U] / (X)
如果G是根节点,则不用改变G的颜色。
否则,由于此时G变为红色,需要递归向上继续进行红红检查。
-
红黑树之删除
# TL;DR
* X没有空树叶:与后继交换,再删除后继(退化为下两种情况)
* X有一个空树叶:
删除X,用非空子节点替换,变黑。
* X有两个空树叶:
* X红色:直接删除X。
* X黑色:双黑调整。
* B红色:PB旋转,变色(此处BP换色),退化为下两种情况。
* B黑色:
* LR均黑:删除X,B变红,P变黑,递归向上双黑调整。
* L黑R(N)红:删除X,PB旋转,换色(PN变黑,B变为P的原色)。
* L(N)黑R红:删除X,BN旋转,PN旋转,换色(PB变黑,N变为P的原色)。
* LR均红:上两种情况任选其一执行即可。
-
待删节点没有空树叶:
根据BST规则,先与右子树最小的后继结点交换(换值不换色),再删除该后继(退化到以下两种情况)。
-
待删节点只有一个空树叶:
首先,待删节点只能是黑色的,且非空的另一个子节点一定是红色节点,该红色节点下只能挂两个叶节点(否则两个子树阶不相等)。删除后把红子节点替换到此处,变黑,结束。
## only one case # ====== delete X ====== / [X] / \ [] (Y) / \ [] [] # ====== lift Y, change color ====== / [Y] / \ [] []
-
待删节点有两个空树叶:
- 待删节点红色,直接删除。
-
待删节点黑色,双黑调整(直接删除会使阶失衡):
设待删节点为X(双黑节点),父节点P,兄弟节点B。
-
B为红色:
则P只能为黑色,B的子节点为非空黑色子节点。
操作:旋转P-B,转化为后两种情况。
## delete Condition 1 # ====== init ====== # l、r是阶为2的子树, l、r本身是非空**黑**节点。 \ P / \ [X] (B) / \ / \ [] [] l r # ====== rotate P, B ====== \ (B) / \ P r / \ [X] l # ====== change color ====== \ B / \ (P) r / \ [X] l # since l is a black Brother for X now # this question degenerates to the following two cases:
-
B为黑色,B的子节点均黑色:
此时P为红色或黑色。B的两个子节点只能为叶节点。
## delete Condition 2 # ====== init ====== \ P / \ [X] [B] / \ / \ [] [] [] [] # ====== delete X ====== \ P / \ [] [B] / \ [] [] # ====== change color ====== \ [P] / \ [] (B) / \ [] []
P原来红色时,调整结束。
P原来黑色时,由于这一个子树整体少了一阶,需要递归向上双黑调整。
-
B为黑色,B的子节点有至少一个红色:
分三种情况讨论。
### delete Condition 3 ### for simplicity, omit NULL leaves. ## only right Nephew is red ## so B's left child is black, and thus must be NULL leaf. # ====== init ====== \ P / \ [X] [B] \ (N) # ====== delete X ====== \ P \ [B] \ (N) # ====== rotate P,B ====== \ [B] / \ P (N) # ====== change color ====== # here B is changed to the original color of P \ B / \ [P] [N] ## only left Nephew is red # ====== init ====== \ P / \ [X] [B] / (N) # ====== delete X ====== \ P \ [B] / (N) # ====== rotate B,N ====== \ P \ (N) \ [B] # ====== rotate P,N ====== \ (N) / \ P [B] # ====== change color ====== # here N is changed to the original color of P \ N / \ [P] [B] ## both Nephews are red ## do either of the first two methods is OK ## for simplicity, we adopt the first method, to work on (R). # ====== init ====== \ P / \ [X] [B] / \ (L) (R) # ====== delete X ====== \ P \ [B] / \ (L) (R) # ====== rotate P,B ====== \ [B] / \ P (R) \ (L) # ====== change color ====== \ B / \ [P] [R] \ (L) # ====== another possible method ====== # \ # L # / \ # [P] [B] # \ # (R)
-
红黑树性能
平均和最差检索\(O(lgn)\)。
统计性能优于AVL树。且增删算法相对简单。
set,multiset,map,multimap均为红黑树变体。