【发布时间】:2011-10-01 22:53:10
【问题描述】:
我见过基于数组的静态二叉树实现,它们不会为指针浪费内存,而是对当前索引进行操作以转到其父级或子级。是否有任何文章讨论了您必须插入或删除的二叉树的类似方法。我可以看到,除非您对允许的插入数量有上限,否则该数组将不再有用。
【问题讨论】:
-
不清楚你想要什么。您能否详细说明,或者提供一个示例?
标签: java c++ data-structures binary-tree
我见过基于数组的静态二叉树实现,它们不会为指针浪费内存,而是对当前索引进行操作以转到其父级或子级。是否有任何文章讨论了您必须插入或删除的二叉树的类似方法。我可以看到,除非您对允许的插入数量有上限,否则该数组将不再有用。
【问题讨论】:
标签: java c++ data-structures binary-tree
总是可以在数组中构建二叉树,使用简单的算法从父节点中查找子节点。一种常见的方法(尤其是二进制堆)是使用以下...
left_child_index = (2 * parent_index) + 1
right_child_index = (2 * parent_index) + 2
所以 0 处的根节点在 1 和 2 有子节点,1 处的节点在 3 和 4 有子节点,依此类推。
这种方案的缺点是,虽然您通过不存储指针来获得空间,但您通常需要在数组中为未使用的节点留出空隙而损失空间。二叉堆通过成为完整的二叉树来避免这种情况——当前项目数范围内的每个节点都是有效的。这适用于堆,但不适用于二叉搜索树操作。
只要您可以调整数组的大小(例如 C++ 中的 std::vector),您就不需要为插入次数设置上限,但您可以得到一个 lot 阵列较深部分的间隙,尤其是在树变得不平衡时。
您还需要一些方法来确定数组中的某个位置是否包含有效节点 - 一个标志或一个不能出现在有效节点中的数据值。标志可能会存储为压缩位数组,与主节点分开。
另一个缺点是重构树意味着移动数据——而不仅仅是调整指针。指针旋转(许多平衡二叉树需要,例如红黑树和 AVL 树)成为潜在的非常昂贵的操作 - 它们不仅需要移动通常的三个节点,而且整个子树都从旋转的节点下降。
也就是说,如果您的项目非常小,并且您的树会保持较小,或者您可以使用简单的不平衡树,那么这个方案很有可能会很有用。这可能只是作为一组整数数据结构似乎是合理的。
顺便说一句 - “合理”并不意味着“推荐”。即使您设法找到一个效率更高的案例,我也很难相信开发时间是合理的。
可能更有用...
多路树在每个节点中包含小的项目数组,而不是通常的一个键。它们最常用于硬盘上的数据库索引。最著名的是 B 树、B+ 树和 B* 树。
多路树有子节点指针,但是对于一个最多可以保存 n 个键的节点,子指针的数量通常是 n 或 n+1 - 不是 n 的两倍。此外,一种常见的策略是对分支节点和叶节点使用不同的节点布局。只有分支节点有子指针。每个数据项都在一个叶子节点中,只有叶子节点包含非关键数据。分支节点纯粹用于指导搜索。由于叶节点是迄今为止数量最多的节点,因此其中没有子指针是一种有用的节省。
但是 - 多路树节点很少被填满。同样,未使用的阵列插槽也会产生空间开销。通常的规则是每个节点(除了根节点)必须至少半满。一些实现在避免分裂节点方面付出了相当多的努力,从而最大限度地减少了空间开销,但通常预期的开销与项目数大致成正比。
我还听说过一种树的形式,每个节点拥有多个键,但每个节点只有两个子指针。恐怕我什至不记得这叫什么了。
也可以将(父指针,子指针)对存储在单独的数据结构中。这对于在数据库中表示树是相当常见的,使用(父 ID,子 ID)对的表,或(父 ID,兄弟索引,子 ID)三元组或其他的表。一个优点是您不需要存储“空”指针。
然而,与其试图减少或消除存储指针的开销相比,最好的选择可能是更好地利用这些开销。线程二叉树更好地利用子指针来支持树的有效遍历 - http://en.wikipedia.org/wiki/Threaded_binary_tree。
【讨论】:
至少在 C++ 中,使用数组而不是单独分配的结构的部分好处是避免了创建每个对象的开销。 (C++ 中的结构数组在内存中是连续的,没有标题或分配对齐问题)相比之下,保存一个指针可能很小。
不幸的是,在 Java 中,对象数组不能以这种方式工作,因此使用数组不会给您带来想象中的好处。在 C++ 中计算对每个对象的引用,但在 Java 中,对每个对象的引用存储在内存中,即使它们碰巧是连续的。
Java 为您做的唯一一件事就是在 64 位 JVM 中使用 32 位引用。
除非您的设备内存有限,或者数据结构非常大(例如数百万个元素),否则您不太可能注意到差异,并且您可以以不到 100 英镑的价格购买 16 GB。
【讨论】: