DSA Programiz
DSA Programiz
PROGRAMIZ
DSA INTRODUCTION
WHAT IS AN ALGORITHM?
In this tutorial, we will learn what algorithms are with the help of examples.
In computer programming terms, an algorithm is a set of well-defined instructions to solve a
particular problem. It takes a set of input and produces a desired output. For example,
An algorithm to add two numbers:
1. Take two number inputs
2. Add numbers using the + operator
3. Display the result
Algorithm Examples
Algorithm to add two numbers
Algorithm to find the largest among three numbers
Algorithm to find all the roots of the quadratic equation
Algorithm to find the factorial
Algorithm to check prime number
Algorithm of Fibonacci series
Algorithm 6: Find the Fibonacci series till the term less than 1000
Step 1: Start
Step 2: Declare variables first_term,second_term and temp.
Step 3: Initialize variables first_term ← 0 second_term ← 1
Step 4: Display first_term and second_term
Step 5: Repeat the steps until second_term ≤ 1000
5.1: temp ← second_term
5.2: second_term ← second_term + first_term
5.3: first_term ← temp
5.4: Display second_term
Step 6: Stop
In this article, you will learn about data strucrture and its types.
What are Data Structures?
Data structure is a storage that is used to store and organize data. It is a way of arranging data on
a computer so that it can be accessed and updated efficiently.
Depending on your requirement and project, it is important to choose the right data structure for
your project. For example, if you want to store data sequentially in the memory, then you can go
for the Array data structure.
In a stack, operations can be perform only from one end (top here).
3. Queue Data Structure
Unlike stack, the queue data structure works in the FIFO principle where first element stored in
the queue will be removed first.
It works just like a queue of people in the ticket counter where first person on the queue will get
the ticket first. To learn more, visit Queue Data Structure.
A linked list
The data items are arranged in sequential The data items are arranged in non-sequential
order, one after the other. order (hierarchical manner).
All the items are present on the single layer. The data items are present at different layers.
In this article, we will learn why every programmer should learn data structures and algorithms
with the help of examples.
This article is for those who have just started learning algorithms and wondered how impactful it
will be to boost their career/programming skills. It is also for those who wonder why big
companies like Google, Facebook, and Amazon hire programmers who are exceptionally good at
optimizing Algorithms.
Programming is all about data structures and algorithms. Data structures are used to hold data
while algorithms are used to solve the problem using that data.
Data structures and algorithms (DSA) goes through solutions to standard problems in detail and
gives you an insight into how efficient it is to use each one of them. It also teaches you the
science of evaluating the efficiency of an algorithm. This enables you to choose the best of
various choices.
More on Scalability
Scalability is scale plus ability, which means the quality of an algorithm/system to handle the
problem of larger size.
Consider the problem of setting up a classroom of 50 students. One of the simplest solutions is to
book a room, get a blackboard, a few chalks, and the problem is solved.
But what if the size of the problem increases? What if the number of students increased to
200?
The solution still holds but it needs more resources. In this case, you will probably need a much
larger room (probably a theater), a projector screen and a digital pen.
What if the number of students increased to 1000?
The solution fails or uses a lot of resources when the size of the problem increases. This means,
your solution wasn't scalable.
What is a scalable solution then?
Consider a site like Khanacademy, millions of students can see videos, read answers at the same
time and no more resources are required. So, the solution can solve the problems of larger size
under resource crunch.
If you see our first solution to find the sum of first N natural numbers, it wasn't scalable. It's
because it required linear growth in time with the linear growth in the size of the problem. Such
algorithms are also known as linearly scalable algorithms.
Our second solution was very scalable and didn't require the use of any more time to solve a
problem of larger size. These are known as constant-time algorithms.
Memory is expensive
Memory is not always available in abundance. While dealing with code/system which requires
you to store or produce a lot of data, it is critical for your algorithm to save the usage of memory
wherever possible. For example: While storing data about people, you can save memory by
storing only their date of birth, not their age. You can always calculate it on the fly using their
date of birth and current date.
Final Words
Generally, software development involves learning new technologies on a daily basis. You get to
learn most of these technologies while using them in one of your projects. However, it is not the
case with algorithms.
If you don't know algorithms well, you won't be able to identify if you can optimize the code you
are writing right now. You are expected to know them in advance and apply them wherever
possible and critical.
We specifically talked about the scalability of algorithms. A software system consists of many
such algorithms. Optimizing any one of them leads to a better system.
However, it's important to note that this is not the only way to make a system scalable. For
example, a technique known as distributed computing allows independent parts of a program to
run to multiple machines together making it even more scalable.
In this tutorial, you will learn what asymptotic notations are. Also, you will learn about Big-O
notation, Theta notation and Omega notation.
The efficiency of an algorithm depends on the amount of time, storage and other resources
required to execute the algorithm. The efficiency is measured with the help of asymptotic
notations.
An algorithm may not have the same performance for different types of inputs. With the increase
in the input size, the performance will change.
The study of change in performance of the algorithm with the change in the order of the input
size is defined as asymptotic analysis.
Asymptotic Notations
Asymptotic notations are the mathematical notations used to describe the running time of an
algorithm when the input tends towards a particular value or a limiting value.
For example: In bubble sort, when the input array is already sorted, the time taken by the
algorithm is linear i.e. the best case.
But, when the input array is in reverse condition, the algorithm takes the maximum time
(quadratic) to sort the elements i.e. the worst case.
When the input array is neither sorted nor in reverse order, then it takes average time. These
durations are denoted using asymptotic notations.
There are mainly three asymptotic notations:
Big-O notation
Omega notation
Theta notation
In this tutorial, you will learn how the divide and conquer algorithm works. We will also
compare the divide and conquer approach versus other approaches to solve a recursive problem.
A divide and conquer algorithm is a strategy of solving a large problem by
1. breaking the problem into smaller sub-problems
2. solving the sub-problems, and
3. combining them to get the desired output.
To use the divide and conquer algorithm, recursion is used. Learn about recursion in different
programming languages:
Recursion in Java
Recursion in Python
Recursion in C++
Time Complexity
The complexity of the divide and conquer algorithm is calculated using the master theorem.
T(n) = aT(n/b) + f(n),
where,
n = size of input
a = number of subproblems in the recursion
n/b = size of each subproblem. All subproblems are assumed to have the same size.
f(n) = cost of the work done outside the recursive call, which includes the cost of dividing the
problem and cost of merging the solutions
DSA STRUCTURES II
In this tutorial, you will learn about linked list data structure and it's implementation in Python,
Java, C, and C++.
A linked list is a linear data structure that includes a series of connected nodes. Here, each node
store the data and the address of the next node. For example,
Linked list Data Structure
You have to start somewhere, so we give the address of the first node a special name
called HEAD. Also, the last node in the linked list can be identified because its next portion
points to NULL.
Linked lists can be of multiple types: singly, doubly, and circular linked list. In this article, we
will focus on the singly linked list. To learn about other types, visit Types of Linked List.
Note: You might have played the game Treasure Hunt, where each clue includes the information
about the next clue. That is how the linked list operates.
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
If you didn't understand any of the lines above, all you need is a refresher on pointers and structs.
In just a few steps, we have created a simple linked list with three nodes.
#include <stdio.h>
#include <stdlib.h>
// Creating a node
struct node {
int value;
struct node *next;
};
int main() {
// Initialize nodes
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
// Allocate memory
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
// Connect nodes
one->next = two;
two->next = three;
three->next = NULL;
// printing node-value
head = one;
printLinkedlist(head);
}
In this tutorial, you will learn different operations on a linked list. Also, you will find
implementation of linked list operations in C/C++, Python and Java.
There are various linked list operations that allow us to perform different actions on linked lists.
For example, the insertion operation adds a new element to the linked list.
Here's a list of basic linked list operations that we will cover in this article.
Traversal - access each element of the linked list
Insertion - adds a new element to the linked list
Deletion - removes the existing elements
Search - find a node in the linked list
Sort - sort the nodes of the linked list
Before you learn about linked list operations in detail, make sure to know about Linked List first.
Things to Remember about Linked List
head points to the first node of the linked list
next pointer of the last node is NULL, so if the next current node is NULL, we have
reached the end of the linked list.
In all of the examples, we will assume that the linked list has three nodes 1 --->2 --->3 with node
structure as below:
struct node {
int data;
struct node *next;
};
temp->next = newNode;
3. Insert at the Middle
Allocate memory and store data for new node
Traverse to node just before the required position of new node
Change next pointers to include new node in between
struct node *newNode;
newNode = malloc(sizeof(struct node));
newNode->data = 4;
struct node *temp = head;
temp->next = temp->next->next;
if (head_ref == NULL) {
return;
} else {
while (current != NULL) {
// index points to the node next to current
index = current->next;
#include <stdio.h>
#include <stdlib.h>
// Create a node
struct Node {
int data;
struct Node* next;
};
new_node->next = (*head_ref);
new_node->data = new_data;
new_node->next = NULL;
if (*head_ref == NULL) {
*head_ref = new_node;
return;
}
last->next = new_node;
return;
}
// Delete a node
void deleteNode(struct Node** head_ref, int key) {
struct Node *temp = *head_ref, *prev;
free(temp);
}
// Search a node
int searchNode(struct Node** head_ref, int key) {
struct Node* current = *head_ref;
if (head_ref == NULL) {
return;
} else {
while (current != NULL) {
// index points to the node next to current
index = current->next;
// Driver program
int main() {
struct Node* head = NULL;
insertAtEnd(&head, 1);
insertAtBeginning(&head, 2);
insertAtBeginning(&head, 3);
insertAtEnd(&head, 4);
insertAfter(head->next, 5);
int item_to_find = 3;
if (searchNode(&head, item_to_find)) {
printf("\n%d is found", item_to_find);
} else {
printf("\n%d is not found", item_to_find);
}
sortLinkedList(&head);
printf("\nSorted List: ");
printList(head);
}
In this tutorial, you will learn different types of linked list. Also, you will find implementation of
linked list in C.
Before you learn about the type of the linked list, make sure you know about the LinkedList Data
Structure.
There are three common types of Linked List.
1. Singly Linked List
2. Doubly Linked List
3. Circular Linked List
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
two->next = three;
three->next = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
one->prev = NULL;
two->next = three;
two->prev = one;
three->next = NULL;
three->prev = two;
Circul
ar linked list
A circular linked list can be either singly linked or doubly linked.
for singly linked list, next pointer of last item points to the first item
In the doubly linked list, prev pointer of the first item points to the last item as well.
A three-member circular singly linked list can be created as:
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
two->next = three;
three->next = one;
DSA STRUCTURES I
In this tutorial, you will learn about the stack data structure and its implementation in Python,
Java and C/C++.
A stack is a linear data structure that follows the principle of Last In First Out (LIFO). This
means the last element inserted inside the stack is removed first.
You can think of the stack data structure as the pile of plates on top of another.
#include <stdio.h>
#include <stdlib.h>
#define MAX 10
int count = 0;
// Creating a stack
struct stack {
int items[MAX];
int top;
};
typedef struct stack st;
createEmptyStack(s);
push(s, 1);
push(s, 2);
push(s, 3);
push(s, 4);
printStack(s);
pop(s);
In this tutorial, you will learn what a queue is. Also, you will find implementation of queue in
C, C++, Java and Python.
A queue is a useful data structure in programming. It is similar to the ticket queue outside a
cinema hall, where the first person entering the queue is the first person who gets the ticket.
Queue follows the First In First Out (FIFO) rule - the item that goes in first is the item that
comes out first.
Working of Queue
Queue operations work as follows:
two pointers FRONT and REAR
FRONT track the first element of the queue
REAR track the last element of the queue
initially, set value of FRONT and REAR to -1
Enqueue Operation
check if the queue is full
for the first element, set the value of FRONT to 0
increase the REAR index by 1
add the new element in the position pointed to by REAR
Dequeue Operation
check if the queue is empty
return the value pointed by FRONT
increase the FRONT index by 1
for the last element, reset the values of FRONT and REAR to -1
Enqueue and Dequeue Operations
Queue Implementations in Python, Java, C, and C++
We usually use arrays to implement queues in Java and C/++. In the case of Python, we use
lists.
Python
Java
C
C++
// Queue implementation in C
#include <stdio.h>
#define SIZE 5
void enQueue(int);
void deQueue();
void display();
int main() {
//deQueue is not possible on empty queue
deQueue();
//enQueue 5 elements
enQueue(1);
enQueue(2);
enQueue(3);
enQueue(4);
enQueue(5);
display();
return 0;
}
void enQueue(int value) {
if (rear == SIZE - 1)
printf("\nQueue is Full!!");
else {
if (front == -1)
front = 0;
rear++;
items[rear] = value;
printf("\nInserted -> %d", value);
}
}
void deQueue() {
if (front == -1)
printf("\nQueue is Empty!!");
else {
printf("\nDeleted : %d", items[front]);
front++;
if (front > rear)
front = rear = -1;
}
}
Limitations of Queue
As you can see in the image below, after a bit of enqueuing and dequeuing, the size of the
queue has been reduced.
Limitation of a queue
And we can only add indexes 0 and 1 only when the queue is reset (when all the elements
have been dequeued).
After REAR reaches the last index, if we can store extra elements in the empty spaces (0 and
1), we can make use of the empty spaces. This is implemented by a modified queue called
the circular queue.
Complexity Analysis
The complexity of enqueue and dequeue operations in a queue using an array is O(1). If you
use pop(N) in python code, then the complexity might be O(n) depending on the position of
the item to be popped.
Applications of Queue
CPU scheduling, Disk Scheduling
When data is transferred asynchronously between two processes.The queue is used for
synchronization. For example: IO Buffers, pipes, file IO, etc
Handling of interrupts in real-time systems.
Call Center phone systems use Queues to hold people calling them in order.
Recommended Readings
Types of Queue
Circular Queue
Deque Data Structure
Priority Queue
TYPES OF QUEUES
In this tutorial, you will learn different types of queues with along with illustration.
A queue is a useful data structure in programming. It is similar to the ticket queue outside a
cinema hall, where the first person entering the queue is the first person who gets the ticket.
There are four different types of queues:
Simple Queue
Circular Queue
Priority Queue
Double Ended Queue
Simple Queue
In a simple queue, insertion takes place at the rear and removal occurs at the front. It strictly
follows the FIFO (First in First out) rule.
Circular Queue
In a circular queue, the last element points to the first element making a circular link.
Priority Queue
A priority queue is a special type of queue in which each element is associated with a priority
and is served according to its priority. If elements with the same priority occur, they are
served according to their order in the queue.
Deque Representation
In this tutorial, you will learn what a circular queue is. Also, you will find implementation of
circular queue in C, C++, Java and Python.
A circular queue is the extended version of a regular queue where the last element is
connected to the first element. Thus forming a circle-like structure.
#include <stdio.h>
#define SIZE 5
int items[SIZE];
int front = -1, rear = -1;
// Adding an element
void enQueue(int element) {
if (isFull())
printf("\n Queue is full!! \n");
else {
if (front == -1) front = 0;
rear = (rear + 1) % SIZE;
items[rear] = element;
printf("\n Inserted -> %d", element);
}
}
// Removing an element
int deQueue() {
int element;
if (isEmpty()) {
printf("\n Queue is empty !! \n");
return (-1);
} else {
element = items[front];
if (front == rear) {
front = -1;
rear = -1;
}
// Q has only one element, so we reset the
// queue after dequeing it. ?
else {
front = (front + 1) % SIZE;
}
printf("\n Deleted element -> %d \n", element);
return (element);
}
}
int main() {
// Fails because front = -1
deQueue();
enQueue(1);
enQueue(2);
enQueue(3);
enQueue(4);
enQueue(5);
display();
deQueue();
display();
enQueue(7);
display();
return 0;
}
PRIORITY QUEUE
In this tutorial, you will learn what priority queue is. Also, you will learn about it's
implementations in Python, Java, C, and C++.
A priority queue is a special type of queue in which each element is associated with
a priority value. And, elements are served on the basis of their priority. That is, higher
priority elements are served first.
However, if elements with the same priority occur, they are served according to their order in
the queue.
Assigning Priority Value
Generally, the value of the element itself is considered for assigning the priority. For
example,
The element with the highest value is considered the highest priority element. However, in
other cases, we can assume the element with the lowest value as the highest priority element.
We can also set priorities according to our needs.
Removing Highest Priority Element
Heapify the tree.
Heapify after insertion
Algorithm for insertion of an element into priority queue (max-heap)
If there is no node,
create a newNode.
else (a node is already present)
insert the newNode at the end (last node from left to right.)
#include <stdio.h>
int size = 0;
void swap(int *a, int *b) {
int temp = *b;
*b = *a;
*a = temp;
}
// Function to heapify the tree
void heapify(int array[], int size, int i) {
if (size == 1) {
printf("Single element in the heap");
} else {
// Find the largest among root, left child and right child
int largest = i;
int l = 2 * i + 1;
int r = 2 * i + 2;
if (l < size && array[l] > array[largest])
largest = l;
if (r < size && array[r] > array[largest])
largest = r;
// Driver code
int main() {
int array[10];
insert(array, 3);
insert(array, 4);
insert(array, 9);
insert(array, 5);
insert(array, 2);
deleteRoot(array, 4);
printArray(array, size);
}
In this tutorial, you will learn what a double ended queue (deque) is. Also, you will find
working examples of different operations on a deque in C, C++, Java and Python.
Deque or Double Ended Queue is a type of queue in which insertion and removal of elements
can either be performed from the front or the rear. Thus, it does not follow FIFO rule (First In
First Out).
Representation of Deque
Types of Deque
Input Restricted Deque
In this deque, input is restricted at a single end but allows deletion at both the ends.
Output Restricted Deque
In this deque, output is restricted at a single end but allows insertion at both the ends.
Operations on a Deque
Below is the circular array implementation of deque. In a circular array, if the array is full,
we start from the beginning.
But in a linear array implementation, if the array is full, no more elements can be inserted. In
each of the operations below, if the array is full, "overflow message" is thrown.
Before performing the following operations, these steps are followed.
1. Take an array (deque) of size n.
2. Set two pointers at the first position and set front = -1 and rear = 0.
// Deque implementation in C
#include <stdio.h>
#define MAX 10
int main() {
int arr[MAX];
int front, rear, i, n;
n = count(arr);
printf("\nTotal number of elements in deque: %d", n);
}
if (*pfront == -1) {
*pfront = *prear = 0;
arr[*pfront] = item;
return;
}
if (*prear != MAX - 1) {
c = count(arr);
k = *prear + 1;
for (i = 1; i <= c; i++) {
arr[k] = arr[k - 1];
k--;
}
arr[k] = item;
*pfront = k;
(*prear)++;
} else {
(*pfront)--;
arr[*pfront] = item;
}
}
if (*pfront == -1) {
*prear = *pfront = 0;
arr[*prear] = item;
return;
}
if (*prear == MAX - 1) {
k = *pfront - 1;
for (i = *pfront - 1; i < *prear; i++) {
k = i;
if (k == MAX - 1)
arr[k] = 0;
else
arr[k] = arr[i + 1];
}
(*prear)--;
(*pfront)--;
}
(*prear)++;
arr[*prear] = item;
}
if (*pfront == -1) {
printf("\nDeque is empty.\n");
return 0;
}
item = arr[*pfront];
arr[*pfront] = 0;
if (*pfront == *prear)
*pfront = *prear = -1;
else
(*pfront)++;
return item;
}
if (*pfront == -1) {
printf("\nDeque is empty.\n");
return 0;
}
item = arr[*prear];
arr[*prear] = 0;
(*prear)--;
if (*prear == -1)
*pfront = -1;
return item;
}
Time Complexity
The time complexity of all the above operations is constant i.e. O(1).