替换字符串中的括号内容 图解题解
这道题到底在问什么
- 输入
- s = "(name)is(age)yearsold", knowledge = [["name","bob"],["age","two"]]
- 输出
- "bobistwoyearsold"(name 换 bob、age 换 two,中间字母不动)
- 输入
- s = "hi(name)", knowledge = [["a","b"]]
- 输出
- "hi?"(清单里没有 name,这对括号换成问号)
最优解:为什么这么做
一句话答案:LeetCode 1807 替换字符串中的括号内容:先把 knowledge 建成哈希表,再从左到右扫一遍 s,括号外字母照抄、括号内的键查表替换、查不到换问号,一趟拼出结果,时间 O(n+m)。
括号里的键换成值,拼出完整结果串
给一个字符串 s,只有小写字母和成对、不嵌套的小括号,每对括号里是一个非空的键。另给清单 knowledge,每项是「键,值」。要把 s 里每对括号连同键换成对应的值,查不到的键整对换成一个问号,括号外的字母原样保留。题面 s = "(name)is(age)yearsold",name 对应 bob、age 对应 two,得到 "bobistwoyearsold"。
每查一个键都翻遍清单,慢在哪
不建表的话,收出括号里的键后,就得到 knowledge 里从头找哪项的键和它一样。可 s 里可能有很多对括号,每对都把清单从头翻一遍。设 s 长 n、清单键值总字符数是 m,括号对数最多到 n 量级、每次线性找又是 m 量级,乘起来接近 n 乘 m,键一多、清单一长就拖慢。症结在「按键找值」每回都白翻,翻一次记下就够。
先建一张表,把查键做成一步到位
把 knowledge 一次性搬进哈希表,也就是按键直接拿到值的那种表。表建好后不论查多少次键都是常数时间,不必再回原清单里翻。
剩下的事是从左到右把 s 扫一遍:遇到括号外的字母直接抄进结果;遇到左括号,就从下一个字符起把键一个个收起来,直到撞见右括号,再拿整段键去表里查——查到就把这对括号换成对应的值,查不到就换一个问号。一趟扫完,结果串就拼好了。
指针为什么一趟扫到底,不用回头
括号不嵌套、每个左括号都配一个右括号,这两条保证扫描不必回头。参考代码里下标 i 从头走到末尾:s 当前位不是左括号就把字母接进结果;是左括号就用 find 找到配对的右括号位置 j,把两括号之间那段当键去查表、把查到的接进结果,再让 i 直接跳到 j 后接着走。每段括号一次处理完、指针不倒退,整趟就是线性的。收键要把括号里整段收全再查,只取头一个字符会把多字符的键查错。
(name)is(age)yearsold 扫到最后拼成什么
先建表,记下 name 对应 bob、age 对应 two。指针从下标 0 出发:先撞上左括号,收键收到 name,到右括号闭合,查表得 bob,结果是 "bob"。接着 i、s 两个字母在括号外,原样抄上成 "bobis"。再遇左括号,收键 age,查表得 two,拼成 "bobistwo"。最后 yearsold 全在括号外逐个抄进去,得到 "bobistwoyearsold"。若换成 s = "hi(name)"、清单里查不到 name,这对括号就换成问号、hi 原样保留,结果是 "hi?"。
两段都线性,剩几个坑要防
建表把 knowledge 的 m 个字符过一遍,扫描把 s 的 n 个字符过一遍,查表平均常数时间,相加是 O(n+m)。空间上哈希表存下所有键值是 O(m)、拼结果的容器和输出等长是 O(n),峰值取两者之和 O(n+m),不是常数。
查不到的键必须换成一个问号,留空会漏字符、保留原键会把括号或键名混进结果。括号外的字母只能原样抄,别也拿去查表,像 (a)(a)aaa 里括号中的 a 要换值、后面裸着的三个 a 得原样留着。同一个键出现几次就各查各换,别只换第一次。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套动作:先建表,再一遍扫。括号外的字母照抄,括号内的字符拼成键去查表,查到用值替换、查不到用问号替换。下面每一帧都在套这个流程,先看建表,再看指针一格一格往右走。
- 4先看清这串 "z(a)(a)(bc)" 的结构。开头的 z 在括号外,是要原样保留的普通字母;后面是三对括号,里面的键分别是 a、a、bc。绿色标出的是括号内的键、灰色是括号本身。心里有了这张地图,再开始建表和扫描。
- 5正式扫描前,先把 knowledge 这张清单搬进哈希表。现在表还空着,一条都没有。建表的好处是,后面不管查多少次键,每次都是常数时间,不用每次都在原始清单里从头找一遍。
- 6knowledge 的第一条是键 a 对应值 go,放进表里。以后只要在括号里读到键 a,一查表就能拿到 go。
- 7第二条是键 k 对应值 hi,也放进表里。注意这条 k 在待处理的字符串里其实一次都用不上,但表里存着不碍事,查不到才是问题,查得到用不到没关系。
- 8表建好了,一共两条。现在把指针放回字符串开头,准备从下标 0 开始一格一格往右扫。结果串目前是空的,接下来会一点点拼出来。
- 9指针在下标 0,这是括号外面的普通字母 "z"。不用查表,直接原样抄进结果。结果串现在是 "z"。
- 10指针走到下标 1,这是一个左括号。看到左括号就知道:接下来一段是括号里的键。先把键的缓冲清空,从下一格开始一个字符一个字符地把键收进来,直到撞见右括号为止。
- 11指针在括号里,读到字符 "a",把它接到键的缓冲上,现在收集到的键是 "a"。还没到右括号,继续往右收,看键会不会更长。
- 12指针走到下标 3 的右括号,这对括号闭合了。中间收集到的键是 "a"。现在拿这个键去知识表里查一下,看看表里有没有这一行。
- 13表里正好有键 "a" 这一行,对应的值是 "go"。于是把左括号、键、右括号这一整对,统统换成 "go" 接到结果后面。现在结果串变成 "zgo"。
- 14指针走到下标 4,这是一个左括号。看到左括号就知道:接下来一段是括号里的键。先把键的缓冲清空,从下一格开始一个字符一个字符地把键收进来,直到撞见右括号为止。
- 15指针在括号里,读到字符 "a",把它接到键的缓冲上,现在收集到的键是 "a"。还没到右括号,继续往右收,看键会不会更长。
- 16指针走到下标 6 的右括号,这对括号闭合了。中间收集到的键是 "a"。现在拿这个键去知识表里查一下,看看表里有没有这一行。
- 17表里正好有键 "a" 这一行,对应的值是 "go"。于是把左括号、键、右括号这一整对,统统换成 "go" 接到结果后面。现在结果串变成 "zgogo"。
- 18指针走到下标 7,这是一个左括号。看到左括号就知道:接下来一段是括号里的键。先把键的缓冲清空,从下一格开始一个字符一个字符地把键收进来,直到撞见右括号为止。
- 19指针在括号里,读到字符 "b",把它接到键的缓冲上,现在收集到的键是 "b"。还没到右括号,继续往右收,看键会不会更长。
- 20指针在括号里,读到字符 "c",把它接到键的缓冲上,现在收集到的键是 "bc"。还没到右括号,继续往右收,看键会不会更长。
- 21指针走到下标 10 的右括号,这对括号闭合了。中间收集到的键是 "bc"。现在拿这个键去知识表里查一下,看看表里有没有这一行。
- 22拿这个键到哈希表里查一次,发现没有 "bc" 这个键。按规则,查不到的键要用一个问号来替换,而不是留空或保留原样。于是这对括号换成 "?" 接到结果后面,结果串变成 "zgogo?"。
- 23指针走到字符串末尾,整个 s 只扫了一遍,每个字符都处理过了。括号外的字母原样抄,括号内的键查表替换,结果串一路拼到现在是 "zgogo?"。
- 24回放一遍怎么拼出来的:开头的 z 在括号外,原样保留;第一对 (a) 查到 go、第二对 (a) 同一个键还是查到 go,同一个键出现两次都替换;最后 (bc) 在表里查不到,换成一个问号。连起来正好是 "zgogo?"。
- 25最后对照一下规则有没有走偏:括号外的字母原样保留、括号内的键查到就用值替换、查不到就用一个问号替换,同一个键出现几次就替换几次。演示这三种情况都碰上了,答案 "zgogo?" 完全符合规则。
⚠️ 容易写错的地方
✗ 错:查不到的键随手留空,或者把原来的键原样保留
✓ 对:查不到必须替换成一个问号 "?",既不是留空也不是保留原键
题目把「未知键」的处理规定得很死:整对括号换成一个问号。留空会漏字符,保留原键会把括号或键名混进结果,都是错的
✗ 错:把括号外的字母也拿去查表替换
✓ 对:只有括号里的键才替换,括号外的字母一律原样抄进结果
例子 "(a)(a)(a)aaa" 里,前面括号中的 a 要换成值,后面裸露的三个 a 不在括号里,必须原样保留。把括号外字母也替换会得到完全不同的错误串
✗ 错:以为同一个键只替换第一次出现,或者假设键只有一个字符
✓ 对:每对括号都独立查表替换,同一个键出现多少次就替换多少次;键可能是多字符,要整段收集
键在 s 里可以重复出现,像两对 (a) 都要各换一次;键也可能是 "name" 这种多字符,只取一个字符当键会查错。必须把括号里整段收全再查
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def evaluate(self, s: str, knowledge: List[List[str]]) -> str:
d = {a: b for a, b in knowledge}
i, n = 0, len(s)
ans = []
while i < n:
if s[i] == '(':
j = s.find(')', i + 1)
ans.append(d.get(s[i + 1 : j], '?'))
i = j
else:
ans.append(s[i])
i += 1
return ''.join(ans)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
string evaluate(string s, vector<vector<string>>& knowledge) {
unordered_map<string, string> d;
for (auto& e : knowledge) {
d[e[0]] = e[1];
}
string ans;
for (int i = 0; i < s.size(); ++i) {
if (s[i] == '(') {
int j = s.find(")", i + 1);
auto t = s.substr(i + 1, j - i - 1);
ans += d.count(t) ? d[t] : "?";
i = j;
} else {
ans += s[i];
}
}
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public String evaluate(String s, List<List<String>> knowledge) {
Map<String, String> d = new HashMap<>(knowledge.size());
for (List<String> e : knowledge) {
d.put(e.get(0), e.get(1));
}
StringBuilder ans = new StringBuilder();
for (int i = 0; i < s.length(); ++i) {
if (s.charAt(i) == '(') {
int j = s.indexOf(')', i + 1);
ans.append(d.getOrDefault(s.substring(i + 1, j), "?"));
i = j;
} else {
ans.append(s.charAt(i));
}
}
return ans.toString();
}
}复杂度
时间
O(n + m)
n 是 s 的长度,m 是 knowledge 里所有键和值的总字符数。建表把 m 个字符过一遍;扫描时每个字符只经过常数次(找右括号是不重叠地往后走,整趟合起来仍是 O(n)),查表平均常数时间。两段相加是 O(n + m)
空间
O(n + m)
峰值占用两块:哈希表存下所有键值,是 O(m);拼结果的容器最终和输出等长,是 O(n)。两者都随输入变大而变大,峰值取二者之和 O(n + m),不是常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 替换字符串中的括号内容 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一遍扫描就够,不用反复回头?+
因为题目保证括号不嵌套,而且每个左括号都有配对的右括号。从左往右扫,遇到左括号就一路往右把键收到右括号为止,这段括号一次处理完,指针直接跳到右括号后面继续,不会再折回来。每个字符至多被读常数次,整趟就是一遍线性扫描。
知识清单为什么要搬进哈希表,直接在原清单里找不行吗?+
哈希表把「按键查值」做成平均常数时间。若每遇到一个键就在原始 knowledge 里从头找,单次是 m 量级,s 里括号一多,总代价会退化成括号数乘以清单长度,明显更慢。先花一次 m 的代价建表,之后每次查都是常数,整体更省。
如果题目改成允许嵌套括号,思路要怎么变?+
一遍平扫就不够了,因为里层括号得先算出来再拼给外层。通用做法是用一个栈:遇到左括号压栈、另开一段收集区,遇到右括号弹栈,把里层已经替换好的结果并回外层;或者写成递归,从最内层往外层逐层求值。本题明说了不嵌套,才可以用最省事的一遍扫描。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 替换字符串中的括号内容 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。