这些面试题都可以采用递归的方式解决,而想要更好的理解各级递归之间的联系,我们最 好可以尝试从这两方面入手
一,递归函数有终点
二,递归函数总是朝着终点运行
创建二叉树
设定一个树节点,节点包含了节点内容,左孩子的指针,右孩子的指针
我们采用递归的方式
首先,我们寻找函数的终点,应该是数组指针指向‘#’的时候,‘#’是我们自己设计的标记。
然后,将第一个数组元素赋给第一个树节点,再调用本函数将第一个树节点的左孩子作为参数传入,进入左孩子函数,给左孩子赋值,再起来去调用左孩子的左孩子的函数,去寻找‘#’,找不到就继续调用本函数将当前树节点的左孩子作为参数传入,去寻找‘#’,总会找到,然后不做任何处理,直接返回。
再调用本函数将当前树节点的右孩子作为参数传入,去寻找‘#’,总会找到,然后直接返回
下面用画图的方式讲解
下面是代码
//获得一个节点
PTree GetNewNode(BTDataType data)
{
PTree NewNode=(PTree)malloc(sizeof(Tree));
if(NewNode==NULL)
{
return 0;
}
NewNode->data=data;
NewNode->Left=NULL;
NewNode->Right=NULL;
return NewNode;
}
void _CreatTree(PTree *ps,BTDataType *array,int size,BTDataType invalue,int *index)
{
if((*index<size)&&(invalue!=array[*index]))//退出条件一:数组遍历完了
//退出条件二;遇到‘#’
{
*ps=GetNewNode(array[*index]);//创建新的节点
//printf("%d ",array[*index]);
++(*index);
_CreatTree(&((*ps)->Left),array,size,invalue,index);//进入左孩子
++(*index);
_CreatTree(&((*ps)->Right),array,size,invalue,index);//进入右孩子
}
}
//外部——供用户使用的函数
//这里我们多封装了一层函数,目的是使用户不用关心数组索引(下标)
void CreatTree(PTree *ps,BTDataType *array,int size,BTDataType invalue)
{
int index=0;//这个变量作为数组的索引,必须要传地址,以便将来回到某级函数的时候索引是最新的值
_CreatTree(ps,array,size,'#',&index);
}
void TestcreateTree()
{
PTree str;
BTDataType array[]="ABC###DE##F";//我们将要遍历赋值的数组
CreatTree(&str,array,strlen(array),'#');
}
int main ()
{
TestcreateTree();
return 0;
}
遍历二叉树
遍历二叉树共有三种方式,前序遍历(根左右),中序遍历(左根右),后序遍历(左右根)
这三种方式都可以用递归实现,实际上没有什么区别,和创建二叉树的递归过程一样,参考上面的递归图很有用,
只是改变了一下函数调用的顺序
下面是代码
//遍历树
void FrontTreeOrder(PTree ps)//前序遍历(根左右)
{
if(ps)
{
printf("%c ",ps->data);
FrontTreeOrder(ps->Left);
FrontTreeOrder(ps->Right);
}
}
void MidTreeOrder(PTree ps)//中序遍历(左根右)
{
if(ps)
{
MidTreeOrder(ps->Left);
printf("%c ",ps->data);
MidTreeOrder(ps->Right);
}
}
void BackTreeOrder(PTree ps)//后序遍历(左右根)
{
if(ps)
{
BackTreeOrder(ps->Left);
BackTreeOrder(ps->Right);
printf("%c ",ps->data);
}
}
拷贝二叉树
这个过程很有趣,它的实现是通过在每个递归函数调用栈中创建一个新的树节点,并且将原节点中的数据放入新的节点,然后通过函数返回将各个新的节点按照原节点的连节顺序连接起来。
递归的过程如上面的递归图所示
下面是代码
//拷贝
PTree cupyTree(PTree ps)
{
PTree Node=NULL;
if(ps)
{
Node=GetNewNode(ps->data);
Node->Left=cupyTree(ps->Left);
Node->Right=cupyTree(ps->Right);
}
return Node;
}
前序遍历(递归)
这个过程也是上图的过程,每次进入新的递归函数时我们都打印当前节点,直到遇到空了,然后逐级打印右节点向上返回
下面是代码
void FrontTreeOrder(PTree ps)
{
if(ps)
{
printf("%c ",ps->data);
FrontTreeOrder(ps->Left);
FrontTreeOrder(ps->Right);
}
}
前序遍历(非递归)
这个过程中我们用到了一个栈,先将当根节点存入,在进入while后拿到栈顶元素并打印,随后出栈,必须在当前节点的左右子树入栈之前将当前元素出栈,这个程序用队列也可以实现,而且队列可以没有栈的限制。因为要按照根左右的顺序遍历,所以我们让右节点先入,这样可以让栈顶元素取到的是左节点。然后取到左节点,出左节点,入左节点的左右节点,直到遇到NULL,逐级返回打印右子树
void NotRecursionFront(PTree ps,StackD* pd)
{
PTree pCur;
assert(ps);
assert(pd);
StackDPush(pd,ps);
while(StackDEmpty(pd))
{
pCur=StackDTop(pd);
printf("%c ",pCur->data);
StackDPop(pd);
if(pCur->Right)
{
StackDPush(pd,pCur->Right);
}
if(pCur->Left)
{
StackDPush(pd,pCur->Left);
}
}
}
中序遍历(递归)
这个过程也是函数递归图的递归线路,只不过这次我们没有在一进入函数就打印了当前的节点,而是先找到了最左边的左子树,并且打印当前节点,然后进入当前节点的右子树,若右子树为空,则当前函数结束,返回上一层函数,打印当前节点后,继续处理右子树,这样我们可以打印出左中右的顺序
下面是代码
void MidTreeOrder(PTree ps)
{
if(ps)
{
MidTreeOrder(ps->Left);
printf("%c ",ps->data);
MidTreeOrder(ps->Right);
}
}
中序遍历(非递归)
我们先将数的最左侧的一条分支全部入栈,拿出栈顶元素,这时的栈顶元素左子树必定为空,所以左子树不用打印,打印当前元素就是打印中元素,所以我们打印当前节点后去找当前节点的右子树,当右节点不为空,将右节点作为根节点重新重复以上入栈和打印动作,右节点总有一天会为空,后去取新的栈顶元素继续打印节点并寻找右节点
需要注意的是,这次我们不能像以前一样在未进入循环的时候就将参数根节点入栈,原因是会将参数根节点入栈两次,这们可以用节点是否为空来作为进入循环的条件
说实话这种方式就是让每个节点都经历入栈的过程,右节点==NULL时返回,描述的不够清晰,但是此刻我心里明白,以后会后有更更好的描述
下面是代码
void NotRecursionMid(PTree ps,StackD* pd)
{
PTree pCur;
assert(ps);
assert(pd);
pCur=ps;
while(StackDEmpty(pd)||pCur)
{
while(pCur)
{
StackDPush(pd,pCur);
pCur=pCur->Left;
}
if(StackDEmpty(pd))
{
pCur=StackDTop(pd);
StackDPop(pd);
printf("%c ",pCur->data);
pCur=pCur->Right;
}
}
}
后序遍历(递归)
先递归进入数的最左节点,左右都为空,则打印当前节点并返回上一级,这个上一级就处理完了左子树,也就是打印了左子树,再去处理右子树,也就是打印右子树,然后打印当前节点也就是中节点,这就按照这左右中的顺序在打印,重复这些动作,只到函数返回完毕
以下是代码
void BackTreeOrder(PTree ps)
{
if(ps)
{
BackTreeOrder(ps->Left);
BackTreeOrder(ps->Right);
printf("%c ",ps->data);
}
}
后序遍历(非递归)
个人觉得这个过程还是挺难的,因为我看着代码也不明白道理
这个代码的主要思想是,先找到数的最左边的节点,并将路径全部入栈,此时该节点左肯定为空,进入while循环后我们首先判定该节点的右是否为为空,为空那么直接打印出栈,并设计一个标记p,使得p==当前节点,目的是不在进入打印条件内,而是进入改变节点条件内,这里也有一个标记flag,作用是不再进入判定循环内,而去判定新的节点是否可以入栈,
不为空我们应该将右节点作为参数根节点一样进行最左侧路径节点入栈,再判断右节点,直到遇到了右为空,出栈并打印,
这个过程了解的不是很深刻,需要多复习
void NotRecursionBack(PTree ps)
{
int top=-1;
int flag=1;
PTree p;
PTree pCur;
StackD str;
assert(ps);
StackDInit(&str);
pCur=ps;
do
{
while(pCur)//判断是否可以入栈
{
StackDPush(&str,pCur);
pCur=pCur->Left;
}
flag=1;
p=NULL;
while(StackDEmpty(&str)||flag==1)//判定循环
{
pCur=StackDTop(&str);
if(pCur->Right==p)//打印条件
{
StackDPop(&str);
printf("%d ",pCur->data);
p=pCur;
}
else//改变节点条件
{
pCur=pCur->Right;
flag=0;
}
}
}while(StackDEmpty(&str));
}
前序遍历方法二(非递归)
这个过程用到了一个栈,我们遍历左侧每个节点时并不入栈而是直接打印,并且将当前节点右节点入栈,这种方法其实和第一种思想是一样的,只不过这种方法没有将左节点入栈后有出栈,而是直接打印
//非递归前序遍历方法二
void NotRecursionFrontOrder(PTree ps)
{
PTree pCur;
StackD str1;
StackDInit(&str1);
assert(ps);
StackDPush(&str1,ps);
while(StackDEmpty(&str1))
{
pCur=StackDTop(&str1);
StackDPop(&str1);
while(pCur)
{
printf("%d ",pCur->data);
if(pCur->Right)
{
StackDPush(&str1,pCur->Right);
}
pCur=pCur->Left;
}
}
}
层序遍历(队列)
这个过程其实是非常简单的,本质就是,保存入队列A,然后将A的孩子按右左的顺序如队列,在出队列A,再入B的孩子,循环执行就行
/层序遍历二叉树
void levelOrder(PTree ps)
{
if(ps==NULL)
{
return ;
}
printf("%c ",ps->data);
if(ps->Left==NULL)
{
return;
}
levelOrder(ps->Left);
if(ps->Right==NULL)
{
return ;
}
levelOrder(ps->Right);
}
//二叉树的销毁
二叉树的销毁很简单,这个过程,利用遍历就可以完成,但是只能利用后序遍历和层序遍历,因为前序遍历和中序遍历都会提前销毁掉根节点,这就使得我们无法再找到右节点。而后序遍历是先销毁掉左右节点,层序遍历用栈保存节点,不用担心找不到
下面是后序遍历销毁节点的代码
//二叉树的销毁方法一(后序递归)
void DestroyTree2(PTree ps)
{
if(ps)
{
DestroyTree2(ps->Left);
DestroyTree2(ps->Right);
free(ps);
}
}
下面是层序遍历销毁的代码
//二叉树的销毁方法二(用队列层序遍历)
void DestroyTree(PTree ps)
{
Queue str;
PTree pCur;
assert(ps);
QueueInit(&str);
QueuePush(&str,ps);
while(QueueEmpty(&str))
{
pCur=QueueFront(&str);
if(pCur->Left)
{
QueuePush(&str,pCur->Left);
}
if(pCur->Right)
{
QueuePush(&str,pCur->Right);
}
QueuePop(&str);
free(pCur);
}
}
二叉树的镜像操作(递归)
镜像操作十分简单,这个过程就是找到根节点左右孩子,交换两个孩子的指针,再以左右孩子为根节点交换左右孩子
先来看看代码就明白了
//获取镜像二叉树
void MirrorTree(PTree ps)
{
PTree pCur;
if(ps==NULL)
{
return ;
}
pCur= ps->Left;
ps->Left=ps->Right;
ps->Right=pCur;
if(ps->Left)
{
MirrorTree(ps->Left);
}
if(ps->Right)
{
MirrorTree(ps->Right);
}
}
二叉树的镜像操作(非递归)
这里用到了栈,其实就是找到每个节点,交换节点的左右指针,用栈来保存节点
//用栈获取镜像二叉树
void StackMirrorTree(PTree ps ,StackD*pd)
{
PTree tmp;
PTree pCur;
assert(ps);
assert(pd);
StackDPush(pd,ps);
while(StackDEmpty(pd))
{
pCur=StackDTop(pd);
StackDPop(pd);
tmp=pCur->Left;
pCur->Left=pCur->Right;
pCur->Right=tmp;
//左
if(pCur->Left)
{
StackDPush(pd,pCur->Left);
}
if(pCur->Right)
{
StackDPush(pd,pCur->Right);
}
}
}
二叉树中节点的个数
这个过程很简单,遍历所有节点,逐级加一返回,不是很好描述,但是代码很简单,
//二叉树中节点的个数
int NumNodeTree(PTree ps)
{
int retRight=0;
int retLeft=0;
if(ps==NULL)
{
return 0;
}
retLeft=NumNodeTree(ps->Left);
retRight=NumNodeTree(ps->Right);
return retLeft+retRight+1;
}
二叉树的叶子结点数
这个过程和计算节点数的过程类似,不同的是,我们只叠加了左右子树为空的节点
下面是代码
//二叉树的叶子结点数
int LeafNum(PTree ps)
{
int retRight=0;
int retLeft=0;
if(ps==NULL)
{
return 0;
}
retLeft=LeafNum(ps->Left);
retRight=LeafNum(ps->Right);
if(retLeft==0&&retRight==0)
{
return 1;
}
return retRight+retLeft;
}
求二叉树中第K层节点的个数
这个过程我实在是描述不了了,直接看代码吧
//求二叉树中第k层节点的个数
int NlevelTree(PTree ps ,int k)
{
int retRight=0;
int retLeft=0;
if(ps==NULL||k==0)
{
return 0;
}
if(k==1)
{
return 1;
}
retLeft=NlevelTree(ps->Left,k-1);
retRight=NlevelTree(ps->Right,k-1);
return retLeft+retRight;
}
// 求二叉树的高度
这几种求值的程序其实都一样,
下面是代码
//二叉树的高度(自己写的)
int HighTreeByMy(PTree ps)
{
int retLeft=0;
int retRight=0;
if(ps==NULL)
{
return 0;
}
retLeft=HighTreeByMy(ps->Left);
retRight=HighTreeByMy(ps->Right);
/*if(retLeft>=retRight)
{
return retLeft+1;
}
else
{
return retRight+1;
}*/
return ((retLeft>=retRight)?retLeft:retRight)+1;
//找出来每个节点的左右子树的高度,比较后将较大值+1返回,+1是因为要包括当前节点
}
//在二叉树中查找值为data的节点,找到返回该节点,找不到返回空
下面是代码
//在二叉树中查找值为data的节点,找到返回该节点,找不到返回空(递归)
PTree FindData(PTree ps ,BTDataType data)
//这种方法不太好,一是不能找到后及时返回,二是设计返回值时麻烦
//所以试试非递归
{
if(ps==NULL)
{
return NULL;
}
if(ps->data==data)
{
return ps ;
}
return ( FindData(ps->Left,data)!=NULL)? FindData(ps->Left,data): FindData(ps->Right,data);
//突然发现这种递归也挺好的,在左边找到了就直接返回退出了,根本不会进入右,
//在右边找到了,就返回到上一级的左,不会进入其他的右,找不到也会返回NULL
}
查找节点(非递归)
//在二叉树中查找值为data的节点,找到返回该节点,找不到返回空(非递归)
//必须要用栈去储存节点
PTree FindDataNor(PTree ps ,BTDataType data)
{
PTree pCur;
StackD str;
assert(ps);
StackDInit(&str);
StackDPush(&str,ps);
while(StackDEmpty(&str))
{
pCur=StackDTop(&str);
if(pCur->data==data)
{
return pCur;
}
StackDPop(&str);
if(pCur->Left)
{
StackDPush(&str,ps->Left);
}
if(pCur->Right)
{
StackDPush(&str,ps->Right);
}
}
return NULL;
}
// 检测一个节点是否在二叉树中
这和查找节点一样,递归法直接前序遍历
// 检测一个节点是否在二叉树中
int IsNodeInBinTree(PTree ps, PTree pNode)
{
if(ps==pNode)
{
return 1;
}
if(ps==NULL)
{
return 0;
}
return (IsNodeInBinTree(ps->Left,pNode)!=0)?IsNodeInBinTree(ps->Left,pNode):IsNodeInBinTree(ps->Right,pNode);
//仔细想想其实就是前序遍历,只不过遇到 if(ps==pNode)条件成立时会立即返回
}
// 检测一棵树是否为完全二叉树
这个过程很有趣,首先我们使用的是层序遍历法,我们要明确什么是完全二叉树,既一个树的所有节点都按照根左右的顺序排列,所有节点靠左排列
那么完全二叉树就必定有一个特点,就是如果存在临界点,那么临界点之后的所有节点都没有孩子
画图吧
下面是代码
// 判断一棵二叉树是否为完全二叉树(层序遍历变形)
int IsCompleteBinTree(PTree ps)
{
int flag=0;
PTree pCur;
Queue str1;
assert(ps);
QueueInit(&str1);
QueuePush(&str1,ps);
while(QueueEmpty(&str1))
{
pCur=QueueFront(&str1);
QueuePop(&str1);
if(flag)
{
if(pCur->Left||pCur->Right)
{
return 0;
}
}
else
{
if(pCur->Left&&pCur->Right)
{
QueuePush(&str1,pCur->Left);
QueuePush(&str1,pCur->Right);
}
else if(pCur->Left)
{
QueuePush(&str1,pCur->Left);
flag=1;
}
else
{
return 0;
}
}
}
return 0;
}
//后序遍历(非递归课堂版)
void BackOrderClass(PTree ps )
{
PTree pFor;//这是我们用来记录最近一个被访问的节点指针
//原因是我们无法判断例如‘F’这个点它是否被访问过了
PTree pCur;//这个节点是用来保存当前节点的
StackD str;
StackDInit(&str);
pCur=ps;
while(pCur||StackDEmpty(&str))//通过栈是否为空判断是否遍历完毕
//第一次栈是空的,所以用pCur辅助我们通过第一次
{
while(pCur)//将左侧的节点全部入栈
{
StackDPush(&str,pCur);
pCur=pCur->Left;
}
pCur=StackDTop(&str);//取栈顶元素
if(pCur->Right==NULL||pCur->Right==pFor)
//若能走到这一步,则左定为空,若右也为空,则直接打印当前节点,
//若pCur->Right==最新一次访问的节点,我们也出栈打印,否则会造成死循环
{
StackDPop(&str);
printf("%c ",pCur->data);
pFor=pCur;//保存最新访问的节点
pCur=NULL;//为了防止对一个节点的左侧所有子树重复入栈
}
else
{
pCur=pCur->Right;//右子树不空,则将右子树当做根从头遍历
}
}
}
重建二叉树
当给定中序遍历结果数组和前序遍历数组结果
BTDataType *Pre="ABDCEF";
BTDataType *Mid="DBAECF";
我们用如下方式进行重建
//重建二叉树
PTree ReBuildBinTree(BTDataType *Pre,int PreSize,BTDataType *Mid,int left,int right)
{
//该函数的5个参数的意义,Pre:前序遍历的结果数组,PreSize:数组的大小
//Mid:中序遍历数组的结果,left;中序数组的左边界,right:中序数组的右边界(闭区间)
int i=left;//将左边界赋给i,确定区间
static int index=0;//这是用来逐个索引前序数组元素的下标
PTree PNewNode;
if(left>right)//判断递归结束的条件
{
return NULL;//满足条件说明没有该节点,直接赋NULL返回
}
assert(Pre);
assert(Mid);
while(Mid[i]!=Pre[index])//寻找当前区间的根节点
{
i++;
}
PNewNode=GetNewNode(Pre[index]);//将根节点的创建起来
index++;//将前序数组索引向前走一步,为下次寻找根节点准备
PNewNode->Left=ReBuildBinTree(Pre,PreSize,Mid,left,i-1);//创建当前区间的左子树
PNewNode->Right=ReBuildBinTree(Pre,PreSize,Mid,i+1,right);//创建当前区间的右子树
return PNewNode;//创建完成返回创建的节点,这一步和二叉树的拷贝类似
}
线索化二叉树(前序遍历)
右空指针域指向前驱,左空指针域指向后继
先创建线索二叉树节点
typedef enum Flag//枚举类型,用来区分是否已线索化
{
THREAD,
LINK
}Flag;
typedef struct ThreadTree
{
BTDataType data;
struct ThreadTree* TNLeft;
struct ThreadTree* TNRight;
Flag TLeft;
Flag TRight;
}ThreadTree,*PThreadTree;
然后创建可线索化的二叉树
//获得一个可线索化二叉树节点
PThreadTree GetNewTNode(BTDataType data)
{
PThreadTree NewNode=(PThreadTree)malloc(sizeof(ThreadTree));
if(NewNode==NULL)
{
return NULL;
}
NewNode->data=data;
NewNode->TNLeft=NULL;
NewNode->TNRight=NULL;
NewNode->TLeft=LINK;
NewNode->TRight=LINK;
return NewNode;
}
//创建可线索化的二叉树
void _CreatThreadTree(PThreadTree* ps,BTDataType *array,int size,BTDataType invalue,int *index)
{
if((*index<size)&&(invalue!=array[*index]))//退出条件一:数组遍历完了
//退出条件二;遇到‘#’
{
*ps=GetNewTNode(array[*index]);//创建新的节点
//printf("%d ",array[*index]);
++(*index);
_CreatThreadTree(&((*ps)->TNLeft),array,size,invalue,index);//进入左孩子
++(*index);
_CreatThreadTree(&((*ps)->TNRight),array,size,invalue,index);//进入右孩子
}
}
//外部——供用户使用的函数
//这里我们多封装了一层函数,目的是使用户不用关心数组索引(下标)
void CreatThreadTree(PThreadTree* ps,BTDataType *array,int size,BTDataType invalue)
{
int index=0;//这个变量作为数组的索引,必须要传地址,以便将来回到某级函数的时候索引是最新的值
_CreatThreadTree(ps,array,size,'#',&index);
}
线索化二叉树
//线索化二叉树
static PThreadTree pPrev=NULL;//这是因为单级函数返回时,pPrev还是原来的值
void PreThreadBinTree(PThreadTree ps,Flag right)//传right的原因是有可能将以线索化过得点重新线索化
{
if(ps&&right==LINK)//在这里使用right
{
if(ps->TNLeft==NULL)//遇到一个点的左是空
{
ps->TNLeft=pPrev;//将之前保存的节点赋给这个点的左,既空的左子树指向前驱节点
ps->TLeft=THREAD;//并标记左子树为已线索化
}
if(pPrev&&(NULL==pPrev->TNRight))//因为我们在线索化一个节点时并不知道它的后继节点是什么
//所以我们在线索化下一个节点的时候才处理前一个节点的右节点
//原因是我们想让一个节点的右空节点指向它的后继节点
//当然不同的遍历顺序后继前驱节点也必然不同
{
pPrev->TNRight=ps;//ps就是那个后继节点
pPrev->TRight=THREAD;
}
pPrev=ps;//这步是为了保存当前节点,为了下次函数调用时可以将其作为前驱节点
PreThreadBinTree(ps->TNLeft,ps->TLeft);//线索化左子树
PreThreadBinTree(ps->TNRight,ps->TRight);//线索化右子树
}
}
下来给出销毁二叉树的操作
// 销毁二叉搜索树 (后序遍历递归版)
void DestroyBSTree(PBSTreeNode* pp)
{
if(*pp)
{
DestroyBSTree((*pp)->Left);
DestroyBSTree((*pp)->Right);
free(*pp);
}
}