我刚刚发现有一些基于树的数据结构,在寻找高性能时,通常会存储为连续的内存块,这在使用所谓的基于策略的数据结构时特别受欢迎#34;
问题在于我无法理解为什么人们会这样做;当你试图"线性化"将树存储为向量/数组的树,如何确保以有意义的方式重新排列分支和叶子以帮助提高性能?这只适用于完美平衡的树木吗?
换句话说,我无法想象用于访问跨越多个级别并具有多个叶子的线性数据结构的模式;通常,树为每个节点/叶子添加1级间接,这为用户简化了很多事情,但是应该如何组织这样的"线性"树?
答案 0 :(得分:7)
您可能会发现短文here感兴趣
基本上,为这种结构使用连续内存块的原因是它在处理潜在的大型数据集时大大提高了查找和扫描时间。如果您的内存不连续,则可能必须使用昂贵的遍历算法从数据结构中检索数据。
希望这符合您的兴趣。
以下是文章中描述这一概念的两张图片:
平衡树
存储在连续内存中的树:
答案 1 :(得分:4)
将数据结构存储在连续内存中是一种用于内存受限系统的技术,例如嵌入式系统。该技术还可用于安全和性能关键系统。
桌面系统通常有很多内存,而且它们的应用程序很短暂。它们的动态内存分配过程是在内存池中找到下一个可用块并返回它。如果没有可用的内存(例如碎片),则分配失败。无法控制可以消耗多少内存。
通过使用连续分配方法,可以限制或限制创建的节点数量。这意味着在具有32k内存的系统中,树不会耗尽所有内存并留下漏洞。
使用连续系统,分配过程更快。你知道块在哪里。而且,不是存储链接的指针,而是可以存储索引值。这也允许将树存储到文件中并轻松检索。
您可以通过创建节点的数组或向量来对此进行建模。更改节点数据结构以使用数组索引而不是指针。
请记住,了解性能问题的唯一方法是分析。
答案 2 :(得分:4)
实际上有很多这样的模式,它们有两个目的:节省内存,保持节点togeather,主要用于分页性能。
一个非常简单的版本只是分配三个块,一个父项和两个孩子,这个块有四个孩子"街区,每个孩子两个。这会使您的分配减少三分之一。这不是一个优化,直到你扩展它,分配7,15,31,63 ...如果你能得到它,以便尽可能多的键适合单个内存系统页面,然后您最大限度地减少等待硬盘驱动器所花费的时间。如果您的密钥每个都是32个字节,而页面是4K,那么您最多可以存储125个密钥,这意味着您只需要为树的每7行从硬盘驱动器加载一个页面。此时,您加载了"孩子"页面,然后按照另外7行。常规二叉树每页只能有一个节点,这意味着您在迭代树时只需花费7倍的时间来等待硬盘驱动器。很慢。旋转有点棘手,因为你必须实际交换数据,而不是树实现常见的指针。此外,当树变大时,浪费很多空间。
---------
| O |
| / \ |
| O O |
_---------_
__/ \ / \__
/ | | \
--------- --------- --------- ---------
| O | | O | | O | | O |
| / \ | | / \ | | / \ | | / \ |
| O O | | O O | | O O | | O O |
--------- --------- --------- ---------
另一个更为复杂的模式"是将树垂直切成两半,所以顶部是一个"子树",它有很多孩子"子树",并存储每个"子树"线性。你递归地重复这个。这是一个非常奇怪的模式,但最终模糊地类似于上面的模式,除了它" cache-oblivious",这意味着它适用于任何页面大小或缓存层次结构。非常酷,但它们很复杂,几乎所有东西都在三个众所周知的架构之一上运行,因此它们并不受欢迎。他们也非常难以插入/删除
另一个非常简单的变体是将整个树放入通过indecies访问的数组中,这样可以节省总内存,但只有顶部是缓存友好的,较低级别更差缓存比常规二进制更明智树。实际上,根位于索引i = 0,左子位于(n*2+1 = 1
),右子位于(n*2+2 = 2
)。如果我们在索引24处的节点处,它的父节点是((n-1)/2 = 12
),并且它的左右儿童分别是49和50。这适用于小树,因为它不需要任何指针开销,数据存储为连续的值数组,并且关系由索引推断。此外,添加和删除子项总是发生在右端附近,并且正常的二叉树插入/旋转/擦除适用。这些也有一个有趣的数学新颖性,如果你将索引加1转换为二进制,那么它与树中的位置相对应。如果我们考虑索引24处的节点,则二进制中的24 + 1是11001 - >第一个1总是意味着根,从那里每个1意味着"向右"并且每个0表示"向左走",这意味着从右侧,左侧,左侧,右侧开始到根索引24,并且您在那里。另外,由于有5个二进制数字,你知道它在第五行。这些观察结果都没有特别有用,除了它们暗示根节点是一个正确的孩子,这是非常有趣的。 (如果你扩展到其他基础,根就是最右边的孩子)。话虽如此,如果你使用双向迭代器,将root实现为左节点仍然很有用。
0
/ \
/ \
1 2
/ \ / \
3 4 5 6
[0][1][2][3][4][5][6]
答案 3 :(得分:2)
如何确保重新安排树枝和树叶 以有意义的方式帮助提高绩效?
如果您已经运行了程序(使用非连续树),您可以随时检测程序以报告其实际的节点访问模式通常是什么样的。一旦您对如何访问节点有了一个很好的了解,您就可以自定义节点分配器,以便以相同的顺序在内存中分配节点。
答案 4 :(得分:1)
" ...尝试"线性化"将树存储为矢量/数组的树,如何确保以有意义的方式重新排列树枝和树叶,以帮助提高性能......"
我相信你的想法太难了。
在普通树中,您使用“' new'请求创建节点的可用空间。
使用delete将不再需要的空间返回给堆。
然后使用指针连接节点。
对于'树中的矢量',您可能只需重新实现新的和删除即可在向量中找到空格。
我认为使用索引(对于父节点,左节点或右节点)而不是指针(对于父节点或左节点或右节点)。
我相信向量中第n项的索引(在重新分配增长之前和之后)都没有变化。
另一个挑战是删除节点......但这可以像任何比删除的节点减少1的节点(或索引)一样简单。
这些选择对于很少变化的树来说是公平交易,但必须尽快捕获。
存储树真的需要执行矢量块保存吗?矢量块保存实际上比同一树的深度优先保存更快。你真的需要衡量。
答案 5 :(得分:1)
区分树和AVL tree很重要。在你的问题中,你谈到平衡树所以你的问题可能是关于数组中的AVL树表示。
所有其他答案都谈论树而不是AVL树。据我所知,这种树可以用数组表示但不能有效更新,因为你必须重新排序数组的许多元素而不是使用内存指针。
这意味着只要输入元素已经排序,您就可以在数组中表示完美排序的平衡树。这棵树比通常的内存树更快,但更新它将会更难"。
我的结论是: