欧几里得距离作为最直观的空间度量方式,在二维平面中就是我们熟悉的"两点间直线距离"。其数学表达式为√((x₂-x₁)² + (y₂-y₁)²),这个公式完美体现了勾股定理在n维空间的推广。当扩展到三维空间时,只需增加z坐标项;而N维空间则对应各维度坐标差的平方和开方。
在实际工程中,欧氏距离的应用远超几何计算。机器学习中的KNN算法依赖它寻找最近邻样本,计算机视觉通过它比较图像特征向量的相似度,游戏开发用它计算物体碰撞距离,物联网设备靠它进行位置指纹匹配。这些场景对计算效率的要求差异显著——有的需要单次精确计算,有的则要处理海量距离矩阵运算。
实现欧氏距离计算时,首要考虑的是接口的通用性与安全性。我们采用模板函数设计,使其能处理各种数值类型(int, float, double等)。函数签名设计为:
cpp复制template<typename T>
double euclideanDistance(const std::vector<T>& a, const std::vector<T>& b);
这里使用vector容器而非原生数组,既避免手动内存管理,又方便获取维度信息。const引用传参确保不会意外修改输入数据,同时避免拷贝开销。返回统一用double类型保证精度,即使输入是整数也能获得小数结果。
健壮的实现必须包含输入验证:
cpp复制if(a.size() != b.size() || a.empty()) {
throw std::invalid_argument("Vectors must be non-empty and same dimension");
}
这种显式错误检查比隐式假设更安全。工程实践中,还可以扩展为返回错误码或使用std::optional的方案,根据项目异常处理规范选择适当方式。
cpp复制#include <vector>
#include <cmath>
#include <stdexcept>
template<typename T>
double euclideanDistance(const std::vector<T>& a, const std::vector<T>& b) {
if(a.size() != b.size() || a.empty()) {
throw std::invalid_argument("Vectors must be non-empty and same dimension");
}
double sum = 0.0;
for(size_t i = 0; i < a.size(); ++i) {
double diff = static_cast<double>(a[i]) - static_cast<double>(b[i]);
sum += diff * diff;
}
return std::sqrt(sum);
}
关键细节说明:
对于性能敏感场景,可用SSE/AVX指令并行化计算:
cpp复制#include <immintrin.h>
double euclideanDistanceSIMD(const std::vector<float>& a, const std::vector<float>& b) {
// 参数校验略...
__m256 sum = _mm256_setzero_ps();
for(size_t i = 0; i < a.size(); i += 8) {
__m256 vecA = _mm256_loadu_ps(&a[i]);
__m256 vecB = _mm256_loadu_ps(&b[i]);
__m256 diff = _mm256_sub_ps(vecA, vecB);
sum = _mm256_fmadd_ps(diff, diff, sum);
}
// 水平求和
float partial[8];
_mm256_storeu_ps(partial, sum);
float total = partial[0] + partial[1] + partial[2] + partial[3]
+ partial[4] + partial[5] + partial[6] + partial[7];
// 处理剩余元素
for(size_t i = a.size() & ~0x7; i < a.size(); ++i) {
float diff = a[i] - b[i];
total += diff * diff;
}
return std::sqrt(total);
}
注意:使用SIMD需要确保内存对齐,现代编译器通常能自动优化。实测在10000维向量上,AVX版本比标量实现快3-5倍。
当坐标值非常大时,平方操作可能导致浮点数溢出。改进方案是先对向量做归一化:
cpp复制std::vector<double> normalize(const std::vector<double>& v) {
double max_val = *std::max_element(v.begin(), v.end());
std::vector<double> result;
result.reserve(v.size());
for(auto x : v) result.push_back(x/max_val);
return result;
}
另一种方案是使用更稳定的计算方式:
cpp复制double sum = 0.0, compensation = 0.0; // Kahan补偿算法
for(size_t i = 0; i < a.size(); ++i) {
double diff = static_cast<double>(a[i]) - static_cast<double>(b[i]);
double term = diff*diff - compensation;
double temp = sum + term;
compensation = (temp - sum) - term;
sum = temp;
}
当仅需比较距离大小时(如找最近邻),可省去耗时的开方操作,直接比较平方距离:
cpp复制template<typename T>
double squaredDistance(const std::vector<T>& a, const std::vector<T>& b) {
// 实现与欧氏距离类似,去掉最后的std::sqrt
}
这在KNN等算法中能显著提升性能,因为开方运算通常需要15-30个时钟周期。
完善的测试应覆盖以下场景:
cpp复制#include <cassert>
#include <limits>
void testEuclideanDistance() {
// 基础功能测试
std::vector<int> v1{1, 2}, v2{4, 6};
assert(fabs(euclideanDistance(v1, v2) - 5.0) < 1e-9);
// 浮点数测试
std::vector<double> v3{1.5, 2.5}, v4{3.5, 5.5};
assert(fabs(euclideanDistance(v3, v4) - 3.605551) < 1e-6);
// 边界值测试
std::vector<float> v5(1000, 1.0f), v6(1000, 1.0f);
assert(euclideanDistance(v5, v6) == 0.0);
// 异常测试
try {
std::vector<int> empty;
euclideanDistance(empty, empty);
assert(false); // 不应执行到此
} catch(const std::invalid_argument&) {}
// 数值极限测试
std::vector<double> max_vec{std::numeric_limits<double>::max()};
assert(std::isinf(euclideanDistance(max_vec, max_vec)));
}
通过Benchmark测试不同实现的性能(使用Google Benchmark):
cpp复制static void BM_Naive(benchmark::State& state) {
std::vector<double> a(state.range(0), 1.1);
std::vector<double> b(state.range(0), 2.2);
for(auto _ : state) {
benchmark::DoNotOptimize(euclideanDistance(a, b));
}
}
BENCHMARK(BM_Naive)->Range(8, 8<<10);
static void BM_SIMD(benchmark::State& state) {
std::vector<float> a(state.range(0), 1.1f);
std::vector<float> b(state.range(0), 2.2f);
for(auto _ : state) {
benchmark::DoNotOptimize(euclideanDistanceSIMD(a, b));
}
}
BENCHMARK(BM_SIMD)->Range(8, 8<<10);
典型测试结果(Intel i7-11800H):
进一步优化建议:
在大型项目中,建议通过以下方式封装距离计算:
cpp复制class DistanceMetric {
public:
virtual double compute(const std::vector<double>&, const std::vector<double>&) = 0;
virtual ~DistanceMetric() = default;
};
cpp复制class EuclideanDistance : public DistanceMetric {
double compute(const std::vector<double>& a, const std::vector<double>& b) override {
// 实现略...
}
};
cpp复制std::unique_ptr<DistanceMetric> createDistanceMetric(MetricType type) {
switch(type) {
case MetricType::EUCLIDEAN: return std::make_unique<EuclideanDistance>();
// 其他度量标准...
}
}
这种设计允许灵活切换不同的距离度量方式,符合开闭原则。在机器学习管线中,还可以通过模板策略模式进一步优化:
cpp复制template<typename Distance>
class KNNClassifier {
Distance dist;
public:
void train(/*...*/) { /*...*/ }
int predict(const DataPoint& p) {
// 使用dist计算距离
}
};
// 使用示例
KNNClassifier<EuclideanDistance> knn;