“遍历”是二叉树各种操作的基础,可以在遍历过程中对节点进行各种操作,如:求节点的双亲,求节点的孩子,判断节点的层次。当然,还有一些更重要的操作,例如,依据遍历序列建立二叉树,对建立的二叉树进行线索化,等等。
二叉树的各种遍历操作必须了然于心,无论是递归的,还是非递归的。递归算法的优点是形式简单,当然,正如一句话所说“迭代是人,递归是神”。递归算法的整个详细过程还是很烧脑的,我们只有完全一遍调试下来才能懂它具体在干嘛。递归每一步都把未知当作已知,每一步又在把未知变为已知。想不到描述起来又用上了递归的概念,生活中其实有很多递归的现象,我印象最深的就是两面面对面放置的镜子,你永远也找不到最深处的自己(当然我们假设你的视力无限好同时光在反射过程中没有亮度损失),也就是说这个递归过程没有边界条件,会无限递归,永远也不会返回,这在程序中可就是死循环呀,相当可怕,CPU很快就会被掏空。所以每次设计递归算法时,首先要考虑到的就是边界返回条件。还有一个我印象很深的电影,《盗梦空间》,三层梦境,从现实中进入第一层,再从第一层梦境进入第二层,最后再从第二层进入第三层。当然电影里也说了可以无限往下递归,进入一个叫迷失域的地方,不过好在电影的设定是无论你掉入了多深的梦,你只要在梦里死了,就能返回上层梦境,也就是说,每层梦境都有一个返回条件,所以原则上是不会死循环的。。。
哎呀,扯太远了,其实我一直想写一篇关于递归的哲学文章,阐述程序和我们现实的联系,算了,不扯了。
递归形式上很简单,代码很简洁,然后时间复杂度却很大,同时还得花费一定的空间复杂度(递归栈深度),比如经典的Hanoi问题,时间复杂度是O(2^n),n为圆盘的个数,时间复杂度膨胀的很厉害。所以我们在很多时候不能依赖于递归算法,而需要去寻找相应的非递归算法,当然并非所有问题都能找到非递归算法,即便能,那估计也复杂到没人能读懂。而非递归算法形式上就比较复杂,不过它其实更接近人的思维过程,虽然过程也不简单,但毕竟代码不涉及递归过程的一层一层的概念,不存在利用未知信息求解的过程。所以只要把代码表面的逻辑弄懂了就是懂了。
在二叉树的问题上,由于树的结构很有规律,所以用递归算法形式上会比非递归简单很多,也不用去考虑很多细节问题,只要把边界条件想好就行。
下面我把前序,中序,后序遍历的递归算法和非递归算法代码给出,其实都不长,要经常复习,思考这几个过程,以达到熟练于心的程度。
1 //以下三种递归算法均对每个节点只访问一次,故时间复杂度都是O(n) 2 //递归工作栈的深度恰好是树的深度,所以最坏的情况下空间复杂度为O(n) 3 4 //递归前序遍历二叉树 5 int PreOrderTraverse_Recursive(BiPtr T) 6 { 7 if (T) 8 { 9 cout << T->data << " "; 10 PreOrderTraverse_Recursive(T->lchild); 11 PreOrderTraverse_Recursive(T->rchild); 12 } 13 return 0; 14 } 15 16 //递归中序遍历二叉树 17 int InOrderTraverse_Recursive(BiPtr T) 18 { 19 if (T) 20 { 21 InOrderTraverse_Recursive(T->lchild); 22 cout << T->data << " "; 23 InOrderTraverse_Recursive(T->rchild); 24 } 25 return 0; 26 } 27 28 //递归后序遍历二叉树 29 int PostOrderTraverse_Recursive(BiPtr T) 30 { 31 if (T) 32 { 33 PostOrderTraverse_Recursive(T->lchild); 34 PostOrderTraverse_Recursive(T->rchild); 35 cout << T->data << " "; 36 } 37 return 0; 38 }
从以上我们可以看出递归算法的简洁,而且我们发现三种顺序的遍历方式只影响了其中两行代码的顺序而已,三种递归算法形式上非常的一致和统一,这正是递归的魅力所在。
我们再看看非递归算法
1 //非递归前序遍历二叉树 2 int PreOrderTraverse_NonRecursive(BiPtr T) 3 { 4 BiPtr p = T; 5 SqStack S; 6 InitStack(S); 7 8 while (p || !EmptyStack(S)) 9 { 10 while (p) 11 { 12 cout << p->data << " "; 13 Push(S, p); 14 p = p->lchild; 15 } 16 if (!EmptyStack(S)) 17 { 18 Pop(S, p);//刚刚访问过的根节点 19 p = p->rchild;//访问该根节点的右子树,并对右子树重复上述过程 20 } 21 } 22 return 0; 23 }
1 //非递归中序遍历二叉树 方法1 2 int InOrderTraverse_NonRecursive_1(BiPtr T) 3 { 4 BiPtr p = T; 5 SqStack S; 6 InitStack(S); 7 8 Push(S, T); 9 while (!EmptyStack(S)) 10 { 11 while (GetTop(S)) 12 { 13 Push(S, p->lchild); 14 p = p->lchild;//不停地“向左下走” 15 } 16 Pop(S, p);//前面会“走过头”,也就是说最后栈顶会多出一个空指针,所以弹出多入栈的空指针 17 18 if (!EmptyStack(S)) 19 { 20 Pop(S, p); 21 cout << p->data << " "; 22 Push(S, p->rchild); 23 p = p->rchild; 24 } 25 } 26 return 0; 27 } 28 29 //非递归中序遍历二叉树 方法2 30 int InOrderTraverse_NonRecursive_2(BiPtr T) 31 { 32 BiPtr p = T;//从树根开始 33 SqStack S; 34 InitStack(S); 35 36 while (p || !EmptyStack(S))//还有未访问的结点 37 { 38 if (p)//p向左下走到底并记录下沿途的节点 39 { 40 Push(S, p); 41 p = p->lchild; 42 } 43 else//p走到了底,再依次弹出刚才路过却没有访问的节点,访问之,然后p向右走 44 { 45 Pop(S, p); 46 cout << p->data << " "; 47 p = p->rchild; 48 } 49 } 50 return 0; 51 }
1 //非递归后序遍历二叉树 2 /* 3 后序遍历是先访问左子树再访问右子树,最后访问根节点 4 由于返回根节点时有可能时从左子树返回的,也可能是从右子树返回的, 5 要区分这两种情况需借助辅助指针r,其指向最近访问过的节点 6 */ 7 int PostOrderTraverse_NonRecursive(BiPtr T) 8 { 9 BiPtr p = T,r = nullptr;//从树根开始,r用于指向最近访问过的节点 10 SqStack S; 11 InitStack(S); 12 13 while (p || !EmptyStack(S)) 14 { 15 if (p) 16 { 17 Push(S, p); 18 p = p->lchild;//走到最左下方 19 } 20 else//说明已经走到最左下了,要返回到刚刚经过的根节点,即从null的左孩子返回到它的双亲节点 21 { 22 p = GetTop(S); 23 if (p->rchild && p->rchild != r)//右孩子尚未访问,且右孩子不为空,于是,访问右孩子 24 { 25 p = p->rchild; 26 Push(S, p); 27 p = p->lchild;//走到右孩子的最左下方,即重复最开始的过程 28 } 29 else//右孩子为空,则直接输出该节点,或者右孩子刚刚被访问过了,同样直接输出该节点 30 { 31 Pop(S, p); 32 cout << p->data << " "; 33 r = p;//r指向刚刚访问过的节点 34 p = nullptr;//左右子树都已经处理完,根节点也刚处理完,返回到根节点的双亲,即栈顶元素,下次重新取栈顶元素分析 35 } 36 } 37 } 38 return 0; 39 }
从以上看出,三种顺序的非递归算法基本完全不同,因为非递归算法其实本质上是把我们人类的思维过程用一些循环判读语句表达出来了而已,人类的思维自然没有神那么高大上,所以自然得一步一步,正如我们自己在纸上思考的那样,还得考虑每一个细节。
下面我再把其他一些非关键代码也贴出来(其实我之前发的文章中已经有了,这篇文章知识做一个总结,因为最近在复习数据结构,方便以后自己忘了复习)
建立二叉树
1 //默认按前序遍历的顺序输入,尾结点用#表示 2 int Create_BiTree(BiPtr& T) 3 { 4 ElemType c; 5 //cout << "请输入当前节点元素值:" << endl; 6 cin >> c; 7 if (c == '#') T = NULL; 8 else 9 { 10 T = new BiNode;//新建一个节点 11 T->data = c; 12 Create_BiTree(T->lchild); 13 Create_BiTree(T->rchild); 14 } 15 return 0; 16 }
栈结构定义
1 //--------------------------------栈结构---------------------------------- 2 int InitStack(SqStack &S) 3 { 4 S.base = (BiPtr *)malloc(STACK_INIT_SIZE * sizeof(BiPtr)); 5 if (!S.base) 6 { 7 cout << "分配空间失败!"; 8 exit(-1); 9 } 10 S.top = S.base; 11 S.stacksize = STACK_INIT_SIZE; 12 return 0; 13 } 14 15 int Push(SqStack &S, BiPtr e) 16 { 17 if ((S.top - S.base) >= STACK_INIT_SIZE) 18 { 19 //空间不足时重新分配空间,并将原来的元素拷贝到新分配的空间中去 20 S.base = (BiPtr *)realloc(S.base, (STACK_INIT_SIZE + STACKINCREASE) * sizeof(BiPtr)); 21 if (!S.base) 22 { 23 cout << "分配空间失败!"; 24 exit(-1); 25 } 26 S.top = S.base + STACK_INIT_SIZE; 27 S.stacksize = STACK_INIT_SIZE + STACKINCREASE; 28 } 29 *(S.top) = e;//结构体 30 S.top++;//先插入元素,再后移指针,始终保证top指向栈顶元素下一个位置 31 return 0; 32 } 33 34 int Pop(SqStack &S, BiPtr &e) 35 { 36 if (S.base == S.top) 37 { 38 cout << "栈为空!"; 39 exit(0); 40 } 41 S.top--; 42 e = *(S.top); 43 return 0; 44 } 45 46 BiPtr GetTop(SqStack &S) 47 { 48 if (S.base == S.top) 49 { 50 cout << "栈为空!"; 51 return 0; 52 } 53 else return *(S.top - 1);//返回一个指针 54 } 55 56 57 int EmptyStack(SqStack &S) 58 { 59 if (S.base == S.top) return 1;//stack is empty! 60 else return 0;//stack is not empty! 61 }
结构体定义
1 #define STACK_INIT_SIZE 100 2 #define STACKINCREASE 10 3 4 typedef char ElemType; 5 typedef struct BiNode 6 { 7 ElemType data; 8 struct BiNode *lchild, *rchild; 9 }BiNode, *BiPtr; 10 11 typedef struct 12 { 13 BiPtr *base;//始终处于栈底位置 14 BiPtr *top;//始终处于栈定元素的下一个位置,当top == base时,栈为空;当base == NULL时,栈不存在 15 int stacksize;//栈当前可使用的最大容量 16 }SqStack;