AVL树的插入(C++实现)

news2024/11/18 17:18:56

1. 概念

AVL树(Adelson-Velsky and Landis Tree)于1962年被提出,是计算机科学中最早被发明的平衡二叉查找树。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis。

在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 O ( l o g N ) O(logN) O(logN)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。

1.1 性质

AVL树有这样的性质:

  1. 任意一个结点的左右子树高度之差都不大于1;
  2. 任意子树都是AVL树。

高度之差,在这里也叫做平衡因子(balance factor):
平 衡 因 子 = 右 子 树 高 度 − 左 子 树 高 度 平衡因子 = 右子树高度 - 左子树高度 =
所以平衡因子的值只有三个:-1/0/1。

为什么要限制左右子树的高度呢?

高度平衡二叉搜索树,控制每个结点的左右子树高度差不超过1。因为偶数个节点如2或4,都无法让左右高度差相等,所以退而求其次,让它接近完全二叉树,控制树的高度在 log ⁡ 2 N \log_2{N} log2N之内( N N N是结点数)。

所以AVL的高度平衡是相对于每个根结点而言的,也就是平衡因子的绝对值不大于1。

这不是一棵AVL树,其中,值为76的结点左子树比右子树高度多3,即平衡因子是-3。

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EmDOILm7-1668576447045)(IMG/2223px-AVLtreef.svg.png)]

同一棵高度平衡的AVL树,每个结点的平衡因子都不大于1。

平衡因子是一种实现AVL树的限定方法,它不是必须的,只是一种叫法。

本文GIF动图皆使用ScreentoGif软件在网站https://visualgo.net录制。

2. 定义结点类

AVL树是改良以后的二叉搜索树,由于它有着平衡因子的限制,所以将AVL树的结点用三叉链表示,其中新增的是根结点的父结点的指针,以便稍后进行旋转后的链接操作。除此之外,也需要为每个结点增加一个新的属性:平衡因子。

这里使用了pair,它是储存一个键值对(key和value)的单位,分别用模板参数K和V表示。其中,构造函数分别把指针默认设置为nullptr,平衡因子默认设置为0,由于模板参数在传入时就必须指定类型,所以使用传入的模板参数组合构造pair。

// 定义结点类
template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;

	pair<K, V> _kv;

	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

3. 插入函数

由于AVL树是优化的二叉搜索树,我们知道,在二叉搜索树中,插入是要按照规则在指定区域插入的,所以二叉搜索树的各种操作都是有查找功能的。

AVL树的查找步骤和二叉搜索树别无二致,只是多了限制条件:平衡因子的绝对值不大于1,所以在每次插入以后都要根据平衡因子的情况对树的形态进行调整,比如旋转。

插入可以分为两种情况:

  1. 空树:插入根结点;
  2. 非空:插入后根据平衡因子是否符合条件选择是否旋转。

二叉搜索树的插入规则:规则:

  • 新结点的值小于根结点的值,将结点插入到左子树当中;
  • 新结点的值大于根结点的值,将结点插入到右子树当中;
  • 新结点的值等于根结点的值,插入结点失败。

那么如何决策是否要旋转呢?如何旋转?

3.1 更新平衡因子

当我们插入或删除某一个结点时,都会引起这个结点的所有祖先节点的平衡因子发生改变。

什么是祖先节点?

  • 结点A->B之间如果存在(唯一)一条路径,那么称A为B的祖先,B称为A的子代。一个结点的祖先不止一个。

image-20221114215831654

从新插入结点的第一个父结点开始往上更新平衡因子,最坏的情况可能要一直更新到根结点。除了root的父结点是nullptr,其他所有结点都有父结点,所以可以用这个条件判断是否遍历完祖先节点。

规则

当前祖先节点,就是新插入结点的祖先节点中的某一个结点。

  • 新插入结点在当前祖先节点(parent)的左边,平衡因子-1;
  • 新插入结点在当前祖先节点(parent)的右边,平衡因子+1;

每次执行以上操作以后,还要继续判断当前祖先结点(parent)平衡因子的情况,以判断是否还要继续向上调整:

  • parent的平衡因子等于-1或1,接近平衡,继续往上更新;
    • 说明未插入结点之前parent是平衡的,插入后改变了parent这棵子树的平衡,它的子树一边比另一边高。而更高的一边虽然让parent这棵子树满足bf的绝对值小于等于1,但是它仍然有可能会影响parent往上的祖先节点。
  • parent的平衡因子等于0,平衡,停止往上更新;
    • 说明未插入结点之前parent是不平衡的,插入以后使得parent这棵子树平衡,它的两边子树一样高,不会影响所有祖先结点的平衡因子,停止更新。
  • parent的平衡因子等于-2或2,不平衡,无法通过调整平衡因子达到平衡,需要旋转。
    • 说明在未插入之前,parent这棵子树已经是一边高一边低(1层)了,插入以后一边比另一边高2层,通过调整平衡因子已经无法解决平衡问题了,所以需要进行旋转操作。

由于新插入的结点可能会影响所有祖先结点的平衡因子,所以最坏的情况要一直更新到根节点。因为从下到上迭代祖先节点,所以结点类的实现使用了三叉链结构,其中新增的是当前结点指向父结点的指针。

动图中的平衡因子 = 左子树高度 - 右子树高度,是一样的。

3.2 直接插入

AVL树插入新结点,如果最后新结点的所有祖先结点的平衡因子都符合AVL树的要求,就不用旋转。

AVL插入1

通过动图可以知道,当插入新结点以后,比二叉搜索树多出的步骤就是检查新增结点的祖先节点的平衡因子是否符合条件。插入后的祖先结点的bf变化情况如下:

image-20221115223807798

由于这部分的代码会被包含在下面的情况,而且插入的操作和二叉搜索树的逻辑并无二致,所以在此只给出插入以后向上检查祖先节点的平衡因子的逻辑:

while (parent)
{
    if (parent->_right == cur)				// 插入在右边,bf+1
    {
        parent->_bf++;
    }
    else									// 插入在左边,bf-1
    {
        parent->_bf--;
    }

    if (parent->_bf == 0)					// 直到符合AVL树的规则停止
    {
        break;
    }
    else if (abs(parent->_bf) == 1)			// 往上调整
    {
        parent = parent->_parent;
        cur = cur->_parent;
    }
    else if (abs(parent->_bf) == 2)			// 不平衡
    {
		// 不平衡,要进行下面的旋转操作
    }
}

抽象图表示了无数种情况的集合,但具体采取哪种旋转方式,只取决于平衡因子不符合条件的那一小部分。

3.3 左单旋

示例

首先看动图中的示例:

AVL左单旋

下面是新结点的所有被影响的祖先节点的平衡因子的变化情况:

image-20221115230537969

理解

当新结点插入以后,值为86的结点就不平衡了,这时需要对其进行旋转,怎么知道向哪边旋转的呢?

image-20221115232318794

如果86结点就是根结点,那么这样旋转后整棵树就直接平衡了,而实际情况可能是它作为一棵子树旋转的,所以旋转以后还要重新链接到原树上。

上面的图示中是(左)单旋的最简情况,实际上AVL树的结点往往不止这些,所以为了理解上的方便,由于平衡因子是左右子树的高度差,所以以一个固定值h表示高度,把这h高度的结点看作一个整体,相对于它的偏移量就能表示许多情况了。

为了描述和代码上的实现方便,新插入结点叫做cur,子树的根结点叫做parent,根结点的右孩子叫做subR,右孩子的左孩子叫做subRL

image-20221116000524661

抽象图要从整体感受,上图中插入结点以后parent不平衡,bf=2,说明它的右子树比左子树高2层,可以感性地认为右子树更重。所以以parent结点为轴点(从图形看是从subR为轴点,但是函数中的参数是parent),逆时针旋转,让那个高的一边单独做parent的右子树,然后让subRL“离家出走”,做parent的右孩子(看图)。

步骤

  1. 让subR的左子树subRL作为parent的右子树;
  2. 让parent作为subR的左子树;
  3. 让subR作为子树的整棵子树的根结点;
  4. 向上按照规则更新平衡因子。
// 左单旋函数
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* pParent = parent->_parent;				// 保存父结点的父结点

    parent->_right = subRL;							// 重建subRL和parent联系
    if (subRL != nullptr)
    {
        subRL->_parent = parent;
    }

    subR->_left = parent;							// 重建subR和parent联系
    parent->_parent = subR;


    if (parent == _root)							// 父结点为根结点,旋转后的subR作为根结点,无父结点
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
        {
            pParent->_left = subR;
        }
        else
        {
            pParent->_right = subR;
        }

        subR->_parent = pParent;
    }
    subR->_bf = 0;									// 更新平衡因子
    parent->_bf = 0;
}

注意

更新后的平衡因子是根据旋转以后的抽象图才能知道的,所以写树的代码时(数据结构)一定要画图,否则在脑子里是很乱的。

3.4 右单旋

示例

AVL右单旋

新插入结点的祖先结点的平衡因子变化情况如下:

image-20221116084356416

理解

同样地,使用新插入结点叫做cur,子树的根结点叫做parent,根结点的左孩子叫做subL,右孩子的左孩子叫做subLR

image-20221116001232284

右单旋和左单旋是对称的,只是平衡因子的正负有所区别。依然可以形象地用“重量”理解它,就像扁担一样。

步骤

  1. 让subL的右子树subLR作为parent的左子树;
  2. 让parent作为subL的右子树;
  3. 让subL作为整棵子树的根结点;
  4. 向上按照规则更新平衡因子。
// 右单旋函数
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* pParent = parent->_parent;				// 保存父结点的父结点

    parent->_left = subLR;							// 重建subLR和parent联系
    if (subLR != nullptr)
    {
        subLR->_parent = parent;
    }

    subL->_right = parent;							// 重建subL和parent联系
    parent->_parent = subL;


    if (parent == _root)							// 父结点为根结点,旋转后的subL作为根结点,无父结点
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
        {
            pParent->_left = subL;
        }
        else
        {
            pParent->_right = subL;
        }

        subL->_parent = pParent;
    }
    subL->_bf = 0;									// 更新平衡因子
    parent->_bf = 0;
}

注意

单旋的图示中的树都可能是子树,所以步骤的第三步中只是将旋转后的subR、subL作为子树的根结点。既然是子树,旋转以后的子树就必须链接到原树上,所以这也是AVL树的结点类使用三叉链结构的原因之一,其中新增的是parent指针。

3.5 左右双旋

示例

AVL左右双旋

新插入结点的祖先结点的平衡因子变化情况如下:

image-20221116085055283

理解

image-20221116093437936

当在subLR的右子树中插入结点,会让parent的平衡因子变为-2,不平衡。从旋转之前的二叉树来看(第二个二叉树),对parent而言,它的左子树更“重”,而且就从子树的个数而言,parent的左孩子subL有3个子树,而右孩子只有一个子树。

这样讨论是合理的,因为AVL树严格限制了每个结点子树的高度,所以对于parent而言,这4个子树的高度是接近的。

这里的子树是相对于parent,就parent的左右孩子而言的,所以总共四个子树。

从结果来看,引发双旋的原因是parent某一边的子树有3个子树,而另一边只有1个子树。经过双旋以后将那3个中的一个子树分到另一边,这样就让整棵树满足AVL树的规则。

步骤

  1. 以subL为轴点左单旋;
  2. 以parent为轴点右单旋;
  3. 更新平衡因子。
// 左右双旋函数
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(subL);
    RotateR(parent);

    if(bf == -1)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if(bf == 1)
    {
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if(bf == 0)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
        assert(false);
}

注意

根据插入后未旋转之前subLR的平衡因子的情况(-1/0/1),双旋以后的平衡因子更新情况有三种:

  1. 当插入后subLR的平衡因子为-1:

    image-20221116124031359

    更新后的平衡因子:parent:1;subL:0;subLR:0。

  2. 当插入后subLR的平衡因子为0:

    image-20221116124820062

  3. 当插入后subLR的平衡因子为1:

    image-20221116124845621

3.6 右左双旋

示例

AVL右左双旋

新插入结点的祖先结点的平衡因子变化情况如下:

image-20221116084747874

理解

image-20221116094404155

双旋的过程是对称的。但是思想都是相同的,将子树多的那一部分放到少的那一边。

// 右左双旋函数
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(subR);
    RotateL(parent);

    if(bf == -1)
    {
        subR->_bf = 1;
        parent->_bf = 0;
        subRL->_bf = 0;
    }
    else if(bf == 1)
    {
        subR->_bf = 0;
        parent->_bf = -1;
        subRL->_bf = 0;
    }
    else if(bf == 0)
    {
        subR->_bf = 0;
        parent->_bf = 0;
        subRL->_bf = 0;
    }
    else
        assert(false);
}

4. AVL树的验证

首先AVL树是二叉搜索树,所以首先可以验证它是否符合二叉搜索树的性质:

// 中序遍历子函数
void _InOrder(Node* root)
{
    if (root == nullptr)
    {
        return;
    }

    _InOrder(root->_left);
    cout << root->_kv.first << ":" << root->_kv.second << endl;
    _InOrder(root->_right);
}

然而中序遍历只能验证它是一棵二叉搜索树,而AVL树限制条件更严格,所以可以遍历每个结点,让结点中的平衡因子和实际的平衡因子比较。

如何知道实际的平衡因子?

  • 使用平衡因子公式:
    平 衡 因 子 = 右 子 树 高 度 − 左 子 树 高 度 平衡因子 = 右子树高度 - 左子树高度 =

使用递归得到当前结点子树高度时,返回的是左右子树中更高的子树高度再加上自身结点高度(+1):

// 验证平衡因子 子函数
bool _IsBalance(Node* root)
{
    if(root == nullptr)
    {
        return false;
    }

    int leftH = Height(root->_left);
    int rightH = Height(root->_right);
    int diff = rightH - leftH;

    if(diff != root->_bf)
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }

    return abs(diff) < 2
        && _IsBalance(root->_left)
        && _IsBalance(root->_right);

}
int Height(Node* root)								// (子)树的高度等于结点高度+高的子树
{
    if(root == nullptr)								// 空树高度为0
        return 0;

    return max(Height(root->_left), Height(root->_right)) + 1;
}

5. AVL树的性能

AVL树有着严格的高度限制,是高度平衡的二叉搜索树,可以保证二叉树在任何情况都是接近完全二叉树的,这个特性使得在查找时的效率稳定性比普通的二叉搜索树好,因为后者不是所有情况下查找的效率都是 O ( l o g 2 N ) O(log_2N) O(log2N。AVL树即使在最坏的情况下,查找的时间复杂度也是 O ( l o g 2 N ) O(log_2N) O(log2N

但是,虽然它查找的效率很高,是严格的 O ( l o g 2 N ) O(log_2N) O(log2N),然而删除操作的时间复杂度在 l o g ( 2 l o g 2 N ) log(2log_2N) log(2log2N)左右。插入操作的时间复杂度在 O ( l o g 2 N ) O(log_2N) O(log2N之上,因为AVL树的平衡因子的绝对值只要超过1,就必须旋转,由于频繁插入使得效率变低。

红黑树是AVL树的优化,它解决了AVL树频繁插入导致效率变低的问题。

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

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

相关文章

大一新生HTML期末作业 个人网页王嘉尔明星介绍网页设计与制作

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

领悟《信号与系统》之 信号与系统的描述-上节

信号与系统的描述-上节一、信号分类1. 一维信号和多维信号2. 确定信号和随机信号3. 连续时间信号和离散时间信号4. 周期信号和非周期信号5. 能量信号和功率信号二、常见工程信号1. 正弦信号2. 指数信号3. 取样信号4. 单位矩形脉冲信号5. 符号函数在真实的物理世界中&#xff0c…

Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

Redis的优惠券秒杀问题&#xff08;五&#xff09;全局唯一ID 以及 秒杀下单 关于优惠秒杀问题的Redis实现章节总览 全局唯一ID 场景分析 不能用自增的原因 id的规律性太明显 受单表数据量的限制 全局唯一ID的条件 全局唯一ID的Redis实现 代码实现 单元测试 其它…

【FPGA】FPGA实现SPI协议读写FLASH(一)----- M25P16操作概述

文章目录一、FLASH介绍&#xff08;M25P16&#xff09;1、M25P16概述2、SPI模式3、存储结构4、指令集5、时间参数二、M25P16工作原理三、M25P16指令操作1、页编程 (PP)2、扇区擦除和整块擦除 (SE and BE)3、写使能 (WREN)4、读ID&#xff08;RDID&#xff09;5、读状态寄存器&a…

使用c#将aj-report桌面化:1.winform嵌入浏览器

说到底,aj-report是个工具,我想大多数人还是想快速使用它来创建一个可以展示的工具。通过之前的章节,你应该可以制作自己的报表页面了,下面我们来看看怎么把aj-report包装成一个桌面能够运行的软件。 当然作为扩展开发,受开源协议限制,我们不能大规模修改aj-report的源代…

【毕业设计】深度学习图像修复算法研究与实现 - python

文章目录1 前言2 什么是图像内容填充修复3 原理分析3.1 第一步&#xff1a;将图像理解为一个概率分布的样本3.2 补全图像3.3 快速生成假图像3.4 生成对抗网络(Generative Adversarial Net, GAN) 的架构3.5 使用G(z)生成伪图像4 在Tensorflow上构建DCGANs5 最后1 前言 &#x1…

PC_OS中断/中断屏蔽字

文章目录程序中断&#x1f383;中断概念中断功能(作用)中断请求中断源中断分类外中断非屏蔽中断和可屏蔽中断陷入(内中断)硬件中断和软件中断关系整理&#x1f388;中断判优&#x1f388;中断优先级CPU响应中断的条件外中断实现思路&#x1f388;中断隐指令 及其工作①关中断②…

下一个倒下的是不是Genesis

今日&#xff0c;一个关于“Genesis今晚破产”的传言在各个社交平台传播&#xff0c;包括行业的KOL也在讨论这个事情&#xff0c;认为Genesis或存在偿付能力问题&#xff0c;该公司将于美国东部时间11月17日8&#xff1a;00am与债权人通话以解释情况。若消息属实&#xff0c;Ge…

【AGC】flutter之agconnect_crash在ios上崩溃

问题背景 flutter agconnect_crash-1.2.0300 运行在ios平台上&#xff0c;出现了如下这个崩溃 NSInvalidArgumentException: *** [NSJSONSerialization dataWithJSONObject:options:error:]: value parameter is nil 0 CoreFoundation 0x00000001830d005c 0x183037000 62678…

视频讲解vue2基础之渲染v-if/v-show/v-for/v-html

大家好&#xff0c;我是你们的老朋友lqj_本人&#xff0c;最近一周没有更新文章了&#xff0c;是因为最近学校有一些活动比赛&#xff0c;也有一部分原因就是我在录制一些关于前端方面的视频&#xff0c;涉及到的领域主要一前端&#xff0c;比如&#xff1a;H5开发&#xff0c;…

项目经理如何搞懂难缠的客户【静说】

作为乙方的项目经理&#xff0c;是否经常遇见难缠的客户&#xff0c;现环境下&#xff0c;大部分都是甲方强势&#xff0c;乙方弱势&#xff0c;双方处于一种不对等的基础上&#xff0c;项目经理如何生产&#xff0c;成功交付项目呢&#xff1f; 流程机制上如何应对&#xff1…

计算机毕业设计jsp教师课堂教学评价系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 教师课堂教学评价系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql&#xff0c…

智慧公交解决方案-最新全套文件

智慧公交解决方案-最新全套文件一、建设背景二、思路架构三、建设方案3大能力&#xff1a;1、数据驱动的智慧公交全息感知能力2、精细化精准化的公交健康诊断能力3、高品质的公交运营组织能力6大系统&#xff1a;1、公交线网健康诊断系统2、职能部门指挥决策支持系统3、公共出行…

Spring读取.xml和通过Java类配置对比

Spring读取配置文件获取容器,通过容器获得javaBean演示 1.创建一个空项目 配置项目JDK 新建module 选择Maven项目 注意路径 pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"…

C实现扫雷小游戏(简易版)

你知道&#xff0c;有些鸟儿是注定不会被关在牢笼里的&#xff0c;它们的每一片羽毛都闪耀着自由的光辉。——《肖申克的救赎》 目录 1、设计框架 2、设计流程 2.1菜单 2.2初始化雷阵 2.3生成雷 2.4玩家输入坐标 2.5显示有多少个雷 3、所有程序的源码 3.1game.h 3.2…

Slimming剪枝方法

本文参考&#xff1a;5-剪枝后模型参数赋值_哔哩哔哩_bilibiliz https://github.com/foolwood/pytorch-slimming 一、模型剪枝理论说明 论文&#xff1a;Learning Efficient Convolutional Networks through Network Slimming &#xff08;1&#xff09;卷积后得到多个特征图…

通过逻辑回归和感知器算法对乳腺癌数据集breastCancer和鸢尾花数据集iris进行线性分类

逻辑回归和感知器算法进行线性分类 代码使用了LogisticRegression和Perceptron两种分类方法 # 使用LogisticRegreeion分类器学习和测试 lr LogisticRegression() lr.fit(X_train_scaler, y_train) y_pred_lr lr.predict(X_test_scaler)#定义感知机 perceptron Perceptron(…

N3-PEG-ALD,Azide-PEG-Aldehyde,醛基-聚乙二醇-叠氮

1、名称 英文&#xff1a;N3-PEG-ALD&#xff0c;Azide-PEG-Aldehyde 中文&#xff1a;叠氮-聚乙二醇-醛基 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Aldehyde / Acetal PEG Azide PEG 4、分子量&#xff1a;可定制&#xff0c;5000 N3-PEG-ALD、10000 叠氮-PEG…

用html做一个漂亮的网站【茶文化12页】期末网页制作 HTML+CSS网页设计实例 企业文化网站制作

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

【仿牛客网笔记】项目进阶,构建安全高效的企业服务——Spring Security

https://spring.io/projects/spring-security 认证判断用户有没有登录。 授权 是访问有没有访问的权限 Spring MVC 的核心组件是DispatcherServlet&#xff0c;所有的组件都是交给DispatcherServlet处理&#xff0c;然后将请求分发给控制器&#xff0c;具体由某个控制器控制请求…