《编程珠玑(续)(修订版)》—第2章2.3节拓扑排序

2.3 拓扑排序
拓扑排序算法的输入是一个有向无环图,例如:

如果图中包含从A到B的边,那么A就是B的祖先而B是A的后代。算法必须对图中结点排序以使得所有祖先出现在它们的后代之前,下图是许多可能排序中的一种。

算法还必须能够处理输入图中包含回路因此无法完成排序的可能情况。
这样的算法可以应用于绘制物体的三维场景。如果物体B在视图中处于物体A的前面,那么A就在B之前,因为A必须在B之前完成绘制。下图左边由4个矩形构成的场景能够导出右边的偏序。

图2-5中有两种对顶点的有效排序,即A B C D和A C B D。每种排序都恰当地给出了物体的覆盖图。拓扑排序的其他应用包括对技术文档进行布局(术语在使用之前必须先定义)以及处理层次化VLSI设计(算法在处理一个部件之前必须先处理其组成部分)。继续阅读之前,请花一分钟思考你将如何编写对有向图进行拓扑排序的程序。
我们将研究一个拓扑排序算法,该算法引自Knuth的《计算机程序设计艺术,卷1:基本算法》④一书的2.2.3节。该算法的循环步骤如下:选出一个没有祖先的结点T,将T写到输出文件中,然后从图中删除从T出发的所有边。下图显示了这个算法如何应用在之前的四结点场景图上。算法的各阶段从左向右进行,每一阶段选出的结点T都画了圈。

排序结果是A B C D。
这个算法的一种低效的实现在每一步都要扫描整个图以找到没有祖先的结点。现在来研究一个更简单也更高效的算法。对每个结点,新算法都存储它的祖先数量以及后代结点集合。比如,上面所画的四结点图表示如下:

算法的循环步骤每次选出一个祖先结点数为零的结点,把它写到输出文件中,其所有后代结点的祖先数量全部减少1。然而,算法必须仔细记住不同顶点其祖先计数变为零的先后顺序,这里使用了一个结点队列。(如果在所有结点的祖先计数变为零之前队列为空,那么程序报告图中存在回路。)
下面的伪代码假设输入给程序的图是由一系列(祖先,后代)对表示的,每行一对。

as each (pred, succ) pair is read
   increment pred count of succ
   append succ to successors of pred
at the end of the input file
   initialize queue to empty
   for each node i
     if pred count of i is zero then append i to queue
   while queue isn't empty do
     delete t from front of queue; print t
     for each successor s of t
        decrement pred count of s
        if that goes to zero then append x to queue
   if some nodes were not output then report a cycle

算法的运行时间与图中的边数成比例,只比最理想的情况多出一个常数因子。(每条边处理两次:一次是读,一次是从队列中将其删除。)

Awk程序使用一个索引范围是1..n的数组来实现队列。整型变量qlo是队列首元素的索引,qhi是尾元素的索引。后代集合由两个数组实现。如果A有后代B、C、D,那么下面的关系成立:

succct["A"] == 3
succlist["A",  "1"] == "B"
succlist["A",  "2"] == "C"
succlist["A",  "3"] == "D"

这个Awk程序的输入是一个祖先-后代对文件,其输出是排序好的结点列表或关于这样的列表不存在的警告。

{ ++predct[$2]        _#_ record nodes in predct,
    predct[$1] = predct[$1]  _#_ even those with no preds
    succlist[$1, ++succcnt[$1]] = $2
   }
END  { qlo = 1
    for (i in predct)  {
      n++; if (predct[i] == 0) q[++qhi] = i
    }
    while (qlo <= qhi)  {
      t = q[qlo++]; print t
      for (i = 1; i <= succcnt[t]; i++)  {
         s = succlist[t, i]
         if (--predct[s] == 0) q[++qhi] = s
      }
    }
    if (qhi != n) print "tsort error: cycle in input"
}

程序第二行保证所有结点都作为predct的索引出现,即便没有祖先的结点也一样。

本程序的关联数组表示了几种不同的抽象数据类型:一个结点名字的符号表、一个记录数组、一个二维的后代集合序列以及一个结点队列。本程序小巧、便于理解,但在较大的程序中无法清楚分辨不同的抽象数据类型就会降低程序可读性。

时间: 2024-12-25 07:23:41

《编程珠玑(续)(修订版)》—第2章2.3节拓扑排序的相关文章

《编程珠玑(续)(修订版)》目录—导读

内容提要 编程珠玑(续)(修订版) 本书是计算机科学方面的经典名著<编程珠玑>的姊妹篇,讲述了对于程序员有共性的知识.本书延续了<编程珠玑>的特色,通过一些精心设计的有趣而又颇具指导意义的程序,对实用程序设计技巧及基本设计原则进行透彻而睿智的描述,为复杂的编程问题提供清晰而完备的解决思路.书中涵盖了程序员操纵程序的技术.程序员取舍的技巧.输入和输出设计以及算法示例,这些内容结合成一个有机的整体,如一串串珠玑展示给程序员.本书对各个层次的程序员都具有很高的阅读价值. 版权声明 编程珠

编程珠玑--粗略估算

粗略估算是<编程珠玑>中第七章提到的内容.   这篇文章将"粗略估算"看做是一项工程技术,是程序员必备的一项技能之一. 本人非常同意这个观点.粗略估算是一种把复杂的事情简单化的能力.我们对某个算法的时间复杂度和空间复杂度的估算就是基于这种估算的能力.如果你能较为准确的估算出一个程序的输出结果,如果你能准确估算出这个程序的运行时间,如果你能准确估算出这个项目的开发时间--如果你能拥有这样的能力,该有多么美好啊.所以难怪乎像微软.Google这样的大公司老喜欢出"请计

《编程珠玑(续)(修订版)》—第1章1.1节计算素数

第1章 性能监视工具 编程珠玑(续)(修订版) 听诊器是一种简单工具,却给医生的工作带来了革命:它让内科医生能有效地监控病人的身体.性能监视工具(profiler)对程序起着同样的作用. 你现在用什么工具来研究程序?复杂的分析系统很多,既有交互式调试器,又有程序动画系统.正如CT扫描仪永远代替不了听诊器一样,复杂的软件也永远代替不了程序员用来监控程序的最简单工具--性能监视工具,我们用它了解程序各部分的执行频率. 本章先用两种性能监视工具来加速一个小程序(记住真正的目的是说明性能监视工具).后续

《编程珠玑(续)(修订版)》—第2章2.1节Awk中的关联数组

第2章 关联数组编程珠玑(续)(修订版)人类学家说,语言深刻地影响了世界观.一般把这个观察结果称为"Whorf假说"① ,也经常把它总结为"语言塑造了人的思想". 跟大多数程序员一样,我使用的Algol系列的语言塑造了我的计算思维.对于像我这样的程序员来说,PL/1.C和Pascal看起来都很相似,我们不难把这样的代码翻译成COBOL或Fortran的代码.用这些语言能轻易地表达我们旧的.习以为常的思维模式. 另外一些语言则挑战了我们对于计算的看法.我们感到惊奇的是

《编程珠玑(续)(修订版)》—第2章2.4节原理

2.4 原理Awk可以使程序员事半功倍.我们目前看到的多数程序如果使用传统的语言编写,代码量恐怕会多出一个数量级.规模的减小归功于Awk的几个特性:输入行之上的隐式循环.自动分隔成字段.变量的初始化和转换,以及关联数组. 关联数组是Awk将字符串和数值这样的基本数据类型结合起来的唯一机制,别无他法.幸而关联数组可以很自然地表示许多数据结构. 数组.一维.多维和稀疏数组实现起来都很容易.顺序结构.队列和栈是由关联数组加一个或两个索引产生的.符号表.符号表提供了从名字到值的一个映射:symtab[n

《编程珠玑(第2版•修订版)》—第1章1.1节一次友好的对话

第一部分 基础 编程珠玑(第2版•修订版) 这一部分的5章回顾程序设计的基础知识.第1章介绍一个问题的历史,我们把仔细的问题定义和直接的程序设计技术结合起来,得到优美的解决方案.这一章揭示了本书的中心思想:对实例研究的深入思考不仅很有趣,而且可以获得实际的益处. 第2章讨论3个问题,其中重点强调了如何由算法的融会贯通获得简单而高效的代码.第3章总结数据结构在软件设计中所起到的关键作用. 第4章介绍一个编写正确代码的工具--程序验证.在第9章.第11章和第14章中生成复杂(且快速)的函数时,大量使

《编程珠玑(第2版•修订版)》—第2章2.1节三个问题

第2章 啊哈!算法 编程珠玑(第2版•修订版) 研究算法给实际编程的程序员带来许多好处.算法课教给学生完成重要任务的方法和解决新问题的技术.在后续的章节中将会看到,先进的算法工具有时候对软件系统影响很大--减少开发时间,同时使执行速度更快. 算法与其他那些深奥的思想一样重要,但在更一般的编程层面上具有更重要的影响.在<啊哈!灵机一动>一书中(本章的标题就借鉴了它),Martin Gardner①描述了深得我心的一个思想:"看起来很困难的问题也可以有一个简单的.意想不到的答案.&quo

《编程珠玑(第2版•修订版)》目录—导读

作者简介编程珠玑(第2版•修订版)Jon Bentley 世界著名计算机科学家,被誉为影响算法发展的十位大师之一.他先后任职于卡内基-梅隆大学(1976~1982).贝尔实验室(1982~2001)和Avaya实验室(2001年至今).在卡内基-梅隆大学担任教授期间,他培养了包括Tcl语言设计者John Ousterhout.Java语言设计者James Gosling.<算法导论>作者之一Charles Leiserson在内的许多计算机科学大家.2004年荣获Dr. Dobb's程序设计卓

编程珠玑--旋转算法

旋转算法出自<编程珠玑>第二章题目.   <编程珠玑>一书对算法是极度推崇,这点意识在我们看书的时候每每都有被灌输.使用一种好的算法往往能使得程序更加漂亮,也很能带给我们程序员某种满足感.   题目:将一个n元一维数组a[n]左移i个位置.例如,当n=8,i=3时,数组abcdefgh旋转为defghabc.请设计一个算法完成这个任务.   1. 块交换法: 分析:将n元一维数组a[n]分解为两块,将第一块存储在临时数组中,将第二块前移i个单位,再将临时数组加入到第二块后面.