一、集合
在前端中其实集合我们听的是比较少的;那么什么是集合呢?集合的作用又是什么呢?
Set()
- 在ES6中添加了Map(字典)和Set(集合)两种新的数据结构。ES6的Set数据结构使用的是哈希表来存储元素。
- 在Set中,每个元素都是唯一的,而且没有顺序。
- 没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只会存在一份。 Set()常见的方法:
add(value):向集合添加一个新的项;remove(value):从集合中移除一个值;has(value):如果值在集合中,返回true,否则返回false;clear():移除集合中的所有项;size():返回集合所包含元素的数量,与数组的length属性相似;values():返回一个包含集合中所有值的数组;
集合的常用操作:去重、判断某元素是否在集合中、求交集…
// 查看集合
const arr = [1, 1, 1, 2, 2, 2, 3];
const set = new Set(arr)//Set(2) {1, 2, 3}
//查看集合的长度
console.log(set.size);//3
// 去重
const arr1 = [1, 1, 2, 2, 2];
const arr2 = [...new Set(arr1)];
console.log(arr2); //[ 1, 2 ]
// 判断元素是否在集合中
const set = new Set(arr);
console.log(set.has(3)); //true
//求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));
console.log(set3); //{ 2, 3 }
二、字典
Map()字典的特点:
- 字典存储的是键值对,主要特点是一 一对应;
- 比如保存一个人的信息:数组形式:[18,‘Tom’,1.75],可通过下标值取出信息;字典形式:{"age":18,"name":"Tom","height":175},可以通过key取出value。
- 此外,在字典中key是不能重复且无序的,而Value可以重复。
Map()字典与js中普通对象的区别:
从Map()字典的特点我们可以知道Map()字典与普通的js的对象object是很相似的。那它们两者有什么区别呢?
区别: 在 js 中,当你使用对象 object 时, 键 key 只能有 string 和 symbol 。然而 Map() 的 key 支持的就比较多了,可以支持 string, symbol, number, function, object, 和 primitives。
const map = new Map();
const myFunction = () => console.log("I am a useful function.");
const myNumber = 666;
const myObject = {
name: "plainObjectValue",
otherKey: "otherValue",
};
map.set(myFunction, "function as a key");
map.set(myNumber, "number as a key");
map.set(myObject, "object as a key");
console.log(map.get(myFunction)); // function as a key
console.log(map.get(myNumber)); // number as a key
console.log(map.get(myObject)); // object as a key
字典类常见的操作:
- set(key,value):向字典中添加新元素。
- remove(key):通过使用键值来从字典中移除键值对应的数据值。
- has(key):如果某个键值存在于这个字典中,则返回
true,反之则返回false。 - get(key):通过键值查找特定的数值并返回。
- clear():将这个字典中的所有元素全部删除。
- size():返回字典所包含元素的数量。与数组的
length属性类似。 - keys():将字典所包含的所有键名以数组形式返回。
- values():将字典所包含的所有数值以数组形式返回。
三、哈希表
3.1 认识哈希表
什么是哈希表?
哈希表(Hash Table):也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。哈希表通过键key和映射函数Hash(key)计算出对应的值value,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数(散列函数),存放记录的数组叫做哈希表(散列表)。
3.2 哈希表的概念
哈希表的特点:
哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势:
- 哈希表可以提供非常快速的插入-删除-查找操作;
- 无论多少数据,插入和删除值都只需要非常短的时间,即O(1)的时间级。实际上,只需要几个机器指令即可完成;
- 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。但是相对于树来说编码要简单得多。
哈希表同样存在不足之处:
- 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。
- 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
哈希表的概念:
哈希表是一个典型的以空间换时间的数据结构,其存储方式为索引存储。在进行数据存储的时候,首先会根据某种规则,对数据进行分类,然后将数据放入至相应类别的存储结构中去,在哈希表中,每一个类别都会有一个自己的存储空间,专门存放类别符合索引的数据们,在查询的时候,同样是按照规则先得到类别,然后直接查相应类别对应的存储结构中的数据,进而缩小查询范围,达到节省时间的目的。
哈希表的一般构造:
哈希表通常是什么样的呢?我们举两个例子:
- 1、公司想要存储1000个人的信息,每一个工号对应一个员工的信息。若使用数组,增删数据时比较麻烦;使用链表,获取数据时比较麻烦。有没有一种数据结构,能把某一员工的姓名转换为它对应的工号,再根据工号查找该员工的完整信息呢?没错此时就可以使用哈希表的哈希函数来实现。
- 2、存储联系人和对应的电话号码:当要查找张三(比如)的号码时,若使用数组:由于不知道存储张三数据对象的下标值,所以查找起来十分麻烦,使用链表时也同样麻烦。而使用哈希表就能通过哈希函数把张三这个名称转换为它对应的下标值,再通过下标值查找效率就非常高了。
哈希表最后还是基于数据来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值,建立字符串和下标值的对应关系。
3.3 哈希化
为了把字符串转化为对应的下标值,需要有一套编码系统,为了方便理解我们创建这样一套编码系统:比如a为1,b为2,c为3,以此类推z为26,空格为27(暂时不考虑大写情况)。
有了编码系统后,将字母转化为数字也有很多种方式:
方式一: 数字相加。例如cats转化为数字:3+1+20+19=43,那么就把43作为cats单词的下标值储存在数组中;
但是这种方式会存在这样的问题:很多的单词按照该方式转化为数字后都是43,比如was。而在数组中一个下标值只能储存一个数据,所以该方式不合理。
方式二: 幂的连乘。我们平时使用的大于10的数字,就是用幂的连乘来表示它的唯一性的。比如: 6543=6 * 103 + 5 * 102 + 4 * 10 + 3;这样单词也可以用该种方式来表示:cats = 3 * 273 + 1 * 272 + 20 * 27 + 17 =60337;
虽然该方式可以保证字符的唯一性,但是如果是较长的字符(如aaaaaaaaaa)所表示的数字就非常大,此时要求很大容量的数组,然而其中却有许多下标值指向的是无效的数据(比如不存在zxcvvv这样的单词),造成了数组空间的浪费。
两种方案总结:
第一种方案(让数字相加求和)产生的数组下标太少;
第二种方案(与27的幂相乘求和)产生的数组下标又太多;
现在需要一种压缩方法,把幂的连乘方案系统中得到的巨大整数范围压缩到可接受的数组范围中。可以通过取余操作来实现。虽然取余操作得到的结构也有可能重复,但是可以通过其他方式解决。
解决冲突常见的两种方案:
- 方案一:链地址法(拉链法);
如下图所示,我们将每一个数字都对10进行取余操作,则余数的范围0~9作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。
这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。
总结: 链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。
- 方案二:开放地址法;
开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。
根据探测空白单元格位置方式的不同,可分为三种方法:
- 线性探测
- 二次探测
- 再哈希法
线性探测
当插入13时:
- 经过哈希化(对10取余)之后得到的下标值index=3,但是该位置已经放置了数据33。而线性探测就是从index位置+1开始向后一个一个来查找合适的位置来放置13,所谓合适的位置指的是空的位置,如上图中index=4的位置就是合适的位置。
当查询13时:
- 首先13经过哈希化得到index=3,如果index=3的位置存放的数据与需要查询的数据13相同,就直接返回;
- 不相同时,则线性查找,从index+1位置开始一个一个位置地查找数据13;
- 查询过程中不会遍历整个哈希表,只要查询到空位置,就停止,因为插入13时不会跳过空位置去插入其他位置。
当删除13时:
- 删除操作和上述两种情况类似,但需要注意的是,删除一个数据项时,不能将该位置下标的内容设置为null,否则会影响到之后其他的查询操作,因为一遇到为null的位置就会停止查找。
- 通常删除一个位置的数据项时,我们可以将它进行特殊处理(比如设置为-1),这样在查找时遇到-1就知道要继续查找。
线性探测存在的问题:
- 线性探测存在一个比较严重的问题,就是聚集;
- 如哈希表中还没插入任何元素时,插入23、24、25、26、27,这就意味着下标值为3、4、5、6、7的位置都放置了数据,这种一连串填充单元就称为聚集;
- 聚集会影响哈希表的性能,无论是插入/查询/删除都会影响;
- 比如插入13时就会发现,连续的单元3~7都不允许插入数据,并且在插入的过程中需要经历多次这种情况。二次探测法可以解决该问题。
二次探测
上文所说的线性探测存在的问题:
-
如果之前的数据是连续插入的,那么新插入的一个数据可能需要探测很长的距离;
二次探测是在线性探测的基础上进行了优化:
-
线性探测:我们可以看成是步长为1的探测,比如从下表值x开始,那么线性探测就是按照下标值:x+1、x+2、x+3等依次探测;
-
二次探测:对步长进行了优化,比如从下标值x开始探测:x+12、x+22、x+33 。这样一次性探测比较长的距离,避免了数据聚集带来的影响。
二次探测存在的问题:
- 当插入数据分布性较大的一组数据时,比如:13-163-63-3-213,这种情况会造成步长不一的一种聚集(虽然这种情况出现的概率较线性探测的聚集要小),同样会影响性能。
再哈希化
在开放地址法中寻找空白单元格的最好的解决方式为再哈希化:
- 二次探测的步长是固定的:1,4,9,16依次类推;
- 现在需要一种方法:产生一种依赖关键字(数据)的探测序列,而不是每个关键字探测步长都一样;
- 这样,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列;
- 再哈希法的做法为:把关键字用另一个哈希函数,再做一次哈希化,用这次哈希化的结果作为该关键字的步长;
第二次哈希化需要满足以下两点:
- 和第一个哈希函数不同,不然哈希化后的结果仍是原来位置;
- 不能输出为0,否则每次探测都是原地踏步的死循环;
优秀的哈希函数:
- stepSize = constant - (key % constant) ;
- 其中constant是质数,且小于数组的容量;
- 例如:stepSize = 5 - (key % 5),满足需求,并且结果不可能为0;
哈希化的效率
哈希表中执行插入和搜索操作效率是非常高的。
- 如果没有发生冲突,那么效率就会更高;
- 如果发生冲突,存取时间就依赖后来的探测长度;
- 平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度会越来越长。
理解概念装填因子:
- 装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值;
- 装填因子 = 总数据项 / 哈希表长度;
- 开放地址法的装填因子最大为1,因为只有空白的单元才能放入元素;
- 链地址法的装填因子可以大于1,因为只要愿意,拉链法可以无限延伸下去;
不同探测方式性能的比较
- 线性探测:
随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。
- 二次探测和再哈希化的性能:
二次探测和再哈希法性能相当,它们的性能比线性探测略好。随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。
- 链地址法的性能:
随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如Java中的HashMap中使用的就是链地址法。
优秀的哈希函数
哈希表的优势在于它的速度,所以哈希函数不能采用消耗性能较高的复杂算法。提高速度的一个方法是在哈希函数中尽量减少乘法和除法。
性能高的哈希函数应具备以下两个优点:
- 快速的计算;
- 均匀的分布;
快速计算
霍纳法则:在中国霍纳法则也叫做秦久韶算法,具体算法为:
求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求n次多项式f(x)的值就转化为求n个一次多项式的值。
变换之前:
- 乘法次数:n(n+1)/2次;
- 加法次数:n次;
变换之后:
- 乘法次数:n次;
- 加法次数:n次;
如果使用大O表示时间复杂度的话,直接从变换前的O(N2) 降到了O(N) 。
均匀分布
为了保证数据在哈希表中均匀分布,当我们需要使用常量的地方,尽量使用质数;比如:哈希表的长度、N次幂的底数等。
Java中的HashMap采用的是链地址法,哈希化采用的是公式为:index = HashCode(key)&(Length-1)
即将数据化为二进制进行与运算,而不是取余运算。这样计算机直接运算二进制数据,效率更高。但是JavaScript在进行叫大数据的与运算时会出现问题,所以以下使用JavaScript实现哈希化时还是采用取余运算。
哈希表的常见操作为:
- put(key,value):插入或修改操作;
- get(key):获取哈希表中特定位置的元素;
- remove(key):删除哈希表中特定位置的元素;
- isEmpty():如果哈希表中不包含任何元素,返回trun,如果哈希表长度大于0则返回false;
- size():返回哈希表包含的元素个数;
- resize(value):对哈希表进行扩容操作;
哈希表的一些概念:
- 哈希化: 将大数字转化成数组范围内下标的过程,称之为哈希化;
- 哈希函数: 我们通常会将单词转化成大数字,把大数字进行哈希化的代码实现放在一个函数中,该函数就称为哈希函数;
- 哈希表: 对最终数据插入的数组进行整个结构的封装,得到的就是哈希表。