BFS (Level Order Traversal)

本文介绍如何使用队列辅助实现树的广度优先遍历,并对比了栈辅助下的遍历顺序,深入探讨了递归与迭代在树遍历中的应用。

Breadth-First Traversal of a Tree


  1. Helper data structure:

    Certain programming problems are easier to solve using multiple data structures.

    For example, testing a sequence of characters to determine if it is a palindrome (i.e., reads the same forward and backward, like "radar") can be accomplished easily with one stackand one queue. The solution is to enter the sequence of characters into both data structures, then remove letters from each data structure one at a time and compare them, making sure that the letters match.

    In this palindrome example, the user (person writing the main program) has access to both data structures to solve the problem. Another way that 2 data structures can be used in concert is to use one data structure to help implement another.

    We will examine how a common data structure can be used to help traverse a tree in breadth-first order.

  2. Depth-first traversal:

    We have already seen a few ways to traverse the elements of a tree. For example, given the following tree:

          tree
          ----
           j    <-- root
         /   \
        f      k
      /   \      \
     a     h      z
      \
       d
    

    preorder traversal would visit the elements in the order: j, f, a, d, h, k, z.

    This type of traversal is called a depth-first traversal. Why? Because it tries to go deeper in the tree before exploring siblings. For example, the traversal visits all the descendants of f(i.e., keeps going deeper) before visiting f's sibling k (and any of k's descendants).

    As we've seen, this kind of traversal can be achieved by a simple recursive algorithm:

    PREORDER-TRAVERSE(tree)
    
    if (tree not empty)
      visit root of tree
      PREORDER-TRAVERSE(left subtree)
      PREORDER-TRAVERSE(right subtree)
    

    The 2 other traversal orders we know are inorder and postorder. An inorder traversal would give us:  a, d, f, h, j, k, z. A postorder traversal would give us: d, a, h, f, z, k, j.

    Well, inorder and postorder traversals, like a preorder traversal, also try to go deeper first...

    For example, the inorder traversal visits a and d before it explores a's sibling h. Likewise, it visits all of j's left subtree (i.e., "a, d, f, h") before exploring j's right subtree (i.e., "k, z"). The same is true for the postorder traversal. It visits all of j's left subtree (i.e., "d, a, h, f") before exploring any part of the right subtree (i.e., "z, k").

  3. Breadth-first traversal:

    Depth-first is not the only way to go through the elements of a tree. Another way is to go through them level-by-level.

    For example, each element exists at a certain level (or depth) in the tree:

          tree
          ----
           j         <-- level 0
         /   \
        f      k     <-- level 1
      /   \      \
     a     h      z  <-- level 2
      \
       d             <-- level 3
    

    (Computer people like to number things starting with 0.)

    So, if we want to visit the elements level-by-level (and left-to-right, as usual), we would start at level 0 with j, then go to level 1 for f and k, then go to level 2 for a, h and z, and finally go to level 3 for d.

    This level-by-level traversal is called a breadth-first traversal because we explore the breadth, i.e., full width of the tree at a given level, before going deeper.

    Now, how might we traverse a tree breadth-first? We'll need some other mechanism than the ones we've already used since preorder, inorder and postorder traversals don't produce breadth-first order.

  4. Why breadth-first:

    You may be thinking: "Why would we ever want to traverse a tree breadth-first?" Well, there are many reasons....

    Tree of Officers
    Suppose you have a tree representing some command structure:
                   Captain Picard
                 /                \
        Commander Riker       Commander Data
          /         \               |
     Lt. Cmdr.   Lt. Cmdr.      Lt. Cmdr.
     Worf        LaForge        Crusher
         |                          |
    Lieutenant                  Lieutenant
    Cameo-Appearance            Selar
    

    This tree is meant to represent who is in charge of lower-ranking officers. For example, Commander Riker is directly responsible for Worf and LaForge. People of the same rank are at the same level in the tree. However, to distinguish between people of the same rank, those with more experience are on the left and those with less on the right (i.e., experience decreases from left to right).

    Suppose a fierce battle with an enemy ensues. If officers start dropping like flies, we need to know who is the next person to take over command. One way to trace the path that command will follow is to list the officers in the tree in breadth-first order. This would give:

    1. Captain Picard
    2. Commander Riker
    3. Commander Data
    4. Lt. Cmdr. Worf
    5. Lt. Cmdr. LaForge
    6. Lt. Cmdr. Crusher
    7. Lieutenant Cameo-Appearance
    8. Lieutenant Selar
    Game Tree
    Another time when breadth-first traversal comes in handy is with game trees. Suppose we have a tree for a game of chess. In other words, levels of the tree alternately represent possible moves by you, and then by your opponent, and then by you...
                   current state of game
            /                |             \
       move                move     ...   move    <-- your moves
       queen's             king's         queen
       bishop              knight
     /     |    \         /    |   \        |
    move  move   ...   move  move  ...     ...    <-- opponent's moves
    king  queen's      king  king's
          rook               knight
            |                  |
            .                  .
            .                  .
            .                  .
    

    You have exactly 1 minute to decide on a move. Now, which would be a better use of that time: exploring the branch where you "move your queen's bishop" to its fullest extent (go deep) or explore each of your possible next move first, and then your opponent's responses (breadth-first).

    In this case, traversing the game tree breadth-first makes more sense than exploring one move infinitely (depth-first) before exploring another move.

  5. Help for breadth-first traversing:

    Let's return to example trees that are binary and that just hold characters.

    As we've seen, the recursive tree traversals go deeper in the tree first. Instead, if we are going to implement a breadth-first traversal of a tree, we'll need some help....Perhaps one of the data structures we already know can be of assistance?

    How to use helper data structure: What we'll do is store each element in the tree in a data structure and process (or visit) them as we remove them from the data structure.

    We can best determine what data structure we need by looking at an example:

       f
     /   \
    a     h
     \
      d
    

    When we are at element f, that is the only time we have access to its 2 immediate children, a and h. So, when we are at f, we'd better put its children in the data structure. Obviously then, f must have been in the data structure before them (i.e., first), since we'd have put f in when we were at f's parent.

    So, if we put the parent in the data structure before its children, what data structure will give us the order we need? In other words, to explore the tree breadth-first, do we want the children to be removed from the data structure first or the parent to be removed first?

    Answer: A queue will give us the order we want! A queue enforces first-in-first-out order, and we want to process the first thing in the data structure, the parent, before its descendents.

  6. Using one data structure to implement another:

    The organization of a program that uses a breadth-first traversal might look like:

    main program        tree.h              tree.c
    ------------        ------              ------
    
    call                TreeBreadthFirst    TreeBreadthFirst
    TreeBreadthFirst    prototype           definition
    

    In other words, the main program needs to call some function that performs a breadth-first traversal, like TreeBreadthFirst(). This means that the interface (tree.h) must provide a prototype for such a function. Also, the implementation (tree.c), which the main program has no direct access to, must define the function TreeBreadthFirst().

    If we use a queue to help us implement the breadth-first traversal, then we must extend the picture...

    ...     tree.c             queue.h         queue.c
            ------             -------         -------
    
    ...     TreeBreadthFirst   queue funcs.    queue funcs.
            definition (uses   prototypes      definitions
            a queue)
    

    The tree implementation will use types and functions from the queue interface. The queue implementation will provide definitions for those functions, but they are hidden from the user of the queue--here, the user of the queue is the tree implementation!

    Finally, since the main program cannot see the implementation of the tree, it won't even know that a queue is involved and won't have any access to that queue.

  7. Connecting the queue to the tree:

    Previously, we've seen that the following organization of types can be used for a tree of characters:

    tree.h                          tree.c
    ------                          ------
                                    #include "tree.h"
                                    #include "queue.h"
    
    typedef char treeElementT;      typedef struct treeNodeTag {
                                      treeElementT element;
                                      struct treeNodeTag *left,
                                                         *right;
                                    } treeNodeT;
    
    typedef struct treeCDT          typedef struct treeCDT {
            *treeADT;                 treeNodeT *root;
                                    } treeCDT;
    

    The one adjustment needed is that the tree implementation will have to include queue.h, since it uses a queue.

    Now, the types for a queue have similar organization:

    queue.h                         queue.c
    -------                         -------
                                    #include "queue.h"
    
    type-of-an-element
    
    abstract-type-of-a-queue        concrete-type-of-a-queue
    
    Of course, we know what the abstract type will be and that the concrete type will be a structure:
    queue.h                         queue.c
    -------                         -------
                                    #include "queue.h"
    
    typedef ??
            queueElementT;
    
    typedef struct queueCDT         typedef struct queueCDT {
            *queueADT;                ??
                                    } queueCDT;
    

    It's actually irrelevant (to those writing the tree) what the internals of the queue are, i.e., what a queueCDT really holds and what other types it may need for the implementation (e.g.,queueNodeTs?).

    However, we still must determine what a queueElementT is! This is what will make the queue useful to help traverse a tree.

    So, we are really asking: What type of thing should the queue store?

    We can answer that question by asking:  What type is present throughout the tree that we can use to refer both to the top-level tree and each and every subtree?

    an ADT
      |     --------
      +---> | root | a CDT
            |  |   |
            ---+----
               |
               v
             -----
             | j |
             |---|
             | | |
             /---\
            v     v
        -----     -----
        | f |     | k |
        |---|     |---|
        | | |     |0| |
        /---\     ----\
       v     v         v
     ...     ...        ...
    

    Answer: treeNodeT *! It is the only type that allows us to refer to both the top-level and all subtrees.

    Therefore, the queue must be able to store things of type treeNodeT *.

    There is more than one way we can achieve this, we could:

    • Move the type definition for treeNodeT into queue.h. Then we could do:
      typedef treeNodeT *queueElementT;
      

      That would work since we'd get the type treeNodeT in tree.c (where we need it) when we include queue.h.

      It's not ideal though, since treeNodeT really belongs in the tree implementation file.

    • Leave treeNodeT in tree.c, but refer to it in queue.h.

      Remember that we can refer to a pointer to a structure that is not defined, as in:

      typedef struct treeNodeTag *queueElementT;
      

      As long as we don't try to dereference that pointer. That would work because the queue will just store these pointers, not dereference them.

    • Finally, we could use the generic pointer type, void *, as the element type.

      void * can take on the value of any pointer (i.e., can store the address of anything), so we could use it to store the value of a treeNodeT *.

    We'll choose the last option, because it produces the most general queue.

    So, we can complete the queue types as:

    queue.h                         queue.c
    -------                         -------
                                    #include "queue.h"
    
    typedef void *
            queueElementT;
    
    typedef struct queueCDT         typedef struct queueCDT {
            *queueADT;                ??  /* Not important to tree! */
                                    } queueCDT;
    

  8. TreeBreadthFirst() function:

    Finally, we can implement the function TreeBreadthFirst(), which traverses a tree using a queue to achieve breadth-first order. Right now, we don't care what it does when its visits an element.

    This function will have to receive a tree via an ADT...and doesn't need to return anything, so its prototype looks like:

    void TreeBreadthFirst(treeADT tree);
    

    Now, the essence of the algorithm is to use a queue, in other words, to process nodes while there are node pointers left in the queue still to be processed.

    So, the core of the function will be looping through the contents of the queue:

    while (!QueueIsEmpty(queue)) {
      ...
    

    However, pointers to all the nodes in the tree won't be in the queue at once. We'll have to place them in the queue when we have access to them (i.e., we only get access to a child when we are at its parent).

    Here's one solution to the problem:

    void TreeBreadthFirst(treeADT tree)
    {
      /* Temporary queue. */
      queueADT queue;
      /* Points to node we are processing. */
      treeNodeT *traverse;
    
      if (tree->root == NULL)
        return;  /* Nothing to traverse. */
    
      /* Create a queue to hold node pointers. */
      queue = QueueCreate();
    
      /*
       * Gotta put something in the queue initially,
       * so that we enter the body of the loop.
       */
      QueueEnter(queue, tree->root);
    
      while (!QueueIsEmpty(queue)) {
        traverse = QueueDelete(queue);
    
        Visit the node pointed to by traverse.
    
        /*
         * If there is a left child, add it
         * for later processing.
         */
        if (traverse->left != NULL)
          QueueEnter(queue, traverse->left);
    
        /*
         * If there is a right child, add it
         * for later processing.
         */
        if (traverse->right != NULL)
          QueueEnter(queue, traverse->right);
      }
    
      /* Clean up the queue. */
      QueueDestroy(queue);
    }
    

    Notice that, because we can solve this problem with iteration (instead of recursion), we do not need a wrapper function that pulls out the top-level node pointer from the CDT.

  9. Using a stack instead:

    Suppose we replaced the use of a queue with a stack...

    void TreeDifferentTraversal(treeADT tree)
    {
      /* Temporary stack. */
      stackADT stack;
      /* Points to node we are processing. */
      treeNodeT *traverse;
    
      if (tree->root == NULL)
        return;  /* Nothing to traverse. */
    
      /* Create a stack to hold node pointers. */
      stack = StackCreate();
    
      /*
       * Gotta put something in the stack initially,
       * so that we enter the body of the loop.
       */
      StackPush(stack, tree->root);
    
      while (!StackIsEmpty(stack)) {
        traverse = StackPop(stack);
    
        Visit the node pointed to by traverse.
    
        /*
         * If there is a left child, add it
         * for later processing.
         */
        if (traverse->left != NULL)
          StackPush(stack, traverse->left);
    
        /*
         * If there is a right child, add it
         * for later processing.
         */
        if (traverse->right != NULL)
          StackPush(stack, traverse->right);
      }
    
      /* Clean up the stack. */
      StackDestroy(stack);
    }
    

    What kind of traversal does this new function produce?

    Answer: A preorder traversal! However, it gives us right-to-left order, since the right child will come out of the stack before the left child.


    Why does a stack (using iteration) give us a preorder traversal? Well, suppose we had a recursive function Traverse(), which performed a preorder traversal. Suppose you used this recursive function on the following tree:
          tree
          ----
           j
         /   \
        f      k
      /   \      \
     a     h      z
      \
       d
    

    Within each call of Traverse() on a particular element (or whatever an element is held in, e.g., a node, an array element, etc.), there would be recursive calls to traverse its children. (Below indentation indicates another level of recursion.)

    Traverse(j)
      Traverse(f)
        Traverse(a)
          Traverse(d)
        Traverse(h)
    

    Each of these recursive calls is a function call. When you call a function, the parameter(s) of the function actually get pushed onto something called the call stack.

    So, the sequence of recursive calls above has the following effect on the call stack:

                             j
    call                ----------
    Traverse(j)         call stack
    
                             f
                             j
    call                ----------
    Traverse(f)         call stack
    
    
                             a
                             f
                             j
    call                ----------
    Traverse(a)         call stack
    
    
                             d
                             a
                             f
                             j
    call                ----------
    Traverse(d)         call stack
    
    
                             a
                             f
                             j
    return from         ----------
    Traverse(d)         call stack
    
                             f
                             j
    return from         ----------
    Traverse(a)         call stack
    
    
                             h
                             f
                             j
    call                ----------
    Traverse(h)         call stack
    
    ...
    

    When the function is called, its parameter(s) go on the top of the call stack, and when the function returns, its parameter(s) get popped off of the call stack.

    In other words, with recursive traversal functions, there is really a stack helping with the traversal.

<think>我们正在讨论二叉树的相关知识点。用户之前已经询问了二叉树的核心概念和专业名词,现在用户提出了几个具体问题: 1. 满二叉树和完全二叉树的区别 2. BFS path的含义 3. Pre-order traversal和In-order traversal的定义 根据用户提供的引用,我们可以参考以下信息: 引用[1]提到了二叉树的遍历方式,包括前序、中序、后序和层序。 引用[2]进一步解释了前序遍历、中序遍历和后序遍历,以及广度优先搜索(BFS)。 引用[3]和[4]则提供了层序遍历的代码示例。 因此,我们将针对这三个问题逐一解答。 1. 满二叉树(Full Binary Tree)和完全二叉树(Complete Binary Tree)的区别: - 满二叉树:每个结点都有0个或2个子结点,也就是说,除了叶子结点外,每个结点都有两个子结点。而且,所有叶子结点都在同一层上。满二叉树的层数$h$与结点总数$n$的关系为:$n = 2^h - 1$。 - 完全二叉树:除了最后一层外,其他层的结点数都达到最大值,并且最后一层的结点都连续集中在最左边。也就是说,完全二叉树是满二叉树在最后一层从右向左连续缺失若干个结点而形成的。完全二叉树常用于数组存储(堆结构)。 2. BFS path的含义: - BFS(Breadth-First Search,广度优先搜索)是一种按层次遍历树的方法,从根结点开始,一层一层地向下遍历。在二叉树中,BFS通常指层序遍历(Level Order Traversal)。 - BFS path通常指的是从根结点到目标结点所经过的结点序列,且这个序列是按照BFS遍历的顺序得到的。但更常见的,BFS用于寻找最短路径(在树中,根到叶子的最短路径就是层数最小的路径)。例如,在无权图中,BFS可以找到两点之间的最短路径,而在二叉树中,BFS遍历的路径就是按层展开的结点序列。 3. Pre-order traversal(前序遍历)和In-order traversal(中序遍历)的定义: - 前序遍历(Pre-order traversal):按照“根结点-左子树-右子树”的顺序遍历二叉树。即先访问根结点,然后递归地前序遍历左子树,最后递归地前序遍历右子树[^1][^2]。 - 中序遍历(In-order traversal):按照“左子树-根结点-右子树”的顺序遍历二叉树。即先递归地中序遍历左子树,然后访问根结点,最后递归地中序遍历右子树[^1][^2]。 另外,我们可以通过一个例子来展示这两种遍历方式: 1 / \ 2 3 / \ 4 5 前序遍历:1 -> 2 -> 4 -> 5 -> 3 中序遍历:4 -> 2 -> 5 -> 1 -> 3 根据用户要求,我们将在回答中使用数学公式的LaTeX格式,并生成相关问题。 注意:用户提供的引用中并没有直接定义“BFS path”,所以我们根据图论中的常规用法进行解释。在树中,BFS遍历得到的路径序列实际上是按层访问的序列,而最短路径指的是层数最少的路径(因为树是无环的,所以从根到任意结点的路径是唯一的,但BFS可以保证我们按层数递增的顺序访问结点,因此最先访问到目标结点的路径就是最短路径)。 最后,按照要求生成相关问题。 </think>### 二叉树核心概念解析 #### 一、满二叉树 vs 完全二叉树 | **特性** | 满二叉树 (Full Binary Tree) | 完全二叉树 (Complete Binary Tree) | |----------------|------------------------------------------|-------------------------------------------| | **定义** | 所有非叶子结点都有两个子结点 | 除最后一层外完全填充,最后一层左对齐填充[^1] | | **数学关系** | 高度$h$的树共有$2^h - 1$个结点 | 高度$h$的树结点数$n$满足:$2^{h-1} \leq n < 2^h$ | | **结构示例** | ``` 1 → 所有非叶结点都有2个子结点 / \ 2 3 / \ / \ 4 5 6 7``` | ``` 1 → 最后一层左对齐 / \ 2 3 / \ / 4 5 6``` | | **应用场景** | 理论模型 | 堆结构实现、内存管理 | #### 二、BFS Path(广度优先路径) 1. **定义**: 在广度优先遍历(Breadth-First Search)中形成的路径,按层级顺序访问结点[^2][^3] 2. **核心特性**: - 访问顺序:从根节点开始,逐层从左到右访问 - 路径长度:记录从根节点到目标结点的边数(即深度) - 数学表示:树的高度$h$时,第$k$层最多有$2^k$个结点 3. **算法实现**(队列结构): ```python def bfs(root): queue = collections.deque([root]) while queue: node = queue.popleft() process(node) # 处理当前结点 if node.left: queue.append(node.left) # 左子结点入队 if node.right: queue.append(node.right) # 右子结点入队 ``` 4. **应用场景**: - 最短路径搜索(树中任意两节点间唯一路径)[^4] - 层次遍历(如LeetCode 102题) - 二叉树序列化 #### 三、遍历方式对比 | **遍历方式** | 访问顺序 | 伪代码 | 应用场景 | |-------------|----------------------|-------------------------------|--------------------------| | **前序遍历**<br>(Pre-order) | 根结点 → 左子树 → 右子树 | ```visit(root)```<br>```preorder(root.left)```<br>```preorder(root.right)``` | 复制树结构<br>前缀表达式生成 | | **中序遍历**<br>(In-order) | 左子树 → 根结点 → 右子树 | ```inorder(root.left)```<br>```visit(root)```<br>```inorder(root.right)``` | 二叉搜索树排序输出<br>表达式解析 | **遍历示例**(同一棵树): ``` A / \ B C / \ D E ``` - **前序遍历**:A → B → D → E → C (根结点优先) - **中序遍历**:D → B → E → A → C (根结点在中间) ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值