如果你熟悉 Shell 编程,那么一定了解过花括号展开,它可以用来生成任意字符串。
花括号展开的表达式可以看作一个由 花括号、逗号 和 小写英文字母 组成的字符串,定义下面几条语法规则:
- 如果只给出单一的元素
x
,那么表达式表示的字符串就只有"x"
。R(x) = {x}
- 例如,表达式
"a"
表示字符串"a"
。 - 而表达式
"w"
就表示字符串"w"
。
- 例如,表达式
- 当两个或多个表达式并列,以逗号分隔,我们取这些表达式中元素的并集。
R({e_1,e_2,...}) = R(e_1) ∪ R(e_2) ∪ ...
- 例如,表达式
"{a,b,c}"
表示字符串"a","b","c"
。 - 而表达式
"{{a,b},{b,c}}"
也可以表示字符串"a","b","c"
。
- 例如,表达式
- 要是两个或多个表达式相接,中间没有隔开时,我们从这些表达式中各取一个元素依次连接形成字符串。
R(e_1 + e_2) = {a + b for (a, b) in R(e_1) × R(e_2)}
- 例如,表达式
"{a,b}{c,d}"
表示字符串"ac","ad","bc","bd"
。
- 例如,表达式
- 表达式之间允许嵌套,单一元素与表达式的连接也是允许的。
- 例如,表达式
"a{b,c,d}"
表示字符串"ab","ac","ad"
。 - 例如,表达式
"a{b,c}{d,e}f{g,h}"
可以表示字符串"abdfg", "abdfh", "abefg", "abefh", "acdfg", "acdfh", "acefg", "acefh"
。
- 例如,表达式
给出表示基于给定语法规则的表达式 expression
,返回它所表示的所有字符串组成的有序列表。
假如你希望以「集合」的概念了解此题,也可以通过点击 “显示英文描述” 获取详情。
示例 1:
输入:expression = "{a,b}{c,{d,e}}" 输出:["ac","ad","ae","bc","bd","be"]
示例 2:
输入:expression = "{{a,z},a{b,c},{ab,z}}" 输出:["a","ab","ac","z"] 解释:输出中 不应 出现重复的组合结果。
提示:
1 <= expression.length <= 60
expression[i]
由'{'
,'}'
,','
或小写英文字母组成- 给出的表达式
expression
用以表示一组基于题目描述中语法构造的字符串
提示 1
You can write helper methods to parse the next "chunk" of the expression.
If you see eg. "a", the answer is just the set {a}.
If you see "{", you parse until you complete the "}" (the number of { and } seen are equal) and that becomes a chunk that you find where the appropriate commas are, and parse each individual expression between the commas.
解法1:递归解析
思路与算法
表达式可以拆分为多个子表达式,以逗号分隔或者直接相接。我们应当先按照逗号分割成多个子表达式进行求解,然后再对所有结果求并集。这样做的原因是求积的优先级高于求并集的优先级。
我们用 expr 表示一个任意一种表达式,用 term 表示一个最外层没有逗号分割的表达式,那么 expr 可以按照如下规则分解:
expr→term ∣ term,expr
其中的 ∣ 表示或者,即 expr 可以分解为前者,也可以分解为后者。
再来看 term, term 可以由小写英文字母或者花括号包括的表达式直接相接组成,我们用 item 来表示每一个相接单元,那么 term 可以按照如下规则分解:
term→item ∣ item term
item 可以进一步分解为小写英文字母 letter 或者花括号包括的表达式,它的分解如下:
item→letter ∣ {expr}
在代码中,我们编写三个函数,分别负责以上三种规则的分解:
- expr 函数,不断的调用 term,并与其结果进行合并。如果匹配到表达式末尾或者当前字符不是逗号时,则返回。
- term 函数,不断的调用 item,并与其结果求积。如果匹配到表达式末尾或者当前字符不是小写字母,并且也不是左括号时,则返回。
- item 函数,根据当前字符是不是左括号来求解。如果是左括号,则调用 expr,返回结果;否则构造一个只包含当前字符的字符串集合,返回结果。
以下示意图以 {a,b}{c,{d,e}} 为例,展示了表达式递归拆解以及回溯的全过程。
在代码实现过程中有以下细节:
维护一个外部指针来遍历整个表达式,或者将表达式和当前遍历下标以引用的方式传递给被调函数。
因为最终答案需要去重,所以可以先用集合来求解中间结果,最后再转换成已排序的列表作为最终答案。
解题思路
-
理解表达式结构:首先,我们需要理解表达式的结构。表达式由基本元素(小写字母)和花括号包含的子表达式组成。子表达式可以包含逗号分隔的多个选项,也可以与其他子表达式或基本元素相连。
-
分解表达式:表达式可以分解为更小的单元,这些单元可以是单个字符或花括号内的组合。我们需要递归地分解这些单元,直到无法进一步分解。
-
处理并集和积:表达式中的逗号表示并集(即取所有可能的选项),而连续的单元表示积(即所有单元的排列组合)。我们需要先处理并集,再处理积。
-
递归解析:通过递归地解析表达式,我们可以生成所有可能的字符串组合。递归的底部是遇到单个字符或花括号内的表达式已经完全解析。
-
去重和排序:由于递归过程中可能会生成重复的字符串,我们需要在最后将结果去重并排序,以满足题目要求的有序列表。
算法逻辑
-
分解表达式:使用递归函数
expr
来分解表达式。这个函数会处理表达式中的逗号,并将其拆分为更小的表达式term
。 -
分解单元:在
expr
函数中,使用term
函数来分解表达式中的单元。term
函数会处理连续的字符或花括号内的表达式。 -
处理花括号:在
term
函数中,如果遇到花括号,使用item
函数来处理花括号内的表达式。item
函数会递归地解析花括号内的表达式,直到找到闭合的花括号。 -
生成组合:在
item
函数中,如果当前字符不是花括号,那么它就是一个基本元素,直接添加到结果集合中。如果是花括号,那么递归解析内部的表达式,并生成所有可能的选项。 -
回溯:在递归过程中,我们需要回溯到上一个函数调用,以便尝试所有可能的选项。这通过在
item
函数中添加和删除当前处理的字符或选项来实现。 -
去重和排序:在所有递归调用完成后,我们将结果集合转换为列表,并对其进行排序,以生成最终的有序列表。
Java版:
class Solution {
private String expression;
private int index;
private int n;
public List<String> braceExpansionII(String expression) {
this.expression = expression;
this.index = 0;
this.n = expression.length();
Set<String> ret = expr();
return new ArrayList<String>(ret);
}
private Set<String> expr() {
Set<String> ret = new TreeSet<>();
while (true) {
ret.addAll(term());
if (index < n && expression.charAt(index) == ',') {
index++;
continue;
} else {
break;
}
}
return ret;
}
private Set<String> term() {
Set<String> ret = new TreeSet<>();
ret.add("");
while (index < n && (expression.charAt(index) == '{' || Character.isLowerCase(expression.charAt(index)))) {
Set<String> sub = item();
Set<String> tmp = new TreeSet<>();
for (String left : ret) {
for (String right : sub) {
tmp.add(left + right);
}
}
ret = tmp;
}
return ret;
}
private Set<String> item() {
Set<String> ret = new TreeSet<>();
if (expression.charAt(index) == '{') {
index++;
ret = expr();
} else {
ret.add(String.valueOf(expression.charAt(index)));
}
index++;
return ret;
}
}
Python3版:
使用 nonlocal 关键字允许函数内部的 index 变量与方法外部的 index 绑定
class Solution:
def braceExpansionII(self, expression: str) -> List[str]:
index = 0
n = len(expression)
def expr() -> set:
nonlocal index
ret = set()
while True:
ret = ret.union(term())
if index < n and expression[index] == ',':
index += 1
continue
else:
break
return ret
def term() -> set:
nonlocal index
ret = {""}
while index < n and (expression[index].islower() or expression[index] == '{'):
sub = item()
temp = set()
for left in ret:
for right in sub:
temp.add(left + right)
ret = temp
return ret
def item() -> set:
nonlocal index
ret = set()
if expression[index] == '{':
index += 1
ret = expr()
else:
ret = {expression[index]}
index += 1
return ret
return sorted(list(expr()))
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是 expression 的长度。整个 expression 只会遍历一次,时间复杂度为 O(n),集合合并以及求积运算的时间复杂度为 O(nlogn),因此总的时间复杂度为 O(nlogn)。
- 空间复杂度:O(n)。递归过程所需的栈空间为 O(n),以及存放中间答案的空间复杂度为 O(n),因此总的空间复杂度为 O(n)。
解法2:栈
思路与算法
如果把题目中的表达式并列关系看做是求和,把相接看做是求积,那么求解整个表达式的过程可以类比于求解中缀表达式的过程,例如:{a,b}{c,{d,e}} 可以看做是 {a,b}×{c+{d+e}}。
与求解中缀表达式一样,在遍历表达式的过程中我们需要用到两个栈,一个用来存放运算符(即加号和乘号,以及左大括号),另一个用来存运算对象(即集合)。
在本题中有一个特殊情况需要处理,就是乘号需要我们自己来添加,我们按照当前字符的种类来判断前面是否需要添加乘号:
- 如果当前字符是 ‘‘{",并且前面是 ‘‘}" 或者小写英文字母时,需要添加乘号运算。
- 如果当前字符是小写字母,并且前面是 ‘‘}" 或者是小写英文字母时,需要添加乘号运算。
- 如果当前字符是 ‘‘," ,则前面一定不需要添加乘号运算。
- 如果当前字符是 ‘‘}",则前面一定不需要添加乘号运算。
因此,只有当前字符是 ‘‘{" 或者小写字母时,才需要考虑是否在前面添加乘号。
接下来我们分析运算优先级的问题,在本题中只涉及加法和乘法两种运算。如果一个表达式同时有并列和相接,那我们应该先计算相接的结果,再计算并列的结果。因此,乘法的优先级要大于加法。
至此,我们可以按照如下流程来计算表达式的值:
- 如果遇到 ‘‘,",则先判断运算符栈顶是否是乘号,如果是乘号则需要先计算乘法,直到栈顶不是乘号为止,再将加号放入运算符栈中。
- 如果遇到 ‘‘{",则先判断是否需要添加乘号,再将 { 放入运算符栈。
- 如果遇到 ‘‘}",则不断地弹出运算符栈顶,并进行相应的计算,直到栈顶为左括号为止。
- 如果遇到小写字母,则先判断是否需要添加乘号,再构造一个只包含当前小写字母的字符串集合,放入集合栈中。
按照上述流程遍历完一次之后,由于题目给定的表达式中最外层可能没有大括号,例如 {a,b}{c,{d,e}},因此运算符栈中可能依然有元素,我们需要依次将他们弹出并进行计算。最终,集合栈栈顶元素即为答案。
算法逻辑:
-
初始化两个栈:一个用于存放运算符,另一个用于存放集合。
-
遍历表达式:对表达式中的每个字符进行遍历。
-
字符分类处理:
- 逗号(
,
):检查运算符栈顶元素,如果是乘号,则执行乘法运算直到栈顶不是乘号,然后压入加号。 - 左大括号(
{
):检查前一个字符,如果需要,则先压入乘号,然后压入左大括号。 - 右大括号(
}
):连续弹出运算符栈顶元素,执行相应运算,直到遇到左大括号。 - 小写字母:检查前一个字符,如果需要,则先压入乘号,然后将字母作为一个新集合压入集合栈。
- 逗号(
-
运算符执行:根据运算符栈顶元素,执行加法或乘法运算。加法是集合的合并,乘法是集合的笛卡尔积。
-
循环结束处理:如果遍历结束后运算符栈不为空,继续执行运算直到栈为空。
-
结果输出:集合栈的栈顶元素即为最终结果,将其转换为排序后的列表返回。
Java版:
class Solution {
public List<String> braceExpansionII(String expression) {
Deque<Character> op = new ArrayDeque<>();
List<Set<String>> stack = new ArrayList<>();
for (int i = 0; i < expression.length(); i++) {
char ch = expression.charAt(i);
if (ch == ',') {
// 不断地弹出栈顶运算符,直到栈为空或者栈顶不为乘号
while (!op.isEmpty() && op.peek() == '*') {
ope(op, stack);
}
op.push('+');
} else if (ch == '{') {
// 首先判断是否需要添加乘号,再将 { 添加到运算符栈中
if (i > 0 && (expression.charAt(i - 1) == '}' || Character.isLowerCase(expression.charAt(i - 1)))) {
op.push('*');
}
op.push('{');
} else if (ch == '}') {
// 不断地弹出栈顶运算符,直到栈顶为 {
while (!op.isEmpty() && op.peek() != '{') {
ope(op, stack);
}
op.pop();
} else {
// 首先判断是否需要添加乘号,再将新构造的集合添加到集合栈中
if (i > 0 && (expression.charAt(i - 1) == '}' || Character.isLowerCase(expression.charAt(i - 1)))) {
op.push('*');
}
String x = String.valueOf(ch);
stack.add(new TreeSet<String>() {{
add(x);
}});
}
}
while (!op.isEmpty()) {
ope(op, stack);
}
return new ArrayList<String>(stack.get(stack.size() - 1));
}
// 弹出栈顶运算符,并进行计算
private void ope(Deque<Character> op, List<Set<String>> stack) {
int l = stack.size() - 2;
int r = stack.size() - 1;
if (op.peek() == '+') {
stack.get(l).addAll(stack.get(r));
} else {
Set<String> tmp = new TreeSet<>();
for (String left : stack.get(l)) {
for (String right : stack.get(r)) {
tmp.add(left + right);
}
}
stack.set(l, tmp);
}
stack.remove(stack.size() - 1);
op.pop();
}
}
Python3版:
class Solution:
def braceExpansionII(self, expression: str) -> List[str]:
op = []
stack = []
def ope():
if op[-1] == '+':
stack[-2].update(stack[-1])
else:
tmp = set()
for left in stack[-2]:
for right in stack[-1]:
tmp.add(left + right)
stack[-2] = tmp
stack.pop()
op.pop()
for i, ch in enumerate(expression):
if ch == ',':
while op and op[-1] == '*':
ope()
op.append('+')
elif ch == '{':
if i > 0 and (expression[i - 1] == '}' or expression[i - 1].islower()):
op.append('*')
op.append('{')
elif ch == '}':
while op and op[-1] != '{':
ope()
op.pop()
else:
if i > 0 and (expression[i - 1] == '}' or expression[i - 1].islower()):
op.append('*')
stack.append({expression[i]})
while op:
ope()
return sorted(list(stack[-1]))
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是 expression 的长度。整个 expression 只会遍历一次,时间复杂度为 O(n),集合合并以及求积运算的时间复杂度为 O(nlogn),因此总的时间复杂度为 O(nlogn)。
- 空间复杂度:O(n)。过程中用到了两个栈,他们都满足在任意时刻元素个数不超过 O(n),包含 n 个元素的集合的时间复杂度为 O(n),因此总的空间复杂度为 O(n)。