Essential Data Structures Overview
Essential Data Structures Overview
Before Mid
Table of Contents
Week 1 ............................................................................................................................................................... 3
Data Structure ................................................................................................................................................ 3
Importance of Data Structure ........................................................................................................................ 3
Real world applications of data structures in software development ........................................................... 4
Week 2 ............................................................................................................................................................... 6
Abstract Data Type (ADT) ............................................................................................................................... 6
Complexity analysis in data structures .......................................................................................................... 7
➢ Asymptotic Notations in Complexity Analysis.................................................................................... 7
➢ How to measure complexity .............................................................................................................. 7
Big O notation .............................................................................................................................................. 10
Week 3 ............................................................................................................................................................. 12
Basic terminology......................................................................................................................................... 12
1. Arrays ................................................................................................................................................... 12
2. Linked Lists ........................................................................................................................................... 13
3. Stacks ................................................................................................................................................... 14
4. Queues ................................................................................................................................................. 14
Week 4 ............................................................................................................................................................. 15
Searching an Unsorted Array ....................................................................................................................... 15
1. Linear Search / Sequential Search / Sentinel Linear Search ............................................................ 15
2. Binary Search / Half-Interval Search / Logarithmic Search / Binary Chop ....................................... 16
Operations on arrays .................................................................................................................................... 18
Week 5 ............................................................................................................................................................. 23
Stack ............................................................................................................................................................. 23
Implementation ........................................................................................................................................... 23
• Array-based Implementation ........................................................................................................... 24
• Linked List-based Implementation ................................................................................................... 27
Week 6 ............................................................................................................................................................. 32
Recursion in Data Structures........................................................................................................................ 32
Divide and Conquer Algorithms ................................................................................................................... 33
Week 7 ............................................................................................................................................................. 34
Sorted linked list, single and doubly linked list ............................................................................................ 34
Types of Linked List ...................................................................................................................................... 35
1. Singly Linked List .............................................................................................................................. 35
1
2. Doubly Linked List ............................................................................................................................ 40
3. Circular Linked List ........................................................................................................................... 49
Sorted linked list........................................................................................................................................... 57
Sort Linked List using Bubble Sort ............................................................................................................ 57
Sort Linked List using Insertion Sort ......................................................................................................... 58
Sort Linked List using Quick Sort .............................................................................................................. 58
Sort Linked List using Merge Sort............................................................................................................. 58
Week 8 ............................................................................................................................................................. 59
Sorting Algorithm ......................................................................................................................................... 59
Selection sort ................................................................................................................................... 60
Insertion Sort ................................................................................................................................... 64
Merge sort........................................................................................................................................ 67
Quick Sort ......................................................................................................................................... 71
Comparison Between Sorting Algorithms................................................................................................ 75
The End ............................................................................................................................................................ 76
2
Week 1
Data Structure
“A data structure is a specialized format for organizing, storing, and managing data in
a computer's memory or storage. It defines how data elements are arranged, the
relationships among them, and the operations that can be performed on the data.”
The primary goal of using data structures is to enable efficient access, modification, and processing
of data, optimizing both time and space complexity in software applications e.g. Arrays.
→ Data Organization:
They provide a systematic way to store and organize data, making it easier for
computers to process and manage.
→ Algorithm Design:
Data structures are the foundation for designing efficient algorithms, which are
sequences of instructions that solve specific problems.
→ Resource Management:
Appropriate data structures help optimize memory usage, improving overall system
performance and resource management.
→ Scalability:
They are vital for handling the growing volume of data in modern applications, from
databases to big data frameworks and AI.
→ Code Reusability:
3
By providing structured ways to format and store data, they allow for the creation of
reusable components that can be accessed and utilized by different parts of a system.
Data structures are a core concept in computer science, forming the basis for
understanding how data is managed and manipulated at a fundamental level.
→ Employability:
Proficiency in data structures is a highly valued skill in the tech industry and is often
tested in job interviews for programming roles.
Efficiently index and organize large datasets, optimizing search and retrieval
operations.
• Hash Tables: Provide fast data access and indexing for key-value pairs.
3. Web Development:
• Hash Maps:
• Decision Trees:
4
Used for classification and regression tasks, creating a tree-like model of
decisions.
• Graphs:
5. Search Engines:
Efficiently index web pages and facilitate rapid searching and auto-completion.
• Graphs:
• Arrays:
7. Networking:
Model network topologies and enable routing algorithms to find optimal paths.
8. Compiler Design:
Store information about variables, functions, and other program entities during
compilation.
5
Week 2
Abstract Data Type (ADT)
“An Abstract Data Type (ADT) is a mathematical model for data types, defined by its
behavior and operations rather than its implementation.”
It specifies what operations can be performed on the data and what their behavior is, but not
how those operations are implemented or how the data is organized in memory.
→ ADTs hide the internal details of data representation and operation implementation from
the user.
→ Users interact with the ADT through a defined interface of operations, without needing to
know the underlying mechanisms.
• Behavioral Definition:
Defines operations like push, pop, peek, isEmpty, with LIFO behavior.
• Queue ADT:
Defines operations like enqueue, dequeue, front, isEmpty, with FIFO (First-In-First-Out) behavior.
• List ADT:
Defines operations like add, remove, get, set, size, allowing ordered collections of elements.
• Set ADT:
Defines operations like add, remove, contains, union, intersection, storing unique elements without
a specific order.
Summarizing / Conclusion:
6
In essence, ADTs provide a high-level, conceptual view of data and its manipulation, promoting
modularity, reusability, and easier reasoning about software systems. They focus on the "what" of
data management, leaving the "how" to concrete data structure implementations.
OR
This analysis helps in understanding and comparing the efficiency of different algorithms.
Represents the upper bound or worst-case scenario of an algorithm's running time. It describes
the maximum time an algorithm will take for a given input size.
Represents the lower bound or best-case scenario. It describes the minimum time an algorithm
will take.
Represents both the upper and lower bounds, indicating that the algorithm's running time is
tightly bound within a certain range.
“Time complexity measures how the execution time of an algorithm grows with
the input size.”
7
Statement 1: int a=5; // reading a variable
• Assuming that n is the size of the input, let's use T(n) to represent the overall
time and t to represent the amount of time that a statement or collection of
statements takes to execute.
Operations that take a fixed amount of time regardless of input size (e.g., accessing an
element in an array by index).
Operations where the time required grows logarithmically with the input size (e.g., binary
search).
Operations where the time required grows linearly with the input size (e.g., traversing a linked
list).
Often seen in efficient sorting algorithms (e.g., Merge Sort, Quick Sort).
Operations where the time required grows exponentially with the input size (e.g., some
recursive algorithms without memorization).
8
2. Space Complexity:
→ This includes both the fixed part (memory for constants, simple variables) and the
variable part (memory for dynamic data structures, recursion stack, temporary
variables).
→ How is space complexity computed?
• The space Complexity of an algorithm is the total space taken by the algorithm with
respect to the input size. Space complexity includes both Auxiliary space and
space used by input.
• Space complexity is a parallel concept to time complexity. If we need to create an
array of size n, this will require O(n) space. If we create a two-dimensional array
of size n*n, this will require O(n2) space.
• In recursive calls stack space also counts.
→ Example:
int add (int n){
if (n <= 0)}
return 0;
}
return n + add (n-1);
1. add(4)
2. -> add(3)
3. -> add(2)
4. -> add(1)
5. -> add(0)
Each of these calls is added to call stack and takes up actual memory.
So it takes O(n) space.
However, just because you have n calls total doesn’t mean it takes O(n) space.
3. Auxiliary Space:
“The temporary space needed for the use of an algorithm is referred to as auxiliary
space.”
9
→ For example, sorting algorithms take O(n) space, as there is an input array to sort.
but auxiliary space is O(1) in that case.
Allows for objective comparison of different algorithms for the same problem.
• Performance Prediction:
• Optimization:
Identifies bottlenecks and areas for optimization within an algorithm or data structure.
• Resource Management:
Aids in making informed decisions about resource allocation for software development.
Big O notation
“Big O notation is a powerful tool used in computer science to describe the time
complexity or space complexity of algorithms.”
Big O notation, in the context of data structures and algorithms, is a mathematical notation used to
describe the worst-case time or space complexity of an algorithm as the input size grows.
It provides an upper bound on the growth rate of an algorithm's resource consumption.
Key Characteristics:
Here's a breakdown of its key aspects:
• Upper Bound:
Big O notation focuses on the maximum time or space an algorithm will require. It represents a limit
that the algorithm's performance will not exceed.
• Asymptotic Analysis:
It describes the algorithm's behavior for very large input sizes, ignoring constant factors and lower-
order terms that become insignificant as 'n' grows.
• Growth Rate:
Big O describes how the time or space requirements scale with increasing input size. It
categorizes algorithms based on their fundamental growth patterns, such as constant,
logarithmic, linear, quadratic, or exponential.
It categorizes algorithms based on their growth rate, such as constant (O(1)), logarithmic
(O(log n)), linear (O(n)), quadratic (O(n²)), or exponential (O(2^n)).
10
• Efficiency Comparison:
It allows for a standardized way to compare the efficiency of different algorithms, helping to
determine which algorithm is more suitable for a given problem, especially with large datasets.
• Worst-Case Scenario:
Big O focuses on the maximum amount of time or space an algorithm will take to complete,
assuming the most unfavorable input. This is important for understanding how an algorithm will
perform under stress.
The complexity is expressed as a function of 'n', which represents the size of the input data. For
example, in an array, 'n' might be the number of elements.
• Machine Independence:
Big O notation abstracts away machine-specific details (like processor speed or memory access
times), focusing on the number of operations or memory units required.
Summarizing / Conclusion:
In essence, Big O notation provides a standardized way to compare and analyze the
efficiency of different data structures and algorithms, allowing developers to choose the most
appropriate solution for a given problem based on its performance characteristics.
11
Week 3
Basic terminology
1. Arrays
• Definition: A linear data structure that stores a collection of elements of the same data type
in contiguous memory locations.
• Index / position / Subscript: A numerical position of an element within the array, typically
starting from 0.
• Size / Length / no. of elements: The total number of elements an array can hold.
• Types of arrays
→ Static Arrays: These have a fixed size that is determined at compile-time. They are
generally stored on the stack.
→ Fast Access: Because elements are stored in adjacent memory locations, you can access
any element in constant time, 𝑂(1), using its index.
→ Memory Efficient: There is very little overhead since no extra memory is needed for links or
pointers between elements.
→ Cache Friendly: Contiguous memory access is more efficient for the CPU cache, which can
lead to better performance for large datasets.
• Disadvantages:
→ Fixed Size: The most significant drawback is its static nature. If you need to add more
elements than the array can hold, you must create a new, larger array and copy all the
elements over, which is an inefficient process.
→ Inefficient Insertions/Deletions: Adding or removing an element in the middle of an array
requires shifting all subsequent elements, which is a slow operation.
→ Wasted Memory: If an array is allocated with more space than is ultimately needed, the
unused memory is wasted.
12
→ Homogeneous: The restriction to a single data type limits its flexibility.
• Common applications:
→ Storing data: Arrays are used to store collections of data like a list of exam scores, inventory
items, or phone contacts.
→ Implementing other data structures: Stacks, queues, and hash tables can all be built using
an array as their foundation.
→ CPU scheduling: Algorithms for managing CPU processes often utilize arrays.
→ Sorting and searching: Most sorting algorithms, such as quicksort or merge sort, and
searching algorithms like binary search, operate on arrays.
2. Linked Lists
• Definition: A linear data structure where elements are stored in nodes, and each node
contains data and a reference (or pointer) to the next node in the sequence.
• Node: The fundamental building block of a linked list, comprising data and a pointer to the
next node.
• Head: A pointer to the first node in the linked list.
• Null/None: A special value in the pointer of the last node, indicating the end of the list.
→ Traversal:
Starting from the head node, a temporary pointer iterates through the list, moving from
one node to the next using the next pointer until the end of the list (where
the next pointer is nullptr) is reached.
During traversal, the data within each node can be accessed or processed.
→ Insertion:
At the beginning: A new node is created, its next pointer is set to the current head,
and the head pointer is updated to point to the new node.
At the end: A new node is created, and the next pointer of the current last node is
updated to point to the new node. If the list is empty, the new node becomes the head.
At a specific position: A new node is created, and the list is traversed to find the
node before the desired insertion point. The new node's next pointer is set to
the next of the preceding node, and the preceding node's next pointer is updated to
point to the new node.
→ Deletion:
13
From the beginning: The head pointer is moved to the next node, and the original
head node is deallocated.
From the end: The list is traversed to find the second-to-last node. The next pointer
of the second-to-last node is set to nullptr, and the last node is deallocated.
Of a specific node: The list is traversed to find the node to be deleted and its
preceding node. The next pointer of the preceding node is updated to bypass the
deleted node, and the deleted node is deallocated.
→ Search:
The list is traversed from the head, comparing the data in each node with the target
value.
If a match is found, the search stops, and the node (or its data) is returned. If the end
of the list is reached without a match, the element is not found.
→ Sorting:
Various sorting algorithms can be applied to linked lists, such as bubble sort, insertion
sort, or merge sort. These algorithms involve rearranging the nodes or their data
based on a defined order.
3. Stacks
• Definition: A linear data structure that follows the Last-In, First-Out (LIFO) principle, meaning
the last element added is the first one to be removed.
→ Pop: The operation of removing and returning the top element from the stack.
→ Peek/Top: An operation to view the top element of the stack without removing it.
4. Queues
• Definition: A linear data structure that follows the First-In, First-Out (FIFO) principle, meaning
the first element added is the first one to be removed.
• Operation on Queue:
→ Enqueue: The operation of adding an element to the rear (or tail) of the queue.
→ Dequeue: The operation of removing and returning the element from the front (or head)
of the queue.
→ Front/Peek: An operation to view the element at the front of the queue without removing
it.
→ Rear/Tail: The position where new elements are added to the queue.
14
Week 4
Searching an Unsorted Array
Searching for an element in an unsorted array in C++ is typically done using a linear search
algorithm.
This method involves iterating through each element of the array sequentially until the target
element is found or the end of the array is reached.
• Comparison: Compare the current element with the target value you are searching for.
• Match Found: If the current element matches the target value, the search is successful, and
the index of the current element is returned.
• No Match: If the current element does not match the target value, move to the next element
in the array.
• Iteration: Repeat steps 2-4 until either the target element is found or all elements in the array
have been checked.
• Element Not Found: If the end of the array is reached without finding the target element, it
means the element is not present in the array, and a suitable indicator (e.g., -1) is returned.
15
cout << "Number to find: " ;
cin >> target;
if (loc == -1) {
cout << "Element not found in the list." <<endl;
} else {
cout << "Element found at index " << loc << " in the list." <<endl;
}
return 0;
}
• Initialize:
Define a low pointer at the beginning of the array and a high pointer at the end.
• Find Midpoint:
Calculate the mid index, typically as low + (high - low) / 2 to prevent potential integer overflow.
• Compare:
→ If the element at mid is equal to the target, the target is found, and its index is returned.
→ If the element at mid is less than the target, the target must be in the right half of the current
search space. Update low = mid + 1.
→ If the element at mid is greater than the target, the target must be in the left half of the current
search space. Update high = mid - 1.
• Repeat:
Continue steps 2 and 3 until the target is found or low becomes greater than high, indicating the
target is not present in the array.
Program:
16
#include <iostream>
using namespace std;
int main() {
// The array must be sorted for binary search to work
int arr[] = {2, 5, 9, 12, 17, 37, 86};
int n = sizeof(arr) / sizeof(arr[0]); // Calculate the size of the array
int target;
int low = 0;
int high = n - 1;
int result = -1;
Binary search is an efficient algorithm for finding an item within a sorted array. It works by repeatedly
dividing the search interval in half. If the value of the search key is less than the item in the middle
of the interval, the algorithm narrows the interval to the lower half. Otherwise, it narrows it to the
upper half. This process continues until the value is found or the interval is empty.
Operations on arrays
The following operations can be done on the Array:
→ Traversal:
Traversal is the process of visiting and accessing each element of an array exactly
once. It is typically done using a loop that iterates from the first element to the last.
→ Insertion:
Insertion adds a new element to the array. The time complexity of this operation varies
depending on the insertion point.
Insertion at the end: If there is space, this is very fast, taking 𝑂(1)constant time.
Insertion at the beginning or middle: Requires shifting all subsequent elements to make
space, which is an 𝑂(𝑛) operation.
Example: Inserting 25 at index 2 in an array [10, 20, 30, 40].
o Shift 30 to index 3.
o Shift 40 to index 4.
o Place 25 at index 2.
o Resulting array: [10, 20, 25, 30, 40].
→ Deletion:
Deletion removes an element from the array. The performance is dependent on the
position of the element being removed.
Deletion from the end: A constant-time 𝑂(1) operation, as no other elements need to
be moved.
Deletion from the beginning or middle: Requires shifting all subsequent elements
to fill the gap, making it an 𝑂(𝑛) operation.
Example: Deleting the element at index 1 (20) from [10, 20, 30, 40].
18
o Shift 30 to index 1.
o Shift 40 to index 2.
o The array becomes [10, 30, 40].
→ Searching:
Searching involves finding the location of a specific element within the array.
Linear Search: Checks each element sequentially until a match is found. It works on
unsorted arrays and has a time complexity of 𝑂(𝑛) in the worst case.
Binary Search: An optimized search method that only works on a sorted array. It repeatedly
divides the search interval in half and has a much faster time complexity of 𝑂(log𝑛).
→ Update
Updating an element involves changing the value at a specific index. This is a very
efficient operation.
Time Complexity:
▪ 𝑂(1), because you can access the element directly using its index and overwrite
its value.
Example: Updating the element at index 2 in array [10, 20, 30, 40] to 35.
1. Access arr[2].
2. Assign the new value: arr[2] = 35.
3. Resulting array: [10, 20, 35, 40].
Program(Operations):
#include <iostream>
using namespace std;
// Function prototypes
void displayArray(const int arr[], int size);
void insertElement(int arr[], int &size, int element, int position);
void deleteElement(int arr[], int &size, int position);
int linearSearch(const int arr[], int size, int target);
void updateElement(int arr[], int size, int position, int newValue);
int main() {
int arr[MAX_SIZE] = {10, 20, 30, 40, 50};
int size = 5;
int choice, element, position, target, newValue;
int searchResult;
do {
cout << "\n--- Array Operations Menu ---" << endl;
cout << "1. Display Array" << endl;
cout << "2. Insert Element" << endl;
cout << "3. Delete Element" << endl;
cout << "4. Search Element" << endl;
19
cout << "5. Update Element" << endl;
cout << "6. Exit" << endl;
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
displayArray(arr, size);
break;
case 2:
cout << "Enter element to insert: ";
cin >> element;
cout << "Enter position to insert at (0-indexed): ";
cin >> position;
insertElement(arr, size, element, position);
break;
case 3:
cout << "Enter position to delete (0-indexed): ";
cin >> position;
deleteElement(arr, size, position);
break;
case 4:
cout << "Enter element to search: ";
cin >> target;
searchResult = linearSearch(arr, size, target);
if (searchResult != -1) {
cout << "Element " << target << " found at index " << searchResult << "."
<< endl;
} else {
cout << "Element " << target << " not found in the array." << endl;
}
break;
case 5:
cout << "Enter position to update (0-indexed): ";
cin >> position;
cout << "Enter new value: ";
cin >> newValue;
updateElement(arr, size, position, newValue);
break;
case 6:
cout << "Exiting program. Goodbye!" << endl;
break;
default:
cout << "Invalid choice. Please try again." << endl;
}
} while (choice != 6);
20
return 0;
}
22
Week 5
Stack
A stack is a linear data structure that adheres to the Last In, First Out (LIFO) principle. This means
that the last element added to the stack is the first one to be removed. Imagine a stack of plates:
you always add new plates to the top, and when you take a plate, you take the one from the top,
which was the last one placed.
Key Characteristics:
• LIFO (Last In, First Out): The fundamental principle governing element access.
• Single-ended Operations: All insertions and deletions occur at one end, referred to as the
"top" of the stack.
Basic Operations:
• Push: Adds a new element to the top of the stack.
• Pop: Removes and returns the element from the top of the stack.
• Peek (or Top): Returns the top element without removing it.
• isFull: Checks if the stack has reached its maximum capacity (relevant for array-based
implementations).
Implementation
Stacks can be implemented using various underlying data structures, such as arrays or linked lists.
23
• Array-based Implementation
Uses a fixed-size array to store elements and a pointer (often called "top") to keep track of the top
element's index.
Example Program:
#include <iostream>
using namespace std;
class Stack {
private:
int arr[MAX];
int top;
public:
Stack() {
top = -1; // Stack is initially empty
}
// Push operation
void push(int value) {
if (isFull()) {
cout << "Stack Overflow! Cannot push " << value << endl;
} else {
24
top++;
arr[top] = value;
cout << value << " pushed into the stack.\n";
}
}
// Pop operation
void pop() {
if (isEmpty()) {
cout << "Stack Underflow! Nothing to pop.\n";
} else {
cout << arr[top] << " popped from the stack.\n";
top--;
}
}
25
}
}
};
// Main Function
int main() {
Stack s;
int choice, value;
do {
cout << "\n--- Stack Menu ---\n";
cout << "1. Push\n";
cout << "2. Pop\n";
cout << "3. Peek (Top Element)\n";
cout << "4. Check if Empty\n";
cout << "5. Check if Full\n";
cout << "6. Display Stack\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value to push: ";
cin >> value;
[Link](value);
break;
case 2:
[Link]();
break;
case 3:
[Link]();
26
break;
case 4:
cout << ([Link]() ? "Stack is empty.\n" : "Stack is not empty.\n");
break;
case 5:
cout << ([Link]() ? "Stack is full.\n" : "Stack is not full.\n");
break;
case 6:
[Link]();
break;
case 0:
cout << "Exiting program...\n";
break;
default:
cout << "Invalid choice! Try again.\n";
}
} while (choice != 0);
return 0;
}
#include <iostream>
using namespace std;
public:
Stack() {
top = NULL;
}
// Push operation
void push(int value) {
Node* newNode = new Node;
newNode->data = value;
newNode->next = top; // new node points to old top
top = newNode; // update top
cout << value << " pushed into the stack.\n";
}
// Pop operation
void pop() {
if (isEmpty()) {
cout << "Stack Underflow! Nothing to pop.\n";
return;
}
Node* temp = top;
cout << top->data << " popped from the stack.\n";
top = top->next;
delete temp;
}
28
void peek() {
if (isEmpty()) {
cout << "Stack is empty!\n";
} else {
cout << "Top element: " << top->data << endl;
}
}
29
};
// Main Function
int main() {
Stack s;
int choice, value;
do {
cout << "\n--- Stack Menu (Linked List Implementation) ---\n";
cout << "1. Push\n";
cout << "2. Pop\n";
cout << "3. Peek (Top Element)\n";
cout << "4. Check if Empty\n";
cout << "5. Display Stack\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value to push: ";
cin >> value;
[Link](value);
break;
case 2:
[Link]();
break;
case 3:
[Link]();
break;
case 4:
30
cout << ([Link]() ? "Stack is empty.\n" : "Stack is not empty.\n");
break;
case 5:
[Link]();
break;
case 0:
cout << "Exiting program...\n";
break;
default:
cout << "Invalid choice! Try again.\n";
}
} while (choice != 0);
return 0;
}
Applications:
Stacks are widely used in computer science for various tasks, including:
Compilers use stacks to manage function calls and local variables during program execution.
• Expression Evaluation:
Stacks are crucial for converting infix expressions to postfix/prefix and evaluating them.
• Undo/Redo Functionality:
Many applications use stacks to store changes, allowing users to undo or redo actions.
• Browser History:
Web browsers often use a stack to manage visited pages, enabling back and forward navigation.
• Backtracking Algorithms:
Stacks are used in algorithms like maze solving or searching file directories, where exploring
different paths and backtracking is necessary.
31
Week 6
Recursion in Data Structures
“Recursion is a programming technique where a function calls itself to solve a
problem.”
It is essential for implementing many algorithms, especially those following the "divide and conquer"
paradigm.
A condition that stops the recursion, preventing an infinite loop. It defines the simplest version of the
problem that can be solved directly.
• Recursive Step:
The part of the function that calls itself with a modified input, moving closer to the base case.
Example Program:
The factorial of a number n is n * (n-1) * ... * 1. The recursive function handles this by:
int factorial(int n) {
if (n == 0) { // Base Case
return 1;
} else { // Recursive Step
return n * factorial(n - 1);
}
}
int main() {
int num = 5;
cout << "Factorial of " << num << " is " << factorial(num) << endl;
return 0;
}
32
Divide and Conquer Algorithms
Divide and Conquer is a powerful algorithmic design paradigm that naturally lends itself to
recursive implementations. It involves three steps:
• Divide: Break the problem into smaller subproblems of the same type.
• Quick Sort: Partitions an array around a pivot element and recursively sorts the sub-arrays.
• Binary Search: Divides the search space in half at each step.
Master Theorem:
A common tool for solving recurrence relations of the form
where:
• f(n) is the cost of the work done outside the recursive calls (dividing and combining).
The recurrence relation for Merge Sort is T(n) = 2T(n/2) + O(n). Using the Master Theorem, this
resolves to O(n log n).
33
Week 7
Sorted linked list, single and doubly linked list
Linked list:
• Definition: A linear data structure where elements are stored in nodes, and each node
contains data and a reference (or pointer) to the next node in the sequence.
• Node: The fundamental building block of a linked list, comprising data and a pointer to the
next node.
• Null/None: A special value in the pointer of the last node, indicating the end of the list.
→ Traversal
→ Insertion
→ Deletion
→ Search
→ Sorting
• Characteristics:
Data Structure: Non-contiguous
Memory Allocation: Typically allocated one by one to individual elements
Insertion/Deletion: Efficient
Access: Sequential
34
Types of Linked List
1. Singly Linked List
A singly linked list is a linear data structure composed of a sequence of connected
nodes. Each node in a singly linked list consists of two main parts:
Data Field: This part stores the actual data or value associated with that node.
Next Link (or Pointer): This part stores the memory address (or reference) of the next
node in the sequence. The last node's next link typically points to NULL or None,
signifying the end of the list.
Key Characteristics:
➢ One-Way Traversal:
You can only traverse a singly linked list in one direction, from the beginning (head) to the
end, by following the next pointers.
➢ Head Node:
A special pointer, often called head, points to the first node of the list. If the list is
empty, head will be NULL.
➢ Dynamic Size:
Unlike arrays, linked lists do not require contiguous memory allocation and can grow or shrink
dynamically as elements are added or removed.
➢ Non-Contiguous Memory:
Nodes in a linked list are not necessarily stored in adjacent memory locations. Their
connection is established through the pointers.
Basic Operations:
Common operations on a singly linked list include:
► Insertion: Adding a new node at the beginning, end, or a specific position in the list.
► Traversal: Iterating through the list to access or process the data in each node.
Advantages:
→ Dynamic size, allowing for flexible memory usage (no fixed limit like arrays).
→ Efficient insertion and deletion at beginning and end. We also have insertion at the
middle efficient if we have reference or pointer to the node after which we need to
insert. (O(1) time complexity).
Disadvantages:
35
→ Accessing an element at a specific index requires traversing from the beginning,
resulting in O(N) time complexity.
Example program:
#include <iostream>
using namespace std;
// Node structure
class Node {
public:
int data;
Node* next;
//CONSTRUCTOR
Node(int val) {
data = val;
next = NULL;
}
};
public:
//Constructor
SinglyLinkedList() {
head = NULL;
}
36
// Insert node at the end
void insert(int val) {
Node* newNode = new Node(val);
if (head == NULL) {
head = newNode;
return;
}
if (head->data == val) {
Node* toDelete = head;
head = head->next;
delete toDelete;
cout << "Node deleted successfully.\n";
return;
}
37
if (temp->next == NULL) {
cout << "Value not found!\n";
return;
}
// Search a value
void search(int val) {
Node* temp = head;
int pos = 1;
while (temp != NULL) {
if (temp->data == val) {
cout << "Value found at position " << pos << endl;
return;
}
temp = temp->next;
pos++;
}
cout << "Value not found in the list!\n";
}
38
Node* temp = head;
cout << "Linked List: ";
while (temp != NULL) {
cout << temp->data << " -> ";
temp = temp->next;
}
cout << "NULL\n";
}
};
// Main function
int main() {
SinglyLinkedList list;
int choice, value;
do {
cout << "\n--- Singly Linked List Menu ---\n";
cout << "1. Insert\n";
cout << "2. Delete\n";
cout << "3. Search\n";
cout << "4. Display\n";
cout << "5. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value to insert: ";
cin >> value;
[Link](value);
break;
case 2:
cout << "Enter value to delete: ";
cin >> value;
39
[Link](value);
break;
case 3:
cout << "Enter value to search: ";
cin >> value;
[Link](value);
break;
case 4:
[Link]();
break;
case 5:
cout << "Exiting program...\n";
break;
default:
cout << "Invalid choice!\n";
}
return 0;
}
40
The list is managed by a head pointer, which points to the first node, and often
a tail pointer, which points to the last node.
→ Terminators:
The previous pointer of the first node and the next pointer of the last node typically point
to null or a sentinel node to mark the list's boundaries.
Basic Operations:
Common operations on a doubly linked list include:
► Insertion: Adding a new node at the beginning, end, or a specific position in the list.
Advantages:
→ Efficient Backward Traversal:
Allows easy navigation to previous elements without starting from the head.
→ Simplified Deletion:
Deleting a node is generally more straightforward as the previous node's next pointer
can be easily updated using the deleted node's previous pointer.
→ Efficient Insertion/Deletion in the Middle:
Operations in the middle of the list can be more efficient compared to singly linked
lists, as finding the previous node is direct.
Disadvantages:
→ Increased Memory Usage:
Each node requires extra memory to store the previous pointer compared to a singly
linked list.
→ More Complex Operations:
Insertion and deletion operations are slightly more complex as they involve updating
two pointers (next and previous) instead of just one.
Example Program:
#include <iostream>
using namespace std;
// Node structure
struct Node {
int data;
Node* prev;
Node* next;
};
41
// Doubly Linked List Class
class DoublyLinkedList {
private:
Node* head;
public:
DoublyLinkedList() {
head = NULL;
}
// Insert at Beginning
void insertAtBeginning(int value) {
Node* newNode = new Node;
newNode->data = value;
newNode->prev = NULL;
newNode->next = head;
if (head != NULL)
head->prev = newNode;
head = newNode;
cout << "Inserted " << value << " at beginning.\n";
}
// Insert at End
void insertAtEnd(int value) {
Node* newNode = new Node;
newNode->data = value;
newNode->next = NULL;
if (head == NULL) {
newNode->prev = NULL;
head = newNode;
42
} else {
Node* temp = head;
while (temp->next != NULL)
temp = temp->next;
temp->next = newNode;
newNode->prev = temp;
}
cout << "Inserted " << value << " at end.\n";
}
43
delete temp;
head = NULL;
return;
}
cout << "Deleted " << temp->data << " from end.\n";
temp->prev->next = NULL;
delete temp;
}
// ? Delete by Value
void deleteByValue(int value) {
if (head == NULL) {
cout << "List is empty!\n";
return;
}
44
temp = temp->next;
if (temp == NULL) {
cout << "Value " << value << " not found in the list.\n";
return;
}
// Adjust links
if (temp->next != NULL)
temp->next->prev = temp->prev;
if (temp->prev != NULL)
temp->prev->next = temp->next;
cout << "Deleted node with value " << value << ".\n";
delete temp;
}
// Traverse Forward
void traverseForward() {
if (head == NULL) {
cout << "List is empty!\n";
return;
}
Node* temp = head;
cout << "Traversing forward: ";
while (temp != NULL) {
cout << temp->data << " ";
temp = temp->next;
}
cout << endl;
}
// Traverse Backward
void traverseBackward() {
45
if (head == NULL) {
cout << "List is empty!\n";
return;
}
Node* temp = head;
while (temp->next != NULL)
temp = temp->next;
// Search by Value
void search(int value) {
if (head == NULL) {
cout << "List is empty!\n";
return;
}
Node* temp = head;
int pos = 1;
bool found = false;
46
if (!found)
cout << "Value " << value << " not found in the list.\n";
}
};
// Main Function
int main() {
DoublyLinkedList dll;
int choice, value;
do {
cout << "\n--- Doubly Linked List Menu ---\n";
cout << "1. Insert at Beginning\n";
cout << "2. Insert at End\n";
cout << "3. Delete from Beginning\n";
cout << "4. Delete from End\n";
cout << "5. Delete by Value\n";
cout << "6. Traverse Forward\n";
cout << "7. Traverse Backward\n";
cout << "8. Search a Value\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value: ";
cin >> value;
[Link](value);
break;
case 2:
cout << "Enter value: ";
cin >> value;
47
[Link](value);
break;
case 3:
[Link]();
break;
case 4:
[Link]();
break;
case 5:
cout << "Enter value to delete: ";
cin >> value;
[Link](value);
break;
case 6:
[Link]();
break;
case 7:
[Link]();
break;
case 8:
cout << "Enter value to search: ";
cin >> value;
[Link](value);
break;
case 0:
cout << "Exiting program...\n";
break;
default:
cout << "Invalid choice! Try again.\n";
}
} while (choice != 0);
return 0;
}
48
3. Circular Linked List
A circular linked list is a variation of a traditional linked list where the last node in the list
points back to the first node (head), forming a continuous loop. Unlike linear linked lists where
the last node's next pointer is NULL, in a circular linked list, the next pointer of the last node
contains the address of the first node.
In this type, each node has a data field and a single next pointer. The next pointer of the last node
points to the first node, creating a circular structure.
Key Characteristics:
o No NULL pointer:
The absence of a NULL pointer at the end of the list is a defining feature, as the last node always
points to the first.
o Continuous Traversal:
Traversal can continue indefinitely, as there is no designated end point. Careful handling is required
to prevent infinite loops during traversal, typically by stopping when the starting node is encountered
again.
o Efficient end-to-beginning operations:
Operations that involve wrapping around from the end of the list to the beginning, such as in round-
robin scheduling or playlist management, are naturally supported.
49
→ Efficient Traversal
→ Uniform Traversal
→ Efficient Memory Utilization
→ Harder to Debug
→ Deletion Complexity
Example Program:
#include <iostream>
using namespace std;
// Node structure
struct Node {
int data;
Node* next;
};
public:
CircularLinkedList() {
last = NULL;
}
50
// Check if list is empty
bool isEmpty() {
return last == NULL;
}
// Insert at Beginning
void insertAtBeginning(int value) {
Node* newNode = new Node;
newNode->data = value;
if (isEmpty()) {
last = newNode;
last->next = last; // Points to itself
} else {
newNode->next = last->next;
last->next = newNode;
}
cout << "Inserted " << value << " at beginning.\n";
}
// Insert at End
void insertAtEnd(int value) {
Node* newNode = new Node;
newNode->data = value;
if (isEmpty()) {
last = newNode;
last->next = last;
} else {
newNode->next = last->next;
last->next = newNode;
last = newNode;
}
cout << "Inserted " << value << " at end.\n";
51
}
52
temp = temp->next;
cout << "Deleted " << last->data << " from end.\n";
temp->next = last->next;
delete last;
last = temp;
}
}
// ? Delete by Value
void deleteByValue(int value) {
if (isEmpty()) {
cout << "List is empty!\n";
return;
}
53
delete curr;
return;
}
prev = curr;
curr = curr->next;
} while (curr != last->next);
cout << "Value " << value << " not found in the list.\n";
}
// Traverse / Display
void traverse() {
if (isEmpty()) {
cout << "List is empty!\n";
return;
}
54
int pos = 1;
bool found = false;
do {
if (temp->data == value) {
cout << "Value " << value << " found at position " << pos << ".\n";
found = true;
}
temp = temp->next;
pos++;
} while (temp != last->next);
if (!found)
cout << "Value " << value << " not found in the list.\n";
}
};
// Main Function
int main() {
CircularLinkedList cll;
int choice, value;
do {
cout << "\n--- Circular Linked List Menu ---\n";
cout << "1. Insert at Beginning\n";
cout << "2. Insert at End\n";
cout << "3. Delete from Beginning\n";
cout << "4. Delete from End\n";
cout << "5. Delete by Value\n";
cout << "6. Traverse / Display\n";
cout << "7. Search a Value\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
55
switch (choice) {
case 1:
cout << "Enter value: ";
cin >> value;
[Link](value);
break;
case 2:
cout << "Enter value: ";
cin >> value;
[Link](value);
break;
case 3:
[Link]();
break;
case 4:
[Link]();
break;
case 5:
cout << "Enter value to delete: ";
cin >> value;
[Link](value);
break;
case 6:
[Link]();
break;
case 7:
cout << "Enter value to search: ";
cin >> value;
[Link](value);
break;
case 0:
cout << "Exiting program...\n";
break;
56
default:
cout << "Invalid choice! Try again.\n";
}
} while (choice != 0);
return 0;
}
Key Characteristics:
• Ordered Elements:
Unlike a general linked list, elements in a sorted linked list are always arranged according to a
defined sorting criterion.
• Dynamic Sizing:
Like all linked lists, it can grow or shrink dynamically as elements are added or removed.
When a new element is inserted, the list must be traversed to find the correct position to maintain
the sorted order. This involves comparing the new element's value with existing node values.
Each node typically contains the data element and a pointer (or reference) to the next node in the
sequence. In a doubly sorted linked list, nodes also have a pointer to the previous node.
57
Sort Linked List using Insertion Sort
→ The algorithm for sorting a linked list using Insertion Sort involves gradually building a sorted
portion of the list within the same memory space.
→ For each node, the algorithm determines its correct position within the sorted list.
→ If the node is smaller than the current head of the sorted list, it becomes the new head.
→ Otherwise, it is inserted into its appropriate position by traversing the sorted list.
→ This process is repeated until all nodes are correctly positioned, resulting in the sorted list.
→ Time Complexity: O(n2), In the worst case, we might have to traverse all nodes of the sorted
list for inserting a node.
→ Auxiliary Space: O(1), no extra space is required.
Please refer to Insertion Sort for Singly Linked List for implementation.
• Merge Sort guarantees a time complexity of O(nlogn) in the average, best, and worst cases.
• Merge Sort naturally fits well with Linked List because it traverses items in sequential manner
(no random access).
• When we compare linked list implementation with the array implementation we can notice
that the linked list implementation does not require extra space to merge because linked list
allows insertion and deletion in the middle in O(1) time.
58
Week 8
Sorting Algorithm
Sorting algorithms are fundamental procedures in data structures used to arrange elements
of a list or array in a specific order, typically ascending or descending.
→ Bubble Sort:
• Compares adjacent elements and swaps them if they are in the wrong order.
• Repeatedly passes through the list until no more swaps are needed.
• Simple to understand but inefficient for large datasets.
→ Selection Sort:
• Finds the minimum (or maximum) element from the unsorted part of the list and swaps
it with the element at the beginning of the unsorted part.
→ Insertion Sort:
• Each new element is inserted into its correct position within the already sorted portion
of the array.
• Efficient for small datasets or nearly sorted data.
→ Merge Sort:
• A divide-and-conquer algorithm.
• Divides the unsorted list into n sublists, each containing one element, and then
repeatedly merges sublists to produce new sorted sublists until there is only one
sublist remaining.
• Picks an element as a pivot and partitions the array around the pivot, placing all smaller
elements before the pivot and all greater elements after it.
• Recursively sorts the sub-arrays.
59
• Generally considered one of the fastest sorting algorithms in practice, with an average
time complexity of O(n log n).
→ Heap Sort:
• Builds a max-heap (or min-heap) from the input data, then repeatedly extracts the
maximum (or minimum) element and rebuilds the heap.
→ Shell Sort:
• Sorts elements that are far apart, then gradually reduces the gap between elements
to be sorted.
Selection sort
Selection sort is a simple, in-place comparison-based sorting algorithm.
It works by repeatedly finding the minimum element from the unsorted part of the array and placing
it at the beginning of the sorted part.
• Iterate through the unsorted part: For each iteration, starting from the first element of the
unsorted part:
a. Find the minimum: Find the minimum element in the unsorted subarray.
b. Swap: Swap the found minimum element with the first element of the unsorted
subarray. This effectively moves the minimum element to its correct position in the sorted
subarray.
• Update boundaries:
Increment the boundary of the sorted subarray by one, and decrement the boundary of
the unsorted subarray by one.
• Repeat:
Continue this process until the entire array is sorted (i.e., the unsorted subarray becomes
empty).
→ Minimal Swaps:
Selection sort performs a maximum of N-1 swaps, where N is the number of elements. This can be
advantageous in scenarios where swapping elements is a computationally expensive operation.
→ In-place Sorting:
It requires a constant amount of extra memory (O(1)) as it sorts the array directly without needing
additional temporary storage.
For small lists, its simplicity and minimal swap count can make it a reasonable choice, as the
overhead of more complex algorithms might outweigh their asymptotic advantages.
Its time complexity is O(N^2) in all cases (best, average, and worst), making it highly inefficient for
large lists compared to algorithms like Quick Sort or Merge Sort.
→ Not Adaptive:
Selection sort does not take advantage of any pre-existing order in the input array. It performs the
same number of comparisons regardless of whether the array is partially or fully sorted.
→ Not Stable:
It is not a stable sorting algorithm, meaning the relative order of equal elements might not be
preserved after sorting.
→ Inefficient Comparisons:
The algorithm performs a high number of comparisons, even when the array is nearly sorted, leading
to wasted computations for larger datasets.
o Small Datasets:
For sorting very small arrays or lists, the simplicity of Selection Sort can make it a practical choice,
as the overhead of more complex algorithms might not be justified.
o Memory-Constrained Environments (e.g., Embedded Systems):
Selection Sort is an in-place algorithm, meaning it requires only a constant amount of extra memory
(O(1) space complexity). This makes it suitable for environments with limited memory resources.
o Educational Purposes:
61
Its straightforward logic and step-by-step nature make it an excellent algorithm for teaching
fundamental sorting concepts and algorithmic design to beginners in computer science.
o Minimizing Swaps/Writes:
Selection Sort performs a maximum of N-1 swaps, which is the minimum possible for any sorting
algorithm. This can be beneficial in scenarios where memory write operations are costly or have a
limited lifespan (e.g., certain types of Flash memory).
When preparing small subsets of data for further processing, Selection Sort can be used to organize
them before applying more advanced algorithms.
Due to its quadratic time complexity, Selection Sort should not be used for sorting large datasets,
as it will be significantly slower than algorithms like Merge Sort or Quick Sort.
Be aware of Selection Sort's performance characteristics and choose it only when its advantages
(simplicity, in-place nature, minimal swaps) outweigh its time complexity limitations for the specific
application.
o Clear Implementation:
While simple, ensure the implementation is clear and correctly handles edge cases, such as empty
arrays or arrays with a single element.
For most general-purpose sorting tasks, especially with larger datasets, more efficient algorithms
are preferred. Selection Sort is a specialized tool for particular scenarios.
Example Program:
#include<iostream>
#include<conio.h>
using namespace std;
int main()
{
int arr[5],i,j,temp;
for(i=0;i<5;i++)
{
cout<<"Enter value: ";
cin>>arr[i];
62
}
for(i=0;i<5;i++)
{
for(j=0;j<4;j++)
if(arr[j]>arr[j+1])
{
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
cout<<"\nThe sorted array is: " ;
for(i=0;i<5;i++)
cout<<arr[i]<<" ";
getch();
}
Output:
Enter value: 31
Enter value: 4
Enter value: 55
Enter value: 6
Enter value: 12
The original value in array: 31 4 55 6 12
The sorted array is: 4 6 12 31 55
Complexity Analysis of Selection sort:
► Time Complexity:
Selection Sort exhibits a time complexity of O(n^2) in all cases: best, average, and
worst. This quadratic complexity arises from its nested loop structure.
• Outer Loop:
63
This loop iterates n-1 times, where n is the number of elements in
the array. Each iteration places one element in its correct sorted position.
• Inner Loop:
Within each iteration of the outer loop, the inner loop traverses the
remaining unsorted portion of the array to find the minimum element. In the first iteration, it
performs n-1 comparisons, in the second, n-2 comparisons, and so on, until the last iteration with 1
comparison.
The total number of comparisons sums up to (n-1) + (n-2) + ... + 1, which is equivalent to n(n-
1)/2. This sum simplifies to O(n^2), indicating that the execution time grows quadratically with the
input size. The number of swaps is at most n-1.
► Space Complexity:
Selection Sort is an in-place sorting algorithm, meaning it sorts the arraywithout requiring
significant additional memory. Its space complexity is O(1), as it only uses a constant amount of
extra space for temporary variables during swaps.
Insertion Sort
o Insertion Sort is a simple comparison-based sorting algorithm.
o It works by taking one element at a time and placing it in its correct position.
o Similar to arranging cards in your hand one by one in sorted order.
Working Principle:
❖ The first element is considered sorted.
❖ Starting from index 1, each element is compared with previous elements.
❖ While previous elements are greater, they are shifted to the right.
❖ The current element is then inserted into its correct position.
64
Example:
Unsorted Array: {4, 1, 5, 2, 3}
Step 1 → {1, 4, 5, 2, 3}
Step 2 → {1, 4, 5, 2, 3}
Step 3 → {1, 2, 4, 5, 3}
Step 4 → {1, 2, 3, 4, 5}
Result: Sorted Array {1, 2, 3, 4, 5}
• Works with:
Advantages:
• Simple and easy to implement.
Disadvantages:
• Slow for large datasets (O(n²)).
Use Cases:
o Small or partially sorted arrays.
o Online sorting (data arriving one by one).
o Teaching and algorithm demonstrations.
Best Practices:
o Use for arrays < 25 elements.
o Effective inside hybrid algorithms like Timsort.
Example program:
#include<iostream>
#include<conio.h>
using namespace std;
void insertionsort(int arr[],int n)
{
for(int i=1;i<n;i++)
{
int curr=arr[i];
int prev=i-1;
while(prev>=0 && arr[prev]>curr)
{
arr[prev+1]=arr[prev];
prev--;
}
arr[prev+1]=curr;
}
}
int main()
{
int n=5;
int arr[]={4,1,5,2,3};
insertionsort( arr, n);
for (int i=0;i<n;i++)
{
cout<<arr[i] << “ ”;
}
return 0;
}
Output:
12345
66
• Each element is compared only once to its preceding element, and no shifts are
required. The number of comparisons is roughly proportional to the number of
elements (n).
• Each element must be compared with all previously sorted elements and shifted to its
correct position, resulting in a quadratic number of comparisons and shifts.
• For randomly ordered inputs, the average performance is also quadratic, similar to the
worst case, as a significant number of comparisons and shifts are still typically
required.
► Space Complexity:
O(1):
Insertion Sort is an in-place sorting algorithm, meaning it requires a constant amount
of extra space regardless of the input size. It only uses a few temporary variables for
comparisons and element storage during shifts.
Summarization / Conclusion:
Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item
at a time. It is an in-place comparison-based sorting algorithm, meaning it sorts the array by
comparing elements and doesn't require extra space proportional to the input size.
→ Insertion Sort builds the final sorted array one element at a time.
→ Best for small datasets and educational use.
→ Easy to understand and implement.
→ Not efficient for large, unsorted datasets.
Merge sort
Merge sort is a popular sorting algorithm known for its efficiency and stability.
It follows the Divide and Conquer approach.
It works by recursively dividing the input array into two halves, recursively sorting the two
halves and finally merging them back together to obtain the sorted array.
Key components:
• Divide: The algorithm starts with breaking up the array into smaller and smaller pieces until
one such sub-array only consists of one element.
• Conquer: The algorithm merges the small pieces of the array back together by putting the
lowest values first, resulting in a sorted array.
• The breaking down and building up of the array to sort the array is done recursively.
67
Table:
Merge Sort works by dividing the data, sorting the parts, and then merging them back together.
How it works?
1. Divide the unsorted array into two sub-arrays, half the size of the original.
2. Continue to divide the sub-arrays as long as the current piece of the array has more than one
element.
3. Merge two sub-arrays together by always putting the lowest value first.
• Best Case: O(n log n), When the array is already sorted or nearly sorted.
• Average Case: O(n log n), When the array is randomly ordered.
• Worst Case: O(n log n), When the array is sorted in reverse order.
68
→ Auxiliary Space: O(n), Additional space is required for the temporary array used during
merging.
Pros / Advantages:
► Stability: Merge sort is a stable sorting algorithm, which means it maintains the relative order
of equal elements in the input array.
► Guaranteed worst-case performance: Merge sort has a worst-case time complexity of O(N
logN), which means it performs well even on large datasets.
Cons / Disadvantages:
► Space complexity: Merge sort requires additional memory to store the merged sub-arrays
during the sorting process.
► Not in-place: Merge sort is not an in-place sorting algorithm, which means it requires
additional memory to store the sorted data. This can be a disadvantage in applications where
memory usage is a concern.
Example program:
#include <iostream>
using namespace std;
69
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
70
}
// Main function
int main() {
int arr[] = {38, 27, 43, 3, 9, 82, 10};
int size = sizeof(arr) / sizeof(arr[0]);
return 0;
}
Output:
Original array: 38 27 43 3 9 82 10
Sorted array: 82 43 38 27 10 9 3
Quick Sort
Quick Sort is a sorting algorithm based on the Divide and Conquer that picks an element as a pivot
and partitions the given array around the picked pivot by placing the pivot in its correct position in
the sorted array.
It works on the principle of divide and conquer, breaking down the problem into smaller sub-
problems.
Working:
There are mainly three steps in the algorithm:
1. Choose a Pivot: Select an element from the array as the pivot. The choice of pivot can vary
(e.g., first element, last element, random element, or median).
71
2. Partition the Array: Re arrange the array around the pivot. After partitioning, all elements
smaller than the pivot will be on its left, and all elements greater than the pivot will be on its
right. The pivot is then in its correct position, and we obtain the index of the pivot.
3. Recursively Call: Recursively apply the same process to the two partitioned sub-arrays (left
and right of the pivot).
4. Base Case: The recursion stops when there is only one element left in the sub-array, as a
single element is already sorted.
Choice of Pivot
There are many different choices for picking pivots.
• Always pick the first (or last) element as a pivot. The below implementation picks the last
element as pivot. The problem with this approach is it ends up in the worst case when array
is already sorted.
• Pick a random element as a pivot. This is a preferred approach because it does not have
a pattern for which the worst case happens.
• Pick the median element is pivot. This is an ideal approach in terms of time complexity
as we can find median in linear time and the partition function will always divide the input
array into two halves. But it takes more time on average as median finding has high constants.
Partition Algorithm
The key process in quicksort is a partition(). There are three common algorithms to partition. All
these algorithms have O(n) time complexity.
1. Naive Partition: Here we create copy of the array. First put all smaller elements and then all
greater. Finally, we copy the temporary array back to original array. This requires O(n) extra
space.
2. Lomuto Partition: We have used this partition in this article. This is a simple algorithm we
keep track of index of smaller elements and keep swapping. We have used it here in this
article because of its simplicity.
72
3. Hoare's Partition: This is the fastest of all. Here we traverse array from both sides and keep
swapping greater element on left with smaller on right while the array is not partitioned.
• Best Case: (Ω(n log n)), Occurs when the pivot element divides the array into two
equal halves.
• Average Case (θ(n log n)), On average, the pivot divides the array into two parts, but
not necessarily equal.
• Worst Case: (O(n²)), Occurs when the smallest or largest element is always chosen
as the pivot (e.g., sorted arrays).
► Auxiliary Space:
10, 7, 8, 9, 1, 5
o Pivot = 5
o After first partition → 1, 5, 8, 9, 10, 7
o Then recursively sort left and right sides.
o Finally → 1, 5, 7, 8, 9, 10
Advantages:
→ Fast and efficient (Average time: O(n log n))
Disadvantages:
→ It has a worst-case time complexity of O(n2), which occurs when the pivot is chosen poorly.
→ It is not a good choice for small data sets.
→ It is not a stable sort, meaning that if two elements have the same key, their relative order
will not be preserved in the sorted output in case of quick sort, because here we are swapping
elements according to the pivot's position (without considering their original positions).
73
Example program:
#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = 6;
quickSort(arr, 0, n - 1);
return 0;
}
Output:
74
Sorted array: 1 5 7 8 9 10
o On average, Quick Sort is the fastest because of its efficient in-place partitioning.
o It minimizes memory usage (only needs O(log n) extra space).
o It’s widely used in system-level implementations (like C++’s std::sort()).
• However…
o If you need stable sorting (i.e., order of equal elements preserved) → use Merge Sort.
o For small datasets or almost sorted lists, Insertion Sort can outperform both due to
very low overhead.
75
The End
76