kd-樹筆記
阿新 • • 發佈:2018-11-29
以下內容均為本人近幾天學習筆記,個人理解,並非完美答案,請抱著懷疑眼光閱讀,如有錯誤請告知,感謝!
1.kd-樹簡介
1.1 特徵:在任何情況下,kd-樹都是一棵遞迴定義的平衡二叉搜尋樹
1.2 用途:常用於範圍查詢,高效解決多維範圍查詢。例如:快速在校友資料庫中找到1970-2000年畢業並且身高在170-190cm且性別為男的校友。
2.kd-樹的實現
2.1 一維kd-樹:一維kd-樹本質上就是平衡二叉搜尋樹,也可以看成線段樹,一維的範圍查詢問題完全可以用線段樹解決。這樣便於推廣到二維乃至k維。
2.2 kd-樹的建樹:
2.2.1 構造演算法:
- kd-樹的建樹應該將每個維度分為兩個部分
- 增加一個屬性深度(deep),那麼kd-樹維度維k,當深度為deep時,該對deep%k維進行劃分。
- k維kd-樹本質上仍是平衡二叉搜尋樹,只是在每一層對不同維度進行劃分,使左右節點數量相等,從而維持樹高。
2.2.2 虛擬碼:(指標更方便,但更容易出錯)
void BuildTree( int l , int r , int root , int deep){ //l是該維度的資料的左邊界,r是右邊界
if(l > r) return;//不存在資料
isExist[root] = 1;//標記root存在資料
isExist[ls] = isExist[rs] = -1;//(左右兒子初始化為不存在)
int idx = deep%k;//找出劃分哪個維度
找中位點mid,同時使mid左面所有節點小於mid,右邊所有節點大於mid;
BuildTree(l , mid-1 , ls , deep+1);
BuildTree(mid+1 , r , rs , deep+1);
}
2.3 kd-樹的查詢:
2.3.1 當前節點範圍查詢的三種情況:
- A:該範圍完全包含於該節點的左子樹或右子樹
- B:該範圍一部分在左子樹,一部分在右子樹
- C:該範圍既不在左子樹也不在右子樹
2.4 程式碼例項:HDU4347
#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <math.h>
#include <queue>
using namespace std;
#define N 50005
#define lson rt << 1
#define rson rt << 1 | 1
#define Pair pair<double, Node>
#define Sqrt2(x) (x) * (x)
int n, k, idx;
struct Node
{
int feature[5]; //定義屬性陣列
bool operator < (const Node &u) const
{
return feature[idx] < u.feature[idx];
}
}_data[N]; //_data[]陣列代表輸入的資料
priority_queue<Pair> Q; //佇列Q用於存放離p最近的m個數據
class KDTree{
public:
void Build(int, int, int, int); //建樹
void Query(Node, int, int, int); //查詢
private:
Node data[4 * N]; //data[]陣列代表K-D樹的所有節點資料
int flag[4 * N]; //用於標記某個節點是否存在,1表示存在,-1表示不存在
}kd;
//建樹步驟,引數dept代表樹的深度
void KDTree::Build(int l, int r, int rt, int dept)
{
if(l > r) return;
flag[rt] = 1; //表示編號為rt的節點存在
flag[lson] = flag[rson] = -1; //當前節點的孩子暫時標記不存在
idx = dept % k; //按照編號為idx的屬性進行劃分
int mid = (l + r) >> 1;
nth_element(_data + l, _data + mid, _data + r + 1); //nth_element()為STL中的函式
data[rt] = _data[mid];
Build(l, mid - 1, lson, dept + 1); //遞迴左子樹
Build(mid + 1, r, rson, dept + 1); //遞迴右子樹
}
//查詢函式,尋找離p最近的m個特徵屬性
void KDTree::Query(Node p, int m, int rt, int dept)
{
if(flag[rt] == -1) return; //不存在的節點不遍歷
Pair cur(0, data[rt]); //獲取當前節點的資料和到p的距離
for(int i = 0; i < k; i++)
cur.first += Sqrt2(cur.second.feature[i] - p.feature[i]);
int dim = dept % k; //跟建樹一樣,這樣能保證相同節點的dim值不變
bool fg = 0; //用於標記是否需要遍歷右子樹
int x = lson;
int y = rson;
if(p.feature[dim] >= data[rt].feature[dim]) //資料p的第dim個特徵值大於等於當前的資料,則需要進入右子樹
swap(x, y);
if(~flag[x]) Query(p, m, x, dept + 1); //如果節點x存在,則進入子樹繼續遍歷
//以下是回溯過程,維護一個優先佇列
if(Q.size() < m) //如果佇列沒有滿,則繼續放入
{
Q.push(cur);
fg = 1;
}
else
{
if(cur.first < Q.top().first) //如果找到更小的距離,則用於替換佇列Q中最大的距離的資料
{
Q.pop();
Q.push(cur);
}
if(Sqrt2(p.feature[dim] - data[rt].feature[dim]) < Q.top().first)
{
fg = 1;
}
}
if(~flag[y] && fg)
Query(p, m, y, dept + 1);
}
//輸出結果
void Print(Node data)
{
for(int i = 0; i < k; i++)
printf("%d%c", data.feature[i], i == k - 1 ? '\n' : ' ');
}
int main()
{
while(scanf("%d%d", &n, &k)!=EOF)
{
for(int i = 0; i < n; i++)
for(int j = 0; j < k; j++)
scanf("%d", &_data[i].feature[j]);
kd.Build(0, n - 1, 1, 0);
int t, m;
scanf("%d", &t);
while(t--)
{
Node p;
for(int i = 0; i < k; i++)
scanf("%d", &p.feature[i]);
scanf("%d", &m);
while(!Q.empty()) Q.pop(); //事先需要清空優先佇列
kd.Query(p, m, 1, 0);
printf("the closest %d points are:\n", m);
Node tmp[25];
for(int i = 0; !Q.empty(); i++)
{
tmp[i] = Q.top().second;
Q.pop();
}
for(int i = m - 1; i >= 0; i--)
Print(tmp[i]);
}
}
return 0;
}
《資料結構(C++語言版)》——鄧俊輝 P242