list遍历_6. 树(二):有根树的遍历

本文将回答以下问题:

  1. 如何遍历一棵有根树?
  2. 如何超越遍历?

如何遍历一棵有根树?

其实要说一棵树的遍历,大家其实都很清楚,无非先序、后序、层次三种(当然中序遍历只存在于二叉树中,我们后面介绍)。不过既然是函数式角度,免不了还要多bb几句。

遍历,是以一定顺序依次访问树的每个结点。所以看到顺序这个词,其实大家就很清楚这个东西并不是函数式的产物,函数式的程序设计中中我们要尽力避免这种东西的使用。在本文最后我们将会分析这个问题。

预先的一些约定

首先,我们仍然沿用上一篇文章的那棵树,也就是

a848ff651046f3a1fff7bc0518318e15.png
荣国府男性族谱
(

存储方式当然也与上一次一样。

为了能让scheme这个函数式语言类似其他过程式语言一样依次做一些事情,scheme给出了这样的语法:

(

为了一个类似过程化语言的实现,我还是写了这个一个用来实现for循环的函数:

(

可以看到就是就是个将这个列表里的每个元素执行一遍函数

,类似python的
for 

或者C++的

for 

先序遍历

众所周知,先序遍历就是先访问根节点,然后依次递归访问孩子,例如C++中我们会这么写:

void 

这里假设的是

template 

借助前面我们定义的for-list,用scheme写的话就是:

(

其中(newline)是换行。

感兴趣的同学可以试一下,将前面的树的声明与这里的代码复制进入scheme解释器中,然后

(

看看效果。

理论上结果应该是这个顺序:

贾代善->贾赦->贾琏->贾政->贾珠->贾宝玉->贾环

后序遍历

后序遍历同理,只不过先递归子树再访问根节点,那么就是

(

不多解释了。一样用上面那棵树测试的话,结果应该是

贾琏->贾赦->贾珠->贾宝玉->贾环->贾政->贾代善

层次遍历

学过图论的同学应该知道,其实先序遍历和后序遍历本质上是深度优先搜索(DFS),而层次遍历就是宽度优先搜索(BFS)。

层次遍历会按照“辈分”来访问结点,也就是说根结点先来,然后是根节点第一层孩子(模仿数学的话应该叫一阶孩子?),接着是根节点的孩子的孩子(二阶孩子),以此类推。

在过程化语言中,我们通常采用队列来完成这个任务。例如C++中:

template 

(当然这里的Queue得用std::deque<*Tree<T>>一类的方法来实现,否则存储量挺惊人的——不过这个不是本文讨论的重点啦)

首先我们补充定义一个函数

,将两个列表合并成一个列表
(

举个栗子,一目了然:

3fc019eef74a2e1c2fc0c7cafc65d3ef.png
cons-list使用说明

然后,我们只需要处理一个to-do-list即可(也就是我们前面的Queue)。代码如下:

(

这里做个备注,也就是新的to-do-list,我这里是

(

一行生成的——其实很简单,(cdr to-do-list)就是pop以后的queue,而(cdr (car to-do-list))是pop出来的那个结点的孩子列表,正好cons-list一下就行。

以前面荣国府的栗子,结果应该是:

贾代善->贾赦->贾政->贾琏->贾珠->贾宝玉->贾环

如何超越遍历?

如果只写到这里,我们只是用函数式语言中一个最不好的东西(scheme的串行begin)来实现过程语言的任务——用不合适的语言,以一个丑陋的形式去做。

但是回到函数式的起点,我们要函数式的作用,其实是为了避免思考操作的先后顺序,用函数来完成全部任务。而遍历这个事情本身,就是要给出一个操作顺序,自然是先后矛盾的。

另一个角度,其实遍历很多时候是出于一种无奈——树是分叉的,但是(冯·诺伊曼)计算机的体系结构同时只能做一件事情,于是我们不得不考虑给他们一个计算的先后顺序。比如说一棵数字树,我要算所有结点的数值之和,因为体系结构只能将两个数字加起来,所以我们必须初始化一个sum = 0,然后按照一定顺序把树的每个结点x都来个sum += x。

函数式,我们试图不考虑体系结构。正如当年mit-scheme诞生的时代——1975年,那时离第一枚多核处理器的诞生还有30年历史,离现在并行计算最常用的设备——GPU的诞生也还有24年,但是当年的先驱者就已经在scheme中引入了

这样的并行语句,并且用随机数这些方法去模拟并发计算时顺序的不确定性。畅想未来,也许我们终有一天能借助某些技术(生物?量子?)去实现同时任意多线程的并发,而不是今天受制于核心数的并发,那个时候,树的遍历又会是怎样呢?

我想,也许,那个时候就不再有遍历的问题了,我们也许可以同时对全部子树分别开一个线程,这样我们便无所谓顺序了。这也许正是函数式的高瞻远瞩之处。

以前面的栗子,如果我们需要把一棵树的全部结点加起来(而不是display这样的过程),我们可以完完全全用纯粹的函数式的手法去描述(对先序的程序稍加修改即可):

(

如果此时,我们能把+并行计算,比如说(+ (f a) (f b)),能开两个线程同时计算(f a)和(f b),然后再求和,那么其实就实现了我们想要的并发模式——再想想,此时这个“访问顺序”还是先序吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值