https://labuladong.online/algo/data-structure-basic/binary-heap-implement/#代码实现
今天学二叉堆的时候,让gemini给了一份代码,发现它去实现了一个getPathToParent函数,可以很方便的找到插入节点的父节点。
// getPathToParent 根据节点索引n(1-based)的二进制表示,计算从根到其父节点的路径。
func getPathToParent(n int) string {
if n <= 1 {
return ""
}
// 获取n的二进制字符串,并去掉代表根的最高位'1'和代表节点本身的最低位。
binaryStr := strconv.FormatInt(int64(n), 2)
return binaryStr[1 : len(binaryStr)-1]
}
让我们一步步分解它的工作原理。
1. 问题:我们想做什么?
在一个完全二叉堆中,新元素总是被添加到树的最后一层的、从左到右的第一个可用位置。
想象一下我们已经有5个节点的堆:
(1)
/ \
(2) (3)
/ \
(4) (5)
现在我们要插入第 6 个节点。它应该放在哪里?它应该成为节点 (3) 的左子节点。
问题是,在我们的代码中,我们如何从根节点 (1)
出发,准确地找到节点 (3)
,从而可以执行 parent.Left = newNode
的操作?这就是 getPathToParent
的任务。
2. 核心思想:节点位置和二进制路径的映射
一个完全二叉树的结构和二进制数有着奇妙的对应关系。如果我们把节点的**位置(1-based 索引)**写成二进制,这个二进制数就隐藏了从根节点到达该节点的路径。
规则如下:
- 把节点位置
n
写成二进制。 - 忽略最高位的
1
(因为它代表根节点本身)。 - 剩下的二进制位就是路径:
0
代表向左走,1
代表向右走。
让我们用上面的树来验证一下:
节点位置 (n) | 二进制 | 忽略最高位’1’后的路径 | 实际路径 (从根节点出发) |
---|---|---|---|
1 | 1 |
(空) | (就是根节点) |
2 | 10 |
0 |
向左 |
3 | 11 |
1 |
向右 |
4 | 100 |
00 |
向左,再向左 |
5 | 101 |
01 |
向左,再向右 |
6 | 110 |
10 |
向右,再向左 |
7 | 111 |
11 |
向右,再向右 |
这个规律完美成立!
3. 从“节点路径”到“父节点路径”
我们已经知道如何找到去往目标节点 n
的路径了。但是,我们需要的是去往它父节点的路径。
这很简单:父节点的路径,就是目标节点路径去掉最后一步。
- 要到达节点 6,路径是
向右,再向左
(二进制10
)。 - 那么要到达它的父节点(节点 3),路径就是
向右
(二进制1
)。
4. 代码如何实现这个逻辑
现在我们再来看这几行代码,就清晰了:
// 例子: 我们要插入第 6 个节点, 所以 n = 6
func getPathToParent(n int) string {
// n = 6
if n <= 1 { // 如果是插入第一个节点,它没有父节点
return ""
}
// 1. 将 n 转换为二进制字符串
// strconv.FormatInt(6, 2) --> "110"
binaryStr := strconv.FormatInt(int64(n), 2)
// 2. 提取代表父节点路径的子字符串
// binaryStr 是 "110", len(binaryStr) 是 3
// 我们要的切片是 [1 : len-1],即 [1:2]
// string[1:2] 提取的是索引为 1 的字符(不包括索引2)
// "110"
// ^ (索引0, 最高位 '1', 忽略)
// ^ (索引1, 我们需要的部分)
// ^ (索引2, 最后一步, 丢弃)
//
// 所以 binaryStr[1:2] 的结果是 "1"
return binaryStr[1 : len(binaryStr)-1]
}
总结一下 binaryStr[1 : len(binaryStr)-1]
这行代码的含义:
[1:]
的部分:跳过最高位的1
,因为路径是从根节点的子节点开始计算的。[:len(binaryStr)-1]
的部分:忽略最后一个字符,因为我们想在到达目标节点的前一步停下来,也就是停在父节点上。
所以,当我们要插入第 6
个节点时,getPathToParent(6)
返回字符串 "1"
。在 Insert
函数中,循环就会根据这个 "1"
从根节点向右走一步,准确地找到节点 (3)
作为新节点的父节点。