Beruflich Dokumente
Kultur Dokumente
3. Data Structures
3.1. Structures
Structures are aggregate data types built using elements of primitive data types.
The struct keyword creates a new user defined data type that is used to declare variables of an
aggregate data type.
The Arrow operator (->): to access data members of pointer variables pointing to the structure.
cout<< timeObject.hour; or
cout<<timeptr->hour;
The parentheses is required since (*) has lower precedence than (.).
Linked lists are the most basic self-referential structures. Linked lists allow you to have a chain
of structs with related data.
Arrays are simple and fast but we must specify their size at construction time. This has its own
drawbacks. If you construct an array with space for n, tomorrow you may need n+1.Here comes
a need for a more flexible system.
Flexible space use by dynamically allocating space for each element as needed. This implies that
one need not know the size of the list in advance. Memory is efficiently utilized.
This linked list has four nodes in it, each with a link to the next node in the series. The last node
has a link to the special value NULL, which any pointer (whatever its type) can point to, to show
that it is the last link in the chain. There is also another special pointer, called Start (also called
head), which points to the first link in the chain so that we can keep track of it.
The important part of the structure is the line before the closing curly brackets. This gives a
pointer to the next node in the list. This is the only case in C++ where you are allowed to refer to
a data type (in this case node) before you have even finished defining it!
We have also declared a pointer called start_ptr that will permanently point to the start of the list.
To start with, there are no nodes in the list, which is why start_ptr is set to NULL.
Firstly, we declare the space for a pointer item and assign a temporary pointer to it. This is done
using the new statement as follows:
We can refer to the new node as *temp, i.e. "the node that temp points to". When the fields of
this structure are referred to, brackets can be put round the *temp part, as otherwise the compiler
will think we are trying to refer to the fields of the pointer. Alternatively, we can use the arrow
pointer notation.
The last line sets the pointer from this node to the next to NULL, indicating that this node, when
it is inserted in the list, will be the last node. Having set up the information, we have to decide
what to do with the pointers. Of course, if the list is empty to start with, there's no problem - just
set the Start pointer to point to this node (i.e. set it to the same value as temp):
if (start_ptr == NULL)
start_ptr = temp;
It is harder if there are already nodes in the list. In this case, the secret is to declare a second
pointer, temp2, to step through the list until it finds the last node.
temp2 = start_ptr;
// We know this is not NULL - list not empty!
while (temp2->next != NULL)
{ temp2 = temp2->next; // Move to next link in chain
}
The loop will terminate when temp2 points to the last node in the chain, and it knows when this
happened because the next pointer in that node will point to NULL. When it has found it, it sets
the pointer from that last node to point to the node we have just declared:
temp2->next = temp;
The link temp2->nxt in this diagram is the link joining the last two nodes. The full code for
adding a node at the end of the list is shown below, in its own little function:
void add_node_at_end ()
{ node *temp, *temp2; // Temporary pointers
1. Set a temporary pointer to point to the same thing as the start pointer.
2. If the pointer points to NULL, display the message "End of list" and stop.
3. Otherwise, display the details of the node pointed to by the start pointer.
4. Make the temporary pointer point to the same thing as the nxt pointer of the node it is
currently indicating.
5. Jump back to step 2.
The temporary pointer moves along the list, displaying the details of the nodes it comes across.
At each stage, it can get hold of the next node in the list by using the nxt pointer of the node it is
currently pointing to. Here is the C++ code that does the job:
temp = start_ptr;
do
{ if (temp == NULL)
cout << "End of list" << endl;
else
{ // Display details for what temp points to
cout << "Name : " << temp->name << endl;
cout << "Age : " << temp->age << endl;
cout << "Height : " << temp->height << endl;
cout << endl; // Blank line
We will call the mobile pointer current. First of all, it is declared, and set to the same value as the
start_ptr pointer:
node *current;
current = start_ptr;
Notice that you don't need to set current equal to the address of the start pointer, as they are both
pointers. The statement above makes them both point to the same thing:
It's easy to get the current pointer to point to the next node in the list (i.e. move from left to right
along the list). If you want to move current along one node, use the nxt field of the node that it is
pointing to at the moment:
current = current->nxt;
In fact, we had better check that it isn't pointing to the last item in the list. If it is, then there is no
next node to move to:
if (current->nxt == NULL)
cout << "You are at the end of the list." << endl;
else
current = current->next;
Moving the current pointer back one step is a little harder. This is because we have no way of
moving back a step automatically from the current node. The only way to find the node before
the current one is to start at the beginning, work our way through and stop when we find the node
before the one we are considering at the moment. We can tell when this happens, as the next
pointer from that node will point to exactly the same place in memory as the current pointer (i.e.
the current node).
previous current
Start
Sto
p
NULL
First of all, we had better check to see if the current node is also first the one. If it is, then there is
no "previous" node to point to. If not, check through all the nodes in turn until we detect that we
are just behind the current one (Like a pantomime - "behind you!")
if (current == start_ptr)
cout << "You are at the start of the list" << endl;
else
{ node *previous; // Declare the pointer
previous = start_ptr;
while (previous->nxt != current)
{ previous = previous->nxt;
}
Current = previous;
}
The else clause translates as follows: Declare a temporary pointer (for use in this else clause
only). Set it equal to the start pointer. All the time that it is not pointing to the node before the
current node, move it along the line. Once the previous node has been found, the current pointer
is set to that node - i.e. it moves back along the list.
Now that you have the facility to move back and forth, you need to do something with it. Firstly,
let's see if we can alter the details for that particular node in the list:
cout << "Please enter the new name of the person: ";
cin >> current->name;
cout << "Please enter the new age of the person : ";
cin >> current->age;
cout << "Please enter the new height of the person : ";
cin >> current->height;
The next easiest thing to do is to delete a node from the list directly after the current position. We
have to use a temporary pointer to point to the node to be deleted. Once this node has been
"anchored", the pointers to the remaining nodes can be readjusted before the node on death row
is deleted. Here is the sequence of actions:
1. Firstly, the temporary pointer is assigned to the node after the current one. This is the
node to be deleted:
current temp
NULL
2. Now the pointer from the current node is made to leap-frog the next node and point to the
one after that:
current temp
NULL
Here is the code for deleting the node. It includes a test at the start to test whether the current
node is the last one in the list:
if (current->nxt == NULL)
cout << "There is no node after current" << endl;
else
{ node *temp;
temp = current->nxt;
current->nxt = temp->nxt; // Could be NULL
delete temp;
}
Here is the code to add a node after the current one. This is done similarly, but we haven't
illustrated it with diagrams:
if (current->nxt == NULL)
add_node_at_end();
else
{ node *temp;
new temp;
get_details(temp);
// Make the new node point to the same thing as
// the current node
temp->nxt = current->nxt;
// Make the current node point to the new link
// in the chain
current->nxt = temp;
}
We have assumed that the function add_node_at_end() is the routine for adding the node to the
end of the list that we created near the top of this section. This routine is called if the current
pointer is the last one in the list so the new one would be added on to the end.
Similarly, the routine get_temp(temp) is a routine that reads in the details for the new node similar
to the one defined just above.
When a node is deleted, the space that it took up should be reclaimed. Otherwise the computer
will eventually run out of memory space. This is done with the delete instruction:
However, we can't just delete the nodes willy-nilly as it would break the chain. We need to
reassign the pointers and then delete the node at the last moment. Here is how we go about
deleting the first node in the linked list:
void delete_start_node()
{ node *temp;
temp = start_ptr;
start_ptr = start_ptr->nxt;
delete temp;
}
Deleting a node from the end of the list is harder, as the temporary pointer must find where the
end of the list is by hopping along from the start. This is done using code that is almost identical
to that used to insert a node at the end of the list. It is necessary to maintain two temporary
pointers, temp1 and temp2. The pointer temp1 will point to the last node in the list and temp2 will
point to the previous node. We have to keep track of both as it is necessary to delete the last node
and immediately afterwards, to set the nxt pointer of the previous node to NULL (it is now the
new last node).
1. Look at the start pointer. If it is NULL, then the list is empty, so print out a "No nodes to
delete" message.
2. Make temp1 point to whatever the start pointer is pointing to.
3. If the nxt pointer of what temp1 indicates is NULL, then we've found the last node of the
list, so jump to step 7.
4. Make another pointer, temp2, point to the current node in the list.
5. Make temp1 point to the next item in the list.
6. Go to step 3.
7. If you get this far, then the temporary pointer, temp1, should point to the last item in the
list and the other temporary pointer, temp2, should point to the last-but-one item.
8. Delete the node pointed to by temp1.
9. Mark the nxt pointer of the node pointed to by temp2 as NULL - it is the new last node.
Let's try it with a rough drawing. This is always a good idea when you are trying to understand
an abstract data type. Suppose we want to delete the last node from this list:
Firstly, the start pointer doesn't point to NULL, so we don't have to display a "Empty list, wise
guy!" message. Let's get straight on with step2 - set the pointer temp1 to the same as the start
pointer:
The nxt pointer from this node isn't NULL, so we haven't found the end node. Instead, we set the
pointer temp2 to the same node as temp1
and then move temp1 to the next node in the list:
Going back to step 3, we see that temp1 still doesn't point to the last node in the list, so we make
temp2 point to what temp1 points to
start_ptr
NULL
temp 2 temp1
NULL
temp 2 temp1
Now we have reached step 8. The next thing to do is to delete the node pointed to by temp1
We suppose you want some code for all that! All right then ....
void delete_end_node()
{node *temp1, *temp2;
if (start_ptr == NULL)
cout << "The list is empty!" << endl;
else
{ temp1 = start_ptr;
while (temp1->nxt != NULL)
{ temp2 = temp1;
temp1 = temp1->nxt;
}
delete temp1;
temp2->nxt = NULL;
}
}
The code seems a lot shorter than the explanation!
Now, the sharp-witted amongst you will have spotted a problem. If the list only contains one
node, the code above will malfunction. This is because the function goes as far as the temp1 =
start_ptr statement, but never gets as far as setting up temp2. The code above has to be adapted so
that if the first node is also the last (has a NULL nxt pointer), then it is deleted and the start_ptr
pointer is assigned to NULL. In this case, there is no need for the pointer temp2:
void delete_end_node()
{
node *temp1, *temp2;
if (start_ptr == NULL)
cout << "The list is empty!" << endl;
else
{ temp1 = start_ptr;
if (temp1->nxt == NULL) // This part is new!
{ delete temp1;
start_ptr = NULL;
}
else
{ while (temp1->nxt != NULL)
{ temp2 = temp1;
temp1 = temp1->nxt;
}
delete temp1;
temp2->nxt = NULL;
}
}
A doubly linked list is one where there are links from each node in both directions:
You will notice that each node in the list has two pointers; one to the next node and one to
the previous one - again, the ends of the list are defined by NULL pointers. Also there is no
pointer to the start of the list. Instead, there is simply a pointer to some position in the list that
can be moved left or right.
The reason we needed a start pointer in the ordinary linked list is because, having moved on from
one node to another, we can't easily move back, so without the start pointer, we would lose track
of all the nodes in the list that we have already passed. With the doubly linked list, we can move
the current pointer backwards and forwards at will.
void add_node_at_end ()
{ // Declare a temporary pointer and move it to the end
node *temp = current;
while (temp->nxt != NULL)
temp = temp->nxt;
// Declare a new node and link it in
node *temp2;
temp2 = new node;
temp2->name = new_name; // Store the new name in the node
temp2->nxt = NULL; // This is the new start of the list
temp2->prv = temp; // Links to current list
temp->nxt = temp2;
}
Here, the new name is passed to the appropriate function as a parameter. We'll go through the
function for adding a node to the right-most end of the list. The method is similar for adding a
node at the other end. Firstly, a temporary pointer is set up and is made to march along the list
until it points to last node in the list.
Start_Ptr
After that, a new node is declared, and the name is copied into it. The nxt pointer of this new
node is set to NULL to indicate that this node will be the new end of the list.
The prv pointer of the new node is linked into the last node of the existing list.
The nxt pointer of the current end of the list is set to the new node.
3.4. Stacks
A simple data structure, in which insertion and deletion occur at the same end, is termed (called) a
stack. It is a LIFO (Last In First Out) structure.
The operations of insertion and deletion are called PUSH and POP
TOS=> 8
TOS=> 4 4 TOS=> 4
1 1 1
3 3 3
6 6 6
Our Purpose:
To develop a stack implementation that does not tie us to a particular data type or to a particular
implementation.
Implementation:
Stacks can be implemented both as an array (contiguous list) and as a linked list. We want a set of
operations that will work with either type of implementation: i.e. the method of implementation is
hidden and can be changed without affecting the programs that use them.
The Basic Operations:
Push()
{
if there is room {
put an item on the top of the stack
else
give an error message
}
}
Pop()
{
if stack not empty {
return the value of the top item
remove the top item from the stack
}
else {
give an error message
}
}
CreateStack()
{
remove existing items from the stack
initialise the stack to empty
}
Algorithm:
Step-1: Increment the Stack TOP by 1. Check whether it is always less than the Upper Limit of
the stack. If it is less than the Upper Limit go to step-2 else report -"Stack Overflow"
Step-2: Put the new element at the position pointed by the TOP
Implementation:
push(int item)
{
top = top + 1;
if(top < UPPERLIMIT)
stack[top] = item; /*step-1 & 2*/
else
cout<<"Stack Overflow";
}
Note:- In array implementation,we have taken TOP = -1 to signify the empty stack, as this
simplifies the implementation.
POP is the synonym for delete when it comes to Stack. So, if you're taking an array as the stack,
remember that you'll return an error message, "Stack underflow", if an attempt is made to Pop an
item from an empty Stack. OK.
Algorithm
Step-1: If the Stack is empty then give the alert "Stack underflow" and quit; or else go to step-2
Step-2: a) Hold the value for the element pointed by the TOP
b) Put a NULL value instead
c) Decrement the TOP by 1
Implementation:
int pop()
{
int del_val = 0;
if(top == -1)
cout<<"Stack underflow"; /*step-1*/
else
{
del_val = stack[top]; /*step-2*/
stack[top] = NULL;
top = top -1;
}
return(del_val);
}
Note: - Step-2: (b) signifies that the respective element has been deleted.
It’s very similar to the insertion operation in a dynamic singly linked list. The only difference is
that here you'll add the new element only at the end of the list, which means addition can happen
only from the TOP. Since a dynamic list is used for the stack, the Stack is also dynamic, means it
has no prior upper limit set. So, we don't have to check for the Overflow condition at all!
Algorithm
Implementation:
struct node{
int item;
struct node *next;
}
struct node *stack = NULL; /*stack is initially empty*/
struct node *top = stack;
main()
{
..
..
push(item);
..
}
push(int item)
{
if(stack == NULL) /*step-1*/
{
newnode = new node /*step-2*/
newnode -> item = item;
newnode -> next = NULL;
stack = newnode;
top = stack;
}
else
{
newnode = new node; /*step-3*/
newnode -> item = item;
newnode -> next = NULL;
top ->next = newnode;
top = newnode; /*step-4*/
}
}
This is again very similar to the deletion operation in any Linked List, but you can only delete
from the end of the list and only one at a time; and that makes it a stack. Here, we'll have a list
pointer, "target", which will be pointing to the last but one element in the List (stack). Every time
we POP, the TOP most element will be deleted and "target" will be made as the TOP most
element.
In step[1] we got the "target" pointing to the last but one node.
In step[2] we freed the TOP most element.
In step[3] we made the "target" node as our TOP most element.
Supposing you have only one element left in the Stack, then
we won't make use of "target" rather we'll take help of our
"bottom" pointer. See how...
Algorithm:
Step-1: If the Stack is empty then give an alert message "Stack Underflow" and quit; or else
proceed
Step-2: If there is only one element left go to step-3 or else step-4
Step-3: Free that element and make the "stack", "top" and "bottom" pointers point to NULL and
quit
Step-4: Make "target" point to just one element before the TOP; free the TOP most element;
make "target" as your TOP most element
Implementation:
struct node
{
int nodeval;
struct node *next;
}
struct node *stack = NULL; /*stack is initially empty*/
struct node *top = stack;
main()
{
int newvalue, delval;
..
push(newvalue);
..
delval = pop(); /*POP returns the deleted value from the stack*/
}
int pop( )
{
int pop_val = 0;
struct node *target = stack;
if(stack == NULL) /*step-1*/
cout<<"Stack Underflow";
else
{
if(top == bottom) /*step-2*/
{
pop_val = top -> nodeval; /*step-3*/
delete top;
stack = NULL;
top = bottom = stack;
}
else /*step-4*/
{
while(target->next != top) target = target ->next;
pop_val = top->nodeval;
delete top;
top = target;
target ->next = NULL;
}
}
return(pop_val);
}
simple calculator: 45
Question:
Can we develop a method of evaluating arithmetic expressions without having to ‘look
ahead’ or ‘look back’? ie consider the quadratic formula:
x = (-b+(b^2-4*a*c)^0.5)/(2*a)
In it’s current form we cannot solve the formula without considering the ordering of the
parentheses. i.e. we solve the innermost parenthesis first and then work outwards also
considering operator precedence. Although we do this naturally, consider developing an
algorithm to do the same . . . . . . possible but complex and inefficient. Instead . . . .
Computers solve arithmetic expressions by restructuring them so the order of each calculation is
embedded in the expression. Once converted an expression can then be solved in one pass.
Types of Expression
The normal (or human) way of expressing mathematical expressions is called infix form, e.g.
4+5*5. However, there are other ways of representing the same expression, either by writing all
operators before their operands or after them,
e.g.: 4 5 5 * +
+4*55
This method is called Polish Notation (because this method was discovered by the Polish
mathematician Jan Lukasiewicz).
When the operators are written before their operands, it is called the prefix form
e.g. + 4 * 5 5
When the operators come after their operands, it is called postfix form (suffix form or reverse
polish notation)
e.g. 4 5 5 * +
Postfix notation arises from the concept of post-order traversal of an expression tree (see Weiss
p. 93 - this concept will be covered when we look at trees).
For now, consider postfix notation as a way of redistributing operators in an expression so that
their operation is delayed until the correct time.
Notice the order of the operands remain the same but the operands are redistributed in a non-
obvious way (an algorithm to convert infix to postfix can be derived).
Purpose
The reason for using postfix notation is that a fairly simple algorithm exists to evaluate such
expressions based on using a stack.
Postfix Evaluation
Unary operators: unary minus, square root, sin, cos, exp, etc.,
So for 6 5 2 3 + 8 * + 3 + *
TOS=> 5
5
6
TOS=> 8
5 TOS=> 40
5 5
6 6
(8, 5 popped, 40 pushed)
TOS=> 3
TOS=> 45 45
6 6
(40, 5 popped, 45 pushed, 3 pushed)
Next is operator +, so 3 and 45 are popped and 45+3=48 is pushed
TOS=> 48
6
Next is operator *, so 48 and 6 are popped, and 6*48=288 is pushed
TOS=> 288
Now there are no more items and there is a single value on the stack, representing the final
answer 288.
Note the answer was found with a single traversal of the postfix expression, with the stack being
used as a kind of memory storing values that are waiting for their operands.
Algorithm
2:/*
1:+-
The algorithm immediately passes values (operands) to the postfix expression, but
remembers (saves) operators on the stack until their right-hand operands are fully
translated.
eg., consider the infix expression a+b*c+(d*e+f)*g
Stack Output
ab
TOS=> +
TOS=> * abc
abc*+
TOS=> +
TOS=> *
abc*+de
(
TOS=> +
abc*+de*f
(
abc*+de*f+
+
TOS=>
TOS=> * abc*+de*f+g
empty abc*+de*f+g*+
3.4.5.3. Function Calls
When a function is called, arguments (including the return address) have to be passed to the
called function.
If these arguments are stored in a fixed memory area then the function cannot be called
recursively since the 1st return address would be overwritten by the 2nd return address before the
first was used:
10 call function abc(); /* retadrs = 11 */
11 continue;
...
90 function abc;
91 code;
92 if (expression)
93 call function abc(); /* retadrs = 94 */
94 code
95 return /* to retadrs */
A stack allows a new instance of retadrs for each call to the function. Recursive calls on the
function are limited only by the extent of the stack.
10 call function ABC (); /* retadrs1 = 11 */
11 continue;
...
90 function ABC;
91 code;
92 if (expression)
93 call function abc(); /* retadrs2 = 94 */
94 code
95 return /* to retadrsn */
5.Queue
a data structure that has access to its data at the front and rear.
operates on FIFO (Fast In First Out) basis.
uses two pointers/indices to keep tack of information/data.
has two basic operations:
o enqueue - inserting data at the rear of the queue
o dequeue – removing data at the front of the queue
dequeue enqueue
Front Rear
Example:
Analysis:
Consider the following structure: int Num[MAX_SIZE];
We need to have two integer variables that tell:
- the index of the front element
- the index of the rear element
We also need an integer variable that tells:
- the total number of data in the queue
int FRONT =-1,REAR =-1;
int QUEUESIZE=0;
To enqueue data to the queue
o check if there is space in the queue
REAR<MAX_SIZE-1 ?
Yes: - Increment REAR
- Store the data in Num[REAR]
- Increment QUEUESIZE
FRONT = = -1?
Yes: - Increment FRONT
No: - Queue Overflow
To dequeue data from the queue
o check if there is data in the queue
QUEUESIZE > 0 ?
Yes: - Copy the data in Num[FRONT]
- Increment FRONT
- Decrement QUEUESIZE
No: - Queue Underflow
Implementation:
const int MAX_SIZE=100;
int FRONT =-1, REAR =-1;
int QUEUESIZE = 0;
void enqueue(int x)
{
if(Rear<MAX_SIZE-1)
{
REAR++;
Num[REAR]=x;
QUEUESIZE++;
if(FRONT = = -1)
FRONT++;
}
else
cout<<"Queue Overflow";
}
int dequeue()
{
int x;
if(QUEUESIZE>0)
{
x=Num[FRONT];
FRONT++;
QUEUESIZE--;
}
else
cout<<"Queue Underflow";
return(x);
}
5.2. Circular array implementation of enqueue and dequeue operations
A problem with simple arrays is we run out of space even if the queue never reaches the size of
the array. Thus, simulated circular arrays (in which freed spaces are re-used to store data) can be
used to solve this problem.
The circular array implementation of a queue with MAX_SIZE can be simulated as follows:
12 11
13
10
9
MAX_SIZE - 1 8
0 7
1 6
2 5
3 4
Analysis:
Consider the following structure: int Num[MAX_SIZE];
We need to have two integer variables that tell:
- the index of the front element
- the index of the rear element
We also need an integer variable that tells:
- the total number of data in the queue
int FRONT =-1,REAR =-1;
int QUEUESIZE=0;
Implementation:
const int MAX_SIZE=100;
int FRONT =-1, REAR =-1;
int QUEUESIZE = 0;
void enqueue(int x)
{
if(QUEUESIZE<MAX_SIZE)
{
REAR++;
if(REAR = = MAX_SIZE)
REAR=0;
Num[REAR]=x;
QUEUESIZE++;
if(FRONT = = -1)
FRONT++;
}
else
cout<<"Queue Overflow";
}
int dequeue()
{
int x;
if(QUEUESIZE>0)
{
x=Num[FRONT];
FRONT++;
if(FRONT = = MAX_SIZE)
FRONT = 0;
QUEUESIZE--;
}
else
cout<<"Queue Underflow";
return(x);
}
Front Rear
Example: Consider the following queue of persons where females have higher priority
than males (gender is the key to give priority).
Thus, in the above example the implementation of the dequeue operation need to be
modified.
Example: The following two queues can be created from the above priority queue.
Aster Meron Abebe Alemu Belay Kedir Yonas
Female Female Male Male Male Male Male
Algorithm:
create empty females and males queue
while (PriorityQueue is not empty)
{
Data=DequeuePriorityQueue(); // delete data at the front
if(gender of Data is Female)
EnqueueFemale(Data);
else
EnqueueMale(Data);
}
Example: The following two queues (females queue has higher priority than the males
queue) can be merged to create a priority queue.
Aster Meron Abebe Alemu Belay Kedir Yonas
Female Female Male Male Male Male Male
Algorithm:
6. Trees
A tree is a set of nodes and edges that connect pairs of nodes that connect pairs of nodes. It is
an abstract model of a hierarchical structure. Rooted tree has the following structure:
One node distinguished as root.
Every node C except the root is connected from exactly other node P. P is C's parent,
and C is one of C's children.
There is a unique path from the root to the each node.
The number of edges in a path is the length of the path.
B E F G
C D H I J
K L M
Root: a node with out a parent. A
Internal node: a node with at least one child. A, B, F, I, J
External (leaf) node: a node without a child. C, D, E, H, K, L, M, G
Ancestors of a node: parent, grandparent, grand-grandparent, etc of a node.
Ancestors of K A, F, I
Descendants of a node: children, grandchildren, grand-grandchildren etc of a node.
Descendants of F H, I, J, K, L, M
Depth of a node: number of ancestors or length of the path from the root to the node.
Depth of H 2
Height of a tree: depth of the deepest node. 3
Subtree: a tree consisting of a node and its descendants.
H I J
K L M
Binary tree: a tree in which each node has at most two children called left child and right child.
Full binary tree: a binary tree where each node has either 0 or 2 children.
Balanced binary tree: a binary tree where each node except the leaf nodes has left and right
children and all the leaves are at the same level.
Complete binary tree: a binary tree in which the length from the root to any leaf node is either h
or h-1 where h is the height of the tree. The deepest level should also be
filled from left to right.
Binary search tree (ordered binary tree): a binary tree that may be empty, but if it is not empty it
satisfies the following.
Every node has a key and no two elements have the same key.
The keys in the right subtree are larger than the keys in the root.
The keys in the left subtree are smaller than the keys in the root.
The left and the right subtrees are also binary search trees.
10
6 15
4 8 14 18
7 12
16 19
11 13
struct DataModel
{
Declaration of data fields
DataModel * Left, *Right;
};
DataModel *RootDataModelPtr=NULL;
6.3. Operations on Binary Search Tree
6.3.1. Insertion
When a node is inserted the definition of binary search tree should be preserved. Suppose
there is a binary search tree whose root node is pointed by RootNodePtr and we want to
insert a node (that stores 17) pointed by InsNodePtr.
17 17
InsertBST(RootNodePtr, InsNodePtr)
17 10
10
6 15 6 15
4 8 14 4 8 14 18
18
7 12 7 12 16 19
16 19
11 13 11 13 17
6.3.2. Traversing
Binary search tree can be traversed in three ways.
a. Pre order traversal - traversing binary tree in the order of parent, left and right.
b. Inorder traversal - traversing binary tree in the order of left, parent and right.
c. Postorder traversal - traversing binary tree in the order of left, right and parent.
Example:
RootNodePtr
10
6 15
4 8 14 18
7 12 16 19
11 13 17
Preorder traversal - 10, 6, 4, 8, 7, 15, 14, 12, 11, 13, 18, 16, 17, 19
Inorder traversal - 4, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
==> Used to display nodes in ascending order.
Postorder traversal- 4, 7, 8, 6, 11, 13, 12, 14, 17, 16, 19, 18, 15, 10
Example:
+
B C E F
Function calls:
Preorder(RootNodePtr);
Inorder(RootNodePtr);
Postorder(RootNodePtr);
6.3.4. Searching
To search a node (whose Num value is Number) in a binary search tree (whose root node is
pointed by RootNodePtr), one of the three traversal methods can be used.
Function call:
ElementExists = SearchBST (RootNodePtr, Number);
// ElementExists is a Boolean variable defined as: bool ElementExists = false;
Implementation:
bool SearchBST (Node *RNP, int x)
{
if(RNP = = NULL)
return(false);
else if(RNP->Num = = x)
return(true);
else if(RNP->Num > x)
return(SearchBST(RNP->Left, x));
else
return(SearchBST(RNP->Right, x));
}
When we search an element in a binary search tree, sometimes it may be necessary for the
SearchBST function to return a pointer that points to the node containing the element
searched. Accordingly, the function has to be modified as follows.
Function call:
SearchedNodePtr = SearchBST (RootNodePtr, Number);
// SearchedNodePtr is a pointer variable defined as: Node *SearchedNodePtr=NULL;
Implementation:
Node *SearchBST (Node *RNP, int x)
{
if((RNP = = NULL) || (RNP->Num = = x))
return(RNP);
else if(RNP->Num > x)
return(SearchBST(RNP->Left, x));
else
return(SearchBST (RNP->Right, x));
}
6.3.5. Deletion
To delete a node (whose Num value is N) from binary search tree (whose root node is
pointed by RootNodePtr), four cases should be considered. When a node is deleted the
definition of binary search tree should be preserved.
Consider the following binary search tree.
RootNodePtr
10
6 14
3 8 12 18
4 7 9 11 13 16 19
2
15 17
1 5
Delete 7
10
10
6 14
6 14
3 8 12 18
3 8 12 18
4 9 11 13 16 19
16 2
2 4 7 9 11 13 19
15 17
17 1 5
5 15
1
Delete 2
10
10
6 14
6 14
3 8 12 18
3 8 12 18
4 9 11 13 16 19
16 1
2 4 7 9 11 13 19
15 17
17 5
5 15
1
Delete 2
10
10
6 14
6 14
3 8 12 18
3 8 12 18
4 9 11 13 16 19
16 1
2 4 7 9 11 13 19
15 17
17 5
5 15
1
Delete 6
10 10
6 14 8 14
3 8 12 18 7 9 12 18
4 7 9 11 13 16 19 11 13 16 19
2 3
15 17 15 17
1 5 4
2
1 5
RootNodePtr
RootNodePtr
Delete 6 10
10
3 14
6 14
2 4 12 18
3 8 12 18
5 11 13 16 19
1
4 7 9 11 13 16 19
2
8 15 17
15 17
1 5
7 9
Approach 2: Deletion by copying- the following is done
Copy the node containing the largest element in the left (or the smallest element in the right)
to the node containing the element to be deleted
Delete the copied node
RootNodePtr RootNodePtr
Delete 6
10 10
6 14 5 14
3 8 12 18 3 8 12 18
4 7 9 11 13 16 19 4 7 9 11 13 16 19
2 2
15 17 15 17
1 5 1
RootNodePtr
RootNodePtr
Delete 6 10
10
7 14
6 14
3 8 12 18
3 8 12 18
4 9 11 13 16 19
16 2
2 4 7 9 11 13 19
5 15 17
15 17 1
1 5
Case 4: Deleting the root node, 10
Approach 1: Deletion by merging- one of the following is done
If the tree has only one node the root node pointer is made to point to nothing (NULL)
If the root node has left child
o the root node pointer is made to point to the left child
o the right child of the root node is made the right child of the node containing the largest
element in the left of the root node
If root node has right child
o the root node pointer is made to point to the right child
o the left child of the root node is made the left child of the node containing the smallest
element in the right of the root node
RootNodePtr RootNodePtr
RootNodePtr
10 Delete 10 6
6 14 3 8
3 8 12 18 4 7 9
2
4 7 9 11 13 16 19
2 1 5 14
15 17 12
1 5 18
11 13 16 19
15 17
RootNodePtr RootNodePtr
RootNodePtr
10 14
Delete 10
6 14 12 18
3 8 12 18 16
11 13 19
4 7 9 11 13 16 19 17
2 6 15
15 17 8
1 5 3
2 4 7 9
1 5
Approach 2: Deletion by copying- the following is done
Copy the node containing the largest element in the left (or the smallest element in the right)
to the node containing the element to be deleted
Delete the copied node
RootNodePtr
RootNodePtr
9
10 Delete 10
6 14
6 14
3 8 12 18
3 8 12 18
4 7 11 13 16 19
16 2
2 4 7 9 11 13 19
15 17
17 1 5
5 15
1
RootNodePtr
RootNodePtr
11
10 Delete 10
6 14
6 14
3 8 12 18
3 8 12 18
4 7 9 16 19
16 2 13
2 4 7 9 11 13 19
15 17
17 1 5
5 15
1