使用Levenshtein距离比较多个单词名称

时间:2014-11-06 19:36:30

标签: objective-c string algorithm levenshtein-distance

我正在将校园里的建筑名称与各种数据库的输入进行比较。人们输入了这些名字,每个人都使用自己的缩写方案。我正在尝试从用户输入到名称的规范形式找到最佳匹配。

我已经实现了一个递归的Levenshtein距离方法,但是我正在尝试解决一些边缘情况。 My implementation is on GitHub

一些建筑名称是一个单词,而其他建筑名称是两个。单个单词上的单个单词会产生相当准确的结果,但我需要记住两件事。

  1. 缩写:假设输入是名称的缩短版本,我有时可以在输入和任意名称之间获得相同的Levenshtein距离,以及正确的名称。 例如,如果我的输入为“Ing”且建筑物名称 1。["Boylan", "Ingersoll", "Whitman", "Whitehead", "Roosevelt", and "Library"],则BoylanIngersoll的最终结果为6 Ingersoll。所需的结果是New Ing

  2. 多字符串:第二种边缘情况是输入和/或输出是两个字,用空格分隔。例如,New IngersollNew的缩写。在这种情况下,New Ingersoll和Boylan都得到了Levenshtein距离为6.如果我要分割字符串,New完全匹配{{1}},那么我只需要参考我的解决方案前一个边缘案例。

  3. 处理这两种边缘情况的最佳方法是什么?

    <子> 1。对于好奇的人来说,这些都是纽约市布鲁克林学院的建筑。

2 个答案:

答案 0 :(得分:3)

我认为你应该使用Longest Common Subsequence的长度而不是Levenshtein距离。对您的案例而言,这似乎是一个更好的衡量标准。从本质上讲,正如我在评论中所建议的那样,它优先考虑插入和删除而不是替换。

它显然在&#34; Ing&#34;之间徘徊。 - &GT; &#34;的Ingersoll&#34;和&#34; Ing&#34; - &GT; &#34;伊兰&#34; (分数为3和1)处理空格没有问题(&#34; New Ing&#34; - &gt;&#34; New Ingersoll&#34;得分7&#34; New Ing&#34; - &gt;& #34; Boylan&#34;再次得分1),如果你遇到像&#34; Ingsl&#34;这样的缩写,也会很好地工作。

该算法很简单。如果你的两个字符串的长度为 m n ,则按字符顺序比较字符串的连续前缀(从空前缀开始),将分数保持在大小 m的矩阵中+1,n + 1 。如果特定对匹配,则在前两个前缀的分数中添加一个(矩阵中的一行向上和一列);否则保持这些前缀的两个分数中的最高分(比较上面的条目和紧接着的条目并采取最佳)。当您浏览了两个字符串时,分数矩阵中的最后一个条目是LCS的长度。

&#34; Ingsll&#34;的示例得分矩阵和&#34;英格索尔&#34;:

      0 1 2 3 4 5 6
        I n g s l l
    ---------------
0   | 0 0 0 0 0 0 0
1 I | 0 1 1 1 1 1 1
2 n | 0 1 2 2 2 2 2
3 g | 0 1 2 3 3 3 3
4 e | 0 1 2 3 3 3 3
5 r | 0 1 2 3 3 3 3
6 s | 0 1 2 3 4 4 4
7 o | 0 1 2 3 4 4 4
8 l | 0 1 2 3 4 5 5
9 l | 0 1 2 3 4 5 6

这是一个长度的ObjC实现。这里的大部分复杂性仅仅是因为想要处理组合字符序列 - 例如@"o̶" - 正确。

#import <Foundation/Foundation.h>

@interface NSString (WSSComposedLength)

- (NSUInteger)WSSComposedLength;

@end

@implementation NSString (WSSComposedLength)

- (NSUInteger)WSSComposedLength
{
    __block NSUInteger length = 0;
    [self enumerateSubstringsInRange:(NSRange){0, [self length]}
                             options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationSubstringNotRequired
                          usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
                              length++;
                          }];

    return length;
}

@end


@interface NSString (WSSLongestCommonSubsequence)

- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target;
- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target;

@end

@implementation NSString (WSSLongestCommonSubsequence)

- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target
{
    NSUInteger * const * scores;
    scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes];

    return scores[[target WSSComposedLength]][[self WSSComposedLength]];
}

- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target
{
    NSUInteger * const * scores;
    scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes];

    //FIXME: Implement this.

    return nil;
}

- (NSData *)scoreMatrixForLongestCommonSubsequenceWithString:(NSString *)target{

    NSUInteger selfLength = [self WSSComposedLength];
    NSUInteger targetLength = [target WSSComposedLength];
    NSMutableData * scoresData = [NSMutableData dataWithLength:(targetLength + 1) * sizeof(NSUInteger *)];
    NSUInteger ** scores = [scoresData mutableBytes];

    for( NSUInteger i = 0; i <= targetLength; i++ ){
        scores[i] = [[NSMutableData dataWithLength:(selfLength + 1) * sizeof(NSUInteger)] mutableBytes];
    }

    /* Ranges in the enumeration Block are the same measure as
     * -[NSString length] -- i.e., 16-bit code units -- as opposed to
     * _composed_ length, which counts code points. Thus:
     *
     * Enumeration will miss the last character if composed length is used
     * as the range and there's a substring range with length greater than one.
     */
    NSRange selfFullRange = (NSRange){0, [self length]};
    NSRange targetFullRange = (NSRange){0, [target length]};
    /* Have to keep track of these indexes by hand, rather than using the
     * Block's substringRange.location because, e.g., @"o̶", will have
     * range {x, 2}, and the next substring will be {x+2, l}.
     */
    __block NSUInteger col = 0;
    __block NSUInteger row = 0;
    [target enumerateSubstringsInRange:targetFullRange
                             options:NSStringEnumerationByComposedCharacterSequences
                          usingBlock:^(NSString * targetSubstring,
                                       NSRange targetSubstringRange,
                                       NSRange _, BOOL * _0)
        {
            row++;
            col = 0;

            [self enumerateSubstringsInRange:selfFullRange
                                     options:NSStringEnumerationByComposedCharacterSequences
                                  usingBlock:^(NSString * selfSubstring,
                                               NSRange selfSubstringRange,
                                               NSRange _, BOOL * _0)
                {
                    col++;
                    NSUInteger newScore;
                    if( [selfSubstring isEqualToString:targetSubstring] ){

                        newScore = 1 + scores[row - 1][col - 1];
                    }
                    else {
                        NSUInteger upperScore = scores[row - 1][col];
                        NSUInteger leftScore = scores[row][col - 1];
                        newScore = MAX(upperScore, leftScore);
                    }

                    scores[row][col] = newScore;
                }];
        }];

    return scoresData;
}

@end

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        NSArray * testItems = @[@{@"source" : @"Ingso̶ll",
                                  @"targets": @[
                                    @{@"string"   : @"Ingersoll",
                                      @"score"    : @6,
                                      @"sequence" : @"Ingsll"},
                                    @{@"string"   : @"Boylan",
                                      @"score"    : @1,
                                      @"sequence" : @"n"},
                                    @{@"string"   : @"New Ingersoll",
                                      @"score"    : @6,
                                      @"sequence" : @"Ingsll"}]},
                                @{@"source" : @"Ing",
                                  @"targets": @[
                                         @{@"string"   : @"Ingersoll",
                                           @"score"    : @3,
                                           @"sequence" : @"Ing"},
                                         @{@"string"   : @"Boylan",
                                           @"score"    : @1,
                                           @"sequence" : @"n"},
                                         @{@"string"   : @"New Ingersoll",
                                           @"score"    : @3,
                                           @"sequence" : @"Ing"}]},
                                @{@"source" : @"New Ing",
                                  @"targets": @[
                                         @{@"string"   : @"Ingersoll",
                                           @"score"    : @3,
                                           @"sequence" : @"Ing"},
                                         @{@"string"   : @"Boylan",
                                           @"score"    : @1,
                                           @"sequence" : @"n"},
                                         @{@"string"   : @"New Ingersoll",
                                           @"score"    : @7,
                                           @"sequence" : @"New Ing"}]}];

        for( NSDictionary * item in testItems ){
            NSString * source = item[@"source"];
            for( NSDictionary * target in item[@"targets"] ){
                NSString * targetString = target[@"string"];
                NSCAssert([target[@"score"] integerValue] ==
                           [source WSSLengthOfLongestCommonSubsequenceWithString:targetString],
                          @"");
//                NSCAssert([target[@"sequence"] isEqualToString:
//                           [source longestCommonSubsequenceWithString:targetString]],
//                          @"");
            }
        }


    }
    return 0;
}

答案 1 :(得分:2)

我认为Levenshtein距离仅在您处理几乎相似的单词(如偶然拼写错误)时才有用。如果Levenshtein距离比单词本身长,则它与相似值没有任何有价值的含义。 (在你的例子中,&#34; Ing&#34;和#34; Boylan&#34;没有任何共同之处;没有人会混淆这些词。来自&#34; Ing&#34对于&#34; Boylan&#34;,你需要进行六次编辑,是单词有两个字母的两倍。)我甚至不会考虑具有明显不同长度的单词之间的Levenshtein距离,例如&#34; Ing& #34;和&#34;英格索尔&#34;并宣布它们不同。

相反,我会检查缩写模式下比原文短的单词。要检查单词是否是较长单词的缩写,您可以检查缩写的所有字母是否以相同的顺序出现在原始单词中。您还应该强制使用相同的字母开头。但是,该方法并不能解释错误的缩写。

我认为多字符串字符串更好地解析。你需要区分Ingersoll和New Ingersoll吗?在这种情况下,您可以建立一个评分系统,其中单词匹配得分为100,可能减去Levenshtein距离的十倍。不匹配有负分,比如-100。然后你评估每个单词的得分并除以建筑物中的单词数量:

如果您的字符串是&#34; Ingersoll&#34;:

  • &#34;的Ingersoll&#34;得分100 / 1 == 100
  • &#34;新英格索尔&#34;得分100 / 2 == 50

如果您的字符串是&#34; New Ingersoll&#34;:

  • &#34;的Ingersoll&#34;得分(100 - 100) / 1 == 100
  • &#34;新英格索尔&#34;得分(100 + 100) / 2 == 100

当您的缩写包含来自不同单词的字母时,单词方法会失败,例如: &#34; NI&#34;或&#34; NIng&#34;对于New Ingersoll,如果你不能在单词到单词匹配中找到匹配项,那么你可以在整个建筑物名称上尝试上面的缩写匹配。

(我意识到所有这些都不是一个真正的答案,而是一堆松散的想法。)