JS實現(xiàn)字典與散列表

??集合、字典和散列表可以存儲不重復(fù)的值。在集合中,我們感興趣的是每個值本身,并把它當(dāng)作主要元素。在字典中,我們用[鍵,值]的形式來存儲數(shù)據(jù)。在散列表中也是一樣(也是以[鍵, 值]對的形式來存儲數(shù)據(jù))。但是兩種數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)方式略有不同。

字典
??集合表示一組互不相同的元素(不重復(fù)的元素)。在字典中,存儲的是[鍵,值] 對,其中鍵名是用來查詢特定元素的。字典和集合很相似,集合以[值,值]的形式存儲元素,字 典則是以[鍵,值]的形式來存儲元素,字典也稱作映射。
??我們用JS來實現(xiàn)字典:

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' ]

散列表
??散列算法的作用是盡可能快地在數(shù)據(jù)結(jié)構(gòu)中找到一個值。在之前的章節(jié)中,我們已經(jīng)知道如果要在數(shù)據(jù)結(jié)構(gòu)中獲得一個值(使用get方法),需要遍歷整個數(shù)據(jù)結(jié)構(gòu)來找到它。如果使用散列函數(shù),就知道值的具體位置,因此能夠快速檢索到該值。散列函數(shù)的作用是給定一個鍵值,然后返回值在表中的地址。
??最常見的散列函數(shù)——“l(fā)ose lose”散列函數(shù),方法是簡單地將每個鍵值中的每個字母的ASCII值相加。

??接下來我們用JS來實現(xiàn)散列表:

function HashMap() {
    let items = []
    let loseloseHashCode = function (key) {
        let hash = 0
        for (let i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i)
        }
        // 為了得到比較小的數(shù)值,我們會使用hash值和一個任意 數(shù)做除法的余數(shù)(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數(shù)組中將位置也移除。
        // 由于元素分布于整個數(shù)組范圍內(nèi),一些位置會沒有任何元素占據(jù),并默認(rèn)為undefined值。
        // 我們也不能將位置本身從數(shù)組中移除(這會改變其他元素的位置),否則,當(dāng)下次需要獲得或移除一個 元素的時候,這個元素會不在我們用散列函數(shù)求出的位置上。
        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

處理散列表中的沖突
??有時候,一些鍵會有相同的散列值。不同的值在散列表中對應(yīng)相同位置的時候,我們稱其為沖突。例如,我們看看下面的代碼會得到怎樣的輸出結(jié)果:

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

??在當(dāng)前的數(shù)據(jù)結(jié)構(gòu)中,對于發(fā)生沖突的元素來說,后者會覆蓋前者。使用一個數(shù)據(jù)結(jié)構(gòu)來保存數(shù)據(jù)的目的顯然不是去丟失這些數(shù)據(jù),而是通過某種方法將它們?nèi)勘4嫫饋?。因此,?dāng)這種情況發(fā)生的時候就要去解決它。

分離鏈接
??分離鏈接法包括為散列表的每一個位置創(chuàng)建一個鏈表并將元素存儲在里面,它是解決沖突最簡單的方法。

??我們來實現(xiàn)一個使用了分離鏈接的HashMap實例,LinkedList類上篇文章已經(jīng)詳細(xì)解釋過了,這里直接引用。

function LinkedList() {

    // Node輔助類,表示要加入列表的項,element是即將添加到列表的值,next是指向列表中下一個節(jié)點項的指針
    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) { // 列表中第一個節(jié)點
            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
        }
    }

    // 把鏈表內(nèi)的值轉(zhuǎn)換成一個字符串
    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)
        }
        // 為了得到比較小的數(shù)值,我們會使用hash值和一個任意 數(shù)做除法的余數(shù)(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) { // 檢查元素在鏈表第一個或最后一個節(jié)點的情況
                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')

線性查找
??另一種解決沖突的方法是線性探查。當(dāng)想向表中某個位置加入一個新元素的時候,如果索引 為index的位置已經(jīng)被占據(jù)了,就嘗試index+1的位置。如果index+1的位置也被占據(jù)了,就嘗試 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)
        }
        // 為了得到比較小的數(shù)值,我們會使用hash值和一個任意 數(shù)做除法的余數(shù)(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

創(chuàng)建更好的散列函數(shù)
??我們實現(xiàn)的“l(fā)ose lose”散列函數(shù)并不是一個表現(xiàn)良好的散列函數(shù),因為它會產(chǎn)生太多的沖突。如果我們使用這個函數(shù)的話,會產(chǎn)生各種各樣的沖突。一個表現(xiàn)良好的散列函數(shù)是由幾個方面構(gòu)成的:插入和檢索元素的時間(即性能),當(dāng)然也包括較低的沖突可能性。我們可以在網(wǎng)上找到一些不同的實現(xiàn)方法,或者也可以實現(xiàn)自己的散列函數(shù)。

 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變量并賦值為一個質(zhì)數(shù),大多數(shù)實現(xiàn)都使用5381,然后迭代參數(shù)key,將hash與33相乘(用來當(dāng)作一個魔力數(shù)),并和當(dāng)前迭代到的字符的ASCII 碼值相加。最后,我們將使用相加的和與另一個隨機(jī)質(zhì)數(shù)(比我們認(rèn)為的散列表的大小要大——在本例 中,我們認(rèn)為散列表的大小為1000)相除的余數(shù)。
??這并不是最好的散列函數(shù),但這是最受社區(qū)推崇的散列函數(shù)之一。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容