If you drove somewhere today, you likely trusted a blue line on your phone screen. You didn’t question it. You just turned left when it said turn left.
But how did it know? How did it sift through millions of road segments, traffic lights, and speed limits to find the single optimal route in milliseconds?
It wasn’t magic. It was a graph traversal algorithm. specifically, a heuristic search likely derived from A* (A-Star).
We will be doing this in Java, which was my first programming language, and despite the hate it gets, its strict Object-Oriented nature makes it the perfect vessel for complex algorithms.
The Logic: Why Dijkstra Wasn’t Enough
The classic algorithm for finding the shortest path is Dijkstra’s Algorithm. It works by exploring every possible direction equally, expanding outward like a ripple in a pond until it hits the target.
This is guaranteed to find the shortest path, but it’s wasteful. If you are in Zurich and want to go to Paris (West), there is no point in checking roads leading to Vienna (East).
Enter A* (A-Star).
A* introduces a “brain” to the process. It doesn’t just look at the distance traveled (); it also estimates the distance remaining (, or the heuristic).
The core math is elegant in its simplicity:
- : The actual cost to get from the start to node (traffic, distance).
- : The estimated “crow flies” distance from to the goal.
By minimizing , the algorithm prioritizes paths that are both short and moving in the right direction.
The Code: Java’s OOP Power
I implemented this in Java because the PriorityQueue and Class structures handle the complexity cleanly. Here is the core Node class that implements Comparable—this is what allows Java to instantly sort the most promising paths to the top of the pile.
static class Node implements Comparable<Node> {
int id;
int g; // Cost from start
int h; // Heuristic (guess) to end
int f; // Total score (g + h)
Node(int id, int g, int h) {
this.id = id;
this.g = g;
this.h = h;
this.f = g + h;
}
// This is the magic: Java automatically sorts by 'f' score
@Override
public int compareTo(Node other) {
return Integer.compare(this.f, other.f);
}
}
And here is the search loop. Unlike the raw pointer arithmetic of my C projects, this reads almost like English.
while (!pq.isEmpty()) {
Node current = pq.poll(); // Get the most promising node
if (current.id == goalNode) return reconstructPath(parent, goalNode);
// If we found a shorter path to a neighbor, record it
if (newG < dist[v]) {
dist[v] = newG;
parent[v] = u;
pq.add(new Node(v, newG, h[v]));
}
}
The Visualization: Watching it Think
The real beauty of A* emerges when you visualize its decision-making. Unlike Dijkstra’s uniform expansion, A* is drawn toward the goal like a magnet. Let’s trace through a simple grid.
Consider this 4x4 world. S is start, G is goal, # is a wall. The heuristic is the Manhattan distance (sum of horizontal and vertical steps remaining).
Initial State:
S . . #
. # . .
. . # .
# . . G
Our Java PriorityQueue starts with the S node at (0,0). Let’s label each explored node with its [f = g + h] score.
Step 1: Expand S (0,0). Its neighbors are (0,1) and (1,0).
- (0,1):
g=1(one step from S),h=4(to G at (3,3)), sof=5 - (1,0):
g=1,h=5, sof=6PriorityQueue now:[(0,1):5, (1,0):6]← Lowestfis first.
Step 2: Expand the most promising node, (0,1) f=5. Its open neighbor is (0,2).
- (0,2):
g=2,h=3, sof=5Queue:[(0,2):5, (1,0):6]← A tie! OurcompareTomethod will take (0,2), but both have equal priority.
Step 3: Expand (0,2) f=5. Its open neighbor is (1,2).
- (1,2):
g=3,h=2, sof=5Queue:[(1,2):5, (1,0):6]
Step 4: Expand (1,2) f=5. Critical choice! Neighbors: (1,3) is a wall, (2,2) is a wall, (0,2) is visited. Only (2,1) is open.
- (2,1):
g=4,h=2, sof=6Queue:[(1,0):6, (2,1):6]← Notice the “backtrack”: (1,0) from Step 1 is now just as promising. The algorithm isn’t stubborn; it reconsiders.
Step 5: Expand (1,0) f=6. Its neighbor (2,0) opens a new frontier.
- (2,0):
g=2,h=3, sof=5← A better path is found! Queue:[(2,0):5, (2,1):6]
Step 6: Expand (2,0) f=5. This leads to (3,0) which is a wall, and (2,1) which is already queued. The algorithm proceeds, navigating around the obstacles until…
Final Path: The algorithm will find this optimal 6-step path, having explored far fewer nodes than Dijkstra would have:
S 1 2 #
1 # 3 4
2 5 # 5
# 6 7 G
A Note on “Greed” vs “Structure”
A* is often called a “Best-First” search, which can feel greedy, it always grabs the shiny, promising node next. But sometimes, being greedy isn’t the answer.
It’s important to mention algorithms like Prim’s or Kruskal’s (used for Minimum Spanning Trees). Unlike A*, which finds a single path for a single agent, MSTs are about infrastructure. They connect *all* nodes with the minimum total wire/road length.
If A* is the GPS in your car, MST is the civil engineer who decided where to pave the roads in the first place.
Why Java?
There is a reason Big Tech still runs on Java. When you are processing graph data with millions of nodes (think social networks or street maps), you need the strictness of strong typing and the reliability of the JVM.
Writing this in C would have required manually implementing a Priority Queue and managing heap memory for every node expansion. Writing it in Python would have been slow. Java hits that sweet spot: high-level abstractions with near-native performance.