目录
1.树的相关概念以及定义
1. 1 树形结构
1.2 树的定义
1.3 树的基本术语
2. 二叉树
2.1 二叉树的定义
2.2 二叉树抽象类型数据定义
2.3 三种特殊形式的二叉树、
2.3.1 斜树
2.3.2 满二叉树
2.3.3 完全二叉树
2.4 二叉树的性质
2.5 二叉树的存储结构
2.5.1 二叉树的顺序存储
2.5.2二叉树的链式存储
2.5.3 C++ 的类定义
2.6 遍历二叉树
2.7 根据遍历序列确定二叉树
2.8 遍历的递归算法实现
2.8.1 先序遍历
2.8.1 中序遍历
2.8.1 后序遍历
2.10 遍历二叉树的非递归算法
2.11 二叉树的层次遍历
2.12 二叉树遍历算法的应用
2.12.1 二叉树的建立
2.12.2 二叉树的复制
结点之间有分支,具有层次关系
树(Tree)是个结点的有限集。
若n=0,称为空树。
若n>0,则它满足如下两个条件:
(1)有且只仅有一个特定的称为根(Root)的结点。
(2)其余节点可分为m(m>=0)个互不相交的有限集T1,T2,T3...其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
显然,树的定义是递归的定义。
图 1.2.1 树的示意图
结点:数据元素以及指向子树的分支。
根节点:非空树中五前驱结点的结点。
叶子(终端)结点:度为0。
分支节点(非终端)节点:度不为0。
内部节点:根节点以外的分支节点。
结点的度:结点拥有的子树数。
树的度:树内各节点的度的最大值。
结点子树的根称为该节点的孩子,该节点称为孩子的双亲。拥有共同双亲的结点称为兄弟结点。双亲在同一层的结点称为堂兄弟。
结点的祖先:从根到该节点所经分支上的所有节点。
结点的子孙:以某节点为根的子树中任一结点。
树的深度:树中结点最大层数。
有序树:树中结点的各子树从左到右依次有序(最左边的为第一个孩子)。
无序树:树中结点的各子树无次序。
森林:是m(m>=0)棵互不相交的集合。把根节点删除,树就变成了森林。一棵树可以看成一个特殊的森林,给森林中的各子树加上一个双亲结点,森林就变成了树。树一定是森林,森林不一定是树。
个节点最多只有两个“叉”的树,普通树(多叉树)若不转为二叉树,则运算很难实现。
✔ 二叉树的结构最简单,规律性最强。
✔ 可以证明,所有树都能转为唯一对应的二叉树,不失一般性。
二叉树在树结构的应用中起着非常重要的作用,因为二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构以及运算中存在的复杂性。
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根节点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
1. 每个节点最多有俩孩子(二叉树中不存在度大于2的结点)
2. 子树有左右之分,其次序不能颠倒。
3. 二叉树可以是空集合,根可以有空的左子树或空的右子树。
注:
1. 二叉树不是树的特殊情况,它们是两个概念。
2. 二叉树结点的子树要区分左子树和右子树,即使只有一颗子树也要进行区分,说明它是左子树还是右子树。
3. 树当节点只有一个孩子时,就无需区分它是左还是右的次序,因此二者是不同的。这是二叉树与树最主要的差别。
也就是说二叉树每个结点位置或者次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,就无所谓左右了。
二叉树的五种基本形态:
(1)空二叉树;(2)根和空的左右子树;(3)根和左子树;(4)根和右子树;(5)根和左右子树
ADT BinaryTree{
数据对象D:D是具有相同特性的数据元素的集合。
数据关系R:
// 若D=Φ,则R=Φ,称BinaryTree为空二叉树;
// 若D≠Φ,则R={H},H是如下二元关系;
// (1)在D中存在惟一的称为根的数据元素root,它在关系H下无前驱;
// (2)若D-{root}≠Φ,则存在D-{root}={D1,Dr},且D1∩Dr =Φ;
// (3)若D1≠Φ,则D1中存在惟一的元素x1,
// (4)(D1,{H1})是一棵符合本定义的二叉树,称为根的左子树;(Dr,{Hr})是一棵符合本定义的二叉树,称为根的右子树。
基本操作P:// 至少20个,只列出比较重要的
PreOrderTraverse( T, visit() )
// 初始条件:二叉树T存在,Visit是对结点操作的应用函数。
// 操作结果:先序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
InOrderTraverse( T, visit() )
// 初始条件:二叉树T存在,Visit是对结点操作的应用函数。
// 操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
PostOrderTraverse( T, visit() )
// 初始条件:二叉树T存在,Visit是对结点操作的应用函数。
// 操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。
}ADT BinaryTree
满二叉树与完全二叉树在顺序储存方式下可以复原。
所有结点都只有左子树的二叉树称为左斜树,所有结点都只有右子树的二叉树称为右斜树,左斜树、右斜树统称为斜树。斜树中,每层只有一个结点,所有斜树的结点个数与深度相同。
一棵深度为k,且有个节点的二叉树称为满二叉树。
特点:
(1)每一层上的结点数都是最大结点数(即每层都满)。
(2)叶子结点全部在最底层。
(3)只有度为0或2的结点。
(4)满二叉树在同样深度的二叉树中节点个数最多,叶子结点个数也最多。
图2.3.2 对满二叉树结点位置进行编号
编号规则:从根节点开始,自上而下,自左到右。每一结点位置都有元素。
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树。
特点:
(1)叶子只可能分布在层次最大的两层上。
(2)对任一结点,如果右子树的最大层次为i,则其左子树的最大层次必为i或i+1。
1. 性质1. 在二叉树的第i层至多有个结点。至少有一个结点。
2. 性质2. 深度为k的二叉树至多有个结点。至少有k个结点。
3. 性质3. 对任何一棵二叉树T,如果其叶子树为,度为2的节点数为,则。
总边数B,总结点数为n,从下至上,每个结点都有一个双亲,只有根节点没有,
从上至下,度为2的节点有两个边,度为1的节点有一个边,
4. 性质4. 具有n个节点的完全二叉树的深度为 (表明了n与k的关系)
[x]称作x的底,表示不大于x的最大整数。
5. 性质5. 对于一棵具有n个结点的完全二叉树,其中的结点从1开始按层序编号,则对于任意的编号为的结点,有以下结论:
(1)如果i=1,则i是二叉树的根,无双亲,如果i>1,则其双亲结点是[i/2]
(2)如果2i>n,则结点i为叶子节点,无左孩子,否则,其左孩子为结点2i
(3)如果2i+1>n,则结点i为叶子节点,无右孩子,否则,其右孩子为结点2i+1
表明双亲结点编号与孩子结点编号之间的关系。
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。
图 2.5 二叉树的顺序储存
从图中可以看出,这种储存方法会造成空间的浪费,最坏的情况就是右斜树,一棵深度为k的右斜树,只有k个结点,却要分配个储存单元,事实上,二叉树的顺序储存结构一般仅适合于储存完全二叉树。
二叉树一般采用二叉链表储存结构,令二叉树每一个结点对应一个链表结点,链表结点除了存放与二叉树结点有关的数据信息之外,还要指示其左右孩子结点的指针。在n个结点的二叉链表中,必有2n个链域,除n结点外,每个节点有且仅有一个双亲,所以只会有n-1个结点的链域存放指针,指向非空子女节点。空指针数目为n+1
图2.2.5 二叉树的链式存储结构
template
class BiNode
{
T node;
BiNode * lchild, * rchild; //左右孩子指针
};
三叉链表增加一个指针域,指向结点的双亲。
template
class TriNode
{
T node;
TriNode* lchild, * rchild; //左右孩子指针
TriNode* parent; // 双亲指针
};
遍历定义——顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
访问的含义很广,可以是对结点进行各种处理,如输出结点的信息、修改结点的数据值,但要求这种访问不能破坏原来的数据结构。
遍历目的——得到树中所有节点的一个线性排列
遍历用途——它是树结构插入,删除,修改,查找和排序运算的前提,是二叉树一切运算的基础和核心。
遍历方法——依次遍历二叉树中的三个组成部分(根结点,左子树,右子树),便是遍历了整个二叉树,假设L为遍历左子树,D为访问根节点,R遍历右子树,则遍历方案有:DLR, DRL, RLD, RDL, LDR, LRD6种。若规定先左后右,则只有三种情况:DLR——先序遍历,LDR——中序遍历,LRD——后续遍历。
先序遍历:A B D G C E H F
中序遍历:D G B A E H C F
后序遍历:G D B H E F C A
若二叉树中各结点的值均不相同,则二叉树结点的先序序列,中序序列,后序序列都是唯一的。
由二叉树的先序序列和中序序列,或者由后序序列和中序序列都可以确定唯一一棵二叉树。
例1:已知二叉树的先序和中序遍历,构造出相应的二叉树:先序A B C D E F G H I J,中序 C D B F E A I H G J
分析过程:由先序序列确定根为A,由中序可知左子树为CDBFE,右子树为IHGJ,再分别在左,右,子树的序列中找出根,左子树,右子树序列,以此类推,得到完整的二叉树
图2.7.1 例1中的二叉树示意图
例2:已知二叉树的后序和中序遍历,构造出相应的二叉树:后序 D E C B H G F A,中序 B D C E A F H G
分析过程:后序遍历的最后一个结点A就是根节点,由中序遍历可知,左子树为B D C E,右子树为F H G,在后序中 D E C B中,最后的B为根节点,那么D C E为B的右子树,在D E C中,最后的C为根节点,根据中序里的D C E可知,D为C的左子树,E为C的右子树。
图2.7.2 例2中的二叉树示意图
若二叉树为空,则空操作,若二叉树非空,访问根节点(D),前序遍历左子树(L),前序遍历右子树(R)。
template
void Binary::PreOrder(BiNode* root)
{
if (root == NULL)
{
cout<<"空二叉树" << endl;
return;
}
else
{
visit(root); //访问根结点
PreOrder(root->lchild); // 递归遍历左子树
PreOrder(root->rchild);// 递归遍历右++子树
}
}
若二叉树为空,则空操作,若二叉树非空,中序遍历左子树(L),访问根节点(D),中序遍历右子树(R)。
template
void Binary::InOrder(BiNode* root)
{
if (root == NULL)
{
cout << "空二叉树" << endl;
return;
}
else
{
InOrder(root->lchild); // 递归遍历左子树
visit(root); //访问根结点
InOrder(root->rchild);// 递归遍历右++子树
}
}
若二叉树为空,则空操作,若二叉树非空,后序遍历左子树(L),后序遍历右子树(R),访问根节点(D)。
template
void Binary::PostOrder(BiNode* root)
{
if (root == NULL)
{
cout << "空二叉树" << endl;
return;
}
else
{
PostOrder(root->lchild); // 递归遍历左子树
PostOrder(root->rchild);// 递归遍历右++子树
visit(root); //访问根结点
}
}
若去掉输出语句,从递归的角度看,三种算法是完全相同的,或者说三种算法的访问路径是相同的,只是结点的访问时机不同。第一次经过时访问是先序遍历,第二次经过时访问是中序遍历,第三次经过时访问是后序遍历
中序遍历非递归算法
二叉树中序遍历的非递归算法的关键:在中序遍历过某节点的整个左子树后,如何找到该点的根和右子树。
基本思想:
(1)建立一个栈。
(2)根结点进栈,遍历左子树。
(3)根节点出栈,输出根节点,遍历右子树。
void InOrderTraverse1(BiTree *Tree)
{
std::stack treeStack;//定义一个顺序栈
BiTree *p = NULL;//临时指针
treeStack.push(Tree);
while (!treeStack.empty())
{
//栈内不为空,程序继续运行
while ((p = treeStack.top()) && p)
{
//取栈顶元素,且不能为NULL
treeStack.push(p->lchild);//将该结点的左孩子进栈,如果没有左孩子,NULL进栈
}
treeStack.pop();//跳出循环,栈顶元素肯定为NULL,将NULL弹栈
if (!treeStack.empty())
{
p = treeStack.top();//取栈顶元素
treeStack.pop();//栈顶元素弹栈
cout << p->data << endl;
treeStack.push(p->rchild);//将p指向的结点的右孩子进栈
}
}
}
————————————————
原代码链接:https://blog.csdn.net/yang4344/article/details/107613204
对于一棵二叉树来说,从根节点开始,按照从上到下,从左到右的顺序访问每一个节点。在进行层序遍历时,对某一层的结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问。这样一层一层地进行下去,先访问的结点其左右孩子也要先访问,这与队列的操作原则比较吻合,因此,在进行层序遍历时,可设置一个队列存放已访问的结点。遍历从二叉树的根结点开始,首先将根指针入队,然后从队头取出一个元素,每取一个元素,执行下面的操作。
(1)访问该结点所指指针。
(2)若该结点所指结点的左右孩子结点非空,则将其左孩子指针和右孩子指针入队。
此过程不断进行,当队列为空时,二叉树的层次遍历结束。
template
void LeverOrder(BiNode* root)
{
SeqQueue* Q; //创建循环队列
BiNode* p;
Q->front = Q->rear = 0; //初始化
if (root == Q->rear)
{
return; //若根节点为空,结束遍历
}
else
{
Q[++rear] = root; //根节点入队
while (Q->front!= Q->rear) //当front等于rear时,队列为空,跳出循环
{
DeleteSeqQueue(Q, p);//结点p出队
cout << p->data; //访问结点p
if (!p->lchild = NULL)
EnterSeqQueue(Q, p->lchild);//有左孩子就入队
if (!p->rchild = NULL)
EnterSeqQueue(Q, p->rchild);//有右孩子就入队
}
}
}
按照先序遍历建立二叉树的二叉链表
(1)从键盘获取二叉树的结点信息,建立二叉树的存储结构
(2)在建立二叉树的过程中,按照先序遍历的方式进行建立。
void CreateBinTree()
{
cin >> ch;
if (ch == "#")
{
T=NULL:
}
else
{
if (!(T = new BiNode))
{
T->data = ch; //生成根节点
CreateBinTree(T->lchild); //构造左子树
CreateBinTree(T->rchild); //构造右子树
}
}
}
如果是空树,递归结束,否则申请新节点空间,复制根节点。递归复制左子树,递归复制右子树。
void Copy(BiTree t,BiTree &newt){
if(t==NULL){
newt = NULL; //空树则返回
return;
}
else{
newt = new BiTNode;
newt->data = t->data;
Copy(t->lchild,newt->lchild);
Copy(t->rchild,newt->rchild);
}
当用二叉链表作为二叉树的储存结构时,可以很方便的找到某个结点的左右孩子,但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点。
那么如何寻找遍历序列中二叉树结点的前驱和后继:
解决的方法
1. 通过遍历寻找——费时间
2. 再增设前驱,后继指针——增加储存负担(牺牲空间,换取时间)
3. 利用二叉链表中的空指针域
二叉链表中,空指针域的数量,具有n个结点的二叉链表中,一共有2n个指针域,n个结点,共有n-1个孩子。因为根结点不是任何结点的孩子,即,2n个指针域中,有n-1个用来指示结点的左右孩子,那么就共有n+1个指针域为空。
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱, 如果某个结点的右孩子为空,则将空的右孩子指针域改为指向其后继——这种改变指向的指针就称之为线索。加上了线索的二叉树就称之为线索二叉树(Thread Binary Tree)
对二叉树按照某种遍历次序使其变成线索二叉树的过程叫线索化
对二叉链表中每个结点增设标志域ltag和rtag,并yueding
ltag=0:lchild指向该节点的左孩子 ltag=1:lchild指向该节点的前驱
rtag=0:lchild指向该节点的右孩子 rtag=1:lchild指向该节点的后继
这样,结点的结构为
用C++的结构类型进行描述:
enum flag{Child, Thread};
template
class ThrNode
{
T data;
ThrNode *lchild, *rchild;
flag rtag, ltag;
};
增设一个头节点 ltag=0,lchild指向根节点,rtag=0,rchild指向遍历序列中最后一个结点,遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头节点。