本系列全部文章:
第一篇文章:学习数据结构与算法之栈与队列
第二篇文章:学习数据结构与算法之链表
第三篇文章:学习数据结构与算法之集合
第四篇文章:学习数据结构与算法之字典和散列表
第五篇文章:学习数据结构与算法之二叉搜索树javascript
不是新华字典哦,这里的字典也称做_映射_,是一种数据结构,跟set集合很类似的一种数据结构,均可以用来存储无序不重复的数据。不一样的地方是集合以[值,值]的形式存储,而字典则是以[键,值]或者叫做{key: value}的形式。java
先实现一个构造函数:node
function Dictionary () { var items = {} }
字典须要实现如下方法:git
由于后面的方法都要用到has,因此先实现这个方法github
this.has = function (key) { // 书上用的是in操做符来判断,可是in对于继承来的属性也会返回true,因此我换成了这个 return items.hasOwnProperty(key) }
没啥好说的,简单的赋值算法
this.set = function (key, value) { items[key] = value }
首先判断key是否属于该字典,而后再删除segmentfault
this.remove = function (key) { if (this.has(key)) { delete items[key] return true } return false }
返回由数值组成的数组数组
this.values = function () { var values = [] for (var k in items) { if (items.has(k)) { values.push(items[k]) } } return values }
其余的比较简单,和集合的方法实现相似,我就直接贴源代码了数据结构
this.keys = function () { return Object.keys(items) } this.size = function () { return Object.keys(items).length } this.clear = function () { items = {} } this.getItems = function () { return items } this.get = function (key) { return this.has(key) ? items[key] : undefined }
源代码在此:app
关于散列表的定义,这里摘抄一下维基百科的解释:
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它经过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表。
能够理解为,数据中的键经过一个散列函数的计算后返回一个数据存放的地址,所以能够直接经过地址来访问它的值,这样的查找就很是快。
先看这个构造函数,注意:存储数据用的是数组,由于寻址访问元素最方便的仍是数组了。
function HashTable () { var table = [] }
须要实现的方法:
把键名的每一个字母转成ASCII码再相加起来,最后和一个任意的数求余,获得数据存储的地址。
var loseloseHashCode = function (key) { var hash = 0 for (var i = 0; i < key.length; i++) { hash += key.charCodeAt(i) } return hash % 37 }
由于这里的方法比较简单,我就直接全贴上来了
this.put = function (key, value) { var position = loseloseHashCode(key) console.log(position + ' - ' + key) table[position] = value } this.remove = function (key) { table[loseloseHashCode(key)] = undefined } this.get = function (key) { return table[loseloseHashCode(key)] } // 用来输出整个散列表,查看结果用的 this.print = function () { for (var i = 0; i < table.length; i++) { if (table[i] !== undefined) { console.log(i + ': ' + table[i]) } } }
稍等,还没完呢,如今看似很完美,咱们调用一下刚才写的:
var hash = new HashTable() hash.put('Gandalf', 'gandalf@email.com') hash.put('John', 'johnsnow@email.com') hash.put('Tyrion', 'tyrion@email.com') // 输出结果 // 19 - Gandalf // 29 - John // 16 - Tyrion
好像没什么不对,可是仔细想一想:若是在某些状况下,散列函数根据传入的键计算获得的地址是同样的会怎么样呢?
看看下面这个例子:
var hash = new HashTable() hash.put('Gandalf', 'gandalf@email.com') hash.put('John', 'john@email.com') hash.put('Tyrion', 'tyrion@email.com') hash.put('Aaron', 'aaron@email.com') hash.put('Donnie', 'donnie@email.com') hash.put('Ana', 'ana@email.com') hash.put('Jonathan', 'jonathan@email.com') hash.put('Jamie', 'jamie@email.com') hash.put('Sue', 'sue@email.com') hash.put('Mindy', 'mindy@email.com') hash.put('Paul', 'paul@email.com') hash.put('Nathan', 'nathan@email.com') // 输出结果 // 19 - Gandalf // 29 - John // 16 - Tyrion // 16 - Aaron // 13 - Donnie // 13 - Ana // 5 - Jonathan // 5 - Jamie // 5 - Sue // 32 - Mindy // 32 - Paul // 10 - Nathan
这种状况就比较复杂了:Tyrion和Aaron的值都是16,Donnie和Ana都是13,还有其余不少重复的值,这时散列表表中是什么状况呢
hash.print() // 输出结果 // 5: sue@email.com // 10: nathan@email.com // 13: ana@email.com // 16: aaron@email.com // 19: gandalf@email.com // 29: john@email.com // 32: paul@email.com
很明显,后面的元素会覆盖前面的元素,但这样是不行的,要想办法解决冲突
目前主流的方法主要是两种:分离连接法和线性探查法,这里就简单介绍一下分离连接法。
思路很简单:你不是重复了吗,那我就在同一个位置里面放一个链表,重复的数据全都放在链表里面,你要找数据就在链表里面找。
若是不理解,能够参见下图:
(图片来源谷歌搜索,侵删)
根据这个思路,咱们从新实现一下三个方法,不过在这以前,咱们须要一个对象来保存键值对
var ValuePair = function (key, value) { this.key = key this.value = value // 重写toString主要是方便输出查看 this.toString = function () { return '[' + this.key + ' - ' + this.value + ']' } }
接下来就是重写方法了
this.put = function (key, value) { var position = loseloseHashCode(key) // 若是发现该位置尚未元素,就先放一个链表,再用append加进去 if (table[position] === undefined) { // 由于使用node执行文件,这里LinkedList是我在顶部用require引入的LinkedList.js table[position] = new LinkedList() } table[position].append(new ValuePair(key, value)) } this.get = function (key) { var position = loseloseHashCode(key) if (table[position] !== undefined) { var current = table[position].getHead() // 遍历链表查找值 while (current.next) { if (current.element.key === key) { return current.element.value } current = current.next } // 检查元素若是是最后一个的状况 if (current.element.key === key) { return current.element.value } } return undefined } this.remove = function (key) { var position = loseloseHashCode(key) if (table[position] !== undefined) { var current = table[position].getHead() // 遍历查找值 while (current.next) { if (current.element.key === key) { // 使用链表的remove方法 table[position].remove(current.element) // 当链表为空了,就把散列表该位置设为undefined if (table[position].isEmpty()) { table[position] = undefined } return true } current = current.next } if (current.element.key === key) { table[position].remove(current.element) if (table[position].isEmpty()) { table[position] = undefined } return true } } return false }
以上就是用分离连接法重写的哈希表。
第二种办法思路更粗暴:你不是占了这个位置嘛,那我就占下一个,下个位置还被占了?那就占再下一个~
具体的实现我就不贴出来了,读者能够自行思考并实现,而后对照代码看看。
这里先把源代码放出来了
以上是哈希表的两个冲突解决办法,但实际上应用哈希表的时候能避免冲突就尽可能避免冲突,一开始的散列函数不是一个好的函数,由于冲突太多了,这里就贴书上的一个不错的散列函数(社区),原理大体是:相加所得的hash数要够大,且尽可能为质数,用hash与另外一个大于散列表大小的质数作求余,这样获得的地址也能尽可能不重复。
var djb2HashCode = function (key) { var hash = 5381 for (var i = 0; i < key.length; i++) { hash = hash * 33 + key.charCodeAt(i) } return hash % 1013 }
实现了字典和哈希表感受没有想象中那么困难,固然这仍是开始。
不过,这几天本身不断地研究数据结构,也让本身对于JS的理解进一步加深了。继续努力吧~