左倾堆(或左偏树)和之前记录过的二叉堆一样,是堆的一种;和普通的二叉堆不同,它是一种可合并堆。可合并堆相比于普通的二叉堆在对两个堆进行合并的操作上具有很大的优势:对于基本的二叉堆合并,时间复杂度为O(n), 而对于可合并堆,其时间复杂度为O(log2n).
左倾堆性质
左倾堆(也叫左偏树),是一种可合并堆.它有以下性质:
- 每个节点含有左子结点指针、右子节点指针、键值key、NPL(Null Path Length,表示当前节点到最近的一个不满子节点的长度,不满节点指的是节点含有少于两个子节点)
- 每个节点的键值都小于其左右子节点的键值(最小堆的性质)
- 每个节点的左子节点的NPL 大于等于 右子节点的NPL
- 节点的NPL值等于其右子节点的NPL值 + 1
- 左倾堆的左右子节点及其下方节点构成的堆也分别都是左倾堆
- 叶子节点的NPL值等于0,空节点的NPL值等于 -1
左倾堆合并
可合并堆相比普通二叉堆在进行堆的合并上具有很大优势,对于左倾堆而言,合并操作为核心操作,像插入、删除操作等都可以在合并操作的基础上完成:插入操作可以将新插入的元素视为一个堆,然后进行两个左倾堆的合并;删除操作(堆的删除总是从堆顶位置取出元素)去除堆顶元素之后,将堆顶元素的左子堆和右子堆进行合并。
左倾堆的合并过程为:
- 如果一个空左倾堆和一个非空左倾堆进行合并,直接返回非空左倾堆
- 如果两个左倾堆都非空,比较两个堆的根节点,去除较小的堆的根节点作为新的根节点。然后将“较小堆”的根节点的右子节点(和它下方的堆)和“较大堆”进行合并(递归)
- 如果新堆的右子节点的NPL大于左子结点的NPL,则交换左右子节点
- 设置新堆的根节点的NPL = 右子节点的NPL + 1
关于堆合并的具体过程,可以参考 左倾堆-图文解析
实现(c++)
二叉堆和可合并堆经常用在优先队列(priority queue)中,下面的代码就进行了堆的合并、元素的入队和出队
#include<iostream> using namespace std; struct TreeNode{ int key; int npl; TreeNode* left; TreeNode* right; TreeNode(int k){ key = k; npl = 0; left = right = NULL; } }; void Swap(TreeNode* node1, TreeNode* node2){ TreeNode* tmp = node1; node1 = node2; node2 = tmp; } //左倾堆的合并操作 TreeNode* Merge(TreeNode* heap1, TreeNode* heap2){ //如果其中一个为空堆,则直接返回另外一个 if (!heap1) return heap2; if (!heap2) return heap1; //选择“较小堆”,将其根节点作为新堆的根节点 if (heap1->key > heap2->key){ Swap(heap1, heap2); } //将较小堆的右子堆和较大堆进行合并,并置为较小堆的右子堆 heap1->right = Merge(heap1->right, heap2); if (heap1->left == NULL || (heap1->left && heap1->right && heap1->left->npl < heap1->right->npl)){ //如果堆的根节点的左子结点的npl小于右子节点的npl,则交换左右子节点 Swap(heap1->left, heap1->right); } if (heap1->right == NULL) heap1->npl = 1; else heap1->npl = heap1->right->npl + 1; //新堆的npl设为右子节点的npl +1 return heap1; } //将元素k插入堆 heap中,即元素入队。 注意这里使用指针的引用 void Enqueue(TreeNode*& heap, int k){ if (!heap){ heap = new TreeNode(k); return; } TreeNode* new_heap = new TreeNode(k); heap = Merge(heap, new_heap); } //元素出队,注意这里使用指针的引用 int Dequeue(TreeNode*& heap){ int result = heap->key; heap = Merge(heap->left, heap->right); return result; }