Data Structures and Algorithms (2014)
Data Structures and Algorithms (2014)
III
Preface
Welcome to the world of data structures and algorithms! This book is designed to be your comprehensive guide to
understanding and implementing fundamental data structures and algorithms that are essential for solving complex
problems in computer science and software engineering.
In today's digital age, where vast amounts of data are generated and processed every second, the ability to efficiently
organize and manipulate data is crucial. Data structures provide the foundation for storing and managing data
effectively, while algorithms offer the means to perform various operations on that data with optimal efficiency.
Whether you are a student studying computer science, a software engineer looking to enhance your problem-solving
skills, or a curious individual interested in understanding the inner workings of computer programs, this book aims to
equip you with the necessary knowledge and skills to tackle real-world challenges.
Throughout this book, we will explore a wide range of data structures such as arrays, linked lists, stacks, queues, trees,
heaps, hash tables, and graphs. Each chapter will delve into the theoretical foundations of these data structures,
discussing their characteristics, advantages, and limitations.
Moreover, we will delve into the realm of algorithms, which are step-by-step procedures for solving specific problems.
We will cover various algorithmic paradigms, including searching, sorting, graph traversal, dynamic programming,
and divide-and-conquer. You will learn how to analyze the efficiency of algorithms using Big O notation, enabling you
to make informed decisions about algorithm selection and optimization.
To facilitate your learning experience, this book will provide numerous examples, illustrations, and pseudocode
implementations. You will have the opportunity to apply your knowledge through hands-on coding exercises and
programming challenges, allowing you to solidify your understanding and develop practical skills.
It is important to note that while this book covers a broad range of data structures and algorithms, it is by no means an
exhaustive resource. The field of data structures and algorithms is vast and constantly evolving. This book will serve as
a strong foundation upon which you can build your knowledge and explore more advanced topics in the future.
Let us embark on this journey together and unlock the power of data structures and algorithms. By mastering the
concepts and techniques presented in this book, you will develop the ability to design efficient and elegant solutions to
complex computational problems, opening doors to exciting opportunities in computer science and beyond.
IV
V
Chapter 1: Introduction to Data Structures and Algorithms
A famous quote:
Program = Algorithm + Data Structure.
A program prescribes "what" is to be done with "which" data.
#include <iostream>
using namespace std;
int main()
{
int i, j,sum;
cin >> i >> j;
sum=i+j;
cout <<sum << endl;
return 0;
}
1
1.1.2 Algorithms
1.1.4 Efficiency
A solution is said to be efficient if it solves the problem within its resource constraints
or less cost
2
Cost is the mount of resources that the solution consumes such as time
Constraints:
Space (typical for many programs )
Time (specially for real time systems)
Bandwidth
However, that does not mean we always strive for the most efficient program.
If the program works well within resource constraints, there is no benefit to making it
faster or smaller.
Question: processor speed and memory size still continue to improve, will not today's
efficiency problem be solved by tomorrow's hardware?
The answer is no, our history proved it.
Reasons: as we develop more powerful computers, that addition is being used to
tackle more complex problems.
More sophisticated user interface
Bigger problem sizes
New problems previously deemed unfeasible
Data structure
1. Analyze your problem to determine the basic operations that must be supported.
Inserting a data item into the data structure,
Deleting a data item from the data structure,
Finding a specified data item etc…
2. Quantify the resource constraints for each operation.
Such as Time
3. Select the data structure that best meets these requirements.
The "simplest" that meets requirements
Note: Resource constraints on key operations such as search, insert and delete
drives the data structure selection.
4
Examples:
Integer
Values are …., -3, -2, -1, 0, 1, 2, 3, …..
Operations are +, -, *, /, % …
The abstract data type Integer is an infinite set.
The built-in data structure int is a particular implementation of the abstract data
type Integer.
Another built-in data structure long long int also implements the same abstract
type.
Programming 1.2
Programming 1.3
6
Definiteness: Each step must be clearly defined, having one and only one
interpretation. At each point in computation, one should be able to tell exactly what
happens next.
Sequence: Each step must have a unique defined preceding and succeeding step. The
first step (start step) and last step (halt step) must be clearly noted.
Feasibility: It must be possible to perform each instruction.
Correctness: It must compute correct answer for all possible legal inputs.
Language Independence: It must not depend on any one programming language.
Completeness: It must solve the problem completely.
Efficiency: It must solve with the least amount of computational resources such as
time and space.
Generality: Algorithm should be valid on all possible inputs.
Input/Output: There must be a specified number of input values, and one or more
result values.
1.6 Programs
7
Chapter 2: Algorithms and Algorithm Analysis
2.1 Algorithms
8
5. Efficiency: Algorithms are also evaluated based on their efficiency, which refers to
the use of computational resources, such as time and memory. An efficient algorithm
solves a problem in the most optimal and resource-friendly manner.
Algorithms are fundamental to computer science and play a vital role in various
applications, ranging from simple everyday tasks to complex computational problems.
They are used in software development, data analysis, artificial intelligence,
cryptography, optimization, and many other fields.
By designing and analyzing algorithms, computer scientists aim to develop efficient
and effective solutions to problems, improve computational processes, and advance
our understanding of what can be computed.
It enables us to:
Predict performance of algorithms
Compare algorithms.
Provide guarantees on running time/space of algorithms
Understand theoretical basis.
Primary practical reason: avoid performance bugs.
Client gets poor performance because programmer did not understand performance
characteristics.
9
Two approaches to measure algorithms efficiency/performance:
Empirical: Implement the algorithms and trying them on different instances of input
Use/plot actual clock time to pick one.
Theoretical/Asymptotic Analysis: Determine quantity of resource required
mathematically needed by each algorithms.
It is difficult to use actual clock because clock time varies based on:
Specific processor speed
Current processor load
Specific data for a particular run of the program
Input size
Input properties
Programming language (C++, java, python …)
The programmer (You, Me, Bill gate …)
Operating environment/platform (PC, sun, smartphone etc)
Therefore, it is quite machine dependent.
Critical resources:
Time, Space (disk, RAM), Programmer’s effort, Ease of use (user’s effort).
Factors affecting running time:
System dependent effects:
Hardware: CPU, memory, cache, …
Software: compiler, interpreter, garbage collector, …
System: operating system, network, other apps, …
System independent effects:
Algorithm.
Input data/ Problem size.
For most algorithms, running time depends on "size" of the input.
Size is often the number of inputs processed
Example: in searching problem, size is the no of items to be sorted
Running time is expressed as T(n) for some function T on input size n.
Examples:
int count()
{
int k=0; - 1 is for the assignment statement; int k=0;
cout<< “Enter an integer”; - 1 is for the output statement.
cin>>n; - 1 is for the input statement.
- In the for loop:
for (i = 0;i < n;i++) - 1 assignment, n+1 tests, and n increments
k = k+1; - n loops of 2 units for assignment and addition.
return 0; - 1 is for return statement.
}
T(n) = 1+1+1+(1+n+1+n)+2n+1 = 4n+6
void func()
{
int x=0; - 1 is for first assignment statement.
int i=0; - 1 is for second assignment statement.
int j=1; - 1 is for third assignment statement.
cout<< “Enter an Integer value”; - 1 is for output statement
cin>>n; - 1 is for input statement
while (i<n){ - In the first while loop:
x++; - n+1 tests
11
i++; - n loops of 2 units for two increments.
}
while (j<n) - In the second while loop:
{ - n tests.
j++; - n-1 increments.
}
}
T(n) = 1+1+1+1+1+n+1+2n+n+n-1 = 5n+5
Examples: Suppose we have hardware capable of executing 106 instructions per second.
How long would it take to execute an algorithm whose complexity function was T(n) =
2�2 on an input size of n =108 ?
The number of seconds per day is 86,400 so this is about 231,480 days (634 years).
12
2.3 Types of Algorithm Complexity Analysis
Best case:
Lower bound on cost.
Determined by “easiest” input.
Provides a goal for all inputs.
Worst case:
Upper bound on cost.
Determined by “most difficult” input.
Provides a guarantee for all inputs.
Average case:
Expected cost for random input.
Need a model for “random” input.
Provides a way to predict performance.
13
We also know that these coefficients will change if we buy a faster computer or disk
drive, or use a different language or compiler.
We want to ignore constant factors (which get smaller and smaller as technology
improves)
In fact, we will not worry about the exact values, but will look at “broad classes" of
values.
Growth Rates
The growth rate for an algorithm is the rate at which the cost of the algorithm grows
as the size of its input grows.
Refers to the study of an algorithm as the input size "gets big" or reaches a limit.
To compare two algorithms with running times f(n) and g(n), we need a rough
measure that characterizes how fast each function growsgrowth rate.
Ignore constants [especially when input size very large]
But constants may have impact on small input size
Several notations are used to describe the running-time equation for an algorithm.
Big-Oh (O), Little-Oh (o)
Big-Omega (Ω), Little-Omega(w)
Theta Notation(θ)
Big-Oh Notation
14
Definition:
For f(n) a non-negatively valued function, f(n) is in set O(g(n)) if there exist two
positive constants c and �0 such that f(n)≤cg(n)for all n>�0 .
Usage: The algorithm is in O(�2 ) in [best ,average, worst] case.
Meaning: For all data sets big enough (i.e., n > �0 ), the algorithm always executes in
less than cg (n) steps [in best, average or worst case].
Big-Oh Visualization
O(g(n)) is the set of functions with smaller or same order of growth as f(n)
Wish tightest upper bound:
While T(n) = 3�2 is in O(�3 ), we prefer O(�2 ).
Because, it provides more information to say O(�2 ) than O(�3 ).
Big-Oh
Demonstrating that a function f(n) is in big-O of a function g(n) requires that we find
specific constants c and no for which the inequality holds.
The following points are facts that you can use for Big-Oh problems:
1<= n for all n >= 1
n <= �2 for all n >= 1
2n <= n! for all n >= 4
log2 � <= n for all n >= 2
n <= n log2 � for all n >= 2
Example:
15
2.5.1 No Uniqueness
There is no unique set of values for �0 and c in proving the asymptotic bounds
Prove that 100n + 5 = O(�2 )
100n + 5 ≤ 100n + n = 101n ≤ 101�2 for all n ≥ 5
�0 = 5 and c = 101 is a solution
100n + 5 ≤ 100n + 5n = 105n ≤ 105�2 for all n ≥ 1
�0 = 1 and c = 105 is also a solution
Must find SOME constants c and �0 that satisfy the asymptotic notation relation.
16
2.5.4 Implication of Big-Oh Notations
We use Big-Oh notation to say how slowly code might run as its input grows.
Suppose we know that our algorithm uses at most O(f(n)) basic steps for any n inputs,
and n is sufficiently large, then we know that our algorithm will terminate after
executing at most constant times f(n) basic steps.
We know that a basic step takes a constant time in a machine.
Hence, our algorithm will terminate in a constant times f(n) units of time, for all large
n.
Chapter 3: Arrays
Arrays are fundamental data structures in computer science and programming. They
consist of a collection of elements, each identified by at least one array index or key.
These elements are typically of the same data type, stored in contiguous memory
locations, and accessed by their position within the array.
Here's a breakdown of key aspects regarding arrays in data structures and algorithms:
1. Ordered Collection: Arrays maintain the order of elements they contain. Each
element has a unique index or position within the array, starting from 0 to n-1 for an
array of size n.
2. Fixed Size: In most programming languages, arrays have a fixed size determined at
the time of declaration. This fixed size imposes constraints on the number of elements
that can be stored in the array.
17
3. Random Access: Arrays support constant-time access to elements based on their
index. This means accessing any element within the array takes the same amount of time,
regardless of its position.
4. Homogeneous Elements: Arrays typically store elements of the same data type. This
homogeneity allows for efficient memory allocation and retrieval.
5. Memory Efficiency: Arrays offer efficient memory allocation because they store
elements in contiguous memory locations. This property facilitates faster access
compared to other data structures like linked lists.
6. Insertion and Deletion: Insertion and deletion operations in arrays can be costly,
especially if they involve shifting elements to maintain order or resizing the array. In
some cases, appending elements at the end of the array (if space is available) can be done
relatively efficiently.
7. Dynamic Arrays: Some programming languages provide dynamic array
implementations (e.g., ArrayList in Java, vector in C++), which automatically resize
themselves as needed to accommodate additional elements. This dynamic resizing
mitigates the limitations of fixed-size arrays.
8. Multidimensional Arrays: Arrays can have multiple dimensions, forming matrices or
higher-dimensional structures. Accessing elements in multidimensional arrays requires
specifying indices for each dimension.
9. Common Operations: Arrays support common operations such as traversal (iterating
through elements), searching (finding a specific element), sorting (rearranging elements
in a specified order), and slicing (extracting a portion of the array).
10. Applications: Arrays are widely used in various algorithms and applications,
including sorting algorithms (e.g., quicksort, mergesort), searching algorithms (e.g.,
binary search), dynamic programming, representing matrices and images, implementing
hash tables, and more.
There are some very common problems that we use computers to solve:
Searching: Looking for specific data item/record from list of data items or set of records.
Sorting: placing records/items in order.
In general, the faster the algorithm is, the more complex it is.
You don’t always need to use or should use the fastest algorithm.
The list to be searched can either be ordered or unordered list.
18
3.2.1 Linear/Sequential Search
Basic algorithm:
Get the search criterion (key)
Get the first record from the file
While ( (record != key) and (still more records) )
Get the next record
End_while
#include <iostream>
using namespace std;
int main() {
int arr[] = {45, 56, 78, 99, 112, 135};
int n = sizeof(arr) / sizeof(arr[0]); // Calculate the size
of the array
int key;
if (index != -1) {
cout << "Element found at index: " << index << endl;
19
} else {
cout << "Element not found" << endl;
}
return 0;
}
This program defines a function linearSearch that takes an array arr, its size n,
and a key to search for.
It iterates through the array elements sequentially and returns the index of the key if
found, or -1 if not found.
In the main function, an array [45, 56, 78, 99, 112, 135] is defined, and the user is
prompted to enter a number to search for.
The linearSearch function is called with the array, its size, and the key to search
for, and the result is printed accordingly.
Observation: the search is faster on an ordered list only when the item being searched
for is not in the list.
Also, keep in mind that the list has to first be placed in order for the ordered search.
Conclusion: the efficiency of these algorithms is roughly the same.
So, if we need a faster search, on sorted list we need a completely different algorithm.
Sequential search is not efficient for large lists because, on average, the sequential
search searches half the list.
If we have an ordered list and we know how many things are in the list, we can use a
different strategy.
20
The binary search gets its name because the algorithm continually divides the list into
two parts.
Uses the divide-and-conquer technique to search the list.
#include <iostream>
using namespace std;
if (arr[mid] == key) {
return mid; // Return the index of the key if found
} else if (arr[mid] < key) {
low = mid + 1; // If key is greater, search in the
right half
} else {
high = mid - 1; // If key is smaller, search in the
left half
}
}
return -1; // Return -1 if the key is not found
}
int main() {
int arr[] = {27, 31, 45, 67, 88, 98, 111, 122, 147};
int n = sizeof(arr) / sizeof(arr[0]); // Calculate the size
of the array
int key;
if (index != -1) {
cout << "Element found at index: " << index << endl;
21
} else {
cout << "Element not found" << endl;
}
return 0;
}
This program defines a function binarySearch that takes a sorted array arr, the
lower bound low, the upper bound high, and the key to search for.
It repeatedly divides the search interval in half until the key is found or the interval
becomes empty.
In the main function, an array [27, 31, 45, 67, 88, 98, 111, 122, 147] is defined, and
the user is prompted to enter a number to search for.
The binarySearch function is called with the array, its lower and upper bounds,
and the key to search for, and the result is printed accordingly.
3.2.4 Efficiency
There are many known sorting algorithms: Bubble Sort, Heap Sort, Selection Sort,
Merge Sort, Insertion Sort, Quick Sort.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> arr = { 67, 89, 34, 21, 58, 79, 88 };
bubbleSort(arr);
return 0;
}
This code defines a function bubbleSort that implements the bubble sort algorithm
and sorts the given vector of integers.
In the main function, we create the array [67, 89, 34, 21, 58, 79, 88], print it, sort it
using bubbleSort, and then print the sorted array.
24
Figure 3.1: Example of Bubble sort by using sorting algorithms
25
3.3.3 Insertion Sort
Steps:
1. The left most value can be said to be sorted relative to itself. Thus, we don’t need to do
anything.
2. Check to see if the second value is smaller than the first one.
- If it is swap these two values.
- The first two values are now relatively sorted.
3. Next, we need to insert the third value in to the relatively sorted portion
- So that after insertion, the portion will still be relatively sorted.
4. Now the first three are relatively sorted.
5. Do the same for the remaining items in the list.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> arr = {43, 33, 19, 56, 65, 74};
insertionSort(arr);
return 0;
}
This code defines a function insertionSort that performs the insertion sort
algorithm on a vector of integers.
Then, in the main function, an array is initialized, displayed, sorted using insertion
sort, and then displayed again to show the sorted result.
27
Figure 3.2: Example of Insertion sort by using sorting algorithms
Basic Idea:
Loop through the array from i = 0 to n - 1.
Select the smallest element in the array from i to n
Swap this value with value at position i.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> arr = {76, 45, 54, 70, 23, 46};
selectionSort(arr);
return 0;
}
This code defines a function selectionSort that implements the Selection Sort
algorithm.
It iterates through the array and selects the minimum element in the unsorted portion
of the array and swaps it with the first unsorted element.
The main function initializes an array, displays it before sorting, sorts it using the
selection sort algorithm, and displays it again after sorting.
29
Best-case, Average-case and Worst-case for Searching and Sorting
Algorithms
A. Searching Algorithms:
1. Linear Search:
Best-case: O(1) - The element being searched for is the first element in the array.
Average-case: O(n) - On average, the element will be found in the middle of the array.
Worst-case: O(n) - The element is the last one in the array or not present at all.
2. Binary Search:
Best-case: O(1) - The element is found at the first mid-point check.
Average-case: O(log n) - The element is found after several mid-point checks.
Worst-case: O(log n) - The element is not present, and the array is completely divided.
B. Sorting Algorithms:
1. Selection Sort:
2. Bubble Sort:
Best-case: O(n) - The array is already sorted, so only one pass is needed to confirm it.
Average-case: O(n²) - On average, it requires n(n−1)/2 comparisons and swaps.
Worst-case: O(n²) - The array is in reverse order, requiring the maximum number of
swaps.
3. Insertion Sort:
Best-case: O(n) - The array is already sorted, so only n−1 comparisons are needed.
Average-case: O(n²) - On average, elements are inserted in the middle.
Worst-case: O(n²) - The array is in reverse order, requiring maximum shifts for each
insertion.
30
Chapter 4: Linked Lists
Linked lists are a fundamental data structure in computer science, used to store
collections of elements in a linear order. Unlike arrays, linked lists store elements in
nodes that contain data and a reference (or pointer) to the next node in the sequence.
Node: The basic unit of a linked list is a node. Each node contains two parts:
Data: The value or data the node holds.
Pointer (Next): A reference to the next node in the list.
Head: The head is the first node in a linked list. It serves as the entry point to the list.
Tail: The tail is the last node in a linked list. In some implementations, the tail node’s
next pointer is set to null (or None in Python) to indicate the end of the list.
Structures are aggregate data types built using elements of primitive data types.
Structure is defined using the struct keyword:
Example: struct Time {
int hour;
int minute;
int second;
};
The struct keyword creates a new user defined data type that is used to declare
variables of an aggregate data type.
Structure variables are declared like variables of other types.
Syntax: struct<structure tag> <variable name>;
31
Example: struct Time timeObject,
struct Time *timeptr
A list data structure is sequence of zero or more elements: A1, A2, A3, … AN
N: length of the list
A1: first element
AN: last element
Ai: element at position i
If N=0, then empty list
Linearly ordered:
Ai precedes Ai+1
Ai follows Ai-1
32
4.2.3 Common operations of the List of data structures
Need to know the maximum number of elements in the list at the start of the program.
- Difficult
- Wastes space if the guess is bad
Adding/Deleting an element can take O(n) operations if the list has n elements.
- As it requires shifting of elements
Accessing/changing an element anywhere takes O(1) operations independent of n.
- Random access
Elements are stored in contiguous array positions.
Adding an element
Normally first position (A[0]) stores the current size of the list
Actual number of elements currsize+1
Adding at the beginning: Move all elements one position up/behind
33
Add at position 1;
Increment the current size by 1
for (j = A[0]+1; j > 1; j--)
A[j] = A[j-1];
A[1] = new element;
A[0]=A[0]+1;
Adding at kth position: Move all elements one position behind, kth position onwards;
Add the element at the kth position
Increment current size by 1;
for (j = A[0]+1; j > k; j--)
A[j] = A[j-1];
A[k] = new element;
A[0]=A[0]+1;
34
Deleting at the kth position: Move all elements down one position ahead, k+1th
position onwards;
Decrement the current size by 1;
for (j = k; j < A[0]+1; j++)
A[j] = A[j+1];
A[0]=A[0]-1;
The key part of a linked list is a structure, which holds the data for each node.
Example: name, address, age or whatever for the items in the list and, most
importantly, a pointer to the next node.
struct node {
char name[20]; // Name of up to 20 letters
int age;
float height; // In metres
node *next; // Pointer to next node
};
35
struct node *head= NULL;
struct Node {
int data;
Node* next;
This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
The LinkedList class manages the linked list operations and contains a pointer head to
the first node.
The constructor initializes head to nullptr, indicating an empty list.
void display() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
current = nextNode;
}
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
38
int main() {
LinkedList list;
return 0;
}
struct Node {
int data;
Node* next;
This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
The constructor initializes the data and sets next to nullptr.
39
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
return 0;
}
41
This implementation provides a basic understanding of how to create a linked list, insert
elements at the end, and display the list using C++.
struct Node {
int data;
Node* next;
- This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
- The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
- The LinkedList class manages the linked list operations and contains a pointer head
to the first node.
- The constructor initializes head to nullptr, indicating an empty list.
void display() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
current = nextNode;
}
}
45
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
return 0;
}
struct Node {
int data;
Node* next;
46
- This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
- The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
47
Node* temp = head;
head = head->next;
delete temp;
}
- The LinkedList class manages the linked list operations and contains a pointer head
to the first node.
- The constructor initializes head to nullptr, indicating an empty list.
if (head == nullptr) {
head = newNode;
return;
}
48
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
}
void removeFromFront() {
if (head == nullptr) {
cout << "List is empty. Nothing to remove." << endl;
return;
}
- This function removes the node at the front (head) of the linked list.
- If the list is empty (head == nullptr), it prints a message and returns.
- Otherwise, it stores the current head in a temporary variable, updates the head to point
to the next node, and deletes the old head node to free memory.
void display() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
49
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
current = nextNode;
}
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
list.removeFromFront();
cout << "Linked list after removing another element: ";
list.display();
list.removeFromFront();
cout << "Linked list after removing the last element: ";
list.display();
return 0;
}
struct Node {
int data;
Node* next;
- This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
51
- The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
// Delete the last node and update the second last node's
next to nullptr
delete current->next;
current->next = nullptr;
}
53
- The LinkedList class manages the linked list operations and contains a pointer head
to the first node.
- The constructor initializes head to nullptr, indicating an empty list.
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
void removeFromEnd() {
if (head == nullptr) {
cout << "List is empty. Nothing to remove." << endl;
return;
}
54
// Traverse to the second last node
Node* current = head;
while (current->next->next != nullptr) {
current = current->next;
}
// Delete the last node and update the second last node's
next to nullptr
delete current->next;
current->next = nullptr;
}
- This function removes the node at the end of the linked list.
- If the list is empty (head == nullptr), it prints a message and returns.
- If there is only one node in the list, it deletes the head and sets head to nullptr.
- Otherwise, it traverses the list to find the second last node, deletes the last node, and
updates the second last node's next pointer to nullptr.
void display() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
55
current = nextNode;
}
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
list.removeFromEnd();
cout << "Linked list after removing another element: ";
list.display();
list.removeFromEnd();
cout << "Linked list after removing the last element: ";
list.display();
56
return 0;
}
struct Node {
int data;
Node* next;
- This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
- The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
}
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
// If k is out of bounds
if (current == nullptr || current->next == nullptr) {
58
cout << "Position is out of bounds." << endl;
return;
}
- The LinkedList class manages the linked list operations and contains a pointer head
to the first node.
- The constructor initializes head to nullptr, indicating an empty list.
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
void removeFromKthPosition(int k) {
if (head == nullptr) {
cout << "List is empty. Nothing to remove." << endl;
return;
}
- This function removes the node at the k-th position (0-indexed) in the linked list.
- If the list is empty (head == nullptr), it prints a message and returns.
- If the node to be removed is the head (k == 0), it updates the head to the next node and
deletes the old head.
- Otherwise, it traverses the list to find the (k-1)-th node, checks if the k-th node exists,
and then removes the k-th node by updating the (k-1)-th node's next pointer and deleting
the k-th node.
void display() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
61
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
current = nextNode;
}
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
return 0;
}
struct Node {
int data;
Node* next;
- This defines the structure of a node in the linked list. Each node contains an integer data
and a pointer next to the next node.
- The constructor initializes the data and sets next to nullptr.
class LinkedList {
private:
Node* head;
public:
// Constructor to initialize an empty linked list
LinkedList() {
head = nullptr;
63
}
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
- The LinkedList class manages the linked list operations and contains a pointer head
to the first node.
- The constructor initializes head to nullptr, indicating an empty list.
if (head == nullptr) {
head = newNode;
return;
}
current->next = newNode;
}
void traverse() {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "null" << endl;
}
- This function traverses the linked list starting from the head.
65
- It prints each node’s data followed by an arrow until it reaches the end of the list
(nullptr).
~LinkedList() {
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next;
delete current;
current = nextNode;
}
}
The destructor ensures that all dynamically allocated memory for the nodes is properly
freed when the LinkedList object is destroyed.
int main() {
LinkedList list;
return 0;
}
5.1 Stacks
Fundamental operations:
- Push: Equivalent to an insert
Add an element to the top of the stack
- Pop: Equivalent to delete
Removes the most recently inserted element from the stack
In other words, removes the element at the top of the stack
67
- Top/peek: Examines the most recently inserted element.
Retrieves the top element from the stack.
Attributes of Stack:
- maxTop: the max size of stack.
- top: the index of the top element of stack.
- values: element/point to an array which stores elements of stack.
Operations of Stack:
- IsEmpty: return true if stack is empty, return false otherwise.
- IsFull: return true if stack is full, return false otherwise.
- Top: return the element at the top of stack.
- Push: add an element to the top of stack.
- Pop: delete the element at the top of stack.
- DisplayStack: print all the data in the stack.
68
1. Pushing the elements into the stack
#include <iostream>
#include <stdexcept>
using namespace std;
class Stack {
private:
int* arr;
int top;
int capacity;
public:
// Constructor to initialize stack
Stack(int size) {
arr = new int[size];
capacity = size;
top = -1;
}
int main() {
// Array to be pushed onto the stack
int elements[] = {9, 8, -7, 4, 11, 6};
int n = sizeof(elements) / sizeof(elements[0]);
return 0;
}
Output Console:
Figure 5.1: The output form of pushing elements into the stack
1. Class Definition: The Stack class includes private members for the array arr to hold
stack elements, the integer top to keep track of the top index, and the integer capacity to
store the stack's capacity.
2. Constructor and Destructor: The constructor initializes the stack with a given size and
sets top to -1. The destructor frees the allocated memory.
3. Push Function: The push method checks if the stack is full using isFull(). If not, it
increments top and assigns the value to arr[top].
4. Pop Function: The pop method checks if the stack is empty using isEmpty(). If not,
it returns the top element and decrements top.
5. Peek Function: The peek method returns the top element without modifying the stack.
71
6. isEmpty and isFull Functions: These methods check if the stack is empty or full
by comparing top with -1 or capacity -1.
7. Driver Code: In the main function, we create a stack, push the elements of the array
onto it, and then pop and print the elements to demonstrate the stack operations.
This code provides a simple implementation of a stack using an array and demonstrates
basic stack operations.
#include <iostream>
#include <stdexcept>
using namespace std;
class Stack {
private:
int* arr;
int top;
int capacity;
public:
// Constructor to initialize stack
Stack(int size) {
arr = new int[size];
capacity = size;
top = -1;
}
int main() {
// Array to be pushed onto the stack
int elements[] = {7, 11, -9, -10, -18, 2, -15};
int n = sizeof(elements) / sizeof(elements[0]);
73
// Create a stack with sufficient capacity
Stack stack(n);
return 0;
}
Output Console:
Figure 5.2: The output form of popping elements into the stack
1. Class Definition: The Stack class includes private members for the array arr to hold
stack elements, the integer top to keep track of the top index, and the integer capacity to
store the stack's capacity.
2. Constructor and Destructor: The constructor initializes the stack with a given size and
sets top to -1. The destructor frees the allocated memory.
3. Push Function: The push method checks if the stack is full using isFull(). If not, it
increments top and assigns the value to arr[top].
74
4. Pop Function: The pop method checks if the stack is empty using isEmpty(). If not,
it returns the top element and decrements top.
5. Peek Function: The peek method returns the top element without modifying the stack.
6. isEmpty and isFull Functions: These methods check if the stack is empty or full
by comparing top with -1 or capacity -1.
7. Driver Code: In the main function, we create a stack, push the elements of the array
onto it, and then pop and print the elements to demonstrate the stack operations.
push(node *newnode)
{
cout << "Add data" <<endl;
cin >> newnode -> item;
newnode -> next = NULL;
if( topOfStack = = NULL){
topOfStack = newnode;
}
else {
newnode -> next = topOfStack;
topOfStack = newnode;
}
}
75
Algorithm for POP operations:
Step-1: If the Stack is empty then give an alert message "Stack Underflow" and quit; else
proceed.
Step-2: Make "target" point to topOfstack next pointer.
Step-3: Free the topOfstack node;
Step-4: Make the node pointed by "target" as your TOP most element.
int pop( ) {
int pop_val= 0;
if(topOfStack = = NULL)
cout << "Stack Underflow";
else {
node *temp= topOfStack;
pop_val= temp->data;
topOfStack =topOfStack-> next;
delete temp;
}
return(pop_val);
}
Balancing Symbols: to check that every right brace, bracket, and parentheses must
correspond to its left counterpart
Example, [( )] is legal, but [( ] ) is illegal
Algorithm:
(1) Make an empty stack.
(2) Read characters until end of file:
I. If the character is an opening symbol, push it onto the stack
II. If it is a closing symbol, then if the stack is empty, report an error
III. Otherwise, pop the stack. If the symbol popped is not the corresponding opening
symbol, then report an error
(3) At end of file, if the stack is not empty, report an error
- Though infix notation is convenient for human beings, postfix notation is much
cheaper and easy for machines.
- Therefore, computers change the infix to postfix notation first
- Then, the post-fix expression is evaluated.
Example: Suppose we want to see the infix notation: A + B * C, Convert to Prefix and
Postfix notation.
Operators: + and *
Operands: A, B, and C
77
Step 2: Understand Operator Precedence and Associativity
Start from the operator with the highest precedence (* in this case).
Write the operator before its operands (prefix form).
Move to the next operator with lower precedence (+).
Conversion Steps:
Operators: + and *
Operands: A, B, and C
Step 2: Understand Operator Precedence and Associativity
Start from the operator with the highest precedence (* in this case).
Write the operands first, followed by the operator (postfix form).
Move to the next operator with lower precedence (+).
Conversion Steps:
78
Summary:
Infix Expression: A + B * C
Prefix Expression: + A * B C
Postfix Expression: A B C * +
5.5 Queues
Basic operations:
enqueue: insert an element at the rear of the list.
dequeue: delete the element at the front of the list.
Empty queue:
- back = front - 1
79
Full queue:
- We need to count to know if queue is full
Solutions:
- Use a boolean variable to say explicitly whether the queue is empty or not
- Make the array of size n+1 and only allow n elements to be stored
- Use a counter of the number of elements in the queue
Attributes of Queue:
- front/rear: front/rear index.
- counter: number of elements in the queue.
- maxSize: capacity of the queue.
- values: point to an array which stores elements of the queue.
Operations of Queue:
- IsEmpty: return true if queue is empty, return false otherwise.
- IsFull: return true if queue is full, return false otherwise.
- Enqueue: add an element to the rear of queue.
- Dequeue: delete the element at the front of queue.
- DisplayQueue: print all the data.
1. Implementation of C++ to enqueuing and dequeuing from the queue by using array
#include <iostream>
#include <stdexcept>
using namespace std;
class Queue {
private:
int* arr;
int front;
int rear;
int capacity;
int count;
public:
// Constructor to initialize the queue
80
Queue(int size) {
arr = new int[size];
capacity = size;
front = 0;
rear = -1;
count = 0;
}
int main() {
// Array to be enqueued
int elements[] = {5, 7, 3, 4, 8, 11, 14, 17};
int n = sizeof(elements) / sizeof(elements[0]);
return 0;
}
Output Console:
Figure 5.3: The output form of enqueuing and dequeuing elements into the queue by using array
1. Define the Node Structure: Create a structure that represents a node in the linked list,
including a data field and a pointer to the next node.
2. Define the Queue Class: Create a class that represents a queue, including member
variables for the front and rear pointers of the linked list.
3. Initialize the Queue: Provide a constructor to initialize the queue with front and rear
pointers set to nullptr.
4. Enqueue Operation: Implement a function to add elements to the rear of the queue.
5. Dequeue Operation: Implement a function to remove elements from the front of the
queue.
6. Front Operation: Implement a function to return the front element of the queue without
removing it.
8. Driver Code: Use the queue class to enqueue and dequeue the array of integers and
demonstrate its functionality.
83
2. Implementation of C++ to enqueuing and dequeuing from the queue by using linked
list
#include <iostream>
#include <stdexcept>
using namespace std;
public:
// Constructor to initialize the queue
Queue() : front(nullptr), rear(nullptr) {}
int main() {
// Array to be enqueued
int elements[] = {5, 7, 3, 4, 8, 11, 14, 17};
int n = sizeof(elements) / sizeof(elements[0]);
85
// Create a queue
Queue queue;
return 0;
}
Output Console:
Figure 5.4: The output form of enqueuing and dequeuing elements into the queue by using linked list
1. Node Structure: The Node structure represents a node in the linked list. Each node
contains an integer data field and a pointer to the next node.
2. Queue Class: The Queue class contains private member variables front and rear, which
are pointers to the front and rear nodes of the linked list, respectively.
86
3. Constructor and Destructor: The constructor initializes the front and rear pointers to
nullptr, indicating an empty queue. The destructor frees the allocated memory by
dequeuing all elements until the queue is empty.
4. Enqueue Function: The enqueue method creates a new node with the given data. If the
queue is empty, both front and rear point to the new node. Otherwise, the new node is
added to the end of the queue, and rear is updated to point to this new node.
5. Dequeue Function: The dequeue method checks if the queue is empty. If not, it
retrieves the data from the front node, updates front to point to the next node, and deletes
the old front node. If the queue becomes empty after the dequeue operation, rear is also
set to nullptr.
6. Peek Function: The peek method returns the data from the front node without
modifying the queue.
7. isEmpty Function: This method checks if the queue is empty by checking if front is
nullptr.
8. Driver Code: In the main function, we create a queue, enqueue the elements of the
array onto it, and then dequeue and print the elements to demonstrate the queue
operations.
87
Chapter 6: Trees
6.1 Trees
88
Figure 6.1: Trees
SOME TERMINOLOGIES:
Child and parent: Every node except the root has one parent (J is a parent of P and Q)
A node can have an arbitrary number of children (A has 6 while D has 1)
Leaves/External Nodes: Nodes with no children (B, C, H, I, P, Q, K, L, M, N)
Sibling: nodes with same parent (P and Q)
Internal node: A node with at least one child (A,D,E,F,G,J)
Path: is a sequence of nodes from root to a node (arbitrary node in the tree).
Length: Number of edges on the path from node x to node y
Depth of a node: Number of edges from the root to that node (Depth of C =1).
Height of a node:
- Length of the longest path from that node to a leaf (E=2)
- All leaves are at height 0
- The height of a tree is equal to the height of the root.
Ancestor and descendant:
- The ancestors of a node are all the nodes along the path from the root to the node.
- Descendant node reachable by repeated proceeding from parent to child.
89
We use struct/class to abstract nodes
Generic methods:
- integer size()
- boolean isEmpty()
- displayElements()
Accessor methods:
- Object root()
- Object parent(p)
- displayChildren(p)
Query methods:
- boolean isInternal(p)
- boolean isExternal(p)
- boolean isRoot(p)
Update methods:
- swapElements(p, q)
- object replaceElement(p, o)
Additional update methods may be defined by data structures implementing the Tree
ADT.
#include <iostream>
#include <queue>
using namespace std;
public:
// Constructor to initialize the tree
BinaryTree() : root(nullptr) {}
queue<Node*> q;
q.push(root);
92
while (!q.empty()) {
Node* temp = q.front();
q.pop();
if (temp->left == nullptr) {
temp->left = newNode;
return;
} else {
q.push(temp->left);
}
if (temp->right == nullptr) {
temp->right = newNode;
return;
} else {
q.push(temp->right);
}
}
}
93
// Function to search for a value in the tree
bool search(int value) {
if (root == nullptr) return false;
queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* temp = q.front();
q.pop();
if (temp->data == value) {
return true;
}
if (temp->left != nullptr) {
q.push(temp->left);
}
if (temp->right != nullptr) {
q.push(temp->right);
}
}
return false;
}
};
searchValue = 10;
if (tree.search(searchValue)) {
cout << "Value " << searchValue << " found in the tree."
<< endl;
} else {
cout << "Value " << searchValue << " not found in the
tree." << endl;
}
return 0;
}
Output Console:
95
1. Node Structure: The Node structure represents a node in the binary tree. Each node
contains an integer data field and pointers to the left and right child nodes.
2. BinaryTree Class: The BinaryTree class contains a private member root, which is
a pointer to the root node of the tree. The class includes methods for inserting nodes,
traversing the tree (in-order, pre-order, and post-order), and searching for a value in the
tree.
3. Insertion: The insert method creates a new node with the given value and inserts it into
the tree. If the tree is empty, the new node becomes the root. Otherwise, the method
performs a level-order traversal to find the appropriate position for the new node.
5. Search: The search method performs a level-order traversal to find the node with the
specified value. If found, it returns true; otherwise, it returns false.
6. Driver Code: In the main function, we create a BinaryTree object, insert elements
into the tree, perform different traversals, and search for specific values to demonstrate
the tree operations.
96
6.5 Binary Search Trees
Stores keys in the nodes in a way so that searching, insertion and deletion can be done
efficiently.
Binary search tree property
- For every node X, all the keys in its left subtree are smaller than the key value in X, and
all the keys in its right subtree are larger than the key value in X.
#include <iostream>
using namespace std;
97
// Define the BinarySearchTree class
class BinarySearchTree {
private:
Node* root;
return node;
}
public:
// Constructor to initialize the BST
BinarySearchTree() : root(nullptr) {}
100
cout << "Value " << searchValue << " not found in the
BST." << endl;
}
searchValue = 10;
if (bst.search(searchValue)) {
cout << "Value " << searchValue << " found in the BST."
<< endl;
} else {
cout << "Value " << searchValue << " not found in the
BST." << endl;
}
return 0;
}
Output Console:
1. Node Structure: The Node structure represents a node in the BST. Each node contains
an integer data field and pointers to the left and right child nodes.
3. Insertion: The insert method is a recursive function that inserts a new value into the
BST while maintaining the BST property. If the tree is empty, the new node becomes the
root. Otherwise, the method recursively finds the correct position for the new node.
findMin: Return the node containing the smallest element in the tree
Start at the root and go left as long as there is a left child. The stopping point is the
smallest element
Node*findMin(node*root)
{
If(root==NULL)
Return Null;
Else if(root->left==Null)
Return root
Else
Return(findMin(root->left)
}
Node*findMin(node*root)
{
If(root==NULL)
Return Null;
Else if(root->right==Null)
Return root
102
Else
Return(findMin(root->right)
}
delete:When we delete a node, we need to consider how we take care of the children
of the deleted node.
When a node is deleted the definition of a BST should be maintained.
When a node is deleted four cases should be considered
Case 1: Deleting a leaf node (a node with no child)
Case 2: Deleting a node having only one child
Case 3: Deleting a node having two child
Case 4: Deleting a root node
If BST has only one node, make root to point to nothing, Root=NULL
Otherwise, copy the node containing the largest element in the left (or the smallest
element in the right)to the node to be deleted.
Delete the copied node.
103