LeftistTree(LeftistHeap) 左偏树(左偏堆)

左偏树

左偏树是一种类似小根堆的二叉树,它的根节点总是树中的最小值。左偏树的每次取出/加入操作的时间复杂度为O(log2n)O(log_2 n),与小根堆相同。与堆不同的是,合并两个数量为nn的堆的时间复杂度为O(n×log2n)O(n \times log_2 n)(因为合并两个堆需要从其中一个取出元素加入另一个,重复nn次),而合并两个数量为nn的左偏树的时间复杂度为O(log2n)O(log_2 n)

左偏树的主要操作有:(1)(1) 合并两个左偏树;(2)(2) 插入新节点;(3)(3) 查找最值;(4)(4) 删除最值。其中(2)(4)(2) - (4)的实现依赖于(1)(1),合并操作是左偏树的核心。

左偏树中的节点的距离dd是该节点到最右下叶子节点的距离。左偏树具有以下性质:

(1)(1) 父节点的值小于等于其左右孩子节点的值,即valuefathervalueleft,valuefathervaluerightvalue_{father} \leq value_{left}, value_{father} \leq value_{right}。最小的值在根节点类似小根堆的头节点;

(2)(2) 父节点的左孩子节点的距离大于等于右孩子节点的距离,即dleftdrightd_{left} \geq d_{right}

(3)(3) 父节点的距离等于其右孩子节点的距离加11,即dfather=dright+1d_{father} = d_{right} + 1

(4)(4) 具有nn个节点的左偏树的根节点的距离小于等于log2(n+1)1log_2⁡(n+1) - 1,即drootlog2(n+1)1d_{root} \leq log_2⁡(n+1) - 1

下图中每个节点上,上面的数字代表节点的值,下面的数字代表距离。合并两个左偏树的操作,分为两步:

(1)(1) 从根节点开始递归向下合并两个左偏树。每次合并时比较两个子树根节点的值valueavalue_{a}valuebvalue_{b},总是用根节点较大的子树valuebvalue_{b}(设valueb>valueavalue_{b} \gt value_{a})替换根节点较小的子树valueavalue_{a}的右孩子节点valuealeftvalue_{a-left},替换后valuealeftvalue_{a-left}成为新的子树。然后递归的,继续考虑valuealeftvalue_{a-left}作为根节点的子树与valuebvalue_{b}作为根节点的子树;

(2)(2) 从叶子节点开始递归向上更新所有节点的距离。对于每个节点,首先若不满足dleftdrightd_{left} \geq d_{right}则将左右孩子子树交换,其次若不满足dfather=dright+1d_{father} = d_{right} + 1则更新父结点的距离dfatherd_{father}

下图是合并两个左偏树的过程:

(1)(1) 比较两树的根节点6<76 \lt 7,节点77沿着节点66向右下寻找第11个满足7<x7 \lt x的节点xx,替换xx作为节点66的新右孩子节点。该节点为节点887<87 \lt 8),节点66的原右孩子节点88暂时脱离;

(2)(2) 节点88沿着节点77向右下寻找第11个满足8<x8 \lt x的节点xx,替换xx作为节点77的新右孩子节点。该节点为节点12128<128 \lt 12),节点77的原右孩子节点1212暂时脱离;

(3)(3) 节点1212沿着节点88向右下寻找第11个满足12<x12 \lt x的节点xx,替换xx作为节点88的新右孩子节点。该节点为节点131312<1312 \lt 13),节点88的原右孩子节点1313暂时脱离;

(4)(4) 节点1313沿着节点1212向右下寻找第11个满足13<x13 \lt x的节点xx,替换xx作为节点1212的新右孩子节点。该节点为节点262613<2613 \lt 26),节点1212的原右孩子节点2626暂时脱离;

(5)(5) 节点2626沿着节点1313向右下寻找第11个满足26<x26 \lt x的节点xx,节点1313没有右孩子节点,因此节点2626直接成为节点1313的右孩子节点,不再需要替换,合并操作结束;

向右下插入节点的操作会影响到左偏树的平衡性,右子树变得越来越庞大。而且所有节点的距离也是错的(没有更新)。实际上每一步合并操作后还需要检查左右子树的距离属性:(1)(1)对于dleft<drightd_{left} \lt d_{right}的情况,交换左右子树;(2)(2)对于drootdright+1d_{root} \neq d_{right} + 1的情况,更新drootd_{root}

节点距离的更新必须从叶子节点开始向上进行,对于之前步骤中修改过的节点,重新遍历计算其距离(其中dnil=1d_{nil} = -1):

(6)(6) 对于节点2626d27=0dnil=1d_{27} = 0 \ge d_{nil} = - 1,不需要交换左右子树,更新d26=dnil+1=1+1=0d_{26} = d_{nil} + 1 = - 1 + 1 = 0

(7)(7) 对于节点1313d28=0d26=0d_{28} = 0 \ge d_{26} = 0,不需要交换左右子树,更新d13=d26+1=0+1=1d_{13} = d_{26} + 1 = 0 + 1 = 1

(8)(8) 对于节点1212d31=1d26=1d_{31} = 1 \ge d_{26} = 1,不需要交换左右子树,更新d12=d13+1=1+1=2d_{12} = d_{13} + 1 = 1 + 1 = 2

(9)(9) 对于节点88d10=1<d12=2d_{10} = 1 \lt d_{12} = 2,需要交换子树1010和子树1212,更新d8=d10+1=1+1=2d_{8} = d_{10} + 1 = 1 + 1 = 2

(10)(10) 对于节点77d9=1<d8=2d_{9} = 1 \lt d_{8} = 2,需要交换子树99和子树88,更新d7=d9+1=1+1=2d_{7} = d_{9} + 1 = 1 + 1 = 2

(11)(11)对于节点66d11=2d7=2d_{11} = 2 \ge d_{7} = 2,不需要交换左右子树,更新d6=d7+1=2+1=3d_{6} = d_{7} + 1 = 2 + 1 = 3

编码时通过递归的方式将合并和更新距离两个操作放在同一个递归函数中,合并函数Merge返回合并后左偏树的根节点的距离dd,在Merge中调用左右孩子的合并操作,获取左右孩子的距离,然后再决定是否交换左右子树,并更新父节点的距离。本文的将两个步骤分开是为了方便理解算法。

左偏树的插入操作,可以看作左偏树与一个只有根节点的左偏树的合并操作;删除最值的操作,可以看作删除根节点后,合并左右子树的操作。

左偏树的合并操作、插入节点操作、删除根节点操作的时间复杂度都为O(log2n)O(log_2⁡n)

源码

LeftistTree.h

LeftistTree.cpp

测试

LeftistTreeTest.cpp

Last updated