【C++】哈希(2万字)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

unordered系列关联式容器

unordered_map

unordered_map的文档介绍

unordered_map的接口说明

unordered_set

底层结构

哈希概念

哈希冲突

哈希函数

哈希冲突解决

闭散列

线性探测的实现并改造

二次探测

开散列

开散列概念

开散列实现并改造 + 迭代器的实现

开散列增容

开散列与闭散列比较

不同的类型转换成整型的操作

MyOrderedMap.h

MyOrderedSet.h

哈希的应用

位图

位图概念

位图的实现

位图应用

布隆过滤器

布隆过滤器提出

布隆过滤器概念

布隆过滤器的插入

布隆过滤器的查找

布隆过滤器删除

布隆过滤器优点

布隆过滤器缺陷

布隆过滤器的面试题

哈希切割

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2 N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好 的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同本文中只对unordered_map和unordered_set进行介绍, unordered_multimap和unordered_multiset学生可查看文档介绍。

unordered_map

unordered_map的文档介绍

unordered_map文档介绍

  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭 代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问 value。
  6. 它的迭代器至少是前向迭代器。

unordered_map的接口说明

1. unordered_map的构造

函数声明功能介绍
unordered_map构造不同格式的unordered_map对象

2. unordered_map的容量

函数声明功能介绍
bool empty() const检测unordered_map是否为空
size_t size() const获取unordered_map的有效元素个数

3. unordered_map的迭代器

函数声明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
cbegin返回unordered_map第一个元素的const迭代器
cend返回unordered_map最后一个元素下一个位置的const迭代器

4. unordered_map的元素访问

函数声明功能介绍
operator[]返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中, 将key对应的value返回。

5. unordered_map的查询

函数声明功能介绍
iterator find(const K& key)返回key在哈希桶中的位置
size_t count(const K& key)返回哈希桶中关键码为key的键值对的个数

注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1

6. unordered_map的修改操作

函数声明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
void clear()清空容器中有效元素个数
void swap(unordered_map&)交换两个容器中的元素

7. unordered_map的桶操作

函数声明功能介绍
size_t bucket count()const返回哈希桶中桶的总个数
size_t bucket size(size_t n) const返回n号桶中有效元素的总个数
size_t bucket(const K& key)返回元素key所在的桶号

unordered_set

unordered_set文档介绍

底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity;capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

哈希冲突

对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,但有:Hash($k_i$) == Hash($k_j$),即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。

比如:5、25、45分别去%20,映射的位置都是5。

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则:

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

常见哈希函数:

1. 直接定址法--(常用)一一映射

  • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况
  • 使用场景:适合查找比较小且连续的情况

2. 除留余数法--(常用)

  • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

3. 平方取中法--(了解)

  • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
  • 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
  • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法--(了解)

  • 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。
  • 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法--(了解)

  • 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。
  • 通常应用于关键字长度不等时采用此法

6. 数学分析法--(了解)

  • 设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。例如:

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

哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

1. 线性探测

比如下图中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

插入

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

查找

  • i = key % 表的大小  
  • 如果i为不是要查找的key值,就线性往后查找,直到找到或者遇到空,如果找到表的结尾位置,还没有找到key值,要往头回绕。

删除

  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
线性探测的实现并改造
// 开放定址法
namespace open_address
{
	// 状态
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData  // 类模板名:哈希表的数据是结构体的变量(数据和状态)
	{
		pair<K, V> _kv;
		State _state = EMPTY; 
		// 标记默认初始化为空,一旦存进去值,标记为存在,删除值之后,标记位删除
	};

	template<class K>
	struct HashFunc // 仿函数:将key转换成整型
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;// 不传参数三,默认将key强转成整型
		}
	};

	// 特化 ---> 在实践当中string经常做key,所以做特化
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto e : s)
			{
				hash += e;
				hash *= 131;
			}

			return hash;
		}
	};

	// stoi:只有阿拉伯的字符串数字"1224546"才能用stoi;像"比特"就不能用stoi

	// 将字符串强制转换成整型
	//struct HashFuncString
	//{
	//	size_t operator()(const string& s)
	//	{
	//		// "abcd"
	//		// "bcad"
	//		// "aadd"
	//		size_t hash = 0;
	//		for (auto e : s) 
	//		{
	//          // 将字符串中的每个字符ascll码值加起来
	//			hash += e;
	//			hash *= 131;// 这样可以避免ascll码值相加相等的情况
	//		}
	//
	//		return hash;
	//	}
	//};

	// 参数三:默认缺省的仿函数Hash,没有传确定的仿函数,就用缺省的发仿函数HashFunc<K>
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable // 类模板名:哈希表
	{
	public:
		HashTable(size_t size = 10)
		{
			_tables.resize(size);// 使用resize的话,size和capcacity就相等了
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash hs; // 仿函数的对象
			// 线性探测
			size_t hashi = hs(key) % _tables.size();

			while (_tables[hashi]._state != EMPTY)
			{
				if (key == _tables[hashi]._kv.first
					&& _tables[hashi]._state == EXIST)
				{
					return &_tables[hashi];
				}

				++hashi;// 如果++超出size,则取模从头再来
				hashi %= _tables.size();
			}

			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			// 如果已经有了,就返回false
			if (Find(kv.first))
				return false;

			// 扩容的问题  不强制类型转换成double的话,会有7/10==0的情况
			//if ((double)_n / (double)_tables.size() >= 0.7)
			if (_n * 10 / _tables.size() >= 7)
			{
				// 方法一:
				//size_t newSize = _tables.size() * 2;
				// 不能在原表的空间上扩容空间,因为这样会使映射关系混乱
				//vector<HashData> newTables(newSize); // 需要重新开辟一块新空间
				 遍历旧表,重新映射到新表,那么就得此处再次写一遍线性探测的代码,再让两个表交换一下
				....
				//_tables.swap(newTables);

				// 方法二:
				HashTable<K, V, Hash> newHT(_tables.size() * 2);
				// 遍历旧表,插入到新表
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
						// 这里新表调用Insert()函数,并不会陷入死循环,因为空间*2倍之后,不会再次进入if判断条件了
						// 直接复用线性探测的代码
					}
				}
				_tables.swap(newHT._tables);// 交换两表,那么旧表出了作用域就会调用析构函数,旧表数据会被释放
			}

			Hash hs;
			// 线性探测
			size_t hashi = hs(kv.first) % _tables.size(); // 除和取模都不能除或取模0
			// 这里要模取的是size,而不是capacity;假设表中的capacity和size是不一样的,
            // 放值是需要[]的,[]会检查i < size,如果值放在模capacity的那块区间,超出size会越界;
            // 所以只能放值在size区间处,放在size和capacity区间,则越界。
			while (_tables[hashi]._state == EXIST) // 此位置状态为存在
			{
				++hashi;
				hashi %= _tables.size();// 模上一个size,走到尾之后,从头再来
			}
			// 此位置状态为空或被删除
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n; // 实际数据个数+1

			return true;
		}

		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				_n--;
				ret->_state = DELETE; // 直接改状态就相当于删除了
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;  // 实际存储的数据个数
	};

思考:哈希表什么情况下进行扩容?如何扩容?

  • 哈希冲突越多,效率就越低。
  • 负载因子/载荷因子 = 实际存进去数据个数/表的大小。
  • 闭散列(开放定址法):负载因子一般会控制在0.7左右。

线性探测优点:实现非常简单。

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法 为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i = 1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。

对于下图中如果要插入44,产生冲突,使用解决后的情况为:

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

开散列

开散列概念

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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列实现并改造 + 迭代器的实现
template<class K>
struct HashFunc // 仿函数:将key转换成整型
{
	size_t operator()(const K& key)
	{
		return (size_t)key;// 不传参数三,默认将key强转成整型
	}
};

// 特化 ---> 在实践当中string经常做key,所以做特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}

		return hash;
	}
};
// 哈希桶
namespace hash_bucket
{
	// T -> K
	// T -> pair<K, V>
	template<class T>
	struct HashNode
	{
		HashNode<T>* _next;
		T _data;

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


	// 编译器有一个原则:先定义或先声明,再使用。
	// 在使用一个变量、类型、函数,要先定义或先声明,再使用。因为编译器为了提高编译速度,有一个原则,
	// 比如:在使用一个变量、类型或函数时,编译器只会向上找,不会向下找,只向上找,编译速度会快很多。
	// 下面__HTIterator类模板中使用了HashTable<K, T, KeyOfT, Hash>,在上面没有HashTable的定义,
	// 所以编译器会报错,因为编译器不认识HashTable。
	// 类里面是不受影响的,因为类里面的规则,是在整个类域里面进行查找,编译器把类域当成一个整体。

	// 那我们如果把整个HashTable类模板放在__HTIterator类模板之前,也会有问题,
	// 因为HashTable类模板中也使用了__HTIterator类型,这个地方就是一个经典的互相引用。
	// 那么这时候就只能增加一个前置声明

	// 前置声明(声明中不能有缺省值)
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;

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

		Node* _node;
		HT* _ht;

		__HTIterator(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
			{
				// 当前桶走完了,找下一个桶
				KeyOfT kot;
				Hash hs;
				// _tables是HashTable的私有,所以_tables无法使用。我们可以采用友元的方法
				size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
				// 找下一个桶
				hashi++;
				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}

					hashi++;
				}

				// 后面没有桶了
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}

			return *this;
		}

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

	// 参数三:仿函数,对于set来说,返回key;对于map来说,返回pair<key,value>中的key
	// 参数四:转换成整型的仿函数
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable
	{
		// 迭代器想要使用哈希表,就得把迭代器变成哈希表的友元
		template<class K, class T, class KeyOfT, class Hash>
		friend struct __HTIterator;// 普通类的友元,只有这一行代码;类模板的友元,得把模板参数声明一下

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

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				// 找到第一个桶的第一个节点
				if (_tables[i])
				{
					// this就是哈希表对象的地址
					return iterator(_tables[i], this);
				}
			}
			// 找不到返回空
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);// 调用的是__HTIterator的构造函数
		}

		HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 0;
		}
		// 这里析构的是表中所挂的哈希桶中的节点;vector出了作用域之后会自己调用析构函数
		// 哪怕我们自己显示写了析构函数,自定义类型出了作用域也会显示调用析构
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

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

			// 此时Find()函数返回的是迭代器,不能转换成bool值,所以要拿迭代器进行比较
			// 之前Find()函数返回的是节点的指针,可以隐式类型转换成bool值
		/*	if (Find(kot(data)) != end())
				return false;*/

			iterator it = Find(kot(data));
			if (it != end())
				return make_pair(it, false);

			Hash hs;

			// 负载因子到1就扩容
			if (_n == _tables.size())
			{
				// 创建一个新表
				vector<Node*> newTables(_tables.size() * 2, nullptr);// 调用HashTable的构造函数
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 取出旧表中节点,重新计算挂到新表桶中
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;// 保存下一个节点

						// 头插到新表
						size_t hashi = hs(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;// 查看下一个节点应该挂到那个桶中
					}

					_tables[i] = nullptr;// 将旧表置空
				}

				_tables.swap(newTables);// 交换两表之后,旧表出了作用域就被释放掉
			}

			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newnode = new Node(data);

			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n;
			return make_pair(iterator(newnode, this), true);
		}

		iterator Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}

				cur = cur->_next;
			}

			return iterator(nullptr, this);
		}

		bool Erase(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					// 删除
					if (prev) // 不是桶中的第一个节点
					{
						prev->_next = cur->_next;
					}
					else // 是桶中的第一个节点
					{
						_tables[hashi] = cur->_next;
					}

					delete cur;

					--_n;
					return true;
				}

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

			return false;
		}
	private:
		vector<Node*> _tables; // 指针数组
		size_t _n;
	};
}
开散列增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可 以给哈希表增容。

开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

不同的类型转换成整型的操作

struct Date
{
	int _year;
	int _month;
	int _day;
};

// 将日期类转换成整型
struct HashFuncDate
{
	// 2024/6/3
	// 2024/3/6
	size_t operator()(const Date& d)
	{
		size_t hash = 0;
		hash += d._year;
		hash *= 131;

		hash += d._month;
		hash *= 131;

		hash += d._day;
		hash *= 131;

		return hash;
	}
};
struct Person
{
	string _name;
	string _id;   // 身份证号码
	string _tel;
	int _age;
	string _class;

	string _address;  // 
	//...
};


struct HashFuncPerson
{
	// 2024/6/3
	// 2024/3/6
	size_t operator()(const Person& p)
	{
		size_t hash = 0;
		for (auto e : p._id)
		{
			hash += e;
			hash *= 131;
		}

		return hash;
	}
};

MyOrderedMap.h

#include"HashTable.h"

namespace bit
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		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, Hash>::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);
		}

		// Map要把[]实现出来,就得解决insert(),[]的本质就是insert()
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}

		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

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

	void test_map1()
	{
		unordered_map<string, string> dict;
		dict.insert(make_pair("sort", ""));
		dict.insert(make_pair("left", ""));
		dict.insert(make_pair("right", "?"));

		for (auto& kv : dict)
		{
			//kv.first += 'x';
			kv.second += 'y';

			cout << kv.first << ":" << kv.second << endl;
		}
	}
}

MyOrderedSet.h

#include"HashTable.h"

namespace bit
{
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;

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

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

		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}

		pair<iterator, bool> find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

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

	void test_set1()
	{
		unordered_set<int> us;
		us.insert(3);
		us.insert(1);
		us.insert(5);
		us.insert(15);
		us.insert(45);
		us.insert(7);

		unordered_set<int>::iterator it = us.begin();
		while (it != us.end())
		{
			//*it += 100;
			cout << *it << " ";
			++it;
		}
		cout << endl;

		int x = 0;
		cin >> x;
		if (us.find(x) != us.end())
		{
			cout << "找到了" << endl;
		}
		else
		{
			cout << "没有找到" << endl;
		}

		for (auto e : us)
		{
			cout << e << " ";
		}
		cout << endl;
	}

}
int a[10];// 静态数组
// 动态数组:malloc或new出来的数组是动态数组

哈希的应用

位图

位图概念

面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 位图解决 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。比如:

位图概念

  • 所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。

位图的实现

namespace bit
{
	// 用一个非类型模板参数来控制位图要开多大(位图是存在于数组里面的)
	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			// 假如:N是50个比特位,50除以32是1个整型,还有18个比特位没有开出来,所以要向上取整
			// 多开一个整型
			_bits.resize(N / 32 + 1, 0);
			//cout << N << endl;
		}

		// 把x映射的位标记成1
		void set(size_t x)
		{
			assert(x <= N);// x不能超出N

			size_t i = x / 32;// 计算x在第几个整型上
			size_t j = x % 32;// 计算x在这个整型的第几个位上

			_bits[i] |= (1 << j);
		}

		// 把x映射的位标记成0
		void reset(size_t x)
		{
			assert(x <= N);

			size_t i = x / 32;
			size_t j = x % 32;

			_bits[i] &= ~(1 << j);
		}
		// 检测x映射的标记位是1还是0
		bool test(size_t x)
		{
			assert(x <= N);

			size_t i = x / 32;
			size_t j = x % 32;

			return _bits[i] & (1 << j);
		}
	private:
		vector<int> _bits;
	};

	void test_bitset()
	{
		bitset<100> bs1;
		bs1.set(50);
		bs1.set(30);
		bs1.set(90);

		for (size_t i = 0; i < 100; i++)
		{
			if (bs1.test(i))
			{
				cout << i << "->" << "在" << endl;
			}
			else
			{
				cout << i << "->" << "不在" << endl;
			}
		}
		bs1.reset(90);
		bs1.set(91);

		cout << endl << endl;

		for (size_t i = 0; i < 100; i++)
		{
			if (bs1.test(i))
			{
				cout << i << "->" << "在" << endl;
			}
			else
			{
				cout << i << "->" << "不在" << endl;
			}
		}

		// 这三种方式都可以开42亿9千万个位图大小的空间
		bitset<-1> bs2;
		bitset<UINT_MAX> bs3;
		bitset<0xffffffff> bs4;
	}

位图应用

  1. 快速查找某个数据是否在一个集合中
  2. 排序 + 去重
  3. 求两个集合的交集、并集等
  4. 操作系统中磁盘块标记

给定100亿个整数,设计算法找到只出现一次的整数?

思路:出现1次和1次以上的整数需要两个比特位:00 ---> 0次;01 ---> 1次;10 ---> 2次及以上。

代码展示:

template<size_t N>
class two_bit_set
{
public:
	void set(size_t x)
	{
		// 00 -> 01
		if (_bs1.test(x) == false
			&& _bs2.test(x) == false)
		{
			_bs2.set(x);
		}
		// 01 -> 10
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			_bs1.set(x);
			_bs2.reset(x);
		}
	}

	//int test(size_t x)
	//{
	//	if (_bs1.test(x) == false
	//		&& _bs2.test(x) == false)
	//	{
	//		return 0;
	//	}
	//	else if (_bs1.test(x) == false
	//		&& _bs2.test(x) == true)
	//	{
	//		return 1;
	//	}
	//	else
	//	{
	//		return 2; // 2次及以上
	//	}
	//}
	bool test(size_t x)
	{
		if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			return true;
		}

		return false;
	}
private:
	bitset<N> _bs1;// 自定义类型的对象会去调用它的构造函数
	bitset<N> _bs2;
};

void test_bitset2()
{
	int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
	two_bit_set<100> bs;
	for (auto e : a)
	{
		bs.set(e);
	}

	for (size_t i = 0; i < 100; i++)
	{
		//cout << i << "->" << bs.test(i) << endl;
		if (bs.test(i))
		{
			cout << i << endl;
		}
	}
}

给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

思路:分别set到两个位图,同时为1的就是交集。

1G内存是够的,100亿个整数,并不需要100亿个比特位,因为整数最多42亿9千万个,所以说映射的位图只需要42亿9千万个位,42亿9千万个比特位换算成1G,两个0.5G就是1G。

 1GB是2的30次方,是10亿字节,100亿字节是10G,那么100亿个整型是40G。

代码展示:

void test_bitset3()
{
	int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
	int a2[] = { 5,3,5,99,6,99,33,66 };

	bitset<100> bs1;
	bitset<100> bs2;

	for (auto e : a1)
	{
		bs1.set(e);
	}

	for (auto e : a2)
	{
		bs2.set(e);
	}

	for (size_t i = 0; i < 100; i++)
	{
		// 寻找交集
		if (bs1.test(i) && bs2.test(i))
		{
			cout << i << endl;
		}
	}
}

位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

内存当中一般是存不下这些值,这些值都是存在文件里面的。位图不是开40亿,而是按照范围来开的(42亿9千万),因为它的范围是无符号的整数,(0~2^32-1)。

采用两个比特位:00 ---> 0次;01 ---> 1次;10 ---> 2次;11 ---> 3次及以上

给定100亿个整数,只有512M,需要在512M内存中设计算法找到只出现一次的整数?

因为1G是10亿字节,1G是2^30,1G是42亿9千万个比特位,整数的范围最大才到42亿9千万,所以100亿个整数中有大量是重复的数字,所以要在512M内存中查找只出现一次的整数,可以让42亿9千万个整数分成两份,因为512M是是42亿9千万个比特位的一半。

先查找前一半,再查找后一半,映射的过程中就是去重的过程。

布隆过滤器

布隆过滤器提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉 那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用 户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那 些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理 了。
  3. 将哈希与位图结合,即布隆过滤器

布隆过滤器概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆过滤器的插入

#pragma once
#include<bitset>
#include<string>

struct HashFuncBKDR
{
	// BKDR
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};

struct HashFuncAP
{
	// AP
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (size_t i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0) // 偶数位字符
			{
				hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
			}
			else              // 奇数位字符
			{
				hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
			}
		}

		return hash;
	}
};

struct HashFuncDJB
{
	// DJB
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash = hash * 33 ^ ch;
		}

		return hash;
	}
};
// 参数三:三个哈希仿函数的个数,表示一个值能映射3个位
template<size_t N,
	class K = string,
	class Hash1 = HashFuncBKDR,
	class Hash2 = HashFuncAP,
	class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		// 比如:插入第一个数,映射0~M-1的比特位区间
		// 一个值要映射到三个比特位上,为了减少冲突
		size_t hash1 = Hash1()(key) % M;
		size_t hash2 = Hash2()(key) % M;
		size_t hash3 = Hash3()(key) % M;

		_bs->set(hash1);
		_bs->set(hash2);
		_bs->set(hash3);
	}

	// 这里不需要写reset()删除函数,因为删除百度,腾讯判断也可能不在了。因为百度和腾讯可能会映射到同一个位置

	bool Test(const K& key)
	{
		// 值映射的三个比特位上,只要有一个比特位为0,就是该值不在哈希表中
		size_t hash1 = Hash1()(key) % M;
		if (_bs->test(hash1) == false)
			return false;

		size_t hash2 = Hash2()(key) % M;
		if (_bs->test(hash2) == false)
			return false;

		size_t hash3 = Hash3()(key) % M;
		if (_bs->test(hash3) == false)
			return false;

		return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
	}

private:
	// const size_t M = 10 * N;
	// 我们不能用这种成员变量,因为这个成员变量是属于对象的,只是声明,没有空间,只在初始化列表才会初始化

	// 加一个静态static就可以了,那么这个变量就在静态区,就不属于对象了,而是属于整个类
	// N:比特位。插入一个整数,也就是一个整数映射一个比特位,比特位扩容10倍的N
	static const size_t M = 10 * N; // 想降低误判率:可以增大比特位的空间
	bit::bitset<M> _bs;
	
	// 如果就是想要使用库里面的bitset,可以new在堆区开辟一个std::bitset<M>类型的空间,将空间的地址给_bs
	//std::bitset<M>* _bs = new std::bitset<M>;
};

// 库里面的stl::bitset<M>类型所开辟的空间是开在对象里面的,这个对象是一个静态数组
// 我们自己用vector<>实现的bitset是调用resize()函数开辟空间是在堆上的

void TestBloomFilter1()
{
	string strs[] = { "百度","字节","腾讯" };// 中文是由多个字符构成的
	BloomFilter<10> bf;
	for (auto& s : strs)
	{
		bf.Set(s);
	}

	for (auto& s : strs)
	{
		cout << bf.Test(s) << endl;
	}

	for (auto& s : strs)
	{
		cout << bf.Test(s + 'a') << endl;
	}

	cout << bf.Test("摆渡") << endl;
	cout << bf.Test("百渡") << endl;
}

布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特 位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为 零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。

比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其 他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

每个位置改成多个位的引用计数就可以支持。比如:一个映射位置给8个bit标记,但是这样空间的消耗就大了。

布隆过滤器优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无 关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

布隆过滤器缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再 建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

布隆过滤器的面试题

给两个文件,分别有100亿个query(字符串),我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法?

小文件在找交集是没有误判的,因为已经读到内存当中了,不需要在使用布隆过滤器,直接将文件中的数据放到底层为哈希表或红黑树的容器中。

之前的算法要用布隆过滤器,因为数据在数据库中,都去数据库中查找太慢了,所以用布隆过滤,会效率高。

哈希切割

给一个超过100G大小的log file, log中存着IP地址,设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

如果是top K ,就自己建立一个小堆,默认是大堆,我们还得写一个仿函数,因为不能用pair<string,int>类型比,我们要用pair<string,int>类型中的second来进行比较,控制成一个K个数的小堆。

海量数据问题特征:数据量大,内存存不下。

  1. 先考虑具有特点的数据结构能否解决?比如:位图、堆、布隆过滤器等。
  2. 大事化小思路。哈希切分(不能平均切分),切小以后,放到内存中能处理。

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

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

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

相关文章

【康耐视国产案例】Nvidia/算能+智能AI相机:用AI驱动 | 降低电动车成本的未来之路

受环保观念影响、政府激励措施推动与新能源技术的发展&#xff0c;消费者对电动汽车(EV)的需求正在不断增长&#xff0c;电动汽车已经成为了未来出行方式的重要组成部分。然而&#xff0c;电动汽车大规模取代燃油汽车的道路还很漫长。最大的障碍就是电动汽车的售价相对过高。尽…

新设立湖北投资管理公司流程和要求

在湖北投资管理企业进行注册时&#xff0c;需要准备一系列的材料并按照一定的流程进行办理。本文将从注册材料及注册流程两方面来介绍&#xff0c;帮助您了解注册投资管理企业的步骤和所需的具体材料。详情致电咨询我或者来公司面谈。 新注册材料要求: 企业名称申请书:要求提供…

vue路由跳转之【编程式导航与传参】

vue路由有两种跳转方式 ----> 编程式与声明式&#xff0c;本文重点讲解vue路由的【编程式导航 】【编程式导航传参 ( 查询参数传参 & 动态路由传参 ) 】等内容&#xff0c;并结合具体案例让小伙伴们深入理解 &#xff0c;彻底掌握&#xff01;创作不易&#xff0c;需要的…

汇编:调用C函数

在32位汇编程序中可以调用C函数&#xff1b;这种做法在很多情况下是有用的&#xff0c;尤其是在汇编程序需要与C代码进行交互或利用C语言的库函数时。下面是一些情况下使用汇编调用C函数的常见情景&#xff1a; ①优化性能&#xff1a;某些特定的任务可能用汇编语言编写更有效率…

[Linux]重定向

一、struct file内核对象 struct file是在内核中创建&#xff0c;专门用来管理被打开文件的结构体。struct file中包含了打开文件的所有属性&#xff0c;文件的操作方法集以及文件缓冲区&#xff08;无论读写&#xff0c;我们都需要先将数据加载到文件缓冲区中。&#xff09;等…

POP —— Nodes DOP

POP Advect by Volumes —— 使用速度场驱动粒子 此节点被设计为更容易通过流体来驱动粒子&#xff0c;通常流体被单独模拟并从磁盘上读取速度场&#xff1b;此操作将修改force、vel、P属性&#xff1b; Update Force&#xff0c;调整粒子的加速度&#xff0c;类似POP Force&a…

RAID技术迭代、原理对比、产品梳理(HCIA)

目录 一、RAID技术迭代 传统RAID LUN虚拟化2.0 工作原理&#xff1a; 块虚拟化2.0 为什么有RAID2.0&#xff1f; RAID2.0实现原理&#xff1a; RAID-TPRAID 7 华为RAID-TP技术 RAID的4种工作状态 RAID算法 普通RAID算法 华为动态RAID算法 保险箱盘&#xff08;存掉…

el-table中的信息数据过长 :show-overflow-tooltip=‘true‘**

可以在 el-table-column中添加 :show-overflow-tooltip‘true’

数字孪生仿真渲染引擎EasyTwin全新升级,焕新、多元、优质、高效一步到位!

EasyTwin作为数字孪生仿真渲染引擎&#xff0c;自2023年进入公测以来&#xff0c;致力于实现低成本零代码操作。在今年年初&#xff0c;我们重新回归业务场景&#xff0c;将产品定位从“融合渲染”转变为“仿真渲染”&#xff0c;面向数字孪生仿真渲染领域推出全新版本&#xf…

Optional类

一、概述 泛型类、java8引进的、java.util包里 二、作用 解决空指针异常带来的不便 三、做法 将对象封装为一个Optional对象&#xff0c;如果封装的对象为空&#xff08;即该对象不存在&#xff09;&#xff0c;可以使用默认值和或者执行默认操作 四、方法 1、empty() 创…

【Qt秘籍】[006]-Qt 的 Hello World程序-编程第一步

"Hello,World!" 中文意思是“你好&#xff0c;世界”。 因为 The C Programming Language 中使用它做为第一个演示程序&#xff0c;后来很多程序员在学习编程或进行设备调试时延续了这一习惯。 下面&#xff0c;我们也将演示Qt中的"Hello World!" 我们先创…

【脚本篇】---spyglass lint脚本

目录结构 sg_lint.tcl &#xff08;顶层&#xff09; #1.source env #date set WORK_HOME . set REPORT_PATH ${WORK_HOME}/reports puts [clock format [clock second] -format "%Y-%m-%d %H:%M:%S"] #2.generate source filelist #3.set top module puts "##…

Ehcache Java 缓存框架

详解 下图是 Ehcache 在应用程序中的位置&#xff1a; Ecache 是一个广泛使用的 Java 缓存框架&#xff0c;能够有效提升应用性能&#xff0c;并减少与后端数据库的交互次数。它采用了一系列高级缓存策略&#xff0c;包括内存缓存、磁盘缓存、分布式缓存等&#xff0c;并提供了…

战略合作 | 竹云赋能雁塔区数字经济高质量发展

2024年5月30日&#xff0c;由西安市数据局指导&#xff0c;中共西安市雁塔区委、西安市雁塔区人民政府主办的 “雁塔区企业数字化转型发展大会” 在西安开幕。 本次活动以“数智雁塔&#xff0c;引领未来”为主题&#xff0c;特邀业内150余位政府、数字化服务企业、传统行业企…

木叶飞舞之【机器人ROS2】篇章_第三节、给turtlebot3安装realsense深度相机

我们做视觉slam时会用到深度相机&#xff0c;但是gazebo的turtlebot3中只有rgb相机&#xff0c;没有深度&#xff0c;因此本节会修改代码&#xff0c;来给我们的小乌龟增加一个rgbd相机。 效果展示 发布topic如下图 图片大小都是640*480 1. 修改model.sdf文件 1.1 路径位置…

idea项目一直在build

IDEA项目一直在build的原因可能包括构建进程堆大小过小、缓存问题、依赖包下载缓慢或网络问题。12 构建进程堆大小过小&#xff1a;如果IDEA的构建进程堆大小设置得不够大&#xff0c;可能会导致构建过程缓慢或卡顿。解决方法是将构建进程堆大小参数扩大&#xff0c;例如将700…

Pont在小程序开发的使用

Pont是一个很好的前后端桥&#xff0c;但是有个问题。默认产生的代码&#xff0c;无法支持微信小程序开发。根本原因是因为使用了window给全局的对象注入了API和refs属性&#xff0c;由于小程序没有window属性&#xff0c;当然就无法使用了&#xff0c;解决办法也比较简单。只需…

618适合入手哪些数码好物?实用数码好物清单分享,错过拍烂大腿!

在一年一度的618购物狂欢节里&#xff0c;许多数码爱好者们都在这次盛大的购物盛宴中觅得心仪的数码好物&#xff0c;数码产品不仅改变了我们的生活方式&#xff0c;更让我们享受到了前所未有的便捷和乐趣&#xff0c;那么在这个618&#xff0c;哪些数码好物值得我们入手呢&…

Vulnhub项目:doubletrouble

1、靶机地址 靶机地址&#xff1a;doubletrouble: 1 ~ VulnHubdoubletrouble: 1, made by tasiyanci. Download & walkthrough links are available.https://vulnhub.com/entry/doubletrouble-1,743/ 靶机介绍&#xff1a;看这个名字&#xff0c;就觉得内有玄机&#xff…

git随记

git status 查看文件状态 git status -s 比较简洁的查看文件状态。如下代表此文件是新建的&#xff0c;没有被git跟踪的文件&#xff1a; $ git status -s ?? abc.txtgit add abc.txt 将abc添加到暂存区。后再次git status -s $ git status -s A abc.txtgit reset 将暂存…