范围树和线段树是两种数据结构,用于高效地处理和查询数据。
范围树(Range Tree)是一种二叉树,它通过递归地将每个节点分割成两个子节点来存储一个点集。每个节点表示一个范围,并且存储该范围内所有点的最小和最大值。范围树主要用于查询和更新操作,例如找到落在某个范围内的所有点。
线段树(Segment Tree)也是一种二叉树,但它主要用于存储有序的序列。每个节点表示一个线段,存储该线段上的最小和最大值。线段树主要用于查询和更新操作,例如找到某个范围内的最小值或最大值。
这两种数据结构都是用于处理有序数据的强大工具,但在使用时需要小心地处理节点之间的关系和数据更新。
1、定义
本节介绍d维范围和分段树。一维范围树是一维点数据上的二叉搜索树。在这里,我们称所有具有严格顺序的一维数据类型(如整数和双精度数)为点数据。d维点数据是一维点数据的d元组。
一维分段树也是二叉搜索树,但输入数据是一维区间数据。一维区间数据是一对(即二元组)(a,b),其中a和b是相同类型的一维点数据,并且a<b。对(a,b)表示半开区间[a,b)。类似地,d维区间由一维区间的d元组表示。
d维树的输入数据类型是一个容器类,由d维点数据类型、区间数据类型或两者的混合组成,还可以选择一个值类型,可用于存储任意数据。例如,d维多边形的d维边界框可以定义d维段树的区间数据,多边形本身可以作为其值存储。输入数据项是输入数据类型的实例。
范围树和分段树类是完全通用的,因为它们可以用来定义多层树。维度(层数)为d的多层树是第d层的简单树,而树的第k层(1<=k<=d-1)定义了一个树,其中每个(内部)顶点包含一个维度为d-k+1的多层树。嵌套在k维树(T)中的k-1维树称为子层树(T)。例如,d维树可以是第一层的范围树,根据d维数据项的第一维构建。在每个子树中的所有数据项上,根据数据项的第二维,构建一个(d-1)维树,无论是范围树还是分段树。等等。
区间树和分段树中的数字以图形方式说明了子层树的方法。
创建树后,不允许进一步插入或删除数据项。树类既不依赖于数据类型,也不依赖于数据项的具体物理表示。例如,让多层树成为分段树,每个顶点定义一个范围树。我们可以选择由双精度类型区间和整数类型点数据组成的数据项。作为值类型,我们可以选择字符串。
为了实现这种通用性,我们必须定义每个维度的树是什么样子的,以及输入数据是如何组织的。对于维度 k,1 < k < 4,CGAL 提供了现成的范围树和分段树,可以存储 k 维的键(区间)。 示例说明了这些类的使用,如类范围树在类数据上的示例和类分段树在类数据上的示例。这些类的功能描述以及更高维度的树和混合多层树的定义在参考手册中给出。
在接下来的两节中,我们将简要介绍这里实现的区间树和分段树的版本,并给出一些示例。
2、软件设计
为了能够定义多层树,我们首先设计了范围树和分段树,使其具有一个模板参数来定义子层树的类型。有了这个子层树类型信息,子层就可以创建了。这种方法导致了嵌套模板参数,因为子层树可以再次具有一个模板参数来定义子层。因此,内部类和函数的标识符变得比编译器相关的限制还要长。这种情况在d=2时就已经发生了。
因此,我们选择了另一种面向对象的设计。我们定义了一个名为Tree_base的纯虚基类,并从中派生了Range_tree_d和Segment_tree_d类。
这些类的构造函数期望一个名为sublayer_prototype的参数,其类型为Tree_base。由于Range_tree_d和Segment_tree_d类都从Tree_base类派生,因此可以使用Range_tree_d或Segment_tree_d类的实例作为构造函数参数。这个参数定义了树的子层树。
例如,您可以使用一个Segment_tree_d类的实例作为构造函数参数来构造一个Range_tree_d。然后,您就定义了一个具有分段树作为子层树的范围树。由于Range_tree_d和Segment_tree_d类在构造函数中都期望一个子层树,我们不得不从Tree_base类派生出一个名为Tree_anchor的第三个类,它不需要构造函数参数。这个类的实例被用作Range_tree_d或Segment_tree_d类的构造函数参数,以停止递归。
所有类都提供 clone() 函数,该函数返回相同树类型的实例(副本)。在构造树时调用 sublayer_prototype 的 clone() 函数。如果子层树再次具有子层,则它也具有一个 sublayer_prototype,该 sublayer_prototype 也被克隆,依此类推。因此,调用 clone() 函数会生成一个子层树,该子层树具有关于其子层树的完整知识。
树允许对键进行窗口查询、封闭查询和反向范围查询。显然,反向范围查询只在段树中才有意义。为了执行反向范围查询,需要先对 ϵ宽度必须执行。我们不希望为这种查询提供额外功能,因为反向范围查询是范围查询的特例。此外,在段树类中提供反向范围查询意味着在范围树类中也提供此功能,并在特征类中添加一个额外的项目来访问反向范围查询点。
树由三个参数模板化:数据、窗口和特征。类型数据定义了输入数据类型,类型窗口定义了查询窗口类型。树使用一组定义良好的函数来访问数据。这些函数必须由类特征提供。
该设计部分遵循[2]中的原型设计模式。与我们的第一种使用模板的方法相比,我们想指出以下几点:在这种方法中,子层类型是在运行时使用面向对象编程定义的,而在使用模板的方法中,子层类型是在编译时定义的。
在这种面向对象的设计中使用虚成员函数所导致的运行时开销是可以忽略不计的,因为所有虚函数都是非平凡的。
设计理念如下图所示。
范围和分段树数据结构的设计。符号三角形表示下层阶级是从上层阶级派生而来的。
例如,为了定义一个二维多层树,它由第一维的范围树和第二维的分段树组成,我们进行如下操作:我们构造一个类型为tree_ancher的对象,该对象停止递归。然后,我们构造一个类型为Segment_tree_d的对象,该对象得到类型为tree_ancher的对象作为原型参数。之后,我们定义了一个类型为Range_tree_d的对象,该对象是用类型为Segment_tree_d的对象作为原型参数构建的。下面的代码说明了二维多层树的构造。
int main(){Tree_Anchor *anchor=new Tree_Anchor;Segment_Tree_d *segment_tree = new Segment_Tree_d(*anchor);Range_Tree_d *range_segment_tree = new Range_Tree_d(*segment_tree);// let data_items be a list of Data itemsrange_segment_tree->make_tree(data_items.begin(),data_items.end());
}
这里,类Tree_Aanchor、Segment_Tree_d和Range_Tree_d由typedefs定义:
typedef Tree_anchor<Data,Window> Tree_Anchor;
typedef Segment_tree_d<Data,Window,Interval_traits> Segment_Tree_d;
typedef Range_tree_d<Data,Window,Point_traits> Range_Tree_d;
类 Tree_base 和类 Tree_anchor 获得两个模板参数:一个定义了存储在树中的数据类型的类 Data,以及一个定义了查询范围类型的类 Window。派生类 Range_tree_d 和 Segment_tree_d 额外获得一个名为 Tree_traits 的参数,该参数定义了 Data 和树之间的接口。让 Data 类型是一个 d 维元组,在每个维度上可以是点数据或区间数据。然后,类 Tree_traits 提供对该树层的点(或区间)数据的访问器和一个比较函数。让我们回忆一下二维树的例子,它是一个第一维是区间树,第二维是段树的二维树。然后,类 Segment_tree_d 的 Tree_traits 类模板参数定义了 Data 的区间数据的访问器,而类 Range_tree_d 的 Tree_traits 类模板参数定义了 Data 的点数据的访问器。这些类的示例实现如下所示。
struct Data{int min,max; // interval datadouble point; // point data
};
struct Window{int min,max;double min_point, max_point;
};
class Point_traits{
public:typedef double Key;Key get_key(Data& d){return d.point;} // key accessoKey get_left(Window& w){return w.min_point;}Key get_right(Window& w){return w.max_point;}bool comp(Key& key1, Key& key2){return (key1 < key2);}
};
class Interval_traits{
public:typedef int Key;Key get_left(Data& d){return d.min;}Key get_right(Data& d){return d.max;}Key get_left_win(Window& w){return w.min;}Key get_right_win(Window& w){return w.max;}static bool comp(Key& key1, Key& key2){return (key1 < key2);}
};
3、创建任意多层树
现在让我们仔细看看多层树是如何构建的。在创建d维树的情况下,我们处理一系列任意数据项,其中每个项目定义一个d维区间、点或其他对象。树是由该结构的迭代器构造的。在第i层,树是相对于定义第i维的数据槽构建的。因此,我们需要定义哪个数据槽对应哪个维度。此外,我们希望我们的树能够处理任意数据项。这需要在算法和数据项之间使用适配器。这是通过使用特征类解决的,特征类以函数对象的形式实现。这些类提供对数据项的指定数据槽的访问函数。然后通过为每一层定义一个特征类,为每一层分别定义d维树。
4、范围树
一维范围树是一维点数据的二叉搜索树。树的点数据存储在叶子中。每个内部顶点存储其左子树中的最高条目。这里实现的范围树版本是静态的,这意味着在构建树之后,不能插入或删除元素。d维范围树是根据d维点数据的第一个维度构建的二叉叶搜索树,其中每个顶点包含一个关于第二个维度的子树(子层树)中的点的(d-1)-维搜索树。
d维范围树可用于确定给定d维区间内的所有d维点(窗口查询)。
下面的图片显示了一个二维和d维范围树。
二维和d维范围树。
二维树是第一维上的二叉搜索树。 顶点v的每个子层树是第二维上的二叉搜索树。 v的子层树中的数据项是v子树中的所有数据项。
对于d维范围树,图中显示了树中每一层的一个子层树。
该树可以在 O(nlogd−1n)时间内建立,需要 O(nlogd−1n)空间。位于 d 维查询区间内的 d 维点可以在 O(logdn+k)时间内报告,其中 n 是点的总数,k 是报告的点的数量。
5、线段树
线段树是给定坐标集的静态二叉搜索树。坐标集由输入数据区间的端点定义。任何两个相邻坐标构成一个基本区间。每个叶子对应一个基本区间。内部顶点对应于该顶点子树区间的并集。每个顶点或叶子v包含一个子层类型(或列表,如果是一维的),其中包含所有区间I,使得I包含顶点v的区间,但不包含v的父顶点的区间。
d维的段树可以用来解决以下问题:
确定包含d维点的所有d维区间。这种查询类型称为反向范围查询。
确定包围给定d维区间(包围查询)的所有d维区间。
确定部分重叠或包含在给定d维区间中的所有d维区间(窗口查询)。
下面显示了一维分段树的一个示例和二维分段树的一个示例。
一维和二维分段树
对于一维分段树,分段和相应的基本区间显示在树下方。从节点指向其子集的弧线。
对于二维分段树,我们看到树的第一层是根据第一维的基本区间构建的。每个顶点v的子层树都是根据v的所有数据项的第二维构建的分段树。
树可以在O(nlogdn)时间内建立,需要O(nlogdn)空间。d维段树中逆范围查询的处理时间为O(logdn+k)时间,其中n是区间总数,k是报告的区间数。
二维分段树的一个可能应用如下。给定二维空间中的一组凸多边形(Polygon_2),我们想确定与给定矩形查询窗口相交的所有多边形。因此,我们定义了一个二维分段树,其中数据项的二维区间对应于多边形的边界框,值类型对应于多边形本身。分段树由所有数据项的序列创建,并执行窗口查询。最后对结果数据项的多边形进行独立交叉测试。
6、其他
原型设计模式是一种创建型设计模式,它允许通过复制(或克隆)现有对象来创建新对象,而无需从头开始实例化对象。这种设计模式主要用于减少创建对象的成本和时间,同时提供了一种简洁的方式来创建相似或相同的对象。
在原型设计模式中,通常会有一个原型对象作为模板,其他对象可以通过复制这个原型对象来创建。这种复制过程通常是通过实现一个克隆方法(如 clone()
)来完成的,该方法会创建一个与原型对象具有相同状态的新对象。
2维线段树是即一维线段树中每个节点都是一维线段树,多维同理。线段树是二叉搜索树。
一维范围树的示例。除叶子以外的每个节点都在其左子树中存储最大值。
一组一维点上的范围树被视为这些点上的平衡二叉搜索树。存储在树中的点存储在树的叶子中;每个内部节点都存储其左侧子树中包含的最大值。 d维点集上的范围树是递归定义的多级二进制搜索树。数据结构的每个级别都被视为d维之一上的二进制搜索树。第一级是第一个d坐标上的二叉搜索树。