本文主要讨论深度优先遍历与树的前中后序遍历的联系与区别,并对深度优先遍历的几种非递归实现方式进行了对比。
深度优先遍历与树的前中后序遍历的联系和区别
树的前中后序遍历是深度优先遍历应用在树结构上的一(几)种特例。我们都知道树的前序遍历的遍历顺序为:当前节点,左子树,右子树。中序遍历的遍历顺序为:左子树,当前节点,右子树。后序遍历的遍历顺序为:左子树,右子树,当前节点。(这里我使用当前节点表示当前正在处理的节点,为了跟下面深度优先遍历对应)。其实同时还有这么几种遍历方式(1)当前节点,右子树,左子树(2)右子树,当前节点,左子树(3)右子树,左子树,当前节点。这种比较不常规的遍历方式在解决某些题目的时候比较实用。
深度优先遍历通常情况下是访问当前节点,然后访问当前节点的第一个邻居(图),第二个邻居(图)……最后一个邻居(图)。之所以说是通常情况下,是因为还有非通常情况下,我们也可以先访问第一个邻居(图),然后访问当前节点,然后在访问剩下的邻居(图)。事实上,还有另外一种划分法,比如我们将当前节点的邻居根据某种属性(比如树中的左、右)或者方式化成n种邻居,那么先访问第一种邻居中的第一个,第二个,……,第二种邻居中的第一个,第二个……,访问当前节点可以插入其中任何地方。恩,就是这个意思,他们都是深度优先遍历。这里的区别主要在与邻居与当前节点的访问顺序。
这样对比起来,可以很明显的看出来,树的前中后序遍历就是深度优先遍历在树上的特例。前序遍历就对应于将邻居划分两类,先访问当前节点然后在访问邻居节点的情况。而后序遍历对应于将邻居划分为两类,先访问邻居节点然后访问当前节点的情况。中序遍历呢,就是先访问第一类邻居,然后访问当前节点,然后再访问第二类邻居。
深度优先遍历的几种非递归实现方式
- 栈中存储的节点全部都是未访问过的。这里仍旧可以按照节点的展开程序分为两类。先说常见的第一类,完全展开。在这种情况中,访问当前节点后就立马将其出栈,然后入栈其所有的邻居节点。第二类,就是非完全展开了。在这种情况中,得有相应的数据结构支持,也就是说从当前节点,可以知道访问当前节点的父节点的第二个儿子(就是邻接表的那种结构,一个节点的所有邻居是一个链表)。访问完当前(栈顶)节点,将其出栈,然后根据当前节点找到其父节点的第二个儿子,将其入栈,然后再入栈当前节点的第一个未访问的邻居。不过感觉上,这种需要的数据结构有点奇葩。举个简单的列子,在树结构中,除了要父子节点的连接外,还要左儿子和右儿子相连接(这里不是指实际拓扑上相连,而是从左儿子可以访问到右二子)。
非完全展开的,栈中存储的节点比较少,在节点的度比较大的时候比较有效,相反完全展开的,在度比较大的情况下,就需要很大的存储空间,尤其搜索空间再更大的时候,就比较占空间了。这个数量级大概是O(深度*度)。第一类完全展开的,使用起来比较无脑,只用push,pop就行了,不用考虑其他的。而第二类,对数据结构要求有点奇葩。这种情况,由于栈中并未存储已经访问的节点,要获得到当前的路径,并不是很方便,需要自己另开空间来记录。 - 栈中存储的节点,刚好是从开始节点到当前节点的一个路径。当然,它一定包含了某些已经访问的节点,并且显然节点并没有完全展开,并且只有栈顶元素是未访问过的。分析同上,这个也需要存储当前的展开位置,也就是说它的邻居节点展开到哪了,下一次该谁了亦或是没有下一个了。这种情况下,访问当前(栈顶)节点,入栈其第一个邻居,如果没有邻居或者邻居已经访问过了,那么就出栈一直到一个没有完全展开的节点,然后继续展开。好处是显而易见的,占用空间只与深度有关,并且很容易得到当前路径(比如使用vector来实现栈)。
- 栈中存储的节点,不仅仅包含了到当前节点的一个路径。当然每个节点是完全展开的,并且也包含了已访问过的节点。这种实现方式,通常是多余的,不过在某些访问顺序下需要这样做。在先访问,在入栈的话,貌似没有什么好处
。除非要先访问邻居节点,然后再访问自己,这种就类似于后序遍历了。