过两天要填一个大坑(一文带你彻底搞懂递归函数),刚好看到一篇文章用很简洁的代码写了一个简单的正则引擎,里面巧妙的使用了递归完成了整个正则的实现,今天跟大家一起分享下,原文参考附在文末(在此由衷感谢作者),方便大家自取。
正则功能介绍
正则主要支持一般的字母匹配,还有特殊的元字符的实现,(*(匹配0个或多个前一个字符),?(匹配一个或0个前一个字符),.(匹配任意字符),^(匹配以后续字符开始), $(匹配以前面字符结束)),以上5种元字符的含义和js中的元字符的含义完全相同。
题引
类似这种比较抽象的需求,乍一看,脑袋是懵的,不知从何入手,有没有和我一样的小伙伴,很多这种诡异的,但是又可以从数量上进行拆解的,拆解后结构又不会发生变化的问题,通常是能够使用递归的思维进行解决的,说到了这里就提一下递归三要素。
- 递归出口,也就是递归结束的条件,程序要结束,递归就不可能无休止的进行下去。
- 递归基本情况,递归中最简单的情况,可以直接求解,而不需要再次调用递归函数,通常与递归出口相关。
- 递归的递推关系,将原问题拆解成若干个子问题的规律或公式。定义了原问题和子问题之间的关系,它是递归函数的核心。
大概了解了递归的概念,我们先看一个用递归求解的小例子:
function sum(arr) {
// 递归的出口
if(arr.length === 0) {
// 递归的基本情况,如果数组长度为0,则直接返回结果0
return 0
}
// 递归的地推关系sum(arr) = arr[0] + sum(剩余)
return arr.shift() + sum(arr)
}
通过上面这个简单的例子,想必大家对于递归有了初步的认识,接下来跟大家介绍一个递归的核心概念,递归函数的宏观概念,和微观运行,区分这两个概念,然后在递归中不断的从这两个方面去理解,将会有很大的好处,宏观概念就是,对于递归函数有一个清晰的定位,而不去理会实现的细节,如上例,sum的宏观概念就是求数组arr,从0到length-1的和,有了这个宏观概念之后我们再去想如何对他进行递推。微观运行就是每一次的递归运行栈及变量的存储,等等递归执行的细节都需要深入的理解。这个内容我会在后续的文章中再做详细说明。
match基础版实现
从递归的思维来考虑,假设我已经有一个函数match,它的功能就是进行正则的匹配,那么我们可以直接使用这个函数来解决问题
function match(pattern, text) {
// 递归出口和基本情况
if (pattern === "") {
return true;
} else {
// 递推关系
return (matchOne(pattern[0], text[0])
&& match(pattern.slice(1), text.slice(1)));
}
}
上面是一个简单的引擎的实现,如果pattern为空,则返回true,递推的关系如上,可以将字符串分解第一个字符和正则的第一个字符进行匹配,剩余的再交给match方法进行匹配,这样就行成了递归的关系,我们看下matchOne的实现。
matchOne实现
function matchOne(pattern, text) {
// pattern为空直接返回true
if (!pattern) return true;
// pattern不为空,text为空直接返回false
if (!text) return false;
// 如果pattern等于.,或者两者相等都返回true
return pattern === text && pattern === ".";
}
这个也非常好理解,"."元字符就是匹配任意字符。
添加“$”元字符
function match(pattern, text) {
if (pattern === "") return true;
// 当patter是“$”时,text也正好匹配完,则返回true
if (pattern === "$" && text === "") return true;
else
return matchOne(pattern[0], text[0])
&& match(pattern.slice(1), text.slice(1))
}
上面代码添加了元字符"$"的判断,该元字符的含义就是以什么结束,也就是说匹配到它的时候,文本正好结束,那么返回true。
添加“^”元字符
function search(pattern, text) {
if (pattern[0] === "^") {
// 调用match,匹配^之后的正则与text
// 匹配search("^abc", "abc")
return match(pattern.slice(1), text);
} else {
// 匹配search("bc", "abcd")
return text.split("").some((_, index) => {
return match(pattern, text.slice(index));
});
}
}
上面代码添新加了一个方法search,判断第一个字符如果是元字符"^",该元字符的含义就是以什么开始,也就是说它后面的pattern如果能够完整匹配text, 那么它就能匹配,若不是,则判断text中从每个字符开始是否能够匹配pattern,它用的是some,有一个匹配上就返回true。
修改match,添加“?”元字符
function match(pattern, text) {
if (pattern === "") {
return true;
} else if (pattern === "$" && text === "") {
return true;
// 如果pattern的第二个字符是"?",则进行特殊的处理,
// 注意这里没有进行分组的处理,所以只对前一个字母起作用,直接判断pattern[1]
} else if (pattern[1] === "?") {
return matchQuestion(pattern, text);
} else {
return matchOne(pattern[0], text[0])
&& match(pattern.slice(1), text.slice(1));
}
}
添加matchQuestion方法
function matchQuestion(pattern, text) {
// ?的第一种含义,前面字符为1个的时候
if (matchOne(pattern[0], text[0]) && match(pattern.slice(2), text.slice(1))) {
return true;
} else {
// ?的第二种含义,前面字符为0的时候
return match(pattern.slice(2), text);
}
}
该函数分别对“?”元字符的两种释义进行了处理,满足两种中的任意一种就返回true,即,?前面的字符有一个或者0个。
修改match,添加“*”元字符
function match(pattern, text) {
if (pattern === "") {
return true;
} else if (pattern === "$" && text === "") {
return true;
} else if (pattern[1] === "?") {
return matchQuestion(pattern, text);
// 如果pattern的第二个字符是"*",则进行特殊的处理,同样这里没有进行分组的处理,所以只对前一个字母起作用,直接判断pattern[1]
} else if(pattern[1] === "*") {
return matchStar(pattern, text);
} else {
return matchOne(pattern[0], text[0])
&& match(pattern.slice(1), text.slice(1));
}
}
添加matchStar方法
function matchStar(pattern, text) {
// 匹配任意次的前面字符
return ((matchOne(pattern[0], text[0]) && match(pattern, text.slice(1)))
// 匹配0次前面的字符
|| match(pattern.slice(2), text));
}
该函数也是分别对“*”元字符的两种释义进行了处理,满足两种中的任意一种就返回true,即,*前面的字符有多个或者0个。注意这里的调用关系,match里面调用了matchStar, 而matchStar里又调用了match,这也是递归。
基本到这里我们所有的元字符都进行了处理,功能也都完成了,最后做一点点优化
修改search方法
function search(pattern, text) {
if (pattern[0] === "^") {
return match(pattern.slice(1), text);
} else {
//因为我们实现了元字符"."和“*”,所以直接可以通过元字符的组合实现同样的功能
return match(".*" + pattern, text);
}
}