拓扑排序

495 阅读5分钟

定义

In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering. For instance, the vertices of the graph may represent tasks to be performed, and the edges may represent constraints that one task must be performed before another; in this application, a topological ordering is just a valid sequence for the tasks. A topological ordering is possible if and only if the graph has no directed cycles, that is, if it is a directed acyclic graph (DAG). Any DAG has at least one topological ordering, and algorithms are known for constructing a topological ordering of any DAG in linear time.

取自维基百科的定义。上面的定义中有几个关键点:

  1. 每两个顶点u、v 之间都有一条直接的边进行连接。
  2. 必须是有向无环图。
  3. 有向无环图至少有一个拓扑排序的方案,并且能够在线性时间内求解出答案。

英文的 wiki 很多时候比中文的详细太多了。本来想好好梳理一下拓扑排序,现在发现我只要好好意译一下英文的 wiki 就好了。

拓扑排序的代表性应用在于对有时序依赖的任务来进行调度,例如有任务[A,B,C,D,E]待执行,其中任务 A 依赖于任务 C,任务 B 依赖于任务 D,E 无依赖,那么CADBE,CDEBA 都是其合理的拓扑排序答案。

在计算机科学中,拓扑排序在指令调度,表格中的公式求值,逻辑组合,makefile 文件的编译顺序决定、数据序列化、连接器中解决符号依赖等方向的应用越来越重要。甚至被用于以何种顺序去加载拥有外键的数据库的表。

算法

Kahn's algorithm

L ← 包含所有有序元素的空列表
S ← 没有前向边的节点的集合

while S is non-empty do
    remove a node n from S
    add n to tail of L
    for each node m with an edge e from n to m do
        remove edge e from the graph
        if m has no other incoming edges then
            insert m into S

if graph has edges then
    return error   (graph has at least one cycle)
else 
    return L   (a topologically sorted order)

关键其实在于一开始就要找出所有的无前向边的节点,以这些节点为源头开始遍历,將其在S中移除,遍历过的节点加入列表 L 中,移除当前遍历节点的所有后向边, 遍历当前节点结束之后,如果其子节点变成了新的无前向边的节点,將其加入到 S 中。最后只要集合 S 为空,那么 L 就是我们需要求解的答案。

深度优先算法

L ← 包含所有有序元素的空列表
while exists nodes without a permanent mark do
    select an unmarked node n
    visit(n)

function visit(node n)
    if n has a permanent mark then
        return
    if n has a temporary mark then
        stop   (not a DAG)

    mark n with a temporary mark

    for each node m with an edge from n to m do
        visit(m)

    remove temporary mark from n
    mark n with a permanent mark
    add n to head of L

节点有三种状态,未标记、临时标记,永久标记状态。 从未标记过的节点开始遍历,对其进行临时标记,如果其没有前向边的话,將其标记,如果有前向边的话,將此节点打上临时标记的标,先遍历其前向边的节点,如果无前向边的话,將此节点打上永久标记。如果当你发现你要遍历的节点已经被打上临时的标的时候,代表当前图有环。

用简单的例子实践一下这两种算法。

package algorithm;

import java.util.*;

/**
 * 拓扑排序
 * 邻接链表表示方案
 */
public class TopologicalSort {


    public List<Integer> Kahn(Map<Integer, List<Integer>> map, int[] allNode){
        List<Integer> list = new ArrayList<>();
        Queue<Integer> queue = new ArrayDeque<>();
        // 找出所有的不包含前向边的节点
        for (Integer i:allNode) {
            int hasFront = 0;
            for(List<Integer> temp : map.values()){
                if(temp.contains(i)){
                    hasFront = 1;
                    break;
                }
            }
            if(hasFront == 0){
                queue.add(i);
            }
        }

        while (!queue.isEmpty()){
            Integer k = queue.poll();
            list.add(k);
            List<Integer> tempList = map.get(k);
            map.remove(k);
            if(tempList != null){
                for(Integer l : tempList){
                    int hasFront = 0;
                    for(List<Integer> temp : map.values()){
                        if(temp.contains(l)){
                            hasFront = 1;
                            break;
                        }
                    }
                    if(hasFront == 0){
                        queue.add(l);
                    }
                }
            }
        }
        if (!map.isEmpty()){
            //有环
            return null;
        }
        return list;


    }


    public List<Integer> DFS(Map<Integer, List<Integer>> map, int[] allNode){
        List<Integer> list = new ArrayList<>();
        //0为未标记,1 为临时标记,2 为已标记
        HashMap<Integer, Integer> flagMap = new HashMap<>();
        for (int i : allNode){
            flagMap.put(i, 0);
        }
        for (int key : flagMap.keySet()){
            if(!visit(key,flagMap,map,list)){
                list = null;
                break;
            }
        }
        return list;
    }

    public boolean visit(Integer i, Map<Integer, Integer> flagMap, Map<Integer, List<Integer>> map, List<Integer> list){
        if(flagMap.get(i) == 2){
            return true;
        }else if (flagMap.get(i) == 1){
            //图有环
            return false;
        }
        //判断当前节点是否有前置节点,有前置节点先遍历前置节点
        for (Integer k : map.keySet()){
            List<Integer> tempList = map.get(k);
            if (tempList.contains(i)){
                flagMap.put(i,1);
                visit(k, flagMap, map, list);
            }
        }
        flagMap.put(i,2);
        list.add(i);
        return true;
    }



    public static void main(String[] args){
        //  邻接链表来表示图
        int[] allNode = {5,7,3,11,8,2,9,10};
        Map<Integer, List<Integer>> map = new HashMap<>();
        List<Integer> list5 = new ArrayList<>();
        list5.add(11);
        map.put(5,list5);
        List<Integer> list11 = new ArrayList<>();
        list11.addAll(Arrays.asList(new Integer[]{2,9,10}));
        map.put(11,list11);
        List<Integer> list7 = new ArrayList<>();
        list7.addAll(Arrays.asList(new Integer[]{11,8}));
        map.put(7,list7);
        List<Integer> list8 = new ArrayList<>();
        list8.add(9);
        map.put(8,list8);
        List<Integer> list3 = new ArrayList<>();
        list3.add(8);
        list3.add(10);
        map.put(3,list3);
        System.out.println("DFS answer is " + new TopologicalSort().DFS(map, allNode));
        System.out.println("Kahn answer is" + new TopologicalSort().Kahn(map, allNode));
    }
}

输出结果:
DFS answer is [5, 7, 11, 2, 3, 8, 9, 10]
Kahn answer is[5, 7, 3, 11, 8, 2, 10, 9]

写得比较粗糙,深感自己的代码规范还有待提高。

这里的例子是无权重的有向无环图,现实的例子更多的是有权重的有向无环图,在之后的文章里会有拓扑排序实现单源最短路径的查找。

参考文献:

  1. 维基百科--拓扑排序