所以我需要编写一个有效的算法来查找字典中缺少字母的单词,我想要一组可能的单词。
例如,如果我有这个,我可能会回复这些,那些,主题,那些。
我想知道是否有人可以建议我应该使用的一些数据结构或算法。
谢谢!
编辑:特里太空间效率太低,而且速度太慢。还有其他想法修改吗?更新:最多会出现两个问号,当出现两个问号时,它们会按顺序出现。
目前我正在使用3个哈希表,当它是完全匹配,1个问号和2个问号时。 给定一个字典我会散列所有可能的单词。例如,如果我有单词WORD。我哈希WORD,?ORD,W?RD,WO?D,WOR ?, ?? RD,W ?? D,WO ??。进入字典。然后我使用链接列表将冲突链接在一起。所以说hash(W?RD)= hash(STR?NG)= 17. hashtab(17)将指向WORD,WORD指向STRING,因为它是一个链表。
平均查找一个单词的时间约为2e-6s。我希望做得更好,最好是1e-9。
编辑:我没有再看过这个问题,但是3m条目插入需要0.5秒,3m条目查找需要4秒。
谢谢!
答案 0 :(得分:66)
我相信在这种情况下,最好只使用一个平面文件,其中每个单词代表一行。通过这种方式,您可以方便地使用正则表达式搜索的强大功能,该搜索功能经过高度优化,可能会超出您为此问题设计的任何数据结构。
这是针对此问题的Ruby代码:
def query(str, data)
r = Regexp.new("^#{str.gsub("?", ".")}$")
idx = 0
begin
idx = data.index(r, idx)
if idx
yield data[idx, str.size]
idx += str.size + 1
end
end while idx
end
start_time = Time.now
query("?r?te", File.read("wordlist.txt")) do |w|
puts w
end
puts Time.now - start_time
文件wordlist.txt
包含45425个字(可下载here)。查询?r?te
的程序输出是:
brute
crate
Crete
grate
irate
prate
write
wrote
0.013689
因此,读取整个文件并查找其中的所有匹配项只需37毫秒。并且它可以很好地适用于各种查询模式,即使Trie非常慢:
查询????????????????e
counterproductive
indistinguishable
microarchitecture
microprogrammable
0.018681
查询?h?a?r?c?l?
theatricals
0.013608
这对我来说足够快。
如果你想更快,你可以将wordlist分成包含相同长度的单词的字符串,然后根据你的查询长度搜索正确的单词。用以下代码替换最后5行:
def query_split(str, data)
query(str, data[str.length]) do |w|
yield w
end
end
# prepare data
data = Hash.new("")
File.read("wordlist.txt").each_line do |w|
data[w.length-1] += w
end
# use prepared data for query
start_time = Time.now
query_split("?r?te", data) do |w|
puts w
end
puts Time.now - start_time
构建数据结构现在大约需要0.4秒,但所有查询的速度都要快10倍(取决于具有该长度的单词数):
?r?te
0.001112秒?h?a?r?c?l?
0.000852 sec ????????????????e
0.000169 sec 由于您已经更改了要求,因此您可以轻松扩展您的想法,只使用一个包含所有预先计算结果的大哈希表。但是,您可以依靠正确实现的哈希表的性能,而不是自己处理冲突。
在这里,我创建了一个大的哈希表,每个可能的查询都映射到其结果列表:
def create_big_hash(data)
h = Hash.new do |h,k|
h[k] = Array.new
end
data.each_line do |l|
w = l.strip
# add all words with one ?
w.length.times do |i|
q = String.new(w)
q[i] = "?"
h[q].push w
end
# add all words with two ??
(w.length-1).times do |i|
q = String.new(w)
q[i, 2] = "??"
h[q].push w
end
end
h
end
# prepare data
t = Time.new
h = create_big_hash(File.read("wordlist.txt"))
puts "#{Time.new - t} sec preparing data\n#{h.size} entries in big hash"
# use prepared data for query
t = Time.new
h["?ood"].each do |w|
puts w
end
puts (Time.new - t)
输出
4.960255 sec preparing data
616745 entries in big hash
food
good
hood
mood
wood
2.0e-05
查询性能为O(1),它只是哈希表中的查找。时间2.0e-05可能低于计时器的精度。运行1000次时,每次查询平均得到1.958e-6秒。为了更快地实现它,我将切换到C ++并使用Google Sparse Hash,它具有极高的内存效率和快速。
以上所有解决方案都适用,并且对于许多用例而言应该足够好。如果你真的想要认真并且有很多空闲时间,请阅读一些好文章:
答案 1 :(得分:24)
考虑到目前的限制:
我有两个可行的解决方案:
您可以使用哈希,哪些键是您的单词,最多两个'?',值是一个拟合词列表。这个哈希将有大约100,000 + 100,000 * 6 + 100,000 * 5 = 1,200,000个条目(如果你有2个问号,你只需要找到第一个的位置......)。每个条目都可以保存单词列表或指向现有单词的指针列表。如果你保存一个指针列表,我们假设每个单词平均少于20个单词与两个'?'匹配,那么附加内存小于20 * 1,200,000 = 24,000,000。
如果每个指针大小为4个字节,则此处的内存要求为(24,000,000 + 1,200,000)* 4个字节= 100,800,000个字节〜= 96兆字节。
总结这个解决方案:
注意:如果你想使用较小尺寸的哈希,你可以,但最好是在每个条目而不是链表中保存一个平衡的搜索树,以获得更好的性能。
此解决方案使用以下观察:
如果'?'这个词的结尾处是一个标志,trie将是一个很好的解决方案。
在trie中搜索将搜索单词的长度,对于最后几个字母,DFS遍历将带来所有结尾。 非常快速,非常精通内存的解决方案。
所以让我们使用这个观察,以便建立一个完全像这样的工作。
您可以考虑字典中的每个单词,作为以@结尾的单词(或字典中不存在的任何其他符号)。 所以“空间”这个词就是“空间@”。 现在,如果您使用“@”符号旋转每个单词,则会得到以下结果:
space@, pace@s, ace@sp, *ce@spa*, e@spac
(没有@作为第一个字母)。
如果您将所有这些变体插入到TRIE中,您可以通过“旋转”您的单词轻松找到您在单词长度上搜索的单词。
实施例: 你想找到所有符合's ?? ce'的词(其中一个是空格,另一个是切片)。 你建立了这个词:s ?? ce @,然后旋转它以便?标志到底是什么时候。即'ce @ s ??'
所有旋转变化都存在于trie中,特别是'ce @ spa'(用*标记为*)。在找到开始之后 - 您需要以适当的长度检查所有延续,并保存它们。然后,你需要再次旋转它们,以便@是最后一个字母,而walla - 你有你要找的所有单词!
总结这个解决方案:
内存消耗: 对于每个单词,它的所有旋转都出现在trie中。平均而言,内存大小的* 6保存在trie中。 trie大小约为* 3(只是猜测...)其中保存的空间。因此,此trie所需的总空间为6 * 3 * 100,000 = 1,800,000字〜= 6.8兆字节。
每次搜索的时间:
总结一下,它非常快,取决于字长*小常数。
第二种选择具有很好的时间/空间复杂性,并且是您使用的最佳选择。第二种解决方案存在一些问题(在这种情况下,您可能希望使用第一种解决方案):
答案 2 :(得分:22)
对我来说,这个问题听起来非常适合Trie数据结构。在整个字典中输入整个字典,然后查找单词。对于丢失的字母,您必须尝试所有子尝试,使用递归方法应该相对容易。
编辑:我刚才在Ruby中写了一个简单的实现:http://gist.github.com/262667。
答案 3 :(得分:16)
Directed Acyclic Word Graph将是此问题的完美数据结构。它结合了trie的效率(trie可以看作是DAWG的一个特例),但更节省空间。典型的DAWG将占用带有单词的纯文本文件的一小部分。
枚举满足特定条件的单词很简单,就像在trie中一样 - 你必须以深度优先的方式遍历图形。
答案 4 :(得分:9)
安娜的第二个解决方案就是这个解决方案的灵感来源。
首先,将所有单词加载到内存中,然后根据单词长度将字典分成几部分。
对于每个长度,使 n 复制指向单词的指针数组。对每个数组进行排序,以便在旋转一定数量的字母时,字符串按顺序显示。例如,假设5个字母单词的原始列表是[plane,apple,space,train,happy,stack,hacks]。然后你的五个指针数组将是:
rotated by 0 letters: [apple, hacks, happy, plane, space, stack, train]
rotated by 1 letter: [hacks, happy, plane, space, apple, train, stack]
rotated by 2 letters: [space, stack, train, plane, hacks, apple, happy]
rotated by 3 letters: [space, stack, train, hacks, apple, plane, happy]
rotated by 4 letters: [apple, plane, space, stack, train, hacks, happy]
(而不是指针,你可以使用识别单词的整数,如果这样可以节省你平台上的空间。)
要进行搜索,只需询问旋转模式所需的量,以便最后显示问号。然后你可以在适当的列表中进行二进制搜索。
如果你需要找到?? ppy的匹配项,你必须将它旋转2才能生成ppy ??。因此,当旋转2个字母时,请查看按顺序排列的数组。快速二进制搜索发现“快乐”是唯一的匹配。
如果你需要找到匹配的东西,你必须将它旋转4才能生成gth ??所以看看数组4,其中二进制搜索发现没有匹配。
无论有多少问号,只要它们都出现在一起,这就有效。
需要空间除了字典本身之外:对于长度为N的单词,这需要空间(长度为N的字数的N倍)指针或整数。
每次查询的时间:O(log n)其中n是适当长度的字数。
在Python中实施:
import bisect
class Matcher:
def __init__(self, words):
# Sort the words into bins by length.
bins = []
for w in words:
while len(bins) <= len(w):
bins.append([])
bins[len(w)].append(w)
# Make n copies of each list, sorted by rotations.
for n in range(len(bins)):
bins[n] = [sorted(bins[n], key=lambda w: w[i:]+w[:i]) for i in range(n)]
self.bins = bins
def find(self, pattern):
bins = self.bins
if len(pattern) >= len(bins):
return []
# Figure out which array to search.
r = (pattern.rindex('?') + 1) % len(pattern)
rpat = (pattern[r:] + pattern[:r]).rstrip('?')
if '?' in rpat:
raise ValueError("non-adjacent wildcards in pattern: " + repr(pattern))
a = bins[len(pattern)][r]
# Binary-search the array.
class RotatedArray:
def __len__(self):
return len(a)
def __getitem__(self, i):
word = a[i]
return word[r:] + word[:r]
ra = RotatedArray()
start = bisect.bisect(ra, rpat)
stop = bisect.bisect(ra, rpat[:-1] + chr(ord(rpat[-1]) + 1))
# Return the matches.
return a[start:stop]
words = open('/usr/share/dict/words', 'r').read().split()
print "Building matcher..."
m = Matcher(words) # takes 1-2 seconds, for me
print "Done."
print m.find("st??k")
print m.find("ov???low")
在我的计算机上,系统字典大小为909KB,除了存储字(指针为4字节)之外,该程序还使用大约3.2MB的内存。对于这个字典,你可以通过使用2字节整数而不是指针来减少一半,因为每个长度的字数少于2个 16 。
测量:在我的计算机上,m.find("st??k")
运行0.000032秒,m.find("ov???low")
运行0.000034秒,m.find("????????????????e")
运行0.000023秒。
通过写出二进制搜索而不是使用class RotatedArray
和bisect
库,我将前两个数字降低到0.000016秒:快两倍。用C ++实现它会使它更快。
答案 5 :(得分:4)
首先,我们需要一种方法来将查询字符串与给定条目进行比较。让我们假设使用正则表达式的函数:matches(query,trialstr)
。
O(n)算法将简单地遍历每个列表项(您的字典将在程序中表示为列表),将每个列表项与查询字符串进行比较。
通过一些预先计算,您可以通过为每个字母构建一个额外的单词列表来改进大量查询,因此您的字典可能如下所示:
wordsbyletter = { 'a' : ['aardvark', 'abacus', ... ],
'b' : ['bat', 'bar', ...],
.... }
但是,这将是有限的用途,特别是如果您的查询字符串以未知字符开头。因此,我们可以通过注意特定字母在特定字母中的位置来做得更好,生成:
wordsmap = { 'a':{ 0:['aardvark', 'abacus'],
1:['bat','bar']
2:['abacus']},
'b':{ 0:['bat','bar'],
1:['abacus']},
....
}
正如您所看到的,如果不使用索引,最终会大大增加所需的存储空间量 - 特别是n个字的字典和平均长度m将需要nm 2 的存储空间。但是,您现在可以非常快速地查找每个可以匹配的所有单词。
最后的优化(你可以在天真的方法中使用它)也是将相同长度的所有单词分成不同的商店,因为你总是知道单词有多长。
此版本为O( kx ),其中 k 是查询字中已知字母的数量, x = x ( n )是在实现中查找长度为 n 的字典中的单个项目的时间(通常日志(名词的)。
所以最后的字典如:
allmap = {
3 : {
'a' : {
1 : ['ant','all'],
2 : ['bar','pat']
}
'b' : {
1 : ['bar','boy'],
...
}
4 : {
'a' : {
1 : ['ante'],
....
然后我们的算法就是:
possiblewords = set()
firsttime = True
wordlen = len(query)
for idx,letter in enumerate(query):
if(letter is not '?'):
matchesthisletter = set(allmap[wordlen][letter][idx])
if firsttime:
possiblewords = matchesthisletter
else:
possiblewords &= matchesthisletter
最后,集合possiblewords
将包含所有匹配的字母。
答案 6 :(得分:3)
如果生成与模式匹配的所有可能单词(arate,arbte,arcte ... zryte,zrzte),然后在字典的二叉树表示中查找它们,那将具有<的平均性能特征< strong> O(e ^ N1 * log(N2))其中N1是问号的数量,N2是字典的大小。对我来说似乎足够好,但我确信可以找出更好的算法。
编辑:如果您有三个以上的问号,请查看Phil H的回答和他的信件索引方法。
答案 7 :(得分:3)
假设你有足够的内存,你可以构建一个巨大的hashmap来提供恒定时间的答案。这是python中的一个简单示例:
from array import array
all_words = open("english-words").read().split()
big_map = {}
def populate_map(word):
for i in range(pow(2, len(word))):
bin = _bin(i, len(word))
candidate = array('c', word)
for j in range(len(word)):
if bin[j] == "1":
candidate[j] = "?"
if candidate.tostring() in big_map:
big_map[candidate.tostring()].add(word)
else:
big_map[candidate.tostring()] = set([word])
def _bin(x, width):
return ''.join(str((x>>i)&1) for i in xrange(width-1,-1,-1))
def run():
for word in all_words:
populate_map(word)
run()
>>> big_map["y??r"]
set(['your', 'year'])
>>> big_map["yo?r"]
set(['your'])
>>> big_map["?o?r"]
set(['four', 'poor', 'door', 'your', 'hour'])
答案 8 :(得分:2)
你可以看看它在aspell中是如何完成的。它提示错误拼写单词的正确单词的建议。
答案 9 :(得分:2)
如果80-90%的准确度是可以接受的,你可以使用Peter Norvig的spell checker来管理。实施小而优雅。
答案 10 :(得分:2)
构建所有单词的哈希集。要查找匹配项,请使用每个可能的字母组合替换模式中的问号。如果有两个问号,则查询包含26个 2 = 676个快速,恒定预期时间的哈希表查找。
import itertools
words = set(open("/usr/share/dict/words").read().split())
def query(pattern):
i = pattern.index('?')
j = pattern.rindex('?') + 1
for combo in itertools.product('abcdefghijklmnopqrstuvwxyz', repeat=j-i):
attempt = pattern[:i] + ''.join(combo) + pattern[j:]
if attempt in words:
print attempt
这比我的其他答案使用更少的内存,但是当你添加更多问号时它会以指数方式变慢。
答案 11 :(得分:1)
懒惰的解决方案是让SQLite或其他DBMS为您完成工作。
只需创建一个内存数据库,加载你的单词并使用LIKE运算符运行select。
答案 12 :(得分:1)
摘要:使用两个紧凑的二进制搜索索引,其中一个单词和一个反向单词。空间成本是指数的2N指针;几乎所有的查找都非常快;最坏的情况,“?? e”,仍然不错。如果你为每个字长制作单独的表格,那么即使是最坏的情况也会非常快。
详细信息:Stephen C.发布了good idea:搜索有序字典以查找模式可以显示的范围。但是,当模式以通配符开头时,这没有任何帮助。您也可以按字长进行索引,但这是另一个想法:在反向字典单词上添加有序索引;那么一个模式总是在前向索引或反向词索引中产生一个小范围(因为我们被告知没有类似的模式?ABCD?)。单词本身只需要存储一次,两个结构的条目都指向相同的单词,查找程序可以向前或向后查看;但是为了使用Python的内置二进制搜索功能,我已经制作了两个单独的字符串数组,浪费了一些空间。 (我正在使用一个排序的数组而不是其他人建议的树,因为它节省了空间,并且至少速度相当快。)
<强>代码强>:
import bisect, re
def forward(string): return string
def reverse(string): return string[::-1]
index_forward = sorted(line.rstrip('\n')
for line in open('/usr/share/dict/words'))
index_reverse = sorted(map(reverse, index_forward))
def lookup(pattern):
"Return a list of the dictionary words that match pattern."
if reverse(pattern).find('?') <= pattern.find('?'):
key, index, fixup = pattern, index_forward, forward
else:
key, index, fixup = reverse(pattern), index_reverse, reverse
assert all(c.isalpha() or c == '?' for c in pattern)
lo = bisect.bisect_left(index, key.replace('?', 'A'))
hi = bisect.bisect_right(index, key.replace('?', 'z'))
r = re.compile(pattern.replace('?', '.') + '$')
return filter(r.match, (fixup(index[i]) for i in range(lo, hi)))
测试:(该代码也适用于像?AB?D?这样的模式,但没有速度保证。)
>>> lookup('hello')
['hello']
>>> lookup('??llo')
['callo', 'cello', 'hello', 'uhllo', 'Rollo', 'hollo', 'nullo']
>>> lookup('hel??')
['helio', 'helix', 'hello', 'helly', 'heloe', 'helve']
>>> lookup('he?l')
['heal', 'heel', 'hell', 'heml', 'herl']
>>> lookup('hx?l')
[]
效率:这需要2N指针加上存储字典单词文本所需的空间(在调整后的版本中)。最糟糕的情况出现在'?? e'模式中,该模式在我的235k字/ usr / share / dict / words中查看44062个候选人;但是几乎所有的查询都要快得多,比如'h ?? lo'看190,如果我们需要的话,首先在字长上索引就会减少'?? e'几乎为零。每个候选检查都比其他人建议的哈希表查找更快。
这类似于rotations-index解决方案,它避免了所有错误匹配候选者,代价是需要大约10N指针而不是2N(假设平均字长约为10,如我/ usr / share / dict /字)。
您可以使用自定义搜索功能对每个查找执行单个二进制搜索,而不是两个,同时搜索低限和高限(因此不会重复搜索的共享部分)。
答案 13 :(得分:1)
答案 14 :(得分:1)
我的第一篇帖子有一个Jason发现的错误,它什么时候效果不好?在一开始。我现在借用了安娜的循环转变。
我的解决方案: 引入一个单词结束字符(@)并将所有循环移位的单词存储在已排序的数组中!!对每个字长使用一个排序的数组。当查找“th ?? e @”时,移动字符串以将?标记移动到结尾(获得e @ th ??)并选择包含长度为5的单词的数组并对发生的第一个单词进行二进制搜索字符串“e @ th”之后。数组中剩余的所有单词都匹配,即我们会找到“e @ thoo(thoose),e @s(这些)等等。
解决方案具有时间复杂度Log(N),其中N是字典的大小,并且它将数据的大小扩展了大约6倍(平均字长)
答案 15 :(得分:1)
我是这样做的:
'?'
。TreeMap.higherKey(base)
和TreeMap.lowerKey(next(base))
查找字符串中将找到匹配项的范围。 (next
方法需要计算具有相同数字或更少字符的基本字符串的下一个更大的单词;例如next("aa")
是"ab"
,next("az")
是{{1} }。)"b"
搜索与该范围对应的子字符串。预先完成步骤1和2,使用Matcher.find()
空格提供数据结构,其中O(NlogN)
是单词数。
当N
出现在第一个位置时,这种方法退化为对整个字典的强制正则表达式搜索,但是越往右,就越不需要匹配。
编辑:
要在'?'
是第一个字符的情况下提高性能,请创建一个辅助查找表,记录第二个字符为“a”,“b”和“b”的单词运行的开始/结束偏移量。等等。这可用于第一个非'?'的情况是第二个角色。对于第一个非'?'的情况,你可以采用类似的方法是第三个字符,第四个字符,依此类推,但最终会有越来越多的越来越小的运行,最终这种“优化”变得无效。
需要更多空间但在大多数情况下更快的替代方法是为字典中的单词的所有旋转准备如上所述的字典数据结构。例如,第一次旋转将由所有单词组成2个字符或更多,单词的第一个字符移动到单词的末尾。第二个旋转将是3个字符或更多的单词,前两个字符移动到结尾,依此类推。然后进行搜索,查找非'?'的最长序列搜索字符串中的字符。如果此子字符串的第一个字符的索引是'?'
,请使用N
旋转查找范围,并在第N个旋转字列表中搜索。
答案 16 :(得分:1)
您想要的数据结构称为 trie - 请参阅wikipedia article以获取简短摘要。
trie是一种树形结构,其中通过树的路径形成您要编码的所有单词的集合 - 每个节点最多可以有26个子节点,对于下一个字符位置的每个可能的字母。请参阅维基百科文章中的图表以了解我的意思。
答案 17 :(得分:1)
基于正则表达式的解决方案将考虑字典中的每个可能值。如果性能是您最大的约束,那么可以构建一个索引以大大加快它的速度。
您可以从每个单词长度的索引开始,其中包含每个索引的索引=匹配单词集的字符。对于长度为5个单词,例如2=r : {write, wrote, drate, arete, arite}, 3=o : {wrote, float, group}
等。要获得原始查询的可能匹配项,请说“?ro ??”,单词集将相交,在这种情况下会生成{wrote, group}
这假设唯一的通配符是单个字符,并且字长是预先知道的。如果这些都不是有效的假设,我可以推荐基于n-gram的文本匹配,如this paper中所述。
答案 18 :(得分:0)
如果您只有?
个通配符,没有匹配可变数量字符的*
通配符,您可以尝试这样做:对于每个字符索引,构建一个从字符到单词集的字典。即如果单词是 write,write,drate,arete,arite ,你的字典结构将如下所示:
Character Index 0:
'a' -> {"arete", "arite"}
'd' -> {"drate"}
'w' -> {"write", "wrote"}
Character Index 1:
'r' -> {"write", "wrote", "drate", "arete", "arite"}
Character Index 2:
'a' -> {"drate"}
'e' -> {"arete"}
'i' -> {"write", "arite"}
'o' -> {"wrote"}
...
如果你想查找a?i??
,你会得到与字符索引0 =&gt;对应的集合。 'a'{“arete”,“arite”}和对应于字符索引2的集合='i'=&gt; {“写”,“arite”}并采取集合交集。
答案 19 :(得分:0)
如果你真的想要每秒十亿次搜索的事情(虽然我无法想象为什么除了制作下一个大师拼字游戏AI之外的任何人或者某个巨大的网络服务之类的东西会想要那么快),我建议利用线程来产生[你的机器上的核心数]线程+一个主线程,它将工作委托给所有这些线程。然后应用到目前为止找到的最佳解决方案,并希望您不会耗尽内存。
我的想法是,你可以通过字母准备切片字典来加快某些情况,如果你知道选择的第一个字母,你可以诉诸于一个小得多的干草堆。
我的另一个想法是你试图暴力破解某些东西 - 或许是为了拼字游戏而建立一个数据库或列表?