【测的没问题,有问题麻烦告知或一起讨论】
【文章是从我笔记直接复制过来的,字体颜色标记都没了,看着眼花可以跳转我的笔记手搓B树总结笔记(电脑端)】
【下文所说的键值对就是代码中的Entry,关于Entry的比较说的是key的比较】
性质:b树是查找树,也是平衡树,它的所有叶子节点都在同一层上
相关概念:
1)树阶m:表示一个节点最多可以有m个子节点
2)一个节点中,最多只能有 m - 1 个键值对(k)
3)一个节点必须有 k + 1 或 0 个子节点,不能是其他个数的子节点
4)一个非根节点中最少有[m / 2.0] - 1个键值对(k),根节点中最少有一个键值对(k)
一、查找键值对
1)节点内查找:每个节点都能在自身键值对列表中查找(二分查找,返回比较结果>=0的最左边的索引)【细节:这个索引表示:键值对存在的位置索引 / 键值对不存在则需插入的位置索引 / 键值对会出现在当前节点的哪一个子节点内(直接表示子节点索引)】
2)树内查找:
○从根节点开始进行节点内查找,如果查找到存在,或者没有子节点了,则返回查找信息
○如果有子节点,则根据节点内查找返回的查找信息中的索引(此时的索引就表示 键值对会出现在当前调用查找的节点的哪一个子节点内(该索引直接表示子节点索引) ),向子节点递归查找;
二、添加键值对
思想:键值对数量多了,就找出中间键值对给父节点,然后自己拆分,再向父节点递归
概述:查找位置 -> 添加 -> 判断拆分(键值对数 > m - 1) -> 中间键值对给父节点 -> 拆分出新节点 -> 设置新节点的父节点 -> 设置新节点的子节点 -> 向父节点递归判断拆分
1)查找指定键值对在树中的情况。如果key重复,则替换value。如果不重复,则在所在节点(所在节点指:查找到键值对所在的节点)的 返回查找信息的索引的位置 插入该键值对
2)添加后立即判断是否拆分。如果添加后所在节点的 键值对数量 > m - 1,则需要拆分。
○确保有父节点:如果没有父节点,先new一个节点作为父节点,将所在节点加入新父节点的子节点列表中(直接加入即可,因为此时所在节点一定是新父节点的最左边的子节点)
○找到中间键值对:找到所在节点的键值对列表中的中间键值对(索引为size / 2)
○父节点加入中间键值对:在父节点键值对列表中查找中间键值对的位置,此时返回信息的索引表示的是 键值对不存在则需插入的位置索引。所在节点键值对列表删除中间键值对,并加入到父节点的键值对列表的该索引位置
○拆分键值对列表:进行拆分。new一个新节点作为所在节点拆分出的节点,将其父节点设置为所在节点的父节点。将所在节点的键值对列表从中间索引开始到结尾,加入到拆分的新节点的键值对列表中。(顺序加入即可)
○拆分子节点列表:如果所在节点存在子节点,则将子节点列表从 键值对列表中间索引+1 开始到结尾,加入new的节点的子节点列表中(因为中间索引被丢给父节点了,所在节点的子节点从该索引位置被分开,+1就是分开后右边节点(new的节点)的第一个子节点)。(顺序加入即可)
○拆分的新节点加入树:最后将new的节点加入所在节点的父节点的子节点列表中,位置索引为上一步父节点中查找返回的查找信息的索引+1,作为所在节点的兄弟节点(千万不可直接加在结尾)
3)向所在节点的父节点递归进行判断拆分
4)如果递归到最深层的节点的父节点为null,则代表该节点可能是上一层递归new的新父节点(参考2)的第一步),则该节点作为树的根节点(就算不是new的新父节点,其本身也是根节点)。
如果递归到最深层的节点的父节点不为null,则代表没递归到根节点就合法了,则还是返回原本的根节点root最为根节点;
三、删除键值对
思想:如果自己键值对少了,则向父节点拿取键值对,然后和兄弟节点合并,再向父节点递归
概述:查找 -> 删除 -> 判断合并(键值对数量 < [m / 2.0] - 1) -> 从父节点相应位置拿一个键值对 -> 与相邻兄弟节点合并 -> 向父节点递归判断合并
1)在树中查找指定要删除键值对,如果不存在,啥也不干。如果存在,则从所在节点的键值对列表中删除该键值对
2)删除后立即判断是否合并。如果删除后所在节点的键值对数量 < [m / 2.0] - 1,则需要向父节点拿取键值对
○转移:如果要删除的键值对在叶子节点中,直接往下进行。如果不在,则需查找小于该键值对的最大键值对的位置,将 要删除键值对 和小于该键值对的最大键值对 交换,将要删除键值对转移到叶子节点中
○基准key:如果要删除的键值对在叶子节点中,则该键值对本身的key就是基准key。如果在非叶子节点中,则小于该键值对的最大键值对的key为基准key
○查找所在节点是父节点的第几个子节点:先查找 基准key在父节点的键值对列表中的位置索引,此索引表示 键值对会出现在当前调用查找的节点的哪一个子节点内(该索引直接表示子节点索引) 也就是得知了所在节点是父节点的第几个子节点
○从父节点拿取键值对:此索引也表示 要拿取的键值对在父节点的位置索引 。通过这个索引判断,所在节点是否是父节点的最右子节点,是则拿取 第此索引 - 1个 键值对,否则拿取 第此索引个 键值对。
■将拿取的键值对从父节点键值对列表中删除并加入所在节点。
■如果所在节点是父节点的最右子节点,则将拿取的键值对加入到所在节点的第一个;否则加入到最后即可。
■(除了所在节点是父节点的最右子节点情况外,该索引也表示 父节点的键值对列表中比较结果>=1的最左边的键值对的位置索引,该键值对一定大于所在节点的所有键值对,所以加入到最后即可)
○确定合并的兄弟节点:通过这个索引判断,所在节点是否是父节点的最右子节点,是的话,则合并所在节点的左兄弟节点和所在节点。不是最右子节点的话,则合并所在节点和所在节点的右兄弟节点 (注意:传入合并方法的两个节点必须按照其在父节点的子节点列表中的顺序)
○合并:将node2的键值对加入到node1的键值对列表中。如果node2有子节点,则将node2的子节点加入到node1的子节点列表中(注意改变node2子节点的父节点为node1)。node2的父节点的子节点列表中删除node2(因为传入的时候是顺序传入的,所以node2的键值对和子节点肯定都在node1的键值对和子节点的右边,所以直接加入即可);
○判断拆分:这样合并有可能导致合并后的节点(就是node1,也是传入合并方法的第一个节点) 键值对数量 >m - 1,所以要在递归前进行一次判断拆分
3)向父节点递归判断合并,此时父节点这一层递归的基准key就是上一层被拿走的键值对的key
四、核心
添加的核心:查找所在节点的中间键值对在父节点的键值对列表的位置索引。该索引即可以确定中间键值对在父节点的插入位置,也可以得知所在节点是父节点的第几个子节点,同时索引+1就是所在节点拆分出的节点在父节点的子节点列表中的位置索引。所在节点的中间键值对的索引+1就是要分给拆分出的节点的子节点的开始索引
删除的核心:在所在节点的父节点查找基准key的位置索引,这个索引既能确定要拿取的键值对,也能确定所在节点的在是父节点的第几个子节点,同时也能确定要和哪个兄弟节点合并
五、代码
package btree;
import java.util.*;
/**
* @author Li
* @version 1.0
*/
public class B_Tree<k, v> {
//自定义树阶
private int m;
//最大键值对数
private int maxKeySize;
//最小键值对数
private int minKeySize;
//b树的根节点
private B_TreeNode<k, v> root;
//b树的节点个数
// private int size = 0;
//自定义比较器
private Comparator<k> comparator;
//构造器
public B_Tree() {
this(3);
}
public B_Tree(int m) {
this.m = m;
this.maxKeySize = this.m - 1;
this.minKeySize = (int)Math.ceil(m / 2.0) - 1;
}
public B_Tree(Comparator<k> comparator) {
this();
this.comparator = comparator;
}
public B_Tree(int m, Comparator<k> comparator) {
this(m);
this.comparator = comparator;
}
//根据是否传入比较器确定创建b数节点的方式
private B_TreeNode<k, v> creatNewNode(B_TreeNode<k, v> parent) {
return comparator == null ? new B_TreeNode<>(parent) :
new B_TreeNode<>(parent, comparator);
}
//在树中查询指定Entry
private SearchInfo<k, v> searchEntry(B_TreeNode<k, v> node, k key) {
//在当树中查找是否有该Entry
SearchInfo<k, v> searchInfo = node.search(key);
if (!searchInfo.isExist && !node.children.isEmpty()) {
//如果当前节点没有要查找的Entry,并且有子节点
//则向子节点递归查找
return searchEntry(node.children.get(searchInfo.index), key);
}
return searchInfo;
}
//添加新元素
public void put(k key, v value) {
//如果root为null,直接添加b树节点,在该节点的键值对列表中添加传入的键值对
if (root == null) {
root = creatNewNode(null);
root.entries.add(new Entry<>(key, value));
return;
}
put(root, key, value);
}
private void put(B_TreeNode<k, v> node, k key, v value) {
//查询要添加的Entry对在树中的情况
SearchInfo<k, v> addInfo = searchEntry(node, key);
if (addInfo.isExist) {
//如果key重复,替换value
addInfo.node.entries.get(addInfo.index).value = value;
} else {
//如果key不重复
//在索引位置将Entry加入搜索到的节点的键值对集合
addInfo.node.entries.add(addInfo.index, new Entry<>(key, value));
root = split(addInfo.node);
}
}
//拆分
private B_TreeNode<k, v> split(B_TreeNode<k, v> node) {
if (node.entries.size() > maxKeySize) {
//如果当前节点key数超出节点最大key数量
//如果当前节点没有根节点
if (node.parent == null) {
//创建一个节点,设为当前节点的根节点
node.parent = creatNewNode(null);
node.parent.children.add(node);
}
//找到当前节点键值对列表的midEntry
int midEntryIndex = node.entries.size() / 2;
Entry<k, v> midEntry = node.entries.get(midEntryIndex);
//在父节点中查询键值对位置
SearchInfo<k, v> searchInfo = node.parent.search(midEntry.key);
//在查询到的索引处添加midEntry
node.parent.entries.add(searchInfo.index, midEntry);
//从当前节点的键值对列表中删除midEntry
node.entries.remove(midEntryIndex);
//将mid索引后的Entry,都放到拆分出的新节点的键值对列表中
B_TreeNode<k, v> newNode = creatNewNode(node.parent);
//删除中间Entry后entries的大小
int nowEntriesSize = node.entries.size();
for (int i = midEntryIndex; i < nowEntriesSize; i++) {
newNode.entries.add(node.entries.get(midEntryIndex));
node.entries.remove(midEntryIndex);
}
if (!node.children.isEmpty()) {
//如果当前节点有子节点
int childrenSize = node.children.size();
for (int i = midEntryIndex + 1; i < childrenSize; i++) {
//从当前节点的子节点列表中删除这些子节点
B_TreeNode<k, v> changeChild = node.children.remove(midEntryIndex + 1);
//将这些子节点放在newNode的children中
newNode.children.add(changeChild);
//子节点的父节点设置为newNode
changeChild.parent = newNode;
}
}
//父节点的children中加入拆分出的新节点
node.parent.children.add(searchInfo.index + 1, newNode);
//向父节点递归进行拆分
return split(node.parent);
}
//如果parent为null,代表是新创建的根节点,加入键值对后要作为树的新的根节点
return node.parent == null ? node : root;
}
//删除节点
public void delete(k key) {
if (root == null) return;
delete(root, key);
}
private void delete(B_TreeNode<k, v> node, k key) {
//查找要删除的key
SearchInfo<k, v> deleteInfo = searchEntry(node, key);
if (deleteInfo.isExist) {
//如果在搜索到的节点找到要删除的key
//实际删除开始发生的节点(删除的Entry在非叶子节点上需要转移到叶子节点上删除)
B_TreeNode<k, v> deleteNode = deleteInfo.node;
//以比删除Entry小的最大Entry 或者删除的Entry自己为标准,在父节点查找索引位置
Entry<k, v> standerEntry = deleteInfo.node.entries.get(deleteInfo.index);
int deleteIndex = deleteInfo.index;
将要删除的节Entry与小于该Entry的最大Entry交换
if (!deleteNode.children.isEmpty()) {
//并且当前节点不是叶子节点,则将要删除的节Entry与小于该Entry的最大Entry交换
//找到小于该key中最大的key的Entry所在的节点
deleteNode = searchMaxOfLess(deleteNode.children.get(deleteInfo.index));
//从叶子节点中删除原本的最大的Entry
standerEntry = deleteNode.entries.remove(deleteNode.entries.size() - 1);
//将小于要删除Entry的最大Entry添加到要删除的Entry位置
deleteInfo.node.entries.add(deleteInfo.index, standerEntry);
//从所在节点的Entry列表中删除要删除的Entry
//将要删除的节点添加到当前叶子节点Entry列表的最后一个
deleteNode.entries.add(deleteInfo.node.entries.remove(deleteInfo.index + 1));
deleteIndex = deleteNode.entries.size() - 1;
}
deleteNode.entries.remove(deleteIndex);
//如果删除了Entry后当前节点的Entry数量小于minKeySize
//向父节点拉取Entry补上
root = getEntryFromParent(deleteNode, standerEntry.key);
//要是没搜索到指定Entry,则代表树中没有该Entry,啥也不做
}
}
//搜索并返回小于要删除Entry的最大Entry
private B_TreeNode<k, v> searchMaxOfLess(B_TreeNode<k, v> node) {
if (node.children.isEmpty()) {
//如果当前节点是叶子节点,则返回当前节点Entry列表的最后一个
return node;
} else {
return searchMaxOfLess(node.children.get(node.children.size() - 1));
}
}
//从父节点拉取Entry
private B_TreeNode<k, v> getEntryFromParent(B_TreeNode<k, v> deleteNode, k standerKey) {
//如果没有父节点了,则要开始向下出栈,返回树的新的根节点
if (deleteNode.parent == null) {
if (deleteNode.entries.isEmpty()) {
if (deleteNode.children.isEmpty()) {
return null;
} else {
deleteNode.children.get(0).parent = null;
return deleteNode.children.get(0);
}
} else {
return deleteNode;
}
}
if (deleteNode.entries.size() < minKeySize) {
//如果当前节点的Entry数量小于minKeySize
B_TreeNode<k, v> parent = deleteNode.parent;
//在父节点中查找删除的Entry在父节点中应该在的位置
SearchInfo<k, v> searchInfo = parent.search(standerKey);
//要从父节点拉下的Entry的索引位置
int getEntryIndex = searchInfo.index;
//父节点中被拉下的Entry
Entry<k, v> getEntry;
if (getEntryIndex == parent.children.size() - 1) {
//代表删除Entry所在节点没有右兄弟
//从父节点Entry列表中删除被拉的Entry,加入deleteNode的Entry中
getEntry = parent.entries.remove(--getEntryIndex);
//没有右兄弟,说明deleteNode中所有Entry都比父节点中的大,所以要添加在第一个
deleteNode.entries.add(0, getEntry);
} else {
//有右兄弟
//从父节点Entry列表中删除被拉的Entry,加入deleteNode的Entry中
getEntry = parent.entries.remove(getEntryIndex);
deleteNode.entries.add(getEntry);
}
//合并
merge(parent.children.get(getEntryIndex), parent.children.get(getEntryIndex + 1));
//判断合并后的节点的键值对数量是否超过m - 1,超过则要拆分
root = split(parent.children.get(getEntryIndex));
//向父节点递归判断Entry数量是否小于minKeySize
return getEntryFromParent(parent, getEntry.key);
}
return root;
}
//合并
private void merge(B_TreeNode<k, v> node1, B_TreeNode<k, v> node2) {
//合并Entry
node1.entries.addAll(node2.entries);
//合并children
if (!node2.children.isEmpty()) {
node1.children.addAll(node2.children);
for (int i = 0; i < node2.children.size(); i++) {
node2.children.get(i).parent = node1;
}
}
//丢弃node2
node2.parent.children.remove(node2);
node2.parent = null;
}
//层序遍历打印树
public void printTree() {
if (root == null) return;
Queue<B_TreeNode<k, v>> queue = new LinkedList<>();
queue.offer(root);
int currentLevelSize;
B_TreeNode<k, v> currentNode;
while (!queue.isEmpty()) {
currentLevelSize = queue.size();
for (int i = 0; i < currentLevelSize; i++) {
currentNode = queue.poll();
if (!currentNode.children.isEmpty()) {
for (int j = 0; j < currentNode.children.size(); j++) {
queue.offer(currentNode.children.get(j));
}
}
System.out.print(currentNode.entries);
System.out.print(" ");
}
System.out.println();
}
}
//b树的节点类
private class B_TreeNode<k, v> {
//b树节点的键值对列表
private final List<Entry<k, v>> entries;
//b树节点指向字节点的索引列表
private final List<B_TreeNode<k, v>> children;
//b树节点的根节点
private B_TreeNode<k, v> parent;
//自定义比较器
private Comparator<k> comparator;
public B_TreeNode(B_TreeNode<k, v> parent) {
entries = new ArrayList<>(B_Tree.this.m);
children = new ArrayList<>(B_Tree.this.m);
this.parent = parent;
}
public B_TreeNode(B_TreeNode<k, v> parent, Comparator<k> comparator) {
this(parent);
this.comparator = comparator;
}
//如果没有传入比较器,则需要存储的键实现Comparable接口,传入了比较器则使用比较器
private int compare(k k1, k k2) {
return comparator == null ? ((Comparable<k>) k1).compareTo(k2) : comparator.compare(k1, k2);
}
//在b树节点的键值对列表中查询键值对
//二分查找,查找>=传入键的最左边索引(等于则是重复key,不等于则是指向下一个b树节点的索引)
public SearchInfo<k, v> search(k key) {
int left = 0;
int right = entries.size() - 1;
int res = -1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (compare(entries.get(mid).key, key) >= 0) {
res = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
if (res == -1) {
//如果键值对列表中没有符合范围要求的键,则代表加入的键应该在最右边的子节点中,指向该节点的索引为entries.size()
return new SearchInfo<>(this, entries.size(), false);
} else {
if (compare(entries.get(res).key, key) == 0) {
//如果索引为res上的key是传入的key,则代表key重复
return new SearchInfo<>(this, res, true);
} else {
//如果索引为res上的key不是传入的key,则代表传入key<res上的key,
//加入的键应该在 索引res - 1 的子节点上
return new SearchInfo<>(this, res, false);
}
}
}
}
//在b树节点中的键值对列表中查找返回的信息类
private class SearchInfo<k, v> {
//键值对所在的节点
private B_TreeNode<k, v> node;
//键值对在节点键值对链表中的索引
private int index;
//键值对在节点键值对链表中是否存在
private boolean isExist;
public SearchInfo(B_TreeNode<k, v> node, int index, boolean isExist) {
this.node = node;
this.index = index;
this.isExist = isExist;
}
}
//b树节点中的键值对类
private static class Entry<k, v> {
private final k key;
private v value;
public Entry(k key, v value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "key=" + key + "|value=" + value;
}
}
}
若有收获,就点个赞吧