解决方案
以下代码片段生成了完成任务的模式(在ideone.com上查看它运行):
class TriangularSplitter {
static String assertPrefix(String pattern) {
return "(?<=(?=^pattern).*)".replace("pattern", pattern);
}
static String assertEntirety(String pattern) {
return "(?<=(?=^pattern$).*)".replace("pattern", pattern);
}
static String forEachDotBehind(String assertion) {
return "(?<=^(?:.assertion)*?)".replace("assertion", assertion);
}
public static void main(String[] args) {
final String TRIANGULAR_SPLITTER =
"(?x) (?<=^.) | measure (?=(.*)) check"
.replace("measure", assertPrefix("(?: notGyet . +NBefore +1After)*"))
.replace("notGyet", assertPrefix("(?! \\1 \\G)"))
.replace("+NBefore", forEachDotBehind(assertPrefix("(\\1? .)")))
.replace("+1After", assertPrefix(".* \\G (\\2?+ .)"))
.replace("check", assertEntirety("\\1 \\G \\2 . \\3"))
;
String text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
System.out.println(
java.util.Arrays.toString(text.split(TRIANGULAR_SPLITTER))
);
}
}
请注意,此解决方案使用了我正则表达式文章系列中已经介绍过的技术。这里唯一新的东西是
\G
和前向引用。
参考资料
以下是所使用的基本正则表达式构造的简要描述:
相关问题
解释
该模式匹配零宽度断言。使用相当复杂的算法来断定当前位置是否为三角形数。有两种主要的替代方案:
(?<=^.)
,即我们可以回顾并查看距字符串开头一个点的位置
- 否则,我们
测量
以重构上次匹配是如何进行的(使用\G
作为参考点),将测量结果存储在“before”\G
和“after”\G
捕获组中。然后,我们检查
当前位置是否符合测量要求,以确定下一次匹配应该在哪里进行。
因此,第一种替代方案是微不足道的“基础情况”,而第二种替代方案设置了如何在此之后进行所有后续匹配。Java没有自定义命名组,但以下是3个捕获组的语义:
\1
捕获字符串"before"\G
\2
捕获一些字符串“after”\G
- 如果
\1
的长度为例如1+2+3+...+k,则\2
的长度需要为k。
- 因此,
\2 .
的长度为k+1,应该是我们split
中的下一个部分!
\3
捕获当前位置右侧的字符串
- 因此,当我们可以在
\1 \G \2 . \3
上进行assertEntirety
时,我们匹配并设置新的\G
您可以使用数学归纳法来严格证明此算法的正确性。
为了帮助说明这是如何工作的,让我们通过一个示例来解释。假设我们将abcdefghijklm
作为输入,并且已经部分拆分了[a,bc,def]
。
\G we now need to match here!
↓ ↓
a b c d e f g h i j k l m n
\____1____/ \_2_/ . \__3__/ <--- \1 G \2 . \3
L=1+2+3 L=3
请记住,
\G
标记上一个匹配的结尾,并且它出现在三角形数索引处。如果
\G
出现在
1+2+3+...+k 处,则下一个匹配需要在
\G
后面的
k+1 个位置处才能成为三角形数索引。
因此,在我们的示例中,假设在刚刚拆分
def
的位置是
\G
,我们测量得到
k=3,下一个匹配将按预期拆分出
ghij
。
为了使
\1
和
\2
按照上述规范构建,我们基本上进行了一个
while
"循环":只要它还没有达到
notGyet
,我们按以下方式计数到
k:
+NBefore
,即我们通过 forEachDotBehind
扩展了一个 \1
+1After
,即我们只扩展了一个 \2
请注意,
notGyet
包含对模式中稍后定义的第1组的
前向引用。本质上,我们循环直到
\1
“命中”
\G
。
结论
毫无疑问,这种特定解决方案的性能非常差。正则表达式引擎只记住上一次匹配的位置(使用 \G
),而忘记了如何匹配(即所有捕获组在下一次尝试匹配时被重置)。因此,我们的模式必须重新构建匹配方式(传统解决方案中不需要这样做,因为变量不是那么“健忘”),通过逐个字符地追加字符串来费力地构建(这是 O(N^2)
)。每个简单的测量都是线性的,而不是恒定时间(因为它是作为一个字符串匹配来完成的,其中长度是一个因素),并且我们进行了许多冗余的测量(即要扩展一个字符,我们需要先重新匹配已有的内容)。
可能有许多“更好”的正则表达式解决方案。尽管如此,这种特定解决方案的复杂性和低效性应该表明正则表达式不是为这种类型的模式匹配而设计的。
话虽如此,出于学习目的,这是一个非常棒的问题,因为研究和制定解决方案的知识丰富。希望这种特定解决方案及其说明是有益的。
"polygenelubricants".hashCode()
是Integer.MIN_VALUE
。在二进制补码表示中,最负值是唯一的,因为它没有正数对应项。也就是说,在Java中,Math.abs(Integer.MIN_VALUE)
是一个负数。该字符串的来源归功于Josh Bloch,他在其中一个谜题中使用了它。 - polygenelubricants