数据结构——“AVL树”的四种数据旋转的方法

news2024/10/2 18:13:53

        因为上次普通的二叉搜索树在极端情况下极容易造成我们的链式结构(这会导致我们查询的时间复杂度变为O(n)),然而AVL树就很好的解决了这一问题(归功于四种旋转的方法),它让我们的树的查询的时间复杂度变得接近于甚至等于O(logN)。它相比于普通的二叉搜索树,它增加了平衡因子来维持树的高度,增加了纸箱上一个节点的parent指针。另外,平衡因子的计算方法是右子树的高度减去左子树的高度,当当前节点的平衡因子的值为-1、0、1的时候,我们认为当前节点是平衡的,当为其他值的时候就不平衡,需要通过旋转来将树调整平衡。

        废话不多说,让我们了解一下四种旋转方式吧。

        一、右旋

        右旋的情况(最小)出现在一直向左子树插入数据,就像这样:

74ce36d1ae1f4d979c9a088dc67ff7e3.png

        最后插入的6让10的平衡因子变为-2,让本就不怎么平衡的数变得彻底不平衡,因此就需要右旋。它的规则是什么呢?

        当树满足右旋的条件的时候(当前节点的平衡因子为2或者-2),右旋的时候,平衡因子为-2,在这里满足条件的是10节点,我们把它的左子树的右子树给该节点的左子树,即:用节点10替换掉8的右子树,但是8的右子树是有东西的,而10的左子树刚好又不指向8,那么就进行互换一次。

bfadcfd6dd8c452e9768a90fd199efdd.png

        最后再次修改平衡因子:

9b934bd33f1349188b78e01a8e0b1f02.png

        下边可以看一个实例:

        这是一棵已经满足AVL树的树:

7538a8530da84cac9b16c9e95287f0a6.png

        在5的左子树插入数据:

96dc88a2bbb643ecbd06baafe1ae852c.png

     节点更新到6,不满足条件,需要旋转:

69f3fd583c5e4de4b93e21e5cf13772d.png

                更新平衡因子:

668e0d3c75a44053bf03d2cbde021f44.png

        用过重复的测试和观察可以发现,到最后,旋转的节点和他的左子树最后的平衡因子都是从-2、-1变到0、0.

        值得注意的是在插入节点后,需要向上更新平衡因子,倘若更新后遇到平衡因子为0,那就没有再次向上更新的必要了。

插入代码结构如下:

	//AVL树的插入
	bool Insert(K key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		//插入数据
		if (cur == nullptr)
		{
			_root = new Node(key);
		}
		else
		{
			//寻找插入的位置
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//插入
			cur = new Node(key);
			if (key > parent->_key)//插入的值大于父亲节点,那么就需要在父亲节点的右边插入
			{
				parent->_right = cur;
				parent->_right->_parent = parent;
			}
			else if(key < parent->_key)//插入的值小于父亲节点,那么就需要在父亲节点的左边插入
			{
				parent->_left = cur;
				parent->_left->_parent = parent;
			}
			else
			{
				assert(false);
			}
		}
		//更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bl--;
			}
			else if (cur == parent->_right)
			{
				parent->_bl++;
			}
			//检查树是否平衡
			if (parent->_bl == 0)//平衡,退出函数
			{
				return true;
			}
			else if (parent->_bl == 1 || parent->_bl == -1)//半平衡,向上更新,直到为0或者更新到根
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bl == 2 || parent->_bl == -2)//不平衡,旋转
			{
                这里是四种旋转的区域

				return true;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}

        那么知道原理后,就来用代码实现以下:

	//右旋
	void RotateR(Node* node)
	{
		Node* kidL = node->_left;
		Node* kidLR = node->_left->_right;
		Node* pparent = node->_parent;

		//先把该节点移动到kidl的右边
		node->_left = kidLR;
		if (kidLR)//只有在kidLR不为空的情况下才可以更新kidLR的父亲节点
		{
			kidLR->_parent = node;
		}
		kidL->_right = node;
		node->_parent = kidL;

		
		if (pparent == nullptr)//如果被旋转节点的父亲为空,则说明此节点为根节点
		{
			_root = kidL;
			kidL->_parent = nullptr;
		}
		else//不为根节点
		{
			if (node == pparent->_left)//判断被旋转的节点在父亲节点的左边还是右边
			{
				pparent->_left = kidL;
			}
			else
			{
				pparent->_right = kidL;
			}

			kidL->_parent = pparent;
		}
		
		kidL->_bl = node->_bl = 0;//更新平衡因子
	}

        在写代码的过程中,为了防止出错,建议把需要更改的节点的位置用指针记录下来,一方面是为了防止在写代码的过程中因为误操作修改了指针的指向,导致再次使用的时候因为指针的变化从而操作本不应该操作的节点,另一方面是优化代码可读性,毕竟连续的“->”会让人感觉头疼。

看看示例:

int main()
{
	AVLTree<int> tree;
	tree.Insert(10);
	tree.Insert(8);
	tree.Insert(7);
	tree.Insert(6);
	tree.Insert(5);
	tree.Insert(4);


	tree.InTraversal();


	return 0;
}

         运行结果(中序遍历):

41eced73b97f4715a0811f5548124d69.png

二、左旋

        左旋和右旋一样,它的发生情况是这种的:

896a1d48783b432983479a923fae9791.png

        它和右旋一样,不过是方向相反的,我们需要把13的左边给10的右边,然后把10给13的左边,做后需要注意的细节和右旋一样,就是父亲节点的更新。这一点需要大家画图研究一下。

        另外,经过观察可以得到,左旋的条件是被旋转的节点平衡因子为2,它的右子树的平衡因子为1。

        由于和右旋相反,所以可以在右旋代码中做修改(几乎所有的被改数值都和右旋相反):

//左旋
	void RotateL(Node* node)
	{
		Node* kidR = node->_right;
		Node* kidRL = node->_right->_left;
		Node* pparent = node->_parent;

		//先把该节点移动到kidl的左边
		node->_right = kidRL;

		if (kidRL)//只有在kidRL不为空的情况下才可以更新kidRL的父亲节点
		{
			kidRL->_parent = node;
		}
		kidR->_left = node;
		node->_parent = kidR;


		if (pparent == nullptr)//如果被旋转节点的父亲为空,则说明此节点为根节点
		{
			_root = kidR;
			kidR->_parent = nullptr;
		}
		else//不为根节点
		{
			if (node == pparent->_left)//判断被旋转的节点在父亲节点的左边还是右边
			{
				pparent->_left = kidR;
			}
			else
			{
				pparent->_right = kidR;
			}

			kidR->_parent = pparent;
		}

		kidR->_bl = node->_bl = 0;//更新平衡因子
	}


示例:

int main()
{
	AVLTree<int> tree;
	tree.Insert(1);
	tree.Insert(2);
	tree.Insert(3);
	tree.Insert(4);
	tree.Insert(5);
	tree.Insert(6);


	tree.InTraversal();


	return 0;
}

 运行结果:

3be2c82787664c05a876d18fa3419f27.png

三、左右双旋

        左右双旋的情况(最小)大致是这样:

f50f5d19c1f94745881020a13df09ce1.png

        这种情况比较复杂,但是解决方法就是先对5进行左旋,再对10进行右旋,对5左旋是为了把这棵树变为纯粹的左边高(旋转后满足右旋的条件),然后再进行右旋。

e34dc7bd998b4fa9b90fb46a4f7d1cb4.png

        在这里有一个值得注意的问题:被调整的节点的左子树的右子树的高度会影响被调整的节点的左子树或者被调整的节点的平衡因子,举个例子:

        1.上边的例子是8的平衡因子为0的情况,那么最后parent(10)和kid(5)的平衡因子为0。

        2.当8的平衡因子为1的时候(框里的代表能够满足左右双旋的情况)影响的是被调整的节点的左子树的平衡因子,即5的平衡因子

68a744aed5414d68adef53add42ca79d.png

       a.在8节点右边插入节点:

3c2310b752364f5081f351f854a2a80e.png

旋转:

dfddb5ac533046c3ab9dd709c2b641ab.png

        最后5的节点的平衡因子为-1,10的节点的平衡因子为0。

        b. a.在8节点左边插入节点:

5853e9174b1c43328a7dffb7b31ff205.png

最后5的节点的平衡因子为0,10的节点的平衡因子为1。

        另外通过观察可得被旋转的节点的平衡因子为-2,它的左子树平衡因子为1的时候,发生左右旋转。

代码如下:

	//左右双旋
	void RotateLR(Node* node)
	{
		Node* kidL = node->_left;
		Node* kidLR = node->_left->_right;
		int bl = kidLR->_bl;
		//先对左边的节点进行左旋
		RotateL(kidL);
		//再对该节点进行右旋
		RotateR(node);

		if (bl == 0)
		{
			node->_bl = kidL->_bl = kidLR->_bl = 0;
		}
		else if (bl == 1)
		{
			kidL->_bl = -1;
			node->_bl = kidLR->_bl = 0;
		}
		else if (bl == -1)
		{
			node->_bl = 1;
			kidL->_bl = kidLR->_bl = 0;
		}

	}

测试示例:

int main()
{
	AVLTree<int> tree;
	tree.Insert(10);
	tree.Insert(5);
	tree.Insert(11);
	tree.Insert(4);
	tree.Insert(8);
	tree.Insert(9);


	tree.InTraversal();


	return 0;
}

运行结果:

25100a08c10f464683045755ccb7b9e6.png

四、右左双旋

        同左旋和右旋的关系,他们只是方向相反,能更改的东西也应该相反,它的形成条件如下图:

57aed070786d4dfa8f1beb5697f54e4b.png

代码:

	//右左双旋
	void RotateRL(Node* node)
	{
		Node* kidR = node->_right;
		Node* kidRL = kidR->_left;
		int bl = kidRL->_bl;
		//先对右边的节点进行右旋
		RotateR(kidR);
		//再对该节点进行左旋
		RotateL(node);

		if (bl == 0)
		{
			node->_bl = kidR->_bl = kidRL->_bl = 0;
		}
		else if (bl == 1)
		{
			node->_bl = -1;
			kidR->_bl = kidRL->_bl = 0;
		}
		else if (bl == -1)
		{
			kidR->_bl = 1;
			node->_bl = kidRL->_bl = 0;
		}
		else
		{
			return assert(false);
		}
	}

示例:

int main()
{
	AVLTree<int> tree;
	tree.Insert(10);
	tree.Insert(9);
	tree.Insert(15);
	tree.Insert(11);
	tree.Insert(16);
	tree.Insert(12);
	tree.Insert(13);

	tree.InTraversal();

	return 0;
}


ee934faf8a2b4aef95665af1a79614cf.png

总代码:


#include<iostream>
#include<assert.h>

using namespace std;


template <class K>
struct AVLTreenode
{
	AVLTreenode(K key)
		:_key(key)
	{}
	K _key;
	AVLTreenode* _left = nullptr;
	AVLTreenode* _right = nullptr;
	AVLTreenode* _parent = nullptr;
	int _bl = 0;
};


template <class K>
class AVLTree
{
private:
	using Node = AVLTreenode<K>;
	Node* _root = nullptr;
public:

	void _InTraversal(Node* p)
	{
		if (p == nullptr)
			return;

		_InTraversal(p->_left);
		cout << p->_key << " ";
		_InTraversal(p->_right);
	}
	//搜索树的中序遍历
	void InTraversal()
	{
		_InTraversal(_root);
	}
	//搜索树的查找
	Node* Find(const K& key)
	{
		assert(_root);
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

	//AVL树的插入
	bool Insert(K key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		//插入数据
		if (cur == nullptr)
		{
			_root = new Node(key);
		}
		else
		{
			//寻找插入的位置
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//插入
			cur = new Node(key);
			if (key > parent->_key)//插入的值大于父亲节点,那么就需要在父亲节点的右边插入
			{
				parent->_right = cur;
				parent->_right->_parent = parent;
			}
			else if(key < parent->_key)//插入的值小于父亲节点,那么就需要在父亲节点的左边插入
			{
				parent->_left = cur;
				parent->_left->_parent = parent;
			}
			else
			{
				assert(false);
			}
		}
		//更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bl--;
			}
			else if (cur == parent->_right)
			{
				parent->_bl++;
			}
			//检查树是否平衡
			if (parent->_bl == 0)//平衡,退出函数
			{
				return true;
			}
			else if (parent->_bl == 1 || parent->_bl == -1)//半平衡,向上更新,直到为0或者更新到根
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bl == 2 || parent->_bl == -2)//不平衡,旋转
			{
				if (parent->_bl == -2 && parent->_left->_bl == -1)//右旋
				{
					RotateR(parent);
				}
				else if (parent->_bl == 2 && parent->_right->_bl == 1)//左旋
				{
					RotateL(parent);
				}
				else if (parent->_bl == -2 && parent->_left->_bl == 1)//左右双旋
				{
					RotateLR(parent);
				}
				else if (parent->_bl == 2 && parent->_right->_bl == -1)//右左双旋
				{
					RotateRL(parent);
				}

				return true;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}
	//右旋
	void RotateR(Node* node)
	{
		Node* kidL = node->_left;
		Node* kidLR = node->_left->_right;
		Node* pparent = node->_parent;

		//先把该节点移动到kidl的右边
		node->_left = kidLR;
		if (kidLR)//只有在kidLR不为空的情况下才可以更新kidLR的父亲节点
		{
			kidLR->_parent = node;
		}
		kidL->_right = node;
		node->_parent = kidL;

		
		if (pparent == nullptr)//如果被旋转节点的父亲为空,则说明此节点为根节点
		{
			_root = kidL;
			kidL->_parent = nullptr;
		}
		else//不为根节点
		{
			if (node == pparent->_left)//判断被旋转的节点在父亲节点的左边还是右边
			{
				pparent->_left = kidL;
			}
			else
			{
				pparent->_right = kidL;
			}

			kidL->_parent = pparent;
		}
		
		kidL->_bl = node->_bl = 0;//更新平衡因子
	}

//左旋
	void RotateL(Node* node)
	{
		Node* kidR = node->_right;
		Node* kidRL = node->_right->_left;
		Node* pparent = node->_parent;

		//先把该节点移动到kidl的左边
		node->_right = kidRL;

		if (kidRL)//只有在kidRL不为空的情况下才可以更新kidRL的父亲节点
		{
			kidRL->_parent = node;
		}
		kidR->_left = node;
		node->_parent = kidR;


		if (pparent == nullptr)//如果被旋转节点的父亲为空,则说明此节点为根节点
		{
			_root = kidR;
			kidR->_parent = nullptr;
		}
		else//不为根节点
		{
			if (node == pparent->_left)//判断被旋转的节点在父亲节点的左边还是右边
			{
				pparent->_left = kidR;
			}
			else
			{
				pparent->_right = kidR;
			}

			kidR->_parent = pparent;
		}
		kidR->_bl = node->_bl = 0;//更新平衡因子
	}
	//左右双旋
	void RotateLR(Node* node)
	{
		Node* kidL = node->_left;
		Node* kidLR = node->_left->_right;
		int bl = kidLR->_bl;
		//先对左边的节点进行左旋
		RotateL(kidL);
		//再对该节点进行右旋
		RotateR(node);

		if (bl == 0)
		{
			node->_bl = kidL->_bl = kidLR->_bl = 0;
		}
		else if (bl == 1)
		{
			kidL->_bl = -1;
			node->_bl = kidLR->_bl = 0;
		}
		else if (bl == -1)
		{
			node->_bl = 1;
			kidL->_bl = kidLR->_bl = 0;
		}
		else
		{
			return assert(false);
		}

	}
	//右左双旋
	void RotateRL(Node* node)
	{
		Node* kidR = node->_right;
		Node* kidRL = kidR->_left;
		int bl = kidRL->_bl;
		//先对右边的节点进行右旋
		RotateR(kidR);
		//再对该节点进行左旋
		RotateL(node);

		if (bl == 0)
		{
			node->_bl = kidR->_bl = kidRL->_bl = 0;
		}
		else if (bl == 1)
		{
			node->_bl = -1;
			kidR->_bl = kidRL->_bl = 0;
		}
		else if (bl == -1)
		{
			kidR->_bl = 1;
			node->_bl = kidRL->_bl = 0;
		}
		else
		{
			return assert(false);
		}
	}

};

 

 

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

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

相关文章

TIM(Timer)定时器的原理

一、介绍 硬件定时器的工作原理基于时钟信号源提供稳定的时钟信号作为计时器的基准。计数器从预设值开始计数&#xff0c;每当时钟信号到达时计数器递增。当计数器达到预设值时&#xff0c;定时器会触发一个中断信号通知中断控制器处理相应的中断服务程序。在中断服务程序中&a…

无人化焦炉四大车系统 武汉正向科技 工业机车无人远程控制系统

焦炉四大车无人化系统介绍 采用格雷母线光编码尺双冗余定位技术&#xff0c;炉门视觉定位自学习技术&#xff0c;wifi5G无线通讯技术&#xff0c;激光雷达安全识别技术&#xff0c;焦化智慧调度&#xff0c;手机APP监控功能。 焦炉四大车无人化系统功能 该系统能自动生成生产…

遥感图像垃圾处理场分割,北京地区高分2图像,3500张图像,共2GB,分割为背景,空地,垃圾,垃圾处理设施四类

遥感图像垃圾处理场分割&#xff0c;北京地区高分2图像&#xff0c;3500张图像&#xff0c;共2GB&#xff0c;分割为背景&#xff0c;空地&#xff0c;垃圾&#xff0c;垃圾处理设施四类 遥感图像垃圾处理场分割数据集 规模 图像数量&#xff1a;3500张数据量&#xff1a;2G…

黑科技!Llama 3.2多模态AI震撼发布

黑科技&#xff01;Llama 3.2多模态AI震撼发布 Meta发布Llama 3.2模型&#x1f680;&#xff0c;引领AI新潮流&#xff01;它能处理文字、图片、视频&#x1f4f8;&#xff0c;满足不同需求&#xff0c;性能媲美大牌选手✨。一键启动包已准备好&#xff0c;让你轻松体验AI的魔…

模版and初识vector

一、引言 在C语言中&#xff0c;不论是数组&#xff0c;还是结构体定义的数组&#xff0c;功能都比较欠缺&#xff0c;不是单纯的添加几个变量就能够解决的。缺少增删查改的功能&#xff0c;为了解决这个问题&#xff0c;C决定填上C语言这个坑&#xff0c;但是填过坑的人都知道…

秋招突击——算法练习——复习{双指针:移动零、盛最多的水、三数之和}——新作{接雨水}

文章目录 引言复习移动零盛最多的水三数之和 新作接雨水个人实现参考实现 总结 引言 这段时间还是很迷茫的&#xff0c;秋招到了一个阶段&#xff0c;但是收获并不是很多&#xff0c;基本上都在泡池子&#xff0c;没有意向。也就没有在坚持刷题&#xff0c;只是整理一些专门的…

Arduino UNO R3自学笔记15 之 Arduino如何驱动数码管?

注意&#xff1a;学习和写作过程中&#xff0c;部分资料搜集于互联网&#xff0c;如有侵权请联系删除。 前言&#xff1a;学习使用数码管。 1.数码管介绍 数码管的一种是半导体发光器件&#xff0c;数码管可分为七段数码管和八段数码管&#xff0c;区别在于八段数码管比七段数…

【数据结构】图论基础

文章目录 图的概念图的基本概念图的类型图的表示方法 图的相关基本概念1. 路径&#xff08;Path&#xff09;2. 连通性&#xff08;Connectivity&#xff09;3. 图的度&#xff08;Degree&#xff09;4. 子图&#xff08;Subgraph&#xff09;5. 生成树&#xff08;Spanning Tr…

LabVIEW提高开发效率技巧----快速实现原型和测试

在LabVIEW开发中&#xff0c;DAQ助手&#xff08;DAQ Assistant&#xff09;和Express VI为快速构建原型和测试功能提供了极大的便利&#xff0c;特别适合于简单系统的开发和早期验证阶段。 DAQ助手&#xff1a;是一种可视化配置工具&#xff0c;通过图形界面轻松设置和管理数据…

CSS3渐变

一、线性渐变 通过background-image: linear-gradient(...)设置线性渐变 语法&#xff1a; linear-gradient(direction,color1,color2, . . ) direction&#xff1a;渐变方向&#xff0c;默认从上到下&#xff0c;可选值&#xff1a; 简单选取&#xff1a; ① to right&…

Python和C++及MATLAB和R时间序列中数学物理金融气象运动和电子材料

&#x1f3af;要点 小波分析&#xff0c;量化噪声概率分布和统计推理物理量和化学量数值计算确定性非线性系统金融资本市场和市场流动性波形传播气象建模评估 Python时间序列数学 时间序列分析是一种强大的统计技术&#xff0c;广泛应用于经济学、金融学、环境科学和工程学…

基于SSM+Vue技术的定制式音乐资讯平台

文未可获取一份本项目的java源码和数据库参考。 一、选题的背景与意义&#xff1a; 随着个人计算机的普及和互联网技术的日渐成熟&#xff0c;网络正逐渐成为人们获取信息及消费的主要渠道。然而在当前这个信息时代&#xff0c;网络中的信息种类和数量呈现爆炸性增长的趋势&a…

基于Node.js+Express+MySQL+VUE实现的在线电影视频点播网站管理系统的设计与实现部署安装

目录 1. 引言 1.1开发背景 1.2开发意义 1.3国内外研究 2. 需求分析 3. 系统架构设计 4. 关键技术选型 5. 功能模块设计 5.1功能图 5.2界面介绍 6. 总结 1. 引言 随着互联网技术的快速发展和普及&#xff0c;人们获取信息的方式发生了巨大变化&#xff0c;其中在…

PCL库简单的icp配准

#include <pcl/io/pcd_io.h> #include <pcl/point_types.h> #include <pcl/registration/icp.h>int main(int argc, char** argv) {// 确保提供了两个PCD文件作为输入if (argc ! 3) {PCL_ERROR("请提供两个PCD文件作为输入。\n");return (-1);}// …

dcatadmin 自定义登录页面

一、问题&#xff1a; 在后台管理系统中&#xff0c;不同的项目想要不同的登录页面&#xff0c;但是框架自带的登录页面就只有一个。 解决&#xff1a; 由芒果系统改造的dcatadmin登录插件&#xff0c;实现一键安装改变登录页面。 项目介绍 基于Laravel和Vue的快速开发的后台管…

html5 + css3(上)

目录 HTML认知web标准vscode的简介和使用注释标题和段落换行和水平线标签文本格式化标签图片图片-基本使用图片-属性 绝对路径相对路径音频标签视频标签超链接 HTML基础列表列表-无序和有序列表-自定义 表格表格-使用表格-表格标题和表头单元格表格-结构标签&#xff08;了解&a…

CentOS 6文件系统

由冯诺依曼在 1945 年提出的计算机五大组成部分&#xff1a;运算器&#xff0c;控制器&#xff0c;存储器&#xff0c;输入设 备&#xff0c;输出设备。 1. 硬盘结构&#xff1a; &#xff08;1&#xff09;机械硬盘结构&#xff1a; 磁盘拆解图&#xff1a; 扇区&#xff0c;…

白杨SEO:抖音上做自然搜索流量怎么挖掘出抖音流量关键词及布局进去?【举例】

前言&#xff1a;为什么想到再分享这个&#xff1f;因为发现很多人在抖音做搜索流量时怎么挖掘抖音关键词这个基础以及怎么布局进去不太清楚&#xff0c;所以再来写下&#xff0c;希望对大家有帮助。 文章大纲&#xff1a; 1、抖音搜索流量如何确定业务词&#xff1f; 2、抖音…

Ubuntu下安装Zookeeper集群

Zookeeper集群是一个开源的分布式协调服务系统&#xff0c;它由Apache软件基金会维护&#xff0c;旨在为分布式应用提供一致性和可靠性的服务。 在Zookeeper集群中&#xff0c;服务器可以扮演三种角色——领导者&#xff08;Leader&#xff09;、跟随者&#xff08;Follower&a…

开放式耳机哪个品牌好?值得选购的开放式蓝牙耳机推荐

2024年&#xff0c;蓝牙耳机市场迎来了开放式耳机的热潮。但其实对于许多消费者来说&#xff0c;如何选择合适的开放式耳机仍然充满疑问&#xff1a;佩戴稳固舒适的开放式耳机应该怎么选择&#xff1f;开放式耳机的蓝牙版本该怎么选择&#xff1f;又有哪些开放式耳机品牌是可靠…