可以用于查询的数据结构非常的多,比如说二插搜索树、平衡树、哈希表、位图、布隆过滤器,但如果需要存储一些数据量极大的数据,上述的数据结构就很不适用了,内存是不一定放的下的,比如用平衡树搜索一个贼大的文件。
因为数据量非常大,一次性无法加载到内存中,那么就可以在平衡树中保存数据项需要查找的部分,以及指向该数据在磁盘中的地址的指针。
虽然在上诉方法在内存中存放的是数据的地址,真实的数据是在磁盘上,解决了内存占用的问题。但是也有一定的缺陷:
解决办法:就是减少I/O的次数,降低树的高度,引入多叉平衡树(B树)
B树是一棵M阶(M>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性
质:
为了简单起见,假设M = 3. 即三叉树,每个节点中存储两个数据,两个数据可以将区间分割成三个部分,因此节点应该有三个孩子,为了后续实现简单期间,节点的结构如下:
注意:孩子永远比数据多一个。
插入过程当中,有可能需要分裂,分裂的
前提是:
假设,当前是要组成一个M路查找树,关键字数必须<=M-1**(这里关键字数**>M-1**就要进行节点拆分)**规则是:
把中间的元素,提取出来,放到父亲节点上,左边的单独构成一个节点,右边的单独构成一个节点。
假设用 53 , 139 , 75 , 49 , 145 , 36 , 101 {53,139,75,49,145,36,101} 53,139,75,49,145,36,101构造B树的过程如下:
再插入75,插入75后按照插入排序的思想对元素进行排序
因为该节点的元素已经满了,就需要进行分裂
节点分裂的步骤:
插入49和145
插入36
插入后违法了B树的性质,需要进行分裂
对cur节点进行分裂:
插入数字101
分裂完后,发现根节点也需要进行分裂,根节点分裂需要进行特殊处理,因为它会增加树的高度。
如果树为空,直接插入新节点中,该节点为树的根节点
树非空,找待插入元素在树中的插入位置(注意:找到的插入节点位置一定在叶子节点中)
检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入)
按照插入排序的思想将该元素插入到找到的节点中
检测该节点是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足
如果插入后节点不满足B树的性质,需要对该节点进行分裂:
如果向上已经分裂到根节点的位置,插入结束
B树的节点定义
// M叉树
private static final int M = 3;
static class BTreeNode {
// 节点关键字
int[] keys;
// 孩子
BTreeNode[] subs;
// 关键字个数
int keySize;
// 父节点
BTreeNode parent;
public BTreeNode() {
keys = new int[M];
// 多给一个方便分裂
subs = new BTreeNode[M+1];
}
}
定义一个类方便查找指定节点
public class Pair<K,V> {
K key;
V val;
public Pair(K key, V val) {
this.key = key;
this.val = val;
}
}
插入代码
/**
* B树
*/
public class BTree {
// B树根节点
BTreeNode root;
public boolean insert(int key) {
if (root == null) {
root = new BTreeNode();
root.keys[0] = key;
root.keySize++;
return true;
}
// 查找关键字是否已经存在
Pair<BTreeNode,Integer> cur = find(key);
if (cur.val != -1) {
// 说明key已经存在,插入失败
return false;
}
// 开始插入(插入排序)
BTreeNode parent = cur.key;
int i = 0;
for (i = parent.keySize-1; i >= 0; i--) {
if (parent.keys[i] >= key) {
parent.keys[i+1] = parent.keys[i];
} else {
break;
}
}
parent.keys[i+1] = key;
parent.keySize++;
// 判断是否需要分裂
if (parent.keySize >= M) {
split(parent);
}
return true;
}
/**
* 对cur节点进行分裂
* @param cur
*/
private void split(BTreeNode cur) {
BTreeNode newNode = new BTreeNode();
BTreeNode parent= cur.parent;
// 中间位置
int mid = cur.keySize>>1;
// 把mid往后的元素移动到新节点
int j = 0;
int i = mid+1;
for (; i < cur.keySize; i++) {
newNode.keys[j] = cur.keys[i];
// 子节点引用
newNode.subs[j] = cur.subs[i];
// 修改子节点的parent引用
if (newNode.subs[j] != null) {
newNode.subs[j].parent = newNode;
}
newNode.keySize++;
j++;
}
// 多拷贝一次最后的子节点
newNode.subs[j] = cur.subs[i];
if (newNode.subs[j] != null) {
newNode.subs[j].parent = newNode;
}
//更新分裂节点关键字个数,-1指的是mid位置的节点要向上提
cur.keySize = cur.keySize-mid-1;
// 处理根节点分裂情况!
if (cur == root) {
root = new BTreeNode();
root.keys[0] = cur.keys[mid];
root.keySize++;
root.subs[0] = cur;
root.subs[1] = newNode;
cur.parent = root;
newNode.parent = root;
return;
}
// 更新新节点的父亲节点
newNode.parent = parent;
// 将mid位置的元素向上提
int midVal = cur.keys[mid];
i = parent.keySize-1;
for (; i >= 0; i--) {
if (parent.keys[i] >= midVal) {
parent.keys[i+1] = parent.keys[i];
parent.subs[i+2] = parent.subs[i+1];
} else {
break;
}
}
parent.keys[i+1] = midVal;
parent.keySize++;
// 将当前父节点的孩子节点设置为newNode
parent.subs[i+2] = newNode;
// 如果个数超过M递归分裂
if (parent.keySize >= M) {
split(parent);
}
}
/**
* 在B树中查找关键字是否已经存在
* 找到返回节点和起下标,否则返回其父节点和-1
* @param key
* @return
*/
private Pair<BTreeNode, Integer> find(int key) {
BTreeNode cur = root;
BTreeNode parent = root;
int index = 0;
while (cur != null) {
while (index < cur.keySize) {
if (cur.keys[index] == key) {
return new Pair<>(cur,index);
} else if (cur.keys[index] < key) {
index++;
} else {
break;
}
}
parent = cur;
// 向子节点找
cur = cur.subs[index];
index = 0;
}
return new Pair<>(parent,-1);
}
}
对B树进行中序遍历,只要打印出来的数据是有序说明插入正确
/**
* 验证B树
* @param root
*/
private void inorder(BTreeNode root){
if(root == null)
return;
for(int i = 0; i < root.keySize; ++i){
inorder(root.subs[i]);
System.out.println(root.keys[i]);
}
inorder(root.subs[root.keySize]);
}
对于一棵节点为N度的B树,查找和插入需要 l o g M − 1 N log_{M-1}N logM−1N~ l o g M / 2 N log_{M/2}N logM/2N次比较。对于度为M的B树,每一个节点的子节点个数为 M / 2 M/2 M/2~ ( M − 1 ) (M-1) (M−1)之间,因此树的高度应该要在 l o g M − 1 N log_{M-1}N logM−1N和 l o g M / 2 N log_{M/2}N logM/2N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素。
B树的效率是很高的对于 N = 62 ∗ 1000000000 N=62*1000000000 N=62∗1000000000个节点,如果M为1024,则 l o g M / 2 N < = 4 log_{M/2}N<=4 logM/2N<=4,既在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找,可以快速定位到该元素,大大减少了读取磁盘的次数。
总结:
B+树是B树的变形,也是一种多路搜索树:
其定义基本与B树相同,除了:
对于B+树来说,如果要查找数据,那么一定得遍历整个树的高度,因为数据在叶子节点,但B树的每个节点都存储了数据,就不一定要遍历到叶子节点。
B+树的分裂:当一个结点满时,分配一个新的结点,
并将原结点中1/2的数据
复制到新结点,最后在父结点中增加新结点的指针,B+树的分裂只影响原结点和父
结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树
B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。
B树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了),如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如:书籍目录可以让读
者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价值的分类网站,本质上就是互联网
页面中的索引结构。
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:索引就是数据结
构。
注意:索引是基于表的,而不是基于数据库的。
MyISAM引擎是MySQL5.5.8版本之前默认的存储引擎,不支持事物,支持全文检索,使用B+Tree作为索引结构,
叶节点的data域存放的是数据记录的地址,其结构如下:
上图是以以Col1为主键,MyISAM的示意图,可以看出MyISAM的索引文件仅仅保存数据记录的地址**。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key**可以重复。如果想在Col2上建立一个辅助索引,则此索引的结构如下图所示 :
同样也是一棵B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算
法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。
MyISAM的索引方式也叫做**“非聚集索引”**的
InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理的应用,从MySQL数据库5.5.8版本开始,InnoDB存储引擎是默认的存储引擎。InnoDB支持B+树索引、全文索引、哈希索引。但InnoDB使用B+Tree作为索引结构
时,具体实现方式却与MyISAM截然不同。
第一个区别是InnoDB的数据文件本身就是索引文件。MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而InnoDB索引,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保
存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
上图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录,这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
第二个区别是InnoDB的辅助索引data域存储相应记录主键的值而不是地址,所有辅助索引都引用主键作为data
域。
**聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录 **
像链表,顺序表这样的是否可以?
时间复杂度太高,不行
AVL树&红黑树是否可以?
数据量大时树的高度较高,增加了I/O次数
哈希表是否可以?
哈希表存在哈希冲突哈希只能支持 是或者不是。比如: where id =? 或者 where id =? 做不到范围查找: where id >10
为什么使用B+树,不适用B树?
聚簇索引和非聚簇索引的区别