我是否可以基于深度优先顺序而不是宽度优先顺序为完整树提供类似堆的连续布局?

Can I have a heap-like contiguous layout for complete trees based on a depth first order rather than breadth first?

本文关键字:顺序 布局 连续 是否 深度优先      更新时间:2023-10-16

堆是一种经典的数据结构,它将一个完整的二进位树(或广义版本的d-ary树)放入一个连续的数组中,以宽度优先的遍历顺序存储元素。通过这种方式,树中同一层的所有元素一个接一个地连续存储。

我正在实现一个数据结构,在底层,它是一个固定度d的完整平衡树,我想以连续的形式存储树,以释放节点指针的空间。所以我想把节点按宽度优先的顺序放在堆中,但我担心从根节点到叶节点的典型搜索的缓存性能,因为在每一级,我都要跳过很多元素。

是否有一种方法来获得d元完整树的紧凑连续表示,基于深度优先顺序代替?

这样,在我看来,在搜索叶子时接触的节点更有可能彼此靠近。问题是如何检索节点的父节点和子节点的索引,但我也想知道在这种设置下,树上的哪些操作通常是有效的。

我正在c++中实现这个东西,以防它很重要。

为简单起见,我将把我的讨论限制在二叉树,但我所说的也适用于n树。

堆(和一般的树)存储在数组宽度优先的原因是因为这样更容易添加和删除项:增加和缩小树。如果你是深度优先存储,那么要么树必须按其最大预期大小分配,要么你必须在添加级别时做大量的移动项目。

但是如果你知道你要有一个完整的,平衡的,n元树,那么选择BFS或DFS表示很大程度上是一个风格问题。在内存性能方面,没有任何特别的好处。在一种表示(DFS)中,您将缓存缺失放在前面,而在另一种情况(BFS)中,您将缓存缺失放在最后。

考虑一棵二叉树,有20层(即2^20 - 1项),包含从0到(2^20 - 1)的数字,每个节点占用4个字节(整数的大小)。

使用BFS,当获得树的第一个块时,会导致缓存丢失。但是树的前四层在缓存中。因此,您的下三个查询保证在缓存中。在此之后,当节点索引大于15时,您可以保证有缓存丢失,因为左子节点位于x*2 + 1,这将距离父节点至少16个位置(64字节)。

使用DFS,在读取树的第一个块时会导致缓存丢失。只要你正在搜索的数字在当前节点的左子树中,你就可以保证在前15个级别中不会出现缓存丢失(即你不断地向左移动)。但是任何向右的分支都会导致缓存丢失,直到你到达叶子以上的三层。此时,整个子树将适合缓存,并且您的剩余查询不会导致缓存丢失。

对于BFS,缓存缺失的数量与你必须搜索的级别数量成正比。使用DFS,缓存丢失的次数与通过树的路径和必须搜索的层数成正比。但是平均,在搜索项时,DFS和BFS所导致的缓存未命中次数是相同的。

计算节点位置的数学方法对于BFS来说比DFS更容易,特别是当你想找到一个特定节点的父节点时。

看来需要一个is_leaf指示器。因为大多数事物都与层相关,我们需要一种快速找到它的方法,这似乎取决于知道节点是否是叶节点。

下面的代码段还假设节点相对于父节点的位置是已知的…它既不漂亮,也没什么用,因为整个目的是为了节省空间。

int parent_index(int index, int pos){
  if (pos == LEFT){
    return i-1;
  } else {
    return i - pow(2,num_levels - level(i));
  }
}
int left_child_index(int index){
  return i+1;
}
int right_child_index(int index){
  return i+pow(2,num_levels - level(index))
}

要得到一个节点的级别,你可以遍历左边的子节点,直到到达一个叶子节点。

树指数之间的差异似乎类似于帕斯卡三角形,所以这可能也有帮助。

我刚有个想法。

中缀顺序呢?这样一切都更容易计算:

bool is_leaf(unsigned int i){
  return !(i%2);
}
unsigned int left_child(unsigned int i){
  return i - pow(2,num_levels - level(i));
}
unsigned int left_child(unsigned int i){
  return i + pow(2,num_levels - level(i));
}
int level(unsigned int i){
  unsigned int offset = 1;
  unsigned int level_bits = 1;
  int level = 0;
  while ((i - offset)&level_bits == 0){
    level++;
    offset += pow(2,level);
    level_bits = (level_bits << 1) + 1; /* should be a string of trailing 1s */
  }
  return level;
}

这样,你应该只在最上面的节点得到大的跳跃。之后,跳变的指数变小。这样做的好处在于,由于低层的节点较少,因此可以缓存这些节点。当树更密集时(即,更多的比较),跳跃要小得多。

缺点是插入很慢:

void insert_node(node[] node_array, node new_node){
  for (unsigned int i = num_nodes-1; i >= 0; i--){
    node_array[i*2 + 1] = node_array[i];
    node_array[i] = NULL_NODE_VALUE; /* complete (but not full) trees are discontiguous */
  }
  node_arry[0] = new_node;
}

这种中缀顺序无疑比前缀(深度优先搜索)顺序要好得多,因为树在逻辑上和物理上都是"平衡的"。在前缀顺序中,左侧更受青睐——所以无论如何它都会表现得像一棵不平衡的树。至少有了中缀,你可以在密集的节点间进行平衡和快速的搜索。

二叉搜索树用于存储信息,这些信息以后可以高效地查询和排序。任意节点的左节点的值小于该节点的值,右节点的值大于该节点的值。

堆是几乎完全二叉搜索树的有效实现?

二叉搜索树需要至少两个其他指针(因为它们也可以是父指针),除了由特定节点表示的数据值。基于堆的结构利用几乎完全BST的特性,将指针操作转换为数组索引操作。我们还知道,如果特定的BST不接近几乎完整的BST,我们在该二叉树的数组表示中创建孔,以便维护父节点和子节点之间的关系。这意味着在这种情况下,使用指针的成本可能会被抵消。

实现基于深度一阶树遍历的堆状结构?

通过尝试实现类似堆的结构对树进行深度一阶遍历,我们不再能够证明首先使用堆的理由。由于深度与树的宽度(可以在给定树几乎完全BST的特定级别计算)相比不是固定的,因此我们必须处理元素之间的复杂关系。每当有新的元素从树中添加或删除时,我们还必须重新排列元素,使它们仍然满足堆的属性。所以,如果这样实现的话,我不认为我们能够证明在BST上使用堆是合理的。