JavaScript数据结构——字典和散列表

78 阅读5分钟

前言

嘿,掘友们!今天我们来了解并实现数据结构 —— 字典和散列表。

字典

字典和集合相似,集合以 [值,值]的形式存储元素,字典则是以 [键、值]的形式来存储元素。字典也称作映射、符号表或关联数组。

在字典中,理想的情况是用字符串作为键名,值可以是任何类型。但是,我们不能保证键一定是字符串。所以,需要一个方法把左右作为键名传入的对象转化为字符串

function defaultToString(item) {
  if (item === null) return 'NULL'
  if (item === undefined) return 'UNDEFINED'
  if (typeof item === 'string' || item instanceof String) return item
  return item.toString()
}

我们来创建字典类的骨架

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

为字典类声明一些方法

  • set(key, value) 添加新元素,如果存在,会被覆盖
  • remove(key) 移除元素
  • get(key) 判断字典内是否存在某个键值
  • clear() 清空字典
  • size() 字典所含值的数量
  • isEmpty() 判断字典是否为空
  • keys() 字典的所有键名以数组形式返回
  • keyValues() 字典的所有数值以数组形式返回
  • forEach(callbackFn) 迭代字典中所有的键值对

检查一个键是否存在于字典中

hasKey(key) {
  return this.table[this.toStrFn(key)] != null
}

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

在字典中,我们将key转化为字符串,为了保存信息的需要,保存原始的 key,创建一个 ValuePair类,来存储 原始的key 和 value

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

然后我们实现 set(key, value)

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

从字典中移除一个值

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

从字典中检索一个值

get(key) {
  const valuePair = this.table[this.toStrFn(key)]
  return valuePair == null ? undefined : valuePair.value
}

首先检索存储在给定 key 属性中的对象。如果valuePair 对象存在,将返回该值,否则返回 undefined

还有另一种实现方法,先判断 key 是否存在,再找到它并返回。

get(key) {
  if(this.hasKey(key)) {
    return this.table[this.toStrFn(key)].value
  }
  return undefined
}

但是,第二种方式,要访问两次 key 的字符串以及访问两次 table 对象。相比来说,第一次方式的消耗更少。

keys、values、keyValues方法

keyValues()方法会以数组的形式返回所有的数值。

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

代码很简单,但是并不是所有浏览器都支持 Object.values 方法。

keyValues() {
  const valuePairs = []
  for (const k in this.table) {
    if(this.hasKey(k)) valuePairs.push(this.table[k])
  }
  return valuePairs
}

我们迭代了 table 对象的所有属性。为了保证 key 是存在的,使用 hasKey 函数来进行检验,然后将 table 对象中的值加入数组中。

然后创建 keys 方法,该方法会以数组的形式返回所有键

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

我们也可以利用 数组内置的map函数创建一个value函数,返回一个包含所有值的数组

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

用 forEach 迭代字典中的每个键值对

forEach(callbackFn) {
  const valuePair = this.keyValues()
  for (let i = 0; i < valuePair.length; i++) {
    const result = callbackFn(valuePair[i].key, valuePair[i].value)
    if(result === false) break
  }
}

我们获取字典中所有 valuePair 构成的数组。然后,迭代每个valuePair 并执行以参数的形式传入 callbackFn 函数,保存它的结果。如果回调函数返回 false, 就中断 forEach 方法的执行,打断 for 循环。

clear、size、isEmpty 和 print 方法

size() {
  return Object.keys(this.table).length
}
isEmpty() {
  return this.size() === 0
}
clear() {
  this.table = {}
}
print() {
  if(this.isEmpty()) return ''
  const valuePair = this.keyValues()
  let string = `${valuePair[0].toString()}`
  for (let i = 1; i < valuePair.length; i++) {
    string = `${string}, ${valuePair[i].toString()}`
  }
  return string
}

散列表

散列表是字典类的一种实现方式

散列算法的作用是尽可能快地在数据结构中找到一个值。在前面实现字典类中,我们要在数据结构中获取一个值(get方法),需要迭代整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址。

创建散列表

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

定义三个基本方法

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

创建散列函数

loseHashCode(key) {
  if(typeof key === 'number') return key
  const tableKey = this.toStrFn(key)
  let hash = 0
  for (let i = 0; i < tableKey.length; i++) {
    hash += tableKey.charCodeAt(i)
  }
  return hash % 37
}

hashCode(key) {
  this.loseHashCode(key)
}

hashCode 只是简单的调用了 loseHashCode 方法,将key作为参数传入。

loseHashCode 方法中,首先检验 key 是否是一个数。如果是,直接返回。

然后,给定一个 key 参数,根据组成 key 的每个字符的 ASCII码 值的和得到一个数。在这之前,我们要将 key 转换为一个字符串,防止 key 是一个对象而不是一个字符串。

我们用 hash 来存储这个总和,然后,遍历 key 并将从 ASCII 码表中查出对应的值,使用 String类中的 charCodeAt 方法,可以查出。

最后返回 hash 值。为了得到比较小的数,我们可以使用 取余 的方式,来避免 hash 值超过数值变量最大表示范围的风险。

将键和值加入散列表

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

首先检验 key 和 value 是否合法,不合法则返回 false。

通过 hashCode 函数找出 key 在表中的位置,然后用 key 和 value 创建一个 ValuePair 实例作为值存储。

从散列表中获取一个值

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
}

处理散列表的冲突

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

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