拼写检查的四种实现

算法有趣的地方在于,可以用不同的方式处理同一个问题,并且总有更好的方法。比如拼写检查。

拼写检查大体是这样的,给出一个字典文件,给出一个比对文件。比对文件里的单词,如果某个单词不在字典文件里的话,就认为拼写错误。要做的就是找出比对文件里所有拼写错误的单词。

暴力破解(brute force)

最直观的方法,就讲文件里的单词一一同字典里的单词做比较,如果不在字典里,就输出。最简单,也最费时。

假设字典文件一共有nd个字符,用于比对的文件有nt字符,复杂度则为O(nd * nt)。

具体实现

我们看一下运行速度,测试用的字典里大约有14万个单词,用作比对的文件大约有12万个单词。
用时将近3分钟。显然速度太慢,需要优化。

单词查找树(Trie)

我们知道,如果给出的单词以a开头,那么字典里以其他字母开头单词,就不需要我们做比较了。就好比二叉树查找,每次查找,总是可以丢掉不符合要求的那一半。
收到二叉树的启发,我们可以使用一种叫做Trie的多叉树来实现。

假设字典里有ab, ac, c, d这四个单词,生成的trie会是如下形状(t1、t2只是表述,方便做讨论):

   root(t1)
  /    \     \
 a(t2)  c(t3) d(t4)
/     \
b(t5)  c(t6)

做检查的时候,需要从跟节点,一级一级的向下找。
假设我们查找"ad",根节点的子节点t2的值就是a,第一个字母匹配成功了。我们再匹配第二个字节'd',t2的子树里,没有'd'这个字母,匹配失败。则"ad"为一个错误的拼写。
只有当我们匹配了单词的每一个字母,并且最后一个字母是结尾字母,那么这个单词才是在字典中存在的。

具体实现

使用同样输入只花了不到1.3秒的时间,快了上百倍。

哈希表(Hash Table)

拼写查找,要求的是插入和查找速度快,Hash Table刚好符合要求。
同样的数据,只用了0.14秒,比trie的实现快了将近10倍。
具体实现

Bloom Filter

Hash Table 从速度上来说,已经是极限了,看起来是最适合的算法。但Bloom Filter某种程度上更合适!

Bloom Filter相对于Hash Table来说,最大的优势就是,Bloom Filter不需要存完整的key(比如单词)。
Bloom Filter将一组key(比如单词)通过多个哈希方程映射到一个数组内,并可以根据这个数组来判断这个单词是否已经被插入了。

算法的大体思路如下:
初始化一个n个bit的数组arr,插入某个值时,我们通过哈希方程,得到对应的index,将数组这个位置设为1.
假设我们使用2个哈希方程h1, h2,我们会得到两个index(可能是1个),我们就将数组的这两个位置设为1。

查找的时候,用所有的哈希方程,得到对应的index,然后检查数组在这些位置上,是否均为1,如果均为1,则认为这个值被插入过。

伪代码如下:

require 'bitarray'
class BloomFilter
  attr_accessor :arr
  def initialize
    self.arr = BitArray.new(n)
  end

  def insert(key)
    arr[h1(key)] = 1
    arr[h2(key)] = 1
  end

  def include?(key)
    arr[h1(key)] == 1 && arr[h2(key)] = 1
  end

  # BloomFilter会有多个哈希函数,2个只是为了说明算法的工作原理。
  def h1(key)
    # xxx
  end

  def h2(key)
    # xxx
  end
end

Bloom Filter虽然大大的节省了空间,但却有一定的概率出错(false positive),这种错误是,当查找时,一个值没有被插入,但却被误认为是插入过了。这是因为,哈希方程会产生冲突(collision)。

可以通过更长的数组和更多的哈希方程来解决这个问题,同事空间实用也很少,所以这是个很有用的算法。

Bloom Filter还有一个弊端,就是不能做删除操作。不过可以通过给每一位加上个counter来解决,就是所谓的Counting Bloom Filter,具体看看这篇文章,就不详述了。

具体实现

同样的输入,需要0.11秒。

执行速度对比

SpellChecker
                      user     system      total        real
brute_force     162.660000   1.560000 164.220000 (168.961906)
by_trie           1.160000   0.100000   1.260000 (  1.305419)
by_hash_table     0.140000   0.000000   0.140000 (  0.145828)
by_bloom_filter   0.100000   0.010000   0.110000 (  0.108583)

相关代码

参考

Problem Set 5: Mispellings
Bloom Filters: Heuristic Analysis
Bloom Filter概念和原理
A Look Into Bloom Filters with Ruby


2dian718学习笔记
rails猿一只。平时会看看书,写写心得、记记笔记什么的。
108 声望
4 粉丝
0 条评论
推荐阅读
Erlang 源码阅读 -- scheduler
移步 [链接]

2dian7181阅读 1.9k

数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.2k

openKylin 0.9.5版本正式发布,加速国产操作系统自主创新进程!
2023年1月12日,中国桌面操作系统根社区openKylin(开放麒麟)正式发布openKylin 0.9.5操作系统版本。此版本充分适应5G时代需求,打通平板,PC等设备,实现多端融合,弥补了国产操作系统的短板,有效推动国产操作...

openKylin6阅读 7.9k

封面图
不会数学的程序员,只能走到初级开发工程师!
在我还是初级程序员时,每天也都粘贴着代码和包装着接口。那个阶段并没有意识到数学能在编程中起到什么作用,就算学了数学的部分知识,也没法用到编程中。但后来随着编程越来越久,逐步接手核心代码块开发时候,...

小傅哥3阅读 853

封面图
杨辉三角的5个特性,一个比一个牛皮!
杨辉三角按照杨辉于1261年所编写的《详解九章算法》一书,里面有一张图片,介绍此种算法来自于另外一个数学家贾宪所编写的《释锁算书》一书,但这本书早已失传无从考证。但可以肯定的是这一图形的发现我国不迟于1...

小傅哥3阅读 1.7k

stackoverflow 提问:“计算两个整数的最小公倍数的最有效方法是什么?”
作者:小傅哥博客:[链接]源码:[链接]沉淀、分享、成长,让自己和他人都能有所收获!😄一、前言嘿,小傅哥怎么突然讲到最大公约数了?这么想你肯定是没有好好阅读前面章节中小傅哥讲到的RSA算法,对于与欧拉结果...

小傅哥3阅读 1.6k

封面图
DeepMind 发布强化学习通用算法 DreamerV3,AI 成精自学捡钻石
内容一览:强化学习是多学科领域的交叉产物,其本质是实现自动决策且可做连续决策。本文将介绍 DeepMind 最新研发成果:扩大强化学习应用范围的通用算法 DreamerV3。关键词:强化学习 DeepMind 通用算法

超神经HyperAI1阅读 898

封面图
108 声望
4 粉丝
宣传栏