繁体   English   中英

Java:如何实现通配符匹配?

[英]Java: How to implement wildcard matching?

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

'? 匹配任何单个字符。

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

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

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

一些例子:

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

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

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

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

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

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

isMatch("aab", "c a b") → false

代码:

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;的目的是什么ii=i; 并且jj=j; 在第一种情况下, i=ii+1; 并且j=jj; 在第三种情况下?

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

例子对理解非常有帮助。 在此先感谢您,并将接受回答/投票。

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

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

isMatch("aa","a") :这将返回 false 因为j<p.length()语句将在我们离开 while 循环之前失败,因为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不小于“的长度”啊”。 我们返回j == p.length() ,其计算结果为2 == 2 ,并得到true

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

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

从这里您可以找出其他测试用例背后的逻辑。 我希望这有帮助!

让我们看看这个有点乱。

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

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

然而,如果p*结尾,那也是有效的(例如isMatch("ab", "ab*") ),这就是while(j<p.length() && 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;
}

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

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

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

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

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

while(j<p.length() && 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进行Matcher来处理这个问题。

    例如: Java 中的通配符匹配

我知道你在问 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,...)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM