(c语言实现)二叉树的相关操作 (一) 二叉树的递归遍历和循环遍历

本文所指二叉树 皆为普通二叉树,下同

二叉树的定义

最淳朴的二叉树,左右节点各一个…
在介绍相关操作时,请记住一点,
二叉树构建核心思想是递归,

typedef char treeNodeType;
typedef struct _treeNode{

    struct _treeNode* left;
    struct _treeNode* right;
    treeNodeType value;
}treeNode;

相关操作有:

1 二叉树的先序,中序,后续遍历的递归版,使用栈循环版,还有使用队列层序遍历版
2 求二叉树某层的节点数,求二叉树的总节点数,求二叉树叶子节点数,
3 求某节点的左右子节点 或者父节点
3 将二叉树镜像翻转
4 判断一颗二叉树是否是完全二叉树
5 在二叉树中查找某节点
6 二叉树的深拷贝(clone)
7通过前序有标记空节点结果 构建二叉树
8 通过前序中序遍历构建二叉树

这篇博客主要将 加粗部分解决,详细代码请参见
其中有完整头文件,源文件,和单元测试.
感谢!

剩余操作请参见
(c语言实现)二叉树的相关操作 (二) 二叉树节点操作

(c语言实现)二叉树的相关操作 (三) 通过遍历构建二叉树的两种类型


前中后序遍历递归版

前序遍历

递归的 先访问根节点,再访问左节点,再访问右节点

void PreOrder(treeNode* node)
{
    if( node == NULL )
        return;
    printf("%c ",node->value);
    PreOrder(node->left);
    PreOrder(node->right);
}

中序遍历

递归的先访问左节点,在访问根节点,最后访问右节点

void InOrder(treeNode* node)
{
    if( node == NULL )
        return;
    InOrder(node->left);
    printf("%c ",node->value);
    InOrder(node->right);

}

后序遍历

递归的先访问左节点,再访问有节点,最后访问根节点

void PostOrder(treeNode* node)
{
    if( node == NULL )
        return;
    PostOrder(node->left);
    PostOrder(node->right);
    printf("%c ",node->value);

}

虽然代码很简单,但是还是需要自己画一颗二叉树,一步一步跟着代码执行,才能理解透彻,
所有递归都有是这个特点,代码简单,要理解还是要花费一定工夫
这里给出先序遍历的过程

先序遍历

先中后序遍历(使用栈循环版)

因为树是递归定义的,所以用递归遍历还是很方便的,如果不使用递归,我们就必须使用栈来保存节点.
一个简单栈的定义如下栈的定义如下

#define MAX_SIZE 100
typedef treeNode*  StackType;
typedef struct _stack
{
    StackType array[MAX_SIZE];
    size_t base;
    size_t top;
}stack;

这里给出操作栈的几个基本函数

int StackEmpty(stack* s);//判断是否为空
void StackInit(stack* s);//初始化栈
void StackDestroy(stack* s)//摧毁栈
StackType StackPop(stack* s);//入栈
void StackPush(stack* s, StackType key);//出栈
StackType StackTop(stack* s);//查看栈顶元素

先序遍历

我们定义一个变量 cur 指向将要入栈的元素
先序遍历是先打印根节点,所以我们考虑一直入栈该节点左子节点(左子节点是其左子树的根节点)
入栈的同时打印节点
当cur等于NULL,但是栈中还有元素时, 表示此时栈顶元素就是叶子节点, 让cur = 栈顶元素,
因为我们是先打印再入栈的,所以此时显然叶子节点已经打印过,那么将其出栈
但注意到,我们虽然把该节点(也是当前树的根节点),左子树遍历过,但是其右子树还没遍历
那么我们就应该让 cur = cur->right ,入栈其右节点 (cur 保存了 刚才出栈的元素,又指向了其右节点)
什么时候表明遍历完了呢?
当cur == NULL 显然有两个结果 1. 碰到叶子节点 2 没有可行的元素入栈
但是当栈为空时, 1明显不成立,所以结束条件应该是 cur == NULL && 栈为空
分析完,代码就很好写了

void PreOrderByStack(treeNode* root)
{
    stack s;
    StackInit(&s);
    if(root == NULL)
        return;
    treeNode* cur = root;
    while(!StackEmpty(&s) ||  cur != NULL)
    {
        while(cur != NULL)
        {
            printf("%c ",cur->value);
            StackPush(&s, cur);
            cur = cur->left;
        }
        //发现cur走到NULL 回退一步
        //因为回退一步已经走过了,所以把其出栈
        //在看其右子树有没有节点
        if(!StackEmpty(&s))
        {
            cur = StackTop(&s);
            StackPop(&s);
            cur = cur->right;
        }

    }
}

中序遍历

中序遍历和前序遍历很相似,不过是先将左子树入栈,等到cur->NULL时
使cur退回到栈顶,此刻打印栈顶元素就行了 (因为根节点的左子节点是其左子树的根节点)


 void InOrderByStack(treeNode* root)
{
    if(root == NULL)
    {
        printf("empty tree\n");
        return;
    }
    stack s;
    StackInit(&s);
    treeNode* cur = root;
    while(!StackEmpty(&s) || cur != NULL)
    {
        while(cur != NULL)
        {
            StackPush(&s,cur);
            cur = cur-> left;
        }
        if(!StackEmpty(&s))
        {
            cur = StackTop(&s);
            printf("%c ", cur->value);
            StackPop(&s);
            cur = cur->right;
        }
    }

后序遍历

方法一 判断两次入栈法

对于后续遍历,因为要分别打印完左子树和右子树才能打印根节点,
当我们将cur压入栈并且cur->left 一路入栈后,
栈顶元素恰好是叶子节点,也是该节点的根节点,此时的操作不应该是将其出栈(因为还没打印),而是要继续遍历其右子树,当右子树遍历完成后,该节点又再次出现在栈顶,
此刻表明该节点的左子树打印完了,右子树也打印完了.那么就需要打印其本身(根节点).

我们发现栈顶的元素在打印过程中会出现两次且仅出现两次,一次表明左子树遍历完成,一次表示右子树遍历完成.

那么我们必须在每个节点增加一个标记,记录其是否是第一次出现在栈顶

typedef char treeNodeType;
typedef struct _treeNode{

    struct _treeNode* left;
    struct _treeNode* right;
    treeNodeType value;
    int IsFristInStack;//对于每个节点新增添的标记

}treeNode;

当发现是第一次出现在栈顶,就继续压栈其右子树,
若第二次出现在栈顶则打印其并且将其出栈.

void PostOrderByStack_1(treeNode* root)
{
    stack s;
    StackInit(&s);
    treeNode* cur = root;
    while(!StackEmpty(&s) || cur != NULL)
    {
        while(cur != NULL)
        {
            cur->IsFristInStack = 1;
            StackPush(&s,cur);
            cur = cur->left;
        }
        if(!StackEmpty(&s))
        {
            cur = StackTop(&s);
            if(cur->IsFristInStack == 1)
            {
                cur->IsFristInStack = 0;
                cur = cur->right;
            }
            //走到这表明右子树也走完了
            else
            {
                printf("%c ",cur->value);
                StackPop(&s);
                //因为是左右子树都访问完了才打印的根节点,
                //所以让cur = NULL 避免重复走
                //当cur = NULL 第一个循环就进不去
                //而是直接判断栈是否为空
                cur = NULL;
            }
        }
    }
}

方法二 顺序入栈判断法

因为是后序遍历, 当一个节点的左子树和右子树为空时,我们可以直接打印该节点,
同时我们入栈的顺序变为 : 先入栈根节点,在入栈右子树,最后入栈左子树.
这样出栈顺序就是后序遍历,对于栈顶元素,若前一个打印的节点是其左右子节点的任何一个,
那么栈顶元素就可以打印并且出栈了
原因是: 因为我们特殊的入栈方式,根节点总是最后一个出现在栈顶,
若前一个出栈的是右节点,则有两个情况
1.该节点的左子树打印完了,右子树也打印完了.就该打印根节点了
2该节点没有左子树,打印玩右子节点后也应该打印根节点.
若前一个出栈的是左节点 则
1.该节点没有右子树,并且打印完了左子树,那么也应该打印根节点
分析完后,我们发现只需一个节点保存上一次打印的节点,就可以完成对树的后序遍历

void PostOrderByStack_2(treeNode* root)
{
    // 入栈的时候先入根节点,然后入右节点,再入左节点  当出栈的时候就可以后续出栈了
    // A B C 入栈 A C  B   出栈   B  C  A
    // 如果 node 左右都为空 打印node 并且出栈
    // 因为node入栈的顺序 top 先是左子树 然后是右子树 最后是根节点
    // 如果 node 的左右节点任何一个被被访过了,那么node 也可以出栈 很巧妙
    if( root == NULL)
        return ;
    stack s;
    StackInit(&s);
    treeNode* cur ;
    StackPush(&s, root);
    //跟踪上次访问的节点
    treeNode* pre = NULL;
    while( !StackEmpty(&s))
    {
        cur = StackTop(&s);
        if( ( cur->left == NULL &&  cur->right == NULL ) || \
            ( ( pre != NULL ) && ( cur->left == pre || cur->right == pre ) ))
        {
            printf("%c ",cur->value);
            //记得能打印的就应该pop
            StackPop(&s);
            //记得跟新pre
            pre = cur;
        }
        else
        {
            //注意入栈顺序
            //先右后左
            if( cur->right != NULL )
                StackPush(&s,cur->right);
            if(cur->left != NULL)
                StackPush(&s, cur->left);
        }
    }
}

层序遍历

层续遍历顾名思义,就是一层一层的遍历二叉树,遍历的过程需要用到队列
一个简单的队列定义如下

typedef struct _queue
{
    treeNode* data[MAX_SIZE];
    int size;
    int head;
    int tail;
}queue;

操作函数包括

void queueFront(queue* q, treeNode* n) ;//出队列,第二个参数为输出型参数,获得出队列的值
treeNode*  queueHead(queue*q);//取队首元素
void queueInit(queue* q);//队列初始化
void queuePush(queue* q,treeNode* n );//从队首加入元素

我们将头结点先入队列,
然后打印头结点,
接着将头结点的左右节点(非空)按顺序入队列,
以后每当我们将要把队首元素出对列前,都将其结点的左右节点(非空)按顺序入队列,
这就完成了层序遍历

void LevelOrder(treeNode* node)
{
    if(node == NULL)
        return;
    queue q;
    queueInit(&q);
    queuePush(&q, node);
    treeNode ret;
    while(1)
    {
        if(q.size == 0)
            break;
        if(queueHead(&q)->left != NULL)
            queuePush(&q,queueHead(&q)->left);
        if(queueHead(&q)->right != NULL);
            queuePush(&q,queueHead(&q)->right);
        /*printf("%c ", queueHead(&q)->value);*/
        queueFront(&q,&ret);
        printf("%c ",ret.value);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值