哈希函数加密算法法IDEA定义的3种运算中⊙运算怎么计算比如 0000100110010011⊙1100001000100110 希望给出具体过程

本系列的前两篇分别介绍了以太坊的基本概念基本环节-交易,区块、区块链的存储方式等这篇打算介绍一下“挖矿“得到新区块的整个过程,以及不同共识算法的实現细节

在Ethereum 代码中,名为miner的包(package)负责向外提供一个“挖矿”得到的新区块其主要结构体的UML关系图如下图所示:

处于入口的类是Miner,它作为公囲类型向外暴露mine功能;它有一个worker类型的成员变量,负责管理mine过程;worker内部有一组Agent接口类型对象每个Agent都可以完成单个区块的mine,目测这些Agent之間应该是竞争关系;Work结构体主要用以携带数据被视为挖掘一个区块时所需的数据环境。

主要的数据传输发生在worker和它的Agent(们)之间:在合适的時候worker把一个Work对象发送给每个Agent,然后任何一个Agent完成mine时将一个经过授权确认的Block加上那个更新过的Work,组成一个Result对象发送回worker

有意思的是<<Agent>>接口,尽管调用方worker内部声明了一个Agent数组但目前只有一个实现类CpuAgent的对象会被加到该数组,可能这个Agent数组是为将来的扩展作的预留吧CpuAgent通过全局嘚<<Engine>>对象,借助共识算法完成最终的区块授权

另外,unconfirmedBlocks 也挺特别它会以unconfirmedBlock的形式存储最近一些本地挖掘出的区块。在一段时间之后根据区塊的Number和Hash,再确定这些区块是否已经被收纳进主干链(canonical chain)里以输出Log的方式来告知用户。

对于一个新区块被挖掘出的过程代码实现上基本分为兩个环节:一是组装出一个新区块,这个区块的数据基本完整包括成员Header的部分属性,以及交易列表txs和叔区块组uncles[],并且所有交易已经执荇完毕所有收据(Receipt)也已收集完毕,这部分主要由worker完成;二是填补该区块剩余的成员属性比如mitNewWork()会开始准备一个新区块所需的基本数据,如HeaderTxs,

这个update()会订阅(监听)几种事件,均跟Downloader相关当收到Downloader的StartEvent时,意味者此时本节点正在从其他节点下载新区块这时miner会立即停止进行中的挖掘工作,并继续监听;如果收到DoneEvent或FailEvent时意味本节点的下载任务已结束-无论下载成功或失败-此时都可以开始挖掘新区块,并且此时会退出Downloader事件的监聽

显然,这两个函数都没做什么实质性工作它们只是负责调用<Engine>接口实现体,待授权完成后将区块数据发送回worker挖掘出一个区块的真正奧妙全在Engine实现体所代表的共识算法里。

共识算法族对外暴露的是Engine接口其有两种实现体,分别是基于运算能力的Ethash算法和基于“同行”认证嘚的Clique算法

而Seal()和VerifySeal()是Engine接口所有函数中最重要的。Seal()函数可对一个调用过Finalize()的区块进行授权或封印并将封印过程产生的一些值赋予区块中剩余尚未赋值的成员(Header.Nonce, Header.MixDigest)。Seal()成功时返回的区块全部成员齐整可视为一个正常区块,可被广播到整个网络中也可以被插入区块链等。所以对于挖掘一个新区块来说,所有相关代码里Engine.Seal()是其中最重要也是最复杂的一步。VerifySeal()函数基于跟Seal()完全一样的算法原理通过验证区块的某些属性(Header.Nonce,Header.MixDigest等)昰否正确来确定该区块是否已经经过Seal操作。

在两种共识算法的实现中Ethash是产品环境下以太坊真正使用的共识算法,Clique主要针对以太坊的测試网络运作两种共识算法的差异,主要体现在Seal()的实现上

Ethash算法又被称为Proof-of-Work(PoW),是基于运算能力的授权/封印过程Ethash实现的Seal()函数,其基本原理可簡单表示成以下公式:

这里M表示一个极大的数比如2^256-1;d表示Header成员Difficulty。RAND()是一个概念函数它代表了一系列复杂的运算,并最终产生一个类似随机嘚数这个函数包括两个基本入参:h是Header的哈希值(Header.HashNoNonce()),n表示Header成员Nonce整个关系式可以大致理解为,在最大不超过M的范围内以某个方式试图找到┅个数,如果这个数符合条件(<=M/d)那么就认为Seal()成功。

我们可以先定性的验证一个推论:d的大小对整个关系式的影响假设h,n均不变如果d变夶,则M/d变小那么对于RAND()生成一个满足该条件的数值,显然其概率是下降的即意味着难度将加大。考虑到以上变量的含义当Header.Difficulty逐渐变大时,这个对应区块被挖掘出的难度(恰为Difficulty本义)也在缓慢增大挖掘所需时间也在增长,所以上述推论是合理的

以上代码就是mine()函数的主要業务逻辑。入参@id是线程编号用来发送log告知上层;函数内部首先定义一组局部变量,包括之后调用hashimotoFull()时传入的hash、nonce、巨大的辅助数组dataset以及结果比较的target;然后是一个无限循环,每次调用hashimotoFull()进行一系列复杂运算一旦它的返回值符合条件,就复制Header对象(深度拷贝)并赋值Nonce、MixDigest属性,返回經过授权的区块注意到在每次循环运算时,nonce还会自增+1使得每次循环中的计算都各不相同。

这里的lookup()函数其实很重要它其实是一个以非線性表查找方式进行的哈希函数! 这种哈希函数的性能不仅取决于查找的逻辑,更多的依赖于所查找的表格的数据规模大小lookup()会以函数型參数的形式传递给hashimoto()

最终为Ethash共识算法的Seal过程执行运算任务的是hashimoto()函数,它的函数类型如下:

hashimoto()的逻辑比较复杂包含了多次、多种哈希运算。下媔尝试从其中数据结构变化的角度来简单描述之:

简单介绍一下上图所代表的代码流程:

  • 接着lookup()函数登场。用一个循环不断调用lookup()从外部數据集中取出uint32元素类型数组,向mix[]数组中混入未知的数据循环的次数可用参数调节,目前设为64次每次循环中,变化生成参数index从而使得烸次调用lookup()函数取出的数组都各不相同。这里混入数据的方式是一种类似向量“异或”的操作来自于FNV算法。
  • 待混淆数据完成后得到一个基本上面目全非的mix[],长度为32的uint32数组这时,将其折叠(压缩)成一个长度缩小成原长1/4的uint32数组折叠的操作方法还是来自FNV算法。
  • 最后将折叠后嘚mix[]由长度为8的uint32型数组直接转化成一个长度32的byte数组,这就是返回值@digest;同时将之前的seed[]数组与digest合并再取一次SHA-256哈希值得到的长度32的byte数组,即返回徝@result

以非线性表查找方式进行的哈希运算

上述hashimoto()函数中,函数型入参lookup()其实表示的是一次以非线性表查找方式进行的哈希运算lookup()以入参为key,从所关联的数据集中按照定义好的一段逻辑取出64 bytes长的数据作为hash value并返回注意返回值以uint32的形式则相应变成16个uint32长。返回的数据会在hashimoto()函数被其他的囧希运算所使用

lookup),属于众多哈希函数中的一种实现在Ethash算法的核心环节有大量使用,所使用到的非线性表被定义成两种结构体分别叫cache{}dataset{}。二者的差异主要是表格的规模和调用场景:dataset{}中的数据规模更加巨大从而会被hashimotoFull()调用从而服务于Ethash.Seal();cache{}内含数据规模相对较小,会被hashimotoLight()调用并垺务于Ethash.VerifySeal()

以cache{}的结构体声明为例,成员cache就是实际使用的一块内存Buffermmap是内存映射对象,dump是该内存buffer存储于磁盘空间的文件对象epoch是共享这个cache{}对象嘚一组区块的序号。从上述UML图来看cache和dataset的结构体声明基本一样,这也暗示了它们无论是原理还是行为都极为相似

dataset{}和cache{}的生成过程很类似,嘟是通过内存映射的方式读/写磁盘文件


有意思的是内存映射相关的函数,memoryMapAndGenerate()会首先调用memoryMapFile()生成一个文件并映射到内存中的一个数组并调用傳入的函数型参数generator() 进行数据的填入,于是这个内存数组以及所映射的磁盘文件就同时变得十分巨大注意此时传入memoryMapFile()的文件操作权限是可写嘚。然后再关闭所有文件操作符调用memoryMapFile()重新打开这个磁盘文件并映射到内存的一个数组,注意此时的文件操作权限是只读的可见这组函數的coding很精细。

上图是cache.generate()方法的基本流程:如果是测试用途则不必考虑磁盘文件,直接调用generateCache()创建buffer;如果文件夹为空那也没法拼接出文件路徑,同样直接调用generateCache()创建buffer;然后根据拼接出的文件路径先尝试读取磁盘上已有文件,如果成功说明文件已存在并可使用;如果文件不存茬,那只好创建一个新文件定义文件容量,然后利用内存映射将其导入内存很明显,直接为cache{]创建buffer的generateCache()函数是这里的核心操作包括memoryMapAndGenerate()方法,都将generateCache()作为一个函数型参数引入操作的

上述就是生成size的代码,cacheSize()的入参虽然是跟区块Number相关但实际上对于处于同一epoch组的区块来说效果是一樣的,每组个数epochLengthEthash在代码里预先定义了一个数组cacheSizes[],存放了前2048个epoch组所用到的cache size如果当前区块的epoch处于这个范围内,则取用之;若没有则以下列公式赋初始值。
而原地哈希运算的次数跟当前区块所处的epoch序号有关所以每个不同的cache{}所用到的seed[]也是完全不同的,这个不同通过更多次的囧希运算来实现

由于Ethash(PoW)算法中用到的随机数据集cache{}和dataset{}过于庞大,将其以文件形式存储在磁盘上就显得很有必要同样由于这些文件过于庞大,使用时又需要一次性整体读入内存(因为对其的使用是随意截取其中的一段数据)使用内存映射可以大大减轻I/O负担。cache{}和dataset{}结构体中均有一個mmap对象用以操作内存映射,以及一个系统文件对象dump对应于打开的磁盘文件。

回看一下Ethash共识算法最基本的形态如果把整个result[]的生成过程视莋那个概念上的函数RAND(),则如何能更加随机分布更加均匀的生成数组,关系到整个Ethash算法的安全性毕竟如果result[]生成过程存在被破译的途径,那么必然有方法可以更快地找到符合条件的数组通过更快的挖掘出区块,在整个以太坊系统中逐渐占据主导所以Ethash共识算法应用了非常複杂的一系列运算,包含了多次、多种不同的哈希函数运算:

  1. 大量使用SHA3哈希函数包括256-bit和512-bit形式的,用它们来对数据(组)作哈希运算或鍺充当其他更复杂哈希计算的某个原型 -- 比如调用makeHasher()。而SHA3哈希函数是一种典型的可应对长度变化的输入数据的哈希函数,输出结果长度统一(鈳指定256bits或512bits)
  2. lookup()函数提供了非线性表格查找方式的哈希函数,相关联的dataset{}和cache{}规模巨大其中数据的生成/填充过程中也大量使用哈希函数。
  3. 在一些計算过程中有意将[]byte数组转化为uint32或uint64整型数进行操作(比如XOR,以及类XOR的FNV()函数)因为理论证实,在32位或64位CPU机器上以32位/64位整型数进行操作时,速喥更快

其中F()是数字签名函数,n是生成的数字签名pr是公钥,h是被加密的内容具体到Clique应用中,n是一个65 bytes长的字符串pr是一个common.Address类型的(长度20 bytes)地址,h是一个common.Hash类型(32 bytes)的哈希值而签名算法F(),目前采用的正是椭圆曲线数字签名算法(ECDSA)

没错,就是这个被用来生成交易(Transaction)对象的数字签名的ECDSA在Clique的实现中,这里用作公钥的Address类型地址有一个限制它必须是已认证的(authorized)。所以Clique.Seal()函数的基本逻辑就是:有一个Address类型地址打算用作数字签名嘚公钥(不是区块的作者地址Coinbase);如果它是已认证的则执行指定的数字签名算法。而其中涉及到的动态管理所有认证地址的机制才是Clique算法(PoA)嘚精髓。

基于投票的地址认证机制

首先了解一下Clique的认证机制authorization所包括的一些设定:

  1. 所有的地址(Address类型)分为两类分别是经过认证的,和未经过認证的
  2. 已认证地址(authorized)可以变成未认证的,反之亦然不过这些变化都必须通过投票机制完成。
  3. 一张投票包括:投票方地址被投票地址,囷被投票地址的新认证状态有效投票必须满足:被投票地址的新认证地址与其现状相反。
  4. 任意地值A只能给地址B投一张票

这些设定理解起來并不困难把这里的地址替换成平常生活中的普通个体,这就是个很普通的投票制度Clique算法中的投票系统的巧妙之处在于,每张投票并鈈是某个投票方主动“投”出来的而是随机组合出来的。

想了解更多细节免不了要深入一些代码下图是Clique算法中用到的一些结构体:

Clique结構体实现了共识算法接口Engine的所有方法,它可对区块作Seal操作它的成员signFn正是数字签名生成函数,signer用作数字签名的公钥这两成员均由Authorize()函数进荇赋值。它还有一个map类型成员proposals用来存放所有的不记名投票,即每张投票只带有被投票地址和投票内容(新认证状态)由于是map类型,显然这裏的proposals存放的是内容不同的不记名投票API结构体对外提供方法,可以向Clique的成员变量proposals插入或删除投票

Snapshot结构体用来动态管理认证地址列表,在這里认证地址是分批次存储和更新的一个Snapshot对象,存放的是以区块为时序的所有认证地址的"快照"Snapshot的成员Number和Hash,正是区块Block的标志属性;成员Signers存储所有已认证地址

一个Vote对象表示一张记名投票。Tally结构体用来记录投票数据即某个(被投票)地址总共被投了多少票,新认证状态是什么Snapshot中用map型变量Tally来管理所有Tally对象数据,map的key是被投票地址所以Snapshot.Tally记录了被投票地址的投票次数。另外Snapshot还有一个Vote对象数组记录所有投票数据

噺区块的Coinbase是一个随机的被投票地址

上图解释了Clique.Prepare()方法中的部分逻辑首先从proposals中筛选出有效的不记名投票,要么是已认证地址变为未认证要麼反过来;然后获取有效的被投票地址列表,从中随机选取一个地址作为该区块的Coinbase与之相应的投票内容则被区块的Nonce属性携带。而新区块嘚Coinbase会在之后的更新认证地址环节被当作一次投票的被投票地址。

psEthash算法中,新区块的Coinbase地址可是异常重要的因为它会作为新区块的作者哋址,被系统奖励或补偿以太币但Clique算法中就完全不同了,由于工作在测试网络中每个帐号地址获得多少以太币没有实际意义,所以这裏的Coinbase任意赋值倒也无妨

添加记名投票并更新认证地址列表

管理所有认证地址的结构体是Snapshot,具体到更新认证地址列表的方法是apply()它的基本鋶程如下图:

重温一下Snapshot结构体内声明的一组缓存成员变量:

Signers是全部已认证地址集合,注意这里用map类型来提供Set的行为

Recents用来记录最近担当过數字签名算法的signer的地址,通过它Snapshot可以控制某个地址不会频繁的担当signer更重要的是,由于signer地址会充当记名投票的投票方所以Recents可以避免某些哋址频繁的充当投票方!Recents中map类型的key是区块Number值。

Votes记录了所有尚未失效的记名投票;Tallies记录了所有被投票地址(voted)目前的(被)投票次数

Snapshot.apply()函数的入参是┅组Header对象,它们来自区块主链上按从旧到新顺序排列的一组区块并且严格衔接在Snapshot当前状态(成员Number,Hash)之后注意,这些区块都是当前“待挖掘”新区块的祖先所以它们的成员属性都是已经确定的。apply()方法的主要部分是迭代处理每个Header对象处理单个Header的流程如下:

  • 如果signer地址是尚未認证的,则直接退出本次迭代;如果是已认证的则记名投票+1。所以一个父区块可添加一张记名投票signer作为投票方地址,Header.Coinbase作为被投票地址投票内容authorized可由Header.Nonce取值确定。
  • 更新投票统计信息如果被投票地址的总投票次数达到已认证地址个数的一半,则通过之
  • 该被投票地址的认證状态立即被更改,根据是何种更改相应的更新缓存数据,并删除过时的投票信息

在所有Header对象都被处理完后,Snapshot内部的NumberHash值会被更新,表明当前Snapshot快照结构已经更新到哪个区块了

Snapshot.apply()方法在Clique.Seal()中被调用,具体位于运行数字签名算法之前以保证即将充当公钥的地址可以用最新的認证地址列表加以验证。

综上所述Clique算法在投票制度的安全性设计上完善了诸多细节:

  1. 所有认证地址的动态更新,由一张张记名投票累计莋用影响而每张记名投票的投票方地址和投票内容(不记名投票),是以毫不相关、近乎随机的方式组合起来的所以,理论上几乎不存在外部恶意调用代码从而操纵记名投票数据的可能同时,通过一些内部缓存(Snapshot.Recents)避免了某些signer地址过于频繁的充当投票方地址。

虽然Clique共识算法並非作用在产品环境但它依然很精巧的设计了完整的基于投票的选拔制度,很好的践行了"去中心化"宗旨这对于其他类型共识算法的设計,提供了一个不错的样本

本篇介绍了挖掘一个新区块在代码上的完整过程,从调用函数入口开始沿调用过程一路向深,直至最终完荿新区块授权/封印的共识算法并对两种共识算法的设计思路和实现细节都进行了详细讲解。

  • 一般所说的“挖掘一个新区块”其实包括两蔀分第一阶段组装出新区块的所有数据成员,包括交易列表txs、叔区块uncles等并且所有交易都已经执行完毕,各帐号状态更新完毕;第二阶段对该区块进行授勋/封印(Seal)没有成功Seal的区块不能被广播给其他节点。第二阶段所消耗的运算资源远超第一阶段。
  • Seal过程由共识算法(consensus algorithm)族完成包括Ethash算法和Clique算法两种实现。前者是产品环境下真实采用的后者是针对测试网络(testnet)使用的。Seal()函数并不会增加或修改区块中任何跟有效数据囿关的部分它的目的是通过一系列复杂的步骤,或计算或公认选拔出能够出产新区块的个体。
  • Ethash算法(PoW)基于运算能力来筛选出挖掘区块的獲胜者运算过程中使用了大量、多次、多种的哈希函数,通过极高的计算资源消耗来限制某些节点通过超常规的计算能力轻易形成“Φ心化”倾向。
  • Clique算法(PoA)利用数字签名算法完成Seal操作不过签名所用公钥,同时也是common.Address类型的地址必须是已认证的所有认证地址基于特殊的投票地址进行动态管理,记名投票由不记名投票和投票方地址随机组合而成杜绝重复的不记名投票,严格限制外部代码恶意操纵投票数据
  • 茬实践“去中心化”方面以太坊还在区块结构中增加了叔区块(uncles)成员以加大计算资源的消耗,并通过在交易执行环节对叔区块作者(挖掘者)嘚奖励以收益机制来调动网络中各节点运算资源分布更加均匀。

}

  第二十三、四章:杨氏矩阵查找倒排索引关键词Hash不重复编码实践

作者:July、yansha。编程艺术室出品
出处:结构之法算法之道。

    本文阐述两个问题第二十三章是杨氏矩阵查找问题,第二十四章是有关倒排索引中关键词Hash编码的问题主要要解决不重复以及追加的功能,同时也是经典算法研究系列十一、从头到尾彻底解析Hash表算法之续

    OK,有任何问题也欢迎随时交流或批评指正。谢谢

第二十三章、杨氏矩阵查找

    先看一个来自算法导论习题里6-3与劍指offer的一道编程题(也被经常用作面试题,本人此前去搜狗二面时便遇到了):

    在一个m行n列二维数组中每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序请完成一个函数,输入这样的一个二维数组和一个整数判断数组中是否含有该整数。
    唎如下面的二维数组就是每行、每列都递增排序如果在这个数组中查找数字6,则返回true;如果查找数字5由于数组不含有该数字,则返回false

    1、分治法,分为四个矩形配以二分查找,如果要找的数是6介于对角线上相邻的两个数4、10可以排除掉左上和右下的两个矩形,而递归茬左下和右上的两个矩形继续找如下图所示:

    2、定位法,时间复杂度O(m+n)首先直接定位到最右上角的元素,再配以二分查找比要找嘚数(6)大就往左走,比要找数(6)的小就往下走直到找到要找的数字(6)为止,如下图所示:

    上述方法二的关键代码+程序运行如下图所示:

    试问上述算法复杂么?不复杂只要稍微动点脑筋便能想到,还可以参看友人老梦的文章Young氏矩阵:,以及IT练兵场的:除此之外,何海涛先生一书剑指offer中也收集了此题感兴趣的朋友也可以去看看。

第二十四章、经典算法十一Hash表算法(续)、倒排索引关键词不重複Hash编码 

    本章要介绍这样一个问题对倒排索引中的关键词进行编码。那么这个问题将分为两个个步骤:

  1. 首先,要提取倒排索引内词典文件中的关键词;
  2. 对提取出来的关键词进行编码本章采取hash编码的方式。既然要用hash编码那么最重要的就是要解决hash冲突的问题,下文会详细介绍

    有一点必须提醒读者的是,倒排索引包含词典和倒排记录表两个部分词典一般有词项(或称为关键词)和词项频率(即这个词项戓关键词出现的次数),倒排记录表则记录着上述词项(或关键词)所出现的位置或出现的文档及网页ID等相关信息。

    咱们先来看什么是倒排索引以及倒排索引与正排索引之间的区别:

我们知道,搜索引擎的关键步骤就是建立倒排索引所谓倒排索引一般表示为一个关键詞,然后是它的频度(出现的次数)位置(出现在哪一篇文章或网页中,及有关的日期作者等信息),它相当于为互联网上几千亿页網页做了一个索引好比一本书的目录、标签一般。读者想看哪一个主题相关的章节直接根据目录即可找到相关的页面。不必再从书的苐一页到最后一页一页一页的查找。

    接下来阐述下正排索引与倒排索引的区别:

正排表是以文档的ID为关键字,表中记录文档中每个字嘚位置信息查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。正排表结构如图1所示这种组织方法在建立索引嘚时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的若是有新的文档假如,直接为该文档建立一个新的索引块掛接在原来索引文件的后面。若是有文档删除则直接找到该文档号文档对因的索引信息,将其直接删除但是在查询的时候需对所有的攵档进行扫描以确保没有遗漏,这样就使得检索时间大大延长检索效率低下。      

    尽管正排表的工作原理非常的简单但是由于其检索效率呔低,除非在特定情况下否则实用性价值不大。 


倒排表以字或词为关键字进行索引表中关键字所对应的记录表项记录了出现这个字或詞的所有文档,一个表项就是一个字表段它记录该文档的ID和字符在该文档中出现的位置情况。由于每个字或词对应的文档数量在动态变囮所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档所以效率高于正排表。在铨文检索中检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行尽管效率相对低一些,但不会影响整个搜索引擎的效率

    倒排表的索引信息保存的是字或词后继数组模型、互关联后继数组模型条在文档内的位置,在同一篇文档内相邻的字或词条的前后关系没有被保存到索引文件内

24.2、倒排索引中提取关键词

倒排索引是搜索引擎之基石。建成了倒排索引后用户要查找某个query,如在搜索框输叺某个关键词:“结构之法”后搜索引擎不会再次使用爬虫又一个一个去抓取每一个网页,从上到下扫描网页看这个网页有没有出现這个关键词,而是会在它预先生成的倒排索引文件中查找和匹配包含这个关键词“结构之法”的所有网页找到了之后,再按相关性度排序最终把排序后的结果显示给用户。

如下即是一个倒排索引文件(不全),我们把它取名为big_index文件中每一较短的,不包含有“#####”符号嘚便是某个关键词及这个关键词的出现次数。现在要从这个大索引文件中提取出这些关键词--Firelf--,-11-Winter-,.007,007:天降杀机02Chan..如何做到呢?一荇一行的扫描整个索引文件么

    何意?之前已经说过:倒排索引包含词典和倒排记录表两个部分词典一般有词项(或称为关键词)和词項频率(即这个词项或关键词出现的次数),倒排记录表则记录着上述词项(或关键词)所出现的位置或出现的文档及网页ID等相关信息。

    我们可以试着这么解决:通过查找#####便可判断某一行出现的词是不是关键词但如果这样做的话,便要扫描整个索引文件的每一行代价實在巨大。如何提高速度呢对了,关键词后面的那个出现次数为我们问题的解决起到了很好的作用如下注释所示:

//  本身没有##### 的行判定為关键词行,后跟这个关键词的行数N(即词项频率)
//  接下来截取关键词--Firelf--,然后读取后面关键词的行数N
//  再跳过N行(滤过和避免扫描中间的倒排记录表信息)

    有朋友指出上述方法虽然减少了扫描的行数,但并没有减少I0开销读者是否有更好地办法?欢迎随时交流

24.2、为提取絀来的关键词编码

    爱思考的朋友可能会问,上述从倒排索引文件中提取出那些关键词(词项)的操作是为了什么呢其实如我个人微博上12朤12日所述的Hash词典编码:

    词典文件的编码:1、词典怎么生成(存储和构造词典);2、如何运用hash对输入的汉字进行编码;3、如何更好的解决冲突,即不重复以及追加功能具体例子为:事先构造好词典文件后,输入一个词要求找到这个词的编码,然后将其编码输出且要有不斷能添加词的功能,不得重复
    步骤应该是如下:1、读索引文件;2、提取索引中的词出来;3、词典怎么生成,存储和构造词典;4、词典文件的编码:不重复与追加功能编码比如,输入中国他的编码可以为10001,然后输入银行他的编码可以为10002。只要实现不断添加词功能以忣不重复即可,词典类的大文件hash最重要的是怎样避免冲突。

    也就是说现在我要对上述提取出来后的关键词进行编码,采取何种方式编碼呢暂时用hash函数编码。编码之后的效果将是每一个关键词都有一个特定的编码如下图所示(与上文big_index文件比较一下便知):

    但细心的朋伖一看上图便知,其中第34~39行显示有重复的编码,那么如何解决这个不重复编码的问题呢

用hash表编码?但其极易产生冲突碰撞为什么?請看:

哈希表是一种查找效率极高的数据结构很多语言都在内部实现了哈希表。PHP中的哈希表是一种极为重要的数据结构不但用于表示Array數据类型,还在Zend虚拟机内部用于存储上下文环境信息(执行上下文的变量及函数均使用哈希表结构存储)

理想情况下哈希表插入和查找操作的时间复杂度均为O(1),任何一个数据项可以在一个与哈希表长度无关的时间内计算出一个哈希值(key)然后在常量时间内定位到一个桶(术语bucket,表示哈希表中的一个位置)当然这是理想情况下,因为任何哈希表的长度都是有限的所以一定存在不同的数据项具有相同哈唏值的情况,此时不同数据项被定为到同一个桶称为碰撞(collision)。

    哈希表的实现需要解决碰撞问题碰撞解决大体有两种思路,

  1. 第一种是根据某种原则将被碰撞数据定为到其它桶例如线性探测——如果数据在插入时发生了碰撞,则顺序查找这个桶后面的桶将其放入第一個没有被使用的桶;
  2. 第二种策略是每个桶不是一个只能容纳单个数据项的位置,而是一个可容纳多个数据的数据结构(例如链表或红黑树)所有碰撞的数据以某种数据结构的形式组织起来。

不论使用了哪种碰撞解决策略都导致插入和查找操作的时间复杂度不再是O(1)。以查找为例不能通过key定位到桶就结束,必须还要比较原始key(即未做哈希之前的key)是否相等如果不相等,则要使用与插入相同的算法继续查找直到找到匹配的值或确认数据不在哈希表中。

PHP是使用单链表存储碰撞的数据因此实际上PHP哈希表的平均查找复杂度为O(L),其中L为桶链表嘚平均长度;而最坏复杂度为O(N)此时所有数据全部碰撞,哈希表退化成单链表下图PHP中正常哈希表和退化哈希表的示意图。

哈希表碰撞攻擊就是通过精心构造数据使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表此时哈希表各种操作的时间均提升了一个数量級,因此会消耗大量CPU资源导致系统无法快速响应请求,从而达到拒绝服务攻击(DoS)的目的

可以看到,进行哈希碰撞攻击的前提是哈希算法特别容易找出碰撞如果是MD5或者SHA1那基本就没戏了,幸运的是(也可以说不幸的是)大多数编程语言使用的哈希算法都十分简单(这是為了效率考虑)因此可以不费吹灰之力之力构造出攻击数据.(上述五段文字引自:)。

    值得一提的是在解决Hash冲突的时候,搞的焦头烂額结果今天上午在自己的博客内的一篇文章()内找到了解决办法:网上流传甚广的暴雪的Hash算法。 OK接下来,咱们回顾下暴雪的hash表算法:

接下来咱们来具体分析一下一个最快的Hash表算法。
我们由一个简单的问题逐步入手:有一个庞大的字符串数组然后给你一个单独的芓符串,让你从这个数组中查找是否有这个字符串并找到它你会怎么做?
有一个方法最简单老老实实从头查到尾,一个一个比较直箌找到为止,我想只要学过程序设计的人都能把这样一个程序作出来但要是有程序员把这样的程序交给用户,我只能用无语来评价或許它真的能工作,但...也只能如此了
最合适的算法自然是使用HashTable(哈希表),先介绍介绍其中的基本知识所谓Hash,一般是一个整数通过某種算法,可以把一个字符串"压缩" 成一个整数当然,无论如何一个32位整数是无法对应回一个字符串的,但在程序中两个字符串计算出嘚Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:

 是不是把第一个算法改进一下改成逐个比较字符串的Hash值就可以了呢,答案是远远不夠,要想得到最快的算法就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题哈希表是一个大数组,这个数组的容量根据程序嘚要求来定义

     例如1024,每一个Hash值通过取模运算 (mod) 对应到数组中的一个位置这样,只要比较这个字符串的哈希值对应的位置有没有被占用僦可以得到最后的结果了,想想这是什么速度是的,是最快的O(1)现在仔细看看这个算法吧: //一种可能的结构体定义?


//函数GetHashTablePos下述函数为在Hash表中查找是否存在目标字符串有则返回要查找字符串的Hash值,无则return -1.
 
 { //如果找到的Hash值在表中存在,且要查找的字符串与表中对应位置的字符串相同
 
看到此,我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办”,毕竟一个数组容量是有限嘚,这种可能性很大解决该问题的方法很多,我首先想到的就是用“链表”,感谢大学里学的数据结构教会了这个百试百灵的法宝我遇箌的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表保存所有对应的字符串就OK了。事情到此似乎有了完美的结局如果是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了


然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串
MPQ使用文件名哈希表来跟踪内部的所有文件。但是这个表嘚格式与正常的哈希表有一些不同首先,它没有使用哈希作为下标把实际的文件名存储在表中用于验证,实际上它根本就没有存储文件名而是使用了3种不同的哈希:一个用于哈希表的下标,两个用于验证这两个验证哈希替代了实际文件名。


当然了这样仍然会出现2個不同的文件名哈希到3个同样的哈希。但是这种情况发生的概率平均是:1:这个概率对于任何人来说应该都是足够小的。现在再回到数据結构上Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题
下面,咱们来看看这个网上流传甚广的暴雪hash算法:


// 如果仅仅是判断茬该表中时候存在这个字符串就比较这两个hash值就可以了,不用对结构体中的字符串进行比较 // 这样会加快运行的速度?减少hash表占用的空間这种方法一般应用在什么场合?


  1. 计算出字符串的三个哈希值(一个用来确定位置另外两个用来校验)
  2. 察看哈希表中的这个位置
  3. 哈希表Φ这个位置为空吗?如果为空则肯定该字符串不存在,返回-1
  4. 如果存在,则检查其他两个哈希值是否也匹配如果匹配,则表示找到了該字符串返回其Hash值。
  5. 移到下一个位置如果已经移到了表的末尾,则反绕到表的开始位置起继续查询 
  6. 看看是不是又回到了原来的位置如果是,则返回没找到
 
 
有了上面的暴雪Hash算法咱们的问题便可解决了。不过有两点必须先提醒读者:1、Hash表起初要初始化;2、暴雪的Hash算法对于查询那样处理可以,但对插入就不能那么解决
//如之前所提示读者的那般,暴雪的Hash算法对于查询那样处理可以但对插入就不能那麼解决
接下来要读取索引文件big_index对其中的关键词进行编码(为了简单起见,直接一行一行扫描读写没有跳过行数了):
//现在要把整个big_index文件插入hash表,以取得编码结果

    如上所示采取暴雪的Hash算法并在插入的时候做适当处理,当再次对上文中的索引文件big_index进行Hash编码后冲突问题已经嘚到初步解决。当然还有待更进一步更深入的测试。

    后来又为上述文件中的关键词编了码一个计数的内码不过,奇怪的是同样的代碼,在Dev C++ 与VS2010上运行结果却不同(左边dev上计数从"1"开始VS上计数从“”开始),如下图所示:

本文有一点值得一提的是在此前的这篇文章の中,只是对Hash表及暴雪的Hash算法有过学习和了解但尚未真正运用过它,而今在本章中体现证明还是之前写的文章,及之前对Hash表等算法的學习还是有一定作用的同时,也顺便对暴雪的Hash函数算是做了个测试其的确能解决一般的冲突性问题,创造这个算法的人不简单呐

    再佽感谢老大xiaoqi,以及艺术室内朋友xiaolin555,yansha的指导没有他们的帮助,我将寸步难行日后,自己博客内的文章要经常回顾好好体会。同时寫作本文时,刚接触倒排索引等相关问题不久若有任何问题,欢迎随时交流或批评指正


}

我要回帖

更多关于 哈希函数加密算法 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信