【数据结构】优先队列专题 左式堆

文章目录

  • 1. 左式堆概述
  • 2. 左式堆性质
  • 3. 左式堆操作和代码实现
    • (1) 合并操作
    • (2) 插入操作
    • (3) 删除最小值操作


1. 左式堆概述

为了有效支持合并操作,即以 o ( N ) o(N) o(N) 时间进行 Merge ,我们需要使用动态的链接结构或者静态链表。如果只使用数组形式的堆结构,在进行合并时光拷贝一个数组到另一个数组中,就将花费 Θ ( N ) \Theta(N) Θ(N) 的时间。

左式堆 (leftist tree, leftist heap ),或者说左偏堆、左偏树,是一种优先队列实现方式,属于可并堆(除了左式堆外,还有如斜堆、二项堆、配对堆、斐波那契堆等)。在统计问题、最值问题、模拟问题和贪心问题等等类型的题目中,都有着广泛的应用。

左式堆和二叉堆一样具有结构性质堆序性质,它具有和其他堆一样的堆序性质,同时也是一棵二叉树。和二叉堆唯一的区别在于:左式堆是很不平衡的

2. 左式堆性质

为了完全了解左式堆的性质,我们需要以下定义:

  • 外结点:只有一个儿子没有儿子的节点,即左右儿子至少有一个为空节点的节点。
  • 距离/零路径长 null path lengthnpl(X) 定义为 X 结点到它的后代中离它最近的外结点最短路径长度,即两结点之间路径的权值和。特别的,外结点的距离为 0 ,空结点的距离为 npl(NULL) = -1
  • 左偏树/左式堆:一种满足左偏性质堆有序二叉树,其左偏性质体现在左儿子的距离大于等于右儿子的距离
  • 左偏树/左式堆的距离:我们将一棵左偏树根结点的距离作为该树的距离。

左式堆的基本性质如下:

  • 满足堆的基本性质,如小根堆中,结点的键值小于等于其左右儿子结点的键值
  • 对于任意结点,左儿子的距离大于等于右儿子的距离
  • 对于任意结点,其距离等于它右儿子的距离加一
  • 一个距离为 d d d 的左式堆,其结点数必然不小于 2 d + 1 − 1 2^{d + 1} - 1 2d+11 个结点
  • 对于一个有 n n n 个结点的左式堆,其根结点的距离不超过 log ⁡ n \log n logn

下图中,距离或者说 null path length 标记在结点内部。很明显,只有左边的树是左式的,因为它之中任何结点左儿子的距离都大于等于右儿子的距离:
【数据结构】优先队列专题 左式堆_第1张图片

左式堆性质:对于堆中的任意结点 X ,其左儿子的距离至少与右儿子的距离一样长。任一结点的距离,都比它的诸儿子结点的距离的最小值(就是右儿子的距离)多 1

上述性质不仅确保了树平衡的要求,而且偏重于使树向左增加深度,甚至很可能存在由左结点形成的长路径构成的树,在实际上更有利于合并操作——因此,得名左式堆 leftist heap

由于左式堆倾向于增长左路径,所以右路径应该短。甚至,沿着左式堆右侧的右路径是该堆中最短的路径。不然,就存在一条路径通过某个结点 X 向左儿子深入,X 就破坏了左式堆的左偏性质。

定理:在右路径上 r r r 个结点的左式堆必然至少存在 2 r − 1 2^r - 1 2r1 个结点。或者说,一个距离为 d d d 的左式堆,其结点数必然不小于 2 d + 1 − 1 2^{d + 1} - 1 2d+11 个结点。

证明:数学归纳法证明。

  • 如果 r = 1 r = 1 r=1 ,则必然至少存在一个树结点。
  • 设定理对 1 , 2 , … , r 1,2,\dots,r 1,2,,r 个结点成立。考虑在右路径上有 r + 1 r + 1 r+1 个结点的左式树。此时,根具有在右路径上含有 r r r 个结点的右子树,以及在右路径上至少含有 r r r 个结点的左子树(否则就不是左式堆)。对这两个子树应用归纳假设,可知每棵子树上最少有 2 r − 1 2^r - 1 2r1 个结点,加上根结点,于是该树上最少有 2 r + 1 − 1 2^{r + 1} - 1 2r+11 个结点。

从这个定理可知,对于一个有 n n n 个结点的左式堆,其根结点的距离,即右路径最多含有 ⌊ log ⁡ ( n + 1 ) ⌋ \lfloor \log (n + 1) \rfloor log(n+1) 个结点。


3. 左式堆操作和代码实现

对左式堆操作的一般思路是:将所有的工作放到右路径进行,从而保证树的深度更短。不过,对于右路径的 insert, merge 可能破坏左式堆性质。尽管恢复起来也很容易。

左式堆可能有的基本结点信息,具体实现时可以酌情添加和修改:

  • v a l val val :键值
  • l e f t left left :左儿子
  • r i g h t right right :右儿子
  • d i s t dist dist :距离
  • f a t h e r father father :父亲

左式堆的C++结构:

#include 
using namespace std;

template <typename Comparable>
class LeftistHeap {
private:
	struct LeftistNode {
		Comparable element;  //数据值 
		LeftistNode *left;   //左儿子 
		LeftistNode *right;  //右儿子 
		int dist;			 //距离 
		
		LeftistNode(const Comparable &theElement, LeftistNode *lt = nullptr, 
					LeftistNode *rt = nullptr, int d = 0)
			: element(theElement), left(lt), right(rt), dist(d) { }
	};
	LeftistNode *root;
	LeftistNode *merge(LeftistNode *h1, LeftistNode *h2);  	//私有的合并方法 
	LeftistNode *merge1(LeftistNode *h1, LeftistNode *h2);  //实际执行的merge方法 
	
	void swapChildren(LeftistNode *t);						//交换左右子树	
	//void reclaimMemory(LeftistNode *t);						
	//LeftistNode *clone(LeftistNode *t) const;
public:
	LeftistHeap();							//建立空堆 
	LeftistHeap(const LeftistHeap &rhs);	//复制rhs建堆 
	~LeftistHeap();							//析构函数 
	bool empty() const;				   		//判断是否为空 
	const Comparable &findMin() const; 		//找到最小值 
	 
	void insert(const Comparable &x);  		//插入:合并单结点和堆即可 
	void deleteMin();				   		//删除最小值 
	void deleteMin(Comparable &minItem); 	//删除并返回最小值 
	void makeEmpty();				   		//清空左式堆 
	void merge(LeftistHeap &rhs);	   		//公共的合并方法 
	
	const LeftistHeap& operator=(const LeftistHeap &rhs); //赋值运算符函数 
};

(1) 合并操作

左式堆最基本也最重要的操作是合并,毕竟是可并堆。下面介绍一个简单的递归算法,输入是两个左式堆 H 1 , H 2 H_1, H_2 H1,H2 ,如下图,这两个堆都是左式堆,而且是小根堆:
【数据结构】优先队列专题 左式堆_第2张图片
如果两个堆中有一个堆为空,就直接返回另一个堆。否则为了合并两个堆,需要比较它们的根。首先,递归将根值较大的堆根值较小的堆的右子堆合并。这里递归地将 H 2 H_2 H2 H 1 H_1 H1 中根值为 8 8 8 的右子堆合并,得到下图的堆:
【数据结构】优先队列专题 左式堆_第3张图片

这棵树是递归形成的,虽然现在不知道是如何得到的,不过处理基准情形后,我们假设递归步骤能够成立,合并可以完成。接着要让这个新的堆成为 H 1 H_1 H1 的根的右儿子:
【数据结构】优先队列专题 左式堆_第4张图片
这时的堆满足堆序性质,但不满足左偏性质,不是左式堆。可以看出其根结点的左子树的距离为 1 1 1 ,而根的右子树的距离为 2 2 2 。左式堆的性质在根处被破坏。

但是树的其余部分必然是左式的,由于递归步骤,根的右子树是左式的,左子树没有变化还是左式的。所以,只要对根进行调整就好了。为了让整个树都是左式的,交换树根的左儿子和右儿子,并更新根结点的距离,新的距离是新的右儿子的距离加一,这样就完成了 merge
【数据结构】优先队列专题 左式堆_第5张图片

可以手动模拟这个过程,就能够知道这个递归算法如何运行了。步骤总结如下:(函数 merge(x, y) 表示合并 x, y ,返回值是合并后的根结点)

  • 如果有一个根结点是空,则返回另一个根结点;
  • xx, y 中权值较小的那个,即 v a l x ≤ v a l y val_x \leq val_y valxvaly 。若 x 的权值大于 y,交换 x, y 进行合并即可;
  • 为了维护小根性质,合并好的堆的堆顶一定还是 x ,所以递归向下合并 x 的子树(左右都行,一般用右子树)和 y
  • 递归结束后,yx 的右子树形成一个新的左式堆,其结果成为 x 的新的右子树;
  • 合并后可能破坏 x左偏性质,所以需要检查 x 此时左右子树的距离,如果左子树的距离小于右子树,就交换左右子树,更新 x 的距离为新的右子树的距离加一。

代码如下:

//公有的merge方法将rhs合并到已有的优先队列(左式堆),rhs成为空堆 
void LeftistHeap::merge(LeftistHeap &rhs) {
	//rhs必须和this不同
	if (this == &rhs) return; //不接受h.merge(h) 
	root = merge(root, rhs.root); //调用私有的merge方法merge(h1, h2); 
	rhs.root = nullptr;           //rhs的root指向空 
}
//内部方法合并两个根,消除一些特殊情形,保证H1有较小根; 
//作为驱动程序调用merge1进行实际的合并 
LeftistNode *merge(LeftistNode *h1, LeftistNode *h2) {
	if (h1 == nullptr) return h2;
	if (h2 == nullptr) return h1; 
	if (h1->element < h2->element) //保证h1是较小的根 
		return merge1(h1, h2); 
	else 
		return merge1(h2, h1);
}
//merge1进行实际的合并和距离的调整 
LeftistNode *merge1(LeftistNode *h1, LeftistNode *h2) {
	if (h1->left == nullptr) //单结点
		h1->left = h2; 	     //h1的其他部分已经准确了
	else {
		h1->right = merge(h1->right, h2);
		if (h1->left->dist < h1->right->dist) 
			swapChildren(h1);
		h1->dist = h1->right->dist + 1;
	}
	return h1;
}

执行合并的时间与右路径的长的和成正比。因此,合并两个左式堆的时间为 O(logN) \text{O(logN)} O(logN)

(2) 插入操作

插入只是合并的特殊情况,可以看成是单结点堆和更大的堆的 merge

void insert(const Comparable &x) { //push,新建一个新结点和堆合并即可
	root = merge(new LeftistNode(x), root);
}

(3) 删除最小值操作

由于是小根堆,可以除掉根从而得到两个堆,接着将两个堆进行合并即可。执行一次 deleteMin 操作的时间是 O(logN) \text{O(logN)} O(logN)

void deleteMin() {
	if (empty()) throw underflow_error("LeftistHeap Empty");
	LeftistNode *oldRoot = root;
	root = merge(root->left, root->right);
	delete oldRoot;
}

void deleteMin(Comparable &minItem) {
	minItem = findMin();
	deleteMin();
}

你可能感兴趣的:(=====数据结构====,优先队列,#,左式堆)