Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions src/main/java/com/thealgorithms/others/AStarSearch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.thealgorithms.others;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.PriorityQueue;

/**
* A* (A-Star) Search Algorithm implementation for finding the shortest path
* between two nodes in a graph.
*
* <p>This algorithm uses a heuristic to guide its search, making it more efficient
* than Dijkstra’s algorithm in many cases.
*
* <p>f(n) = g(n) + h(n)
* where:
* - g(n): cost from start node to current node
* - h(n): estimated cost from current node to goal (heuristic)
*
* <p>Time Complexity:
* - Worst case: O(E log V)
*
* <p>Space Complexity:
* - O(V)
*
* <p>Use Cases:
* - Pathfinding (maps, games)
* - AI planning
*
* @author Suraj Devatha
*/
public final class AStarSearch {

private AStarSearch() {
}

/**
* Finds shortest path using A*.
*
* @param graph adjacency list
* @param start start node
* @param goal goal node
* @param heuristic heuristic function
* @return list of nodes representing shortest path
*/
public static List<Node> findPath(Map<Node, List<Edge>> graph, Node start, Node goal, Heuristic heuristic) {

Map<Node, Double> gScore = new HashMap<>();
Map<Node, Double> fScore = new HashMap<>();
Map<Node, Node> cameFrom = new HashMap<>();

PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingDouble(fScore::get));

gScore.put(start, 0.0);
fScore.put(start, heuristic.estimate(start, goal));

openSet.add(start);

while (!openSet.isEmpty()) {
Node current = openSet.poll();

if (current.equals(goal)) {
return reconstructPath(cameFrom, current);
}

for (Edge edge : graph.getOrDefault(current, Collections.emptyList())) {
Node neighbor = edge.target;
double tentativeG = gScore.get(current) + edge.cost;

if (tentativeG < gScore.getOrDefault(neighbor, Double.POSITIVE_INFINITY)) {
cameFrom.put(neighbor, current);
gScore.put(neighbor, tentativeG);
fScore.put(neighbor, tentativeG + heuristic.estimate(neighbor, goal));

if (!openSet.contains(neighbor)) {
openSet.add(neighbor);
}
}
}
}

return Collections.emptyList(); // No path found
}

/**
* Reconstructs path from goal to start.
*/
private static List<Node> reconstructPath(Map<Node, Node> cameFrom, Node current) {
List<Node> path = new ArrayList<>();
path.add(current);

while (cameFrom.containsKey(current)) {
current = cameFrom.get(current);
path.add(current);
}

Collections.reverse(path);
return path;
}

/**
* Heuristic interface (can plug different heuristics).
*/
public interface Heuristic {
double estimate(Node current, Node goal);
}

/**
* Node class representing a vertex in the graph.
*/
static class Node {
String id;

Node(String id) {
this.id = id;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Node)) {
return false;
}
Node node = (Node) o;
return Objects.equals(id, node.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

/**
* Edge class representing weighted connections.
*/
static class Edge {
Node target;
double cost;

Edge(Node target, double cost) {
this.target = target;
this.cost = cost;
}
}
}
116 changes: 116 additions & 0 deletions src/test/java/com/thealgorithms/others/AStarSearchTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.thealgorithms.others;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Test cases for AStarSearch algorithm.
*/
class AStarSearchTest {

/**
* Simple heuristic that returns 0 (reduces A* to Dijkstra).
*/
private static final AStarSearch.Heuristic ZERO_HEURISTIC = (node, goal) -> 0;
private static Map<AStarSearch.Node, List<AStarSearch.Edge>> graph;

private static AStarSearch.Node nodeA;
private static AStarSearch.Node nodeB;
private static AStarSearch.Node nodeC;
private static AStarSearch.Node nodeD;

@BeforeAll
static void setUp() {
graph = new HashMap<>();

nodeA = new AStarSearch.Node("A");
nodeB = new AStarSearch.Node("B");
nodeC = new AStarSearch.Node("C");
nodeD = new AStarSearch.Node("D");

graph.put(nodeA, Arrays.asList(new AStarSearch.Edge(nodeB, 1), new AStarSearch.Edge(nodeC, 4)));

graph.put(nodeB, Arrays.asList(new AStarSearch.Edge(nodeC, 2), new AStarSearch.Edge(nodeD, 5)));

graph.put(nodeC, Arrays.asList(new AStarSearch.Edge(nodeD, 1)));

graph.put(nodeD, Collections.emptyList());
}

@Test
void testPathExists() {
List<AStarSearch.Node> path = AStarSearch.findPath(graph, nodeA, nodeD, ZERO_HEURISTIC);

// Expected shortest path: A -> B -> C -> D
List<AStarSearch.Node> expected = Arrays.asList(nodeA, nodeB, nodeC, nodeD);

assertEquals(expected, path);
}

@Test
void testDirectPath() {
List<AStarSearch.Node> path = AStarSearch.findPath(graph, nodeA, nodeB, ZERO_HEURISTIC);

List<AStarSearch.Node> expected = Arrays.asList(nodeA, nodeB);

assertEquals(expected, path);
}

@Test
void testStartEqualsGoal() {
List<AStarSearch.Node> path = AStarSearch.findPath(graph, nodeA, nodeA, ZERO_HEURISTIC);

List<AStarSearch.Node> expected = Collections.singletonList(nodeA);

assertEquals(expected, path);
}

@Test
void testNoPathExists() {
AStarSearch.Node nodeE = new AStarSearch.Node("nodeE");

List<AStarSearch.Node> path = AStarSearch.findPath(graph, nodeE, nodeA, ZERO_HEURISTIC);

assertTrue(path.isEmpty());
}

@Test
void testPathCostOptimality() {
List<AStarSearch.Node> path = AStarSearch.findPath(graph, nodeA, nodeD, ZERO_HEURISTIC);

// Calculate total cost
double cost = calculatePathCost(path);

// Expected shortest cost = 1 (A->B) + 2 (B->C) + 1 (C->D) = 4
assertEquals(4.0, cost);
}

/**
* Utility method to calculate path cost.
*/
private double calculatePathCost(List<AStarSearch.Node> path) {
double total = 0;

for (int i = 0; i < path.size() - 1; i++) {
AStarSearch.Node current = path.get(i);
AStarSearch.Node next = path.get(i + 1);

for (AStarSearch.Edge edge : graph.getOrDefault(current, Collections.emptyList())) {
if (edge.target.equals(next)) {
total += edge.cost;
break;
}
}
}

return total;
}
}
Loading