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),leftright 指针初始化为空。

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 语言版本使用 typedefmalloc,结构与 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,leftright 默认为 nil


合并操作(Merge)— 核心

合并(Merge)是左偏堆最核心的操作,插入和提取最小值都建立在合并之上。合并过程递归进行:

  1. 比较两个堆的根节点,取较小者作为新根
  2. 将新根的右子树与另一个堆递归合并
  3. 如果左子节点的 npl < 右子节点的 npl,交换左右子节点
  4. 更新根节点的 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)操作分两步:

  1. 保存根节点的键值
  2. 将根节点的左右子树合并,作为新的堆

由于根节点始终是堆中的最小值(最小堆性质),提取最小值只需移除根节点,然后合并其两棵子树即可。

提取最小值示例:

原堆:
        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 等算法中,当需要处理多个优先队列的合并时,左偏堆比二叉堆更高效
  • 多路归并排序:多个已排序的子序列可以用左偏堆高效管理
  • 事件模拟:多个事件队列合并为一个全局队列
  • 教学用途:左偏堆是理解可合并堆概念的最佳入门数据结构,比二项式队列和斐波那契堆更简单直观
posted @ 2026-04-17 02:19  游翔  阅读(12)  评论(0)    收藏  举报