가변 길이 문자열에 대한 더 나은 유사성 순위 알고리즘


152

나는 일반적으로 제안되는 것 (levenshtein distance, soundex 등)보다 가변 길이 문자열에서 더 나은 결과를 얻는 문자열 유사성 알고리즘을 찾고 있습니다.

예를 들어

주어진 문자열 A : "Robert",

그런 다음 문자열 B : "Amy Robertson"

보다 더 나은 일치

문자열 C : "리차드"

또한이 알고리즘은 언어에 구애받지 않아야합니다 (영어 이외의 언어에서도 작동).



또한 확인하십시오 : 주사위의 계수
avid_useR

답변:


155

Catalysoft의 Simon White는 내 목적에 잘 맞는 인접한 문자 쌍을 비교하는 매우 영리한 알고리즘에 대한 기사를 작성했습니다.

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

Simon은 Java 버전의 알고리즘을 가지고 있으며 그 아래에 PL / Ruby 버전을 작성했습니다 (Mark Wong-VanHaren의 관련 포럼 항목 주석에서 수행 된 일반 루비 버전에서 가져옴). 내 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 분 안에 모든 것을 썼습니까? 감동적인!
Matt J

28
몇 가지 연구와 구현 후에 답변을 준비했습니다. 관련 질문에 대한 대부분의 답변이 levenshtein 또는 soundex와 관련이있는 것처럼 보이기 때문에 다른 알고리즘을 사용하여 실용적인 답변을 찾고있는 다른 사람의 이익을 위해 여기에 넣었습니다.
marzagao

18
내가 찾던 것. 나랑 결혼 해 줄래?
BlackTea

6
사실,이 - @JasonSundram은 권리 입니다 저자는 "부록"의 글을 참고로, 캐릭터 레벨 bigrams에 잘 알려진 주사위 계수 (페이지의 하단).
Fred Foo

4
이 예제와 같이 하나의 분리 된 문자가있는 문자열을 차이로 비교할 때 1 (100 % 일치)의 "점수"를 반환합니다. string_similarity("vitamin B", "vitamin C") #=> 1이 유형의 동작을 방지하는 쉬운 방법이 있습니까?
MrYoshiji

77

marzagao의 답변 은 훌륭합니다. 나는 그것을 C #으로 변환 했으므로 여기에 게시 할 것이라고 생각했습니다.

Pastebin Link

/// <summary>
/// This class implements string comparison algorithm
/// based on character pair similarity
/// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
/// </summary>
public class SimilarityTool
{
    /// <summary>
    /// Compares the two strings based on letter pair matches
    /// </summary>
    /// <param name="str1"></param>
    /// <param name="str2"></param>
    /// <returns>The percentage match from 0.0 to 1.0 where 1.0 is 100%</returns>
    public double CompareStrings(string str1, string str2)
    {
        List<string> pairs1 = WordLetterPairs(str1.ToUpper());
        List<string> pairs2 = WordLetterPairs(str2.ToUpper());

        int intersection = 0;
        int union = pairs1.Count + pairs2.Count;

        for (int i = 0; i < pairs1.Count; i++)
        {
            for (int j = 0; j < pairs2.Count; j++)
            {
                if (pairs1[i] == pairs2[j])
                {
                    intersection++;
                    pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success

                    break;
                }
            }
        }

        return (2.0 * intersection) / union;
    }

    /// <summary>
    /// Gets all letter pairs for each
    /// individual word in the string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private List<string> WordLetterPairs(string str)
    {
        List<string> AllPairs = new List<string>();

        // Tokenize the string and put the tokens/words into an array
        string[] Words = Regex.Split(str, @"\s");

        // For each word
        for (int w = 0; w < Words.Length; w++)
        {
            if (!string.IsNullOrEmpty(Words[w]))
            {
                // Find the pairs of characters
                String[] PairsInWord = LetterPairs(Words[w]);

                for (int p = 0; p < PairsInWord.Length; p++)
                {
                    AllPairs.Add(PairsInWord[p]);
                }
            }
        }

        return AllPairs;
    }

    /// <summary>
    /// Generates an array containing every 
    /// two consecutive letters in the input string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private string[] LetterPairs(string str)
    {
        int numPairs = str.Length - 1;

        string[] pairs = new string[numPairs];

        for (int i = 0; i < numPairs; i++)
        {
            pairs[i] = str.Substring(i, 2);
        }

        return pairs;
    }
}

2
내가 할 수 있다면 +100 당신은 나에게 힘든 일 동료를 구했다! 건배.
vvohra87

1
아주 좋아요! 내가 가진 유일한 제안은 이것을 확장으로 만드는 것입니다.
Levitikon

+1! Java도 약간 수정하여 작동합니다. 그리고 그것은 Levenshtein보다 더 나은 응답을 반환하는 것 같습니다.
Xyene

1
이것을 아래의 확장 방법으로 변환하는 버전을 추가했습니다. 원본 버전과 멋진 번역에 감사드립니다.
Frank Rundatz

@Michael La Voie 감사합니다. 정말 좋습니다! 약간의 문제가 있지만 (2.0 * intersection) / union두 개의 빈 문자열을 비교할 때 Double.NaN이 발생합니다.
Vojtěch Dohnal 8

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
단어에 중복 된 ngram이 있으면 string_similarity에 작은 버그가 발생하여 동일한 문자열에 대해 1보다 큰 점수를 얻습니다. "hit_count + = 1"뒤에 'break'를 추가하면 문제가 해결됩니다.
jbaiter

1
@jbaiter : 잘 잡았습니다. 변경 사항을 반영하여 변경했습니다.
John Rutledge

3
Simon White의 기사에서 "일치가 발견 될 때마다 해당 문자 쌍이 두 번째 배열 목록에서 제거되어 동일한 문자 쌍에 대해 여러 번 일치하는 것을 방지합니다. 그렇지 않으면 'GGGGG'는 완벽하게 일치합니다. ‘GG '에 반대합니다.) ")이 문장을 수정하여 완벽하게 일치하는 것보다 높을 것이라고 말합니다. 이것을 고려하지 않으면 알고리즘이 전 이적이지 않은 결과가있는 것 같습니다 (유사성 (x, y) = / = similarity (y, x)). hit_count + = 1 행 다음에 pairs2.remove (y)를 추가하면 문제가 해결됩니다.
NinjaMeTimbers

17

Simon White가 제안한 StrikeAMatch 알고리즘의 PHP 구현은 다음과 같습니다. 장점은 (링크에 나와있는 것처럼) 다음과 같습니다.

  • 어휘 유사성의 진정한 반영 -차이가 작은 문자열은 비슷한 것으로 인식되어야합니다. 특히, 중요한 부분 문자열 겹침은 문자열 사이의 높은 수준의 유사성을 가리켜 야합니다.

  • 단어 순서 변경에 대한 견고성 – 동일한 단어를 포함하지만 다른 순서로 된 두 개의 문자열은 유사한 것으로 인식되어야합니다. 반면에, 한 문자열이 다른 문자열에 포함 된 문자의 임의의 아나그램 인 경우 (보통) 다른 것으로 인식해야합니다.

  • 언어 독립성 -알고리즘은 영어뿐만 아니라 여러 언어로 작동해야합니다.

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

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

intersection변수 조차도 라인 낭비입니다.
Chibueze Opata

14

이 토론은 정말 도움이되었습니다. Excel과 함께 사용하기 위해 알고리즘을 VBA로 변환하고 한 쌍의 문자열을 간단하게 비교하고 다른 하나를 문자열의 범위 / 배열과 비교하기위한 몇 가지 버전의 워크 시트 함수를 작성했습니다. strSimLookup 버전은 마지막으로 가장 일치하는 것을 문자열, 배열 인덱스 또는 유사성 메트릭으로 반환합니다.

이 구현은 Simon White 웹 사이트의 Amazon 예제에 나열된 것과 동일한 결과를 얻습니다. 차이가 어디에서 발생하는지 확실하지 않으면 VBA의 Split 기능이 될 수 있지만 내 목적에 잘 작동하는지 조사하지 않았습니다.

'Implements functions to rate how similar two strings are on
'a scale of 0.0 (completely dissimilar) to 1.0 (exactly similar)
'Source:   http://www.catalysoft.com/articles/StrikeAMatch.html
'Author: Bob Chatham, bob.chatham at gmail.com
'9/12/2010

Option Explicit

Public Function stringSimilarity(str1 As String, str2 As String) As Variant
'Simple version of the algorithm that computes the similiarity metric
'between two strings.
'NOTE: This verision is not efficient to use if you're comparing one string
'with a range of other values as it will needlessly calculate the pairs for the
'first string over an over again; use the array-optimized version for this case.

    Dim sPairs1 As Collection
    Dim sPairs2 As Collection

    Set sPairs1 = New Collection
    Set sPairs2 = New Collection

    WordLetterPairs str1, sPairs1
    WordLetterPairs str2, sPairs2

    stringSimilarity = SimilarityMetric(sPairs1, sPairs2)

    Set sPairs1 = Nothing
    Set sPairs2 = Nothing

End Function

Public Function strSimA(str1 As Variant, rRng As Range) As Variant
'Return an array of string similarity indexes for str1 vs every string in input range rRng
    Dim sPairs1 As Collection
    Dim sPairs2 As Collection
    Dim arrOut As Variant
    Dim l As Long, j As Long

    Set sPairs1 = New Collection

    WordLetterPairs CStr(str1), sPairs1

    l = rRng.Count
    ReDim arrOut(1 To l)
    For j = 1 To l
        Set sPairs2 = New Collection
        WordLetterPairs CStr(rRng(j)), sPairs2
        arrOut(j) = SimilarityMetric(sPairs1, sPairs2)
        Set sPairs2 = Nothing
    Next j

    strSimA = Application.Transpose(arrOut)

End Function

Public Function strSimLookup(str1 As Variant, rRng As Range, Optional returnType) As Variant
'Return either the best match or the index of the best match
'depending on returnTYype parameter) between str1 and strings in rRng)
' returnType = 0 or omitted: returns the best matching string
' returnType = 1           : returns the index of the best matching string
' returnType = 2           : returns the similarity metric

    Dim sPairs1 As Collection
    Dim sPairs2 As Collection
    Dim metric, bestMetric As Double
    Dim i, iBest As Long
    Const RETURN_STRING As Integer = 0
    Const RETURN_INDEX As Integer = 1
    Const RETURN_METRIC As Integer = 2

    If IsMissing(returnType) Then returnType = RETURN_STRING

    Set sPairs1 = New Collection

    WordLetterPairs CStr(str1), sPairs1

    bestMetric = -1
    iBest = -1

    For i = 1 To rRng.Count
        Set sPairs2 = New Collection
        WordLetterPairs CStr(rRng(i)), sPairs2
        metric = SimilarityMetric(sPairs1, sPairs2)
        If metric > bestMetric Then
            bestMetric = metric
            iBest = i
        End If
        Set sPairs2 = Nothing
    Next i

    If iBest = -1 Then
        strSimLookup = CVErr(xlErrValue)
        Exit Function
    End If

    Select Case returnType
    Case RETURN_STRING
        strSimLookup = CStr(rRng(iBest))
    Case RETURN_INDEX
        strSimLookup = iBest
    Case Else
        strSimLookup = bestMetric
    End Select

End Function

Public Function strSim(str1 As String, str2 As String) As Variant
    Dim ilen, iLen1, ilen2 As Integer

    iLen1 = Len(str1)
    ilen2 = Len(str2)

    If iLen1 >= ilen2 Then ilen = ilen2 Else ilen = iLen1

    strSim = stringSimilarity(Left(str1, ilen), Left(str2, ilen))

End Function

Sub WordLetterPairs(str As String, pairColl As Collection)
'Tokenize str into words, then add all letter pairs to pairColl

    Dim Words() As String
    Dim word, nPairs, pair As Integer

    Words = Split(str)

    If UBound(Words) < 0 Then
        Set pairColl = Nothing
        Exit Sub
    End If

    For word = 0 To UBound(Words)
        nPairs = Len(Words(word)) - 1
        If nPairs > 0 Then
            For pair = 1 To nPairs
                pairColl.Add Mid(Words(word), pair, 2)
            Next pair
        End If
    Next word

End Sub

Private Function SimilarityMetric(sPairs1 As Collection, sPairs2 As Collection) As Variant
'Helper function to calculate similarity metric given two collections of letter pairs.
'This function is designed to allow the pair collections to be set up separately as needed.
'NOTE: sPairs2 collection will be altered as pairs are removed; copy the collection
'if this is not the desired behavior.
'Also assumes that collections will be deallocated somewhere else

    Dim Intersect As Double
    Dim Union As Double
    Dim i, j As Long

    If sPairs1.Count = 0 Or sPairs2.Count = 0 Then
        SimilarityMetric = CVErr(xlErrNA)
        Exit Function
    End If

    Union = sPairs1.Count + sPairs2.Count
    Intersect = 0

    For i = 1 To sPairs1.Count
        For j = 1 To sPairs2.Count
            If StrComp(sPairs1(i), sPairs2(j)) = 0 Then
                Intersect = Intersect + 1
                sPairs2.Remove j
                Exit For
            End If
        Next j
    Next i

    SimilarityMetric = (2 * Intersect) / Union

End Function

@bchatham 이것은 매우 유용 해 보이지만 VBA를 처음 접했고 코드에 약간의 어려움이 있습니다. 당신이 기여한 것을 사용하는 Excel 파일을 게시 할 수 있습니까? 내 목적을 위해 Excel의 단일 열에서 비슷한 이름을 약 1000 개의 항목과 일치시키기 위해 사용하고 싶습니다 (dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ). 그런 다음 사람들 검색에서 일치 항목을 동의어로 사용합니다. (또한 softwarerecs.stackexchange.com/questions/38227/… 참조 )
bjornte


10

Simon White의 알고리즘을 PL / pgSQL로 변환했습니다. 이것이 나의 공헌입니다.

<!-- language: lang-sql -->

create or replace function spt1.letterpairs(in p_str varchar) 
returns varchar  as 
$$
declare

    v_numpairs integer := length(p_str)-1;
    v_pairs varchar[];

begin

    for i in 1 .. v_numpairs loop
        v_pairs[i] := substr(p_str, i, 2);
    end loop;

    return v_pairs;

end;
$$ language 'plpgsql';

--===================================================================

create or replace function spt1.wordletterpairs(in p_str varchar) 
returns varchar as
$$
declare
    v_allpairs varchar[];
    v_words varchar[];
    v_pairsinword varchar[];
begin
    v_words := regexp_split_to_array(p_str, '[[:space:]]');

    for i in 1 .. array_length(v_words, 1) loop
        v_pairsinword := spt1.letterpairs(v_words[i]);

        if v_pairsinword is not null then
            for j in 1 .. array_length(v_pairsinword, 1) loop
                v_allpairs := v_allpairs || v_pairsinword[j];
            end loop;
        end if;

    end loop;


    return v_allpairs;
end;
$$ language 'plpgsql';

--===================================================================

create or replace function spt1.arrayintersect(ANYARRAY, ANYARRAY)
returns anyarray as 
$$
    select array(select unnest($1) intersect select unnest($2))
$$ language 'sql';

--===================================================================

create or replace function spt1.comparestrings(in p_str1 varchar, in p_str2 varchar)
returns float as
$$
declare
    v_pairs1 varchar[];
    v_pairs2 varchar[];
    v_intersection integer;
    v_union integer;
begin
    v_pairs1 := wordletterpairs(upper(p_str1));
    v_pairs2 := wordletterpairs(upper(p_str2));
    v_union := array_length(v_pairs1, 1) + array_length(v_pairs2, 1); 

    v_intersection := array_length(arrayintersect(v_pairs1, v_pairs2), 1);

    return (2.0 * v_intersection / v_union);
end;
$$ language 'plpgsql'; 

plruby를 지원하지 않는 PostgreSQL에서 작동합니다! 감사합니다!
hostnik

감사합니다! Oracle SQL에서 어떻게 하시겠습니까?
olovholm

이 포트가 잘못되었습니다. 정확한 문자열은 1을 반환하지 않습니다.
Brandon Wigfield

9

문자열 유사성 메트릭 에는 문자열 비교에 사용되는 다양한 메트릭의 개요가 포함되어 있습니다 ( Wikipedia 에도 개요가 있음). 이러한 메트릭의 대부분은 라이브러리 시뮬레이션으로 구현됩니다 .

주어진 개요에 포함되지 않은 또 다른 메트릭의 예는 압축 거리 ( Kolmogorov의 복잡성 을 근사하려는 시도 )입니다. 이는 제시 한 것보다 약간 긴 텍스트에 사용할 수 있습니다.

당신은 또한 훨씬 더 광범위한 자연어 처리 주제를 고려하는 것을 고려할 수 있습니다 . 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 비교)에서 Igal Alkon 솔루션의 경우 0.58 초의 시간과 광산 의 경우 0.35 초의 실행 시간이있었습니다 .


9

아름다운 스칼라 버전 :

  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

알고리즘 에서 영감을 얻은 C99에 marzagao의 답변 게시

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

확장 방법을 만들기위한 요청에 따라 Michael La Voie의 멋진 C # 버전을 기반으로 여기에 내가 생각해 냈습니다. 이 방법으로 수행 할 때의 주요 이점은 백분율 일치로 일반 목록을 정렬 할 수 있다는 것입니다. 예를 들어, 개체에 "City"라는 문자열 필드가 있다고 가정하십시오. 사용자가 "Chester"를 검색하면 내림차순으로 결과를 반환하려고합니다. 예를 들어, 체스터의 리터럴 일치가 로체스터 전에 나타나기를 원합니다. 이렇게하려면 객체에 두 가지 새로운 속성을 추가하십시오.

    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로 사용하는 것이 더 나을 것이라고 생각합니다. 그것이 사실 일지라도 구현이 훨씬 깨끗합니다.
Jordan

5

내 JavaScript 구현에는 문자열 또는 문자열 배열과 선택적 바닥 (기본 바닥은 0.5)이 필요합니다. 문자열을 전달하면 문자열의 유사성 점수가 바닥보다 크거나 같은지 여부에 따라 true 또는 false를 반환합니다. 문자열 배열을 전달하면 유사성 점수가 바닥보다 크거나 같은 문자열 배열을 점수로 정렬하여 반환합니다.

예 :

'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

주사위 계수 알고리즘 (Simon White / marzagao 's answer)은 Ruby에서 amatch gem의 pair_distance_similar 메소드로 구현됩니다.

https://github.com/flori/amatch

이 gem에는 Levenshtein 편집 거리, 판매자 편집 거리, 해밍 거리, 가장 긴 공통 부분 시퀀스 길이, 가장 긴 공통 부분 문자열 길이, 페어 거리 메트릭, Jaro-Winkler 메트릭 등 여러 대략적인 일치 및 문자열 비교 알고리즘의 구현도 포함됩니다. .


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

클로저 :

(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

첫 번째 줄의 길이로 나눈 (또는 두 줄의 최소 / 최대 / 평균 길이를 나눈) 레 벤슈 테인 거리는 어떻습니까? 그것은 지금까지 나를 위해 일했습니다.


그러나이 주제에 대한 다른 게시물을 인용하기 위해 반환되는 내용은 종종 "정상적"입니다. 'echo'는 'dog'과 매우 유사합니다.
Xyene

@Nox :이 회신의 "첫 번째 문자열의 길이로 나눈"부분이 중요합니다. 또한 이것은 오타 및 전치 오류, 심지어 일반적인 활용 (예 : "swim"및 "swam"비교)에 대해 크게 찬사를받은 주사위 알고리즘보다 성능이 우수합니다.
Logan Pickup

1

안녕하세요, 저는 이것을 자바 스크립트로 사용해 보았지만 처음 사용합니다. 누가 더 빠른 방법을 알고 있습니까?

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의 입력에 대해 bigram 함수가 중단됩니다. string_similarity 메소드는 두 번째 루프 내에서 올바르게 중단되지 않으므로 쌍을 여러 번 계산하여 리턴 값이 100 %를 초과 할 수 있습니다. 또한 선언 x및을 잊어 버렸 y으므로 루프를 사용하여 루프를 반복해서는 안됩니다 for..in..( for(..;..;..)대신 사용).
Rob W

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 답변에서, 그는 루비 젬 일치 가 순수한 루비가 아닌 구현 을 나타냅니다 . 그래서 나는 조금 발견 보석 searchd fuzzy_match 순수 루비 구현 (이 보석을 사용하지만이 amatch에) 여기를 . 나는 이것이 나와 같은 누군가를 도울 수 있기를 바랍니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.