B-Tree在不同的文献中的定义略显不同,所以在我初学B-Tree的时候非常困惑,知乎的一篇回答解答了我的困惑为什么 B-tree 在不同著作中度的定义有一定差别? - oldsharp的回答 - 知乎 ,本文将以算法导论中的定义来对B-Tree展开讨论。
在《算法导论》和《计算机程序设计艺术》一书中中对B-Tree的度的定义有略微的不同,在《算法导论》中定义了一个「最小度数t」表示「半满」状态,即最小孩子数,而2t则表示「全满」状态,即最大孩子数。
在《计算机程序设计艺术》一书中定义了一个「度m」表示「全满」状态,即最大孩子节点数,而ceil(m/2)则表示「半满」状态,即最小孩子数(ceil表示向上取整),相比于算法导论中定义的「度t」,《计算机程序设计艺术》一书中定义的「度m」更像是b树的阶,m代表m叉树。从定义可以看出,此种定义的「半满」状态(最小孩子节点数)是不稳定的,这取决于m的奇偶性。
关于这两种不同的定义,实现的最简的b树也是不同的,对于《算法导论》中的定义,一颗最简b树是一颗2-3-4树,而对于《计算机程序设计艺术》一书中的定义,一颗最简的b树是一颗2-3树。
推荐文章
在我学习B~Tree的时候,在网络上看到了一些不错的文章,接下来一一分享之,首先要理解mysql索引的本质是一种数据结构,而mysql的索引包括BTree索引、hash索引、全文索引等,本文主要讨论BTree索引,BTree索引是通过B+树实现的,所以要学习BTree索引第一步首先是要学习B~Tree,B-tree/B+tree/B*tree这篇文章非常简洁、明了、易懂的描述了B~Tree的结构及原理,在学习了B~Tree之后,MySQL索引背后的数据结构及算法原理这篇文章很好的分析了mysql的BTree索引实现以及一些mysql索引优化思路,mysql索引的实现是基于存储引擎的,这就意味着,对于不同的存储引擎,mysql的索引实现方式不同,在《MySQL索引背后的数据结构及算法原理》这篇文章中也分析了MyISAM 存储引擎中索引的实现以及InnoDB 存储引擎中索引实现的不同,有关mysql各类存储引擎的分析与比较,可以参看这篇文章Mysql 存储引擎。
使用java实现B-Tree
上面推荐的文章已经很好的解释了mysql索引及b~tree,在下也就不重复造轮子,本文主要分享一下我通过java语言实现的B-Tree,该实现是参照算法导论中的定义,但是在分裂时我选择在「全满」时就开始分裂,而不是等到溢出时再分裂,所以我虽然是参考算法导论中的定义,但是我实现的B-Tree的最简树是一颗2-3树(才疏学浅,实现略显拙劣,望大神指点),由于时间关系,我没有写删除操作,以下为代码:
/**
* Created by 落叶刻痕 on 2017/11/29.
* 本实现是基于算法导论中的定义:
* d是B-Tree的度,表示半满状态下孩子节点的个数,即非根内节点的最少孩子数量,所以d-1表示最少的key个数
* 2d表示 全满状态下孩子节点的个数,即非根内节点的最多孩子数量,所以2d-1表示最多的key个数
* put操作时,在全满时就开始分裂而不是等到溢出时再分裂
*/
public class BTree<K extends Comparable<K>, V> implements Map<K, V> {
private static final int DEFAULT_D = 2;
//d为大于1的一个正整数,称为B-Tree的度,d表示半满状态,d必须大于等于2
private final int d;
private final int minKeys;
private final int maxKeys;
//h为一个正整数,称为B-Tree的高度。
private int h;
//B-Tree的根节点
private Node root;
//B-Tree中元素个数
private int size;
public BTree(int d) {
if (d<2) throw new IllegalArgumentException("Illegal Capacity: "+
d);
this.d = d;
this.size = 0;
this.h = 1;
this.minKeys = d - 1;
this.maxKeys = 2 * d - 1;
this.root = new Node<>(maxKeys, null);
}
public BTree() {
this(DEFAULT_D);
}
@Override
public int size() {
return this.size;
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean containsKey(Object key) {
boolean hasKey = false;
Iterator> iterator = entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry = iterator.next();
if (entry.getKey().equals(key)) {
hasKey = true;
break;
}
}
return hasKey;
}
@Override
public boolean containsValue(Object value) {
boolean hasValue = false;
Iterator> iterator = entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry = iterator.next();
if (entry.getValue().equals(value)) {
hasValue = true;
break;
}
}
return hasValue;
}
@Override
public V get(Object key) {
assert key!=null;
if (!(key instanceof Comparable)){
throw new ClassCastException("key need to implements Comparable");
}
return find(key, root);
}
public V find(Object key, Node node) {
Entry[] entry = node.data;
for (int i = 0; i < node.getCount(); i++) {
if (((Comparable) key).compareTo(entry[i].getKey()) == 0){
return entry[i].getValue();
} else if (i == 0 && ((Comparable) key).compareTo(entry[0].getKey()) < 0 && node.children[0]!=null) {
return find(key, node.children[0]);
} else if (i < node.getCount() - 1 &&
((Comparable) key).compareTo(entry[i].getKey()) > 0 &&
((Comparable) key).compareTo(entry[i+1].getKey()) < 0) {
if (node.children[0]==null)
return null;
else
return find(key, node.children[i+1]);
} else if (i == node.getCount() - 1 &&
((Comparable) key).compareTo(entry[i].getKey()) > 0 &&
node.children[0]!=null) {
return find(key, node.children[i+1]);
}else {
System.out.println("---");
continue;
}
}
return null;
}
public V put(K key, V value) {
if (key == null) throw new NullPointerException("key can not be null!");
//插入元素
Node node = insert(this.root, key, value, 1);
//判断插入的节点是否需要分裂,当插入之后节点达到全满状态,立即分裂节点而不是等到溢出时再分裂
trySplit(node);
return value;
}
@Override
public V remove(Object key) {
//TODO remove(Object key)
return null;
}
@Override
public void putAll(Map extends K, ? extends V> m) {
//TODO putAll(Map extends K, ? extends V> m)
}
@Override
public void clear() {
//TODO clear()
}
@Override
public Set keySet() {
Set keySet = new TreeSet<>();
entrySet().forEach(entry -> keySet.add(entry.getKey()));
return keySet;
}
@Override
public Collection values() {
//TODO values()
return null;
}
@Override
public Set> entrySet() {
Set> es = new TreeSet<>();
Node node = this.root;
addToSet(node, es);
return es;
}
public void addToSet(Node node, Set> es){
if (node.isLeaf()){
for (int i = 0; i < node.count; i++) {
es.add(node.data[i]);
}
}else {
//先添加最左边子节点的元素到set
addToSet(node.children[0], es);
for (int i = 0; i < node.count; i++) {
//然后依次交替添加根节点、根节点右边子节点。
es.add(node.data[i]);
addToSet(node.children[i+1], es);
}
}
}
private void trySplit(Node node) {
if (node.count < node.data.length) {
return;
}
Node root = null;
if (node.parent == null) {
//由于没有父节点,所以重新生成一个父节点
root = new Node<>(maxKeys, null);
root.setLeaf(false);
//根节点变为新增的父节点
this.root = root;
//树的高度加一
this.h++;
} else {
root = node.parent;
}
//将node的parent指向父节点。
node.parent = root;
//取node节点的中间值
int index = (int) Math.ceil(node.count / 2.0) - 1;
//取出要放入父节点的值
Entry entry = node.data[index];
//清空node节点中的该值
node.data[index] = null;
node.count--;
//分裂之后的兄弟节点
Node brother = new Node<>(maxKeys, root);
//将node节点中index后面的元素放入兄弟节点brother中
for (int i = index + 1; i <= node.data.length; i++) {
if (i < node.data.length) {
brother.data[i - index - 1] = node.data[i];
brother.count++;
node.data[i] = null;
node.count--;
}
//将node节点index后的元素的孩子节点放入兄弟节点中
brother.children[i - index - 1] = node.children[i];
node.children[i] = null;
}
if (brother.children[0] != null) {
brother.setLeaf(false);
}
//将node节点中index位置的元素entry放入父节点root中
int parentIndex = addToParent(root, entry);
root.children[parentIndex] = node;
root.children[parentIndex + 1] = brother;
//如果分分裂后父节点也达到了分裂条件,则让父节点也分裂
trySplit(root);
}
int addToParent(Node node, Entry entry) {
//当为根节点时
if (node.count == 0) {
node.data[0] = entry;
node.count++;
return 0;
}
int index = -1;
for (int i = 0; i < node.count; i++) {
if (entry.getKey().compareTo(node.data[i].getKey()) <= 0) {
index = i;
break;
}
}
index = index == -1 ? node.count : index;
//将插入点后面的元素都后移一位
for (int i = node.count; i > index; i--) {
node.data[i] = node.data[i - 1];
node.children[i + 1] = node.children[i];
}
node.children[index + 1] = null;
//插入数据
node.data[index] = entry;
node.count++;
return index;
}
public Node insert(Node node, K key, V value, int ht) {
//当插入第一个值到根节点时
if (node.count == 0) {
Entry entry = new Entry<>(key, value);
node.data[0] = entry;
node.count++;
this.size++;
return node;
}
//在节点中寻找key应该放在哪个位置
int index = -1;
for (int i = 0; i < node.count; i++) {
if (key.compareTo(node.data[i].getKey()) <= 0) {
index = i;
break;
}
}
index = index == -1 ? node.count : index;
//找到了应该插入的位置,先判断是否到达最后一层,如果没有到达最后一层,即当前不是叶子节点,则递归到下一层
if (ht < this.h) {
Node child = node.children[index];
return insert(child, key, value, ht + 1);
} else if (ht == this.h) { //到达最底层即到达了叶子节点,将数据插入叶子节点
Entry entry = new Entry<>(key, value);
//将插入点后面的元素都后移一位
for (int i = node.count; i > index; i--) {
node.data[i] = node.data[i - 1];
// node.children[i+1] = node.children[i];
}
//插入数据
node.data[index] = entry;
node.count++;
this.size++;
return node;
}
return node;
}
static class Node<K extends Comparable<K>, V> {
Entry[] data;
Node[] children;
Node parent;
//该节点key的个数
int count;
//是否为叶子节点,默认为true
boolean leaf = true;
public int getCount() {
return count;
}
public boolean isLeaf() {
return leaf;
}
public void setLeaf(boolean leaf) {
this.leaf = leaf;
}
Node(int maxKeys, Node parent) {
this.data = (Entry[]) new Entry[maxKeys];
this.children = (Node[]) new Node[maxKeys + 1];
this.parent = parent;
count = 0;
}
}
static class Entry<K extends Comparable<K>, V> implements Map.Entry, Comparable<Entry<K, V>> {
K key;
V value;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
@Override
public Object setValue(Object value) {
this.value = (V) value;
return value;
}
/**
* 方便在遍历的时候将元素加入TreeSet集合中,其实根据中序遍历,
* 得到的元素已经根据key按照从小到大的顺序排好序了,所以完全可以用一个list或者数组来装
* @param o
* @return
*/
@Override
public int compareTo(Entry o) {
return o.getKey().compareTo(key)*(-1);
}
}
public int getHeight() {
return h;
}
}