Unleash the Power of Searching: A Journey through Popular Searching Algorithms – Part 1

Muhammad Saimon Uddin

21 September, 2024

Have you ever lost your keys in your messy room, only to find them hanging on the doorknob later? Or maybe you’ve searched through a cluttered fridge for a slice of pizza, only to see it right in front of you? We’ve all been there. Even Sherlock Holmes would struggle with such messy searches. But fear not, fellow detective wannabes! Today, we’re exploring the world of Searching Algorithms. These tools will help you find things faster and easier, whether it’s a needle in a haystack or just a data element in a list. We’ll discover seven legendary searching techniques: Linear Search, Binary Search, Jump Search, Ternary Search, Interpolation Search, Hashing, and Exponential Search. Buckle up because this is going to be an epic adventure!

The Quest for the Missing Sock — Linear Search

Imagine this: You wake up one morning, ready to conquer the world, only to find one of your favorite socks has gone missing! You know it’s somewhere in your messy drawer. You start searching from the top and rummage through every single item until, at last, you find it hidden beneath that old concert T-shirt. Congratulations, you’ve just performed a Linear Search!

What is Linear Search?

The Linear Search, the simplest of them all, is like the old-fashioned way of looking for something: you check every item individually until you find what you’re looking for or reach the end. It’s a straightforward and easy-to-implement method that works on sorted and unsorted lists, making it a comfortable choice for smaller datasets.

Algorithm
  1. Start from the first element and move through the list one by one.
  2. Compare each element with the target.
  3. If it matches, rejoice! You found your element.
  4. If the elements do not match and you reach the end of the list, return the component the algorithm could not find.
Code Example

Here’s how you can code this quest in Java:

				
					public class LinearSearch {
   public static int linearSearch(int[] arr, int target) {
       for (int i = 0; i < arr.length; i++) {
           if (arr[i] == target) {
               return i; // Target found, return the index
           }
       }
       return -1; // Target not found
   }

   public static void main(String[] args) {
       int[] drawer = {42, 13, 8, 64, 25, 61, 3, 28};
       int targetSock = 25;
       int index = linearSearch(drawer, targetSock);
      
       if (index != -1) {
           System.out.println("Found the missing sock at index: " + index);
       } else {
           System.out.println("Sock not found!");
       }
   }
}
				
			
Pros
  • Simple and easy to implement
  • Works on both sorted and unsorted lists
  • Suitable for smaller datasets
Cons
  • It can be prolonged for large data sets, as it examines every element in the worst case.
Complexity
  • Time Complexity: O(n) — In the worst case, the algorithm must scan the entire dataset, resulting in linear time complexity.
  • Space Complexity: O(1)—It  uses constant additional space regardless of the data set size.
The Mastermind Detective — Binary Search

Imagine a bookshelf with hundreds of books meticulously organized alphabetically. You are looking for a Java book. Instead of searching through each book individually, you decide to be a detective and use your deductive skills. You start by dividing the shelf in half. Is your book title likely in the first half or the second? By repeatedly halving the remaining books based on alphabetical order, you quickly pinpoint where your book might be without needing to check each title individually. Such is the art of Binary Search!
Binary Search, conversely, is a more efficient method that requires the data to be sorted. It works by repeatedly dividing the search interval in half, allowing you to pinpoint your target quickly without checking each element individually. This speed and efficiency make Binary Search an impressive and intriguing algorithm.

Algorithm
  1. Find the middle element of the list
  2. If the middle element is the target, then you’re done!
  3. If the target is smaller than the middle element, repeat the search on the left half
  4. If the target is larger, repeat the search on the right half
  5. Continue searching until you find the target or the subarray size becomes zero.
Code Example
				
					public class BinarySearch {
   public static int binarySearch(int[] array, int target) {
       int left = 0;
       int right = array.length - 1;
      
       while (left <= right) {
           int mid = left + (right-left) / 2;
          
            // Check if the target is present at mid
           if (array[mid] == target) {
               return mid;
           }

           // If the target is greater, ignore the left half
           if (array[mid] < target) {
               left = mid + 1;
           } else {
               // If the target is smaller, ignore the right half
               right = mid - 1;
           }
       }
      
       return -1; // target not found
   }

   public static void main(String[] args) {
       int[] sortedShelf = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
       int targetBook = 23;
       int result = binarySearch(sortedShelf, targetBook);
       if (result != -1) {
           System.out.println("Book found at index: " + result);
       } else {
           System.out.println("Book not found.");
       }
   }
}
				
			
Pros
  • It is much faster for large data collection
  • Much faster on sorted arrays than linear search
Cons
  • Requires sorting the data set beforehand, which can add extra cost.
Complexity
  • Time Complexity: Best Case: O(1), Average & Worse Case: O(log n)
  • Space Complexity: O(1)
The Magical Organizer — Hashing

Now, let’s leap into the realm of Hashing. Imagine you have a magical map that tells you exactly where your book is hidden. Instead of searching through every possible location, you just consult your map, which points you directly to the book. This is the wonder of Hashing!

What is Hashing?

Hashing is a technique that uses a hash function to map data to unique locations in a hash table, making searches extremely efficient. It’s like having a magical organizer that instantly knows where everything is, mapping data of arbitrary size to fixed-size values using a hash function. This technique is used to quickly locate a data record, given its search key.

Algorithm
  1. Compute the hash code of the target value using a hash function.
  2. Use the hash code to find the index in the hash table.
  3. Retrieve the value at that index.
  4. If there’s a collision (another value at that index), use a collision resolution technique (e.g., chaining or open addressing) to find the correct value.
Visualization of Hashing
Code Example
				
					import java.util.HashMap;

public class HashingExample {
   public static void main(String[] args) {
       // Create a hash table to store books with their title as key
       HashMap<String, Integer> bookshelf = new HashMap<>();
      
       // Add some books to the shelf
       bookshelf.put("Moby Dick", 1);
       bookshelf.put("War and Peace", 2);
       bookshelf.put("The Great Gatsby", 3);
       bookshelf.put("1984", 4);
      
       // Search for a specific book
       String targetBook = "The Great Gatsby";
       if (bookshelf.containsKey(targetBook)) {
           System.out.println("Book found at slot: " + bookshelf.get(targetBook));
       } else {
           System.out.println("Book not found.");
       }
   }
}
				
			
Handling Collisions

In real-world scenarios, two keys might hash to the same slot, a collision. Various methods handle collisions, such as chaining (linking elements at the same index) or open addressing (finding another open slot).

Code Example with Chaining
				
					import java.util.LinkedList;

public class HashTable {
   private LinkedList<Entry>[] table;
   private int size;

   public HashTable(int size) {
       this.size = size;
       table = new LinkedList[size];
       for (int i = 0; i < size; i++) {
           table[i] = new LinkedList<>();
       }
   }

   public void put(String key, int value) {
       int hash = hashFunction(key);
       LinkedList<Entry> entries = table[hash];
       for (Entry entry : entries) {
           if (entry.key.equals(key)) {
               entry.value = value;
               return;
           }
       }
       entries.add(new Entry(key, value));
   }

   private int hashFunction(String key) {
       return Math.abs(key.hashCode() % size);
   }

   public Integer get(String key) {
       int hash = hashFunction(key);
       LinkedList<Entry> entries = table[hash];
       for (Entry entry : entries) {
           if (entry.key.equals(key)) {
               return entry.value;
           }
       }
       return null; // key not found
   }

   private static class Entry {
       String key;
       int value;

       Entry(String key, int value) {
           this.key = key;
           this.value = value;
       }
   }

   public static void main(String[] args) {
       HashTable bookshelf = new HashTable(10);

       // Add some books to the shelf
       bookshelf.put("Moby Dick", 1);
       bookshelf.put("War and Peace", 2);
       bookshelf.put("The Great Gatsby", 3);
       bookshelf.put("1984", 4);
       bookshelf.put("The Great Gatsby", 7);

       // Search for a specific book
       String targetBook = "The Great Gatsby";
       Integer slot = bookshelf.get(targetBook);
       if (slot != null) {
           System.out.println("Book found at slot: " + slot);
       } else {
           System.out.println("Book not found.");
       }
   }
}
				
			
Pros
  • Speedy search, insert, and delete operations on average (O(1) average time complexity)
  • Ideal for implementing key-value data structures
  • Requires less memory as it allocates a fixed space for storing elements
  • Performs well with large data sets, maintaining constant access time
Cons
  • Requires a good hash function to minimize collisions
  • The hash function can be complex to design
  • Handling collisions can be tricky and affect performance
  • Worst-case time complexity is O(n) if many collisions occur
Complexity
  • Time Complexity: O(1) (average case) — In the ideal scenario, hashing offers constant time complexity for element retrieval. However, collisions can increase the complexity in the worst case. O(n) in the worst case.
  • Space Complexity: O(n) — The hash table scales linearly with the data set size.
Epilogue: Choosing Your Weapon

So, brave adventurers, which searching algorithm should you choose for your next quest? Here are some parting tips:

  • Linear Search: Use it for small or unsorted lists where simplicity is key.
  • Binary Search: Ideal for large, sorted lists where efficiency is essential.
  • Hashing: Perfect for large datasets when you need constant time complexity and can efficiently hash the dataset.

Remember, the key to a successful search lies in knowing your data and choosing the right tool for the job.

Part 2 Awaits!

This blog concludes the first part of our journey through the world of searching algorithms. But the adventure doesn’t end here! In the next part, we’ll explore more advanced techniques, including Jump Search, Interpolation Search, Exponential Search, and Ternary Search. These methods are perfect for those who want to optimize their searches further and tackle unique data challenges. Stay tuned, and prepare to jump, interpolate, and explore more in the next part!

Happy coding, and may you always find what you seek!

Stay classy!

Muhammad Saimon Uddin

21 September, 2024