基于音译的单词查找的高效数据结构/算法

7
我正在寻找一种高效的数据结构/算法来存储和搜索基于音译的单词查找(例如Google的http://www.google.com/transliterate/,但我不打算使用Google音译API)。不幸的是,我想要处理的自然语言没有任何声音编码实现,所以只能自己摸索。

目前,对于一个开源项目,我正在使用普通数组来存储单词列表,并根据用户输入动态生成正则表达式来匹配它们。这个方法很好用,但正则表达式比我需要的功能过于强大或资源密集。例如,如果我尝试将其移植到手持设备中,由于使用正则表达式搜索数千个单词会消耗太多电池,我担心这种解决方案会非常昂贵。

必须有更好的方法来完成这项工作,例如拼音输入法是如何工作的?有什么建议可以开始吗?

提前感谢。


编辑:如果我理解正确,这是@Dialecticus-建议的:

我想要从语言1音译,它有3个字符a、b、c,转换成语言2,它有6个字符p、q、r、x、y、z。由于每种语言所拥有的字符数量以及它们的音素不同,很难定义一对一映射。

在这里假设我们有一个关联数组/音译表:

a -> p, q
b -> r
c -> x, y, z

我们还有针对Language2的有效单词列表,存储在纯数组中:

...
px
qy
...

如果用户输入了ac,在音译步骤1之后可能的组合变成了px,py,pz,qx,qy,qz。 在步骤2中,我们必须在有效单词列表中进行另一次搜索,并将除pxqy之外的所有人都排除掉。
我目前所做的与上述方法并没有太大区别。 我正在构建一个正则表达式[pq][xyz],并与我的有效单词列表进行匹配,从而提供输出pxqy
我渴望知道是否有比这更好的方法。

请明确一下,音译的最终结果是否总是必须由有效单词列表中的单词组成? - MAK
@MAK,最好是这样。提议一个不合适的词毫无意义。 - Mehdi
2个回答

4
据我理解,您有一个使用字母表A1的输入字符串S,并希望将其转换为另一个字母表A2中等效的字符串S'。实际上,如果我理解正确,您想生成一个包含可能与S等效的输出字符串[S'1,S'2,...,S'n]的列表。
一种可行的方法是对于A2中的每个有效单词,在A1中生成与之匹配的字符串列表。使用您编辑中的示例,我们有:
px->ac
qy->ac
pr->ab

为了更清晰地表达,我已添加一个额外的有效单词 pr

现在我们知道哪些可能的输入符号序列将始终映射到有效单词,我们可以使用表格构建Trie

每个节点将保存一个指向A2中与从Trie根到当前节点路径上形成的A1符号序列相对应的有效单词列表的指针。

因此,对于我们的示例,Trie看起来会像这样:

                                  Root (empty)
                                    | a
                                    |
                                    V
                              +---Node (empty)---+
                              | b                | c
                              |                  |
                              V                  V
                           Node (px,qy)         Node (pr)      

从根节点开始,当符号被消耗时,会从当前节点向其标记为已消耗的子节点进行转移,直到我们读取了整个字符串。如果在任何时候该符号未定义转移,则输入的字符串不存在于我们的Trie中,因此不映射到目标语言中的有效单词。否则,在该过程结束时,与当前节点关联的单词列表是输入字符串映射到的有效单词列表。
除了构建Trie的初始成本(如果我们永远不想更改有效单词列表,则可以预先构建Trie),这需要O(n)的时间复杂度来查找映射有效单词的列表,其中n是输入的长度。
使用Trie还提供了一个优势,即您还可以使用它来查找可以通过将更多的符号添加到输入的末尾生成的所有有效单词的列表 - 即前缀匹配。例如,如果输入符号“a”,我们可以使用Trie查找所有以“a”开头的有效单词('px','qr','py')。但执行此操作的速度没有找到精确匹配快。
以下是解决方案的简单示例(使用Java):
import java.util.*;

class TrieNode{
    // child nodes - size of array depends on your alphabet size,
    // her we are only using the lowercase English characters 'a'-'z'
    TrieNode[] next=new TrieNode[26];
    List<String> words;

    public TrieNode(){
        words=new ArrayList<String>();
    }
}

class Trie{
    private TrieNode root=null;

    public void addWord(String sourceLanguage, String targetLanguage){
        root=add(root,sourceLanguage.toCharArray(),0,targetLanguage);
    }

    private static int convertToIndex(char c){ // you need to change this for your alphabet
        return (c-'a');
    }

    private TrieNode add(TrieNode cur, char[] s, int pos, String targ){
        if (cur==null){
            cur=new TrieNode();
        }
        if (s.length==pos){
            cur.words.add(targ);
        }
        else{

            cur.next[convertToIndex(s[pos])]=add(cur.next[convertToIndex(s[pos])],s,pos+1,targ);
        }
        return cur;
    }

    public List<String> findMatches(String text){
        return find(root,text.toCharArray(),0);

    }

    private List<String> find(TrieNode cur, char[] s, int pos){
        if (cur==null) return new ArrayList<String>();
        else if (pos==s.length){
            return cur.words;
        }
        else{
            return find(cur.next[convertToIndex(s[pos])],s,pos+1);
        }
    }
}

class MyMiniTransliiterator{
    public static void main(String args[]){
        Trie t=new Trie();
        t.addWord("ac","px");
        t.addWord("ac","qy");
        t.addWord("ab","pr");

        System.out.println(t.findMatches("ac")); // prints [px,qy]
        System.out.println(t.findMatches("ab")); // prints [pr]
        System.out.println(t.findMatches("ba")); // prints empty list since this does not match anything
    }
}

这是一个非常简单的trie树,没有任何压缩或加速,并且仅适用于输入语言为小写英文字符。但它可以很容易地修改为其他字符集。


非常感谢您花时间写出如此精彩的回复!我希望有人能写关于 Trie 的内容。虽然我还没有测试过,但如果有效单词列表大约有 100,000 个项目,Trie 可能需要多少内存? - Mehdi
1
@MHK:这实际上取决于这些单词具体是什么。许多单词最终会拥有共同的前缀,因此需要的节点数量比那么多单词的最坏情况要少得多。当然,前缀共性的数量取决于您的词典。有一些策略可以减少 Trie 中使用的内存量(例如,通过折叠长序列的非分支节点)。您可以在维基百科文章或优秀的算法/数据结构书籍中查看相关章节(据我所知,《算法》(Sedgewick)一书中有很好的解释)。 - MAK
@MAK,使用HashTable来存储节点怎么样? - Rifat
@MAK 我的意思是使用 Trie + HashTable :) - Rifat
@Rifat:从您之前的评论中我所理解的是,您想要将一个节点的子节点存储在哈希表中(键是输入语言中的符号,值是子节点),而不是使用固定大小的节点数组。如果输入语言的字母表很大,那么这种方法可以行得通,并且可能会更好。但请记住,一个完整的通用哈希表(例如Java的HashMap或Python的dict)通常会有很多开销与它们相关联,如果每个哈希表中的数据量不大,那么拥有这么多哈希表可能不会给您带来净收益。 - MAK

2
我会逐个符号地构建音译句子,而不是逐个单词。对于大多数语言,可以将每个符号独立于单词中的其他符号进行音译。你仍然可能有整个单词必须作为完整单词音译的例外情况,但音译表中的符号和异常肯定比所有现有单词的音译表要小。
最佳的音译表结构应该是某种关联数组,可能利用哈希表。在C++中有std::unordered_map,在C#中你会使用Dictionary

1
我曾经也这样想过,但问题是,大多数情况下逐个转换字符的组合结果会产生无效或拼写错误的单词。你仍然需要针对有效的单词列表进行检查。 - Mehdi
@Mehdi 请编辑您的问题,给我们一些音译(t13n)的例子,其中t13n-by-symbol会给出不正确的结果,以及理想的t13n结果。 - Dialecticus
谢谢您的回复。我在编辑后的问题中写了一个简化版本的问题。 - Mehdi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接