Set简介
阮一峰:“Set类似于数组,但是成员的值都是唯一的,没有重复的值。”
Set如何实现元素不重复?
Array能不能实现元素不重复?
Array实现Set特性
Array只要稍微封装一下就能实现Set特性。
但是用数组实现的缺点是低效。数组基于值查找的时间复杂度是O(n),低效的原因来源于其底层数据结构,Array是按索引存储的。
Array的存储方式
数组的底层数据结构是一个线性表,即在内存上体现为一段连续的内存。
数组按索引查找的方式:起始地址 + 索引。按索引查找的时间复杂度是O(1)
数组的优势:根据索引号来查找数组元素,速度块,效率高。
数组的劣势:按值查找时,只能根据索引遍历出每一个数组元素,然后和要查找的数据元素进行对比,再优化一点就需要使用算法了,比如二分查找,排序
后二分查找。
Set的底层实现:哈希表
为了优化按值查找,Set 使用了哈希表。
哈希表在内存上,也是开辟一段连续内存用来存储数据,但是有别于数组,哈希表不会将元素按照添加顺序依次存入连续内存中,元素在内存中存储位置是由哈希算法计算得出的。
哈希算法:将【元素值】和【哈希表长度】经过一种算法,计算出元素在哈希表中的存储位置。最简单的哈希算法:哈希值 % 哈希表长度。
哈希表不使用索引,也不会产出索引,而是直接根据元素计算出其内存地址。所以没有数组中的 get(index),只有has(value)
哈希表的缺陷:哈希冲突
哈希冲突举例:abc bca cab
哈希表处理哈希冲突的方式是拉链法,即哈希表每一个存储位置可以看出一个桶,当需要在一个存储位置上存储多个值的话,就相当于像桶中放数据,而在数据结构上,这个桶就是单向链表。所以哈希表数据结构可以看出是(数组+单向链表),每个数组元素位置上可以拉一条单向链表存储多个值,一次来解决哈希冲突的元素的存放问题。
当哈希表某一个位置发生的哈希冲突次数很多时,会导致该位置拉出来的单向链表越来越长。
当我们在哈希表上查找的值,刚好在某个位置的单向链表上的话,那么此时该值的查询效率就会骤降,因为单向链表的长处在于元素的插入删除,而不在于元素的查询,我们可以将单向链表的查询操作的时间复杂度看成O(n),差不多和数组一样。
所以Set没有length,只有size属性。
Set总结
对于 Array 和 Set 底层数据结构的探究,我们再来回顾Set特性:
“Set 成员的值都是唯一的,没有重复的值”
就会发现,这是很浅的理解,Set 比较 Array 的最大不同就是:Set 的值查找效率要比 Array 快的多,原因就是 Set 底层是哈希表,它查找元素,不是真的找,而是根据哈希算法,算出元素在哈希表中位置。
如何记住存储的顺序?
答:维护一条插入链表。
Set.entries()
Set 的 entries 应当理解为插入链,而不是数组的索引
Map简介
Map 和 Object 其实都是存储的键值对(以下称为属性 Properties),但是 Map 比 Object 对象最明显的区别是Object 对象的 key 只能是字符串,就算
设置 Object 对象的 key 为对象,最终也会调用 key 的 toString 方法将其转为字符串,而 Map 的 key 可以是任意类型的数据。
主要区别:实现属性唯一的方法不同。
我们可以思考一下 Object 的属性是如何保证唯一的?我们访问一个 Object 的属性的值时,是如何查询到属性的?
要实现Object的属性唯一,那么最简单就是哈希表!
Object的存储结构(小于128)
但是 Object 并没有首选哈希表来存储属性,而是当属性个数小于 128 时,新增了一个 searchCache 缓存。
每次新增属性都要去这个缓存中找一找是否已存在,若已存在,则将属性值覆盖(覆盖采用顺序查找/二分查找)到 FixedArray 的同名属性中,若不存在,
则将属性哈希值保存进 searchCache,并将属性的键值对加入 FixedArray 中。FixedArray 是 v8 实现的一个类似于数组的类,它表示一段连续的内存。
Object的存储结构(大于128)
当属性个数大于128时,就会使用哈希表(按值存储)代替 searchCache 来保证属性唯一性,而键值对依旧保存在 FixedArray 中。
思考
为什么在小于128个属性的时候要专门搞一个 searchCache 呢,是因为 searchCache 比哈希表快么?
答:快。searchCache 和 hashMap 两种存储方式背区分为快属性/慢属性
快是一方面,我想另一方面在于 searchCache 不会冲突,每次都是直接定位,而哈希表如果冲突了需要不断地找下一个元素,增加了比较次数。当属性比较少,并且有几个属性经常被用到的时候,searchCache 应该会有明显的优势。
Object的其余场景
再考虑key为整数的情况。
当key为整数的时候,这些对象整数属性的属性值会保存带一个elements有序数组中,且保存位置就是整数属性对应的element数组索引号,这样就可以根据索引号是属性唯一,并且可以实现根据索引号快速查询属性值。缺点就是,会造成内存浪费,因为数组的容量必须大于数组元素个数,且随着元素个数不断增加会扩容。
{ 0: ‘a’, 1: ‘b’, 4: ’c’ } ——有序数组存储
{ 0: ‘a , 10000: ‘b’} ——哈希表存储
可以看见Object对象将属性查询和存储搞得很复杂。
而 ES6 的 Map 就简单了,单纯使用哈希表来保证属性唯一。
那么二者在属性查找性能上的对比,考虑储存简单类型的情况:
如果属性个数 <=128,那么 Object 对象是 searchCache 查找,Map 是哈希表查找,考虑到可能存在哈希冲突的情况,Object更快一点。
如果属性个数 >128,那么 Object 是哈希表查找,Map 也是哈希表查找,区别不大。
如果属性是整数属性,且分布均匀,那Object对象会将整数属性当成数组索引查找,而Map还是使用哈希表查找,这个时候应该看Map的哈希算法对于整数属性的哈希值计算快不快了,但是应该还是没有Object对象直接当成数组索引来查快,因为一个要计算得到位置,一个直接当成位置。
(完)