簡介

  k-d樹(k-dimensional),是一種分割k維資料空間的資料結構(對資料點在k維空間中劃分的一種資料結構),主要應用於多維空間關鍵資料的搜尋(如:範圍搜尋和最近鄰搜尋)。

舉例



  上圖就是一顆kdtree,可以看出kdtree是二叉搜尋樹的變種。

  kdtree的性質:

  • kdtree具有平衡的特質,兩樹葉的高度差不超過1。(樹越平衡代表著分割得越平均,搜尋的時間越少)
  • 資料只存放在葉子結點,而根結點和中間結點存放一些空間劃分資訊(例如劃分維度、劃分值)。
  • 將每一個元組按0排序(第一項序號為0,第二項序號為1,第三項序號為2),在樹的第n層,第 n%3 項被用粗體顯示,而這些被粗體顯示的樹就是作為二叉搜尋樹的key值,比如,根節點的左子樹中的每一個節點的第一個項均小於根節點的的第一項,右子樹的節點中第一項均大於根節點的第一項,子樹依次類推。

分割的作用

一維

  對於一個標準的BSTree,每個節點只有一個key值。

  將key值對應到一維的座標軸上。

  根節點對應的就是2,左子樹都在2的左邊,右子樹都在2的右邊,整個一維空間就被根節點分割成了兩個部分,當要查詢結點0的時候,由於是在2的左邊,所以可以放心的只搜尋左子樹的部分。整個搜尋的過程可以看成不斷分割搜尋區間的過程,直到找到目標節點。

二維

這樣的分割可以擴充套件到二維甚至更多維的情況。

但是問題來了,二維的節點怎麼比較大小?

在BSTree中,節點分割的是一維數軸,那麼在二維中,就應當是分割平面了,就像這樣:



黃色的點作為根節點,上面的點歸左子樹,下面的點歸右子樹,接下來再不斷地劃分,最後得到一棵樹就是赫赫有名的BSPTree(binary space partitioning tree). 分割的那條線叫做分割超平面(splitting hyperplane),在一維中是一個點,二維中是線,三維的是面。

n維

KDTree就是超平面都垂直於軸的BSPTree。同樣的資料集,用KDTree劃分之後就是這樣:

黃色節點就是Root節點,下一層是紅色,再下一層是綠色,再下一層是藍色。為了更好的理解KDTree的分割,我們在圖形中來看一下搜尋的過程,假設現在需要搜尋右下角的一個點,首先要做的就是比較這個點的x座標和root點的x座標值,由於x座標值大於root節點的x座標,所以只需要在右邊搜尋,接下來,要比較該節點和右邊紅色節點y值得大小...後面依此類推。整個過程如下圖:

1.



2.



3.

關於kdtree的重要問題

一.樹的建立

1.節點的資料結構

定義:

Node-data - 資料向量, 資料集中某個資料點,是n維向量(這裡也就是k維)

Range - 空間向量, 該節點所代表的空間範圍

split - 整數, 垂直於分割超平面的方向軸序號

Left - k-d樹, 由位於該節點分割超平面左子空間內所有資料點所構成的k-d樹

Right - k-d樹, 由位於該節點分割超平面右子空間內所有資料點所構成的k-d樹

parent - k-d樹, 父節點

2. 優化

1.切分維度優化

構建開始前,對比資料點在各維度的分佈情況,資料點在某一維度座標值的方差越大分佈越分散,方差越小分佈越集中。從方差大的維度開始切分可以取得很好的切分效果及平衡性。

2.中值選擇優化

第一種,演算法開始前,對原始資料點在所有維度進行一次排序,儲存下來,然後在後續的中值選擇中,無須每次都對其子集進行排序,提升了效能。

第二種,從原始資料點中隨機選擇固定數目的點,然後對其進行排序,每次從這些樣本點中取中值,來作為分割超平面。該方式在實踐中被證明可以取得很好效能及很好的平衡性。

2.最近鄰域搜尋(Nearest-Neighbor Lookup)

給定一個KDTree和一個節點,求KDTree中離這個節點最近的節點.(這個節點就是最臨近點)

這裡距離的求法用的是歐式距離。



基本的思路很簡單:首先通過二叉樹搜尋(比較待查詢節點和分裂節點的分裂維的值,小於等於就進入左子樹分支,等於就進入右子樹分支直到葉子結點),順著“搜尋路徑”很快能找到最近鄰的近似點,也就是與待查詢點處於同一個子空間的葉子結點;然後再回溯搜尋路徑,並判斷搜尋路徑上的結點的其他子結點空間中是否可能有距離查詢點更近的資料點,如果有可能,則需要跳到其他子結點空間中去搜索(將其他子結點加入到搜尋路徑)。重複這個過程直到搜尋路徑為空。

這裡還有幾個細節需要注意一下,如下圖,假設標記為星星的點是 test point, 綠色的點是找到的近似點,在回溯過程中,需要用到一個佇列,儲存需要回溯的點,在判斷其他子節點空間中是否有可能有距離查詢點更近的資料點時,做法是以查詢點為圓心,以當前的最近距離為半徑畫圓,這個圓稱為候選超球(candidate hypersphere),如果圓與回溯點的軸相交,則需要將軸另一邊的節點都放到回溯佇列裡面來。



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

關鍵程式碼

構建KDTree

void KDTree::buildKdTree(KDTree *tree, vector<vector<double>> data, unsigned int depth)
{
//樣本的數量
unsigned long samplesNum = data.size();
//終止條件
if (samplesNum == 0)
{
return;
}
if (samplesNum == 1)
{
tree->root = data[0];
return;
}
//樣本的維度
unsigned long k = data[0].size();//座標軸個數
vector<vector<double> > transData = Transpose(data);
//選擇切分屬性
unsigned splitAttribute = depth % k;
vector<double> splitAttributeValues = transData[splitAttribute];
//選擇切分值
double splitValue = findMiddleValue(splitAttributeValues);
//cout << "splitValue" << splitValue << endl; // 根據選定的切分屬性和切分值,將資料集分為兩個子集
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]);
}
} //子集遞迴呼叫buildKdTree函式
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);
}

查詢目標點的最近鄰點

vector<double> KDTree::searchNearestNeighbor(vector<double> goal, KDTree *tree)
{
/*第一步:在kd樹中找出包含目標點的葉子結點:從根結點出發,
遞迴的向下訪問kd樹,若目標點的當前維的座標小於切分點的
座標,則移動到左子結點,否則移動到右子結點,直到子結點為
葉結點為止,以此葉子結點為“當前最近點”
*/
unsigned long k = tree->root.size();//計算出資料的維數
unsigned d = 0;//維度初始化為0,即從第1維開始
KDTree* currentTree = tree;
vector<double> currentNearest = currentTree->root;
while(!currentTree->is_leaf())
{
unsigned index = d % k;//計算當前維
if (currentTree->right_child->is_empty() || goal[index] < currentNearest[index])
{
currentTree = currentTree->left_child;
}
else
{
currentTree = currentTree->right_child;
}
++d;
}
currentNearest = currentTree->root; /*第二步:遞迴地向上回退, 在每個結點進行如下操作:
(a)如果該結點儲存的例項比當前最近點距離目標點更近,則以該例點為“當前最近點”
(b)當前最近點一定存在於某結點一個子結點對應的區域,檢查該子結點的父結點的另
一子結點對應區域是否有更近的點(即檢查另一子結點對應的區域是否與以目標點為球
心、以目標點與“當前最近點”間的距離為半徑的球體相交);如果相交,可能在另一
個子結點對應的區域記憶體在距目標點更近的點,移動到另一個子結點,接著遞迴進行最
近鄰搜尋;如果不相交,向上回退*/ //當前最近鄰與目標點的距離
double currentDistance = measureDistance(goal, currentNearest, 0); //如果當前子kd樹的根結點是其父結點的左孩子,則搜尋其父結點的右孩子結點所代表
//的區域,反之亦反
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;
} //如果搜尋區域對應的子kd樹的根結點不是整個kd樹的根結點,繼續回退搜尋
while (searchDistrict->parent != nullptr)
{
//搜尋區域與目標點的最近距離
double districtDistance = abs(goal[(d+1)%k] - searchDistrict->parent->root[(d+1)%k]); //如果“搜尋區域與目標點的最近距離”比“當前最近鄰與目標點的距離”短,表明搜尋
//區域內可能存在距離目標點更近的點
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++實現

參考:

https://blog.csdn.net/silangquan/article/details/41483689

https://leileiluoluo.com/posts/kdtree-algorithm-and-implementation.html