为了试试看我是否有能力处理 Hard 类型题目,就随机挑选了一道。是字符串处理问题。题意就是给出一个化学式,求出所有元素及其个数。
合理的化学式应该只包含大写字母、小写字母、左右括号和数字。而且一个化学元素名必然是大写字母开头,其余字母都是小写字母。
我的确输了。做是做出来了,但是时间太久了。
很明显需要用到栈,然后就是按顺序读字符串做处理了。我刚开始构造了一个树结构来存储一个化学式,树节点(一个基团)的定义是
function Group() {
this.children = [];
this.count = 0;
}
count是基团的个数,children是一个包含元素(字符串)或者其他基团(Group对象)的数组。
为了便于处理,对字符串中单个元素或基团加上了一个数字1,同时加上结尾指示符$。(这步其实没必要)
// Mg(OH)2 -> Mg1(O1H1)2
function process(str) {
str += '$';
let res = '';
let j = 0;
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (c >= 'A' && c <= 'Z' || c == '(' || c == ')' || c == '$') {
if (j - 1 >= 0 && !(res[j - 1] >= '0' && res[j - 1] <= '9') && res[j - 1] != '(') {
res += '1';
j++;
}
}
res += c;
j++;
}
return res;
}
然后核心逻辑
var countOfAtoms = function(formula) {
formula = process(formula);
let element = '';
let group = null;
let count = 0;
let i = 0;
let root = new Group();
const stack = [root];
while (i < formula.length) {
const c = formula[i];
if (c >= 'A' && c <= 'Z' || c == '$') {
if (i > 0) {
update();
}
element += c;
} else if (c >= 'a' && c <= 'z') {
element += c;
} else if (c >= '0' && c <= '9') {
count = count * 10 + parseInt(c);
} else if (c == '(') { //进入一个新的 group
if (i > 0) {
update();
}
root = new Group();
stack.push(root);
} else { // ')'
update();
group = stack.pop();
root = stack[stack.length - 1];
}
i++;
}
root = stack[0];
root.count = 1;
const obj = traverse(root);
return Object.keys(obj).sort().map(x => {
return x + (obj[x] == 1 ? '' : obj[x]);
}).join('');
}
其中这个update函数用来处理element和group两个变量到dict里去。
function update() {
if (element.length > 0) {
group = new Group();
//元素也当做一个基团处理
group.children.push(element);
group.count = count;
element = '';
}
if (group) {
root.children.push(group);
group.count = count;
group = null;
}
count = 0;
}
update方法调用时机为遇到大写字母、结束符、左右括号时,这些时候都代表即将结束当前基团或即将进入下个基团。
例如 K4(ON(SO3)2)2 构造出来的树是这样的:
+-------------+
| root |
| |
| count: 1 |
+--+------+---+
| |
+-----+ +------+
| |
+-----+---+ +----+-----+
| [K] | | [] |
| count:4 | | count: 2 |
+---------+ +-+--+---+-+
| | |
+-----------+ | +---------+
| | |
| | |
+----+----+ +---+-----+ +----+-----+
| [O] | | [N] | | [] |
| count:1 | | count:1 | | count:2 |
+---------+ +---------+ +--+--+----+
| |
+-----------+ |
| |
| |
+----+----+ +---+-----+
| [S] | | [O] |
| count:1 | | count:3 |
+---------+ +---------+
接下来就是对树进行后序遍历(?),把子节点上的所有元素个数统计到根节点上,返回一个{element: count}字典。
function traverse(root) {
if (!(root instanceof Group)) {
return {
[root]: 1
};
}
let sum = {};
for (const sub of root.children) {
const res = traverse(sub);
for (const key of Object.keys(res)) {
if (sum[key]) {
sum[key] += res[key];
} else {
sum[key] = res[key];
}
}
}
for (const key of Object.keys(sum)) {
sum[key] *= root.count;
}
return sum;
}
我觉得思路上是没有问题的,但是对于这道题来说太麻烦了……这绝对是不能用来面试的。
经过考虑,我觉得转换成树这一步是没必要的,栈里面直接存字典就好了。核心逻辑没有改变,update的变化也不大。代码改成了:
/**
* @param {string} formula
* @return {string}
*/
var countOfAtoms = function(formula) {
formula += '$';
const stack = [];
let dict = {}, count = 0, element = '', group = null; //group不再是树节点,而是{}
for(const c of formula) {
if (c >= 'A' && c <= 'Z') {
update();
element += c;
} else if (c >= 'a' && c <= 'z') {
element += c;
} else if (c >= '0' && c <= '9') {
count = count * 10 + parseInt(c);
} else if (c == '(') {
update();
stack.push(dict);
dict = {};
} else { //处理结尾符的逻辑放到了这里
update();
group = dict; //这是 group 唯一的赋值处
dict = stack.pop();
}
}
//使用 reduce 代码更漂亮了,注意要有初始字符串''
return Object.keys(group).sort().reduce((s, x) => s + x + (group[x] == 1 ? '' : group[x]), '');
function update() {
if (count == 0) count = 1;
if (element.length > 0) {
if (dict[element]) {
dict[element] += count;
} else {
dict[element] = count;
}
element = '';
}
if (group) { //只有刚刚处理完')'或者到结尾,group 才不是 null
group.times(count);
dict.merge(group);
group = null;
}
count = 0;
}
}
Object.prototype.merge = function(dict) {
for (const key of Object.keys(dict)) {
if (this[key]) {
this[key] += dict[key];
} else {
this[key] = dict[key];
}
}
}
Object.prototype.times = function(n) {
for (const key of Object.keys(this)) {
this[key] *= n;
}
}
尽管代码更加清晰了,但是多处调用update看着还是很麻烦。还能再优雅一些吗?我参考了最快的(48 ms)代码示例,改写成了下面的样子:
var countOfAtoms = function(formula) {
let dic = {};
const stk = [1]; //stack 顶数代表当前处理的基团/元素的乘数
for(let i = formula.length - 1; i >= 0; i--) {
if(formula[i]=='(') {
stk.pop();
continue;
}
const t = stk[stk.length-1];
let j = i;
while (formula[i] >= '0' && formula[i] <= '9') {
i--;
}
const n = t * ((i < j) ? Number(formula.substring(i + 1, j + 1)) : 1);
if(formula[i] == ')') {
stk.push(n);
continue;
}
j = i
while (formula[i] >= 'a' && formula[i] <= 'z'){
i--;
}
const s = formula.substring(i, j + 1);
dic[s] = dic[s] || 0;
dic[s] += n;
}
return Object.keys(dic).sort()
.reduce((s, c) => s + (dic[c] > 1 ? c + dic[c] : c), '');
};
他的做法挺巧的,从右往左处理,因为基团或元素的个数是写在右边的,所以这种方法更好。看懂比较容易,一次性写对不易,可能会有些细节上的坑。