冰冰学习笔记:二叉搜索树

news2025/2/23 13:17:32

欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:《二叉树的功能函数与OJ练习题》

冰冰学习笔记:《多态》


目录

系列文章推荐

前言

1.二叉搜索树的概念

2.二叉搜索树的实现

2.1二叉搜索树的插入

2.3二叉搜索树的查找

2.3二叉搜索树的删除

2.4其他函数

2.5二叉搜索树的性能

3.二叉搜索树的应用

3.1K模型

3.2K-V模型


前言

        二叉搜索树是二叉树的一种实际应用,二叉树的特性和功能函数在前面的数据结构章节中进行了详细的介绍,忘记的可以自行前去复习查看。想要理解map和set这两个容器,就必须了解二叉搜索树。

1.二叉搜索树的概念

        什么是二叉搜索树呢?二叉搜索树又称二叉排序树,它或者是一颗空树,或者是一颗具备以下条件的二叉树。

(1)若左子树不为空,则左子树上所有的节点的值都小于根节点的值

(2)若右子树不为空,则右子树上所有的节点的值都大于根节点的值

(3)左右子树也满足前两个条件,偶分别为二叉搜索树。

2.二叉搜索树的实现

        二叉搜索树也是二叉树,二叉树的功能函数其都应该具备。但是二叉搜索树在一些函数上具备不同的性质。

        二叉搜索树和二叉树具备相同的节点结构:

    template<class K>
	struct TreeNode
	{
		TreeNode<K>* _left;
		TreeNode<K>* _right;
		K _key;
		TreeNode(const K& data)
			:_left(nullptr), _right(nullptr), _key(data)
		{}
	};

2.1二叉搜索树的插入

        在学习二叉树的插入操作时我们只是将新节点直接链接即可,但是二叉搜索树需要满足条件才能链接插入。二叉搜索树中不允许存在相同的节点,因此二叉搜索树的插入操作是一个bool类型的返回函数。当插入成功返回true,失败返回false。

(1)非递归插入

        调用插入函数后,我们需要先判断插入节点是否为第一次插入,如果为第一次插入,那么新节点就是的根节点。如果不是根节点,那我们就需要找到新插入节点的位置,然后进行链接。

        如何找到根节点的位置呢?这就需要利用二叉搜索树的特性进行查找。新插入节点的值比根节点大,我们需要向根节点的右子树查询,比根节点小就需要向左子树进行查询。当遇到与插入节点的值相同的节点时,跳出查找并返回false,插入失败。当查找到nullptr指针时,说明该位置极为新插入节点的位置。

        这里需要注意一点,我们在链接新节点的时候需要有指向父亲节点的指针才能链接,否则无法链接。 因此我们在每次查找之后都需要记录父亲节点,然后再更新查找节点。

		bool Insert(const K& val)
		{
			if (_root == nullptr)//第一次插入
			{
				_root = new Node(val);
				return true;
			}
			else
			{
				Node* cur = _root;
				Node* parent = _root;
				while (cur)
				{
					if (cur->_key < val)
					{
						parent = cur;
						cur = cur->_right;
					}
					else if (cur->_key > val)
					{
						parent = cur;
						cur = cur->_left;
					}
					else
					{
						return false;
					}
				}
				cur = new Node(val);
				if (parent->_key > val)
					parent->_left = cur;
				else
					parent->_right = cur;
			}
			return true;
		}

(2)递归插入

        我们知道二叉树天然适应递归操作,因此二叉搜索树也可以使用递归来进行插入操作。

        这里我们需要注意,递归调用的时候我们的函数需要显示的接受root节点,但是在外面调用的时候我们又不能显示的传递_root,因此我们需要封装一层函数。在类里面就可以使用_root。

        递归写法与之前循环的写法类似。当我们遇到空节点,意味着找到了正确的插入位置,此时我们需要将新节点链接到原有的结构中。如果没有遇到空节点我们就需要对节点的_key和新插入的val进行比对,val>_key则需要递归到根节点的右子树中进行插入,反之则去左子树进行插入操作。当遇到相同的节点的时候就直接层层返回false。

        但是我们要注意,如果我们递归使用的root是传值调用,那么在进行节点链接时并不会影响上一层调用的结果,因为这一层的root节点是仅属于此次调用的局部变量,即便链接到root的后面也不会影响到全局的结构。

        如果我们采用的是引用传参,在最后一层调用时,当前的root还是上一层子树节点的别名,链接将会直接连接到上一层子树的后面,从而完成树的结构链接。引用传参在前面的递归中并没有起到实际作用,但是在最后进行连接的时候才真正做到了点睛之笔。

bool InsertR(const K& val)
{
	return _InsertR(_root, val);
}
bool _InsertR(Node*& root, const K& val)
{
	if (root == nullptr)
	{
		root = new Node(val);
		return true;
	}
	if (root->_key < val)
	{
		return _InsertR(root->_right, val);
	}
	else if (root->_key > val)
	{
		return _InsertR(root->_left, val);
	}
	else
		return false;
}

2.3二叉搜索树的查找

        在有了插入函数的基础后,查找函数就比较简单,我们只需要沿用插入的思路进行改进即可。

		bool Find(const K& val)
		{
			if (_root == nullptr)
			{
				return false;
			}
			else
			{
				Node* cur = _root;
				while (cur)
				{
					if (cur->_key < val)
					{
						cur = cur->_right;
					}
					else if (cur->_key > val)
					{
						cur = cur->_left;
					}
					else
						return true;
				}
			}
			return false;
		}
        bool FindR(const K& val)
		{
			return _FindR(_root, val);
		}
        bool _FindR(Node* root, const K& val)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < val)
			{
				return _FindR(root->_right, val);
			}
			else if (root->_key > val)
			{
				return _FindR(root->_left, val);
			}
			else
				return true;
		}

2.3二叉搜索树的删除

        二叉树的删除需要考虑多种情况。

(1)删除节点没有孩子,可以直接删除。

(2)删除节点有一个孩子,需要将孩子托管之后在进行删除。

(3)删除节点左右孩子都有,需要替换节点,将替换后的节点进行删除。

        在删除节点之前我们还是需要先找到该节点,如果没有删除的节点,那么就返回false。在找到节点后,我们需要考虑一些特殊情况,例如当前节点是否为根节点,如果为根节点我们需要找到替换根的新节点来充当树的根。

        仔细分析会发现,我们在删除叶子节点的时候,删除后也需要对删除节点的父亲节点的指针进行置空处理避免出现野指针问题,所以我们将父节点原先指向删除节点的指针指向删除节点的孩子就可以完成置空处理,此时就将此种删除转化为删除节点只有一个孩子的情况了。

 

情况一:待删除节点的左子树为空

        如果此时删除的节点为根节点,那么我们只需要将根节点更新为该节点的右子树即可,然后将此节点进行释放。

        如果待删除节点并不是根节点,我们需要将待删除节点的右子树托管给删除节点的父亲节点,此时就有两种情况,如果待删除节点是父节点的左子树,那么删除节点的右子树就需要托管给父节点的左子树,反之就是托管给父节点的右子树。这就需要我们在前面进行查找的时候将父节点进行保存。

情况二:待删除节点右子树为空

        此种情况与上面的情况正好相反,如果待删除节点是根节点,那么我们就让左子树做新的根节点。如果待删除节点不是根节点,那么我们就需要将待删除节点的左子树托管给待删除节点的父亲节点,也需要区分两种情况。

情况三:删除节点左右子树都不为空

        此种情况最为复杂,我们的做法是找到一个叶子或者只含有一个孩子的节点来替换当前需要删除的节点,替换完毕后,转换成了将替换后的位置进行删除,此时就是上面两种情况的删除。

        那么什么样的节点满足替换条件呢?替换后,新节点同样需要满足左子树小于根节点,右子树大于根节点。通过画图分析,我们发现,左子树中最大的节点和右子树中最小的节点都满足替换条件,因此我们任选一个就可以。

        左子树的最大节点一定是最右边的节点,右子树的最小节点一定是最左边的节点,但是这两个节点都有可能具备子树。因此我们再替换后,需要将子树连接在删除节点的父节点上,然后再将节点删除。这就需要我们在寻找替换节点的时候也需要将替换节点的父亲节点进行保存。

        这里我们以找右子树中最小的节点来做替换节点,因此我们需要两个节点指针,一个为rightmin指向右边的最小节点,一个为minpraent指向最小节点的父亲节点。在找到后我们将rightmin指向的数据与删除节点替换,然后判断minparent的左边指向的是rightmin还是右边指向的是rightmin,将rightmin的孩子节点托给minparent节点。

//替换法删除代码
Node* minParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
	minParent = rightMin;
	rightMin = rightMin->_left;
}
	cur->_key = rightMin->_key;
	//判断极端情况,删除根节点
	if (minParent->_left == rightMin)
	    minParent->_left = rightMin->_right;
    else
		minParent->_right = rightMin->_right;
	delete rightMin;

        这里难免会有些人出现疑问,既然都知道4一定是右子树中最左边的节点,那么替换后,4一定在他的父节点的左子树中,我们直接将4的右孩子连接在父亲节点的左侧不行吗?为什么还需要保存父亲节点的指针判断是在左边还是右边呢?常规情况下确实是这样,但是不排除我们遇到下面情况的删除。

        当我们想删除根节点8时,发现左右子树都不为空,此时我们需要去右边子树中找到最小的节点,经过循环我们找到了10这个节点,我们需要将10与8进行替换,此时我们发现rightmin指向8,minparent指向10,此时如果我们简单的将rightmin的孩子连接到minparent的左边会出现错误。因为10的左边有子树,并且原本rightmin不再是minparent的左孩子而是右孩子。

在上面的情况都考虑后我们的代码就实现成下面的状况:

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			// 开始删除
			// 1、左为空
			// 2、右为空
			// 3、左右都不为空
			if (cur->_left == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;
				cur = nullptr;
			}
			else if (cur->_right == nullptr)
			{
				if (_root == cur)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}

				delete cur;
				cur = nullptr;
			}
			else
			{
				// 替换法删除 -- 找右边的最小值
				Node* minParent = cur;
				Node* rightMin = cur->_right;
				while (rightMin->_left)
				{
					minParent = rightMin;
					rightMin = rightMin->_left;
				}
				cur->_key = rightMin->_key;
				//判断极端情况,删除根节点
				if (minParent->_left == rightMin)
					minParent->_left = rightMin->_right;
				else
					minParent->_right = rightMin->_right;
				delete rightMin;
			}
		}
	}
	return false;

        递归形式的删除代码和上面的逻辑基本一致,只不过查找方式不是循环而是递归查找,找到后进行删除的时候如果是左右子树都不为空的时候我们就直接利用替换删除将两个节点替换,然后将删除问题转化为递归删除替换后的子树中的删除节点。删除时,节点的传参依旧时引用传参。

bool EraseR(const K& val)
{
	return _EraseR(_root, val);
}
bool _EraseR(Node*& root, const K& val)
{
	if (root == nullptr)
	{
		return false;
	}
	if (root->_key < val)
	{
		return _EraseR(root->_right, val);
	}
	else if (root->_key > val)
	{
		return _EraseR(root->_left, val);
	}
	else
	{
		//找到了,开始删除
		Node* del = root;
		if (root->_left == nullptr)
			root = root->_right;
		else if (root->_right == nullptr)
			root = root->_left;
		else
		{
			//左右都不为空
			Node* min = root->_right;
			while (min->_left)
			{
				min = min->_left;
			}
			swap(min->_key, root->_key);
			_EraseR(root->_right, val);
		}
	}
	return true;
}

2.4其他函数

        二叉搜索树的中序遍历也是一个常被的函数,由于二叉树的性质吗,在进行中序调用后我们会得到一组有序且去重的递增数列。因为二叉树永远满足左子树小于根节点小于右子树。

        另外二叉搜索树的拷贝构造和赋值重载都需要完成深拷贝,因此我们的拷贝和赋值需要一个节点一个节点的进行拷贝构造。在析构函数中我们还需要将节点一一释放,因此我们采用后续遍历的方式进行析构,我们先析构左子树,在析构右子树,最后析构根节点。

        在完成拷贝构造后,我们还需要自己实现默认构造函数,当然我们也可以使用C++11的语法,强制编译器生成默认的构造函数。

void InOrder()//中序遍历
{
	_InOrder(_root);
	cout << endl;
}
BSTree() = default;//强迫生成默认构造函数C++11
BSTree<K>& operator=(BSTree<K> t)//赋值重载
{
	swap(_root, t._root);
	return *this;
}
BSTree(const BSTree<K>& t)//拷贝构造
{
	_root = _copy(t._root);
}
~BSTree()//析构
{
	_Destory(_root);
}
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}
Node* _copy(Node* root)
{
	if (root == nullptr)
		return nullptr;
	Node* CopyRoot = new Node(root->_key);
	CopyRoot->_left = _copy(root->_left);
	CopyRoot->_right = _copy(root->_right);
	return CopyRoot;

}
void _Destory(Node*& root)
{
	if (root == nullptr)
	{
		return;
	}
	_Destory(root->_left);
	_Destory(root->_right);
	delete root;
	root = nullptr;
}

2.5二叉搜索树的性能

        我们发现在对二叉搜索树进行操作时都需要先进行搜索,因此搜索的性能就可以等效为搜索树的性能。理想情况下二叉搜索树的效率可以达到O(logN)的性能,当然这是在这颗二叉树是完全或近似完全二叉树的情况下。但是当我们插入一组有序数据时,二叉搜索树的查找性能就会降低到O(N),二叉树将会退化为单枝树。插入和删除只会在一边进行操作。

        基于这种情况,大佬们又提出了AVL树和红黑树。这两个结构将通过某种特殊的机制维持二叉树的平衡,使得搜索二叉树近似一种完全二叉树,这样在进行插入,删除,查找的操作就会基本维持在O(logN)左右。

3.二叉搜索树的应用

3.1K模型

        K模型就是二叉搜索树中存储的只有一个关键字,set就是K模型的一种应用。我们使用这种模型可以判断某个数据存不存在,每次通过给定的key进行搜索,找得到就是存在,找不到就不存在。

3.2K-V模型

        K-V模型就是通过key来寻找val,每个key都对应一个val,map就是K-V的应用。这种模型我们可以记录某种数据出现的次数,数据类型作为key独立存在,次数作为val进行计算,首次出现则将key插入,再次出现只需要更改key对应的val的数值即可。        

        下面是对K-V模型的简单实现,与K类似,只不过多了一个参数V。

namespace KEY_VAL
{
	template<class K,class V>
	struct TreeNode
	{
		TreeNode<K,V>* _left;
		TreeNode<K,V>* _right;
		K _key;
		V _val;
		TreeNode(const K& key,const V& val)
			:_left(nullptr), _right(nullptr),_key(key), _val(val)
		{}
	};
	template<class K,class V>
	class BSTree_KV
	{
		typedef TreeNode<K,V>  Node;
	public:
		//BSTree_KV() {}
		BSTree_KV() = default;//强迫生成默认构造函数C++11
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
		//
		//递归
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		bool InsertR(const K& key,const V& val)
		{
			return _InsertR(_root, key,val);
		}
		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}
		~BSTree_KV()
		{
			_Destory(_root);
		}
		BSTree_KV<K,V>& operator=(BSTree_KV<K,V> t)
		{
			swap(_root, t._root);
			return *this;
		}
		BSTree_KV(const BSTree_KV<K,V>& t)
		{
			_root = _copy(t._root);
		}
	private:
		Node* _copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			Node* CopyRoot = new Node(root->_key,root->_val);
			CopyRoot->_left = _copy(root->_left);
			CopyRoot->_right = _copy(root->_right);
			return CopyRoot;

		}
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				//找到了,开始删除
				Node* del = root;
				if (root->_left == nullptr)
					root = root->_right;
				else if (root->_right == nullptr)
					root = root->_left;
				else
				{
					//左右都不为空
					Node* min = root->_right;
					while (min->_left)
					{
						min = min->_left;
					}
					swap(min->_key, root->_key);
					swap(min->_val, root->_val);
					_EraseR(root->_right, key);
				}
			}
			return true;
		}
		bool _InsertR(Node*& root, const K& key,const V& val)
		{
			if (root == nullptr)
			{
				root = new Node(key,val);
				return true;
			}
			if (root->_key < key)
			{
				return _InsertR(root->_right, key,val);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key,val);
			}
			else
				return false;
		}

		void _Destory(Node*& root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Destory(root->_left);
			_Destory(root->_right);
			delete root;
			root = nullptr;
		}
		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
				return root;
		}
		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;
			_InOrder(root->_left);
			cout << root->_key << ":"<<root->_val<<" ";
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};
}

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

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

相关文章

堆外内存和堆内内存及虚引用的应用

目录 内存区域划分&#xff1a; 元空间 程序计数器 直接内存 对象的创建 对象的访问定位 判断对象是否存活 堆外内存 堆内内存的缺点以及引入堆外内存 为什么需要堆外内存&#xff1f; 如何分配堆外内存&#xff1f; 如何回收堆外内存&#xff1f; 1) System.gc()…

C语言函数章--第二弹(让冤种室友用你的函数,但不给他看函数源码)

前言 &#x1f496;作者&#xff1a;龟龟不断向前 ✨简介&#xff1a;宁愿做一只不停跑的慢乌龟&#xff0c;也不想当一只三分钟热度的兔子。 &#x1f47b;专栏&#xff1a;C初阶知识点 &#x1f47b;工具分享&#xff1a; 刷题&#xff1a; 牛客网 leetcode笔记软件&#xff…

Error注入攻击

&#x1f4aa;&#x1f4aa;Error注入攻击1.创建漏洞环境2.漏洞攻击2.1判断是否有注入2.2信息收集2.3注入获取数据库名2.4注入获取表名2.5注入获取列名2.6注入获取信息3.sql靶场实战1.创建漏洞环境 &#x1f4aa;&#x1f4aa;第一步创建sql环境&#xff0c;直接再mysql下运行 …

Flutter——软件安装与环境配置

Flutter入门官网Flutter SDK下载创建Flutter项目在ios上运行第一个Flutter项目效果图代码总结官网 Flutter开发手册网址如下 Flutter SDK下载 下载地址 第一步&#xff1a;进入官网&#xff0c;选择自己相对应的系统 第二步&#xff1a;选择对应版本SDK并下载到本地 创建Flu…

electron调用dll文件

Electron 对系统层能力的使用可能比较弱&#xff0c;此时需要求助 Python、C、C# 等语言&#xff0c;通过 ffi-napi 库可以让 Node.js 使用 C dll&#xff0c;通过 electron-edge-js 库可以让 Node.js 使用 C# dll 1. 先确定dll文件是用什么语言写的. 使用peid 应用查看- 这个…

【Transformers】第 2 章:主题的实践介绍

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

Node.js | 基于 MongoDB 的简易用户管理系统

&#x1f5a5;️ NodeJS专栏&#xff1a;Node.js从入门到精通 &#x1f5a5;️ 博主的前端之路&#xff08;源创征文一等奖作品&#xff09;&#xff1a;前端之行&#xff0c;任重道远&#xff08;来自大三学长的万字自述&#xff09; &#x1f5a5;️ TypeScript知识总结&…

C++秋招经验贴

文章目录一、个人背景及秋招情况1.个人背景2.秋招情况二、求职C强相关开发岗位的准备过程以及一些建议1. 八股2. 力扣刷题3. 实习4. 项目三、总结一、个人背景及秋招情况 1.个人背景 本科&#xff1a;二本&#xff0c;材料专业   硕士&#xff1a;211硕&#xff0c;光学工程…

TI IWR1642毫米波雷达使用串口原始数据采集与分析

本文编辑&#xff1a;调皮哥的小助理 1.引言 如果文章能够给你带来价值&#xff0c;希望能够关注我。 如果文章能够让你学习到知识&#xff0c;希望你能够点个赞&#xff01; 好了下面开始今天的学习内容吧。 今天给大家分享的是 《TI 的IWR1642毫米波雷达使用串口原始数据…

深度学习入门(十五)环境和分布偏移(了解)

深度学习入门&#xff08;十五&#xff09;环境和分布偏移前言环境和分布偏移教材1 分布偏移的类型1.1 协变量偏移1.2 标签偏移1.3 概念偏移2 分布偏移示例2.1医学诊断2.2 自动驾驶汽车2.3 非平稳分布2.4 更多轶事3 分布偏移纠正3.1 经验风险与实际风险3.2 协变量偏移纠正3.3 标…

MATLAB | 一起来感受数学之美叭

前两天去观摩了MATHWORKS官方举办的Mathematics is beautiful数学之美投票比赛&#xff0c;见到了很多非常惊艳的作品&#xff0c;在这里分享给大家让大家一同感受大神们的创造力&#xff0c;接下来由我来做全程解说。 虽然看起来代码都写好了&#xff0c;&#xff0c;&#x…

程序员眼中看到的网页是如何制作出来的?

一、认识网页 在学习之初&#xff0c;我们需要认识一下网页的概念&#xff0c;因为网页与我们的 html是息息相关的。 那么接下来我们来看一下&#xff0c;我们经常去通过浏览器查看的网页&#xff0c;它的本质是什么&#xff1f;在此我们需要去做一个对比。我们眼中看到的网页…

聚类算法概要及相关知识准备

聚类的概念 聚类分析是在数据中发现数据对象之间的关系&#xff0c;将数据进行分组&#xff0c;组内的相似性越大&#xff0c;组间的差别越大&#xff0c;则聚类效果越好。 将物理或抽象对象的集合分成由类似对象组成的多个类或簇&#xff08;cluster&#xff09;的过程被称为…

SpringBoot+Vue的社区疫情防控管理系统|基于Python+Django的社区物资采购系统

&#x1f496;&#x1f496;作者&#xff1a;IT跃迁谷毕设展 &#x1f499;&#x1f499;个人简介&#xff1a;曾长期从事计算机专业培训教学&#xff0c;本人也热爱上课教学&#xff0c;语言擅长Java、微信小程序、Python、Golang、安卓Android等。平常会做一些项目定制化开发…

Cookie使用详解

Cookie使用详解 目录Cookie使用详解理论知识前言创建Cookiecookie 的属性介绍name 、valuedomainpathExpires 、Max-AgeSameSiteSecure&#xff0c;HttpOnlyCookie与跨域、安全知识点小结实践相关配置修改代码实践实验过程记录其它小结理论知识 前言 HTTP Cookie&#xff08;…

使用Charles和iPhone进行微信小程序抓包详解

基于工作原因&#xff0c;需要对一款微信小程序进行测试。本次任务是纯黑盒方式&#xff0c;所以只有通过抓包的方式找到接口及参数列表&#xff0c;再逐一进行功能和性能测试。 一、使用工具 网络抓包工具&#xff1a;Charles 设备&#xff1a;iPhone6s&#xff0c;iPhone1…

数据分析 | Pandas 200道练习题,每日10道题,学完必成大神(8)

文章目录前期准备1. 将收盘价5日均线&#xff0c;20日均线与原始数据绘制在同一个图上2. 按周为采样规则&#xff0c;取一周收盘价的最大值3. 绘重制采样数据与原始数据4. 将数据往后移动5天、5. 将数据向前移动5天6. 使用expending函数计算开盘价的移动窗口的均值7. 绘制上一题…

牛客刷题系列(汽水瓶,跳台阶扩展问题,斐波那契凤尾)

牛客刷题系列一&#xff1a;汽水瓶题目链接常规写法简便写法二.跳台阶扩展问题三&#xff1a;斐波那契凤尾很多小伙伴为了刷题发愁 今天为大家推荐一款刷题神奇哦&#xff1a;刷题面试神器牛客 各大互联网大厂面试真题。从基础到入阶乃至原理刨析类面试题 应有尽有&#xff0c;…

云IDE介绍——CSDN开发云

云IDE产品介绍云IDE使用教程 免费使用地址&#xff1a;点击【云IDE】&#xff0c;即可开始创建工作空间啦~ 作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xf…

【学习笔记之数据结构】时间复杂度与空间复杂度

一、算法效率 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间&#xff08;内存&#xff09;资源。因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的&#xff0c;即时间复杂度和空间复杂度。   时间复杂度主要衡量一个算法的运行快…