比较两个字符串的有效方法(不相关的字符排序)

时间:2009-10-24 06:02:37

标签: java regex string algorithm compare

我试图想出一个比较两个字符串的算法。它会在任何包含相同字母的单词中注册匹配。例如,租金和燕鸥是等价的,因为它们都包含字母r,e,n,t。

编辑我为这么模糊而道歉。比较将在两组几千个单词上进行数百次。这只是整个代码的一小部分,所以我不希望它让一切都陷入困境。

对于那些要求过度匹配的人来说非常重要,例如租金也会与ternicate相匹配。

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

12 个答案:

答案 0 :(得分:11)

好的,这是一个非常糟糕的主意,但它真的太疯狂了!

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

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

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

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

    因为我们正在增加素数,所以我们总会得到独特的字母组合产品。这些数字可以分解回它们的素数因子,这些因素可以准确地告诉我们原始单词中的哪些字母。不保留字母的顺序,但字母中包含哪些字母以及有多少字母。

    例如,请使用“face”和“cafe”。

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

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

    ...

    好的,不,也许不是。实际使用这有点太荒谬了。虽然很好。

答案 1 :(得分:6)

首先对每个字符串的字符进行排序,然后进行比较。

rent == tern
enrt == enrt

答案 2 :(得分:4)

另一种方法是计算每个字符串中每个字符的数量并比较计数。一个简单的实现应该花费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;
}

对此有一些可能的改进。例如,您可以通过减少范围来处理任意字符集;即首先通过s1s2寻找每个中最小和最大的字符,并使用它来确定c1c2的大小和基数偏移。这将平均使用更少的空间并减少初始化计数阵列的时间。它还提供了比较的短路;例如当s1s2的最小和最大字符不相同时。

相比之下,比较使用heapsort或quicksort排序的字符串平均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,那么结果实际上是一个对字符顺序不敏感的非常好的哈希; e.g。

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次或少于2 * N次进一步操作的任何算法都可证明是错误的。

答案 3 :(得分: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的字符的子集时,该测试才会成立。位集的“或”运算相当于实数集的并集(∪)运算符。 / p>

当且仅当A∪B= B时,请参阅set mathemtatics上的维基百科条目 - A⊆B。

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

答案 4 :(得分:2)

我必须同意Stephen C的观点 - 这还不足以定义回答

我不打算投票,但你可以解释一下,例如,租金是否相当于特权?你有答案的人会认为它是(人们认为发生次数无关紧要,而其他答案者则认为最糟糕。其中一个团队正在浪费他们的时间。

此外,由于您关注的是性能问题,因此我们需要了解有关您的通话模式的更多信息。你能解释一下你是否会不止一次看一对套装,或者套装是否有所不同?

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

你说租金与ternicate匹配,但很明显,ternicate与租金不匹配。 所以你并不是真的在寻找等价物。你正在寻找类似“被发现”或“可以来自”的东西。

这意味着你必须关心订单 - 根据你如何访问你的套装,你会得到不同的结果。

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

答案 5 :(得分:1)

也许不是紧固件,但可能是使用java + google-collections + guava的最短解决方案(用于投射char[] - &gt; List<Character>

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)

假设:

  1. 你的单词只包含ascii字符
  2. 案件无关紧要
  3. abc匹配abcde,abcde与abc
  4. 不匹配

    你可以通过匹配字符串(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之间的非对称函数。

答案 7 :(得分:1)

我做了很多与文字游戏和字谜相关的代码。通常的方法是将单词转换为排序键,这样,如上所述,'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 :(得分:0)

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

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

模糊的警告: 1.如果多个字母可以,例如rent == rreenntt,那么在向数组添加字母时,检查密钥是否存在,如果存在,则不要再添加。
2.如果额外的字母是可以的,例如rent == renter,但是fernt!= renter,那么在最后检查数组值时,检查1和-1是否同时不在数组中。换句话说,1和0只是正常,或-1和0都可以,但不是1和-1不能同时在数组中。

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

答案 9 :(得分:0)

我认为你应该建一棵树。 我已经写了一些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

答案 10 :(得分:0)

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

答案 11 :(得分:0)

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