破解宇宙

 找回密码
 立即注册
查看: 78|回复: 0

【开源分享】一个纯 C++ 实现的 K-Means 聚类工具:轻量、可读、适合学习

[复制链接]

8

主题

10

回帖

73

积分

注册会员

积分
73
发表于 3 天前 | 显示全部楼层 |阅读模式
分享一个纯 C++ 实现的 K-Means 聚类程序。它不依赖第三方库,结构完整、逻辑清晰,编译后就能直接跑,适合拿来学习 C++ 算法实现,也可以作为后续扩展的基础模板。
它是什么
这个程序实现的是经典的 K-Means 聚类算法,主要用来把一组二维数据点自动划分成若干个簇。
简单来说,它可以完成这些工作:
●       从文本文件中加载二维数据;
●       随机初始化聚类中心;
●       反复把数据点分配到最近的聚类中心;
●       更新每个簇的中心位置;
●       根据惯性变化判断是否收敛;
●       把最终聚类结果保存到输出文件中。
它比较适合用来学习 C++ 文件读写、算法流程、工程结构,也适合做课程练习、算法实验,或者在此基础上继续加可视化、多线程、参数解析等功能。
适合什么人
●       想练习 C++ 基础算法实现的人;
●       想了解 K-Means 聚类流程的人;
●       想做一个可以编译运行的 C++ 小项目的人;
●       想在此基础上继续扩展功能的人。
使用方法
1. 准备数据文件
程序默认读取当前目录下的 data.txt。
文件格式很简单:每行两个数字,用空格分开,分别表示一个点的 x 和 y 坐标。
例如:
2.1 2.3
7.8 8.1
5.0 1.2
不需要表头,也不需要逗号分隔,只要每行两个浮点数即可。
2. 编译程序
使用支持 C++11 及以上的编译器即可。
编译命令示例:
g++ -o kmeans kmeans.cpp -std=c++11
如果你使用的是其他编译器,只要支持 C++11 标准,一般也可以正常编译。
3. 运行程序
编译完成后,直接运行生成的可执行文件:
./kmeans
程序运行后会输出当前迭代信息,包括迭代次数和惯性值。惯性值越小,说明数据点离各自簇中心的距离总和越小,聚类效果通常越稳定。
4. 查看结果
运行结束后,程序会生成一个 result.txt 文件。
输出文件每行包含三个值:
x y cluster_id
其中:
●       x:数据点的横坐标;
●       y:数据点的纵坐标;
●       cluster_id:该点所属的簇编号,从 0 开始。
你可以用这个结果文件做后续分析、绘图,或者导入到其他工具里查看聚类分布。
默认参数说明
程序内置了几个默认参数:
●       输入文件:data.txt
●       输出文件:result.txt
●       聚类数量:3
●       最大迭代次数:100
●       收敛阈值:0.0001
如果想修改这些参数,可以直接在源码中调整对应变量,也可以后续自己扩展成命令行参数形式。
注意事项
●       数据文件必须存在,且至少包含两列有效数字;
●       聚类数量不能超过数据点数量;
●       由于初始中心是随机选择的,多次运行结果可能略有差异;
●       如果数据分布本身不适合 K-Means,聚类效果可能不理想;
●       当前版本只支持二维数据,如需更高维度可以继续扩展。
后续可以怎么玩
这个版本适合作为基础模板。后面可以继续扩展:
●       支持命令行参数;
●       支持多维数据;
●       加入肘部法则自动选择 K 值;
●       支持 CSV 文件读取;
●       输出可视化数据;
●       加入多线程加速;
●       封装成更完整的 C++ 小库。
如果大家对这类 C++ 算法小项目感兴趣,后面可以继续分享更多版本。
代码:
  1. // kmeans.cpp
  2. #include <iostream>
  3. #include <fstream>
  4. #include <sstream>
  5. #include <vector>
  6. #include <cmath>
  7. #include <random>
  8. #include <algorithm>
  9. #include <limits>
  10. #include <iomanip>

  11. struct Point {
  12.     double x, y;
  13.     int cluster;
  14.     Point(double x = 0, double y = 0) : x(x), y(y), cluster(-1) {}
  15. };

  16. double euclidean_distance(const Point& a, const Point& b) {
  17.     return std::sqrt((a.x - b.x) * (a.x - b.x) +
  18.                      (a.y - b.y) * (a.y - b.y));
  19. }

  20. std::vector<Point> load_data(const std::string& filename) {
  21.     std::vector<Point> data;
  22.     std::ifstream file(filename);
  23.     if (!file.is_open()) {
  24.         std::cerr << "无法打开文件: " << filename << std::endl;
  25.         return data;
  26.     }
  27.     std::string line;
  28.     while (std::getline(file, line)) {
  29.         std::istringstream iss(line);
  30.         double x, y;
  31.         if (iss >> x >> y) {
  32.             data.emplace_back(x, y);
  33.         }
  34.     }
  35.     return data;
  36. }

  37. std::vector<Point> initialize_centroids(const std::vector<Point>& data, int k) {
  38.     std::vector<Point> centroids;
  39.     std::random_device rd;
  40.     std::mt19937 gen(rd());
  41.     std::uniform_int_distribution<size_t> dist(0, data.size() - 1);

  42.     for (int i = 0; i < k; ++i) {
  43.         centroids.push_back(data[dist(gen)]);
  44.     }
  45.     return centroids;
  46. }

  47. void assign_clusters(std::vector<Point>& data, const std::vector<Point>& centroids) {
  48.     for (auto& point : data) {
  49.         double min_dist = std::numeric_limits<double>::max();
  50.         int best_cluster = 0;
  51.         for (size_t i = 0; i < centroids.size(); ++i) {
  52.             double dist = euclidean_distance(point, centroids[i]);
  53.             if (dist < min_dist) {
  54.                 min_dist = dist;
  55.                 best_cluster = static_cast<int>(i);
  56.             }
  57.         }
  58.         point.cluster = best_cluster;
  59.     }
  60. }

  61. std::vector<Point> update_centroids(const std::vector<Point>& data, int k) {
  62.     std::vector<Point> new_centroids(k);
  63.     std::vector<int> counts(k, 0);

  64.     for (const auto& point : data) {
  65.         new_centroids[point.cluster].x += point.x;
  66.         new_centroids[point.cluster].y += point.y;
  67.         counts[point.cluster]++;
  68.     }

  69.     for (int i = 0; i < k; ++i) {
  70.         if (counts[i] > 0) {
  71.             new_centroids[i].x /= counts[i];
  72.             new_centroids[i].y /= counts[i];
  73.         }
  74.     }
  75.     return new_centroids;
  76. }

  77. double compute_inertia(const std::vector<Point>& data, const std::vector<Point>& centroids) {
  78.     double inertia = 0.0;
  79.     for (const auto& point : data) {
  80.         inertia += euclidean_distance(point, centroids[point.cluster]);
  81.     }
  82.     return inertia;
  83. }

  84. void kmeans(std::vector<Point>& data, int k, int max_iters, double tolerance) {
  85.     auto centroids = initialize_centroids(data, k);
  86.     double prev_inertia = std::numeric_limits<double>::max();

  87.     for (int iter = 0; iter < max_iters; ++iter) {
  88.         assign_clusters(data, centroids);
  89.         auto new_centroids = update_centroids(data, k);
  90.         double inertia = compute_inertia(data, new_centroids);

  91.         std::cout << "迭代 " << iter + 1
  92.                   << " | 惯性: " << std::fixed << std::setprecision(4) << inertia << std::endl;

  93.         if (std::abs(prev_inertia - inertia) < tolerance) {
  94.             std::cout << "收敛于迭代 " << iter + 1 << std::endl;
  95.             break;
  96.         }

  97.         centroids = new_centroids;
  98.         prev_inertia = inertia;
  99.     }
  100. }

  101. void save_results(const std::vector<Point>& data, const std::string& filename) {
  102.     std::ofstream file(filename);
  103.     for (const auto& point : data) {
  104.         file << point.x << " " << point.y << " " << point.cluster << std::endl;
  105.     }
  106. }

  107. int main() {
  108.     std::string input_file = "data.txt";
  109.     std::string output_file = "result.txt";
  110.     int k = 3;
  111.     int max_iters = 100;
  112.     double tolerance = 1e-4;

  113.     auto data = load_data(input_file);
  114.     if (data.empty()) {
  115.         std::cerr << "数据为空" << std::endl;
  116.         return 1;
  117.     }

  118.     std::cout << "加载了 " << data.size() << " 个数据点" << std::endl;
  119.     kmeans(data, k, max_iters, tolerance);
  120.     save_results(data, output_file);
  121.     std::cout << "结果已保存到 " << output_file << std::endl;

  122.     return 0;
  123. }
复制代码
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|破解宇宙

GMT+8, 2026-10-27 16:43 , Processed in 0.051670 second(s), 17 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表