1. 程式人生 > >kd-樹筆記

kd-樹筆記

以下內容均為本人近幾天學習筆記,個人理解,並非完美答案,請抱著懷疑眼光閱讀,如有錯誤請告知,感謝!

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