【C++ —— 哈希】哈希原理 | 哈希表的模拟实现 | 模拟实现封装unordered_map和unordered_set


前言

本文参考文档:https://legacy.cplusplus.com/reference/unordered_map/


一、unordered系列关联式容器

1.1 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的迭代器(无序set和map的迭代器都是单向迭代器)

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

4.unordered_map的元素访问

函数功能函数介绍
operator[]返回key值对应的val值(没有默认值)

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

5.unordered_map查询

函数功能函数介绍
find()查询key值是否存在,存在返回key值对应的迭代器的位置,返回key在哈希桶中的位置
count返回哈希桶中关键码为key的键值对的个数

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

在这里插入图片描述
6. unordered_map的修改操作

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

7.unordered_map的桶操作

函数功能函数介绍
size_t bucket_count()const返回哈希桶中桶的总个数
size_t bucket_size(size_t n)const返回n号桶中有效元素的总个数
size_t bucket(const K& key)返回元素key所在的桶号
1.2 unordered_set

unordered_set接口根map差不多一样,不过set只存key值且不能修改
这里就不过多赘述了,详情可自行观看文档
set文档资料

二、底层结构

2.1哈希概念(哈希是一种算法思想)

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

理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素

哈希/散列:映射 一个值和另一个值建立关系。
哈希表/散列表: 映射 关键字和存储位置建立一个关系。

2.2哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址该种现象称为哈希冲突
或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

2.3 解决哈希冲突方法:
1.直接定址法(值和位置关系是唯一关系,每个人都有唯一位置,值很分散,直接定址法会导致空间开很大,资源的浪费)

在这里插入图片描述

2.闭散列
2.1 开放地址法/线性探测

当前位置被占用,在开放空间里按某种规则,找一个没有被占用的位置存储

  1. 线性探测 hashi + i(i >= 0)
  2. 二次探测 hashi + i^2(i >= 0)
    在这里插入图片描述
    多定义一个负载因子,存储有效关键字个数
    当有效关键字个数超过表大小的6~8成就再次扩容,用此种方法可以有效的减少哈希冲突的次数。
    以空间换效率。

    注意:
    负载因子太多:会影响效率。
    负载因子太少:会浪费空间。
    代码实现:
	enum Stutas

	{
		//删除状态的意义:
		//1.再插入,这个位置可以覆盖
		//2.防止后面冲突的值,出现找不到的情况,遇到删除状态还要继续向后寻找
		DELETE,
		EMPTY,
		EXIST

	};
	template<class K,class V>
	struct HashData
	{
		pair<K,V> _kv;
		Stutas _s = EMPTY;
	};


	//开放地址法
	//线性查找,插入
	template<class K>
	struct Hashfunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	//struct HashfuncString
	//{
	//	//BKDR算法
	//	size_t operator()(const string& key)
	//	{
	//		size_t hash = 0;
	//		//把他们的ascll码值加起来
	//		for (auto e : key)
	//		{
	//			hash += e;
	//		}
	//		return hash;
	//	}
	//};
	template<>
	struct Hashfunc<string>
	{

		size_t operator()(const string& key)
		{
			size_t hash = 0;
			//把他们的ascll码值加起来
			for (auto e : key)
			{
				hash += e;
			}
			return hash;
		}
	};

	template<class K,class V, class Hash = Hashfunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);//resize也直接会开初始化size的大小。reserve之会开capecity的大小

		}
		//提供find解决insert能够插入相同key的问题
		
		HashData<K, V>* Find(const K& key)
		{
			Hash ht;
			size_t hashi = ht(key) % _tables.size();
			while (_tables[hashi]._s != EMPTY)
			{
				if (_tables[hashi]._s == EXIST && _tables[hashi]._kv.first == key)
				{
					//找到return找到位置的地址
					//没找到return空
					return &_tables[hashi];
				}
				hashi++;
				//hashi超出table的size大小就给他%回来
				hashi = hashi % _tables.size();
			}

			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			Hash ht;
			if (Find(kv.first))
			{
				return false;
			}
			if ((double)_n / _tables.size() >= 0.7)
			{
				//不能直接扩容size的二倍,还要转移数据,重新映射关系(扩容之后值和空间的位置关系已经乱了)
				//释放旧空间
				size_t newsize = _tables.size() * 2;
				HashTable<K,V,Hash> newHT;
				newHT._tables.resize(newsize);
				//遍历旧表,插入新表
				for ( size_t i= 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);

			}
			//size_t hashi = kv.first % _tables.size();//key不一定是整形,如果是string呢?用仿函数把kv.first的值转化整形值
			//先用字符串映射一个整形
			size_t hashi = ht(kv.first) % _tables.size();

			while (_tables[hashi]._s == EXIST)
			{
				hashi++;
				//hashi超出table的size大小就给他%回来
				hashi = hashi % _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._s = EXIST;
			++_n;

		}
		bool Erase(const K& key)
		{
			HashData<K,V>* ret =  Find(key);
			//find找到会返回找到位置的地址
			if (ret)
			{
				ret->_s = DELETE;
				--_n;
				return true;
			}
			return false;
		}
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._s == EXIST)
				{
					//cout << i << "->";
					cout <<"["<<i<<"]->"<< _tables[i]._kv.first << ":"<< _tables[i]._kv.second << endl;
					/*printf("[%d]->%d\n", i, _tables[i]._kv.first);
					printf("")*/
				}
				else if (_tables[i]._s == EMPTY)
				{
					printf("[%d]->\n", i);
				}
				else 
				{
					printf("[%d]->DELETE\n", i);
				}

			}
			cout << endl;
		}
	private:
		vector<HashData<K,V>> _tables;
		size_t _n = 0;//存储的关键字个数
	};
	

3.开散列
1. 哈希桶/拉链法

哈希桶的平均时间复杂度为O(1)

思路概念:
我们把每个hashi映射的位置,替换成list,链接每次插入的冲突的新key

可以参考下图:如4位置都是冲突的关键字。
在这里插入图片描述

2.字符串映射问题

整形还是可以用除留余数法计算key映射的位置,但是例如string这种字符串呢
我们可以使用仿函数,实现类型的转换。
例如把字符串的每个字符的ascll码值加起来。
代码实现:

template<class K>
	struct Hashfunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	struct HashfuncString
	{
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			//把他们的ascll码值加起来
			for (auto e : key)
			{
				hash += e;
			}
			return hash;
		}
	};

但是这种转化函数会有一些缺陷,例如abcacb这两个不同的字符串ascll码值加起来都是相同的,导致了映射位置冲突。
有没有什么办法能解决这类的问题呢?
有大佬专门研究出了很多种算法我们可以参考:
字符串哈希算法
例如:
在这里插入图片描述
这类算法会减少字符串转哈希算法的冲突。但是肯定不能完全避免。
我们也可以模仿大佬算法完善我们的代码:

struct HashfuncString
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		//把他们的ascll码值加起来
		for (auto e : key)
		{
			hash *= 31;//在加ascll码值前*31/131
			hash += e;
		}
		return hash;
	}
};

这种方式也是类似库里面unordered_map的实现方式(要传一个仿函数)
在这里插入图片描述
但是库里面其实比我们实现的更好一些,
库里面不用传第三个模板参数直接取模,
库里面的unordered_map用string做key可以直接转化为整型。

std::unordered_map<string,string> dict;

我们可以用特化模板参数来模拟实现这总功能。

template<class K>
struct Hashfunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct Hashfunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		//把他们的ascll码值加起来
		for (auto e : key)
		{
			hash += e;
		}
		return hash;
	}
};

利用哈希桶实现哈希表代码:

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

		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};
	template<class K>
	struct Hashfunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	
	template<>
	struct Hashfunc<string>
	{
		//BKDR算法
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			//把他们的ascll码值加起来
			for (auto e : key)
			{
				hash *= 31;//在加ascll码值之前*31/131...
				hash += e;
			}
			return hash;
		}
	};
	template<class K,class V ,class Hash = Hashfunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10);
		}
		~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;
			}
		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			
			Hash ht;
			//扩容用开放地址法的扩容方法会产生大量的资源浪费
			//可以考虑直接把旧表的数据挪动下来
			if (_n == _tables.size())
			{
				vector<Node*> newtable;
				newtable.resize(_tables.size() * 2, nullptr);
				//遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						//挪动旧表的数据映射到新表
						Node* next = cur->_next;
						size_t hashi = ht(cur->_kv.first) % newtable.size();
						cur->_next = newtable[i];
						newtable[i] = cur;

						//利用cur把节点挪动到新表
						//cur利用next返回到旧表以此循环
						cur = next;

						
					}
					_tables[i] = nullptr;
					//旧表这个桶数据已经被挪动完,记得制空。
					//以防后面析构有问题
				}
				_tables.swap(newtable);

			}
			
			//先算出数据要存入哪个桶
			size_t hashi = ht(kv.first) % _tables.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}
		Node* Find(const K& key)
		{
			Hash ht;
			size_t hashi = ht(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return NULL;
		}
		bool Erase(const K& key)
		{
			Hash ht;

			size_t hashi = ht(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;

			}
			return false;
		}
		void Some()
		{
			size_t bucketsize = 0;
			size_t maxbucketlen = 0;
			size_t sumbucketlen = 0;
			double averagebucketlen = 0;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur == nullptr)
				{
					++bucketsize;
				}
				size_t bucketlen = 0;
				while (cur)
				{
					++bucketlen;
					cur = cur->_next;
				}
				sumbucketlen += bucketlen;
				if (bucketlen > maxbucketlen)
				{
					maxbucketlen = bucketlen;
				}
				

			}
			averagebucketlen = (double)sumbucketlen / (double)bucketsize;
			printf("bucketsize:%d\n", bucketsize);
			printf("maxbucketlen:%d\n", maxbucketlen);
			printf("averagebucketlen:%lf\n", averagebucketlen);
			
		}
		
	private:
		vector<Node*> _tables;
		size_t _n = 0;

	};

三、unordered_map和unordered_set封装哈希表模拟实现

1.UnOrdered_map.h

#include"HashTable.h"

namespace goat
{
	template<class K,class V, class Hash = Hash_Bucket::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;


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

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

	private:

		Hash_Bucket::HashTable<K, pair<const K,V> ,MapKeyOfT ,Hash> _mp;
	};
}

2.UnOrdered_set.h

#include"HashTable.h"

namespace goat
{
	template<class K ,class Hash = Hash_Bucket::Hashfunc<K>>
	class unordered_set
	{

		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}

		};
	public:
		typedef typename Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
		typedef typename Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;

		/*iterator begin() 
		{
			return _st.begin();
		}
		iterator end() 
		{
			return _st.end();
		}*/

		const_iterator begin() const
		{
			return _st.begin();
		}
		const_iterator end() const
		{
			return _st.end();
		}

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

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


		//返回值pair要实现operator[]再去修改
		pair<iterator, bool> insert(const K& key)
		{
			//return _st.Insert(key);//返回普通迭代器,但是这里insert接受的是const迭代器
								   //正常解决方法1:支持const迭代器转化成普通迭代器,方法二:下一层Insert使用节点的指针返回(指针会产生隐式类型转换为迭代器
								   //但是这里unordered迭代器有三个值不能使用方法二
								   //iterator(cur , this ,hashi)
			//这里用另外一种简单的方法
			auto ret = _st.Insert(key);
			return pair<iterator, bool>(iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);


			
		}

	private:
		
		Hash_Bucket::HashTable<K, K , SetKeyOfT, Hash> _st;
	};
	
}

3.HashTable.h

#include<iostream>
#include<vector>
#include<utility>
#include<list>
#include<set>

using namespace std;

namespace Hash_Bucket
{
	template<class T>
	struct HashNode
	{

		HashNode<T>* _next;
		T _data;

		//库里面的unordered是实现插入顺序遍历的
		//要单独维护一个链表
		//HashNode<T>* _linknext;
		//HashNode<T>* _linkprev;
		
		HashNode(const T& data)
			:_data(data)
			,_next(nullptr)
		{}
	};
	template<class K>
	struct Hashfunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	
	template<>
	struct Hashfunc<string>
	{
		
		size_t operator()(const string& key)
		{
			size_t hash = 0;
		
			for (auto e : key)
			{
				hash *= 31;
				hash += e;
			}
			return hash;
		}
	};
	//解决互相依赖方法一
	//前置声明(不用给缺省参数)
	//因为hashiterator和hashtable是互相依赖的关系

	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;

	template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	struct __HashIterator
	{
		typedef HashNode<T> Node;
		typedef __HashIterator<K, T,Ref,Ptr, KeyOfT, Hash> Self;

		Node* _node;
		const HashTable<K, T, KeyOfT, Hash>* _pht;
		//解决互相依赖方法二
		//实际上哈希迭代器需要的是:
		//vector<Node*>* _ptb;


		//1.记录当前桶的所在位置
		size_t _hashi;

		__HashIterator(Node* node,const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
			: _node(node)
			, _pht(pht)
			, _hashi(hashi)
		{}
		
		Self& operator++()
		{
			if (_node->_next)
			{
				//桶还有节点,就走下一个节点
				_node = _node->_next;
			}
			else
			{
				//当前桶已经走完了,需要下一个桶的开始
				//传个哈希表过来
				2.直接算出当前所在桶位置
				//KeyOfT kot;
				//Hash hf;
				//size_t hashi = hf(kot(_node->_data)) % _pht._tables.size();
				++_hashi;


				//_tables是私有成员变量
				//可以考虑友元
				while (_hashi < _pht->_tables.size())
				{
					if (_pht->_tables[_hashi])
					{
						_node = _pht->_tables[_hashi];
						break;
					}
					else
					{
						_hashi++;
					}
				}
				if (_hashi == _pht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &_node->_data;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;

		}
	};
	//unordered_set -> HashTable<K,K>
	//unordered_map -> HashTable<K,pair<K,V>>
	template<class K,class T , class KeyOfT,class Hash = Hashfunc<K>>
	class HashTable
	{

		template<class K, class T,class Ref ,class Ptr ,class KeyOfT, class Hash>
		friend struct __HashIterator;


		typedef HashNode<T> Node;
	public:
		typedef __HashIterator<K, T, T&,T* , KeyOfT, Hash> iterator;
		typedef __HashIterator<K, T, const T&,const T* , KeyOfT, Hash> const_iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return iterator(_tables[i] ,this , i);
				}
			}
			return end();
		}
		iterator end()
		{
			return iterator(nullptr, this, -1);
		}
		const_iterator begin() const //const修饰this-> HashTable<K, T, KeyOfT, Hash>* 
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return const_iterator(_tables[i], this, i);
				}
			}
			return end();
		}
		const_iterator end() const
		{
			return const_iterator(nullptr, this, -1);
		}



		HashTable()
		{
			_tables.resize(10);
		}
		~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;
			/*if (Find(kot(data)))
			{
				return false;
			}*/
			iterator it = Find(kot(data));
			if(it != end())
			{
				return make_pair(it, false);
			}
			
			Hash ht;
			
			//扩容用开放地址法的扩容方法会产生大量的资源浪费
			//可以考虑直接把旧表的数据挪动下来
			if (_n == _tables.size())
			{
				vector<Node*> newtable;
				newtable.resize(_tables.size() * 2, nullptr);
				//遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						//挪动旧表的数据映射到新表
						Node* next = cur->_next;
						size_t hashi = ht(kot(cur->_data)) % newtable.size();
						cur->_next = newtable[i];
						newtable[i] = cur;

						//利用cur把节点挪动到新表
						//cur利用next返回到旧表以此循环
						cur = next;

						
					}
					_tables[i] = nullptr;
					//旧表这个桶数据已经被挪动完,记得制空。
					//以防后面析构有问题
				}
				_tables.swap(newtable);

			}
			
			//先算出数据要存入哪个桶
			size_t hashi = ht(kot(data)) % _tables.size();
			//头插
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode,this,hashi),true);
		}
		iterator Find(const K& key)
		{
			Hash ht;
			KeyOfT kot;

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

			size_t hashi = ht(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;

			}
			return false;
		}
		void Some()
		{
			size_t bucketsize = 0;
			size_t maxbucketlen = 0;
			size_t sumbucketlen = 0;
			double averagebucketlen = 0;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur == nullptr)
				{
					++bucketsize;
				}
				size_t bucketlen = 0;
				while (cur)
				{
					++bucketlen;
					cur = cur->_next;
				}
				sumbucketlen += bucketlen;
				if (bucketlen > maxbucketlen)
				{
					maxbucketlen = bucketlen;
				}
				

			}
			averagebucketlen = (double)sumbucketlen / (double)bucketsize;
			printf("bucketsize:%d\n", bucketsize);
			printf("maxbucketlen:%d\n", maxbucketlen);
			printf("averagebucketlen:%lf\n", averagebucketlen);
			
		}
		
	private:
		vector<Node*> _tables;
		size_t _n = 0;

	};
	

	
}
  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值