如何查找给定文本中给定单词的所有排列?

时间:2012-05-23 20:11:14

标签: java string algorithm

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

我会回答这个问题如下:

  • 显然,我可以遍历给定单词的所有排列并使用标准substring函数。但是(现在对我来说)编写代码以生成所有单词排列可能很困难。

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

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

你会如何回答这个问题?

6 个答案:

答案 0 :(得分:12)

这可能不是算法上最有效的解决方案,但从类设计的角度来看它是干净的。该解决方案采用比较“已排序”给定单词的方法。

如果一个单词包含相同数字的相同字母,我们可以说单词是另一个单词的排列。这意味着您可以将单词从String转换为Map<Character,Integer>。这种转换将具有复杂度O(n),其中n是String的长度,假设Map实现中的插入花费为O(1)。

Map将包含在单词中找到的所有字符作为键,并将字符的频率作为值。

示例即可。 abbc转换为[a->1, b->2, c->1]

bacb转换为[a->1, b->2, c->1]

因此,如果您必须知道两个单词是否是另一个单词的排列,您可以将它们转换为映射,然后调用Map.equals

然后你必须迭代文本字符串并将转换应用于你正在寻找的相同长度的所有子串。

Inerdial提出的改进

通过以“滚动”的方式更新Map,可以改进这种方法。

即。如果您在OP中的示例haystack(子字符串i=3)中的索引xya处进行匹配,则映射将为[a->1, x->1, y->1]。在干草堆中前进时,减少haystack[i]的字符数,并增加haystack[i+needle.length()]的计数。

(删除零以确保Map.equals()有效,或者仅执行自定义比较。)

Max提出的改进

如果我们还引入matchedCharactersCnt变量怎么办?在干草堆的开头,它将是0。每次将地图更改为所需的值时 - 都会增加变量。每次将其从期望值更改时 - 您将减少变量。每次迭代检查变量是否等于针的长度。如果是 - 你找到了匹配。它比每次比较完整的地图要快。

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)

答案 1 :(得分:5)

要查找字符串的排列,可以使用数论。 但在使用此算法回答问题之前,您必须事先知道此算法背后的“理论”。

有一种方法可以使用素数计算字符串的哈希值。 相同字符串的每个排列都将提供相同的哈希值。所有其他不是排列的字符串组合将给出一些其他哈希值。

哈希值由c 1 * p 1 + c 2 * p 2 +计算... + c n * p n 其中c i 是字符串中当前char的唯一值,其中p i 是c i char的唯一素数值

这是实施。

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 驾驶室

答案 2 :(得分: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,减去找到的字符串的长度。

这是一个有趣的谜题。感谢。

答案 3 :(得分:1)

这就是我要做的 - 用一个设置一个标志数组 元素等于0或1表示是否该字符 在STR中已匹配

将第一个结果字符串RESULT设置为空。

对于TEXT中的每个字符C:

将数组X等于STR的长度设为全零。

STR中每个字符S的

:        如果C是STR中的JTH字符,和        X [J] == 0,然后设置X [J]&lt; = 1并添加        C到结果。        如果RESULT的长度等于STR,        将RESULT添加到排列列表中        并将X []的元素重新设置为零。

如果C中的任何字符J都不是X [J] == 0,    然后再将X []的元素设置为零。

答案 4 :(得分:1)

第二种方法对我来说似乎非常优雅,应该是完全可以接受的。我认为它会缩放到O(M * N log N),其中N是字长,M是文字长度。

我可以提出一个更复杂的O(M)算法:

  1. 计算单词
  2. 中每个字符的出现次数
  3. 对文本
  4. 的前N个(即length(word))字符执行相同操作
  5. 减去两个频率向量,产生subFreq
  6. 计算subFreq中的非零数,产生numDiff
  7. 如果numDiff等于零,则匹配
  8. 通过更新文本中的第一个和后一个字符,在固定时间内更新subFreqnumDiff
  9. 转到5直到文本末尾
  10. 编辑:看到几个类似的答案已经发布。大多数此算法相当于其他人建议的滚动频率计数。我的谦虚添加也是以滚动方式更新差异的数量,产生O(M+N)算法而不是O(M*N)算法。

    EDIT2 :刚看到Max在评论中基本上已经提出这个建议,所以布朗尼指向他。

答案 5 :(得分: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 List用于跟踪已在循环中找到的字符。需要避免使用“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

做了一些基准测试,上面的代码在短短的4.5秒内在36M的随机字符串中找到了30815匹配的“abc”。正如吉姆已经说过的,感谢这个难题...