集合、字典和散列表能够存储不重复的值。在集合中,咱们感兴趣的是每一个值自己,并把它看成主要元素。在字典中,咱们用[键,值]的形式来存储数据。在散列表中也是同样(也是以[键,值]对的形式来存储数据)。可是两种数据结构的实现方式略有不一样,本文将详细介绍字典和散列表这两种数据结构html
集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很类似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称做映射算法
【建立字典】编程
与Set类类似,ECMAScript 6一样包含了一个Map类的实现,即咱们所说的字典数组
下面将要实现的类就是以ECMAScript 6中Map类的实现为基础的。它和Set类很类似(但不一样于存储[值,值]对的形式,咱们将要存储的是[键,值]对)浏览器
这是咱们的Dictionary类的骨架:数据结构
function Dictionary() { var items = {}; }
与Set类相似,咱们将在一个Object的实例而不是数组中存储元素。 而后,咱们须要声明一些映射/字典所能使用的方法app
set(key,value):向字典中添加新元素。 remove(key):经过使用键值来从字典中移除键值对应的数据值。 has(key):若是某个键值存在于这个字典中,则返回true,反之则返回false。 get(key):经过键值查找特定的数值并返回。 clear():将这个字典中的全部元素所有删除。 size():返回字典所包含元素的数量。与数组的length属性相似。 keys():将字典所包含的全部键名以数组形式返回。 values():将字典所包含的全部数值以数组形式返回。
【has】编程语言
首先来实现has(key)方法。之因此要先实现这个方法,是由于它会被set和remove等其余方法调用。这个方法的实现和以前在Set类中的实现是同样的。使用JavaScript中的in操做符来验证一个key是不是items对象的一个属性。能够经过以下代码来实现:函数
this.has = function(key) { return key in items; }
【set】性能
该方法接受一个key和一个value做为参数。咱们直接将value设为items对象的key属性的值。它能够用来给字典添加一个新的值,或者用来更新一个已有的值
this.set = function(key, value) { items[key] = value; //{1} }
【remove】
它和Set类中的remove方法很类似,惟一的不一样点在于咱们将先搜索key(而不是value),而后咱们可使用JavaScript的delete操做符来从items对象中移除key属性
this.remove = function(key) { if (this.has(key)) { delete items[key]; return true; } return false; }
【get】
get方法首先会验证咱们想要检索的值是否存在(经过查找key值),若是存在,将返回该值, 反之将返回一个undefined值
this.get = function(key) { return this.has(key) ? items[key] : undefined; };
【values】
首先,咱们遍历items对象的全部属性值(行{1})。为了肯定值存在,咱们使用has函数来验证key确实存在,而后将它的值加入values数组(行{2})。最后,咱们就能返回全部找到的值。这个方法以数组的形式返回字典中全部values实例的值:
this.values = function() { var values = {}; for (var k in items) { //{1} if (this.has(k)) { values.push(items[k]); //{2} } } return values; };
【clear】
this.clear = function(){ items = {}; };
【size】
this.size = function(){ return Object.keys(items).length; };
【keys】
keys方法返回在Dictionary类中全部用于标识值的键名。要取出一个JavaScript对象中全部的键名,能够把这个对象做为参数传入Object类的keys方法,以下:
this.keys = function() { return Object.keys(items); };
【items】
下面来验证items属性的输出值。咱们能够实现一个返回items变量的方法,叫做getItems:
this.getItems = function() { return items; }
【完整代码】
Dictionary类的完整代码以下所示
function Dictionary(){ var items = {}; this.set = function(key, value){ items[key] = value; //{1} }; this.delete = function(key){ if (this.has(key)){ delete items[key]; return true; } return false; }; this.has = function(key){ return items.hasOwnProperty(key); //return value in items; }; this.get = function(key) { return this.has(key) ? items[key] : undefined; }; this.clear = function(){ items = {}; }; this.size = function(){ return Object.keys(items).length; }; this.keys = function(){ return Object.keys(items); }; this.values = function(){ var values = []; for (var k in items) { if (this.has(k)) { values.push(items[k]); } } return values; }; this.each = function(fn) { for (var k in items) { if (this.has(k)) { fn(k, items[k]); } } }; this.getItems = function(){ return items; } }
【使用Dictionary类】
首先,咱们来建立一个Dictionary类的实例,而后给它添加三条电子邮件地址。咱们将会使用这个dictionary实例来实现一个电子邮件地址簿。使用咱们建立的类来执行以下代码:
var dictionary = new Dictionary(); dictionary.set('Gandalf', 'gandalf@email.com'); dictionary.set('John', 'johnsnow@email.com'); dictionary.set('Tyrion', 'tyrion@email.com');
若是执行了以下代码,输出结果将会是true:
console.log(dictionary.has('Gandalf'));
下面的代码将会输出3,由于咱们向字典实例中添加了三个元素:
console.log(dictionary.size());
如今,执行下面的几行代码:
console.log(dictionary.keys()); console.log(dictionary.values()); console.log(dictionary.get('Tyrion'));
输出结果分别以下所示:
["Gandalf", "John", "Tyrion"] ["gandalf@email.com", "johnsnow@email.com", "tyrion@email.com"] tyrion@email.com
最后,再执行几行代码:
dictionary.remove('John');
再执行下面的代码:
console.log(dictionary.keys());
console.log(dictionary.values());
console.log(dictionary.getItems());
输出结果以下所示:
["Gandalf", "Tyrion"] ["gandalf@email.com", "tyrion@email.com"] Object {Gandalf: "gandalf@email.com", Tyrion: "tyrion@email.com"}
移除了一个元素后,如今的dictionary实例中只包含两个元素了
下面将详细介绍HashTable类,也叫HashMap类,是Dictionary类的一种散列表实现方式
散列算法的做用是尽量快地在数据结构中找到一个值。若是要在数据结构中得到一个值(使用get方法),须要遍历整个数据结构来找到它。若是使用散列函数,就知道值的具体位置,所以可以快速检索到该值。散列函数的做用是给定一个键值,而后返回值在表中的地址
举个例子,咱们继续使用在前面使用的电子邮件地址簿。咱们将要使用最多见的散列函数——“lose lose”散列函数,方法是简单地将每一个键值中的每一个字母的ASCII值相加
【建立散列表】
咱们将使用数组来表示咱们的数据结构,从搭建类的骨架开始:
function HashTable(){ var table = []; }
而后,给类添加一些方法。咱们给每一个类实现三个基础的方法
put(key,value):向散列表增长一个新的项(也能更新散列表)。 remove(key):根据键值从散列表中移除值。 get(key):返回根据键值检索到的特定的值。
在实现这三个方法以前,要实现的第一个方法是散列函数,它是HashTable类中的一个私有方法:
var loseloseHashCode = function (key) { var hash = 0; //{1} for (var i = 0; i < key.length; i++) { //{2} hash += key.charCodeAt(i); //{3} } return hash % 37; //{4} };
给定一个key参数,就能根据组成key的每一个字符的ASCII码值的和获得一个数字。因此,首先须要一个变量来存储这个总和(行{1})。而后,遍历key(行{2})并将从ASCII表中查到的每一个字符对应的ASCII值加到hash变量中(可使用JavaScript的String类中的charCodeAt方法——行{3})。最后,返回hash值。为了获得比较小的数值,咱们会使用hash值和一个任意数作除法的余数(mod)
【put】
如今,有了散列函数,咱们就能够实现put方法了:
this.put = function(key, value) { var position = loseloseHashCode(key); //{5} console.log(position + ' - ' + key); //{6} table[position] = value; //{7} };
首先,根据给定的key和所建立的散列函数计算出它在表中的位置(行{5})。为了便于展现信息,咱们将计算出的位置输出至控制台(行{6})。因为它不是必需的,咱们也能够将这行代码移除。而后要作的,是将value参数添加到用散列函数计算出的对应的位置上(行{7})
【get】
从HashTable实例中查找一个值也很简单。为此,将会实现一个get方法。首先,咱们会使用所建立的散列函数来求出给定key所对应的位置。这个函数会返回值的位置,所以咱们所要作的就是根据这个位置从数组table中得到这个值。
this.get = function (key) { return table[loseloseHashCode(key)]; };
【remove】
要从HashTable实例中移除一个元素,只须要求出元素的位置(可使用散列函数来获取)并赋值为undefined。
对于HashTable类来讲,咱们不须要像ArrayList类同样从table数组中将位置也移除。因为元素分布于整个数组范围内,一些位置会没有任何元素占据,并默认为undefined值。咱们也不能将位置自己从数组中移除(这会改变其余元素的位置),不然,当下次须要得到或移除一个元素的时候,这个元素会不在咱们用散列函数求出的位置上
this.remove = function(key) { table[loseloseHashCode(key)] = undefined; };
【完整代码】
HashTable类的完整代码以下所示
function HashTable() { var table = []; var loseloseHashCode = function (key) { var hash = 0; for (var i = 0; i < key.length; i++) { hash += key.charCodeAt(i); } return hash % 37; }; var djb2HashCode = function (key) { var hash = 5381; for (var i = 0; i < key.length; i++) { hash = hash * 33 + key.charCodeAt(i); } return hash % 1013; }; var hashCode = function (key) { return loseloseHashCode(key); }; this.put = function (key, value) { var position = hashCode(key); console.log(position + ' - ' + key); table[position] = value; }; this.get = function (key) { return table[hashCode(key)]; }; this.remove = function(key){ table[hashCode(key)] = undefined; }; this.print = function () { for (var i = 0; i < table.length; ++i) { if (table[i] !== undefined) { console.log(i + ": " + table[i]); } } }; }
【使用HashTable类】
下面执行一些代码来测试HashTable类:
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
下面的图表展示了包含这三个元素的HashTable数据结构:
如今来测试get方法:
console.log(hash.get('Gandalf')); console.log(hash.get('Loiane'));
得到以下的输出:
gandalf@email.com
undefined
因为Gandalf是一个在散列表中存在的键,get方法将会返回它的值。而因为Loiane是一个不存在的键,当咱们试图在数组中根据位置获取值的时候(一个由散列函数生成的位置),返回值将会是undefined(即不存在)
而后,咱们试试从散列表中移除Gandalf:
hash.remove('Gandalf'); console.log(hash.get('Gandalf'));
因为Gandalf再也不存在于表中,hash.get('Gandalf')方法将会在控制台上给出undefined的输出结果
【散列集合】
在一些编程语言中,还有一种叫做散列集合的实现。散列集合由一个集合构成,可是插入、移除或获取元素时,使用的是散列函数。咱们能够重用本章中实现的全部代码来实现散列集合,不一样之处在于,再也不添加键值对,而是只插入值而没有键。例如,可使用散列集合来存储全部的英语单词(不包括它们的定义)。和集合类似,散列集合只存储惟一的不重复的值
有时候,一些键会有相同的散列值。不一样的值在散列表中对应相同位置的时候,咱们称其为冲突。例如,咱们看看下面的代码会获得怎样的输出结果:
var hash = new HashTable(); hash.put('Gandalf', 'gandalf@email.com'); hash.put('John', 'johnsnow@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),Jonathan、Jamie和Sue有相同的散列值(5),Mindy和Paul也有相同的散列值(32)
那HashTable实例会怎样呢?执行以前的代码后散列表中会有哪些值呢?为了得到结果,咱们来实现一个叫做print的辅助方法,它会在控制台上输出HashTable中的值:
this.print = function() { for (var i = 0; i < table.length; ++i) { //{1} if (table[i] !== undefined) { //{2} console.log(i + ": " + table[i]);//{3} } } };
首先,遍历数组中的全部元素(行{1})。当某个位置上有值的时候(行{2}),会在控制台上输出位置和对应的值(行{3})。如今来使用这个方法:
hash.print();
在控制台上获得以下的输出结果:
5:sue@email.com 10:nathan@email.com 13:ana@email.com 16:aaron@email.com 19:gandalf@email.com 29:johnsnow@email.com 32:paul@email.com
Jonathan、Jamie和Sue有相同的散列值,也就是5。因为Sue是最后一个被添加的,Sue将是在HashTable实例中占据位置5的元素。首先,Jonathan会占据这个位置,而后Jamie会覆盖它,而后Sue会再次覆盖。这对于其余发生冲突的元素来讲也是同样的。
使用一个数据结构来保存数据的目的显然不是去丢失这些数据,而是经过某种方法将它们所有保存起来。所以,当这种状况发生的时候就要去解决它。处理冲突有几种方法:分离连接、线性探查和双散列法
【分离连接】
分离连接法包括为散列表的每个位置建立一个链表并将元素存储在里面。它是解决冲突的最简单的方法,可是它在HashTable实例以外还须要额外的存储空间
例如,咱们在以前的测试代码中使用分离连接的话,输出结果将会是这样:
在位置5上,将会有包含三个元素的LinkedList实例;在位置1三、16和32上,将会有包含两个元素的LinkedList实例;在位置十、19和29上,将会有包含单个元素的LinkedList实例
对于分离连接和线性探查来讲,只须要重写三个方法:put、get和remove。这三个方法在每种技术实现中都是不一样的
为了实现一个使用了分离连接的HashTable实例,咱们须要一个新的辅助类来表示将要加入LinkedList实例的元素。咱们管它叫ValuePair类(在HashTable类内部定义):
var ValuePair = function(key, value){ this.key = key; this.value = value; this.toString = function() { return '[' + this.key + ' - ' + this.value + ']'; } };
这个类只会将key和value存储在一个Object实例中。咱们也重写了toString方法,以便以后在浏览器控制台中输出结果
咱们来实现第一个方法,put方法,代码以下:
this.put = function(key, value){ var position = loseloseHashCode(key); if (table[position] == undefined) { //{1} table[position] = new LinkedList(); } table[position].append(new ValuePair(key, value)); //{2} };
在这个方法中,将验证要加入新元素的位置是否已经被占据(行{1})。若是这个位置是第一次被加入元素,咱们会在这个位置上初始化一个LinkedList类的实例(你已经在第5章中学习过)。而后,使用append方法向LinkedList实例中添加一个ValuePair实例(键和值)(行{2})
而后,咱们实现用来获取特定值的get方法:
this.get = function(key) { var position = loseloseHashCode(key); if (table[position] !== undefined){ //{3} //遍历链表来寻找键/值 var current = table[position].getHead(); //{4} while(current.next){ //{5} if (current.element.key === key){ //{6} return current.element.value; //{7} } current = current.next; //{8} } //检查元素在链表第一个或最后一个节点的状况 if (current.element.key === key){ //{9} return current.element.value; } } return undefined; //{10} };
咱们要作的第一个验证,是肯定在特定的位置上是否有元素存在(行{3})。若是没有,则返回一个undefined表示在HashTable实例中没有找到这个值(行{10})。若是在这个位置上有值存在,咱们知道这是一个LinkedList实例。如今要作的是遍历这个链表来寻找咱们须要的元素。在遍历以前先要获取链表表头的引用(行{4}),而后就能够从链表的头部遍历到尾部(行{5},current.next将会是null)。
Node链表包含next指针和element属性。而element属性又是ValuePair的实例,因此它又有value和key属性。能够经过current.element.next来得到Node链表的key属性,并经过比较它来肯定它是否就是咱们要找的键(行{6})。(这就是要使用ValuePair这个辅助类来存储元素的缘由。咱们不能简单地存储值自己,这样就不能肯定哪一个值对应着特定的键。)若是key值相同,就返回Node的值(行{7});若是不相同,就继续遍历链表,访问下一个节点(行{8})。
若是要找的元素是链表的第一个或最后一个节点,那么就不会进入while循环的内部。所以,须要在行{9}处理这种特殊的状况
使用分离连接法从HashTable实例中移除一个元素和以前在本章实现的remove方法有一些不一样。如今使用的是链表,咱们须要从链表中移除一个元素。来看看remove方法的实现:
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){ //{11} table[position].remove(current.element); //{12} if (table[position].isEmpty()){ //{13} table[position] = undefined; //{14} } return true; //{15} } current = current.next; } // 检查是否为第一个或最后一个元素 if (current.element.key === key){ //{16} table[position].remove(current.element); if (table[position].isEmpty()){ table[position] = undefined; } return true; } } return false; //{17} };
在remove方法中,咱们使用和get方法同样的步骤找到要找的元素。遍历LinkedList实例时,若是链表中的current元素就是要找的元素(行{11}),使用remove方法将其从链表中移除。而后进行一步额外的验证:若是链表为空了(行{13}——链表中再也不有任何元素了),就将散列表这个位置的值设为undefined(行{14}),这样搜索一个元素或打印它的内容的时候,就能够跳过这个位置了。最后,返回true表示这个元素已经被移除(行{15})或者在最后返回false表示这个元素在散列表中不存在(行{17})。一样,须要和get方法同样,处理元素在第一个或最后一个的状况(行{16})
重写了这三个方法后,咱们就拥有了一个使用了分离连接法来处理冲突的HashMap实例
分离连接的HashMap的完整代码以下所示
function HashTableSeparateChaining(){ var table = []; var ValuePair = function(key, value){ this.key = key; this.value = value; this.toString = function() { return '[' + this.key + ' - ' + this.value + ']'; } }; var loseloseHashCode = function (key) { var hash = 0; for (var i = 0; i < key.length; i++) { hash += key.charCodeAt(i); } return hash % 37; }; var hashCode = function(key){ return loseloseHashCode(key); }; this.put = function(key, value){ var position = hashCode(key); console.log(position + ' - ' + key); if (table[position] == undefined) { table[position] = new LinkedList(); } table[position].append(new ValuePair(key, value)); }; this.get = function(key) { var position = hashCode(key); if (table[position] !== undefined && !table[position].isEmpty()){ //iterate linked list to find key/value var current = table[position].getHead(); do { if (current.element.key === key){ return current.element.value; } current = current.next; } while(current); } return undefined; }; this.remove = function(key){ var position = hashCode(key); if (table[position] !== undefined){ //iterate linked list to find key/value var current = table[position].getHead(); do { if (current.element.key === key){ table[position].remove(current.element); if (table[position].isEmpty()){ table[position] = undefined; } return true; } current = current.next; } while(current); } return false; }; this.print = function() { for (var i = 0; i < table.length; ++i) { if (table[i] !== undefined) { console.log(table[i].toString()); } } }; }
【线性探查】
另外一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,若是索引为index的位置已经被占据了,就尝试index+1的位置。若是index+1的位置也被占据了,就尝试index+2的位置,以此类推
继续实现须要重写的三个方法。第一个是put方法:
this.put = function(key, value){ var position = loseloseHashCode(key); // {1} if (table[position] == undefined) { // {2} table[position] = new ValuePair(key, value); // {3} } else { var index = ++position; // {4} while (table[index] != undefined){ // {5} index++; // {6} } table[index] = new ValuePair(key, value); // {7} } };
和以前同样,先得到由散列函数生成的位置(行{1}),而后验证这个位置是否有元素存在(若是这个位置被占据了,将会经过行{2}的验证)。若是没有元素存在,就在这个位置加入新元素(行{3}——一个ValuePair的实例)
若是这个位置已经被占据了,须要找到下一个没有被占据的位置(position的值是undefined),所以咱们声明一个index变量并赋值为position+1(行{4}——在变量名前使用自增运算符++会先递增变量值而后再将其赋值给index)。而后验证这个位置是否被占据(行{5}),若是被占据了,继续将index递增(行{6}),直到找到一个没有被占据的位置。而后要作的,就是将值分配到这个位置(行{7})
若是再次执行前面实例中插入数据的代码,下图展现使用了线性探查的散列表的最终结果:
让咱们来模拟一下散列表中的插入操做
一、试着插入Gandalf。它的散列值是19,因为散列表刚刚被建立,位置19仍是空的——能够在这里插入数据
二、试着在位置29插入John。它也是空的,因此能够插入这个姓名
三、试着在位置16插入Tyrion。它是空的,因此能够插入这个姓名
四、试着插入Aaron,它的散列值也是16。位置16已经被Tyrion占据了,因此须要检查索引值为position+1的位置(16+1)。位置17是空的,因此能够在位置17插入Aaron
五、接着,试着在位置13插入Donnie。它是空的,因此能够插入这个姓名
六、想在位置13插入Ana,可是这个位置被占据了。所以在位置14进行尝试,它是空的,因此能够在这里插入姓名
七、而后,在位置5插入Jonathan,这个位置是空的,因此能够插入这个姓名
八、试着在位置5插入Jamie,可是这个位置被占了。因此跳至位置6,这个位置是空的,所以能够在这个位置插入姓名
九、试着在位置5插入Sue,可是位置被占据了。因此跳至位置6,但也被占了。接着跳至位置7,这里是空的,因此能够在这里插入姓名。以此类推
如今插入了全部的元素,下面实现get方法来获取它们的值
this.get = function(key) { var position = loseloseHashCode(key); if (table[position] !== undefined){ //{8} if (table[position].key === key) { //{9} return table[position].value; //{10} } else { var index = ++position; while (table[index] === undefined || table[index].key !== key){ //{11} index++; } if (table[index].key === key) { //{12} return table[index].value; //{13} } } } return undefined; //{14} };
要得到一个键对应的值,先要肯定这个键存在(行{8})。若是这个键不存在,说明要查找的值不在散列表中,所以能够返回undefined(行{14})。若是这个键存在,须要检查咱们要找的值是否就是这个位置上的值(行{9})。若是是,就返回这个值(行{10})。
若是不是,就在散列表中的下一个位置继续查找,直到找到一个键值与咱们要找的键值相同的元素(行{11})。而后,验证一下当前项就是咱们要找的项(行{12}——只是为了确认一下)而且将它的值返回(行{13})。
咱们没法肯定要找的元素实际上在哪一个位置,这就是使用ValuePair来表示HashTable元素的缘由
remove方法和get方法基本相同,不一样之处在于行{10}和{13},它们将会由下面的代码代替:
table[index]=undefined;
要移除一个元素,只须要给其赋值为undefined,来表示这个位置再也不被占据而且能够在必要时接受一个新元素
线性探查的HashTable的完整代码以下所示
function HashLinearProbing(){ var table = []; var ValuePair = function(key, value){ this.key = key; this.value = value; this.toString = function() { return '[' + this.key + ' - ' + this.value + ']'; } }; var loseloseHashCode = function (key) { var hash = 0; for (var i = 0; i < key.length; i++) { hash += key.charCodeAt(i); } return hash % 37; }; var hashCode = function(key){ return loseloseHashCode(key); }; this.put = function(key, value){ var position = hashCode(key); console.log(position + ' - ' + key); if (table[position] == undefined) { table[position] = new ValuePair(key, value); } else { var index = ++position; while (table[index] != undefined){ index++; } table[index] = new ValuePair(key, value); } }; this.get = function(key) { var position = hashCode(key); if (table[position] !== undefined){ if (table[position].key === key) { return table[position].value; } else { var index = ++position; while (table[index] !== undefined && (table[index] && table[index].key !== key)){ index++; } if (table[index] && table[index].key === key) { return table[index].value; } } } else { //search for possible deleted value var index = ++position; while (table[index] == undefined || index == table.length || (table[index] !== undefined && table[index] && table[index].key !== key)){ index++; } if (table[index] && table[index].key === key) { return table[index].value; } } return undefined; }; this.remove = function(key){ var position = hashCode(key); if (table[position] !== undefined){ if (table[position].key === key) { table[position] = undefined; } else { var index = ++position; while (table[index] === undefined || table[index].key !== key){ index++; } if (table[index].key === key) { table[index] = undefined; } } } }; this.print = function() { for (var i = 0; i < table.length; ++i) { if (table[i] !== undefined) { console.log(i + ' -> ' + table[i].toString()); } } }; }
【更好的散列函数】
“loselose”散列函数并非一个表现良好的散列函数,由于它会产生太多的冲突。若是使用这个函数的话,会产生各类各样的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),固然也包括较低的冲突可能性
另外一个能够实现的比“loselose”更好的散列函数是djb2:
var djb2HashCode = function (key) { var hash = 5381; //{1} for (var i = 0; i < key.length; i++) { //{2} hash = hash * 33 + key.charCodeAt(i); //{3} } return hash % 1013; //{4} };
它包括初始化一个hash变量并赋值为一个质数(行{1}——大多数实现都使用5381),而后迭代参数key(行{2}),将hash与33相乘(用来看成一个魔力数),并和当前迭代到的字符的ASCII码值相加(行{3})
最后,咱们将使用相加的和与另外一个随机质数(比咱们认为的散列表的大小要大——在本例中,咱们认为散列表的大小为1000)相除的余数。
若是再次执行前面实例中插入数据的代码,这将是使用djb2HashCode代替loseloseHashCode的最终结果:
798-Gandalf 838-John 624-Tyrion 215-Aaron 278-Donnie 925-Ana 288-Jonathan 962-Jamie 502-Sue 804-Mindy 54-Paul 223-Nathan
没有冲突!这并非最好的散列函数,但这是最被社区推荐的散列函数之一
ECMAScript 2015新增了Map类。咱们能够基于ES6的Map类开发咱们的Dictionary类
看看原生的Map类怎么用。仍是用咱们原来测试Dictionary类的例子:
var map = new Map(); map.set('Gandalf', 'gandalf@email.com'); map.set('John', 'johnsnow@email.com'); map.set('Tyrion', 'tyrion@email.com'); console.log(map.has('Gandalf')); //输出true console.log(map.size); //输出3 console.log(map.keys()); //输出["Gandalf", "John", "Tyrion"] console.log(map.values()); //输出["gandalf@email.com", "johnsnow@email.com", "tyrion@email.com"] console.log(map.get('Tyrion')); //输出tyrion@email.com
和Dictionary类不一样,ES6的Map类的values方法和keys方法都返回Iterator,而不是值或键构成的数组。另外一个区别是,咱们实现的size方法返回字典中存储的值的个数,而ES6的Map类则有一个size属性
删除map中的元素能够用delete方法:
map.delete('John');
clear方法会重置map数据结构,这跟咱们在Dictionary类里实现的同样
除了Set和Map这两种新的数据结构,ES6还增长了它们的弱化版本,WeakSet和WeakMap。基本上,Map和Set与其弱化版本之间仅有的区别是:
一、WeakSet或WeakMap类没有entries、keys和values等方法;
二、只能用对象做为键
建立和使用这两个类主要是为了性能。WeakSet和WeakMap是弱化的(用对象做为键),没有强引用的键。这使得JavaScript的垃圾回收器能够从中清除整个入口。另外一个优势是,必须用键才能够取出值。这些类没有entries、keys和values等迭代器方法,所以,除非知道键,不然没有办法取出值
使用WeakMap类的例子以下:
var map = new WeakMap(); var ob1 = {name:'Gandalf'}, //{1} ob2 = {name:'John'}, ob3 = {name:'Tyrion'}; map.set(ob1, 'gandalf@email.com'); //{2} map.set(ob2, 'johnsnow@email.com'); map.set(ob3, 'tyrion@email.com'); console.log(map.has(ob1)); //{3} 输出true console.log(map.get(ob3)); //{4} 输出tyrion@email.com map.delete(ob2); //{5}
WeakMap类也能够用set方法,但不能使用数字、字符串、布尔值等基本数据类型,须要将名字转换为对象(行{1}和行{2})。搜索(行{3})、读取(行{4})和删除值(行{5}),也要传入做为键的对象。一样的逻辑也适用于WeakSet类