🥳每日一练-B树的创建-JS不难版

如果你是一个熟悉 B 树的性质,正在寻找代码如何实现的同学,那这篇文章就是为你准备的

这篇文章来分享一个复杂度稍弱于红黑树难度的数据结构--B 树

什么是 B 树?

简单来说,B树是一个特殊的搜索多叉树。相比于搜索二叉树,搜索多叉树也满足左节点 < 根节点 < 右节这样的性质,与之不同的是搜索多叉树有多个分支,根节点的关键词也可以有多个,就像下图所示。

相对于普通的搜索树,B树的要求会更加严格。下面是 B树性质的介绍:

在这样的严格要求之下,对于 B树就可以推出它的核心特性:

上面的截图均来自 B站的王道数据结构视频课: 王道-7.4_2_B树的插入删除

假设一个 B树的阶是 5,那么根节点的关键词个数范围就是[1, m-1], 非根节点的关键词个数的范围是 [2, 4],任意非叶节点的分支个数的范围是 [3, 5]

并且所有的子树的高度都相同,这个比 AVL 平衡二叉树的要求更加严格。一颗 AVL 只要求左右子树的要读差不超过 1 。不过这样的严格要求也为代码的编写带来了方便。

下面我们一起来看看代码怎么写吧

准备数据

javascript 复制代码
class BTreeNode {
	constructor() {
		this.keys = [];
		this.children = [];
	}
}

class BTree {
	constructor(level) {
		this.root = new BTreeNode();
		this.level = level;
	}
  insert(){}
}

const data = Array(9)
	.fill(1)
	.map((item, index) => index);

const btree = new BTree(5);
data.forEach((item) => btree.insert(item));

先定义了 B树节点的数据结构,其中有 keys,用来存储节点的关键词;children 用来储存节点的分支。

然后定义了 BTree 的结构,用来构建 B树。

接下来就是实现 insert 方法了

创建 B树

javascript 复制代码
/**
* 向 B 树中插入一个新值。
* @param {*} value - 要插入到 B 树中的新值。
*/
insert(value) {
    // 调用私有方法 _insert,传入根节点和要插入的值。
    this._insert(null, this.root, value);
}

/**
* 私有方法,用于向 B 树中插入一个新值。
* @param {BTreeNode} parent - 当前插入节点的孩子节点的父节点。
* @param {BTreeNode} node - 当前插入节点。
* @param {*} value - 要插入到 B 树中的新值。
*/
_insert(parent, node, value) {
    // 如果当前节点的子节点数为 0,将新值添加为新的键。
    if (node.children.length == 0) {
        // 将新值添加为新的键。
        this.addKeys(node, value);
    } else {
        // 遍历当前节点的子节点。
        let flag = -1;
        for (let i = 0; i < node.keys.length; i++) {
            // 如果新值小于或等于当前键,将新值插入到对应子节点的键中。
            if (value <= node.keys[i]) {
                this._insert(node, node.children[i], value);
                flag = i;
                break;
            }
        }
        // 如果新值大于所有键,将其插入到子节点数组中的最后一个元素。
        if (flag == -1) {
            this._insert(node, node.children.slice(-1)[0], value);
        }
    }
    // 判断节点数量是否过多
    this.judgeOver();
}

judgeOver(){
  	...
}

/**
* Adds a new key to the current node.
* @param {BTreeNode} node - The current node.
* @param {*} value - The new key to be added.
*/
addKeys(node, value) {
  	...
}

上面的代码比较多,一起来分析分析

首先 insert 方法是提供外界调用的方法,接收一个节点的值。insert 内部调用 _insert 函数,实现节点的插入。
_insert 函数实现插入的过程和排序二叉树的类似,如果小于根节点,就往左节点插入;如果大于根节点,就往右节点插入。我这里支持相同节点的重复插入,所以插入的节点等于当前节点,也往左节点插入。

与排序二叉树不同的是,B树有多个关键词,也有多个分支,所以需要与各个关键词比较,才能知道要往哪个分支继续插入。代码中设置了 flag 变量,用来监控是否大于所有的关键词,如果 flag==-1,那就将 value 插入到最后一个分支中。

继续往子节点插入是通过递归调用_insert 方法实现的,非常方便。等到了叶子节点,也就是node.children.length == 0,就不能再往下了递归了。就在叶子节点的关键词中找个合适位置插入。

合适的位置就是使插入之后,叶子节点关键词依旧是有序的,即从左往右,依次增大。插入的逻辑是 addKeys 实现的,相当于一个插入排序的算法:

addKeys

javascript 复制代码
/**
* 向当前节点中添加一个新的键。
* @param {BTreeNode} node - 当前节点。
* @param {number} value - 要添加的新键。
*/
addKeys(node, value) {
    // 遍历当前节点的键数组,查找插入新键的位置。
    for (let i = 0; i < node.keys.length; i++) {
        // 如果新键小于或等于当前键,将新键添加到当前键数组中。
        if (value <= node.keys[i]) {
            // 将新键添加到当前键数组中。
            node.keys.splice(i, 0, value);
            return;
        }
    }
    // 如果新键大于所有键,将新键添加到子节点数组中的最后一个元素。
    node.keys.push(value);
}

在结束了插入节点的工作后,就要检查叶子节点是否超标了。上文提到,节点关键词的数量最多是 m-1,即比阶数小一。实现检查的逻辑是 judgeOver 实现的

judgeOver

javascript 复制代码
/**
* 检查当前节点是否有过多键。如果是,则分裂节点。
* @param {BTreeNode} parent - 当前节点的父节点。
* @param {BTreeNode} node - 当前节点。
*/
judgeOver(parent, node) {
    // 如果当前节点的键数超过最大允许的节点数,将节点分裂。
    if (node.keys.length >= this.level) {
        // 创建一个新的节点并将一些键和子节点移动到新节点中。
        this.splitNode(parent, node);
    }
}

/**
* 分裂一个节点,创建一个新的节点并将一些键和子节点移动到新节点中。
* @param {BTreeNode} parent - 当前节点的父节点。
* @param {BTreeNode} node - 当前节点。
* @returns {BTreeNode} - 新节点的父节点。
*/
splitNode(parent, node) {
   ...
}

如果node.keys.length >= this.level 就说明关键词多了,必须要差分节点,将节点的关键一分为二。

如何拆分呢?

将节点的关键词从中间破开,分成三份,左边 [0~mid-1), mid-1, (mid-1, level] mid 就是Math.ceil(level/ 2) - 1,翻译一下:阶数除以 2,向上取整,然后减一。

左边的关键词和右边的关键词各生成一个节点,中间的关键词上升到父节点的位置。除此之外,父节点还多了一个指向右边新节点的分支。

这就是拆分的过程。很简单吧

这个图也是从王道考研数据结构视频课中截取的,咸鱼学长可太厉害了,快去给他打 call 吧 7.4_2_B树的插入删除

splitNode

splitCode 代码实现:

javascript 复制代码
/**
* 分裂一个节点,创建一个新的节点并将一些键和子节点移动到新节点中。
* @param {BTreeNode} parent - 当前节点的父节点。
* @param {BTreeNode} node - 当前节点。
* @returns {BTreeNode} - 新节点的父节点。
*/
splitNode(parent, node) {
    // 如果父节点为空,则创建一个新的父节点。
    if (parent == null) {
        parent = new BTreeNode();
        this.root = parent;
    }
    // 计算分割键的索引。
    const mid = Math.ceil(node.keys.length / 2) - 1;
    // 将分割键添加到父节点中。
    this.addKeys(parent, node.keys[mid]);

    // 创建一个新的节点并将分割后的键和子节点移动到新节点中。
    const rightKeys = node.keys.slice(mid + 1);
    const rightChildren = node.children.slice(mid + 1);
    const newLeaf = new BTreeNode();
    newLeaf.keys = rightKeys;
    newLeaf.children = rightChildren;

    // 如果父节点没有子节点,将新节点添加为父节点的子节点。
    if (parent.children.length == 0) {
        parent.children.push(node, newLeaf);
    } else {
        // 否则,将新节点添加为父节点的子节点。
        parent.children.push(newLeaf);
    }
    // 从当前节点中移除分割键和子节点。
    node.keys = node.keys.slice(0, mid);
    node.children = node.children.slice(0, mid + 1);

}

代码不难,就是按照刚才讲的。将 node 一分为三,一左,一父,一右。

方法开头的判断parent == null,是为了拆分根节点做处理的。如果 node 为根节点,那么 parent 就一定为 null,所以需要创建一个父节点,并将 root 指向 parent

拆分节点会增加父节点的关键词,所以检查完当前节点后,还需要检查父节点是否 Over。怎么实现呢?其实递归的_insert 已经实现了。当子节点 judgeOver 之后,就会回到父节点的_insert 函数,从而对父节点进行judgeOver。这就是递归的好处

完整代码

代码讲完了,来看看完整代码:

javascript 复制代码
class BTree {
  constructor(level) {
    this.root = new BTreeNode();
    this.level = level;
  }
  /**
* 向 B 树中插入一个新值。
* @param {*} value - 要插入到 B 树中的新值。
*/
  insert(value) {
    // 调用私有方法 _insert,传入根节点和要插入的值。
    this._insert(null, this.root, value);
  }

  /**
* 私有方法,用于向 B 树中插入一个新值。
* @param {BTreeNode} parent - 当前插入节点的孩子节点的父节点。
* @param {BTreeNode} node - 当前插入节点。
* @param {*} value - 要插入到 B 树中的新值。
*/
  _insert(parent, node, value) {
    // 如果当前节点的子节点数为 0,将新值添加为新的键。
    if (node.children.length == 0) {
      // 将新值添加为新的键。
      this.addKeys(node, value);
    } else {
      // 遍历当前节点的子节点。
      let flag = -1;
      for (let i = 0; i < node.keys.length; i++) {
        // 如果新值小于或等于当前键,将新值插入到对应子节点的键中。
        if (value <= node.keys[i]) {
          this._insert(node, node.children[i], value);
          flag = i;
          break;
        }
      }
      // 如果新值大于所有键,将其插入到子节点数组中的最后一个元素。
      if (flag == -1) {
        this._insert(node, node.children.slice(-1)[0], value);
      }
    }
    // 如果当前节点的键数超过最大允许的节点数,将节点分裂。
    this.judgeOver(parent, node);
  }

  /**
* 检查当前节点是否有过多键。如果是,则分裂节点。
* @param {BTreeNode} parent - 当前节点的父节点。
* @param {BTreeNode} node - 当前节点。
*/
  judgeOver(parent, node) {
    // 如果当前节点的键数超过最大允许的节点数,将节点分裂。
    if (node.keys.length >= this.level) {
      // 创建一个新的节点并将一些键和子节点移动到新节点中。
      this.splitNode(parent, node);
    }
  }

  /**
* 分裂一个节点,创建一个新的节点并将一些键和子节点移动到新节点中。
* @param {BTreeNode} parent - 当前节点的父节点。
* @param {BTreeNode} node - 当前节点。
* @returns {BTreeNode} - 新节点的父节点。
*/
  splitNode(parent, node) {
    // 如果父节点为空,则创建一个新的父节点。
    if (parent == null) {
      parent = new BTreeNode();
      this.root = parent;
    }
    // 计算分割键的索引。
    const mid = Math.ceil(node.keys.length / 2) - 1;
    // 将分割键添加到父节点中。
    this.addKeys(parent, node.keys[mid]);

    // 创建一个新的节点并将分割后的键和子节点移动到新节点中。
    const rightKeys = node.keys.slice(mid + 1);
    const rightChildren = node.children.slice(mid + 1);
    const newLeaf = new BTreeNode();
    newLeaf.keys = rightKeys;
    newLeaf.children = rightChildren;

    // 如果父节点没有子节点,将新节点添加为父节点的子节点。
    if (parent.children.length == 0) {
      parent.children.push(node, newLeaf);
    } else {
      // 否则,将新节点添加为父节点的子节点。
      parent.children.push(newLeaf);
    }
    // 从当前节点中移除分割键和子节点。
    node.keys = node.keys.slice(0, mid);
    node.children = node.children.slice(0, mid + 1);

  }
  /**
* 向当前节点中添加一个新的键。
* @param {BTreeNode} node - 当前节点。
* @param {*} value - 要添加的新键。
*/
  addKeys(node, value) {
    // 遍历当前节点的键数组,查找插入新键的位置。
    for (let i = 0; i < node.keys.length; i++) {
      // 如果新键小于或等于当前键,将新键添加到当前键数组中。
      if (value <= node.keys[i]) {
        // 将新键添加到当前键数组中。
        node.keys.splice(i, 0, value);
        return;
      }
    }
    // 如果新键大于所有键,将新键添加到子节点数组中的最后一个元素。
    node.keys.push(value);
  }

}

测试一下:

javascript 复制代码
const data = Array(9)
	.fill(1)
	.map((item, index) => index);

const btree = new BTree(5);
data.forEach((item) => btree.insert(item));

创建了一个从 0 到 8 的数据,然后将这 9 个数字依次插入到 B树中,将 B树打印出来看看:

javascript 复制代码
let res = "";

const printTree = (data, deeps = [1]) => {
	if (!data) return res;
	let space = deeps
		.slice(0, -1)
		.map((item) => {
			return item == 1 ? "|\t\t" : "\t\t";
		})
		.join("");

	space += "|__";
	const content = data.keys.join(", ");
	res = res + space + content + "\n";

	if (!data) return res;
	data.children.forEach((item, index) => {
		printTree(item, [...deeps, index == data.children.length - 1 ? 0 : 1]);
	});
	return res;
};

printTree(btree.root);

console.log(res);

printTree 是一个将 B 树结构在控制台以树形结构输出的函数,下面是打印结果:

确实满足 B树的性质,但打印结果也不能说明一切,来个更具说服力的证据

这是一个线上的数据结构网站,我依次插入了 9 个数字,B树的结构和我控制台打印出来的结构一模一样。

这是网址,感兴趣的网页可以去看看:B-Tree Visualization

再来看几组:



完美,一摸一样,代码完全正确🙆

总结:

这篇文章分享了一个比较有难度的数据结构--B树

如果你是一个熟悉 B 树的性质,正在寻找代码如何实现的同学,那这篇文章就是为你准备的;当然如果你仅有一些 B 树的基础,文中的代码你还是可以看得懂的。如果你一点 B 树的基础都没有,建议先看 B 栈王道考研数据结构的视频,对 B 树有个大致的了解,再来看这篇文章,相信你会很有收获

下篇文章,我将会分享 B 树节点删除,这个比 B树创建更加复杂。不过我相信,只要你认真看,还是可以拿下的

你觉得这篇文章怎么样?喜欢就点赞+关注吧

相关推荐
是小Y啦7 分钟前
leetcode 106.从中序与后续遍历序列构造二叉树
数据结构·算法·leetcode
liuyang-neu17 分钟前
力扣 42.接雨水
java·算法·leetcode
子非鱼92117 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
y_dd24 分钟前
【machine learning-12-多元线性回归】
算法·机器学习·线性回归
m0_6312704024 分钟前
标准c语言(一)
c语言·开发语言·算法
万河归海42825 分钟前
C语言——二分法搜索数组中特定元素并返回下标
c语言·开发语言·数据结构·经验分享·笔记·算法·visualstudio
小周的C语言学习笔记29 分钟前
鹏哥C语言36-37---循环/分支语句练习(折半查找算法)
c语言·算法·visual studio
y_dd29 分钟前
【machine learning-七-线性回归之成本函数】
算法·回归·线性回归
想退休的搬砖人31 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
小魏冬琅1 小时前
K-means 算法的介绍与应用
算法·机器学习·kmeans