1. 前言
笛卡尔树
是一种特殊的二叉树数据结构,融合了二叉堆
和二叉搜索树
两大特性。笛卡尔树
可以把数列(组)
对象映射成二叉树
,便于使用笛卡尔树
结构的逻辑求解数列的区间最值或区间排名等类似问题。
如有数列 {5,7,1,9,12,16,2,18,3,11}
,任一存储单元格均有 2
个属性:
- 值:单元格中的数据。
- 位置:单元格在数列中的位置(下标、索引)。
构建笛卡尔树
要求节点至少有 2
个权重,把数列映射成树结构时,可使用这2
个属性作为笛卡尔树节点的权重。把线性结构的数列映射成笛卡尔树
后,此树需要满足如下 2
个特征:
-
如果观察
值
,则在树中具有堆的有序性,即任一父节点的值大于(最大堆)或小于(最小堆)子节点的值。根据堆的特性,可以查询笛卡尔树
或子树
上的最大值或最小值。且可以使用堆排序
算法排序数列。 -
如果观察
位置
,则在树中遵循搜索树
的逻辑结构,或者说,对笛卡尔树进行中序遍历,可以得到原来的数组。
2. 构建笛卡尔树
可以说笛卡尔树
是双权重的节点树。那么,如何构建才能保证数列最终能映射成笛卡尔树。
实现过程中,需要借助单调栈
数据结构。
什么是单调栈?
本质指的就是栈,但在存储数据时,需要要保持栈中数据按递增或递减顺序,本文构建最小堆,要求栈单调递减。
2.1 构建流程
现把数组 {5,7,1,9,12,16,2,18,3,11}
构建成笛卡尔树的过程详细讲解一下:
- 首先,准备一个栈。把数组中位置为
0
、值为5
的元素(后面统一以(0,5)
格式描述)。将其压入栈中,且设置成树的根节点。
-
把数组中的
(1,7)
和栈中上已有元素(0,5)
比较,因值比5
大。因能保持栈的递减顺序,可以直接入栈。且把入栈的元素作为(0,5)
的右子节点。Tips: 至此,总结一下:能压入栈的元素作为栈中比它小的元素的右子节点。
- 从数组中取出
(2,1)
,因比栈顶元素(1,7)
小 ,不能直接入栈,则需要把比它大的元素从栈中移走,然后把最后一个比它大的元素作为它的左子结点。如下图所示:
-
从数组中取出
(3,9)、(4,12)、(5,16)
,因都比先入栈的元素大,可以直接依次入栈,且成为比它小的节点的右子节点。Tips: 可以观察到,栈中的元素均为
右链(图中绿色颜色所标记的节点)
上的节点。
- 从数组中抽出
(6,2)
元素,为了维持栈的递减顺序,则需要把比它大的元素从栈中移走,直到留下(2,1)
。把(6,2)
作为(2,1)
的右子节点,且让最后一个出栈的(3,9)
成为(6,2)
的左子节点。
- 继续拿出
(7,18)
,可以直接入栈,让其成为(6,2)
的右子节点。
- 拿出
(8,3)
。把比它大的元素出栈,成为最近比它小的元素的右子结点,最后出栈的元素成为它的左子节点。
- 从数组中最后拿出
(9,11)
元素。因比栈中所有元素大,直接入栈,且成为栈顶元素的右子节点。
2.2 小结
使用单调栈构建笛卡尔树的原则:
- 如果需要移走其它元素后,元素才能入栈,则移走的最后一个节点成为其左子节点。
- 当数据入栈时,需要在栈中找到比它小的第一个元素,让会成为此元素的右节点。
- 栈中始终存储的最新
右链
上的节点。 - 构建完毕后,最后栈底的元素为根节点。
3. 笛卡尔树的物理结构
二叉树的物理实现有矩阵
和链式
两种方案,本文将使用这 2
种方案:
3.1 矩阵实现
笛卡尔树类结构:
类中主要API
为构建函数和中序遍历,其它的为辅助性函数。代码中有详细注解。
实现 buildDiKeErTree
函数:
buildDiKeErTree
函数是核心API
,用来构建笛卡尔树。
实现此API
之前,需先实现辅助API check
,用来实现把数组中的数据压入栈中时,查询到比之大的最后一个数据。本质是使用了单调栈的逻辑特性。
buildDiKeErTree
函数:
测试:
输出结果:-1
表示不存在左或右子节点。
因中序遍历笛卡尔树
可以输出原数组,可用于验证笛卡尔树的构建是否正确。在DkeTree
类中可添加中序遍历函数的实现。
测试中序遍历:
输出结果:
3.2 链式实现
链式实现逻辑与矩阵实现本质没有什么不一样。不做过多解释,直接上完整代码。
4. 总结
本文讲解了笛卡尔树,对其特性作了较全面的分析,并且讲解了矩阵和链式两种实现方案。
本文同时收录在“编程驿站”公众号。