其实,FastDict、FastList以及FastQueue本质上和.NET框架提供的没有什么差别——根本就没有什么代码嘛!没错,我主要是为了书写简便,把这些个泛型给封闭了。虽然是没有什么代码,这几个结构却是算法要处理的核心数据。当我们初始化这个关键字过滤引擎的时候,就需要把所有的关键字导入到FastDict/FastList数据结构,以及一个CharacterType数组当中。而当我们进行过滤的时候,就需要把一些待处理项转化为FastWorkItem,并放入一个FastQueue队列中。那么我们是如何初始化这个引擎的呢?
这段代码看着应该还算是简单,只是其中又是Dictionary又是List的,比较难搞明白其中的关系,之前的一些回复里面也有这样的问题。其实啊,之前的“理论如此”篇章已经解说过一遍了,可能太抽象,不太好理解。这里呢,我就从Hashtable的理论知识开始说起,以便对Hashtable还不是很了解的读者能够搞明白其中的关系。
当我们需要把一堆数据组织起来,比如说认为是一个集合,这个时候大家可以有很多的选择。比如说数组、链表、队列、栈、排序数等等,当然也包括了哈希表。我们把这些数据组织起来,必然面临如何往里面添加一个元素,如何遍历每一个元素,如何找到某一个元素,如何删除一个元素等等问题。这些问题都涉及了两个效率:空间效率和时间效率。比如说数组无疑是遍历速度最快的,存储空间最为节省的形式(不考虑压缩的情况下)。但是通常来讲,我们面临的问题并不是遍历所有数据的速度,而是——例如在本任务当中,查找某一个元素的速度如何。那么哈希表是如何提高这方面的效率的呢?简单的讲,就是首先计算数据的特征值,然后根据特征值来确定该数据应该存在于一个内部数组的什么位置(下标)。由于通常两个不同的数据,计算出来的特征值是不一样的,因此根据特征值来找到的位置通常也就是我们想要的数据所在的位置。例如我们有A和BC两个数据,特征值的计算函数就是该数据的ASCII拼起来,那么有:
数据A,特征值:0x41000000
数据BC,特征值:0x42430000
而内部数组大小为11个元素,则
数据A可能所在的位置是:0x41000000 % 11 = 6
数据B可能所在的位置是:0x42430000 % 11 = 9
很显然,A和BC两个数据是可以放在位置6和位置9,而不会互相干扰的。但是,有的时候,不同的数据是有可能产生相同的特征值的。很容易想象嘛,如果说用一个优先大小的特征值,就能够一一代表无穷尽的各种字符串组合,那么RAR就没有市场了,直接用特征值来“压缩”数据好了。同时,即便是不同的特征值,经过一个模运算计算该特征值在内部数组的位置时,仍然可能会产生相同的位置结果。例如数据AB,特征值是0x41420000,算出来的位置也是6。那么这个时候就叫做出现了冲突,有冲突就要解决,解决的办法有很多种。比如说再往下找一个空余的位置,或者干脆在该位置用一个列表来保存所有具有相同的位置计算结果的数据。说道这里,大家都应该明白了,Hashtable在添加(查找)一个数据的时候,其步骤为:
1、计算数据特征值(也就是哈希值)
2、根据特征值计算下标
3、看看该位置是否为空余状态
4、对于添加,如果空余,就可以直接添加了,否则需要解决冲突;
对于查找,如果空余就返回没有结果,否则还要对比一下是否就是要找的数据:如果不是,则看一下是否有“冲突”的数据在后面,直到查找到没有冲突数据为止。
其实上面说的是Hashtable的一种特殊形式,叫做HashSet(.NET 3.5里面有提供)。比较正式的Hashtable,实际上添加和查询的时候,是通过一个key来操作的,上述的那些计算也不是对数据,而是对这个key进行的。比如找到key为A的数据的过程,就变成通过计算key A的特征值,再根据特征值找到可能的位置,然后通过比对所有的冲突直到找到"A"这个key,然后再把这个位置上的具体数据返回出来。
从上面这一大队的描述,我们基本上应该可以看出来,一个包含了n个数据的哈希表的查找时间速度接近于O(c)。也就是说,基本上是和数据的数量多少没有太大关系的。而c的大小基本上是受下列因素影响的:哈希表本身的代码是否有效率;哈希值计算公式是否能有效的把数据特征值分散到每一个可能的数值上,可以想象,不好的公式会导致大量数据有相同的哈希值,也就是会产生大量的冲突;解决冲突的机制是否有效率,有的解决方法可能会导致接下来添加的数据也会产生冲突,或者找了半天还是没有找到空余的位置。
说了这半天好像还是没有解决你的困惑,别着急,马上就要说明白了:我们可以知道,由于存在冲突的可能性,在做查询的时候不能直接比对完特征值就确定该数据一定存在,因此必不可少的一步就是做“数据比对”。也就是要看看,根据数据AB的特征值0x41420000计算出来的位置上面,是否真的存着"AB"这个字符串。所以,如果我们直接使用HashTable或者HashSet,都会遇到一个问题:我们要直接提供一个数据"AB",交由HashTable/HashSet内部的算法来计算哈希值,以及进行冲突检查。因此,在我们的关键字过滤算法中,一旦触发“检索”,其代码就类似这样书写:
可以看到,这样的算法有这么几个问题(为了便于描述,我们假设在最糟糕的情况下):
1、我们要分别计算从currentIndex开始,长度为1到maxLength的每一个字符串的哈希值;
2、我们要分别对从currentIndex开始,长度为1到maxLength的每一个情况,取出一个字符串对象,也就是不得不产生maxLength个字符串对象,也就不得不复制1+2+...+maxLength个字符。
实际上,这些基本上都是不必要的,或者说是可以被优化掉的。要不要优化,我们就要看一下这样的优化是否值得:对于我那个280K字符的正常文本,检出关键字数量为1000个左右,但是触发检索的次数大概在10K到20K次左右。也就是说,如果采用上述算法,我们很可能至少得要为每一次检出关键字,多负担10次的额外运算。为了减少这样可观数量的不必要运算,我简单的对哈希表进行了一个拆解——把计算哈希值,以及进行字符串对比(冲突检查)的工作给提取出来,交由我的代码来进行运算。于是,就有了FastDict和FastList这两个基础数据结构了。
FastDict就是根据一个哈希值,给出该哈希值所对应的一个FastList。而FastList里面就保存着同一个哈希值所对应的所有字符串。换而言之,FastDict就是根据数据的特征值,找到的可能的数据所在位置,FastList则是用来解决冲突的。通过这样的改造,我们就有机会对偏移量从1到maxLength的字符 逐一扫描一遍,就可以分别得到长度为1到maxLength的maxLength个字符串的哈希值。同时,我们也不需要为每一次比较产生一个新的字符串了。下面是其中一部分的代码: