【问题标题】:Java: How to implement wildcard matching?Java:如何实现通配符匹配?
【发布时间】:2016-12-13 00:50:27
【问题描述】:

我正在研究如何在 BST 中找到最接近目标的 k 值,并遇到了以下带有规则的实现:

'?'匹配任何单个字符。

'*' 匹配任意字符序列(包括空序列)。

匹配应该覆盖整个输入字符串(不是部分)。

函数原型应该是: bool isMatch(const char *s, const char *p)

一些例子:

isMatch("aa","a") → 假

isMatch("aa","aa") → 真

isMatch("aaa","aa") → 假

isMatch("aa", "*") → 真

isMatch("aa", "a*") → 真

isMatch("ab", "?*") → 真

isMatch("aab", "cab") → 假

代码:

import java.util.*;

public class WildcardMatching {
    boolean isMatch(String s, String p) {
        int i=0, j=0;
        int ii=-1, jj=-1;

        while(i<s.length()) {
            if(j<p.length() && p.charAt(j)=='*') {
                ii=i;
                jj=j;
                j++;
            } else if(j<p.length() && 
                      (s.charAt(i) == p.charAt(j) ||
                       p.charAt(j) == '?')) {
                i++;
                j++;
            } else {
                if(jj==-1) return false;

                j=jj;
                i=ii+1;
            }
        }

        while(j<p.length() && p.charAt(j)=='*') j++;

        return j==p.length();
    }

    public static void main(String args[]) {
        String s = "aab";
        String p = "a*";

        WildcardMatching wcm = new WildcardMatching();
        System.out.println(wcm.isMatch(s, p));
    }
}

我的问题是,有两个额外的索引iijj 的原因是什么,为什么它们会使用-1 进行初始化?每个的目的是什么?用ij 遍历它还不够吗?

ii=i;jj=j; 在第一个 if 情况下,i=ii+1;j=jj; 在第三个 if 情况下的目的是什么?

最后,在什么情况下你会遇到while(j&lt;p.length() &amp;&amp; p.charAt(j)=='*') j++;

示例对理解非常有帮助。 提前感谢您,我们将接受回答/赞成票。

【问题讨论】:

  • 你想重新发明正则表达式的轮子吗?还是 Java 提供的预先构建的 RegExp 可以为你做?
  • @Arvind 对于算法实践,我正在实施。
  • 如果你真的想要一个更直接的方法来做这件事,你可以研究一下 Apache Lucene

标签: java algorithm wildcard


【解决方案1】:

看起来iijj 用于处理与任何序列匹配的通配符“*”。它们初始化为 -1 充当一个标志:它告诉我们是否遇到了不匹配的序列并且当前没有评估“*”。我们可以一次一个地浏览您的示例。

注意i 与参数s(原始字符串)相关,j 与参数p(模式)相关。

isMatch("aa","a"): 这将返回 false,因为在我们离开 while 循环之前j&lt;p.length() 语句将失败,因为p ("a") 的长度只有 1,而 s ("aa") 的长度是 2,所以我们'将跳转到else块。这就是-1初始化的地方:因为我们在p中没有看到任何通配符,jj仍然是-1,表明字符串不可能匹配,所以我们返回false。

isMatch("aa","aa"): sp 完全一样,所以程序反复评估 else-if 块没有问题,最后在 i 等于 2(“aa”的长度)时跳出 while 循环。第二个while循环永远不会运行,因为j不小于p.length()——实际上,由于else-if将ij加在一起,它们都等于2,并且2不小于“aa”的长度。我们返回j == p.length(),计算结果为2 == 2,得到true

isMatch("aaa","aa"): 这个失败的原因与第一个相同。也就是说,字符串的长度不同,我们从不打通配符。

isMatch("aa","*"):这就是有趣的地方。首先我们将进入 if 块,因为我们在p 中看到了一个“*”。我们将iijj 设置为0,并且只增加j。在第二次迭代中,j&lt;p.length() 失败,所以我们跳转到 else 块。 jj 不再是 -1(它是 0),所以我们将 j 重置为 0 并将 i 设置为 0+1。这基本上允许我们继续评估通配符,因为j 只是被重置为jj,它保存了通配符的位置,ii 告诉我们从原始字符串的哪里开始。这个测试用例还解释了第二个 while 循环。在某些情况下,我们的模式可能比原始字符串短得多,因此我们需要确保它与通配符匹配。例如,isMatch("aaaaaa","a**") 应该返回 true,但最后的 return 语句是检查是否j == p.length(),询问我们是否检查了整个模式。通常我们会在第一个通配符处停止,因为它匹配任何内容,所以我们需要最终遍历模式的其余部分并确保它只包含通配符。

从这里你可以弄清楚其他测试用例背后的逻辑。我希望这会有所帮助!

【讨论】:

    【解决方案2】:

    让我们看看这个有点乱。

    首先,这是字符串 (s) 和通配符模式 (p) 的并行迭代,使用变量 i 来索引 s 和变量 j 来索引 p

    当到达s 的末尾时,while 循环将停止迭代。发生这种情况时,希望 p 也已结束,以防万一它返回 true (j==p.length())。

    如果p* 结尾,那也是有效的(例如isMatch("ab", "ab*")),这就是while(j&lt;p.length() &amp;&amp; p.charAt(j)=='*') j++; 循环所确保的,即此时模式中的任何* 都会被跳过,如果到达p 的末尾,则返回true。如果未到达p 的末尾,则返回 false。

    这就是你上一个问题的答案。现在让我们看看循环。只要有匹配项,else if 将同时迭代 ij,例如'a' == 'a''a' == '?'

    当找到* 通配符(首先是if)时,它会将当前位置保存在iijj 中,以防需要回溯,然后跳过通配符。

    这基本上首先假设通配符匹配空字符串(例如isMatch("ab", "a*b"))。当它继续迭代时,else if 将匹配其余部分,并且方法最终返回 true

    现在,如果发现不匹配(else 块),它将尝试回溯。当然,如果它没有保存的通配符(jj==-1),它不能回溯,所以它只返回false。这就是jj 初始化为-1 的原因,因此它可以检测是否保存了通配符。 ii 可以初始化为任何值,但为了一致性,初始化为 -1

    如果在iijj 中保存了通配符位置,它将恢复这些值,然后将i 转发一个,即假设如果下一个字符与通配符匹配,则其余匹配将成功并返回true

    这就是逻辑。现在,它可以稍微优化一下,因为回溯是次优的。它当前将j 重置回*,并将i 重置回下一个字符。当它循环时,它会输入if并再次将保存值保存在jj中并将i值保存在ii中,然后递增j。由于这是给定的(除非到达s 的结尾),回溯也可以这样做,节省迭代循环,即

    } else {
        if(jj==-1) return false;
    
        i=++ii;
        j=jj+1;
    }
    

    【讨论】:

      【解决方案3】:

      代码在我看来有问题。 (见下文)

      iijj 的表面目的是实现一种回溯形式。

      例如,当您尝试将“abcde”与模式“a*e”进行匹配时,算法将首先将模式中的“a”与输入字符串中的“a”进行匹配。然后它会热切地将“*”与字符串的其余部分进行匹配......并发现它出错了。此时,它需要回溯并尝试替代方法

      iijj 用于记录要回溯到的点,这些变量的用途是记录新的回溯点或回溯。

      或者至少,这可能是作者在某个时候的意图。

      while(j&lt;p.length() &amp;&amp; p.charAt(j)=='*') j++; 似乎正在处理边缘情况


      但是,我认为这段代码不正确。

      1. 在模式中有多个“*”通配符的情况下,它肯定无法处理回溯。这需要递归解决方案。

      2. 部分:

            if(j<p.length() && p.charAt(j)=='*') {
                ii=i;
                jj=j;
                j++;
        

        没有多大意义。我原以为它应该增加i 而不是j。它可能会与 else 部分的行为“啮合”,但即使这样做也是一种复杂的编码方式。


      建议:

      1. 请勿将此代码用作示例。即使它有效(在有限的意义上),它也不是完成这项任务的好方法,也不是清晰或良好风格的例子。
      2. 我会通过将通配符模式转换为正则表达式然后使用Pattern / Matcher 进行匹配来处理这个问题。

        例如:Wildcard matching in Java

      【讨论】:

        【解决方案4】:

        我知道你问的是 BST,但老实说,还有一种方法可以使用正则表达式(不是用于竞争性编程,但足够稳定和快速,可用于生产环境):

        import java.util.regex.Pattern;
        import java.util.regex.Matcher;
        public class WildCardMatcher{
        
            public static void main(String []args){
                // Test
                String urlPattern = "http://*.my-webdomain.???",
                       urlToMatch = "http://webmail.my-webdomain.com";
                WildCardMatcher wildCardMatcher = new WildCardMatcher(urlPattern);
                System.out.printf("\"%s\".matches(\"%s\") -> %s%n", urlToMatch, wildCardMatcher, wildCardMatcher.matches(urlToMatch));
            }
             
            private final Pattern p;
            public WildCardMatcher(final String urlPattern){
               Pattern charsToEscape = Pattern.compile("([^*?]+)([*?]*)");
                
               // here we need to escape all the strings that are not "?" or "*", and replace any "?" and "*" with ".?" and ".*"
               Matcher m = charsToEscape.matcher(urlPattern);
               StringBuffer sb = new StringBuffer();
               String replacement, g1, g2;
               while(m.find()){
                   g1 = m.group(1);
                   g2 = m.group(2);
                   // We first have to escape pattern (original string can contain charachters that are invalid for regex), then escaping the '\' charachters that have a special meaning for replacement strings
                   replacement = (g1 == null ? "" : Matcher.quoteReplacement(Pattern.quote(g1))) +
                                 (g2 == null ? "" : g2.replaceAll("([*?])", ".$1")); // simply replacing "*" and "?"" with ".*" and ".?"
                   m.appendReplacement(sb, replacement);
               }
               m.appendTail(sb);
               p = Pattern.compile(sb.toString());
            }
             
            @Override
            public String toString(){
                return p.toString();
            }
             
            public boolean matches(final String urlToMatch){
                return p.matcher(urlToMatch).matches();
            }
        }
        

        您仍然可以实施一系列优化(小写/大写区分,为要检查的字符串设置最大长度以防止攻击者让您检查 4-GigaByte-String,...)。

        【讨论】:

          猜你喜欢
          • 2014-08-11
          • 1970-01-01
          • 2016-03-05
          • 2018-01-01
          • 2012-10-23
          • 2021-12-02
          • 2012-02-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多