CS321 Lecture: Threaded Binary Trees                    last revised 10/31/97

Objectives:

1. To introduce inorder-threading of binary trees, and to show how inthreaded
   trees can be traversed easily.
2. To discuss threading schemes based on pre or post-order traversal instead

Materials: Transparency + Handout of a completely in-threaded binary tree

I. Inorder Threading of Binary Trees
-  ------- --------- -- ------ -----

   A. When we talked about preorder, inorder, and postorder traversal of binary
      trees in CS122, we saw that a stack is always needed to accomplish the 
      traversal - either explicitly or implicitly because of recursion.  

      1. Example: If we had a Node class with data members _info, _lchild, and
                  _rchild, we might implement a recursive inorder traversal 
                  function as follows:

                void inorder(Node * p)
                  { if (p != NULL)
                      { inorder(p -> _lchild);
                        // Do whatever visiting the _data of this node means
                        inorder(p -> _rchild)
                      }
                  }

         This could be done non-recursively as follows:

                void inorder(Node * p)
                  { stack < Node * > s;
                    do
                      {
                        while (p != NULL)
                          { s.push(p);
                            p = p -> _lchild;
                          }
                        if (! s.isempty())
                          { p = s.pop();
                            // Do whatever it means to visit p's data
                            p = p -> _rchild;
                          }
                     } 
                   while (p != NULL || s.isempty());
                    
         Implementing of the non-recursive version would use a stack whose size 
         is equal to the height of the tree; the recurive version would use an
         implicit stack of the same size.

      2. Since these traversals are used often, we would like to avoid the 
         space and time overhead of the stack if we can. It turns out that there
         is a simple tree representation that allows us to do this, while also
         allowing us to define an iterator for the tree that allows us to
         easily move from one node to the next in the appropriate order.

   B. Consider inorder traversal.  We will define the inorder successor of
      a node n as being the next node that will be visited in doing an
      inorder traversal of the tree - or some sentinel value (e.g. NULL or
      a pointer back to the header node if there is one) if the node is the
      last one visited in inorder.  

      1. Suppose we were able to define a function

                Node * insucc(Node * p);
                /* returns a pointer to the inorder successor of p */

         Suppose, further, that we arranged for there to be a header node for
         the tree, whose insucc is the first node in the inorder traversal 
         order.  if so, we could implement inorder traversal of a tree as 
         follows, without the use of a stack or recursion:

                p = insucc(header);
                while (p <> sentinel)
                  { // Do whatever it means to visit the _data at this node
                    p = insucc(p);
                  }

      2. If a node has a non-NULL right child, insucc is easy to define:

                Node * c = p -> _rchild;
                while (c -> _lchild != NULL)
                    c = c -> lchild;
                return c;

      3. However, if a node has a NULL right child, then its inorder successor
         is "above" it in the tree.  This is what the stack does for us - note
         that, in the non-recursive inorder traversal algorithm, when 
         p -> _rchild is NULL then we fall through the while loop and pop the 
         stack again, getting the node that was the parent of p.  If its right 
         child is also NULL we end up popping the stack again, going further 
         up the tree.

      4. We have also noted that a binary tree with n nodes contains n+1 NULL
         pointers.  It would be nice to do something useful with these.  One
         thing we could do with a NULL rchild pointer is to use it to point to 
         the inorder successor of the node.  We will call such a pointer a 
         thread, and a tree containing such pointers a right-inthreaded binary
         tree.

      5. Of course, we must have some way of tagging the pointers to distinguish
         between a regular child pointer and a thread.  Since this requires just
         one bit, it can generally be done at no additional cost by using a bit
         somewhere that is otherwise unused.

         a. For example, on some machines a pointer must be even, since words 
            in memory begin on even address boundaries.  Therefore, the 
            low-order bit of a pointer must be zero.  We can differentiate 
            threads from regular pointers by setting this bit to 1.

         b. On most machines, the number of bits used to store an address far
            exceeds the number needed to represent the range of addresses
            needed for the physical memory installed; hence, the high order
            bit is normally 0.  We can differentiate threads from regular
            pointers by setting this bit to 1.

   C. We can now implement insucc - and hence inorder traversal - as follows:

        Node * insucc(Node * p)
        /* returns a pointer to the inorder successor of p */
          {
            if (isthread(p -> _rchild)
                return makepointer(p -> _rchild);
            else
              { Node * c = p ->< _rchild;
                while (c -> _lchild != NULL)
                    c = c -> _lchild;
                return c;
              }
          }

        - where isthread tests the extra bit of a pointer to see if it is a
          regular pointer or a thread, and makepointer clears the extra bit
          so that the thread can be used like a poionter.

      1. What is the time efficiency of this algorithm?  Clearly, any one
         application of insucc can require time proportional to the height
         of the tree.  But what is of more interest is the average cost of
         applying insucc n times in order to visit all the nodes of the
         tree.  We call the average cost per use, averaged over all cases,
         the AMORTIZED COST.

      2. Note that a tree of n nodes contains n lchild pointers and n rchild
         pointers.  In the process of traversing the tree, insucc follows each
         non-NULL lchild pointer exactly once, and each rchild pointer (normal
         or thread) exactly once.  Therefore, the total time for traversing
         a right-intreaded tree of n nodes in inorder is O(n)!  This, of
         course, is optimal - since we must visit all n nodes.

      3. From this, it follows that the amortized cost of one use is O(n/n) =
         O(1).

   D. Note that this trick only made use of the NULL rchild pointers.  What
      about the NULL lchild pointers?  Suppose we define inpred as the
      inorder predecessor.  By symmetry, it turns out that inpred looks like
      insucc with lchild and rchild pointers interchanged.  Thus, we can
      replace NULL lchild pointers by threads to the inorder predecessor.  If we
      do so, then we can perform inorder traversal in either direction without
      the use of recursion or a stack.

      1. Such a tree is called completely inthreaded.

      2. A tree which contained only left-threads would allow reverse inorder
         traversal only.  Such a tree is called left-inthreaded.

   E. A completely inthreaded binary tree might look like the following.  

        TRANSPARENCY + HANDOUT

      Note that we make use of a header node to simplify some of the algorithms
      to follow.  The header convention is this:

      1. If the tree is empty, then the header's left child is a thread back
         to the header.  Otherwise, it points to the root of the tree.

      2. The header's right child is a pointer (not a thread) to itself.

      3. The first node (in inorder) in the tree has an lchild thread back to
         the header.  (Note that our insertion algorithm will ensure this.)
         Likewise, the last node has an rchild thread back to the header. 
         (Insert will also do this.)

      4. Note how this choice causes our insucc algorithm, when applied to the
         header, to yield the first node of the tree.  Our inpred algorithm
         also works correctly.  Finally, both algorithms return a pointer to
         the header when applied to the first/last node in the tree (as the
         case may be.)

   F. How can be build such a tree?  If we always insert new nodes in place
      of previously NULL pointers, then the following approach will work:

      1. If the new node is the lchild of its parent, then it lies between
         its parent's inpred and its parent in inorder traversal.  Therefore,
         let the lchild of the new node be the original lchild (thread) of
         the parent, and let the rchild of the new node be a thread to its
         parent.

      2. If the new node is the rchild of its parent, then it lies between
         its parent and its parent's insucc in inorder traversal.  Therefore,
         let the rchild of the new node be the original rchild (thread) of
         the parent, and let the lchild of the new node be a thread to its
         parent.

   G. As a further consideration, note that while threads as we have 
      implemented them are based on inorder traversal, they can assist the
      other traversals as well:

      1. Preorder - define the function presucc.  Note that:

         a. If a node has an lchild, then its lchild is its presucc.
            Ex: node 1 in diagram.

         b. Otherwise, if it has an rchild, then its rchild is its presucc.
            Ex: node 19

         c. If it has no children, then it is the last node to be visited in
            preorder in the left subtree of some node Q.  Let Q be the nearest
            such node having a non-empty right child.  (If all else fails, the
            header qualifies.)  Then Q's rchild is the presucc. 

                Ex: node 17     Q is 2          presucc is 5
                         27          1                     3

         d. From a node P having no actual rchild, this node Q can be found as 
            follows:

            i. Follow P's rchild thread to a node above it.  Clearly, P is in
               the left subtree of this node.  If this node has a non-thread
               rchild, then it is node Q.

           ii. If this node's rchild is a thread, then repeat the process
               as many times as necessary until a node is found having a
               non-thread rchild.  This is node Q.

          iii. Having found this node Q, P's presucc is Q's rchild.

        Time complexity for a complete traversal: note that each non-thread
        lchild is followed exactly once, and that each rchild is followed
        exactly once - therefore, the traversal is O(n), and the amortized cost
        of presucc is O(1).

      2. Reverse preorder - define the function prepred.  This is not quite as
         easy, since we must always go through the parent of the node.  Note:

         a. If a node is the lchild of its parent, then its parent is its
            prepred.  Ex: node 2.

         b. If a node is the rchild of its parent and the parent has no lchild,
            then the parent is the prepred.  Ex: node 25.

         c. Otherwise, its prepred is the last node (in preorder) in the left 
            subtree of its parent.  This can be found by going down the left
            subtree of the parent as far as possible - going right whenever
            possible, otherwise left.

            Ex: node 13 - prepred is 38.

         d. Thus, we must first define a function parent (which is useful in
            its own right and also for postsucc, it turns out.)

            For any node P, there exists a nearest ancestor Q, such that
            P is in its right subtree.  (If all else fails, the header is
            such.)  We can find this node by following lchild pointers 
            until we have followed a thread.  Then, if P is the rchild of Q,
            then Q is its parent - otherwise, we follow lchild pointers in
            the right subtree of Q until we hit P.

                ex: node 3 - Q = 1 and is its parent
                    node 12 - Q = 1.  Note that we can find the parent
                        (node 6) by going right from Q, then continuing left.
                    node 13 - Q = 6 and is its parent

         e. Given the parent function, prepred is easily defined as discussed
            above.  Note that reverse preorder traversal using prepred will
            not be O(n) for the whole tree, but rather O(n*h), since parent
            potentially involves visiting one node on each level of the tree,
            and in subsequent applications of parent the same path can be
            retraced.  Thus, the amortized cost of prepred is O(h) =
            O(log n) if the tree is well balanced.  (But O(n) worst case).

      3. Postorder traversal - define a function postsucc.

         a. By symmetry, this turns out to be similar to prepred, but with
            the roles of lchild and rchild interchanged.  To find the postsucc,
            we first find the parent of the node in question.  

         b. If the node is the rchild of its parent, or if it is the only
            child of its parent, then the parent is the postucc.
            Ex: nodes 3, 24.

         c. Otherwise, we find the first node in postorder in the right subtree
            of the parent.  This can be found by going down the subtree as far
            as possible, preferring to go left whenever possible, otherwise
            right.
            Ex: node 2 - postsucc = 32.

         d. As with prepred, postorder traversal using postsucc is O(n*h);
            amortized cost of postsucc is O(h) = O(log n).

         e. A caution on implementation: with the previous algorithms, our
            header convention has worked to our advantage to produce desired
            results - e.g. we could apply inpred, insucc, prepred, or presucc
            to the header and get a correct node, and in each case applying
            the function to the last node would lead us back to the header.
            With postsucc, some special cases are needed when leaving or
            coming back to the header due to our trick of making the header
            its own right child.  (However, the fact that the header is its
            own right child makes it easy to recognize the header.)

      4. Reverse postorder traversal - define a function postpred.

         a. By symmetry, this is analgous to presucc, but with lchild and
            rchild roles reversed.  Reverse postorder traversal using postpred
            is therefore O(n), so the amortized cost of postpred is O(1).

         b. As with postsucc, some special cases are needed around the
            header.

II. Preorder and postorder threading
--  -------- --- --------- ---------

   A. The threading scheme we have discussed has been based on the inorder
      traversal of the tree.  However, as we have seen, the inorder threads
      can also be used to accomplish other traversals (though not necessarily
      in O(1) amortized time.)

   B. If some other traversal is going to be used regularly instead of inorder,
      then an alternate threading scheme might be considered.  We could, for
      example, base a scheme on pre-order:

      1. We might build a threading scheme on the fact that if a node has a
         left child, then that child is its preorder successor.  If it has no
         left child, then its lchild pointer could be made into a thread to
         its pre-order successor.  (In this case, the rchild pointer is used
         as in an unthreaded tree.)  Presucc now becomes simply:

                if (! isthread(p -> _lchild))
                    return p -> _lchild;
                else
                    return makepointer(p -> _lchild);

      2. Alternately, we could adopt the following scheme for pre-order:

         a. If the node has no lchild, then make its lchild pointer a thread
            to its pre-order predecessor.

         b. If the node has no rchild, then make its rchild pointer a thread
            to its pre-order successor.

         c. This scheme, like the previous one, makes forward pre-order 
            traversal fairly easy:

                if (! isthread(p -> _lchild))
                    return p -> _lchild;
                else if (! isthread(p -> _rchild))
                    return p -> _rchild;
                else
                    return makepointer(p -> _rchild);

        d. Reverse pre-order is also possible with this scheme, THOUGH WE WOULD
           OCCASSIONALLY HAVE TO GO TO THE HEADER AND APPLY PRESUCC REPEATEDLY.
           (This is because a node's prepred is never below it in the tree.)

   C. We could also base a scheme on post-order.  Unfortunately, forward
      post-order will always be hard, because a node's post-order successor
      is never below it in the tree.  However, a scheme to support reverse
      post-order would be somewhat easier!

Copyright ©1998 - Russell C. Bjork