哈希结构(图文详解)哈希表,哈希桶,位图,布隆过滤器

Posted AllenSquirrel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈希结构(图文详解)哈希表,哈希桶,位图,布隆过滤器相关的知识,希望对你有一定的参考价值。

哈希结构

哈希概念

常见的K-V结构,实现了元素关键码与元素值的映射关系,但没有实现元素关键值与元素存储位置的映射关系,在遍历过程中,一般的顺序表或搜索二叉树要进行关键值的多次比较,其中顺序表的时间复杂度为O(n),二叉搜索树的时间复杂度O(lgn)

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

哈希函数

常见哈希函数
1. 直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 
2. 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

哈希操作

  • 插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

  • 搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

  • 删除元素

根据待删除元素的关键码,以此函数计算出该元素的存储位置并按此位置进行删除

  • 哈希冲突解决

  • 闭散列(线性探测法)

哈希表结构:在闭散列中,将哈希表底层结构设置为vector容器,容器中每一个元素为一个hashNode结构体,包括kv键值对和状态标志位

初始化将其hashNode的状态设置为空

template <class k,class v>
struct HashNode
{
	pair<k, v> kv;
	STATE state = EMPTY;
};

template <class k, class v>
class HashTable
{
public:
	typedef HashNode<k, v> Node;
	HashTable(size_t n = 10)
		:HSTable(n)
		,_size(0)
	{
	}
private:
	vector<Node> HSTable;
	size_t _size;
};

上述插入操作,可能会产生哈希冲突,通过线性探测法,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,插入一个新元素

在查找过程值,要想找到发生冲突元素,就必须给标志位

删除也类似,采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

enum STATE{
	EXIST  //已存在
	,DELETE  //删除过
	,EMPTY   //空位置
};

通过定义一个枚举类型,包括已存在不可插入状态(EXIST),空位置可插入状态(EMPTY),删除过的位置状态(DELETE)

插入代码如下:


	bool insert(const pair<k, v>& kv)
	{
		checkcapacity();
		//计算hash位置
		int idx = kv.first%HSTable.size();
		//搜索
		while (HSTable[idx].state != EMPTY)//其余两种状态都需要继续查找
		{
			//当前位置已经有数据,且与要插入新数据相同,则插入失败
			if (HSTable[idx].state ==EXIST&& kv.first == HSTable[idx].kv.first)
				return false;
			++idx;
			if (idx == HSTable.size() - 1)//向后查找过程到结尾 从头再开始找空位
				idx = 0;
		}
		//此时找到空位置,插入新元素,状态置为已存在,hash表大小+1
		HSTable[idx].kv = kv;
		HSTable[idx].state = EXIST;
		_size++;
		return true;
		//插满扩容
		
	}

由于插入过程实际上是在vector容器基础上完成,如果需要插入元素足够多,可能导致当前所创造的哈希表插满,此时就需要检查容量,进行扩充操作

扩容判断条件是:负载因子>0.7,此时认为产生冲突的可能性大,需要进行扩容

扩容并非在原表基础上增大size,而是开辟新表,将原表已存在状态位置元素进行逐一存放,代码如下:

void checkcapacity()
	{
		//负载因子控制是否增容
		//负载因子越小,冲突越小,但空间浪费越大
		if (HSTable.size()==0&&_size * 10 / HSTable.size() > 7)
		{
			//开新表
			int newcap = HSTable.size() == 0 ? 10 : 2 * HSTable.size();
			HashTable<k, v> newHST(newcap);
			//直接拷贝会导致hash值计算前后不一致
			//需要重新计算hash位置
			for (int i = 0; i < HSTable.size(); i++)
			{
				if (HSTable[i].state == EXIST)
				{
					newHST.insert(HSTable[i].kv);
				}
			}
			swap(newHST);
		}
	}
	void swap(HashTable<k, v>& HT)
	{
		swap(HSTable,HT.HSTable);
		swap(_size, HT._size);
	}

查找:由于hash冲突,直接通过索引查找难以找到发生hash冲突而通过线性探测法存放的元素,非空位置逐一查找,如果索引超过了表大小,需要循环从头开始

Node* find(const k& key)
	{
		//计算hash位置
		int idx = key%HSTable.size();
		//搜索
		while (HSTable[idx].state != EMPTY)
		{
			if (HSTable[idx].state == EXIST && key == HSTable[idx].first)
				return &HSTable[idx];
			++idx;
			if (idx == HSTable.size() - 1)
				idx = 0;
		}
		return nullptr;
	}

删除:删除并非真删除,释放元素,而是改变对应元素位置状态信息

bool erase(const k& key)
	{
		Node* node = find(key);
		if (node)
		{
			node->state = DELETE;
			_size--;
			return true;
		}
		return false;
	}
  • 开散列(哈希桶)

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

bool insert(const V& val)
	{
		checkcapacity();
		KeyofValue kov;
		int idx = kov(val) % _hst.size();

		//查找
		Node* cur = _hst[idx];
		while (cur)
		{
			if (kov(cur->_val) == kov(val))
			{
				//key重复
				return false;
			}
			cur = cur->_next;
		}
		//插入  头插
		cur = new Node(val);
		cur->_next = _hst[idx];
		_hst[idx] = cur;
		_size++;
		return true;
	}

哈希桶通过建立新连接进行扩容操作:

void checkcapacity()
	{
		if (_size == _hst.size())
		{
			int newcap = _size == 0 ? 10 : 2 * _size;
			vector<Node*> newhst(newcap);
			KeyofValue kov;
			for (int i = 0; i < _hst.size(); i++)
			{
				Node* cur = _hst[i];
				while (cur)
				{
					Node* next = cur->_next;
					int idx = kov(cur->_val) % newhst.size();
					//新表头插
					cur->_next = newhst[idx];
					newhst[idx] = cur;

					cur = next;
				}
				//原表断开 置空
				_hst[i] = nullptr;
			}
			swap(_hst,newhst);
		}
	}
  • 迭代器操作:

迭代器的++步骤:

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

template <class V>
struct HashNode
{
	V _val;
	HashNode<V>* _next;

	HashNode(const V& val)
		:_val(val)
		,_next(nullptr)
	{

	}
};

//hash表的前置声明
template<class K, class V, class KeyofValue>
class HSTable;


//hash表迭代器  封装单链表节点
template<class K, class V, class KeyofValue>
struct HashIterator{
	typedef HashNode<V> Node;
	typedef HSTable<K, V, KeyofValue> ht;
	typedef HashIterator<K, V, KeyofValue> Self;
	//成员:节点指针,哈希表指针
	Node* _node;
	ht* _hptr;

	HashIterator(Node* node,ht* hptr)
		:_node(node)
		,_hptr(hptr)
	{

	}

	Self& operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		//找下一个非空链表头结点
		else
		{
			//计算当前节点在hash表中位置
			KeyofValue kov;
			int idx = kov(_node->_val) % _hptr->_hst.size();
			//从下一位置开始查找
			++idx;
			for (int; idx < _hptr->_hst.size(); idx++)
			{
				if (_hptr->_hst[idx])
				{
					_node = _hptr->_hst[idx];//找到非空链表头结点给node记录
					break;
				}
			}
			if (idx = _hptr->_hst.size())//此时走到end位置  将node节点置为空
			{
				_node = nullptr;
			}
		}
		return *this;
	}
};



template<class K,class V,class KeyofValue>
class HSTable
{
public:
	typedef HashNode<V> Node;
	typedef HashIterator<K, V, KeyofValue> iterator;

	//由于迭代器需要访问其私有成员_hst  声明为友元类
	friend HashIterator<K, V, KeyofValue>;
	HSTable(int n=10)
		:_hst(n)
		,_size(0)
	{

	}

	iterator begin()
	{
		for (int i = 0; i < _hst.size(); i++)
		{
			if (_hst[i])
			{
				return iterator(_hst[i], this);
			}
			else
				return iterator(nullptr,this);

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


private:
	vector<Node*> _hst;
	int _size;
};

哈希表的应用

  • 位图

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

比如Tencent的一道面试题:

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

此题解法较多,一般容易想到的就是暴力求解,直接遍历查询,但海量数据导致时间复杂度过大,无法实际中应用;稍微进阶一些的会想到采用二分查找,虽然相比较于直接遍历,把时间复杂度从O(n)提升到O(lgn),但对于40亿数据还是难以实现

而通过位图可以实现:数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:

代码实现如下:

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

/*
位图应用:
(1)存放不重复数据简单信息,不需要存放数据本身   优点:节省空间,查找效率高

*/
class bitset
{
public:
	//位图内存大小与数据范围有关
	bitset(size_t range)
		:_bit(range/32+1)
	{

	}
	//存储信息
	void set(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位置1  按位或运算
		_bit[idx] |= (1 << bitidx);  //把1向左移动bixidx位置  或运算之后  将其置为1
	}
	//查找信息
	bool test(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位右移bitidx后 与1 与运算  如果为1 则为1  否则为0
		return (_bit[idx] >> bitidx)&1;		
	}

	//删除信息
	void reset(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位置10
		_bit[idx] &= ~(1 << bitidx);
	}
private:
	//数组
	vector<int> _bit;
	//默认bit位个数为 32 位
};
  • 布隆过滤器

布隆过滤器是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆过滤器的实现依然需要借助位图的实现来完成,依然依靠0-1进行标记数据是否存在

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

  • 查找:

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

  • 删除:

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

代码如下:

//布隆过滤器
// 假设布隆过滤器中元素类型为K,每个元素对应3个哈希函数
template<class T, class KToInt1, class KToInt2,class KToInt3>
class BloomFilter
{
public:
	BloomFilter(size_t size) // 布隆过滤器中元素个数
		: _bmp(3 * size), _bitcount(3 * size)
	{}

	//存储信息:使用多个bit位存储
	void set(const T& val)
	{
		KToInt1 k1;
		KToInt2 k2;
		KToInt3 k3;
		int idx1 = k1(val) % _bitcount;
		int idx2 = k2(val) % _bitcount;
		int idx3 = k3(val) % _bitcount;
		
		_bmp.set(idx1);
		_bmp.set(idx2);
		_bmp.set(idx3);
	}

	bool test(const T& val)
	{
		KToInt1 k1;
		KToInt2 k2;
		KToInt3 k3;
		int idx1 = k1(val) % _bitcount;
		int idx2 = k2(val) % _bitcount;
		int idx3 = k3(val) % _bitcount;

		if (!_bmp.test(idx1))
			return false;//三个有一个为0 肯定不存在  三个均为1  可能存在(不能说一定存在)
		if (!_bmp.test(idx2))
			return false;
		if (!_bmp.test(idx3))
			return false;
		if (_bmp.test(idx1) && _bmp.test(idx2) && _bmp.test(idx3))
			return true;
	}
private:
	bitset _bmp;
	size_t _bitcount; // bit位的个数
};

struct KToInt1
{
	//hash函数计算方式1  
};

struct KToInt2
{
	//hash函数计算方式2 
};

struct KToInt3
{
	//hash函数计算方式3 
};
void test()
{
	BloomFilter<string, KToInt1, KToInt2, KToInt3> bf(10);
}

 

以上是关于哈希结构(图文详解)哈希表,哈希桶,位图,布隆过滤器的主要内容,如果未能解决你的问题,请参考以下文章

哈希表

哈希表应用(位图和布隆过滤器)

bingc++(mapset树哈希位图布隆过滤器)

bingc++(mapset树哈希位图布隆过滤器)

bingc++(mapset树哈希位图布隆过滤器)

数据结构----哈希