树形结构是数据结构中一种非常常用的非线性结构。通常用来表示具备分支关系的层次结构。其中二叉树又是树形结构中最简单最常见的一种。
二叉树,顾名思义,只有两个分叉的树,他的特点就是每个节点至多只有两颗子树(即二叉树中不存在度大于2的节点),通常情况下,我们称二叉树的两颗子树为左子树和右子树。二叉树又可递归定义为:①一个空树;②左子树和右子树均为二叉树。换句话说,二叉树有三个部分组成,根节点和同为二叉树的左右子树。这种递归定义非常有趣,它将帮助我们解决很多二叉树的操作问题。
二叉树中有两种特殊的形态,分别是完全二叉树和满二叉树。
满二叉树是指,除叶子节点外,每个节点都有两个子节点,从视觉上讲,就是这颗二叉树被完全填充。
完全二叉树形象上讲是指“最右下角”的节点可能空缺,即只能是最右边且是最大层次的节点空缺。他有两个重要的特点:
①、叶子节点只可能出现在最大的两层上。
②、对任意节点,若其右子树的最大层次为l,那么其左子树的最大层次必定为l或l+1。
性质1:在二叉树的第i层至多有2i-1个节点(i>=1),这点有数学归纳法很容易证得。
性质2:深度为k的二叉树至多有2k-1个节点(k>=1),显然,节点最多的情形即为满二叉树。
性质3:对任意一颗二叉树,若其叶子节点(其度为0)数为n0,度为2的节点数为n2,则有n0=n2+1。
证明:设度为1的节点数为n1,那么总结点数n = n0+n1+n2;
设二叉树的分支数为B,由于除了根节点外,每个节点都由一个分支射入,即对应一个分支,即n = B+1。
而每个分支都是有度为1或2的节点射出的,即B = n1+2*n2。
则有n = n1+2*n2+1
综上,有
n0 = n2+1
遍历操作是二叉树最重要的操作,其他的操作大都围绕遍历来完成。根据二叉树的递归定义我们可以知道,只要分别完成对二叉树的三个部分的遍历,即可完成对整个二叉树的遍历。
二叉树的遍历通常根据对根节点的访问顺序,分为先序遍历、中序遍历和后续遍历,即先(根)序遍历、中(根)序遍历和后(根)序遍历。以下将详细阐述三种遍历方式。
PreOder(T) = T+PreOder(T的左子树)+PreOder(T的右子树)
InOder(T)=InOder(T的左子树)+T+InOder(T的右子树)
PostOder(T) = PostOder(T的左子树)+PostOder(T的右子树)+T
1、二叉树的链式存储结构:
由二叉树的定义可知,每个节点至少要包含三个部分,即数据、左节点指针、右节点指针。当然,在一些特殊的场合,可能还会有一些其他数据域,如父节点指针、层次、访问标记等。目前暂时考虑最简单的情形。
1 typedef char TElemType; 2 /****二叉树的节点的存储结构****/ 3 typedef struct BiTNode 4 { 5 TElemType data; 6 struct BiTNode *lchild, *rchild; 7 }BiTNode,*BiTree;
2、先序遍历:
递归式先序遍历:
根据先序遍历的定义,先访问根节点,再依次访问左右子树,不难得出先序遍历的递归代码如下:
1 bool PreOrderTraverse_rec( BiTree T ,bool (* PrintElem)(TElemType e)) 2 { 3 if(T) 4 { 5 if (PrintElem(T->data)) 6 { 7 if (PreOrderTraverse_rec( T->lchild,PrintElem)) 8 { 9 if (PreOrderTraverse_rec( T->rchild ,PrintElem)) 10 return true; 11 } 12 } 13 return false; 14 } 15 else 16 return true; 17 }
PrintElement是每个元素的操作函数,最简单的操作就是打印了。
1 bool PrintElem( TElemType e) 2 { 3 cout<<e<<" "; 4 return true; 5 }
非递归式先序遍历:
采用栈模拟递归过程实现非递归。对栈中的每一个节点,执行操作,并压栈,沿着左路走到底,再弹出,将右节点压栈,重复上述操作。
算法步骤:
a.从根开始,访问栈顶元素,不断将左子树指针压栈,直到指针为空为止;
b.弹栈,对栈顶元素。
如果,该节点的右结点不为空,则转到a;
否则,转到b。
c.栈空,则结束。
版本一:
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式先序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PreOrderTraverse( BiTree T ,bool (*PrintElem)(TElemType e)) 5 { 6 SqStack S; BiTree p; 7 InitStack(S); Push(S,T); 8 while (!StackEmpty(S)) 9 { 10 while(GetTop(S,p)&&p) 11 { 12 if (!PrintElem(p->data)) return false; 13 Push(S,p->lchild); 14 } 15 Pop(S,p);//弹出压入的空字符 16 if(!StackEmpty(S)) 17 { 18 Pop(S,p); 19 Push(S,p->rchild); 20 } 21 } 22 cout<<endl; 23 return true; 24 }
版本二:
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式先序遍历二叉树第二种方法,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PreOrderTraverse2( BiTree T ,bool (*PrintElem)(TElemType e)) 5 { 6 SqStack S; BiTree p; 7 InitStack(S); 8 p = T; 9 while(p||!StackEmpty(S)) 10 { 11 if(p) 12 { 13 Push(S,p); 14 if (!PrintElem(p->data)) return false; 15 p = p->lchild; 16 } 17 else 18 { 19 Pop(S,p); 20 p = p->rchild; 21 } 22 } 23 24 cout<<endl; 25 return true; 26 }
3、中序遍历:
递归式中序遍历:
1 /////////////////////////////////////////////////////////////// 2 //用递归的方式中序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool InOrderTraverse_rec( BiTree T,bool (*PrintElem)(TElemType e)) //递归 5 { 6 if(T) 7 { 8 if (InOrderTraverse_rec( T->lchild ,PrintElem)) 9 if (PrintElem(T->data)) 10 if (InOrderTraverse_rec( T->rchild, PrintElem)) 11 return true; 12 return false; 13 } 14 else 15 return true; 16 }
非递归式中序遍历:
与先序遍历类似。先将左子树节点压栈,直到尽头,弹出时,执行操作,再将右子树节点压栈,重复上述操作。
算法步骤:
a.从根开始,不断将左子树指针压栈,直到指针为空为止。
b.访问栈顶元素。
如果,该节点的右结点不为空,则转到a;
否则,转到b。
c.栈空,则结束。
版本一:
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式中序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool InOrderTraverse( BiTree T ,bool(*PrintElem)(TElemType e)) //非递归1 5 { 6 SqStack S; BiTree p; 7 InitStack(S); Push(S,T); 8 while(!StackEmpty(S)) 9 { 10 while(GetTop(S,p)&&p) 11 { 12 Push(S,p->lchild); 13 p=p->lchild; 14 } 15 Pop(S,p); 16 if(!StackEmpty(S)) 17 { 18 Pop(S,p); 19 if (!PrintElem(p->data)) return false; 20 Push(S,p->rchild); 21 } 22 } 23 cout<<endl; 24 return true; 25 }
版本二:
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式中序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool InOrderTraverse2( BiTree T ,bool(*PrintElem)(TElemType e)) //非递归1 5 { 6 SqStack S; BiTree p; 7 InitStack(S); 8 p = T; 9 while(p||!StackEmpty(S)) 10 { 11 if(p) 12 { 13 Push(S,p); 14 p = p->lchild; 15 } 16 else 17 { 18 Pop(S,p); 19 if (!PrintElem(p->data)) return false; 20 p = p->rchild; 21 } 22 } 23 24 cout<<endl; 25 return true; 26 }
可以发现,非递归式的先序遍历和中序遍历代码非常相似,仅是改变了操作函数的位置。
4、后序遍历:
递归式后序遍历:
1 /////////////////////////////////////////////////////////////// 2 //用递归的方式后序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PostOrderTraverse_rec( BiTree T ,bool (* PrintElem)(TElemType e)) 5 { 6 if(T) 7 { 8 if (PostOrderTraverse_rec( T->lchild ,PrintElem)) 9 if (PostOrderTraverse_rec( T->rchild, PrintElem)) 10 if (PrintElem(T->data)) 11 return true; 12 return false; 13 } 14 else 15 return true; 16 }
非递归式后序遍历:
后续遍历的非递归方式相比前面两种有点复杂。主要原因在于前面两种遍历,每个结点仅有一次出入栈,而后序遍历中,每个节点会有两次出入栈.
算法步骤:
a.从根开始,不断将左子树指针压栈,直到指针为空为止;
b.看栈顶元素
如果:它没有节点,或者它的右结点被访问过,访问该结点,弹栈;转到b;
否则:它的节点不空且未被访问过,则必须将它的右结点继续压栈;因为可能他的右结点是另一颗子树的根节点。转到a;
c.栈空,则结束。
版本一:
根据算法步骤,在原结点结构体中加入一个字段代表是否访问过。
1 typedef struct BiTNode 2 { 3 TElemType data; 4 struct BiTNode *lchild, *rchild; 5 int mark ; //初始化为未被访问过,0 6 }BiTNode,*BiTree;
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式后序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PostOrderTraverse( BiTree T ,bool (* PrintElem)(TElemType e)) 5 { 6 BiTree p,q; 7 SqStack S; 8 9 p = T; 10 q = NULL; 11 InitStack(S); 12 13 while (p || !StackEmpty(S)) 14 { 15 if (p && p->mark==0) 16 { 17 Push(S,p); 18 p = p->lchild; 19 } 20 else 21 { 22 Pop(S,p); 23 24 if (p->rchild && p->rchild->mark==0) //存在右孩子,则还把该节点压栈,顺着其左子树继续 25 { 26 q = p; 27 p = p->rchild;//讨论p的右结点,现在右结点还没有被压栈 28 Push(S,q);//把原来的p重新压回栈 29 continue; 30 } 31 32 if (!PrintElem(p->data)) return false; 33 p->mark = 1;//标志为已访问过 34 if (p == T) break;//已经访问到了头结点 35 }//else 36 }//while 37 cout<<endl; 38 return true; 39 }
版本二:
算法步骤中,要访问某节点,必须确认其右结点已经访问过,即右结点必须是上一个访问的节点,所以版本二中通过保存上次访问节点来实现。不需要添加额外字段。
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式后序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PostOrderTraverse2( BiTree T ,bool (* PrintElem)(TElemType e)) 5 { 6 BiTree p,q; 7 SqStack S; 8 9 p = T; 10 q = NULL; 11 InitStack(S); 12 Push(S,p); 13 while(!StackEmpty(S)) 14 { 15 while(GetTop(S,p) && p) 16 { 17 Push(S,p->lchild); 18 p=p->lchild; 19 } 20 Pop(S,p); 21 q = NULL;//代表刚刚访问的节点 22 while(!StackEmpty(S)) 23 { 24 GetTop(S,p); 25 if (p->rchild==NULL || p->rchild==q)//q表示刚访问过的结点 26 { 27 if (!PrintElem(p->data)) return false; 28 q=p; //记录访问过的结点 29 Pop(S,p); 30 } 31 else 32 { 33 p=p->rchild; 34 Push(S,p); 35 break; 36 } 37 } 38 39 } 40 cout<<endl; 41 return true; 42 }
版本三:
在原结构体中加入字段标记左右子树,当为右子树时,说明右子树访问过,可以访问该节点。
1 typedef enum{L,R} TagType; 2 3 typedef struct BiTNode 4 { 5 TElemType data; 6 struct BiTNode *lchild, *rchild; 7 TagType tag; 8 }BiTNode,*BiTree;
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式后序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PostOrderTraverse3( BiTree T ,bool (* PrintElem)(TElemType e)) 5 { 6 BiTree p; 7 SqStack S; 8 9 p = T; 10 InitStack(S); 11 12 do 13 { 14 while(p!=NULL) 15 { 16 p->tag = L;//标记为左子树 17 Push(S,p); 18 p = p->lchild; 19 } 20 while(!StackEmpty(S) && GetTop(S,p) && p->tag==R)//表示右子树访问结束,可以访问该节点 21 { 22 Pop(S,p); 23 if (!PrintElem(p->data)) return false; 24 } 25 26 if(!StackEmpty(S)) 27 { 28 GetTop(S,p); 29 p->tag = R;//标记为右子树 30 p = p->rchild; 31 } 32 }while(!StackEmpty(S)); 33 cout<<endl; 34 return true; 35 }
版本四:
前面所有的方式,本质上都是通过栈记录历史信息来模拟递归,版本四提供了一种巧妙的方法,可以不用栈,实现非递归式后序遍历。具体的实现方法是在节点中加入一个状态域来保存当前状态,是该访问左子树、右子树,还是访问节点。
1 typedef enum{L,R,V} TagType; 2 3 typedef struct BiTNode 4 { 5 TElemType data; 6 struct BiTNode *lchild, *rchild; 7 TagType tag; 8 }BiTNode,*BiTree;
1 /////////////////////////////////////////////////////////////// 2 //用非递归的方式后序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool PostOrderTraverse4( BiTree T ,bool (* PrintElem)(TElemType e)) 5 { 6 BiTree p,q; 7 p = T; 8 9 while(p) 10 { 11 switch(p->tag) 12 { 13 case L: 14 p->tag = R;//接下来要访问右子树 15 if(p->lchild) 16 p = p->lchild; 17 break; 18 case R: 19 p->tag = V;//右子树访问完毕,接下来可以访问根了 20 if(p->rchild) 21 p = p->rchild; 22 break; 23 case V: 24 if (!PrintElem(p->data)) return false; 25 p->tag = L; 26 p = GetParent(T,p);//指向父节点 27 break; 28 } 29 } 30 cout<<endl; 31 return true; 32 }
以上代码中有一部分是指向某节点的父节点,一种做法是节点数据域中加入一个指针指向父节点,在创建二叉树时就做好这项工作。另一种是每次都从二叉树中找某节点的父节点。上述代码用的是第二种方法:编写了从二叉树T中找到节点child的父节点的函数GetParent。
1 BiTree GetParent(BiTree T,BiTree child) 2 { 3 LinkQueue Q; 4 BiTree p; 5 Q.EnQueue(T); 6 while(!Q.QueueEmpty()) 7 { 8 Q.DeQueue(p); 9 if (p->lchild == child || p->rchild == child) 10 return p; 11 12 if(p->lchild) 13 { 14 Q.EnQueue(p->lchild); 15 } 16 17 if(p->rchild) 18 { 19 Q.EnQueue(p->rchild); 20 } 21 } 22 return NULL; 23 }
5、层序遍历:
层序遍历形象的就是按层次访问二叉树。上面的节点永远都是先访问的,很容易联想到“先进先出”的队列。层序遍历就是借助队列来实现的。
算法步骤:
a.将根节点移进队列;
b.访问队列头节点,若其有左右节点,分别将其左右节点也移进队列,转到b;
c.队列空,则操作结束。
1 /////////////////////////////////////////////////////////////// 2 //层序遍历二叉树,若失败则返回false 3 /////////////////////////////////////////////////////////////// 4 bool LevelOrderTraverse( BiTree T, bool (* PrintElem)(TElemType e) ) 5 { 6 LinkQueue Q; 7 BiTree p; 8 Q.EnQueue(T); 9 while(!Q.QueueEmpty()) 10 { 11 Q.DeQueue(p); 12 if (!PrintElem(p->data)) return false; 13 14 if(p->lchild) 15 { 16 Q.EnQueue(p->lchild); 17 } 18 19 if(p->rchild) 20 { 21 Q.EnQueue(p->rchild); 22 } 23 } 24 cout<<endl; 25 return true; 26 }
练习题:
1、Binary Tree Preorder Traversal
四、二叉树的其他操作
1、递归创建二叉树
1 bool CreateBiTreeFromStdin( BiTree &T ) 2 { 3 TElemType ch; 4 5 ch = getchar(); 6 if( ch=='#' ) T = NULL; 7 else 8 { 9 if( !(T = (BiTNode *)malloc(sizeof(BiTNode))) ) 10 exit(OVERFLOW); 11 T->data = ch; 12 CreateBiTreeFromStdin( T->lchild); 13 CreateBiTreeFromStdin( T->rchild); 14 } 15 return OK; 16 }
2、递归复制二叉树
1 ///////////////////////////////////////////////////////////////////////////// 2 //递归复制二叉树,若成功则返回true 3 ////////////////////////////////////////////////////////////////////////////// 4 bool CopyTree(BiTree ST, BiTree &DT) 5 { 6 if (!ST) 7 DT = NULL; 8 else 9 { 10 DT = (BiTree)malloc(sizeof(BiTNode)); 11 DT->data = ST->data; 12 13 CopyTree(ST->lchild,DT->lchild); 14 CopyTree(ST->rchild,DT->rchild); 15 } 16 return true; 17 }
3、判断一个二叉树是否为完全二叉树
利用层序遍历的思想
1 //////////////////////////////////////////////////////////////////////////// 2 //判断一个二叉树是否为完全二叉树,若是则返回true,否则返回false 3 //////////////////////////////////////////////////////////////////////////// 4 bool IsFullBiTree( BiTree T ) 5 { 6 LinkQueue Q; 7 BiTree p; 8 int flag=0; 9 Q.EnQueue(T); 10 11 while(!Q.QueueEmpty()) 12 { 13 Q.DeQueue(p); 14 if(!p) 15 flag=1; 16 else 17 if(flag) 18 { 19 cout<<"该树不是完全二叉树!\n"; 20 return false; 21 } 22 else 23 { 24 Q.EnQueue(p->lchild); 25 Q.EnQueue(p->rchild); 26 } 27 } 28 cout<<"该树是完全二叉树!\n"; 29 return true; 30 }
4、交换左右子树
1 ///////////////////////////////////////////////////////////////////////////// 2 //递归交换二叉树的左右子树,若成功则返回true 3 ////////////////////////////////////////////////////////////////////////////// 4 bool Revolute_BT( BiTree &T ) 5 { 6 7 BiTree temp; 8 //交换 9 temp = T->lchild; 10 T->lchild = T->rchild; 11 T->rchild = temp; 12 13 if(T->lchild) 14 Revolute_BT(T->lchild); 15 if(T->rchild) 16 Revolute_BT(T->rchild); 17 18 return true; 19 }
5、求二叉树叶子节点数
1 ///////////////////////////////////////////////////////////////////////////// 2 //递归求二叉树的叶子节点的个数,参数为二叉树的头结点指针和个数count的引用,若成功返回true, 3 //若为空树,返回false 4 /////////////////////////////////////////////////////////////////////////// 5 bool CountLeaf( BiTree T ,int &count) 6 { 7 if (!T) return false; 8 9 if ( (!T->lchild)&&(!T->rchild) )//既无左孩子,也无右孩子 10 count++; 11 CountLeaf( T->lchild,count ); 12 CountLeaf( T->rchild,count ); 13 14 return true; 15 }
6、求二叉树的繁茂度(高度*单层最多节点数)
1 /////////////////////////////////////////////// 2 //求二叉树的繁茂度(高度*单层最多节点数),参数为二叉树的头节点,返回繁茂度值 3 ////////////////////////////////////////////// 4 int GetFanMao(BiTree T) 5 { 6 int count[100]; //用来存储每一层的节点数,0号单元未用 7 memset(count,0,sizeof(count)); 8 int h; //树的高度 9 int maxn; //单层最大节点数 10 int i; 11 int fm;//繁茂度 12 BiTree p; 13 if (!T) return 0; 14 LinkQueue Q; 15 Q.EnQueue(T); 16 while(!Q.QueueEmpty()) 17 { 18 Q.DeQueue(p); 19 count[p->layer]++; 20 21 if(p->lchild) 22 { 23 Q.EnQueue(p->lchild); 24 p->lchild->layer = p->layer+1; //可以求得该节点的层数 25 } 26 if(p->rchild) 27 { 28 Q.EnQueue(p->rchild); 29 p->rchild->layer = p->layer+1; 30 } 31 } 32 h=p->layer;//高度 33 34 for(maxn=count[1],i=1;count[i];i++)//求层最大结点数 35 if(count[i]>maxn) 36 maxn=count[i]; 37 38 fm = h*maxn; //计算繁茂度 39 return fm; 40 }
7、求二叉树的高度
1 /////////////////////////////////////////////////////////////// 2 //求二叉树的高度,参数为二叉树的头节点,返回高度值 3 /////////////////////////////////////////////////////////////// 4 int GetHeight( BiTree T ) 5 { 6 int lheight,rheight,max,h; 7 8 if ( !T ) return 0; 9 else 10 { 11 lheight = GetHeight( T->lchild ); 12 rheight = GetHeight( T->rchild ); 13 max = lheight > rheight ? lheight : rheight; 14 h = max+1; 15 return h; 16 } 17 }
8、 求完全二叉树的节点数(递归版本和非递归版本)
http://www.cnblogs.com/codershell/p/3306676.html
9、递归释放一颗二叉树
1 void Remove(BiTNode* u) 2 { 3 if(u==NULL) return; 4 Remove(u->left); 5 Remove(u-right); 6 free(u); 7 }
10、二叉树的重建
11、二叉树的旋转
12、二叉树中两结点的最低公共祖先
后续将会补充更多与二叉树相关的操作,如果文中有任何错误或表述不清楚的,欢迎大家及时指出。