Is there a good way to do an efficient, nonrecursive topological sort on an immutable graph? I've a situation where I'm traversing a graph linked together by pointers and I need to do a topological sort. It's important to me not to modify the graph, but I'm not sure how to mark a node as visited and check it efficiently without doing so. At the moment, I have a set to store the markings, but I know the search occurs in log(m)
time. Is there a way to do this better? Here's some working code:
// For std::shared_ptr
#include <memory>
// For std::list, std::stack, std::set
#include <list>
#include <stack>
#include <set>
// For std::cout
#include <iostream>
// Node in a directed acyclic graph with only two exiting edges
struct Node {
// Identity of the node for debugging
char identity;
// Left and right branches
std::shared_ptr <Node> left;
std::shared_ptr <Node> right;
// Create a node
Node(
char const & identity_,
std::shared_ptr <Node> const & left_,
std::shared_ptr <Node> const & right_
) : identity(identity_), left(left_), right(right_)
{}
};
// Determines a topological sort of a directed acyclic graph of compute nodes
std::list <std::shared_ptr <Node>> topo_sort(
std::shared_ptr <Node> const & root
) {
// Add the root node to the todo list. The todo list consists of
// (ptr,whether we've searched left,whether we've searched right).
auto todo = std::stack <std::tuple <std::shared_ptr <Node>,bool,bool>> ();
todo.push(std::make_tuple(root,false,false));
// Add an empty list for the sorted elements
auto sorted = std::list <std::shared_ptr <Node>> {};
// Keep track of which nodes have been marked
auto marked = std::set <std::shared_ptr <Node>> {root};
// Determines if a node has been marked
auto is_marked = [&](auto const & node) {
return marked.find(node)!=marked.end();
};
// Loop over the elements in the todo stack until we have none left to
// process
while(todo.size()>0) {
// Grab the current node
auto & current = todo.top();
auto & node = std::get <0> (current);
auto & searched_left = std::get <1> (current);
auto & searched_right = std::get <2> (current);
// Grab the left and right nodes
auto left = node->left;
auto right = node->right;
// Do a quick check to determine whether we actually have children
if(!left)
searched_left = true;
if(!right)
searched_right = true;
// If we've already transversed both left and right, add the node to
// the sorted list
if(searched_left && searched_right) {
sorted.push_front(node);
marked.insert(node);
todo.pop();
// Otherwise, traverse the left branch if that node hasn't been marked
} else if(!searched_left) {
searched_left = true;
if(!is_marked(left)) {
todo.push(std::make_tuple(left,false,false));
marked.insert(left);
}
// Next, traverse the right branch if that node hasn't been marked
} else if(!searched_right) {
searched_right = true;
if(!is_marked(right)) {
todo.push(std::make_tuple(right,false,false));
marked.insert(right);
}
}
}
// Return the topological sort
return sorted;
}
int main() {
// Create a graph with some dependencies
auto a = std::make_shared <Node> ('a',nullptr,nullptr);
auto b = std::make_shared <Node> ('b',nullptr,nullptr);
auto c = std::make_shared <Node> ('c',a,a);
auto d = std::make_shared <Node> ('d',b,c);
auto e = std::make_shared <Node> ('e',d,c);
auto f = std::make_shared <Node> ('f',e,c);
// Sort the elements
auto sorted = topo_sort(f);
// Print out the sorted order
for(auto const & node : sorted)
std::cout << node->identity << std::endl;
}
which gives
f
e
d
c
a
b
The above should do a depth first search for the sort. And, yes, I realize this is a funny looking tree for a graph, but the left and right elements don't have to point at unique elements. In any case, thanks in advance for the help.
std::unorderd_set Solution
Instead of std::set<std::shared_ptr<Node>>
, you can use std::unordered_set<Node*>
to mark the visited nodes. unordered_set
use hash to index nodes (complexity: constant on average), and it should be faster than set
in most cases. Also save the raw pointer, ie Node*
, in container, is faster than shared_ptr
, since there's no reference count operations.
If this solution takes too much memory, you can try the bitmap
solution.
bitmap Solution
Give each node an id beginning from 0, and use a bitmap
to save the visited status.
Initialize the bitmap
with all bits setting to 0. When visiting the nth node (whose id is n
), set the nth bit on the bitmap
. When you want to find out if a given node has been visited, you just check whether the corresponding bit has been set.
This solution only takes n
bits of memory, where n
is the number of nodes in your tree.
Logarithmic complexity is almost always fast enough. std::map and std::set also have another advantage over hash tables - the performance is guaranteed 100% of the time, a property very useful in eg hard real-time systems. A hash table is faster than a red-black tree (map,set) most of the time, but eg if a rehash is necessary, worst-case performance will hit you.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.