目录
ps:此文章只是为了总结学习数据结构笔记,便于以后忘记查阅,因此部分图片会借用书上的图片,望理解。
(一)二叉树的定义
**二叉树(Binary Tree)是 n (n ≥ 0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别成为根节点的左子树和右子树的二叉树组成。**下图就是一颗二叉树:
1、二叉树的特点
- 每个结点最多有两棵子树,所以二叉树中不会存在度大于 2 的结点。注意不是只有两棵子树,而是最多有。
- 左子树和右子树都是有序的,次序不能任意颠倒。
- 即使树中只有一棵子树,也要区分是左子树还是右子树。
二叉树具有五种基本形态:
- 空二叉树。
- 只有一个根节点。
- 根节点只有左子树。
- 根节点只有右子树。
- 根节点既有左子树又有右子树。
2、特殊二叉树
(1)斜树:所有结点都只有左子树的二叉树叫左斜树,所有结点都只有右子树的二叉树叫右斜树,这两种统称为斜树。
(2)满二叉树:在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,这样的二叉树称为满二叉树,如下图所示:
满二叉树的特点:
- 叶子节点只能出现在最下层,出现在其他层则不平衡。
- 非叶子节点的度一定是二。
- 在同样深度的二叉树中,满二叉树结点个数最多,叶子树最多。
(3)完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为 i (1 ≤ i ≤ n)的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。(看着树的示意图,给每个结点按照满二叉树的结构逐层顺序编号,如果编号出现空挡,说明不是完全二叉树,反之则是)。
完全二叉树的特点:
- 叶子节点只能出现在最下两层
- 最下层的叶子一定集中在左部连续位置。
- 倒数二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
- 同样节点数的二叉树,完全二叉树的深度最小。
(一)二叉树的性质
- 性质一:二叉树的第 i 层上最多有 2^(i-1) 个结点(i>=1)。
- 性质二:深度为 k 的二叉树中,最多有2^k-1个结点,最少有k个结点。
- 性质三:在一棵二叉树中,如果叶子结点的个数为n0,度为 2 的结点个数为n2,则 n0=n2+1。如下图:
结点总数为10,由A、B、C、D等度为 2 的结点,F、G、H、I、J 等度为 0 的叶子节点和 E 这个度为1的结点组成。 - 性质四: 具有n个结点的完全二叉树的深度为⌊log2(n)⌋+1 (⌊ x ⌋表示不大于x的最大整数)。
- 性质四: 对一棵具有 n 个结点的完全二叉树中的结点从 1 开始按层序编号,则对于任意的编号为i(1 < =i <= n)的结点,有:
1.如果i>1,则结点i的双亲编号为⌊i/2⌋;否则结点i是根结点,无双亲。
2.如果2i<=n,则结点i的左孩子的编号为2i;否则结点i无左孩子。
3.如果2i+1<=n,则结点i的右孩子的编号为2i+1;否则结点i无右孩子。
(三)二叉树的存储结构
1、二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组下标能够体现结点逻辑关系。如下面的一棵完全二叉树的存储:
由于二叉树的定义严格,因此用顺序结构可以表现出二叉树的结构,对于一般的二叉树将不存在的结点在数组中设为” ^ "即可但如果极端情况下这是一棵右斜树,只有 k 个结点却要分配 2^k -1 个存储单元,那就会对存储空间造成浪费因此顺序结构一般用于完全二叉树。
2、二叉链表
二叉树的每个结点最多有两个孩子,所以需要为他设计一个数据域和两个指针域,我们称这样的链表叫做二叉链表。如下图所示:
二叉链表的节点结构如下:
typedef char TElemType;
typedef struct BiTNode /* 结点结构 */
{
TElemType data; /* 结点数据 */
struct BiTNode *lchild,*rchild; /* 左右孩子指针 */
}BiTNode,*BiTree;
(四)二叉树的遍历及实现
二叉树的遍历( traversing binary tree ) 是指从根结点出发,按照 某种次序依次访问二叉树中所有结点.使得每个结点被访问一次 旦仅被访问一次。
二叉树的遍历方法:
1、先序遍历:
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树, 再前序遍历右子树。如下图所示遍历的顺序为:ABDGHCEIF。
实现算法:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 前序递归遍历T */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
2、中序遍历:
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点) ,中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 如下图所示,其遍历顺序为:GDHBAEICF。
实现算法:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 中序递归遍历T */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
2、后序遍历:
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如下图所示,其遍历顺序为:GHDBIEFCA。
实现算法:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 后序递归遍历T */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
}
3、层序遍历:
emsp; 规则是若树为空, 则空操作返回,否则从树的第一层,也就是根结点开始访问, 从上而下逐层遍历,在同一层中, 按从左到右的顺序对结点逐个访问。如下图所示,遍历顺序为:ABCDEFGHI。
二叉树遍历的性质:
- 已知前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。
- 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
(五)二叉树的建立
为了让每个结点确认是否有左右孩子,我们需要对其进行扩展,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一个特定值假设为“ # ”,扩展后的二叉树可以做到一个遍历序列确定一颗二叉树,如下图所示其前序遍历序列为AB#D##C##。
如何通过输入前序序列AB#D##C##生成二叉树呢?
示例代码:
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T。 */
void CreateBiTree(BiTree *T)
{
TElemType ch;
/* scanf("%c",&ch); */
ch=str[index++];
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
二叉树的遍历和创建都用到了递归的思想,根据先序、中序、后序的不同来修改相应位置的代码。
(六)搜索二叉树
对于一个有 n 个结点的二叉链表,每个结点都有指向左孩子和右孩子的两个指针域,一共是 2n 个指针域。而 n 个结点的二叉树一共有 n-1 条分支线数,即存在 2n - (n-1)= n+1个空指针域,浪费着内存的资源。而在二叉链表中我们只知道每个结点指向其左右孩子结点的地址,而不只知道该结点的前驱和后继是谁。我们如果需要知道则需遍历一次,结合刚才的问题我们可以考虑在创建二叉树的时候将那些空指针域利用起来存放结点的前驱和后继。我们将这种指向前驱和后继的指针成为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
上图中将二叉树的所用空指针域的 rchild 改成指向该结点的后继, lchild 改成指向该结点的前驱(空心箭头为前驱,黑色箭头为后继),这样的就够相当于将一颗二叉树转变成一个双向链表,着样对插入、删除和查找都带来方便。我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做线索化。
我们如何知道这个节点的 lchild 是它的前驱还是他的左孩子,因此我们还需要改良: 我们在每个结点再增设两个标志域 ltag(为0时指向该结点左孩子,为1时指向该节点前驱) 和 rtag(为0时指向该结点左孩子,为1时指向该节点前驱) 存放 0 或 1 数字的bool型变量。结构如下图所示:
线索二叉树结构实现:
typedef char TElemType;
typedef enum {Link,Thread} PointerTag; /* Link==0表示指向左右孩子指针, */
/* Thread==1表示指向前驱或后继的线索 */
typedef struct BiThrNode /* 二叉线索存储结点结构 */
{
TElemType data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
} BiThrNode, *BiThrTree;
线索化的过程就是在遍历的过程中修改空指针的过程。
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); /* 递归左子树线索化 */
if(!p->lchild) /* 没有左孩子 */
{
p->LTag=Thread; /* 前驱线索 */
p->lchild=pre; /* 左孩子指针指向前驱 */
}
if(!pre->rchild) /* 前驱没有右孩子 */
{
pre->RTag=Thread; /* 后继线索 */
pre->rchild=p; /* 前驱右孩子指针指向后继(当前结点p) */
}
pre=p; /* 保持pre指向p的前驱 */
InThreading(p->rchild); /* 递归右子树线索化 */
}
}
if (!p->lchild) 表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,贼值给了 pre,所以可以将 pre 赋值给 p->lchild,并修改 p->LTag =Tread (也就是定义 为 1) 以完成前驱结点的线索化。
后继就要稍稍麻烦一些。因为此时 p 结点的后继还没有访问到,因此只能对它的 前驱结点 pre 的右指针 rchild 做判断, if (!pre->rchild) 表示如果为空,则 p 就是 pre 的后继,于是 pre->rchild=p,并且设置 pre->RTag=Thread ,完成后继结点的线索化。
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么应采用线索二叉链表的存储结。
(七)树、森林与二叉树的转换
树转换为二叉树:
- 加线:在所有兄弟结点间加一条线。
- 去线:对数中的每个结点只保留与他第一个孩子的连线,删除它与其他孩子结点间的连线。
- 层次调整:以树的根结点为中心,整棵树顺时针旋转一定角度,使层次分明。第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树:
- 把每个树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为 前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后 就得到了由森林转换来的二叉树。
二叉树转换为树:
- 加线:将该结点与这些右孩子结点用线连接起来。
- 去钱。删除原二叉树中所有结点与其右孩子结点的连线。
- 层次调整。使之结构层次分明。
二叉树转换为森林:
- 从根结点开始, 若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连续删除……,直到所有右孩子连线都删除为 止,得到分离的二叉树。
- 再将每棵分离后的二叉树转换为树即可。