简体   繁体   中英

How to do an efficient, nonrecursive topological sort on an immutable graph

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM