项目背景详细介绍
距离(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
点到线段 / 线 / 平面距离
点到线段最近点可通过向量投影计算投影参数 t,再裁剪到 [0,1] 得到最近点。点到平面可通过平面方程(法向量)直接计算带符号距离。
大整数/高精度(BigDecimal)
BigDecimal 没有内置 sqrt,需实现牛顿法或二分法求平方根,指定精度(小数位数或 MathContext)。牛顿迭代速度较快但需要良好的初始值。
复杂度分析
各单次距离计算一般为 O(d)(d 为维度或常数 2/3),线段/线/平面距离也为 O(1)。两线段最短距离可能需要处理多种相对位置,但仍为 O(1) 的几个向量运算。
实现思路详细介绍
我们将把实现分为若干模块(在代码中以注释区分):
数据结构:Point2D, Point3D 简单不可变类;N 维使用 double[] 或 List
基本距离函数:实现 euclideanDistance、euclideanDistanceSquared、manhattanDistance、chebyshevDistance 等多种签名(点对象、坐标对、数组)。
地理距离:实现 haversineDistanceKm/ m,允许指定球半径。
点到线段/线/平面距离:实现点到线段的最近点投影方法并返回距离与最近点坐标;实现点到平面距离(给定平面法向量与常数项)。
线段到线段最短距离:基于向量代数使用参数化表示与求解两个参数(s,t)来得到最近点对;处理平行或近似平行情况。
BigDecimal 高精度版本:实现 bigDecimalEuclideanDistance,其中实现 sqrt(BigDecimal, MathContext) 使用牛顿迭代并支持用户传入精度参数。
Stream 示例:演示用 DoubleStream / Stream
示例与基准: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 调用。