【C++】用哈希桶模拟实现unordered_set和unordered_map

目录

  • 一、哈希介绍
    • 1.1 哈希概念
    • 1.2 哈希冲突解决
      • 1.2.1 闭散列
      • 1.2.2 开散列
  • 二、哈希桶
    • 2.1 实现哈希桶
      • 2.1.1 构造节点和声明成员变量
      • 2.1.2 构造与析构
      • 2.1.3 仿函数
      • 2.1.4 查找
      • 2.1.5 插入
      • 2.1.6 删除
    • 2.2 kv模型哈希桶源代码
  • 三、改造哈希桶
    • 3.1 begin+end
    • 3.2 迭代器
      • 3.2.1 前置++
    • 3.3 改造后哈希桶与迭代器源代码
  • 四、模拟实现unordered_set
  • 五、模拟实现unordered_map

一、哈希介绍

1.1 哈希概念

顺序结构中(数组)查找一个元素需要遍历整个数组,时间复杂度为O(N);树形结构中(二叉搜索树)查找一个元素,时间复杂度最多为树的高度次logN。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

主要有3种操作:

  • 插入——根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 查找——根据要搜索的元素的关键码,用函数计算出存储位置,取该位置的元素关键码进行比较,如果相等,查找成功
  • 删除——根据待删除元素的关键码计算出该元素的存储位置,如果改元素存在,则进行删除

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

如果在上面的例子中插入元素44会怎样?44%10也是4,与原来元素4的位置冲突了。那么这个新插入的44应该如何放置呢?

1.2 哈希冲突解决

首先要知道哈希冲突的原因——哈希函数设计不够合理
哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

下面介绍两种常见的哈希函数:

  1. 闭散列
  2. 开散列

1.2.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,这里的下一个可能也有元素,所以可能继续重复前面的操作,直到遇到空位置。

线性探测:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
在这里插入图片描述
44%4=4,与元素4的位置冲突,到它的下一个位置,位置5也有元素,继续下一个,直到位置8没有存储元素,就把44存储到位置8中。

1.2.2 开散列

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

在这里插入图片描述

开散列中每个桶中放的都是发生哈希冲突的元素。

二、哈希桶

2.1 实现哈希桶

2.1.1 构造节点和声明成员变量

哈希表的每个位置是一个桶,这个桶的结构是单链表,单链表由每个节点组成。节点有数据域和指针域,指针域是用来连接下一个节点的,数据域存放的是节点的值。节点的数据域有两种:k模型和kv模型。这里实现哈希桶的是数据域是kv模型。

template<class K, class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		, _next(nullptr)
	{}
};

成员变量有存放的数据个数和哈希表的每个位置,即每个桶,用头指针进行连接,所以哈希表的每个位置是单链表的头指针。

vector<Node*> _table;//哈希表的每个位置-桶-单链表
size_t _n = 0;//存储的元素个数

2.1.2 构造与析构

1️⃣构造
刚开始给哈希表一定的空间大小,每个位置初始化为空指针。

HashTable(size_t n = 5)
{
	_table.resize(n, nullptr);
}

2️⃣析构
哈希表的结构是STL库中的vector,当程序结束时,vector会自动调用它的析构来清理哈希表,但是表中的每个位置是单链表,单链表的每个节点是动态开辟出来的,vector的析构不能清理它们。所以要自己写析构函数来清理这些节点。

~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;
	}
}

2.1.3 仿函数

哈希函数的计算公式:hash(key) = key % capacity,capacity就是表的空间大小,key必须是整数,但是key值是不确定的,有可能是整形、浮点型或者是字符串,所以要对key值作一些处理,使其变成整数才能进行取模操作。

1️⃣key不是字符串
返回值都转化成无符号整数

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

2️⃣key是字符串
因为传过来的参数固定就是字符串(string类型),不像前面,可能是int、double等,所以这里可以直接特化处理。定义一个临时变量为无符号整数作为返回值,遍历每个字符加到临时变量里,每个字符会自动转换成ASCII码值,然后再乘上权值131(在《The C Programming Language》书中有解释),保证不会出现year和raey相同的场景。

template<>
struct HashFind<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto& e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

在后面的操作中使用到哈希函数:hash(key) = key % capacity,都要通过调用仿函数来实现。

2.1.4 查找

查找一个元素是否存在,首先要计算出该元素的位置。假设该元素存在,但是它在某个位置的桶中,通过遍历单链表找到该元素,然后返回它在链表中的节点位置。不存在,返回nullptr

Node* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _table.size();//表中的位置
	Node* cur = _table[hashi];//得到当前位置头节点
	while (cur)
	{
		if (cur->_kv.first == key)//找到了
		{
			return cur;
		}
		cur = cur->_next;
	}
	//cur为空,不存在这个数据
	return nullptr;
}

2.1.5 插入

  1. 插入新的元素,不能有重复的,所以先对要插入的值进行查找,如果找到了,说明是重复元素,不能插入,返回失败。
  2. 插入新的数据不是重复元素,计算该元素在哈希表的映射位置,创建一个新节点,用头插法插入。
  3. 如果要插入数据前,哈希表的元素个数与哈希表的空间大小相等,就要扩容。创建一个新的哈希表,扩容的大小可以给原来的两倍,初始化为空。遍历旧表,将旧表的节点移动到新表中。注意,移动的过程中节点在旧表中的位置与新表可能是不对应的,所以还要用哈希函数得到节点在新表的位置,然后插入的话还是头插法。旧表中的每个位置即每个桶移动完成,,就将旧表的该位置置空。最后全部转移完,把旧表和新表进行交换,后面使用的就是新表了。
bool Insert(const pair<K,V>& kv)
{
	//重复元素不能插入
	if (Find(kv.first))
	{
		return false;
	}
	Hash hs;
	//扩容
	if (_n == _table.size())
	{
		vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间
		//将旧表的节点移到新表中,再交换
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				size_t hashi = hs(_table[i]->_kv.first) % newTable.size();
				Node* next = cur->_next;
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(newTable);
	}
	
	//插入数据
	size_t hashi = hs(kv.first) % _table.size();//表中的位置
	Node* newNode = new Node(kv);//新节点
	//头插法
	newNode->_next = _table[hashi];
	_table[hashi] = newNode;
	++_n;
	return true;
}

2.1.6 删除

删除某个元素,首先得查找该元素是否存在。如果不存在返回false;存在,通过哈希函数计算该元素的位置(是哪个桶的),得到该位置的头指针(第一个节点),然后遍历单链表,找到后删除。

遍历的过程中要注意两种可能:

  1. 要删除的节点是第一个节点
  2. 要删除的节点是中间某个节点或者最后一个节点
bool Erase(const K& key)
{
	Hash hs;
	Node* ret = Find(key);
	if (ret)
	{
		size_t hashi = hs(ret->_kv.first) % _table.size();//先找到表的位置
		Node* cur = _table[hashi];//得到该位置的头节点
		Node* prev = nullptr;//前面的节点连接
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev)//第一个节点不是要删除的
				{
					prev->_next = cur->_next;
				}
				else
				{
					_table[hashi] = cur->_next;
				}
				delete cur;
				break;//找到具体节点删除后跳出
			}
			prev = cur;
			cur = cur->_next;
		}
		return true;
	}
	else//找不到,删除失败
	{
		return false;
	}
}

2.2 kv模型哈希桶源代码

#include <iostream>
#include <vector>
#include <string>
using namespace std;

//仿函数
template<class K>
struct HashFind
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

template<>
struct HashFind<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto& e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

//节点
template<class K, class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		, _next(nullptr)
	{}
};


template<class K, class V, class Hash = HashFind<K>>
class HashTable
{
	typedef HashNode<K,V> Node;
public:

	//构造
	HashTable(size_t n = 5)
	{
		_table.resize(n, 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;
		}
	}

	//查找
	Node* Find(const K& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _table.size();//表中的位置
		Node* cur = _table[hashi];//得到当前位置头节点
		while (cur)
		{
			if (cur->_kv.first == key)//找到了
			{
				return cur;
			}
			cur = cur->_next;
		}
		//cur为空,不存在这个数据
		return nullptr;
	}

	//插入
	bool Insert(const pair<K,V>& kv)
	{
		//重复元素不能插入
		if (Find(kv.first))
		{
			return false;
		}
		Hash hs;
		//扩容
		if (_n == _table.size())
		{
			vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间
			//将旧表的节点移到新表中,再交换
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					size_t hashi = hs(_table[i]->_kv.first) % newTable.size();
					Node* next = cur->_next;
					cur->_next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
			_table.swap(newTable);
		}
		
		//插入数据
		size_t hashi = hs(kv.first) % _table.size();//表中的位置
		Node* newNode = new Node(kv);//新节点
		//头插法
		newNode->_next = _table[hashi];
		_table[hashi] = newNode;
		++_n;
		return true;
	}

	//删除
	bool Erase(const K& key)
	{
		Hash hs;
		Node* ret = Find(key);
		if (ret)
		{
			size_t hashi = hs(ret->_kv.first) % _table.size();//先找到表的位置
			Node* cur = _table[hashi];//得到该位置的头节点
			Node* prev = nullptr;//前面的节点连接
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev)//第一个节点不是要删除的
					{
						prev->_next = cur->_next;
					}
					else
					{
						_table[hashi] = cur->_next;
					}
					delete cur;
					break;//找到具体节点删除后跳出
				}
				prev = cur;
				cur = cur->_next;
			}
			return true;
		}
		else//找不到,删除失败
		{
			return false;
		}
	}
	
private:
	vector<Node*> _table;
	size_t _n = 0;
};

三、改造哈希桶

前面的哈希桶的数据类型是固定的kv模型,为了后面方便模拟实现unordered_set和unordered_map,将数据类型改成T,这个T可以是key,也可以是kv。到底是哪个取决于使用的是unordered_set还是unordered_map,用的是unordered_set,模板就用unordered_set的仿函数,另一个同理。

template<class T>
struct HashNode
{
	HashNode<T>* _next;
	T _data;
	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}
};

Find的返回值修改为迭代器,Insert的返回值修改为pair< iterator, bool>

3.1 begin+end

1️⃣begin
遍历哈希表,一旦遇到有节点的位置,返回该位置的迭代器。迭代器需要传两个参数,该位置的头指针和当前哈希表(与后面实现迭代器类中的成员变量对应)

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

2️⃣end
返回的是最后一个节点的下一个位置,单链表的最后一个节点的下一个是空指针,所以返回迭代器,参数传的是空指针和当前哈希表。

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

3.2 迭代器

虽然哈希表的结构用的是vector,但是每个位置是单链表,++操作不能像数组一样就行,所以要对每个节点的迭代器进行封装,方便++找到下一个节点。

注意:每个位置的桶是单链表,所以没有前置- -

模板参数有KeyOfT和Hash是因为前置++需要数据类型的仿函数和转换为无符号整数的仿函数,这里先说明一下。迭代器的成员有节点应该就可以了,为什么还要哈希表类变量?因为等会前置++的代码中要通过哈希函数计算节点数据的位置,哈希函数要有哈希表的空间大小,没有哈希表类成员,怎么得到哈希表的空间大小。

其他的与之前一样,不作重复叙述了

template<class K, class T, class KeyOfT, class Hash>
struct HashIterator
{
	typedef HashNode<T> Node;
	typedef HashTable<K, T, KeyOfT, Hash> Ht;
	typedef HashIterator< K, T, KeyOfT, Hash> Self;

	Node* _node;
	Ht* _ht;

	HashIterator(Node* node, Ht* ht)
		:_node(node)
		,_ht(ht)
	{}

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

	Self& operator++()
	{
		//....
	}

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

3.2.1 前置++

分为两种情况:

  1. 当前节点的下一个节点不为空
  2. 当前节点的下一个节点为空

1️⃣情况一:
在这里插入图片描述
直接到下一个节点即可。

2️⃣情况二:
在这里插入图片描述

下一个节点为空,先计算当前节点在哈希表的位置(具体哪个桶),然后在该位置往后开始找,只要遇到有节点的位置,下一个节点就是它;如果后面都没有节点,下一个节点就是空。

Self& operator++()
{
	if (_node->_next)
	{
		_node = _node->_next;
	}
	else
	{
		Hash hs;
		KeyOfT kot;
		size_t hashi = hs(kot(_node->_data)) % _ht->_table.size();
		hashi++;
		while (hashi < _ht->_table.size())
		{
			if (_ht->_table[hashi])
			{
				_node = _ht->_table[hashi];
				break;
			}
			hashi++;
		}
		if (_ht->_table.size() == hashi)
		{
			_node = nullptr;
		}
	}
	return *this;
}

3.3 改造后哈希桶与迭代器源代码

#include <iostream>
#include <vector>
#include <string>
using namespace std;

//仿函数
template<class K>
struct HashFind
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

template<>
struct HashFind<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto& e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

//节点
template<class T>
struct HashNode
{
	HashNode<T>* _next;
	T _data;
	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}
};

//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//正向迭代器
template<class K, class T, class KeyOfT, class Hash>
struct HashIterator
{
	typedef HashNode<T> Node;
	typedef HashTable<K, T, KeyOfT, Hash> Ht;
	typedef HashIterator< K, T, KeyOfT, Hash> Self;

	Node* _node;
	Ht* _ht;

	HashIterator(Node* node, Ht* ht)
		:_node(node)
		,_ht(ht)
	{}

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

	Self& operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(kot(_node->_data)) % _ht->_table.size();
			hashi++;
			while (hashi < _ht->_table.size())
			{
				if (_ht->_table[hashi])
				{
					_node = _ht->_table[hashi];
					break;
				}
				hashi++;
			}
			if (_ht->_table.size() == hashi)
			{
				_node = nullptr;
			}
		}
		return *this;
	}

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

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	template<class K, class T, class KeyOfT, class Hash>
	friend struct HashIterator;

	typedef HashNode<T> Node;
public:
	typedef HashIterator <K, T, KeyOfT, Hash> iterator;

	//迭代器
	iterator begin()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i])
			{
				return iterator(_table[i], this);
			}
		}
		return end();
	}
	iterator end()
	{
		return iterator(nullptr, this);
	}

	//构造
	HashTable(size_t n = 5)
	{
		_table.resize(n, 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;
		}
	}

	//查找
	iterator Find(const K& key)
	{
		Hash hs;
		KeyOfT kot;
		size_t hashi = hs(key) % _table.size();//表中的位置
		Node* cur = _table[hashi];//得到当前位置头节点
		while (cur)
		{
			if (kot(cur->_data) == key)//找到了
			{
				return iterator(cur, this);
			}
			cur = cur->_next;
		}
		//cur为空,不存在这个数据
		return end();
	}

	//插入
	pair<iterator, bool> Insert(const T& data)
	{
		KeyOfT kot;
		//重复元素不能插入
		iterator pos = Find(kot(data));
		if (pos != end())
		{
			return make_pair(pos, false);
		}
		Hash hs;
		//扩容
		if (_n == _table.size())
		{
			vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间
			//将旧表的节点移到新表中,再交换
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					size_t hashi = hs(kot(_table[i]->_data)) % newTable.size();
					Node* next = cur->_next;
					cur->_next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
			_table.swap(newTable);
		}

		//插入数据
		size_t hashi = hs(kot(data)) % _table.size();//表中的位置
		Node* newNode = new Node(data);//新节点
		//头插法
		newNode->_next = _table[hashi];
		_table[hashi] = newNode;
		++_n;
		return make_pair(iterator(newNode, this), true);
	}

	//删除
	bool Erase(const K& key)
	{
		Hash hs;
		KeyOfT kot;
		iterator ret = Find(key);
		if (ret != end())
		{
			size_t hashi = hs(kot(ret._node->_data)) % _table.size();//先找到表的位置
			Node* cur = _table[hashi];//得到该位置的头节点
			Node* prev = nullptr;//前面的节点连接
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev)//第一个节点不是要删除的
					{
						prev->_next = cur->_next;
					}
					else
					{
						_table[hashi] = cur->_next;
					}
					delete cur;
					break;//找到具体节点删除后跳出
				}
				prev = cur;
				cur = cur->_next;
			}
			return true;
		}
		else//找不到,删除失败
		{
			return false;
		}
	}

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

四、模拟实现unordered_set

unordered_set是k模型,它的仿函数就返回key。模板参数有key和Hash,Hash是用来转换key变成无符号整数的。其他接口调用哈希桶的即可。

#include "HashTable.h"
namespace yss
{
	template <class K, class Hash = HashFind<K>>
	class unordered_set
	{
		//仿函数
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:
		typedef typename HashTable<K, const K, SetKeyOfT, HashFind<K>>::iterator iterator;

		//迭代器
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		
		//插入
		pair<iterator, bool> Insert(const K& key)
		{
			return _ht.Insert(key);
		}

		//查找
		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}

		//删除
		bool Erase(const K& key)
		{
			return _ht.Erase(key);
		}

	private:
		HashTable<K, const K, SetKeyOfT, Hash> _ht;
	};
}

在这里插入图片描述

typename的作用是把HashTable<K, const K, SetKeyOfT, HashFind< K >>::iterator当做一个类型,而不是变量。typedef就是重命名。

五、模拟实现unordered_map

unordered_map是kv模型,仿函数传的是kv中的key。除了查找、插入、删除外,还有operator[],可以进行统计元素等操作

#include "HashTable.h"
namespace yss
{
	template <class K, class V, class Hash = HashFind<K>>
	class unordered_map
	{
		//仿函数
		struct MapKeyOfT
		{
			const K& operator()(const pair<K,V>& kv)
			{
				return kv.first;
			}
		};

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

		//迭代器
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}

		//插入
		pair<iterator, bool> Insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		//查找
		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}

		//删除
		bool Erase(const K& key)
		{
			return _ht.Erase(key);
		}

		//operator[]
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = Insert(make_pair(key, V()));
			return ret.first->second;
		}

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

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

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

相关文章

【C语言】strcmp 的使⽤和模拟实现

前言 这篇文章将要带我们去实现模拟一个strcmp函数 首先我们要知道strcmp函数的定义 strcmp()定义和用法 我们先看一下strcmp在cplusplus网站中的定义 链接: link int strcmp ( const char * str1, const char * str2 );比较两个字符串将 C 字符串 str1 与 C 字符串 str2 …

pin脚的缺陷检测

忍不住 我才是最大的缺陷首先应该学好表达头脑风暴分割paddledetection小目标检测也不行缺陷检测1.缺陷标注修改代码为自己的数据集训练训练结果结果图片 结论再次出发 我才是最大的缺陷 真的&#xff0c;我真的被整无语了。测测测测&#xff0c;测个鬼。一天天的净整些没用的…

国内ip地址推荐,畅享网络新体验!

在数字化时代&#xff0c;IP地址不仅是网络连接的基石&#xff0c;也是互联网产业发展的重要标志。国内作为全球互联网市场的重要参与者&#xff0c;拥有众多IP地址资源。虎观代理小二旨在探索并推荐一些国内IP地址&#xff0c;分析它们的价值所在&#xff0c;并探讨如何更好地…

数据结构和算法:搜索

二分查找 二分查找&#xff08;binary search&#xff09; 是一种基于分治策略的高效搜索算法。它利用数据的有序性&#xff0c;每轮缩小一半搜索范围&#xff0c;直至找到目标元素或搜索区间为空为止。 给定一个长度为 &#x1d45b; 的数组 nums &#xff0c;元素按从小到大…

PTA L2-036 网红点打卡攻略

一个旅游景点&#xff0c;如果被带火了的话&#xff0c;就被称为“网红点”。大家来网红点游玩&#xff0c;俗称“打卡”。在各个网红点打卡的快&#xff08;省&#xff09;乐&#xff08;钱&#xff09;方法称为“攻略”。你的任务就是从一大堆攻略中&#xff0c;找出那个能在…

精品凉拌菜系列热卤系列课程

这一系列课程涵盖精美凉拌菜和美味热卤菜的制作技巧。学员将学习如何选材、调味和烹饪&#xff0c;打造口感丰富、色香俱佳的菜肴。通过实践训练&#xff0c;掌握独特的烹饪技能&#xff0c;为家庭聚餐或职业厨艺提升增添亮点。 课程大小&#xff1a;6.6G 课程下载&#xff1…

【C语言进阶篇】编译和链接

【C语言进阶篇】编译和链接 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C语言&#x1f353; &#x1f33c;文章目录&#x1f33c; 编译环境与运行环境 1. 翻译环境 2. 编译环境&#xff1a;预编译&#xff08;预处理&#xff09;编…

docker关闭全部运行容器命令是什么?

环境&#xff1a; docker v22.1 问题描述&#xff1a; docker关闭全部运行容器命令是什么&#xff1f; 解决方案&#xff1a; 要关闭所有正在运行的Docker容器&#xff0c;可以使用如下命令&#xff1a; docker stop $(docker ps -a -q)这条命令首先执行 docker ps -a -q…

C语言从入门到实战----数据在内存中的存储

1. 整数在内存中的存储 在讲解操作符的时候&#xff0c;我们就讲过了下⾯的内容&#xff1a; 整数的2进制表⽰⽅法有三种&#xff0c;即 原码、反码和补码 有符号的整数&#xff0c;三种表⽰⽅法均有符号位和数值位两部分&#xff0c;符号位都是⽤0表⽰“正”&#xff0c;⽤…

LeetCode:547. 省份数量(并查集 Java)

目录 547. 省份数量 题目描述&#xff1a; 实现代码与解析&#xff1a; 原理思路&#xff1a; 547. 省份数量 题目描述&#xff1a; 有 n 个城市&#xff0c;其中一些彼此相连&#xff0c;另一些没有相连。如果城市 a 与城市 b 直接相连&#xff0c;且城市 b 与城市 c 直接…

MySQL 高级语句(二)

一、子查询 1.1 相同表子查询 1.2 不同表/多表子查询 1.3 子查询的应用 1.3.1 语法 1.3.2 insert 子查询 1.3.3 update 子查询 1.3.4 delete 子查询 1.4 exists 关键字 1.4.1 true 1.4.2 false 1.5 as别名 二、视图 2.1 视图和表的区别和联系 2.1.1 区别 2.1.2 …

详细描述红黑树如何左旋、右旋(图文结合)

红黑树 首先要理解二叉查找树 二叉查找树&#xff08;BST&#xff09;具备什么特性呢&#xff1f; 左子树上所有结点的值均小于或等于它的根结点的值。 右子树上所有结点的值均大于或等于它的根结点的值。 左、右子树也分别为二叉排序树。 二叉查找树是二分查找的思想&…

使用IDEA的反编译插件 反编译jar包

反编译插件介绍 安装IDEA后, 一般自带反编译插件, Java Bytecode Decompiler 如果没有可以自己安装下 1.首先找到插件的jar包, 在IDEA安装目录的plugins文件夹下 D:\IntelliJ IDEA 2021.2.2\plugins\java-decompiler\lib 2.运行java命令, 指定插件的jar包目录和你要反编译的ja…

【Hexo + Github 搭建自己的专属博客】

目录 一、前提环境配置 1. 安装Git和NodeJS 2. 安装Hexo 3. 加载主题 4. 修改主题配置 二、搭建博客 1. 将博客部署在GitHub上 2. 写文章并上传 3. 配置一些特效 三、最终成果 ​编辑 一、前提环境配置 1. 安装Git和NodeJS 在 Windows 上使用 Git &#xff0c;可以…

OpenCV模块熟悉:点云处理相关

1. 显示--VIZ 曾经基于PCL 做过不少点云相关的开发&#xff0c;采样VTK进行有点云显示。后来基于OpenCV做了不少三维重建工作&#xff0c;总是将点云保存下来&#xff0c;然后借助CloudCompare等查看结果。如果能够将VIZ编译进来&#xff0c;预计会提升开发速度。 …

一文搞懂大疆机场kmz航线和图新地球导出的kmz的区别

0序&#xff1a; 近期有用户问“ 把KML文件放到图新后&#xff0c;想转出来KMZ&#xff08;大疆的机场用的格式&#xff09;但是转出来的KMZ显示格式不对 ” 之前只是知道大疆的航线规划采用的是kml规范&#xff0c;但具体是什么样并不清楚。就这这个问题把这个事情给弄明白。…

【问题处理】蓝鲸监控-数据断点解决

本文来自腾讯蓝鲸智云社区用户&#xff1a;fadewalk 在问答社区看到有小伙伴在落地蓝鲸的过程中出现监控平台的grafana面板数据断点问题&#xff0c;往往出现这种问题&#xff0c;都比较的头疼。 如果将CMDB&#xff08;配置管理数据库&#xff09;比作运维的基石&#xff0c;…

【Leetcode】2580. 统计将重叠区间合并成组的方案数

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 给你一个二维整数数组 ranges &#xff0c;其中 ranges[i] [starti, endi] 表示 starti 到 endi 之间&#xff08;包括二者&#xff09;的所有整数都包含在第 i 个区间中。 你需要…

文献阅读笔记(Transformer)

文献阅读笔记&#xff08;Transformer&#xff09; 摘要Abstract1、文献阅读1.1 文献题目1.2 文献摘要1.3 研究背景1.4 模型架构1.4.1 Encoder-Decoder1.4.2 注意力机制1.4.3 多头注意力1.4.4 Position-wise Feed-Forward Networks1.4.5 Embeddings and Softmax1.4.6 Positiona…

应对Locked勒索病毒威胁:你的数据安全准备好了吗?

导言&#xff1a; .Locked勒索病毒&#xff0c;作为一种新型的恶意软件&#xff0c;已经在全球范围内引起了广泛的关注。这种病毒通过加密受害者的文件&#xff0c;并要求支付赎金以获取解密密钥&#xff0c;从而实现对受害者的勒索。本文旨在深入解析.Locked勒索病毒的特点、…