赞
踩
顺序结构以及平衡树结构中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数:顺序查找时间复杂度为O(N),平衡树中为树的高度O(logN )。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(HashFunc)使元素的存储位置与它的关键码之间能够建立映射关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,最终完整构造出来的搜索结构称为哈希表(Hash Table)(或者称散列表)。
PS:哈希函数有很多种,但里面存储数据的数据结构通常是顺序表,因为顺序表随机访问的效率很高,所以在哈希(散列)表中一般封装的是顺序表。
虽然哈希函数 Hash(key) 有很多种,但它们的设计都应该满足下面几个原则:
常用哈希函数包括两种:直接定址法、除留余数法。
1、直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
示例
情景:输入一个全是小写字母的字符串,统计每个字符出现的次数。
通过哈希函数:Hash(key) = key - ‘a’,我们可以得到每个字符所映射的唯一下标位置,进而可以拿到或修改字符串中该字符的出现次数。可以看到数据范围较小且连续时,使用直接定址法可以很方便地管理数据。
2、除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。除留余数法可以管理范围相差较大的数据。
示例
情景:给定一个整数集合,之后输入任意一个整数搜索该数字是否在给定集合中。
按照上述哈希方式,继续向集合中插入元素44,发现Hash(44) = 44%10 = 4,可是下标四位置处已经有元素了,那么44应该插到哪里呢?我们把这种情况叫做哈希冲突。
对于两个数据元素的关键字 i 和 j , ( i != j );有:Hash( i ) == Hash( j ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
解决哈希冲突两种常见的方法是:闭散列和开散列。
这两种方法的相同点都是控制负载因子的大小来解决哈希冲突,只不过处理数据的方式不同。其中:负载因子 = 表中有效数据个数 / 表的大小。 负载因子越小,冲突的概率越低,效率越高,但是浪费的空间也越多。
实际中开散列的解决哈希冲突方法更实用,相比闭散列它有如下两个优点:
问题一:哈希查找的时间复杂度一定是O(1)吗?
答:不一定,因为存在哈希冲突,一般平均下来是O(1)。
问题三:常见的哈希函数?
答:直接定址法、除留余数法、平方取中法、随机数法、数字分析法、叠加法等。
问题三:解决哈希函数中出现冲突问题常采用的方法?
答:线性探测法、多重散列法、链地址法。
问题四:哈希冲突是可以通过设计好的哈希函数来杜绝的?
答:错误,哈希冲突是不能杜绝的,这个与存储的元素以及哈希函数相关。
问题五:哈希冲突是不同的元素,通过不同的哈希函数而产生相同的哈希地址而引起的?
答:错误,哈希冲突是不同的元素,通过相同的哈希函数而产生相同的哈希地址而引起的。
问题六:哈希冲突产生的原因一定是插入了相同的元素?
答:错误,不同元素在计算出相同的哈希值时就会冲突。
优点
缺点:
数据key的类型不一定是整型,还可能是字符型、浮点型、自定义的结构体类型等等,考虑哈希函数中的除留余数法的话,这些非整型的key是不能进行取模运算的,但是我们总有办法把它们转化成整型值:
补充1:关于key类型是字符串时
根据BKDR算法,累加每一个字符的ASCLL码之前,先乘上一个整型数字131,可以避免由于字符串中字符位置、数量不同等产生的哈希冲突:
更详细了解可以参考下面文章:字符串哈希算法
补充2:如果key类型是结构体,能不用结构体的地址作为key呢?
答:不能
闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,我们可以把数据存放到冲突位置中的“下一个” 空位置中去。那么如何寻找下一个空位置呢?
从发生冲突的位置开始,依次向后线性探测,直到寻找到下一个空位置为止。
对于线性探测,一旦发生哈希冲突,并且所有的冲突连在一起,很容易产生数据“踩踏”效应。即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
// 哈希表每个空间给个枚举的标记
enum State
{
EMPTY, //表示此位置为空
EXIST, //表示此位置已经有元素
DELETE,//表示此位置已经删除
};
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找。二次探测为了避免该问题,更改了寻找下一个空位置的方法:
下面我们模拟实现一个哈希表:
在哈希表主体中的第三个模板参数GetHashKey
是一个仿函数类型,作用是把数据的key转换成整型,key就是整型的话可以不用传,使用默认的就行;因为string类型也常用来作key,所以我们可以专门搞一个string类型的特化仿函数来处理key为字符串类型的情况。
// 把key转化为整型的仿函数 template<class K> struct HashKey { size_t operator()(const K& key) { return key; } }; // 当key为string类型时默认调用的特化版本 // 把每个字符的ASCLL码相加得到的整型值作为哈希的key template<> struct HashKey<string> { size_t operator()(const string& s) { size_t sum = 0; for(const auto& e : s) { sum = sum * 131 + e; } return sum; } }; // 枚举,用来表示每一个位置数据的状态 enum State { EXITS, EMPTY, DELATE, }; // 封装哈希表存储数据的类型 template<class K, class V> struct HashData { pair<K, V> _kv; State _state = EMPTY; }; // 哈希表主体,包含哈希表增删查改功能 template<class K, class V, class GetHashKey = HashKey<K>> class HashTable { typedef struct HashData<K, V> HashData; private: size_t _n = 0;// 当前有效元素个数 vector<HashData> _table;// 存储数据的数组 };
注意事项
使用typedef给结构体类型重命名时,不能直接用结构体名,还应该加上struct关键字,类的话就不用加:
EXITS
,说明找到了,返回这个位置的数据。// 查找数据 HashData* Find(const K& key) { // 1、一个有效数据都没有的话就直接返回空 if(_n == 0) { return nullptr; } // 2、通过输入的key线性探测,如果如果为空都没有找到说明不存在 GetHashKey kot; size_t index = kot(key) % _table.size(); while(_table[index]._state != EMPTY) { if(_table[index]._state == EXITS && _table[index]._kv.first == key) { return &_table[index]; } ++index; index %= _table.size(); } return nullptr; }
注意事项
线性探测每一次下标加一后,还要模上表的长度,确保index在表的范围内。
哈希表的长度是_table.size(),而不是_table.capacity()。因为顺序表的operator[ ]内部检查的是index必须小于size()。
EMPTY
直接插入,否则线性探测找到空位置插入。// 插入数据 bool Insert(const pair<K, V>& kv) { // 1、先查找要插入的key是否已经存在,存在的话就不能再插入了 if(Find(kv.first)) { return false; } // 2、元素已经确认要插入,接下来需要检查空间是否足够 // a、空间为0,默认开10个空间 // b、负载因子超过0.7,增容确保有适量的空位置 if(_table.size() == 0)// PS:不能用_n判断 { _table.resize(10); } else if((double)_n / (double)_table.size() > 0.7) { HashTable<K, V, GetHashKey> newHashTable; newHashTable._table.resize(_table.size() * 2); for(const auto& e : _table) { if(e._state == EXITS) { newHashTable.Insert(e._kv); } } _table.swap(newHashTable._table); } GetHashKey kot; size_t index = kot(kv.first) % _table.size(); while(_table[index]._state == EXITS) { ++index; index %= _table.size(); } _n++; _table[index]._kv = kv; _table[index]._state = EXITS; return true; }
注意事项
EXITS
的有效数据:DELETE
// 删除数据
bool Erase(const K& key)
{
HashData* ret = Find(key);
if(ret)
{
--_n;
ret->_state = DELATE;
return true;
}
return false;
}
HashTable.h
#pragma once #include <iostream> #include <string> #include <vector> using namespace std; // 把key转化为整型的仿函数 template<class K> struct HashKey { K operator()(const K& key) { return key; } }; // 当key为string类型时默认调用的特化版本 // 把每个字符的ASCLL码相加得到的整型值作为哈希的key template<> struct HashKey<string> { size_t operator()(const string& s) { size_t sum = 0; for(const auto& e : s) { sum = sum * 131 + e; } return sum; } }; // 枚举,用来表示每一个位置数据的状态 enum State { EXITS, EMPTY, DELATE, }; // 封装哈希表存储数据的类型 template<class K, class V> struct HashData { pair<K, V> _kv; State _state = EMPTY; }; // 哈希表主体,包含哈希表增删查改功能 template<class K, class V, class GetHashKey = HashKey<K>> class HashTable { typedef struct HashData<K, V> HashData; public: // 插入数据 bool Insert(const pair<K, V>& kv) { // 1、先查找要插入的key是否已经存在,存在的话就不能再插入了 if(Find(kv.first)) { return false; } // 2、元素已经确认要插入,接下来需要检查空间是否足够 // a、空间为0,默认开10个空间 // b、负载因子超过0.7,增容确保有适量的空位置 if(_table.size() == 0)// PS:不能用_n判断 { _table.resize(10); } else if((double)_n / (double)_table.size() > 0.7) { HashTable<K, V, GetHashKey> newHashTable; newHashTable._table.resize(_table.size() * 2); for(const auto& e : _table) { if(e._state == EXITS) { newHashTable.Insert(e._kv); } } _table.swap(newHashTable._table); } GetHashKey kot; size_t index = kot(kv.first) % _table.size(); while(_table[index]._state == EXITS) { ++index; index %= _table.size(); } _n++; _table[index]._kv = kv; _table[index]._state = EXITS; return true; } // 删除数据 bool Erase(const K& key) { HashData* ret = Find(key); if(ret) { --_n; ret->_state = DELATE; return true; } return false; } // 查找数据 HashData* Find(const K& key) { // 1、一个有效数据都没有的话就直接返回空 if(_n == 0) { return nullptr; } // 2、通过输入的key线性探测,如果如果为空都没有找到说明不存在 GetHashKey kot; size_t index = kot(key) % _table.size(); while(_table[index]._state != EMPTY) { if(_table[index]._state == EXITS && _table[index]._kv.first == key) { return &_table[index]; } ++index; index %= _table.size(); } return nullptr; } // 打印哈希表数据 void PrintfHashTable() { for(const auto& e : _table) { if(e._state == EXITS) { cout<<'<'<<e._kv.first<<", "<<e._kv.second<<'>'<<endl; } } } private: size_t _n = 0;// 当前有效元素个数 vector<HashData> _table;// 存储数据的数组 };
Test.cpp
#include "HashTable.h" void TestHashTable() { HashTable<string, size_t> table; string arr[] = {"苹果", "香蕉", "菠萝", "苹果", "香蕉", "菠萝", "橘子"}; for(const auto& e : arr) { HashData<string, size_t>* ret = table.Find(e); if(ret) { ret->_kv.second++; } else { table.Insert(make_pair(e, 1)); } } table.PrintfHashTable(); } int main() { TestHashTable(); return 0; }
编译运行:
开散列法又叫链地址法(拉链法),首先对关键码集合用哈希函数计算得到对应的地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中:
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
首先我们声明了一个模板类型struct HashNode
作为每一个桶中链起来的节点,它的成员包括:
而哈希表的主体calss HastTable
中有两个成员变量:
template<class K, class V> struct HashNode { HashNode(const pair<K, V>& data) :_kv(data) ,_next(nullptr) {} pair<K, V> _kv; struct HashNode* _next; }; template<class K> struct GetHashKey { size_t operator()(const K& key) { return key; } }; template<> struct GetHashKey<string> { size_t operator()(const string& s) { size_t ret = 0; for(const auto e : s) { ret = ret * 131 + e; } return ret; } }; template<class K, class V, class GetHashKey = HashKey<K>> class HashTable { typedef struct HashNode<K, V> HashNode; private: size_t _n = 0; vector<HashNode*> _table; };
// 通过key查找哈希表中的数据 HashNode* Find(const K& key) { // 有效数据为0的话直接退出 if(_n == 0) return nullptr; // 1、通过除留余数法计算key的地址 GetHashKey kot; size_t index = kot(key) % _table.size(); // 2、遍历单链表中所有节点的key,看是否有对应的 if(_table[index]) { HashNode* cur = _table[index]; while(cur) { if(cur->_kv.first == key) { return cur; } else { cur = cur->_next; } } } // 遍历完单链表也没有找到说明不存在,返回空即可 return nullptr; }
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,可能会导致每个桶中链表节点非常多,影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,必定会发生哈希冲突,因此,在元素个数刚好等于桶的个数(即负载因子为1)时,可以给哈希表增容。
具体增容操作:
bool Insert(const pair<K, V>& data) { if(Find(data.first)) return false; GetHashKey kot; if(_n == _table.size()) { size_t length = _table.size()==0 ? 10 : 2 * _table.size(); vector<HashNode*> newTable(length, nullptr); for(size_t i = 0; i < _table.size(); ++i) { if(_table[i]) { HashNode* cur = _table[i]; while(cur) { HashNode* next = cur->_next; size_t index = kot(data.first) % length; cur->_next = newTable[index]; newTable[index] = cur; cur = next; } _table[i] = nullptr; } } _table.swap(newTable); } size_t index = kot(data.first) % _table.size(); HashNode* newNode = new HashNode(data); newNode->_next = _table[index]; _table[index] = newNode; ++_n; return true; }
bool Erase(const K& key) { if(_table.size() == 0) return false; GetHashKey kot; size_t index = kot(key) % _table.szie(); HashNode* prev = nullptr; HashNode* cur = _table[index]; while(cur) { if(cur->_kv.first == key) { if(prev == nullptr) { _table[index] = cur->_next; } else { prev->_next = cur->_next; } delete cur; --_n; return true; } else { cur = cur->_next; } } return false; }
补充说明
删除一个单链表节点的方法除了记录前一个节点的地址prev外,还有一种替换法删除,即不定义prev,仅仅知道待删除节点cur,我们把cur后一个节点的数据拷贝给cur,然后删除后一个节点即可,但这种方法不适用于最后一个节点的删除。
HashTable.h
#include <iostream> #include <string> #include <vector> using namespace std; template<class K, class V> struct HashNode { HashNode(const pair<K, V>& data) :_kv(data) ,_next(nullptr) {} pair<K, V> _kv; struct HashNode* _next; }; template<class K> struct HashKey { size_t operator()(const K& key) { return key; } }; template<> struct HashKey<string> { size_t operator()(const string& s) { size_t ret = 0; for(const auto e : s) { ret = ret * 131 + e; } return ret; } }; template<class K, class V, class GetHashKey = HashKey<K>> class HashTable { typedef struct HashNode<K, V> HashNode; public: // 通过key查找哈希表中的数据 HashNode* Find(const K& key) { // 有效数据为0的话直接退出 if(_n == 0) return nullptr; // 1、通过除留余数法计算key的地址 GetHashKey kot; size_t index = kot(key) % _table.size(); // 2、遍历单链表中所有节点的key,看是否有对应的 if(_table[index]) { HashNode* cur = _table[index]; while(cur) { if(cur->_kv.first == key) { return cur; } else { cur = cur->_next; } } } // 遍历完单链表也没有找到说明不存在,返回空即可 return nullptr; } bool Insert(const pair<K, V>& data) { if(Find(data.first)) return false; GetHashKey kot; if(_n == _table.size()) { size_t length = _table.size()==0 ? 10 : 2 * _table.size(); vector<HashNode*> newTable(length, nullptr); for(size_t i = 0; i < _table.size(); ++i) { if(_table[i]) { HashNode* cur = _table[i]; while(cur) { HashNode* next = cur->_next; size_t index = kot(data.first) % length; cur->_next = newTable[index]; newTable[index] = cur; cur = next; } _table[i] = nullptr; } } _table.swap(newTable); } size_t index = kot(data.first) % _table.size(); HashNode* newNode = new HashNode(data); newNode->_next = _table[index]; _table[index] = newNode; ++_n; return true; } bool Erase(const K& key) { // 有效数据个数为0时直接退出,删除失败 if(_n == 0) return false; // 1、通过除留取余法找到key值对应的桶的位置 GetHashKey kot; size_t index = kot(key) % _table.szie(); // 2、记录好前一个节点prev,从第一个节点cur开始往后找要删除的节点。 HashNode* prev = nullptr; HashNode* cur = _table[index]; while(cur) { if(cur->_kv.first == key) { if(prev == nullptr) { _table[index] = cur->_next; } else { prev->_next = cur->_next; } delete cur; --_n; return true; } else { cur = cur->_next; } } // 遍历完单链表后依然没有找到说明不存在该节点,删除失败 return false; } void PrintHashTable() { for(size_t index = 0; index < _table.size(); ++index) { if(_table[index]) { HashNode* cur = _table[index]; while(cur) { cout<<'<'<<cur->_kv.first<<", "<<cur->_kv.second<<'>'<<endl; cur = cur->_next; } } } } private: size_t _n = 0; vector<HashNode*> _table; };
Test.cpp
#include "HashTable.h" void TestHashTable() { HashTable<string, size_t> table; string arr[] = {"苹果", "香蕉", "菠萝", "苹果", "香蕉", "菠萝", "橘子"}; for(const auto& e : arr) { HashNode<string, size_t>* ret = table.Find(e); if(ret) { ret->_kv.second++; } else { table.Insert(make_pair(e, 1)); } } table.PrintHashTable(); } int main() { TestHashTable(); return 0; }
编译运行:
各方面看来开散列是要比闭散列好的:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。