快速算法比较字符串列表的相似度

3
我收到了一个包含超过90,000个名称的列表。我需要检查相似度大于等于50%的名称,并将结果以以下格式写入文件:
ID 1,ID 2,相似度百分比。
我已经有一个检查相似度的算法,但是遍历整个列表需要很长时间。是否有人能提供一种更快速的算法来比较名称?
以下是代码:
public static void main(String[] args) throws IOException {


    List<String> list = new ArrayList<>();
    int count = 0;
    FileWriter f = new FileWriter(new File("output.txt"));
    StringBuilder str = new StringBuilder();
    Scanner scanner = new Scanner(new File("name.csv"));

    while (scanner.hasNextLine()) {


        count++;
        list.add(scanner.nextLine());

    }


    long start = System.currentTimeMillis();

    //////////////////////////////////////////////////////////
    for (int i = 0; i < list.size(); i++) {

        for (int j = i + 1; j < list.size(); j++) {


            int percent = StringSimilarity.simi(list.get(i), list.get(j));
            if (percent >= 50) {

                str.append("ID " + i + ",ID " + j + "," + percent + " percent");
                str.append("\n");
            }
        }
    }
    ////////////////////////////////////////////////////////

    long end = System.currentTimeMillis();

    f.write(str.toString());

    System.out.println((end - start) / 1000 + " second(s)");

    f.close();
    scanner.close();

}

public static String getString(String s) {
    Pattern pattern = Pattern.compile("[^a-z A-Z]");
    Matcher matcher = pattern.matcher(s);
    String number = matcher.replaceAll("");
    return number;
}

这是数据的一个示例,名字被存储在一个 .csv 文件中,所以我读取了这个文件,并将名字存储在列表中。
名字至少包括名、姓、其他名和母亲的婚前姓氏。
Kingsley, eze, Ben, cici
Eze, Daniel, Ben, julie Jon, Smith, kelly, Joe Joseph, tan, , chellie Joseph,tan,jese,chellie ......等等
一个人至少有3个名字。正如我之前所述,程序的目的是检查姓名的相似程度,因此当比较ID 1和ID 2时,“Ben”和“eze”都是共同的,因此它们具有50%的相似度。
比较ID 4和ID 5,相似度为75%......因为它们有三个共同的名字,即使ID 4没有第三个名字......
现在问题在于,在使用两个循环进行相似性检查时,我从第一个ID开始,并通过剩余的90,000个名称进行检查,并保存与其相似度≥50%的ID,然后取下一个ID 2并执行相同操作......以此类推。

1
有一个名为soundex的算法,通常也可以在数据库中使用,它创建了一个最小的“同音”字母组。 - Joop Eggen
1
“x% 相似度”是如何定义的? - mm759
3
可以,相似性可以定义为两个或多个对象之间的共同特征或属性的度量。 - serhiyb
2
不知道“相似”是什么意思就无法回答你的问题。字符串相似度可能不是最好的选择。例如,字符串相似度会告诉你“Joe Smith”与“Jim Smith”比“James Smith”更相似,尽管“Jim Smith”和“James Smith”很可能是同一个人。 - Jim Mischel
能否给我们提供一些样本数据呢?你的例子“Jon, Smith, Joe, kenny”和“Jon, Smith, king, kelly”并没有真正解释清楚事情。你是想比较个人姓名,看看它们是否与其他个人姓名相似50%?还是在比较列表,并将出现在两个列表中的姓名写入文件? - Jim Mischel
显示剩余6条评论
5个回答

2

假设相似度函数是最优的:如果11个字母中有6个不同,那么直接返回0。

一个小改进是不使用StringBuilder并跳过已找到的匹配项。这有点关键,因为可能存在A ≈ B ∧ B ≈ C ∧ A ≉ C,导致一些匹配项丢失。

Charset charset = StandardCharsets.ISO_8859_1; // Better UTF_8

Path inputPath = Paths.get("names.txt");
List<String> list = Files.readAllLines(inputPath, charset);

Path outputPath = Paths.get("output.txt");
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(path, charset))) {

    int n = list.size();
    for (int i = 0; i < n; ++i) {
        list.set(i, normalize(list.get(i)));
    }

    for (int i = 0; i < n; ++i) {
        String ithWord = list.get(i);
        for (int j = i + 1; j < n; ++j) {
            String jthWord = list.get(j);
            if (jthWord != null) {
                int perc = similarity(ithWord, list.get(j));
                if (similarity >= 50) {
                    out.printf("ID %d,ID %d,%d percent or greater%n", i, j, perc);
                    list.set(j, null); // Skip it for other i
                }
            }
        }
    }
 }

您可以使用Java 8的并行性:

final List<String> list = ...
IntStream.range(0, list.size())
    .parallelStream()
    .map(i -> ...
    ...

但这并不会改变二次复杂度。
有帮助的是对列表进行排序,并从第i个单词中得出所有前缀,这些前缀将在90%范围内。不幸的是,在50%的情况下,这是不可行的(n超过n/2)。
我会要求其他要求,例如发音相似,最多有3个错别字等。或者在晚上运行它。

1
以下是问题作者的重要评论:
“相似性是指……Jon、Smith、Joe、Kenny和Jon、Smith、King、Kelly有50%的相似性,因为他们有两个共同的名字……如果他们有三个名字,那么就是75%,如果他们有四个名字,那么就是100%。”
可以使用基于地图的方法,正如Sakalya已经建议的那样。我建议使用一个HashMap,将名称集作为值和键的名称部分集合。例如,映射可以是:
{"Jon", "Smith"} -> {"Jon, Smith, Joe, kenny", "Jon, Smith, king, kelly"}

填充地图的想法是取出每个名称,创建包含所有名称部分的集合,并创建该集合的所有子集(不包括空集)。如果您有名称"Jon, Smith, Joe, kenny",则集合将是:

{"Jon"}, {"Smith"}, {"Joe"}, {"kenny"},
{"Jon", "Smith"}, {"Jon", "Joe"}, {"Jon", "kenny"}, {"Smith", "Joe"}, {"Smith", "kenny"}, {"Joe", "kenny"},
{"Jon", "Smith", "Joe"}, {"Jon", "Smith", "kenny"}, {"Jon", "Joe", "kenny"}, {"Smith", "Joe", "kenny"}
{"Jon", "Smith", "Joe", "kenny"}

每个名称都必须作为值元素添加到地图中,以设置为键。这必须针对每个名称完成。
在填充地图后,必须再次遍历每个名称。必须重新创建名称的部分集。想法是找到其他具有共同集合的名称。仅具有最小大小的集合才相关,因此共享该集合的另一个名称具有相似度>=50%。可以通过查询每个相关集合的地图来查找这些名称。
如果我没有漏掉任何东西,则复杂性(时间和空间)与名称数量成线性关系。假定名称的最大部分数是恒定的。具有n个部分的名称的部分集数为2 ^ n-1(参见“幂集”):
这个算法所需的空间要比问题中的算法高,但我认为在普通台式电脑上仍然不会有问题。假设每个名称有20个集合(平均),每个集合需要40字节。在这种情况下,所需的空间将是90,000*20*40 = 72,000,000字节。使用字符串池和String.intern()可以减少空间要求。

我认为主要任务是从包含姓名所有部分的集合中创建名称部分集合。以下是如何执行此操作的示例:https://dev59.com/xnI-5IYBdhLWcg3wy7wd。 - mm759

0
你的算法在相似度方面的时间复杂度是O(n^2)。最快的方法是只扫描一个列表,并将该列表中的值保存在哈希映射中作为键值。当你扫描第二个列表时,检查该元素是否已经存在于哈希映射中。这样做会更快。

问题在于该问题涉及到相似名称而非相等名称。 - mm759
第二个列表是什么?你在哈希映射中存储哪些值? - serhiyb
问题说:“我已经有一个检查相似度的算法,但是遍历整个列表需要很长时间。有人可以帮忙设计一个更快速的比较名称的算法吗?” - Sakalya
一个单词的Soundex代码可以用作映射键(参见Joop Eggen的评论)。这使用了一个等价关系作为相似性关系。 - mm759

0

这个问题的挑战在于寻找最小相似度的配对,而不必将每个单词都与另一个单词进行比较。 - mm759
@mm759,那里讨论的算法比较了最小数量的单字符编辑,即删除、插入或替换,因此它已经优化过了。但是就要求而言,通过审查算法方法可以进行进一步优化。 - Rishal

0

我认为你也可以很容易地并行化这个任务。同时计算多个相似度即可。虽然这不会改善算法的时间复杂度,但总比什么都不做好。:-)


并行化一个n^2的算法在n变得非常大时通常帮助不大。在这种情况下,他讨论的是80亿次比较。当然,你可以使用四个核心,并且将速度提高近四倍,但更好的算法可能会使单个核心的速度提高400倍。 - Jim Mischel
是的,我完全同意。但正如我所说,实际上并行化比什么都不做要好。 - Firzen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接