js-关于四叉树的学习与理解

最近在写2维空间的碰撞检测功能,遇到了检测性能消耗过大的问题,随后研究解决方案,学习四叉树原理并实践解决问题。

关于四叉树

也被称为四向划分树,是一种常见的空间数据结构,用于划分二维空间。它将空间递归地划分为四个象限,每个象限又可以继续划分为四个象限,以此类推,直到达到某个停止条件。

四叉树特点:

  • 树的根节点表示整个空间,在根节点下划分为四个象限(通常是左上、右上、左下和右下)。
  • 每个节点可以是四个子节点之一,或者是叶节点。
  • 叶节点存储实际的数据(如点、线、面等)。
  • 父节点和子节点之间的关系是通过位置和大小来确定的。
  • 四叉树的应用非常广泛,特别是在计算机图形学、地理信息系统(GIS)和碰撞检测等领域。它可以有效地处理问题,如空间查询、区域划分、近邻搜索等。

四叉树优点:

  • 空间效率高:四叉树可以动态地适应数据的分布,只划分需要划分的区域,避免了不必要的空间占用。
  • 查询效率高:四叉树可以通过减少搜索区域来加速查询操作,特别是对于大规模的数据集。
  • 简单易懂:四叉树的基本原理和操作相对简单,易于理解和实现。

四叉树缺点:

  • 数据分布不均匀时可能导致树的深度增加,增加查询的时间复杂度。
  • 插入和删除操作可能导致树的不平衡,需要进行平衡操作来维护树的性能。
  • 对于高维数据,四叉树的效率可能会降低,因为树的深度会增加。

个人理解

  • 四叉树主要是对于2d的容器 进行查找一个元素,快速定位当前的位置以及附近的元素
  • 大大的减少了2维空间内 需要查询2个物体是否碰撞的循环条件
  • 通过进行 四块 四块的分割 快速查询 当前物体 是在那个 小四块区域内 与相邻的 元素
  • 没有在其他的区块中 就不进行 查询
  • 为什么叫四叉树呢 和二叉树一样 为了高效快速的查找到目标
  • 可以 一对多 也可以 多对多

源码:

(function() {
    function Quadtree(bounds, max_objects = 10, max_levels = 4, level = 0) {
        this.max_objects = max_objects;
        this.max_levels = max_levels;
        this.level = level;
        this.bounds = bounds;
        this.objects = [];
        this.nodes = [];
    }

    Quadtree.prototype.split = function() {
        const nextLevel = this.level + 1;
        const { x, y, width, height } = this.bounds;
        const subWidth = width / 2;
        const subHeight = height / 2;

        this.nodes[0] = new Quadtree({ x: x + subWidth, y, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
        this.nodes[1] = new Quadtree({ x, y, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
        this.nodes[2] = new Quadtree({ x, y: y + subHeight, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
        this.nodes[3] = new Quadtree({ x: x + subWidth, y: y + subHeight, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
    };

    Quadtree.prototype.getIndex = function(pRect) {
        const indexes = [];
        const verticalMidpoint = this.bounds.x + this.bounds.width / 2;
        const horizontalMidpoint = this.bounds.y + this.bounds.height / 2;

        const startIsNorth = pRect.y < horizontalMidpoint;
        const startIsWest = pRect.x < verticalMidpoint;
        const endIsEast = pRect.x + pRect.width > verticalMidpoint;
        const endIsSouth = pRect.y + pRect.height > horizontalMidpoint;

        if (startIsNorth) {
            if (endIsEast) indexes.push(0);
            if (startIsWest) indexes.push(1);
        } else if (endIsSouth) {
            if (startIsWest) indexes.push(2);
            if (endIsEast) indexes.push(3);
        }

        return indexes;
    };

    Quadtree.prototype.insert = function(pRect) {
        if (this.nodes.length) {
            const indexes = this.getIndex(pRect);
            for (let i = 0; i < indexes.length; i++) {
                this.nodes[indexes[i]].insert(pRect);
            }
            return;
        }

        this.objects.push(pRect);
        if (this.objects.length > this.max_objects && this.level < this.max_levels) {
            if (!this.nodes.length) this.split();
            for (let i = 0; i < this.objects.length; i++) {
                const indexes = this.getIndex(this.objects[i]);
                for (let k = 0; k < indexes.length; k++) {
                    this.nodes[indexes[k]].insert(this.objects[i]);
                }
            }
            this.objects.length = 0; // 清空数组但保持引用
        }
    };

    Quadtree.prototype.retrieve = function(pRect) {
        const indexes = this.getIndex(pRect);
        const returnObjects = []; // 新建数组用于存储结果
        const seenObjects = new Set(); // 使用 Set 去重

        // 如果有子节点,检索其对象
        if (this.nodes.length) {
            for (let i = 0; i < indexes.length; i++) {
                const childObjects = this.nodes[indexes[i]].retrieve(pRect);
                for (let j = 0; j < childObjects.length; j++) {
                    if (!seenObjects.has(childObjects[j])) {  // 检查是否已存在
                        returnObjects.push(childObjects[j]);
                        seenObjects.add(childObjects[j]); // 添加到 Set
                    }
                }
            }
        }

        // 添加当前节点的对象,去重
        for (let i = 0; i < this.objects.length; i++) {
            if (!seenObjects.has(this.objects[i])) {
                returnObjects.push(this.objects[i]);
            }
        }

        return returnObjects;
    };

    Quadtree.prototype.clear = function() {
        this.objects.length = 0; // 清空数组但保持引用
        for (let i = 0; i < this.nodes.length; i++) {
            this.nodes[i].clear();
        }
        this.nodes.length = 0; // 清空节点
    };

    if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
        module.exports = Quadtree;
    } else {
        window.Quadtree = Quadtree;
    }
})();

相关属性解释:

  • max_objects: 每个节点可以包含的最大对象数。当节点中的对象数超过此值时,节点将分裂。
  • max_levels: 树的最大深度。当达到此深度时,即使节点中的对象数超过 max_objects,也不会再分裂。
  • level: 当前节点的深度(从0开始)。
  • bounds: 当前节点所代表的矩形区域(包括x, y坐标、宽度和高度)。
  • objects: 存储在当前节点中的对象数组。
  • nodes: 存储当前节点的四个子节点的数组(如果节点已分裂)
  • split(): 将当前节点分裂成四个子节点,每个子节点代表当前节点区域的一个四分之一。
  • getIndex(pRect): 根据给定的矩形(pRect)的位置和大小,确定它应该被插入到哪个子节点中(如果有的话)。由于一个矩形可能跨越多个子区域,因此此方法返回一个索引数组,而不是单个索引。
  • insert(pRect): 将给定的矩形(pRect)插入到四叉树中。如果节点已分裂,则根据 getIndex 方法的结果将矩形插入到相应的子节点中。如果节点中的对象数超过了 max_objects 并且未达到 max_levels,则节点将分裂并重新分配对象。
  • retrieve(pRect): 检索与给定矩形(pRect)相交的所有对象。这包括当前节点中的对象以及所有相关子节点中的对象。使用 Set 来确保结果中不包含重复的对象。
  • clear(): 清空四叉树中的所有对象和子节点。

使用方法

先定义一个四叉树实例
var myTree = new Quadtree({
                x: 0,
                y: 0,
                width: 1920,
                height: 1080
            }, 4);

如果插入元素 也需要包括x、y、width、height属性
myTree.insert({
 	x: 200,
 	y: 300,
 	width: 100,
 	height: 300,
	})
查询 你想要检测元素,是否和其他元素碰撞 
const ele={
	x: 100,
 	y: 300,
 	width: 300,
 	height: 400,	
	}
var candidates = myTree.retrieve(); 
返回一个ele附近的的元素的集合

之后在candidates数组中元素 进行判断 是否相交 
大大的减少了 需要判断的计算量 数据越多能体现

在线demo
检测碰撞
检测相互碰撞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值