一.概念

哈希表就是为了解决查找必须遍历的问题而生

一般哈希表有两种形式

二.特点

1.线性探测表

1.1取余法

a.原理

index = key % capacity;

  • index:存放数据的位置下标
  • key:存放数据的key值
  • capacity:存放数据的空间大小

通过元素的关键码(key)与元素储存位置(index)建立一一映射的函数叫哈希函数

b.举例

现在有6个数据:2,18,22,23

2%10 = 2,所以2存放在下标为2的位置

18%10 = 8,所以18存放在下标为8的位置

 22%10 = 2,所以22存放在下标为2的位置

但是,现在下标为2的位置已经有了一个值,这就是哈希冲突

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

这个位置冲突了,怎么办?那就放到后面没有冲突的位置。

下标为3的位置没有冲突,所以,放到3

23%10 = 3,所以23存放在下标为3的位置

但是,下标为3的位置已经有数据了。即所谓哈希冲突了。

很简单,往后挪,挪到没有冲突的位置。

所以,往后下标4的位置没有数据,所以23存在下标为4的位置

那么,如何取出数据呢?

取18,将18%10=8,然后到下标为8的位置取即可。

但是,22%10=2,但是22却不在下标为2的位置。往后找。

23呢?也不在下标为3的位置。还是往后找。

 如果当前位置找不到值,就往后挨个查找,直到找到数据;遇到无数据存放的位置也停止查找。

往后找,就像是探测,而且是一个一个探测,是线性的查找。

这就是线性探测

所以,我们把这个表,就叫做线性探测表

1.2负载因子

随着数据的增多,冲突的概率增加,查找的成本越来越高。访问一个数据,老是要往后遍历,效率低。总的来说,问题源于数据太多,而空间不够。

这时候需要扩容。

用负载因子判断什么时候扩容

负载因子就是数据个数所占整个空间的比率

(eg.10个空间,有2个数据,负载因子就是0.2)

而一般来说,负载因子达到0.7就要进行扩容。

2.哈希桶

2.1意义

线性探测形式的哈希表有着致命的弱点:无法对余数相同的数据进行处理

冲突了,只有往后放。即我的位置冲突了,我就去放别人的位置,让别人冲突

2.2结构


 

哈希桶将冲突的值,放到同一个位置下,用单链表管理起来

极大的优化了线性探测表无法处理哈希冲突的缺点

同时,单链表的访问,其时间复杂度也控制在O(n)的量级

2.3扩容

哈希桶也存在负载因子的问题,和线性探测负载因子是一样的逻辑

同样,也是大于0.7就进行扩容

3.闭散列与开散列

3.1闭散列

闭散列:开放定址法(线性探测/二次探测)

二次检测:当数据比较集中的时候,查找会比较慢

补充:为了更快的查找,下一个位置查找偏移量不为1,可以为2次方

思路:我的位置被别人占了,我就去占别人得位置

特点:冲突越多,效率越低

因此,有人提出了开散列

3.2开散列

开散列:哈希桶/拉链法

4.减少哈希冲突的方法

哈希表的大小为素数,可以减少哈希冲突

以下为相差大约2倍的素数
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
};

三.设计hash表

1.闭散列的设计

1.0增容

a.直接增容

 建新的数组=>拷贝=>释放旧空间

//增容法一
//1.建新的数组
vector<HashData<T>> newtables;
size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
newtables.resize(newsize);
//2.拷贝
for (size_t i = 0; i < _tables.size(); ++i)
{
				if (_tables[i]._state == EXIST)
				{
					size_t index = koft(_tables[i]._data) % newsize;
					while (newtables[index]._state == EXIST)
					{
						index++;
						if (index == newtables.size())
						{
							index = 0;
						}
					}
					newtables[index] = _tables[i];
				
				}
				
}
//3.释放旧空间
_tables.swap(newtables);

b.交换法

建新的哈希表=>复用Insert的单个插入的代码=>交换哈希表中的容器指针

//增容法二
//建新的哈希表
HashTable<K, T, KOfT> newht;
size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
newht._tables.resize(newsize);
for (size_t i = 0; i < _tables.size(); i++)
{
				if (_tables[i]._state == EXIST)
				{
                    //复用插入代码
					newht.Insert(_tables[i]._data);
				}
}
_tables.swap(newht._tables);

1.1插入

思路: 

如果位置不被占,插入;如果位置被占,遇到空才插入 

插入逻辑:

记录Hash表中数据的数量,插入前计算负载因子,若大于0.7就先增容

通过哈希函数找到映射位置,按照规则进行插入

1.2查找

遇到空位置才停止,但是如果中间位置因为删除存在有空位置,就查不到后面的位置,如何解决?

设置一个位置状态:EMPTY(空)、DELETE(删除),EXIST(存在)                                                     

 a.修改后的插入:

  • 若映射位置状态为EMPTY或DELETE,直接插入
  • 若映射位置状态为EXIST,向后探测,在状态为EMPTY或DELETE的位置上插入
  • (若探测到最后一个位置,就重新从第一个位置开始重新探测)

 注:若遇到状态为EXIST且值相同的位置,则插入失败

b.修改后的查找:

从映射位置开始,向后查找,直到状态为EMPTY的位置停止 

(若探测到最后一个位置,就重新从第一个位置开始重新探测)

  • 若找到相同的值,检查其状态:
  • 若状态为DELETE,则查找失败
  • 若状态为EXIST,则查找成功,返回当前位置节点的指针

1.3删除

删除的值不必抹除,而是将状态设置为DELETE

(如果抹除,设置为什么值都不合适)

 因此,删除是一个惰性删除,查找值时依旧能找到

1.4代码

//设置状态
enum State
{
	EMPTY,
	EXIST,
	DELETE
};

//设置节点内容
template<class T>
struct HashData
{
	T _data;
	State _state;
};

//设置仿函数,方便取出元素的关键码key
template<class K>
struct KeyOfSet
{
	const K& operator()(const K& k)
	{
		return k;
	}
};

//unordered_set<K,K> -> HashTable<K,K,KOfSet>
//unordered_map<K,pair<K,V>> -> HashTable<K,pair<K,V>,KOfMap>
template<class K, class T, class KOfT>
class HashTable
{
public:
	bool Insert(const T& data)
	{
        //仿函数实例化
		KOfT koft;
		//负载因子=表中数据量/表的大小 衡量哈希表满的程度
		//表越满,插入数据越容易冲突,效率越低
		//负载因子达到0.7左右就开始增容
		if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
		{
            //增容法一
            //1.建新的数组
            vector<HashData<T>> newtables;
            size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
            newtables.resize(newsize);
            //2.拷贝
            for (size_t i = 0; i < _tables.size(); ++i)
            {
				if (_tables[i]._state == EXIST)
				{
					size_t index = koft(_tables[i]._data) % newsize;
					while (newtables[index]._state == EXIST)
					{
						index++;
						if (index == newtables.size())
						{
							index = 0;
						}
					}
					newtables[index] = _tables[i];
				
				}
				
            }
            //3.释放旧空间
             _tables.swap(newtables);

			//增容法二
			//建新的哈希表
			HashTable<K, T, KOfT> newht;
			size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
			newht._tables.resize(newsize);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
                    //复用Insert的单个插入的代码
					newht.Insert(_tables[i]._data);
				}
			}
			_tables.swap(newht._tables);
		}

		//计算data在表中映射的位置
		size_t index = koft(data) % _tables.size();
		while (_tables[index]._state == EXIST)
		{
			if (koft(_tables[index]._data) == koft(data))
			{
				return false;
			}
			index++;
			if (index == _tables.size())
			{
				index = 0;
			}
		}
        //插入
		_tables[index]._data = data;
		_tables[index]._state = EXIST;
		_num++;
		return true;
	}

    //查找
	T* Find(const K& key)
	{
		KOfT koft;
		//计算key在表中映射的位置
		size_t index = key % _tables.size();
		while (_tables[index]._state != EMPTY)
		{
			if (koft(_tables[index]._data) == key)
			{
				if (_tables[index]._state == EXIST)
				{
					return &_tables[index]._data;
				}
				else if (_tables[index]._state == DELETE)
				{
					return nullptr;
				}
			}

			index++;
			if (index == _tables.size())
			{
				index = 0;
			}
		}
		return nullptr;
	}

    //删除
	bool Erase(const K& key)
	{
		HashData* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			_num--;
			return true;
		}
		else
		{
			return false;
		}

	}
private:
	vector<HashData<T>> _tables;
	size_t _num = 0;//存储的有效数据的数量
};

2.开散列的设计

2.1结构

表中存放单链表指针

若数据太多也可存放红黑树的根节点

2.2功能

  • 插入:判断是否需要增容,链表头插
  • 查找:遍历链表
  • 删除:删除链表中的节点
  • 清理:删除链表

2.3哈希函数

a.仿函数

哈希函数是将元素的关键码(key)与元素储存位置(index)建立一一映射关系的仿函数                     

(通过重载operator(),达到像函数一样的效果)

一般情况下,对于整型、浮点型、指针都进行size_t强转为整型,就可以取%,找到位置               

但是,对于某些特殊的类型,常见的有string不可以转化为整型

b.哈希函数(特殊的仿函数)

下面讲解将string转化成整型的常见的哈希算法(仿函数的特化),而其他不能转化成整形的类型

需要自己写出特化的仿函数

将每一个字符的ASCII值*某个数值,累计加,得到整型

其中一种算法是乘以131,研究发现不同字符串得出的数字重复较少

//哈希算法
template<class K>
struct _Hash
{
	const K& operator()(const K& k)
	{
		return k;
	}
};
//特化string类
template<>
struct _Hash<string>
{
	size_t operator()(const string& str)
	{
		//转化后的数字
		size_t hash = 0;
		//BKDR Hash
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash = hash * 131 + str[i];
		}
		return hash;
	}
};

2.4代码

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

//哈希算法
template<class K>
struct _Hash
{
	const K& operator()(const K& k)
	{
		return k;
	}
};
//特化string类
template<>
struct _Hash<string>
{
	size_t operator()(const string& str)
	{
		//转化后的数字
		size_t hash = 0;
		//BKDR Hash
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash = hash * 131 + str[i];
		}
		return hash;
	}
};

//哈希表
template<class K,class T,class KOfT,class Hash =_Hash<K>>
class HashTable
{
public:
	typedef HashNode<T> Node;
	//析构函数
	~HashTable()
	{
		Clear();
	}
	//清理
	void Clear()
	{
		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;
		}

	}
    
    //哈希函数
	//由于某些类型的数据不能直接取模,所以需要仿函数将它转为整形
	size_t HashFunc(const K& k)
	{
		Hash hash;
		return hash(k);
	}


	//插入数据
	bool Insert(const T& data)
	{
		KOfT koft;
		//如果负载因子大于1,则增容
		if (_tables.size() == _num)
		{
			vector<Node*> newtables;
			size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
			newtables.resize(newsize);
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				//将旧表中的节点取下,重新插入到新表中
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					//头插
					size_t index = HashFunc(koft(cur->_data)) % _tables.size();
					cur->_next = _tables[index];
					_tables[index] = cur;

					cur = next;
				}
				//防止原表中出现野指针
				_tables[i] = nullptr;
			}
			_tables.swap(newtables);
		}

		//插入
		//计算映射位置
		size_t index = HashFunc(koft(data))%_tables.size();
		//先查找是否在表中
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == koft(data))
			{
				return false;
			}
			else
			{
				cur = cur->_next;
			}
		}
		//不在表中,头插到挂着的链表中
		Node* newNode = new Node(data);
		newNode->_next = _tables[index];
		_tables[index] = newNode;
		_num++;
		return true;
	}

	//查找
	Node* find(const K& key)
	{
		KOfT koft;
		size_t index = HashFunc(key) % _tables.size();
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				return cur;
			}
			else
			{
				cur = cur->_next;
			}
		}
		return nullptr;
	}

	//删除
	bool Erase(const K& key)
	{
		KOfT koft;
		size_t index = HashFunc(key) % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				if (prev == nullptr)
				{
					//表示要删的为第一个节点
					_tables[index] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}

private:
	vector<Node*> _tables;
	size_t _num = 0;//记录表中的数据个数
};

四.封装map和set

1.hash表的基础功能介绍

一个哈希表要有的功能:插入、删除、遍历(迭代器)

2.对应的迭代器

 2.1结构

 哈希表的迭代器包含哈希表本身(方便找下一个链表),以及哈希表对应位置的节点

 2.2operator++重载

  1. 先算出当前所在位置
  2. 同一链表下,当前位置的下一个位置不为空,那就直接返回
  3. 当前位置为空,要继续找后面的位置
  4. 若找不到就返回空指针

2.3代码

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

//迭代器
template<class K, class T, class KOfT,class Hash>
struct __HashTableIterator
{
	typedef HashNode<T> Node;//节点
	typedef __HashTableIterator<K, T, KOfT,Hash> Self;//迭代器
	typedef  HashTable<K, T, KOfT, Hash> HT;//哈希表

	Node* _node;
	//为了找到下一个链表的起始位置而引入
	HT* _pht;

    //构造函数
	__HashTableIterator(Node* node, HT* pht)
		:_node(node)
		,_pht(pht)
	{}
    
    //重载运算符
	T& operator* ()
	{
		return _node->_data;
	}

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

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

	Self operator++()
	{
        //当前位置的下一个位置不为空
		if (_node->_next)
		{
			_node = _node->_next;
		}
        //当前位置的下一个位置为空
		else
		{
			//当前桶走完了,到下一个桶
			KOfT koft;
			//计算当前桶在映射表中的位置
			size_t index = _pht->HashFunc(koft(_node->_data)) % _pht->_tables.size();
            //下一个桶的位置
			++index;
			for (; index < _pht->_tables.size(); ++index)
			{
				Node* cur = _pht->_tables[index];
                 //找到下一个不为空的桶
				if (cur)
				{
					_node = cur;
					return *this;
				}
			}
            //未找到下一个不为空的桶,当前已经是最后一个节点
			_node = nullptr;
		}
		return *this;

	}
	
};

3.哈希表的改造

3.1迭代器的begin和end函数

iterator begin()
{
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		if (_tables[i])
		{
			return iterator(_tables[i], this);
		}
	}
	//哈希表中无节点
	return end();
}

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

3.2Insert的返回值改造

bool=>pair<iterator,bool>

返回值是一个pair,第一个参数是迭代器,第二个参数是bool,判断是否添加成功

如果已经存在,返回已存在节点的迭代器,以及false

如果节点不存在,返回新插入节点的迭代器,以及true

4.map的【】方括号重载

方括号[]重载返回值为value

直接对insert进行复用,插入key,通过iterator找到pair<key,value>,返回value

5.代码

a.HashTable

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


//迭代器
//编译时只会向前找,不认识HashTable,需要前置声明
template<class K, class T, class KOfT, class Hash>
class HashTable;

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

	Node* _node;
	//为了找到下一个桶的起始位置而引入
	HT* _pht;

	__HashTableIterator(Node* node, HT* pht)
		:_node(node)
		,_pht(pht)
	{}

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

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

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

	Self operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else
		{
			//如果当前桶走完了,到下一个桶
			KOfT koft;
			//计算当前桶在映射表中的位置
			size_t index = _pht->HashFunc(koft(_node->_data)) % _pht->_tables.size();
			++index;
			for (; index < _pht->_tables.size(); ++index)
			{
				Node* cur = _pht->_tables[index];
				if (cur)
				{
					_node = cur;
					return *this;
				}
			}
			_node = nullptr;
		}
		return *this;

	}
	
};

template<class K,class T,class KOfT,class Hash =_Hash<K>>
class HashTable
{
public:
	typedef HashNode<T> Node;
	typedef __HashTableIterator<K, T, KOfT, Hash> iterator;
	//友元关系,要访问私有成员
	friend struct __HashTableIterator<K, T, KOfT, Hash>;

	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i])
			{
				return iterator(_tables[i], this);
			}
		}
		//哈希表中无节点
		return end();
	}
	iterator end()
	{
		return iterator(nullptr,this);
	}

	//析构函数
	~HashTable()
	{
		Clear();
	}
	//清理
	void Clear()
	{
		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;
		}

	}
    
    //哈希函数
	//由于某些类型的数据不能直接取模,所以需要仿函数将它转为整形
	size_t HashFunc(const K& k)
	{
		Hash hash;
		return hash(k);
	}


	//插入
	pair<iterator, bool> Insert(const T& data)
	{
		KOfT koft;
		//如果负载因子大于1,则增容
		if (_tables.size() == _num)
		{
			vector<Node*> newtables;
			size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
			newtables.resize(newsize);
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				//将旧表中的节点取下,重新插入到新表中
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					//头插
					size_t index = HashFunc(koft(cur->_data)) % _tables.size();
					cur->_next = _tables[index];
					_tables[index] = cur;

					cur = next;
				}
				//防止原表中出现野指针
				_tables[i] = nullptr;
			}
			_tables.swap(newtables);
		
		}

		//插入数据
		//计算映射位置
		size_t index = HashFunc(koft(data))%_tables.size();
		//先查找是否在表中
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == koft(data))
			{
				return make_pair(iterator(cur,this),false);
			}
			else
			{
				cur = cur->_next;
			}
		}
		//头插到挂的链表中
		Node* newNode = new Node(data);
		newNode->_next = _tables[index];
		_tables[index] = newNode;
		_num++;
		return make_pair(iterator(newNode, this), true);
	}

	//查找
	Node* find(const K& key)
	{
		KOfT koft;
		size_t index = HashFunc(key) % _tables.size();
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				return cur;
			}
			else
			{
				cur = cur->_next;
			}
		}
		return nullptr;
	}

	//删除
	bool Erase(const K& key)
	{
		KOfT koft;
		size_t index = HashFunc(key) % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[index];
		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				if (prev == nullptr)
				{
					//表示要删的为第一个节点
					_tables[index] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}

private:
	vector<Node*> _tables;
	size_t _num = 0;//记录表中的数据个数
};

b.unordered_set

//哈希函数
//使类型可以取模
template<class K>
struct _Hash
{
	const K& operator()(const K& k)
	{
		return k;
	}
};
//特化string类
template<>
struct _Hash<string>
{
	size_t operator()(const string& str)
	{
		//转化后的数字
		size_t hash = 0;
		//BKDR Hash
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash = hash * 131 + str[i];
		}
		return hash;
	}
};

//unordered_set类
template<class K, class Hash = _Hash<K>>
class unordered_set
{
public:
	struct KOfSet
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};
	//类未实例化找不到类中的对象,需要明确是类型名
	typedef typename HashTable<K, K, KOfSet, Hash>::iterator iterator;

	iterator begin()
	{
		return _ht.begin();
	}
	iterator end()
	{
		return _ht.end();
	}
	pair<iterator, bool> insert(const K& k)
	{
		return _ht.Insert(k);
	}
private:
	HashTable<K, K, KOfSet,Hash> _ht;
};

c.unordered_map

//哈希函数
//使类型可以取模
template<class K>
struct _Hash
{
	const K& operator()(const K& k)
	{
		return k;
	}
};
//特化string类
template<>
struct _Hash<string>
{
	size_t operator()(const string& str)
	{
		//转化后的数字
		size_t hash = 0;
		//BKDR Hash
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash = hash * 131 + str[i];
		}
		return hash;
	}
};

//unodered_map类
template<class K, class V, class Hash = _Hash<K>>
class unordered_map
{
public:
	struct KOfMap
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
	//类未实例化找不到类中的对象,需要明确是类型名
	typedef typename HashTable<K, pair<K, V>, KOfMap, 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);
	}
	V& operator[](const K& k)
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(k, V()));
		return ret.first->second;
	}
private:
	HashTable<K, pair<K, V>, KOfMap, Hash> _ht;
};

五.位图

1.概念

位图就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。

通常是用来判断某个数据存不存在的

2.实现

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态

那么可以使用一个二进制比特位来代表数据是否存在的信息: 

  • 为1代表存在
  • 为0代表不存在

存放数据实例:

使用一个整形作为一个位图(有32个比特位),通过修改该整形的比特位,实现数据的存储

3.代码

class bitset
{
public:
    //构造函数
	bitset(size_t N)//表示存储范围为0-N的数
	{
		bits.resize(N / 32 + 1, 0);
		_num = 0;
	}
    //插入数据x
	void set(size_t x)
	{
		size_t index = x / 32;//算出映射的位置在第几个整形
		size_t pos = x % 32;//算出映射的位置在该整形的第几个位
		bits[index] |= (1 << pos);
		_num++;
	}
    //删除数据x
	void reset(size_t x)
	{
		size_t index = x / 32;//算出映射的位置在第几个整形
		size_t pos = x % 32;//算出映射的位置在该整形的第几个位
		bits[index] &= ~(1 << pos);
		_num--;
	}
    //查找x是否在其中
	bool test(size_t x)
	{
		size_t index = x / 32;//算出映射的位置在第几个整形
		size_t pos = x % 32;//算出映射的位置在该整形的第几个位
		return bits[index] & (1 << pos);
	}
private:
	vector<int> bits;
	size_t _num;//存储的数据数量
};

4.扩展

  • 判断在或不在是两种状态,可用一个比特位表示                                                 
  • 若有3种状态,可用两位表示
  • 若有更多种状态,可用更多位表示,或者,直接用两个或多个位图表示

六.布隆过滤器

1.出现

哈希表存储数据,缺点:数据类型不变,直接存入,浪费空间                                 

位图存储数据,缺点:位图一般只能处理整形,如果数据类型是字符串等,就无法处理了。         

将哈希与位图结合,即布隆过滤器

2.实现

数据通过不同的哈希算法转化成不同的整型,分别存入位图的比特位中

  • 当位图中该数据转化的整型全部存在,代表数据存在                                           
  • 当位图中该数据转化的整型部分存在,代表数据不存在

3.缺点

a.不支持删除

布隆过滤器不支持删除,因为不同元素转化的整形可能部分相同,比特位刚好有重叠,删除会互相影响 

b.存在误判

不同元素转化的整形可能完全相同,比特位完全重叠,存在误判

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

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

4.代码

//3种string的哈希算法
struct HashStr1
{
	//BKDR
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash *= 131;
			hash += str[i];
		}
		return hash;
	}
};

struct HashStr2
{
	//BKDR
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		size_t magic = 63689;
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash *= magic;
			hash += str[i];
			magic *= 378551;
		}
		return hash;
	}
};

struct HashStr3
{
	//BDBMHash
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		for (size_t i = 0; i < str.size(); ++i)
		{
			hash *= 65599;
			hash += str[i];
		}
		return hash;
	}
};

//布隆过滤器
template<class K=string, class Hash1=HashStr1, class Hash2=HashStr2, class Hash3=HashStr3>
class bloomfilter
{
public:
	bloomfilter(size_t num)//要存的数据个数
		:_bs(5*num)
		,_N(5*num)
	{}
    //插入数据
	void set(const K& key)
	{
        //存放位置
		size_t index1 = Hash1()(key) % _N;
		size_t index2 = Hash2()(key) % _N;
		size_t index3 = Hash3()(key) % _N;
		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}
    //查找数据
	bool test(const K& key)
	{
		size_t index1 = Hash1()(key) % _N;
		if (_bs.test(index1) == false)
		{
			return false;
		}
		size_t index2 = Hash2()(key) % _N;
		if (_bs.test(index2) == false)
		{
			return false;
		}
		size_t index3 = Hash3()(key) % _N;
		if (_bs.test(index3) == false)
		{
			return false;
		}
		//这里也不是一定存在,可能有误判
		return true;
	}
private:
	bitset _bs;//位图
	size_t _N;//位图的最大存放数据
};

5.增加删除功能

a.思路

  • 将布隆过滤器中的每个比特位扩展成一个小的计数器,而不是简单的 0 或 1 。   
  • 计数器长度一般为一个字节或两个字节,分别可以记录的次数上限为2^8和2^16
  • 每个计数器用于记录一个或多个哈希值对应的位置。                   

b.实现                               

将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一;删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。

c.缺陷                   

若记录的次数过多也可能会溢出,但是,如果使用更大的空间映射一个位置,消耗的空间也会变大

七.哈希的运用

1.哈希切割

a.思路

将大文件切割为可以放进内存的小文件 : 

根据哈希算法,将大文件中的数据放进不同编号的小文件,每个小文件的大小可能不同   

若某一小文件中的数据过多,也可能导致文件过大,需要分成更多份

b.好处

哈希切割的好处:每份小文件都可以放进内存直接处理

c.举例

比方说有10G存放IP地址的文件,而内存只有1G

这时可以分成100份小文件,idx = HashFunc(IP) % 100,idx是几就把IP放进几号文件中               

这样不一样的IP可能分进同一个小文件中,但是同一个IP一定会分进同一个小文件

为后续处理做准备

d.应用

现在有两份存放IP地址的文件,找它们的交集

将他们用同一种哈希切割后,只需分别比较相同编号的小文件

2.一致性哈希

假设有大量的数据需要放到多个服务器中

a.映射方式一

原来的映射位置:index = HashFunc(x) % number

number:服务器的数量

一个编号映射一台服务器

b.映射方式二

现在的映射位置:index = HashFunc(x) % y

y:特别大的数字(2^32......)

一段范围的编号映射一台服务器

这种映射方式的好处:增加机器时,不用重新计算映射位置,数据大量迁移,只需改变映射范围,迁移极小部分数据

更多推荐