#include
#include
#include
#include
#include
using namespace std;
void test_time()
{
int n = 1000000;
vector<int> v;
srand(time(0)); //初始化随机数发生器
for (int i = 0; i < n; ++i)
{
v.push_back(rand()); //随机数发生器
}
set<int> s;
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set : " << end1 - begin1 << endl;
unordered_set<int> us;
size_t begin2 = clock();
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "unordered_set : " << end2 - begin2 << endl;
}
int main()
{
test_time();
}
通过对于很多个数的插入(数越多,插入之间所消耗的时间差距越大),其实也是能够看出来HashTable的优越之处。
可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系
,那么在查找时通过该函数可以很快找到该元素。
哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
常见哈希函数
直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况。
除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
平方取中法
折叠法
随机数法
数学分析法
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列
:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
线性探测
:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
伪删除法
来删除一个元素。载荷因子
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起
,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。(引发踩踏效应,就像我的位置被人占了,我就去占别人的位置,后来的人都效仿,会导致效率变低)
二次探测
:线性探测的缺陷是产生冲突的数据堆积在一块,这和找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,他不再是挨着找下一个空位置,而是平方式的跳跃找下一个空位置,这样冲突就不会堆积在一片,而是会相对散开一些。
比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
namespace Close
{
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class T>
struct HashNode
{
State _state = EMPTY;
T _t;
};
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
//模板特化
template<>
struct Hash < string >
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
//hash += ch;
hash = hash * 131 + ch; //
}
return hash;
}
};
//这里和使用红黑树来封装map和set是一样的,都是由第二个模板参数来控制存储的类型是K,还是pair
template<class K, class T, class HashFunc = Hash<K>>//需要把这里的K转换为整形
class HashTable
{
public:
bool Insert(const T& t)
{
//载荷因子= 填入表中的元素个数 / 散列表的长度
//负载因子>0.7(严格控制在0.7以下)
//只有在这种情况下:效率相对较高,并且空间利用率也较好
if (_tables.size() == 0 || _size * 10 / _tables.size() == 7)
{
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
//但是这里是不对的,因为有可能原来冲突的,由于空间的扩容,而不再是冲突的了,
//所以我们应该开辟好一段空间,然后重新对上面的数据进行计算,然后放入这段空间内,在释放原来的空间
//vector> newtables;
//newtables.resize(newsize);
此时就是把原空间上的数据,拿来重新计算放到相对应的新空间上
//for (size_t i = 0; i < _tables.size(); ++i)
//{
// if (_tables[i]._state == EXIST)
// {
// //线性探测找在新表中的位置
// }
//}
//newtables.swap(_tables);
HashTable<K, T, HashFunc> newht;
newht._tables.resize(newsize);
for (auto& e : _tables)
{
if (e._state == EXIST)
{
//重新计算位置,然后放在这个新的表中
newht.Insert(e._t); //拿旧空间的数据重新计算我嫌弃太麻烦,我还不如直接重新来一遍插入呢
}
}
_tables.swap(newht._tables);
}
//不允许数据冗余
HashNode<T>* ret = Find(t);
if (ret)
return false;
HashFunc hf;
//这里在一开始的时候,你的哈希表里面是没有元素的,所以会出现模0,出错情况
size_t start = hf(t) % _tables.size();
size_t index = start; //这样写的好处就是,能够在二次探测的时候方便修改
//线性探测,找一个空位置
size_t i = 1;
while (_tables[index]._state == EXIST) //应该去找下一个空位置
{
index = start + i;
index %= _tables.size();
++i;
}
//跳出循环就是两种情况1.EMPTY 2. DELETE这两种情况都可以把值放进去
_tables[index]._t = t;
_tables[index]._state = EXIST;
_size++;
return true;
}
HashNode<T>* Find(const K& key)
{
HashFunc hf;
size_t start = hf(key) % _tables.size();
size_t index = start;
size_t i = 1;
while (_tables[index]._state != EMPTY)
{
if (_tables[index]._t == key && _tables[index]._state == EXIST) //因为可能找到的这个值被删除掉了
{
return &_tables[index];
}
index = start + i;
index %= _tables.size();
++i;
}
return nullptr;
}
bool Erase(const K& key)
{
HashNode<T>* ret = Find(key);
if (ret == nullptr)
{
return false;
}
else
{
//伪删除
ret->_state = DELETE;
return true;
}
}
private:
vector<HashNode<T>> _tables;
size_t _size = 0; //有效数据的个数
};
}
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
那如果就是出现了极端的情况,所有的数此时都冲突到一个桶中,那么这个桶中的数据就会太多了,应该怎么办?
字符串哈希算法
,最常使用的就是C语言之父所写的BKDRHash算法,也就是给每个字符先乘131,然后在把字符的ASCII码进行相加。当然这里还是用到了模板的特化
,当数据是具体的string的时候,就会调用这个仿函数进行计算。namespace Open
{
size_t GetNextPrime(size_t prime)
{
static const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
template<class T>
struct HashLinkNode
{
T _t;
HashLinkNode<T>* _next;
HashLinkNode(const T& t)
: _t(t)
, _next(nullptr)
{}
};
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct Hash < string >
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
//hash += ch;
hash = hash * 131 + ch;
}
return hash;
}
};
//前置声明,因为在构造迭代器的时候,需要HashTable,但是这里模板还没有进行特化,找不到
template<class K, class T, class KeyOfT, class hash>
class HashTable;
//对于封装迭代器来说,++那么就会跳到桶里面的下一个结点的位置,但是当这个桶不再有结点的时候,就需要直接的跳到下一个有hash映射的位置
template<class K, class T, class Ref,class Ptr ,class KeyOfT, class hash>
struct HashIterator
{
typedef HashIterator<K, T, Ref,Ptr,KeyOfT, hash> Self;
typedef HashLinkNode<T> Node;
Node* _node;
HashTable<K, T, KeyOfT, hash>* _pht; //还需要一个指向哈希表的指针,因为需要遍历哈希表,如果此时这个通遍历完了
//能够帮助找到下一个需要到跳跃的位置
HashIterator(Node* node, HashTable<K, T, KeyOfT, hash>* pht)
:_node(node)
, _pht(pht)
{}
Ref operator*()
{
return _node->_t;
}
Ptr operator->()
{
return &(_node->_t);
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
//哈希表的迭代器是没有--的,因为这是一个单项迭代器
//对于operator++(前置++)来说,加完之后依旧返回的是一个迭代器的指针
//1. 当前桶还有数据,继续走
//2. 当前桶没有数据,跳到下一个桶,从第一个开始
Self operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
KeyOfT kot;
//一个桶已经走完了,找到下一个桶
//因为我在当前桶是可以算出我的位置的,但是还需要一个hash表
//走到这里说明我算出来的index的原来位置,已经全部都走完了,那么应该从他的下一个位置开始计算
size_t index = _pht->HashFunc(kot(_node->_t),_pht->_tables.size());
++index;
while (index < _pht->_tables.size())
{
if (_pht->_tables[index])
{
//就开始遍历这个桶
_node = _pht->_tables[index];
break;
}
else
{
++index;
}
}
//但是while结束有可能是两种可能1.break跳出来的 2.循环走结束了
//所以这两种情况最好能够区分一下
if (index == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
template<class K,class T,class KeyOfT,class hash = Hash<K>>
class HashTable
{
typedef HashLinkNode<T> Node;
friend struct HashIterator < K, T, T&, T*, KeyOfT, hash > ;
public:
typedef HashIterator<K, T, T&,T*,KeyOfT, hash> Iterator;
typedef HashIterator<K, T, const T&, const T*, KeyOfT, hash> Const_Iterator;
Iterator Begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return Iterator(_tables[i],this); //this是指向HashTable的指针
}
}
//如果哈希表中一个数据也没有,那么返回nullptr也是正确的
return End();
}
Iterator End()
{
return Iterator(nullptr,this);
}
size_t HashFunc(const K& key, size_t n)
{
hash hf;
size_t ki = hf(key);
return ki % n;
}
pair<Iterator, bool> Insert(const T& t)
{
//对于开散列/哈希桶依旧要考虑负载因子的问题,但是一般控制在1,也就是满了在加,空间利用率更高
//自己冲突的多,只会影响我自己,不会影响别人
KeyOfT kot;
//控制负载因子 == 1的时候增容
if (_size == _tables.size())
{
size_t newsize = GetNextPrime(_tables.size());//得到素数,有效验证能够提高效率
vector<Node*> newtables;
newtables.resize(newsize,nullptr);
//循环的把每个哈希桶拿过来,重新挂接
for (size_t i = 0; i < _tables.size(); ++i)
{
//旧表中结点直接取下来挂在新表中
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t index = HashFunc(kot(cur->_t),newtables.size());//重新计算在所开辟的新表中的位置
//头插
cur->_next = newtables[index];
newtables[index] = cur;
cur = next;
}
_tables[i] = nullptr;//因为你算来的表 指针可能还指向着这个位置
}
newtables.swap(_tables);
}
//需要得到下标的位置
size_t index = HashFunc(kot(t),_tables.size());//首先把T里面的key提取出来,但是还是不能够确定是否可以取模,所以还需要在套用一层
//不允许键值冗余,所以需要查找t在不在
//先找到那个位置,然后在遍历整个哈希桶
Node* cur = _tables[index];
while (cur)
{
if (kot(cur->_t) == kot(t))
{
return make_pair(Iterator(cur,this), false);
}
cur = cur->_next;
}
//对于哈希表中一开始存储的是NULL,当底下有挂接的时候,存储的将会变为第一个结点的地址
//头插到链表桶里面
Node* newnode = new Node(t);
newnode->_next = _tables[index];
_tables[index] = newnode;
return make_pair(Iterator(newnode,this), true);
}
//Find就是通过Key来寻找的
Iterator Find(const K& key)
{
KeyOfT kot;
size_t index = HashFunc(key,_tables.size());
Node* cur = _tables[index];
//现在hash表中找到位置,然后在遍历该位置的整个hash桶
while (cur)
{
if (kot(cur->_t) == key)
{
return Iterator(cur,this);
}
cur = cur->_next;
}
return End();
}
bool Earse(const K& key)
{
KeyOfT kot;
size_t index = HashFunc(key, _tables.size());
//这里就是一个单链表的删除
Node* prev = nullptr;
Node* cur = _tables[index];
//现在hash表中找到位置,然后在遍历该位置的整个hash桶
while (cur)
{
if (kot(cur->_t) == key)
{
//就进行删除
if (prev == nullptr)
{
//说明删除的是头
_tables[index] = cur->_next; //链表挂接的第一个就直接找到了
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
private:
vector<Node*> _tables;
size_t _size = 0;//有效数据的个数
};
}
#pragma once
#include "HashTable.hpp"
//其实和map和set一样,本身他们自身是没有什么东西的,他们的种种操作都是在红黑树的基础上所实现的
//现在的unordered_set和unordered_map也是,都是在开散列HashTable基础上所实现的
namespace wzy
{
template<class K,class hash = Open::Hash<K>> //对于orderedset_set来说你想怎样使用,你就怎么控制这个模板参数
class unordered_set
{
struct SetKOfT
{
const K& operator()(const K& k)
{
return k;
}
};
public:
typedef typename Open::HashTable<K, K, SetKOfT, hash>::Iterator iterator;
iterator begin()
{
return _t.Begin();
}
iterator end()
{
return _t.End();
}
iterator find(const K& key)
{
return _t.Find(key);
}
bool earse(const K& key)
{
return _t.Earse(key);
}
pair<iterator, bool> insert(const K& k)
{
return _t.Insert(k);
}
private:
Open::HashTable<K, K, SetKOfT, hash> _t;
};
void test_unordered_set()
{
wzy::unordered_set<int> us;
us.insert(1);
us.insert(54);
us.insert(58);
us.insert(59);
us.insert(21);
us.insert(22);
us.insert(23);
us.insert(24);
for (auto& e : us)
{
cout << e << " ";
}
cout << endl;
unordered_set<int>::iterator it = us.find(22);
cout << *it << endl;
us.earse(24);
us.earse(21);
us.earse(22);
for (auto& e : us)
{
cout << e << " ";
}
cout << endl;
}
}
#pragma once
#include"HashTable.hpp"
namespace wzy
{
template<class K,class V,class hash = Open::Hash<K>>
class unordered_map
{
struct MapKOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename Open::HashTable<K, pair<const K, V>, MapKOfT, hash>::Iterator iterator;
pair<iterator, bool> insert(const pair<const K, V>& kv)
{
return _ht.Insert(kv);
}
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool earse(const K& key)
{
return _ht.Earse(key);
}
V& operator[](const K& key)
{
pair<iterator,bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
Open::HashTable<K, pair<const K, V>, MapKOfT,hash> _ht;
};
void test_unordered_map()
{
wzy::unordered_map<int, int> um;
um.insert(make_pair(1, 1));
um.insert(make_pair(2, 1));
um.insert(make_pair(3, 1));
unordered_map<int,int>::iterator it = um.begin();
while (it != um.end())
{
cout << it->first << ":"<<it->second<<endl;
++it;
}
wzy::unordered_map<string,string> dict;
dict["hash"] = "哈希";
dict["sort"] = "排序";
dict["insert"] = "插入";
dict["find"] = "查找";
for (auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout <<endl;
dict.earse("hash");
dict.earse("find");
for (auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
}
}
由于开放定址法必须保持大量的空闲空间以确保搜索效率,并且非常容易发生踩踏效应,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
。相对于开放定址法来说,开链法的空间利用率为1,只有满的时候才需要增容(空间利用率很高),并且不同的位置冲突值不再相互干扰踩踏。