Data structures 树的紧表示

Data structures 树的紧表示,data-structures,tree,Data Structures,Tree,我试图用更紧凑的格式表示一棵树,着眼于嵌入式系统 我的树是二进制的,并且相当平衡,最大深度约为20,但大小约为50K个节点。生成它们的算法使用类似于的节点结构 class Node { BinaryFunction BF(Input->Boolean); [optional] Node LeftNode; [optional] Result LeftResult; [optional] Node RightNode; [optional] Result Rig

我试图用更紧凑的格式表示一棵树,着眼于嵌入式系统

我的树是二进制的,并且相当平衡,最大深度约为20,但大小约为50K个节点。生成它们的算法使用类似于的节点结构

class Node {
   BinaryFunction BF(Input->Boolean);
   [optional] Node LeftNode;
   [optional] Result LeftResult;
   [optional] Node RightNode;
   [optional] Result RightResult;
}
其中,结果取数位,节点作为指针存储为4/8字节。虽然LeftNode和LeftResult在技术上是可选的,但每个节点都包含一个LeftNode或一个LeftResult,对right进行必要的修改。为输入I遍历三个节点包括重复计算node->BFI,然后向左或向右。如果有子节点,则递归,如果没有,则返回结果

所以,这需要节食。我有完整的树可用,不需要担心修改,所以我将把它放在一个连续的内存块中。我的第一个观察是,我们可以用16位索引替换节点,因为我的节点通常少于65K。如果我存储深度优先表示,我只需要一个位来指示左节点是否存在,因为如果存在左节点,则左节点将立即跟随其父节点。在没有结果值的情况下,该位已经是隐式的

我可以通过使用一个函数来完全消除左右节点引用,但这会留下一些空白,而且由于我的二进制函数的大小,索引的节省量不足以弥补所有这些空白

那么,有没有更紧凑的方式来储存这些树木呢?可能通过为leave和branch节点使用不同的节点类型?我该如何区分它们

我的目标是嵌入式系统,所以我们这里讨论的是位/节点。我仍然希望结果5-8位的合理范围和最少16位的节点数。我当然可以使用一个或几个哨兵值。二进制函数可能将用48位表示

[编辑]
BinaryFunctionInput->Boolean在伪代码中应该是UnaryFunctionInput->Boolean;我应该在简化示例时更新名称

googleprotobuf将整数存储为一个整数。小整数比大整数占用更少的空间

变量中的每个字节(最后一个字节除外)都具有最高的有效值 位msb set–这表示还有更多字节。 每个字节的低7位用于存储两个字节的补码 以7位为一组表示数字,最低有效 先分组


正如您在倒数第二段中所指出的,通过使用不同类型的节点可以节省空间

您可以使用两个位确定正在处理的节点类型,使用此信息转换为适当的节点类型

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
}

依此类推-每个节点存储两级树,以更复杂的遍历代码为代价节省一些指针空间。

如果我理解正确,节点的逻辑结构将是:

struct node
    BinaryFunction (48 bits)
    union Left
        LeftNode (16 bits)
        LeftResult (8 bits)
    union Right
        RightNode (16 bits)
        RightResult (8 bits)
所以每个节点在逻辑上至少有三个字段。有4种类型的节点:

LeftNode,RightNode LeftNode,RightResult LeftResult,RightNode LeftResult,RightResult 正如您所说的,您可以去掉LeftNode索引,因为如果有一个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位,索引到节点数组仍然很容易

或者,如果您可以将该二进制函数压缩为46位,那么就可以为节点类型留出空间

编辑 上面假设最大内存块大小为64 KB,这是我的误解。如果您需要支持64K节点,那么情况会有所不同

可以使用两种不同类型的节点:16位和24位。您必须放弃左节点优化,但可以消除节点类型中每个节点的两个位。所以节点类型1和3将是24位,节点类型2和4将是16位。然后,将所有16位节点存储在内存块的前面,然后再存储所有24位节点。您只需要计算16位节点的数量,就可以知道24位节点从何处开始

假设您有1000个16位节点和1000个24位节点。所以你的BigNodeOffset是1000。给定节点索引,可以执行以下操作:

if (nodeIndex > BigNodeOffset)
    nodeOffset = 16*BigNodeOffset + (nodeIndex - BigNodeOffset)*24;
else
    nodeOffset = 16*nodeIndex;
通过将所有类型1节点存储在一起,将所有类型2节点存储在一起,等等,可以避免每节点2位的节点类型,并保留四个值来说明位置 存储每种类型的第一个节点。关键是您可以根据节点在内存中的位置来确定节点的类型


在某些情况下,您可以扩展此想法以利用左节点优化,但这样做会变得非常复杂,可能不值得付出努力。

如果他的树很大,这对他来说并没有任何好处。当然,数字0-127将表示为单个字节,但大于32767的数字将需要三个字节。如果他有超过3289432767+127个节点,那么这个表示将需要比将每个索引存储为2个字节更多的字节。问题是,可变大小的节点要求我存储偏移量,而不是索引。由于我有大约400KB的数据,这是一个19-20位的数字。如果节点大小可变,我无法通过索引访问它们,但我需要一个偏移量。对于分数字节大小的对象,我甚至需要以位为单位的偏移量。这是22位而不是16位。@MSalters:我明白你的意思。出于某种原因,我认为您使用的内存块不超过64K,因此节点值被偏移到该缓冲区中。重读你的问题,我发现我误解了。我得好好考虑一下。如果可以安排树,使子级与其父级之间的距离不超过65535字节,则可以使用此选项。然后节点指针从当前节点的位置偏移到缓冲区中。我希望答案之一就是这样的方案。但其中一个挑战是如何在给定输入树的情况下分配这些节点。深度优先遍历方案的优点是将一个子元素放置在+1的固定偏移量处,这节省了相当多的存储空间。
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
if (nodeIndex > BigNodeOffset)
    nodeOffset = 16*BigNodeOffset + (nodeIndex - BigNodeOffset)*24;
else
    nodeOffset = 16*nodeIndex;