前言
在C++98中,stl提供了底层为红黑树的几个关联式容器,查询时最差的情况就是需要比较红黑树的高度次,当节点变多的时候,查询的效率就不那么明显了,而在C++11中新增的unordered系列的关联式容器,它们的用法和map/set类似,但底层采用的是哈希结构。
关于unordered系列容器
1、unordered_map
属于<Key,Value>键值对的关联性容器,通过key来快速索引到其对应的Value。
这里的value可以和key的类型不同,它们两者之间只是映射关系。
unordered_map在内部没有对键值对进行按顺序排序,同时将相同的哈希值的键值对放置在同一个桶中。
unordered_map在通过key索引到value的速度相比map是较快的,但是遍历其元素子集的范围迭代效率就较慢了
迭代器是前向迭代器。
2、unordered_set
是以特定顺序存储唯一元素的容器,它允许根据单个元素的值快速检索它们。
在unordered_set中,元素的值同时是其键,用于唯一标识它。键是不可变的,因此,unordered_set中的元素在容器中一次就不能修改 ,但是可以插入和删除它们。
在内部,unordered_set中的元素不按任何特定顺序排序,而是根据其哈希值组织到存储桶中,以便直接按其值快速访问单个元素(平均平均时间复杂度恒定)。
unordered_set容器通过其键访问单个元素的速度比设置容器更快,尽管它们通常通过其元素的子集进行范围迭代的效率较低。
容器中的迭代器是前向迭代器。
3、关于哈希结构
一般的平衡树中查找,需要对目标和经过的关键码进行比较,而对于哈希结构而言,不经过比较,而是通过某种函数使元素储存的位置和关键码建立映射关系,从而在查询中能更快的找到元素。
常见的哈希函数有:
1、直接定址法,优点简单、均匀,缺点需要事先知道关键字的大小,适用于比较小而连续的情况。
2、除留余数法,假设地址数为p,取一个比p小的m,类似于hash(key)=key%m(m<=P)进行取余,将这个结果转换成哈希地址
3、平方取中法,适用于不知道关键字分布,而位数又不大的情况。
4、折叠法,适用于不需事先知道关键字分布,而关键字位数比较多的情况。
5、随机数法,适用于关键字长度不等的情况。
哈希冲突:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。
产生哈希冲突的原因就是哈希函数设计的不合理,常见解决方法有闭散列和开散列。
1、闭散列
又称开放定址法,发生哈希冲突时,如果哈希表未被填满,会优先往下一个空的位置放置。
而找到下一个位置的方法有:
线性探测::从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
eg:给一个序列{1,4,3,7,44,9},假设p的10,去留余数的话,
则hash(1)=1%10=1,hash(4)=4%10=4,...但是在hash(44)=44%10=4;
这里就发生了哈希冲突,原本索引为4的位置有了,那么就会从5开始向后查询,直到有一个空位置就插入进去。当哈希表的载荷因子在0.7左右的时候就需要控制扩容了,载荷因子=填入表中的元素个数/散列表的长度。
缺点是不好删除,采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。
//赋予空间状态
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct Data
{
pair<K, V> _kv;
State _state=EMPTY;
};
template<class K, class V>
class HashTable
{
private:
vector<Data> _tables;
size_t _n;//储存的个数
public:
bool Insert(const pair<K,V>& kv)
{
if(Find(kv.first)
{
return false;
}
if(_table.size()==0||_n*10/_tables.size()>=7)//这样就不用强转成double了
{
size_t newsize=_table.size()==0?10:_tables.size()*2;
HashTable<K,V> newtable;
for(auto& data:_tables)
{
newtable.Insert(data._kv);
}
_tables.swap(newtable._tables);
}
size_t hash=kv.first%_tables.size();
size_t i=1;
size_t index=hash;
while(_tables[index]._state==EXIST)
{
index=i+hash;
index%=_tables.size();
i++;
}
_table[hash]._kv=kv;
_table[hash]._state=EXIST;
_n++;
return true;
}
HashTable<K,V>* Find(const K& key)
{
if(_table.size()==0)
{
return false;
}
size_t hash=kv.first%_table.size();
size_t i=1;
size_t index=hash;
while(_table[index]._state!=EMPTY)
{
if(_table[index]._kv.first==key
&&_table[index]._state==EXIST)
{
return &_table[index];
}
index=hash+i;
index%=_tables.size();
++i;
if(hash==index)
{
break;//排除全是delete的状态。
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashTable<K,V> *ret=Find(key);
if(key)
{
ret->_state=DELETE;
--_n;
return true;
}
else
{
reutrn false;
}
}
}
线性探测优点:实现非常简单, 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易导致不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
2、开散列
又称开链法,首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中,而这里的扩容则是在某个桶的链表长度等于这个哈希表的长度后,就需要扩容了。
template<class K, class V>
struct HashNode
{
HashNode<K,V>* _next;
pair<K,V> _kv;
HashNode(const pair<K,V>& kv)
:_next(nullptr),
_kv(kv)
{};
}
template<class K,class V>
class Hashtale
{
typedef HashNode<K,V> Node;
private:
vector<Node*> _tables;
size_t _n=0;//储存的有效个数
public:
~HashTable()
{
for(auto cur: _tables)
{
while(cur)
{
Node* next=cur->next;
delete cur;
cur=next;
}
cur=nullptr;
}
}
Node* Find(const K& key)
{
if(_tables.size()==0)
{
return nullptr;
}
size_t hash=key%_tables.size();
Node* cur= _tables[hash];
while(cur)
{
if(cur->_kv.first==key)
{
return cur;
}
cur=cur->next;
}
return nullptr;
}
bool Insert(const pair<K,V>& kv)
{
if(Find(kv.first))
{
return false;
}
if(_n==_tables.size())
{
size_t newsize=_tables.size()==0?10:_tables.size*2;
vector<Node*> newtables(newsize,nullptr);
for(auto& cur :_tables)
{
while(cur)
{
Node* next=cur->next;
size_t hash=cur->_kv.first%newtables.size();
cur->next=newtable[hash];
newtables[hash]=cur;
}
}
_tables.swap(newtables);
}
size_t hash =kv.first%_tables.size();
//头插
Node* newnode=new Node(kv);
newnode->_next=_tables[hash];
_tables[hash]=newnode;
++_n;
return true;
}
bool Erase(const K& key)
{
size_t hash=key%_tables.size();
Node* prev=nullptr;
Node* cur=_tables[hash];
while(cur)
{
if(cur->kv.first==key)
{
if(prev==nullptr)
{
_tables[hash]=cur->next;
}
else
{
prev->next=cur->next;
}
delete cur;
return true;
}
else
{
prev=cur;
cur=cur->next;
}
}
return false;
}
}
#先记录在闭散列和开散列的部分,后面如果有需要补充的笔记继续下面进行记录。