【问题标题】:Data-oriented tree traversal without recursion无递归的面向数据的树遍历
【发布时间】:2015-04-17 04:10:06
【问题描述】:

我有一个这样的树结构:一个模型有一个根节点,每个节点有任意数量的子节点和任意数量的网格。

我的应用程序中的很多时间都花在遍历这棵树并进行诸如视锥体剔除和矩阵乘法之类的计算上。目前,它是幼稚的实现,其中每个节点都有子节点和网格的向量,并且树是递归遍历的。这很慢。

我一直在研究面向数据的设计,我喜欢它对缓存非常友好的想法。我一直在想这样的事情:

struct Mesh
{
    // misc data
    MeshID mMeshID;
}

// probably needs more information?
struct Node
{
    // begin and end index into Models 'mNodes'
    uint32_t mChildrenBegin;
    uint32_t mChildrenEnd;

    // as above but for meshes
    uint32_t mMeshesBegin;
    uint32_t mMeshesEnd;
}

struct Model
{
    std::vector<Node> mNodes;
    std::vector<Mesh> mMeshes;
}

现在我需要遍历树以获取可见网格的列表。在每个节点上,我必须检查该节点是否可见。以下分支:

  • 节点可见。它下面的所有子节点和网格也是可见的。不要深入这个分支。检查相同深度的其他节点。
  • 节点不可见。此节点或其下方的子节点或网格不可见。不要深入这个分支。检查相同深度的其他节点。
  • 节点部分可见。一些节点和/或一些网格是可见的。必须深入层次结构。

树是静态的。一旦模型被加载到应用程序中,树就永远不会改变。所以不知何故,我肯定能够使用这些信息来获得一个有效的结构。

我很困惑如何解决这个问题。

几个问题;

  1. 如何在内存中布局节点?是第一个索引的根节点吗?其他节点是根据深度添加的吗?
  2. 如何在不使用递归的情况下迭代树?我不想访问每个节点,除非我真的必须这样做。例如,如果 depth=2 的节点不可见,则不应测试其所有网格和子节点(及其网格),而应完全跳过。

【问题讨论】:

  • 对于 1) 我们需要更多关于如何构建这棵树的信息。
  • 第一句话还不够吗?
  • 我可以描述一种非递归方法,但我看不出它如何比递归方法快得多——如果递归方法被正确实现的话。无论哪种方式,都必须访问每个部分可见的节点及其所有子节点。如果您已经确认您的递归方法没有做错任何事情(例如访问不应该访问的节点,访问同一个节点两次),那么 recursion 可能不是问题。
  • 无关:Alot of time.
  • "现在我需要遍历树以获得可见网格的列表。" “节点是可见的。它下面的所有子节点和网格也是可见的。不要深入这个分支。”第一句话提出了一个重要的问题:如果一个节点是可见的,那么你是否需要能够快速记录或以其他方式引用它下面的所有后代可见网格而不以某种方式递归到后代中???

标签: c++ memory-management tree game-engine data-oriented-design


【解决方案1】:

短版:改用 6502 的预购答案。我将在下面留下我之前的答案,因为它仍然有一些有趣的代码和 cmets。

以半预订方式布置您的存储阵列。即:sentinel end node、roots、first root的children、first root的first child的children、first root的first grandchild的children,以此类推。然后使用递归半前序遍历遍历树,该遍历将直接祖先及其兄弟姐妹的索引信息的副本保留在堆栈上。这样,您的遍历将从左到右扫描存储阵列,无需回溯。您不需要使用递归访问所有节点,并且您所做的任何跳过子树只会在存储阵列中向前跳转。

model.semiPreOrderTraversalRecur(model.getEnd());  // traverse the whole tree

...

// NOTE: pass prnt by *COPY* -- break Node into index portion and app. data portion; we only need the index portions here

void Model::semiPreOrderTraversalRecur(Node prnt)
{  
  Node children[prnt.mChildrenEnd - prnt.mChildrenBegin];  // just index info
  uint32_t i;

  // visit children of prnt; determine visibility etc.

  for (i = prnt.mChildrenBegin; i < prnt.mChildrenEnd; ++i)
  {
    cout << "Visiting " << mNodes[i].mVal << endl;
    mNodes[i].mInvisible = false;  // TODO: determine based on culling, etc.
    children[i - prnt.mChildrenBegin] = mNodes[i];  // just index info
  }

  // recurse on children as necessary

  for (i = 0; i < prnt.mChildrenEnd - prnt.mChildrenBegin; ++i)
    if (!children[i].mInvisible && children[i].mChildrenBegin != children[i].mChildrenEnd)
      semiPreOrderTraversalRecur(children[i]);
}

长版本(其中一些已被排除):我认为您可以通过向您的节点结构添加更多信息来实现您想要的:节点的父节点的索引和当前节点的索引。 (后者可能不是绝对必要的,因为它可能从指向 Node 的指针和 Node 存储向量派生而来。)

这应该为您提供足够的上下文信息,以便根据树中的任何节点轻松地将“向上”、“向下”或“侧向”移动到同级。要“向上”移动,您只需转到父索引。要向下移动,请转到任何一个子索引。要“横向”移动到同级,您只需增加/减少当前节点的索引(在检查您不是父母的最后一个/第一个孩子之后)。

您可能需要考虑组合节点和网格结构,以便可以将它们连续存储在一个向量中。对鹅有益的缓存性能通常对鹅有益。由于您的 Mesh 存储在另一个向量中,它们在内存中可能与它们的节点兄弟姐妹相距甚远,并且在遍历中间访问它们将对缓存施加更大的压力。当然,如果你的 Mesh 比你的 Node 有更多的数据(反之亦然),或者你不需要在遍历过程中访问 Mesh,那么这可能不是一个好的选择,因为它会浪费内存。此外,您的网格可能不需要所有树索引,因为它们是终端节点,并且在您访问节点的子节点时可能是特殊情况。根据具体情况,您最初单独存储 Mesh 的建议可能会更好。

在下面的代码中,我将您的节点和网格结构组合起来,并将它们存储在一个向量中。

#include <cstdint>
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
#include <thread>

using namespace std;

struct Node
{
  uint32_t mParentIndex;    // index of parent Node in Model
  uint32_t mIndex;          // index of this Node in Model (may be possible to derive this from Node pointer and Model's vector rather than storing it)

  uint32_t mChildrenBegin;  // begin and end index of children Node in Model
  uint32_t mChildrenEnd;

  bool     mInvisible;      // used during semiPreOrderTraversal to remember no need to traverse subtree rooted at this Node

  int      mVal;            // dummy data for debugging
};

struct Model
{
  vector<Node> mNodes;  // preferably private, but your call

  Model(istream *in = NULL);

  Node *getEnd(void) { return &mNodes[0]; }  // sentinel end node that always exists and has itself as parent
  Node *getRoot(void) { return getChild(getEnd()); }

  Node *getParent(Node *curr) { return &mNodes[curr->mParentIndex]; }  // always safe to call; returns end as sentinel
  Node *getNextSibling(Node *curr) { Node *prnt = getParent(curr); return (curr->mIndex && curr->mIndex + 1 < prnt->mChildrenEnd ? &mNodes[curr->mIndex + 1] : NULL); }
  Node *getPrevSibling(Node *curr) { Node *prnt = getParent(curr); return (curr->mIndex > prnt->mChildrenBegin ? &mNodes[curr->mIndex - 1] : NULL); }
  Node *getChild(Node *curr, unsigned nth_child = 0) { unsigned index = curr->mChildrenBegin + nth_child; return (index < curr->mChildrenEnd ? &mNodes[index] : NULL); }

  void semiPreOrderTraversal(void);
  void semiPreOrderTraversalRecur(Node prnt);

private:
  int  buildFromStreamRecur(Node *curr, int val, istream &in);
};

void Model::semiPreOrderTraversal(void)
{
  Node *curr = getRoot();
  Node *next;

  cout << "Beginning Semi-Pre-Order traversal of tree!" << endl;

  if (NULL == curr)
    goto DONE;

  while (1)
  {
    // TODO: visit curr; determine and record mInvisible in curr

    cout << "Visiting " << curr->mVal << endl;
    curr->mInvisible = false;

    // try to move to sibling

    if (NULL == (next = getNextSibling(curr)))
    {
      // try to move to children of siblings

      curr = getChild(getParent(curr));  // move back to oldest sibling

      cout << "No more siblings to visit! Try to visit their children. Rewinding to oldest sibling " << curr->mVal << endl;

      while (curr->mInvisible || NULL == (next = getChild(curr)))
      {
        cout << "No children to visit of " << curr->mVal << endl;

        // shouldn't visit curr's children or it has no children
        // try to move to sibling

        if (NULL == (next = getNextSibling(curr)))  
        {
          cout << "Reached end of siblings again -> completed traversal of subtree rooted by parent " << getParent(curr)->mVal << endl;
          // ascend while we can't find a uncle to check for children to visit

          do {
            if (getEnd() == (curr = getParent(curr)))  
              goto DONE;  // got back to end -> traversal complete

            cout << "Moved back up to " << curr->mVal << endl;

          } while (NULL == (next = getNextSibling(curr)));

          cout << "Found a great^Nth uncle (" << next->mVal << ") to check for children to visit!" << endl;
        }
        // else check sibling for children to visit

        curr = next;
        cout << "Trying to visit children of " << curr->mVal << endl;
      }
      // visit children of curr

      cout << "Will visit children of " << curr->mVal << endl;
    }
    else
      cout << "Will visit sibling of " << curr->mVal << endl;

    curr = next;
  }

DONE:
  cout << "Finished Semi-Pre-Order traversal of tree!" << endl;
}

void Model::semiPreOrderTraversalRecur(Node prnt)
{  
  Node children[prnt.mChildrenEnd - prnt.mChildrenBegin];  // just index info
  uint32_t i;

  // visit children of prnt; determine visibility etc.

  for (i = prnt.mChildrenBegin; i < prnt.mChildrenEnd; ++i)
  {
    cout << "Visiting " << mNodes[i].mVal << endl;
    mNodes[i].mInvisible = false;
    children[i - prnt.mChildrenBegin] = mNodes[i];  // just index info
  }

  // recurse on children as necessary

  for (i = 0; i < prnt.mChildrenEnd - prnt.mChildrenBegin; ++i)
    if (!children[i].mInvisible && children[i].mChildrenBegin != children[i].mChildrenEnd)
      semiPreOrderTraversalRecur(children[i]);
}

Model::Model(istream *in)
{
  Node dmy, *end;

  mNodes.push_back(dmy);  // create sentinel end node

  end = getEnd();
  end->mParentIndex   = 0;
  end->mIndex         = 0;
  end->mChildrenBegin = 1;
  end->mChildrenEnd   = 1;
  end->mVal           = -1;  

  if (in)
    buildFromStreamRecur(getEnd(), 0, *in);
}

int Model::buildFromStreamRecur(Node *curr, int val, istream &in)
{
  uint32_t numChildren, i, currInd = curr->mIndex;

  // read in how many children curr will have

  in >> numChildren;

  // allocate children in mNodes and set curr's children references
  // NOTE: protect against reallocations of mNodes

  curr->mChildrenBegin = mNodes.size();

  for (i = 0; i < numChildren; ++i)
  {
    Node node;

    node.mParentIndex   = currInd;
    node.mIndex         = mNodes.size();
    node.mChildrenBegin = (uint32_t) -1;
    node.mChildrenEnd   = (uint32_t) -1;
    node.mVal           = val++;

    mNodes.push_back(node);
  }

  curr = &mNodes[currInd];
  curr->mChildrenEnd = mNodes.size();

  cout << "Node " << curr->mVal << " is complete! mParentIndex = " << curr->mParentIndex << "; mIndex = " << curr->mIndex
       << "; mChildrenBegin = " << curr->mChildrenBegin << "; mChildrenEnd = " << curr->mChildrenEnd << endl;

  // recursively build children
  // NOTE: protect against reallocations of mNodes

  for (i = 0; i < numChildren; ++i)
  {
    curr = &mNodes[currInd];
    curr = &mNodes[curr->mChildrenBegin + i];
    val = buildFromStreamRecur(curr, val, in);
  }

  return val;  
}

int main(int argc, char **argv)
{
  Model m(&cin);

  m.semiPreOrderTraversal();
  m.semiPreOrderTraversalRecur(*m.getEnd());

  return 0;
}

整个树的非递归、前序遍历看起来像这样:

Node *curr = model.getRoot();  // TODO: handle empty tree
Node *next;
bool  shouldnt_visit_children;

while (1)
{
  // TODO: visit curr -- determine shouldnt_visit_children

  if (shouldnt_visit_children || NULL == (next = model.getChild(curr)))
  {
    // move to next sibling; if no siblings remain then ascend while no siblings remain

    while (NULL == (next = model.getNextSibling(curr)))
      if (model.getEnd() == (curr = model.getParent(curr)))
        goto DONE;

    curr = next;
  }
  else
    curr = next;  // descend to first child
}

DONE:;

话虽如此,我看不出有任何明显的原因说明这种实现(与您之前的链接结构相反)可能具有更好的缓存性能。向量的布局方式和访问方式将推动缓存性能。无论如何,我看不出有什么令人信服的理由,为什么以任何特定的方式进行布局,您不可能在类似地构建链接树时获得类似的结果。例如,如果您发现/相信以半预先排序的树遍历方式布置向量(即 - 向量的布局如下:end、root、root 的孩子、root 的第一个孩子的孩子、root 的第一个孙的孩子等)为您的应用程序提供最佳的缓存性能,那么很有可能使用相同的半预定构建顺序构建链接树将产生类似的结果,因为您的内存分配器可能会将您的树打包在内存中与您明确执行的类似方式。当然,通过您的方法,您可以确定地控制这一点,而不是依赖于您的结构和相关的分配器是否智能。

显式管理 Node 和 Mesh 在内存中的布局可能会为您带来更好的缓存性能,因为您可以“强制”它在内存中紧密地打包您的对象并强制执行您喜欢的访问模式/位置类型——但是体面的分配器可能会达到类似的结果,特别是如果树构建只在启动时完成一次。

如果您主要按照您的问题所暗示的那样进行预购遍历,那么我建议您以半预购方式布置您的存储向量,就像我上面描述的那样:end, root, root's children, root's第一个孩子的孩子,root 的第一个孙子的孩子等等。

PS - 如果您的遍历总是从根开始,那么您也可以消除 Node 中的 mParentIndex,而是在遍历树时构建一个显式的祖先堆栈,以便您在下降后遍历树(这可能是递归隐式所做的)。如果您需要能够从任何给定的随机节点在树中移动,那么您需要将父节点的索引存储在节点中。

编辑: 正如我在我的一个 cmets 中提到的,我提出的半预购布局还具有一个属性,即节点的所有后代 Mesh 都可以在一个简单的数字范围内表示[Node.mDescedantMeshBegin, Node.mDescendantMeshEnd) 当您按照建议单独存储网格时。因此,如果您需要或想要通过遍历树来构建可见 Mesh 的显式列表,那么无论何时找到可见节点,您都可以轻松地将整个可见后代 Mesh 范围合并到您的列表中。

EDIT2:我大幅更新了代码。我添加了一个基于半预订输入流的递归模型构建器。我修复了一些错误。最重要的是,我添加了一个对模型进行非递归、半前序遍历的函数。它不像真正的预购遍历那么简单,但它可能会让您感兴趣。递归版本可能要简单得多——我会考虑添加它。在我的测试中,节点的访问在 mNodes 中从左到右进行得非常好。然而,当我们向上爬回树时,内存访问有时会在存储阵列中倒退。我相信这些反向引用可以通过在遍历期间在堆栈上维护一个显式的副本数组(至少是它们的索引信息)来删除。必须重写 getParent() 和 getNextSibling() 等调用的功能才能使用此数组,而不是在存储向量本身中跳转,但可以这样做。然后你的 mNodes 的内存访问只会从左向右滑动,这可能接近于缓存性能的最佳值(假设你的树足够浅,堆栈上的祖先总是在缓存中,不会产生过度的缓存压力)。

EDIT3:我添加了一个递归半预定遍历,与迭代版本相比它是微不足道的。传递节点的索引部分的副本以保留在堆栈中也非常容易,这样当您的递归展开时,您实际上不会返回并再次访问存储阵列的早期部分。相反,您只需查看堆栈上的内容,几乎肯定会在缓存中 - 假设您的树不是超深+宽。您确实需要存储给定级别的所有子项的副本。仅存储您的直系祖先是不够的,您还必须存储他们的兄弟姐妹。使用此代码并以半预定方式布置存储向量,遍历中的所有内存访问都将在存储阵列上从左到右扫描,而不会回溯。我认为您将获得接近最佳的缓存性能。我不知道它会如何变得更好。

示例 input.txt:每个数字表示隐式当前节点在预购方式中有多少个子节点。在下面,sentinel端节点只有1个孩子,一个单数根(代码支持多根),根有5个孩子,根的第一个孩子有0个孩子,根的第二个孩子有2个孩子等等。我使用间距来指示眼睛的树结构,但对于程序本身来说不是必需的。

1
        5
                0
                2
                        7
                                0
                                0
                                0
                                1
                                        0
                                0
                                0
                                0
                        2
                                1
                                        0
                                0
                1
                        0
                4
                        1
                                0
                        2
                                1
                                        0
                                1
                                        0
                        0
                        0
                0

样本输出:

john-schultzs-macbook-pro:~ jschultz$ clear; ./a.out < input.txt

Node -1 is complete! mParentIndex = 0; mIndex = 0; mChildrenBegin = 1; mChildrenEnd = 2
Node 0 is complete! mParentIndex = 0; mIndex = 1; mChildrenBegin = 2; mChildrenEnd = 7
Node 1 is complete! mParentIndex = 1; mIndex = 2; mChildrenBegin = 7; mChildrenEnd = 7
Node 2 is complete! mParentIndex = 1; mIndex = 3; mChildrenBegin = 7; mChildrenEnd = 9
Node 6 is complete! mParentIndex = 3; mIndex = 7; mChildrenBegin = 9; mChildrenEnd = 16
Node 8 is complete! mParentIndex = 7; mIndex = 9; mChildrenBegin = 16; mChildrenEnd = 16
Node 9 is complete! mParentIndex = 7; mIndex = 10; mChildrenBegin = 16; mChildrenEnd = 16
Node 10 is complete! mParentIndex = 7; mIndex = 11; mChildrenBegin = 16; mChildrenEnd = 16
Node 11 is complete! mParentIndex = 7; mIndex = 12; mChildrenBegin = 16; mChildrenEnd = 17
Node 15 is complete! mParentIndex = 12; mIndex = 16; mChildrenBegin = 17; mChildrenEnd = 17
Node 12 is complete! mParentIndex = 7; mIndex = 13; mChildrenBegin = 17; mChildrenEnd = 17
Node 13 is complete! mParentIndex = 7; mIndex = 14; mChildrenBegin = 17; mChildrenEnd = 17
Node 14 is complete! mParentIndex = 7; mIndex = 15; mChildrenBegin = 17; mChildrenEnd = 17
Node 7 is complete! mParentIndex = 3; mIndex = 8; mChildrenBegin = 17; mChildrenEnd = 19
Node 16 is complete! mParentIndex = 8; mIndex = 17; mChildrenBegin = 19; mChildrenEnd = 20
Node 18 is complete! mParentIndex = 17; mIndex = 19; mChildrenBegin = 20; mChildrenEnd = 20
Node 17 is complete! mParentIndex = 8; mIndex = 18; mChildrenBegin = 20; mChildrenEnd = 20
Node 3 is complete! mParentIndex = 1; mIndex = 4; mChildrenBegin = 20; mChildrenEnd = 21
Node 19 is complete! mParentIndex = 4; mIndex = 20; mChildrenBegin = 21; mChildrenEnd = 21
Node 4 is complete! mParentIndex = 1; mIndex = 5; mChildrenBegin = 21; mChildrenEnd = 25
Node 20 is complete! mParentIndex = 5; mIndex = 21; mChildrenBegin = 25; mChildrenEnd = 26
Node 24 is complete! mParentIndex = 21; mIndex = 25; mChildrenBegin = 26; mChildrenEnd = 26
Node 21 is complete! mParentIndex = 5; mIndex = 22; mChildrenBegin = 26; mChildrenEnd = 28
Node 25 is complete! mParentIndex = 22; mIndex = 26; mChildrenBegin = 28; mChildrenEnd = 29
Node 27 is complete! mParentIndex = 26; mIndex = 28; mChildrenBegin = 29; mChildrenEnd = 29
Node 26 is complete! mParentIndex = 22; mIndex = 27; mChildrenBegin = 29; mChildrenEnd = 30
Node 28 is complete! mParentIndex = 27; mIndex = 29; mChildrenBegin = 30; mChildrenEnd = 30
Node 22 is complete! mParentIndex = 5; mIndex = 23; mChildrenBegin = 30; mChildrenEnd = 30
Node 23 is complete! mParentIndex = 5; mIndex = 24; mChildrenBegin = 30; mChildrenEnd = 30
Node 5 is complete! mParentIndex = 1; mIndex = 6; mChildrenBegin = 30; mChildrenEnd = 30
Beginning Semi-Pre-Order traversal of tree!
Visiting 0
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 0
Will visit children of 0
Visiting 1
Will visit sibling of 1
Visiting 2
Will visit sibling of 2
Visiting 3
Will visit sibling of 3
Visiting 4
Will visit sibling of 4
Visiting 5
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 1
No children to visit of 1
Trying to visit children of 2
Will visit children of 2
Visiting 6
Will visit sibling of 6
Visiting 7
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 6
Will visit children of 6
Visiting 8
Will visit sibling of 8
Visiting 9
Will visit sibling of 9
Visiting 10
Will visit sibling of 10
Visiting 11
Will visit sibling of 11
Visiting 12
Will visit sibling of 12
Visiting 13
Will visit sibling of 13
Visiting 14
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 8
No children to visit of 8
Trying to visit children of 9
No children to visit of 9
Trying to visit children of 10
No children to visit of 10
Trying to visit children of 11
Will visit children of 11
Visiting 15
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 15
No children to visit of 15
Reached end of siblings again -> completed traversal of tree rooted by parent 11
Moved back up to 11
Found a great^Nth uncle (12) to check for children to visit!
Trying to visit children of 12
No children to visit of 12
Trying to visit children of 13
No children to visit of 13
Trying to visit children of 14
No children to visit of 14
Reached end of siblings again -> completed traversal of tree rooted by parent 6
Moved back up to 6
Found a great^Nth uncle (7) to check for children to visit!
Trying to visit children of 7
Will visit children of 7
Visiting 16
Will visit sibling of 16
Visiting 17
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 16
Will visit children of 16
Visiting 18
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 18
No children to visit of 18
Reached end of siblings again -> completed traversal of tree rooted by parent 16
Moved back up to 16
Found a great^Nth uncle (17) to check for children to visit!
Trying to visit children of 17
No children to visit of 17
Reached end of siblings again -> completed traversal of tree rooted by parent 7
Moved back up to 7
Moved back up to 2
Found a great^Nth uncle (3) to check for children to visit!
Trying to visit children of 3
Will visit children of 3
Visiting 19
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 19
No children to visit of 19
Reached end of siblings again -> completed traversal of tree rooted by parent 3
Moved back up to 3
Found a great^Nth uncle (4) to check for children to visit!
Trying to visit children of 4
Will visit children of 4
Visiting 20
Will visit sibling of 20
Visiting 21
Will visit sibling of 21
Visiting 22
Will visit sibling of 22
Visiting 23
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 20
Will visit children of 20
Visiting 24
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 24
No children to visit of 24
Reached end of siblings again -> completed traversal of tree rooted by parent 20
Moved back up to 20
Found a great^Nth uncle (21) to check for children to visit!
Trying to visit children of 21
Will visit children of 21
Visiting 25
Will visit sibling of 25
Visiting 26
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 25
Will visit children of 25
Visiting 27
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 27
No children to visit of 27
Reached end of siblings again -> completed traversal of tree rooted by parent 25
Moved back up to 25
Found a great^Nth uncle (26) to check for children to visit!
Trying to visit children of 26
Will visit children of 26
Visiting 28
No more siblings to visit! Try to visit their children. Rewinding to oldest sibling 28
No children to visit of 28
Reached end of siblings again -> completed traversal of tree rooted by parent 26
Moved back up to 26
Moved back up to 21
Found a great^Nth uncle (22) to check for children to visit!
Trying to visit children of 22
No children to visit of 22
Trying to visit children of 23
No children to visit of 23
Reached end of siblings again -> completed traversal of tree rooted by parent 4
Moved back up to 4
Found a great^Nth uncle (5) to check for children to visit!
Trying to visit children of 5
No children to visit of 5
Reached end of siblings again -> completed traversal of tree rooted by parent 0
Moved back up to 0
Finished Semi-Pre-Order traversal of tree!

【讨论】:

  • @AlexisWilke 这当然是合法的。当然,您可能更喜欢另一种方式。
  • 添加了经过全面测试的代码和一个非递归的半前序遍历。
  • 添加了一个递归半前序遍历,其中存储在堆栈上的祖先索引数据的副本。这超级简单,我看不出你是怎么打败它的。
  • 我认为 6502 的回答更胜一筹。纯前置布局和递归遍历优于我推荐的。
【解决方案2】:

我为 Qt 实现了一个尚未完全兼容的 XPath 版本,因此它包括在不需要使用递归函数的情况下遍历节点树的方法。有一个相关部分的副本使用一种算法来遍历给定节点的所有后代(最终包括 Self)。

如果您想查看whole implementation(第 5480 行附近),请访问SourceForge.net GIT repository。最新的现在在github

    case AXIS_DESCENDANT:
    case AXIS_DESCENDANT_OR_SELF:
        switch(node_type)
        {
        case NODE_TYPE_NODE:
        case NODE_TYPE_ELEMENT:
            {
                // as far as I know the type node is considered to be
                // the same as elements (at least in XPath 1.0)
                QDomNode node(context_node);
                if(axis == AXIS_DESCENDANT_OR_SELF
                && (local_part.isEmpty() || local_part == context_node.toElement().tagName())
                && (any_prefix || prefix == context_node.prefix()))
                {
                    result.push_back(context_node);
                }
                while(!node.isNull())
                {
                    QDomNode next(node.firstChild());
                    if(next.isNull())
                    {
                        next = node;
                        while(!next.isNull()) // this should never happend since we should bump in context_node first
                        {
                            if(next == context_node)
                            {
                                // break 2;
                                goto axis_descendant_done;
                            }
                            QDomNode parent(next.parentNode());
                            next = next.nextSibling();
                            if(!next.isNull())
                            {
                                break;
                            }
                            next = parent;
                        }
                    }
                    // the local_part & prefix must match for us to keep the node
                    node = next;
                    if((local_part.isEmpty() || local_part == node.toElement().tagName())
                    && (any_prefix || prefix == node.prefix()))
                    {
                        // push so nodes stay in document order
                        result.push_back(node);
                    }
                }
axis_descendant_done:;
            }
            break;

        default:
            throw QDomXPathException_NotImplemented(QString("this axis (%1) does not support this node type (%2)").arg(static_cast<int>(axis)).arg(static_cast<int>(node_type)).toStdString());

        }
        break;

【讨论】:

    【解决方案3】:

    您可以将树表示为内存中深度优先遍历顺序的单个数组

    struct Node {
        ... node data ...
        int next;
    };
    
    std::vector<Node> nodes;
    

    next字段保持下一个节点的索引在相同或更高级别;节点的第一个子节点没有明确说明,因为它是序列中当前节点之后的节点(除非 next 也指向它;在这种情况下,当前节点没有子节点)。 例如在树中:

    红色箭头表示next 指向的位置。

    例如用于渲染的遍历变成:

    void check(int ix) {
        switch(nodes[ix].visibility()) {
            case VISIBLE:
                // Draw whole subtree, no more checking needed
                for (int i=ix,e=nodes[ix].next; i!=e; i++) {
                    nodes[i].draw();
                }
                break;
            case PARTIALLY_VISIBLE:
                nodes[ix].draw();
                for (int c=ix+1, e=nodes[ix].next; c!=e; c=nodes[c].next) {
                    check(c);
                }
                break;
        }
    }
    

    同样的代码也可以通过保留一个显式堆栈来转换为非递归代码(不知道为什么这是一个好主意,除非节点操作和检查非常快或者树非常深)。

    【讨论】:

    • 我同意预购布局和遍历优于我推荐的半预购。我对 Node 能够明确列出它的孩子是谁的想法很感兴趣,而不是像你在 PARTIALLY_VISIBLE 循环中那样迭代地推导它们。
    猜你喜欢
    • 2018-07-12
    • 2014-03-11
    • 1970-01-01
    • 2017-05-11
    • 2014-12-21
    • 2019-08-23
    • 2010-11-20
    • 2013-07-23
    • 1970-01-01
    相关资源
    最近更新 更多