我爲何要使用哈希

什麼是哈希(Hash)

原本這裏不該該出現這一節的,由於實際上你們應該都知道什麼是哈希。不過有時候爲了文章的完整性,我這裏就稍微教條性地說明一下吧。ヽ(́◕◞౪◟◕‵)ノjavascript

散列(英語:Hashing),一般音譯做哈希,是電腦科學中一種對資料的處理方法,經過某種特定的函數、算法將要檢索的項與用來檢索的索引關聯起來,生成一種便於搜索的數據結構。也譯爲散列。前端

-- From 散列, Wikipediajava

實際上通俗的說法就是把某種狀態或者資料給映射到某個值上的操做。git

本醬大概就解釋到這裏了,至於哈希的進一步認知包括衝突的產生和解決等,若是米娜桑不瞭解的話還請自行學習咕。థ౪థgithub

引子——子樹問題

這個不是我在實踐中遇到的問題,而是當年去某不做惡的大廠面試時候遇到的問題,以爲比較經典,因此就拿出來了。ᕙ༼ຈل͜ຈ༽ᕗ面試

問題描述

給定一棵二叉樹,假設每一個節點的數據只有左右子節點,自身並不存儲數據。請找出兩兩徹底相等的子樹們。算法

有興趣的童鞋能夠本身先思考一下。₍₍◝(・'ω'・)◟⁾⁾數據庫

個人作法

實際上我也不知道本身的作法是否是正確作法,不過既然經過了那一輪面試,想來也不會誤差到哪去喵。ლ(╹ε╹ლ)編程

作法大概以下:api

  1. 後序遍歷一遍整棵樹。

  2. 對於遍歷到每個節點,都獲取到左右子節點的哈希值,而後將其拼接從新計算出自身的哈希值,並返回給父親節點。

至於哈希值怎麼算,方法有不少。最簡單的就是設葉子節點一個哈希值,好比是 md5(""),而後每次非葉子節點的哈希值就用 md5(LEFT_HASH + RIGHT_HASH) 來計算。你們也能夠本身隨便想一種方法來作就行了。

不少人可能不解了,明明是用 md5,這篇文章是講哈希,有毛線關係。(╯°O°)╯┻━┻

實際上 md5 就是一種哈希算法,並且是很是經典的哈希算法。

典型的哈希算法包括 MD二、MD四、MD5 和 SHA-1 等。固然不侷限於這些,對於數字來講,取模也算是哈希算法,對於字符串狀態轉整數狀態哈希來講還有諸如 BKDRELF 等等。

若是你們想多瞭解一些字符串轉數字哈希的算法,能夠參考一下 BYVoid 的這篇《各類字符串Hash函數比較》,或者想直接在 Node.js 裏面使用的小夥伴們能夠光顧下這個包——bling-hashes

初步的輪廓已經明晰了,說白了就是將每一個節點的哈希全算出來,若是是父親節點就用子節點的哈希拼接起來再哈希一遍。σ`∀´)σ

把這些哈希算出來以後放在一個散列表裏面待查。若是一個算出來的哈希跟以前已有的哈希值相等,那麼就是說這個節點跟那個節點爲根節點的子樹有可能徹底相等。

注意:有可能徹底相等。

注意:只是有可能徹底相等。

注意:重要的事情說三遍,只是有可能徹底相等。

哈希是存在着必定的衝突機率的,因此說兩個相等的哈希所檢索到的源不必定同樣,因此咱們根據這些計算到的哈希創建哈希表,而後把表中同哈希值的子樹再兩兩同時遍歷一遍以檢驗是否相等。

  1. 同時遞歸,取兩個子樹的根節點。

  2. 後序遍歷,看看每一個節點是否是都同樣存在(或者不存在)左子節點以及存在(或者不存在)右子節點。

  3. 循環往復一直到兩兩遍歷完整棵樹獲得驗證結果。若是半路有一個節點的左右子節點狀態不同就能夠直接跳出遞歸返回 false

至此爲止,咱們能夠看出大概是兩大步——計算各子樹的哈希值驗證各同哈希子樹的相等性。不過稍微變通一下,咱們就能夠在計算出哈希值的時候就去跟之前的對比了。

剪枝

實際上上面的作法還有一個優化的方案,不過跟哈希相關性已經基本上很小了。不過仍是跟解決衝突有一丟丟的關係的,沒興趣的童鞋也能夠直接跳過了。(๑•́ ₃ •̀๑)

因爲子樹哈希值是存在必定的衝突機率的,因此兩個同哈希的子樹不必定相同。那麼咱們若是能一眼看出這樣的兩棵子樹是不相等的,就能夠省略驗證這一個遞歸的步驟了。

這裏有一種最顯而易見的狀況咱們是能夠忽略省略步驟的,那就是深度。

若是兩棵子樹兩兩徹底相等,那麼說明這倆基佬的深度(或者說高度)是同樣的,若是連深度都不同了還如何愉快搞基——因此說若是有兩個相等哈希值的子樹的深度不同的話能夠直接略過驗證步驟了。

那麼就能夠這麼作:

  1. 設全部葉子節點的深度爲 0,而後每往上一層加一。

  2. 遇到左右子節點深度不同的父節點時,取深度大的那個子節點深度去加一。

以上步驟在遍歷計算哈希的時候順便也作了,這樣就多了一個驗證標記了。

因此差很少就這樣了,淺嘗輒止。( ˘・з・)

引子的小結

就上述的場景來講,哈希很是好地將一個很是複雜的狀態轉化成一個能夠檢索的狀態。原本毫無頭緒的一個問題使用了哈希以後就徹底變成了一個檢索加驗證的過程了。

報告圖問題

這個問題就是我在大搜車中確實遇到的場景了。你們也不須要知道什麼是報告圖,就當它是一個代號了。

問題描述

要作的事情大概就是說給定一個報告,咱們根據報告的各個細節選定各類圖層而後揉成一團疊加在一塊兒造成最後一個結果圖。

其實原本就有個系統在作這件事情的——每來一個報告就生成一張圖,而後存儲好以後給前端使用。

我作的事情是將邏輯遷移到另外一套計算密集型任務集中處理系統中去。(´艸`)

其實生成這樣一張圖片的邏輯是 CPU 計算密集型的邏輯,因此比較耗費資源和時間的,那麼咱們就能在這上面作點手腳優化一下。

優化方法

首先咱們要知道的是,有哪些圖層是固定的,因此其實這算半個排列組合的問題了。

不過咱們也知道排列組合的增加性很是快,更況且我這裏有約 100 個圖層選擇,因此可能性很是多,一會兒全生成好不可能。

那麼就能夠用哈希和懶惰的思想來實現了。(ˇωˇ人)

雖然報告是有無限種可能的,可是把報告轉成圖層數據以後,擁有徹底同樣的圖層數據的報告就能夠用同一張圖片了,這樣就能夠大大節省空間和時間了。

其實大概的步驟很是簡單:

  1. 把圖層數據計算成哈希。(好比把全部圖層文件路徑用某種符號拼接,再用 md5 計算一下)

  2. 去數據庫查找這個哈希主鍵存不存在。

    • 若是存在則驗證源圖層數據域當前圖層數據是否吻合。

      • 若是不吻合則按某種算法從新計算哈希,繼續步驟 2。

      • 若是吻合則能夠直接拿着這個數據返回了,跳出計算。

    • 若是不存在就說明當前數據庫尚未這個圖層狀況的報告圖生成,那麼就執行生成報告圖邏輯。

  3. 報告圖生成以後,將其存入數據庫中。

    • 計算出這個報告圖圖層數據的哈希,去數據庫查存不存在。

      • 若是不存在則說明哈希不衝突,能用,直接用這個哈希存進去。

      • 若是存在則說明哈希衝突,那麼按某種算法從新計算哈希,繼續上面的步驟直到不衝突爲止。

若是你們想知道「按某種算法從新生成哈希」裏面「某種算法」的話能夠看看下面的瞎狗眼的說明了。(ノ◕ヮ◕)ノ*:・゚✧

其實很簡單,把圖層數據的這個字符串加某個固定字符當小尾巴,若是哈希仍是衝突則繼續加這個小尾巴,直到計算出來的哈希不衝突爲止。

好比我就用了這字符當小尾巴——?(麻將牌中的蘭)。(♛‿♛)

報告圖的小結

在這種場景中,我把哈希拿來做檢索某種報告圖是否已經生成的用途。若是沒有生成則生成一張,若是已經生成則直接拿已有的報告圖去用。

至少比原來的來一張報告就生成一張圖片來得快,而且省空間——至關於做冗餘處理了。

事實上在不少的網盤系統中也有做冗餘處理的。你覺得你有多少多少 T 的空間,實際上相同的文件最終在網盤系統裏面只存一份(不過排除備份的那些),而我相信作這些冗餘判斷的原理就是哈希了,SHA-1 也好 MD5 也好,反正就是這樣。

上面網盤的冗餘處理原理也只是個人猜想,我沒在那些廠子裏面工做過因此不能說就是就是這樣子的。歡迎指正。。゚ヽ(゚´Д`)ノ゚。

惟一主鍵問題

這是我來這邊工做後的另外一個小插曲了,遇到一個主鍵生成的小需求。

問題描述

有一個數據要插入到數據庫,因此要給它生成一個主鍵,可是需求比較奇葩,多是歷史遺留問題吧。(눈‸눈)

  • 非自增。

  • 是一個全是數字的字符串。

  • 不一樣類型的這個表的數據用不一樣的前綴,好比 101112 等。

  • 位數在十幾位左右(不過在我這裏就固定了)。

解決方案

若是是 前綴 + 隨機數 的衝突機率會比較大的,因此仍是用哈希來搞。

很是簡單。首先前綴是固定的,咱們就無論了,而後我根據此次進來的數據拼接成字符串(數據不會徹底同樣的),加上一點隨機鹽,而後用字符串哈希計算一遍,加上前導零,加上當前時間戳的後幾位拼接起來,最後接上前綴就行了。

這個 generate 函數看起來就像這樣子:

var bling = require("bling-hashes");
function generate(type, bodyParamStr) {
    var basePrefix;
    switch(type) {
        case 'foo': basePrefix = '10'; break;
        case 'bar': basePrefix = '11'; break;
        default: base_prefix = '00';
    }

    var date = moment();
    var hash = bling.bkdr(bodyParamStr + date.valueOf()).pad(10);
    hash = date.millisecond().pad(3) + hash;

    return basePrefix + hash;
};

注意:這裏的 bling 就是上面提到過的那個 bling-hashes,採用了 BKDR 算法來計算哈希。以及 Number.prototype.pad 函數是我邪惡得使用了 SugarJs 裏面的函數,就是加上前導零的意思。若是受「千萬不要修改原型鏈」影響較深地童鞋別學我哦。bodyParamStr 是前端傳過來的 Raw Form Data,它看起來像 "data1=1&data2=2&..."

最後獲得的這個字符串是咱們所要的主鍵了。。:.゚ヽ(*´∀`)ノ゚.:。

不過要注意的是,這個主鍵仍然又衝突的可能性,因此一旦衝突了(不管是本身檢測到的仍是插入數據庫的時候疼了)就須要再生產一遍。就目前來講再生成的時候毫秒時間戳後三位會不同,因此問題不大,容許存在的偏差——畢竟不是那種分分鐘集千萬條的數據,確定在 int 範圍內。若是到時候真出問題了再改進。

主鍵的小結

這裏的哈希是用在生成基本上沒有碰撞的主鍵身上,感受效果也是很是不錯的——前提是你也有這種奇葩需求。

真·小結

本文大體介紹了哈希的幾種用途,有多是你們熟知的用途,也有多是巧用,總之就是說了爲何我要用哈希。

在編程中,不管是實際用途仍是本身玩玩的題目,多動動腦子就會出來一些「奇技淫巧」。哈希也好,別的東西也罷,反正都是爲了解決問題的——千萬別由於實際開發中一般性的「並無什麼卵用」而去忽視它們,雖然哈希已是夠經常使用的了。(๑•ૅω•´๑)

相關文章
相關標籤/搜索