Data Structures and Algorithms Notes
Data Structures and Algorithms Notes
Course Outcome: -
To have a thorough understanding of the basic structure and operation of a digital computer.
To discussindetailtheoperationofthearithmeticunitincludingthealgorithms&
implementationoffixed-pointandfloating-pointaddition,subtraction,multiplication& division.
To study the different ways of communicating with I/O devices and standard I/O interfaces.
To study the hierarchical memory system including cache memories and virtual memory
Course Goal
To understand basic structure of a digital computer.
To Perform arithmetic operations of binary number system.
To Know the organization of the control unit, arithmetic and logical unit, memory unit and
theI/Ounit.
Syllabus
Unit 2- Linked Structure: List representation, Polish notations, operations on linked list get
node and free node operation, implementing the list operation, inserting into an ordered
linked list, deleting, circular linked list, doubly linked list.
Unit 3-Tree Structure: Concept and terminology, Types of trees, Binary search tree,
inserting, deleting and searching into binary search tree, implementing the insert, search and
delete algorithms, tree traversals, Huffman's algorithm.
2. A.V. Aho., J.E. Hopcroft, and J.D. Ullman, Data Structures and Algorithms.
4. Sara Baase and Allen Van Gelder: Computer Algorithms, Pearson Education Asia
2
CONTENTS
3
UNIT 1 Introduction to Algorithm Design & Linear
Structure
Structure:
1.0 Learning Objectives
1.1 Algorithm and Its characteristics
1.2 Efficiency of algorithms
1.3 Analyzing Algorithms and problems
1.4 Linear Structure: Arrays, records
1.5 Stack, operation on stack
1.6 Implementation of stack as an array
1.7 Queue, types of queues
1.8 Operations on queue
1.9 Implementation of queue.
1.10 Unit End Questions
4
1.0 Learning Objectives
Categories of Algorithm:
Based on the different types of steps in an Algorithm, it can be divided into
three categories, namely
Sequence
Selection and
Iteration
Sequence: The steps described in an algorithm are performed successively one
by one without skipping any step. The sequence of steps defined in an
algorithm should be simple and easy to understand. Each instruction of such
an algorithm is executed, because no selection procedure or conditional
branching exists in a sequence algorithm.
Example:
// adding two numbers
Step 1: start
Step 2: read a,b
Step 3: Sum=a+b
Step 4: write Sum
Step 5: stop
Selection: The sequence type of algorithms are not sufficient to solve the
problems, which involves decision and conditions. In order to solve the
5
problem which involve decision making or option selection, we go for
Selection type of algorithm. The general format of Selection type of statement
is as shown below:
if(condition)
Statement-1;
else
Statement-2;
The above syntax specifies that if the condition is true, statement-1 will be
executed otherwise statement-2 will be executed. In case the operation is
unsuccessful. Then sequence of algorithm should be changed/ corrected in
such a way that the system will re- execute until the operation is successful.
Iteration: Iteration type algorithms are used in solving the problems which
involves repetition of statement. In this type of algorithms, a particular
number of statements are repeated ‘n’ no. of times.
Example1:
Step 1 : start
Step 2 : read n
Step 3 : repeat step 4 until n>0
Step 4 : (a) r=n mod 10
(b) s=s+r
(c) n=n/10
Step 5 : write s
Step 6 : stop
6
ii. Space Complexity:
The amount of space occupied by an algorithm is known as Space Complexity.
An algorithm is said to be efficient if it occupies less space and required the
minimum amount of time to complete its execution.
2. Write an algorithm to find the largest among three different numbers entered by
user
Step 1: Start
Step 2: Declare
variables a,b and c.
Step 3: Read
variables a,b and c.
Step 4: If a>b
If a>c
Display a is the
largest number.
Else
Display c is the largest number.
Else
If b>c
Display b is the
largest number.
Else
Display c is the greatest number.
Step 5: Stop
4. Write an algorithm to find the Simple Interest for given Time and Rate of Interest .
Step 1: Start
Step 2: Read P,R,S,T.
Step 3:
Calculate
S=(PTR)/100
Step 4: Print S
Step 5: Stop
ASYMPTOTIC NOTATIONS
Asymptotic analysis of an algorithm refers to defining the mathematical
boundation/framing of its run-time performance. Using asymptotic analysis,
we can very well conclude the best case, average case, and worst case
scenario of an algorithm.
Asymptotic analysis is input bound i.e., if there's no input to the algorithm, it
is concluded to work in a constant time. Other than the "input" all other
factors are considered constant.
Asymptotic analysis refers to computing the running time of any operation in
mathematical units of computation. For example, the running time of one
operation is computed as f(n) and may be for another operation it is
computed as g(n2). This means the first operation running time will increase
linearly with the increase in n and the running time of the second operation
will increase exponentially when n increases. Similarly, the running time of
both operations will be nearly the same if n is significantly small.
The time required by an algorithm falls under three types −
Best Case − Minimum time required for program execution.
Average Case − Average time required for program execution.
Worst Case − Maximum time required for program execution.
Asymptotic Notations
Following are the commonly used asymptotic notations to calculate the
running timecomplexity of an algorithm.
Ο Notation
Ω Notation
θ Notation
Big Oh Notation, Ο
The notation Ο(n) is the formal way to express the upper bound of an
algorithm's running time. It measures the worst case time complexity or the
longest amount of time an algorithm can possibly take to complete.
8
For example, for a function f(n)
Ο(f(n)) = { g(n) : there exists c > 0 and n0 such that f(n) ≤ c.g(n) for all n > n0. }
Omega Notation, Ω
The notation Ω(n) is the formal way to express the lower bound of an
algorithm's running time. It measures the best case time complexity or the
best amount of time an algorithm can possibly take to complete.
θ(f(n)) = { g(n) if and only if g(n) = Ο(f(n)) and g(n) = Ω(f(n)) for all n > n0. }
9
1.3 Analysing Algorithms and problems
Best case
Worst case
Average case
Best case: Define the input for which algorithm takes less time or minimum time. In the best case
calculate the lower bound of an algorithm. Example: In the linear search when search data is present
at the first location of large data then the best case occurs.
Worst Case: Define the input for which algorithm takes a long time or maximum time. In the worst
calculate the upper bound of an algorithm. Example: In the linear search when search data is not
present at all then the worst case occurs.
Average case: In the average case take all random inputs and calculate the computation time for all
inputs.
And then we divide it by the total number of inputs.
Average case = all random case time / total no of case
1.4 Linear Structure: Arrays, records
Linear data structure: Data structure in which data elements are arranged sequentially or
linearly, where each element is attached to its previous and next adjacent elements is called a
linear data structure.
Examples of linear data structures are array, stack, queue, linked list, etc.
10
Arrays:
An array is a linear data structure and it is a collection of items stored at contiguous
memory locations. The idea is to store multiple items of the same type together in one
place. It allows the processing of a large amount of data in a relatively short period. The
first element of the array is indexed by a subscript of 0. There are different operations
possible in an array, like Searching, Sorting, Inserting, Traversing, Reversing, and
Deleting.
Array
Characteristics of an Array:
An array has various characteristics which are as follows:
Arrays use an index-based data structure which helps to identify each of the elements in
an array easily using the index.
If a user wants to store multiple values of the same data type, then the array can be
utilized efficiently.
An array can also handle complex data structures by storing data in a two-
dimensional array.
An array is also used to implement other data structures like Stacks, Queues, Heaps,
Hash tables, etc.
The search process in an array can be done very easily.
11
Applications of Array:
Different applications of an array are as follows:
An array is used in solving matrix problems.
Database records are also implemented by an array.
It helps in implementing a sorting algorithm.
It is also used to implement other data structures like Stacks, Queues, Heaps, Hash
tables, etc.
An array can be used for CPU scheduling.
Can be applied as a lookup table in computers.
Arrays can be used in speech processing where every speech signal is an array.
The screen of the computer is also displayed by an array. Here we use a
multidimensional array.
The array is used in many management systems like a library, students, parliament,
etc.
The array is used in the online ticket booking system. Contacts on a cell phone are
displayed by this array.
In games like online chess, where the player can store his past moves as well as
current moves. It indicates a hint of position.
Arrays provide O(1) random access lookup time. That means, accessing the 1st index of the
array and the 1000th index of the array will both take the same time. This is due to the fact
that array comes with a pointer and an offset value. The pointer points to the right location of
the memory and the offset value shows how far to look in the said memory.
array_name[index]
| |
Pointer Offset
Therefore, in an array with 6 elements, to access the 1st element, array is pointed towards the
0th index. Similarly, to access the 6th element, array is pointed towards the 5th index.
Array Representation
Arrays are represented as a collection of buckets where each bucket stores one element.
These buckets are indexed from ‘0’ to ‘n-1’, where n is the size of that particular array. For
example, an array with size 10 will have buckets indexed from 0 to 9.
12
This indexing will be similar for the multidimensional arrays as well. If it is a 2-dimensional
array, it will have sub-buckets in each bucket. Then it will be indexed as array_name[m][n],
where m and n are the sizes of each level in the array.
As per the above illustration, following are the important points to be considered.
The basic operations in the Arrays are insertion, deletion, searching, display, traverse, and
update. These operations are usually performed to either modify the data in the array or to
report the status of the array.
In C, when an array is initialized with size, then it assigns defaults values to its elements in
following order.
13
Data Type Default Value
bool False
char 0
int 0
float 0.0
double 0.0f
void
wchar_t 0
Insertion Operation
In the insertion operation, we are adding one or more elements to the array. Based on the
requirement, a new element can be added at the beginning, end, or any given index of array.
This is done using input statements of the programming languages.
Algorithm
Following is an algorithm to insert elements into a Linear Array until we reach the end of the
array −
1. Start
2. Create an Array of a desired datatype and size.
3. Initialize a variable ‘i’ as 0.
4. Enter the element at ith index of the array.
5. Increment i by 1.
6. Repeat Steps 4 & 5 until the end of the array.
7. Stop
Example
#include <stdio.h>
int main(){
int LA[3] = {}, i;
printf("Array Before Insertion:\n");
for(i = 0; i < 3; i++)
printf("LA[%d] = %d \n", i, LA[i]);
printf("Inserting Elements.. \n");
14
printf("The array elements after insertion :\n"); // prints array values
for(i = 0; i < 3; i++) {
LA[i] = i + 2;
printf("LA[%d] = %d \n", i, LA[i]);
}
return 0;
}
Output
Array Before Insertion:
LA[0] = 0
LA[1] = 0
LA[2] = 0
Inserting Elements..
The array elements after insertion :
LA[0] = 2
LA[1] = 3
LA[2] = 4
Deletion Operation
In this array operation, we delete an element from the particular index of an array. This
deletion operation takes place as we assign the value in the consequent index to the current
index.
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such that K<=N.
Following is the algorithm to delete an element available at the Kth position of LA.
1. Start
2. Set J = K
3. Repeat steps 4 and 5 while J < N
4. Set LA[J] = LA[J + 1]
5. Set J = J+1
6. Set N = N-1
7. Stop
Example
15
#include <stdio.h>
void main(){
int LA[] = {1,3,5};
int n = 3;
int i;
printf("The original array elements are :\n");
for(i = 0; i<n; i++)
printf("LA[%d] = %d \n", i, LA[i]);
for(i = 1; i<n; i++) {
LA[i] = LA[i+1];
n = n - 1;
}
printf("The array elements after deletion :\n");
for(i = 0; i<n; i++)
printf("LA[%d] = %d \n", i, LA[i]);
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
The array elements after deletion :
LA[0] = 1
LA[1] = 5
Search Operation
Searching an element in the array using a key; The key element sequentially compares every
value in the array to check if the key is present in the array or not.
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such that K<=N.
Following is the algorithm to find an element with a value of ITEM using sequential search.
1. Start
2. Set J = 0
3. Repeat steps 4 and 5 while J < N
4. IF LA[J] is equal ITEM THEN GOTO STEP 6
5. Set J = J +1
6. PRINT J, ITEM
7. Stop
16
Example
#include <stdio.h>
void main(){
int LA[] = {1,3,5,7,8};
int item = 5, n = 5;
int i = 0, j = 0;
printf("The original array elements are :\n");
for(i = 0; i<n; i++) {
printf("LA[%d] = %d \n", i, LA[i]);
}
for(i = 0; i<n; i++) {
if( LA[i] == item ) {
printf("Found element %d at position %d\n", item, i+1);
}
}
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Found element 5 at position 3
Traversal Operation
This operation traverses through all the elements of an array. We use loop statements to carry
this out.
Algorithm
Following is the algorithm to traverse through all the elements present in a Linear Array −
1 Start
2. Initialize an Array of certain size and datatype.
3. Initialize another variable ‘i’ with 0.
4. Print the ith value in the array and increment i.
5. Repeat Step 4 until the end of the array is reached.
6. End
17
Example
#include <stdio.h>
int main(){
int LA[] = {1,3,5,7,8};
int item = 10, k = 3, n = 5;
int i = 0, j = n;
printf("The original array elements are :\n");
for(i = 0; i<n; i++) {
printf("LA[%d] = %d \n", i, LA[i]);
}
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Update Operation
Update operation refers to updating an existing element from the array at a given index.
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such that K<=N.
Following is the algorithm to update an element available at the Kth position of LA.
1. Start
2. Set LA[K-1] = ITEM
3. Stop
Example
#include <stdio.h>
void main(){
int LA[] = {1,3,5,7,8};
int k = 3, n = 5, item = 10;
int i, j;
18
printf("The original array elements are :\n");
for(i = 0; i<n; i++) {
printf("LA[%d] = %d \n", i, LA[i]);
}
LA[k-1] = item;
printf("The array elements after updation :\n");
for(i = 0; i<n; i++) {
printf("LA[%d] = %d \n", i, LA[i]);
}
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5 LA[3] = 7
LA[4] = 8
The array elements after updation :
LA[0] = 1
LA[1] = 3
LA[2] = 10
LA[3] = 7
LA[4] = 8
Display Operation
This operation displays all the elements in the entire array using a print statement.
Algorithm
1. Start
2. Print all the elements in the Array
3. Stop
Example
#include <stdio.h>
int main(){
int LA[] = {1,3,5,7,8};
int n = 5;
int i;
printf("The original array elements are :\n");
for(i = 0; i<n; i++) {
19
printf("LA[%d] = %d \n", i, LA[i]);
}
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Records
In computer science, records are a fundamental data structure used to store multiple pieces of
related information together under one name. They are also known as structures (in languages
like C) or classes (in object-oriented languages like Java or Python). Records allow you to
create custom data types that can hold different types of data in a structured way.
struct Person {
char name[50];
int age;
float height;
};
// Example usage:
struct Person person1;
strcpy(person1.name, "John");
person1.age = 30;
person1.height = 5.9;
What is a Stack?
A Stack is a linear data structure that follows the LIFO (Last-In-First-Out) principle. Stack
has one end, whereas the Queue has two ends (front and rear). It contains only one
pointer top pointer pointing to the topmost element of the stack. Whenever an element is
20
added in the stack, it is added on the top of the stack, and the element can be deleted only
from the stack. In other words, a stack can be defined as a container in which insertion and
deletion can be done from the one end known as the top of the stack.
Working of Stack
Stack works on the LIFO pattern. As we can observe in the below figure there are five
memory blocks in the stack; therefore, the size of the stack is 5.
Suppose we want to store the elements in a stack and let's assume that stack is empty. We
have taken the stack of size 5 as shown below in which we are pushing the elements one by
one until the stack becomes full.
Since our stack is full as the size of the stack is 5. In the above cases, we can observe that it
goes from the top to the bottom when we were entering the new element in the stack. The
stack gets filled up from the bottom to the top.
When we perform the delete operation on the stack, there is only one way for entry and exit
as the other end is closed. It follows the LIFO pattern, which means that the value entered
first will be removed last. In the above case, the value 5 is entered first, so it will be removed
only after the deletion of all the other elements.
21
Standard Stack Operations
o push(): When we insert an element in a stack then the operation is known as a push.
If the stack is full then the overflow condition occurs.
o pop(): When we delete an element from the stack, the operation is known as a pop. If
the stack is empty means that no element exists in the stack, this state is known as an
underflow state.
o isEmpty(): It determines whether the stack is empty or not.
o isFull(): It determines whether the stack is full or not.'
o peek(): It returns the element at the given position.
o count(): It returns the total number of elements available in a stack.
o change(): It changes the element at the given position.
o display(): It prints all the elements available in the stack.
PUSH operation
22
POP operation
The steps involved in the POP operation is given below:
o Before deleting the element from the stack, we check whether the stack is empty.
o If we try to delete the element from the empty stack, then the underflow condition
occurs.
o If the stack is not empty, we first access the element which is pointed by the top
o Once the pop operation is performed, the top is decremented by 1, i.e., top=top-1.
23
Applications of Stack
o Balancing of symbols: Stack is used for balancing a symbol. For example, we have
the following program:
1. int main()
2. {
3. cout<<"Hello";
4. cout<<"javaTpoint";
5. }
As we know, each program has an opening and closing braces; when the opening braces
come, we push the braces in a stack, and when the closing braces appear, we pop the opening
braces from the stack. Therefore, the net value comes out to be zero. If any symbol is left in
the stack, it means that some syntax occurs in a program.
o String reversal: Stack is also used for reversing a string. For example, we want to
reverse a "javaTpoint" string, so we can achieve this with the help of a stack.
First, we push all the characters of the string in a stack until we reach the null
character.
After pushing all the characters, we start taking out the character one by one until we
reach the bottom of the stack.
o UNDO/REDO: It can also be used for performing UNDO/REDO operations. For
example, we have an editor in which we write 'a', then 'b', and then 'c'; therefore, the
text written in an editor is abc. So, there are three states, a, ab, and abc, which are
24
stored in a stack. There would be two stacks in which one stack shows UNDO state,
and the other shows REDO state.
If we want to perform UNDO operation, and want to achieve 'ab' state, then we
implement pop operation.
o Recursion: The recursion means that the function is calling itself again. To maintain
the previous states, the compiler creates a system stack in which all the previous
records of the function are maintained.
o DFS (Depth First Search): This search is implemented on a Graph, and Graph uses
the stack data structure.
o Backtracking: Suppose we have to create a path to solve a maze problem. If we are
moving in a particular path, and we realize that we come on the wrong way. In order
to come at the beginning of the path to create a new path, we have to use the stack
data structure.
o Expression conversion: Stack can also be used for expression conversion. This is
one of the most important applications of stack. The list of the expression conversion
is given below:
o Infix to prefix
o Infix to postfix
o Prefix to infix
o Prefix to postfix
Postfix to infix
o Memory management: The stack manages the memory. The memory is assigned in
the contiguous memory blocks. The memory is known as stack memory as all the
variables are assigned in a function call stack memory. The compiler knows the
memory size assigned to the program. When the function is created, all its variables
are assigned in the stack memory. When the function completed its execution, all the
variables assigned in the stack are released.
In array implementation, the stack is formed by using the array. All the operations regarding
the stack are performed using arrays. Lets see how each operation can be implemented on the
stack using array data structure.
Adding an element into the top of the stack is referred to as push operation. Push operation
involves following two steps.
25
1. Increment the variable Top so that it can now refere to the next memory location.
2. Add element at the position of incremented top. This is referred to as adding new
element at the top of the stack.
Stack is overflown when we try to insert an element into a completely filled stack therefore,
our main function must always avoid stack overflow condition.
Algorithm:
1. begin
2. if top = n then stack full
3. top = top + 1
4. stack (top) : = item;
5. end
Deletion of an element from the top of the stack is called pop operation. The value of the
variable top will be incremented by 1 whenever an item is deleted from the stack. The top
most element of the stack is stored in an another variable and then the top is decremented by
1. the operation returns the deleted value that was stored in another variable as the result.
The underflow condition occurs when we try to delete an element from an already empty
stack.
Algorithm :
1. begin
2. if top = 0 then stack empty;
3. item := stack(top);
4. top = top - 1;
26
5. end;
Peek operation involves returning the element which is present at the top of the stack without
deleting it. Underflow condition can occur if we try to return the top element in an already
empty stack.
Algorithm :
1. Begin
2. if top = -1 then stack empty
3. item = stack[top]
4. return item
5. End
27
8. else
9. {
10. return stack [top];
11. }
12. }
C program
#include <stdio.h>
int stack[100],i,j,choice=0,n,top=-1;
void push();
void pop();
void show();
void main ()
{
printf("\n----------------------------------------------\n");
while(choice != 4)
{
printf("Chose one from the below options...\n");
printf("\n1.Push\n2.Pop\n3.Show\n4.Exit");
printf("\n Enter your choice \n");
scanf("%d",&choice);
switch(choice)
{
case 1:
{
push();
break;
}
case 2:
{
pop();
break;
}
case 3:
28
{
show();
break;
}
case 4:
{
printf("Exiting....");
break;
}
default:
{
printf("Please Enter valid choice ");
}
};
}
}
void push ()
{
int val;
if (top == n )
printf("\n Overflow");
else
{
printf("Enter the value?");
scanf("%d",&val);
top = top +1;
stack[top] = val;
}
}
void pop ()
{
if(top == -1)
printf("Underflow");
else
top = top -1;
}
void show()
29
{
for (i=top;i>=0;i--)
{
printf("%d\n",stack[i]);
}
if(top == -1)
{
printf("Stack is empty");
}
}
What is a Queue?
Queue is the data structure that is similar to the queue in the real world. A queue is a data
structure in which whatever comes first will go out first, and it follows the FIFO (First-In-
First-Out) policy. Queue can also be defined as the list or collection in which the insertion is
done from one end known as the rear end or the tail of the queue, whereas the deletion is
done from another end known as the front end or the head of the queue.
The real-world example of a queue is the ticket queue outside a cinema hall, where the person
who enters first in the queue gets the ticket first, and the last person enters in the queue gets
the ticket at last. Similar approach is followed in the queue in data structure.
30
Types of Queue
There are four different types of queue that are listed as follows -
The major drawback of using a linear Queue is that insertion is done only from the rear end.
If the first three elements are deleted from the Queue, we cannot insert more elements even
though the space is available in a Linear Queue. In this case, the linear Queue shows the
overflow condition as the rear is pointing to the last element of the Queue.
31
Circular Queue
In Circular Queue, all the nodes are represented as circular. It is similar to the linear Queue
except that the last element of the queue is connected to the first element. It is also known as
Ring Buffer, as all the ends are connected to another end. The representation of circular
queue is shown in the below image -
The drawback that occurs in a linear queue is overcome by using the circular queue. If the
empty space is available in a circular queue, the new element can be added in an empty space
by simply incrementing the value of rear. The main advantage of using the circular queue is
better memory utilization.
Priority Queue
It is a special type of queue in which the elements are arranged based on the priority. It is a
special type of queue data structure in which every element has a priority associated with it.
Suppose some elements occur with the same priority, they will be arranged according to the
FIFO principle. The representation of priority queue is shown in the below image -
Insertion in priority queue takes place based on the arrival, while deletion in the priority
queue occurs based on the priority. Priority queue is mainly used to implement the CPU
scheduling algorithms.
There are two types of priority queue that are discussed as follows -
32
o Descending priority queue - In descending priority queue, elements can be inserted
in arbitrary order, but only the largest element can be deleted first. Suppose an array
with elements 7, 3, and 5 in the same order, so, insertion can be done with the same
sequence, but the order of deleting the elements is 7, 5, 3.
In Deque or Double Ended Queue, insertion and deletion can be done from both ends of the
queue either from the front or rear. It means that we can insert and delete elements from both
front and rear ends of the queue. Deque can be used as a palindrome checker means that if we
read the string from both ends, then the string would be the same.
Deque can be used both as stack and queue as it allows the insertion and deletion operations
on both ends. Deque can be considered as stack because stack follows the LIFO (Last In First
Out) principle in which insertion and deletion both can be performed only from one end. And
in deque, it is possible to perform both insertion and deletion from one end, and Deque does
not follow the FIFO principle.
o Input restricted deque - As the name implies, in input restricted queue, insertion
operation can be performed at only one end, while deletion can be performed from
both ends.
o Output restricted deque - As the name implies, in output restricted queue, deletion
operation can be performed at only one end, while insertion can be performed from
33
both ends.
o Enqueue: The Enqueue operation is used to insert the element at the rear end of the
queue. It returns void.
o Dequeue: It performs the deletion from the front-end of the queue. It also returns the
element, which has been removed, from the front-end. It returns an integer value.
o Peek: This is the third operation that returns the element, which is pointed by the
front pointer in the queue but does not delete it.
o Queue overflow (isfull): It shows the overflow condition when the queue is
completely full.
o Queue underflow (isempty): It shows the underflow condition when the Queue is
empty, i.e., no elements are in the Queue.
34
Operations performed on queue
The fundamental operations that can be performed on queue are listed as follows -
o Enqueue: The Enqueue operation is used to insert the element at the rear end of the
queue. It returns void.
o Dequeue: It performs the deletion from the front-end of the queue. It also returns the
element, which has been removed, from the front-end. It returns an integer value.
o Peek: This is the third operation that returns the element, which is pointed by the
front pointer in the queue but does not delete it.
o Queue overflow (isfull): It shows the overflow condition when the queue is
completely full.
o Queue underflow (isempty): It shows the underflow condition when the Queue is
empty, i.e., no elements are in the Queue.
// implementation of queue
#include <bits/stdc++.h>
using namespace std;
unsigned capacity;
int* array;
};
35
// function to create a queue
// of given capacity.
36
}
if (isEmpty(queue))
return INT_MIN;
return queue->array[queue->front];
if (isEmpty(queue))
return INT_MIN;
return queue->array[queue->rear];
int main()
enqueue(queue, 10);
enqueue(queue, 20);
enqueue(queue, 30);
enqueue(queue, 40);
37
return 0;
}
Output
10 enqueued to queue
20 enqueued to queue
30 enqueued to queue
40 enqueued to queue
10 dequeued from queue
Front item is 20
Rear item is 40
1. Write short note on different operations that can be performed on data strucutres.
2. Write an algorithm for Push() and Pop() operations on Stack using arrays.
38
UNIT 2 Linked Structure
Structure:
2.1 List Representation
2.1 Polish Notations
2.2 Operations on linked list – get node and free node operation
2.3 Implementing the list operation
2.4 Inserting into an ordered linked list
2.5 Deleting circular linked list
2.6 Doubly linked list
2.7 Unit End Questions
Linked lists have several advantages that make them useful in various scenarios. Here
are some of the key advantages of linked lists:
1. Dynamic Size:
- Linked lists can easily grow or shrink in size during program execution because
memory allocation is dynamic. This contrasts with static arrays, where the size is fixed at
compile time.
3. No Wasted Memory:
- Linked lists do not require a fixed amount of memory like arrays. Memory is allocated
as needed, minimizing wasted space and making efficient use of memory.
4. No Pre-allocation of Space:
39
- Unlike arrays, which may need to pre-allocate space for a certain size, linked lists
allocate memory as elements are added. This can be beneficial when the number of
elements is uncertain or varies.
8. No Predefined Size:
- Linked lists do not have a predefined size, which means that they can accommodate
data of any size without requiring modifications to the underlying data structure.
Despite these advantages, it's important to note that linked lists also have some drawbacks.
They may have higher memory overhead due to the storage of pointers, and direct access
to elements by index is not as efficient as with arrays. The choice between linked lists and
other data structures depends on the specific requirements of the task at hand.
While linked lists have several advantages, they also come with certain disadvantages. It's
essential to consider these drawbacks when choosing a data structure for a particular
application. Here are some of the disadvantages of linked lists:
1. Memory Overhead:
- Linked lists require additional memory to store the pointers/references for each
element. This can lead to higher memory overhead compared to arrays, where elements
are stored contiguously.
3. Traversal Overhead:
40
- Traversing a linked list can have a higher constant factor than iterating through an
array due to the extra memory accesses required for following pointers.
4. Cache Issues:
- Linked lists may not utilize the cache as efficiently as arrays. Since the elements are
scattered in memory, there is a higher likelihood of cache misses, leading to slower access
times.
8. Complexity in Implementation:
- Implementing and managing linked lists can be more complex than using other data
structures, especially for beginners. Dealing with pointers and ensuring proper memory
management can be error-prone.
Despite these disadvantages, linked lists remain valuable in certain situations where
dynamic size, efficient insertions/deletions, and flexibility in memory usage are crucial.
The choice of data structure depends on the specific requirements of the task at hand.
Linked lists come in various types, and each type has different characteristics to suit
different needs. The main types of linked lists are:
struct Node {
int data;
Node* next;
};
struct Node {
int data;
Node* next;
Node* prev;
};
5. Self-adjusting Lists:
- Lists designed to reorganize themselves dynamically to optimize for frequently
accessed elements.
- Examples include the move-to-front list and transpose list.
It's important to choose the appropriate type of linked list based on the specific
requirements of the task at hand. Each type has its own advantages and trade-offs, and the
choice depends on factors such as memory efficiency, access patterns, and the types of
operations you need to perform on the list.
Example:
- Infix: \( (3 + 4) \times 5 \)
- Prefix: \( \times + 3 4 5 \)
Example:
- Infix: \( (3 + 4) \times 5 \)
- Postfix: \( 3 4 + 5 \times \)
Polish notation eliminates the need for parentheses and the rules of precedence because
the position of the operator determines its scope. Evaluating expressions in Polish notation
is often done using a stack-based algorithm.
#include <iostream>
#include <stack>
#include <sstream>
std::istringstream iss(expression);
std::string token;
if (token == "+") {
stack.push(operand1 + operand2);
} else if (token == "-") {
stack.push(operand1 - operand2);
} else if (token == "*") {
stack.push(operand1 * operand2);
} else if (token == "/") {
stack.push(operand1 / operand2);
}
}
}
45
return stack.top();
}
int main() {
std::string postfixExpression = "3 4 + 5 *";
std::cout << "Result: " << evaluatePostfix(postfixExpression) << std::endl;
return 0;
}
This example evaluates the postfix expression "3 4 + 5 *", which corresponds to the infix
expression \( (3 + 4) \times 5 \). The result should be 35.
An expression is any word or group of words or symbols that generates a value on evaluation.
Parsing expression means analyzing the expression for its words or symbols depending on a
particular criterion. Expression parsing is a term used in a programming language to evaluate
arithmetic and logical expressions.
The way to write arithmetic expression is known as a notation. An arithmetic expression can
be written in three different but equivalent notations, i.e., without changing the essence or
output of an expression. These notations are −
Infix Notation
Prefix (Polish) Notation
Postfix (Reverse-Polish) Notation
These notations are named as how they use operator in expression. We shall learn the same
here in this chapter.
Infix Notation
We write expression in infix notation, e.g. a - b + c, where operators are used in-between
operands. It is easy for us humans to read, write, and speak in infix notation but the same
does not go well with computing devices. An algorithm to process infix notation could be
difficult and costly in terms of time and space consumption.
Prefix Notation
In this notation, operator is prefixed to operands, i.e. operator is written ahead of operands.
For example, +ab. This is equivalent to its infix notation a + b. Prefix notation is also known
as Polish Notation.
Postfix Notation
This notation style is known as Reversed Polish Notation. In this notation style, the operator
is postfixed to the operands i.e., the operator is written after the operands. For example, ab+.
This is equivalent to its infix notation a + b.
46
The following table briefly tries to show the difference in all three notations −
2 (a + b) ∗ c ∗+abc ab+c∗
3 a ∗ (b + c) ∗a+bc abc+∗
5 (a + b) ∗ (c + d) ∗+ab+cd ab+cd+∗
Parsing Expressions
To parse any arithmetic expression, we need to take care of operator precedence and
associativity also.
Precedence
When an operand is in between two different operators, which operator will take the operand
first, is decided by the precedence of an operator over others. For example −
Associativity
Associativity describes the rule where operators with the same precedence appear in an
expression. For example, in expression a + b − c, both + and − have the same precedence,
then which part of the expression will be evaluated first, is determined by associativity of
those operators. Here, both + and − are left associative, so the expression will be evaluated
as (a + b) − c.
47
Sr.No. Operator Precedence Associativity
The above table shows the default behavior of operators. At any point of time in expression
evaluation, the order can be altered by using parenthesis. For example −
In a + b*c, the expression part b*c will be evaluated first, with multiplication as precedence
over addition. We here use parenthesis for a + b to be evaluated first, like (a + b)*c.
Linked lists support various operations for manipulating and accessing data. Here are
some common operations on linked lists:
1. Insertion:
- Insert at the Beginning: Add a new node at the beginning of the linked list.
- Insert at the End: Add a new node at the end of the linked list.
- Insert at a Specific Position: Add a new node at a specified position in the linked
list.
48
// Insert at the End
void insertAtEnd(Node*& head, int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
return;
}
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
}
2. Deletion:
- Delete at the Beginning: Remove the first node from the linked list.
- Delete at the End: Remove the last node from the linked list.
- Delete at a Specific Position: Remove a node from a specified position in the
linked list.
3. Traversal:
- Traverse the linked list to access and print each element.
4. Search:
- Search for a specific element in the linked list.
5. Length:
- Determine the length (number of nodes) of the linked list.
These are basic operations, and depending on the specific requirements, more complex
operations and variations may be implemented. Additionally, some operations might be more
efficient with specific types of linked lists (e.g., singly linked lists, doubly linked lists,
circular linked lists).
A doubly linked list is a type of linked list in which each node contains a data element
and two pointers or references—one pointing to the next node in the sequence (as in a
singly linked list), and another pointing to the previous node. This bidirectional
linking allows for easier traversal in both forward and backward directions.
struct Node {
int data;
Node* next;
Node* prev;
51
1. Insertion:
- Insert at the Beginning:
2. Deletion:
- Delete at the Beginning:
These are basic operations for a doubly linked list. The doubly linked list provides
bidirectional traversal and makes certain operations, such as deletion at the end or
backward traversal, more efficient compared to a singly linked list.
A circular linked list is a type of linked list in which the last node of the list points
back to the first node, creating a closed loop. In a circular linked list, the "next"
pointer of the last node is not `nullptr` but points to the first node in the list. This
allows for continuous traversal and is especially useful in applications where
continuous access to the list is required.
struct Node {
int data;
Node* next;
1. Insertion:
- Insert at the Beginning:
2. Deletion:
- Delete at the Beginning:
3. Traversal:
In a circular linked list, operations are performed similarly to a singly linked list, with
the main difference being how the last node points back to the first. This allows for
continuous traversal without the need to check for `nullptr` as an end condition.
Structure:
3.1 Concept and terminology
3.2 Types of trees
3.3 Binary Search Trees
3.3 Inserting, deleting and searching into binary search tree
3.4 Implementing the insert, search and delete algorithms
3.5 Tree traversals
3.6 Huffman’s algorithm
3.7 Unit End Questions
57
3.1 Concept and Terminology
A tree data structure is a hierarchical structure that is used to represent and organize data in a way that
is easy to navigate and search. It is a collection of nodes that are connected by edges and has a
hierarchical relationship between the nodes.
The topmost node of the tree is called the root, and the nodes below it are called the child nodes. Each
node can have multiple child nodes, and these child nodes can also have their own child nodes,
forming a recursive structure.
Parent Node: The node which is a predecessor of a node is called the parent node of that node. {B} is
the parent node of {D, E}.
Child Node: The node which is the immediate successor of a node is called the child node of that
node. Examples: {D, E} are the child nodes of {B}.
Root Node: The topmost node of a tree or the node which does not have any parent node is called the
root node. {A} is the root node of the tree. A non-empty tree must contain exactly one root node and
exactly one path from the root to all other nodes of the tree.
Leaf Node or External Node: The nodes which do not have any child nodes are called leaf nodes.
{K, L, M, N, O, P, G} are the leaf nodes of the tree.
Ancestor of a Node: Any predecessor nodes on the path of the root to that node are called Ancestors
of that node. {A,B} are the ancestor nodes of the node {E}
Descendant: Any successor node on the path from the leaf node to that node. {E,I} are the
descendants of the node {B}.
58
Sibling: Children of the same parent node are called siblings. {D,E} are called siblings.
Level of a node: The count of edges on the path from the root node to that node. The root node has
level 0.
Internal node: A node with at least one child is called Internal Node.
Neighbour of a Node: Parent or child nodes of that node are called neighbors of that node.
A tree consists of a root and zero or more subtrees T1, T2… Tk such that there is an edge from the
root of the tree to the root of each subtree.
59
Representation of a Node in Tree Data Structure:
struct Node
{
int data;
struct Node *first_child;
struct Node *second_child;
struct Node *third_child;
.
.
.
struct Node *nth_child;
};
There are several types of trees in computer science, each with its own characteristics and use cases.
Here are some common types of trees:
1. Binary Tree:
- A tree in which each node has at most two children, usually referred to as the left child and the
right child.
3. AVL Tree:
- A self-balancing binary search tree where the heights of the two child subtrees of any node differ
by at most one. AVL trees maintain balance to ensure efficient search, insertion, and deletion
operations.
4. Red-Black Tree:
- A self-balancing binary search tree with additional properties, ensuring that the tree remains
balanced during insertions and deletions. Red-Black trees are used in various applications, including
C++'s `std::set` and `std::map` implementations.
5. B-Tree:
- A self-balancing search tree that maintains sorted data and is designed to work well on secondary
storage, such as disks. B-trees are commonly used in databases and file systems.
60
6. Trie (Prefix Tree):
- A tree-like data structure used for efficiently storing a dynamic set or associative array where the
keys are usually strings. Trie structures are frequently used in applications like spell checkers and IP
routers.
7. Heap:
- A specialized tree-based data structure that satisfies the heap property. A binary heap is commonly
used to implement priority queues.
8. Expression Tree:
- A tree representation of an algebraic expression, where the leaves are operands and internal nodes
are operators. Expression trees are useful in compilers for evaluating expressions.
9. Segment Tree:
- A tree structure used for storing information about intervals or segments. It is particularly useful in
applications like range queries and updates.
10. Quadtree:
- A tree data structure in which each internal node has four children, dividing a space into four
quadrants. Quadtrees are often used in image processing and geographical information systems.
11. Octree:
- Similar to a quadtree, but in three dimensions, dividing space into octants. Octrees are used in 3D
computer graphics and simulations.
These are just a few examples of the many types of trees used in computer science. The choice of a
tree structure depends on the specific requirements and characteristics of the application or problem
being solved.
Tree Traversals
Tree traversal is the process of visiting each node in a tree data structure and performing a
specific operation at each node. There are different strategies for tree traversal, and they can
be broadly categorized into three main types: depth-first traversal, breadth-first traversal, and
level order traversal. Here are the common tree traversal techniques:
61
Depth-First Traversal:
In depth-first traversal, you start from the root and explore as far as possible along each
branch before backtracking.
62
1. Level Order Traversal:
Traverse the tree level by level, from left to right.
while (!q.empty()) {
Node* current = q.front();
std::cout << current->data << " ";
q.pop();
if (current->left != nullptr) {
q.push(current->left);
}
if (current->right != nullptr) {
q.push(current->right);
}
}
}
These traversal techniques are fundamental for various applications, including searching,
printing tree structures, and evaluating expressions. The choice of the traversal method
depends on the specific requirements of the problem at hand.
A Binary Search Tree (BST) is a binary tree data structure with the following properties:
2. Ordering Property:
- For every node \(n\), all nodes in its left subtree have values less than \(n\), and all nodes
in its right subtree have values greater than \(n\).
This ordering property makes BSTs suitable for efficient search, insertion, and deletion
operations. The binary search tree is a common and versatile data structure used in various
applications.
63
### Operations on Binary Search Trees:
1. Search Operation:
- Start at the root and recursively navigate left or right based on the comparison of the
target value with the current node's value until finding the target or reaching a leaf node.
2. Insertion Operation:
- Starting at the root, recursively navigate left or right based on the comparison of the value
to be inserted with the current node's value. When reaching a null pointer, insert the new
node.
return root;
}
3. Deletion Operation:
64
- Search for the node to be deleted. If it has one or no children, remove it. If it has two
children, find its in-order successor (or predecessor), replace it with the successor (or
predecessor), and then recursively delete the successor (or predecessor).
return root;
}
4. Traversal Operations:
- In-order, pre-order, and post-order traversals are commonly used to visit nodes in a BST.
65
void inorderTraversal(Node* root) {
if (root != nullptr) {
inorderTraversal(root->left);
std::cout << root->data << " ";
inorderTraversal(root->right);
}
}
Binary Search Trees are widely used in databases, symbol tables, and other applications
where efficient search, insertion, and deletion operations are important. However, in certain
situations, unbalanced BSTs can become degenerate, leading to poor performance. Self-
balancing variants like AVL trees and Red-Black trees help address this issue by maintaining
balance during insertions and deletions.
Certainly! Here are C++ implementations for inserting, deleting, and searching operations in
a Binary Search Tree (BST). We'll assume you have a basic `Node` structure as described
earlier:
#include <iostream>
struct Node {
66
int data;
Node* left;
Node* right;
return root;
}
67
// Function to delete a node with a given value from the BST
Node* deleteNode(Node* root, int key) {
if (root == nullptr) {
return root;
}
return root;
}
int main() {
Node* root = nullptr;
68
// Insert some values into the BST
root = insert(root, 50);
insert(root, 30);
insert(root, 20);
insert(root, 40);
insert(root, 70);
insert(root, 60);
insert(root, 80);
std::cout << "In-order traversal after deleting " << deleteKey << ": ";
inorderTraversal(root);
std::cout << std::endl;
return 0;
}
This example demonstrates the basic operations of inserting, deleting, and searching in a
Binary Search Tree. The `insert`, `search`, and `deleteNode` functions are implemented based
on the properties of the Binary Search Tree. The `inorderTraversal` function is used to print
the elements in the tree in sorted order.
Tree traversals are techniques for systematically visiting each node in a tree data structure.
There are several ways to traverse a tree, and the three main types are:
69
1. In-order Traversal (LNR - Left-Node-Right):
- In this traversal, the left subtree is explored first, then the current node is processed, and
finally, the right subtree is explored.
70
These traversals are fundamental in understanding and working with tree structures. The
choice of traversal depends on the specific requirements of the problem at hand.
### Example:
struct Node {
int data;
Node* left;
Node* right;
int main() {
Node* root = new Node(1);
root->left = new Node(2);
root->right = new Node(3);
root->left->left = new Node(4);
root->left->right = new Node(5);
return 0;
}
71
This example creates a simple binary tree and performs in-order, pre-order, and post-order
traversals, printing the node values in different orders.
Huffman coding is a popular algorithm used for lossless data compression. It was developed
by David A. Huffman in 1952. The key idea behind Huffman coding is to assign variable-
length codes to input characters, with shorter codes assigned to more frequently occurring
characters. This way, the most common characters are represented with fewer bits, leading to
more efficient compression.
1. Frequency Count:
- Calculate the frequency of each character in the input data.
4. Assign Codes:
- Assign binary codes to each character based on their position in the Huffman tree. Left
edges are labeled with '0', and right edges are labeled with '1'. The codes are constructed by
traversing from the root to each leaf.
5. Encode Data:
- Replace each character in the input data with its corresponding Huffman code to obtain
the compressed data.
6. Decode Data:
- Use the Huffman tree to decode the compressed data back to the original form.
#include <iostream>
72
#include <queue>
#include <unordered_map>
struct Node {
char data;
unsigned frequency;
Node* left;
Node* right;
struct Compare {
bool operator()(Node* left, Node* right) {
return left->frequency > right->frequency;
}
};
minHeap.push(internalNode);
}
return minHeap.top();
}
int main() {
std::string input = "abracadabra";
return 0;
}
74
This example demonstrates the encoding part of Huffman coding. The Huffman tree is built,
and codes are generated for each character based on their frequency. The input data is then
encoded using these Huffman codes. In practice, Huffman coding is often used in
combination with other techniques for efficient compression.
75
UNIT 4 Graph Structure
Structure:
4.1 Graph Representation
4.2 Adjacency Matrix
4.3 Adjacency List
4.4 Warshall’s Algorithm
4.5 Adjacency Multilist Representation
4.6 Orthogonal Representation of a graph
4.7 Graph Traversals – BFA and DFS
4.8 Shortest Path – All pairs of shortest paths
4.9 Transitive Closure
4.10 Unit End Questions
Introduction:
A Graph is a non-linear data structure consisting of vertices and edges. The vertices are
sometimes also referred to as nodes and the edges are lines or arcs that connect any two
nodes in the graph. More formally a Graph is composed of a set of vertices( V ) and a set of
edges( E ). The graph is denoted by G(V, E).
Components of a Graph
Vertices: Vertices are the fundamental units of the graph. Sometimes, vertices are also known
as vertex or nodes. Every node/vertex can be labeled or unlabelled.
Edges: Edges are drawn or used to connect two nodes of the graph. It can be ordered pair of
nodes in a directed graph. Edges can connect any two nodes in any possible way. There are
no rules. Sometimes, edges are also known as arcs. Every edge can be labelled/unlabelled.
Types Of Graph
1. Null Graph
A graph is known as a null graph if there are no edges in the graph.
76
2. Trivial Graph
Graph having only a single vertex, it is also the smallest graph possible.
3. Undirected Graph
A graph in which edges do not have any direction. That is the nodes are unordered pairs in
the definition of every edge.
4. Directed Graph
A graph in which edge has direction. That is the nodes are ordered pairs in the definition of
every edge.
77
5. Connected Graph
The graph in which from one node we can visit any other node in the graph is known as a
connected graph.
6. Disconnected Graph
The graph in which at least one node is not reachable from a node is known as a disconnected
graph
7. Regular Graph
The graph in which the degree of every vertex is equal to K is called K regular graph.
8. Complete Graph
The graph in which from each node there is an edge to each other node.
78
9. Cycle Graph
The graph in which the graph is a cycle in itself, the degree of each vertex is 2.
79
13. Weighted Graph
A graph in which the edges are already specified with suitable weight is known as a
weighted graph.
Weighted graphs can be further classified as directed weighted graphs and undirected
weighted graphs
Adjacency Matrix
Adjacency List
Adjacency Matrix
80
In this method, the graph is stored in the form of the 2D matrix where rows and columns
denote vertices. Each entry in the matrix represents the weight of the edge between those
vertices.
Adjacency List
This graph is represented as a collection of linked lists. There is an array of pointer which
points to the edges connected to that vertex.
81
Basic Operations on Graphs
Below are the basic operations on the graph:
Usage of graphs
Maps can be represented using graphs and then can be used by computers to provide
various services like the shortest path between two cities.
When various tasks depend on each other then this situation can be represented using a
Directed Acyclic graph and we can find the order in which tasks can be performed using
topological sort.
State Transition Diagram represents what can be the legal moves from current states. In-
game of tic tac toe this can be used.
If a graph has n number of vertices, then the adjacency matrix of that graph is n x n, and
each entry of the matrix represents the number of edges from one vertex to another.
An adjacency matrix is also called as connection matrix. Sometimes it is also called a Vertex
matrix.
aij = 0 {Otherwise}
82
Let's see some of the important points with respect to the adjacency matrix.
o If there exists an edge between vertex Vi and Vj, where i is a row, and j is a
column, then the value of aij = 1.
o If there is no edge between vertex Vi and Vj, then the value of aij = 0.
o If there are no self loops in the simple graph, then the vertex matrix (or
adjacency matrix) should have 0s in the diagonal.
o An adjacency matrix is symmetric for an undirected graph. It specifies that the
value in the ith row and jth column is equal to the value in jth row ith
o If the adjacency matrix is multiplied by itself, and if there is a non-zero value is
present at the ith row and jth column, then there is the route from Vi to Vj with a
length equivalent to 2. The non-zero value in the adjacency matrix represents
that the number of distinct paths is present.
Where the aij equals the number of edges from the vertex i to j. As mentioned above,
the Adjacency matrix is symmetric for an undirected graph, so for an undirected
graph, aij = aji.
When the graphs are simple and there are no weights on the edges or multiple
edges, then the entries of the adjacency matrix will be 0 and 1. If there are no self-
loops, then the diagonal entries of the adjacency matrix will be 0.
Now, let's see the adjacency matrix for an undirected graph and for directed graphs.
83
Let us consider the below-undirected graph and try to construct the adjacency matrix
of it.
n the graph, we can see there is no self-loop, so the diagonal entries of the adjacent
matrix will be 0. The adjacency matrix of the above graph will be -
Let us consider the below directed graph and try to construct the adjacency matrix of
it.
84
In the above graph, we can see there is no self-loop, so the diagonal entries of the
adjacent matrix will be 0. The adjacency matrix of the above graph will be -
Let's see some questions of the adjacency matrix. Below questions are on the weighted
undirected, and directed graphs.
Question 1 - What will be the adjacency matrix for the below undirected weighted
graph?
85
Solution - In the given question, there is no self-loop, so it is clear that the diagonal
entries of the adjacent matrix for the above graph will be 0. The above graph is a
weighted undirected graph. The weights on the graph edges will be represented as
the entries of the adjacency matrix.
Question 2 - What will be the adjacency matrix for the below directed weighted
graph?
Solution - In the given question, there is no self-loop, so it is clear that the diagonal
entries of the adjacent matrix for the above graph will be 0. The above graph is a
weighted directed graph. The weights on the graph edges will be represented as the
entries of the adjacency matrix.
86
4.3 Adjacency List
An Adjacency list is a collection of nodes that have an edge over each other. The advantage
of this list over a regular graph is that you can edit or remove nodes from it easily. You can
also add additional edges between two nodes if needed by using the add_edge method on
your list object or calling another method (such as add_node()).
It is a list that can be used to represent a graph. It is implemented as an array, with each
element of the array representing a vertex in the graph. The elements of the array store the
vertices that are adjacent to the vertex represented by the element. Each item contains two
pieces of information:
The first element of the array represents the number of neighbours (if any) surrounding it.
While the second element holds its location in space, i.e., left/right/top/bottom etc.
You may have noticed that there are some similarities between an adjacency list and a
directed graph, where nodes connect only if they share an edge or have some kind of
relationship with each other. However, unlike directed graphs where edges are always
identified by numbers instead of text labels like A-B or 1->2->3->4->5. In contrast,
undirected graphs don’t require such identification labels. Because all paths between two
points are available simultaneously, without having any restrictions on how many paths could
exist between them. Therefore, no restriction on how many paths exist.
Adjacency lists are a data structure that stores the relationship between vertices in a graph.
The nodes in an adjacency list are referred to as vertices, and their neighbours are stored at
the same level of abstraction (e.g., two adjacent vertices). They also allow you to add new
edges or remove existing ones easily by simply adding or removing items from your list
respectively.
87
Adjacent pairs are stored in sorted order so that you can easily find them when needed. This
makes it easy for us to perform operations such as finding all paths between two vertices
using their indices within these structures.
An adjacency list only stores the edges of a graph, not the vertices, making it a space-
efficient representation of a graph.
It is also simple to implement and easy to modify.
An adjacency list can be easily modified to store additional information about the edges in
the graph, such as weights or labels.
It is a data structure that stores the set of all vertices that are adjacent in a graph. It has several
advantages over other graph structures such as adjacency lists being more efficient in terms
of storage and access.
Adjacency lists can also be used to store and retrieve data. In addition, they can be used to
implement algorithms on graphs. Like the shortest path or topological ordering which
requires numeric values (the distance between two vertices).
A graph is a collection of nodes and edges. Nodes are connected by edges, which can be
directed or undirected. An edge is a connection between two nodes, where one node is called
the source and the other one is called the target. Edges are often represented as lines on paper
or screens, but they can also be visualized as arrows in space (e.g., in Google Earth).
An important concept to remember about graphs is that we can add more than one edge
between two given vertices. Like this:
88
This means we have multiple paths connecting two vertices together. Also note that there
may be multiple paths connecting two vertices if they share some common property (e.g.,
location).
Vertex 0: [1, 2]
Vertex 1: [0, 2]
In this example, the adjacency list for vertex 0 is [1, 2], which means that vertex 0
is connected to vertices 1 and 2. Similarly, the adjacency list for vertex 1 is [0, 2],
which means that vertex 1 is connected to vertices 0 and 2.
89
To represent this graph as list:
Vertex 0: [1, 2]
Vertex 1: [0, 2]
The Floyd-Warshall algorithm, named after its creators Robert Floyd and Stephen Warshall,
is a fundamental algorithm in computer science and graph theory. It is used to find the
shortest paths between all pairs of nodes in a weighted graph. This algorithm is highly
efficient and can handle graphs with both positive and negative edge weights, making it a
versatile tool for solving a wide range of network and connectivity problems.
The Floyd Warshall Algorithm is an all pair shortest path algorithm unlike Dijkstra and
Bellman Ford which are single source shortest path algorithms. This algorithm works for
both the directed and undirected weighted graphs. But, it does not work for the graphs with
negative cycles (where the sum of the edges in a cycle is negative). It follows Dynamic
Programming approach to check every possible path going via every possible node in order
to calculate shortest distance between every pair of nodes.
Suppose we have a graph G[][] with V vertices from 1 to N. Now we have to evaluate a
shortestPathMatrix[][] where shortestPathMatrix[i][j] represents the shortest path between
vertices i and j.
90
Obviously the shortest path between i to j will have some k number of intermediate nodes.
The idea behind floyd warshall algorithm is to treat each and every vertex from 1 to N as an
intermediate node one by one.
The following figure shows the above optimal substructure property in floyd warshall
algorithm:
For k = 0 to n – 1
For i = 0 to n – 1
For j = 0 to n – 1
Distance[i, j] = min(Distance[i, j], Distance[i, k] + Distance[k, j])
where i = source Node, j = Destination Node, k = Intermediate Node
91
Illustration of Floyd Warshall Algorithm :
Step 1: Initialize the Distance[][] matrix using the input graph such that
Distance[i][j]= weight of edge from i to j, also Distance[i][j] = Infinity if there is
no edge from i to j.
92
Step 3: Treat node B as an intermediate node and calculate the Distance[][]
for every {i,j} node pair using the formula:
= Distance[i][j] = minimum (Distance[i][j], (Distance from i to B) + (Distance
from B to j ))
= Distance[i][j] = minimum (Distance[i][j], Distance[i][B] + Distance[B][j])
93
Step 5: Treat node D as an intermediate node and calculate the Distance[][]
for every {i,j} node pair using the formula:
= Distance[i][j] = minimum (Distance[i][j], (Distance from i to D) + (Distance
from D to j ))
= Distance[i][j] = minimum (Distance[i][j], Distance[i][D] + Distance[D][j])
94
Step 7: Since all the nodes have been treated as an intermediate node, we
can now return the updated Distance[][] matrix as our answer matrix.
95