数据结构之字典和散列表

学习github地址

今天来学习使用字典和散列表来存储唯一值(不重复的值)的数据结构

字典

在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的, 字典也称作映射、符号表或关联数组, 在计算机科学中, 字典经常用来保存对象的引用地址

  1. 创建字典类

与 Set 类相似,ECMAScript6 同样包含了一个 Map 类的实现,即我们所说的字典

今天要实现的类就是以 ES6 中 Map 类的实现为基础的。你会发现它和 Set 类很相似,但不同于存储[值,值]对的形式,我们将要存储的是[键,值]对

在字典中,理想的情况是用字符串作为键名,值可以是任何类型, 但是,由于 JavaScript 不是强类型的语言,我们不能保证键一定是字符串。我们需要把所有作为键名传入的对象转化为字符串

// 转化为字符串
const 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;
};
// 打一个骨架
class Dictionary {
  constructor(toStrFn = defaultToString) {
    this.toStrFn = toStrFn;
    this.table = {};
  }
}
// 不是只将 value 保存在字典中,而是要保存两个值:原始的key 和 value
class ValuePair {
  constructor(key, value) {
    this.key = key;
    this.value = value;
  }
  toString() {
    return `[#${this.key}: ${this.value}]`;
  }
}
  1. 声明一些字典所使用的方法

    • set(key,value):向字典中添加新元素。如果 key 已经存在,那么已存在的 value 会被新的值覆盖
    • remove(key):通过使用键值作为参数来从字典中移除键值对应的数据值
    • hasKey(key):如果某个键存在与字典中, 返回 true, 否则返回 false
    • get(key):通过键值为参数查找特定的值返回
    • clear():删除字典中的所有值
    • size():返回字典包含值的数量
    • isEmpty():在 size 等于 0 的时候为 true
    • keys():将字典所包含的所有键名以数组形式返回
    • values():将字典所包含的所有数值以数组形式返回
    • keyValues():将字典中所有[键,值]以数组形式返回
    • forEach(callbackFn):迭代字典所有的键值对, callbackFn有两个参数: key 和 value. 该方法可以在回调函数返回 false 时被中止
  2. 实现上面的方法

// 字典的实现
class Dictionary {
  constructor(toStrFn = defaultToString) {
    this.toStrFn = toStrFn;
    this.table = {};
  }

  // 已经存在一个给定键名的键值对(表中的位置不是 null 或 undefined),那么返回 true
  hasKey(key) {
    return this.table[this.toStrFn(key)] != null;
  }

  set(key, value) {
    if (key != null && value != null) {
      const tableKey = this.toStrFn(key); // 获取 key 的字符串
      this.table[tableKey] = new ValuePair(key, value); // 创建一个新的键值对并将其赋值给 table 对象上的 key属性(tableKey)
      return true;
    }
    return false;
  }

  // 检索一个值
  get(key) {
    // 首先检索存储在给定key 属性中的对象,
    const valuePair = this.table[this.toStrFn(key)];
    return valuePair == null ? undefined : valuePair.value;
  }

  // 这个也可以检索一个值: 获取两次key的字符串以及访问两次 table 对象
  get2(key) {
    if (this.hasKey(key)) {
      return this.table[this.toStrFn(key)].value;
    }
    return undefined;
  }

  remove(key) {
    if (this.hasKey(key)) {
      delete this.table[this.toStrFn(key)];
      return true;
    }
    return false;
  }

  keyValues() {
    return Object.values(this.table);
  }

  // 兼容版
  keyValues2() {
    const valuePairs = [];
    for (const key in this.table) {
      if (this.hasKey(key)) {
        valuePairs.push(this.table[key]);
      }
    }
    return valuePairs;
  }

  keys() {
    return this.keyValues().map((i) => i.key);
  }

  values() {
    return this.keyValues().map((i) => i.value);
  }

  // 迭代字典中的每个键值对
  forEach(callbackFn) {
    const valuePairs = this.keyValues();
    for (let i = 0; i < valuePairs.length; i++) {
      // 执行以参数形式传入 forEach 方法的 callbackFn 函数, 保存结果,
      const result = callbackFn(valuePairs[i].key, valuePairs[i].value);
      // 回调函数返回了 false,我们会中断 forEach 方法的执行
      if (result === false) {
        break;
      }
    }
  }

  size() {
    return Object.keys(this.table).length;
  }

  isEmpty() {
    return this.size() === 0;
  }

  clear() {
    this.table = {};
  }

  toString() {
    if (this.isEmpty()) {
      return "";
    }
    const valuePairs = this.keyValues();
    let objString = `${valuePairs[0].toString()}`;
    for (let i = 1; i < valuePairs.length; i++) {
      objString = `${objString},${valuePairs[i].toString()}`;
    }
    return objString;
  }
}

散列表

HashTable 类,也叫 HashMap 类,它是 Dictionary 类的一种散列表实现方式, 散列算法的作用是尽可能快地在数据结构中找到一个值, 散列函数的作用是给定一个键值,然后返回值在表中的地址

  1. 应用

    • 它是字典的一种实现,所以可以用作关联数组
    • 也可以用来对数据库进行索引, 创建一个索引来更快地查询到记录的 key
    • 使用散列表来表示对象, JavaScript 语言内部就是使用散列表来表示每个对象
  2. 创建散列表

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

// 打一个骨架, 和字典差不多
class HashTable {
  constructor(toStrFn = defaultToString) {
    this.toStrFn = toStrFn;
    this.table = {};
  }
}
  1. 给类添加方法

    • put(key, value):向散列表增加一个新的项(也能更新散列表)
    • remove(key):根据键值从散列表中移除值
    • get(key):返回根据键值检索到的特定的值
class HashTable {
  constructor(toStrFn = defaultToString) {
    this.toStrFn = toStrFn;
    this.table = {};
  }

  // 散列函数
  loseloseHashCode(key) {
    if (typeof key === "number") {
      return key;
    }
    const tableKey = this.toStrFn(key);

    let hash = 0; // 防止 key 是一个对象而不是字符串。我们需要一个 hash 变量来存储这个总和

    // charCodeAt() 方法可返回指定位置的字符的 Unicode 编码,返回值是 0 - 65535 之间的整数
    // 遍历 key 并将从 ASCII表中查到的每个字符对应的 ASCII 值加到 hash 变量中
    for (let i = 0; i < tableKey.length; i++) {
      hash += tableKey.charCodeAt(i);
    }

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

  hashCode(key) {
    return this.loseloseHashCode(key);
  }

  // 将键和值加入散列表
  put(key, value) {
    if (key != null && value != null) {
      //对于给出的 key 参数,我们需要用所创建的 hashCode 函数在表中找到一个位置
      const position = this.hashCode(key);
      // 为了信息备份将原始的 key 保存下来
      this.table[position] = new ValuePair(key, value);
    }
  }

  // 从散列表中获取一个值
  get(key) {
    const valuePair = this.table[this.hashCode(key)];
    return valuePair == null ? undefined : valuePair.value;
  }

  // 移除一个值
  remove(key) {
    const hash = this.hashCode(key);
    const valuePair = this.table[hash];
    if (valuePair != null) {
      delete this.table[hash];
      return true;
    }
    return false;
  }

  toString() {
    if (this.isEmpty()) {
      return "";
    }
    const keys = Object.keys(this.table);
    let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
    for (let i = 1; i < keys.length; i++) {
      objString = `${objString},{${keys[i]} => ${this.table[
        keys[i]
      ].toString()}}`;
    }
    return objString;
  }
}
// 使用 HashTable
const hash = new HashTable();
hash.put("Gandalf", "[email protected]");
hash.put("John", "[email protected]");
hash.put("Tyrion", "[email protected]");

console.log(hash.hashCode("Gandalf") + " - Gandalf"); // 19 - Gandalf
console.log(hash.hashCode("John") + " - John"); // 29 - John
console.log(hash.hashCode("Tyrion") + " - Tyrion"); // 16 - Tyrion
数据结构
  1. 散列表和散列集合

在一些编程语言中,还有一种叫作散列集合的实现。散列集合由一个集合构成,但是插入、移除或获取元素时,使用的是 hashCode 函数, 可以使用散列集合来存储所有的英语单词(不包括它们的定义)。和集合相似,散列集合只存储不重复的唯一值

  1. 处理散列表中的冲突

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

const hash = new HashTable();
hash.put("Ygritte", "[email protected]"); // 4
hash.put("Jonathan", "[email protected]"); // 5
hash.put("Jamie", "[email protected]"); // 5
hash.put("Jack", "[email protected]"); // 7
hash.put("Jasmine", "[email protected]"); // 8
hash.put("Jake", "[email protected]"); // 9
hash.put("Nathan", "[email protected]"); // 10
hash.put("Athelstan", "[email protected]"); //7
hash.put("Sue", "[email protected]"); // 5
hash.put("Aethelwulf", "[email protected]"); // 5
hash.put("Sargeras", "[email protected]"); // 10
  • 处理冲突有几种方法:分离链接、线性探查和双散列法 (了解为主, 后续补充)

ES6 Map 类

ECMAScript 2015 新增了 Map 类。可以基于 ES2015 的 Map 类开发我们的 Dictionary 类

ES6 WeakMap 和 WeakSet 类

  1. Map 和 Set 与其弱化版本之间仅有的区别是:
    • WeakSet 或 WeakMap 类没有 entries、keys 和 values 等方法
    • 只能用对象作为键

ES6 的 Set Map WeakMap WeakSet 数据结构 => 阮一峰文档

你可能感兴趣的:(数据结构之字典和散列表)