傻瓜式解决 LeetCode 中的正则表达式匹配问题

101 阅读2分钟

最近失业了,在网上刷刷题找下手感,刷到了一个正则表达式匹配的问题。不禁感到很有意思。自己平就把写正则当做消遣,所以对这个题目自然手痒,想试着解决下。经过一些特殊用例的波折,算是调出来了。感觉写的还算工整易懂,运行时间和内存占用也还可以,比较满意,在这里存档下。

题目要求

image.png

题目地址::leetcode.cn/problems/re…

其实这类稍微复杂一些的字符分析问题的解题思路大致流程是类似的:

1. 都是把一个复杂问题拆解成多个步骤,每个步骤解决一个问题。
2. 上个步骤的产出结果交给下面的流程处理。
3. 构造一个合适的上下文对象便于多个流程之间同步信息
/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
var isMatch = function(s, p) {
    const strLen = s.length;
    const ctx = { lastIndex: 0, matched: [] };

    // 定义正则引擎支持的匹配类型
    const PattenFuncs = {
        matchWord: (ctx, word) => {
            const wordSize = word.length;
            const lastIndex = ctx.lastIndex;
            const isMatch = s.slice(lastIndex, lastIndex + wordSize) === word;
            
            if (isMatch) { ctx.lastIndex += wordSize; }

            return { isMatch, options: [{ match: isMatch ? word : '' }] };
        },

        '.': (ctx) => {
            const matchChar = s[ctx.lastIndex];
            if (matchChar) { ctx.lastIndex++; }
            
            return { isMatch: matchChar, options: [{ match: matchChar }] };
        },

        '*': (ctx, pattern) => {
            const options = [{ match: '', lastIndex: ctx.lastIndex }]; // 可以重复 0 次
            const repeatChar = pattern[0];
            const from = ctx.lastIndex;
            
            while (ctx.lastIndex < strLen) {
                const curChar = s[ctx.lastIndex];
                const isMatch = repeatChar === '.' ? true : curChar === repeatChar;
                
                if (!isMatch) {
                    break;
                } else {
                    options.push({
                        lastIndex: ctx.lastIndex + 1,
                        match: s.slice(from, ctx.lastIndex + 1)
                    });

                    ctx.lastIndex++;
                }
            }

            options.reverse(); // 正则引擎中 *、?、+ 属于贪婪匹配模式,需要优先尝试最大匹配
            return { isMatch: true, options };
        }
    };

    const parsePatterns = (pattenStr) => {
        const getPatternFunc = (pattern) => {
            if (pattern.includes('*')) { return PattenFuncs['*']; }
            if (pattern.includes('.')) { return PattenFuncs['.']; }

            return PattenFuncs.matchWord;
        };

        // 将正则表达式拆分为重复字符通配符、任意字符通配符、普通单词。拆分时需要注意
        // * 号不能单独存在,必须和普通字符或者 . 一起使用才行,所以正则匹配时要优先匹配,
        // 避免被拆开
        let pattens = pattenStr.split(/(.\*|\.)/).filter((i, idx) => i);
        
        // 将连续的多个冗余匹配过滤掉以节省性能否则会超时,比如 a*a*a*a* => a*
        pattens = pattens.filter((i, idx) => {
            const pre = pattens[idx -1];
            const hasRepeat = i.includes('*');
            const preHasRepeat = pre && pre.includes('*');
            return !(hasRepeat && preHasRepeat && i === pre)
        }).map(p => {
            return { pattern: p, func: getPatternFunc(p) };
        });

        return pattens;
    };

    const runPatterns = (ctx, patterns) => {
        const matched = ctx.matched;
        let allPatternRun = true;

        for (let i = 0; i < patterns.length; i++) {
            const { pattern, func } = patterns[i];
            const checkResult = func(ctx, pattern);
            
            if (!checkResult.isMatch) {
                allPatternRun = false;
                break;
            }

            if (checkResult.options.length === 1) {
                matched.push(checkResult.options[0].match);
            } else {
                // 如果是包含多个选项的表达式,尝试从中找出一个选项让其他的剩余表达式完全匹配
                // 如果能找到这样的选项则认为当前表达式可以匹配对应的字符串,否则不成.
                return checkResult.options.find((option) => {
                    return runPatterns({
                        lastIndex: option.lastIndex, 
                        matched: ctx.matched.concat(option.match)
                    }, patterns.slice(i + 1));
                }) !== undefined;
            }
        }

        // 如果是不包含多个选项的普通表达式,看是否全部表达式都执行完毕且匹配结果与字符串相等。
        return allPatternRun && matched.join('') === s;
    };

    return runPatterns(ctx, parsePatterns(p));
};