之前关于深度和广度优先遍历觉得算是比较简单的东西了,特别是深度优先遍历,用递归实现起来几乎是非常自然的,然而最近进行了一些思考探索,仍然有一些非常有意思的点,不论是从实际应用,还是优化方向。由于线性结构遍历比较朴素就不讨论了,这里主要针对图和树两种模型来探讨。

深度优先遍历还是广度优先遍历

从结果上看,二者都是遍历整个关联结构,而且时间复杂度都一样,跟对象群的规模呈线性关系, 没有太大的影响,但过程上还是有些差别。

我们先来聊下深度优先遍历

深度优先遍历有递归和非递归的写法,对于递归来说,它的代价在于递归的栈开销,所来带的问题是当递归的层次过深,便可能有栈溢出的问题。我们知道栈空间相比堆空间来说要小很多。但递归的好处在于设计思路清晰,实现起来非常的自然和容易。因此在具有较多栈空间开销的递归实现中(参数多,局部变量多),就要考虑优化。

我们可以看一个经典的例子:C#中的Sort

我们一般认为商业代码中,会使用快排作为默认的排序实现,实际上C#中的Sort有两种以快排为原型的改进,一种称为DepthLimitedQuickSort,一种称为IntroSort

通过DepthLimited关键字我们也能猜出大概的意思,就是对递归深度做限制,我们看一段源代码

internal static void DepthLimitedQuickSort(T[] keys,int left,int right,IComparer<T> comparer,int depthLimit){.....while (depthLimit != 0){--depthLimit;if (index2 - left <= right - index1){if (left < index2)ArraySortHelper<T>.DepthLimitedQuickSort(keys, left, index2, comparer, depthLimit);left = index1;}else{if (index1 < right)ArraySortHelper<T>.DepthLimitedQuickSort(keys, index1, right, comparer, depthLimit);right = index2;}if (left >= right)return;}ArraySortHelper<T>.Heapsort(keys, left, right, comparer);}

期间我省略了一些不重要的代码,通过观察可以知道,在递归深度限制之内,仍然是递归调用排序,当超出递归深度后转而调用堆排序,这就是对递归深度的优化。

而C#官方给的这个最大递归深度是32

ArraySortHelper<T>.DepthLimitedQuickSort(keys, index, length + index - 1, comparer, 32);

这里给出微软官方底层实现的源代码链接,感兴趣的可以看一下,多看源码收获还是很大的,读商业代码和学习代码完全是两种感受。

https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs

至于IntroSort就做了更多的优化,不仅有深度限制,还会在元素个数少于16个的时候,直接调用插入排序等其他针对更小规模数据的排序方法。

有些同学可能会觉得,32是不是很大了,按照我们对二叉树的理解,一棵32层深的书,包含的叶子节点数在2^31。这只是理论上限,虽然快排做了很多优化,但快排的轴划分毕竟不会是完全均衡的。我们学习快排的时候知道,快排在最差的情况下,复杂度是O(N^2)的,就是因为轴划分不均等的影响。在此种情况下,递归深度就不能按照这个来计算了。

那如果是非递归实现呢?我们知道深度优先遍历的非递归实现依赖于一个栈,递归过程中会将遍历的节点子孩子压入栈,通过逆序弹出来达到深度有限的效果,但对于一个简单实现来说,栈最大存储占用并不是仅仅和树的高度有关,因为你总是会将遍历的某个子树的第一个节点的所有孩子都压入栈。在极端情况下,当你真正开始遍历第一个节点的时候。所有的元素都已经压入栈中,感兴趣的同学可以思考下这是种什么情况。

另外在一些通过深度优先遍历进行重新分配空间的情况来说,比如GC中的压缩算法,通过深度优先遍历,可以让有关联的对象排列在一起,从而增加缓存的命中,提高速度。

另外请注意,前边所涉及的情况都是在假设我们所遍历的结构是一棵树,如果是图结构的话,还必须考虑带环的问题,需要记录下已经访问过点以防止重复访问。

我们在笔试的时候曾出过一道题目,就是深拷贝一个对象。很多同学能够意识到对引用类型对象需要递归的进行拷贝,却忽略了不同属性引用同一个对象,属性引用自身,以及环式引用(循环链表)。在考虑遍历时一定要对所遍历对象的模型认识清楚,从而选择最优的实现方法。

接下来我们说一下广度优先遍历

广度优先遍历一般是需要依赖队列这种数据结构来实现的,其天然的迭代属性使得其结构性开销通常来说会比较小。如果我们把遍历的过程展开成一棵遍历树的情况下会发现,队列中最大元素的个数是和树的宽度相关的,而深度优先遍历之前也说了,是和树的高度相关的。因此树越扁平化队列的峰值消耗就会越大。但在实际应用中,广度优先遍历的消耗会比预计的要大。我们从两点来考虑这个问题。

第一、从缓存命中的角度看,队列的入队和出队是在队列的两头进行操作的,相对于总是从一端进行操作的栈来说,当队列元素过多的时候,缓存失效的可能性会更大。

第二、从容器的扩容机制来说,队列底层也是用数组实现的(C#),当数组元素不足以容纳元素的个数的时候,数组会以2倍扩容的机制进行扩容。当我们卡到临界点的时候,甚至可能会造成一倍的空间浪费。

关于空间的浪费其实是增大了峰值内存的消耗,我们关注峰值是因为过高的峰值内存可能会引起程序的闪退,特别是做移动应用开发的时候。因为这部分内存最终在遍历结束后会清空,所以并不会造成后续的困扰。

在一些特定的情况下,广度优先遍历可以省去队列这种额外的需要。例如我们需要用线性表收集所有遍历后的结果。就可以直接将线性表本身作为队列,通过双指针的模式来模拟队列的行为,就可以达到最终的效果。

举个例子:

List<Node> res = new List<Node>();
res.Add(root);
for (int i = 0; i < res.Count; ++i)
{res[i].Children.ForEach(child => res.Add(child));
}

请注意,这里仍然是以C#为例去写的,请确保你足够了解系统的机制,比如这里的res.Count,这是一个属性而不是表达式,并不会提前计算结果。因此才能保证每次插入后循环会继续进行。

通过IL代码可以看到,每次循环都会重新调用方法获取Count

IL_0048: callvirt     instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()

关于非递归算法的补充说明

非递归的实现方式还有一个好处是,可以做分步处理,因为所有的待考察对象都维护在一个栈或队列等容器里,因此我们可以将这个容器保存起来,每次只执行规定的迭代次数。这也是了解垃圾回收算法中的增量式回收的概念时看到的。不过递归算法,特别是广度优先,改写为多线程似乎更容易一些,因为没有实际应用过,就不讨论了

总结

在实际的应用开发中,搜索和遍历是经常遇到的情况,这里仅针对两种常见的遍历方法进行了一些深入的探讨,不过也可以发现,我们例子里多数情况下,遍历的过程并不会改变原对象,而在实际的处理中,还会有在遍历中操作等更复杂的行为,比如先序遍历和后续遍历,对于遍历过程中比较依赖于父子关系的情况,就要仔细考虑实现的模式了。代码设计的难易度,时间,空间效率都要根据实际情况去做权衡。

关于深度优先遍历和广度优先遍历的一些深入思考相关推荐

  1. 二叉树深度优先遍历和广度优先遍历

    二叉树深度优先遍历和广度优先遍历

  2. 多级树的深度优先遍历与广度优先遍历(Java实现)

    目录 多级树的深度优先遍历与广度优先遍历(Java实现) 节点模型 深度优先遍历 广度优先遍历 多级树的深度优先遍历与广度优先遍历(Java实现) 深度优先遍历与广度优先遍历其实是属于图算法的一种,多 ...

  3. 二叉树的深度优先遍历和广度优先遍历

    二叉树是一种很重要的数据结构,对于二叉树的遍历,有深度优先遍历和广度优先遍历,深度优先遍历又有先序.中序.后续遍历,广度优先遍历就是按层遍历. 1. 深度优先遍历 深度优先遍历,也就是先序.中序.后续 ...

  4. 广度优先搜索生成树怎么画_图的深度优先遍历与广度优先遍历以及最小生成树...

    图的深度优先遍历 题目:写出附从每个顶点出发的一次深度优先搜索遍历序列.在纸上画出遍历过程和序列,提交截图. 错误回答 从A点开始遍历:0124-01324-0134-0324-034 从B点开始遍历 ...

  5. 大话数据结构 17:图的深度优先遍历和广度优先遍历

    深度优先遍历 主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底-,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点 ...

  6. [js] 解释下深度优先遍历和广度优先遍历的区别及如何实现

    [js] 解释下深度优先遍历和广度优先遍历的区别及如何实现 1.深度优先采用堆栈结构,先进后出,所占的空间较小,执行时间较长: 2.广度优先采用队列结构先进先出,所占空间较大,执行时间短,空间换时间: ...

  7. 图:图的邻接表创建、深度优先遍历和广度优先遍历代码实现

    邻接表介绍 邻接矩阵是不错的一种图存储结构,但是我们也发现,对于边数相对顶点较少的图,这种结构比较较浪费存储空间.如果不想浪费存储空间,大家肯定会先到链表.需要空间的时候再才想内存去申请,同样适用于图 ...

  8. 图:图的邻接矩阵创建、深度优先遍历和广度优先遍历详解

    邻接矩阵介绍 直接说,邻接矩阵是图的一种存储结构.那么图是什么呢?图是一种逻辑结构,和线性结构.树形结构.集合结构一样 是一种逻辑结构用来描述数据对象中的数据元素之间的关系.来看下图的定义:图(Gra ...

  9. 数据结构之图:邻接矩阵和邻接表、深度优先遍历和广度优先遍历

    简介 线性表是一种线性结构,除了头结点和尾节点,线性表的每个元素都只有一个前取节点和一个后继节点.而树结构则相较于线性表更加复杂,它描述的关系为数据元素之间的父子关系,也是现实世界父子关系的缩影, 一 ...

  10. 数据结构—无向图创建邻接矩阵、深度优先遍历和广度优先遍历(C语言版)

    无向图创建邻接矩阵.深度优先遍历和广度优先遍历 一.概念解析: (1)无向图: (2)邻接矩阵: 二.创建邻接矩阵: 三.深度遍历.广度遍历 (1)深度遍历概念: (2)广度遍历概念: 四.实例展示 ...

最新文章

  1. 查看mysql是否安装成功和mysql的版本信息
  2. android9.0首发机型,安卓9.0正式发布,EMUI开启多款机型同步内部测试
  3. stand-alone android sdk tools,android make-standalone-toolchain.sh 使用说明
  4. iis7+php_5.5,IIS7+php5.5+fastcgi
  5. ADSL Modern+无线路由实现无线上网
  6. SAP gateway系统和后台系统的OData双重cache机制
  7. Theano 中文文档 0.9 - 7.2.1 起手式 —— 代数
  8. winform 填充圆形 锯齿_Qt项目中,三种图形渐变填充方式详细总结
  9. FastDFS之文件服务器集群部署详解
  10. 阶段3 1.Mybatis_01.Mybatis课程介绍及环境搭建_01.mybatis课程介绍
  11. WinForm主窗口框架的设计
  12. 浅析h3c交换机端口模式access,trunk与hybrid之联系与区别
  13. 目前可用的通用DNS
  14. windows保护无法启动修复服务器,解决使用sfc命令提示“windows 资源保护无法启动修复服务”的方法...
  15. RAW 图像格式转换工具 bayer2rgb
  16. flv网页播放器开源代码
  17. “九阳神功”是怎么炼成的?
  18. 名编辑电子杂志大师教程 | 如何调用外部本地文件?
  19. 笔记本双系统安装Ubuntu 20.04.3 LTS没有WIFI的解决方法
  20. 解析单总线协议(1-wire)

热门文章

  1. IcedTea:首个100%兼容、开源的Java
  2. ICT融合和创新带来制造业的持续变革
  3. 「产品读书」精益创业
  4. 小刘同学的sansen-virtuosoIC618电路仿真记录
  5. npm 清理缓存命令 【最新的】
  6. 基于Tiny6410的LCD与一线触屏移植
  7. DirectX11 指定材质
  8. 基于ARM处理器的U-BOOT详细移植总结
  9. Maven3.8.1下载
  10. 子网掩码-掩码位-反掩码 对照表