散列表(哈希表) `HashTable`

散列表(哈希表) HashTable

散列表是字典的一种实现, ES6 对字典的实现是 Mapjs中没有字典这种数据结构, 其实普通的 js 对象也可以看成是一个字典。

散列表的特点存储和读取特别快, 时间复杂度约为 O(1)。一般数组、链表等顺序表查找一个元素需要遍历全部元素, 而散列表可以通过散列函数实现快速查找和快速插入。

散列函数有个缺点, 容易发生值的碰撞。而且理论上没有完美的散列函数, 样本足够大,总会发生碰撞。

解决散列函数碰撞的常见方法有分离链接、线性探查和双散列法等。详见下。

1. 实现简单的散列表

class HashTable {

  constructor() {
    this.data = []
  }

  /**
   * 散列函数, 针对字符串、数字类型的键
   * 对键中每个字符的 ASCII 码求和, 再对一个质数求余数
   * @param {String | Number} key 
   * @returns {Number}
   */
   static generateHashByAscii  (key) {
    const sumOfCharCode = [...key].reduce((prev, cur,index)=> prev + key.charCodeAt(index),0)
    const hash = sumOfCharCode % 41
    return hash
  }

  /**
   * 存储键值对到哈希表
   * @param {String | Number} key 
   * @param {any} value 
   */
  put(key, value) {
    const hash = HashTable.generateHashByAscii(key)
    console.log(hash)
    this.data[hash] = value
  }
  /**
   * 根据键读取值
   * @param {String | Number} key 
   * @returns {any}
   */
  get(key) {
    const hash = HashTable.generateHashByAscii(key)
    return this.data[hash]
  }

  remove(key) {
    const hash = HashTable.generateHashByAscii(key)
    this.data[hash] = undefined
  }
}

// 测试代码
const ht = new HashTable()
ht.put('snk',`43423423@qq.com`)
ht.put('loa',`2342344874@qq.com`)
ht.put('am',`2342344874@qq.com`)
console.log(ht);
ht.remove('snk')
console.log(ht)

2. 实现分离链接的散列表

分离链接的思路是,数组索引仍然由普通的哈希函数计算, 并且使用链表在对应的索引存储键值对, 这样即使哈希函数计算的结果冲突了, 仍然可以在链表中保存完整的键值对。 但是这样有个缺点, 就是样本数量大的时候, 链表中可能保存有很多个键值对,去查找元素的时候会变慢,因为在链表中查找元素还是要遍历的,这就有违设计散列表的初衷了。

由于 ES6 支持 Map 数据结构, 所以就直接用 Map 来存储键值对吧。ES6Map 类型是键值对的有序列表,而键和值都可以是任意类型。键的比较使用的是 Object.is().

其实直接用 Map 的实例去存储散列表的键值对也可以, 比链表还要快一点。

// 节点类
// 每个节点包含节点的值和节点下一个节点的引用(`next`属性)
// 刚初始化的节点默认指向 null
class Node {
  constructor(value) {
    this.value = value
    this.next = null
  }
}
/**
 * 链表类
 */
class LinkedList {
  constructor() {
    this.head = new Node('head')
    this.length = 0
  }

  // 向链表的尾部插入一个元素
  append(...values) {
    values.forEach(v => {
      const tailNode = this.findTailNode()
      tailNode.next = new Node(v)
      this.length++
    })
  }

  // 查找链表末尾元素
  findTailNode() {
    let tail = this.head
    while (tail.next) {
      tail = tail.next
    }
    return tail
  }

  // 在某个节点后插入一个节点
  // 在值为 value 的节点后边新添加一个元素 newValue
  insertAfter(newValue, value) {
    const newNode = new Node(newValue);
    const nodeBefore = this.findByValue(value)
    newNode.next = nodeBefore.next
    nodeBefore.next = newNode
    this.length++
  }

  // 在某个节点前插入一个节点
  // 在值为 value 的节点前边边新添加一个元素 newValue
  insertBefore(newValue, value) {
    const prevNode = this.findPreviosByValue(value)
    const currentNode = this.findByValue(value)
    const newNode = new Node(newValue)
    newNode.next = currentNode
    prevNode.next = newNode
    this.length++

  }

  // 通过值去查找节点, 
  // 查找到了, 返回元素
  // 找不到的话返回 null
  findByValue(value) {
    let currentNode = this.head
    while (currentNode && currentNode.value !== value) {
      currentNode = currentNode.next
    }
    return currentNode
  }
  // 通过值删除链表节点
  removeByValue(value) {
    const prevNode = this.findPreviosByValue(value)
    let currentNode = this.findByValue(value)
    if (!prevNode || !currentNode) return false;
    prevNode.next = currentNode.next
    currentNode = null
    this.length--
    return true
  }
  // // 通过值查找节点索引
  indexOf(value) {
    // 头结点不计入索引, 从头结点后第一个节点开始记录
    let currentNode = this.head.next,
      i = 0
    for (; i < this.length; i++) {
      if (currentNode.value === value) {
        return i
      } else {
        currentNode = currentNode.next
      }
    }
    return -1

  }
  // 是否有包含某个值的节点
  includes(value) {
    return this.indexOf(value) > -1
  }
  // 通过索引查找节点
  findByIndex(index) {
    const isOutBoundary = index < -1 || index >= this.length
    if (isOutBoundary) return null
    let currentNode = this.head.next
    for (let i = 0; i < this.length; i++) {
      if (index === i) {
        return currentNode
      } else {
        currentNode = currentNode.next
      }
    }
  }
  // 查找当前索引的上一个元素
  findPreviosByIndex(index) {
    const isOutBoundary = index <= 0 || index > this.length
    if (isOutBoundary) return null
    let currentNode = this.head.next
    for (let i = 0; i < this.length; i++) {
      if (index === i + 1) {
        return currentNode
      } else {
        currentNode = currentNode.next
      }
    }
    return null


  }
  // 通过索引删除元素
  removeByIndex(index) {
    const isOutBoundary = index < 0 || index > this.length - 1
    if (isOutBoundary) return null
    const prevNode = this.findPreviosByIndex(index)
    const currentNode = this.findByIndex(index)
    prevNode.next = currentNode.next
    this.length--
    return currentNode
  }

  // 根据自定义条件删除
  // 传入一个 fn(node) 返回 true 则删除
  // fn(node) 返回 false 不删除
  removeByCondition(fn) {
    let currentNode = this.head.next
    while (currentNode) {
      if (fn.call(null, currentNode)) {
        this.removeByValue(currentNode.value)
      }
      currentNode = currentNode.next
    }
  }
  // 查找当前节点的上一个节点
  // 删除当前节点的时候要将上个节点的 next 指向当前节点的 next 属性指向的节点
  findPreviosByValue(value) {
    let prevNode = this.head
    while (prevNode.next && prevNode.next.value !== value) {
      prevNode = prevNode.next
    }
    const isFound = prevNode.next && prevNode.next.value === value
    return isFound ? prevNode : null
  }

  // 通过索引更新节点值
  updateByIndex(index, newValue) {
    const currentNode = this.findByIndex(index)
    if (currentNode) {
      currentNode.value = newValue
      return true
    }
    return false
  }
  // 通过值更新节点值
  updateByValue(oldValue, newValue) {
    const currentNode = this.findByValue(oldValue)
    if (currentNode) {
      currentNode.value = newValue
      return true
    }
    return false
  }

  // 打印链表到控制台
  print() {
    console.log(JSON.stringify(this, null, 2))
  }
}

/**
 * @description 分离链接的散列表
 */
class Hashtable {
  constructor() {
    this.data = []
  }
  /**
   * 散列函数, 针对字符串、数字类型的键
   * 对键中每个字符的 ASCII 码求和, 再对一个质数求余数
   * @param {String | Number} key 
   * @returns {Number}
   */
   static generateHashByAscii(key) {
    const sumOfCharCode = [...key].reduce((prev, cur, index) => prev + key.charCodeAt(index), 0)
    const hash = sumOfCharCode % 41
    return hash
  }

  /**
   * @param {String} key 
   * @returns {any}
   */
  get(key) {
    const hash = Hashtable.generateHashByAscii(key)
    const list = this.data[hash]
    if(!list || !list.length) return undefined
    let curNode = list.head.next
    while(curNode) {
      if(curNode.value.has(key)) {
        return curNode.value.get(key)
      }
      curNode = curNode.next
    }
    return undefined
  }

  /**
   * 向散列表中增加键值对
   * @param {String} key 
   * @param {any} value 
   */
  put(key, value) {
    const hash = Hashtable.generateHashByAscii(key)
    const pare = new Map([[key, value]])
    if(!this.data[hash]) {
      this.data[hash] = new LinkedList()
    }
    this.data[hash].append(pare)
  }

  /**
   * 删除散列表中的键值对
   * @param {String} key 散列表的键
   * @returns {Boolean}
   */
  remove(key) {
    const hash = Hashtable.generateHashByAscii(key)
    if(!this.data[hash]) return false
    let list = this.data[hash]
    let curNode = list.head.next
    while(curNode) {
      if(curNode.value.has(key)) {
        if(list.length === 1) {
          this.data[hash] = undefined
        }else {
          list.removeByCondition(node=> node.value.has(key))
        }
        return true
      }
      curNode = curNode.next
    }
    return false
  }
}

const h = new Hashtable()
h.put("name", "leorick")
h.put("mbnd", "loa")

h.remove("name")
console.log(h.get('name'))
console.log(h.get('mbnd'))
console.log(h)

3. 社区推荐的散列函数:

在原来简单哈希表的基础上做了一点点改动, 增加了 djb2 哈希算法;并修改了构造函数,可以通过给构造函数传参来选择哈希码的计算函数, 默认还是使用 ASCII 进行计算。

class HashTable {
  constructor(type = `ascii`) {
    this.data = []
    this.type = type
    console.log(HashTable.types)
  }
  
  /**
   * 哈希码计算函数映射表
  */
  static types = {
    ascii : HashTable.generateHashByAscii,
    djb2 : HashTable.generateHashByDJB2
  }
  /**
   * 散列函数, 针对字符串、数字类型的键
   * 对键中每个字符的 ASCII 码求和, 再对一个质数求余数
   * @param {String | Number} key 
   * @returns {Number}
   */
  static generateHashByAscii(key) {
    const sumOfCharCode = [...key].reduce((prev, cur, index) => prev + key.charCodeAt(index), 0)
    const hash = sumOfCharCode % 41
    return hash
  }

  /**
   * 使用 djb2 算法生成哈希码
   * @param {String} key 
   * @param {Number} value 
   */
   static generateHashByDJB2(key) {
    var hash = 5381;
    for (var i = 0; i < key.length; i++) {
      hash = hash * 33 + key.charCodeAt(i)
    }
    return hash % 1013;
  }

  /**
   * 存储键值对到哈希表
   * @param {String | Number} key 
   * @param {any} value 
   */
  put(key, value) {
    const hash = HashTable.types[this.type](key)
    this.data[hash] = value
  }
  /**
   * 根据键读取值
   * @param {String | Number} key 
   * @returns {any}
   */
  get(key) {
    const hash = HashTable.types[this.type](key)
    return this.data[hash]
  }
  /**
   * 删除, 不能直接将这一项删除, 应设置为 undefined
   * @param {String | Number} key 
   */
  remove(key) {
    const hash = HashTable.types[this.type](key)
    this.data[hash] = undefined
  }
}

const ht = new HashTable('djb2')
ht.put('snk', `43423423@qq.com`)
ht.put('loa', `2342344874@qq.com`)
ht.put('am', `2342344874@qq.com`)
console.log(ht);
ht.remove('snk')
console.log(ht)

4. 线性探查实现散列表

实现思路就是在在操作数组时, 先检测当前索引有没有没占用, 若被占用则继续查询后边的索引, 否则就是用当前索引。

代码就先不写了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值