四叉树是一种空间索引(划分)技术,通过递归地将一整块区域均匀划分为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,可见查找效率还是非常高的,数据量只有几千时能非常快地找到邻点。