# 左高树 Height-biased Leftist Tree 合并思路 合并两个左高树(Height-biased Leftist Tree, HBLT)是其核心操作,利用递归和左右子树的`s(x)`(通常指null path length,NPL)来保持“左高性质”和堆序。下面给你完整详细步骤和示例代码。 --- ## 左高树合并的基本思路 假设有两棵左高树的根节点指针 `h1` 和 `h2`,要合并它们,得到合并后的左高树根节点。 ### 主要步骤 1. **处理空树情况** * 如果 `h1` 是空,直接返回 `h2`。 * 如果 `h2` 是空,直接返回 `h1`。 2. **保证根节点的键值满足最大堆性质** * 如果是最大左高树(Max-Heap),令 `h1` 的键值大于等于 `h2`。 * 若 `h1->key < h2->key`,交换 `h1` 和 `h2`。 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`。 --- ## 伪代码 ```cpp template struct binaryTreeNode { std::pair data; // 键值和负载 binaryTreeNode *left; binaryTreeNode *right; int s; // null path length (NPL) }; template binaryTreeNode>* merge(binaryTreeNode>* h1, binaryTreeNode>* 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; } ``` --- ## 说明 * `s`(null path length)定义为从该节点到其最近的空外部节点(外部节点视为 `nullptr`)的路径长度。 * 由于左高树特性,左子树的 `s` 不小于右子树的 `s`,合并后通过交换左右子树保证这个性质。 * 这个合并过程保证了合并操作时间复杂度为 O(log n)。 --- # 左高树合并 — 逐步图解(示例:根 50 与 根 45) 下面是你给出的两个左高树(Max-Leftist)的逐步合并图解。我把每一步的递归调用、比较、左右交换和 s 值(null path length,定义为:s(nullptr)=0,s(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)`。 --- ## 递归 1:`merge(30, 45)` * 比较根:30 < 45 → 交换(让数值大的作为子树根),所以在实现上会把参数交换,使 `h1` 指向 45,`h2` 指向 30。 * 结果:以 45 为当前子树根,继续将 `h2(30)` 合并到 `45->right`: 递归调用:`merge(45,30)` → `45->right = merge(25, 30)`(因为 45 的右子树原为 25)。 当前(在这一层)状态: ``` (暂时) 45 / \ 35 25 (将与 30 合并) ``` --- ## 递归 2:`merge(25, 30)` * 比较根:25 < 30 → 交换 → 以 30 为根,25 为待合并树。 * 执行 `30->right = merge(30->right, 25)`。但原来 30 没有孩子,所以 `30->right` 是 `nullptr`。 * 所以调用变成 `merge(nullptr, 25)` → 直接返回 25。 所以在这一步,临时构造出: ``` 30 \ 25 ``` 接下来要维护左高性质(保证 s(left) >= s(right)): * 计算 s 值:原来 30 的左子为空(left\_s = 0),右子为 25(right\_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、5;30 下保留 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))来保证。 --- ## 伪代码回顾(便于对应每一步) ```cpp 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; } ``` ---