0
点赞
收藏
分享

微信扫一扫

【关联容器】

老北京的热干面 2022-04-14 阅读 21
数据结构

文章目录


map和set的各种实现中的名称对应如表,低层实现几乎相同。

stlboostjava
map/multimap、set/mutisetmap/multimap、set/mutisettreemap/treemultimap、treeset/treemultiset
unordered_map/unordered_multimap、unordered_set/unordered_multisethash_map/hash_multimap、hash_set/hash_multisethashmap/hashmultimap、hashset/hashmultiset

map/multimap、set/mutiset【红黑树】

底层是红黑树实现,红黑树4大规则:

规则3和规则4保证了任意节点到其每个叶子节点路径最长不会超过最短路径的2倍,因为最长路径和最短路径的黑色节点数相同,极端情况下最短路径就是全有黑色节点组成,而红色节点不能连续出现,因此最多就交替出现,则极端情况下最长路径红色节点 = 黑色节点,所以最长路径不会超过最短路径两倍。
红黑树的操作包括旋转、插入、删除等,网上很多资料可以查看,此处不再赘述。。


unordered_map/unordered_multimap、unordered_set/unordered_multiset【哈希表】

扩容策略:c++/stl的实现和java的实现不太一样

c++/stljava
桶元素超过8个或者装载因子超过0.75则扩容桶元素超过8个则将桶指向的列表元素由链表转变为红黑树,当装载因子超过0.75则扩容

装载因子定0.75、桶大小定8原因?
首先定装载因子和桶大小是为了扩容和将链表转为红黑树。
扩容和链表转为红黑树是时间和空间成本的tradeoff,因为hash表的特性,随着数据增多,产生碰撞概率加大,则开链的长度越长。那么在每个桶的链条中找数据耗费时间就多了,因此扩容重哈希一下。

哈 希 表 可 存 放 数 据 大 小 = 哈 希 表 长 度 ∗ 装 载 因 子 哈希表可存放数据大小 = 哈希表长度 * 装载因子 =
从公式可以看出,要想要哈希表可以存放更多的数据,可以从两个方面进行改进:
1. 牺牲空间换时间:增加哈希表长度(扩容),扩容后碰撞概率变小,桶的链大小变短,找到元素速度变快,但是牺牲了扩容的空间大小。
2. 牺牲时间换空间:增加装载因子大小(装载因子大小介于0~1之间),最大可以设为1,但是装载因子越大,碰撞概率越高,桶的链大小越长,找到元素速度变慢,但是不需要扩容哈希表的大小。
因此为了权衡时间和空间效率,需要设定一个合适的装载因子,通过计算装载因子在0.6~0.8之间,每个桶的链长度不会过长,桶的链长度为8时的概率是亿分之六了,所以业界就协商一个0.6-0.8之间的一个数,最好肯定是定0.8呀,毕竟装载因子越大,不许扩容条件下可以塞更多元素,但是最后还是源码编写大神们将装载因子定为了0.75(挺不错的数,为什么呢,因此哈希表的长度(初识长度为16按2倍扩容)一般是2的幂次方,2的幂次方乘0.75是个整数呀,乘于0.8可不一定是整数)。
为什么哈希表的容量要取2的幂呢?
采用二进制位操作 & 相对于 % 能够提高运算效率,取模运算中如果除数是2的幂次方则等价于其与除数减一的&操作,即:
 hash  %  length  = =  hash  & (  length  − 1 ) \text { hash } \% \text { length }==\text { hash } \&(\text { length }-1)  hash % length == hash &( length 1)


数学推导(对数学不感兴趣的可以略过。。。)
假设哈希函数哈希数据位置是随机分布的,则哈希表每个桶出现数据的频率服从泊松分布。

泊松分布公式:
P ( N ( t ) = n ) = ( λ t ) n e − λ t n ! P(N(t)=n)=\frac{(\lambda t)^{n} e^{-\lambda t}}{n !} P(N(t)=n)=n!(λt)neλt
P 表示概率,N表示某种函数关系,t 表示时间,n 表示数量。等号的右边,λ 表示事件的频率。

由于哈希函数随机分布,假设数据大小为x,装载因子为k,则哈希表最小长度为 x k \frac{x}{k} kx ,哈希表最大长度为 2 x k \frac{2x}{k} k2x,哈希表平均长度为 x k + 2 x k 2 = 3 x 2 k \frac{\frac{x}{k} + \frac{2x}{k}}{2} = \frac{3x}{2k} 2kx+k2x=2k3x,桶有数据的概率 λ 为 x / 3 x 2 k = 2 k 3 x/\frac{3x}{2k} = \frac{2k}{3} x/2k3x=32k,当装载因此为0.75时,桶有数据的概率为 λ 为0.5,代入公式可得:
P ( X = k ) = e − 0.5 0. 5 k k ! , k = 0 , 1 , 2 … P(X=k)=\frac{e^{-0.5} 0.5^{k}}{k !}, k=0,1,2 \ldots P(X=k)=k!e0.50.5k,k=0,1,2
k各个取值如下表,可见每个桶大小为8的时候,概率就很小了。因此决定桶大小为8就转为红黑树或扩容。

桶的大小概率
00.60653066
10.30326533
20.07581633
30.01263606
40.00157952
50.00015795
60.00001316
70.00000094
80.00000006

总结来说:
就是时间和空间成本的权衡,决定了装载因子是0.75,然后通过泊松分布计算出每个桶的链长度为8的概率很低亿分之六,因此要是桶的链大小为8时,说明你的哈希函数可能设置不合理了,那么c++的做法就是扩容重哈希,java的做法是不扩容,只是将链表结构改成红黑树的结构,java这样的好处在于方便我们调试代码,在调试代码的时候,发现桶中的元素是红黑树节点而不是链表节点,说明你的哈希函数设置不合理,哈希得不够随机,此时就要考虑重新设置哈希函数了。


再补充一点知识:
stl的容器都是线程不安全的,并发情况下,vector和deque可能造成迭代器失效、list会造成数据覆盖、关联容器会造成迭代器失效、数据覆盖等并发问题。
以哈希表在多线程环境下为例,存在的并发问题有:

并发问题1:死循环问题
原因:扩容时采用头插法,使得桶中链表元素的顺序与扩容前桶中链表元素顺序相反,多线程下会造成死循环问题
解决:头插法改尾插法,因为尾插法在扩容时元素在链表中的顺序不变,不会出现链表成环问题。
问题分析
现在有两个线程执行rehash的过程,线程2早于线程1执行完rehash操作,此时线程1再执行rehash时,会出现链表成环的情况,则当执行比如说get(11)的操作的时候,就会陷入死循环!

 do {
  Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了,执行其他操作
  int i = indexFor(e.hash, newCapacity);
  e.next = newTable[i];
  newTable[i] = e;
  e = next;
 } while (e != null);

在这里插入图片描述

在这里插入图片描述
并发问题2-3:数据丢失问题
有时间再分析。。

解决
并发环境下java有concurrenthashmap,以提供并发环境下安全的使用哈希表,但是C++没有支持的实现,最简单粗暴的方法就是使用锁,但是效率太低,如何采用无锁的解决方案呢?当然是参考专家大佬们的论文学习啦,java的concurrenthashmap就是对大佬们论文思想的代码实现。

举报

相关推荐

0 条评论