二叉树,序列结构,本质上等同于链表,后者也可看做单叉树、直树。不分叉有一个好处,就是前进方向是唯一、固定的。反过来讲,分叉的麻烦之处就是要走回头路。而对于单向的结构而言,就得想办法保存该信息。若在二叉树结点内再维护一个prev结点,变成双向结构,则遍历问题会变得相当容易。不过这样做,也仅是将实现算法时需要的额外空间,转移到了原始数据结构中,代价仍旧是存在的。当然,并不是每走一步,都要用空间保存回程信息,我们仅需在走到尽头时,能够跳转回即可。因此,理论上可以实现 O 1 O_{1} O1的空间复杂度。
-
不管是迭代遍历还是递归遍历,先序遍历或是中、后序遍历,二叉树遍历都只做了三件事:处理左子树,处理右子树,处理当前结点。
-
依据处理当前结点的时机不同,可分为:先序、中序、后序三种遍历方式。此外,还有层序遍历的方式,即按层输出结点(可利用队列实现)。
-
二叉树迭代遍历的主要难点在于:如何在处理完子树后,返回父结点(或右结点,总之,这里强调的是“返回”)。常用思路是利用栈先入后出的特性。
-
经典思路一:仿照层序遍历的方式,可利用栈,遍历结点,同时压左右非空子结点入栈。不同的是,栈在处理栈顶结点时,又会压新栈,使得旧结点(亦即右结点)被打压。而队列的层序遍历方式,先到先得(FIFO)。
由于先序遍历可立即处理完当前结点,故可使用该方法。而对于中序、后序遍历,处理完子树后,还要返回处理父结点。
因此只能采用其他方法。(此处可行性的判断属于个人猜测)fine,找到反例了。考察父结点与子结点输出的相对顺序,不难发现后序遍历是先序遍历的逆序,而子结点间,只要将左右结点顺序交换下即可。这就解决了必须先处理子结点才能处理当前结点的矛盾,因此可以将保存结果的数据结构换成链表,将每次输出插入到头部(而非尾部)即可。(栈还是要用的,因为必须要保存之前经过的结点)。(又:这种方法并没有实际的后序遍历,而是打印出与后序遍历一样的结果,属于取巧的做法)
对应 preOrderNormalTraverse,postOrderNormalTraverse。
-
经典思路二:核心思想与思路一相同,只是对流程进一步简化。思路一中,不断压新栈的结果就是,不断向左遍历且保存右结点。因此,我们对每结点,压栈并遍历至最左结点,而后依次出栈、转至右结点(也可以是父结点)。换句话讲,我们可将右节点视为下一层的左节点。对每结点沿左臂压栈并遍历至其尽头。(访问路径呈从右上至左下的对角线,每次跳至右结点,也就是开始下一级的对角线遍历)
对应 preOrderDiagonalTraverse_V1,preOrderDiagonalTraverse_V2,inOrderDiagonalTraverse,postOrderDiagonalTraverse。
-
经典思路三:无论是递归遍历还是以上所说的遍历方式,其空间、时间复杂度都是 O N O_{N} ON。我们引入额外空间的根本原因还是为了保存父结点,以便回档。但实际上还有一种更巧妙方式,我们可以在进入左子树前,可利用树结构的特点,将子树中的某一子结点的子结点指向父结点,这样就能保存父结点的地址,实现 O 1 O_{1} O1的空间复杂度。
对应 preOrderMorrisTraverse,inOrderMorrisTraverse,postOrderMorrisTraverse。
-
即所谓 Morris Traversal。
-
该方法的核心思想是,处理父结点(亦即当前结点)的左子树前,先将左子树的最右结点的右结点(此时为null)指向父结点(传送门),再处理左子树。这样当我们处理完左子树后,即可顺便从最右结点的右结点返回(因为最右结点即为左子树处理流程的最后一个结点)。这种做法,每结点最多访问两次(一次要设立传送门,一次要实际遍历处理),因此时间复杂度是 O N O_{N} ON。
这里有个隐藏问题,为什么要在左子树的终结点设立传送门,而不是右子树?答案很简单,无论是先中后序遍历,不考虑父结点的处理时机的话,都要先处理左子树,再处理右子树。即,在处理完左子树后,必须要能回到父结点。这就是为何要对左子树特殊处理的原因。更进一步,在进行后序遍历时,处理完右子树后,还要再返回父结点,那为何没给右子树设立传送门呢?因为后序遍历的特殊性,我们采用了另一种操作:从更高一级的角度看,由于右子树与父结点都在爷爷结点的左子树内,因此在处理完右子树,通过末端传送门返回爷爷结点后,将余下未处理的对角线支路逆序处理即可。而对于最顶端结点,由于其没有父结点(导致子树没有爷爷结点),故须人为引入dummyHead,将其左结点指向原最顶端结点。(事实上,从次一级的角度看,右子树可以看作是有传送门的,只不过它返回的不是父结点,而是爷爷结点)
附上代码:
import java.util.*;
public class BTreeTraversal {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
TreeNode createBTreeFromArray(Integer[] arr) {
if(arr.length == 0){
return null;
}
TreeNode root = new TreeNode(arr[0]);
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
boolean isLeft = true;
for(int i = 1; i < arr.length; i++){
TreeNode node = queue.peek();
// for each arr[i], they will be processed by if and else branches separately.
if(isLeft){
if(arr[i] != null){
node.left = new TreeNode(arr[i]);
queue.add(node.left);
}
isLeft = false;
}
else {
if(arr[i] != null){
node.right = new TreeNode(arr[i]);
queue.add(node.right);
}
queue.remove();
isLeft = true;
}
}
return root;
}
}
void preOrderNormalTraverse(TreeNode root){
// this way of traverse only suited for preOrder.
// because in the while-loop, we first pop and then push, which
// means we MUST process the cur node RIGHT AWAY.
// however, for inOrder and postOrder, we have to process child nodes before cur node.
// fine, the core idea can also be used for postOrder.
// preOrder: mid->left->right. postOrder: left->right->mid.
// just REVERSE preOrder and SWAP left and right, we will get the postOrder.
// and it would cost more space than the current version.
// T: O(n), S: O(n)
if(root==null)
return;
List<Integer> tmp = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root = stack.pop();
tmp.add(root.val);
if(root.right!=null) stack.push(root.right);
if(root.left!=null) stack.push(root.left);
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void preOrderDiagonalTraverse_V1(TreeNode root){
// this way of traverse only suited for preOrder.
// because in the while-loop, we first pop and then push, which
// means we MUST process the cur node RIGHT AWAY.
// however, for inOrder and postOrder, we have to process child nodes before cur node.
// T: O(n), S: O(n)
if(root==null)
return;
List<Integer> tmp = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root = stack.pop();
while(root!=null){
tmp.add(root.val);
if(root.right!=null) stack.push(root.right);//store NonNull right node.
root = root.left;//if left==null, while-loop end and root get updated.
}
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void preOrderDiagonalTraverse_V2(TreeNode root){
// this way is suited for inOrder and postOrder.
// because in the while-loop, we first push and then pop.
// T: O(n), S: O(n)
List<Integer> tmp = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while(root!=null || !stack.isEmpty()){
// the condition of !stack.isEmpty() is easy to understand.
// and the root!=null condition:
// 1.in case of root is null at the beginning;
// 2.the right node may be nonNull while the stack is empty,
// which means in our hands, there is a new right subtree to visit.
// in this situation, just limited with !stack.isEmpty() condition will make us miss it.
while(root!=null){
tmp.add(root.val);
// if(root.right!=null)
// stack.push(root.right);//store NonNull right node.
// //since it's preOrder and the father node has been processed, we can store
// // nonNull right node rather than father node. In this situation,
// // the pop operation MUST be limited with !stack.isEmpty(). However, if we
// // choose to store father node, then it's NO NEED to limit the pop operation.
// // Because there MUST be a father node for each left node.
stack.push(root);
root = root.left;
}
// if(!stack.isEmpty()) root = stack.pop();
root = stack.pop();
root = root.right;
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void preOrderMorrisTraverse(TreeNode root){
// T: O(n), S: O(1)
List<Integer> tmp = new ArrayList<>();
TreeNode cur, prev;
cur=root;
while(cur!=null){
if(cur.left==null){
tmp.add(cur.val);
//directly output cur node since it's preOrder.
cur=cur.right;
//jump to right subtree. If it's portal, we will pass it and meet the old father node,
// Then, else-branch in below will be satisfied AGAIN, but this time, the else-branch
// in that else-branch will be useful.
}
else{
prev=cur.left;
while(prev.right!=null && prev.right!=cur)
//search for the predecessor, i.e., the most right node in left subtree.
//on the way back, we WOULD STILL get in this route AGAIN. the
//prev.right!=cur condition will AVOID the endless loop and indicates which
//node the predecessor is.
prev=prev.right;
if(prev.right==null){
//first time to predecessor. go deep into the left subtree layer by layer.
prev.right=cur; //set flag.
tmp.add(cur.val); //output cur node.
cur=cur.left; //go to next left subtree since the flag is set and we get a portal.
}
else{
//second time to predecessor. flag is set, so the left subtree is processed,
//it's on the way back.
prev.right=null; //remove flag since we have pass the portal.
cur=cur.right; //go to the right subtree. A new world is waiting for us.
}
}
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void inOrderDiagonalTraverse(TreeNode root){
// detailed explanation for this algorithm framework is issued in
// the void preOrderDiagonalTraverse_V2(TreeNode root) method, which is also
// included in current class.
// T: O(n), S: O(n)
List<Integer> tmp = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while(root!=null || !stack.isEmpty()){
while(root!=null){
stack.push(root);
root = root.left;
}
root = stack.pop();
tmp.add(root.val);
root= root.right;
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void inOrderMorrisTraverse(TreeNode root){
// T: O(n), S: O(1)
List<Integer> tmp = new ArrayList<>();
TreeNode cur, prev;
cur = root;
while(cur!=null){
if(cur.left==null){
tmp.add(cur.val);
cur = cur.right;
}
else {
prev = cur.left;
while(prev.right!=null && prev.right!=cur){
prev = prev.right;
}
if(prev.right==null){
prev.right=cur;
cur = cur.left;
}
else{
prev.right=null;
tmp.add(cur.val);
cur = cur.right;
}
}
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void postOrderNormalTraverse(TreeNode root){
// NOTE that here we use LinkedList to use the addFirst() method easily.
// and LinkedList needs MORE SPACE than ArrayList.
// Strictly speaking, this method didn't TRAVERSE in a postOrder. It just PRINTS.
// T: O(n), S: O(n)
LinkedList<Integer> tmp = new LinkedList<>();
if(root==null)
return;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root = stack.pop();
tmp.addFirst(root.val);
if(root.left!=null) stack.push(root.left);
//left first in last out
if(root.right!=null) stack.push(root.right);
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void postOrderDiagonalTraverse(TreeNode root){
// detailed explanation for this algorithm framework is issued in
// the void preOrderDiagonalTraverse_V2(TreeNode root) method, which is also
// included in current class.
// T: O(n), S: O(n)
List<Integer> tmp = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode visitedNode = null;
while(root!=null || !stack.isEmpty()){
while(root!=null){
stack.push(root);
root = root.left;
}
root = stack.pop(); //peek() is more suit, here we use pop() to get a unified frameworks.
if(root.right==null || root.right==visitedNode){
//postOrder is MORE COMPLICATED, we need a FLAG to tell us whether we HAVE PROCESSED the right node and
//we are on the way BACK when we pop the father node.
tmp.add(root.val);
visitedNode = root; //ESSENTIAL code!!! NEXT STEP will meet father node, so we should update visitedNode.
root = null; //ESSENTIAL code!!! SKIP the NEXT while(root!=null) loop to POP the father of root.
}
else{
stack.push(root);
root = root.right; //enter into right tree.
}
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void postOrderMorrisTraverse(TreeNode root){
// T: O(n), S: O(1)
//FINE, postOrderMorrisTraverse is INGENIOUS. Still, the problem is how to print the
// left and right subtree when we meet father node. S: O(1) means we can't use stack or
// other space. The only method is to TRAVERSE AGAIN when we ARE SURE that we HAVE PROCESSED
// left and right subtree. Hence, printReverse() and reverseLinkedList() are useful.
// Particularly, dummyHead is introduced to be the FATHER of original TOP node.
List<Integer> tmp = new ArrayList<>();
TreeNode cur, prev, dummyHead;
dummyHead = new TreeNode();
dummyHead.left = root;
cur = dummyHead;
while(cur!=null){
if(cur.left==null){
cur = cur.right;
}
else {
prev = cur.left;
while(prev.right!=null && prev.right!=cur){
prev = prev.right;
}
if(prev.right==null){
prev.right=cur;
cur = cur.left;
}
else{
prev.right=null; // it's time to REVERSELY print the diagonal.
printReversely(cur.left, prev, tmp);
cur = cur.right;
}
}
}
for(Integer e : tmp){
System.out.print(e + " ");
}
}
void printReversely(TreeNode from, TreeNode to, List<Integer> tmp){
reverseLinkedList(from, to);
TreeNode cur = to;
while(cur!=null){
tmp.add(cur.val);
cur=cur.right;
}
reverseLinkedList(to, from);
}
void reverseLinkedList(TreeNode from, TreeNode to){
TreeNode pre=null, cur=from, next=null;
while(true){
next=cur.right;
cur.right=pre;
pre=cur;
cur=next;
if(pre==to)
break;
}
}
public static void main(String[] args) {
Integer[] arr = new Integer[]{1,null,2,3,4,5,6,null,7,null,null,null,null,null,8};
TreeNode head = new TreeNode().createBTreeFromArray(arr);
BTreeTraversal test = new BTreeTraversal();
test.postOrderMorrisTraverse(head);
}
}
参考资料: