KD树(K-Dimensional Tree)是一种高效的空间划分数据结构,广泛应用于最近邻搜索、范围查询与多维数据索引。本文用Java实现KD树,讲解k维空间递归划分、最近邻搜索与范围查询算法。
核心数据结构
import java.util.*;
public class KDTree {
// 二维点(可扩展为任意维度)
static class Point {
double[] coords;
Point(double... coords) { this.coords = coords; }
int dim() { return coords.length; }
double get(int i) { return coords[i]; }
double squaredDistance(Point other) {
double sum = 0;
for (int i = 0; i < dim(); i++) {
double d = coords[i] - other.coords[i];
sum += d * d;
}
return sum;
}
@Override
public String toString() {
return Arrays.toString(coords);
}
}
// KD树节点
static class Node {
Point point;
Node left, right;
int axis; // 当前节点的划分维度
Node(Point point, int axis) {
this.point = point;
this.axis = axis;
}
}
Node root;
int dimension;
KDTree(int dimension) {
this.dimension = dimension;
}
递归构建:中位数划分
// 构建KD树:按中位数递归划分,保证树高O(log n)
void build(List<Point> points) {
root = buildRecursive(points, 0);
}
Node buildRecursive(List<Point> points, int depth) {
if (points.isEmpty()) return null;
int axis = depth % dimension;
// 按当前维度排序并取中位数
points.sort(Comparator.comparingDouble(p -> p.get(axis)));
int median = points.size() / 2;
Node node = new Node(points.get(median), axis);
node.left = buildRecursive(points.subList(0, median), depth + 1);
node.right = buildRecursive(points.subList(median + 1, points.size()), depth + 1);
return node;
}
最近邻搜索
// 最近邻搜索:递归搜索+回溯剪枝
Point nearestNeighbor(Point target) {
if (root == null) return null;
Result best = new Result(null, Double.MAX_VALUE);
nearest(root, target, best);
return best.point;
}
static class Result {
Point point;
double dist;
Result(Point p, double d) { point = p; dist = d; }
}
void nearest(Node node, Point target, Result best) {
if (node == null) return;
double dist = node.point.squaredDistance(target);
if (dist < best.dist) {
best.point = node.point;
best.dist = dist;
}
int axis = node.axis;
double diff = target.get(axis) - node.point.get(axis);
Node near = diff < 0 ? node.left : node.right;
Node far = diff < 0 ? node.right : node.left;
// 先搜索较近一侧
nearest(near, target, best);
// 若超平面距离小于当前最优距离,需搜索另一侧
if (diff * diff < best.dist) {
nearest(far, target, best);
}
}
k近邻与范围查询
// k近邻搜索:使用优先队列(大顶堆)维护最近的k个点
List<Point> kNearest(Point target, int k) {
PriorityQueue<Result> pq = new PriorityQueue<>(
Comparator.comparingDouble((Result r) -> r.dist).reversed());
kNearest(root, target, k, pq);
List<Point> result = new ArrayList<>();
for (Result r : pq) result.add(r.point);
return result;
}
void kNearest(Node node, Point target, int k, PriorityQueue<Result> pq) {
if (node == null) return;
double dist = node.point.squaredDistance(target);
if (pq.size() < k) {
pq.offer(new Result(node.point, dist));
} else if (dist < pq.peek().dist) {
pq.poll();
pq.offer(new Result(node.point, dist));
}
int axis = node.axis;
double diff = target.get(axis) - node.point.get(axis);
Node near = diff < 0 ? node.left : node.right;
Node far = diff < 0 ? node.right : node.left;
kNearest(near, target, k, pq);
if (diff * diff < (pq.size() < k ? Double.MAX_VALUE : pq.peek().dist)) {
kNearest(far, target, k, pq);
}
}
// 范围查询:返回矩形区域内的所有点
List<Point> rangeQuery(Point min, Point max) {
List<Point> result = new ArrayList<>();
rangeQuery(root, min, max, result);
return result;
}
void rangeQuery(Node node, Point min, Point max, List<Point> result) {
if (node == null) return;
if (inRange(node.point, min, max)) result.add(node.point);
int axis = node.axis;
if (min.get(axis) <= node.point.get(axis))
rangeQuery(node.left, min, max, result);
if (max.get(axis) >= node.point.get(axis))
rangeQuery(node.right, min, max, result);
}
boolean inRange(Point p, Point min, Point max) {
for (int i = 0; i < dimension; i++) {
if (p.get(i) < min.get(i) || p.get(i) > max.get(i)) return false;
}
return true;
}
主程序与性能测试
public static void main(String[] args) {
int n = 100000;
Random rand = new Random(42);
List<Point> points = new ArrayList<>();
for (int i = 0; i < n; i++) {
points.add(new Point(rand.nextDouble() * 1000,
rand.nextDouble() * 1000));
}
KDTree tree = new KDTree(2);
long t1 = System.currentTimeMillis();
tree.build(new ArrayList<>(points));
long t2 = System.currentTimeMillis();
System.out.println("构建KD树耗时: " + (t2 - t1) + "ms");
// 最近邻搜索测试
Point target = new Point(500, 500);
t1 = System.currentTimeMillis();
Point nearest = tree.nearestNeighbor(target);
t2 = System.currentTimeMillis();
System.out.println("最近邻: " + nearest + ", 距离: " +
Math.sqrt(target.squaredDistance(nearest)));
System.out.println("最近邻搜索耗时: " + (t2 - t1) + "ms");
// 暴力对比
t1 = System.currentTimeMillis();
Point brute = points.get(0);
double minDist = target.squaredDistance(brute);
for (Point p : points) {
double d = target.squaredDistance(p);
if (d < minDist) { minDist = d; brute = p; }
}
t2 = System.currentTimeMillis();
System.out.println("暴力搜索耗时: " + (t2 - t1) + "ms");
// k近邻测试
List<Point> knn = tree.kNearest(target, 5);
System.out.println("5近邻: " + knn);
}
}
算法分析
构建复杂度:每层排序 O(n log n),递归深度 O(log n),总构建时间 O(n log^2 n),若使用线性时间中位数算法可优化至 O(n log n)。
最近邻搜索:平均 O(log n),最坏 O(n)。剪枝条件 diff^2 < bestDist 是关键:若目标点到划分超平面的距离大于当前最优距离,则无需搜索另一侧子树。
空间复杂度:存储所有点 O(n)。