基本概念
树与二叉树的区别:
\ | 树 | 二叉树 |
---|---|---|
结点数 | 至少为1 | 可以为0 |
最大度数 | 无限制 | 为2 |
五种基本形态:
特殊的二叉树:
(1)斜树:所有结点都只有左子树的二叉树称为左斜树;所有结点都只有右子树的二叉树称为右斜树。
<左斜树>
(2)满二叉树:所有的分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
(3)完全二叉树:按从上到下、从左到右的顺序为结点编号,与满二叉树序号一一对应的二叉树。
常见性质:
性质一 :一棵非空二叉树的第i层上至多有
2
i
−
1
2^{i-1}
2i−1个结点
证明:用数学归纳法即可
性质二:深度为h的二叉树至多有
2
h
−
1
2^{h}-1
2h−1个结点
证明:根据性质一,在结合等比数列前n项和公式
性质三:对于任何一棵二叉树T,如果其终端结点数为
n
0
n_{0}
n0,度为2的结点数为
n
2
n_{2}
n2,则
n
0
=
n
2
+
1
n_{0}=n_{2}+1
n0=n2+1
证明:
结点:
n
=
n
0
+
n
1
+
n
2
n=n_{0}+n_{1}+n_{2}
n=n0+n1+n2
分支:
n
=
n
1
+
2
n
2
+
1
n=n_{1}+2n_{2}+1
n=n1+2n2+1
故
n
0
=
n
2
+
1
n_{0}=n_{2}+1
n0=n2+1
性质四:具有n个结点的完全二叉树的深度为
log
2
n
\log_{2}n
log2n
证明:
2
k
−
1
≤
n
<
2
k
2^{k-1}\leq n<2^{k}
2k−1≤n<2k
取对数
k
−
1
<
log
2
n
≤
k
k-1<\log_{2}n\leq k
k−1<log2n≤k
推出
log
2
n
<
k
≤
log
2
n
+
1
\log_{2}n<k\leq\log_{2}n+1
log2n<k≤log2n+1
又k为整数
k
=
⌊
log
2
n
⌋
+
1
k=\lfloor \log_{2}n\rfloor+1
k=⌊log2n⌋+1
性质五:
对含n个结点的完全二叉树从上到下,从左至右进行1至n的编号,则任意一个编号为i的结点:
(1) 如果i=1,为根;如果i>1,则其双亲为
⌊
i
/
2
⌋
\lfloor i/2\rfloor
⌊i/2⌋
(2) 如果2i>n,则结点无左孩子,否则其左孩子为2i
(3) 如果2i+1>n,则无右孩子,否则其右孩子为2i+1
证明:归纳法即可
基本操作——遍历
1、前序遍历(DLR)
(1)访问根结点
(2)前序遍历左子树
(3)前序遍历右子树
2、中序遍历(LDR)
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
3、后序遍历(LRD)
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点
4、层序遍历(也称为广度遍历)
从第一层开始,从上到下逐层遍历,同层按从左到右的顺序遍历
前序:ABDEFGC
中序:DBFEGAC
后序:DFGEBCA
存储结构
顺序存储结构
方法:
1、将二叉树按照完全二叉树编号
2、然后用一维数组存储该二叉树,其中无结点的位置使用NULL表示
缺点:
浪费大量空间
二叉链表
template <class T>
struct Node
{
T data;
Node<T>* lch;
Node<T>* rch;
};
三叉链表
template <class T> class Node
{
public:
T data;
Node<T>* parent;
Node<T>* lchild;
Node<T>* rchild;
};
实现
声明
递归算法1 :
template<class T>
struct BiNode//这里采用的是二叉链表法
{
T data;
BiNode<T> *lchild;
BiNode<T> *rchild;
};
template<class T>
class BiTree
{
private:
void Create(BiNode<T> *&R,T data[],int i,int n);//创建二叉树
void Release(BiNode<T> *R);//释放二叉树
public:
BiNode<T> *root; //根结点
BiTree():root(NULL){} //空构造函数
BiTree(T data[],int n); //构造函数
void PreOrder(BiNode<T>*R); //前序遍历
void InOrder(BiNode<T>*R); //中序遍历
void PostOrder(BiNode<T>*R); //后序遍历
void LevelOrder(BiNode<T>*R);//层序遍历
~BiTree(); //析构函数
};
非递归算法:
template<class T>
class SNode
{
public:
BiNode <T>*ptr;
int tag; //栈结点标记,1为左子树,2为右子树
};
关键算法
1、创建
以顺序存储结构作为建立二叉树的输入,根据二叉树的定义,分三步建树:
1、建立根结点
2、建立左子树
3、建立右子树
template <class T>
void BiTree<T>::Create(BiNode<T>* &R, T data[], int i, int n) //i表示位置,从1开始
{
if ((i <= n) && (data[i - 1] != '0'))
{
R = new BiNode<T>; //创建根结点
R->data = data[i - 1];
R->lchild = NULL;
R->rchild = NULL;
Create(R->lchild, data, 2 * i, n); //创建左子树
Create(R->rchild, data, 2 * i + 1, n);//创建右子树
}
}
template<class T>
BiTree<T>::BiTree(T data[], int n)
{
Create(root, data, 1, int n);
}
2、前序、中序、后序遍历的实现
前序:
递归算法:
template<class T>
void BiTree<T>::PreOrder(BiNode<T> *Root)
{
if(Root != NULL)
{
cout<<Root->data; //访问结点
PreOrder(Root->lchild);//遍历左子树
PreOrder(Root->rchild);//遍历右子树
}
}
非递归算法1:
栈顶元素永远为当前元素的父结点,设R为当前访问的结点,则:
(1)若R!= NULL,访问R并人栈,调用R= R-> lchild(设R标记为1)返回(1)。
(2)若R= = NULL,重新设R=栈顶元素:
①若R标记=2,说明右子树返回,R出栈,重新设R=栈顶元素,返回①;
②若R标记-1,说明左子树返回,调用R=R-> rchlid(设R标记为2)返回(1)。
反复执行上述操作,直到栈空,程序结束
template<class T>
void BiTree<T>::PreOrder(BiNode<T> *R)//设R为当前访问的结点
{
BiNode<T> S[100];
int top = -1;
do
{
while (R != NULL)//(1)若R!= NULL,访问R并人栈,调用R= R-> lchild(设R标记为1)返回(1)
{
S[++top].R = R;
S[top].tag = 1;
cout << R->data;
R = R->lch;
}
//(2)若R= = NULL,重新设R=栈顶元素:
while ((top != -1) && (S[top].tag == 2))top--;//①若R标记=2,说明右子树返回,R出栈,重新设R=栈顶元素,返回①;
if ((top != -1) && S[top].tag == 1)//②若R标记-1,说明左子树返回,调用R=R-> rchlid(设R标记为2)返回(1)。
{
R = S[top].R->rch;
S[top].tag = 2;
}
} while (top != 1);
}
非递归算法2(考虑到1内部过于复杂,于是有基于1的优化):
问题分析:
二叉树前序遍历非递归的关键
在前序遍历完某结点的左子树后,找到该结点的右子树的根!
这就需要栈:
1)保存当前结点;
2)访问当前结点的左子树
3)访问当前结点左子树完毕后,当前结点出栈
4)访问其右子树
伪代码:
设R为当前访问的结点
(1)如果当前结点非空
访问当前结点
当前结点入栈;
将当前结点的左孩子作为当前结点;
(2)如果当前结点为空
栈顶结点出栈,
将该结点的右孩子作为当前结点;
反复执行(1)(2),直到当前结点=NULL && 栈空.
template<class T>
void BiTree<T>::PreOrder(BiNode<T> *R)//设R为当前访问的结点
{
BiNode<T> S[100];
int top = -1;
while((top != -1)||(R != NULL))//反复执行(1)(2),直到当前结点=NULL && 栈空.
{
if(R != NULL) //(1)如果当前结点非空
{
cout<<R->data; //访问当前结点
S[++top] = R; //当前结点入栈
R = R->lchild; //将当前结点的左孩子作为当前结点
}
else //(2)如果当前结点为空
{
R = S[top--]; //栈顶结点出栈
R = R->rchild; //将该结点的右孩子作为当前结点
}
}
}
中序:
递归算法:
template<class T>
void BiTree<T>::InOrder(BiNode<T> *Root)
{
if (Root != NULL)
{
PreOrder(Root->lchild);//遍历左子树
cout << Root->data; //访问结点
PreOrder(Root->rchild);//遍历右子树
}
}
非递归算法1:
伪代码
伪代码:(栈顶元素永远为当前结点的父结点)
(1)若R != NULL,R入栈,调用 R=R->lchild(设R标记为1) 返回(1)
(2)若 R==NULL,重新设 R=栈顶元素
①若R标记为2,说明右子树返回,R出栈,重新设R=栈顶元素,返回(1)
②若R标记为1,说明左子树返回,访问R,并调用 R=R->rchild(设R标记为2),返回(1)
反复执行上述操作直到栈空
template<class T>
void BiTree<T>::InOrder(BiNode<T> *R)
{
SNode<T> S[100]; //栈
int top = -1; //栈顶指针
do
{
while(R != NULL) //入栈,设置遍历左子树
{
S[++top].R = R;
S[top].tag = 1;
R = R->lchild;
}
while((top != -1)&&(S[top].tag == 2)) top--; //出栈
if((top != 1)&&(S[top].tag == 1)) //访问栈顶元素
{ //遍历右子树
cout<<S[top].R->data;
R = S[top].R->rchild;
S[top].tag = 2;
}
}while(top != -1);
}
非递归算法2:
伪代码
(1)如果当前结点非空
当前结点入栈;
将当前结点的左孩子作为当前结点;
(2)如果当前结点为空
栈顶结点出栈,
访问当前结点
将该结点的右孩子作为当前结点;
反复执行1)2),直到当前结点NULL && 栈空
template <class T>
void BiTree<T>::InOrder(Node<T> *R)
{
Stack<Node<T>*> S;
while (!S.IsEmpty() || (R != NULL))//反复执行1)2),直到当前结点NULL && 栈空
{
if (R != NULL) //(1)如果当前结点非空
{
S.Push(R); //当前结点入栈;
R = R->lchild; //将当前结点的左孩子作为当前结点;
}
else //(2)如果当前结点为空
{
R = S.Pop(); //栈顶结点出栈
cout << R->data; //访问当前结点
R = R->rchild; //将该结点的右孩子作为当前结点
}
}
}
后序:
递归算法:
template<class T>
void BiTree<T>::PostOrder(BiNode<T> *Root)
{
if (Root != NULL)
{
PreOrder(Root->lchild);//遍历左子树
PreOrder(Root->rchild);//遍历右子树
cout << Root->data; //访问结点
}
}
非递归算法1:
伪代码:栈顶元素永远为当前结点的父结点
(1)若R != NULL,R入栈,调用 R=R->lchild(设R标记为1) 返回(1)
(2)若 R==NULL,重新设 R=栈顶元素
①若R标记为2,说明右子树返回,R出栈,重新设R=栈顶元素,返回(1)
②若R标记为1,说明左子树返回,访问R,并调用 R=R->rchild(设R标记为2),返回(1)
反复执行上述操作直到栈空
template<class T>
void BiTree<T>::PostOrder(BiNode<T> *R)
{
SNode<T> S[100];
int top= -1;
do
{
while(R != NULL)
{
S[++top].R = R;
S[top].tag = 1;
R = R->lchild;
}
while((top != -1)&&(S[top].tag == 2))
{
cout<<S[top].R->data;
top--;
}
if((top != 1)&&(S[top].tag == 1))
{
R = S[top].R->rchild;
S[top].tag = 2;
}
}while(top != -1);
}
非递归算法2:
无法实现,父结点在子结点后被访问,无法提前出栈。
3、层序遍历的实现
根结点非空,入队。
如果队列不空
{
队头元素出队
访问该元素
若该结点的左孩子非空,则左孩子入队;
若该结点的右孩子非空,则右孩子入队;
}
template<class T>
void BiTree<T>::LevelOrder(BiNode<T> *R)
{
BiNode<T> *queue[MAXSIZE];
int f=0,r=0; //初始化空队列
if(R != NULL)
queue[++r] = R; //根结点入队
while(f != r)
{
BiNode<T> *p=queue[++f]; //队头元素出队
cout<<p->data;//出队打印
if(p->lchild != NULL)
queue[++r] = p->lchild;//左孩子入队
if(p->rchild != NULL)
queue[++r] = p->rchild;//右孩子入队
}
}
4、析构函数的实现
采用后序遍历的方式,防止内存泄漏
template < class T >
void BiTree<T>::Release(Node<T> *Root)
{
if (Root != NULL)
{
Release(Root->lchild); // 释放左子树
Release(Root->rchild); // 释放右子树
delete Root; // 释放根结点
}
}
template < class T >
void BiTrees<T>::~BiTree()
{
Release(root);
}
1、所有递归函数都可以改写成效率高的非递归函数 ;2、递归调用普遍规律:(1)形参为局部变量(2)函数调用实参入栈(3)函数调用完毕,实参出栈,自动返回上一级 ↩︎