二叉树,数组与链接
Binary Trees, arrays vs linked
一般来说,基于二叉树的抽象可以使用实际的链接节点 objects 来实现,其中每个节点都有指向它的两个 children 的指针,或者一个数组,其中 children 索引 k 中的节点是 2k 和 2k+1.
除了节点的小额外内存开销外,一般的复杂度似乎是相同的。
两者之间有什么具体的优势吗?有趣的是,我发现二叉堆倾向于使用数组实现,而二叉搜索树倾向于使用链接节点实现。有什么原因吗?
数组不能有效表示任意形状的二叉树,只能表示完整棵树。完全二叉树是所有层都满的,或者除最深层以外的所有层都满的,最深层的所有节点都尽可能向左。 (你可以想象,关卡从左到右都是节点,必须先填满一层才能开始下一层。)
根据定义,堆是完整的二叉树 - 因此由于其卓越的内存效率而使用数组实现。另一方面,必须支持在任意位置插入和删除的二叉搜索树(因此可能不是完整的树)不能使用数组实现。
首先,是一个合理的问题:二叉树确实可以嵌入到数组中。 是不正确的:可以通过一些努力将任意形状的树嵌入到数组中(只要你有足够的内存)。一种直接的表示形式是将 Node
定义为变体类型:它是 Filled
或 Empty
,其中 Filled
包含密钥和辅助数据,而 Empty
类似于 Nil
(又名空指针)。如果你需要支持的唯一操作是delete
,那么你就全部设置好了:只需将build
操作实现到return一棵完整的树,然后再实现普通的二叉树delete
操作。无需平衡即可达到 O(log n)
复杂性界限(其中 n
是树的初始项目数)。
也可以通过保持树的平衡形状来实施 insert
操作。稍微简化一下,您维护一个几乎完整的树,其存储大小不超过 2n
(其中 n
是树中的当前项目数)。插入新项时,检查要插入的适当数组单元格的位置:如果它在分配的存储空间内,则只需将其写入该单元格。否则,您从该单元格的父级开始向上爬树,直到找到一个子树,该子树的存储空间足以容纳所有项目,包括新项目;如果不存在这样的子树,则将存储空间加倍。找到该子树后,将其重建为接近完整的形状,并将新项插入正确的数组单元格(现在保证在分配的存储空间内)。所有这些都可以在摊销的 O(log^2(n))
时间内完成,或者在摊销的 O(log(n))
时间内完成更多工作。
以上算法草图基于"Cache Oblivious Search Trees via
小高度二叉树.
我实现了一个名为TeardownTree的数据结构,它使用了这种嵌入。我支持 master 分支上的 build
、delete
、delete_range
、query_range
、iter
操作,以及 [=] 上的实验性 insert
操作21=] 分支。项目页面还包含一些基准,表明数据结构至少对于某些用途绝对可行。
您可能还对 this blog post 解释如何在常量辅助 space 中构建树感兴趣(一种在实践中非常快速的方法)。
一般来说,基于二叉树的抽象可以使用实际的链接节点 objects 来实现,其中每个节点都有指向它的两个 children 的指针,或者一个数组,其中 children 索引 k 中的节点是 2k 和 2k+1.
除了节点的小额外内存开销外,一般的复杂度似乎是相同的。
两者之间有什么具体的优势吗?有趣的是,我发现二叉堆倾向于使用数组实现,而二叉搜索树倾向于使用链接节点实现。有什么原因吗?
数组不能有效表示任意形状的二叉树,只能表示完整棵树。完全二叉树是所有层都满的,或者除最深层以外的所有层都满的,最深层的所有节点都尽可能向左。 (你可以想象,关卡从左到右都是节点,必须先填满一层才能开始下一层。)
根据定义,堆是完整的二叉树 - 因此由于其卓越的内存效率而使用数组实现。另一方面,必须支持在任意位置插入和删除的二叉搜索树(因此可能不是完整的树)不能使用数组实现。
首先,是一个合理的问题:二叉树确实可以嵌入到数组中。 Node
定义为变体类型:它是 Filled
或 Empty
,其中 Filled
包含密钥和辅助数据,而 Empty
类似于 Nil
(又名空指针)。如果你需要支持的唯一操作是delete
,那么你就全部设置好了:只需将build
操作实现到return一棵完整的树,然后再实现普通的二叉树delete
操作。无需平衡即可达到 O(log n)
复杂性界限(其中 n
是树的初始项目数)。
也可以通过保持树的平衡形状来实施 insert
操作。稍微简化一下,您维护一个几乎完整的树,其存储大小不超过 2n
(其中 n
是树中的当前项目数)。插入新项时,检查要插入的适当数组单元格的位置:如果它在分配的存储空间内,则只需将其写入该单元格。否则,您从该单元格的父级开始向上爬树,直到找到一个子树,该子树的存储空间足以容纳所有项目,包括新项目;如果不存在这样的子树,则将存储空间加倍。找到该子树后,将其重建为接近完整的形状,并将新项插入正确的数组单元格(现在保证在分配的存储空间内)。所有这些都可以在摊销的 O(log^2(n))
时间内完成,或者在摊销的 O(log(n))
时间内完成更多工作。
以上算法草图基于"Cache Oblivious Search Trees via 小高度二叉树.
我实现了一个名为TeardownTree的数据结构,它使用了这种嵌入。我支持 master 分支上的 build
、delete
、delete_range
、query_range
、iter
操作,以及 [=] 上的实验性 insert
操作21=] 分支。项目页面还包含一些基准,表明数据结构至少对于某些用途绝对可行。
您可能还对 this blog post 解释如何在常量辅助 space 中构建树感兴趣(一种在实践中非常快速的方法)。