层序遍历 和 基于栈实现的先\中\后序遍历

1 层序遍历 level order traversal

请添加图片描述
  以完美二叉树举例,但适用于任何m叉树。


PS1:分析时不用要从0开始编号,从0开始编号是计算机和数学的一个显著差异,二者关注的东西是不一样的,从0开始在乎的是移位的性能,从1开始才是数学上的分析(0是加法的单位元,1是乘法的单位元,是先有逻辑再有代号);
PS2:上面的二叉树,可以看到根节点的序号是左儿子序号的一半,可以通过 n n n层的完美二叉树的节点总数是 2 n − 1 2^n-1 2n1来证明。


  上面的二叉树实际上可以拆分成:
请添加图片描述
  可以看到层序遍历的实现就是以三个节点(根左右)为一个最小单元,最开始的根节点1在队列,然后从1开始的各个最小单元执行:根出列、左右儿子依次入列的操作,这样循环就实现了层序遍历。

  上面的拆分就是层序遍历的实质,这种拆解和层序遍历的实现也说明了为什么要用队列。也是二叉树的一种分解方式,注意这种分解是包含链的,第一棵子树的左右儿子链到后面的单元子树。

  但特别注意的是,目前说的其实是广度优先搜索,还不是真正意义上的层序遍历(有细微差异),直接给代码:

// breadth first search:
	void BreadthFirstSearch(TreeNode root) {
   
		Queue<TreeNode> queue = new Qeque<>();
		queue.enqueue(root);
		
		while (!queue.isEmpty()) {
   
		TreeNode node = queue.dequeue();
				if (node.left != null) {
   
					queue.enqueue(node.left );
				} // of if
				if (node.right != null) {
   
					queue.enqueue(node.right);
				} // of if
			} // of while
		}// of BreadthFirstSearch(TreeNode)

  在 BFS 遍历的基础上区分遍历的每一层,就得到了层序遍历。简单的说,输出顺序没变,但是多了层的概念。层序遍历可以是二维数组,而 BFS 的遍历结果只是一个一维数组
在这里插入图片描述
直接看代码,其实就是BFS的循环内,再一个以队列元素个数(即各层节点数)的内部循环。

	void levelOrderTraversal(TreeNode root) {
   
		Queue<TreeNode> queue = new Queue<>();
		queue.enqueue(root);
		
		while (!queue.isEmpty()) {
   
			int NumPerLayer = queue.depth();
			
			for (int i = 0; i < NumPerLayer; i++) {
   
				TreeNode node = queue.dequeue();
				
				if (node.left != null) {
   queue.enqueue(node.left);}
				if (node.right != null) {
   queue.enqueue(node.right);}		
			} // of for i
		} // of while
	} // of levelOrderTraversal(TreeNode)

  

2 基于栈实现的先/中/后序遍历 pre/in/post order traversal

  1. 怎么想到 栈这种数据结构?
  2. 怎么结合 栈给出简洁有效的代码?

  关于第一个问题,以下图为例:
在这里插入图片描述
  我们是先走到左子树,一直走到树叶,输出了再逐一退回到根进入各根的右子树,也就是我们先碰到的数据不是直接用到,而是会先保留,等后面的用完了,再利用先碰到的点,比如这里进入右子树就必须要根节点作为桥梁。所以,我们需要一种栈这样设计原理的数据结构。

  关于第二个问题,再回到上图。个人采用如下简记方式(代码太低效了,还是得纸、笔、公式):n表示图中标记为n的节点,+n表示push(n)操作,-n表示pop(n),pn表示print(n),特别的,null节点在这里记为0。首先以中序遍历进行分析:

  它的中序遍历是:2 7 4 1 8 5 9 3 6
  中序的特点是:左中右。因此,中序出栈的关键:若有左儿子则一直进栈,否则根节点出栈的同时,让右子树根节点进栈。右子树根节点即右儿子,它是根节点到右子树的链,对于中序出栈,右子树根节点进栈就是从左子树和根节点完全转到右子树的意思。中序的关键在于根节点出栈在右子树进栈前。
  中序遍历对应的栈操作是:+1; 1 +2; 12 (-2+4); 14 +7; 147 (-7+0); 14 (-4+0); 1 (-1+3); 3 +5; 35 +8; 358 (-8+0);
35 (-5+9); 39 (-9+0); 3 (-3+6); 6 (-6+0)。
  上面实质为循环的每一步的变量和操作,上面写出来,循环就基本写出来了(具体实现时,也需要见识了大量代码后熟练了才行),同时方便理解、纠错、设计新算法,比如后面的先序遍历就是我自己设计的,哪怕其他人早就给出了吧。同时中序遍历给出了一种不够巧妙、但逻辑清晰的算法实现。

  即中序遍历的出栈顺序是:2 7 4 1 8 5 9 3 6,也就中序出栈的顺序(出栈前打印)就是中序遍历的顺序——这句话看似是显然的,但其实不是,后面我们可以看到出栈顺序和打印的遍历顺序可以是不同的,也即如果按打印顺序(实现的效果)来判断是不是先序的话,那么中序的出栈顺序可以实现先序遍历的效果,但实现方式其实是中序的,这是被很多代码混淆的。
  实际上,除去打印操作的位置不同,它们的栈操作是一致的:


在这里插入图片描述
  接着分析先序遍历。该图的先序遍历是:1 2 4 7 3 5 8 9 6

  我们 可以用中序出栈顺序实现先序打印顺序 的效果:+1p1; 1 +2p2; 12 (-2+4p4); 14 +7p7; 147 (-7+0); 14 (-4+0);
1 (-1+3p3); 3 +5p5; 35 +8p8; 358 (-8+0); 35 (-5+9p9); 39 (-9+0); 3 (-3+6p6); 6 (-6+0)。

  这其实说明了打印顺序(即实现的效果)本质上并不是遍历顺序本身 ,前面中序出栈实现了先序打印,因为先序打印只是先打印了根节点元素(而没有出栈),或者是只要采用 根节点来作为一侧子树进入另一侧子树的链,那么根节点就必然晚于左子树节点出栈!
  因此,一方面根节代码输出结果判断代码显然是有缺陷的,另一方面,个人认为:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值