Download as pdf or txt
Download as pdf or txt
You are on page 1of 52

Lab 01 Revision of programming concepts of C language

Objectives:

• To revise definition and declaration (instantiation) of Structures in C.


• To revise Pointers in C and their use with Structures.
• To revise file handling in C and writing/reading Structure arrays to/from files.

Reading Task 1: Working with Structures in C

In C programming, a struct (or structure) is a collection of variables (can be of different types)


under a single name.

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.

Creating Structure Variables:

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.

Here's how we create structure 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.

Accessing Members of a structure:

There are two types of operators used for accessing members of a structure.

Dot (.) - Member operator


Arrow (->) - Structure pointer operator (will be discussed in the next section)

Suppose, we want to access the salary of person2. Here's how we can do it.

person2.salary

C Pointers to structures:

Here's how we can create 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.

Example: Dynamic memory allocation of structs


#include <stdio.h>
#include <stdlib.h>
struct person {
   int age;
   float weight;
   char name[30];
};
int main()
{
   struct person *ptr;
   int i, n;
   printf("Enter the number of persons: ");
   scanf("%d", &n);
   // allocating memory for n numbers of struct person
   ptr = (struct person*) malloc(n * sizeof(struct person));
   for(i = 0; i < n; ++i)
   {
       printf("Enter first name and age respectively: ");
       // To access members of 1st struct person,
       // ptr­>name and ptr­>age is used
       // To access members of 2nd struct person,
       // (ptr+1)­>name and (ptr+1)­>age is used
       scanf("%s %d", (ptr+i)­>name, &(ptr+i)­>age);
   }
   printf("Displaying Information:\n");
   for(i = 0; i < n; ++i)
       printf("Name: %s\tAge: %d\n", (ptr+i)­>name, (ptr+i)­>age);
   return 0;
}

When we run the program, the output will be:

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.

To allocate the memory for n number of struct person, we used,

ptr = (struct person*) malloc(n * sizeof(struct person));

Then, we used the ptr pointer to access elements of person.


In-Lab Task 1:

Build and run the program given in Code Listing 1.

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:

Write functions ‘read_records_from_file’, and ‘print_records’ with following prototypes

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:

• Learn to create a singly linked list.


• Learn to insert nodes at the end/start of a singly linked list.
• Learn to implement other simple linked list operations such as searching the list for key, find
the length of the linked list etc.

Pre-Lab

Reading Task 1:

Working with Linked Lists in C:

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.

Figure 1: Visual depiction of a singly linked list

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;

1. ‘main.c’ // This file contains the main application (menu based)

2. ‘employee.c’ // This file contains the functions to implement linked list

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:

CodeListing 1: A simple implementation of a linked list


Lab 03 Advanced Topics in Singly Linked List Implementation

Objectives:

• Learn to insert nodes in a linked list.


• Learn to delete nodes from the linked list.
• Learn to store/load database to/from a file.

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.

Inserting Nodes in a Linked List:

New nodes can be inserted in a linked list in a variety of ways. Some of the cases are listed below:

Case 1: The new node is inserted at the beginning.


Case 2: The new node is inserted at the end.
Case 3: The new node is inserted after a given node.
Case 4: The new node is inserted before a given node.

Case 1: The new node is inserted at the beginning:

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).

Figure 1: Inserting a new node at the beginning of the list


Case 2: The new node is inserted at the end:

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.

Figure 2: Inserting new node to the end of a linked list

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).

Figure 3: Deleting first node from the list

Case 2: The last node is to be deleted.


When the last node is to be deleted from the list following steps should be performed.
• Declare an pointer ‘ptr’ pointing to the first node in the list.
• Move this pointer to the second-last node in the array.
• De-allocate the memory for the last node.
• Make the ‘next’ pointer of the previous second-last node equal to NULL.

Figure 4: Deleting the last node from the list

Read rest of the description of deleting nodes from the book.


Storing Data to file:

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; 

CodeListing 1: Example program for writing structures to file.

Further explanation can be found at the following link:

https://www.geeksforgeeks.org/readwrite-structure-file-c/
In-Lab Tasks:

You are given the following four files for this lab;

1. ‘main.c’ // This file contains the main application (menu based)


2. ‘SinglyLinkedList.c’ // This file contains the functions to implement linked list.
3. ‘SinglyLinkedList.h’ // This file contains the prototypes of functions.
4. ‘Node.h’ // This file contains the structure definitions for the linked list.

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;
};

CodeSnippet 1: Our Definition of the Node

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.

******** End of Document *******


Lab 03 Implementation of Circular Link List in C language

Objectives:

• To create the circular link list.


• To insert and traverse all node of circular link list.
• To solve classical Josephus problem
• To perform insertion and deletion of nodes
• To perform efficient memory usage through circular link list.

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

Figure 3.1 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.

Figure 3.3 Memory representation of two circular

Pre Lab:

Task01: Inserting a New Node in a Circular Linked List

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.

Inserting a Node at the Beginning of a Circular Linked List


Consider the linked list shown in Fig. 3.4. Suppose we want to add a new node with data 9 as the
first node of the list. Then the following changes will be done in the 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

Inserting a Node at the End of a Circular Linked List


Consider the linked list shown in Fig. 3.6. Suppose we want to add a new node with data 9 as the
last node of the list. Then the following changes will be done in the linked list.
Figure 3.6 Inserting a new node at the end of a circular linked list

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.

Task02: Deleting a Node from a Circular Linked List


In this section, we will discuss how a node is deleted from an already existing circular linked list.
We will take two cases and then see how deletion is done in each case. Rest of the cases of deletion
are same as that given for singly linked lists.
Case 1: The first node is deleted.
Case 2: The last node is deleted.
Deleting the First Node from a Circular Linked List
Consider the circular linked list shown in Fig. 3.7. 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.7 Algorithm to insert a new node at the end

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.

Figure 3.9 Algorithm to delete the first node

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

Figure 3.11 Algorithm to delete the last node


In Lab
Task01: Write a program to create a circular linked list.
Task 02: Use the created circular linked list in task 01 to perform insertion and deletion at the
beginning and end of the list using code listing 01,02,03 and 04.
Task 03: Let say there is a group of soldiers surrounded by an overwhelming enemy force. There is
no hope for victory without reinforcement, but there is only a single horse available for escape. The
soldiers agree to a pact to determined which of them is to escape and summon help. They form a
circle and a number n is picked from a hat. One of their names is also picked from a hat. Beginning
with the soldier whose name is picked, they begin to count clockwise around the circle. When the
count reaches n, that soldier is removed from the circle, and the count begin again with the next
soldier. The process continues so that each time the counter reaches n, another soldier is remove
form the circle. Any soldier removed from the circle is no longer counted. The last soldier
remaining is to take the horse and escape. The problem is , given number n, the ordering of the
soldiers in the circle, and the soldier from whom the count begins, to determine the order in which
soldiers are eliminated from the circle and which soldier escapes.
The input to program is the number n and list of names which is clockwise ordering of circle,
beginning with the soldier form whom the count is to start. The last input line contains the string
“end” indicating the end of the input. The program should print the names in order that they are
eliminated and the name of the soldier who escapes.
Post Lab Task: Complete the task 02 of in lab as your post lab.
Lab 05 Stack Implementation with Applications
Learning Outcomes
After completing the lab students will be able to:
• Implement stacks using linked lists.
• Implement stacks of varying data types using linked lists
• Solve different problems using stacks
Pre-Lab Task:
A stack is a special case of a singly-linked list which works according to LIFO algorithm. Access is
restricted to one end, called the top of the stack. Its operations are:
push − push an element onto the top of the stack.
pop − pop an element from the top of the stack.
peek − read the element at the top of the stack without removing it.
Read Chapter 7 from “Data Structures using C”, by Reema Thareja, 2 nd edition for more
information on stacks, their implementation and applications.
Go through the code listings for an implementation of stack using linked lists. Push and pop
functions are already implemented. Implement the peek function to familiarize yourself with this
implementation of stack.
In-lab Task 1: Reversing an array of numbers.
You have to write a function to reverse the order of integers in an array. Call this function from your
main function to demonstrate that this functions successfully reverses (in-place) the contents of the
input array. You should declare and use a stack within this function. The functions for stack
implementation are already given to you. The prototype for the function is given below;
void reverse_num_array(int * num_array);
In-Lab Task 2: Testing if a mathematical expression is balanced or not.
Write a function that checks whether parentheses in a mathematical expression are balanced or not.
The input to the function will be a pointer to a null-terminated array of characters (string). Assume
that all the operands in the expression are single digit numbers. You should call this function from
‘main’ function to demonstrate that it works as intended. The function should return 0 if the
expression is balanced, and index of mismatch otherwise. The function prototype is given below:
int isBalanced(char * ptr_array);
Post-Lab Task: Infix to postfix conversion
Write a function that converts a mathematical expression containing parentheses from infix form to
postfix form. The function should take pointers to both source infix array and destination postfix
array as arguments. You may assume that the maximum array size is fixed. The infix expression will
contain multi digit positive integers only. The function must first check if the brackets in the
expression are balanced. The function prototype is given below:
void infixToPostfix(char * src, char * dst);
/* Program to demonstrate that different data types can be pushed onto the 
stack using structures. Each element contains an extra field 'd_type' to tell 
which data type is being pushed onto the stack.
*/
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
#include "stack_functions.h"

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.

Figure 1: Queue Abstract Data Type

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.

Figure 2: A 2D grid with source and


destination cells marked
The skeleton code provided uses the Lee Algorithm and uses Breadth First Search. The algorithm is
given below.
We start from the source cell and calls BFS procedure.

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.

We LOOP till queue is not empty

Dequeue front cell from the queue

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.

Figure 3: Shortest path shown in blue

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:

• To understand the basics of recursive functions.


• To convert an iterative solution to a problem to its recursive counterpart.
• To understand the advantages and disadvantages of recursion

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.

What is base condition in recursion?

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(n­1);    
}

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.

How a particular problem is solved using recursion?

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.

Why Stack Overflow error occurs in recursion?

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(n­1);
}

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.

How memory is allocated to different function calls in recursion?

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(test­1);    // 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+length­i­1)))
            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.

Task 02 Convert the following recursive function to iterative one.

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

Your task is to write an iterative function for computing GCD.

Post Lab

Write a program to reverse a string using recursion.

******** End of document ********

Page 4 of 4
Lab 08 Implementation of Sorting Algorithms

Learning Objectives

• Learn to implement different sorting algorithms using integer arrays


• Theoretically estimate and compare running times of different sorting algorithms
• Empirically test and compare running times of different sorting algorithms

Pre-Lab Reading

Bubble Sort Algorithm:

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).

Algorithm 1: Bubble Sort

Selection Sort Algorithm:


Selection sort is a sorting algorithm that has a quadratic running time complexity of O(n2) , thereby
making it inefficient to be used on large lists. Although selection sort performs worse than insertion
sort algorithm, it is noted for its simplicity and also has performance advantages over more
complicated algorithms in certain situations. Selection sort is generally used for sorting files with
very large objects (records) and small keys.
Algorithm 2: Selection Sort

Insertion Sort Algorithm:

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[j­1] > A[j]
        swap A[j] and A[j­1]
        j ← j ­ 1
    end while
    i ← i + 1
end while
Algorithm 3: Insertion Sort

Merge Sort Algorithm:

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

Figure 1 Merging two sorted arrays (start)

Figure 2: Merging two sorted arrays (end)

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

Pre-Lab Reading Task:

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:

Select an element pivot from the array elements.

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.

Algorithm 2: The Quick Sort algorithm

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.

******** End of Document ********


Lab 10 Binary Search Tree Implementation

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.

Pre-Lab Reading Task:


Binary Search Trees:
Binary Search Tree is a node-based binary tree data structure which has the following properties:
1. The left subtree of a node contains only nodes with keys lesser than the node’s key.
2. The right subtree of a node contains only nodes with keys greater than the node’s key.
3. The left and right subtree each must also be a binary search tree.

Figure 1: A Binary Search Tree (BST)

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:

1. Understand the properties of AVL Trees and their balancing features.


2. Develop C programs for implementing AVL Trees and their balancing features.

Pre-Lab Reading Task:


AVL Trees:
In an AVL tree, the heights of the two child subtrees of any node differ by at most one; if at any time
they differ by more than one, re-balancing is done to restore this property. Lookup, insertion, and
deletion all take O(log n) time in both the average and worst cases, where n is the number of nodes
in the tree prior to the operation. Insertions and deletions may require the tree to be rebalanced by one
or more tree rotations.

Why AVL Trees?


Most of the BST operations (e.g., search, max, min, insert, delete.. etc) take O(h) time where h is the
height of the BST. The cost of these operations may become O(n) for a skewed Binary tree. If we
make sure that height of the tree remains O(log n) after every insertion and deletion, then we can
guarantee an upper bound of O(log n) for all these operations. The height of an AVL tree is always
O(log n) where n is the number of nodes in the tree
Balance Factor:
The balance factor of any node of an AVL tree is in the integer range [-1,+1]. If after any modification
in the tree, the balance factor becomes less than −1 or greater than +1, the subtree rooted at this node
is unbalanced, and a rotation is needed.
Balance Factor = height(left subtree) - height(right subtree)
Insertion
To make sure that the given tree remains AVL after every insertion, we must augment the standard
BST insert operation to perform some re-balancing. Following are two basic operations that can be
performed to re-balance a BST without violating the BST property (keys(left) < key(root) <
keys(right)).
1. Left Rotation
2. Right Rotation
Steps to follow for insertion
Let the newly inserted node be w
1. Perform standard BST insert for w.
2. Starting from w, travel up and find the first unbalanced node. Let z be the first unbalanced
node, y be the child of z that comes on the path from w to z and x be the grandchild of z that
comes on the path from w to z.
3. Re-balance the tree by performing appropriate rotations on the subtree rooted with z. There
can be 4 possible cases that needs to be handled as x, y and z can be arranged in 4 ways.
Following are the possible 4 arrangements:
◦ y is left child of z and x is left child of y (Left Left Case)
◦ y is left child of z and x is right child of y (Left Right Case)
◦ y is right child of z and x is right child of y (Right Right Case)
◦ y is right child of z and x is left child of y (Right Left Case)
Following are the operations to be performed in above mentioned 4 cases. In all of the cases, we only
need to re-balance the subtree rooted with z and the complete tree becomes balanced as the height of
subtree (After appropriate rotations) rooted with z becomes same as it was before insertion.
Left Left Case: ( We will need to perform a right rotation )

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.

1. Implement the functions rotateLeft() and rotateRight().


2. Implement the 4 cases of balancing the tree.

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:

1. To understand and implement the Binary Heaps


2. To understand the basic insertion and deletion operations on Binary Heaps

Pre-Lab Reading Task:


A binary heap is a complete binary tree in which every node satisfies the heap property which states
that:
If B is a child of A, then key(A) ≥ key(B)
This implies that elements at every node will be either greater than or equal to the element at its left
and right child. Thus, the root node has the highest key value in the heap. Such a heap is commonly
known as a max-heap. Alternatively, elements at every node will be either less than or equal to the
element at its left and right child. Thus, the root has the lowest key value. Such a heap is called a
min-heap.

Figure12. 1: Min and Max Heaps

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.

Figure12. 2: Insertion of a node (99) in Max 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

Figure12. 4: Deleting a node from a binary heap


Applications of Binary Heaps
Binary heaps are mainly applied for
1. Sorting an array using heap sort algorithm.
2. Implementing priority queues.
Binary Heap Implementation of Priority Queues
We learned about priority queues. We have also seen how priority queues can be implemented using
linked lists. A priority queue is similar to a queue in which an item is dequeued (or removed) from
the front. However, unlike a regular queue, in a priority queue the logical order of elements is
determined by their priority. While the higher priority elements are added at the front of the queue,
elements with lower priority are added at the rear. Conceptually, we can think of a priority queue as
a bag of priorities. In this bag you can insert any priority but you can take out one with the highest
value. Though we can easily implement priority queues using a linear array, but we should first
consider the time required to insert an element in the array and then sort it. We need O(n) time to
insert an element and at least O(n log n) time to sort the array. Therefore, a better way to implement
a priority queue is by using a binary heap which allows both enqueue and dequeue of elements in
O(log n) time.
In-Lab Tasks:
1. Complete the function ‘void  insert_node(int x, struct heap_struct * H)’ in the
skeleton code provided.
2. Complete the function ‘void  delete_root(struct heap_struct * H)’ in the skeleton
code provided.
Post Lab: Study Binomial Heaps and Fibonacci Heaps.

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.

Figure 1: An example graph

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.

Figure 2: PIA Flight Network

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.

Figure 3: Example of Directed and Undirected Graphs

Weighted vs Unweighted Graphs


If edges in your graph have weights then your graph is said to be a weighted graph, if the edges do
not have weights, the graph is said to be unweighted. A weight is a numerical value attached to each
individual edge. In a weighted graph relationships between nodes have a magnitude and this

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.

Figure 4: A Weighted Graph

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:

Figure 5: A Social Media 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:

Figure 7: Adjacency Matrix


Representation of Graph shown in
Figure 5

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.

Figure 8: A Weighted Directed Graph with 10 Vertices


Task 2:
For this part you will have to complete the function ‘find_path_dfs()’ which finds the cost of
going from the source vertex (src) to destination vertex (dst) using Depth First Search (DFS)
algorithm. You will use a stack for implementing DFS algorithm.
Post Lab Task:
Complete the code for Task 2 and submit along with a report explaining your implementation.

Page 5 of 5

You might also like