赞
踩
我们学习了数据结构,其实都在做一件事情那就是 数据的结构 无论是学了啥?目的都是为了 存储数据 数据排序 查找数据
“散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
remark: 通过某种映射关系 比方将你映射为 阿兰图灵 那么在这个名人散列表查找到图灵时就等于找到了你
通常无论是在 顺序表,链表,树,图,均是 迭代访问数据,即遍历(挨个挨个访问),效率通常比较慢,但是当来到了hash它就会变得非常快(一映射,根据映射的地方查找,很快就能找到)
哈希表的几个概念
散列函数
比如说,我现在给你个电话本,上面记录的有姓名和对应的手机号,我想让你帮我找王二的手机号是多少,那么你会怎么做呢?
你可能会说,那挨个找呗。
确实可以,那么你有没有想过,如果这个王二是在最后几页,那你去岂不是前面几页都白找了,有没有更快的方式呢?
是不是可以按照人名给分个类,比如按照首字母来排序,就abcd那样的顺序,这样根据王二我就知道去找w这些,这样不久快很多了
我们可以按照人名的首字母去弄一个表格,比如像这样:
我们取姓名的首字母作为一个标志,就可以很快的找到以这个字母开头的人名了,那么王二也就能更快的被我们找到,我们也不用再费力气去找什么张二和李二的,因为人家的名字首字母都不是w。
这里我们用到了一种方法:那就是取姓名的首字母做一个排序,那么这是不是就是通过一些特定的方法去得到一个特定的值,比如这里取人名的首字母,那么如果是放到数学中,是不是就是类似一个函数似的,给你一个值,经过某些加工得到另外一个值,就像这里的给你个人名,经过些许加工我们拿到首字母,那么这个函数或者是这个方法在哈希表中就叫做散列函数
这种映射关系我们可能称之为 做白日梦
remark:其实 十辈子 都不会成为阿兰这种天才,我们普通人所能做的是尽量去理解天才
关键值key
就像画的这个图,阿兰 是怎么得出来得,是不是由我映射而来,这个映射过程其实就是个散列函数,而我就是整个散列体系的关键值
所以说:哈希表就是通过将关键值也就是key通过一个散列函数加工处理之后得到一个值,这个值就是数据存放的位置,我们就可以根据这个值快速的找到我们想要的数据
之前我们已经知道了哈希表的本质其实是个数组,数组有啥特点?
——下表从0开始,连续的,直接通过下标访问
看上面的图,我们已经知道了哈希表本质是个数组,所以这里有个数组,长度是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个计数器减一,通过多占用几倍存储 空间的代价来增加删除操作。
对于哈希函数的取法,是数学领域所擅长的,哈希函数取得好,我们的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;
-
-
- }

布隆过滤器优点
布隆过滤器缺陷
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。