赞
踩
哈希表(Hash Table)是一种非常重要的数据结构,它基于哈希函数将键(Key)映射到值(Value)上,从而实现对数据的快速存储、查找和删除。在哈希表中,数据并不是按顺序存储的,而是根据哈希函数计算出的键的哈希值,确定数据在表中的位置。由于哈希函数的设计使得键与位置之间存在直接对应关系,因此哈希表在查找、插入和删除操作上的平均时间复杂度可以达到O(1),即常数时间复杂度。这使得哈希表在处理大规模数据时具有极高的效率。
在C++标准库中,unordered_map
和unordered_set
是两种基于哈希表实现的关联容器。它们利用哈希表的优势,提供了快速的数据访问能力。unordered_map
存储的是键值对(Key-Value Pair),允许我们根据键快速查找、插入或删除对应的值。而unordered_set
则只存储键,用于快速判断某个元素是否存在于集合中。我们将在下一篇文章根据本文内容介绍unordered_map
和unordered_set
。
unordered
系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。哈希是一种查找的方法,不是数据结构。
哈希表与哈希函数是计算机科学中非常重要的概念,它们在数据结构的构建和算法优化中发挥着关键作用。以下是关于哈希表的基本原理、哈希函数的作用与特点,以及哈希冲突处理方法的详细介绍。
哈希表,又称为散列表,是一种根据关键码值(key-value)直接进行访问的数据结构。它通过一个哈希函数将键映射到表中的位置,以便快速查找、插入和删除元素。哈希表的主要优势在于其高效的查找性能,理想情况下,其查找时间复杂度可以达到O(1),即常数时间复杂度。
在哈希表中,每个键都唯一对应一个哈希值,这个哈希值就是该键在表中的位置。当需要插入一个新元素时,通过哈希函数计算键的哈希值,然后将元素存储在对应的位置。当需要查找一个元素时,同样通过哈希函数计算键的哈希值,然后直接定位到表中的位置进行查找。
哈希函数是哈希表的核心,它负责将输入的键转换为哈希值。哈希函数的主要作用是将任意长度的数据映射为固定长度的哈希值,这个哈希值在哈希表中用作元素的索引。
哈希函数具有以下特点:
常见的哈希函数有:
直接定址法:
除留余数法:
平方取中法:
折叠法:
随机数法:
数学分析法:
总的来说,这些哈希函数方法各有其特点和适用场景。在实际应用中,需要根据具体的数据特点和需求来选择合适的哈希函数,以达到最优的哈希效果。同时,也需要注意哈希函数的冲突处理问题,通过合理的冲突解决策略来减少冲突的发生,提高哈希表的性能。
尽管哈希函数的设计旨在使哈希冲突的可能性尽可能低,但在实际应用中,哈希冲突仍然可能发生。
解决哈希冲突两种常见的方法是:闭散列和开散列。
链地址法(Chaining):当两个或多个键的哈希值相同时,将这些键对应的元素存储在一个链表中。链表的头节点存储在哈希表的对应位置。当查找元素时,首先计算键的哈希值,然后遍历对应位置上的链表以找到元素。在 SGI STL 源代码中,称表格内的元素为桶子(bucket),意为:表格内的每个单元,涵盖的不只是个节点,可能是一“桶”节点。即开散列。
开放地址法(Open Addressing):当哈希冲突发生时,尝试在哈希表的其他位置查找一个空闲槽来存储元素。根据具体实现方式的不同,开放地址法又可分为线性探测、二次探测和双重散列等。即闭散列。
线性探测:当哈希冲突发生时,顺序检查哈希表中的下一个槽,直到找到一个空闲槽。如果到达尾端,就绕道头部继续寻找。例如:假设 x 是任意整数, tablesize 是数组大小,则 x % tablesize 得到的整数范围在 [ 0, tablesize - 1] 。刚好可以作为数组的索引:
置于元素的删除,只能标记删除记号,实际删除则待表格重新整理时再进行,这是因为表中的每个元素不仅仅表述它自己,也关系到其它元素的排列。
二次探测:当哈希冲突发生时,根据一个二次方程来计算新的槽位置,以减少聚集现象。F(i) = i2 。如果使用哈希函数计算得到的位置是 H 。而该位置已被使用,那么我们一次尝试 H2+ 12 ,H2+ 22,H2+ 32,….,H2+ i2 。
这些方法都有各自的优缺点,需要根据具体的应用场景和需求来选择合适的方法。在实际应用中,链地址法因其实现简单和灵活性而得到广泛应用。然而,在处理大量哈希冲突时,链表的长度可能会变得很长,从而影响查找性能。因此,在设计哈希表时,需要综合考虑哈希函数的选择、哈希表的大小以及冲突处理策略等因素,以实现高效的数据存储和访问。
哈希桶(Hash Bucket)是哈希表的一个基本组成单位,用于存储具有相同哈希值的键值对。哈希桶通常与链表或其他数据结构结合使用,以解决哈希冲突。 此部分内容与后面unordered_xxx
的实现有关!
template<class T>
struct HashNode {
HashNode<T>* _next;
T _data;
HashNode(const T& data) :_next(nullptr), _data(data) {}
};
template<class K, class T, class KeyOfT, class Hash >
class HashTable {
typedef HashNode<T> Node;
template<class K, class T, class KeyOfT, class Hash>
friend struct __HTIterator;
public:
typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
//...
private:
vector<Node*> _tables;
size_t _n;
KeyOfT kot;
Hash hs;
};
根据HashNode
结构体的定义,这个结构体表示哈希表中每个槽位上的链表中的一个节点。每个HashNode
对象包含以下部分:
_next
:一个指向下一个HashNode
的指针。当多个键值对具有相同的哈希值时,它们会被链接到这个链表上,通过_next
指针来遍历这些节点。_data
:存储实际的键值对信息。这里,_data
只存储了值T
,而没有直接存储键K
。这意味着我们将通过KeyOfT
函数对象从值中提取。在HashTable
类中,_tables
是一个向量(vector
),它存储了指向HashNode
的指针。每个槽位(即_tables
中的一个元素)可以是一个空指针(表示该槽位没有存储任何键值对),或者是一个指向链表头节点的指针(表示该槽位上有一个或多个具有相同哈希值的键值对)。
当向哈希表中插入一个键值对时,哈希函数hs
以及提取键的kot
会被用来计算键的哈希值,这个哈希值随后被用来确定键值对应该放在_tables
的哪个槽位上。如果那个槽位已经有一个链表,新的键值对将被作为一个新的HashNode
添加到链表的末尾;如果槽位是空的,则创建一个新的HashNode
并放在那里。
查找操作也类似:计算键的哈希值,找到对应的槽位,然后遍历链表来查找具有匹配键的节点。
请注意,由于
HashNode
只存储了值T
而没有直接存储键K
,HashTable
类中的KeyOfT
函数对象就变得非常重要了。它负责从值T
中提取出键K
,以便在插入、查找和删除操作中能够正确地比较键。
哈希函数是用于将任意长度的数据映射为固定长度的数值的函数。在哈希表等数据结构中,哈希函数对于性能至关重要,因为它决定了数据如何在哈希表中分布:
template<class K>
struct HashFunc {
size_t operator()(const K& key) { return (size_t)key; }
};
template<>
struct HashFunc<string> {
size_t operator()(const string& s) {
size_t hashi = 0;
for (auto& e : s) {
hashi += e;
hashi *= 31;
}
return hashi;
}
};
两个HashFunc
模板特化定义展示了如何为不同类型的键实现哈希函数。
第一个模板定义是一个通用版本,它将键K
直接转换为size_t
类型并返回。这适用于那些其数值本身就可以作为哈希值的简单类型,例如整数或浮点数。然而,这种转换对于复杂的类型(如自定义类或字符串)可能并不适用,因为这些类型的值可能不适合直接用作哈希值。
第二个模板特化是为string
类型定义的。它使用了一个常见的字符串哈希算法,即所谓的“Rabin-Karp”哈希算法的一个简化版本。这个算法通过遍历字符串中的每个字符,并累加一个与字符值相关的值(在这里是通过将字符值加到hashi
变量中),然后将结果乘以一个常数(在这里是31)。这个算法的目的是生成一个与字符串内容紧密相关的哈希值,同时保持一定的随机性以减少哈希冲突。
这种哈希函数的一个缺点是它可能不是均匀分布的,特别是在处理长字符串时,哈希值可能会快速溢出,导致分布不均匀。此外,如果字符串中包含的字符很多,累加和乘法的结果可能会受到整数溢出的影响,从而导致不可预测的行为。
Find
函数:这个函数用于在哈希表中查找具有给定键的节点。
iterator Find(const K& key) {
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur) {
if (kot(cur->_data) == key)
return iterator(cur, this);
cur = cur->_next;
}
return iterator(nullptr, this);
}
hs(key) % _tables.size()
)。nullptr
的迭代器,表示未找到。Insert
函数:这个函数用于在哈希表中插入一个新的键值对。
pair<iterator,bool> Insert(const T& data) { iterator it = Find(kot(data)); if (it != end()) return { it,false}; if (_n == _tables.size()) { vector<Node*> newTables(_tables.size() * 2, nullptr); for (size_t i = 0; i < _tables.size(); i++) { Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; size_t hashi = hs(kot(cur->_data)) % newTables.size(); cur->_next = newTables[hashi]; newTables[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newTables); } size_t hashi = hs(kot(data)) % _tables.size(); Node* newnode = new Node(data); newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; return { iterator(newnode, this),true }; }
Find
函数来检查是否已存在具有相同键的节点。pair
,并标记为未插入(false
)。_n
等于哈希表的大小_tables.size()
)。
_tables
指向新的、更大的哈希表。Node
,并将其插入到正确的槽位上。_n
,并返回一个包含新节点迭代器的pair
,并标记为已插入(true
)。Erase
函数:这个函数用于从哈希表中删除具有给定键的节点。
bool Erase(const K& key) { size_t hashi = hs(key) % _tables.size(); Node* prev = nullptr; Node* cur = _tables[hashi]; while (cur) { if (kot(cur->_data) == key) { // 删除 if (prev) prev->_next = cur->_next; else _tables[hashi] = cur->_next; delete cur; --_n; return true; } prev = cur; cur = cur->_next; } return false; }
_next
指针),释放该节点的内存,并更新已存储的键值对数量_n
。true
表示删除成功。false
表示删除失败。上述内容不难理解,不做赘述。
哈希桶迭代器用于遍历哈希表中的所有元素。hashtable
的迭代器没有后退操作,也没有定义所谓的逆向迭代器。且以哈希桶为底层的unordered_xxx
都是单向迭代器。
单向迭代器在大多数情况下已经足够满足哈希桶的遍历需求。哈希桶主要用于存储和快速查找键值对,而单向迭代器能够按顺序遍历桶中的元素,满足基本的遍历需求。如果需要反向遍历或进行更复杂的操作,通常可以在外部逻辑中处理,而不必要求迭代器本身支持这些功能。
template<class K, class T, class KeyOfT, class Hash>
struct __HTIterator {
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef __HTIterator<K, T, KeyOfT, Hash> Self;
Node* _node;
HT* _ht;
};
Node
: 指向HashNode<T>
类型,表示哈希表中的一个节点。HT
: 指向HashTable<K, T, KeyOfT, Hash>
类型,表示整个哈希表。Self
: 指向迭代器自身的类型,方便在迭代器内部引用自身。_node
: 指向当前哈希节点的指针。_ht
: 指向哈希表的指针,用于迭代器内部的操作,比如寻找下一个非空桶。__HTIterator(Node* node, HT* ht)
: 接收一个指向节点的指针和一个指向哈希表的指针,用于初始化迭代器。解引用操作:
T& operator*() { return _node->_data; }
T* operator->() { return &_node->_data; }
T& operator*()
: 返回当前节点存储的数据的引用。T* operator->()
: 返回当前节点存储数据的指针,允许使用箭头操作符访问节点的数据成员。比较操作:
bool operator!=(const Self& s) { return _node != s._node; }
bool operator==(const Self& s) { return _node == s._node; }
bool operator!=(const Self& s)
: 比较当前迭代器和另一个迭代器是否不等,通过比较它们的节点指针实现。bool operator==(const Self& s)
: 比较当前迭代器和另一个迭代器是否相等,不仅比较节点指针,还比较它们所属的哈希表指针,确保它们在同一个哈希表中。递增操作:
Self& operator++() { if (_node->_next) { _node = _node->_next; } else { KeyOfT kot; Hash hs; size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size(); hashi++; while (hashi < _ht->_tables.size()) { if (_ht->_tables[hashi]) { _node = _ht->_tables[hashi]; break; } hashi++; } if (hashi == _ht->_tables.size()) { _node = nullptr; } } return *this; }
Self& operator++()
: 实现迭代器的递增功能,即移动到下一个节点。
_node->_next
不为空),则迭代器直接移动到下一个节点。_node
设置为nullptr
,表示迭代器已经到达哈希表的末尾。这个迭代器设计适用于基于哈希表的关联容器,能够按照桶的顺序遍历元素。当桶内部存在链表来处理哈希冲突时,迭代器能够正确地在链表内部进行遍历。
operator++
中,通常,KeyOfT
和Hash
这些函数作为模板参数传递给哈希表的,并在哈希表内部使用。KeyOfT
提取了键,哈希函数Hash
通过提取出来的键来计算哈希值。HashTable
模板类中,_tables
是一个私有成员变量,它存储了指向HashNode<T>
类型对象的指针。这个_tables
通常表示哈希表的内部存储结构,也就是所谓的“桶”(buckets),每个桶可能包含一个链表或其他结构来处理哈希冲突。
__HTIterator
是一个模板结构体,它作为HashTable
的迭代器。由于迭代器需要访问HashTable
的私有成员(特别是_tables
),通常迭代器会被声明为HashTable
的友元(friend)。这样,__HTIterator
就可以访问HashTable
的私有成员变量,包括_tables
,以便能够正确地遍历哈希表中的元素。
在HashTable
模板类中,通过以下方式声明了__HTIterator
为友元:
template<class K, class T, class KeyOfT, class Hash>
friend struct __HTIterator;
这意味着对于任何HashTable
的特定实例,其对应的__HTIterator
实例都可以访问该HashTable
的私有成员。
iterator
是__HTIterator<K, T, KeyOfT, Hash>
的一个类型别名,定义在HashTable
的公共部分。这样,用户可以使用HashTable::iterator
来引用迭代器类型,而不需要写出完整的模板实例化。
现在,关于迭代器如何访问哈希桶内的私有变量:
__HTIterator
是HashTable
的友元,它可以直接访问_tables
等私有成员。HashTable
类的成员函数来执行,以确保哈希表的一致性和正确性。下面我给出哈希桶及其迭代器的完整实现:
namespace hash_bucket { template<class T> struct HashNode { HashNode<T>* _next; T _data; HashNode(const T& data) :_next(nullptr), _data(data) {} }; template<class K, class T, class KeyOfT, class Hash > class HashTable; template<class K, class T, class KeyOfT, class Hash> struct __HTIterator { typedef HashNode<T> Node; typedef HashTable<K, T, KeyOfT, Hash> HT; typedef __HTIterator<K, T, KeyOfT, Hash> Self; Node* _node; HT* _ht; __HTIterator(Node* node, HT* ht) :_node(node), _ht(ht) {} T& operator*() { return _node->_data; } T* operator->() { return &_node->_data; } bool operator!=(const Self& s)const { return _node != s._node; } bool operator==(const Self& s) const { return _node == s._node; } Self& operator++() { if (_node->_next) { _node = _node->_next; } else { KeyOfT kot; Hash hs; size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size(); hashi++; while (hashi < _ht->_tables.size()) { if (_ht->_tables[hashi]) { _node = _ht->_tables[hashi]; break; } hashi++; } if (hashi == _ht->_tables.size()) { _node = nullptr; } } return *this; } }; template<class K, class T, class KeyOfT, class Hash > class HashTable { typedef HashNode<T> Node; template<class K, class T, class KeyOfT, class Hash> friend struct __HTIterator; public: typedef __HTIterator<K, T, KeyOfT, Hash> iterator; 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() //:kot(KeyOfT()),hs(Hash()) { _tables.resize(10, nullptr); _n = 0; kot = KeyOfT(); hs = Hash(); } ~HashTable() { 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; } } pair<iterator,bool> Insert(const T& data) { iterator it = Find(kot(data)); if (it != end()) return { it,this }; if (_n == _tables.size()) { vector<Node*> newTables(_tables.size() * 2, nullptr); for (size_t i = 0; i < _tables.size(); i++) { Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; size_t hashi = hs(kot(cur->_data)) % newTables.size(); cur->_next = newTables[hashi]; newTables[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newTables); } size_t hashi = hs(kot(data)) % _tables.size(); Node* newnode = new Node(data); newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; return { iterator(newnode, this),true }; } bool Erase(const K& key) { size_t hashi = hs(key) % _tables.size(); Node* prev = nullptr; Node* cur = _tables[hashi]; while (cur) { if (kot(cur->_data) == key) { // 删除 if (prev) prev->_next = cur->_next; else _tables[hashi] = cur->_next; delete cur; --_n; return true; } prev = cur; cur = cur->_next; } return false; } iterator Find(const K& key) { size_t hashi = hs(key) % _tables.size(); Node* cur = _tables[hashi]; while (cur) { if (kot(cur->_data) == key) return iterator(cur, this); cur = cur->_next; } return iterator(nullptr, this); } private: vector<Node*> _tables; size_t _n; KeyOfT kot; Hash hs; }; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。