如何在给定的文本中找出给定单词的所有排列组合?

10

这是一道面试问题(电话面试):编写一个函数(使用Java语言),在给定的文本中查找给定单词的所有排列。例如,对于单词abc和文本abcxyaxbcayxycab,该函数应返回abc,bca,cab

我的回答如下:

  • 显然,我可以循环遍历给定单词的所有排列并使用标准的substring函数。然而,编写代码以生成所有单词排列可能有些困难(目前对我来说是这样的)。

  • 更容易的方法是循环遍历单词大小的所有文本子字符串,对每个子字符串进行排序,并将其与“排序后”的给定单词进行比较。我可以立即编写这样的函数。

  • 我可能可以修改一些子字符串搜索算法,但我现在不记得这些算法。

你会如何回答这个问题?

6个回答

12
这可能不是最高效的算法解决方案,但从类设计角度来看,它是干净的。该解决方案采用比较“排序”给定单词的方法。
我们可以说一个单词是另一个单词的排列,如果它包含相同数量的相同字母。这意味着您可以将单词从String转换为Map。这种转换的复杂度为O(n),其中n是String的长度,假设在您的Map实现中插入成本为O(1)。
地图将包含作为键的单词中找到的所有字符,并且作为字符频率的值。
示例。 abbc转换为[a->1,b->2,c->1]
bacb转换为[a->1,b->2,c->1]
因此,如果您必须知道两个单词是否是彼此的排列,您可以将它们都转换为映射,然后调用Map.equals。
然后,您必须遍历文本字符串并将变换应用于要查找的单词的相同长度的所有子字符串。
Inerdial提出的改进
通过“滚动”方式更新地图可以改进此方法。
即。如果您在OP中的示例干草堆中匹配索引i = 3(子字符串xya),则地图将为[a->1,x->1,y->1]。在干草堆中前进时,将减少haystack [i]的字符计数,并增加haystack [i + needle.length()]的计数。
(删除零以确保Map.equals()起作用,或者只是实现自定义比较。)
Max提出的改进
如果我们还引入matchedCharactersCnt变量怎么办?在干草堆的开头,它将为0。每次您将地图更改为所需值时-您会增加该变量。每次您将其从所需值更改时-您会将该变量减少。每次迭代时,检查变量是否等于needle的长度。如果是-您已找到匹配项。它比每次比较完整的地图要快。
Max提供的伪代码:
needle = "abbc"
text = "abbcbbabbcaabbca"

needleSize = needle.length()
//Map of needle character counts
targetMap = [a->1, b->2, c->1]

matchedLength = 0
curMap = [a->0, b->0, c->0]
//Initial map initialization
for (int i=0;i<needle.length();i++) {
    if (curMap.contains(haystack[i])) {
        matchedLength++
        curMap[haystack[i]]++
    }
}

if (matchedLength == needleSize) {
    System.out.println("Match found at: 0");
}

//Search itself
for (int i=0;i<haystack.length()-needle.length();i++) {
    int targetValue1 = targetMap[haystack[i]]; //Reading from hashmap, O(1)
    int curValue1 = curMap[haystack[i]]; //Another read
    //If we are removing beneficial character
    if (targetValue1 > 0 && curValue1 > 0 && curValue1 <= targetValue1) {       
        matchedLength--;
    }
    curMap[haystack[i]] = curValue1 + 1; //Write to hashmap, O(1)


    int targetValue2 = targetMap[haystack[i+needle.length()]] //Read
    int curValue2 = curMap[haystack[i+needle.length()]] //Read
    //We are adding a beneficial character
    if (targetValue2 > 0 && curValue2 < targetValue2) { //If we don't need this letter at all, the amount of matched letters decreases
        matchedLength++;
    }
    curMap[haystack[i+needle.length()]] = curValue2 + 1; //Write

    if (matchedLength == needleSize) {
        System.out.println("Match found at: "+(i+1));
    }
}

//Basically with 4 reads and 2 writes which are 
//independent of the size of the needle,
//we get to the maximal possible performance: O(n)

这个回答似乎不完整。您提到了如何将单词规范化,但没有讲如何在文本中寻找排列组合。您是否会使用与帖子中第二个人相同的思路? - Colin D
1
当与OP的第二个想法相结合时,可以通过“滚动”方式更新Map来改进此方法。即,如果在OP示例中的索引i=3(子字符串xya)处匹配,则映射将为[a->1, x->1, y->1]。在草堆中前进时,减少haystack[i]的字符计数,并增加haystack[i+needle.length()]的计数。(删除零以确保Map.equals()有效,或者只是实现自定义比较。) - millimoose
@Inerdial,你的改进真的很优雅!恭喜! - Vitaly Olegovitch
@ColinD 你是对的。我已经更新了我的答案。如果你认为它仍然不完整,请随意编辑它。 :) - Vitaly Olegovitch
3
如果我们还引入一个名为matchedCharactersCnt的变量会怎样呢?在haystack字符串的开头,将其设为0。每次你将映射“向着”目标值进行更改时 - 增加该变量的值。每次你将映射“远离”目标值进行更改时 - 减少该变量的值。每次迭代时,检查变量是否等于needle字符串的长度。如果是,则找到了匹配项。这比每次完全比较映射要快。 - bezmax
显示剩余7条评论

5
要找到一个字符串的排列,可以使用数论。 但是在使用此算法回答问题之前,您必须事先了解该算法背后的“理论”。
有一种方法可以使用质数计算字符串的哈希值。 相同字符串的每个排列都将给出相同的哈希值。所有其他不是排列的字符串组合将给出另一些哈希值。
哈希值由c1 * p1 + c2 * p2 + ... + cn * pn计算 其中ci是字符串中当前字符的唯一值,pi是ci字符的唯一质数值。
以下是实现内容。
public class Main {
    static int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 
        19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 
        73, 79, 83, 89, 97, 101, 103 };

    public static void main(String[] args) {        
        final char[] text = "abcxaaabbbccyaxbcayaaaxycab"
            .toCharArray();     
        char[] abc = new char[]{'a','b','c'};       
        int match = val(abc);                   
        for (int i = 0; i < text.length - 2; i++) {
            char[] _123 = new char[]{text[i],text[i+1],text[i+2]};          
            if(val(_123)==match){
                System.out.println(new String(_123) );      
            }
        }
    }   
    static int p(char c) {
        return primes[(int)c - (int)'a'];
    }   
    static int val(char[] cs) {
        return 
        p(cs[0])*(int)cs[0] + p(cs[1])*(int)cs[1] + p(cs[2])*(int)cs[2];        
    }
}

这段代码的输出结果为: abc bca cab

这不是正确的方法。计算这样的哈希的正确方式是使用 p(cs[0]) * p(cs[1]) * p(cs[2])。对于小字符串来说没问题,但对于大字符串,你将不得不使用 BigInteger 来进行哈希计算,因此它的适用性有限。 - vladich

3

您应该能够在一次遍历中完成此操作。首先建立一个包含要搜索的单词中所有字符的映射。因此,最初的映射包含[a, b, c]

现在,逐个字符地查看文本内容。循环在伪代码中如下所示。

found_string = "";
for each character in text
    if character is in map
        remove character from map
        append character to found_string
        if map is empty
            output found_string
            found_string = ""
            add all characters back to map
        end if
    else
        // not a permutation of the string you're searching for
        refresh map with characters from found_string
        found_string = ""
    end if
end for

如果你想要唯一的出现次数,改变输出步骤,使其将找到的字符串添加到一个映射中。这样就可以消除重复项。
存在包含重复字母的单词问题。如果这是一个问题,可以将键设置为字母,值设置为计数。'删除'一个字符意味着在映射中递减其计数。如果计数变为0,则该字符实际上已从映射中删除。
所编写的算法无法找到重叠的出现次数。也就是说,在给定文本abcba的情况下,它只会找到abc。如果您想处理重叠的出现次数,可以修改算法,使其在找到匹配项时将索引减去找到的字符串长度减1。
那是一个有趣的谜题。谢谢。

同意:一个有趣的拼图。我只是注意到我的答案中的代码受到了重复字母问题的影响。我会编辑我的代码,受到你的答案的启发。 - Andrea Parodi

1

这段代码应该可以完成工作:

import java.util.ArrayList;
import java.util.List;

public class Permutations {
    public static void main(String[] args) {
        final String word = "abc";
        final String text = "abcxaaabbbccyaxbcayxycab";
        List<Character> charsActuallyFound = new ArrayList<Character>();
        StringBuilder match = new StringBuilder(3);

        for (Character c : text.toCharArray()) {
            if (word.contains(c.toString()) && !charsActuallyFound.contains(c)) {
                charsActuallyFound.add(c);
                match.append(c);
                if (match.length()==word.length())
                {
                    System.out.println(match);
                    match = new StringBuilder(3);
                    charsActuallyFound.clear();
                }
            } else {
                match = new StringBuilder(3);
                charsActuallyFound.clear();
            }
        }
    }
}

charsActuallyFound列表用于跟踪循环中已经找到的字符。这是为了避免匹配“aaa”“bbb”“ccc”(由我添加到您指定的文本中)。

经过进一步思考,我认为我的代码仅在给定单词没有重复字符的情况下才能正常工作。 上面的代码正确打印

abc
bca
cab

但是如果你搜索单词“aaa”,则不会打印任何内容,因为每个字符不能匹配超过一次。受Jim Mischel答案的启发,我编辑了我的代码,最终得到了这个:

import java.util.ArrayList;
import java.util.List;

public class Permutations {
    public static void main(String[] args) {
        final String text = "abcxaaabbbccyaxbcayaaaxycab";

        printMatches("aaa", text);
        printMatches("abc", text);
    }

    private static void printMatches(String word, String text) {
        System.out.println("matches for "+word +" in "+text+":");

        StringBuilder match = new StringBuilder(3);
        StringBuilder notYetFounds=new StringBuilder(word);

        for (Character c : text.toCharArray()) {
            int idx = notYetFounds.indexOf(c.toString());
            if (idx!=-1) {
               notYetFounds.replace(idx,idx+1,"");

                match.append(c);
                if (match.length()==word.length())
                {
                    System.out.println(match);
                    match = new StringBuilder(3);
                    notYetFounds=new StringBuilder(word);
                }
            } else {
                match = new StringBuilder(3);
                notYetFounds=new StringBuilder(word);
            }
        }
        System.out.println();
    }

}

这会给我以下输出:

matches for aaa in abcxaaabbbccyaxbcayaaaxycab:
aaa
aaa

matches for abc in abcxaaabbbccyaxbcayaaaxycab:
abc
bca
cab

进行了一些基准测试,上面的代码在一个随机字符串的 36M 中仅用了 4.5 秒就找到了 30815 个 "abc" 的匹配项。正如 Jim 已经说过的那样,感谢这个谜题...


使用Map可以获得更高的性能。你代码中的这部分 notYetFounds.indexOf(c.toString()); 使整个算法的复杂度达到了惊人的 O(needle.length()*haystack.length()),因为在你的情况下,indexOf 的复杂度基本上是 O(needle.length())。然而,读/写Map的复杂度是 O(1)。因此,使用Map的算法结果的复杂度为 O(haystack.length()),速度快了一个数量级(特别是对于巨大的needle)。 - bezmax
你的算法无法处理重叠的针头,我也看不到修改它以处理它们的方法。例如:haystack="abcba"needle="abc" 会产生两个匹配项:[abc]baab[cba],而你的算法只产生第一个匹配项(然后重置缓冲区)。 - bezmax
是的,我看到了。如果 needle 增加长度,代码将开始变慢。我会尝试使用 maps 编写另一个版本。哦,对了,我的代码不匹配重叠的单词... - Andrea Parodi
Max:我进行了其他测试。我的代码在needle.length()上的性能不是线性的。当使用3个字符的needle时,它需要453毫秒,而当使用405个字符的needle时,它需要2515毫秒。当needle.legth > ~ 140时,使用频率映射比使用StringBuilder更快。我将编辑我的答案以包括映射版本的源代码。 - Andrea Parodi
嗯,我完全错了:使用频率映射的代码始终比使用字符串构建器的代码快。两个版本的性能都受到事实的影响,即我为每个字符重新构建构建器(或映射),即使它们没有被修改!更改这些使两个版本更快,而频率映射代码现在在搜索单词的每个长度方面表现更好。 - Andrea Parodi

1
这是我会做的 - 设置一个标志数组,其中一个元素等于0或1,以指示STR中的该字符是否已匹配。
将第一个结果字符串RESULT设置为空。
对于TEXT中的每个字符C:
将数组X设置为STR的长度,全部为零。
对于STR中的每个字符S: 如果C是STR中的第J个字符,并且X[J] == 0,则将X[J] <= 1,并将C添加到RESULT中。 如果RESULT的长度等于STR,则将RESULT添加到排列列表中,并将X[]的元素再次设置为零。
如果C不是具有X[J]==0的STR中任何字符J,则将X[]的元素再次设置为零。

1

第二种方法对我来说非常优雅,应该是完全可接受的。我认为它的规模为O(M * N log N),其中N是单词长度,M是文本长度。

我可以想出一个稍微复杂一些的O(M)算法:

  1. 计算单词中每个字符的出现次数
  2. 对文本的前N个字符(即length(word))执行相同的操作
  3. 将两个频率向量相减,得到subFreq
  4. 计算subFreq中非零元素的数量,得到numDiff
  5. 如果numDiff等于零,则表示匹配成功
  6. 通过更新文本中第一个和最后一个字符的方式,在常数时间内更新subFreqnumDiff
  7. 重复步骤5直到达到文本末尾

编辑:看到已经发布了几个类似的答案。这个算法大部分与其他人建议的滚动频率计数等价。我谦虚地补充了一个滚动方式更新差异数量的方法,从而得到一个O(M+N)算法,而不是O(M*N)

编辑2:刚刚看到Max在评论中基本上也提出了这个建议,所以给他加分。


我不确定为什么你的算法是O(M+N),你是否假设读写Map的时间复杂度是O(N)?我认为你的算法是O(M)。对于适合此任务的正确Map实现,读写应该是O(1) - bezmax
@Max,实际上是O(M),因为N < M。我相信你认为N是文本大小 :) 我也是这么想的。 - Chip
@Chip 是的,抱歉,我以为N是干草堆的长度。我已经修复了我的评论。 - bezmax
@smocking 是的,但由于 O(M+N) 最大可以是 O(2M),因此我们可以简化为 O(M)。 - Chip
@smocking 噢,那好吧。这是该算法的伪代码:http://pastebin.com/H4GLY0zb - bezmax
显示剩余4条评论

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