左偏树的正确性和复杂度分析

版权声明:允许转载,但不得改动内容或用于商业目的 https://blog.csdn.net/OIljt12138/article/details/51234164

摘录——实现分析

原文http://m.blog.csdn.net/article/details?id=7247454

左偏树,也可以称之为左式堆。称其为树,是因为其存储结构通常采用二叉树,所以可以认为是一种特殊的二叉树。称其为堆,是因为在逻辑结构上,它属于可合并堆的一种。其实数据结构中最欣欣向荣的两个分支就是:平衡树 和可合并堆。高级树结构的核心都是围绕如何使树到达平衡而展开,高级堆结构的核心就是如何有效地进行合并。

首先看左偏树的性质:

  1. 【堆性质】:任意节点的关键字大于等于其孩子节点的关键字

  2. 【左偏性质】:定义到最近的孩子的距离为节点距离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,证明合并的堆符合左偏性和堆性质。这种情况又可以分两类:
    1. H1.root < H2.right;这又十分简单。根据递归操作H2->right = merge(H2->right, H1.root)可以直接归纳为情况(1)。(情况2)
    2. H1.root > H2.right;根据递归操作,要么归纳为情况1或2,要么归纳为自身,且规模更小,最终必然归纳为1或2。(情况3)
  • 左偏堆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;
    }
};
阅读更多
换一批

没有更多推荐了,返回首页