Exercise 1-13. Write a program to print a histogram of the lengths of words in its input. It is easy to draw the histogram with the bars horizontal; a vertical orientation is more challenging.
比起之前的几道题,这一题开始作者给我们上难度了。作者让我们输出读取到的(伪)单词长度的直方图。笔者认为作者的意思是:
- 先弄清楚输入内容中,有多少种不同的输入(伪)单词长度,比如出现了长度为 5 的(伪)单词、长度为8的(伪)单词等等;
- 统计在输入中每种(伪)单词长度分别有多少个(伪)单词,比如长度为 5 的(伪)单词出现了几次,长度为 8 的(伪)单词又出现了几次。笔者认为在计数的时候,对出现了多次的同一单词,应该进行重复计数;
- 对上面的统计结果输出直方图。(有水平和垂直两种,但是垂直的稍有难度)
不管是输出水平直方图还是垂直直方图,首先我们都必须解决统计上的问题。因此我们需要一个数组,来存放输入中不同的(伪)单词长度对应的(伪)单词次数。
在进入 main() 函数之前,因为我们只能设置定长数组,所以我们不得不给能纳入我们计算的(伪)单词长度设置一个上限。在这里笔者设置的上限是 20 。即我们只统计长度在 20 和 20 以内的(伪)单词个数。所以我们先设置一个宏确定上限:
#define WORD_MAX_LENGTH 20
由于我们要计算每个(伪)单词的长度,也就是重复多次“ 1, 2,··· ”这样的过程。我们必须得告诉计算机,什么时候是一个新的(伪)单词,要重新从 1 开始计数;以及什么时候计算完了一个(伪)单词的长度,要给相应的(伪)单词长度对应的(伪)单词个数的统计结果 +1 。笔者这里定义了两个宏用来表示读单词的状态:
#define OUT 0
#define IN 1
接着我们进入 main() 函数(头文件啥的应该都不会忘记吧,不会真有人忘记了吧)。
首先我们先得选取一种形式来存放我们的统计结果。从抽象上来说,我们统计的结果应该是形如 这样的二元有序数对。而一维数组从抽象上恰好也可以视为
这样的二元有序数对,所以一维数组对我们来说是一个很好的选择。
int wordLength[WORD_MAX_LENGTH + 1];
因为笔者想让 wordLength[i] 来记录长为 i 的(伪)单词个数,如果我们设置数组长度为 WORD_MAX_LENGTH 的话,数组的最后一个下标是 WORD_MA_LENGTH - 1 ,所以我们设置的数组长度是 WORD_MAX_LENGTH + 1 。(记得所有元素初始化为0)
因为我们记录(伪)单词长度的方法是:
每当进入一个新的(伪)单词,就开始计算字符的个数,直到遇到空白符为止。
所以我们需要一个变量用来当作是否进入新的(伪)单词、以及是否发现空白的标记。并且用一个变量来记录(伪)单词的长度。由于我们一开始不在任何(伪)单词之内,所以我们对他们进行如下的初始化:
int state, len;
state = OUT;
len = 0;
如果我们还想计算有多少(伪)单词的长度超过了我们给定的范围——20。我们还需要一个新的变量来存放超长的(伪)单词个数。
int overLen = 0;
接着我们就可以开始读取字符并进行计数了。由于根据当前字符是空白符还是非空白符,以及 state 状态是 IN 还是 OUT ,我们分为以下几种情况:
- 读到空白符
- state == IN: 表示前一个读到的字符是非空白,因此现在是从非空白刚进入到空白的阶段,这个时候意味着我们读完了一个(伪)单词,如果此时 len 小于我们设置的上限20,我们应该将数组中对应该长度的值加 1 ,表示新纪录了一个具有该长度的(伪)单词。我们还应该将 state 改成 OUT ,刷新目前状态,并且将 len 赋值为0,为下一次计算作准备。(如果 len > 20, 那么 overLen++ 。)
- state == OUT: 表示前一个读到的字符还是空白,此时我们不需要做什么。
- 读到非空白符
- state == IN:表示前一个读到的字符是非空白,我们目前还在某个(伪)单词的内部,我们只需要给 len 加 1 。刷新我们在这个(伪)单词内遇到的字符个数。
- state == OUT: 表示前一个读到的字符是空白,说明此时我们遇到了一个新单词的开头,我们需要将 len 加一,并且刷新 state 为 IN 状态。
这部分代码如下:
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\t' || c == '\n') {
wordLength[0]++;
if(state == IN) {
if (len > WORD_MAX_LENGTH)
overLen++;
else
++wordLength[len];
len = 0;
}
state = OUT;
} else {
if (state == OUT) {
len++;
state = IN;
} else
len++;
}
}
然后我们开始输出直方图(水平)。
很简单,我们用 for 循环遍历之前的数组,每个下标元素对应一行输出:
- 如果该下标元素为0,说明在输入中没有遇到具有该长度的(伪)单词;
- 如果该下标元素大于0, 说明在输入中该长度的(伪)单词出过,出现的次数恰好等于该下标元素值。我们这时候只需要用一个 while 循环打印 ‘-’ 即可,打印次数恰好等于该下标元素值。
也就是这样:
for (i = 1; i < WORD_MAX_LENGTH + 1; i++) {
printf("%2d - %2d : ", i, wordLength[i]);
while (wordLength[i]) {
printf("-");
wordLength[i]--;
}
printf("\n");
}
到此我们的程序也就算完成了。(注意记得 return 0 )
完整的代码如下:
#include <stdio.h>
#define OUT 0
#define IN 1
#define WORD_MAX_LENGTH 20
int main() {
int i, c, state, len, overLen;
int wordLength[WORD_MAX_LENGTH + 1];
state = OUT;
len = 0;
overLen = 0;
for (i = 0; i < WORD_MAX_LENGTH + 1; i++)
wordLength[i] = 0;
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\t' || c == '\n') {
wordLength[0]++;
if(state == IN) {
if (len > WORD_MAX_LENGTH)
overLen++;
else
++wordLength[len];
len = 0;
}
state = OUT;
} else {
if (state == OUT) {
len++;
state = IN;
} else
len++;
}
}
for (i = 1; i < WORD_MAX_LENGTH + 1; i++) {
printf("%2d - %2d : ", i, wordLength[i]);
while (wordLength[i]) {
printf("-");
wordLength[i]--;
}
printf("\n");
}
return 0;
}
输出结果如下:
> Exercise1_13_1 < Exercise1_13_1.c
1 - 41 : -----------------------------------------
2 - 29 : -----------------------------
3 - 13 : -------------
4 - 12 : ------------
5 - 6 : ------
6 - 6 : ------
7 - 4 : ----
8 - 3 : ---
9 - 1 : -
10 - 2 : --
11 - 1 : -
12 - 1 : -
13 - 2 : --
14 - 0 :
15 - 5 : -----
16 - 3 : ---
17 - 0 :
18 - 1 : -
19 - 0 :
20 - 0 :
至于如何输出垂直的直方图,我们下回再说。