Unit - 1 DSA Notes
Unit - 1 DSA Notes
In C, the number of bytes used to store a data type depends on the Compiler(depending on the bit
size of a compiler and also the OS). But irrespective of the bit-size of the compiler and OS, the
following rules are followed, such as –
Data Type Size Range
char at least 1 byte -128 to 127
unsigned char at least 1 byte 0 to 255
short at least 2 bytes -32768 to 32767
unsigned short at least 2 bytes 0 to 65535
int at least 2 bytes -32768 to 32767
unsigned int at least 2 bytes 0 to 65535
long at least 4 bytes -2,147,483,648 to 2,147,483,647
unsigned long at least 4 bytes 0 to 4,294,967,295
float at least 2 bytes 3.4e-038 to 3.4e+038
double at least 8 bytes 1.7e-308 to 1.7e+308
long double at least 10 bytes 1.7e-4932 to 1.7e+4932
C struct
In C programming, a struct (or structure) is a collection of variables (can be of different types)
under a single name.
Define Structures
Before you can create structure variables, you need to define its data type. To define a struct,
the struct keyword is used.
Syntax of struct
struct structureName {
dataType member1;
dataType member2;
...
};
For example,
struct Person {
char name[50];
int citNo;
float salary;
};
a derived type struct Person is defined. Now, you can create variables of this type.
int main() {
struct Person person1, person2, p[20];
return 0;
}
C structs
#include <stdio.h>
#include <string.h>
int main() {
Notice that we have used strcpy() function to assign the value to person1.name.
This is because name is a char array (C-string) and we cannot use the assignment
operator = with it after we have declared the string.
Finally, we printed the data of person1.
Nested Structures
You can create structures within a structure in C programming. For example,
struct complex {
int imag;
float real;
};
struct number {
struct complex comp;
int integers;
} num1, num2;
Suppose, you want to set imag of num2 variable to 11. Here's how you can do it:
num2.comp.imag = 11;
#include <stdio.h>
struct complex {
int imag;
float real;
};
struct number {
struct complex comp;
int integer;
} num1;
int main() {
return 0;
}
Output
Imaginary Part: 11
Real Part: 5.25
Integer: 6
Why structs in C?
Suppose you want to store information about a person: his/her name, citizenship number, and
salary. You can create different variables name, citNo and salary to store this information.
What if you need to store information of more than one person? Now, you need to create different
variables for each information per person name1, citNo1, salary1, name2, citNo2, salary2, etc.
A better approach would be to have a collection of all related information under a single
name Person structure and use it for every person.
Structure Pointer in C
A structure pointer is defined as the pointer which points to the address of the memory block
that stores a structure known as the structure pointer. Complex data structures like Linked lists,
trees, graphs, etc. are created with the help of structure pointers. The structure pointer tells the
address of a structure in memory by pointing the variable to the structure variable.
Example:
struct point {
int value;
};
int main()
{
struct point s;
return 0;
}In the above code s is an instance of struct point and ptr is the struct pointer because it is
storing the address of struct point.
Accessing the Structure Member with the Help of Pointers
There are two ways to access the members of the structure with the help of a structure pointer:
1. With the help of (*) asterisk or indirection operator and (.) dot operator.
2. With the help of ( -> ) Arrow operator.
Below is the program to access the structure members using the structure pointer with the help
of the dot operator.
// C Program to demonstrate Structure pointer
#include <stdio.h>
#include <string.h>
struct Student {
int roll_no;
char name[30];
char branch[40];
int batch;
};
int main()
{
s1.roll_no = 27;
strcpy(s1.name, "Kamlesh Joshi");
strcpy(s1.branch, "Computer Science And Engineering");
s1.batch = 2019;
return 0;
}
Output:
Roll Number: 27
Name: Kamlesh Joshi
Branch: Computer Science And Engineering
Batch: 2019
Below is the program to access the structure members using the structure pointer with the help
of the Arrow operator. In this program, we have created a Structure Student containing structure
variable s. The Structure Student has roll_no, name, branch, and batch.
int main()
{
ptr = &s;
// Taking inputs
printf("Enter the Roll Number of Student\n");
scanf("%d", &ptr->roll_no);
printf("Enter Name of Student\n");
scanf("%s", &ptr->name);
printf("Enter Branch of Student\n");
scanf("%s", &ptr->branch);
printf("Enter batch of Student\n");
scanf("%d", &ptr->batch);
return 0;
}
Enter the Roll Number of Student
27
Enter Name of Student
Kamlesh_Joshi
Enter Branch of Student
Computer_Science_And_Engineering
Enter batch of Student
2019
Student details are:
Roll No: 27
Name: Kamlesh_Joshi
Branch: Computer_Science_And_Engineering
Batch: 2019
Example:
#include<stdio.h>
struct dog
{
char name[10];
char breed[10];
int age;
char color[10];
};
int main()
{
struct dog my_dog = {"tyke", "Bulldog", 5, "white"};
struct dog *ptr_dog;
ptr_dog = &my_dog;
After changes
Self Referential structures are those structures that have one or more pointers which point to
the same type of structure, as their member.
In other words, structures pointing to the same type of structures are self-referential in nature
struct node {
int data1;
char data2;
struct node* link;
};
int main()
{
struct node ob;
return 0;
}
In the above example ‘link’ is a pointer to a structure of type ‘node’. Hence, the structure
‘node’ is a self-referential structure with ‘link’ as the referencing pointer.
An important point to consider is that the pointer should be initialized properly before
accessing, as by default it contains garbage value.
Types of Self Referential Structures
1. Self Referential Structure with Single Link
2. Self Referential Structure with Multiple Links
Self Referential Structure with Single Link: These structures can have only one self-pointer
as their member. The following example will show us how to connect the objects of a self-
referential structure with the single link and access the corresponding data members. The
connection formed is shown in the following figure.
#include <stdio.h>
struct node {
int data1;
char data2;
struct node* link;
};
int main()
{
struct node ob1; // Node1
// Initialization
ob1.link = NULL;
ob1.data1 = 10;
ob1.data2 = 20;
// Initialization
ob2.link = NULL;
ob2.data1 = 30;
ob2.data2 = 40;
#include <stdio.h>
struct node {
int data;
struct node* prev_link;
struct node* next_link;
};
int main()
{
struct node ob1; // Node1
// Initialization
ob1.prev_link = NULL;
ob1.next_link = NULL;
ob1.data = 10;
// Initialization
ob2.prev_link = NULL;
ob2.next_link = NULL;
ob2.data = 20;
// Initialization
ob3.prev_link = NULL;
ob3.next_link = NULL;
ob3.data = 30;
// Forward links
ob1.next_link = &ob2;
ob2.next_link = &ob3;
// Backward links
ob2.prev_link = &ob1;
ob3.prev_link = &ob2;
In the above example we can see that ‘ob1’, ‘ob2’ and ‘ob3’ are three objects of the self
referential structure ‘node’. And they are connected using their links in such a way that any of
them can easily access each other’s data. This is the beauty of the self referential structures.
The connections can be manipulated according to the requirements of the programmer.
Applications: Self-referential structures are very useful in creation of other complex data
structures like:
Linked Lists
Stacks
Queues
Trees
Graphs etc.
As it can be seen that the length (size) of the array above made is 9. But what if there is a requirement to
change this length (size). For Example,
If there is a situation where only 5 elements are needed to be entered in this array. In this case, the
remaining 4 indices are just wasting memory in this array. So there is a requirement to lessen the
length (size) of the array from 9 to 5.
Take another situation. In this, there is an array of 9 elements with all 9 indices filled. But there is a
need to enter 3 more elements in this array. In this case, 3 indices more are required. So the length
(size) of the array needs to be changed from 9 to 12.
This procedure is referred to as Dynamic Memory Allocation in C.
Therefore, C Dynamic Memory Allocation can be defined as a procedure in which the size of a data
structure (like Array) is changed during the runtime. C provides some functions to achieve these tasks.
There are 4 library functions provided by C defined under <stdlib.h> header file to facilitate dynamic
memory allocation in C programming. They are:
1. malloc()
2. calloc()
3. free()
4. realloc()
1. malloc() method
The “malloc” or “memory allocation” method in C is used to dynamically allocate a single large
block of memory with the specified size. It returns a pointer of type void which can be cast into a
pointer of any form. It doesn’t initialize memory at execution time so that it has initialized each block with
the default garbage value initially.
Syntax of malloc() in C
Example of malloc() in C
#include <stdio.h>
#include <stdlib.h>
int main()
{
return 0;
}
Output:
Enter number of elements: 5
Memory successfully allocated using malloc.
The elements of the array are: 1, 2, 3, 4, 5,
2. C calloc() method
1. “calloc” or “contiguous allocation” method in C is used to dynamically allocate the specified number
of blocks of memory of the specified type. it is very much similar to malloc() but has two different
points and these are:
2. It initializes each block with a default value ‘0’.
3. It has two parameters or arguments as compare to malloc().
Syntax of calloc() in C
Example of calloc() in C
#include <stdio.h>
#include <stdlib.h>
int main()
{
return 0;
}
Output:
Enter number of elements: 5
Memory successfully allocated using calloc.
The elements of the array are: 1, 2, 3, 4, 5,
What is malloc()?
The malloc is also known as the memory allocation function. malloc() dynamically allocates a
large block of memory with a specific size. It returns a void type pointer and is cast into any
form.
What is calloc()?
The calloc() function allocates a specific amount of memory and initializes it to zero. The
function can be cast to the desired type when it returns to a void pointer to the memory
location.
Difference between malloc() and calloc()
4. malloc() has high time efficiency. calloc() has low time efficiency.
C free() method
“free” method in C is used to dynamically de-allocate the memory. The memory allocated using functions
malloc() and calloc() is not de-allocated on their own. Hence the free() method is used, whenever the
dynamic memory allocation takes place. It helps to reduce wastage of memory by freeing it.
Syntax of free() in C
free(ptr);
#include <stdio.h>
#include <stdlib.h>
int main()
{
return 0;
}
Output:
Enter number of elements: 5
Memory successfully allocated using malloc.
Malloc Memory successfully freed.
C realloc() method
“realloc” or “re-allocation” method in C is used to dynamically change the memory allocation of a
previously allocated memory. In other words, if the memory previously allocated with the help of malloc or
calloc is insufficient, realloc can be used to dynamically re-allocate memory. re-allocation of memory
maintains the already present value and new blocks will be initialized with the default garbage value.
Syntax of realloc() in C
free(ptr);
}
return 0;
}
Output:
Enter number of elements: 5
Memory successfully allocated using calloc.
The elements of the array are: 1, 2, 3, 4, 5,
Write a C program for multiplication of two matrices using dynamic memory allocation
/*Matrix multiplication using dynamic memory allocation*/
#include <stdio.h>
#include<stdlib.h>
/* Main Function */
int main()
{
/* Declaring pointer for matrix multiplication.*/
int **ptr1, **ptr2, **ptr3;
/* Declaring integer variables for row and columns of two matrices.*/
int row1, col1, row2, col2;
/* Declaring indexes. */
int i, j, k;
/* Request the user to input number of columns of the matrices.*/
printf("\nEnter number of rows for first matrix : ");
scanf("%d", &row1);
printf("\nEnter number of columns for first matrix : ");
scanf("%d", &col1);
printf("\nEnter number of rows for second matrix : ");
scanf("%d", &row2);
printf("\nEnter number of columns for second matrix : ");
scanf("%d", &col2);
if(col1 != row2)
{
printf("\nCannot multiply two matrices.");
return(0);
}
/* Allocating memory for the matrix rows. */
ptr1 = (int **) malloc(sizeof(int *) * row1);
ptr2 = (int **) malloc(sizeof(int *) * row2);
ptr3 = (int **) malloc(sizeof(int *) * row1);
/* Allocating memory for the col of the matrices. */
for(i=0; i<row1; i++)
ptr1[i] = (int *)malloc(sizeof(int) * col1);
for(i=0; i<row2; i++)
ptr2[i] = (int *)malloc(sizeof(int) * col2);
for(i=0; i<row1; i++)
ptr3[i] = (int *)malloc(sizeof(int) * col2);
/* Request the user to input members of first matrix. */
printf("\nEnter elements of first matrix :\n");
for(i=0; i< row1; i++)
{
for(j=0; j< col1; j++)
{
printf("\tA[%d][%d] = ",i, j);
scanf("%d", &ptr1[i][j]);
}
}
/* request to user to input members of second matrix. */
printf("\nEnter elements of second matrix :\n");
for(i=0; i< row2; i++)
{
for(j=0; j< col2; j++)
{
printf("\tB[%d][%d] = ",i, j);
scanf("%d", &ptr2[i][j]);
}
}
/* Calculation begins for the resultant matrix. */
for(i=0; i < row1; i++)
{
for(j=0; j < col1; j++)
{
ptr3[i][j] = 0;
for(k=0; k<col2; k++)
ptr3[i][j] = ptr3[i][j] + ptr1[i][k] * ptr2[k][j];
}
}
/* Printing the contents of third matrix. */
printf("\n\nResultant matrix :");
for(i=0; i< row1; i++)
{
printf("\n\t\t\t");
for(j=0; j < col2; j++)
printf("%d\t", ptr3[i][j]);
}
return 0;
}
OUTPUT
Resultant matrix :
3 3 3
3 3 3
3 3 3
Linear data structure: Data structure in which data elements are arranged sequentially or linearly, where
each element is attached to its previous and next adjacent elements, is called a linear data structure.
Examples of linear data structures are array, stack, queue, linked list, etc.
Static data structure: Static data structure has a fixed memory size. It is easier to access the elements in
a static data structure.
An example of this data structure is an array.
Dynamic data structure: In dynamic data structure, the size is not fixed. It can be randomly updated
during the runtime which may be considered efficient concerning the memory (space) complexity of the
code.
Examples of this data structure are queue, stack, etc.
Non-linear data structure: Data structures where data elements are not placed sequentially or linearly are
called non-linear data structures. In a non-linear data structure, we can’t traverse all the elements in a single
run only.
Examples of non-linear data structures are trees and graphs.
For example, we can store a list of items having the same data-type using the array data structure.
Array Data Structure
Think of ADT as a black box which hides the inner structure and design of the data type. Now we’ll define
three ADTs namely List ADT, Stack ADT, Queue ADT.
List ADT
Lists are linear data structures that hold data in a non-continuous structure. The list is made up of data
storage containers known as "nodes." These nodes are linked to one another, which mean that each node
contains the address of another block. All of the nodes are thus connected to one another via these links.
You can discover more about lists in this article: Linked List Data Structure.
Some of the most essential operations defined in List ADT are listed below.
front(): returns the value of the node present at the front of the list.
back(): returns the value of the node present at the back of the list.
push_front(int val): creates a pointer with value = val and keeps this pointer to the front of the linked list.
push_back(int val): creates a pointer with value = val and keeps this pointer to the back of the linked list.
size(): returns the number of nodes that are present in the list.
Stack ADT
A stack is a linear data structure that only allows data to be accessed from the top. It simply has two
operations: push (to insert data to the top of the stack) and pop (to remove data from the stack). (used to
remove data from the stack top).
Some of the most essential operations defined in Stack ADT are listed below.
top(): returns the value of the node present at the top of the stack.
push(int val): creates a node with value = val and puts it at the stack top.
size(): returns the number of nodes that are present in the stack.
Queue ADT
A queue is a linear data structure that allows data to be accessed from both ends. There are two main
operations in the queue: push (this operation inserts data to the back of the queue) and pop (this operation is
used to remove data from the front of the queue).
Some of the most essential operations defined in Queue ADT are listed below.
Circular Queue
In Circular Queue, all the nodes are represented as circular. It is similar to the linear Queue except that the
last element of the queue is connected to the first element. It is also known as Ring Buffer, as all the ends
are connected to another end. The representation of circular queue is shown in the below image -
Priority Queue
It is a special type of queue in which the elements are arranged based on the priority. It is a special type of
queue data structure in which every element has a priority associated with it. Suppose some elements occur
with the same priority, they will be arranged according to the FIFO principle. The representation of priority
queue is shown in the below image -
Insertion in priority queue takes place based on the arrival, while deletion in the priority queue occurs based
on the priority. Priority queue is mainly used to implement the CPU scheduling algorithms.
In Deque or Double Ended Queue, insertion and deletion can be done from both ends of the queue either
from the front or rear. It means that we can insert and delete elements from both front and rear ends of the
queue. Deque can be used as a palindrome checker means that if we read the string from both ends, then the
string would be the same.
Deque can be used both as stack and queue as it allows the insertion and deletion operations on both ends.
Deque can be considered as stack because stack follows the LIFO (Last In First Out) principle in which
insertion and deletion both can be performed only from one end. And in deque, it is possible to perform both
insertion and deletion from one end, and Deque does not follow the FIFO principle.
The fundamental operations that can be performed on queue are listed as follows -
Provides abstraction, which simplifies the complexity of the data structure and allows users to focus on the
functionality.
Enhances program modularity by allowing the data structure implementation to be separate from the rest of
the program.
Enables code reusability as the same data structure can be used in multiple programs with the same
interface.
Promotes the concept of data hiding by encapsulating data and operations into a single unit, which enhances
security and control over the data.
Supports polymorphism, which allows the same interface to be used with different underlying data
structures, providing flexibility and adaptability to changing requirements.
Limited control: ADTs can limit the level of control that a programmer has over the data structure, which
can be a disadvantage in certain scenarios.
Performance impact: Depending on the specific implementation, the performance of an ADT may be
lower than that of a custom data structure designed for a specific application.
Conclusion
In conclusion, Abstract Data Types in data structures is a fundamental concept that provides a high level of
abstraction and encapsulation for data and operations. Understanding and using ADTs can help
programmers develop more efficient and maintainable code that is better suited to changing requirements
and scalability needs.
Imagine that we are inserting a node B (NewNode), between A (LeftNode) and C (RightNode).
Then point B.next to C −
NewNode.next −> RightNode;
It should look like this −
Now, the next node at the left should point to the new node.
LeftNode.next −> NewNode;
Deletion Operation
Deletion is also a more than one step process. We shall learn with pictorial representation. First, locate
the target node to be removed, by using searching algorithms.
The left (previous) node of the target node now should point to the next node of the target node −
LeftNode.next −> TargetNode.next;
This will remove the link that was pointing to the target node. Now, using the following code, we will
remove what the target node is pointing at.
TargetNode.next −> NULL;
We need to use the deleted node. We can keep that in memory otherwise we can simply deallocate
memory and wipe off the target node completely.
Stack Representation
A Stack ADT allows all data operations at one end only. At any given time, we can only access the top
element of a stack.
The following diagram depicts a stack and its operations −
A stack can be implemented by means of Array, Structure, Pointer, and Linked List. Stack can either be a
fixed size one or it may have a sense of dynamic resizing. Here, we are going to implement stack using
arrays, which makes it a fixed size stack implementation.
Insertion: push()
push() is an operation that inserts elements into the stack. The following is an algorithm that describes the
push() operation in a simpler way.
Algorithm
1 − Checks if the stack is full.
2 − If the stack is full, produces an error and exit.
3 − If the stack is not full, increments top to point next empty space.
4 − Adds data element to the stack location, where top is pointing.
5 − Returns success.
Deletion: pop()
pop() is a data manipulation operation which removes elements from the stack. The following pseudo
code describes the pop() operation in a simpler way.
Algorithm
1 − Checks if the stack is empty.
2 − If the stack is empty, produces an error and exit.
3 − If the stack is not empty, accesses the data element at which top is pointing.
4 − Decreases the value of top by 1.
5 − Returns success.
isFull()
isFull() operation checks whether the stack is full. This operation is used to check the status of the stack
with the help of top pointer.
isEmpty()
The isEmpty() operation verifies whether the stack is empty. This operation is used to check the status of the
stack with the help of top pointer.
https://www.tutorialspoint.com/data_structures_algorithms/stack_algorithm.htm
// Driver Code
int main()
{
// Initialise array
int arr[] = { 1, 2, 3, 4 };
// size of array
int N = sizeof(arr)/sizeof(arr[0]);
Insertion: It is the operation which we apply on all the data-structures. Insertion means to add an element in
the given data structure. The operation of insertion is successful when the required element is added to the
required data-structure. It is unsuccessful in some cases when the size of the data structure is full and when
there is no space in the data-structure to add any additional element. The insertion has the same name as an
insertion in the data-structure as an array, linked-list, graph, tree. In stack, this operation is called Push. In
the queue, this operation is called Enqueue.
Deletion: It is the operation which we apply on all the data-structures. Deletion means to delete an element
in the given data structure. The operation of deletion is successful when the required element is deleted from
the data structure. The deletion has the same name as a deletion in the data-structure as an array, linked-list,
graph, tree, etc. In stack, this operation is called Pop. In Queue this operation is called Dequeue.
Sorting : Sorting is the process of arranging the elements of an array so that they can be placed either in
ascending or descending order.
Merging: List of two sorted data items can be combinedto form a sigle list of sorted data items.
Asymptotic Notation
Asymptotic Notations are the expressions that are used to represent the complexity of an
algorithm.
Asymptotic Notation is used to describe the running time of an algorithm - how much time an algorithm
takes with a given input, n. There are three different notations: big O, big Theta (Θ), and big Omega (Ω).
There are three types of analysis that we perform on a particular algorithm.
Best Case: In which we analyse the performance of an algorithm for the input, for which the
algorithm takes less time or space.
Worst Case: In which we analyse the performance of an algorithm for the input, for which the
algorithm takes long time or space.
Average Case: In which we analyse the performance of an algorithm for the input, for which the
algorithm takes time or space that lies between best and worst case.
Big Oh Notation, Ο
Big-O notation represents the upper bound of the running time of an algorithm. Thus, it
gives the worst-case complexity of an algorithm.
Big Omega Notation, Ω
Omega notation represents the lower bound of the running time of an algorithm. Thus, it
provides the best case complexity of an algorithm.
Theta Notation, θ
Theta notation encloses the function from above and below. Since it
represents the upper and the lower bound of the running time of an algorithm,
it is used for analyzing the average-case complexity of an algorithm.