DSA notes
DSA notes
DATA STRUCTURE:
Definition: Data Structures are the programmatic way of storing data so that it can be
stored, updated and retrieved efficiently and effectively.
Examples: For example, we can store a list of items having the same data-type using
the array data structure.
Suppose we want to store ‘n’ number of integers, we will have to declare ‘n’ int variables.
On the other hand, using an array, we can do it in a single statement (int array[n];). Similarly, if
we want to add these ‘n’ numbers, we will have to write all the int variables, but using an array,
we can do it very easily using loops. Similarly, using an array data structure, we can sort, find
and change values very easily.
There are many other examples where data structures are used in real life. Facebook,
Google Maps and many other social media apps use a data structure called Graph. Facebook
uses it to show you mutual connections with your friends on Facebook. Similarly, Google Maps
uses Graphs to show you the shortest path from your location to another location on map.
Data structures can also be categorized as static data structures (whose size cannot be
changed) and dynamic data structures (which can change their size).
ALGORITHM:
Definition: An Algorithm is a step-by-step procedure, which defines a set of instructions
to be executed in a certain order to get the desired output.
From the data structure point of view, following are some important categories of
algorithms −
• Search − Algorithm to search an item in a data structure.
• Sort − Algorithm to sort items in a certain order.
• Insert − Algorithm to insert item in a data structure.
• Update − Algorithm to update an existing item in a data structure.
• Delete − Algorithm to delete an existing item from a data structure.
PROPERTIES OF AN ALGORITHM:
An algorithm is defined as a finite set of instructions that, if followed, performs a
particular task. All algorithms must satisfy the following criteria/properties/characteristics:
Input: An algorithm has zero or more inputs, taken or collected from a specified set of
objects.
Output: An algorithm has one or more outputs having a specific relation to the inputs.
Definiteness: Each step must be clearly defined; each instruction must be clear and
unambiguous.
Finiteness: The algorithm must always finish or terminate after a finite number of steps.
Effectiveness: All operations to be accomplished must be sufficiently basic that they can
be done exactly and in finite length.
Feasibility − Should be feasible (possible) with the available resources.
NOTES:
• Write an algorithm to add two numbers entered by the user (A Natural Language
Example)
• Step 1: Start
• Step 2: Declare variables num1, num2 and sum.
• Step 3: Read values num1 and num2.
• Step 4: Add num1 and num2 and assign the result to sum.
• sum←num1+num2
• Step 5: Display sum
• Step 6: Stop
If we want to go from city "A" to city "B", there can be many ways of doing this. We can
go by flight, by bus, by train and also by bicycle. Depending on the availability and convenience,
we choose the one which suits us.
Similarly, in computer science, there are multiple algorithms to solve a problem. When
we have more than one algorithm to solve a problem, we need to select the best one.
Performance analysis helps us to select the best algorithm from multiple algorithms to solve a
problem.
Performance analysis of an algorithm is the process of calculating space and time required
by that algorithm.
Efficiency of an Algorithm:
When we want to analyze an algorithm, we consider two important factors i.e. space
complexity and time complexity.
ALGORITHM COMPLEXITY:
SPACE COMPLEXITY:
When we design an algorithm to solve a problem, it needs some computer memory to complete
its execution. For any algorithm, memory is required for the following purposes...
Note - When we want to perform analysis of an algorithm based on its Space complexity, we
consider only Data Space and ignore Instruction Space as well as Environmental Stack.
That means we calculate only the memory required to store Variables, Constants, Structures, etc.
To calculate the space complexity, we must know the memory required to store different
datatype values (according to the compiler). For example, the C++ Programming Language
compiler requires the following...
Example 1
int square(int a)
{
return a*a;
}
In the above piece of code, it requires 4 bytes of memory to store variable 'a' and another
4 bytes of memory is used for return value.
That means, totally it requires 8 bytes of memory to complete its execution. And this 8
bytes of memory is fixed for any input value of 'a'. This space complexity is said to be Constant
Space Complexity.
If any algorithm requires a fixed amount of space for all input values then that space
complexity is said to be Constant Space Complexity.
Example 2
int sum(int A[ ], int n)
{
int sum = 0, i;
for(i = 0; i < n; i++)
sum = sum + A[i];
return sum;
}
In the above piece of code it requires:
'n*4' bytes of memory to store array variable 'a[ ]'
4 bytes of memory for integer parameter 'n'
8 bytes of memory for local integer variables 'sum' and 'i' (4 bytes each)
4 bytes of memory for return value.
That means, totally it requires '4n+16' bytes of memory to complete its execution. Here,
the total amount of memory required depends on the value of 'n'. As 'n' value increases the space
required also increases proportionately. This type of space complexity is said to be Linear Space
Complexity.
If the amount of space required by an algorithm is increased with the increase of input value,
then that space complexity is said to be Linear Space Complexity.
TIME COMPLEXITY:
Every algorithm requires some amount of computer time to execute its instruction to
perform the task. This computer time required is called time complexity.
The time complexity of an algorithm is the total amount of time required by an algorithm to
complete its execution.
5. Input data.
Note - When we calculate time complexity of an algorithm, we consider only input data and
ignore the remaining things, as they are machine dependent. We check only, how our program
is behaving for the different input values to perform all the operations like Arithmetic, Logical,
Return value and Assignment etc.,
To calculate the time complexity of an algorithm, we need to define a model machine. Let us
assume a machine with following configuration...
Now, we calculate the time complexity of following example code by using the above-defined
model machine...
In the above sample code, it requires 1 unit of time to calculate a+b and 1 unit of time
to return the value. That means, totally it takes 2 units of time to complete its execution. And it
does not change based on the input values of a and b. That means for all input values, it require
the same amount of time i.e. 2 units.
If any program requires a fixed amount of time for all input values then its time complexity is
said to be Constant Time Complexity.
Example 2
int sum(int A[], int n)
{
int sum = 0, i;
for(i = 0; i < n; i++)
sum = sum + A[i];
return sum;
}
In above calculation
Cost is the amount of computer time required for a single operation in each line.
Repetition is the amount of computer time required by each operation for all its repetitions.
Total is the amount of computer time required by each operation to execute.
So above code requires '4n+4' Units of computer time to complete the task. Here the
exact time is not fixed. And it changes based on the n value. If we increase the n value then the
time required also increases linearly.
Totally it takes '4n+4' units of time to complete its execution and it is Linear Time
Complexity.
If the amount of time required by an algorithm is increased with the increase of input value
then that time complexity is said to be Linear Time Complexity.
ASYMPTOTIC NOTATIONS:
Whenever we want to perform analysis of an algorithm, we need to calculate the
complexity of that algorithm. But when we calculate the complexity of an algorithm it does not
provide the exact amount of resource required. So instead of taking the exact amount of
resource, we represent that complexity in a general form (Notation) which produces the basic
nature of that algorithm. We use that general form (Notation) for analysis process.
Asymptotic notation of an algorithm is a mathematical representation of its complexity.
Note - In asymptotic notation, when we want to represent the complexity of an algorithm, we
use only the most significant terms in the complexity of that algorithm and ignore least significant
terms in the complexity of that algorithm (Here complexity can be Space Complexity or Time
Complexity).
For example, consider the following time complexities of two algorithms...
• Algorithm 1 : 5n2 + 2n + 1
• Algorithm 2 : 10n2 + 8n + 3
Generally, when we analyze an algorithm, we consider the time complexity for larger values
of input data (i.e. 'n' value). In above two time complexities, for larger value of 'n' the term '2n +
1' in algorithm 1 has least significance than the term '5n2', and the term '8n + 3' in algorithm 2
has least significance than the term '10n2'.
Here, for larger value of 'n' the value of most significant terms ( 5n2 and 10n2 ) is very larger
than the value of least significant terms ( 2n + 1 and 8n + 3 ). So for larger value of 'n' we ignore
the least significant terms to represent overall time required by an algorithm. In asymptotic
notation, we use only the most significant terms to represent the time complexity of an
algorithm.
Majorly, we use THREE types of Asymptotic Notations and those are as follows...
1. Big - Oh (O)
2. Big - Omega (Ω)
3. Big - Theta (Θ)
Array: An Array is a linear data structure which is a finite collection of items of similar
data-types stored in successive or consecutive or contiguous (next to each other) memory
locations.
Arrays can be declared in various ways in different languages. For illustration, let's take
C++ array declaration:
As per the above illustration, following are the important points to be considered.
• Index starts with 0.
• Array length is 10 which means it can store 10 elements.
• Each element can be accessed via its index. For example, we can fetch an element at
index 6 as : array[6];
BASIC OPERATIONS WITH ARRAYS: MEMORY REPRESENTATION OF ARRAYS:
Following are the basic operations supported by an array.
• Traverse − print all the array elements one by one.
• Insertion − Adds an element at the given index.
• Deletion − Deletes an element at the given index.
• Search − Searches an element using the given index or by the value.
• Update − Updates an element at the given index.
MEMORY REPRESENTATION OF ARRAYS:
Stack: Stack is a linear data structure which follows a particular order in which the
operations are performed. The order may be LIFO (Last In First Out) or FILO (First In Last Out).
There are many real-life examples of a stack. Consider an example of plates or deck of
cards stacked over one another. It is named as stack because it behaves like a real-world stack.
A real-world stack allows operations at one end only. For example, we can place or
remove a card or plate from the top of the stack only. Likewise, Stack Data Structure allows all
data operations at one end only. At any given time, we can only access the top element of a
stack.
This feature makes it LIFO or FILO data structure. LIFO stands for Last-in-first-out, FILO
stands for First-In-Last-Out while. Here, the element which is placed (inserted or added) last, is
accessed first. In stack terminology, insertion operation is called PUSH operation and removal
operation is called POP operation
STACK REPRESENTATION:
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.
BASIC OPERATIONS:
Stack operations may involve initializing the stack, using it and then de-initializing it. Apart
from these basic stuffs, a stack is used for the following two primary operations −
• push() − Pushing (storing) an element on the top of stack.
• pop() − Removing (accessing) an element from the top of stack.
When data is PUSHed onto stack.
To use a stack efficiently, we need to check the status of stack as well. For the same purpose,
the following functionality is added to stacks −
• peek() / top() − get the top data element of the stack, without removing it.
• isFull() − check if stack is full.
• isEmpty() − check if stack is empty.
At all times, we maintain a pointer to the last PUSHed data on the stack. As this pointer
always represents the top of the stack, hence named top. The top pointer provides top value
of the stack without actually removing it.
First we should learn about procedures to support stack functions –
PEEK() / TOP():
Algorithm of peek() / top() function −
begin procedure peek
return stack[top]
end procedure
Implementation of peek() function in C++ programming language −
Example
int peek() {
return stack[top];
}
ISFULL():
Algorithm of isfull() function −
end procedure
Implementation of isfull() function in C++ programming language −
Example
bool isfull() {
if(top == MAXSIZE - 1)
return true;
else
return false;
}
ISEMPTY():
Algorithm of isempty() function −
end procedure
Here's the C++ code −
Example
bool isempty() {
if(top < 0)
return true;
else
return false;
}
PUSH OPERATION:
The process of putting a new data element onto stack is known as a Push Operation. Push
operation involves a series of steps −
• Step 1 − Checks if the stack is full.
• Step 2 − If the stack is full, produces an error and exit.
• Step 3 − If the stack is not full, increments top to point next empty space.
• Step 4 − Adds data element to the stack location, where top is pointing.
• Step 5 − Returns success.
If the linked list is used to implement the stack, then in step 3, we need to allocate space
dynamically.
Algorithm for PUSH Operation
A simple algorithm for Push operation can be derived as follows −
if stack is full
return null
endif
top ← top + 1
stack[top] ← data
end procedure
Implementation of this algorithm in C++, is very easy. See the following code −
Example
POP OPERATION:
Accessing the content while removing it from the stack, is known as a Pop Operation. In
an array implementation of pop() operation, the data element is not actually removed,
instead top is decremented to a lower position in the stack to point to the next value. But in
linked-list implementation, pop() actually removes data element and deallocates memory
space.
A Pop operation may involve the following steps −
• Step 1 − Checks if the stack is empty.
• Step 2 − If the stack is empty, produces an error and exit.
• Step 3 − If the stack is not empty, accesses the data element at which top is pointing.
• Step 4 − Decreases the value of top by 1.
• Step 5 − Returns success.
if stack is empty
return null
endif
data ← stack[top]
top ← top - 1
return data
end procedure
Implementation of this algorithm in C, is as follows −
Example
int pop() {
int data;
if(!isempty()) {
data = stack[top];
top = top - 1;
return data;
} else {
cout << "Could not retrieve data, Stack is empty.\n";
}
}
Queue: Like Stack, Queue is a linear structure which follows a particular order in which
the operations are performed. The order is First In First Out (FIFO) , i.e., the data item stored first
will be accessed first. Unlike stacks, a queue is open at both its ends (operations can be
performed at both ends). One end (rear) is always used to insert data (enqueue) and the other
(front) is used to remove data (dequeue).
EXAMPLES:
• Any queue of consumers for a in a departmental store, airport etc. where the
consumer that came first is served first.
• Another real-world example of queue can be a single-lane one-way road, where
the vehicle which enters first, exits first.
• More real-world examples can be seen as queues at the ticket windows and bus-
stops.
• CPU scheduling, Disk Scheduling.
• When data is transferred asynchronously between two processes.
• Queue is used for synchronization. e.g: IO Buffers, pipes, file IO, etc.
• Handling of interrupts in real-time systems.
• Call Center phone systems uses Queues to hold people calling them in an order.
The difference between stacks and queues is in removing. In a stack we remove the
item the most recently added; in a queue, we remove the item the least recently added.
QUEUE REPRESENTATION:
As we now understand that in queue, we access both ends for different reasons. The
following diagram given below tries to explain queue representation as data structure −
As in stacks, a queue can also be implemented using Arrays, Linked-lists, Pointers and
Structures. For the sake of simplicity, we shall implement queues using one-dimensional array.
BASIC OPERATIONS:
Queue operations may involve initializing or defining the queue, utilizing it, and then
completely erasing it from the memory. Here we shall try to understand the basic operations
associated with queues −
• enqueue() − add (store) an item to the queue.
• dequeue() − remove (access) an item from the queue.
Few more functions are required to make the above-mentioned queue operation efficient.
These are −
• peek() / front() − Gets the element at the front of the queue without removing it.
• isfull() − Checks if the queue is full.
• isempty() − Checks if the queue is empty.
In queue, we always dequeue (or access) data, pointed by front pointer and while enqueing
(or storing) data in the queue we take help of rear pointer.
Let's first learn about supportive functions of a queue −
PEEK():
This function helps to see the data at the front of the queue. The algorithm of peek()
function is as follows −
Algorithm
begin procedure peek
if queue is empty
display error msg
else
return queue[front]
end procedure
Implementation of peek() function in C++ programming language −
Example
int peek()
{
if(isEmpty() == true)
{
cout << "\n====>ERROR! Could not FRONT VALUE<====\n";
cout << "......Queue is Empty.....\n";
}
else
{
return Q[front_index];
}
ISFULL():
As we are using single dimension array to implement queue, we just check for the rear
pointer to reach at MAXSIZE to determine that the queue is full. In case we maintain the queue
in a circular linked-list, the algorithm will differ. Algorithm of isfull() function −
Algorithm
end procedure
Implementation of isfull() function in C++ programming language −
Example
bool isFull()
{
if(rear_index == MAX_SIZE - 1)
{
return true;
}
else
{
return false;
}
}
ISEMPTY():
Algorithm of isempty() function −
Algorithm
end procedure
If the value of both front and rear is equal to -1, it tells that the queue is not yet initialized, hence
empty.
Here's the C++ programming code −
Example
bool isEmpty()
{
if(front_index == -1 && rear_index == -1)
{
return true;
}
else
{
return false;
}
}
ENQUEUE OPERATION:
Queues maintain two data pointers, front and rear. Therefore, its operations are
comparatively difficult to implement than that of stacks.
The following steps should be taken to enqueue (insert) data into a queue −
• Step 1 − Check if the queue is full.
• Step 2 − If the queue is full, produce overflow error and exit.
• Step 3 − If the queue is not full, increment rear pointer to point the next empty space.
• Step 4 − Add data element to the queue location, where the rear is pointing.
• Step 5 − return success.
Sometimes, we also check to see if a queue is initialized or not, to handle any unforeseen
situations.
Algorithm for enqueue operation
procedure enqueue(data)
if queue is empty
rear front 0
q[rear] data
if queue is full
error msg
else
rear ← rear + 1
queue[rear] ← data
endif
end procedure
Implementation of enqueue() in C++ programming language −
Example
// Empty Case
if (isEmpty() == true)
{
front_index = 0;
rear_index = 0;
Q[rear_index] = value;
}
// Full Case
else if(isFull() == true)
{
cout << "\n====>ERROR! Could not Insert Data<====\n";
cout << "......Queue is Full.....\n";
}
// In Between Case
else
{
rear_index++;
Q[rear_index] = value;
}
}
DEQUEUE OPERATION:
Accessing data from the queue is a process of two tasks − access the data where front is
pointing and remove the data after access. The following steps are taken to
perform dequeue operation −
• Step 1 − Check if the queue is empty.
• Step 2 − If the queue is empty, produce underflow error and exit.
• Step 3 − If the queue is not empty, access the data where front is pointing.
• Step 4 − Increment front pointer to point to the next available data element.
• Step 5 − Return success.
if queue is empty
error msg
else if rear == front
data = q[front]
rear front -1
else
data = q[front]
front ← front + 1
return data
end procedure
Implementation of dequeue() in C programming language −
Example
int dequeue()
{
cout<<"Dequeuing \n";
int q_data;
// Empty Case
if(isEmpty())
{
cout << "\n====>ERROR! Could not Get Data<====\n";
cout << "......Queue is Empty.....\n";
}
// Only ONE ELEMENT in Queue
else if(front_index == rear_index)
{
q_data = Q[front_index];
rear_index = -1;
front_index = -1;
}
// More than ONE Elements
else
{
q_data = Q[front_index];
front_index++;
}
return q_data;
}
1) isFull():
Just replace:
if(rear_index == MAX_SIZE - 1)
with:
if((rear_index + 1) % MAX_SIZE == front_index)
2) enqueue():
In the last else condition, Just replace:
rear_index++;
with:
rear_index = (rear_index + 1) % MAX_SIZE;
3) dequeue():
In the last else condition, Just replace:
front_index++;
with:
front_index = (front_index + 1) % MAX_SIZE;
4) displayQueue():
There will be changes in this method as well, but display is not a
functionality of a queue. It is just a checking process.
PRIORITY QUEUE:
Priority Queue: A Priority Queue is a data structure in which each element is assigned a
priority. The priority of the element will be used to determine the order in which the elements
will be processed.
The general rules for processing the elements of a priority queue are:
1) An element/item with higher priority will be processed before an element with lower
priority.
2) Two elements with same priority are processed on a FCFS (First Come First Serve) basis.
RECURSION:
The concept of recursion in computer science works similarly. Here, we talk about
recursive algorithms and recursive functions. An algorithm that finds the solution to a given
problem by reducing the problem to smaller versions of itself is called a recursive algorithm. The
recursive algorithm must have one or more base cases, and the general solution must
eventually be reduced to a base case.
A function that calls itself is called a recursive function. That is, the body of the
recursive function contains a statement that causes the same function to execute again
before completing the current call. Recursive algorithms are implemented using recursive
functions.
PROPERTIES:
There are two properties that a recursive function must have −
• Base criteria − There must be at least one base criteria or condition, such that, when this
condition is met the function stops calling itself recursively.
• Progressive approach − The recursive calls should progress in such a way that each time
a recursive call is made it comes closer to the base criteria.
IMPLEMENTATION:
Many programming languages implement recursion by means of stacks. Generally,
whenever a function (caller) calls another function (callee) or itself as callee, the caller function
transfers execution control to the callee. This transfer process may also involve some data to be
passed from the caller to the callee.
This implies, the caller function has to suspend its execution temporarily and resume later
when the execution control returns from the callee function. Here, the caller function needs to
start exactly from the point of execution where it puts itself on hold. It also needs the exact same
data values it was working on. For this purpose, an activation record (or stack frame) is created
for the caller function.
This activation record keeps the information about local variables, formal parameters,
return address and all information passed to the caller function.
Example:
TYPES OF RECURSION:
Direct Recursion: in Direct Recursion, a function calls itself directly. For Example, A
function fun is called direct recursive if it calls itself.
Indirect Recursion: in Indirect Recursion, a function calls another function and the
function calls back the previous function again. For Example, A function fun1 is called indirect
recursive if it calls another function say fun2 and fun2 calls fun1 directly or indirectly.
// An example of direct recursion
void directRecFun()
{
// Some code....
directRecFun();
// Some code...
}
int main()
{
int num = 6, factorial = 1;
return 0;
}
Mathematically, we can break down 6! In the form of simple sub cases as:
Here, we have two different cases, the base case and the general cases. We can say that
we have divided our problem in a top-to-bottom (Divide and Conquer) approach. At the top
level, we have general cases and then moving down till the end which is our Base Case that ends
the division. So, we can simplify our program by writing a recursive function of the above stated
cases. Below is the recursive function:
#include <iostream>
return 0;
}
Example-2:
Power of a number: Suppose we want to find the power of a number, let’s say 3 ^ 5.
#include <iostream>
Mathematically, we can break down 3 ^ 5 in the form of simple sub cases as:
#include <iostream>
using namespace std;
int main()
{
int num = 3, pow = 5, result = 1;
result = power(num,pow);
cout << num << " ^ " << pow << " = " << result << endl;
return 0;
}
SEARCHING AND SORTING:
SEARCHING ALGORITHMS:
1) LINEAR SEARCH:
Linear search is the simplest searching algorithm that searches for an element in a list
in sequential order. We start at one end and check every element until the desired element is
not found.
The following steps are followed to search for an element k = 1 in the list below.
2) BINARY SEARCH:
Binary Search is a searching algorithm for finding an element's position in a sorted array. In
this approach, the element is always searched in the middle of a portion of an array. Binary
search can be implemented only on a sorted list of items. If the elements are not sorted already,
we need to sort them first.
2. Set two pointers low and high at the lowest and the highest positions respectively.
3. Find the middle element mid of the array ie. (arr[low + high]) / 2 = 6.
4. If x == mid, then return mid.
Else, compare the element to be searched with m.
5. If x > mid, compare x with the middle element of the elements on the right side of mid.
This is done by setting low to low = mid + 1.
6. Else, compare x with the middle element of the elements on the left side of mid.
This is done by setting high to high = mid - 1.
8. x = 4 is found.
2. Conquer: Solve the smaller sub-problems recursively. If the sub-problem is small enough,
then solve it directly.
3. Combine: Combine the solutions of the sub-problems which is part of the recursive
process to get the solution to the actual problem.
Here, we are going to sort an array using the divide and conquer approach (ie. merge sort).
Again, divide each subpart recursively into two halves until you get individual elements.
2. Compare minimum with the second element. If the second element is smaller
than minimum, assign the second element as minimum.
Compare minimum with the third element. Again, if the third element is smaller, then
assign minimum to the third element otherwise do nothing. The process goes on until
the last element.
3. After each iteration, minimum is placed in the front of the unsorted list.
4. For each iteration, indexing starts from the first unsorted element. Step 1 to 3 are
repeated until all the elements are placed at their correct positions.
PROGRAM: in code file.
2) INSERTION SORT:
Insertion sort works similarly as we sort cards in our hand in a card game.
We assume that the first card is already sorted then, we select an unsorted card. If the
unsorted card is greater than the card in hand, it is placed on the right otherwise, to the left. In
the same way, other unsorted cards are taken and put at their right place.
A similar approach is used by insertion sort. Insertion sort is a sorting algorithm that places
an unsorted element at its suitable place in each iteration.
1. The first element in the array is assumed to be sorted. Take the second element and store it
separately in key.
Compare key with the first element. If the first element is greater than key, then key is placed
in front of the first element.
Take the third element and compare it with the elements on the left of it. Placed it just behind
the element smaller than it. If there is no element smaller than it, then place it at the
beginning of the array.
3. Similarly, place every unsorted element at its correct position.
4. Finally, we have:
PROGRAM: in code file.
3) BUBBLE SORT:
Bubble sort is an algorithm that compares the adjacent elements and swaps their positions
if they are not in the intended order. The order can be ascending or descending.
1. Starting from the first index, compare the first and the second elements. If the first element
is greater than the second element, they are swapped.
Now, compare the second and the third elements. Swap them if they are not in order.
In each iteration, the comparison takes place up to the last unsorted element.
The array is sorted when all the unsorted elements are placed at their correct positions.
PROGRAM: in code file.
4) QUICK SORT:
Quick sort is a highly efficient sorting algorithm based on divide and conquer approach and
is also based on partitioning of array of data into smaller arrays. A large array is partitioned into
two arrays one of which holds values smaller than the specified value, say pivot, based on which
the partition is made and another array holds values greater than the pivot value.
Quicksort partitions an array and then calls itself recursively twice to sort the two resulting
subarrays.
How QuickSort Works?
1. A pivot element is chosen from the array. You can choose any element from the array as the
pivot element.
Here, we have taken the rightmost (i.e. the last element) of the array as the pivot
element.
2. The elements smaller than the pivot element are put on the left and the elements greater
than the pivot element are put on the right.
a. A pointer is fixed at the pivot element. The pivot element is compared with the elements
beginning from the low index using ‘j’ pointer. If the element greater than the pivot
element is reached, a second pointer ‘I’ pointer is set for that element.
b. Now, the pivot element is compared with the other elements (a third pointer). If an
element smaller than the pivot element is reached, the smaller element is swapped with
the greater element found earlier.
c. The process goes on until the second last element is reached.
Finally, the pivot element is swapped with the second pointer.
3. Pivot elements are again chosen for the left and the right sub-parts separately. Within these
sub-parts, the pivot elements are placed at their right position. Then, step 2 is repeated.
4. The sub-parts are again divided into smaller sub-parts until each subpart is formed of a
single element.
• Divide
The array is divided into subparts taking pivot as the partitioning point. The elements
smaller than the pivot are placed to the left of the pivot and the elements greater than
the pivot are placed to the right.
• Conquer
The left and the right subparts are again partitioned using the by selecting pivot elements
for them. This can be achieved by recursively passing the subparts into the algorithm.
• Combine
This step does not play a significant role in quicksort. The array is already sorted at the
end of the conquer step.
You can understand the working of quicksort with the help of the illustrations below.
PROGRAM: in code file.
5) MERGE SORT:
Merge Sort: Like Quick Sort, Merge Sort is also a Divide and Conquer algorithm. It is also
one of the most popular sorting algorithms.
Merge sort first divides the array into equal halves and then combines them in a sorted
manner.
After that, the merge function comes into play and combines the sorted arrays into
larger arrays until the whole array is merged.
mid = (low+high)/2
}
}
The following diagram shows the complete merge sort process for an example array {38,
27, 43, 3, 9, 82, 10}. If we take a closer look at the diagram, we can see that the array is recursively
divided in two halves till the size becomes 1. Once the size becomes 1, the merge processes
comes into action and starts merging arrays back till the complete array is merged.
Merge Sort Diagram:
The algorithm maintains three pointers, one for each of the two arrays and one for
maintaining the current index of the final sorted array.
No:
Yes:
This is why we only need the array, the first position, the last index of the first subarray(we
can calculate the first index of the second subarray) and the last index of the second subarray.
Our task is to merge two subarrays A[p..q] and A[q+1..r] to create a sorted array A[p..r].
So the inputs to the function are A, p, q and r
3. Until we reach the end of either L or M, pick the larger among the elements
from L and M and place them in the correct position at A[p..q]
4. When we run out of elements in either L or M, pick up the remaining elements and put
in A[p..q]
Merge( ) Function Explained Step-By-Step
A lot is happening in this function, so let's take an example to see how this would work.
The array A[0..5] contains two sorted subarrays A[0..3] and A[4..5]. Let us see how the merge
function will merge the two arrays.
int i, j, k;
i = 0;
j = 0;
k = p;
Step 3: Until we reach the end of either L or M, pick larger among elements L and M and place
them in the correct position at A[p..r]
Step 4: When we run out of elements in either L or M, pick up the remaining elements and put
in A[p..r]
This step would have been needed if the size of M was greater than L. At the end of the
merge function, the subarray A[p..r] is sorted.
6) COUNTING SORT:
Counting Sort: Counting Sort is a sorting algorithm that does not use any sort of
comparison for sorting the elements. Because of this ability of this algorithm, it is even more
powerful and efficient than merge sort and quick sort in some situations.
For example, assume that we are asked to sort ‘n’ elements, but we are informed that
each element is in the range of (0—k), where ‘k’ is much smaller than ‘n’. Counting Sort
algorithm can take advantage of this situation and it can work better than quick sort and merge
sort algorithms. The algorithm was created by Harold H. Seward in 1954.
Counting sort is a sorting algorithm that sorts the elements of an array by counting the
number of occurrences of each unique element in the array. The count is stored in an auxiliary
array and the sorting is done by mapping the count as an index of the auxiliary array.
Where, n = total number of elements in input array and k = max value in input array.
EXPLANATION #1:
2. Initialize count array of length ‘k’ with all elements 0. This array is used for storing the
count of the elements (i.e. how many times an element appears in the initial array) in
the array.
3. Store the count of each element at their respective index in count array.
For example: if the count of element 3 is 2 then, 2 is stored in the 3rd position
of count array. If element "5" is not present in the array, then 0 is stored in 5th position.
4. Store cumulative sum of the elements of the count array. It helps in placing the elements
into the correct index of the sorted array.
5. Find the index of each element of the original array in the count array. This gives the
cumulative count. Place the element at the index calculated as shown in figure below.
6. After placing each element at its correct position, decrease its count by one.
EXPKLANATION #2:
2) Modify the count array such that each element at each index
stores the sum of previous counts.
Index: 0 1 2 3 4 5 6 7 8 9
Count: 0 2 4 4 5 6 6 7 7 7
countingSort(array, size)