C++實(shí)現(xiàn)KDTree 附完整代碼
簡介
k-d樹(k-dimensional),是一種分割k維數(shù)據(jù)空間的數(shù)據(jù)結(jié)構(gòu)(對(duì)數(shù)據(jù)點(diǎn)在k維空間中劃分的一種數(shù)據(jù)結(jié)構(gòu)),主要應(yīng)用于多維空間關(guān)鍵數(shù)據(jù)的搜索(如:范圍搜索和最近鄰搜索)。
kdTree概念
kd-tree或者k維樹是計(jì)算機(jī)科學(xué)中使用的一種數(shù)據(jù)結(jié)構(gòu),用來組織表示k維空間中點(diǎn)的集合。它是一種帶有其他約束條件的二分查找樹。Kd-tree對(duì)于區(qū)間和近鄰搜索十分有用。一般位于三維空間中的鄰域搜索常用kd-tree,因此本文中所有的kd-tree都是三維的kd-tree。
舉例

上圖就是一顆kdtree,可以看出kdtree是二叉搜索樹的變種。
kdtree的性質(zhì):
- kdtree具有平衡的特質(zhì),兩樹葉的高度差不超過1。(樹越平衡代表著分割得越平均,搜索的時(shí)間越少)
- 數(shù)據(jù)只存放在葉子結(jié)點(diǎn),而根結(jié)點(diǎn)和中間結(jié)點(diǎn)存放一些空間劃分信息(例如劃分維度、劃分值)。
- 將每一個(gè)元組按0排序(第一項(xiàng)序號(hào)為0,第二項(xiàng)序號(hào)為1,第三項(xiàng)序號(hào)為2),在樹的第n層,第 n%3 項(xiàng)被用粗體顯示,而這些被粗體顯示的樹就是作為二叉搜索樹的key值,比如,根節(jié)點(diǎn)的左子樹中的每一個(gè)節(jié)點(diǎn)的第一個(gè)項(xiàng)均小于根節(jié)點(diǎn)的的第一項(xiàng),右子樹的節(jié)點(diǎn)中第一項(xiàng)均大于根節(jié)點(diǎn)的第一項(xiàng),子樹依次類推。
分割的作用
一維
對(duì)于一個(gè)標(biāo)準(zhǔn)的BSTree,每個(gè)節(jié)點(diǎn)只有一個(gè)key值。

將key值對(duì)應(yīng)到一維的坐標(biāo)軸上。

根節(jié)點(diǎn)對(duì)應(yīng)的就是2,左子樹都在2的左邊,右子樹都在2的右邊,整個(gè)一維空間就被根節(jié)點(diǎn)分割成了兩個(gè)部分,當(dāng)要查找結(jié)點(diǎn)0的時(shí)候,由于是在2的左邊,所以可以放心的只搜索左子樹的部分。整個(gè)搜索的過程可以看成不斷分割搜索區(qū)間的過程,直到找到目標(biāo)節(jié)點(diǎn)。
二維
這樣的分割可以擴(kuò)展到二維甚至更多維的情況。
但是問題來了,二維的節(jié)點(diǎn)怎么比較大???
在BSTree中,節(jié)點(diǎn)分割的是一維數(shù)軸,那么在二維中,就應(yīng)當(dāng)是分割平面了,就像這樣:

黃色的點(diǎn)作為根節(jié)點(diǎn),上面的點(diǎn)歸左子樹,下面的點(diǎn)歸右子樹,接下來再不斷地劃分,最后得到一棵樹就是赫赫有名的BSPTree(binary space partitioning tree). 分割的那條線叫做分割超平面(splitting hyperplane),在一維中是一個(gè)點(diǎn),二維中是線,三維的是面。
n維
KDTree就是超平面都垂直于軸的BSPTree。同樣的數(shù)據(jù)集,用KDTree劃分之后就是這樣:

黃色節(jié)點(diǎn)就是Root節(jié)點(diǎn),下一層是紅色,再下一層是綠色,再下一層是藍(lán)色。為了更好的理解KDTree的分割,我們?cè)趫D形中來看一下搜索的過程,假設(shè)現(xiàn)在需要搜尋右下角的一個(gè)點(diǎn),首先要做的就是比較這個(gè)點(diǎn)的x坐標(biāo)和root點(diǎn)的x坐標(biāo)值,由于x坐標(biāo)值大于root節(jié)點(diǎn)的x坐標(biāo),所以只需要在右邊搜尋,接下來,要比較該節(jié)點(diǎn)和右邊紅色節(jié)點(diǎn)y值得大小...后面依此類推。整個(gè)過程如下圖:
1.

2.

3.

關(guān)于kdtree的重要問題
一.樹的建立
1.節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu)
定義:
Node-data - 數(shù)據(jù)矢量, 數(shù)據(jù)集中某個(gè)數(shù)據(jù)點(diǎn),是n維矢量(這里也就是k維)
Range - 空間矢量, 該節(jié)點(diǎn)所代表的空間范圍
split - 整數(shù), 垂直于分割超平面的方向軸序號(hào)
Left - k-d樹, 由位于該節(jié)點(diǎn)分割超平面左子空間內(nèi)所有數(shù)據(jù)點(diǎn)所構(gòu)成的k-d樹
Right - k-d樹, 由位于該節(jié)點(diǎn)分割超平面右子空間內(nèi)所有數(shù)據(jù)點(diǎn)所構(gòu)成的k-d樹
parent - k-d樹, 父節(jié)點(diǎn)
2. 優(yōu)化
1.切分維度優(yōu)化
構(gòu)建開始前,對(duì)比數(shù)據(jù)點(diǎn)在各維度的分布情況,數(shù)據(jù)點(diǎn)在某一維度坐標(biāo)值的方差越大分布越分散,方差越小分布越集中。從方差大的維度開始切分可以取得很好的切分效果及平衡性。
2.中值選擇優(yōu)化
第一種,算法開始前,對(duì)原始數(shù)據(jù)點(diǎn)在所有維度進(jìn)行一次排序,存儲(chǔ)下來,然后在后續(xù)的中值選擇中,無須每次都對(duì)其子集進(jìn)行排序,提升了性能。
第二種,從原始數(shù)據(jù)點(diǎn)中隨機(jī)選擇固定數(shù)目的點(diǎn),然后對(duì)其進(jìn)行排序,每次從這些樣本點(diǎn)中取中值,來作為分割超平面。該方式在實(shí)踐中被證明可以取得很好性能及很好的平衡性。
2.最近鄰域搜索(Nearest-Neighbor Lookup)
給定一個(gè)KDTree和一個(gè)節(jié)點(diǎn),求KDTree中離這個(gè)節(jié)點(diǎn)最近的節(jié)點(diǎn).(這個(gè)節(jié)點(diǎn)就是最臨近點(diǎn))
這里距離的求法用的是歐式距離。

基本的思路很簡單:首先通過二叉樹搜索(比較待查詢節(jié)點(diǎn)和分裂節(jié)點(diǎn)的分裂維的值,小于等于就進(jìn)入左子樹分支,等于就進(jìn)入右子樹分支直到葉子結(jié)點(diǎn)),順著“搜索路徑”很快能找到最近鄰的近似點(diǎn),也就是與待查詢點(diǎn)處于同一個(gè)子空間的葉子結(jié)點(diǎn);然后再回溯搜索路徑,并判斷搜索路徑上的結(jié)點(diǎn)的其他子結(jié)點(diǎn)空間中是否可能有距離查詢點(diǎn)更近的數(shù)據(jù)點(diǎn),如果有可能,則需要跳到其他子結(jié)點(diǎn)空間中去搜索(將其他子結(jié)點(diǎn)加入到搜索路徑)。重復(fù)這個(gè)過程直到搜索路徑為空。
這里還有幾個(gè)細(xì)節(jié)需要注意一下,如下圖,假設(shè)標(biāo)記為星星的點(diǎn)是 test point, 綠色的點(diǎn)是找到的近似點(diǎn),在回溯過程中,需要用到一個(gè)隊(duì)列,存儲(chǔ)需要回溯的點(diǎn),在判斷其他子節(jié)點(diǎn)空間中是否有可能有距離查詢點(diǎn)更近的數(shù)據(jù)點(diǎn)時(shí),做法是以查詢點(diǎn)為圓心,以當(dāng)前的最近距離為半徑畫圓,這個(gè)圓稱為候選超球(candidate hypersphere),如果圓與回溯點(diǎn)的軸相交,則需要將軸另一邊的節(jié)點(diǎn)都放到回溯隊(duì)列里面來。

判斷軸是否與候選超球相交的方法可以參考下圖:

關(guān)鍵代碼
構(gòu)建KDTree
void KDTree::buildKdTree(KDTree *tree, vector<vector<double>> data, unsigned int depth)
{
//樣本的數(shù)量
unsigned long samplesNum = data.size();
//終止條件
if (samplesNum == 0)
{
return;
}
if (samplesNum == 1)
{
tree->root = data[0];
return;
}
//樣本的維度
unsigned long k = data[0].size();//坐標(biāo)軸個(gè)數(shù)
vector<vector<double> > transData = Transpose(data);
//選擇切分屬性
unsigned splitAttribute = depth % k;
vector<double> splitAttributeValues = transData[splitAttribute];
//選擇切分值
double splitValue = findMiddleValue(splitAttributeValues);
//cout << "splitValue" << splitValue << endl;
// 根據(jù)選定的切分屬性和切分值,將數(shù)據(jù)集分為兩個(gè)子集
vector<vector<double> > subset1;
vector<vector<double> > subset2;
for (unsigned i = 0; i < samplesNum; ++i)
{
if (splitAttributeValues[i] == splitValue && tree->root.empty())
tree->root = data[i];
else
{
if (splitAttributeValues[i] < splitValue)
subset1.push_back(data[i]);
else
subset2.push_back(data[i]);
}
}
//子集遞歸調(diào)用buildKdTree函數(shù)
tree->left_child = new KDTree;
tree->left_child->parent = tree;
tree->right_child = new KDTree;
tree->right_child->parent = tree;
buildKdTree(tree->left_child, subset1, depth + 1);
buildKdTree(tree->right_child, subset2, depth + 1);
}
查詢目標(biāo)點(diǎn)的最近鄰點(diǎn)
vector<double> KDTree::searchNearestNeighbor(vector<double> goal, KDTree *tree)
{
/*第一步:在kd樹中找出包含目標(biāo)點(diǎn)的葉子結(jié)點(diǎn):從根結(jié)點(diǎn)出發(fā),
遞歸的向下訪問kd樹,若目標(biāo)點(diǎn)的當(dāng)前維的坐標(biāo)小于切分點(diǎn)的
坐標(biāo),則移動(dòng)到左子結(jié)點(diǎn),否則移動(dòng)到右子結(jié)點(diǎn),直到子結(jié)點(diǎn)為
葉結(jié)點(diǎn)為止,以此葉子結(jié)點(diǎn)為“當(dāng)前最近點(diǎn)”
*/
unsigned long k = tree->root.size();//計(jì)算出數(shù)據(jù)的維數(shù)
unsigned d = 0;//維度初始化為0,即從第1維開始
KDTree* currentTree = tree;
vector<double> currentNearest = currentTree->root;
while(!currentTree->is_leaf())
{
unsigned index = d % k;//計(jì)算當(dāng)前維
if (currentTree->right_child->is_empty() || goal[index] < currentNearest[index])
{
currentTree = currentTree->left_child;
}
else
{
currentTree = currentTree->right_child;
}
++d;
}
currentNearest = currentTree->root;
/*第二步:遞歸地向上回退, 在每個(gè)結(jié)點(diǎn)進(jìn)行如下操作:
(a)如果該結(jié)點(diǎn)保存的實(shí)例比當(dāng)前最近點(diǎn)距離目標(biāo)點(diǎn)更近,則以該例點(diǎn)為“當(dāng)前最近點(diǎn)”
(b)當(dāng)前最近點(diǎn)一定存在于某結(jié)點(diǎn)一個(gè)子結(jié)點(diǎn)對(duì)應(yīng)的區(qū)域,檢查該子結(jié)點(diǎn)的父結(jié)點(diǎn)的另
一子結(jié)點(diǎn)對(duì)應(yīng)區(qū)域是否有更近的點(diǎn)(即檢查另一子結(jié)點(diǎn)對(duì)應(yīng)的區(qū)域是否與以目標(biāo)點(diǎn)為球
心、以目標(biāo)點(diǎn)與“當(dāng)前最近點(diǎn)”間的距離為半徑的球體相交);如果相交,可能在另一
個(gè)子結(jié)點(diǎn)對(duì)應(yīng)的區(qū)域內(nèi)存在距目標(biāo)點(diǎn)更近的點(diǎn),移動(dòng)到另一個(gè)子結(jié)點(diǎn),接著遞歸進(jìn)行最
近鄰搜索;如果不相交,向上回退*/
//當(dāng)前最近鄰與目標(biāo)點(diǎn)的距離
double currentDistance = measureDistance(goal, currentNearest, 0);
//如果當(dāng)前子kd樹的根結(jié)點(diǎn)是其父結(jié)點(diǎn)的左孩子,則搜索其父結(jié)點(diǎn)的右孩子結(jié)點(diǎn)所代表
//的區(qū)域,反之亦反
KDTree* searchDistrict;
if (currentTree->is_left())
{
if (currentTree->parent->right_child == nullptr)
searchDistrict = currentTree;
else
searchDistrict = currentTree->parent->right_child;
}
else
{
searchDistrict = currentTree->parent->left_child;
}
//如果搜索區(qū)域?qū)?yīng)的子kd樹的根結(jié)點(diǎn)不是整個(gè)kd樹的根結(jié)點(diǎn),繼續(xù)回退搜索
while (searchDistrict->parent != nullptr)
{
//搜索區(qū)域與目標(biāo)點(diǎn)的最近距離
double districtDistance = abs(goal[(d+1)%k] - searchDistrict->parent->root[(d+1)%k]);
//如果“搜索區(qū)域與目標(biāo)點(diǎn)的最近距離”比“當(dāng)前最近鄰與目標(biāo)點(diǎn)的距離”短,表明搜索
//區(qū)域內(nèi)可能存在距離目標(biāo)點(diǎn)更近的點(diǎn)
if (districtDistance < currentDistance )//&& !searchDistrict->isEmpty()
{
double parentDistance = measureDistance(goal, searchDistrict->parent->root, 0);
if (parentDistance < currentDistance)
{
currentDistance = parentDistance;
currentTree = searchDistrict->parent;
currentNearest = currentTree->root;
}
if (!searchDistrict->is_empty())
{
double rootDistance = measureDistance(goal, searchDistrict->root, 0);
if (rootDistance < currentDistance)
{
currentDistance = rootDistance;
currentTree = searchDistrict;
currentNearest = currentTree->root;
}
}
if (searchDistrict->left_child != nullptr)
{
double leftDistance = measureDistance(goal, searchDistrict->left_child->root, 0);
if (leftDistance < currentDistance)
{
currentDistance = leftDistance;
currentTree = searchDistrict;
currentNearest = currentTree->root;
}
}
if (searchDistrict->right_child != nullptr)
{
double rightDistance = measureDistance(goal, searchDistrict->right_child->root, 0);
if (rightDistance < currentDistance)
{
currentDistance = rightDistance;
currentTree = searchDistrict;
currentNearest = currentTree->root;
}
}
}//end if
if (searchDistrict->parent->parent != nullptr)
{
searchDistrict = searchDistrict->parent->is_left()?
searchDistrict->parent->parent->right_child:
searchDistrict->parent->parent->left_child;
}
else
{
searchDistrict = searchDistrict->parent;
}
++d;
}//end while
return currentNearest;
}
完整代碼下載地址:KDTreeC++實(shí)現(xiàn)
參考:
https://blog.csdn.net/silangquan/article/details/41483689
https://leileiluoluo.com/posts/kdtree-algorithm-and-implementation.html
到此這篇關(guān)于C++實(shí)現(xiàn)KDTree的文章就介紹到這了,更多相關(guān)C++實(shí)現(xiàn)KDTree內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++關(guān)于類結(jié)構(gòu)體大小和構(gòu)造順序,析構(gòu)順序的測試詳解
這篇文章主要介紹了C++類結(jié)構(gòu)體大小和構(gòu)造順序,析構(gòu)順序的測試,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
C++ 設(shè)置和獲取當(dāng)前工作路徑的實(shí)現(xiàn)代碼
這篇文章主要介紹了C++ 設(shè)置和獲取當(dāng)前工作路徑的實(shí)現(xiàn)代碼,防止DLL加載不到配置和文件,需要的朋友可以參考下2017-09-09
C++實(shí)現(xiàn)基于時(shí)序公平的讀寫鎖詳解
讀寫鎖與普通的互斥鎖的區(qū)別在于有兩種上鎖方式:讀鎖和寫鎖,不用的用戶對(duì)同一個(gè)讀寫鎖獲取讀鎖是非互斥的,其他情況則是互斥的,本文小編將給大家詳細(xì)介紹C++實(shí)現(xiàn)基于時(shí)序公平的讀寫鎖,需要的朋友可以參考下2023-10-10
C++實(shí)現(xiàn)LeetCode(17.電話號(hào)碼的字母組合)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(17.電話號(hào)碼的字母組合),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C++與QML進(jìn)行數(shù)據(jù)交互的常見方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了C++與QML進(jìn)行數(shù)據(jù)交互的常見方法,文中 的示例代碼講解詳細(xì),具有一定的參考價(jià)值,有需要的小伙伴可以跟隨小編一起了解一下2023-10-10
錯(cuò)誤:sem_union的存儲(chǔ)大小未知問題的解決方法
這篇文章主要介紹了錯(cuò)誤:sem_union的存儲(chǔ)大小未知問題的解決方法,需要的朋友可以參考下2016-10-10

