Sei sulla pagina 1di 16

Data Structures – Fakhar lodhi – Chapter 5: Trees

Chapter 5 Trees – Introduction


Linked-lists are essentially used to store and organize data where the
relationship among data is linear or one-dimensional (a circular structure
can be considered as a special case of a linear structure). A lot of data is
however non-linear in nature. Examples of such data include hierarchical
data organizations such as lineal charts, file directories, and table of
contents of a book. Figure… shows one such hierarchy which presents a
section of the family tree of perhaps the most famous Arab tribe Banu-
Quresh, highlighting lineage of the Holy Prophet Muhammad (PBUH).
Such hierarchical organizations are very common and cannot be
expressed with linear lists. We therefore need other data structures to
model such organizations. Tree is perhaps the most important non-linear
data structure with numerous applications in computing and other fields.
This chapter is dedicated to the discussion of tree data structure.

5.1 Tree – some definitions


The elements of a tree are called nodes. A tree consists of a finite set of
nodes and links. A link connects two nodes of a tree. Two nodes are said
to be adjacent if they are directly connected by a link. A path is a
sequence of adjacent nodes. Every node is connected to every other node
in the tree through a unique path. A tree which does not have any node is
called an empty tree. Our focus of interest is a special kind of tree called
the rooted tree. A rooted tree is a non-empty tree which has a specially
designated node called the root node.

By convention, rooted trees are shown to grow downwards with the root
node shown at the top and serves as start or the entry point in to the tree.
All other nodes are said to be descendents of the root node. The direct
descendents (nodes that have a direct link) of a node are called its
children and the node itself is called the parent of those nodes. Children
of the same node are called siblings. The root node does not have any
parent. Every other node has exactly one parent. A node may have 0 or

Page 1 of 16
Quresh

Ghalib Muharib

Taiem Luayy

Auf Ka'ab Amir Hirs

Husais Sehm Murrah Jamha Adi

Taiem Kilab Makhzoom

Zahra Qusayy

Mugheera Abd Munaf Abdud Dar Abdul Uza

Mutlib Abdus Shams Naufil Hashim Abu Amr Abu Ubaida


Data Structures – Fakhar lodhi – Chapter 5: Trees

Asad Nuzlah Abdul Mutlib Aba' Saifi

Haris Zubair Abu Talib Abdullah Musa'ab Abu Lahab Maqoom Hajl Abbas Mugheera Hamza Zarrar

Muhammad
(PBUH)

Page 2 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

more children. A node with zero children is called a leaf node. Nodes
other than the leaf nodes and the root node are called internal or
intermediate nodes. That is, an intermediate node has exactly one parent
and at least one child.

A subset of a tree that can be viewed as a complete tree in itself is called a


subtree. That is, a node with all its descendents is a subtree. In other
words, every node in a tree can be seen as the root node of the subtree
rooted at that node. The subtree corresponding to the root node is the
entire tree and hence a tree is defined and identified by its root node.

Figure … shows a rooted tree with root A with nodes B, F, and K as its
children/subtrees. Nodes B, F, K , C, D, and X are internal nodes whereas
H, L, Q, G, M, I, N, and P are leaf nodes.

Degree of a node is defined as the number of children of that node. In


Figure…, nodes H, L, Q, G, M, I, N, and P have degree 0, nodes F and X
have degree 1, nodes B, K, and D have degree 2, and A and C have degree
3.

Level of a node is defined as the distance of the node from the root node.
In Figure … A is at level 0, B, F, and K are at level 1, C, H, D, L and X are
level 2, and the rest are at level 3.

Height of a tree is defined as the maximum level of any node in the tree1.
Height of tree in Figure… is 3.

1
Definition of height and level is not standardized and may slightly vary from one
text to the other.

Page 3 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.2 Binary tree


A binary tree is a special kind of rooted tree with the property that all
nodes in a binary tree have at most two children. That is, the maximum
degree of a node in a binary tree is 2. The two children of a node in a
binary tree are usually designated as the left and right child.

A binary tree can be defined recursively as the following:

a) A binary tree is either empty or non-empty


b) If the binary tree is non-empty then
1. There is a root node
2. The root node has two children, the left child and the right child.
These children correspond to two subtrees – the left subtree and
the right subtree.
3. Each subtree is a binary tree.

Figure… (a) and (b) are examples of binary trees. In Figure…(a), B is the
left child of A and H is the right child of B.

It can be very easily seen that the maximum number of nodes on level i of
a binary tree is 2i and the maximum number of nodes in a binary tree of
height k is 2k+1 – 1.

A binary tree of height k having 2k+1 – 1 nodes is called a full binary tree.
A binary tree is a full binary tree if and only if all leaf nodes are at the
same level and all other nodes have degree 2. Figure… shows a full binary
tree of height 4 with 15 nodes in it.

Page 4 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

A complete binary tree is a binary tree that is completely filled, with the
possible exception of the bottom level, which is filled from left to right.

Another way to determine whether a tree is complete or not is to assign


numbers to all its nodes as follows:

1. root node is assigned number 1.


2. for a node numbered i, assign 2*i and 2*i + 1 to its left and right
children respectively.

then the tree is a complete binary tree iff the maximum number thus
assigned is equal to the number of nodes in the tree. Note that full binary
tree is also a complete binary tree.

Figure… (a) is an example of a complete binary tree whereas the tree in


Figure…(b) is not complete.

Page 5 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.3 Binary tree implementation using linked structures


Binary trees can be easily implemented using dynamically created nodes
which are linked through pointers. As shown in Figure…, a binary tree
node structure is quite similar to that of a doubly linked-list – instead of
having pointers to the next and previous nodes, in this case, we have
pointers to the left and right children of the node. Some implementations
also add a pointer to the parent node but in most cases this is not required.

data
template <class T>
struct TreeNode {
T data;
TreeNode<T> *left, *right;
};
left right

We can now use this node structure to define our binary tree whose
specification is shown in Figure…
template <class T>
class BinaryTree {
public:
BinaryTree(){root = NULL;} // create an empty binary tree
void buildBinaryTree( T data, BinaryTree<T> &leftSubtree,
BinaryTree<T> &rightSubtree);
/*************************************************************
This method can be used to build a binary tree bottoms up. The
modified tree is rooted at a node with the input data and its
left and right children are made-up of the left and right
subtrees passed as input parameters. The left and right
subtrees passed as input lose their contents and their roots
are set to NULL.
**************************************************************/
~BinaryTree(){clear(root);}
void inOrder(){inOrder(root);} // in-order traversal
void preOrder(){inOrder(root);} // pre-order traversal
void postOrder(){inOrder(root);} // post-order traversal
BinaryTree(const BinaryTree & bt); // copy constructor
const BinaryTree & operator=(const BinaryTree & rhs);
// assignment operator
private:
TreeNode <T> *root;
friend class LNRIterator<T>; // in-order iterator
TreeNode<T> * createTreeNode(T data);
void clear(TreeNode<T> *t); // cleanup tree
void inOrder(TreeNode<T> *t); // in-order workhorse
void preOrder(TreeNode<T> *t); // pre-order workhorse
void postOrder(TreeNode<T> *t); // post-order workhorse
};

Page 6 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.3.1 Building a binary tree

Figure… shows the methods to build the binary from bottom to top.
template <class T>
TreeNode<T> * BinaryTree<T> :: createTreeNode(T data) {
TreeNode<T> *t = new TreeNode<T>;
if (t != NULL) {
t->data = data;
t->left = t->right = NULL;
return t;
}
else
throw OUT_OF_MEMORY;
}

template <class T>


void BinaryTree<T> ::
buildBinaryTree(T Data,BinaryTree<T> &leftSubtree,
BinaryTree<T> &rightSubtree) {

// create a new tree node and store data in it

TreeNode<T> *t = createTreeNode(data);

// make the left and right of the new node point to the roots
// of the left and right sub-trees respectively

t->left = leftSubtree->root;
t->right = rightSubtree->root;

// assign NULL to roots of the input trees so that the nodes


// that have now been shifted to the target tree are not
// destroyed when the destructor is called for these trees.

leftSubtree->root = NULL;
rightSubtree->root = NULL;

clear(root); // return any existing nodes in the tree to heap


root = t; // make the new node the root of the target tree
}

There are a couple of points to be noted.

1. This method detaches the nodes present in the trees passed as input
parameters and attaches those nodes to the tree which we are building
from these sub-trees. Hence these trees are effectively destroyed by
assigning NULL value to their roots.
2. The second last statement in the method, clear(root), is very
important. It essentially destroys the target tree by returning any
nodes attached to back to heap. Failing to do so could create garbage.

Page 7 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

Figure …(b to f) demonstrates how this method could be used to build the
tree of Figure….(a).

2 5

root root root


NULL NULL NULL
1 3
t1 t2 nullTree
(a) (b) BinaryTree<int> t1, t2, nullTree;

root root
2 NULL
t1 t2
root root
1 3
t1 t2
1 3
(c) t1.buildTree(1,nullTree, nullTree);
t2.buildTree(3,nullTree, nullTree); (d) t1.buildTree(2,t1, t2);

root root
4 NULL
t1 t2

2 5

root
5 1 3
t2
(e) t2.buildTree(5, nullTree, nullTree); (f) t1.buildTree(4,t1, t2);

Page 8 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.3.2 Binary tree traversal

It is often required to visit (access) each node in the tree and process data
present in there. For that purpose some tree traversal algorithm is used
which provides a systematic approach to access all the nodes in the tree. A
complete traversal generates a linear order of all the nodes in the tree.

As described in section 5.2, a non-empty tree is defined by the node


pointed to by its root and its two children – the left subtree and the right
subtree. These two subtrees are also trees rooted at the left and right child
of the parent node. A traversal algorithm can be developed easily by
making use of this recursive definition.

N - Node
NLR – process the node, then traverse the left subtree, then traverse the right subtree
NRL – process the node, then traverse the right subtree, then traverse the left subtree
LNR – traverse the left subtree, then process the node, then traverse the right subtree
LRN – traverse the left subtree, then traverse the right subtree, then process the node
RNL – traverse the right subtree, process the node, then then traverse the left subtree
RLN – traverse the right subtree, then traverse the left subtree, then process the node
L R
Left subtree Right subtree

Let N, L, and R denote node, left subtree, and right subtree of a tree. As
shown in Figure…, these three letters (N, L, and R) can be arranged in six
different permutations – NLR, NRL, LNR, LRN, RNL, and RLN. These six
permutations correspond to six different traversals of the binary tree. For
example, LNR corresponds to a traversal in which we complete the tree
traversal by first traversing the left subtree, then visiting the node, and
then traversing the right subtree. If we always traverse left before right
then we are left with three possible traversal – NLR, LNR, and LRN.
These three are called pre-order, in-order, and post-order respectively.
C++ code for in-order and pre-order workhorse functions is given in
Figure…

template <class T> template <class T>


void BinaryTree<T> :: void BinaryTree<T> ::
inOrder(TreeNode<T> *t) { preOrder(TreeNode<T> *t) {
if (t != NULL) { if (t != NULL) {
inOrder(t->left); process(t);
process(t); preOrder(t->left);
inOrder(t->right); preOrder(t->right);
} }
} }

Page 9 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

The in-order, pre-order, and post-order traversals can be generated by


drawing the tree and just walking around it – starting from the left of the
root and ending at the right of it. For each node we note when we are on
its left, under it, and on its right. As shown in Figure…, the pre-order, in-
order, and post-order traversals are generated by visiting each node when
we are on the left, under, and right of it respectively.

B K

C H L X

Q M P

NLR – visit when at the left of the Node A B C Q M H K LX P


LNR – visit when under the Node Q C M B H A L KX P
LRN – visit when at the right of the Node Q M C H B L P XK A

Figure… shows the three tree traversals for an expression tree. Note that
the in-order, pre-order, and post-order traversals generate the expression
in infix, prefix, and postfix respectively.

+ /

A * D *

B C E F

LNR: A + B * C – D / E * F
NLR: – + A * B C / D * E F
LRN: A B C * + D E F * / –

Page 10 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.3.2.1 The destructor

As shown is Figure…, the binary tree destructor uses clear function. As


can be seen easily, clear requires a post-order traversal of the tree. The
resulting code is given in Figure…
template <class T>
void BinaryTree<T> :: clear(TreeNode<T> *t) {
if (t != NULL) {
clear(t->left);
clear(t->right);
delete t;
}
}

Exercise

1. write the copy constructor


2. write the assignment operator
3. write a member function to count the number of nodes in the tree
4. write a member function to count the leaf nodes
5. write a member function to count the nodes with degree 1
6. write a member function to count nodes with degree 2
7. write a member function to calculate the height of the tree
8. write a member function to make a mirror image of the tree
9. write a member function to determine the presence of a key in the
tree.
10. write a function to determine the level of the node containing a given
key. Return -1 if the key is not present.
11. write a member function == which returns true if the two trees are
equal.
12. write a member function that takes a key and prints the entire subtree
rooted at the node that contains the key.
13. write a member function that takes a key and prints the entire subtree
rooted at the sibling of the node that contains the key.
14. write a member function that deletes all the leaf nodes from the tree.

Page 11 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

15. write a member function that deletes all the nodes with degree 1 from
the tree.
16. write a member function that takes two keys as input and inserts a
node with second key as the left most descendent of the node with
the first key if it the first key present in the tree making the new node
the first node to be visited in in-order in the tree rooted at the node
that contains the first key.
17. write a member function that takes two keys as input and inserts a
node with second key as the right most descendent of the node with
the first key if it the first key present in the tree making the new node
the last node to be visited in in-order in the tree rooted at the node
that contains the first key.

Page 12 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.4 Tree iterators


Just like the linked-list class, we need tree iterators that would allow us to
access each element of the tree in some order so that we can process data
according to our specific need. Since, for each node reached through the
iterator we process the data in the node and then request for the next
node, we cannot use recursion because once we return from that function,
we cannot restart the recursion from that node – the context (addresses of
the all the ancestors stored on to the implicit stack) would have been lost.
We would therefore need an iterative solution – where, in order to
remember the context, an explicit stack needs to be employed. Following
this line of thought, an in-order iterator for the binary tree class is
developed. The iterator supports the four basic functions – create, isDone,
begin, next, and getData and is presented in Figure….

template <class T>


class LNRIterator {
private:
const BinaryTree<T> &tree; // the associated tree

TreeNode<T> *current; // pointer to the current node

Stack<TreeNode<T> *> stk; // used for storing those


// ancestors of current that are
// yet to be visited

TreeNode<T> * gotoTheLeftmostDescendent(TreeNode<T> *t);


// given a node, go its leftmost descendent
// to be used in begin() and next()

public:
LNRIterator(const BinaryTree<T> &bt):tree(bt), current(NULL){ }
bool isDone() {return current == NULL; }
T getData() {
if (!isDone()) return current->data;
else throw ILLEGAL_REFERNCE_EXCEPTION;
}

void begin(); // takes current to the first node to be visited


void next(); // moves current to the next node in order

};

The constructor, isDone, and getData are trivial and do not need
elaboration. We will therefore only focus on begin and next which are
given in Figure ….

Page 13 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

template <class T>


TreeNode<T> * LNRIterator<T> ::
gotoTheLeftmostDescendent(TreeNode<T> *t)
{
TreeNode<T> *temp = t;
while(temp->left != NULL) {
stk.push(temp);
temp = temp->left;
}
return temp;
}

template <class T>


void LNRIterator<T> :: begin() {
stk.clear();
stk.push(NULL);
current = root;
if (root != NULL) // if the tree is not empty
current = gotoTheLeftmostDescendent(root);
}
Begin sets the value of the current to the first node to be visited in order.
Starting from the root, it gets to the left-most child of the root by keeping
moving to the left until by a node is reached whose left child is not
present. Before moving to the left, address of the node is saved so that it
can be used later for visiting the node and then traversing its right
subtree. It may be noted that begin also initializes the stack by clearing it
of its contents and puts NULL on to it. This will be used to set the value of
current to NULL when the iterator goes beyond the last node.

After visiting a node, the in-order traversal of the right subtree by moving
to its right child and then to the leftmost descendent of the right child
and saving the address of the nodes encountered in the process. This is
like repeating the process similar to begin for the tree rooted at the right
child of a node. Once the traversal on the right is complete, this means
the entire subtree rooted at that node is complete and hence current
moves back one step in the hierarchy by popping the address of its
ancestor. The process continues until NULL is popped from the stack and
assigned to current, indicating end of traversal. The code for the next()
operation is given in Figure…
template <class T>
void LNRIterator<T> :: next() {
if (current == NULL) throw ILLEGAL_REFERENCE_EXCEPTION;
if (current->right != NULL) {
current = gotoTheLeftmostDescendent(current->right);
else current = stk.pop();
}

Page 14 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

We can now use this iterator to for our specific purpose as shown below:

BinaryTree<int> binTree;
// add data to the tree
LNRIterator<int> myIterator(binTree);

for(myIterator.begin(); myIterator.isDone(); myIterator.next())


cout << getData();

Exercise:

1. Write pre-order iterator.


2. write post-order iterator.
3. write iterative stack-based in-order traversal
4. write iterative stack-based pre-order traversal
5. write iterative stack-based post-order traversal
6. write a member function of the in-order iterator class to add a new
node as the leftmost descendent of the current node.
7. Write a function to delete the leftmost descendent of the current
class. Warning: the node to be deleted may have a right child which
must be preserved.
8. write a member function of the in-order iterator class to add a new
node as the rightmost descendent of the current node.
9. Write a function to delete the rightmost descendent of the current
class. Warning: the node to be deleted may have a left child which
must be preserved.

Page 15 of 16
Data Structures – Fakhar lodhi – Chapter 5: Trees

5.5 Level-order traversal


Level-order traversal traverses the tree level by level. That is, it traverses
all the nodes at level i before moving to nodes at level i+1. Such a traversal
is also called depth-first traversal. Traditionally, level-order traversal is
performed top-to-bottom and left-to-tight. For example, level-order
traversal of the tree of Figure… will visit nodes in the following order:

ABKCHLXQMP

Level-order traversal can be implemented by using a queue. A node at


level i would have its children at level i+1. Therefore, after visiting a
node, if its children were added to the queue, then they could be retrieved
from the queue and visited in their order of insertion, resulting in level-
order traversal.

We start by inserting the root node to the queue. Then we remove a node
from the queue, visit it and put its children, if any, on the queue. The
process continues until there is nothing left on the queue. The code for
level-order traversal is presented in Figure…
template <class T>
void BinaryTree<T> :: levelOrder() {
if (root != NULL) {
TreeNode<T> *t;
Queue<TreeNode<T> *> que;
que.add(root);
while(!que.isEmpty()) {
t = que.remove();
visit(t);
if (t->left != NULL) que.add(t->left);
if (t->right != NULL) que.add(t->right);
}
}
}
Exercise:

1. Write a function that takes a number as input and prints all the nodes
present at that level.
2. Write level-order iterator.
3. Write a recursive function to achieve the task specified in Q. 1

Page 16 of 16

Potrebbero piacerti anche