Skip to main content

Comparing Search and Graph Algorithms

Algorithm Time Complexity (Avg) Space Complexity Requirements Notes
Linear Search O(n) O(1) None Works on unsorted arrays
Binary Search O(log n) O(1) Sorted array Must be sorted to function

Example: Linear Search Implementation

function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}

const arr = [5, 3, 8, 1, 9];
console.log(linearSearch(arr, 8)); // Output: 2

Example: Binary Search Implementation

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

const sortedArr = [1, 3, 5, 8, 9];
console.log(binarySearch(sortedArr, 8)); // Output: 3

When to Use Which

  • Linear Search: When data is unsorted or small dataset
  • Binary Search: When data is sorted and performance matters

2. Comparing Advanced Searches

Algorithm Time Complexity Space Complexity Use Case
Binary Search O(log n) O(1) Sorted arrays/vectors
Interpolation Search O(log log n) average
O(n) worst
O(1) Uniformly distributed sorted data
Exponential Search O(log i) where i is index of element O(1) Unbounded or unknown size arrays
Ternary Search O(log₃ n) O(1) Sorted arrays with unimodal functions
Jump Search O(√n) O(1) Sorted arrays with large datasets

Example: Binary Search Implementation

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

Example: Interpolation Search Implementation

function interpolationSearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right && target >= arr[left] && target <= arr[right]) {
    let pos = left + Math.floor(((target - arr[left]) / (arr[right] - arr[left])) * (right - left));
    if (arr[pos] === target) return pos;
    if (arr[pos] < target) left = pos + 1;
    else right = pos - 1;
  }
  return -1;
}

Comparison Summary

  • Binary Search: Best for sorted arrays, consistent performance
  • Interpolation Search: Efficient for uniformly distributed data
  • Exponential Search: Ideal when array size is unknown or very large
  • Ternary Search: Useful for finding min/max in unimodal functions
  • Jump Search: Good for large datasets where memory is limited

3. Comparing String Search Algorithms

Algorithm Time Complexity Space Complexity Description
Naive String Search O(n*m) O(1) Checks every position in the text for a match with the pattern. Simple but inefficient for large texts.
KMP (Knuth-Morris-Pratt) O(n+m) O(m) Preprocesses the pattern to avoid unnecessary character comparisons. Efficient for repeated searches.
Boyer-Moore O(n*m) worst case, O(n/m) average O(k) Searches from right to left and skips characters based on mismatched characters. Fast in practice for large alphabets.
Rabin-Karp O(n+m) average, O(n*m) worst O(1) Uses hashing to find a pattern in the text. Useful for multiple pattern searches or when rolling hash is beneficial.

Example: KMP Algorithm Implementation

function buildLPS(pattern) {
  const lps = new Array(pattern.length).fill(0);
  let len = 0;
  let i = 1;

  while (i < pattern.length) {
    if (pattern[i] === pattern[len]) {
      len++;
      lps[i] = len;
      i++;
    } else {
      if (len !== 0) {
        len = lps[len - 1];
      } else {
        lps[i] = 0;
        i++;
      }
    }
  }

  return lps;
}

function KMPSearch(text, pattern) {
  const lps = buildLPS(pattern);
  let i = 0;
  let j = 0;

  while (i < text.length) {
    if (pattern[j] === text[i]) {
      i++;
      j++;
    }

    if (j === pattern.length) {
      console.log("Found pattern at index " + (i - j));
      j = lps[j - 1];
    } else if (i < text.length && pattern[j] !== text[i]) {
      if (j !== 0) {
        j = lps[j - 1];
      } else {
        i++;
      }
    }
  }
}

Example: Boyer-Moore Implementation

function buildBadCharTable(pattern) {
  const table = new Array(256).fill(-1);
  for (let i = 0; i < pattern.length; i++) {
    table[pattern.charCodeAt(i)] = i;
  }
  return table;
}

function boyerMooreSearch(text, pattern) {
  const badCharTable = buildBadCharTable(pattern);
  let s = 0;

  while (s <= text.length - pattern.length) {
    let j = pattern.length - 1;

    while (j >= 0 && pattern[j] === text[s + j]) {
      j--;
    }

    if (j < 0) {
      console.log("Pattern found at index " + s);
      s += (s + pattern.length < text.length) ? pattern.length - badCharTable[text.charCodeAt(s + pattern.length)] : 1;
    } else {
      s += Math.max(1, j - badCharTable[text.charCodeAt(s + j)]);
    }
  }
}

When to Use Which Algorithm

  • Naive: For small texts or when simplicity is preferred over performance.
  • KMP: When the pattern is long and repeated searches are expected.
  • Boyer-Moore: When the alphabet size is large and the pattern is relatively short.
  • Rabin-Karp: For searching multiple patterns or when rolling hash is beneficial.

4. Comparing BFS vs DFS

Aspect BFS (Breadth-First Search) DFS (Depth-First Search)
Traversal Order Level-by-level Go deep before wide
Data Structure Used Queue Stack (or recursion)
Memory Usage Higher (stores all nodes at current level) Lower (only stores path to current node)
Time Complexity O(V + E) O(V + E)
Space Complexity O(V) O(V)
Use Case Examples Shortest path in unweighted graphs, level-order traversal Topological sorting, cycle detection, maze solving

Example: BFS vs DFS Implementation

const graph = {
  'A': ['B', 'C'],
  'B': ['D', 'E'],
  'C': ['F'],
  'D': [],
  'E': ['F'],
  'F': []
};

// BFS Implementation
function bfs(start, target) {
  const queue = [start];
  const visited = new Set();
  while (queue.length > 0) {
    const node = queue.shift();
    if (node === target) return true;
    if (!visited.has(node)) {
      visited.add(node);
      queue.push(...graph[node]);
    }
  }
  return false;
}

// DFS Implementation
function dfs(start, target) {
  const stack = [start];
  const visited = new Set();
  while (stack.length > 0) {
    const node = stack.pop();
    if (node === target) return true;
    if (!visited.has(node)) {
      visited.add(node);
      stack.push(...graph[node]);
    }
  }
  return false;
}

When to Use BFS vs DFS

  • BFS: When you need the shortest path in an unweighted graph or want to explore all nodes at a given depth before moving deeper.
  • DFS: When exploring all paths from a node, or when memory is constrained and you're looking for any path to a target.

5. Comparing Shortest Path Algorithms

Algorithm Description Time Complexity Space Complexity Use Cases
Dijkstra's Algorithm Finds shortest paths from a single source to all other nodes in weighted graphs with non-negative edge weights. O((V + E) log V) O(V) Routing protocols, GPS navigation
Bellman-Ford Algorithm Finds shortest paths from a single source in weighted graphs that may have negative edge weights. O(VE) O(V) Financial modeling, detecting negative cycles
Floyd-Warshall Algorithm Computes shortest paths between all pairs of nodes in a weighted graph. O(V^3) O(V^2) Network routing, transitive closure
A* Algorithm Heuristic-based search algorithm that finds the shortest path between nodes using a heuristic function. O(b^d) where b is branching factor and d is depth O(b^d) Pathfinding in games, robotics

Example: Dijkstra's Algorithm Implementation in JavaScript

function dijkstra(graph, start) {
  const distances = {};
  const visited = new Set();
  const pq = new PriorityQueue();

  for (let node in graph) {
    distances[node] = node === start ? 0 : Infinity;
  }

  pq.enqueue(start, 0);

  while (!pq.isEmpty()) {
    const current = pq.dequeue();
    visited.add(current.node);

    for (let neighbor in graph[current.node]) {
      if (!visited.has(neighbor)) {
        const newDistance = distances[current.node] + graph[current.node][neighbor];
        if (newDistance < distances[neighbor]) {
          distances[neighbor] = newDistance;
          pq.enqueue(neighbor, newDistance);
        }
      }
    }
  }

  return distances;
}

Example: Bellman-Ford Algorithm Implementation in JavaScript

function bellmanFord(edges, vertices, source) {
  const distances = Array(vertices).fill(Infinity);
  distances[source] = 0;

  for (let i = 0; i < vertices - 1; i++) {
    for (let edge of edges) {
      const [u, v, weight] = edge;
      if (distances[u] !== Infinity && distances[u] + weight < distances[v]) {
        distances[v] = distances[u] + weight;
      }
    }
  }

  return distances;
}

Example: Floyd-Warshall Algorithm Implementation in JavaScript

function floydWarshall(graph) {
  const n = graph.length;
  const dist = Array(n).fill().map(() => Array(n).fill(Infinity));

  for (let i = 0; i < n; i++) {
    dist[i][i] = 0;
    for (let j = 0; j < n; j++) {
      if (graph[i][j] !== 0) {
        dist[i][j] = graph[i][j];
      }
    }
  }

  for (let k = 0; k < n; k++) {
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        if (dist[i][k] + dist[k][j] < dist[i][j]) {
          dist[i][j] = dist[i][k] + dist[k][j];
        }
      }
    }
  }

  return dist;
}

6. Comparing MST Algorithms

Algorithm Time Complexity Space Complexity Notes
Kruskal's O(E log E) O(V) Uses Union-Find; works with disconnected graphs
Prim's (Array-based) O(V^2) O(V) Simple implementation; good for dense graphs
Prim's (Binary Heap) O(E log V) O(V) Better for sparse graphs
Prim's (Fibonacci Heap) O(E + V log V) O(V) Optimal for sparse graphs; complex to implement

When to Use Which

  • Kruskal's: When the graph is sparse and you want to avoid complex data structures.
  • Prim's (Array-based): When the graph is dense and simplicity is preferred.
  • Prim's (Binary Heap): When the graph is sparse and performance matters.
  • Prim's (Fibonacci Heap): When you need the best theoretical performance and can handle complexity.

Example: Kruskal's MST

const edges = [
  { u: 0, v: 1, weight: 4 },
  { u: 0, v: 2, weight: 2 },
  { u: 1, v: 2, weight: 1 },
  { u: 1, v: 3, weight: 5 }
];

class UnionFind {
  constructor(n) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.rank = new Array(n).fill(0);
  }

  find(x) {
    if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]);
    return this.parent[x];
  }

  union(x, y) {
    let rootX = this.find(x);
    let rootY = this.find(y);
    if (rootX !== rootY) {
      if (this.rank[rootX] < this.rank[rootY]) [rootX, rootY] = [rootY, rootX];
      this.parent[rootY] = rootX;
      if (this.rank[rootX] === this.rank[rootY]) this.rank[rootX]++;
    }
  }
}

function kruskalMST(n, edges) {
  const uf = new UnionFind(n);
  const mst = [];
  edges.sort((a, b) => a.weight - b.weight);

  for (const edge of edges) {
    if (uf.find(edge.u) !== uf.find(edge.v)) {
      uf.union(edge.u, edge.v);
      mst.push(edge);
    }
  }

  return mst;
}

console.log(kruskalMST(4, edges));
// Output: [{ u: 0, v: 2, weight: 2 }, { u: 1, v: 2, weight: 1 }, { u: 1, v: 3, weight: 5 }]

7. Analyzing Graph Algorithm Complexity

Algorithm Time Complexity (Best) Time Complexity (Average) Time Complexity (Worst) Space Complexity
Breadth-First Search (BFS) O(V + E) O(V + E) O(V + E) O(V)
Depth-First Search (DFS) O(V + E) O(V + E) O(V + E) O(V)
Dijkstra's Algorithm O((V + E) log V) O((V + E) log V) O((V + E) log V) O(V)
A* Search O(b^d) O(b^d) O(b^d) O(b^d)
Floyd-Warshall O(V^3) O(V^3) O(V^3) O(V^2)
Johnson's Algorithm O(V^2 log V + VE) O(V^2 log V + VE) O(V^2 log V + VE) O(V^2)
Note: V represents the number of vertices, and E represents the number of edges in the graph.

Key Considerations for Graph Algorithm Complexity

  • BFS and DFS have the same complexity as they both traverse each vertex and edge once.
  • Dijkstra's algorithm uses a priority queue, leading to logarithmic time complexity.
  • A* is heuristic-based; its complexity depends on the quality of the heuristic function.
  • Floyd-Warshall computes all-pairs shortest paths, making it suitable for dense graphs.
  • Johnson's algorithm is efficient for sparse graphs with negative edge weights.

8. Choosing by Graph Properties

Algorithm Selection Based on Graph Characteristics

  • Directed vs Undirected: Use directed graph algorithms (e.g., topological sort, Kosaraju's) for directed graphs; undirected algorithms (e.g., Union-Find, BFS/DFS) for undirected graphs.
  • Weighted vs Unweighted: Use weighted algorithms (e.g., Dijkstra, Bellman-Ford) for weighted graphs; unweighted algorithms (e.g., BFS) for unweighted graphs.
  • Acyclic vs Cyclic: Use acyclic-specific algorithms (e.g., topological sort, DAG shortest path) for directed acyclic graphs; general-purpose algorithms (e.g., Dijkstra, Floyd-Warshall) for cyclic graphs.
  • Connected vs Disconnected: Use connected graph algorithms (e.g., BFS, DFS) for connected graphs; disconnected graph algorithms (e.g., multiple BFS/DFS traversals) for disconnected graphs.
Algorithm Graph Type Time Complexity Use Case
BFS (Breadth-First Search) Unweighted, Undirected O(V + E) Shortest path in unweighted graphs
Dijkstra's Algorithm Weighted, Directed/Undirected O((V + E) log V) Shortest path in weighted graphs
Floyd-Warshall Weighted, Directed/Undirected O(V^3) All-pairs shortest paths
Topological Sort Directed Acyclic Graph (DAG) O(V + E) Ordering of tasks with dependencies
Kosaraju's Algorithm Directed Graph O(V + E) Strongly connected components
Note: For dense graphs, Floyd-Warshall may be preferred despite higher complexity due to its simplicity and consistent performance. For sparse graphs, Dijkstra or Bellman-Ford are more efficient.

Was this article helpful?