student notes / est. for the classroom

HTML, CSS, JavaScript, Python, data science, computer networks — written the way you'd explain it to a classmate, not a compiler.

Top Job & Internship Portals

Handpicked portals for fresher jobs, tech roles, and listings in Hyderabad

GFG

GeeksforGeeks

Tech & Software Roles

Visit →
INT

Internshala

Fresher Jobs & Internships

Visit →
GOOG

Google Careers

Global Google Openings

Visit →
APN

Apna Jobs

Local Jobs in Hyderabad

Visit →
INS

Instahyre

Tech Roles in Hyderabad

Visit →
NAUK

Naukri.com

Fresher Jobs in Hyderabad

Visit →
📢 Updated daily

Internship & Job Alerts

01

Latest notes

December 18, 2023

Design and Analysis of Algorithms (DAA)

 Hand written Notes

What Is An Algorithm? Definition, Types, Characteristics




What is an Algorithm?

An algorithm is a set of commands that must be followed for a computer to perform calculations or other problem-solving operations.According to its formal definition, an algorithm is a finite set of instructions carried out in a specific order to perform a particular task. It is not the entire program or code; it is simple logic to a problem represented as an informal description in the form of a flowchart or pseudocode.



Problem: A problem can be defined as a real-world problem or real-world instance problem for which you need to develop a program or set of instructions. An algorithm is a set of instructions. 

Algorithm: An algorithm is defined as a step-by-step process that will be designed for a problem.

Input: After designing an algorithm, the algorithm is given the necessary and desired inputs.

Processing unit: The input will be passed to the processing unit, producing the desired output.

Output: The outcome or result of the program is referred to as the output.

After defining what an algorithm is, you will now look at algorithm characteristics.

How do Algorithms Work?

Algorithms are step-by-step procedures designed to solve specific problems and perform tasks efficiently in the realm of computer science and mathematics. These powerful sets of instructions form the backbone of modern technology and govern everything from web searches to artificial intelligence. Here's how algorithms work:

Input: Algorithms take input data, which can be in various formats, such as numbers, text, or images.

Processing: The algorithm processes the input data through a series of logical and mathematical operations, manipulating and transforming it as needed.

Output: After the processing is complete, the algorithm produces an output, which could be a result, a decision, or some other meaningful information.

Efficiency: A key aspect of algorithms is their efficiency, aiming to accomplish tasks quickly and with minimal resources.

Optimization: Algorithm designers constantly seek ways to optimize their algorithms, making them faster and more reliable.

Implementation: Algorithms are implemented in various programming languages, enabling computers to execute them and produce desired outcomes.

What is the Need for Algorithms?

You require algorithms for the following reasons:

Scalability

It aids in your understanding of scalability. When you have a sizable real-world problem, you must break it down into small steps to analyze it quickly.

Performance

The real world is challenging to break down into smaller steps. If a problem can be easily divided into smaller steps, it indicates that the problem is feasible.

After understanding what is an algorithm, why you need an algorithm, you will look at how to write one using an example.

Types of Algorithms

Brute Force Algorithm: A straightforward approach that exhaustively tries all possible solutions, suitable for small problem instances but may become impractical for larger ones due to its high time complexity.

Recursive Algorithm: A method that breaks a problem into smaller, similar subproblems and repeatedly applies itself to solve them until reaching a base case, making it effective for tasks with recursive structures.

Encryption Algorithm: Utilized to transform data into a secure, unreadable form using cryptographic techniques, ensuring confidentiality and privacy in digital communications and transactions.

Backtracking Algorithm: A trial-and-error technique used to explore potential solutions by undoing choices when they lead to an incorrect outcome, commonly employed in puzzles and optimization problems.

Searching Algorithm: Designed to find a specific target within a dataset, enabling efficient retrieval of information from sorted or unsorted collections.

Sorting Algorithm: Aimed at arranging elements in a specific order, like numerical or alphabetical, to enhance data organization and retrieval.

Hashing Algorithm: Converts data into a fixed-size hash value, enabling rapid data access and retrieval in hash tables, commonly used in databases and password storage.

Divide and Conquer Algorithm: Breaks a complex problem into smaller subproblems, solves them independently, and then combines their solutions to address the original problem effectively.

Greedy Algorithm: Makes locally optimal choices at each step in the hope of finding a global optimum, useful for optimization problems but may not always lead to the best solution.

Dynamic Programming Algorithm: Stores and reuses intermediate results to avoid redundant computations, enhancing the efficiency of solving complex problems.

Randomized Algorithm: Utilizes randomness in its steps to achieve a solution, often used in situations where an approximate or probabilistic answer suffices.

 How to Write an Algorithm?

There are no well-defined standards for writing algorithms. It is, however, a problem that is resource-dependent. Algorithms are never written with a specific programming language in mind.

As you all know, basic code constructs such as loops like do, for, while, all programming languages share flow control such as if-else, and so on. An algorithm can be written using these common constructs.

Algorithms are typically written in a step-by-step fashion, but this is not always the case. Algorithm writing is a process that occurs after the problem domain has been well-defined. That is, you must be aware of the problem domain for which you are developing a solution.

Example 

Now, use an example to learn how to write algorithms.

Problem: Create an algorithm that multiplies two numbers and displays the output.

Step 1 − Start

Step 2 − declare three integers x, y & z

Step 3 − define values of x & y

Step 4 − multiply values of x & y

Step 5 − store result of step 4 to z

Step 6 − print z

Step 7 − Stop

Algorithms instruct programmers on how to write code. In addition, the algorithm can be written as:

Step 1 − Start mul

Step 2 − get values of x & y

Step 3 − z ← x * y

Step 4 − display z

Step 5 − Stop

In algorithm design and analysis, the second method is typically used to describe an algorithm. It allows the analyst to analyze the algorithm while ignoring all unwanted definitions easily. They can see which operations are being used and how the process is progressing. It is optional to write step numbers. To solve a given problem, you create an algorithm. A problem can be solved in a variety of ways.


As a result, many solution algorithms for a given problem can be derived. The following step is to evaluate the proposed solution algorithms and implement the most appropriate solution.

As you progress through this "what is an Algorithm" tutorial, you will learn about some of the components of an algorithm.

Factors of an Algorithm

The following are the factors to consider when designing an algorithm:

Modularity: This feature was perfectly designed for the algorithm if you are given a problem and break it down into small-small modules or small-small steps, which is a basic definition of an algorithm.

Correctness: An algorithm's correctness is defined as when the given inputs produce the desired output, indicating that the algorithm was designed correctly. An algorithm's analysis has been completed correctly.

Maintainability: It means that the algorithm should be designed in a straightforward, structured way so that when you redefine the algorithm, no significant changes are made to the algorithm.

Functionality: It takes into account various logical steps to solve a real-world problem.

Robustness: Robustness refers to an algorithm's ability to define your problem clearly.

User-friendly: If the algorithm is difficult to understand, the designer will not explain it to the programmer.

Simplicity: If an algorithm is simple, it is simple to understand.

Extensibility: Your algorithm should be extensible if another algorithm designer or programmer wants to use it.

You will now see why an algorithm is so essential after understanding some of its components.


 Qualities of a Good Algorithm

Efficiency: A good algorithm should perform its task quickly and use minimal resources.

Correctness: It must produce the correct and accurate output for all valid inputs.

Clarity: The algorithm should be easy to understand and comprehend, making it maintainable and modifiable.

Scalability: It should handle larger data sets and problem sizes without a significant decrease in performance.

Reliability: The algorithm should consistently deliver correct results under different conditions and environments.

Optimality: Striving for the most efficient solution within the given problem constraints.

Robustness: Capable of handling unexpected inputs or errors gracefully without crashing.

Adaptability: Ideally, it can be applied to a range of related problems with minimal adjustments.

Simplicity: Keeping the algorithm as simple as possible while meeting its requirements, avoiding unnecessary complexity.

The Complexity of an Algorithm

The algorithm's performance can be measured in two ways:

Time Complexity

The amount of time required to complete an algorithm's execution is called time complexity. The big O notation is used to represent an algorithm's time complexity. The asymptotic notation for describing time complexity, in this case, is big O notation. The time complexity is calculated primarily by counting the number of steps required to complete the execution. Let us look at an example of time complexity.

mul = 1;

// Suppose you have to calculate the multiplication of n numbers.  

for i=1 to n  

mul = mul *1;  

// when the loop ends, then mul holds the multiplication of the n numbers  

return mul;  

The time complexity of the loop statement in the preceding code is at least n, and as the value of n escalates, so does the time complexity. While the code's complexity, i.e., returns mul, will be constant because its value is not dependent on the importance of n and will provide the result in a single step. The worst-time complexity is generally considered because it is the maximum time required for any given input size.

Space Complexity

The amount of space an algorithm requires to solve a problem and produce an output is called its space complexity. Space complexity, like time complexity, is expressed in big O notation.

The space is required for an algorithm for the following reasons:

To store program instructions.

To store track of constant values.

To store track of variable values.

To store track of function calls, jumping statements, and so on.

Space Complexity = Auxiliary Space + Input Size

Finally after understanding what is an algorithm, its analysis and approches, you will look at different types of algorithms.

Advantage and Disadvantages of Algorithms

Advantages of Algorithms:

Efficiency: Algorithms streamline processes, leading to faster and more optimized solutions.

Reproducibility: They yield consistent results when provided with the same inputs.

Problem-solving: Algorithms offer systematic approaches to tackle complex problems effectively.

Scalability: Many algorithms can handle larger datasets and scale with increasing input sizes.

Automation: They enable automation of tasks, reducing the need for manual intervention.

Disadvantages of Algorithms:

Complexity: Developing sophisticated algorithms can be challenging and time-consuming.

Limitations: Some problems may not have efficient algorithms, leading to suboptimal solutions.

Resource Intensive: Certain algorithms may require significant computational resources.

Inaccuracy: Inappropriate algorithm design or implementation can result in incorrect outputs.

Maintenance: As technology evolves, algorithms may require updates to stay relevant and effective.

 

Randomized Algorithms

Introduction

As we all know, algorithms are step-by-step procedure that is used to solve problems or to do some calculations. While most algorithms use a defined set of rules, we can introduce some randomness in their logic to make them into randomized algorithms.


Randomized Algorithms use some randomness in their logic to improve efficiency, time complexity, or maybe the total memory used. Let us dig deeper into the blog; Randomized Algorithms to know more.

What is Randomized Algorithms?

Randomized algorithms are a class of algorithms in computer science that use randomness or randomness in combination with deterministic steps to solve computational problems. These algorithms introduce randomness intentionally into their calculations to achieve specific goals, such as improving efficiency or increasing the likelihood of finding a correct solution.

Randomized algorithms are particularly useful in situations where finding an exact solution to a problem is computationally expensive or impractical. They often provide approximate solutions with a controlled level of error. Some common applications of randomized algorithms include:

A Las Vegas or a Monte Carlo are the two most frequent algorithms to build randomized algorithms. We will see many things about them in the later section of the blog.

Need for Randomized Algorithms

Several questions must arise in your brain, like why do we even need to add randomness to our logic? What is the requirement of doing so? For example, if we have a quick sort that works fine, why do we add randomness to its logic?

This section of the blog will give you a few advantages of Randomized algorithms that will help to clear most of your doubts regarding the need for Randomized algorithms.



 

1.      Improved Efficiency: Adding randomness to the logic of an Algorithm might reduce the likelihood of that worst-case scenario happening, thus increasing its efficiency.
 

2.      Complex Problems can be Handled Effectively:  Randomized algorithms are suitable for difficult situations because they manage complex issues that are difficult to solve deterministically by providing approximate solutions or many other techniques.
 

3.      Worst-Case Scenarios can be avoided: we have already discussed this advantage of randomized algorithms. Take the example of the quick sort; introducing random pivot selection in the logic can reduce the probability of falling into the worst-case scenario of the algorithm, improving the overall time complexity.
 

4.      Cryptography: In cryptography, randomness is essential for creating secure keys, ensuring unpredictable results, and protecting against attacks. To increase security, randomized algorithms are used in random number generation, encryption techniques, and cryptographic protocols.
 

Looking at the above advantages of randomized algorithms we can safely say that randomized algorithms not only increase the efficiency of the program but also sometimes increase the security of the program.

 

Classification of Randomized Algorithms

Randomized algorithms can be roughly grouped into numerous categories based on their properties and applications. Here are two broadly classified categories of randomized algorithms.



 

1.      Monte Carlo Algorithms: To approximate numerical values or solve issues probabilistically, Monte Carlo methods use randomization. With a certain degree of assurance, they offer an approximate solution. Example includes, calculating the value of π, resolving optimization problems, or doing probabilistic simulations.
 

2.      Las Vegas Algorithms: A Las Vegas algorithm executes in a predetermined period of time. It is predictable that it runs out of time and doesn't find any solutions, but if it finds one within that window, it will be precisely correct.
 

Note: Due to the algorithm's potential for producing inaccurate results, an algorithm called as amplification is utilized to increase the likelihood of correctness while degrading runtime. The randomized algorithm is used numerous times with various random subsamples of the input, and the results are then compared to achieve amplification.

 

Let’s discuss each one of them one by one in the next few sections of this blog.

Monte Carlo Algorithms

The goal of the Monte Carlo approach for randomized algorithms is to complete the execution within the allotted time limit. This method's running time is therefore predictable. For instance, when performing string matching, Monte Carlo starts the procedure over from the last error it encountered. Thus, time is saved.

 

A deterministic algorithm's output is always anticipated to be right, whereas Monte Carlo techniques can not guarantee this. These algorithms are typically categorized as either false-biased or true-biased for decision issues. When a false-biased Monte Carlo algorithm delivers false, it is always correct; likewise, a true-biased method always yields true.

 

Technical Example: Approximating π(pi)

The answer to approximating pi, or the ratio of a circle's circumference to its diameter, or "pi", provides a famous illustration of a more complex Monte Carlo process. This approximation problem is so typical that huge banks and other mathematically demanding businesses ask candidates for programming positions in interviews about it.

Las Vegas Algorithms

This is a randomized algorithm that consistently yields the right answer or fails. Still, it cannot guarantee a time limit because the time complexity depends on the input. Virtually every search result contains a Las Vegas algorithm. Consider a Las Vegas algorithm as a problem whose answer, when found, is certain to be the right one but whose route to that answer may be uncertain.

 

The term "Las Vegas" for this algorithm is credited to mathematician Laszlo Babai, who came up with it in 1979 simply as a comparison to the much older Monte Carlo algorithm as both are significant global gaming centers.
 

Technical Example: Randomized Quicksort

Quicksort is a common Las Vegas randomized sorting method that uses no additional memory and sorts elements in place. Since this technique is comparison-based, the worst case will happen when doing a pairwise comparison, which takes O(n^2) time complexity, where the time needed grows as a square of the number of digits to be sorted. However, with randomization, this algorithm's worst-case time complexity can be lowered to O(n log(n)).
  

If the pivot element selected at random is the first or final member in the array, the worst-case scenario for this technique is that it takes O(n^2) time to sort n digits.
 

The anticipated runtime for randomized algorithms, i.e., Quicksort, where the pivot is selected randomly, and neither the smallest nor largest number in the array is chosen, is O(n log(n)).

Relation between Monte-carlo and Las Vegas

By executing a Las Vegas algorithm for a predetermined amount of time and producing a random response when it fails to terminate, it can be transformed into a Monte Carlo algorithm.
Below is the table that shows the relation between two algorithms.

 

Monte Carlo Algorithm

Las Vegas Algorithm

Correctness

Probabilistic

Certain

Running Time

Certain

Probabilistic

Now let us discuss some frequently Asked Questions on Randomized Algorithm.

Advantages of Randomized Algorithms 

Advantages of randomized algorithms are:

·         Efficiency: They often provide faster solutions, especially for complex problems.
 

·         Simplicity: They can be simpler to implement than deterministic alternatives.
 

·         Approximate Solutions: Useful when exact solutions are hard or unnecessary.
 

·         Probabilistic Correctness: Offer high probability of correctness, balancing reliability and speed.
 

·         Versatility: Applicable in various fields like computer science, math, and optimization.
 

·         Parallelism: Easily parallelizable, making them suitable for modern computing architectures.
 

·         Privacy: Useful for ensuring data privacy in scenarios like cryptography.
 

·         Robustness: Resilient to variations and uncertainties, valuable in real-world scenarios.

Limitations of Randomized Algorithms

Limitations of randomized algorithms are:

·         Probabilistic Output: They provide approximate solutions with a probability of correctness, not guaranteed accuracy.
 

·         Analysis Complexity: Evaluating their performance can be challenging due to randomness.
 

·         Deterministic Alternatives: In some cases, deterministic algorithms provide certainty at the cost of speed.
 

·         Difficulty in Reproduction: Results may vary between runs, making them less predictable.
 

·         Resource Usage: May consume more memory or time due to random operations.
 

·         Not Always Suitable: Unsuitable for tasks requiring exact, reliable results.
 

·         Complexity: Designing and analyzing randomized algorithms can be complex.
 

·         Error Control: Ensuring the acceptable level of error requires careful consideration.


Divide and Conquer Introduction

Divide and Conquer is an algorithmic pattern. In algorithmic methods, the design is to take a dispute on a huge input, break the input into minor pieces, decide the problem on each of the small pieces, and then merge the piecewise solutions into a global solution. This mechanism of solving the problem is called the Divide & Conquer Strategy.

Divide and Conquer algorithm consists of a dispute using the following three steps.

  1. Divide the original problem into a set of subproblems.
  2. Conquer: Solve every subproblem individually, recursively.
  3. Combine: Put together the solutions of the subproblems to get the solution to the whole problem.


Divide and Conquer Introduction

Generally, we can follow the divide-and-conquer approach in a three-step process.

Examples: The specific computer algorithms are based on the Divide & Conquer approach:

  1. Maximum and Minimum Problem
  2. Binary Search
  3. Sorting (merge sort, quick sort)
  4. Tower of Hanoi.

Fundamental of Divide & Conquer Strategy:

There are two fundamental of Divide & Conquer Strategy:

  1. Relational Formula
  2. Stopping Condition

1. Relational Formula: It is the formula that we generate from the given technique. After generation of Formula we apply D&C Strategy, i.e. we break the problem recursively & solve the broken subproblems.

2. Stopping Condition: When we break the problem using Divide & Conquer Strategy, then we need to know that for how much time, we need to apply divide & Conquer. So the condition where the need to stop our recursion steps of D&C is called as Stopping Condition.

Applications of Divide and Conquer Approach:

Following algorithms are based on the concept of the Divide and Conquer Technique:

  1. Binary Search: The binary search algorithm is a searching algorithm, which is also called a half-interval search or logarithmic search. It works by comparing the target value with the middle element existing in a sorted array. After making the comparison, if the value differs, then the half that cannot contain the target will eventually eliminate, followed by continuing the search on the other half. We will again consider the middle element and compare it with the target value. The process keeps on repeating until the target value is met. If we found the other half to be empty after ending the search, then it can be concluded that the target is not present in the array.
  2. Quicksort: It is the most efficient sorting algorithm, which is also known as partition-exchange sort. It starts by selecting a pivot value from an array followed by dividing the rest of the array elements into two sub-arrays. The partition is made by comparing each of the elements with the pivot value. It compares whether the element holds a greater value or lesser value than the pivot and then sort the arrays recursively.
  3. Merge Sort: It is a sorting algorithm that sorts an array by making comparisons. It starts by dividing an array into sub-array and then recursively sorts each of them. After the sorting is done, it merges them back.
  4. Closest Pair of Points: It is a problem of computational geometry. This algorithm emphasizes finding out the closest pair of points in a metric space, given n points, such that the distance between the pair of points should be minimal.
  5. Strassen's Algorithm: It is an algorithm for matrix multiplication, which is named after Volker Strassen. It has proven to be much faster than the traditional algorithm when works on large matrices.
  6. Cooley-Tukey Fast Fourier Transform (FFT) algorithm: The Fast Fourier Transform algorithm is named after J. W. Cooley and John Turkey. It follows the Divide and Conquer Approach and imposes a complexity of O(nlogn).
  7. Karatsuba algorithm for fast multiplication: It is one of the fastest multiplication algorithms of the traditional time, invented by Anatoly Karatsuba in late 1960 and got published in 1962. It multiplies two n-digit numbers in such a way by reducing it to at most single-digit.

Advantages of Divide and Conquer

  • Divide and Conquer tend to successfully solve one of the biggest problems, such as the Tower of Hanoi, a mathematical puzzle. It is challenging to solve complicated problems for which you have no basic idea, but with the help of the divide and conquer approach, it has lessened the effort as it works on dividing the main problem into two halves and then solve them recursively. This algorithm is much faster than other algorithms.
  • It efficiently uses cache memory without occupying much space because it solves simple subproblems within the cache memory instead of accessing the slower main memory.
  • It is more proficient than that of its counterpart Brute Force technique.
  • Since these algorithms inhibit parallelism, it does not involve any modification and is handled by systems incorporating parallel processing.

Disadvantages of Divide and Conquer

  • Since most of its algorithms are designed by incorporating recursion, so it necessitates high memory management.
  • An explicit stack may overuse the space.
  • It may even crash the system if the recursion is performed rigorously greater than the stack present in the CPU.

Binary Search – Data Structure and Algorithm Tutorials


Conditions for when to apply Binary Search in a Data Structure:

To apply Binary Search algorithm:

  • The data structure must be sorted.
  • Access to any element of the data structure takes constant time.

Binary Search Algorithm:

In this algorithm, 

finding the middle index "mid" in Binary Search Algorithm

Finding the middle index “mid” in Binary Search Algorithm

  • Compare the middle element of the search space with the key. 
  • If the key is found at middle element, the process is terminated.
  • If the key is not found at middle element, choose which half will be used as the next search space.
    • If the key is smaller than the middle element, then the left side is used for next search.
    • If the key is larger than the middle element, then the right side is used for next search.
  • This process is continued until the key is found or the total search space is exhausted.

Consider an array arr[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}, and the target = 23.

First Step: Calculate the mid and compare the mid element with the key. If the key is less than mid element, move to left and if it is greater than the mid then move search space to the right.

  • Key (i.e., 23) is greater than current mid element (i.e., 16). The search space moves to the right.
Binary Search Algorithm : Compare key with 16

Binary Search Algorithm : Compare key with 16

  • Key is less than the current mid 56. The search space moves to the left.
Binary Search Algorithm : Compare key with 56

Binary Search Algorithm : Compare key with 56

Second Step: If the key matches the value of the mid element, the element is found and stop search.


binary-search-step-3

// C++ program to implement iterative Binary Search

#include <bits/stdc++.h>

using namespace std;


// An iterative binary search function.

int binarySearch(int arr[], int l, int r, int x)

{

while (l <= r) {

int m = l + (r - l) / 2;


// Check if x is present at mid

if (arr[m] == x)

return m;


// If x greater, ignore left half

if (arr[m] < x)

l = m + 1;


// If x is smaller, ignore right half

else

r = m - 1;

}


// If we reach here, then element was not present

return -1;

}


// Driver code

int main(void)

{

int arr[] = { 2, 3, 4, 10, 40 };

int x = 10;

int n = sizeof(arr) / sizeof(arr[0]);

int result = binarySearch(arr, 0, n - 1, x);

(result == -1)

? cout << "Element is not present in array"

: cout << "Element is present at index " << result;

return 0;

}


Output
Element is present at index 3
Finding the maximum and minimum number

// C++ implementation of the above approach

#include <bits/stdc++.h>

using namespace std;


struct Pair {

int min;

int max;

};


Pair getMinMax(int arr[], int n)

{

Pair minmax;


sort(arr, arr + n);


minmax.min = arr[0];

minmax.max = arr[n - 1];


return minmax;

}


int main()

{

int arr[] = { 1000, 11, 445, 1, 330, 3000 };

int arr_size = sizeof(arr) / sizeof(arr[0]);


Pair minmax = getMinMax(arr, arr_size);


cout << "Minimum element is " << minmax.min << endl;

cout << "Maximum element is " << minmax.max << endl;


return 0;

}


Merge Sort – Data Structure and Algorithms

  In simple terms, we can say that the process of merge sort is to divide the array into two halves, sort each half, and then merge the sorted halves back together. This process is repeated until the entire array is sorted.

Merge-Sort-Algorithm-(1)

Merge Sort Algorithm

How does Merge Sort work?

Merge sort is a recursive algorithm that continuously splits the array in half until it cannot be further divided i.e., the array has only one element left (an array with one element is always sorted). Then the sorted subarrays are merged into one sorted array.

// C++ program for Merge Sort

#include <bits/stdc++.h>

using namespace std;


// Merges two subarrays of array[].

// First subarray is arr[begin..mid]

// Second subarray is arr[mid+1..end]

void merge(int array[], int const left, int const mid,

int const right)

{

int const subArrayOne = mid - left + 1;

int const subArrayTwo = right - mid;


// Create temp arrays

auto *leftArray = new int[subArrayOne],

*rightArray = new int[subArrayTwo];


// Copy data to temp arrays leftArray[] and rightArray[]

for (auto i = 0; i < subArrayOne; i++)

leftArray[i] = array[left + i];

for (auto j = 0; j < subArrayTwo; j++)

rightArray[j] = array[mid + 1 + j];


auto indexOfSubArrayOne = 0, indexOfSubArrayTwo = 0;

int indexOfMergedArray = left;


// Merge the temp arrays back into array[left..right]

while (indexOfSubArrayOne < subArrayOne

&& indexOfSubArrayTwo < subArrayTwo) {

if (leftArray[indexOfSubArrayOne]

<= rightArray[indexOfSubArrayTwo]) {

array[indexOfMergedArray]

= leftArray[indexOfSubArrayOne];

indexOfSubArrayOne++;

}

else {

array[indexOfMergedArray]

= rightArray[indexOfSubArrayTwo];

indexOfSubArrayTwo++;

}

indexOfMergedArray++;

}


// Copy the remaining elements of

// left[], if there are any

while (indexOfSubArrayOne < subArrayOne) {

array[indexOfMergedArray]

= leftArray[indexOfSubArrayOne];

indexOfSubArrayOne++;

indexOfMergedArray++;

}


// Copy the remaining elements of

// right[], if there are any

while (indexOfSubArrayTwo < subArrayTwo) {

array[indexOfMergedArray]

= rightArray[indexOfSubArrayTwo];

indexOfSubArrayTwo++;

indexOfMergedArray++;

}

delete[] leftArray;

delete[] rightArray;

}


// begin is for left index and end is right index

// of the sub-array of arr to be sorted

void mergeSort(int array[], int const begin, int const end)

{

if (begin >= end)

return;


int mid = begin + (end - begin) / 2;

mergeSort(array, begin, mid);

mergeSort(array, mid + 1, end);

merge(array, begin, mid, end);

}


// UTILITY FUNCTIONS

// Function to print an array

void printArray(int A[], int size)

{

for (int i = 0; i < size; i++)

cout << A[i] << " ";

cout << endl;

}


// Driver code

int main()

{

int arr[] = { 12, 11, 13, 5, 6, 7 };

int arr_size = sizeof(arr) / sizeof(arr[0]);


cout << "Given array is \n";

printArray(arr, arr_size);


mergeSort(arr, 0, arr_size - 1);


cout << "\nSorted array is \n";

printArray(arr, arr_size);

return 0;

}


 Quick sort

// C++ Implementation of the Quick Sort Algorithm.

#include <iostream>

using namespace std;


int partition(int arr[], int start, int end)

{


int pivot = arr[start];


int count = 0;

for (int i = start + 1; i <= end; i++) {

if (arr[i] <= pivot)

count++;

}


// Giving pivot element its correct position

int pivotIndex = start + count;

swap(arr[pivotIndex], arr[start]);


// Sorting left and right parts of the pivot element

int i = start, j = end;


while (i < pivotIndex && j > pivotIndex) {


while (arr[i] <= pivot) {

i++;

}


while (arr[j] > pivot) {

j--;

}


if (i < pivotIndex && j > pivotIndex) {

swap(arr[i++], arr[j--]);

}

}


return pivotIndex;

}


void quickSort(int arr[], int start, int end)

{


// base case

if (start >= end)

return;


// partitioning the array

int p = partition(arr, start, end);


// Sorting the left part

quickSort(arr, start, p - 1);


// Sorting the right part

quickSort(arr, p + 1, end);

}


int main()

{


int arr[] = { 9, 3, 4, 2, 1, 8 };

int n = 6;


quickSort(arr, 0, n - 1);


for (int i = 0; i < n; i++) {

cout << arr[i] << " ";

}


return 0;

}

Output
1 2 3 4 8 9 

Selection Sort – Data Structure and Algorithm



The algorithm repeatedly selects the smallest (or largest) element from the unsorted portion of the list and swaps it with the first element of the unsorted part. This process is repeated for the remaining unsorted portion until the entire list is sorted. 

How does Selection Sort Algorithm work?

Lets consider the following array as an example: arr[] = {64, 25, 12, 22, 11}

First pass:

  • For the first position in the sorted array, the whole array is traversed from index 0 to 4 sequentially. The first position where 64 is stored presently, after traversing whole array it is clear that 11 is the lowest value.
  • Thus, replace 64 with 11. After one iteration 11, which happens to be the least value in the array, tends to appear in the first position of the sorted list.
Selection Sort Algorithm | Swapping 1st element with the minimum in array

Selection Sort Algorithm | Swapping 1st element with the minimum in array

Second Pass:

  • For the second position, where 25 is present, again traverse the rest of the array in a sequential manner.
  • After traversing, we found that 12 is the second lowest value in the array and it should appear at the second place in the array, thus swap these values.
Selection Sort Algorithm | swapping i=1 with the next minimum element

Selection Sort Algorithm | swapping i=1 with the next minimum element

Third Pass:

  • Now, for third place, where 25 is present again traverse the rest of the array and find the third least value present in the array.
  • While traversing, 22 came out to be the third least value and it should appear at the third place in the array, thus swap 22 with element present at third position.
Selection Sort Algorithm | swapping i=3 with the next minimum element

Selection Sort Algorithm | swapping i=2 with the next minimum element

Fourth pass:

  • Similarly, for fourth position traverse the rest of the array and find the fourth least element in the array 
  • As 25 is the 4th lowest value hence, it will place at the fourth position.
Selection Sort Algorithm | swapping i=3 with the next minimum element

Selection Sort Algorithm | swapping i=3 with the next minimum element

Fifth Pass:

  • At last the largest value present in the array automatically get placed at the last position in the array
  • The resulted array is the sorted array.
Selection Sort Algorithm | Required sorted array

Strassen's matrix multiplication problem

#include <bits/stdc++.h> using namespace std; #define ROW_1 4 #define COL_1 4 #define ROW_2 4 #define COL_2 4 void print(string display, vector<vector<int> > matrix, int start_row, int start_column, int end_row, int end_column) { cout << endl << display << " =>" << endl; for (int i = start_row; i <= end_row; i++) { for (int j = start_column; j <= end_column; j++) { cout << setw(10); cout << matrix[i][j]; } cout << endl; } cout << endl; return; } void add_matrix(vector<vector<int> > matrix_A, vector<vector<int> > matrix_B, vector<vector<int> >& matrix_C, int split_index) { for (auto i = 0; i < split_index; i++) for (auto j = 0; j < split_index; j++) matrix_C[i][j] = matrix_A[i][j] + matrix_B[i][j]; } vector<vector<int> > multiply_matrix(vector<vector<int> > matrix_A, vector<vector<int> > matrix_B) { int col_1 = matrix_A[0].size(); int row_1 = matrix_A.size(); int col_2 = matrix_B[0].size(); int row_2 = matrix_B.size(); if (col_1 != row_2) { cout << "\nError: The number of columns in Matrix " "A must be equal to the number of rows in " "Matrix B\n"; return {}; } vector<int> result_matrix_row(col_2, 0); vector<vector<int> > result_matrix(row_1, result_matrix_row); if (col_1 == 1) result_matrix[0][0] = matrix_A[0][0] * matrix_B[0][0]; else { int split_index = col_1 / 2; vector<int> row_vector(split_index, 0); vector<vector<int> > result_matrix_00(split_index, row_vector); vector<vector<int> > result_matrix_01(split_index, row_vector); vector<vector<int> > result_matrix_10(split_index, row_vector); vector<vector<int> > result_matrix_11(split_index, row_vector); vector<vector<int> > a00(split_index, row_vector); vector<vector<int> > a01(split_index, row_vector); vector<vector<int> > a10(split_index, row_vector); vector<vector<int> > a11(split_index, row_vector); vector<vector<int> > b00(split_index, row_vector); vector<vector<int> > b01(split_index, row_vector); vector<vector<int> > b10(split_index, row_vector); vector<vector<int> > b11(split_index, row_vector); for (auto i = 0; i < split_index; i++) for (auto j = 0; j < split_index; j++) { a00[i][j] = matrix_A[i][j]; a01[i][j] = matrix_A[i][j + split_index]; a10[i][j] = matrix_A[split_index + i][j]; a11[i][j] = matrix_A[i + split_index] [j + split_index]; b00[i][j] = matrix_B[i][j]; b01[i][j] = matrix_B[i][j + split_index]; b10[i][j] = matrix_B[split_index + i][j]; b11[i][j] = matrix_B[i + split_index] [j + split_index]; } add_matrix(multiply_matrix(a00, b00), multiply_matrix(a01, b10), result_matrix_00, split_index); add_matrix(multiply_matrix(a00, b01), multiply_matrix(a01, b11), result_matrix_01, split_index); add_matrix(multiply_matrix(a10, b00), multiply_matrix(a11, b10), result_matrix_10, split_index); add_matrix(multiply_matrix(a10, b01), multiply_matrix(a11, b11), result_matrix_11, split_index); for (auto i = 0; i < split_index; i++) for (auto j = 0; j < split_index; j++) { result_matrix[i][j] = result_matrix_00[i][j]; result_matrix[i][j + split_index] = result_matrix_01[i][j]; result_matrix[split_index + i][j] = result_matrix_10[i][j]; result_matrix[i + split_index] [j + split_index] = result_matrix_11[i][j]; } result_matrix_00.clear(); result_matrix_01.clear(); result_matrix_10.clear(); result_matrix_11.clear(); a00.clear(); a01.clear(); a10.clear(); a11.clear(); b00.clear(); b01.clear(); b10.clear(); b11.clear(); } return result_matrix; } int main() { vector<vector<int> > matrix_A = { { 1, 1, 1, 1 }, { 2, 2, 2, 2 }, { 3, 3, 3, 3 }, { 2, 2, 2, 2 } }; print("Array A", matrix_A, 0, 0, ROW_1 - 1, COL_1 - 1); vector<vector<int> > matrix_B = { { 1, 1, 1, 1 }, { 2, 2, 2, 2 }, { 3, 3, 3, 3 }, { 2, 2, 2, 2 } }; print("Array B", matrix_B, 0, 0, ROW_2 - 1, COL_2 - 1); vector<vector<int> > result_matrix( multiply_matrix(matrix_A, matrix_B)); print("Result Array", result_matrix, 0, 0, ROW_1 - 1, COL_2 - 1); } // Time Complexity: O(n^3) // Code Contributed By: lucasletum

Output
Array A =>
         1         1         1         1
         2         2         2         2
         3         3         3         3
         2         2         2         2


Array B =>
         1         1         1         1
         2         2         2         2
         3         3         3         3
         2         2         2         2


Result Array =>
         8         8         8         8
        16        16        16        16
        24        24        24        24
        16        16        16        16

Convex Hull using Divide and Conquer Algorithm

A convex hull is the smallest convex polygon containing all the given points.

convexHull

Input is an array of points specified by their x and y coordinates. The output is the convex hull of this set of points. Examples:

Input : points[] = {(0, 0), (0, 4), (-4, 0), (5, 0), 
(0, -6), (1, 0)};
Output : (-4, 0), (5, 0), (0, -6), (0, 4)

Pre-requisite: Tangents between two convex polygons Algorithm: Given the set of points for which we have to find the convex hull. Suppose we know the convex hull of the left half points and the right half points, then the problem now is to merge these two convex hulls and determine the convex hull for the complete set. This can be done by finding the upper and lower tangent to the right and left convex hulls. This is illustrated here Tangents between two convex polygons Let the left convex hull be a and the right convex hull be b. Then the lower and upper tangents are named as 1 and 2 respectively, as shown in the figure. Then the red outline shows the final convex hull.Now the problem remains, how to find the convex hull for the left and right half. Now recursion comes into the picture, we divide the set of points until the number of points in the set is very small, say 5, and we can find the convex hull for these points by the brute algorithm. The merging of these halves would result in the convex hull for the complete set of points. Note: We have used the brute algorithm to find the convex hull for a small number of points and it has a time complexity of O(n^3). But some people suggest the following, the convex hull for 3 or fewer points is the complete set of points. This is correct but the problem comes when we try to merge a left convex hull of 2 points and right convex hull of 3 points, then the program gets trapped in an infinite loop in some special cases. So, to get rid of this problem I directly found the convex hull for 5 or fewer points by O(n^3)algorithm, which is somewhat greater but does not affect the overall complexity of the algorithm. 

// A divide and conquer program to find convex

// hull of a given set of points.

#include<bits/stdc++.h>

using namespace std;


// stores the centre of polygon (It is made

// global because it is used in compare function)

pair<int, int> mid;


// determines the quadrant of a point

// (used in compare())

int quad(pair<int, int> p)

{

if (p.first >= 0 && p.second >= 0)

return 1;

if (p.first <= 0 && p.second >= 0)

return 2;

if (p.first <= 0 && p.second <= 0)

return 3;

return 4;

}


// Checks whether the line is crossing the polygon

int orientation(pair<int, int> a, pair<int, int> b,

pair<int, int> c)

{

int res = (b.second-a.second)*(c.first-b.first) -

(c.second-b.second)*(b.first-a.first);


if (res == 0)

return 0;

if (res > 0)

return 1;

return -1;

}


// compare function for sorting

bool compare(pair<int, int> p1, pair<int, int> q1)

{

pair<int, int> p = make_pair(p1.first - mid.first,

p1.second - mid.second);

pair<int, int> q = make_pair(q1.first - mid.first,

q1.second - mid.second);


int one = quad(p);

int two = quad(q);


if (one != two)

return (one < two);

return (p.second*q.first < q.second*p.first);

}


// Finds upper tangent of two polygons 'a' and 'b'

// represented as two vectors.

vector<pair<int, int>> merger(vector<pair<int, int> > a,

vector<pair<int, int> > b)

{

// n1 -> number of points in polygon a

// n2 -> number of points in polygon b

int n1 = a.size(), n2 = b.size();


int ia = 0, ib = 0;

for (int i=1; i<n1; i++)

if (a[i].first > a[ia].first)

ia = i;


// ib -> leftmost point of b

for (int i=1; i<n2; i++)

if (b[i].first < b[ib].first)

ib=i;


// finding the upper tangent

int inda = ia, indb = ib;

bool done = 0;

while (!done)

{

done = 1;

while (orientation(b[indb], a[inda], a[(inda+1)%n1]) >=0)

inda = (inda + 1) % n1;


while (orientation(a[inda], b[indb], b[(n2+indb-1)%n2]) <=0)

{

indb = (n2+indb-1)%n2;

done = 0;

}

}


int uppera = inda, upperb = indb;

inda = ia, indb=ib;

done = 0;

int g = 0;

while (!done)//finding the lower tangent

{

done = 1;

while (orientation(a[inda], b[indb], b[(indb+1)%n2])>=0)

indb=(indb+1)%n2;


while (orientation(b[indb], a[inda], a[(n1+inda-1)%n1])<=0)

{

inda=(n1+inda-1)%n1;

done=0;

}

}


int lowera = inda, lowerb = indb;

vector<pair<int, int>> ret;


//ret contains the convex hull after merging the two convex hulls

//with the points sorted in anti-clockwise order

int ind = uppera;

ret.push_back(a[uppera]);

while (ind != lowera)

{

ind = (ind+1)%n1;

ret.push_back(a[ind]);

}


ind = lowerb;

ret.push_back(b[lowerb]);

while (ind != upperb)

{

ind = (ind+1)%n2;

ret.push_back(b[ind]);

}

return ret;


}


// Brute force algorithm to find convex hull for a set

// of less than 6 points

vector<pair<int, int>> bruteHull(vector<pair<int, int>> a)

{

// Take any pair of points from the set and check

// whether it is the edge of the convex hull or not.

// if all the remaining points are on the same side

// of the line then the line is the edge of convex

// hull otherwise not

set<pair<int, int> >s;


for (int i=0; i<a.size(); i++)

{

for (int j=i+1; j<a.size(); j++)

{

int x1 = a[i].first, x2 = a[j].first;

int y1 = a[i].second, y2 = a[j].second;


int a1 = y1-y2;

int b1 = x2-x1;

int c1 = x1*y2-y1*x2;

int pos = 0, neg = 0;

for (int k=0; k<a.size(); k++)

{

if (a1*a[k].first+b1*a[k].second+c1 <= 0)

neg++;

if (a1*a[k].first+b1*a[k].second+c1 >= 0)

pos++;

}

if (pos == a.size() || neg == a.size())

{

s.insert(a[i]);

s.insert(a[j]);

}

}

}


vector<pair<int, int>>ret;

for (auto e:s)

ret.push_back(e);


// Sorting the points in the anti-clockwise order

mid = {0, 0};

int n = ret.size();

for (int i=0; i<n; i++)

{

mid.first += ret[i].first;

mid.second += ret[i].second;

ret[i].first *= n;

ret[i].second *= n;

}

sort(ret.begin(), ret.end(), compare);

for (int i=0; i<n; i++)

ret[i] = make_pair(ret[i].first/n, ret[i].second/n);


return ret;

}


// Returns the convex hull for the given set of points

vector<pair<int, int>> divide(vector<pair<int, int>> a)

{

// If the number of points is less than 6 then the

// function uses the brute algorithm to find the

// convex hull

if (a.size() <= 5)

return bruteHull(a);


// left contains the left half points

// right contains the right half points

vector<pair<int, int>>left, right;

for (int i=0; i<a.size()/2; i++)

left.push_back(a[i]);

for (int i=a.size()/2; i<a.size(); i++)

right.push_back(a[i]);


// convex hull for the left and right sets

vector<pair<int, int>>left_hull = divide(left);

vector<pair<int, int>>right_hull = divide(right);


// merging the convex hulls

return merger(left_hull, right_hull);

}


// Driver code

int main()

{

vector<pair<int, int> > a;

a.push_back(make_pair(0, 0));

a.push_back(make_pair(1, -4));

a.push_back(make_pair(-1, -5));

a.push_back(make_pair(-5, -3));

a.push_back(make_pair(-3, -1));

a.push_back(make_pair(-1, -3));

a.push_back(make_pair(-2, -2));

a.push_back(make_pair(-1, -1));

a.push_back(make_pair(-2, -1));

a.push_back(make_pair(-1, 1));


int n = a.size();


// sorting the set of points according

// to the x-coordinate

sort(a.begin(), a.end());

vector<pair<int, int> >ans = divide(a);


cout << "convex hull:\n";

for (auto e:ans)

cout << e.first << " "

<< e.second << endl;


return 0;

}

Output
convex hull:
-5 -3
-1 -5
1 -4
0 0
-1 1

0/1 kanpsack problem


/* A Naive recursive implementation of 0-1 Knapsack problem */ #include <bits/stdc++.h> using namespace std; // A utility function that returns // maximum of two integers int max(int a, int b) { return (a > b) ? a : b; } // Returns the maximum value that // can be put in a knapsack of capacity W int knapSack(int W, int wt[], int val[], int n) { // Base Case if (n == 0 || W == 0) return 0; // If weight of the nth item is more // than Knapsack capacity W, then // this item cannot be included // in the optimal solution if (wt[n - 1] > W) return knapSack(W, wt, val, n - 1); // Return the maximum of two cases: // (1) nth item included // (2) not included else return max( val[n - 1] + knapSack(W - wt[n - 1], wt, val, n - 1), knapSack(W, wt, val, n - 1)); } // Driver code int main() { int profit[] = { 60, 100, 120 }; int weight[] = { 10, 20, 30 }; int W = 50; int n = sizeof(profit) / sizeof(profit[0]); cout << knapSack(W, weight, profit, n); return 0; } // This code is contributed by rathbhupendra












 

5 comments:

02

Capstone resource hub

Codingacharya

Capstone Learning Resources, Notes & Project Hub

TCS NQT Questions
Read Notes
Machine Learning – ACE Theory
Read Notes
Machine Learning PPT
Read Notes
MachienLearning LAB
Read Notes
CSPT LAB programs
Read Notes
Time table and CSPT syllabus
Read Notes
Appreciations
Read Notes
ISTE life memberships
Read Notes
Artificial Intelligence & Analytics
Read Notes
Fullstack Web Dev
Read Notes
MERN Web Dev
Read Notes
Course Structure
Read Notes
Cloud Computing
Read Notes
90 Days ML Challenge
Read Notes
Advanced Analytics & Viz
Read Notes
Advanced Machine Learning
Read Notes
React JS
Read Notes
ML Chaitanya
Read Notes
Important Links
Read Notes
CSS Effects
Read Notes
RESUME
Read Notes
Bootstrap CSS
Read Notes
MongoDB
Read Notes
OWN Python Package
Read Notes
HTML Course
Read Notes
HTML Projects
Read Notes
GitHub Projects
Read Notes
Angular JS
Read Notes
Journals
Read Notes
NLP Notes
Read Notes
Videos
Read Notes
Data Analytics & Viz
Read Notes
Cloud Computing (Archive)
Read Notes
Open CV
Read Notes
jQuery
Read Notes
React JS (Archive)
Read Notes
Node JS
Read Notes
DAV Theory
Read Notes
DAV Lab
Read Notes
Big Data Notes
Read Notes
R-Programming
Read Notes
HADOOP Lab
Read Notes
GATE DA
Read Notes
JAVA Lab
Read Notes
Computer Networks
Read Notes
03

Live projects & profiles