阅读本文前,请确保您已经了解了二叉搜索树的相关内容(如定义、增删查改的方法以及效率等)。否则,建议您先学习二叉搜索树。本文假定您对二叉搜索树有了足够的了解。

效率?

众所周知,在平衡条件下,对二叉搜索树中的元素进行增删查改,时间效率为\(O(log(n))\)。

然而,理想很丰满,现实很骨感,实际上,二叉搜索树并不总是能够保持平衡状态。

最极端的情况就是,除了叶节点之外,每个节点只有一个子节点,如图所示:

这种情况下二叉搜索树已经退化成一个链表,时间复杂度达到了\(O(n)\),二叉搜索树在时间效率上的优势并没有发挥出来。

要想解决这个问题,我们就要在每次动态操作(插入、删除)之后,采取某种措施,使二叉搜索树重新回到平衡状态(重平衡)。

平衡?

最理想的状态就是完全二叉树,其时间复杂度为\(O(log(n))\),但是如果每次动态操作之后都要让二叉搜索树处于这样一种理想状态的话,实现起来比较复杂,大量的时间消耗在重平衡上,最后反而得不偿失。

鱼和熊掌不可兼得,我们需要在“平衡状态”和“花在重平衡上的时间成本”之间做出一个取舍,将平衡条件适当放宽。

一种可能的折中方案

这次,我们不再要求二叉搜索树必须是完全二叉树,而是放宽到每一个节点的左右子树高度差不超过1(规定空树的高度为0)。这就是所谓的AVL树。

引入一个新的定义:平衡因子(Balance Factor),是指一个节点的左子树高度减去右子树高度。所以,AVL树的性质等价于每个节点的平衡因子的绝对值不超过1。(之所以不在平衡因子的定义里加上绝对值,是因为后面要判断是左子树比右子树高还是右子树比左子树高)

下图就是一棵AVL树的例子。

可以看出,每个节点的左右子树高度差不超过1。这棵树确实不是一棵完全二叉树,但是其时间效率依然近似为\(O(log(n))\),是可以接受的。

动态操作之后的重平衡

不难发现,对AVL树进行动态操作,有可能破坏AVL树的性质。因此,在每一次动态操作之后,我们都要对其进行某种措施,使其重新恢复AVL树性质。

等价变换

我们知道,一棵二叉搜索树的中序遍历序列是单调递增的,但是中序遍历序列相同的二叉搜索树的结构不一定相同,这里我们可以将中序遍历序列相同的二叉搜索树看做是等价的。比如以下这4棵二叉搜索树的中序遍历序列均为1 2 3,因此这4棵二叉搜索树都可以看做是等价的。

对于发生失衡的节点,常用“旋转”的方法使之恢复平衡。所谓“旋转”操作,就是以下两种结构的相互变换。

以左旋操作为例,令待旋转节点为x,x的父节点为p,x的右子节点为y,y的左右子节点分别为b和c,则左旋的操作步骤为:

  1. 将b设为x的右子节点。
  2. 将x设为y的左子节点。
  3. 将y设为p的左子节点/右子节点(这取决于原先x是p的左子节点还是右子节点)。

右旋操作与此对称,不再赘述。

几种失衡的情况

以下是一些失衡的情况,需要做不同的处理来使之恢复平衡。

LL与RR

所谓LL,就是失衡节点的左子树比右子树高,且左孩子的左子树又比右子树高或等高,这时候我们只需要对失衡节点做一次右旋操作就可以恢复平衡。如图:

RR就是失衡节点的右子树比左子树高,且右孩子的右子树又比左子树高或等高。恢复措施完全是对称的,这里不再赘述了。

LR与RL

所谓LR,就是失衡节点的左子树比右子树高,但左孩子的右子树却比左子树高。这时候我们需要进行两步旋转操作:首先对失衡节点的左孩子进行一次左旋,使得整个结构变成LL,然后对失衡节点进行一次右旋,这样才能平衡,如图:

RL与此是完全对称的,操作步骤也是对称的。

插入节点后的重平衡

在插入节点后,有可能出现失衡的情况,如图:

这时候就要进行相应的恢复平衡的措施。步骤如下:

  1. 上溯到最低的失衡节点。(如果上溯到根节点也没有发现失衡节点就可以不用重平衡了)
  2. 根据失衡类型(LL、LR、RL、RR)进行相应的调整。

调整后的子树的高度会与插入前保持一致,因此不需要进行后续的重平衡操作了。

重平衡的时间复杂度:\(O(log(n))\)。其中上溯的时间复杂度为\(O(log(n))\),旋转的时间复杂度为\(O(1)\)。

删除节点后的重平衡

对节点进行删除,也有可能导致出现失衡,如图:

这时候的操作和插入操作一样,先上溯到最深的失衡节点然后进行调整。

但是!调整后的子树的高度有可能会变化,这就意味着这棵子树虽然恢复了平衡,但是有可能反而会导致父节点的失衡,如图:

因此,我们需要进一步上溯到父节点,然后进行重平衡处理。

同样的,这样做可能会继续导致父节点的父节点的失衡,因此我们需要不断地上溯、重平衡,直到上溯到的节点不再失衡为止。如果上溯到根节点依然没有发现失衡节点,那么自然就不需要再进行重平衡了。

重平衡的时间复杂度:\(O(log(n))\)。其中上溯的时间复杂度为\(O(log(n))\),旋转的时间复杂度为\(O(1)\)。

代码实现

类定义

AVL树类框架

为了能够让AVL树能够适应不同的数据类型(intfloatlong)等,我们以模板类的方式来实现。

整个AVL树类应该对外提供以下接口:查找节点是否存在、插入节点、删除节点。

为了隐藏实现细节,只暴露接口,我们将AVL树节点类以private嵌套类的形式封装在AVL树内部。

另外,为了方便,我们引入一个哑结点,其左孩子为整棵树的根节点,父节点和右孩子为nullptr

由于插入、删除操作为动态操作,除了哑结点之外的所有节点全都是通过new运算符动态分配在堆内存中的,因此我们需要一种方式在AVL树对象销毁时将动态分配出来的节点所占用的内存空间释放掉。因为我们释放节点时只能在左右孩子均释放之后才能释放当前节点(否则会因为访问未分配的内存而引发程序异常退出),因此我们需要单独定义一个函数来递归的释放节点。

因此,整个AVL树类的大体框架如下:

template<typename T>
class AVLTree { class AVLTreeNode {
// ...
}; AVLTreeNode dummyHead; public:
AVLTree() : /* ... */ {} virtual ~AVLTree() {
// ...
}
protected:
// 上述3个接口(存在、插入、删除)均依赖于这个内部接口。
// 返回给定数据所在的节点。如果不存在的话,就返回插入操作中所插入的节点的父节点。
AVLTreeNode *find(T data) {
// ...
} public:
// 判断AVL树中是否存在指定数据所在的节点。
bool exists(T data) {
// ...
} // 如果给定数据所在的节点不存在,则插入节点。
void insert(T data) {
// ...
} // 如果存在给定数据所在的节点,则删除节点。
void remove(T data) {
// ...
} protected:
// 释放以给定节点为根节点的子树所占用的内存空间
void release(AVLTreeNode *node) {
// ...
}
};

AVL节点类框架

而AVL树节点类有以下几个成员变量:数据、父节点、左右孩子节点、以该节点为根节点的子树高度(以下简称“该节点的高度”)。为了便于操作,类的内部也定义了一些接口,同时给AVL树类暴露了两个接口——插入和删除。注意这里AVL树节点类并没有定义为模板类,这是因为节点类的存放的数据类型是跟随着AVL树的数据类型的。

大体框架如下:

class AVLTreeNode {
public:
T data;
AVLTreeNode *parent;
AVLTreeNode *left;
AVLTreeNode *right;
std::size_t height; public:
explicit AVLTreeNode(T data) : /* ... */ {
// ...
} protected:
// 更新高度
void updateHeight() {
// ...
} // 计算平衡因子
int getBalanceFactor() {
// ...
} // 判断平衡因子是否在可接受的范围内
bool isBalanceFactorLegal() {
// ...
} // 判断是否为哑结点
bool isDummyHead() {
// ...
} // 判断是否为父节点的左孩子
bool isLeftChild() {
// ...
} // 将另一个节点作为当前节点的左孩子
void connectLeft(AVLTreeNode *child) {
// ...
} // 将另一个节点作为当前节点的右孩子
void connectRight(AVLTreeNode *child) {
// ...
} // 右旋
AVLTreeNode *rotateClockwise() {
// ...
} // 左旋
AVLTreeNode *rotateCounterclockwise() {
// ...
} // 对LL的情况进行重平衡
AVLTreeNode *reBalanceLeftLeft() {
// ...
} // 对LR的情况进行重平衡
AVLTreeNode *reBalanceLeftRight() {
// ...
} // 对RL的情况进行重平衡
AVLTreeNode *reBalanceRightLeft() {
// ...
} // 对RR的情况进行重平衡
AVLTreeNode *reBalanceRightRight() {
// ...
} // 重平衡
AVLTreeNode *reBalance() {
// ...
} public:
// 以下两个接口虽然是public的,但是由于AVL树节点类在外层的AVL树类中是private的,所以这两个接口只对AVL树类可见,而在类外不可见。 // 将新节点插入到当前节点的底部
void insert(T newData) {
// ...
} // 删除当前节点
void remove() {
// ...
}
};

AVL树节点接口实现

初始化节点对象时,我们只需要初始化数据即可,父节点、左右孩子均设为nullptr。按照此前对树高的定义,该节点的高度为1。

explicit AVLTreeNode(T data) : data(data), parent(nullptr), left(nullptr), right(nullptr), height(1) {}

在设置左右孩子节点的时候,自身的height成员变量不会自己变,因此我们需要手动更新高度为左右子树各自的高度的最大值加上1。

void updateHeight() {
std::size_t leftHeight = left == nullptr ? 0 : left->height;
std::size_t rightHeight = right == nullptr ? 0 : right->height;
height = std::max(leftHeight, rightHeight) + 1;
}

计算平衡因子,理论上只需要用左子树高度减去右子树高度即可,但是实际上由于左右子树高度都是用std::size_t存储的,虽然std::size_t在不同的平台上的具体实现有可能有所不同,但是都是unsigned的(可能是unsigned long longunsigned long等),因此直接相减依然会得到unsigned的数据,可能不能得到正确的结果。所以这里需要根据左右子树的相对大小具体处理。因为平衡因子的取值只有可能在-1、0、1之间,以及短暂的处于-2、2,因此用int存储完全足够了。

int getBalanceFactor() {
std::size_t leftHeight = left == nullptr ? 0 : left->height;
std::size_t rightHeight = right == nullptr ? 0 : right->height;
int diff = leftHeight > rightHeight ? leftHeight - rightHeight : rightHeight - leftHeight;
return leftHeight > rightHeight ? diff : -diff;
}

判断平衡因子是否在-1~1之间,我们单独定义一个函数,这样在需要判断的时候就不用单独定义一个变量存储平衡因子了:

bool isBalanceFactorLegal() {
auto balanceFactor = getBalanceFactor();
return balanceFactor >= -1 && balanceFactor <= 1;
}

判断节点是否为哑结点,根据此前的定义,只需要判断父节点是否为空:

bool isDummyHead() {
return parent == nullptr;
}

判断节点是否为父节点的左孩子:

bool isLeftChild() {
return this == parent->left;
}

将新节点作为当前节点的左/右孩子,我们首先需要更新当前节点的子节点,以及子节点(如果非空)的父节点,然后更新高度:

void connectLeft(AVLTreeNode *child) {
left = child;
if (child != nullptr)
child->parent = this;
updateHeight();
} void connectRight(AVLTreeNode *child) {
right = child;
if (child != nullptr)
child->parent = this;
updateHeight();
}

对节点进行左旋、右旋,并返回旋转后在原来位置上新的节点:

AVLTreeNode *rotateClockwise() {
auto l = left;
if (l == nullptr) return this;
auto lr = l->right;
auto p = parent;
bool isLeft = isLeftChild(); // 节点之间的重连
connectLeft(lr);
l->connectRight(this);
if (isLeft)
p->connectLeft(l);
else
p->connectRight(l);
return l;
} AVLTreeNode *rotateCounterclockwise() {
auto r = right;
if (right == nullptr) return this;
auto rl = right->left;
auto p = parent;
bool isLeft = isLeftChild();
connectRight(rl);
r->connectLeft(this);
if (isLeft)
p->connectLeft(r);
else
p->connectRight(r);
return r;
}

对不同的失衡类型进行重平衡,并返回重平衡后原来位置上新的节点:

AVLTreeNode *reBalanceLeftLeft() {
return rotateClockwise();
} AVLTreeNode *reBalanceLeftRight() {
left->rotateCounterclockwise();
return rotateClockwise();
} AVLTreeNode *reBalanceRightLeft() {
right->rotateClockwise();
return rotateCounterclockwise();
} AVLTreeNode *reBalanceRightRight() {
return rotateCounterclockwise();
}

重平衡的统一接口,失衡类型的判断在接口内部进行:

AVLTreeNode *reBalance() {
int balanceFactor = getBalanceFactor();
int sonBalanceFactor;
if (balanceFactor == 0) { // 如果平衡因子等于0,就不需要重平衡了
return this;
}
AVLTreeNode *son = nullptr;
if (balanceFactor > 0) { // 左子树高于右子树
son = left;
sonBalanceFactor = son->getBalanceFactor();
return sonBalanceFactor < 0
? reBalanceLeftRight() // 对左孩子来说,其右子树高于左子树,为LR
: reBalanceLeftLeft(); // 对左孩子来说,其左子树高于右子树或者等高,为LL
} else { // 右子树高于左子树
son = right;
sonBalanceFactor = son->getBalanceFactor();
return sonBalanceFactor > 0
? reBalanceRightLeft() // 对右孩子来说,其左子树高于右子树,为RL
: reBalanceRightRight(); // 对右孩子来说,其右子树高于左子树或者等高,为RR
}
}

在当前节点的下方插入一个新的节点,步骤:

  1. 判断要插入的数据是否与当前节点的数据相同,如果当前节点不是哑结点且新数据与存放的数据相同则不需要插入,直接退出。
  2. 判断新数据与当前节点的数据的大小关系,并决定是插入到左边还是右边。如果当前节点是哑结点则无条件插入到左边。
  3. 上溯到最深的失衡节点(边上溯边更新高度),直到上溯到哑结点为止。
  4. 如果上溯到哑结点了,说明没有节点出现失衡,结束。
  5. 如果发现某个节点失衡了,那就重新平衡。

代码:

void insert(T newData) {
if (!isDummyHead() && newData == data) return;
auto cur = new AVLTreeNode(newData, this); if (isDummyHead() || newData < data) {
connectLeft(cur);
} else {
connectRight(cur);
} do {
cur = cur->parent;
cur->updateHeight();
} while (!cur->isDummyHead() && cur->isBalanceFactorLegal());
if (cur->isDummyHead()) {
return;
} cur->reBalance();
}

删除当前节点,步骤:

  1. 按照二叉搜索树删除节点的步骤删除该节点。
  2. 上溯到第一个出现失衡的节点。
  3. 重平衡,继续上溯并重平衡,直到不再遇到失衡节点为止。
  4. 释放该节点所占用的内存空间。

代码:

void remove() {
bool hasLeftChild = left != nullptr;
bool hasRightChild = right != nullptr;
AVLTreeNode *cur = parent; // 按照当前节点的子节点数量分情况讨论
if (hasLeftChild) {
if (hasRightChild) { // 左右孩子节点都有,用当前节点的中序遍历后继替换掉,然后转而删除后继节点
auto successor = right;
for (; successor->left != nullptr; successor = successor->left);
data = successor->data;
successor->remove();
return;
} else { // 只有左子节点,将父节点的子节点设为当前节点的左子节点
if (isLeftChild()) {
parent->connectLeft(left);
} else {
parent->connectRight(left);
}
}
} else {
if (hasRightChild) { // 只有右子节点,将父节点的子节点设为当前节点的右子节点
if (isLeftChild()) {
parent->connectLeft(right);
} else {
parent->connectRight(right);
}
} else { // 没有孩子节点,将父节点的子节点置空
if (isLeftChild()) {
parent->connectLeft(nullptr);
} else {
parent->connectRight(nullptr);
}
}
} while (!cur->isDummyHead() && cur->isBalanceFactorLegal()) {
cur = cur->parent;
cur->updateHeight();
} for (; !cur->isDummyHead() && !cur->isBalanceFactorLegal(); cur = cur->parent) {
cur = cur->reBalance();
} delete this;
}

AVL树接口实现

初始化:

AVLTree() : dummyHead(T()) {}

find内部接口,按照二叉树查找节点的方法,查找给定数据所在的节点。如果不存在的话,就返回插入操作中所插入的节点的父节点。

AVLTreeNode *find(T data) {
auto cur = dummyHead.left;
if (cur == nullptr) return &dummyHead;
while (cur->data != data) {
if (data < cur->data) {
if (cur->left == nullptr) {
return cur;
}
cur = cur->left;
} else {
if (cur->right == nullptr) {
return cur;
}
cur = cur->right;
}
}
return cur;
}

判断给定的数据是否存在,通过判断find接口返回的节点存储的数据是否为给定的数据来实现。(如果整棵树没有节点,那么find将会返回哑结点,因此还需额外判断返回的是否为哑结点)

bool exists(T data) {
AVLTreeNode *found = find(data);
return found != &dummyHead && found->data == data;
}

插入节点:(由于在节点的insert接口内判断了新的数据与当前节点的数据是否相等,因此树的插入节点的接口无需再判断了)

void insert(T data) {
AVLTreeNode *found = find(data);
found->insert(data);
}

删除节点:

void remove(T data) {
AVLTreeNode *found = find(data);
if (found != &dummyHead && found->data == data) found->remove();
}

内存释放:

void release(AVLTreeNode *node) {
if (node == nullptr) return;
release(node->left);
release(node->right);
delete node;
} virtual ~AVLTree() {
release(dummyHead.left);
}

完整代码

由于嵌套类看起来非常冗长,不好看,因此我们将AVL树类和AVL树节点类单独放在两个文件内,AVL树类里面#include对应的文件。

AVLTreeNode.cpp文件内定义节点类:

// AVLTreeNode.cpp
class AVLTreeNode {
public:
T data;
AVLTreeNode *parent;
AVLTreeNode *left;
AVLTreeNode *right;
std::size_t height; public:
explicit AVLTreeNode(T data) : data(data), parent(nullptr), left(nullptr), right(nullptr), height(1) {} protected:
// 更新高度
void updateHeight() {
std::size_t leftHeight = left == nullptr ? 0 : left->height;
std::size_t rightHeight = right == nullptr ? 0 : right->height;
height = std::max(leftHeight, rightHeight) + 1;
} // 计算平衡因子
int getBalanceFactor() {
std::size_t leftHeight = left == nullptr ? 0 : left->height;
std::size_t rightHeight = right == nullptr ? 0 : right->height;
int diff = leftHeight > rightHeight ? leftHeight - rightHeight : rightHeight - leftHeight;
return leftHeight > rightHeight ? diff : -diff;
} // 判断平衡因子是否在可接受的范围内
bool isBalanceFactorLegal() {
auto balanceFactor = getBalanceFactor();
return balanceFactor >= -1 && balanceFactor <= 1;
} // 判断是否为哑结点
bool isDummyHead() {
return parent == nullptr;
} // 判断是否为父节点的左孩子
bool isLeftChild() {
return this == parent->left;
} // 将另一个节点作为当前节点的左孩子
void connectLeft(AVLTreeNode *child) {
left = child;
if (child != nullptr)
child->parent = this;
updateHeight();
} // 将另一个节点作为当前节点的右孩子
void connectRight(AVLTreeNode *child) {
right = child;
if (child != nullptr)
child->parent = this;
updateHeight();
} // 右旋
AVLTreeNode *rotateClockwise() {
auto l = left;
if (l == nullptr) return this;
auto lr = l->right;
auto p = parent;
bool isLeft = isLeftChild(); // 节点之间的重连
connectLeft(lr);
l->connectRight(this);
if (isLeft)
p->connectLeft(l);
else
p->connectRight(l);
return l;
} // 左旋
AVLTreeNode *rotateCounterclockwise() {
auto r = right;
if (right == nullptr) return this;
auto rl = right->left;
auto p = parent;
bool isLeft = isLeftChild();
connectRight(rl);
r->connectLeft(this);
if (isLeft)
p->connectLeft(r);
else
p->connectRight(r);
return r;
} // 对LL的情况进行重平衡
AVLTreeNode *reBalanceLeftLeft() {
return rotateClockwise();
} // 对LR的情况进行重平衡
AVLTreeNode *reBalanceLeftRight() {
left->rotateCounterclockwise();
return rotateClockwise();
} // 对RL的情况进行重平衡
AVLTreeNode *reBalanceRightLeft() {
right->rotateClockwise();
return rotateCounterclockwise();
} // 对RR的情况进行重平衡
AVLTreeNode *reBalanceRightRight() {
return rotateCounterclockwise();
} // 重平衡
AVLTreeNode *reBalance() {
int balanceFactor = getBalanceFactor();
int sonBalanceFactor;
if (balanceFactor == 0) { // 如果平衡因子等于0,就不需要重平衡了
return this;
}
AVLTreeNode *son = nullptr;
if (balanceFactor > 0) { // 左子树高于右子树
son = left;
sonBalanceFactor = son->getBalanceFactor();
return sonBalanceFactor < 0
? reBalanceLeftRight() // 对左孩子来说,其右子树高于左子树,为LR
: reBalanceLeftLeft(); // 对左孩子来说,其左子树高于右子树或者等高,为LL
} else { // 右子树高于左子树
son = right;
sonBalanceFactor = son->getBalanceFactor();
return sonBalanceFactor > 0
? reBalanceRightLeft() // 对右孩子来说,其左子树高于右子树,为RL
: reBalanceRightRight(); // 对右孩子来说,其右子树高于左子树或者等高,为RR
}
} public:
// 以下两个接口虽然是public的,但是由于AVL树节点类在外层的AVL树类中是private的,所以这两个接口只对AVL树类可见,而在类外不可见。 // 将新节点插入到当前节点的底部
void insert(T newData) {
if (!isDummyHead() && newData == data) return;
auto cur = new AVLTreeNode(newData, this); if (isDummyHead() || newData < data) {
connectLeft(cur);
} else {
connectRight(cur);
} do {
cur = cur->parent;
cur->updateHeight();
} while (!cur->isDummyHead() && cur->isBalanceFactorLegal());
if (cur->isDummyHead()) {
return;
} cur->reBalance();
} // 删除当前节点
void remove() {
bool hasLeftChild = left != nullptr;
bool hasRightChild = right != nullptr;
AVLTreeNode *cur = parent; // 按照当前节点的子节点数量分情况讨论
if (hasLeftChild) {
if (hasRightChild) { // 左右孩子节点都有,用当前节点的中序遍历后继替换掉,然后转而删除后继节点
auto successor = right;
for (; successor->left != nullptr; successor = successor->left);
data = successor->data;
successor->remove();
return;
} else { // 只有左子节点,将父节点的子节点设为当前节点的左子节点
if (isLeftChild()) {
parent->connectLeft(left);
} else {
parent->connectRight(left);
}
}
} else {
if (hasRightChild) { // 只有右子节点,将父节点的子节点设为当前节点的右子节点
if (isLeftChild()) {
parent->connectLeft(right);
} else {
parent->connectRight(right);
}
} else { // 没有孩子节点,将父节点的子节点置空
if (isLeftChild()) {
parent->connectLeft(nullptr);
} else {
parent->connectRight(nullptr);
}
}
} while (!cur->isDummyHead() && cur->isBalanceFactorLegal()) {
cur = cur->parent;
cur->updateHeight();
} for (; !cur->isDummyHead() && !cur->isBalanceFactorLegal(); cur = cur->parent) {
cur = cur->reBalance();
} delete this;
}
};

AVLTree.cpp文件内定义AVL树类:

// AVLTree.cpp
template<typename T>
class AVLTree {
#include "AVLTreeNode.cpp" AVLTreeNode dummyHead; public:
AVLTree() : dummyHead(T()) {} virtual ~AVLTree() {
release(dummyHead.left);
}
protected:
// 上述3个接口(存在、插入、删除)均依赖于这个内部接口。
// 返回给定数据所在的节点。如果不存在的话,就返回插入操作中所插入的节点的父节点。
AVLTreeNode *find(T data) {
auto cur = dummyHead.left;
if (cur == nullptr) return &dummyHead;
while (cur->data != data) {
if (data < cur->data) {
if (cur->left == nullptr) {
return cur;
}
cur = cur->left;
} else {
if (cur->right == nullptr) {
return cur;
}
cur = cur->right;
}
}
return cur;
} public:
// 判断AVL树中是否存在指定数据所在的节点。
bool exists(T data) {
AVLTreeNode *found = find(data);
return found != &dummyHead && found->data == data;
} // 如果给定数据所在的节点不存在,则插入节点。
void insert(T data) {
AVLTreeNode *found = find(data);
found->insert(data);
} // 如果存在给定数据所在的节点,则删除节点。
void remove(T data) {
AVLTreeNode *found = find(data);
if (found != &dummyHead && found->data == data) found->remove();
} protected:
// 释放以给定节点为根节点的子树所占用的内存空间
void release(AVLTreeNode *node) {
if (node == nullptr) return;
release(node->left);
release(node->right);
delete node;
}
};

PS:事实上,实际的代码实现比这还要复杂,还要考虑到重载赋值运算符、传参的时候传入的是左值引用还是右值引用等等,这里主要是为了讲述原理就不搞那么复杂了。

手撕AVL树(C++)的更多相关文章

  1. 手写AVL 树(下)

    上一篇 手写AVL树上实现了AVL树的插入和查询 上代码: 头文件:AVL.h #include <iostream> template<typename T1,typename T ...

  2. 手写AVL 树(上)

    平衡二叉树 左旋,右旋,左右旋,右左旋 具体原理就不说了,网上教程很多.这里只实现了建树的过程,没有实现删除节点的操作. 下一篇会实现删除节点的操作. // // main.cpp // AVL // ...

  3. AVL树(三)之 Java的实现

    概要 前面分别介绍了AVL树"C语言版本"和"C++版本",本章介绍AVL树的Java实现版本,它的算法与C语言和C++版本一样.内容包括:1. AVL树的介绍 ...

  4. AVL树(一)之 图文解析 和 C语言的实现

    概要 本章介绍AVL树.和前面介绍"二叉查找树"的流程一样,本章先对AVL树的理论知识进行简单介绍,然后给出C语言的实现.本篇实现的二叉查找树是C语言版的,后面章节再分别给出C++ ...

  5. AVL树(二)之 C++的实现

    概要 上一章通过C语言实现了AVL树,本章将介绍AVL树的C++版本,算法与C语言版本的一样. 目录 1. AVL树的介绍2. AVL树的C++实现3. AVL树的C++测试程序 转载请注明出处:ht ...

  6. "《算法导论》之‘树’":AVL树

    本文关于AVL树的介绍引自博文AVL树(二)之 C++的实现,与二叉查找树相同的部分则不作介绍直接引用:代码实现是在本文的基础上自己实现且继承自上一篇博文二叉查找树. 1.AVL树的介绍 AVL树是高 ...

  7. AVL树之 Java的实现

    AVL树的介绍 AVL树是高度平衡的而二叉树.它的特点是:AVL树中任何节点的两个子树的高度最大差别为1. 上面的两张图片,左边的是AVL树,它的任何节点的两个子树的高度差别都<=1:而右边的不 ...

  8. PAT甲级1123 Is It a Complete AVL Tree【AVL树】

    题目:https://pintia.cn/problem-sets/994805342720868352/problems/994805351302414336 题意: 给定n个树,依次插入一棵AVL ...

  9. AVL树的实现——c++

    一.概念 AVL树是根据它的发明者G.M. Adelson-Velsky和E.M. Landis命名的.它是最先发明的自平衡二叉查找树,也被称为高度平衡树.相比于"二叉查找树",它 ...

  10. 平衡树以及AVL树

    平衡树是计算机科学中的一类数据结构. 平衡树是计算机科学中的一类改进的二叉查找树.一般的二叉查找树的查询复杂度是跟目标结点到树根的距离(即深度)有关,因此当结点的深度普遍较大时,查询的均摊复杂度会上升 ...

随机推荐

  1. Windows docker环境安装

    前期准备 1.hyper-v功能 win10家庭版没有提供hyper-v的问题可通过如下脚本解决,保存为bat并运行重启电脑即可. pushd "%~dp0" dir /b %Sy ...

  2. CURL提交--POST/GET-带header信息

    function https_request($url, $param, $data = '', $method = 'GET', $headers = '') { $opts = array( CU ...

  3. go-zero docker-compose 搭建课件服务(一):编写服务api和proto

    0.转载 go-zero docker-compose 搭建课件服务(一):编写服务api和proto 0.1源码地址 https://github.com/liuyuede123/go-zero-c ...

  4. ES6 学习笔记(十二)代理器Proxy的简单使用

    1.前言 以前在学习react时做了个仿手机端的QQ音乐项目.当时的数据是通过proxy代理的QQ音乐数据接口,直接写在package.json里面.Proxy 对象(Proxy)是 ES6的特性,只 ...

  5. Aspose.Cell和NPOI生成Excel文件

    1.使用Aspose.Cell生成Excel文件,Aspose.Cell是.NET组件控件,不依赖COM组件 1首先一点需要使用新建好的空Excel文件做模板,否则容易产生一个多出的警告Sheet 1 ...

  6. nydusd 源码理解(一)

    " 尝试通过 nydus[1] 源码理解工作流程.可能由于代码变动导致和本文记录的内容有出入. 1. 环境准备 git clone https://github.com/dragonflyo ...

  7. bugku web基础$_POST

    这道题也是让what=flag就行了 直接试试通过max hackbar来进行post传入 得到flag

  8. Java 中经常被提到的 SPI 到底是什么?

    layout: post categories: Java title: Java 中经常被提到的 SPI 到底是什么? tagline: by 子悠 tags: - 子悠 Java 程序员在日常工作 ...

  9. Excel二维码图片生成器

    Excel二维码图片生成器 它可以将excel文件的数据,每行数据生成一张二维码图片,并保存到电脑.软件无需安装,解压后即可直接使用,无需联网,操作简便快捷. 软件下载地址:点此下载 步骤1:导入事先 ...

  10. 【Hadoop学习】中:HDFS、shell操作、客户端API操作、数据流、1NN、2NN原理、DataNode配置

    一.概述 1.背景.定义.使用场景(一次写入.不支持修改) 2.优(容错)缺点(延迟.不支持小文件.不支持修改) 3.组成架构 NameNode:Master,管理命名空间.配置策略 DataNode ...