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

C++ AVL树详解(含模拟实现)

目录

AVL树的概念

AVL树节点的定义

AVL树的插入

AVL树的旋转(难点)

AVL树的验证

AVL树的删除(本文不做具体的模拟实现)

AVL树的性能

AVL树的模拟实现


AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

因此,两位俄罗斯的数学家G.M.Adelson - Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1 / 0 / 1) -- 注:之所以保持 1 是因为:左右高度差没办法保证能一直相等(例如只有两个结点呢?)

注:这里的 平衡因子 并不是必须的,这只是它的一种实现方式。

如果一棵二叉搜索树是高度平衡的,它就是AVL树。

如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)。

AVL树节点的定义

template<class T>
struct AVLTreeNode
{AVLTreeNode(const T& data): _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _data(data), _bf(0){}AVLTreeNode<T>* _pLeft;   // 该节点的左孩子AVLTreeNode<T>* _pRight;  // 该节点的右孩子AVLTreeNode<T>* _pParent; // 该节点的双亲T _data;int _bf;				  // 该节点的平衡因子
};

AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:

1. 按照二叉搜索树的方式插入新节点

2. 调整节点的平衡因子 (插入一个结点,只会影响到它的祖先的平衡因子)

  • a、如果插入在双亲的左边,双亲平衡因子--
  • b、如果插入在双亲的右边,双亲的平衡因子++

   当经过a、b两步情况过后,此时又可以分为接下来的三种情况

  • c、双亲的平衡因子 == 0,此时双亲所在的子树高度不变,就不再需要继续往上更新平衡因子了,本次插入就算结束了。
  • d、双亲的平衡因子 == 1 or -1,此时双亲所在的子树高度变了,即需要继续往上更新平衡因子(按照 a、b 的规则继续更新),直到到达 c 的情况/出现 e 的情况。
  • e、双亲的平衡因子 == 2 or -2,此时双亲所在的子树已经不平衡了,需要旋转处理。

以上就是 AVL树的插入 的主要流程。

AVL树的旋转(难点)

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

根据节点插入位置的不同,AVL树的旋转分为四种:

1. 新节点插入较高左子树的左侧---左左:右单旋

2. 新节点插入较高右子树的右侧---右右:左单旋

3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋

4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋

注:旋转首先要保证一件事,就是旋转完后这棵树还是符合搜索二叉树的规则的树!!

总结:

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者 - 2,分以下情况考虑:

1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR

  • a、当pSubR的平衡因子为 1 时,执行左单旋
  • b、当pSubR的平衡因子为 -1 时,执行右左双旋

2. pParent的平衡因子为 -2,说明pParent的左子树高,设pParent的左子树的根为pSubL

  • a、当pSubL的平衡因子为 -1 时,执行右单旋
  • b、当pSubL的平衡因子为 1 时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡(新的pParent的平衡因子为0),不需要再向上更新。

补充:如果是单纯的单旋,旋转完之后参与旋转的两个根结点的平衡因子都会变成0。如果是双旋,旋转完之后参与旋转的三个根结点的平衡因子会受一开始新节点插入的位置影响。

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

1.验证其为二叉搜索树

   如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。

2.验证其为平衡树

   每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子),节点的平衡因子是否计算正确。

AVL树的删除(本文不做具体的模拟实现)

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,最差情况下一直要调整到根节点的位置。

具体实现可参考《算法导论》(基本都是伪代码,不一定好看懂) 或《数据结构 - 用面向对象方法与C++描述》殷人昆版。

注:删除调整平衡因子的判断条件和插入不同,删除是当平衡因子为 0 时,就继续往上更新,为 1 或 -1 才停止,为 2 或 -2 就要旋转。

主要逻辑:

  1. 先按搜索树的规则进行删除
  2. 再进行平衡因子的更新(出现不平衡旋转一下)

AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log(N)。但是如果要对AVL树做一些结构修改的操作时,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

AVL树的模拟实现

AVLTree.h:

#pragma once#include <iostream>
#include <vector>
#include <assert.h>template <class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V> *_left;AVLTreeNode<K, V> *_right;AVLTreeNode<K, V> *_parent; // 注意:AVL树 这里还需要多一个 _parent,方便往上更新 平衡因子 // 没有 _parent 会很麻烦std::pair<K, V> _kv;int _bf; // balance factorAVLTreeNode(const std::pair<K, V> &kv): _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};template <class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;public:bool insert(const std::pair<K, V> &kv){if (_root == nullptr){_root = new Node(kv);return true;}Node *parent = nullptr;Node *cur = _root;while (cur){parent = cur;if ((cur->_kv).first < kv.first){cur = cur->_right;}else if ((cur->_kv).first > kv.first){cur = cur->_left;}else{return false;}}cur = new Node(kv);if ((parent->_kv).first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent; // AVL树的 插入这里要比 普通搜索二叉树 的插入多一步// 到这里,上面的代码仅仅只是完成了插入一个结点的工作// 接下来,开始更新平衡因子 _bfwhile (parent) // 可能存在一直更新都没触发下面的两个break的情况,根据分析一直更新下去,最后 parent 是变成 nullptr。{// 向左插入,--_bfif (cur == parent->_left){--parent->_bf;}// 向右插入,++_bfelse{++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 && cur->_bf == -1) // 右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋{RotateL(parent);}// 注:一正一负就是双旋else if (parent->_bf == 2 && cur->_bf == -1) // 右左双旋 {RotateRL(parent);}else if (parent->_bf == -2 && cur->_bf == 1) // 左右双旋{RotateLR(parent);}// 旋转完了,也可以直接结束循环// 原因:旋转之后,该出问题的子树变平衡了,并且该子树的高度和未插入这个最新结点时的高度是一样的。break;}else // 理论上不可能还有其他情况,出现其他情况证明你代码写出问题了{assert(false); // 直接断言报错}}return true;}void RotateR(Node *parent) // 右单旋 // 这里的 parent 的 _bf == -2{// 要抬高左边,降低右边Node *subL = parent->_left; // 这里的 subL 的 _bf 为 -1Node *subLR = subL->_right; // subL 的右子树 要给给 parent,当 parent 的左子树// subL的值 < subLR的值 < parent的值// 处理一个节点的同时也要记得处理它的父亲parent->_left = subLR; // 将 subL 的右子树给给 parentif (subLR)			   // 这边要注意 subLR 可能是 nullptrsubLR->_parent = parent;subL->_right = parent; // 提高左边subL->_parent = parent->_parent; // 要注意,要先把 parent 的 parent 给给 subLif (parent->_parent)			 // 还要注意改 parent 的 parent  // 因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subL;}else{parent->_parent->_right = subL;}}else // 如果原本的 parent 是根的话,就更新一下根{_root = subL;}parent->_parent = subL; // 降低右边// 根据图分析,旋转完之后,subL的_bf 和 parent的_bf 都为 0// 所以,更新一下两者的_bfparent->_bf = subL->_bf = 0;}void RotateL(Node *parent) // 左单旋 // 这里的 parent 的 _bf == 2{// 要抬高右边,降低左边Node *subR = parent->_right; // 这里的 subR 的 _bf 为 1Node *subRL = subR->_left;	 // subR 的左子树 要给给 parent,当 parent 的右子树// parent的值 < subRL的值 < subR的值// 处理一个节点的同时也要记得处理它的父亲parent->_right = subRL; // 将 subR 的右子树给给 parentif (subRL)				// 这边要注意 subRL 可能是 nullptrsubRL->_parent = parent;subR->_left = parent; // 提高右边subR->_parent = parent->_parent; // 要注意,要先把 parent 的 parent 给给 subRif (parent->_parent)			 // 还要注意改 parent 的 parent  // 因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subR;}else{parent->_parent->_right = subR;}}else // 如果原本的 parent 是根的话,就更新一下根{_root = subR;}parent->_parent = subR; // 降低右边// 根据图分析,旋转完之后,subR的_bf 和 parent的_bf 都为 0// 所以,更新一下两者的_bfparent->_bf = subR->_bf = 0;}// 注:双旋的难点在于对节点的平衡因子的调节void RotateRL(Node *parent) // 右左双旋(就是对 左单旋和右单旋 的封装) // 这里的 parent 的 _bf == 2{// 因为单旋会导致这几个结点的平衡因子都直接变为0,这可能和实际分析的最终结果不符,需要一些特殊处理Node *subR = parent->_right; // 这里的 subR 的 _bf 为 -1Node *subRL = subR->_left;int bf = subRL->_bf;// 先对 parent->_right 进行右单旋,然后这颗子树就变为了单纯的右边高RotateR(parent->_right);// 再对 parent 自己本身进行左单旋即可RotateL(parent);// 调整为正确的平衡因子(虽然上面的旋转函数对平衡因子的改变有一部分是正确的,但我们下面还是最好全都给它更新一下,做到万无一失)subRL->_bf = 0; // subRL 去做新的根了,根据分析,不管什么情况新的根的_bf都为0。if (bf == -1)	// 代表一开始在 subRL 的左边插入{subR->_bf = 1;	 // subRL 的右子树(h-1)给了 subR 做左子树parent->_bf = 0; // subRL 的左子树(h)给了 parent 做右子树}else if (bf == 1) // 代表一开始在 subRL 的右边插入{subR->_bf = 0;	  // subRL 的右子树(h)给了 subR 做左子树parent->_bf = -1; // subRL 的左子树(h-1)给了 parent 做右子树}else if (bf == 0) // 代表 subRL 自己就是新增的结点{subR->_bf = 0;parent->_bf = 0;}else // 理论不应该还有其他的情况{assert(false);}}void RotateLR(Node *parent) // 左右双旋(就是对 左单旋和右单旋 的封装) // 这里的 parent 的 _bf == -2{// 因为单旋会导致这几个结点的平衡因子都直接变为0,这可能和实际分析的最终结果不符,需要一些特殊处理Node *subL = parent->_left; // 这里的 subL 的 _bf 为 1Node *subLR = subL->_right;int bf = subLR->_bf;// 先对 parent->_left 进行左单旋,然后这颗子树就变为了单纯的左边高RotateL(parent->_left);// 再对 parent 自己本身进行右单旋即可RotateR(parent);// 调整为正确的平衡因子(虽然上面的旋转函数对平衡因子的改变有一部分是正确的,但我们下面还是最好全都给它更新一下,做到万无一失)subLR->_bf = 0; // subLR 去做新的根了,根据分析,不管什么情况新的根的_bf都为0。if (bf == -1)	// 代表一开始在 subLR 的左边插入{subL->_bf = 0;	 // subLR 的左子树(h)给了 subL 做右子树parent->_bf = 1; // subLR 的右子树(h-1)给了 parent 做左子树}else if (bf == 1) // 代表一开始在 subLR 的右边插入{subL->_bf = -1;	 // subLR 的左子树(h-1)给了 subL 做右子树parent->_bf = 0; // subLR 的右子树(h)给了 parent 做左子树}else if (bf == 0) // 代表 subLR 自己就是新增的结点{subL->_bf = 0;parent->_bf = 0;}else // 理论不应该还有其他的情况{assert(false);}}Node *Find(const std::pair<K, V> &kv){Node *cur = _root;while (cur){if ((cur->_kv).first < kv.first){cur = cur->_right;}else if ((cur->_kv).first > kv.first){cur = cur->_left;}else{return cur;}}return nullptr;}void InOrder(){_InOrder(_root);std::cout << std::endl;}bool IsBalance() // 下面的遍历打印,只能证明这是一颗搜索二叉树,并不能说明它是否平衡{return _IsBalance(_root);}int Height() // 返回树的高度{return _Height(_root);}int Size() // 计算树的大小{return _Size(_root);}private:bool _IsBalance(Node *root) // 我们要做的事就是不断的去看每个结点的平衡因子就好(直接前序遍历就行){if (root == nullptr)return true;if (root->_bf <= -2 || 2 <= root->_bf) // 这也算剪枝return false;bool left = _IsBalance_k(root->_left);if (left == false)return false; // 剪枝bool right = _IsBalance_k(root->_right);if (right == false)return false; // 剪枝return left && right;}int _Size(Node *root){return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;}int _Height(Node *root){if (root == nullptr)return 0;return std::max(_Height(root->_left), _Height(root->_right)) + 1;}void _InOrder(Node *root /* = _root */) // 注意:这里不能给缺省值 _root,因为 _root 需要this指针调用,但是this指针本身就是形参,这样写玩不了。{if (root == nullptr)return;_InOrder(root->_left);													  // 左std::cout << (root->_kv).first << ":" << (root->_kv).second << std::endl; // 根_InOrder(root->_right);													  // 右}private:Node *_root = nullptr;
};void TestAVL_Insert_Balance1()
{int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};int a1[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};int a2[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};AVLTree<int, int> t;for (auto &x : a2){t.insert({x, x});std::cout << x << "->" << t.IsBalance() << std::endl;}t.InOrder(); // 这里有个问题,没法给 InOrder() 这个函数传参,因为 _root 是私有函数,你在这里调不动。// 那么该怎么解决呢?// 给个缺省值吗? 这是不行的,给不了// 那该怎么办?// 三种方法:1、把这个测试函数定义成友元。(这个方法很不好,就一个测试函数又不是要经常用,定义成友元有点太没边界感了)//           2、学Java,弄一个 Get() 函数,把 _root 拿出来。//			 3、看上面的操作。(封装一下,套一层)std::cout << t.IsBalance() << std::endl;
}
// 补充:对于这种比较复杂的程序,如果哪里出了bug,有个好方法就是在某些关键的步骤或循环里加个打印,这种方式比较容易知道哪里出错了。void TestAVL_Insert_Balance2()
{const int N = 100000000; // 注:32位环境下,这里插入 1亿 个节点会失败,因为空间不够,new 爆了srand((unsigned int)time(nullptr));std::vector<int> v(N);AVLTree<int, int> t;for (int i = 0; i < N; ++i){v[i] = rand() + i;}for (auto x : v){t.insert({x, x});}std::cout << "t.Height():" << t.Height() << std::endl;std::cout << "t.Size():" << t.Size() << std::endl;std::cout << "t.IsBalance():" << t.IsBalance() << std::endl;size_t begin = clock();for (auto x : v){t.Find({ x,x });}size_t end = clock();std::cout << "t.Find():" << end - begin << std::endl;
}
// 注:程序主要时间消耗在于插入时的 new 节点。

相关文章:

C++ AVL树详解(含模拟实现)

目录 AVL树的概念 AVL树节点的定义 AVL树的插入 AVL树的旋转&#xff08;难点&#xff09; AVL树的验证 AVL树的删除(本文不做具体的模拟实现) AVL树的性能 AVL树的模拟实现 AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索…...

Spring Boot 3.x 系列【3】Spring Initializr快速创建Spring Boot项目

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Spring Boot版本3.0.3 源码地址&#xff1a;https://gitee.com/pearl-organization/study-spring-boot3 文章目录 前言安装JDK 17创建Spring Boot 项目 方式1&#xff1a;网页在线生成方式2&#…...

Elasticsearch:过滤 HNSW 搜索,快速模式

作者&#xff1a;来自 Elastic Benjamin Trent 通过我们的 ACORN-1 算法实现&#xff0c;探索我们对 Apache Lucene 中的 HNSW 向量搜索所做的改进。 多年来&#xff0c;Apache Lucene 和 Elasticsearch 一直支持使用 kNN 查询的过滤搜索&#xff0c;允许用户检索符合指定元数据…...

TCP长连接与短连接

TCP长连接与短连接 TCP&#xff08;传输控制协议&#xff09;中的长连接和短连接是两种不同的连接管理方式&#xff0c;各有优缺点&#xff1a; 短连接 短连接是指客户端与服务器完成一次数据交换后就断开连接。下次需要通信时&#xff0c;再重新建立连接。 特点&#xff1…...

【AI测试学习】AnythingLLM+Ollama+DeepSeek部署私人知识库

1.搭建DeepSeek大语言模型 1.1Ollama大预言模型部署 Ollama简化了大型语言模型的运行,让每个人都能在本地轻松体验AI的强大,打开浏览器-下载Ollama-输入命令-搞定,这是本地部署大语言模型的全新方式。 这里我们借助Ollama大预言模型部署工具进行搭建 官网如下:Ollama …...

防流、节抖、重绘、回流原理,以及实现方法和区别

防流、节抖、重绘、回流原理&#xff0c;以及实现方法和区别&#xff0c;还有就是为什么会出现这种情况&#xff1f; 防抖&#xff08;Debounce&#xff09; 原理 防抖就像是你坐电梯&#xff0c;如果你一直不停地按开门按钮&#xff0c;电梯不会每次都开门&#xff0c;而是…...

通义灵码插件安装入门教学 - IDEA(安装篇)

在开发过程中&#xff0c;使用合适的工具和插件可以极大地提高我们的工作效率。今天&#xff0c;我们将详细介绍如何在 IntelliJ IDEA 中安装并配置通义灵码插件&#xff0c;这是一款旨在提升开发者效率的实用工具。无论你是新手还是有经验的开发者&#xff0c;本文都将为你提供…...

ES、OAS、ERP、电子政务、企业信息化(高软35)

系列文章目录 ES、OAS、ERP、电子政务、企业信息化 文章目录 系列文章目录前言一、专家系统&#xff08;ES&#xff09;二、办公自动化系统&#xff08;OAS&#xff09;三、企业资源规划&#xff08;ERP&#xff09;四、典型信息系统架构模型1.政府信息化和电子政务2.企业信息…...

用大白话解释缓存Redis +MongoDB是什么有什么用怎么用

Redis和MongoDB是什么&#xff1f; Redis&#xff1a;像你家的“小冰箱”&#xff0c;专门存高频使用的食物&#xff08;数据&#xff09;。它是基于内存的键值数据库&#xff0c;读写速度极快&#xff08;每秒超10万次操作&#xff09;。比如你每次打开手机App&#xff0c;用…...

华为数通Datacom认证体系详解:从HCIA到HCIE的进阶路径

华为数通Datacom&#xff08;Data Communication&#xff09;课程是华为认证体系中的核心方向之一&#xff0c;聚焦企业网络通信与数据通信技术&#xff0c;适合从事网络规划、部署和运维的人员。 一、数通Datacom课程体系 华为数通Datacom认证分为 三个级别&#xff0c;逐级递…...

PyTorch 的 nn.NLLLoss:负对数似然损失全解析

PyTorch 的 nn.NLLLoss&#xff1a;负对数似然损失全解析 在 PyTorch 的损失函数家族中&#xff0c;nn.NLLLoss&#xff08;Negative Log Likelihood Loss&#xff0c;负对数似然损失&#xff09;是一个不太起眼但非常重要的成员。它经常跟 LogSoftmax 搭配出现&#xff0c;尤…...

《数据安全架构设计与实战》的目录

《数据安全架构设计与实战》的目录如下1&#xff1a; 第一部分 安全架构基础 第 1 章 架构第 2 章 安全架构 2.1 什么是安全2.2 为什么使用 “数据安全” 这个术语2.3 什么是安全架构2.4 安全架构 5A 方法论2.5 安全架构 5A 与 CIA 的关系 第二部分 产品安全架构 …...

python-leetcode-删除并获得点数

740. 删除并获得点数 - 力扣&#xff08;LeetCode&#xff09; 解法 1&#xff1a;动态规划&#xff08;O(n) 时间&#xff0c;O(n) 空间&#xff09; class Solution:def deleteAndEarn(self, nums: List[int]) -> int:if not nums:return 0# 统计每个数的贡献points Cou…...

助力DeepSeek私有化部署服务:让企业AI落地更简单、更安全

在数字化转型的浪潮中&#xff0c;越来越多的企业选择私有化部署AI技术&#xff0c;以保障数据安全、提升业务效率并实现自主可控。DeepSeek作为行业领先的AI开源技术&#xff0c;其技术可以支持企业私有化部署&#xff0c;企业需要一站式服务私有化部署&#xff0c;涵盖硬件采…...

Android JNI开发指南

了解Android JNI开发。首先&#xff0c;我需要回忆一下JNI的基本概念和作用。JNI允许Java代码与本地代码&#xff08;如C/C&#xff09;交互&#xff0c;这在需要高性能或者使用现有C/C库时非常有用。 接下来&#xff0c;我应该整理开发步骤。用户可能想知道如何开始&#xff…...

【每天认识一个漏洞】url重定向

&#x1f31d;博客主页&#xff1a;菜鸟小羊 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 常见应用场景 主要是业务逻辑中需要进行跳转的地方。比如登录处、注册处、访问用户信息、订单信息、加入购物车、分享、收…...

纯代码实战--用Deepseek+SQLite+Ollama搭建数据库助手

如何用Python调用本地模型实现DeepSeek提示词模板&#xff1a;一步步教你高效解决13种应用场景 从零到一&#xff1a;纯代码联合PyQt5、Ollama、Deepseek打造简易版智能聊天助手 用外接知识库武装大模型&#xff1a;基于Deepseek、Ollama、LangChain的RAG实战解析 纯代码实战–…...

2025 最新版鸿蒙 HarmonyOS 开发工具安装使用指南

为保证 DevEco Studio 正常运行&#xff0c;建议电脑配置满足如下要求&#xff1a; Windows 系统 操作系统&#xff1a;Windows10 64 位、Windows11 64 位内存&#xff1a;16GB 及以上硬盘&#xff1a;100GB 及以上分辨率&#xff1a;1280*800 像素及以上 macOS 系统 操作系统…...

日期时间 API

日期时间 API (java.time 包)&#xff0c;旨在解决旧版 java.util.Date 和 java.util.Calendar 存在的一些设计缺陷&#xff0c;比如线程不安全、时区处理不一致等问题。新 API 基于 ISO 8601 标准&#xff0c;更加直观、简洁&#xff0c;且支持时区和区域设置。主要类有&#…...

AI数字人开发,引领科技新潮流

引言 随着人工智能技术的迅猛发展&#xff0c;AI 数字人在影视娱乐、客户服务、教育及医疗等多个领域展现出巨大的潜力。本文旨在为开发者提供一份详细的 AI 数字人系统开发指南&#xff0c;涵盖从基础架构到实现细节的各个方面&#xff0c;包括人物建模、动作生成、语音交互、…...

领域驱动设计:事件溯源架构简介

概述 事件溯源架构通常由3种应用设计模式组成,分别是:事件驱动(Event Driven),事件溯源(Event Source)、CQRS(读写分离)。这三种应用设计模式常见于领域驱动设计(DDD)中,但它们本身是一种应用设计的思想,不仅仅局限于DDD,每一种模式都可以单独拿出来使用。 E…...

自定义类加载器国密版本冲突

自定义类加载器国密版本冲突 对接三方接口经常使用到国密加密包&#xff08;bcprov&#xff09;&#xff0c;此时系统已经引入了1.5版本&#xff0c;而三方提供的sdk中引用了1.6版版本&#xff0c;两个版本有冲突&#xff0c;如果系统加载到1.5版本的将会加密异常(各种奇怪的异…...

‌Debian 包版本号比较规则详解

1 版本号组成结构 Debian 版本号格式为&#xff1a;[epoch:]upstream_version[-debian_revision] 示例‌&#xff1a;2:1.18.3~betadfsg1-5b1 组件说明比较优先级‌Epoch‌冒号前的数字 (2:)最高‌Upstream‌主版本 (1.18.3~betadfsg1)中‌Debian修订号‌减号后的部分 (5)最…...

STM32之影子寄存器

预分频寄存器计数到一半的时候&#xff0c;改变预分频值&#xff0c;此时不会立即生效&#xff0c;会等到计数完成&#xff0c;再从影子寄存器即预分频缓冲器里装载修改的预分频值。 如上图&#xff0c;第一行是内部时钟72M&#xff0c;第二行是时钟使能&#xff0c;高电平启动…...

x64汇编下过程参数解析

简介 好久没上博客, 突然发现我的粉丝数变2700了, 真是这几个月涨的粉比我之前好几年的都多, 于是心血来潮来写一篇, 记录一下x64下的调用约定(这里的调用约定只针对windows平台) Windows下的x64程序的调用约定有别于x86下的"stdcall调用约定"以及"cdecl调用约…...

Blender调整最佳渲染清晰度

1.渲染采样调高 512 2.根据需要 开启AO ,开启辉光 , 开启 屏幕空间反射 3.调高分辨率 4096x4096 100% 分辨率是清晰度的关键 , 分辨率不高 , 你其他参数调再高都没用 4.世界环境开启体积散射 , 可以增强氛围感 5.三点打光法 放在模型和相机45夹角上 白模 白模带线条 成品...

TSMaster【第二十篇:华山论剑——知识图谱全览】

(三维思维导图「独孤九剑总诀式」技能树「经脉贯通」检测系统未来技术「武学秘境」预测) 【武侠场景导入】光明顶秘道惊变 明教光明顶密道中,张无忌面对错综复杂的甬道体系,以乾坤大挪移心法贯通九阳神功与太极拳剑,终成武林至尊。今时今日,三电工程师面对庞杂的TSMaste…...

神经性手抖是一种常见的症状

神经性手抖是一种常见的症状&#xff0c;表现为手部无意识或不受控制地颤抖。为了预防神经性手抖&#xff0c;我们可以采取以下几种方法&#xff1a; 1. 放松身心&#xff1a;压力和焦虑是导致神经性手抖的常见原因之一。因此&#xff0c;学会放松身心是预防手抖的关键。可以通…...

前端项目打包生成 JS 文件的核心步骤

前端项目打包生成 JS 文件的过程通常涉及以下核心步骤&#xff0c;以主流工具&#xff08;如 Webpack、Vite、Rollup 等&#xff09;为例&#xff1a; 一、项目准备阶段 项目结构 源代码目录&#xff08;如 src/&#xff09;包含 JS/TS、CSS、图片等资源配置文件&#xff08;pa…...

金融支付行业技术侧重点

1. 合规问题 第三方支付系统的平稳运营&#xff0c;严格遵循《非银行支付机构监督管理条例》的各项条款是基础与前提&#xff0c;其中第十八条的规定堪称重中之重&#xff0c;是支付机构必须牢牢把握的关键准则。 第十八条明确指出&#xff0c;非银行支付机构需构建起必要且独…...