KD-Tree构建及邻近点搜索 C++实现

本文介绍了KD-Tree的C++实现,包括构建原理,通过计算划分维度和平均值递归划分数据;遍历与删除,遍历方式同二叉树,删除可避免内存泄漏;近邻点搜索,有K近邻搜索和半径搜索两种方式;还提及用索引代替数据可减少内存消耗,并给出完整代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

KD-Tree构建及邻近点搜索 C++实现

1、KD-Tree构建原理

KD-Tree是一种树形数据结构,常用于点云处理。KD-Tree会在其非叶结点上对数据进行划分,在叶子结点上存储数据。
结点数据结构如下:
//KD树结点
struct TreeNode
{
	//数据维度,如三维点,二维点
	int dim = 1;

	//当前结点的分割维度,表示在那个维度进行分割
	int split_dim = -1;

	//当前结点所有叶子结点存储数据的大小
	int data_size = 0;

	//是否为左叶子结点,false表示当前结点是父结点的右结点
	bool is_left = true;

	//孩子结点
	TreeNode* chirld[2];

	//父结点
	TreeNode* father;

	//当前结点所有数据的平均值
	double* split_center;

	//当前结点所存储数据的索引,非叶子结点会在数据分割后清空内存
	vector<int> data;

	//有参构造,in_dim为输入的维度
	TreeNode(int in_dim)
	{
		dim = in_dim;
		chirld[0] = nullptr;
		chirld[1] = nullptr;
		father = nullptr;
		split_center = new double[dim];
		memset(split_center, 0, dim * sizeof(double));
	}
};
通常情况下,KD-Tree的构建过程有三步:
(1)计算最佳的划分维度以及当前待划分数据的平均值,通常以当前结点待划分数据的最大方差维度作为划分轴。
(2)根据划分轴及数据平均值对数据进行划分。
(3)递归左孩子,右孩子,直到所有数据划分完成。
当数据量特别大时,使用递归创建的方式将会使存储函数的栈空间都被使用完,后续在创建函数会弹出异常。
void KDTreeSearch::GetDataFeatrue(
	const vector<vector<double>>& data, 
	const vector<int>&            index, 
	int                           dim, 
	double*                       center, 
	int&                          split_dim)
{
	const int data_size = index.size();
	
	vector<double> average(dim, 0.);
	vector<double> standard(dim, 0.);

	for (int i = 0; i < data_size; i++)
	{
		int k = index[i];
		const auto& p = data[k];
		for (int j = 0; j < dim; j++)
		{
			average[j] += p[j];
		}
	}
	for (int j = 0; j < dim; j++)
	{
		average[j] /= data_size;
	}
	memcpy(center, average.data(), dim * sizeof(double));

	for (int i = 0; i < data_size; i++)
	{
		int k = index[i];
		const auto& p = data[k];
		for (int j = 0; j < dim; j++)
		{
			standard[j] += pow(p[j] - average[j], 2);
		}
	}
	for (int j = 0; j < dim; j++)
	{
		standard[j] = sqrt(standard[j] / data_size);
	}

	split_dim = 0;
	double max_val = standard[split_dim];
	for (int i = 1; i < dim; i++)
	{
		if (max_val < standard[i])
		{
			max_val = standard[i];
			split_dim = i;
		}
	}
}

void KDTreeSearch::CreateKDTree(const vector<vector<double>>& data, TreeNode* head)
{
	//当前结点只有一个数据时,退出
	if (head->data_size <= 1) return;

	//1.获取分割维度以及平均值
	GetDataFeatrue(data, head->data, head->dim, head->split_center, head->split_dim);

	//2.构造左右子树
	for (int i = 0; i < 2; i++)
	{
		head->chirld[i] = new TreeNode(head->dim);
		head->chirld[i]->is_left = 1 - i;            //表示当前结点是否为父结点的左孩子
		head->chirld[i]->father = head;              //将孩子结点指向父结点
	}

	//3.根据分割维度对数据进行分割
	for (int i = 0; i < head->data_size; i++)
	{
		int k = head->data[i];
		const auto& p = data[k];
		if (p[head->split_dim] < head->split_center[head->split_dim])
		{
			//小于分割值,加入左孩子
			head->chirld[0]->data.push_back(k);
			head->chirld[0]->data_size++;
		}
		else
		{
			//大于分割值,加入右孩子
			head->chirld[1]->data.push_back(k);
			head->chirld[1]->data_size++;
		}
	}

	//4.清空当前结点存储的数据
	vector<int>().swap(head->data);

	//5.递归
	for (int i = 0; i < 2; i++)
	{
		CreateKDTree(data, head->chirld[i]);
	}
}

2、KD-Tree的遍历与删除

KD-Tree的遍历与二叉树的遍历相同,可以使用先序、中序、后序等遍历方式,但KD-Tree只有叶子结点存储真正的数据,所以非叶结点直接跳过即可。下面为KD-Tree的后续遍历方式。
void KDTreeSearch::ErgodicKDTree(TreeNode* head, vector<int>& index)
{
	if (head == nullptr) return;
	//后续遍历叶子结点所有元素
	ErgodicKDTree(head->chirld[0], index);
	ErgodicKDTree(head->chirld[1], index);
	if (head->chirld[0] == nullptr)
		index.insert(index.end(), head->data.begin(), head->data.end());
}
<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值