系列文章:数据结构
目录
map和set的各种实现中的名称对应如表,低层实现几乎相同。
stl | boost | java |
---|---|---|
map/multimap、set/mutiset | map/multimap、set/mutiset | treemap/treemultimap、treeset/treemultiset |
unordered_map/unordered_multimap、unordered_set/unordered_multiset | hash_map/hash_multimap、hash_set/hash_multiset | hashmap/hashmultimap、hashset/hashmultiset |
map/multimap、set/mutiset【红黑树】
底层是红黑树实现,红黑树4大规则:
1.节点是红或黑
2.根结点和叶节点为黑
3.红节点有两个黑的子节点
4.从根结点开始的每条路径的黑色节点数相同
规则3和规则4保证了任意节点到其每个叶子节点路径最长不会超过最短路径的2倍,因为最长路径和最短路径的黑色节点数相同,极端情况下最短路径就是全有黑色节点组成,而红色节点不能连续出现,因此最多就交替出现,则极端情况下最长路径红色节点 = 黑色节点,所以最长路径不会超过最短路径两倍。
红黑树的操作包括旋转、插入、删除等,网上很多资料可以查看,此处不再赘述。。
unordered_map/unordered_multimap、unordered_set/unordered_multiset【哈希表】
扩容策略:c++/stl的实现和java的实现不太一样
c++/stl | java |
---|---|
桶元素超过8个或者装载因子超过0.75则扩容 | 桶元素超过8个则将桶指向的列表元素由链表转变为红黑树,当装载因子超过0.75则扩容 |
注意:c++实现的unordered_map没有转红黑树这个操作,要是桶大小超过8个元素,就直接扩容重哈希。
装载因子定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