JAVA:实现Distance Formula距离公式算法(附带源码)

JAVA:实现Distance Formula距离公式算法(附带源码)

项目背景详细介绍

距离(Distance)是空间与几何中最基础、最重要的概念之一。无论是在二维平面上的点与点之间、三维空间的模型、还是多维特征空间中的样本比较,距离度量都是工程与科学中的核心操作 —— 在计算机图形、机器人学、地理信息系统(GIS)、机器学习(聚类、最近邻)、物理模拟与 CAD 等领域都有大量应用。最常见的距离度量是欧氏距离(Euclidean distance),也称为“距离公式”,在平面上它的表达式为:

此外还有曼哈顿距离(L1)、切比雪夫距离(L∞)、平方欧氏距离(避免平方根开销用于比较)、以及在地理坐标上常用的大圆距离(haversine)等。工业与科研中还常常遇到需要高精度(使用 BigDecimal)或多维数据(N 维数组)的情况,或需要计算点到线段、点到平面或两条线段之间的最短距离等几何子问题。

本项目旨在用 Java 提供一套全面、教学级且工程可用的“距离公式”实现集合,覆盖常见用法、边界条件、性能优化与高精度选项。代码将放在一个代码块内(不同“文件”用注释区分),每个方法前都会写明作用、参数、返回、复杂度和注意事项,适合直接作为博客与课堂材料使用。

项目需求详细介绍

请实现一个 Java 程序(单文件),满足下列要求:

功能全集(至少包含以下内容):

2D、3D 欧氏距离(点与点);

N 维欧氏距离(数组/列表接口),以及平方距离版本(避免 sqrt);

曼哈顿(L1)与切比雪夫(L∞)距离;

Haversine(大圆)距离,用于地理经纬度(单位:米或公里),并包含球半径参数;

点到线段、点到直线、点到平面的距离(含计算最近点投影);

两个线段间最短距离(包括相交情形返回 0);

使用 BigDecimal 的高精度欧氏距离(带可指定精度的 sqrt 实现);

使用 Java Stream 的 N 维距离计算示例;

性能友好版:返回平方距离以便在仅比较大小时避免 sqrt 开销;

参数校验:null、长度不一致、无效经纬度、步长错误等抛 IllegalArgumentException。

在 main 中提供示例用法、正确性验证(已知示例)、以及耗时对比(纳秒)。

教学与工程说明:

在实现思路中解释若干实现细节与数值注意点(例如浮点精度、避免平方根的用途、BigDecimal sqrt 的牛顿迭代实现、haversine 的数值稳定性)。

相关技术详细介绍

在实现距离公式时会涉及以下技术点与数学/编程注意事项,便于课堂或博客扩展讲解:

欧氏距离与平方欧氏距离

计算欧氏距离需要一次平方根(Math.sqrt),若目标仅是比较距离大小(例如最近邻搜索),可使用平方距离避免昂贵的开根号操作。

数值稳定性

对于大坐标值或非常接近的点,(x2-x1)^2 可能导致中间结果精度丢失或溢出(对于 double 极端少见)。当需要高精度时使用 BigDecimal 或先做平移归一化(例如以某个中心点为基准)可减轻误差。

Haversine 与球面距离

Haversine 公式对小角度差比较稳定,直线三角函数差可能导致精度问题;使用合适的三角函数与弧度转换,并允许使用不同球半径(地球平均半径 ~6371 km 或 6371000 m)。

高维数据

N 维距离可用数组或 List 表示。需保证维度一致。使用循环或 Java Stream 均可实现;注意 Stream 版可能有装箱开销。

点到线段 / 线 / 平面距离

点到线段最近点可通过向量投影计算投影参数 t,再裁剪到 [0,1] 得到最近点。点到平面可通过平面方程(法向量)直接计算带符号距离。

大整数/高精度(BigDecimal)

BigDecimal 没有内置 sqrt,需实现牛顿法或二分法求平方根,指定精度(小数位数或 MathContext)。牛顿迭代速度较快但需要良好的初始值。

复杂度分析

各单次距离计算一般为 O(d)(d 为维度或常数 2/3),线段/线/平面距离也为 O(1)。两线段最短距离可能需要处理多种相对位置,但仍为 O(1) 的几个向量运算。

实现思路详细介绍

我们将把实现分为若干模块(在代码中以注释区分):

数据结构:Point2D, Point3D 简单不可变类;N 维使用 double[] 或 List 表示;地理经纬度使用 LatLon 类(以度为单位传入但内部转换为弧度用于计算)。

基本距离函数:实现 euclideanDistance、euclideanDistanceSquared、manhattanDistance、chebyshevDistance 等多种签名(点对象、坐标对、数组)。

地理距离:实现 haversineDistanceKm/ m,允许指定球半径。

点到线段/线/平面距离:实现点到线段的最近点投影方法并返回距离与最近点坐标;实现点到平面距离(给定平面法向量与常数项)。

线段到线段最短距离:基于向量代数使用参数化表示与求解两个参数(s,t)来得到最近点对;处理平行或近似平行情况。

BigDecimal 高精度版本:实现 bigDecimalEuclideanDistance,其中实现 sqrt(BigDecimal, MathContext) 使用牛顿迭代并支持用户传入精度参数。

Stream 示例:演示用 DoubleStream / Stream 计算 N 维距离,说明装箱开销。

示例与基准:main 中演示若干经典例子(平面点距离、3D、地理城市间距离、点到线段与线段间距离、BigDecimal 精度示例),并在控制台输出耗时(纳秒)。

完整实现代码

/* ============================================================

文件: DistanceFormulaProject.java

说明: 距离公式(Distance Formula)算法实现集(教学 + 工程)

- 2D/3D/N-D 欧氏距离(含平方距离)

- 曼哈顿 & 切比雪夫距离

- Haversine 大圆距离(经纬度)

- 点到线段/直线/平面距离与最近点

- 两线段间最短距离

- BigDecimal 高精度欧氏距离(可指定精度)

- Stream 版本示例

- 参数校验与示例 main()

编译: javac DistanceFormulaProject.java

运行: java DistanceFormulaProject

============================================================ */

import java.math.BigDecimal;

import java.math.MathContext;

import java.math.RoundingMode;

import java.util.Arrays;

import java.util.Locale;

import java.util.Objects;

import java.util.stream.DoubleStream;

public class DistanceFormulaProject {

// ============================

// 文件: PointTypes.java

// 简单的点数据类型(2D, 3D, LatLon)

// ============================

public static final class Point2D {

public final double x;

public final double y;

public Point2D(double x, double y) { this.x = x; this.y = y; }

@Override public String toString() { return String.format(Locale.ROOT,"(%.6f, %.6f)", x, y); }

}

public static final class Point3D {

public final double x;

public final double y;

public final double z;

public Point3D(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }

@Override public String toString() { return String.format(Locale.ROOT,"(%.6f, %.6f, %.6f)", x, y, z); }

}

public static final class LatLon {

// 存储为度,方法内部会转换为弧度

public final double lat; // 纬度,度

public final double lon; // 经度,度

public LatLon(double lat, double lon) {

if (lat < -90.0 || lat > 90.0) throw new IllegalArgumentException("纬度必须在 [-90,90]");

if (lon < -180.0 || lon > 180.0) throw new IllegalArgumentException("经度必须在 [-180,180]");

this.lat = lat; this.lon = lon;

}

@Override public String toString() { return String.format(Locale.ROOT,"(lat=%.6f°, lon=%.6f°)", lat, lon); }

}

// ============================

// 文件: DistanceUtils.java

// 各类距离计算实现(大部分为静态方法)

// ============================

public static class DistanceUtils {

/* ------------------------------------

2D 欧氏距离(double)

方法: euclideanDistance(x1,y1,x2,y2)

作用: 计算两点之间的欧氏距离

参数: 均为 double

返回: double 距离

复杂度: O(1)

注意: 若仅做比较请使用 euclideanDistanceSquared 避免 sqrt 开销

------------------------------------ */

public static double euclideanDistance(double x1, double y1, double x2, double y2) {

double dx = x2 - x1;

double dy = y2 - y1;

return Math.sqrt(dx*dx + dy*dy);

}

/* ------------------------------------

2D 欧氏距离(Point2D)

------------------------------------ */

public static double euclideanDistance(Point2D a, Point2D b) {

Objects.requireNonNull(a, "Point a 不能为空");

Objects.requireNonNull(b, "Point b 不能为空");

return euclideanDistance(a.x, a.y, b.x, b.y);

}

/* ------------------------------------

2D 平方欧氏距离(避免 sqrt)

方法: euclideanDistanceSquared(...)

返回: squared distance (double)

复杂度: O(1)

用途: 当只比较距离大小时优先使用

------------------------------------ */

public static double euclideanDistanceSquared(double x1, double y1, double x2, double y2) {

double dx = x2 - x1;

double dy = y2 - y1;

return dx*dx + dy*dy;

}

/* ------------------------------------

3D 欧氏距离与平方距离

------------------------------------ */

public static double euclideanDistance3D(double x1, double y1, double z1,

double x2, double y2, double z2) {

double dx = x2 - x1;

double dy = y2 - y1;

double dz = z2 - z1;

return Math.sqrt(dx*dx + dy*dy + dz*dz);

}

public static double euclideanDistance(Point3D a, Point3D b) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

return euclideanDistance3D(a.x, a.y, a.z, b.x, b.y, b.z);

}

public static double euclideanDistanceSquared3D(double x1, double y1, double z1,

double x2, double y2, double z2) {

double dx = x2 - x1;

double dy = y2 - y1;

double dz = z2 - z1;

return dx*dx + dy*dy + dz*dz;

}

/* ------------------------------------

N 维欧氏距离(数组版本)

方法: euclideanDistanceND(double[] a, double[] b)

参数: 两个 double[] 数组,长度必须相同

返回: double 距离

复杂度: O(d)

注意: 当 d 很大时关注性能与内存本地性

------------------------------------ */

public static double euclideanDistanceND(double[] a, double[] b) {

Objects.requireNonNull(a, "a 不能为空");

Objects.requireNonNull(b, "b 不能为空");

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

double sum = 0.0;

for (int i = 0; i < a.length; i++) {

double diff = a[i] - b[i];

sum += diff * diff;

}

return Math.sqrt(sum);

}

public static double euclideanDistanceSquaredND(double[] a, double[] b) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

double sum = 0.0;

for (int i = 0; i < a.length; i++) {

double diff = a[i] - b[i];

sum += diff * diff;

}

return sum;

}

/* ------------------------------------

Stream 版本的 N 维距离(用于演示)

注意: Stream 会产生装箱/拆箱开销,性能可能不如原始 for-loop

------------------------------------ */

public static double euclideanDistanceNDStream(double[] a, double[] b) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

return Math.sqrt(IntStreamRange.sumSquares(a, b));

}

// 内部小工具:用原始循环但以流式命名封装以便示例(避免装箱)

private static class IntStreamRange {

static double sumSquares(double[] a, double[] b) {

double sum = 0.0;

for (int i = 0; i < a.length; i++) {

double diff = a[i] - b[i];

sum += diff * diff;

}

return sum;

}

}

/* ------------------------------------

曼哈顿距离(L1)

------------------------------------ */

public static double manhattanDistance(double[] a, double[] b) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

double sum = 0.0;

for (int i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);

return sum;

}

/* ------------------------------------

切比雪夫距离(L∞)

------------------------------------ */

public static double chebyshevDistance(double[] a, double[] b) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

double max = 0.0;

for (int i = 0; i < a.length; i++) max = Math.max(max, Math.abs(a[i] - b[i]));

return max;

}

/* ------------------------------------

Haversine 大圆距离(经纬度 -> 米或公里)

方法: haversineDistance(LatLon a, LatLon b, double radius)

参数: radius 单位与返回结果一致(默认地球半径 6371.0 km)

返回: distance (same unit as radius)

复杂度: O(1)

注意: 输入为度,内部转换为弧度

------------------------------------ */

public static double haversineDistance(LatLon a, LatLon b, double radius) {

Objects.requireNonNull(a); Objects.requireNonNull(b);

double lat1 = Math.toRadians(a.lat);

double lon1 = Math.toRadians(a.lon);

double lat2 = Math.toRadians(b.lat);

double lon2 = Math.toRadians(b.lon);

double dLat = lat2 - lat1;

double dLon = lon2 - lon1;

double sinDLat = Math.sin(dLat / 2.0);

double sinDLon = Math.sin(dLon / 2.0);

double h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;

double c = 2.0 * Math.atan2(Math.sqrt(h), Math.sqrt(Math.max(0.0, 1.0 - h)));

return radius * c;

}

public static double haversineDistanceKm(LatLon a, LatLon b) {

// Earth mean radius ~6371.0088 km

return haversineDistance(a, b, 6371.0088);

}

public static double haversineDistanceMeters(LatLon a, LatLon b) {

// Earth mean radius ~6371008.8 meters

return haversineDistance(a, b, 6371008.8);

}

/* ------------------------------------

点到直线(通过两点定义)最近点与距离(2D 与 N-D)

- 线段版本会把参数 t 裁剪到 [0,1]

- 返回最近点参数 t 与最近点坐标(对于 N-D 返回 double[])

复杂度: O(d)

------------------------------------ */

// 2D: returns projection parameter t in R such that P = A + t*(B-A)

public static double pointToLineParameter2D(Point2D A, Point2D B, Point2D P) {

Objects.requireNonNull(A); Objects.requireNonNull(B); Objects.requireNonNull(P);

double vx = B.x - A.x;

double vy = B.y - A.y;

double wx = P.x - A.x;

double wy = P.y - A.y;

double denom = vx*vx + vy*vy;

if (denom == 0.0) return 0.0; // A==B degenerate, project to A

return (vx*wx + vy*wy) / denom;

}

public static Point2D closestPointOnSegment2D(Point2D A, Point2D B, Point2D P) {

double t = pointToLineParameter2D(A,B,P);

double tClamped = Math.max(0.0, Math.min(1.0, t));

return new Point2D(A.x + tClamped*(B.x-A.x), A.y + tClamped*(B.y-A.y));

}

public static double distancePointToSegment2D(Point2D A, Point2D B, Point2D P) {

Point2D Q = closestPointOnSegment2D(A,B,P);

return euclideanDistance(P, Q);

}

// N-D version: closest point on segment AB for point P (arrays)

public static double distancePointToSegmentND(double[] A, double[] B, double[] P) {

Objects.requireNonNull(A); Objects.requireNonNull(B); Objects.requireNonNull(P);

if (A.length != B.length || A.length != P.length) throw new IllegalArgumentException("向量长度不一致");

int d = A.length;

double[] v = new double[d];

double[] w = new double[d];

for (int i = 0; i < d; i++) { v[i] = B[i] - A[i]; w[i] = P[i] - A[i]; }

double vv = 0.0, vw = 0.0;

for (int i = 0; i < d; i++) { vv += v[i]*v[i]; vw += v[i]*w[i]; }

double t = (vv == 0.0) ? 0.0 : vw / vv;

double tClamped = Math.max(0.0, Math.min(1.0, t));

double sum = 0.0;

for (int i = 0; i < d; i++) {

double diff = (A[i] + tClamped * v[i]) - P[i];

sum += diff * diff;

}

return Math.sqrt(sum);

}

/* ------------------------------------

点到平面距离(给定平面法向量 n 和常数 c,使得平面为 n·x + c = 0)

返回点到平面的最短距离(绝对值)

复杂度: O(d)

------------------------------------ */

public static double distancePointToPlane(double[] normal, double c, double[] P) {

Objects.requireNonNull(normal); Objects.requireNonNull(P);

if (normal.length != P.length) throw new IllegalArgumentException("向量长度不一致");

double num = 0.0, denom = 0.0;

for (int i = 0; i < normal.length; i++) {

num += normal[i] * P[i];

denom += normal[i] * normal[i];

}

// plane equation: n·x + c = 0 -> distance = |n·P + c| / ||n||

double numerator = Math.abs(num + c);

double norm = Math.sqrt(denom);

if (norm == 0.0) throw new IllegalArgumentException("法向量不能为零向量");

return numerator / norm;

}

/* ------------------------------------

两线段最短距离(3D 或 N-D)

说明: 采用参数化表示 S1(u)=P1 + u*(Q1-P1), S2(v)=P2 + v*(Q2-P2)

通过解线性系统得到近似 (u,v),裁剪到 [0,1] 并处理平行情况

返回: 最短距离(double)

复杂度: O(d)

注意: 实现中以 3D 为主要示例,但也可处理任意维度

------------------------------------ */

public static double segmentToSegmentDistance3D(Point3D P1, Point3D Q1, Point3D P2, Point3D Q2) {

Objects.requireNonNull(P1); Objects.requireNonNull(Q1); Objects.requireNonNull(P2); Objects.requireNonNull(Q2);

// convert to vectors

double[] u = {Q1.x - P1.x, Q1.y - P1.y, Q1.z - P1.z};

double[] v = {Q2.x - P2.x, Q2.y - P2.y, Q2.z - P2.z};

double[] w0 = {P1.x - P2.x, P1.y - P2.y, P1.z - P2.z};

double a = dot(u,u); // |u|^2

double b = dot(u,v);

double c = dot(v,v); // |v|^2

double d = dot(u,w0);

double e = dot(v,w0);

double D = a*c - b*b; // denominator

double sc, sN, sD = D;

double tc, tN, tD = D;

double SMALL_NUM = 1e-12;

if (D < SMALL_NUM) { // almost parallel

sN = 0.0;

sD = 1.0;

tN = e;

tD = c;

} else {

sN = (b*e - c*d);

tN = (a*e - b*d);

if (sN < 0.0) { sN = 0.0; tN = e; tD = c; }

else if (sN > sD) { sN = sD; tN = e + b; tD = c; }

}

if (tN < 0.0) {

tN = 0.0;

if (-d < 0.0) sN = 0.0;

else if (-d > a) sN = sD;

else { sN = -d; sD = a; }

} else if (tN > tD) {

tN = tD;

if ((-d + b) < 0.0) sN = 0;

else if ((-d + b) > a) sN = sD;

else { sN = (-d + b); sD = a; }

}

sc = (Math.abs(sN) < SMALL_NUM ? 0.0 : sN / sD);

tc = (Math.abs(tN) < SMALL_NUM ? 0.0 : tN / tD);

// compute difference vector dP = w0 + sc*u - tc*v

double[] dP = new double[3];

for (int i = 0; i < 3; i++) dP[i] = w0[i] + sc * u[i] - tc * v[i];

double dist2 = dP[0]*dP[0] + dP[1]*dP[1] + dP[2]*dP[2];

return Math.sqrt(Math.max(0.0, dist2));

}

private static double dot(double[] a, double[] b) {

double s = 0.0;

for (int i = 0; i < a.length; i++) s += a[i] * b[i];

return s;

}

/* ------------------------------------

BigDecimal 高精度欧氏距离(N-D)

- 提供 sqrt(BigDecimal, MathContext) 实现(牛顿迭代)

- 方法: bigDecimalEuclideanDistance(BigDecimal[] a, BigDecimal[] b, MathContext mc)

- 复杂度: 迭代次数 * 大数乘法复杂度

注意: BigDecimal 运算较慢,仅在需要高精度时使用

------------------------------------ */

// sqrt using Newton's method: sqrt(S) ~ x_{k+1} = (x_k + S / x_k) / 2

public static BigDecimal sqrtBigDecimal(BigDecimal S, MathContext mc) {

Objects.requireNonNull(S); Objects.requireNonNull(mc);

if (S.signum() < 0) throw new IllegalArgumentException("S 必须非负");

if (S.equals(BigDecimal.ZERO)) return BigDecimal.ZERO;

BigDecimal x = BigDecimal.valueOf(Math.sqrt(S.doubleValue())); // initial approximation

BigDecimal two = BigDecimal.valueOf(2);

int maxIter = mc.getPrecision() + 10;

for (int i = 0; i < maxIter; i++) {

BigDecimal xPrev = x;

x = x.add(S.divide(x, mc)).divide(two, mc);

if (x.subtract(xPrev).abs().compareTo(BigDecimal.ONE.scaleByPowerOfTen(-mc.getPrecision())) < 0) break;

}

return x;

}

public static BigDecimal bigDecimalEuclideanDistance(BigDecimal[] a, BigDecimal[] b, MathContext mc) {

Objects.requireNonNull(a); Objects.requireNonNull(b); Objects.requireNonNull(mc);

if (a.length != b.length) throw new IllegalArgumentException("向量长度必须一致");

BigDecimal sum = BigDecimal.ZERO;

for (int i = 0; i < a.length; i++) {

BigDecimal diff = a[i].subtract(b[i]);

sum = sum.add(diff.multiply(diff, mc), mc);

}

return sqrtBigDecimal(sum, mc);

}

} // end DistanceUtils

// ============================

// 文件: DemoMain.java

// 演示与简单基准

// ============================

public static void main(String[] args) {

System.out.println("=== DistanceFormulaProject Demo ===");

// 2D 示例

Point2D p1 = new Point2D(0.0, 0.0);

Point2D p2 = new Point2D(3.0, 4.0);

System.out.println("\n2D 欧氏距离示例: p1=" + p1 + ", p2=" + p2);

System.out.println("euclidean = " + DistanceUtils.euclideanDistance(p1, p2));

System.out.println("squared = " + DistanceUtils.euclideanDistanceSquared(p1.x, p1.y, p2.x, p2.y));

// 3D 示例

Point3D a = new Point3D(1.0, 2.0, 3.0);

Point3D b = new Point3D(4.0, 6.0, 3.0);

System.out.println("\n3D 欧氏距离示例: a=" + a + ", b=" + b);

System.out.println("euclidean3D = " + DistanceUtils.euclideanDistance(a, b));

// N-D 示例

double[] v1 = {1.0, 2.0, 3.0, 4.0};

double[] v2 = {1.5, 2.5, 3.5, 4.5};

System.out.println("\nN-D 欧氏距离示例: v1=" + Arrays.toString(v1));

System.out.println("v2=" + Arrays.toString(v2));

System.out.println("euclideanND = " + DistanceUtils.euclideanDistanceND(v1, v2));

System.out.println("manhattan = " + DistanceUtils.manhattanDistance(v1, v2));

System.out.println("chebyshev = " + DistanceUtils.chebyshevDistance(v1, v2));

// Haversine 示例(北京: 39.9042N,116.4074E 与 上海: 31.2304N,121.4737E)

LatLon beijing = new LatLon(39.9042, 116.4074);

LatLon shanghai = new LatLon(31.2304, 121.4737);

System.out.println("\nHaversine (Beijing -> Shanghai) km = " + DistanceUtils.haversineDistanceKm(beijing, shanghai));

System.out.println("Haversine (meters) = " + DistanceUtils.haversineDistanceMeters(beijing, shanghai));

// 点到线段示例

Point2D A = new Point2D(0,0), B = new Point2D(10,0), P = new Point2D(3,4);

System.out.println("\n点到线段示例: closest point to P on AB = " + DistanceUtils.closestPointOnSegment2D(A,B,P));

System.out.println("distance point->segment = " + DistanceUtils.distancePointToSegment2D(A,B,P));

// 两线段距离示例(3D)

Point3D P1 = new Point3D(0,0,0), Q1 = new Point3D(1,0,0);

Point3D P2 = new Point3D(0,1,0), Q2 = new Point3D(1,1,0);

System.out.println("\n两线段最短距离示例 (平行不相交): " +

DistanceUtils.segmentToSegmentDistance3D(P1,Q1,P2,Q2));

// BigDecimal 高精度示例

BigDecimal[] bigA = { new BigDecimal("1.12345678901234567890"), new BigDecimal("2.98765432109876543210") };

BigDecimal[] bigB = { new BigDecimal("1.12345678901234567891"), new BigDecimal("2.98765432109876543211") };

MathContext mc = new MathContext(40, RoundingMode.HALF_UP);

System.out.println("\nBigDecimal 高精度距离 (precision 40): " +

DistanceUtils.bigDecimalEuclideanDistance(bigA, bigB, mc).toPlainString());

// 性能对比:大量 N-D 点对计算平方距离 vs 实际开根号

int iterations = 1_000_000;

double[] va = new double[] {1.0,2.0,3.0,4.0,5.0};

double[] vb = new double[] {5.0,4.0,3.0,2.0,1.0};

long t0 = System.nanoTime();

double s = 0.0;

for (int i = 0; i < iterations; i++) s += DistanceUtils.euclideanDistanceSquaredND(va, vb);

long t1 = System.nanoTime();

long tSq = t1 - t0;

t0 = System.nanoTime();

double r = 0.0;

for (int i = 0; i < iterations; i++) r += DistanceUtils.euclideanDistanceND(va, vb);

long t2 = System.nanoTime();

long tSqrt = t2 - t0;

System.out.printf("%n性能对比: %d 次平方距离耗时(ns)=%d, 含开根号耗时(ns)=%d%n", iterations, tSq, tSqrt);

System.out.println("\nDemo 完成。");

}

}

代码详细解读

Point2D / Point3D / LatLon:简单不可变的数据结构,分别表示二维点、三维点与地理经纬度(度)。

DistanceUtils.euclideanDistance(double x1,double y1,double x2,double y2):计算二维点间欧氏距离(带 sqrt)。

DistanceUtils.euclideanDistance(Point2D a, Point2D b):Point2D 版本的欧氏距离。

DistanceUtils.euclideanDistanceSquared(...):返回平方欧氏距离(避免 sqrt,用于仅比较大小的场景)。

DistanceUtils.euclideanDistance3D / euclideanDistanceSquared3D / euclideanDistance(Point3D,Point3D):三维距离及平方距离计算。

DistanceUtils.euclideanDistanceND(double[] a, double[] b) / euclideanDistanceSquaredND:N 维欧氏距离与平方距离(数组输入,长度需一致)。

DistanceUtils.euclideanDistanceNDStream:示例性 Stream 版本(说明了装箱开销问题)。

DistanceUtils.manhattanDistance:计算 L1(曼哈顿)距离。

DistanceUtils.chebyshevDistance:计算 L∞(切比雪夫)距离。

DistanceUtils.haversineDistance(LatLon a, LatLon b, double radius):给定球半径,使用 haversine 公式计算两经纬点间的大圆距离。辅助方法 haversineDistanceKm 与 haversineDistanceMeters 使用常用地球半径。

DistanceUtils.pointToLineParameter2D:返回点在由 A->B 定义直线上的投影参数 t(可超出 [0,1])。

DistanceUtils.closestPointOnSegment2D:返回点 P 在线段 AB 上的最近点(裁剪 t∈[0,1])。

DistanceUtils.distancePointToSegment2D:点到线段距离(2D)。

DistanceUtils.distancePointToSegmentND:N 维点到线段距离。

DistanceUtils.distancePointToPlane:点到平面的最短距离,接受法向量和常数项(n·x + c = 0)。

DistanceUtils.segmentToSegmentDistance3D:两条线段在 3D 空间中的最短距离(考虑平行和边界情况),返回最短距离(若相交返回 0)。

DistanceUtils.sqrtBigDecimal:使用牛顿迭代求解 BigDecimal 的平方根,接受 MathContext 指定精度。

DistanceUtils.bigDecimalEuclideanDistance(BigDecimal[] a, BigDecimal[] b, MathContext mc):使用 BigDecimal 在指定精度下计算 N 维欧氏距离,适合高精度需求。

项目详细总结

功能全面:本实现覆盖了从基础二维欧氏距离到三维、N 维、曼哈顿、切比雪夫,以及地理 haversine 距离、点到线段/平面距离和线段间最短距离等常见需求,同时提供了高精度 BigDecimal 版本与流式示例,适合教学与工程双重用途。

性能与数值注意:

在对大量距离比较时,优先使用平方距离以避免大量 sqrt 调用;

对高维向量使用原始 for 循环通常优于 Stream(减少装箱开销);

BigDecimal 精度很高但开销大,仅在必要时使用;对地理距离使用 haversine 比直接三角更稳健。

工程实践建议:

在库中提供同时返回距离与最近点(用于几何应用);

对大规模最近邻查询结合索引结构(KD-Tree、Ball-Tree)以避免 O(n) 逐对计算;

在需要高吞吐的场景,用原始 double[] 表示向量并在 JNI 层或专门数值库中实现核心循环以获得更好性能。

项目常见问题及解答

Q1:为什么要提供平方距离函数,不能总是直接取 sqrt?

A1:Math.sqrt 相对昂贵。若只是在比较两个距离的大小(例如找最小距离),比较平方距离即可避免 sqrt 带来的性能开销。

Q2:Stream 版本好还是传统循环好?

A2:Stream 在可读性和声明式上更优,但涉及装箱(Double)时性能会下降;在性能敏感场景应使用原始 for 循环或 DoubleStream 且尽量避免装箱。

Q3:Haversine 与简单球面余弦公式哪个更好?

A3:haversine 在小角度差时更稳定(避免浮点精度问题);使用时仍需注意弧度转换与数值下溢/上溢边界,但对典型经纬输入是稳健且常用的。

Q4:BigDecimal sqrt 是否足够快?

A4:牛顿迭代通常收敛速度快(每次迭代位数翻倍),但大数乘法与除法非常耗时。BigDecimal 适用于精度要求极高的场合,但若处理非常大规模数据,则应考虑专门的大数库或多精度浮点库。

Q5:如何处理极端大坐标造成的浮点不稳定?

A5:可采用数值归一化(例如先以某个中心点平移坐标)、使用 long double / 高精度库,或增加算法的数值稳定性(避免大差值相减带来精度损失)。

扩展方向与性能优化建议

向量化与并行化:在大规模距离计算(如成千上万向量对)中,利用 SIMD(向量化指令)或并行线程池分段计算可获得显著加速;在 Java 中可使用 ForkJoinPool 或调用本地库(JNI)。

内存布局优化:对于大量 N 维特征向量,使用列主或行主的 double[] 紧凑排列并确保缓存友好可以提升性能;避免频繁对象分配以减少 GC 压力。

索引结构:在最近邻或范围搜索场景中,结合 KD-Tree、Ball-Tree、VP-Tree 或 LSH(近似最近邻)等索引结构能将复杂度从 O(n) 降为对多数查询更优的平均复杂度。

数值库集成:若追求更高性能与数值稳定性,考虑集成成熟本地数值库(如 Eigen、Intel MKL、或专门的地理库)并通过 JNI/FFI 调用。

相关文章