二叉树——遍历篇(递归/非递归,C++)

二叉树——遍历篇

二叉树很多算法题都与其遍历相关,笔者经过大量学习、思考,整理总结写下二叉树的遍历篇,涵盖递归和非递归实现。

1、二叉树数据结构及访问函数

#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 << " ";
}

2、二叉树的遍历——深度优先遍历(DFS)(先序、中序、后序)

2.1、具体遍历顺序

  • 先序遍历:访问根节点、先序遍历左孩子、先序遍历右孩子 (根、左、右)
  • 中序遍历:中序遍历左孩子、访问根节点、中序遍历右孩子 (左、根、右)
  • 后序遍历:后序遍历左孩子、后序遍历右孩子、访问根节点 (左、右、根)

2.2、递归遍历

/** 
* 先序遍历二叉树
*/
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);
    }
}

2.3、非递归遍历——借助栈

  • 借助栈,可以实现非递归遍历。
  • 在这里三种非递归遍历都总结和介绍一种算法思路,其栈中保存的节点可以用于路径搜索类的题目,即保存着从根节点到当前访问节点最短路径的所有节点信息,以*标记。
  • 介绍仅用于遍历访问的简单思路,栈中信息难以应用于搜索路径类的题目。
2.31 先序非递归遍历
* PreOrder_1a

算法过程:

由先序遍历过程可知,先序遍历的开始节点是根节点,然后用指针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_1b

算法过程:此算法与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 和 PreOrder_2b 算法思路大体相同,PreOrder_2a 实现比较简洁

算法过程:

用指针p指向当前要处理的节点,沿着左下方向逐一访问并压栈,直至指针P为空,栈顶节点为最左下节点。然后p指向栈顶节点的右节点(不管是否空);若右节点为空,则继续弹栈。若右节点非空,则按上述同样处理右节点。如此重复操作直至栈空为止。

缺陷:PreOrder_2 当访问栈顶节点的右节点时,会丢失当前栈顶节点信息,导致从根节点到当前栈顶节点的右节点路径不完整。

优点:算法思路清晰易懂,逻辑简单。

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();
        }
    } 
}
PreOrder_3

算法过程:

用指针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);
    }
}
2.32 中序非递归遍历

中序遍历非递归算法要把握访问栈顶节点的时机。

* InOrder_1

算法过程:

用指针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());
}
InOrder_2a && InOrder_2b

算法过程:
用指针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();
        }
    }
}
2.33 后序非递归遍历
*PostOrder_1
算法过程:
用指针cur指向当前要处理的节点。先扫描(并非访问)根节点的所有左节点并将它们一一进栈,直至栈顶节点为最左下节点,指针cur为空。此时有两种情况。(1)栈顶节点的右节点为空或已访问,访问当前栈顶节点(访问时机很重要!),用指针p保存刚刚访问过的节点(初值为NULL),弹栈;(2)栈顶节点有未被访问的右节点,设置flag,退出当前访问搜索循环。如此重复处理,直至栈空为止。
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());
}
PostOrder_2

算法过程:
先把根节点压栈,用指针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);
        }
    }
}

3、二叉树的遍历——广度优先遍历(BFS)

  • 二叉树的广度优先遍历,就是层次遍历,借助队列实现

    在进行层次遍历时,对某一层的节点访问完后,再按照对它们的访问次序对各个节点的左、右孩子顺序访问。这样一层一层进行,先访问的节点其左、右孩子也要先访问,与队列的操作原则比较吻合,且符合广度优先搜索的特点。

    算法过程:先将根节点进队,在队不空时循环;从队列中出列一个节点,访问它;若它有左孩子节点,将左孩子节点进队;若它有右孩子节点,将右孩子节点进队。如此重复操作直至队空为止。

  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);
    }
  }

原创所有,转载务必注明出处。

转载于:https://www.cnblogs.com/pz-Feng/p/8119681.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值