A Beginner’s Guide to Search Algorithms: Exploring Java Code Examples

Lucas Pham
9 min readJun 27, 2023

Search algorithms are an essential part of computer science and programming. They allow us to efficiently find specific elements or patterns within a collection of data. As a newbie programmer, understanding these algorithms and their implementation in Java can greatly enhance your problem-solving skills. In this blog post, we’ll explore several popular search algorithms, accompanied by Java code examples and detailed explanations. Let’s dive in!

Linear Search: The linear search algorithm is the simplest and most straightforward method to find an element in a list. It sequentially checks each element until a match is found.

Code Example:

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

public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
int target = 30;
int result = linearSearch(array, target);
if (result == -1) {
System.out.println("Element not found");
} else {
System.out.println("Element found at index " + result);
}
}
}

Explanation: The linearSearch method takes an array and a target element as input. It iterates over the array using a loop and compares each element with the target. If a match is found, it returns the index of the element. If the element is not found, it returns -1.

Binary Search: Binary search is an efficient algorithm for finding an element in a sorted array. It works by repeatedly dividing the search space in half.

Code Example:

public class BinarySearch {
public static int binarySearch(int[] array, int target) {
int low = 0;
int high = array.length - 1;

while (low <= high) {
int mid = low + (high - low) / 2;

if (array[mid] == target) {
return mid;
}

if (array[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}

return -1; // Element not found
}

public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
int target = 30;
int result = binarySearch(array, target);
if (result == -1) {
System.out.println("Element not found");
} else {
System.out.println("Element found at index " + result);
}
}
}

Explanation: The binarySearch method takes a sorted array and a target element as input. It initializes the low and high pointers to the start and end of the array, respectively. Inside the while loop, it calculates the mid index and compares the element at that index with the target. Based on the comparison, it updates the low or high pointer to narrow down the search space. The loop continues until the element is found or the search space is exhausted.

Depth-First Search (DFS): DFS is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It is commonly implemented using recursion or a stack.

Code Example:

import java.util.ArrayList;
import java.util.List;

public class DepthFirstSearch {
private static void dfs(List<List<Integer>> graph, int vertex, boolean[] visited) {
visited[vertex] = true;
System.out.print(vertex + " ");

for (int neighbor : graph.get(vertex)) {
if (!visited[neighbor]) {
dfs(graph, neighbor, visited);
}
}
}

public static void main(String[] args) {
int vertices = 7;
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < vertices; i++) {
graph.add(new ArrayList<>());
}

// Add edges to the graph
graph.get(0).add(1);
graph.get(0).add(2);
graph.get(1).add(3);
graph.get(1).add(4);
graph.get(2).add(5);
graph.get(2).add(6);

boolean[] visited = new boolean[vertices];
System.out.print("DFS traversal: ");
dfs(graph, 0, visited);
}
}

Explanation: The dfs method takes a graph represented as an adjacency list, a starting vertex, and an array to keep track of visited vertices. It marks the current vertex as visited, prints it, and recursively explores its neighbors that haven't been visited yet. The main method initializes the graph, adds edges, and calls the dfs method starting from vertex 0.

Breadth-First Search (BFS): BFS is another graph traversal algorithm that explores all the vertices of a graph in breadth-first order. It uses a queue to keep track of the vertices to visit next.

Code Example:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class BreadthFirstSearch {
private static void bfs(List<List<Integer>> graph, int startVertex) {
int vertices = graph.size();
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();

visited[startVertex] = true;
queue.offer(startVertex);

while (!queue.isEmpty()) {
int vertex = queue.poll();
System.out.print(vertex + " ");

for (int neighbor : graph.get(vertex)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
}

public static void main(String[] args) {
int vertices = 7;
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < vertices; i++) {
graph.add(new ArrayList<>());
}

// Add edges to the graph
graph.get(0).add(1);
graph.get(0).add(2);
graph.get(1).add(3);
graph.get(1).add(4);
graph.get(2).add(5);
graph.get(2).add(6);

System.out.print("BFS traversal: ");
bfs(graph, 0);
}
}

Explanation: The bfs method performs a breadth-first search traversal on a graph. It uses a boolean array visited to keep track of visited vertices. Initially, the start vertex is marked as visited and enqueued in the queue. The method then enters a loop where it dequeues a vertex, prints it, and visits all its neighbors that haven't been visited yet. The process continues until the queue becomes empty.

Hashing with Separate Chaining: Hashing is a technique that allows for efficient search, insertion, and deletion of elements in constant time on average. In separate chaining, collisions are resolved by storing multiple elements in the same location of the hash table using linked lists.

Code Example:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class HashingWithSeparateChaining {
static class MyHashMap<K, V> {
private int size;
private List<List<Entry<K, V>>> table;

static class Entry<K, V> {
K key;
V value;

Entry(K key, V value) {
this.key = key;
this.value = value;
}
}

MyHashMap(int size) {
this.size = size;
table = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
table.add(new LinkedList<>());
}
}

private int getHash(K key) {
return key.hashCode() % size;
}

public void put(K key, V value) {
int index = getHash(key);
List<Entry<K, V>> bucket = table.get(index);

for (Entry<K, V> entry : bucket) {
if (entry.key.equals(key)) {
entry.value = value;
return;
}
}

bucket.add(new Entry<>(key, value));
}

public V get(K key) {
int index = getHash(key);
List<Entry<K, V>> bucket = table.get(index);

for (Entry<K, V> entry : bucket) {
if (entry.key.equals(key)) {
return entry.value;
}
}

return null; // Key not found
}

public void remove(K key) {
int index = getHash(key);
List<Entry<K, V>> bucket = table.get(index);

for (Entry<K, V> entry : bucket) {
if (entry.key.equals(key)) {
bucket.remove(entry);
return;
}
}
}
}

public static void main(String[] args) {
MyHashMap<String, Integer> map = new MyHashMap<>(10);

map.put("apple", 1);
map.put("banana", 2);
map.put("cat", 3);

System.out.println("Value of 'apple': " + map.get("apple")); // Output: 1
System.out.println("Value of 'banana': " + map.get("banana")); // Output: 2
System.out.println("Value of 'cat': " + map.get("cat")); // Output: 3

map.remove("banana");
System.out.println("Value of 'banana': " + map.get("banana")); // Output: null
}
}

Explanation: The MyHashMap class implements a hash map using separate chaining. It uses a list of linked lists to represent the hash table, where each linked list corresponds to a hash bucket. The put method calculates the hash of the key, retrieves the corresponding bucket, and checks if the key already exists in the bucket. If it does, the value is updated; otherwise, a new entry is added. The get method retrieves the value associated with a key, and the remove method removes an entry from the hash map based on a key.

Interpolation Search: Interpolation search is an efficient search algorithm for uniformly distributed sorted arrays. It calculates the probable position of the target element by using interpolation formulae. It works well on uniformly distributed data but may perform poorly on data with unevenly distributed or clustered values.

Code Example:

public class InterpolationSearch {
public static int interpolationSearch(int[] array, int target) {
int left = 0;
int right = array.length - 1;

while (left <= right && target >= array[left] && target <= array[right]) {
if (left == right) {
if (array[left] == target)
return left;
return -1; // Element not found
}

int pos = left + (((right - left) / (array[right] - array[left])) * (target - array[left]));

if (array[pos] == target)
return pos;

if (array[pos] < target)
left = pos + 1;
else
right = pos - 1;
}

return -1; // Element not found
}

public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50, 60};
int target = 30;
int result = interpolationSearch(array, target);
if (result == -1) {
System.out.println("Element not found");
} else {
System.out.println("Element found at index " + result);
}
}
}

Explanation: The interpolationSearch method performs interpolation search on a sorted array. It uses an interpolation formula to estimate the probable position of the target element within the array. The algorithm compares the target with the values at the estimated position and adjusts the left and right boundaries accordingly until the target is found or the search space is exhausted.

Jump Search: Jump search is an algorithm that works on sorted arrays. It jumps ahead by fixed steps and performs linear search in the subarray where the target element is likely to be present. It combines the benefits of both linear search and binary search, making it suitable for large-sized arrays.

Code Example:

public class JumpSearch {
public static int jumpSearch(int[] array, int target) {
int n = array.length;
int step = (int) Math.floor(Math.sqrt(n));
int prev = 0;

while (array[Math.min(step, n) - 1] < target) {
prev = step;
step += (int) Math.floor(Math.sqrt(n));

if (prev >= n)
return -1; // Element not found
}

while (array[prev] < target) {
prev++;

if (prev == Math.min(step, n))
return -1; // Element not found
}

if (array[prev] == target)
return prev;

return -1; // Element not found
}

public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50, 60};
int target = 30;
int result = jumpSearch(array, target);
if (result == -1) {
System.out.println("Element not found");
} else {
System.out.println("Element found at index " + result);
}
}
}

Explanation: The jumpSearch method performs jump search on a sorted array. It determines the step size as the square root of the array size and jumps ahead by fixed steps until it finds a range where the target element might be present. It then performs a linear search in that range to find the target element.

Which is the fastest one?

The speed of a search algorithm depends on various factors such as the size of the input data, the distribution of data, and the specific requirements of the search problem. The fastest algorithm for a particular scenario can vary.

In general, binary search is considered one of the fastest search algorithms for sorted data. It has a time complexity of O(log n), where n is the number of elements in the collection. Binary search quickly narrows down the search space by dividing it in half with each comparison, resulting in efficient searching.

On the other hand, linear search has a time complexity of O(n), where n is the number of elements in the collection. It sequentially checks each element until a match is found or the end of the collection is reached. Linear search is suitable for small collections or unsorted data, but it can become inefficient for large collections.

Both depth-first search (DFS) and breadth-first search (BFS) are graph traversal algorithms and are not primarily used for searching specific elements within a collection. Their time complexity depends on the size of the graph and the structure of the graph. In the worst case, they can have a time complexity of O(V + E), where V is the number of vertices and E is the number of edges in the graph.

Hashing with separate chaining provides constant-time average case complexity for search operations. However, in the worst case scenario, the performance can degrade to O(n), where n is the number of elements in the hash table.

It’s important to consider the characteristics of the data and the specific requirements of the search problem when choosing the most appropriate algorithm. Each algorithm has its strengths and weaknesses, and the “fastest” algorithm may vary depending on the context.

Conclusion:

We’ve explored three fundamental search algorithms: Linear Search, Binary Search, and Depth-First Search (DFS), Breadth-First Search (BFS), Hashing with Separate Chaining. By understanding these algorithms and studying the accompanying Java code examples, programmers can gain a solid foundation in search algorithms and their implementation. Remember, practice and experimentation are crucial to becoming proficient in these concepts.

Keep reading:

--

--

Lucas Pham

Engineering manager with 20 years of software development experiences. Subscribe me to get update with my posts https://medium.com/@phamtuanchip