一个简洁的二叉树,包括求和,数组构造和寻址

时间:2017-07-04 01:39:18

标签: c arrays algorithm go tree

使用'sum'作为一些任意计算的简写。我有一个过程,通过递归求和值对来计算值列表中的单个和。未配对的值会在树中不加改变地提升,直到它们可以配对。

鉴于此计算,我正在寻找平衡计算的最佳方法(即访问数组元素/节点所需的操作数),以及1维数组中所有节点的最简洁编码(即无间隙) ,nil值或重复值),并且最好没有额外的索引数组,这些索引数组不能从简洁的编码中轻易导出,因此必须与数组一起保存。

虽然以下是简单示例,但实际上初始列表中的值的数量可能非常大(2 ^ 47或更多)。

例如,给定列表[1,2,3,4],数组很简单:[10,3,7,1,2,3,4],并且很好地拆分成易于寻址的行按节点,或作为整行的参考。

但对于5项列表,树看起来像这样:

树1

         15
        /  \
       /    \
      /      \
     /        \
    10          5
  /   \       /   \
 3     7     5     -
/ \   / \   / \   / \
1  2  3  4 5   - -   -

标准制图left = i*2+1right = i*2+2为我们提供了这个数组:

数组1

[ 15, 10,  5,  3,   7,   5,  nil,   1,   2,   3,   4,   5,   nil,   nil, nil]

此数组有4个零值,列表“5”中的最后一个元素重复2次。

为了改善这一点,我们可以暗示重复5,并删除零值:

数组2

[15, 10, 3, 7, 1, 2, 3, 4, 5]

哪个更紧凑。这棵树是一样的,但在概念上看起来有点像:

树2

       15
      / \
     /   \
    10    \
  /   \    \
 3     7    \
/ \   / \    \
1  2  3  4    5

数组2 编码中,我有4行:

1. [1, 2, 3, 4]
2. [3, 7]
3. [10, 5]
4. [15]

第1,2和4行可以简单地引用数组2 ,允许我在没有分配或副本的情况下“就地”计算结果。非常快。但是,第3行包含两个非连续单元格中的值。我必须打破用于其他行的简单逻辑,并可能为地图添加复制,索引或存储。

我可以构建完整/平衡的子树(例如索引1-7,1,2,3,4的树),但是当奇数个项出现时,它们似乎并不总是很好地对齐在不同的行,取决于输入长度。例如,考虑一个具有6个元素的初始列表的树。

2 个答案:

答案 0 :(得分:1)

假设您的树在最后(最多)行上有N个节点。

如果确实存储了仅向上传播的节点,则树的总节点数在2*N-12*N-1+log2(N)之间。节点的确切总数由OEIS A120511给出。其中,最多floor(2 + log2(N-1))是复制/传播的节点。

树有floor(2 + log2(N-1))行。作为N函数的行数(最后一行中的元素数量)为OEIS A070941

此类树中的行数非常少。例如,如果最后一行中有2个 40 ≈1,000,000,000,000个节点,则树中只有42行。对于2个 64 节点,您只有66个。因此,如果每行需要一些操作,则开销不高。

在给定最后一行N中的节点数的情况下,一个简单的对数时间函数可以计算行数和节点总数:

# Account for the root node
rows = 1
total = 1

curr_left = N
While (curr_left > 1):
    rows = rows + 1
    total = total + curr_left
    curr_left = (curr_left + 1) / 2
End While

其中/表示整数除法,即任何小数部分被丢弃/截断/舍入为零。同样,对于最后一行中的2个 64 节点,上述循环仅为65次。

当我们知道树中节点的总数和行数时,我们可以使用另一个对数时间循环来计算树的每一行上第一个元素的偏移量,以及节点上的节点数那一行:

first_offset = []
nodes = []

curr_row = rows - 1
curr_offset = total - N
curr_left = N

While (curr_left > 1):
    nodes[curr_row] = curr_left
    first_offset[curr_row] = curr_offset
    curr_row = curr_row - 1
    curr_left = (curr_left + 1) / 2
    curr_offset = curr_offset - curr_left
}

first_offset[0] = 0
nodes[0] = 1

和以前一样,对于最后一行中的2个 64 节点,上面的循环只有65次。

一行中的所有元素在内存中是连续的,如果我们使用从零开始的索引,并且N是最后一行上的节点数,我们应用上面的那些,那么

  • rows是树中的行数

  • total是树中节点的总数

  • nodes[r]上有r个节点,r >= 0r < rows

  • r上的节点,列c的数组索引为first_offset[r] + c

  • r上的节点,列cr > 0,行r-1上的父级,列c/2,数据索引first_offset[r-1] + c/2

  • r上的节点cr < rows - 1,行r+1上的左子节点,列2*c,数组index first_offset[r+1] + 2*c

  • r上的节点,列cr < rows - 1c < nodes[r] - 1,行r+1上有一个正确的子节点,列{{ 1}},在数组索引2*c+1

  • first_offset[r+1] + 2*c + 1上的节点,列rcr < rows - 1,左右儿童

这个数组是紧凑的,除了向上传播的节点(因此,对于一个TB级数据集可能有几十个节点),不会浪费任何存储空间。

如果最后一行中的节点数与数组一起存储(例如,作为数组数据之后的额外c < nodes[r] - 1),则所有读者都可以恢复uint64_ttotalrowsfirst_offset[],轻松导航树。 (但请注意,不是只使用数组索引,而是使用&#34;列&#34;和&#34; row&#34;而是使用它们派生数组索引。)

由于nodes[]first_offset[]数组最多只有几十个条目,因此它们应该在缓存中保持热销,使用它们不应该损害性能。

请注意,并非所有树木大小都适用于上述第二段中规定的规则。例如,具有两个节点的树没有意义:为什么要复制根节点?

如果您确实知道树的大小(nodes[])有效,则可以使用二进制搜索在total时间复杂度中基于N找到total,或者在O(log2(total)*log2log2(total))中如果使用简单的循环。请注意,O((log2(total))²)介于total2*N-1之间。相反,2*N-1+log2(N)不能大于N或小于(N + 1)/2,因为(N + 1)/2 - log2(total)大于total,因此N更少比log2(N)。因此,二进制搜索可以实现为

log2(total)

请记住,即使树中有2个 64 节点,上述函数最多只能Function Find_N(total): Nmax = (total + 1) / 2 Nmin = Nmax - log2(total) t = Total(Nmin) If t == total: Return Nmin Else if t < total: Return "Bug!" End if t = Total(Nmax) if t == total: Return Nmax Else if t > total: Return "Bug!" End if Loop: N = (Nmin + Nmax) / 2 If N == Nmin: Return "Invalid tree size!" End If t = Total(N) If t > total: Nmax = N Else if t < total: Nmin = N Else: return N End If End Loop End Function = 6次调用1 + log2(64),这是一个实现第一个伪代码的函数这个答案中的片段。由于每个程序调用通常只需要一次,因此开销实际上是无关紧要的。

您可以使用Total使用log2(x)函数计算log(x)/log(2),因为C99(log2()的精度低于<math.h> },我会将double添加到结果中,或者使用uint64_t将其向正无穷大舍入,只是为了确定),或者甚至使用简单的循环:

+1

再次,ceil()表示整数除法。

答案 1 :(得分:0)

听起来你要求对二叉树进行简洁的编码。为此,足以以一个附加位预先存储节点数据以表示叶与内部节点。用于编码和解码的算法相当简单,并在a Wikipedia article中给出,所以我不会在这里重复它们。这种编码非常接近信息理论的最佳可能性。它可能不值得寻找更好的。