二叉树不少算法题都与其遍历相关,笔者通过大量学习、思考,整理总结写下二叉树的遍历篇,涵盖递归和非递归实现。node
#include <stdio.h> #include <iostream> #include <stack> using namespace std; struct BTNode { int value; struct BTNode *left, *right; BTNode(int value_) :value(value_),left(NULL),right(NULL){}; }; //访问函数:可根据实际进行修改 void visit(BTNode* node) { cout << node->value << " "; }
/** * 先序遍历二叉树 */ void PreOrder(BTNode* root) { if (root) { visit(root); PreOrder(root->left); PreOrder(root->right); } } /** * 中序遍历二叉树 */ void InOrder(BTNode* root) { if (root) { InOrder(root->left); visit(root); InOrder(root->right); } } /** * 后序遍历二叉树 */ void PostOrder(BTNode* root) { if (root) { PostOrder(root->left); PostOrder(root->right); visit(root); } }
算法过程:ios
由先序遍历过程可知,先序遍历的开始节点是根节点,而后用指针p 指向当前要处理的节点,沿着二叉树的左下方逐一访问,并将它们一一进栈,直至栈顶节点为最左下节点,指针p为空。此时面临两种状况。(1)对栈顶节点的右子树访问(若是有的话,且未被访问),对右子树进行一样的处理;(2)若无右子树,则用指针last记录最后一个访问的节点,指向栈顶节点,弹栈。如此重复操做,直至栈空为止。算法
void PreOrder_1a(BTNode *root) { if (NULL == root) return; stack<BTNode*> stack; BTNode *p = root; BTNode *last = NULL; do { while (p) { visit(p); stack.push(p); p = p->left; } //此时p = NULL,栈顶节点为左子树最左节点 if (!stack.empty()) { BTNode *t = stack.top(); if (t->right != NULL && t->right != last) { p = t->right; } else {//若无右子树,则指针P仍为空,则不断弹栈(沿着双亲方向)寻找有未被访问的右子树的节点 last = t; stack.pop(); } } } while (!stack.empty()); }
算法过程:此算法与PreOrder_1a有殊途同归之处,巧妙之处在于对上述两种状况的处理。指针p记录当前栈顶结点的前一个已访问的结点。若无右子树、或者右子树已被访问,则用指针p记录当前栈顶节点,弹栈,不断沿着双亲方向寻找有未访问右子树的节点,找到即退出循环,不然直至栈空。当栈顶节点的右孩子是p时,则将cur指向右孩子,设置flag =0 ,退出当前搜索循环(不断弹栈,搜索有右节点且未被访问的祖先节点),而后对右子树进行一样的处理。如此反复操做,直至栈空为止。数据结构
void PreOrder_1b(BTNode *root) { if (NULL == root) return; stack<BTNode*>stack; int flag; BTNode *cur = root,*p; do{ while (cur) { visit(cur); stack.push(cur); cur = cur->left; } //执行到此处时,栈顶元素没有左孩子或左子树均已访问过 p = NULL; //p指向栈顶结点的前一个已访问的结点 flag = 1; //表示*cur的左孩子已访问或者为空 while (!stack.empty() && flag == 1) { cur = stack.top(); if (cur->right == p) //表示右孩子结点为空或者已经访问完右孩子结点 { stack.pop(); p = cur; //p指向刚访问过的结点 } else { cur = cur->right; //cur指向右孩子结点 flag = 0; //设置未被访问的标记 } } } while (!stack.empty()); }
PreOrder_2a 和 PreOrder_2b 算法思路大致相同,PreOrder_2a 实现比较简洁函数
算法过程:post
用指针p指向当前要处理的节点,沿着左下方向逐一访问并压栈,直至指针P为空,栈顶节点为最左下节点。而后p指向栈顶节点的右节点(无论是否空);若右节点为空,则继续弹栈。若右节点非空,则按上述一样处理右节点。如此重复操做直至栈空为止。学习
缺陷:PreOrder_2 当访问栈顶节点的右节点时,会丢失当前栈顶节点信息,致使从根节点到当前栈顶节点的右节点路径不完整。spa
优势:算法思路清晰易懂,逻辑简单。指针
void PreOrder_2a(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode*> stack; while (p || !stack.empty()) { if (p) { visit(p); stack.push(p); p = p->left; } else { BTNode *top = stack.top(); p = top->right; stack.pop(); } } } void PreOrder_2b(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode*> stack; while (!stack.empty() ||p) { while (p) { visit(p); stack.push(p); p = p->left; } if (!stack.empty()) { BTNode *top = stack.top(); p = top->right; stack.pop(); } } }
算法过程:code
用指针p指向当前要处理的节点。先把根节点压栈,栈非空时进入循环,出栈栈顶节点并访问,而后按照先序遍历先左后右的逆过程把当前节点的右节点压栈(若是有的话),再把左节点压栈(若是有的话)。如此重复操做,直至栈空为止。
特色:栈顶节点保存的是先序遍历下一个要访问的节点,栈保存的全部节点不是根到要访问节点的路径。
void PreOrder_3(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode *> stack; stack.push(p); while (!stack.empty()) { p = stack.top(); stack.pop(); visit(p); if (p->right) stack.push(p->right); if (p->left) stack.push(p->left); } }
中序遍历非递归算法要把握访问栈顶节点的时机。
算法过程:
用指针cur指向当前要处理的节点。先扫描(并不是访问)根节点的全部左节点并将它们一一进栈,直至栈顶节点为最左下节点,指针cur为空。
此时主要分两种状况。(1)栈顶节点的左节点为空或者左节点已访问,访问栈顶节点(访问位置很重要!),如有右节点则将cur指向右节点,退出当前while循环,对右节点进行上述一样的操做,若无右节点则用指针p记录站顶节点并弹栈;(2)站顶节点的右节点为空或已访问,用指针p记录站顶节点并弹栈。
内部第二个while循环可称为访问搜索循环,在栈顶节点的左节点为空或者左节点已访问的状况下,访问栈顶节点,如有右节点,则退出循环,不然不断弹栈。
如此重复操做,直至栈空为止。
void InOrder_1(BTNode *root) { if (NULL == root) return; stack<BTNode *>stack; BTNode *cur = root,*p = NULL; int flag = 1; do{ while (cur){ stack.push(cur); cur = cur->left; } //执行到此处时,栈顶元素没有左孩子或左子树均已访问过 p = NULL; //p指向栈顶结点的前一个已访问的结点 flag = 1; //表示*cur的左孩子已访问或者为空 while (!stack.empty() && flag) { cur = stack.top(); if (cur->left == p) //左节点为空 或者左节点已访问 { visit(cur); //访问当前栈顶节点 if (cur->right) //如有右节点 当前节点指向右节点,并退出当前循环,进入上面的压栈循环 { cur = cur->right; flag = 0; //flag = 0 标记右节点的左子树未访问 } else //当前节点没有右节点,P记录访问完的当前节点,弹栈 { p = cur; stack.pop(); } } else // 此时 cur->right == P 即访问完右子树 ,P记录访问完的当前节点,弹栈 { p = cur; stack.pop(); } } } while (!stack.empty()); }
算法过程:
用指针p指向当前要处理的节点。先扫描(并不是访问)根节点的全部左节点并将它们一一进栈,当无左节点时表示栈顶节点无左子树,而后出栈这个节点,并访问它,将p指向刚出栈节点的右孩子,对右孩子进行一样的处理。如此重复操做,直至栈空为止。
须要注意的是:当节点*p的全部左下节点入栈后,这时的栈顶节点要么没有左子树,要么其左子树已访问,就能够访问栈顶节点了!
InOrder_2a 、 InOrder_2b 与 PreOrder_1a 、PreOrder_1b 代码基本相同,惟一不一样的是访问节点的时机,把握好可方便理解和记忆。
void InOrder_2a(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode *>stack; while (p || !stack.empty()) { while (p) { stack.push(p); p = p->left; } if (!stack.empty()) { p = stack.top(); visit(p); stack.pop(); } } } void InOrder_2b(BTNode *root) { if (NULL == root) return; stack<BTNode *>stack; BTNode *p = root; while (p || !stack.empty()) { while (p) { stack.push(p); p = p->left; } if (!stack.empty()) { p = stack.top(); visit(p); p = p->right; stack.pop(); } } }
void PostOrder_1(BTNode *root) { if (NULL == root) return; stack<BTNode*>stack; int flag; BTNode *cur = root, *p; do{ while (cur) { stack.push(cur); cur = cur->left; } //执行到此处时,栈顶元素没有左孩子或左子树均已访问过 p = NULL; //p指向栈顶结点的前一个已访问的结点 flag = 1; //表示*cur的左孩子已访问或者为空 while (!stack.empty() && flag == 1) { cur = stack.top(); if (cur->right == p) {//表示右孩子结点为空,或者已经访问cur的右子树(p一定是后序遍历cur的右子树最后一个访问节点) visit(cur); p = cur; //p指向刚访问过的结点 stack.pop(); } else { cur = cur->right; //cur指向右孩子结点 flag = 0; //表示*cur的左孩子还没有访问过 } } } while (!stack.empty()); }
算法过程:
先把根节点压栈,用指针p记录上一个被访问的节点。在栈为空时进入循环,取出栈顶节点。
此时有两种状况:
(1)访问当前节点,注意把握访问的时机,若是当前节点是叶子节点,访问当前节点;若是上一个访问的节点是当前节点的左节点(说明无右节点),访问当前节点;若是上一个访问的节点是当前节点的右节点(说明左右节点都有),访问当前节点;指针p记录当前访问的节点,弹栈。 因此,只有当前节点是叶子节点,或者上一个访问的节点是当前节点的左节点(无右) 或右节点(左右都有) ,才能够访问当前节点。
(2)压栈。 后序遍历顺序为 左、右、根,按照逆序,先把右压栈,再把左压栈(若是有的话)。
如此重复操做,直至栈空为止。
void PostOrder_2(BTNode* root) { if (NULL == root) return; BTNode *cur = root, *p=NULL; stack<BTNode*> stack; stack.push(root); while (!stack.empty()) { cur = stack.top(); if (cur->left == NULL && cur->right == NULL || (p!= NULL && (cur->left==p || cur->right == p))) { visit(cur); stack.pop(); p = cur; } else { if (cur->right) stack.push(cur->right); if (cur->left) stack.push(cur->left); } } }
二叉树的广度优先遍历,就是层次遍历,借助队列实现
在进行层次遍历时,对某一层的节点访问完后,再按照对它们的访问次序对各个节点的左、右孩子顺序访问。这样一层一层进行,先访问的节点其左、右孩子也要先访问,与队列的操做原则比较吻合,且符合广度优先搜索的特色。
算法过程:先将根节点进队,在队不空时循环;从队列中出列一个节点,访问它;若它有左孩子节点,将左孩子节点进队;若它有右孩子节点,将右孩子节点进队。如此重复操做直至队空为止。
void LevelOrder(BTNode *root) { if (NULL == root) return; queue<BTNode*> queue; queue.push(root); BTNode* p; while (!queue.empty()) { p = queue.front(); queue.pop(); visit(p); if (p->left) queue.push(p->left); if (p->right) queue.push(p->right); } }