图 - 拓扑排序的算法实现及复杂度分析

- 3 mins

数据结构与算法系列(五十三)

上篇文章介绍了什么是拓扑排序以及拓扑排序的应用场景,还是老规矩,介绍完基本概念,我们接着通过数据结构和算法来实现它,拓扑排序的基本实现思路如下:

在 AOV 网中,找到一个入度为 0 的顶点,然后删除此顶点,并删除以此顶点为起点的弧,重复此步骤,直到输出所有顶点,或者 AOV 网中不存在入度为 0 的顶点为止。

和最小生成树和最短路径一样,我们还是通过邻接表来定义一个 AOV 网,考虑到始终需要查找入度为 0 的顶点,我们在原来的顶点数据结构基础上,新增一个入度域 in,用来表示入度值:

/**
 * 顶点
 * Class VNode
 */
class VNode extends Node
{
    public $in;  // 入度

    public function __construct($data, $in = 0)
    {
        $this->in = $in;
        parent::__construct($data);
    }
}

此外拓扑排序针对的是有向图,而我们之前实现最小生成树和最短路径的时候,都是基于无向图,所以我们还要编写一个新的继承自 EdgeWeightedGraph 的子类:

class DirectedWeightedGraph extends EdgeWeightedGraph
{
    // 添加边
    public function addEdge($s, $t, $weight)
    {
        $etNode = new ENode($t, $weight);
        $this->adj[$s]->insertENode($etNode);
        $this->edges[] = new Edge($s, $t, $weight);
        // 将指向顶点入度值加1
        $vNode = $this->getVertex($t);
        $vNode->in += 1;
    }

    /**
     * @param $position
     * @return bool|VNode
     */
    public function getVertex($position)
    {
        $vertex = $this->adj[$position]->get(0);
        return $vertex;
    }

}

我们在表示有向图的子类 DirectedWeightedGraph 中重写了 addEdge 方法,因为有向图的方向是固定的,需要和无向图区分开来,另外,在新增弧的时候,还为弧指向的顶点更新了入度值。

> 注:记得将父类 EdgeWeightedGraph 中的所有属性可见性调整为 protected,以便在子类中可以访问。

做了以上准备工作后,接下来,我们在子类 DirectedWeightedGraph 中编写拓扑排序实现方法 TopologicalSort

public function TopologicalSort()
{
    $stack = [];  // 存放入度为零的顶点(以栈的方式实现)
    $count = 0;   // 统计输出顶点数
    $sorted = []; // 存储拓扑排序结果
    foreach ($this->vData as $pos => $data) {
        $vertex = $this->getVertex($pos);
        if ($vertex && $vertex->in == 0) {
            $stack[] = $pos;
        }
    }
    while ($stack) {
        $start = array_pop($stack);  // 开始顶点
        $vertex = $this->getVertex($start);
        if (!in_array($vertex->data, $sorted)) {
            $sorted[$start] = $vertex->data;
        }
        $count++;
        $eNode = $vertex->next;
        while ($eNode) {
            $k = $eNode->data;  // 与起点相邻的顶点
            $oVertex = $this->getVertex($k);
            if (!(--$oVertex->in)) {  // 「删除」该弧,将对应顶点入度值减1
                $stack[] = $k;  // 若为0则入栈,以便下次循环输出
            }
            $eNode = $eNode->next;
        }
    }

    if ($count < $this->vNum) {
        return false;   // 存在环
    }

    return $sorted;
}

实现逻辑很简单,分析整个算法,对于一个有 n 个顶点和 e 条边的有向图来说,扫描顶点表查找入度为 0 的顶点的时间复杂度为 O(n),而之后的 while 循环中,每个顶点进一次栈,出一次栈,总共执行此次数为e,所以整个算法的时间复杂度为 O(n+e)。

测试代码可以参照之前编写的示例,这里就不演示了。

rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora