什么是散列表?

  • 散列表是Dictionary(字典)的一种散列表实现方式,字典传送门
  • 一个很常见的应用是使用散列表来表示对象。Javascript语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法(成员)被存储为key对象类型,每个key指向对应的对象成员。
  • 字典中使用的电子邮件地址簿为例。我们将使用最常见的散列函数:lose lose散列函数,方法是简单的将每个键值中的每个字符的ASCII值相加,如下图所示:

创建散列表

class HashTable {
this.table = {};
}

实现几个简单方法

  1. toStrFn() 转字符串 和字典中一样
toStrFn (key){
if (key === null) {
return 'NULL';
} else if (key === undefined) {
return 'UNDEFINED';
} else if (typeof key === 'string' || key instanceof String) {
return `${key}`;
}else if ( Object.prototype.toString.call({})==='[object Object'] ){
return JSON.stringify(obj)
}
return key.toString();
}
  1. hashCode(key) 创建散列函数
loseloseHashCode(key) {
if (typeof key === 'number') { // 检验key是否是一个数字
return key;
}
const tableKey = this.toStrFn(key); // 将 key 转换为一个字符串
let hash = 0; // 创建一个hash变量
for (let i = 0; i < tableKey.length; i++) { // 迭代转为字符串后的key
hash += tableKey.charCodeAt(i); // 从ASCII表中查到的每个字符对应的 ASCII 值加到 hash 变量中
}
return hash % 37; // 返回hash值。为了得到比较小的值,使用hash值和任意数取余(规避超过最大表示范围的风险,暂时有坑!!!)
} hashCode(key) { //hashCode 方法简单地调用了 loseloseHashCode 方法,将 key 作为参数传入
return this.loseloseHashCode(key);
}
  1. put(key,value) 将键和值加入散列表
put(key, value) {
if (key != null && value != null) { // 检验 key 和 value 是否合法,如果不合法就返回 false
const position = this.hashCode(key); // 根据给出的key,在表中找到一个位置
this.table[position] = new ValuePair(key, value); // 用 key 和 value 创建一个 ValuePair (此实例和字典中的一样)实例
return true;
}
}
return false;
  1. get(key)从散列表中获取一个值
get(key) {
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
  1. remove(key) 从散列表中移除一个值
remove(key) {
const hash = this.hashCode(key); // 获取hash
const valuePair = this.table[hash]; // 获取值
if (valuePair != null) { // 如果有值
delete this.table[hash]; // 删除它
return true; // 返回true
}
return false; // 如果没找到对应的值,返回false
}

使用 HashTable 类

  • 用一些代码来测试一下
const hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com');
hash.put('Tyrion', 'tyrion@email.com'); console.log(hash.hashCode('Gandalf') + ' - Gandalf'); // 19 - Gandalf
console.log(hash.hashCode('John') + ' - John'); // 29 - John
console.log(hash.hashCode('Tyrion')+' - Tyrion'); // 16 - Tyrion console.log(hash.get('Gandalf')); // gandalf@email.com
console.log(hash.get('Loiane')); // undefined 由于 Loiane 是一个不存在的键,所以返回会是 undefined(即不存在)。 hash.remove('Gandalf');
console.log(hash.get('Gandalf')); // undefined

处理散列表中的冲突(解决上面的坑)

  • 有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为 冲突。来看一下下面代码的输出结果:
const hash = new HashTable();
hash.put('Jonathan', 'jonathan@email.com'); 0
hash.put('Jamie', 'jamie@email.com'); 通过对每个提到的名字调用 hash.hashCode 方法,输出结果如下。
5 - Jonathan
5 - Jamie
  • Jonathan和Jamie有相同的散列值5。
  • 由于 Jamie是最后一个被添加的,它将是在 HashTable 实例中占据位置 5 的元素。
  • 如果调用Hash.get(Jonathan)后输出的是'jonathan@email.com'还是'jamie@email.com'呢?
  • 有两种处理冲突的方法:分离链接和线性探查。

分离链接

  • 分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的 最简单的方法,但是在 HashTable 实例之外还需要额外的存储空间。
  • 重写一下三个方法:put、get和remove。
  1. put()
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) { // 判断新元素的位置是否已被占据
this.table[position] = new LinkedList(); // 初始化一个 LinkedList 类(链表类的实现方法见链表传送门)的实例
}
this.table[position].push(new ValuePair(key, value)); // 向链表中添加一个ValuePair实例
return true;
}
return false;
}

链表传送门

  1. get()
get(key) {
const position = this.hashCode(key); // 转化hash值
const linkedList = this.table[position]; // 获取hash对应的地址
if (linkedList != null && !linkedList.isEmpty()) { // 如果链表实例存在
let current = linkedList.getHead(); // 如果有,获取链表头的引用地址
while (current != null) { // 迭代到最后
if (current.element.key === key) { // 找到key值与传入key相同的
return current.element.value; // 返回value值
}
current = current.next; // 如果key值与传入key不同,再往下找
}
}
return undefined; // 如果链表实例不存在,返回undefined
}
  1. remove()
remove(key) {
const position = this.hashCode(key); // 转化hash值
const linkedList = this.table[position]; // 获取hash对应的地址
if (linkedList != null && !linkedList.isEmpty()) { // 如果链表实例存在
let current = linkedList.getHead(); // 如果有,获取链表头的引用地址
while (current != null) { // 迭代到最后
if (current.element.key === key) { // 找到key值与传入key相同的
linkedList.remove(current.element); // 使用 remove 方法将其从链表中移除
if (linkedList.isEmpty()) { // 删除后如果空了
delete this.table[position]; // 也要在散列表中的位置删除
}
return true; // 返回 true 表示该元素已经被移除
}
current = current.next; // 如果key值与传入key不同,再往下找
}
}
return false; // 返回false表示该元素在散列表中不存在
}

线性探查

  • 另一种解决冲突的方法是线性探查。之所以称作线性,是因为它处理冲突的方法是将元素直 4 接存储到表中,而不是在单独的数据结构中。
  • 当想向表中某个位置添加一个新元素的时候,如果索引为 position 的位置已经被占据了,就尝试 position+1 的位置。如果 position+1 的位置也被占据了,就尝试 position+2 的位 置,以此类推,直到在散列表中找到一个空闲的位置。
  • 想象一下,有一个已经包含一些元素的散列表,我们想要添加一个新的键和值。我们计算这个新键的 hash,并检查散列表中对应的位置 是否被占据。如果没有,我们就将该值添加到正确的位置。如果被占据了,我们就迭代散列表, 直到找到一个空闲的位置。
  • 同样的也需要重写一下三个方法
  1. put()
put(key, value) {
if (key != null && value != null) { // 检验传入的key和value是否有效
const position = this.hashCode(key); // 获取hash值
if (this.table[position] == null) { // 如果这个hash值的位置没有元素存在
this.table[position] = new ValuePair(key, value); // 直接等于一个ValuePair实例就好了
} else { // 反之,不存在
let index = position + 1; // 先创建一个变量,等于hash值加一
while (this.table[index] != null) { // 迭代,直到找到一个空位置
index++;
}
this.table[index] = new ValuePair(key, value); // 在这个空位置处放入一个ValuePair实例
}
return true;
}
return false;
}
  1. get()
get(key) {  const position = this.hashCode(key);
if (this.table[position] != null) { // 确定这个键存在
if (this.table[position].key === key) { // 如果这个值没变动过
return this.table[position].value; // 直接返回该位置的value值
}
while (this.table[index] != null && this.table[index].key !== key) { // 如果这个值改变了,就从下一个位置继续迭代,直到找到要找的元素或者空位置
let index = position + 1;
index++;
}
if (this.table[index] != null && this.table[index].key === key) { //当跳出循环时,如果该位置不是空并且它的key和传入的key相同,返回它的value
return this.table[position].value;
}
return undefined; // {8}
}
}
  1. remove() 和get方法基本相同

verifyRemoveSideEffect(key, removedPosition) { // 该函数用于在删除后把添加时移动的值移回原位置,接收两个值:被删除的 key 和该 key 被删除的位置。
const hash = this.hashCode(key); // 获取被删除的 key 的 hash 值
let index = removedPosition + 1; // 创建一个变量,等于删除位置+1
while (this.table[index] != null) { // 迭代 直到找到空位置
const posHash = this.hashCode(this.table[index].key); // 迭代时当前位置上元素的 hash 值
if (posHash <= hash || posHash <= removedPosition) { // 如果当前元素的hash值小于等于原始的值或者小于等于删除key的hash值,就需要把它移动到删除的位置
this.table[removedPosition] = this.table[index];
delete this.table[index]; // 再删除它当前元素(因为它已经被复制到删除的位置了)
removedPosition = index; // 再把变量更新为新删除的位置,重复,直到有空位置
}
index++;
}
} remove(key) {
const position = this.hashCode(key);
if (this.table[position] != null) { // 判断该位置是否有值
if (this.table[position].key === key) { // 如果该位置的key等于传入的key
delete this.table[position]; // 删除该值
this.verifyRemoveSideEffect(key, position); // 删除后把原来属于该位置的值挪回来
return true;
}
let index = position + 1; // 如果该位置的key不等于传入的key,证明被移动过
while (this.table[index] != null && this.table[index].key !== key ) { // 迭代
index++;
}
if (this.table[index] != null && this.table[index].key === key) {// 如果该位置不为空,并且它的key等于传入的key
delete this.table[index]; // 删除
this.verifyRemoveSideEffect(key, index); // 挪回来
return true;
}
}
return false;
}

创建更好的散列函数

  • 我们实现的散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。 一个表现良好的散列函数是由几个方面构成的:

    • 插入和检索元素的时间(即性能)
    • 较低的冲突可能性。
  • 另一个可以实现的更好的散列函数:
djb2HashCode(key) {
const tableKey = this.toStrFn(key); // 先将键转化为字符串
let hash = 5381; // 初始化一个hash变量并复制为一个质数(大多数实现都使用 5381)
for (let i = 0; i < tableKey.length; i++) { // 迭代key
hash = (hash * 33) + tableKey.charCodeAt(i); // 将hash与33相乘再加上当前迭代到的字符的 ASCII码
}
return hash % 1013; // 最后,我们将使用相加的和与另一质数(1013)相除后的余数
}

ES2015 Map 类

  • 和我们的 Dictionary 类不同,ES2015 的 Map 类的 values 方法和 keys 方法都返回 Iterator,而不是值或键构成的数组。
  • 另一个区别是,我们实现的 size 方法返回字典中存储的值的个数,而 ES2015 的 Map 类则有一个 size 属性。
const 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 map.delete('Gandalf');
console.log(map.has('Gandalf')); // false

ES2105 WeakMap 类和 WeakSet 类

  • 除了 Set 和 Map 这两种新的数据结构,ES2015还增加了它们的弱化版本,WeakSet 和 WeakMap。
  • WeakSet 或 WeakMap 类没有 entries、keys 和 values 等方法;
  • 只能用对象作为键。因此,除非你知道键,否则没有办法取出值。

使用 WeakMap 类

const map = new WeakMap();
const ob1 = { name: 'Gandalf' };
const ob2 = { name: 'John' };
const ob3 = { name: 'Tyrion' }; map.set(ob1, 'gandalf@email.com'); //// WeakMap 类也可以用 set方法,但不能使用数、字符串、布尔值等基本数据类型,需要将名字转换为对象
map.set(ob2, 'johnsnow@email.com');
map.set(ob3, 'tyrion@email.com'); console.log(map.has(ob1)); // true
console.log(map.get(ob3)); // tyrion@email.com map.delete(ob2); // 删除
console.log(map.has(ob2)); // false

【阅读笔记:散列表】Javascript任何对象都是一个散列表(hash表)!的更多相关文章

  1. Phaser中很多对象都有一个anchor属性

    游戏要用到的一些图片.声音等资源都需要提前加载,有时候如果资源很多,就有必要做一个资源加载进度的页面,提高用户等待的耐心.这里我们用一个state来实现它,命名为preload. 因为资源加载进度条需 ...

  2. Effective Java 读书笔记之二 对于所有对象都通用的方法

    尽管Object是一个具体的类,但设计它主要是为了扩展.它的所有非final方法都有明确的通用约定.任何一个类在override时,必须遵守这些通用约定. 一.覆盖equals时请遵守通用的约定 1. ...

  3. 【你不知道的javaScript 上卷 笔记7】javaScript中对象的[[Prototype]]机制

    [[Prototype]]机制 [[Prototype]]是对象内部的隐试属性,指向一个内部的链接,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototyp ...

  4. javascript设计模式与开发实践阅读笔记(6)——代理模式

    代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问. 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对 ...

  5. 《javascript设计模式与开发实践》阅读笔记(10)—— 组合模式

    组合模式:一些子对象组成一个父对象,子对象本身也可能是由一些孙对象组成. 有点类似树形结构的意思,这里举一个包含命令模式的例子 var list=function(){ //创建接口对象的函数 ret ...

  6. javascript设计模式与开发实践阅读笔记(9)——命令模式

    命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系. 说法很复 ...

  7. 《Thinking In Java》阅读笔记

    <Thinking In Java>阅读笔记 前四章:对象导论. 一切都是对象. 操作符. 控制执行流程 public在一个文件中只能有一个,可以是一个类class或者一个接口interf ...

  8. Redis学习笔记一:数据结构与对象

    1. String(SDS) Redis使用自定义的一种字符串结构SDS来作为字符串的表示. 127.0.0.1:6379> set name liushijie OK 在如上操作中,name( ...

  9. JavaScript判断对象的类型

    JavaScript判断对象的类型 最近阅读了一些关于JavaScript判断对象类型的文章.总结下来,主要有constructor属性.typeof操作符.instanceof操作符和Object. ...

随机推荐

  1. Oracle复习思路

    目录 Oracle复习 题型 复习大纲 附录 SQL题目一 SQL题目二 SQL题目三 SQL题目四 SQL题目五 SQL题目六 Oracle复习 题型 选择题15题 每题2分,共30分 判断题10题 ...

  2. Research Guide for Neural Architecture Search

    Research Guide for Neural Architecture Search 2019-09-19 09:29:04 This blog is from: https://heartbe ...

  3. JavaScript工具类(三):localStorage本地储存

    localStorage Web 存储 API 提供了 sessionStorage (会话存储) 和 localStorage(本地存储)两个存储对象来对网页的数据进行添加.删除.修改.查询操作. ...

  4. apicloud打包成apk

    前言:本文是打包vue项目,其他项目也是这样打包 页面的开发过程跟我们平时开发一样,利用vue把页面全部完成,最后进行npm run build将项目打包. 接下来就是apicloud打包的过程,首先 ...

  5. 转:laydate只显示时分,不显示秒

    @转载地址 原文全文: 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/weixin_40 ...

  6. Linux下 PostgrelSQL 基本操作

    一.在默认配置条件下,本机访问PostgreSQL 切换到Linux用户postgres,然后执行psql: $ su - postgres Last login: Wed Mar 1 13:16:4 ...

  7. [ Docker ] 基础概念

    目录- 什么是容器- 虚拟化和容器技术- docker 的基本概念 1. 什么是容器 容器英文:Container,容器是一种基础工具:泛指任何可以用于容纳其他物品的工具,可以部分或者完全封闭,被用于 ...

  8. [LeetCode] 211. Add and Search Word - Data structure design 添加和查找单词-数据结构设计

    Design a data structure that supports the following two operations: void addWord(word) bool search(w ...

  9. Kubernetes 控制器之 Service 讲解(七)

    一.背景介绍 我们这里准备三台机器,一台master,两台node,采用kubeadm的方式进行安装的,安装过程大家可以参照我之前的博文. IP 角色 版本 192.168.1.200 master ...

  10. IFC文件介绍

    IFC是一个数据交换标准, 用于不同系统交换和共享数据. IFC是采用EXPRESS语言定义的实体关系模型,由几百个实体对象组成.实体对象包括建筑要素如IfcWall,几何元素如IfcExtruded ...