我试图以更紧凑的格式表示一棵树,着眼于嵌入式系统。
我的树是二元的并且相当平衡(最大深度~20,但是大小~50K节点)。生成它们的算法使用类似于
的节点结构class Node {
BinaryFunction BF(Input->Boolean);
[optional] Node LeftNode;
[optional] Result LeftResult;
[optional] Node RightNode;
[optional] Result RightResult;
}
其中Result
占用几位,Node存储为指针(4/8字节)。虽然LeftNode
和LeftResult
在技术上是可选的,但每个节点都包含leftNode或LeftResult,而mutatis mutandis for right。走三个输入I
包括重复评估node->BF(I)
,然后向左或向右。如果有子节点,则递归,如果没有,则返回结果。
所以,这需要节食。我已经获得了完整的树,并且不需要担心修改,所以我将它放在一个连续的内存块中。我的第一个观察是我们可以用16位索引替换Node,因为我通常节点少于65K。如果我存储深度优先表示,我只需要一个位来指示左节点是否完全存在,因为如果它存在,则左节点紧跟其父节点。在没有Result
值的情况下,该位已经隐含了。
我可以通过使用Ahnentafel来完全消除左右节点引用,但是留下了空白,并且使用我的BinaryFunction的大小,索引的节省量不足以超过所有这些间隙。
那么,是否有更紧凑的方式存储这些树?也许通过为离开和分支节点使用不同的节点类型?我怎么能分开呢?
我的目标是嵌入式系统,所以我们在这里谈论位/节点。我仍然希望得到一个合理的结果范围(5-8位)和节点数(最少16位)。我当然可以使用一个或几个标记值。 BinaryFunction可能以48位表示。
[编辑]
BinaryFunction(Input->Boolean)
应该是伪代码中的UnaryFunction(Input->Boolean)
;我简化示例时应该更新名称。
答案 0 :(得分:2)
正如您在倒数第二段中所述,您可以使用不同类型的Node
来节省空间
class FullNode {
BitArray(2) nodeType = 0;
BinaryFunction BF(Input->Boolean);
Node LeftNode;
Result LeftResult;
Node RightNode;
Result RightResult;
}
class LeftNode {
BitArray(2) nodeType = 1;
BinaryFunction BF(Input->Boolean);
Node LeftNode;
Result LeftResult;
}
class RightNode {
BitArray(2) nodeType = 2;
BinaryFunction BF(Input->Boolean);
Node RightNode;
Result RightResult;
}
class LeafNode {
BitArray(2) nodeType = 3;
BinaryFunction BF(Input->Boolean);
}
您可以使用此信息确定要使用两个位来处理的节点类型,以转换为适当的节点类型
Result LeftResult(Node node) {
if(node.nodeType == 0)
return (static_cast<FullNode>(node) -> LeftResult)
else if(node.nodeType == 1)
return (static_cast<LeftNode>(node) -> LeftResult)
else
return NULL
}
如果您能够确定节点的大小,那么您只需要一位来区分LeftNode
和RightNode
您可以进一步展开节点以消除更多指针,例如
class FullNodeLevel2 {
BinaryFunction BF(Input->Boolean);
Node LeftNode;
Result LeftResult;
Result LeftRightResult;
Result LeftLeftResult;
Node RightNode;
Result RightResult;
Result RightRightResult;
Result RightLeftResult;
}
// Level 2 node with a complete right subtree and only one left branch
class RightRightLeftNode {
BinaryFunction BF(Input->Boolean);
Node LeftNode;
Result LeftResult;
Node RightNode;
Result RightResult;
Result RightRightResult;
Result RightLeftResult;
}
等等 - 每个节点都存储树的两个级别,以更复杂的遍历代码为代价来节省一些指针空间。
答案 1 :(得分:1)
如果我理解正确,节点的逻辑结构将是:
struct node
BinaryFunction (48 bits)
union Left
LeftNode (16 bits)
LeftResult (8 bits)
union Right
RightNode (16 bits)
RightResult (8 bits)
所以每个节点都有(逻辑上至少)三个字段。有4种类型的节点:
正如你所说,你可以摆脱LeftNode
索引,因为如果有一个左节点,它将紧接在内存中的当前节点之后。
鉴于此,您的节点变为:
BinaryFunction (48 bits)
NodeType (2 bits)
union
NodeType1 { RightNode (16 bits) } // 16 bits
NodeType2 { RightResult (8 bits) } // 8 bits
NodeType3 { LeftResult (8 bits), RightNode (16 bits) } // 24 bits
NodeType4 { LeftResult (8 bits), RightResult (8 bits) } // 16 bits
因此,每个节点的大小范围为58到74位。
这两个位是令人不安的,因为它们导致结构不是字节对齐的,这意味着每个节点要么吃6位,要么必须对节点数组进行位寻址。另一种方法是从节点中删除NodeType
字段,并将它们存储在内存块开头的单独数组中。这样,您的节点都适合字节边界,每个节点提供(56,64或72)位。索引本身每个节点需要两位,但是每个字节可以打包四个,这意味着你将为整个树浪费最多6位,并且索引到节点数组仍然很容易。
或者,如果你可以将BinaryFunction
压缩成46位,那么你就可以为节点类型留出空间。
以上假设最大内存块大小为64千字节,这对我来说是一个误解。如果你需要支持64K节点,那么事情会有所不同。
您可以使用两种不同类型的节点:16位和24位。您不得不放弃左侧节点优化,但您可以为节点类型消除每个节点的两位。因此,节点类型1和3将是24位,节点类型2和4将是16位。然后,将所有16位节点存储在存储器块的前面,之后存储所有24位节点。您只需要计算16位节点的数量,以便知道24位节点的起始位置。
假设您有1,000个16位节点和1,000个24位节点。所以你的BigNodeOffset
是1,000。给定节点索引,您可以这样做:
if (nodeIndex > BigNodeOffset)
nodeOffset = 16*BigNodeOffset + (nodeIndex - BigNodeOffset)*24;
else
nodeOffset = 16*nodeIndex;
通过将所有类型1节点存储在一起,将所有类型2节点存储在一起等,避免每节点2位节点类型。并且保留四个值来说明每种类型的第一个节点的存储位置。关键是你可以根据节点在内存中的位置来确定节点的类型。
你可能能够扩展这个想法,以便在某些情况下利用左节点优化,但这样做变得相当复杂,并且可能不值得付出努力。
答案 2 :(得分:0)
Google Protobuf将整数存储为variable sized field。小整数比较大的整数占用更少的空间。
varint中的每个字节(最后一个字节除外)具有最重要的字节 bit(msb)set - 这表示还有其他字节。 每个字节的低7位用于存储二进制补码 以7位为单位表示数字,最不重要 小组第一。