一个有趣的知乎问题实现

前几天在知乎上看到了这样一个有趣的问题:

男友让我打十万个「对不起」,汉字标上多少遍。如何快速实现?
链接: https://www.zhihu.com/question/27229082/answer/369776555

感觉挺有意思,毕竟题目要求“汉字标上”,所以是不能用下面这种简单的写法了:

1
2
3
4
5
public static void saySorry() {
for (int i = 1; i < 10001; i++) {
System.out.println("对不起," + i)
}
}

实际上,题目的难点也就是在于如何把0到10000的阿拉伯数字(即 int )转化为中文数字表达(即 String )。在不借用外部类库的情况下,我自己用遍历的方法写出了一个可以表达 1 到 99999 中文表达的Java实现。由于iteration的方便性,我的实现目前来看比大多数知乎回答要更加精简。

I: Test Case


1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
System.out.println(num2Chinese(3));
System.out.println(num2Chinese(30001));
System.out.println(num2Chinese(10203));
System.out.println(num2Chinese(39182));
}

输出:

三万零一
一万零二百零三
三万九千一百八十二


可以看出,上述程序很好的实现了中文语义,包括数字后“万”,“千”等单位的添加以及“零”作为补足。那么我们下面就来看一下实现这样一个程序需要考虑的需求有哪些。

II: Requirements

  1. 对于每一数位,都需要将数位上的数字转化为 中文数字+单位 的形式。比如9325,那么千位上的 9 要变成“九千”,其中“九”为数字,“千”为单位。注意这一个需求在各位数的时候要忽略,毕竟上面例子中的 5 输出就为“五”而非“五一”。
  2. 如果数位中有 0 的存在,我们要分为两种情况,第一种就是在非0数字之间出现0。例如 9034,那么要做到的就是在“九千”后面加上一个“零”,然后继续输出“34”。注意就是输出0的时候不需要再输出数位的单位,这一点与 (1)冲突,所以在程序中会使用 if - else 的方式处理。
  3. 然而上一点又有一个特殊情况! 考虑这一个例子:90034。根据中文语义应该输出“九万零三十四”。看得出来,虽然中间有两个零(千位和百位),但是实际输出只输出一个。那么程序中要做的就是判断在之前已经添加了一个“零”了,如果是则不继续添加。但如果两个零所在位置不连续,则两个零都要添加:比如 90203 应该输出“九万零二百零三”。所以我们会设定一个全局的 boolean 值,每次添加零都进行判断,在添加了零之后变为false,然后在添加不为零的数字后恢复位true。
  4. 继续第 (2) 点的另外一个情况。如果是在数字末尾的0,比如32000,那么我们会完全忽视掉(“三万二千”)。这种情况只需要判断数字z最后一位是否为 0 就可以了。程序中我们可以在循环的过程中按照 1-3 的规则先走,完全忽略第4点,最后在对要输出的字符串进行处理(删除掉末尾的“零”)。

我们下面来看看源代码。

III: Implementation


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.HashMap;
import java.util.Map;

public class printSorryWithChinese {

private static Map<Integer, String> map = new HashMap<>();

static {
map.put(0, "零");
map.put(1, "一");
map.put(2, "二");
map.put(3, "三");
map.put(4, "四");
map.put(5, "五");
map.put(6, "六");
map.put(7, "七");
map.put(8, "八");
map.put(9, "九");
map.put(10, "十");
map.put(100, "百");
map.put(1000, "千");
map.put(10000, "万");
}

public static String num2Chinese(int n)
{
StringBuffer buffer = new StringBuffer();
boolean addZero = true;
int input = n;

for (int num = 10000; num >=1; num = num / 10) {
int mod = n / num;
if (mod != 0) {
buffer.append(map.get(mod));

if (num != 1)
buffer.append(map.get(num));

addZero = true;
}
else if (buffer.length() != 0 && addZero && num != 1) {
buffer.append(map.get(0));
addZero = false;
}

n = n % num;
}

if ((input+"").endsWith("0")) {
return buffer.toString().substring(0, buffer.length()-1);
}

return buffer.toString();
}
}

在上面讲完需求之后,具体的实现就很简单了。下面基于代码讲一下具体的思路:

Step1: 创建并使用一个Map来承载数字=>中文的映射关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//一个整型对应字符串的 key-value HashMap
private static Map<Integer, String> map = new HashMap<>();

static {
map.put(0, "零");
map.put(1, "一");
map.put(2, "二");
map.put(3, "三");
map.put(4, "四");
map.put(5, "五");
map.put(6, "六");
map.put(7, "七");
map.put(8, "八");
map.put(9, "九");
map.put(10, "十");
map.put(100, "百");
map.put(1000, "千");
map.put(10000, "万");
}

注意使用 static 关键词的内容。这一代码段会在类加载阶段将内容注入到 map 中去。

Step2: 声明变量

1
2
3
4
5
6
//一个StringBuffer用于拼接字符串,在之前的文章中提过使用 “+” 运算符直接拼接速度很慢
StringBuffer buffer = new StringBuffer();
//上文中提到的boolean值,用于判断是否可以添加零
boolean addZero = true;
//由于传入的参数int本身会在操作中被改变,所以需要提前用一个新的int承载其原本的值
int input = n;

Step3: 循环处理(从万位到个位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//从万位开始,每一次除以 10 降低一位,实现循环
for (int num = 10000; num >=1; num = num / 10) {
//除以位数后的商即是该位数上的数字
int mod = n / num;
//商不等于0时,比如 90000/10000 = 9
if (mod != 0) {
buffer.append(map.get(mod));
//上面说过,除了个位数,都要添加代表位数的单位
if (num != 1)
buffer.append(map.get(num));
//如果不是零的情况,要将boolean恢复为true(上面第三点)
addZero = true;
}
//商是零的情况,如 9000/10000 = 0
//进行判断,要求添加的零不能作为返回字符串的开头,并且addZero允许添加零,而且位数不为各位
else if (buffer.length() != 0 && addZero && num != 1) {
buffer.append(map.get(0));
//将addZero调回为false
addZero = false;
}
//改变n的值,判断下一位。如95320变为5320。
n = n % num;
}

Step4: 删除末尾的零
这一步很简单,看代码就好了

1
2
3
4
5
if ((input+"").endsWith("0")) {
return buffer.toString().substring(0, buffer.length()-1);
}
//最后返回字符串
return buffer.toString();

最后总结:


这个小程序是在AP CS课上无聊完成的。
对于这种逻辑相对复杂,要求繁琐的任务,知乎上很多的人直接使用了硬算的办法来解决。这样做的缺点是很大的,比如下次要扩展到一亿或者更大,要改动的地方就很多了。
反而,我们要先观察其中的规律,然后想想递归/遍历是否能够完成这一任务。最重要的是要将学会的设计模式运用在其中。