题目描述:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
示例 1:
ini
输入: s = "3[a]2[bc]"
输出: "aaabcbc"
示例 2:
ini
输入: s = "3[a2[c]]"
输出: "accaccacc"
示例 3:
ini
输入: s = "2[abc]3[cd]ef"
输出: "abcabccdcdcdef"
示例 4:
ini
输入: s = "abc3[cd]xyz"
输出: "abccdcdcdxyz"
提示:
1 <= s.length <= 30
s
由小写英文字母、数字和方括号'[]'
组成s
保证是一个 有效 的输入。s
中所有整数的取值范围为[1, 300]
思路:
用栈实现,题目想说把一个规则给你,让你实现出来,然后把结果字符串给输出出来。
首先想到的是,要区分出来每个字符是什么类型。分类三类:
- a-z
- 0-9
- ]
后面发现[也有用。他是用来把数字给分开的。
- 对于a-z的情况,定义一个全局变量level,如果level == 0,意味着可以直接进入final。否则,先入栈
- 对于0-9的情况,要考虑数字部分有没有完事儿,每完事儿要*10然后累加的。
- 对于]的情况,栈要退出。
这就是我的思路。于是有了下面的实现:
我的实现:
Java
class Solution {
public String decodeString(String s) {
StringBuilder finalSb = new StringBuilder();
Deque<String> stack = new LinkedList<>();
int temp = 0;
boolean numberFlag = false;
int level = 0;
for (char c: s.toCharArray()) {
if ('a' <= c && 'z' >= c || c == '[') { // got a a-z
if (numberFlag) { // number end, so add level
level++;
numberFlag = false;
stack.push(String.valueOf(temp));
temp = 0;
}
if ( c != '[') {
if (level == 0) {
finalSb.append(c);
} else {
stack.push(String.valueOf(c));
}
}
} else if ('0' <= c && '9' >= c) { // got a num
numberFlag = true;
temp *= 10;
temp += (c - '0');
continue;
} else if (']' == c) {
String top = stack.pop();
StringBuilder sb = new StringBuilder();
while(!(top.charAt(0) >= '0' && top.charAt(0) <= '9')) { // while is not anumber
sb.insert(0, top);
top = stack.pop();
}
int times = Integer.valueOf(top);
StringBuilder ssb = new StringBuilder();
String s_ = sb.toString();
while (times > 0) {
ssb.append(s_);
times--;
}
level--;
String ss = ssb.toString();
if (stack.isEmpty()) {
finalSb.append(ss);
} else {
stack.push(ss);
}
}
}
return finalSb.toString();
}
}
这是最终版。中间其实漏掉了挺多东西的,靠着调试一点一点改对了。像temp,numberFlag都是后加的。经验告诉我,这种numberFlag在逻辑好的代码里是不会出现的,但是我这里还是用了。
改进
我当然对这次的实现不太满意,于是请教了DS。于是有了下面的回答:
如果抛开原有思路,我会采用更简洁的双栈方案来实现字符串解码。这种方法将数字和字符串分别处理,代码结构清晰且符合直觉:
java
class Solution {
public String decodeString(String s) {
// 数字栈保存当前需要重复的次数
Deque<Integer> countStack = new ArrayDeque<>();
// 字符串栈保存上层已构建的结果
Deque<StringBuilder> stringStack = new ArrayDeque<>();
StringBuilder currentStr = new StringBuilder();
int currentNum = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
// 处理多位数累计
currentNum = currentNum * 10 + (c - '0');
} else if (c == '[') {
// 遇到左括号时保存当前状态
countStack.push(currentNum);
stringStack.push(currentStr);
currentStr = new StringBuilder();
currentNum = 0;
} else if (c == ']') {
// 遇到右括号时构建重复字符串
StringBuilder decodedStr = stringStack.pop();
int repeatTimes = countStack.pop();
decodedStr.append(currentStr.toString().repeat(repeatTimes));
currentStr = decodedStr;
} else {
// 普通字符直接追加
currentStr.append(c);
}
}
return currentStr.toString();
}
}
算法特点及优势:
-
双栈分治: • 数字栈单独处理数值计算 • 字符串栈保存每层上下文 • 时间复杂度 O(n),空间复杂度 O(n)
-
状态管理 : • 遇到
[
时冻结当前状态 • 遇到]
时解冻并合并结果 • 完美处理嵌套结构(如3[a2[c]]
) -
性能优化 : • 全程使用
StringBuilder
避免字符串拷贝 • 利用String.repeat()
简化重复逻辑(JDK11+特性) -
边界处理 : • 自动处理多位数(如
100[abc]
) • 正确处理前缀字符(如abc3[d]
) • 兼容无括号场景(如纯字符串abcdef
)
执行示例解析 (以输入3[a2[c]]
为例):
- 遍历到字符
3
:累计数字 currentNum=3 - 遇到
[
:数字3入栈,空StringBuilder入栈,重置状态 - 遍历到
a
:currentStr = "a" - 遍历到
2
:currentNum=2 - 遇到
[
:数字2入栈,当前字符串"a"入栈,重置状态 - 遍历到
c
:currentStr = "c" - 遇到
]
:弹出数字2,弹出字符串"a",构建"a"+"c".repeat(2) → "acc" - 再次遇到
]
:弹出数字3,弹出空字符串,构建"acc".repeat(3) → "acccaccacc"
这种实现不仅保持了算法的高效性,更通过清晰的逻辑分层让代码具有极佳的可读性,非常便于后续维护和扩展。
我的评价:
真是优秀的实现啊。核心思想我这么理解:
- 双栈,分别是Integer栈和StringBuilder的栈
- 遇到左括号,数字,当前的sb入栈,重置状态
- 遇到右括号,分别从两个栈获得数字和sb,使用String.repeat(int)来简化操作
- 使用Character.isDigit()判断数字了,其实还有个isLetter可以判断字符
升级后的实现
在完整理解了DS的思路之后,我仿照DS的写法也写了一遍:
Java
class Solution {
public String decodeString(String s) {
Deque<Integer> nums = new LinkedList<>();
Deque<StringBuilder> sbs = new LinkedList();
int currentNum = 0;
StringBuilder currentSb = new StringBuilder();
for (char c : s.toCharArray()) {
// number
if (Character.isDigit(c)) {
currentNum = currentNum * 10 + (c - '0');
} else if (c == '[') { // left
sbs.push(currentSb);
nums.push(currentNum);
currentSb = new StringBuilder();
currentNum = 0;
} else if (c == ']') { // right
var dequeSb = sbs.pop();
int repeatNum = nums.pop();
dequeSb.append(currentSb.toString().repeat(repeatNum));
currentSb = dequeSb;
} else {
currentSb.append(c);
}
}
return currentSb.toString();
}
}
好像除了命名(countStack
, stringStack
, 以及喜欢叫StringBuilder
string
而不是sb
)之外,基本上是一样的。
我想说这才是值得我们去学习的代码 - 简洁,直观,容易维护。
注意到一点,其实这个代码的写法和思维的顺序还挺不同的。像我会按照例子的顺序去考虑,比如先数字,然后是字符,最后是结尾括号(当然我漏掉了开头括号一开始)。但是DS这个代码一开始考虑的就很全,先数字,而且考虑多位,然后是开头括号,然后是终止括号,最后是字符。这个也值得学习,从一开头就考虑所有的case,然后再考虑他们的顺序。