模式匹配面试Q.

时间:2015-02-13 20:39:00

标签: java algorithm

我最近在接受采访时问他以下问题:

  

如果字符串与模式匹配,则写一个函数返回true,false   否则

模式:每个项目1个字符,(a-z),输入:空格分隔字符串

这是我解决第一个问题的方法:

static boolean isMatch(String pattern, String input) {
    char[] letters = pattern.toCharArray();
    String[] split = input.split("\\s+");

    if (letters.length != split.length) {
        // early return - not possible to match if lengths aren't equal
        return false;
    }

    Map<String, Character> map = new HashMap<>();
    // aaaa test test test1 test1
    boolean used[] = new boolean[26];
    for (int i = 0; i < letters.length; i++) {
        Character existing = map.get(split[i]);
        if (existing == null) {
            // put into map if not found yet
            if (used[(int)(letters[i] - 'a')]) {
                return false;
            }

            used[(int)(letters[i] - 'a')] = true;
            map.put(split[i], letters[i]);
        } else {
            // doesn't match - return false
            if (existing != letters[i]) {
                return false;
            }
        }
    }

    return true;
}

public static void main(String[] argv) {
    System.out.println(isMatch("aba", "blue green blue"));
    System.out.println(isMatch("aba", "blue green green"));
}

问题的下一部分让我感到难过:

  

如果输入中没有分隔符,请编写相同的函数。

例如:

isMatch("aba", "bluegreenblue") -> true
isMatch("abc","bluegreenyellow") -> true
isMatch("aba", "t1t2t1") -> true
isMatch("aba", "t1t1t1") -> false
isMatch("aba", "t1t11t1") -> true
isMatch("abab", "t1t2t1t2") -> true
isMatch("abcdefg", "ieqfkvu") -> true
isMatch("abcdefg", "bluegreenredyellowpurplesilvergold") -> true
isMatch("ababac", "bluegreenbluegreenbluewhite") -> true
isMatch("abdefghijklmnopqrstuvwxyz", "zyxwvutsrqponmlkjihgfedcba") -> true

我编写了一个强力解决方案(生成大小为letters.length的输入字符串的所有可能拆分,并依次针对isMatch进行检查)但是面试官说它不是最优的。

我不知道如何解决这部分问题,这是可能的,还是我错过了什么?

他们正在寻找时间复杂度为O(M x N ^ C)的东西,其中M是模式的长度,N是输入的长度,C是一些常数。

澄清

  • 即使有效,我也不会寻找正则表达式解决方案。
  • 我没有寻找产生所有可能的分裂并检查它们的天真解决方案,即使是优化,因为它总是指数时间。

5 个答案:

答案 0 :(得分:10)

可以优化回溯解决方案。我们可以“在飞行中”检查它,而不是先生成所有拆分,然后检查它是否是有效的。假设我们已经分割了初始字符串的前缀(长度为p)并且从模式中匹配了i个字符。我们来看看i + 1字符。

  1. 如果前缀中有一个与i + 1字母对应的字符串,我们应该检查从位置p + 1开始的子字符串是否等于它。如果是,我们只需继续i + 1p + the length of this string。否则,我们可以杀死这个分支。

  2. 如果没有这样的字符串,我们应该尝试从位置p + 1开始并在其后的某个位置结束的所有子字符串。

  3. 我们还可以使用以下想法来减少解决方案中的分支数量:我们可以估计尚未处理的模式后缀的长度(我们知道已经代表某些字母的长度)字符串,我们知道模式中任何字母的字符串长度的平凡下限(它是1))。如果初始字符串的剩余部分太短而不匹配模式的其余部分,它允许我们终止分支。

    此解决方案仍然具有指数时间复杂度,但它可以比生成所有拆分更快地工作,因为无效解决方案可以更早地丢弃,因此可达状态的数量可以显着减少。

答案 1 :(得分:3)

我觉得这是作弊,我不相信捕获组和不情愿的量词会做正确的事情。或者也许他们正在寻找你是否能够认识到这一点,因为量词的工作方式,匹配是模棱两可的。

boolean matches(String s, String pattern) {
    StringBuilder patternBuilder = new StringBuilder();
    Map<Character, Integer> backreferences = new HashMap<>();
    int nextBackreference = 1;

    for (int i = 0; i < pattern.length(); i++) {
        char c = pattern.charAt(i);

        if (!backreferences.containsKey(c)) {
            backreferences.put(c, nextBackreference++);
            patternBuilder.append("(.*?)");
        } else {
            patternBuilder.append('\\').append(backreferences.get(c));
        }
    }

    return s.matches(patternBuilder.toString());
}

答案 2 :(得分:2)

更新: 这是我的解决方案。基于我之前的解释。

import com.google.common.collect.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.math3.util.Combinations;

import java.util.*;

/**
 * Created by carlos on 2/14/15.
 */
public class PatternMatcher {

    public static boolean isMatch(char[] pattern, String searchString){
        return isMatch(pattern, searchString, new TreeMap<Integer, Pair<Integer, Integer>>(), Sets.newHashSet());
    }
    private static boolean isMatch(char[] pattern, String searchString, Map<Integer, Pair<Integer, Integer>> candidateSolution, Set<String> mappedStrings) {
        List<Integer> occurrencesOfCharacterInPattern = getNextUnmappedPatternOccurrences(candidateSolution, pattern);
        if(occurrencesOfCharacterInPattern.size() == 0)
            return isValidSolution(candidateSolution, searchString, pattern, mappedStrings);
        List<Pair<Integer, Integer>> sectionsOfUnmappedStrings = sectionsOfUnmappedStrings(searchString, candidateSolution);
        if(sectionsOfUnmappedStrings.size() == 0)
            return false;
        String firstUnmappedString = substring(searchString, sectionsOfUnmappedStrings.get(0));


        for (int substringSize = 1; substringSize <= firstUnmappedString.length(); substringSize++) {
            String candidateSubstring = firstUnmappedString.substring(0, substringSize);
            if(mappedStrings.contains(candidateSubstring))
                continue;
            List<Pair<Integer, Integer>> listOfAllOccurrencesOfSubstringInString = Lists.newArrayList();
            for (int currentIndex = 0; currentIndex < sectionsOfUnmappedStrings.size(); currentIndex++) {
                Pair<Integer,Integer> currentUnmappedSection = sectionsOfUnmappedStrings.get(currentIndex);
                List<Pair<Integer, Integer>> occurrencesOfSubstringInString =
                        findAllInstancesOfSubstringInString(searchString, candidateSubstring,
                                currentUnmappedSection);
                for(Pair<Integer,Integer> possibleAddition:occurrencesOfSubstringInString) {
                    listOfAllOccurrencesOfSubstringInString.add(possibleAddition);
                }
            }

            if(listOfAllOccurrencesOfSubstringInString.size() < occurrencesOfCharacterInPattern.size())
                return false;

            Iterator<int []> possibleSolutionIterator =
                    new Combinations(listOfAllOccurrencesOfSubstringInString.size(),
                            occurrencesOfCharacterInPattern.size()).iterator();
            iteratorLoop:
            while(possibleSolutionIterator.hasNext()) {
                Set<String> newMappedSets = Sets.newHashSet(mappedStrings);
                newMappedSets.add(candidateSubstring);
                TreeMap<Integer,Pair<Integer,Integer>> newCandidateSolution = Maps.newTreeMap();
                // why doesn't Maps.newTreeMap(candidateSolution) work?
                newCandidateSolution.putAll(candidateSolution);

                int [] possibleSolutionIndexSet = possibleSolutionIterator.next();

                for(int i = 0; i < possibleSolutionIndexSet.length; i++) {
                    Pair<Integer, Integer> candidatePair = listOfAllOccurrencesOfSubstringInString.get(possibleSolutionIndexSet[i]);
                    //if(candidateSolution.containsValue(Pair.of(0,1)) && candidateSolution.containsValue(Pair.of(9,10)) && candidateSolution.containsValue(Pair.of(18,19)) && listOfAllOccurrencesOfSubstringInString.size() == 3 && candidateSolution.size() == 3 && possibleSolutionIndexSet[0]==0 && possibleSolutionIndexSet[1] == 2){
                    if (makesSenseToInsert(newCandidateSolution, occurrencesOfCharacterInPattern.get(i), candidatePair))
                        newCandidateSolution.put(occurrencesOfCharacterInPattern.get(i), candidatePair);
                    else
                        break iteratorLoop;
                }

                if (isMatch(pattern, searchString, newCandidateSolution,newMappedSets))
                    return true;
            }

        }
        return false;
    }

    private static boolean makesSenseToInsert(TreeMap<Integer, Pair<Integer, Integer>> newCandidateSolution, Integer startIndex, Pair<Integer, Integer> candidatePair) {
        if(newCandidateSolution.size() == 0)
            return true;

        if(newCandidateSolution.floorEntry(startIndex).getValue().getRight() > candidatePair.getLeft())
            return false;

        Map.Entry<Integer, Pair<Integer, Integer>> ceilingEntry = newCandidateSolution.ceilingEntry(startIndex);
        if(ceilingEntry !=null)
            if(ceilingEntry.getValue().getLeft() < candidatePair.getRight())
                return false;

        return true;
    }

    private static boolean isValidSolution( Map<Integer, Pair<Integer, Integer>> candidateSolution,String searchString, char [] pattern, Set<String> mappedStrings){
        List<Pair<Integer,Integer>> values = Lists.newArrayList(candidateSolution.values());
        return  areIntegersConsecutive(Lists.newArrayList(candidateSolution.keySet())) &&
                arePairsConsecutive(values) &&
                values.get(values.size() - 1).getRight() == searchString.length() &&
                patternsAreUnique(pattern,mappedStrings);
    }

    private static boolean patternsAreUnique(char[] pattern, Set<String> mappedStrings) {
        Set<Character> uniquePatterns = Sets.newHashSet();
        for(Character character:pattern)
            uniquePatterns.add(character);

        return uniquePatterns.size() == mappedStrings.size();
    }

    private static List<Integer> getNextUnmappedPatternOccurrences(Map<Integer, Pair<Integer, Integer>> candidateSolution, char[] searchArray){
        List<Integer> allMappedIndexes = Lists.newLinkedList(candidateSolution.keySet());
        if(allMappedIndexes.size() == 0){
            return occurrencesOfCharacterInArray(searchArray,searchArray[0]);
        }
        if(allMappedIndexes.size() == searchArray.length){
            return Lists.newArrayList();
        }
        for(int i = 0; i < allMappedIndexes.size()-1; i++){
            if(!areIntegersConsecutive(allMappedIndexes.get(i),allMappedIndexes.get(i+1))){
                return occurrencesOfCharacterInArray(searchArray,searchArray[i+1]);
            }
        }
        List<Integer> listOfNextUnmappedPattern = Lists.newArrayList();
        listOfNextUnmappedPattern.add(allMappedIndexes.size());
        return listOfNextUnmappedPattern;
    }

    private static String substring(String string, Pair<Integer,Integer> bounds){
        try{
            string.substring(bounds.getLeft(),bounds.getRight());
        }catch (StringIndexOutOfBoundsException e){
            System.out.println();
        }
        return string.substring(bounds.getLeft(),bounds.getRight());
    }

    private static List<Pair<Integer, Integer>> sectionsOfUnmappedStrings(String searchString, Map<Integer, Pair<Integer, Integer>> candidateSolution) {
        if(candidateSolution.size() == 0) {
            return Lists.newArrayList(Pair.of(0, searchString.length()));
        }
        List<Pair<Integer, Integer>> sectionsOfUnmappedStrings = Lists.newArrayList();
        List<Pair<Integer,Integer>> allMappedPairs = Lists.newLinkedList(candidateSolution.values());

        // Dont have to worry about the first index being mapped because of the way the first candidate solution is made
        for(int i = 0; i < allMappedPairs.size() - 1; i++){
            if(!arePairsConsecutive(allMappedPairs.get(i), allMappedPairs.get(i + 1))){
                Pair<Integer,Integer> candidatePair = Pair.of(allMappedPairs.get(i).getRight(), allMappedPairs.get(i + 1).getLeft());
                sectionsOfUnmappedStrings.add(candidatePair);
            }
        }

        Pair<Integer,Integer> lastMappedPair = allMappedPairs.get(allMappedPairs.size() - 1);
        if(lastMappedPair.getRight() != searchString.length()){
            sectionsOfUnmappedStrings.add(Pair.of(lastMappedPair.getRight(),searchString.length()));
        }

        return sectionsOfUnmappedStrings;
    }

    public static boolean areIntegersConsecutive(List<Integer> integers){
        for(int i = 0; i < integers.size() - 1; i++)
            if(!areIntegersConsecutive(integers.get(i),integers.get(i+1)))
                return false;
        return true;
    }

    public static boolean areIntegersConsecutive(int left, int right){
        return left == (right - 1);
    }

    public static boolean arePairsConsecutive(List<Pair<Integer,Integer>> pairs){
        for(int i = 0; i < pairs.size() - 1; i++)
            if(!arePairsConsecutive(pairs.get(i), pairs.get(i + 1)))
                return false;
        return true;
    }


    public static boolean arePairsConsecutive(Pair<Integer, Integer> left, Pair<Integer, Integer> right){
        return left.getRight() == right.getLeft();
    }

    public static List<Integer> occurrencesOfCharacterInArray(char[] searchArray, char searchCharacter){
        assert(searchArray.length>0);

        List<Integer> occurrences = Lists.newLinkedList();
        for(int i = 0;i<searchArray.length;i++){
            if(searchArray[i] == searchCharacter)
                occurrences.add(i);
        }
        return occurrences;
    }

    public static List<Pair<Integer,Integer>> findAllInstancesOfSubstringInString(String searchString, String substring, Pair<Integer,Integer> bounds){
        String string = substring(searchString,bounds);
        assert(StringUtils.isNoneBlank(substring,string));

        int lastIndex = 0;
        List<Pair<Integer,Integer>> listOfOccurrences = Lists.newLinkedList();
        while(lastIndex != -1){
            lastIndex = string.indexOf(substring,lastIndex);
            if(lastIndex != -1){
                int newIndex = lastIndex + substring.length();
                listOfOccurrences.add(Pair.of(lastIndex + bounds.getLeft(), newIndex + bounds.getLeft()));
                lastIndex = newIndex;
            }
        }
        return listOfOccurrences;
    }
}

它适用于所提供的案例,但未经过全面测试。如果有任何错误,请告诉我。

原始回复:

假设你正在搜索的字符串可以有任意长度的标记(你的一些例子都有),那么:

您想开始尝试将字符串分解为与模式匹配的部分。寻找减少搜索树的方式的矛盾。

当你开始处理时,你将要选择字符串开头的N个字符。现在,去看看你是否可以在字符串的其余部分找到子字符串。如果你不能那么它就不可能是一个解决方案。如果可以,那么你的字符串就像这样

(N个字符)&lt; ...&gt; [(N个字符)&lt; ...&gt;]其中一个&lt; ...&gt;包含0+个字符,不一定是相同的子字符串。 []中的内容可能会重复多次,等于字符串中出现的次数(N个字符)。

现在,你的模式的第一个字母是匹配的,你不确定模式的其余部分是否匹配,但你基本上可以重复使用这个算法(带有修改)来询问&lt; ...&gt;字符串的一部分。

你会这样做N = 1,2,3,4 ...... 有意义吗?

我将做一个例子(它不包括所有情况,但希望说明)注意,当我指的是模式中的子串时,我将使用单引号,当我指的是子串的时候字符串我将使用双引号。

isMatch(“ababac”,“bluegreenbluegreenbluewhite”)

好的,'a'是我的第一个模式。 对于N = 1我得到字符串“b” 搜索字符串中的“b”在哪里? 的 B'/强> luegreen的 B'/强> luegreen的 B'/强> luewhite。

好的,所以此时此字符串可能与“b”匹配为“a”模式。让我们看看我们是否可以对模式'b'做同样的事情。从逻辑上讲,'b'必须是整个字符串“luegreen”(因为它在两个连续的'a'模式之间挤压)然后我在第2和第3'a'之间检查。 YUP,它的“luegreen”。

好的,到目前为止,除了我的模式的'c'之外,我已经匹配了。简单的情况下,其余的字符串。它匹配。

这基本上是编写Perl正则表达式解析器。 ababc =(。+)(。+)(\ 1)(\ 2)(。+)。所以你只需要将它转换为Perl正则表达式

答案 3 :(得分:2)

您可以通过首先假设令牌长度并检查令牌长度的总和等于测试字符串的长度来改善暴力。这比每次模式匹配更快。然而,随着独特令牌数量的增加,这仍然非常缓慢。

答案 4 :(得分:2)

以下是我的代码的示例代码段:

public static final boolean isMatch(String patternStr, String input) {
    // Initial Check (If all the characters in the pattern string are unique, degenerate case -> immediately return true)
    char[] patt = patternStr.toCharArray();
    Arrays.sort(patt);
    boolean uniqueCase = true;
    for (int i = 1; i < patt.length; i++) {
        if (patt[i] == patt[i - 1]) {
            uniqueCase = false;
            break;
        }
    }
    if (uniqueCase) {
        return true;
    }
    String t1 = patternStr;
    String t2 = input;
    if (patternStr.length() == 0 && input.length() == 0) {
        return true;
    } else if (patternStr.length() != 0 && input.length() == 0) {
        return false;
    } else if (patternStr.length() == 0 && input.length() != 0) {
        return false;
    }
    int count = 0;
    StringBuffer sb = new StringBuffer();
    char[] chars = input.toCharArray();
    String match = "";
    // first read for the first character pattern
    for (int i = 0; i < chars.length; i++) {
        sb.append(chars[i]);
        count++;
        if (!input.substring(count, input.length()).contains(sb.toString())) {
            match = sb.delete(sb.length() - 1, sb.length()).toString();
            break;
        }
    }
    if (match.length() == 0) {
        match = t2;
    }
    // based on that character, update patternStr and input string
    t1 = t1.replace(String.valueOf(t1.charAt(0)), "");
    t2 = t2.replace(match, "");
    return isMatch(t1, t2);
}

我基本上决定首先解析模式字符串并确定模式字符串中是否存在任何匹配的字符。例如,在“aab”中,“a”在模式字符串中使用两次,因此“a”无法映射到其他内容。否则,如果字符串中没有匹配的字符,例如“abc”,则输入字符串是什么并不重要,因为模式是唯一的,因此每个模式字符匹配的内容并不重要(退化情况)。

如果模式字符串中有匹配的字符,那么我将开始检查每个字符串匹配的内容。不幸的是,在不知道分隔符的情况下,我不知道每个字符串会有多长。相反,我只是决定一次解析1个字符,并检查字符串的其他部分是否包含相同的字符串,并继续逐字母地添加字符到缓冲区,直到在输入字符串中找不到缓冲区字符串。一旦我确定了字符串,它现在在缓冲区中我只是删除输入字符串中的所有匹配字符串,然后从模式字符串中删除字符模式然后递归。

如果我的解释不是很清楚,我很抱歉,我希望我的代码可以清楚。