【数据结构】AVL树(万字超详细 附动图)

news2024/11/23 0:36:31

一、前言

二、AVL树的性质

三、AVL树节点的定义

四、AVL树的插入

五、AVL树的平衡调整

六、AVL树的验证

6.1 验证有序

6.2 验证平衡

七、AVL树的删除

八、AVL树的性能和代码


一、前言

还没有学习过二叉搜索树的同学可以移步

【数据结构】二叉搜索树-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Eristic0618/article/details/137919573?spm=1001.2014.3001.5501AVL树,又称为平衡二叉树,它基于二叉搜索树并通过平衡而得到。

在前面的学习中我们提到,二叉搜索树可以提高搜索数据的效率,但在数据有序的情况下会退化为单支树,此时在树中查找元素就得遍历一整个分支,时间复杂度也会退化至O(N)。

如果有一种算法,可以使二叉搜索树时刻保持左右子树的平衡,就可以避免这种最坏情况。

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


二、AVL树的性质

当我们向二叉搜索树中插入新节点时,如果能用某种方法时刻保证树中每个节点的左右子树高度之差不超过1,就可以降低整棵树的高度,保证每条分支的平衡

AVL树的性质如下:

  • AVL树可以是空树
  • 一颗AVL树的左右子树都是AVL树
  • 一颗AVL树的左右子树高度差不超过1

例如:


三、AVL树节点的定义

AVL树的左右子树高度差不能超过1,但是如何便捷的去检测该性质是否被打破呢?

我们可以在节点中定义一个平衡因子,如果左子树比右子树高一层,那么平衡因子就为-1;如果左右子树一样高,平衡因子就为0;如果右子树比左子树高一层,那么平衡因子就为1,这三种情况下AVL树的性质都没有被打破。

按照这个规则,如果平衡因子为-2、2或其他值,则说明左右子树已经失衡,性质被打破。

在调整失衡的AVL树时,我们需要频繁的访问父节点,所以在AVL树中我们需要使用三叉链,因此AVL树的节点除了包含左右子节点的指针,还需要一个指向父节点的指针

另外需要说明一下,本文中,我们使用key/value模型的AVL树

AVL树节点的定义如下:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	
	pair<K, V> _kv; //第一个数据存储key,第二个数据存储value
	int _bf; //平衡因子(balance factor)

	AVLTreeNode(const pair<const K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0) //新节点左右都为空,平衡因子为0
	{}
};

可能有些同学对pair没有了解,这里简单介绍一下

pair可以将两个数据组成一组元素,因此对于key/value模型这种需要用到两个数据为一组的元素时就可以使用,内部的成员变量为first和second,其主要使用方法为:

pair<T1, T2> p1(v1, v2); //输入两个数据创建pair类型变量
make_pair(v1, v2);       //输入两个数据通过函数创建pair类型变量
p1.first                 //访问p1的第一个数据
p1.second                //访问p1的第二个数据


四、AVL树的插入

向AVL树中插入节点与向二叉搜索树中插入节点的过程基本相同,唯一的区别就是AVL树在插入节点后可能存在失衡的情况,需要调整。

我们先按照二叉搜索树的规则将节点插入到AVL树中,并判断插入的节点在父节点的左边还是右边

按照平衡因子的规则,如果新节点插入到了父节点的左侧,那么父节点的平衡因子-1

如果新节点插入到了父节点的右侧,那么父节点的平衡因子+1

以上,便是新增节点的父节点平衡因子可能的变化情况。

但是!插入一个节点不但会影响父节点,还可能会影响到祖先节点

我们观察上面的四种可能,其中左边的两种情况下,插入节点后以父节点为根的子树高度发生了变化;在右边的两种情况下,插入节点后以父节点为根的子树高度没有发生变化。

观察过后可以发现,当父节点的平衡因子从0变为1/-1后,子树高度发生变化;当父节点的平衡因子从1/-1变为0后,子树高度不发生变化 

如果以父节点为根的子树高度没有发生变化,那么就不会影响到祖先节点的平衡因子;如果高度变了就会继续向上影响到祖先节点的平衡因子

因此,我们可以通过判断节点的插入位置计算父节点的平衡因子,进而判断子树高度是否发生变化,再进一步计算对祖先节点平衡因子的影响,来判断AVL树是否失衡

至此,我们已经可以开始写插入新节点和更新平衡因子的代码了:

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const pair<const K, V>& kv)
	{
		if (_root == nullptr) //检测为空树的情况
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;

		while (cur) //搜索新节点的插入位置
		{
			parent = cur;
			if (kv.first > cur->_kv.first)
				cur = cur->_right;
			else if (kv.first < cur->_kv.first)
				cur = cur->_left;
			else
				return false;
		}

		cur = new Node(kv);
        //将父节点与新节点链接
        //比较新节点和父节点的key判断插入到左边还是右边
		if (kv.first > parent->_kv.first) //这里防止有人看不懂再强调一遍,kv是pair类型的对象,kv.first是key,kv.second是value
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		while (cur != _root)
		{
            //插入节点后除了对父节点造成影响还可能对祖宗节点造成影响
            //因此随着循环进行,这里的cur不一定为新节点,可以理解为高度发生变化的子树的根节点
            //更新父节点的平衡因子
			if (cur == parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

            //更新后检测父节点的平衡因子
			if (parent->_bf == 0) //平衡因子为0说明没有打破性质,跳出循环
				break;
			else if (parent->_bf == 1 || parent->_bf == -1) //更新后平衡因子为1或-1说明高度发生变化,改变cur和parent的指向后继续向上更新
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) //更新后平衡因子为2或-2.说明已经失衡,需要调整
			{
                //不同情况的调整方法...
				if (parent->_bf == 2)
				{
					if (cur->_bf == 1)
					{
						//...
					}
					else if (cur->_bf == -1)
					{
						//...
					}
				}
				else
				{
					if (cur->_bf == 1)
					{
						//...
					}
					else if (cur->_bf == -1)
					{
						//...
					}
				}
				break;
			}
			else //平衡因子出现意外情况,报错
			{
				assert(false);
			}
		}

		return true;
	}

private:
	Node* _root = nullptr;
};


五、AVL树的平衡调整(附动图)

如果在一颗原本平衡的AVL树中插入一个新节点,可能会造成失衡,此时需要调整树的结构使之重新平衡,这种调整方法称为旋转

根据树的原本结构和节点插入位置的不同分为四种情况四种旋转方式

(1)新节点插入较高左子树左侧:右单旋

自己做的动图大家有需要自取~这里就不加水印了

问题来了:如何判断插入的新节点的方位呢?

很简单,以上面的情况为例,插入新节点后60的平衡因子变成-2,说明左子树更高,而30的平衡因子变成-1,说明新节点插入到了30的左子树。后面左单旋以及双旋中都同理,我们使用平衡因子就可以判断新节点插入的位置

右单旋代码如下:

void RotateRight(Node *parent) //parent为平衡因子发生失衡的节点
{
    Node *subL = parent->_left; //subL为parent的左子节点
    Node *subLR = subL->_right; //subLR为subL的右子节点
    //parent,subL和subLR三个节点是旋转中唯三需要进行操作的三个节点

    // 将parent与subLR节点进行链接
    parent->_left = subLR;
    if (subLR) //subLR可能为空
        subLR->_parent = parent;

    Node *parentParent = parent->_parent; //记录parent的父节点

    if (parent != _root)
    {
        subL->_parent = parentParent; //将subL与parent的父节点链接
        if (parent == parentParent->_left)
            parentParent->_left = subL;
        else
            parentParent->_right = subL;
    }
    else //如果parent为根,旋转后subL成为新的根
    {
        _root = subL;
        subL->_parent = nullptr;
    }

    //将subL与parent链接
    subL->_right = parent;
    parent->_parent = subL;

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

(2)新节点插入较高右子树右侧:左单旋

因为左单旋的原理和右单旋是类似的,只要理解了右单旋,加上动图的配合,左单旋和后面的双旋都是很好理解的 

左单旋代码如下:

void RotateLeft(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;

    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;

    Node *parentParent = parent->_parent;

    if (parent != _root)
    {
        subR->_parent = parentParent;
        if (parent == parentParent->_left)
            parentParent->_left = subR;
        else
            parentParent->_right = subR;
    }
    else
    {
        _root = subR;
        subR->_parent = nullptr;
    }

    subR->_left = parent;
    parent->_parent = subR;

    parent->_bf = subR->_bf = 0;
}

(3)新节点插入较高左子树右侧:先左单旋再右单旋(左右双旋) 

这种情况又可以分为两种情况:

不过这两种情况都属于在较高左子树的右侧插入,处理方式都是相同的,唯一的区别在于最后旋转完成后,更新平衡因子时的值不同。

接下来我们以上面的那个情况为例展示左右双旋的过程:

而下面的情况和上面的情况唯一的区别在于,最后更新的平衡因子不同

如何去决定每个节点更新后的平衡因子呢?可以看到这两种情况中,如果在b下面插入新节点,那么旋转过后30和60的平衡因子更新成0,90的平衡因子更新成1;如果在c下面插入新节点,则是60和90的平衡因子更新成0,30的平衡因子更新成-1

而新节点究竟插入到了b下面还是在c下面,我们可以通过插入节点后60的平衡因子来判断

左右双旋代码如下:

void RotateLR(Node *parent)
{
    Node *subL = parent->_left;
    Node *subLR = subL->_right;
    int bf = subLR->_bf; //记录插入节点后subLR的平衡因子

    RotateLeft(subL); //先左单旋
    RotateRight(parent); //再右单旋

    //更新平衡因子
    //通过前面记录的平衡因子判断更新的情况
    if (bf == 0)
    {
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
    else if (bf == 1)
    {
        subL->_bf = -1;
        parent->_bf = subLR->_bf = 0;
    }
    else if (bf == -1)
    {
        parent->_bf = 1;
        subL->_bf = subLR->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

(4)新节点插入较高右子树左侧:先右单旋再左单旋(右左双旋)

这种情况和左右双旋的情况原理一样,我们直接上动图和代码

右左双旋的代码如下:

void RotateRL(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;
    int bf = subRL->_bf;

    RotateRight(subR);
    RotateLeft(parent);

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

现在四个旋转的函数都实现了,完整的插入函数代码如下:

bool insert(const pair<const K, V> &kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    Node *parent = nullptr;
    Node *cur = _root;

    while (cur)
    {
        parent = cur;
        if (kv.first > cur->_kv.first)
            cur = cur->_right;
        else if (kv.first < cur->_kv.first)
            cur = cur->_left;
        else
            return false;
    }

    cur = new Node(kv);
    if (kv.first > parent->_kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }

    while (cur != _root)
    {
        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) //说明右子树更高
            {
                if (cur->_bf == 1) //说明新节点插入到了右边,符合在更高右子树的右侧插入的情况
                {
                    RotateLeft(parent); //执行左单旋
                }
                else if (cur->_bf == -1) //说明新节点插入到了左边,符合在更高右子树的左侧插入的情况
                {
                    RotateRL(parent); //执行右左双旋
                }
            }
            else //左子树更高
            {
                if (cur->_bf == 1) //说明新节点插入到了右边,符合在更高左子树的右侧插入的情况
                {
                    RotateLR(parent); //执行左右双旋
                }
                else if (cur->_bf == -1) //说明新节点插入到了左边,符合在更高左子树的左侧插入的情况
                {
                    RotateRight(parent); //执行右单旋
                }
            }
            break;
        }
        else
        {
            assert(false);
        }
    }

    return true;
}


六、AVL树的验证

6.1 验证有序

最重要的插入节点部分完成了,不过在验证是否符合AVL树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}

void _InOrder(Node *root)
{
    if (root == nullptr)
        return;
    _InOrder(root->_left);
    cout << root->_kv.first << " "; // key/value模型,我们只打印key即可
    _InOrder(root->_right);
}

验证:

说明符合二叉搜索树性质

6.2 验证平衡

要验证是否符合AVL树性质,只需要检测它的所有节点的子树高度差不超过1即可

需要注意的是,这里不可以直接通过判断平衡因子的绝对值是否大于1来验证平衡,因为平衡因子是不客观的,可以被修改

因此,我们通过递归来得到每棵子树的高度并进行判断即可

代码如下:

bool IsBalance()
{
    return _IsBalance(_root);
}

bool _IsBalance(Node *root)
{
    if (root == nullptr)
        return true;
    int leftHeigit = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    if (rightHeight - leftHeigit != root->_bf)
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }

    return abs(rightHeight - leftHeigit) <= 1 
        && _IsBalance(root->_left) 
        && _IsBalance(root->_right);
}

int Height()
{
    return _Height(_root);
}

int _Height(Node *root)
{
    if (root == nullptr)
        return 0;
    int higher = max(_Height(root->_left), _Height(root->_right));
    return higher + 1;
}

验证:

如果在验证是否是AVL树中需要更多大量的测试用例,我们可以取一些随机数:

int main()
{
	const int N = 1000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (int i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}

	AVLTree<int, int> t;
	for (auto i : v)
	{
		t.insert(make_pair(i, 1));
	}
	t.InOrder();
	if (t.IsBalance())
		cout << "是AVL树" << endl;
	else
		cout << "不是AVL树" << endl;
	return 0;
}

 


七、AVL树的删除

AVL树的删除并不是本文的重点,因为其原理我们在前面已经学习过了

我们只需要按照二叉搜索树的方式来对目标节点进行删除,再判断是否失衡,如果失衡则旋转即可


八、AVL树的性能和代码

AVL树追求的是严格平衡,因此可以保证查找时高效的时间复杂度O(logN),但是如果我们需要频繁的对其进行旋转来维护平衡,一定程度上会影响效率,尤其是删除节点时的最差情况下我们可能需要一路旋转到根的位置。

相对于AVL树的严格平衡,红黑树则追求一种相对平衡,因此会略胜一筹,后面的文章中会对红黑树进行讲解。

AVL树的完整代码如下:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	
	pair<K, V> _kv;
	int _bf; //平衡因子

	AVLTreeNode(const pair<const 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 pair<const K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{
			parent = cur;
			if (kv.first > cur->_kv.first)
				cur = cur->_right;
			else if (kv.first < cur->_kv.first)
				cur = cur->_left;
			else
				return false;
		}

		cur = new Node(kv);
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		while (cur != _root)
		{
			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)
				{
					if (cur->_bf == 1)
					{
						RotateLeft(parent);
					}
					else if (cur->_bf == -1)
					{
						RotateRL(parent);
					}
				}
				else
				{
					if (cur->_bf == 1)
					{
						RotateLR(parent);
					}
					else if (cur->_bf == -1)
					{
						RotateRight(parent);
					}
				}
				break;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}

	void RotateLeft(Node* parent) //新节点插入较高右子树的右侧:左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if(subRL)
			subRL->_parent = parent;

		Node* parentParent = parent->_parent;

		if (parent != _root)
		{
			subR->_parent = parentParent;
			if (parent == parentParent->_left)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
		}
		else
		{
			_root = subR;
			subR->_parent = nullptr;
		}

		subR->_left = parent;
		parent->_parent = subR;

		parent->_bf = subR->_bf = 0;
	}

	void RotateRight(Node* parent) //新节点插入较高左子树的左侧:右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		Node* parentParent = parent->_parent;

		if (parent != _root)
		{
			subL->_parent = parentParent;
			if (parent == parentParent->_left)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		else
		{
			_root = subL;
			subL->_parent = nullptr;
		}

		subL->_right = parent;
		parent->_parent = subL;

		parent->_bf = subL->_bf = 0;
	}

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateRight(subR);
		RotateLeft(parent);

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

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateLeft(subL);
		RotateRight(parent);

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

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	int Height()
	{
		return _Height(_root);
	}

	size_t Size()
	{
		return _Size(_root);
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_kv.first)
				cur = cur->_right;
			else if (key < cur->_kv.first)
				cur = cur->_left;
			else
				return cur;
		}
		return nullptr;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int leftHeigit = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		if (rightHeight - leftHeigit != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return abs(rightHeight - leftHeigit) <= 1 
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int higher = max(_Height(root->_left), _Height(root->_right));
		return higher + 1;
	}

	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return _Size(root->_left) + _Size(root->_right) + 1;
	}
private:
	Node* _root = nullptr;
};

如果有错误的地方,欢迎在评论区指出

完.

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

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

相关文章

Acrobat Pro DC 2021:强大的PDF编辑软件

Acrobat Pro DC 2021是Adobe公司推出的一款全面而强大的PDF编辑软件&#xff0c;凭借其卓越的性能和丰富的功能&#xff0c;成为了PDF编辑领域的领军者。 Acrobat Pro DC 2021中文激活版下载 这款软件具备全面的PDF编辑功能&#xff0c;包括文本编辑、图片处理、页面组织等&…

Vue3引入高德地图js API 2.0

文章目录 前言一、地图加载1.本文准备环境2.引入库3.加载地图4.加载地图控件 二、POI搜索1.什么是poi搜索2.如何使用 三、绘制点标记与信息窗体1.场景描述2.案例3.信息窗体-链接路由跳转4.进阶-通过Marker自动触发标记点&#xff08;非鼠标手动点击&#xff09; 四、jsApi地图事…

指挥中心实战指挥平台-通信指挥类装备多链路聚合设备解决方案实例

一、建设目标及要求 坚持“一切为了实战、一切围绕实战、一切服务实战”的总要求&#xff0c;紧紧围绕大数据应用和自动化、智能化、智慧化这一主题主线&#xff0c;建设升级改造支队指挥中心&#xff0c;集成语音、视频、即时消息、短信、对讲、会议等多媒体通信能力&#xf…

AJAX——图片上传

图片上传流程 1.获取图片文件对象 2.使用FormData携带图片文件 3.提交表单数据到服务器&#xff0c;使用图片url网址 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible"…

埋点,自己写插件,自己写的按钮埋点,掘金同款投递简历

20分钟掌握 Vite 插件开发 - 掘金 vite的生命周期啥的 1.浏览器的控制台输出有样式的字 // const randomLetterPlugin ()>{ // const letters [wwwwwww,000000000000,888888888888]; // //随机获取一个字符并打印 // const printRandomLetter ()>{ // …

LCD彩屏显示方案选型攻略:从接口到GUI开发工具的全面评估

在现代人类社会&#xff0c;彩色显示技术是科技王国里最为绚丽夺目的技术奇葩&#xff0c;LCD彩屏通过显示实时信息并提供交互式的体验&#xff0c;将信息时代打扮得多姿多彩。无论是智能家电还是医疗健康设备领域&#xff0c;精美直观的LCD彩屏显示&#xff0c;往往能够为用户…

学习JFinal

1.五个配置类 configConstants&#xff08;配置&#xff09;&#xff1a; configRoute&#xff08;路由&#xff09;&#xff1a; 2.Controller层&#xff08;控制器&#xff09; 访问流程&#xff1a; Action&#xff1a; getPara&#xff1a; 参数说明&#xff1a;第一个参…

monkey测试详解

一、monkey测试的定义 Monkey 测试是通过向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),实现对应用程序客户端的稳定性测试;通俗来说,Monkey 测试即“猴子测试”,是指像猴子一样,不知道程序的任何用户交互方面的知识,就对界面进行无目的、乱点乱按…

MATLAB基于图像特征的火灾检测

实验目标 基于图像特征的火灾检测 实验环境 Windows电脑、MATLAB R2020a 实验内容 1.读取图像&#xff0c;彩色图像的灰度化处理&#xff0c;进行边缘检测&#xff0c;实现对火焰的检测。 2.对两幅RGB图像进行火焰特征提取实验 3.对比分析边缘检测算法处理结果和基于图像…

离散型制造行业智能工厂解决方案,助力国家新智产业升级

离散型制造行业智能工厂标准解决方案 离散型制造行业的智能工厂解决方案是推动国家智能制造产业升级的关键。 1. 集中优势资源&#xff1a;实施攻关计划&#xff0c;瞄准关键核心技术和重点产业进行定向突破&#xff0c;特别是在集成电路(IC)、AI、生物医药等领域。2. 国家创…

华为外派伊拉克,一天补助6000元

大家好&#xff0c;我是YUAN哥&#xff01; 最近有朋友询问关于华为外派伊拉克的补助情况&#xff0c;听说一天能拿到6000元&#xff0c;这听起来相当诱人&#xff0c;但真实性有待考证。 据我所知&#xff0c;华为对艰苦地区的外派员工确实有补助&#xff0c;但最高一天是100美…

关于pdf.js中文本坐标尺寸的使用

一个电子教材项目中有这样一个需求&#xff1a; 用户向网站上传一个PDF书籍后&#xff0c;网站可以对PDF书籍进行解析&#xff0c;并支持用户对PDF书籍的每一页做一些操作&#xff0c;比如&#xff1a;为英语课本的单词和句子添加音频热区。因为热区数量很多&#xff0c;所以&a…

C语言进阶课程学习记录- 函数与宏分析

C语言进阶课程学习记录- 函数与宏分析 实验-宏和函数实验-宏的副作用实验-宏的妙用小结 本文学习自狄泰软件学院 唐佐林老师的 C语言进阶课程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记录 实验-宏和函数 #include <stdio.h>#define RESET(p, len) …

详解Mixtral-8x7B背后的MoE!

高端的模型往往只需最朴素的发布方式。 这个来自欧洲的大模型团队在12月8日以一条磁力链接的方式发布了Mixtral-8x7B,这是一种具有开放权重的**「高质量稀疏专家混合模型」**(SMoE)。 该模型在大多数基准测试中都优于Llama2-70B,相比之下推理速度快了6倍,同时在大多数标准基…

Web3 游戏周报(4.14-4.20)

【4.14-20】Web3 游戏行业动态&#xff1a; 前迪士尼老板与漫威、星球大战人才携手推出 Web3 游戏工作室 加密集换式卡牌游戏《Fantasy》在 Blast 主网上线 加密游戏工作室 Avalon 融资 1,000 万美元&#xff0c;Hashed 领投 Faraway 收购 Yuga Labs 旗下两大游戏 IP“HV-MT…

JSON Web Token 入门

JSON Web Token&#xff08;缩写 JWT&#xff09;是目前最流行的跨域认证解决方案&#xff0c;本文介绍它的原理和用法。 一、跨域认证的问题 互联网服务离不开用户认证。一般流程是下面这样。 1、用户向服务器发送用户名和密码。 2、服务器验证通过后&#xff0c;在当前对话&…

以赛促学、生态共建 | 软通动力子公司鸿湖万联成功举办基于x86架构的OpenHarmony应用生态挑战赛

近日&#xff0c;由开放原子开源基金会、央视网、江苏省工业和信息化厅、无锡市人民政府、江苏软件产业人才发展基金会、苏州工业园区、无锡高新区等共同承办&#xff0c;鸿湖万联参与共建的“基于x86架构的OpenHarmony应用生态挑战赛”决赛路演在无锡圆满落幕。本次挑战赛历时…

贪心(贪婪)算法

主要思想 贪心算法的思想主要可以概括为“总是做出当前看起来最优的选择”&#xff0c;也就是不从整体上进行考虑&#xff0c;所得到的答案是某种意义上的局部最优解&#xff0c;不一定是整体最优解。 贪心算法没有固定算法框架&#xff0c;算法设计的关键是贪心策略的选择。…

统一SQL 支持Oracle CHAR和VARCHAR2 (size BYTE|CHAR)转换

统一SQL介绍 https://www.light-pg.com/docs/LTSQL/current/index.html 源和目标 源数据库&#xff1a;Oracle 目标数据库&#xff1a;Postgresql&#xff0c;TDSQL-MySQL&#xff0c;达梦8&#xff0c;LightDB-Oracle 操作目标 在Oracle中的CHAR和VARCHAR2数据类型&…

以始为终梳理前端的发展方向

嗨&#xff0c;我是小路。一位努力向上生长的90后前端开发工程师。 以下是正文&#xff1a; 前段时间朋友和我吐槽&#xff1a;“做了多年的PHP开发&#xff0c;突然被离职&#xff0c;然后去招聘市场一看&#xff0c;发现PHP已经没有市场了。偶尔会出现一两个相关的职位&#…