CS301 - Handouts - Data Structure
CS301 - Handouts - Data Structure
___________________________________________________________________
Data Structure
Page 1 of 519
Page 2 of 519
Lecture No. 01
Reading Material
Data Structures and algorithm analysis in C++
Chapter. 3
3.1, 3.2, 3.2.1
Summary
Welcome to the course of data structure. This is very important subject as the topics
covered in it will be encountered by you again and again in the future courses. Due to
its great applicability, this is usually called as the foundation course. You have
already studied Introduction to programming using C and C++ and used some data
structures. The focus of that course was on how to carry out programming with the
use of C and C++ languages besides the resolution of different problems. In this
course, we will continue problem solving and see that the organization of data in
some cases is of immense importance. Therefore, the data will be stored in a special
way so that the required result should be calculated as fast as possible.
Following are the goals of this course:
Prepare the students for (and is a pre-requisite for) the more advanced material
students will encounter in later courses.
Cover well-known data structures such as dynamic arrays, linked lists, stacks,
queues, trees and graphs.
Implement data structures in C++
You have already studied the dynamic arrays in the previous course. We will now
discuss linked lists, stacks, queues, trees and graphs and try to resolve the problems
with the help of these data structures. These structures will be implemented in C++
language. We will also do programming assignments to see the usage and importance
of these structures.
Page 4 of 519
Page 6 of 519
Arrays
You have already studied about arrays and are well-versed with the techniques to
utilize these data structures. Here we will discuss how arrays can be used to solve
computer problems. Consider the following program:
main( int argc, char** argv )
{
int x[6];
int j;
for(j = 0; j < 6; j++)
x[j] = 2 * j;
}
We have declared an int array of six elements and initialized it in the loop.
Lets revise some of the array concepts. The declaration of array is as int x[6]; or
float x[6]; or double x[6]; You have already done these in your programming
assignments. An array is collection of cells of the same type. In the above program,
we have array x of type int of six elements. We can only store integers in this array.
We cannot put int in first location, float in second location and double in third
location. What is x? x is a name of collection of items. Its individual items are
numbered from zero to one less than array size. To access a cell, use the array name
and an index as under:
x[0], x[1], x[2], x[3], x[4], x[5]
To manipulate the first element, we will use the index zero as x[0] and so on. The
arrays look like in the memory as follows:
X[0]
Array cells are
contiguous in
computer memory
X[1]
X[2]
X[3]
X[4]
X[5]
Array occupies contiguous memory area in the computer. In case of the above
example, if some location is assigned to x[0], the next location can not contain data
other than x[1]. The computer memory can be thought of as an array. It is a very big
array. Suppose a computer has memory of 2MB, you can think it as an array of size 2
million and the size of each item is 32 bits. You will study in detail about it in the
computer organization, and Assembly language courses. In this array, we will put our
Page 7 of 519
Page 8 of 519
Page 9 of 519
Description
Create a new list (presumably empty)
Set one list to be a copy of another
Clear a list (remove all elements)
Insert element X at a particular position in the list
Remove element at some position in the list
Get element at a given position
Replace the element at a given position with X
Determine if the element X is in the list
Returns the length of the list.
createList() is a function which creates a new list. For example to create an array, we
use int x[6] or int* y = new int[20]; we need similar functionality in lists too. The
copy() function will create a copy of a list. The function clear() will remove all the
elements from a list. We want to insert a new element in the list, we also have to tell
where to put it in the list. For this purpose insert(X, position) function is used.
Similarly the function remove(position) will remove the element at position. To get an
element from the list get(position) function is used which will return the element at
position. To replace an element in the list at some position the function update(X,
position) is used. The function find(X) will search X in the list. The function length()
tells us about the number of elements in the list.
We need to know what is meant by particular position we have used ? for this in
the above table. There are two possibilities:
Use the actual index of element: i.e. insert it after element 3, get element
number 6. This approach is used with arrays
Use a current marker or pointer to refer to a particular position in the list.
Page 10 of 519
Data Structures
Lecture No. 02
Reading Material
Page 11 of 519
Chapter. 3
3.1, 3.2, 3.2.1, 3.2.2
Summary
1)
2)
3)
4)
List Implementation
add Method
next Method
remove Method
find Method
Other Methods
Analysis Of Array List
List Using Linked Memory
Linked List
Today, we will discuss the concept of list operations. You may have a fair idea of
start operation that sets the current pointer to the first element of the list while the
tail operation moves the current pointer to the last element of the list. In the previous
lecture, we discussed the operation next that moves the current pointer one element
forward. Similarly, there is the back operation which moves the current pointer one
element backward.
List Implementation
Now we will see what the implementation of the list is and how one can create a list
in C++. After designing the interface for the list, it is advisable to know how to
implement that interface. Suppose we want to create a list of integers. For this
purpose, the methods of the list can be implemented with the use of an array inside.
For example, the list of integers (2, 6, 8, 7, 1) can be represented in the following
manner where the current position is 3.
A
current
size
In this case, we start the index of the array from 1 just for simplification against the
usual practice in which the index of an array starts from zero in C++. It is not
necessary to always start the indexing from zero. Sometimes, it is required to start the
indexing from 1. For this, we leave the zeroth position and start using the array from
index 1 that is actually the second position. Suppose we have to store the numbers
from 1 to 6 in the array. We take an array of 7 elements and put the numbers from the
index 1. Thus there is a correspondence between index and the numbers stored in it.
This is not very useful. So, it does not justify the non-use of zeroth position of the
array out-rightly. However for simplification purposes, it is good to use the index
from 1.
add Method
Now we will talk about adding an element to the list. Suppose there is a call to add an
Page 12 of 519
7
4
current
size
Now in the second step, we put the element 9 at the empty space i.e. position 4. Thus
the array will attain the following shape. The figure shows the elements in the array in
the same order as stored in the list.
A
current
size
We have moved the current position to 4 while increasing the size to 6. The size
shows that the elements in the list. Where as the size of the array is different that we
have defined already to a fixed length, which may be 100, 200 or even greater.
next Method
Now lets see another method, called next. We have talked that the next method
moves the current position one position forward. In this method, we do not add a new
element to the list but simply move the pointer one element ahead. This method is
required while employing the list in our program and manipulating it according to the
requirement. There is also an array to store the list in it. We also have two variablescurrent and size to store the position of current pointer and the number of elements in
the list. By looking on the values of these variables, we can find the state of the list
i.e. how many elements are in the list and at what position the current pointer is.
The method next is used to know about the boundary conditions of the list i.e. the
array being used by us to implement the list. To understand the boundary conditions,
we can take the example of an array of size 100 to implement the list. Here, 100
elements are added to the array. Lets see what happens when we want to add 101st
element to the array? We used to move the current position by next method and
reached the 100th position. Now, in case of moving the pointer to the next position
(i.e. 101st), there will be an error as the size of the array is 100, having no position
after this point. Similarly if we move the pointer backward and reach at the first
position regardless that the index is 0 or 1. But what will happen if we want to move
backward from the first position? These situations are known as boundary conditions
and need attention during the process of writing programs when we write the code to
use the list. We will take care of these things while implementing the list in C++
programs.
remove Method
We have seen that the add method adds an element in the list. Now we are going to
discuss the remove method. The remove method removes the element residing at the
current position. The removal of the element will be carried out as follows. Suppose
Page 13 of 519
current
size
6
5
We fill in the blank position left by the removal of 7 by shifting the values on the
right of position 5 to the left by one space. This means that we shift the remaining
elements on the right hand side of the current position one place to the left so that the
element next to the removed element (i.e. 1) takes its place (the fifth position) and
becomes the current position element. We do not change the current pointer that is
still pointing to the position 5. Thus the current pointer remains pointing to the
position 5 despite the fact that there is now element 1 at this place instead of 7. Thus
in the remove method, when we remove an element, the element next to it on the right
hand side comes at its place and the remaining are also shifted one place to the right.
This step is represented by the following figure.
current
size
find Method
Now lets talk about a function, used to find a specific element in the array. The find
(x) function is used to find a specific element in the array. We pass the element, which
is to be found, as an argument to the find function. This function then traverses the
array until the specific element is found. If the element is found, this function sets the
current position to it and returns 1 i.e. true. On the other hand, if the element is not
found, the function returns 0 i.e. false. This indicates that the element was not found.
Following is the code of this find(x) function in C++.
int find (int x)
{
int j ;
for (j = 1; j < size + 1; j++ )
if (A[j] == x )
break ;
if ( j < size + 1)
// x is found
{
current = j ;
//current points to the position where x found
return 1 ;
// return true
}
return 0 ;
//return false, x is not found
}
In the above code, we execute a for loop to traverse the array. The number of
Page 14 of 519
Page 15 of 519
Page 16 of 519
Page 17 of 519
Linked List
For the utilization of the concept of linked memory, we usually define a structure,
called linked list. To form a linked list, at first, we define a node. A node comprises
two fields. i.e. the object field that holds the actual list element and the next that holds
the starting location of the next node.
object
next
A chain of these nodes forms a linked list. Now lets consider our previous list, used
with an array i.e. 2, 6, 8, 7, 1. Following is the figure which represents the list stored
as a linked list.
Head
2
size = 5
current
This diagram just represents the linked list. In the memory, different nodes may occur
at different locations but the next part of each node contains the address of the next
node. Thus it forms a chain of nodes which we call a linked list.
While using an array we knew that the array started from index 1that means the first
element of the list is at index 1. Similarly in the linked list we need to know the
starting point of the list. For this purpose, we have a pointer head that points to the
first node of the list. If we dont use head, it will not be possible to know the starting
position of the list. We also have a pointer current to point to the current node of the
list. We need this pointer to add or remove current node from the list. Here in the
linked list, the current is a pointer and not an index as we used while using an array.
The next field of the last node points to nothing .It is the end of the list. We place the
memory address NULL in the last node. NULL is an invalid address and is
inaccessible.
Now again consider the list 2, 6, 8, 7, 1. The previous figure represents this list as a
linked list. In this linked list, the head points to 2, 2 points to 6, 6 points to 8, 8 points
to 7 and 7 points to 1. Moreover we have the current position at element 8.
This linked list is stored in the memory. The following diagram depicts the process
through which this linked list is stored in the memory.
Page 18 of 519
current
1051
1052
1063
1053
1054
1055
1051
1056
1057
1058
7
1060
1059
head
1060
1061
1062
0
1054
1063
1064
8
1057
1065
We can see in the figure that each memory location has an address. Normally in
programming, we access the memory locations by some variable names. These
variable names are alias for these locations and are like labels that are put to these
memory locations. We use head and current variable names instead of using the
memory address in numbers for starting and the current nodes. In the figure, we see
that head is the name of the memory location 1062 and the name current is used for
the memory address 1053. The head holds the address 1054 and the element 2, the
first one in the list, is stored in the location 1054. Similarly current holds the address
1063 where the element 8 is stored which is our current position in the list. In the
diagram, two memory locations comprise a node. So we see that the location 1054
holds the element 2 while the next location 1055 holds the address of the memory
location (1051) where the next element of the list (i.e. 6) is stored. Similarly the next
part of the node that has value 6 holds the memory address of the location occupied
by the next element (i.e. 8) of the list. The other nodes are structured in a similar
fashion. Thus, by knowing the address of the next element we can traverse the whole
list.
Page 19 of 519
Data Structures
Lecture No. 03
Reading Material
Data Structures and algorithm analysis in C++
Chapter. 3
3.2.2, 3.2.3, 3.2.5
Summary
In the previous lectures, we used an array to construct a list data structure and
observed the limitation that array being of fixed size can only store a fixed number of
elements. Therefore, no more elements can be stored after the size of the array is
reached.
In order to resolve this, we adopted a new data structure called linked list. We started
discussing, how linked lists are stored in computer memory and how memory chains
are formed.
Page 20 of 519
currenten
1052
1063
1053
1063
1054
1055
1051
1056
head
2
1051
1057
1058
1060
1059
headent
1060
1061
1062
1054
1063
1064
1057
1065
Fig 1. Linked list in memory
There are two parts of this figure. On the left is the linked list chain that is actually the
conceptual view of the linked list and on the right is the linked list inside the
computer memory. Right part is a snapshot of the computer memory with memory
addresses from 1051 to 1065. The head pointer is pointing to the first element in the
linked list. Note that head itself is present in the memory at address 1062. It is
actually a pointer containing the memory address 1054. Each node in the above linked
list has two parts i.e. the data part of the node and the pointer to the next node. The
first node of the linked list pointed by the head pointer is stored at memory address
1054. We can see the data element 2 present at that address. The second part of the
first node contains the memory address 1051. So the second linked lists node starts at
memory address 1051. We can use its pointer part to reach the third node of the list
and in this way, we can traverse the whole list. The last node contains 1 in its data
part and 0 in its pointer part. 0 indicates that it is not pointing to any node and it is the
last node of the linked list.
current
3
9
newNode
In the above figure, there is a linked list that contains five nodes with data elements as
2, 6, 8, 7 and 1. The current pointer is pointing to the node with element as 8. We
want to insert a new node with data element 9. This new node will be inserted at the
current position (the position where the current pointer is pointing to). This insertion
operation is performed in a step by step fashion.
- The first step is to point next pointer of the new node (with data element as 9) to
Page 22 of 519
Now, the updated linked list has nodes with data elements as 2, 6, 8, 9, 7 and 1. The
list size has become 6.
Page 23 of 519
Page 24 of 519
Page 25 of 519
Page 26 of 519
List list;
size = 0
currentNod
headNod
list.add(2);
size = 1
lastCurrentNod
currentNod
list.add(6);
headNode
size = 2
lastCurrentNod
Fig 3. Add operation of linked list
Following is the crux of this add() operation :
Firstly, it will make a new node by calling Node class constructor. Insert the value
e.g. 2. of the node into the node by calling the set method. Now if the list already
exists (has some elements inside or its size is non-zero), it will insert the node after
the current position. If the list does not already exist, this node is added as the first
element inside the list.
Lets try to add few more elements into the above linked list in the figure. The
following are the lines of code to be executed to add nodes with values 8, 7 and 1 into
the linked list.
list.add(8); list.add(7);
list.add(1);
currentNode
headNode
size = 5
lastCurrentNode
Page 27 of 519
Example Program
Given below is the full source code of the example program. You can copy, paste and
compile it right away. In order to understand the linked list concept fully, it is highly
desirable that you understand and practice with the below code.
#include <iostream.h>
#include <stdlib.h>
/* The Node class */
class Node
{
public:
int
get() { return object; };
Page 28 of 519
Page 29 of 519
Page 30 of 519
1
2
3
4
5
2
6
8
7
1
Page 31 of 519
Data Structures
Lecture No. 04
Reading Material
Data Structures and algorithm analysis in C++
Chapter. 3
3.2.3, 3.2.4, 3.2.5
Summary
Page 32 of 519
Size = 5
lastCurrentNode
Suppose that the currentNode is pointing at the location that contains the value 6. A
request for the removal of the node is made. Resultantly, the node pointed by
currentNode should be removed. For this purpose, at first, the next pointer of the node
with value 2 (the node pointed by the lastCurrentNode pointer), that is before the
node with value 6, bypasses the node with value 6. It is, now pointing to the node
with value 8. The code of the first step is as:
lastCurrentNode->setNext(currentNode->getNext());
What does the statement currentNode->getNext() do? The currentNode is pointing to
the node with value 6 while the next of this node is pointing to the node with value 8.
That is the next pointer of node with value 6 contains the address of the node with
value 8. The statement lastCurrentNode->setNext(currentNode->getNext()) will set
the next pointer of the node pointed by the lastCurrentNode to the node with value 8.
So the next pointer of the node with value 2 is pointing to the node with value 8.
currentNode
headNode
Step1
2
Size = 5
lastCurrentNode
You see that the next pointer of the node having data element 2 contains the address
of the node having data element 8. The node with value 6 has been disconnected from
the chain while the node with value 2 is connected to the node with the value 8.
The code of the next step is:
delete currentNode;
You already know, in case of allocation of the memory with the help of the new
Page 33 of 519
Step1
2
Size = 5
Step2
lastCurrentNode
In the next step, we have moved the currentNode to point the next node. The code is:
currentNode = lastCurrentNode;
In the fourth step, the size of the list has been reduced by 1 after the deletion of one
node i.e.
size--;
Step3
currentNode
headNode
Step1
Step4
Size = 4
Step2
lastCurrentNode
The next method is length() that simply returns the size of the list. The code is as
follows:
// returns the size of the list
int length()
{
return size;
};
The private data members of the list are:
private:
int size;
// contains the size of the list
Node *headNode;
// points to the first node of the list
Page 34 of 519
Page 35 of 519
add
For the addition purposes, we simply insert the new node after the current node.
So add is a one-step operation. We insert a new node after the current node in
the chain. For this, we have to change two or three pointers while changing the
values of some pointer variables. However, there is no need of traversing too
much in the list. In case of an array, if we have to add an element in the centre of
the array, the space for it is created at first. For this, all the elements that are after
the current pointer in the array, should be shifted one place to the right. Suppose if
we have to insert the element in the start of the array, all the elements to the right
one spot are shifted. However, for the link list, it is not something relevant. In link
lists, we can create a new node very easily where the current pointer is pointing.
We have to adjust two or three pointers. Its cost, in terms of CPU time or
computing time, is not much as compared to the one with the use of arrays.
remove
Remove is also a one-step operation. The node before and after the node to be
Page 36 of 519
find
The worst-case in find is that we may have to search the entire list. In find, we
have to search some particular element say x. If found, the currentNode pointer is
moved at that node. As there is no order in the list, we have to start search from
the beginning of the list. We have to check the value of each node and compare it
with x (value to be searched). If found, it returns true and points the currentNode
pointer at that node otherwise return false. Suppose that x is not in the list, in this
case, we have to search the list from start to end and return false. This is the worst
case scenario. Though time gets wasted, yet we find the answer that x is not in the
list. If we compare this with array, it will be the same. We dont know whether x
is in the array or not. So we have to search the complete array. In case of finding
it, we will remember that position and will return true. What is the average case? x
can be found at the first position , in the middle or at the end of the list. So on
average, we have to search half of the list.
back
In the back method, we move the current pointer one position back. Moving the
current pointer back, one requires traversing the list from the start until the node
whose next pointer points to current node. Our link list is singly linked list i.e. we
can move in one direction from start towards end. Suppose our currentNode
pointer and lastCurrentNode are somewhere in the middle of the list. Now we
want to move one node back. If we have the pointer of lastCurrentNode, it will be
easy. We will assign the value of lastCurrentNode to currentNode. But how can
we move the lastCurrentNode one step back. We dont have the pointer of
previous node. So the solution for this is to go at the start of the list and traverse
the list till the time you reach the node before the lastCurrentNode is pointing.
That will be the node whose next pointer contains the value lastCurrentNode. If
the currentNode and the lastCurrentNode are at the end of the list, we have to
traverse the whole list. Therefore back operation is not a one step operation. We
not only need a loop here but also require time.
Doubly-linked List
If you look at single link list, the chain is seen formed in a way that every node has a
field next that point to the next node. This continues till the last node where we set the
next to NULL i.e. the end of the list. There is a headNode pointer that points to the
start of the list. We have seen that moving forward is easy in single link list but going
back is difficult. For moving backward, we have to go at the start of the list and begin
from there. Do you need a list in which one has to move back or forward or at the
start or in the end very often? If so, we have to use double link list.
In doubly-link list, a programmer uses two pointers in the node, i.e. one to point to
next node and the other to point to the previous node. Now our node factory will
Page 37 of 519
prev
element
next
First part is prev i.e. the pointer pointing to the previous node, second part is element,
containing the data to be inserted in the list. The third part is next pointer that points
to the next node of the list. The objective of prev is to store the address of the
previous node.
Lets discuss the code of the node of the doubly-link list. This node factory will create
nodes, each having two pointers. The interface methods are same as used in singly
link list. The additional methods are getPrev and setPrev. The method getPrev returns
the address of the previous node. Thus its return type is Node*. The setPrev method
sets the prev pointer. If we have to assign some address to prev pointer, we will call
this method. Following is the code of the doubly-linked list node.
/* this is the doubly-linked list class, it uses the next and prev pointers */
class Node {
public:
int get() { return object; }; // returns the value of the element
void set(int object) { this->object = object; }; // set the value of the element
Node* getNext() { return nextNode; }; // get the address of the next node
void setNext(Node* nextNode)
// set the address of the next node
{ this->nextNode = nextNode; };
Node* getPrev() { return prevNode; }; // get the address of the prev node
void setPrev(Node* prevNode)
// set the address of the prev node
{ this->prevNode = prevNode; };
private:
int object;
// it stores the actual value of the element
Node* nextNode; // this points to the next node
Node* prevNode; // this points to the previous node
};
Most of the methods are same as those in singly linked list. A new pointer prevNode
is added and the methods to get and set its value i.e. getPrev and setPrev. Now we
will use this node factory to create nodes.
You have to be very cautious while adding or removing a node in a doubly linked list.
The order in which pointers are reorganized is important. Lets have a pictorial view
of doubly-link list. The diagram can help us understand where the prevNode and
nextNode are pointing.
Page 38 of 519
size=5
current
This is a doubly link list. The arrows pointing towards right side are representing
nextNode while those pointing towards left side are representing prevNode. Suppose
we are at the last node i.e. the node with value 1. In case of going back, we usually
take the help of prevNode pointer. So we can go to the previous node i.e. the node
with value 7 and then to the node with value 8 and so on. In this way, we can traverse
the list from the end to start. We can move forward or backward in doubly-link list
very easily. We have developed this facility for the users to move in the list easily.
Lets discuss other methods of the doubly-linked list. Suppose we have created a new
node from the factory with value 9. We will request the node factory to create a new
object using new keyword. The newly created node contains three fields i.e. object,
prevNode and nextNode. We will store 9 into object and connect this new node in the
chain. Lets see how the pointers are manipulated to do that. Consider the above
diagram, the current is pointing at the node with value 6. The new node will be
inserted between the node with value 6 and the one with value 8.
In the first step, we assign the address of the node with value 8 to the nextNode of the
new node.
newNode->setNext( current->getNext() );
current
head
newNode
size=5
In the next step, a programmer points the prevNode of the newNode to the node with
value 6.
newNode->setprev( current );
Page 39 of 519
head
size=5
2
newNode
In the third step, we will set the previous node with value 8 to point to the newNode.
(current->getNext())->setPrev(newNode);
current
head
size=5
newNode
Now the prevNode of the node with value 8 is pointing to the node with value 9.
In the fourth step, the nextNode of the node with value 6 is pointing to the newNode
i.e. the node with value 9. Point the current to the newNode and add one to the size of
the list.
current->setNext( newNode );
current = newNode;
size++;
head
6
4
2
newNode
size=6
3
1
current
Now the newNode has been inserted between node with value 6 and node with value
8.
Page 40 of 519
Circularly-linked lists
Lets talk about circularly linked list. The next field in the last node in a singly-linked
list is set to NULL. The same is the case in the doubly-linked list. Moving along a
singly-linked list has to be done in a watchful manner. Doubly-linked lists have two
NULL pointers i.e. prev in the first node and next in the last node. A way around this
potential hazard is to link the last node with the first node in the list to create a
circularly-linked list.
The next method in the singly-linked list or doubly-linked list moves the current
pointer to the next node and every time it checks whether the next pointer is NULL or
not. Similarly the back method in the double-linked list has to be employed carefully
if the current is pointing the first node. In this case, the prev pointer is pointing to
NULL. If we do not take care of this, the current will be pointing to NULL. So if we
try to access the NULL pointer, it will result in an error. To avoid this, we can make a
circularly linked list.
We have a list with five elements. We have connected the last node with the first
node. It means that the next of the last node is pointing towards the first node.
current
head
size=5
size=
2
7
1
You have noticed that there is no such node whose next field is NULL. What is the
benefit of this? If you use the next or back methods that move the current pointer, it
will never point to NULL. It may be the case that you keep on circulating in the list.
To avoid this, we get help from the head node. If we move the head node in the
circularly linked list, it will not be certain to say where it was pointing in the start. Its
advantages depend on its use. If we do not have to move too much in the list and have
Page 41 of 519
Josephus Problem
Now we will see an example where circular link list is very useful. This is Josephus
Problem. Consider there are 10 persons. They would like to choose a leader. The way
they decide is that all 10 sit in a circle. They start a count with person 1 and go in
clockwise direction and skip 3. Person 4 reached is eliminated. The count starts with
the fifth and the next person to go is the fourth in count. Eventually, a single person
remains.
You might ask why someone has to choose a leader in this way. There are some
historical stories attached to it. This problem is also studied in mathematics. Lets see
its pictorial view.
N=10, M=3
4
10
9
We have ten numbers representing the ten persons who are in a circle. The value of M
shows the count. As the value of M is three, the count will be three. N represents the
number of persons. Now we start counting clockwise. After counting up to three, we
have the number four. The number four is eliminated and put in the eliminated
column.
Page 42 of 519
N=10, M=3
10
9
After eliminating the number four, we will start our counting from number five.
Counting up to three, we have number eight which is eliminated and so on.
Eliminated
N=10, M=3
4
3
5
8
6
10
9
N=10, M=3
5
8
2
7
3
10
9
1
6
Page 43 of 519
}
We have included the CList.cpp. It means that we are using the circularly-linked
list. In the main method, CList factory is called to create a circular link list as CList
list; After this, we assign the values to N and M. We have used for loop to add the
nodes in the list. When this loop finishes, we have ten nodes in the list having values
from 1 to 10. But here a programmer may not pay attention to the internal details of
the list. We have created a list and stored ten numbers in it. Then we moved the
pointers of the list at the start of the list using the start method. It means that the
pointers are pointing at the position from where we want to start the counting of the
list.
There is a while loop that will continue executing until only one node is left in the list.
Inside this loop, we have a for loop. It will execute from 1 to M. It has only one
statement i.e. list.next(). This will move the pointer forward three times (as the value
of M is 3). Now the current pointer is at the 4th node. We called the remove method.
Before removing the node, we display its value on the screen using cout. Again we
come into the while loop, now the length of the list is 9. The for loop will be
executed. Now the list.next() is not starting from the start. It will start from the
position where the current pointer is pointing. The current pointer is pointing at the
next node to the node deleted. The count will start again. The list.next() will be called
for three times. The current pointer will point at the 8th node. Again the remove
method will be called and the current pointer moved to the next node and so on. The
nodes will be deleted one by one until the length of the list is greater than one. When
the length of the list is one, the while loop will be terminated. Now only one node is
left in the list i.e. the leader. We will display its value using the get method.
We can change the values of M and N. Similarly, these values can be read from the
file or can use the command line arguments to get values. There are many variations
of this problem. One variation is that the value of M keeps on changing. Sometimes, it
is 3, sometimes 4 or 5 and so on. Due to this, it will become difficult to think that who
will become leader. Make a picture in your mind that ten persons are sitting in a
circle. Every time the value of M is incremented by one. Now try to ascertain which
position you should sit to get chosen as a leader. You may like to write a program to
solve this or use the mathematical formula.
Page 45 of 519
Data Structures
Lecture No. 05
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 3
3.1, 3.2.5, 3.3.1, 3.3.2
(array implementation)
Summary
5)
6)
7)
8)
In the previous lecture, we demonstrated the use of the circular list for the resolution
of the Josephus problem. After writing a program with the help of this data structure,
a leader among ten persons was selected. You must have noted many things while
trying to solve the problem. These things will help us to understand the usage of data
structures in C++, thus making the programming easy. The code of the program is
given below.
#include "CList.cpp"
void main(int argc, char *argv[])
{
CList list;
int i, N=10, M=3;
for(i=1; i <= N; i++ ) list.add(i);
list.start();
while( list.length() > 1 ) {
for(i=1; i <= M; i++ ) list.next();
cout << "remove: " << list.get() << endl;
list.remove();
}
cout << "leader is: " << list.get() << endl;
}
In the program, we include the file of the class CList and create its object i.e. list.
Then we solve the problem by using the add, start, length, next, remove and get
methods of the class CList.
In the program, we have included already-defined data structure CList. After defining
its different methods, we have an interface of Clist. There is no need to be worry
Page 46 of 519
Stacks
Lets talk about another important data structure. You must have a fair idea of stacks.
Some examples of stacks in real life are stack of books, stack of plates etc. We can
add new items at the top of the stack or remove them from the top. We can only
access the elements of the stack at the top. Following is the definition of stacks.
Stack is a collection of elements arranged in a linear order
Lets see an example to understand this. Suppose we have some video cassettes. We
took one cassette and put it on the table. We get another cassette and put it on the top
of first cassette. Now there are two cassettes on the table- one at the top of other. Now
we take the third cassette and stack it on the two. Take the fourth cassette and stack it
on the three cassettes.
Now if we want to take the cassette, we can get the fourth cassette which is at the top
and remove it from the stack. Now we can remove the third cassette from the stack
and so on. Suppose that we have fifty cassettes stacked on each other and want to
Page 49 of 519
Description
Insert x as the top element of the stack
Remove the top element of the stack and return it.
Return the top element without removing it from the stack.
The push(x) method will take an element and insert it at the top of the stack. This
element will become top element. The pop() method will remove the top element of
the stock and return it to the calling program. The top() method returns the top-most
stack element but does not remove it from the stack. The interface method names that
we choose has special objective. In case of list, we have used add, remove, get, set as
the suitable names. However, for stack, we are using push, pop and top. We can
depict the activity from the method name like push means that we are placing an
element on the top of the stack and pushing the other elements down.
The example of a hotels kitchen may help understand the concept of stacks in a
comprehensive manner. In the kitchen, the plates are stacked in a cylinder having a
spring on the bottom. When a waiter picks a plate, the spring moves up the other
plates. This is a stack of plates. You will feel that you are pushing the plates in the
cylinder and when you take a plate from the cylinder it pops the other plates. The top
method is used to get the top- most element without removing it.
When you create classes, interfaces and methods, choose such names which depicts
what these method are doing. These names should be suitable for that class or factory.
Lets discuss the working of stack with the help of a diagram.
Page 50 of 519
top
top
5
7
5
7
5
push(2)
push(5)
push(7)
push(1)
top
top
top
top
21
7
5
7
5
pop()
top
push(21)
7
5
top
2
21
2
pop()
top
pop()
2
5
pop()
At the start, the stack is empty. First of all, we push the value 2 in the stack. As a
result, the number 2 is placed in the stack. We have a top pointer that points at the top
element. Then we said push(5). Now see how 2 and 5 are stacked. The number 5 is
placed at the top of number 2 and the pointer top moves one step upward. Then we
pushed the number 7 which is placed on the top and the number 2 and 5 are below.
Similarly, we push number 1. The last figure in the first row shows the stacked values
of the numbers- 1, 7, 5 and 2.
Lets pop the elements from the stack. The first figure of second row shows the pop
operation. As a result, the number 1 is popped. Than again we push the number 21 on
the stack. The number 7, 5, and 2 are already in the stack and number 21 is pushed at
the top. If we pop now, the number 21 is popped. Now number 7 is at the top. If we
pop again, the number 7 is popped. Pop again and the number 5 is popped and number
2 remains in the stack. Here with the help of this diagram, we are proving that the
values are added at the top and removed at the top in a stack.
The last element to go into the stack is the first to come out. That is why, a stack is
known as LIFO (Last In First Out) structure. We know that the last element pushed in
the stack is at the top which is removed when we call pop. Lets see some other
scenarios. What happens if we call pop() while there is no element? One possible
way-out is that we have isEmpty() function that returns true if stack is empty and false
otherwise. This is a boolean function that returns true if there is no element in the
stack. Otherwise, it will return false. The second option is this that when we call pop
on an empty stack, it throws an exception. This is a concept of advanced C++.
Exception is also a way to convey that some unusual condition has arisen or
something has gone wrong. Suppose, if we have a division method and try to divide
some number with zero. This method will throw division by zero exception.
Page 51 of 519
top
7
5
2
top = 3
In the above diagram, on the left side we have a stack. There are four elements in the
stack i.e. 1, 7, 5 and 2. The element 1 is the extreme-most that means that it is inserted
in the end whereas 7, 5, and 2 have been added before. As this is a LIFO structure so
the element 1 should be popped first. On the right side we have an array with
positions 0, 1, 2, 3 and so on. We have inserted the numbers 2, 5, 7 and 1. We have
decided that the elements should be inserted at the end of the array. Therefore the
most recent element i.e. 1 is at position 3. The top is the index representing the
position of the most recent element. Now we will discuss the stack implementation in
detail using array.
We have to choose a maximum size for the array. It is possible that the array may
fill-up if we push enough elements. Now more elements cannot be pushed. Now
what should the user of the stack do? Internally, we have implemented the stack
using array which can be full. To avoid this, we write isFull() method that will return
Page 52 of 519
Page 53 of 519
};
Page 54 of 519
Page 55 of 519
Data Structures
Lecture No. 06
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 3
3.3.2, 3.3.3
(Postfix expressions)
Summary
head
top
1
7
5
2
Fig 1. Stack using array (on left side) and linked list (on right side)
There are two parts of above figure.On the left hand, there is the stack implemented
using an array. The elements present inside this stack are 1, 7, 5 and 2. The most
Page 57 of 519
head
top
5
2
Fig 2. A node removed from the stack after the pop() call
Page 58 of 519
head
top
9
7
5
2
newNode
Page 59 of 519
Use of Stack
Examples of uses of stack include- traversing and evaluating prefix, infix and postfix
expressions.
Consider the expression A+B: we think of applying the operator + to the operands
A and B. We have been writing this kind of expressions right from our primary
classes. There are few important things to consider here:
Firstly, + operator requires two operators or in other words + is a binary operator.
Secondly, in the expression A+B, the one operand A is on left of the operator while
the other operand B is on the right side. This kind of expressions where the operator is
present between two operands called infix expressions. We take the meanings of this
expression as to add both operands A and B.
There are two other ways of writing expressions:
We could write +AB, the operator is written before the operands A and B. These
kinds of expressions are called Prefix Expressions.
We can also write it as AB+, the operator is written after the operands A and B.
This expression is called Postfix expression.
The prefixes pre and post refer to the position of the operator with respect to the two
operands.
Consider another expression in infix form: A + B * C. It consists of three operands A,
B, C and two operator +,* . We know that multiplication () is done before addition
(+), therefore, this expression is actually interpreted as: A + (B * C). The
interpretation is because of the precedence of multiplication (*) over addition (+). The
precedence can be changed in an expression by using the parenthesis. We will discuss
it a bit later.
Lets see, how can we convert the infix expression A + (B * C) into the postfix form.
Firstly, we will convert the multiplication to postfix form as: A + (B C *). Secondly,
we will convert addition to postfix as: A (B C *) + and finally it will lead to the
resultant postfix expression i.e. : A B C * +. Lets convert the expression (A + B) * C
to postfix. You might have noticed that to overcome the precedence of multiplication
operator (*) we have used parenthesis around A + B because we want to perform
addition operation first before multiplication.
(A + B) * C
infix form
(A B +) * C
convert addition
(A B +) C *
convert multiplication
AB+C*
postfix form
Page 61 of 519
Precedence of Operators
There are five binary operators, called addition, subtraction, multiplication, division
and exponentiation. We are aware of some other binary operators. For example, all
relational operators are binary ones. There are some unary operators as well. These
require only one operand e.g. and +. There are rules or order of execution of
operators in Mathematics called precedence. Firstly, the exponentiation operation is
executed, followed by multiplication/division and at the end addition/subtraction is
done. The order of precedence is (highest to lowest):
Exponentiation
Multiplication/division *, /
Addition/subtraction
+, For operators of same precedence, the left-to-right rule applies:
A+B+C means (A+B)+C.
For exponentiation, the right-to-left rule applies:
A B C means A (B C)
We want to understand these precedence of operators and infix and postfix forms of
expressions. A programmer can solve a problem where the program will be aware of
the precedence rules and convert the expression from infix to postfix based on the
precedence rules.
Postfix
AB+
12 60 + 23
AB+CD*
A B C*D E F/+
In the next lecture we will see, how to convert infix to postfix and how to evaluate
postfix form besides the ways to use stack for these operations.
Page 62 of 519
Data Structures
Lecture No. 07
Reading Material
Page 63 of 519
Chapter. 3
3.3.3
Summary
9)
10)
11)
Postfix
AB+
12 60 + 23
AB+CD*
A B C*D E F/+
The last expression seems a bit confusing but may prove simple by following the
rules in letter and spirit. In the postfix form, parentheses are not used. Consider the
infix expressions as 4+3*5 and (4+3)*5. The parentheses are not needed in the
first but are necessary in the second expression. The postfix forms are:
4+3*5
(4+3)*5
435*+
43+5*
In case of not using the parenthesis in the infix form, you have to see the precedence
rule before evaluating the expression. In the above example, if we want to add first
then we have to use the parenthesis. In the postfix form, we do not need to use
parenthesis. The position of operators and operands in the expression makes it clear in
which order we have to do the multiplication and addition.
Now we will see how the infix expression can be evaluated. Suppose we have a
postfix expression. How can we evaluate it? Each operator in a postfix expression
refers to the previous two operands. As the operators are binary (we are not talking
about unary operators here), so two operands are needed for each operator. The nature
of these operators is not affected in the postfix form i.e. the plus operator (+) will
apply on two operands. Each time we read an operand, we will push it on the stack.
We are going to evaluate the postfix expression with the help of stack. After reaching
an operator, we pop the two operands from the top of the stack, apply the operator and
push the result back on the stack. Now we will see an example to comprehend the
working of stack for the evaluation of the postfix form. Here is the algorithm in
pseudo code form. After reading this code, you will understand the algorithm.
Page 64 of 519
An Example
In the earlier example, we have used the stack to solve the postfix expression. Lets
see another comprehensive example. The postfix expression is:
623+-382/+*23+
Page 65 of 519
op1
op2
value
stack
6
2
6
3
2
6
Page 66 of 519
6
6
5
5
1
1
*
2
1
1
7
7
7
7
7
7
2
2
49
49
49
52
5
6
1
3
1
8
3
1
2
8
3
1
4
3
1
7
1
7
2
7
49
3
49
52
With the help of stack we can easily solve a very big postfix expression. Suppose you
want to make a calculator that is a part of some application e.g. some spreadsheet
program. This calculator will be used to evaluate expressions. You may want to
calculate the value of a cell after evaluating different cells. Evaluation of the infix
form programmatically is difficult but it can be done. We will see another data
structure which being used to solve the expressions in infix form. Currently, we have
to evaluate the values in different cells and put this value in another cell. How can we
do that? We will make the postfix form of the expression associated with that cell.
Then we can apply the above algorithm to solve the postfix expression and the final
result will be placed at that cell. This is one of the usages of the stack.
Page 68 of 519
Page 69 of 519
postfix
A
A
AB
AB
ABC
ABC*
ABC*+
stack
+
+
*
+
*
+
+
If we have to convert the infix expression into the postfix form, the job is easily done
with the help of stack. The above algorithm can easily be written in C++ or C
language, specially, if you already have the stack class. Now you can convert very big
infix expressions into postfix expressions. Why we have done this? This can be
understood with the help of the example of spreadsheet programming where the value
of cell is the evaluation of some expression. The user of the spreadsheets will use the
infix expressions as they are used to it.
Sometimes we do need the parenthesis in the infix form. We have to evaluate the
lower precedence operator before the higher precedence operator. If we have the
expression (A+B) *C, this means that we have to evaluate + before the multiplication.
The objective of using parenthesis is to establish precedence. It forces to evaluate the
expression first of all. We also have to handle parenthesis while converting the infix
expression into postfix one. When an open parenthesis ( is read, it must be pushed
on the stack. This can be done by setting prcd(op,( ) to be FALSE. What is the
reason to put the parenthesis on the stack? It is due to the fact that as long as the
closing parenthesis is not found, the open parenthesis has to wait. It is not a unary or
binary operator. Actually, it is a way to show or write precedence. We can handle the
parenthesis by adding some extra functionality in our prcd function. When we call
prcd(op, (), it will return false for all the operators and be pushed on the stack. Also,
prcd( (,op ) is FALSE which ensures that an operator after ( is pushed on the stack.
When a ) is read. All operators up to the first ( must be popped and placed in the
postfix string. To achieve this our function prcd( op,) ) should return true for all the
operators. Both the ( and the) will not go to the postfix expression. In postfix
expression, we do not need parenthesis. The precedence of the operators is established
in such a way that there is no need of the parenthesis. To include the handling of
parenthesis, we have to change our algorithm. We have to change the line s.push(c)
to:
if( s.empty() || symb != ) )
s.push( c );
else
s.pop(); // discard the (
If the input symbol is not ) and the stack is not empty, we will push the operator on
the stack. Otherwise, it is advisable to pop the stack and discard the (. The following
functionality has to be added in the prcd function.
Page 70 of 519
=
=
=
=
FALSE
FALSE
TRUE
error
In the next lecture we will see in detail an example regarding the use of parenthesis.
Page 71 of 519
Data Structures
Lecture No. 08
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 3
3.3.3
Summary
Symbol
Postfix
Stack
Page 72 of 519
(
A
+
B
)
*
C
A
A
AB
AB+
AB+
AB+C
AB+C*
(
(
(+
(+
*
*
First of all, there is the input symbol ((i.e. opening parenthesis). As this is not an
operand, it may be put on the stack. The next input symbol is A. Being an operand it
goes to the postfix string and the stack remains unchanged. Then there is + operator of
binary type. Moreover, there is one operand in the postfix string. We push this +
operator on the stack and it has to wait for its second operand. Now in the input
symbol, there is an operand B. We put his operand in the postfix string. Then after
this, there is the closing parenthesis ) in the input symbol. We know that the
presence of a closing parenthesis in the input means that an expression (within the
parentheses) has been completed. All of its operands and operators are present with in
the parentheses. As studied in the algorithm, we discard a closing parenthesis when it
comes in the input. Then the operators from the stack are popped up and put in the
postfix string. We also pop the opening parenthesis and discard it as we have no need
of opening as well as closing parenthesis in the postfix notation of an expression. This
process is carried out in the 5th row of the table. The + operator is put in the postfix
string. We also discard the opening parenthesis, as it is not needed in the postfix.
Now the next input symbol is *. We put this operator on the stack. There is one
operand for the * operator i.e. AB+. The * operator being a binary operator, has to
wait for the second operand. C is the Next input symbol that is an operand. We put it
in the postfix string. After this, the input string (expression) ends so we come out of
the loop. We check if there is any thing on the stack now? There is * operator in the
stack. We pop the operator and put it into the postfix string. This way, we get the
postfix form of the given infix expression that becomes AB+C*. In this postfix
expression, the + operator is before the * operator. So addition operation is done
before the multiplication. This is mainly due to the fact that in the infix expression,
we have put parentheses to give + operator the precedence higher than the * operator.
Note that there are no parentheses in the postfix form of the given infix expression.
Now we apply the evaluation algorithm on this postfix expression (i.e. AB+C*). The
two operands A and B, will go to the stack. Then operator + will pop these operands
from the stack, will add them and push the result back on the stack. This result
becomes an operand. Next C will go to the stack and after this * operator will pop
these two operands (result of addition and C). Their multiplication will lead to the
final result. The postfix notation is simple to evaluate as compared to the infix one. In
postfix, we need not to worry about what operation will be carried first. The operators
in this notation are in the order of evaluation. However, in the infix notation, we have
to force the precedence according to our requirement by putting parentheses in the
expression. With the help of a stack data structure, we can do the conversion and
evaluation of expressions easily.
Page 73 of 519
C++ Templates
We can use C++ templates for stack and other data structures. We have seen that
stack is used to store the operands while evaluating an expression. These operands
may be integers, floating points and even variables. We push and pop the operands to
and from the stack. In the conversion of an expression, a programmer uses the stack
for storing the operators like +, *, -, and / etc which are single characters. In both
cases, the functionality is the same. We push and pop things on and from the stack. At
times, we check if the stack is empty or not. Thus identical methods are employed
while using stack in evaluating and converting the expressions. However, there may
be a difference in the type of the elements (data) used in these cases. We may define a
stack, which can store different types of data but for the time being we are restricting
ourselves to the stack that can store elements of only one type. In C++ programming,
we will have to create two classes FloatStack and CharStack for operands and
operators respectively. These classes of stack have the same implementation. Thus,
we write the same code twice only with the difference of data type. Is there any
method to write the code for the stack once and then use it for different types of data?
This means is there any way that we can make a stack for storing integers, floating
points, characters or even objects with the same code written once. The language C++
provides us the facility of writing templates. A template can be understood with the
example of a factory that bakes biscuits. The factory may use flour, corn or starch as
ingredients of the product. But the process of baking biscuits is the same whatever
ingredients it uses. There is no difference in the machinery for producing biscuits with
different ingredients. So we call the factory as the template for the biscuits. Similarly
in C++ language, a template is a function or class that is written with a generic data
type. When a programmer uses this function or class, the generic data type is replaced
with the data type, needed to be used in the template function or in the template class.
We only give the data type of our choice while calling a template function or creating
an object of the template class. The compiler automatically creates a version of that
function or class with that specified data type. Thus if we write a template class for
stack, then later on we can use it for creating a stack for integers, floating points or
characters etc. So instead of writing code for different stacks of different data types,
we write one code as a template and reuse it for creating different stacks. We declare
the template class in a separate file in addition to the main program file. This file can
be used in our program by including it in that file. Following is the code of the
template class for stack. This is written in the file Stack.h.
template <class T>
class Stack
{
public:
Stack();
int empty(void);
// 1=true, 0=false
int push(T &);
// 1=successful,0=stack overflow
T pop(void);
T peek(void);
~Stack();
private:
int top;
T* nodes;
};
Page 74 of 519
Implementation
Now we will see how to implement this stack class in our program. Following is the
code of the program. We save this code in the file named Stack.cpp.
#include <iostream.h>
#include <stdlib.h>
#include "Stack.h"
#define MAXSTACKSIZE 50
template <class T>
Stack<T>::Stack()
{
top = -1;
nodes = new T[MAXSTACKSIZE];
}
template <class T>
Stack<T>::~Stack()
{
delete nodes;
}
Page 75 of 519
Page 76 of 519
Page 77 of 519
Page 78 of 519
second argument
first argument
top ------> return address
In the calling function, after the execution of the function called, the program
continues its execution form the next line after the function call. The control comes
back here because when the execution of the function ends the compiler pops the
address from the stack which it has pushed when the function call was made. Thus the
control goes at that point in the program and the execution continues in the calling
function or program.
Consider the following code of a function that takes two integer arguments a, b and
returns the average of these numbers.
int i_avg (int a, int b)
{
return (a + b) / 2;
}
Page 79 of 519
The first statement is globl_i_avg which shows that its a global function that can be
called by other functions or programs. After it, there is a label, written as _i_avg:
The next statement is movl 4(%esp), %eax. Here in this statement, there is the use of
stack. Here esp is a register in assembly language that is now a stack pointer for us
(i.e. top). The movl (move long) takes offset 4 from top (4 is number of bytes, we use
4 bytes as in C++ an integer is of 4 bytes.) that means after 4 bytes from the top in the
stack it gets the value and put it in the eax register. We know that the compiler pushes
the arguments of the function in reverse order on the stack. And pushes return address
at the end. Thus the order of stack will be that on the top will be the return address
and immediately after it will be the first argument. Here in the assembly code
generated by the compiler, the compiler pops first argument from offset 4 and puts it
in eax register. The next statement is
addl 8(%esp), %eax
The addl takes offset 8 from the stack pointer that is second argument and adds it to
eax. Thus in the previous two statements, the compiler got the first argument i.e. a
from the stack and the second argument b from the stack, added them before putting
the result in eax. The next statement
sarl $1, %eax
is the division statement of assembly language. This statement divides the value in
eax by 2 and thus eax has the resultant value. The last statement i.e. ret, returns the
value on the top of the stack to the caller function.
So we have seen the use of stack in the execution of a function and how the
arguments are passed to the function, how the functions return its return value and
finally how the control goes back to the caller function .All this process is executed
by using a stack. All the things about the functionality of the function calls are
necessary to understand as these will be needed while going to write our own
compilers. We will read this in the compilers course. The whole process we have
discussed about the use of stack in function calling is known as run time environment.
Different data structures are also used in run time environment of the computer. We
know that an executable program while in run, is loaded in the memory and becomes
a process. This process is given a block of memory which it uses during its execution.
Even the operating system, in which we are working, itself takes memory. Suppose
we are running many programs simultaneously, which for example include browser,
MS Word, Excel and dev-C++. We can also run programs written by us. Every
Page 80 of 519
We can also see the details of all the programs running at a specific time. If we press
the key combination Ctrl-Alt-Del, there appears a window task manager on the
screen. Following is the figure of the task manager. In the figure, there are many
columns i.e. PID (process ID), CPU, CPU time, Memory usage, page faults, I/O
Reads, I/O Writes, I/O Read Bytes. These all things we will read in the course of
Operating Systems in detail.
Page 81 of 519
Here the thing of our interest is the first, second and fifth column. These columns are
Image Name, PID and Mem Usage (i.e. memory usage). Now look at the row where
explorer.exe is written in the first column. The process ID (PID) of it is 888 and
memory usage is 4696K. This means that the process size of explorer.exe in the
memory is 4696 Kilo Bytes (KB). All the processes in the first column of the task
manager are present in the memory of the computer at that time. The column Image
name has the names of the processes being executed. These have extension .exe but
there may be other executable programs that have extension other than .exe.
Page 82 of 519
Process 1
(browser)
Code
Process 3
(Word)
Static data
Process 4
(Excel)
Stack
Process 2
(Dev-C++)
Windows Os
Heap
This shows that the first part of the memory of the process is for the code. This is the
code generated by the compiler of C++, JAVA or VB etc with respect to the language
in which the actual code was written. Then the static data of the program occupies the
memory. This holds the global variables and different variables of objects. Then in the
memory of the process, there is stack. After it there is heap. The stack and heap are
used in function calls. We have discussed the use of stack in function calls. When we
allocate memory dynamically in our programs, it is allocated from the heap. The use
of heap is a topic related to some programming course or to the operating system
course.
Page 83 of 519
Data Structures
Lecture No. 09
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 3
3.3.3, 3.4.1, 3.4.2
Summary
Memory Organization
Stack Layout During a Function Call
Queues
Queue Operations
Implementing Queue
Queue using Array
Use of Queues
Memory Organization
By the end of last lecture, we discussed the uses of stack to develop a process from an
executable file and then in function calls. When you run an executable, the operating
system makes a process inside memory and constructs the followings for that purpose.
- A code section that contains the binary version of the actual code of the program
written in some language like C/C++
- A section for static data including global variables
- A stack and
- Finally, a heap
Page 84 of 519
Code
Process 3
(Word)
Static Data
Process 4
(Excel)
Stack
Process 2
(Dev-C++)
Heap
Windows OS
Parameters (F)
Parameters (G)
sp
sp
Parameters (G)
Local variables (G)
Return address (G)
Parameters (F)
Local variables (F)
Return address (F)
sp
The above diagrams depict the layout of the stack when a function F calls a function
G. Here sp stands for stack pointer. At the very left, you will find the layout of the
stack just before function F calls function G. The parameters passed to function F are
Page 85 of 519
Queues
A queue is a linear data structure into which items can only be inserted at one end and
removed from the other. In contrast to the stack, which is a LIFO (Last In First Out)
structure, a queue is a FIFO (First In First Out) structure.
The usage of queue in daily life is pretty common. For example, we queue up while
depositing a utility bill or purchasing a ticket. The objective of that queue is to serve
persons in their arrival order; the first coming person is served first. The person, who
comes first, stands at the start followed by the person coming after him and so on. At
the serving side, the person who has joined the queue first is served first. If the
requirement is to serve the people in some sort of priority order, there is a separate
data structure that supports priorities. The normal queue data structure, presently
under discussion, only supports FIFO behavior.
Now, lets see what are the operations supported by the queue.
Queue Operations
The queue data structure supports the following operations:
Operation
enqueue(X)
dequeue()
Description
Place X at the rear of the queue.
Remove the front element and return it.
front()
Page 86 of 519
Page 87 of 519
Implementing Queue
There are certain points related to the implementation of the queue. Suppose we are
implementing queue with the help of the linked -list structure. Following are the key
points associated with the linked list implementations:
- Insert works in constant time for either end of a linked list.
- Remove works in constant time only.
- Seems best that head of the linked list be the front of the queue so that all removes
will be from the front.
- Inserts will be at the end of the list.
The above figure shows queue elements on the left with two pointers front and rear.
This is an abstract view of the queue, independent of its implementation method of
array or linked list. On the right side is the same queue ,using linked list and pointers
of front and rear. When dequeue() function is called once, the front element 1 is
removed. The picture of the queue showing one element removal is also depicted
below. Note that front pointer has been moved to the next element 7 in the list afer
removing the front element 1.
After dequeue() is called once
Now at this stage of the queue, we will call enqueue (9) to insert an element 9 in it. .
The following figure shows that the new element is inserted at the rear end and rear
pointer starts pointing this new node with element 9.
At this point of time, the code of these functions of dequeue() and enqueue() should
not be an issue.
Page 88 of 519
Note that in this queue data structure, the new elements are inserted at rear end and
removed from the front. This is in contrast to stack structure where the elements are
inserted and removed from the same end.
Lets see the code for queue operations:
/* Remove element from the front */
1. int dequeue()
2. {
3. int x = front->get();
4.
Node* p = front;
5.
front = front->getNext();
6.
delete p;
7.
return x;
8. }
/* Insert an element in the rear */
9. void enqueue(int x)
10. {
11.
Node* newNode = new Node();
12.
newNode->set(x);
13.
newNode->setNext(NULL);
14.
rear->setNext(newNode);
15.
rear = newNode;
16. }
In dequeue() operation, at line 3, the front element is retrieved from the queue and
assigned to the int variable x.
In line 4, the front pointer is saved in Node pointer variable p.
In line 5, the front pointer is moved forward by retrieving the address of the next node
by using front->getNext() and assigning it to the front pointer.
In line 6, the node pointed to by the front pointer is deleted by using delete front
statement.
At the end of dequeue() implementation, the value of deleted node that was saved in
the int variable x, is returned back.
The enqueue(int ) is used to add an element in the queue. It inserts the element in the
rear of the queue. At line 11, a new Node object is created using the new Node()
statement and the returned starting address of the created object is assigned to the
newNode pointer variable.
Page 89 of 519
Page 90 of 519
rea
front
rear
front
front
rear
As shown in the above diagram, an element i.e. 6 has been inserted in the queue.
Now, the rear index is containing 4 while the front has the same 0 index. Lets see
the figure of the array when another element 8 is inserted in the queue.
front
enqueue(8)
rea
1 7 5 2 6 8
front
rear
When an element is removed from the queue. It is removed from the front index.
dequeue( )
front
rea
7
7 5 2 6 8
0
5
2
front
2
3
6
4
8 Page 91 of 519
5
6
rear
5 2 6 8
0
front
rear
With the removal of element from the queue, we are not shifting the array elements.
The shifting of elements might be an expensive exercise to perform and the cost is
increased with the increase in number of elements in the array. Therefore, we will
leave them as it is.
enqueue(9)
enqueue(12)
front
rea
5
5 2 6 8 9 12
0
6
4
front
rear
12
7
After insertion of two elements in the queue, the array that was used to implement it,
has reached its limit as the last location of the array is in use now. We know that there
is some problem with the array after it attained the size limit. We observed the similar
problem while implementing a stack with the help of an array.
We can also see that two locations at the start of the array are vacant. Therefore, we
should can consider how to use those locations appropriately in to insert more
Page 92 of 519
front
rea
2
12
12
2
8
rear
7
The number of locations in the above circular array are also eight, starting from index
0 to index 7. The index numbers are written outside the circle incremented in the
clock-wise direction. To insert an element 21 in the array , we insert this element in
the location, which is next to index 7.
enqueue(21)
front
0
rea
5 2 6 8 9 12 21
6
Fig 13. An element added in circular array
1
front
21
12
2
8
5
6
4
size
rear
noElements
Now, we will have to maintain four variables. front has the same index 2 while the,
size is 8. rear has moved to index 0 and noElements is 7. Now, we can see that rear
index has decreased instread of increasing. It has moved from index 7 to 0. front is
containing index 2 i.e. higher than the index in rear. Let see, how do we implement
the enqueue() method.
void enqueue( int x)
{
1. rear = (rear + 1) % size;
2. array[rear] = x;
3. noElements = noElements + 1;
}
In line 1 of the code, 1 is added in rear and the mod operator (that results in
Page 93 of 519
front
0
7
5 2 6 8 9 12 21 7
6
1
front
21
12
2
8
5
Fig 14. Another element added in circular array
size
rear
noElements
Now, the queue, rather the array has become full. It is important to understand, that
queue does not have such characteristic to become full. Only its implementation array
has become full. To resolve this problem, we can use linked list to implement a
queue. For the moment, while working with array, we will write the method isFull(),
to determine the fullness of the array.
int isFull()
{
return noElements == size;
}
int isEmpty()
{
return noElements == 0;
}
isFull() returns true if the number of elements (noElements) in the array is equal to the
size of the array. Otherwise, it returns false. It is the responsibility of the caller of the
queue structure to call isFull() function to confirm that there is some space left in the
queue to enqueue() more elements.
Similarly isEmpty() looks at the number of elements (noElements) in the queue. If
there is no element, it returns true or vice versa..
Lets see the dequeue() method.
0
dequeue()
front
rea
6 8 9 12 21 7
6
1
21
front
12
9
8
5
6
4
size
8
Page 94 of 519
rear noElements
1
int dequeue()
{
int x = array[front];
front = (front + 1) % size;
noElements = noElements - 1;
return x;
}
In the first line, we take out an element from the array at front index position and
store it in a variable x. In the second line, front is incremented by 1 but as the array is
circular, the index is looped from 0 to 7. That is why the mod (%) is being used. In the
third line, number of elements (noElements) is reduced by 1 and finally the saved
array element is returned.
Use of Queues
We saw the uses of stack structure in infix, prefix and postfix expressions. Lets see
the usage of queue now.
Out of the numerous uses of the queues, one of the most useful is simulation. A
simulation program attempts to model a real-world phenomenon. Many popular video
games are simulations, e.g., SimCity, Flight Simulator etc. Each object and action in
the simulation has a counterpart in the real world. Computer simulation is very
powerful tool and it is used in different high tech industries, especially in engineering
projects. For example, it is used in aero plane manufacturing. Actually Computer
Simulation is full-fledged subject of Computer Science and contains very complex
Mathematics, sometimes. For example, simulation of computer networks, traffic
networks etc.
If the simulation is accurate, the result of the program should mirror the results of the
real-world event. Thus it is possible to understand what occurs in the real-world
without actually observing its occurrence.
Let us look at an example. Suppose there is a bank with four tellers.
A customer enters the bank at a specific time (t1) desiring to conduct a transaction.
Any one of the four tellers can attend to the customer. The transaction (withdraws,
deposit) will take a certain period of time (t2). If a teller is free, the teller can process
the customers transaction immediately and the customer leaves the bank at t1+t2. It is
possible that none of the four tellers is free in which case there is a line of customers
at each teller. An arriving customer proceeds to the back of the shortest line and waits
for his turn. The customer leaves the bank at t2 time units after reaching the front of
the line.
The time spent at the bank is t2 plus time waiting in line.
So what we want to simulate is the working environment of the bank that there are
Page 95 of 519
Page 96 of 519
Data Structures
Lecture No. 10
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 3, 6
3.4.3, 6.1
Summary
12)
13)
14)
15)
Queues
Simulation Models
Priority Queue
Code of the Bank simulation
Queues
In the previous lecture, we discussed the queue data structure and demonstrated its
implementation by using array and link list. We also witnessed the usefulness of
queue as data structure in the simulation example. This concept can be further
elaborated with a daily life example relating to banking sector. Suppose, customers
want to deposit or withdraw money from a bank, having four cashiers or tellers. The
teller helps you in depositing the money or withdrawing the money from the same
window. This window is known as teller window. The customer needs some service
from the bank like depositing the money, bill etc. This transaction needs some time
that may be few minutes. A person enters the bank and goes to the teller who is free
and requests him to do the job. After the completion of the transaction, the person
goes out of the bank. Now we will discuss a scenario when there is a lot of rush of
customers. The tellers are just four. Now the tellers are busy and the new customers
will form a queue. In this example, we need a queue in front of each of the tellers. A
new customer enters the bank and analyzes the four queues and wants to join the
shortest queue. This person has to wait for the persons in front of him to be served.
Another person may come behind him. In this simulation, we will restrict the person
from changing the queue. A person comes into the bank at 10 O clock. His transaction
time is 5 minutes. He has to wait for another fifteen minutes in the queue. After this,
the teller serves him in 5 min. This person comes at 10 am and waits for fifteen
minutes. As the transaction time is 5 minutes, so he will leave the bank at 1020. Now
this is the situation of simulation and we have to write a program for this. We can go
to some bank and analyze this situation and calculate the time. At the end of the day,
we can calculate the average time for each of the customer. This time can be 30
minutes. Here we will simulate this situation with the help of a computer program.
This is the real life example. Lets see the picture of simulations to understand what is
happening in the bank.
In the picture below, we have four tellers and four queues, one for each of the tellers.
Each teller is serving a customer. When the transaction of a customer is completed, he
Page 97 of 519
teller
1
teller
2
teller
3
teller
4
A person enters the bank. He sees that all the four tellers are busy and in each queue
there are two persons waiting for their turn. This person chooses the queue no. 3.
Another person enters the bank. He analyzed all the queues. The queue no 3 is the
biggest and all other are having 2 persons in the queue. He chooses the queue no 1.
teller
1
teller
2
teller
3
teller
4
Now we have three persons waiting in queue no 1 and 3 and two persons waiting in
queue no 2 and 4. The person in queue no.1 completes his transaction and leaves the
bank. So the person in the front of the queue no. 1 goes to the teller and starts his
transaction. Similarly the person at queue No. 3 finishes his transaction and leaves the
premises. The person in front of queue number 3 goes to the teller.
Another person enters the bank and goes to the queue No. 1. This activity goes on.
Page 98 of 519
Simulation Models
Lets discuss little bit about the simulation models. Two common models of
simulation are time-based simulation and event-based simulation. In time-based
simulation, we maintain a timeline or a clock. The clock ticks and things happen
when the time reaches the moment of an event.
Suppose we have a clock in the computer. The minute hand moves after every minute.
We know the time of the customers entry into the bank and are aware that his
transaction takes 5 minutes. The clock is ticking and after 5 minutes, we will ask the
customer to leave the bank. In the program, we will represent the person with some
object. As the clock continues ticking, we will treat all the customers in this way.
Note that when the customer goes to some teller, he will take 5 minutes for his
transaction. During this time, the clock keeps on ticking. The program will do nothing
during this time period. Although some other customer can enter the bank. In this
model, the clock will be ticking during the transaction time and no other activity will
take place during this time. If the program is in some loop, it will do nothing in that
loop until the completion of the transaction time.
Now consider the bank example. All tellers are free. Customer C1 comes in 2 minutes
after the opening of the bank. Suppose that bank opens at 9:00 am and the customer
arrives at 9:02 am. His transaction (withdraw money) will require 4 minutes.
Customer C2 arrives 4 minutes after the bank opens (9:04 am). He needs 6 minutes
for transaction. Customer C3 arrives 12 minutes after the bank opens and needs 10
minutes for his transaction.
We have a time line and marks for every min.
Time (minutes)
0
C1 in
10
11
12
13
14
15
C1 out
C2 in
C2 out
C3 in
C1 in
10
11
12
13
14
15
C1 out
C2 in
Event 1:
2 mins C1 in
Event 2: 4 mins C2 in
Event 3: 6 mins C1 out
Event 4: 10 mins C2 out
Event 5: 12 mins C3 in
C2 out
C3 in
The customer C1 comes at 2 min and leaves at 6 min. Customer C2 comes at 4 min
and leaves at 10 min and so on. We have written the events list in the above figure.
Do not see the clock but see the events on time. Event 1 occurs at 2 min that is the
customer C1 enters the bank 2 minutes after its opening. Event 2 is that C2 enters at 4
min. Event 3 is that the customer C1 leaves the bank at 6 min. Event 4 is that the C2
leaves the bank at 10 min and event 5 is that C3 enters the bank at 12 min. Here we
have a list of events. How can we process them? We will make a queue of these
events. Remove the event with the earliest time from the queue and process it. Insert
the newly created events in the queue. A queue where the de-queue operation depends
not on FIFO, is called a priority queue.
Priority Queue
As stated earlier, the queue is a FIFO (First in first out) structure. In daily life, you
have also seen that it is not true that a person, who comes first, leaves first from the
queue. Lets take the example of traffic. Traffic is stopped at the signal. The vehicles
are in a queue. When the signal turns green, vehicles starts moving. The vehicles
which are at the front of the queue will cross the crossing first. Suppose an ambulance
comes from behind. Here ambulance should be given priority. It will bypass the queue
and cross the intersection. Sometimes, we have queues that are not FIFO i.e. the
person who comes first may not leave first. We can develop such queues in which the
condition for leaving the queue is not to enter first. There may be some priority. Here
we will also see the events of future like the customer is coming at what time and
leaving at what time. We will arrange all these events and insert them in a priority
queue. We will develop the queue in such a way that we will get the event which is
going to happen first of all in the future. This data structure is known as priority
queue. In a sense, FIFO is a special case of priority queue in which priority is given to
the time of arrival. That means the person who comes first has the higher priority
while the one who comes later, has the low priority. You will see the priority queue
being used at many places especially in the operating systems. In operating systems,
we have queue of different processes. If some process comes with higher priority, it
will be processed first. Here we have seen a variation of queue. We will use the
priority queue in the simulation. The events will be inserted in the queue and the
event going to occur first in future, will be popped.
Data Structures
Lecture No. 11
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.1.1, 4.2.1
Summary
In the previous lecture, we demonstrated the technique of data structure priority queue
with the help of the example from the banking system. Data structure priority queue is
a variation of queue data structure to store the events. We have witnessed this
simulation by animation. The simulation and animation help a programmer to
understand the functionality of a program. As these are not the part of the current
course, so we will study simulation and animation in detail in the coming courses.
A queue is a FIFO structure (i.e. first in first out). But there are its variations
including the priority queue, discussed in the previous lecture. In priority queue, we
were adding data to a queue but did not get the data from the queue by First In, First
Out (FIFO) rule. We put events in the queue and got these from the queue with
respect to the time of occurrence of the event. Thus we get the items from a priority
queue in a particular order with respect to a priority given to the items (events). The
priority queue is also an important data structure. Lets see its implementation.
Tree
Now lets talk about a data structure called tree. This is an important data structure.
This data structure is used in many algorithms. We will use it in most of our
assignments. The data structures that we have discussed in previous lectures are linear
data structures. The linked list and stack are linear data structures. In these structures,
the elements are in a line. We put and get elements in and from a stack in linear order.
Queue is also a linear data structure as a line is developed in it. There are a number of
applications where linear data structures are not appropriate. In such cases, there is
need of some non-linear data structure. Some examples will show us that why nonlinear data structures are important. Tree is one of the non-linear data structures.
Look at the following figure. This figure (11.1) is showing a genealogy tree of a
family.
T
.
Sohail Aslam
Haaris
Saad
Javed Aslam
Qasim
Asim
Yasmeen Aslam
Fahd
Ahmed
Sara
Omer
In this genealogy tree, the node at the top of the tree is Muhammad Aslam Khan i.e.
the head of the family. There are three nodes under this one. These are Sohail Aslam,
Javed Aslam and Yasmeen Aslam. Then there are nodes under these three nodes i.e.
the sons of these three family members. You may have seen the tree like this of some
other family. You can make the tree of your family too. The thing to be noted in this
genealogical tree is that it is not a linear structure like linked list, in which we have to
tell that who is the father and who is the son. We make it like a tree. We develop the
tree top-down, in which the father of the family is on the top with their children
downward. We can see the similar tree structure of a company. In the tree of a
company, the CEO of the company is on the top, followed downwardly by the
managers and assistant managers. This process goes downward according to the
administrative hierarchy of the company. Our tree structure is different from the
actual tree in the way that the actual tree grows from down to up. It has root downside
and the leaves upside. The data structure tree is opposite to the real tree as it goes
upside down. This top-down structure in computer science makes it easy to
understand the tree data structure.
There may be situations where the data, in our programs or applications, is not in the
linear order. There is a relationship between the data that cannot be captured by a
linked list or other linear data structure. Here we need a data structure like tree.
In some applications, the searching in linear data structures is very tedious. Suppose
we want to search a name in a telephone directory having 100000 entries. If this
directory is in a linked list manner, we will have to traverse the list from the starting
position. We have to traverse on average half of the directory if we find a name. We
may not find the required name in the entire directory despite traversing the whole
list. Thus it would be better to use a data structure so that the search operation does
not take a lot of time. Taking into account such applications, we will now talk about a
special tree data structure, known as binary tree.
Binary Tree
The mathematical definition of a binary tree is
A binary tree is a finite set of elements that is either empty or is partitioned into three
disjoint subsets. The first subset contains a single element called the root of the tree.
The other two subsets are themselves binary trees called the left and right sub-trees.
Each element of a binary tree is called a node of the tree.
Following figure shows a binary tree.
Root
Left subtree
Right subtree
Fig 11.3: Analysis of a binary tree
The same process of sub classing the tree can be done at the second level. That means
the left and right subtrees can also be divided into three parts similarly. Now consider
the left subtree of node A. This left subtree is also a tree. The node B is the root of
this tree and its left subtree is the node D. There is only one node in this left subtree.
The right subtree of the node B consists of two nodes i.e. E and G. The following
figure shows the analysis of the tree with root B.
A
Root
B
Left subtree
Right subtree
Fig 11.4: Analysis of a binary tree
On going one step down and considering right subtree of node B, we see that E is the
root and its left subtree is the single node G. There is no right subtree of node E or we
can say that its right subtree is empty. This is shown in the following figure.
A
Root
Leftsubtree
Root
Right subtree
Fig 11.6: Analysis of a binary tree
Now we apply the same recursive definition on the level below the node C. Now the
right subtree of the node C will be considered as a tree. The root of this tree is the
node F. The left and right subtrees of this root F are the nodes H and I respectively.
The following figure depicts this.
C
Root
Left subtree
Right subtree
A
Left descendant
Right descendant
B
Leaf nodes
Leaf nodes
Fig 11.11: Terminologies used in a binary tree
Level
The level of a node in a binary tree is defined as follows:
Root has level 0,
Level of any other node is one more than the level its parent (father).
The depth of a binary tree is the maximum level of any leaf in the tree.
To understand level of a node, consider the following figure 11.13. This figure shows
the same tree of figure 11.2, discussed so far.
A
---------------------------------------- Level 0
D 2
1 ------------------- Level 1
2 ------- Level 2
-Level 3
H
3
J
3
K
3
L
3
M
3
N
3
O
3
--------------
Level 2: 22 nodes
-----------------
O
-
------------------------------------ Level 3: 23 nodes -------------------------------------------Fig 11.15: Number of nodes at each level in a complete binary tree
By observing the number of nodes at a particular level, we come to the conclusion
that the number of nodes at a level is equal to the level number raising to the power of
two. Thus we can say that in a complete binary tree at a particular level k, the number
of nodes is equal to 2k. Note that this formula for the number of nodes is for a
complete binary tree only. It is not necessary that every binary tree fulfill this
criterion. Applying this formula to each level while going to the depth of the tree (i.e.
d), we can calculate the total number of nodes in a complete binary tree of depth d by
adding the number of nodes at each level. This can be written as the following
summation.
20+ 21+ 22 + + 2d = d j=0 2j = 2d+1 1
Thus according to this summation, the total number of nodes in a complete binary tree
of depth d will be 2d+1 1. Thus if there is a complete binary tree of depth 4, the
total number of nodes in it will be calculated by putting the value of d equal to 4. It
will be calculated as under.
24+1 - 1 = 25 1 = 32 1 = 31
Thus the total number of nodes in the complete binary tree of depth 4 is 31.
We know that the total number of nodes (leaf and non-leaf) of a complete binary tree
of depth d is equal to 2d+1 1. In a complete binary tree, all the leaf nodes are at the
Page 124 of 519
Tips
A priority queue is a variation of queue that does not follow FIFO rule.
Tree is a non-linear data structure.
The maximum level of any leaf in a binary tree is called the depth of the tree.
Other than the root node, the level of each node is one more than the level of
Data Structures
Lecture No. 12
Reading Material
Data Structures And Algorithm analysis in C++
Chapter 4
4.2, 4.3(4.3.2, 4.3.4)
Summary
Description
Returns a pointer to the left sub-tree
Returns a pointer to the right sub-tree
Returns the father node of p
Returns the brother node of p
Returns the contents of node p
These methods have already been discussed at the end of the previous lecture,
however, few more methods are required to construct a binary tree:
Description
Creates the left child node of p and set
the value x into it.
Creates the right child node of p, the
child node contains the info x.
All these methods are required to build and to retrieve values from a tree.
One way of finding duplicates is to compare each number with all those that precede
it. Lets see it in detail.
14 15 4, 9, 7, 4,
18 3, 5, 16 4, 20 17 9, 14 5
14 15 4, 9, 7, 4,
18 3, 5, 16 4, 20 17 9, 14 5
Fig 12.1: Search for Duplicates
Suppose, we are looking for duplicates for the number 4, we will start scanning from
the first number 14. While scanning, whenever we find the number 4 inside, we
remember the position and increment its frequency counter by 1. This comparison
will go on till the end of the list to get the duplicates or fequence of the number 4.
You might have understood already that we will have to perform this whole scanning
of list every time for each number to find duplicates. This is a long and time
consuming process.
So this procedure involves a large number of comparisons, if the list of numbers is
large and is growing.
A linked list can handle the growth. But it can be used where a programmer has no
idea about the size of the data before hand. The number of comparisons may still be
large. The comparisons are not reduced after using linked list as it is a linear data
14
14, 15, 4, 9, 7, 18, 3, 5, 16, 4, 20, 17, 9, 14, 5
14
The next step is to add this node in the tree. We compare 15, the number in the new
node with 14, the number in the root node. As number 15 is greater than number 14,
therefore, it is placed as right child of the root node.
The next number in the list i.e. 4, is compared with 14, the number in the root node.
As the number 4 is less than number 14, so we see if there is a left child of the root
node to compare the number 4 with that. At this point of time in the tree, there is no
further left child of the root node. Therefore, a new node is created and the number 4
is put into it.
The below figure shows the newly created node.
14
15
14
4
15
Fig 12.6: The node is added as the left child of the root node
The next number in the list is 9. To add this number in the tree, we will follow the
already defined and experimented procedure. We compare this number first with the
number in the root node of the tree. This number is found to be smaller than the
number in the root node. Therefore, left sub-tree of the root node is sought. The left
child of the root node is the one with number 4. On comparison, number 9 is found
greater than the number 4. Therefore, we go to the right child of the node with
number 4. At the moment, there is no further node to the right of it, necessitating the
need of creating a new node. The number 9 is put into it and the new node is added as
the right child of the node with number 4. The same is shown in the figure given
below.
14
4
15
9
14, 15, 4, 9, 7, 18, 3, 5, 16, 4, 20, 17, 9, 14, 5
Fig 12.7: A new node is added in the tree
We keep on adding new nodes in the tree in line with the above practiced rule and
eventually, we have the tree shown in the below figure.
14
4
3
15
9
7
5
18
16
20
17
Trace of insert
We will take the tree and the figure, we constructed above. At this time, we want to
insert some new numbers in the tree as given in the figure below:
p
14
q
17
4
15
9
3
7
5
18
16
20
17, 9, 14, 5
After moving the pointer q forward, we make the pointer p point to the same node.
We do this operation as the first step inside the while loop. It can be seen at line 23
above. So following will be the latest position of pointers in the tree.
14
17
4
3
15
p
q
9
7
18
16
20
5
17, 9, 14, 5
17
4
3
15
9
18
p
q
16
20
The previous process is repeated again (while loop is the repeating construct) that the
number 17 is compared with the number 18. This time the left child node is traversed
and q pointer starts pointing the node with number 16 inside. In the next step, p is
moved forward to point to the same node as q.
The same comparison process starts again, number 17 is found to be greater than the
number 16, therefore, the right child is seek. But we see that the current tree node
does not have right child node, so it returns NULL. Following figure depicts it.
14
17
4
15
18
9
7
20
16
p
5
q
17, 9, 14, 5
Fig 12.13: Insertion of a new node in progress
Above shown (q is pointing to NULL) is the condition that causes the while loop in
the code above to terminate. Later we insert the new node as the right child of the
current node.
14
4
3
15
18
9
7
20
16
p
5
17, 9, 14, 5
node
17
p->setRight( node ) ;
Fig 12.14: A new node is inserted in the tree
Page 138 of 519
Data Structures
Lecture No. 13
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.3, 4.6
Summary
Cost of Search
Binary Search Tree (BST)
Traversing a Binary Tree
C++ code
Example
Exercise
In the previous lecture, we had written and demonstrated the use of C++ code of
insert routine for a tree through an example. We also saw how a new node is inserted
into a binary tree. If the to-be-inserted number (node) is already in the tree i.e. it
matches a number already present in the tree, we display a message that the number is
already in the tree. In the last lecture, the advantages of the tree data structure vis-vis linked list data structure were also discussed. In a linked list, a programmer has to
search the whole list to find out a duplicate of a number to be inserted. It is very
tedious job as the number of stored items in a linked list is very large. But in case of
tree data structure, we get a dynamic structure in which any number of items as long
as memory is available, can be stored. By using tree data structure, the search
operation can be carried out very fast. Now we will see how the use of binary tree can
help in searching the duplicate number in a very fast manner.
Cost of Search
Consider the previous example where we inserted the number 17 in the tree. We
executed a while loop in the insert method and carried out a comparison in while loop.
If the comparison is true, it will reflect that in this case, the number in the node where
the pointer p is pointing is not equal to 17 and also q is not NULL. Then we move p
actually q to the left or right side. This means that if the condition of the while loop is
true then we go one level down in the tree. Thus we can understand it easily that if
there is a tree of 6 levels, the while loop will execute maximum 6 times. We conclude
from it that in a given binary tree of depth d, the maximum number of executions of
the while loop will be equal to d. The code after the while loop will do the process
depending upon the result of the while loop. It will insert the new number or display a
message if the number was already there in the tree.
15
Fig 13.1: A three node binary tree
Here we see that the root node has the number 14. The left sub-tree has only one node
i.e. number 4. Similarly the right sub-tree consists of a single node with the number
15. If we apply the permutations combinations on these three nodes to print them,
there may be the following six possibilities.
1: (4, 14, 15)
2: (14, 4, 15)
3: (15, 4, 14)
4: (4, 15, 14)
5: (14, 15, 4)
6: (15, 14, 4)
Look at these six combinations of printing the nodes of the tree. In the first
combination, the order of printing the nodes is 4-14-15. It means that left subtreeroot-right subtree. In the second combination the order is root-left subtree-right
subtree. In the third combination, the order of printing the nodes is right subtree-rootleft subtree. The fourth combination has left subtree-right subtree-root. The fifth
combination has the order root-rigth subtree- left subtree. Finally the sixth
combination has the order of printing the nodes right subtree-root-left subtree. These
six possibilities are for the three nodes only. If we have a tree having a large number
of nodes, then there may increase number of permutations for printing the nodes.
Lets see the general procedure of traversing a binary tree. We know by definition that
a binary tree consists of three sets i.e. root, left subtree and right subtree. The
following figure depicts a general binary tree.
N
node
R
left
subtre
right
subtree
C++ code
Lets write the C++ code for it. Following is the code of the preorder method.
void preorder(TreeNode<int>* treeNode)
{
if( treeNode != NULL )
{
cout << *(treeNode->getInfo())<<" ";
preorder(treeNode->getLeft());
preorder(treeNode->getRight());
}
}
In the arguments, there is a pointer to a TreeNode. We may start from any node and
the pointer of the node will be provided as argument to the preorder method. In this
method, first of all we check whether the pointer provided is NULL or not. If it is not
NULL, we print the information stored in that node with the help of the getInfo()
method. Then we call the getLeft() method that returns a pointer of left node, which
may be a complete subtree. With the help of this method, we get the root of that
subtree. We call the preorder method again passing that pointer. When we return
Example
Lets have a look on the following tree.
15
9
7
18
16
20
17
Fig 13.3:
20
Preorder: 14 4 3 9 7 5 15 18 16 17
This is the same tree we have been using previously. Here we want to traverse the
tree. In the bottom of the figure, the numbers are printed with the help of preorder
method. These numbers are as 14 4 3 9 7 5 15 18 16 17 20. Now take these
numbers and traverse the tree. In the preorder method, we print the root, followed by
traversing of the left subtree and the right subtree respectively. As the value of the
root node is 14, so it will be printed first of all. After printing the value of the root
node, we call the preorder for the left node which is 4. Forget the node 14 as the root
is 4 now. So the value 4 is printed and we call the preorder again with the left sub tree
i.e. the node with value 3. Now the root is the node with value 3. We will print its
value before going to its left. The left side of node with value 3 is NULL. Preorder
will be called if condition is false. In this case, no action will be taken. Now the
preorder of the left subtree is finished. As the right subtree of this node is also NULL,
so there is no need of any action. Now the left subtree of the node with value 4 is
complete. The method preorder will be called for the right subtree of the node with
value 4. So we call the preorder with the right node of the node with value 4. Here,
the root is the node with value 9 that is printed. We will call its left subtree where the
node value is 7. It will be followed by its left subtree i.e. node 5 which will be printed.
In the preorder method, we take the root i.e. 14 in this case. Its value is printed,
followed by its left subtree and so on. This activity takes us to the extreme left node.
Then we back track and print the right subtrees.
Lets try to understand the inorder method from the following statement.
15
9
7
18
16
20
17
Fig 13.4:
Inorder: 3 4 5 7 9 14 15 16 17 18 20
Parameters(F)
Local variables(F)
Local variables(F)
Local variables(F)
Return address(F)
Return address(F)
sp
Parameters(F)
Parameters(F)
Parameters(F)
Return address(F)
sp
Local variables(F)
sp
At point of
Return address(F)
During execution of
After
Fig 13.5
When F is called first time, the parameters, local variables and its return address are
put in a stack, as some function has called it. Now when F calls F, the stack will
increase the parameters, local variables and return address of F comes in the stack
again. It is the second call to F. After coming back from this second call, we go to the
state of first call of F. The stack becomes as it was at first call. In the next lecture, we
will see the functionality of recursive calls by an example.
Exercise
Please find out the preorder, inorder and postorder traversal of the tree given below:
Data Structures
Lecture No. 14
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.3, 4.6
Summary
Recursive Calls
Preorder Recursion
Inorder Recursion
Non-Recursive Traversal
Traversal Trace
We discussed the methods of traversal of the binary tree in the previous lecture. These
methods are- preorder, inorder and postorder. It was witnessed that in the C++ code
that the coding of these methods becomes very short with the use of recursive calls.
The whole tree can be traversed with a few lines of code. We also demonstrated the
benefits of the methods with the help of an example in which a tree was traversed by
preorder, inorder and postorder methods. Implementation of the recursion also came
under discussion in the previous lecture.
Recursive Calls
We know that function calls are made with the help of stacks. When a function calls
some other function, the parameters and return address of the function is put in a
stack. The local variables of the function are also located at the stack and the control
is passed to the called function. When the called function starts execution, it performs
its job by using these parameters and local variables. When there in the function there
comes a return statement or when the function ends then the return address, already
stored in the stack, is used and control goes back to the next statement after the
calling function. Similarly when a function is calling to itself, there is no problem
regarding the stack. We know, in recursion a function calls to itself instead of calling
some other function. Thus the recursion is implemented in the way as other function
calls are implemented.
Preorder Recursion
Now lets see the preorder traversal with respect to the recursion. We will see what
changes happen when preorder function calls itself recursively. Consider the
following binary search tree used in our previous example.
14
15
9
7
5
18
20
16
17
preorder(14)
14
..preorder(4)
4
....preorder(3)
3
......preorder(NULL)
......preorder(NULL)
....preorder(9)
9
......preorder(7)
7
........preorder(5)
5
..........preorder(NULL)
..........preorder(NULL)
........preorder(NULL)
......preorder(NULL)
For the preorder traversal, we call the preorder function and pass it the root node of
the tree (i.e. 14) as a parameter. We have discussed in the preorder traversing that
preorder method first prints the value of the node that is passed to it. So number 14 is
printed. After it, the preorder method calls itself recursively. Thus we will call the
preorder and give it the left subtree of 14 (actually the root node of left subtree) as an
argument. So this call will be preorder (4). As it is preorder traversing, the value of
the root node i.e. 4 will be printed. Now the preorder method looks if there is a left
subtree of the node (i.e.4). If it has a left subtree, the preorder function will call itself
again. The root node of this left subtree will be passed as an argument to the new
function call. Here it is 3, so the number 3 is printed. Thus by the previous three calls
(i.e. preorder(14), preorder(4) and preorder(3) ) we have printed the values 14, 4 and
3. Now the left subtree of 3 is NULL. We know that in preorder method, first of all
we check whether the node is NULL or not. If it is not NULL, the recursion process
continues. It means that we print the value of the node and call the preorder function
again. If the node is NULL, the process ends.
There is always a terminating condition in recursive call or recursive procedure. In the
previous lecture, while trying to end the factorial recursion, we defined the condition
that the factorial of zero is 1. When the factorial of n starts, the factorial is called
again and again by decreasing the value of n by 1. So when we reach at 0, there is no
further recursive call for the factorial as we have set the condition for it i.e. the
factorial of 0 is equal to 1. Thus the recursion ends. Equally is true about the preorder
process. When a node is NULL, then if statement becomes false and we exit the
function (see code of preorder function in previous lecture).
In the above figure, the preorder process that started from node 14, came to an end at
node 3 as the left subtree of 3 is NULL. Now this preorder process will make no
further recursive call. We will come back from here by using the return address of the
call preorder (NULL) from the stack. If we see the stack, this fourth call will come
back to the third call i.e. preorder (3). The left subtree of 3 has ended. Now we have
Page 152 of 519
9
7
5
18
20
16
17
..preorder(15)
15
....preorder(NULL)
....preorder(18)
18
......preorder(16)
16
........preorder(NULL)
........preorder(17)
17
..........preorder(NULL)
..........pre3
der(NULL)
......preord er(20)
20
........preorder(NULL)
preorder(NULL)
Inorder Recursion
Now lets see the inorder recursive calls for traversing a tree. It is not different from
the preorder. The pattern of recursive calls will be changed as in the inorder we
traverse the left subtree first before printing the root. Afterwards, the right subtree is
traversed. The following figures (Fig 14.2(a) and 14.2(b)) explains this phenomenon
of inorder recursion by showing the recursive calls.
9
7
5
18
20
16
17
inorder(14)
..inorder(4)
....inorder(3)
......inorder(null)
3
......inorder(null)
4
....inorder(9)
......inorder(7)
........inorder(5)
..........inorder(null)
5
..........inorder(null)
7
........inorder(null)
9
......inorder(null)
14
We start the inorder with the call inorder (14) as the root of the tree is 14. Now in the
inorder traversing, a programmer has to traverse the left subtree before printing the
value of the root node and then going to the right subtree. So the call to the left
subtree of 14 i.e. inorder (4) will lead to the node 4. At the node 4, the same process
of calling its left subtree, before printing its value, will be applied and we reach at
node 3 that is the left subtree of 4. From the node 3, we go to its left subtree. This left
subtree is NULL. Here in the inorder method, we have the same terminating condition
as that seen in the preorder i.e. we will not make further recursive calls if there
becomes a NULL node in the method call. We end the recursion at that node and
come back. Now when we come back from the NULL left subtree of 3 this shows that
we have visited the left subtree of 3. So the value of the node i.e. 3 is printed. Now we
go to the right subtree of 3. Here the inorder call is inorder (NULL). This call will
make the if statement, in the inorder method, false, leading to the end of the recursion.
Thus we have visited the whole tree whose root is node 3. So we come back to node
4. As the left subtree of node 4 has been visited, we print the value of node i.e. 4.
Thus we have printed the numbers 3 and 4. Now we go to the right subtree of 4. The
right subtree of 4 is the node 9. Now we send 9 as an argument to inorder method. So
there is a call inorder (9). As it is inorder traversing, we go to traverse its left subtree
before printing the number 9. We reach the node 7 and make a call inorder (7). Later,
we go to the left subtree of 7 i.e. the node 5. Now we visit the left subtree of 5 which
is NULL. So here the inorder recursion ends and we come back to node 5, print it and
go to the right subtree of 5. The right subtree of 5 is also NULL. So ending recursion
here, we have finally traversed the tree with 5 as its root. After traversing it, we come
back to the node 7. After visiting the left subtree of 7, we print 7 and go to the right
subtree of 7 which is NULL.
Page 155 of 519
14
15
9
7
5
18
20
16
17
..inorder(15)
....inorder(null)
15
....inorder(18)
......inorder(16)
........inorder(null)
16
........inorder(17)
..........inorder(null)
17
..........inorder(null)
18
......inorder(20)
........inorder(null)
20
........inorder(null)
14
15
9
7
18
20
16
17
push(14)
..push(4)
....push(3)
3
4
..push(9)
....push(7)
......push(5)
5
7
9
14
push(15)
15
push(18)
..push(16)
16
..push(17)
17
18
push(20)
20
This is the same tree earlier discussed with reference to node as 14. We create a stack
and assign the root to the pointer p and have an inner while loop. In the while loop,
we pushed the root node in the stack i.e. push(14). After this, we assign the pointer p
to the left subtree of the root and return to while loop again. Now p is pointing to the
node with value 4, so we push 4 and then 3 on the stack. As the left subtree if 3 is null
so we exit from the inner loop. Afterwards, we pop the stack in the inner loop and
print it. As we know that stack is a LIFO structure (last in, first out). Finally, we have
pushed the node 3. So this node will be popped and the number 3 will be printed.
Now we will assign the right subtree of the node 3 to the p. As this is NULL, the inner
loop will not be executed and again come to the if statement. We will pop the stack in
the if statement and as a result, the node 4 will be popped and printed. Then we will
assign p to the right subtree of node 4 i.e. node 9. The control comes to the inner loop
in which we will push the node 9 and all of its left nodes one by one i.e. push(9),
push(7) and push(5). As the left subtree of the node 5 is NULL, the inner while loop
will be terminated. We will pop the stack and print 5. As the right subtree of node 5 is
NULL, the node 7 will be popped and printed. Here the right subtree of node 7 is
NULL. The stack is again popped and the number 9 will be printed. Here, the right
subtree of node 9 is also NULL. The stack will be popped resulting in popping and
printing of the node 14. Then we go the right subtree of the node 14 which is the node
15. The same rules of left subtree are applied here. You can understand it from the
above table.
Traversal Trace
Page 159 of 519
14
4
15
9
7
5
Level-order:
18
16
20
17
14 4 15 3 9 18 7 16 20 5 17
Fig 14.4: level order traversal
We started from the root with 14 as a root node. Later, we printed its value. Then we
move to the next level. This is the binary tree so that we may have two elements at
next level i.e. 4 and 15. So we printed 4 and 15 while moving from left to right. Now
we are not moving to left or right subtree of any node. Rather, we move to the next
level. At next level, a programmer will have three nodes that are from left to right as
3, 9 and 18. We printed these values. If we take root node as zero level, it will be at
level 2. Now we move to the level 3. Here at level 3, we have 7, 16 and 20. At next
level, we have 5 and 17. So the numbers we are having are- 14 4 15 3 9 18 7 16
20 5 17. How can we programmatically traverse the tree in level order? We will
discuss it in the next lecture.
Data Structures
Lecture No. 15
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.3, 4.3.5, 4.6
Summary
14
4
15
9
7
18
16
20
17
14
4
15
18
16
20
17
Queue: 14
Output:
i
d i i
di
After insertion of node containing number 14, we reach the while loop statement,
where this is taken out from the queue and element 14 is printed. Next the left and
right subtrees are inserted in the queue. The following figure represents the current
stage of the tree.
14
4
15
18
16
20
17
Queue: 4 15
Output: 14
i
i i d d
l
dd d i h
The element that has been printed on the output is shown with dark gray shade while
the elements that have been inserted are shown in the gray shade. After this the
control is transferred again to the start of the while loop. This time, the node
containing number 14 is taken out (because it was inserted first and then the right
node) and printed. Further, the left and right nodes (3 and 9) are inserted in the queue.
The following figure depicts the latest picture of the queue.
14
4
15
9
7
5
18
16
20
17
Queue: 15 3 9
Output: 14 4
i
In the above Queue, numbers 15, 3 and 9 have been inserted in the queue until now.
We came back to the while loop starting point again and dequeued a node from the
queue. This time a node containing number 15 came out. Firstly, the number 15 is
printed and then we checked for its left and right child. There is no left child of the
This time the node that is taken out is containing number 3. It is printed on the output.
Now, node containing number 3 does not have any left and right children, therefore,
no new nodes is inserted in the queue in this iteration.
14
4
15
9
7
5
18
16
20
17
Queue: 9 18
Output: 14 4 15 3
i we take out the node containing number 9. Print it on the output.
In the next iteration,
Look for its left subtree, it is there, hence inserted in the queue. There is no right
subtree of 9.
Similarly, if we keep on executing this function. The nodes are inserted in the queue
and printed. At the end, we have the following picture of the tree:
14
4
15
18
16
20
17
Queue:
Output: 14 4 15 3 9 18 7 16 20 5 17
i
As shown in the figure above, the Queue has gone empty and in the output we can see
that we have printed the tree node elements in the level order.
In the above algorithm, we have used queue data structure. We selected to use queue
data structure after we analyzed the problem and sought most suitable data structure.
The selection of appropriate data structure is done before we think about the
programming constructs of if and while loops. The selection of appropriate data
structure is very critical for a successful solution. For example, without using the
6
2
4
3
6
2
4
3
If we want to delete the node containing number 4 then we have to adjust the right
subtree pointer in the node containing value 2 to the inorder successor of 4. The
important point is that the inorder traversal order has to be maintained after the
delete.
6
2
6
8
6
8
2
1
6
2
5
3
Inorder successor
In this tree, we want to delete the node containing number 2. Lets do inorder
traversal of this tree first. The inorder traversal give us the numbers: 1, 2, 3, 4, 5, 6
and 8.
In order to delete the node containing number 2, firstly we will see its right subtree
and find the left most node of it.
The left most node in the right subtree is the node containing number 3. Pay attention
to the nodes of the tree where these numbers are present. You will notice that node
containing number 3 is not right child node of the node containing number 2 instead it
is left child node of the right child node of number 2. Also the left child pointer of
node containing number 3 is NULL.
After we have found the left most node in the right subtree of the node containing
number 2, we copy the contents of this left most node i.e. 3 to the node to be deleted
with number 2.
6
4
4
4
Inorder
successor
Next step isFi
to delete
most node containing
15 13 the
d lleft
t (2)
th i d value 3. Now being the left most
node, there will be no left subtree of it, it might have a right subtree. Therefore, the
deletion of this node is the case 2 deletion and the delete operation can be called
recursively to delete the node.
6
6
3
8
3
5
1
3
4
Now if we traverse the tree in inorder, we get the numbers as: 1, 3, 4, 5, 6 and 8.
Data Structures
Lecture No. 16
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.3 (all sub sections)
Summary
Inorder
successor
In the above example, the node to be deleted is 2. It has both right and left subtrees.
As this is the BST, so while traversing the tree in inorder, we will have sorted data.
The strategy is to find the inorder successor of the node to be deleted. To search the
inorder successor, we have to go to its right subtree and find the smallest element.
Afterwards we will copy this value into the node to be deleted. In the above example,
the number 3 is the smallest in the right subtree of the node 2. A view of data of
inorder traversal shows that number 3 is the inorder successor of number 2. We copy
the number 3 in the node to be deleted. Now in this transition tree, the number 3 is at
two places. Now we have to delete the inorder successor node i.e. node 3. The node 3
has only right child. So this is the Case II of deletion.
6
We will get the inorder successor of node 3 i.e. number 4. Therefore we will connect
the node 5 with node 4 that is the left pointer of 5 which was earlier pointing to node
3. But now it is pointing to node 4. We delete the node 3. Remember that number 3
has been copied in the node 2. This way, we will delete the node having right and left
both subtrees.
// For NULL
Sample Program
Here is the code of the program. BinarySearchTree.h file.
/* This file contains the declaration of binary node and the binary search tree */
#ifndef BINARY_SEARCH_TREE_H_
#define BINARY_SEARCH_TREE_H_
#include <iostream.h>
// For NULL
/**
* Find the smallest item in the tree.
* Return smallest item or ITEM_NOT_FOUND if empty.
*/
template <class EType>
const EType & BinarySearchTree<EType>::findMin( ) const
{
return elementAt( findMin( root ) );
}
/**
* Find the largest item in the tree.
* Return the largest item of ITEM_NOT_FOUND if empty.
*/
template <class EType>
const EType & BinarySearchTree<EType>::findMax( ) const
{
return elementAt( findMax( root ) );
}
/**
* Find item x in the tree.
* Return the matching item or ITEM_NOT_FOUND if not found.
*/
template <class EType>
const EType & BinarySearchTree<EType>::
find( const EType & x ) const
{
return elementAt( find( x, root ) );
}
/**
* Make the tree logically empty.
*/
template <class EType>
void BinarySearchTree<EType>::makeEmpty( )
{
makeEmpty( root );
}
/**
* Test if the tree is logically empty.
* Return true if empty, false otherwise.
*/
template <class EType>
bool BinarySearchTree<EType>::isEmpty( ) const
{
Data Structures
Lecture No. 17
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
Summary
Reference Variables
Sample Program
After demonstrating the use of const and reference variables in the interface class for
a binary search tree in the previous lectures, we will now see what these reference
variables are and how does internal process go on during the use of reference
variables.
Reference Variables
Before proceeding ahead, there is need to know why the reference variables are used;
and what benefits a programmer can get while employing them. We will discuss these
all things in detail with the help of examples.
The symbol &, used for reference variable has a few different purposes with respect
to its occurrence in the code. In C++ programming, we have seen that when the
ampersand sign i.e. & appears in front of a variable name, it is the address operator. It
returns the address of the variable in front of which it is written. Thus for example, if
x is a variable name, then &x ; will return the address of the variable x. In general we
can say that
Page 195 of 519
Process 1
(browser)
Code
Process 3
(Word)
Static data
Process 4
(ourtest.exe)
Stack
Process 2
(Dev-C++)
Windows Os
Heap
sp
Parameters (intMinus1)
Local
variables(intMinus1)
R
dd
stack grows downwards
Fig 17.2: call stack layout
In the stack layout figure, there are the entries of the caller in the upper portion. This
is due to the fact that the caller function itself was called (executed) by some one else.
The lower portion of the stack describes the stack status when the function intMinus1
is called. The stack contains parameters, local variables and return address of the
function intMinus1. There is a stack pointer sp that points to the top of the stack (the
figure shows the stack downward; it should not be confused with the top). As the
caller has called intMinus1, this function is on the top of the stack.
The following figure shows the contents of the stack when the function intMinus1 is
called.
1072
1068
31
?
myInt
retVal
callers other
stuff
1060
31
1056
oldVal
Called function
1052
sp
stack grows downward
The figure shows the stack in two parts. The first part that is in the upper curly
bracket, shows the contents of the calling function i.e. caller. The lower part shows
the contents of the called function i.e. intMinus1. Recall that we declared two
variables in the caller function. These variables were myInt and retVal. We also
assigned a value 31 to myInt. This value (31) is in the memory location of myInt in the
stack. When we declared the variable retVal, there was no value assigned to it. So its
memory location contains nothing yet. After it, there is the other stuff of the caller
function. We have also shown the memory addresses of the locations on the left hand
side. We can see that the memory address of myInt is 1072 while that of retVal is
1068. Similarly, we see the addresses 1060, 1056 and 1052 further down. We note
that the addresses are with a difference of four. This is because of the use of the
integer variable, as an integer requires four bytes in the memory for storing. Similarly
the pointer variables are also of four bytes. The memory addresses are in 32 bits that
is four bytes (eight bits make a byte). Generally we start addressing the memory from
down. The lowest byte is addressed 0, followed by 1, 2, 3 and so on to upward
direction. If we have larger variables, for example, objects created by our defined
classes, these will require more memory. In that case, the memory chunks will not be
of four bytes. Rather, it will be equal to the size of the object. In C++, we have an
operator size or sizeof by which the size of a type or variable, acquired in the memory,
can be found. There are some machines that do not use byte addressing. These
machines use word addressing. Every word is of 48 bits. All these things (memory
addressing etc) relate to computer architecture course.
Now in the stack, there are the entries of the called function. There is the parameter
oldVal of the called function in the stack. It has the value 31. This is the same value as
that of myInt in the caller function. As studied earlier, in call by value phenomenon,
when arguments (whatever type they have) are sent to a function, a copy of these
arguments is made before sending. That copy is sent to the called function. The called
function uses this copy of the argument. Thus a copy of myInt is passed to the called
function intMinus1. Thus its parameter oldVal has value 31. Now intMinus1 function
uses this value (i.e. oldVal) to do its functionality. It makes alternations to this copy.
The original value that is myInt does not change. This phenomenon is shown in the
following figure. Here the function intMinus1 decreases the value of oldVal by 1 and
becomes 30. The original value (myInt) remains unchanged.
1072
1068
31
?
myInt
retVal
callers other
stuff
1060
31 30
1056
oldVal
Called function
1052
sp
When the return call of intMinus1 is executed, the control comes back to the calling
function. The stuff of the called function intMinus1 is popped from the stack and the
pointer sp moves up accordingly. In other words, the activation record of the function
intMinus1 has been removed from the stack. The oldval (value of which is 30 now) is
returned to the caller function. We have written in the code that this value is assigned
to retVal variable. So in the call stack layout, the memory location of retVal has the
value 30. The figure 17.5 shows the stack layout after return from the called function
i.e. intMinus1.
1072
1068
31
30
myInt
retVal
callers other
stuff
sp
1072
1068
31
?
myInt
retVal
callers other
stuff
1060
1072
1056
oldVal
Called function
1052
sp
stack grows downward
31 29
?
myInt
retVal
callers other
stuff
1060
1072
oldVal
1056
Called function
1052
sp
stack grows downward
1072
1068
31 29
29
myInt
retVal
callers other
stuff
sp
Notice that the value of myInt of the caller function has also been changed.
We have seen that in call by value a copy of the argument is passed and used by the
called function. In this method, the original value of the argument in the calling
function remains unchanged. In the other method, we saw that when we pass the
address of the variable, the pointer is used to manipulate the variable data. In this
case, the original value of the variable in the calling function is changed. Suppose that
we want a function to change an object (variable/argument) but dont want to send the
copy of the object to the function. The reason of not sending a copy is that the object
may be large (as we have seen the object Customer in bank simulation example) and
making a copy of it costs time and memory space. Say, the object Customer is of 500
bytes. Now when a calling function will call a function using call by value method, a
copy of this object will be made on the call stack same as in our previous example a
copy of myInt was made. The copy constructor will make a copy of this whole object
that costs time. Moreover, the copy of this object will take 500 bytes on the stack that
is a lot of memory consuming. There will be as many copies as the number of calls. In
case of a large number of calls, there may be inefficient memory as call stack has a
limited memory. So we do not use call by value methodology. Furthermore, we want
to avoid the massive syntax of pointers. For this purpose, we do not use pointers in
function calling. Now the question arises is there any way through which we can
fulfill our requirement. (i.e. we dont want to make a copy and want to change the
objective without using the pointers ). The use of reference variables may be a
suitable answer to this very ticklish situation. The phenomenon of function calls using
reference variables is termed as call by reference. Following is the code of the caller
function that involves call by reference.
Void caller()
{
int retVal;
int myInt = 31;
retVal = intMinus3( myInt );
cout << myInt << retVal;
}
Note that this is almost the same function, we wrote at first to call intMinus1. The
only difference is that here we are calling the function intMinus3 instead of
intMinus1. We did not use & sign with myInt as used in the call for intMinus2. This is
only due to the fact that we are not sending an address. It is pertinent to note that in
the definition of the function intMinus3, we have used & sign with the argument
variable. We have written it as
int intMinus3( int& oldVal)
myInt
31
?
1068
retVal
callers other
stuff
oldVal
1060
1056
1052
sp
stack grows downward
The caller function part of the stack contains myInt, retVal and other stuff of the caller
function. In the stack part intMinus3, the name oldVal shows that it has nothing in this
portion but it is the other name of the memory location 1072, named as myInt in the
caller function. The dotted arrow shows this.
In the following figure 17.10, we show the oldVal along with the myInt at same
memory location. Here we want to show that the oldVal of called function and myInt
of caller function are the two names of the same memory location.
OldVal
1072
1068
31
?
myInt
retVal
callers other
stuff
Page 205 of 519
1060
1056
Now when the body of the intMinus3 executes the statement i.e.
oldVal = oldVal 3 ;
Sample Program
Following is the program, which demonstrate the above three function calls that we
discussed in the previous example. We define the three functions intMinus1,
intMinus2 and intMinus3. These functions accept the argument as value, pointer and
reference variable respectively. The endl puts a new line and is used in the program
for the output clarity. Here is the code of the program followed by the output of the
program.
/*This program demonstrate tha how the value in a caller function is effected when it
is passed to a function by using call by value, by using pointers and by using call by
reference methods.
*/
#include <iostream.h>
//Function 1, call by value
int intMinus1( int oldVal)
{
oldVal = oldVal 1;
return oldVal;
}
// Function 2, call by using pointers
int intMinus2( int* oldVal)
{
*oldVal = *oldVal 2;
return *oldVal;
}
// Function 3, call by reference
Data Structures
Lecture No. 18
Reading Material
Data Structures and Algorithm Analysis in C++
4.3.3
Chapter. 4
Summary
Reference Variables
const keyword
Tips
Reference Variables
In the last lecture we were discussing about reference variables, we saw three
examples; call by value, call by reference and call by pointer. We saw the use of stack
when a function is called by value, by reference or by pointer. The arguments passed
to the function and local variables are pushed on to the stack.
There is one important point to note that in this course, we are using C/C++ but the
usage of stack is similar in most of the computer languages like FORTRAN and Java .
The syntax we are using here is C++ specific, like we are sending a parameter by
pointer using & sign. In Java, the native data types like int, float are passed by value
and the objects are passed by reference. In FORTRAN, every parameter is passed by
Page 209 of 519
Process 1
(Browser)
Code
Process 3
(Word)
Static Data
Process 4
(Excel)
Stack
Process 2
(Dev-C++)
Windows OS
Heap
One the left of the picture, we can see different processes in the computer memory.
When we zoomed into the one of the processes, we saw the picture on the right. That
firstly, there is a section for code, then for static data and for stack. Stack grows in the
downward section. You can see the heap section given at the end, which grows
upward. An interesting question arises here is that why the stack grows downward
and heap in the upward direction. Think about an endless recursive call of a function
to itself. For every invocation, there will be an activation record on stack. So the
stack keeps on growing and growing even it overwrites the heap section. One the
688
sohail
Customer(sohail) -> c2
644
irfan
Customer(irfan) -> c1
600
1072
1068
600
644
loadCustome
c
.
.
.
.
1060
(644
(elt
1056
1052
enqueue
sp
stack grows downwards
Fig 18.3: Stack layout when q.enqueue(2) called from loadCustomer
The loadCustomer() is being executed. It is containing two pointers c1 and c2
containing the addresses 600 and 643 respectively. enqueue(elt) method is called and
the parameter values (which actually are addresses) 600 and 643 are inserted in the
queue.
Tips
The arithmetic operations we perform on pointers, cannot be performed on
references
Reference variables must be declared and initialized in one statement.
To avoid dangling reference, dont return the reference of a local variable
(transient) from a function.
In functions that return reference, return global, static or dynamically allocated
variables.
The reference data types are used as ordinary variables without any dereference
operator. We normally use arrow operator (->) with pointers.
const objects cannot be assigned any other value.
If an object is declared as const in a function then any further functions called
from this function cannot change the value of the const object.
Data Structures
Lecture No. 19
Reading Material
Data Structures and Algorithm Analysis in C++
4.4
Chapter. 4
Summary
15
18
16
20
17
It does not seem to be a binary tree. Rather, it gives a look of a linked list, as there is a
link from 3 to 4, a link from 4 to 5, a link from 5 to 7. Similarly while traversing the
right link of the nodes, we reached at the node 20. There is no left child of any node.
Thats why, it looks like a link list. What is the characteristic of the link list? In link
list, every node has a pointer that points to the next node. While following this
pointer, we can go to the next node. The root of this tree is 3. Now we have to find the
node with value 20 in this tree. Remember that it is a tree, not a link list. We will use
find method to search the number 20 in this tree. Now we will start from the root. As
20 is greater than 3, the recursive call to the method find will be made and we come to
the next node i.e. 4. As 20 is greater than 4, so again a recursive call is generated.
Similarly we will come to 5, then 7, 9 and so on. In the end, we will reach at 20 and
the recursion will stop here.
15
16
17
18
20
This tree seems to be a balanced tree. We have made 14 as the root. The nodes at the
left side occur at the left of all the nodes i.e. left subtree of 14 is 9, the left subtree of 9
is 7, the left subtree of 7 is 5 and so on. Similarly the right subtree contains the nodes
15, 16, 17, 18, 20. This tree seems to be a balanced tree. Lets see its level. The node
14 i.e. the root is at level zero. Then at level one, we have 9 and 15. At level two,
there are 7 and 16. Then 5 and 17, followed by 4 and 18. In the end, we have 3 and
20. It seems that we have twisted the tree in the middle, taking 14 as a root node. If
we take other nodes like 9 or 7, these have only left subtree. Similarly if we take 15 or
16, these have right subtrees only. These nodes do not have both right and left
subtree. In the earlier example, we have seen that the nodes have right and left
subtrees. In that example, the data was not sorted. Here the tree is not shallow. Still
we can not get the required BST. What should we do? With the sorted data, the tree
can not become complete binary search tree and the search is not optimized. We want
the data in unsorted form that may not be available.
We want to make a balanced tree, keeping in mind that it should not be shallow one.
We could insist that every node must have left and right subtrees of same height. But
this requires that the tree be a complete binary tree. To achieve it, there must be (2d+1
1) data items, where d is the depth of the tree. Here we are not pleading to have
unsorted data. Rather, we need as much data which could help make a balanced
binary tree. If we have a tree of depth d, there will be need of (2d+1 1) data items i.e.
we will have left and right subtrees of every node with the same height. Now think
yourself that is it possible that whenever you build a tree or someone uses your BST
class can fulfill this condition. This is not possible that whenever we are going to
create a tree, there will be (2d+1 1) data items for a tree of depth d. The reason is
that most of the time you do not have control over the data. Therefore this is too rigid
condition. So this is also not a practical solution.
AVL Tree
AVL tree has been named after two persons Adelson-Velskii and Landis. These two
had devised a technique to make the tree balanced. According to them, an AVL tree is
identical to a BST, barring the following possible differences:
An AVL Tree
Level
0
2
3
This is an AVL tree. The root of the tree is 5. At next level, we have 2 and 8, followed
by 1, 4 and 7 at next level where 1, 4 are left and right subtrees of node 2 and 7 is the
left subtree of node 8. At the level three, we have 3. We have shown the levels in the
figure at the right side. The root is at level 0, followed by the levels 1, 2 and 3. Now
see the height of the left subtree of 5. It is 3. Similarly the height of the right subtree
is 2. Now we have to calculate the difference of the height of left subtree and right
subtree of 5. The height of left subtree of 5 is 3 and height of right subtree of 5 is 2.
So the difference is 1. Similarly, we can have a tree in which right subtree is deeper
than left subtree. The condition in the AVL tree is that at any node the height of left
subtree can be one more or one less than the height of right subtree. These heights, of
course, can be equal. The difference of heights can not be more than 1. This
difference can be -1 if we subtract the height of left subtree from right subtree where
the height of left subtree is one less than the height of right subtree. Remember that
this condition is not at the root. It should satisfy at any level at any node. Lets
analyze the height of left subtree and right subtree of node 2. This should be -1, 0 or
1. The height of left subtree of node 2 is 1 while that of right subtree of the node 2 is
2. Therefore the absolute difference between them is 1. Similarly at node 8, the height
of left subtree is 1 and right subtree does not exist so its height is zero. Therefore the
difference is 1. At leaves, the height is zero, as there is no left or right subtree. In the
above figure, the balanced condition is satisfactory at every level and node. Such trees
have a special structure.
Lets see another example. Here is the diagram of the tree.
Not an AVL
Level
0
4
3
2
5
The height of the left subtree of node 6 is three whereas the height of the right subtree
is one. Therefore the difference is 2. The balanced condition is not satisfactory.
Therefore, it is not an AVL tree.
Lets give this condition a formal shape that will become a guiding principle for us
while creating a tree. We will try to satisfy this condition during the insertion of a
node in the tree or a deletion of a node from the tree. We will also see later how we
can enforce this condition satisfactorily on our tree. As a result, we will get a tree
whose structure will not be like a singly linked list.
The definition of height of a tree is:
The height of a binary tree is the maximum level of its leaves (also called the
depth).
The height of a tree is the longest path from the root to the leaf. This can also be
calculated as the maximum level of the tree. If we have to calculate the height of
some node, we should start counting the levels from that node.
The balance of a node is defined as:
The balance of a node in a binary tree is defined as the height of its left subtree
minus height of its right subtree.
Here, for example, is a balanced tree whose each node has an indicated balance of 1,
0, or 1.
0
-1
0
0
0
0
0
0
0
0
In this example, we have shown the balance of each node instead of the data item. In
the root node, there is the value -1. With this information, you know that the height of
the right subtree at this node is one greater than that of the left subtree. In the left
subtree of the root, we have node with value 1. You can understand from this example
that the height of the right subtree at this node is one less than the height of the left
subtree. In this tree, some nodes have balance -1, 0 or 1. You have been thinking that
we have to calculate the balance of each node. How can we do that? When we create
a tree, there will be a need of some information on the balance factor of each node.
With the help of this information, we will try to balance the tree. So after getting this
balance factor for each node, we will be able to create a balance tree even with the
sorted data. There are other cases, which we will discuss, in the next lecture. In short,
a balance tree with any kind of data facilitates the search process.
Data Structures
Lecture No. 20
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.4
Summary
AVL Tree
Insertion in AVL Tree
Example (AVL Tree Building)
We will continue the discussion on AVL tree in this lecture. Before going ahead, it
will be better to recap things talked about in the previous lecture. We built a balanced
search tree (BST) with sorted data. The numbers put in that tree were in increasing
sorted order. The tree built in this way was like a linked list. It was witnessed that the
use of the tree data structure can help make the process of searches faster. We have
seen that in linked list or array, the searches are very time consuming. A loop is
executed from start of the list up to the end. Due to this fact, we started using tree data
structure. It was evident that in case, both the left and right sub-trees of a tree are
almost equal, a tree of n nodes will have log2 n levels. If we want to search an item in
this tree, the required result can be achieved, whether the item is found or not, at the
maximum in the log n comparisons. Suppose we have 100,000 items (number or
names) and have built a balanced search tree of these items. In 20 (i.e. log 100000)
comparisons, it will be possible to tell whether an item is there or not in these 100,000
items.
AVL Tree
In the year 1962, two Russian scientists, Adelson-Velskii and Landis, proposed the
criteria to save the binary search tree (BST) from its degenerate form. This was an
effort to propose the development of a balanced search tree by considering the height
as a standard. This tree is known as AVL tree. The name AVL is an acronym of the
names of these two scientists.
An AVL tree is identical to a BST, barring one difference i.e. the height of the left
and right sub-trees can differ by at most 1. Moreover, the height of an empty tree is
defined to be (1).
Keeping in mind the idea of the level of a tree, we can understand that if the root of a
tree is at level zero, its two children (subtrees) i.e. nodes will be at level 1. At level 2,
there will be 4 nodes in case of a complete binary tree. Similarly at level 3, the
number of nodes will be 8 and so on. As discussed earlier, in a complete binary tree,
the number of nodes at any level k will be 2k. We have also seen the level order
traversal of a tree. The term height is identical to the level of a tree. Following is the
figure of a tree in which level/height of nodes is shown.
Page 227 of 519
4
3
---------------------------
----------------------------------
---------------------------------------------------------
level
-1
0
1
0
---------------------------
---------------------------------
---------------------------------------------------------
-1
0
0
0
---------------------------
-----------------------------------------------
4
0
------------------------------------
Height
The height of a binary tree is the maximum level of its leaves. This is the same
definition as of depth of a tree.
Balance
The balance of a node in a binary search tree is defined as the height of its left subtree
minus height of its right subtree. In other words, at a particular node, the difference in
heights of its left and right subtree gives the balance of the node.
The following figure shows a balanced tree. In this figure the balance of each node is
shown along with. We can see that each node has a balance 1, 0 or 1.
6
-1
1
0
4
0
1 10
0 3
12
-1 14
0 11
0 13
15
16
17
Here in the figure, we see that the balance of the root (i.e. node 6) is 1. We can find
out this balance. The deepest level of the left subtree is 3 where the nodes 1 and 3 are
located. Thus the height of left subtree is 3. In the right subtree, we see some leaf
nodes at level 3 while some are found at level 4. But we know that the height of the
tree is the maximum level. So 4 is the height of the right subtree. Now we know that
the balance of the root node will be the result of height of left subtree minus the
height of right subtree. Thus the balance of the root node is 3 4 = -1. Similarly we
can confirm the balance of other nodes. The confirmation of balance of the other
nodes of the tree can be done. You should do it as an exercise. The process of height
computation should be understood as it is used for the insertion and deletion of nodes
in an AVL tree. We may come across a situation, when the tree does not remain
balanced due to insertion or deletion. For making it a balanced one, we have to carry
out the height computations.
0
U1
U2
4
0
2
0
U3
1 10
5
B
-1 14
0 11
U4
0
U5
12
0
U6
U7
0 13
B
0
U8
16
U9
15
0
U10
17
U11
U12
By looking at the labels B, U1, U2 .U12, we conclude some conditions that will
be implemented while writing the code for insert method of a balanced tree.
We may conclude that the tree becomes unbalanced only if the newly inserted node
The above conditions are obvious. The balance 1 of a node indicates that the height of
its left subtree is 1 more than the height of its right subtree. Now if we add a node to
this left subtree, it will increase the level of the tree by 1. Thus the difference of
heights will become 2. It violates the AVL rule, making the tree unbalanced.
Similarly the balance 1 of a node indicates that the right subtree of this node is one
level deep than the left subtree of the node. Now if the new node is added in the right
subtree, this right subtree will become deeper. Its depth/height will increase as a new
node is added at a new level that will increase the level of the tree and the height.
Thus the balance of the node, that previously has a balance 1, will become 2.
The following figure (Fig 20.6) depicts this rule. In this figure, we have associated the
new positions with their grand parent. The figure shows that U1, U2, U3 and U4 are
the left descendents of the node that has a balance 1. So according to the condition,
the insertion of new node at these positions will unbalance the tree. Similarly the
positions U5, U6, U7 and U8 are the left descendents of the node that has a balance 1.
Moreover we see that the positions U9, U10, U11 and U12 are the right descendents
of the node that has balance 1. So according to the second condition as stated earlier,
the insertion of a new node at these positions would unbalance the tree.
-1
1
0
0
U1
U2
4
0
2
0 3
U3
5
B
U5
-1 14
10
0 11
U4
0 7
0
U6
12
U7
0 13
B
0
U8
16
U9
15
U10
17
U11
U12
Now lets discuss what should we do when the insertion of a node makes the tree
unbalanced. For this purpose, consider the node that has a balance 1 in the previous
tree. This is the root of the left subtree of the previous tree. This tree is shown as
shaded in the following figure.
-1
1
0
0
U1
U2
4
0
2
0
U3
1 10
5
B
-1 14
0 11
U4
0
U5
12
0
U6
U7
0 13
B
0
U8
16
U9
15
0
U10
17
U11
U12
Now considering the notations of figure 20.8, lets insert a new node in this tree and
observe the effect of this insertion in the tree. The new node can be inserted in the tree
T1, T2 or T3. We suppose that the new node goes to the tree T1. We know that this
new node will not replace any node in the tree. Rather, it will be added as a leaf node
at the next level in this tree (T1). The following figure (fig 20.9) shows this
phenomenon.
A
B
1
T3
T1
T2
1
2
new
Fig 20.9: Inserting new node in AVL tree
Due to the increase of level in T1, its difference with the right subtree of node A (i.e.
T3) will become 2. This is shown with the help of dotted line in the above figure. This
difference will affect the balances of node A and B. Now the balance of node A
becomes 2 while balance of node B becomes 1. These new balances are also shown in
the figure. Now due to the balance of node A (that is 2), the AVL condition has been
violated. This condition states that in an AVL tree the balance of a node cannot be
other than 1, 0 or 1. Thus the tree in fig 20.9 is not a balanced (AVL) tree.
Now the question arises what a programmer should do in case of violation of AVL
condition .In case of a binary search tree, we insert the data in a particular order. So
0
A
0
T1
T2
T3
-2
2
Lets see the balance of nodes at this stage. We see that node 1 is at level 0 (as it is the
root node). The nodes 2 and 3 are at level 1 and 2 respectively. So with respect to the
-2
2
2
1
3
Non AVL Tree
We see that after the rotation, the tree has become balanced. The figure reflects that
the balance of node 1, 2 and 3 is 0. We see that the inorder traversal of the above tree
before rotation (tree on left hand side) is 1 2 3. Now if we traverse the tree after
rotation (tree on right hand side) by inorder traversal, it is also 1 2 3. With respect to
the inorder traversal, both the traversals are same. So we observe that the position of
nodes in a tree does not matter as long as the inorder traversal remains the same. We
have seen this in the above figure where two different trees give the same inorder
traversal. In the same way we can insert more nodes to the tree. After inserting a node
we will check the balance of nodes whether it violates the AVL condition. If the tree,
after inserting a node, becomes unbalance then we will apply rotation to make it
balance. In this way we can build a tree of any number of nodes.
Data Structures
Lecture No. 21
Reading Material
Data Structures and Algorithm Analysis in C++
4.4, 4.4.1
Chapter. 4
Summary
-2
2
3
F ig 2 1 .1 : in s e r t(3 ) s in g le le ft r o ta tio n
4
Fig 21.3: insert(4)
Once we insert a node in the tree, it is necessary to check its balance to see whether it
is within AVL defined balance. If it is not so, then we have to rotate a node. The
balance factor of the node containing number 4 is zero due to the absence of any left
or right subtrees. Now, we see the balance factor of the node containing number 3. As
it has no left child, but only right subtree, the balance factor is 1. The balance factor
of the node containing number 1 is 0. For the node containing number 2, the height of
the left subtree is 1 while that of the right subtree is 2. Therefore, the balance factor
of the node containing number 2 is 1 2 = -1. So every node in the tree in fig. 21.3
has balance factor either 1 or less than that. You must be remembering that the
condition for a tree to be an AVL tree, every nodes balance needs not to be zero
necessarily. Rather, the tree will be called AVL tree, if the balance factor of each
node in a tree is 0, 1 or 1. By the way, if the balance factor of each node inside the
tree is 0, it will be a perfectly balanced tree.
-2
Next, we insert a node containing number 5 and see the balance factor of each node.
The balance factor for the node containing 5 is 0. The balance factor for node
containing 4 is 1 and for the node containing 3 is -2. The condition for AVL is not
Page 239 of 519
You see in the above figure that the node containing number 4 has become the right
child of the node containing number 2. The node with number 3 has been rotated. It
has become the left child of the node containing number 4. Now, the balance factor
for different nodes containing numbers 5, 3 and 4 is 0. To get the balance factor for
the node containing number 2, we see that the height of the left subtree containing
number 2 is 1 while height of the right subtree is 2. So the balance factor of the node
containing number 2 is 1. We saw that all the nodes in the tree above in Fig 21.5
fulfill the AVL tree condition.
If we traverse the tree Fig 21.5, in inorder tree traversal, we get:
1 2 3 4 5
Similarly, if we traverse the tree in inorder given in Fig 21.4 (the tree before we had
rotated the node containing number 3), following will be the output.
1 2 3 4 5
In both the cases above, before and after rotation, we saw that the inorder traversal of
trees gives the same result. Also the root (node containing number 2) remained the
same.
See the Fig 21.4 above. Considering the inorder traversal, we could arrange the tree in
such a manner that node 3 becomes the root of the tree, node 2 as the left child of
node 3 and node 1 as the left child of the node 2. The output after traversing the
changed tree in inorder will still produce the same result:
1 2 3 4 5
While building an AVL tree, we rotate a node immediately after finding that that the
node is going out of balance. This ensures that tree does not become shallow and
remains within the defined limit for an AVL tree.
Lets insert another element 6 in the tree. The figure of the tree becomes:
-2
5
6
The newly inserted node 6 becomes the right child of the node 5. Usually, after the
insertion of a node, we will find out the node factor for each node and rotate it
immediately. This is carried out after finding the difference out of limit. The balance
factor for the node 6 is 0, for node 5 is 1 and 0 for node 3. Node 4 has 1 balance
factor and node 1 has 0. Finally, we check the balance factor of the root node, node 2,
the left subtrees height is 1 and the right subtrees height is 3. Therefore, the balance
factor for node 2 is 2, which necessitates the rotation of the root node 2. Have a look
on the following figure to see how we have rotated the node 2.
4
2
5
3
Now the node 4 has become the root of the tree. Node 2, which was the root node, has
become the left child of node 4. Nodes 5 and 6 are still on their earlier places while
remaining the right child and sub-child of node 4 respectively. However, the node 3,
which was left child of node 4, has become the right child of node 2.
Now, lets see the inorder traversal of this tree:
1 2 3 4 5 6
You are required to practice this inorder traversal. It is very important and the basic
point of performing the rotation operation is to preserve the inorder traversal of the
tree. There is another point to note here that in Binary Search Tree (BST), the root
node remains the same (the node that is inserted first). But in an AVL tree, the root
node keeps on changing.
In Fig 21.6: we had to traverse three links (node 2 to node 4 and then node 5) to reach
the node 6. While after rotation, (in Fig 21.7), we have to traverse the two links (node
Page 241 of 519
-2
5
3
Node 7 is inserted as the right child of node 6. We start to see the balance factors of
the nodes. The balance factors for node 7, 6 are 0 and 1 respectively. As the balance
factor for node 5 is 2, the rotation will be performed on this node. After rotation, we
get the tree as shown in the following figure.
4
2
After the rotation, node 5 has become the left child of node 6. We can see in the Fig
21.9 that the tree has become the perfect binary tree. While writing our program, we
will have to compute the balance factors of each node to know that the tree is a
perfectly balanced binary tree. We find that balance factor for all nodes 7, 5, 3, 1, 6, 2
and 4 is 0. Therefore, we know that the tree is a perfect balanced tree. Let see the
inorder traversal output here:
1 2 3 4 5 6 7
It is still in the same sequence and the number 7 has been added at the end.
16
Fig 21.10: insert(16)
We have inserted a new node 16 in the tree as shown in the above Fig 21.10. This
node has been added as the right child of the node 7. Now, lets compute the balance
factors for the nodes. The balance factor for nodes 16, 7, 5, 3, 1, 6, 2 and 4 is either 0
or 1. So this fulfills the condition of a tree to be an AVL. Lets insert another node
containing number 15 in this tree. The tree becomes as given in the figure below:
4
2
-2
16
15
Fig 21.11: insert(15)
Next step is to find out the balance factor of each node. The factors for nodes 5 and 16
are 0 and 1 respectively. This is within limits of an AVL tree but the balance factor
for node 7 is 2. As this is out of the limits of AVL, we will perform the rotation
operation here. In the above diagram, you see the direction of rotation. After rotation,
we have the following tree:
16
15
Node 7 has become the left child of node 16 while node 15 has attained the form of
the right child of node 7. Now the balance factors for node 15, 7 and 16 are 0, -1 and
2 respectively. Note that the single rotation above when we rotated node 7 is not
enough as our tree is still not an AVL one. This is a complex case that we had not
encountered before in this example.
Cases of Rotation
The single rotation does not seem to restore the balance. We will re-visit the tree and
rotations to identify the problem area. We will call the node that is to be rotated as
(node requires to be re-balanced). Since any node has at the most two children, and a
height imbalance requires that s two sub-trees differ by two (or 2), the violation
will occur in four cases:
1. An insertion into left subtree of the left child of .
2. An insertion into right subtree of the left child of .
3. An insertion into left subtree of the right child of .
4. An insertion into right subtree of the right child of .
The insertion occurs on the outside (i.e., left-left or right-right) in cases 1 and 4.
Single rotation can fix the balance in cases 1 and 4.
Insertion occurs on the inside in cases 2 and 3 which a single rotation cannot fix.
kk22
k
k11
kk11
k
k22
Z
Level n-2
X
Z
Level n-1
new
Level n
new
Fig 21.13: Single right rotation to fix case 1
We have shown, single right notation to fix case 1. Two nodes k2 and k1 are shown in
the figure, here k2 is the root node (and also the node, k1 is its left child and Z
shown in the triangle is its right child. The nodes X and Y are the left and right
subtrees of the node k1. A new node is also shown below to the triangle of the node X,
the exact position (whether this node will be right or left child of the node X) is not
mentioned here. As the new node is inserted as a child of X that is why it is called an
outside insertion, the insertion is called inside if the new node is inserted as a child of
the node Y. This insertion falls in case 1 mentioned above, so by our definition above,
single rotation should fix the balance. The k2 node has been rotated single time
towards right to become the right child of k1 and Y has become the left child of k2. If
we traverse the tree in inorder fashion, we will see that the output is same:
X k1 Y k2 Z
Consider the the figure below:
kk11
k1
kk22
k2
k
k12
Level n-2
Level n-1
Level n
Fig 21.14: Single left rotation to fix case 4
In this figure (Fig 21.14), the new node has been inserted as a child node of Z, that is
why it is shown in bigger size covering the next level. Now this is an example of case
4 because the new node is inserted below the right subtree of the right child of the
Page 245 of 519
k1
k2
k1
kk12
kk22
Z
Level n-2
Level n-1
Level n
new
new
Fig 21.15: Single right rotation fails to fix case 2
We see here that the new node is inserted below the node Y. This is an inside
insertion. The balance factor for the node k2 became 2. We make single rotation by
making right rotation on the node k2 as shown in the figure on the right. We compute
the balance factor for k1, which is 2. So the tree is still not within the limits of AVL
tree. Primarily the reason for this failure is the node Y subtree, which is unchanged
even after making one rotation. It changes its parent node but its subtree remains
intact. We will cover the double rotation in the next lecture.
It is very important that you study the examples given in your text book and try to
practice the concepts rigorously.
Data Structures
Lecture No. 22
Reading Material
Data Structures and Algorithm Analysis in C++
4.4.2
Chapter. 4
Summary
Cases of rotations
Left-right double rotation to fix case 2
Right-left double rotation to fix case 3
C++ Code for avlInsert method
Cases of rotations
In the previous lecture, we discussed how to make insertions in the AVL tree. It was
seen that due to the insertion of a node, the tree has become unbalanced. Resultantly,
it was difficult to fix it with the single rotation. We have analyzed the insertion
method again and talked about the node. The new node will be inserted at the left or
right subtree of the s left child or at the left or right subtree of the s right child.
Now the question arises whether the single rotation help us in balancing the tree or
not. If the new node is inserted in the left subtree of the s left child or in the right
subtree of s right child, the balance will be restored through single rotation.
However, if the new node goes inside the tree, the single rotation is not going to be
successful in balancing the tree.
We face four scenarios in this case. We said that in the case-1 and case-4, single
rotation is successful while in the case-2 and case-3 single rotation does not work.
Lets see the tree in the diagram given below.
Single right rotation fails to fix case 2.
k2
k1
k1
k2
Z
Level n-2
Level n-1
X
Y
Level n
new
new
k2
k1
Z
X
Y
Here k2 is the root node while k1 and Z are the right and left children respectively.
The new node is inserted under Y so we have shown Y in a big triangle. The new node
is inserted in the right subtree of k1, increasing its level by 1. Y is not empty as the
new node was inserted in it. If Y is empty, the new node will be inserted under k1. It
means that Y has a shape of a tree having a root and possibly left and right subtrees.
Now view the entire tree with four subtrees connected with 3 nodes. See the diagram
below.
k3
k1
D
k2
A
B
We have expanded the Y and shown the root of Y as K2, B and C are its left and right
subtrees. We have also changed the notations of other nodes. Here, we have A, B, C
and D as subtrees and k1, k2 and k3 as the nodes. Lets see where the new node is
inserted in this expanded tree and how can we restore its balance. Either tree B or C is
two levels deeper than D. But we are not sure which one is deeper. The value of new
node will be compared with the data in k2 that will decide that this new node should
be inserted in the right subtree or left subtree of the k2. If the value in the new node is
greater than k2, it will be inserted in the right subtree i.e. C. If the value in the new
node is smaller than k2, it will be inserted in the left subtree i.e. B. See the diagram
given below:
k3
k1
k2
D
A
B
1
2
new
new
We have seen the both possible locations of the new node in the above diagram. Lets
see the difference of levels of the right and left subtrees of the k3. The difference of B
or C from D is 2. Therefore the expansion of either of B or C, due to the insertion of
the new node, will lead to a difference of 2. Therefore, it does not matter whether the
new node is inserted in B or C. In both of the cases, the difference becomes 2. Then
we try to balance the tree with the help of single rotation. Here the single rotation
does not work and the tree remains unbalanced. To re-balance it, k3 cannot be left as
the root. Now the question arises if k3 cannot become root, then which node will
become root? In the single rotation, k1 and k3 were involved. So either k3 or k1 will
come down. We have two options i.e. left rotation or right rotation. If we turn k1 into
a root, the tree will be still unbalanced. The only alternative is to place k2 as the new
root. So we have to make k2 as root to balance the tree. How can we do that?
If we make k2 the root, it forces k1 to be k2s left child and k3 to be its right child.
When we carry out these changes, the condition is followed by the inorder traversal.
Lets see the above tree in the diagram. In that diagram, the k3 is the root and k1 is its
left child while k2 is the right child of k1. Here, we have A, B, C and D as subtrees.
You should know the inorder traversal of this tree. It will be A, k1, B, k2, C, k3 and D
where A, B, C and D means the complete inorder traversal of these subtrees. You
should memorize this tree traversal.
Page 250 of 519
k3
k3
Rotate left
k1
k2
k2
k1
D
A
B
C
1
B
2
new
new
new
A
new
On the left side, we have the same tree with k3 as its root. We have also shown the
new nodes as new and new i.e. the new node will be attached to B or C. At first, we
will carry out the left rotation between k1 and k2. During the process of left rotation,
the root k1 comes down and k2 goes up. Afterwards, k1 will become the left child of
k2 and the left subtree of k2 i.e. B, will become the right subtree of k1. This is the
single rotation. You can see the new rotated tree in the above figure. It also shows that
the B has become the right child of the k1. Moreover, the new node is seen with the B.
Now perform the inorder traversal of this new rotated tree. It is A, k1, B, k2, C, k3 and
D. It is same as witnessed in case of the inorder traversal of original tree. With this
single rotation, the k2 has gone one step up while k1 has come down. Now k2 has
become the left child of k3. We are trying to make the k2 the root of this tree. Now
what rotation should we perform to achieve this?
Now we will perform right rotation to make the k2 the root of the tree. As a result, k1
and k2 have become its left and right children respectively. The new node can be
inserted with B or C. The new tree is shown in the figure below:
k3
k2
Rotate right
k2
k1
k3
k1
B
new
new
new
new
Now lets see the levels of new and new. Of these, one is the new node. Here you can
see that the levels of new, new i.e. A and D are the same. The new tree is now a
balanced one. Lets check the inorder traversal of this tree. It should be the same as
that of the original tree. The inorder traversal of new tree is A, k1, B, k2, C, k3 and D,
which is same as that of the original tree.
This is known as double rotation. In double rotation, we perform two single rotations.
As a result, the balance is restored and the AVL condition is again fulfilled. Now we
will see in which order, the double rotation is performed? We performed a left
rotation between k1 and k2 link, followed by a right rotation.
k1
k1
k3
k2
A
k2
k3
D
Here k1 is the root of the tree while k3 is the right child of the k1. k2 is the inner child.
It is the Y tree expanded again here and the new node will be inserted in the k2s right
subtree C or left subtree B. As we have to transform the k2 into the root of the tree, so
the right rotation between the link k2 and k3 will be carried out. As a result of this
rotation, k2 will come up and k3 will go down. The subtree B has gone up with the k2
while subtree C is now attached with the k3. To make the k2 root of the tree, we will
perform the left rotation between then k1 and k2. Lets see this rotation in the figure
below:
Rotate left
k1
k2
k2
k1
k3
A
k3
B
A
C
In the above figure at the right side, we have the final shape of the tree. You can see
that k2 has become the root of the tree. k1 and k3 are its left and right children
respectively. While performing the inorder traversal, you will see that we have
preserved our inorder traversal.
We have started this activity while building an example tree. We inserted numbers in
it. When the balance factor becomes more than one, rotation is performed. During this
process, we came at a point when single rotation failed to balance the tree. Now there
is need to perform double rotation to balance the tree that is actually two single
rotations. Do not take double rotation as some complex function, it is simply two
4
2
1
6
3
7 k1
16 k2
X
(null
Y
Z
(null
15
Here we have shown X, Y and Z in case of the double rotation. We have shown Y
expanded and 15 is inside it. Here we will perform the double rotation, beginning
with the right rotation first.
4
2
1
6
3
k1
7
k3
16
k2
15
Rotate right
We have identified the k1, k2 and k3 nodes. This is the case where we have to perform
right-left double rotation. Here we want to promote k2 upwards. For this purpose, the
right rotation on the link of k2 and k3 i.e. 15 and 16 will be carried out.
4
2
1
6
3
5
Rotate left
k1
7
k2
15
16
k3
The node 15 now comes up while node 16 has gone down. We have to promote k2 to
the top and k3 and k1 will become its right and left children respectively. Now we will
Page 255 of 519
4
2
6
k2
15
k1
7
k3
16
Here we have to check two things. At first, the tree is balanced or not i.e. the AVL
condition is fulfilled or not. Secondly we will confirm that the inorder traversal is
preserved or not. The inorder traversal should be the same as that of the inorder
traversal of original tree. Lets check these two conditions. The depth of the left
subtree of node 4 is 2 while the depth of the right subtree of node 4 is three.
Therefore, the difference of the levels at node 4 is one. So the AVL condition is
fulfilled at node 4. At node 6, we have one level on it left side while at the right side
of node 6, there are two levels. As the difference of levels is one, therefore node 6 is
also balanced according to the AVL condition. Similarly other nodes are also
fulfilling the AVL condition. If you see the figure above, it is clear that the tree is
balanced.
We are doing all this to avoid the link list structure. Whenever we perform rotation on
the tree, it becomes clear from the figure that it is balanced. If the tree is balanced, in
case of searching, we will not have to go very deep in the tree. After going through
the mathematical analysis, you will see that in the worst case scenario, the height of
the tree is 1.44 log2 n. This means that the searching in AVL is logarithmic. Therefore
if there are ten million nodes in an AVL tree, its levels will be roughly as log2(10
million) which is very few. So the traversal in an AVL tree is very simple.
Lets insert some more nodes in our example tree. We will perform single and double
rotations, needed to make the tree balanced. The next number to be inserted is 14. The
position of node 14, according to the inorder traversal, is the right child of 7. Lets see
this in the diagram as:
4
k1
2
1
6
3
k3
15
5
k2
Rotate right
16
14
The new node 14 is inserted as the right child of 7 that is the inner subtree of 15. Here
we have to perform double rotation again. We have identified the k1, k2 and k3. k2
has to become the root of this subtree. The nodes k1 and k3 will come down with their
subtrees while k2 is going to become the root of this subtree. After the right rotation
the tree will be as:
4
k1
2
1
6
3
Rotate left
k2
15
14
k3
16
Page 257 of 519
4
k2
2
7
k1
k3
15
6
5
14
16
k2 has become the root of the subtree. k1 has attained the role of the left child of k2
and k3 has become the right child of the k2. The other nodes 5, 14 and 16 have been
rearranged according to the inorder traversal. The node 7 has come up where as node
6 and 15 have become its left and right child. Now just by viewing the above figure, it
is clear that the tree is balanced according to the AVL condition. Also if we find out
its inorder traversal, it should be the same as the inorder traversal of original tree. The
inorder traversal of the above tree is 1, 2, 3, 4, 5, 6, 7, 14, 15, and 16. This is in sorted
order so with the rotations the inorder traversal is preserved.
Lets insert some more numbers in our tree. The next number to be inserted is 13.
Rotate left
6
5
15
14
16
13
We have to perform single rotation here and rearrange the tree. It will look like as:
15
2
1
14
6
3
16
13
The node 7 has become the root of the tree. The nodes 4, 2, 1, 3, 6, 5 have gone to its
left side while the nodes 15, 14, 13, 16 are on its right side. Now try to memorize the
tree which we build with these sorted numbers. If you remember that it looks like a
link list. The root of that tree was 1. After that we have its right child as 2, the right
child of 2 as 3, then its right child 4 and so on up to 16. The shape of that tree looks
exactly like a linked list. Compare that with this tree. This tree is a balanced one. Now
if we have to traverse this tree for search purposes, we have to go at the most three
levels.
Now you must be clear why we need to balance the trees especially if we have to use
the balanced search trees. While dealing with this AVL condition, it does not matter
Data Structures
Lecture No. 23
Reading Material
Data Structures and Algorithm Analysis in C++
4.4.1, 4.4.2
Chapter. 4
Summary
We demonstrated how to write the insert procedure for an AVL tree in the previous
lecture. The single and double rotation call methods were also discussed at length.
While inserting a node if we see that the tree is going to be unbalanced, it means that
the AVL condition is being violated. We also carried out the balancing of the tree by
calling single or double rotation. Now lets see the code of these routines i.e. single
and double rotation.
k2
k1
Z
Y
X
Fig 23.1: Single Right Rotation
We are going to apply single right rotation on the link between k1 and k2. The node
k1 will be the new root node after rotation. So we get its value by the following
statement
TreeNode <int>* k1 = k2 -> getLeft() ;
Due to the single right rotation, k2 has come down, resulting in the upward movement
of k1. The tree Y has to find its new place. The place of the trees X and Z remains
intact. The change of place of Y is written in the code as below.
k2->setLeft( k1->getRight() );
In the above statement, we get the right child of k1 (i.e. k1 -> getRight() ) i.e. Y and
pass it to setLeft function of k2. Thus it becomes the left child of k2. By the statement
k1 -> setRight (k2) ;
We set the node k2 as right child of k1. Now after these three statements, the tree has
been transformed into the following figure.
k1
k2
X
From the above figure, it is reflected that k1 is now the root of the tree while k2 is its
right child. The Y, earlier the right subtree of k1 (see fig 23.1), has now become the
right subtree of k2. We see that the inorder traversal of this tree is X k1 Y k2 Z. It is
the same as of the tree before rotation in fig 23.1 i.e. X k1 Y k2 Z.
Now we set the heights of k1 and k2 in this re-arranged tree. In the code, to set the
height of k2, we have written
int h = Max(height(k2->getLeft()) , height(k2->getRight()));
k2->setHeight( h+1 );
Here we take an integer h and assign it a value. This value is the maximum height
among the two subtrees i.e. height of left subtree of k2 and height of right subtree of
k2. Then we add 1 to this value and set this value as the height of k2. We add 1 to h as
the height of root is one more than its child.
Similarly we set the height of k1 and get the heights of its left and right subtrees
before finding the higher value of these two by the Max function. Afterwards, we add
1 to this value and set this value as the height of k1. The following two statements
perform this task.
h = Max( height(k1->getLeft()), k2->getHeight());
k1->setHeight( h+1 );
In the end, we return the root node i.e. k1. Thus the right single rotation is completed.
In this routine, we saw that this routine takes root node of a tree as an argument and
then rearranges it, resets the heights and returns the new root node.
Height Function
In the above SingleRightRotation, we used the height routine. This routine calls the
getHeight function for the argument passed to it and returns the value got from that
function. If there is an empty tree, then by definition its height will be -1. So in this
routine, we return the value -1 if the argument passed to it is NULL. Following is the
code of this routine.
k2
k2
k1
Z
Y
Following is the code of the function performing the single left rotation.
TreeNode<int>* singleLeftRotation( TreeNode<int>* k1 )
{
if( k1 == NULL ) return NULL;
// k2 is now the new root
TreeNode<int>* k2 = k1->getRight();
k1->setRight( k2->getLeft() ); // Y
k2->setLeft( k1 );
// reassign heights. First k1 (demoted)
int h = Max(height(k1->getLeft()), height(k1->getRight()));
k1->setHeight( h+1 );
// k1 is now k2's left subtree
h = Max( height(k2->getRight()), k1->getHeight());
k2->setHeight( h+1 );
return k2;
k1
k1
k2
k3
A
A
k2
D
B
k3
In the left portion of the above figure, we see that k1 is the root of the tree. k3 is the
right child of k1 while k2 is the left child of k3. A, B, C and D are trees. We carry out
the right rotation between the link of k3 and k2. In the code, it is shown that if k1 is
not NULL, we go ahead and perform the rotation. The code of this double rotation is
given below.
TreeNode<int>* doubleRightLeftRotation(TreeNode<int>* k1)
{
if( k1 == NULL ) return NULL;
// single right rotate with k3 (k1's right child)
k1->setRight( singleRightRotation(k1->getRight()));
// now single left rotate with k1 as the root
return singleLeftRotation(k1);
}
We perform the right rotation with the help of k3. Now think about the single right
rotation. We are at k3 and its left child k2 will be involved in the rotation. In the code,
for the single right rotation, we have written
k1->setRight( singleRightRotation(k1->getRight()));
Here, we are passing the right child of k1 (i.e. k3) to the single right rotation. The
function singleRightRotation itself will find the left child of k3 to perform the single
right rotation. The singleRightRotation will take k3 downward and bring up its left
k2
k1
k1
k2
k3
A
k3
A
In the above figure, it was witnessed that the double rotation consists of few
statements. Thats why, we have had written the routines for single right and left
rotations. In the double right-left rotation, we just carried out a single right rotation
and the single left rotation to complete the double right-left rotation.
k3
k3
k2
k1
D
D
k1
k2
A
A
B
C
Fig 23.6: Double left-right rotation (A)
In this new rearranged tree, k2 comes up while k1 goes down, becoming the left
subtree of k2. The subtree B, earlier left subtree of k2, now becomes the right subtree
of k1.
The second step in the double left-right rotation is to apply the single right rotation on
this new tree. We will carry out the single right rotation on k3. The pictorial
representation of this single right rotation on k3 is given in the figure below.
k3
k2
k2
k1
k3
k1
C
A
B
Fig 23.7: Double left-right rotation (B)
Page 271 of 519
N
F
C
A
I
D
G
E
K
H
L
M
Here in this tree, the deletion of node A from the left subtree causes the worst case of
deletion in the sense that we have to do a large number of rotations. Now we delete
the node A from the tree. The effect of this deletion is that if we consider the node C
the height of its left subtree is zero now. The height of the right subtree of C is 2.
Thus the balance of C is 2 now. This makes the tree unbalance. Now we will do a
rotation to make the tree balanced. We rotate the right subtree of C that means the link
between C and D so that D comes up and C goes down. This is mentioned in the
figure below.
N
F
C
I
D
G
E
K
H
L
M
After this rotation the tree has transformed into the following figure. Now D becomes
the left child of F and C becomes the left child of D.
N
F
D
C
I
E
K
H
L
M
By looking at the inorder traversal of the tree, we notice that it is preserved. The
inorder traversal of the tree before rotation (i.e. fig 23.8) is C D E F G H I J K L M N.
Page 274 of 519
N
I
F
D
C
K
G
J
H
L
M
Here we see that the nodes G and H, which were in the left subtree of I, now become
the right subtree of F. We see that the tree with I as root is balanced now. Now we
consider the node N. We have not expanded the right subtree of N. Although we have
not shown but there may be nodes in the right subtree of N. If the difference of
heights of left and right subtree of N is greater than 1 then we have to do rotation on
N node to balance the tree.
Thus we see that there may be such a tree that if we delete a node from it we have to
do rotation at each level of the tree. We notice that we have to do more rotations in
deletion as compared to insertion. In deletion when we delete a node we have to
check the balance at each level up to the root. We do rotation if any node at any level
violates the AVL condition. If nodes at a level do not violate AVL condition then we
do not stop here we check the nodes at each level and go up to the root. We know that
a binary tree has log2 N levels (where N is total number of nodes) thus we have to do
log2 N rotations. We have to identify the required rotations that mean we have to
identify that which one rotation out of the four rotations (i.e. single left rotation,
single right rotation, double right-left rotation and double left-right rotation) we have
to do. We have to identify this at each level.
Delete on
Fig 23.12: Deletion Case 1a
this side
Now the action that will be taken in this case is that, change the balance of the parent
node and stop. No further effect on balance of any higher node. There is no need of
rotation in this case. Thus it is the easiest case of deletion.
Lets consider an example to demonstrate the above case.
Consider the tree shown in the following figure i.e. Fig 23.13. This is a perfectly
balanced tree. The root of this tree is 4 and nodes 1, 2 and 3 are in its left subtree. The
nodes 5, 6 and 7 are in the right subtree.
-------------------------------------- 0
------------------ 1
----- 2
Consider the node 2. We have shown the balance of this node with a horizontal line,
which indicates that the height of its left subtree is equal to that of its right subtree.
Similarly we have shown the balance of node 4.
Now we remove the node 1 which is the left subtree of node 2. After removing the left
child (subtree) of 2 the height of left subtree of 2 is 0. The height of right subtree of 2
is 1 so the balance of node 2 becomes 1. This is shown in the figure by placing a
0
---------------------------------------
1
---------------------------
4
2
2
-------------------------
Case 1b:
This case is symmetric to case 1a. In this case, the parent of the deleted node had a
balance of 0 and the node was deleted in the parents right subtree. The following
figure shows that the balance of the node was zero as left and right subtree of it have
the same heights.
Delete on
this side
Fig23.15: Deletion Case 1b
After removing the right child the balance of it changes and it becomes 1, as the
height of its left subtree is 1 greater than the height of right subtree. The action
performed in this case is the same as that in case 1a. That is change the balance of the
parent node and stop. No further effect on balance of any higher node. The previous
example can be done for this case only with the change that remove the right child of
node 2 i.e. 3.
Delete on
this side
Fig 23.16: Deletion Case 2a
Now if we remove a node from the left subtree of this node then the height of left
subtree will decrease by 1 and get equal to the height of right subtree. Thus the
balance of this node will be zero. In this case, the action that we will perform to
balance the tree is that change the balance of the parent node. This deletion of node
may have caused imbalance in higher nodes. So it is advised to continue up to the root
of the tree. We will do rotation wherever the balance of the node violates the AVL
condition.
Data Structures
Lecture No. 24
Reading Material
Data Structures and Algorithm Analysis in C++
4.4
Chapter. 4
Summary
Delete on
this side
Fig 24.1
In the left tree in the Fig 24.1, the horizontal line inside the tree node indicates that
the balance is 0, the right and left subtrees of the node are of equal levels. Now, when
a node is deleted from the left subtree of this node, this may reduce by one level and
cause the balance of the right subtree of the node to increase by 1 relatively. The
Page 280 of 519
Fig 24.2
The node 4 is the root node, nodes 2 and 6 are on level 1 and nodes 1, 3, 5, 7 are
shown on level 2. Now, if we delete the node 1, the balance of the node 2 is tilted
towards right, it is 1. The balance of the root node 4 is unchanged as there is no
change in the number of levels within right and left subtrees of it. Similarly, there is
no change in the balances of other nodes. So we dont need to perform any rotation
operation in this case.
Lets see the second case.
Case 1b: the parent of the deleted node had a balance of 0 and the node was deleted
in the parents right subtree.
Delete on
this side
Fig 24.3
On the left of Fig 24.3, the tree is within the balance limits of AVL. After a node is
deleted from the right subtree of it. The balance of the tree is tilted towards left as
shown in the right tree show in the Fig 24.3. Now, we see what action will be
required to make the tree balanced again.
Delete on
this side
Fig 24.4
In the Fig 24.4 above,
the tree on the left contains the balance factor as 1, which
means that the left subtree of the parent node is one level more than the number of
levels in the right subtree of it. When we delete one node from the left subtree of the
node, the height of the left subtree is changed and the balance becomes 0 as shown in
the right side tree of Fig 24.4. But it is very important understand that this change of
levels may cause the change of balance of higher nodes in the tree i.e.
Change the balance of the parent node. May have caused imbalance in higher nodes
so continue up the tree.
So in order to ensure that the upper nodes are balanced, we calculate their balance
factors for all nodes in higher levels and rotate them when required.
Case 2b: The parent of the deleted node had a balance of -1 and the node was deleted
in the parents right subtree.
Similar to the Case 2a, we will do the following action:
Change the balance of the parent node. May have caused imbalance in higher nodes
so continue up the tree.
Fig 24.5
As shown in the left tree in Fig 24.5, the node A is tilted towards right but the right
subtree of A (node B above) is balanced. The deleted node lies in the left subtree of
the node A. After deletion, the height of the left subtree is changed to h-1 as depicted
in the right tree of above figure. In this situation, we will do the following action:
Perform single rotation, adjust balance. No effect on balance of higher nodes so stop
here.
Single rotate
i
Node A has become the left subtree of node B and node 2 left subtree of node B has
become the right subtree of node A. The balance of node B is tiled towards left and
balance of node A is tilted towards right but somehow, both are within AVL limits.
Hence, after a single rotation, the balance of the tree is restored in this case.
Fig 24.7
In the last case 3a, the right subtree of node A was balanced. But in this case, as
shown in the figure above, the node C is tilted towards left. The node to be deleted
lies in the left subtree of node A. After deleting the node the height of the left subtree
of node A has become h-1. The balance of the node A is shown tilted towards right by
showing two triangular knobs inside node A. So what is the action here.
Double rotation at B. May have affected the balance of higher nodes, so continue up
the tree.
double
rotate
Fig 24.8
Node A, which was the root node previously, has become the left child of the new
root node B. Node C, which was the right child of the root node C has now become
the right child of the new root node B.
Fig 24.9
In the figure above, the right tree of the node B has a height of h-1 while the right
subtree is of height h. When we remove a node from the left subtree of node A, the
new tree is shown on the right side of Fig 24.9. The subtree 1 has height h-1 now,
while subtrees 2 and 3 have the same heights. So the action we will do in this case is:
Single rotation at B. May have effected the balance of higher nodes, so continue up
the tree. single
rotate
Fig 24.10
These were the five cases of deletion of a node from an AVL tree. Until now, we are
trying to understand the concept using the figures. You might have noticed the phrase
continue up the tree in the actions above. How will we do it? One way is to maintain
the pointer of the parent node inside each node. But often the easiest method when we
go in downward direction and then upward is recursion. In recursion, the work to be
done later is pushed on to the stack and we keep on moving forward until at a certain
point we back track and do remaining work present in the stack. We delete a node
when we reach at the desired location and then while traversing back, do the rotation
operation on our way to the root node.
Symmetrical to case 2b, we may also have cases 3b, 4b and 5b. This should not be a
problem in doing it yourself.
Fig 24.11
<assign
<id
A
<expr
<expr
<term
<term
<factor>
<id>
Expression grammar
<assign>
<id> := <expr>
<id>
A|B|C
<expr>
<expr> + <term> | <term>
<term>
<term> * <factor> | <factor>
<term
<factor>
<factor>
<id
<id
B
Fig 24.12
SELECT
<SelList>
<Attribute>
title
<FromList>
FROM
<RelName>
WHERE
<FromList>
<Condition>
AND
<RelName>
StarsIn
MovieStar
Condition
Condition
<Attribute>
<Attribute>
<Attribute>
LIKE
<Pattern>
Fig 24.13
setName
name
birthdate
%1960
The root node is Query. This node is further divided into SELECT, <SelList>,
FROM, <FromList>, WHERE and <Condition> subnodes. <SelList> will be an
Attribute and finally a title is reached. Observe the tree figure above, how the tree is
*
d
+
e
*
d
g
f
Fig 24.14
The root node is +, left subtree is capturing the f+d*e expression while the right
subtree is capturing (d*e+f)*g.
Normally compilers has intelligence to look at the common parts inside parse trees.
For example in the tree above, the expressions f+d*e and d*e+f are same basically.
These common subtrees are called common subexpressions. To gain efficiency,
instead of calculating the common subexpressions again, the compilers calculates
them once and use them at other places. The part of the compiler that is responsible to
do this kind of optimization is called optimizer.
See the figure below, the optimizer (part of compiler) will create the following graph
while performing the optimization. Because both subtrees were equivalent, it has
taken out one subtree and connected the link from node * to node +.
g
Graph!
e
Fig 24.15
This figure is not a tree now because it has two or more different paths to reach a
node. Therefore, this has become a graph. The new connection is containing a
directed edge, which is there in graphs.
Optimizer uses the expressions trees and converts them to graphs for efficiency
purposes. You read out from your book, how the expression trees are formed, what
are the different methods of creating them.
Lecture No. 25
Reading Material
Data Structures and Algorithm Analysis in C++
4.2.2, 10.1.2
Chapter. 4, 10
Summary
Expression tree
Huffman Encoding
Expression tree
We discussed the concept of expression trees in detail in the previous lecture. Trees
are used in many other ways in the computer science. Compilers and database are two
major examples in this regard. In case of compilers, when the languages are translated
into machine language, tree-like structures are used. We have also seen an example of
expression tree comprising the mathematical expression. Lets have more discussion
on the expression trees. We will see what are the benefits of expression trees and how
can we build an expression tree. Following is the figure of an expression tree.
+
*
d
In the above tree, the expression on the left side is a + b * c while on the right side,
we have d * e + f * g. If you look at the figure, it becomes evident that the inner nodes
contain operators while leaf nodes have operands. We know that there are two types
of nodes in the tree i.e. inner nodes and leaf nodes. The leaf nodes are such nodes
which have left and right subtrees as null. You will find these at the bottom level of
the tree. The leaf nodes are connected with the inner nodes. So in trees, we have some
inner nodes and some leaf nodes.
In the above diagram, all the inner nodes (the nodes which have either left or right
child or both) have operators. In this case, we have + or * as operators. Whereas leaf
Page 291 of 519
*
d
*
c
*
d
Inorder: ( a + ( b * c )) + ((( d * e ) + f ) * g )
We put an opening parenthesis and start from the root and reach at the node a. After
reaching at plus (+), we have a recursive call for the subtree *. Before this recursive
call, there is an opening parenthesis. After the call, we have a closing parenthesis.
Therefore the expression becomes as (a + ( b * c )). Similarly we recursively call the
right node of the tree. Whenever, we have a recursive call, there is an opening
parenthesis. When the call ends, we have a closing parenthesis. As a result, we have
an expression with parenthesis, which saves a programmer from any problem of
precedence now.
Here we have used the inorder traversal. If we traverse this tree using the postorder
mode, then what expression we will have? As a result of postorder traversal, there
will be postorder expression.
+
*
d
Postorder traversal: a b c * + d e * f + g * +
This is the same tree as seen by us earlier. Here we are performing postorder traversal.
In the postorder, we print left, right and then the parent. At first, we will print a.
Our stack is growing from left to right. The top is moving towards right. Now we
have two nodes in the stack. Go back and read the expression, the next symbol is +
which is an operator. When we have an operator, then according to the algorithm, two
operands are popped. Therefore we pop two operands from the stack i.e. a and b. We
made a tree node of +. Please note that we are making tree nodes of operands as well
as operators. We made the + node parent of the nodes a and b. The left link of the
node + is pointing to a while right link is pointing to b. We push the + node in the
stack.
ab+cde+**
+
a
If symbol is an operator, pop two trees from the stack, form a new tree with operator
as the root and T1 and T2 as left and right subtrees and push this tree on the stack.
Actually, we push this subtree in the stack. Next three symbols are c, d, and e. We
made three nodes of these and push these on the stack.
Next we have an operator symbol as +. We popped two elements i.e. d and e and
linked the + node with d and e before pushing it on the stack. Now we have three
nodes in the stack, first + node under which there are a and b. The second node is c
while the third node is again + node with d and e as left and right nodes.
ab+cde+**
The next symbol is * which is multiplication operator. We popped two nodes i.e. a
subtree of + node having d and e as child nodes and the c node. We made a node of *
and linked it with the two popped nodes. The node c is on the left side of the * node
and the node + with subtree is on the right side.
c
+
The last symbol is the * which is an operator. The final shape of the stack is as under:
ab+cde+**
c
+
In the above figure, there is a complete expression tree. Now try to traverse this tree
in the inorder. We will get the infix form which is a + b * c * d + e. We dont have
parenthesis here but can put these as discussed earlier.
Huffman Encoding
There are other uses of binary trees. One of these is in the compression of data. This is
known as Huffman Encoding. Data compression plays a significant role in computer
networks. To transmit data to its destination faster, it is necessary to either increase
the data rate of the transmission media or simply send less data. The data compression
is used in computer networks. To make the computer networks faster, we have two
options i.e. one is to somehow increase the data rate of transmission or somehow send
the less data. But it does not mean that less information should be sent or transmitted.
Information must be complete at any cost.
Suppose you want to send some data to some other computer. We usually compress
the file (using winzip) before sending. The receiver of the file decompresses the data
before making its use. The other way is to increase the bandwidth. We may want to
use the fiber cables or replace the slow modem with a faster one to increase the
network speed. This way, we try to change the media of transmission to make the
network faster. Now changing the media is the field of electrical or communication
engineers. Nowadays, fiber optics is used to increase the transmission rate of data.
With the help of compression utilities, we can compress up to 70% of the data. How
can we compress our file without losing the information? Suppose our file is of size 1
Mb and after compression the size will be just 300Kb. If it takes ten minutes to
transmit the 1 Mb data, the compressed file will take 3 minutes for its transmission.
You have also used the gif images, jpeg images and mpeg movie files. All of these
standards are used to compress the data to reduce their transmission time.
Compression methods are used for text, images, voice and other types of data.
We will not cover all of these compression algorithms here. You will study about
algorithms and compression in the course of algorithm. Here, we will discuss a
special compression algorithm that uses binary tree for this purpose. This is very
simple and efficient method. This is also used in jpg standard. We use modems to
connect to the internet. The modems perform the live data compression. When data
comes to the modem, it compresses it and sends to other modem. At different points,
compression is automatically performed. Lets discuss Huffman Encoding algorithm.
Huffman code is method for the compression of standard text documents. It makes
use of a binary tree to develop codes of varying lengths for the letters used in the
original message. Huffman code is also part of the JPEG image compression scheme.
The algorithm was introduced by David Huffman in 1952 as part of a course
assignment at MIT.
Now we will see how Huffman Encoding make use of the binary tree. We will take a
Page 299 of 519
List all the letters used, including the "space" character, along with the
frequency with which they occur in the message.
Consider each of these (character, frequency) pairs to be nodes; they are
actually leaf nodes, as we will see.
Pick the two nodes with the lowest frequency. If there is a tie, pick randomly
amongst those with equal frequencies.
Make a new node out of these two, and turn two nodes into its children.
This new node is assigned the sum of the frequencies of its children.
Continue the process of combining the two nodes of lowest frequency till the
time only one node, the root is left.
Lets apply this algorithm on our sample phrase. In the table below, we have all the
characters used in the phrase and their frequencies.
Original text:
traversing threaded binary trees
size:
33
newline)
characters
Letters :
Frequency
NL
(space
and
:
:
:
:
:
:
:
3
3
1
2
5
1
1
The new line occurs only once and is represented in the above table as NL. Then
white space occurs three times. We counted the alphabets that occur in different
words. The letter a occurs three times, letter b occurs just once and so on.
Now we will make tree with these alphabets and their frequencies.
2 is equal to sum of
the frequencies of the
a
3
e
5
t
3
d
2
i
2
n
2
r
5
2
s
2
N
L
b
1
g
1
SP
3
h
1
v
1
y
1
Lecture No. 26
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 4
4.4.2
Summary
Hoffman Encoding
Mathematical Properties of Binary Trees
Huffman Encoding
We will continue our discussion on the Huffman encoding in this lecture. In the
previous lecture, we talked about the situation where the data structure binary tree
was built. Huffman encoding is used in data compression. Compression technique is
employed while transferring the data. Suppose there is a word-document (text file)
that we want to send on the network. If the file is, say, of one MB, there will be a lot
of time required to send this file. However, in case of reduction of size by half
through compression, the network transmission time also get halved. After this
example, it will be quite easy to understand the Hoffman encoding to compress a text
file.
We know that Huffman code is a method for the compression of standard text
documents. It makes use of a binary tree to develop codes of varying lengths for the
letters used in the original message. Huffman code is also a part of the JPEG image
compression scheme. David Huffman introduced this algorithm in the year 1952 as
part of a course assignment at MIT.
In the previous lecture, we had started discussing a simple example to understand
Huffman encoding. In that example, we were encoding the 32-character phrase:
"traversing threaded binary trees". If this phrase were sent as a message in a network
using standard 8-bit ASCII codes, we would have to send 8*32= 256 bits. However,
the Huffman algorithm can help cut down the size of the message to 116 bits.
In the Huffman encoding, following steps are involved:
1. List all the letters used, including the "space" character, along with the
frequency with which they occur in the message.
2. Consider each of these (character, frequency) pairs as nodes; these are actually
leaf nodes, as we will see later.
3. Pick two nodes with the lowest frequency. If there is a tie, pick randomly
amongst those with equal frequencies
4. Make a new node out of these two and develop two nodes as its children.
5. This new node is assigned the sum of the frequencies of its children.
6. Continue the process of combining the two nodes of lowest frequency till the
time, only one node, the root, is left.
In the first step, we make a list of all letters (characters) including space and end line
character and find out the number of occurrences of each letter/character. For example
a, 5
b, 5
Fig 26.1:
We continue this process of combining the two nodes of lowest frequency till the
time, only one node i.e. the root is left.
Now we come back to our example. In this example, there is a text string as written
below.
traversing threaded binary trees
The size of this character string is 33 (it includes 3 space characters and one new line
character). In the first step, we perform the counting of different characters in the
string manually. We do not assign a fake or zero frequency to a letter that is not
present in the string. A programmer may be concerned only with the characters/letters
that are present in the text. We see that the letters and their frequencies in the above
text is as given below.
Character frequency character
NL
1
I
SP
3
n
A
3
r
B
1
s
D
2
t
E
5
v
G
1
y
H
1
Table 1: Frequency table
frequency
2
2
5
2
3
3
1
In the second step, we make nodes of these pairs of letters and frequencies. The
2 is equal to sum
of the frequencies of
the two children
d
a
3
e
5
t
3
d
2
i
2
n
2
r
5
s
2
SP
3
2
N
L
b
1
g
1
h
1
v
1
y
1
Now we continue this process with other nodes. Now we join the nodes g and h as
children of a new node. The frequency of this node is 2 i.e. the sum of frequencies of
g and h. After this, we join the nodes NL and b. This also makes a new node of
frequency 2. Thus the nodes having frequency 1 have joined to the respective parent
nodes. This process is shown in the following figure (Fig 26.3).
e
5
t
3
a
3
i
2
d
2
n
2
s
2
2
N
L
r
5
2
b
1
g
1
2
h
1
v
1
SP
3
y
1
6
a
3
t
3
d
2
4
i
2
n
2
e
5
4
s
2
2
N
L
r
5
2
b
1
g
1
SP
3
2
h
1
v
1
y
1
6
a
3
t
3
d
2
4
i
2
n
2
e
5
4
s
2
2
N
L
10
r
5
2
b
1
g
1
2
h
1
v
1
SP
3
y
1
Now we will join the nodes of frequency 6 and 8 to create the node of frequency 14
and join the nodes of frequency of 9 and 10 to develop a new node of frequency of 19.
At the end, we make the root node with the frequency 33 and it comprises nodes of
frequency 14 and 19. Thus the tree is completed and shown in the following figure.
33
19
14
6
4
t
3
a
3
d
2
4
i
2
n
2
e
5
4
s
2
2
N
L
10
r
5
2
b
1
g
1
2
h
1
v
1
SP
3
y
1
Now we will perform other steps of Hoffman encoding and develop characterencoding scheme needed for data compression.
To go ahead, we have to do the following steps.
Start at the root. Assign 0 to left branch and 1 to the right branch.
Repeat the process down the left and right subtrees.
To get the code for a character, traverse the tree from the root to the character
leaf node and read off the 0 and 1 along the path.
We start from the root node of the tree and assign the value 0 to the left branch and 1
33
1
19
14
1
a
3
t
3
d
2
i
2
10
n
2
s
2
0
0
N
L
e
5
r
5
b
1
g
1
5
0
h
1
2
0
v
1
y
1
SP
3
In the last step, we get the code for the characters. To get
the code for a character, there is need of traversing the
tree from the root to the character leaf node and read off
the 0 and 1 along the path. We start from the root and go
to the letter of leaf node following the edges. 0 and 1 are
written down in the order in which they come in this
traversal to leaf node. For example, we want the code of
Page 312 of 519
code
0101
0110
110
0111
001
11100
11101
We know that every character is stored in the computer in binary format. Each
character has a code, called ASCII code. The ASCII code of a character consists of
ones and zeros i.e. it is in binary form. ASCII code is of eight bits. Normally, we
remember the decimal value of the characters. For example, letter A has decimal
value 65. We can easily convert the decimal value into binary one in bit pattern. We
need not to remember these values. ASCII value of any character can be found from
the ASCII table. The ASCII code of each character is represented in eight bits with
different bit patterns.
Here in the example, we assign a code of our own (i.e. Hoffman code) to the letters
that are in the message text. This code also consists of ones and zeros. The above
table shows the characters and their Hoffman code. Now we come back to the
message text from which we developed the tree and assigned codes to the letters in
the text.
Look at the table (Table 2) shown above. Here we notice that the code of letters is of
variable length. We see that letters with higher frequency have shorter code. There are
some codes with a length five that are the codes of NL, b, g, h, v and y. Similarly we
see that the letters SP, d, i, n and s have codes of length four. The codes of the
Page 313 of 519
00111000011100101110011101010110100101111001
100111101010000100101010011111000101010110000
110111011111001110101101011110000
We see that there are only ones and zeros in the code of message. Some letters have
been shown with their corresponding code. The first three bits 001 are for letter t.
The next three bits 110 are for letter r. After this the letter a has three bits i.e. 000.
Next to it is the letter v that has five bits. These bits are 11100 (shaded in the
figure). Similarly we have replaced all the letters of the message with their
corresponding Hoffman code. The encoded message is shown above.
Lets compare this Hoffman encoded message with the encoding of message with
ASCII code. The ASCII code encoding means that if we have encoded this message
with 8 bits per character, the total length of message would be 264. As there are 33
characters, so the total length is 33 x 8 = 264. But in the Hoffman encoded message
(written in above table), there are only 120 bits. This number of bits is 54% less than
the number of bits in ASCII encoding form. Thus we can send the message with
almost half of the original length to a receiver.
Here the Hoffman encoding process comes to an end. In this process, we took a
message, did the frequency count of its characters and built a tree from these
frequencies. From this tree, we made codes of letters consisting of ones and zeros
only. Then with the help of these codes, we did the data compression. In this process
we saw that less data is required for the same message.
Now an important thing to be noted is regarding the tree building. We have built the
tree with our choices of nodes with same and different frequencies. Now if we have
chosen the nodes to join in different way, the tree built would be in a different form.
This results in the different Hoffman code for the letters. These will be in ones and
zeros but with different pattern. Thus the encoded message would have different
codes for the letters. Now the question arises how does the receiver come to know
that what code is used for what letter? The answer is very simple that the sender has
In this figure, the nodes with value i.e. A, B, C, D, E, F and G are the internal nodes.
Note that the leaf nodes are also included in internal nodes. In the figure, we see that
the right pointer of B is NULL. Similarly, the left pointer of D is NULL. The square
nodes in the figure are the NULL nodes. There is no data in these nodes as these are
NULL pointers. However these are the positions where the nodes can exist. These
square nodes are the external nodes. Now we see in the figure that the internal nodes
(leaf nodes are also included) are 9 and the external nodes (NULL nodes indicated by
squares) are 10 (i.e. 9 + 1). Hence it is the property that we have stated. We will see
the usage of this property in the upcoming lectures.
Data Structures
Lecture No. 27
Reading Material
Data Structures and Algorithm Analysis in C++
4.3
Chapter. 4
Summary
A
Internal link
C
external link
Fig 27.1
Now if you count the total number of links in the diagram between internal and
external nodes, it will be 2N. Remember, we are talking about links and not nodes. In
this tree, we have 9 nodes marked with capital letters, 8 internal links and 10 external
links. Adding the both kinds of links, we get 18, which is exactly 2 x 9.
As discussed already that these properties are mathematical theorems and can
therefore be proven mathematically. Let us now prove this property as to how do we
get 2N links in a binary tree with N internal nodes.
Property
A binary tree with N internal nodes has 2N links, N-1 links to internal nodes and N+1
links to external nodes.
In every rooted tree, each node, except the root, has a unique parent.
Every link connects a node to its parents, so there are N-1 links connecting
internal nodes.
Similarly each of the N+1 external nodes has one link to its parents.
Thus N-1+N+1=2N links.
In the previous lectures, I told you about the important property of the trees, that they
contain only one link between the two nodes. I had also shown you some structures,
which did not follow this property and I told you, that those were graphs.
A
Internal node
F
external node
Fig 27.2
In the figure above, the tree is the same as shown in Fig 27.1. The square nodes
shown in this figure are external nodes. Thinking in terms of pointers all the pointers
of these nodes are NULL or in other words they are available to be used later. We
recognize these nodes as leaf nodes. Besides that, what can we achieve using them is
going to be covered in Threaded Binary Trees.
The threaded tree data structure will replace these NULL pointers with pointers
to the inorder successor (predecessor) of a node as appropriate.
We are creating a new data structure inside the tree and when the tree will be
constructed, it will be called a threaded binary tree. The NULL pointers are replaced
by the inorder successor or predecessor. That means while visiting a node, we can tell
which nodes will be printed before and after that node.
We'll need to know whenever formerly NULL pointers have been replaced by non
NULL pointers to successor/predecessor nodes, since otherwise there's no way to
distinguish those pointers from the customary pointers to children.
This is an important point as we need to modify our previous logic of identifying leaf
nodes. Previously the node with left and right nodes as NULL was considered as the
leaf node but after this change the leaf node will contain pointers to predecessor and
successor. So in order to identify that the pointers has been modified to point to their
inorder successor and predecessor, two flags will be required in the node. One flag
will be used for successor and other for predecessor. If both the pointers were NULL,
left pointer variable will be used to point inorder predecessor, the flag for this will be
turned on and the right pointer variable will be used to keep inorder successor and the
flag will be turned on once the successor address is assigned.
14
15
18
20
16
Fig 27.3
If we print the above tree in inorder we will get the following output:
14 15 18 20
In the above figure, the node 14 contains both left and right links. The left pointer is
pointing to a subtree while the right subtree is pointing to the node 15. The node 15s
right link is towards 18 but the left link is NULL but we have indicated it with a
rounded dotted line towards 14. This indicates that the left pointer points to the
predecessor of the node.
Below is the code snippet for this logic.
t->L = p->L; // copy the thread
t->LTH = thread;
t->R = p; // *p is successor of *t
t->RTH = thread; p->L = t; // attach the new leaf
p->LTH = child;
Lets insert a new node in the tree shown in the above figure. The Fig 27.4 indicates
this new insertion.
14
15
18
20
16
Fig 27.4
The new node 16 is shown in the tree. The left and right pointers of this new node are
NULL. As node 16 has been created, it should be pointed to by some variable. The
name of that variable is t. Next, we see the location in the tree where this new node
with number 16 can be inserted. Clearly this will be after the node 15 but before node
18. As a first step to insert this node in the tree as the left child of the node 18, we did
the following:
1. t->L = p->L; // copy the thread
2. t->LTH = thread;
3. t->R = p; // *p is successor of *t
4. t->RTH = thread;
5. p->L = t; // attach the new leaf
6. p->LTH = child;
As the current predecessor of node 18 is 15. After node 16 will be inserted in the tree,
it will become the inorder predecessor of 18, therefore, in the first line of the code t>L = p->L, left pointer of node 18 (pointed to by pointer p) is assigned to the left
pointer of node 16 (pointer to by pointer t).
14
15
18
p
1
20
16
t
2
Fig 27.5
In the next line of code t->LTH = thread, the left flag is assigned a variable thread
that is used to indicate that it is on.
14
15
18
p
1
20
t
16
2
Fig 27.6
In the third line of code, t->R = p, 18 being the successor of node 18, its pointer p is
assigned to the right pointer (t->R) of node 16.
14
15
18
p
1
20
16 4
2
Fig 27.7
Next line, t->RTH = thread contains flag turning on code.
14
15
18
5
20
16 4
2
Fig 27.8
In the next line p->L = t, the node 16 is attached as the left child of the node 18.
14
15
18
20
16 4
2
Fig 27.9
The flag is truned on in the last line, p->LTH = child.
If we insert few more nodes in the tree, we have the tree as given below:
14
15
4
3
18
16
20
5
Fig 27.10
Above given is a BST and you have seen many BSTs before, which are not thread
binary trees. Without the threads, it is clear from the figure that there are number of
links present in the tree that are NULL. We have converted the NULLs to threads in
this tree.
Lets do inorder non-recursive traversal of the tree. We started at 14 then following
the left link came to 4 and after it to 3. If we use recursion then after the call for node
3 is finished (after printing 3), it returns to node 4 call and then 4 is printed using the
recursive call stack. Here we will print 3 but will not stop. As we have used threads,
we see the right pointer of node 3 that is not NULL and pointing to its successor node
Page 326 of 519
14
15
4
3
18
16
20
5
Fig 27.11
We are at node 4 and want to find its inorder successor. If you remember the delete
operation discussed in the previous lecture, where we searched for the inorder
successor and found it to be the left-most node in the right subtree of the node.
Inorder successor of 4.
14
15
4
3
18
16
20
5
Fig 27.12
Left most node in right subtree of 4
In this figure, the right subtree of 4 is starting from node 9 and ending at node 5. Node
5 is the left most node of it and this is also the inorder successor of node 4. We cannot
go to node 5 directly from node 4, we go to node 9 first then node 7 and finally to
node 5.
14
15
4
3
18
16
20
5
Fig 27.13
We move from node 9 to node 5 following the normal tree link and not thread. As
long as the normal left tree link is there of a node, we have set the LTH flag to child.
When we reach at node 5, the left link is a thread and it is indicated with a flag. See
the while loop given in the above routine again:
while(p->LTH == child)
p = p->L;
return p;
Inorder Traversal
Now by using this routine, we try to make our inorder traversal procedure that is nonrecursive and totally stack free.
If we can get things started correctly, we can simply call nextInorder repeatedly
(in a simple loop) and move rapidly around the tree inorder printing node labels
(say) - without a stack.
14
15
4
3
18
16
20
5
Fig 27.14
The pointer p is pointing to the root node of the tree. If we start traversing from this
node and pass this root node pointer to our routine nexInorder above, it will create a
problem.
We see the routine again to see the problem area clearly:
TreeNode* nextInorder(TreeNode* p)
{
if(p->RTH == thread)
return(p->R);
else {
p = p->R;
while(p->LTH == child)
p = p->L;
return p;
}
}
In the first part, it is checking for the RTH flag to be set to thread, which is not the
case for the root node. The control will be passed to the else part of the routine. In
else part, in the very first step, we are moving towards right of root that is to node 15.
14
15
4
3
P?
18
16
20
5
Fig 27.15
If we call nextInorder with the root of the binary tree, we're going to have some
difficulty. The code won't work at all the way we want.
Note that in this tree inorder traversal, the first number we should print is 3 but now
we have reached to node 15 and we dont know how can we reach node 3. This has
created a problem. In the lecture, we will make a small change in this routine to cater
to this situation i.e. when a root node pointer is passed to it as a parameter. After that
change the routine will work properly in case of root node also and it will be nonrecursive, stack free routine to traverse the tree.
Data Structures
Lecture No. 28
Reading Material
Data Structures and Algorithm Analysis in C++
6.3.1
Chapter. 6
Summary
14
4
p
15
9
7
18
16
20
In the above figure, we have a binary search tree. Threads are also seen in it. These
threads points to the successor and predecessor.
Our nextInoder routine, first of all checks that the right pointer of the node is thread.
It means that it does not point to any tree node. In this case, we will return the right
pointer of the node as it is pointing to the inorder successor of that node. Otherwise,
we will go to some other part. Here we will change the value of pointer p to its right
before running a while loops as long as the left pointer is the node. That means the
left child is not a thread. We move to the left of the pointer p and keep on doing so till
the time the left pointer becomes a thread.
We will pass the root of the tree to the nextInorder routine. The pointer p is pointing
to the node 14 i.e. the root node. As the right pointer of the node 14 is not a thread, so
the pointer p will move to the node 15 as shown below:
14
4
p?
15
9
7
18
16
20
Here we want the inorder traversal. It is obvious from the above figure that 15 is not
the first value. The first value should be 3. This means that we have moved in the
wrong direction. How this problem can be overcome? We may want to implement
some logic that in case of the root node, it is better not to go towards the right side.
Rather, the left side movement will be appropriate. If this is not the root node, do as
usual. It may lend complexities to our code. Is there any other way to fix it? Here we
will use a programming trick to fix it.
We will make this routine as a private member function of the class so other classes
do not have access to it. Now what is the trick? We will insert a new node in the tree.
With the help of this node, it will be easy to find out whether we are on the root node
or not. This way, the pointer p will move in the correct direction.
Lets see this trick. We will insert an extra node in the binary tree and call it as a
dummy node. This is well reflected in the diagram of the tree with the dummy node.
We will see where that dummy node has been inserted.
dummy
14
4
15
18
16
20
This dummy node has either no value or some dummy value. The left pointer of this
node is pointing to the root node of the tree while the right pointer is seen pointing
itself i.e. to dummy node. There is no problem in doing all these things. We have put
the address of dummy node in its right pointer and pointed the left thread of the left
most node towards the dummy node. Similarly the right thread of the right-most node
is pointing to the dummy node. Now we have some extra pointers whose help will
make the nextInorder routine function properly.
Following is a routine fastInorder that can be in the public interface of the class.
/* This routine will traverse the binary search tree */
void fastInorder(TreeNode* p)
{
while((p=nexInorder(p)) != dummy)
}
This routine takes a TreeNode as an argument that make it pass through the root of the
tree. In the while loop, we are calling the nextInorder routine and pass it p. The
pointer returned from this routine is then assigned to p. This is a programming style of
C. We are performing two tasks in a single statement i.e. we call the nextInorder by
passing it p and the value returned by this routine is saved in p. Then we check that
the value returned by the nextInorder routine that is now actually saved in p, is not a
dummy node. Then we print the info of the node. This function is called as:
fastInorder(dummy);
We are not passing it the root of the tree but the dummy node. Now we will get the
correct values and see in the diagrams below that p is now moving in the right
direction. Lets try to understand this with the help of diagrams.
Page 335 of 519
dummy
p
14
4
15
9
7
18
16
20
The pointer p is pointing to the dummy node. Now we will check whether the right
pointer of this node is not thread. If so, then it is advisable to move the pointer
towards the right pointer of the node. Now we will go to the while loop and start
moving on the left of the node till the time we get a node with the left pointer as
thread. The pointer p will move from dummy to node 14. As the left pointer of node
14 is not thread so p will move to node 4. Again the p will move to node 3. As the left
pointer of p is thread, the while loop will finish here. This value will be returned that
is pointing to node 3. The node 3 should be printed first of all regarding the inorder
traversal. So with the help of our trick, we get the right information.
Now the while loop in the fastInorder will again call the nextInorder routine. We
have updated the value of p in the fastInorder that is now pointing to the node 3. This
is shown in the figure below:
14
4
15
9
7
18
16
20
According to the code, we have to follow the right thread of the node 3 that is
pointing to the node 4. Therefore p is now pointing to the node 4. Here 4 is inorder
successor of 3. So the pointer p has moved to the correct node for inorder traversal.
As the right pointer of the node 4 is a link, p will move to node 9. Later, we will go on
the left of nodes and reach at the node 5. Looking at the tree, we know that the inorder
successor of the node 4 is node 5. In the next step, we will get the node 7 and so on.
With the help of threads and links, we are successful in getting the correct inorder
traversal. No recursive call has been made so far. Therefore stack is not used. This
inorder traversal will be faster than the recursive inorder traversal. When other classes
use this routine, it will be faster. We have not used any additional memory for this
routine. We are using the null links and putting the values of thread in it. This routine
is very simple to understand. In the recursive routines, we have to stop the recursion
at some condition. Otherwise, it will keep on executing and lead to the aborting of our
program.
A complete binary tree is a tree that is completely filled, with the possible
exception of the bottom level.
The bottom level is filled from left to right.
You may find the definition of complete binary tree in the books little bit different
from this. A perfectly complete binary tree has all the leaf nodes. In the complete
binary tree, all the nodes have left and right child nodes except the bottom level. At
A
B
log2N
We have taken the floor of the log2 N. If the answer is not an integer, we will take the
next smaller integer. So far, we have been using the pointers for the implementation
of trees. The treeNode class has left and right pointers. We have pointers in the
balance tree also. In the threaded trees, these pointers were used in a different way.
But now we can say that an array can be stored in a complete binary tree without
needing the help of any pointer.
Now we will try to remember the characteristics of the tree. 1) The data element can
be numbers, strings, name or some other data type. The information is stored in the
node. We may retrieve, change or delete it. 2) We link these nodes in a special way
i.e. a node can have left or right subtree or both. Now we will see why the pointers are
being used. We just started using these. If we have some other structure in which trees
can be stored and information may be searched, then these may be used. There should
be reason for choosing that structure or pointer for the manipulation of the trees. If we
A
B
9 10 11 12 13 14
For any array element at position i, the left child is at 2i, the right child is at
(2i +1) and the parent is at floor(i/2).
In the tree, we have links between the parent node and the children nodes. In case of
having a node with left and right children, stored at position i in the array, the left
child will be at position 2i and the right child will be at 2i+1 position. If the value of i
is 2, the parent will be at position 2 and the left child will be at position 2i i.e. 4 .The
right child will be at position 2i+1 i.e. 5. You must be aware that we have not started
from the 0th position. It is simply due to the fact if the position is 0, 2i will also
become 0. So we will start from the 1st position, ignoring the 0th.
Lets see this formula on the above array. We have A at the first position and it has two
children B and C. According to the formula the B will be at the 2i i.e. 2nd position and
C will be at 2i+1 i.e. 3rd position. Take the 2nd element i.e. B, it has two children D
9 10 11 12 13 14
If we want to keep the trees data in the array, the children of B should be at the
position 4 and 5. This is true. We can apply this formula on the remaining nodes also.
Now you have understood how to store trees data in an array. In one respect, we are
using pointers here. These are not C++ pointers. In other words, we have implicit
pointers in the array. These pointers are hidden. With the help of the formula, we can
obtain the left and right children of the nodes i.e. if the node is at the ith position, its
children will be at 2i and 2i+1 position. Lets see the position of other nodes in the
array.
As the node C is at position 3, its children should be at 2*3 i.e. 6th position and 2*3+1
i.e. 7th position. The children of C are F and G which should be at 6th and 7th position.
Look at the node D. It is at position 4. Its children should be at position 8 and 9. E is
at position 5 so its children should be at 10 and 11 positions. All the nodes have been
stored in the array. As the node E does not have a right child, the position 11 is empty
in the array.
9 10 11 12 13 14
You can see that there is only one array going out of E. There is a link between the
parent node and the child node. In this array, we can find the children of a node with
the help of the formula i.e. if the parent node is at ith position, its children will be at 2i
and 2i+1 position. Similarly there is a link between the child and the parent. A child
can get its parent with the help of formula i.e. if a node is at ith position, its parent
will be at floor(i/2) position. Lets check this fact in our sample tree. See the diagram
below:
1 A
2
3
B
C
5
4
D
8
10
9
H
7
G
9 10 11 12 13 14
1 A
2
3
B
C
5
4
D
8
10
9
H
7
G
9 10 11 12 13 14
In the above figure, we see that the number of node A is 1. The node B is on number 2
Page 342 of 519
Data Structures
Lecture No. 29
Reading Material
Data Structures and Algorithm Analysis in C++
6.3
Chapter. 6
Summary
1 A
2
3
B
C
5
4
D
10
7
G
9 10 11 12 13 14
Suppose that this tree is not complete. In other words, B has no right subtree that
means E and J are not there. Similarly we suppose that there is no right subtree of A.
Now the tree will be in the form as shown in the following figure (29.2).
1 A
2
B
4
D
8
9
H
I
A
D
3
9 10 11 12 13 14
D
3
9 10 11 12 13 14
Now imagine that an incomplete binary tree is very deep. We can store this tree in the
array that needs to be of large size. There will be holes in the array. This is the
wastage of memory. Due to this reason, it is thought that if a tree is not completely
binary, it is not good to store it into an array. Rather, a programmer will prefer to use
pointers for the storage.
Remember that two things are kept into view while constructing a data structure that
is memory and time. There should such a data structure that could ensure the running
of the programs in a fast manner. Secondly, a data structure should not use a lot of
memory so that a large part of memory occupied by it does not go waste. To manage
the memory in an efficient way, we use dynamic memory with the help of pointers.
With the use of pointers only the required amount of memory is occupied.
We also use pointers for complex operations with data structure as witnessed in the
deletion operation in AVL tree. One of the problems with arrays is that the memory
becomes useless in case of too many empty positions in the array. We cannot free it
and use in other programs as the memory of an array is contiguous. It is difficult to
free the memory of locations from 50 to 100 in an array of 200 locations. To manage
the memory in a better way, we have to use pointers.
Now we come to a new data structure, called heap.
Heap
Heap is a data structure of big use and benefit. It is used in priority queue. Recall the
example of bank simulation. In that case, we used event-based queues. We put the
events that were going to happen in a special queue i.e. priority queue. This priority
queue does not follow the FIFO rule. We put the elements in the queue at the end but
later got the element with respect to its priority. We get the element that is going to
occur first in the future. In that example, we implemented the priority queue with
arrays. It was seen that when we insert an element in the queue, the internally used
data was sorted in the array. Thus the event with minimum time of occurrence
becomes at first position in the sorted array. We get the event with minimum time
first. After the removal of the element, a programmer shifts the array elements to left.
When we insert a new element, the array is sorted again. As there, in the bank
example, were five or six events, the use of array fulfilled our requirement. We need
not to have other data type that makes the queue efficient. The array is efficient but
sorting is an expensive procedure. It may be complex and time-consuming. Secondly,
Page 346 of 519
13
21
16
24
65
31
26
19
68
32
Figure 29.3: A min heap
This is a complete binary tree. Now consider the values (numbers) in the nodes. This
is not a binary search tree. If we carry out the inorder traversal, the result will be
65, 24, 26, 21, 32, 31, 13, 19, 16, 68.
We can see that it is not in a sorted order as got while traversing a binary search tree.
13
9
21
65
19
31
26
16
68
32
Figure 29.4: Not a heap
Look at the node having value 19. The values in its left and right child nodes are 16
and 68 respectively. Thus the value of left child (i.e. 16) is less than that of the
parent. So it is not a heap. If it were a heap or min heap, the value 16 should have
been parent and the value 19 should have its child. Due to this violation, (the value of
child is less than that of the parent) it is not a heap (min heap).
Max Heap
We can also make a max heap. In max heap, each node has a value greater than the
value of its left and right child nodes. Moreover, in this case, the value of the root
node will be largest and will become lesser at downward levels. The following figure
shows a max heap.
73
52
63
40
25
31
26
27
57
13
Figure 29.5: A max heap
Consider the min heap again. By removing the root from the min heap, we get the
smallest value. Now if the remaining values adjust themselves to again form a heap,
the minimum value among these remaining values will get on the top. And if we
remove this value, there will be the second minimum value of the heap. The
remaining values again form a heap and there will be the smallest number among
these on the top. Thus we will get the third minimum number. If we continue this
process of getting the root node and forming a heap of remaining values, the numbers
will get in ascending order. Thus we get the sorted data. By putting data in heap and
getting it in a particular way, we achieve a sorting procedure. This way, a programmer
gets sorted data.
Now suppose that the numbers in heap are priorities or events. If we build the max
heap, the number with greater priority will get on the top. Now if we get data from the
heap, the data on top will be gotten first. As it is max heap, the data on top will be the
largest value.
Insertion in a Heap
Now lets discuss the insertion of a value in a min or max heap. We insert a value in a
min heap and rearrange it in such a way that the smallest value gets on the top.
Similarly, if we are inserting a value in a max heap, the largest value goes on the top.
To discuss insertion in min heap, consider the following existing heap. This is the
same heap discussed earlier. As it is a complete binary tree, we keep it in an array. In
the figure, the level order of numbers of the nodes is also seen that describes the
position of nodes (index number) in the array.
13
21
4
24
65
26
10
31
16
19 6
68
32
13 21 16 24 31 19 68 65 26 32
0
9 10 11 12 13 14
13
21
4
24
65
26
10
31
19 6
11
32
16
7
68
14
13 21 16 24 31 19 68 65 26 32 14
0
9 10 11 12 13 14
13
21
4
24
65
26
10
31
16
19 6
68
11
32
13 21 16 24 31 19 68 65 26 32
0
9 10 11 12 13 14
Now we compare the new value to be inserted (that is 14) at position 11 with its
parent node. The parent node of newly inserted node is 31 that is greater than the new
value. This is against the (min) heap property. According to the heap property, the
node 14 may be the parent of 31 but not its child. So we move the node 31 to the
position 11 and the new node to the position 5 that was earlier the position of 31. This
technique is also employed in the array. Thus we get the array and tree in the form as
shown in the following figure.
Now if the new node comes at the position 5, its parent (that is node 21 at position 2)
is again greater than it. This again violates the heap property because the parent (i.e.
21) is greater than the child whose value is 14. So we bring the node 21 down and
take the new node up. Thus the node 21 goes to the position 5 and the new node
attains the position 2 in the tree and array as shown in the following figure.
1
13
65
24
26
10
32
21
19 6
11
16
7
68
31
16 24 21 19 68 65 26 32 31
13
0
9 10 11 12 13 14
13
14
4
24
65
26
10
21
19 6
11
32
16
7
68
31
13 14 16 24 21 19 68 65 26 32 31
0
9 10 11 12 13 14
It is clear by now that the tree after insertion of a new value follows the heap
property.
We see that there is an algorithm for inserting a new value in the heap. If we have a
heap stored in an array as a complete binary tree, a new value is put in the array at a
21
5
24
65
26
13
10
31
19 6
11
32
16
7
68
14
13 14 16 24 21 19 68 65 26 32 31
0
9 10 11 12 13 14
13 21 16 24 14 19 68 65 26 32 31
0
9 10 11 12 13 14
After this we compare the node 14 with its new parent. The new parent of node 14
can be found by the formula of floor (i / 2). The parent node of 14 will be at position
floor (5/2) that is position 2. We can see that the node at position 2 is 21. Thus 21 is
greater than its child i.e. 14, violating the heap property. So we again exchange these
values. The node 14 now becomes the parent of 21 and 21 gets its child. In the array,
the nodes 14 and 21 are at positions 2 and 5 respectively. The array representation of
it is as below.
13 14 16 24 21 19 68 65 26 32 31
1
9 10 11 12 13 14
Now we compare this node 14 with its new parent i.e. 13. Here the heap property
stands preserved due to the fact that the parent node i.e. 13 is less than its child node
i.e. 14. So this tree is a heap now. The following figure shows this heap and array
representation of it.
13
14
4
65
24
26
10
32
21
19 6
11
16
7
68
31
Page 356 of 519
13 14 16 24 21 19 68 65 26 32 31
0
9 10 11 12 13 14
Suppose we want to add another node to this heap. This new node has value 15. As
the heap is a complete binary tree, so this new node will be added as left child of node
19. In the array, we see that the position of 19 is 6 so the position of its left child will
be (by formula of 2i) 6 x 2 that is 12. This is shown in the following figure.
13
14
4
24
65
26
10
21
32
11
31
19
16
7
68
15 12
13 14 16 24 21 19 68 65 26 32 31 15
0
9 10 11 12 13 14
13 14 16 24 21 15 68 65 26 32 31 19
0
9 10 11 12 13 14
Now we compare the value 15 with its new parent i.e.16. Here again the parent (16) is
greater than its child (15). So to preserve the heap property we need to exchange these
values. After the exchange, the value 15 is now at position 3 and value 16 is seen
position 6. The following figure shows this step.
13
14
4
24
65
26
10
21
32
11
31
16
15
7
68
19 12
13 14 15 24 21 16 68 65 26 32 31 19
0
9 10 11 12 13 14
Data Structures
Lecture No. 30
Reading Material
Data Structures and Algorithm Analysis in C++
6.3
Chapter. 6
Summary
14
24
65
13
Insert (15)
with exchange
10
26
21
11
32
12
31
16
19
68
15
13
14
16
24
21
19
68
65
26
32
31
15
10
11
12
13
14
Fig 30.1
The new element 15 is inserted at the last array position 12. Being a complete binary
tree, the next new node will be the left child of node 19. As node 19 is at array
position 6 (or level order traversal), its left child will be 6 * 2 = 12th position node or
the value at 12th position in the array. Now, we see where we will have to carry out
the exchange operation. As the parent of 15, the number 19 is greater than it, the first
exchange will be among 19 and 15 as shown in the above figure. After exchange, the
new figure is shown in Fig 30.2.
1
Insert (15)
with exchange
2
14
24
26
21
10
65
13
11
32
12
31
16
15
68
19
13
14
16
24
21
15
68
65
26
32
31
19
10
11
12
13
14
Fig 30.2
You can see that both the elements have exchanged positions i.e. 19 has come down
and 15 gone up. But number 15 is still less than its parent 16, so we will have another
exchange operation.
14
24
26
21
10
65
13
Insert (15)
with exchange
11
32
12
31
15
16
68
19
13
14
15
24
21
16
68
65
26
32
31
19
10
11
12
13
14
Fig 30.3
Now the new parent of 15 is 13 which is less than it. Therefore, the exchange
operation will stop here as 15 has found its destination in the tree. This new tree is not
violating any condition of heap order as witnessed before insertion of 15. It has
become min-heap again while maintaining its status as a complete binary tree.
1
Insert (15)
with exchange
2
14
24
10
65
13
26
21
11
32
12
31
15
16
68
19
13
14
15
24
21
16
68
65
26
32
31
19
10
11
12
13
14
Fig 30.4
A view of the path shows that the number 15 has gone through to find its final
destination in the tree. It has only affected the right sub-trees left branch that was
containing 16, 19 and 15. It has not affected any other branch. This is not a binary
65
14
24
10
26
13
32
21
11
19
16
68
31
Fig 30.5
To understand it better, consider the tree form first, instead of thinking about the
array. Remember, we have implemented the complete binary tree with the help of an
array for the sake of efficiency. It is not necessary to implement it this way. We can
also implement it with pointers. However, heap is normally implemented through
array.
Coming back to the deletion, it is necessary to understand that
Deletion (or removal) causes a hole which needs to be filled.
24
65
14
10
26
21
11
32
16
19
68
31
Fig 30.6
In the above figure, we have deleted the root node, resulting in a hole at the root
position. To maintain it as a complete binary tree, we have to fill this hole. Now, we
will identify the appropriate candidates to fill this hole. As the parent is the minimum
as compared to its right and left children, the appropriate candidates are both right and
left children of the root node. Both children will be compared and the smaller one will
take the vacant place. In the above figure, the children 14 and 16 are candidates for
the vacant position. Being the minimum, 14 will be put in the hole.
deleteMin()
14
65
24
10
26
32
21
11
19
16
68
31
Fig 30.7
In the Fig 30.7, we can see that the 14 has been placed at the root position. However,
the vacuum at the previous position of 14 has created a hole. To fill this hole, the
same logic is applied i.e. we compare both right and left children of the hole and let
the minimum take over the place. Here 24 and 21 are compared. Being the smaller
one, 21 is placed at the vacant position. This way we get the latest figure of the tree as
seen in the following figure. (Fig 30.8.)
21
24
65
14
10
26
11
32
16
19
68
31
Fig 30.8
The hole is now created at previous position of 21. This time, the children- 32 and 31
are compared and being smaller, 31 takes over the place and the tree becomes as
shown in the figure Fig 30.9.
deleteMin()
65
21
24
10
26
14
32
31
19
16
68
11
Fig 30.9
The hole has been transferred from top to the bottom. It has reached at such a position
where it can be deleted.
65
14
3
21
24
31
19
16
68
10
26
32
Fig 30.10
While using array to store complete binary tree, we can free array element at the end.
From the above figure Fig 30.10, the last array index 11 is no more there and the node
has been deleted.
We saw that the data element was inserted at the bottom of the tree and moved
upwards while comparing it with the parents (following the i/2 scheme) until its
destination was found. Similarly, when we deleted a node, the hole was produced.
While following the definition of min-heap, the hole kept on moving from top to the
bottom (following the 2i or 2i+1 scheme).
65
31
32
26
21
19
68
13
24
15
14
16
70
12
10
11
12
13
14
15
Fig 30.11
As shown in the figure Fig 30.11, we are given a list of 15 elements and asked to
construct a heap of them. The numbers are stored in an array, starting from index
(position) 1. This start of array from position 1 is to make the tree construction easier.
It will be clear in a moment. Contrary to the previous situation when array was used
to store heap or complete binary tree, we should now think what would be the picture
of complete binary tree out of it. It may seem complex. But actually it is quite easy.
You start from the very first element in the array i.e. the root of the tree. The children
of root are present at 2i and 2i+1, which in this case, are at positions 2(1) = 2 and
2(1)+1=3. The children nodes for the node at position 2 are at positions 2(2)=4 and
2(2)+1=5. Similarly, the children nodes for the node at position 3 are at positions 6
and 7. Apply this logic to the whole of this array and try to construct a tree yourself.
You should build a tree as given in the figure Fig 30.12.
65
26
13
31
10
24
21
11
15
19
13
12
14
32
16
14
68
70
15
65
31
32
26
21
19
68
13
24
15
14
16
70
12
10
11
12
13
14
15
12
Fig 30.12
Is this tree binary one? Yes, every node has only two left and right children.
Is it a complete binary tree? It surely is as there is no node that has missing left or
right child.
The next question is; is it a min-heap. By looking at the root node and its children, as
the root node is containing 65 and the children nodes are containing 31 and 32, you
can abruptly say that no, it is not a min-heap.
How can we make min-heap out of it? We may look for percolateDown(p).method to
convert it into a min-heap. As discussed above, the percolateDown(p).moves the node
with value p down to its destination in the min-heap tree orders. The destination of a
node, can be its next level or maximum the bottom of the tree (the leaf node). The
node will come down to its true position (destination) as per min-heap order and other
nodes (the other data) will be automatically moving in the upward direction.
The general algorithm is to place the N keys in an array and consider it to be an
unordered binary tree.
The following algorithm will build a heap out of N keys.
for( i = N/2; i > 0; i-- )
percolateDown(i);
A close look on the above loop shows that it is starts from N/2 position and goes
down to 0. The loop will be terminated, once the position 0 is reached. Inside, this
loop is a single function call percolateDown(i). We will try to understand this loop
using figures.
65
31
26
24
21
10
13
Why i = n/2?
11
15
19
14
13
12
14
32
16
68
15
70
12
i
0
65
31
32
26
21
19
68
13
24
15
14
16
70
12
10
11
12
13
14
15
Fig 30.13
You can see the top left of the figure Fig 30.13. It is given i = 15/2 = 7. This is the
initial value of i .The value in the array at position 7 is 68. In our discussion, we will
interchangeably discuss array and binary tree. The facts arising in the discussion
would be applicable to both. Considering this position 7, we will try to build minheap below that. We know that for position 7, we have children at positions 2(7)=14
and 2(7)+1=15. So as children of the number 68 (which is the value at position 7), we
have 70 and 12. After applying the percolateDown(i), we get the tree as shown in Fig
30.14.
i = 15/2 = 7
65
26
13
31
10
24
21
11
15
19
13
12
14
32
16
14
12
70
15
68
i
0
65
31
32
26
21
19
12
13
24
15
14
16
70
68
10
11
12
13
14
15
Fig 30.14
You can see in the figure that 12 has moved upward and 68 has gone down. Now,
what about this little tree starting from 12 and below it is min-heap. Surely, it is.
Next, we go for second iteration in the loop, the value of i is decremented and it
becomes 6.
Page 369 of 519
26
13
31
10
24
21
11
15
19
14
13
12
14
32
15
70
16
12
68
i
65
31
32
26
21
19
12
13
24
15
14
16
70
68
10
11
12
13
14
15
Fig 30.15
The node at position 6 is 19. Here the method percolateDown(i) is applied and we get
the latest tree as shown in Fig 30.16.
i=5
65
26
13
31
10
24
21
11
15
13
12
14
32
16
14
19
12
70
15
68
i
0
65
31
32
26
21
12
13
24
15
14
16
19
70
68
10
11
12
13
14
15
Fig 30.16
The node 5 has come upward while the node 19 has moved downward. Its value of
has decremented. It is now ready for next iteration. If we see the positions of i in the
tree, we can see that it is traveling in one level of the tree, which is the second last
level.
The question might have already arisen in your minds that why did we start i by N/2
instead of N. You think about it and try to find the answer. The answer will be given
in the next lecture. We will continue with this example in the next lecture. You are
strongly encouraged to complete it yourself before the next lecture.
Data Structures
Lecture No. 31
Reading Material
Data Structures and Algorithm Analysis in C++
6.3.3, 6.3.4
Chapter. 6
Summary
BuildHeap
Other Heap methods
C++ Code
BuildHeap
In the previous lecture, we discussed about the BuildHeap method of heap abstract
data structure. In this handout, you are going to know why a programmer adopts this
method. Suppose we have some data that can be numbers or characters or in some
other form and want to build a min-Heap or max-Heap out of it. One way is to use the
insert() method to build the heap by inserting elements one by one. In this method, the
heap property will be maintained. However, the analysis shows that it is NlogN
algorithm i.e. the time required for this will be proportional to NlogN. Secondly, if we
have all the data ready, then it will be better to build a heap at once as it is a better
option than NlogN.
In the delete procedure, when we delete the root, a new element takes the place of
root. In case of min-heap, the minimum value will come up and the larger elements
will move downwards. In this regard, percolate procedures may be of great help. The
percolateDown procedure will move the smaller value up and bigger value down.
This way, we will create the heap at once, without calling the insert() method
internally. Lets revisit it again:
Suppose, there are N data elements (also called as N Keys). We will put this data in
an array and call it as a binary tree. As discussed earlier, a complete binary tree can be
stored in an array. We have a tree in an array but it is not a heap yet. It is not
necessary that the heap property is satisfied. In the next step, we apply the algorithm.
Why did we start the i from N/2? Lets apply this algorithm on the actual data. We
will use the diagram to understand the BuildHeap. Consider the diagram given below:
65
2 31
3 32
4 26
9 24 10 15
13
6 19
21
11 14
12 16
7 68
13 5 14 70
15 12
65 31 32 26 21 19 68 13 24 15 14 16 5 70 12
0
9 10 11 12 13 14 15
In the above diagram, there is an array containing the elements as 65, 31, 32, 26, 21,
19, 68, 13, 24, 15, 14, 16, 5, 70, 12. We have all the data in this array. The zeroth
element of the array is empty. We kept it empty intentionally to apply the 2i, 2i+1
scheme. We will take this array as a complete binary tree. Lets take the first element
that is 65, applying the 2i and 2i+1 formula. This element has 31 and 32 as its
children. Take the second element i.e. 32 and use the formula 2i and 2i+1. Its children
are 26 and 21. We can take the remaining elements one by one to build the tree. The
tree is shown in the above figure. This tree is not a min-heap as it has 65 as root
element while there are smaller elements like 13, 15, 5 as its children. So, this being a
binary tree has been stored in an array.
Lets think about the above formula, written for the heap building. What will be the
initial value of i. As the value of N is 15, so the value of i (i=N/2) will be 7 (integer
division). The first call to percolateDown will be made with the help of the value of i
as 7. The element at 7th position is 68. It will take this node as a root of subtree and
make this subtree a minimum heap. In this case, the left child of node 68 is 70 while
the right child of node 68 is 12. The element 68 will come down while the element 12
moves up.
65
2 31
3 32
4 26
9 24 10 15
13
6 19
21
12 16
11 14
7 12
13 5 14 70
15 68
65 31 32 26 21 19 12 13 24 15 14 16 5 70 68
0
9 10 11 12 13 14 15
Look at this subtree with 12 as the root node. Its left and right children are smaller
than root. This subtree is a minimum heap. Now in the loop, the value of i is
decreased by 1, coming to the mark of 6. The element at 6th position is 19 that has
been passed to percolateDown method. It will convert this small tree into a minimum
heap. The element 19 is greater than both of its children i.e.16 and 5. One of these
will move up. Which node should move up? The node 5 will move up while node 19
will move down. In the next repetition, the value of i becomes 5, i.e. the element 21.
We apply the percolateDown() method again and convert it into minimum heap. Now
the subtree with node 26 will be converted into a minimum heap.
65
2 31
3 32
4 13
9 24 10 15
26
6 5
14
11 21
12 16
7 12
13 19 14 70
15 68
65 31 32 13 14 5 12 26 24 15 21 16 19 70 68
0
9 10 11 12 13 14 15
Page 373 of 519
2 13
3 12
4 24
9 31 10 15
26
6 16
14
11 21
12 32
7 65
13 19 14 70
15 68
5 13 12 24 14 16 65 26 31 15 21 32 19 70 68
0
9 10 11 12 13 14 15
Is this a minimum heap? Does the heap property satisfy or not? Lets analyze this
tree. Start from the root of the tree. Is the value of the root is smaller than that of its
left and right children? The node 5 is smaller than the node 13 and node 12. Check
this on the next level. You will see that the values at each node are smaller than its
children. Therefore it satisfies the definition of the min-heap.
Now we have to understand why we have started the for loop with i = N/2. We can
start i from N. As i moves from level to level in the tree. If we start i from N, it will
Page 374 of 519
C++ Code
Now we will look at the C++ code of the Heap class. The objects of Heap may be got
from this factory class. This class will contain the methods including those discussed
earlier and some new ones. Heap is used both in priority queues and sorting.
We have some public and private methods in the class. Lets have a look on the code.
/* The heap class. This is heap.h file */
template <class eType>
class Heap
{
Page 376 of 519
Data Structures
Lecture No. 32
Reading Material
Data Structures and Algorithm Analysis in C++
6.3
Chapter. 6
Summary
perculateDown Method
getMin Method
buildHeap Method
buildHeap in Linear Time
Theorem
In the previous lecture, we defined a heap class and discussed methods for its
implementation. Besides, the insert and delete procedures were talked about. Here, we
will continue with discussion on other methods of the heap class. C++ language code
will be written for these procedures so that we could complete the heap class and use
it to make heap objects.
We are well aware of the deleteMin method. In this method, array[1] element ( a root
element) is put in the minItem variable. In the min heap case, this root element is the
smallest among the heap elements. Afterwards, the last element of the array is put at
the first position of the array. The following two code lines perform this task.
minItem = array[ 1 ];
array[ 1 ] = array[ currentSize-- ];
perculateDown Method
Then we call the perculateDown method. This is the same method earlier employed in
the build heap process. We passed it the node, say i, form where it starts its function
to restore the heap order. Lets look at this perculateDown method. It takes the array
index as an argument and starts its functionality from that index. In the code, we give
the name hole to this array index. Following is the code of this method.
// hole is the index at which the percolate begins.
template <class eType>
void Heap<eType>::percolateDown( int hole )
{
int child;
eType tmp = array[ hole ];
for( ; hole * 2 <= currentSize; hole = child )
{
child = hole * 2;
if( child != currentSize && array[child+1] < array[ child ] )
Page 381 of 519
getMin Method
We discussed this method in the previous lectures while talking about the interface of
heap. Under this method, the minimum value in the heap is determined. We just try
that element and do not remove it. It is like the top method of stack that gives us the
element on the top of the stack but do not remove it from the stack. Similarly, the
getMin method gives us the minimum value in the heap, which is not deleted from the
heap. This method is written in the following manner.
template <class eType>
const eType& Heap<eType>::getMin( )
{
if( !isEmpty( ) )
return array[ 1 ];
}
Now we will discuss the buildHeap method.
buildHeap Method
This method takes an array along with its size as an argument and builds a heap out of
it. Following is the code of this method.
template <class eType>
void Heap<eType>::buildHeap(eType* anArray, int n )
{
for(int i = 1; i <= n; i++)
array[i] = anArray[i-1];
currentSize = n;
for( int i = currentSize / 2; i > 0; i-- )
percolateDown( i );
}
In the body of this method, at first, there is a for loop that copies the argument array
(i.e. anArray) into our internal array, called array. We do this copy as the array that
this method gets as an argument starts from index zero. But we want that the array
should start from index 1 so that the formula of 2i and 2i +1 could be used easily. In
this for loop, we start putting values in the internal array from index 1. Afterwards,
we set the currentSize variable equal to the number of the elements got as an
argument. Next is the for loop that we have already seen while discussing the build of
heap. In the previous lecture, we used N for number of elements but here, it will be
appropriate to use the currentSize, which is also the number of elements. We start this
loop from the value i = currentSize / 2 and decrement the value of i by 1 for each
iteration and execute this loop till the value of i is greater than 0. In this for loop, we
call the method perculateDown to find the proper position of the value given as an
argument to this method. Thus we find the position for each element and get a heap.
Then there are three small methods. The isEmpty method is used to check whether the
heap is empty. Similarly the isFull method is used to check if the heap is full. The
getSize method is used to get the size of the heap. These three methods i.e. isEmpty,
isFull ans getSize are written below.
Theorem
h : 20 nodes
h -1: 21 nodes
h -2: 22 nodes
h -3: 23 nodes
For any node in the tree that has some height h, darken h tree edges
Go down the tree by traversing left edge then only right edges.
There are N 1 tree edges, and h edges on right path, so number of darkened
edges is N 1 h, which proves the theorem.
Figure 32.3: Marking the first left edge and the subsequent
right edge for height 2 nodes
Similarly we go to the nodes at height 3, mark the first left edge and the subsequent
right edge up to the leaf node. Thus we mark one left and then two right edges for the
nodes at height 3. This is shown in the following figure. The edges marked in this step
are seen as the dark lines.
Figure 32.4: Marking the first left edge and the subsequent two right
edges for height 3 nodes
Now we reach at the root whose height is 4. We mark the first left edge and the
subsequent three right edges for this node. This is shown in the figure below.
Figure 32.5: Marking the first left edge and the subsequent three
right edges for height 4 nodes
Now consider the following figure. In this figure, we show all the marked edges (that
we have marked in the previous steps) in gray and the non-marked edges are shown
with dotted lines.
The marked links are the ones through which the data can move up and down in the
tree. We can move the data in any node at a height to a node at other height following
the marked links. However, for the movement of the data in the root node to right side
of it, we can have opposite of the above figure. The opposite of the above figure can
be drawn by symmetry of the above steps. That means we first mark the right edge
and then the subsequent left edges. This will give us the figure of marked links in
which we can move to the right subtree of root.
Now lets sort out different aspects of the tree shown in above figure. We know that
in case of tree of N nodes, there will be N 1. Now in the tree above there are 31
nodes that means n = 31, so the number of edges is 31 - 1 = 30. The height of the tree
is 4. Height is represented by the letter H. so H = 4. The number of dotted edges (that
were not marked) in the tree is 4 that is the same as the height of the tree. Now we put
these values in the formula for the sum of the heights of the nodes. We know the
formula i.e.
S=NH1
By putting values in this formula, we get
S = 31 4 1 = 26
If we count the darkened edges (marled links) in the above tree, it is also 26 that is
equal to the sum of heights. Thus with the help of these figures, the theorem earlier
proved mathematically, is established by a non-mathematical way.
Data Structures
Lecture No. 33
Reading Material
Data Structures and Algorithm Analysis in C++
6.3, 8.1
Chapter. 6, 8
Summary
#include Event.cpp
#include Heap.cpp
#define PQMAX 30
class PriorityQueue
{
public:
PriorityQueue()
{
heap = new Heap <Event> ( PQMAX );
};
~PriorityQueue()
{
delete heap;
};
Event * remove()
{
if( !heap->isEmpty() )
{
Event * e;
heap->deleteMin( e );
return e;
}
cout << "remove - queue is empty." << endl;
return (Event *) NULL;
};
int insert(Event * e)
{
if( !heap->isFull() )
{
heap->insert( e );
return 1;
}
cout << "insert queue is full." << endl;
return 0;
};
int full(void)
{
return heap->isFull();
};
int length()
{
return heap->getSize();
};
The first line has a file Event.cpp that contains all the events used for simulation. We
are including .cpp files here as done in case of templates of C++. In the second line,
there is heap.cpp while a constant PQMAX has been defined in third line, declaring
the maximum size of the priority queue to be 30. In line 4, class PriorityQueue is
declared. public keyword is given at line 6 indicating that all class members below
will be of public scope. Line 7 starts the class constructors definition while in the line
9; a new heap object is being created. This object is a collection of Event type objects
and the number of elements in the collection is PQMAX. The address (pointer) of the
newly created heap is stored in the heap object pointer. The heap is a private pointer
variable of PriorityQueue class. Now there is the destructor of the class, where we
One way is to put these N elements in an array and sort it. The k smallest of these
th
is at the k position.
It will take Nlog2N time ,in case we use array data structure. Now, we want to see if it
is possible to reduce the time from Nlog2N by using some other data structure or by
improving the algorithm? Yes, we can apply heap data structure to make this
operation more efficient.
A faster way is to put the N elements into an array and apply the buildHeap
algorithm on this array.
Finally, we perform k deleteMin operations. The last element extracted from the
heap is our answer.
The buildHeap algorithm is used to construct a heap of given N elements. If we
construct min-heap, the minimum of the N elements, will be positioned in the root
node of the heap. If we take out (deleteMin) k elements from the heap, we can get the
Kth smallest element. BuildHeap works in linear time to make a min or a max-heap.
The interesting case is k = N/2 as it is also is known as the median.
In Statistics, we take the average of numbers to find the minimum, maximum and
median. Median is defined as a number in the sequence where the half of the numbers
are greater than this number while the remaining half are smaller ones. Now, we can
come up with the mechanism to find median from a given N numbers. Suppose, we
want to compute the median of final marks of students of our class while the
maximum aggregate marks for a student are 100. We use the buildHeap to construct a
heap for N number of students. By calling deleteMin for N/2 times, the minimum
marks of the half number students will be taken out. The N/2th marks would be the
Page 393 of 519
Deleted: array,
Deleted: .
Deleted: Clearly then this new
implementation is more efficient because
the heap can readjusts itself in log2N
times. Gain in performance is the major
benefit of implementing PriorityQueue
using heap over implementing with array
and very important for you to remember
as a student of Data Structures.
There are other significant benefits of
using heap and there are number of other
uses of heap that would be covered in this
course time to time. At the moment, we
willsome common example usages of
heap in order to make you clear about
other uses of heap. The heap data
structure will be covered in the
Algorithms course also.
Heap Sort
To take the 100th minimum element out from the min-heap, we will call deleteMin to
take out 1st element, 2nd element, 3rd element. Eventually we will call deleteMin 100th
time to take out our required 100th minimum element. Suppose, if the size of the heap
is 100 elements, we have taken out all the elements out from the heap. Interestingly
the elements are sorted in ascending order. If somehow, we can store these numbers,
lets say in an array, all the elements sorted (in ascending order in this min-heap case)
can be had.
Hence,
If k = N, and we record the deleteMin elements as they come off the heap. We will
have essentially sorted the N elements.
Later in the course, we will fine-tune this idea to obtain a fast sorting algorithm
called heapsort.
We conclude our discussion on the heap here. However, it will be discussed in the
forthcoming courses. At the moment, let see another Abstract Data Type.
Key property: If Haaris is related to Saad and Saad is related to Ahmad, then
Haaris is related to Ahmad.
See the key property lines first part above Harris is related to Saad and Saad is
related to Ahmad. Suppose we have a program to handle this list of people and their
relationships. After providing all the names Harris, Saad and Ahmad to that
program and their relationships, the program might be able to determine the
remaining part Harris related to Ahmad.
``
Fig 33.1
This image is black and white, consisting of five non-overlapping black colored
regions of different shapes, called blobs. These blobs are two elliptics- n and u shaped
(two blobs) and one arc at the bottom. We can see these five blobs. How can robot
identify them? So the problem is:
We want to partition the pixels into disjoint sets, one set per blob.
If we make one set per blob, there will be five sets for the above image. To
understand the concept of disjoint sets, we can take an analogy with two sets- A and B
(as in Mathematics) where none of the elements inside set A is present in set B. The
sets A and B are called disjoint sets.
Another problem related to the Computer Vision is the image segmentation problem.
See the image below on the left of an old ship in gray scales.
Fig 33.2
We want to find regions of different colors in this picture e.g. all regions in the picture
of color black and all regions in the image of color gray. The image on the right
represents the resultant image.
Different scanning processes carried out in hospitals through MRI (Magnetic
Equivalence Relations
Lets discuss some Mathematics about sets. You might have realized that
Mathematics is handy whenever we perform analysis of some data structure.
A binary relation R over a set S is called an equivalence relation if it has
following propertiesReflexivity: for all element x S, x R x
2. Symmetry: for all elements x and y, x R y if and only if y R x
3. Transitivity: for all elements x, y and z, if x R y and y R z then x R z
The relation is related to is an equivalence relation over the set of people.
You are advised to read about equivalence relations yourself from your text books or
from the internet.
Data Structures
Lecture No. 34
Reading Material
Data Structures and Algorithm Analysis in C++
8.1, 8.2, 8.3
Chapter. 8
Summary
Equivalence Relations
Disjoint Sets
Dynamic Equivalence Problem
Equivalence Relations
We will continue discussion on the abstract data structure, disjointSets in this
lecture with special emphasis on the mathematical concept of Equivalence Relations.
You are aware of the rules and examples in this regard. Lets discuss it further and
define the concept of Equivalence Relations.
A binary relation R over a set S is called an equivalence relation if it has following
properties:
1. Reflexivity: for all element x S, x R x
2. Symmetry: for all elements x and y, x R y if and only if y R x
3. Transitivity: for all elements x, y and z, if x R y and y R z then x R z
The relation is related to is an equivalence relation over the set of people. This is an
example of Equivalence Relations. Now lets see how the relations among people
satisfy the conditions of Equivalence Relation. Consider the example of Haris, Saad
and Ahmed. Haris and Saad are related to each other as brother. Saad and Ahmed are
related to each other as cousin. Here Haris is related to Saad and Saad is related
to Ahmed. Lets see whether this binary relation is Equivalence Relation or not. This
can be ascertained by applying the above mentioned three rules.
First rule is reflexive i.e. for all element x S, x R x. Suppose that x is Haris so Haris
R Haris. This is true because everyone is related to each other. Second is Symmetry:
for all elements x and y, x R y if and only if y R x. Suppose that y is Saad. According
to the rule, Haris R Saad if and only if Saad R Haris. If two persons are related, the
relationship is symmetric i.e. if I am cousin of someone so is he. Therefore if Haris is
brother of Saad, then Saad is certainly the brother of Haris. The family relationship is
symmetric. This is not the symmetric in terms of respect but in terms of relationship.
The transitivity is: for all elements x, y and z. If x R y and y R z, then x R z. Suppose
x is Haris, y is Saad and z is Ahmed. If Haris is related to Saad, Saad is related to
Ahmed. We can deduce that Haris is related to Ahmed. This is also true in
Disjoint Sets
In the beginning of the lecture, it was told that equivalence relationship partitioned the
set. Suppose we have a set of elements and a relation which is also equivalence
relation. Lets see what it does mathematically to the set. An equivalence relation R
over a set S can be viewed as a partitioning of S into disjoint sets. Here we have an
equivalence relationship which satisfies the three conditions. Keep in mind the
examples of family relationships or electrical circuits. Here the second point is that
each set of the partition is called an equivalence class of R (all elements that are
related).
Consider the diagram below. We have a set in the shape of an ellipse.
This set has been partitioned into multiple sets. All these parts are disjoint sets and
belong to an equivalence class.
Page 399 of 519
S
a
a
d
A
h
m
e
d
O
m
a
r
A
s
i
m
Q
a
s
i
m
T
T
Now visit each cell of matrix. If two persons are related, store T(true) in the
corresponding column and row. As Haris and Saad are related to each other, so we
take the row of Haris and column of Saad and store T in that cell. Also take the row of
Saad and column of Ahmed and mark it with T. By now, we do not know that Haris
and Ahmed are related so the entry in this regard will be false. We cannot write
T(True) here as this relationship has not be stated explicitly. We can deduce that but
cannot write it in the matrix. Take all the pairs of relations and fill the matrix. As
Haris is related to Haris so we will write T in the Haris row and Haris column.
Similarly, the same can be done in the Saad row and Saad column and so on. This will
be square matrix as its rows and columns are equal. All of the diagonal entries are T
because a person is related to himself. This is a self relation. Can we find out that
Haris and Ahmed are related? We can find the answer of this question with the help
of two relations between Haris & Saad and Saad & Ahmed. Now we can write T in
the Haris row and Ahmed column. The problem is that this information was not given
initially but we deduced this from the given information. We have 1000 people so the
size of the matrix will be 1000 rows * 1000 columns. Suppose if we have 100,000
people, then the size of the array will be 100,000*100,000. If each entry requires one
byte then how much storage will be required to store the whole array? Suppose if we
have one million people, the size will be 10 to the power 14. Do we have as much
memory available in the computer? We have one solution but it is not efficient in
terms of memory. There is always need of efficient solution in terms of space and
time. For having a fast algorithm, we can compromise on the space. If we want to
conserve memory, a slow algorithm can be used. The algorithm we discussed is fast
a1 R a2,
a3 R a4,
a5 R a1,
a4 R a2,
We have five people in the example and their relation is given in four pairs. If two
persons are brothers, then cousin of one brother is also the cousin of other. We can
find out this information with the help of a matrix. We would like to be able to infer
this information quickly.
We made nodes of each element. These five nodes are as a1, a2, a3, a4, a5.
a1
a5
a2
a3
a4
As a1 R a2, so we link the nodes a1 and a2. Similarly a3 R a4, these two nodes are
also linked. Following this pattern, it is established that a5 R a1 and a4 R a2. So we
connect these nodes too. It is clear from the figure that all of these nodes are related to
each other. If they are related to each other as cousin, then all of these five persons
belong to the same family. They need to be in the same set. They are not in disjoint
sets. Is a3 R a5? You can easily say yes. How you get this information? This relation
is not provided in the given four relations. With the above tree or graph we can tell
that a3 is related to a5. We can get the information of relationship between different
persons using these nodes and may not need the matrix.
We want to get the information soon after the availability of the input data. So the
data is processed immediately. The input is initially a collection of n sets, each with
one element. Suppose if we have 1000 people, then there will be need of 1000 sets
Find returns the name of the set (equivalence class) that contains a given
element, i.e., Si = find(a)
Union merges two sets to create a new set Sk = Si U Sj.
We give an element to the find method and it returns the name of the set. The method
union groups the member of two sets into a new set. We will have these two
operations in the disjoint abstract data type. If we want to add the relation a R b, there
is need to see whether a and b are already related. Here a and b may be two persons
and a relation is given between them. First of all we will see that they are already
related or not. This is done by performing find on both a and b to check whether they
are in the same set or not. At first, we will send a to the find method and get the name
of its set before sending b to the find method. If the name of both sets is same, it
means that these two belong to the same set. If they are in the same set, there is a
relation between them. We did not get any useful information with this relation. Lets
again take the example of the Haris and Saad. We know that Haris is the brother of
Saad, so they can be placed into a new set. Afterwards, we get the information that
Saad is the brother of Haris.
Now the question arises, if they are not in the same set, what should we do? We will
not waste this information and apply union which merges the two sets a and b into a
new set. This information will be helpful later on. We keep on building the database
Data Structures
Lecture No. 35
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 8
8.2, 8.3
Summary
Before talking about the data structure implementation in detail, we will recap the
things discussed in the previous lecture. The concepts that came under discussion in
the last lecture included the implementation of the disjoint set. It was observed that in
case of data elements, we assign a unique number to each element to make sets from
these numbers. Moreover, techniques of merger and searching in these sets by using
the operations union and find were, talked about. It was seen that if we send a set item
or a number to find method, it tells us the set of which this item is a member. It will
return the name of the set. This name of set may also be a number. We talked about
the union method in which members of two sets join together to form a new set. The
union method returns the name or number of this new set.
Now we will discuss the data structure used to implement the disjoint sets, besides
ascertaining whether this data structure implements the find and union operations
efficiently.
Now we call the union function by passing it set 5 and set 7 as arguments i.e. we call
union (5,7). Here the sets 5 and 7 have two members each. 5 and 6 are the members
of 5 and similarly the two members of 7 are 7 and 8. After merging these sets, the
name of the new set will be 5 as stated earlier that the new set will be named after the
first argument. The following figure (figure 35.4) shows that now in the set 5, there
are four members i.e. 5, 6, 7 and 8.
So we conclude that it is not necessary that the caller should send the roots to union. It
is necessary that the union function should be such that if a caller sends elements of
two sets, it should find the sets of those elements, merge them and return the name of
new set. Thus our previous call i.e. union (4,5) was actually carried out in the way
that first the union finds the root of 4 that is 3. Then it looks for 5 that itself is a root
of its set. After this, it merges both the trees (sets). This merger is shown in the above
figure i.e. Figure 35.6.
Up to now, we have come to know that the formation of this tree is not like a binary
tree in which we go down ward. Moreover, a binary tree has left and right children
that are not seen yet in the tree we developed. This is a tree like structure with some
properties.
Parent[i] = -1
// if i is the root
Now we will keep these tree structures (forest) in an array in the same manner. With
the merger of the trees, the parents of the nodes will be changed. There may be nodes
that have no parent (we have seen this in the previous example). For such nodes, we
will keep 1 in the array. This shows that this node has no parent. Moreover, this
node will be a root that may be a parent of some other node.
Now we will develop the algorithm for find and union. Lets consider an example to
see the implementation of this disjoint set data structure with an array.
Parent Array
Initialization
We know that at the start we have n elements. These n elements may be the original
names of the elements or unique numbers assigned to them. Now we take an array and
with the help of a for loop, keep these n elements as root of each set in the array.
These numbers are used as the index of the array before storing 1 at each location
from index zero to n. We keep 1 to indicate a number as root. In code, this for loop
is written as under.
for ( i = 0; i < n ; i ++)
Parent [i] = -1 ;
Find ( i )
Now look at the following code of a loop. This loop is used
to find the parent of an element or the name of the set that
contains that element.
// traverse to the root (-1)
for(j=i; parent[j] >= 0; j=parent[j])
;
return j;
In
-1
-1
-1
-1
-1
-1
-1
-1
Union Operation
Now we come to the union operation. First of all, we call
union (5,6). This union operation forms an up-tree in
which 5 is up and 6 down to it. Moreover 6 is pointing to 5.
In the array, we put 5 instead of 1 at the position 6. This
shows that now the parent of 6 is 5. The other positions
have 1 that indicates that these numbers are the roots of
some tree. The only number, not a root now is 6. It is now
the child of 5 or in other words, its parent is 5 as shown in
the array in the following figure.
-1
-1
-1
-1
-1
-1
-1
Now we carry out the same process (i.e. union) with the
numbers 7 and 8 by calling union (7,8). The following
figure represents this operation. Here we can see that the
parent of 8 is 7 and 7 itself is still a root.
1
-1
-1
-1
-1
-1
-1
8
-1
-1
-1
-1
-1
8
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
Data Structures
Lecture No. 36
Reading Material
Data Structures and Algorithm Analysis in C++
8.4, 8.5, 8.6
Chapter. 8
Summary
Union by Size
Following are the salient characteristics of this method:
Maintain sizes (number of nodes) of all trees, and during union.
Make smaller tree, the subtree of the larger one.
Implementation: for each root node i, instead of setting parent[i] to -1, set it
to -k if tree rooted at i has k nodes.
This is also called union-by-weight.
We want to maintain the sizes of the trees as given in the first point. Until now, we
are not maintaining the size but only the parent node. If the value for parent in a node
is 1, then this indicates that the node is the root node. Consider the code of the find
operation again:
// find(i):
// traverse to the root (-1)
for(j=i; parent[j] >= 0; j=parent[j])
;
return j;
The terminating condition for loop is checking for non-negative values. At any point
when the value of parent[j] gets negative, the loop terminates. Note that the condition
does not specify exactly the negative number. It may be the number 1, 2 , 3 or
some other negative number that can cause the loop to be terminated. That means we
can also put the number of nodes (size) in the tree in this place. We can put the
number of nodes in a tree in the root node in the negative form. It means that if a tree
consists of six nodes, its root node will be containing 6 in its parent. Therefore, the
node can be identified as the root being the negative number and the magnitude (the
absolute value) of the number will be the size (the number of nodes) of the tree.
When the two trees would be combined for union operation, the tree with smaller size
will become part of the larger one. This will cause the reduction in tree size. That is
why it is called union-by-size or union-by-weight.
Lets see the pseudo-code of this union operation (quite close to C++ language). This
contains the logic for reducing tree size as discussed above:
-1
-1
-1
-1
-1
-1
-1
-1
These are eight nodes containing initially 1 in the parent indicating that each tree
Page 420 of 519
6
Union(4,6)
-1
-1
-1
-2
-1
-1
-1
Fig 36.2
From the figure, you can see the node 6 has come down to node 4. The array also
depicts that the parent at position 4 is containing 2. The number of nodes has
become 2. The position 6 is set to 4 indicating that parent of 6 is 4. Next, we similarly
perform the union operation on nodes 2 and 3.
After union(2,3)
-1
-2
-2
-1
-1
-1
Fig 36.3
Lets perform the union operation further and merge the trees 1 and 4.
union(1,4)
-2
-3
-1
-1
-1
Fig 36.4
1 was a single node tree (-1 in the array at position 1) and 4 was the root node of the
tree with two elements (-2 in the array at position 4) 4 and 6. As the tree with root 4 is
greater, therefore, node 1 will become part of it. Previously (when the union was not
based on weight), it happened contrary to it with second argument tree becoming the
part of the first tree.
In this case, the number of levels in the tree still remains the same (of two levels) as
that in the greater tree (with root 4). But we apply our previous logic i.e. the number
of levels of the trees would have been increased from two to three after the union.
Reducing the tree size was our goal of this approach.
In the next step, we merge the trees of 2 and 4. The size of the tree with root 2 is 2
(actually 2 in the parent array) while the size of the tree with root 4 is 3 (actually
3). So the tree of node 2 will be joined in the tree with root 4.
3
4
-5
-1
-1
-1
union(2,4)
Fig 36.5
The latest values in the array i.e. at position 4, have been updated to 5 (indicating the
size of the tree has reached 5). At position 2, the new value is 4, which indicates that
union(5,4)
-6
-1
-1
Fig 36.6
The updates inside the array can be seen as the value at position 4 has been changed
to 6 and new value at position 5 is 4. This is the tree obtained after applying the
union-by-size approach. Consider, if we had applied the previous method of union,
trees depth would have been increased more.
It seems that we have achieved our goal of reducing the tree size up to certain extent.
Remember, the benefit of reducing the tree height is to increase performance while
finding the elements inside the tree.
Union by Height
Alternative to union-by-size strategy: maintain heights,
During union, make a tree with smaller height a subtree of the other.
Details are left as an exercise.
This is an alternate way of union-by-size that we maintain heights of the trees and join
them based on their heights. The tree with smaller height will become part of the one
with greater height. This is quite similar with the union-by-size. In order to implement
Disjoint Set ADT, any of these solutions can work. Both the techniques i.e. union-bysize and union-by-height are equivalent, although, there are minor differences when
analyzed thoroughly.
Now, lets see what can we do for find operation to make it faster.
Sprucing up Find
So far we have tried to optimize union.
Can we optimize find?
Yes, it can be achieved by using path compression (or compaction).
Considering performance, we can optimize the find method. We have already
optimized the trees union operation and now we will see how can optimize the find
operation using the path compression.
During find(i), as we traverse the path from i to root, update parent entries for all
these nodes to the root.
This reduces the heights of all these nodes.
Pay now, and reap the benefits later!
Subsequent find may do less work.
To understand the statements above, lets see the updated code for find below:
find (i)
{
if (parent[i] < 0)
return i;
7
13
17
31
35
32
11
22
10
30
20
16
14
12
Find (1)
13
18
19
Fig 36.7
This tree is bigger as compared to the ones in our previous examples, it has been
constructed after performing some union operations on trees. Now we have called find
method with argument 1. We want to find the set with node 1. By following the
previous method of find, we will traverse the tree from 1 to 2, from 2 to 9, 9 to 4, 4 to
13 and finally from 13 to 7, which is the root node, containing the negative number.
So the find will return 7.
If we apply recursive mechanism given in the above code, the recursive call will start
from find(1). The parent (find(1)) will return 2. The recursive call for 2 will take the
control to node 9, then to 4, 13 and then eventually to 7. When find(7) is called, we
get the negative number and reach the recursion stopping condition. Afterwards, all
the recursive calls, on the stack are executed one by one. The call for 13, find(13)
returns 7. find(4) returns 7 because find(parent(4)) returns 7. That is why the link
between 4 and 13 has been dropped and a new link is established between 4 and 7 as
shown in the Fig 36.7. So the idea is to make the traversal path shorter from a node to
the root. The remaining recursive calls will also return 7 as the result of find
Page 425 of 519
7
13
17
22
31
32
10
11
20
30
16
14
12
Find (1)
35
13
Fig 36.8
19
18
Similarly the node 2 is directly connected to the root node and the interlink between
the node 2 and 9 is dropped.
7
13
17
31
35
32
11
22
10
30
20
16
14
12
Find (1)
13
18
19
Fig 36.9
7
13
17
32
10
11
20
30
16
14
12
Find (1)
35
22
31
13
Fig 36.10
19
18
7
13
17
31
35
32
11
22
10
30
20
16
14
12
F in d (1)
13
18
19
F ig 36.11
The union operation is based on size or weight but the reducing the in-between links
or path compression from nodes to the root is done by the find method. The find
d
Find(a)
Fig 36.12
If we have called find(a) then see the path from node a to the root node f. a is
connected to root node f through b, c, d and e nodes. Notice that there may be further
subtrees below these nodes. After we apply this logic of path compression for find
operation, we will have the tree as follows:
f
Find(a)
Fig 36.13
Data Structures
Lecture No. 37
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 8
Summary
Review
Image Segmentation
Maze Example
Pseudo Code of the Maze Generation
Review
In the last lecture, we talked about union and find methods with special reference to
the process of optimization in union. These methods were demonstrated by reducing
size of tree with the help of the techniques-union by size or union by weight.
Image Segmentation
In image segmentation, we will divide the image into different parts. An image may
be segmented with regard to the intensity of the pixels. We may have groups of pixels
having high intensity and those with pixels of low intensity. These pixels are divided
on the basis of their threshold value. The pixels of gray level less than 50 can be
combined in one group, followed by the pixels of gray level less between 50 and 100
in another group and so on. The pixels can be grouped on the basis of threshold for
difference in intensity of neighbors. There are eight neighbors of the pixel i.e. top,
bottom, left, right, top-left, top-right, bottom-left and bottom-right. Now we will see
the difference of the threshold of a pixel and its neighbors. Depending on the
difference value, we will group the pixels. The pixels can be grouped on the basis of
texture (i.e. a pattern of pixel intensities). You will study all these in detail in the
image processing subject.
Lets see an example. Consider the diagram below:
It seems a sketch or a square of black, gray and white portions. These small squares
represent a pixel or picture element. The color of these picture elements may be
white, gray or black. It has five rows and columns each. This is a 5 * 5 image. Now
we will have a matrix of five rows and five columns. We will assign the values to the
elements in the matrix depending on their color. For white color, we use 0, 4 for black
and 2 for gray color respectively.
0
1
2
3
4
0
0
2
4
4
0
1
0
0
2
4
2
2
0
4
2
0
2
3
4
4
4
4
4
4
4
0
4
4
0
When we get the image from a digital device, it is stored in numbers. In the above
matrix, we have numbers from 0 to 4. These can be between 0 and 255 or may be
more depending upon the digital capturing device. Now there is a raw digital image.
We will apply some scheme for segmentation. Suppose we need the pixels having
value 4 or more and the pixels with values less than 4 separately. After finding such
pixels, put 1 for those that have values more than 4 and put 0 for pixels having less
than 4 values. We want to make a binary array by applying the threshold. Lets apply
this scheme on it.
0
1
2
3
4
0
0
0
1
1
0
1
0
0
0
1
0
2
0
1
0
0
0
3 4
1 1
1 0
1 1
1 1
1 0
We have applied threshold equal to 4. We have replaced all 4s with 1 and all the
values less than 4, have been substituted by zero. Now the image has been converted
into a binary form. This may represent that at these points, blood is present or not.
The value 4 may represent that there is blood at this point and the value less than 4
represents that there is no blood. It is very easy to a program for this algorithm.
Our objective was to do image segmentation. Suppose we have a robot which
captures an image. The image obtained contains 0, 2 and 4 values. To know the
Page 431 of 519
0
1
2
3
4
1
0
0
0
1
0
2
0
1
0
0
0
3
1
1
1
1
1
4
1
0
1
1
0
0
0
1
1
1
0
1
0
0
1
1
1
2
0
1
1
0
1
3
1
1
1
1
1
4
1
0
1
1
0
We can apply the union scheme on it to find the region of 1s. Here we have a blob of
1.
0 1 2 3 4
0
1
1
1
0
0
0
1
1
1
0
1
1
0
1
1
1
1
1
1
1
0
1
1
0
The region is shaded. The image has been segmented. With the help of union/find
algorithm, we can very quickly segment the image. The union/find algorithm does not
require much storage. Initially, we have 25 sets that are stored in an array i.e. the uptree. We do all the processing in this array without requiring any extra memory. For
the image segmentation, disjoint sets and union-find algorithm are very useful. In the
image analysis course, you will actually apply these on the images.
Maze Example
You have seen the maze game in the newspapers. This is a puzzle game. The user
enters from one side and has to find the path to exit. Most of the paths lead to blind
alley, forcing the user to go back to square one.
This can be useful in robotics. If the robot is in some room, it finds its path between
the different things. If you are told to build mazes to be published in the newspaper, a
new maze has to be developed daily. How will you do that? Have you done like this
before? Did anyone told you to do like this? Now you have the knowledge of disjoint
sets and union-find algorithm. We will see that the maze generation is very simple
with the help of this algorithm.
Lets take a 5 * 5 grid and generate a 5 * 5 maze. A random maze generator can use
union-find algorithm. Random means that a new maze should be generated every
time.
Consider a 5x5 maze:
0
5
10
15
20
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Here we have 25 cells. Each cell is isolated by walls from the others. These cells are
numbered from 0 to 24. The line between 0 and 1 means that there is a wall between
them, inhibiting movement from 0 to 1. Similarly there is a wall between 0 and 5.
Take the cell 6, it has walls on 4 sides restricting to move from it to anywhere. The
internal cells have walls on all sides. We will remove these walls randomly to
establish a path from the first cell to the last cell.
Page 433 of 519
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Randomly remove walls until the entrance and exit cells are in the same set.
Removal of the wall is the same as doing a union operation.
Do not remove a randomly chosen wall if the cells it separates are already in
the same set.
We will take cells randomly. It means that the probability of each cell is equal. After
selecting a cell, we will choose one of its surrounding walls. The internal cells have
four walls around them. Now we will randomly choose one wall. In this way, we will
choose the neighboring cell. Initially we have 25 sets. Now after removing a wall
between 2 cells, we have combined these two cells into one set. Here the union
method has been applied to combine these two cells. If these cells are already in the
same set, we will do nothing.
We will keep on randomly choosing the cells and removing the walls. The cells will
merge together. The elements of the set will keep on growing and at some point, we
may have the entrance cell (cell 0) and the exit cell (cell 24) in the same set. When the
entrance cell and the exit cell are in the same set, it means that we have a set in which
the elements are related to each other and there is no wall between them. In such a
situation, we can move from start to the end going through some cells of this set. Now
we have at least one path from entrance to exit. There may be other paths in which we
have the entrance cell but not the exit. By following this path, you will not reach at
the exit as these paths take you to the dead end. As these are disjoint sets, so unless
the entrance and exit cell are in the same set, you will not be able to find the solution
of the maze.
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Apply the algorithm on it. We randomly choose a cell. Suppose we get cell 11. After
this, we randomly choose one of its walls. Suppose we get the right wall. Now we will
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
By now, each cell is in its own set. Therefore find(cell 11) will return set_11 and
find(cell 12) will return set_12. The wall between them is removed and union is
applied on them. Now we can move from cell 11 to cell 12 and vice versa due to the
symmetry condition of disjoint sets. We have created a new set ( set_11 = {11,12} )
that contains cell 11 and cell 12 and all other cells are in their own cells.
0
5
10
15
20
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Now we randomly choose the cell 6 and its bottom wall. Now the find(cell 6) will
return set_6. The find(cell 11) will return set_11 which contains cell 11 and cell 12.
The sets returned by the two find calls are different so the wall between them is
removed and union method applied on them. The set set_11 now contains three
elements cell 11, cell 12 and cell 6. These three cells are joined together and we can
move into these cells.
Now we randomly select the cell 8. This cell is not neighbor of set_11 elements. We
can randomly choose the cell 6 again and its bottom wall. However, they are already
in the same set so we will do nothing in that case. We randomly choose the cell 8 and
its top wall (cell 3). As these two cells are in different sets, the wall between them is
removed so that union method could be applied to combine them into a set (set_8 =
{8, 3}).
0
5
10
15
20
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Now we randomly choose the cell 14 and its top i.e. cell 9. Now we keep on
combining cells together but so far entrance and exit cells are not in the same set.
0
5
10
15
20
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
Page 436 of 519
1
6
11
16
21
2
7
12
17
22
3
8
13
18
23
4
9
14
19
24
If you keep on using this algorithm, the walls between cells will keep on vanishing,
resulting in the growth of set elements. At some point, the entrance and exit cell will
come into one set and there will be a path taking us from the start to exit. There may
be some other sets which do not contain the exit cell. Take it as a programming
exercise and do it. This is very interesting exercise. Find some information on the
internet regarding maze generation. In the next lecture, we will talk about the new
data type i.e. table.
Data Structures
Lecture No. 38
Summary
We will discuss the concepts of Tables and Dictionaries in this lecture with special
reference to the operations on Table ADT and the implementation.
Tables and Dictionaries
The table, an abstract data type, is a collection of rows and columns of information.
From rows and columns, we can understand that this is like a two dimensional array.
But it is not always a two dimensional array of same data type. Here in a table, the
Address
50 Zahoor Elahi Rd, Gulberg-4, Lahore
30-T Phase-IV, LCCHS, Lahore
131-D Model Town, Lahore
Phone
567-4567
517-2349
756-5678
Address
50 Zahoor Elahi Rd, Gulberg-4, Lahore
30-T Phase-IV, LCCHS, Lahore
131-D Model Town, Lahore
Phone
567-4567
517-2349
756-5678
Implementation of Table
Lets talk about why and how we should implement a table. Our choice for
implementation of the Table ADT depends on the answers to the following.
In a table for searching purposes, it is best to store the key and the entry separately
(even though the keys value may be inside the entry). Now, suppose we have a
record of a person Saleem whose address is 124 Hawkers Lane while the phone
number is 9675846. Similarly we have another record of a person Yunus. The
address and phone fields of this person are 1 Apple crescent and 622455
respectively. For these records in the table, we will have two parts. One part is the
complete entry while the other is the key in which we keep the unique item of the
entry. This unique item is twice in a record one as part of the entry and the second in a
field i.e. the key field. This key will be used for searching and deletion purposes of
records. With the help of key, we reach at the row and can get other fields of it. We
also call TableNode to row of the table. Its pictorial representation is given below.
key
Saleem
entry
Saleem, 124 Hawkers Lane, 9675846
TableNod
Yunus Yunus, 1 Apple Crescent, 0044 1970
Binary Search
The binary search is an algorithm of searching, used with the sorted data. As we have
sorted elements in the array, binary search method can be employed to find data in the
array. The binary search finds an element in the sorted array in log n time. If we have
100000 elements in the array, the log 1000000 will be 20 i.e. very small as compared
to 100000. Thus binary search is very fast.
The binary search is like looking up a phone number in the directory or looking up a
word in the dictionary. For looking a word in the dictionary, we start from the middle
in the dictionary. If the word that we are looking for comes before words on the page,
it shows that the word should be before this page. So we look in the first half.
Otherwise, we search for the word in the second half of the dictionary. Suppose the
word is in the first half of the dictionary, we consider first half for looking the word.
We have no need to look into the second half of the dictionary. Thus the data to be
searched becomes half in a step. Now we divide this portion into two halves and look
for the word. Here we again come to know that the word is in the first half or in the
second half of this portion. The same step is repeated with the part that contains the
required word. Finally, we come to the page where the required word exists. We see
that in the binary search, the data to be searched becomes half in each step. And we
find the entry very fast. The number of maximum steps needed to find an entry is log
n, where n is the total number of entries. Now if we have 100000 entries, the
maximum number of attempts (steps) required to find out the entry will be 20 (i.e. log
1000000).
Data Structures
Lecture No. 39
Reading Material
Data Structures and Algorithm Analysis in C++
10.4.2
Chapter. 10
Summary
Fig 39 1
The telephone directory is the quotable example to understand the way the binary
search method works.
In this lecture, we will focus on data structures for performing search operation.
Consider the data is present in an array as we discussed in the previous lecture. For
the first implementation, we supposed that the data is not sorted in the array. For
second implementation, we considered that the data inside the array is put in sorted
array. The advantage of the effort of putting the data in the array in sorted order pays
off when the searches on data items are performed.
Now, lets first see the algorithm (in pseudo code) for this operation below. It is
important to mention that this algorithm is independent of data type i.e. the data can
be of any type numeric or string.
if ( value == middle element )
value is found
else if ( value < middle element )
a:
low
10 13 17 19 27
4
mid
8
high
Fig 39.2
You can see an array a in the Fig 39.2 with indexes 0 to 8 and values 1, 5, 7, 9, 10, 13,
17, 19 and 27. These values are our data items. Notice that these are sorted in
ascending (increasing) order.
You can see in the first line of case 1 that val = 10, which indicates that we are
searching for value 10. From second line, the range of data to search is from 0 to 8. In
this data range, the middle position is calculated by using a simple formula (low +
high)/2. In this case, it is mid =(0+8)/2=4. This is the middle position of the data
array. See the array in the above figure Fig 39.2, which shows that the item at array
position 4 is 10, exactly the value we are searching for. So, in this case, we have
found the value right away in the middle position of the array. The search operation
can stop here and an appropriate value can be returned back.
Lets see the case 2 now:
a:
low
10 13 17 19 27
4
mid new
low
8
high
Fig 39.3
The second case is about the scenario when value (val) is greater than the middle
value (a[mid]). The range of data items (low and high) is the same as that in case 1.
Therefore, the middle position (mid) is also the same. But the value (val) 19 is greater
than the value at the middle (mid) 10 of the array. As this array is sorted, therefore,
the left half of the array must not contain value 19. At this point of time, our
information about val 19 is that it is greater than the middle. So it might be present in
the right half of the array. The right half part starts from position 5 to position 8. It is
shown in Fig 39.3 that the new low is at position 5. With these new low and high
positions, the algorithm is applied to this right half again.
Now, we are left with one more case.
a:
low
10 13 17 19 27
4
new mid
high
8
high
Fig 39.4
The value to be searched (val) in this case is 7. The data range is the same starting
from low=0 to high=8. Middle is computed in the same manner and the value at the
middle position (mid) is compared with the val. The val is less than the value at mid
position. As the data is sorted, therefore, a value lesser than the one at mid position
should be present in the lower half (left half) of the array (if it is there). The left half
of the array will start from the same starting position low=0 but the high position is
going to be changed to mid-1 i.e. 3. Now, lets execute this algorithm again on this
left half.
a:
a:
a:
10
13
17
19
27
10
13
17
19
27
10
13
17
19
27
F ig 3 9 .5
Firstly, we will compute the middle of 0 and 3 that results in 1 in this integer division.
This is shown in the top array in Fig 39.5. Now, the val 7 is compared with the value
at the middle position (mid) i.e.5. As 7 is greater than 5, we will process the right half
of this data range (which is positioned from low=0 to high=3). The right half of this
data range starts from position 2 and ends at position 3. The new data range is low=2
and high=3. The middle position (mid) is computed as (2+3)/2=2. The value at the
mid position is 7. We compare the value at mid position (7) to the val we are looking
for. These are found to be equal and finally we have the desired value.
Our desired number is found within positions- 0 to 8 at position 2. Without applying
this binary search algorithm, we might have performed lot more comparisons. You
might feel that finding this number 7 sequentially is easier as it is found at position 2
only. But what will happen in case we are searching for number 27. In that case, we
have to compare with each element present in the array to find out the desired
number. On the other hand, if this number 27 is searched with the help of the binary
search algorithm, it is found in third comparison.
Actually, we have already seen binary search while studying the binary search tree.
While comparing the number with the root element of the tree, we had come to know
that if the number was found smaller than the number in the root, we had to switch to
left-subtree of the root (ensuring that it cannot be found in the right subtree).
Now, lets see the C++ code for this algorithm:
First half
First half
Second half
Second half
First half
First half
Second half
First half
Fig 39.6
The search divides a list into two small sub-lists till a sub-list is no more divisible.
You might have realized about the good performance of binary trees by just looking
at these if you remember the fully balanced trees of N items discussed earlier.
entry
and so on
Fig 39.7
Well, linked list is one choice to implement table abstract data type. For unsorted
elements, the insertion at front operation will take constant time. (as each element is
inserted in one go). But if the data inside the list is kept in sorted order then to insert a
new element in the list, the entire linked list is traversed through to find the
appropriate position for the element.
For find operation, all the keys are scanned through whether they are sorted or
unsorted. That means the time of find is proportional to N.
tail
20
30
40
50
60
Fig 39.8
As shown in the figure. The head and tail are special nodes at start and end of the list
respectively. If we have to find number 60 in the list then we have no other choice but
starting from head traverse the subsequent nodes using the next pointer until the
required node is found or the tail is reached. To find 70 in the list, we will scan
through the whole list and then get to know that it is not present in it. The professor
Pugh suggested something here:
head
20
30
40
tail
50
60
Fig 39.9
Firstly, we use two pointers head and tail. Secondly, the node in the middle has two
next pointers; one is the old linked list pointer leading to next node 50 and the second
is leading to the tail node. Additionally the head node also has two pointers, one is the
old liked list pointer pointing to the next node 20 and second one is pointing to the
middle elements next pointer, which is (as told above) further pointing to the tail
node.
Now, if we want to find element 60 in the above list. Is it possible to search the list in
relatively quick manner than the normal linked list shown in Fig 39.8? Yes, it is with
the help of the additional pointers we have placed in the list. We will come to the
middle of the list first and see that the middle element (40) is smaller than 60,
therefore the right half part of the list is selected to process further (as the linked list is
sorted). Isnt it the same we did in binary search? It definitely is.
What if we can add additional pointers (links) and boost the performance. See the
figure below.
tail
20
26
30
40
50
57
60
Fig 39.10
Data Structures
Lecture No. 40
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 10
10.4.2
Summary
Skip List
Skip List Search
Insertion in Skip List
Deletion from Skip List
In the previous lecture, we had started the discussion on the concept of the skip lists.
We came across a number of definitions and saw how the use of additional pointers
was effective in the list structures. It was evident from the discussion that a
programmer prefers to keep the data in the linked list sorted. If the data is not sorted,
we cannot use binary search algorithm. And the insert, find and remove methods are
proportional to n. But in this case, we want to use the binary search on linked list. For
this purpose, skip list can be used. In the skip list, there is no condition of the upper
limit of the array.
Skip List
A skip list for a set S of distinct (key, element) items is a series of lists S0, S1 , , Sh
such that
Each list Si contains the special keys + and -
List S0 contains the keys of S in non-decreasing order
Each list is a subsequence of the previous one, i.e.,
S0 S 1 S h
List Sh contains only the two special keys
Now lets see an example for the skip list. First of all, we have S0 i.e. a linked list.
We did not show the arrows in the list in the figure. The first and last nodes of S0
contain - and + respectively. In the computer, we can put these values by
max(int) and max(int). The values can also be used about which we are sure that these
will not be in the data items. However, for the sake of discussion to show these
values, the - and + are the best notations. We can see that - is less than any value
of data item while + is greater than any value of data item. If we insert any value
much ever, it is large the + will be greater than it. Moreover, we see that the
numbers in S0 are in the non- decreasing order. This S0 is the first list in which all
keys are present.
Now we will take some nodes from this list and link them. That will be not every
other node or every fourth node. It may happen this way. However, we will try that
Page 454 of 519
S3
S2
S1
S0
31
23
12
23
26
31
34
31
34
64
56
64
78
Figure 40.1:
Now we have the list i.e. from S0 to S3. Actually, these list are made during insert
operation. We will see the insert method later. Lets first talk about the search
method.
x = y: we return element(after(p))
Page 455 of 519
To search a key x, we start at the first position of the top list. For example, we are
discussing the top list is S3. We note the current position with p. We get the key in the
list after the current list (i.e. in which we are currently looking for key) by the key
(after(p)) function. We get this key as y. Then we compare the key to be searched for
i.e. x with this y. If x is equal to y then it means y is the element that we are searching
for so we return element(after(p)). If x is greater than y, we scan forward and look at
the next node. If x is less than y, we drop down and look in the down lists. No if we
drop down and past the bottom list, it means that the element (item) is not there and
we return NO_SUCH_KEY.
To see the working of this search strategy lets apply it on the skip list, already
discussed and shown in the figure 40.1. This figure shows four lists. Remember that
these four lists are actually in the one skip list and are made by the additional pointers
in the same skip list. There is not such situation thatS1 is developed by extracting the
data from S0 and S1 duplicates this data. Actually every node exists once and is
pointed by additional pointers. For example, the node 23 exists once but has two next
pointers. One is pointing to 26 while the other pointing to 31. In the same way, there
are three pointers in node 31, two are to the node 34 and the third is toward the +.
S3
S2
S1
S0
31
23
12
23
26
31
34
31
34
64
56
64
78
Suppose we want to search 78. We start from the first node of the top list i.e. S3. The
78 will be the current node and we denote it with p. Now we look at the value in the
node after p. In the figure, it is +. Now as the + is greater than 78, we drop down
to S2. Note in the figure that we drop down vertically. We dont go to the first
element p in the down list. This process of going down will be discussed later. Now
we drop from S3 to S2. This is our current pointer. Now we look for the value in the
next node to it. We see that this value is 31. Now 31 is less than 78, so we will do
scan forward. The next node is + that is obviously greater than 78. So we drop from
here and go to the list S1. In this list, the current position is 34. We compare 78 with
Page 456 of 519
If i > h, we add to the skip list new lists Sh+1, , Si +1, each containing only
the two special keys
Here we compare i (that is the count of heads came up) with h (that is the number of
list) if i is greater than or equal to h then we add new lists to the skip list. These new
lists are Sh+1, , Si +1. Suppose if i is 8 and h is 4 , we add additional lists S5, S6,
S7, S8 and S9. These lists initially will contain the only two special keys that means and +. The next steps are:
We search for x in the skip list and find the positions p0, p1 , , pi of the
items with largest key less than x in each list S0, S1, , Si
For j 0, , i, we insert item (x, o) into list Sj after position pj
S2
S1
S0
23
10
23
36
Figure 40.3:
Now we proceed with this list and insert a value in it. The value that we are going to
insert is 15. We have tossed the coin and figured out that the value of i is 2. As it is
randomized algorithm so in this case, the value of i has become 2. The value of I is
the count number of heads before the tail comes up by tossing the coin. From the
above figure, we see that the value of h is 2. As h and i are equal, we are not adding
S3, S4 and S5 to the list. Rather, we will apply the search algorithm. We start from
the left most node of the top list. We call this position p2. We label these positions for
their identification. We see that the item being added i.e.15 is less than the +, so we
drop down to list S1. We denote this step with p1. In this list, the next value is 23 that
is also greater than 15. So we again drop down and come in the list S0. Our current
position is still the first node in the list as we did not have any scan forward. Now the
next node is 10 that is less than 15. So we skip forward. We note this skip forward
with p0. Now after p0 the value in next node is 23 that is greater than 15. As we are in
the bottom list, there is no more drop down. So here is the position of the new node.
This position of new node is after the node 10 and before the node 23. We have
labeled the positions po, p1 and p2 to reach there. Now we add the value 15
additionally to the list that we have traversed to find the positions and labeled them to
remember. After this we add a list S3 that contains only two keys that are - and +,
as we have to keep such a list at the top of the skip list. The following figure shows
this insertion process.
S2
15
S1
15
23
S0
15
23
10
36
S3
S2
S1
S0
12
34
23
34
23
34
45
S2
S1
S0
23
12
23
45
Data Structures
Lecture No. 41
Reading Material
Data Structures and Algorithm Analysis in C++
10.4.2
Chapter. 10
Summary
Review
Quad Node
Performance of Skip Lists
AVL Tree
Hashing
Examples of Hashing
Review
In the previous lecture, we studied three methods of skip list i.e. insert, find and
remove and had their pictorial view. Working of these methods was also discussed.
With the help of sketches, you must have some idea about the implementation of the
extra pointer in the skip list.
Lets discuss its implementation. The skip list is as under:
S
1
2
3
4
2
3
3
4
2
3
3
4
4
5
We have some nodes in this skip list. The data is present at 0, 1st and 2nd levels. The
actual values are 12, 23, 34 and 45. The node 34 is present in three nodes. It is not
necessary that we want to do the same in implementation. We need a structure with
next pointers. Should we copy the data in the same way or not? Lets have a look at
the previous example:
tail
Tower Node
20
26
30
40
50
57
60
Here, the data is 20, 26, 30, 40, 50, 57, 60. At the lowest level, we have a link list. A
view of the node 26, node 40 and node 57 reveals that there is an extra next pointer.
The head pointer is pointing to a node from where three pointers are pointing at
different nodes.
We have seen the implementation of link list. At the time of implementation, there is
a data field and a next pointer in it. In case of doubly link list, we have a previous
pointer too. If we add an extra pointer in the node, the above structure can be
obtained. It is not necessary that every node contains maximum pointers. For
example, in the case of node 26 and node 57, there are two next pointers and the node
40 has three next pointers. We will name this node as TowerNode.
TowerNode will have an array of next pointers. With the help of this array of pointers,
a node can have multiple pointers. Actual number of next pointers will be decided by
the random procedure. We also need to define MAXLEVEL as an upper limit on
number of levels in a node. Now we will see when this node is created. A node is
created at a time of calling the insert method to insert some data in the list. At that
occasion, a programmer flips the coin till the time he gets a tail. The number of heads
represents the levels. Suppose we want to insert some data and there are heads for six
times. Now you know how much next pointers are needed to insert which data. Now
we will create a listNode from the TowerNode factory. We will ask the factory to
allocate the place for six next pointers dynamically. Keep in mind that the next is an
array for which we will allocate the memory dynamically. This is done just due to the
fact that we may require different number of next pointers at different times. So at the
time of creation, the factory will take care of this thing. When we get this node from
the factory, it has six next pointers. We will insert the new data item in it. Then in a
loop, we will point all these next pointers to next nodes. We have already studied it in
the separate lecture on insert method.
If your random number generation is not truly so and it gives only the heads. In this
case, we may have a very big number of heads and the Tower will be too big, leading
to memory allocation problem. Therefore, there is need to impose some upper limit on
it. For this purpose, we use MAXLEVEL. It is the upper limit for the number of next
pointers. We may generate some error in our program if this upper limit is crossed.
The next pointers of a node will point at their own level. Consider the above figure.
Suppose we want to insert node 40 in it. Its 0 level pointer is pointing to node 50. The
2nd pointer is pointing to the node 57 while the third pointer pointing to tail. This is
the case when we use TowerNode. This is one of the solutions of this problem.
Quad Node
Lets review another method for this solution, called Quad node. In this method, we
do not have the array of pointers. Rather, there are four next pointers. The following
details can help us understand it properly.
A quad-node stores:
item
link to the node before
link to the node after
link to the node below
link to the node above
This will require copying of the key (item) at different levels. We do not have an
array of next pointers in it. So different ways are adopted to create a multilevel node
of skip list. While requiring six levels, we will have to create six such nodes and copy
the data item x in all of these nodes and insert these in link list structure. The
following figure depicts it well.
S3
S2
S1
S0
31
23
12
23
26
31
34
31
34
64
44
56
64
78
You can see next and previous and down and up pointers here. In the bottom layer, the
AVL Tree
Page 465 of 519
key
entry
entry
key
key
entry
entry
Hashing
The hashing is an algorithmic procedure and a methodology. It is not a new data
structure. It is a way to use the existing data structure. The methods- find, insert and
remove of table will get of constant time. You will see that we will be able to do this
in a single step. What is its advantage? If we need table data structure in some
program, it can be used easily due to being very efficient. Moreover, its operations are
of constant time. In the recent lectures, we were talking about the algorithms and
procedures rather than data structure. Now we will discuss about the strategies and
Page 466 of 519
array
index
hash
function
Key
We have a key that may be a name, or roll number or login name etc. We will pass
this key to a hash function. This is a mathematical function that will return an array
index. In other words, an integer number will be returned. This number will be in
some range but not in a sequence.
Keys and entries are scattered throughout the array. Suppose we want to insert the
data of our employee. The key is the name of the employee. We will pass this key to
the hash function which will return an integer. We will use this number as array
index. We will insert the data of the employee at that index.
key
entry
10
123
Examples of Hashing
Lets see some examples of hashing and hash functions. With the help of these
examples you will easily understand the working of find, insert and remove methods.
Suppose we want to store some data. We have a list of some fruits. The names of
fruits are in string. The key is the name of the fruit. We will pass it to the hash
function to get the hash key.
Suppose our hash function gave us the following values:
HashCode ("apple")
=
hashCode ("watermelon") =
hashCode ("grapes")
=
hashCode ("cantaloupe")
=
hashCode ("kiwi")
=
hashCode ("strawberry")
=
hashCode ("mango")
=
hashCode ("banana")
=
5
3
8
7
0
9
6
2
Our hash function name is hashCode. We pass it to the string apple. Resultantly, it
returns a number 5. In case of watermelon we get the number 3. In case of grapes
there is number 8 and so on. Neither we are sending the names of the fruits in some
order to the function, nor is function returning the numbers in some order. It seems
that some random numbers are returned. We have an array to store these strings. Our
array will look like as:
0
1
2
banana
watermelon
4
5
apple
mango
cantaloupe
grapes
strawberry
We store the data depending on the indices got from the hashCode. The array size is
10. In case of apple, we get the index 5 from hashCode so apple is stored at array
index 5. As we are dealing with strings, so the array will be an array of strings. The
watermelon is at position 3 and so on every element is at its position. This array
will be in the private part of our data structure and the user will not know about it. If
our array is table then it will look like as under:
table[5]
table[3]
table[8]
table[7]
table[0]
table[9]
table[6]
table[2]
=
=
=
=
=
=
=
=
"apple"
"watermelon"
"grapes"
"cantaloupe"
"kiwi"
"strawberry"
"mango"
"banana"
We will store our data in the Table array using the string copy. The user is storing the
data using the names of the fruits and wants to retrieve or delete the data using the
names of fruits. We have used the array for storage purposes but did not store the data
consecutively. We store the data using the hash function which provides us the array
index. You can note that there are gaps in the array positions.
Similarly we will retrieve the data using the names of fruit and pass it to the
hashCode to get the index. Then we will retrieve the data at that position. Consider
the table array, it seems that we are using the names of fruits as indices.
table["apple"]
table["watermelon"]
table["grapes"]
table["cantaloupe"]
table["kiwi"]
Page 469 of 519
lengt 1
=
h( str ) str[i ] %TableSize
i =0
length 1
i
h( str ) str[i] b %T
i =0
Summary
Collision
Linear Probing
In the previous lecture, we were discussing about the hash functions. The hash
algorithm depends on the hash function. The hash function generates the array index
to enable us to insert data in the table array. We have seen two examples of hash
functions in the previous lecture. Both the functions use the ASCII values of
characters to generate the index. Here the question arises how can we implement the
hash function in case of having integer data? We may have employee ID, user ID or
student ID as integers. Here we may take mod with some number or table size and the
result is used as an array index.
If the keys are integers then key%T is generally a good hash function unless the data
has some undesirable features. If we want to store the employee record, user record or
student record in the table, this can be done through hash function. We take the mod
of the value with the T. The value of T may be 10, 15 or 100 depending on the
requirements. There may be some problem. For example, if T = 10 and all keys end in
zeros, then key%T = 0 for all keys. The hash function gives 0 for all the keys, used as
array index. Now it is a problem. We cannot store our values as all the records have
same index i.e. 0. In general, to avoid such situations, T should be a prime number.
Internally, we have to store the data in the array and there is complete freedom to use
this array by taking the size of our own choice.
We have to store some data in the array. As the array is private, we will decide about
its size on our own. We will take the size of the array in prime numbers. To store 100
records, we will take prime number near 100. We will select this prime number as
MAXTABLESIZE. Then we will use this number in our hash function. This will help
resolve the problem arising due to situation where all keys end with 0. Using the
prime number, the values from the hash function will not be 0 for all the keys.
With the help of prime number, we cannot solve this problem completely. Similarly, it
cannot be made sure that the values from the hash function are unique for all the keys.
Sometimes, we may have same index for two different keys. This phenomenon is
known as collision i.e. the hash values are same of two different keys. How can we
solve this collision problem?
Collision
Collision takes place when two or more keys (data items) produce the same index.
Lets see the previous example of storing the names of fruits. Suppose our hash
function gives us the following values:
= 5
= 3
8
= 7
0
= 9
6
2
kiwi
1
2
banana
watermelon
4
5
apple
mango
cantaloupe
grapes
strawberry
We store these data items in the respective index in the array. In the above example,
the index given by the hash function does not collide with any other entry. Suppose
we want to add another fruit honeydew in it. When honeydew is passed to the
hash function, we get the value 6 i.e.
hash("honeydew") = 6
This is not the responsibility of the hash function to see the data in the array before
generating the index. Hash function is generally a mathematical formula that takes the
keys and returns a number. It is responsibility of the caller to find its solution. We
have already mango at position 6. The user of our ADT calls the insert function
giving the value honeydew. We call the hash function to find out its index that
comes out to be 6. Now the problem is this that position 6 is already occupied. What
should we do to avoid it?
There are a lot of solutions of this problem. These solutions can be divided into two
main categories. One type of solutions is the changing of the hash function. Even with
the introduction of a new function, it is not guaranteed that there will be no collision
with future data. Second option is that we live with the collision and do something to
resolve it.
The definition of collision is:
When two values hash to the same array location, this is called a collision
We cannot say that the usage of this hash function will not result in collision
especially when the data is changing. Collisions are normally treated as a
phenomenon of first come, first served, the first value that hashes to the location
gets it. We have to find something to do with the second and subsequent values that
Page 474 of 519
Linear Probing
Page 475 of 519
robin
143
sparrow
144
hawk
145
seagull
146
147
148
...
bluejay
owl
robin
143
sparrow
144
hawk
145
seagull
146
147
148
...
bluejay
owl
We know that hawk is already in the table. Also seagull is added in the table. We will
see that that data that we want to insert already exists or not. First of all, we call the
hash function that is hashCode as:
hashCode(hawk) = 143
key entry
key entry
4
key entry
key entry
10
key entry
123
On the left side, we have vertical array that contains the pointers in it. When we insert
the first item, we attach a list node. When we have collision, the new item is inserted
in the start of the link list. Suppose an item is stored at position 4 and we have another
data, requiring the position 4. So there is a collision and we will insert the data at the
start of the list.
The problem in linear probing is that when our array is full what we should do. This
problem can be solved using the link list.
In the next lecture, we will continue our discussion on hashing. We will see an
animation. Hashing is very important methodology and can be used in data structures
besides Table and Dictionary.
Data Structures
Lecture No. 43
Reading Material
Data Structures and Algorithm Analysis in C++
5.4, 5.5, 5.6, 7.1
Chapter. 5, 7
Summary
Hashing Animation
Applications of Hashing
When Hashing is Suitable?
Sorting
Elementary Selection Algorithms
Selection Sort
Hashing Animation
In the previous lecture, we discussed about collision strategies in hashing. We studied
Applications of Hashing
Lets see few examples of those applications where hashing is highly useful. The
hashing can be applied in table ADT or you can apply hashing using your array to
store and retrieve data.
Compilers use hash tables to keep track of declared variables (symbol table).
Compilers use hash tables in order to implement symbol tables. A symbol table is an
important part of compilation process. Compiler puts variables inside symbol table
during this process. Compiler has to keep track of different attributes of variables. The
name of the variable, its type, scope and function name where it is declared etc is put
into the symbol table. If you consider the operations on symbol table, those can be
insertion of variable information, search of a variable information or deletion of a
variable. Two of these insert and find are mostly used operations. You might have
understood already that a variable name will be parameter (or the key) to these
operations. But there is one slight problem that if you named a variable as x outside of
a code block and inside that code block, you declared another variable of the similar
type and name then only name cannot be the key and scope is the only differentiating
factor. Supposing that all the variables inside the program have unique names,
variable name can be used as the key. Compiler will insert the variable information by
calling the insert function and by passing in the variable information. It retrieves the
variable value by passing in the variable name. Well, this exercise is related to your
Compiler Construction course where you will construct you own language compiler.
Another usage of hashing is given below:
A hash table can be used for on-line spelling checkers if misspelling detection
Page 483 of 519
Sorting
Sorting means to put the data in a certain order or sequence. We have discussed
sorting before in different scattered through topics in this course but it has not been
discussed so far as a separate topic. You must be remembering that when we traverse
the binary search tree in in-order way, the obtained data happens to be sorted.
Similarly, we saw other data structures, where we used to keep data in sorted order. In
case of min-heap if we keep on removing elements one by one, we get data in sorted
order.
Sorting is so useful that in 80-90% of computer applications, sorting is there in one
form or the other. Normally, sorting and searching go together. Lot of research has
been done on sorting; you can get lot of stuff on it from different sources. Very
efficient algorithms have already been developed for it. Moreover, a vast
Sorting Integers
How to sort integers in this array?
20
10
10 20
Fig 43.1
We want to sort the numbers given in the above array. Apparently, this operation may
seem very simple. But think about it, if you are given a very large volume of data
(may be million of numbers) then you may realize that there has to be an efficient
mechanism to perform this operation. Firstly, lets put the problem in words:
We have a very large array of numbers. We want to sort the numbers inside the array
in ascending order such that the minimum number of the array will be the first
element of it and the largest element will be the last element at the end of the array.
Lets go to the algorithms of sorting. Point to be noted here that we are going to study
algorithms of sorting; we are not talking about data structures. Until now, you might
have realized that algorithms go along data structures. We use a data structure to
contain data and we use algorithms to perform certain operations or actions on that
data.
Selection Sort
Main idea:
o find the smallest element
o put it in the first position
Page 486 of 519
Data Structures
Lecture No. 44
Reading Material
Data Structures and Algorithm Analysis in C++
Chapter. 7
7.1, 7.2
Summary
Selection Sort
o Selection Sort analysis
Insertion Sort
o Insertion Sort Analysis
Bubble Sort
o Bubble Sort analysis
Summary
N log2 (N) Algorithms
This is the sequel of the previous lecture in which we talked about the sort algorithms.
Page 487 of 519
Selection Sort
Suppose we have an array with different numbers. For sorting it in an ascending
order, selection sorting will be applied on it in the following manner. We find the
smallest number in the array and bring it to the position 1. We do the same process
with the remaining part of the array and bring the smallest number among the
remaining array to the next position. This process continues till the time all the
positions of the array are filled with the elements. Thus the main idea of selection sort
is that
find the smallest element
put it in the first position
find the next smallest element in the remaining elements
put it in the second position
And so on, until we get to the end of the array
Lets understand this algorithm by considering an example with the help of figures.
Consider an array that has four numbers i.e. 19, 5, 7 and 12 respectively. Now we
want to sort this array in ascending order. To sort the array, selection algorithm will
be applied on it.
19
12
19
12
19
12
12
19
12
19
20
10
20
10
20
10
10
20
10
20
Insertion Sort
Page 491 of 519
19
12
12
19
12
19
12
19
Bubble Sort
The third sorting algorithm is bubble sort. The basic idea of this algorithm is that we
bring the smaller elements upward in the array step by step and as a result, the larger
elements go downward. If we think about array as a vertical one, we do bubble sort.
The smaller elements come upward and the larger elements go downward in the array.
Thus it seems a bubbling phenomenon. Due to this bubbling nature, this is called the
bubble sort. Thus the basic idea is that the lighter bubbles (smaller numbers) rise to
the top. This is for the sorting in ascending order. We can do this in the reverse order
for the descending order.
The steps in the bubble sort can be described as below
Exchange neighboring items until the largest item reaches the end of
the array
Repeat the above step for the rest of the array
In this sort algorithm, we do not search the array for the smallest number like in the
other two algorithms. Also we do not insert the element by shifting the other
elements. In this algorithm, we do pair-wise swapping. We will take first the elements
and swap the smaller with the larger number. Then we do the swap between the next
pair. By repeating this process, the larger number will be going to the end of the array
Page 494 of 519
a:
a:
a:
a:
19
12
19
12
12
19
12
19
12
19
a:
a:
a:
a:
a:
12
19
12
19
12
19
12
19
12
19
First of all, we compare the first pair i.e. 19 and 5. As 5 is less than 19, we swap these
elements. Now 5 is at its place and we take the next pair. This pair is 19, 12 and not
12, 7. In this pair 12 is less than 19, we swap 12 and 19. After this, the next pair is 19,
7. Here 7 is less than 19 so we swap it. Now 7 is at its place as compared to 19 but it
is not at its final position. The element 19 is at its final position. Now we repeat the
pair wise swapping on the array from index 0 to 2 as the value at index 3 is at its
position. So we compare 5 and 12. As 5 is less than 12 so it is at its place (that is
before 12) and we need not to swap them. Now we take the next pair that is 12 and 7.
In this pair, 7 is less than 12 so we swap these elements. Now 7 is at its position with
respect to the pair 12 and 7. Thus we have sorted the array up to index 2 as 12 is now
at its final position. The element 19 is already at its final position. Note that here in
the bubble sort, we are not using additional storage (array). Rather, we are replacing
the elements in the same array. Thus bubble sort is also an in place algorithm. Now as
index 2 and 3 have their final values, we do the swap process up to the index 1. Here,
the first pair is 5 and 7 and in this pair, we need no swapping as 5 is less than 7 and is
at its position (i.e. before 7). Thus 7 is also at its final position and the array is sorted.
Following is the code of bubble sort algorithm in C++.
void bubbleSort(int *arr, int N)
{
int i, temp, bound = N-1;
int swapped = 1;
while (swapped > 0 )
Summary
Now considering the above three algorithms, we see that these algorithms are easy to
understand. Coding for these algorithms is also easy. These three algorithms are in
place algorithms. There is no need of extra storage for sorting an array by these
Page 496 of 519
N2
10
100
100
10000
1000
1000000
10000
100000000
100000 10000000000
1000000
1E+12
N Log2 (N)
33.21
664.38
9965.78
132877.12
1660964.04
19931568.57
From this table we can see that for a particular value of N, the value of N2 is very
large as compared to the value of N log2 (N). Thus we see that the algorithms whose
time complexity is proportional to N2 are much time consuming as compared to the
algorithms the time complexity of which is proportional to N log2 (N). Thus we see
that the N log2 (N) algorithms are better than the N2 algorithms.
Now lets see the algorithms that are N log2 (N) algorithms. These include the
following algorithms.
Merge Sort
Quick Sort
Heap Sort
These three algorithms fall under divide and conquer category. The divide and
conquer strategy is well known in wars. The philosophy of this strategy is , divide
your enemy into parts and then conquer these parts. To conquer these parts is easy,
as these parts cannot resist or react like a big united enemy. The same philosophy is
applied in the above algorithms. To understand the divide and conquer strategy in
sorting algorithm, lets consider an example. Suppose we have an unsorted array of
numbers is given below.
10
12
11
Now we split this array into two parts shown in the following figure.
10
12
11
10
12
11
After this we merge these two parts and get the sorted array as shown below.
2
10
11
12
Data Structures
Lecture No. 45
Reading Material
Data Structures and Algorithm Analysis in C++
7.6, 7.7
Chapter. 7
Summary
10
12
11
11
11
S o rt th e tw o p a rts
10
12
M e rg e th e tw o p a rts to g e th e r
10
12
F ig 4 5 .1
Let see few analysis to confirm the usefulness of the divide and conquer technique.
To sort the halves approximate time is (n/2)2+(n/2)2
To merge the two halves approximate time is n
So, for n=100, divide and conquer takes approximately:
= (100/2)2 + (100/2)2 + 100
= 2500 + 2500 + 100
(n2 = 10,000)
= 5100
We know that elementary three sorting algorithms were taking approximately n2 time.
Suppose we are using insertion sort of those elementary algorithms. We divide the list
into two halves then the time will be approximately (n/2)2+(n/2)2. The time required
for merging operation is approximately n. This operation contains a simple loop that
goes to n.
Suppose that n is 100. Considering if we apply insertion sort algorithm on it then the
time taken will be approximately (100)2 = 10000. Now, if we apply divide and
conquer technique on it. Then for first half approximate time will be (100/2)2.
Similarly for second half it will be (100/2)2. The merging approximate time will be
100. So the whole operation of sorting using this divide and conquer technique in
insertion sort will take around (100/2)2 + (100/2)2+100 = 5100. Clearly the time
spent (5100) after applying divide and conquer mechanism is significantly lesser than
Page 499 of 519
So we stop subdividing the list when we reach to the single element level. This divide
and conquer strategy is not a thing, we have already prepared binary search tree on
the same lines. One side of the tree contains the greater elements than the root and
other part contains the smaller elements. Especially, when performing binary search
in an array, we had started our search from mid of it. Subdivided the array and kept on
comparing and dividing the array until we got success or failure. The subdivision
process may prolong to individual element of the array.
Search
Search
Fig 45.2
Hence, we used to perform binary search on the same lines of divide and conquer
strategy. Remember, we applied binary search on sorted array. From this one can
realize that sorting facilitates in searching operations.
Sort
Sort
Sort
Sort
Sort
Sort
Sort
Fig 45.3
Combine
Combine
Combine
Fig 45.4
In Fig 45.4, we have four sorted smaller parts. We combine them to become two
sorted parts and two sorted parts are further combined or merged to become one
sorted list.
Mergesort
Mergesort is a divide and conquer algorithm that does exactly that.
It splits the list in half
Mergesorts the two halves
Then merges the two sorted halves together
Mergesort can be implemented recursively
Lets see the mergsort algorithm, how does that work.
The mergesort algorithm involves three steps:
o If the number of items to sort is 0 or 1, return
o Recursively sort the first and second halves separately
o Merge the two sorted halves into a sorted groupIf the data is consisting
of 0 or 1 element then there is nothing required to be done further. If the number of
elements is greater than 1 then apply divide and conquer strategy in order to sort
them. Divide the list into two halves and sort them separately using recursion. After
the halves (subparts) have been sorted, merge them together to get a list of sorted
elements.
We will discuss recursive sorting in a moment, before that lets see the merging
operation using pictures. We have two sorted array and another empty array whose
size is equal to the sum of sizes of two sorted arrays.
10
12
11
2
Fig 45.5
You can see from Fig 45.5, array 1 on the top left is containing 4 elements, top right is
also containing 4 elements and both of them are sorted internally. Third array is
containing 4+4 = 8 elements.
Initially, very first elements (present at the starting index 0 of array) of both the arrays
are compared and the smaller of them is placed in the initial position of the third
array. You can see from Fig 45.5 that elements 4 and 2 are compared pointed to by
the indexes (actually arrays current indexes). The smaller of them is 2, therefore, it is
placed in the initial position in the third array. A pointer (the current index of array) is
also shown for third array that will move forward as the array is filled in. The smaller
number was from right array, therefore, its pointer is moved forward one position as
shown in Fig 45.6.
10
12
11
Fig 45.6
This time the numbers at current positions in both the arrays are compared. As 4 is
smaller of the two numbers, therefore, it is put in the third array and third arrays
pointer is moved one position forward. Also because this time, the number has been
chosen from left array, therefore, its pointer is also moved forward. The updated
figure is shown in Fig 45.7.
10
12
11
Fig 45.7
Next, numbers 8 and 5 are compared. As 5 is smaller of the two, it is put in the third
array. The changed positions of pointers and the next comparison are shown in Fig
45.8.
10
12
11
Fig 45.8
Fig 45.9 has showed the situation after next comparison done in the similar manner.
10
12
11
Fig 45.9
By now, you must have understood how the merging operation works. Remember, we
do merging when we have two parts sorted already.
Consider this operation in terms of time and complexity. It is performed using a
simple loop that manipulates three arrays. An index is used for first array, which starts
from the initial position to the size of the array. Similar an index is used for second
array, which starts from the initial position and ends at the end of the array. A third
index is used for third array that sizes to the sum of the maximum values of both
previous indexes. This is simple single loop operation, which is not complex.
10
10
10
12
11
12
10
4
Fig 45.10
At the top of Fig 45.10 is an array, which is not sorted. In order to sort it using
recursive mechanism, we divide the array into two parts logically. Only logical
partitions of the array are required bases on size and index of the arrays. The left half
of the array containing 10,4,8 and 12 is processed further for sorting. So actually,
while calling the mergesort algorithm recursively, only that particular half of the array
is a passed as an argument. This is spitted further into two parts; 10, 4 and 8, 12. 10, 4
half is processed further by calling the mergesort recursively and we get both
numbers 10 and 4 as separate halves. Now these halves are numbers and they cannot
be subdivided therefore recursive call to mergesort will stop here. These numbers
(new halves) 10 and 4 are sorted individually, so they are merged. When they are
merged, they become as 4,10 as shown in Fig 45.11.
10
12
10
12
11
10
12
12
Fig 45.11
The recursive call to mergesort has done its work for left half that consisted of 10 and
4. Now, we apply the same technique (the recursive calls to mergesort) to the right
half that was consisting of 8 and 12. 8 and 12 are spitted into separate numbers as
indicated in Fig 45.11. Further division of them is not possible, therefore, they are
merged as shown in Fig 45.12.
10
12
10
12
11
10
12
Fig 45.12
At this point in time, we have two parts one is containing elements as 4, 10 and other
as 8, 12. These two parts are merged. The merged half is shown in Fig 45.12. Note
that it is completely sorted as 4,8,10 and 12. This completes the operation on the left
half of the array. We do similar process with the right half now, which is consisting of
11, 2, 7 and 5.
10
10
12
12
11
11
11
11
Fig 45.13
Similarly, left half of it 11,2 is processed recursively. It is divided into two halves and
we have two parts as two individual numbers. The recursive call for this part stops
here and the merging process starts. When the parts 11 and 2 are merged, they
become one sorted part as shown in Fig 45.14.
Mergesort the right half.
10
10
12
12
11
11
11
Fig 45.14
Now the same procedure is applied on right half consisting of 7 and 5. By applying
recursive mechanism, it is further subdivided into two individual number parts.
10
10
12
11
11
12
11
Fig 45.15
The merging operation starts, the resultant is shown in the Fig 45.16. After merging,
the new part has become 5,7.
Mergesort the right half.
10
10
12
12
11
11
11
Fig 45.16
When 5,7 merged part is further merged with already merged part 2,11. The new half
becomes as 2,5,7,and 11 as shown in Fig 45.17.
10
10
12
12
11
11
Fig 45.17
Now, we can merge the two biggest halves to get the array in sorted order. After
merging, the resulted sorted array is shown in Fig 45.18.
Merge the two halves.
10
11
12
Fig 45.18
Lets see the C++ code for this sorting algorithm. We are not going to write a class of
it but at the moment, we are only writing it as a procedure. We can use it standalone
or later on, we can also change it to make it a class member.
void mergeSort(float array[], int size)
{
int * tmpArrayPtr = new int[size];
if (tmpArrayPtr != NULL)
mergeSortRec(array, size, tmpArrayPtr);
else
{
cout << Not enough memory to sort list.\n);
return;
}
delete [] tmpArrayPtr;
}
void mergeSortRec(int array[], int size, int tmp[])
{
int i;
int mid = size/2;
if (size > 1)
{
mergeSortRec(array, mid, tmp);
mergeSortRec(array+mid, size-mid, tmp);
Page 508 of 519
mergeArrays
Lets see the merging of two array while using their indexes as i and j.
a:
15
28
30
aSize: 5
b:
10
14
22
43
50
bSize: 6
tmp:
Fig 45.19
We are doing the same operation of merging of arrays but with little more detail here.
One the left is an array a consisting of 2,5,15,28 and 30 and on the right is another
array b consisting of 6,10,14,22,43 and 50. The size of array a is 6 while bs size is 6.
We create a temporary array named tmp, whose size is 11. We have used indexes i, j
and k for arrays a, b and tmp respectively and each one of them has been initialized
with 0.
a:
15
28
30
b:
i=0
10
14
22
43
50
j=0
tmp:
k=0
Fig 45.20
We compare the initial elements 3 and 6 of the arrays a and b. As 3 is smaller than 6,
therefore, it is put in the temporary array tmps starting location where k=0.
a:
15
28
30
b:
i=0
10
14
22
43
50
j=0
tmp:
3
k=0
Fig 45.21
i is incremented and now elements 5 and 6 are compared. 5 being smaller of these
take the place in the tmp array as shown in Fig 45.22.
a:
15
28
30
i=1
b:
10
14
22
43
50
j=0
tmp:
3
k=1
Fig 45.22
i is incremented again and it reaches the element 15. Now, elements 15 and 6 are
compared. Obviously 6 is smaller than 15, therefore, 6 will take the place in the tmp
array as shown in Fig 45.23.
a:
15
28
30
b:
i=2
10
14
22
43
50
j=0
tmp:
k=2
Fig 45.23
Because this time the element was taken from right array with index j, therefore, this
time index j will be incremented. Keep an eye on the index k that is increasing after
every iteration. So this time, the comparison is made between 15 and 10. 10 being
smaller will take the place in tmp array as shown in Fig 45.24.
a:
15
28
30
b:
i=2
10
14
22
43
50
j=1
tmp:
k=3
10
Fig 45.24
Again, j will be incremented because last element was chosen from this array.
Elements 15 and 14 are compared. Because 14 smaller, so it is moved to tmp array in
the next position.
a:
15
28
30
b:
i=2
10
14
22
43
50
j=2
tmp:
3
k=4
10
14
Fig 45.25
With these movements, the value of k is increasing after each iteration. Next, 22 is
compared with 15, 15 being smaller of two is put into the tmp array as shown in Fig
45.26.
a: 3
15
28
30
b:
i=2
10
14
22
43
50
j=3
tmp:
k=5
10
14
15
Fig 45.26
By now , you must have understood how this is working, lets see the next iterations
pictures below:
a: 3
15
28
30
b:
i=3
k=6
15
10
14
15
22
14
22
43
50
b:
10
14
22
43
50
22
43
50
Fig 45.27
28
30
i=3
j=4
tmp:
k=7
a: 3
10
j=3
tmp:
a: 3
15
10
14
15
22
28
b:
10
14
28
30
Fig 45.28
28
30
i=4
j=4
tmp:
3
k=8
10
14
15
22
Fig 45.29
Note that array a is all used up now, so the comparison operation will end here. All
the remaining elements of array b are incorporated in tmp array straightaway as
Page 512 of 519
a: 3
15
28
30
b:
i=5
10
14
22
43
50
j=4
Done.
tmp:
k=9
10
14
15
22
28
30
43
50
Fig 45.30
Sort
Sort
M erge
Fig 45.31
Mergesort Analysis
As you have seen, we used an additional temporary array while performing the
merging operation. At the end of the merging operation, elements are copied from
temporary array to the original array. The merge sort algorithm is not an inplace
sorting algorithm because it requires an additional temporary array of the same size as
the size of the array under process for sorting. This algorithm is still a good sorting
algorithm, we see this fact from its analysis.
O(n)
O(n)
.
.
.
O(log2n)
times
O(n)
Fig 45.32
Mergesort is O(n log2 n)
Space?
The other sorts we have looked at (insertion, selection) are in-place (only require
a constant amount of extra space)
Mergesort requires O(n) extra space for merging
As shown in Fig 45.32, the array has been divided into two halves. We know that
merging operation time is proportional to n, as it is done in a single loop regardless of
the number of equal parts of the original array. We also know that this dividing the
array into halves is similar mechanism as we do in a binary tree and a complete or
perfect balance tree has log2n number of levels. Because of these two factors, the
merge sort algorithm is called nlog2n algorithm. Now, lets discuss about quick sort
algorithm, which is not only an nlog2n algorithm but also an inplace algorithm. As
you might have guessed, we dont need an additional array while using this algorithm.
Quicksort
Quicksort is another divide and conquer algorithm.
Quicksort is based on the idea of partitioning (splitting) the list around a pivot or
split value.
Quicksort is also a divide and conquer algorithm. We see pictorially, how the quick
sort algorithm works. Suppose we have an array as shown in the figure Fig 45.33.
4
12
10
11
5
pivot value
Fig 45.33
We select an element from the array and call it the pivot. In this array, the pivot is the
middle element 5 of the array. Now, we swap this with the last element 3 of the array.
The updated figure of the array is shown in Fig 45.34.
4
12
10
11
low
high
5
pivot value
Fig 45.34
As shown in Fig 45.34, we used two indexes low and high. The index low is started
from 0th position of the array and goes towards right until n-1th position. Inside this
loop, an element that is bigger than the pivot is searched. The low index is
incremented further as 4 is less than 5.
4
12
10
low
11
high
5
pivot value
Fig 45.35
low is pointing to element 12 and it is stopped here as 12 is greater than 5. Now, we
start from the other end, the high index is moved towards left from n-1th position to 0.
12
10
low
11
high
5
pivot value
Fig 45.36
Both of the indexes have been stopped, low is stopped at a number 12 that is greater
than the pivot and high is stopped at number 2 that is smaller than the pivot. In the
next step, we swap both of these elements as shown in Fig 45.37.
4
10
low
12
11
high
5
pivot value
Fig 45.37
Note that our pivot element 5 is still there at its original position. We again go to
index low and start moving towards right, trying to find a number that is greater than
the pivot element 5. It immediately finds the next number 10 greater than 5. Similarly,
the high is moved towards left in search to find an element smaller than the pivot
element 5. The very next element 3 is smaller than 5, therefore, the high index stops
here. These elements 10 and 3 are swapped, the latest situation is shown in Fig 45.38.
4
low
10
12
11
high
5
pivot value
Fig 45.38
Now, in the next iteration both low and high indexes cross each other. When the high
10
12
11
12
11
high low
5
pivot value
Fig 45.39
10
high low
Fig 45.40
This array is not sorted yet but element 5 has found its destination. The numbers on
the left of 5 should be smaller than 5 and on right should be greater than it and we can
see in Fig 45.40 that this actually is the case here. Notice that smaller numbers are on
left and greater numbers are on right of 5 but they are not sorted internally.
Next, we recursively quick sort the left and right parts to get the whole array sorted.
4
42
10
6
12
11
Fig 45.41
Now, we see the C++ code of quick sort.
void quickSort(int array[], int size)
{
int index;
if (size > 1)
{
index = partition(array, size);
quickSort(array, index);
quickSort(array+index+1, size - index-1);
}
}
Course Overview
We had started this course while keeping the objectives of data structures in mind that
appropriate data structures are applied in different applications in order to make them
work efficiently. Secondly, the applications use data structures that are not memory
hungry. In the initial stages, we discussed array data structure. After we found one
significant drawback of arrays; their fixed size, we switched our focus to linked list
and different other data structures. However, in the meantime, we started realizing the
significance of algorithms; without them data structures are not really useful rather I
should say complete.
We also studied stack and queue data structures. We implemented them with array
and linked list data structures. With their help, we wrote such applications which
seemed difficult apparently. I hope, by now, you must have understood the role of
stack in computers runtime environment. Also Queue data structure was found very
helpful in Simulations.
Later on, we also came across the situations when we started thinking that linear data
structures had to be tailored in order to achieve our goals with our work, we started
studying trees then. Binary tree was found specifically very useful. It also had a
degenerate example, when we constructed AVL trees in order to balance the binary
search tree. We also formed threaded binary trees. We studied union/find data
structure, which was an up tree. At that time, we had already started putting special
Abutt
5/26/2003 8:59:00 PM
therefore, the first line inside the insert function is checking if the heap has gone full. If
the heap is not full then the if-block is entered. At line 33 inside the if-block, an element
that is passed as a parameter to insert method of heap is inserted into the queue by calling
the insert method of heap. This insert call will internally perform percolate up and down
operations to place the new element at its correct position. The returned value 1 indicates
the successful insertion in the queue. If the heap has gone full then a message is
displayed that insert queue is full. Note that we have used the word queue not heap in
that message. It has to that way otherwise the users of the PriorityQueue class are
unaware of the internal representation of the queue (whether it is implemented using a
heap or an array). Next line is returning 0indicating that the insert operation was not
successful. Below to insert, we have full() method. This method returns an int value.
Internally, it is calling isFull() method of heap. The full() method returns whatever is
returned by the isFull() method of heap. Next is length() method, as the size of heap is
also the size of the queue, therefore, length() is internally calling the getSize() method of
heap.
In this new implementation the code is better readable than the PriorityQueues
implementation with array. You must be remembering when we implemented the
PriorityQueue with an array, at each insertion in the
Page 393: [2] Deleted
Abutt
5/26/2003 8:59:00 PM
Clearly then this new implementation is more efficient because the heap can readjusts
itself in log2N times. Gain in performance is the major benefit of implementing
PriorityQueue using heap over implementing with array and very important for you to
remember as a student of Data Structures.
There are other significant benefits of using heap and there are number of other uses of
heap that would be covered in this course time to time. At the moment, we willsome
common example usages of heap in order to make you clear about other uses of heap.
The heap data structure will be covered in the Algorithms course also.
One way is to put these N elements in an array and sort it. The k smallest of these is
th
at the k position.
It will take Nlog2N time in case we use array data structure. Now, we want to see if it is
possible to reduce the time from Nlog2N by using some other data structure or by
improving the algorithm? Yes, we can apply heap data structure in order to make this
operation more efficient.
A faster way is to put the N elements into an array and apply the buildHeap algorithm
on this array.
Finally, we perform k deleteMin operations. The last element extracted from the heap
is our answer.
The buildHeap algorithm is used to construct a heap of given N elements. If we
constructed min-heap, the minimum of the N elements will be positioned in the root
node of the heap. If we take out (deleteMin) k elements from the heap, we can get the kth
smallest element. BuildHeap works in linear time to make a min or a max-heap.
The interesting case is k = N/2, since this is known as the median.
In Statistics, we take the average of numbers, find the minimum, maximum and median
as well. Median is defined as a number in the sequence where the half of the numbers are
greater than this number and half of the numbers are smaller. Now, we can come up with
our mechanism to find median from a given N numbers. Lets say , we want to compute
the median of final marks of students of our class. Further suppose that the maximum
aggregate marks for a student are 100. We use the buildHeap to construct a heap for N
number of students. By calling deleteMin for N/2 times, the minimum marks of the half
number students will be taken out. The N/2th marks would be the median of the marks of
our class. The alternates methods are there to calculate median, however, we are
discussing here about the possible uses of heap. Lets see another use of heap.
Heap Sort
As discussed above in The Selection Problem that to take the 100th minimum element out
from the min-heap, we will call deleteMin to take out 1st element, 2nd element, 3rd
element and eventually we will call deleteMin 100th time to take out our required 100th
minimum element. Suppose, if the size of the heap is 100 elements then we have taken
out all the elements out from the heap. Interestingly the elements are sorted in increasing
order. If somehow, we can store these taken out numbers, lets say in an array we can
have all the elements sorted (in ascending order in this min-heap case).
Hence,
If k = N, and we record the deleteMin elements as they come off the heap, we will
have essentially sorted the N elements.
Later in the course, we will refine this idea to obtain a fast sorting algorithm called
heapsort.
We finish our discussion above heap here but it will be discussed in the forthcoming
courses and a little bit when we will discuss about sort in the next lectures of this course.
At the moment, let see another Abstract Data Type.
there is a software working internal to robots body that is doing all this controlling part
and vision to the robot. This is very complex problem and broad area of Computer
Science and Electrical Engineering called Robotics (Computer Vision in particular).
Consider the image below:
``
Fig 33.1
This image is black and white, which consist of five non-overlapping black colored
regions of different shapes called blobs. These blobs are two elliptic, n and u shaped (two
blobs) and one arc at the bottom. From human eye we can see these five blobs but how a
robot can identify these. So the problem is:
We want to partition the pixels into disjoint sets, one set per blob.
Now if we make one set per blob then for five blobs in the image above, we have to have
five sets. To understand the concept of disjoint sets, we can take an analogy where we
have two sets A and B (as in Mathematics) where none of the elements inside set A is
present in set B. The sets A and B are called disjoint sets.
Another problem relatedthe Computer Vision is the image segmentation problem. See the
image below on the left of an old ship in gray scales.
Fig 33.2
We want to find regions of different colors in this picture, for example all regions in the
picture of color black, all regions in the image of color gray. The image on the right
represents the resultant image.
Different scanning done in hospitals mri scan, cat scan or ct scan scans the inner parts of
the whole body of humans. Those scans images in gray scales represent organs of the
human body. All these are application of Disjoint Set ADT.