Handbook

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 51

CSC212: Algorithm : Handbook

Nii, Ike, Sam & Sem


2

Table of Contents

1 Introduction to Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1 Importance of Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Complexity Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1 Time Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 Space Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3 Sorting Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.1 Bubble Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2 Selection Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.4 Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4 Searching Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.1 Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.2 Linear Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5 Sieve of Eratosthenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.1 Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
6 Encryption and Decryption Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . 23
6.1 Encryption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
6.2 Decryption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
6.3 Key Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
6.4 Caesar Cipher Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
6.5 Single-Key Encryption Procedure . . . . . . . . . . . . . . . . . . . . . . . . 25
7 Sequential Storage Allocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
7.1 Sequential List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
7.2 Sequential Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
7.3 Sequential Queue (Circular) . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
8 Linked List Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
8.1 Insertion at the Beginning . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
8.2 Insertion at the End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.3 Deletion from the Beginning . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
8.4 Deletion from the End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
9 Doubly Linked List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
9.1 Doubly Linked List Node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
9.2 Insertion at the Beginning . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
10 Stack Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
10.1 C++ Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
11 Queue algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
12 Integration of functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
12.1 C++ Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
13 Simultaneous linear equations: Gaussian elimination . . . . . . . . . . . . . . . . . 44
13.1 C++ Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
14 Grammar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3

1 Introduction to Algorithms

In the world of computer science and problem-solving, algorithms play a crucial role. An algorithm
is a step-by-step procedure or a set of instructions designed to solve a specIfic problem or perForm a
particular task. It is the backbone of computer programs and Forms the foundation of computational
thinking.

Algorithms are used to solve a wide range of problems, from simple tasks like sorting a list to com-
plex challenges such as route planning, image processing, and machine learning. They are essential
in various domains, including computer science, mathematics, engineering, and data analysis.

Efficiency is a critical aspect of algorithms. It measures how well an algorithm perForms in terms
of time and space Requirements. Time complexity evaluates the amount of time an algorithm takes
to execute, While space complexity measures the amount of memory or space it Requires.

There are dIfferent approaches to designing algorithms, including brute Force, divide and conquer,
dynamic programming, and greedy algorithms, among others. Each approach has its advantages
and is suitable For solving specIfic types of problems.

In this document, we will explore various algorithms, their design principles, and their applications.
We will examine dIfferent algorithmic techniques, analyze their time and space complexity, and
provide practical examples and solutions.

By understanding algorithms and their underlying principles, we can develop efficient and opti-
mized solutions to problems, enhance program perFormance, and tackle complex computational
challenges.

Now, let’s delve into the fascinating world of algorithms and discover the power of problem-solving
through efficient algorithmic design.

1.1 Importance of Algorithms

Algorithms are the backbone of computer programs, driving everything from basic operations to
complex computations. They enable us to tackle problems efficiently and effectively, making them
essential in various domains, such as computer science, mathematics, engineering, and data analy-
sis.

Consider the task of sorting a list of numbers in ascending order. This seemingly simple task Re-
quires an algorithm such as Quicksort, Merge Sort, or Bubble Sort. Algorithms also find applications
in more intricate problems like route planning, image recognition, machine learning, and cryptog-
raphy.
4

2 Complexity Theory
Complexity theory is a branch of theoretical computer science that studies the inherent complexity
of computational problems. It aims to classIfy problems based on their computational dIfficulty and
identIfy the resources (such as time and space) Required to solve them.
In complexity theory, the two primary measures of complexity are time complexity and space com-
plexity:

2.1 Time Complexity

Time complexity is a measure of the amount of time Required by an algorithm to run as a function of
the input size. It gives an estimate of the number of operations or steps perFormed by an algorithm
as the input grows. Time complexity is usually expressed using the Big O notation (O), Big Omega
(Ω), and Big Theta (Θ), which provides an upper bound on the growth rate of the algorithm.
For example, an algorithm with a time complexity of O(n2 ) indicates that the algorithm’s running
time grows quadratically with the size of the input.
Here are some common time complexity classes along with their descriptions, examples, and prac-
tical questions:

Constant Time Complexity:O(1)

Description: The algorithm takes a constant amount of time to run, regardless of the input size.
Example: Accessing an element in an array by its index.
Practical Question: Write an algorithm to find the sum of the first and last element of an array.

Algorithm 1 SumOfFirstAndLastElement
Require: Array A of size n
Ensure: Sum of the first and last element
1: function SumOfFirstAndLastElement(A, n)
2: sum ← 0
3: if n < 2 then
4: return 0 ▷ Array should have at least two elements
5: end if
6: sum ← A[0] + A[n − 1]
7: return sum
8: end function
5

Linear Time Complexity:O(n)

Description: The algorithm’s running time grows linearly with the input size.
Example: Finding the maximum element in an unsorted array.
Practical Question: Write an algorithm to count the number of occurrences of a given element in an
array.

Algorithm 2 CountOccurrences
Require: Array A of size n, element x
Ensure: Number of occurrences of x in A
1: function CountOccurrences(A, n, x)
2: count ← 0 ▷ Initialize count as 0
3: for i from 1 to n do
4: if A[i] = x then
5: count ← count + 1
6: end if
7: end for
8: return count
9: end function

Logarithmic Time Complexity: O(logn)

Description: The algorithm’s running time increases logarithmically with the input size.
Example: Binary search in a sorted array.
Practical Question: Write an algorithm to find the position of the first occurrence of a given element
in a sorted array.

Algorithm 3 FindFirstOccurrence
Require: Sorted array A of size n, element x
Ensure: Position of the first occurrence of x in A (or -1 If not found)
1: function FindFirstOccurrence(A, n, x)
2: Initialize low as 0, high as n − 1, position as -1
3: while low ≤ high do
4: mid ← (low + high)/2
5: if A[mid] = x then
6: position ← mid
7: high ← mid − 1 ▷ Search For earlier occurrences
8: else if A[mid] < x then
9: low ← mid + 1
10: else
11: high ← mid − 1
12: end if
13: end while
14: return position
15: end function
6

Quadratic Time Complexity: O(n2 )

Description: The algorithm’s running time grows quadratically with the input size.
Example: Selection sort.
Practical Question: Write an algorithm to check If an array contains duplicate elements.

Algorithm 4 CheckDuplicateElements
Require: Array A of size n
Ensure: Boolean value indicating whether A contains duplicate elements
1: function CheckDuplicateElements(A, n)
2: Initialize an empty set S
3: for i from 1 to n do
4: if A[i] is in S then
5: return true ▷ Duplicate element found
6: else
7: Add A[i] to S
8: end if
9: end for
10: return false ▷ No duplicate elements found
11: end function

Exponential Time Complexity: O(2n )

Description: The algorithm’s running time grows exponentially with the input size.
Example: Solving the "Traveling Salesman Problem" using brute Force.
Practical Question: Write an algorithm to generate all subsets of a set.

Algorithm 5 GenerateSubsets
Require: Set S of size n
Ensure: List of all subsets of S
1: function GenerateSubsets(S, n)
2: Create an empty list subsets
3: Initialize an empty set currentSubset
4: GenerateSubsetsRecursive(S, 0, currentSubset, subsets, n)
5: return subsets
6: end function
7

Algorithm 6 GenerateSubsetsRecursive
Require: Set S of size n, current index currentIndex, current subset currentSubset, list of subsets
subsets
Ensure: List of all subsets of S in subsets
1: function GenerateSubsetsRecursive(S, currentIndex, currentSubset, subsets, n)
2: if currentIndex = n then
3: Add currentSubset to subsets
4: else
5: GenerateSubsetsRecursive(S, currentIndex + 1, currentSubset, subsets) ▷ Exclude
current element
6: Add S[currentIndex] to currentSubset
7: GenerateSubsetsRecursive(S, currentIndex + 1, currentSubset, subsets) ▷ Include
current element
8: Remove S[currentIndex] from currentSubset
9: end if
10: end function

Factorial Time Complexity: O(n!)

Description: The algorithm’s running time grows factorially with the input size.
Example: Solving the "Traveling Salesman Problem" using brute Force with dynamic programming.
Practical Question: Write a dynamic programming algorithm that uses memoization to solve smaller
subproblems and avoid redundant computations.

Algorithm 7 TSPDynamicProgramming
Require: Graph G with n cities
Ensure: Minimum cost of a Hamiltonian cycle in G
1: function TSPDynamicProgramming(G, n)
2: Initialize a memoization table dp with size 2n and set all entries to ∞
3: return TSPRecursion(0, 1, G, dp)
4: end function
8

Algorithm 8 TSPRecursion
Require: Current city current, bitmask visited, graph G, memoization table dp
Ensure: Minimum cost of a Hamiltonian cycle starting from current and visiting all unvisited cities
1: function TSPRecursion(current, visited, G, dp)
2: if visited = (2n − 1) then
3: return G[current][0] ▷ Return cost to Return to the starting city
4: end if
5: if dp[current][visited] < ∞ then
6: return dp[current][visited] ▷ Return the precomputed result
7: end if
8: minCost ← ∞
9: for each unvisited city i do
10: newCost ← G[current][i] + TSPRecursion(i, visited|(1 << i), G, dp)
11: minCost ← min(minCost, newCost)
12: end for
13: dp[current][visited] ← minCost ▷ Store the result in the memoization table
14: return minCost
15: end function

2.2 Space Complexity

Space complexity is a measure of the amount of memory or space Required by an algorithm to solve
a problem as a function of the input size. It estimates the maximum amount of memory used during
the execution of an algorithm. Space complexity can be expressed in terms of the total space used
or the space used by additional data structures.
Here are some common space complexity categories along with their descriptions, examples, and
practical questions:

Constant Space Complexity: O(1)

Description: The algorithm uses a constant amount of space regardless of the input size.
Example: Swapping two variables.
Practical Question: Write an algorithm to calculate the sum of two numbers using constant space.

Algorithm 9 SumOfTwoNumbers
Require: Two numbers, a and b
Ensure: Sum of the two numbers, sum
1: function SumOfTwoNumbers(a, b)
2: Set sum ← a + b
3: return sum
4: end function
9

Linear Space Complexity: O(n)

Description: The algorithm’s space usage grows linearly with the input size.
Example: Storing elements of an array in a separate data structure.
Practical Question: Write an algorithm to reverse an array using linear space.

Algorithm 10 ReverseArray
Require: Array A of size n
Ensure: Reversed array A
1: function ReverseArray(A, n)
2: Initialize an empty array B of size n
3: Set j ← n
4: for i ← 1 to n do
5: B[j] ← A[i]
6: j ←j−1
7: end for
8: Set A ← B
9: return A
10: end function

Quadratic Space Complexity: O(n2 )

Description: The algorithm’s space usage grows quadratically with the input size.
Example: Creating a matrix or two-dimensional array.
Practical Question: Write an algorithm to find all pairs of elements in an array that sum to a given
value using quadratic space.

Algorithm 11 FindPairsWithSum
Require: Array A of size n, target sum target
Ensure: List of all pairs in A that sum to target
1: function FindPairsWithSum(A, n, target)
2: Initialize an empty list pairs
3: for i ← 1 to n − 1 do
4: for j ← i + 1 to n do
5: if A[i] + A[j] = target then
6: Add pair (A[i], A[j]) to pairs
7: end if
8: end for
9: end for
10: return pairs
11: end function
10

Exponential Space Complexity: O(2n )

Description: The algorithm’s space usage grows exponentially with the input size.
Example: Generating all subsets or combinations of a set.
Practical Question: Write an algorithm to solve the subset sum problem using exponential space.

Algorithm 12 SubsetSum
Require: Array A of size n, target sum target
Ensure: Boolean value indicating whether a subset of A sums to target
1: function SubsetSum(A, n, target)
2: return SubsetSumRecursion(A, n, target)
3: end function
4:
5: function SubsetSumRecursion(A, n, target)
6: if target = 0 then
7: return true ▷ Subset with sum equal to target found
8: end if
9: if n = 0 then
10: return false ▷ Reached end of the array without finding a subset with the target sum
11: end if
12: return SubsetSumRecursion(A, n − 1, target) ▷ Exclude the current element or
SubsetSumRecursion(A, n − 1, target − A[n]) ▷ Include the current element
13: end function

Recursive Space Complexity: O(h)

Description: The algorithm’s space usage is proportional to the maximum depth of recursion.
Example: Recursive algorithms like depth-first search (DFS) or quicksort.
Practical Question: Write an algorithm to find the height of a binary tree using recursive space.

Algorithm 13 HeightOfBinaryTree
Require: Root node of a binary tree and Node node
Ensure: Height of the binary tree and Height of the subtree rooted at node
1: function HeightOfBinaryTree(root)
2: return CalculateHeight(root)
3: end function
4:
5: function CalculateHeight(node)
6: if node is null then
7: return 0 ▷ Base case: empty subtree has height 0
8: end if
9: lef tHeight ← CalculateHeight(node.left) ▷ Recursively calculate height of left subtree
10: rightHeight ← CalculateHeight(node.right) ▷ Recursively calculate height of right subtree
11: return max(lef tHeight, rightHeight) + 1 ▷ Height of the current node is the maximum
height of its subtrees plus 1
12: end function
11

Complexity theory provides a framework For understanding the intrinsic dIfficulty of computational
problems and plays a crucial role in algorithm design, optimization, cryptography, and the study of
computational intractability.
12

3 Sorting Algorithms
Sorting algorithms are fundamental in computer science and are used to arrange elements in a spe-
cIfic order, such as ascending or descending. There are numerous sorting algorithms, each with its
own characteristics, time complexity, and best-case or worst-case scenarios. Here, I will describe
four popular sorting algorithms: Bubble Sort, Selection Sort, Merge Sort and Insertion Sort.

3.1 Bubble Sort

Bubble Sort is a simple and intuitive algorithm that repeatedly compares adjacent elements and
swaps them If they are in the wrong order. The process continues until the entire array is sorted.

Algorithm 14 Bubble Sort


Require: Array A of size n
Ensure: Sorted array A
1: for i ← 0 to n − 1 do
2: for j ← 0 to n − 1 − i do
3: if A[j] > A[j + 1] then
4: Swap A[j] and A[j + 1]
5: end if
6: end for
7: end for

Bubble Sort has a time complexity of O(n2 ) in the average and worst cases, making it inefficient For
large arrays. However, it perForms well on nearly sorted arrays.
C++ Implementation
1 void bubbleSort(int arr[], int n) {
2 For (int i = 0; i < n-1; ++i) {
3 For (int j = 0; j < n-1-i; ++j) {
4 If (arr[j] > arr[j+1]) {
5 // Swap elements
6 int temp = arr[j];
7 arr[j] = arr[j+1];
8 arr[j+1] = temp;
9 }
10 }
11 }
12 }
13

3.2 Selection Sort

Selection Sort works by dividing the array into two sections: the sorted and the unsorted. It repeat-
edly selects the minimum element from the unsorted section and places it at the beginning of the
sorted section.

Algorithm 15 Selection Sort


Require: Array A of size n
Ensure: Sorted array A
1: for i ← 0 to n − 1 do
2: minIndex ← i
3: for j ← i + 1 to n − 1 do
4: if A[j] < A[minIndex] then
5: minIndex ← j
6: end if
7: end for
8: Swap A[i] and A[minIndex]
9: end for

Selection Sort also has a time complexity of O(n2 ) in the average and worst cases, making it ineffi-
cient For large arrays. It perForms a constant number of swaps, making it useful when the cost of
swapping elements is high.
C++ Implementation
1 void selectionSort(int arr[], int n) {
2 For (int i = 0; i < n-1; ++i) {
3 int minIndex = i;
4 For (int j = i+1; j < n; ++j) {
5 If (arr[j] < arr[minIndex]) {
6 minIndex = j;
7 }
8 }
9 // Swap elements
10 int temp = arr[i];
11 arr[i] = arr[minIndex];
12 arr[minIndex] = temp;
13 }
14 }
14

3.3 Merge Sort

Merge Sort is a divide-and-conquer algorithm that recursively divides the array into two halves,
sorts them individually, and then merges them back into a single sorted array.

Algorithm 16 Merge Sort


Require: Array A of size n
Ensure: Sorted array A
1: function MergeSort(A, n)
2: if n > 1 then
3: Divide A into two halves: lef tHalf , rightHalf ,
4: calculate lef tsize and rightsize
5: MergeSort(lef tHalf , lef tsize)
6: MergeSort(rightHalf , ,rightsize)
7: Merge(A, lef tHalf , rightHalf )
8: end if
9: end function

Algorithm 17 Merge
Require: Array A, sorted subarrays lef tHalf , rightHalf , lef tsize, rightsize
Ensure: Merged array A
1: function Merge(A, lef tHalf , rightHalf , lef tsize, rightsize)
2: i←0
3: j←0
4: k←0
5: while i < lef tsize and j < rightsize do
6: if leftHalf[i] ≤ rightHalf[j] then
7: A[k] ← leftHalf[i]
8: i←i+1
9: else
10: A[k] ← rightHalf[j]
11: j ←j+1
12: end if
13: k ←k+1
14: end while
15: while i < lef tsize do
16: A[k] ← leftHalf[i]
17: i←i+1
18: k ←k+1
19: end while
20: while j < rightsize do
21: A[k] ← rightHalf[j]
22: j ←j+1
23: k ←k+1
24: end while
25: end function

Merge Sort has a time complexity of O(n log n) in all cases. It perForms well even on large arrays
15

and is widely used in practice. However, it Requires additional space For merging, resulting in a
space complexity of O(n).
C++ Implementation
1 void merge(int arr[], int left[], int leftSize, int right[], int rightSize) {
2 int i = 0, j = 0, k = 0;
3 While (i < leftSize && j < rightSize) {
4 If (left[i] <= right[j]) {
5 arr[k] = left[i];
6 ++i;
7 } else {
8 arr[k] = right[j];
9 ++j;
10 }
11 ++k;
12 }
13
14 While (i < leftSize) {
15 arr[k] = left[i];
16 ++i;
17 ++k;
18 }
19
20 While (j < rightSize) {
21 arr[k] = right[j];
22 ++j;
23 ++k;
24 }
25 }
26
27 void mergeSort(int arr[], int n) {
28 If (n <= 1) {
29 Return;
30 }
31
32 int mid = n / 2;
33 int left[mid];
34 int right[n - mid];
35

36 // Split array into two halves


37 For (int i = 0; i < mid; ++i) {
38 left[i] = arr[i];
39 }
40 For (int i = mid; i < n; ++i) {
41 right[i - mid] = arr[i];
42 }
43
44 // Recursively sort the two halves
45 mergeSort(left, mid);
46 mergeSort(right, n - mid);
47

48 // Merge the sorted halves


49 merge(arr, left, mid, right, n - mid);
50 }
16

3.4 Insertion Sort

Insertion Sort is an efficient comparison-based sorting algorithm that builds the final sorted array
one element at a time. It works by dividing the array into two sections: the sorted section and
the unsorted section. It iterates through the unsorted section, comparing each element with the
elements in the sorted section and inserting it at the appropriate position to maintain the sorted
order.

Algorithm 18 Insertion Sort


Require: Array A of size n
Ensure: Sorted array A
1: function InsertionSort(A, n)
2: for i ← 1 to n − 1 do
3: key ← A[i]
4: j ←i−1
5: while j ≥ 0 and A[j] > key do
6: A[j + 1] ← A[j]
7: j ←j−1
8: end while
9: A[j + 1] ← key
10: end for
11: end function

Insertion Sort has a time complexity of O(n2 ) in the worst case. However, it perForms efficiently
For small arrays or partially sorted arrays, where the number of inversions is small.
Insertion Sort is an in-place sorting algorithm that Requires no additional space. It is also considered
stable since it preserves the relative order of elements with equal values.
Note: It is important to note that While Insertion Sort is efficient For small or partially sorted arrays,
other sorting algorithms such as Merge Sort or Quick Sort are often preferred For larger arrays due
to their better average and worst-case time complexity.
C++ Implementation
1 void insertionSort(int arr[], int n) {
2 For (int i = 1; i < n; ++i) {
3 int key = arr[i];
4 int j = i - 1;
5
6 While (j >= 0 && arr[j] > key) {
7 arr[j + 1] = arr[j];
8 --j;
9 }
10
11 arr[j + 1] = key;
12 }
13 }

These are just a few examples of sorting algorithms, each with its own trade-offs in terms of time
complexity, space complexity, and perFormance characteristics. Choosing the right sorting algo-
rithm depends on the specIfic Requirements and constraints of your problem.
17

4 Searching Algorithms

A searching algorithm is a method or procedure used to locate a specIfic item or element within a
collection of data. The goal of a searching algorithm is to determine whether a particular element
exists in the given data set and, If so, its position or some other relevant inFormation associated with
it.
Searching algorithms are commonly used in various applications, such as inFormation retrieval sys-
tems, databases, sorting algorithms, and more. The efficiency of a searching algorithm is often mea-
sured in terms of time complexity, which indicates how the algorithm’s perFormance scales with
the size of the input data.
There are numerous searching algorithms available, each with its own characteristics and suitable
applications. Some popular searching algorithms include linear search, binary search, hashing, jump
search, interpolation search, exponential search, and more. The choice of algorithm depends on
factors such as the size and characteristics of the data, the availability of a sorted or indexed structure,
and the specIfic Requirements of the search operation, such as time and space constraints.

4.1 Binary Search

Binary search is a widely used searching algorithm that efficiently locates a target element within
a sorted array or list. It repeatedly divides the search space in half, comparing the target element
with the middle element of the remaining range and discarding one half of the search space at each
step.
Here’s a step-by-step explanation of the binary search algorithm:

1. Begin with a sorted array or list.


2. Set the lower bound low to the first index of the array and the upper bound high to the last
index.
3. While the lower bound is less than or equal to the upper bound:
• Compute the middle index mid as (low + high) / 2.
• Compare the target element with the element at the middle index:
– If they are equal, the search is successful, and the middle index is Returned.
– If the target is less than the middle element, update the upper bound high to mid -
1.
– If the target is greater than the middle element, update the lower bound low to mid
+ 1.
4. If the loop terminates without finding the target element, it means the element is not present
in the array, and the algorithm Returns a "not found" indication.

Binary search offers a signIficant advantage over linear search, particularly For large sorted datasets,
as it reduces the search space by half at each step. This results in a signIficantly lower number of
comparisons, leading to a faster search. The time complexity of binary search is O(log n), where n is
the number of elements in the array. This logarithmic time complexity makes binary search highly
efficient, especially compared to linear search, which has a time complexity of OO(n).
18

However, it’s important to note that binary search Requires a sorted array as input. If the array is
unsorted, the algorithm won’t produce correct results. ThereFore, it is essential to either sort the
array first or choose a dIfferent searching algorithm If the data is not sorted.
Overall, binary search is a powerful and efficient searching algorithm that is commonly used when
dealing with sorted arrays or lists. Its ability to quickly narrow down the search space makes it a
fundamental technique in computer science and a building block For various other algorithms and
data structures.

Algorithm 19 Binary Search


Require: Sorted array arr of size n, target element target
Ensure: Index of the target element in the array or "Element not found."
1: function BinarySearch(arr, target)
2: Set lower to the first index of the array.
3: Set upper to the last index of the array.
4: while lower ≤ upper do
5: Compute the middle index as (lower + upper)/2.
6: if arr[middle] = target then
7: return middle ▷ Target element found at middle index.
8: else if arr[middle] < target then
9: Set lower to middle + 1 ▷ Search in the right half.
10: else
11: Set upper to middle − 1 ▷ Search in the left half.
12: end if
13: end while
14: return "Element not found." ▷ Target element not present in the array.
15: end function
19

Implementation in C++
1 #include <iostream>
2 using namespace std;
3
4 int binarySearch(int arr[], int size, int target) {
5 int lower = 0;
6 int upper = size - 1;
7
8 While (lower <= upper) {
9 int middle = (lower + upper) / 2;
10
11 If (arr[middle] == target)
12 Return middle;
13
14 If (arr[middle] < target)
15 lower = middle + 1;
16 else
17 upper = middle - 1;
18 }
19

20 Return -1; // Element not found


21 }
22
23 int main() {
24 int arr[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
25 int size = sizeof(arr) / sizeof(arr[0]);
26 int target = 23;
27
28 int result = binarySearch(arr, size, target);
29
30 If (result == -1)
31 cout << "Element not found." << endl;
32 else
33 cout << "Element found at index: " << result << endl;
34
35 Return 0;
36 }

4.2 Linear Search

Linear search, also known as sequential search, is a straightForward searching algorithm that se-
quentially checks each element in a list or array until a match is found or the end of the list is
reached. It is applicable to both sorted and unsorted data.
Here’s a step-by-step explanation of the linear search algorithm:
1. Begin with an array or list of elements.
2. Start from the first element of the array.
3. Compare the current element with the target element being searched For:
• If they are equal, the search is successful, and the current position (index) is Returned.
• If they are not equal, move to the next element in the array.
4. Repeat step 3 until either a match is found or the end of the array is reached.
20

5. If the loop terminates without finding a match, it means the target element is not present in
the array, and the algorithm Returns a "not found" indication.
Linear search examines each element one by one until a match is found or the entire list has been
traversed. It has a worst-case time complexity of O(n), where n is the number of elements in the
array. This means that in the worst case, the algorithm may need to compare the target element
with all elements in the array.
Linear search is simple to understand and implement, and it works on both sorted and unsorted
data. However, it is not as efficient as binary search or other advanced searching algorithms for
large datasets. Linear search is typically used when the data is small or unsorted, or when the
position of the element in the list is important.

Algorithm 20 Linear Search


Require: Array arr of size size, target element target
Ensure: Index of the target element in the array (or "Element not found.")
1: function LinearSearch(arr, size, target)
2: for i ← 0 to size − 1 do
3: if arr[i] = target then
4: return i
5: end if
6: end for
7: return "Element not found."
8: end function

Implementation in C++
1 #include <iostream>
2 using namespace std;
3
4 int linearSearch(int arr[], int size, int target) {
5 For (int i = 0; i < size; i++) {
6 If (arr[i] == target)
7 Return i;
8 }
9
10 Return -1; // Element not found
11 }
12

13 int main() {
14 int arr[] = {10, 4, 6, 8, 2, 7, 3, 5, 9};
15 int size = sizeof(arr) / sizeof(arr[0]);
16 int target = 7;
17
18 int result = linearSearch(arr, size, target);
19
20 If (result == -1)
21 cout << "Element not found." << endl;
22 else
23 cout << "Element found at index: " << result << endl;
24

25 Return 0;
26 }
21

5 Sieve of Eratosthenes
The Sieve of Eratosthenes is an ancient and efficient algorithm For finding all prime numbers up to a
given limit. It eliminates multiples of each prime number to gradually sieve out non-prime numbers,
leaving only the prime numbers behind.

5.1 Algorithm

The Sieve of Eratosthenes algorithm can be described as follows:

1. Create a boolean array of size n + 1, where n is the given limit. Initially, all elements are set
to true to indicate that they are potentially prime numbers.
2. Start with the first prime number, which is 2.
3. Mark all multiples of 2 (excluding 2 itself) as false in the boolean array, as they are not
prime.
4. Find the next prime number that is not marked as false in the array. In this case, it is 3.
5. Mark all multiples of 3 (excluding 3 itself) as false in the boolean array.
6. Repeat steps 4 and 5 until the square of the current prime number is greater than n. At this
point, all non-prime numbers have been sieved out.
7. The numbers that are still marked as true in the boolean array are the prime numbers.

The Sieve of Eratosthenes algorithm works by iteratively sieving out the multiples of each prime
number. By eliminating non-prime numbers efficiently, it drastically reduces the number of opera-
tions Required to determine all prime numbers within a given range.
The time complexity of the Sieve of Eratosthenes algorithm is approximately O(n log log n), where n
is the given limit. This makes it much faster than other naive methods of prime number generation,
such as checking divisibility For each number individually.
22

Algorithm 21 Sieve of Eratosthenes


Require: Integer n ≥ 2
Ensure: List of prime numbers up to n
1: function FindPrimes(n)
2: Create a boolean array isPrime[0..n], initialized to true.
3: Set isPrime[0] √and isPrime[1] to false (0 and 1 are not prime).
4: for i = 2 to n do
5: if isPrime[i] is true then
6: for j = i2 to n step i do
7: Set isPrime[j] to false.
8: end for
9: end if
10: end for
11: Create an empty list primes.
12: for i = 2 to n do
13: if isPrime[i] is true then
14: Add i to primes.
15: end if
16: end for
17: return primes.
18: end function

Implementation in C++
1 #include <iostream>
2 #include <vector>
3 #include <algorithm>
4 using namespace std;
5
6 vector<int> sieveOfEratosthenes(int n) {
7 vector<bool> isPrime(n + 1, true);
8 isPrime[0] = isPrime[1] = false;
9
10 for (int i = 2; i * i <= n; i++) {
11 If (isPrime[i]) {
12 For (int j = i * i; j <= n; j += i)
13 isPrime[j] = false;
14 }
15 }
16 vector<int> primes;
17 for (int i = 2; i <= n; i++) {
18 If (isPrime[i])
19 primes.push_back(i);
20 }
21 return primes;
22 }
23
24 int main() {
25 int n = 30;
26 vector<int> primes = sieveOfEratosthenes(n);
27 cout << "Prime numbers up to " << n << ":" << endl;
28 for (int prime : primes)
29 cout << prime << " ";
30 cout << endl;
31 return 0;
32 }
23

6 Encryption and Decryption Algorithms


Encryption and decryption algorithms are fundamental components of cryptographic systems, pro-
viding a means to secure and protect sensitive inFormation. These algorithms play a vital role in
ensuring the confidentiality and integrity of data by transForming plaintext into ciphertext (encryp-
tion) and vice versa (decryption).

6.1 Encryption

Encryption is the process of converting plaintext into ciphertext using an encryption algorithm and
a secret key. The encryption algorithm takes the plaintext and the encryption key as input and
produces the corresponding ciphertext. The key serves as a parameter to customize the encryption
process and determine the relationship between the plaintext and ciphertext. The resulting cipher-
text appears random and is designed to be dIfficult to understand or decipher without the correct
decryption key.
The encryption process provides confidentiality by preventing unauthorized individuals from read-
ing or understanding the encrypted data. It Ensures that even If the ciphertext is intercepted, it
remains indecipherable without the proper decryption key. Encryption algorithms can range from
simple substitution ciphers like the Caesar cipher to complex mathematical operations used in mod-
ern symmetric and asymmetric encryption algorithms.

6.2 Decryption

Decryption is the process of converting ciphertext back into plaintext using a decryption algorithm
and the correct decryption key. The decryption algorithm takes the ciphertext and the decryption
key as input and produces the corresponding plaintext. It reverses the encryption process by apply-
ing mathematical operations to the ciphertext using the decryption key.
The decryption key must be kept secret and known only to authorized parties. It is used to reverse
the encryption process and recover the original plaintext. By using the correct decryption key,
authorized individuals can decipher the message and understand its contents.
Decryption enables authorized parties to securely access and comprehend the original inFormation.
It is an essential component of cryptographic systems, allowing For the secure transmission and
storage of sensitive data.

6.3 Key Points

• Encryption and decryption algorithms are paired together, using the same algorithm but with
dIfferent keys.
• Symmetric-key algorithms use the same key For both encryption and decryption, While asymmetric-
key algorithms use dIfferent keys.
• Symmetric encryption provides faster encryption/decryption but Requires securely sharing
the key between communicating parties.
• Asymmetric encryption provides enhanced security by using a pair of public and private keys,
eliminating the need to share a secret key.
24

Encryption and decryption algorithms Form the foundation of modern cryptography, and their de-
sign and implementation are critical to the security of sensitive inFormation. These algorithms must
be carefully developed, tested, and used in a manner that Ensures the confidentiality and integrity
of data.

6.4 Caesar Cipher Algorithm

The Caesar cipher, also known as the shIft cipher, is a simple substitution cipher that operates by
shIfting each letter in the plaintext a certain number of positions down or up the alphabet to create
the ciphertext.

Encryption

To encrypt a message using the Caesar cipher:


• Choose a shIft value (key) that determines the number of positions each letter will be shIfted.
• Take the plaintext message and shIft each letter by the specIfied number of positions.
• Wrap around the alphabet, so If a letter reaches the end, it starts again from the beginning.
• Preserve any non-alphabetic characters (such as spaces or punctuation) as they are.
For example, with a shIft of 3, ’A’ would be encrypted as ’D’, ’B’ as ’E’, and so on. The alphabet
wraps around, so ’Z’ would be encrypted as ’C’.

Decryption

To decrypt a message encrypted with the Caesar cipher, the same shIft value (key) is used, but in
reverse. Each letter in the ciphertext is shIfted back by the specIfied amount to retrieve the original
plaintext. The decryption process is essentially the same as the encryption process, but with the
inverse shIft.

Example

Let’s take an example to illustrate the Caesar cipher with a shIft of 6:


Plaintext: ALGORITHM
Ciphertext: GRMUXOZNS
Here, each letter in the plaintext is shIfted three positions to the right to obtain the ciphertext.

Implementation

The Caesar cipher algorithm can be implemented in various programming languages, including C++,
Python, or Java. The implementation involves mapping each letter to its corresponding shIfted letter
using the shIft value.
Note: The Caesar cipher is a very basic encryption technique and is easily broken by trying all
possible shIft values. It is often used as an introductory example to demonstrate the concept of
substitution ciphers.
25

6.5 Single-Key Encryption Procedure

In this encryption procedure, we assume the existence of a character table consisting of alphabets A
to Z and a space mark separator, totaling 27 characters. Each character is associated with a number
code.

Character Table and Codes

Alphabet Code Alphabet Code


Space 0
A 1 N 14
B 2 O 15
C 3 P 16
D 4 Q 17
E 5 R 18
F 6 S 19
G 7 T 20
H 8 U 21
I 9 V 22
J 10 W 23
K 11 X 24
L 12 Y 25
M 13 Z 26

Encryption Procedure

To encrypt a communication using the single-key encryption procedure:

Algorithm 22 Single-Key Encryption


Require: Message M , Key K (both of length n)
Ensure: Encrypted message E of length n
1: function Encrypt(M, K, n)
2: Initialize an empty string E
3: for i from 0 to n do
4: t ← (M [i] + K[i]) mod 27
5: E[i] ← t
6: end for
7: return E
8: end function

The encryption procedure iterates through each element of the communication string M . It adds
the corresponding key value from the key string K to the communication value, and If the sum is
greater than or equal to 27, it wraps around by subtracting 27. The result is stored in the encrypted
string E.
26

Implementation in C++
1 #include <iostream>
2 #include <string>
3

4 using namespace std;


5
6 string encrypt(const string& message, const string& key) {
7 string encryptedMessage;
8 int messageLength = message.length();
9

10 for (int i = 0; i < messageLength; i++) {


11 // Convert the current character to its corresponding number
12 int messageValue;
13 if (message[i] == ’ ’) {
14 messageValue = 0;
15 } else {
16 messageValue = message[i] - ’A’ + 1;
17 }
18
19 // Get the corresponding character from the key
20 int keyValue = key[i] - ’A’ + 1;
21

22 // Perform the encryption


23 int encryptedValue = (messageValue + keyValue) % 27;
24
25 // Convert the encrypted value back to the corresponding character
26 char encryptedChar;
27 if (encryptedValue == 0) {
28 encryptedChar = ’ ’;
29 } else {
30 encryptedChar = encryptedValue + ’A’ - 1;
31 }
32
33 // Append the encrypted character to the encrypted message
34 encryptedMessage += encryptedChar;
35 }
36
37 return encryptedMessage;
38 }
39

40 int main() {
41 string message = "HELLO WORLD";
42 string key = "KEYKEYKEYKE";
43 string encryptedMessage = encrypt(message, key);
44
45 cout << "Encrypted Message: " << encryptedMessage << endl;
46
47 return 0;
48 }
49
27

Decryption Procedure

Algorithm 23 Single-Key Decryption


Require: Encrypted message E, Key K (both of length n)
Ensure: Decrypted message M of length n
1: function Decrypt(E, K, n)
2: Initialize an empty string M
3: for i from 0 to n do
4: t ← (E[i] − K[i] + 27) mod 27
5: M [i] ← t
6: end for
7: return M
8: end function

The decryption algorithm is similar to the encryption algorithm, but it subtracts the corresponding
key value from the encrypted value and adds 27 before taking the modulo 27 to handle wrapping
around. The result is stored in the decrypted message string M .
28

Implementation in C++
1 #include <iostream>
2 #include <string>
3

4 using namespace std;


5
6 string decrypt(const string& encryptedMessage, const string& key) {
7 string decryptedMessage;
8 int messageLength = encryptedMessage.length();
9

10 for (int i = 0; i < messageLength; i++) {


11 // Convert the current character to its corresponding number
12 int encryptedValue;
13 if (encryptedMessage[i] == ’ ’) {
14 encryptedValue = 0;
15 } else {
16 encryptedValue = encryptedMessage[i] - ’A’ + 1;
17 }
18
19 // Get the corresponding character from the key
20 int keyValue = key[i] - ’A’ + 1;
21

22 // Perform the decryption


23 int decryptedValue = (encryptedValue - keyValue + 27) % 27;
24
25 // Convert the decrypted value back to the corresponding character
26 char decryptedChar;
27 if (decryptedValue == 0) {
28 decryptedChar = ’ ’;
29 } else {
30 decryptedChar = decryptedValue + ’A’ - 1;
31 }
32
33 // Append the decrypted character to the decrypted message
34 decryptedMessage += decryptedChar;
35 }
36
37 return decryptedMessage;
38 }
39

40 int main() {
41 string encryptedMessage = "SJJWTYGTPWI";
42 string key = "KEYKEYKEYKE";
43 string decryptedMessage = decrypt(encryptedMessage, key);
44
45 cout << "Decrypted Message: " << decryptedMessage << endl;
46
47 return 0;
48 }

Please note that this encryption and decryption procedures are basic examples for educational pur-
poses. In real-world scenarios, it is crucial to use robust encryption algorithms and follow best
practices for secure communication.
29

7 Sequential Storage Allocation

7.1 Sequential List

A sequential list is a linear data structure that stores a collection of items in a specific order. It allows
efficient insertion, deletion, and retrieval of items.
Example
Let’s consider a sequential list of integers. Initially, the list is empty.

List: []

We can perform various operations on the list:

• Insertion: Insert an item at a specified position. For example, inserting the value 5 at index
0.
Insert(0, 5): [5]
• Deletion: Remove an item from a specified position. For example, deleting the value at index
0.
Delete(0): []
• Search: Search for an item and return its position. For example, searching for the value 8.
Search(8): Not found
• Access: Retrieve the item at a specified position. For example, accessing the value at index 0.
Access(0): Error - Index out of range
• Size: Return the number of items in the list. For example,
Size(): 0
• IsEmpty: Check if the list is empty. For example,
IsEmpty(): True

These operations can be further extended based on the requirements and implementation details.

7.2 Sequential Stack

A sequential stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. It
allows efficient insertion and deletion of items from one end, known as the top of the stack.
Example
Let’s consider a sequential stack of characters. Initially, the stack is empty.

Stack: []

We can perform various operations on the stack:

• Push: Insert an item onto the top of the stack. For example, pushing the characters ’A’, ’B’,
and ’C’ onto the stack.
30

Push(’A’): [’A’]
Push(’B’): [’A’, ’B’]
Push(’C’): [’A’, ’B’, ’C’]
• Pop: Remove the item from the top of the stack. For example, popping an item from the stack.
Pop(): [’A’, ’B’]
• Top: Retrieve the item at the top of the stack without removing it. For example,
Top(): ’B’
• IsEmpty: Check if the stack is empty. For example,
IsEmpty(): False

7.3 Sequential Queue (Circular)

A sequential queue is a linear data structure that follows the First-In-First-Out (FIFO) principle. It
allows efficient insertion of items at the rear and removal of items from the front. In a circular
sequential queue, the front and rear wrap around to the beginning when they reach the end of the
queue, creating a circular structure.
Example
Let’s consider a circular sequential queue of integers with a maximum capacity of 5. Initially, the
queue is empty.

Queue: []

We can perform various operations on the queue:

• Enqueue: Insert an item at the rear of the queue. For example, enqueueing the integers 10,
20, 30, and 40.
Enqueue(10): [10]
Enqueue(20): [10, 20]
Enqueue(30): [10, 20, 30]
Enqueue(40): [10, 20, 30, 40]
• Dequeue: Remove the item from the front of the queue. For example, dequeuing an item from
the queue.
Dequeue(): [20, 30, 40]
• Front: Retrieve the item at the front of the queue without removing it. For example,
Front(): 20
• IsEmpty: Check if the queue is empty. For example,
IsEmpty(): False
• IsFull: Check if the queue is full. For example,
IsFull(): False
31

8 Linked List Algorithm


A linked list is a data structure that consists of nodes connected together through pointers. Each
node contains data and a pointer that points to the next node in the sequence.

8.1 Insertion at the Beginning

The insertion at the beginning algorithm creates a new node with the desired data. It sets the ‘next‘
pointer of the new node to the current head of the list and updates the head pointer to point to the
new node.

Algorithm 24 Insertion at the Beginning of a Linked List


1: FunctionInsertAtBeginningNode head, Data data
2: Create a new node newN ode with data
3: Set newN ode.next to head
4: Set head to newN ode
5: EndFunction

Implementation in C++
1 #include <iostream>
2
3 struct Node {
4 int data;
5 Node* next;
6 };
7
8 void InsertAtBeginning(Node*& head, int data) {
9 Node* newNode = new Node;
10 newNode->data = data;
11 newNode->next = head;
12 head = newNode;
13 }
14
15 int main() {
16 // Creating a sample linked list
17 Node* head = nullptr;
18

19 // Inserting elements at the beginning


20 InsertAtBeginning(head, 5);
21 InsertAtBeginning(head, 10);
22 InsertAtBeginning(head, 15);
23
24 // Printing the linked list
25 Node* current = head;
26 while (current != nullptr) {
27 std::cout << current->data << " ";
28 current = current->next;
29 }
30

31 return 0;
32 }
32

8.2 Insertion at the End

The insertion at the end algorithm also creates a new node with the desired data. If the list is empty
(head is null), it sets the head pointer to the new node. Otherwise, it traverses the list starting from
the head until reaching the last node. Then, it sets the ‘next‘ pointer of the last node to the new
node, making it the new last node.

Algorithm 25 Insertion at the End of a Linked List


Require: head: Node representing the head of the linked list
Require: data: Data to be inserted
Ensure: Linked list with data inserted at the end
1: function InsertAtEnd(head, data)
2: Create a new node newN ode with data
3: if head is null then
4: Set head to newN ode
5: else
6: Set temp to head
7: while temp.next is not null do
8: Set temp to temp.next
9: end while
10: Set temp.next to newN ode
11: end if
12: end function
33

Implementation in C++
1 #include <iostream>
2
3 struct Node {
4 int data;
5 Node* next;
6 };
7
8 void InsertAtEnd(Node*& head, int data) {
9 Node* newNode = new Node();
10 newNode->data = data;
11 newNode->next = nullptr;
12
13 if (head == nullptr) {
14 head = newNode;
15 } else {
16 Node* temp = head;
17 while (temp->next != nullptr) {
18 temp = temp->next;
19 }
20 temp->next = newNode;
21 }
22 }
23
24 void PrintList(Node* head) {
25 Node* temp = head;
26 while (temp != nullptr) {
27 std::cout << temp->data << " ";
28 temp = temp->next;
29 }
30 std::cout << std::endl;
31 }
32
33 int main() {
34 Node* head = nullptr;
35
36 // Insert elements at the end
37 InsertAtEnd(head, 67);
38 InsertAtEnd(head, 8);
39 InsertAtEnd(head, 76);
40 InsertAtEnd(head, 9);
41
42 // Print the linked list
43 std::cout << "Linked List: ";
44 PrintList(head);
45

46 return 0;
47 }
48

These insertion algorithms allow adding new elements to the linked list at the desired position.
34

8.3 Deletion from the Beginning

The deletion from the beginning algorithm checks If the head pointer is null. If it is, it Returns null
indicating that the list is empty. Otherwise, it sets a temporary variable ‘temp‘ to the head node,
updates the head pointer to point to the next node, and frees the memory occupied by the removed
node.

Algorithm 26 Deletion from the Beginning of a Linked List


Require: head is the reference to the head node of a linked list
Ensure: The first node of the linked list is deleted
1: function DeleteFromBeginning( head)
2: if head is null then
3: Return null
4: end if
5: Set temp to head
6: Set head to head.next
7: clear temp
8: end function
35

Implementation in C++
1 #include <iostream>
2
3 using namespace std;
4
5 struct Node {
6 int data;
7 Node* next;
8 };
9

10 void DeleteFromBeginning(Node*& head) {


11 if (head == nullptr) {
12 return;
13 }
14
15 Node* temp = head;
16 head = head->next;
17 delete temp;
18 }
19
20 int main() {
21 // Create a sample linked list with nodes 1, 2, 3
22 Node* head = new Node();
23 head->data = 1;
24
25 Node* second = new Node();
26 second->data = 2;
27

28 Node* third = new Node();


29 third->data = 3;
30
31 head->next = second;
32 second->next = third;
33 third->next = nullptr;
34
35 cout << "Linked List before deletion: ";
36 Node* current = head;
37 while (current != nullptr) {
38 cout << current->data << " ";
39 current = current->next;
40 }
41 cout << endl;
42
43 DeleteFromBeginning(head);
44
45 cout << "Linked List after deletion: ";
46 current = head;
47 while (current != nullptr) {
48 cout << current->data << " ";
49 current = current->next;
50 }
51 cout << endl;
52
53 return 0;
54 }
36

8.4 Deletion from the End

The deletion from the end algorithm checks If the head pointer is null. If it is, it Returns null indicat-
ing that the list is empty. If the list has only one node, it frees the memory occupied by the head node
and sets the head pointer to null. Otherwise, it initializes a variable ‘prev‘ to null and a temporary
variable ‘temp‘ to the head node. It traverses the list until reaching the second-to-last node and then
updates the ‘next‘ pointer of the second-to-last node to null. Finally, it frees the memory occupied
by the last node.

Algorithm 27 Deletion from the End of a Linked List


Require: Head node of the linked list: head
Ensure: Linked list with the last node deleted
1: function DeleteFromEnd(head)
2: if head is null then
3: Return null
4: end if
5: if head.next is null then
6: clear head
7: Set head to null
8: Return
9: end if
10: Set prev to null
11: Set temp to head
12: while temp.next is not null do
13: Set prev to temp
14: Set temp to temp.next
15: end while
16: Set prev.next to null
17: clear temp
18: end function

These deletion algorithms allow removing elements from the linked list at the desired position.
37

Implementation in C++
1 #include <iostream>
2
3 using namespace std;
4
5 struct Node {
6 int data;
7 Node* next;
8 };
9

10 void DeleteFromEnd(Node*& head) {


11 if (head == nullptr) {
12 return;
13 }
14
15 if (head->next == nullptr) {
16 delete head;
17 head = nullptr;
18 return;
19 }
20
21 Node* prev = nullptr;
22 Node* temp = head;
23
24 while (temp->next != nullptr) {
25 prev = temp;
26 temp = temp->next;
27 }
28
29 prev->next = nullptr;
30 delete temp;
31 }
32
33 int main() {
34 // Create a linked list: 1 -> 2 -> 3 -> nullptr
35 Node* head = new Node();
36 head->data = 1;
37
38 Node* second = new Node();
39 second->data = 2;
40 head->next = second;
41
42 Node* third = new Node();
43 third->data = 3;
44 second->next = third;
45 third->next = nullptr;
46
47 // Delete the last node from the linked list
48 DeleteFromEnd(head);
49
50 // Print the updated linked list
51 Node* current = head;
52 while (current != nullptr) {
53 cout << current->data << " ";
54 current = current->next;
55 }
56
57 return 0;
58 }
38

9 Doubly Linked List


A Doubly Linked List is a type of data structure in which each node contains two pointers: one point-
ing to the previous node and another pointing to the next node. This allows for efficient traversal
in both directions.

9.1 Doubly Linked List Node

Algorithm 28 Doubly Linked List Node


1: struct Node
2: data: Data stored in the node
3: prev: Pointer to the previous node
4: next: Pointer to the next node

9.2 Insertion at the Beginning

Algorithm 29 Insertion at the Beginning of a Doubly Linked List


1: function InsertAtBeginning(head, data)
2: Create a new node newN ode with data
3: Set newN ode.next to head
4: Set newN ode.prev to null
5: if head is not null then
6: Set head.prev to newN ode
7: end if
8: Set head to newN ode
9: end function
39

10 Stack Algorithm
A stack is a linear data structure that follows the Last-In-First-Out (LIfO) principle. The stack sup-
ports two main operations: push and pop. The push operation adds an element to the top of the
stack, While the pop operation removes and Returns the topmost element from the stack.
The stack can be implemented using an array or a linked list. In this example, we will use a linked
list to implement the stack.

Algorithm 30 Stack Algorithm


1: Structure Node
2: Data data
3: Node next
4: EndStructure
5:
6: Structure Stack
7: Node top
8: EndStructure
9:
10: Function Push(Stack stack, Data data)
11: Create a new node newN ode with data
12: Set newN ode.next to stack.top
13: Set stack.top to newN ode
14: EndFunction
15:
16: Function Pop(Stack stack)
17: if stack.top is null then
18: Return null ▷ Stack is empty
19: end if
20: Set poppedN ode to stack.top
21: Set stack.top to stack.top.next
22: Return poppedN ode.data
23: EndFunction
40

10.1 C++ Implementation


1 #include <iostream>
2

3 // Structure For a single node in the stack


4 struct Node {
5 int data;
6 Node* next;
7 };
8

9 // Structure For the stack


10 struct Stack {
11 Node* top;
12 };
13
14 // Function to create a new node
15 Node* createNode(int data) {
16 Node* newNode = new Node;
17 newNode->data = data;
18 newNode->next = nullptr;
19 Return newNode;
20 }
21
22 // Function to push an element to the stack
23 void Push(Stack& stack, int data) {
24 Node* newNode = createNode(data);
25 newNode->next = stack.top;
26 stack.top = newNode;
27 std::cout << "Pushed element: " << data << std::endl;
28 }
29
30 // Function to pop an element from the stack
31 int Pop(Stack& stack) {
32 If (stack.top == nullptr) {
33 std::cout << "Stack is empty" << std::endl;
34 Return -1;
35 }
36
37 Node* poppedNode = stack.top;
38 int poppedData = poppedNode->data;
39 stack.top = stack.top->next;
40 delete poppedNode;
41 Return poppedData;
42 }
43

44 int main() {
45 Stack stack;
46 stack.top = nullptr;
47
48 Push(stack, 5);
49 Push(stack, 10);
50 Push(stack, 15);
51
52 int poppedElement = Pop(stack);
53 If (poppedElement != -1)
54 std::cout << "Popped element: " << poppedElement << std::endl;
55

56 Return 0;
57 }
58
41

11 Queue algorithm
A queue follows the First-In-First-Out (FIFO) principle, meaning that the item added first will be the
first one to be removed.

Algorithm 31 Queue Algorithm


Require: queue: A valid queue and item: The item to be enqueued
Ensure: The item is added to the rear of the queue
1: procedure Enqueue(queue, item)
2: Create a new node newN ode and set its value to item
3: if queue is empty then
4: Set both the f ront and rear pointers to newN ode
5: else
6: Set the next pointer of rear to newN ode
7: Set rear to newN ode
8: end if
9: end procedure
10:
Require: queue: A valid queue
Ensure: The item at the front of the queue is removed and returned
11: procedure Dequeue(queue)
12: if queue is empty then
13: Throw an error: "Queue is empty"
14: end if
15: Set item to the value of f ront
16: Set f ront to the next node
17: if f ront is null then
18: Set rear to null
19: end if
20: return item
21: end procedure
22: function Front(queue)
Require: queue: A valid queue
Ensure: The value of the item at the front of the queue is returned
23: if queue is empty then
24: Throw an error: "Queue is empty"
25: end if
26: return the value of f ront
27: end function
28: function IsEmpty(queue)
Require: queue: A valid queue
Ensure: Returns true if the queue is empty, false otherwise
29: return true if queue is empty, false otherwise
30: end function
31: function Size(queue)
Require: queue: A valid queue
Ensure: Returns the number of items in the queue
32: return the number of nodes in queue
33: end function
42

12 Integration of functions
Integration is a fundamental operation in calculus that calculates the area under a curve. There
are several algorithms and techniques For approximating integrals, such as the trapezoidal rule,
Simpson’s rule, and numerical integration methods like the midpoint rule or the Euler method.
Below is the algorithm For numerical integration using the trapezoidal rule:

Algorithm 32 Trapezoidal Rule Integration


Require: f unc: the function to be integrated, a: lower bound, b: upper bound, n: number of subin-
tervals
Ensure: The approximate integral value
1: Function integrate(func, a, b, n)
2: h = b−a
n
3: sum = f unc(a)+f
2
unc(b)

4: For i = 1 to n − 1 do
5: x=a+i·h
6: sum = sum + f unc(x)
7: end For
8: result = h · sum
9: Return result
10: EndFunction

This algorithm takes as input the function to be integrated (func), the lower limit of integration (a),
the upper limit of integration (b), and the number of subdivisions (n). It calculates the width of each
subdivision (h), initialises the sum variable with the average of the function values at the endpoints,
and then iteratively adds the function values at the intermediate points within the range. Finally, it
multiplies the sum by h to obtain the approximate integral value and Returns it.
43

12.1 C++ Implementation

Use the trapezoidal rule to estimate


Z 1
x2 dx
0

using four subintervals.


1 #include <iostream>
2 #include <functional>
3
4 using namespace std;
5
6 double integrate(function<double(double)> func, double a, double b, int n) {
7 double h = (b - a) / n;
8 double sum = (func(a) + func(b)) / 2.0;
9
10 for (int i = 1; i < n; i++) {
11 double x = a + i * h;
12 sum += func(x);
13 }
14
15 double result = h * sum;
16 return result;
17 }
18
19 // Example usage
20 double square(double x) {
21 return x * x;
22 }
23
24 int main() {
25 double a = 0.0; // Lower limit of integration
26 double b = 1.0; // Upper limit of integration
27 int n = 4; // Number of subdivisions
28
29 double result = integrate(square, a, b, n);
30 cout << "Approximate integral: " << result << endl;
31

32 return 0;
33 }
34
44

13 Simultaneous linear equations: Gaussian elimination

The concept of Gaussian elimination is a widely-used method For solving a system of simultaneous
equations numerically. In electrical engineering and various other fields, it is a recurring problem
to find the numerical values of variables that satisfy a set of equations.

a11 x1 + a12 x2 + . . . + a1n xn = b1


a21 x1 + a22 x2 + . . . + a2n xn = b2
..
.
am1 x1 + am2 x2 + . . . + amn xn = bm

The system of equations may also be written using sigma For summing elements :

n
X
a1j xj = b1
j=1
Xn
a2j xj = b2
j=1
..
.
n
X
amj xj = bm
j=1

Or simply as

n
For i = 1, 2, . . . , n
X
ai,j xj = bi
j=1

Gaussian elimination algorithm For solving a system of simultaneous linear equations:


45

Algorithm 33 Gaussian Elimination


Require: Coefficient matrix A, constant vector B
Ensure: Solution vector X
1: function GaussianElimination(A, B)
2: for i = 1 to n − 1 do
3: for j = i + 1 to n do
4: ratio = A[j][i]/A[i][i]
5: for k = i to n do
6: A[j][k] = A[j][k] − ratio · A[i][k]
7: end for
8: B[j] = B[j] − ratio · B[i]
9: end for
10: end for
11: for i = n to 1 do
12: sum = 0
13: for j = i + 1 to n do
14: sum = sum + A[i][j] · X[j]
15: end for
16: X[i] = B[i]−sum
A[i][i]
17: end for
18: Return X
19: end function
46

13.1 C++ Implementation

Solve the following system of equations using Gauss elimination method.

x+y+z =9
2x + 5y + 7z = 52
2x + y˘z = 0

1 #include <iostream>
2 #include <vector>
3
4 using namespace std;
5
6 vector<double> gaussianElimination(vector<vector<double>>& A, vector<double>& B)
7 {
8 int n = A.size();
9 for (int i = 0; i < n - 1; i++) {
10 for (int j = i + 1; j < n; j++) {
11 double ratio = A[j][i] / A[i][i];
12 for (int k = i; k < n; k++) {
13 A[j][k] -= ratio * A[i][k];
14 }
15 B[j] -= ratio * B[i];
16 }
17 }
18
19 vector<double> X(n);
20 for (int i = n - 1; i >= 0; i--) {
21 double sum = 0;
22 for (int j = i + 1; j < n; j++) {
23 sum += A[i][j] * X[j];
24 }
25 X[i] = (B[i] - sum) / A[i][i];
26 }
27
28 return X;
29 }
30
31 int main(){
32 vector<vector<double>> A = { {1, 1, 1},
33 {2, 5, 7},
34 {2, 1, -1} };
35 vector<double> B = {9, 52, 0};
36
37 vector<double> X = gaussianElimination(A, B);
38 cout << "Solution vector X:" << endl;
39 for (int i = 0; i < X.size(); i++) {
40 cout << "X[" << i + 1 << "] = " << X[i] << endl;
41 }
42
43 return 0;
44 }
47

14 Grammar

The given grammar describes a simple English language sentence structure. Let’s break down the
components of the grammar:

• N: Set of non-terminals

– ⟨sentence⟩: Represents a sentence

– ⟨noun phrase⟩: Represents a noun phrase

– ⟨verb phrase⟩: Represents a verb phrase

– ⟨adjective⟩: Represents an adjective

– ⟨noun⟩: Represents a noun

– ⟨verb⟩: Represents a verb

– ⟨adverb⟩: Represents an adverb

• T: Set of terminals

– "the", "little", "fish", "swam", "quickly"

• P: Set of production rules

– ⟨sentence⟩ → ⟨noun phrase⟩⟨verb phrase⟩ (Production Rule 1)

– ⟨noun phrase⟩ → ⟨adjective⟩⟨noun phrase⟩ (Production Rule 2)

– ⟨noun phrase⟩ → ⟨adjective⟩⟨noun⟩ (Production Rule 3)

– ⟨verb phrase⟩ → ⟨verb⟩⟨adverb⟩ (Production Rule 4)

– ⟨adjective⟩ → "the" | "little" (Production Rule 5)

– ⟨noun⟩ → "fish" (Production Rule 6)

– ⟨verb⟩ → "swam" (Production Rule 7)

– ⟨adverb⟩ → "quickly" (Production Rule 8)

• S: Starting symbol

– ⟨sentence⟩

This grammar can generate sentences such as "The little fish swam quickly." by applying the produc-
tion rules in a recursive manner starting from the starting symbol ⟨sentence⟩. Each non-terminal is
replaced by its corresponding production rule until only terminals remain.
48

⟨sentence⟩

⟨noun phrase⟩ ⟨verb phrase⟩

⟨adjective⟩ ⟨noun phrase⟩ ⟨verb⟩ ⟨adverb⟩

the ⟨adjective⟩ ⟨noun⟩ swam quickly

little fish

Figure 1: Syntax tree For the sentence "The little fish swam quickly."

Another example of grammar:

The given grammar describes a simple arithmetic expression language. Let’s break down the com-
ponents of the grammar:

• N: Set of non-terminals

– ⟨expr⟩: Represents an arithmetic expression

– ⟨term⟩: Represents a term in an expression

– ⟨factor⟩: Represents a factor in a term

• T: Set of terminals

– Numbers: 0, 1, 2, 3, . . .

– Operators: +, −, ∗

– Parentheses: (, )

• P: Set of production rules

– ⟨expr⟩ → ⟨expr⟩ + ⟨term⟩ (Production Rule 1)

– ⟨expr⟩ → ⟨expr⟩ − ⟨term⟩ (Production Rule 2)

– ⟨expr⟩ → ⟨term⟩ (Production Rule 3)

– ⟨term⟩ → ⟨term⟩ ∗ ⟨factor⟩ (Production Rule 4)

– ⟨term⟩ → ⟨factor⟩ (Production Rule 5)

– ⟨factor⟩ → ( ⟨expr⟩ ) (Production Rule 6)

– ⟨factor⟩ → Number (Production Rule 7)

• S: Starting symbol

– ⟨expr⟩
49

Syntax Tree:

⟨expr⟩

⟨term⟩ + ⟨term⟩

⟨factor⟩ ⟨factor⟩

Number ( ⟨expr⟩ )

2 ⟨term⟩ ∗ ⟨factor⟩

⟨factor⟩ Number

Number 4

Figure 2: Syntax tree For the expression "2 + (3 * 4)".


50

Another example of grammar:


The given grammar describes a simple English Statement. Let’s break down the components of the
grammar:

• N: Set of non-terminals
– ⟨sentence⟩: Represents a sentence
– ⟨subject⟩: Represents the subject of a sentence
– ⟨verb⟩: Represents the verb in a sentence
– ⟨object⟩: Represents the object of a sentence
– ⟨adjective⟩: Represents an adjective
– ⟨preposition⟩: Represents a preposition
– ⟨For⟩: Represents the word "For"
• T: Set of terminals
– Words: "Algorithms", "are", "essential", "programming"
• P: Set of production rules
– ⟨sentence⟩ → ⟨subject⟩ ⟨verb⟩ (Production Rule 1)
– ⟨sentence⟩ → ⟨subject⟩ ⟨verb⟩ ⟨object⟩ (Production Rule 2)
– ⟨subject⟩ → "Algorithms" (Production Rule 3)
– ⟨verb⟩ → "are" (Production Rule 4)
– ⟨verb⟩ → "essential" (Production Rule 5)
– ⟨object⟩ → "programming" (Production Rule 6)
– ⟨adjective⟩ → "essential" (Production Rule 7)
– ⟨preposition⟩ → "For" (Production Rule 8)
– ⟨For⟩ → "For" (Production Rule 9)
• S: Starting symbol
– ⟨sentence⟩

Syntax Tree

⟨sentence⟩

⟨subject⟩ ⟨verb⟩ ⟨adjective⟩ ⟨preposition⟩ ⟨object⟩

Algorithms are essential For programming

Figure 3: Syntax tree For the Statement "Algorithms are essential For programming."
51

Another example of grammar:


The given grammar describes a simple English Statement. Let’s break down the components of the
grammar:

• N: Set of non-terminals
– ⟨sentence⟩: Represents a sentence
– ⟨subject⟩: Represents the subject of a sentence
– ⟨verb⟩: Represents the verb in a sentence
– ⟨object⟩: Represents the object of a sentence
– ⟨adjective⟩: Represents an adjective
– ⟨noun⟩: Represents a noun
– ⟨preposition⟩: Represents a preposition
– ⟨time⟩: Represents a time expression
• T: Set of terminals
– Words: "My", "family", "had", "spaghetti", "For", "dinner", "last", "night"
• P: Set of production rules
– ⟨sentence⟩ → ⟨subject⟩ ⟨verb⟩ ⟨object⟩ (Production Rule 1)
– ⟨subject⟩ → "My" (Production Rule 2)
– ⟨verb⟩ → "had" (Production Rule 3)
– ⟨object⟩ → ⟨adjective⟩ ⟨noun⟩ (Production Rule 4)
– ⟨adjective⟩ → ε (Production Rule 5)
– ⟨noun⟩ → "family" (Production Rule 6)
– ⟨preposition⟩ → "For" (Production Rule 7)
– ⟨time⟩ → "dinner" "last" "night" (Production Rule 8)
• S: Starting symbol
– ⟨sentence⟩

Syntax Tree

⟨sentence⟩

⟨subject⟩ ⟨verb⟩ ⟨object⟩ ⟨preposition⟩ ⟨time⟩

My had ⟨adjective⟩ ⟨noun⟩ For dinner last night

family

Figure 4: Syntax tree For the Statement "My family had spaghetti For dinner last night."

You might also like