One of the cool things about Python is that iterators are just a normal datatype. Although one usually uses for loops and comprehensions to iterate, there is nothing to stop you from doing the iteration manually if that is convenient. One reason it might be convenient is to avoid deep recursion, by replacing it with an explicit stack.
While this does "take away the recursion", it does not significantly complicate the program, and the recursive structure is still evident. Python simply does not recurse gracefully, so this sort of transformation is often useful.
When you write
for child in graph_map[start]:
...
You're doing something very similar to the following:
it = iter(graph_map[start])
try:
child = next(it)
...
except StopIteration:
pass
[See Note 1]
The iter function returns an iterator for a collection (or other iterable object, such as a comprehension, a generator, or a range). The next method returns the next value, and advances the iterator; if there is no next value, a StopIteration exception is raised.
The DFS function simply calls itself recursively for every unvisited child of its argument. We can emulate that behaviour precisely using a stack of iterators. Instead of recursively calling with a node, we'll push an iterator to the node's children onto the iterator stack. When the iterator at the top of the stack terminates, we'll pop it off the stack.
def DFS(graph_map):
visited = [False] * len(graph_map)
# By initializing the stack with a range iterator, we do
# the equivalent of DFS_Loop.
# In Python2, you should use xrange instead of range
stack = [iter(range(len(graph_map), 0, -1))]
while stack:
try:
child = next(stack[-1])
if not visited[child - 1]:
visited[child - 1] = True
# Do whatever you want to do in the visit
stack.append(iter(graph_map[child]))
except StopIteration:
stack.pop()
The above function applied to the sample data in the file provided in the OP used a maximum of 62794 stack slots. On my Linux laptop, it took somewhere around 3 seconds, once the data was read in; I didn't time it precisely.
It's also interesting to note that the above can be changed to do a breadth-first search simply by changing the stack to a queue. For a depth search first, the stack must be a stack of iterators (or equivalent, in languages in which that is not so simple); for breadth-search first, the queue may be a queue of iterators, but it would work as well to use a queue of values.
Notes
- The actual virtual machine uses the defined iterator protocol, which varies slightly between Python2 and Python3. In both versions, the container (or other iterable) objection has a member function called
__iter__ which returns a newly created iterator object. The iterator object has a method called next in Python2 and __next__ in Python3, which returns the current value and advances the iterator. Since 2.6, you can use the iter and next global functions to avoid having to worry about these details, and that's what I've done here.