Loading...

DESIGN & ANALYSIS OF ALGORITHMS  

>

LEARNING OUTCOME 4

SEARCHING

Searching is the process of finding a specific element within a collection of elements. It is a fundamental operation in computer science with various applications in data retrieval, database systems, and problem-solving.

Needs for Searching Algorithms

SEARCHING ALGORITHMS

Linear Search

Linear search sequentially examines each element in a collection until the target element is found or the end of the collection is reached.

Linear Search Algorithm

A linear search algorithm is a simple searching algorithm that sequentially checks each element of a list until a match is found or the entire list has been searched.

Here's a step-by-step explanation:

  1. Start from the beginning: Begin at the first element of the list.
  2. Compare: Compare the current element with the target value.
  3. Match found: If the current element matches the target value, return the index of the element.
  4. Move to the next element: If the current element doesn't match, move to the next element in the list.
  5. Repeat: Repeat steps 2-4 until a match is found or the end of the list is reached.
  6. Not found: If the end of the list is reached without finding a match, return an indication that the target value is not in the list (e.g., -1).

Implementation in C++:

		
		#include <iostream>
		using namespace std;
		
		int linearSearch(int arr[], int size, int target) {
			for (int i = 0; i < size; i++) {
				if (arr[i] == target) {
					return i; // Return the index of the target
				}
			}
			return -1; // Target not found
		}
		
		int main() {
			int arr[] = {2, 4, 6, 8, 10};
			int target = 6;
			int size = sizeof(arr) / sizeof(arr[0]);
			int result = linearSearch(arr, size, target);
			if (result != -1) {
				cout << "Element found at index: " << result << endl;
			} else {
				cout << "Element not found" << endl;
			}
			return 0;
		}
		
		

Time Complexity

Space Complexity

Binary Search

Binary search is a more efficient searching algorithm that works on sorted collections. It repeatedly divides the search space in half until the target element is found or the search space is empty.

Binary search is a more efficient algorithm for searching in a sorted array. It works by repeatedly dividing the search interval in half.

Here's how it works:

  1. Find the middle element: Determine the middle index of the search interval.
  2. Compare: Compare the middle element with the target value.
  3. Match found: If the middle element matches the target value, return the index.
  4. Adjust the search interval:
    • If the middle element is greater than the target value: discard the right half of the interval.
    • If the middle element is less than the target value: discard the left half of the interval.
  5. Repeat: Repeat steps 1-4 until the target value is found or the search interval becomes empty.

Implementation in C++

Code
// C++ implementation of binary search
int binarySearch(int arr[], int size, int target) {
int left = 0, right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] > target) right = mid - 1;
else left = mid + 1;
}
return -1;
}

Binary Search in C++

This code implements the binary search algorithm in C++ to find a specific element in a sorted array.

C++ Code:

#include <iostream>

using namespace std;

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 is 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, element was not present
    return -1;
}

int main() {
    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);

    if (result == -1)
        cout << "Element is not present in array" << endl;
    else
        cout << "Element is present at index " << result << endl;

    return 0;
}
        

Explanation:

The binarySearch function efficiently searches for an element x in a sorted array arr. It works by repeatedly dividing the search interval in half. If the middle element is x, the function returns its index. If x is greater than the middle element, the search continues in the right half; otherwise, the search continues in the left half.

How it works (Step-by-step example with the code's data):

Let's trace the execution with x = 10 and arr = {2, 3, 4, 10, 40}:

  1. Initial call: binarySearch(arr, 0, 4, 10)
  2. Middle index m = 0 + (4 - 0) / 2 = 2. arr[2] = 4. Since 10 > 4, search the right half.
  3. Next call: binarySearch(arr, 3, 4, 10)
  4. Middle index m = 3 + (4 - 3) / 2 = 3. arr[3] = 10. Since 10 == 10, the element is found at index 3.
  5. The function returns 3.

Time Complexity

Space Complexity

Comparison of Searching Algorithms

Feature Linear Search Binary Search
Time complexity O(n) in the worst case O(log n) in the worst case
Space complexity O(1) O(1)
Precondition No specific requirements Collection must be sorted
Efficiency Less efficient for large collections More efficient for large sorted collections
Use cases Suitable for unsorted collections or when the collection size is small Suitable for large sorted collections
Implementation Simple to implement Requires sorting the collection beforehand
Applications Widely used in various applications Used in databases, search engines, and other applications where efficiency is critical

SORTING

Sorting is the process of arranging elements in a specific order, typically ascending or descending. It is a fundamental operation in computer science with various applications in data processing, database systems, and algorithm design.

In-Place vs. Not In-Place Sorting

Feature In-Place Sorting Not In-Place Sorting
Definition Sorts elements within the original array, using a constant amount of extra space. Requires additional space (usually proportional to the input size) to sort the elements.
Examples Bubble Sort, Insertion Sort, Selection Sort, Heap Sort, Quick Sort (in-place implementation) Merge Sort
Space Complexity O(1) O(n) or more
Advantages Efficient in terms of space usage, especially for large datasets. Can often be more efficient in terms of time complexity.
Disadvantages Can be less efficient in terms of time complexity for some algorithms. Requires additional memory, which can be a limitation for large datasets.

Stable Sorting

Unstable Sorting

Adaptive Sorting

Non-Adaptive Sorting

Sorting Algorithms

Bubble Sort

Bubble Sort is a simple sorting algorithm that repeatedly iterates through a list, compares adjacent elements, and swaps them if they are in the wrong order. It's like bubbling the largest elements to the top. While simple to understand, it's inefficient for large datasets due to its quadratic time complexity.

#include <iostream>
		#include <vector>
		
		void bubbleSort(std::vector<int>& arr) {
			int n = arr.size();
			for (int i = 0; i < n - 1; i++) {
				bool swapped = false; // Optimization: Check if any swaps occurred in this pass
				for (int j = 0; j < n - i - 1; j++) {
					if (arr[j] > arr[j + 1]) {
						// Swap arr[j] and arr[j+1]
						std::swap(arr[j], arr[j + 1]);
						swapped = true;
					}
				}
		
				// If no swaps occurred in this pass, the array is sorted
				if (swapped == false) {
					break;
				}
			}
		}
		
		int main() {
			std::vector<int> data = {5, 1, 4, 2, 8};
		
			std::cout << "Unsorted array: ";
			for (int x : data) {
				std::cout << x << " ";
			}
			std::cout << std::endl;
		
			bubbleSort(data);
		
			std::cout << "Sorted array: ";
			for (int x : data) {
				std::cout << x << " ";
			}
			std::cout << std::endl;
		
			return 0;
		}
		

Selection Sort

Selection Sort involves finding the minimum element in the unsorted part of the array and swapping it with the first element. This process is repeated for the remaining unsorted portion until the entire array is sorted. It's less efficient than Bubble Sort in terms of time complexity.

#include <iostream>
	#include <vector>
	
	void selectionSort(std::vector<int>& arr) {
		int n = arr.size();
	
		for (int i = 0; i < n - 1; i++) {
			// Find the minimum element in the unsorted part
			int minIndex = i;
			for (int j = i + 1; j < n; j++) {
				if (arr[j] < arr[minIndex]) {
					minIndex = j;
				}
			}
	
			// Swap the found minimum element with the first element of the unsorted part
			std::swap(arr[i], arr[minIndex]);
		}
	}
	
	int main() {
		std::vector<int> data = {64, 25, 12, 22, 11};
	
		std::cout << "Unsorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		selectionSort(data);
	
		std::cout << "Sorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		return 0;
	}
	

Insertion Sort

Insertion Sort builds the sorted array one element at a time. It iterates through the input array, taking one element at a time and inserting it into its correct position in the sorted portion of the array. It's efficient for small datasets and partially sorted arrays.

#include <iostream>
	#include <vector>
	
	void insertionSort(std::vector<int>& arr) {
		int n = arr.size();
		for (int i = 1; i < n; i++) {
			int key = arr[i]; // The current element to be inserted
			int j = i - 1;   // Index of the element to compare with key
	
			// Move elements of arr[0..i-1], that are greater than key, 
			// to one position ahead of their current position.
			// This loop makes space for the 'key' to be inserted in correct position
			while (j >= 0 && arr[j] > key) {
				arr[j + 1] = arr[j];
				j = j - 1;
			}
			arr[j + 1] = key; // Insert the 'key' in its correct position
		}
	}
	
	int main() {
		std::vector<int> data = {12, 11, 13, 5, 6};
	
		std::cout << "Unsorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		insertionSort(data);
	
		std::cout << "Sorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		return 0;
	}
	

Shell Sort

Shell Sort is an improved version of Insertion Sort. It sorts elements that are far apart from each other, then progressively reduces the gap between elements to be compared. This technique allows elements to move larger distances in fewer iterations, making it more efficient than simple Insertion Sort.

#include <iostream>
		#include <vector>
		
		void shellSort(std::vector<int>& arr) {
			int n = arr.size();
		
			// Start with a large gap, then reduce the gap
			for (int gap = n / 2; gap > 0; gap /= 2) {
				// Do a gapped insertion sort for this gap size.
				for (int i = gap; i < n; i++) {
					int temp = arr[i];
					int j;
					for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
						arr[j] = arr[j - gap];
					}
					arr[j] = temp;
				}
			}
		}
		
		int main() {
			std::vector<int> data = {12, 34, 54, 2, 52, 9, 64, 3};
		
			std::cout << "Unsorted array: ";
			for (int x : data) {
				std::cout << x << " ";
			}
			std::cout << std::endl;
		
			shellSort(data);
		
			std::cout << "Sorted array: ";
			for (int x : data) {
				std::cout << x << " ";
			}
			std::cout << std::endl;
		
			return 0;
		}
		

Merge Sort

Merge Sort is a divide-and-conquer algorithm that recursively divides the array into two halves, sorts each half independently, and then merges the sorted halves back together. It's a highly efficient sorting algorithm with a time complexity of O(n log n).

#include <iostream>
			#include <vector>
			
			// Merges two sorted subarrays arr[l..m] and arr[m+1..r]
			void merge(std::vector<int>& arr, int l, int m, int r) {
				int n1 = m - l + 1; // Size of the left subarray
				int n2 = r - m;     // Size of the right subarray
			
				// Create temporary arrays
				std::vector<int> L(n1);
				std::vector<int> R(n2);
			
				// Copy data to temporary arrays L[] and R[]
				for (int i = 0; i < n1; i++) {
					L[i] = arr[l + i];
				}
				for (int j = 0; j < n2; j++) {
					R[j] = arr[m + 1 + j];
				}
			
				// Merge the temporary arrays back into arr[l..r]
				int i = 0;   // Initial index of first subarray
				int j = 0;   // Initial index of second subarray
				int k = l;   // Initial index of merged subarray
				while (i < n1 && j < n2) {
					if (L[i] <= R[j]) {
						arr[k] = L[i];
						i++;
					} else {
						arr[k] = R[j];
						j++;
					}
					k++;
				}
			
				// Copy the remaining elements of L[], if there are any
				while (i < n1) {
					arr[k] = L[i];
					i++;
					k++;
				}
			
				// Copy the remaining elements of R[], if there are any
				while (j < n2) {
					arr[k] = R[j];
					j++;
					k++;
				}
			}
			
			// Main function that sorts arr[l..r] using merge()
			void mergeSort(std::vector<int>& arr, int l, int r) {
				if (l < r) {
					int m = l + (r - l) / 2; // Calculate the middle point
			
					// Sort first and second halves
					mergeSort(arr, l, m);
					mergeSort(arr, m + 1, r);
			
					merge(arr, l, m, r); // Merge the sorted halves
				}
			}
			
			int main() {
				std::vector<int> data = {12, 11, 13, 5, 6, 7};
			
				std::cout << "Unsorted array: ";
				for (int x : data) {
					std::cout << x << " ";
				}
				std::cout << std::endl;
			
				mergeSort(data, 0, data.size() - 1); // Sort the entire array
			
				std::cout << "Sorted array: ";
				for (int x : data) {
					std::cout << x << " ";
				}
				std::cout << std::endl;
			
				return 0;
			}
			

Heap Sort

Heap Sort is a comparison-based sorting technique that uses a binary heap data structure. It builds a heap from the input array, then repeatedly extracts the maximum element from the heap and places it at the end of the sorted portion of the array. It's efficient and has a time complexity of O(n log n).

#include <iostream>
	#include <vector>
	#include <algorithm> // for std::swap (or you can write your own swap function)
	
	// Function to heapify a subtree rooted at index i
	void heapify(std::vector<int>& arr, int n, int i) {
		int largest = i;  // Initialize largest as root
		int left = 2 * i + 1;  // left = 2*i + 1
		int right = 2 * i + 2; // right = 2*i + 2
	
		// If left child is larger than root
		if (left < n && arr[left] > arr[largest]) {
			largest = left;
		}
	
		// If right child is larger than root
		if (right < n && arr[right] > arr[largest]) {
			largest = right;
		}
	
		// If largest is not root
		if (largest != i) {
			std::swap(arr[i], arr[largest]); // Swap the root with the largest element
	
			// Recursively heapify the affected subtree
			heapify(arr, n, largest);
		}
	}
	
	void heapSort(std::vector<int>& arr) {
		int n = arr.size();
	
		// Build heap (rearrange array)
		// Since last parent will be at ((n/2)-1) we can start at that location.
		for (int i = n / 2 - 1; i >= 0; i--) {
			heapify(arr, n, i);
		}
	
		// One by one extract elements from heap
		for (int i = n - 1; i > 0; i--) {
			std::swap(arr[0], arr[i]); // Move current root to end
			heapify(arr, i, 0);       // call max heapify on the reduced heap
		}
	}
	
	int main() {
		std::vector<int> data = {12, 11, 13, 5, 6, 7};
	
		std::cout << "Unsorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		heapSort(data);
	
		std::cout << "Sorted array: ";
		for (int x : data) {
			std::cout << x << " ";
		}
		std::cout << std::endl;
	
		return 0;
	}
	
Feature Bubble Sort 🐘 Selection Sort 🐬 Insertion Sort 🦍 Shell Sort 🦅 Merge Sort 🦁 Heap Sort 🐺
In-place Yes Yes Yes Yes No Yes
Stable Yes No Yes No Yes No
Adaptive Yes No Yes Yes No No
Time complexity (worst case) O(n^2) O(n^2) O(n^2) O(n^2) O(n log n) O(n log n)
Time complexity (average case) O(n^2) O(n^2) O(n^2) O(n^1.5) O(n log n) O(n log n)
Time complexity (best case) O(n) O(n^2) O(n) O(n) O(n log n) O(n log n)
Space complexity O(1) O(1) O(1) O(1) O(n) O(1)
Use cases Small datasets, simple implementation Small datasets, simple implementation Small datasets, partially sorted data Medium to large datasets Large datasets, stable sorting Large datasets, efficient performance
Advantages Simple to implement, stable Simple to implement Efficient for partially sorted data, stable Efficient for medium to large datasets Stable, efficient for large datasets Efficient for large datasets, in-place
Disadvantages Inefficient for large datasets Inefficient for large datasets Inefficient for large datasets Can be complex to implement Requires additional memory Can be complex to implement

End of Outcome Quiz

1 of 20

    Quiz Score

    Percentage: 0%

    Answered Questions: 0

    Correct Answers: 0

    Faults: