Кращий алгоритм ранжування подібності для рядків змінної довжини


152

Я шукаю алгоритм подібності рядків, який дає кращі результати на рядках змінної довжини, ніж ті, які зазвичай пропонуються (відстань Левенштейна, soundex тощо).

Наприклад,

Дано рядок A: "Роберт",

Потім рядок B: "Емі Робертсон"

було б краще відповідати, ніж

Рядок C: "Річард"

Також бажано, щоб цей алгоритм був мовним агностиком (також працює іншими мовами, крім англійської).



Також ознайомтесь: коефіцієнт кістки
avid_useR

Відповіді:


155

Саймон Уайт з Catalysoft написав статтю про дуже розумний алгоритм, який порівнює сусідні пари символів, які дуже добре працюють для моїх цілей:

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

Саймон має версію алгоритму Java, і нижче я написав його версію PL / 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';

Працює як шарм!


32
Ви знайшли відповідь і все це написали за 4 хвилини? Вражає!
Метт J

28
Я підготував свою відповідь після деяких досліджень та впровадження. Я ставлю це тут на користь тому, хто ще приходить шукати в SO для практичної відповіді, використовуючи альтернативний алгоритм, оскільки більшість відповідей у ​​відповідних питаннях, здається, обертаються навколо levenshtein або soundex.
marzagao

18
Тільки те, що я шукав. Ти вийдеш за мене заміж?
BlackTea

6
@JasonSundram прав - насправді, це є добре відомим коефіцієнтом Dice на биграмм символьного рівня, як пише автор в «додаванні» ( в нижній частині сторінки).
Фред Фоо

4
Це повертає "бал" 1 (100% збігу) при порівнянні рядків, що мають одну різницю, як різницю, як цей приклад: string_similarity("vitamin B", "vitamin C") #=> 1чи є простий спосіб запобігти подібній поведінці?
MrYoshiji

77

Відповідь marzagao чудова. Я перетворив його на C #, тому подумав, що опублікую його тут:

Вставити посилання

/// <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;
    }
}

2
+100 якби я міг, ти просто врятував мене важким робочим днем! Ура.
vvohra87

1
Дуже хороша! Єдина пропозиція, яку я маю, - перетворити це на розширення.
Левитикон

+1! Чудово, що він працює, з незначними модифікаціями і для Java. І це, схоже, повертає кращі відповіді, ніж Левенштейн.
Xyene

1
Нижче я додав версію, що перетворює це в метод розширення. Дякуємо за оригінальну версію та дивовижний переклад.
Френк Рундац

@Michael La Voie Дякую, це дуже приємно! Хоча невелика проблема (2.0 * intersection) / union- я отримую Double.NaN при порівнянні двох порожніх рядків.
Vojtěch Dohnal

41

Ось ще одна версія відповіді marzagao , написана в 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()

2
Існує невеликий помилку у подібності string_s, коли в слові є дублікати nграм, в результаті чого результат> 1 для однакових рядків. Додавання "перерви" після "hit_count + = 1" виправляє його.
jbaiter

1
@jbaiter: Хороший улов. Я змінив його, щоб відобразити ваші зміни.
Джон Рутлідж

3
У статті Саймона Уайта він говорить: "Зауважте, що кожного разу, коли зустрічається відповідність, ця пара символів видаляється зі списку другого масиву, щоб запобігти нам декілька разів зіставляти одну і ту ж пару символів. (Інакше" GGGGG "отримає ідеальну відповідність проти "GG".) "Я б змінив це твердження, щоб сказати, що воно дасть вищий, ніж ідеальний матч. Не враховуючи це, також здається, що алгоритм не є транзитивним (подібність (x, y) = / = подібність (y, x)). Додавання пар2.remove (y) після рядка hit_count + = 1 усуває проблему.
NinjaMeTimbers

17

Ось моя реалізація PHP запропонованого алгоритму StrikeAMatch від Саймона Уайта. Перевагами (як написано у посиланні) є:

  • Справжнє відображення лексичної подібності - рядки з невеликими відмінностями слід визнати подібними. Зокрема, значне перекриття підрядків повинно вказувати на високий рівень подібності між рядками.

  • Надійність до змін порядку слів - два рядки, які містять однакові слова, але в іншому порядку, слід визнати подібними. З іншого боку, якщо одна рядок є лише випадковою анаграмою символів, що містяться в іншій, її слід (як правило) визнавати різницею.

  • Мовна незалежність - алгоритм повинен працювати не тільки англійською мовою, але й багатьма різними мовами.

<?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;
    }
}

17

Більш коротка версія відповіді Джона Рутлідж :

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змінна - це відходи ліній.
Chibueze Opata

14

Ця дискусія була дуже корисною, дякую. Я перетворив алгоритм у VBA для використання з Excel і написав кілька версій функції робочого аркуша, один для простого порівняння пари рядків, інший для порівняння однієї рядка з діапазоном / масивом рядків. Версія strSimLookup повертає або останню найкращу відповідність у вигляді рядка, індексу масиву або показника подібності.

Ця реалізація дає ті самі результати, що наведені на прикладі Amazon на веб-сайті Саймона Уайта, за кількома незначними винятками у матчах з низьким рахунком; не впевнений, де різниця повзає, може бути функція розбиття VBA, але я не досліджував, як це працює добре для моїх цілей.

'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

@bchatham Це виглядає надзвичайно корисно, але я новачок у VBA і трохи оскаржений кодом. Чи можна опублікувати файл Excel, який використовує ваш внесок? Для своїх цілей я сподіваюся використовувати його для співпадіння подібних імен з одного стовпця в Excel з приблизно 1000 записами (витяг тут: dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ). Тоді я буду використовувати сірники як синоніми в пошуку людей. (див. також softwarerecs.stackexchange.com/questions/38227/… )
bjornte

12

Вибачте, відповідь автор не вигадав. Це добре відомий алгоритм, який вперше був представлений корпорацією Digital Equipment Corporation і його часто називають шинглінг.

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


10

Я переклав алгоритм Саймона Уайта на 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'; 

Працює на моєму PostgreSQL, який не має підтримки plruby! Дякую!
hostnik

Дякую! Як би ви це зробили в Oracle SQL?
olovholm

Цей порт неправильний. Точні рядки не повертаються 1.
Брендон Вігфілд

9

Метрики подібності рядків містить огляд багатьох різних показників, що використовуються при порівнянні рядків (у Вікіпедії також є огляд). Значна частина цих показників реалізована в симметриці бібліотеки .

Ще один приклад метрики, не включеної в даний огляд, - наприклад, відстань стиснення (спроба наблизити складність Колмогорова ), яка може бути використана для трохи довших текстів, ніж представлена ​​вами.

Ви також можете розглянути набагато ширший предмет обробки природних мов . Ці R-пакети можуть швидко розпочати роботу (або принаймні дати кілька ідей).

І остання редакція - пошук інших запитань з цього приводу в SO, існує досить багато пов'язаних.


9

Більш швидка версія алгоритму 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 порівнянь) у мене був час роботи 0,58 сек з розчином Igal Alkon проти 0,35 сек з моїм.


9

Версія в прекрасній 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)

  }

6

Ось версія 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 іншими словами, це занадто повільно. Чи можемо ми збільшити його показники в швидкості ??
indra_patil

6

Опублікування відповіді marzagao в C99, натхненний цими алгоритмами

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);
}

Деякі тести на основі оригінальної статті :

#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;
}

5

Спираючись на дивовижну версію C # Майкла Ла Воя, відповідно до запиту зробити його методом розширення, ось що я придумав. Основна перевага цього способу полягає в тому, що ви можете сортувати загальний список за відсотковою відповідністю. Наприклад, розгляньте, що у вашому об’єкті є рядок із назвою "Місто". Користувач шукає "Честер", і ви хочете повернути результати у порядку зменшення. Наприклад, ви хочете, щоб перед Рочестером з’явились буквальні матчі Честера. Для цього додайте до об'єкта дві нові властивості:

    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;
    }

Я думаю, вам було б краще використовувати bool isCaseSensitive зі значенням за замовчуванням false - навіть якщо це правда, реалізація набагато чистіша
Йорданія,

5

Моя реалізація 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];
      });
    }
  };
})();

1
Помилка / помилка! for(var j = 0; j < pairs1.length; j++){має бутиfor(var j = 0; j < pairs2.length; j++){
Searle

3

Алгоритм коефіцієнта кубика (відповідь Саймона Уайта / відповідь marzagao) реалізований в Ruby методом pair_distance_s аналогічно в камені amatch

https://github.com/flori/amatch

Цей дорогоцінний камінь також містить реалізацію ряду приблизних алгоритмів порівняння та порівняння рядків: відстань редагування Левенштейна, відстань редагування продавців, відстань Хеммінга, найдовша загальна довжина підрядки, найдовша загальна довжина підрядки, метрика відстані пари, метрика Яро-Вінклера .


2

Версія 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

2

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))

1

А як щодо відстані Левенштейна, поділеної на довжину першого рядка (або, як варіант, поділити мою min / max / avg довжину обох рядків)? Це працювало для мене досі.


Однак, щоб процитувати ще одну публікацію на цю тему, те, що вона повертає, часто є "хаотичним". Він називає "відлунням" настільки ж схожим на "собаку".
Xyene

@Nox: Частина цієї відповіді "розділена на довжину першого рядка" є важливою. Крім того, це краще, ніж алгоритм сильно розхваленого Dice щодо помилок друку і транспозиції, і навіть звичайних сполучень (розгляньте, наприклад, порівняння "плавання" та "плавання").
Логан Пікап

1

Ей, хлопці, я спробував це у 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]));
}

Ця реалізація є неправильною. Функція біграму переривається для введення довжини 0. Метод подібності string_ не належним чином розбивається всередині другого циклу, що може призвести до підрахунку пар декілька разів, що призводить до повернення значення, яке перевищує 100%. І ви також забули оголосити xі y, і вам не слід перебирати цикли, використовуючи for..in..цикл (використовуйте for(..;..;..)замість цього).
Роб Ш

1

Ось ще одна версія подібності, що базується на індексі 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);
}

1

Я шукав чистого рубінового виконання алгоритму, зазначеного у відповіді @ marzagao. На жаль, посилання, вказане @marzagao, порушено. У відповіді @ s01ipsist він вказав на ruby ​​gem amatch, де реалізація не є чистою рубіном. Так що я Searchd трохи і знайшов дорогоцінний камінь fuzzy_match який має чисту реалізацію рубіновий (хоча це використання дорогоцінних каменів amatch) на тут . Сподіваюся, це допоможе комусь, як я.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.