平衡树:AVL 树、红黑树、B 树、B+ 树

平衡树:

平衡树是一种数据结构,它能够保持在插入或删除操作后,树的高度保持较小的增量,从而保持树的平衡性,提高了查找、插入和删除操作的效率。AVL树、红黑树、B树以及B+树都是常见的平衡树的变种,它们在不同的应用场景中具有不同的优势和特点。

  • AVL 树在需要严格平衡的场景下使用,例如需要快速的插入、删除和查找操作,而对内存消耗要求较高的情况。

  • 红黑树是一种近似平衡的二叉搜索树,它在插入和删除操作上比 AVL 树更加高效,常用于各种编程语言的标准库中,例如 C++ 的 std::map 和 Java 的 TreeMap

  • B 树和 B+ 树广泛应用于数据库索引和文件系统中,它们能够有效地支持范围查询和顺序访问,并且能够处理大量数据。

​​​​​​​

AVL树(严格平衡的二叉搜索树)

AVL树是一种严格平衡的二叉搜索树,它要求任何节点的左子树和右子树的高度最多相差1。为了维持平衡,AVL树在插入或删除操作后会通过旋转操作来调整树的结构,以保持平衡性。AVL树的查询、插入和删除操作的时间复杂度都是O(log n)。

代码示例:

class AVLNode {
  /**
   * 构造函数,用于创建一棵二叉树节点
   *
   * @param key 节点的键值
   */
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
    this.height = 1;
  }
}

class AVLTree {
  /**
   * 构造函数
   *
   * @returns 无返回值
   */
  constructor() {
    this.root = null;
  }

  /**
   * 获取节点高度
   *
   * @param node 节点对象
   * @returns 返回节点高度,如果节点不存在则返回0
   */
  getHeight(node) {
    // 如果节点存在,则返回节点的高度,否则返回0
    return node ? node.height : 0;
  }

  /**
   * 获取节点的平衡因子
   *
   * @param node 节点对象
   * @returns 返回节点的平衡因子,若节点为空则返回0
   */
  getBalanceFactor(node) {
    // 如果节点存在,则返回节点的高度,否则返回0
    return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0;
  }

  /**
   * 更新节点高度
   *
   * @param node 节点对象
   */
  updateHeight(node) {
    // 更新节点的高度为左子树和右子树中较高的高度加1
    node.height = Math.max(this.getHeight(node.left), this.getHeight(node.right)) + 1;
  }

  /**
   * 将节点y进行右旋操作
   *
   * @param y 要进行右旋操作的节点
   * @returns 返回旋转后的新根节点
   */
  rotateRight(y) {
    // 将y的左子节点赋值给x
    let x = y.left;
    // 将x的右子节点赋值给T2
    let T2 = x.right;

    // 将y赋值给x的右子节点
    x.right = y;
    // 将T2赋值给y的左子节点
    y.left = T2;

    // 更新y的高度
    this.updateHeight(y);
    // 更新x的高度
    this.updateHeight(x);

    // 返回x
    return x;
  }

  /**
   * 左旋操作
   *
   * @param x 待旋转的节点
   * @returns 返回旋转后的新根节点
   */
  rotateLeft(x) {
    // 将节点x的右子节点赋值给y
    let y = x.right;
    // 将节点y的左子节点赋值给T2
    let T2 = y.left;

    // 将节点x赋值给节点y的左子节点
    y.left = x;
    // 将T2赋值给节点x的右子节点
    x.right = T2;

    // 更新节点x的高度
    this.updateHeight(x);
    // 更新节点y的高度
    this.updateHeight(y);

    // 返回旋转后的新根节点y
    return y;
  }

  /**
   * 向AVL树中插入节点
   *
   * @param node 当前节点
   * @param key 待插入节点的键值
   * @returns 返回插入后的节点
   */
  insert(node, key) {
    // 如果节点为空,则创建一个新的节点并返回
    if (!node) {
      return new AVLNode(key);
    }

    // 如果插入的键小于节点的键,则在左子树中插入
    if (key < node.key) {
      node.left = this.insert(node.left, key);
    }
    // 如果插入的键大于节点的键,则在右子树中插入
    else if (key > node.key) {
      node.right = this.insert(node.right, key);
    }
    // 如果插入的键等于节点的键,则直接返回该节点
    else {
      return node;
    }

    // 更新节点的高度
    this.updateHeight(node);

    // 获取节点的平衡因子
    let balance = this.getBalanceFactor(node);

    // 左左情况:左子树高度大于右子树高度2及以上,且插入的键小于左子树的最小键
    // 右旋
    // 左左情况
    if (balance > 1 && key < node.left.key) {
      return this.rotateRight(node);
    }

    // 右右情况:右子树高度大于左子树高度2及以上,且插入的键大于右子树的最大键
    // 左旋
    // 右右情况
    if (balance < -1 && key > node.right.key) {
      return this.rotateLeft(node);
    }

    // 左右情况:左子树高度大于右子树高度2及以上,且插入的键大于左子树的最小键
    // 先左旋,再右旋
    // 左右情况
    if (balance > 1 && key > node.left.key) {
      node.left = this.rotateLeft(node.left);
      return this.rotateRight(node);
    }

    // 右左情况:右子树高度大于左子树高度2及以上,且插入的键小于右子树的最大键
    // 先右旋,再左旋
    // 右左情况
    if (balance < -1 && key < node.right.key) {
      node.right = this.rotateRight(node.right);
      return this.rotateLeft(node);
    }

    // 返回插入后的节点
    return node;
  }
}

// 示例
const avlTree = new AVLTree();
avlTree.root = avlTree.insert(avlTree.root, 10);
avlTree.root = avlTree.insert(avlTree.root, 20);
avlTree.root = avlTree.insert(avlTree.root, 30);
console.log(avlTree.root); // AVLNode { key: 20, left: AVLNode { key: 10, ... }, right: AVLNode { key: 30, ... }, ... }

红黑树

红黑树是一种近似平衡的二叉搜索树,它通过一些附加的约束条件来保持树的大致平衡。红黑树的节点可以是红色或黑色,它要求在插入或删除操作后,树的黑色高度相同。红黑树通过颜色变换和旋转操作来调整树的结构,以满足约束条件。红黑树相对于AVL树来说,旋转操作更少,插入和删除操作更快,但是查询操作略慢一些。

代码示例:

const RED = 'red';
const BLACK = 'black';

class RedBlackNode {
  /**
   * 构造函数,用于创建一个节点对象
   *
   * @param key 节点的键值
   * @param color 节点的颜色
   */
  constructor(key, color) {
    this.key = key; // 设置当前节点的键值
    this.left = null; // 初始化左子节点为 null
    this.right = null;// 初始化右子节点为 null
    this.parent = null; // 初始化父节点为 null
    this.color = color;// 设置当前节点的颜色
  }
}

class RedBlackTree {
  /**
   * 构造函数
   */
  constructor() {
    // 初始化根节点为null
    this.root = null;
  }

  /**
    * 将二叉树节点左旋转
    *
    * @param node 需要左旋转的节点
    */
  rotateLeft(node) {
    // 获取当前节点的右子节点
    let rightChild = node.right;
    // 将当前节点的右子节点的左子节点赋值给当前节点的右子节点
    node.right = rightChild.left;
    // 如果当前节点的右子节点的左子节点不为空
    if (rightChild.left !== null) {
      // 将当前节点的右子节点的左子节点的父节点指向当前节点
      rightChild.left.parent = node;
    }

    // 将当前节点的右子节点的父节点指向当前节点的父节点
    rightChild.parent = node.parent;

    // 如果当前节点的父节点为空
    if (node.parent === null) {
      // 将当前节点的右子节点设置为根节点
      this.root = rightChild;
      // 如果当前节点是其父节点的左子节点
    } else if (node === node.parent.left) {
      // 将当前节点的父节点的左子节点设置为当前节点的右子节点
      node.parent.left = rightChild;
      // 如果当前节点是其父节点的右子节点
    } else {
      // 将当前节点的父节点的右子节点设置为当前节点的右子节点
      node.parent.right = rightChild;
    }

    // 将当前节点的右子节点的左子节点设置为当前节点
    rightChild.left = node;
    // 将当前节点的父节点设置为当前节点的右子节点
    node.parent = rightChild;
  }

  /**
   * 将节点右旋
   *
   * @param node 要旋转的节点
   */
  rotateRight(node) {
    // 获取当前节点的左子节点
    let leftChild = node.left;
    // 将当前节点的左子节点指向左子节点的右子节点
    node.left = leftChild.right;
    // 如果左子节点的右子节点不为空
    if (leftChild.right !== null) {
      // 将左子节点的右子节点的父节点指向当前节点
      leftChild.right.parent = node;
    }
    // 将左子节点的父节点指向当前节点的父节点
    leftChild.parent = node.parent;
    // 如果当前节点的父节点为空
    if (node.parent === null) {
      // 将根节点指向左子节点
      this.root = leftChild;
      // 如果当前节点是父节点的右子节点
    } else if (node === node.parent.right) {
      // 将父节点的右子节点指向左子节点
      node.parent.right = leftChild;
      // 否则
    } else {
      // 将父节点的左子节点指向左子节点
      node.parent.left = leftChild;
    }
    // 将左子节点的右子节点指向当前节点
    leftChild.right = node;
    // 将当前节点的父节点指向左子节点
    node.parent = leftChild;
  }

  /**
   * 插入节点
   *
   * @param key 节点值
   */
  insert(key) {
    // 创建一个新的红黑树节点,颜色为红色
    let newNode = new RedBlackNode(key, RED);

    // 将新节点插入到红黑树中,并返回更新后的根节点
    this.root = this.insertNode(this.root, newNode);

    // 修复红黑树的性质
    this.fixTreeProperties(newNode);
  }

  /**
   * 在二叉搜索树中插入节点
   *
   * @param root 二叉搜索树的根节点
   * @param node 要插入的节点
   * @returns 返回插入节点后的二叉搜索树的根节点
   */
  insertNode(root, node) {
    // 如果根节点为空,直接返回新节点作为根节点
    if (root === null) {
      return node;
    }
    // 如果新节点的键值小于根节点的键值,则递归地将新节点插入到左子树中
    if (node.key < root.key) {
      root.left = this.insertNode(root.left, node);
      // 将新节点的父节点设置为当前根节点
      root.left.parent = root;
      // 如果新节点的键值大于根节点的键值,则递归地将新节点插入到右子树中
    } else if (node.key > root.key) {
      root.right = this.insertNode(root.right, node);
      // 将新节点的父节点设置为当前根节点
      root.right.parent = root;
    }

    // 返回根节点
    return root;
  }

  /**
   * 修复树属性
   *
   * @param node 当前节点
   */
  fixTreeProperties(node) {
    // 当当前节点不是根节点且其父节点颜色为红色时,执行循环
    while (node !== this.root && node.parent.color === RED) {
      // 获取当前节点的父节点和祖父节点
      let parentNode = node.parent;
      let grandParentNode = node.parent.parent;

      // 如果父节点是祖父节点的左子节点
      if (parentNode === grandParentNode.left) {
        // 获取叔叔节点
        let uncleNode = grandParentNode.right;

        // 如果叔叔节点不为空且颜色为红色
        if (uncleNode !== null && uncleNode.color === RED) {
          // 将祖父节点颜色设为红色
          grandParentNode.color = RED;
          // 将父节点颜色设为黑色
          parentNode.color = BLACK;
          // 将叔叔节点颜色设为黑色
          uncleNode.color = BLACK;
          // 将当前节点更新为祖父节点,继续下一轮循环
          node = grandParentNode;
        } else {
          // 如果当前节点是父节点的右子节点
          if (node === parentNode.right) {
            // 执行左旋操作
            this.rotateLeft(parentNode);
            // 将当前节点更新为父节点
            node = parentNode;
            // 更新父节点为当前节点的父节点
            parentNode = node.parent;
          }
          // 执行右旋操作
          this.rotateRight(grandParentNode);
          // 交换父节点和祖父节点的颜色
          [parentNode.color, grandParentNode.color] = [grandParentNode.color, parentNode.color];
          // 将当前节点更新为父节点,继续下一轮循环
          node = parentNode;
        }
      } else {
        // 如果父节点是祖父节点的右子节点
        // 获取叔叔节点
        let uncleNode = grandParentNode.left;

        // 如果叔叔节点不为空且颜色为红色
        if (uncleNode !== null && uncleNode.color === RED) {
          // 将祖父节点颜色设为红色
          grandParentNode.color = RED;
          // 将父节点颜色设为黑色
          parentNode.color = BLACK;
          // 将叔叔节点颜色设为黑色
          uncleNode.color = BLACK;
          // 将当前节点更新为祖父节点,继续下一轮循环
          node = grandParentNode;
        } else {
          // 如果当前节点是父节点的左子节点
          if (node === parentNode.left) {
            // 执行右旋操作
            this.rotateRight(parentNode);
            // 将当前节点更新为父节点
            node = parentNode;
            // 更新父节点为当前节点的父节点
            parentNode = node.parent;
          }
          // 执行左旋操作
          this.rotateLeft(grandParentNode);
          // 交换父节点和祖父节点的颜色
          [parentNode.color, grandParentNode.color] = [grandParentNode.color, parentNode.color];
          // 将当前节点更新为父节点,继续下一轮循环
          node = parentNode;
        }
      }
    }
    // 将根节点颜色设为黑色
    this.root.color = BLACK;
  }
}

// 示例
const rbTree = new RedBlackTree();
rbTree.insert(10);
rbTree.insert(20);
rbTree.insert(30);
console.log(rbTree.root); // RedBlackNode { key: 20, color: 'black', ... }

B树

B树是一种多路搜索树,它的每个节点可以存储多个关键字,并且可以有多个子节点。B树的节点拥有较大的容量,使得每个节点的关键字数量相对较少,这样可以降低树的高度,提高查询效率。B树常用于文件系统和数据库中,用于索引大量数据的情况。

代码示例:

class BTreeNode {
  /**
   * 构造函数
   *
   * @param leaf 是否为叶子节点,默认为false
   */
  constructor(leaf = false) {
    this.keys = [];
    this.children = [];
    this.leaf = leaf;
  }
}

class BTree {
  /**
   * 构造函数
   *
   * @param degree 树的度数
   */
  constructor(degree) {
    this.root = null;
    this.degree = degree;
  }

  /**
   * 搜索指定节点
   *
   * @param key 要搜索的节点值
   * @returns 返回与key值相等的节点
   */
  search(key) {
    // 调用 searchNode 方法,传入根节点和关键字进行搜索
    return this.searchNode(this.root, key);
  }

  /**
   * 在 B-tree 节点中查找给定的键值,并返回该键值所在的节点
   *
   * @param node B-tree 节点
   * @param key 要查找的键值
   * @returns 返回找到的节点,如果未找到则返回 null
   */
  searchNode(node, key) {
    let i = 0;

    // 遍历节点的 keys 数组,查找 key 所在的索引位置
    while (i < node.keys.length && key > node.keys[i]) {
      i++;
    }

    // 如果找到了 key,并且 key 等于当前索引位置的元素,则返回当前节点
    if (i < node.keys.length && key === node.keys[i]) {
      return node;
      // 如果当前节点是叶子节点,则返回 null
    } else if (node.leaf) {
      return null;
      // 否则,在子节点中继续搜索 key
    } else {
      return this.searchNode(node.children[i], key);
    }
  }

  /**
   * 向B树中插入一个键值
   *
   * @param key 要插入的键值
   */
  insert(key) {
    // 如果根节点不存在
    if (!this.root) {
      // 创建一个新的B树节点作为根节点,并且标记为叶子节点
      this.root = new BTreeNode(true);
      // 将传入的键值添加到根节点的键值数组中
      this.root.keys.push(key);
    } else {
      // 如果根节点的键值数组已满(达到2倍的度数减1)
      if (this.root.keys.length === (2 * this.degree) - 1) {
        // 创建一个新的B树节点作为新的根节点,并且标记为非叶子节点
        let newRoot = new BTreeNode(false);
        // 将原来的根节点添加到新根节点的子节点数组中
        newRoot.children.push(this.root);
        // 调用splitChild方法拆分子节点
        this.splitChild(newRoot, 0, this.root);
        // 更新根节点为新创建的节点
        this.root = newRoot;
      }
      // 在非满的节点中插入键值
      this.insertNonFull(this.root, key);
    }
  }

  /**
   * 在非满节点中插入键值对
   *
   * @param node 节点对象
   * @param key 待插入的键
   */
  insertNonFull(node, key) {
    // 获取节点中键的最后一个索引
    let i = node.keys.length - 1;

    // 如果节点是叶子节点
    if (node.leaf) {
      // 当索引大于等于0且key小于当前索引对应的键时,索引减1
      while (i >= 0 && key < node.keys[i]) {
        i--;
      }
      // 在索引i+1的位置插入key
      node.keys.splice(i + 1, 0, key);
    } else {
      // 当索引大于等于0且key小于当前索引对应的键时,索引减1
      while (i >= 0 && key < node.keys[i]) {
        i--;
      }
      // 索引加1
      i++;
      // 如果当前索引对应的子节点的键的数量等于(2 * this.degree) - 1
      if (node.children[i].keys.length === (2 * this.degree) - 1) {
        // 调用splitChild方法拆分子节点
        this.splitChild(node, i, node.children[i]);
        // 如果key大于当前索引对应的键
        if (key > node.keys[i]) {
          // 索引加1
          i++;
        }
      }
      // 递归调用insertNonFull方法,将key插入到当前索引对应的子节点中
      this.insertNonFull(node.children[i], key);
    }
  }

  /**
   * 将子节点拆分为两个节点,并将拆分后的新节点插入到父节点中
   *
   * @param parent 父节点
   * @param index 父节点中子节点的索引
   * @param child 要拆分的子节点
   */
  splitChild(parent, index, child) {
    // 创建一个新的B树节点,叶子节点状态与child相同
    let newChild = new BTreeNode(child.leaf);
    // 计算中间索引位置
    let midIndex = Math.floor(this.degree - 1);

    // 将child节点的keys数组从中间索引位置后的部分赋值给newChild的keys数组
    newChild.keys = child.keys.splice(midIndex + 1);
    // 如果child节点不是叶子节点
    if (!child.leaf) {
      // 将child节点的children数组从中间索引位置后的部分赋值给newChild的children数组
      newChild.children = child.children.splice(midIndex + 1);
    }

    // 将child节点的keys数组中间索引位置的元素插入到parent节点的keys数组的指定位置
    parent.keys.splice(index, 0, child.keys[midIndex]);
    // 将newChild节点插入到parent节点的children数组的指定位置
    parent.children.splice(index + 1, 0, newChild);
  }
}

// 示例
const bTree = new BTree(2);
bTree.insert(10);
bTree.insert(20);
bTree.insert(5);
console.log(bTree.search(10)); // BTreeNode { keys: [ 10 ], children: [], leaf: true }
console.log(bTree.search(15)); // null

B+树

B+树是B树的一种变体,它与B树的区别在于B+树的非叶子节点不存储关键字的值,只存储关键字和指向子节点的指针。所有的叶子节点按顺序连接起来,形成一个有序链表,方便范围查询。B+树适用于数据库索引等需要范围查询的场景。

代码示例:

class BPlusTreeNode {
  /**
   * 构造函数
   */
  constructor() {
    this.keys = [];
    this.children = [];
  }
}

class BPlusTree {
  /**
   * 构造函数
   *
   * @param degree 树的度数
   */
  constructor(degree) {
    this.root = null;
    this.degree = degree;
  }

  /**
   * 搜索指定关键字
   *
   * @param key 关键字
   * @returns 返回搜索到的节点,若未找到则返回null
   */
  search(key) {
    // 调用 searchNode 方法进行搜索
    return this.searchNode(this.root, key);
  }

  /**
   * 在树中搜索节点
   *
   * @param node 当前节点
   * @param key 待搜索的键
   * @returns 返回找到的节点,若未找到则返回null
   */
  searchNode(node, key) {
    // 如果节点为空,则返回null
    if (!node) return null;
    // 初始化索引i为0
    let i = 0;

    // 循环遍历节点的keys数组,直到找到key应该插入的位置
    while (i < node.keys.length && key >= node.keys[i]) {
      i++;
    }
    // 如果节点没有子节点,则返回当前节点
    if (node.children.length === 0) {
      return node;
    }
    // 递归调用searchNode方法,在子节点中继续搜索
    return this.searchNode(node.children[i], key);
  }

  /**
   * 向B+树中插入键值
   *
   * @param key 键值
   */
  insert(key) {
    // 如果根节点不存在
    if (!this.root) {
      // 创建一个新的根节点
      this.root = new BPlusTreeNode();
      // 将键值添加到根节点的键值数组中
      this.root.keys.push(key);
    } else {
      // 如果根节点的键值数组已满
      if (this.root.keys.length === (2 * this.degree) - 1) {
        // 创建一个新的根节点
        let newRoot = new BPlusTreeNode();
        // 将原根节点添加到新根节点的子节点数组中
        newRoot.children.push(this.root);
        // 调用 splitChild 方法进行节点分裂
        this.splitChild(newRoot, 0, this.root);
        // 更新根节点为新创建的根节点
        this.root = newRoot;
      }
      // 调用 insertNonFull 方法将键值插入到非满的节点中
      this.insertNonFull(this.root, key);
    }
  }

  /**
   * 在非满节点中插入一个键值对
   *
   * @param node 当前节点
   * @param key 待插入的键
   */
  insertNonFull(node, key) {
    // 获取节点最后一个键的索引
    let i = node.keys.length - 1;
    // 如果节点没有子节点
    if (node.children.length === 0) {
      // 当索引大于等于0且key小于当前索引对应的键时,索引减1
      while (i >= 0 && key < node.keys[i]) {
        i--;
      }
      // 在索引i+1的位置插入key
      node.keys.splice(i + 1, 0, key);
    } else {
      // 当索引大于等于0且key小于当前索引对应的键时,索引减1
      while (i >= 0 && key < node.keys[i]) {
        i--;
      }
      // 索引加1,指向当前key应该插入的位置
      i++;
      // 如果当前子节点的键数量等于最大度减1
      if (node.children[i].keys.length === (2 * this.degree) - 1) {
        // 对当前子节点进行分裂
        this.splitChild(node, i, node.children[i]);
        // 如果key大于当前索引对应的键
        if (key > node.keys[i]) {
          // 索引加1,指向分裂后应该插入的位置
          i++;
        }
      }
      // 在指定子节点中递归插入key
      this.insertNonFull(node.children[i], key);
    }
  }

  /**
   * 将子节点从父节点中拆分,生成新的子节点并插入到父节点的子节点数组中
   *
   * @param parent 父节点
   * @param index 父节点中子节点的索引
   * @param child 要拆分的子节点
   */
  splitChild(parent, index, child) {
    // 创建一个新的B+树节点
    let newChild = new BPlusTreeNode();

    // 计算中间索引
    let midIndex = Math.floor(this.degree - 1);

    // 将child节点的keys数组从midIndex+1处开始的部分赋值给newChild节点的keys
    newChild.keys = child.keys.splice(midIndex + 1);

    // 如果child节点有子节点
    if (child.children.length !== 0) {
      // 将child节点的children数组从midIndex+1处开始的部分赋值给newChild节点的children
      newChild.children = child.children.splice(midIndex + 1);
    }

    // 将child节点的keys数组中midIndex位置的元素插入到parent节点的keys数组的index位置
    parent.keys.splice(index, 0, child.keys[midIndex]);

    // 将newChild节点插入到parent节点的children数组的index+1位置
    parent.children.splice(index + 1, 0, newChild);
  }
}

// 示例
const bPlusTree = new BPlusTree(2);
bPlusTree.insert(10);
bPlusTree.insert(20);
bPlusTree.insert(5);
console.log(bPlusTree.search(10)); // BPlusTreeNode { keys: [ 10 ], children: [] }
console.log(bPlusTree.search(15)); // BPlusTreeNode { keys: [ 10 ], children: [] }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿online

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值