Trees
Definitions
A tree is either null or a node followed by 0 or more trees
-
[root]
Start of the tree. This is a relative term in that the child of a root
node is the root node of a tree below it.
-
[node]
element in a tree, as in lists
-
[forest]
a collection of trees
-
[leaf]
node with no subtrees
-
[children]
subtrees of a node
-
[parent]
node above a given node
-
[siblings]
nodes with same parents
-
[level]
root is zero 1 + level(parent) sometimes, the level is defined to start
at 1
-
[height]
max level of tree
-
[order]
width of a tree
A tree is a subset of a general connected structure called a graph. A node
in a tree may have many children, but it has only one parent. A list is
a tree of order 1.
Uses
-
family tree
-
file systems cataloging (Dewey decimal)
-
org charts
-
sorting and searching
Example
struct tree {
int value;
struct tree *child1;
struct tree *child2;
struct tree *child3;
};
This tree is of order 3. This is like a linked list where each node
can point to three other nodes instead of one. All we know about linked
lists still applies although altered.
Traversals
Going through a tree is called a traversal. This is a description of a
path through each node in a tree. This is the same as the techniques we
developed to look at every node in a list. Three common binary tree traversals
are:
-
[preorder]
root left right
-
[inorder]
left root right
-
[postorder]
left right root
get class to suggest how to do these first. In these routines, the function
doroot() indicates the location in the code where we should put the statements
or functions that are to be performed on the node we are looking at. Examples
Traversal |
Notation |
Results |
preorder |
prefix |
+ c * a b |
postorder |
postfix (RPN) |
c a b * + |
inorder |
infix |
c + a * b |
Discuss pre, post and inorder notation, and conversion stack actions to
evaluate post instead of inorder
Iterative Traversals
The routines above are recursive, it is possible to traverse a tree iteratively
but is more difficult. Below are two routines the perform an inorder traversal
of a tree without recursion.
inorder(T)
ptr = T
do
while(ptr != nil) {
push(ptr)
ptr = ptr->left
}
if (not stack_empty()) {
x = pop()
print(x->value)
ptr = ptr->right
}
until (stack_empty() && (ptr == nil))
Ordered Binary Trees
The algorithms that we will be discussing will concern a particular type
of tree called a binary tree which is of order 2. And we are looking at
a variant on binary trees that are ordered in the following way:
In this tree, for every node, val2 < val1 < val3. This ordering is
only a useful convention, it is not part of the definition of binary trees.
Example
struct bin {
int value;
struct bin *rchild;
struct bin *lchild;
};
Tree Search
Searching an ordered binary tree is like searching a linear list and like
a binary search on an array. If the node we are looking at is not the one
we want, then if the searched for value is less than the one we are looking
at, it must be to the left of it and to the right if it is greater than
the current node.
Example Ordered Binary Tree
This tree results from inserting the nodes from data that is in this order:
8,7,15,3,10,18. Search through this tree for 15 and then for 20. Note
the value and location of T when the 20 is not found.
Tree Search And Insertion
The algorithm presented above has the interesting property of leaving us
at the location where a new node is to be inserted if it is not already
in the tree. So a simple change will produce a search and insert algorithm.
Walk through what happens when an attempt is made to add a 20 to the
tree above. Don't immediately connect the new node to the position in the
18 node. Let the class figure out why the connection is made.
The fact that T is passed in by reference is critical to this routine.
When the T=makenode() is executed for node 20, the value (right hand value)
of T is nil. The location of T (left hand value) is the right child field
of the 11 node.
After the walk through of adding the 20 node, present the addchar()
solution to insertion in a linked list as an example of the same kind of
thing but without recursion or trees. This is insert2() in "Linkstrings".
The critical element in this solution is the use of pass by reference.
Procedure addchar(var L,ch)
{ add the char ch to the front of the list L }
1. new(fresh)
2. fresh^.value = ch
3. fresh^.next = L
4. L = fresh
end add char
Procedure insert{L,target,ch)
{ add the character ch to the list L after target }
1. z = findchar(L,target)
2. if z <> nil then
3. addchar(z^.next,ch)
4. else
5. error(no list)
6. endif
7. end insert
Since at line 3, z points to the node we want to add the ch
after, z->next is the location that should point to the new node
and while alterations to z are not carried out of the routine, changes
to the thing z points to are permanent. From the point of view of
the addchar routine, whatever is passed into it as the L variable is the
front of a list, it alters it appropriately. So even though the L variable
is in this case pointing to an element somewhere in the middle of a list,
it puts the element on the front of the list it is given, even though the
pointer passed in points to the middle of the list. The next routine can
be used to print the tree on its side. It mostly illustrates the use of
a second parameter to hold a value and count the number of levels of recursion.
Tree Deletion
Deleting an element from an ordered binary tree is more difficult. There
are several cases to deal with.
-
[ A leaf node]
This is the simplest case. Simply set the pointer to it in its parent
to nil. This happens at around line 22. Example, delete 3.
-
[A non-leaf node with one child]
Here we replace the pointer to the deleted node in its parent with
the pointer to the child. This happens at either line 22 or line 25 depending
on which child is non-nil. Example, delete 7.
-
[A non-leaf node with two children]
This is the tricky case. Here we must rotate one of the children into
the parent and delete the child. This is the case that is dealt with at
line 28 by calling the internal subroutine del(). It finds the right most
leaf node on the left side of the node to be deleted.
Tree Algorithms, Part 1
These algorithms have to do with binary trees. The record structure used
to build them is below followed by routines to traverse the tree, search,
search and insert and print the tree.
struct treenode {
int value;
struct treenode *lchild;
struct treenode *rchild;
};
Traversals
> |
void pre(T) |
void post(T) |
void in(T) |
1. |
if(T == NULL) |
if(T == NULL) |
if(T == NULL) |
2. |
return; |
return ; |
return; |
3. |
else { |
else { |
else { |
4. |
doroot(T); |
post(T->left); |
in(T->left); |
5. |
pre(T->left); |
post(T->right); |
doroot(T); |
6. |
pre(T->right); |
doroot(T); |
in(T->right) |
7. |
} |
} |
} |
Tree search
struct treenode *
search(struct treenode *T,int val) {
/* returns either NULL if it doesn't find it or */
/* a pointer to the node containing val. */
1. if(T == NULL)
2. return(NULL);
3. else
4. if(T->value == val)
5. return(T);
6. else
7. if(val < T->value)
8. return(search(T->left,val));
9. else
10. return(search(T->right,val));
} /* search */
Tree search with insertion
struct treenode * insert(struct treenode **T,int val){
/* this searchs T for val and if it doesn't find */
/* it, it creates a node for it and returns a pointer */
/* to it. T is passed by reference */
1. if(*T == NIL)
2. *T=createnode(); /* make a new tree node */
3. *T->value = val;
4. *T->left = T->right = NIL;
5. return(*T);
6. else
7. if(*T->value == val)
8. return(*T);
9. else
10. if(val < *T->value)
11. return(insert(*T->left,val));
12. else
13. return(insert(*T->right,val));
} /* insert */
Tree printing
void treeprint(struct treenode *T,int L) {
/* this prints the tree T sideways showing it's structure. */
/* L is a helping variable to keep a count of the indentation to
print */
int i;
1. if(T != NIL)
2. treeprint(T->left,L+1);
3. for(i = 1; i<=L;i++)
4. printf(" ");
5. end for
6. printf("%d\n",T->value); /* prints with a newline */
7. treeprint(T->right,L+1);
} /* treeprint */
Tree Algorithms, Part 2
This continues from Trees 1 and presents an algorithm to delete nodes from
a binary tree. The tree node is described by the record below.
struct treenode {
int value;
struct treenode *lchild;
struct treenode *rchild;
};
Binary Tree Deletion
struct treenode *Hold;
void delete (int val,struct treenode **T) {
/* Hunt through T for the node containing val and delete it.
Use a local procedure called del().
*/
1. if(*T == NULL)
2. printf("it is not here\n")
3. else
4. if(val < *T->value)
5. delete(val,*T->left);
6. else
7. if(val > *T->value)
8. delete(val,*T->right);
9. else {
10. Hold
= *T; /* remember where we are */
11. if(Hold->right
== NULL)
12.
*T = Hold->left; /* move left child pointer up */
13. else
14.
if(Hold->left == NULL)
15.
*T = Hold->right; /* move right child pointer up */
16.
else
17.
del(Hold->left);
18. }
19. } /* delete */
void del(struct treenode **r){
1. if(r->right != NULL)
2. del(r->right);
3. else {
4. Hold->value = r->value;
5. r = r->left;
6. }
7. } /* del */
Tree Algorithms, Part 3
This is an iterative version of the binary tree insertion algorithm. This
is most closely related to the search-insert routine in the recursive tree
routines handout, Trees 1. This particular implementation is from "Applied
Data Structures Using Pascal", Guy J. Hale, Richard J. Easton, D. C. Heath
and Company,1987.
void inserttree(struct treenode *root,int val)
/* iterative binary tree insertion */
1. if((newnode=createnode()) == NULL) {
printf("No space for new node\n");
return;
}
2. newnode->value = val;
3. newnode->rchild = newnode->lchild = NULL;
4. inserted = FALSE;
5. top = root;
6. if(root = NULL)
7. root = newnode;
8. else {
9. while(!inserted){
10. if(val < top->value)
11. if(top->lchild
!= NULL)
12.
top = top->lchild;
13. else
{
14.
top->lchild = newnode;
15.
inserted = TRUE;
16. }
17. else
18. if(top->rchild
!= NULL)
19.
top = top->rchild;
20. else
{
21.
top->rchild = newnode;
22.
inserted = TRUE;
23. }
25. }/* endwhile */
26. }
27. } /* insertree */