B树

5 篇文章 0 订阅

1 描述

B树中的一个结点存放多个元素,并允许存在多个子结点,元素与子结点的对应关系为N+1关系,如下图所示。
B树示例
上图秒速了一棵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中:点击跳转

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值