本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构。
集合、字典和散列表可以存储不重复的值。在集合中,我们感兴趣的是每个值本身,并把它作为主要元素。而字典和散列表中都是用 [键,值]的形式来存储数据。但是两个数据结构的实现方式略有不同。

第七章 字典和散列表

字典

集合表示一组互不相同的元素(不重复的元素)。在字典里,存储的是 [键,值] 对,其中键名是用来查询特定元素的。字典和结合很相似,集合以 [值,值] 的形式存储元素,字典则是以 [键,值]的形式来存储元素。字典也成为映射。

创建字典

与 Set 类相似,ES6 中也包含了一个 Map 类的实现,即我们所说的字典。

我们在本章中,将要实现的是以ES6 中 Map 类的实现为基础的。你会发现它跟 Set 类很相似(但不同与存储 [值,值]对的形式,我们将要存储的是 [键,值]对。)

下面是我们的 Dictionary 类的骨架:

function Dictionary(){
    let items = {};
}

然后我们要声明一些 映射/ 字典 所能使用的方法。

  • set(key,value):想字典中添加新的元素。
  • delete(key):通过使用键值来从字典中移除键值对应的数据值
  • has(key):如果某个键值存在于这个字典,则返回true,没有则返回 false
  • get(key):通过键值查找特定的数值并返回
  • clear():键这个字典中的所有元素全部删除
  • size():返回字典所包含元素的数量。与数组的 length 相似
  • keys():将字典所包含的所有键名以数组形式返回
  • values():将字典所包含的所有数值以数组形式返回。

has 和 set 方法

首先要实现 has 这个方法,因为它会被 set 和 remove 其他方法调用。我么可以通过以下代码实现

this.has = function(key){
    return items.hasOwnProperty(key);
}

这个方法 的实现和我们之前在 Set 类中的实现是一样的。我们使用 JavaScript 中的 in 操作符来验证一个 key 是否是 items 对象的一个属性。

然后就是 set 方法的实现

this.set = function(key,value){
    items[key] = value;
}

该方法接受一个 key 和 一个 value 作为参数。我们直接将 value 设为 items 对象的 key 属性的值。它可以用来给字典添加一个新的值,或者用来更新一个已有的值。

delete方法

它和 Set 类的 delete 方法很类似,唯一不同的是,我们将先搜索 key (而不是 value)

this.delete = function(key){
    if(this.has(key)){
        delete items[key];
        return true;
    }
    return false;
}

我们可以使用 JavaScript 中的 delete 操作符来删除 items 对象中的 key 属性。

get 和 values 方法

如果我们想在字典里查找一个特定的项,并检索它的值,可以通过以下方法

this.get = function(key){
    return this.has(key) ? items[key] : undefined;
}

get 方法会首先通过has 方法验证是否有 key 值存在,存在就返回该值,没有就返回一个 undefined 。

下面是 values 方法,以数组的形式返回 字典中所有的 values 的值。

this.values = function(){
    const values = [];
    for(let k i items){
        if(this.has(k)){
            values.push(items[k]);
        }
    }
    return values;
}

clear、size、keys 和 getItems 方法

clear 和 size 方法和 集合那章的实现是完全一模一样的。

this.clear = function(){
    this.items = {};
}
this.size = function(){
    let count = 0;
    if(let k in items){
        if(this.has(k)){
            ++count;
        }
    }
    return count;
}

keys 方法返回所有用于标识值的键名,可以使用 Object 类的 keys 方法

this.keys = function(){
    return Object.keys(items);
}

最后,验证 items 属性的输出值,我们加多一个返回 items 变量的方法,叫做 getItems

this.getItem = function(){
    return items;
}

使用 Dictionary 类

const dictionary = new Dictionary();
dictionary.set('mail','544289495@qq.com');
dictionary.set('website','http://laibh.top');
dictionary.set('name','lbh');
dictionary.set('word','1414');
console.log(dictionary.getItem()); // {mail: "544289495@qq.com", website: "http://laibh.top", name: "lbh", word: "1414"}
console.log(dictionary.get('word')); // 1414
console.log(dictionary.size()); // 4
console.log(dictionary.keys()); // ["mail", "website", "name", "word"]
console.log(dictionary.values()); // ["544289495@qq.com", "http://laibh.top", "lbh", "1414"]

dictionary.delete('word');

console.log(dictionary.getItem()); // {mail: "544289495@qq.com", website: "http://laibh.top", name: "lbh"}
console.log(dictionary.get('word')); // undefined
console.log(dictionary.size()); // 3
console.log(dictionary.keys()); // ["mail", "website", "name"]
console.log(dictionary.values()); // ["544289495@qq.com", "http://laibh.top", "lbh"]

散列表

本节中,你将会学到 HashTable 类,也叫 HashMap 类,它是 Dictionary 类的一种散列表实现方式。

散列算法的作用是尽可能快地在数据结构中找到一个值。之前的章节里面你已经知道要在数据结构中获得一个值(使用 get 方法),需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址。

创建散列表

我们将使用数组来表示我们的数据结构,与之前一样,从搭建类的骨架开始

function HashTable(){
    let table = [];
}

然后,给类加一些方法。我们给每个类实现三个基本方法.

  • put(key,value):向散列表中增加一个新的项(也能更新散列表)
  • remove(key):根据键值从散列表中移除值
  • get(key):返回根据键值检索到的特定的值。

在实现三个方法之前,要实现的第一个方法就是散列函数,它是 HashTable 类中的一个私有方法。

var loseloseHashCode = function(key){
    var hash = 0;
    for(var i = 0; i < key.length; i++){
        hash += key.charCodeAt(i);
    }
    return hash % 37;
}

给定一个 key 参数,我们就能根据组成 key 的每个字符的 ASCII码值的和的得到一个数字。所以,首先需要一个变量来存储这个总和。然后,遍历 key 并将从 ASCII 表中查到的每个字符对应的 ASCII 值加到 hash 变量中(可以使用String 类的 charCodeAt方法)。最后,返回 hash 值。为了得到较小的值,我们会使用 hash 值和一个任意的数做除法的余数。

现在有了散列函数,基于可以实现 put 方法

this.put = function(key,value){
    var position  = loseloseHashCode(key);
    console.log(position + '-' + key);
    table[position] = value;
}

首先,根据给定的 key ,我们需要根据所创建的散列函数计算出它在表中的位置。为了方便展示消息,我们需要计算出的位置输出至控制台。由于它不是必需的,我们也可以选择省略。然后要做的就是,将 value 参数添加到 用散列函数计算出的对应的位置上。

从 HashTable 实例中查找一个值也很简单。为此我们会实现一个 get 方法

this.get = function(key){
    return table[loseloseHashCode(key)];
}

首先我们会使用所创建的散列函数来求出所对应的位置。这个函数会返回值的位置,因为我们需要做的就是根据这个位置来从数组 table 中获得这个值。

实现的最后一个方法是 remove 方法

this.remove = function(key){
    table[loseloseHashCode(key)] = undefined;
}

要从 HashTable 实例中移除一个元素,只需要求出元素的位置并赋值为 undefined。

对于 HashTable 类来说,我们不需要像链表一样从 table 数组中将位置也移除。由于元素分布整个数组范围内,一些位置会没有任何元素占据,并默认为 undefined值。我们也不能将位置本身刚从数组中移除(这样会改变元素的位置),否则,当下次需要获得或者是移除一个元素的时候,这个元素会不在我们用散列函数求出的位置上。

使用 HashTable 类

测试 HashTable 类

const hashTable = new HashTable();

hashTable.put('mail','544289495@qq.com'); // 12-mail
hashTable.put('website','http://laibh.top'); // 15-website
hashTable.put('name','lbh'); // 10-name
hashTable.put('word','1414'); // 0-word

console.log(hashTable.get('website')); // http://laibh.top
console.log(hashTable.get('word')); // 1414

hashTable.remove('word');

console.log(hashTable.get('website')); // http://laibh.top
console.log(hashTable.get('word')); // undefined

散列表和散列结合

散列表和散列映射是一样的,在一些编程语言中,还有一种叫做散列集合的实现。散列集合由一个集合构成,但是插入、移除或获取元素时,使用的还是散列函数。我们可以重用本章中实现的所有代码来实现散列集合,不同之处在于,不再添加键值对,而是只插入而没有键。例如,可以使用散列集合来存储所有的英文单词(不包括它的定义)。和集合相似,散列集合值存储唯一的不重复的值。

处理列表中更多冲突

有时候,一些表会有相同的散列值。不同的值在散列表中对应相同的位置的时候,我们称其为冲突。

hashTable.put('Gandalf','Gandalf@qq.com');
hashTable.put('John','John@qq.com');
hashTable.put('Tyrion','Tyrion@qq.com');
hashTable.put('Aaron','Aaron@qq.com');
hashTable.put('Donnie','Donnie@qq.com');
hashTable.put('Ana','Ana@qq.com');
hashTable.put('Jonathan','Jonathan@qq.com');
hashTable.put('Jamie','Jamie@qq.com');
hashTable.put('Sue','Sue@qq.com');
hashTable.put('Mindy','Mindy@qq.com');
hashTable.put('Paul','Paul@qq.com');
hashTable.put('Nathan','Nathan@qq.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

可以看出来,上面有很多相同的散列值。

为了获得结果,我们来实现一个print 的辅助函数。

this.print = function(){
    for(let i = 0; i < table.length; ++i){
        if(table[i] !== undefined){
            console.log(i + ':' + table[i]);
        }
    }
}

运行 print 函数,得到以下输出

5:Sue@qq.com
10:Nathan@qq.com
13:Ana@qq.com
16:Aaron@qq.com
19:Gandalf@qq.com
29:John@qq.com
32:Paul@qq.com

可以看出来,按照先后的顺序,有相同 散列值的被后面的覆盖了。

使用一个数据结构来保存数据的目的显然不是去丢失这些数据,而是通过某个方法将它们全部保存起来。因此,当这种情况发生的时候,处理冲突的方法有几种:分离链接、线性探查、双散列法。

这里介绍第一种方法

分离链接

分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的最简单的方法,但是它在 HashTable 实例之外还需要额外的存储空间。

例如:我们在之前的测试代码中使用分离链接的话,输出结果会是这样的:

在位置5上,将会有包含3个元素的 LinkedList 实例;在位置13、16、32上,将会有包含两个元素的 LinkedList 实例;在位置10、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){
        table[position] = new LinkedList();
    }

    table[position].append(new ValuePair(key,value));
}

在这个方法中,将验证要加入新元素的位置是否已经被占据,如果这个位置是第一次被加入元素,我们会在这个位置上初始化一个 LinkedList 类的实例。然后使用 LinkedList里面的append 方法来向LinkedList 实例添加一个 ValuePair 实例(键和值)

get 方法

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;
}

首先要判断在特定位置上面是否有元素存在,没有则返回一个 undefined 。反之则遍历链表来寻找我们需要的元素,先获取链表表头的引用,然后就可以从链表的头部遍历到尾部

Node 链表包含 next 和 element 属性。而 element 属性又是 ValuePair 的实例,所以它又有 value 和 key 属性。可以通过 current.element.key 来获得 Node链表的 key 属性,并通过比较它来确定它是否就是我们要找的键。key 相同就返回 Node 值,不相同,就继续遍历。

如果要找的元素是链表的第一个或者是最后一个节点,就不会进入 while 循环,因此需要在后面加多一个判断。

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){
                table[position].remove(current.element);
                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;
}

在 remove方法中,我们使用和get 方法一样的步骤找到要找的元素。遍历 LinkedList 实例,如果链表中的 current 元素就是我们要找的元素,使用 remove 方法将其从链表中移除,然后进一步额外的验证,如果链表为空,就将散列表这个位置设为 undefined ,这样搜索一个元素或打印它的内容的时候,就可以跳过这个位置。最后返回 true 表示整个元素已经被移除或者在最后返回 false 表示整个元素在散列表中不存在。同样,和 get 方法一样,处理元素在第一个或者最后一个的情况。

重写了这三个方法,我们就拥有了一个分离链接法来处理冲突的HashMap 实例了。

重新运行之前的例子,会得到以下结果

5:[Jonathan-Jonathan@qq.com]-[Jamie-Jamie@qq.com]-[Sue-Sue@qq.com]
10:[Nathan-Nathan@qq.com]
13:[Donnie-Donnie@qq.com]-[Ana-Ana@qq.com]
16:[Tyrion-Tyrion@qq.com]-[Aaron-Aaron@qq.com]
19:[Gandalf-Gandalf@qq.com]
29:[John-John@qq.com]
32:[Mindy-Mindy@qq.com]-[Paul-Paul@qq.com]

创建更好的散列函数

我们实现的 lose lose散列函数并不是一个表现良好的散列函数。因为它会产生太多的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),当然也包括较低的冲突可能性。

另一个可以实现的比 lose lose 更好的散列函数是 djb2

var djb2HashCode = function(key){
    var hash = 5381;
    for(var i = 0; i < key.length; i++){
        hash = hash* 33 + key.charCodeAt(i);
    }
    return hash % 1013;
}

它包括初始化一个 hash 变量并赋值一个质数,然后迭代参数 key ,将 hash 与参数33相乘

,并和当前迭代到的字符的 ASCII码值相加。

最后,我们将使用相加的和于另一个随机质数相除的余数。

这并不是最好的散列函数,但是是社区最被推崇的散列函数之一。

ES6——Map类

ES6中有Map,我们可以基于这个类来开发Dictionary 类

const map = new Map();
map.set('Gandalf','Gandalf@qq.com');
map.set('John','John@qq.com');
map.set('Tyrion','Tyrion@qq.com');
map.set('Aaron','Aaron@qq.com');
map.set('Donnie','Donnie@qq.com');
map.set('Ana','Ana@qq.com');
map.set('Jonathan','Jonathan@qq.com');
map.set('Jamie','Jamie@qq.com');
map.set('Sue','Sue@qq.com');
map.set('Mindy','Mindy@qq.com');
map.set('Paul','Paul@qq.com');
map.set('Nathan','Nathan@qq.com');

console.log(map.has('Gandalf')); //true
console.log(map.size); // 12
console.log(map.keys()); // MapIterator
console.log(map.values());  // MapIterator
console.log(map.get('Gandalf')); // Gandalf@qq.com

可以看到,values 和 keys 方法返回都是 MapIterator,而不是值或键构成的数组。另一个区别是,size 是属性。

删除 map 中的元素可以用 delete 方法

map.delete('Gandalf');

clear 方法会重置 map 数据结构,这跟我们在之前的实现时一样的。

ES6——WeakMap 类 和 WeakSet 类

除了 Set 和 Map 两种新的数据结构,ES6还增加了它们的弱化版本,WeakMap 和 WeakSet 类。

基本上,Map 和 Set 与其弱化版本之间仅有的区别是

  • WeakMap 和 WeakSet 类没有 entries、keys、values 等方法。
  • 只能用对象作为键

创建和使用这两个类主要是为了性能。没有强引用的键,这使得JavaScript 的垃圾回收器可以从中清除整个入口。

另外的一个优点是,必须用键才可以取出值。这些类没有 entries 、keys、values 等迭代器方法,因此,除非你知道键,否则没有办法取出值。

小结

本章,我们学习了字典的相关知识,了解了如何添加、移除和获取元素已经其他的一些方法,我们还了解了字典和集合的不同之处。

学习了散列运算,知道该怎么去创建一个散列表数据结构,如何创建一个散列函数。

下一章,我们将学习一种新的数据结构——树。

书籍链接: 学习JavaScript数据结构与算法

为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表的更多相关文章

  1. 为什么我要放弃javaScript数据结构与算法(第九章)—— 图

    本章中,将学习另外一种非线性数据结构--图.这是学习的最后一种数据结构,后面将学习排序和搜索算法. 第九章 图 图的相关术语 图是网络结构的抽象模型.图是一组由边连接的节点(或顶点).学习图是重要的, ...

  2. 为什么我要放弃javaScript数据结构与算法(第二章)—— 数组

    第二章 数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构.JavaScript里也有数组类型,虽然它的第一个版本并没有支持数组.本章将深入学习数组数据结构和它的能力. 为什么 ...

  3. 为什么我要放弃javaScript数据结构与算法(第一章)—— JavaScript简介

    数据结构与算法一直是我算比较薄弱的地方,希望通过阅读<javaScript数据结构与算法>可以有所改变,我相信接下来的记录不单单对于我自己有帮助,也可以帮助到一些这方面的小白,接下来让我们 ...

  4. 为什么我要放弃javaScript数据结构与算法(第十一章)—— 算法模式

    本章将会学习递归.动态规划和贪心算法. 第十一章 算法模式 递归 递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题.递归通常涉及函数调用自身. 递归函数是像下面能够直接调用自身的 ...

  5. 为什么我要放弃javaScript数据结构与算法(第十章)—— 排序和搜索算法

    本章将会学习最常见的排序和搜索算法,如冒泡排序.选择排序.插入排序.归并排序.快速排序和堆排序,以及顺序排序和二叉搜索算法. 第十章 排序和搜索算法 排序算法 我们会从一个最慢的开始,接着是一些性能好 ...

  6. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  7. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

  8. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  9. 为什么我要放弃javaScript数据结构与算法(第四章)—— 队列

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第四章 队列 队列数据结构 队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序 ...

随机推荐

  1. Ubuntu Server无线连接配置

    由于Ubuntu  Linux内核对于WPA的加密方式支持不是很好,所有使用普通方式的命令行无法连接WPA2类型的路由.首先检查是否安装了wpasupplicant,Ubuntu 10.04已经安装了 ...

  2. Windows 更改桌面位置

    运行regedit HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders 找到 ...

  3. 快速入门系列--深入理解C#

    C#语言在近些年得到了长足的方法,代码风格越来越简洁美观,例如常用的泛型及其约束.可空类型.隐式类型.匿名类型和委托等,通过下面的表格可以对这部分相对简单的特性的使用有一个初步的了解. 特性 示例 泛 ...

  4. js计算24点

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  5. C# 向Http服务器送出 POST 请求

    //向Http服务器送出 POST 请求 public string m_PostSubmit(string strUrl,string strParam) { string strResult = ...

  6. Java基础知识强化之集合框架笔记37:用户登录注册案例

    1. 登录注册案例分析图解: 2. 用户登录案例 详细分析 和 分包实现: (1)用户登录案例详细分析(面向对象思想) 按照如下的操作,可以让我们更符合面向对象思想: • 有哪些类呢?         ...

  7. Installation error: INSTALL_CANCELED_BY_USER

    这个错误,从字面上理解,就是已经安装的时候被用户取消了. 我出现这个错误的时候,是手机连接在电脑上的. 经过网上搜索,确认这是安装apk到手机时,被手机取消了安装操作. 1. 确保手机处于开发者模式. ...

  8. 管理 sendmail 的邮件队列

    邮件队列是存储 sendmail 命令传送的邮件消息数据和控制文件的目录.缺省情况下,邮件队列是 /var/spool/mqueue. 邮件消息可能由于很多原因而排入队列. 例如: sendmail  ...

  9. 限定只能处理&quot;A仓&quot;和&quot;B仓&quot;入库

    应用 Oracle Inventory 层 Level Function 函数名 Funcgtion Name INV_INVTTMTX_MISC 表单名 Form Name INVTTMTX 说明 ...

  10. WinSock 异步I/O模型-1

    异步选择(WSAAsyncSelect):异步选择基本定义 异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O 模型.利用这个模型,应用程序可在一个套接字上,接收以 Windows ...