我为何要使用哈希

什么是哈希(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 范围内。若是到时候真出问题了再改进。

主键的小结

这里的哈希是用在生成基本上没有碰撞的主键身上,感受效果也是很是不错的——前提是你也有这种奇葩需求。

真·小结

本文大体介绍了哈希的几种用途,有多是你们熟知的用途,也有多是巧用,总之就是说了为何我要用哈希。

在编程中,不管是实际用途仍是本身玩玩的题目,多动动脑子就会出来一些“奇技淫巧”。哈希也好,别的东西也罢,反正都是为了解决问题的——千万别由于实际开发中一般性的“并无什么卵用”而去忽视它们,虽然哈希已是够经常使用的了。(๑•ૅω•´๑)

相关文章
相关标签/搜索