Hash 哈希表

一 <<什么是哈希表>>

我们学习了数据结构,其实都在做一件事情那就是   数据的结构    无论是学了啥?目的都是为了 存储数据  数据排序  查找数据

  • 线性结构有 顺序表 链表
  • 非线性的有  图       树       哈希

1.1 哈希表的定义

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

 remark: 通过某种映射关系  比方将你映射为 阿兰图灵  那么在这个名人散列表查找到图灵时就等于找到了你

通常无论是在  顺序表,链表,树,图,均是 迭代访问数据,即遍历(挨个挨个访问),效率通常比较慢,但是当来到了hash它就会变得非常快(一映射,根据映射的地方查找,很快就能找到)

 哈希表的几个概念

散列函数

比如说,我现在给你个电话本,上面记录的有姓名和对应的手机号,我想让你帮我找王二的手机号是多少,那么你会怎么做呢?

你可能会说,那挨个找呗。

确实可以,那么你有没有想过,如果这个王二是在最后几页,那你去岂不是前面几页都白找了,有没有更快的方式呢?

是不是可以按照人名给分个类,比如按照首字母来排序,就abcd那样的顺序,这样根据王二我就知道去找w这些,这样不久快很多了

我们可以按照人名的首字母去弄一个表格,比如像这样:
 

我们取姓名的首字母作为一个标志,就可以很快的找到以这个字母开头的人名了,那么王二也就能更快的被我们找到,我们也不用再费力气去找什么张二和李二的,因为人家的名字首字母都不是w。

这里我们用到了一种方法:那就是取姓名的首字母做一个排序,那么这是不是就是通过一些特定的方法去得到一个特定的值,比如这里取人名的首字母,那么如果是放到数学中,是不是就是类似一个函数似的,给你一个值,经过某些加工得到另外一个值,就像这里的给你个人名,经过些许加工我们拿到首字母,那么这个函数或者是这个方法在哈希表中就叫做散列函数

这种映射关系我们可能称之为  做白日梦 

remark:其实 十辈子 都不会成为阿兰这种天才,我们普通人所能做的是尽量去理解天才 

关键值key

就像画的这个图,阿兰  是怎么得出来得,是不是由我映射而来,这个映射过程其实就是个散列函数,而我就是整个散列体系的关键值

哈希表

所以说:哈希表就是通过将关键值也就是key通过一个散列函数加工处理之后得到一个值,这个值就是数据存放的位置,我们就可以根据这个值快速的找到我们想要的数据

1.2 哈希表的存储方式

之前我们已经知道了哈希表的本质其实是个数组,数组有啥特点?
——下表从0开始,连续的,直接通过下标访问

  •  键值对:有一个key和一个value对应着,比如图中的101011是键值key,对应value张三,学生的学号和姓名就是一个键值对
  • Entry:在java jdk里把键值对叫做Entry,C++则为pair
  • 在散列表中存储的是键值对

1.3 哈希表如何存数据

看上面的图,我们已经知道了哈希表本质是个数组,所以这里有个数组,长度是8,现在我们要做的是把这个学生信息存放到哈希表中,也就是这个数组中去,那我们需要考虑怎么去存放呢?

这里的学号是个key,我们之前也知道了,哈希表就是根据key值来通过哈希函数计算得到一个值,这个值就是用来确定这个pair要存放在哈希表中的位置的,实际上这个值就是一个下标值,来确定放在数组的哪个位置上。

比如这里的学号是101011,那么经过哈希函数的计算之后得到了1,这个1就是告诉我们应该把这个pair放到哪个位置,这个1就是数组的确切位置的下标,也就是需要放在数组中下表为1的位置,如图中所示。

我们之前已经介绍过什么是pair了,所以这里你要知道,数组中1的位置存放的是一个pair,它不是一个简单的单个数值,而是一个键值对,也就是存放了key和value,key就是学号101011,value就是张三,我们经过哈希函数计算得出的1只是为了确定这个pair该放在哪个位置而已。

现在我们就成功把这个pair放到了哈希表中了

二 <<常见的哈希函数>>  

//有好多方法

除留余数法

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

哈希冲突 

不同Key经过哈希函数映射,得到相同的散列地址,这时便产生了冲突,你说你是阿兰,那我还说我是图灵呢!

通常我们有  两种方法解决哈希冲突

                             1.链地址法  2.线性探测法 

 开散列

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

 为啥要进行头插,因为若是尾插我们先要通过遍历找到尾部节点,  O(n)浪费时间,     头插O(1) 

#include<iostream>
#include<vector>

using namespace std;

template<class Type>
class HashTable;

template<class Type>
class HashNode
{
	friend class HashTable<Type>;
public:
	HashNode(Type d = Type(), HashNode<Type>* n = nullptr) :data(d), next(n)
	{}
	~HashNode()
	{}
private:
	Type data;
	HashNode* next;
};

template<class Type>
class HashTable
{
public:
	HashTable()
	{
		memset(m_ht, 0, sizeof(m_ht));
	}
	HashNode<Type>* Find(const Type& Key)
	{
		size_t idx = Hash(Key);//idx就是在顺序表的下表位置
		
		HashNode<Type>* p = m_ht[idx];//对数组所悬挂的链表进行遍历
		while (p != nullptr && p->data != Key)//两种逻辑找了一圈没找到返回的p就是nullptr, 找到了就把p这个节点返回了
			p = p->next;
    
		return p;
	}
	void Insert(const Type& Key)
	{
		size_t idx = Hash(Key);

		HashNode<Type>* Node = new HashNode<Type>(Key);//产生节点
		
		Node->next = m_ht[idx];//对节点进行头插
		m_ht[idx] = Node;
	}
	void Remove(const Type& Key)
	{
		size_t idx = Hash(Key);
		HashNode<Type>* p = m_ht[idx];

		/*  第一种情况  :  删除的节点 不存在  即哈希表上啥也没挂 Key也不存在 
		 *  第二种情况  :  删除的节点  存在   是哈希表的头节点
		 *  第三种情况  :  哈希表不空         遍历链表要删除的节点不存在
		 *  第四种情况  :  删除的节点  存在
		 *  
		 */ 
		if (p == nullptr)
			return ;

		if (p->data == Key)
			m_ht[idx] = m_ht[idx]->next;
		else{
			while (p != nullptr && p->next->data != Key)//找的是删除节点的pre节点
				p = p->next;

			if (p == nullptr)
				return;

			HashNode<Type>* pre = p;
			p = p->next;
			pre->next = p->next;
		}
		delete p;
	}
	void Show()const
	{
		int i;
		cout << "Hash" << endl;
		for (i = 0; i < HASH_TABLE_SIZE; ++i)
		{
			cout << i << " :";

			HashNode<Type>* p = m_ht[i];
			while (p!= nullptr)
			{
				cout << p->data << "->";
				p = p->next;
			}
			cout << "NIL."<<endl;
		}
	}
protected:
	size_t Hash(const Type& Key)
	{
		return Key % HASH_TABLE_SIZE;//除留余数法
	}
	enum {HASH_TABLE_SIZE=7};
private:
	HashNode<Type>* m_ht[HASH_TABLE_SIZE];//这是个指针数组里面存储的元素是HashNode*类型
};

void main()
{
	int ar[] = { 1, 9,  10, 8, 22, 20 };
	int n = sizeof(ar) / sizeof(ar[0]);

	HashTable<int> ht;
	for (int i = 0; i < n; ++i)
	{
		ht.Insert(ar[i]);
	}

	ht.Remove(8);
	ht.Show();
}

闭散列 

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

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

enum State { EMPTY, EXIST, DELETE };
template<class K, class V>
class HashTable
{
	struct Elem
	{
		pair<K, V> _val;
		State      _state;
	};
public:
	HashTable(size_t sz):m_ht(sz), m_size(0)
	{
		for(int i=0; i<sz; ++i)
		{
			m_ht[i]._state = EMPTY;
		}
	}
public:
	void Insert(const pair<K,V> &val)
	{
		size_t hash_idx = Hash(val);
		size_t origin_idx = hash_idx;

		CheckCapacity();

		while(m_ht[hash_idx]._state == EXIST)
		{
			hash_idx  = (hash_idx+1) % m_ht.capacity(); //空间循环
			if(hash_idx == origin_idx)
				return;
		}
		Elem e = {val, EXIST};
		m_ht[hash_idx] = e;
		m_size++;
	}

	int Find(const pair<K,V> &key)
	{
		size_t hash_idx = Hash(key);
		size_t origin_idx = hash_idx;
		while(m_ht[hash_idx]._state==EXIST && key!=m_ht[hash_idx]._val)
		{
			hash_idx  = (hash_idx+1) % m_ht.capacity(); //空间循环
			if(hash_idx == origin_idx)
				return -1;
		}

		if(m_ht[hash_idx]._state == EXIST)
			return hash_idx;
		return -1;
	}

	void Remove(const pair<K,V> &key)
	{
		int hash_idx = Find(key);

		if(hash_idx != -1)
		{
			m_ht[hash_idx]._state = DELETE;  //标记删除法
			m_size--;
		}
	}

	int GetNextPrime(int cur_prime)
	{
		static int prime_table[] = {7, 13, 19, 23, 29, 43, 53, 93, 103};
		int n = sizeof(prime_table) / sizeof(prime_table[0]);

		int i;
		for(i=0; i<n; ++i)
		{
			if(cur_prime == prime_table[i])
				break;
		}
		return i<n ? prime_table[i+1] : prime_table[n-1];
	}

	void CheckCapacity()
	{
		if (m_size * 10 / m_ht.capacity() >= 7)  // 0.7
		{
			HashTable<K, V> new_ht(GetNextPrime(m_ht.capacity()));

			for (size_t i = 0; i < m_ht.capacity(); ++i)
			{
				if (m_ht[i]._state == EXIST)
					new_ht.Insert(m_ht[i]._val);
			}

			m_ht.swap(new_ht.m_ht);
		}
	}

protected:
	size_t Hash(const pair<K,V> &val)
	{
		return val.first % m_ht.capacity();
	}
private:
	vector<Elem> m_ht;
	size_t       m_size;
};

void main()
{
	int ar[] = {1, 9, 4, 10, 8, 22, 20, 15};
	int n = sizeof(ar) / sizeof(ar[0]);
	HashTable<int,int> ht(7);

	for(int i=0; i<n; ++i)
	{
		pair<int,int> v = make_pair(ar[i],ar[i]);
		ht.Insert(v);
	}

	int idx = ht.Find(make_pair(22,22));

	ht.Remove(make_pair(22,22));

	idx = ht.Find(make_pair(22,22));
}

哈希桶

解决冲突的方法选了链地址法的哈希表

 

 伴随着冲突的不断加剧,虽然链地址法可以无限解决冲突,但是挂的节点越多,当目标节点在链表尾部时,我们寻找这个元素需要遍历整个链表,

寻找效率极其低下,因此我们可以对哈希表进行扩容,这样它所悬挂的节点会分散开,以达到高效查找的目的

#include<iostream>

using namespace std;


template<class Type>
class HashTable;

const size_t primeList[] = { 7, 13, 19, 23 };
size_t GetNextPrime(size_t prime)
{
	for (int i = 0; i < 4; ++i)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[3];
}
template<class Type>
class HashNode
{
	friend class HashTable<Type>;
public:
	HashNode(Type d = Type(), HashNode<Type>* n = NULL) :data(d), next(n)
	{}
	~HashNode()
	{}
private:
	Type data;
	HashNode* next;
};

template<class Type>
class HashTable
{
public:
	HashTable() :m_size(0)
	{
		m_ht = new HashNode<Type>*[DEFAULT_HASH_TABLE_SIZE];
		memset(m_ht, 0, sizeof(HashNode<Type>*) * DEFAULT_HASH_TABLE_SIZE);

		m_bucket_count = DEFAULT_HASH_TABLE_SIZE;
	}
public:
	HashNode<Type>* Find(const Type& key)
	{
		size_t idx = Hash(key);
		HashNode<Type>* p = m_ht[idx];
		while (p != NULL && key != p->data)
			p = p->next;
		return p;
	}
	void Insert(const Type& v)
	{
		m_size++;
		CheckCapacity();

		//hash地址
		size_t idx = Hash(v);

		//插入数据
		HashNode<Type>* node = new HashNode<Type>(v);
		node->next = m_ht[idx];
		m_ht[idx] = node;


	}
	void Remove(const Type& key)
	{
		//查找
		size_t idx = Hash(key);
		HashNode<Type>* p = m_ht[idx];
		if (p == NULL)
			return;
		if (key == m_ht[idx]->data)
		{
			m_ht[idx] = p->next;
		}
		else
		{
			while (p != NULL && p->next->data != key)
				p = p->next;
			if (p == NULL)
				return;

			HashNode<Type>* prev = p;
			p = p->next;
			prev->next = p->next;
		}

		//删除
		delete p;
	}
	void Show()const
	{
		for (int i = 0; i < m_bucket_count; ++i)
		{
			cout << i << " : ";
			HashNode<Type>* p = m_ht[i];
			while (p != NULL)
			{
				cout << p->data << "-->";
				p = p->next;
			}
			cout << "Nil." << endl;
		}
	}
protected:
	size_t Hash(const Type& key)
	{
		//除留余数法
		return key % m_bucket_count;
	}

protected:
	void CheckCapacity()
	{
		if (m_size > m_bucket_count)
		{
			//扩容
			size_t new_bucket_count = GetNextPrime(m_bucket_count);
			HashNode<Type>** new_ht = new HashNode<Type>*[new_bucket_count];
			memset(new_ht, 0, sizeof(HashNode<Type>*) * new_bucket_count);


			for (int i = 0; i < m_bucket_count; ++i)
			{
				HashNode<Type>* p = m_ht[i];
				while (p != NULL)
				{
					m_ht[i] = p->next;
					size_t hash_index = p->data % new_bucket_count;
					p->next = new_ht[hash_index];
					new_ht[hash_index] = p;

					p = m_ht[i];
				}
			}

			delete[]m_ht;
			m_ht = new_ht;
			m_bucket_count = new_bucket_count;
		}
	}
protected:
	enum { DEFAULT_HASH_TABLE_SIZE = 7 };
private:
	HashNode<Type>** m_ht;
	size_t           m_bucket_count;//哈希表长
	size_t           m_size;//已经挂载的节点个数
};



void main()
{
	int ar[] = { 1, 9, 10, 8, 22, 20, 43, 32, 21, 4, 6, 76, 8, 9, 56, 54, 48, 25, 5 };
	//int ar[] = {1, 9, 10, 8, 22, 20, 43, 32};
	int n = sizeof(ar) / sizeof(ar[0]);

	HashTable<int> ht;
	for (int i = 0; i < n; ++i)
	{
		ht.Insert(ar[i]);
	}

	//ht.Remove(8);
	ht.Show();

	//HashNode<int> *p = ht.Find(80);
}

哈希的应用 ---布隆过滤器

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

  • 用哈希表存储用户记录,缺点:浪费空间

  • 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理 了。

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

布隆过滤器概念

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

布隆过滤器的插入

布隆过滤器通过多个哈希函数将我们期望“Key ”  映射到位图中,高效且节省空间

 布隆过滤器的查找

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

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

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

布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。【上图若你将baidu 删除 ,那么tencent也将无法找到(缺少7位置的1)】

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

  • 缺陷: 1.
  • 无法确认元素是否真正在布隆过滤器中
  •  存在计数回绕

对于哈希函数的取法,是数学领域所擅长的,哈希函数取得好,我们的Key便极具敏感性,Key稍微发生改变,Key的映射便产生极大的变化

 

#include<iostream>
#include<string>
#include<bitset>
#include<assert.h>
using namespace std;

#define _N 10   //误判率

template<class T>
struct Hashfunc1
{
	size_t operator()(const T& key)
	{
		return BKDRHash(key.c_str());
	}
	size_t BKDRHash(const char* str)
	{
		register size_t hash = 0;
		while (size_t ch = (size_t)*str++)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};


template<class T>
struct Hashfunc2
{
	size_t operator()(const T& key)
	{
		return SDBMHash(key.c_str());
	}
	size_t SDBMHash(const char* str)
	{
		register size_t hash = 0;
		while (size_t ch = (size_t)*str++)
		{
			hash = 65599 * hash + ch;
		}
		return hash;
	}
};

template<class T>
struct Hashfunc3
{
	size_t operator()(const T& key)
	{
		return RSHash(key.c_str());
	}
	size_t RSHash(const char* str)
	{
		register size_t hash = 0;
		size_t magic = 63689;
		while (size_t ch = (size_t)*str++)
		{
			hash = hash * magic + ch;
			magic *= 378551;
		}
		return hash;
	}
};
template<class T, class KeyToInt1 = Hashfunc1<T>,class KeyToInt2 = Hashfunc2<T>,class KeyToInt3 = Hashfunc3<T>>
class BloomFilter
{
public:
	BloomFilter() :_size(0)
	{}
public:
	void Insert(const T& v)
	{
		size_t idx1 = KeyToInt1()(v) % _N;
		_bitmap.set(idx1);
		size_t idx2 = KeyToInt2()(v) % _N;
		_bitmap.set(idx2);
		size_t idx3 = KeyToInt3()(v) % _N;
		_bitmap.set(idx3);

		_size++;
	}
	bool Test(const T& key)const
	{
		size_t idx1 = KeyToInt1()(key) % _N;
		if (_bitmap.test(idx1) == 0)
			return false;
		size_t idx2 = KeyToInt2()(key) % _N;
		if (_bitmap.test(idx2) == 0)
			return false;
		size_t idx3 = KeyToInt3()(key) % _N;
		if (_bitmap.test(idx3) == 0)
			return false;

		return true; //可能存在,有可能存在误判
	}
private:
	bitset<_N> _bitmap;  //位图
	size_t     _size;
};

void main()
{
	
	const string url1 = "www.baidu.com";
	const string url2 = "www.taobao.com";
	const string url3 = "www.jingdong.com";
	const string url4 = "www.pinduoduo.com";
	const string url5 = "www.qq.com";
	const string url6 = "www.weixin.com";
	BloomFilter<string> bf;
	bf.Insert(url1);
	bf.Insert(url2);
	//bf.Insert(url3);
	//bf.Insert(url4);
	//bf.Insert(url5);
	cout << bf.Test(url2) << endl;
	

}

 布隆过滤器优点

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

布隆过滤器缺陷

  • 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  • 不能获取元素本身
  • 一般情况下不能从布隆过滤器中删除元素
  • 如果采用计数方式删除,可能会存在计数回绕问题
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值