嘮叨一下js對象與哈希表那些事

最近在整理數據結構和算法相關的知識,小茄專門在github上開了個repo https://github.com/qieguo2016...,後續內容也會更新到這裏,歡迎圍觀加星星!javascript

js對象

js中的對象是基於哈希表結構的,而哈希表的查找時間複雜度爲O(1),因此不少人喜歡用對象來作映射,減小遍歷循環。java

好比常見的數組去重:git

function arrayUnique(target) {
  var result = [target[0]];
  var temp = {};
  temp[target[0]] = true;
  for (var i = 1, targetLen = target.length; i < targetLen; i++) {
    if (typeof temp[target[i]] === 'undefined') {
      result.push(target[i]);
      temp[target[i]] = true;
    }
  }
  return result;
}

這裏使用了一個temp對象來保存出現過的元素,在循環中每次只須要判斷當前元素是否在temp對象內便可判斷出該元素是否已經出現過。github

上面的代碼看起來沒有問題,但有點經驗的同窗可能會說了,假如目標數組是[1,'1'], 這是2個不一樣類型元素,因此咱們的指望值應該是原樣輸出的。但結果倒是[1]。
同理的還有true、null等,也就是說對象中的key在obj[key]時都被自動轉成了字符串類型。
因此,若是要區分出不一樣的類型的話,temp裏面的屬性值就不能是一個簡單的true了,而是要包含幾種數據類型。好比能夠是:算法

temp[target[0]]={};
    temp[target[0]][(typeof temp[target[i]])] = 1;

在判斷的時候除了要判斷鍵是否存在以外,也要判斷對應的數據類型計數是否大於1,以此來判斷元素是否重複。編程

另外,上面的代碼語法也有點問題,不知道你發現了沒?
咱們造的這個temp對象並非徹底空白,他是基於Object原型鏈繼承而來的,因此自帶了一個__proto__屬性,若是你的目標數組裏面剛好有"__proto__"這個值,返回的結果就有問題了,具體結果能夠本身測試確認。解決方法有兩種:數組

1) 想辦法去掉這個磨人的__proto__。顯然,咱們須要去掉原型鏈,那麼可使用Object.create(null)的方式來建立一個徹底空白、無原型的空對象。數據結構

2) 使用!temp.hasOwnProperty(target[i])代替typeof temp[target[i]] === 'undefined',這時候表明原型鏈的__proto__屬性就不能干擾到咱們的結果判斷了。 感謝@天生愛走神的指正,obj.hasOwnProperty(__proto__)會獲得false,可是假如咱們的目標數組裏麪包含__proto__的話,就不能對__proto__進行去重了。數據結構和算法

上面說了js中使用對象的一點小竅門,核心在於對象的hashmap結構,那hashmap是怎樣的一個結構呢?且聽小茄細細道來。函數

Hash Map

在真實世界中,咱們描述一個事物最經常使用的方式是使用屬性-key-value)這樣的鍵值對數據,面向對象編程中對象的定義和js中的對象都是這種模式。好比咱們描述一我的是這樣的:

一個對象

那在計算機中怎麼保存這樣的數據呢?

計算機存儲空間有兩個屬性:存儲地址和所存儲的,機器能夠根據給定的存儲地址去讀寫該地址下的。根據這種結構,假如咱們將一塊存儲空間分紅一個一個的格子,而後將這些數據依次塞到每一個格子裏,接下來咱們就能夠根據格子編號直接訪問格子的內容了。這種方式就是數組(也叫線性連續表):數組頭保存整個數組儲存空間的起始地址,不一樣下標表明不一樣的儲存地址的偏移量,訪問不一樣下標所對應的地址就能實現數組元素的讀寫。因此,很天然就會想到將上述的鍵值對數據的key映射成數組下標,接着讀寫數組就變成了讀寫value值。將key的字符串轉換成表明下標數值比較簡單,能夠用特定的碼錶(如ASCII)進行轉換。

上述小明的屬性名(key)通過變換,可能就變成了這樣:

屬性名轉換

因爲key的值不一樣長度不一,因此轉換後的下標的值也相差巨大,另外key的個數不肯定,也就意味着下標的個數也有很大的範圍,甚至無窮多,就有了下面的問題:

怎麼將一組值相差範圍巨大,個數波動範圍很大的下標放入特定的數組空間呢?

若是咱們直接取下標值做爲存儲數組的下標,雖然簡單,可是你會發現這個長度爲10010的數組只存了8個值,太浪費!若是咱們想要縮短數組的長度,好比縮爲10,最簡單的方式可使用取模的方式來肯定下標:69 % 10 = 9,7 % 10 = 7, 198 % 10 = 8……。這個取模就是哈希算法、也叫散列算法

經過這樣的方式獲得的下標分別爲九、七、八、三、六、二、0,能夠獲得保存小明數據的數組:

圖片描述

可是這種方式很容易出現重複,假如咱們增長一個屬性phone,對應的映射值是29,那麼29跟69算出來的下標就重複了。也就是哈希算法中的衝突,也叫碰撞。好的哈希算法能極大減小衝突,但因爲輸入幾乎是無窮的,而輸出卻要求在有限的空間內,因此衝突是不可避免的。

那如何處理衝突呢?

仍是上面這個例子,29和69發生了衝突,可是咱們能夠將他們組成一個鏈表,鏈表的頭部放在數組中,獲得。鏈表結構中,每一個元素(除單向鏈表的尾部)都包含了相連元素的內存地址和自己的值,上文中的衝突放入一個鏈表中,能夠獲得這樣的結構:

圖片描述

最終獲得的這個數據結構,也就是咱們常說哈希表了。這種將數組與鏈表結合生成哈希表的方法,叫拉鍊法

哈希表數據的查找

好比想知道小明的name屬性,即小明.name。流程是這樣的:

1)根據字符映射關係獲得映射值爲69
2)使用哈希算法獲得下標 index=hash(69)=9
3)遍歷數組中下標爲9的鏈表,鏈表的第一個元素的key恰好就是咱們要找的name,因此返回value小明

哈希表中增刪一個元素並不會影響到其餘的元素,不像數組同樣須要改變後面全部的元素下標。在拉鍊式的哈希表中,屬性的增刪就是鏈表的增刪,很是方便。而在純數組形式的哈希表中,對屬性的刪並非真的刪除,而是作一個空標誌而已,因此不影響其餘元素。

Hash Map的擴展知識

對於哈希表來講,最重要的莫過於生成哈希串的哈希算法和處理衝突的策略了。下面進行簡單的介紹。

哈希算法(散列算法)

根據上面的例子得知,哈希算法的目的就是將不定的輸入轉換成特定範圍的輸出,而且要求輸出儘可能均勻分佈。因爲散列算法是應用在每一次數據定位中的,它的使用頻率很是的高,這意味着咱們必需要選擇簡單的算法。散列算法有不少,這裏簡單介紹幾種。

1,除法散列法
最直觀的一種,小茄上文使用的就是這種散列法,公式:
index = key % 16

2,平方散列法
index是很是頻繁的操做,而乘法的運算要比除法來得省時(對如今的CPU來講,估計咱們感受不出來),因此咱們考慮把除法換成乘法和一個位移操做。公式:
index = (key * key) >> 28
若是數值分配比較均勻的話這種方法能獲得不錯的結果,另外key若是很大,key * key會發生溢出。但咱們這個乘法不關心溢出,由於咱們根本不是爲了獲取相乘結果,而是爲了獲取index

3,斐波那契(Fibonacci)散列法
平方散列法的缺點是顯而易見的,因此咱們能不能找出一個理想的乘數,而不是拿value自己看成乘數呢?答案是確定的。

1,對於16位整數而言,這個乘數是40503
2,對於32位整數而言,這個乘數是2654435769
3,對於64位整數而言,這個乘數是11400714819323198485

這幾個「理想乘數」是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。

對咱們常見的32位整數而言,公式:
index = (key* 2654435769) >> 28

處理衝突的策略

上文介紹了拉鍊法來處理衝突,處理衝突的方法其實也有不少,下面簡單介紹一下另外幾種:

1)拉鍊法變種。因爲鏈表的查找須要遍歷,若是咱們將鏈表換成樹或者哈希表結構,那麼就能大幅提升衝突元素的查找效率。不過這樣作則會加大哈希表構造的複雜度,也就是構建哈希表時的效率會變差。

2)開放尋址: 當關鍵字key的哈希地址p=H(key)出現衝突時,以p爲基礎,產生另外一個哈希地址p1,若是p1仍然衝突,再以p爲基礎,產生另外一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi,將相應元素存入其中。這種方法有一個通用的函數形式:

Hi=(H(key)+di)% m i=1,2,…,n

根據di的不一樣,又能夠分爲線性的、平方的、隨機數之類的。。。這裏再也不展開。

開發尋址的好處是存儲空間更加緊湊,利用率高。可是這種方式讓衝突元素之間產生了聯繫,在刪除元素的時候,不能直接刪除,不然就打亂了衝突元素的尋址鏈。

3)再哈希法

這種方法會預先定義一組哈希算法,發生衝突的時候,調用下一個哈希算法計算一直計算到不發生衝突的時候則插入元素,這種方法跟開放尋址的方法優缺點相似。函數表達式:

index=Hi(key) , i=1,2,…,n

哈希相關的應用實踐

哈希算法經常使用的場景除了上文所說的快速查找以外,還有一個很是重要的應用就是加密算法,這個加密更準確的說法是加簽,也便是「消息摘要」。

根據上文的基礎介紹可知,哈希算法就是將任意數據轉換成必定範圍數據的算法,這種算法的反作用就是會產生衝突。可是呢,在快速查找中出現的反作用,倒是加密功能中的核心,由於有衝突,因此從結果就沒法逆推出輸入值,這樣就實現了數據的單向加密。而輸入數據的變化卻又會影響到哈希串的值,因此咱們能夠用哈希串來進行數據的校驗。

關於js對象與哈希相關的東西就說到這裏了,用文字總結一下以後發現不少知識點都明確了不少,尤爲是要用最平白的語言組織出來,就必須有本身的理解才行。任何一個細節均可以看出不少東西,謹以此文與君共勉!

相關文章
相關標籤/搜索