You are on page 1of 3

Unit1 emphirical analysis.

Unit1
Asymptotic notation is a mathematical tool used in the analysis of algorithms
to describe their behavior as the size of their input approaches infinity. It
allows us to understand how the performance of an algorithm scales with
increasing input size. There are three primary notations used in asymptotic
analysis: Big O (O), Omega (Ω), and Theta (Θ).
1. Big O Notation (O):
o Big O notation describes the upper bound or the worst-case scenario of an
algorithm's time or space complexity.
o It represents the maximum rate of growth of a function.
o Formally, for a function f(n), O(g(n)) represents an upper bound if there exist
constants c and n₀ such that |f(n)| ≤ c * |g(n)| for all n > n₀.
o In simpler terms, it provides an estimate of the worst-case running time of an
algorithm as a function of the input size.
2. Omega Notation (Ω):
o Omega notation describes the lower bound or the best-case scenario of an
algorithm's time or space complexity.
o It represents the minimum rate of growth of a function.
o Formally, for a function f(n), Ω(g(n)) represents a lower bound if there exist
constants c and n₀ such that |f(n)| ≥ c * |g(n)| for all n > n₀.
o In simpler terms, it provides an estimate of the best-case running time of an
algorithm as a function of the input size.
3. Theta Notation (Θ):
o Theta notation provides both upper and lower bounds, essentially giving a
tight bound on an algorithm's time or space complexity.
o It represents the exact growth rate of a function.
o Formally, for a function f(n), Θ(g(n)) represents a tight bound if there exist
constants c₁, c₂, and n₀ such that c₁ * |g(n)| ≤ |f(n)| ≤ c₂ * |g(n)| for all n > n₀.
o In simpler terms, it provides an estimate of both the best and worst-case
running time of an algorithm as a function of the input size.

Unit1 Unit 2 Unit1

Selection sort merge sort Insertion sort


1. MERGE_SORT(arr, beg, end) insertionSort(arr):
procedure selectionSort(arr): 2.
3. if beg < end n = length of arr
n = length of arr 4. set mid = (beg + end)/2
5. MERGE_SORT(arr, beg, mid) for i from 1 to n - 1: // Start from the second element
for i from 0 to n - 2: // Iterate through the array (0 to n-2) 6. MERGE_SORT(arr, mid + 1, end)
7. MERGE (arr, beg, mid, end) key = arr[i] // Select the key element to be inserted
min_index = i // Assume the current index has the 8. end of if
minimum value 9. j=i-1 // Set j to the index before the current element
10. END MERGE_SORT
for j from i+1 to n - 1: // Find the index of the minimum
Quick sort
element in the unsorted portion // Move elements of arr[0..i-1], that are greater than key, to one position
1. QUICKSORT (array A, start, end) ahead of their current position
if arr[j] < arr[min_index]: 2. {
3. 1 if (start < end) while j >= 0 and arr[j] > key:
min_index = j 4. 2{
5. 3 p = partition(A, start, end) arr[j + 1] = arr[j] // Move the element one position ahead
swap(arr[i], arr[min_index]) // Swap the minimum element with 6. 4 QUICKSORT (A, start, p - 1)
the first unsorted element 7. 5 QUICKSORT (A, p + 1, end) j=j-1 // Move to the previous element
8. 6}
9. }
bubble sort
Partition arr[j + 1] = key // Insert the key into its correct position in the
procedure bubbleSort(arr): sorted subarray
1. PARTITION (array A, start, end)
n = length of arr 2. {
3. 1 pivot ? A[end]
for i from 0 to n-1: // Iterate through the entire array 4. 2 i ? start-1
5. 3 for j ? start to end -1 {
6. 4 do if (A[j] < pivot) {
for j from 0 to n-i-2: // Iterate through the unsorted portion of 7. 5 then i ? i + 1
the array 8. 6 swap A[i] with A[j]
9. 7 }}
if arr[j] > arr[j+1]: // Compare adjacent elements 10. 8 swap A[i+1] with A[end]
11. 9 return i+1
swap(arr[j], arr[j+1]) // Swap if they are in the wrong order 12. }

A = A1 , A0 Depth-First Search (DFS) and Breadth-First Search (BFS) are two


straxxen matrix

• B = B1 , B0 fundamental algorithms used for traversing or searching graphs or
• Recursively compute the following values:
tree data structures. Here are the main differences between DFS and
BFS:
A = | A11 A12 | B = | B11 B12 | • P1 = A1 * B1
• P2 = A0 * B0 1. Traversal Order:
| A21 A22 | | B21 B22 | • P3 = (A1 + A0) * (B1 + B0) - P1 - P2 o DFS: DFS explores a branch of the graph as deeply as possible

before backtracking. It explores one branch of the graph fully before


M1 = (A11 + A22) * (B11 + B22) • Compute the result: moving to the next branch.
o BFS: BFS explores all the neighbors of a node before moving on to
M2 = (A21 + A22) * B11 • Result = P1 * 10^n + P3 * 10^(n/2) + P2
the next level of nodes. It explores nodes level by level starting from
M3 = A11 * (B12 - B22) Heap sort the source node.
2. Data Structure:
1. HeapSort(arr)
o DFS: DFS can be implemented using recursion (implicit stack) or
M4 = A22 * (B21 - B11) 2. BuildMaxHeap(arr)
3. for i = length(arr) to 2 using an explicit stack data structure.
4. swap arr[1] with arr[i]
M5 = (A11 + A12) * B22 5. heap_size[arr] = heap_size[arr] ? 1 o BFS: BFS is typically implemented using a queue data structure.
6. MaxHeapify(arr,1) 3. Memory Usage:
7. End
M6 = (A21 - A11) * (B11 + B12) o DFS: DFS can use less memory compared to BFS because it only
BuildMaxHeap(arr) needs to store information about the current path being explored.
M7 = (A12 - A22) * (B21 + B22) 1. BuildMaxHeap(arr) o BFS: BFS may consume more memory as it needs to store
2. heap_size(arr) = length(arr) information about all the nodes at the current level being explored.
3. for i = length(arr)/2 to 1
C11 = M1 + M4 - M5 + M7 4. MaxHeapify(arr,i) 4. Efficiency:
5. End o DFS: DFS can be more memory-efficient in sparse graphs or when
C12 = M3 + M5 6.
the solution is located deep in the graph.
MaxHeapify(arr,i) o BFS: BFS is generally more efficient for finding the shortest path in
C21 = M2 + M4 1. MaxHeapify(arr,i) unweighted graphs or for finding the shortest path with the fewest
2. L = left(i) edges.
C22 = M1 - M2 + M3 + M6 3. R = right(i)
4. if L ? heap_size[arr] and arr[L] > arr[i] 5. Applications:
5. largest = L o DFS: DFS is useful for tasks like topological sorting, cycle
6. else
7. largest = i detection, and solving problems where you need to explore a single
8. if R ? heap_size[arr] and arr[R] > arr[largest] branch completely before moving to the next branch.
9. largest = R
10. if largest != i o BFS: BFS is useful for finding the shortest path in unweighted

Long integer multi 11. swap arr[i] with arr[largest] graphs, determining connectivity, and solving problems where you need
12. MaxHeapify(arr,largest)
• Otherwise, split both A and B into two equal-sized parts: to explore nodes level by level.
DFS(graph, start_node): Johnson-Trotter algorithm: The fake coin problem, also known as the counterfeit coin
problem, is a classic problem in mathematics and computer science. The
Initialize an empty set to keep track of visited nodes 1. Initialize an array representing the permutation, with elements problem is to find a counterfeit coin among a set of coins using a balance scale,
from 1 to n arranged in ascending order. which can only compare the weights of two sets of coins.Divide and Conquer:
Call the recursive dfs_util function with the start_node and the 2. Initialize an array to keep track of the directions of each Divide the set of coins into two equal (or nearly equal) subsets.
visited set element. If an element is pointing left (denoted by -1) it moves 1. Weigh the Two Sets: Use the balance scale to compare the weights of
dfs_util(graph, node, visited): left in the permutation, and if it's pointing right (denoted by 1) it the two sets of coins:
Add the current node to the visited set moves right. o If the two sets have equal weights, the fake coin must be in the
remaining unweighed coins.
Process the current node (e.g., print it, perform some operation) 3. Find the largest mobile element in the permutation. A mobile o If one set is heavier than the other, the fake coin must be in that set.
element is an element that is greater than its adjacent element in 2. Recursively Repeat: Recursively repeat steps 1 and 2 with the subset
For each neighbor of the current node: the direction it is pointing. containing the fake coin until the fake coin is found.
4. Swap the mobile element with the element it is pointing to
If the neighbor has not been visited: def find_fake_coin(coins):
(adjacent) in the direction it is pointing.
Recursively call dfs_util with the neighbor and the visited set 5. Reverse the direction of all elements greater than the swapped if len(coins) == 1:
element.
BFS(graph, start_node): 6. Repeat steps 3-5 until there are no mobile elements left. return coins[0] # Base case: only one coin left, it must
be the fake one
Initialize an empty set to keep track of visited nodes The Josephus problem is a theoretical problem related to a # Weigh the two subsets
certain counting-out game. In this problem, a group of n people
Initialize an empty queue and enqueue the start_node (numbered from 1 to n) are standing in a circle. Starting from person 1, middle_index = len(coins) // 2
every kth person is eliminated from the circle until only one person
Add the start_node to the visited set
remains. The problem is to find the position of the last remaining person. weight_left = sum(coins[:middle_index])
while the queue is not empty:
function josephus(n, k): weight_right = sum(coins[middle_index:])
Dequeue a node from the queue
if n == 1:
Process the dequeued node (e.g., print it, perform some
return 1 // Base case: only one person remains, so their # Recursively find the fake coin in the heavier subset
operation)
position is 1
For each neighbor of the dequeued node: if weight_left == weight_right:
else:
If the neighbor has not been visited: return find_fake_coin(coins[middle_index:])
// Recursively find the position of the survivor for a circle
Add the neighbor to the visited set with n-1 people else:

Enqueue the neighbor into the queue return (josephus(n - 1, k) + k - 1) % n + 1 return find_fake_coin(coins[:middle_index])

Booyre more uniqueness using presorting


horspool algo function is_unique(arr):
function boyer_moore_search(text, pattern):
function horspool_search(text, pattern): sort(arr) // Sort the array using a sorting algorith
initialize bad character table for pattern
initialize bad character table for pattern for i from 0 to length of arr - 2:
initialize good suffix table for pattern
i = length of pattern - 1 if arr[i] == arr[i + 1]:
i = length of pattern - 1
while i < length of text: return false // Duplicates found
while i < length of text:
j = length of pattern - 1 return true // No duplicates found, array is unique
j = length of pattern - 1
while j >= 0 and text[i] == pattern[j]: max heap
while j >= 0 and text[i] == pattern[j]:
i=i-1 function insert_max_heap(heap, value):
i=i-1
j=j-1 heap.append(value) // Add the new element at the end of the heap
j=j-1
if j == -1: index = length of heap - 1 // Index of the newly inserted element
if j == -1:
return i + 1 // match found while index > 0:
return i + 1 // match found
i = i + shift value of text[i] in the bad character table parent_index = (index - 1) // 2 // Parent index
bad_char_shift = shift value of text[i] in the bad character
• return -1 // match not found table if heap[parent_index] < heap[index]:
text is the text being searched.
• pattern is the pattern being searched for. good_suffix_shift = shift value based on the good suffix table swap(heap[parent_index], heap[index]) // Swap parent and
• The bad character table is precomputed based on the pattern, and j child
which helps determine the shift value for each character.
• The algorithm starts matching the pattern against the text from i = i + max(bad_char_shift, good_suffix_shift) index = parent_index // Move up the tree
right to left.
• When a mismatch occurs, it consults the bad character table to return -1 // match not found else:
determine the shift value for the mismatched character.
• It continues searching until the entire text is searched or a match break
is found.

naïve string matching Floydd and Warshall prism


function naive_string_match(text, pattern):
function floyd_warshall(graph): function prim(graph):
n = length of text
n = number of vertices in the graph n = number of vertices in the graph
m = length of pattern
dist = initialize a 2D array of size n x n with each cell initialized visited = array of size n, initialized to false
for i from 0 to n - m:
to INF
j=0
parent = array of size n, initialized to -1
// Initialize distances based on the edges in the graph
while j < m and text[i + j] == pattern[j]: key = array of size n, initialized to INF
for each edge (u, v) with weight w in the graph:
j = j + 1. if j == m: // Start with vertex 0. ->key[0] = 0
dist[u][v] = w
return i // Pattern found starting at position i for count from 0 to n-1:
// Floyd-Warshall algorithm to find shortest paths between all
return -1 // Pattern not found pairs of vertices u = vertex with the minimum key value among vertices
for k from 0 to n-1: not yet visited

for i from 0 to n-1: visited[u] = true

for j from 0 to n-1: // Update key values and parent pointers of adjacent
vertices of u
// If vertex k is included in the shortest path from i to j,
update the distance for each vertex v adjacent to u:
if dist[i][k] + dist[k][j] < dist[i][j]: if v is not yet visited and weight of edge u-v is less
dist[i][j] = dist[i][k] + dist[k][j] than key[v]:

return dist parent[v] = u


key[v] = weight of edge u-v
return parent // Return the array representing the minimum
spanning tree
dijkistra The advantages of using memoization (memory function) include: 1. **P (Polynomial Time)**:
function dijkstra(graph, source): 1. Improved Performance: Memoization can significantly improve the - P class contains decision problems that can be solved by a
performance of algorithms by storing the results of expensive function calls
n = number of vertices in the graph deterministic Turing machine in polynomial time.
and avoiding redundant computations. This is particularly useful for
recursive algorithms with overlapping subproblems. - These are problems for which an algorithm exists that can solve
visited = array of size n, initialized to false
2. Reduced Time Complexity: Memoization can reduce the time complexity
them efficiently, with time complexity bounded by a polynomial
distance = array of size n, initialized to INF of algorithms by eliminating the need to recompute results for subproblems
that have already been solved. This can lead to a significant reduction in the function of the input size.
parent = array of size n, initialized to -1 overall time required to solve a problem.
- Examples include basic arithmetic operations, sorting
3. Simplified Implementation: Memoization can simplify the
implementation of algorithms by separating concerns and avoiding the need algorithms like merge sort and quicksort, and many graph
// Distance to source vertex is 0
for complex bookkeeping code to track previously computed results. This algorithms like breadth-first search (BFS) and depth-first search
distance[source] = 0 can make the code easier to understand, debug, and maintain. (DFS).
for count from 0 to n-1: 4. Scalability: Memoization can make algorithms more scalable by reducing
the computational overhead associated with solving large instances of a 2. **NP (Nondeterministic Polynomial Time)**:
u = vertex with the minimum distance value among vertices not
yet visited problem. By caching results and reusing them as needed, memoization can
help ensure that algorithms remain efficient even as problem sizes increase. - NP class contains decision problems for which a solution can be
visited[u] = true verified in polynomial time by a deterministic Turing machine.
Let's break down each complexity class:
- These are problems for which a potential solution can be
checked quickly, even though finding the solution itself may be
// Update distance values and parent pointers of adjacent vertices
difficult.
of u
- Examples include the traveling salesman problem, the graph
for each vertex v adjacent to u:
coloring problem, and the subset sum problem.
if v is not yet visited and distance[u] + weight of edge u-v is
3. **NP-Complete (Nondeterministic Polynomial Time-
less than distance[v]:
Complete)**:
parent[v] = u
- NP-Complete class contains the most difficult problems in NP,
distance[v] = distance[u] + weight of edge u-v in the sense that if any NP-Complete problem can be solved in
polynomial time, then all problems in NP can be solved in
return distance, parent // Return the array representing shortest polynomial time.
distances and parent pointers
- Every problem in NP can be reduced to an NP-Complete
problem in polynomial time.

4. **NP-Hard**:

- NP-Hard class contains problems that are at least as hard as the


hardest problems in NP.
- Unlike NP-Complete problems, NP-Hard problems do not
necessarily need to be decision problems.
- Examples include the halting problem, the vertex cover
problem, and many optimization problems like the shortest path
problem and the maximum flow problem.
Now, let's compare these classes:
- P is a subset of NP, meaning every problem in P is also in NP.
This is because if a problem can be solved efficiently, then it can
certainly be verified efficiently.
- NP-Complete problems are the hardest problems in NP, and they
are believed to be inherently difficult, with no known polynomial-
time algorithm for solving them.
- NP-Hard problems are at least as hard as NP-Complete problems
but may not necessarily be decision problems.
- Solving an NP-Complete problem in polynomial time would
imply that P = NP, which is one of the most famous unsolved
problems in computer science.

You might also like