AVL树模拟

1.概念

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

        1.它的左右子树都是AVL树

        2.左右子树的高度差的绝对值不超过1

如果一个二叉搜索树是高度平衡的,它就是AVL树。如果它有n个节点,其高度可保持在log N,搜索时间复杂度为O(log N);

2.节点定义

template<class T>
struct AVLTreeNode
{
	AVLTreeNode(T key)
		:_bf(0), _key(key)
	{}

	AVLTreeNode* _left = nullptr;
	AVLTreeNode* _right = nullptr;
	AVLTreeNode* _parent = nullptr;
	int _bf;					//平衡因子
	T _key;
};

3.AVL插入

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

AVL树的插入过程可以分为两部分:

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

        2.调整平衡因子

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

 

bool Insert(T key)
{
	Node* newnode = new Node(key);
	if (_root == nullptr)
	{
		_root = newnode;
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	//寻找插入位置
	while (cur)
	{
		if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else return false;
	}
	//插入新的节点
	newnode->_parent = parent;
	if (key < parent->_key) parent->_left = newnode;
	else parent->_right = newnode;
}

3.2调整平衡因子

新节点插入后,AVL树的平衡性可能遭到破坏,因此就需要更新平衡因子,并检测是否破坏了AVL树的平衡性

当newnode插入后,parent的平衡因子一点需要调整,在插入之前parent的平衡因子分为三种情况:-1,0,1。

调整方式分为以下两种:

        当新节点插入到parent左侧时,parent平衡因子-1;

        当新节点插入到parent右侧时,parent平衡因子+1;

此时parent的平衡因子有以下三种情况:0,正负1,正负2

        1.如果此时的平衡因子为0,说明插入新的节点后parent平衡了,满足AVL树的性质,无需继续向上调整。

        2.如果此时平衡因子为正负1,说明插入新的节点后parent为根的树高度增加,需要继续向上调整。

        3.如果此时平衡因子为正负2,说明parent违反了AVL树的性质,需要对其经行旋转处理。

cur = newnode;
while (parent)
{
    //更新平衡因子
	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)
		{
            //...
		}
		else
		{
            //...
		}
		break;
	}
	else
	{
		cout << "error: _bf";
		break;
	}
}

4. AVL树的旋转

 上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左 子树增加了一层,导致以60为根的二叉树不平衡。

要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,  即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点 的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:  1. 30节点的右孩子可能存在,也可能不存在  2. 60可能是根节点,也可能是子树如果是根节点,旋转完成后,要更新根节点如果是子树,可能是某个节点的左子树,也可能是右子树

a.右单旋


void _RotateR(Node* pParent)
{
	// pSubL: pParent的左孩子
	// pSubLR: pParent左孩子的右孩子,注意:该
	PNode* pSubL = pParent->_pLeft;
	PNode* pSubLR = pSubL->_pRight;

	// 旋转完成之后,30的右孩子作为双亲的左孩子
	pParent->_pLeft = pSubLR;

	// 如果30的左孩子的右孩子存在,更新亲双亲
	if (pSubLR) pSubLR->_pParent = pParent;
	// 60 作为 30的右孩子


	// 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
	PNode pPParent = pParent->_pParent;

	// 更新60的双亲
	pParent->_pParent = pSubL;

	// 更新30的双亲
	pSubL->_pParent = pPParent;

	// 如果60是根节点,根新指向根节点的指针
	if (NULL == pPParent)
	{
		_pRoot = pSubL;
		pSubL->_pParent = NULL;
	}
	else
	{
		// 如果60是子树,可能是其双亲的左子树,也可能是右子树
		if (pPParent->_pLeft == pParent)
			pPParent->_pLeft = pSubL;
		else
			pPParent->_pRight = pSubL;
	}

	// 根据调整后的结构更新部分节点的平衡因子
	pParent->_bf = pSubL->_bf = 0;
}

b.左单旋 

 

具体细节与右单旋一致。

void _RotateL(Node* pParent)
{
	Node* RSub = pParent->_right;
	Node* RSubL = RSub->_left;
	Node* pPParent = pParent->_parent;


	if (RSubL) RSubL->_parent = pParent;
	pParent->_right = RSubL;

	RSub->_left = pParent;
	pParent->_parent = RSub;
	RSub->_parent = pPParent;

	if (pParent == _root) _root = RSub;
	else
	{
		if (pPParent->_left == pParent) pPParent->_left = RSub;
		else pPParent->_right = RSub;
	}
	//更新平衡因子
	RSub->_bf = pParent->_bf = 0;
}

c.先左旋在右旋

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再 考虑平衡因子的更新。

 

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进
//行调整
void _RotateLR(PNode pParent)
{
	PNode pSubL = pParent->_pLeft;
	PNode pSubLR = pSubL->_pRight;

	// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
	//点的平衡因子
		int bf = pSubLR->_bf;

	// 先对30进行左单旋
	_RotateL(pParent->_pLeft);

	// 再对90进行右单旋
	_RotateR(pParent);
	if (1 == bf)
		pSubL->_bf = -1;
	else if (-1 == bf)
		pParent->_bf = 1;
}

d. 先右旋在左旋

void _RotateRL(Node* pParent)
{
	Node* RSub = pParent->_right;
	Node* RSubL = RSub->_left;
	int bf = RSubL->_bf;

	_RotateR(RSub);
	_RotateL(pParent);
	RSub->_bf = 0;
	if (bf == 1)
	{
		RSub->_bf = 0;
		pParent->_bf = -1;
	}
	else if (bf == -1)
	{
		pParent->_bf = 0;
		RSub->_bf = 1;
	}
	else
	{
		RSub->_bf = pParent->_bf = 0;
	}
}

总结:

加入以parent为根的子树不平衡,即以parent的平衡因子为2或-2,分别考虑以下情况

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

                当RSub的平衡因子为1时,执行左单旋,

                当RSub的平衡因子为-1时,执行左右双旋。

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

                当LSub的平衡因子为-1时,执行右单旋。

                当LSub的平衡因子为1时,执行做单旋。

当旋转接完成后,parent为根的子树高度已经降低,以及平衡,无需向上更新。

5.AVL树的验证

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

        1.验证其为二叉搜索树

                如果中序遍历结果为有序,则为二叉搜索树

        2.验证其为平衡树

                1.每个子树高度差的绝对值不超过1

                2.节点平衡因子正确

void InOrder()
{
	_InOrder(_root);
}

int GETHeight()
{
	return _GETHeight(_root);
}

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

bool _isBalance(Node* root)
{
	if (root->_bf >= 2 || root->_bf <= -1) return false;
	int HeightLeft = _GETHeight(root->_left);
	int HeightRight = _GETHeight(root->_right);
	if (abs(HeightRight - HeightLeft) > 1) return false;
	return _isBalance(root->_left) && _isBalance(root->_right);
}
int _GETHeight(Node* root)
{
	if (root == nullptr) return 0;
	return max(_GETHeight(root->_left), _GETHeight(root->_right)) + 1;
}
void _InOrder(Node* root)
{
	if (root == nullptr) return;
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

测试代码 

void test_AVL01()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int> t1;
	for (auto e : a)
	{

		t1.Insert(e);

		//cout << "Insert:" << e << "->" << t1.IsBalance() << endl;
	}
	cout << t1.GETHeight() << endl;
	t1.InOrder();

	//cout << t1.IsBalance() << endl;
}

6.AVL树模拟代码

#pragma once

template<class T>
struct AVLTreeNode
{
	AVLTreeNode(T key)
		:_bf(0), _key(key)
	{}

	AVLTreeNode* _left = nullptr;
	AVLTreeNode* _right = nullptr;
	AVLTreeNode* _parent = nullptr;
	int _bf;					//平衡因子
	T _key;
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	bool Insert(T key)
	{
		Node* newnode = new Node(key);
		if (_root == nullptr)
		{
			_root = newnode;
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		//寻找插入位置
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else return false;
		}
		//插入新的节点
		newnode->_parent = parent;
		if (key < parent->_key) parent->_left = newnode;
		else parent->_right = newnode;

		//更新平衡因子
		//插入后AVL树平衡,无需调整
		//插入后AVL树高度增加,继续向上调整
		cur = newnode;
		while (parent)
		{
			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 (parent->_right->_bf == 1)
					{
						_RotateL(parent);
					}
					else
					{
						_RotateRL(parent);
					}
				}
				else
				{
					//右单旋
					if (parent->_left->_bf == -1)
					{
						_RotateR(parent);
					}
					else
					{
						_RotateLR(parent);
					}
				}
				break;
			}
			else
			{
				cout << "error: _bf";
				break;
			}
		}
		return true;
	}

	Node* Find(const T& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key) cur = cur->_right;
			else if (key < cur->_key) cur = cur->_left;
			else return cur;
		}
		return nullptr;
	}
	size_t Size()
	{
		return _Size(_root);
	}
	void InOrder()
	{
		_InOrder(_root);
	}

	int GETHeight()
	{
		return _GETHeight(_root);
	}

	bool IsBalance()
	{
		return _isBalance(_root);
	}	
private:
	
	
	bool _isBalance(Node* root)
	{
		if (root->_bf >= 2 || root->_bf <= -1) return false;
		int HeightLeft = _GETHeight(root->_left);
		int HeightRight = _GETHeight(root->_right);
		if (abs(HeightRight - HeightLeft) > 1) return false;
		return _isBalance(root->_left) && _isBalance(root->_right);
	}
	int _GETHeight(Node* root)
	{
		if (root == nullptr) return 0;
		return max(_GETHeight(root->_left), _GETHeight(root->_right)) + 1;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr) return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	size_t _Size(Node* root)
	{
		if (root == nullptr) return 0;
		size_t SizeLeft = _Size(root->_left);
		size_t SizeRight = _Size(root->_right);
		return SizeLeft + SizeRight + 1;
	}
	//右旋
	void _RotateR(Node* pParent)
	{
		Node* LSub = pParent->_left;
		Node* LSubR = LSub->_right;
		Node* pPParent = pParent->_parent;


		if(LSubR)LSubR->_parent = pParent;
		pParent->_left = LSubR;

		LSub->_right = pParent;
		pParent->_parent = LSub;
		LSub->_parent = pPParent;

		if (pParent == _root) _root = LSub;
		else
		{
			if (pPParent->_left == pParent) pPParent->_left = LSub;
			else pPParent->_right = LSub;
		}
		//更新平衡因子
		LSub->_bf = pParent->_bf = 0;
	}

	//左旋
	void _RotateL(Node* pParent)
	{
		Node* RSub = pParent->_right;
		Node* RSubL = RSub->_left;
		Node* pPParent = pParent->_parent;


		if (RSubL) RSubL->_parent = pParent;
		pParent->_right = RSubL;

		RSub->_left = pParent;
		pParent->_parent = RSub;
		RSub->_parent = pPParent;

		if (pParent == _root) _root = RSub;
		else
		{
			if (pPParent->_left == pParent) pPParent->_left = RSub;
			else pPParent->_right = RSub;
		}
		//更新平衡因子
		RSub->_bf = pParent->_bf = 0;
	}
	void _RotateRL(Node* pParent)
	{
		Node* RSub = pParent->_right;
		Node* RSubL = RSub->_left;
		int bf = RSubL->_bf;

		_RotateR(RSub);
		_RotateL(pParent);
		RSub->_bf = 0;
		if (bf == 1)
		{
			RSub->_bf = 0;
			pParent->_bf = -1;
		}
		else if (bf == -1)
		{
			pParent->_bf = 0;
			RSub->_bf = 1;
		}
		else
		{
			RSub->_bf = pParent->_bf = 0;
		}
	}

	void _RotateLR(Node* pParent)
	{
		Node* LSub = pParent->_left;
		Node* LSubR = LSub->_right;
		int bf = LSubR->_bf;

		_RotateL(LSub);
		_RotateR(pParent);

		LSub->_bf = 0;
		if (bf == 0)
		{
			LSubR->_bf = LSubR->_bf = 0;
		}
		else if (bf == 1)
		{
			LSub->_bf = -1;
			pParent->_bf = 0;
		}
		else if (bf == -1)
		{
			LSub->_bf = 0;
			pParent->_bf = 1;
		}

	}

private:
	Node* _root = nullptr;
};


void test_AVL01()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int> t1;
	for (auto e : a)
	{
		int i = 1;


		t1.Insert(e);

		//cout << "Insert:" << e << "->" << t1.IsBalance() << endl;
	}
	cout << t1.GETHeight() << endl;
	t1.InOrder();

	//cout << t1.IsBalance() << endl;
}

void test_AVL02()
{
	const int N = 10000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
		//cout << v.back() << endl;
	}

	size_t begin2 = clock();
	AVLTree<int> t;
	for (auto e : v)
	{
		t.Insert(e);
		//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}
	size_t end2 = clock();

	cout << "Insert:" << end2 - begin2 << endl;
	//cout << t.IsBalance() << endl;

	cout << "Height:" << t.GETHeight() << endl;
	cout << "Size:" << t.Size() << endl;

	size_t begin1 = clock();
	// 确定在的值
	for (auto e : v)
	{
		t.Find(e);
	}

	// 随机值
	/*for (size_t i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}*/

	size_t end1 = clock();

	cout << "Find:" << end1 - begin1 << endl;
}



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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++特殊类设计单例模式...

文章目录 请设计一个类&#xff0c;不能被拷贝请设计一个类&#xff0c;只能在堆上创建对象请设计一个类&#xff0c;只能在栈上创建对象请设计一个类&#xff0c;不能被继承请设计一个类&#xff0c;只能创建一个对象(单例模式)单例模式&#xff1a;饿汉模式&#xff1a;懒汉模…

steam社区载入失败、加载不出来、打不开?

随着steam夏季大促的到来&#xff0c;最近steam在线用户越来越多了&#xff0c;很多玩家在自己喜欢的游戏社区里看最新的玩法、攻略和玩家的游戏心得。不过有不少玩家表示有时候会打不开游戏社区或是社区加载失败等问题。根据大家遇到的问题&#xff0c;这里总结了几种解决方法…

Mongodb安装与配置

Mongodb的下载 这里下载的是MongoDB 7.0.11版本的 首先进入官网&#xff1a;https://www.mongodb.com/ 点击完上面两步后&#xff0c;加载来到该页面&#xff0c;选择自己的版本、系统&#xff0c;是压缩包(zip)还是安装包(msi)。 下载好之后能&#xff0c;来到安装包哪里&a…

程序员福利-一种高效的治疗颈椎病的方法

我从18年开始出现颈椎病&#xff0c;只要在电脑前低头工作两个小时&#xff0c;颈部就会不舒服&#xff0c;脖子的肌肉酸痛无力、僵硬麻木&#xff0c;影响血液循环系统&#xff0c;大脑供血不足&#xff0c;导致心烦意乱&#xff0c;注意力无法集中&#xff0c;还会影响消化系…

在HBuilder X中ElementUI框架的搭建

前言 本文将详解基于Vue-cli脚手架搭建的项目如何使用ElementUI &#xff1f;所以在学习本篇文章内容之前建议先学习vue-cli脚手架项目的搭建和学习 使用HbuilderX快速搭建vue-cil项目https://mp.csdn.net/mp_blog/creation/editor/140043776 ElementUI框架: Element&#xff…

【C++】类、静态、枚举、重载、多态、继承、重写、虚函数

五、类 面向对象编程是一个巨大的编程范式。C中的类class就是基于对象的程序设计。 我们可以用类来定义一个新的类型&#xff0c;这些新类型就可以像内置类型一样使用。 内置类型颗粒度太太小&#xff0c;现实需求又非常复杂&#xff0c;这就需要我们把内置类型适度的进行拼搭…

微软推出集成GPT-4o的文本转语音虚拟数字人服务

微软近日宣布&#xff0c;其全新的文本转语音虚拟数字人服务正式上线&#xff0c;并集成了GPT-4o技术。这一服务为用户提供了创建实时互动数字人的可能。通过先进的自然语言处理技术&#xff0c;数字人能够将文本转化为自然流畅的语音&#xff0c;并配以生动的虚拟形象&#xf…

C++【函数重载】【附有C语言为何不能实现函数重载的讲解】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 博主主页&#xff1a;LiUEEEEE                        …

【硬件视界2】什么是CPU和GPU?有什么区别?

名人说&#xff1a;莫听穿林打叶声&#xff0c;何妨吟啸且徐行。—— 苏轼《定风波莫听穿林打叶声》 本篇笔记整理&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 1、CPU (中央处理器)①主要作用②特点 2、 GPU (图形处理…

支持纳管达梦数据库,命令存储支持对接Elasticsearch 8,JumpServer堡垒机v3.10.11 LTS版本发布

2024年6月24日&#xff0c;JumpServer开源堡垒机正式发布v3.10.11 LTS版本。JumpServer开源项目组将对v3.10 LTS版本提供长期的支持和优化&#xff0c;并定期迭代发布小版本。欢迎广大社区用户升级至v3.10 LTS最新版本&#xff0c;以获得更佳的使用体验。 在JumpServer v3.10.…

50-2 内网信息收集 - 内网工作环境(域相关知识)

一、工作组 工作组(Work Group)是局域网中最基本的资源管理模式,适用于小规模网络环境。 工作组的定义: 工作组是将不同功能或部门的计算机分组管理的方式。它提供了层次化的网络资源管理,使得组织内的计算机可以按照功能或部门分类。每个工作组有一个自定义的主机名称,…

短视频矩阵系统搭建APP源码开发

前言 短视频矩阵系统不仅有助于提升品牌影响力和营销效率&#xff0c;还能帮助企业更精准地触达目标受众&#xff0c;增强用户互动&#xff0c;并利用数据分析来持续优化营销策略。 一、短视频矩阵系统是什么&#xff1f; 短视频矩阵系统是一种通过多个短视频平台进行内容创作…

使用supportFragmentManager管理多个fragment切换

android studio创建的项目就没有一个简单点的框架&#xff0c;生成的代码都是繁琐而复杂&#xff0c;并且不实用。 国内的页面一般都是TAB页面的比较多&#xff0c;老外更喜欢侧边菜单。 如果我们使用一个activity来创建程序&#xff0c;来用占位符管理多个fragment切换&…

广东省钟表行业协会第十二届会员大会暨2024年钟表行业发展交流会

6月25日广东省钟表行业协会第十二届会员大会暨2024年钟表行业发展交流会在广州万富希尔顿酒店隆重召开。大会选举沙胜昔为广东省钟表行业协会第十二届理事会会长。 领导发言 新任会长 沙胜昔 首席荣誉会长 吴伟阳 新老会长交接仪式 本次大会&#xff0c;全国钟表大伽齐参与…

特斯拉下一代自动驾驶芯片的深度预测

引言 特斯拉一直以来都在自动驾驶技术上不断突破&#xff0c;随着AI大模型技术的爆发&#xff0c;其下一代自动驾驶芯片&#xff08;HW5.0&#xff09;也备受瞩目。本文将深入分析和预测特斯拉下一代自动驾驶芯片AI5的技术特点及其对行业的影响。 深入技术分析 现有自动驾驶…

Java实现ATM系统

效果: 目录结构 Account 账户类 package com.mytest;public class Account {private String cardId;private String userName;private char sex;private String password;private double balance;private double limit; //限额public String getCardId() {return cardId;}publ…

imx6ull/linux应用编程学习(5)FrameBuffer的应用编程

什么是FrameBuffer&#xff1f; Frame 是帧的意思&#xff0c; buffer 是缓冲的意思&#xff0c;所以 Framebuffer 就是帧缓冲&#xff0c; 这意味着 Framebuffer 就是一块内存&#xff0c;里面保存着一帧图像。帧缓冲&#xff08;framebuffer&#xff09;是 Linux 系统中的一种…

存储请求地址但是使用时请求的是端口

baseURL默认全局加载一次&#xff0c;后续直接读取缓存 解决方案&#xff1a;

Ubuntu qemu虚拟机 NAT网络 第一次使用,VNC访问

比如Windows 7 虚拟机 要手工设置网络

AI大模型到底有没有智能?一篇文章给你讲明明白白

生成式人工智能 (GenAI[1] ) 和大语言模型 (LLM [2] )&#xff0c;这两个词汇想必已在大家的耳边萦绕多时。它们如惊涛骇浪般席卷了整个科技界&#xff0c;登上了各大新闻头条。ChatGPT&#xff0c;这个神奇的对话助手&#xff0c;也许已成为你形影不离的良师益友。 然而&…