本文所指二叉树 皆为普通二叉树,下同
二叉树的定义
最淳朴的二叉树,左右节点各一个…
在介绍相关操作时,请记住一点,
二叉树构建核心思想是递归,
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);
}
}