C++:使用传输和交换实现聚类算法(附带源码)

一、项目背景详细介绍

在前面的聚类算法实现中,我们已经系统地介绍并实现了 K-Means 聚类算法。K-Means 以其简单高效著称,但在真实工程与统计建模中,它存在一些先天局限性

  • 对异常值(Outliers)极其敏感
  • 必须使用“均值”作为中心(不一定是实际样本)
  • 仅适用于欧氏空间
  • 对非凸分布表现较差

在许多实际应用场景中,我们更希望:

聚类中心一定是“真实样本点”,并且对异常值更鲁棒。

这正是 K-Medoids 聚类算法(也称 PAM,Partitioning Around Medoids)的出发点。


1.1 什么是“传输”和“交换”思想?

K-Medoids 并不是通过“求均值”来更新中心,而是通过:

  • 传输(Relocation)
    将样本重新分配到最近的中心(medoid)
  • 交换(Swap)
    尝试用“非中心点”替换当前中心点,看是否能降低整体代价函数

📌 一句话概括:

K-Medoids = “分配(传输) + 中心交换(Swap)” 的组合优化算法。

1.2 K-Medoids 与 K-Means 的核心差异

维度K-MeansK-Medoids
中心均值(虚拟点)实际样本
距离通常欧氏任意距离
鲁棒性
计算量较低较高
工程适用性连续数据离散 / 非欧氏

1.3 工程与统计中的应用场景

  • 异常值较多的数据
  • 非欧氏距离(编辑距离、图距离)
  • 小样本高可靠性聚类
  • 生物信息学
  • 社交网络分析
  • 推荐系统中的代表点选择

二、项目需求详细介绍

2.1 功能性需求

本项目目标是:

👉 使用“传输 + 交换”策略,在 C++ 中从零实现 K-Medoids 聚类算法

需要支持:

  1. 任意维度的数值向量
  2. 指定聚类数 K
  3. 明确区分:
    • 样本分配(Relocation)
    • 中心交换(Swap)
  4. 输出:
    • 每个样本的簇标签
    • 每个簇的 medoid 索引

核心接口示例:

class KMedoids { public: KMedoids(int k, int maxIter); void fit(const std::vector<std::vector<double>>& data); const std::vector<int>& labels() const; const std::vector<int>& medoids() const; };


2.2 非功能性需求

  1. 不依赖第三方机器学习库
  2. 使用标准 C++ 数据结构
  3. 算法逻辑清晰、可教学
  4. 可控的时间复杂度
  5. 易于扩展(如任意距离度量)

2.3 适用规模

  • 中小规模数据集(n ≤ 几千)
  • 教学、科研、统计实验
  • 需要高鲁棒性的工程场景

三、相关技术详细介绍


3.2 传输(Relocation)步骤

  • 固定当前 medoids
  • 将每个样本分配给最近的 medoid
  • 这是一个“最近邻”问题

3.3 交换(Swap)步骤

  • 尝试用任意一个非 medoid 点替换某个 medoid
  • 重新计算总代价
  • 若代价下降,则接受交换

📌 这是 K-Medoids 的核心,也是计算最昂贵的部分。


3.4 PAM(Partitioning Around Medoids)

经典 K-Medoids 实现即 PAM 算法:

  1. BUILD:初始化 medoids
  2. SWAP:迭代进行中心交换优化

本项目采用教学友好的简化 PAM 流程


四、实现思路详细介绍

4.1 总体算法流程

随机选择 K 个样本作为初始 medoids repeat: 1. 传输(Relocation) - 将每个样本分配到最近的 medoid 2. 交换(Swap) - 对每个 medoid m: 对每个非 medoid 点 x: 尝试用 x 替换 m 若总代价下降,执行交换 until 无交换发生 或 达到最大迭代次数


4.2 距离度量设计

本项目使用欧氏距离,但代码结构允许轻松替换为:

  • Manhattan 距离
  • 编辑距离
  • 自定义相似度

4.3 时间复杂度说明

  • 传输步骤:O(nK)
  • 交换步骤:O(K(n−K)n)

📌 因此不适合超大规模数据,但非常适合教学与中小规模工程。


五、完整实现代码

/****************************************************** * File: kmedoids.h ******************************************************/ #ifndef KMEDOIDS_H #define KMEDOIDS_H #include <vector> class KMedoids { public: KMedoids(int k, int maxIterations = 100); void fit(const std::vector<std::vector<double>>& data); const std::vector<int>& labels() const; const std::vector<int>& medoids() const; private: int K; int maxIter; std::vector<int> labelVec; // 每个样本的簇标签 std::vector<int> medoidIdx; // medoid 在数据中的索引 double distance(const std::vector<double>& a, const std::vector<double>& b); double totalCost(const std::vector<std::vector<double>>& data, const std::vector<int>& medoids); }; #endif /****************************************************** * File: kmedoids.cpp ******************************************************/ #include "kmedoids.h" #include <random> #include <limits> #include <stdexcept> #include <cmath> #include <algorithm> KMedoids::KMedoids(int k, int maxIterations) : K(k), maxIter(maxIterations) { if (K <= 0) throw std::invalid_argument("K must be positive"); } double KMedoids::distance(const std::vector<double>& a, const std::vector<double>& b) { double sum = 0.0; for (size_t i = 0; i < a.size(); ++i) { double d = a[i] - b[i]; sum += d * d; } return std::sqrt(sum); } double KMedoids::totalCost(const std::vector<std::vector<double>>& data, const std::vector<int>& medoids) { double cost = 0.0; for (size_t i = 0; i < data.size(); ++i) { double minDist = std::numeric_limits<double>::max(); for (int m : medoids) { double d = distance(data[i], data[m]); if (d < minDist) minDist = d; } cost += minDist; } return cost; } void KMedoids::fit(const std::vector<std::vector<double>>& data) { if (data.empty()) throw std::invalid_argument("Empty dataset"); int n = static_cast<int>(data.size()); labelVec.assign(n, 0); /* 随机初始化 medoids */ std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dist(0, n - 1); medoidIdx.clear(); while ((int)medoidIdx.size() < K) { int idx = dist(gen); if (std::find(medoidIdx.begin(), medoidIdx.end(), idx) == medoidIdx.end()) medoidIdx.push_back(idx); } /* 主迭代过程 */ for (int iter = 0; iter < maxIter; ++iter) { /* ---------- 传输(Relocation) ---------- */ for (int i = 0; i < n; ++i) { double minDist = std::numeric_limits<double>::max(); int best = 0; for (int k = 0; k < K; ++k) { double d = distance(data[i], data[medoidIdx[k]]); if (d < minDist) { minDist = d; best = k; } } labelVec[i] = best; } /* ---------- 交换(Swap) ---------- */ bool improved = false; double currentCost = totalCost(data, medoidIdx); for (int k = 0; k < K; ++k) { for (int i = 0; i < n; ++i) { if (std::find(medoidIdx.begin(), medoidIdx.end(), i) != medoidIdx.end()) continue; std::vector<int> candidate = medoidIdx; candidate[k] = i; double newCost = totalCost(data, candidate); if (newCost < currentCost) { medoidIdx = candidate; currentCost = newCost; improved = true; } } } if (!improved) break; } } const std::vector<int>& KMedoids::labels() const { return labelVec; } const std::vector<int>& KMedoids::medoids() const { return medoidIdx; } /****************************************************** * File: main.cpp ******************************************************/ #include <iostream> #include "kmedoids.h" int main() { std::vector<std::vector<double>> data = { {1.0, 2.0}, {1.2, 1.9}, {0.8, 2.1}, {8.0, 8.0}, {8.2, 7.9}, {7.9, 8.1} }; KMedoids model(2, 100); model.fit(data); std::cout << "Cluster labels:\n"; for (int l : model.labels()) std::cout << l << " "; std::cout << "\n"; std::cout << "Medoid indices:\n"; for (int m : model.medoids()) std::cout << m << " "; std::cout << "\n"; return 0; } 

六、代码详细解读(仅解读方法作用)

6.1 fit

  • 实现完整的 PAM 聚类流程
  • 包含传输与交换两个核心阶段
  • 支持提前收敛

6.2 totalCost

  • 计算当前 medoids 下的总代价函数
  • 是 Swap 判断是否接受的依据

6.3 distance

  • 提供样本间距离计算
  • 可替换为任意自定义距离

七、项目详细总结

通过本项目,我们:

  1. 系统实现了基于“传输 + 交换”的聚类算法
  2. 深刻理解了 K-Medoids 与 K-Means 的本质差异
  3. 构建了一个鲁棒、可解释的聚类模块
  4. 为复杂聚类与统计推断打下基础

该实现:

  • 对异常值鲁棒
  • 中心可解释
  • 工程可控
  • 教学价值极高

八、项目常见问题及解答

Q1:为什么 K-Medoids 比 K-Means 慢?

A:因为 Swap 阶段需要大量代价评估。


Q2:什么时候应该用 K-Medoids?

A:当你关心鲁棒性、中心可解释性或非欧氏距离时。


Q3:是否适合大规模数据?

A:不适合,需要 CLARA / CLARANS 等改进算法。


九、扩展方向与性能优化

  1. CLARA(基于采样的 K-Medoids)
  2. CLARANS(随机化 Swap)
  3. 支持任意距离矩阵输入
  4. 并行化 Swap 评估
  5. 与异常检测结合使用

Read more

【C++笔记】STL详解:vector容器的实现

【C++笔记】STL详解:vector容器的实现

前言:         在学习了vector类的基本使用的前提下,本文将重点分析vector类的常用接口及其应用实现。          一、vector成员变量          vector本质上是一个动态数组,通过原生指针来实现底层维护,为了使得STL接口调用的统一性,我们需要将原生指针重命名为迭代器。          其核心目的是:将数据结构(容器)与操作(算法)分离,并通过一种统一的接口(迭代器)将它们粘合在一起。          成员变量分析 template <class T> class vector { public: // 将原生指针重命名为迭代器,实现接口统一 typedef T* iterator; typedef const T* const_iterator; private: iterator _start; // 指向目前使用空间的头 iterator _finish; // 指向目前使用空间的尾 iterator _end_of_storage; // 指向目前可用空间的尾 };          成员变量分析:

By Ne0inhk
Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

◆ 博主名称: 晓此方-ZEEKLOG博客 大家好,欢迎来到晓此方的博客。 ⭐️C++系列个人专栏: Re:从零开始的C++_晓此方的博客-ZEEKLOG博客  ⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 目录 0.1概要&序論 一,布局模型与常见误区解析 1.1C/C++内存布局 1.2内存布局易误解点 二,复习C语言的内存管理方法 2.1malloc 2.2calloc 2.3relloc 2.4free 2.5罗列常见的内存管理错误 三,C++内存管理方法 3.1new/delete管理体系 3.1.1开辟单个空间与释放 3.1.2开辟多个连续的空间与释放

By Ne0inhk
Microsoft Visual C++ 6.0 中文版下载与安装教程,附详细图文,建议收藏

Microsoft Visual C++ 6.0 中文版下载与安装教程,附详细图文,建议收藏

Microsoft Visual C++ 6.0 下载与安装教程 1. 软件介绍 Microsoft Visual C++(简称 VC++ 或 VC)是 Microsoft 公司推出的 C++ 语言开发工具,广泛用于 Windows 平台软件开发。它具有强大的集成开发环境(IDE),提供了代码编辑、调试、编译等一站式开发体验。 Microsoft Visual C++ 6.0(简称 VC6.0)是微软于 1998 年发布的经典 C++ 编译器,集成了 MFC 6.0,提供了标准版(Standard Edition)、专业版(Professional

By Ne0inhk