c++ 绘制频率直方图

y4ekin9u  于 2024-01-09  发布在  其他
关注(0)|答案(2)|浏览(155)

我用C++写了一段代码,它首先对一个十进制值数组进行排序,然后询问用户他们想要直方图(稍后绘制)的分割数,使用它来计算类宽度,从而计算每个类中值的频率。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<float> lengths = {
    2.1, 2.5, 1.8, 2.2, 2.9, 2.0, 1.5, 2.8, 2.3, 2.6,
    3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2.0, 2.5};
vector<int> hist;
int divisions;

int main(void)
{

    sort(lengths.begin(), lengths.end());

    cin >> divisions;
    float class_w = static_cast<float>(lengths[lengths.size() - 1]) / divisions;
    float range_top = class_w;
    int freq = 0;

    for (float x : lengths)
    {
        while (x > range_top) //allows range_top to catch up without x moving on to next value
        {
            hist.push_back(freq);
            freq = 0;
            cout << range_top << " ";
            range_top += class_w;
        }
        if (x <= range_top)
        {
            freq++;
        }
    }
    hist.push_back(freq);

    cout << endl;

    for (int x : hist)
    {
        cout << x << " ";
    }

    cout << endl;

    for (float y : lengths)
    {
        cout << y << " ";
    }
}

字符串
当数组的最大/最终值等于range_top值时,问题出现。
下面是一个例子,当我把divisions作为10时:长度(排序):1.5 1.8 1.8 1.9 2 2.1 2.2 2.2 2.3 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.9 3.1 range_top:0.31 0.62 0.93 1.24 1.55 1.86 2.17 2.48 2.79 3.1历史:0 0 0 0 1 2 4 5 4 3 1
它应该是。历史:0 0 0 1 2 4 5 4
怎么解决?

p8ekf7hl

p8ekf7hl1#

正如你所说的,问题是最大值等于range_top值(甚至可能由于舍入问题而稍大)。一个基本的解决方案是有效地确定最后一个直方图值没有上限。一种方法是跟踪当前的除法值,并确保它永远不会大于最大值。我通过以下更改做到了这一点:
1.在main函数中,添加int currDivision = 1;行。
1.将while (x > range_top)更改为while (currDivision < division && x > range_top)
1.在该循环的底部,在range_top += class_w;行之后,添加currDivision++;行。
1.由于最大值实际上可能比最后一个range_top值稍大,因此删除if (x <= range_top)行。这甚至与您的初始代码是多余的,但我们现在不需要或不希望它确保每个x值总是在freq变量中计数,特别是在最后一组中。
通过这些更改,以下是使用divisions值10的输出:

0.31 0.62 0.93 1.24 1.55 1.86 2.17 2.48 2.79
0 0 0 0 1 2 4 5 4 4
1.5 1.8 1.8 1.9 2 2 2.1 2.2 2.2 2.3 2.3 2.4 2.5 2.5 2.6 2.7 2.8 2.9 2.9 3.1

字符串
如您所见,直方图输出现在是正确的值集。

bnl4lu3b

bnl4lu3b2#

当数组的最大/最终值等于range_top值时,就会出现问题。[这会导致向直方图添加额外的直方图桶,其中包含等于range_top值的值。]
您是浮点值不精确表示的受害者。
您的程序通过将先前的range_top值与class_w相加来计算每个range_top值。当您到达第10个直方图桶时,你已经做了9次求和。累积的回合-该计算的off错误导致range_top的值比您期望的3.1小一点点。它小于3.1从向量lengths,这导致创建新的直方图桶。
我认为这与基数为10的值0.1在转换为基数为2时是一个无限重复的“十进制”有关。基数为2的值必须被截断以存储在类型float中,这导致了上面描述的舍入错误。
解决办法是什么?
解决方法很简单。填充每个直方图桶,除了最后一个。剩下的任何东西都放在最后一个直方图桶中。
你可以做的另一件事是使用类型double而不是类型float执行浮点计算。类型float适合6到7位小数的精度。类型double给你15到16。两者都受到舍入错误的影响,但是类型float更快地击中你。
类型float可能是合适的--在计算完成后--当你需要存储一个大的结果向量时。它很少用于计算本身。

是否使用从零开始的直方图范围?

程序中的直方图从0.0开始,一直延伸到向量lengths。因此,直方图中的前几个桶是空的,因为没有一个长度“接近”0.0。
直方图桶的宽度(即class_w)通过首先对向量lengths进行排序,然后将最大长度(即lengths[lengths.size() - 1])除以桶的数量(即divisions)来找到。

float class_w = static_cast<float>(lengths[lengths.size() - 1]) / divisions;

// You could also use member function `back` to make this computation:
float class_w = lengths.back() / divisions;

字符串
您可能希望直方图从向量lengths的最小值开始,这将改变class_w的计算。

float class_w = (lengths.back() - lengths.front()) / divisions;


下面的程序使用了一个标志,它允许您选择其中一种方法:

bool const histogram_is_zero_based{ true };
    auto const class_w
    {
        histogram_is_zero_based
        ? lengths.back() / divisions
        : (lengths.back() - lengths.front()) / divisions
    };

填充每个直方图桶,最后一个除外

存储桶的数量由变量divisions给出。在省略最后一个存储桶的循环中,迭代的总次数为divisions - 1。我们将其保存在变量imax中。

enum : std::size_t { one = 1u };
std::size_t const imax{ divisions - one };
for (std::size_t i{}; i < imax; ++i)
{
    // Fill every histogram bucket, except the last.
}


这很好,但这个循环也跟踪了向量lengths的迭代器。如果我们到达了向量lengths的末尾,我们就停止循环。

auto it{ lengths.cbegin() };  // iterator into vector `lengths`
enum : std::size_t { one = 1u };
std::size_t const imax{ divisions - one };
for (std::size_t i{}; i < imax && it != lengths.cend(); ++i)
{
    // Perfect!
}


剩下的很简单。内部循环扫描向量lengths,当它发现一个不属于当前直方图桶的值时停止。在循环内部(值属于当前直方图桶),它递增桶计数。

  • “桶计数”存储在向量hist中。
  • “当前桶”由变量i索引。
  • “属于当前直方图桶”翻译为*it < range_top[i]
  • “递增当前存储桶计数”表示++hist[i];
std::vector<int> hist(divisions, 0);

    // Fill every histogram bucket, except the last.
    auto it{ lengths.cbegin() };  // iterator into vector `lengths`
    enum : std::size_t { one = 1u };
    std::size_t const imax{ divisions - one };
    for (std::size_t i{}; i < imax && it != lengths.cend(); ++i)
    {
        while (it != lengths.cend() && *it < range_top[i])
        {
            ++hist[i];
            ++it;
        }
    }

所有剩余的内容都将放入最后一个直方图桶中

这里不需要循环。最后一个直方图桶的计数可以通过简单的迭代器减法来找到。这是因为迭代器it在前一个代码块完成时仍然是活动的。

// Fill the final histogram bucket.
    hist.back() = static_cast<int>(lengths.cend() - it);

完成的函数main

其中大部分在上面已经讨论过了。一个新的东西是向量range_top。它的元素通过重复将class_w添加到初始值threshold来初始化。当histogram_is_zero_based时,变量threshold0.0开始。否则,它从lengths.front()开始,即向量lengths中的最小值。

// main.cpp
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <limits>
#include <string>
#include <vector>

// Helper functions (see below)
//  - get_int
//  - put
//  - operator<<

int main()
{
    std::vector<double> lengths = {
        2.1, 2.5, 1.8, 2.2, 2.9, 2.0, 1.5, 2.8, 2.3, 2.6,
        3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2.0, 2.5 };
    std::cout << "Raw data: " << lengths << "\n\n";

    auto const divisions
    { 
        static_cast<std::size_t>
        (get_int("Number of histogram divisions? ", 1, 20))
    };

    std::sort(lengths.begin(), lengths.end());
    bool const histogram_is_zero_based{ false };
    auto const class_w
    {
        histogram_is_zero_based
        ? lengths.back() / divisions
        : (lengths.back() - lengths.front()) / divisions
    };

    std::vector<double> range_top;
    range_top.reserve(divisions);
    auto threshold{ histogram_is_zero_based ? 0.0 : lengths.front() };
    for (auto i{ divisions }; i--;)
        range_top.push_back(threshold += class_w);

    std::vector<int> hist(divisions, 0);

    // Fill every histogram bucket, except the last.
    auto it{ lengths.cbegin() };  // iterator into vector `lengths`
    enum : std::size_t { one = 1u };
    std::size_t const imax{ divisions - one };
    for (std::size_t i{}; i < imax && it != lengths.cend(); ++i)
    {
        while (it != lengths.cend() && *it < range_top[i])
        {
            ++hist[i];
            ++it;
        }
    }

    // Fill the final histogram bucket.
    hist.back() = static_cast<int>(lengths.cend() - it);

    std::cout
        << "Histogram range is "
        << (histogram_is_zero_based ? "" : "NOT ")
        << "zero-based."
        << "\nLengths: " << lengths
        << "\nRange top: " << range_top
        << "\nHistogram: " << hist
        << "\n\n";
    return 0;
}
// end file: main.cpp

输出

首先,histogram_is_zero_based时的输出。这与OP的预期输出相匹配。

Raw data: [2.1, 2.5, 1.8, 2.2, 2.9, 2, 1.5, 2.8, 2.3, 2.6, 3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2, 2.5]

Number of histogram divisions? 10

Histogram range is zero-based.
Lengths: [1.5, 1.8, 1.8, 1.9, 2, 2, 2.1, 2.2, 2.2, 2.3, 2.3, 2.4, 2.5, 2.5, 2.6, 2.7, 2.8, 2.9, 2.9, 3.1]
Range top: [0.31, 0.62, 0.93, 1.24, 1.55, 1.86, 2.17, 2.48, 2.79, 3.1]
Histogram: [0, 0, 0, 0, 1, 2, 4, 5, 4, 4]


histogram_is_zero_based为false时的输出。

Raw data: [2.1, 2.5, 1.8, 2.2, 2.9, 2, 1.5, 2.8, 2.3, 2.6, 3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2, 2.5]

Number of histogram divisions? 10

Histogram range is NOT zero-based.
Lengths: [1.5, 1.8, 1.8, 1.9, 2, 2, 2.1, 2.2, 2.2, 2.3, 2.3, 2.4, 2.5, 2.5, 2.6, 2.7, 2.8, 2.9, 2.9, 3.1]
Range top: [1.66, 1.82, 1.98, 2.14, 2.3, 2.46, 2.62, 2.78, 2.94, 3.1]
Histogram: [1, 2, 1, 3, 2, 3, 3, 1, 3, 1]

辅助函数:get_int

get_int是一个方便的函数,从键盘输入一个整数(即从std::cin)。它捕获无效(非数字)条目,也捕获超出范围的条目。
使用函数get_int可以很容易地输入直方图中使用的分割数。例如,下面的代码将值限制为1到20之间的整数。结果保存为类型std::size_t

auto const divisions
{ 
    static_cast<std::size_t>
    (get_int("Number of histogram divisions? ", 1, 20))
};
int get_int(
    std::string const& prompt, 
    int const min = std::numeric_limits<int>::min(),
    int const max = std::numeric_limits<int>::max(),
    std::istream& ist = std::cin,
    std::ostream& ost = std::cout)
{
    int n{};
    for (;;)
    {
        ost << prompt;
        if (!(ist >> n))
        {
            // Trap non-numeric entries.
            ost << "Entries must be integers between "
                << min << " and " << max
                << ". Please reenter.\n\n";
            ist.clear();
            ist.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        }
        else if (n < min || max < n)
        {
            // Trap values that are out of range.
            ost << "Entries must be integers between "
                << min << " and " << max
                << ". Please reenter.\n\n";
        }
        else
        {
            // Otherwise, accept the entry.
            ost.put('\n');
            break;
        }
    }
    return n;
}

Helper函数:putoperator<<

函数put显示向量中的值。它有两个参数:

  1. ost-对std::ostream对象的引用。通常,oststd::cout
  2. v-一个常量引用,指向一个包含T类型对象的向量。
    因为put是作为函数模板编写的,所以它可以用来输出函数main中使用的三个向量中的任何一个。
put(std::cout, lengths);    // Display the values in vector `lengths`.
put(std::cout, range_top);  // Display the values in vector `range_top`.
put(std::cout, hist);       // Display the values in vector `hist`.
template< typename T >
void put(std::ostream& ost, std::vector<T> const& v)
{
    enum : std::size_t { zero, one };
    ost.put('[');
    if (v.size() > zero)
    {
        std::size_t const n_commas{ v.size() - one };
        for (std::size_t i{}; i < n_commas; ++i)
            ost << v[i] << ", ";
        ost << v.back();
    }
    ost.put(']');
}

给定函数put,编写一个调用它的流操作符是一件小事。

template< typename T >
std::ostream& operator<< (std::ostream& ost, std::vector<T> const& v)
{
    put(ost, v);
    return ost;
}

现在,您可以像这样显示lengthsrange_tophist

std::cout
        << "Histogram range is "
        << (histogram_is_zero_based ? "" : "NOT ")
        << "zero-based."
        << "\nLengths: " << lengths
        << "\nRange top: " << range_top
        << "\nHistogram: " << hist
        << "\n\n";

旁注

专业程序员尽量避免使用“全局”变量。在一个大型程序中,很难跟踪全局变量在何时何地改变了它的值。这使得很难证明程序的正确性(在所有执行路径上)。更糟糕的是,它几乎不可能调试。
所以,我把所有的全局变量都移到了函数main中,它们在这里是“局部的”。
考虑到大量的教科书都使用using namespace std;,您可能会惊讶地发现,专业程序员几乎从不在生产代码中使用using namespace std;,您也不应该使用它。
相反,专业人士每次需要从标准库引用名称时只需输入std::。有一些例外,主要围绕着所谓的 * 参数依赖查找 ,但这里没有足够的空间来介绍它们。( 提示:* std::swap的重载经常使用ADL。)
所以,我排除了using namespace std;
类型float容易出现过多的舍入错误。除非有压倒性的理由使用类型float-而不是类型double-否则应该使用类型doublefloat适合的一个地方是存储浮点值的 * 大 * 向量(在使用类型double计算之后)。
不过,我已经将float更改为double

相关问题