【问题标题】:Efficient Array Storage for Binary Tree二叉树的高效阵列存储
【发布时间】:2011-02-10 04:06:03
【问题描述】:

我们必须将二叉树的节点写入文件。编写二叉树最节省空间的方法是什么。我们可以将其以数组格式存储,父对象位于i,其子对象位于2i2i+1。但这会在稀疏二叉树的情况下浪费大量空间。

【问题讨论】:

    标签: arrays algorithm data-structures binary-tree


    【解决方案1】:

    我喜欢的一种方法是存储前序遍历,但也包括其中的“空”节点。存储“空”节点无需同时存储树的顺序。

    这种方法的一些优点

    • 在大多数实际情况下,您可以比 pre/post + inorder 方法进行更好的存储。
    • 序列化只需遍历一次
    • 可以一次性完成反序列化。
    • 中序遍历可以在不构建树的情况下一次性完成,如果情况需要,这可能很有用。

    例如,假设您有一个 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;)。

    【讨论】:

    • 我们如何才能得到你在优点中提到的中序遍历??
    • @manyu 这里提到的中序遍历是树的深度优先遍历,可以通过遍历结果数组,跳过任何为空的内容来找到。跨度>
    • 问题的重点不是重建与以前相同的树。我不明白你怎么能用你的解决方案做到这一点。最后只有中序遍历。你能解释一下吗?
    • @user529734 最后,您将进行有序和前序遍历,因此您可以像以前一样重建树。在这里解释geeksforgeeks.org/…
    • 什么是n-1指示位?以及如何在代码中处理 64 位整数(用于值)和位(用于空指针)?
    【解决方案2】:

    想想 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></>
       </>
    </>
    

    删除空格:&lt;1&gt;&lt;2&gt;&lt;/2&gt;&lt;3&gt;&lt;4&gt;&lt;/&gt;&lt;5&gt;&lt;/&gt;&lt;/&gt;&lt;/&gt;

    去掉尖括号:12/34/5///

    现在的问题是:如果一个节点有一个空的左子树和非空的右子树怎么办? 然后我们可以使用另一个特殊的字符“#”来表示一个空的左子树。

    例如:

        1
      /   \
          2
         /  \
        3
    

    这棵树可以序列化为:1#23///

    【讨论】:

    • 这也是使用2n空间,这比preoder/inorder有什么好处?
    【解决方案3】:

    如果您有一棵(几乎)完整的树,那么 2i、2i+1(二叉堆)方法确实是最好的方法。

    否则,您将无法避免为每个节点存储 ParentId(父索引)。

    【讨论】:

      【解决方案4】:

      您可以将二叉树的in-orderpre/post-order 遍历保存在文件中,并根据这些遍历重构树。

      【讨论】:

      • 这种情况下我总是会消耗 2n 个空间(n 代表 inorder,n 代表 post order)。
      【解决方案5】:

      由于只有 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] &lt; item[2*i] &amp;&amp; item[i] &gt; 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*index2*index-1 计算中使用 index - subtree_base_index 调整。失败搜索所需的最大比较总数(主表 + 子树)通常为 ⌈log₂N⌉,有时——当搜索键大于主表的第一个条目时——小于(不为 N 的 0 位创建子树)。

      您还应该将每个子树的级别计数存储在主表中(以便您知道在搜索了多少次后应该停止搜索子树)。

      【讨论】:

        【解决方案6】:

        您可以存储级别顺序,即 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));

        【讨论】:

        • 不错的建议。一个尼特:我相信级别顺序是 BFS(广度优先)而不是 DFS(深度优先)的输出
        猜你喜欢
        • 2013-01-20
        • 2021-09-07
        • 1970-01-01
        • 2011-08-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多