Two structures: a set and a list.
In the set, you store nodes you have already visited. This prevents you from following cycles.
The list is of objects containing: (1) a node, and (2) a pointer back to the node that found it.
Starting at the start node, add it to the set, add it to the list with a null back reference, and then add all the nodes it can reach to the list with back references to the index 0 in the list (the start node).
Then for each element in the list thereafter, up until you reach the end, do the following:
- if it is in the set already skip it (you have already visited it) and move to the next item in the list.
- otherwise, add it to the set, and add all nodes it can reach to the list with back references to the index you are 'looking at' to the end of the list. Then go to the next index in the list and repeat.
If at any point you reach the end node (optimally as you are adding it to the list - as opposed to visiting it in the list), track back through the back references to the start node and invert the path.
Example:
Given nodes 0 through 3, where
node0 --> node1
node0 --> node2
node1 --> node2
node2 --> node3
and node0 is START and node3 is END
SET = {}
LIST = []
Step 1 - add START:
SET = {node0}
LIST = [[node0, null]]
Step 2 - at index 0 of the list - add reachable nodes:
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 3 - at index 1 of the LIST - add reachable nodes:
node2 is already in the SET. skip adding reachable nodes to LIST.
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 4 - at index 2 of the LIST - add reachable nodes:
SET = {node0, node1, node2, node3}
LIST = [[node0, null], [node1, 0], [node2, 0], [node3, 2]]
Step 5 - reached END, now backtrack:
The END node (node3) inserted in the LIST has a back reference to index 2 in the list, which is node2. This has a back reference to index 0 in the list, which is node0 (START). Invert this and you get a shortest path of node0 --> node2 --> node3.