B+树增删改查的可视化

477 阅读3分钟

B+ 树是一种高效的数据结构,在实现过程中需要考虑到多个因素,比如节点的分裂合并、数据的查找插入删除等。实现思路如下:

  1. 创建一个 BplusTree 类,包含根节点和度数
  2. 定义节点类,包括节点类型、键值数组、指针数组等属性和方法
  3. 实现 B+ 树的查找算法,遍历节点进行递归查找
  4. 实现 B+ 树的插入算法,遍历节点寻找合适的位置进行插入,如果节点已满则进行分裂操作
  5. 实现 B+ 树的删除算法,遍历节点寻找要删除的键值,如果是叶子节点直接删除,否则将其后代节点的最小值替换到该节点中
  6. 实现 B+ 树的更新算法,首先进行查找操作,然后用插入删除的方式更新节点

代码如下

class Node {
  constructor(isLeaf = false) {
    this.isLeaf = isLeaf;
    this.keys = [];
    this.values = [];
    this.parent = null;
    this.next = null;
  }
}

class BPlusTree {
  constructor(degree) {
    this.root = new Node(true);
    this.degree = degree;
  }

  insert(key, value) {
    let node = this.root;
    while (!node.isLeaf) {
      let i = 0;
      while (i < node.keys.length && node.keys[i] < key) {
        i++;
      }
      node = node.values[i];
    }
    this.insertIntoNode(node, key, value);
  }

  insertIntoNode(node, key, value) {
    let i = 0;
    while (i < node.keys.length && node.keys[i] < key) {
      i++;
    }
    node.keys.splice(i, 0, key);
    node.values.splice(i, 0, value);
    if (node.keys.length === this.degree) {
      this.splitNode(node);
    }
  }

  splitNode(node) {
    let right = new Node(node.isLeaf);
    right.next = node.next;
    node.next = right;
    let midIndex = Math.floor(node.keys.length / 2);
    let midKey = node.keys[midIndex];
    for (let i = midIndex; i < node.keys.length; i++) {
      right.keys.push(node.keys[i]);
      right.values.push(node.values[i]);
      if (!right.isLeaf) {
        right.values[i - midIndex].parent = right;
      }
    }
    node.keys.length = midIndex;
    node.values.length = midIndex;
    if (node === this.root) {
      this.createNewRoot(node, midKey, right);
    } else {
      let parent = node.parent;
      this.insertIntoNode(parent, midKey, right);
    }
  }

  createNewRoot(left, key, right) {
    let newRoot = new Node();
    newRoot.keys.push(key);
    newRoot.values.push(left);
    newRoot.values.push(right);
    left.parent = newRoot;
    right.parent = newRoot;
    this.root = newRoot;
  }

  delete(key) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    if (index !== -1) {
      node.keys.splice(index, 1);
      node.values.splice(index, 1);
      this.rebalance(node);
    }
  }

  find(key) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    return index !== -1 ? node.values[index] : null;
  }

  findLeafNodeWithIndex(key) {
    let node = this.root;
    let index = -1;
    while (!node.isLeaf) {
      let i = 0;
      while (i < node.keys.length && node.keys[i] < key) {
        i++;
      }
      node = node.values[i];
    }
    index = node.keys.findIndex(k => k === key);
    return [node, index];
  }

  update(key, value) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    if (index !== -1) {
      node.values[index] = value;
    }
  }

  rebalance(node) {
    if (node === this.root) {
      if (node.keys.length === 0 && !node.isLeaf) {
        this.root = node.values[0];
        this.root.parent = null;
      }
      return;
    }
    if (node.keys.length < Math.ceil(this.degree / 2) - 1) {
      let parent = node.parent;
      let leftSibling = node.prev();
      let rightSibling = node.next;
      let leftDiff = leftSibling !== null ? node.keys.length - leftSibling.keys.length : Infinity;
      let rightDiff = rightSibling !== null ? node.keys.length - rightSibling.keys.length : Infinity;
      if (leftSibling !== null && leftDiff <= rightDiff) {
        this.borrowFromLeft(leftSibling, node, parent);
      } else if (rightSibling !== null) {
        this.borrowFromRight(rightSibling, node, parent);
      } else if (leftSibling !== null) {
        this.merge(leftSibling, node, parent);
      }
      this.rebalance(parent);
    }
  }

  borrowFromLeft(left, node, parent) {
    let borrowIndex = left.keys.length - 1;
    let key = left.keys[borrowIndex];
    let value = left.values[borrowIndex];
    left.keys.length--;
    left.values.length--;
    node.keys.splice(0, 0, key);
    node.values.splice(0, 0, value);
    if (!node.isLeaf) {
      value.parent = node;
    }
    parent.keys[parent.values.indexOf(node)] = node.keys[0];
  }

  borrowFromRight(right, node, parent) {
    let borrowIndex = 0;
    let key = right.keys[borrowIndex];
    let value = right.values[borrowIndex];
    right.keys.splice(borrowIndex, 1);
    right.values.splice(borrowIndex, 1);
    node.keys.push(key);
    node.values.push(value);
    if (!node.isLeaf) {
      value.parent = node;
    }
    parent.keys[parent.values.indexOf(right)] = right.keys[0];
  }

  merge(left, node, parent) {
    let mergeIndex = parent.values.indexOf(left);
    let keyIndex = mergeIndex > 0 ? mergeIndex - 1 : 0;
    let key = parent.keys[keyIndex];
    left.keys.push(key, ...node.keys);
    left.values.push(...node.values);
    for (let i = 0; i < node.values.length; i++) {
      if (!node.isLeaf) {
        node.values[i].parent = left;
      }
    }
    parent.keys.splice(keyIndex, 1);
    parent.values.splice(mergeIndex, 1);
    node.keys.length = 0;
    node.values.length = 0;
  }
}

Node.prototype.prev = function () {
  let curr = this;
  if (curr.isLeaf) {
    while (curr !== null && curr.keys.length === 0) {
      curr = curr.parent;
    }
    if (curr === null) {
      return null;
    }
    let index = curr.next.values.findIndex(v => v === this);
    if (index > 0) {
      return curr.next.values[index - 1];
    } else {
      return curr;
    }
  } else {
    return curr.values[curr.values.length - 1];
  }
};

Node.prototype.next = function () {
  let curr = this;
  if (curr.isLeaf) {
    while (curr !== null && curr.next !== null && curr.keys.length === 0) {
      curr = curr.next;
    }
    return curr.next;
  } else {
    return curr.values[0];
  }
};

要在HTML上可视化B+树的增删改查,可以利用HTML5中的Canvas元素。首先需要在HTML中嵌入一个Canvas元素:

<canvas id="myCanvas" width="800" height="600"></canvas>

然后,在JavaScript中写一个B+树类,并实现相应的方法,例如insert、delete、find和update。在调用这些方法时,可以在Canvas上绘制出B+树的图形以进行可视化展示。

下面是一个简单的示例,基于上面提供的 B+ 树实现代码以进行可视化展示。代码使用了Fabric.js库来简化Canvas的操作。

// 创建Canvas对象
var canvas = new fabric.Canvas('myCanvas');

// B+树节点大小和间距
var nodeSize = 40;
var levelGap = 80;
var siblingGap = 20;

// B+树节点模板
var nodeTemplate = new fabric.Rect({
  width: nodeSize,
  height: nodeSize / 2,
  fill: 'white',
  stroke: 'black',
  strokeWidth: 2,
  originX: 'center',
  originY: 'center',
});

// B+树指针模板
var pointerTemplate = new fabric.Line([0, 0, 0, nodeSize], {
  stroke: 'black',
  strokeWidth: 2,
});

// B+树类
class BPlusTree {
  constructor(degree) {
    this.root = new Node(true);
    this.degree = degree;
  }

  insert(key, value) {
    let node = this.root;
    while (!node.isLeaf) {
      let i = 0;
      while (i < node.keys.length && node.keys[i] < key) {
        i++;
      }
      node = node.values[i];
    }
    this.insertIntoNode(node, key, value);
    this.draw();
  }

  insertIntoNode(node, key, value) {
    let i = 0;
    while (i < node.keys.length && node.keys[i] < key) {
      i++;
    }
    node.keys.splice(i, 0, key);
    node.values.splice(i, 0, value);
    if (node.keys.length === this.degree) {
      this.splitNode(node);
    }
  }

  splitNode(node) {
    let right = new Node(node.isLeaf);
    right.next = node.next;
    node.next = right;
    let midIndex = Math.floor(node.keys.length / 2);
    let midKey = node.keys[midIndex];
    for (let i = midIndex; i < node.keys.length; i++) {
      right.keys.push(node.keys[i]);
      right.values.push(node.values[i]);
      if (!right.isLeaf) {
        right.values[i - midIndex].parent = right;
      }
    }
    node.keys.length = midIndex;
    node.values.length = midIndex;
    if (node === this.root) {
      this.createNewRoot(node, midKey, right);
    } else {
      let parent = node.parent;
      this.insertIntoNode(parent, midKey, right);
    }
  }

  createNewRoot(left, key, right) {
    let newRoot = new Node();
    newRoot.keys.push(key);
    newRoot.values.push(left);
    newRoot.values.push(right);
    left.parent = newRoot;
    right.parent = newRoot;
    this.root = newRoot;
  }

  delete(key) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    if (index !== -1) {
      node.keys.splice(index, 1);
      node.values.splice(index, 1);
      this.rebalance(node);
      this.draw();
    }
  }

  find(key) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    return index !== -1 ? node.values[index] : null;
  }

  findLeafNodeWithIndex(key) {
    let node = this.root;
    let index = -1;
    while (!node.isLeaf) {
      let i = 0;
      while (i < node.keys.length && node.keys[i] < key) {
        i++;
      }
      node = node.values[i];
    }
    index = node.keys.findIndex(k => k === key);
    return [node, index];
  }

  update(key, value) {
    let [node, index] = this.findLeafNodeWithIndex(key);
    if (index !== -1) {
      node.values[index] = value;
      this.draw();
    }
  }

  rebalance(node) {
    if (node === this.root) {
      if (node.keys.length === 0 && !node.isLeaf) {
        this.root = node.values[0];
        this.root.parent = null;
      }
      return;
    }
    if (node.keys.length < Math.ceil(this.degree / 2) - 1) {
      let parent = node.parent;
      let leftSibling = node.prev();
      let rightSibling = node.next;
      let leftDiff = leftSibling !== null ? node.keys.length - leftSibling.keys.length : Infinity;
      let rightDiff = rightSibling !== null ? node.keys.length - rightSibling.keys.length : Infinity;
      if (leftSibling !== null && leftDiff <= rightDiff) {
        this.borrowFromLeft(leftSibling, node, parent);
      } else if (rightSibling !== null) {
        this.borrowFromRight(rightSibling, node, parent);
      } else if (leftSibling !== null) {
        this.merge(leftSibling, node, parent);
      }
      this.rebalance(parent);
    }
  }

  borrowFromLeft(left, node, parent) {
    let borrowIndex = left.keys.length - 1;
    let key = left.keys[borrowIndex];
    let value = left.values[borrowIndex];
    left.keys.length--;
    left.values.length--;
    node.keys.splice(0, 0, key);
    node.values.splice(0, 0, value);
    if (!node.isLeaf) {
      value.parent = node;
    }
    parent.keys[parent.values.indexOf(node)] = node.keys[0];
  }

  borrowFromRight(right, node, parent) {
    let borrowIndex = 0;
    let key = right.keys[borrowIndex];
    let value = right.values[borrowIndex];
    right.keys.splice(borrowIndex, 1);
    right.values.splice(borrowIndex, 1);
    node.keys.push(key);
    node.values.push(value);
    if (!node.isLeaf) {
      value.parent = node;
    }
    parent.keys[parent.values.indexOf(right)] = right.keys[0];
  }

  merge(left, node, parent) {
    let mergeIndex = parent.values.indexOf(left);
    let keyIndex = mergeIndex > 0 ? mergeIndex - 1 : 0;
    let key = parent.keys[keyIndex];
    left.keys.push(key, ...node.keys);
    left.values.push(...node.values);
    for (let i = 0; i < node.values.length; i++) {
      if (!node.isLeaf) {
        node.values[i].parent = left;
      }
    }
    parent.keys.splice(keyIndex, 1);
    parent.values.splice(mergeIndex, 1);
    node.keys.length = 0;
    node.values.length = 0;
  }

  // 在Canvas上绘制B+树
  draw() {
    canvas.clear();
    let nodes = [];
    let pointers = [];
    this.traverse(this.root, nodes, pointers, 0);
    for (let i = 0; i < nodes.length; i++) {
      canvas.add(nodes[i]);
    }
    for (let i = 0; i < pointers.length; i++) {
      canvas.add(pointers[i]);
    }
  }

  // 中序遍历B+树,生成节点和指针对象数组
  traverse(node, nodes, pointers, level) {
    let x = nodeSize / 2 + (node.keys.length + 1) * siblingGap / 2;
    let y = level * levelGap + nodeSize / 2;
    let currNode = new fabric.Text(node.keys.join('\n'), {
      left: x,
      top: y,
      fontFamily: 'Arial',
      fontSize: 16,
      originX: 'center',
      originY: 'center',
    });
    let currPointer = null;
    if (!node.isLeaf) {
      for (let i = 0; i < node.values.length; i++) {
        let childNode = node.values[i];
        this.traverse(childNode, nodes, pointers, level + 1);
        let childX = childNode.prev() ? childNode.prev().left + nodeSize + siblingGap : x - (node.keys.length - i) * (nodeSize + siblingGap);
        currPointer = new fabric.Line([x, y, childX + nodeSize / 2, (level + 1) * levelGap], {
          stroke: 'black',
          strokeWidth: 2,
          originX: 'center',
          originY: 'center',
        });
        pointers.push(currPointer);
      }
    }
    currNode.set({ left: x, top: y });
    nodes.push(nodeTemplate.clone().set({ left: x, top: y }));
  }
}

// 定义B+树节点类
class Node {
  constructor(isLeaf = false) {
    this.isLeaf = isLeaf;
    this.keys = [];
    this.values = [];
    this.parent = null;
    this.next = null;
  }

  prev() {
    let curr = this;
    if (curr.isLeaf) {
      while (curr !== null && curr.keys.length === 0) {
        curr = curr.parent;
      }
      if (curr === null) {
        return null;
      }
      let index = curr.next.values.findIndex(v => v === this);
      if (index > 0) {
        return curr.next.values[index - 1];
      } else {
        return curr;
      }
    } else {
      return curr.values[curr.values.length - 1];
    }
  }

  next() {
    let curr = this;
    if (curr.isLeaf) {
      while (curr !== null && curr.next !== null && curr.keys.length === 0) {
        curr = curr.next;
      }
      return curr.next;
    } else {
      return curr.values[0];
    }
  }
}

// 创建B+树对象
var bptree = new BPlusTree(4);

// 监听HTML上的按键事件
document.addEventListener('keydown', function(event) {
  var key = parseInt(event.key);
  if (!Number.isNaN(key)) { // 如果按下的是数字键
    if (event.shiftKey) { // 如果同时按下了Shift键,则执行删除操作
      bptree.delete(key);
    } else if (event.ctrlKey) { // 如果同时按下了Ctrl键,则执行更新操作
      bptree.update(key, 'value' + key);
    } else { // 否则执行插入操作
      bptree.insert(key, 'value' + key);
    }
  }
});

使用上述代码,当用户在HTML页面中按下数字键时,会在B+树中插入相应的键值对;按下Shift加数字键时,会删除对应的键值对;按下Ctrl加数字键时,会更新对应键值对的值。同时,B+树的结构会在Canvas上进行实时绘制,以方便用户观察。