数据结构的学习已经接近尾声了,最后我学习了哈希表的一些相关知识,了解了源代码的大致框架,在最后模拟实现一次哈希桶,这是一个比较考验代码能力的练习,也算是一次自我挑战吧。
哈希算法就是给定参数的值通过一个哈希函数映射一个数组中的位置,然后将该参数存入哈希表。由于存在一对一映射关系,这种存储方式的查找已经存储的东西是最快的。但是由于哈希函数一般不会给出连续的空间,因此消耗的空间也较之前的存储方式更多(很多闲置的空间),所以这是一种典型的空间换时间交换时间的算法。
但是,不同的参数有时候会映射到同一个位置上,这种问题被称为哈希冲突,解决哈希冲突的算法很多,其中常用的方法有线性探测,二次探测和哈希桶等。我们要实现一个用哈希桶解决哈希冲突的哈希表(尽量和stl保持一致)。
哈希桶就是当不将参数直接存入数组中,而是在数组中放置一个节点指针,当映射在数组对应的位置时,new一个节点空间,将该节点“挂”到这个指针数组的对应指针下面,如果再冲突,就再挂到下面。脑海里就是一个数组+链表的存储方式。结构和算法就是这样接下来话不多说,放码过来。
#include <iostream>
#include <vector>
#include <windows.h>
#include <assert.h>
using namespace std;
template<class V>
struct HashNode //哈希节点
{
V _valuefield;
HashNode<V>* _next;
HashNode()
{}
HashNode(const V& valuefield)
:_valuefield(valuefield), _next(NULL)
{}
};
template < class K, class V, class Keyofvalue,class _Hashfunc>
class HashTable;
template<class K> //哈希函数
struct __Hashfunc
{
size_t operator()(const K& key , const size_t size)
{
return key % size;
}
};
template<>
struct __Hashfunc<string>//string对象经常被存储,因此在这里顺便实现。
{
size_t SDBMHash(const char *str )
{
unsigned int hash = 0;
while (*str)
{
// equivalent to: hash = 65599*hash + (*str++);
hash = (*str++) + (hash << 6) + (hash << 16) - hash;
}
return (hash & 0x7FFFFFFF);
}
size_t operator()(const string& s, const size_t size)
{
return SDBMHash(s.c_str()) % size;
}
};
template<class K,class V>
struct _Keyofvalue //实现keyofvalue是因为我们往节点里存的不一定是一个值,但是我们用来映射到数组的某个位置时,却只需要一个值,这个仿函数要做的
//就是告诉我们到底需要哪个值
{
const K& operator()(const V& Valuefield)
{
return Valuefield;
}
};
template<class K, class V>
struct _PairKeyofvalue
{
const K& operator()(const pair<K,V>& Valuefield)
{
return Valuefield.first;
}
};
template<class K, class V, class Keyofvalue,class _Hashfunc>
struct HashTableIterator//迭代器
{
typedef typename HashTable<K, V, Keyofvalue, _Hashfunc> _Table;
typedef HashNode<V> Node;
typedef HashTableIterator<K, V, Keyofvalue, _Hashfunc> Iterator;
HashTableIterator()
{}
HashTableIterator(Node* node, _Table* tb)
:_node(node), _tb(tb)
{}
Node& operator*()const
{
return _node->_valuefield;
}
Node* operator->()const
{
return _node;
}
bool operator!=(const Iterator& it)const
{
return _node != it._node;
}
Iterator& operator++()
{
assert(_node);
if ((_node->_next)!= NULL)
_node = _node->_next;
//找到本迭代器对应的index,并且返回下一个不为空的index的值
else
{
_Hashfunc hashfunc;
Keyofvalue keyofvalue;
size_t index = hashfunc(keyofvalue(_node->_valuefield), _tb->_table.size());
index++;
while ((index) < (_tb->_table.size()) && ((_tb->_table[index]) == NULL))
index++;
if (index >= _tb->_table.size())
_node = NULL;
else
_node = _tb->_table[index];
}
return *this;
}
Node* _node;
_Table* _tb;
};
template < class K, class V, class Keyofvalue = _Keyofvalue<K,V> , class _Hashfunc = __Hashfunc <K>>
class HashTable
{
typedef HashNode<V> Node;
typedef V Valuetype;
public:
typedef typename HashTableIterator<K, V, Keyofvalue, _Hashfunc> Iterator;
friend HashTableIterator<K, V, Keyofvalue, _Hashfunc>;//迭代器要访问哈希表的数据。
HashTable()
:_size(0)
{}
~HashTable()
{
destory();
}
Node* find(const K& key)
{
_Hashfunc hashfunc;
Keyofvalue keyofvalue;
size_t index = hashfunc(key, _table.size());
Node* cur = _table[index];
while (cur)
{
if (keyofvalue(cur->_valuefield) == key)
break;
cur = cur->_next;
}
return cur;
}
Iterator Begin()
{
if (_size==0)
return Iterator(NULL, this);
Node* cur = _table[0];
size_t i = 0;
while (cur == NULL)
{
cur = _table[++i];
}
return Iterator(cur, this);
}
Iterator End()
{
return Iterator(NULL, this);
}
bool insert(const Valuetype& valufield)//插入
{
Checkcapcity();//判断是否需要增容,这个操作是哈希表中最耗费时间的。
Keyofvalue keyofvalue;
if (find(keyofvalue(valufield)) != NULL)
{
return false;
}
else{
_Hashfunc hashfunc;
size_t index = hashfunc(keyofvalue(valufield), _table.size());
Node* dst = new Node(valufield);
dst->_next = _table[index];
_table[index] = dst;
_size++;
return true;
}
}
size_t Size()
{
return _size;
}
bool remove(const K& key)//删除
{
Keyofvalue keyofvalue;
_Hashfunc hashfunc;
size_t index = hashfunc(key, _table.size());
Node* cur = _table[index];
if (cur == NULL)
{
return false;
}
if (keyofvalue(cur->_valuefield)== key)
{
_table[index] = cur->_next;
cur->_next = NULL;
delete cur;
_size--;
return true;
}
else
{
Node* prev = cur;
cur = cur->_next;
while (cur)
{
if (keyofvalue(cur->_valuefield) == key)
{
prev->_next = cur->_next;
cur->_next = NULL;
delete cur;
_size--;
return true;
}
}
return false;
}
}
protected:
void destory()
{
size_t i = 0;
for (i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
Node* next = NULL;
while (cur)
{
next = cur->_next;
delete cur;
cur = next;
}
}
}
void Checkcapcity()
{
if (_size == 0)
_table.resize(10);
else if (_size == _table.size())
{
vector<Node*> newtable;
Keyofvalue keyofvalue;
_Hashfunc hashfunc;
newtable.resize(_size * 3 + 3);
for (size_t i = 0; i < _size; i++)
{
Node* cur = _table[i];
_table[i] = NULL;
Node* next = NULL;
while (cur)//重新负载,如果能提前知道空间打小对应开辟空间,哈希表的各种操作将会非常快。
{
next = cur->_next;
int index = hashfunc(keyofvalue(cur->_valuefield), newtable.size());
cur->_next = newtable[index];
newtable[index] = cur;
cur = next;
}
}
_table.swap(newtable);
}
}
protected:
vector<Node*> _table;
size_t _size;
};
void TestHashTable()
{
HashTable<int, int> t;
/*int i = 0;
for (i = 30; i < 80; i++)
{
t.insert(i);
}*/
t.insert(1);
t.insert(2);
t.insert(11);
/*t.remove(1);
t.remove(2);
t.remove(11);
t.remove(13);*/
HashTable<int, int> ::Iterator it = t.Begin();
while (it != t.End())
{
cout << it->_valuefield<<' ';
++it;
}
cout <<endl;
//cout << t.find(1) << endl;
HashTable<string, pair<string, int>, _PairKeyofvalue<string, int>> t1;
t1.insert(make_pair("lizi", 1));
t1.insert(make_pair("xiangjiao", 3));
t1.insert(make_pair("taozi", 6));
t1.insert(make_pair("pingguo", 55));
/*t1.remove(5);
t1.remove(5);
t1.remove(5);
t1.remove(55);*/
//cout << t.find(2) << endl;
HashTable<string, pair<string, int>, _PairKeyofvalue<string, int>> ::Iterator it1 = t1.Begin();
while (it1 != t1.End())
{
cout << ((it1->_valuefield).first).c_str() << ":" << it1->_valuefield.second << endl;
++it1;
}
cout << endl;
}
哈希表有一个负载因子,用来控制空间的分配(当数据存放多到什么时候应该增容),负载因子=允许负载的最大节点数/当前数组空间的大小,细心的访客已经看到了,我这个哈希表的负载因子为1.负载因子的选取是哈希表性能的关键,太大了会导致冲突变多,太小了会导致空间浪费严重。
也许有人不明白为什么要这样写,这样使用起来很麻烦啊。答案是你们说的没错,就是很麻烦,但是我们这样写全部是为了代码复用性好。可能大家不熟悉,在STL中,unordered_map和unordered_set其实原型都是这个哈希桶,只是在这上层做了一个简单的封装,就可以将两个容器的代码,共用一个模板,举个例子,我们使用unordered_set<K>(一个模板参数)时,将模板的key和value都使用成一个类型如上图中的HashTable<K,K> ,即为unordered_set(将两个模板参数传成相同的),而我们使用unordered_map<K,V>时,只是将模板改变一下变为HashTable<K,std::pari<K,V>>即可,这样设计是不是很省事的就将一份代码完成了两份工作呢?只是可能这样子设计一开始会让我们想窥探源码的人看的头皮发麻,但是当我们搞清楚这样设计的目的,再返回来看STL的源代码,就可以理解其中蕴含的高深的思想。(经常使用的map和set也是这样设计的哦,不过底层结构是红黑树而已)。
当然,哈希表也是有缺陷的,在极端情况下,如果数据就是‘’不听话‘’,一直冲突在某一个位置,这样还是会导致空间浪费严重, 并且时间复杂度也不再是O(1),因此还有扩展,当哈希桶的某个指针下的节点长度大于一定程度时,不再是链式存储,而转换为红黑树(一种平衡搜索树),就可以有效解决这种问题。