c++ 绘制频率直方图

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

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

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. using namespace std;
  5. vector<float> lengths = {
  6. 2.1, 2.5, 1.8, 2.2, 2.9, 2.0, 1.5, 2.8, 2.3, 2.6,
  7. 3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2.0, 2.5};
  8. vector<int> hist;
  9. int divisions;
  10. int main(void)
  11. {
  12. sort(lengths.begin(), lengths.end());
  13. cin >> divisions;
  14. float class_w = static_cast<float>(lengths[lengths.size() - 1]) / divisions;
  15. float range_top = class_w;
  16. int freq = 0;
  17. for (float x : lengths)
  18. {
  19. while (x > range_top) //allows range_top to catch up without x moving on to next value
  20. {
  21. hist.push_back(freq);
  22. freq = 0;
  23. cout << range_top << " ";
  24. range_top += class_w;
  25. }
  26. if (x <= range_top)
  27. {
  28. freq++;
  29. }
  30. }
  31. hist.push_back(freq);
  32. cout << endl;
  33. for (int x : hist)
  34. {
  35. cout << x << " ";
  36. }
  37. cout << endl;
  38. for (float y : lengths)
  39. {
  40. cout << y << " ";
  41. }
  42. }

字符串
当数组的最大/最终值等于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的输出:

  1. 0.31 0.62 0.93 1.24 1.55 1.86 2.17 2.48 2.79
  2. 0 0 0 0 1 2 4 5 4 4
  3. 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)来找到。

  1. float class_w = static_cast<float>(lengths[lengths.size() - 1]) / divisions;
  2. // You could also use member function `back` to make this computation:
  3. float class_w = lengths.back() / divisions;

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

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


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

  1. bool const histogram_is_zero_based{ true };
  2. auto const class_w
  3. {
  4. histogram_is_zero_based
  5. ? lengths.back() / divisions
  6. : (lengths.back() - lengths.front()) / divisions
  7. };

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

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

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


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

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


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

  • “桶计数”存储在向量hist中。
  • “当前桶”由变量i索引。
  • “属于当前直方图桶”翻译为*it < range_top[i]
  • “递增当前存储桶计数”表示++hist[i];
  1. std::vector<int> hist(divisions, 0);
  2. // Fill every histogram bucket, except the last.
  3. auto it{ lengths.cbegin() }; // iterator into vector `lengths`
  4. enum : std::size_t { one = 1u };
  5. std::size_t const imax{ divisions - one };
  6. for (std::size_t i{}; i < imax && it != lengths.cend(); ++i)
  7. {
  8. while (it != lengths.cend() && *it < range_top[i])
  9. {
  10. ++hist[i];
  11. ++it;
  12. }
  13. }

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

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

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

完成的函数main

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

  1. // main.cpp
  2. #include <algorithm>
  3. #include <cstddef>
  4. #include <iostream>
  5. #include <limits>
  6. #include <string>
  7. #include <vector>
  8. // Helper functions (see below)
  9. // - get_int
  10. // - put
  11. // - operator<<
  12. int main()
  13. {
  14. std::vector<double> lengths = {
  15. 2.1, 2.5, 1.8, 2.2, 2.9, 2.0, 1.5, 2.8, 2.3, 2.6,
  16. 3.1, 2.9, 2.7, 1.8, 2.2, 2.4, 1.9, 2.3, 2.0, 2.5 };
  17. std::cout << "Raw data: " << lengths << "\n\n";
  18. auto const divisions
  19. {
  20. static_cast<std::size_t>
  21. (get_int("Number of histogram divisions? ", 1, 20))
  22. };
  23. std::sort(lengths.begin(), lengths.end());
  24. bool const histogram_is_zero_based{ false };
  25. auto const class_w
  26. {
  27. histogram_is_zero_based
  28. ? lengths.back() / divisions
  29. : (lengths.back() - lengths.front()) / divisions
  30. };
  31. std::vector<double> range_top;
  32. range_top.reserve(divisions);
  33. auto threshold{ histogram_is_zero_based ? 0.0 : lengths.front() };
  34. for (auto i{ divisions }; i--;)
  35. range_top.push_back(threshold += class_w);
  36. std::vector<int> hist(divisions, 0);
  37. // Fill every histogram bucket, except the last.
  38. auto it{ lengths.cbegin() }; // iterator into vector `lengths`
  39. enum : std::size_t { one = 1u };
  40. std::size_t const imax{ divisions - one };
  41. for (std::size_t i{}; i < imax && it != lengths.cend(); ++i)
  42. {
  43. while (it != lengths.cend() && *it < range_top[i])
  44. {
  45. ++hist[i];
  46. ++it;
  47. }
  48. }
  49. // Fill the final histogram bucket.
  50. hist.back() = static_cast<int>(lengths.cend() - it);
  51. std::cout
  52. << "Histogram range is "
  53. << (histogram_is_zero_based ? "" : "NOT ")
  54. << "zero-based."
  55. << "\nLengths: " << lengths
  56. << "\nRange top: " << range_top
  57. << "\nHistogram: " << hist
  58. << "\n\n";
  59. return 0;
  60. }
  61. // end file: main.cpp

输出

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

  1. 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]
  2. Number of histogram divisions? 10
  3. Histogram range is zero-based.
  4. 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]
  5. Range top: [0.31, 0.62, 0.93, 1.24, 1.55, 1.86, 2.17, 2.48, 2.79, 3.1]
  6. Histogram: [0, 0, 0, 0, 1, 2, 4, 5, 4, 4]


histogram_is_zero_based为false时的输出。

  1. 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]
  2. Number of histogram divisions? 10
  3. Histogram range is NOT zero-based.
  4. 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]
  5. Range top: [1.66, 1.82, 1.98, 2.14, 2.3, 2.46, 2.62, 2.78, 2.94, 3.1]
  6. Histogram: [1, 2, 1, 3, 2, 3, 3, 1, 3, 1]

辅助函数:get_int

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

  1. auto const divisions
  2. {
  3. static_cast<std::size_t>
  4. (get_int("Number of histogram divisions? ", 1, 20))
  5. };
  1. int get_int(
  2. std::string const& prompt,
  3. int const min = std::numeric_limits<int>::min(),
  4. int const max = std::numeric_limits<int>::max(),
  5. std::istream& ist = std::cin,
  6. std::ostream& ost = std::cout)
  7. {
  8. int n{};
  9. for (;;)
  10. {
  11. ost << prompt;
  12. if (!(ist >> n))
  13. {
  14. // Trap non-numeric entries.
  15. ost << "Entries must be integers between "
  16. << min << " and " << max
  17. << ". Please reenter.\n\n";
  18. ist.clear();
  19. ist.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  20. }
  21. else if (n < min || max < n)
  22. {
  23. // Trap values that are out of range.
  24. ost << "Entries must be integers between "
  25. << min << " and " << max
  26. << ". Please reenter.\n\n";
  27. }
  28. else
  29. {
  30. // Otherwise, accept the entry.
  31. ost.put('\n');
  32. break;
  33. }
  34. }
  35. return n;
  36. }

Helper函数:putoperator<<

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

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

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

  1. template< typename T >
  2. std::ostream& operator<< (std::ostream& ost, std::vector<T> const& v)
  3. {
  4. put(ost, v);
  5. return ost;
  6. }

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

  1. std::cout
  2. << "Histogram range is "
  3. << (histogram_is_zero_based ? "" : "NOT ")
  4. << "zero-based."
  5. << "\nLengths: " << lengths
  6. << "\nRange top: " << range_top
  7. << "\nHistogram: " << hist
  8. << "\n\n";

旁注

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

展开查看全部

相关问题