【发布时间】:2011-02-10 04:06:03
【问题描述】:
我们必须将二叉树的节点写入文件。编写二叉树最节省空间的方法是什么。我们可以将其以数组格式存储,父对象位于i,其子对象位于2i、2i+1。但这会在稀疏二叉树的情况下浪费大量空间。
【问题讨论】:
标签: arrays algorithm data-structures binary-tree
我们必须将二叉树的节点写入文件。编写二叉树最节省空间的方法是什么。我们可以将其以数组格式存储,父对象位于i,其子对象位于2i、2i+1。但这会在稀疏二叉树的情况下浪费大量空间。
【问题讨论】:
标签: arrays algorithm data-structures binary-tree
我喜欢的一种方法是存储前序遍历,但也包括其中的“空”节点。存储“空”节点无需同时存储树的顺序。
这种方法的一些优点
例如,假设您有一个 64 位整数的二叉树,您可以在每个节点之后存储一个额外的位,说明下一个节点是否为空节点(第一个节点始终是根节点)。空节点,可以用一个位表示。
因此,如果有 n 个节点,则空间使用量为 8n 字节 + n-1 指示位 + 空节点的 n+1 位 = 66*n 位。
在前/后 + 顺序中,您最终将使用 16n 字节= 128*n 位。
因此,您在这种 pre/post + inorder 方法上节省了 62*n 位的空间。
考虑树
100
/ \
/ \
/ \
10 200
/ \ / \
. . 150 300
/ \ / \
. . . .
'.'是空节点。
您将其序列化为100 10 . . 200 150 . . 300 . .
现在每个(包括子树)'preorder traversal with null'都具有空节点数 = 节点数 + 1 的属性。
这允许您创建树,给定一次传递的序列化版本,因为第一个节点是树的根。后面的节点是左子树,右子树,可以这样看:
100 (10 . .) (200 (150 . .) (300 . .))
要创建中序遍历,您可以使用堆栈并在看到节点时推送并在看到空值时弹出(到列表中)。结果列表是中序遍历(对此的详细解释可以在这里找到:C++/C/Java: Anagrams - from original string to target;)。
【讨论】:
想想 XML。这是一种树序列化。例如:
<node id="1">
<node id="2"> 1
</node> / \
<node id="3"> 2 3
<node id="4"> / \
</node> 4 5
<node id="5">
</node>
</node>
</node>
那么,为什么是空格和标签呢?我们可以逐步省略它们:
<1>
<2></>
<3>
<4></>
<5></>
</>
</>
删除空格:<1><2></2><3><4></><5></></></>。
去掉尖括号:12/34/5///
现在的问题是:如果一个节点有一个空的左子树和非空的右子树怎么办? 然后我们可以使用另一个特殊的字符“#”来表示一个空的左子树。
例如:
1
/ \
2
/ \
3
这棵树可以序列化为:1#23///。
【讨论】:
如果您有一棵(几乎)完整的树,那么 2i、2i+1(二叉堆)方法确实是最好的方法。
否则,您将无法避免为每个节点存储 ParentId(父索引)。
【讨论】:
您可以将二叉树的in-order 和pre/post-order 遍历保存在文件中,并根据这些遍历重构树。
【讨论】:
由于只有 2ⁿ-1 项可以存储而不会浪费任何空间,因此您必须将 N 个元素拆分为子树,按顺序存储,并保留树之间值的“主索引”以决定在哪棵树中存储搜索。决定子树的一个明显方法是利用数字的二进制格式,这里我说的是N,即树中的项目数。
假设您有一个包含 20 个不同项目的排序序列:
['aeu', 'bfz', 'cdi', 'dfc', 'eap', 'ggk', 'gsb', 'guj', 'idm', 'ieg',
'izr', 'pba', 'plp', 'rap', 'rhp', 'tat', 'uho', 'uwb', 'wdf', 'yhp']
20₁₀ 是 10100₂,1 位决定分割。
您的主表中的第一个条目将是 item[15], 0 (15₁₀≡01111₂ 小于等于 N 的 2 的最大幂,并且 0是 到目前为止您的输出列表的长度,也就是 子树的起始索引)。之前的 15 (0–14) 个项目将被附加到您的输出列表中,再次利用二进制算术:item[7₁₀≡0111₂] at 0,item[3₁₀≡0011₂] at 1,item[11₁₀≡1001₂] at 2 以此类推,从而保留item[i] < item[2*i] && item[i] > item[2*i+1] 的规则。
在这第一步之后:
master_table = [('tat', 0)]
output_list = ['guj', 'dfc', 'pba', 'bfz', 'ggk', 'ieg', 'rap', 'aeu', 'cdi', 'eap', 'gsb', 'idm', 'izr', 'plp', 'rhp']
如果 N == 2ⁿ 那么这个 python 生成器会产生所需的顺序:
def heap_order(pow2):
"""Return the tree order of items
pow2 MUST be a power of two"""
bit= pow2>>1
while bit:
yield from range(bit - 1, pow2, bit*2)
bit>>= 1
删除(或忽略)列表中的前 16 项,并在列表长度的下一位重复:
剩下的输入列表是['uho', 'uwb', 'wdf', 'yhp'],它的长度是4₁₀==100₂。最大的 2ⁿ-1 是 3,因此您将从零开始的项目 3 以及输出列表的当前长度附加到您的主表中,然后将前 3 个项目附加到您的输出列表中,结果是:
master_table = [('tat', 0), ('yhp', 15)]
# master table items: (key, subtree_base_index)
output_list = [
'guj', 'dfc', 'pba', 'bfz', 'ggk',
'ieg', 'rap', 'aeu', 'cdi', 'eap',
'gsb', 'idm', 'izr', 'plp', 'rhp',
'uwb', 'uho', 'wdf'
]
整个结果相当于一棵二叉树,其中所有左子树都有任一个叶子节点或有两个子节点的树节点。
要搜索一个值:扫描主表以找到大于您的搜索键的键,并在相应的子树中搜索您的键,始终在您的 2*index、2*index-1 计算中使用 index - subtree_base_index 调整。失败搜索所需的最大比较总数(主表 + 子树)通常为 ⌈log₂N⌉,有时——当搜索键大于主表的第一个条目时——小于(不为 N 的 0 位创建子树)。
您还应该将每个子树的级别计数存储在主表中(以便您知道在搜索了多少次后应该停止搜索子树)。
【讨论】:
您可以存储级别顺序,即 DFS 输出,包括任何 null 子级。从值和空值列表重构树时,使用节点队列,并知道列表中的下两个项目始终是队列中当前节点的左侧和右侧。
这仍然需要存储总共 n+1 个“空符号”,但反序列化是迭代完成的,而不是在预排序的情况下递归完成。
JS 中的 POC:
class Node {
constructor(val, left=null, right=null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function dfs(root) {
const queue = [root];
const output = [];
while (queue.length > 0) {
const curr = queue.pop();
// if curr is null, push null, else curr.val
output.push(curr && curr.val);
if (curr !== null) {
queue.unshift(curr.left);
queue.unshift(curr.right);
}
}
return output;
}
function undfs(list) {
const root = new Node(list.shift());
const queue = [root];
while (queue.length > 0) {
const curr = queue.shift();
const [leftVal, rightVal] = list.splice(0,2);
if (leftVal) {
curr.left = new Node(leftVal);
queue.push(curr.left);
}
if (rightVal) {
curr.right = new Node(rightVal);
queue.push(curr.right);
}
}
return root;
}
const root =
new Node(100,
new Node(10),
new Node(200,
new Node(150),
new Node(300)
)
);
const dfsOutput = dfs(root);
console.log("List output:");
console.log(dfsOutput.map(a => a || "null").join(", "));
console.log("\nTree output:");
console.log(undfs(dfsOutput));
【讨论】: