目录
一、闭散列(开放定址定法)
1、哈希表的结构:
2、哈希表的插入:
3、哈希表的查找:
4、哈希表的删除:
二、开散列(哈希桶)
1、哈希表的结构:
2、构造与析构:
3、哈希表的插入:
4、哈希表的查找:
5、哈希表的删除:
三、拓展:
前言:
在模拟实现哈希表的过程中,每当发生哈希冲突之后有两种方法进行解决:分别是开放定址法和链地址法(哈希桶)
一、闭散列(开放定址定法)
闭散列开放定址法是当使用哈希函数计算出的地址发生哈希冲突后,依次向下一个位置去寻找,直到找到空位置,然后填进去。
1、哈希表的结构:
每个位置中存储的数据中需要有两个信息:键值对和这个位置的状态。
键值对:可以是K结构或者是K/V结构。
所处位置的状态:
enum STATE{EXIST,EMPTY,DELETE};
EXIST :存在,表示这里存在数据
EMPTY :空,表示这里是空
DELETE :删除,表示这里的值被删除了,这也是需要状态的原因,因为当对哈希表进行删除的时候,并不是将里面的值进行删除,更好的是将这里面的状态进行修改就可以了。
以下就是哈希表中每个位置所存储的信息
template<class K, class V>struct HashDate{pair<K, V> _kv;STATE _state = EMPTY;};
哈希表的框架:
_table这个是存放哈希表的数组,
_n是这个哈希表中存在元素的个数
template<class K, class V>
class HashTable
{
public:HashTable(){_table.resize(10);}
private:vector<HashDate<K, V>> _table;size_t _n = 0;
};
接着在public下面实现插入,查找,删除等等。
2、哈希表的插入:
思路:
首先通过哈希函数找到所处位置,接着依次判断是否为存在,如果是存在就向后走,如果找到第一个不存在的将这个位置的_kv修改为kv,并且将这个位置的状态修改为EXIST存在。
接着++_n。
但是在插入之前要进行判断,如果负载因子大于0.7的时候就进行扩容。
判断扩容思路:
首先看负载因子的大小,当负载因子大于0.7的时候就进行扩容,首先创建一个新的哈希数组,要resize为原来数组的两倍大小,开好后再进行映射过去。
映射思路:
for循环中,每当找到一个位置的状态为EXIST的时候,就将这个数插入到新开好的数组中,因为在插入的时候会判断负载因子所以就不会出现无限循环,这个新的数组就是我需要的,
最后将_table和新创的哈希表进行交换即可
bool Insert(const pair<K, V>& kv)
{//看负载因子的大小if (_n * 10 / _table.size() >= 7){size_t newsize = _table.size() * 2;HashTable<K, V> ht;ht._table.resize(newsize);//开一个新的哈希数组,把这个原来的映射过去for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){ht.Insert(_table[i]._kv);}}_table.swap(ht._table);}//扩容逻辑size_t hashi = kv.first % _table.size();while (_table[hashi]._state == EXIST){++hashi;hashi = hashi % _table.size();//防止hashi大于capacity}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;
}
注意:
因为存在负载因子的原因,所以哈希表是不可能被装满的。
在扩容的时候,是将原哈希表重新映射到新位置,而不仅仅是拷贝过去,如下图15就可以看出是重新映射而不是拷贝。
3、哈希表的查找:
思路:
首先:通过哈希函数计算出对应的地址。
然后,从哈希地址处开始向后找,直到找到待查找的元素则为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。
注意: 在查找过程中,找到的元素的状态必须是EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了,如果是EMPTY就证明是在这个哈希表中不存在这个元素,查找失败
HashDate<const K, V>* Find(const K& key){size_t hashi = key % _table.size();while (_table[hashi]._state != EMPTY){if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key){return (HashDate<const K, V>*) & _table[hashi];}hashi++;hashi %= _table.size();}return nullptr;}
4、哈希表的删除:
哈希表的删除就比较好搞了,并不是传统意义上的删除,而是直接将所删除的位置的状态修改为DELETE即可。
思路:
首先用Find函数进行查找,没找到删除失败,找到后就将这个位置的状态修改一下,将_n--
bool Erase(const K& key){HashDate<const K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}return false;}
测试插入,查找,删除:
int main()
{open_address::HashTable<int, int> ht;ht.Insert(make_pair(1, 1));ht.Insert(make_pair(9, 9));ht.Insert(make_pair(15, 15));ht.Insert(make_pair(4, 4));ht.Insert(make_pair(13, 13));ht.Insert(make_pair(2, 2));ht.Insert(make_pair(7, 7));ht.Insert(make_pair(17, 17));ht.Insert(make_pair(6, 6));if (ht.Find(7)){cout << "7在哈希表里面" << endl;}else{cout << "7不在哈希表里面" << endl;}ht.Erase(7);if (ht.Find(7)){cout << "7在哈希表里面" << endl;}else{cout << "7不在哈希表里面" << endl;}return 0;
}
二、开散列(哈希桶)
在每一个位置中,不仅仅是只有数据和状态了,还挂着一个单链表,和指向这个单链表的下一个节点的指针,哈希表的每个位置存储的实际上是某个单链表的头结点,
总体框架大概像下面
(每个单链表的整体看做一个哈希桶)
1、哈希表的结构:
每一个节点的定义
template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};
哈希表这个数组中就变成了节点的指针
template<class K, class V>
class HashTable
{typedef HashNode<K, V> Node;
public:private:vector<Node*> _table;size_t _n = 0;
};
2、构造与析构:
构造函数就是将这个哈希表进行初始化,开好空间,都置为空。
析构函数就是将每一个哈希桶都删除掉,然后将哈希表中的每一个指针都置空
HashTable()
{_table.resize(10, nullptr);
}~HashTable()
{for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}
}
3、哈希表的插入:
思路:
总体来说和闭散列的差不多
首先通过find函数查找这个数,如果有就不能够插入但会false,再通过哈希函数找到待插入的位置为hashi,接着new一个新节点作为待插入节点,将这个节点头插到哈希桶中。
待插入节点的next指针指向了newTable的下标为hashi的这个元素中链表的第一个节点,再把newTable[hashi]的值改成cur的指针,就可以吧cur头插到下标为hashi的这个链表中了
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first)){return false;}if (_table.size() == _n){size_t newSize = _table.size() * 2;vector<Node*> newTable;newTable.resize(newSize, nullptr);for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first / newTable.size();//将cur的_next指向newTable所在位置的第一个节点cur->_next = newTable[hashi];//newTable[hashi]处的指针指向curnewTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}size_t hashi = kv.first % _table.size();Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;
}
4、哈希表的查找:
思路:
首先通过哈希函数找到哈希地址,之后遍历当前位置的哈希桶,找到就返回已查找的节点否则返回空。
Node* Find(const K& key)
{size_t hashi = key % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}
5、哈希表的删除:
思路:
首先通过哈希函数找到哈希地址,之后遍历当前位置的哈希桶,找到待删除节点的位置后就将待删除节点的上一个位置的next指针指向待删除节点的下一个位置,在delete待删除节点即可。
bool Erase(const K& key)
{size_t hashi = key % _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}--_n;return false;
}
测试插入,查找,删除:
int main()
{hash_bucket::HashTable<int, int> ht;ht.Insert(make_pair(1, 1));ht.Insert(make_pair(9, 9));ht.Insert(make_pair(15, 15));ht.Insert(make_pair(4, 4));ht.Insert(make_pair(13, 13));ht.Insert(make_pair(2, 2));ht.Insert(make_pair(7, 7));ht.Insert(make_pair(17, 17));ht.Insert(make_pair(6, 6));ht.Print();ht.Erase(17);ht.Erase(13);cout << "删除17,删除13后" << endl;ht.Print();return 0;
}
三、拓展:
上述的哈希表只能插入或者删除整型的,如果是字符串就搞不好,所以就需要仿函数来进行操作,将每一个键值对中取first的通过仿函数将字符串类转化为整型类。
思路:
首先在仿函数中,将模版进行半特化,如下,如果不是string类就走上面的,这就是走个过场,直接返回key,但如果是string类的那么就通过重载运算符(),将str类转化为整型
template<class K>
struct HashFunctest
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct HashFunctest<string>
{size_t operator()(const string& str){size_t n = 0;for (auto e : str){n *= 131;n += e;}return n;}
};
测试:
int main()
{hash_bucket::HashTable<string, string> ht;ht.Insert(make_pair("apple", "苹果"));ht.Insert(make_pair("pear", "梨子"));ht.Insert(make_pair("banana", "香蕉"));ht.Insert(make_pair("watermelon", "西瓜"));ht.Print();cout << "删除pear后" << endl;ht.Erase("pear");ht.Print();return 0;
}