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

时间:2016-09-16 17:28:32

标签: java algorithm

我获得了一个包含超过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;
}

这是数据如何看的样本......名称存储在a中。 csv文件,所以我读了文件并将名字存储在列表中。

姓名,姓名,其他姓名,母亲的女性姓名

Kingsley,eze,Ben,cici

Eze,Daniel,Ben,julie

乔恩,史密斯,凯莉,乔

Joseph,tan,chellie

约瑟夫,棕褐色,谢斯,chellie

....等等 一个人至少可以拥有3个名字......就像我之前所说的那样,该程序将检查名称的相似程度,因此在比较Id 1和id 2时,“ben”是常见的,“eze”很常见,所以他们有50%的相似性。 比较id 4和id 5,相似度为75%....因为它们有3个共同名称,即使id 4没有第3个名字....

所以问题是......在使用两个for循环的相似性检查期间,我从第一个ID开始并通过剩余的90,000个名称检查它并保存它具有&gt; = 50%相似度的id,然后拿下一个id 2并做同样的......依此类推

5 个答案:

答案 0 :(得分:2)

假设simularity函数是最优的:如果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 :(得分:1)

以下评论作者的评论很重要:

  

通过相似性我的意思是........ Jon,Smith,Joe,kenny和Jon,Smith,国王,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"}

必须将名称作为value-element添加到地图中,作为键的每个集合。必须为每个名称进行此操作。

填充地图后,必须再次对每个名称进行迭代。一个人必须再次创建名称的部分集。我们的想法是找到具有共同点的其他名称。仅具有最小尺寸的集合是相关的,因此共享该集合的另一个名称具有相似性> = 50%。查找这些名称可以通过查询每个相关集合的地图来完成。

如果我不错过任何事情,复杂性(时间和空间)与名字数量呈线性关系。假定名称的最大部分数量是常数。包含n部分的名称的部分集数量为2^n-1(cp。&#34; power set&#34;):

  • 1部分:1
  • 2部分:3
  • 3部分:7
  • 4部分:15
  • 5部分:31
  • 6部分:63
  • 7部分:127

空间要求高于属于该问题的算法,但我认为它们在普通台式计算机上仍然没有问题。假设每个名称有20组(平均值),每组需要40个字节。在这种情况下,所需的空间将是90,000*20*40 = 72,000,000个字节。通过使用带有String.intern()的字符串池可以减少空间要求。

答案 2 :(得分:0)

你的算法对于相似性是O(n ^ 2)。最快的方法是扫描一个列表并将这些列表的值保存在哈希映射中作为键值。当你扫描第二个列表然后检查如果该元素已经存在于hashmap中。这将更快地运行。

答案 3 :(得分:0)

存在许多字符串匹配算法,并且已经在SO上进行了大量讨论。

浏览此链接https://stackoverflow.com/questions/955110/similarity-string-comparison-in-java

答案 4 :(得分:0)

你也可以很容易地并行化这个任务恕我直言。只需同时计算多个相似度。它不会提高算法的时间复杂度,但总比没有好。 : - )