【问题标题】:Efficient way to compare two strings (ordering of characters irrelevant)比较两个字符串的有效方法(字符顺序无关)
【发布时间】:2010-12-09 16:20:48
【问题描述】:

我正在尝试提出一种算法来比较两个字符串。它将注册匹配任何包含相同字母的单词。例如,rent 和 tern 是等价的,因为它们都包含字母 r,e,n,t。

编辑我很抱歉如此含糊。将在两组数千个单词上进行数百次比较。这只是整个代码的一小部分,所以我不希望它让一切陷入困境。

对于那些询问“是”的人来说,过度匹配将非常重要,例如,租金也将匹配 ternicate。

EDIT 2 对于像rent == ternicate 这样的匹配,ternicate 不会匹配rent。它更像是单词二包含单词一的字母。因此,如果您有额外的字母,只要该单词包含第一个单词的所有字母,它仍然是匹配的。

【问题讨论】:

  • 你在乎被欺骗的角色吗? “abcd”和“abcdcba”是否相等?
  • 你能证明你需要“效率”吗?如果你做不到,大多数人都会把 Knuth 扔给你。 :)
  • 请澄清“rent”匹配“ternicate” - 是否可以交换使得“ternicate”也匹配“rent”?更多示例会有所帮助...
  • -1 - 您从一个关于比较一对字符串的简单且略显模棱两可的问题开始,并将其变成了一个关于比较字符串集的严重模棱两可的问题。
  • 所以,澄清一下,您需要所有对(单词 A,单词 B),其中单词 A 中的字母是单词 B 中的字母的子集,其中单词 A 和 B 来自不同的列表?这就是我的回答。

标签: java regex string algorithm compare


【解决方案1】:

简单地先对每个字符串的字符进行排序,然后比较它们。

rent == tern
enrt == enrt

【讨论】:

  • 我考虑过,但我认为它不够高效。
  • @Rob - 这取决于你如何对它们进行排序。
  • 对于小型数据集,看似过于简单的方法往往是最有效的方法。
  • 保持简单!在使用真实数据对其进行分析之前,不要猜测速度。
  • 回复:“我曾考虑过,但我认为它不够高效。”我经常在 python 中对 175K 单词的英语词典做这种事情,大约需要 2-3 秒才能完成所有单词。您的效率问题基于什么?
【解决方案2】:

另一种方法是计算每个字符串中每个字符的数量并比较计数。一个简单的实现应该花费O(max(N, A)) 时间,其中 N 是较大字符串的长度,A 是用于存储计数的数组的大小。例如,在 Java 中:

public boolean equalIgnoringOrder(String s1, String s2) {
    if (s1.length() != s2.length()) {
        return false;
    }
    // Assuming characters in the range ASCII 0 to 127 
    int[] c1 = new int[128];
    int[] c2 = new int[128];
    for (int i = 0; i < s1.length(); i++) {
        c1[s1.charAt(i)]++;
        c2[s2.charAt(i)]++;
    }
    for (int i = 0; i < c1.length; i++) {
        if (c1[i] != c2[i]) {
            return false;
        }
    }
    return true;
}

对此有一些可能的改进。例如,您可以通过缩小范围来处理任意字符集;即通过s1 和s2 进行初始传递,寻找每个字符中的最小和最大字符,并使用它来确定c1 和c2 的大小以及基本偏移量。这将平均使用更少的空间并减少初始化计数数组的时间。它还为比较提供了短路;例如当s1 和s2 的最小和最大字符不相同时。

相比之下,比较使用堆排序或快速排序排序的字符串平均为O(NlogN) 与O(N) 空间,其中N 是较大字符串的长度。

但是,正如@pst 指出的那样,如果 N 不大,比例常数可以使 O(NlogN) 甚至 O(N*N) 算法优于 O(N) 算法。在这种情况下,被比较的字符串的平均长度可能是最重要的因素。

上面的代码有效地执行了带有几个短路的基数排序。 (如果包括与范围缩小相关的短路,则为三个。)因此最终归结为快速排序/堆排序或基数排序是否更好。这取决于输入字符串的长度和字符范围。


采取不同的策略。 @John 的回答建议我们计算素数的乘积。如果我们使用任意精度表示进行计算,则结果值对于每个不同的“相等忽略顺序”字符串集都是唯一的。不幸的是,计算将是O(N*N)。 (每个中间产品都有O(N) 数字,将一个N 位数字乘以一个常数是O(N)。对N 个字符执行此操作,您将得到O(N*N)。)

但是如果我们以 64 为模进行计算,则结果实际上是一个非常好的散列,它对字符顺序不敏感;例如

long hash = 1;
for (int i = 0; i < s.length(); i++) {
    hash = hash * primes[s.charAt(i)];
}

因此,我认为在比较随机生成的字符串时平均提供最佳性能和空间使用率的算法可能是以下形式:

if (s1.length() != s2.length()) {
    return false;
}
if (hash(s1) != hash(s2)) { // computed as above
    return false;
}
// Compare using sorting or character counting as above.

最后一点。如果我们假设字符串指针不相同并且字符串的长度不相等,那么计算此equals 谓词的任何算法都必须为O(N) 或更糟。它必须检查两个字符串中的每个字符才能做出决定,这需要O(N) 操作。

在这种情况下,任何算法对获取的值执行少于2 * N fetches 或少于2 * N 的进一步操作都可证明是不正确的。

【讨论】:

  • @Stephen Idea 小偷(开个玩笑)。 +1 用于代码的良好显示。但是,这种排序可以作为基数排序执行,因此排序复杂度的 O 可以大大降低。
  • @pst - 这个 >>IS
  • @Stephen 我对此提出质疑:) 它建立频率表,然后比较它们。排序失败。
  • (好吧,好吧,这是一个便宜的镜头。)
  • 所以你的哈希从 0 开始,然后乘以一系列素数?我打赌你在发布之前没有运行该代码。
【解决方案3】:

好的,这是一个非常糟糕的主意,但是 这太疯狂了,它可能会奏效!

  1. 创建前 26 个素数的列表。

    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, ...]
    
  2. 对于单词的每个字母,找到对应的质数。 A→2、B→3、C→5等

  3. 将这些质数相乘。你最终会得到一个(非常大的)数字。

具有相同字母的单词将具有相同的数字。不同字母的单词保证有不同的数字。这是为什么呢?

因为我们将质数相乘,所以我们总是会得到用于唯一字母组合的唯一乘积。这些数字可以分解回它们的主要因素,这些因素准确地告诉我们原始单词中有哪些字母。字母的顺序没有保留,但单词中有哪些字母以及有多少个。

例如,使用“face”和“cafe”这两个词。

FACE = 13 * 2 * 5 * 11 = 1430  
CAFE = 5 * 2 * 13 * 11 = 1430

哈!还有什么比简单的整数比较更有效的呢?

...

好吧,不,也许不是。这有点太荒谬了,无法实际使用。不过很整洁。

【讨论】:

  • 这只能在您对数字使用 BigInteger 表示时才有效。如果你使用固定大小的整数,你会遇到冲突和误报。
  • 使用适合 int 的 32 位位图。
  • 这也适用于过度匹配,因为您可以只检查可分性而不是相等性。仍然需要处理重复的字母,但由于问题没有指定如何处理它们,因此无法设计该部分。不过,在我看来,这种情况也很简单,无论需要哪种(合理的)处理。
  • @pst - 查看我对 32 位位图解决方案的回答,该解决方案似乎满足模糊的要求...
  • 你真的应该考虑 Alnitak 的回应 - 它不那么“花哨”(就像在“哦,它太疯狂了,它可能会起作用!”)但实际上是相同的想法,只是更多的内存和计算有效(处理器喜欢二进制操作)。
【解决方案4】:

考虑到问题的含糊性,这里的关键是它确实似乎没有必要计算任何字母出现的次数,只是它确实出现了。

因此,假设所有字母都在a-z 范围内,并且还假设可以使用整数索引将原始单词列表索引为数组:

1. 创建两个数组(每个列表一个)。

2. 为两个列表中的每个单词计算位图如下:

bitmap = 0
foreach (character in word) {
    bitmap |= (1 << (character - 'a'))
}
arrayX[index] = bitmap;

此位图表示出现在该单词中的所有字母的集合。

3. 然后对集合 A 中的每个单词,迭代集合 B,并在何时匹配

arrayA[indexA] | arrayB[indexB] == arrayB[indexB]

只有当单词 A 中的字符集是单词 B 字符的子集时,该测试才会为真。位集的“或”操作等效于实集的并集 (∪) 运算符。

查看set mathemtatics 上的维基百科条目 - A ⊆ B 当且仅当 A ∪ B = B。

顺便说一句,第 3 步是 O(n^2),但仍然应该非常快,因为它只是按位比较。每个列表中有几千个单词(约 4M 测试)应该不到一秒钟。

【讨论】:

  • 请注意采取任何必要的预防措施,例如:1) 将所有 words 转换为小写 2) 如果您有除 'a'-'z' 以外的字符(例如不是 7 位),请进行特殊处理-ASCII-English) 3) bitmap 必须能够包含每个字符的标志,因此至少有 26 (?) 位宽
  • 这不是位图,而是直方图。
  • @Nick - 嗯?不,它是一个位图(或位集),表示出现了哪些字母。它不计算每个发生多少次,这将是一个直方图。
  • @akavel - 它是 java - 整数总是 32 位宽,我也确实陈述了“a-z”假设...... :)
  • 优秀的解决方案。然后,当然,我们需要知道他是否会多次遍历每对集合,还是每个单词只遍历一次。因为,当然,如果他多次遍历它们,他会希望将每个单词都存储在数组中。
【解决方案5】:

也许不是最快的,但可能是使用 java+google-collections+guava 的最短解决方案(用于转换 char[]->List&lt;Character&gt;)

import com.google.common.collect.ImmutableMultiset;
import com.google.common.primitives.Chars;

public class EqualsOrderignore {
private static boolean compareIgnoreOrder(final String s1, String s2) {
    return ImmutableMultiset.copyOf(Chars.asList(s1.toCharArray()))
            .equals(ImmutableMultiset.copyOf(Chars.asList(s2.toCharArray())));
} 
}

该算法的运行时间:O(s1.length + s2.length)

我非常相信该解决方案将在 -server VM 上与手工制作的 O(N1+N2) 解决方案执行同等的性能。

此外,此解决方案适用于任何字符实例,而不仅仅是 a-Z。

【讨论】:

    【解决方案6】:

    假设:

    1. 您的文字仅包含 ascii 字符
    2. 大小写无关
    3. abc 匹配 abcde 而 abcde 不匹配 abc

    您可以通过匹配字符串 (s2) 计数字符,然后通过值 (s1) 并检查所有字符是否存在于另一个中,例如(伪代码,未检查):

    boolean matches(String s1, String s2) {
       int[]  counts = new int[256];
       char[] c1;
       char[] c2;
    
       c1 = s1.getCharArray();
       c2 = c2.getCharArray();
    
       // count char occurences in longest string
       for (int n = 0; n < c2.length; n++) {
           counts[(int)c2[n]]++;
       }
    
       // check all chars in shortest string are foud in the longest
       for (int n = 0; n < c1.length; n++) {
           if (0 == counts[(int)c1[n]]) {
              return false;
           }
       }
    
       return true;
    }
    

    对于参数长度的总和来说,这将是 O(n)。

    编辑:问题已更改为 s1 和 s2 之间的不对称函数。

    【讨论】:

    • 为什么把count数比为0呢?这只是说明在另一个字符串中是否至少找到一个实例。也许在 s2 上做同样的计数并查看 s1_count(c) &lt;= s2_count(c) 是否为 s1 中的每个 c 更有意义。
    • 这正是所要求的:如果 s1 中的所有字符无论顺序如何都出现在 s2 中,则 s1 匹配 s2。因此,如果 s1 中的一个字符没有出现在 s2 中,则它不匹配,因此比较不匹配 0。
    【解决方案7】:

    我编写了很多与文字游戏和字谜有关的代码。通常的方法是将单词转换为排序键,以便如上所述,'rent' 匹配 'tern',因为两者都映射到 'enrt'。但是,一旦您开始使用这条路线,拥有一个字符字典和出现次数就变得非常有用。下面是一些 Python 代码,可以将未排序的字符串转换为具有 (key=character, value=count) 的字典:

    import collections
    
    # Create a defaultdict(int) from a string
    def create_collections_dict(key):
        dk = collections.defaultdict(int)
        for k in key:
            dk[k] += 1
        return dk
    

    现在您可以通过即时查看单词的共同点数来对其他单词进行评分:

    # Score the similarity of a defaultdict(int) against a string
    # (which is temporarily converted to a defaultdict(int))
    def score(dk, cand) :
        dc = create_collections_dict(cand)
        return sum(min(dk[k], dc[k]) for k in dk.keys() if k in dc)
    
    if __name__ == '__main__':
        base = create_collections_dict('rent')
        for word in ['tern', 'ternicate', 'foobar']:
            print word, score(base, word)
    

    结果:

    tern 4
    ternicate 4
    foobar 1
    

    【讨论】:

      【解决方案8】:

      我必须同意 Stephen C 的观点 - 这还不够明确,无法回答。

      我不会投反对票,但您能否解释一下,例如,租金是否等同于 terrent?你有一些回答者假设它是(人们认为发生的次数并不重要,而其他回答者则假设最坏。其中一个群体正在浪费他们的时间。

      另外,由于您关心的是性能,我们需要了解更多有关您的调用模式的信息。你能解释一下你是否会不止一次地看一对集合,或者集合是否有所不同?

      就像术语抽搐一样,您可能已经知道这一点,但是根据当前的公式,您的算法不是对称的。

      您说rent 将与 ternicate 匹配,但显然 ternicate 与rent 不匹配。 因此,您并不是真的在寻找等价物。 您在寻找诸如“发现于”或“可以制成”之类的东西。

      这意味着您必须关心顺序 - 根据您访问集合的方式,您会得到不同的结果。

      不要误会我的意思:这是一个有趣的问题......我只是不知道问题是什么。

      【讨论】:

      • 定义得很好,现在可以回答了
      【解决方案9】:

      这很模糊,但我会使用关联数组来解决它:

      使用每个单词的每个字母作为整数关联数组的键。一个单词的字母会增加值,而另一个会减少。然后最后你可以通过所有键运行 foreach 并检查所有值是否为零,然后它匹配。这将为您提供基本的rent==tren 功能。

      含糊的注意事项: 1.如果多个字母都可以,比如rent==rreenntt,那么在给数组添加字母的时候,检查key是否存在,如果存在就不要再添加了。
      2. 如果额外的字母没问题,例如rent==renter,但是fernt!=renter,那么在最后检查数组值时,检查1 和-1 是否同时在数组中。也就是说,只有1和0都可以,或者-1和0都可以,但不是1和-1不能同时在数组中。

      我不知道这相对于其他方法有多快,但它很容易实现。

      【讨论】:

        【解决方案10】:

        我认为你应该建一棵树。 我写了一些 python 代码来说明这个想法,但它可能是错误的:

        class knot():
            def __init__(self, char, is_word, string = "" way = 0):
                self.children = []
                self.shortest_way = way
                self.char = char
                self.word = is_word
                self.string = string
        
        def comparing(strings):
            sorted_strings = []
            for string in strings:
                array_of_string = []
                for char in string:
                    array_of_string.append(char)
                sorted_strings.append(array_of_string.sort())
            sorted_strings.sort()
        
            start = []
            matches = []
        
            for array_of_string in sorted_strings:
                matches += insert_string(array_of_string, start)
        
        def insert_string(array, start):
            for match_string in test_string(array, start):
                matches += (array, match_string.string)
            add_string(array, start, 0):
        
        def add_string(array, knots, n):
            minimum = 0
            maximum = len(knots) - 1
            while minimum != maximum:
                num = int((minimum + maximum) / 2)
                if (knots[num].char > array[n]):
                    minimum = num
                elif (knots[num].char < array[n]):
                    maximum = num
                elif (knots[num].char == array[n]):
                    return add_string(array, knots[num], n+1)
            knots.append(new_knots(array, n))
            knots.sort
        
            """ more insertion routine needed"""
        
        def search_children(array, knots):
            minimum = 0
            maximum = len(knots) - 1
            while minimum != maximum:
                num = int((minimum + maximum) / 2)
                if (knots[num].char > array[0]):
                    minimum = num
                elif (knots[num].char < array[0]):
                    maximum = num
                elif (knots[num].char == array[0]):
                    return test_string(array, knots[num])
            return []
        
        def test_string(array, target_knot):
            if len(array) > target_knot.sortest_way + 1:
                return []
            match_knots = []
            if len(array) == 1 and target_knot.is_word == True:
                match_knots.append(target_knot)
            for i in range(1, len(array)):
                match_knots += search_children(array[i:], target_knot.children)
            return match_knots
        

        【讨论】:

          【解决方案11】:

          假设您只是在寻找子集,并且仅限于常见的英文字母,那么有效的直方图就可以了。我会考虑使用 64 位无符号整数,其中 2 位用于最多计数 2 次,额外的 12 位用于添加溢出标志并最多计数 3 次“e t a o i n s r h l d”。位被填充而不是使用二进制(因此对于三个'e',您将拥有 111,否则您需要比二进制 & 更复杂的东西来测试包容性)。要测试子集关系,请检查正在测试的子集的溢出位,如果未设置,则可以使用按位和来测试子集。如果直方图溢出,则回退到 O(Length) 检查字符串的排序内容。

          【讨论】:

            【解决方案12】:

            对于您选择的任何算法,都可能会针对相同长度的字符串进行优化。您所要做的就是对每个字符进行异或运算,如果结果为 0,那么它们包含相同的字母。这在子字符串的情况下没有帮助,但它可能有助于缩短更昂贵的比较。

            【讨论】:

              【解决方案13】:

              区分大小写。 1-将字符串都转换为小写/大写。 2-计算所有字符的总和以获得单词的总和 - 即租 - > r + e + n + t(将添加每个字符的等效ascii值) 3-对第二个字符串重复步骤 2 4-如果两个总和相等,则意味着两个字符串都包含完全相同的字符,与排序无关。

              private static boolean matchIrrespectiveOfCharsOrder(String wordOne,String wordTwo){
                  int sumOfAsciiCharsOne=sum(wordOne.toCharArray());
                  int sumOfAsciiCharsTwo=sum(wordTwo.toCharArray());
                      if(sumOfAsciiCharsOne==sumOfAsciiCharsTwo)
                          return true;
                  }       
                  return false;
              }
              private static int sum(char[] arr) {
                  int sum=0;
                  for(char chh:arr) {
                      sum+=chh;
                  }
                  return sum;
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-12-25
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-10-28
                相关资源
                最近更新 更多