红黑树模拟实现STL中的map与set

news2024/11/25 2:57:56

1.map

在C++标准模板库(STL)中,std::map是一种非常实用且强大的容器,它提供了键值对的存储机制。这使得std::map成为处理具有唯一关键的关联数据的理想选择。

1.1 map的特性

1、键值对存储:std::map通过键值对的形式存储数据,其中每个键都是唯一的,并且与一个值相关联。
2、自动排序:std::map内部使用一种平衡二叉搜索树(通常是红黑树)来存储元素,这使得元素根据键自动排序。
3、元素唯一性:在std::map中,键必须是唯一的。如果尝试插入一个已经存在的键,插入操作将失败。
4、直接访问:可以使用键直接访问std::map中的元素,这提供了高效的查找能力。
5、灵活的元素操作:std::map提供了丰富的元素操作,包括插入、删除、查找等。

1.2 map的性能

1、插入操作:插入操作的时间复杂度为O(log n),其中n是std::map中元素的数量。这是因为需要在平衡二叉树中找到合适的位置来插入新元素。
2、查找操作:查找操作的时间复杂度也是O(log n),由于std::map的有序性,可以快速定位到任何键。
3、删除操作:删除操作的时间复杂度同样为O(log n),需要找到要删除的元素并在保持树平衡的同时移除它。
4、遍历操作:遍历std::map的时间复杂度为O(n),因为需要访问容器中的每个元素。

1.3 C++标准库中map的基本用法

#include<iostream>
#include<map>
using namespace std;

int main()
{
	//创建一个map,键和值都是int类型
	map<int, int> myMap;

	//插入元素
	myMap.insert(make_pair(1, 100));
	myMap.insert({ 2,200 });
	myMap[3] = 300;//使用下标操作符直接插入或修改
	myMap.insert({ 4,400 });

	//访问元素
	cout << "Element with key 2:" << myMap[2] << endl;

	//迭代元素
	cout << "Iterating over elements:" << endl;
	for (const auto& pair : myMap)
	{
		cout << pair.first << "=>" << pair.second << endl;
	}

	//查找元素
	auto search = myMap.find(2);//查找键位2的元素
	if (search != myMap.end())
	{
		cout << "Found element with key 2:" << search->second << endl;
	}
	else
	{
		cout << "Element with key 2 was not found." << endl;
	}

	//删除元素
	myMap.erase(2);//删除键为2的元素
	cout << "Element with key 2 erased." << endl;

	//再次遍历,查看删除效果
	cout << "iterating over elements after deletion:" << endl;
	for (const auto& pair : myMap)
	{
		cout << pair.first << "=>" << pair.second << endl;
	}

	return 0;
}

1.4 map工作原理

std::map的内部实现通常基于红黑树,红黑树相关介绍可参考文章红黑树的实现,红黑树自身支持排序,且我们实现的红黑树支持插入键值对。

2.set

std::set是C++标准模板库(STL)中提供的有序关联容器之一。它基于红黑树(Red-Black-Tree)实现,用于存储唯一的元素,并按照元素的值进行排序。

2.1set的特性

1、唯一性:std::set中不允许存储重复的元素,每个元素都是唯一的。插入重复元素的操作会被忽略。
2、有序性:std::set中的元素是按照升序进行排序的。这种排序是通过红黑树的自平衡性质实现的,保证了插入、删除等操作的高效性。
3、插入元素:使用insert成员函数可以将元素插入到集合中,如果元素已经存在,则插入操作会被忽略。

2.2 C++标准库中set的基本用法

#include<iostream>
#include<set>
using namespace std;

int main()
{
	//创建std::set对象
	std::set<int>mySet;

	//插入元素
	mySet.insert(42);
	mySet.insert(21);
	mySet.insert(63);
	mySet.insert(21);

	//删除元素
	mySet.erase(63);

	//查找元素
	auto it = mySet.find(42);
	
	for (const auto& element : mySet)
	{
		cout << element << " ";
	}
	cout << endl;

	return 0;
}

3.map和set类模板

在同时封装set和map时,面临的第一个问题是:两者的参数不匹配。

set只需要key,map则需要key和value。用红黑树同时封装出set和map时,set传给value的是一个value,map传给value的是一个pair,set和map传给红黑树的value决定了这棵树里面存的节点值类型。上层容器不同,底层红黑树的key和value也不同。

参考STL库中的解决方案:不论是k还是k和v,都看作是value_type,获取key值时再使用别的方法解决。如下图所示。其中rb_tree的参数3就是获取key的方式,也就是上文提到的解决办法,后文会有介绍。参数4是比较方式,参数5是空间配置器。

能否省略 参数1 key_type?

对于set来说,可以省略 参数1 key_type,因为冗余了。但是对于map来说,不能省略参数1。因为map中的函数参数类型为key_type,省略后就无法确定参数类型了,比如Find、Erase中都需要key_type这个类型。

在上层容器set中,K和T都代表Key,底层红黑树节点当中存储K和T都是一样的;map中,K代表键值Key,T代表由Key和Value构成的键值对pair,底层红黑树中只能存储T。所以红黑树为了满足同时支持set和map,节点当中存储T。这就需要对红黑树进行改动。

4.红黑树节点的定义

4.1 红黑树节点的修改

原来红黑树节点的定义:

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;//节点的左孩子
	RBTreeNode<K, V>* _right;//节点的右孩子
	RBTreeNode<K, V>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	pair<K, V> _kv;//节点的值域

	Colour _col;//节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

这里将红黑树节点中的K-V键值对pair<K,V>修改成类型T,T类型的_data是pair键值对还是单个的值,视情况而定。如果是map的需求,那么就是pair;如果是set的需求,那么就是一个K。如下所示:

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;//节点的左孩子
	RBTreeNode<T>* _right;//节点的右孩子
	RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	T _data;//节点的值域

	Colour _col;//节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)
	{}
};

4.2 仿函数

(1)节点比较大小时存在的问题

往红黑树中插入节点时,需要比较节点的大小,我们知道map中的元素是pair类型的关键字-值(key-value)对,关键字起到索引的作用,值则表示与索引相关联的数据。而在set中每个元素只包含一个关键字。比如文章红黑树中实现的查找元素的函数Find,该函数的形参是一个pair类型的关键字-值对,使用关键字first来比较大小。map完全可以借助该红黑树来实现,但是set中的元素只包含一个关键字,故传入到底层的红黑树的Find函数中的参数不是pair类型,所以不能借助该红黑树来实现set。凡是涉及到获取关键字的地方都有这个问题,因为对于map和set来说传入形参中的_data是不确定的,对于这种不确定的类型,一般使用仿函数来解决。

//2、红黑树查找节点
Node* Find(const pair<K, V>& kv)
{
	Node* cur = _root;
	while (cur)
	{
		if (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
 
	return nullptr;
}

(2)解决不同类型的关键字获取的问题

现在可以研究stl库中rb_tree的参数3了,它是一个函数对象,可以传递仿函数,用来从不同的T中获取key值。

set和map有自己各自的仿函数,这样底层的红黑树就能更具仿函数分别获取set和map的关键字。

①map的仿函数

template<class K, class V>
class MyMap
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};

public:
	bool Insert(const pair<K, V>& kv)
	{
		return _t.Insert(kv);
	}

private:
	RBTree<K, pair<K, V>,MapKeyOfT> _t;
};

②set的仿函数

template<class K>
class MySet
{
	struct SetKeyOfT
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};

public:
	bool Insert(const K& k)
	{
		return _t.Insert(k);
	}

private:
	RBTree<K, K,SetKeyOfT> _t;
};

当我们得到不同的关键字的获取方式后,就可以更改红黑树中相应的代码了,比如查找函数。

//2、红黑树查找节点
	Node* Find(const T& data)
	{
		KOfT koft;
		Node* cur = _root;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

5.红黑树的迭代器

map和set迭代器的实现本质是红黑树迭代器的实现,迭代器的实现模板类型、模板类型引用、模板类型指针。将红黑树的节点再一次封装,构建一个单独的迭代器类。因为节点的模板参数有K和V,所以迭代器类也需要这两个参数。不同的迭代器传递不同的参数,额外增加Ref和Ptr的目的是为了让普通迭代器和const迭代器能使用同一个迭代器类。其中Ref和Ptr具体是什么类型,取决于调用方传递的参数。

template<class T,class Ref,class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T> Self;
	Node* _node;

	__TreeIterator(Node* node)
		:_node(node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

5.1 移动操作

红黑树的迭代器是一个双向迭代器,只支持++和--操作,树形结构的容器在进行遍历时,默认按中序遍历的顺序进行迭代器移动,因为这样遍历二叉搜索树后,结果为有序。如下图中的二叉树遍历的结果为:5 6 7 8 10 11 12 13 15。

++移动的思路:
1.判断当前节点的右子树是否存在,如果存在,则移动至右子树的最左节点;
2.如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点;
3.如果父亲为空,则下一个节点就是空。

具体过程如下:it在节点5位置时,++是走到它的父亲节点6;但是it在节点7的位置时,++是走到它的祖先节点8的位置。it指向的节点5的右为空,节点5的右为空,表明节点5已经被访问完了。由于节点5是其父亲节点的左子树,节点5访问完了,此时该访问节点5的父亲节点,即节点6。此时节点6的右不为空,此时要访问节点6的右子树节点7。节点7的左子树为空,接着访问节点7,下一步访问节点7的右子树,节点7的右子树为空,表明节点7访问完了,此时节点6也访问完了,节点6是其父亲节点的左子树,接着访问节点6的父亲节点,即节点8。最后访问完之后,it指向NULL,则结束。

这里解释两个问题:
1、为什么右子树不为时,要访问右子树的最左节点?
因为此时是中序遍历,路径为左-根-右,如果右边路径存在,就要从它的最左节点开始访问。

2、为什么右子树为空时,要访问当前路径中孩子节点为左孩子的父亲节点?
因为孩子节点为右孩子的父亲节点已经被访问过了。

Self& operator++()
{
	//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
	//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
	//
	if (_node->_right)
	{
		Node* subLeft = _node->_right;
		while (subLeft->_left)
		{
			subLeft = subLeft->_left;
		}

		_node = subLeft;
	}
	else
	{
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent&&cur==parent->_right)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}

	return *this;
}

6.map模拟实现

template<class K, class V>
class MyMap
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<const K, V>& kv)
		{
			return kv.first;
		}
	};

public:
	typedef typename RBTree<K, pair<const K,V>, MapKeyOfT>::iterator iterator;

	iterator begin()
	{
		return _t.begin();
	}

	iterator end()
	{
		return _t.end();
	}

	pair<iterator,bool> Insert(const pair<const K, V>& kv)
	{
		return _t.Insert(kv);
	}

	//[]返回key对应的value值
	V& operator[](const K& key)
	{
		//这里的插入操作可能会成功,也可能会失败,如果key已经存在则失败;
		//无论是插入成功还是插入失败,都会返回节点对应的迭代器,所以就能拿到该节点对应的value
		//V()是缺省值,如果插入的是int即为0;如果是string,那么就构造一个空字符串对象
		pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
		return ret.first->second;
	}

private:
	RBTree<K, pair<const K, V>,MapKeyOfT> _t;
};

7.set模拟实现

template<class K>
class MySet
{
	struct SetKeyOfT
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};

public:
	typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;

	iterator begin()
	{
		return _t.begin();
	}

	iterator end()
	{
		return _t.end();
	}

	pair<iterator, bool> Insert(const K& k)
	{
		return _t.Insert(k);
	}

private:
	RBTree<K, K,SetKeyOfT> _t;
};

8.RBTree完整代码

#include<iostream>

using namespace std;

enum Colour
{
	BLACK,
	RED,
};


template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;//节点的左孩子
	RBTreeNode<T>* _right;//节点的右孩子
	RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	T _data;//节点的值域,_data可能是一个key值,也可能是一个K-V键值对

	Colour _col;//节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)
	{}
};

template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> Self;
	Node* _node;

	//节点指针构造迭代器
	__TreeIterator(Node* node)
		:_node(node)
	{}

	//使用普通迭代器构造const迭代器的构造函数
	__TreeIterator(const __TreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	bool operator==(const Self& s)
	{
		return _node == s._node;
	}

	Self& operator++()
	{
		//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
		//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
		//
		if (_node->_right)
		{
			Node* subLeft = _node->_right;
			while (subLeft->_left)
			{
				subLeft = subLeft->_left;
			}

			_node = subLeft;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}
			_node = parent;
		}

		return *this;
	}

	Self& operator--()
	{

	}
};

template<class K, class T, class KOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef __TreeIterator<T, T&, T*> iterator;
	typedef __TreeIterator<T, const T&, const T*> const_iterator;

	iterator begin()
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}

		return iterator(cur);
	}

	iterator end()
	{
		return iterator(nullptr);
	}

	//1、红黑树插入节点
	pair<iterator, bool> Insert(const T& data)
	{
		//1、按二叉搜索树的规则插入节点
		//如果二叉树为空,则将新插入的节点作为根节点
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;//红黑树的根节点为黑色
			return make_pair(iterator(_root), true);
		}

		KOfT koft;
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(iterator(cur), false);
			}
		}

		cur = new Node(data);
		Node* newnode = cur;
		if (koft(cur->_data) > koft(parent->_data))
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else if (koft(cur->_data) < koft(parent->_data))
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//cur->_col = RED;
		//这里默认新插入的节点是红色节点,为什么呢?
		//因为插入新节点时,就涉及破坏规则2(红黑树中没有连续的红节点);还是规则3(红黑树每条路径都有相同数量的黑节点)。
		//首先,插入红色节点时,不一定会破坏规则2(如果插入节点的父亲节点是黑色节点);即使破坏了规则2,新插入的节点是
		//红色节点也只会影响一条路径。
		//如果新插入的节点是黑色的,会影响二叉树的所有路径,因为红黑树的每条路径都要有相同数量的黑色节点。

		//情况1:cur节点为红色、parent节点为红色、grandfather为黑色、uncle节点存在且为红色;
		//情况2:uncle节点不存在
		//情况3:uncle节点存在且为黑
		while (parent && parent->_col == RED)
		{
			//cur节点的父亲节点parent是红色,此时parent节点不可能是根节点
			//此时看cur节点的叔叔节点uncle的颜色。
			Node* grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//如果grandfather不是根节点,继续往上处理。
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况3:双旋;先parent节点左旋,转换为情况2
					if (parent->_right == cur)
					{
						RotateL(parent);
						swap(parent, cur);
					}

					//情况2:情况2也可能是情况3进行左单旋之后,需要再进行右单旋
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;

					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				//情况1:uncle存在、且为红
				//情况2 or 情况3:uncle不存在 or uncle存在、且为黑
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//如果grandfather不是根节点,继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况3
					if (cur == parent->_left)
					{
						RotateR(parent);
						swap(cur, parent);
					}

					//情况2
					RotateL(grandfather);

					grandfather->_col = RED;
					parent->_col = BLACK;
				}
			}
		}

		//最终将根节点变为黑色
		_root->_col = BLACK;

		return make_pair(iterator(newnode), true);
	}



	//2、红黑树查找节点
	iterator Find(const T& data)
	{
		KOfT koft;
		Node* cur = _root;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return iterator(cur);
			}
		}

		return iterator(nullptr);
	}

	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

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

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

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

		subR->_left = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subR;

		//如果原来parent是这棵树的根节点,左旋转完成后subR节点变成这棵树的根节点
		if (grandfather == nullptr)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == grandfather->_left)
			{
				grandfather->_left = subR;
			}
			else if (parent == grandfather->_right)
			{
				grandfather->_right = subR;
			}

			subR->_parent = grandfather;
		}
	}

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

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

		subL->_right = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subL;

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

			subL->_parent = grandfather;
		}
	}

	Node* _root = nullptr;
};

完整代码可参考:set/map的模拟实现。

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

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

相关文章

【数据结构专栏】二叉搜索树(Binary Search Tree)的剖析?

文章目录 &#x1f9e8;前言1、二叉搜索树的基本概念&#xff1f;2、二叉搜索树的节点结构组成&#xff1f;3、二叉搜索树的插入操作&#xff1f;4、二叉搜索树的删除操作&#xff1f;5、二叉搜索树的遍历&#xff1f; 6、二叉搜索树的性能分析&#xff1f; &#x1f389;完整代…

FastApi学习第三天:两表联查

两表联查 在 FastAPI 中&#xff0c;使用 Tortoise ORM 查询两表联查&#xff08;通常是通过外键关系进行联接&#xff09;是非常简单的。可以使用 select_related 或 prefetch_related 来执行联表查询&#xff0c;它们类似于 Django ORM 的 select_related 和 prefetch_relate…

Redis原理及应用

Redis简介 Redis是开源的&#xff08;BSD许可&#xff09;&#xff0c;数据结构存储于内存中&#xff0c;被用来作为数据库&#xff0c;缓存和消息代理。它支持多种数据结构&#xff0c;例如&#xff1a;字符串&#xff08;string&#xff09;&#xff0c;哈希&#xff08;hash…

Unity类银河战士恶魔城学习总结(P141 Finalising ToolTip优化UI显示)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ UI部分暂时完结&#xff01;&#xff01;&#xff01; 本章节优化了UI中物品描述的显示效果&#xff0c;技能描述的显示效果 并且可以批…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册&#xff1a; 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 &#xff0c; 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册&#xff0c;在动态注册不稳定时使用&#xff0c;特点是&#xff1a;稳定&…

社交电商专业赋能高校教育与产业协同发展:定制开发AI智能名片及2+1链动商城小程序的创新驱动

摘要&#xff1a;本文围绕社交电商有望成为高校常态专业这一趋势展开深入探讨&#xff0c;剖析国家政策认可下其学科发展前景&#xff0c;着重阐述在专业建设进程中面临的师资短缺及实践教学难题。通过引入定制开发AI智能名片与21链动商城小程序&#xff0c;探究如何借助这些新…

数据指标与标签在数据分析中的关系与应用

导读&#xff1a;分享数据指标体系的文章很多&#xff0c;但讲数据标签的文章很少。实际上&#xff0c;标签和指标一样&#xff0c;是数据分析的左膀右臂&#xff0c;两者同样重要。实际上&#xff0c;很多人分析不深入&#xff0c;就是因为缺少对标签的应用。今天系统的讲解下…

使用Electron将vue2项目打包为桌面exe安装包

目录 一、下载electron模板项目 【electron-quick-start】​ 二、打开项目&#xff0c;安装所有依赖 三、在打exe包的时候报错是因为没有&#xff0c;需要检查并安装之后重新打包&#xff1b; 四、经过这么疯狂的一波操作之后&#xff0c;就可以打包出你想要的exe安装包&am…

MySQL基础大全(看这一篇足够!!!)

文章目录 前言一、初识MySQL1.1 数据库基础1.2 数据库技术构成1.2.1 数据库系统1.2.2 SQL语言1.2.3 数据库访问接口 1.3 什么是MySQL 二、数据库的基本操作2.1 数据库创建和删除2.2 数据库存储引擎2.2.1 MySQL存储引擎简介2.2.2 InnoDB存储引擎2.2.3 MyISAM存储引擎2.2.4 存储引…

Linux之NFS共享文件操作

一、注意点 以下操作使用root用户 代理端需要访问服务端的2049、111端口二、nfs下载 # 服务端和代理端都要安装 yum –y install rpcbind yum –y install nfs-utils三、配置共享目录-【服务端】 *修改/etc/exports文件&#xff0c;追加以下内容 /home/app_adm/test ip1(in…

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云 C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用 发布于 2021-06-10 00:10:59 7.1K0 举报 文章被收录于专栏&#xff1a;c#学习笔记 一、介绍 DockPanelSuite是托管在…

杰发科技AC7840——EEP中RAM的配置

sample和手册中示例代码的sram区地址定义不一样 这个在RAM中使用没有限制&#xff0c;根据这个表格留下足够空间即可 比如需要4096字节的eep空间&#xff0c;可以把RAM的地址改成E000&#xff0c;即E000-EFFF&#xff0c;共4096bytes即可。

web-03

CSS回顾 选择器 标签选择器 标签{}ID选择器 标签中定义ID属性。 #ID值{}类选择器 标签中使用class属性 .类名{}关于DIV/span div任意的大小的长方形&#xff0c;大小css&#xff1a; width, height控制。—换行 span-- 一行内 CSS常用属性 width/height 宽度/高度 定义&…

CI配置项,IT服务的关键要素

随着现今数字经济的不断发展&#xff0c;逐渐成熟的IT 基础设施已不再是简单的竞争优势&#xff0c;而已成为企业生存和发展的基石。然而&#xff0c;仅仅拥有强大的基础设施是不够的。为了保障 IT 服务的平稳运行和持续交付&#xff0c;企业还需要重点关注 IT 服务的核心构建模…

ApiChain-编写迭代单测用例

项目地址&#xff1a;ApiChain 项目主页 写单测用例&#xff0c;就像画一幅有向不循环的图&#xff0c;图中的每个节点是这个单测用例的每一个步骤&#xff0c;连线代表着数据的流向&#xff0c;这幅图通常有一个或者多个起点&#xff0c;但通常只有一个终点。起点的数据来源于…

九、FOC原理详解

1、FOC简介 FOC&#xff08;field-oriented control&#xff09;为磁场定向控制&#xff0c;又称为矢量控制&#xff08;vectorcontrol&#xff09;&#xff0c;是目前无刷直流电机&#xff08;BLDC&#xff09;和永磁同步电机&#xff08;PMSM&#xff09;高效控制的最佳选择…

企业OA管理系统:Spring Boot技术实现与案例研究

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了企业OA管理系统的开发全过程。通过分析企业OA管理系统管理的不足&#xff0c;创建了一个计算机管理企业OA管理系统的方案。文章介绍了企业OA管理系统的系统分析部…

【tensorflow的安装步骤】

创建一个虚拟环境 conda create -n tensorflow python3.6激活虚拟环境 conda activate tensorflow使用镜像源下载 pip install tensorflow1.15.0 -i https://pypi.tuna.tsinghua.edu.cn/simple/特别特别重要的点&#xff01;&#xff01;&#xff01; 别用WiFi或者校园网下…

网络安全-web架构-nginx配置

1. nginx访问&#xff1a; 访问的是index.html&#xff0c; 访问ip访问的资源就是在/usr/share/nginx/html中&#xff1b; 当nginx不认识&#xff0c;浏览器认识的话&#xff0c;浏览器会自动渲染。 当nginx认识&#xff0c;浏览器不认识的话&#xff0c;浏览器会把它加载成…

ES6 模块化语法

目录 ES6 模块化语法 分别暴露 统一暴露 ​编辑 默认暴露 ES6 模块化引入方式 ES6 模块化语法 模块功能主要由两个命令构成&#xff1a;export 和 import。 ⚫ export 命令用于规定模块的对外接口&#xff08;哪些数据需要暴露&#xff0c;就在数据前面加上关键字即可…