c++ 实现 AVL 树

在这里插入图片描述

AVL 树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在 1962 年发明了一种解决上述问题的方法:

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

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

  • 它的左右子树都是AVL树。

  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。

    在这里插入图片描述

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

AVL 树的基本结构

AVL 树的实现方式有很多,我们这里选择一种较为简单的方式:引入平衡因子来维护 AVL 树的高度。因为在实现 AVL 树的时候,涉及调整树的高度,此过程需要频繁的通过子节点找到父节点,所以 AVL 树的节点需要定义成三叉链式存储。

AVL 树存储的数据我们定义成 key-val 键值对的形式,因为 map 和 set 存储的数据就是键值对的形式,而 map 和 set 又和 AVL 树有一定的关系。所以我们只需要模仿着做就行啦!

#pragma oncetemplate<class K, class V>
struct AVLTreeNode
{pair<K, v> _kv;AVLTreeNode<K, V>* _parent;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;int _bf; //平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr){}
};template<class K, class V>
class AVLTree
{
public:typedef AVLTreeNode<K, V> Node;
public:AVLTree():_root(nullptr){}private:Node* _root;
};

这里解释一下 pair 是个啥?

pair 是 C++ 标准库里面的一个模板类,类中维护了两个变量。通过对象可以直接访问维护的两个变量。第一个通过 对象.first 访问;第二个通过 对象.second 访问。你姐可以看作是 C 语言的结构体,结构体中有两个变量,仅此而已!

在这里插入图片描述

bool insert(const pair<K, V>& kv)

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

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

  1. 按照二叉搜索树的方式插入新节点。
  2. 调整节点的平衡因子。

一个节点的平衡因子可以有两种计算方式:

  • 该节点左子树的高度减去右子树的高度。
  • 该节点右子树的高度减去左子树的高度。

我们选择使用第一种方式哈,用左子树的高度减去右子树的高度来计算一个节点的平衡因子。其实用哪种都无所谓的。

废话不多说直接看插入节点的时候有哪些情况:

插入节点之后无需旋转

AVL 树为什么需要旋转?因为 AVL 树中的每个节点的平衡因子只能是 0,1,或者 -1。一旦我们插入一个新的节点,向上更新平衡因子的过程中,发现某个节点的平衡因子不满足这个要求,那么这就不是一颗 AVL 树了,就必须进行旋转,使得整棵树重新满足 AVL 树的性质。

插入一个节点之后,一定会涉及到平衡因子的更新,那么我们就得弄清楚下面这两个问题:

问题一:如何更新平衡因子?

这个问题比较简单哈,根据我们实现的 AVL 树平衡因子的定义:左子树的高度减去右子树的高度。那么如果新插入 的节点是父节点的左孩子,那么父节点的平衡因子就要加一,如果新插入的节点是父节点的右孩子,那么父节点的平衡因子就要减一。

问题二:什么时候不用继续向上更新平衡因子了呢?

当向上更新平衡因子的过程中,如果父节点的平衡因子为 0 了,就不需要继续更新平衡因子了!这是为什么呢?这就得弄清楚为什么插入一个节点需要向上更新平衡因子了!需要更新平衡因子是因为插入一个新节点之后,改变了父节点某颗子树的高度,父节点的某一颗子树的高度改变了,那么当然就需要更新平衡因子啦!因为平衡因子是用左子树的高度减去右子树的高度嘛。相反,如果更新平衡因子的过程中发现,并没有改变父节点的某颗子树的高度,当然就不必继续向上更新平衡因子啦!

表现在平衡因子上就是:父节点的平衡因子由 -1 或者 1 变成了 0。父节点的平衡因子由 -1 或者 1 变为 0,说明插入一个新节点之后,父节点的左右子树高度平衡了。但是以父节点为根节点的树整体高度并没有发生变化,因此以父节点作为一颗左子树或者右子树向上更新平衡因子已经是无意义的行为了。

在这里插入图片描述

在上面的插入例子中,我们观察到,更新平衡因子的过程中,如果 parent 的平衡因子更新之后为 0,再向上更新平衡因子已经没有意义了!原因最开始已经讲得很清楚啦!

插入节点之后进行左单旋

插入的过程要是都像上面那样只需要更新更新平衡因子就好了,但是那是不可能的!当我们插入新节点向上更新平衡因子的过程中 parent 的平衡因子出现了 -2 或者 2 的情况,那么就无法单纯通过更新平衡因子来实现插入了。就需要旋转节点使得整棵树再次满足 AVL 树的性质啦!

在这里插入图片描述

在上面的例子中插入一个新的节点 17 在向上更新平衡因子的过程中发现 parent 的平衡因子为 -2,这不满足 AVL 树的性质,需要通过旋转来解决。

我们观察这种情况:11,15,17 三个节点呈现一条直线,并且是从左上角到右下角的直线,这个时候就我们要以 parent 为旋转点进行左单旋。

表现在平衡因子上:parent 的平衡因子为 -2,cur 的平衡因子为 -1,进行左单旋。

左单旋的关键步骤:

parent->_right = cur->_left;
cur->left = parent;

在这里插入图片描述

在上面的例子中,5 链接到 15 显然不是必须的,如果旋转之前父节点的 _parent 就是 nullptr,则不会有这一步。旋转完成后将 _parent 和 _cur的平衡因子改成 0,就完成了新节点的插入。至于为什么可以直接改成 0,等用抽象图来讲解AVL树的插入时会说明原因。

void RotateL(Node* parent)
{Node* cur = parent->_right;Node* curLeft = cur->_left;parent->_right = curLeft; //左单旋的核心步骤之一if (curLeft) //如果curLeft不为空才需要向上链接,我们讲解AVL树的插入。curLeft 全为空,就没有进行链接curLeft->_parent = parent;cur->_left = parent; //左单旋的核心操作步骤之二//记录parent的父节点,方便后续进行判断,确定链接的方式Node* ppnode = parent->_parent;parent->_parent = cur;//处理一下特殊情况, 还可以使用 ppnode 是不是等于 nullptr 来判断if (parent == _root) //parent 就是旋转之前的根节点,那么就需要更新根节点{_root = cur;cur->_parent = nullptr;}else //如果不是的话,就需要判断parent位于其父节点的左侧还是右侧,进行对应的链接即可{if (ppnode->_left == parent){ppnode->_left = cur; //parent位于ppnode的左侧}else{ppnode->_right = cur; //parent 位于ppnode 的右侧}cur->_parent = ppnode;}//更新平衡因子cur->_bf = parent->_bf = 0;
}

插入节点之后进行右单旋

右单旋和左单旋相差不大呢!右单旋看起来就是一条从右上角到左下角的直线。

表现在平衡因子上:parent 的平衡因子为 2,cur 的平衡因子为 1,进行右单旋。

在这里插入图片描述

右单旋的操作与左单旋同样也相差不大呢!对称的嘛。

parent->_left = cur->_right;
cur->_right = parent;

在这里插入图片描述

同左单旋,5 链接到 1 也不是必须的,如果旋转之前父节点的 _parent 就是 nullptr,就不会有这一步。但是需要更新根节点为 cur,并让 cur 的 _parent 指向空。如果父节点的parent不为空就需要判断 parent 是的 parent 的父节点的左孩子还是右孩子,对 cur 进行不同方式的链接。右单旋后将 cur 与 parent 的平衡因子变为0。至于为什么后面讲。

void RotateR(Node* parent)
{Node* cur = parent->_left;Node* curRight = cur->_right;parent->_left = curRight; //右单旋的核心操作之一//判断一下 curRight 是不是 nullptr,只有不是 nullptr 才需要向上链接 parentif (curRight)curRight->_parent = parent;//提前记录一下 parent 的父节点Node* ppnode = parent->_parent;cur->_right = parent; //右单旋的核心步骤之二parent->_parent = cur; //向上链接父节点//判断parent是不是根节点, 判断方式依然是有两种哈,一种是判断ppnode是不是等于nullptr//                                          另一种是判断 parent 是不是等于 _rootif (ppnode == nullptr){_root = cur;cur->_parent = nullptr; //记得将根节点的父节点置为nullptr}else{//如果不是根节点那么就需要你判断 parent 处于 ppnode 的什么位置if (ppnode->_left == parent){//位于左侧就链接左侧ppnode->_left = cur;}else{//位于右侧当然就链接右侧啦ppnode->_right = cur;}cur->_parent = ppnode;}//更新平衡因子cur->_bf = parent->_bf = 0;
}

插入节点之后进行左右双旋

其实只要弄清楚一个旋转,其他的旋转就很好理解了!我么来看看需要进行左右双旋的插入例子:

在这里插入图片描述

我们看到需要左右双旋的形状就是一个折线,从右上到左下再到右下。

表现在平衡因子上:parent 的平衡因子为 2,cur 的平衡因子为 -1,进行左右双旋。

这种情况单旋是解决不了问题的,需要分两步操作:

  • 以 cur 为旋转点进行左单旋。
  • 以 parent 为旋转点进行右单旋。

在这里插入图片描述

双旋更新平衡因子的策略会在 AVL 抽象图讲解中说明。

//左右双旋
void RotateLR(Node* parent)
{Node* cur = parent->_left;Node* curRight = cur->_right;int curRightBf = curRight->_bf; //我们先记录一下 curRightBf 的平衡因子,因为左单旋会修改平衡因子嘛, 还可以用来判断新插入的节点位于 curRight 的位置RotateL(cur); //以 cur 为旋转点进行左单旋RotateR(parent); //以 parent 为旋转点进行右单旋//更新平衡因子,我们需要判断新插入的节点位于他的左侧还是右侧,直接利用平衡因子判断哈if (curRightBf == 0){parent->_bf = 0;cur->_bf = 0;curRight->_bf = 0;}else if (curRightBf == 1) //新插入的节点位于 curRight 的左侧{parent->_bf = -1;cur->_bf = 0;curRight->_bf = 0;}else if (curRightBf == -1)//新插入的节点位于 curRight 的右侧{parent->_bf = 0;cur->_bf = 1;curRight->_bf = 0;}}

插入节点之后进行右左双旋

左单旋你回了,右单旋你也会了,左右双旋你也会了!接下来的右左双旋我相信你也一定会的!这里就不在制作动图演示过程啦!

右单旋的形状也是一个折线哈,从左上到右下再到左下角的折线。

表现在平衡因子上:parent 的平衡因子为 -2,cur 的平衡因子为 1,进行右左双旋。

//右左双旋
void RotateRL(Node* parent)
{Node* cur = parent->_right;Node* curLeft = cur->_left;int curLeftBf = curLeft->_bf; //我们先记录一下 curLeftBf 的平衡因子,因为右单旋会修改平衡因子嘛, 还可以用来判断新插入的节点位于 curLeft 的位置RotateR(cur); //以 cur 为旋转点进行右单旋RotateL(parent); //以 parent 为旋转点进行左单旋//更新平衡因子,我们需要判断新插入的节点位于他的左侧还是右侧,直接利用平衡因子判断哈if (curLeftBf == 0) //curLeftBf为 0 说明 抽象图中的 b 和 c 均是空树,经过左右双旋之后这三个节点的平衡因子其实已经被修改成为了 0 但是为了解耦嘛,还是需要判断一下{parent->_bf = 0;cur->_bf = 0;curLeft->_bf = 0;}else if (curLeftBf == 1) //新插入的节点位于 curLeft 的左侧{parent->_bf = 0;cur->_bf = -1;curLeft->_bf = 0;}else if (curLeftBf == -1)//新插入的节点位于 curLeft 的右侧{parent->_bf = 1;cur->_bf = 0;curLeft->_bf = 0;}
}

AVL 树插入抽象图讲解

上面我们都是拿具体的例子来理解AVL树的插入,你可能会说,这能代表一切情况嘛?

下面我们会用抽象图讲解 AVL 树的插入,让我们一起来看看插入是不是这么个事儿。

我们来看左单旋的抽象图:这个图是怎么抽象的呢?

  • 节点值为 8 和 6 的两个节点只是一个代表,其值可以是满足 AVL 树的任意值。
  • 这里的 8 不一定是根节点,有可能只是一颗 AVL 树中的一部分。
  • a,b,c 均表示一颗子树。
  • 下图中在 c 下方插入一个节点,更新平衡因子时,在 8 这个节点处检测到了异常的平衡因子,截取了这个节点下面的所有节点。

用这个抽象图能将所有左单旋的情况抽象出来:

在这里插入图片描述

左单旋之后的结果:

在这里插入图片描述

我们看到左单旋之后,parent 的左右子树高度相同,cur 的左右子树的高度相同。这就是为什可以在左单旋之后能够直接将 cur 和 parent 的平衡因子更新为 0 的原因。

我们再来看右单旋的抽象图

在 a 子树中插入一个节点,向上更新平衡因子的过程中,parent 的平衡因子出现异常。

在这里插入图片描述

进行右单旋:

在这里插入图片描述

同样的,我们发现进行右单旋之后,parent 的左右子树高度相等,cur 的左右子树高度相等,这就是为什么能够在进行右单旋之后直接将 parent 和 cur 的平衡因子更新为 0 的原因。

我们再来看左右双旋的抽象图

如下图:我们可以看到想要发生左右双旋,必然是在 b 或者 c 所在的子树插入一个新节点,使得 3 的平衡因子变为 -1,9 的平衡因子变成 2,符合左右双旋特征(插入在 a 下方是右单旋,插入在 d 下方不用旋转)

在这里插入图片描述

在这里插入图片描述

我们可以看到左右双旋其实就是将 b 这个子树给给了 3 的右侧,c 这个子树给给了 9 的左侧,然后 3,9 分别充当 6 的左右子树。根据新节点插入如位置的不同,平衡因子的更新略有不同,因此我们需要判断新插入的节点位于 6 这个节点的左侧还是右侧。

  • 如果在左侧:
    • parent->_bf = -1。
    • cur->_bf = 0。
    • son->_bf = 0;
  • 如果在右侧:
    • parent->_bf = 0。
    • cur->_bf = 1。
    • son->_bf = 0。

接下来我们来看右左双旋:

我们画出了左单旋的抽象图,右单旋的抽象图,左右单旋的抽象图,那么你肯定能画出右左双旋的抽象图。这里就补在绘图了!

检查 AVL 树插入是否正确

我们可以写几个函数来判断我们实现的 AVL 树的插入函数是否正确,判断的依据就是 AVL 树自身的性质:

  • 对于每一个节点,求出其左右子树的高度,相减之后与节点存储的平衡因子作比较,如果不同则 AVL 树的插入出问题了。
  • 左右子树高度之差的绝对值不大于 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 树是否平衡bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;int leftHight = Height(root->_left);int rightHight = Height(root->_right);if (leftHight - rightHight != root->_bf){cout << "平衡因子异常:" << root->_kv.first << "平衡因子:" << root->_bf << endl;return false;}return abs(rightHight - leftHight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}

代码

bool insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);return true;}else{Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;}}//新节点cur = new Node(kv);//新节点的插入位置,parent 的左孩子还是parent的右孩子if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}//链接一下父节点cur->_parent = parent;//更新平衡因子while (parent){//根据你的平衡因子ID定义来,如果你定义的平衡因子时左子树的高度减去右子树的高度//就跟我写的代码一样,如果你的平衡因子的定义是右子树的高度减去左子树的高度//parent 的平衡因子的跟新就跟写的相反就行了//如果cur在parent 的左侧,parent 的平衡因子加加if (cur == parent->_left){parent->_bf++;}else //在右侧parent 的平衡因子减减{parent->_bf--;}//更新一次就要进行一次判断if (parent->_bf == 0){break; //如果更新之后 parent 的平衡因子变成了0,直接结束循环}else if (parent->_bf == 1 || parent->_bf == -1) //继续向上跟新平衡因子{cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//AVL不平衡了,需要旋转if (parent->_bf == -2 && cur->_bf == -1){//以 parent 为旋转点进行左单旋RotateL(parent);}else if (parent->_bf == 2 && cur->_bf == 1){//以 parent 为旋转点进行右单旋RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){//先以 cur 为旋转电进行左单旋,再以 parent 为旋转点进行右单旋RotateLR(parent);}else if (parent->_bf == -2 && cur->_bf == 1){//先以 cur 为旋转电进行右单旋,再以 parent 为旋转点进行左单旋RotateRL(parent);}//旋转之后 AVL 树一定是平衡的,因此直接可以退出更新平衡因子的循环啦break;}else{//平衡因子如果出现上述枚举情况之外的情况那么一定是程序除了问题哈。//直接暴力结束程序assert(false);}}}return true;
}

bool erase(const pair<K, V>& kv)

AVL 树的删除比插入还复杂一些,这里就不做讲解了!有兴趣的 uu 可以自己去了解一下。面试的时候顶多考 AVL 树的插入呢~~😸
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/130589.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 深度学习 解决遇到的报错问题9

本篇继python 深度学习 解决遇到的报错问题8-CSDN博客 目录 一、can only concatenate str (not "int") to str 二、cant convert np.ndarray of type numpy.object_. The only supported types are: float64, float32, float16, complex64, complex128, int64, in…

数据中心系统解决方案

设计思路 系统设计过程中充分考虑各个子系统的信息共享要求&#xff0c;对各子系统进行结构化和标准化设计&#xff0c;通过系统间的各种联动方式将其整合成一个有机的整体&#xff0c;使之成为一套整体的、全方位的数据中心大楼综合管理系统&#xff0c;达到人防、物防和技防…

【vscode输出中文乱码】

vscode输出中文乱码为一个个的问号。 这个链接亲测有用 win11对应的界面在这里&#xff1a;

在PyCharm中直接启动mitmproxy并自动打开关闭系统代理

前言 在前面的文章中&#xff0c;有几篇是介绍mitmproxy 的。 这个mitmproxy 的确是个捕获数据的好工具&#xff0c;但在运行时候需要在命令行启动&#xff0c;这是很令人苦恼的。 之前也尝试过脱离命令行去启动mitmproxy&#xff0c;在Python中启动mitmproxy&#xff0c;脱离…

网络编程套接字(二)

目录 简单的TCP网络程序服务端创建套接字服务端绑定服务端监听服务端获取连接服务端处理请求单执行流服务器的弊端 多进程版TCP网络程序捕捉SIGCHLD信号让孙子进程提供服务多线程版的TCP网络程序客户端创建套接字客户端链接服务器客户端发起请求 线程池版的TCP网络程序 简单的T…

【Python 千题 —— 基础篇】乘方计算

题目描述 题目描述 编写一个程序&#xff0c;接受用户输入的两个数字&#xff0c;然后计算这两个数字的乘方结果&#xff0c;并输出结果。 输入描述 输入两个数字&#xff0c;用回车隔开两个数字。 输出描述 程序将计算这两个数字的乘方结果&#xff0c;并输出结果。 示…

【论文阅读】Equivariant Contrastive Learning for Sequential Recommendation

【论文阅读】Equivariant Contrastive Learning for Sequential Recommendation 文章目录 【论文阅读】Equivariant Contrastive Learning for Sequential Recommendation1. 来源2. 介绍3. 前置工作3.1 序列推荐的目标3.2 数据增强策略3.3 序列推荐的不变对比学习 4. 方法介绍4…

ELK + kafka 日志方案

概述 本文介绍使用ELK&#xff08;elasticsearch、logstash、kibana&#xff09;  kafka来搭建一个日志系统。主要演示使用spring aop进行日志收集&#xff0c;然后通过kafka将日志发送给logstash&#xff0c;logstash再将日志写入elasticsearch&#xff0c;这样elasticsearc…

如何在校园跑腿系统小程序中构建稳健的订单处理与分配系统?

1. 数据库设计 首先&#xff0c;设计订单数据结构。使用数据库&#xff08;例如MySQL、MongoDB等&#xff09;&#xff0c;创建订单表以存储订单相关信息&#xff0c;包括订单ID、用户信息、交付地址、订单状态等。 CREATE TABLE orders (order_id INT AUTO_INCREMENT PRIMAR…

Vue CRUD 弹出窗口 表单验证 增删改查 接口文档

目录 1. 准备工作 2. 弹出窗口 3. 新增更新功能 5.表单验证 5. 接口文档 1. 准备工作 后台服务接口&#xff0c;对书本的增删改查操作 2. 弹出窗口 进入ElementUi官网&#xff0c; 找到Dialog对话框&#xff0c;可以参考“嵌套表单的dialog”实现。 该步骤先实现弹出窗…

任务管理工具

1. 任务规划 有效的任务管理始于适当的规划。取决于预算、资源、范围和时间表&#xff1b;有效的任务规划需要通过结构化方法将工作分解为多个阶段。 有了一个好的任务管理工具&#xff0c;您应该能够将您的工作分解为易于管理的更简单的任务。此外&#xff0c;应该有一个根据…

列举一些常用的Webpack配置和插件

以下是一些常用的Webpack配置和插件&#xff0c;可以用来优化和扩展前端项目的构建过程&#xff1a; 1&#xff1a;入口和出口配置&#xff1a; module.exports {entry: ./src/index.js, // 入口文件output: {path: path.resolve(__dirname, dist), // 输出目录filename: …

一分钟理解npm run dev 和 npm run serve

前端开发过程中运行Vue项目的时候&#xff0c;有时候使用npm run serve命令可以启动项目&#xff0c;有时候却会报错&#xff1b;有时候使用npm run dev命令可以启动项目&#xff0c;有时候却也会报错。是什么原因造成这种情况呢&#xff0c;原因在于Vue脚手架版本的问题&#…

内存池设计实现

1.设计原理 1.内存池实际就是预先分配不同大小的内存块, 然如果需要调用的时候, 直接把这个块的指针返回. 图中, 就是内存池划分. 2.通过一个链表, 将这些分配的内存块串联起来, 每一块最头部都记录这这个块的信息 3.分配的时候, 会遍历一遍链表, 找到is_used未被置1, pool…

相机存储卡被格式化了怎么恢复?数据恢复办法分享!

随着时代的发展&#xff0c;相机被越来越多的用户所使用&#xff0c;这也意味着更多的用户面临着相机数据丢失的问题&#xff0c;很多用户在使用相机的过程中&#xff0c;都出现过不小心格式化相机存储卡的情况&#xff0c;里面的数据也将一并消失&#xff0c;相机存储卡被格式…

[iOS开发]iOS中TabBar中间按钮凸起的实现

在日常使用app的过程中&#xff0c;经常能看到人家实现了底部分栏控制器的中间按钮凸起的效果&#xff0c;那么这是怎么实现的呢&#xff1f; 效果演示&#xff1a; 实现原理&#xff1a; 创建按钮 创建一个UITabBar的子类&#xff0c;重写它的layoutSubviews方法&#xff1…

Global-aware siamese network for change detection on remote sensing images

遥感图像中的变化检测是以有效的方式识别观测变化的最重要的技术选择之一。CD具有广泛的应用&#xff0c;如土地利用调查、城市规划、环境监测和灾害测绘。然而&#xff0c;频繁出现的类不平衡问题给变化检测应用带来了巨大的挑战。为了解决这个问题&#xff0c;我们开发了一种…

数字化时代,数据仓库是什么?有什么用?

在激烈的市场竞争和全新的数字经济共同作用下&#xff0c;数字化转型成为了大多数企业的共识&#xff0c;也是获取数字经济的最佳方式。在整个数据价值生产链路中&#xff0c;数据仓库的主要作用就是中心化分发&#xff0c;将原始数据与数据价值挖掘活动隔离。 所有的原始数据…

基于 LangChain 构建 LLM 应用程序设计与实践

▼最近直播超级多&#xff0c;预约保你有收获 近期直播&#xff1a;《基 LangChain大模型架构案例实践》 —1— LangChain 是什么&#xff1f; LangChain 和 Semantic Kernel 是当前比较受欢迎的两款 LLM 应用开发框架。 LangChain 作为一个大语言模型应用开发框架&#xff0c;…

SpringBoot 将 jar 包和 lib 依赖分离,dockerfile 构建镜像

前言 Spring Boot 是一个非常流行的 Java 开发框架&#xff0c;它提供了很多便利的功能&#xff0c;例如自动配置、快速开发等等。 在使用 Spring Boot 进行开发时&#xff0c;我们通常会使用 Maven 或 Gradle 进行项目构建。 本文将为您介绍如何使用 Maven 将 Spring Boot …