Một thuật toán xếp hạng tương tự tốt hơn cho các chuỗi có độ dài thay đổi


152

Tôi đang tìm kiếm một thuật toán tương tự chuỗi mang lại kết quả tốt hơn trên các chuỗi có độ dài thay đổi so với các thuật toán thường được đề xuất (khoảng cách levenshtein, soundex, v.v.).

Ví dụ,

Cho chuỗi A: "Robert",

Sau đó, chuỗi B: "Amy Robertson"

sẽ là một trận đấu tốt hơn

Chuỗi C: "Richard"

Ngoài ra, tốt nhất, thuật toán này nên là ngôn ngữ bất khả tri (cũng hoạt động trong các ngôn ngữ khác ngoài tiếng Anh).



Ngoài ra hãy xem: Hệ số của Dice
avid_useR

Câu trả lời:


155

Simon White của Catalysoft đã viết một bài báo về một thuật toán rất thông minh để so sánh các cặp nhân vật liền kề hoạt động thực sự tốt cho mục đích của tôi:

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

Simon có một phiên bản Java của thuật toán và bên dưới tôi đã viết một phiên bản PL / Ruby của nó (được lấy từ phiên bản ruby ​​đơn giản được thực hiện trong bình luận nhập diễn đàn liên quan của Mark Wong-VanHaren) để tôi có thể sử dụng nó trong các truy vấn PostgreQuery của mình:

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

Hoạt động như một lá bùa!


32
Bạn tìm thấy câu trả lời và viết tất cả những điều đó trong 4 phút? Ấn tượng!
Matt J

28
Tôi đã chuẩn bị câu trả lời của mình sau một số nghiên cứu và thực hiện. Tôi đặt nó ở đây vì lợi ích của bất kỳ ai khác tìm kiếm trong SO để có câu trả lời thực tế bằng thuật toán thay thế bởi vì hầu hết các câu trả lời trong các câu hỏi liên quan dường như xoay quanh levenshtein hoặc soundex.
marzagao

18
Chỉ là những gì tôi đã tìm kiếm. Em sẽ lấy anh chứ?
BlackTea

6
@JasonSundram là đúng - trên thực tế, đây hệ số Dice nổi tiếng trên các bigram cấp độ nhân vật, như tác giả viết trong "phụ lục" (dưới cùng của trang).
Fred Foo

4
Điều này trả về "điểm" là 1 (khớp 100%) khi so sánh các chuỗi có một chữ cái bị cô lập là khác biệt, như ví dụ này:, string_similarity("vitamin B", "vitamin C") #=> 1có cách nào dễ dàng để ngăn chặn loại hành vi này không?
MrYoshiji

77

Câu trả lời của marzagao là tuyệt vời. Tôi đã chuyển đổi nó thành C # vì vậy tôi nghĩ rằng tôi sẽ đăng nó ở đây:

Liên kết quá khứ

/// <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 nếu tôi có thể, bạn vừa cứu tôi một ngày làm việc vất vả! Chúc mừng.
vvohra87

1
Rất đẹp! Gợi ý duy nhất tôi có, sẽ biến điều này thành một phần mở rộng.
Levitikon

+1! Thật tuyệt khi nó hoạt động, với những sửa đổi nhỏ cho Java. Và nó dường như trả lại phản hồi tốt hơn Levenshtein.
Xyene

1
Tôi đã thêm một phiên bản chuyển đổi này thành một phương thức mở rộng dưới đây. Cảm ơn phiên bản gốc và bản dịch tuyệt vời.
Frank Rundatz

@Michael La Voie Cảm ơn bạn, nó rất hay! Mặc dù có một chút vấn đề với (2.0 * intersection) / union- tôi nhận được Double.NaN khi so sánh hai chuỗi trống.
Vojtěch Dohnal

41

Đây là một phiên bản khác của câu trả lời của marzagao, phiên bản này được viết bằng 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
Có một lỗi nhỏ trong chuỗi_similarity khi có các ngram trùng lặp trong một từ, dẫn đến số điểm> 1 cho các chuỗi giống hệt nhau. Thêm một 'break' sau khi "hit_count + = 1" sửa nó.
jbaiter

1
@jbaiter: Bắt tốt. Tôi đã thay đổi nó để phản ánh những thay đổi của bạn.
John Rutledge

3
Trong bài viết của Simon White, ông nói "Lưu ý rằng bất cứ khi nào tìm thấy một trận đấu, cặp nhân vật đó sẽ bị xóa khỏi danh sách mảng thứ hai để ngăn chúng tôi khớp với cặp nhân vật đó nhiều lần. (Nếu không, 'GGGGG' sẽ ghi điểm một trận đấu hoàn hảo chống lại 'GG'.) "Tôi sẽ thay đổi tuyên bố này để nói rằng nó sẽ cho kết quả cao hơn so với kết hợp hoàn hảo. Không tính đến điều này, dường như cũng có kết quả là thuật toán không mang tính bắc cầu (độ tương tự (x, y) = / = độ tương tự (y, x)). Thêm cặp2.remove (y) sau dòng hit_count + = 1 khắc phục sự cố.
NinjaMeTimbers

17

Đây là triển khai PHP của tôi về thuật toán StrikeAMatch được đề xuất, bởi Simon White. những lợi thế (như nó nói trong liên kết) là:

  • Một sự phản ánh đúng về sự tương tự từ vựng - các chuỗi có sự khác biệt nhỏ nên được công nhận là tương tự nhau. Cụ thể, sự chồng chéo chuỗi con đáng kể sẽ chỉ ra mức độ tương tự cao giữa các chuỗi.

  • Sự mạnh mẽ đối với những thay đổi của trật tự từ - hai chuỗi chứa cùng một từ, nhưng theo một thứ tự khác, nên được công nhận là tương tự nhau. Mặt khác, nếu một chuỗi chỉ là một đảo chữ ngẫu nhiên của các ký tự chứa trong chuỗi kia, thì nó sẽ (thường) được công nhận là không giống nhau.

  • Ngôn ngữ độc lập - thuật toán nên hoạt động không chỉ bằng tiếng Anh, mà ở nhiều ngôn ngữ khác nhau.

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

Một phiên bản ngắn hơn của câu trả lời của 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))

Ngay cả các intersectionbiến là một chất thải dòng.
Chibueze Opata

14

Cuộc thảo luận này đã thực sự hữu ích, cảm ơn. Tôi đã chuyển đổi thuật toán sang VBA để sử dụng với Excel và đã viết một vài phiên bản của hàm bảng tính, một phiên bản để so sánh đơn giản một cặp chuỗi, còn lại để so sánh một chuỗi với một chuỗi / chuỗi chuỗi. Phiên bản strSimLookup trả về kết quả khớp tốt nhất cuối cùng dưới dạng chuỗi, chỉ mục mảng hoặc số liệu tương tự.

Việc triển khai này tạo ra kết quả tương tự được liệt kê trong ví dụ của Amazon trên trang web của Simon White với một vài ngoại lệ nhỏ trong các trận đấu có điểm thấp; không chắc chắn sự khác biệt ở đâu, có thể là chức năng Split của VBA, nhưng tôi đã không điều tra vì nó hoạt động tốt cho mục đích của tôi.

'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 Điều này có vẻ cực kỳ hữu ích, nhưng tôi chưa quen với VBA và một chút bị thách thức bởi mã. Bạn có thể đăng một tệp Excel sử dụng sự đóng góp của bạn không? Đối với mục đích của tôi, tôi hy vọng sử dụng nó để khớp các tên đầu tiên tương tự từ một cột duy nhất trong Excel với khoảng 1000 mục nhập (trích đoạn tại đây: dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ). Sau đó tôi sẽ sử dụng các kết hợp làm từ đồng nghĩa trong tìm kiếm người. (xem thêm softwarerecs.stackexchange.com/questions/38227/ )
bjornte


10

Tôi đã dịch thuật toán của Simon White sang PL / pgSQL. Đây là đóng góp của tôi.

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

Hoạt động trên PostgreSQL của tôi không có hỗ trợ plruby! Cảm ơn bạn!
hostnik

Cảm ơn bạn! Làm thế nào bạn sẽ làm điều này trong Oracle SQL?
olovholm

Cổng này không chính xác. Chuỗi chính xác không quay lại 1.
Brandon Wigfield 17/07/17

9

Số liệu tương tự chuỗi chứa tổng quan về nhiều số liệu khác nhau được sử dụng trong so sánh chuỗi ( Wikipedia cũng có tổng quan). Phần lớn các số liệu này được thực hiện trong một mô phỏng thư viện .

Một ví dụ khác về số liệu, không bao gồm trong tổng quan đã cho là ví dụ về khoảng cách nén (cố gắng xấp xỉ độ phức tạp của Kolmogorov ), có thể được sử dụng cho các văn bản dài hơn một chút so với văn bản bạn đã trình bày.

Bạn cũng có thể xem xét việc xem xét một chủ đề rộng hơn nhiều về Xử lý ngôn ngữ tự nhiên . Những gói R có thể giúp bạn bắt đầu nhanh chóng (hoặc ít nhất là đưa ra một số ý tưởng).

Và một chỉnh sửa cuối cùng - tìm kiếm các câu hỏi khác về chủ đề này tại SO, có khá nhiều câu hỏi liên quan.


9

Một phiên bản PHP nhanh hơn của thuật toán:

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

Đối với dữ liệu tôi có (khoảng 2300 so sánh) tôi có thời gian chạy 0,58 giây với dung dịch Igal Alkon so với 0,35 giây với tôi.


9

Một phiên bản trong Scala đẹp:

  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

Đây là phiên bản 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)
}

Thuật toán này tốt hơn nhưng khá chậm đối với dữ liệu lớn. Ý tôi là nếu người ta phải so sánh 10000 từ với 15000 từ khác thì quá chậm. Chúng ta có thể tăng hiệu suất của nó về tốc độ không ??
indra_patil

6

Đăng câu trả lời của marzagao trong C99, lấy cảm hứng từ các thuật toán này

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

Một số thử nghiệm dựa trên bài viết gốc :

#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

Xây dựng trên phiên bản C # tuyệt vời của Michael La Voie, theo yêu cầu biến nó thành một phương thức mở rộng, đây là những gì tôi nghĩ ra. Lợi ích chính của việc thực hiện theo cách này là bạn có thể sắp xếp Danh sách chung theo tỷ lệ phần trăm phù hợp. Ví dụ: xem xét bạn có một trường chuỗi có tên "Thành phố" trong đối tượng của bạn. Một người dùng tìm kiếm "Chester" và bạn muốn trả về kết quả theo thứ tự giảm dần của trận đấu. Ví dụ: bạn muốn các trận đấu theo nghĩa đen của Chester xuất hiện trước Rochester. Để làm điều này, thêm hai thuộc tính mới vào đối tượng của bạn:

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

Sau đó, trên mỗi đối tượng, đặt SearchText thành những gì người dùng đã tìm kiếm. Sau đó, bạn có thể sắp xếp nó dễ dàng với một cái gì đó như:

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

Đây là sửa đổi nhỏ để làm cho nó một phương thức mở rộng:

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

Tôi nghĩ rằng bạn sẽ tốt hơn khi sử dụng bool isCaseSensitive với giá trị mặc định là false - ngay cả khi đó là sự thực thi sạch hơn nhiều
Jordan

5

Việc triển khai JavaScript của tôi có một chuỗi hoặc một chuỗi các chuỗi và một tầng tùy chọn (tầng mặc định là 0,5). Nếu bạn truyền cho nó một chuỗi, nó sẽ trả về đúng hay sai tùy thuộc vào điểm tương tự của chuỗi đó lớn hơn hoặc bằng sàn hay không. Nếu bạn truyền cho nó một mảng các chuỗi, nó sẽ trả về một mảng các chuỗi có điểm tương tự lớn hơn hoặc bằng sàn, được sắp xếp theo điểm.

Ví dụ:

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

Đây là:

(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
Lỗi / Typo! for(var j = 0; j < pairs1.length; j++){nên làfor(var j = 0; j < pairs2.length; j++){
Searle

3

Thuật toán hệ số Dice (câu trả lời của Simon White / marzagao) được triển khai trong Ruby theo phương thức cặp_distance_similar trong viên ngọc amatch

https://github.com/flori/amatch

Viên ngọc này cũng chứa các triển khai của một số thuật toán so sánh chuỗi và so sánh gần đúng: khoảng cách chỉnh sửa Levenshtein, khoảng cách chỉnh sửa của người bán, khoảng cách Hamming, độ dài chuỗi phổ biến dài nhất, độ dài chuỗi con chung dài nhất, số liệu khoảng cách cặp, số liệu Jaro-Winkler .


2

Một phiên bản Haskell, hãy thoải mái đề xuất các chỉnh sửa vì tôi chưa thực hiện nhiều 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

Áo choàng:

(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

Điều gì về khoảng cách Levenshtein, chia cho chiều dài của chuỗi đầu tiên (hoặc chia cách khác chiều dài tối thiểu / tối đa / avg của cả hai chuỗi)? Điều đó đã làm việc cho tôi cho đến nay.


Tuy nhiên, để trích dẫn một bài viết khác về chủ đề này, những gì nó trả về thường là "thất thường". Nó xếp hạng 'echo' khá giống với 'dog'.
Xyene

@Nox: Phần "chia cho độ dài của chuỗi đầu tiên" của câu trả lời này là đáng kể. Ngoài ra, điều này thực hiện tốt hơn thuật toán của Dice được nhiều người khen ngợi về lỗi chính tả và lỗi chuyển vị, và thậm chí cả các cách chia thông thường (ví dụ như xem xét "bơi" và "bơi").
Pickup Logan

1

Chào các bạn, tôi đã thử cái này trong javascript, nhưng tôi chưa quen với nó, có ai biết cách nhanh hơn để làm nó không?

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

Việc thực hiện này là không chính xác. Hàm bigram ngắt cho đầu vào có độ dài 0. Phương thức string_similarity không ngắt đúng trong vòng lặp thứ hai, điều này có thể dẫn đến việc đếm các cặp nhiều lần, dẫn đến giá trị trả về vượt quá 100%. Và bạn cũng đã quên khai báo xy, và bạn không nên lặp qua các vòng lặp bằng cách sử dụng một for..in..vòng lặp (sử dụng for(..;..;..)thay thế).
Rob W

1

Đây là một phiên bản khác của Tương tự dựa trên chỉ số Sice của Dørenen (câu trả lời của marzagao), phiên bản này được viết bằng 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

Tôi đang tìm kiếm việc thực hiện ruby ​​thuần túy của thuật toán được chỉ ra bởi câu trả lời của @ marzagao. Thật không may, liên kết được chỉ định bởi @marzagao bị hỏng. Trong câu trả lời @ s01ipsist, anh chỉ đá quý ruby amatch nơi thực hiện không có trong ruby tinh khiết. Vì vậy, tôi đã tìm kiếm một chút và tìm thấy gem fuzzy_match có triển khai ruby ​​thuần túy (mặc dù sử dụng đá quý này amatch) tại đây . Tôi hy vọng điều này sẽ giúp một người như tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.