🥳每日一练-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站的王道数据结构视频和我的代码,为大家的学习保驾护航。一切都可以搞定😄

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

相关推荐
悦涵仙子32 分钟前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
兔老大的胡萝卜33 分钟前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
pianmian13 小时前
python数据结构基础(7)
数据结构·算法
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
好奇龙猫5 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
sp_fyf_20245 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
ChoSeitaku6 小时前
链表交集相关算法题|AB链表公共元素生成链表C|AB链表交集存放于A|连续子序列|相交链表求交点位置(C)
数据结构·考研·链表
偷心编程6 小时前
双向链表专题
数据结构