Files
Data-Structure/BinaryTree/priorityQueue/maxHBLT
2025-08-10 18:18:31 +08:00
..
2025-08-10 18:13:25 +08:00
2025-08-10 18:13:25 +08:00
2025-08-10 18:13:25 +08:00
2025-08-10 18:13:25 +08:00
2025-08-10 18:18:31 +08:00
2025-08-10 18:16:51 +08:00

左高树 Height-biased Leftist Tree 合并思路

合并两个左高树Height-biased Leftist Tree, HBLT是其核心操作利用递归和左右子树的s(x)通常指null path lengthNPL来保持“左高性质”和堆序。下面给你完整详细步骤和示例代码。


左高树合并的基本思路

假设有两棵左高树的根节点指针 h1h2,要合并它们,得到合并后的左高树根节点。

主要步骤

  1. 处理空树情况

    • 如果 h1 是空,直接返回 h2
    • 如果 h2 是空,直接返回 h1
  2. 保证根节点的键值满足最大堆性质

    • 如果是最大左高树Max-Heaph1 的键值大于等于 h2
    • h1->key < h2->key,交换 h1h2
  3. 递归合并

    • h2 合并到 h1 的右子树。
    • h1->right = merge(h1->right, h2)
  4. 保持左高性质(左子树的 null path length ≥ 右子树)

    • 如果 s(h1->left) < s(h1->right),交换左右子树指针。
  5. 更新当前节点的 s 值

    • s(h1) = s(h1->right) + 1
  6. 返回新的根 h1


伪代码

template<typename T>
struct binaryTreeNode {
    std::pair<int, T> data;  // 键值和负载
    binaryTreeNode *left;
    binaryTreeNode *right;
    int s;  // null path length (NPL)
};

template<typename T>
binaryTreeNode<std::pair<int, T>>* merge(binaryTreeNode<std::pair<int, T>>* h1,
                                         binaryTreeNode<std::pair<int, T>>* h2) {
    if (!h1) return h2;
    if (!h2) return h1;

    // 保证最大堆性质,根值较大
    if (h1->data.first < h2->data.first)
        std::swap(h1, h2);

    // 递归合并 h2 到 h1 的右子树
    h1->right = merge(h1->right, h2);

    // 保持左高性质
    int leftNPL = h1->left ? h1->left->s : 0;
    int rightNPL = h1->right ? h1->right->s : 0;
    if (leftNPL < rightNPL)
        std::swap(h1->left, h1->right);

    // 更新当前节点的 s 值
    h1->s = (h1->right ? h1->right->s : 0) + 1;

    return h1;
}

说明

  • snull path length定义为从该节点到其最近的空外部节点外部节点视为 nullptr)的路径长度。
  • 由于左高树特性,左子树的 s 不小于右子树的 s,合并后通过交换左右子树保证这个性质。
  • 这个合并过程保证了合并操作时间复杂度为 O(log n)。

左高树合并 — 逐步图解(示例:根 50 与 根 45

下面是你给出的两个左高树Max-Leftist的逐步合并图解。我把每一步的递归调用、比较、左右交换和 s 值null path length定义为s(nullptr)=0s(leaf)=1节点 s = right.s + 1都写清楚了。


初始两棵树与 s 值计算

Tree A根 50

      50(s=2)
     /      \
 40(s=2)   30(s=1)
  /   \
20(s=1) 10(s=1)

计算过程:

  • s(20)=1, s(10)=1 → s(40)=right.s + 1 = 1 + 1 = 2
  • s(30)=1叶子
  • s(50)=right.s + 1 = 1 + 1 = 2

Tree B根 45

      45(s=2)
     /      \
 35(s=2)   25(s=1)
  /   \
15(s=1) 5(s=1)

计算过程:

  • s(15)=1, s(5)=1 → s(35)=1 + 1 = 2
  • s(25)=1叶子
  • s(45)=right.s + 1 = 1 + 1 = 2

合并入口:merge(50, 45)

  • 比较根50 >= 45 → 保留 50 为新根。
  • 按算法把 第二棵树45合并到 50 的右子树

递归调用: merge(50,45) → 需要计算 50->right = merge(30, 45)


递归 1merge(30, 45)

  • 比较根30 < 45 → 交换(让数值大的作为子树根),所以在实现上会把参数交换,使 h1 指向 45h2 指向 30。
  • 结果:以 45 为当前子树根,继续将 h2(30) 合并到 45->right

递归调用:merge(45,30)45->right = merge(25, 30)(因为 45 的右子树原为 25

当前(在这一层)状态:

   (暂时) 45
   /   \
 35    25
       (将与 30 合并)

递归 2merge(25, 30)

  • 比较根25 < 30 → 交换 → 以 30 为根25 为待合并树。
  • 执行 30->right = merge(30->right, 25)。但原来 30 没有孩子,所以 30->rightnullptr
  • 所以调用变成 merge(nullptr, 25) → 直接返回 25。

所以在这一步,临时构造出:

  30
   \
   25

接下来要维护左高性质(保证 s(left) >= s(right)

  • 计算 s 值:原来 30 的左子为空left_s = 0右子为 25right_s = 1
  • 因为 left_s < right_s所以交换左右子树,得到:
   30
  /
25
  • 更新 s(30) = (right ? right.s : 0) + 1 = (nullptr ? 0 : ) + 1 = 0 + 1 = 1

所以 merge(25,30) 返回的子树为:

  30(s=1)
  /
25(s=1)

回到上层(恢复到 45 的层)

  • 把返回的子树作为 45->right:现在 45 的左右子树为
   45
  /  \
35(s=2) 30(s=1)
       /
     25(s=1)
  • 检查并维持左高性质:

    • left_s = s(35) = 2
    • right_s = s(30) = 1
    • left_s >= right_s不必交换
  • 更新 s(45) = right.s + 1 = 1 + 1 = 2保持不变

merge(30,45) 到此结束,返回的子树根为 45其 s=2


回到最顶层(恢复到 50 的层)

  • 将上一步得到的子树赋为 50->right,现在 50 的结构为:
        50
       /  \
  40(s=2) 45(s=2)
  /  \   /  \
20 10 35  30
        /\   /
      15 5  25

为清楚起见35 下保留 15、530 下保留 25

  • 检查并维持左高性质:

    • left_s = s(40) = 2
    • right_s = s(45) = 2
    • left_s >= right_s等于也符合所以不交换
  • 更新 s(50) = right.s + 1 = 2 + 1 = 3。


最终合并结果(图与 s 值)

         50(s=3)
        /      \
   40(s=2)   45(s=2)
   /   \     /   \
20(1)10(1)35(2) 30(1)
          /\     /
       15(1)5(1)25(1)

说明:

  • 合并过程中确实发生了你关心的“原来右子树的位置30被另一棵树的节点45/35取代”的情况这很正常合并算法优先保证堆序(根值最大),于是较大的根会在递归中“上移”到合适位置。
  • 同时,算法通过「递归合并到右子树」+「必要时交换左右子树」来保持左高性质。最终时间复杂度按树的右路径长度上界(均摊 O(log n))来保证。

伪代码回顾(便于对应每一步)

Node* merge(Node* h1, Node* h2) {
  if (!h1) return h2;
  if (!h2) return h1;
  if (h1->key < h2->key) swap(h1, h2); // 确保 h1 的根更大Max-heap
  h1->right = merge(h1->right, h2);
  // 保持左高:如果左子 s < 右子 s则交换
  if (npl(h1->left) < npl(h1->right)) swap(h1->left, h1->right);
  h1->s = npl(h1->right) + 1;
  return h1;
}