🥳每日一练-B树节点的删除-JS不难版

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

上篇文章分享了 B树的创建--节点的插入,在此基础,再分享一篇关于 B树的内容--节点的删除

节点的删除比插入要更加复杂,有多种不同的情况:

  1. 删除叶子节点
    1. 节点关键词数量没有低于下限
    2. 节点关键词数量低于下限
      1. 向右兄弟借个关键词
        1. 右兄弟可以借
        2. 没有右兄弟,或者右兄弟关键词不够
      2. 向左兄弟借个关键词
        1. 左兄弟可以借
        2. 没有左兄弟,或者左兄弟关键词不够
      3. 左右兄弟都借不了,那就合并
  2. 删除非叶子节点,用叶子节点替代

捋一遍删除节点的过程

如果是删除非叶子节点 N1,和搜索二叉树一样,删除的节点用中序遍历的直接后驱 N2 替代,即 N2 从树上摘下来放在 N1 的位置 。 这样删除非叶子节点就转换成了删除叶子节点的情况

如果是删除叶子节点 N1,和搜索二叉树一样,直接从树上摘除就好了。不同的 B树还要检查 关键词的数量是否过少。上篇文章中提到节点关键词的数量不能少于 Math.ceil(level / 2)-1,如果少于这个数就需要借了。

  • 先看看有没有右兄弟,如果有右兄弟 N1R,看看 N1R的关键词是否大于Math.ceil(level / 2)-1,如果足够,就可以借了。怎么借?将父节点的一个关键词挪下来,然后把N1R 一个关键词放到父节点上。这样做是为了在调整关键词的位置之后,关键词之间的大小关系依旧符合多叉搜索树。我说的还是有些笼统,可以看下面的动图,会有更直观的感受
  • 如果 N1R 的主意打不了,就看看有没有左兄弟 N1L ,并且 N1L 能不能借,如果能借,就像下面这样:
  • 如果左右兄弟都借不了,应该怎么办呢?应该合并!先看有没有右兄弟 N1R 合并,下面是合并的动图:

上面合并过程中,会减少父节点的关键词,导致父节点需要进行借关键词,甚至合并关键词。过程和子节点相同。

  • 如果没有右兄弟,就合并左兄弟

上面的动图来自 B站王道考研数据结构,文字解释远不如咸鱼学长讲得好,大家有需求可以移步 B 站 观看视频:【王道计算机考研 数据结构】 7.4_2_B树的插入删除_哔哩哔哩

删除节点的过程都讲完了,情况比较多,但一一分析,还是不难的。

到这里还有一个问题,如果在合并的过程中,父节点原先只有一个节点的节点的话,把父节点的关键词借了,父节点的岂不就没有关键词了?是有这种情况,而且只发生在 B 树根节点。也就是说 B 树高度坍塌只发生在根节点身上。大家可以想一想为什么。

因为只有根节点才允许只有一个关键词。

下面来看代码实现

准备数据

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

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

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

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

上面代码准备了有一个 16 个节点 B树

具体的 insert 代码,可以看这篇文章:🥳每日一练-B树的创建-JS不难版 - 掘金

删除节点

javascript 复制代码
class BTree{
  //省略其他代码
  delete(value) {
		this._delete(null, this.root, value, null);
	}
  
/**
* 删除节点
* @param {BTreeNode} parent - 父节点
* @param {BTreeNode} node - 当前节点
* @param {number} value - 要删除的节点的值
* @param {number} index - 当前节点在父节点中的索引
*/
_delete(parent, node, value, index) {
   // 如果当前节点是叶子节点
   if (node.children.length == 0) {
       // 移除要删除的值
       node.keys = node.keys.filter((item) => item !== value);
   } else {
       // 遍历当前节点的子节点
       let flag = false;
       for (let i = 0; i < node.keys.length; i++) {
           // 如果要删除的值等于当前节点的键
           if (value == node.keys[i]) {
               // 获取子节点中的最小值节点
               const minNode = this.getMinNode(node.children[i + 1]);
               // 替换要删除的值,并递归删除最小值节点
               node.keys.splice(i, 1, minNode);
               this._delete(node, node.children[i + 1], minNode, i + 1);
               // 标记已删除
               flag = true;
               break;
           } else if (value < node.keys[i]) {
               // 递归删除要删除的值
               this._delete(node, node.children[i], value, i);
               // 标记已删除
               flag = true;
               break;
           }
       }
       // 如果未找到要删除的值,则删除子节点中的最后一个节点
       if (flag == false) {
           this._delete(node, node.children.slice(-1)[0], value, node.children.length - 1);
       }
   }
   // 判断当前节点的关键是否达标
   this.judgeLess(parent, node, index);
}

getMinNode(node){
  
}

judgeLess(parent, node, index){
  
}

delete 是供外部直接调用的方法,接收一个节点的值,表示需要删除该节点。

_delete 则是供内部调用的函数,用来具体实现删除的逻辑。接收四个参数,parentnodevalueindex

  • parent 表示 node 的父节点。因为节点调整的时候,需要父节点的参与,所以传入了 parent
  • node 表示当前被检查的节点
  • value 表示要删除关键词的值
  • index 表示 node 属于 parent 的哪个分支

node 可能是叶子节点,也可能不是。两者的删除逻辑不一样,所以需要加个判断。

  • 如果是叶子节点,就直接找出对应的关键词,并将其删除。(代码的逻辑有些瑕疵,直接 filter 是建立在删除的节点一定存在于 B树)
  • 如果不是叶子节点,
    • 先看 node 的关键词中是否有要被删除的,如果有,就用getMinNode 找到直接后驱,用 minNode 替代当前的 node,并且删除 minNode,所以代码中继续调用_delete,并且传入的 valueminNode

getMinNode 的逻辑是找到直接后驱,过程和搜索二叉树一致就不解释了

javascript 复制代码
/**
* 获取子节点中的最小值节点
* @param {BTreeNode} node - 子节点
*/
getMinNode(node) {
   // 一直向子节点查找,直到找到最小值节点
   while (node.children.length !== 0) {
       node = node.children[0];
   }
   // 返回最小值节点
   return node.keys[0];
}
  • 如果 node 中的关键词没有匹配上,就去 node 的分支继续找,在 node 的分支上执行删除操作
  • _delete 的后面有个judgeLess 函数调用,目的是在删除关键词之后,看看关键词的数量是否达标,如果不达标就采取借关键词的操作,或者合并节点的操作

判断节点的关键词是否过少

javascript 复制代码
/**
 * 判断当前节点是否需要分裂
 * @param {BTreeNode} parent - 父节点
 * @param {BTreeNode} node - 当前节点
 * @param {number} index - 当前节点在父节点中的索引
 */
judgeLess(parent, node, index) {
  if (parent == null) return node; // 如果是根节点,直接返回

  let haveRightSibling = false,
    haveLeftSibling = false;
  if (node.keys.length < this.minCount) {
    if (index != parent.children.length - 1) {
      haveRightSibling = true;
      // 存在右兄弟节点
      const siblingNodeRight = parent.children[index + 1];
      if (siblingNodeRight.keys.length > this.minCount) {
        // 向右兄弟借关键词
        this.blowFromRight(parent, node, index);
        return;
      }
    } else if (index != 0) {
      haveLeftSibling = true;
      // 存在左兄弟节点
      const siblingNodeLeft = parent.children[index - 1];
      if (siblingNodeLeft.keys.length > this.minCount) {
        // 从左兄弟借关键词
        this.blowFromLeft(parent, node, index);
        return;
      }
    }
    if (haveRightSibling) {
      // 合并右兄弟节点
      this.combineRightSibling(parent, node, index);
    } else {
      // 合并左兄弟节点
      this.combineLeftSibling(parent, node, index);
    }
  }
}

/**
	 *
	 * @param {BTreeNode} parent
	 * @param {BTreeNode} node
	 * @param {number} value
	 */
blowFromRight(parent, node, index) {
  const rightSibling = parent.children[index + 1];
  const rightFirstKey = rightSibling.keys.shift();
  const rightFirstChildren = rightSibling.children.shift();
  node.keys.push(parent.keys[index]);
  rightFirstChildren && node.children.push(rightFirstChildren);

  parent.keys.splice(index, 1, rightFirstKey);
}
/**
 *
 * @param {BTreeNode} parent
 * @param {BTreeNode} node
 * @param {number} value
 */
blowFromLeft(parent, node, index) {
  const leftSibling = parent.children[index - 1];
  const leftLastKey = leftSibling.keys.pop();
  const leftLastChildren = leftSibling.children.pop();
  node.keys.unshift(parent.keys[index - 1]);
  leftLastChildren && node.children.unshift(leftLastChildren);
  parent.keys.splice(index - 1, 1, leftLastKey);
}

/**
 *
 * @param {BTreeNode} parent
 * @param {BTreeNode} node
 * @param {number} value
 */
combineRightSibling(parent, node, index) {
  const rightSibling = parent.children[index + 1];
  node.keys.push(parent.keys[index], ...rightSibling.keys);
  node.children.push(...rightSibling.children);
  if (parent.keys.length == 1) {
    this.root = node;
    return;
  }
  parent.children.splice(index + 1, 1);
  parent.keys.splice(index, 1);
}
/**
 *
 * @param {BTreeNode} parent
 * @param {BTreeNode} node
 * @param {number} value
 */
combineLeftSibling(parent, node, index) {
  const leftSibling = parent.children[index - 1];
  node.keys.unshift(...leftSibling.keys, parent.keys[index - 1]);
  node.children.unshift(...leftSibling.children);
  if (parent.keys.length == 1) {
    this.root = node;
    return;
  }
  parent.children.splice(index - 1, 1);
  parent.keys.splice(index - 1, 1);
}

judgeLess 一开始判断如果是根节点,就直接返回,不做检查。因为根节点关键词永远不会过少,可能还会因为 parent==null 的特殊情况把代码搞复杂。

judgeLess 的逻辑和上文说的一致,先看有没有右兄弟,能不能借;再看有没有左兄弟,能不能借;如果都借不到,就看能不能和并右兄弟,否则合并左兄弟。

一个非根节点,左兄弟和右兄弟必有其中一个。所以上面一轮检查,肯定能解决关键词过少的问题。

剩下的四个函数blowFromRightblowFromLeftcombineRightSiblingcombineLeftSibling分别是调整的时候遇到的四种不同的情况,需要结合上面的动图才能理解。

其中的parent.keys.length == 1 判断,是看 parent 是不是根节点,如果是根节点, 那么合并节点必然会导致高度的坍塌,并且原先的 parent 需要摘出。所以就将 this.root 指向了新的节点 node,然后直接返回。

代码测试

javascript 复制代码
console.log(printTree(btree.root));

btree.delete(12);

res = "";
console.log(printTree(btree.root));

先打印出 B树原本的结构,然后删除关键词 12,再打印出 B 树的结构

这里用网站的结构图辅助证明代码的正确性:

这是网站的网址:B-Tree Visualization

再删除关键词 10

javascript 复制代码
btree.delete(10);

res = "";
console.log(printTree(btree.root));


删除节点 10,发生了节点调整,并且右节点含有三个关键词,可以借给左节点。

再删除关键词 1

javascript 复制代码
btree.delete(1);

res = "";
console.log(printTree(btree.root));


没问题,都和网站保持一致

注:代码和网站的策略有不一致的地方。

  1. 网站优先合并左节点
  2. 在删除非叶节点的时候,网站会找直接前驱,代码中找的是直接后驱

策略不同并不会影响代码的有效性,但测试过程会有出入,请悉知。

总结:

这篇文章分享了 如何删除 B 树的节点。节点的删除很简单,但节点的调整很麻烦。好在有 B站的王道数据结构视频和我的代码,为大家的学习保驾护航。一切都可以搞定😄

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

相关推荐
海的诗篇_2 分钟前
前端开发面试题总结-vue2框架篇(二)
前端·javascript·css·vue.js·前端框架·vue
Pu_Nine_92 分钟前
Vue 组合式 API 与 选项式 API 全面对比教程
前端·javascript·vue.js
IC 见路不走3 分钟前
LeetCode 第75题:颜色分类
数据结构·算法·leetcode
Allen Bright4 分钟前
【JS-2】JavaScript基础语法完全指南:从入门到精通
开发语言·javascript·ecmascript
Navigator_Z17 分钟前
LeetCode //C - 757. Set Intersection Size At Least Two
c语言·算法·leetcode
我不吃饼干17 分钟前
倒反天罡,CSS 中竟然可以写 JavaScript
前端·javascript·css
Blue.ztl1 小时前
DP刷题练习(二)
算法·cpp
青山是哪个青山1 小时前
位运,模拟,分治,BFS,栈和哈希表
算法·散列表·宽度优先
10年前端老司机2 小时前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
magic 2453 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax