哈希和unordered系列封装(C++)

news2024/12/24 10:24:00

哈希和unordered系列封装

  • 一、哈希
    • 1. 概念
    • 2. 哈希函数,哈希碰撞
      • 哈希函数(常用的两个)
      • 哈希冲突(碰撞)
      • 小结
    • 3. 解决哈希碰撞
      • 闭散列
        • 线性探测
        • 二次探测
        • 代码实现
        • 载荷因子(扩容)
      • 开散列
        • 哈希桶
        • 代码实现
        • 扩容
  • 二、unordered系列封装
    • hash_table
      • 迭代器实现原理(单项迭代器)
      • hash_table实现代码
    • unordered_set封装
    • unordered_map封装
  • 三、总结

一、哈希

1. 概念

通过某种函数使用元素的存储位置与其关键码之间建立映射关系。

  1. 插入元素时,通过该函数求得的值,就是该元素的存储位置。
  2. 搜索元素时,通过该函数求得的值进行比对,如果关键码相等则搜索成功。

该方法称为哈希(散列)方法, 而其中的某中函数被称为哈希(散列)函数,构造出来的结构成为哈希表(散列表)。

2. 哈希函数,哈希碰撞

哈希函数(常用的两个)

直接定址法

  1. 函数
    取关键字的某个线性函数得出散列地址:Hash(Key) = A * Key + B
  2. 优缺
    优点:简单均匀
    缺点:关键码的分布范围需要集中
  3. 场景
    统计字符串中字符出现的个数,其中字符是集中的。

除留余数法

  1. 函数
    Hash(Key) = Key % m(m是小于等于表中可取地址数即可(建议:质数))
  2. 场景
    适用于值的方位分散

eg:
除留余数法

注意:

  1. 使用除留余数法,所以就要求被%的key必须是整型。如果key为字符串如何转成整型呢?
    答:字符串哈希函数。评价hash函数性能的一个重要指标就是冲突,在相关资源允许的条件下冲突越少hash函数的性能越好。
    常见的字符串哈希算法BKDRHash,APHash,DJBHash…

eg:

 // BKDR Hash Function
unsigned int BKDRHash(char *str)
{
    unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;
 
    while (*str)
    {
        hash = hash * seed + (*str++);
    }
 
    return (hash & 0x7FFFFFFF);
}
  1. 使用除留余数法,最好模一个素数,如何快速模一个类似两倍关系的素数?
    答:使用了一个默认的素数集合,这个集合中包含了一系列素数。在不同的STL实现中,这个素数集合可能会有所不同。一般来说,这个集合中的素数经过仔细选择,以确保哈希表的负载因子(即平均哈希桶中元素的数量)保持在一个较小的范围内,从而提供更好的性能。
//素数集合
size_t GetNextPrime(size_t prime)
{
	const int PRIMECOUNT = 28;
	static const size_t primeList[PRIMECOUNT] =
	{
		53ul, 97ul, 193ul, 389ul, 769ul,
		1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
		49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
		1572869ul, 3145739ul, 6291469ul, 12582917ul,
		25165843ul,
		50331653ul, 100663319ul, 201326611ul, 402653189ul,
		805306457ul,
		1610612741ul, 3221225473ul, 4294967291ul
	};
	size_t i = 0;
	for (; i < PRIMECOUNT; ++i)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[i];
}

哈希冲突(碰撞)

根据上面的例子,如果在数据集合中添加一个数据25,那么会发现通过哈希函数求的地址已经被别的关键码占据。

概念: 不同关键码通过相同的哈希函数计算出相同的哈希地址,被称为哈希冲突(碰撞)。

小结

哈希函数的设计跟哈希冲突有着必要的联系。
哈希函数的设计:

  1. 哈希函数的定义域,需要包含存储的全部关键码。值域,0到哈希表允许地址数最大值-1
  2. 哈希函数计算的地址,均匀分布在哈希表中
  3. 设计简单

3. 解决哈希碰撞

解决哈希碰撞的两种方法:闭散列和开散列

闭散列

闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被填满,说明哈希表还有空位置,那么就可以从冲突位置为起始找下一个空位置。

线性探测

概念:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

优缺点

  1. 优点:实现简单
  2. 缺点:一旦发生冲突连在一起,容易产生数据“堆积”。搜索效率下降

插入

  • 通过哈希函数获取待插入元素在哈希表的目标位置
  • 如果该位置没有元素直接插入,如果有元素则发生冲突,使用线性探测找到下一个空位置,然后插入。

eg:
线性探测

删除

  • 因为哈希冲突的原因,不能随便删除,会影响后面元素的搜索。例如:删除上个例子哈希表的6,那么我们查找25会被影响。
  • 所以采用伪删除,给哈希表每个空间设置一个状态
    `状态: EMPTY此位置为空,EXIST此位置有元素,DELETE此位置元素被删除。
enum STATE
{
	EXIST, 
	EMPTY,
	DELETE
};`
二次探测

不同于线性探测是依次寻找空位置,二次探测是通过公式跳跃式的寻找空位置。
Hash(i) = (Hash(x) + i^2) % m;
Hash(X):通过哈希函数计算key值得到的位置,但是已经存在元素
Hash(i):将要存放位置
m:哈希表的大小
i = 1,2,3,4…

注意: 除了线性探测,二次探测,还有双重哈希…

代码实现
//开放地址法
namespace open_address 
{
	//哈希函数
	template<class K>
	struct DefaultHashFunc
	{
		size_t operator()(const K& key)
		{
			return size_t(key);     //转成无符号整型
		}
	};
	
	//模板特化 -- 针对字符串    BKDRHash算法
	template<>
	struct DefaultHashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash *= 131;
				hash += ch;
			}
			return hash;
		}
	};


	//状态
	enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

	//数据
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		STATE _state = EMPTY;
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);     //给哈希表初始化十个空间
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			//扩容   -->   根据载荷因子
			//if ((double)_n / (double)_table.size() >= 0.7)
			if (10 * _n / _table.size() >= 7)
			{
				size_t newSize = _table.size() * 2;
				
				//造新表
				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSize);

				//遍历旧表重新映射到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}

				//交换新旧表,原空间出作用域自动销毁
				_table.swap(newHT._table);
			}

			//线性探测
			HashFunc hf;
			size_t hashi = hf(kv.first) % _table.size();
			while (_table[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _table.size();
			}
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXIST;
			_n++;
			return true;
		}

		HashData<const K, V>* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._state == EXIST
					&& _table[hashi]._kv.first == key)
				{
					//&_table[hashi]类型是HashData<K, V>*
					return (HashData<const K, V>*)&_table[hashi];    
				}

				++hashi;
				//如果到_table的最后了,绕到最前面
				hashi %= _table.size();
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<const K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			return false;
		}

	private:
		vector<HashData<K, V>> _table;
		size_t _n = 0;           //存储有效数据
	};

}
载荷因子(扩容)

载荷因子的就算方法:α = 表中有效的元素个数 / 散列表的长度。
对于开放地址法,载荷因子是特别重要的元素,通过一些科学实验,载荷因子应严格控制在0.7-0.8。∵散列表的长度是一定的,表中有效元素个数和α成正比,∴如果超过载荷因子0.8,产生冲突的可能就越大,查表时CPU缓存命中率低。

再进行插入操作的时候要根据载荷因子判断需不需要扩容,用空间换时间

开散列

开散列:也叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同关键码的归于同一子集合,每个自己和称为一个桶,各个桶中的元素通过单链表链起来,各链表的头节点存在哈希表中。

哈希桶

哈希桶

5和8下标都存在哈希冲突

代码实现
namespace hash_bucket
{
	template<class K>
	struct DefaultHashFunc
	{
		size_t operator()(const K& key)
		{
			return size_t(key);
		}
	};
	//模板特化 -- 针对字符串
	template<>
	struct DefaultHashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};


	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;

		//初始化
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};


	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<const K, V> Node;
	public:
		HashTable()
		{
			//开十个空间,初始化为nullptr
			_table.resize(10, nullptr);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			HashFunc hf;

			//负载因子到1扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);

				//遍历旧表
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTable);
			}

			size_t hashi = hf(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			_n++;
			return true;
		}

		Node* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first << ":" << cur->_kv.second << "->";
					cur = cur->_next;
				}
				printf("nullptr\n");
			}
			cout << endl;
		}

	private:
		vector<Node*> _table;
		size_t _n = 0;
	};
}
扩容

桶的个数是一定的(桶的个数 == 表的大小)。如果不进行扩容,可能一个桶中有很多元素,会影响哈希表的性能。开散列最完美的情况就是每个哈希桶中刚好挂一个节点,再插入时就会发生哈希冲突,因此判断扩容的条件就可以是: 元素的个数 == 桶的个数

二、unordered系列封装

unordered系列set、map的容器接口和红黑树实现的set、map相似,使用大差不差,所以在这里就不进行介绍了。

hash_table

迭代器实现原理(单项迭代器)

  1. 迭代器++
  1. 当前桶没遍历完,直接通过链表找下一个节点
  2. 当前桶遍历完
    a. 通过哈希函数确定当前存储位置然后+1
    b. 循环(加过1的位置小于哈希表的大小)
    - - Ⅰ.该位置不为空,则成功找到,直接返回
    - - Ⅱ.该位置为空继续向后+1,继续循环判断
    c. 循环结束没找到,返回nullptr
Self& operator++()
{
	if (_node->_next)  //当前桶没完
	{
		_node = _node->_next;
	}
	else               //当前桶完了
	{
		HashFunc hf;
		KeyOfT kot;

		size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
		++hashi;
		while (hashi < _pht->_table.size())
		{
			if (_pht->_table[hashi])
			{
				_node = _pht->_table[hashi];
				return *this;
			}
			else
			{
				hashi++;
			}
		}
		_node = nullptr;
	}
	return *this;
}

hash_table实现代码

#include <vector>

// 1、哈希表
// 2、封装map和set
// 3、普通迭代器
// 4、const迭代器
// 5、insert返回值  operator[]
// 6、key不能修改的问题

namespace hash_bucket
{
	template<class K>
	struct DefaultHashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	template<>   //特化
	struct DefaultHashFunc<string>
	{
		size_t operator()(const string& str)
		{
			size_t hash = 0;
			for (auto ch : str)
			{
				hash *= 131;
				hash += ch;
			}
			return hash;
		}
	};


	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	//类前置声明  -->  因为迭代器的实现会调用哈希表指针
	template<class K, class T, class KeyOfT, class HashFunc>
	class HashTable;


	//迭代器
	template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node;
		typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;

		//普通迭代器
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;

		Node* _node;
		//哈希表指针  注意这里要加上const限制*this,不然哈希表调用时的this是const的会导致权限放大
		const HashTable<K, T, KeyOfT, HashFunc>* _pht;
		
		HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)
			:_node(node)
			,_pht(pht)
		{}

		//普通迭代器时,是拷贝构造
		//const迭代器时,是构造。普通迭代器构造const迭代器
		HTIterator(const Iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}

		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++()
		{
			if (_node->_next)  //当前桶没完
			{
				_node = _node->_next;
			}
			else               //当前桶完了
			{
				HashFunc hf;
				KeyOfT kot;

				size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
				++hashi;
				while (hashi < _pht->_table.size())
				{
					if (_pht->_table[hashi])
					{
						_node = _pht->_table[hashi];
						return *this;
					}
					else
					{
						hashi++;
					}
				}
				_node = nullptr;
			}
			return *this;
		}
	};


	//set -> hash_bucket::HashTable<K, K> _ht
	//map -> hash_bucket::HashTable<K, pair<K, V>> _ht
	template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<T> Node;

		//友元         迭代器的实现会调用哈希表指针
		template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
		friend struct HTIterator;

	public:

		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;
		typedef HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return iterator(cur, this);
				}
			}

			return iterator(nullptr, this);
		}

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


		const_iterator begin()  const
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return const_iterator(cur, this);
				}
			}

			return const_iterator(nullptr, this);
		}

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}



		HashTable()
		{
			_table.resize(10, nullptr);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}

				_table[i] = nullptr;
			}
		}

		pair<iterator, bool> Insert(const T& data)
		{
			HashFunc hf;
			KeyOfT kot;

			iterator it = Find(kot(data));


			if (it != end())
			{
				return make_pair(it, false);
			}


			// 负载因子到1--扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);

				// 遍历旧表,把节点牵下来挂到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hf(kot(data)) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_table[i] = nullptr;
				}

				_table.swap(newTable);
			}

			size_t hashi = hf(kot(data)) % _table.size();
			// 头插
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode, this), true);
		}

		iterator Find(const K& key)
		{
			HashFunc hf;
			KeyOfT kot;

			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}

				cur = cur->_next;
			}

			return iterator(nullptr, this);
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			KeyOfT kot;

			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			--_n;
			return false;
		}

	private:
		vector<Node*> _table; // 指针数组
		size_t _n = 0;        // 存储有效数据个数
	};
}

unordered_set封装

namespace kpl
{
	template<class K>
	class unordered_set
	{
		//该仿函数只是跟map跑
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;


		const_iterator begin() const
		{
			return _ht.begin();
		}

		const_iterator end() const
		{
			return _ht.end();
		}


		pair<iterator, bool> insert(const K& key)
		{
			//这里返回值的first的迭代器是普通迭代器,用普通迭代器接收
			pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
			//使用普通迭代器构造一个const的迭代器,这里就体现出迭代器实现中的那个拷贝构造
			return pair<iterator, bool>(ret.first, ret.second);
		}

	private:
		hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
	};
}

unordered_map封装

namespace kpl
{
	template<class K, class V>
	class unordered_map
	{
		//仿函数的主要作用在这里,set的封装只是跟跑,为了就是去键值对的key
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};

	public:
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;

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

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

		const_iterator begin() const
		{
			return _ht.begin();
		}

		const_iterator end() const
		{
			return _ht.end();
		}

		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}
	
		//返回值是与key对应的value的值。
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

	private:
		hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;
	};

}

三、总结

闭散列的缺陷:空间利用率低,用空间换时间,这也是哈希缺陷
开散列和闭散列的区别:
链地址法比开地址法更加的节省存储空间。原因:虽然链地址法增加了连接指针,但是开地址法为了保证搜索效率,必须保持大量的空闲空间。

unordered_set和unordered_map

  1. 前向迭代器
  2. 遍历出来不是有序
  3. 通过key访问当个元素的效率比set和map快,遍历元素子集的范围迭代方面效率较低

其余的特征和set,map大差不差,可以参考我的上一篇博客

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

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

相关文章

bluez inquiry 流程梳理--从代码层面理解bluez架构

贴一张bluez架构图方便理解 user space APP&#xff1a;上层应⽤程序 Pluseaudio/pipewire&#xff1a;A2DP的组件 Bluetoothd: 蓝⽛守护进程 Bluez: 包括Bluez tool跟Bluez lib kernel space 内核代码包含以下⼏部分 driver/bluetooth net/bluetooth include/net/bluetooth…

webpack项目工程初始化

一、初始化项目 默认系统已经安装node //初始化 pnpm init//安装webpack pnpm i -D webpack webpack-cli 新建一个index.html的入口文件 新建一个src文件存放js代码&#xff0c;src里面新建一个index.js package.josn配置打包命令 {"name": "webpack-cs&q…

vue+elementUI的tabs与table表格联动固定与滚动位置

有个变态的需求&#xff0c;要求tabs左侧固定&#xff0c;右侧是表格&#xff0c;点击左侧tab&#xff0c;右侧表格滚动到指定位置&#xff0c;同时&#xff0c;右侧滚动的时候&#xff0c;左侧tab高亮相应的item 上图 右侧的高度非常高&#xff0c;内容非常多 常规的瞄点不适…

零基础可以学编程吗,不懂英语怎么学编程,中文编程工具实例

零基础可以学编程吗&#xff0c;不懂英语怎么学编程&#xff0c;中文编程工具实例 上图是中文编程工具界面、标尺实例。 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&#x…

spring-boot对rabbitMQ的操作

一、安装rabbitMQ 1、直接使用docker拉取镜像 docker pull rabbitmq:3.82、启动容器 docker run \-e RABBITMQ_DEFAULT_USERadmin \-e RABBITMQ_DEFAULT_PASS123456 \-v mq-plugins:/plugins \--name rabbit01 \--hostname rabbit01 --restartalways \-p 15672:15672 \-p 5672:…

在微服务架构中的数据一致性

当从传统的单体应用架构转移到微服务架构时&#xff0c;特别是涉及数据一致性时&#xff0c;数据一致性是微服务架构中最困难的部分。传统的单体应用中&#xff0c;一个共享的关系型数据库负责处理数据一致性。在微服务架构中&#xff0c;如果使用“每个服务一个数据库”的模式…

碧莲盛 x Tapdata:实时数据如何赋能医疗美容行业,助力医疗决策及个性化服务升级

使用 Tapdata&#xff0c;化繁为简&#xff0c;轻量代替 OGG、DSG 这样的同步工具&#xff0c;以及 Kettle、Informatica、Python 这样的 ETL 工具或脚本&#xff0c;帮助企业在五花八门的数据需求面前&#xff0c;实现“做且仅做最后一次 ETL”的目标&#xff0c;这绝非纸上谈…

TIME_WAIT状态TCP连接导致套接字无法重用实验

理论相关知识可以看一下《TIME_WAIT相关知识》。 #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #include<errno.h> #include<syslog.h> #inc…

API接口测试工具的主要作用及选择指南

API接口测试是现代软件开发中至关重要的一环。为了确保不同组件之间的无缝集成和功能正常运作&#xff0c;API接口测试工具应运而生。本文将介绍API接口测试工具的主要作用&#xff0c;以及在选择适合项目的工具时需要考虑的因素。 1、功能测试&#xff1a;API接口测试工具的首…

基于Python+requests编写的自动化测试项目-实现流程化的接口串联

框架产生目的&#xff1a;公司走的是敏捷开发模式&#xff0c;编写这种框架是为了能够满足当前这种发展模式&#xff0c;用于前后端联调之前&#xff08;后端开发完接口&#xff0c;前端还没有将业务处理完毕的时候&#xff09;以及日后回归阶段&#xff0c;方便为自己腾出学(m…

Stable Diffusion绘画系列【3】:二次元动漫画风

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

C语言——求π的近似值

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> #include<math.h> int main() {int s;double n,t,pi;t1;pi0;n1.0;s1;while (fabs(t)>1e-6){pipit; nn2; s-s; ts/n;}pipi*4;printf("pi%lf\n",pi);return 0; }这里是求小数点后6位——1e-6&#…

漏洞环境靶场搭建(内含DVWA SQLi-LABS upload-labs等)

一、docker环境搭建(以redhat8为例&#xff0c;centos7命令基本一致) 1、安装docker yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2、启动docker systemctl start docker 3、配置镜像加速 vim /etc/docker/daemon.json {"registr…

Linux下基于MPI的hello程序设计

Linux下基于MPI的hello程序设计 一、MPICH并行计算库安装实验环境部署创建SSH信任连接&#xff0c;实现免密钥互相连接node1安装MPICH 3.4配置NFS注意(一定要先看)环境测试 二、HELLO WORLD并行程序设计 一、MPICH并行计算库安装 在Linux环境下安装MPICH执行环境&#xff0c;配…

Photoshop Elements 2023 v21.0(ps简化版)

Photoshop Elements 2023是一款ps简化版图像处理软件&#xff0c;它加入了一些新的功能和工具&#xff0c;以帮助用户更高效地处理图片。 新功能&#xff1a;软件加入了黑科技&#xff0c;采用Adobe Sensei AI技术&#xff0c;主打人工智能&#xff0c;一键P图&#xff0c;新增…

tabs切换,当点击tabItem时候,改变选中样式,以及content内容区域

效果图展示&#xff1a; html原生代码&#xff1a; <div><div class"buttons-row nav-select riskType" style"padding: 10px;"><div class"shoucang-title-box flex-start"><div class"shoucang-title-item active&q…

Django回顾【二】

目录 一、Web框架 二、WSGI协议 三、 Django框架 1、MVC与MTV模型 2、Django的下载与使用 补充 3、启动django项目 补充 5、 Django请求生命周期 四、路由控制 1、路由是什么&#xff1f; 2、如何使用 3、path详细使用 4、re_path详细使用 5、反向解析 6、路由…

The Sandbox 与 Shemaroo Entertainment 达成合作,将宝莱坞标杆作品带入元宇宙

我们非常高兴地宣布与印度领先的媒体和娱乐集团 Shemaroo Entertainment 建立合作伙伴关系。BharatBox 是全新的文化元宇宙中心&#xff0c;将展示印度娱乐业的知名艺术家和品牌。该项目标志着 Shemaroo 将逐步涉足数字领域&#xff0c;将电影传统与最先进的技术相结合&#xf…

算法之插入排序及希尔排序(C语言版)

我们来实现上述排序 一.插入排序. 当插入第i(i>1)个元素时&#xff0c;前面的array[0],array[1],.,array[i-1]已经排好序&#xff0c;此时用array[i的排序码与array[i-1]array[i-2].的排序码顺序进行比较&#xff0c;找到插入位置即将arrayU插入&#xff0c;原来位置上的元…

【小布_ORACLE】Part11-1--RMAN Backups笔记

Oracle的数据备份于恢复RMAN Backups 学习第11章需要掌握&#xff1a; 一.RMAN的备份类型 二.使用backup命令创建备份集 三.创建备份文件 四.备份归档日志文件 五.使用RMAN的copy命令创建镜像拷贝 文章目录 Oracle的数据备份于恢复RMAN Backups1.RMAN Backup Concepts&#x…