JS实现字典与散列表

  集合、字典和散列表可以存储不重复的值。在集合中,我们感兴趣的是每个值本身,并把它当作主要元素。在字典中,我们用[键,值]的形式来存储数据。在散列表中也是一样(也是以[键, 值]对的形式来存储数据)。但是两种数据结构的实现方式略有不同。

字典
  集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值] 对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字 典则是以[键,值]的形式来存储元素,字典也称作映射。
  我们用JS来实现字典:

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}
let dic = new Dictionary()
dic.set('name', 'hello')
dic.set('age', 22)
console.log(dic.has('name')) // true
dic.remove('age')
console.log(dic.values()) // [ 'hello' ]

散列表
  散列算法的作用是尽可能快地在数据结构中找到一个值。在之前的章节中,我们已经知道如果要在数据结构中获得一个值(使用get方法),需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址。
  最常见的散列函数——“lose lose”散列函数,方法是简单地将每个键值中的每个字母的ASCII值相加。

  接下来我们用JS来实现散列表:

function HashMap() {
    let items = []
    let loseloseHashCode = function (key) {
        let hash = 0
        for (let i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i)
        }
        // 为了得到比较小的数值,我们会使用hash值和一个任意 数做除法的余数(mod)。
        return hash % 37
    }
    this.put = function (key, value) {
        let position = loseloseHashCode(key)
        console.log(position + '-' + key)
        items[position] = value
    }
    this.get = function (key) {
        return items[loseloseHashCode(key)]
    }
    this.remove = function (key) {
        // 对于HashMap类来说,我们不需要像ArrayList类一样从items数组中将位置也移除。
        // 由于元素分布于整个数组范围内,一些位置会没有任何元素占据,并默认为undefined值。
        // 我们也不能将位置本身从数组中移除(这会改变其他元素的位置),否则,当下次需要获得或移除一个 元素的时候,这个元素会不在我们用散列函数求出的位置上。
        items[loseloseHashCode(key)] = undefined
    }
}
let map = new HashMap()
map.put('name', 'hello') // 10-name
map.put('age', 22) // 5-age
console.log(map.get('name')) // hello
map.remove('name')
console.log(map.get('name')) // undefined
console.log(map.get('age')) // 22

处理散列表中的冲突
  有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为冲突。例如,我们看看下面的代码会得到怎样的输出结果:

var hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com'); 10 hash.put('Tyrion', 'tyrion@email.com');
hash.put('Aaron', 'aaron@email.com');
hash.put('Donnie', 'donnie@email.com');
hash.put('Ana', 'ana@email.com');
hash.put('Jonathan', 'jonathan@email.com');
hash.put('Jamie', 'jamie@email.com');
hash.put('Sue', 'sue@email.com');
hash.put('Mindy', 'mindy@email.com');
hash.put('Paul', 'paul@email.com');
hash.put('Nathan', 'nathan@email.com');
19-Gandalf
29-John
16-Tyrion
16-Aaron
13-Donnie
13-Ana
5-Jonathan
5-Jamie
5-Sue
32-Mindy
32-Paul
10-Nathan

  在当前的数据结构中,对于发生冲突的元素来说,后者会覆盖前者。使用一个数据结构来保存数据的目的显然不是去丢失这些数据,而是通过某种方法将它们全部保存起来。因此,当这种情况发生的时候就要去解决它。

分离链接
  分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面,它是解决冲突最简单的方法。

  我们来实现一个使用了分离链接的HashMap实例,LinkedList类上篇文章已经详细解释过了,这里直接引用。

function LinkedList() {

    // Node辅助类,表示要加入列表的项,element是即将添加到列表的值,next是指向列表中下一个节点项的指针
    let Node = function (element) {
        this.element = element
        this.next = null
    }

    let length = 0
    let head = null

    // 向链表尾部追加元素
    this.append = function (element) {
        let node = new Node(element)
        let current
        if (head === null) { // 列表中第一个节点
            head = node
        } else {
            current = head
            while (current.next) {
                current = current.next // 找到最后一项,是null
            }
            current.next = node // 给最后一项赋值
        }
        length++ // 更新列表的长度
    }

    // 从链表中移除指定位置元素
    this.removeAt = function (position) {
        if (position > -1 && position < length) { // 值没有越界
            let current = head
            let previous, index = 0
            if (position === 0) { //  移除第一项
                head = current.next
            } else {
                while (index++ < position) {
                    previous = current
                    current = current.next
                }
                previous.next = current.next // 将previous与current的下一项连接起来,跳过current,从而移除
            }
            length-- // 更新列表的长度
            return current.element
        } else {
            return null
        }
    }

    // 在链表任意位置插入一个元素
    this.insert = function (position, element) {
        if (position >= 0 && position <= length) { // 检查越界值
            let node = new Node(element),
                current = head,
                previous,
                index = 0
            if (position === 0) { // 在第一个位置添加
                node.next = current
                head = node
            } else {
                while (index++ < position) {
                    previous = current
                    current = current.next
                }
                node.next = current // 在previous与current的下一项之间插入node
                previous.next = node
            }
            length++
            return true
        } else {
            return false
        }
    }

    // 把链表内的值转换成一个字符串
    this.toString = function () {
        let current = head,
            string = ''
        while (current) {
            string += current.element + ' '
            current = current.next
        }
        return string
    }

    // 在链表中查找元素并返回索引值
    this.indexOf = function (element) {
        let current = head,
            index = 0
        while (current) {
            if (element === current.element) {
                return index
            }
            index++
            current = current.next
        }
        return -1
    }

    // 从链表中移除指定元素
    this.remove = function (element) {
        let index = this.indexOf(element)
        return this.removeAt(index)
    }

    this.isEmpty = function () {
        return length === 0
    }

    this.size = function () {
        return length
    }

    this.getHead = function () {
        return head
    }
}

function HashMap() {
    let items = []
    let ValuePair = function (key, value) { // 辅助类,表示将要加入LinkedList实例的元素。
        this.key = key
        this.value = value
        this.toString = function () {
            return '[' + this.key + '-' + this.value + ']'
        }
    }
    let loseloseHashCode = function (key) {
        let hash = 0
        for (let i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i)
        }
        // 为了得到比较小的数值,我们会使用hash值和一个任意 数做除法的余数(mod)。
        return hash % 37
    }
    
    this.put = function (key, value) {
        let position = loseloseHashCode(key)
        if (items[position] == undefined) {
            items[position] = new LinkedList() // 初始化一个LinkedList类的实例
        }
        items[position].append(new ValuePair(key, value)) // 在单链表中添加element
    }
    
    this.get = function (key) {
        let position = loseloseHashCode(key)
        if (items[position] !== undefined) {
            let current = items[position].getHead()
            while(current.next) { // 遍历链表来寻找键/值
                if (current.element.key === key) {
                    return current.element.value
                }
                current = current.next
            }
            if (current.element.key === key) { // 检查元素在链表第一个或最后一个节点的情况
                return current.element.value
            }
        }
        return undefined
    }
    
    this.remove = function (key) {
        let position = loseloseHashCode(key)
        if (items[position] !== undefined) {
            let current = items[position].getHead()
            while (current.next) {
                if (current.element.key === key) {
                    items[position].remove(current.element)
                    if (items[position].isEmpty()) {
                        items[position] = undefined
                    }
                    return true
                }
                current = current.next
            }
            if (current.element.key === key) {
                items[position].remove(current.element)
                if (items[position].isEmpty()) {
                    items[position] = undefined
                }
                return true
            }
        }
        return false
    },
        
    this.getItems = function () {
        return items
    }
}

let hash = new HashMap()
hash.put('Gandalf', 'gandalf@email.com')
hash.put('John', 'johnsnow@email.com')
hash.put('Tyrion', 'tyrion@email.com')
hash.put('Aaron', 'aaron@email.com')
hash.put('Donnie', 'donnie@email.com')
hash.put('Ana', 'ana@email.com')
hash.put('Jonathan', 'jonathan@email.com')
hash.put('Jamie', 'jamie@email.com')
hash.put('Sue', 'sue@email.com')
hash.put('Mindy', 'mindy@email.com')
hash.put('Paul', 'paul@email.com')
hash.put('Nathan', 'nathan@email.com')

线性查找
  另一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,如果索引 为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试 index+2的位置,以此类推。

function HashMap() {

    let items = []
    let ValuePair = function (key, value) { // 辅助类,表示将要加入LinkedList实例的元素。
        this.key = key
        this.value = value
        this.toString = function () {
            return '[' + this.key + '-' + this.value + ']'
        }
    }

    let loseloseHashCode = function (key) {
        let hash = 0
        for (let i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i)
        }
        // 为了得到比较小的数值,我们会使用hash值和一个任意 数做除法的余数(mod)。
        return hash % 37
    }

    this.put = function (key, value) {
        let position = loseloseHashCode(key)
        if (items[position] == undefined) {
            items[position] = new ValuePair(key, value)
        } else {
            let index = ++position
            while (items[index] != undefined) {
                index++
            }
            items[index] = new ValuePair(key, value)
        }
    }

    this.get = function (key) {
        let position = loseloseHashCode(key)
        if (items[position] !== undefined) {
            if (items[position].key === key) {
                return items[position].value
            } else {
                let index = ++position
                while (items[index] === undefined || items[index].key !== key) {
                    index++
                }
                if(items[index].key === key) {
                    return items[index].value
                }
            }
        }
        return undefined
    }

    this.remove = function (key) {
        let position = loseloseHashCode(key)
        if (items[position] !== undefined) {
            if (items[position].key === key) {
                items[position] = undefined
            } else {
                let index = ++position
                while (items[index] === undefined || items[index].key !== key) {
                    index++
                }
                if(items[index].key === key) {
                    items[position] = undefined
                }
            }
        }
        return undefined
    }
}
let map = new HashMap()
map.put('name', 'hello') // 10-name
map.put('age', 22) // 5-age
console.log(map.get('name')) // hello
map.remove('name')
console.log(map.get('name')) // undefined
console.log(map.get('age')) // 22

创建更好的散列函数
  我们实现的“lose lose”散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。如果我们使用这个函数的话,会产生各种各样的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),当然也包括较低的冲突可能性。我们可以在网上找到一些不同的实现方法,或者也可以实现自己的散列函数。

 let djb2HashCode = function (key) {
        let hash = 5381; //{1}
        for (let i = 0; i < key.length; i++) { //{2}
            hash = hash * 33 + key.charCodeAt(i); //{3}
        }
        return hash % 1013; //{4}
    }

  它包括初始化一个hash变量并赋值为一个质数,大多数实现都使用5381,然后迭代参数key,将hash与33相乘(用来当作一个魔力数),并和当前迭代到的字符的ASCII 码值相加。最后,我们将使用相加的和与另一个随机质数(比我们认为的散列表的大小要大——在本例 中,我们认为散列表的大小为1000)相除的余数。
  这并不是最好的散列函数,但这是最受社区推崇的散列函数之一。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容