树的紧凑表示

时间:2014-09-16 11:37:26

标签: data-structures tree

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

我的树是二元的并且相当平衡(最大深度~20,但是大小~50K节点)。生成它们的算法使用类似于

的节点结构
class Node {
   BinaryFunction BF(Input->Boolean);
   [optional] Node LeftNode;
   [optional] Result LeftResult;
   [optional] Node RightNode;
   [optional] Result RightResult;
}

其中Result占用几位,Node存储为指针(4/8字节)。虽然LeftNodeLeftResult在技术上是可选的,但每个节点都包含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);我简化示例时应该更新名称。

3 个答案:

答案 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
}

如果您能够确定节点的大小,那么您只需要一位来区分LeftNodeRightNode

您可以进一步展开节点以消除更多指针,例如

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种类型的节点:

  1. LeftNode,RightNode
  2. LeftNode,RightResult
  3. LeftResult,RightNode
  4. LeftResult,RightResult
  5. 正如你所说,你可以摆脱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位为单位表示数字,最不重要   小组第一。