在数据结构------二叉树的基本操作(递归实现)一文中,介绍了二叉树的结构定义以及一些基本操作,本文将继续介绍一些有关二叉树的面试题。
1. 非递归实现二叉树的先序遍历
二叉树的非递归先序遍历要通过一个栈来实现。
先序的遍历的访问顺序为先根节点,再左孩子,最后右孩子。所以,根据栈后进先出的特点,应先入栈右孩子,再入栈左孩子。
(1)根节点入栈
(2)取栈顶元素,将根节点出栈,并访问。如果栈顶元素获取失败,说明栈为空,此时遍历结束,直接返回。
(3)如果右孩子不为空,先入栈右孩子
(4)如果左孩子不为空,再在入栈左孩子。
(5)循环(2)~(4)
假设有一棵二叉树如下图右侧,整个遍历过程见下图左侧:
代码如下:
//非递归实现树的先序遍历 void TreePreOrderByLoop(TreeNode* root) { if(root == NULL) { //空树 return; } SeqStack stack;//定义一个顺序栈 InitSeqStack(&stack);//初始化顺序栈 SeqStackPush(&stack,root);//入栈根节点 SeqQueueType top;//top用于存放栈顶元素 while(1) { int ret = SeqStackTop(&stack,&top);//取栈顶元素 if(ret == -1) { //此时栈为空,树已经遍历完成 return; } //访问栈顶元素 printf("%c ",top->data); //出栈栈顶元素 SeqStackPop(&stack); //栈顶元素的右子树非空时,入栈右子树 if(top->rchild != NULL) { SeqStackPush(&stack,top->rchild); } //栈顶元素的左子树非空时,入栈左子树 if(top->lchild != NULL) { SeqStackPush(&stack,top->lchild); } } return; }
2. 二叉树的非递归中序遍历
中序遍历还需要借助一个栈来实现。假设有一棵二叉树如下图:
在中序遍历中,最先遍历的是最左下方的结点D,所以要从根节点A开始,只要左子树不为空就将其入栈,然后继续判断左子树,直到左子树为空找到结点D。
结点D相当于它所在子树的根节点。该结点的左子树为空,所以直接访问D结点并出栈。
接下来要访问的是该结点的右子树(G所在的子树),而对于右子树G来说,还要找到右子树的最左下方的结点,所以先将右孩子G入栈,在循环入栈寻找最左下方的结点H。
找到H之后,访问H并出栈。此时便访问完了G的左子树。然后访问出栈根节点G。
此时,便访问完了子树D,即访问完了B的左子树,接下来访问出栈B,然后B的右子树,右子树为空,所以子树B访问完成即树A的左子树访问完成。
然后访问出栈根节点A,再访问树A的右子树。再次循环入栈寻找右子树的最左下方的结点C。访问出栈C,再访问出栈C的左子树F,然后树C访问完成。此时A的右子树访问完成即整棵树访问完成。
上述整个过程总结如下:设置当前节点指针cur,初始为根节点的指针
(1)如果cur的不为空就将cur入栈,然后cur指向cur的左子树。循环该过程直到为空则停止入栈
(2)取栈顶元素。
a)如果栈顶元素获取失败,则栈为空,表明树遍历结束,直接退出即可。
b)否则访问并出栈。
(3)使cur指向栈顶元素的右子树。
(4)循环(1)~(3)
代码如下:
//非递归实现树的中序遍历 void TreeInOrderByLoop(TreeNode* root) { if(root == NULL) { //空树 return; } SeqStack stack; InitSeqStack(&stack); //1. 定义cur为当前指针变量,初始指向root; TreeNode* cur = root; while(1) { //2. 当cur不为空时, while(cur != NULL) { // a)将cur入栈, SeqStackPush(&stack,cur); // b)并使cur指向cur的左子树, cur = cur->lchild; // c)循环a)b) } //3. 当cur为空时,取栈顶元素,使cur指向栈顶元素 int ret = SeqStackTop(&stack,&cur); // a)栈顶元素为空时,遍历结束 if(ret == -1) { return; } // b)栈顶元素不为空时,访问栈顶元素,并出栈 printf("%c ",cur->data); SeqStackPop(&stack); //4. 使cur指向cur的右子树 cur = cur->rchild; //5.循环2~4 } return; }
3. 非递归实现树的后序遍历
后序遍历与中序遍历过程类似,也需要利用一个栈来实现。整个实现过程如下:
设置当前节点指针cur,初始为根节点的指针,定义一个存储上一个访问结点的指针pre,初始为NULL,
(1)如果cur的不为空就将cur入栈,然后cur指向cur的左子树。循环该过程直到为空则停止入栈
(2)取栈顶元素top。
a)如果栈顶元素获取失败,则栈为空,表明树遍历结束,直接退出即可。
b)否则,如果top的右子树为空或者右子树已经访问过了(该栈顶元素的右子树与上一次访问的结点同)
则访问并出栈该栈顶元素,并将pre置为top.
c)如果不为a)b),因为要先访问完右子树,才能访问根节点,所以不能访问栈顶元素,而要将cur指向
栈顶元素的右子树
(4)循环(1)~(2)
根据上述描述,代码如下:
//非递归实现树的后序遍历 void TreePostOrderByLoop(TreeNode* root) { if(root == NULL) { return; } SeqStack stack; InitSeqStack(&stack); //1. 定义cur为当前指针变量,初始为root TreeNode* cur = root; TreeNode* pre = NULL; while(1) { //2. 当cur不为空时, while(cur != NULL) { // a) 将cur入栈 SeqStackPush(&stack,cur); // b)cur指向cur的左孩子 cur = cur->lchild; // c) 循环a)b) } //3. 当cur为空时,取栈顶元素,由cur指向 TreeNode* top = NULL; int ret = SeqStackTop(&stack,&top); //4. 如果栈顶元素为空,则遍历完程,直接返回即可 if(ret == -1) { return; } //5. 若栈顶元素不为空 // 1)如果栈顶元素的右孩子为空或栈顶元素的右孩子与上一个访问的元素相同, // 则访问栈顶元素,并出栈,然后继续回到3取栈顶元素进行判断 if(top->rchild == NULL || top->rchild == pre) { printf("%c ",top->data); pre = top; SeqStackPop(&stack); } // 2) 如果不为1),则不做处理,使cur指向cur的右孩子 else{ cur = top->rchild; } //6. 循环2~5 } return;
4. 求一棵树的镜像
如下图,左侧为原树,右侧为镜像,二者对叠起来完全重合:
从上图可以看出,对于镜像树中的任一节点,它的左右孩子都是原树左右孩子交换过后的结果。所以要遍历到原树中的每个节点,遍历到一个节点,将其左右孩子进行交换。
因此这里可以通过四种遍历方法来实现,遍历到一个结点,将打印操作替换为交换左右子树的操作,即可实现。
利用先序递归来实现:
//求一棵树的镜像 void Swap(TreeNode** a,TreeNode** b)//交换左右子树 { TreeNode* tmp; tmp = *a; *a = *b; *b = tmp; } void TreeMirror(TreeNode* root) { if(root == NULL) { //空树 return; } //先序遍历树的各节点,访问到一个节点,将该节点的左右孩子交换 //此时,访问操作为交换 Swap(&root->lchild,&root->rchild); TreeMirror(root->lchild); TreeMirror(root->rchild); return; }
利用层序非递归来实现:
//非递归求一棵树的镜像:将树的三种非递归遍历中的打印操作改为交换操作即可 //这里采用层序遍历来实现非递归求镜像 void TreeMirrorByLoop(TreeNode* root) { if(root == NULL) { //空树 return; } SeqQueue queue;//定义一个队列 SeqQueueInit(&queue); //1. 将根节点入队列 SeqQueuePush(&queue,root); while(1) { //2. 取队首元素, SeqQueueType top; int ret = SeqQueueTop(&queue,&top); //a)如果获取失败,表明队列为空,此时树遍历结束,直接返回即可 if(ret == -1) { return; } //b)否则并出队列,访问队首元素:交换左右子树 SeqQueuePop(&queue); Swap(&top->lchild,&top->rchild); //3. 如果队首元素的左右子树非空,就分别将左右子树入队列 if(top->lchild != NULL) { SeqQueuePush(&queue,top->lchild); } if(top->rchild != NULL) { SeqQueuePush(&queue,top->rchild); } //4. 循环2~3 } }
5. 判断一棵树是否为完全二叉树
下图为完全二叉树的四种表现形式:
上述四棵完全二叉树描述如下:对于层序遍历来说
(1)如果某个节点只有左子树,则它后面的结点必然没有子树(上图2,4)
(2)如果某个节点没有子树,则它后面的结点必然没有子树(上图1,3)
所以可以层序遍历树的各个节点,首先根节点进入分支一进行判断
分支一:
(1)如果某个节点左右子树都有,则它之后的结点继续进入分支一进行判断
(2)如果某个节点只有左子树,则它后面的结点进入分支二进行判断
(3)如果某个节点只有右子树,则该树必不为完全二叉树,直接退出即可
(4)如果某个节点没有子树,则它后面的结点要进入分支二进行判断
分支二:
(1)如果某个节点进入了该分支,则它后面的所有节点都应该进入该分支。
(2)如果进入该分支的结点的左右子树只要有一个不为空,则必然不是完全二叉树,直接退出即可。
当所有节点均遍历完成后还没退出,说明是完全二叉树。
上述利用的是层序遍历各节点,遍历到一个节点,进行一次上述的判断,所以,只需在层序遍历中将打印操作改为上述的判断操作即可。
代码如下:
//判断一棵树是否为完全二叉树(层序实现) int IsCompleteByLevel(TreeNode* root) { if(root == NULL) { //空树 return 0; } SeqQueue queue; SeqQueueInit(&queue); SeqQueuePush(&queue,root); SeqQueueType top; int which_flags = 0;//用于判断某个节点应该进入哪个分支,根节点首先进入分支一进行判断,所以初始为0 while(1) { int ret = SeqQueueTop(&queue,&top); if(ret == -1) { return 1;//当队列为空时,即所有节点都已经遍历并判断结束,还没有退出,说明是完全二叉树 } //出栈 SeqQueuePop(&queue); //访问该节点,这里的访问操作实际为对该节点进行如下判断 //分支一 if(which_flags == 0) { //1. 如果一个节点左右孩子均存在,则后面的节点还进入分支1进行判断 if(top->lchild != NULL && top->rchild != NULL) { which_flags = 0; } //2. 如果一个节点只有右孩子,则该树不是完全二叉树,直接返回即可。 else if(top->lchild == NULL && top->rchild != NULL) { return 0; } //3. 如果一个节点只有左子树,则后面的节点进入分支二 else if(top->lchild != NULL && top->rchild == NULL) { which_flags = 1; } //4,如果一个节点没有左右孩子,则后面的节点进入分支二 else { which_flags = 1; } } //分支二: //如果某个节点进入了分支2,则它后面的节点都应进入该分支 //进入分支2的所有节点如果左右孩子都没有,则为完全二叉树 else { //如果进入该分支的某个节点左孩子或右孩子不为空,则必然不是完全二叉树,直接返回即可 if(top->lchild != NULL || top->rchild != NULL) { return 0; } } if(top->lchild != NULL) { SeqQueuePush(&queue,top->lchild); } if(top->rchild != NULL) { SeqQueuePush(&queue,top->rchild); } } }
6. 根据先序和中序序列结果还原一颗树
如先序序列:ABDFGCH,中序序列:DBFGAHC。因为先序是:根节点->左子树->右子树,中序是:左子树->根节点->右子树。所以,在上述序列中:定义index为先序遍历序列中的下标,left为中序序列中某棵树所在范围左侧的下标,right为中序序列中某棵树所在范围右侧的下标。该范围是一左闭右开区间,如树A的范围为:[0,7)
(1)根据下标范围判断该树是否为空(为空:left >= right)
(2)不为空,则根据index处的元素创建结点,作为当前树的当前根节点
(3)找到当前根节点在中序序列中的下标,从而确定当前根节点的左右子树的范围。然后index++遍历到之后的结点。
(4)如果左子树不为空,则index处的元素必为左子树的根节点,此时可以根据index创建新的结点作为左子树的根结点(此处的操作与(1)~(3)相同,因此可以递归的创建左子树)。否则转至(5)
(5)如果右子树不为空,则index处的元素必为右子树的根节点,此时可以根据index处创建新的结点作为右子树的根节点(此处的操作与(1)~(3)相同,因此可以递归的创建右子树)。否则根节点的左右子树均创建完毕,直接返回根节点即可。
代码如下:
//寻找根节点在中序序列中的下标 int Find(char in_order[],size_t in_order_left,size_t in_order_right,TreeType to_find) { int cur = in_order_left; for(;cur < in_order_right;cur++) { if(in_order[cur] == to_find) { return cur; } } return -1; } TreeNode* _TreeRebuild(char pre_order[],size_t* pre_order_index,size_t size,\ char in_order[],size_t in_order_left,size_t in_order_right) { if(pre_order_index == NULL) { //非法输入 return NULL; } if(*pre_order_index >= size) { //先序序列遍历结束 return NULL; } if(in_order_left >= in_order_right) { //空树 return NULL; } TreeNode* root = CreateNode(pre_order[*pre_order_index]);//创建当前节点 //寻找当前节点在中序遍历中的位置下标 size_t cur_index_in_order = Find(in_order,in_order_left,in_order_right,pre_order[*pre_order_index]); (*pre_order_index)++;//遍历到后一个节点 //判断当前节点的左子树是否为空,不为空则后一个节点为当前节点的左子树,创建即可 root->lchild = _TreeRebuild(pre_order,pre_order_index,size,in_order,in_order_left,cur_index_in_order); //当前节点的左子树为空,判断当前节点的右子树是否为空,不为空则后一个节点为当前节点的右子树,创建即可 root->rchild = _TreeRebuild(pre_order,pre_order_index,size,in_order,cur_index_in_order + 1,in_order_right); //如果当前节点的左右子树都为空,则返回到当前节点的上一层节点 //当前节点的左右子树都创建完毕后,返回当前节点所在的子树 return root; } TreeNode* TreeRebuild(char pre_order[],char in_order[],size_t size) { if(pre_order == NULL || in_order == NULL || size <= 0) { //空树 return NULL; } //先序序列的当前遍历下标 size_t pre_order_index = 0; //当前树在中序序列中的下标范围,前闭后开:[0,size) size_t in_order_left = 0; size_t in_order_right = size; //进入递归函数,判断树是否为空,为空,则直接返回,不为空则创建节点 return _TreeRebuild(pre_order,&pre_order_index,size,in_order,in_order_left,in_order_right); }