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樹的最近鄰搜索算法應用廣泛,其思路如下:
- 從根節點開始,遞歸的往下移。往左還是往右的決定方法與插入元素的方法一樣(如果輸入點在分區面的左邊則進入左子節點,在右邊則進入右子節點)。
- 一旦移動到葉節點,將該節點當作"當前最佳點"。
- 解開遞歸,並對每個經過的節點運行下列步驟:
- 如果當前所在點比當前最佳點更靠近輸入點,則將其變爲當前最佳點。
- 檢查另一邊子樹有沒有更近的點,如果有則從該節點往下找。
- 當根節點搜索完畢後完成最鄰近搜索。
本例中詳細代碼如下:
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樹的幾個功能,應該還有很多細節可以完善。謝謝觀看:)