Algoritma peringkat kesamaan yang lebih baik untuk string panjang variabel


152

Saya mencari algoritma kesamaan string yang menghasilkan hasil yang lebih baik pada string panjang variabel daripada yang biasanya disarankan (levenshtein distance, soundex, dll).

Sebagai contoh,

Diberikan string A: "Robert",

Kemudian string B: "Amy Robertson"

akan menjadi pertandingan yang lebih baik daripada

String C: "Richard"

Juga, lebih disukai, algoritma ini harus agnostik bahasa (juga berfungsi dalam bahasa selain bahasa Inggris).



Lihat juga: Koefisien Dice
avid_useR

Jawaban:


155

Simon White dari Catalysoft menulis artikel tentang algoritma yang sangat pintar yang membandingkan pasangan karakter yang berdekatan yang bekerja sangat baik untuk tujuan saya:

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

Simon memiliki algoritma versi Java dan di bawah ini saya menulis versi PL / Ruby-nya (diambil dari versi ruby ​​biasa yang dilakukan di komentar entri forum terkait oleh Mark Wong-VanHaren) sehingga saya dapat menggunakannya dalam kueri PostgreSQL saya:

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

Bekerja seperti pesona!


32
Anda menemukan jawabannya dan menulis semua itu dalam 4 menit? Impresif!
Matt J

28
Saya menyiapkan jawaban saya setelah beberapa penelitian dan implementasi. Saya menaruhnya di sini untuk kepentingan siapa pun yang datang mencari SO untuk jawaban praktis menggunakan algoritma alternatif karena sebagian besar jawaban dalam pertanyaan terkait tampaknya berputar di sekitar levenshtein atau soundex.
marzagao

18
Apa yang saya cari. Maukah Anda menikah dengan saya?
BlackTea

6
@JasonSundram benar - pada kenyataannya, ini adalah koefisien Dadu terkenal pada bigrams tingkat karakter, seperti yang ditulis oleh penulis di "addendum" (bagian bawah halaman).
Fred Foo

4
Ini mengembalikan "skor" 1 (kecocokan 100%) ketika membandingkan string yang memiliki satu huruf terpisah sebagai perbedaan, seperti contoh ini:, string_similarity("vitamin B", "vitamin C") #=> 1apakah ada cara mudah untuk mencegah perilaku semacam ini?
MrYoshiji

77

Jawaban marzagao luar biasa. Saya mengonversinya menjadi C # jadi saya pikir saya akan mempostingnya di sini:

Tautan Pastebin

/// <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 jika saya bisa, Anda baru saja menyelamatkan saya dari teman kerja yang sulit! Bersulang.
vvohra87

1
Sangat bagus! Satu-satunya saran yang saya miliki, akan menjadikan ini sebagai perpanjangan.
Levitikon

+1! Hebat itu berfungsi, dengan sedikit modifikasi untuk Java juga. Dan itu tampaknya mengembalikan respons yang lebih baik daripada Levenshtein.
Xyene

1
Saya menambahkan versi yang mengonversikan ini ke metode ekstensi di bawah ini. Terima kasih untuk versi aslinya dan terjemahannya yang luar biasa.
Frank Rundatz

@Michael La Voie Terima kasih, ini sangat bagus! Meskipun sedikit masalah dengan (2.0 * intersection) / union- Saya mendapatkan Double.NaN saat membandingkan dua string kosong.
Vojtěch Dohnal

41

Ini adalah versi lain dari jawaban marzagao , yang ini ditulis dengan 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
Ada bug kecil di string_similarity ketika ada duplikat ngrams dalam sebuah kata, menghasilkan skor> 1 untuk string yang identik. Menambahkan 'break' setelah "hit_count + = 1" memperbaikinya.
jbaiter

1
@ jbaiter: Tangkapan bagus. Saya mengubahnya untuk mencerminkan perubahan Anda.
John Rutledge

3
Dalam artikel Simon White, ia mengatakan "Perhatikan bahwa setiap kali kecocokan ditemukan, pasangan karakter dihapus dari daftar array kedua untuk mencegah kita dari pencocokan terhadap pasangan karakter yang sama beberapa kali. (Jika tidak, 'GGGGG' akan mencetak pasangan yang sempurna terhadap 'GG'.) "Saya akan mengubah pernyataan ini untuk mengatakan bahwa itu akan memberikan kecocokan yang lebih tinggi dari sempurna. Tanpa memperhitungkan hal ini, tampaknya juga hasilnya algoritma tidak transitif (kesamaan (x, y) = / = kesamaan (y, x)). Menambahkan pairs2.remove (y) setelah baris hit_count + = 1 memperbaiki masalah.
NinjaMeTimbers

17

Inilah implementasi PHP saya dari algoritma StrikeAMatch yang disarankan, oleh Simon White. keuntungannya (seperti yang tertulis di tautan) adalah:

  • Sebuah refleksi sejati dari kesamaan leksikal - string dengan perbedaan kecil harus diakui sebagai yang serupa. Secara khusus, tumpang tindih substrat yang signifikan harus menunjukkan tingkat kesamaan yang tinggi antara string.

  • Ketangguhan untuk perubahan urutan kata - dua string yang berisi kata-kata yang sama, tetapi dalam urutan yang berbeda, harus diakui sama. Di sisi lain, jika satu string hanyalah anagram acak dari karakter yang terkandung dalam yang lain, maka harus (biasanya) diakui sebagai berbeda.

  • Kemandirian Bahasa - algoritma harus bekerja tidak hanya dalam bahasa Inggris, tetapi dalam banyak bahasa yang berbeda.

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

Versi singkat dari jawaban John Rutledge :

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

Bahkan intersectionvariabelnya adalah baris limbah.
Chibueze Opata

14

Diskusi ini sangat membantu, terima kasih. Saya mengonversi algoritme menjadi VBA untuk digunakan dengan Excel dan menulis beberapa versi fungsi lembar kerja, satu untuk perbandingan sederhana sepasang string, yang lain untuk membandingkan satu string dengan rentang / array string. Versi strSimLookup mengembalikan kecocokan terbaik terakhir sebagai string, indeks array, atau metrik kesamaan.

Implementasi ini menghasilkan hasil yang sama dengan yang tercantum dalam contoh Amazon di situs web Simon White dengan beberapa pengecualian kecil pada pertandingan dengan skor rendah; tidak yakin di mana perbedaannya merayap, bisa menjadi fungsi Split VBA, tapi saya belum menyelidiki karena berfungsi dengan baik untuk tujuan saya.

'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 Ini terlihat sangat berguna, tapi saya baru di VBA dan sedikit ditantang oleh kode. Apakah mungkin bagi Anda untuk memposting file Excel yang memanfaatkan kontribusi Anda? Untuk tujuan saya, saya berharap untuk menggunakannya untuk mencocokkan nama depan yang serupa dari satu kolom di Excel dengan sekitar 1.000 entri (kutipan di sini: dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ). Saya kemudian akan menggunakan kecocokan sebagai sinonim dalam pencarian orang. (lihat juga softwarerecs.stackexchange.com/questions/38227/… )
bjornte


10

Saya menerjemahkan algoritma Simon White ke PL / pgSQL. Ini kontribusi saya.

<!-- 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'; 

Bekerja pada PostgreSQL saya yang tidak memiliki dukungan plruby! Terima kasih!
hostnik

Terima kasih! Bagaimana Anda melakukan ini dalam Oracle SQL?
olovholm

Port ini salah. String yang tepat tidak kembali 1.
Brandon Wigfield

9

String Similarity Metrics berisi ikhtisar dari berbagai metrik yang digunakan dalam perbandingan string ( Wikipedia juga memiliki ikhtisar). Banyak dari metrik ini diimplementasikan dalam simmetrik perpustakaan .

Contoh metrik lain, yang tidak termasuk dalam ikhtisar yang diberikan adalah misalnya jarak kompresi (mencoba memperkirakan kompleksitas Kolmogorov ), yang dapat digunakan untuk teks yang sedikit lebih panjang daripada yang Anda sajikan.

Anda mungkin juga mempertimbangkan untuk melihat subjek yang jauh lebih luas dari Pemrosesan Bahasa Alami . Paket R ini dapat membantu Anda memulai dengan cepat (atau setidaknya memberikan beberapa ide).

Dan satu edit terakhir - cari pertanyaan lain tentang hal ini di SO, ada beberapa yang terkait.


9

Algoritma PHP versi yang lebih cepat:

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

Untuk data yang saya miliki (sekitar 2.300 perbandingan) saya memiliki waktu berjalan 0,58 dtk dengan larutan Igal Alkon versus 0,35 dtk dengan milik saya.


9

Versi dalam Scala yang indah:

  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

Ini adalah versi 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)
}

Algoritma ini lebih baik tetapi cukup lambat untuk data besar. Maksud saya jika seseorang harus membandingkan 10.000 kata dengan 15.000 kata lain, itu terlalu lambat. Bisakah kita meningkatkan kinerjanya dalam hal kecepatan ??
indra_patil

6

Posting jawaban marzagao di C99, terinspirasi oleh algoritma ini

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

Beberapa tes berdasarkan pada artikel asli :

#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

Membangun versi C # Michael La Voie yang luar biasa, sesuai permintaan untuk menjadikannya metode ekstensi, inilah yang saya kemukakan. Manfaat utama melakukannya dengan cara ini adalah Anda dapat mengurutkan Daftar Generik berdasarkan kecocokan persen. Misalnya, pertimbangkan Anda memiliki bidang string bernama "Kota" di objek Anda. Seorang pengguna mencari "Chester" dan Anda ingin mengembalikan hasil dalam urutan pertandingan yang menurun. Misalnya, Anda ingin pertandingan harfiah Chester muncul sebelum Rochester. Untuk melakukan ini, tambahkan dua properti baru ke objek Anda:

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

Kemudian pada setiap objek, atur SearchText ke apa yang dicari pengguna. Maka Anda dapat mengurutkannya dengan mudah seperti:

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

Berikut sedikit modifikasi untuk menjadikannya metode ekstensi:

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

Saya pikir Anda akan lebih baik menggunakan bool isCaseSensitive dengan nilai default false - bahkan jika itu benar implementasinya jauh lebih bersih
Jordan

5

Implementasi JavaScript saya mengambil string atau array string, dan lantai opsional (lantai default adalah 0,5). Jika Anda memberikannya string, itu akan mengembalikan benar atau salah tergantung pada apakah skor kesamaan string lebih besar atau sama dengan lantai. Jika Anda memberinya array string, itu akan mengembalikan array string yang skor kemiripannya lebih besar atau sama dengan lantai, diurutkan berdasarkan skor.

Contoh:

'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"]

Ini dia:

(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
Bug / salah ketik! for(var j = 0; j < pairs1.length; j++){seharusnyafor(var j = 0; j < pairs2.length; j++){
Searle

3

Algoritme koefisien Dice (jawaban Simon White / marzagao) diimplementasikan di Ruby dalam metode pair_distance_similar di permata amatch

https://github.com/flori/amatch

Permata ini juga berisi implementasi dari sejumlah pencocokan perkiraan dan algoritma perbandingan string: jarak edit Levenshtein, jarak edit Penjual, jarak Hamming, panjang urutan umum terpanjang, panjang substring umum terpanjang, metrik jarak pasangan, metrik jarak pasangan, metrik Jaro-Winkler .


2

Versi Haskell — silakan menyarankan pengeditan karena saya belum banyak melakukan 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

Bagaimana dengan jarak Levenshtein, dibagi dengan panjang string pertama (atau sebagai alternatif, dibagi panjang min / maks / rata-rata kedua string)? Sejauh ini itu berhasil bagi saya.


Namun, untuk mengutip posting lain tentang topik ini, apa yang ia kembalikan sering "tidak menentu". Ini peringkat 'gema' sangat mirip dengan 'anjing'.
Xyene

@Nox: Bagian "dibagi dengan panjang string pertama" dari balasan ini adalah signifikan. Juga, ini berkinerja lebih baik daripada algoritma Dice yang banyak dipuji untuk kesalahan pengetikan dan transposisi, dan bahkan konjugasi umum (pertimbangkan untuk membandingkan "berenang" dan "berenang", misalnya).
Logan Pickup

1

Hai teman-teman saya mencoba ini di javascript, tapi saya baru mengenalnya, ada yang tahu cara cepat untuk melakukannya?

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

Implementasi ini salah. Fungsi bigram rusak untuk input panjang 0. Metode string_similarity tidak menembus dengan benar di dalam loop kedua, yang dapat menyebabkan penghitungan pasangan beberapa kali, yang mengarah ke nilai pengembalian yang melebihi 100%. Dan Anda juga lupa untuk mendeklarasikan xdan y, dan Anda tidak harus mengulang loop menggunakan for..in..loop (gunakan for(..;..;..)saja).
Rob W

1

Berikut adalah versi lain dari Kesamaan berdasarkan indeks Sørensen-Dice (jawaban marzagao), yang ini ditulis dalam 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

Saya sedang mencari implementasi ruby ​​murni dari algoritma yang ditunjukkan oleh jawaban @ marzagao. Sayangnya, tautan yang ditunjukkan oleh @marzagao rusak. Dalam jawaban @ s01ipsist, ia menunjukkan ruby ​​gem amatch di mana implementasi tidak dalam ruby ​​murni. Jadi saya mencari sedikit dan menemukan permata fuzzy_match yang memiliki implementasi ruby ​​murni (meskipun menggunakan permata ini amatch) di sini . Saya harap ini akan membantu orang seperti saya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.