KD-Tree
介绍
kd-tree,是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。K-D树是二进制空间分割树的特殊的情况。
用法
QT图形视图框架绘制曲线图和Smith图_qtcreator画smith图-CSDN博客 在这篇博客中绘图的mark点支持,最初我是使用set的自排序功能来查找最近的点,虽然效率能跟得上使用,但是这种方式弊端非常明显。每次移动的时候就需要重写计算距离,然后重新生成这个set。这个方式浪费不仅浪费空间也浪费时间。
所以在后续更新的时候使用了KD-Tree做了最邻近点的计算,比上述暴力的的排序方式快了不止30倍(这里是只测试了4W个点,使用排序打印的ms为0,使用上面的方式打印的在30左右)。
接下来展示我的代码:
kdtree.h
#ifndef TURBOPLOT_KDTREE_H
#define TURBOPLOT_KDTREE_H
/******************************************************************
* @brief 求最邻近点算法
* @author turbo
* @class KDTree
* @date 2023年11月2日10:59:27
* @version 0.0.1
* @property root_ kdtree跟节点
******************************************************************/
#include <QPointF>
#include <QVector>
namespace turbo
{
class KDTree
{
public:
struct KDTreeNode
{
QPointF point;
KDTreeNode* left;
KDTreeNode* right;
};
KDTree();
~KDTree();
/**
* @brief 设置点,根据点生成对应的KDTree
* @param points
*/
void setPoints(QVector<QPointF> &points);
/**
* @brief 获取离入参点最近的点
* @param point
* @return
*/
QPointF getNearestPoint(const QPointF &point);
protected:
/**
* @brief 计算两个点之间的欧氏距离
* @param point1
* @param point2
* @return
*/
static double euclideanDistance(const QPointF& point1, const QPointF& point2);
/**
* @brief 根据两个点的x比较大小
* @param point1
* @param point2
* @return
*/
static bool compareX(const QPointF& point1, const QPointF& point2);
/**
* @brief 根据两个点的y比较大小
* @param point1
* @param point2
* @return
*/
static bool compareY(const QPointF& point1, const QPointF& point2);
/**
* @brief 构建KD树
* @param points
* @param compareX
* @return
*/
KDTreeNode* buildKdTree(QVector<QPointF>& points, int depth);
/**
* @brief 计算最邻近点
* @param root
* @param target
* @param nearest
* @param minDist
* @param depth
*/
void findNearestNeighbor(KDTreeNode* root, QPointF target, QPointF& nearest, double& minDist, int depth);
/**
* @brief 清理kdtree 释放空间
* @param root
*/
void clearKdTree(KDTreeNode* root);
private:
KDTreeNode *root_;
};
}
#endif //TURBOPLOT_KDTREE_H
kdtree.cpp
#include "kdtree.h"
#include <algorithm>
#include <cmath>
namespace turbo
{
KDTree::KDTree() : root_(nullptr)
{
}
KDTree::~KDTree()
{
clearKdTree(root_);
}
// 清理KD树
void KDTree::clearKdTree(KDTreeNode* root)
{
if (root == nullptr)
{
return;
}
clearKdTree(root->left);
clearKdTree(root->right);
delete root;
root = nullptr;
}
double KDTree::euclideanDistance(const QPointF &point1, const QPointF &point2)
{
double dx = point1.x() - point2.x();
double dy = point1.y() - point2.y();
return std::sqrt(dx*dx + dy*dy);
}
bool KDTree::compareX(const QPointF &point1, const QPointF &point2)
{
return point1.x() < point2.x();
}
bool KDTree::compareY(const QPointF &point1, const QPointF &point2)
{
return point1.y() < point2.y();
}
KDTree::KDTreeNode *KDTree::buildKdTree(QVector<QPointF> &points, int depth)
{
if (points.isEmpty())
{
return nullptr;
}
// 根据分割轴对点进行排序
if (depth % 2 == 0)
{
std::sort(points.begin(), points.end(), compareX);
}
else
{
std::sort(points.begin(), points.end(), compareY);
}
// 选择中间点作为根节点
int mid = points.size() / 2;
auto* root = new KDTreeNode();
root->point = points[mid];
QVector<QPointF> points1 = QVector<QPointF>(points.begin(), points.begin() + mid);
QVector<QPointF> points2 = QVector<QPointF>(points.begin() + mid + 1, points.end());
root->left = buildKdTree(points1, depth + 1);
root->right = buildKdTree(points2, depth + 1);
return root;
}
// 在KD树中查找最近邻点
void KDTree::findNearestNeighbor(KDTree::KDTreeNode* root, QPointF target, QPointF& nearest, double& minDist, int depth)
{
if (root == nullptr)
{
return;
}
// 计算当前节点到目标点的欧氏距离
double dist = euclideanDistance(root->point, target);
// 如果当前节点更近,则更新最近邻点和最小距离
if (dist < minDist)
{
nearest = root->point;
minDist = dist;
}
// 根据深度选择分割轴
int axis = depth % 2;
// 根据分割轴比较目标点和当前节点,并决定遍历顺序
if ((axis == 0 && target.x() < root->point.x()) || (axis == 1 && target.y() < root->point.y()))
{
findNearestNeighbor(root->left, target, nearest, minDist, depth + 1);
if ((axis == 0 && target.x() + minDist >= root->point.x()) || (axis == 1 && target.y() + minDist >= root->point.y()))
{
findNearestNeighbor(root->right, target, nearest, minDist, depth + 1);
}
}
else
{
findNearestNeighbor(root->right, target, nearest, minDist, depth + 1);
if ((axis == 0 && target.x() - minDist <= root->point.x()) || axis == 1 && target.y() - minDist <= root->point.y())
{
findNearestNeighbor(root->left, target, nearest, minDist, depth + 1);
}
}
}
void KDTree::setPoints(QVector<QPointF> &points)
{
clearKdTree(root_);
root_ = buildKdTree(points, 0);
}
QPointF KDTree::getNearestPoint(const QPointF &point)
{
QPointF nearest;
double minDist = std::numeric_limits<double>::max();
// 在KD树中查找最近邻点
findNearestNeighbor(root_, point, nearest, minDist, 0);
return nearest;
}
}
这里面就说一下构建时候的思路和查找时候的思路
KD-Tree的构建
KDTree::KDTreeNode *KDTree::buildKdTree(QVector<QPointF> &points, int depth)
{
if (points.isEmpty())
{
return nullptr;
}
// 根据分割轴对点进行排序
if (depth % 2 == 0)
{
std::sort(points.begin(), points.end(), compareX);
}
else
{
std::sort(points.begin(), points.end(), compareY);
}
// 选择中间点作为根节点
int mid = points.size() / 2;
auto* root = new KDTreeNode();
root->point = points[mid];
QVector<QPointF> points1 = QVector<QPointF>(points.begin(), points.begin() + mid);
QVector<QPointF> points2 = QVector<QPointF>(points.begin() + mid + 1, points.end());
root->left = buildKdTree(points1, depth + 1);
root->right = buildKdTree(points2, depth + 1);
return root;
}
- 检查点集是否为空,如果是则返回空指针。
- 根据当前深度选择分割轴,偶数深度使用compareX函数排序,奇数深度使用compareY函数排序。
- 选择中间点作为根节点,并创建一个新的KDTreeNode对象来存储该点。
- 将点集分为两部分,分别是从起始位置到中间位置的点集points1和从中间位置+1到结束位置的点集points2。
- 递归地调用buildKdTree函数来构建左子树,并将其赋值给根节点的left指针。
- 递归地调用buildKdTree函数来构建右子树,并将其赋值给根节点的right指针。
这么做的主要目的就是将一整个区域分成不同的区域
最邻近点的查找
// 在KD树中查找最近邻点
void KDTree::findNearestNeighbor(KDTree::KDTreeNode* root, QPointF target, QPointF& nearest, double& minDist, int depth)
{
if (root == nullptr)
{
return;
}
// 计算当前节点到目标点的欧氏距离
double dist = euclideanDistance(root->point, target);
// 如果当前节点更近,则更新最近邻点和最小距离
if (dist < minDist)
{
nearest = root->point;
minDist = dist;
}
// 根据深度选择分割轴
int axis = depth % 2;
// 根据分割轴比较目标点和当前节点,并决定遍历顺序
if ((axis == 0 && target.x() < root->point.x()) || (axis == 1 && target.y() < root->point.y()))
{
findNearestNeighbor(root->left, target, nearest, minDist, depth + 1);
if ((axis == 0 && target.x() + minDist >= root->point.x()) || (axis == 1 && target.y() + minDist >= root->point.y()))
{
findNearestNeighbor(root->right, target, nearest, minDist, depth + 1);
}
}
else
{
findNearestNeighbor(root->right, target, nearest, minDist, depth + 1);
if ((axis == 0 && target.x() - minDist <= root->point.x()) || axis == 1 && target.y() - minDist <= root->point.y())
{
findNearestNeighbor(root->left, target, nearest, minDist, depth + 1);
}
}
}
这里注释都写的很清楚了,本质上就是递归查找。这边要说明白得绘图了,单纯用文字描述的话,确实有点不太清楚。但是我绘图技术太菜,直接看代码理解吧。
判断的地方,因为之前划分的时候就是按照深度划分的,这里根据深度来决定是使用x还是y来找最近点。