การเปรียบเทียบสตริงความคล้ายคลึงใน Java


111

ฉันต้องการเปรียบเทียบสตริงหลาย ๆ สายและค้นหาสตริงที่คล้ายกันมากที่สุด ฉันสงสัยว่ามีไลบรารีวิธีการหรือแนวทางปฏิบัติที่ดีที่สุดที่จะส่งคืนฉันว่าสตริงใดคล้ายคลึงกับสตริงอื่นมากกว่า ตัวอย่างเช่น:

  • "สุนัขจิ้งจอกกระโดดเร็ว" -> "สุนัขจิ้งจอกกระโดด"
  • "จิ้งจอกด่วนกระโดด" -> "สุนัขจิ้งจอก"

การเปรียบเทียบนี้จะแสดงให้เห็นว่าครั้งแรกคล้ายกันมากกว่าครั้งที่สอง

ฉันเดาว่าฉันต้องการวิธีการบางอย่างเช่น:

double similarityIndex(String s1, String s2)

มีสิ่งนี้อยู่ที่ไหนสักแห่ง?

แก้ไข: ทำไมฉันถึงทำสิ่งนี้ ฉันกำลังเขียนสคริปต์ที่เปรียบเทียบผลลัพธ์ของไฟล์ MS Project กับผลลัพธ์ของระบบเดิมที่จัดการกับงาน เนื่องจากระบบเดิมมีความกว้างของฟิลด์ที่ จำกัด มากเมื่อเพิ่มค่าคำอธิบายจะถูกย่อ ฉันต้องการวิธีกึ่งอัตโนมัติเพื่อค้นหารายการจาก MS Project ที่คล้ายกับรายการในระบบเพื่อที่ฉันจะได้รับคีย์ที่สร้างขึ้น มีข้อเสียเนื่องจากยังต้องตรวจสอบด้วยตนเอง แต่จะช่วยประหยัดงานได้มาก

คำตอบ:


82

ใช่มีอัลกอริทึมที่มีการจัดทำเอกสารไว้เป็นอย่างดีเช่น:

  • ความคล้ายคลึงกันของโคไซน์
  • ความคล้ายคลึงกันของ Jaccard
  • ค่าสัมประสิทธิ์ของลูกเต๋า
  • จับคู่ความเหมือน
  • ความคล้ายคลึงกันที่ทับซ้อนกัน
  • ฯลฯ ฯลฯ

ข้อมูลสรุปที่ดี ("Sam's String Metrics") สามารถพบได้ที่นี่ (ลิงก์เดิมตายดังนั้นลิงก์ไปยัง Internet Archive)

ตรวจสอบโครงการเหล่านี้ด้วย:


18
+1 ไซต์ simmetrics ดูเหมือนจะไม่ทำงานอีกต่อไป อย่างไรก็ตามฉันพบรหัสใน sourceforge: sourceforge.net/projects/simmetricsขอบคุณสำหรับตัวชี้
Michael Merchant

7
ลิงก์ "คุณสามารถตรวจสอบนี้" เสีย
Kiril

1
นั่นเป็นเหตุผลที่ Michael Merchant โพสต์ลิงก์ที่ถูกต้องด้านบน
emilyk

2
jar สำหรับ simmetrics บน sourceforge ค่อนข้างล้าสมัยgithub.com/mpkorstanje/simmetricsคือหน้า github ที่อัปเดตพร้อมสิ่งประดิษฐ์ maven
tom91136

เพื่อเพิ่มความคิดเห็น @MichaelMerchant ของโครงการยังมีอยู่บนGitHub ไม่ค่อยมีการใช้งานที่นั่น แต่ล่าสุดกว่า sourceforge เล็กน้อย
Ghurdyl

163

วิธีทั่วไปในการคำนวณความคล้ายคลึงกันระหว่างสองสตริงในรูปแบบ 0% -100%ตามที่ใช้ในหลายไลบรารีคือการวัดจำนวน (เป็น%) ที่คุณต้องเปลี่ยนสตริงที่ยาวกว่าเพื่อเปลี่ยนให้สั้นลง:

/**
 * Calculates the similarity (a number within 0 and 1) between two strings.
 */
public static double similarity(String s1, String s2) {
  String longer = s1, shorter = s2;
  if (s1.length() < s2.length()) { // longer should always have greater length
    longer = s2; shorter = s1;
  }
  int longerLength = longer.length();
  if (longerLength == 0) { return 1.0; /* both strings are zero length */ }
  return (longerLength - editDistance(longer, shorter)) / (double) longerLength;
}
// you can use StringUtils.getLevenshteinDistance() as the editDistance() function
// full copy-paste working code is below


การคำนวณeditDistance():

editDistance()ฟังก์ชั่นข้างต้นคาดว่าจะคำนวณแก้ไขระยะห่างระหว่างสองสาย ขั้นตอนนี้มีการนำไปใช้หลายอย่างแต่ละขั้นตอนอาจเหมาะกับสถานการณ์เฉพาะที่ดีกว่า สิ่งที่พบบ่อยที่สุดคืออัลกอริทึมระยะทาง Levenshteinและเราจะใช้ในตัวอย่างด้านล่าง (สำหรับสตริงที่มีขนาดใหญ่มากอัลกอริทึมอื่น ๆ มีแนวโน้มที่จะทำงานได้ดีกว่า)

มีสองตัวเลือกในการคำนวณระยะทางแก้ไข:

  • คุณสามารถใช้การใช้งานระยะทาง Levenshtein ของApache Commons Text : apply(CharSequence left, CharSequence rightt)
  • นำไปใช้ด้วยตัวคุณเอง ด้านล่างนี้คุณจะพบตัวอย่างการใช้งาน


ตัวอย่างการทำงาน:

ดูการสาธิตออนไลน์ได้ที่นี่

public class StringSimilarity {

  /**
   * Calculates the similarity (a number within 0 and 1) between two strings.
   */
  public static double similarity(String s1, String s2) {
    String longer = s1, shorter = s2;
    if (s1.length() < s2.length()) { // longer should always have greater length
      longer = s2; shorter = s1;
    }
    int longerLength = longer.length();
    if (longerLength == 0) { return 1.0; /* both strings are zero length */ }
    /* // If you have Apache Commons Text, you can use it to calculate the edit distance:
    LevenshteinDistance levenshteinDistance = new LevenshteinDistance();
    return (longerLength - levenshteinDistance.apply(longer, shorter)) / (double) longerLength; */
    return (longerLength - editDistance(longer, shorter)) / (double) longerLength;

  }

  // Example implementation of the Levenshtein Edit Distance
  // See http://rosettacode.org/wiki/Levenshtein_distance#Java
  public static int editDistance(String s1, String s2) {
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();

    int[] costs = new int[s2.length() + 1];
    for (int i = 0; i <= s1.length(); i++) {
      int lastValue = i;
      for (int j = 0; j <= s2.length(); j++) {
        if (i == 0)
          costs[j] = j;
        else {
          if (j > 0) {
            int newValue = costs[j - 1];
            if (s1.charAt(i - 1) != s2.charAt(j - 1))
              newValue = Math.min(Math.min(newValue, lastValue),
                  costs[j]) + 1;
            costs[j - 1] = lastValue;
            lastValue = newValue;
          }
        }
      }
      if (i > 0)
        costs[s2.length()] = lastValue;
    }
    return costs[s2.length()];
  }

  public static void printSimilarity(String s, String t) {
    System.out.println(String.format(
      "%.3f is the similarity between \"%s\" and \"%s\"", similarity(s, t), s, t));
  }

  public static void main(String[] args) {
    printSimilarity("", "");
    printSimilarity("1234567890", "1");
    printSimilarity("1234567890", "123");
    printSimilarity("1234567890", "1234567");
    printSimilarity("1234567890", "1234567890");
    printSimilarity("1234567890", "1234567980");
    printSimilarity("47/2010", "472010");
    printSimilarity("47/2010", "472011");
    printSimilarity("47/2010", "AB.CDEF");
    printSimilarity("47/2010", "4B.CDEFG");
    printSimilarity("47/2010", "AB.CDEFG");
    printSimilarity("The quick fox jumped", "The fox jumped");
    printSimilarity("The quick fox jumped", "The fox");
    printSimilarity("kitten", "sitting");
  }

}

เอาท์พุต:

1.000 is the similarity between "" and ""
0.100 is the similarity between "1234567890" and "1"
0.300 is the similarity between "1234567890" and "123"
0.700 is the similarity between "1234567890" and "1234567"
1.000 is the similarity between "1234567890" and "1234567890"
0.800 is the similarity between "1234567890" and "1234567980"
0.857 is the similarity between "47/2010" and "472010"
0.714 is the similarity between "47/2010" and "472011"
0.000 is the similarity between "47/2010" and "AB.CDEF"
0.125 is the similarity between "47/2010" and "4B.CDEFG"
0.000 is the similarity between "47/2010" and "AB.CDEFG"
0.700 is the similarity between "The quick fox jumped" and "The fox jumped"
0.350 is the similarity between "The quick fox jumped" and "The fox"
0.571 is the similarity between "kitten" and "sitting"

11
วิธีระยะทาง Levenshtein มีให้ในorg.apache.commons.lang3.StringUtils.
Cleankod

@Cleankod ตอนนี้เป็นส่วนหนึ่งของ commons-text: commons.apache.org/proper/commons-text/javadocs/api-release/org/…
Luiz

15

ฉันแปลอัลกอริทึมระยะทาง Levenshteinเป็น JavaScript:

String.prototype.LevenshteinDistance = function (s2) {
    var array = new Array(this.length + 1);
    for (var i = 0; i < this.length + 1; i++)
        array[i] = new Array(s2.length + 1);

    for (var i = 0; i < this.length + 1; i++)
        array[i][0] = i;
    for (var j = 0; j < s2.length + 1; j++)
        array[0][j] = j;

    for (var i = 1; i < this.length + 1; i++) {
        for (var j = 1; j < s2.length + 1; j++) {
            if (this[i - 1] == s2[j - 1]) array[i][j] = array[i - 1][j - 1];
            else {
                array[i][j] = Math.min(array[i][j - 1] + 1, array[i - 1][j] + 1);
                array[i][j] = Math.min(array[i][j], array[i - 1][j - 1] + 1);
            }
        }
    }
    return array[this.length][s2.length];
};

11

คุณสามารถใช้ระยะทาง Levenshtein เพื่อคำนวณความแตกต่างระหว่างสองสตริง http://en.wikipedia.org/wiki/Levenshtein_distance


2
Levenshtein เหมาะสำหรับสตริงไม่กี่สาย แต่จะไม่ปรับขนาดเพื่อเปรียบเทียบระหว่างสตริงจำนวนมาก
ผู้ใช้จ่าย

ฉันใช้ Levenshtein ใน Java ด้วยความสำเร็จ ฉันไม่ได้ทำการเปรียบเทียบกับรายการขนาดใหญ่ดังนั้นอาจมีผลงานที่ได้รับความนิยม นอกจากนี้ยังค่อนข้างง่ายและสามารถใช้การปรับแต่งบางอย่างเพื่อเพิ่มเกณฑ์สำหรับคำที่สั้นกว่า (เช่น 3 หรือ 4 ตัวอักษร) ซึ่งมักจะถูกมองว่าคล้ายกันมากกว่าที่ควร (เป็นการแก้ไขเพียง 3 ครั้งจากแมวถึงสุนัข) โปรดทราบว่าการแก้ไขระยะทาง ข้อเสนอแนะด้านล่างนี้ค่อนข้างเหมือนกัน - Levenshtein คือการใช้งานแก้ไขระยะทางโดยเฉพาะ
Rhubarb

นี่คือบทความที่แสดงวิธีการรวม Levenshtein กับแบบสอบถาม SQL ที่มีประสิทธิภาพ: literatejava.com/sql/fuzzy-string-search-sql
Thomas W

10

มีมาตรการความคล้ายคลึงกันของสตริงมากมายที่นั่น:

  • Levenshtein แก้ไขระยะทาง;
  • ระยะ Damerau-Levenshtein;
  • ความคล้ายคลึงกันของ Jaro-Winkler;
  • ระยะทางแก้ไขลำดับต่อมาที่ยาวที่สุด
  • คิว - แกรม (Ukkonen);
  • n-Gram ระยะทาง (Kondrak);
  • ดัชนี Jaccard;
  • สัมประสิทธิ์ Sorensen-Dice;
  • ความคล้ายคลึงกันของโคไซน์
  • ...

คุณสามารถค้นหาคำอธิบายและการใช้งาน java ได้ที่นี่: https://github.com/tdebatty/java-string-similarity


8

คุณสามารถบรรลุนี้โดยใช้ห้องสมุดคอมมอน Apache จาวา ลองดูทั้งสองฟังก์ชั่นภายใน:
- getLevenshteinDistance
- getFuzzyDistance


3
ตั้งแต่เดือนตุลาคม 2017 วิธีการที่เชื่อมโยงจะเลิกใช้งาน ใช้คลาสLevenshteinDistanceและFuzzyScoreจากไลบรารีข้อความคอมมอนส์แทน
vatbub



3

ดูเหมือนเป็นเครื่องมือค้นหาการลอกเลียนแบบสำหรับฉันถ้าสตริงของคุณกลายเป็นเอกสาร บางทีการค้นหาด้วยคำนั้นจะทำให้เกิดสิ่งดีๆ

"Programming Collective Intelligence" มีบทเกี่ยวกับการพิจารณาว่าเอกสารสองชุดมีความคล้ายคลึงกันหรือไม่ รหัสอยู่ใน Python แต่สะอาดและง่ายต่อการพอร์ต


3

ขอบคุณผู้ตอบคนแรกฉันคิดว่ามีการคำนวณ computeEditDistance (s1, s2) 2 รายการ เนื่องจากการใช้เวลานานจึงตัดสินใจปรับปรุงประสิทธิภาพของโค้ด ดังนั้น:

public class LevenshteinDistance {

public static int computeEditDistance(String s1, String s2) {
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();

    int[] costs = new int[s2.length() + 1];
    for (int i = 0; i <= s1.length(); i++) {
        int lastValue = i;
        for (int j = 0; j <= s2.length(); j++) {
            if (i == 0) {
                costs[j] = j;
            } else {
                if (j > 0) {
                    int newValue = costs[j - 1];
                    if (s1.charAt(i - 1) != s2.charAt(j - 1)) {
                        newValue = Math.min(Math.min(newValue, lastValue),
                                costs[j]) + 1;
                    }
                    costs[j - 1] = lastValue;
                    lastValue = newValue;
                }
            }
        }
        if (i > 0) {
            costs[s2.length()] = lastValue;
        }
    }
    return costs[s2.length()];
}

public static void printDistance(String s1, String s2) {
    double similarityOfStrings = 0.0;
    int editDistance = 0;
    if (s1.length() < s2.length()) { // s1 should always be bigger
        String swap = s1;
        s1 = s2;
        s2 = swap;
    }
    int bigLen = s1.length();
    editDistance = computeEditDistance(s1, s2);
    if (bigLen == 0) {
        similarityOfStrings = 1.0; /* both strings are zero length */
    } else {
        similarityOfStrings = (bigLen - editDistance) / (double) bigLen;
    }
    //////////////////////////
    //System.out.println(s1 + "-->" + s2 + ": " +
      //      editDistance + " (" + similarityOfStrings + ")");
    System.out.println(editDistance + " (" + similarityOfStrings + ")");
}

public static void main(String[] args) {
    printDistance("", "");
    printDistance("1234567890", "1");
    printDistance("1234567890", "12");
    printDistance("1234567890", "123");
    printDistance("1234567890", "1234");
    printDistance("1234567890", "12345");
    printDistance("1234567890", "123456");
    printDistance("1234567890", "1234567");
    printDistance("1234567890", "12345678");
    printDistance("1234567890", "123456789");
    printDistance("1234567890", "1234567890");
    printDistance("1234567890", "1234567980");

    printDistance("47/2010", "472010");
    printDistance("47/2010", "472011");

    printDistance("47/2010", "AB.CDEF");
    printDistance("47/2010", "4B.CDEFG");
    printDistance("47/2010", "AB.CDEFG");

    printDistance("The quick fox jumped", "The fox jumped");
    printDistance("The quick fox jumped", "The fox");
    printDistance("The quick fox jumped",
            "The quick fox jumped off the balcany");
    printDistance("kitten", "sitting");
    printDistance("rosettacode", "raisethysword");
    printDistance(new StringBuilder("rosettacode").reverse().toString(),
            new StringBuilder("raisethysword").reverse().toString());
    for (int i = 1; i < args.length; i += 2) {
        printDistance(args[i - 1], args[i]);
    }


 }
}

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.