当前位置: 首页 > news >正文

《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现

摘要

本文深入探讨了 AVL 树(自平衡二叉搜索树)的概念、特点以及实现细节。我们首先介绍了 AVL 树的基本原理,并详细分析了其四种旋转操作,包括左旋、右旋、左右双旋和右左双旋,阐述了它们在保持树平衡中的重要作用。接着,本文从头到尾详细描述了 AVL 树的插入、删除和查找操作,配合完整的代码实现和详尽的注释,使读者能够全面理解这些操作的执行过程。

此外,我们还提供了 AVL 树的遍历方法,包括中序、前序和后序遍历,帮助读者更好地掌握 AVL 树的结构和节点间关系。通过对 AVL 树的优缺点进行分析,本文揭示了其在不同应用场景中的适用性,为读者选择合适的数据结构提供了参考。通过对 AVL 树的全面解析,本文旨在为读者提供一个完整的学习路径,帮助他们掌握这一强大的数据结构。


1、引言

在计算机科学中,数据结构是程序设计的基石,而二叉搜索树 (BST) 则是最基本的树形数据结构之一。二叉搜索树提供了高效的查找、插入和删除操作,但在最坏情况下,这些操作的时间复杂度可能退化为线性。这种退化主要发生在 BST 呈现为链状结构时,例如在顺序插入数据时。因此,为了保证二叉搜索树的性能,出现了多种自平衡树,如红黑树、AVL树、B树等。


关于二叉搜索树的更多细节请见我的这篇博客:《 C++ 修炼全景指南:九 》打破编程瓶颈!掌握二叉搜索树的高效实现与技巧
关于红黑树的更多细节请见我的这篇博客:《 C++ 修炼全景指南:十一 》穿越数据的红与黑:掌握数据平衡的极致艺术


在这些自平衡树中,AVL 树是最早被发明的一种。它由 Adelson-VelskyLandis 于 1962 年提出,因其发明者名字的首字母缩写而得名。AVL 树在插入和删除节点后,通过旋转操作保持树的平衡,从而确保所有操作的时间复杂度始终维持在 O(log⁡n) 。这一特性使得 AVL 树成为实现高效动态集合的一种重要选择。本文将深入探讨AVL树的结构、插入和删除操作的实现,以及其在实际应用中的优缺点。


本篇博客所涉及的所有代码均可从我的代码仓库获得:https://git.lenyiin.com/Lenyiin/AVLTree


2、AVL树概述

AVL 树是一种自平衡二叉搜索树,每个节点都存储一个平衡因子(Balance Factor,简称 BF),用于表示该节点的左子树和右子树的高度差。AVL 树的基本特性是,对于每个节点,要求其左子树和右子树的高度差至多为 1。因此,AVL 树的高度始终保持在 O(log⁡n) ,从而保证查找、插入和删除操作的时间复杂度为 O(log⁡n)

2.1、平衡因子

AVL 树中每个节点的平衡因子是该节点右子树的高度减去左子树的高度,即:

B F ( n o d e ) = h e i g h t ( r i g h t s u b t r e e ) − h e i g h t ( l e f t s u b t r e e ) BF(node)=height(right subtree)−height(left subtree) BF(node)=height(rightsubtree)height(leftsubtree)

  • 当平衡因子为 0 时,表示左子树和右子树高度相等。
  • 当平衡因子为正数时,表示右子树比左子树高。
  • 当平衡因子为负数时,表示左子树比右子树高。

AVL树通过限制每个节点的平衡因子在-1、0、1之间,确保树始终保持平衡状态。

2.2、旋转操作

为了保持AVL树的平衡,在插入或删除节点后,如果某个节点的平衡因子超出范围(即绝对值大于1),则需要进行旋转操作来恢复平衡。AVL树主要有四种旋转操作:

  • 单右旋(LL型):用于修复左子树的左子节点插入导致的不平衡。
  • 单左旋(RR型):用于修复右子树的右子节点插入导致的不平衡。
  • 先左旋后右旋(LR型):用于修复左子树的右子节点插入导致的不平衡。
  • 先右旋后左旋(RL型):用于修复右子树的左子节点插入导致的不平衡。

通过这些旋转操作,AVL树能够在插入和删除节点后迅速恢复平衡,从而保持其高效的操作性能。

2.3、AVL树的应用场景

AVL 树适用于需要频繁查找和插入操作的数据场景,并且对查询操作的效率要求较高。例如,数据库索引、内存中的有序集合、符号表等。虽然 AVL 树在插入和删除操作中需要更多的旋转操作来保持平衡,因此可能不如红黑树等自平衡树高效,但在需要严格平衡以保证查询效率的场景下,AVL 树仍然是一种理想的选择。

2.4、本文的结构

接下来,本文将详细阐述 AVL 树的节点结构、插入与删除操作的具体实现以及旋转操作的原理。随后,我们将对 AVL 树的优缺点进行分析,并探讨其在实际应用中的表现。通过对 AVL 树的全面分析,读者将能够深入理解这一数据结构,并在实际开发中灵活运用。


3、AVL 树的实现

3.1、AVL树节点结构

首先,我们需要定义一个基本的AVL树节点结构。该结构体将包含节点的键值对、左右子节点、父节点,以及用于维护树平衡的平衡因子。平衡因子表示节点右子树高度与左子树高度的差值。

template <class K, class V>
struct AVLTreeNode 
{AVLTreeNode<K, V>* _left;  // 左子节点AVLTreeNode<K, V>* _right; // 右子节点AVLTreeNode<K, V>* _parent; // 父节点int _bf; // 平衡因子 balance factorstd::pair<K, V> _kv; // 键值对// 构造函数AVLTreeNode(const std::pair<K, V>& kv): _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0), _kv(kv) {}
};

详细解释

  • _left:指向左子节点的指针。
  • _right:指向右子节点的指针。
  • _parent:指向父节点的指针,用于在平衡调整时便于向上回溯。
  • _bf:平衡因子,用于判断树是否需要旋转。平衡因子为右子树高度减去左子树高度。
  • _kv:存储节点的键值对。

接下来,我们将基于这个节点结构体,开始AVL树的实现。我们将逐步构建各个功能模块,包括插入、删除、平衡操作等。


3.2、插入操作

插入节点时,首先遵循二叉搜索树的规则进行节点的插入,然后通过调整平衡因子和旋转来维护树的平衡。

bool Insert(const std::pair<K, V>& kv)
{// 1. 先按搜索树的规则进行插入if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;	// 如果相等, 表示已经有了, 不需要插入}}// 找到位置cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;cur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}// 2. 更新平衡因子while (parent){if (cur == parent->_right){parent->_bf++;	// 插在右边, 平衡因子++}else{parent->_bf--;	// 插在左边, 平衡因子--}if (parent->_bf == 0){// 说明 parent 所在的子树高度不变, 更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){// 说明 parent 所在子树的高度变了, 继续往上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent 所在的子树出现问题了, 需要旋转处理一下// 1. 旋转前提是保持它依旧是搜索二叉树// 2. 旋转成平衡树if (parent->_bf == 2){if (cur->_bf == 1){// 左旋RotateL(parent);}else if (cur->_bf == -1){// 右左双旋RotateRL(parent);}}else if (parent->_bf == -2){if (cur->_bf == -1){// 右旋RotateR(parent);}else if (cur->_bf == 1){// 左右双旋RotateLR(parent);}}// 旋转完成后, parent 所在的树的高度恢复到了插入节点前的高度// 如果是子树, 对上一层没有影响, 更新结束break;}}return true;
}

详细解释

  • Insert:插入节点的函数。它根据键值对的大小关系选择插入到左子树还是右子树。

  • _bf 的调整:如果插入在左子树,平衡因子减 1;如果插入在右子树,平衡因子加 1。

  • 插入节点后,我们需要检查树是否仍然平衡。如果树失去平衡(平衡因子的绝对值大于1),就需要执行旋转操作来恢复平衡。

    • RotateL(右子树的右子节点插入):左单旋
    • RotateR(左子树的左子节点插入):右单旋
    • RotateRL(右子树的左子节点插入):右左双旋。
    • RotateLR(左子树的右子节点插入):先左旋后右旋。

3.3、旋转操作

在 AVL 树中,保持树的平衡性是至关重要的,这主要通过四种旋转操作来实现:左旋、右旋、左-右双旋和右-左双旋。这些旋转操作用于修复因插入或删除节点导致的树不平衡,确保 AVL 树的高度维持在 O(log⁡n) 。下面,我们详细描述这四种旋转方式。

3.3.1、左单旋

场景:当新节点插入到某个节点的右子树的右子节点时,可能导致该节点失衡。此时,需要进行单左旋操作来恢复平衡。

旋转过程

  • 假设不平衡节点为 A,其右子节点为 B
  • B 的左子树 BL 将成为 A 的右子树。
  • B 成为新的子树根节点,而 A 成为 B 的左子节点。

示例图

插入导致的结构:

  A\B\C

单左旋后的结构:

    B/ \A   C

代码实现

// 左单旋
void RotateL(Node* parent)
{Node* ppNode = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}subR->_left = parent;parent->_parent = subR;// 1. 原来 parent 是这棵树的根, 现在 subR 是根if (parent == _root){_root = subR;subR->_parent = nullptr;}// 2. parent 为根的树只是整棵树中的子树, 改变链接关系, 那么 subR 要顶替他的位置else{if (ppNode->_left == parent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}parent->_bf = subR->_bf = 0;
}

解释

  • subR 指向 A 的右子节点 B
  • B 的左子树 subRL 被挂接到 A 的右子树。
  • B 成为新的根节点,A 成为 B 的左子节点。

3.3.2、右单旋

场景:当新节点插入到某个节点的左子树的左子节点时,可能导致该节点失衡。此时,需要进行右单旋操作来恢复平衡。

旋转过程

  • 假设不平衡节点为 A,其左子节点为 B
  • B 的右子树 BR 将成为 A 的左子树。
  • B 成为新的子树根节点,而 A 成为 B 的右子节点。

示例图

插入导致的结构:

    A/ B   / 
C 

单右旋后的结构:

    B/ \C   A

代码实现

// 右单旋
void RotateR(Node* parent)
{Node* ppNode = parent->_parent;Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;}parent->_bf = subL->_bf = 0;
}

详细解释

  • parent 是节点 A
  • subL 指向 A 的左子节点 B
  • B 的右子树 subLR 被挂接到 A 的左子树。
  • B 成为新的根节点,A 成为 B 的右子节点。

3.3.3、左右双旋

场景:当新节点插入到某个节点的左子树的右子节点时,可能导致该节点失衡。此时,需要先对其左子树进行左旋,再对整个子树进行右旋来恢复平衡。

旋转过程

  1. 左旋:对左子节点 B 进行单左旋,使得 B 的右子节点 C 成为新的根节点。
  2. 右旋:对不平衡节点 A 进行单右旋,使得 C 成为新的根节点。

示例图

插入导致的结构:

    A/B\C

左旋后的中间结构:

    A/C/
B

右旋后的最终结构:

    C/ \B   A

代码实现

// 左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);RotateR(parent);if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}
}

解释

  • 首先对 A 的左子节点 B 进行单左旋,使得 C 成为新的子树根节点。
  • 然后对不平衡节点 A 进行单右旋,使得 C 成为新的根节点。

3.3.4、右左双旋

场景:当新节点插入到某个节点的右子树的左子节点时,可能导致该节点失衡。此时,需要先对其右子树进行右旋,再对整个子树进行左旋来恢复平衡。

旋转过程

  1. 右旋:对右子节点 B 进行单右旋,使得 B 的左子节点 C 成为新的根节点。
  2. 左旋:对不平衡节点 A 进行单左旋,使得 C 成为新的根节点。

示例图

插入导致的结构:

  A\B/C

右旋后的中间结构:

  A\C\B

左旋后的最终结构:

    C/ \A   B

代码实现

// 右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);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 if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}
}

解释

  • 首先对 A 的右子节点 B 进行单右旋,使得 C 成为新的子树根节点。
  • 然后对不平衡节点 A 进行单左旋,使得 C 成为新的根节点。

3.3.5、旋转操作总结

通过这四种旋转操作,AVL 树能够在插入或删除节点后迅速恢复平衡。无论是单旋转还是双旋转,这些操作的时间复杂度都为 O(1) ,因为它们只涉及常数级别的节点重新链接。这使得 AVL 树在保持自平衡状态的同时,能够保证所有基本操作的时间复杂度为 O(log⁡n)


3.4、删除操作

AVL 树中,节点的删除操作相对复杂,因为删除节点后不仅要移除目标节点,还需要重新平衡树,以保持 AVL 树的平衡性质。AVL 树的删除操作可以分为以下几个步骤:

  1. 找到并删除节点:和普通的二叉搜索树(BST)删除操作类似。
  2. 调整平衡因子:从删除的节点开始,沿着路径向上调整每个节点的平衡因子。
  3. 执行旋转操作:在调整平衡因子的过程中,如果发现某个节点失衡(平衡因子的绝对值大于1),需要进行相应的旋转操作来恢复平衡。

我们将按照这三个步骤详细解释AVL树中节点的删除操作。

3.4.1、找到并删除节点

首先,我们需要在AVL树中找到要删除的节点,并按照 BST 的删除规则将其删除。BST中的删除操作有以下几种情况:

  • 节点是叶子节点:直接删除该节点即可。
  • 节点有一个子节点:用其唯一的子节点替换它。
  • 节点有两个子节点:找到该节点的中序后继节点,用后继节点的值替换该节点的值,然后删除后继节点。

3.4.2、调整平衡因子

在删除节点后,需要调整其祖先节点的平衡因子。沿着从删除节点到根节点的路径进行调整,规则如下:

  • 如果删除的是左子树的节点,则父节点的平衡因子增加1。
  • 如果删除的是右子树的节点,则父节点的平衡因子减少1。

在调整平衡因子的过程中,如果某个节点的平衡因子的绝对值超过1,就需要通过旋转来恢复平衡。

3.4.3、执行旋转操作

在删除节点并调整平衡因子后,如果发现某个节点失衡,则需要根据不同的失衡情况进行旋转操作。

3.4.4、总结

删除操作是AVL树中较为复杂的一部分。需要仔细处理删除节点的不同情况,及时更新平衡因子,并在必要时进行旋转操作以保持树的平衡。每个步骤都需要在 O(log⁡n) 的时间内完成,确保 AVL 树的效率。通过精心设计的平衡调整机制,AVL 树在删除操作后能够快速恢复平衡,保持所有操作的时间复杂度为 O(log⁡n)


3.5、遍历操作

在 AVL 树中,遍历是一项基础操作,用于访问树中的每个节点。AVL 树继承了二叉搜索树(BST)的性质,因此可以使用与 BST 相同的遍历方法,包括前序遍历(Pre-order Traversal)、中序遍历(In-order Traversal)、后序遍历(Post-order Traversal)、层序遍历(Level-order Traversal)等。下面将详细介绍这些遍历方法,并给出相应的代码实现。

3.5.1、前序遍历(Pre-order Traversal)

前序遍历的顺序是:先访问根节点,再访问左子树,最后访问右子树。其遍历顺序为 “根 -> 左 -> 右”。

前序遍历的递归实现

// 前序遍历的递归实现
// 辅助函数
void _PreOrder(Node* root)
{if (root == nullptr){return;}std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;   // 访问根节点_PreOrder(root->_left);   // 访问左子树_PreOrder(root->_right);  // 访问右子树
}void PreOrder()
{_PreOrder(_root);std::cout << std::endl;
}

前序遍历的非递归实现(使用栈):

// 前序遍历的非递归实现
void PreOrder()
{if (_root == nullptr){return;}std::stack<Node*> stack;stack.push(_root);while (!stack.empty()) {Node* cur = stack.top();stack.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;  // 访问当前节点// 先压入右子树再压入左子树(确保左子树先处理)if (cur->_right != nullptr){stack.push(cur->_right);}if (cur->_left != nullptr) {stack.push(cur->_left);}}std::cout << std::endl;
}

3.5.2、中序遍历(In-order Traversal)

中序遍历的顺序是:先访问左子树,再访问根节点,最后访问右子树。其遍历顺序为 “左 -> 根 -> 右”。对于 AVL 树,中序遍历会得到一个按序排列的节点序列。

中序遍历的递归实现

// 中序遍历
void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;_InOrder(root->_right);
}void InOrder()
{_InOrder(_root);std::cout << std::endl;
}

中序遍历的非递归实现(使用栈):

// 中序遍历 非递归
void InOrder()
{std::stack<Node*> stack;Node* cur = _root;while (cur != nullptr || !stack.empty()) {// 找到最左边的节点while (cur != nullptr) {stack.push(cur);cur = cur->_left;}// 处理当前节点cur = stack.top();stack.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;// 转向右子树cur = cur->_right;}std::cout << std::endl;
}

3.5.3、后序遍历(Post-order Traversal)

后序遍历的顺序是:先访问左子树,再访问右子树,最后访问根节点。其遍历顺序为 “左 -> 右 -> 根”。

后序遍历的递归实现

// 后序遍历的递归实现
// 辅助函数
void _PostOrder(Node* root) {if (root == nullptr){return;}_PostOrder(root->_left);    // 访问左子树_PostOrder(root->_right);   // 访问右子树std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;     // 访问根节点
}void PostOrder()
{_PostOrder(_root);std::cout << std::endl;
}

后序遍历的非递归实现(使用栈):

// 后序遍历的非递归实现
void PostOrder()
{if (_root == nullptr){return;}std::stack<Node*> st;Node* cur = _root;Node* lastNode = nullptr; // 最近访问的节点while (cur || !st.empty()){while (cur){st.push(cur);cur = cur->_left;}Node* top = st.top();if ((top->_right == nullptr) || (lastNode == top->_right)){std::cout << top->_kv.first << ":" << top->_kv.second << std::endl;lastNode = top;st.pop();}else {cur = top->_right;}}std::cout << std::endl;
}

3.5.4、层序遍历(Level-order Traversal)

层序遍历是按层次逐层访问节点,先访问根节点,再访问其左右子节点,依次类推。可以使用队列来实现。

层序遍历的实现

// 层序遍历
void LevelOrder()
{if (_root == nullptr){return;}std::queue<Node*> queue;queue.push(_root);while (!queue.empty()){Node* cur = queue.front();queue.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;   // 访问当前节点if (cur->_left != nullptr) {queue.push(cur->_left);      // 将左子树压入队列}if (cur->_right != nullptr) {queue.push(cur->_right);     // 将右子树压入队列}}std::cout << std::endl;
}

3.5.5、总结

AVL 树的遍历方法与普通二叉搜索树类似,包括前序遍历、中序遍历、后序遍历和层序遍历。通过递归非递归方式,我们可以访问 AVL 树的所有节点,并按照特定的顺序输出它们的键值。中**序遍历是最常用的遍历方法之一,特别是在需要输出一个有序序列时。层序遍历则常用于按层次输出或分析树的结构。**每种遍历方法都有其独特的应用场景,在编写和理解 AVL 树的相关算法时,选择合适的遍历方式十分重要。


3.6、查找操作

在AVL树中,查找操作主要依赖于树的有序性,即对于任意一个节点,左子树的所有节点都小于该节点,右子树的所有节点都大于该节点。查找过程从根节点开始,逐层向下比较查找值与当前节点的值,并根据比较结果选择进入左子树或右子树。

3.6.1、查找操作的步骤

  1. 从根节点开始:从AVL树的根节点开始查找。
  2. 比较当前节点的键值
    • 如果查找的键等于当前节点的键,查找成功,返回当前节点。
    • 如果查找的键小于当前节点的键,进入左子树继续查找。
    • 如果查找的键大于当前节点的键,进入右子树继续查找。
  3. 递归或迭代:重复上述步骤,直到找到匹配的节点或者子树为空(查找失败)。
  4. 返回结果:找到目标节点则返回,否则返回 nullptr(表示查找失败)。

3.6.2、查找的递归实现

递归实现相对简单,直接通过函数递归调用在树中查找目标节点。

// 查找的递归实现
// 辅助函数
Node* _find(Node* root, const K& key)
{if (root == nullptr || root->_kv.first == key){return root;}if (key < root->_kv.first){return _find(root->_left, key);}else{return _find(root->_right, key);}
}

3.6.3、查找的非递归实现

非递归实现使用迭代方式,避免了递归调用带来的额外栈空间开销。

// 查找 非递归
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_kv.first > key){cur = cur->_left;}else if (cur->_kv.first < key){cur = cur->_right;}else{return cur;}}return nullptr;
}

4、AVL 树的其他功能

除了基本的插入、查找、删除等操作,AVL 树还可以实现一些高级操作,如查找前驱和后继、区间查询、树的高度、判断是否是平衡二叉树等。这些操作可以扩展 AVL 树的功能,增加其实用性。

4.1、查找后继节点

前驱是指比某个节点值小的最大节点,后继则是比某个节点值大的最小节点。在 AVL 树中,查找前驱和后继是常见的操作,尤其在区间查询或范围搜索时非常有用。

查找后继的递归实现

// 查找后继节点
Node* findMin(Node* node)
{while (node->_left != nullptr){node = node->_left;}return node;
}Node* FindSuccessor(Node* node)
{if (node->_right != nullptr){return findMin(node->_right);}Node* cur = _root;Node* successor = nullptr;while (cur != nullptr){if (node->_kv.first < cur->_kv.first){successor = cur;cur = cur->_left;}else if (node->_kv.first > cur->_kv.first){cur = cur->_right;}else{break;}}return successor;
}

查找后继时,如果节点有右子树,则后继为右子树中最小的节点;否则,后继为第一个大于该节点的祖先节点。


4.2、查找前驱节点

查找前驱的递归实现

// 查找前驱节点
Node* findMax(Node* node)
{while (node->_right != nullptr){node = node->_right;}return node;
}Node* FindPredecessor(Node* node)
{if (node->_left != nullptr){return findMax(node->_left);}Node* cur = _root;Node* predecessor = nullptr;while (cur != nullptr){if (node->_kv.first < cur->_kv.first){cur = cur->_left;}else if (node->_kv.first > cur->_kv.first){predecessor = cur;cur = cur->_right;}else{break;}}return predecessor;
}

前驱的查找逻辑与后继类似,只是方向相反。


4.3、获取树的高度

// 获取树的高度
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;
}

4.4、判断是否是平衡二叉树

// 判断是否是平衡二叉树
bool _IsBalance(Node* root)
{if (root == nullptr){return true;}int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (abs(leftHeight - rightHeight) > 1){return false;}return _IsBalance(root->_left) && _IsBalance(root->_right);
}bool IsBalance()
{return _IsBalance(_root);
}

5、完整实现代码和测试

5.1、AVLTree.hpp

#pragma once#include <iostream>
#include <stack>
#include <queue>namespace Lenyiin
{template <class K, class V>struct AVLTreeNode{AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf;		// balance factor	平衡因子std::pair<K, V> _kv;	// key-value// 普通构造AVLTreeNode(const std::pair<K, V> &kv): _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0), _kv(kv){}};template <class K, class V>class AVLTree{private:typedef AVLTreeNode<K, V> Node;public:bool Insert(const std::pair<K, V>& kv){// 1. 先按搜索树的规则进行插入if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;	// 如果相等, 表示已经有了, 不需要插入}}// 找到位置cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;cur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}// 2. 更新平衡因子while (parent){if (cur == parent->_right){parent->_bf++;	// 插在右边, 平衡因子++}else{parent->_bf--;	// 插在左边, 平衡因子--}if (parent->_bf == 0){// 说明 parent 所在的子树高度不变, 更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){// 说明 parent 所在子树的高度变了, 继续往上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent 所在的子树出现问题了, 需要旋转处理一下// 1. 旋转前提是保持它依旧是搜索二叉树// 2. 旋转成平衡树if (parent->_bf == 2){if (cur->_bf == 1){// 左旋RotateL(parent);}else if (cur->_bf == -1){// 右左双旋RotateRL(parent);}}else if (parent->_bf == -2){if (cur->_bf == -1){// 右旋RotateR(parent);}else if (cur->_bf == 1){// 左右双旋RotateLR(parent);}}// 旋转完成后, parent 所在的树的高度恢复到了插入节点前的高度// 如果是子树, 对上一层没有影响, 更新结束break;}}return true;}// 左单旋void RotateL(Node* parent){Node* ppNode = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}subR->_left = parent;parent->_parent = subR;// 1. 原来 parent 是这棵树的根, 现在 subR 是根if (parent == _root){_root = subR;subR->_parent = nullptr;}// 2. parent 为根的树只是整棵树中的子树, 改变链接关系, 那么 subR 要顶替他的位置else{if (ppNode->_left == parent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}parent->_bf = subR->_bf = 0;}// 右单旋void RotateR(Node* parent){Node* ppNode = parent->_parent;Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;}parent->_bf = subL->_bf = 0;}// 右左双旋void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);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 if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}}// 左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);RotateR(parent);if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}}// 中序遍历//void _InOrder(Node* root)//{//	if (root == nullptr)//	{//		return;//	}//	_InOrder(root->_left);//	std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;//	_InOrder(root->_right);//}//void InOrder()//{//	_InOrder(_root);//	std::cout << std::endl;//}// 中序遍历 非递归void InOrder(){std::stack<Node*> stack;Node* cur = _root;while (cur != nullptr || !stack.empty()) {// 找到最左边的节点while (cur != nullptr) {stack.push(cur);cur = cur->_left;}// 处理当前节点cur = stack.top();stack.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;// 转向右子树cur = cur->_right;}std::cout << std::endl;}// 前序遍历的递归实现// 辅助函数//void _PreOrder(Node* root)//{//	if (root == nullptr)//	{//		return;//	}//	std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;   // 访问根节点//	_PreOrder(root->_left);   // 访问左子树//	_PreOrder(root->_right);  // 访问右子树//}//void PreOrder()//{//	_PreOrder(_root);//	std::cout << std::endl;//}// 前序遍历的非递归实现void PreOrder(){if (_root == nullptr){return;}std::stack<Node*> stack;stack.push(_root);while (!stack.empty()) {Node* cur = stack.top();stack.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;  // 访问当前节点// 先压入右子树再压入左子树(确保左子树先处理)if (cur->_right != nullptr){stack.push(cur->_right);}if (cur->_left != nullptr) {stack.push(cur->_left);}}std::cout << std::endl;}// 后序遍历的递归实现// 辅助函数//void _PostOrder(Node* root) {//	if (root == nullptr)//	{//		return;//	}//	_PostOrder(root->_left);    // 访问左子树//	_PostOrder(root->_right);   // 访问右子树//	std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;     // 访问根节点//}//void PostOrder()//{//	_PostOrder(_root);//	std::cout << std::endl;//}// 后序遍历的非递归实现void PostOrder(){if (_root == nullptr){return;}std::stack<Node*> st;Node* cur = _root;Node* lastNode = nullptr; // 最近访问的节点while (cur || !st.empty()){while (cur){st.push(cur);cur = cur->_left;}Node* top = st.top();if ((top->_right == nullptr) || (lastNode == top->_right)){std::cout << top->_kv.first << ":" << top->_kv.second << std::endl;lastNode = top;st.pop();}else {cur = top->_right;}}std::cout << std::endl;}// 层序遍历void LevelOrder(){if (_root == nullptr){return;}std::queue<Node*> queue;queue.push(_root);while (!queue.empty()){Node* cur = queue.front();queue.pop();std::cout << cur->_kv.first << ":" << cur->_kv.second << std::endl;   // 访问当前节点if (cur->_left != nullptr) {queue.push(cur->_left);      // 将左子树压入队列}if (cur->_right != nullptr) {queue.push(cur->_right);     // 将右子树压入队列}}std::cout << std::endl;}// 查找后继节点Node* findMin(Node* node){while (node->_left != nullptr){node = node->_left;}return node;}Node* FindSuccessor(Node* node){if (node->_right != nullptr){return findMin(node->_right);}Node* cur = _root;Node* successor = nullptr;while (cur != nullptr){if (node->_kv.first < cur->_kv.first){successor = cur;cur = cur->_left;}else if (node->_kv.first > cur->_kv.first){cur = cur->_right;}else{break;}}return successor;}// 查找前驱节点Node* findMax(Node* node){while (node->_right != nullptr){node = node->_right;}return node;}Node* FindPredecessor(Node* node){if (node->_left != nullptr){return findMax(node->_left);}Node* cur = _root;Node* predecessor = nullptr;while (cur != nullptr){if (node->_kv.first < cur->_kv.first){cur = cur->_left;}else if (node->_kv.first > cur->_kv.first){predecessor = cur;cur = cur->_right;}else{break;}}return predecessor;}// 获取树的高度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;}// 判断是否是平衡二叉树bool _IsBalance(Node* root){if (root == nullptr){return true;}int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (abs(leftHeight - rightHeight) > 1){return false;}return _IsBalance(root->_left) && _IsBalance(root->_right);}bool IsBalance(){return _IsBalance(_root);}// 查找 非递归//Node* Find(const K& key)//{//	Node* cur = _root;//	while (cur)//	{//		if (cur->_kv.first > key)//		{//			cur = cur->_left;//		}//		else if (cur->_kv.first < key)//		{//			cur = cur->_right;//		}//		else//		{//			return cur;//		}//	}//	return nullptr;//}// 查找的递归实现// 辅助函数Node* _find(Node* root, const K& key){if (root == nullptr || root->_kv.first == key){return root;}if (key < root->_kv.first){return _find(root->_left, key);}else{return _find(root->_right, key);}}// 查找节点Node* Find(const K& key){return _find(_root, key);}private:Node* _root = nullptr;};
}

5.2、AVLTree.cpp

#include "AVLTree.hpp"using namespace Lenyiin;void test_AVLTree()
{//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};AVLTree<int, int> t;for (const auto& e : a){t.Insert(std::make_pair(e, e));}t.InOrder();t.PreOrder();t.PostOrder();t.LevelOrder();std::cout << t.IsBalance() << std::endl;auto ret = t.Find(6);if (ret == nullptr){std::cout << "没有找到" << std::endl;}else{std::cout << "找到了" << std::endl;std::cout << ret->_kv.first << ":" << ret->_kv.second << std::endl;}auto pre = t.FindPredecessor(ret);if (pre != nullptr){std::cout << ret->_kv.first << " 的前驱节点是: " << pre->_kv.first << std::endl;}else{std::cout << "前驱节点为空" << std::endl;}auto suc = t.FindSuccessor(ret);if (suc != nullptr){std::cout << ret->_kv.first << " 的后继节点是: " << suc->_kv.first << std::endl;}else{std::cout << "后继节点为空" << std::endl;}
}int main()
{test_AVLTree();return 0;
}

6、AVL树的优缺点和总结

6.1、优点

  1. 平衡性保证AVL 树是自平衡二叉搜索树,它严格保持了树的平衡状态。每个节点的左右子树高度差(平衡因子)最多为 1,因此在最坏情况下,AVL 树的高度被限制在 O(log⁡n) 级别。这保证了查找、插入、删除操作的时间复杂度始终为 O(log⁡n) ,使其在各种场景中表现出较高的效率。
  2. 快速查找:由于 AVL 树保持了较高的平衡性,查找操作的性能得到了优化。相比于未平衡的二叉搜索树,AVL 树在查找操作中能够更快地找到目标节点。
  3. 稳定的性能AVL 树的平衡性维护使得其在最坏情况下(例如频繁的插入和删除操作)也能提供稳定的性能。无论数据插入的顺序如何,AVL 树都能保持平衡,避免出现链表化的情况。
  4. 较好的空间利用率AVL 树在保持平衡的同时,并不需要额外的节点空间,除了存储平衡因子。相比红黑树等其他自平衡树,AVL 树的平衡性更严格,查找性能更好。

6.2、缺点

  1. 插入和删除的复杂性AVL 树在插入和删除节点时,需要进行旋转操作以保持树的平衡。每次插入或删除操作都可能导致树的不平衡,从而触发重新平衡过程。虽然旋转操作的时间复杂度为 O(1) ,但在频繁的插入和删除场景中,这些旋转操作可能增加整体的处理时间。
  2. 实现复杂性AVL 树的实现相对较复杂,尤其是插入和删除操作。与红黑树相比,AVL 树需要更多的旋转操作来维持平衡,这使得其代码编写和维护的难度较大。
  3. 额外的存储空间:为了维护平衡性,AVL 树的每个节点需要额外存储平衡因子(balance factor),这增加了节点的内存开销。虽然这一开销较小,但对于内存非常敏感的系统,可能会有一定的影响。
  4. 性能折衷:尽管 AVL 树在查找操作中表现优异,但其插入和删除操作的性能可能会受到影响。在一些场景中,如需要频繁插入和删除的情况下,红黑树可能比 AVL 树更具优势,因为红黑树的旋转操作次数通常比 AVL 树少。

6.3、总结

AVL 树是一种自平衡的二叉搜索树,它通过在每次插入和删除节点时自动调整自身结构,确保树的高度始终保持在 O(log⁡n) 级别,从而提供高效的查找、插入和删除操作。AVL 树的核心优势在于其严格的平衡性,这使得它在查找操作中具有较高的性能,并且在最坏情况下也能保持较优的时间复杂度。

然而,这种严格的平衡性也带来了更高的实现复杂性和插入、删除操作的额外开销。在需要频繁插入和删除的应用场景中,AVL 树的性能可能不如红黑树等其他自平衡树。此外,AVL 树的每个节点需要额外存储平衡因子,略微增加了内存开销。

尽管如此,AVL 树在追求高效查找操作的场景中仍然是一个非常实用的数据结构。它适用于需要快速数据访问、并且插入和删除操作相对较少的场景,如字典、集合等。在实际应用中,应根据具体的需求和场景,综合考虑选择适合的自平衡二叉树结构。

通过本博客对 AVL 树的全面阐述,希望能够帮助读者更好地理解和运用 AVL 树,并在实际开发中合理选择和实现这一数据结构。



希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。更多知识分享可以访问我的个人博客网站 : https://blog.lenyiin.com/ 。本博客所涉及的代码也可以访问我的 git 仓库获取 :https://git.lenyiin.com/Lenyiin/AVLTree



相关文章:

《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现

摘要 本文深入探讨了 AVL 树&#xff08;自平衡二叉搜索树&#xff09;的概念、特点以及实现细节。我们首先介绍了 AVL 树的基本原理&#xff0c;并详细分析了其四种旋转操作&#xff0c;包括左旋、右旋、左右双旋和右左双旋&#xff0c;阐述了它们在保持树平衡中的重要作用。…...

SAP 特别总账标识[SGL]

1. 特别总账标识(SGL)概述 1.1 定义与目的 特别总账标识&#xff08;Special General Ledger, SGL&#xff09;在SAP系统中用于区分客户或供应商的不同业务类型&#xff0c;以便将特定的业务交易记录到非标准的总账科目中。 定义&#xff1a;SGL是一个用于标记特殊业务类型的…...

认知杂谈77《简单:通往高手的技巧》

内容摘要&#xff1a;          在信息爆炸、关系复杂的时代&#xff0c;简单是复杂背后的真谛。简单如“112”&#xff0c;是智慧的朴素呈现。简单有强大力量&#xff0c;像清泉般纯净&#xff0c;如“我爱你”简单却有力&#xff0c;基础财务知识也体现其在理财中的作…...

《SmartX ELF 虚拟化核心功能集》发布,详解 80+ 功能特性和 6 例金融实践

《SmartX ELF 虚拟化核心功能集》电子书现已发布&#xff01;本书详细介绍了 SmartX ELF 虚拟化及云平台核心功能&#xff0c;包含虚机服务、容器服务、网络服务、存储服务、运维管理、工具服务、数据保护等各个方面。 即刻下载电子书&#xff0c;了解如何利用基于 SmartX ELF …...

9月23日

思维导图 作业 统计家目录下.c文件的个数 #!/bin/bashnum0for file in ~/*.c; doif [ -f "$file" ]; then((num))fi doneecho "家目录下.c文件的个数: $num"...

如何使用Jinja定义dbt宏

dbt宏在dbt框架内的工作方式与传统编程中的函数类似。它允许用户将特定的、通常是重复的SQL逻辑封装到可调用的命名单元中&#xff0c;就像在其他编程语言中用函数来避免重复代码一样&#xff1b;dbt宏定义特定业务的SQL逻辑&#xff0c;然后在dbt项目中需要的地方调用该宏函数…...

深入理解 JavaScript 三大作用域:全局作用域、函数作用域、块级作用域

一. 作用域 对于多数编程语言&#xff0c;最基本的功能就是能够存储变量当中的值、并且允许我们对这个变量的值进行访问和修改。那么有了变量之后&#xff0c;应该把它放在哪里、程序如何找到它们&#xff1f;是否需要提前约定好一套存储变量、访问变量的规则&#xff1f;答案…...

【门牌制作 / A】

题目 代码 #include <bits/stdc.h> using namespace std; int main() {int cnt 0;for (int i 1; i < 2020; i){string s;s to_string(i);cnt count(s.begin(), s.end(), 2);}cout << cnt; }...

Git+Jenkins 基本使用(Basic Usage of Git+Jenkins)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…...

智谱清言:智能语音交互的引领者,解锁高效沟通新体验

哪个编程工具让你的工作效率翻倍&#xff1f; 在日益繁忙的工作环境中&#xff0c;选择合适的编程工具已成为提升开发者工作效率的关键。不同的工具能够帮助我们简化代码编写、自动化任务、提升调试速度&#xff0c;甚至让团队协作更加顺畅。那么&#xff0c;哪款编程工具让你…...

前端组件库

vant2现在的地址 Vant 2 - Mobile UI Components built on Vue...

后端常用的mybatis-plus方法以及配合querywapper使用

目录 一、插入数据 save方法 二、删除操作 removeById方法 三、更新操作 updateById方法 四、查询操作 selectById方法 五、条件构造器QueryWrapper的更多用法 1.比较操作符 2.逻辑操作符 3.模糊查询 4.空值判断 一、插入数据 save方法 save(T entity):向数据库中插入…...

【设计模式】万字详解:深入掌握五大基础行为模式

作者&#xff1a;后端小肥肠 &#x1f347; 我写过的文章中的相关代码放到了gitee&#xff0c;地址&#xff1a;xfc-fdw-cloud: 公共解决方案 &#x1f34a; 有疑问可私信或评论区联系我。 &#x1f951; 创作不易未经允许严禁转载。 姊妹篇&#xff1a; 【设计模式】&#xf…...

C++ 9.19

练习&#xff1a;要求在堆区申请5个double类型的空间&#xff0c;用于存储5名学生的成绩。请自行封装函数完成 1> 空间的申请 2> 学生成绩的录入 3> 学生成绩的输出 4> 学生成绩进行降序排序 5> 释放申请的空间 主程序中用于测试上述函数 #include<ios…...

[Unity Demo]从零开始制作空洞骑士Hollow Knight第五集:再制作更多的敌人

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作敌人另个爬虫Crawler 1.公式化导入制作另个爬虫Crawler素材2.制作另个爬虫Crawler的Crawler.cs状态机3.制作敌人另个爬虫Crawler的playmaker状态机二、…...

怎么把excel翻译成英文?这些翻译技巧记得收藏

在处理Excel数据时&#xff0c;我们常常会遇到多语言的数据集&#xff0c;这无疑给数据分析和整理带来了不小的挑战。 幸运的是&#xff0c;随着技术的发展&#xff0c;现在有多种工具可以帮助我们进行Excel中的批量翻译&#xff0c;这些工具以其强大的翻译功能和便捷的操作方…...

信息技术引领的智能化未来

信息技术引领的智能化未来 随着信息技术的飞速发展&#xff0c;社会各个领域正在加速迈入智能化的新时代。信息技术的广泛应用&#xff0c;尤其是人工智能、大数据、物联网等前沿技术的创新与融合&#xff0c;正在从根本上改变着人们的生产和生活方式。本文将探讨信息技术在智…...

【QT开发-Pyside】使用Pycharm与conda配置Pyside环境并新建工程

知识拓展 Pycharm 是一个由 JetBrains 开发的集成开发环境&#xff08;IDE&#xff09;&#xff0c;它主要用于 Python 编程语言的开发。Pycharm 提供了代码编辑、调试、版本控制、测试等多种功能&#xff0c;以提高 Python 开发者的效率。 Pycharm 与 Python 的关系 Pycharm 是…...

vue选项式写法项目案例(购物车)

一、初始化项目结构 1.初始化vite项目 npm create vite cd vite-project npm install 2.清理项目结构 清空App.vue 删除components目录下的HelloWorld.vue组件 3.为组件的样式启用sacc或less组件 npm i sass4.初始化index.css全局样式 :root{font-size:12px } 二、封装…...

[Linux][进程] 认识进程

基本概念 进程是一个操作系统术语,用来管理与操作程序.在windows下打开任务管理器即可查看目前打开的所有进程 PCB 进程控制块,从代码层面来说 PCB 是进程所有属性的一个结构体,在Linux源码中PCB指的是struct task_struct. Linux环境下: 进程 task_struct 代码 …...

如何安装和注册 GitLab Runner

如何安装和注册 GitLab Runner GitLab Runner 是一个用于运行 GitLab CI/CD (Continuous Integration/Continuous Deployment) 作业。它是一个与 GitLab 配合使用的应用程序&#xff0c;可以在本地或云中运行。Runner 可以执行不同类型的作业&#xff0c;例如编译代码、运行测…...

专业学习|动态规划(概念、模型特征、解题步骤及例题)

一、引言 &#xff08;一&#xff09;从斐波那契数列引入自底向上算法 &#xff08;1&#xff09;知识讲解 &#xff08;2&#xff09;matlap实现递归 &#xff08;3&#xff09;带有备忘录的遗传算法 &#xff08;4&#xff09;matlap实现带有备忘录的递归算法 “&#xff1…...

数据结构与算法 #时间复杂度 #空间复杂度

文章目录 前言 一、算法的复杂度 二、时间复杂度 三、空间复杂度 四、例题 1、例1&#xff1a;冒泡排序 2、例2&#xff1a; 3、例3&#xff1a; 4、例4: 二分查找 5、例5: 阶乘 6、例6: 斐波那契 五、常见算法复杂度 总结 前言 路漫漫其修远兮&#xff0c;吾将上下而求索&…...

【多机器人轨迹规划最优解问题】

此类应用场景通常很难有严格意义上的最优解&#xff0c;一般只能得到较优解。限制其获得最优解的主要因素如下&#xff1a; 一、问题的复杂性 多机器人系统的高维度性&#xff1a;每台机器人都有自己的位置、速度、任务等多个状态变量&#xff0c;多台机器人组合在一起使得问…...

机器学习及其应用领域【金融领域】

机器学习及其应用领域【金融领域】 一、智能投顾与资产配置二、信贷审批与风险评估三、支付与交易安全四、金融欺诈检测五、市场预测与情绪分析六、客户服务与个性化推荐七、面临的挑战与未来趋势八、总结 一、智能投顾与资产配置 智能投顾&#xff1a;通过机器学习技术&#…...

【实战教程】PHP与七牛云的完美对接,你值得拥有!

前言&#xff1a; 随着互联网的迅速发展&#xff0c;越来越多的网站和应用程序需要处理大量的图片、视频和其他文件。为了有效地存储和管理这些文件&#xff0c;并提供快速的内容分发服务&#xff0c;开发者们常常依赖于云存储和CDN服务提供商。 七牛云是一家领先的云存储和CD…...

2024网易低代码大赛 | 想参赛但不知道搭什么?灵感就这么水灵灵地来了!

9月6日-10月15日&#xff0c;报名进行时&#xff01;戳我即可报名&#xff01; 如果你还没想好要开发什么作品来参赛&#xff0c;那就必须往下 我们采访了n位网易内部人士&#xff0c;搜罗了他们的建议&#xff0c;给你多一些灵感&#xff01; 注意&#xff1a;下文仅为本次比赛…...

(附源码)基于django的电力工程作业现场物资管理系统的设计与实现-计算机毕设 22067

基于django的电力工程作业现场物资管理系统的设计与实现 摘 要 随着电力工程的快速发展&#xff0c;作业现场物资管理成为保障工程进度和质量的关键环节。本文旨在设计并实现一个基于Django框架的电力工程作业现场物资管理系统&#xff0c;以提高物资管理的效率和准确性。该系统…...

数据链路层协议 —— 以太网协议

目录 1.数据链路层解决的问题 2.局域网通信方式 以太网 令牌环网 无线局域网 3.以太网协议 以太网帧格式 对比理解Mac地址和IP地址 认识MTU MTU对IP协议的影响 MTU对UDP的影响 MTU对TCP的影响 基于以太网协议的报文转发流程 交换机的工作原理 4.ARP协议 ARP协议…...

【Javascript】一文看懂JS中的symbol到底是什么东西

作为一名经验丰富的 JavaScript 开发者&#xff0c;你可能对 JavaScript 中的各种数据类型已经了如指掌&#xff0c;比如数字、字符串、布尔值和对象。但是你知道吗&#xff1f;JavaScript 还有一种叫做 Symbol 的类型。在这篇文章里&#xff0c;我们将深入探讨 Symbol 的世界&…...