十五、【优先级队列】左式堆(leftist heap)

  • Post author:
  • Post category:其他



一、左式堆存在的意义

除了标准的插入和删除操作,堆结构在实际应用中的另一个常见的操作是

合并

,即将堆A和堆B重新组织成一个堆。若采用最基本的完全二叉堆数据结构,其最高效的实现策略是采用Floyd建堆算法将堆A和堆B直接重构成一个新堆,但是该算法的时间复杂度为O(n),这种性能在实际中不让人满意,而左式堆结构能保证在合并操作中只需要调整很少的点,所以能保证合并操作的时间复杂度低至O(logn)。而且基于合并操作,其插入删除操作也可以足够的高效,均为O(logn)。

左式堆: 左式堆(leftist heap)是优先级队列的另一种实现方式,可高效地支持堆合并操作。其基本思路:

在保持堆序性的前提下附加新的条件,使得在堆的合并过程中,只需调整很少量的节点,具休地,需参与调整的节点不超过O(logn)个,故可达到极高的效率


二、左式堆的结构


左式堆的整体结构呈单侧倾斜状,其中节点的分布均偏向左侧

,也链是说,

左式堆将不再如完全二又堆那样满足结构性,但事实上这无伤大雅,毕竟堆序性才是堆结构的关键条件,而结构性只不过是堆的一项附加条件

,正如稍后将要看到的,在将平衡性替换为左倾性之后,左式推结构的merge()操作乃至insert()和delMax()操作均可以高效地实现。

左式堆的为实现倾斜,借鉴了AVL树和红黑树技巧,为各节点引入“空节点路径长度”指标,并依此确定相关算法的执行方向。


空节点路径长度(nurl path length)

:记作npl(x),若x为外部节点,则约定 npl(x) = npl (null) =0,反之若x为内部节点,则npl (x)可递归地定义为:



npl(x) =1 + min(npl(lc(x)),npl(rc(x))

也就是说,节点x的npl取决于左右孩子npl值中的小者。

对照上图不难验证:

npl(x)既等于x到外部节点的最近距(该指标由此得名) ,同时也等于以x为根的最大满子树(图中矩形框)的高度


结构上的约束:

左式堆是处处满足“左倾性”的二叉堆,即任一内部节点x都满足:



npl(lc(x))>=npl(rc(x))

也就是说,就npl指标而言,任一内部节点的左孩子都不小于其右孩子,所以左式堆中每个节点的npl值都仅取决于其右孩子。


最右侧通路:


从x出发沿右侧分支一直前行直至空节点,经过的通路称作其最右侧通路(rightmost path)

,记作rPath(x),由“各节点npl值均决定与其右孩子”这一事实不难发现,每个节点的npl值,应恰好等于其最右侧通路的长度。

事实上,在包含n个节点的左式堆中,最右侧通路不会长于O(logn)(可由极限情况–满二叉树的节点数和高度之间的关系证明)

。最右侧通路的最大长度限制确定了合并操作的时间复杂度的上界。


三、左式堆的操作

(1) 合并

假设待合并的左式堆如下左图所示,分别以a和b为堆顶,且不失一般性地 a >= b。

为合并两个堆,



递归

地将a的右子堆和堆b进行合并,然后将合并的子堆作为a的右子堆。为保证依然满足左倾性条件,最后还需要比较a左、右孩子的npl值,如有必要还要讲二者交换

,以保证左孩子的npl值不低于右孩子。

(a) 递归深入

对于两个需要合并的子堆,首先对堆顶进行优先级(关键码)的比较,把大者a作为左侧,小者b放在右侧(这样合并成的子堆才能作为a的孩子,因为a最大),大者的右子堆ar和右侧的堆b进行合并(过程和前述过程一样),合成的新堆作为a的右子堆,如此迭代向下,直至一侧堆为空。

(b) 递归返回

当(a)步骤结束后,在结构上两个堆已经合并完成,但为了保证左倾性依然处处满足,在沿左侧链逐级递归返回的过程中,还需及时比较各对左、右兄弟的npl值,如有必要还应令其交换位置。

复杂度分析: 递归只可能发生于两个待合并推的最右侧通路上。若待合并堆的规模分别为n和m,则其两条量右侧通路的长度则不会超过O(logn)和O(logm),因此合并算法的总体运行时间应不超过:



O(logn) +O(logm) = O(log(max(n, m)))

(2) 删除(基于合并)

在摘除e之后, 左右子堆即可被视作为两个彼此独立的将合井的堆,于是,只需通过合并操作将它们合并起来,则其效果完全等间于一次常规的删除操作。

复杂度分析:

时间成本主要是对合并操作的调用,总体不超过O(logn)。

(3) 插入(基于合并)

拟将元素e插入堆中,只要将x也视作仅含单个节点的堆,则通过合并操作将两个堆合并即可,其效果完全等同于完成了一次词条插入操作。

复杂度分析:

时间成本主要是对合并操作的调用,总体不超过O(logn)。


四、左式堆的实现

由于左式堆已经不满足结构性,所以不能使用向量作为底层容器,故采用二叉树的这种灵活的数据结构,所以左式堆类PQ_LeftHeap由二叉树类binTree和优先级队列PQ多重继承得到。

PQ_LeftHeap接口列表
操作 功能 对象
PQ_LeftHeap() 默认构造函数 左式堆
PQ_LeftHeap(T* E, int n) 构造函数,由数组构造 左式堆
insert(T e) 按照比较器确定的优先级次序插入元素 左式堆
getMax() 取出优先级最高的元素 左式堆
delMax() 删除优先级最高的元素 左式堆
merge(binNode<T>* a, binNode<T>* b) 合并以a和b为根节点的左式堆 左式堆

(1) PQ_LeftHeap.h

#pragma once
#include"PQ.h"
#include"binTree.h"

#define lt(a,b) ((a)<(b))    //比较大小


/*左式堆是基于二叉树结构的*/
template<typename T> class PQ_LeftHeap :public PQ<T>, public binTree<T>
{
public:
	//构造函数
	PQ_LeftHeap() {}
	PQ_LeftHeap(T* E, int n);

	void insert(T e);  //按照比较器确定的优先级次序插入元素
	T getMax();   //取出优先级最高的元素
	T delMax();   //删除优先级最高的元素
	static binNode<T>* merge(binNode<T>* a, binNode<T>* b);   //合并以a和b为根节点的左式堆
};

template<typename T> PQ_LeftHeap<T>::PQ_LeftHeap(T* E, int n)
{
	for (int i = 0; i < n; i++)   //直接插入
		insert(E[i]);
}

template<typename T> binNode<T>* PQ_LeftHeap<T>::merge(binNode<T>* a, binNode<T>* b)
{
	if (!a) return b;   //递归基
	if (!b) return a;   
	if (lt(a->data, b->data))   //保证左节点大
		swap(a, b);
	a->rc = merge(a->rc, b);   //将a的右子堆与b合并
	a->rc->parent = a;
	if (!a->lc || a->lc->nlp < a->rc->npl)   //若a的左孩子不存在或者a的左子树的npl比右子树的小,则转换左右子树
		swap(a->lc, a->rc);
	a->npl = a->rc ? a->rc->npl + 1 : 1;  //更新a的npl
	return a;   //返回合并后的堆顶
}

template<typename T> T PQ_LeftHeap<T>::delMax() 
{
	binNode<T>* lHeap = _root->lc;   //左子堆
	binNode<T>* rHeap = _root->rc;   //右子堆
	T e = _root->date;
	delete _root;
	_size--;
	_root = merge(lHeap, rHeap);   //原左右子堆合并
	if (_root)
		_root->parent = nullptr;
	return e;
}

template<typename T> T PQ_LeftHeap<T>::getMax()
{
	return _root->date;
}

template<typename T> void PQ_LeftHeap<T>::insert(T e)
{
	binNode<T>* v = new binNode<T>(e);  //为e创建一个二叉树节点
	_root = merge(_root,v);  //通过合并完成插入
 	_root->parent = nullptr;
	_size++;
}



版权声明:本文为qq_18108083原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。