簡易KD樹(C++)

k-d樹介紹和類定義

k-d樹( k-維的縮寫)是在k歐幾里德空間組織的數據結構,可用在空間數據庫和遊戲優化等領域。具體來說,k-d樹是每個節點都爲k維點的二叉樹。所有非葉子節點可以視作用一個超平面把空間分割成兩個半空間

下面是我按着描述粗糙寫的一棵簡易KD樹,完成了樹的創建、插入、按區域搜索、最近鄰搜索等操作。本例中多維點由vector<double>類型表示,其size表示點的維數。KD樹的類定義如下。

#include<iostream>
#include<vector>
#include<numeric>
#include<algorithm>
using namespace std;

#ifndef KDTREE_H
#define KDTREE_H


class KDTree
{
	struct KDNode
	{
		bool m_isLeaf;
		vector<double>m_point;//k維的點
		int m_split;//在第幾維被分開
		KDNode*m_parentNode;
		KDNode*m_leftNode;
		KDNode*m_rightNode;
	};
private:
	using st = vector<double>::size_type;

	KDNode*m_root;//根節點
	int m_k;//k維
	int m_pointNum;//點的數量
	vector<vector<double>>m_points;//點的集合
public:
	//下面是給用戶提供的接口
	KDTree(int k, vector<vector<double>>allpoints) :m_k(k)
	{
		m_root = new KDNode();
		m_root->m_isLeaf = false;
		m_root->m_leftNode = nullptr;
		m_root->m_rightNode = nullptr;

		m_pointNum = allpoints.size();
		m_points = allpoints;
		KDTreeBuild(allpoints, m_root);
	}

	void Insert(vector<double>newpoint);

	vector<vector<double>>SearchByRegion(vector<double>from, vector<double>to)const;

	vector<double> SearchNearestNeighbor(vector<double> goalpoint);
private:
        ~KDTree();
	void KDTreeBuild(vector<vector<double>>points, KDNode* root);
	//遍歷尋找一個樹中與目標點最近的點
	void SearchNearestByTree(vector<double> goalpoint, double&curdis, const KDNode*treeroot, vector<double>&nearestpoint);
	//遞歸查找區域內的點
	void SearchRecu(vector<double>from, vector<double>to, const KDNode*temp, vector<vector<double>>&nodes)const;
	//計算兩個點的距離
	double CalDistance(vector<double> point1, vector<double> point2);
};

#endif // !1

kd樹的構建

樹的構建思路如下圖

樹的構建代碼:

void KDTree::KDTreeBuild(vector<vector<double>>points, KDNode* root)
{
	int indexpart = 0, max = 0;
	vector<double>temp;
	for (st i = 0; i < m_k; i++)
	{
		temp.clear();
		for each (auto var in points)temp.push_back(var[i]);

		//計算平均值
		double ave = accumulate(temp.begin(), temp.end(), 0.0) / m_pointNum;

		//計算方差
		double accum = 0.0;
		for each(auto var in temp) 	accum += (var - ave)*(var - ave);

		if (accum > max)
		{
			max = accum;
			indexpart = i;
		}
	}
	//此時indexpart的值爲當前要進行分裂的維數
	temp.clear();
	for each (auto var in points)temp.push_back(var[indexpart]);
	//找到中值
	sort(temp.begin(), temp.end());
	double median = temp[(temp.size()) >> 1];
	//將點分爲左右兩部分
	vector<vector<double>>leftpoints, rightpoints;
	for each (auto var in points)
	{
		if (var[indexpart] < median)
			leftpoints.push_back(var);

		if (var[indexpart] == median)
		{
			root->m_split = indexpart + 1;
			root->m_point = var;
		}
		if (var[indexpart] > median)
			rightpoints.push_back(var);
	}
	//遞歸建樹
	if (leftpoints.size() == 0 && rightpoints.size() == 0)root->m_isLeaf = true;
	if (leftpoints.size() != 0)
	{
		root->m_leftNode = new KDNode();
		root->m_leftNode->m_parentNode = root;
		KDTreeBuild(leftpoints, root->m_leftNode);
	}
	if (rightpoints.size() != 0)
	{
		root->m_rightNode = new KDNode();
		root->m_rightNode->m_parentNode = root;
		KDTreeBuild(rightpoints, root->m_rightNode);
	}
}

節點的插入

節點的插入與二叉樹類似(二叉樹就是一維kd樹),即從根節點開始,在每一個層比較對應的座標值,如果小於則跟蹤到左子節點,若大於則跟蹤到右子節點,當一個空指針出現時,就找到了結點將要插入的位置。代碼如下:

void KDTree::Insert(vector<double>newpoint)
{
	if (newpoint.size() != m_k)
	{
		cerr << "插入點維數與KD樹不匹配" << endl;
		exit(1);
	}
	KDNode*temp = m_root;
	if (temp == nullptr)//若樹爲空樹
	{
		temp = new KDNode();
		temp->m_isLeaf = true;
		temp->m_split = 1;
		temp->m_point = newpoint;
		return;
	}
	if (temp->m_isLeaf)//若樹只有一個節點,做好被插入的準備
	{
		temp->m_isLeaf = false;
		int max = 0, partindex = 0;
		for (st i = 0; i < m_k; i++)
		{
			double delta = abs(newpoint[i] - temp->m_point[i]);
			if (delta > max)
			{
				max = delta;
				temp->m_split = i + 1;
			}
		}
	}
	while (true)
	{
		int partindex = temp->m_split - 1;
		KDNode*nextnode;
		if (newpoint[partindex] > temp->m_point[partindex])
		{
			if (temp->m_rightNode == nullptr)//插入
			{
				temp->m_rightNode = new KDNode();
				temp->m_rightNode->m_parentNode = temp;
				temp->m_rightNode->m_isLeaf = true;
				temp->m_rightNode->m_split = 1;
				temp->m_rightNode->m_point = newpoint;
				break;
			}
			else nextnode = temp->m_rightNode;
		}
		else
		{
			if (temp->m_leftNode == nullptr)//插入
			{
				temp->m_leftNode = new KDNode();
				temp->m_leftNode->m_parentNode = temp;
				temp->m_leftNode->m_isLeaf = true;
				temp->m_leftNode->m_split = 1;
				temp->m_leftNode->m_point = newpoint;
				break;
			}
			else nextnode = temp->m_leftNode;
		}

		if (nextnode->m_isLeaf)//如果下一個點是葉子節點,做好被插入的準備
		{
			nextnode->m_isLeaf = false;
			int max = 0, partindex = 0;
			for (st i = 0; i < m_k; i++)
			{
				double delta = abs(newpoint[i] - nextnode->m_point[i]);
				if (delta > max)
				{
					max = delta;
					nextnode->m_split = i + 1;
				}
			}
		}
		temp = nextnode;//往下走
	}
}

根據區域進行查詢

所謂根據區域進行查詢,即輸入一個區域(需與KD樹維數一致),得到KD樹中在該區域內的所有座標點。該搜索往往是多條路向下搜索,並將沿路的符合條件的點都進行記錄。在本系統中,輸入區域的方式爲由用戶輸入兩個點,起點的所有座標需均小於終點,這樣的兩個點就能代表一個查詢區域,該模塊的詳細代碼如下。

vector<vector<double>>KDTree::SearchByRegion(vector<double>from, vector<double>to)const
{
	vector<vector<double>>result;
	if (from.size() != m_k || to.size() != m_k)
	{
		cerr << "搜索區域維數與KD樹不匹配" << endl;
		exit(1);
	}
	for (st i = 0; i < m_k; i++)
	{
		if (from[i] > to[i])
		{
			cerr << "請保證區域起始點的所有座標值小於區域終點" << endl;
			exit(1);
		}
	}
	SearchRecu(from, to, m_root, result);
	return result;
}

void KDTree::SearchRecu(vector<double>from, vector<double>to, const KDNode*temp, vector<vector<double>>&nodes)const
{
	if (temp == nullptr)return;

	int partindex = temp->m_split - 1;
	int value = temp->m_point[partindex];
	if (from[partindex] <= value && to[partindex] >= value)//當前點在範圍內
	{
		bool inregion = true;
		for (st i = 0; i < m_k; i++)
		{
			if (from[i] > temp->m_point[i] || to[i] < temp->m_point[i])
				inregion = false;
		}
		if (inregion)nodes.push_back(temp->m_point);
		SearchRecu(from, to, temp->m_leftNode, nodes);
		SearchRecu(from, to, temp->m_rightNode, nodes);
	}
	else if (value > to[partindex])
		SearchRecu(from, to, temp->m_leftNode, nodes);

	else if (value < from[partindex])
		SearchRecu(from, to, temp->m_rightNode, nodes);
}

最近鄰搜索

kd樹的最近鄰搜索算法應用廣泛,其思路如下:

  1. 從根節點開始,遞歸的往下移。往左還是往右的決定方法與插入元素的方法一樣(如果輸入點在分區面的左邊則進入左子節點,在右邊則進入右子節點)。
  2. 一旦移動到葉節點,將該節點當作"當前最佳點"。
  3. 解開遞歸,並對每個經過的節點運行下列步驟:
    1. 如果當前所在點比當前最佳點更靠近輸入點,則將其變爲當前最佳點。
    2. 檢查另一邊子樹有沒有更近的點,如果有則從該節點往下找。
  4. 當根節點搜索完畢後完成最鄰近搜索。

本例中詳細代碼如下:

double KDTree::CalDistance(vector<double> point1, vector<double> point2)
{
	if (point1.size() != point2.size())
	{
		cerr << "兩個點的維度不相同";
		exit(1);
	}
	double distance = 0.0;
	for (st i = 0; i < point1.size(); i++)
		distance += pow((point1[i] - point2[i]), 2);

	return sqrt(distance);
}

vector<double> KDTree::SearchNearestNeighbor(vector<double> goalpoint)
{
	vector<double>nearestpoint;
	KDNode*temp = m_root;
	//找到最鄰近的葉子節點
	while (!temp->m_isLeaf)
	{
		int partindex = temp->m_split - 1;
		if (temp->m_leftNode != nullptr && goalpoint[partindex] < temp->m_point[partindex])
		{
			temp = temp->m_leftNode;
		}
		else if (temp->m_rightNode != nullptr)
		{
			temp = temp->m_rightNode;
		}
	}
	nearestpoint = temp->m_point;
	double curdis = CalDistance(goalpoint, nearestpoint);
	//向上回溯
	bool isleft = false;
	while (temp != m_root)
	{
		isleft = (temp == temp->m_parentNode->m_leftNode);//判斷當前點是否其父節點的左子節點

		temp = temp->m_parentNode;//指針向上跟蹤
		if (CalDistance(goalpoint, temp->m_point) < curdis)
		{
			nearestpoint = temp->m_point;
			curdis = CalDistance(goalpoint, nearestpoint);
		}
		int partindex = temp->m_split - 1;
		//若圓與另一區域有相交,即另一邊子樹可能有更近的點
		if (curdis > abs(temp->m_point[partindex] - goalpoint[partindex]))
		{
			if (isleft)
			{
				SearchNearestByTree(goalpoint, curdis, temp->m_rightNode, nearestpoint);
			}
			else SearchNearestByTree(goalpoint, curdis, temp->m_leftNode, nearestpoint);
		}
	}
	return nearestpoint;
}

void KDTree::SearchNearestByTree(vector<double> goalpoint, double&curdis, const KDNode*treeroot, vector<double>&nearestpoint)
{
	if (treeroot == nullptr)return;
	double newdis = CalDistance(goalpoint, treeroot->m_point);
	if (newdis < curdis)
	{
		curdis = newdis;
		nearestpoint = treeroot->m_point;
	}
	SearchNearestByTree(goalpoint, curdis, treeroot->m_leftNode, nearestpoint);
	SearchNearestByTree(goalpoint, curdis, treeroot->m_rightNode, nearestpoint);
}

本人在這裏只是粗略的實現了kd樹的幾個功能,應該還有很多細節可以完善。謝謝觀看:)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章