LeetCode 423. 从英文中重建数字
题目描述

题意分析
输入由若干个数字零到九的英文单词中的全部字母打乱组成,恢复这些数字,并按从小到大的顺序返回数字字符串。每个数字可以出现多次,输出也要保留对应次数。
字母已经整体打乱,不能按连续子串寻找单词,也不需要恢复原来数字的排列顺序。题目保证输入有效,因此可以利用各英文单词的字母组成直接推导数量;结果允许以零开头,应当作为字符串返回。
解法:唯一字母计数
核心思路
[!blue]
先统计每个字母的出现次数。虽然大部分字母被多个数字单词共享,但有五个字母只属于一个数字:
z只在zero,w只在two,u只在four,x只在six,g只在eight。它们在对应单词中又都只出现一次,所以能直接确定零、二、四、六、八的数量。确定一部分数字后,共享字母也可以继续使用。
h由three和eight贡献,扣掉八的数量就得到三;f由four和five贡献,扣掉四就得到五;s由six和seven贡献,扣掉六就得到七。字母
o出现在zero、one、two、four中,扣掉已经确定的零、二、四,就是一的数量。字母i出现在five、six、eight、nine中,扣掉五、六、八,才能得到九。这是一条按依赖关系求解的计数链。原字母频次表始终保留总次数,已知贡献通过公式相减,不需要真的把整个单词逐字删除;但九必须在五确定之后计算,否则会把五贡献的字母也算进去。输入有效保证这些推导得到的数量非负且能够完整还原。
求解顺序服务于消除歧义,输出顺序服务于题目的升序要求,两者不同。所有数量求完后,再从数字零枚举到九,按各自数量追加数字字符,得到最终字符串。
解题步骤
- 扫描字符串,统计二十六个小写字母的频次。
- 用唯一字母直接得到零、二、四、六、八的数量。
- 从共享字母中减去已知贡献,依次求出三、五、七、一、九。
- 按零到九的顺序,将每个数字重复输出对应次数。
代码实现
class Solution {
// z,w,u,x,g 分别只出现在 zero,two,four,six,eight 中,可以先确定这些数字数量。
public String originalDigits(String s) {
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
int[] nums = new int[10];
nums[0] = count['z' - 'a'];
nums[2] = count['w' - 'a'];
nums[4] = count['u' - 'a'];
nums[6] = count['x' - 'a'];
nums[8] = count['g' - 'a'];
// 唯一字母对应数字已确定,再扣掉共享字母的已知贡献
nums[3] = count['h' - 'a'] - nums[8];
nums[5] = count['f' - 'a'] - nums[4];
nums[7] = count['s' - 'a'] - nums[6];
nums[1] = count['o' - 'a'] - nums[0] - nums[2] - nums[4];
// 九还依赖刚求出的五,不能提前计算
nums[9] = count['i' - 'a'] - nums[5] - nums[6] - nums[8];
StringBuilder builder = new StringBuilder(s.length());
for (int i = 0; i < 10; i++) {
for (int j = 0; j < nums[i]; j++) {
builder.append(i);
}
}
return builder.toString();
}
}
func originalDigits(s string) string {
// z,w,u,x,g 分别只出现在 zero,two,four,six,eight 中,可以先确定这些数字数量。
count := make([]int, 26)
for i := 0; i < len(s); i++ {
count[s[i]-'a']++
}
nums := make([]int, 10)
nums[0] = count['z'-'a']
nums[2] = count['w'-'a']
nums[4] = count['u'-'a']
nums[6] = count['x'-'a']
nums[8] = count['g'-'a']
// 唯一字母对应数字已确定,再扣掉共享字母的已知贡献
nums[3] = count['h'-'a'] - nums[8]
nums[5] = count['f'-'a'] - nums[4]
nums[7] = count['s'-'a'] - nums[6]
nums[1] = count['o'-'a'] - nums[0] - nums[2] - nums[4]
// 九还依赖刚求出的五,不能提前计算
nums[9] = count['i'-'a'] - nums[5] - nums[6] - nums[8]
builder := make([]byte, 0, len(s))
for i := 0; i < 10; i++ {
for j := 0; j < nums[i]; j++ {
builder = append(builder, byte('0'+i))
}
}
return string(builder)
}
复杂度分析
- 时间复杂度:$O(n)$,
n为输入字母数。扫描和结果构造都是线性,十个数字的公式推导为常数工作。- 空间复杂度:$O(n)$,当前实现的字符处理与结果构造缓冲为线性空间;两个频次数组本身大小固定。
关键点总结
[!green]
- 先用唯一字母确定部分数字,再从共享字母中扣除已知贡献。
- 公式有依赖顺序,原频次表不变,不能一边整体减字母又重复使用这些扣减公式。
- 数量全部确定后才按升序输出,重复数字与前导零都保留。
易错点总结
[!yellow]
- 九的数量忘记减去八或五贡献的
i,会凭空增加数字九。- 五尚未确定就计算九,使用了未完成的依赖,导致计数错误。
- 按数字被推导出来的先后顺序输出,会破坏从小到大的要求。
- 先把已知单词从频次中扣除,再套用同样的减法公式,会把贡献重复扣掉。
- 将结果转换成整数,会丢掉应当保留的前导零,也没有必要。
- Java 把数字值直接强转为字符,得到的是字符编码而非十进制数字;当前代码使用
append(int)输出其数字表示。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 383. 赎金信 | 简单 | 同样按字母频次消耗需求,本题利用某些数字单词的唯一字母确定数量,再扣除已使用字符。 |
| 869. 重新排序得到 2 的幂 | 中等 | 原题枚举候选数并比较数码频次,本题从混合字母频次中逐步还原多个数字单词。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!