【C++】高阶数据结构 -- 平衡二叉树(AVLTree)

【C++】高阶数据结构 -- 平衡二叉树(AVLTree)
本篇文章主要讲解一种特殊的 BST -- 平衡二叉树(AVLTree)

目录

1  平衡二叉树的概念

2  AVLTree 的增删查改(不允许插入重复 key 的 key-value 场景)

1) AVLTree 的节点结构

2) AVLTree 的插入

(1) 如何更新平衡因子

(2) AVL 树的旋转

3) AVLTree 的查找

3  AVLTree 的检测

4  总结


1  平衡二叉树的概念

        平衡二叉树是一种特殊的二叉搜索树,它不仅满足二叉搜索树的性质,而且在二叉搜索树的基础上实现了自平衡,能使得左子树与右子树的高度差不超过1,从而提高了查找效率。

        AVLTree 是最早的平衡二叉树,其是由两位苏联数学家 Georgy Adelson-Velsky 和 Evgenii Landis 在 1962 年发表在 《An algorithm for the organization of information》论文上。AVLTree 的明名正式取自这两位发明者的名字首字母。

        在 AVLTree 中每个节点都有一个平衡因子 (balance factor),该平衡因子可以是左子树的高度减去右子树的高度,也可以是右子树的高度减去左子树的高度,我们这里选择平衡因子为右子树的高度减去左子树的高度,而对于 AVLTree 来说,每个节点左右子树的高度差不能超过1,所以平衡因子只能是 0、1、-1。这里的平衡因子不是必须的,但是有了平衡因子可以帮助我们观察和检测整棵 AVLTree 的结构。

        那么根据 BST 的结构,AVLTree 的结构也与之类似。AVLTree 要么是一棵空树,要么就是一棵如下结构的二叉树:

(1) 每个节点的左子树和右子树的高度差不能超过1,也就是平衡因子只能为 0、1、-1

(2) 每个节点的左子树又是一棵 AVLTree

(3) 每个节点的右子树又是一棵 AVLTree

以下的这棵树就是一棵 AVLTree:

红色的就是每个节点的平衡因子。需要注意的是 AVLTree 也是一棵 BST,在满足平衡的同时也需要满足二叉搜索树的规则。

        以下的这棵树就不是一棵 AVLTree:

因为其有一个节点的平衡因子为 2,大于1,所以其不是一棵 AVLTree。

        所以对于 AVLTree 来说,由于左右子树的高度差不能超过1,所以其高度会和完全二叉树类似,高度可以控制在 logN 级别,而二叉搜索树的搜索次数就是其高度次,所以 AVLTree 增删查改的效率就是 logN 级别,比起 BST 来说会有很大的提升。


2  AVLTree 的增删查改(不允许插入重复 key 的 key-value 场景)

1) AVLTree 的节点结构

        由于实现的 key-value 场景,所以在节点里面我们直接使用 pair<K, V> 对象来作为数据类型,另外左孩子、右孩子也必不可少。在上一小节里面,我们知道了在 AVLTree 里面可以利用一个平衡因子(balance factor)来帮助我们检测整棵 AVLTree 的结构,所以我们选择在节点结构里面再添加一个平衡因子 _bf,帮助我们检测与更新 AVL 树。

        那么节点里面有没有别的需要添加的成员变量呢?在这里我们选择添加一个 _parent 指针变量来指向当前节点的父亲节点。添加该成员变量的原因是因为在后面更新 AVL 树的结构时方便找到父节点,但是你不添加 _parent 也是可以的,只是添加了之后更加方便。

template<class K, class V> struct AVLNode { pair<K, V> _data; AVLNode<K, V> _left; AVLNode<K, V> _right; AVLNode<K, V> _parent; int _bf; AVLNode(const pair<K, V>& data) :_data(data) ,_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_bf(0) {} };

        那么有了节点结构之后,整棵树的结构也就很好写了。因为只需要有一个根就可以找到整棵树,那么我们只需要一个根节点的成员变量就可以了:

template<class K, class V> class AVLTree { typedef AVLNode<K, V> Node; public: private: Node* _root = nullptr; };

2) AVLTree 的插入

        由于 AVL 树依然是一棵 BST,所以插入节点前面情况与 BST 插入是相同的,也就是:

(1) 如果树为空,直接创建当前节点

(2) 创建 parent 指针与 cur 指针,parent = nullptr, cur = _root

(3) 根据要插入数据 key 的大小来判断插入到左边还是右边,直到 cur 为空

(4) 创建节点,改变 parent 节点指向与 cur 中的 _parent 指针

具体代码为:

bool Insert(const pair<K, V>& data) { if (_root == nullptr) { _root = new Node(data); return true; } Node* cur = _root, *parent = nullptr; while (cur) { if (data.first > cur->_data.first) { parent = cur; cur = cur->_right; } else if (data.first < cur->_data.first) { parent = cur; cur = cur->_left; } else return false; } //找到了该插入的位置 cur = new Node(data); //更改parent 指向 if (data.first > parent->_data.first) parent->_right = cur; else parent->_left = cur; //不要忘记更改 cur 的 _parent 指针 cur->_parent = parent; return true; }

但是在 AVLNode 中还有一个 _bf 平衡因子成员,我们需要来更新它。更新平衡因子比较复杂,接下来我们来看一下如何更新平衡因子。

(1) 如何更新平衡因子

平衡因子更新原则:

        之前在 AVLTree 的概念那一小节我们选择平衡因子 = 右子树高度  - 左子树高度,所以在新插入一个节点之后,首先就会对其 parent 节点的高度产生影响,我们需要先更新 parent 节点的平衡因子,也就是如果在右子树插入,那么右子树的高度就会 +1,所以 ++_bf 就可以;如果插入到左子树,那么左子树的高度就会 +1,需要 --_bf。那么这里只更新 parent 节点的平衡因子就可以吗?我们来看这么一种情况:

红色节点为新插入的节点,将 parent 节点的平衡因子更新为了 -1,但是看 38 的 parent 节点 30,其右子树的高度变为了 2,所以应该也将其更新为 1,对于根节点也是一样的,由于其右子树节点高度变为了 3,所以其平衡因子应该更新为 0。所以插入一个节点之后,不仅要更新当前节点父亲节点的平衡因子,其所有祖先的平衡因子都要更新。究其原因就是因为如果插入节点的 parent 子树高度变化了,其所有祖先的高度也必然会发生变化,所以所有祖先的平衡因子都要更新。

总结一下平衡因子更新原则

a.  平衡因子 = 右子树高度 - 左子树高度

b.  插入在左子树,parent 的平衡因子 ++;插入在右子树,parent 的平衡因子 --

c.  如果 parent 的子树高度变化了,就需要向上更新,直到其祖先节点的高度不变或者更新到根节点

平衡因子停止更新的条件:

a.  如果 parent 节点的平衡因子更新为了 0,也就是由 1->0 或者 -1->0,说明更新之后,其左右子树的高度相等了,那就不需要再进行更新了,更新结束

b.  如果 parent 节点的平衡因子更新为了 1 或者 -1,那就说明其子树的高度改变了,那就需要继续向上更新

c.  如果 parent 节点的平衡因子更新为了 2 或者 -2,那就说明节点是插入到了更高的子树里,这时候整棵树已经不平衡了,所有的平衡因子不是 1,0,-1 了,这时候需要进行旋转处理,将 parent 及其子树重新旋转成为一棵 AVL 树,进行旋转之后,整棵树会恢复平衡,此时也不需要进行平衡因子的更新了,更新结束

d.  如果一直更新到根,更新完 _root 节点的平衡因子之后,更新也会结束

以下是对应于上面四种情况的图形:

a:

此时 parent->_bf 更新为 0,不影响其他祖先的平衡因子,所以更新结束。

b 上面已经演示过了,就不再演示。

c:

更新平衡因子时出现了 -2,此时需要进行旋转处理,旋转之后的树会变为:

旋转完之后,其就变成了一棵 AVL 树,所以就不需要更新平衡因子了,直接退出即可。

d 比较简单,就不演示了。

        所以插入完节点之后,我们需要更新平衡因子,而且需要更新到根。这部分代码为:

bool Insert(const pair<K, V>& data) { if (_root == nullptr) { _root = new Node(data); return true; } Node* cur = _root, *parent = nullptr; while (cur) { if (data.first > cur->_data.first) { parent = cur; cur = cur->_right; } else if (data.first < cur->_data.first) { parent = cur; cur = cur->_left; } else return false; } //找到了该插入的位置 cur = new Node(data); //更改parent 指向 if (data.first > parent->_data.first) parent->_right = cur; else parent->_left = cur; //不要忘记更改 cur 的 _parent 指针 cur->_parent = parent; //更新平衡因子 while (parent) { if (cur == parent->_left) --parent->_bf; else ++parent->_bf; //根据不同情况来判断是否向上更新 if (parent->_bf == 0) //不更新了,直接退出 break; else if (parent->_bf == 1 || parent->_bf == -1) { //继续向上更新 cur = parent; parent = parent->_parent; } else if (parent->_bf == 2 || parent->_bf == -2) { //旋转处理 //结束之后,也不需要更新了 break; } else { //为了处理平衡因子出错的情况 assert(false); } } return true; }

接下来就是 AVL 树里面最难的部分,也就是如何将一棵不平衡树旋转成为一棵平衡树,我们来仔细讲解这部分。

(2) AVL 树的旋转

        在 AVL 树中为了使得整棵树保持平衡的特性,以保持较高的查找效率,当有节点出现平衡因子为 2 或者 -2 时,AVL 树会进行旋转的操作,使得所有节点的平衡因子重新变为 1,0,-1。

        AVL 树中共有四种旋转方式,分别是右单旋、左单旋、左右双旋、右左双旋,分别对应四种不同的情况。

右单旋

        如果出现了以下的情况,我们会对整棵树进行右单旋:

当 10 插入在 15 的左子树里面时,使得本来就高的左子树变得更加高时,这时候就要采取右单旋的形式。如果用一个抽象图来表示的话就是这样的:

20 的平衡因子本来是 -1,左子树高度为 h+1,右子树高度为 h,但是在 14 的左子树 A 插入一个节点之后使得 20 的左子树高度变为了 h+2,也就是在本来就高的左子树又插入了一个节点,使得平衡因子变为了 -2,此时就需要使用右单旋来解决这种场景。

右单旋的旋转步骤

        这里就以上面那幅抽象图演示右单旋的具体步骤。

a.  先将 20 的左子树的根节点,也就是 14 旋转成为这棵树或者子树的根,将 20 变为 14 的右子树的根节点

b.  再将 14 的右子树变为 20 的左子树,也就是 B 变为 20 的左子树

按照上面的旋转步骤,旋转之后的树如图所示:

经过右单旋之后,树的高度由原来的 h+3 变为了旋转之后的 h+2,高度不仅变回了插入节点之前的高度,而且 14 和 20 的平衡因子也变为了 0。最重要的是,如果这棵树是一棵子树,其是不影响树中的其他部分的,而且整棵树的高度恢复成为了插入之前的高度,别的节点的平衡因子也不必改变,这样不仅完成了树平衡的调整,也完成了平衡因子的更新。

右单旋代码

void RotateR(Node* parent) { Node* Parent = parent->_parent; Node* subLR = parent->_left->_right; Node* subL = parent->_left; //不要忘记更改父亲的指向 //parent 可能为根节点 if (parent == _root) _root = subL; else { if (parent == Parent->_left) Parent->_left = subL; else Parent->_right = subL; } //subL 的父亲节点也需要更改 subL->_parent = Parent; //更改左右孩子指向 parent->_left = subLR; subL->_right = parent; //不要忘记更改 parent 以及 subLR 的父亲指向 parent->_parent = subL; if (subLR) subLR->_parent = parent; //最后不要忘记更新平衡因子 subL->_bf = parent->_bf = 0; }

在写代码的时候,不要只顾着更改左孩子与右孩子指针指向。由于在 AVLNode 中还有 _parent 指针,所以我们也需要更改 _parent 指针指向,在右单旋过程中,一共需要更改的节点有三个,前两个就是上面图中的 20 与 14,还有一个就是 14 右子树中的根节点,也就是 B 的根节点,这些节点的 _parent 指针都需要改变。但是 B 可能为空树,所以我们需要先判断 B 根节点是否为空,再去更改 _parent 指向。

左单旋

        左单旋与右单旋正好相反,但是大致逻辑是相同的,这里就只展示抽象图。

在右单旋中,是在高的左子树中再插入节点使之变得更高,会用右单旋;而在左单旋中,适合于在高的右子树插入节点,使右子树变得更高的情况。整棵子树的根节点平衡因子会由原来的 1 变为 2。

左单旋的旋转步骤

        还是以上面那幅抽象图来说明左单旋的旋转步骤。

a.  先将 20 的右子树的根节点,也就是 28 旋转成为这棵树或者子树的根,将 20 变为 28 的左子树的根节点

b.  再将 28 的左子树变为 20 的右子树,也就是 B 变为 20 的右子树

旋转之后的树如图所示:

和右单旋一样,左单旋之后的树依然会恢复到之前树的高度,也就是 h+2;同时,20 与 28 的平衡因子也同步更新为 0。

左单旋代码

void RotateL(Node* parent) { Node* Parent = parent->_parent; Node* subR = parent->_right; Node* subRL = subR->_left; //不要忘记更改父亲的指向 //parent 可能为根节点 if (parent == _root) _root = subR; else { if (parent == Parent->_left) Parent->_left = subR; else Parent->_right = subR; } //subR 的父亲节点也需要更改 subR->_parent = Parent; //更改孩子指向 subR->_left = parent; parent->_right = subRL; //不要忘记更改 parent 与 subRL 的父亲指向 parent->_parent = subR; if (subRL) subRL->_parent = parent; //更新平衡因子 parent->_bf = subR->_bf = 0; }

依然是不要忘记更改 _parent 指针的指向与更新平衡因子。

左右双旋

        如果出现了以下场景,是会采用左右双旋的场景:

与右单旋类似,都是在高的左子树里面插入节点使得左子树变得更高;但是与右单旋不同的是,其并不是在其左子树的左子树里面插入,而是在其左子树的右子树里面插入,也就是不在 A 里面插入,而在 B 里面插入时,会采用左右双旋的方式来恢复平衡。

左右双旋的旋转步骤

        根据名字来看,左右双旋具体的旋转步骤是先用一次左单旋,再用一次右单旋。具体的旋转步骤如下:

a.  先以 14 为父亲节点,进行一次左单旋,也就是 RotateL(14)

b.  再以 20 为父亲节点进行一次右单旋,也就是 RotateR(20)

但是呢,左右双旋具体又分为三种情况。但是这三种情况仅仅是平衡因子更新不同,旋转步骤都是相同的:

情况一

第一种情况是因为插入在了 16 的左子树从而导致 20 的平衡因子由 -1 变为了 -2,旋转之后的树如图。

经过旋转之后,原来的孙子节点 16 现在变成了整棵子树的根节点,平衡因子更新为了 0;原来的祖先节点 14 和 20 分别变成了 16 的左孩子和右孩子,平衡因子分别更新为了 0 和 1;同时整棵树的高度也变成了插入之前的高度 h+2,所以旋转之后也不必更新其他节点的平衡因子了。

情况二

情况二是因为在 16 的右子树中插入使得 20 的左子树变高。旋转之后的树如图所示。

情况二中 16 的平衡因子依旧为 0,但是 20 的平衡因子变为了 0,14 的平衡因子变为了 -1。

情况三

第三种情况比较特殊,就是在 14 为叶子节点,且 20 右子树为空的情况。此时在 14 的右子树插入节点,使得 20 的左子树变高。旋转之后的树如图所示。

旋转之后,三个节点的平衡因子都变为了 0。

左右双旋代码

void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; //先以 subL 为 parent 进行左单旋 RotateL(subL); //再以 parent 为 parent 进行右单旋 RotateR(parent); //根据三种情况进行平衡因子的更新 //插入到 subLR 的左子树 if (bf == -1) { subL->_bf = 0; parent->_bf = 1; subLR->_bf = 0; } //插入到 subLR 的右子树 else if (bf == 1) { subL->_bf = -1; parent->_bf = 0; subLR->_bf = 0; } //subLR 为叶子节点 else if (bf == 0) { subL->_bf = parent->_bf = 0; } else { //进行差错处理 assert(false); } }

在这里面就不需要更新 _parent 指针了,因为 RotateL 与 RotateR 函数会帮助我们更新的。

右左双旋

        右左双旋与左右双旋恰好相反,与左单旋的情况类似,在下面这种情况下会进行右左双旋。

就是在原本更高的右子树里再插入节点,只不过不是在 28 的右子树中插入,而是在左子树中插入;即不是在 C 中插入,而是在 B 中插入,这时候就需要进行右左双旋。

右左双旋的旋转步骤

        从名字来看,右左双旋呢就是先经历一次右单旋,再进行一次左单旋。以上面的抽象图来说步骤就是:

a.  先以 28 为父亲节点进行一次右单旋,也就是 RotateR(28)

b.  再以 20 为父亲节点进行一次左单旋,也就是 RotateL(20)

右左双旋呢,也分为三种情况,与左右双旋的三种情况类似。具体三种情况如图。

情况一

情况一是在 20 的右子树的 28 的左子树 25 的右子树中插入节点导致右子树高,进行右左双旋后的树如图所示。

旋转之后,25 与 28 的平衡因子变为了 0, 20 的平衡因子变为了 -1。

情况二

节点因为插入在了 20 的孙子节点 25 的左子树而导致右子树变高,进行右左双旋后的树如图所示。

此时平衡因子是 25 和 20 更新为了 0, 28 更新为了 1。

情况三

第三种情况就是插入的 25 为叶子节点的情况,此时右左双旋之后的树如图所示。

此时三个节点的平衡因子都更新到了 0。

右左双旋代码

void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; //先以 subR 为 parent 进行右单旋 RotateR(subR); //再以 parent 为 parent 进行左单旋 RotateL(parent); //根据不同情况更新平衡因子 if (bf == 0) { subR->_bf = parent->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; subRL->_bf = 0; } else if (bf == 1) { parent->_bf = -1; subR->_bf = 0; subRL->_bf = 0; } else { assert(false); } }

        上面就是旋转的四种情况,尤其是双旋,比较复杂,但是理清了逻辑之后还是很好实现的。


3) AVLTree 的查找

        查找的话就很简单了,跟 BST 的查找逻辑相同,这里就不再赘述。只不过对于 AVL 树来说,其查找效率是 O(logN) 的,效率更高。

查找代码

Node* Find(const K& key) { Node* cur = _root; while (cur) { if (key > cur->_data.first) cur = cur->_right; else if (key < cur->_data.first) cur = cur->_left; else return cur; } return nullptr; }

3  AVLTree 的检测

        由于 AVL 树的结构比较复杂,所以我们这里可以采用一定的算法来检测当前这棵树是否是 AVL 树,这里我们采用检测每个节点平衡因子的方法来检测是否是一棵 AVL 树。

        既然检测平衡因子,首先我们先需要一个计算二叉树高度的函数,之后我们计算出左子树的高度与右子树的高度,用右子树高度减去左子树高度就得到了当前节点的平衡因子,然后我们检测平衡因子的绝对值是否大于等于 2,如果是,那就说明高度出问题了;如果平衡因子与当前节点的平衡因子不相等,那就说明是平衡因子出问题了。

计算二叉树高度

        这个函数我们在二叉树时实现过,首先如果是空树,那高度肯定为0;然后我们递归的计算左子树高度,再递归的计算右子树高度;最后返回的是左右子树高度中较大值 + 1。

代码

int Height(Node* root) { if (root == nullptr) return 0; int LeftHeight = Height(root->_left); int RightHeight = Height(root->_right); return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1; }

检测是否平衡

        这个函数也很简单。首先,如果是一棵空树,那肯定是一棵 AVL 树;然后计算出左子树高度,再计算出右子树高度,用右子树高度减去左子树高度得到平衡因子,然后判断平衡因子绝对值是否大于等于 2 或者是否与当前节点平衡因子相等;最后再递归的检查左子树与右子树是否是二叉平衡树就可以了。

代码

bool _IsBalance(Node* root) { //空树也是一棵AVL树 if (root == nullptr) return true; int LeftHeight = Height(root->_left); int RightHeight = Height(root->_right); int dif = RightHeight - LeftHeight; if (dif >= 2 || dif <= -2) { std::cout << root->_data.first << "高度差异常" << std::endl; return false; } if (dif != root->_bf) { std::cout << root->_data.first << "平衡因子异常" << std::endl; return false; } //如果左边与右边都是AVL树,那就是一棵AVL树 return _IsBalance(root->_left) && _IsBalance(root->_right); }

4  总结

        在这里给出 AVLTree 的所有实现代码:

//AVLTree.hpp #pragma once #include <iostream> #include <assert.h> using namespace std; template<class K, class V> struct AVLNode { pair<K, V> _data; AVLNode<K, V>* _left; AVLNode<K, V>* _right; AVLNode<K, V>* _parent; int _bf; AVLNode(const pair<K, V>& data) :_data(data) ,_left(nullptr) , _right(nullptr) ,_parent(nullptr) ,_bf(0) {} }; template<class K, class V> class AVLTree { typedef AVLNode<K, V> Node; void RotateR(Node* parent) { Node* Parent = parent->_parent; Node* subLR = parent->_left->_right; Node* subL = parent->_left; //不要忘记更改父亲的指向 //parent 可能为根节点 if (parent == _root) _root = subL; else { if (parent == Parent->_left) Parent->_left = subL; else Parent->_right = subL; } //subL 的父亲节点也需要更改 subL->_parent = Parent; //更改左右孩子指向 parent->_left = subLR; subL->_right = parent; //不要忘记更改 parent 以及 subLR 的父亲指向 parent->_parent = subL; if (subLR) subLR->_parent = parent; //最后不要忘记更新平衡因子 subL->_bf = parent->_bf = 0; } void RotateL(Node* parent) { Node* Parent = parent->_parent; Node* subR = parent->_right; Node* subRL = subR->_left; //不要忘记更改父亲的指向 //parent 可能为根节点 if (parent == _root) _root = subR; else { if (parent == Parent->_left) Parent->_left = subR; else Parent->_right = subR; } //subR 的父亲节点也需要更改 subR->_parent = Parent; //更改孩子指向 subR->_left = parent; parent->_right = subRL; //不要忘记更改 parent 与 subRL 的父亲指向 parent->_parent = subR; if (subRL) subRL->_parent = parent; //更新平衡因子 parent->_bf = subR->_bf = 0; } void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; //先以 subL 为 parent 进行左单旋 RotateL(subL); //再以 parent 为 parent 进行右单旋 RotateR(parent); //根据三种情况进行平衡因子的更新 //插入到 subLR 的左子树 if (bf == -1) { subL->_bf = 0; parent->_bf = 1; subLR->_bf = 0; } //插入到 subLR 的右子树 else if (bf == 1) { subL->_bf = -1; parent->_bf = 0; subLR->_bf = 0; } //subLR 为叶子节点 else if (bf == 0) { subL->_bf = parent->_bf = 0; } else { //进行差错处理 assert(false); } } void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; //先以 subR 为 parent 进行右单旋 RotateR(subR); //再以 parent 为 parent 进行左单旋 RotateL(parent); //根据不同情况更新平衡因子 if (bf == 0) { subR->_bf = parent->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; subRL->_bf = 0; } else if (bf == 1) { parent->_bf = -1; subR->_bf = 0; subRL->_bf = 0; } else { assert(false); } } int _Height(Node* root) const { if (root == nullptr) return 0; int LeftHeight = _Height(root->_left); int RightHeight = _Height(root->_right); return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1; } bool _IsBalance(Node* root) const { //空树也是一棵AVL树 if (root == nullptr) return true; int LeftHeight = _Height(root->_left); int RightHeight = _Height(root->_right); int dif = RightHeight - LeftHeight; if (dif >= 2 || dif <= -2) { std::cout << root->_data.first << "高度差异常" << std::endl; return false; } if (dif != root->_bf) { std::cout << root->_data.first << "平衡因子异常" << std::endl; return false; } //如果左边与右边都是AVL树,那就是一棵AVL树 return _IsBalance(root->_left) && _IsBalance(root->_right); } size_t _size(Node* root) const { //利用递归来算节点个数 if (root == nullptr) return 0; return _size(root->_left) + _size(root->_right) + 1; } public: bool Insert(const pair<K, V>& data) { if (_root == nullptr) { _root = new Node(data); return true; } Node* cur = _root, *parent = nullptr; while (cur) { if (data.first > cur->_data.first) { parent = cur; cur = cur->_right; } else if (data.first < cur->_data.first) { parent = cur; cur = cur->_left; } else return false; } //找到了该插入的位置 cur = new Node(data); //更改parent 指向 if (data.first > parent->_data.first) parent->_right = cur; else parent->_left = cur; //不要忘记更改 cur 的 _parent 指针 cur->_parent = parent; //更新平衡因子 while (parent) { if (cur == parent->_left) --parent->_bf; else ++parent->_bf; //根据不同情况来判断是否向上更新 if (parent->_bf == 0) //不更新了,直接退出 break; else if (parent->_bf == 1 || parent->_bf == -1) { //继续向上更新 cur = parent; parent = parent->_parent; } else if (parent->_bf == 2 || parent->_bf == -2) { //旋转处理 if (parent->_bf == -2) { Node* subL = parent->_left; if (subL->_bf == -1) { //右单旋 RotateR(parent); } else if (subL->_bf == 1) { //左右双旋 RotateLR(parent); } else { assert(false); } } else { Node* subR = parent->_right; if (subR->_bf == 1) { //左单旋 RotateL(parent); } else if (subR->_bf == -1) { //右左双旋 RotateRL(parent); } else { assert(false); } } //结束之后,也不需要更新了 break; } else { //为了处理平衡因子出错的情况 assert(false); } } return true; } Node* Find(const K& key) { Node* cur = _root; while (cur) { if (key > cur->_data.first) cur = cur->_right; else if (key < cur->_data.first) cur = cur->_left; else return cur; } return nullptr; } size_t Height() const { return _Height(_root); } bool IsBalance() const { return _IsBalance(_root); } size_t size() const { return _size(_root); } private: Node* _root = nullptr; }; //testAVLTree.cpp #include "AVLTree.hpp" #include <vector> int main() { //这里直接采用随机值测试 const int N = 100000; vector<int> v; v.reserve(N); srand(time(NULL)); for (size_t i = 0; i < N; i++) { v.push_back(rand() + i); } //测试性能 size_t begin1 = clock(); AVLTree<int, int> t; for (auto& e : v) { t.Insert({ e, e }); } size_t end1 = clock(); cout << "Insert: " << end1 - begin1 << endl; cout << "is balance: " << t.IsBalance() << endl; cout << "Height: " << t.Height() << endl; cout << "Size: " << t.size() << endl; //查找 size_t begin2 = clock(); for (size_t i = 0; i < N; i++) { t.Find(rand() + i); } size_t end2 = clock(); cout << "Find: " << end2 - begin2 << endl; return 0; }

        这篇文章主要是讲解第一个二叉平衡树 -- AVL 树的实现,其中最难的就是如何旋转来实现树的平衡。旋转共包括 4 种旋转:右单旋、左单旋、左右双旋、右左双旋,双旋中又各有三种不同的旋转场景。

        本篇文章并没有讲解删除节点,如果大家有兴趣可以自己学习之后实现一下。

Read more

DeepSeek各版本说明与优缺点分析_deepseek各版本区别

DeepSeek各版本说明与优缺点分析 DeepSeek是最近人工智能领域备受瞩目的一个语言模型系列,其在不同版本的发布过程中,逐步加强了对多种任务的处理能力。本文将详细介绍DeepSeek的各版本,从版本的发布时间、特点、优势以及不足之处,为广大AI技术爱好者和开发者提供一份参考指南。 1. DeepSeek-V1:起步与编码强劲 DeepSeek-V1是DeepSeek的起步版本,这里不过多赘述,主要分析它的优缺点。 发布时间: 2024年1月 特点: DeepSeek-V1是DeepSeek系列的首个版本,预训练于2TB的标记数据,主打自然语言处理和编码任务。它支持多种编程语言,具有强大的编码能力,适合程序开发人员和技术研究人员使用。 优势: * 强大编码能力:支持多种编程语言,能够理解和生成代码,适合开发者进行自动化代码生成与调试。 * 高上下文窗口:支持高达128K标记的上下文窗口,能够处理较为复杂的文本理解和生成任务。 缺点: * 多模态能力有限:该版本主要集中在文本处理上,缺少对图像、语音等多模态任务的支持。 * 推理能力较弱:尽管在自然语言

By Ne0inhk

用DeepSeek和Cursor从零打造智能代码审查工具:我的AI编程实践

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群👉 点此查看详情 引言:AI编程革命下的机遇与挑战 GitHub统计显示,使用AI编程工具的开发者平均效率提升55%,但仅有23%的开发者能充分发挥这些工具的潜力。作为一名全栈工程师,我曾对AI编程持怀疑态度,直到一次紧急项目让我彻底改变了看法。客户要求在72小时内交付一个能自动检测代码漏洞、优化性能的智能审查系统,传统开发方式根本不可能完成。正是这次挑战,让我探索出DeepSeek和Cursor这对"黄金组合"的惊人潜力。 一、工具选型:深入比较主流AI编程工具 1.1 为什么最终选择DeepSeek+Cursor? 经过两周的对比测试,我们发现不同工具在代码审查场景的表现差异显著: 工具代码理解深度响应速度定制灵活性多语言支持GitHub Copilot★★★☆★★★★★★☆★★★★Amazon CodeWhisperer★★☆★★★☆★★★★★★☆DeepSeek★★★★☆★★★★★★★☆★★★★☆Cursor★★★☆★★★★☆★★★★★★★★ 关键发现: * Dee

By Ne0inhk
【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱

【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱

【DeepSeek应用】Deepseek R1 本地部署(Ollama+Docker+OpenWebUI) 【DeepSeek应用】DeepSeek 搭建个人知识库(Ollama+CherryStudio) 【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱 【DeepSeek应用】Zotero+Deepseek 阅读与分析文献 【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱 * 1. DeepSeek 工具箱:应用程序 * 2. DeepSeek 工具箱:AI Agent 框架 * 3. DeepSeek 工具箱:RAG 框架 * 4. DeepSeek 工具箱:即时通讯软件 * 5. DeepSeek 工具箱:浏览器插件 * 6. DeepSeek 工具箱:

By Ne0inhk
“现在的AI就像1880年的笨重工厂!”微软CSO斯坦福泼冷水:别急着造神

“现在的AI就像1880年的笨重工厂!”微软CSO斯坦福泼冷水:别急着造神

大模型仍未对上商业的齿轮? 编译 | 王启隆 来源 | youtu.be/aWqfH0aSGKI 出品丨AI 科技大本营(ID:rgznai100) 现在的硅谷,空气里都飘着一股“再不上车就晚了”的焦躁感。 最近 OpenClaw 风头正旺,强势登顶 GitHub,终结了 React 神话,许多人更是觉得“AI 自己干活赚钱”的日子就在明天了。 特别是在斯坦福商学院(GSB)这种地方,台下坐着的都是成天琢磨怎么用下一个技术风口搞个独角兽出来的狠人。 微软的首席科学官(CSO)Eric Horvitz 被请到了这个几乎全美最想用 AI 变现的礼堂里。作为从上世纪 80 年代就开始搞 AI 的绝对老炮、也是微软技术底座的“扫地僧”,这位老哥并没有顺着台下的胃口,去吹捧下个月大模型又要颠覆什么行业,而是兜头给大家浇了一盆带点学术味的冷水。 他讲了一个挺有画面感的比喻:大家都在聊

By Ne0inhk