设计思路:
(1)我们首先需要提出一个指标来衡量单词的相似程度,利用编辑距离,即字符串A经过插入,删除,改变三个操作变成字符串B需要花费的最少操作次数,利用动态规划即可解决.
(2)如何高效找出候选单词呢,建立字典树然后遍历比较是可以的,但是在单词数量大的时候效率很低。利用BK树可以避免大量的不必要比较,简单来说,A->B+B->C的距离一定大于等于A->C的距离,它满足三角不等式,由此可以推导出两边之和小于第三边的公式。假设我们希望A与B的距离小于N1,那么对于任意C,不妨假设AC距离是d,CB距离是X,有如下结论:
D+N=>D+Distance(A,B)>=x
X+Distance(A,B)>=d => X>= d-Distance(A,b) >= d – N
X+d>=Distance(A,B) = > x>=N-d
所以X一定位于(|d-n|,d+n)之间。一般来说,n相对比较小,不妨假设是2,那么x至多有5种可能,如果我们将每一个单词看作节点,每个距离看作边,显然可以构造一棵树,然后从当前节点出发只有5条边可以走,不妨假设5条边占全部数据的0.6,树有5层,那么需要比较的数据量就变成了0.6**5 = %7.7左右,极大的减少了不必要的比较。
(3)当我们获取到了一个单词候选集合之后,需要进行排序,如果简单的利用编辑距离来衡量优劣有些违反人的直觉,考虑这样两种情况:人们输错单词往往是在中间部分出错,一般来说开头几个字母是很少出错的,其次后缀一般来说比较好记,比如ing,ort,ence之类的后缀也较少出错。所以我们可以认为前缀和后缀匹配较好的单词可信程度较高,利用这个思路从两边分别匹配打分,再加上原始编辑距离的打分,就可以得到一个相对满意的结果。
实现细节:算法与逻辑部分
(1) 主要考虑一些corner case,
首先考虑输入单词能否查找到,如果可以就没有候选单词,如果查找不到,才需要进行搜索。
其次在搜索的时候如何选取我们期望的误差呢,这里有个陷阱,如果我们将误差选的太小会导致搜索不到任何结果,如果我们将误差选的太大,又容易造成搜索范围太大效率不高。
我的做法是:对于三个字母及以下的字符串,设置误差为1,其余设置为2。然后计算能够容忍的最大误差max_error = length*0.4,即最多有40%的单词不匹配。
接下来从初始误差开始枚举,每次误差加1,循环终止条件是大于最大误差或者候选集合不为0(这里也可以设置为某个阈值)
一般来讲编辑距离小于等于2的结果不为空的可能性非常大,所以需要进行一次以上的循环的可能极小。
(2) 搜索的时候如何选择候选集合?首先自然的想法是利用一个大顶堆来存储距离最小的k个节点,k可以取10左右,但是这导致一个不好的现象,很多满足编辑距离是2的节点无法进入满的大顶堆!比如spor,soprts匹配程度很高,但是saor,por,sprt等编辑距离都小于等于2,那么访问顺序的不同可能导致大顶堆很快满了,这样sports无法进入这个堆。解决方法就是把k设置为一个很大的值,这样可能会在一定程度上降低效率,但是结果的准确率更加重要.在这里要挑选出10个左右的最终单词,具体数量由候选集大小确定。
(3) 数据结构:主要需要实现一个大顶堆,那么就利用treemap即可实现,但是这里有一个陷阱,treemap不支持multi-key,但是这里的距离肯定会有重复的,所以我们还需要手动实现multi-map,利用arraylist作为value即可解决。
(4)考虑确实没有匹配的单词,输出No similar words
实现细节:内存资源方面
(1) 注意我们单词的来源,我的逻辑如下,首先检测指定文件是否存在,如果不存在,创建指定文件,从安桌的资源文件(运行时不是文件,是静态资源,直接嵌入APK文件)里面进行拷贝10000单词的默认字典到新文件。新的文件是内部文件,用户无法访问,保证安全性。
(2) 考虑一个接口让用户上传自己的字典。打开文件选择器,只能上传简单文本格式,我们进行分割单词(用空格以及逗号进行分割),然后直接把内容加载到我们的指定的文件(内部文件)。
(3) 从内部文件里面加载单词到内存,建立一颗BK树。注意两件事,释放以前的BK树,以及每次上传单词都要立即建立BK树。
实现细节:UI方面
这里实现的非常简单,输入字符串,按下Query就可以跳转到查询结果的页面,按下Upload就可以选择更新字典。输出的格式每最相似的单词第一行,接下来每5个单词进行换行,最多不超过10个单词。
未来的优化:
(1)首先如果有10万单词,我们都可以载入内存,但是如果数量过大比如1000万单词,那么就需要考虑使用别的手段,一个简单有效的办法就是利用Json来存储树而不是加载到内存里面,或者利用一些开源项目比如FlatBuffers来进行存储,总之不要在数据库存储,因为数据库里面建立树或者其他数据结构是非常麻烦而且低效的。
其次考虑部分载入的办法,比如说我们完全可以把内存当作一个缓存,每次把一部分的数据载入内存,当某一个单词查找不到足够好的候选集合时,就需要重新从磁盘里面重新载入新的数据进行查询。最简单的办法就是按照单词出现频率的高低来分割成很多小的block,然后每次启动的时候都载入出现频率最高的那个,交换完之后还要交换回来。
如果数据量大到磁盘无法存下,那么就每次考虑从网络下载部分数据,把磁盘当作更低一级别的缓存,这里完全可以用Json或者FlatBuffer进行存储。
(2)其次考虑输入单词集合的校验,如果字典里面有重复就会导致输入重复,这里我攒书没有处理,如果要确保正确,将输入的单词去重即可。
(3)打分的合理化,最后提一下,可以利用不同权重的编辑距离来衡量,比如第一个字母的相同会有更高的权重,这样也会引入很多更加复杂的算法,同时还可以配合后缀词组,前缀词组来缩小候选集合。还可以构造自动状态机,这样算法就更加复杂了,当然也可能更加高效和准确。
遇到的细节问题和整体花费的时间:
(1) 学习安桌开发的基本API,首先下载并且安装配置好IDE Android studio,然后参考官方的文档进行简单的学习。这里耗时6-8个小时左右。
(2) 没有用过java,解决办法就是临时在google上搜索,所以很多地方可能不高效和简洁,也可能没有选对java里面的数据结构,但是时间非常有限,所以也没有心思仔细研究了,这一部分学习时间大概在4-6个小时左右。
(3) IDE的调试和使用,这里非常耗时间,一个小的按钮就可以让你困惑2个小时,最后发现不是bug而是选项没有选对,但是这种问题相对不好搜索,很可能是自己对某个功能或者配置的理解有误差,只能自己摸索。这部分耗时间4-5个小时左右。
(4) 逻辑和算法的实现,和其他息息相关,不会调试,不会使用合适的API就很难写出正确的算法和逻辑。
(5) 最后就是测试+录制视频,由于模拟器不好用,所以最后在腾讯优测上面进行云手机测试,但是由于没有权限,所以上传的文件需要通过沙盒方式(在程序内部)来访问,这样我没办法直接通过文件选择器找到上传的文件,所以只能随便找了一个txt文件测试上传字典的功能。
耗时3个小时左右
总结:前后花费了4天时间,每天平均5-6个小时左右,整个项目还有很大的提升空间,迫于考试和时间限制,只能先粗略的完成基本功能。