了解深度优先遍历

时间:2017-01-10 11:58:31

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

使用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注释中)和为三个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评论中)。

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

我的问题:

1)您是否看到4步算法中的逻辑缺陷和所有三次遍历的代码?

2)在上面的代码中,在访问节点之前是否需要进行条件检查?

注意:遍历算法中的初学者

2 个答案:

答案 0 :(得分:1)

  

1)您是否看到4步算法中的逻辑缺陷和所有三次遍历的代码?

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

但是,如M Ohem所述,访问字段最初可能设置为false,但在任何遍历之后,它将被设置为true。因此,您将无法再次遍历树。

树遍历通常使用递归函数完成,或者如果要使用迭代函数,则需要push和pop to stack。

  

2)在上面的代码中,在访问节点之前是否需要进行条件检查?

是的,在再次访问节点之前,您必须检查节点是否被访问,否则遍历将是错误的。

编辑 - 关于这一点的进一步说明。 让我们说我们有一棵树

      a
    b   c
  d   e
f  g

条件,检查不适用于预订遍历。

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

现在,root = d。由于条件检查不存在,再次访问d,这是错误的,然后你转到第二个if条件并访问f。再次root = root->parent和root = d,再次打印d。

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

答案 1 :(得分:1)

显然,您需要一个二元树遍历函数,该函数不是递归的,并且不使用堆栈。 (你没有在问题中明确说明,但你的意见暗示了这一点。)

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

递归函数很简单:

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标志。这种方法不合适,因为atraversal会改变树的状态。您必须在遍历之前重置此状态,但为此,您必须遍历树。哎哟!

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

但是有一个解决方案:您的树节点有一个指向其父节点的链接。您可以保留指向先前访问过的节点的链接,然后根据该节点确定下一步的位置:

  • 从上一个null节点开始。
  • 永远不要访问任何null的子节点。否则,您将无法区分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有点像kludge,但它是必需的,以便函数知道迭代器是否已经被提升:我们不会在第一次调用中转到父级。

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

PostIter iter = {head};

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

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

编写预订和反转的迭代代码留给读者练习。 :)