Data Structures SOL
Data Structures SOL
Editorial Board
Prof. Ajay Jaiswal, Ms Aishwarya Anand Arora
Content Writers
.Dr Geetika Vashishta, Ms Aishwarya Anand Arora, Dr Shweta Tyagi
.Academic Coordinator
Mr Deekshant Awasthi
Published by:
Department of Distance and Continuing Education
Campus of Open Learning/School of Open Learning,
University of Delhi, Delhi-110 007
Printed by:
School of Open Learning, University of Delhi
Printed at: Taxmann Publications Pvt. Ltd., 21/35, West Punjabi Bagh,
Printed at: Vikas Publishing House New
Pvt. Ltd. Plot 20/4,
Delhi Site-IV, Industrial
- 110026 Area Sahibabad,
(500 Copies, 2025) Ghaziabad - 201 010 (600 Copies)
SYLLABUS
Data Structures
Syllabus Mapping
Unit I
Arrays, Linked Lists, Stacks, Queues, Deques: Arrays: array operations, Lesson 1: Arrays - Building
applications, sorting, two-dimensional arrays, dynamic allocation of arrays; Blocks of Data Structures
Linked Lists: singly linked lists, doubly linked lists, circularly linked lists, Lesson-2: Linked Lists: Dynamic
Stacks: Stack as an ADT, implementing stacks using arrays, implementing Data Structure
stacks using linked lists, applications of stacks; Queues: Queue as an ADT, Lesson-3: Stacks - The LIFO
implementing queues using arrays, implementing queues using linked lists, Data Structure
double-ended Queue as an ADT. Lesson-4: Queue - The FIFO
Data Structure
(1–72)
Unit II
Searching and Sorting: Linear Search, Binary Search, Insertion Sort, and Lesson-5: Searching and
Count Sort. Sorting Algorithms - Efficient
Data Manipulation Techniques
(73–109)
Unit III
Recursion: Recursive Functions, Linear Recursion, Binary Recursion. Lesson-6: Recursion - Iteration
Through Self-Reference
(Pages 111–129)
Unit IV
Trees, Binary Trees: Trees: Definition and Properties, Binary Trees: Definition Lesson-7: Trees - Nurturing
and Properties, Traversal of Binary Trees. Hierarchical Structures in
Computing
(Pages 131–143)
Unit V
Binary Search Trees: insert, delete (by copying), search operations. Lesson-8: Binary Search Trees -
Crafting Ordered Hierarchies
for Efficient Retrieval
Lesson-9: Heap Data Structure
(Pages 145–172)
CONTENTS
UNIT I
UNIT II
UNIT III
UNIT IV
UNIT V
LESSON 1 NOTES
Structure
1.1 Learning Objectives
1.2 Introduction
1.2.1 Key Characteristics of Arrays
1.2.2 Applications of Arrays
1.3 Operation on Arrays
1.4 Multi-dimensional Arrays
1.5 Dynamic Allocation of Arrays
1.6 Summary
1.7 Glossary
1.8 Answers to In-Text Questions
1.9 Self-Assessment Questions
1.10 References
1.11 Suggested Readings
Self-Instructional
Material 3
NOTES
1.2 INTRODUCTION
An array is a data structure that allows you to store a fixed-size collection of elements
of the same type. It provides a way to organise related data items under a single name
and allows efficient access to individual elements using an index.
Arrays are a consecutive sequence of memory locations where each element
occupies a specific position. The elements are typically homogeneous, meaning they
have the same data type. For example, an array can store a sequence of integers,
floating-point numbers, characters, or even complex objects.
Fixed Size: Arrays have a predetermined size specified at the time of declaration,
which remains constant throughout the program execution.
Homogeneous Elements: All elements within an array are of the same data
type. This constraint allows for efficient memory allocation and predictable access
to elements.
Zero-based Indexing: In most programming languages, including C++, arrays
use zero-based indexing. This means that the first element is accessed with an
index of 0, the second element with an index of 1, and so on.
Contiguous Memory Allocation: Array elements are stored in adjacent
memory locations, ensuring efficient memory utilisation and enabling fast access
to elements using index calculations.
comparatively simple and very efficient in terms of space when any modification NOTES
is done.
3. CPU scheduling is a process by which the CPU decides the order in which
various tasks will be executed. Arrays are handy data structures that contain a
list of processes that need to be scheduled for the CPU.
4. Arrays are also used in image processing, as each array cell corresponds to a
pixel of an image.
5. Arrays can also be used in the implementation of a complete binary tree. They
can store the tree’s data value corresponding to a node position inside it.
6. Arrays can also be used in machine learning and data science. They store datasets
that play a significant role in training models that are used for predictions.
Arrays support various operations for accessing and manipulating their elements. Some
common operations include:
Declaration: Defining an array variable, specifying its data type and size.
Initialisation: Assigning initial values to array elements at the time of declaration
or later using assignment statements or loops.
Accessing Elements: Retrieving the value of an individual element by using its
index within square brackets.
Modifying Elements: Updating the value of an element by assigning a new
value to it using the index.
Traversing: Iterating over the array to perform operations on each element.
Bounds Checking: Ensuring that array indices are within the valid range to
prevent accessing elements outside the array’s boundaries.
Array Length: Determining the number of elements in an array using the length
or size property or the sizeof operator.
Self-Instructional
Material 5
NOTES
Arrays are widely used in programming for tasks such as storing collections of
data, implementing algorithms, and solving various computational problems efficiently.
They provide a fundamental building block for many other data structures and
algorithms.
Consider a scenario where you need to store and manage the grades of a group
of students in a class. You can use an array to store and process this data efficiently.
Let us say you have a class of 30 students, and you want to store their grades
for a particular subject. Each student’s grade can be represented as a numerical value
ranging from 0 to 100.
Here is how you can use an array to store and manipulate the grades:
#include <iostream>
const int NUM_STUDENTS = 30; // Number of students in the class
int main() {
int grades[NUM_STUDENTS]; // Array to store the grades
// Input: Read grades for each student
for (int i = 0; i < NUM_STUDENTS; i++) {
std::cout << “Enter the grade for student” << (i + 1) <<
“:”;
std::cin >> grades[i];
}
// Output: Display all grades
std::cout << “Grades of all students:” << std::endl;
for (int i = 0; i < NUM_STUDENTS; i++) {
std::cout << “Student” << (i + 1) << “:” << grades[i] <<
std::endl;
Self-Instructional
6 Material
} NOTES
// Processing: Calculate average grade
int sum = 0;
for (int i = 0; i < NUM_STUDENTS; i++) {
sum += grades[i];
}
double average = static_cast<double>(sum) / NUM_STUDENTS;
// Output: Display average grade
std::cout << “Average grade:” << average << std::endl;
return 0;
}
To declare an array, you need to specify the type of its elements, followed by the array
name and the number of elements enclosed in square brackets. Here is an example:
int numbers[5]; // Declares an integer array with 5 elements
You can also initialise an array during declaration by providing a comma-separated list
of values enclosed in curly braces. The number of values must match the size of the
array:
int numbers[5] = {1, 2, 3, 4, 5}; // Initialises the array with
values Self-Instructional
Material 7
You can access individual elements of an array using the array name followed by the
index enclosed in square brackets. The index ranges from 0 to (size - 1). Here is an
example:
int numbers[5] = {1, 2, 3, 4, 5};
int firstElement = numbers[0]; // Accesses the first element (1)
int thirdElement = numbers[2]; // Accesses the third element (3)
You can modify the value of an array element by assigning a new value to it using the
assignment operator (=). Here is an example:
int numbers[5] = {1, 2, 3, 4, 5};
numbers[1] = 10; // Modifies the second element to 10
numbers[3] = numbers[0] + numbers[2]; // Modifies the fourth
element to the sum of the first and third elements
It is important to note that arrays in C++ are zero-indexed, meaning the first element is
at index 0, and the last element is at index (size - 1). Accessing elements outside these
bounds can lead to undefined behaviour and should be avoided.
int numbers[5] = {1, 2, 3, 4, 5};
int invalidElement = numbers[5]; // Accesses an element outside
the array bounds (undefined behaviour)
You can use loops to iterate over the elements of an array. One common approach is
to use a for loop with the index variable ranging from 0 to (size - 1). Here is an
example:
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
cout << numbers[i] << “ ”; // Prints each element separated
by a space
Self-Instructional
8 Material
} NOTES
// Output: 1 2 3 4 5
Explanation: In this code, we declare and initialise an integer array numbers. We use
a loop to iterate through all the elements of the array and calculate their sum. Finally,
we divide the sum by the total number of elements to obtain the average and display it.
3. Searching for an Element in an Array:
#include <iostream>
int main() {
int numbers[] = {12, 45, 8, 27, 34, 19}; // Declare and
initialise an integer array
int target = 27; // Element to search for
bool found = false;
//Search for the element in the array
for (int i = 0; i < sizeof(numbers) / sizeof(numbers[0]);
i++) {
if (numbers[i] == target) {
found = true;
break;
}
}
// Display the result
if (found) {
std::cout << “Element” << target << “found in the array.”
<< std::endl;
} else {
std::cout << “Element” << target << “not found in the
array.” << std::endl;
}
Self-Instructional return 0;
10 Material
}
Explanation: In this code, we declare and initialise an integer array numbers. We NOTES
specify a target element (in this case, 27) that we want to search for in the array. Using
a loop, we iterate through the elements and check if any of them match the target
element. If a match is found, we set the found flag to true and exit the loop using break.
Finally, we display whether the target element was found in the array or not.
4. Reversing the Elements of an Array:
#include <iostream>
int main() {
int numbers[] = {1, 2, 3, 4, 5}; // Declare and initialise an
integer array
int start = 0;
int end = sizeof(numbers) / sizeof(numbers[0]) - 1;
// Reverse the elements of the array
while (start < end) {
// Swap elements
int temp = numbers[start];
numbers[start] = numbers[end];
numbers[end] = temp;
start++;
end--;
}
// Display the reversed array
std::cout << “Reversed array:”;
for (int i = 0; i < sizeof(numbers) / sizeof(numbers[0]);
i++) {
std::cout << numbers[i] << “ ”;
}
std::cout << std::endl;
return 0;
}
NOTES Using a while loop, we swap the elements at the start and end indices and increment/
decrement the indices until they meet in the middle. This effectively reverses the order
of the elements in the array. Finally, we display the reversed array.
C++ also supports multi-dimensional arrays, which are essentially arrays of arrays.
They can represent matrices, grids, or any other tabular data structure. Multi-
dimensional arrays play a crucial role in organising and manipulating complex data
structures that require more than one dimension. They allow you to represent and
process data in a tabular or matrix-like form, where the data is arranged in rows and
columns.
Here are some key roles and use cases of multi-dimensional arrays:
Matrices and Grids: Multi-dimensional arrays commonly represent
mathematical matrices, grids, or tables. For example, in image processing, a
two-dimensional array can store pixel values, where each element represents
the colour intensity at a specific location.
Board Games and Puzzles: Multi-dimensional arrays are helpful in
implementing board games, puzzles, or mazes. The elements of the array can
represent the cells or squares of the game board, and each element stores the
state or properties of that particular cell.
Spatial Data: Multi-dimensional arrays are suitable for storing and processing
spatial data, such as geographical coordinates, three-dimensional models, or
voxel-based representations. Each dimension of the array corresponds to a
specific spatial coordinate, enabling efficient manipulation, and analysis of the
data.
Scientific Simulations: Multi-dimensional arrays are essential in scientific
simulations, such as computational physics or weather modelling. They can store
data points in multiple dimensions, allowing scientists to analyse and predict
complex systems.
Self-Instructional
12 Material
Self-Instructional
Material 13
In C++, you can determine the size of an array using the sizeof operator, which returns
the total number of bytes occupied by the array. To get the number of elements, divide
the size by the size of an individual element. Here is an example:
int numbers[5] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]); // Calculates
the number of elements (5)
In-Text Questions
Q1. Which of the following statements is true regarding arrays in programming?
A. Arrays can only store elements of the same data type.
B. Arrays can dynamically change their size during runtime.
C. Arrays cannot be passed to functions in programming languages.
D. Arrays are always sorted automatically.
Q2. In an array with 10 elements indexed from 0 to 9, what is the index of the last
element?
A. 9 B. 10
C. 8 D. 0
Q3. What is the time complexity to access an element in an array by index?
A. O(n) B. O(1)
C. O(log n) D. O(n^2)
Q4. Which of the following methods is used to find the length of an array in many
programming languages?
A. array.size() B. array.length
C. array.count D. array.size
Q5. What term is used to access elements in an array using an index?
A. Mapping B. Traversal
C. Indexing D. Iteration
Self-Instructional
14 Material
NOTES
1.5 DYNAMIC MEMORY ALLOCATION
Unlike a fixed array, where the array size must be fixed at compile time, dynamically
allocating an array allows us to choose an array length at runtime.
To allocate an array dynamically, we use the array form of new and delete (often
called new[] and delete[]):
#include <cstddef>
#include <iostream>
int main()
{
std::cout << “Enter a positive integer:”;
std::size_t length{};
std::cin >> length;
When deleting a dynamically allocated array, we have to use the array version of
delete, which is delete[].
Self-Instructional
Material 15
NOTES This tells the CPU to clean up multiple variables instead of a single variable.
One of the most common mistakes that new programmers make when dealing with
dynamic memory allocation is to use delete instead of delete[] when deleting a
dynamically allocated array. Using the scalar version of delete on an array will result in
undefined behaviour, such as data corruption, memory leaks, crashes, or other problems.
If you want to initialise a dynamically allocated array to 0, the syntax is quite simple:
int* array{ new int[length]{} };
There was no easy way to initialise a dynamic array to a non-zero value (initialiser lists
only worked for fixed arrays). This means you had to explicitly loop through the array
and assign element values.
int* array = new int[5];
array[0] = 9;
array[1] = 7;
array[2] = 5;
array[3] = 3;
array[4] = 1;
1.6 SUMMARY
In this lesson, you learned the basics of arrays in C++, including declaration, initialisation,
accessing elements, modifying elements, iterating over arrays, working with multi-
dimensional arrays, and obtaining array size using the sizeof operator. Arrays provide
a powerful and efficient way to handle collections of data in your C++ programs.
Key points
Arrays are declared using square brackets ([]), and their elements are accessed NOTES
using an index starting from 0.
Elements of an array are stored in contiguous memory locations, enabling efficient
memory utilisation and fast access using index calculations.
Arrays have a fixed size specified at the time of declaration, and their size remains
constant throughout the program execution.
Arrays are typically used for homogeneous data types, such as integers, floating-
point numbers, characters, or objects of a specific class.
Accessing and modifying elements of an array is done using the array name
followed by the index enclosed in square brackets.
Arrays support operations such as declaration, initialisation, element access,
modification, and traversal using loops.
It is essential to perform bounds checking to ensure that array indices are within
the valid range to prevent accessing elements outside the array’s boundaries.
Multi-dimensional arrays allow you to represent and process data in multiple
dimensions, such as matrices, grids, or spatial data.
Multi-dimensional arrays are widely used in various domains, including image
processing, scientific simulations, board games, and data analysis.
Arrays provide an efficient way to store and manipulate large amounts of data,
making them a fundamental building block for many other data structures and
algorithms.
Understanding arrays and their operations is essential for effective programming,
as they provide a powerful tool for managing and processing collections of data in a
structured manner.
1.7 GLOSSARY
NOTES Index: A numeric value representing the position of an element within an array.
Size/Length: The total number of elements that an array can hold, often
determined at its creation.
Static Array: An array with a fixed size that cannot be changed during runtime.
Dynamic Array: An array-like data structure that can dynamically resize itself
to accommodate varying numbers of elements.
7. Describe the process of accessing elements stored in memory locations within NOTES
an array.
8. Explain the concept of contiguous memory allocation in arrays and its impact on
memory usage and access speed.
9. Discuss the importance of array initialisation and its various methods in
programming languages.
10. Describe the process of resizing an array and the challenges associated with it.
1.10 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
Material 19
LESSON 2 NOTES
2.2 INTRODUCTION
A linked list is a fundamental data structure used to store and manage collections of Self-Instructional
Material 21
data. Unlike arrays, which store elements in contiguous memory locations, linked lists
NOTES organise data using nodes that are linked together through references or pointers.
Each node in a linked list contains two components: data and a pointer to the next
node in the sequence. The data component holds the value or information being stored,
while the pointer component, often called the “next” pointer, indicates the memory
address of the next node in the list.
The structure of a linked list allows for efficient insertion and deletion operations,
especially when compared to arrays, where resizing can be costly. Since the nodes are
not required to be stored in consecutive memory locations, they can be dynamically
allocated and linked together, allowing for flexibility in managing the list’s size.
There are different types of linked lists, including singly linked lists, doubly linked
lists, and circular linked lists. In a singly linked list, each node contains a reference to
the next node in the sequence. Doubly linked lists, on the other hand, have nodes with
references to both the next and previous nodes, enabling traversal in both directions.
Circular linked lists form a loop, where the last node’s next pointer points back to the
first node, creating a circular structure.
Linked lists offer advantages in scenarios where dynamic data structures are
needed or when frequent insertions and deletions are expected. However, they can be
less efficient than arrays when it comes to random access, as accessing elements
requires traversing the list from the beginning. Nonetheless, various algorithms and
techniques can be applied to optimise linked list operations for specific use cases.
Let us consider a scenario where you are using a music streaming service that
allows you to create and manage playlists. Here is how a linked list can be used to
implement a playlist:
Each song in the playlist can be represented by a node in the linked list. The
node would contain two main fields: the song data (such as the song title, artist, duration,
etc.) and a reference to the next song in the playlist.
Structure for a Node:
struct SongNode {
Song song;
SongNode* next;
};
Self-Instructional
22 Material
In this example, the Song structure represents the data for a particular song, NOTES
and the next pointer points to the next song in the playlist.
To create a playlist, you would initialise the head pointer to the first song in the list, and
the next pointer of the last song would be set to NULL, indicating the end of the
playlist.
When you add a new song to the playlist, you create a new node and assign the song
data to it. The next pointer of the current last song node would then point to the newly
created node, making it the new last song in the playlist.
To play the songs in the playlist, you start from the head of the list and play each song
in order until you reach the end of the list (NULL).
When you want to remove a song from the playlist, you need to update the next
pointer of the previous song node to skip the node containing the song you want to
remove. Then, you can deallocate the memory for the removed node.
Linked lists offer several advantages in certain situations compared to other data
structures like arrays. Here are some of the key advantages of linked lists:
(i) Dynamic Size: Linked lists can easily grow or shrink in size during runtime.
This is in contrast to arrays, which have a fixed size that needs to be declared in
advance.
(ii) Ease of Insertion and Deletion: Insertion and deletion of elements in a linked
list are generally faster and more efficient than in arrays. In a linked list, adding
or removing elements involves adjusting pointers, while in an array, elements
may need to be shifted to accommodate the change. Self-Instructional
Material 23
Self-Instructional
24 Material
NOTES
Note: Linked lists are a fundamental data structure in computer science, commonly
used to store and manipulate collections of data.
Unlike arrays, linked lists provide dynamic memory allocation, allowing efficient
insertion and deletion of elements. This lesson will cover the basic concepts of linked
lists and their implementation and provide examples and code snippets for better
understanding.
A linked list has one or more nodes. Each node contains some data and a pointer (or
reference or link) to the next node in the list. The first node of the linked list is named
as the head. Correspondingly, the last node containing NULL indicates the end of the
list. Linked lists can be either singly (each node points to the next node) or doubly
linked (each node points to both the previous and next nodes).
Self-Instructional
Material 25
We’ll start by defining a basic structure for a node in a singly linked list:
struct Node {
int data;
Node* next;
};
Here, the data field represents the content stored in the node, and ‘next’ is a
pointer to the next or the subsequent node in the list.
To create a linked list, we need to initialize the head node and ensure the last
node points to NULL:
Node* head = NULL;
Insertion Operations
Inserting elements is a common operation in linked lists. We’ll cover three scenarios:
inserting at the beginning, end, and in the middle.
To add a new node at the beginning, we create a new node, assign its data, and update
the next pointer to the current head.
Finally, we update the head to point to the new node.
Self-Instructional
26 Material
To add a new node at the end, we have traverse through the list from the head node
until we reach the last node.
Then, we create a new node, assign its data, and update the next pointer of the
last node so that it now points to the newly created node.
if (head == NULL) {
head = newNode;
return;
}
current->next = newNode;
}
To add a new node in the middle of the linked list, we have to first locate the desired
position.
Starting from the head node, we have to traverse the list until we reach the
desired position. Now, create a new node with the specific value to be added. Also,
and update the next pointers accordingly.
Self-Instructional
28 Material
if (current == NULL) {
// Position exceeds the size of the list
// Handle the error or insert at the end
return;
}
newNode->next = current->next;
current->next = newNode;
}
Deletion Operations
Similar to insertion, deletion is a common operation. We’ll cover deletion from the
beginning, end, and middle of the list.
To delete the first node of a given linked list, we have to update the head pointer so
that it now points to the second node of the list. After doing this, de-allocate memory
for the old head.
void deleteFromBeginning() {
if (head == NULL) {
// Handle the error, list is empty
return;
}
To delete the last node, we traverse the list until we reach the second-to-last node.
We update its next pointer to NULL and deallocate memory for the last node.
void deleteFromEnd() {
if (head == NULL) {
// Handle the error, list is empty
return;
}
if (head->next == NULL) {
// Only one node in the list
delete head;
head = NULL;
return;
}
delete current->next;
current->next = NULL;
}
To delete a node from the middle, we need to locate the desired position and then do
the following steps:
Find the node that just comes prior to the node to be deleted.
Change the next of the previous node.
Free or de-allocate the memory occupied by the node to be deleted.
Self-Instructional
30 Material
We traverse the list until we find the node before the position, update its next NOTES
pointer, and deallocate memory.
if (position == 1) {
deleteFromBeginning();
return;
}
Self-Instructional
Material 31
To traverse a linked list, we start from the head and move through each node, printing
its data until we reach NULL.
void displayList() {
Node* current = head;
while (current != NULL) {
cout << current->data << “ “;
current = current->next;
}
cout << endl;
}
current = current->next;
currentPosition++;
}
Self-Instructional
32 Material
In this type of linked list, each node holds two pointer fields. A node is divided into
three parts. The first part points to the previous note, the second part holds the note
value and the last part Points to the next node. Pointers exist between adjacent notes
in both directions. The list can be traversed either in the forward direction or in the
backward direction.
Doubly linked lists are more convenient than singly-linked lists since we maintain
links for bi-directional traversing, which is the primary advantage of this list. Links in
both forward and backward directions allow references to the previous and the next
node in the sequence of notes very efficiently.
A circular list is a list in which the Link field of the last note is made to point to the start
or the first note of the list.
In this list, each node is divided into two parts. The first part contains information
about the current node and the next part points to the next node. The last node points
back to the start of the list, which is why it is called a circular linked list.
In this type of linked list, the address of the last note contains the address of the
first node.
Self-Instructional
Material 33
NOTES
In-Text Questions
Q1. In a singly linked list, how many pointers does each node contain?
A. One B. Two
C. Three D. None
Q2. Which of the following is an advantage of a doubly linked list over a singly
linked list?
A. Efficient memory usage
B. Simplicity in traversal
C. Ability to traverse in both directions.
D. Lower time complexity for insertion
Q3. What is the time complexity to insert a node at the end of a singly linked list
with ‘n’ nodes?
A. O(1) B. O(log n)
C. O(n) D. O(n^2)
Q4. Which node in a linked list contains a null reference?
A. Last node B. Middle node
C. First node D. Any node in the list
Q5. What is the term used for a linked list where the last node points to the first
node, forming a circular structure?
A. Linear linked list B. Circular linked list.
C. Doubly linked list. D. Circular doubly linked list.
Self-Instructional
34 Material
NOTES
2.6 MEMORY ALLOCATION IN LINKED LIST
Together with the linked lists in memory, a particular list, which consists of unused
memory cells, is maintained. This list, which has its pointer, is called the list of available
space, the free storage list or the free pool.
This free storage list will also be called the AVAIL list.
Overflow will occur with linked lists when AVAIL = NULL, and there is an insertion.
Underflow will occur with linked lists when START = NULL and there is a
deletion.
2.7 SUMMARY
Linked lists are a versatile data structure for efficient insertion and deletion operations.
Understanding the implementation and operations of linked lists is crucial for mastering
Self-Instructional
Material 35
NOTES more complex data structures. A linked list is a dynamic data structure that consists of
nodes, each containing data and a reference to the next node in the sequence. This
structure allows for efficient memory utilisation and flexible data management. The key
points about linked lists include:
Key points:
efficient insertions/deletions and dynamic memory allocation are critical, while NOTES
other structures may be more suitable for different needs.
Continual learning: Understanding the characteristics, strengths, and limitations
of linked lists is essential for designing efficient algorithms and data structures.
Further exploration and study can deepen knowledge and improve problem-
solving skills.
2.8 GLOSSARY
Linked List: A linear collection of elements where each element points to the
next one in the sequence using pointers.
Node: An individual element in a linked list containing both data and a reference
to the next node.
Singly Linked List: A linked list where each node points only to the next node
in the sequence.
Doubly Linked List: A linked list where each node has pointers to both the
next and previous nodes.
Head: The first node of a linked list.
Tail: The last node of a linked list.
1. A. One
2. C. Ability to traverse in both directions.
3. C. O(n)
4. A. Last node
5. B. Circular linked list.
Self-Instructional
Material 37
NOTES
2.10 SELF-ASSESSMENT QUESTIONS
2.11 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
LESSON 3 NOTES
Structure
3.1 Learning Objectives
3.2 Introduction
3.3 Stack as an ADT
3.4 Implementing Stacks using Arrays
3.5 Implementing stacks using Linked Lists
3.6 Applications of Stacks
3.7 Summary
3.8 Glossary
3.9 Answers to In-Text Questions
3.10 Self-Assessment Questions
3.11 References
3.12 Suggested Readings
NOTES
3.2 INTRODUCTION
Imagine you are at a cafeteria, and you see a stack of plates on a table. Each plate is
placed on top of another, forming a stack. Here, the topmost plate is the only accessible
one, and you can perform two primary operations: adding a plate to the stack or
removing a plate from the Stack.
Push Operation: When you want to add a plate to the stack, you take a new
plate and place it on top of the stack. This action is similar to pushing an element
onto a stack in programming. The new plate becomes the topmost plate, and
any existing plates remain underneath it. By adding plates one by one, you keep
extending the stack vertically.
Pop Operation: Now, let us say you want to remove a plate from the stack.
You can only remove the topmost plate, as the plates underneath are not
accessible until the top plate is removed. You lift the topmost plate from the
stack and place it on your tray or table. This action corresponds to the pop
operation in programming. The plate that was popped off is no longer part of
the stack, and you can access the plate beneath it if needed.
Last-In-First-Out (LIFO) Principle: The concept of a stack follows the Last-
In-First-Out (LIFO) principle. It means that the last plate added to the Stack is
the first one to be removed. In our cafeteria example, if you add plates in the
order A, B, and C, then C would be the topmost plate. When you start removing
plates, C would be the first one to be removed, followed by B and then A. This
principle ensures that the most recently added element is always accessible and
processed first.
Stack Overflow and Underflow: In the cafeteria scenario, there is a limit to
the number of plates you can stack. If you keep adding plates beyond the
maximum capacity, you will encounter a stack overflow situation. Similarly, if
you try to remove a plate from an empty stack, you will face a stack underflow
situation. These scenarios reflect the limitations and constraints of stacks in
programming as well.
Overall, the Stack of plates in a cafeteria serves as a tangible example of a
Self-Instructional
40 Material stack data structure. Understanding how plates are added and removed and the LIFO
principle associated with them can help in comprehending the concept of stacks in NOTES
programming.
A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle.
It can be visualised as a stack of plates, where the topmost plate is the only accessible
one. In programming, a stack allows operations only at one end, known as the top.
Elements are added or removed from the top of the stack, similar to stacking or
unstacking plates.
Stack Operations
Self-Instructional
Material 41
Figure 3.1: Diagrammatic Representation of a Stack
NOTES
3.4 IMPLEMENTATION OF STACK USING ARRAYS
In C++, stacks can be implemented using arrays or linked lists. Here, we will focus on
implementing stacks using arrays.
Self-Instructional Stack() {
42 Material
void pop() {
if (isEmpty()) {
cout << “Stack underflow! Cannot pop element.” <<
endl;
return;
}
int poppedElement = elements[top--];
cout << “Popped element:” << poppedElement << endl;
}
int peek() {
if (isEmpty()) {
cout << “Stack is empty!” << endl;
return -1; // Returning -1 as an error value
}
Self-Instructional
Material 43
Explanation
The Stack class has private member variables, top and elements, to maintain the top
index and store the stack elements, respectively.
The constructor initialises top as -1, indicating an empty stack.
The isEmpty() function checks if the stack is empty by comparing the value of
top with -1.
The isFull() function checks if the stack is full by comparing the value of top with
the maximum size of the stack.
The push() function adds an element to the stack if it is not full. It increments top
and assigns the value to the corresponding index in elements[].
Self-Instructional
44 Material
The pop() function removes the topmost element from the stack if it is not NOTES
empty. It retrieves the element at the index top, decrements the top, and prints the
popped element.
The peek() function returns the value of the topmost element without removing
it. It checks if the stack is empty and returns -1 as an error value.
The size() function returns the number of elements in the stack by adding 1 to
the top.
In the main() function, we create an instance of the stack class and perform
stack operations.
Sample Output
Element 10 pushed to the Stack.
Element 20 pushed to the Stack.
Element 30 pushed to the Stack.
Stack size: 3
Top element: 30
Popped element: 30
Popped element: 20
Stack size after popping: 1
Top element after popping: 10
Linked lists provide an efficient way to implement stacks dynamically, allowing for
easy insertion and deletion of elements.
Node Structure
To implement a stack using a linked list, we define a node structure. Each node represents
an element in the stack and contains two components: the data value and a pointer to
the next node. The node structure can be defined as follows:
Self-Instructional
Material 45
Top Pointer
In a linked list-based stack implementation, we maintain a pointer called ‘top’, which
points to the topmost element of the stack. Initially, when the stack is empty, the top
pointer is set to nullptr.
Node* top = nullptr;
Push Operation
The push operation adds a new element to the top of the stack. To implement push,
we create a new node, assign the data value, and update the next pointer to point to
the current top node. Finally, we set the top pointer to the newly added node.
void push(int value) {
Node* newNode = new Node(value);
newNode->next = top;
top = newNode;
}
Pop Operation
The pop operation removes the topmost element from the stack. To avoid any errors,
we need to check if the stack is empty before performing the pop operation. To pop
an element, we first store the address of the top node in a temporary pointer, update
the top pointer to the next node, delete the temporary node, and return the popped
element.
Self-Instructional
46 Material
Peek Operation
The peek operation retrieves the value of the topmost element without removing it
from the stack. Similar to the pop operation, we need to check if the stack is empty. If
not, we return the data value of the top node.
int peek() {
if (isEmpty()) {
//Stack is empty
return -1; // Or any other appropriate error value
}
return top->data;
}
//isEmpty operation:
//The isEmpty operation checks if the Stack is empty by verifying
if the top pointer is nullptr.
bool isEmpty() {
return (top == nullptr);
}
Time Complexity
The time complexity of the push, pop, and peek operations in a linked list-based stack
implementation is O(1) since they involve constant-time operations.
Self-Instructional
Material 47
NOTES By understanding the theory behind implementing a stack using linked lists, you can
leverage this data structure to solve various programming problems efficiently. Let us
have a look at the complete program:
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
class Stack {
private:
Node* top; //pointer to the top of the Stack
public:
Stack() {
top = nullptr; //Initialising top as nullptr to indicate
an empty stack
}
bool isEmpty() {
return (top == nullptr);
}
cout << “Element” << value << “ pushed to the stack.” << NOTES
endl;
}
void pop() {
if (isEmpty()) {
cout << “Stack underflow! Cannot pop element.” <<
endl;
return;
}
Node* temp = top;
int poppedElement = temp->data;
top = top->next;
delete temp;
cout << “Popped element:” << poppedElement << endl;
}
int peek() {
if (isEmpty()) {
cout << “Stack is empty!” << endl;
return -1; // Returning -1 as an error value
}
return top->data;
}
int size() {
int count = 0;
Node* current = top;
while (current != nullptr) {
count++;
current = current->next;
}
Self-Instructional
Material 49
int main() {
Stack stack;
stack.push(10);
stack.push(20);
stack.push(30);
cout << “Stack size:” << stack.size() << endl;
cout << “Top element:” << stack.peek() << endl;
stack.pop();
stack.pop();
cout << “Stack size after popping:” << Stack.size() << endl;
cout << “Top element after popping:” << Stack.peek() << endl;
return 0;
}
In this implementation, we use a linked list to represent the stack. Each element of the
stack is stored in a node, which contains the data value and a pointer to the next node.
The Node class represents a node in the linked list. It contains a data member
to store the value and a next pointer to point to the next node in the stack.
The Stack class has a private member variable top, which is a pointer to the top
node of the stack. The constructor initialises the top as nullptr to indicate an
empty stack.
The isEmpty() function checks if the stack is empty by checking if the top is
nullptr.
The push() function adds a new element to the top of the stack. It creates a new
node, assigns the value, updates the next pointer to the current top node, and
sets the top pointer to the new node.
The pop() function removes the topmost element from the stack. It checks if the
stack is empty, assigns the top node to a temporary pointer, updates the top
Self-Instructional
pointer to the next node, deletes the temporary node, and prints the popped
50 Material element.
The peek() function returns the value of the topmost element without removing NOTES
it. It checks if the Stack is empty and returns -1 as an error value.
The size() function calculates the number of elements in the stack by traversing
the linked list and counting the nodes.
In the main() function, we create an instance of the stack class and perform
stack operations.
This implementation of a stack using a linked list provides dynamic memory
allocation and allows the stack to grow and shrink as elements are pushed and popped.
Stack Overflow
Stack Overflow is a situation that can occur when a stack data structure exceeds its
maximum capacity, resulting in an error. Let us explore this concept further with code
examples. In programming, a stack is typically implemented using arrays or linked
lists. The size of an array-based stack is predetermined, meaning it has a fixed capacity.
When attempting to push elements beyond this capacity, a stack overflow error occurs.
Consider the following code snippet that demonstrates a stack implementation using
an array:
#include <iostream>
#define MAX_SIZE 5
class Stack {
private:
int arr[MAX_SIZE];
int top;
public:
Stack() {
top = -1; //Initialising top as -1 to indicate an empty
stack
}
bool isFull() {
Self-Instructional
return (top == MAX_SIZE - 1); Material 51
NOTES }
In the above code, the isFull() function checks if the stack is full by comparing
the value of top with MAX_SIZE - 1. If the condition is true, it means the Stack is full.
The push() function adds an element to the stack if it is not full. It increments top
and assigns the value to the corresponding index in the arr[] array. However, before
pushing, it checks if the stack is already full. If it is, a stack overflow error message is
displayed.
Now, let us illustrate a scenario where a stack overflow occurs:
int main() {
Stack stack;
stack.push(10);
stack.push(20);
stack.push(30);
stack.push(40);
stack.push(50);
stack.push(60); // Trying to push beyond the maximum capacity
return 0;
}
In the above example, we attempt to push six elements into the stack, which has
a maximum capacity of five. After pushing elements 10, 20, 30, 40, and 50, the next
Self-Instructional
52 Material
push operation stack.push(60) exceeds the Stack’s capacity. Consequently, a stack NOTES
overflow error message will be displayed.
Stack overflow errors are critical because they can lead to program crashes or
unexpected behaviour. It is crucial to handle stack capacity appropriately and ensure
that elements are pushed within the Stack’s limits.
Remember, understanding stack overflow situations helps in designing robust
programs and managing data structures effectively.
Stacks find applications in various domains due to their Last In, First Out (LIFO)
nature and efficient operations. Some common applications of stacks are briefed below:
Stacks are used to manage function calls in programming languages. When a function
is called, its information is pushed onto the call stack, and it is popped off when the
function returns.
2. Expression Evaluation
3. Undo Mechanisms
Many applications, such as text editors and graphic design software, use stacks to
implement undo functionalities. Each operation gets pushed onto the stack, enabling a
stepwise reversal.
4. Backtracking Algorithms
Stacks are employed in maintaining the backward and forward navigation history in
web browsers.
6. Memory Management
Some programming languages use a call stack for managing memory, especially for
local variables and function calls.
7. Parentheses Matching
8. Algorithmic Problems
Various algorithms, such as Depth - First Search (DFS) in graph traversal or tower of
Hanoi, can be efficiently solved using stacks.
9. Postfix Notation
Stacks are employed in converting infix expressions to postfix (Reverse Polish Notation)
for easier evaluation.
Compilers and interpreters use stacks to parse and evaluate mathematical expressions
during the compilation or interpretation process.
Operating systems use stacks to manage the execution of processes and tasks, including
keeping track of function calls within a process.
Beyond simple undo, stacks can be extended to implement redo features, allowing
Self-Instructional users to revert and reapply multiple actions.
54 Material
Stacks are used in syntax parsing during the compilation of programming languages to
ensure correct syntax structure.
Recursion heavily relies on stacks, where each recursive call pushes a new frame onto
the call stack.
Stacks are employed in simulating real-world scenarios where items are stacked or
processed in a Last In, First Out manner.
The versatility of stacks makes them a fundamental and widely applicable data
structure in computer science, playing a crucial role in various algorithms, systems,
and applications.
In-Text Questions
Q1. Which data structure follows the Last in First out (LIFO) principle?
A. Queue B. Stack
C. Array D. Linked List
Q2. What operation in a stack adds an element to the top of the Stack?
A. Push B. Pop
C. Insert D. Enqueue
3.7 SUMMARY
In this lesson, we introduced the concept of stacks and implemented a basic stack
class in C++ using arrays. We covered stack operations such as push, pop, peek,
isEmpty, and size. A stack is a fundamental data structure that adheres to the Last In,
First Out (LIFO) principle. Elements are added or removed from one end, commonly
Self-Instructional
Material 55
NOTES referred to as the “top”. The primary operations include “push”, which adds an element
to the top of the stack, and “pop”, which removes the top element. The “peek” operation
allows viewing the top element without removal. Stacks are used in various applications,
such as managing function calls in a call stack, evaluating mathematical expressions in
reverse Polish notation, and implementing undo mechanisms. They are pivotal for
maintaining order and tracking program execution flow, making them a versatile tool in
computer science and programming.
Key Points
Self-Instructional
56 Material
NOTES
3.8 GLOSSARY
Stack: A linear data structure that follows the Last In, First Out (LIFO) principle
for adding and removing elements.
Push: The operation to add an element to the top of the Stack.
Pop: The operation to remove the top element from the Stack.
Peek/Top: Accessing the top element of the stack without removing it.
Empty Stack: A stack is considered empty when it contains no elements.
Full Stack: Some stack implementations have a maximum capacity; a stack is
considered full when it reaches this capacity.
Stack Overflow: A situation where elements cannot be pushed onto a stack
because it has reached its maximum capacity.
Stack Underflow: A situation where elements cannot be popped from a stack
because it is empty.
1. B. Stack
2. A. Push
NOTES 5. Describe the implementation of a stack using arrays and linked lists. Highlight
their advantages and limitations.
6. Explain the process of pushing and popping elements in a stack and their
associated time complexities.
3.11 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
58 Material
LESSON 4 NOTES
Structure
4.1 Learning Objectives
4.2 Introduction
4.3 Queues: Queue as an ADT
4.4 Implementing Queues Using Arrays
4.4.1 Inserting an Element in a Queue Using Arrays
4.4.2 Deleting an Element in a Queue Using Arrays
4.4.3 Drawbacks
4.5 Implementing Queues Using Linked Lists
4.5.1 Insert an Element in a Queue Using Linked List
4.5.2 Delete an Element from a Queue Using Linked List
4.6 Double-ended Queue as an ADT
4.7 Summary
4.8 Glossary
4.9 Answers to In-Text Questions
4.10 Self-Assessment Questions
4.11 References
4.12 Suggested Readings
To understand the core concept of queues, which follows the First In, First Out
order.
Self-Instructional
Material 59
NOTES To learn the enqueue operation (adding an element to the rear) and dequeue
operation (removing an element from the front).
To learn about double-ended queues that support operations at both ends.
4.2 INTRODUCTION
Working of Queue
Self-Instructional
60 Material
NOTES
Self-Instructional
Material 61
NOTES
4.4 IMPLEMENTING QUEUES USING ARRAYS
We can easily represent queues using a linear array. Two variables can be used, which
are front and rear. They are implemented in the case of every queue, and the front and
rear variables point to the position from where elements can be inserted and deleted in
the queue.
In the figure below, we can see that there is an empty array that represents a
queue. When three elements, A, B, and C, are inserted into the queue, the value of the
front is 1, and the rear is 3.
When the Dequeue operation is performed, and element A is deleted. The value
of the front becomes 2, and the rear becomes 3.
Again, when three more elements are inserted, D, E, and F, the value of the
front becomes 2, and the rear is incremented and becomes 6.
When again another element, G, is inserted, notice that because there is no
more place left in the queue, it checks the initial position, and because it is empty, the
value of the rear becomes 1, and the front is at 2.
You can insert any element in a queue by first checking if the queue is already full and
comparing the front and the rear values. If the rear is at the last position and the front
is at the first position, then the queue is full, and no element can be inserted in this
queue. This is known as an overflow error, if the item to be inserted takes the first
position then in this case front and rear are equal to 0. Likewise, if more elements are
inserted, then the rear value gets incremented by one.
You can delete an element from the queue by first checking if there is an underflow
error or not. When the front value is greater than the rear value, then the queue is Self-Instructional
Material 63
NOTES empty, and no element can be deleted from this queue; this is an underflow error.
Otherwise, keep increasing the value of the front by one as one element is deleted
from the queue; the front value is incremented by one as elements are deleted from the
front end of the queue.
{
printf(“underflow”);
}
else
{
y = queue[front];
if(front == rear)
{
front = rear = -1;
else
front = front + 1;
}
Self-Instructional
64 Material return y;
} NOTES
}
4.4.3 Drawbacks
Several techniques are used for creating queues, out of which array implementation is
an easy technique. However, some drawbacks are associated with this technique.
1. Memory wastage: The space that the array utilises to store elements of the
queue can never be reused to store the elements of that queue because the
elements can only be inserted at the front end. The value in the front might be
high so that all the spaces before that can never be filled.
2. Deciding the size of the array: One of the most common problems while
using array implementation in queue is that the size of the array needs to be
declared in advance, due to which the queue cannot be extended at runtime
depending upon the problem statement. As the size of the array is fixed, it is
impossible to extend its size if required.
There are a number of drawbacks that are associated with the array implementation of
the queue. One alternative to this implementation is the linked list implementation of the
queue. The storage requirement of a linked representation of a queue is higher than that
of an array. However, dynamic allocation is possible in this representation. In this
representation, each note of the queue consists of two parts: a data part and a link part.
Self-Instructional
Material 65
NOTES Each element of the queue points to the immediate next element which is placed
in the memory. In this representation, two pointers are maintained in the memory: the
front pointer and the rear pointer. The front pointer contains the address of the starting
element of the queue, and the rear pointer contains the address of the last element,
which is there in the queue.
Insertion and deletion are performed at the rear and the front, respectively, like
the array representation. Although the pointers need to be maintained and updated
accordingly, if both the rear and the front are null, then it indicates that the queue is
empty.
There are a number of operations that can be performed on queues, and two
basic operations that can be implemented on linked queues are insert and delete.
The insert operation modifies the cube by adding an element at the end of the queue.
All the elements in the queue are added from the rare position, which increments the
rare value by one. The new element is the last element of the queue. First, memory is
allocated to the new note; we can insert the element into the empty Q by incrementing
the front by one if there is no element in the queue. In another scenario, when Q
contains a few elements and the front is not equal to null, then we need to update the
last Notes pointer.
The deletion operation allows you to remove elements that were inserted in the queue.
Self-Instructional
Elements are deleted from the first from the front position of the queue. If the front is Material 67
NOTES equal to null, which means that the list is empty, then in this case, it is an underflow
error, and no element can be deleted from the queue; otherwise, the front pointer is
shifted to the next node, and the front-most node is deleted.
NOTES
4.6 DOUBLE-ENDED QUEUE AS AN ADT
A queue is a data structure in which whatever comes in first goes out first. It follows
the FIFO (first in, first out) policy. When any element needs to be inserted in a queue,
it needs to be done from the rear end or the tail of the queue. In contrast, deletion is
done from the other end, which is the front end or the head of the queue. In the real
world, queues are used in various scenarios like a Canteen, a ticket queue outside a
cinema hall, and more.
A dequeue stands for a double-ended queue. It is also a linear data structure
like a queue where insertion and deletion operations both are performed from both
ends. It is a generalised version of a queue in which there is no restriction as to from
where the elements need to be inserted or deleted; there are two types of dequeue:
Input-restricted Queue and Output-restricted queue.
Self-Instructional
Material 69
NOTES
In-Text Questions
Q1. In a queue, which element gets removed first?
A. Element at the front B. Element at the rear
C. Random element D. Element at the center
Q2. Which of the following is an example of a non-linear data structure?
A. Stack B. Queue
C. Linked List D. Binary Tree
Q3. What is the data structure used to implement a queue using two stacks?
A. Linked List B. Array
C. Queue D. Stack
4.7 SUMMARY
Queues and Deques (Double-ended queues) are pivotal data structures that govern
the orderly processing of elements in a computer program. Queues adhere to the First
In, First Out (FIFO) principle, where elements are enqueued at the rear and dequeued
from the front, mimicking real-world scenarios like waiting lines. This structure is
fundamental for tasks like breadth-first search in graphs or managing processes in an
orderly manner. On the other hand, Deques extend this functionality, allowing operations
at both ends and enabling flexibility in handling data. Deques are particularly useful in
scenarios where elements need to be added or removed from both the front and rear,
providing a versatile tool for various algorithms and applications. Both queues and
deques play integral roles in data processing, synchronisation, and memory management,
offering structured solutions to a wide array of programming challenges.
4.8 GLOSSARY
Queue: A linear data structure that follows the First In, First Out (FIFO) principle
Self-Instructional
70 Material for adding and removing elements.
NOTES 6. Explain the process of enqueue and dequeue operations in a queue and their
associated time complexities.
7. Discuss the concept of priority queues and their implementation using different
data structures.
4.11 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
72 Material
LESSON 5 NOTES
NOTES
5.2 INTRODUCTION
In the realm of computer science and algorithmic design, the twin concepts of searching
and sorting stand as the cornerstones of efficient data manipulation. Searching involves
the quest for a specific element within a dataset, akin to locating a needle in a haystack.
Conversely, sorting is the art of arranging elements in a particular order, transforming
chaos into structured patterns. Both searching and sorting algorithms play pivotal roles
in shaping the efficiency and functionality of computer programs. The quest for optimal
solutions to fundamental challenges has motivated the development of a rich array of
algorithms, each tailored to specific scenarios and datasets. As we embark on the
exploration of searching and sorting, we delve into the core strategies employed to
streamline information retrieval and organization, unlocking the door to enhanced
computational efficiency and problem-solving skills.
Definition
Linear search, also known as sequential search, is a simple searching technique that
sequentially examines each element in a collection until the target element is found or
the entire collection is traversed. It is applicable to both ordered and unordered data
sets.
Here is the algorithm for Linear Search:
LinearSearch(arr, target):
Let n be the length of the array.
Self-Instructional
76 Material
The LinearSearch function takes an array ‘arr’ and a target value ‘target’ as
input. It iterates through each element in the array using a loop.
In each iteration, the algorithm compares the element arr[i] with the target value.
If they are equal, the target is found, and the function returns the index i where the
target was found.
If the loop completes without finding the target, it means the target value is not
present in the array. In this case, the function returns -1 to indicate that the target was
not found.
Code Example
#include <iostream>
using namespace std;
int linearSearch(int arr[], int size, int target) { for (int i
= 0; i < size; i++) {
if (arr[i] == target) {
return i; // Element found at index i
}
}
return -1; // Element not found
}
int main() {
int arr[] = {10, 20, 30, 40, 50};
int target = 30; Self-Instructional
Material 77
int size = sizeof(arr) / sizeof(arr[0]);
In the above example, the linearSearch function performs a linear search on the
given array arr[] of size 5 to find the target element. If the element is found, the function
returns the index of the element; otherwise, it returns -1. The main function demonstrates
how to use the linearSearch function.
Time Complexity: The time complexity of linear search is O(n), where n is the
number of elements in the collection. In the worst-case scenario, a linear search may
need to traverse the entire collection.
BinarySearch(arr, target)
Let n be the length of the array.
Set left = 0 and right = n - 1.
While left <= right:
o Set mid = (left + right) / 2.
o If arr[mid] equals target, return mid. (Target found)
Self-Instructional
78 Material
The BinarySearch function takes a sorted array arr and a target value target as
input. It initializes two pointers, left and right, which represent the current search space
within the array. The algorithm uses a while loop to narrow down the search space
repeatedly.
In each iteration, the algorithm calculates the middle index, mid by averaging left
and right. It compares the value at arr[mid] with the target value. If they are equal, the
target is found, and the function returns the index mid.
If arr[mid] is less than the target, it means the target is in the right half of the
remaining search space. In this case, the algorithm updates left to mid + 1 to discard
the left half.
If arr[mid] is greater than the target, it means the target is in the left half of the
remaining search space. The algorithm updates right to mid - 1 to discard the right half.
The loop continues until either the target value is found (returning the
corresponding index) or the search space is exhausted (left becomes greater than
right), indicating that the target is not present in the array. In the latter case, the function
returns -1 to indicate that the target was not found.
int main() {
int arr[] = {10, 20, 30, 40, 50};
int target = 30;
int size = sizeof(arr) / sizeof(arr[0]);
int result = binarySearch(arr, 0, size - 1, target);
if (result == -1) {
cout << “Element not found.” << endl;
} else {
cout << “Element found at index:” << result << endl;
}
return 0;
}
Time Complexity: The time complexity of binary search is O(log n), where n is NOTES
the number of elements in the sorted collection. Binary search reduces the search
space by half in each iteration, making it highly efficient for large datasets.
The BubbleSort function takes an array arr as input and performs the sorting in
place. It uses two nested loops to iterate through the array. The outer loop (i) controls
the number of passes needed to sort the array, which is one less than the total number
of elements. The inner loop (j) compares adjacent elements and swaps them if they
are out of order.
In each iteration of the inner loop, the algorithm compares arr[j] and arr[j+1]. If
arr[j] is greater than arr[j+1], indicating that they are in the wrong order, the algorithm
swaps them by assigning arr[j+1] to arr[j] and arr[j] to arr[j+1]. This swapping operation
places the larger element towards the end of the array. The algorithm continues this
process until the inner loop completes one pass through the remaining unsorted portion
of the array.
Self-Instructional
Material 81
NOTES The outer loop repeats this process, gradually reducing the number of unsorted
elements by one in each iteration. After n-1 iterations, the largest element will have moved
to its correct position at the end of the array. At this point, the entire array is sorted.
Code Example
#include <iostream>
using namespace std;
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, size);
Self-Instructional
82 Material
In the above example, the bubbleSort function performs bubble sort on the
given array arr[] of size, size. The nested loops compare adjacent elements and swap
them if they are in the wrong order. The main function demonstrates how to use the
bubbleSort function.
Time Complexity: The time complexity of bubble sort is O(n^2), where n is the
number of elements in the collection. Bubble sort has poor performance for large
datasets.
Insertion sort is a simple sorting algorithm that builds the final sorted array one element
at a time. It iterates over the collection, shifting elements to the right to find the correct
position for each element.
Here is the algorithm for Insertion Sort:
1. InsertionSort(arr):
Let n be the length of the array.
For i from 1 to n-1: (n-1 iterations)
o Set currentElement = arr[i].
o Set j = i - 1.
o While j >= 0 and arr[j] > currentElement:
Shift arr[j] to the right by one position: arr[j+1] =
arr[j].
Decrement j by 1.
o Place currentElement at its correct position: arr[j+1] =
currentElement. Self-Instructional
Material 83
NOTES The InsertionSort function takes an array arr as input and performs the sorting
in place. It uses a loop to iterate through the array from the second element to the last
element. In each iteration, it selects the current element (currentElement) from the
unsorted portion and compares it with the elements in the sorted portion (from index 0
to j, where j starts at i-1).
While j is greater than or equal to 0 and the element at index j is greater than the
currentElement, the algorithm shifts the element at index j to the right by one position
(making space for currentElement) by assigning arr[j+1] = arr[j]. It then decrements j
by 1 to continue comparing with the previous elements in the sorted portion.
Once the while loop is exited, the correct position for currentElement is found,
and it is placed at index j+1 (i.e., arr[j+1] = currentElement).
By repeatedly inserting each element into its correct position, Insertion Sort
gradually builds the sorted array. The sorted portion grows by one element in each
iteration until all elements are sorted.
Code Example
#include <iostream>
using namespace std;
void insertionSort(int arr[], int size) {
for (int i = 1; i < size; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
Self-Instructional
}
84 Material
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
insertionSort(arr, size);
cout << “Sorted array:”;
for (int i = 0; i < size; i++) {
cout << arr[i] << “ ”;
}
cout << endl;
return 0;
}
In the above example, the insertionSort function performs insertion sort on the
given array arr[] of size, size. It iterates through the collection, comparing each element
with the previous elements and shifting them to the right until the correct position is
found for the current element. The main function demonstrates how to use the
insertionSort function.
Time Complexity: The time complexity of insertion sort is O(n^2), where n is
the number of elements in the collection. Insertion sort performs well for small or
partially sorted datasets.
Self-Instructional
Material 85
Self-Instructional
86 Material
Figure 5.5: Diagrammatic Representation of Sorting Using Selection Sort
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, size);
cout << “Sorted array:”;
for (int i = 0; i < size; i++) {
cout << arr[i] << “ ”;
}
cout << endl;
return 0;
}
In the above example, the selectionSort function performs selection sort on the
given array, arr[] of size, size. It iterates through the collection, finding the minimum
element from the unsorted part and swapping it with the element at the beginning of the
unsorted part. The main function demonstrates how to use the selectionSort function.
Time Complexity: The time complexity of the selection sort is O(n^2), where n
is the number of elements in the collection. Selection sort performs the same number
Self-Instructional
of comparisons irrespective of the initial order of the elements. Material 87
NOTES
5.8 MERGE SORT
The MergeSort function is the entry point for the algorithm. It takes an array arr
and the indices left and right, which represent the range of elements to be sorted. The
function first checks if left is less than right to ensure that there is more than one
element in the subarray. If so, it calculates the middle index, mid and performs three
recursive calls: one to sort the left half, one to sort the right half, and one to merge the
Self-Instructional
88 Material two sorted halves using the Merge function.
The Merge function takes the array, arr and the indices left, mid, and right. It NOTES
first determines the sizes of the two subarrays to be merged, n1 and n2. Then, it
creates temporary arrays L and R and copies the respective elements from the original
array. After that, it iterates through the subarrays, comparing elements from L and R
and merging them back into the original array, arr in sorted order. Finally, if any elements
are remaining in either L or R, they are copied back into arr.
By repeatedly dividing the array into smaller halves and merging them back
together, Merge Sort achieves a time complexity of O(n log n) in the average and
worst cases, where n is the number of elements in the array.
Code Example
#include <iostream>
using namespace std;
Self-Instructional
Material 89
NOTES void merge(int arr[], int left, int middle, int right) {
int i, j, k;
int n1 = middle - left + 1;
int n2 = right - middle;
i = 0;
j = 0;
k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
j++; NOTES
k++;
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
mergeSort(arr, 0, size - 1);
cout << “Sorted array:”;
for (int i = 0; i < size; i++) {
cout << arr[i] << “ ”;
}
cout << endl;
return 0;
}
In the above example, the merge function merges two sorted halves of the
array, while the mergeSort function recursively divides the array into smaller halves
and merges them. The main function demonstrates how to use the mergeSort function.
Time Complexity: The time complexity of merge sort is O(n log n), where n is the
number of elements in the collection. Merge sort provides a stable and efficient sorting
solution for large datasets.
Self-Instructional
Material 91
NOTES
5.9 QUICK SORT
The partitions are already sorted in place, and the original array is now fully NOTES
sorted.
The above steps are typically implemented using the following pseudo-code:
function quickSort(arr, low, high):
if low < high:
pivotIndex = partition(arr, low, high) // Partition the
array
quickSort(arr, low, pivotIndex - 1) // Recursively
sort the left partition
quickSort(arr, pivotIndex + 1, high) // Recursively
sort the right partition
// Usage:
quickSort(arr, 0, size - 1) // Call the quickSort function to
sort the entire array
Let us understand with the help of an example where we have to sort the list of
elements: {10, 42, 15, 12, 37, 7, 9}.
Let us go through the steps:
1. Partitioning
The pivot element is chosen as 9.
Self-Instructional
Material 93
NOTES The partitioning process rearranges the elements in such a way that all elements
smaller than the pivot are moved to the left, and all elements greater than the
pivot are moved to the right.
After the partitioning process, the array may look like this: [7, 9, 10, 42, 37,
15, 12]
The pivot element, 9, is now in its final sorted position.
All elements to the left of the pivot (7) are smaller than the pivot.
All elements to the right of the pivot (10, 42, 37, 15, 12) are greater than the
pivot.
2. Recursion
At this point, we have two partitions: [7] and [10, 42, 37, 15, 12].
We recursively apply the Quick Sort algorithm on each partition.
For the partition [7]:
Since it contains only one element, no further sorting is required.
For the partition [10, 42, 37, 15, 12]:
We choose a new pivot element, let us say 15.
Perform the partitioning process again:
o The elements to the left of the pivot (10, 12) are smaller.
o The elements to the right of the pivot (42, 37) are greater.
After the partitioning process, the partition [10, 12, 15, 42, 37] is obtained.
3. Recursion:
We now have two partitions: [7] and [10, 12, 15, 42, 37].
Again, we recursively apply the Quick Sort algorithm on each partition.
For the partition [7]:
No further sorting is required since it contains only one element.
For the partition [10, 12, 15, 42, 37]:
We choose a new pivot element, let us say 37.
Self-Instructional
94 Material
Note: The choice of the pivot element can affect the performance of the Quick
Sort algorithm. In the example, the first pivot chosen was 9, but different choices
may lead to different partitioning and sorting outcomes. Self-Instructional
Material 95
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, size - 1);
cout << “Sorted array:”;
for (int i = 0; i < size; i++) {
Self-Instructional
96 Material
In the above example, the partition function selects a pivot and partitions the
array into two sub-arrays. The quickSort function recursively sorts the sub-arrays by
calling the partition function. The main function demonstrates how to use the quickSort
function.
Time Complexity: The average time complexity of quick sort is O(n log n), where n
is the number of elements in the collection. Quick sort is efficient and widely used in
practice.
Self-Instructional
Material 97
After placing each element in its correct position, decrease its count by one.
Self-Instructional
98 Material
countingSort(inputArray);
return 0;
Choosing the appropriate sorting algorithm depends on various factors, such as the
size of the data set, the desired time complexity, and the characteristics of the data.
Here are some guidelines on when to use which sorting algorithm:
Bubble Sort
Bubble sort is simple to understand and implement, but it has poor performance
compared to other sorting algorithms.
It is suitable for small data sets or nearly sorted data.
Bubble sort is not recommended for large or unsorted data sets due to its time
complexity of O(n^2).
Self-Instructional
100 Material
Insertion sort is efficient for small data sets or partially sorted data.
It has a time complexity of O(n^2), but it performs better than bubble sort.
Insertion sort is useful when the data set is already partially sorted or when the
number of elements is small.
Selection Sort
Merge Sort
Merge sort has a time complexity of O(n log n), making it efficient for large data
sets.
It is a stable sorting algorithm and is suitable for sorting linked lists.
Merge sort is useful when a stable sort is required or when dealing with large
data sets.
Quick Sort
Quick sort has an average time complexity of O(n log n) and performs well in
practice.
It is suitable for large data sets and is often used as a standard sorting
algorithm.
Quick sort is not stable, meaning the order of equal elements may change.
Self-Instructional
Material 101
Radix sort: Efficient for sorting integers with a fixed number of digits.
Counting sort: Efficient for sorting integers within a specific range.
Heap sort: Efficient for large data sets and guarantees a time complexity of
O(n log n).
Remember that the choice of sorting algorithm depends on the specific
requirements of your application. Consider the size and characteristics of the data, as
well as the desired time complexity, to select the most suitable algorithm. It is also
important to analyse the average and worst-case scenarios of each algorithm to ensure
optimal performance.
In-Text Questions
Q1. What is the time complexity of the linear search algorithm in the worst-case
scenario?
A. O(log n) B. O(n)
C. O(n^2) D. O(1)
Q2. In a list of 10 elements, what is the maximum number of comparisons needed
to find an element using linear search?
A. 5 B. 9
C. 3 D. 10
Q3. What is the prerequisite for applying the binary search algorithm on a list of
elements?
A. Elements should be sorted.
B. Elements should be in random order.
C. Elements should be in descending order.
D. Elements should be unique.
Q4. What is the time complexity of the binary search algorithm in the worst-case
scenario?
A. O(log n) B. O(n)
Self-Instructional C. O(n^2) D. O(1)
102 Material
Q5. In a sorted list of 16 elements, what is the maximum number of comparisons NOTES
needed to find an element using binary search?
A. 4 B. 5
C. 8 D. 16
Q6. What is the middle index of an array with 9 elements (assuming 0-based
indexing) when using binary search?
A. 3 B. 4
C. 5 D. 8
Q7. Which search algorithm is faster for finding an element in a sorted array?
A. Linear search
B. Binary search
C. Both have the same speed
D. Depends on the size of the array
Q8. Binary search works by repeatedly dividing the search interval by what value?
A. 2 B. 3
C. 10 D. The element to be searched
Q9. What is the main advantage of binary search over linear search?
A. It requires fewer comparisons.
B. It works on unsorted arrays.
C. It has a lower time complexity.
D. It can search for multiple elements simultaneously.
Q10. Which search algorithm is used when the elements are stored in a linked list?
A. Linear search
B. Binary search
C. Both A and B
D. None, linked lists cannot be searched
Q11. How does the insertion sort algorithm sort a list of elements?
A. It repeatedly divides the list into sublists.
B. It swaps adjacent elements until the list is sorted.
Self-Instructional
Material 103
5.12 SUMMARY
Searching techniques play a vital role in data retrieval. In this lesson, we explored
linear search and binary search, two commonly used search techniques. Linear search
is suitable for unordered collections, while binary search is efficient for sorted collections.
Understanding these techniques and their time complexities helps in choosing the
appropriate method for different scenarios. By implementing and utilizing these searching
techniques, you can efficiently locate desired elements within datasets, leading to
optimized and effective algorithms.
Sorting techniques are crucial for organizing data in a specific order. In this
lesson, we explored popular sorting algorithms, including bubble sort, insertion sort,
selection sort, merge sort and quicksort. Each technique has its own advantages and
time complexities. By implementing and understanding these sorting algorithms, you
can efficiently sort data in various scenarios. Choosing the appropriate sorting technique
based on the data size and properties is essential for optimal performance.
5.13 GLOSSARY
NOTES
5.14 ANSWERS TO IN-TEXT QUESTIONS
1. B. O(n)
2. D. 10
3. A. Elements should be sorted
4. A. O(log n)
5. A. 4
6. A. 3
7. B. Binary search
8. A. 2
9. C. It has a lower time complexity.
10. A. Linear search
11. D. It builds the final sorted array one element at a time by comparing elements
and shifting them if needed.
12. C. O(n^2)
13. A. The elements are swapped
14. C. Elements should be within a known range
15. D. O(n + k)
16. Counting the occurrences of each unique element
17. B. Array with a limited or known range of values
18. C. O(k)
1. Define searching in the context of computer science and explain its significance.
2. Discuss the difference between linear search and binary search. Compare their
Self-Instructional
time complexities and conditions for usage. Material 107
NOTES 3. Explain the working principle of the binary search algorithm and its efficiency in
searching sorted arrays.
4. Discuss the concept of hashing in searching algorithms and its application in
hash tables.
5. Explain the concept of interpolation search and discuss its advantages over
binary search.
6. Describe the sequential search method and discuss its effectiveness in different
scenarios.
7. Explain the working principle of the insertion sort algorithm. Discuss its time
complexity and its best and worst-case scenarios.
8. Discuss the key steps involved in the insertion sort algorithm and how it sorts
elements within an array.
9. Describe the process of insertion sort using an example array and illustrate the
step-by-step sorting process.
10. Explain the advantages and limitations of insertion sort compared to other sorting
algorithms.
11. Describe the counting sort algorithm and its working principle. Discuss its time
complexity and the scenarios where it performs efficiently.
12. Explain the counting phase and the rearranging phase in the counting sort
algorithm.
13. Discuss the implementation of counting sort and how it handles sorting elements
with non-negative integer keys.
14. Compare and contrast counting sort with other sorting algorithms in terms of
efficiency and applicability.
5.16 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Self-Instructional
108 Material
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms, NOTES
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
Material 109
LESSON 6 NOTES
Self-Instructional
Material 113
NOTES
6.1 LEARNING OBJECTIVES
To understand how a function can call itself, breaking a problem into simpler
subproblems.
Develop proficiency in solving problems using recursive approaches.
6.2 INTRODUCTION
Recursive Perspective
Self-Instructional
Material 115
The fundamental idea behind recursion is the concept of a function calling itself. When
a function is called, a new instance of the function is created, and control is transferred
to the new instance. The new instance executes its code, which may include calling the
same function again. This process continues until a base case is encountered, at which
point the recursion stops, and the function calls start returning their results.
A recursive function typically consists of two parts: the base case(s) and the recursive
case(s). The base case defines the simplest version of the problem that can be solved
directly without further recursion. The recursive case defines how the function calls
itself to solve a smaller subproblem. It also specifies how the results of these recursive
calls are combined to obtain the final result.
Let us illustrate recursion with an example of calculating the factorial of a number. The
factorial of a non-negative integer n, denoted as n! is the product of all positive integers
less than or equal to n.
#include <iostream>
int factorial(int n) {
if (n == 0)
return 1; // Base case: factorial of 0 is 1
else
return n * factorial(n - 1); // Recursive case: n! = n * (n-
1)!
}
int main() {
int number;
std::cout << “Enter a non-negative integer:”;
Self-Instructional
116 Material std::cin >> number;
std::cout << “Factorial of” << number << “ is ” << factorial(number) NOTES
<< std::endl;
return 0;
}
In this example, the factorial function calculates the factorial of a given number
n. If n is 0, the base case is triggered, and the function returns 1. Otherwise, it recursively
calls itself with n - 1 and multiplies the result by n. This process continues until n
becomes 0 and the base case is reached.
NOTES solving puzzles like Sudoku or the N-Queens problem or finding paths in a
maze.
Dynamic Programming: Dynamic programming often employs recursion to
solve optimization problems by breaking them down into overlapping
subproblems. Recursive calls are combined with memoization (caching computed
results) to avoid redundant computations. Examples include the Fibonacci
sequence, the knapsack problem, and the longest common subsequence
problem.
Parsing and Syntax Analysis: Recursive descent parsing is a top-down parsing
technique that uses recursive functions to analyse and parse the structure of a
grammar or language. Each function corresponds to a production rule in the
grammar, allowing for recursive handling of nested structures.
Comparison
Linear Recursion: In the factorial example, the function breaks down the problem
into one smaller instance by making a single recursive call.
Binary Recursion: In the Fibonacci example, the function breaks down the problem
into two smaller instances by making two recursive calls.
The key distinction is in the number of recursive calls made, and this choice depends
on the nature of the problem and how it can be effectively divided into subproblems.
Linear recursion refers to a situation where a function makes a single recursive call in
its definition.
return 1; NOTES
} else {
return n * factorial(n - 1);
}
}
int main() {
int n = 5;
std::cout << “Factorial of” << n << “ is:” << factorial(n) <<
std::endl;
return 0;
}
Binary recursion involves a function making two recursive calls in its definition.
The Fibonacci series has many interesting properties and appears in various fields,
including mathematics, nature, and computer science. It exhibits self-similarity and can
be found in patterns found in nature, such as the growth of plants and the arrangement
of leaves on stems. In computer science, the Fibonacci series is frequently used as an
example to demonstrate recursion, dynamic programming, and algorithmic analysis.
Note: The Fibonacci series is a sequence of numbers in which each number is the
sum of the two preceding ones. It starts with 0 and 1, and each subsequent
number is the sum of the previous two numbers. The Fibonacci series follows the
pattern:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34,... Self-Instructional
Material 119
NOTES To generate the Fibonacci series, the first two numbers (0 and 1) are explicitly
given. Then, each subsequent number is calculated by adding the two previous numbers.
Explanation: The Fibonacci function calculates the nth Fibonacci number. The base
case is when n is 0 or 1, where the function returns n itself. For larger values of n, the
function recursively calls itself with n-1 and n-2, combining the results to calculate the
Fibonacci number.
Self-Instructional
120 Material
Binary search is a highly efficient algorithm for searching in sorted collections, especially
when the number of elements is large. It is widely used in various applications, including
searching in databases, finding elements in sorted arrays, and implementing other
algorithms like interpolation search.
To perform Binary Search using recursion, write the following code:
int binarySearch(int arr[], int left, int right, int target) {
if (left > right)
return -1; // Base case: element not found
else {
int mid = (left + right) / 2;
if (arr[mid] == target)
return mid; // Base case: element found at mid index
else if (arr[mid] > target)
return binarySearch(arr, left, mid - 1, target); // Recursive
case: search in left half
else
return binarySearch(arr, mid + 1, right, target); // Recursive
case: search in right half
}
}
NOTES In a linked list, each element is represented by a node. A node contains two components:
the data it holds and a pointer/reference to the next node in the sequence. The first
node in the list is called the head, and the last node points to null or is referred to as the
tail, as shown below:
Node Node Node
+———+———+ +———+———+ +———+———+
| Data | Next | —> | Data | Next | —> | Data | Next |—> NULL
+———+———+ +———+———+ +———+———+
Head Tail
In this example, each node consists of a data field (which can hold any value)
and a next pointer that points to the next node in the list. The last node’s next pointer
is null, indicating the end of the list.
To implement Linked List using recursion, write the following code:
struct Node {
int data;
Node* next;
};
Explanation: The power function calculates the result of raising a base number to an
exponent. If the exponent is 0, the base case is triggered, and the function returns 1.
Otherwise, it recursively calls itself with the base and exponent - 1, multiplying the
base with the result of the recursive call to calculate the power.
When a function calls itself, a new instance of the function is created, and a separate
set of local variables and parameters are allocated for each instance. The control flow
of the program moves to the new instance, and the original instance waits for the result
of the recursive call. Each instance maintains its own set of local variables and executes
its code independently.
As recursive calls occur, they are stacked on top of each other in a data structure
called the call stack. The call stack keeps track of the execution context of each
function call, including the values of local variables and the return address. Once the
base case is reached, the recursive calls start returning their results in reverse order,
with each instance passing its result back to the instance that called it. This process
continues until the original instance receives the final result.
It is essential to ensure that a recursive function terminates and does not result in an
infinite loop. To achieve this, the base case(s) must be well-defined and reachable Self-Instructional
Material 123
NOTES from the recursive case(s). Without a proper base case, the recursion would continue
indefinitely, eventually causing a stack overflow error.
Here is an example that illustrates the importance of a well-defined base case in ensuring
the termination of a recursive function:
#include <iostream>
void countDown(int n) {
if (n <= 0) {
std::cout << “Blastoff!” << std::endl;
} else {
std::cout << n << “ ”; countDown(n - 1); // Recursive call
with a smaller value
}
}
int main() {
countDown(5);
return 0;
}
Without the base case, the recursive function would continue indefinitely, resulting
in an infinite loop. In this example, the base case ensures that the recursion terminates
when n becomes 0 or negative. As a result, the countdown progresses in a well-
defined manner, printing the numbers in descending order until “Blastoff!” is reached.
By providing a clear and reachable base case, we prevent the recursion from
causing a stack overflow error, as the function knows when to stop and unwind the
recursive calls.
Self-Instructional
124 Material
These are just a few examples of how recursive algorithms find application in NOTES
various domains. Recursive thinking and problem-solving skills are essential for
programmers to solve complex problems and optimize solutions efficiently.
In-Text Questions
Q1. What is recursion in programming?
A. A programming language feature used for creating loops.
B. A process in which a function calls itself directly or indirectly.
C. A way to limit the number of function calls.
D. A programming technique to sort arrays.
Q2. What is the essential condition for a recursive function to terminate?
A. The function must have a ‘for’ loop inside.
B. The function must call another function.
C. The function must not return any value.
D. The function must have a base case.
Q3. What is the name given to the function that calls itself in a recursive process?
A. Calling function B. Recursive function
C. Base function D. Termination function
Q4. Which data structure is commonly used to implement recursion?
A. Queue B. Stack
C. Array D. Linked List
Q5. What is the term used to describe a scenario where a function directly or
indirectly calls itself in a never-ending loop?
A. Infinite loop B. Segmentation fault
C. Stack overflow D. Recursion limit
6.6 SUMMARY
Recursion is a powerful technique that allows us to solve complex problems by breaking Self-Instructional
them down into simpler subproblems. By using recursive calls and defining appropriate Material 125
NOTES base cases, we can elegantly solve problems that exhibit repetitive and self-similar
patterns. Understanding recursion is crucial for every programmer, as it provides a
powerful tool for solving a wide range of computational problems.
Key points
Comparison
Linear Recursion: In the factorial example, the function breaks down the problem
into one smaller instance by making a single recursive call.
Binary Recursion: In the Fibonacci example, the function breaks down the problem
into two smaller instances by making two recursive calls.
Self-Instructional
126 Material
The key distinction is in the number of recursive calls made, and this choice NOTES
depends on the nature of the problem and how it can be effectively divided into
subproblems.
In conclusion, recursion offers a powerful approach to problem-solving, allowing
programmers to break down complex problems, handle repetitive patterns, and elegantly
solve computational challenges. It is an essential concept for programmers to grasp,
as it provides a valuable tool that can be applied across diverse problem domains.
6.7 GLOSSARY
1. Explain the concept of recursion and how it differs from iteration in programming.
2. Discuss the base case in recursion and its importance in preventing infinite loops.
3. Explain the process of a recursive function calling itself and the role of the call
stack in managing recursive calls.
4. Discuss the advantages and limitations of using recursion in programming.
5. Explain the concept of tail recursion and its significance in optimizing recursive
functions.
6. Describe the process of factorial calculation using recursion and illustrate it with
an example.
7. Discuss the application of recursion in solving problems like the Tower of Hanoi
and the Fibonacci sequence.
8. Explain the concept of indirect recursion and provide an example to illustrate it.
9. Discuss the factors to consider when choosing between iterative and recursive
approaches for problem-solving.
Self-Instructional
128 Material
10. Explain the role of recursion in data structures like trees and graphs and its NOTES
applications in algorithms.
6.10 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
Material 129
LESSON 7 NOTES
Self-Instructional
Material 133
NOTES
7.2 INTRODUCTION
Objects that have a hierarchical structure can be organized easily with the help of a
Binary search tree. Moreover, this data structure is used to store and maintain a sorted
list of numbers. The structure of a tree with n nodes helps to search for the presence of
a number in O(log n) time.
A tree consists of nodes and arcs. The nodes of a tree are connected to each
other with arcs. The top node of the tree is called the root node, and the bottom
node is called the leaf node with no child nodes. Some examples of trees are
given in Figure 7.1.
Self-Instructional
134 Material
NOTES
7.4 BINARY TREE: DEFINITION AND PROPERTIES
If each node of a tree has a maximum of two child nodes, then it is called a binary tree.
Trees shown in Figure 7.1 (a) and (b) are examples of binary trees, whereas Figure
7.1 (c) is not a binary tree.
In a binary tree, each child is categorized as either a left child or a right child.
The quantity of leaves in a binary tree is a crucial factor in determining the expected
efficacy of a sorting method. A leaf node in a binary tree is known as a terminal node
whose pointer to its children is null.
A node’s level, according to the definition of a binary tree, is equal to the path
length it took to reach there plus one. Therefore, we can conclude that the root will be
present at level 1, its offspring at level 2, and so forth. We are aware that every binary
tree only has one root (20 = 1) at level 1, two children (21 = 2) at level 2, a maximum
of four children (22 = 4) at level 3, and so on. This leads us to the conclusion that every
level i + 1 has a maximum of (2i) nodes. Any tree that meets this requirement is
referred to as a complete binary tree.
A binary tree’s internal nodes are those that have two children but do not meet
the criteria for being a leaf node or the root. The total number of edges on the longest
path from the root node to the leaf node determines the height or maximum depth of a
binary tree. In other words, the greatest number of edges from the root to the farthest
leaf node determines the height of a binary tree.
There are a variety of different operations that can be performed on binary trees.
Creation of Binary tree
Deletion of a node in a Binary tree
Finding the number of nodes in a binary tree
Finding the height of a binary tree
Finding the number of leaves in a binary tree
Deleting a Binary tree
Self-Instructional
Material 135
A binary tree is a tree data structure in which each node has at most two children,
which are referred to as the left child and the right child. Here are some key properties
of a binary tree:
Root: The topmost node in a binary tree is called the root. It is the starting point
for traversing the tree.
Node: Each element in a binary tree is called a node. Each node can have at
most two children.
Parent and Child: A node in a binary tree is a parent to its left and right
children. Conversely, the left and right children are considered the children of
the parent node.
Leaf Nodes: Nodes that do not have any children are called leaf nodes or
leaves. They are the nodes at the bottom of the tree.
Internal Nodes: Nodes that have at least one child are called internal nodes.
They are not leaf nodes.
Depth: The depth of a node is the length of the path from the root to that node.
The depth of the root is 0.
Height (or Depth of the Tree): The height of a tree is the length of the longest
path from the root to a leaf. It is also the depth of the deepest node. The height
of an empty tree is typically considered to be -1.
Subtree: A subtree of a node is a tree formed by a node and all its descendants.
Binary Search Tree (BST): If a binary tree follows the property that for each
node, all elements in its left subtree are less than the node, and all elements in its
right subtree are greater than the node, then it is called a binary search tree.
Traversal: Binary trees can be traversed in various ways, including in-order,
pre-order, and post-order traversals.
Balanced Binary Tree: A binary tree is balanced if the height of the left and
right subtrees of any node differ by at most one. Balanced trees help maintain
efficient search and insertion operations.
Self-Instructional
136 Material
Full Binary Tree: A binary tree is full if every node has either 0 or 2 children. NOTES
No node has only one child.
Complete Binary Tree: A binary tree is complete if all levels, except possibly
the last, are completely filled, and all nodes at the last level are as left as possible.
These properties and characteristics make binary trees versatile data structures
with applications in various algorithms and data storage structures.
Binary tree traversal refers to the process of visiting each node in a binary tree exactly
once, following a specific order.
There are three common types of binary tree traversals:
In-order, pre-order, and post-order.
Let us explore each of these traversal methods with an example.
Consider the following binary tree:
1
/\
2 3
/\
4 5
In-Order Traversal
In in-order traversal, we visit the left subtree, then the root, and finally the right subtree.
In-Order: 4, 2, 5, 1, 3
Pre-Order Traversal
In pre-order traversal, we visit the root, then the left subtree, and finally the right
subtree.
Pre-Order: 1, 2, 4, 5, 3
Self-Instructional
Material 137
struct Node {
int data;
Node* left;
Node* right;
int main() {
// Constructing the binary tree
Node* root = new Node(1);
root->left = new Node(2);
root->right = new Node(3);
root->left->left = new Node(4);
root->left->right = new Node(5);
// Perform traversals
std::cout << “In-Order Traversal:”;
inOrderTraversal(root);
std::cout << “\n”;
return 0;
}
Self-Instructional
Material 139
NOTES
In-Text Questions
Q1. What is the maximum number of children a node can have in a binary tree?
A. 0 B. 1
C. 2 D. 3
Q2. In a binary tree, which node is at the topmost position?
A. Root node B. Leaf node
C. Internal node D. Sibling node
Q3. What is the height of a binary tree with only one node?
A. 0 B. 1
C. 2 D. Undefined
Q4. Which traversal visits the root node between the traversal of its left and right
subtrees?
A. Preorder traversal B. Inorder traversal
C. Postorder traversal D. Level order traversal
Q5. In a binary tree, what is the maximum number of nodes possible at the ‘k’
level?
A. 2^k B. k
C. 2^k - 1 D. k^2
7.6 SUMMARY
Types of Trees
Binary Tree: A tree in which each node has at most two children: a left child
Self-Instructional
and a right child.
140 Material
Binary Search Tree (BST): A binary tree where the left child of a node contains NOTES
a value less than the node’s value, and the right child contains a value greater
than the node’s value.
Full Binary Tree: A binary tree in which each node has either zero or two
children.
Complete Binary Tree: A binary tree in which all levels are completely filled
except possibly the last level, and nodes are filled from left to right.
Perfect Binary Tree: A binary tree in which all internal nodes have two children
and all leaf nodes are at the same level.
Traversal Techniques: Inorder Traversal: Traverse left subtree, visit root, traverse
right subtree. Preorder Traversal: Visit root, traverse left subtree, traverse right subtree.
Postorder Traversal: Traverse left subtree, traverse right subtree, visit root.
7.7 GLOSSARY
Binary Tree: A tree data structure in which each node has at most two children,
typically referred to as the left child and right child.
Binary Search Tree (BST): A binary tree in which the left child of a node
contains a value smaller than the node’s value, and the right child contains a
value greater than the node’s value, allowing efficient searching, insertion, and
deletion.
Complete Binary Tree: A binary tree in which all levels except possibly the
last are completely filled, and all nodes at the last level are as left as possible.
Full Binary Tree: A binary tree in which every node other than the leaves has
two children.
Perfect Binary Tree: A binary tree in which all internal nodes have exactly
two children, and all leaf nodes are at the same level.
Balanced Binary Tree: A binary tree in which the height of the two subtrees
of every node never differs by more than one.
Self-Instructional
Material 141
NOTES Traversal: The process of visiting and accessing all nodes in a tree data structure
in a specific order, such as in-order, pre-order, or post-order traversal.
1. C. 2
2. A. Root node
3. A. 0
4. A. Preorder traversal
5. A. 2^k
5. A set of n unique elements and an unlabelled binary tree with n nodes are provided NOTES
to us. How many ways can we fill the tree with the supplied set to make it a
binary search tree?
7.10 REFERENCES
Goodrich, M.T., Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition, Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional
Material 143
LESSON 8 NOTES
Self-Instructional
Material 147
NOTES
8.2 INTRODUCTION
Binary Search Trees (BSTs) stand as fundamental data structures in the realm of
computer science and are widely employed in various applications such as databases,
compilers, and file systems. A Binary Search Tree is a hierarchical structure that
organizes elements in a manner that allows for efficient search, insertion, and deletion
operations. It embodies the principles of binary search, providing a balanced and
ordered arrangement of data that facilitates rapid retrieval.
The Binary Search Tree are important because it optimizes the efficiency of
operations performed on a dataset. Traditional linear data structures, such as arrays or
linked lists, may have linear search times for various operations. However, Binary
Search Trees offer a logarithmic time complexity for search operations, enabling quicker
access to elements in a large dataset. This efficiency is due to the property of BSTs,
where each node has at most two children, and the left subtree of a node contains
values less than the node’s value, while the right subtree contains values greater than
the node’s value.
BSTs are also important in maintaining an ordered structure, making them
particularly advantageous for tasks like sorting and range queries. The logarithmic
time complexity ensures that even as the dataset grows, the performance of these
operations remains acceptable.
Additionally, Binary Search Trees find applications in scenarios where dynamic
data modification is a common requirement. The self-adjusting nature of certain variants,
such as Balanced Binary Search Trees (e.g., AVL trees or Red-Black trees), ensures
that the tree remains balanced, preventing degradation of performance over time. This
adaptability is important in scenarios where frequent insertions or deletions are required.
In essence, the Binary Search Tree form a foundational data structure that aligns
with the core principles of efficiency, order, and adaptability. Its utilization extends
across various computing domains, making it a key concept in the toolkit of any
computer scientist or software engineer.
Self-Instructional
148 Material
NOTES
8.3 BINARY SEARCH TREE
A binary search tree is a binary tree, as the name implies, that also has unique ordering
characteristics. Some properties separate a binary search tree from a regular binary
tree.
All values stored in the right subtree of the tree are more than or equal to the
root node.
All values stored in the left subtree of the tree are less than the root node.
Both subtrees of each node are also binary search trees, that is, they have the
above two properties.
A binary search tree has a parent node that stores a key value that is bigger than
its left child’s key value but smaller than its right child’s key value. Figure 8.1 depicts
an example of a binary search tree. Because of the ordering and hierarchy, binary
search trees are more widely employed in searching algorithms than other data
structures.
The binary search tree alike binary tree has a root node and every node in the
binary search tree is represented by its key value and two pointers for the left child and
right child of the node. Binary search trees provide an excellent structure for searching Self-Instructional
Material 149
NOTES a list and, at the same time, for inserting and deleting data into the list. Some basic
operations on a binary search tree are traversal of the tree, searching a node, inserting
a node and deletion of a node.
Finding a particular element or node within a binary search tree is referred to as searching
it. However, because the elements in a binary search tree are stored in a certain order,
finding a specific node in a binary search tree is rather simple. A binary search tree can
be implemented using several data structures like an array, linked list, queue, stack,
etc., but every structure has limitations on its implementation. Given below are the
steps to find a key (k) or a node in a binary search tree (T), which is implemented
using a linked list.
Binary-Search-Tree(T, k)
if(root(T) == NULL) // Tree is empty
return(FALSE)
elseif(root(T).key == k) // Value k matches with the value
at the root node of the tree
return(TRUE)
elseif(root(T).key < k) // Value k is greater than the alue
at the root node of the tree
The number of comparisons that occurred when looking for the favourable
node determines the time complexity of the search. Recursion or iteration nodes
Self-Instructional
150 Material
encountered during the process generate a path from the root node. The number of NOTES
comparisons depends on how many nodes are encountered along this path. Therefore,
the complexity depends on the length of the path leading to the node plus one. If we
look at the algorithm, we will see that the comparison of the key value is either done
with the left child or the right child. This says that the search operation will perform one
comparison at each level. If we talk about the worst case, the key value would be
present at the leaf node, and we would have to traverse the whole tree to find the
node. So, the total number of comparisons will be equal to the tree’s height, and it
would take us O(lg n) time to find the element. In the best case, the element will be
found at the root, and traversing the whole tree would be unnecessary. It will take us
O(1)to locate the node.
The insert function is used to add a new element at a proper location in a binary search
tree. The insert function must be created in such a way that it consistently violates the
binary search tree’s property. Insertion in a binary tree also resorts to comparisons to
find its right location in the tree. With key value , a tree node must be located, and the
new node must be attached to it in order to insert a new node. Steps to insert a node
with key value in the tree are as follows:
Insert-Binary-Search-Tree (T, k)
New-Node.key = k
Left(New-Node) = NULL
Right(New-Node) = NULL
if (root(T) == NULL)
root(T)=New-Node
return (T)
elseif (root(T).key < k)
Left(root(T)) = Insert-Binary-Search-Tree(Left(root(T)),k)
elseif (root(T).key > k)
Right(root(T)) = Insert-Binary-Search-Tree(Right(root(T)),k)
Like the searching algorithm, the total number of comparisons for looking at the
desired location will be equal to the tree’s height, and it would take us times to find the
Self-Instructional
Material 151
NOTES position to insert an element. In the best case, the element will be found at the root,
and it will take to insert the node.
Another action required to maintain a binary search tree is deleting a node. A node
from a binary search tree can be removed using the delete function. However, we
must remove a node from a binary search tree in a way that does not break its basic
properties. Deleting a leaf node in a tree is the simplest one. We perform a delete
operation on the node, and no swapping will be necessary. In case of deleting a node
with one child, the node to be deleted must be swapped with its child node and then
deleted like a regular leaf node. Deleting a node that has two children and multiple
successors is where it becomes tricky. Since deleting the node could harm the binary
search tree property, steps should be taken to prevent it.
In case of deleting an internal node, we will have to swap the node with its in-
order successor and make it the leaf node to delete it. There are two approaches to
deleting a node in a binary search tree.
1. Deletion by Merging
The two subtrees of the node are combined into one tree in this method, which is then
joined to the parent of the node. Since every right node is greater than its left sibling, to
merge, we find a left node with the greatest key value and make it the parent of the
right child. The left subtree’s rightmost node is the required node. By navigating this
subtree and following the right pointers up until null is reached, it can be found. As a
result, this node will not have any right children, and setting the right pointer of the
rightmost node to the right subtree will not put the original tree’s binary search trees’
attribute in jeopardy. Figure 8.2 depicts an example of the deletion of a node in a
binary search tree by the method of merging.
Self-Instructional
152 Material
16 12
NOTES
10 14
12 21
10 14 19 27 13 21
Delete node
16
19 27
13 24
24
25
25
Now, to understand the complete working of deletion by merging method, the algorithm
is given below.
Delete-by-Merge-Binary-Search-Tree(T,node)
if (node NULL)
if(Right(ode) == NULL) // Right subtree is empty
temp = node
node = Left(node)
elseif(Left(node) == NULL) // Left subtree is empty
temp = node
node = Right(node)
else // Node has both children
temp = Left(node)
while(Right(temp) NULL)
temp = Right(temp)
Right(temp) = Right(node)
temp = node
node = Left(node)
Free(temp) Self-Instructional
Material 153
16 14
12 21 12 21
10 14 19 27 Delete node 10 13 19 27
16
13 24 24
25 25
Now, to understand the complete working of deletion by copying method, the algorithm
is given below.
Delete-by-Copy-Binary-Search-Tree(T.node)
if (node NULL)
if(Right(ode) == NULL) // Right subtree is empty
temp = node
node = Left(node)
elseif(Left(node) == NULL) // Left subtree is empty
temp = node
node = Right(node)
else // Node has both children
Self-Instructional temp = Left(node)
154 Material
In earlier sections, we made the case that searching in a tree, as opposed to a linked
list, which is linear and one-dimensional, is faster due to the hierarchy and sorting it
offers. However, if a tree is asymmetrical, lopsided, or has a significant variance in the
height of the subtrees, it can sacrifice resources even though it offers a more two-
dimensional and approachable structure. If every leaf node on a tree is at the same
level, the tree is said to be perfectly balanced. Finding a node in a balanced tree is
simpler and takes far less time than searching a linked list. Thus, maintaining the balance
of the binary search tree becomes more important.
For a node in the tree, the difference between the heights of the left and right
subtree is called a balance factor. If the balance factor is either 0 or 1, the binary tree
is said to be height-balanced or simply balanced. Otherwise, the tree is said to be
unbalanced or out of balance. For a node in the tree, the difference between the height
of the left and right subtree is called a balance factor. For example, the binary search
tree given in Figure 8.4 (a) is unbalanced, as nodes 12 and 16 have a balance factor of
more than 1. Meanwhile, in Figure 8.4 (b), all nodes have a balance factor of either 0
or 1, hence showing a height-balanced tree.
Self-Instructional
Material 155
NOTES (2)
14
(0)
16
(1)
(2) (1)
(0)
11 16
12 21
(1)
(0) (0)
(0) (0) (0)
(1) (0) 21
27 9 12 19
10 14 19
(0) (0)
(1) (0) 27
8 10
8 11
(0)
(a) (b)
Figure 8.4: Binary Search Tree (a) Unbalanced and (b) Height-Balanced
Various methods are used to maintain the balance of a binary search tree. Some
of the rotational strategies are LL, RR, LR, and RL. These approaches are used at the
time of insertion and deletion of a node in a binary search tree.
In-Text Questions
Q1. What is the property that distinguishes a binary search tree (BST) from other
binary trees?
A. It has only two children per node
B. Its nodes are sorted in descending order
C. It has a root node and leaf nodes
D. It follows a specific ordering of elements within its nodes
Q2. In a binary search tree, which subtree contains elements greater than the root
node’s value?
A. Left subtree B. Right subtree
C. Both subtrees D. Neither subtree
Self-Instructional
156 Material
Q3. What is the time complexity of searching for an element in a balanced binary NOTES
search tree containing ‘n’ nodes?
A. O(log n) B. O(n)
C. O(n^2) D. O(1)
Q4. Which traversal technique visits the nodes of a binary search tree in ascending
order?
A. Preorder traversal B. Inorder traversal
C. Postorder traversal D. Level order traversal
Q5. What operation is used to add a new node to a binary search tree while
maintaining its properties?
A. Insertion B. Deletion
C. Traversal D. Rotation
8.6 SUMMARY
Binary search trees are binary trees where the parent’s key value is higher than its left
child and smaller than its right child.
A binary search tree can be used for a wide variety of tasks, including searching,
insertion, deletion, balancing, and many more. Finding an element in a binary search
tree is simpler than finding it in another data structure. Certain guidelines are followed
when performing operations on a binary search tree in order to avoid distorting the
binary search tree attribute.
The newly inserted node is compared to its parent during insertion, and if there
are any inconsistencies, the node is switched out as necessary. The deletion by merging
method and the deletion by copying method are the two deletion strategies used.
Finding the left node with the highest key value in the event of merging, we make it the
parent of the right child. In a copying situation, we must locate the in-order successor,
copy its content into the node that will be removed, and then delete the in-order
successor.
While researching them, we come across an unbalanced binary search tree as Self-Instructional
well. The binary tree is referred to as being out of balance if the heights of the left and Material 157
NOTES right subtrees deviate by more than 0 or 1. If a tree is out of balance, we use certain
algorithms to balance it in order to correct the nodes’ balance factor. Four different
rotational strategies are available: LL, RR, LR, and RL.
As all operations require the traversal of the binary search tree along the height
of the tree, the time complexity of almost all these algorithms is .
8.7 GLOSSARY
Binary Search Tree (BST): A binary tree data structure where each node has
at most two children, and the left child contains a value less than the node’s
value, while the right child contains a value greater than the node’s value. This
arrangement allows for efficient searching, insertion, and deletion operations.
Node: An individual element within a BST that contains a value and pointers to
its left and right children.
Root Node: The topmost node in a BST, serving as the starting point for
traversal and the parent of all other nodes.
Parent Node: A node that has child nodes directly connected to it.
Child Node: Nodes directly connected to a parent node.
Leaf Node: Nodes that have no child nodes and exist at the ends of branches.
Internal Node: Nodes that have at least one child node and are not leaf nodes.
Inorder Traversal: A traversal method that visits nodes in the sequence: left
subtree, current node, right subtree, resulting in an ordered sequence of values
in a BST.
Preorder Traversal: A traversal method that visits nodes in the sequence:
current node, left subtree, right subtree, often used to create a copy of a BST.
Postorder Traversal: A traversal method that visits nodes in the sequence: left
subtree, right subtree, current node, commonly used in deleting nodes from a
BST.
Search Operation: The process of finding a specific value within a BST by
Self-Instructional
158 Material
comparing it with nodes based on the BST’s ordering property.
Insertion Operation: Adding a new node with a specific value into the BST NOTES
while maintaining its binary search tree properties.
Deletion Operation: Removing a node from the BST while maintaining its
structural and ordering properties.
Balanced BST: A BST in which the heights of the left and right subtrees of any
node differ by at most one, ensuring balanced performance for operations.
Unbalanced BST: A BST in which the heights of the left and right subtrees of
nodes can differ significantly, leading to inefficient operations.
1. Define a Binary Search Tree (BST) and explain its properties. How does a
BST differ from other types of trees?
2. Explain the process of inserting elements into a BST. Discuss the rules for
maintaining the properties of a BST during insertion.
3. Discuss the importance of the BST property (left child < parent < right child)
and its significance in searching elements within the tree.
4. Explain the process of searching for an element in a BST. Discuss the algorithmic
approach and its time complexity.
5. Describe the operations of deletion in a BST. Discuss the scenarios and challenges Self-Instructional
Material 159
involved in deleting nodes from a BST.
NOTES 6. Discuss the advantages and limitations of Binary Search Trees in comparison to
other data structures.
7. What is the difference between a binary tree and a binary search tree?
8. What is the difference between a height-balanced and unbalanced binary search
tree?
9. Why is balancing important for binary search trees?
8.10 REFERENCES
Goodrich, M.T, Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition. Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Sahni, S., Data Structures, Algorithms and applications in C++, 2nd edition,
Universities Press, 2011.
Tenenbaum, A. M., Augenstein, M. J., & Langsam Y., (2009), Data Structures
Using C and C++. 2nd edition. PHI.
Self-Instructional
160 Material
LESSON 9 NOTES
Structure
9.1 Learning Objectives
9.2 Introduction
9.3 Binary Heap
9.3.1 Array Representation of Binary Heap
9.3.2 MAX-HEAPIFY Property
9.3.3 BUILD-MAX-HEAP
9.4 Priority Queue
9.4.1 Max Priority Queue and its Operation
9.4.2 Implementing Priority Queue Using Heap
9.5 Summary
9.6 Glossary
9.7 Self-Assessment Questions
9.8 Answers to In-Text Questions
9.9 References
9.10 Suggested Readings
Self-Instructional
Material 161
NOTES
9.2 INTRODUCTION
Heap is a special type of tree data structure that is based on the theory of binary trees.
Heaps are utilised in numerous well-known algorithms, including priority queue
implementation, the heap sort sorting algorithm, and Dijkstra’s method for determining
the shortest path. In general, heaps are the type of data structure to utilise when you
need to retrieve the maximum or smallest member quickly.
Self-Instructional
162 Material
NOTES
(a) (b)
Figure 9.1: Example of (a) Max-heap and (b) Min-heap
NOTES We can try this idea on the nodes of the example shown in Figure 9.2. Here, a node at
index 2 has a parent node at index 1 and a left child at index 3. Similarly, we can
explore all other nodes.
Although heaps can be implemented as arrays, or we can say that each heap is
an array, we must remember that not all arrays are heaps. If we examine the usage of
max-heap and min-heap, there are several of them that we use in our daily algorithms.
The most frequent application of max-heap, however, is when sorting a data structure
using the heapsort algorithm. The most typical application of min-heap is when we run
priority queue-based algorithms, which we will explore in the next sections.
r Right(i) NOTES
if l heap-size
Largestlargest(A[i], A[l])
if r heap-size
Largestlargest(A[Largest], A[r])
if(i Largest)
Swap(A[i], A[Largest])
The largest variable in the MAX-HEAPIFY method, it contains the index of the
elements in a parent-child relationship with the highest key value. If the largest equals
the parent node’s index, the function terminates and returns. Suppose the largest is not
equal to the parent node’s index. In that case, the children node is swapped with the
parent node, and MAX-HEAPIFY is called recursively on other subtrees to satisfy
additional max-heap properties. The running time of MAX-HEAPIFY is .
MAX-HEAPIFY is a very helpful algorithm that can be combined and used with
other heap techniques. When a node is added to a heap, it becomes the leaf node of
the heap, which may alter the heap’s properties. In order to arrange the heap desirably,
MAX-HEAPIFY is used in this situation. Like deletion, if removing a node causes the
heap to become distorted and no longer satisfy the heap property, MAX-HEAPIFY
aids in repairing the heap. One such algorithm of the heap is BUILD-MAX-HEAP.
9.3.3 BUILD-MAX-HEAP
The technique uses the bottom-up method of invoking MAX-HEAPIFY from an array
of size n, which is called BUILD-MAX-HEAP. Observe Figure 9.2 and notice the
array representation for storing an n-element heap. The heap has the leaf nodes at
indexes . Since leaf nodes have no children, so we do not need to run MAX-HEAPIFY
on them. Thus, to save waste of time and space, the BUILD-MAX-HEAP algorithm is
written in the way given below.
BUILD-MAX-HEAP(A,n)
n
for i = - 1 downto 1
2
Max-Heapify(A,i)
Self-Instructional
Material 165
In the real world, heap data structures have several uses, but the most common and
effective use is as a priority queue. A priority queue is a unique kind of queue where
each element is arranged in a hierarchy and has associated priorities. Operations are
performed according to the priority of elements. That is, an element with higher priority
is attended first. Generally, the key value of the element itself is considered for assigning
the priority. We can also set priorities according to our needs.
Binary heaps can also be used to implement priority queues. In general, there
are two types of priority queues: max-priority queues and min-priority queues. We
will study both specifications clearly in this section.
A max-priority queue is a queue where the highest element is given the maximum
priority, i.e., larger elements are in the front of the queue. Multiple operations can
occur on a max priority queue, , as given below:
Max(A): Returns element of queue with the largest key value.
Extract-Max: Removes and returns the element of queue with the largest key
value.
Insert: Insert element into the queue such that
Increase-key: Changes the key value of to , assuming .
Self-Instructional Task scheduling, shortest path algorithms, event-driven simulations, Huffman
166 Material
coding, and heap sort are just a few of the many uses for priority queues. Additionally,
they are employed in several computer science and engineering disciplines that call for NOTES
the sorting and searching of data according to priority, as well as in network routing
methods. The priority queue is also used in Google Maps to find the shortest path to
save travelling time. Now, let us understand the workings of the Max Priority Queue.
MAX-HEAP
This operation can be used to find the maximum element in the array object of the
max-heap. If we notice the structure of a max-heap, the maximum element is found in
its root. Hence, in array , the element at index 1 will be the maximum. Using this idea,
the MAX-HEAP(A) operation works in the following way.
MAX-HEAP(A)
if heap-size 1
return A[1]
else
Display “Error Message: Heap Underflow”
EXTRACT-MAX-HEAP
NOTES HEAP-INCREASE-KEY
This function first checks to make sure the key in the object x will not decrease due to
the new key k, and if there are no issues, it provides x the new key value. The process
then locates the array index i that corresponds to object x so that x is A[i]. The
operation also compares the value of the new A[i] with its parent to preserve the
max-heap property like MAX-HEAPIFY since raising the key of A[i] can violate the
max-heap.
HEAP-INCREASE-KEY(A,x,k)
if k < A[x]
Display “Error Message: New key is smaller than current
key.”
A[x] = k
while(x > 1 and A[Parent(x)] < A[x])
Swap(A[x], A[Parent(x)])
x = Parent(x)
MAX-HEAP-INSERT
It requires the array A that implements the max-heap, the new object x that is to be
added to the max-heap, and the array A’s size n as inputs. The procedure checks to
see if there is space in the array for the new element first. The max-heap is then
increased by including a new leaf in the tree whose key is – . The key of this new
element is then set to the appropriate value, maintaining the max-heap attribute, and
HEAP-INCREASE-KEY is called to insert an element into the queue.
MAX-HEAP-INSERT(A,x,n)
if heap-size == n
Display “Error Message: Heap Overflow”
heap-size = heap-size + 1
A[heap-size] = -
HEAP-INCREASE-KEY(A,heap-size,x)
NOTES
In-Text Questions
Q1. What is the time complexity of heapify operation in a binary heap with 'n'
elements?
a) O(1) b) O(log n)
c) O(n) d) O(n log n)
Q2. Which type of heap is suitable for implementing a priority queue where the
element with the highest priority is processed first?
a) Min Heap b) Max Heap
c) Binary Heap d) D-ary Heap
Q3. Which of the following is a common application of a heap data structure?
a) Graph traversal
b) Searching in a sorted array
c) Sorting linked lists
d) Implementing Dijkstra's algorithm for shortest paths
9.5 SUMMARY
A heap data structure is represented by a binary heap that takes the form of an almost
complete binary tree. For the implementation of Priority queues, binary heaps are
frequently employed. In 1964, J. W. J. Williams invented the binary heap as a data
structure for heapsort.
When it is required to repeatedly delete the item with the highest (or lowest)
priority or when insertions must be spaced out with root node deletions, a heap is a
useful data structure.
The first-in, first-out rule is used in queues; however, in priority queues, values
are deleted according to priority. The highest-priority component is eliminated first.
Priority queues can be implemented using different data structures like an array,
a linked list, a heap, or a binary search tree. One of these data structures that effectively
implements priority queues is the heap data structure. Self-Instructional
Material 169
NOTES A binary heap can be built with running time , and a heap can support any
priority-queue operation on a set of size in time.
9.6 GLOSSARY
Heap: The heap is a specialized tree-based data structure that satisfies the
heap property. It is commonly used to implement priority queues.
Heap Property: The heap property defines the order of elements in a heap.
For a max heap, each parent node must be greater than or equal to its children,
and for a min heap, each parent must be less than or equal to its children.
Max Heap: A type of heap where the value of each parent node is greater than
or equal to the values of its children.
Min Heap: A type of heap where the value of each parent node is less than or
equal to the values of its children.
Heapify: The process of maintaining the heap property (either max heap or
min heap) by rearranging elements in a heap after insertion or deletion.
Heap Sort: A sorting algorithm that uses the heap data structure. It involves
building a max heap and repeatedly extracting the maximum element.
Priority Queue: An abstract data type that operates similar to a queue but
assigns a priority level to each element. Priority queues are often implemented
using heaps.
Binary Heap: A specific type of heap where each node has at most two
children. Binary heaps are commonly used due to their efficient representation
as arrays.
D-ary Heap: A generalization of the binary heap where each node can have up
to D children. A binary heap is a 2-ary heap.
Parent Node: In a heap, a parent node is a node that has one or more child
nodes.
Child Node: In a heap, a child node is a node that is directly connected to a
Self-Instructional parent node.
170 Material
Sibling Nodes: Nodes that share the same parent in a heap are called sibling NOTES
nodes.
Leaf Node: A node in a heap that has no children. All leaf nodes are typically
found at the last level of the heap.
Heap Size: The number of elements currently present in the heap.
Complete Binary Tree: A binary tree in which all levels are completely filled
except, possibly, for the last level, which is filled from left to right.
Heap Operations: Operations like insertion, deletion (extract-max or extract-
min), and heapify that are performed on a heap.
Decrease Key: An operation in a heap where the value of a key is decreased,
and then the heap is adjusted to maintain the heap property.
Increase Key: An operation in a heap where the value of a key is increased,
and then the heap is adjusted to maintain the heap property.
1. What is the purpose of designing a heap data structure? Give some applications.
2. What is the difference between Max-heap and Min-heap?
3. What is the difference between heap size and array length ?
4. Represent an array as a heap structure.
5. Find the key values of the parent node, left child, and right child of the node at
index 4 and 6 of the heap represented as an array .
6. Find the leaf nodes and root node of the heap represented as an array . Also,
find the height of the heap using the number of nodes present in the heap.
7. Write the algorithm for MIN-HEAPIFY and BUILD-MIN-HEAP.
8. Given an array, use the MAX-HEAPIFY algorithm to build a Max-heap and
show all steps of computations performed.
9. Given an array, use the MIN-HEAPIFY algorithm to produce a Max-heap and
show all steps of computations performed. Self-Instructional
Material 171
1. B) O(log n)
2. B) Max Heap
3. D) Implementing Dijkstra's algorithm for shortest paths
9.9 REFERENCES
Goodrich, M.T, Tamassia, R., & Mount, D., Data Structures and Algorithms
Analysis in C++, 2nd edition. Wiley, 2011.
Cormen, T.H., Leiserson, C.E., Rivest, R. L., Stein C. Introduction to Algorithms,
4th edition, Prentice Hall of India, 2022.
Drozdek, A., Data Structures and Algorithms in C++, 4th edition, Cengage
Learning, 2012.
Self-Instructional Sahni, S., Data Structures, Algorithms and applications in C++, 2nd edition,
172 Material
Universities Press, 2011.