前言
数据结构知识点总结文档:树结构。该文档总结了3种经典的树结构,包括他们各自的原理,以及代码实现。
目录
1. 树
1.1 树的种类
- 二叉搜索树
- 红黑树
- AVL树
- B树
- B+树
······
1.2 树的遍历
- 前序遍历(pre-order)
- DLR,先遍历根节点,接着是左子树,最后是右子树。
- 可分别使用递归和栈结构来实现:
/* Recursive Solution */
void PreOrder(Node* ptr)
{
visit(root);
PreOrder(root->left);
PreOrder(root->right);
}
/* Stack Solution */
while(stack.empty() == false)
{
root = stack.top()
visit(root);
stack.pop();
stack.push(root->right); // Stack is FIFO, hence we need to push right child first.
stack.push(root->left);
}
- 中序遍历(in-order)
- LDR,先遍历左子树,接着是根节点,最后是右子树。
- LDR,先遍历左子树,接着是根节点,最后是右子树。
- 后序遍历(post-order)
- LRD,先遍历左子树,接着是右子树,最后是根节点。
- LRD,先遍历左子树,接着是右子树,最后是根节点。
- 广度优先搜索(Breadth-First Search)
- 先遍历每层的所有节点,完成遍历后才会进入下一层遍历。
- 在代码实现中,通常使用单向队列(queue)来实现。
while(queue.empty() == false)
{
node = queue.head();
visit(node);
queue.enqueue(node->left);
queue.enqueue(node->right);
queue.dequeue();
}
- 深度优先搜索(Depth-First Search)
- 从根节点一直向下探查,抵达叶节点后切换到另一条路径探查,直到抵达另一个叶节点。
- 在代码实现中,通常使用栈(stack)搭配前序遍历来实现。
while(stack.empty() == false)
{
root = stack.top()
visit(root);
stack.pop();
stack.push(root->right); // Stack is FIFO, hence we need to push right child first.
stack.push(root->left);
}
2. 二叉搜索树
2.1 基本知识点
- 对于每个节点,如果不是叶子节点,那么只能有最多两个子节点。
- 若节点的左子树存在,则左子树中的每一个节点值都不大于该节点值,若节点的右子树存在,则右子树中的每一个节点值都不小于该节点值。
- 假设二叉搜索树有
N
N
N 个节点,那么最优深度出现在二叉树平衡分布时,为
log
(
N
+
1
)
\log(N+1)
log(N+1)。 最差深度出现在线性分布时,为
N
N
N。
- 推导:完美二叉搜索树的节点数为 N = 2 d − 1 N=2^d - 1 N=2d−1,但由于底层未必被叶节点填满,因此 N ≤ 2 d − 1 ⟹ log ( N + 1 ) ≤ d N \le 2^d - 1 \ \implies \ \log(N+1) \le d N≤2d−1 ⟹ log(N+1)≤d
2.2 查询节点
- 二叉搜索树的查询操作比较简单,仅需要从根节点开始向下遍历整棵树,让需要查询的值与途中遍历到的每个节点的值相比较,如果相等则返回当前节点;若待查询值大于节点值,则遍历节点的右子树;若待查询值小于节点值,则遍历节点的左子树;若节点不存在于树中,则返回空指针。
- 在具体的代码实现中,即可通过迭代的方式,也可通过递归的方式来遍历二叉搜索树。一般来说,递归会消耗更多资源,性能较低,但由于其能够保留遍历中探查到的每一个节点,因此在某些需要记录遍历路径的情况下具有优势。
- 当二叉搜索树平衡分布时为 O ( log ( n ) ) O(\log(n)) O(log(n))。当二叉搜索树线性分布时为 O ( n ) O(n) O(n)
2.3 插入节点
- 在插入新节点前,需要先查询到新节点所应该插入的位置。查询的方法和上述的操作是一样的。查询到合适的位置后,由于插入的位置必定为上一个节点的某个空子节点,即新节点必将以叶节点的身份被插入到树中。因此可直接构造新的节点,令新节点与上一个节点相连即可。
- 与查询复杂度类似,插入新的节点仅需要找到节点应属的位置即可,因此平衡分布时为 O ( log ( n ) ) O(\log(n)) O(log(n))。线性分布时为 O ( n ) O(n) O(n)
2.4 删除节点
- 二叉搜索树删除节点需要先查询到指定节点再执行删除。删除时存在三种情况:
- 第一种情况: 若待删除节点为叶节点,则可直接删除。
- 第二种情况: 若待删除节点只有一个子节点。则删除该节点,令该节点的父节点指向该节点的子节点。
- 第三种情况: 若待删除节点有两个子节点。先找到左子树中的最大节点(或右子树中的最小节点),用最大节点值替代当前节点值,然后删除最大节点。
- 在具体的代码实现中,需要单独处理待删除节点为根节点的情况。
- 因为删除操作为常数复杂度。第一、二种情况下只查询一次,第三种情况查询两次。所以删除复杂度与查询复杂度类似,为 O ( log ( n ) ) O(\log(n)) O(log(n)) 或 O ( n ) O(n) O(n)。
2.5 代码实现
- 树节点实现
/* Data structure for tree node */
struct Node
{
double data;
Node* left;
Node* right;
Node* parent;
Node() {
data = 0.0;
left = nullptr;
right = nullptr;
parent = nullptr;
}
Node(double data_ = 0.0,
Node* left_ = nullptr,
Node* right_ = nullptr,
Node* parent_ = nullptr) {
data = data_;
left = left_;
right = right_;
parent = parent_;
}
};
- 树实现
/* Data Structure for Binary Search Tree */
struct Tree
{
int size;
Node* root;
Tree() {
root = nullptr;
size = 0;
}
/* Method for finding a specific node, return NULL if not exist */
Node* Find(double value, Node* node) {
while (node != nullptr && value != node->data ) {
node = (value > node->data) ? node->right : node->left;
}
return node;
}
/* Method for finding the minimum node */
Node* FindMin(Node* node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
/* Method for finding the maximum node */
Node* FindMax(Node* node) {
while (node->right != nullptr) {
node = node->right;
}
return node;
}
/* Method for inserting new node */
void Insert(double value) {
Node* locator = this->root;
Node* temp = nullptr;
while (locator != nullptr) // Find the right place to insert new node
{
temp = locator;
locator = (value > locator->data) ? locator->right : locator->left;
}
Node* newNode = new Node(value, nullptr, nullptr, temp);
if (locator == this->root)
this->root = newNode;
else if (value > temp->data)
temp->right = newNode;
else
temp->left = newNode;
this->size += 1;
return;
}
/* Method for deleting specific node */
Node* Delete(double value) {
Node* node = Find(value, this->root);
Node* parent = node->parent;
if (node->left == nullptr && node->right == nullptr) { // Case 1
if (node == this->root)
this->root = nullptr;
else if (parent->right == node)
parent->right = nullptr;
else
parent->left = nullptr;
}
else if (node->right == nullptr || node->left == nullptr) { // Case 2
Node* child = (node->right == nullptr) ? node->left : node->right;
if (parent->right == node)
parent->right = child;
else
parent->left = child;
}
else { // Case 3
Node* minNode = FindMin(node->right);
node->data = minNode->data;
if (minNode->right != nullptr)
minNode->parent->left = minNode->right;
else
minNode->parent->left = nullptr;
}
this->size -= 1;
return node;
}
void PrintTree(Node* node) {
if (node != nullptr)
std::cout << node->data << " ";
else
return;
PrintTree(node->left);
PrintTree(node->right);
return;
}
};
3. 红黑树
3.1 基本知识点
- 红黑树本质上是能够自动平衡的二叉搜索树,所有红黑树均满足以下五条规则:
- 每个节点要么是红色,要么是黑色。
- 红黑树的根节点必须是黑色。
- 红黑树的叶节点(NIL)必须是黑色(这里的叶节点不是指普通树的实体叶节点,而是实体叶节点下抽象出的为空的叶节点)
- 如果一个节点为红色,则其子节点必须为黑色。
- 对任意节点,从该节点到其所有叶节点的路径上,黑色节点的数量必须一样。
- 假设红黑树有 N N N 个节点,那么红黑树深度最深为 2 log ( N + 1 ) 2 \log(N+1) 2log(N+1)。
3.2 左旋操作
| |
| |
X Y
/ \ left rotate X / \
/ \ ----------------> / \
a Y X c
/ \ / \
/ \ / \
b c a b
- 伪代码可表示为
LEFT-ROTATE(T, x):
y ← right[x] # 前提:这里假设x的右子节点为y。下面开始正式操作
right[x] ← left[y] # 将 “y的左子节点” 设为 “x的右子节点”,即将β设为x的右子节点
p[left[y]] ← x # 将 “x” 设为 “y的左子节点的父节点”,即将β的父节点设为x
p[y] ← p[x] # 将 “x的父节点” 设为 “y的父节点”
if p[x] = nil[T]:
then root[T] ← y # 情况1:如果 “x的父节点” 是空节点,则将y设为根节点
else if x = left[p[x]]
then left[p[x]] ← y # 情况2:如果 x是它父节点的左子节点,则将y设为“x的父节点的左子节点”
else right[p[x]] ← y # 情况3:(x是它父节点的右子节点) 将y设为“x的父节点的右子节点”
left[y] ← x # 将 “x” 设为 “y的左子节点”
p[x] ← y # 将 “x的父节点” 设为 “y”
3.3 右旋操作
| |
| |
Y X
/ \ right rotate Y / \
/ \ ----------------> / \
X c a Y
/ \ / \
/ \ / \
a b b c
- 伪代码可表示为
RIGHT-ROTATE(T, y):
x ← left[y] # 前提:这里假设y的左子节点为x。下面开始正式操作
left[y] ← right[x] # 将 “x的右子节点” 设为 “y的左子节点”,即 将β设为y的左子节点
p[right[x]] ← y # 将 “y” 设为 “x的右子节点的父节点”,即 将β的父节点设为y
p[x] ← p[y] # 将 “y的父节点” 设为 “x的父节点”
if p[y] = nil[T]:
then root[T] ← x # 情况1:如果 “y的父节点” 是空节点,则将x设为根节点
else if y = right[p[y]]:
then right[p[y]] ← x # 情况2:如果 y是它父节点的右子节点,则将x设为“y的父节点的左子节点”
else left[p[y]] ← x # 情况3:(y是它父节点的左子节点) 将x设为“y的父节点的左子节点”
right[x] ← y # 将 “y” 设为 “x的右子节点”
p[y] ← x # 将 “y的父节点” 设为 “x”
3.4 查询节点
- 由于红黑树本质上是二叉搜索树,因此节点的排列规律与二叉搜索树一致,因此红黑树的查询复杂度也为 O ( log ( N ) ) O(\log(N)) O(log(N))
3.5 插入节点
- 红黑树插入新节点的步骤分为三步。
- 第一步: 按照二叉搜索树的规则,把新节点插入到树中。
- 第二步: 把新的节点着色为"红色"。
- 为何要着色为"红色"? 把新节点着色为红色,能够避免违反红黑树的第5条规则,即任意节点到其所有叶节点的路径上,黑节点的数量必须一样。少违背一条规则,后续需要处理的情况就越少。
- 第三步: 着色后,红黑树的第2,3,4条规则不会违背,但第5条规则有可能违背。因此需要着色后需要检测新的红黑树是否违背第5条规则,若存在违背,则需调整红黑树。
- 对于第三步,需要分为三种情况处理。
- 第一种情况: 插入节点是根节点,则直接把新节点着色为黑色。
- 第二种情况: 若插入节点的父节点为黑色,则符合红黑树平衡规则,无需进行调整。
- 第三种情况: 若插入节点的父节点为红色,则父节点的父节点一定是非空(即祖父节点,除非是根节点)。进一步讲,祖父节点也一定存在另一个子节点(即插入节点的叔叔节点,即便其为空,我们也视之为存在)。这种情况下,需要进一步细化为三种情况。
现象说明 | 处理策略 | |
---|---|---|
情况1 | 插入节点的父节点是红色,且其叔叔节点也是红色 | 1)将“父节点”和“叔叔节点”设为黑色 2)将“祖父节点”设为红色 3)将“祖父节点”设为插入节点,并对新的“插入节点”继续进行检测和调整操作 |
情况2 | 插入节点的父节点是红色,其叔叔节点是黑色,且插入节点是父节点的右子节点 | 1)以“父节点”为支点进行左旋 2)将“父节点”设为新的插入节点继续进行检测和调整 |
情况3 | 插入节点的父节点是红色,其叔叔节点是黑色,且插入节点是父节点的左子节点 | 1)将“父节点”设为黑色 2)将“祖父节点”设为红色 3)以“祖父节点”为支点进行右旋 |
- 红黑树调整的核心思路是把多出来的红色节点逐步转移到树的根节点,最后把根节点设为黑色。在这种情况下,红黑树的初步插入操作因为与二叉搜索树一样,所以复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))。在后续的调整中,单步调整操作为常数复杂度,最坏的情况下需要从叶节点一直调整到根节点,遍历整个树高,复杂度也为 O ( log ( N ) ) O(\log(N)) O(log(N)),因此红黑树插入的整体复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))
3.6 删除节点
- 红黑树删除操作和二叉搜索树删除操作是类似的,首先需要查询到待删除节点的位置,其后需要对其分为三种情况处理:
- 第一种情况: 待删除节点没有子节点,则直接删除,对删除后的空节点进行调整操作。(由于空叶节点在代码中是一个空指针,所以在寻找兄弟节点时需要单独考虑这种情况)
- 第二种情况: 待删除节点只有一个子节点,则直接删除待删除节点,令其子节点于其父节点相连,然后对其子节点进行平衡操作(因为“单分支黑-黑”和“单支红-黑”的情况会破坏红黑树第5条规则,不可能出现,所以平衡红黑树只存在“单分支黑-红”和双支情况)
- 第三种情况: 待删除节点存在两个子节点,则从待删除节点的右子树中找到最小的节点,交换两者的数值,两者的颜色保持不变,改为删除最小节点。由于最小节点要么没有子节点,要么只有右子节点,因此由第三种情况转换为了第一或二种情况。
- 假设待调整节点为 x x x,红黑树删除后的调整操作需要分为五种情况处理:
现象说明 | 处理策略 | |
---|---|---|
情况1 | x x x 为红色 | 直接设为黑色即可 |
情况2 | x x x 为黑色, x x x 的兄弟节点为红色(兄弟节点的子节点和父节点必为黑色) | 1)互换
x
x
x 的父节点与兄弟节点的颜色 3)以父节点为支点进行左旋 4)对 x x x 继续调整(转化为情况3,4,5处理) |
情况3 | x x x 为黑色,其兄弟节点为黑色,兄弟节点的子节点均为黑色 | 1)将兄弟节点设置为红色 2)把 x x x 的父节点设置为新的 x x x 节点,继续调整(转化为情2,4,5处理) |
情况4 | x x x 为黑色,其兄弟节点为黑色,兄弟节点的右子节点为黑色 | 1)互换兄弟节点与兄弟节点左子节点的颜色 2)对兄弟节点进行右旋 3)对 x x x 继续调整(转化为情况5处理) |
情况5 | x x x 为黑色,其兄弟节点为黑色,兄弟节点的左子节点为黑色,或子节点均为红色 | 1)互换兄弟节点与父节点的颜色 2)对父节点进行左旋 3)把兄弟节点的右子节点设为黑色 |
- 必须注意的是,上述的删除操作是基于待删除节点是一个左子节点的前提。如果待删除节点是一个右子节点,则需要把上述操作中,“左”和“右”的操作互换过来。
- 在具体的代码实现中,对于待删除节点不存在子节点的情况。我们先新建一个黑色、数值为负无穷的节点、与待删除节点共用同一个父节点的临时节点,来表示待删除节点删除后留存的空节点。但我们不令父节点指向这个临时节点(因为空节点本身是不存在的)。通过这种方法,我们能够对空节点执行调整操作。在调整结束后,把临时节点释放掉即可。
- 红黑树删除调整的核心思路是把删除后缺少的黑色节点通过旋转,从相邻的兄弟子树中“借”过来,利用逐步把矛盾往树的上层转移。由于删除前需要事先找到待删除的节点,复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))。在调整中,单步调整操作为常数复杂度,最差的情况下需要遍历整个树高,复杂度也为 O ( log ( N ) ) O(\log(N)) O(log(N))。因此红黑树删除的整体复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))
3.7 代码实现
- 树节点实现
/* Data structure for tree node */
enum Color {RED, BLACK};
struct Node
{
double data;
Color color;
Node* left;
Node* right;
Node* parent;
Node() {
data = 0.0;
color = RED;
left = nullptr;
right = nullptr;
parent = nullptr;
}
Node(double data_ = 0.0,
Color color_ = RED,
Node* left_ = nullptr,
Node* right_ = nullptr,
Node* parent_ = nullptr) {
data = data_;
color = color_;
left = left_;
right = right_;
parent = parent_;
}
};
- 树实现
/* Data Structure for Red-Black Tree */
struct RBTree
{
int size;
Node* root;
RBTree() {
root = nullptr;
size = 0;
}
/* Method for finding a specific node, return NULL if not exist */
Node* RBFind(double value, Node* node) {
while (node != nullptr && value != node->data ) {
node = (value > node->data) ? node->right : node->left;
}
return node;
}
/* Method for finding the minimum node */
Node* RBFindMin(Node* node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
/* Method for finding the maximum node */
Node* RBFindMax(Node* node) {
while (node->right != nullptr) {
node = node->right;
}
return node;
}
/* Method for left rotate a tree */
void LeftRotate(Node* node) {
Node* successor = node->right;
node->right = successor->left;
if (successor->left != nullptr)
successor->left->parent = node;
successor->parent = node->parent;
if (node->parent == nullptr)
this->root = successor;
else if (node->parent->left == node)
node->parent->left = successor;
else
node->parent->right = successor;
successor->left = node;
node->parent = successor;
return;
}
/* Method for right rotate a tree */
void RightRotate(Node* node) {
Node* successor = node->left;
node->left = successor->right;
if (successor->right != nullptr)
successor->right->parent = node;
successor->parent = node->parent;
if (node->parent == nullptr)
this->root = successor;
else if (node->parent->left == node)
node->parent->left = successor;
else
node->parent->right = successor;
successor->right = node;
node->parent = successor;
return;
}
/* Method for inserting new node */
void RBInsert(double value) {
Node* locator = this->root;
Node* parent = nullptr;
while (locator != nullptr) // Find the right place to insert new node
{
parent = locator;
locator = (value > locator->data) ? locator->right : locator->left;
}
Node* newNode = new Node(value, RED, nullptr, nullptr, parent);
if (locator == this->root)
this->root = newNode;
else if (value > parent->data)
parent->right = newNode;
else
parent->left = newNode;
RBInsertFixup(newNode);
this->size += 1;
return;
}
/* Method for maintaining Red-Black Tree properties after insertion */
void RBInsertFixup(Node* node) {
Node* parent = node->parent;
if (node == this->root) { // Case 1
node->color = BLACK;
}
else if (parent->color == RED) { // Case 3, don't need to deal with case 2
Node* gparent = parent->parent;
Node* uncle = (gparent->left == parent) ? gparent->right : gparent->left;
Color uncleColor = (uncle == nullptr) ? BLACK : uncle->color;
if (uncleColor == RED) { // Case 3.1
parent->color = BLACK;
uncle->color = BLACK;
gparent->color = RED;
RBInsertFixup(gparent);
}
else if (uncleColor == BLACK && parent->right == node) { // Case 3.2
LeftRotate(parent);
RBInsertFixup(parent);
}
else if (uncleColor == BLACK && parent->left == node) { // Case 3.3
parent->color = BLACK;
gparent->color = RED;
RightRotate(gparent);
}
}
return;
}
/* Method for deleting specific node */
Node* RBDelete(double value) {
Node* node = this->RBFind(value, this->root);
Node* parent = node->parent;
Node* successor = nullptr;
Color originColor = node->color;
if (node->left == nullptr && node->right == nullptr) { // Case I: have no child node
if (node == this->root)
this->root = nullptr;
else if (parent->right == node)
parent->right = nullptr;
else
parent->left = nullptr;
successor = new Node(INT_MIN, BLACK, nullptr, nullptr, parent);
}
else if (node->right == nullptr || node->left == nullptr) { // Case II: have one child node
Node* child = (node->right == nullptr) ? node->left : node->right;
if (parent->right == node)
parent->right = child;
else
parent->left = child;
successor = child;
}
else { // Case III: have two child nodes
Node* minNode = RBFindMin(node->right);
node->data = minNode->data;
if (minNode->right != nullptr) {
minNode->parent->left = minNode->right;
successor = minNode->right;
}
else {
minNode->parent->left = nullptr;
successor = new Node(INT_MIN, BLACK, nullptr, nullptr, minNode->parent);
}
originColor = minNode->color;
}
if (originColor == BLACK)
RBDeleteFixup(successor);
this->size -= 1;
return node;
}
/* Method for maintaining Red-Black Tree properties after deletion */
void RBDeleteFixup(Node* node) {
if (node != this->root && node->color == BLACK) {
Node* parent = node->parent;
Node* sibling = nullptr;
if (parent->left == nullptr || parent->right == nullptr) // Deal with the case when node is NULL
sibling = (parent->left == nullptr) ? parent->right : parent->left;
else
sibling = (node == parent->left) ? parent->right : parent->left;
if (sibling == parent->right){ // For the case when node is a left child
if (sibling->color == RED) { // Case 2
sibling->color = parent->color; // Case 2
parent->color = RED; // Case 2
LeftRotate(parent); // Case 2
RBDeleteFixup(node); // Case 2
}
else {
if ((sibling->left == nullptr || sibling->left->color == BLACK) &&
(sibling->right == nullptr || sibling->right->color == BLACK)) {
sibling->color = RED; // Case 3
RBDeleteFixup(parent); // Case 3
}
else if (sibling->right == nullptr || sibling->right->color == BLACK) {
sibling->color = sibling->left->color; // Case 4
sibling->left->color = BLACK; // Case 4
RightRotate(sibling); // Case 4
RBDeleteFixup(node); // Case 4
}
else {
sibling->color = parent->color; // Case 5
parent->color = BLACK; // Case 5
sibling->right->color = BLACK; // Case 5
LeftRotate(parent); // Case 5
}
}
}
else { // Do the same as then clause with “right” and “left” exchanged
if (sibling->color == RED) {
sibling->color = parent->color;
parent->color = RED;
RightRotate(parent);
RBDeleteFixup(node);
}
else {
if ((sibling->right == nullptr || sibling->right->color == BLACK) &&
(sibling->left == nullptr || sibling->left->color == BLACK)) {
sibling->color = RED;
RBDeleteFixup(parent);
}
else if (sibling->left == nullptr || sibling->left->color == BLACK) {
sibling->color = sibling->right->color;
sibling->right->color = BLACK;
LeftRotate(sibling);
RBDeleteFixup(node);
}
else {
sibling->color = parent->color;
parent->color = BLACK;
sibling->left->color = BLACK;
RightRotate(parent);
}
}
}
}
else
node->color = BLACK;
return;
}
void RBPrintTree(Node* node) {
if (node != nullptr)
std::cout << node->data << "-" << node->color << " ";
else
return;
RBPrintTree(node->left);
RBPrintTree(node->right);
return;
}
};
4. AVL树
4.1 基本知识点
- AVL树又被称作平衡二叉树(Self-balancing Binary Search Tree),因此AVL树本质上也是二叉搜索树,具有以下性质:
- 若一颗AVL树不为空树,则其左右两个子树的高度差的绝对值不超过1
- AVL树中,其任意的子树都是一颗AVL树
- 在AVL树中,节点的高度被定义为其左右子节点的高度中的最大者+1(叶节点的可视为有两个空的子节点),空节点的高度为-1,因此叶节点的高度为0。
- 由于AVL树是已经平衡的二叉搜索树,因此假设树中有 N N N 个节点,AVL树树高最多为 1.44 ( log ( N ) ) 1.44(\log(N)) 1.44(log(N))
4.2 查询节点
- AVL树查询节点的复杂度与完美二叉搜索树一样,为 O ( log ( N ) ) O(\log(N)) O(log(N))
4.3 插入节点
- 插入新的节点到AVL树中有可能会破坏AVL树原本的平衡性。因此,在插入节点后,需要对新的AVL树进行平衡性检查,在此需要引入一个被称为“平衡因子”的概念。
- 平衡因子(Balance Factor)被定义为AVL树的左子树和右子树的高度差的绝对值,根据AVL树的性质,平衡因子必须为0或1,大于1则说明AVL树不平衡。为了方便计算,树中的每一个节点都会记录自身在树中的高度。
- 如果插入的节点破坏了AVL树的平衡性,则需要进行平衡操作。我们假设需要进行平衡操作的节点为
x
x
x,需要分为四种情况处理:
- 情况1: LL,即插入的节点位于 x x x 的左子节点(L)的左子树(L)。此时需要对 x x x 进行右旋操作。
- 情况2: RR,即插入的节点位于 x x x 的右子节点(R)的右子树(R)。此时需要对 x x x 进行左旋操作。
- 情况3: LR,即插入的节点位于 x x x 的左子节点(L)的右子树(R)。此时需要先对 x x x 左子节点进行一次左旋操作,然后对 x x x 进行一次右旋操作。
- 情况4: RL,即插入的节点位于 x x x 的右子节点(R)的左子树(L)。此时需要先对 x x x 右子节点进行一次右旋操作,然后对 x x x 进行一次左旋操作。
- 在具体的代码实现中,我们采用了递归的方法来查询新节点将要插入的位置。当完成插入后,递归会开始沿着查询的路线逐层返回,直到返回到根节点。我们每返回到一层,就对当前的节点进行平衡性检测,如果违反了平衡性,则对当前节点进行平衡操作。如此便确保了所有与插入操作有关联的节点都能被检测并调整。需要注意的是,在每次执行完平衡操作后,都需要更新节点的高度。
- 查询合适的插入点需要遍历整个树高,递归返回同样需要遍历整个树高,因此两者的复杂度均为 O ( log ( N ) ) O(\log(N)) O(log(N))。调整的操作为常数复杂度,因此AVL树插入节点的整体复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))
4.4 删除节点
- 从某种程度上讲,AVL树的删除可以被看作是另一种形式的AVL树插入。和二叉搜索树删除操作一样,AVL树首先要找到待删除节点的位置,若不存在子节点,则直接删除;若存在一个子节点,则删除后把其子节点与其父节点相连;若存在两个子节点,则在其右子树中,找到右子树中最小的节点,替换掉待删除节点的数值后,删除最小节点。
- 如果删除节点导致了AVL树的平衡性被破坏,同样需要进行平衡操作。我们假设需要进行平衡操作的节点为
x
x
x,同样需要分为四种情况处理:
- 情况1: x x x 的左子树(L)高度大于右子树,且 x x x 的左子节点的左子树(L)高度大于右子树,这种情况相当于插入平衡处理中的LL。此时需要对 x x x 进行右旋操作。
- 情况2: x x x 的左子树(L)高度大于右子树,且 x x x 的左子节点的右子树(R)高度大于左子树,这种情况相当于插入平衡处理中的LR。此时需要对 x x x 的左子节点进行一次左旋操作,然后对 x x x 进行一次右旋操作。
- 情况3: x x x 的右子树(R)高度大于左子树,且 x x x 的右子节点的右子树(R)高度大于左子树,这种情况相当于插入平衡处理中的RR。此时需要对 x x x 进行左旋操作。
- 情况4: x x x 的右子树(R)高度大于左子树,且 x x x 的右子节点的左子树(L)高度大于右子树,这种情况相当于插入平衡处理中的RL。此时需要对 x x x 的右子节点进行一次右旋操作,然后对 x x x 进行一次左旋操作。
- 在具体的代码实现中,我们同样采用递归的方法来查询待删除节点的位置。完成删除后,递归会开始沿着查询的路线逐层返回,直到返回到根节点。在每层中,我们都对当前节点的父节点进行平衡性检测(不对当前节点检测是因为当前节点已经被删掉了),如果违反了平衡性,则进行平衡操作。如此便确保了所有与删除操作有关联的节点都能被检测并调整。需要注意的是,在每次执行完平衡操作后,都需要更新节点的高度。
- 查询待删除节点在最坏的情况下要遍历整个树高,递归返回同样需要遍历整个树高,因此两者的复杂度均为 O ( log ( N ) ) O(\log(N)) O(log(N))。调整的操作为常数复杂度,因此AVL树删除节点的整体复杂度为 O ( log ( N ) ) O(\log(N)) O(log(N))
4.5 代码实现
- 树节点实现
/* Data structure for tree node */
struct Node
{
double data;
int hight;
Node* left;
Node* right;
Node* parent;
Node() {
data = 0.0;
hight = 0;
left = nullptr;
right = nullptr;
parent = nullptr;
}
Node(double data_ = 0.0,
int hight_ = 0,
Node* left_ = nullptr,
Node* right_ = nullptr,
Node* parent_ = nullptr) {
data = data_;
hight = hight_;
left = left_;
right = right_;
parent = parent_;
}
};
- 树实现
#include <algorithm>
/* Data Structure for Red-Black Tree */
struct AVLTree
{
int size;
Node* root;
AVLTree() {
root = nullptr;
size = 0;
}
/* Method for finding a specific node, return NULL if not exist */
Node* AVLFind(double value, Node* node) {
while (node != nullptr && value != node->data ) {
node = (value > node->data) ? node->right : node->left;
}
return node;
}
/* Method for finding the minimum node */
Node* AVLFindMin(Node* node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
/* Method for finding the maximum node */
Node* AVLFindMax(Node* node) {
while (node->right != nullptr) {
node = node->right;
}
return node;
}
/* Method for calculating the tree hight */
int GetTreeHight(Node* node) {
return (node == nullptr) ? -1 : node->hight;
}
/* Method for calculating the balance factor of the node */
int GetBalanceFactor(Node* node) {
return abs(GetTreeHight(node->left) - GetTreeHight(node->right));
}
/* Method for testing if the current node is balanced */
bool isBalanced(Node* node) {
int BalanceFactor = GetBalanceFactor(node);
return (BalanceFactor > 1) ? false : true;
}
/* Method for left rotate a tree */
void LeftRotate(Node* node) {
Node* successor = node->right;
node->right = successor->left;
if (successor->left != nullptr)
successor->left->parent = node;
successor->parent = node->parent;
if (node->parent == nullptr)
this->root = successor;
else if (node->parent->left == node)
node->parent->left = successor;
else
node->parent->right = successor;
successor->left = node;
node->parent = successor;
node->hight = 1 + std::max(GetTreeHight(node->left), GetTreeHight(node->right));
successor->hight = 1 + std::max(GetTreeHight(successor->left), GetTreeHight(successor->right));
return;
}
/* Method for right rotate a tree */
void RightRotate(Node* node) {
Node* successor = node->left;
node->left = successor->right;
if (successor->right != nullptr)
successor->right->parent = node;
successor->parent = node->parent;
if (node->parent == nullptr)
this->root = successor;
else if (node->parent->left == node)
node->parent->left = successor;
else
node->parent->right = successor;
successor->right = node;
node->parent = successor;
node->hight = 1 + std::max(GetTreeHight(node->left), GetTreeHight(node->right));
successor->hight = 1 + std::max(GetTreeHight(successor->left), GetTreeHight(successor->right));
return;
}
/* Method for inserting new node */
void AVLInsert(double value) {
AVLInsert(this->root, nullptr, value);
return;
}
void AVLInsert(Node* locator, Node* parent, double value) {
if (locator == nullptr) {
Node* newNode = new Node(value, 0, nullptr, nullptr, parent);
if (parent == nullptr)
this->root = newNode;
else if (value > parent->data)
parent->right = newNode;
else
parent->left = newNode;
locator = newNode;
this->size += 1;
}
else if (value > locator->data) {
AVLInsert(locator->right, locator, value);
}
else {
AVLInsert(locator->left, locator, value);
}
AVLInsertFixup(locator, value);
return;
}
/* Method for maintaining AVL Tree properties after insertion */
void AVLInsertFixup(Node* node, double value) {
if (!isBalanced(node)) {
int leftValue = (node->left == nullptr) ? -1 : node->left->data;
int rightValue = (node->right == nullptr) ? -1 : node->right->data;
if (value < node->data && value < leftValue) { // Case LL
RightRotate(node);
}
else if (value > node->data && value > rightValue) { // Case RR
LeftRotate(node);
}
else if (value < node->data && value > leftValue) { // Case LR
LeftRotate(node->left);
RightRotate(node);
}
else if (value > node->data && value < rightValue) { // Case RL
RightRotate(node->right);
LeftRotate(node);
}
}
node->hight = 1 + std::max(GetTreeHight(node->left), GetTreeHight(node->right));
return;
}
/* Method for deleting specific node */
Node* AVLDelete(double value) {
return AVLDelete(this->root, nullptr, value);
}
Node* AVLDelete(Node* locator, Node* parent, double value) {
if (value == locator->data) {
if (locator->left == nullptr && locator->right == nullptr) {
if (locator == this->root)
this->root = nullptr;
else if (parent->right == locator)
parent->right = nullptr;
else
parent->left = nullptr;
}
else if (locator->right == nullptr || locator->left == nullptr) {
Node* child = (locator->right == nullptr) ? locator->left : locator->right;
if (parent->right == locator)
parent->right = child;
else
parent->left = child;
}
else {
Node* minNode = AVLFindMin(locator->right);
locator->data = minNode->data;
if (minNode->right != nullptr)
minNode->parent->left = minNode->right;
else
minNode->parent->left = nullptr;
parent = minNode->parent;
}
this->size -= 1;
}
else if (value > locator->data) {
AVLDelete(locator->right, locator, value);
}
else {
AVLDelete(locator->left, locator, value);
}
AVLDeleteFixup(parent);
return locator;
}
/* Method for maintaining AVL Tree properties after deletion */
void AVLDeleteFixup(Node* node) {
if (node == nullptr) // Do nothing if the node is NULL
return;
if (!isBalanced(node)) {
if (GetTreeHight(node->left) > GetTreeHight(node->right)) {
if (GetTreeHight(node->left->left) >= GetTreeHight(node->left->right)) // Case LL
RightRotate(node);
else { // Case LR
LeftRotate(node->left);
RightRotate(node);
}
}
else {
if (GetTreeHight(node->right->right) >= GetTreeHight(node->right->left)) // Case RR
LeftRotate(node);
else { // Case RL
RightRotate(node->right);
LeftRotate(node);
}
}
}
node->hight = 1 + std::max(GetTreeHight(node->left), GetTreeHight(node->right));
return;
}
void AVLPrintTree(Node* node) {
if (node != nullptr)
std::cout << node->data << "-" << node->hight << " ";
else
return;
AVLPrintTree(node->left);
AVLPrintTree(node->right);
return;
}
};
5. 测试数据集
- 本文档中代码的测试数据集如下:
Tree tree;
tree.Insert(12);
tree.Insert(4);
tree.Insert(25);
tree.Insert(3);
tree.Insert(9);
tree.Insert(6);
tree.Insert(8);
tree.Insert(19);
tree.Insert(15);
tree.Insert(21);
tree.Insert(28);
tree.Insert(14);
tree.Insert(17);
tree.Delete(3);
tree.Delete(4);
tree.Delete(9);
6. 参考文献
Thomas, H., Charles, E., Ronald, L., Clifford, S. (2009). Introduction to Algorithms (3rd Edition)
原创博客,不得转载、抄袭