KD树
概述
KD树(K-Dimensional Tree)是一种空间划分数据结构,用于组织K维空间中的点。通过交替选择不同维度进行划分,将空间递归分割成超矩形区域,支持高效的范围搜索和最近邻搜索。
核心思想 :每一层选择一个维度进行划分,将空间分成两部分。对于2D空间,第一层按X轴划分,第二层按Y轴划分,第三层再按X轴划分,以此类推。
KD树特点
特性
说明
多维索引
支持任意维度空间
二叉树结构
每个节点划分一个维度
交替分割
不同层使用不同维度分割
高效查询
最近邻查询平均 O(log n)
空间划分
每个节点对应一个超矩形区域
KD树结构可视化
2D KD树构建过程
点集 :{(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)}
第1层(按X轴划分) :
点集按X排序: (2,3), (4,7), (5,4), (7,2), (8,1), (9,6)
中位数: (7,2)
(7,2)
X < 7
X > 7
完整KD树 :
graph TB
subgraph KD树结构
n1(("7,2<br/>X轴分割"))
n2(("5,4<br/>Y轴分割"))
n3(("9,6<br/>Y轴分割"))
n4(("2,3"))
n5(("4,7"))
n6(("8,1"))
n1 --> n2 --> n4
n2 --> n5
n1 --> n3 --> n6
end
style n1 fill:#4CAF50,color:#fff
style n2 fill:#2196F3,color:#fff
style n3 fill:#2196F3,color:#fff
style n4 fill:#FF9800,color:#fff
style n5 fill:#FF9800,color:#fff
style n6 fill:#FF9800,color:#fff
空间划分可视化
空间划分过程
原始空间
(2,3)
(4,7)
(5,4)
(7,2)
(8,1)
(9,6)
划分规则 :第 d 层使用维度 d % K 进行划分。对于2D树,奇数层用X轴,偶数层用Y轴。
数据结构定义
C #define MAX_DIM 3
typedef struct {
double coords [ MAX_DIM ];
} Point ;
typedef struct KDNode {
Point point ; // 该节点存储的点
int splitDim ; // 分割维度
struct KDNode * left ; // 左子树
struct KDNode * right ; // 右子树
} KDNode ;
typedef struct {
KDNode * root ;
int dim ;
} KDTree ;
创建KD树
节点创建
C KDNode * createKDNode ( Point point , int splitDim ) {
KDNode * node = ( KDNode * ) malloc ( sizeof ( KDNode ));
node -> point = point ;
node -> splitDim = splitDim ;
node -> left = NULL ;
node -> right = NULL ;
return node ;
}
KDTree * createKDTree ( int dim ) {
KDTree * tree = ( KDTree * ) malloc ( sizeof ( KDTree ));
tree -> root = NULL ;
tree -> dim = dim ;
return tree ;
}
构建KD树
C int comparePoints ( const void * a , const void * b , int dim ) {
Point * pa = ( Point * ) a ;
Point * pb = ( Point * ) b ;
if ( pa -> coords [ dim ] < pb -> coords [ dim ]) return -1 ;
if ( pa -> coords [ dim ] > pb -> coords [ dim ]) return 1 ;
return 0 ;
}
KDNode * buildKDTree ( Point points [], int n , int depth , int k ) {
if ( n <= 0 ) return NULL ;
// 选择分割维度
int axis = depth % k ;
// 按当前维度排序
// 这里需要自定义比较函数
qsort ( points , n , sizeof ( Point ), /* 比较函数 */ );
// 选择中位数作为当前节点
int mid = n / 2 ;
KDNode * node = createKDNode ( points [ mid ], axis );
// 递归构建左右子树
node -> left = buildKDTree ( points , mid , depth + 1 , k );
node -> right = buildKDTree ( points + mid + 1 , n - mid - 1 , depth + 1 , k );
return node ;
}
构建过程示意
flowchart TD
A["点集P, 深度d"]
B["选择分割维度<br/>axis = d % K"]
C["按axis排序"]
D["选择中位数作为节点"]
E["构建左子树<br/>点集 = 左半部分"]
F["构建右子树<br/>点集 = 右半部分"]
G["返回节点"]
A --> B --> C --> D --> E --> F --> G
style A fill:#E3F2FD
style D fill:#4CAF50,color:#fff
style G fill:#E8F5E9
插入操作
C KDNode * insertKD ( KDNode * root , Point point , int depth , int k ) {
// 空节点,创建新节点
if ( root == NULL ) {
return createKDNode ( point , depth % k );
}
// 确定分割维度
int axis = root -> splitDim ;
// 根据当前维度决定插入左子树还是右子树
if ( point . coords [ axis ] < root -> point . coords [ axis ]) {
root -> left = insertKD ( root -> left , point , depth + 1 , k );
} else {
root -> right = insertKD ( root -> right , point , depth + 1 , k );
}
return root ;
}
void insert ( KDTree * tree , Point point ) {
tree -> root = insertKD ( tree -> root , point , 0 , tree -> dim );
}
插入示例 :
KD树插入点 (6,5)
插入前
(7,2)
(5,4)
(9,6)
(2,3)
(4,7)
(8,1)
→
插入后
(7,2)
(5,4)
(9,6)
(2,3)
(4,7)
(8,1)
(6,5)
路径: (7,2) → X≥7 → (9,6) → Y≤6 → (8,1) → X≤8 → 插入(6,5)
最近邻搜索
距离计算
C double distance ( Point a , Point b , int k ) {
double dist = 0 ;
for ( int i = 0 ; i < k ; i ++ ) {
double diff = a . coords [ i ] - b . coords [ i ];
dist += diff * diff ;
}
return sqrt ( dist );
}
double distanceSquared ( Point a , Point b , int k ) {
double dist = 0 ;
for ( int i = 0 ; i < k ; i ++ ) {
double diff = a . coords [ i ] - b . coords [ i ];
dist += diff * diff ;
}
return dist ;
}
最近邻搜索算法
C void nearestNeighbor ( KDNode * node , Point target , int k ,
KDNode ** best , double * bestDist ) {
if ( node == NULL ) return ;
// 计算当前节点到目标点的距离
double dist = distanceSquared ( node -> point , target , k );
if ( dist < * bestDist ) {
* bestDist = dist ;
* best = node ;
}
// 确定分割维度
int axis = node -> splitDim ;
double diff = target . coords [ axis ] - node -> point . coords [ axis ];
// 决定先搜索哪个子树
KDNode * first = ( diff < 0 ) ? node -> left : node -> right ;
KDNode * second = ( diff < 0 ) ? node -> right : node -> left ;
// 递归搜索第一子树
nearestNeighbor ( first , target , k , best , bestDist );
// 剪枝:如果分割超平面到目标的距离小于当前最优距离,需要搜索另一子树
if ( diff * diff < * bestDist ) {
nearestNeighbor ( second , target , k , best , bestDist );
}
}
KDNode * findNearest ( KDTree * tree , Point target ) {
KDNode * best = NULL ;
double bestDist = 1e18 ;
nearestNeighbor ( tree -> root , target , tree -> dim , & best , & bestDist );
return best ;
}
最近邻搜索示意
查询点 Q=(6,3) 的最近邻
(7,2)
(5,4)
(9,6)
(2,3)
(4,7)
(8,1)
搜索过程
1. 访问 (7,2), 距离 = √2 ≈ 1.41 best = (7,2), bestDist = 2
2. Q.x=6 < 7,先搜左子树 (5,4) 距离 = √2 ≈ 1.41, best = (5,4)
3. Q.y=3 < 4,先搜 (2,3) 距离 = 4,不更新best
4. 检查剪枝条件,继续搜索...
最终结果: (5,4) 或 (7,2)
graph TB
subgraph 最近邻搜索剪枝
A["计算节点到目标距离"]
B["更新最近邻"]
C["确定优先搜索子树"]
D["递归搜索"]
E{"超平面距离 < 最佳距离?"}
F["搜索另一子树"]
G["剪枝,跳过"]
A --> B --> C --> D --> E
E -->|是| F
E -->|否| G
end
style A fill:#E3F2FD
style G fill:#FFEBEE
剪枝原理 :如果分割超平面到目标点的距离超过当前最优距离,则另一子树中不可能存在更近的点,可以跳过搜索。
范围搜索
C void rangeSearch ( KDNode * node , Point min , Point max , int k ,
Point results [], int * count ) {
if ( node == NULL ) return ;
// 检查当前节点是否在范围内
int inRange = 1 ;
for ( int i = 0 ; i < k ; i ++ ) {
if ( node -> point . coords [ i ] < min . coords [ i ] ||
node -> point . coords [ i ] > max . coords [ i ]) {
inRange = 0 ;
break ;
}
}
if ( inRange ) {
results [( * count ) ++ ] = node -> point ;
}
// 确定分割维度
int axis = node -> splitDim ;
// 递归搜索可能包含范围内点的子树
if ( min . coords [ axis ] <= node -> point . coords [ axis ]) {
rangeSearch ( node -> left , min , max , k , results , count );
}
if ( max . coords [ axis ] >= node -> point . coords [ axis ]) {
rangeSearch ( node -> right , min , max , k , results , count );
}
}
范围搜索示意 :
查询范围 [4,8] × [2,6]
[4,8] × [2,6]
(2,3)
(5,4)
(7,2)
(4,7)
(8,1)
(9,6)
结果: (5,4), (7,2)
C++ 实现
C++ #include <vector>
#include <array>
#include <algorithm>
#include <cmath>
#include <memory>
template < int K >
class KDTree {
private :
struct Node {
std :: array < double , K > point ;
int splitDim ;
std :: unique_ptr < Node > left , right ;
Node ( const std :: array < double , K >& p , int d )
: point ( p ), splitDim ( d ) {}
};
std :: unique_ptr < Node > root ;
double distanceSquared ( const std :: array < double , K >& a ,
const std :: array < double , K >& b ) {
double dist = 0 ;
for ( int i = 0 ; i < K ; i ++ ) {
dist += ( a [ i ] - b [ i ]) * ( a [ i ] - b [ i ]);
}
return dist ;
}
void nearestNeighbor ( Node * node , const std :: array < double , K >& target ,
Node ** best , double * bestDist ) {
if ( ! node ) return ;
double dist = distanceSquared ( node -> point , target );
if ( dist < * bestDist ) {
* bestDist = dist ;
* best = node ;
}
int axis = node -> splitDim ;
double diff = target [ axis ] - node -> point [ axis ];
Node * first = ( diff < 0 ) ? node -> left . get () : node -> right . get ();
Node * second = ( diff < 0 ) ? node -> right . get () : node -> left . get ();
nearestNeighbor ( first , target , best , bestDist );
if ( diff * diff < * bestDist ) {
nearestNeighbor ( second , target , best , bestDist );
}
}
void rangeSearch ( Node * node ,
const std :: array < double , K >& min ,
const std :: array < double , K >& max ,
std :: vector < std :: array < double , K >>& results ) {
if ( ! node ) return ;
// 检查是否在范围内
bool inRange = true ;
for ( int i = 0 ; i < K ; i ++ ) {
if ( node -> point [ i ] < min [ i ] || node -> point [ i ] > max [ i ]) {
inRange = false ;
break ;
}
}
if ( inRange ) results . push_back ( node -> point );
int axis = node -> splitDim ;
if ( min [ axis ] <= node -> point [ axis ]) {
rangeSearch ( node -> left . get (), min , max , results );
}
if ( max [ axis ] >= node -> point [ axis ]) {
rangeSearch ( node -> right . get (), min , max , results );
}
}
public :
std :: array < double , K > findNearest ( const std :: array < double , K >& target ) {
Node * best = nullptr ;
double bestDist = 1e18 ;
nearestNeighbor ( root . get (), target , & best , & bestDist );
return best ? best -> point : std :: array < double , K > {};
}
std :: vector < std :: array < double , K >> rangeQuery (
const std :: array < double , K >& min ,
const std :: array < double , K >& max ) {
std :: vector < std :: array < double , K >> results ;
rangeSearch ( root . get (), min , max , results );
return results ;
}
};
KD树变体
变体
特点
适用场景
2D树
二维空间划分
图像处理、计算几何
3D树
三维空间划分
点云处理、3D图形
KD-B树
结合B树,减少树高度
大规模数据
BKD树
批量构建KD树
只读数据集
graph LR
subgraph KD树变体
A["KD树"]
B["2D树"]
C["3D树"]
D["KD-B树"]
E["BKD树"]
A --> B
A --> C
A --> D
A --> E
end
style A fill:#4CAF50,color:#fff
时间复杂度
操作
平均
最坏
说明
构建
O(n log n)
O(n log n)
每层排序
插入
O(log n)
O(n)
依赖树平衡性
删除
O(log n)
O(n)
需要重新平衡
最近邻
O(log n)
O(n)
点均匀分布时最优
范围查询
O(n^(1-1/k) + m)
O(n)
m为结果数
最坏情况 :当所有点都在一条线上时,KD树退化为链表,查询复杂度变为 O(n)。
空间复杂度
树节点 :O(n)
递归栈 :O(log n) 平均,O(n) 最坏
应用场景
应用领域
具体场景
机器学习
K近邻算法(KNN)
空间数据库
多维索引查询
图像处理
特征点匹配
计算机视觉
点云配准
计算几何
最近点对问题
游戏开发
碰撞检测
KD树 vs 其他空间索引
索引结构
最近邻
范围查询
动态更新
高维性能
KD树
O(log n)
良好
困难
较差
R树
O(log n)
良好
容易
较好
四叉树
O(log n)
良好
容易
仅限低维
网格索引
O(1)
良好
容易
空间爆炸
选择建议 :静态数据且维度较低(≤20)时,KD树是最近邻搜索的最佳选择。动态更新频繁或高维数据,考虑R树或近似最近邻算法。
实际应用示例
点云最近邻搜索
C++ // 3D点云最近邻
KDTree < 3 > tree ;
// ... 构建树
std :: array < double , 3 > query = { x , y , z };
auto nearest = tree . findNearest ( query );
K近邻搜索(KNN)
C++ template < int K , int Dim >
std :: vector < std :: array < double , Dim >> kNearest (
KDTree < Dim >& tree ,
const std :: array < double , Dim >& target ,
int k ) {
// 使用优先队列维护K个最近邻
std :: priority_queue < std :: pair < double , std :: array < double , Dim >>> pq ;
// ... 实现
return results ;
}
参考资料
Bentley, J. L. (1975). Multidimensional Binary Search Trees Used for Associative Searching
Friedman, J. H. et al. (1977). An Algorithm for Finding Best Matches in Logarithmic Expected Time
《计算几何:算法与应用》