查找所有成对的相似单词

时间:2018-08-20 20:14:18

标签: algorithm levenshtein-distance damerau-levenshtein

我有40000个单词,想找到所有相似的对。出于相似性考虑,我使用了Damerau–Levenshtein distance的软度,该软度由字长确定。为简单起见,我不考虑重叠的编辑(就像链接的算法一样)。所有单词(其中大多数是德语,法语或英语)均已转换为小写字母,因为大小写在我们的数据中没有信息。我对距离计算做了两次修改

  • 两个个字符之间的距离为
    • 0,当它们相同时
    • 0.2,当它们的口音不同时(例如aäà
    • 0.2,分别为sß(德国的Sharp S)
    • 1,否则

此外,字符串 ßss的距离设置为0.2。我们的数据表明这种并发症是必要的。

要查找所有相似的对,我的想法是仅考虑通过公用n-gram找到的对,但这对于短单词(可以接受)失败,并且通常由于上述修改而失败。

我的下一个想法是,如果知道结果超过阈值(比如说4),则尽早放弃距离计算。但是,由于许多单词都有共同的前缀,所以这种中止为时已晚,不能很好地提高速度。在常见的后缀中,反转单词的操作失败(不是很严重)。

所有20e6对的整个计算大约需要五分钟,因此,现在一次可以存储所有结果(仅需要距离阈值以下的距离)是可能的。但是我正在寻找更具前瞻性的东西。

是否可以快速计算出Damerau-Levenshtein距离的一个好的下限(理想情况下允许提前退出)?

是否可以进行上述修改?请注意,例如,由于第二次修改,“至少两个字符串的大小之差”不成立。

代码

public final class Levenshtein {
    private static final class CharDistance {
        private static int distance(char c0, char c1) {
            if (c0 == c1) return 0;
            if ((c0|c1) < 0x80) return SCALE;
            return c0<=c1 ? distanceInternal(c0, c1) : distanceInternal(c1, c0);
        }

        private static int distanceInternal(char c0, char c1) {
            assert c0 <= c1;
            final String key = c0 + " " + c1;
            {
                final Integer result = CACHE.get(key);
                if (result != null) return result.intValue();
            }
            final int result = distanceUncached(c0, c1);
            CACHE.put(key, Integer.valueOf(result));
            return result;
        }

        private static int distanceUncached(char c0, char c1) {
            final String norm0 = Normalizer.normalize("" + c0, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
            final String norm1 = Normalizer.normalize("" + c1, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
            if (norm0.equals(norm1)) return DIACRITICS;
            assert c0 <= c1;
            if (c0=='s' && c1=='ß') return DIACRITICS;
            return SCALE;
        }

        private static final Map<String, Integer> CACHE = new ConcurrentHashMap<>();
    }

    /**
     * Return the scaled distance between {@code s0} and {@code s1}, if it's below {@code limit}.
     * Otherwise, return some lower bound ({@code >= limit}.
     */
    int distance(String s0, String s1, int limit) {
        final int len0 = s0.length();
        final int len1 = s1.length();
        int result = SCALE * (len0 + len1);
        final int[] array = new int[len0 * len1];
        for (int i0=0; i0<len0; ++i0) {
            final char c0 = s0.charAt(i0);
            for (int i1=0; i1<len1; ++i1) {
                final char c1 = s1.charAt(i1);
                final int d = CharDistance.distance(c0, c1);

                // Append c0 and c1 respectively.
                result = get(len0, array, i0-1, i1-1) + d;

                // Append c0.
                result = Math.min(result, get(len0, array, i0-1, i1) + SCALE);

                // Append c1.
                result = Math.min(result, get(len0, array, i0, i1-1) + SCALE);

                // Handle the "ß" <-> "ss" substitution.
                if (c0=='ß' && c1=='s' && i1>0 && s1.charAt(i1-1)=='s') result = Math.min(result, get(len0, array, i0-1, i1-2) + DIACRITICS);
                if (c1=='ß' && c0=='s' && i0>0 && s0.charAt(i0-1)=='s') result = Math.min(result, get(len0, array, i0-2, i1-1) + DIACRITICS);

                // Handle a transposition.
                if (i0>0 && i1>0 && s0.charAt(i0-1)==c1 && s1.charAt(i1-1)==c0) result = Math.min(result, get(len0, array, i0-2, i1-2) + SCALE);

                set(len0, array, i0, i1, result);
            }
            // Early exit.
            {
                final int j = i0 - len0 + len1;
                final int lowerBound = get(len0, array, i0, j);
                if (lowerBound >= limit) return lowerBound;
            }
        }
        return result;
    }

    // Simulate reading from a 2D array at indexes i0 and i1;
    private int get(int stride, int[] array, int i0, int i1) {
        if (i0<0 || i1<0) return SCALE * (i0+i1+2);
        return array[i1*stride + i0];
    }

    // Simulate writing to a 2D array at indexes i0 and i1;
    private void set(int stride, int[] array, int i0, int i1, int value) {
        array[i1*stride + i0] = value;
    }

    private static final int SCALE = 10;
    private static final int DIACRITICS = 2;
}

例句

rotwein
rotweincuv
rotweincuvee
rotweincuveé
rotweincuvée
rotweincuvúe
rotweindekanter
rotweinessig
rotweinfass
rotweinglas
rotweinkelch
rotweißkomposition
rotwild
roug
rouge
rougeaoc
rougeaop
rougeots
rougers
rouges
rougeáaop
rough
roughstock
rouladen
roulette
roumier
roumieu
round
rounded
roundhouse
rounds
rouret
rouss
roussanne
rousseau
roussi
roussillion
roussillon
route
rouvinez
rove
roveglia
rovere
roveri
rovertondo
rovo
rowan
rowein
roxburgh
roxx
roy
roya
royal
royalbl
royalblau
royaldanishnavy
royale
royales
royaline
royals
royer
royere
roze
rozenberg
rozes
rozier
rozès
rozés
roßberg
roßdorfer
roßerer
rpa
rr
rrvb
rry
rs
rsaftschorle
rsbaron
rscastillo
rsgut
rsl
rstenapfel
rstenberg
rstenbrõu
rt
rtd
rtebecker
rtebeker
ru
ruadh
ruanda
rub
rubaiyat
ruban
rubata
rubblez
rubenkov
rubeno
rubentino
ruber

1 个答案:

答案 0 :(得分:0)

我建议将所有单词放入trie中,然后针对自己递归搜索trie。

生成特里树应该很快,现在无论有多少个单词共享它们,通用前缀之间的匹配都只有一次。

在游荡部落时,您必须跟踪很多状态,因为您的状态是:“我们可能在计算过程中处于中间状态。”