44. 通配符匹配

172 阅读5分钟

通配符匹配(困难) 给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 '?' 和 '*' 匹配规则的通配符匹配:

  • '?' 可以匹配任何单个字符。
  • '*' 可以匹配任意字符序列(包括空字符序列)。

判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。

示例 1:

输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa", p = "*" 输出:true 解释:'*' 可以匹配任意字符串。

示例 3:

输入:s = "cb", p = "?a" 输出:false 解释:'?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。

提示:

  • 0 <= s.length, p.length <= 2000
  • s 仅由小写英文字母组成
  • p 仅由小写英文字母、'?' 或 '*' 组成

官方题解思路

说明:

  • sp[i][j] 表示字符串 s 的前 i 个字符和模式 p 的前 j 个字符是否能匹配。
  • 对比的是s的第i个字符和p的第j个字符:s[i-1],p[j-1]

动态规划 - 解题思路 - 时间复杂度 O(m*n)

  1. 划分子问题
  2. 状态转移方程
  3. 确定边界条件
  4. 顺序执行,一般由子问题的结果来解决最终问题
  1. 最终问题:ap[i][j]
  2. 划分子问题:

  1. 边界:
    sp[0][0] === true,
    sp[i][0] === false,
    sp[0][j] 当p[j].every(char => char === '*') 为true
  2. 执行顺序:
    由子问题的结果来解决最终问题,则 i 和 j 应从 0-n/m

/**
 * 复杂度 O(n*m)
 */
function getDP(i, j, s, p, dp) {
  if (i === 0 && j === 0) {
    return true;
  } else if (i === 0 && j > 0) {
    return p.slice(0, j).every(char => char === '*');
  } else if (i > 0 && j === 0) {
    return false;
  }
  if (p[j-1] === '*') {
    return dp[i - 1][j] || dp[i][j - 1];
  }
  if (p[j-1] === '?' || s[i-1] === p[j-1]) {
    return dp[i - 1][j - 1];
  }
  return false;
}

const isMatch = function(s, p) {
  const sLength = s.length;
  const pLength = p.length;
  const sl = s.split('');
  const pl = p.split('');
  const dp = [];

  for (let i = 0; i <= sLength; i++) {
    dp[i] = [];
    for (let j = 0; j <= pLength; j++) {
      dp[i][j] = getDP(i, j, sl, pl, dp);
    }
  }

  console.log({dp})
  return dp[sLength][pLength];
};

贪心算法 - 解题思路 - 时间复杂度O(mlogn)-O(m*n)

  1. 建立问题的数学模型;
  2. 把求解的问题分成若干个子问题;
  3. 寻找这些子问题的局部最优解;
  4. 把局部最优解合成全局最优解。
  1. 子问题分析:
    p = '∗abcd∗' 字符串 s 中的每个位置作为起始位置,并判断对应的子串是否为 abcd 即可。这种暴力方法的时间复杂度为 O(mn)
    p = '∗abcd∗efgh∗i∗' 首先暴力找到最早出现的 abcd,随后从下一个位置开始暴力找到最早出现的 efgh,最后找出 i,这样「贪心地」找到最早出现的子串是比较直观的,因为如果 sss 中多次出现了某个子串,那么我们选择最早出现的位置,可以使得后续子串能被找到的机会更大。
    p = 'abcd∗efgh∗i∗opi' split 为 abcd & ∗efgh∗i∗ & opi,s的前4个字符与 abcd , s的后3个字符与opi匹配,中间段同上
    p = ''
  2. 局部最优解合成全局最优解:
    合成首,尾,中间段,p=""时的结果,得到最终结果

个人初次解题思路

💡 优化后执行时间从288ms -> 88ms

输入s p

s[i] ,p[j]

sp[i][j] // p[j] === '?' | p[j] === s[i]

lastPIndex // 上一个 "" 的p下标

lastSIndex // 匹配到上一个 ""时 的s下标,失败则右移一位作为下轮(与lastP*Index+1之后字符)匹配的开始

优化点:新增 lastSIndexNext 代表下一个与 lastPIndex + 1 匹配的s下标

  1. 依次对比sp[i][j],直到 i >= s.length
    • 当遇到 p[j] 为""的时候,记下当前i和j的位置,因为匹配0/n个任意字符,所以i不动,j右移一位
      优化点:lastS*IndexNext = undefined
    • 当遇到 sp[i][j] 为true时,i j分别右移一位
      优化点:s[i] === p[lastPIndex+1] && i !== lastSIndex && !lastSIndexNext则 lastSIndexNext = i
    • 当遇到 sp[i][j] 为false,lastSIndex无值时
      代表j之前没有出现过
      ,且出现了不匹配的情况,则直接输出结果 “匹配失败❌”
    • 当遇到 sp[i][j] 为false,lastSIndex有值时
      代表以 lastS
      Index 作为开始字符与lastPIndex+1(的下一个字符)匹配的这一轮失败,需要以 lastSIndex+1 作为开始字符 继续 与 lastPIndex+1 之后的字符
      优化点:可以优化为记住下一个与lastPIndex+1相同字符的下标,下一轮可以跳到那个记忆点,避免重复匹配,例如 isMatch('abcabczzzde', 'abc???de')
      if (lastS
      IndexNext) {
      lastSIndex = lastSIndexNext;
      lastSIndexNext = undefined;
      } else if (p[lastP
      Index+1] !== '?') {
      lastS*Index = i;
      }
  1. 判断 j 之后是否有非“”的字符,若有则代表规则不匹配,若无则代表规则匹配,return p.slice(j).every(char => char === '')

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */

const isMatch = function(s, p) {
  const sList = s.split('');
  const pList = p.split('');

  let lastStarIndexP;
  let lastStarIndexS;
  let lastStarIndexSNext;
  let j = 0;
  let i = 0;

  while (i < sList.length) {
    if (pList[j] === '*') {
      // console.log('lastStarIndexP', i, j, sList[i], pList[j])
      lastStarIndexP = j;
      lastStarIndexS = i;
      lastStarIndexSNext = undefined;
      // console.log({lastStarIndexS, lastStarIndexP});
      j++;
      
    } else if (pList[j] === '?' || pList[j] === sList[i]) {
      // console.log('next', i, j, sList[i], pList[j]);
      
      // 记住s中下一个与*下一个字符对比的下标
      if (!lastStarIndexSNext && sList[i] === pList[lastStarIndexP + 1] && i !== lastStarIndexS) {
        lastStarIndexSNext = i;
        // console.log({lastStarIndexSNext})
      }
      
      j++;
      i++;

    } else if (lastStarIndexP > -1) {
      // console.log('retry', i, j, sList[i], pList[j], {lastStarIndexS});
      // j 复位到 * 的后一个字符
      j = lastStarIndexP + 1;
      // i 复位到 lastStarIndexS + 1 || lastStarIndexSNext
      if (lastStarIndexSNext) {
        lastStarIndexS = lastStarIndexSNext;
        lastStarIndexSNext = null;
      } else if (p[lastP*Index+1] !== '?') {
        lastStarIndexS = i;
      } else {
        lastStarIndexS++;
      }
      i = lastStarIndexS;
      
    } else {
    	//  console.log('error', i, j, sList[i], pList[j]);
      return false;
    }
  }

	// console.log('exit', {j, p});
  return pList.slice(j).every(char => char === '*');
};