题目:请用非递归方式实现二叉树的先序、中序和后序的遍历打印。给定一个二叉树的根结点root,请依次返回二叉树的先序,中序和后续遍历(二维数组的形式)。
思路:对于非递归的方式遍历二叉树,实现起来较麻烦,由于二叉树是一种特殊的结构,有自己的特征,因此会有遍历二叉树的方法,只是这种规律较为复杂,并不直观,而且要借助额外的数据结构(栈),因此较为麻烦,这里只需要理解代码的意义然后记住先序、中序、后序遍历的逻辑,记住实现的代码,即先接受这种遍历的方法再慢慢理解,通过记住操作过程来写代码,之后慢慢理解。
其实代码很简单,关键是理解,之后很多二叉树的问题是基于树的遍历的,因此要会对遍历的代码进行改编和灵活使用。
先序遍历:
所谓先序遍历是指对于任何的子树,先遍历根结点,再遍历左子树,再遍历右子树。
①创建一个栈用来存放结点对象treeNode,Java类库中有Stack类,Stack
②首先将根结点root压入栈中,stack.push(root),然后从栈中弹出栈顶元素root,每弹出一个结点记作cur,去判断这个弹出的结点是否有左结点、右结点;如果有的话,先将其右结点压入栈中,再将其左结点压入栈中,如果没有结点就不用压入,即每当从栈中弹出一个结点时,要将这个结点的右结点和左结点压入到栈中(弹出一个压入2个,且先压入右结点再压入左结点,因为栈是先进后出的,因此为了先弹出遍历左结点后右结点,需要向栈中先压入右结点再压入左结点),注意这里对于弹出的结点,不管是否有子节点,都要进行这个动作,即判断有无右结点和左结点,有的话压入右结点和左结点。即弹出一个结点之后就要进行2个动作,压入该结点的右结点和左结点,压入之前添加一个判断即可,如果node.right!=null,则stack.push(node.right);如果node.left!=null,则stack.push(node.left),之后再从栈顶弹出栈顶结点并将这个结点的2个子节点压入栈中,即程序的循环逻辑总是先弹出一个栈顶元素,然后将这个元素的右结点、左结点分别压入栈中,然后再弹出一个栈顶元素……当发现栈中没有元素时,停止循环。(理解:循环开始前会先压入root结点,循环开始时总是弹出一个结点压入0个或者1个或者2个结点,一开始通常是弹出1个结点然后压入2个结点,因此栈中的元素会逐渐变多,后来当遍历到叶子结点时总是弹出一个但是压入0个,因此栈中结点会逐渐变少,直到最后一个结点弹出但是又没有新的子节点压入,于是下一次循环开始时栈为null,于是遍历结束,即当stack==null时遍历结束),每次弹出的结点就是当前遍历的结点,于是在每次弹出一个元素时将它打印或者将它放入到集合list中存放。
中序遍历:
所谓中序遍历就是先遍历左子树,再遍历根结点,再遍历右子树。
①创建一个栈Stack
②首先将根结点root压入到栈中,注意理解,中序遍历时会先把最底层最左端的结点④结点压入到栈中,即总是循环进行temp=temp.left,将从root开始的左边界路径上的结点都一次放入到栈中,于是栈中从下到上是①②④,当temp为null时说明左边界路径已经遍历完成,此时从栈中弹出栈顶元素④,每次弹出的元素就是当前遍历的元素;对于弹出的元素已知其左子树是都在栈里的,因此检查它的右子树是否为null,如果为null就继续从栈中弹出一个元素;如果不为null,那么可知这个结点有右子树,此时应该遍历这个右子树(既然已经到了这个步骤,说明这个结点的左子树已经遍历好了,按照中序遍历的顺序,在遍历完左子树、根结点之后应该遍历右子树);遍历右子树的过程和遍历整棵树的过程是完全一样的,令temp=node.right(node是从栈中弹出的结点),也是先遍历这个子树的左边界,temp=temp.left并将左边界上的结点都压入到栈中,当temp==null时从栈中弹出1个结点,检查这个结点是否有右子树,没有右子树就继续从栈中弹出结点,直到
即整个循环过程是这样的:先将根结点root压入到栈中,初始化temp=root,然后while(temp!=null);将temp压入栈中,并令temp=temp.left(注意是先压入再向左移动);当temp==null时,从栈中弹出一个结点表示当前遍历的结点;对于弹出的结点,令temp=temp.right(相当于一棵子树的根结点,相当于从新开始,这个temp相当于root);判断然后逻辑和之前一样,即进行一个新的while(temp!=null)的循环,先将temp压入栈中,然后一直取temp的左边界并存入到栈中,直到temp==null时从栈中弹出一个结点,并将temp赋值为temp.right。即总是先访问(访问不等于遍历)左边界,当左边界访问结束后,向后退一个元素,弹出一个元素然后取其右结点作为根结点进行相同的操作;当弹出的结点的右子树一直为null进一直无法进入while(temp!=null)循环从而无法向栈中添加元素,但是要一直从栈中弹出结点,显然这是一个循环过程,且当栈中元素弹光后就要停止循环。因此在while(temp!=null)循环的外面还要有一层循环,当栈中还有结点时才能进行循环体,当栈中没有结点时循环结束,即while(stack!=null){}。
理解:对于任意一棵树或者子树,总是先将其左边界上的所有结点依次放入到栈中,当左边界上访问结束后,就从栈中弹出一个结点(也就是在左边界上从后往前弹出一个结点进行遍历),然后取这个弹出结点node的右子树结点node.right作为temp,这个结点可以看做是开始时候的根结点,之后的操作是相同的。当栈中元素为null时,循环结束,遍历结束。
为了与先序遍历保持统一,即root结点单独压入栈中,因此挑这个代码:
①创建一个栈stack用来存放结点,创建一个指针遍历cur用来表示当前正在访问的结点,将cur初始化为root结点,将root结点压入到栈中stack.push(cur);
②while(cur!=null),一直取cur结点的左结点,即cur=cur.left,并将其压入push到栈中(先取left结点再将其压入到栈中),当cur==null时,说明左边界遍历结束,此时从栈中弹出pop一个结点node,这个结点就是当前遍历的结点;取出这个结点的右子树,cur=node.right,这个结点就相当于根结点root,判断cur是否为null,如果if(cur!=null),说明node结点有右子树,于是使用当前的cur开始新的一次循环即可(进入while(cur!=null)逻辑),与根结点root一样,由于while循环中是先得到cur的左结点,在将左结点压入栈中,于是这个结点需要手动压入栈中push(cur)之后才重新进入while(cur!=null)循环;如果if(cur==null),说明,node结点没有右子树,于是直接进入while(cur==null)的循环即可,即再从栈中弹出一个结点即可;一直按照这2中情况进行处理,当最后stack中结点是null时,说明遍历结束。
总的来说:逻辑很简单,先将子树根结点压入栈;然后访问左边界并将结点顺序压入栈中;当左边界压完之后(即cur==null时)弹出一个结点node,取这个结点的右结点作为cur,先将其压入栈中然后重新开始循环即可;直到栈中没有元素时遍历结束。
规律:只要弹出一个结点,cur就跑到cur的right上;只要弹出的结点的右结点cur为null就接着弹出;当结点的左结点为null时就从栈中弹出结点;弹出结点之后要判断其右结点是否为null,如果不为null,要将其右结点压入到栈中。因此在循环过程中,有时候当弹出一个结点后stack会变空,但是由于此时循环体还没有结束,此时对于弹出的结点node,要取它的right作为cur,如果cur不为空要将其压入stack中,于是stack并不为null,只有当此时cur也为null,那么说明真的没有元素了。
后序遍历:
所谓后序遍历是指先遍历根结点的左子树,再遍历根结点的右子树,再遍历根结点。
后序遍历的非递归实现有2种方法,第一种方法使用2个栈,第二个方法使用1个栈,第一种方法较为简单,掌握第一种方法,第二种方法需要时再理解。
①创建2个栈stack1,stack2用来中转结点,创建一个cur指针用来表示当前的结点
②与之前先序、中序统一,先将根结点root压入到栈stack1中;从栈stack1中弹出一个结点node,并将其放入到stack2中,然后先后将node的左右结点放入到stack1中(注意是先放左结点再放右结点,这只是2个动作,先判断一下left和right是否为null,如果不为null时才放入到stack1中);这2个动作执行完后就从stack1中弹出一个结点作为node,放入stack2中,并再次将node.left 和node.right分别放入到stack1中,当stack1中没有结点时循环结束,此时所有结点都已经进入到了stack2中,将stack2中的结点依次取出就是后序遍历的顺序。
理解:即stack2中只存放从stack1中弹出的结点,不会弹出结点,只是作为最后的一个逆序的工具;后序遍历和先序遍历过程很相似,只是多了一个栈,并且每次压入栈中时是先压左结点再压右结点;总的来说,后序遍历的过程是这样的:先将根结点cur压入栈stack1中,制作一个循环,当stack1.isEmpty()==false时,先从stack1中弹出一个一个结点node并放入stack2中;然后先后将node的左结点和右结点压入到stack1中;如果stack1不为null就再从栈中弹出一个结点node放入stack2并再次将node的左右结点一起入到栈中,以此类推,直到stack1为空时树中的结点遍历结束,都存在于stack2中了,于是将stack2中的结点倒出就得到了后序遍历的顺序。
总结:其实先序遍历、中序遍历、后序遍历的代码也并不难,只是理解起来并不直观,因为这是一个高度抽象的规律,因此先记住3种遍历的操作,会写出代码,之后在练习中慢慢掌握。
注意:常识:Stack判断有无元素的方法是isEmpty(),不能使用stack==null来判断栈是否为null。
importjava.util.*;
/*
publicclass TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}*/
//非递归实现二叉树的先序、中序、后序遍历
//所谓遍历就是给定根结点,然后按照指定顺序遍历各个结点,将遍历过的结点一次放入集合或者数组中,或者直接打印出来
publicclass TreeToSequence {
public int[][] convert(TreeNode root) {
//特殊输入
if(root==null) return null;
//由于树的结点数目不知道,因此遍历过的结点只能存放在集合list中,之后再转存到数组中
List
List
List
//对以root为根结点的二叉树进行遍历,将遍历过的结点依次放入到preOrderList中
this.preOrder(root,preOrderList);
this.inOrder(root,inOrderList);
this.afterOrder(root,afterOrderList);
//创建一个二维数组用于返回结果
int[][]results=new int[3][preOrderList.size()];
//集合list不能直接转化为int[]数组,因此要遍历集合取出元素放入到数组中
//注意细节:get(i)得到的是结点对象,而返回的是节点的值
for(inti=0;i results[0][i]=preOrderList.get(i).val; results[1][i]=inOrderList.get(i).val; results[2][i]=afterOrderList.get(i).val; } //不要忘记返回结果 return results; } //对以root为根结点的二叉树进行先序遍历,将遍历过的结点依次放入到preOrderList中 private void preOrder(TreeNoderoot,List //①创建一个栈,菱形符 Stack //②创建指针cur表示当前正在访问的结点,初始值为root TreeNode cur=root; //③将根结点放入栈中 stack.push(cur); //④循环,弹栈--右左结点入栈--弹栈…… while(!stack.isEmpty()){ //从栈顶弹出一个结点node cur=stack.pop(); //弹出的元素就是当前遍历的元素,将其放入到集合中 preOrderList.add(cur); //如果弹出元素node有右元素,则入栈 if(cur.right!=null)stack.push(cur.right); //如果弹出元素node有左元素,则入栈 if(cur.left!=null)stack.push(cur.left); } } //对以root为根结点的二叉树进行中序遍历,将遍历过的结点依次放入到inOrderList中 private void inOrder(TreeNoderoot,List //①创建一个栈用来存放结点 Stack //②创建变量指针cur表示当前正在访问的结点,cur初始值为root TreeNode cur=root; //③将根结点放入栈中 stack.push(cur); //④循环:当stack不为空就继续循环 while(!stack.isEmpty()){ //⑤循环,如果左结点不为null就将结点放入栈中 while(cur!=null){ //先取左结点再放入栈中 cur=cur.left; //注意这里cur!=null并不表示cur.left!=null,因此要重新判断,只有left不为null才放入栈中,否则结束循环 if(cur!=null) stack.push(cur); } //当左结点为null,即左结点遍历完成后 //从栈中弹出结点,这就是当前遍历的结点,放入集合 cur=stack.pop(); inOrderList.add(cur); //取弹出结点的右结点,判断是否为null,如果不为null则同root一样放入stack中 cur=cur.right; if(cur!=null) stack.push(cur); //如果有结点为null,则通过循环继续弹出结点再取有右结点即可,直到stack为空 } } //对以root为根结点的二叉树进行后序遍历,将遍历过的结点依次放入到afterOrderList中 private void afterOrder(TreeNode root,List //①创建2个栈用于存放结点 Stack Stack //②创建临时变量cur表示当前正在变量的结点,初始值为root TreeNode cur=root; //③将根结点先放入栈中 stack1.push(cur); //④循环--逻辑类似先序遍历 while(!stack1.isEmpty()){ //先弹出栈顶元素,放入stack2 cur=stack1.pop(); stack2.push(cur); //再将其左、右结点放入栈中 if(cur.left!=null)stack1.push(cur.left); if(cur.right!=null)stack1.push(cur.right); } //此时树的结点都在stack2中,将其放入到集合中 while(!stack2.isEmpty()){ afterOrderList.add(stack2.pop()); } } } 总结:对于二叉树的先序遍历、中序遍历、后序遍历,如果采用递归实现,时间复杂度都是O(n),其中n是二叉树的结点数目,因为每个结点都要遍历一次,即遍历时间总是与二叉树的结点数目成正比;空间复杂度都是O(L),其中L是二叉树的层数(深度、高度),因为递归的次数就是二叉树的层数,因此递归中使用的空间与层数成正比;如果使用非递归实现,那么时间复杂度也是O(n),由于需要使用额外的栈,因此空间复杂度也为O(L),对于后序遍历,即使使用了2个栈,但是空间复杂度的级别还是O(L)级别的。 因此,对于二叉树的遍历,无论使用先序、中序还是后序,无论使用递归还是循环,时间复杂度都是O(n),空间复杂度都是O(L)。