T2080 1.73GHz(双核) 笔记本 1.5GB 内存 Vista Ultra 特性全开(集成显卡)
15M字符/秒
不过即便经过精简,这个算法的代码也还是比一般的算法要复杂:除了算法本身所在的类,它还需要5个额外的数据结构,以及一个特殊枚举的支持。这几个额外的数据结构包括:
FastDict
FastList
FastQueue
FastWorkItem
FastScanResult
一个额外的枚举是
CharacterType
其中前面四个数据类,以及那个枚举对于本算法是至关重要的。下面首先讲解一下这几个数据结构和枚举:




























































其实,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个字符串的哈希值。同时,我们也不需要为每一次比较产生一个新的字符串了。下面是其中一部分的代码:































































































可以看到,上面这段代码确实是比普通的算法要复杂。提高效率自然是要付出代价的,有的时候我们可以通过多消耗点内存来达到,而更加根本、更加有效的还是提高算法的“复杂度”。比如快速排序,其算法复杂度就比普通的冒泡、插入排序要高得多。即便是多消耗再多的内存,冒泡排序在处理速度上仍然是无法比得过快速排序的。当然了,你可以说我的任务只需要每秒钟扫描3百万个字符,这个时候普通的算法就足以应付了。又或者例如我之前的情况,从晚上6点开始到早上6点之前,必须要把所有的内容都检索一遍,于是没功夫想一个更好的算法,有什么代码只要能工作,写起来又简单就好。在这样的任务条件下,也许TTMP-F算法是不适合的。而如果你现在已经有点时间,同时效率又是你最头痛的事情时,你还是不得不仔细看看上面的代码,况且我觉得还不算太复杂。
大部分的代码,我想大家还是轻易就能看明白的。关键有两个地方不太好理解:
1、为什么待处理项currentItem不放在队列中?"To do 1"是怎么实施的?
2、lastLowPart、LowPartDelta这些变量是什么意思?为什么要使用这些变量?
下面是部分的答案:
1、产生一个对象也许没有什么成本,但是如果产生上万次,累计起来的效率也是比较客观的。这还不是关键的,关键是整个核心就是和当前待处理的项目有关,如果我们把当前待处理项加入到队列当中,则我们每次要对一些当前条件做判断的时候,是否也需要从队列中取出来?即使我们用Peek,其对于效率的影响也是非常之高的,甚至该部分调用的次数,比产生currentItem的次数还要多。那么To do 1是怎么实施的呢?就算作我就留给大家的一个作业了。
2、如果大家看过我以前的文章,一定知道我这个人写东西有两个特点:一个是罗嗦,另一个就是爱吊人胃口。没错,放在下一集播出。其实不是我不想一口气写完,是我发现由于废话比较多的缘故,写得太长了——断续写了好几天了,也没有一个结束。想到这个问题,还是觉得先发布一下吧,免得我一不小心IE崩溃,这片心血就奔赴长江了……
那么,下一期的节目,还是会继续实战F模式。(还有好多代码没有贴出来哦……敬请期待)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?