【问题标题】:A better similarity ranking algorithm for variable length strings一种更好的变长字符串相似度排序算法
【发布时间】:2010-10-13 18:45:09
【问题描述】:

我正在寻找一种字符串相似性算法,它在可变长度字符串上产生比通常建议的更好的结果(levenshtein distance、soundex 等)。

例如,

给定字符串 A:“Robert”,

然后是字符串 B:“Amy Robertson”

会比匹配更好

字符串 C:“理查德”

此外,该算法最好与语言无关(也适用于英语以外的其他语言)。

【问题讨论】:

标签: string-matching ranking similarity fuzzy-search


【解决方案1】:

为什么不用于 JavaScript 实现,我也解释了算法。

算法

  • 输入:FranceFrench
  • 将它们都映射到它们的大写字符(使算法对大小写不敏感),然后将它们分成它们的字符对:
FRANCE: {FR, RA, AN, NC, CE}
FRENCH: {FR, RE, EN, NC, CH}
  • 找到那里的交叉点:

  • 结果:

实施

function similarity(s1, s2) {
    const
        set1 = pairs(s1.toUpperCase()), // [ FR, RA, AN, NC, CE ]
        set2 = pairs(s2.toUpperCase()), // [ FR, RE, EN, NC, CH ]
        intersection = set1.filter(x => set2.includes(x)); // [ FR, NC ]
    // Tips: Instead of `2` multiply by `200`, To get percentage.
    return (intersection.length * 2) / (set1.length + set2.length);
}
function pairs(input) {
    const tokenized = [];
    for (let i = 0; i < input.length - 1; i++)
        tokenized.push(input.substring(i, 2 + i));

    return tokenized;
}
console.log(similarity("FRANCE", "FRENCH"));

按(词 - 相似度)排名结果

  1. 密封 - 80%
  2. 健康 - 55%
  3. 听说过 - 44%
  4. 放牧 - 40%
  5. 帮助 - 25%
  6. 已售出 - 0%

来自相同的原始source

【讨论】:

    【解决方案2】:
    **I've converted marzagao's answer to Java.**
    
    import org.apache.commons.lang3.StringUtils; //Add a apache commons jar in pom.xml
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class SimilarityComparator {
    public static void main(String[] args) {
        String str0 = "Nischal";
        String str1 = "Nischal";
        double v = compareStrings(str0, str1);
        System.out.println("Similarity betn " + str0 + " and " + str1 + " = " + v);
    
    }
    
    private static double compareStrings(String str1, String str2) {
        List<String> pairs1 = wordLetterPairs(str1.toUpperCase());
        List<String> pairs2 = wordLetterPairs(str2.toUpperCase());
    
        int intersection = 0;
        int union = pairs1.size() + pairs2.size();
    
        for (String s : pairs1) {
            for (int j = 0; j < pairs2.size(); j++) {
                if (s.equals(pairs2.get(j))) {
                    intersection++;
                    pairs2.remove(j);
                    break;
                }
            }
        }
        return (2.0 * intersection) / union;
    }
    
    private static List<String> wordLetterPairs(String str) {
        List<String> AllPairs = new ArrayList<>();
        String[] Words = str.split("\\s");
        for (String word : Words) {
            if (StringUtils.isNotBlank(word)) {
                String[] PairsInWord = letterPairs(word);
                Collections.addAll(AllPairs, PairsInWord);
            }
        }
        return AllPairs;
    }
    
    private static String[] letterPairs(String str) {
        int numPairs = str.length() - 1;
        String[] pairs = new String[numPairs];
        for (int i = 0; i < numPairs; i++) {
            try {
                pairs[i] = str.substring(i, i + 2);
            } catch (Exception e) {
                pairs[i] = str.substring(i, numPairs);
            }
        }
        return pairs;
      }
    }
    

    【讨论】:

      【解决方案3】:

      我的 JavaScript 实现采用一个字符串或字符串数​​组,以及一个可选的下限(默认下限为 0.5)。如果你传递一个字符串,它会根据字符串的相似度分数是否大于或等于下限返回真或假。如果你传给它一个字符串数组,它会返回一个由相似度分数大于或等于下限的字符串组成的数组,按分数排序。

      例子:

      'Healed'.fuzzy('Sealed');      // returns true
      'Healed'.fuzzy('Help');        // returns false
      'Healed'.fuzzy('Help', 0.25);  // returns true
      
      'Healed'.fuzzy(['Sold', 'Herded', 'Heard', 'Help', 'Sealed', 'Healthy']);
      // returns ["Sealed", "Healthy"]
      
      'Healed'.fuzzy(['Sold', 'Herded', 'Heard', 'Help', 'Sealed', 'Healthy'], 0);
      // returns ["Sealed", "Healthy", "Heard", "Herded", "Help", "Sold"]
      

      这里是:

      (function(){
        var default_floor = 0.5;
      
        function pairs(str){
          var pairs = []
            , length = str.length - 1
            , pair;
          str = str.toLowerCase();
          for(var i = 0; i < length; i++){
            pair = str.substr(i, 2);
            if(!/\s/.test(pair)){
              pairs.push(pair);
            }
          }
          return pairs;
        }
      
        function similarity(pairs1, pairs2){
          var union = pairs1.length + pairs2.length
            , hits = 0;
      
          for(var i = 0; i < pairs1.length; i++){
            for(var j = 0; j < pairs2.length; j++){
              if(pairs1[i] == pairs2[j]){
                pairs2.splice(j--, 1);
                hits++;
                break;
              }
            }
          }
          return 2*hits/union || 0;
        }
      
        String.prototype.fuzzy = function(strings, floor){
          var str1 = this
            , pairs1 = pairs(this);
      
          floor = typeof floor == 'number' ? floor : default_floor;
      
          if(typeof(strings) == 'string'){
            return str1.length > 1 && strings.length > 1 && similarity(pairs1, pairs(strings)) >= floor || str1.toLowerCase() == strings.toLowerCase();
          }else if(strings instanceof Array){
            var scores = {};
      
            strings.map(function(str2){
              scores[str2] = str1.length > 1 ? similarity(pairs1, pairs(str2)) : 1*(str1.toLowerCase() == str2.toLowerCase());
            });
      
            return strings.filter(function(str){
              return scores[str] >= floor;
            }).sort(function(a, b){
              return scores[b] - scores[a];
            });
          }
        };
      })();
      

      【讨论】:

      • 错误/错字! for(var j = 0; j &lt; pairs1.length; j++){ 应该是 for(var j = 0; j &lt; pairs2.length; j++){
      【解决方案4】:

      John Rutledge's 的简短版回答:

      def get_bigrams(string):
          '''
          Takes a string and returns a list of bigrams
          '''
          s = string.lower()
          return {s[i:i+2] for i in xrange(len(s) - 1)}
      
      def string_similarity(str1, str2):
          '''
          Perform bigram comparison between two strings
          and return a percentage match in decimal form
          '''
          pairs1 = get_bigrams(str1)
          pairs2 = get_bigrams(str2)
          return (2.0 * len(pairs1 & pairs2)) / (len(pairs1) + len(pairs2))
      

      【讨论】:

      • 即使是intersection 变量也是一种浪费。
      【解决方案5】:

      这是marzagao's 答案的另一个版本,这个是用 Python 编写的:

      def get_bigrams(string):
          """
          Take a string and return a list of bigrams.
          """
          s = string.lower()
          return [s[i:i+2] for i in list(range(len(s) - 1))]
      
      def string_similarity(str1, str2):
          """
          Perform bigram comparison between two strings
          and return a percentage match in decimal form.
          """
          pairs1 = get_bigrams(str1)
          pairs2 = get_bigrams(str2)
          union  = len(pairs1) + len(pairs2)
          hit_count = 0
          for x in pairs1:
              for y in pairs2:
                  if x == y:
                      hit_count += 1
                      break
          return (2.0 * hit_count) / union
      
      if __name__ == "__main__":
          """
          Run a test using the example taken from:
          http://www.catalysoft.com/articles/StrikeAMatch.html
          """
          w1 = 'Healed'
          words = ['Heard', 'Healthy', 'Help', 'Herded', 'Sealed', 'Sold']
      
          for w2 in words:
              print('Healed --- ' + w2)
              print(string_similarity(w1, w2))
              print()
      

      【讨论】:

      • 当单词中有重复的 ngram 时,string_similarity 中存在一个小错误,导致相同字符串的得分 >1。在“hit_count += 1”之后添加一个“break”可以修复它。
      • @jbaiter:很好。我对其进行了更改以反映您的更改。
      • 在 Simon White 的文章中,他说“请注意,只要找到匹配项,就会从第二个数组列表中删除该字符对,以防止我们多次匹配同一个字符对。(否则, 'GGGGG' 将与 'GG' 完美匹配。)不考虑这一点,似乎也有算法不传递的结果(相似度(x,y)=/=相似度(y,x))。在 hit_count += 1 行之后添加pairs2.remove(y) 可以解决问题。
      • 请注意,您必须复制@NinjaMeTimbers 的解决方案,这相当简单。如果您使用上面未更改的代码,string_similarity('GGGGG', GG'') 将返回 1.6,这很荒谬。
      【解决方案6】:

      我正在寻找@marzagao 的答案所指示的算法的纯红宝石实现。不幸的是,@marzagao 指示的链接已损坏。在@s01ipsist 的回答中,他指出了ruby gem amatch 的实现不是纯红宝石。所以我搜索了一下,发现 gem fuzzy_matchhere 具有纯 ruby​​ 实现(尽管这个 gem 使用 amatch)。我希望这会对像我这样的人有所帮助。

      【讨论】:

        【解决方案7】:

        这是基于 Sørensen–Dice 索引(marzagao 的回答)的另一个版本的相似性,这是用 C++11 编写的:

        /*
         * Similarity based in Sørensen–Dice index.
         *
         * Returns the Similarity between _str1 and _str2.
         */
        double similarity_sorensen_dice(const std::string& _str1, const std::string& _str2) {
            // Base case: if some string is empty.
            if (_str1.empty() || _str2.empty()) {
                return 1.0;
            }
        
            auto str1 = upper_string(_str1);
            auto str2 = upper_string(_str2);
        
            // Base case: if the strings are equals.
            if (str1 == str2) {
                return 0.0;
            }
        
            // Base case: if some string does not have bigrams.
            if (str1.size() < 2 || str2.size() < 2) {
                return 1.0;
            }
        
            // Extract bigrams from str1
            auto num_pairs1 = str1.size() - 1;
            std::unordered_set<std::string> str1_bigrams;
            str1_bigrams.reserve(num_pairs1);
            for (unsigned i = 0; i < num_pairs1; ++i) {
                str1_bigrams.insert(str1.substr(i, 2));
            }
        
            // Extract bigrams from str2
            auto num_pairs2 = str2.size() - 1;
            std::unordered_set<std::string> str2_bigrams;
            str2_bigrams.reserve(num_pairs2);
            for (unsigned int i = 0; i < num_pairs2; ++i) {
                str2_bigrams.insert(str2.substr(i, 2));
            }
        
            // Find the intersection between the two sets.
            int intersection = 0;
            if (str1_bigrams.size() < str2_bigrams.size()) {
                const auto it_e = str2_bigrams.end();
                for (const auto& bigram : str1_bigrams) {
                    intersection += str2_bigrams.find(bigram) != it_e;
                }
            } else {
                const auto it_e = str1_bigrams.end();
                for (const auto& bigram : str2_bigrams) {
                    intersection += str1_bigrams.find(bigram) != it_e;
                }
            }
        
            // Returns similarity coefficient.
            return (2.0 * intersection) / (num_pairs1 + num_pairs2);
        }
        

        【讨论】:

          【解决方案8】:

          在 C99 中发布 marzagao's answer,灵感来自 these 算法

          double dice_match(const char *string1, const char *string2) {
          
              //check fast cases
              if (((string1 != NULL) && (string1[0] == '\0')) || 
                  ((string2 != NULL) && (string2[0] == '\0'))) {
                  return 0;
              }
              if (string1 == string2) {
                  return 1;
              }
          
              size_t strlen1 = strlen(string1);
              size_t strlen2 = strlen(string2);
              if (strlen1 < 2 || strlen2 < 2) {
                  return 0;
              }
          
              size_t length1 = strlen1 - 1;
              size_t length2 = strlen2 - 1;
          
              double matches = 0;
              int i = 0, j = 0;
          
              //get bigrams and compare
              while (i < length1 && j < length2) {
                  char a[3] = {string1[i], string1[i + 1], '\0'};
                  char b[3] = {string2[j], string2[j + 1], '\0'};
                  int cmp = strcmpi(a, b);
                  if (cmp == 0) {
                      matches += 2;
                  }
                  i++;
                  j++;
              }
          
              return matches / (length1 + length2);
          }
          

          一些基于original article的测试:

          #include <stdio.h>
          
          void article_test1() {
              char *string1 = "FRANCE";
              char *string2 = "FRENCH";
              printf("====%s====\n", __func__);
              printf("%2.f%% == 40%%\n", dice_match(string1, string2) * 100);
          }
          
          
          void article_test2() {
              printf("====%s====\n", __func__);
              char *string = "Healed";
              char *ss[] = {"Heard", "Healthy", "Help",
                            "Herded", "Sealed", "Sold"};
              int correct[] = {44, 55, 25, 40, 80, 0};
              for (int i = 0; i < 6; ++i) {
                  printf("%2.f%% == %d%%\n", dice_match(string, ss[i]) * 100, correct[i]);
              }
          }
          
          void multicase_test() {
              char *string1 = "FRaNcE";
              char *string2 = "fREnCh";
              printf("====%s====\n", __func__);
              printf("%2.f%% == 40%%\n", dice_match(string1, string2) * 100);
          
          }
          
          void gg_test() {
              char *string1 = "GG";
              char *string2 = "GGGGG";
              printf("====%s====\n", __func__);
              printf("%2.f%% != 100%%\n", dice_match(string1, string2) * 100);
          }
          
          
          int main() {
              article_test1();
              article_test2();
              multicase_test();
              gg_test();
          
              return 0;
          }
          

          【讨论】:

            【解决方案9】:

            Clojure:

            (require '[clojure.set :refer [intersection]])
            
            (defn bigrams [s]
              (->> (split s #"\s+")
                   (mapcat #(partition 2 1 %))
                   (set)))
            
            (defn string-similarity [a b]
              (let [a-pairs (bigrams a)
                    b-pairs (bigrams b)
                    total-count (+ (count a-pairs) (count b-pairs))
                    match-count (count (intersection a-pairs b-pairs))
                    similarity (/ (* 2 match-count) total-count)]
                similarity))
            

            【讨论】:

              【解决方案10】:

              marzagao's answer 很棒。我将它转换为 C#,所以我想我会把它贴在这里:

              Pastebin Link

              /// <summary>
              /// This class implements string comparison algorithm
              /// based on character pair similarity
              /// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
              /// </summary>
              public class SimilarityTool
              {
                  /// <summary>
                  /// Compares the two strings based on letter pair matches
                  /// </summary>
                  /// <param name="str1"></param>
                  /// <param name="str2"></param>
                  /// <returns>The percentage match from 0.0 to 1.0 where 1.0 is 100%</returns>
                  public double CompareStrings(string str1, string str2)
                  {
                      List<string> pairs1 = WordLetterPairs(str1.ToUpper());
                      List<string> pairs2 = WordLetterPairs(str2.ToUpper());
              
                      int intersection = 0;
                      int union = pairs1.Count + pairs2.Count;
              
                      for (int i = 0; i < pairs1.Count; i++)
                      {
                          for (int j = 0; j < pairs2.Count; j++)
                          {
                              if (pairs1[i] == pairs2[j])
                              {
                                  intersection++;
                                  pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success
              
                                  break;
                              }
                          }
                      }
              
                      return (2.0 * intersection) / union;
                  }
              
                  /// <summary>
                  /// Gets all letter pairs for each
                  /// individual word in the string
                  /// </summary>
                  /// <param name="str"></param>
                  /// <returns></returns>
                  private List<string> WordLetterPairs(string str)
                  {
                      List<string> AllPairs = new List<string>();
              
                      // Tokenize the string and put the tokens/words into an array
                      string[] Words = Regex.Split(str, @"\s");
              
                      // For each word
                      for (int w = 0; w < Words.Length; w++)
                      {
                          if (!string.IsNullOrEmpty(Words[w]))
                          {
                              // Find the pairs of characters
                              String[] PairsInWord = LetterPairs(Words[w]);
              
                              for (int p = 0; p < PairsInWord.Length; p++)
                              {
                                  AllPairs.Add(PairsInWord[p]);
                              }
                          }
                      }
              
                      return AllPairs;
                  }
              
                  /// <summary>
                  /// Generates an array containing every 
                  /// two consecutive letters in the input string
                  /// </summary>
                  /// <param name="str"></param>
                  /// <returns></returns>
                  private string[] LetterPairs(string str)
                  {
                      int numPairs = str.Length - 1;
              
                      string[] pairs = new string[numPairs];
              
                      for (int i = 0; i < numPairs; i++)
                      {
                          pairs[i] = str.Substring(i, 2);
                      }
              
                      return pairs;
                  }
              }
              

              【讨论】:

              • 非常好!我唯一的建议是将它变成一个扩展。
              • +1!它工作得很好,对 Java 也有轻微的修改。而且它似乎确实比 Levenshtein 返回更好的响应。
              • 我在下面添加了一个将其转换为扩展方法的版本。感谢原始版本和出色的翻译。
              • @Michael La Voie 谢谢,非常好!虽然(2.0 * intersection) / union 有一点问题 - 我在比较两个空字符串时得到 Double.NaN。
              【解决方案11】:

              对不起,答案不是作者发明的。这是一种众所周知的算法,最早由 Digital Equipment Corporation 提出,通常被称为 shingling。

              http://www.hpl.hp.com/techreports/Compaq-DEC/SRC-TN-1997-015.pdf

              【讨论】:

                【解决方案12】:

                Haskell 版本 - 请随意提出修改建议,因为我没有做过多少 Haskell。

                import Data.Char
                import Data.List
                
                -- Convert a string into words, then get the pairs of words from that phrase
                wordLetterPairs :: String -> [String]
                wordLetterPairs s1 = concat $ map pairs $ words s1
                
                -- Converts a String into a list of letter pairs.
                pairs :: String -> [String]
                pairs [] = []
                pairs (x:[]) = []
                pairs (x:ys) = [x, head ys]:(pairs ys)
                
                -- Calculates the match rating for two strings
                matchRating :: String -> String -> Double
                matchRating s1 s2 = (numberOfMatches * 2) / totalLength
                  where pairsS1 = wordLetterPairs $ map toLower s1
                        pairsS2 = wordLetterPairs $ map toLower s2
                        numberOfMatches = fromIntegral $ length $ pairsS1 `intersect` pairsS2
                        totalLength = fromIntegral $ length pairsS1 + length pairsS2
                

                【讨论】:

                  【解决方案13】:

                  这是我建议的 StrikeAMatch 算法的 PHP 实现,作者 Simon White。优点(如链接中所说)是:

                  • 词汇相似性的真实反映 - 具有微小差异的字符串应被识别为相似。特别是,显着的子字符串重叠应该指向字符串之间的高度相似性。

                  • 对词序变化的鲁棒性 - 包含相同词但顺序不同的两个字符串应被识别为相似。另一方面,如果一个字符串只是另一个字符串中包含的字符的随机变位,那么它应该(通常)被识别为不同的。

                  • 语言独立性 - 该算法不仅适用于英语,而且适用于多种不同的语言。

                  <?php
                  /**
                   * LetterPairSimilarity algorithm implementation in PHP
                   * @author Igal Alkon
                   * @link http://www.catalysoft.com/articles/StrikeAMatch.html
                   */
                  class LetterPairSimilarity
                  {
                      /**
                       * @param $str
                       * @return mixed
                       */
                      private function wordLetterPairs($str)
                      {
                          $allPairs = array();
                  
                          // Tokenize the string and put the tokens/words into an array
                  
                          $words = explode(' ', $str);
                  
                          // For each word
                          for ($w = 0; $w < count($words); $w++)
                          {
                              // Find the pairs of characters
                              $pairsInWord = $this->letterPairs($words[$w]);
                  
                              for ($p = 0; $p < count($pairsInWord); $p++)
                              {
                                  $allPairs[] = $pairsInWord[$p];
                              }
                          }
                  
                          return $allPairs;
                      }
                  
                      /**
                       * @param $str
                       * @return array
                       */
                      private function letterPairs($str)
                      {
                          $numPairs = mb_strlen($str)-1;
                          $pairs = array();
                  
                          for ($i = 0; $i < $numPairs; $i++)
                          {
                              $pairs[$i] = mb_substr($str,$i,2);
                          }
                  
                          return $pairs;
                      }
                  
                      /**
                       * @param $str1
                       * @param $str2
                       * @return float
                       */
                      public function compareStrings($str1, $str2)
                      {
                          $pairs1 = $this->wordLetterPairs(strtoupper($str1));
                          $pairs2 = $this->wordLetterPairs(strtoupper($str2));
                  
                          $intersection = 0;
                  
                          $union = count($pairs1) + count($pairs2);
                  
                          for ($i=0; $i < count($pairs1); $i++)
                          {
                              $pair1 = $pairs1[$i];
                  
                              $pairs2 = array_values($pairs2);
                              for($j = 0; $j < count($pairs2); $j++)
                              {
                                  $pair2 = $pairs2[$j];
                                  if ($pair1 === $pair2)
                                  {
                                      $intersection++;
                                      unset($pairs2[$j]);
                                      break;
                                  }
                              }
                          }
                  
                          return (2.0*$intersection)/$union;
                      }
                  }
                  

                  【讨论】:

                    【解决方案14】:

                    这个讨论真的很有帮助,谢谢。我将算法转换为 VBA 以与 Excel 一起使用,并编写了几个版本的工作表函数,一个用于简单比较一对字符串,另一个用于将一个字符串与字符串范围/数组进行比较。 strSimLookup 版本将最后一个最佳匹配作为字符串、数组索引或相似度度量返回。

                    此实施产生的结果与 Simon White 网站上的亚马逊示例中列出的结果相同,但在低分比赛中存在一些小例外;不确定差异在哪里,可能是 VBA 的 Split 函数,但我没有调查,因为它对我的目的工作正常。

                    'Implements functions to rate how similar two strings are on
                    'a scale of 0.0 (completely dissimilar) to 1.0 (exactly similar)
                    'Source:   http://www.catalysoft.com/articles/StrikeAMatch.html
                    'Author: Bob Chatham, bob.chatham at gmail.com
                    '9/12/2010
                    
                    Option Explicit
                    
                    Public Function stringSimilarity(str1 As String, str2 As String) As Variant
                    'Simple version of the algorithm that computes the similiarity metric
                    'between two strings.
                    'NOTE: This verision is not efficient to use if you're comparing one string
                    'with a range of other values as it will needlessly calculate the pairs for the
                    'first string over an over again; use the array-optimized version for this case.
                    
                        Dim sPairs1 As Collection
                        Dim sPairs2 As Collection
                    
                        Set sPairs1 = New Collection
                        Set sPairs2 = New Collection
                    
                        WordLetterPairs str1, sPairs1
                        WordLetterPairs str2, sPairs2
                    
                        stringSimilarity = SimilarityMetric(sPairs1, sPairs2)
                    
                        Set sPairs1 = Nothing
                        Set sPairs2 = Nothing
                    
                    End Function
                    
                    Public Function strSimA(str1 As Variant, rRng As Range) As Variant
                    'Return an array of string similarity indexes for str1 vs every string in input range rRng
                        Dim sPairs1 As Collection
                        Dim sPairs2 As Collection
                        Dim arrOut As Variant
                        Dim l As Long, j As Long
                    
                        Set sPairs1 = New Collection
                    
                        WordLetterPairs CStr(str1), sPairs1
                    
                        l = rRng.Count
                        ReDim arrOut(1 To l)
                        For j = 1 To l
                            Set sPairs2 = New Collection
                            WordLetterPairs CStr(rRng(j)), sPairs2
                            arrOut(j) = SimilarityMetric(sPairs1, sPairs2)
                            Set sPairs2 = Nothing
                        Next j
                    
                        strSimA = Application.Transpose(arrOut)
                    
                    End Function
                    
                    Public Function strSimLookup(str1 As Variant, rRng As Range, Optional returnType) As Variant
                    'Return either the best match or the index of the best match
                    'depending on returnTYype parameter) between str1 and strings in rRng)
                    ' returnType = 0 or omitted: returns the best matching string
                    ' returnType = 1           : returns the index of the best matching string
                    ' returnType = 2           : returns the similarity metric
                    
                        Dim sPairs1 As Collection
                        Dim sPairs2 As Collection
                        Dim metric, bestMetric As Double
                        Dim i, iBest As Long
                        Const RETURN_STRING As Integer = 0
                        Const RETURN_INDEX As Integer = 1
                        Const RETURN_METRIC As Integer = 2
                    
                        If IsMissing(returnType) Then returnType = RETURN_STRING
                    
                        Set sPairs1 = New Collection
                    
                        WordLetterPairs CStr(str1), sPairs1
                    
                        bestMetric = -1
                        iBest = -1
                    
                        For i = 1 To rRng.Count
                            Set sPairs2 = New Collection
                            WordLetterPairs CStr(rRng(i)), sPairs2
                            metric = SimilarityMetric(sPairs1, sPairs2)
                            If metric > bestMetric Then
                                bestMetric = metric
                                iBest = i
                            End If
                            Set sPairs2 = Nothing
                        Next i
                    
                        If iBest = -1 Then
                            strSimLookup = CVErr(xlErrValue)
                            Exit Function
                        End If
                    
                        Select Case returnType
                        Case RETURN_STRING
                            strSimLookup = CStr(rRng(iBest))
                        Case RETURN_INDEX
                            strSimLookup = iBest
                        Case Else
                            strSimLookup = bestMetric
                        End Select
                    
                    End Function
                    
                    Public Function strSim(str1 As String, str2 As String) As Variant
                        Dim ilen, iLen1, ilen2 As Integer
                    
                        iLen1 = Len(str1)
                        ilen2 = Len(str2)
                    
                        If iLen1 >= ilen2 Then ilen = ilen2 Else ilen = iLen1
                    
                        strSim = stringSimilarity(Left(str1, ilen), Left(str2, ilen))
                    
                    End Function
                    
                    Sub WordLetterPairs(str As String, pairColl As Collection)
                    'Tokenize str into words, then add all letter pairs to pairColl
                    
                        Dim Words() As String
                        Dim word, nPairs, pair As Integer
                    
                        Words = Split(str)
                    
                        If UBound(Words) < 0 Then
                            Set pairColl = Nothing
                            Exit Sub
                        End If
                    
                        For word = 0 To UBound(Words)
                            nPairs = Len(Words(word)) - 1
                            If nPairs > 0 Then
                                For pair = 1 To nPairs
                                    pairColl.Add Mid(Words(word), pair, 2)
                                Next pair
                            End If
                        Next word
                    
                    End Sub
                    
                    Private Function SimilarityMetric(sPairs1 As Collection, sPairs2 As Collection) As Variant
                    'Helper function to calculate similarity metric given two collections of letter pairs.
                    'This function is designed to allow the pair collections to be set up separately as needed.
                    'NOTE: sPairs2 collection will be altered as pairs are removed; copy the collection
                    'if this is not the desired behavior.
                    'Also assumes that collections will be deallocated somewhere else
                    
                        Dim Intersect As Double
                        Dim Union As Double
                        Dim i, j As Long
                    
                        If sPairs1.Count = 0 Or sPairs2.Count = 0 Then
                            SimilarityMetric = CVErr(xlErrNA)
                            Exit Function
                        End If
                    
                        Union = sPairs1.Count + sPairs2.Count
                        Intersect = 0
                    
                        For i = 1 To sPairs1.Count
                            For j = 1 To sPairs2.Count
                                If StrComp(sPairs1(i), sPairs2(j)) = 0 Then
                                    Intersect = Intersect + 1
                                    sPairs2.Remove j
                                    Exit For
                                End If
                            Next j
                        Next i
                    
                        SimilarityMetric = (2 * Intersect) / Union
                    
                    End Function
                    

                    【讨论】:

                    【解决方案15】:

                    这里是 R 版本:

                    get_bigrams <- function(str)
                    {
                      lstr = tolower(str)
                      bigramlst = list()
                      for(i in 1:(nchar(str)-1))
                      {
                        bigramlst[[i]] = substr(str, i, i+1)
                      }
                      return(bigramlst)
                    }
                    
                    str_similarity <- function(str1, str2)
                    {
                       pairs1 = get_bigrams(str1)
                       pairs2 = get_bigrams(str2)
                       unionlen  = length(pairs1) + length(pairs2)
                       hit_count = 0
                       for(x in 1:length(pairs1)){
                            for(y in 1:length(pairs2)){
                                if (pairs1[[x]] == pairs2[[y]])
                                    hit_count = hit_count + 1
                            }
                       }
                       return ((2.0 * hit_count) / unionlen)
                    }
                    

                    【讨论】:

                    • 这种算法更好,但对于大数据来说速度很慢。我的意思是,如果必须将 10000 个单词与 15000 个其他单词进行比较,那就太慢了。我们可以在速度方面提高它的性能吗??
                    【解决方案16】:

                    漂亮的 Scala 版本:

                      def pairDistance(s1: String, s2: String): Double = {
                    
                        def strToPairs(s: String, acc: List[String]): List[String] = {
                          if (s.size < 2) acc
                          else strToPairs(s.drop(1),
                            if (s.take(2).contains(" ")) acc else acc ::: List(s.take(2)))
                        }
                    
                        val lst1 = strToPairs(s1.toUpperCase, List())
                        val lst2 = strToPairs(s2.toUpperCase, List())
                    
                        (2.0 * lst2.intersect(lst1).size) / (lst1.size + lst2.size)
                    
                      }
                    

                    【讨论】:

                      【解决方案17】:

                      基于 Michael La Voie 的出色 C# 版本,根据将其设为扩展方法的要求,这就是我想出的。这样做的主要好处是您可以按匹配百分比对通用列表进行排序。例如,假设您的对象中有一个名为“City”的字符串字段。用户搜索“Chester”,您希望按匹配的降序返回结果。例如,您希望 Chester 的文字匹配显示在 Rochester 之前。为此,请向您的对象添加两个新属性:

                          public string SearchText { get; set; }
                          public double PercentMatch
                          {
                              get
                              {
                                  return City.ToUpper().PercentMatchTo(this.SearchText.ToUpper());
                              }
                          }
                      

                      然后在每个对象上,将 SearchText 设置为用户搜索的内容。然后您可以使用以下内容轻松对其进行排序:

                          zipcodes = zipcodes.OrderByDescending(x => x.PercentMatch);
                      

                      以下是使其成为扩展方法的轻微修改:

                          /// <summary>
                          /// This class implements string comparison algorithm
                          /// based on character pair similarity
                          /// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
                          /// </summary>
                          public static double PercentMatchTo(this string str1, string str2)
                          {
                              List<string> pairs1 = WordLetterPairs(str1.ToUpper());
                              List<string> pairs2 = WordLetterPairs(str2.ToUpper());
                      
                              int intersection = 0;
                              int union = pairs1.Count + pairs2.Count;
                      
                              for (int i = 0; i < pairs1.Count; i++)
                              {
                                  for (int j = 0; j < pairs2.Count; j++)
                                  {
                                      if (pairs1[i] == pairs2[j])
                                      {
                                          intersection++;
                                          pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success
                      
                                          break;
                                      }
                                  }
                              }
                      
                              return (2.0 * intersection) / union;
                          }
                      
                          /// <summary>
                          /// Gets all letter pairs for each
                          /// individual word in the string
                          /// </summary>
                          /// <param name="str"></param>
                          /// <returns></returns>
                          private static List<string> WordLetterPairs(string str)
                          {
                              List<string> AllPairs = new List<string>();
                      
                              // Tokenize the string and put the tokens/words into an array
                              string[] Words = Regex.Split(str, @"\s");
                      
                              // For each word
                              for (int w = 0; w < Words.Length; w++)
                              {
                                  if (!string.IsNullOrEmpty(Words[w]))
                                  {
                                      // Find the pairs of characters
                                      String[] PairsInWord = LetterPairs(Words[w]);
                      
                                      for (int p = 0; p < PairsInWord.Length; p++)
                                      {
                                          AllPairs.Add(PairsInWord[p]);
                                      }
                                  }
                              }
                      
                              return AllPairs;
                          }
                      
                          /// <summary>
                          /// Generates an array containing every 
                          /// two consecutive letters in the input string
                          /// </summary>
                          /// <param name="str"></param>
                          /// <returns></returns>
                          private static  string[] LetterPairs(string str)
                          {
                              int numPairs = str.Length - 1;
                      
                              string[] pairs = new string[numPairs];
                      
                              for (int i = 0; i < numPairs; i++)
                              {
                                  pairs[i] = str.Substring(i, 2);
                              }
                      
                              return pairs;
                          }
                      

                      【讨论】:

                      • 我认为你最好使用默认值为 false 的 bool isCaseSensitive - 即使它是 true 实现更清晰
                      【解决方案18】:

                      算法的更快的 PHP 版本:

                      /**
                       *
                       * @param $str
                       * @return mixed
                       */
                      private static function wordLetterPairs ($str)
                      {
                          $allPairs = array();
                      
                          // Tokenize the string and put the tokens/words into an array
                      
                          $words = explode(' ', $str);
                      
                          // For each word
                          for ($w = 0; $w < count($words); $w ++) {
                              // Find the pairs of characters
                              $pairsInWord = self::letterPairs($words[$w]);
                      
                              for ($p = 0; $p < count($pairsInWord); $p ++) {
                                  $allPairs[$pairsInWord[$p]] = $pairsInWord[$p];
                              }
                          }
                      
                          return array_values($allPairs);
                      }
                      
                      /**
                       *
                       * @param $str
                       * @return array
                       */
                      private static function letterPairs ($str)
                      {
                          $numPairs = mb_strlen($str) - 1;
                          $pairs = array();
                      
                          for ($i = 0; $i < $numPairs; $i ++) {
                              $pairs[$i] = mb_substr($str, $i, 2);
                          }
                      
                          return $pairs;
                      }
                      
                      /**
                       *
                       * @param $str1
                       * @param $str2
                       * @return float
                       */
                      public static function compareStrings ($str1, $str2)
                      {
                          $pairs1 = self::wordLetterPairs(mb_strtolower($str1));
                          $pairs2 = self::wordLetterPairs(mb_strtolower($str2));
                      
                      
                          $union = count($pairs1) + count($pairs2);
                      
                          $intersection = count(array_intersect($pairs1, $pairs2));
                      
                          return (2.0 * $intersection) / $union;
                      }
                      

                      对于我拥有的数据(大约 2300 次比较),Igal Alkon 解决方案的运行时间为 0.58 秒,而我的解决方案为 0.35 秒。

                      【讨论】:

                        【解决方案19】:

                        骰子系数算法(Simon White / marzagao 的答案)在 Ruby 中实现 amatch gem 中的 pair_distance_similar 方法

                        https://github.com/flori/amatch

                        此 gem 还包含许多近似匹配和字符串比较算法的实现:Levenshtein 编辑距离、Sellers 编辑距离、Hamming 距离、最长公共子序列长度、最长公共子串长度、对距离度量、Jaro -Winkler 度量。

                        【讨论】:

                          【解决方案20】:

                          我将 Simon White 的算法翻译成 PL/pgSQL。这是我的贡献。

                          <!-- language: lang-sql -->
                          
                          create or replace function spt1.letterpairs(in p_str varchar) 
                          returns varchar  as 
                          $$
                          declare
                          
                              v_numpairs integer := length(p_str)-1;
                              v_pairs varchar[];
                          
                          begin
                          
                              for i in 1 .. v_numpairs loop
                                  v_pairs[i] := substr(p_str, i, 2);
                              end loop;
                          
                              return v_pairs;
                          
                          end;
                          $$ language 'plpgsql';
                          
                          --===================================================================
                          
                          create or replace function spt1.wordletterpairs(in p_str varchar) 
                          returns varchar as
                          $$
                          declare
                              v_allpairs varchar[];
                              v_words varchar[];
                              v_pairsinword varchar[];
                          begin
                              v_words := regexp_split_to_array(p_str, '[[:space:]]');
                          
                              for i in 1 .. array_length(v_words, 1) loop
                                  v_pairsinword := spt1.letterpairs(v_words[i]);
                          
                                  if v_pairsinword is not null then
                                      for j in 1 .. array_length(v_pairsinword, 1) loop
                                          v_allpairs := v_allpairs || v_pairsinword[j];
                                      end loop;
                                  end if;
                          
                              end loop;
                          
                          
                              return v_allpairs;
                          end;
                          $$ language 'plpgsql';
                          
                          --===================================================================
                          
                          create or replace function spt1.arrayintersect(ANYARRAY, ANYARRAY)
                          returns anyarray as 
                          $$
                              select array(select unnest($1) intersect select unnest($2))
                          $$ language 'sql';
                          
                          --===================================================================
                          
                          create or replace function spt1.comparestrings(in p_str1 varchar, in p_str2 varchar)
                          returns float as
                          $$
                          declare
                              v_pairs1 varchar[];
                              v_pairs2 varchar[];
                              v_intersection integer;
                              v_union integer;
                          begin
                              v_pairs1 := wordletterpairs(upper(p_str1));
                              v_pairs2 := wordletterpairs(upper(p_str2));
                              v_union := array_length(v_pairs1, 1) + array_length(v_pairs2, 1); 
                          
                              v_intersection := array_length(arrayintersect(v_pairs1, v_pairs2), 1);
                          
                              return (2.0 * v_intersection / v_union);
                          end;
                          $$ language 'plpgsql'; 
                          

                          【讨论】:

                          • 在我的没有 plruby 支持的 PostgreSQL 上工作!谢谢!
                          • 此端口不正确。确切的字符串不返回 1。
                          【解决方案21】:

                          大家好,我在 javascript 中尝试过,但我是新手,有人知道更快的方法吗?

                          function get_bigrams(string) {
                              // Takes a string and returns a list of bigrams
                              var s = string.toLowerCase();
                              var v = new Array(s.length-1);
                              for (i = 0; i< v.length; i++){
                                  v[i] =s.slice(i,i+2);
                              }
                              return v;
                          }
                          
                          function string_similarity(str1, str2){
                              /*
                              Perform bigram comparison between two strings
                              and return a percentage match in decimal form
                              */
                              var pairs1 = get_bigrams(str1);
                              var pairs2 = get_bigrams(str2);
                              var union = pairs1.length + pairs2.length;
                              var hit_count = 0;
                              for (x in pairs1){
                                  for (y in pairs2){
                                      if (pairs1[x] == pairs2[y]){
                                          hit_count++;
                                      }
                                  }
                              }
                              return ((2.0 * hit_count) / union);
                          }
                          
                          
                          var w1 = 'Healed';
                          var word =['Heard','Healthy','Help','Herded','Sealed','Sold']
                          for (w2 in word){
                              console.log('Healed --- ' + word[w2])
                              console.log(string_similarity(w1,word[w2]));
                          }
                          

                          【讨论】:

                          • 这个实现不正确。 bigram 函数因长度为 0 的输入而中断。 string_similarity 方法在第二个循环内未正确中断,这可能导致多次计数对,导致返回值超过 100%。而且您还忘记声明xy,并且您不应该使用for..in.. 循环遍历循环(改用for(..;..;..))。
                          【解决方案22】:

                          String Similarity Metrics 包含字符串比较中使用的许多不同指标的概述(Wikipedia 也有概述)。许多这些指标在库simmetrics 中实现。

                          另一个未包含在给定概述中的指标示例是 compression distance(试图近似于 Kolmogorov's complexity),它可用于比您提供的文本更长的文本。

                          您还可以考虑查看更广泛的主题 Natural Language ProcessingThese R 包可以让你快速入门(或至少提供一些想法)。

                          最后一次编辑 - 在 SO 上搜索有关此主题的其他问题,有很多相关问题。

                          【讨论】:

                            【解决方案23】:

                            Levenshtein 距离除以第一个字符串的长度(或者除以我的两个字符串的最小/最大/平均长度)怎么样?到目前为止,这对我有用。

                            【讨论】:

                            • 但是,引用关于这个主题的另一篇文章,它返回的内容通常是“不稳定的”。它将“echo”列为与“dog”非常相似。
                            • @Nox:这个回复的“除以第一个字符串的长度”部分很重要。此外,这比广受赞誉的 Dice 算法在拼写错误和换位错误,甚至是常见的共轭(例如,考虑比较“swim”和“swam”)方面表现得更好。
                            【解决方案24】:

                            Catalysoft 的 Simon White 写了一篇关于比较相邻字符对的非常聪明的算法的文章,该算法非常适合我的目的:

                            http://www.catalysoft.com/articles/StrikeAMatch.html

                            Simon 有一个 Java 版本的算法,下面我写了一个 PL/Ruby 版本(取自 Mark Wong-VanHaren 在相关论坛条目评论中完成的普通 ruby​​ 版本),以便我可以在我的PostgreSQL 查询:

                            CREATE FUNCTION string_similarity(str1 varchar, str2 varchar)
                            RETURNS float8 AS '
                            
                            str1.downcase! 
                            pairs1 = (0..str1.length-2).collect {|i| str1[i,2]}.reject {
                              |pair| pair.include? " "}
                            str2.downcase! 
                            pairs2 = (0..str2.length-2).collect {|i| str2[i,2]}.reject {
                              |pair| pair.include? " "}
                            union = pairs1.size + pairs2.size 
                            intersection = 0 
                            pairs1.each do |p1| 
                              0.upto(pairs2.size-1) do |i| 
                                if p1 == pairs2[i] 
                                  intersection += 1 
                                  pairs2.slice!(i) 
                                  break 
                                end 
                              end 
                            end 
                            (2.0 * intersection) / union
                            
                            ' LANGUAGE 'plruby';
                            

                            像魅力一样工作!

                            【讨论】:

                            • 有趣的是,Simon 的方法与 q-gram 和 Dice 系数等方法有很多共同之处。
                            • FWIW,您的算法比 en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Dice's_coefficient 上提供的算法快 5 倍(根据 Benchmark.bmbm 超过 50,000 次重复)
                            • 只是添加一些可能会帮助人们的东西 - 在他的网站上的 marzagao 的实现 (Java) 中,它看起来也像端口,他们正在确定 2 个字符部分的匹配。我发现您可以将其调整为 3 或 4 个字符串部分以进行匹配,这将假设更少的拼写错误,但也会在长搜索查询或大型搜索集中过滤更多垃圾。你的里程可能会有所不同,只是想我会把它扔在那里。
                            • @JasonSundram 是对的——事实上,正如作者在“附录”(底部页面)。
                            • 在比较具有单个独立字母作为差异的字符串时,这将返回 1 的“分数”(100% 匹配),例如:string_similarity("vitamin B", "vitamin C") #=&gt; 1,有没有一种简单的方法可以防止这种行为?
                            猜你喜欢
                            • 2013-02-24
                            • 2011-04-04
                            • 2011-12-10
                            • 2014-11-02
                            • 2011-10-03
                            • 2017-02-28
                            • 2010-12-24
                            • 1970-01-01
                            • 1970-01-01
                            相关资源
                            最近更新 更多