Подсчитайте частоту слов и сначала выведите их наиболее часто встречающиеся

Я пишу сравнение простой программы подсчета слов на разных языках. Задача состоит в том, чтобы подсчитать частоту появления уникальных слов, разделенных пробелами, во входных данных и сначала вывести наиболее частые результаты. Регистр должен быть приведен к нижнему регистру (можно использовать ASCII).

Но прошло много времени с тех пор, как я написал C ++ (до C ++ 11 дней). Это идиоматический современный C ++? Любые улучшения, которые я мог бы сделать, чтобы сделать это более идиоматичным или более простым (я не ищу здесь повышения эффективности). Я хочу придерживаться стандартной библиотеки.

#include <algorithm>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

using namespace std;

int main() {
    string word;
    unordered_map<string, int> counts;
    while (cin >> word) {
        transform(word.begin(), word.end(), word.begin(),
            [](unsigned char c){ return tolower(c); });
        counts[word]++;
    }

    vector<pair<string, int>> ordered(counts.begin(), counts.end());
    sort(ordered.begin(), ordered.end(), [](auto &a, auto &b) {
        return a.second > b.second;
    });

    for (auto count : ordered) {
        cout << count.first << " " << count.second << "n";
    }
    return 0;
}

3 ответа
3

В основном это выглядит нормально.

  • using namespace std; это плохая привычка из-за возможных конфликтов имен. Вместо этого предпочитаю вводить std:: где необходимо.

  • counts[word]++;. Нам не нужна неинкрементная копия счетчика, поэтому вместо этого мы должны использовать предварительное приращение: ++counts[word];

  • sort(ordered.begin(), ordered.end(), [](auto &a, auto &b) мы не меняем a или же b в лямбде, поэтому они должны быть const&.

  • for (auto count : ordered) копирует пары. Опять же, мы должны использовать auto const& здесь.

  • return 0; нам это не нужно, так как компилятор добавит его автоматически. Мы также можем использовать именованную константу вместо 0, конкретно EXIT_SUCCESS из <cstdlib>.

  • 2

    Вы пропустили невозможность включить <cctype>, необходим для std::tolower().

    — Тоби Спейт

  • Спасибо — оцените эти настройки!

    — Бен Хойт

Ответ user673679 касается обзора низкого уровня; Посмотрю алгоритм.

Во-первых, высшие оценки за избежание наиболее частой ошибки с функциями из <cctype> — жизненно важно передать значение как unsigned char повышен до int, а не просто char.

Вместо того, чтобы писать собственный цикл для чтения слов, я бы рассмотрел transform() от итератора входного потока до модуля вставки карты. Тем не менее, нам нужно будет создать собственный итератор для нужного нам средства вставки. Было бы очень просто, если бы мы использовали std::multiset, но для этого потребуется больше памяти (поскольку в нем хранятся все добавленные объекты). Мы могли бы создать класс «счетчик» с соответствующими push_back(), если бы мы могли использовать его снова.

После while цикл, мы должны протестировать std::cin.bad() (или настройте поток на выдачу ошибки). В настоящее время любая ошибка потока игнорируется, и мы действуем так, как если бы мы прочитали весь ввод, давая вводящие в заблуждение результаты.

Вместо того, чтобы заполнять вектор и впоследствии сортировать его, мы могли бы предпочесть вставить непосредственно в упорядоченный контейнер (std::set возможно).

Что-то, что мы можем сделать в современном C ++, — это написать вложенные функции, присвоив переменной лямбда-выражение. Это может быть проще, чем писать встроенную лямбду. Или мы можем использовать анонимное пространство имен для функций со статической связью.

Вот моя модификация кода, чтобы не было явных циклов или каких-либо инструкций управления потоком:

#include <algorithm>
#include <cctype>
#include <iostream>
#include <iterator>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
namespace {
    auto downcase(std::string s) {
        std::transform(s.begin(), s.end(), s.begin(),
                       [](unsigned char c){ return std::tolower(c); });
        return s;
    }
}
int main()
{
    using counter = std::unordered_map<std::string, unsigned>;
    using in_it = std::istream_iterator<std::string>;
    using out_it = std::ostream_iterator<std::string>;

    counter counts;
    auto insert = [&](std::string s) { ++counts[downcase(std::move(s))]; };

    // read words into counter
    std::cin.exceptions(std::istream::badbit);
    std::for_each(in_it{std::cin}, in_it{}, insert);

    // sort by frequency, then alphabetical
    auto by_freq_then_alpha = [](const auto &a, const auto &b) {
        return std::pair{ b.second, a.first } < std::pair{ a.second, b.first};
    };
    const std::set ordered{counts.begin(), counts.end(), by_freq_then_alpha};

    // write the output
    auto format = [](const auto& count) {
        return count.first + ' ' + std::to_string(count.second) + 'n';
    };
    std::transform(ordered.begin(), ordered.end(), out_it{std::cout}, format);
}

Для более современного подхода C ++ 20 включает библиотеку Ranges, которая позволяет нам использовать представления для преобразования коллекций:

int main()
{
    using counter = std::unordered_map<std::string, unsigned>;

    counter counts;
    auto insert = [&counts](std::string s) { ++counts[downcase(std::move(s))]; };

    // read words into counter
    std::cin.exceptions(std::istream::badbit);
    auto input_words = std::ranges::istream_view<std::string>(std::cin);
    std::ranges::for_each(input_words, insert);

    // sort by frequency, then alphabetical
    auto by_freq_then_alpha = [](const auto &a, const auto &b) {
        return std::pair{ b.second, a.first } < std::pair{ a.second, b.first};
    };
    const std::set ordered{counts.begin(), counts.end(), by_freq_then_alpha};

    // write the output
    auto write_out = [](const auto& count) {
        std::cout << count.first << ' ' << count.second << 'n';
    };
    std::ranges::for_each(ordered, write_out);
}

Я не говорю, что это обязательно то, что вы должен записывать; по крайней мере, на данный момент циклы на основе диапазонов более знакомы большинству программистов на C ++ и поэтому, вероятно, наиболее понятны. Однако он демонстрирует некоторые возможности современного C ++.

  • 3

    Хотя правильный и очень идиоматичный C ++, я на самом деле немного предпочитаю код OP для удобочитаемости, он немного короче и его легче сканировать, но опять же, у меня есть 20-летняя привычка / опыт чтения для циклов, и они просто читают яснее, чем transform / for_each и компания из . Но у них есть свое место.

    — Эмили Л.

  • Что ж, это было образование, спасибо! Определенно совершенно другой, более функциональный подход. Я предпочитаю более простой подход с использованием цикла for (возможно, потому, что он напоминает мне Scala, к которому у меня нет теплых нечетких чувств). И спасибо за подсказку об обработке ошибок.

    — Бен Хойт

Добавлено: структурированные привязки

Изменено: на использование алгоритмов на основе диапазона. (C ++ 20) Это устраняет необходимость в begin () / end (), и вы можете использовать проекцию в вызове сортировки.

Удалено: лямбды — не нужны

#include <algorithm>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

using namespace std;

int main()
{
    unordered_map<string, int> counts;

    string word;
    while (cin >> word) {
        ranges::transform(word, word.begin(), ::tolower);
        counts[word]++;
    }

    using word_count = pair<string, int>;

    vector<word_count> ordered(counts.begin(), counts.end());
    ranges::sort(ordered, greater{}, &word_count::second);

    for (auto [word, count] : ordered) {
        cout << word << " " << count << "n";
    }

}

Лямбда-упаковка std::tolower очень определенно необходимо — используя <cctype> функции, напрямую продвигая свои аргументы из простых char к int ведет к UB на платформах, где char подписано. Всегда конвертировать в unsigned char перед преобразованием в int.

— Тоби Спейт

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

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