6.6 KiB
左高树 Height-biased Leftist Tree 合并思路
合并两个左高树(Height-biased Leftist Tree, HBLT)是其核心操作,利用递归和左右子树的s(x)
(通常指null path length,NPL)来保持“左高性质”和堆序。下面给你完整详细步骤和示例代码。
左高树合并的基本思路
假设有两棵左高树的根节点指针 h1
和 h2
,要合并它们,得到合并后的左高树根节点。
主要步骤
-
处理空树情况
- 如果
h1
是空,直接返回h2
。 - 如果
h2
是空,直接返回h1
。
- 如果
-
保证根节点的键值满足最大堆性质
- 如果是最大左高树(Max-Heap),令
h1
的键值大于等于h2
。 - 若
h1->key < h2->key
,交换h1
和h2
。
- 如果是最大左高树(Max-Heap),令
-
递归合并
- 将
h2
合并到h1
的右子树。 h1->right = merge(h1->right, h2)
- 将
-
保持左高性质(左子树的 null path length ≥ 右子树)
- 如果
s(h1->left) < s(h1->right)
,交换左右子树指针。
- 如果
-
更新当前节点的 s 值
s(h1) = s(h1->right) + 1
-
返回新的根
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;
}
说明
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))来保证。
伪代码回顾(便于对应每一步)
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;
}