【问题标题】:Understanding Depth First Traversal了解深度优先遍历
【发布时间】:2017-01-10 11:58:30
【问题描述】:

使用给定的 BST 表示,

/****************tree.h ***********************/
typedef void* (*ProcessItem)(void *, void *);

/**************** BSTNode.c ************************/
typedef struct BinarySearchTreeNode{
  void *key;
  void *value;
  bool visit; // For traversals
  struct BinarySearchTreeNode *parent;
  struct BinarySearchTreeNode *left;
  struct BinarySearchTreeNode *right;
}Node;

typedef struct Tree{ //BST
  Node *root;
  int size;
}Tree;
static void visitFunc(ProcessItem, Node *);

作为学习的一部分,下面是详细的算法(在 C cmets 中)和为三个 DFS 遍历编写的相应代码。


预购遍历

/*
  Pre-order traversal of BST and Binary tree: Node -> Left -> Right
  0) Assuming node pointer is pointing to root node, which is, not NULL.

  1) Visit node, if not visited

  2) From that node,
     If left sub-tree exists, and
     If root node of that left sub-tree never visted,
     then traverse to root node of left sub-tree.
     Go to step 1 and continue until a node (that has visted root of left sub-tree) or (that has no left sub-tree exists)

  3) From that node,
     If right sub-tree exists, and
     It root node of right sub-tree not visited,
     then traverse to root node of right sub-tree.
     Go to step 1 and continue, until a node (that has visited root of right sub-tree) or (that has no right sub-tree exists).

  4) We reach this step, because,
     Either left/right sub-tree exist but visited
           or
     left and right sub trees does not exist

     Reach parent node and go to step-1 for applying pre-order traversal on that node

*/
static void preOrderTraverse(Node *n, ProcessItem action){

  if (n == NULL){
    return; // if tree is empty, then go back
  }

  Node *root = n;                              // |Step-0

  while(root !=  NULL){

    if(root->visit == false){
      visitFunc(action, root);                 // |Step-1: Visit node, if not visited
    }

    if(root->left != NULL &&                   // |Step-2: if left sub-tree exists, and
       (root->left->visit == false) ){         // |Step-2: If root node of that left sub-tree's never visted,
      root = root->left;                       // |Step-2: then traverse to root node of left sub-tree.
      continue;                                // |Step-2: Go-to step 1
    }

    if(root->right != NULL &&                  // |Step-3: if right sub-tree exists,
       (root->right->visit == false) ){        // |Step-3: If root node of right sub-tree not visited,
      root= root->right;                       // |Step-3: then traverse to root node of right sub-tree.
      continue;                                // |Step-3: Go-to step 1
    }

    /*
     If the instruction pointer points to below insruction, then that means,
       Either left/right sub-tree exist but visited
           or
       left and right sub trees are empty

     What can be done now?
     Go to parent's tree root node and apply preOrderTraversal
    */
    root = root->parent;                       // |Step-4 Reach parent node.

  }
}

后序遍历

/*
  Algorithm: Post-order traversal of BST and Binary tree: Left -> Right -> Node

  0) Assuming node pointer is pointing to root node, which is, not NULL.

  1) From that node,
     If left sub-tree exists, and
     If root node of that left sub-tree never visted,
     then traverse to root node of left sub-tree.
     Repeat step 1 until a node (that has visted root of left sub-tree) or (that has no left sub-tree exists)

  2) From that node,
     If right sub-tree exists, and
     If root node of that right sub-tree never visted,
     then traverse to root node of right sub-tree.
     Go to step 1 and continue until a node (that has visited root of right sub-tree) or (that has no right sub-tree exists)

  3) Visit that node, if not visited

  4) We reach this step, because,
     Either left/right sub-tree exist but all visited
           or
     left and right sub trees does not exist

     Reach parent node and go to step-1 for applying post-order traversal on that node

*/
static void postOrderTraverse(Node *n, ProcessItem action){

  if (n == NULL){
    return; // if tree is empty, then go back
  }

  Node *root = n;                              // |Step-0

  while(root !=NULL){

    while(root->left != NULL &&                // |Step-1: If left sub-tree exists, and
          (root->left->visit == false)){       // |Step-1: If root node of that left sub-tree never visted,
      root=root->left;                         // |Step-1: then traverse to root node of left sub-tree.
    }

    if(root->right != NULL &&                  // |Step-2: If right sub-tree exists, and
       (root->right->visit == false)){         // |Step-2: If root node of that right sub-tree never visted,
      root=root->right;                        // |Step-2: then traverse to root node of right sub-tree.
      continue;
    }

    visitFunc(action, root);                   // |Step-3: Visit node, if not visited

    /*
      If the instruction pointer points to below insruction, then that means,
       Either left/right sub-tree exist but all visited
           or
       left and right sub trees are empty

     What can be done now?
     Go to parent's tree root node and apply postOrderTraversal
    */

    root = root->parent;                       // |Step-4: Reach parent node.
  }
}

中序遍历

 /*
   Algorithm: In-order traversal of BST and Binary tree: Left -> Node -> Right

   0) Assuming node pointer is pointing to root node, which is, not NULL.

   1) From that node,
      If left sub-tree exists, and
      If root node of left subtree is never visted,
      then traverse to root node of left sub-tree.
      Repeat step1 until a node (that has visited root of left sub-tree) or (has no left sub-tree exists)

   2) Visit that node, if not visited.

   3) From that node,
      If right sub-tree exists,
      If root node of right sub-tree never visited,
      then traverse to root node of right sub-tree
      Goto step-1 and continue until a node (that has visited root of right sub-tree) or (has no right sub-tree exists).

   4) We reach this step, because,
      Either left/right sub-tree exists but visited
         or
      Either left/right sub-tree does not exist.

      What can be done now?
      Reach parent node and go to step-1 for applying In-order traversal on that node.

  */
static void inOrderTraverse(Node *n, ProcessItem action){

  if (n == NULL){
    return; // if tree is empty, then go back
  }

  Node *root = n;                                // |Step-0


  while(root != NULL){

    while(root->left != NULL &&                  // |Step-1: If left sub-tree exists, and
          (root->left->visit == false) ){        // |Step-1: If root node of left subtree is never visted,
      root = root->left;                         // |Step-1: then traverse to root node of right sub-tree
    }

    if(root->visit == false){
      visitFunc(action, root);                   // |Step-2: Visit node, if not visited.
    }

    if(root->right != NULL &&                    // |Step-3: If right sub-tree exists, and
       (root->right->visit == false) ){          // |Step-3: If root node of right sub-tree never visited,
      root = root ->right;                       // |Step-3: then traverse to root node of right sub-tree
      continue;                                  // |Step-3: Go to step 1
    }

    /*
      If instruction pointer reaches below instruction, then,
      Either left/right sub-tree exists but all visited
         or
      Either left/right sub-tree does not exist.
    */

    root = root->parent;                         // |Step-4: Reach parent node
  }
}

其中visitFuncBSTNode.c 中定义,processItem 来自用户,

static void visitFunc(ProcessItem processItem, Node *node){

  if(node->visit == TRUE){
    return;
  }
  node->visit=true;
  processItem(node->key, node->value);
  return;
}

背景:

很明显,以上 DFS 算法都可以在线获得。但是,根据我的学习,这些算法所需的细节水平缺失。我认为,上述算法(在 C cmets 中)已经涵盖了这些缺乏细节。

强调提到的算法(在 C cmets 中)和使用parent 指针而不是显式堆栈的相应遍历代码,

我的问题:

1) 您是否发现所有三个遍历的 4 步算法和代码存在逻辑缺陷?

2) 上述代码中,访问节点前是否需要条件检查?

注意:遍历算法的初学者

【问题讨论】:

  • 我看到一个编译时错误,因为VisitFunc 是一种类型。
  • 是的,它是一种类型。你可以看到上面的typedef。我没有包含visitFunc的定义。
  • 并将其设为小写不会做任何事情,因为您将参数作为action 传递。您是否至少编译过一次?
  • @StoryTeller 抱歉,ProcessItem 输入的不是visitFunc 更新代码
  • 这些函数要求在遍历树之前重置visit 字段。如果不通过遍历,您将如何做到这一点?

标签: c data-structures binary-search-tree tree-traversal


【解决方案1】:

1) 您是否发现所有三个遍历的 4 步算法和代码存在逻辑缺陷?

我检查了预购遍历。看起来不错。

但是,正如 M Ohem 所提到的,访问字段最初可能设置为 false,但经过任何遍历后,它将设置为 true。因此,您将无法再次遍历树。

树遍历通常使用递归函数完成,或者如果您想使用迭代函数,则需要推送和弹出堆栈。

2) 上述代码中,访问节点前是否需要条件检查?

是的,在再次访问之前必须检查一个节点是否被访问过,否则遍历会出错。

编辑 - 关于这一点的进一步解释。 假设我们有一棵树

      a
    b   c
  d   e
f  g

还有条件,检查不存在预购遍历。

你访问,首先是 a,然后是 b,然后是 d,然后是 f,每次都达到第一个 if 条件,然后继续。在 root=f 处,你到达了下一个 if 条件,它是 false,你到达了最终条件 root = root->parent

现在,根 = d。由于没有条件检查,所以再次访问d,这是错误的,然后你去第二个if条件,访问f。再次root = root->parent 和 root = d,然后再次打印 d。

所以,你看到遍历是错误的。

【讨论】:

  • 通常在 School 中使用递归函数,但从不在生产代码中使用,特定于 DFS 遍历。
  • 对于您的第二点答案,我仍然怀疑,是否需要检查条件
  • 你检查过订单遍历算法吗?我怀疑第2步。我不接触这些主题的书籍,所以我发布了这个查询。
  • @overexchange - 如果递归是不可接受的,而且在许多情况下是不可接受的,那么使用堆栈是一个不错的选择。另外,如果在前序遍历中去掉if(root->visit == FALSE)条件,会在返回路径上多次访问节点。
  • 对于第二点,我认为,不,根据我的理解。在遍历的 L 和 R 部分中检查避免多次访问。
【解决方案2】:

显然,您需要一个非递归且不使用堆栈的二叉树遍历函数。 (您没有在问题中明确说明,但您的 cmets 建议这样做。)

让我们看一下后序遍历。 (这个答案可以很容易地扩展到其他深度优先遍历,但后序是最简单的,所以我们只看这个。)

递归函数很简单:

typedef struct Node Node;

struct Node {
    int key;
    Node *left;
    Node *right;
    Node *parent;
};

void postorder_rec(const Node *nd, void (*func)(const Node *nd))
{
    if (nd) {
        postorder_rec(nd->left, func);
        postorder_rec(nd->right, func);
        func(nd);
    };
}

这个函数是轻量级的,并且有很好的递归深度。你说你不会在生产代码中使用递归版本,但是在这样的代码中,你的想法是保持树的平衡,这样你就可以拥有大约 100 万个节点,树(因此递归)深度为 20。

您已提议向您的节点添加visited 标志。这种方法不合适,因为遍历会改变树的状态。您必须在遍历之前重置此状态,但要这样做,您必须遍历树。哎哟!

根据树根的状态将标志视为“已访问”或“未访问”的建议可能是一种解决方法,但如果由于某种原因您想要在中间停止遍历,或者如果您在遍历之间添加新节点。这种方法不可靠。此外,您总是需要为每个节点携带额外的标志。

不过有一个解决方案:您的树节点有一个指向其父节点的链接。您可以保留指向先前访问过的节点的链接,然后根据该链接决定下一步要去哪里:

  • 从前一个空节点开始。
  • 永远不要访问任何为空的子节点。否则你将无法区分 null 子节点和根节点的父节点,父节点也为 null。
  • 当先前访问的节点是当前节点的父节点时:
    • 如果有左孩子,请访问下一个;
    • 否则,如果有正确的孩子,请访问下一个;
    • 否则,调用该函数并返回父级。
  • 当之前访问过的节点是当前节点的左孩子时:
    • 如果有正确的孩子,请访问下一个;
    • 否则,调用该函数并返回父级。
  • 当之前访问过的节点是当前节点的右孩子时:
    • 调用函数并返回父级。
  • 当节点为空时停止 - 您已上升到根的父节点。

这些条件可以简化,函数如下:

void postorder_iter(const Node *nd, void (*func)(const Node *nd))
{
    const Node *prev = NULL;

    while (nd) {
        const Node *curr = nd;

        if (prev == nd->parent && nd->left) {
            nd = nd->left;
        } else if (prev != nd->right && nd->right) {
            nd = nd->right;            
        } else {
            func(nd);
            nd = nd->parent;
        }

        prev = curr;
    }
}

事实上,迭代函数并没有真正的好处——递归函数很简单,不需要在节点中链接到它的父节点。

但是这个函数可以用作保持遍历状态的迭代器的基础:

typedef struct PostIter PostIter;

struct PostIter {
    const Node *nd;
    const Node *prev;
    int count;
};

const Node *postorder_next(PostIter *iter)
{
    if (iter->nd == NULL) return NULL;

    if (iter->count) {
        (iter->prev) = iter->nd;
        iter->nd = iter->nd->parent;
    }

    while (iter->nd) {
        const Node *prev = iter->prev;
        const Node *nd = iter->nd;

        if (prev == nd->parent && nd->left) {
            iter->nd = nd->left;
        } else if (prev != nd->right && nd->right) {
            iter->nd = nd->right;            
        } else {
            iter->count++;
            return nd;
        }

        iter->prev = nd;
    }

    return NULL;
}

这里,遍历的状态,由迭代版本中的局部变量描述,被捆绑到一个迭代器结构中。我们现在从函数返回,而不是调用函数。 count 有点杂乱无章,但需要它以便函数知道迭代器是否已经被推进:我们不会在第一次调用时转到父级。

您现在可以像这样使用迭代器:

PostIter iter = {head};

while (postorder_next(&iter)) {
    printf("%d -> ", iter.nd->key);
}
puts("nil");

您现在可以将访问代码放入循环体中,而不是提供必须在其他地方定义的回调函数,这当然可以访问在循环外声明的变量。遍历代码变得更复杂了,但是调用代码现在简单了很多。

为预排序和中序遍历编写迭代代码留给读者作为练习。 :)

【讨论】:

  • postorder_recur()模式下,在访问节点之前,是否需要检查节点是否已经被访问过?
  • 不!当你总是递归到孩子时,你不可能两次访问同一个节点;树的布局确保了这一点。您将节点标记为已访问的想法仅在您想通过具有循环和双向边的任意图找到路径时才有用。树的遍历是一个简单的任务,上面所有的代码都是完整的。我什至测试过它们。你真的试过了吗?如果是这样,您是否发现节点被多次访问?
猜你喜欢
  • 1970-01-01
  • 2019-08-10
  • 2019-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多