散列表(哈希表) HashTable
散列表是字典的一种实现, ES6
对字典的实现是 Map
,js
中没有字典这种数据结构, 其实普通的 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
来存储键值对吧。ES6
的 Map
类型是键值对的有序列表,而键和值都可以是任意类型。键的比较使用的是 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. 线性探查实现散列表
实现思路就是在在操作数组时, 先检测当前索引有没有没占用, 若被占用则继续查询后边的索引, 否则就是用当前索引。
代码就先不写了。