C++实现四叉树

四叉树是一种空间索引(划分)技术,通过递归地将一整块区域均匀划分为4个子区域,每个区域托管一定数量的二维点,于是任意一个二维点都可以根据它的坐标快速找到所述的子区域,得到它的邻点。

具体介绍略过,只要会实现二叉树的二分查找,就不难体会四叉树划分的思想。因为查找邻点的速度非常快,所以常用于游戏中的碰撞检测,GIS的地理位置索引等。

下面是我实现的一种传统四叉树。

节点定义:

#include 
#include 
#include 
#include 

//二维数据点定义
typedef struct Point_2D {
    int x;
    int y;
}Point;

//节点定义
typedef struct Quad_Tree_Node {
    Point start_xy;               //左上角起始点
    int width;                    //宽度
    int height;                   //高度
    int capacity;                 //节点容量
    bool is_leaf;                 //叶节点标记
    std::vector points;    //存储的二维点
    Quad_Tree_Node* lu_child;     //子节点,左上
    Quad_Tree_Node* ru_child;     //子节点,右上
    Quad_Tree_Node* lb_child;     //子节点,左下
    Quad_Tree_Node* rb_child;     //子节点,右下
    int depth;                    //当前节点深度
}Quad_Node;

二维数据点结构只有横纵坐标,也可以携带别的信息。四叉树节点中需要保存所属子区域的信息,我使用左上角起始点+宽度+高度来定义一块区域。设置了节点容量来限制一块区域能保存的数据量,如果超过容量,节点就要继续分裂。

创建节点方法:

Quad_Node* Quad_Tree::create_node_(const Point& start, const int& w, const int& h) {
    Quad_Node* new_node = new Quad_Node;
    new_node->start_xy = start;
    new_node->width = w;
    new_node->height = h;
    new_node->capacity = _set_capacity;
    new_node->is_leaf = true;
    new_node->lu_child = nullptr;
    new_node->lb_child = nullptr;
    new_node->ru_child = nullptr;
    new_node->rb_child = nullptr;
    return new_node;
}

创建节点方法需要左上起始点、宽度和高度三个入口参数。

分裂节点方法:

int Quad_Tree::split_node_(Quad_Node* node) {
    node->is_leaf = false;

    int sub_w = node->width / 2;
    int sub_h = node->height / 2;

    //确定每个子节点的起始位置
    Point lu_start = node->start_xy;
    Point lb_start;
    lb_start.x = node->start_xy.x;
    lb_start.y = node->start_xy.y + sub_h;
    Point ru_start;
    ru_start.x = node->start_xy.x + sub_w;
    ru_start.y = node->start_xy.y;
    Point rb_start;
    rb_start.x = node->start_xy.x + sub_w;
    rb_start.y = node->start_xy.y + sub_h;

    //创建子节点
    node->lu_child = create_node_(lu_start, sub_w, sub_h);
    node->lb_child = create_node_(lb_start, sub_w, sub_h);
    node->ru_child = create_node_(ru_start, sub_w, sub_h);
    node->rb_child = create_node_(rb_start, sub_w, sub_h);

    //将当前节点保存的数据点插入到下一层
    for (int i = 0, count = node->points.size(); i < count; ++i) {
        insert_node_(node, node->points[i]);
    }
    node->points.clear();

    return 0;
}

分裂节点注意需要把当前节点保存的所有数据点插入到下一层,即重新划分到子节点的区域中。因此,不难看出只有叶节点上会保存数据点。

插入节点方法:

int Quad_Tree::insert_node_(Quad_Node* node, const Point& point)
{
    if (node->is_leaf) {    //数据点都插入到叶节点
        int count = node->points.size();
        if (count + 1 > node->capacity) {    //超过容量就要分裂节点,再插入
            split_node_(node);
            insert_node_(node, point);
        }
        else {
            node->points.push_back(point);    //没超过就正常插入
        }

        return 0;
    }

    //当前节点不是叶节点,找到它所属的子区域
    Region in_region = find_region(node, point);

    if (in_region == Region::LEFT_UP) {
        insert_node_(node->lu_child, point);    //插入到左上子节点
    }
    else if (in_region == Region::LEFT_BOTTOM) {
        insert_node_(node->lb_child, point);    //插入到左下子节点
    }
    else if (in_region == Region::RIGHT_UP) {
        insert_node_(node->ru_child, point);    //插入到右上子节点
    }
    else if (in_region == Region::RIGHT_BOTTOM) {
        insert_node_(node->rb_child, point);    //插入到右下子节点
    }
    else {
        return -1;    //不属于任何子区域
    }

    return 0;
}

插入节点时判断当前节点是不是叶节点,不是叶节点的话需要找到确定所属子区域,插入到对应的子节点中。确定子区域的过程我单独实现为一个函数:

Region Quad_Tree::find_region(Quad_Node* node, const Point& point)
{
    Region found = Region::OUT_OF_RANGE;
    int x = point.x;
    int y = point.y;
    int sub_w = node->width / 2;
    int sub_h = node->height / 2;
    if (x > node->start_xy.x && x < node->start_xy.x + sub_w) {
        if (y > node->start_xy.y && y < node->start_xy.y + sub_h) {
            found = Region::LEFT_UP;
        }
        else if (y > node->start_xy.y + sub_h && y < node->start_xy.y + node->height) {
            found = Region::LEFT_BOTTOM;
        }
        else {
        }
    }
    else if (x > node->start_xy.x + sub_w && x < node->start_xy.x + node->width) {
        if (y > node->start_xy.y && y < node->start_xy.y + sub_h) {
            found = Region::RIGHT_UP;
        }
        else if (y > node->start_xy.y + sub_h && y < node->start_xy.y + node->height) {
            found = Region::RIGHT_BOTTOM;
        }
        else {
        }
    }
    else {
        found = Region::OUT_OF_RANGE;
    }

    return found;
}

根据上面的实现,我对正好位于边界线的数据点是没有处理的。可以把它们保存到父节点上。

区域的定义用了枚举类:

enum class Region {
    LEFT_UP = 0,
    LEFT_BOTTOM,
    RIGHT_UP,
    RIGHT_BOTTOM,
    OUT_OF_RANGE
};

实现一个遍历删除方法:

void Quad_Tree::traverse(Quad_Node* node) {
    if (node->is_leaf) {
        //访问操作
        node->points.clear();
        delete node;
        return;
    }

    traverse(node->lu_child);
    traverse(node->lb_child);
    traverse(node->ru_child);
    traverse(node->rb_child);

    node->points.clear();
    delete node;
}

//遍历输出
void Quad_Tree::show() {
    traverse(root_);
}

分裂到指定深度的方法:

int max_depth = 8;

void Quad_Tree::init_split()
{
    split_to_depth(root_, 0);
}

int Quad_Tree::split_to_depth(Quad_Node* node, const int& depth)
{
    if (depth == max_depth) {
        node->is_leaf = true;
        return 1;
    }
    else {
        node->is_leaf = false;
        node->depth = depth;
        int sub_w = node->width / 2;
        int sub_h = node->height / 2;

        Point lu_start = node->start_xy;
        Point lb_start;
        lb_start.x = node->start_xy.x;
        lb_start.y = node->start_xy.y + sub_h;
        Point ru_start;
        ru_start.x = node->start_xy.x + sub_w;
        ru_start.y = node->start_xy.y;
        Point rb_start;
        rb_start.x = node->start_xy.x + sub_w;
        rb_start.y = node->start_xy.y + sub_h;

        node->lu_child = create_node_(lu_start, sub_w, sub_h);
        node->lb_child = create_node_(lb_start, sub_w, sub_h);
        node->ru_child = create_node_(ru_start, sub_w, sub_h);
        node->rb_child = create_node_(rb_start, sub_w, sub_h);
    }

    split_to_depth(node->lu_child, depth + 1);
    split_to_depth(node->lb_child, depth + 1);
    split_to_depth(node->ru_child, depth + 1);
    split_to_depth(node->rb_child, depth + 1);
}

Quad_Tree类定义:

class Quad_Tree {
private:
    Quad_Node* root_;    //根节点

    Quad_Node* create_node_(const Point& start, const int& w, const int& h);    
    int split_node_(Quad_Node* node);
    int insert_node_(Quad_Node* node, const Point& point);
    Region find_region(Quad_Node* node, const Point& point);
    void traverse(Quad_Node* node);
    int split_to_depth(Quad_Node* node, const int& depth);
    
public:
    Quad_Tree(const int& start_x, const int& start_y, const int& witdh, const int& height);
    ~Quad_Tree();
    void insert_point_(const Point& point);
    void show();
    void init_split();
};

main函数,测试一下划分10000个数据点的时间。

#include "quadTree.h"

void test_time() {

    //设置时间种子
    srand(time(0));
    clock_t startTime, endTime;
    double in_sec = 0, tra_sec = 0;

    Quad_Tree quad_tree(0, 0, 1920, 1536);    //左上起始位置(0,0),宽1920,高1536

    const int rand_size = 10000;
    Point* rand_points = new Point[rand_size];

    //数据点插入
    startTime = clock();
    for (int i = 0; i < rand_size; ++i) {    //划分随机数据点
        rand_points[i].x = rand() % 1920;
        rand_points[i].y = rand() % 1536;
        quad_tree.insert_point_(rand_points[i]);
    }
    endTime = clock();
    in_sec = (double)(endTime - startTime);

    //数据点遍历
    startTime = clock();
    quad_tree.show();
    endTime = clock();
    tra_sec = (double)(endTime - startTime);

    std::cout << "\n";
    std::cout << "insert " << rand_size << " points takes " << in_sec << "ms.\n";
    std::cout << "traverse " << rand_size << " points takes " << tra_sec << "ms.\n";
}

int main() {
    //Quad_Tree quad_tree(0, 0, 1920, 1536);
    //quad_tree.init_split();

    test_time();
}

最后得到1万个数据点建树约9~12ms,遍历约2~3ms。5000个数据点建树约5~6ms,遍历1~2ms,可见查找效率还是非常高的,数据量只有几千时能非常快地找到邻点。

你可能感兴趣的:(c++数据结构和算法)