Professional Documents
Culture Documents
DSA C Lab Manual Combined
DSA C Lab Manual Combined
Objectives:
Defining a Structure:
Before we can create structure variables, we need to define its data type. To define a structure, the
struct keyword is used.
Here is an Example:
struct Person
{
char name[50];
int citNo;
float salary;
};
Here, a derived type struct Person is defined. Now, we can create variables of this type.
When a struct type is declared, no storage or memory is allocated. To allocate memory of a given
structure type and work with it, we need to create variables.
struct Person
{
char name[50];
int citNo;
float salary;
};
int main()
{
struct Person person1, person2, p[20];
return 0;
}
Another way of creating a struct variable is:
struct Person
{
char name[50];
int citNo;
float salary;
} person1, person2, p[20];
In both cases, two variables person1, person2, and an array variable p having 20 elements of type
struct Person are created.
There are two types of operators used for accessing members of a structure.
Suppose, we want to access the salary of person2. Here's how we can do it.
person2.salary
C Pointers to structures:
struct name
{
member1;
member2;
.
.
};
int main()
{
struct name *ptr;
}
Here, a pointer ptr of type struct name is created. That is, ptr is a pointer to struct.
Access members using Pointer:
To access members of a structure using pointers, we use the Arrow “->” operator.
#include <stdio.h>
struct person
{
int age;
float weight;
};
int main()
{
struct person *personPtr, person1;
personPtr = &person1;
printf("Enter age: ");
scanf("%d", &personPtr>age);
printf("Enter weight: ");
scanf("%f", &personPtr>weight);
printf("Displaying:\n");
printf("Age: %d\n", personPtr>age);
printf("weight: %f", personPtr>weight);
return 0;
}
In this example, the address of person1 is stored in the personPtr pointer using
personPtr = &person1;.
Now, we can access the members of person1 using the personPtr pointer
Dynamic Memory Allocation for structures:
Before you proceed with this section, we recommend you to check C dynamic memory allocation in
your programming text book.
Sometimes, the number of struct variables we declared may be insufficient. We may need to
allocate memory during run-time. Here's how we can achieve this in C programming.
Enter the number of persons: 2
Enter first name and age respectively: Harry 24
Enter first name and age respectively: Gary 32
Displaying Information:
Name: Harry Age: 24
Name: Gary Age: 32
In the above example, n number of struct variables are created where n is entered by the user.
ptr = (struct person*) malloc(n * sizeof(struct person));
In-Lab Task 2:
The program in Code Listing 1 writes a single record to the file. Modify it, to use a function
‘write_records_to_file’ with following prototype:
int write_records_to_file (struct emp * sptr, int num_records, FILE * fptr)
This function should write ’num_records’’ number of structures from a dynamically allocated
memory pointed to by ‘sptr’ to a file pointed to by ‘fptr’. It should return the number of
structures successfully written to the file.
In-Lab Task 3:
int read_records_from_file(struct emp * sptr, int num_records, FILE * fptr)
void print_records(struct emp * sptr, int num_records);
Use these functions to read a previously written file (employees_records2.dat) and display the
contents on screen.
Post-Lab Task:
The structures that you write in a file using ‘fwrite()’ are written in the binary format and cannot be
viewed in a text editor properly. Your task is to write the contents of these structures in the text
format so that the contents may be viewed in a text editor.
#include <stdio.h>
#include <stdlib.h>
/// Define a structure 'emp' to hold data about an employee
struct emp
{
char name[48]; // Name of the employee
int age;
float bs; // Basic Salary as a floating point number.
};
void flush(void);
int write_records_to_file (struct emp * sptr, int num_records, FILE * fptr);
int read_records_from_file(struct emp * sptr, int num_records, FILE * fptr);
void print_records(struct emp * sptr, int num_records);
int main(void)
{
FILE *fp ;
char another = 'Y' ;
struct emp employee;
int i = 0;
int num_rec = 0;
// Open the file for writing in the Binary Mode
fp = fopen ( "employees_records.dat", "wb" ) ;
if ( fp == NULL ){
printf ( "Cannot open file\n" ) ;
exit(0) ;
}
while ( another == 'Y' )
{
printf ( "\nEnter the name of the Employee: " ) ;
fgets (employee.name, 48, stdin);
printf ( "\nEnter the age of the Employee: " ) ;
scanf("%d", &employee.age);
printf ( "\nEnter the Basic Salary of the Employee: " ) ;
scanf("%f", &employee.bs ) ;
// Writing to file
fwrite ( &employee, sizeof ( struct emp ), 1, fp );
flush();
printf ( "Add another record (Y/N) " );
another = getchar() ;
flush();
i++;
}
fclose(fp);
return(0);
}
Code Listing 1
Lab 02 Singly Linked List Implementation
Objectives:
Pre-Lab
Reading Task 1:
A linked list, in simple terms, is a linear collection of data elements. These data elements are called
nodes. Linked list is a data structure which in turn can be used to implement other data Learning
Objective A linked list is a collection of data elements called nodes in which the linear representation
is given by links from one node to the next node. In this chapter, we are going to discuss different
types of linked lists and the operations that can be performed on these lists. A linked list can be
perceived as a train or a sequence of nodes in which each node contains one or more data fields and
a pointer to the next node.
In Figure 1, we can see a linked list in which every node contains two parts, an integer and a pointer
to the next node. The left part of the node which contains data may include a simple data type, an
array, or a structure. The right part of the node contains a pointer to the next node (or address of the
next node in sequence). The last node will have no next node connected to it, so it will store a special
value called NULL . In Fig. 6.1, the NULL pointer is represented by X . While programming, we
usually define NULL as –1. Hence, a NULL pointer denotes the end of the list. Since in a linked list,
every node contains a pointer to another node which is of the same type, it is also called a self-
referential data type.
Linked lists contain a pointer variable START that stores the address of the first node in the list. We
can traverse the entire list using START which contains the address of the first node; the next part of
the first node in turn stores the address of its succeeding node. Using this technique, the individual
nodes of the list will form a chain of nodes. If START = NULL , then the linked list is empty and
contains no nodes.
For further discussion on Linked Lists and their implementation read Chapter 6 of the book “Data
Structures using C”, Oxford University Press, 2nd Edition by Reema Thareja.
In-Lab Tasks:
You are given the following three files for this lab;
3. ‘employee.h’ // This file contains the structure definition and prototypes of functions.
In-Lab Task 1:
Make a new project and add the above mentioned files to this project. Compile and run the program.
Use the function ‘int getListLength(struct employee * emp)’ defined in employee.c to print
the length of the linked list. You will have to add an option in the menu so that the user may see the
length of the list any time he wishes.
In-Lab Task 2:
Write a function to implement ‘search by key’ feature for the linked list. This function should be able
to search the database by ‘age’ and list the appropriate records. It is okay at this stage if only the first
record with the selected age is displayed.
Post-Lab Task:
Write appropriate function to implement the delete function. This function should allow the user to
delete the last node in the list.
Make sure that you deallocate the memory for the deleted node (using C function free()).
In C, we can implement a linked list using the following code:
Objectives:
Pre-Lab
Reading Task 1:
Read linked lists node insertion and deletion topics (page 167 to 180) from the book “Data
Structures using C” by Reema Thareja, 2nd Edition. Some excerpts from the book are listed here for
convenience.
New nodes can be inserted in a linked list in a variety of ways. Some of the cases are listed below:
If a new node is to be inserted at the beginning of a linked list, following steps should be performed:
• Allocate space for new node (unless system runs out of memory) and get its data.
• Let the ‘next’ part of new node contain the address of ‘start’
• Make ‘start’ point to the new node (which is now the first node in the list).
If a new node is to be inserted at the end of a linked list, following steps should be performed:
• Allocate space for new node (unless system runs out of memory) and get its data.
• Take a pointer which is pointing to the start of the list.
• Move this pointer to the last node of the list.
• Store the address of the new node in the ‘next’ part of this pointer.
• Now this new node is the last node in the list.
• Assign the ‘next’ pointer of the las node as NULL.
Read through rest of the section for the remaining two cases of node insertion in the linked list.
Deleting Nodes from a Linked List:
Nodes from an existing list can be deleted in a number of ways. Following are the most common
cases when deleting nodes from a linked list.
Case 1: The first node is to be deleted.
Case 2: The last node is to be deleted.
Case 3: The node after a given node is to be deleted.
Case 1: The first node is to be deleted.
When a the first node is to be deleted from the list following steps should be performed.
• Make ‘start’ point to the next node in the list. (but first copy its address in pointer).
• De-allocate the memory for the previous first node (using the above mentioned pointer).
For writing in file, it is easy to write string or int to file using fprintf and putc, but you might have
faced difficulty when writing contents of struct. fwrite and fread make task easier when you want to
write and read blocks of data. Following is a sample C program used to write structures to file using
‘fwrite’ function.
// a struct to read and write
struct person
{
int id;
char fname[20];
char lname[20];
};
int main ()
{
FILE *fptr;
int count = 0;
// open file for writing
fptr = fopen ("person.dat", "wb");
if (fptr == NULL)
{
printf("\nError opening file!!\n");
exit (1);
}
struct person input1 = {1, "Kamran", "Asghar"};
struct person input2 = {2, "Jameel", "Ahmed"};
// write struct to file
count += fwrite(&input1, sizeof(struct person), 1, fptr);
count += fwrite(&input2, sizeof(struct person), 1, fptr);
if(count != 0)
printf("contents to file written successfully !\n");
else
printf("Error writing to file !\n");
// close file
fclose (fptr);
return 0;
}
https://www.geeksforgeeks.org/readwrite-structure-file-c/
In-Lab Tasks:
You are given the following four files for this lab;
As you would know that each node in a singly linked list consists of a data part and a pointer to the
next node in the list. Our implementation defines the node as follows (in file ‘Node.h’).
struct employee
{
char name[50];
int age;
float bs;
};
struct node
{
struct employee data;
struct node * next;
};
You would notice that we have made the data part as a separate structure. This will allow us to write
generic functions for linked list modifications even if there are changes in the ‘data’ part. Moreover
it will allow us to store/load our database to/from a file much more conveniently.
In-Lab Task 1:
‘Inserting nodes at the end’ and ‘inserting node after a given node’ are already implemented in
‘SinglyLinkedList.c’. Your task is to implement ‘insert at the beginning’ and ‘insert before’
functions in the file ‘SinglyLinkedList.c’.
In-Lab Task 2:
Deleting a node from the end is already implemented in ‘SinglyLinkedList.c’ your task is to
implement ‘delete from beginning’ and ‘delete after’ a given node.
Post-Lab Task:
Reading database from a file on the hard disk is already implemented. Your first task is to study and
understand this implementation. Then you will have to implement the write to file function
‘saveListToFile()’. Submit a report on your implementation.
Objectives:
Introduction
In a circular linked list, the last node contains a pointer to the first node of the list. We can have a
circular singly linked list as well as a circular doubly linked list. While traversing a circular linked
list, we can begin at any node and traverse the list in any direction, forward or backward, until we
reach the same node where we started. Thus, a circular linked list has no beginning and no ending.
Figure 3.1 shows a circular linked list
The only downside of a circular linked list is the complexity of iteration. Note that there are no
NULL values in the NEXT part of any of the nodes of list. Circular linked lists are widely used in
operating systems for task maintenance. We will now discuss an example where a circular linked
list is used. When we are surfing the Internet, we can use the Back button and the Forward button to
move to the previous pages that we have already visited. How is this done? The answer is simple. A
circular linked list is used to maintain the sequence of the Web pages visited. Traversing this
circular linked list either in forward or backward direction helps to revisit the pages again using
Back and Forward buttons. Actually, this is done using either the circular stack or the circular
queue. Consider Fig. 3.2. We can traverse the list until we find the NEXT entry that contains the
address of the first node of the list. This denotes the end of the linked list, that is, the node that
contains the address of the first node is actually Figure 3.2 Memory representation of a circular
linked list Linked Lists 181 the last node of the list. When we traverse the DATA and NEXT in this
manner, we will finally see that the linked list in Fig. 3.2 stores characters that when put together
form the word HELLO. Now, look at Fig. 3.3. Two different linked lists are simultaneously
maintained in the memory. There is no ambiguity in traversing through the list because each list
maintains a separate START pointer which gives the address of the first node of the respective
linked list.
Figure 3.2 Memory representation
The remaining nodes are reached by looking at the value stored in NEXT. By looking at the figure,
we can conclude that the roll numbers of the students who have opted for Biology are S01, S03,
S06, S08, S10, and S11. Similarly, the roll numbers of the students who chose Computer Science
are S02, S04, S05, S07, and S09.
Pre Lab:
In this section, we will see how a new node is added into an already existing linked list.
We will take two cases and then see how insertion is done in each case.
Case 1: The new node is inserted at the beginning of the circular linked list.
Case 2: The new node is inserted at the end of the circular linked list.
Figure 3.4 Inserting a new node at the beginning of a circular linked list
Figure 3.5 shows the algorithm to insert a new node at the beginning of a linked list. In Step 1, we
first check whether memory is available for the new node. If the free memory has exhausted, then
an OVERFLOW message is printed. Otherwise, if free memory cell is available, then we allocate
space for the new node. Set its DATA part with the given VAL and the NEXT part is initialized with
the address of the first node of the list, which is stored in START. Now, since the new node is added
as the first node of the list, it will now be known as the START node, that is, the START pointer
variable will now hold the address of the NEW_NODE. While inserting a node in a circular linked
list, we have to use a while loop to traverse to the last node of the list. Because the last node
contains a pointer to START, its NEXT field is updated so that after insertion it points to the new
node which will be now known as START.
Figure 3.5 Algorithm to insert a new node at the beginning
Figure 3.7 shows the algorithm to insert a new node at the end of a circular linked list. In Step 6, we
take a pointer variable PTR and initialize it with START. That is, PTR now points to the first node
of the linked list. In the while loop, we traverse through the linked list to reach the last node. Once
we reach the last node, in Step 9, we change the NEXT pointer of the last node to store the address
of the new node. Remember that the NEXT field of the new node contains the address of the first
node which is denoted by START.
Deleting the First Node from a Circular Linked List Consider the circular linked list shown in Fig.
3.8. When we want to delete a node from the beginning of the list, then the following changes will
be done in the linked list.
Figure 3.8 Deleting the first node from a circular linked list
Deleting the Last Node from a Circular Linked List
Figure 3.9 shows the algorithm to delete the first node from a circular linked list. In Step 1 of the
algorithm, we check if the linked list exists or not. If START = NULL, then it signifies that there are
no nodes in the list and the control is transferred to the last statement of the algorithm. However, if
there are nodes in the linked list, then we use a pointer variable PTR which will be used to traverse
the list to ultimately reach the last node. In Step 5, we change the next pointer of the last node to
point to the second node of the circular linked list. In Step 6, the memory occupied by the first node
is freed. Finally, in Step 7, the second node now becomes the first node of the list and its address is
stored in the pointer variable START.
Consider the circular linked list shown in Fig. 3.10. Suppose we want to delete the last node from
the linked list, then the following changes will be done in the linked list.
Figure 3.11 shows the algorithm to delete the last node from a circular linked list. In Step 2, we take
a pointer variable PTR and initialize it with START. That is, PTR now points to the first node of the
linked list. In the while loop, we take another pointer variable PREPTR such that PREPTR always
points to one node before PTR. Once we reach the last node and the second last node, we set the
next pointer of the second last node to START, so that it now becomes the (new) last node of the
linked list. The memory of the previous last node is freed and returned to the free pool.
Figure 3.10 Deleting the last node from a circular linked list
int main()
{
struct node * top = NULL; /// This is the top of the stack
struct element d1, d2, d3;
d1.d = 10;
d1.d_type = 0;
d2.ch = 'A';
d2.d_type = 1;
d3.d = 45;
d3.d_type = 0;
push(&top, d1);
push(&top, d3);
push(&top, d2);
for(int i = 0; i<3; i++)
{
struct element temp;
temp = pop(&top);
if(temp.d_type == 0)
printf("\nThe data popped is %d", temp.d);
else
printf("\nThe data popped is %c", temp.ch);
}
return 0;
}
CodeListing 1: main.c Stack usage example
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
#include "stack_functions.h"
struct element pop(struct node ** top)
{
struct element temp = (*top)>data; /// I copy the data at the top node into a temporary variable
struct node * ptr_temp = (*top)>next;
free(*top);
*top = ptr_temp;
return(temp);
}
void push(struct node ** top, struct element new_data)
{
struct node * new_node = (struct node *) malloc(sizeof(struct node));
new_node>data = new_data; /// I can assign one struct to another if the type is the same
new_node>next = * top;
* top = new_node;
}
CodeListing 2: stack_functions.c A linked list implementation of stack
struct element
{
int d; /// To store data
char ch; /// To store, brackets and operators
int d_type; /// To tell whether an integer (0) or
/// a charachter (1) is pushed onto
/// the stack
};
struct node
{
struct element data;
struct node * next;
};
CodeListing 3: node.h Structure definitions for stack elements
Lab 06 Queue Implementation with Applications
Learning Outcomes
After completing the lab students will be able to:
• Implement queues using linked lists.
• Use queues to solve certain problems such as finding the shortest path in a graph.
Pre-Lab Reading:
A queue is a special case of a singly-linked list which works according to First In First Out (FIFO)
algorithm. An array implementation of a queue is also possible but it is not discussed here as it does
not allow dynamic memory allocation.
Data is inserted at one end called the ‘Rear’ of the queue and deleted from the other end called the
‘Front’ of the queue. Operations supported in a queue are:
Enqueue (insert) − Insert data at the ‘Rear’ of the queue.
Dequeue (delete) − Delete a data element from the ‘Front’ of the queue.
Peek − Read the element at the Front of the queue without removing it.
Read Chapter 8 from “Data Structures using C”, by Reema Thareja, 2 nd edition, for more
information on queues, priority queues, their implementation and applications.
Skeleton code for queues is provided with this lab. As a pre-lab task implement the basic queue
functions (enqueue, dequeue and peek). You will need them later on for completing the in-lab tasks.
Your implementation of these functions should make sure that no restrictions (prior pointer settings
for boundary cases) are enforced on the way the enqueue and dequeue functions are called.
Making a new queue should be as simple as declaring two pointers for the front and rear of the
queue and call enqueue() and dequeue() functions to add/remove elements from the queue.
Pre-lab Task : Complete the functions ‘enqueue()’ , ‘dequeue()’ and peek() functions.
You are provided skeleton code for basic Queue implementation functions, enqueue(), dequeue()
and peek(). Peek and enqueue are already implemented. You have to complete the dequeue()
function. Also write the main() function to demonstrate correct working of the queue functions.
In-Lab Task 1: Implement a priority queue.
Implement a priority queue with following functions.
void pr_enqueue(struct node ** front, struct node ** rear, struct element new_data);
struct element pr_dequeue(struct node ** front); // This function has been implemented
int pr_isEmpty(struct node ** front); // This function has been implemented
This implementation constructs the queue in a sorted manner. This means that when removing items
from the queue we only need to remove the first item from the front end. But when we want to add
an item (using enqueue) we need to place it to its proper location based on its priority.
Skeleton code is provided. You will have to implement only the ‘enqueue()’ function.
In-Lab Task 2: Find the Shortest Path in Graphs Using BFS and Queues.
For this task you are provided with the skeleton code that generates data into a 2D array populating
it randomly with zeros (0) and ones (1). There will be more zeros than ones. This is by design. The
code finds the shortest path between the source cell and the destination cell. The only movements
allowed in the grid are top, bottom, right and left. This means only immediate four neighbors of a
given cell can be visited. In the following figure the distance between the source (S) cell and the
destination (D) cell is 9 steps. This calculation does not take into account the contents of the cells.
We maintain a queue to store the coordinates of the matrix and initialize it with the source cell.
We also maintain an integer array ‘visited’ of same size as our input matrix and initialize all its elements to 0.
We also maintain an integer array ‘dist’ of same size as our input matrix and initialize all its elements to 1000.
Return the value in dist array cell if the destination coordinates match.
For each of its four adjacent cells, if they are not visited yet, we enqueue it in the queue and also mark them as
visited. We also add 1 to the dist array for the visited cell.
Your task is to modify the given code and find the cost of moving from the source cell to the
destination cell considering that only cell with a zero ‘0’ may be visited. This way the shortest
path for this particular grid will be 9 as shown in Figure 3.
Your code should return the shortest distance between the source and destination cells or (-1) if no
path exists between them.
References:
1. https://www.youtube.com/watch?v=KiCBXu4P-2Y
2. https://www.geeksforgeeks.org/shortest-path-in-a-binary-maze/
3. https://www.hackerearth.com/practice/algorithms/graphs/breadth-first-search/tutorial/
Lab 07 Implementation of Recursion
Objectives:
Pre-Lab
What is Recursion?
The process in which a function calls itself directly or indirectly is called recursion and the
corresponding function is called as recursive function. Using recursive algorithm, certain problems
can be solved quite easily. Examples of such problems are Towers of Hanoi (TOH),
Inorder/Preorder/Postorder Tree Traversals, DFS of Graph, etc.
In the recursive program, the solution to the base case is provided and the solution of the bigger
problem is expressed in terms of smaller problems.
int fact(int n)
{
if (n < = 1) // base case
return 1;
else
return n*fact(n1);
}
In the above example, base case for n < = 1 is defined and larger value of number can be solved by
converting to smaller one till base case is reached.
The idea is to represent a problem in terms of one or more smaller problems, and add one or more
base conditions that stop the recursion. For example, we compute factorial n if we know factorial of
(n-1). The base case for factorial would be n = 0. We return 1 when n = 0.
If the base case is not reached or not defined, then the stack overflow problem may arise. Let us
take an example to understand this.
Page 1 of 4
int fact(int n)
{
// wrong base case (it may cause
// stack overflow).
if (n == 100)
return 1;
else
return n*fact(n1);
}
If fact(10) is called, it will call fact(9), fact(8), fact(7) and so on but the number will never reach
100. So, the base case is not reached. If the memory is exhausted by these functions on the stack, it
will cause a stack overflow error.
When any function is called from main(), the memory is allocated to it on the stack. A recursive
function calls itself, the memory for a called function is allocated on top of memory allocated to
calling function and different copy of local variables is created for each function call. When the
// A C program to demonstrate working of recursion
#include<stdio.h>
#include<stdlib.h>
void printFun(int test)
{
if (test < 1)
return;
else
{
printf(“%d ”, test);
printFun(test1); // statement 2
printf(“%d ”, test);
return;
}
}
int main()
{
int test = 3;
printFun(test);
}
base case is reached, the function returns its value to the function by whom it is called and memory
is de-allocated and the process continues.
Page 2 of 4
Let us take the example how recursion works by taking a simple function.
When printFun(3) is called from main(), memory is allocated to printFun(3) and a local variable test
is initialized to 3 and statement 1 to 4 are pushed on the stack as shown in below diagram. It first
prints ‘3’. In statement 2, printFun(2) is called and memory is allocated to printFun(2) and a local
variable test is initialized to 2 and statement 1 to 4 are pushed in the stack. Similarly, printFun(2)
calls printFun(1) and printFun(1) calls printFun(0). printFun(0) goes to if statement and it return to
printFun(1). Remaining statements of printFun(1) are executed and it returns to printFun(2) and so
on. In the output, value from 3 to 1 are printed and then 1 to 3 are printed. The memory stack has
been shown in below diagram.
Read more about recursion in the book “Data Structure using C” by Reema Thareja.
In-Lab Task 01 : Convert the following iterative function to a recursive one.
bool test_palindrome_itr(char * test_word)
{
/// Get the length of the string.
/// 'strlen()' returns total length including the NULL character
int length = strlen(test_word)1;
int i;
for(i = 0; i<=(length/2); i++)
{
/// Compare the 1st and the Last letters of the word
if((*(test_word+i)) != (*(test_word+lengthi1)))
break;
}
i; /// 'i' would have been one greater because of the 'i++' in the loop
return(i==(length/2));
}
CodeListing 1: Function to test if a given word is a Palindrome (Iterative method)
Page 3 of 4
Palindromes
A palindrome is a word that is the same when reversed, e.g. ‘madam’. The string can be reversed
using an iterative method (given in CodeListing 1), but there is also a recursive method. A word is
a palindrome if it has fewer than two letters, or if the first and last letter are the same and the middle
part (i.e. without the first and last letters) is a palindrome.
Complete the function ‘bool test_palindrome_itr(char * test_word)’ in the provided skeleton code.
The greatest common divisor of two numbers (integers) is the largest integer that divides both the
numbers. We can find the GCD of two numbers recursively by using the Euclid’s algorithm that
states:
GCD can be implemented as a recursive function because if b does not divide a , then we call the
same function (GCD) with another set of parameters that are smaller than the original ones. Here
we assume that a > b . However if a < b, then interchange a and b in the formula given above. In
CodeListing 2 you are given a recursive implementation.
int GCD_rec(int x, int y)
{
int rem;
rem = x%y;
if(rem==0)
return y;
else
return (GCD_rec(y, rem));
}
CodeListing 2: Recursive implementation of finding GCD
Post Lab
Page 4 of 4
Lab 08 Implementation of Sorting Algorithms
Learning Objectives
Pre-Lab Reading
Bubble sort is a very simple method that sorts the array elements by repeatedly moving the largest
element to the highest index position of the array segment (in case of arranging elements in
ascending order). In bubble sort, consecutive adjacent pairs of elements in the array are compared
with each other. If the element at the lower index is greater than the element at the higher index, the
two elements are interchanged so that the element is placed before the bigger one. This process will
continue till the list of unsorted elements exhausts.
This procedure of sort is called bubble sort because elements ‘bubble’ to the top of the list. Note
that at the end of the first pass, the largest element in the list will be placed at its proper position
(i.e., at the end of the list).
Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item at a
time. It is much less efficient on large lists than more advanced algorithms such as quick sort, heap
sort, or merge sort. However, insertion sort provides several advantages like simplicity, efficiency
on small datasets, more efficient compare to selection sort and bubble sort, in-place sorting and
stability.
Insertion sort iterates, consuming one input element each repetition, and growing a sorted output
list. At each iteration, insertion sort removes one element from the input data, finds the location it
belongs within the sorted list, and inserts it there. It repeats until no input elements remain.
Sorting is typically done in-place, by iterating up the array, growing the sorted list behind it. At each
array-position, it checks the value there against the largest value in the sorted list (which happens to
be next to it, in the previous array-position checked). If larger, it leaves the element in place and
moves to the next. If smaller, it finds the correct position within the sorted list, shifts all the larger
values up to make a space, and inserts into that correct position.
i ← 1
while i < length(A)
j ← i
while j > 0 and A[j1] > A[j]
swap A[j] and A[j1]
j ← j 1
end while
i ← i + 1
end while
Algorithm 3: Insertion Sort
All of the comparison-based sorting algorithms that we've seen thus far have sorted the array in
place (used only a small amount of additional memory). Mergesort is a sorting algorithm that
requires an additional temporary array of the same size as the original one. It needs O(n) additional
space, where n is the array size. It is based on the process of merging two sorted arrays into a single
sorted array.
To merge sorted arrays A and B into an array C, we maintain three indices, which start out on the
first elements of the arrays:
We repeatedly do the following:
• compare A[i] and B[j]
• copy the smaller of the two to C[k]
• increment the index of the array whose element was copied
• increment k
The divide and conquer approach of the algorithm first divides the input array into two sub-arrays
and calls itself recursively for each sub-array. The base case is when we are left with one element.
Then it merges the two returning sub-arrays using the algorithm depicted in Figures 1 and Figure 2.
The complete process is shown in Figure 3.
Figure 3: Working of Merge Sort
For further insight, read chapter 14.7 to 14.10 (Pages 434 – 445) from the book “Data Structures
using C”, by Reema Thareja, 2nd Edition.
In-Lab Task 1: Complete the functions for Selection Sort and Insertion Sort
You are provided with the skeleton code for this lab. This program uses Bubble Sort, Selection Sort,
Insertion Sort and Merge Sort to sort an array of integers. Bubble Sort implementation is already
completed and is provided for reference so that you may familiarize yourselves with the working of
the program.
The program computes the time taken by the sorting function and displays this after sorting has
finished. Your task is to complete the Selection Sort and Insertion Sort parts of the code.
In-Lab Task 2: Empirically compute the execution times for the three sorting methods
Here you will run the program with different data sizes and for different sorting techniques, while
compiling the resulting execution times in the table provided below.
Data Size ↓ Bubble Sort Selection Sort Insertion Sort Merge Sort
1 16
2 128
3 1024
4 16384
5 131072
Post Lab Task: Complete the Merge Sort Function and empirically determine its time complexity.
Lab 09 Quick and Merge Sort Implementation
Learning Outcomes:
After successfully completing this lab the students will be able to:
1. Understand the divide and conquer mechanism which is at the heart of the two recursive
sorting algorithm.
2. Develop programming solutions for Merge Sort and Quick Sort
3. Empirically compare the performance of these two sorting algorithms
Quick Sort:
Quick sort is a widely used sorting algorithm developed by C. A. R. Hoare that makes O(nlogn)
comparisons in the average case to sort an array of n elements. For the worst case, it has a
quadratic running time given as O(n2), however its efficient implementation can minimize the
probability of requiring quadratic time. The major advantage of using Quick Sort is that it sorts the
array ‘in-place’. This means that it does not require additional memory space to store data.
Compare this to the Merge Sort algorithm which requires O(n) additional memory for sorting in
O(nlogn) time.
Like merge sort, this algorithm works by using a divide-and-conquer strategy to divide a single
unsorted array into two smaller sub-arrays. The quick sort algorithm works as follows:
Rearrange the elements in the array in such a way that all elements that are less than the pivot
appear before the pivot and all elements greater than the pivot element come after it (equal values
can go either way). After such a partitioning, the pivot is placed in its final position. This is called
the partition operation.
Recursively sort the two sub-arrays thus obtained. (One with sub-list of values smaller than that of
the pivot element and the other having higher value elements.)
Like merge sort, the base case of the recursion occurs when the array has zero or one element
because in that case the array is already sorted.
After each iteration, one element (pivot) is always in its final position. Hence, with every iteration,
there is one less element to be sorted in the array.
Thus, the main task is to find the pivot element, which will partition the array into two halves. To
understand how we find the pivot element, follow the steps given below in Algorithm 1. (We take
the last element in the array as pivot.)
Algorithm 1: Partition function which chooses the last element as pivot
The algorithm for the Quick Sort function with recursive calls to itself and the partition function is
given below in Algorithm 2.
In-Lab Tasks
You are given skeleton code for this lab. Your task is to complete the merge() and partition()
functions for Merge Sort and Quick Sort respectively.
Post Lab
Study and perform comparative analysis between different sorting algorithms we have implemented
in current and previous Lab.
Learning Outcomes:
After successfully completing this lab the students will be able to:
1. Understand the properties of Binary Search Trees and their associated functions.
2. Develop C programs for implementing Binary Search Trees and their associated functions.
3. Use BSTs to load/store data to/from a file on the hard disk.
The above properties of Binary Search Tree provide an ordering among keys so that the operations
like search, minimum and maximum can be done fast. If there is no ordering, then we may have to
compare every key to search a given key.
Searching a key
To search a given key in Binary Search Tree, we first compare it with root, if the key is present at
root, we return root. If key is greater than root’s key, we recur for right subtree of root node.
Otherwise we recur for left subtree.
Insertion of a key
A new key is always inserted at leaf. We start searching a key from root till we hit a leaf node. Once
a leaf node is found, the new node is added as a child of the leaf node.
Time Complexity:
The worst case time complexity of search and insert operations is O(h) where h is height of Binary
Search Tree. In worst case, we may have to travel from root to the deepest leaf node. The height of
a skewed tree may become n and the time complexity of search and insert operation may become
O(n).
Deleting a Node from the tree:
When we delete a node, three possibilities arise.
1. Node to be deleted is leaf.
◦ Simply remove from the tree.
2. Node to be deleted has only one child
◦ Copy the child to the node and delete the child
3. Node to be deleted has two children
◦ Find in-order successor of the node. Copy contents of the in-order successor to the node
and delete the in-order successor. Note that in-order predecessor can also be used.
The important thing to note is, in-order successor is needed only when right child is not empty. In
this particular case, in-order successor can be obtained by finding the minimum value in right child
of the node.
Time Complexity
The worst case time complexity of delete operation is O(h) where h is height of Binary Search Tree.
In worst case, we may have to travel from root to the deepest leaf node. The height of a skewed tree
may become n and the time complexity of delete operation may become O(n)
For more information read Chapter 10 from the book: “Data Structures using C” by Reema
Thareja.
In-Lab Tasks:
You are provided with skeleton code that builds a Binary Search Tree by adding 10 nodes to it.
Functions for node insertion and printing the tree (in-order traversal only) are already implemented.
Your task is to complete the following functions.
1. Node deletion
2. Node search
3. Pre-Order and Post-Order printing
Post-Lab Tasks:
Complete the following functions for the BST:
1. Save the Tree data to a file (In-Order, Pre-Order and Post-Order)
2. Load tree from a file containing numbers.
Lab 11 AVL Trees Implementation
Learning Outcomes:
After successfully completing this lab the students will be able to:
For more information read Chapter 10.4 from the book: “Data Structures using C” by Reema
Thareja.
In-Lab Tasks:
You are provided with skeleton code that builds a Binary Search Tree by adding 10 nodes to it.
Functions for node insertion and printing the tree (in-order traversal only) are already implemented.
Your task is to modify the insert function to incorporate AVL insertion. You will find Programming
Example on Page 324 of the above-mentioned book useful.
Post-Lab Tasks:
Complete the following functions for the BST:
1. Add the delete node functionality.
Lab 12 Binary Heaps Implementation
Learning Outcomes:
After successfully completing this lab the students will be able to:
Figure 12.1 shows a binary min heap and a binary max heap. The properties of binary heaps are
given as follows:
Since a heap is defined as a complete binary tree, all its elements can be stored sequentially in an
array. It follows the same rules as that of a complete binary tree. That is, if an element is at position
i in the array, then its left child is stored at position 2i and its right child at position 2i+1.
Conversely, an element at position i has its parent stored at position i/2.
Being a complete binary tree, all the levels of the tree except the last level are completely filled.
The height of a binary tree is given as log2n, where n is the number of elements. Heaps (also
known as partially ordered trees) are a very popular data structure for implementing priority queues.
A binary heap is a useful data structure in which elements can be added randomly but only the
element with the highest value is removed in case of max heap and lowest value in case of min
heap. A binary tree is an efficient data structure, but a binary heap is more space efficient and
simpler.
Page 1 of 3
Consider a max heap H with n elements. Inserting a new value into the heap is done in the
following two steps:
1. Add the new value at the bottom of H in such a way that H is still a complete binary tree but
not necessarily a heap.
2. Let the new value rise to its appropriate place in H so that H now becomes a heap as well.
To do this, compare the new value with its parent to check if they are in the correct order. If they
are, then the procedure halts, else the new value and its parent’s value are swapped and Step 2 is
repeated.
The first step says that insert the element in the heap so that the heap is a complete binary tree. So,
insert the new value as the right child of node 27 in the heap.
Now, as per the second step, let the new value rise to its appropriate place in H so that H becomes a
heap as well. Compare 99 with its parent node value. If it is less than its parent’s value, then the
new node is in its appropriate place and H is a heap. If the new value is greater than that of its
parent’s node, then swap the two values. Repeat the whole process until H becomes a heap.
Deleting an Element from a Binary Heap
Consider a max heap H having n elements. An element is always deleted from the root of the heap.
So, deleting an element from the heap is done in the following three steps:
1. Replace the root node’s value with the last node’s value so that H is still a complete binary
tree but not necessarily a heap.
2. Delete the last node.
3. Sink down the new root node’s value so that H satisfies the heap property. In this step,
interchange the root node’s value with its child node’s value (whichever is largest among its
children). Here, the value of root node = 54 and the value of the last node = 11. So, replace
54 with 11 and delete the last node.
This is illustrated in Figure 12.3 and 12.4 below.
Page 2 of 3
Figure12. 3: The root node
always gets deleted
Page 3 of 3
Lab 13 Implementation of Graphs in C language
Objectives:
• To understand the concept of weighted and unweighted graph data structures.
• Learn to construct a graph using C language.
• Learn to perform breadth-first and depth-first graph traversals.
• Learn to implement insertion and deletion of vertices and edges in a graph.
Pre Lab:
A graph is a pictorial representation of a set of objects where some pairs of objects are connected
by links. The interconnected objects are represented by points termed as vertices, and the links that
connect the vertices are called edges.
Formally, a graph is a pair of sets (V, E), where V is the set of vertices and E is the set of edges,
connecting the pairs of vertices.
In the above graph, V = {a, b, c, d, e} and E = {ab, ac, bd, cd, de}
Graphs are used to solve many real-life problems. Graphs are used to represent networks. The
networks may include paths in a city or telephone network or circuit network etc. Consider a
network of flights for PIA shown below. Here the cities, Islamabad, Karachi, Rome etc form the
vertices of the graph, while the paths linking these vertices are the edges of the graph.
Page 1 of 5
Types of graphs:
While nodes and edges may have any number of interesting properties and labels, some properties
are more common than others. In particular there are two properties of edges that stand out so much
that they are said to change the type of graph. These two properties are edge weight, and edge
directionality.
Directed vs Undirected Graphs:
If the edges in your graph have directionality then your graph is said to be a directed graph
(sometimes shortened to digraph). In a directed graph all of the edges represent a one way
relationship, they are a relationship from one node to another node — but not backwards. In an
undirected graph all edges are bidirectional. It is still possible (even common) to have bidirectional
relationships in a directed graph, but that relationship involves two edges instead of one, an edge
from A to B and another edge from B to A.
Directed edges have a subtle impact on the use of the term neighbors. If an edge goes from A to B,
then B is said to be A’s neighbor; but the reverse is not true. A is not a neighbor of B unless there is
an edge from B to A. In other words, a node’s neighbors are the set of nodes that can be reached
from that node.
Let’s use two social networks as examples. On Facebook the graph of friends is undirected. If you
are someone’s friend on Facebook they are your friend too — friendship on Facebook is always
bidirectional meaning the graph representation is undirected. On Twitter, however, “following”
someone is a one way relationship. If you follow Shahid Afridi, that doesn’t mean he follows you.
The graph of Twitter users and their followers is a directed graph.
Page 2 of 5
magnitude is important to the relationship we’re studying. In an unweighted graph the existence of a
relationship is the subject of our interest.
As an example of a weighted graph, imagine you run an airline and you’d like a model to help you
estimate fuel costs based on the routes you fly. In this example the nodes would be airports, edges
would represent flights between airports, and the edge weight would be the estimated cost of flying
between those airports. Such a model could be used to determine the cheapest path between two
cities, or run simulations of different potential flight offerings.
Representation of Graphs
There are several ways to represent graphs, each with its advantages and disadvantages. Some
situations, or algorithms that we want to run with graphs as input, call for one representation, and
others call for a different representation. Here, we'll see three ways to represent graphs. Lets
consider the following graph:
Adjacency List:
Representing a graph with adjacency lists combines adjacency matrices with edge lists. For each
vertex iii, store an array of the vertices adjacent to it. We typically have an array of |V|∣V ∣vertical
bar, V, vertical bar adjacency lists, one adjacency list per vertex. Here's an adjacency-list
representation of the social network graph:
Page 3 of 5
Figure 6: Adjacency List for
Graph of Figure 5
Adjacency Matrix:
For a graph with |V|∣V∣vertical bar, V, vertical bar vertices, an adjacency matrix is a |V| \times |V|
∣V∣×∣V∣vertical bar, V, vertical bar, times, vertical bar, V, vertical bar matrix of 0s and 1s, where the
entry in row iii and column jjj is 1 if and only if the edge (i,j)(i,j)left parenthesis, i, comma, j, right
parenthesis is in the graph. If you want to indicate an edge weight, put it in the row iii, column jjj
entry, and reserve a special value (perhaps null) to indicate an absent edge. Here's the adjacency
matrix for the social network graph:
The adjacency matrix for a weighted graph will have the weights in place of ones (1s). A
programming implementation may initialize the adjacency matrix with a negative number to denote
and infinite weight (not adjacent).
Depth First Search (DFS):
The DFS algorithm is a recursive algorithm that uses the idea of backtracking. It involves
exhaustive searches of all the nodes by going ahead, if possible, else by backtracking.
Page 4 of 5
Here, the word backtrack means that when you are moving forward and there are no more nodes
along the current path, you move backwards on the same path to find nodes to traverse. All the
nodes will be visited on the current path till all the unvisited nodes have been traversed after which
the next path will be selected.
This recursive nature of DFS can be implemented using stacks. The basic idea is as follows:
1. Pick a starting node and push all its adjacent nodes into a stack.
2. Pop a node from stack to select the next node to visit and push all its adjacent nodes into a
stack.
3. Repeat this process until the stack is empty. However, ensure that the nodes that are visited
are marked. This will prevent you from visiting the same node more than once. If you do not
mark the nodes that are visited and you visit the same node more than once, you may end up
in an infinite loop.
In-Lab Tasks:
Task 1:
Complete the Adjacency Matrix given as ‘my_graph[][]’ in the main function of the skeleton
code provided. You will use the following figure for completing this matrix. Call the function
‘add_edge()’ with correct weights to fill in the matrix.
Page 5 of 5