Распечатайте частоту символов в виде вертикального графика

Решал K&R 1-14.

Упражнение 1-14. Напишите программу для печати гистограммы частот различных символов во входных данных.

После нескольких правок теперь мне это нравится. Но я впервые пишу длинный код (для меня он длинный). Интересно, есть ли более простой и короткий код с той же логикой, или я написал код плохого стиля или нет. Мне также интересно, выглядит ли мой вертикальный график хорошо и хорошо ли написан. Любое предложение будет оценено по достоинству!

#include <stdio.h>

/*  print a graph of the frequencies of different characters */

#define GRAPH_HEIGHT 20

main()
{
    int af[26] = {0};
    int max = 1;
    int c;
    int i, j;

    /* get characters */
    while ((c = getchar()) != EOF) {
        if (c >= 'a' && c <= 'z') 
            if (++af[c - 'a'] > max) {
                ++max;
                continue;
            }
        if (c >= 'A' && c <= 'Z')
            if (++af[c - 'A'] > max)
                ++max;
    }

    /* calculate a relative frequencies */
    for (i = 0; i < 26; ++i) {
        af[i] = GRAPH_HEIGHT * af[i] / max;
    }

    /* print characters on a graph */
    for (i = GRAPH_HEIGHT; i > 0; --i) {
        printf("%2d |", i);
        for (j = 'a'; j <= 'z'; ++j) {
            if (af[j - 'a'] == i) {
                printf(" | ");
                --af[j - 'a'];
            }
            else
                printf("   ");
        }
        printf("n");
    }

    printf("   " "*" "---------------" "---------------" "---------------" "---------------" "---------------" "---"  "n");
    printf("   ");
    for (i = 'a'; i <= 'z'; ++i) {
        printf("%3c", i);
    }

    printf("n");
}

1 ответ
1

Это неплохое начало. Вы должны быть предупреждены, что K&R описывает очень раннюю версию C, и этот язык значительно изменился с 1980-х годов.

Например, мы обычно всегда пишем объявления функций как прототипы, т.е. указание типов аргументов, и мы не зависим от неявных int. Так:

int main(void)

Кроме того, нам больше не нужно объявлять все переменные в начале их области видимости; вместо этого мы можем объявить их там, где они инициализированы:

for (int i = 0;  i < 26;  ++i)

Хотя в формулировке задачи говорится, что вы должны нанести на диаграмму частоту различных символов, мы реализовали здесь кое-что другое, потому что мы считаем только символы между a и z и между A и Z.

Глядя на это, в этом коде есть непереносимое предположение, что 'Z'-'A' и 'z'-'a' оба равны 25. Это верно для систем ASCII, но определенно не верно для систем EBCDIC, например, где этот код будет индексировать af за его пределами.

Вы можете решить эту проблему, объявив af иначе:

#define AZ_COUNT ('z' - 'a' + 1)
unsigned int af[AZ_COUNT] = {0};

Думаю, я бы сохранил количество всех разных символов, а позже отфильтровал бы только алфавитные (и использовал isalpha() для этого от <ctype.h>):

#include <limits.h>
unsigned int af[UCHAR_MAX+1] = {0};

Это может облегчить адаптацию печатной части (например, для отображения цифр и знаков препинания) позже. (Обратите внимание на использование unsigned intтоже — отрицательный счет не имеет смысла).

Я думаю, что печатная часть была бы лучше, если бы она не меняла счетчики. Мы можем гарантировать, что если разделим подсчет и печать на отдельные функции:

void count_chars(FILE *in, unsigned int *frequencies);
void print_graph(FILE *out, const unsigned int *frequencies);

Это const помогает нам писать код, который можно использовать повторно.


С моими изменениями код выглядит примерно так (не идеально, но я надеюсь, что в нем есть чему поучиться):

#include <ctype.h>
#include <limits.h>
#include <stdio.h>

/*  print a graph of the frequencies of different characters */

#define GRAPH_HEIGHT 20

static void count_chars(FILE *in, unsigned int *frequencies)
{
    int c;
    while ((c = fgetc(in)) != EOF) {
        ++frequencies[(unsigned char)c];
    }
}

static void print_graph(FILE *out, const unsigned int *frequencies)
{
    /* start at 1 to avoid division by 0 later */
    unsigned int max = 1;
    for (unsigned int i = 0;  i <= UCHAR_MAX;  ++i) {
        if (frequencies[i] > max) {
            max = frequencies[i];
        }
    }

    /* print characters on a graph */
    for (unsigned int i = GRAPH_HEIGHT;  i > 0;  --i) {
        fprintf(out, "%2d |", i);
        for (int c = 0;  c <= UCHAR_MAX;  ++c) {
            if (!isalnum(c)) { continue; }
            char bar = frequencies[c] >= i * max / GRAPH_HEIGHT ? '|' : ' ';
            fprintf(out, " %c ", bar);
        }
        fputs("n", out);
    }

    /* print the base line */
    fputs("   +", out);
    for (int c = 0;  c <= UCHAR_MAX;  ++c) {
        if (!isalnum(c)) { continue; }
        fputs("---", out);
    }
    fputs("n", out);
    for (int c = 0;  c <= UCHAR_MAX;  ++c) {
        if (!isalnum(c)) { continue; }
        fprintf(stdout, "%3c", c);
    }
    fputs("n", out);
}

int main(void)
{
    unsigned int af[UCHAR_MAX+1] = {0};
    count_chars(stdin, af);
    print_graph(stdout, af);
}

  • Вау, спасибо за подробный ответ. Мне понравился ваш код, когда я его свободно изменял. Во всяком случае, у меня есть один вопрос. Я видел курс CS50, и один профессор сказал: «Это просто человеческое соглашение — ставить основную программу наверху». Вы помещаете main () внизу. Есть ли для этого особая причина? Чтобы не набирать прототип функции?

    — на ол

  • 1

    Должен признать, я даже не согласен с тем, начинаю ли я с голых прототипов и main(), или напишите определения и положите main() последний. Делайте это так, как вам нравится!

    — Тоби Спейт

  • 1

    Я внес правку, чтобы объявить помощников с статическая связь. Это не делает заметной разницы для подобных однофайловых программ, но помогает разделить проблемы, когда вы переходите к более крупным программам, состоящим из нескольких объектных файлов.

    — Тоби Спейт

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *