B树
1 描述
B树中的一个结点存放多个元素,并允许存在多个子结点,元素与子结点的对应关系为N+1关系,如下图所示。
上图秒速了一棵4阶B树(度最多为4),当前元素大于左子树中的任意元素,小于右子树中的任意元素。
1.1 性质
假设我们要构造一棵m阶B树,其存在如下性质:
- 根节点元素个数:1≤x≤m-1
- 非根节点元素个数:⌈m/2⌉-1≤x≤m-1
- 根节点子节点个数:2≤y≤m
- 非根节点子节点个数:⌈m/2⌉≤y≤m
2 操作流程
对于任意一棵B树,需要符合1.1中的性质。在插入和删除元素的过程中有可能破坏m阶B树的性质,因此需要对其进行一系列的调整。
2.1 添加元素
添加一个元素需从根结点开始遍历,直到找到对应的叶子结点进行添加。有如下两种情况:
- 正常添加,添加到叶子结点后,没有破坏m阶B树的性质。
- 添加上溢,添加到叶子结点后,破坏m阶B树的性质,结点的元素个数>m-1,需要对其进行调整。图示添加21、22两个元素后发生上溢,进而需要对其进行上溢调整。
- 上溢调整,上溢调整的主要操作为:将上溢结点中的最中间元素e选出,将其合并到父节点中,合并后依然保持有序,上溢结点将分裂成两个结点。当父节点添加被合并的元素可能继续发生上溢,父节点进行上述相同的操作即可,上溢现象可能会持续到根节点,最终B树高度加1,如下图所示。
2.2 删除元素
删除元素从根结点开始遍历,直到找到对应的元素进行删除,由于删除该元素后,结点中少了一个元素,其B树的组织结构发生破坏,因此需找到其前驱(或者后继)来代替被删除的元素,从而不破坏B树的组织结构。删除元素需要注意以下几个要点。
- 寻找前驱,若存在子树,则当前元素左边子树最右边的元素即为前驱元素。否则,若当前元素不是结点中的首个元素,则当前元素的前一个元素即为前驱,反之前驱在祖先结点中,沿着树形连线往上找,直到存在左边的元素即为前驱,否则没有前驱。
- 寻找后继,若存在子树,则当前元素右边子树最左边的元旦即为后继元素。否则,若当前元素不是结点中的末尾元素,则当前元素的后一个元素即为后继,反之后继在祖先结点中 ,沿着树形连线往上找,直到存在右边的元素即为后继,否则没有后继。
- 正常删除,删除元素后,没有破坏m阶B树的性质。
- 删除下溢,非根结点删除元素后,结点中的元素个数<⌈m/2⌉-1,破坏了m阶B树的性质,需要进行下溢调整。下图所示,删除22后发生下溢。
- 下溢调整,下溢调整主要操作为:如果兄弟节点至少有⌈m/2⌉个元素,则向兄弟节点借一个元素,父节点元素b插入到下溢节点最左位置,兄弟节点最大(左兄弟选最大,右兄弟选最小)元素a代替父节点元素b,删除a,兄弟的最右孩子要加入到当前节点中。若兄弟节点只有⌈m/2⌉-1个元素,将父节点元素b挪下来和左右子节点合并,下溢现象可能会持续到根节点,最终B树高度减1。如图所示,删除22,21后的下溢调整。
删除元素22后下溢调整。
删除元素21,发生下溢:
3 算法实现
下面使用Java语言描述m阶B树。
3.1 结点设计
对于m阶B树,我们应该注意其内部的元素的顺序和孩子节点的顺序,简单起见,这里在每次添加元素后进行排序。
private class BNode{
public List<E> elements; //元素集合
public List<BNode> childNodes; //孩子集合
public BNode parent; //父节点
private Comparator<BNode> comparator; //用于比较两个BNode结点的大小
public BNode(){
elements = new ArrayList<E>(maxElementSize);
childNodes = new ArrayList<BNode>(maxChildrenSize);
comparator = new Comparator<BNode>() {
@Override
public int compare(BNode o1, BNode o2) {
return BTree.this.compare(o1.getElement(0), o2.getElement(0));
}
};
}
public BNode(BNode parent){
this();
this.parent = parent;
}
/**
* 根据下标获取元素
* @param index
* @return
*/
public E getElement(int index){
return elements.get(index);
}
/**
* 根据下标获取孩子节点
* @param index
* @return
*/
public BNode getChild(int index){
return childNodes.get(index);
}
/**
* //批量添加孩子
* @param children
*/
public void addChildren(List<BNode> children){
for(BNode node:children){
node.parent = this;
}
childNodes.addAll(children);
Collections.sort(childNodes,comparator);
}
public void addElements(List<E> elements){
this.elements.addAll(elements);
Collections.sort(this.elements,new Comparator<E>() {
@Override
public int compare(E o1, E o2) {
return BTree.this.compare(o1, o2);
}
});
}
/**
* 在当前节点中查找元素对应的位置,成功找到则返回元素,否则返回元素该插入的位置
* @param element
* @return
*/
public SearchResult search(E element){
int low = 0;
int high = elements.size() - 1;
int mid = 0;
while(low <= high){
mid = (low + high) >>> 1;
E midElement = elements.get(mid);
int cmp = BTree.this.compare(element, midElement);
if(cmp < 0){
high = mid - 1;
}else if(cmp > 0){
low = mid + 1;
}else{
return new SearchResult(true,mid);
}
}
return new SearchResult(false,mid);
}
/**
* 添加一个元素
* @param element
* @return 如果元素存在,返回false,否则返回true
*/
public boolean add(E element){
SearchResult result = search(element);
if(result.searchStatus){
return false;
}else{
if(elements.size() == 0){ //结点中没有元素无法比较,直接添加
this.elements.add(0, element);
return true;
}
int index = search(element).index;
if(compare(element, elements.get(index)) > 0){
index ++;
}
this.elements.add(index, element);
return true;
}
}
/**
* 添加一个孩子
* @param child
*/
public void add(BNode child){
child.parent = this;
childNodes.add(child);
Collections.sort(childNodes,comparator);
}
/**
* 删除孩子节点
* @param child
*/
public boolean removeChild(BNode child){
return childNodes.remove(child);
}
/**
* 删除节点中的元素
*/
public boolean removeElement(E element){
SearchResult rs = search(element);
if(rs.searchStatus){
elements.remove(rs.index);
return true;
}else{
return false;
}
}
/**
* 获取元素个数
* @return
*/
public int getElementNum(){
return elements.size();
}
/**
* 获取孩子个数
* @return
*/
public int getChildNum(){
return childNodes.size();
}
/**
* 获取值最大的节点
* @return
*/
public E getMaxElement(){
return elements.get(elements.size() - 1);
}
/**
* 获取最右边的节点
* @return
*/
public BNode getRightestChild(){
if(childNodes.size() > 0){
return childNodes.get(childNodes.size() - 1);
}
return null;
}
}
3.2 获取前驱元素
private E predesessor(E element){
BNode node = node(element);
int index = node.search(element).index;
if(node.getChildNum() > 0){ //孩子存在,则前驱元素在孩子的最右
BNode p = node.getChild(index);
while(p.getChildNum() > 0){
p = p.getRightestChild();
}
return p.getMaxElement();
}else{ //孩子不存在,前驱元素在自身或者父节点中
if(index == 0){ //前驱元素在父节点中
BNode parent = node.parent;
if(parent == null){
return null;
}else{
int i = 0;
while((i = parent.search(element).index) == 0 && parent.parent != null){ //如果是在父节点的最左边,继续往上找
parent = parent.parent;
}
if(i == 0){
return null;
}else{
return parent.getElement(i - 1);
}
}
}else{
return node.getElement(index - 1);
}
}
}
3.3 获取后继元素
private E successor(E element){
BNode node = node(element);
int index = node.search(element).index;
if(node.getChildNum() > 0){ //孩子存在,则后继在孩子的最左
BNode p = null;
if(compare(element,node.getMaxElement())> 0){
p = node.getRightestChild();
}else{
p = node.getChild(index);
}
while(p.getChildNum() > 0){
p = p.getChild(0);
}
return p.getElement(0);
}else{ //后继在父节点或者自身中
if(index == node.getElementNum() - 1){
BNode parent = node.parent;
if(parent == null){
return null;
}else{
int i = 0;
while(compare(element,parent.getMaxElement()) > 0 && parent.parent != null){
parent = parent.parent;
}
if(compare(element,parent.getMaxElement())>0){
return null;
}else{
return parent.getElement(parent.search(element).index);
}
}
}else{
return node.getElement(index + 1);
}
}
}
3.4 添加
public boolean add(E element){
if(root == null){
root = new BNode(null);
root.add(element);
return true;
}
BNode node = root;
BNode parent = root;
while(node != null){
parent = node;
if(parent.getChildNum() == 0){ //叶子结点直接跳出进行添加
break;
}
if(compare(element, node.getMaxElement()) > 0){ //比结点中最大的元素还大
node = node.getChild(node.getChildNum() - 1);
}else{
SearchResult sr = node.search(element);
if(sr.searchStatus){ //不添加重复元素
return false;
}
node = node.getChild(sr.index);
}
}
parent.add(element);
if(parent.getElementNum() > this.maxElementSize){ //添加之后判断是否需要分裂
splitNode(parent);
}
size++;
return true;
}
3.5 上溢调整
private void splitNode(BNode node){
int splitIndex = node.getElementNum() / 2;
E splitElement = node.getElement(splitIndex);
BNode left = new BNode(); //分裂左结点
for(int i = 0;i < splitIndex; i++){
left.add(node.getElement(i));
}
if(node.getChildNum() > 0){
left.addChildren(node.childNodes.subList(0, splitIndex + 1)); //subList参数,from,to,不包含to
}
BNode right = new BNode(); //分裂右结点
for (int i = splitIndex + 1; i < node.getElementNum(); i++) {
right.add(node.getElement(i));
}
if(node.getChildNum() > 0){
right.addChildren(node.childNodes.subList(splitIndex + 1, node.getChildNum())); //subList参数,from,to,不包含to
}
if(node.parent != null){
BNode parent = node.parent;
parent.add(splitElement);
parent.removeChild(node);
parent.add(left);
parent.add(right);
if(parent.getElementNum() > maxElementSize){ //父节点上溢,继续递归操作
splitNode(parent);
}
}else{ //到达根结点
root = new BNode();
root.add(splitElement);
root.add(left);
root.add(right);
}
}
3.6 删除
public boolean remove(E element){
BNode node = node(element);
if(node == null){
return false;
}
if(node.getChildNum() > 0){ //不在叶子结点
E replacement = predesessor(element);
node.removeElement(element);
node.add(replacement);
element = replacement; //删除被替换结点
}
node = node(element); //在叶子结点中删除
if(node.parent == null && node.getElementNum() == 1){
root = null;
}else{
node.removeElement(element);
if(node.getElementNum() < minElementSize && node.parent != null){ //非根结点叶子下溢
combine(node);
}
}
size --;
return true;
}
3.7 下溢调整
private void combine(BNode node){
BNode parent = node.parent;
int index = parent.childNodes.indexOf(node);
int leftSiblingIndex = index - 1;
int rightSiblingIndex = index + 1;
BNode sibling = null;
if(leftSiblingIndex >= 0){ //左孩兄弟存在,从左兄弟中借一个结点
if((sibling = parent.getChild(leftSiblingIndex)).getElementNum() > minElementSize){ //兄弟有多余的结点
E siblingMaxElement = sibling.getMaxElement();
sibling.removeElement(siblingMaxElement);
if(sibling.childNodes.size() > 0){
BNode siblingRightestChild = sibling.getRightestChild();
sibling.removeChild(siblingRightestChild);
node.add(siblingRightestChild);
}
node.add(parent.getElement(leftSiblingIndex));
parent.elements.remove(leftSiblingIndex);
parent.add(siblingMaxElement);
return;
}
}
if(rightSiblingIndex < parent.getChildNum()){ //必有一个条件成立
if((sibling = parent.getChild(rightSiblingIndex)).getElementNum()> minElementSize){
E siblingMinElement = sibling.getElement(0);
sibling.removeElement(siblingMinElement);
if(sibling.childNodes.size() > 0){
BNode siblingLeftestChild = sibling.getChild(0);
sibling.removeChild(siblingLeftestChild);
node.add(siblingLeftestChild);
}
node.add(parent.getElement(rightSiblingIndex - 1));
parent.elements.remove(rightSiblingIndex - 1);
parent.add(siblingMinElement);
return;
}
}
int parentRemoveIndex = 0;
if(leftSiblingIndex >= 0){ //上面没有借到,从父节点拿一个下来合并
sibling = parent.getChild(leftSiblingIndex);
parentRemoveIndex = index - 1;
}else if(rightSiblingIndex < parent.getChildNum()){
sibling = parent.getChild(rightSiblingIndex);
parentRemoveIndex = index;
}
node.addElements(sibling.elements);
node.addChildren(sibling.childNodes);
node.add(parent.getElement(parentRemoveIndex));
parent.removeChild(sibling);
parent.elements.remove(parentRemoveIndex);
if(parent.parent == null && parent.getElementNum() == 0){
node.parent = null;
root = node;
}else if(parent.getElementNum() < minElementSize && parent.parent != null){
combine(parent);
}
}
4 应用场景
在磁盘IO中,每次都是读取一块数据到内存中,不论数据中的数据是否全部被用到,都将读取。例如,在磁盘中读取1个字节,但由于磁盘IO特性,每次都读取一块数据到内存中。对于传统的二叉树,每个节点中只存放了一个元素,每次的结点读写,都需要读取一大块内存,消耗大量IO性能。为此,使用B树,一个结点中保存多个数据,对一个结点的读写,可一次从磁盘中读取,进而可节省大量IO开销。因此,B树在数据库中被广泛应用。
5 完整代码
完整代码在我的Github中:点击跳转