C++:哈希-->unordered_map/unordered_set
unordered_map/unordered_set
map/set和unordered_map/unordered_set的区别和联系
//1.它们都可以实现key/key-value的搜索场景,并且功能和使用基本一致
//2.map/set的底层是使用红黑树实现的,遍历出来是有序的,增删查改的时间复杂度是O(logN)
//3.unordered_map/unordered_set的底层是使用哈希表实现的,遍历出来是无序的,增删查改的时间复杂度是O(1)
//4.map和set是双向迭代器,unordered_map/unordered_set是单向迭代器
哈希表
1.直接定址法:映射值跟关键字直接或间接相关的(会浪费空间)
如果数据分布不均匀,就会造成很大的空间浪费(线性映射的情况下距离又很远)
2.除留余数法:key值跟关键字取余得到的值就确定了key要存入的地址。
闭散列:开放定址法,如果遇到哈希冲突,此时哈希表还没满,就找“下一个”空位置放进去。(按照线性探测或二次探测找下一个空位置),线性探测遇到哈希冲突,就往后“抢占”别人的位置...会造成更多的冲突,二次探测 i**2(i=0,1,2...),将冲突分散开,不会周围拥堵在一块。
---插入key:取余找相对位置,存的不是就找下一个位置,遍历下一个位置状态为EXITS就一直往下找,当找到末尾就从0开始继续。
---查找key:取余找相对位置的树,如果为空或相等但状态为删除 返回nullptr,如果存在就继续往下找,当遇到EMPTY状态的空格直接返回FALSE,DELETE或EXITS则继续往下找。
---删除key:调查找函数,查找到就将该元素的状态改为DELETE,没找到就返回FALSE。
---增容:闭散列哈希表不能满了再增容,因为哈希表快满了时候插入数据,冲突的概率很大,效率会很低;所以在接近满时就要增容。提出负载因子的概念来衡量哈希表满的程度—负载因子=表中数据个数/表的大小;一般情况闭散列的负载因子到0.7就要进行扩容,负载因子过小会造成空间浪费,过大效率会降低。
a.开一个原数组2倍大小的新表 b.根据新的除数重新映射 c.释放旧表
开散列:拉链法(哈希桶),闭散列当冲突时不论是线性探测还是二次探测的方式,都会占用到别人的位置,造成更多的冲突;但是哈希桶当遇到冲突时,会在自身的位置挂起来,不会影响别人。
当大量的数据发生哈希冲突,这些冲突的数据就会挂在哈希桶中,但当数据挂的太多时,就会导致查找的效率降低;这时候也需要负载因子来衡量,一般将开散列的负载因子控制到1比较合适。
在Java中,当一个位置挂的节点过多时,就改为挂红黑树(针对单个桶的极端情况)。
---插入:
算出要插入key和表大小取余的结果,表明key要插入的位置。遍历该位置上的节点,确保没有重复的key,将key头插或尾插进去。
size_t index = koft(data) % _tables.size();
Node* cur = _tables[index];//取当前节点的桶地址
while (cur->_next)
{
if (koft(cur->_data == koft(data)))
{
return false;//当遇见相等值时
}
else cur = cur->_next;
}
//2.头插到挂的链表中(尾插也可以)
newnode->_next = tables[index];//没有创建newnode
tables[index] = newnode;
++num;
return true;
---增容:
当一个地址悬挂的节点过多时,也会影响查找的效率,此时就需要增容。当结点数与表的大小差不多相等时我们进行增容。研究得:当表的大小维持一个素数时,哈希冲突的概率会大大降低,所以我们每次增容都保证它是一个素数。给出下面28个素数按照这样的排列进行增容。
size_t GetNextPrime(size_t prime)
{
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];
}
---查找:将查找的key与表的大小取余找到它的地址,遍历该地址悬挂的节点,找到则返回该结点,找不到返回空指针。
Node* Find(const K& key)
{
KeyOfT koft;
size_t index = key % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (koft(cur->_data) == key) return cur;
else cur = cur->_next;
}
return nullptr;
}
---删除:将要删除的key与表的大小取余找到它的地址,遍历该地址悬挂的节点,遍历时保存当前节点及其父节点,当找到key,令其父节点指向其子节点,删除key所在节点。
bool Erase(const K& key)
{
KeyOfT koft;
size_t index = key % _tables.size();
Node* cur = _tables[index];
Node* pre = nullptr;
while (cur)
{
if (koft(cur->_data) == key)
{
if(pre) pre->_next = cur->_next;
else _tables[index] = cur->_next;
delete cur;
return true;
}
else
{
pre = cur;
cur = cur->_next;
}
}
return false;
}
---取余(注意事项):对于取余找key对应位置这个操作,对于自定义类型是无法完成的;对此我们要通过定义仿函数来处理key,使处理之后的key可以完成取余操作。我们首先定义一个通用的、针对部分内置类型如int是无需处理的,也就是处理后的结果仍为它本身;之后我们开始特化这个仿函数,当需要处理某种自定义类型的key时,就实现该模板的一种特化,例如:实现string类型Key的特化,在特化的仿函数类中处理它:遍历该string,每加一个字母就提前*131...算出其累加和并返回(DKDR算法),返回这个整数。
template<class K>
struct _Hash
{
const K& operator()(const K& key)
{
return Key;//int调用传模板参返回原值
}
};
template<>//使用特化并且模板参数是缺省就不用额外去传仿函数参数在模板中了,默认就调用并且识别string类型了
struct _Hash <string>//特化
{//string调用传模板参数返回累加值
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); ++i)
{//DKDR hash可以减少哈希冲突
hash *= 131;
hash += key[i];
}
return hash;
}
//如果是一个结构体,要写它的仿函数类,就找结构体中的唯一项!!!
};//只要key的类型不支持取模,我们就要再写一个可以取余的仿函数
unordered_map 和 unordered_set实现:
unordered_map/unordered_set就是对哈希表的应用和封装。前者传的是pair,后者传的是key,
template<class K,class Hash = _Hash<K>>
class unordered_set
{
struct SetKOfT
{
const K& operator()(const K& k)
{
return k;
}
};
public:
private:
HashTable<K, K, SetKOfT,Hash> _ht;
};
template<class K,class V, class Hash = _Hash<K>>
class unordered_map
{
struct MapKOfT
{
const K& key operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
bool insert(const K& k);
private:
HashTable<K, pair<K, V>, MapKOfT,Hash> _ht;
};
哈希表迭代器的实现:
如何定义哈希表的迭代顺序,通常有两种。第一种是按照插入节点的顺序迭代器遍历,要实现这种遍历方式,就要对上面的哈希表结构进行改变,首先节点必须多包涵一个指向上一个插入节点的指针和指向下一个插入节点的指针,再每次插入新节点时进行链接需要保留上一个节点,在删除某一个节点时,需要它插入前的一个节点指向它插入后的那个节点,增设这些操作过于复杂,我们实现的是第二种遍历方式--按照hash表的顺序遍历,每当某个桶有值时就遍历这个桶。这里存在一个问题:当一个桶遍历完时怎么找下个桶?此时就需要将当前的哈希表对象传入迭代器类中,用到了组合的方式,在迭代器类中定义了成员变量*node和哈希表对象,在构造函数中利用传参初始化_node*和哈希表对象。在实现迭代器时注意,哈希桶的节点是单链表的形式链接起来的,所以不能进行--操作。只能实现operator ++()、operator *()、operator ->()、operator ==()、operator !=()
template <class K, class V, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<K, V, KeyOfValue, HF> HashBucket;
typedef HashBucketNode<V>* PNode;
typedef HBIterator<K, V, KeyOfValue, HF> Self;
HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr);
Self& operator++()
{
// 当前迭代器所指节点后还有节点时直接取其下一个节点
if (_pNode->_pNext)
_pNode = _pNode->_pNext;
else
{
// 找下一个不空的桶,返回该桶中第一个节点
size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode -
> _data)) + 1;
for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
{
if (_pNode = _pHt->_ht[bucketNo])
break;
}
}
return *this;
}
Self operator++(int);
V& operator*();
V* operator->();
bool operator==(const Self& it) const;
bool operator!=(const Self& it) const;
PNode _pNode; // 当前迭代器关联的节点
HashBucket* _pHt; // 哈希桶--主要是为了找下一个空桶时候方便
};