平衡树:
平衡树是一种数据结构,它能够保持在插入或删除操作后,树的高度保持较小的增量,从而保持树的平衡性,提高了查找、插入和删除操作的效率。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: [] }