数据结构详解——最大(小)左倾树
最大(小)左倾树的定义及用途
最大(小)左倾树实际上是对优先级队列的一种实现。所谓优先级队列,与先入先出(FIFO)的一般队列不同,优先级队列是按照元素的优先级确定出队顺序的。优先级队列的一种实现是堆(数据结构详解——堆)。虽然堆结构具有很好的空间和时间利用率,但它并不能适用于所有的优先级队列的应用。特别是希望合并多个优先级队列时,以及涉及多个大小不同的优先级队列时,左倾树的结构更为适用。
在介绍左倾树的定义之前,首先要先介绍另外几个概念:
- 扩展二叉树:对于一棵二叉树,所有的空子树用一个特殊的节点代替,该节点称为外部节点,其余节点称为内部节点。带有外部节点的二叉树就称为扩展二叉树。
- s值:对于一个节点x,x到外部节点的最短路径的长度称为x的s值。若x为外部节点,则s值为0;如果x是内部节点,则x的s值为 m i n { s ( L ) , s ( R ) } + 1 ( L , R 为 x 的 左 右 孩 子 ) min\{s(L),s(R)\}+1(L,R为x的左右孩子) min{s(L),s(R)}+1(L,R为x的左右孩子)。
下面的图给出了一个扩展二叉树的例子:
其中蓝色圆形代表内部节点,红色矩形代表外部节点,并用abcdef作了标注。再根据s值的定义,我们把各个内部节点的s值标在图中:
下面给出左倾树和最大(小)左倾树的定义:
- 一棵二叉树是基于高度的左倾树(HBLT),对于每个内部节点满足:左孩子的s值大于等于右孩子的s值。
- 最大(小)左倾树既是HBLT,也是最大(小)树。(最大树的定义见 数据结构详解——堆)
最大左倾树和最小左倾树本质上是相同的,下面的讨论中将只讨论最大左倾树。前面给出的扩展二叉树中,a的父亲节点不满足HBLT的条件,因此它不是HBLT。下面给出了最大左倾树的一个例子:
另外还有一种基于权重的左倾树(WBLT),与HBLT非常类似,定义如下:
- w值(权重):对于二叉树中的一个节点来说,以该节点为根节点的子树中的内部节点个数称为该节点的w值。
- 如果一个二叉树中,每个内部节点的左孩子的w值都大于等于右孩子的w值,则称该二叉树为基于权重的左倾树(WBLT)。最大(最小)WBLT既是一棵WBLT,也是一棵最大(最小)树。
WBLT和HBLT对于查找、插入、删除、合并等操作是类似的,下面就只介绍对HBLT的操作。利用HBLT,我们也可以实现优先级队列。
对于HBLT,有以下的性质:
- 以 x x x为根的子树中,节点的数量至少为 2 s ( x ) − 1 2^{s(x)}-1 2s(x)−1个。
- 如果以 x x x为根的子树中有 m m m个节点,则 s ( x ) s(x) s(x)最多为 l o g 2 ( m + 1 ) log_2(m+1) log2(m+1).
- 从 x x x沿最右侧路径到某个外部节点(即从 x x x开始,沿右孩子移动构成的路径)的长度为 s ( x ) s(x) s(x).
操作最大HBLT
下面我们来讨论如何操作最大HBLT,主要需要实现以下的操作:
- 插入到HBLT
- 删除HBLT中的最大元素
- 合并两个HBLT
- 初始化HBLT
合并操作
合并策略可以用递归的方式很好地描述。假设A和B是我们要合并的两棵最大HBLT,并且假设A和B都不为空(如果一棵树为空,则另一棵就是结果)。那么合并过程可以用如下方法描述:
- 首先比较A和B的根节点,较大的作为合并后的树的根节点。
- 不妨设A的根节点较大,那么A的左子树L不变,A的右子树和B合并成一个新的最大左倾树C,比较L和C的s值,较大的作为左子树。
举个例子说明,假设我们要合并如下图所示的两棵最大HBLT:
首先我们要比较根节点的值,确定哪一个节点作为合并后的根节点,然后将右子树和另一棵树进行合并。这实际上是一个拆分的过程,直到拆分出一棵空树为止,不断拼接成HBLT,并检查左右孩子的s值大小,并在必要时进行交换即可。下图展示了这个流程:
总结一下,具体编程时,所写的函数的参数是要合并的两棵树的根节点x和y,具体的流程如下:
- 首先检查两棵树是否有一棵为空树,有空树则返回另一棵。
- 通过检查后的两棵树都不为空,那么就比较它们的根节点的值,选取最大的那个。为了方便起见,我们可以约定经过这个操作后,x的值始终大于等于y的值。
- 以x的右子树和y进行递归。
- 检查x的左右孩子的s值,如果左孩子的s值较小则交换左右孩子。并且对于根节点来说,根节点的s值等于右孩子的s值+1.
- 返回x。
(具体的实现代码在最后)
插入操作和删除操作
最大HBLT的插入操作和删除操作实际上都可以统一成两个HBLT的合并问题。
假设要将元素x插入到最大HBLT中,我们可以把元素x视为一个含有一个内部节点的HBLT,就可以将插入操作转化成两棵树的合并操作。
HBLT和堆一样,最大元素位于根节点。如果删除根节点,我们就可以得到左右子树两个HBLT,再将这两棵树合并起来,就可以完成删除操作。
初始化操作
假设我们要初始化一个含有n个元素的最大HBLT,如果我们使用n次插入操作来进行初始化,那么总的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),这并不是最优的初始化方法。事实上,更优的操作如下:
- 首先把n个元素都创建成一个含一个节点的最大HBLT。
- 将它们放入一个普通的先入先出的队列中,然后每次让两棵最大HBLT出队列,进行合并,然后再将合并后的树放入队列,直到这个队列中最后只剩下一个最大HBLT为止。
这样进行初始化的时间复杂度为 O ( n ) O(n) O(n),是更优的算法。
Java语言实现的最大HBLT
实现代码如下:
import java.util.ArrayList;
public class MaxHblt implements MaxPriorityQueue {
private class HbltNode implements BinaryTreeNode{
private Comparable element;
private HbltNode leftChild;
private HbltNode rightChild;
public int s;//s值
//constructor
public HbltNode(){
}
public HbltNode(Comparable element){
this.element = element;
s = 0;
}
public HbltNode(Comparable element,
HbltNode leftChild,
HbltNode rightChild){
this.element = element;
this.leftChild = leftChild;
this.rightChild = rightChild;
s = 0;
}
public HbltNode(Comparable element,
HbltNode leftChild,
HbltNode rightChild,
int s){
this.element = element;
this.leftChild = leftChild;
this.rightChild = rightChild;
this.s = s;
}
//set method
@Override
public void setElement(Object element) {
this.element = (Comparable) element;
}
@Override
public void setLeftChild(BinaryTreeNode leftChild) {
this.leftChild = (HbltNode) leftChild;
}
@Override
public void setRightChild(BinaryTreeNode rightChild) {
this.rightChild = (HbltNode) rightChild;
}
//get method
@Override
public Comparable getElement() {
return element;
}
public HbltNode getLeftChild() {
return leftChild;
}
@Override
public HbltNode getRightChild() {
return rightChild;
}
@Override
public String toString() {
return "HbltNode{" +
"element=" + element +
'}';
}
}
private HbltNode root;
private int queueSize;
MaxHblt(){
root = null;
queueSize = 0;
}
@Override
public Comparable getMax() {
if (size() == 0) return null;
return root.getElement();
}
@Override
public Comparable removeMax() {
if (size() == 0) return null;
Comparable maxElement = root.getElement();
root = meld(root.leftChild,root.rightChild);
queueSize--;
return maxElement;
}
@Override
public boolean isEmpty() {
if (root == null) return true;
else return false;
}
@Override
public int size() {
return this.queueSize;
}
@Override
public void put(Comparable theObject) {
root = meld(root,new HbltNode(theObject));
queueSize++;
}
//合并两棵子树
public void meld(MaxHblt x) {
root = meld(root,x.root);
queueSize += x.size();
}
public static HbltNode meld(HbltNode x,HbltNode y) {
//x或y为空,直接返回
if (x == null) return y;
if (y == null) return x;
//比较根节点的值,将根节点较大的作为x
if (x.getElement().compareTo(y.getElement()) < 0){
HbltNode temp = x;
x = y;
y = temp;
}
//x的右子树和y进行合并,作为x的新右子树
x.setRightChild(meld(x.rightChild,y));
//比较左右孩子的s值,在必要时进行交换
if (x.leftChild == null) {
x.setLeftChild(x.rightChild);
x.setRightChild(null);
x.s = 1;
}
else {
if (x.leftChild.s < x.rightChild.s){
HbltNode temp = x.rightChild;
x.setRightChild(x.leftChild);
x.setLeftChild(temp);
}
//更新s值
x.s = x.rightChild.s + 1;
}
return x;
}
//初始化
public void initialize(Comparable[] element,int size) {
ArrayList<HbltNode> hbltNodeArrayList = new ArrayList<>();
root = null;
queueSize = size;
for (int i = 0;i < size;i++) {
hbltNodeArrayList.add(new HbltNode(element[i]));
}
while (hbltNodeArrayList.size() >= 2){
HbltNode x = hbltNodeArrayList.remove(0);
HbltNode y = hbltNodeArrayList.remove(0);
hbltNodeArrayList.add(meld(x,y));
}
if (size > 0) root = hbltNodeArrayList.remove(0);
}
}
相关联的三个接口定义如下:
/**
* 二叉树节点
*/
public interface BinaryTreeNode {
void setElement(Object element);
void setLeftChild(BinaryTreeNode leftChild);
void setRightChild(BinaryTreeNode rightChild);
Object getElement();
BinaryTreeNode getLeftChild();
BinaryTreeNode getRightChild();
}
/**
* 优先级队列
*/
public interface PriorityQueue {
boolean isEmpty();//队列为空返回True
int size();//返回队列长度
void put(Comparable theObject);//插入元素
}
/**
* 最大优先级队列
*/
public interface MaxPriorityQueue extends PriorityQueue{
Comparable getMax();//返回最大元素
Comparable removeMax();//去除优先级最大的元素
}
可以看到,左倾树实现的优先级队列虽然在空间利用率上小于堆,但它在合并两个优先级队列上的便捷性是无可比拟的,在具体开发过程中,应当根据实际需求确定使用何种数据结构。