6-4 左偏堆
左偏堆(Leftist Heap)
左偏堆(Leftist Heap),又称左偏树(Leftist Tree),是一种可合并堆(Mergeable Heap)数据结构,基于二叉树实现。它通过维持一个关键不变量——每个节点的左子节点零路径长度(Null Path Length, npl)始终大于等于右子节点的零路径长度——来保证从根节点沿右侧路径到达叶子节点的距离始终为 O(log n)。因为左子树总是"偏重"(更深),所以得名"左偏堆"。
零路径长度(Null Path Length, npl)的定义:从节点到最近空子节点的最短路径长度。空节点的 npl 定义为 0。对于任意非空节点,npl = 1 + min(npl(left), npl(right))。左偏性质要求每个节点的左子节点 npl >= 右子节点 npl,这使得右路径(Right Spine)——从根一直沿右子节点向下到达空节点的路径——始终保持最短。
左偏堆在实际中的应用包括:
- 可合并优先队列(Mergeable Priority Queue)
- 图算法中需要频繁合并优先队列的场景
- 多路归并排序
- 不需要预先知道总元素数量的场景
左偏堆的结构
下面展示一棵示例左偏堆,每个节点旁标注了零路径长度(npl):
示例左偏堆(最小堆):
2 (npl=2)
/ \
/ \
5(1) 10(1)
/ \ \
7(0) 8(0) 20(0)
左偏性质验证:
节点 2: npl(左)=1 >= npl(右)=1 ✓
节点 5: npl(左)=0 >= npl(右)=0 ✓
节点 10: npl(左)=0 >= npl(右)=0 ✓(左子为空,npl=0)
叶节点 7, 8, 20: 左右均为空,npl=0 ✓
右路径: 2 -> 10 -> 20(长度 2)
左路径: 2 -> 5 -> 7 或 5 -> 8(长度 2)
这棵树比较平衡,下面看一个更"偏"的例子:
1 (npl=3)
/ \
/ \
3(2) 6(1)
/ \ \
5(1) 8(0) 15(0)
/ \
10(0) 12(0)
右路径: 1 -> 6 -> 15(长度 2)
左路径: 1 -> 3 -> 5 -> 10(长度 3)
可以看到左子树明显比右子树深,这就是"左偏"的含义。
关键性质:一棵有 n 个节点的左偏堆,其右路径长度最多为 floor(log2(n+1))。这是因为右路径上每个节点的 npl 严格递减,而 npl 值为 r 的子树至少包含 2^r - 1 个节点。
零路径长度(Null Path Length)
零路径长度(Null Path Length, npl)是左偏堆的核心概念,它决定了树的结构和操作的效率。
- npl 的定义:从节点到最近空子节点的最短路径长度
- npl(null) = 0
- npl(node) = 1 + min(npl(node.left), npl(node.right))
- 左偏性质:对于每个节点,npl(left) >= npl(right)
- 推论:右路径长度 = O(log n)
npl 计算示例:
A (npl=3)
/ \
B C (npl=2)
/ \
D E (npl=1)
/ \
F G (npl=0)
计算过程:
G: 左右均为 null → npl(G) = 1 + min(0, 0) = 1
但叶子节点的 npl 实际为 1(下面有修正)
修正: npl 定义为到空节点的最短路径长度
F: 两个 null 子节点 → npl = 1 + min(0,0) = 1
G: 同理 → npl = 1
E: min(npl(F), npl(G)) = min(1,1) = 1 → npl = 1 + 1 = 2
D: null 子节点 → npl = 1
B: min(npl(D), npl(E)) = min(1,2) = 1 → npl = 1 + 1 = 2
C: npl = 2(假设其子树满足)
A: min(npl(B), npl(C)) = min(2,2) = 2 → npl = 3
注意: npl 可以简化理解为 "到最近的缺少子节点的位置的距离"。
一个节点如果至少有一个 null 子节点,则 npl = 1。
左偏性质保证了所有操作都沿着右路径进行,而右路径的长度最多为 O(log n),这就是左偏堆高效的原因。
节点定义
左偏堆的节点需要维护三个信息:键值(key)、零路径长度(npl),以及左右子节点指针。
C++ 节点定义
struct LeftistNode {
int key; // key value stored in node
int npl; // null path length
LeftistNode* left; // pointer to left child
LeftistNode* right; // pointer to right child
// constructor
LeftistNode(int val) : key(val), npl(1),
left(nullptr), right(nullptr) {}
};
单个节点(无子节点)的 npl 初始化为 1(因为它到 null 子节点的最短路径长度为 1),left 和 right 指针初始化为空。
C 节点定义
typedef struct LeftistNode {
int key; // key value
int npl; // null path length
struct LeftistNode* left; // left child
struct LeftistNode* right; // right child
} LeftistNode;
// create a new node with given key
LeftistNode* createNode(int val) {
LeftistNode* node = (LeftistNode*)malloc(sizeof(LeftistNode));
node->key = val;
node->npl = 1; // single node has npl = 1
node->left = NULL;
node->right = NULL;
return node;
}
C 语言版本使用 typedef 和 malloc,结构与 C++ 版本一致。
Python 节点定义
class LeftistNode:
def __init__(self, key):
self.key = key # key value
self.npl = 1 # null path length (single node)
self.left = None # left child
self.right = None # right child
Python 版本最为简洁,使用 None 表示空指针,npl 默认为 1。
Go 节点定义
// LeftistNode represents a node in the leftist heap
type LeftistNode struct {
key int // key value
npl int // null path length
left *LeftistNode // pointer to left child
right *LeftistNode // pointer to right child
}
Go 版本使用结构体定义节点,指针类型 *LeftistNode 表示子节点引用。单个节点创建时 npl 初始化为 1,left 和 right 默认为 nil。
合并操作(Merge)— 核心
合并(Merge)是左偏堆最核心的操作,插入和提取最小值都建立在合并之上。合并过程递归进行:
- 比较两个堆的根节点,取较小者作为新根
- 将新根的右子树与另一个堆递归合并
- 如果左子节点的 npl < 右子节点的 npl,交换左右子节点
- 更新根节点的 npl 值
步骤示例:合并两个左偏堆 H1 和 H2
H1: H2:
3 2
/ \ / \
5 10 4 7
/ /
8 9
Step 1: 比较根节点 3 和 2,2 更小 → 2 成为新根
Step 2: 递归合并 H2 的右子树(7) 和 H1
合并 H1(3) 和 子树(7):
比较根节点 3 和 7,3 更小 → 3 成为新根
递归合并 3 的右子树(10) 和 7
合并 10 和 7:
比较根节点 10 和 7,7 更小 → 7 成为新根
7 没有右子树,直接挂上 10
结果:
7
\
10
回到 3: 右子树变为上面的结果
3
/ \
5 7
/ \
8 10
检查左偏性质: npl(左)=1, npl(右)=1 → 满足 ✓
npl(3) = 1 + min(1, 1) = 2
Step 3: 回到根节点 2
2
/ \
4 3
/ / \
9 5 7
/ \
8 10
检查左偏性质: npl(左)=1, npl(右)=2
左 < 右,需要交换!
交换后:
2
/ \
3 4
/ \ \
5 7 9
/ \
8 10
npl(2) = 1 + min(npl(3), npl(4))
npl(3) = 2, npl(4) = 1
npl(2) = 1 + 1 = 2
最终结果: 一棵合法的左偏堆,根为 2。
C++ 合并实现
class LeftistHeap {
private:
LeftistNode* root;
// recursively merge two heaps
LeftistNode* merge(LeftistNode* h1, LeftistNode* h2) {
// base cases
if (h1 == nullptr) return h2;
if (h2 == nullptr) return h1;
// ensure h1 has the smaller root (min-heap)
if (h1->key > h2->key)
std::swap(h1, h2);
// merge h1's right child with h2
h1->right = merge(h1->right, h2);
// enforce leftist property: left npl >= right npl
int leftNpl = h1->left ? h1->left->npl : 0;
int rightNpl = h1->right ? h1->right->npl : 0;
if (leftNpl < rightNpl)
std::swap(h1->left, h1->right);
// update npl: 1 + min(left npl, right npl)
int minChildNpl = h1->right ? h1->right->npl : 0;
h1->npl = 1 + minChildNpl;
return h1;
}
public:
LeftistHeap() : root(nullptr) {}
// merge with another heap
void merge(LeftistHeap& other) {
root = merge(root, other.root);
other.root = nullptr;
}
};
合并操作的核心是递归地沿右路径进行:先确保较小值作为根,然后递归合并右子树,最后检查并维护左偏性质。由于所有操作都在右路径上进行,而右路径长度为 O(log n),所以合并的时间复杂度为 O(log n)。
C 合并实现
typedef struct {
LeftistNode* root;
} LeftistHeap;
// recursively merge two leftist heaps
LeftistNode* leftistMerge(LeftistNode* h1, LeftistNode* h2) {
if (!h1) return h2;
if (!h2) return h1;
// ensure h1 has the smaller root
if (h1->key > h2->key) {
LeftistNode* temp = h1;
h1 = h2;
h2 = temp;
}
// recursively merge right child with h2
h1->right = leftistMerge(h1->right, h2);
// enforce leftist property
int leftNpl = h1->left ? h1->left->npl : 0;
int rightNpl = h1->right ? h1->right->npl : 0;
if (leftNpl < rightNpl) {
LeftistNode* temp = h1->left;
h1->left = h1->right;
h1->right = temp;
}
// update npl
int minChildNpl = h1->right ? h1->right->npl : 0;
h1->npl = 1 + minChildNpl;
return h1;
}
// merge two heaps, result stored in h1
void heapMerge(LeftistHeap* h1, LeftistHeap* h2) {
h1->root = leftistMerge(h1->root, h2->root);
h2->root = NULL;
}
C 语言版本使用结构体包装根指针,合并逻辑与 C++ 版本完全一致,结果存入第一个堆。
Python 合并实现
class LeftistHeap:
def __init__(self):
self.root = None # root of the heap
def _merge(self, h1, h2):
"""Recursively merge two heap roots."""
if not h1:
return h2
if not h2:
return h1
# ensure h1 has the smaller root
if h1.key > h2.key:
h1, h2 = h2, h1
# merge h1's right child with h2
h1.right = self._merge(h1.right, h2)
# enforce leftist property
left_npl = h1.left.npl if h1.left else 0
right_npl = h1.right.npl if h1.right else 0
if left_npl < right_npl:
h1.left, h1.right = h1.right, h1.left
# update npl
min_child_npl = h1.right.npl if h1.right else 0
h1.npl = 1 + min_child_npl
return h1
def merge(self, other):
"""Merge another heap into this one."""
self.root = self._merge(self.root, other.root)
other.root = None
Python 版本逻辑最为清晰,三个版本的核心算法完全一致:比较根节点、递归合并右子树、维护左偏性质、更新 npl。
Go 合并实现
// LeftistHeap represents a leftist heap
type LeftistHeap struct {
root *LeftistNode
}
// mergeNodes recursively merges two heap roots
func mergeNodes(h1, h2 *LeftistNode) *LeftistNode {
// base cases
if h1 == nil {
return h2
}
if h2 == nil {
return h1
}
// ensure h1 has smaller root (min-heap)
if h1.key > h2.key {
h1, h2 = h2, h1
}
// merge h1's right subtree with h2
h1.right = mergeNodes(h1.right, h2)
// enforce leftist property: left npl >= right npl
leftNpl := 0
if h1.left != nil {
leftNpl = h1.left.npl
}
rightNpl := 0
if h1.right != nil {
rightNpl = h1.right.npl
}
if leftNpl < rightNpl {
h1.left, h1.right = h1.right, h1.left
}
// update npl: 1 + min(left npl, right npl)
minChildNpl := 0
if h1.right != nil {
minChildNpl = h1.right.npl
}
h1.npl = 1 + minChildNpl
return h1
}
Go 版本的合并逻辑与其他语言完全一致:确保较小根为新根,递归合并右子树,维护左偏性质并更新 npl。Go 的多重赋值 h1, h2 = h2, h1 使交换操作简洁明了。
插入操作
插入(Insert)操作非常简单:创建一个只含单个节点的左偏堆,然后将其与现有堆合并。
插入示例:向堆中插入 6
原堆: 单节点堆:
3 6
/ \
5 10
/
8
合并两堆(根为 3 和 6):
3 < 6, 3 成为新根
递归合并 3 的右子树(10) 和 6
6 < 10, 6 成为新根
6 无右子树,直接挂上 10
6
\
10
回到 3:
3
/ \
5 6
/ \
8 10
检查: npl(左)=1 >= npl(右)=1 ✓
npl(3) = 2
结果: 插入后仍然是合法的左偏堆。
单次插入的时间复杂度为 O(log n)。
C++ 插入实现
void insert(int key) {
LeftistHeap single;
single.root = new LeftistNode(key);
merge(single);
}
创建一个只含一个节点的临时堆,调用 merge 完成插入。
C 插入实现
void leftistInsert(LeftistHeap* h, int key) {
LeftistHeap single;
single.root = createNode(key);
heapMerge(h, &single);
}
C 语言版本同样创建临时堆后合并。
Python 插入实现
def insert(self, key):
"""Insert a key by creating a single-node heap and merging."""
single = LeftistHeap()
single.root = LeftistNode(key)
self.merge(single)
三个版本的插入操作都是同一个思路:构造单节点堆再合并。
Go 插入实现
// Insert adds a key to the heap by creating a single-node heap and merging
func (h *LeftistHeap) Insert(key int) {
node := &LeftistNode{key: key, npl: 1}
h.root = mergeNodes(h.root, node)
}
Go 版本使用 &LeftistNode{} 创建单节点,然后调用 mergeNodes 完成插入。与其他版本思路完全一致。
提取最小值(Extract Min)
提取最小值(Extract Min)操作分两步:
- 保存根节点的键值
- 将根节点的左右子树合并,作为新的堆
由于根节点始终是堆中的最小值(最小堆性质),提取最小值只需移除根节点,然后合并其两棵子树即可。
提取最小值示例:
原堆:
2
/ \
3 4
/ \ \
5 7 9
/
8
Step 1: 保存最小值 2
Step 2: 移除根节点,得到两个子堆
左子堆: 右子堆:
3 4
/ \ \
5 7 9
/
8
Step 3: 合并两个子堆
3 < 4, 3 成为新根
递归合并 3 的右子树(7) 和 4
4 < 7, 4 成为新根
递归合并 4 的右子树(9) 和 7
7 < 9, 7 成为新根,挂上 9
7
\
9
检查: npl(左)=0 >= npl(右)=1? 否!交换
7
/
9
npl(7) = 1 + min(0, 0) = 1
回到 4: 右子树变为上面的结果
4
/ \
9 7
检查: npl(左)=1 >= npl(右)=1 ✓
npl(4) = 2
回到 3:
3
/ \
5 4
/ / \
8 9 7
检查: npl(左)=1 >= npl(右)=2? 否!交换
3
/ \
4 5
/ \ \
9 7 8
npl(3) = 1 + min(2, 1) = 2
最终结果: 最小值 2 被提取,新堆根为 3。
提取最小值的时间复杂度为 O(log n),因为它只需要一次合并操作。
C++ 提取最小值实现
int extractMin() {
if (root == nullptr) throw std::runtime_error("Heap is empty");
int minKey = root->key;
LeftistNode* oldRoot = root;
// merge left and right subtrees
LeftistHeap leftHeap, rightHeap;
leftHeap.root = root->left;
rightHeap.root = root->right;
merge(leftHeap);
merge(rightHeap);
delete oldRoot;
return minKey;
}
提取最小值的核心步骤就是合并左右子树。代码创建了两个临时堆分别持有左右子树,然后依次合并。
C 提取最小值实现
int leftistExtractMin(LeftistHeap* h) {
if (!h->root) return -1; // error: empty heap
int minKey = h->root->key;
LeftistNode* oldRoot = h->root;
// merge left and right subtrees
LeftistHeap leftHeap, rightHeap;
leftHeap.root = oldRoot->left;
rightHeap.root = oldRoot->right;
h->root = leftistMerge(leftHeap.root, rightHeap.root);
free(oldRoot);
return minKey;
}
C 语言版本直接调用 leftistMerge 合并左右子树,更为直接。
Python 提取最小值实现
def extract_min(self):
"""Remove and return the minimum key."""
if not self.root:
return None
min_key = self.root.key
old_root = self.root
# merge left and right subtrees
left_heap = LeftistHeap()
right_heap = LeftistHeap()
left_heap.root = old_root.left
right_heap.root = old_root.right
self.root = self._merge(left_heap.root, right_heap.root)
return min_key
Python 版本同样合并左右子树,三个版本的逻辑完全一致。
Go 提取最小值实现
// ExtractMin removes and returns the minimum key
func (h *LeftistHeap) ExtractMin() (int, bool) {
if h.root == nil {
return 0, false // empty heap
}
minKey := h.root.key
oldRoot := h.root
// merge left and right subtrees
h.root = mergeNodes(oldRoot.left, oldRoot.right)
return minKey, true
}
Go 版本使用多返回值 (int, bool) 表示提取结果,第二个返回值指示堆是否为空。提取最小值的核心步骤同样是合并左右子树。
完整实现
下面给出 C++、C、Python 三种语言的完整左偏堆实现,包含插入、查找最小值、提取最小值等所有操作。
C++ 完整实现
#include <iostream>
#include <algorithm>
#include <stdexcept>
struct LeftistNode {
int key;
int npl;
LeftistNode* left;
LeftistNode* right;
LeftistNode(int val) : key(val), npl(1),
left(nullptr), right(nullptr) {}
};
class LeftistHeap {
private:
LeftistNode* root;
LeftistNode* mergeNodes(LeftistNode* h1, LeftistNode* h2) {
// base cases
if (!h1) return h2;
if (!h2) return h1;
// ensure h1 has smaller root (min-heap)
if (h1->key > h2->key)
std::swap(h1, h2);
// merge h1's right subtree with h2
h1->right = mergeNodes(h1->right, h2);
// enforce leftist property: swap if needed
int leftNpl = h1->left ? h1->left->npl : 0;
int rightNpl = h1->right ? h1->right->npl : 0;
if (leftNpl < rightNpl)
std::swap(h1->left, h1->right);
// update npl
h1->npl = 1 + (h1->right ? h1->right->npl : 0);
return h1;
}
void printTree(LeftistNode* node, int level = 0) {
if (!node) return;
for (int i = 0; i < level; i++) std::cout << " ";
std::cout << node->key << " (npl=" << node->npl << ")" << std::endl;
printTree(node->left, level + 1);
printTree(node->right, level + 1);
}
public:
LeftistHeap() : root(nullptr) {}
void insert(int key) {
LeftistNode* node = new LeftistNode(key);
root = mergeNodes(root, node);
}
int findMin() {
if (!root) throw std::runtime_error("Heap is empty");
return root->key;
}
int extractMin() {
if (!root) throw std::runtime_error("Heap is empty");
int minKey = root->key;
LeftistNode* oldRoot = root;
root = mergeNodes(root->left, root->right);
delete oldRoot;
return minKey;
}
bool isEmpty() { return root == nullptr; }
void print() {
if (!root) {
std::cout << "(empty)" << std::endl;
return;
}
std::cout << "Leftist Heap:" << std::endl;
printTree(root);
}
};
int main() {
LeftistHeap heap;
// insert values
std::cout << "Inserting: 3, 10, 5, 15, 7, 20, 1" << std::endl;
int values[] = {3, 10, 5, 15, 7, 20, 1};
for (int v : values) {
heap.insert(v);
std::cout << " Inserted " << v << std::endl;
}
// find min
std::cout << "\nMinimum: " << heap.findMin() << std::endl;
// extract min multiple times
std::cout << "\nExtract min: " << heap.extractMin() << std::endl;
std::cout << "New minimum: " << heap.findMin() << std::endl;
std::cout << "\nExtract min: " << heap.extractMin() << std::endl;
std::cout << "New minimum: " << heap.findMin() << std::endl;
// extract all remaining
std::cout << "\nExtracting all remaining:" << std::endl;
while (!heap.isEmpty()) {
std::cout << " Extracted: " << heap.extractMin() << std::endl;
}
return 0;
}
运行该程序将输出:
Inserting: 3, 10, 5, 15, 7, 20, 1
Inserted 3
Inserted 10
Inserted 5
Inserted 15
Inserted 7
Inserted 20
Inserted 1
Minimum: 1
Extract min: 1
New minimum: 3
Extract min: 3
New minimum: 5
Extracting all remaining:
Extracted: 5
Extracted: 7
Extracted: 10
Extracted: 15
Extracted: 20
可以看到,插入 7 个元素后,提取最小值操作始终返回堆中的最小元素。所有值按升序被依次提取出来,验证了左偏堆的正确性。
C 完整实现
#include <stdio.h>
#include <stdlib.h>
typedef struct LeftistNode {
int key;
int npl;
struct LeftistNode* left;
struct LeftistNode* right;
} LeftistNode;
typedef struct {
LeftistNode* root;
} LeftistHeap;
// create a new node
LeftistNode* createNode(int key) {
LeftistNode* node = (LeftistNode*)malloc(sizeof(LeftistNode));
node->key = key;
node->npl = 1;
node->left = NULL;
node->right = NULL;
return node;
}
// recursively merge two heaps
LeftistNode* mergeNodes(LeftistNode* h1, LeftistNode* h2) {
if (!h1) return h2;
if (!h2) return h1;
// ensure h1 has smaller root
if (h1->key > h2->key) {
LeftistNode* temp = h1;
h1 = h2;
h2 = temp;
}
// merge right subtree with h2
h1->right = mergeNodes(h1->right, h2);
// enforce leftist property
int leftNpl = h1->left ? h1->left->npl : 0;
int rightNpl = h1->right ? h1->right->npl : 0;
if (leftNpl < rightNpl) {
LeftistNode* temp = h1->left;
h1->left = h1->right;
h1->right = temp;
}
// update npl
h1->npl = 1 + (h1->right ? h1->right->npl : 0);
return h1;
}
// insert a key
void leftistInsert(LeftistHeap* h, int key) {
LeftistNode* node = createNode(key);
h->root = mergeNodes(h->root, node);
}
// find minimum key
int findMin(LeftistHeap* h) {
if (!h->root) return -1;
return h->root->key;
}
// extract minimum key
int extractMin(LeftistHeap* h) {
if (!h->root) return -1;
int minKey = h->root->key;
LeftistNode* oldRoot = h->root;
h->root = mergeNodes(oldRoot->left, oldRoot->right);
free(oldRoot);
return minKey;
}
// check if empty
int isEmpty(LeftistHeap* h) {
return h->root == NULL;
}
int main() {
LeftistHeap heap = {NULL};
printf("Inserting: 3, 10, 5, 15, 7, 20, 1\n");
int values[] = {3, 10, 5, 15, 7, 20, 1};
for (int i = 0; i < 7; i++) {
leftistInsert(&heap, values[i]);
printf(" Inserted %d\n", values[i]);
}
printf("\nMinimum: %d\n", findMin(&heap));
printf("\nExtract min: %d\n", extractMin(&heap));
printf("New minimum: %d\n", findMin(&heap));
printf("\nExtract min: %d\n", extractMin(&heap));
printf("New minimum: %d\n", findMin(&heap));
printf("\nExtracting all remaining:\n");
while (!isEmpty(&heap)) {
printf(" Extracted: %d\n", extractMin(&heap));
}
return 0;
}
运行该程序将输出:
Inserting: 3, 10, 5, 15, 7, 20, 1
Inserted 3
Inserted 10
Inserted 5
Inserted 15
Inserted 7
Inserted 20
Inserted 1
Minimum: 1
Extract min: 1
New minimum: 3
Extract min: 3
New minimum: 5
Extracting all remaining:
Extracted: 5
Extracted: 7
Extracted: 10
Extracted: 15
Extracted: 20
C 语言版本使用结构体和函数指针实现,逻辑与 C++ 版本一致,需要手动管理内存(malloc / free)。
Python 完整实现
class LeftistNode:
def __init__(self, key):
self.key = key
self.npl = 1
self.left = None
self.right = None
class LeftistHeap:
def __init__(self):
self.root = None
def _merge(self, h1, h2):
"""Recursively merge two heap roots."""
if not h1:
return h2
if not h2:
return h1
# ensure h1 has smaller root
if h1.key > h2.key:
h1, h2 = h2, h1
# merge h1's right subtree with h2
h1.right = self._merge(h1.right, h2)
# enforce leftist property
left_npl = h1.left.npl if h1.left else 0
right_npl = h1.right.npl if h1.right else 0
if left_npl < right_npl:
h1.left, h1.right = h1.right, h1.left
# update npl
h1.npl = 1 + (h1.right.npl if h1.right else 0)
return h1
def insert(self, key):
"""Insert a key into the heap."""
node = LeftistNode(key)
self.root = self._merge(self.root, node)
def find_min(self):
"""Return the minimum key without removing it."""
if not self.root:
return None
return self.root.key
def extract_min(self):
"""Remove and return the minimum key."""
if not self.root:
return None
min_key = self.root.key
old_root = self.root
self.root = self._merge(old_root.left, old_root.right)
return min_key
def is_empty(self):
"""Check if heap is empty."""
return self.root is None
if __name__ == "__main__":
heap = LeftistHeap()
print("Inserting: 3, 10, 5, 15, 7, 20, 1")
for v in [3, 10, 5, 15, 7, 20, 1]:
heap.insert(v)
print(f" Inserted {v}")
print(f"\nMinimum: {heap.find_min()}")
print(f"\nExtract min: {heap.extract_min()}")
print(f"New minimum: {heap.find_min()}")
print(f"\nExtract min: {heap.extract_min()}")
print(f"New minimum: {heap.find_min()}")
print("\nExtracting all remaining:")
while not heap.is_empty():
print(f" Extracted: {heap.extract_min()}")
运行该程序将输出:
Inserting: 3, 10, 5, 15, 7, 20, 1
Inserted 3
Inserted 10
Inserted 5
Inserted 15
Inserted 7
Inserted 20
Inserted 1
Minimum: 1
Extract min: 1
New minimum: 3
Extract min: 3
New minimum: 5
Extracting all remaining:
Extracted: 5
Extracted: 7
Extracted: 10
Extracted: 15
Extracted: 20
Python 版本最为简洁直观,使用 None 表示空指针,无需手动内存管理。三个版本的输出结果完全一致,验证了算法实现的正确性。
Go 完整实现
package main
import "fmt"
// LeftistNode represents a node in the leftist heap
type LeftistNode struct {
key int
npl int
left *LeftistNode
right *LeftistNode
}
// LeftistHeap represents a leftist heap
type LeftistHeap struct {
root *LeftistNode
}
// mergeNodes recursively merges two heap roots
func mergeNodes(h1, h2 *LeftistNode) *LeftistNode {
// base cases
if h1 == nil {
return h2
}
if h2 == nil {
return h1
}
// ensure h1 has smaller root (min-heap)
if h1.key > h2.key {
h1, h2 = h2, h1
}
// merge h1's right subtree with h2
h1.right = mergeNodes(h1.right, h2)
// enforce leftist property: swap if needed
leftNpl := 0
if h1.left != nil {
leftNpl = h1.left.npl
}
rightNpl := 0
if h1.right != nil {
rightNpl = h1.right.npl
}
if leftNpl < rightNpl {
h1.left, h1.right = h1.right, h1.left
}
// update npl
minChildNpl := 0
if h1.right != nil {
minChildNpl = h1.right.npl
}
h1.npl = 1 + minChildNpl
return h1
}
// Insert adds a key to the heap
func (h *LeftistHeap) Insert(key int) {
node := &LeftistNode{key: key, npl: 1}
h.root = mergeNodes(h.root, node)
}
// FindMin returns the minimum key without removing it
func (h *LeftistHeap) FindMin() (int, bool) {
if h.root == nil {
return 0, false
}
return h.root.key, true
}
// ExtractMin removes and returns the minimum key
func (h *LeftistHeap) ExtractMin() (int, bool) {
if h.root == nil {
return 0, false
}
minKey := h.root.key
oldRoot := h.root
h.root = mergeNodes(oldRoot.left, oldRoot.right)
return minKey, true
}
// IsEmpty checks if the heap is empty
func (h *LeftistHeap) IsEmpty() bool {
return h.root == nil
}
func main() {
heap := &LeftistHeap{}
// insert values
fmt.Println("Inserting: 3, 10, 5, 15, 7, 20, 1")
values := []int{3, 10, 5, 15, 7, 20, 1}
for _, v := range values {
heap.Insert(v)
fmt.Printf(" Inserted %d\n", v)
}
// find min
if min, ok := heap.FindMin(); ok {
fmt.Printf("\nMinimum: %d\n", min)
}
// extract min multiple times
if min, ok := heap.ExtractMin(); ok {
fmt.Printf("\nExtract min: %d\n", min)
}
if min, ok := heap.FindMin(); ok {
fmt.Printf("New minimum: %d\n", min)
}
if min, ok := heap.ExtractMin(); ok {
fmt.Printf("\nExtract min: %d\n", min)
}
if min, ok := heap.FindMin(); ok {
fmt.Printf("New minimum: %d\n", min)
}
// extract all remaining
fmt.Println("\nExtracting all remaining:")
for !heap.IsEmpty() {
if min, ok := heap.ExtractMin(); ok {
fmt.Printf(" Extracted: %d\n", min)
}
}
}
运行该程序将输出:
Inserting: 3, 10, 5, 15, 7, 20, 1
Inserted 3
Inserted 10
Inserted 5
Inserted 15
Inserted 7
Inserted 20
Inserted 1
Minimum: 1
Extract min: 1
New minimum: 3
Extract min: 3
New minimum: 5
Extracting all remaining:
Extracted: 5
Extracted: 7
Extracted: 10
Extracted: 15
Extracted: 20
Go 版本使用结构体和指针实现,通过多返回值 (int, bool) 处理空堆的情况。Go 的多重赋值 h1, h2 = h2, h1 使交换操作同样简洁。输出结果与其他三个版本完全一致。
左偏堆的性质
时间复杂度
| 操作 | 左偏堆 | 二叉堆 |
|---|---|---|
| 合并(Merge) | O(log n) | O(n) |
| 插入(Insert) | O(log n) | O(log n) |
| 查找最小值(Find Min) | O(1) | O(1) |
| 提取最小值(Extract Min) | O(log n) | O(log n) |
| 删除(Delete) | O(log n) | O(log n) |
| 降低键值(Decrease Key) | O(log n) | O(log n) |
与二叉堆的对比
左偏堆最大的优势在于合并操作:二叉堆的合并需要 O(n) 时间(需要重建堆),而左偏堆只需 O(log n)。这使得左偏堆在以下场景中更有优势:
- 需要频繁合并的优先队列:例如多路归并排序、图算法中的多源最短路径
- 无法预先确定元素总数的场景:左偏堆不需要预先分配固定大小的数组
- 需要支持所有优先队列操作的可合并堆:左偏堆是实现可合并优先队列的最简单数据结构之一
二叉堆的优势:
- 实现简单,常数因子小
- 可以用数组存储,缓存友好
- 对于不需要合并操作的场景,二叉堆通常是更好的选择
核心性质
- 右路径长度:一棵有 n 个节点的左偏堆,其右路径长度最多为 floor(log2(n+1))。这是左偏堆所有操作高效的根本保证。
- 递归结构:左偏堆的所有操作(合并、插入、提取)都沿着右路径进行,递归深度为 O(log n)。
- 实现简洁:相比二项式队列(Binomial Queue)和斐波那契堆(Fibonacci Heap),左偏堆的实现最为简单,是最容易理解的可合并堆。
空间复杂度
左偏堆的空间复杂度为 O(n),每个节点额外需要两个指针(left、right)和一个 npl 字段。相比二叉堆的数组存储,左偏堆有额外的指针开销,但换来了高效的合并操作。
应用场景
- 可合并优先队列(Mergeable Priority Queue):左偏堆的核心应用,支持高效的堆合并
- 图算法:在 Dijkstra、Prim 等算法中,当需要处理多个优先队列的合并时,左偏堆比二叉堆更高效
- 多路归并排序:多个已排序的子序列可以用左偏堆高效管理
- 事件模拟:多个事件队列合并为一个全局队列
- 教学用途:左偏堆是理解可合并堆概念的最佳入门数据结构,比二项式队列和斐波那契堆更简单直观

浙公网安备 33010602011771号