最近在整理数据结构和算法相关的知识,小茄专门在github上开了个repo https://github.com/qieguo2016...,后续内容也会更新到这里,欢迎围观加星星!javascript
js中的对象是基于哈希表结构的,而哈希表的查找时间复杂度为O(1),因此不少人喜欢用对象来作映射,减小遍历循环。java
好比常见的数组去重:git
function arrayUnique(target) { var result = [target[0]]; var temp = {}; temp[target[0]] = true; for (var i = 1, targetLen = target.length; i < targetLen; i++) { if (typeof temp[target[i]] === 'undefined') { result.push(target[i]); temp[target[i]] = true; } } return result; }
这里使用了一个temp对象来保存出现过的元素,在循环中每次只须要判断当前元素是否在temp对象内便可判断出该元素是否已经出现过。github
上面的代码看起来没有问题,但有点经验的同窗可能会说了,假如目标数组是[1,'1'], 这是2个不一样类型元素,因此咱们的指望值应该是原样输出的。但结果倒是[1]。
同理的还有true、null等,也就是说对象中的key在obj[key]时都被自动转成了字符串类型。
因此,若是要区分出不一样的类型的话,temp里面的属性值就不能是一个简单的true了,而是要包含几种数据类型。好比能够是:算法
temp[target[0]]={}; temp[target[0]][(typeof temp[target[i]])] = 1;
在判断的时候除了要判断键是否存在以外,也要判断对应的数据类型计数是否大于1,以此来判断元素是否重复。编程
另外,上面的代码语法也有点问题,不知道你发现了没?
咱们造的这个temp对象并非彻底空白,他是基于Object原型链继承而来的,因此自带了一个__proto__属性,若是你的目标数组里面刚好有"__proto__"这个值,返回的结果就有问题了,具体结果能够本身测试确认。解决方法有两种:数组
1) 想办法去掉这个磨人的__proto__。显然,咱们须要去掉原型链,那么可使用Object.create(null)
的方式来建立一个彻底空白、无原型的空对象。数据结构
2) 使用 感谢@天生爱走神的指正,!temp.hasOwnProperty(target[i])
代替typeof temp[target[i]] === 'undefined'
,这时候表明原型链的__proto__属性就不能干扰到咱们的结果判断了。obj.hasOwnProperty(__proto__)
会获得false,可是假如咱们的目标数组里面包含__proto__
的话,就不能对__proto__
进行去重了。数据结构和算法
上面说了js中使用对象的一点小窍门,核心在于对象的hashmap
结构,那hashmap
是怎样的一个结构呢?且听小茄细细道来。函数
在真实世界中,咱们描述一个事物最经常使用的方式是使用属性
-值
(key
-value
)这样的键值对数据,面向对象编程中对象的定义和js中的对象都是这种模式。好比咱们描述一我的是这样的:
计算机存储空间有两个属性:存储地址
和所存储的值
,机器能够根据给定的存储地址
去读写该地址下的值
。根据这种结构,假如咱们将一块存储空间分红一个一个的格子,而后将这些数据依次塞到每一个格子里,接下来咱们就能够根据格子编号直接访问格子的内容了。这种方式就是数组(也叫线性连续表):数组头保存整个数组储存空间的起始地址,不一样下标表明不一样的储存地址的偏移量,访问不一样下标所对应的地址就能实现数组元素的读写。因此,很天然就会想到将上述的键值对数据的key
映射成数组下标,接着读写数组就变成了读写value
值。将key
的字符串转换成表明下标数值比较简单,能够用特定的码表(如ASCII)进行转换。
上述小明的属性名(key
)通过变换,可能就变成了这样:
因为key的值不一样长度不一,因此转换后的下标的值也相差巨大,另外key
的个数不肯定,也就意味着下标的个数也有很大的范围,甚至无穷多,就有了下面的问题:
若是咱们直接取下标值做为存储数组的下标,虽然简单,可是你会发现这个长度为10010的数组只存了8个值,太浪费!若是咱们想要缩短数组的长度,好比缩为10,最简单的方式可使用取模的方式来肯定下标:69 % 10 = 9,7 % 10 = 7, 198 % 10 = 8……
。这个取模就是哈希算法
、也叫散列算法
。
经过这样的方式获得的下标分别为九、七、八、三、六、二、0
,能够获得保存小明数据的数组:
可是这种方式很容易出现重复,假如咱们增长一个属性phone
,对应的映射值是29,那么29跟69算出来的下标就重复了。也就是哈希算法中的冲突
,也叫碰撞
。好的哈希算法能极大减小冲突
,但因为输入几乎是无穷的,而输出却要求在有限的空间内,因此冲突
是不可避免的。
仍是上面这个例子,29和69发生了冲突,可是咱们能够将他们组成一个链表,链表的头部放在数组中,获得。链表结构中,每一个元素(除单向链表的尾部)都包含了相连元素的内存地址和自己的值,上文中的冲突放入一个链表中,能够获得这样的结构:
最终获得的这个数据结构,也就是咱们常说哈希表
了。这种将数组与链表结合生成哈希表的方法,叫拉链法
。
好比想知道小明的name
属性,即小明.name
。流程是这样的:
1)根据字符映射关系获得映射值为69
2)使用哈希算法获得下标 index=hash(69)=9
3)遍历数组中下标为9的链表,链表的第一个元素的key
恰好就是咱们要找的name
,因此返回value
值小明
哈希表中增删一个元素并不会影响到其余的元素,不像数组同样须要改变后面全部的元素下标。在拉链式的哈希表中,属性的增删就是链表的增删,很是方便。而在纯数组形式的哈希表中,对属性的删并非真的删除,而是作一个空标志而已,因此不影响其余元素。
对于哈希表来讲,最重要的莫过于生成哈希串的哈希算法和处理冲突的策略了。下面进行简单的介绍。
根据上面的例子得知,哈希算法的目的就是将不定的输入转换成特定范围的输出,而且要求输出尽可能均匀分布。因为散列算法是应用在每一次数据定位中的,它的使用频率很是的高,这意味着咱们必需要选择简单的算法。散列算法有不少,这里简单介绍几种。
1,除法散列法
最直观的一种,小茄上文使用的就是这种散列法,公式:
index = key % 16
2,平方散列法
求index
是很是频繁的操做,而乘法的运算要比除法来得省时(对如今的CPU来讲,估计咱们感受不出来),因此咱们考虑把除法换成乘法和一个位移操做。公式:
index = (key * key) >> 28
若是数值分配比较均匀的话这种方法能获得不错的结果,另外key
若是很大,key
* key
会发生溢出。但咱们这个乘法不关心溢出,由于咱们根本不是为了获取相乘结果,而是为了获取index
。
3,斐波那契(Fibonacci)散列法
平方散列法的缺点是显而易见的,因此咱们能不能找出一个理想的乘数,而不是拿value自己看成乘数呢?答案是确定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。
对咱们常见的32位整数而言,公式:
index = (key* 2654435769) >> 28
上文介绍了拉链法来处理冲突,处理冲突的方法其实也有不少,下面简单介绍一下另外几种:
1)拉链法变种。因为链表的查找须要遍历,若是咱们将链表换成树或者哈希表结构,那么就能大幅提升冲突元素的查找效率。不过这样作则会加大哈希表构造的复杂度,也就是构建哈希表时的效率会变差。
2)开放寻址: 当关键字key
的哈希地址p=H(key)
出现冲突时,以p
为基础,产生另外一个哈希地址p1
,若是p1
仍然冲突,再以p
为基础,产生另外一个哈希地址p2
,…,直到找出一个不冲突的哈希地址pi
,将相应元素存入其中。这种方法有一个通用的函数形式:
Hi=(H(key)+di)% m i=1,2,…,n
根据di
的不一样,又能够分为线性的、平方的、随机数之类的。。。这里再也不展开。
开发寻址的好处是存储空间更加紧凑,利用率高。可是这种方式让冲突元素之间产生了联系,在删除元素的时候,不能直接删除,不然就打乱了冲突元素的寻址链。
3)再哈希法
这种方法会预先定义一组哈希算法,发生冲突的时候,调用下一个哈希算法计算一直计算到不发生冲突的时候则插入元素,这种方法跟开放寻址的方法优缺点相似。函数表达式:
index=Hi(key) , i=1,2,…,n
哈希算法经常使用的场景除了上文所说的快速查找以外,还有一个很是重要的应用就是加密算法,这个加密更准确的说法是加签,也便是“消息摘要”。
根据上文的基础介绍可知,哈希算法就是将任意数据转换成必定范围数据的算法,这种算法的反作用就是会产生冲突。可是呢,在快速查找中出现的反作用,倒是加密功能中的核心,由于有冲突,因此从结果就没法逆推出输入值,这样就实现了数据的单向加密。而输入数据的变化却又会影响到哈希串的值,因此咱们能够用哈希串来进行数据的校验。
关于js对象与哈希相关的东西就说到这里了,用文字总结一下以后发现不少知识点都明确了不少,尤为是要用最平白的语言组织出来,就必须有本身的理解才行。任何一个细节均可以看出不少东西,谨以此文与君共勉!