左偏树的正确性和复杂度分析
摘录——实现分析
原文http://m.blog.csdn.net/article/details?id=7247454
左偏树,也可以称之为左式堆。称其为树,是因为其存储结构通常采用二叉树,所以可以认为是一种特殊的二叉树。称其为堆,是因为在逻辑结构上,它属于可合并堆的一种。其实数据结构中最欣欣向荣的两个分支就是:平衡树 和可合并堆。高级树结构的核心都是围绕如何使树到达平衡而展开,高级堆结构的核心就是如何有效地进行合并。
首先看左偏树的性质:
【堆性质】:任意节点的关键字大于等于其孩子节点的关键字
【左偏性质】:定义到最近的孩子的距离为节点距离dist,那么任意节点的左孩子的距离大于右孩子的距离。
堆性质是为了让最小的结点始终在根的位置,这是所有堆都有的性质。
而左偏性质,则是为了让树状存储的堆,树的深度不能过大,且利于合并。
那么这个性质是怎么完成这两个功能的呢?左偏性质使树的左侧的深度始终大于等于右侧的深度,这一点从名字上就能体会到,也可以画几棵左偏的树试试。而左偏树在实现插入操作时总是从右侧插入,也就是总是让短的一侧生长,如果右侧长于了左侧,那么把左右侧交换一下,继续从短的一侧生长。其实如果不考虑具体的细节,那么这样的直观理解可以看到左偏树的一些本质内涵,一颗树有两个分支,每次要生长的时候,总是让短的一侧先生长,那么这棵树,最后是不是就能够比较对称呢?自然常识和严密的技术逻辑有时候是一致的。
LTNode *merge(LTNode * &A, LTNode * &B) {
if (A == NULL || B == NULL)
return A == NULL ? B : A;
if (A->data > B->data) { /* 确保B->data >= A->data */
swap < LTNode * >(A, B);
}
A->rchild = merge(A->rchild, B); /* 新来个左偏树始终合并到右侧 */
/* 由于新结点合并到右侧,右侧结点现在一定存在了,但左侧不一定 */
if (A->lchild == NULL || /* 左侧为空,一定小于右侧 */
A->rchild->dist > A->lchild->dist) /* 右侧大于了左侧 */
swap < LTNode * >(A->lchild, A->rchild);
if (A->rchild == NULL) {
A->dist = 0;
} /* 右子树为空 */
else {
A->dist = A->rchild->dist + 1;
}
return A;
}
正确性分析
前人之述备矣。然则左偏堆为何正确?下面我们证明左偏堆合并操作的正确性。考虑下面三种情况并分别证明:(下面所有分析针对小根堆)
- 左偏堆H1合并到H2,|H1| = 1(只有一个元素),H1.root < H2.root。这种情况只把H2接在H1的左孩子上,显然可以维持左偏性和堆性质。(情况1)
- 左偏堆H1合并到H2,|H1| = 1(只有一个元素),H1.root > H2.root,证明合并的堆符合左偏性和堆性质。这种情况又可以分两类:
- H1.root < H2.right;这又十分简单。根据递归操作
H2->right = merge(H2->right, H1.root)
可以直接归纳为情况(1)。(情况2) - H1.root > H2.right;根据递归操作,要么归纳为情况1或2,要么归纳为自身,且规模更小,最终必然归纳为1或2。(情况3)
- H1.root < H2.right;这又十分简单。根据递归操作
- 左偏堆H1合并到H2,H1.root < H2.root,证明合并的堆符合左偏性和堆性质。这是要证明的最终情况。根据递归操作,要么归纳为前面的情况,要么归纳为自己,且规模更小。因此正确。
这个证明并不是十分严谨,但是能给读者一个对于左偏堆的原理的更好理解。
复杂度分析
显然,合并操作与左孩子无关。那么对于总孩子数相同的左孩子越多,效率必然越高;左孩子越少,效率必然越低。那么不妨假设左孩子数的两个极端:
每个左孩子比右孩子恰好多1,两颗树规模为n,m,极端下,两颗树交替成为操作中的A数,有
T(n,m) = T(m,n/2) + O(1) = O(lgn + lgm)
。全部为左孩子,为基本情况,复杂度为O(1)。
不难得出所有操作的复杂度:
情况 | 插入 | 删除 | 合并 | 取最值 |
---|---|---|---|---|
最好 | O(1) | O(1) | O(1) | O(1) |
最坏 | O(lgn) | O(lgn) | O(lgn) | O(1) |
但不幸的是,左偏树的实际效率并不理想。如果插入/合并较多,pbds的thin_heap和pairing_heap都更有优势;没有合并的话,甚至std::priority_queue都能碾压左偏树。在现行OI赛制之下,左偏树的实际作用应该并不大。不过它的优雅和简单已经足以让人折服。
手制的模板
#include <iostream>
using std::swap;
template <typename T>
struct LtHeapNode {
typedef LtHeapNode<T> Node;
T data;
size_t dist;
Node *left, *right;
LtHeapNode() : dist(0), left(0), right(0) {}
LtHeapNode(const T& _data) : dist(0), left(0), right(0)
{
data = _data;
}
};
template <typename T, typename Comp>
class LtHeap {
private:
typedef LtHeapNode<T> Node;
typedef LtHeap<T, Comp> Heap;
Node *root;
Comp cmp;
public:
LtHeap():root(0) {}
void clear(Node* &n)
{
if(n) {
clear(n->left);
clear(n->right);
n = 0;
}
}
~LtHeap()
{
clear(root);
}
bool empty()
{
return !root;
}
Node* merge(Node* &A, Node* &B)
{
if(!A||!B)
return A?A:B;
if(cmp(A->data,B->data))
swap(A,B);
A->right = merge(A->right, B);
if(!A->left || A->left->dist < A->right->dist)
swap(A->left, A->right);
A->dist = !A->right?0:A->right->dist+1;
return A;
}
void push(const T& _dat)
{
Node* n = new Node(_dat);
root = merge(root, n);
}
void merge(Heap& _another)
{
root = merge(root, _another.root);
_another.root = 0;
}
void pop()
{
root = merge(root->left, root->right);
}
T top()
{
return root->data;
}
};