This commit is contained in:
e2hang
2025-08-10 18:13:25 +08:00
parent 17105e9e9e
commit a2cbb2ad7d
6 changed files with 386 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
# 左高树合并 — 逐步图解(示例:根 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)`。
---
## 递归 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右子为 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))来保证。
---
## 伪代码回顾(便于对应每一步)
```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;
}
```
---