8-数据结构-字典和散列表(又是一个乍一看贼烧脑,但仔细一看貌似好像也不难的数据结构,也是写了贼多注释)

475 阅读10分钟

字典的基本概念

在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射、符号表或关联数组。 在JSmap就是典型的字典结构

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

image.png

字典的基本功能

我们需要声明一些映射/字典所能使用的方法。

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 方法相似)。

以下是初级版本字典的实现代码:

class ValuePair{ // 用于存储原始值,如果没有原始值,那么字典的内存空间中存储的就只有转换改造后的值,就不知道原始值是什么
    constructor(key,value) { // 单个的字典值的节点
        this.key = key // 模拟内存地址
        this.value = value // 模拟内存中的值
    }
    
    toString(){ // 返回键值对形式的组合值
        return `[${this.key}:${this.value}]` // 单个值节点转成字符串
    }
}

class Dictionary{
    constructor(){
        this.table = {} // 这个对象就是模拟存储数据的内存空间,key就是内存地址,value就是里面的值
    }
    
    toStringFN(item){ // 使用toStringFN方法模拟在内存中开辟生成一个地址,因为js无法直接操作内存,所以只能模拟,形参item用于接收传入的key
        if(item === null){ // 如果传入的key是null
            return ""
        }else if(item === undefined){ // 如果传入的是undefined
            return "Undefined"
        }else if(typeof item === "string" || item instanceof String){ // 如果传入的是与字符串有关的
            return `${item}`
        }
        // 如果以上情况都不是,直接调用原生的toString方法;这里会有个问题,如果是对象类型,后面的对象会覆盖先前的对象(等下升级版字典可以解决这个问题)
        return item.toString()
    }
    
    hasKey(key){ // 判断是否已经在对象中存储了这个值
        return this.table[this.toStringFN(key)] !== undefined
        // 因为我们访问的是 已经存在的对象 的key,访问对象的属性,如果返回的不是undefined,则表示存在,返回undefined则表示不存在
    }
    
    set(key,value){ // 往字典里面存储值
        // 先判断是否存在
        if(key != null && value != null){ // 必须不为null
            let tablekey = this.toStringFN(key) // 得到一个key
            this.table[tablekey] = new ValuePair(key,value) // 存储值,实例化ValuePair生成原始值的对象,并将其存储到字典的内存空间即table对象中
        }
        return false // 如果为空即不存在的时候,返回false
    }
    
    remove(key){ // 删除字典里的某个key所指向的值
        // 删除一个值前提是得有这个值
        if(this.hasKey(key)){
            delete this.table[this.toStringFN(key)] // 如果有这个值则删除
        }
        return false // 没有则返回false
    }
    
    get(key){ // 根据传入的key获取对应的值
        return this.table[this.toStringFN(key)]
    }
    
    keyValues(){ // 获取所有名值对
        let ValuePairs = []
        for(let k in this.table){ // 遍历
            if(this.hasKey(key)){
                ValuePairs.push(this.table[k]) // 添加
            }
        }
        return ValuePairs // 返回
    }
    
    keys(){ // 获取所有名值对的key名
        return this.keyValues().map(item => item.key)
    }
    
    values(){ // 获取所有名值对的value值
        return this.keyValues().map(item => item.value)
    }
    
    forEach(cb){ // 遍历,接收一个回调函数
        let valuePairs = this.keyValues() // 获得名值对
        for(let index = 0; index < valuePairs.length; index++){
            cb(index,valuePairs[index].value,valuePairs) // index item arr 这里我改了第二个valuePairs[index].value(万老手误把里面的下标index写成了i)
        }
    }
    
    size(){ // 获取长度
        return this.keyValues().length // 返回所有名值对的数量
    }
    
    clear(){ // 清空
        this.table = {}
    }
    
    isEmpty(){ // 是否为空
        return this.size() === 0 // 如果长度为0则表示是空
    }
    
    toString(){
        if(this.isEmpty()){ // 如果为空的情况
            return ""
        }
        let valuePairs = this.keyValues()
        let objStr = ""
        for(let i = 0; i < valuePairs.length; i++){
            objStr = `${objStr},${valuePairs[i].toString()}` 
        }
        return objStr.slice(1) // 去掉前面的逗号
    }
}

let d = new Dictionary()
d.set({x:1},"x:1,第1个") // 对象作为key,后面的字符串就是对应的value
d.set({x:1},"x:1,第2个") // 这样子第二个会把第一个覆盖掉
d.get({x:1}) // 就只能获取第二个

以下是升级版的字典结构的实现(加入对象不会被覆盖):

class ValuePair{ // 用于存储原始值,如果没有原始值,那么字典的内存空间中存储的就只有转换改造后的值,就不知道原始值是什么
    constructor(key,value) { // 单个的字典值的节点
        this.key = key // 模拟内存地址
        this.value = value // 模拟内存中的值
    }
    toString(){ // 返回键值对形式的组合值
        return `[${this.key}:${this.value}]` // 单个值节点转成字符串
    }
}

class Dictionary{
    constructor(){
        this.table = {} // 这个对象就是存储数据的内存空间,key就是内存地址,value就是里面的值
        this.id = 1 // 设置一个唯一识别码id,这样如果是对象就不会被认为是同一个了
    }
    
    toStringFN(item){ // 使用toStringFN方法模拟在内存中开辟生成一个地址,因为js无法直接操作内存,所以只能模拟,形参item用于接收传入的key
        if(item === null){ // 如果传入的key是null
            return ""
        }else if(item === undefined){ // 如果传入的是undefined
            return "Undefined"
        }else if(typeof item === "string" || item instanceof String){ // 如果传入的是与字符串有关的
            return `${item}`
        }
        // 当然这个方法不算完美,因为有些对象名可能天生带有_id,所以可以生成一个非常复杂的独一无二的id识别码来避免这种情况
        if(item["_id"]){ // 判断是否是被我们改造过的重名对象,这行代码替换掉了下面的set中注释掉的更新tablekey
            return item.toString() + item["_id"]
        }
        // 如果以上情况都不是
        return item.toString()
    }
    
    hasKey(key){ // 判断是否已经在对象中存储了这个值
        return this.table[this.toStringFN(key)] !== undefined
        // 因为我们访问的是 已经存在的对象 的key,访问对象的属性,如果返回的不是undefined,则表示存在,返回undefined则表示不存在
    }

    // 如果要能加入重名的对象,就需要再添加重名的对象时增加一个唯一的识别码id
    set(key,value){ // 往字典里面存储值
        // 先判断是否存在
        if(key != null && value != null){
            let tablekey = this.toStringFN(key) // 得到一个key
            if(this.hasKey(key)){ // 如果存在重名对象
                if(this.table[tablekey].key !== key){ // 再次判断重名的对象里的key所指向的值是否全等,不是同一个对象则添加唯一id识别码
                    key["_id"] = this.id
                    this.id++ // 这个重名对象的唯一id识别码已经被用了,自增1准备给下一个重名对象使用
                    
                    // tablekey = this.toStringFN(key) // 此时更新这个带有唯一识别码的新的key,这一步可以在toStringFN的最后一个if中完成
                    
                    this.set(key,value) // 递归,再次进入set判断现在是否重名
                }else{
                    this.table[tablekey] = new ValuePair(key,value) // 如果是相同对象则直接覆盖
                }
            }else{ // 如果没有重名
                this.table[tablekey] = new ValuePair(key,value) // 如果没有重名直接存储值
            }
        }
    }
    
    remove(key){ // 删除字典里的某个key所指向的值
        // 删除一个值前提是得有这个值
        if(this.hasKey(key)){
            delete this.table[this.toStringFN(key)] // 如果有这个值则删除
        }
        return false // 没有则返回false
    }
    
    get(key){ // 根据传入的key获取对应的值
        return this.table[this.toStringFN(key)]
    }
    
    keyValues(){ // 获取所有名值对
        let ValuePairs = []
        for(let k in this.table){
            if(this.hasKey(key)){
                ValuePairs.push(this.table[k])
            }
        }
        return ValuePairs
    }
    
    keys(){ // 获取所有名值对的key名
        return this.keyValues().map(item => item.key)
    }
    
    values(){ // 获取所有名值对的value值
        return this.keyValues().map(item => item.value)
    }
    
    forEach(cb){ // 遍历,接收一个回调函数
        let valuePairs = this.keyValues()
        for(let index = 0; index < valuePairs.length; index++){
            cb(index,valuePairs[index].value,valuePairs) // index item arr
        }
    }
    
    size(){ // 获取长度
        return this.keyValues().length
    }
    
    clear(){ // 清空
        this.table = {}
    }
    
    isEmpty(){ // 是否为空
        return this.size() === 0 // 如果长度为0则表示是空
    }
    
    toString(){
        if(this.isEmpty()){
            return ""
        }
        let valuePairs = this.keyValues()
        let objStr = ""
        for(let i = 0; i < valuePairs.length; i++){
            objStr = `${objStr},${valuePairs[i].toString()}` 
        }
        return objStr.slice(1) // 去掉前面的逗号
    }
}

let o = {x:1}
let d = new Dictionary()
d.set(o,"x:1,第1个")
d.set(o,"x:1,第1.1个") // 这样子这一个会把上一个覆盖掉
d.set({x:1},'x:1,第2个') // 而这是个新对象,虽然长的一样,但是地址不同不会覆盖

// 一般往里面添加对象不是用下面这种字面量添加的方式,而是像上面用变量的方式,下面这两种只是来做个例子说明重名情况会怎样
d.set({x:1},"x:1,第3个") // 不是同一个对象
d.set({x:1},"x:1,第3个")
d.get({x:1})

console.log(d)

散列表结构的基本概念

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

image.png

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

最常见的散列函数——lose lose散列函数,方法是简单地将每个键值中的每个字母的ASCII 值相加

实现一个散列函数

// 这是从字典方法直接copy来的当做一个函数
function toStringFN(item){ // 形参item接收传入的key
    if(item === null){ // 如果传入的key是null
        return ""
    }else if(item === undefined){ // 如果传入的是undefined
        return "Undefined"
    }else if(typeof item === "string" || item instanceof String){ // 如果传入的是与字符串有关的
        return `${item}`
    }
    // 当然这个方法不算完美,因为有些对象名可能天生带有_id,所以可以生成一个非常复杂的独一无二的id识别码来避免这种情况
    if(item["_id"]){ // 判断是否是被我们改造过的重名对象,这行代码替换掉了下面的set中注释掉的更新tablekey
        return item.toString() + item["_id"]
    }
    // 如果以上情况都不是
    return item.toString()
}

// 哈希函数 用于将一个复杂的key转换成string
function loseloseHASHCode(key){ // 将key转换成string,但是这样也依旧会算出相同的数,所以还需要再借助分离链接法
    if(typeof key === "number"){ // 如果是number就没必要转换了,直接做下标
        return key
    }
    let tablekey = toStringFN(key) // 先转换成string格式
    let hashCode = 0
    // hash算法非常多,这里用一种遍历其中每一个字符,然后累加每一个字符的字符编码charCodeAt
    for(let i = 0; i < tablekey.length; i++){
        hashCode += tablekey.charCodeAt(i)
    }
    return hashCode % 37 // 质数求余,除以任意一个质数取余,将取得的余数作为key,
    // 密码学中非常难生成所有质数,因为质数无法用通项公式生成,所以用质数比较难被破解
}

散列表结构的自我实现

loseloseHashCode 方法中,我们首先检验key 是否是一个数。如果是,我们直接将其返回。

然后,给定一个key 参数,我们就能根据组成key 的每个字符的ASCII 码值的和得到一个数。所以,首先需要将key 转换为一个字符串,防止key 是一个对象而不是字符串。

我们需要一个hash 变量来存储这个总和。 然后,遍历key 并将从ASCII表中查到的每个字符对应的ASCII 值加到hash 变量中,可以使用JavaScript String类中的charCodeAt 方法。

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

class ValuePair{ // 存储原始值
    constructor(key,value) { // 单个的字典值的节点
        this.key = key
        this.value = value
    }
    toString(){
        return `[${this.key}:${this.value}]` // 单个值节点转成字符串
    }
}

class HashTable{
    constructor(){
        this.table = {} // 创建一个用于存储的对象
    }

    toStringFN(item){ // 形参item接收传入的key
        if(item === null){ // 如果传入的key是null
            return ""
        }else if(item === undefined){ // 如果传入的是undefined
            return "Undefined"
        }else if(typeof item === "string" || item instanceof String){ // 如果传入的是与字符串有关的
            return `${item}`
        }
        // 当然这个方法不算完美,因为有些对象名可能天生带有_id,所以可以生成一个非常复杂的独一无二的id识别码来避免这种情况
        if(item["_id"]){ // 判断是否是被我们改造过的重名对象,这行代码替换掉了下面的set中注释掉的更新tablekey
            return item.toString() + item["_id"]
        }
        // 如果以上情况都不是
        return item.toString()
    }

    loseloseHASHCode(key){ // 将key转换成string,但是这样也依旧会算出相同的数,所以还需要再借助分离链接法
        if(typeof key === "number"){ // 如果是number就没必要转换了,直接做下标
            return key
        }
        let tablekey = toStringFN(key)// 先转换成string格式
        let hashCode = 0
        // hash算法非常多,这里用一种遍历其中每一个字符,然后累加每一个字符的字符编码charCodeAt
        for(let i = 0; i < tablekey.length; i++){
            hashCode += tablekey.charCodeAt(i)
        }
        return hashCode % 37 // 质数求余,除以任意一个质数取余,将取得的余数作为key,
        // 密码学中非常难生成所有质数,因为质数无法用通项公式生成,所以用质数比较难被破解
    }

    add(key,value){ // 散列表增加值
        if(key != null && key != undefined){
            let position = this.loseloseHASHCode(key) // 将哈希函数生成的索引赋值给下标position
            this.table[position] = new ValuePair(key,value)
            return true // 添加成功
        }
        return false // 添加失败
    }

    get(key){ // 通过索引key获取对应值
        let position = this.loseloseHASHCode(key)
        return this.table[position]
        // return this.table[this.loseloseHASHCode(key)] 上面两行代码可以合成这一行
    }

    remove(key){ // 通过索引key删除对应值
        let position = this.loseloseHASHCode(key)
        return delete this.table[position]
    }
}

分离链接法实现的散列表

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

例如:

heloo经过哈希函数loseloseHASHCode转化之后的对应值为17

hel经过哈希函数loseloseHASHCode转化之后的对应值也为17

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

(万老说分离链接法最优美,就用该方法,如下图所示)

image.png

分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面,所以需要用到之前实现过的链表结构,直接copy一份过来。

// 分离链接法需要用到之前实现过的单向链表结构,直接copy过来
// 单向链表
class Node{
    // 每一个节点的结构
    constructor(value) {
        this.value = value // 数据域
        this.next = undefined // 指针域
    }
}

class LinkedList{
    constructor() {
        this.count = 0 // 计数器
        this.head = undefined // 链头,起始节点
    }

    push(value){ // 在尾部新增一个值
        let node = new Node(value) // 实例生成节点对象
        let current // 当前遍历所处的节点
        if(!this.head){
            this.head = node
            // 如果说当前链表里是空的,就说明head也是空的,那就直接把新增的节点设置为链表的第一个
        }else{ // 如果链表第一个不为空,那我们就往后面数
            current = this.head // 链表头为当前已经遍历的节点(如果遇到不好理解的地方,画图)
            while(current.next){ // 如果当前节点的指针域所指向的地方不为空的时候,说明它还不是最后一个节点,则循环的条件依旧为true,就还能继续循环遍历,如果条件为false,就不会进入该循环
                current = current.next // 只要不是最后一个节点,就一直把当前所遍历到的那个节点的指向,指向下一个
            }
            current.next = node // 如果遍历到了最后一个节点,跳出了循环,那么就把当前的指针域指向这被遍历到的最后一个节点
        }
        this.count++ // 每当在尾部添加完一个新值之后,计数器加一
    }

    // 怎么才算删除,当没有指针指向该节点,它就被删除了,所以我们只需要找到被删除节点i的前一个节点i-1,将前一个节点i-1的指针 指向 被删除节点 之后的一个节点i+1 即可:就是(i-1)=> (i+1),i就不见了
    removeAt(index){ // 删除链表上指定下标的节点
        if(index >= 0 && index < this.count){ // 如果要删除一个指定的数据,必须满足下标大于等于0且小于链表长度这两个条件
            let current = this.head // 默认将链头作为当前所处节点位置
            let beRemoved // 被删除的
            if(index = 0){ // 如果删除的是第0个,链头
                beRemoved = current // 由上可知current是this.head,则需要被删除的就是this.head
                this.head = current.next // 链头指向下一个数据,那么原来的链头数据就被删除了
            }else{ // 下面这一步for循环非常难理解,可以试着画图并代入具体的数值0 1 2 3 4来理解
                for(let i = 1; i < index; i++){ // 为啥从1开始,因为刚刚已经排除了第0位
                    current = current.next // 当循环结束时,就遍历到了要被删除节点的前一个节点,index是要被删除的节点的下标,因为i<index,所以i此时的值就是index-1
                }
                beRemoved = current.next // 由上面for循环可知,current是代表待删除节点的前一个节点,则current.next就是代表需要被删除的节点
                current.next = current.next.next // 当前节点的前一个节点的指针域 指向 当前节点的下一个节点,那么当前节点就被删除了
            }
            this.count-- // 删除之后,相应的计数器减1
            return beRemoved // 返回被删掉的节点的值
        }
    }

    removeValue(value){ // 删除链表上指定值的节点
        let current = this.head
        let pre = null // 被删除节点的前一个节点
        // 分情况,当链表头就是要删除的值时或者链表其他节点是要被删除的值的时候
        if(current.value === value){ // 如果输入的值与链头的值相等
            this.head = current.next // 就把链表头指向原来链表头的下一个节点
        }else{ // 否则
            for(let i = 1; i < this.count; i++){ // 循环遍历每一个节点,当前节点的值不是要删除的就一直寻找下一个节点的值
                pre = current // 将i-1保存到变量pre(当前的current是this.head,当前节点成为上一个节点)
                current = current.next // 需要被删除的节点i(当前节点的下一个节点成为当前节点)
                if(current.value === value){ // 当需要被删除值与当前节点的值相同时
                    pre.next = current.next // 前一个节点(i-1)的指针域 指向 需要被删除的节点(i)的指针域所指向的节点(i+1)
                    break // break删除符合条件的第一个值,如果没写break就是继续循环,会删除符合条件的所有值
                }
            }
        }
        this.count-- // 计数器减1
        return `已经删除符合条件的第一个值${value}`
    }

    pop(){ // 模拟pop功能,删除最后一个
        return this.removeAt(this.count - 1)
    }

    indexOf(value){ // 查询一个值在链表里面的位置,返回该值的位置下标
        let current = this.head
        if(value === current.value){ // 三个等号全等,数据类型也要相同
            return 0
        }

        // 如果不是第0位,那么就进入下面这个循环
        for(let i = 1; i < this.count; i++){
            current = current.next // 不是第0位,那么就一直循环往后挪
            if(value === current.value){
                return i
            }
        }
        return undefined // 如果既不是if里面的,也不是for循环里面的,那就说明找不到,找不到就返回undefined
    }

    getNodeAt(index){ // 获取指定下标位置上的值
        if(index >=0 && index < this.count){
            let current = this.head
            if(index === 0){
                return current
            }else{
                for(let i = 1; i < index; i++){
                    current = current.next // 当循环结束时,i是被指定下标节点的前一个节点的下标
                }
                return current.next
            }
        }
    }

    // 假如要在第i个下标插入一个新的节点,那么就是i-1指向新的节点,新的节点指向i,这样新的节点就插入了
    insert(value,index){ // 指定下标位置插入新节点
        let node = new Node(value) // 实例化生成节点
        if(index >= 0 && index <= this.count){ // 输入的索引值在链表长度内
            let current = this.head // 先把原来开头的值保存下来
            if(index === 0){ // 插入开头
                this.head = node
                this.head.next = current // 把新的开头值的指针域 指向 原先开头的值
            }else{
                for(let i = 1; i < index; i++){
                    current = current.next // 当循环结束时,插入新节点的位置 的 前一个节点 等号左边的current变量是i-1
                }
                let nextEle = current.next // nextEle保存原来的i
                current.next = node // i-1的指针域指向新插入的节点
                node.next = nextEle // 新插入的节点的指针域指向原来的i
                this.count++
            }
        }else{ // 输入的索引值超出链表原本的长度
            throw new Error("索引值错误")
        }
    }
    
    unshift(value){ // 在链表头部插入新节点
        this.insert(value, 0)
    }

    isEmpty(){
        return !this.count
    }

    size(){
        return this.count
    }

    getHead(){ // 获取当前链表头的值
        return this.head.value
    }

    toString(){ // 转换成字符串形式输出
        let objString = ""
        let current = this.head
        if(!current){ // 如果链表头不存在
            return "" // 返回空
        }else{
            do{ // 至少会先执行一次do里面的循环语句,然后再判断while的条件是否还满足循环
                objString = `${objString},${current.value}`
                current = current.next
            }while(current) // 只要还有当前值,就一直进行do里面的循环
            return objString.slice(1) // 去掉第一次拼接后的逗号
        }
    }
}

// 以下就是刚刚的散列表

class ValuePair{ // 存储原始值
    constructor(key,value) { // 单个的字典值的节点
        this.key = key
        this.value = value
    }
    toString(){
        return `[${this.key}:${this.value}]` // 单个值节点转成字符串
    }
}

class HashTable{
    constructor(){
        this.table = {}
    }

    toStringFN(item){ // 形参item用于接收传入的key
        if(item === null){ // 如果传入的key是null
            return ""
        }else if(item === undefined){ // 如果传入的是undefined
            return "Undefined"
        }else if(typeof item === "string" || item instanceof String){ // 如果传入的是与字符串有关的
            return `${item}`
        }
        // 当然这个方法不算完美,因为有些对象名可能天生带有_id,所以可以生成一个非常复杂的独一无二的id识别码来避免这种情况
        if(item["_id"]){ // 判断是否是被我们改造过的重名对象,这行代码替换掉了下面的set中注释掉的更新tablekey
            return item.toString() + item["_id"]
        }
        // 如果以上情况都不是
        return item.toString()
    }

    loseloseHASHCode(key){ // 将key转换成string,但是这样也依旧会算出相同的数,所以还需要再借助分离链接法
        if(typeof key === "number"){
            return key
        }
        let tablekey = this.toStringFN(key)
        let hashCode = 0
        // hash算法非常多,这里用一种遍历其中每一个字符,然后累加每一个字符的字符编码charCodeAt
        for(let i = 0; i < tablekey.length; i++){
            hashCode += tablekey.charCodeAt(i)
        }
        return hashCode % 37 // 质数求余,除以任意一个质数取余,将取得的余数作为key,
        // 密码学中非常难生成所有质数,因为质数无法用通项公式生成,所以用质数比较难被破解
    }

    add(key,value){ // 散列表增加值
        if(key != null && key != undefined){
            let position = this.loseloseHASHCode(key) // 将哈希函数生成的索引赋值给下标position
            // 添加之前先判断this.table[position]存在与否
            if(!this.table[position]){ // 如果该索引上不存在值,则添加一个散列表节点
                this.table[position] = new LinkedList() // 分离链接法的散列表中每一个节点都是一个链表结构
            }
            // 下面就是如果存在的情况
            this.table[position].push(new ValuePair(key,value)) // 在该散列表节点索引的值上push进next指针域所指向的值
            return true // 添加成功
        }
        return false // 添加失败
    }

    get(key){ // 通过索引key获取对应值
        let position = this.loseloseHASHCode(key)
        let linkdata = this.table[position]
        if(linkdata){ // 当散列表中存在值
            let current = linkdata.head // 暂时先让current变量保存链头的值
            do{ // 先执行do里面的语句
                if(current.value.key === key){ // 如果current当前保存的值与传入的key一致
                    return current.value // 直接返回current当前保存的值
                }
            }while(current = current.next) // 否则将下一项的值赋值给当前的current再继续进行判断
        }
    }

    remove(key){ // 通过索引key删除对应值,思路和get方法一致,只是改成找到之后删除该值
        let position = this.loseloseHASHCode(key)
        let linkdata = this.table[position]
        if(linkdata){ // 当散列表中存在值
            let current = linkdata.head
            do{
                if(current.value.key === key){
                    linkdata.removeValue(current.value)
                    if(linkdata.isEmpty()){
                        delete this.table[position]
                    }
                    return true
                }
            }while(current = current.next)
        }
        return false
    }
}

let h = new HashTable()
h.add("hello",1)
h.add("world",2)
h.add("heloo",3) // heloo经过哈希函数loseloseHASHCode转化之后的对应值为17
h.add("hel",4) // hel经过哈希函数loseloseHASHCode转化之后的对应值也为17,但此时两者不会进行覆盖
console.log(h)

对于分离链接来说,只需要重写三个方法:addgetremove

这三个方法在每种技术实现中都是不同的:

1、在add方法中,我们将验证要加入新元素的位置是否已经被占据。如果是第一次向该位置加入元素,我们会在该位置上初始化一个LinkedList 类的实例。然后向LinkedList 实例中添加一个ValuePair 实例(键和值)

2、在get方法中,首先要验证的是在特定的位置上是否有元素存在。

我们在position 位置检索linkedList并检验是否存在linkedList 实例。如果没有,则返回一个undefined 表示在HashTable 实例中没有找到这个值。

如果该位置上有值存在,我们知道这是一个LinkedList 实例。现在要做的是迭代这个链表来寻找我们需要的元素。在迭代之前先要获取链 表表头的引用,然后就可以从链表的头部迭代到尾部(最后current.next将会是null)。

3、在remove 方法中,我们使用和get 方法一样的步骤找到要找的元素。迭代LinkedList实例时,如果链表中的current 元素就是要找的元素,使用remove 方法将其从链表中移除。

然后进行一步额外的验证:如果链表为空了(——链表中不再有任何元素了),就使用delete 运算符将散列表的该位置删除,这样搜索一个元素的时候,就可以跳过这个位置了。

最后,返回true 表示该元素已经被移除,或者在最后返回false表示该元素在散列表中不存在。如果不是我们要找的元素,那么和get 方法中一样继续迭代下一个元素。