JavaScript 哈希表的实现

本文详细介绍了JavaScript中哈希表的实现,包括哈希函数的设计,强调了快速计算和均匀分布的重要性,并指出使用质数来优化哈希表的性能。此外,还讨论了哈希表的扩容策略和何时触发扩容,以及如何判断和选择质数作为容量,以确保数据在哈希表中均匀分布。
摘要由CSDN通过智能技术生成

一、哈希函数

快速计算 

  • 好的哈希函数应该尽可能让计算的过程变得简单, 应该可以快速计算出结果 
    • 哈希表的主要优点是它的速度, 所以在速度上不能满足, 那么就达不到设计的目的了.
    • 提高速度的一个办法就是让哈希函数中尽量少的有乘法和除法. 因为它们的性能是比较低的.
  • 在前面, 我们计算哈希值的时候使用的方式
    • cats = 3*27³+1*27²+20*27+17= 60337
    • 这种方式是直观的计算结果, 那么这种计算方式会进行几次乘法几次加法呢? 当然, 我们可能不止4项, 可能有更多项
    • 我们抽象一下, 这个表达式其实是一个多项式: a(n)xn+a(n-1)x(n-1)+…+a(1)x+a(0)
    • 现在问题就变成了多项式有多少次乘法和加法:
      • 乘法次数: n+(n-1)+…+1=n(n+1)/2
      • 加法次数: n次
  • 多项式的优化: 霍纳法则
    • 解决这类求值问题的高效算法――霍纳法则。在中国,霍纳法则也被称为秦九韶算法。
    • 通过如下变换我们可以得到一种快得多的算法,即Pn(x)= anx n+a(n-1)x(n-1)+…+a1x+a0=((…(((anx +an-1)x+an-2)x+ an-3)…)x+a1)x+a0,这种求值的安排我们称为霍纳法则。
    • 变换后, 我们需要多少次乘法, 多少次加法呢?
      • 乘法次数: N次
      • 加法次数: N次.
    • 如果使用大O表示时间复杂度的话, 我们直接从O(N²)降到了O(N).

 均匀的分布

  • 均匀的分布
    • 在设计哈希表时, 我们已经有办法处理映射到相同下标值的情况: 链地址法或者开放地址法.
    • 但是, 为了提供效率, 最好的情况还是让数据在哈希表中均匀分布.
    • 因此, 我们需要在使用常量的地方, 尽量使用质数.
    • 哪些地方我们会使用到常量呢?
  • 质数的使用:
    • 哈希表的长度.
    • N次幂的底数(我们之前使用的是27)
    • 下面我们简单来说一下为什么.
  • 哈希表的长度使用质数:
    • 这个在链地址法中事实上重要性不是特别明显, 明显的是在开放地址法中的再哈希法中.
    • 再哈希法中质数的重要性:
      • 假设表的容量不是质数, 例如: 表长为15(下标值0~14)
      • 有一个特定关键字映射到0, 步长为5. 探测序列是多少呢?
      • 0 - 5 - 10 - 0 - 5 - 10, 依次类推, 循环下去.
      • 算法只尝试着三个单元, 如果这三个单元已经有了数据, 那么会一直循环下去, 知道程序崩溃.
      • 如果容量是一个质数, 比如13. 探测序列是多少呢?
      • 0 - 5 - 10 - 2 - 7 - 12 - 4 - 9 - 1 - 6 - 11 - 3, 一直这样下去.
      • 不仅不会产生循环, 而且可以让数据在哈希表中更加均匀的分布.
    • 链地址法中质数没有那么重要, 甚至在Java中故意是2的N次幂
      • Java中的哈希表采用的是链地址法.
      • HashMap的初始长度是16, 每次自动扩展(我们还没有聊到扩展的话题), 长度必须是2的次幂.
      • 这是为了服务于从Key映射到index的算法.
      • HashMap中为了提高效率, 采用了位运算的方式.
        • HashMap中index的计算公式: index = HashCode(Key) & (Length - 1)
        • 比如计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001
        • 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111
        • 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111
        • 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9
        • 这样的方式相对于取模来说性能是高的, 因为计算机更运算计算二进制的数据.
      • 但是, 我个人发现JavaScript中进行较大数据的位运算时会出问题, 所以我的代码实现中还是使用了取模.
  • N次幂的底数, 使用质数:
    • 这里采用质数的原因是为了产生的数据不按照某种规律递增.
    • 比如我们这里有一组数据是按照4进行递增的: 0 4 8 12 16, 将其映射到成都为8的哈希表中.
    • 它们的位置是多少呢? 0 - 4 - 0 - 4, 依次类推.
    • 如果我们哈希表本身不是质数, 而我们递增的数量可以使用质数, 比如5, 那么 0 5 10 15 20
    • 它们的位置是多少呢? 0 - 5 - 2 - 7 - 4, 依次类推. 也可以尽量让数据均匀的分布.
    • 我们之前使用的是27, 这次可以使用一个接近的数, 比如31/37/41等等. 一个比较常用的数是37.

哈希函数实现 

function hashFunc(str, max) {
    // 1.初始化hashCode的值
    var hashCode = 0

    // 2.霍纳算法, 来计算hashCode的数值
    for (var i = 0; i < str.length; i++) {
        hashCode = 37 * hashCode + str.charCodeAt(i)
    }

    // 3.取模运算
    hashCode = hashCode % max
    return hashCode
}

 

二. 哈希表 

创建哈希表 

// 创建HashTable构造函数
function HashTable() {
    // 定义属性
    this.storage = []
    this.count = 0
    this.limit = 8

    // 定义相关方法
    // 哈希函数
    HashTable.prototype.hashFunc = function(str, max) {
        // 1.初始化hashCode的值
        var hashCode = 0

        // 2.霍纳算法, 来计算hashCode的数值
        for (var i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i)
        }
      
        // 3.取模运算
        hashCode = hashCode % max
        return hashCode
    }
}

插入&修改数据 

// 插入数据方法
HashTable.prototype.put = function (key, value) {
    // 1.获取key对应的index
    var index = this.hashFunc(key, this.limit)

    // 2.取出数组(也可以使用链表)
    var bucket = this.storage[index]

    // 3.判断这个数组是否存在
    if (bucket === undefined) {
        // 3.1创建桶
        bucket = []
        this.storage[index] = bucket
    }
    alert(bucket)
    
    // 4.判断是新增还是修改原来的值.
    var override = false
    for (var i = 0; i < bucket.length; i++) {
        var tuple = bucket[i]
        if (tuple[0] === key) {
            tuple[1] = value
            override = true
        }
    }
    
    // 5.如果是新增, 前一步没有覆盖
    if (!override) {
        bucket.push([key, value])
        this.count++
    }
}

获取数据 

// 获取存放的数据
HashTable.prototype.get = function (key) {
    // 1.获取key对应的index
    var index = this.hashFunc(key, this.limit)

    // 2.获取对应的bucket
    var bucket = this.storage[index]

    // 3.如果bucket为null, 那么说明这个位置没有数据
    if (bucket == null) {
        return null
    }

    // 4.有bucket, 判断是否有对应的key
    for (var i = 0; i < bucket.length; i++) {
        var tuple = bucket[i]
        if (tuple[0] === key) {
            return tuple[1]
        }
    }
    
    // 5.没有找到, return null
    return null
}

删除数据 

// 删除数据
HashTable.prototype.remove = function (key) {
    // 1.获取key对应的index
    var index = this.hashFunc(key, this.limit)
    
    // 2.获取对应的bucket
    var bucket = this.storage[index]
    
    // 3.判断同是否为null, 为null则说明没有对应的数据
    if (bucket == null) {
        return null
    }
    
    // 4.遍历bucket, 寻找对应的数据
    for (var i = 0; i < bucket.length; i++) {
        var tuple = bucket[i]
        if (tuple[0] === key) {
            bucket.splice(i, 1)
            this.count--
            return tuple[1]
        }
    }
    
    // 5.来到该位置, 说明没有对应的数据, 那么返回null
    return null
}

其他方法 

  • 判断哈希表是否为空: isEmpty 
// isEmpty方法
HashTable.prototype.isEmpty = function () {
    return this.count == 0
}
  • 获取哈希表中数据的个数 
// size方法
HashTable.prototype.size = function () {
    return this.count
}

 

三. 哈希表扩容 

哈希表扩容的思想 

  • 为什么需要扩容?
    • 目前, 我们是将所有的数据项放在长度为8的数组中的.
    • 因为我们使用的是链地址法, loadFactor可以大于1, 所以这个哈希表可以无限制的插入新数据.
    • 但是, 随着数据量的增多, 每一个index对应的bucket会越来越长, 也就造成效率的降低.
    • 所以, 在合适的情况对数组进行扩容. 比如扩容两倍.
  • 如何进行扩容?
    • 扩容可以简单的将容量增加大两倍(不是质数吗? 质数的问题后面再讨论)
    • 但是这种情况下, 所有的数据项一定要同时进行修改(重新哈希化, 来获取到不同的位置)
    • 比如hashCode=12的数据项, 在length=8的时候, index=4. 在长度为16的时候呢? index=12.
    • 这是一个耗时的过程, 但是如果数组需要扩容, 那么这个过程是必要的.
  • 什么情况下扩容呢?
    • 比较常见的情况是loadFactor>0.75的时候进行扩容.
    • 比如Java的哈希表就是在装填因子大于0.75的时候, 对哈希表进行扩容.

 哈希表扩容的实现 

// 哈希表扩容
HashTable.prototype.resize = function (newLimit) {
    // 1.保存旧的数组内容
    var oldStorage = this.storage

    // 2.重置属性
    this.limit = newLimit
    this.count = 0
    this.storage = []

    // 3.遍历旧数组中的所有数据项, 并且重新插入到哈希表中
    oldStorage.forEach(function (bucket) {
        // 1.bucket为null, 说明这里面没有数据
        if (bucket == null) {
            return
        }

        // 2.bucket中有数据, 那么将里面的数据重新哈希化插入
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            this.put(tuple[0], tuple[1])
        }
    }.bind(this))
}
  • 修改put方法 
// 插入数据方法
HashTable.prototype.put = function (key, value) {
    // 1.获取key对应的index
    var index = this.hashFunc(key, this.limit)

    // 2.取出数组(也可以使用链表)
    // 数组中放置数据的方式: [[ [k,v], [k,v], [k,v] ] , [ [k,v], [k,v] ]  [ [k,v] ] ]
    var bucket = this.storage[index]

    // 3.判断这个数组是否存在
    if (bucket === undefined) {
        // 3.1创建桶
        bucket = []
        this.storage[index] = bucket
    }

    // 4.判断是新增还是修改原来的值.
    var override = false
    for (var i = 0; i < bucket.length; i++) {
        var tuple = bucket[i]
        if (tuple[0] === key) {
            tuple[1] = value
            override = true
        }
    }

    // 5.如果是新增, 前一步没有覆盖
    if (!override) {
        bucket.push([key, value])
        this.count++
        // 数组扩容
        if (this.count > this.limit * 0.75) {
            this.resize(this.limit * 2)
        }
    }
}
  • 修改remove方法 
// 删除数据
HashTable.prototype.remove = function (key) {
    // 1.获取key对应的index
    var index = this.hashFunc(key, this.limit)

    // 2.获取对应的bucket
    var bucket = this.storage[index]

    // 3.判断同是否为null, 为null则说明没有对应的数据
    if (bucket == null) {
        return null
    }

    // 4.遍历bucket, 寻找对应的数据
    for (var i = 0; i < bucket.length; i++) {
        var tuple = bucket[i]
        if (tuple[0] === key) {
            bucket.splice(i, 1)
            this.count--
            
            // 缩小数组的容量
            if (this.limit > 8 && this.count < this.limit * 0.25) {
                this.resize(Math.floor(this.limit / 2))
            }
        }
        return tuple[1]
    }

    // 5.来到该位置, 说明没有对应的数据, 那么返回null
    return null
}

 

四. 容量质数 

判断质数 

  • 我们这里先讨论一个常见的面试题, 判断一个数是质数.

  • 质数的特点:

    • 质数也称为素数.
    • 质数表示大于1的自然数中, 只能被1和自己整除的数.
  • OK, 了解了这个特点, 应该不难写出它的算法:

function isPrime(num) {
    // 1.获取平方根
    var temp = parseInt(Math.sqrt(num))

    // 2.循环判断
    for (var i = 2; i <= temp; i++) {
        if (num % i == 0) {
            return false
        }
    }
    return true
}

扩容的质数

  • 首先, 将初始的limit为8, 改成7

  • 前面, 我们有对容量进行扩展, 方式是: 原来的容量 x 2

    • 比如之前的容量是7, 那么扩容后就是14. 14还是一个质数吗?
    • 显然不是, 所以我们还需要一个方法, 来实现一个新的容量为质数的算法.
  • 那么我们可以封装获取新的容量的代码(质数)

// 判断是否是质数
HashTable.prototype.isPrime = function (num) {
    var temp = parseInt(Math.sqrt(num))
    // 2.循环判断
    for (var i = 2; i <= temp; i++) {
        if (num % i == 0) {
            return false
        }
    }
    return true
}

// 获取质数
HashTable.prototype.getPrime = function (num) {
    while (!isPrime(num)) {
        num++
    }
    return num
}
  • 插入数据的代码: 
// 扩容数组的数量
if (this.count > this.limit * 0.75) {
    var primeNum = this.getPrime(this.limit * 2)
    this.resize(primeNum)
}
  • 删除数据的代码: 
// 缩小数组的容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
    var primeNum = this.getPrime(Math.floor(this.limit / 2))
    this.resize(primeNum)
}

五. 完整代码 

// 创建HashTable构造函数
function HashTable() {
    // 定义属性
    this.storage = []
    this.count = 0
    this.limit = 8

    // 定义相关方法
    // 判断是否是质数
    HashTable.prototype.isPrime = function (num) {
        var temp = parseInt(Math.sqrt(num))
        // 2.循环判断
        for (var i = 2; i <= temp; i++) {
            if (num % i == 0) {
                return false
            }
        }
        return true
    }

    // 获取质数
    HashTable.prototype.getPrime = function (num) {
        while (!isPrime(num)) {
            num++
        }
        return num
    }

    // 哈希函数
    HashTable.prototype.hashFunc = function(str, max) {
        // 1.初始化hashCode的值
        var hashCode = 0

        // 2.霍纳算法, 来计算hashCode的数值
        for (var i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i)
        }

        // 3.取模运算
        hashCode = hashCode % max
        return hashCode
    }

    // 插入数据方法
    HashTable.prototype.put = function (key, value) {
        // 1.获取key对应的index
        var index = this.hashFunc(key, this.limit)

        // 2.取出数组(也可以使用链表)
        // 数组中放置数据的方式: [[ [k,v], [k,v], [k,v] ] , [ [k,v], [k,v] ]  [ [k,v] ] ]
        var bucket = this.storage[index]

        // 3.判断这个数组是否存在
        if (bucket === undefined) {
            // 3.1创建桶
            bucket = []
            this.storage[index] = bucket
        }

        // 4.判断是新增还是修改原来的值.
        var override = false
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                tuple[1] = value
                override = true
            }
        }

        // 5.如果是新增, 前一步没有覆盖
        if (!override) {
            bucket.push([key, value])
            this.count++

            if (this.count > this.limit * 0.75) {
                var primeNum = this.getPrime(this.limit * 2)
                this.resize(primeNum)
            }
        }
    }

    // 获取存放的数据
    HashTable.prototype.get = function (key) {
        // 1.获取key对应的index
        var index = this.hashFunc(key, this.limit)

        // 2.获取对应的bucket
        var bucket = this.storage[index]

        // 3.如果bucket为null, 那么说明这个位置没有数据
        if (bucket == null) {
            return null
        }

        // 4.有bucket, 判断是否有对应的key
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                return tuple[1]
            }
        }

        // 5.没有找到, return null
        return null
    }

    // 删除数据
    HashTable.prototype.remove = function (key) {
        // 1.获取key对应的index
        var index = this.hashFunc(key, this.limit)

        // 2.获取对应的bucket
        var bucket = this.storage[index]

        // 3.判断同是否为null, 为null则说明没有对应的数据
        if (bucket == null) {
            return null
        }

        // 4.遍历bucket, 寻找对应的数据
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                bucket.splice(i, 1)
                this.count--

                // 缩小数组的容量
                if (this.limit > 7 && this.count < this.limit * 0.25) {
                    var primeNum = this.getPrime(Math.floor(this.limit / 2))
                    this.resize(primeNum)
                }
            }
            return tuple[1]
        }

        // 5.来到该位置, 说明没有对应的数据, 那么返回null
        return null
    }

    // isEmpty方法
    HashTable.prototype.isEmpty = function () {
        return this.count == 0
    }

    // size方法
    HashTable.prototype.size = function () {
        return this.count
    }

    // 哈希表扩容
    HashTable.prototype.resize = function (newLimit) {
        // 1.保存旧的数组内容
        var oldStorage = this.storage

        // 2.重置属性
        this.limit = newLimit
        this.count = 0
        this.storage = []

        // 3.遍历旧数组中的所有数据项, 并且重新插入到哈希表中
        oldStorage.forEach(function (bucket) {
            // 1.bucket为null, 说明这里面没有数据
            if (bucket == null) {
                return
            }

            // 2.bucket中有数据, 那么将里面的数据重新哈希化插入
            for (var i = 0; i < bucket.length; i++) {
                var tuple = bucket[i]
                this.put(tuple[0], tuple[1])
            }
        }).bind(this)
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值