数据结构系列-字典和散列表(ES6中的map,WeakMap,WeakSet)

字典

在字典中,存储的是[键,值] 对,其中键名是用来查询特定元素的。

字典也称作映射、符号表或关联数组。

和集合的区别:字典和集合很相似,集合以[值,值]的形式存储元素,字 典则是以[键,值]的形式来存储元素。

应用场景

在计算机科学中,字典经常用来保存对象的引用地址。例如,打开 Chrome | 开发者工具中 的 Memory 标签页,执行快照功能,我们就能看到内存中的一些对象和它们对应的地址引用(用 @<数>表示)。

创建字典类

import { defaultToString } from '../util';
export default class Dictionary {
   constructor(toStrFn = defaultToString) {
       this.toStrFn = toStrFn; // {1}
       this.table = {}; // {2} 
   }
}

在字典中,理想的情况是用字符串作为键名,值可以是任何类型(从数、字符串等原始类型, 到复杂的对象)。但是,由于 JavaScript 不是强类型的语言,我们不能保证键一定是字符串。我们 需要把所有作为键名传入的对象转化为字符串,使得从 Dictionary 类中搜索和获取值更简单

(同样的逻辑也可以应用在上一章的 Set 类上)。

     //defaultToString 函数声明如下。
     export function defaultToString(item) {
       if (item === null) {
         return 'NULL';
       } else if (item === undefined) {
         return 'UNDEFINED';
       } else if (typeof item === 'string' || item instanceof String) {
         return `${item}`;
       }
       return item.toString(); // {1}
     }

说明:请注意,如果 item 变量是一个对象的话,它需要实现 toString 方法,否则会 导致出现异常的输出结果,如[object Object]。这对用户是不友好的。

常用的声明一些映射/字典所能使用的方法。

  •  set(key,value):向字典中添加新元素。如果 key 已经存在,那么已存在的 value 会 被新的值覆盖。

  •   remove(key):通过使用键值作为参数来从字典中移除键值对应的数据值。

  •   hasKey(key):如果某个键值存在于该字典中,返回 true,否则返回 false。

  •   get(key):通过以键值作为参数查找特定的数值并返回。

  •   clear():删除该字典中的所有值。

  •  size():返回字典所包含值的数量。与数组的 length 属性类似。

  •  isEmpty():在 size 等于零的时候返回 true,否则返回 false。

  •  keys():将字典所包含的所有键名以数组形式返回。

  •   values():将字典所包含的所有数值以数组形式返回。

  •  keyValues():将字典中所有[键,值]对返回。

  •   forEach(callbackFn):迭代字典中所有的键值对。callbackFn 有两个参数:key 和

    value。该方法可以在回调函数返回 false 时被中止(和 Array 类中的 every 方法相似)。

在字典和 ValuePair 类中设置键和值

存储的key是key,value存储的是一个ValuePair类

class ValuePair {
  constructor(key, value) {
    this.key = key; 
    this.value = value;
  }
  toString() {
    return `[#${this.key}: ${this.value}]`;
  } 
}

set(key, value) {
  if (key != null && value != null) {
    const tableKey = this.toStrFn(key); // {1}
    this.table[tableKey] = new ValuePair(key, value); // {2}
    return true;
  }
  return false;
}

其余方法这里不在一一实现

es6引入map方法

es6的map类存储的value是值,上述map数据结构的实现是存储的ValuePair类

和我们的 Dictionary 类不同,ES2015 的 Map 类的 values 方法和 keys 方法都返回 Iterator,而不是值或键构成的数组。

另一个区别是,我们实现的 size 方法 返回字典中存储的值的个数,而 ES2015 的 Map 类则有一个 size 属性。

使用 Dictionary 类

要使用 Dictionary 类,首先需要创建一个实例,然后给它添加三条电子邮件地址,使用这个 dictionary 实例来实现一个电子邮件地址簿。

    const dictionary = new Dictionary();
    dictionary.set('Gandalf', 'gandalf@email.com');
    dictionary.set('John', 'johnsnow@email.com');
    dictionary.set('Tyrion', 'tyrion@email.com')

散列表

这里介绍下HashTable 类,也叫 HashMap 类,它是 Dictionary 类的一种散列表 实现方式。

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

应用场景:

散列表有一些在计算机科学中应用的例子。因为它是字典的一种实现,所以可以用作关联数 组。

1,对应数据库索引

在关系型数据库(如 MySQL、Microsoft SQL Server、 Oracle,等等)中创建一个新的表时,一个不错的做法是同时创建一个索引来更快地查询到记录 的 key。在这种情况下,散列表可以用来保存键和对表中记录的引用。

2,用散列表来表示对象

JavaScript 语言内部就是使用散列表来表示每个对象。此时,对象的每个 属性和方法(成员)被存储为 key 对象类型,每个 key 指向对应的对象成员。

实例:

以使用的电子邮件地址簿为例。我们将使用最常见的散列函数——loselose 散列函数方法是简单地将每个键值中的每个字母的 ASCII 值相加,如下图所示。

创建散列表

我们将使用一个关联数组(对象)来表示我们的数据结构,和我们在 Dictionary 类中所做的一样。

搭建类的骨架

class HashTable {
      constructor(toStrFn = defaultToString) {
        this.toStrFn = toStrFn;
        this.table = {};
      }
}

三个常用基本方法:

 put(key,value):向散列表增加一个新的项(也能更新散列表)。

 remove(key):根据键值从散列表中移除值。
 get(key):返回根据键值检索到的特定的值。

创建散列函数

loseloseHashCode(key) {
      if (typeof key === 'number') { // {1}
        return key; 
      }
      const tableKey = this.toStrFn(key); // {2}
      let hash = 0; // {3}
      for (let i = 0; i < tableKey.length; i++) {
       //charCodeAt(i)返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。
        hash += tableKey.charCodeAt(i); // {4}
      }
      return hash % 37; // {5}
}
hashCode(key) {
    return this.loseloseHashCode(key);
}

loseloseHashCode 实现原理:

将key值转化为字符串,

再通过charCodeAt()方法转化为Unicode码,

再累加取余(为了得到比较小的数值,我们会使用 hash 值 和一个任意数做除法的余数(%)——这可以规避操作数超过数值变量最大表示范围的 风险。)。

put方法 

put 方法和 Dictionary 类中的 set 方法逻辑相似,不过大多数的编程语言会在 HashTable 数据结构中使用 put 方法,因此我们遵循相同的命名方式。

  put(key, value) {
    if (key != null && value != null) {
      const position = this.hashCode(key);
      if (this.table[position] == null) {
        this.table[position] = new ValuePair(key, value);
      } else {
        let index = position + 1;
        while (this.table[index] != null) {
          index++;
        }
        this.table[index] = new ValuePair(key, value);
      }
      return true;
    }
    return false;
  }

hash不存在直接添加,hash存在则+1,上述代码直接是线性探查的实现,如果不判断hash是否存在会出现覆盖

remove和get这里不再实现

散列表和散列集合

散列表和散列映射是一样的

散列集合由一个集合构成,但是插入、 移除或获取元素时,使用的是 hashCode 函数,另外的不同之处在于,不再添加键值对,而是只插入值而没有键。和集合相似,散列集合只存储不重复的唯一值。

散列冲突

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

处理冲突有几种方法:分离链接、线性探 查和双散列法。

分离链接:在新元素的位置已经被占据时,使用链表结构LinkedList

  put(key, value) {
    if (key != null && value != null) {
      const position = this.hashCode(key);
      if (this.table[position] == null) {
        this.table[position] = new LinkedList();
      }
      this.table[position].push(new ValuePair(key, value));
      return true;
    }
    return false;
  }

线性探查  hash不存在直接添加,hash存在则+1

  put(key, value) {
    if (key != null && value != null) {
      const position = this.hashCode(key);
      if (this.table[position] == null) {
        this.table[position] = new ValuePair(key, value);
      } else {
        let index = position + 1;
        while (this.table[index] != null) {
          index++;
        }
        this.table[index] = new ValuePair(key, value);
      }
      return true;
    }
    return false;
  }

更好的散列函数

一个表现良好的散列函数是由几个方面构成的:

1,插入和检索元素的时间(即性能),

2,较低的 冲突可能性。

   djb2HashCode(key) {
      const tableKey = this.toStrFn(key); // {1}
      let hash = 5381; // {2}
      for (let i = 0; i < tableKey.length; i++) { // {3}
        hash = (hash * 33) + tableKey.charCodeAt(i); // {4}
      }
      return hash % 1013; // {5}
    }

ES2105 WeakMap 类和 WeakSet 类

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

 WeakSet 或 WeakMap 类没有 entries、keys 和 values 等方法;

 只能用对象作为键。

为什么会有这WeakMap 类和 WeakSet 类?

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

另一个优点是,必须用键才可以取出值。这些类没有 entries、keys 和 values 等迭代器。因此,除非你知道键,否则没有办法取出值。所以可以使用 WeakMap类封装 ES2015 类的私有属性。

  const Person = (function() {
    const private = new WeakMap();
   
    function Person(name) {
      private.set(this, {});
      private.get(this).name = name;
    }
   
    Person.prototype.getName = function() {
        return private.get(this).name;
    };
   
    return Person;
  }());
   
  let person1 = new Person('小明');
  let person2 = new Person('大明');
  console.log(person1.getName()); // 小明

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值