726. 原子的数量
题目描述
输入字符串 formula,它是一个合法化学式。统计其中每种原子的总个数。
原子名由一个大写字母开头,后接零个或多个小写字母。原子名后面可以跟一个数字表示个数,个数为 1 时不写数字。几个化学式直接拼接仍是化学式;一段化学式用括号括起来后可以再跟一个数字,表示括号内的整体重复这么多次。
返回一个字符串:按原子名字典序依次写出每种原子,名字后紧跟它的总数;总数为 1 时只写名字。
输入:formula = "H2O" 输出:"H2O"
解释:H 有 2 个,O 有 1 个。
输入:formula = "Mg(OH)2" 输出:"H2MgO2"
解释:H 2 个、Mg 1 个、O 2 个,按字典序为 H2MgO2。
输入:formula = "K4(ON(SO3)2)2" 输出:"K4N2O14S4"
解释:展开括号后 K 4 个、N 2 个、O 14 个、S 4 个。
数据范围
1 <= formula.length <= 1000formula只含英文字母、数字、(和)。formula一定是合法的化学式。
解题思路:栈 + 哈希表
用一个“计数器栈”处理嵌套:栈中每层是一个哈希表,记录这一层括号里的原子个数。遇到 ( 压入一个空表;遇到原子就读出名字和后面的数字,累加到栈顶表;遇到 ) 就读出它后面的倍数 mult,弹出栈顶表,把里面每个原子的个数乘以 mult 后合并到新的栈顶。扫完后栈底就是总计数,按名字排序拼接即可。
为什么正确
栈的第 d 层记录的是“当前尚未闭合的第 d 层括号里,已经扫过的部分”的原子个数;栈底是括号外的部分。
复杂度分析
时间 O(n·d + m log m),d 为括号嵌套深度(每次合并可能搬动一层的全部原子),m 为原子种类数;空间 O(n)。
易错点
原子名可能是多个字母(Mg、Be),数字也可能是多位(Be32),都要循环读完;数字省略时个数为 1。输出时个数为 1 不写数字。括号后的倍数要乘到括号内的每一种原子上,而不是只乘最后一个。
参考代码
Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。
class Solution:
def countOfAtoms(self, formula: str) -> str:
stack = [collections.Counter()]
i, n = 0, len(formula)
while i < n:
if formula[i] == '(':
stack.append(collections.Counter())
i += 1
elif formula[i] == ')':
i += 1
j = i
while i < n and formula[i].isdigit():
i += 1
mult = int(formula[j:i] or 1)
top = stack.pop()
for name, c in top.items():
stack[-1][name] += c * mult
else:
j = i
i += 1
while i < n and formula[i].islower():
i += 1
name = formula[j:i]
j = i
while i < n and formula[i].isdigit():
i += 1
stack[-1][name] += int(formula[j:i] or 1)
cnt = stack[0]
return ''.join(name + (str(cnt[name]) if cnt[name] > 1 else '') for name in sorted(cnt))class Solution {
int i, n;
String f;
long readNum() {
int j = i;
while (i < n && Character.isDigit(f.charAt(i))) i++;
return j == i ? 1 : Long.parseLong(f.substring(j, i));
}
public String countOfAtoms(String formula) {
Deque<Map<String, Long>> stack = new ArrayDeque<>();
stack.push(new HashMap<>());
f = formula; i = 0; n = formula.length();
while (i < n) {
char ch = f.charAt(i);
if (ch == '(') {
stack.push(new HashMap<>());
i++;
} else if (ch == ')') {
i++;
long mult = readNum();
Map<String, Long> top = stack.pop();
for (Map.Entry<String, Long> e : top.entrySet()) {
stack.peek().merge(e.getKey(), e.getValue() * mult, Long::sum);
}
} else {
int j = i;
i++;
while (i < n && Character.isLowerCase(f.charAt(i))) i++;
String name = f.substring(j, i);
stack.peek().merge(name, readNum(), Long::sum);
}
}
TreeMap<String, Long> cnt = new TreeMap<>(stack.peek());
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Long> e : cnt.entrySet()) {
sb.append(e.getKey());
if (e.getValue() > 1) sb.append(e.getValue());
}
return sb.toString();
}
}class Solution {
public:
string countOfAtoms(string formula) {
vector<map<string, long long>> stack(1);
int i = 0, n = formula.size();
auto readNum = [&]() {
int j = i;
while (i < n && isdigit(formula[i])) i++;
return j == i ? 1LL : stoll(formula.substr(j, i - j));
};
while (i < n) {
if (formula[i] == '(') {
stack.emplace_back();
i++;
} else if (formula[i] == ')') {
i++;
long long mult = readNum();
auto top = stack.back();
stack.pop_back();
for (auto& [name, c] : top) {
stack.back()[name] += c * mult;
}
} else {
int j = i;
i++;
while (i < n && islower(formula[i])) i++;
string name = formula.substr(j, i - j);
stack.back()[name] += readNum();
}
}
string res;
for (auto& [name, c] : stack[0]) {
res += name;
if (c > 1) res += to_string(c);
}
return res;
}
};func countOfAtoms(formula string) string {
stack := []map[string]int{{}}
i, n := 0, len(formula)
readNum := func() int {
j := i
for i < n && formula[i] >= '0' && formula[i] <= '9' {
i++
}
if j == i {
return 1
}
v, _ := strconv.Atoi(formula[j:i])
return v
}
for i < n {
if formula[i] == '(' {
stack = append(stack, map[string]int{})
i++
} else if formula[i] == ')' {
i++
mult := readNum()
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
for name, c := range top {
stack[len(stack)-1][name] += c * mult
}
} else {
j := i
i++
for i < n && formula[i] >= 'a' && formula[i] <= 'z' {
i++
}
name := formula[j:i]
stack[len(stack)-1][name] += readNum()
}
}
cnt := stack[0]
names := make([]string, 0, len(cnt))
for name := range cnt {
names = append(names, name)
}
sort.Strings(names)
var sb strings.Builder
for _, name := range names {
sb.WriteString(name)
if cnt[name] > 1 {
sb.WriteString(strconv.Itoa(cnt[name]))
}
}
return sb.String()
}var countOfAtoms = function(formula) {
const stack = [new Map()];
let i = 0; const n = formula.length;
const add = (m, k, v) => m.set(k, (m.get(k) || 0) + v);
const readNum = () => {
const j = i;
while (i < n && formula[i] >= '0' && formula[i] <= '9') i++;
return j === i ? 1 : Number(formula.slice(j, i));
};
while (i < n) {
if (formula[i] === '(') {
stack.push(new Map());
i++;
} else if (formula[i] === ')') {
i++;
const mult = readNum();
const top = stack.pop();
for (const [name, c] of top) {
add(stack[stack.length - 1], name, c * mult);
}
} else {
const j = i;
i++;
while (i < n && formula[i] >= 'a' && formula[i] <= 'z') i++;
const name = formula.slice(j, i);
add(stack[stack.length - 1], name, readNum());
}
}
const cnt = stack[0];
return [...cnt.keys()].sort().map(name => name + (cnt.get(name) > 1 ? cnt.get(name) : '')).join('');
};function countOfAtoms(formula: string): string {
const stack: Map<string, number>[] = [new Map()];
let i = 0; const n = formula.length;
const add = (m: Map<string, number>, k: string, v: number) => m.set(k, (m.get(k) || 0) + v);
const readNum = (): number => {
const j = i;
while (i < n && formula[i] >= '0' && formula[i] <= '9') i++;
return j === i ? 1 : Number(formula.slice(j, i));
};
while (i < n) {
if (formula[i] === '(') {
stack.push(new Map());
i++;
} else if (formula[i] === ')') {
i++;
const mult = readNum();
const top = stack.pop()!;
for (const [name, c] of top) {
add(stack[stack.length - 1], name, c * mult);
}
} else {
const j = i;
i++;
while (i < n && formula[i] >= 'a' && formula[i] <= 'z') i++;
const name = formula.slice(j, i);
add(stack[stack.length - 1], name, readNum());
}
}
const cnt = stack[0];
return [...cnt.keys()].sort().map(name => name + (cnt.get(name)! > 1 ? cnt.get(name) : '')).join('');
}其他解法
栈 + 哈希表和递归下降是同一种分层合并的两种实现,都要逐层搬动计数;从右向左扫描先知道倍数,可以一遍直接累加到结果。
解法 2:递归下降解析 时间 O(n·d + m log m),空间 O(n)
写一个 parse 函数,从当前位置读到右括号或末尾为止,返回这一段的原子计数。遇到原子就读名字和数字并累加。遇到左括号就递归调用 parse 得到子计数,跳过右括号后读倍数,乘上后合并。
把显式栈换成了系统调用栈,结构和文法一一对应,更容易扩展到更复杂的文法。嵌套很深时受递归深度限制。
class Solution:
def countOfAtoms(self, formula: str) -> str:
n = len(formula)
i = 0
def read_num():
nonlocal i
j = i
while i < n and formula[i].isdigit():
i += 1
return int(formula[j:i] or 1)
def parse():
nonlocal i
cnt = collections.Counter()
while i < n and formula[i] != ')':
if formula[i] == '(':
i += 1
sub = parse()
i += 1
mult = read_num()
for name, c in sub.items():
cnt[name] += c * mult
else:
j = i
i += 1
while i < n and formula[i].islower():
i += 1
name = formula[j:i]
cnt[name] += read_num()
return cnt
cnt = parse()
return ''.join(name + (str(cnt[name]) if cnt[name] > 1 else '') for name in sorted(cnt))class Solution {
String f;
int i, n;
int readNum() {
if (i >= n || !Character.isDigit(f.charAt(i))) return 1;
int v = 0;
while (i < n && Character.isDigit(f.charAt(i))) v = v * 10 + (f.charAt(i++) - '0');
return v;
}
Map<String, Integer> parse() {
Map<String, Integer> cnt = new HashMap<>();
while (i < n && f.charAt(i) != ')') {
if (f.charAt(i) == '(') {
i++;
Map<String, Integer> sub = parse();
i++;
int mult = readNum();
for (Map.Entry<String, Integer> e : sub.entrySet()) cnt.merge(e.getKey(), e.getValue() * mult, Integer::sum);
} else {
int j = i++;
while (i < n && Character.isLowerCase(f.charAt(i))) i++;
String name = f.substring(j, i);
cnt.merge(name, readNum(), Integer::sum);
}
}
return cnt;
}
public String countOfAtoms(String formula) {
f = formula;
n = f.length();
i = 0;
TreeMap<String, Integer> cnt = new TreeMap<>(parse());
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Integer> e : cnt.entrySet()) {
sb.append(e.getKey());
if (e.getValue() > 1) sb.append(e.getValue());
}
return sb.toString();
}
}class Solution {
public:
string f;
int i = 0, n = 0;
int readNum() {
if (i >= n || !isdigit(f[i])) return 1;
int v = 0;
while (i < n && isdigit(f[i])) v = v * 10 + (f[i++] - '0');
return v;
}
map<string, int> parse() {
map<string, int> cnt;
while (i < n && f[i] != ')') {
if (f[i] == '(') {
i++;
map<string, int> sub = parse();
i++;
int mult = readNum();
for (auto& [name, c] : sub) cnt[name] += c * mult;
} else {
int j = i++;
while (i < n && islower(f[i])) i++;
string name = f.substr(j, i - j);
cnt[name] += readNum();
}
}
return cnt;
}
string countOfAtoms(string formula) {
f = formula;
n = f.size();
i = 0;
map<string, int> cnt = parse();
string res;
for (auto& [name, c] : cnt) {
res += name;
if (c > 1) res += to_string(c);
}
return res;
}
};func countOfAtoms(formula string) string {
n, i := len(formula), 0
readNum := func() int {
if i >= n || formula[i] < '0' || formula[i] > '9' {
return 1
}
v := 0
for i < n && formula[i] >= '0' && formula[i] <= '9' {
v = v*10 + int(formula[i]-'0')
i++
}
return v
}
var parse func() map[string]int
parse = func() map[string]int {
cnt := map[string]int{}
for i < n && formula[i] != ')' {
if formula[i] == '(' {
i++
sub := parse()
i++
mult := readNum()
for name, c := range sub {
cnt[name] += c * mult
}
} else {
j := i
i++
for i < n && formula[i] >= 'a' && formula[i] <= 'z' {
i++
}
name := formula[j:i]
cnt[name] += readNum()
}
}
return cnt
}
cnt := parse()
names := make([]string, 0, len(cnt))
for name := range cnt {
names = append(names, name)
}
sort.Strings(names)
var sb strings.Builder
for _, name := range names {
sb.WriteString(name)
if cnt[name] > 1 {
sb.WriteString(strconv.Itoa(cnt[name]))
}
}
return sb.String()
}var countOfAtoms = function(formula) {
const n = formula.length;
let i = 0;
const isDigit = (c) => c >= '0' && c <= '9';
const readNum = () => {
const j = i;
while (i < n && isDigit(formula[i])) i++;
return j === i ? 1 : parseInt(formula.slice(j, i), 10);
};
const parse = () => {
const cnt = new Map();
while (i < n && formula[i] !== ')') {
if (formula[i] === '(') {
i++;
const sub = parse();
i++;
const mult = readNum();
for (const [name, c] of sub) cnt.set(name, (cnt.get(name) || 0) + c * mult);
} else {
const j = i++;
while (i < n && formula[i] >= 'a' && formula[i] <= 'z') i++;
const name = formula.slice(j, i);
cnt.set(name, (cnt.get(name) || 0) + readNum());
}
}
return cnt;
};
const cnt = parse();
const names = [...cnt.keys()].sort();
return names.map(name => name + (cnt.get(name) > 1 ? cnt.get(name) : '')).join('');
};function countOfAtoms(formula: string): string {
const n = formula.length;
let i = 0;
const isDigit = (c: string): boolean => c >= '0' && c <= '9';
const readNum = (): number => {
const j = i;
while (i < n && isDigit(formula[i])) i++;
return j === i ? 1 : parseInt(formula.slice(j, i), 10);
};
const parse = (): Map<string, number> => {
const cnt = new Map<string, number>();
while (i < n && formula[i] !== ')') {
if (formula[i] === '(') {
i++;
const sub = parse();
i++;
const mult = readNum();
for (const [name, c] of sub) cnt.set(name, (cnt.get(name) || 0) + c * mult);
} else {
const j = i++;
while (i < n && formula[i] >= 'a' && formula[i] <= 'z') i++;
const name = formula.slice(j, i);
cnt.set(name, (cnt.get(name) || 0) + readNum());
}
}
return cnt;
};
const cnt = parse();
const names = [...cnt.keys()].sort();
return names.map(name => name + (cnt.get(name)! > 1 ? cnt.get(name) : '')).join('');
}解法 3:从右向左扫描 时间 O(n + m log m),空间 O(n)
从右往左读化学式,先读到的数字是它左边原子或括号的倍数。用一个栈保存“当前累计倍数”:遇到右括号压入栈顶倍数乘以待用数字,遇到左括号弹出。遇到原子时把栈顶倍数乘以待用数字直接加到总计数里。
每个原子只累加一次,不需要逐层合并哈希表,所以与嵌套深度无关。反向读原子名和数字时边界容易写错。
class Solution:
def countOfAtoms(self, formula: str) -> str:
cnt = collections.Counter()
stack = [1]
num = 1
i = len(formula) - 1
while i >= 0:
c = formula[i]
if c.isdigit():
j = i
while i >= 0 and formula[i].isdigit():
i -= 1
num = int(formula[i + 1:j + 1])
elif c == ')':
stack.append(stack[-1] * num)
num = 1
i -= 1
elif c == '(':
stack.pop()
i -= 1
else:
j = i
while formula[i].islower():
i -= 1
cnt[formula[i:j + 1]] += stack[-1] * num
num = 1
i -= 1
return ''.join(name + (str(cnt[name]) if cnt[name] > 1 else '') for name in sorted(cnt))class Solution {
public String countOfAtoms(String formula) {
TreeMap<String, Integer> cnt = new TreeMap<>();
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
int num = 1;
int i = formula.length() - 1;
while (i >= 0) {
char c = formula.charAt(i);
if (Character.isDigit(c)) {
int j = i;
while (i >= 0 && Character.isDigit(formula.charAt(i))) i--;
num = Integer.parseInt(formula.substring(i + 1, j + 1));
} else if (c == ')') {
stack.push(stack.peek() * num);
num = 1;
i--;
} else if (c == '(') {
stack.pop();
i--;
} else {
int j = i;
while (Character.isLowerCase(formula.charAt(i))) i--;
cnt.merge(formula.substring(i, j + 1), stack.peek() * num, Integer::sum);
num = 1;
i--;
}
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Integer> e : cnt.entrySet()) {
sb.append(e.getKey());
if (e.getValue() > 1) sb.append(e.getValue());
}
return sb.toString();
}
}class Solution {
public:
string countOfAtoms(string formula) {
map<string, int> cnt;
vector<int> stack = {1};
int num = 1;
int i = formula.size() - 1;
while (i >= 0) {
char c = formula[i];
if (isdigit(c)) {
int j = i;
while (i >= 0 && isdigit(formula[i])) i--;
num = stoi(formula.substr(i + 1, j - i));
} else if (c == ')') {
stack.push_back(stack.back() * num);
num = 1;
i--;
} else if (c == '(') {
stack.pop_back();
i--;
} else {
int j = i;
while (islower(formula[i])) i--;
cnt[formula.substr(i, j - i + 1)] += stack.back() * num;
num = 1;
i--;
}
}
string res;
for (auto& [name, c] : cnt) {
res += name;
if (c > 1) res += to_string(c);
}
return res;
}
};func countOfAtoms(formula string) string {
cnt := map[string]int{}
stack := []int{1}
num := 1
isDigit := func(c byte) bool { return c >= '0' && c <= '9' }
i := len(formula) - 1
for i >= 0 {
c := formula[i]
if isDigit(c) {
j := i
for i >= 0 && isDigit(formula[i]) {
i--
}
num, _ = strconv.Atoi(formula[i+1 : j+1])
} else if c == ')' {
stack = append(stack, stack[len(stack)-1]*num)
num = 1
i--
} else if c == '(' {
stack = stack[:len(stack)-1]
i--
} else {
j := i
for formula[i] >= 'a' && formula[i] <= 'z' {
i--
}
cnt[formula[i:j+1]] += stack[len(stack)-1] * num
num = 1
i--
}
}
names := make([]string, 0, len(cnt))
for name := range cnt {
names = append(names, name)
}
sort.Strings(names)
var sb strings.Builder
for _, name := range names {
sb.WriteString(name)
if cnt[name] > 1 {
sb.WriteString(strconv.Itoa(cnt[name]))
}
}
return sb.String()
}var countOfAtoms = function(formula) {
const cnt = new Map();
const stack = [1];
let num = 1;
const isDigit = (c) => c >= '0' && c <= '9';
let i = formula.length - 1;
while (i >= 0) {
const c = formula[i];
if (isDigit(c)) {
const j = i;
while (i >= 0 && isDigit(formula[i])) i--;
num = parseInt(formula.slice(i + 1, j + 1), 10);
} else if (c === ')') {
stack.push(stack[stack.length - 1] * num);
num = 1;
i--;
} else if (c === '(') {
stack.pop();
i--;
} else {
const j = i;
while (formula[i] >= 'a' && formula[i] <= 'z') i--;
const name = formula.slice(i, j + 1);
cnt.set(name, (cnt.get(name) || 0) + stack[stack.length - 1] * num);
num = 1;
i--;
}
}
const names = [...cnt.keys()].sort();
return names.map(name => name + (cnt.get(name) > 1 ? cnt.get(name) : '')).join('');
};function countOfAtoms(formula: string): string {
const cnt = new Map<string, number>();
const stack = [1];
let num = 1;
const isDigit = (c: string): boolean => c >= '0' && c <= '9';
let i = formula.length - 1;
while (i >= 0) {
const c = formula[i];
if (isDigit(c)) {
const j = i;
while (i >= 0 && isDigit(formula[i])) i--;
num = parseInt(formula.slice(i + 1, j + 1), 10);
} else if (c === ')') {
stack.push(stack[stack.length - 1] * num);
num = 1;
i--;
} else if (c === '(') {
stack.pop();
i--;
} else {
const j = i;
while (formula[i] >= 'a' && formula[i] <= 'z') i--;
const name = formula.slice(i, j + 1);
cnt.set(name, (cnt.get(name) || 0) + stack[stack.length - 1] * num);
num = 1;
i--;
}
}
const names = [...cnt.keys()].sort();
return names.map(name => name + (cnt.get(name)! > 1 ? cnt.get(name) : '')).join('');
}