【知识框架】
一.树的定义
树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:1.有且仅有一个特定的称为根(Root)的结点;2.当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1.T2.T3...Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree),如图:
强调:1,n>0时根结点是唯一的,不可能存在多个根节点,数据结构 中的树是只能有一个根节点;2.m>0时,子树的个数没有限制,但它们一定是互不相交的。
考虑结点K。根A到结点K的唯一路径上的任意结点,称为结点K的祖先。如结点B是结点K的祖先,而结点K是结点B的子孙。路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子。根A是树中唯一没有双亲的结点。有相同双亲的结点称为兄弟,如结点K和结点L有相同的双亲E,即K和L为兄弟。
树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。如结点B的度为2,结点D的度为3,树的度为3。
度大于0的结点称为分支结点(又称非终端结点);度为0(没有子女结点)的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
结点的深度、高度和层次。
结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推。双亲在同一层的结点互为堂兄弟,图中结点G与E,F,H,I,J互为堂兄弟。
结点的深度是从根结点开始自顶向下逐层累加的。
结点的高度是从叶结点开始自底向上逐层累加的。
树的高度(或深度)是树中结点的最大层数。图中树的高度为4。
有序树和无序树。树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
路径和路径长度。树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
森林。森林是m (m≥0)棵互不相交的树的集合。森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。
二.树的存储结构
1.双亲表示法
假设用一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在数组中的位置;就是说每个结点除了知道自己的位置以外,还知道双亲在哪里;结点结构如图:
data 是数据域,存储结点的数据信息;parent是指针域,存储该结点的双亲在数组中的下标;
双亲表示法的结点结构定义代码:
#define max 100
typedef int TElemType; //树节点的数据类型,目前定为整型
typedef struct PtNode //结点结构
{
TElemType data; //结点数据
int parent; //双亲位置
}PtNode;
typedef struct //树结构
{
PtNode nodes[max]; //结点数组
int r,n; 根的位置和节点数
}Ptree;
2.孩子表示法
实现方法:
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针有组成一个线性表,采用顺序存储结构,存放进一个一维数组中;
为此设计有两种结构,一个是孩子链表的孩子结点
child是数据域,用来存储某个结点在表头数组中的下标;next是指针域,用来存储指向某节点的下一个孩子结点的指针;
另一个是表头数组的表头结点,
data是数据域,存储某节点的数据信息;firstchild是头指针域,存储该节点的孩链表的头指针;
孩子表示法结构定义:
#define max 100
typedef int TEkenType; //树节点的数据类型,目前定位整型
typedeff struct CTNode //孩子结点
{
int child;
struct CTNode* next;
}*childPtr;
typedef struct //表头结构
{
TElemType data;
ChidPtr firstchild;
}CTBox;
typedef struct //树结构
{
CTBox nodes[max]; //结点数组
int r,n; //根的位置和节点数
}CTree;
这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
但是,这也存在着问题,我如何知道某个结点的双亲是谁呢?比较麻烦,需要整棵树遍历才行,难道就不可以把双亲表示法和孩子表示法综合一下吗? 当然是可以,这个读者可自己尝试结合一下;
3.孩子兄弟表示法
任意一棵树,它的结点第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的;因此,我们可以设置两个指针,分别指向该节点的第一个孩子和此结点的右兄弟;
结点结构如图:
data是数据域;firstchild为指针域,存储该节点的第一个孩子结点的存储地址;rightsib为指针域,存储该节点的右兄弟结点的存储地址;
结构定义代码如下
typedef struct CSNode
{
TElemType data;
struct CSNode* firstchild,*rightsib;
}CSNode,*CSTree;
我们可以通过图来看代码实现的结构
然而,这样实现就变成了二叉树;
三,二叉树
1.二叉树的定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两颗互不相交的分别称为根节点的左子树和右子树的二叉树组成;
二叉树抽象函数的定义:
//通过前序遍历的数组“ABD##E#H##CF##G##”构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a,int n,int* pi);
//二叉树的销毁
void BinaryTreeCreate(BTNode** root);
//二叉树的结点个数
int BinaryTreeSize(BTNode* root);
//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树第k层结点个数
int BinaryTreeLevelkSize(BTNode* root,int k);
//二叉树查找值为x的结点
BTNode*BinaryTreeFind(BTNode* root,BTDataType x);
//二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
//二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
//二叉树后续遍历
void BinaryTreePostOrder(BTNode* root);
//层次遍历
void BinaryTreeLevelOrder(BTNode* root);
//判断二叉树是否为完全二叉树
int BinaryTreeComplete(BTNode* root);
2,二叉树的特点
1.每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2,左子树和右子树是有顺序的,次序不可以任意颠倒。
3,即使树中某节点只有一颗子树,也要区分它是左子树还是右子树。
其有五种形态
3、几个特殊的二叉树
(1)斜树
所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
(2)满二叉树
在一课二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树;
(3)完全二叉树
对一颗具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同伴深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这颗二叉树称为完全二叉树;
(4)二叉排序树
左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
(5)平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过1。
4,二叉树的性质
1,在二叉树的第i层至多有2*(i-1)个结点(i>=1);
2,深度为k的二叉树至多有2*k-1个结点(k>=1);
3,对任何一颗二叉树T,如果其终端结点树(叶节点)为n0,度为2的结点数,则n0=n2+1;
4具有n个结点的完全二叉树的深度为
5、二叉树的存储结构
(1)顺序存储结构
二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i ii的结点元素存储在一维数组下标为i − 1 i-1i−1的分量中。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
但对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。然而,在最坏情况下,一个高度为h hh且只有h hh个结点的单支树却需要占据近2 h − 1 2h-12h−1个存储单元。二叉树的顺序存储结构如图所示,其中0表示并不存在的空结点。
6.二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,这样的链表为二叉链表。
data是数据域;lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针;
二叉链表的结点结构定义代码
typedef struct BiTNode //结点结构
{
TElemType data; //结点数据
struct BiTNod* lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
7.遍历二叉树
二叉树的遍历原理:
二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历方法
二叉树的定义是用递归的方式,所以实现遍历算法也可以采用递归;
前序遍历
规则是若二叉树为空,则空操作返回,
否则先访问根节点,
然后前序遍历左子树,
再前序遍历右子树;
代码实现
///初始条件:二叉树存在
//操作结果:前序递归遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
PrevOrder(root->left); //先序遍历左子树
PrevOrder(root->right); //先序遍历右子树
}
运行代码与结果
#include<stdio.h>
#include<stdlib.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;//或者写BinaryTreeNode
//初始条件:二叉树存在
//操作结果:前序递归遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
PrevOrder(root->left); //先序遍历左子树
PrevOrder(root->right); //先序遍历右子树
}
BTNode* CreateNode(int x) //构造二叉树
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));//申请新节点,可以申请结点数,相当于数组
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
int main(void)
{
BTNode* A = CreateNode('A');//调用函数CreateNode
BTNode* B = CreateNode('B');
BTNode* C = CreateNode('C');
BTNode* D = CreateNode('D');
BTNode* E = CreateNode('E');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
PrevOrder(A);
printf("\n");
// 中序遍历
//InOrder(A);
//printf("\n");
//后序遍历
//PostOrder(A);
//printf("\n");
getchar();//获取字符
return 0;
}
过程图解
递归调用次数及代码结果
#include<stdio.h>
#include<stdlib.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;//或者写BinaryTreeNode
//初始条件:二叉树存在
//操作结果:前序递归遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
PrevOrder(root->left); //先序遍历左子树
PrevOrder(root->right); //先序遍历右子树
}
BTNode* CreateNode(int x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));//申请新节点
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
//递归次数使用的函数
int TreeSize(BTNode* root, int* psize)
{
if (root == NULL)
{
//printf("NULL ");
return;
}
else
(*psize)++;
TreeSize(root->left, psize);
TreeSize(root->right, psize);
return psize;
}
int main(void)
{
BTNode* A = CreateNode('A');//调用函数CreateNode
BTNode* B = CreateNode('B');
BTNode* C = CreateNode('C');
BTNode* D = CreateNode('D');
BTNode* E = CreateNode('E');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
PrevOrder(A);
printf("\n");
int sizea = 0;
TreeSize(A, &sizea);
printf("TreeSize:%d\n", sizea);
// 中序遍历
//InOrder(A);
//printf("\n");
//后序遍历
//PostOrder(A);
//printf("\n");
getchar();//获取字符
return 0;
}
求叶子结点的个数
#include<stdio.h>
#include<stdlib.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;//或者写BinaryTreeNode
//初始条件:二叉树存在
//操作结果:前序递归遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
PrevOrder(root->left); //先序遍历左子树
PrevOrder(root->right); //先序遍历右子树
}
BTNode* CreateNode(int x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));//申请新节点
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
int TreeLeafSize(BTNode* root)
{
if(root==NULL)
return 0;
if(root->left==NULL&&root->right==NULL)
return 1;
return TreeLeafSize(root->left)+TreeLeafSize(root->right);
}
int main(void)
{
BTNode* A = CreateNode('A');//调用函数CreateNode
BTNode* B = CreateNode('B');
BTNode* C = CreateNode('C');
BTNode* D = CreateNode('D');
BTNode* E = CreateNode('E');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
PrevOrder(A);
printf("\n");
int sizea = 0;
TreeSize(A, &sizea);
printf("TreeSize:%d\n", sizea);
// 中序遍历
//InOrder(A);
//printf("\n");
//后序遍历
//PostOrder(A);
//printf("\n");
getchar();//获取字符
return 0;
}
中序遍历
规则是若二叉树为空,则空操作返回,否则从根节点开始(注意并不是先访问根节点),
中序遍历根节点的左子树,
然后访问根节点,
最后中序遍历右子树。
代码实现
//初始条件:二叉树存在
//操作结果:中序递归遍历root
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left); //先序遍历左子树
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
InOrder(root->right); //先序遍历右子树
}
后序遍历
规则是若二叉树为空,则空操作返回,
否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根节点;
代码实现
//初始条件:二叉树存在
//操作结果:后序递归遍历root
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left); //先序遍历左子树
PostOrder(root->right); //先序遍历右子树
printf("%c ", root->data);//访问根节点,显示结点数据,可以更改为其他对结点的操作
}
层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问;
void LevelOrder(BiTree root){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, root); //将根节点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q, p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL){
EnQueue(Q, p->lchild); //左子树不空,则左子树根节点入队
}
if(p->rchild != NULL){
EnQueue(Q, p->rchild); //右子树不空,则右子树根节点入队
}
}
}
非递归实现二叉树遍历
非递归后序遍历算法
后序遍历的非递归实现是三种遍历方法中最难的。因为在后序遍历中,要保证左孩了和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难题。
算法思想:后序非递归遍历二叉树是先访问左子树,再访问右子树,最后访问根结点。
沿着根的左孩子,依次入栈,直到左孩子为空。此时栈内元素依次为ABD。
读栈顶元素:若其右孩子不空且未被访问过,将右子树转执行①;否则,栈顶元素出栈并访问。
栈顶D的右孩子为空,出栈并访问,它是后序序列的第一个结点;栈顶B的右孩子不空且未被访问过,E入栈,栈顶E的左右孩子均为空,出栈并访问;栈顶B的右孩子不空但已被访问,B出栈并访问;栈项A的右孩子不空且未被访问过,C入栈,栈项C的左右孩子均为空,出栈并访问;栈顶A的右孩子不空但已被访问,A出栈并访问。由此得到后序序列DEBCA。
在上述思想的第②步中,必须分清返回时是从左子树返回的还是从右子树返回的,因此设定一个辅助指针r,指向最近访问过的结点。也可在结点中增加一个标志域,记录是否已被访问。
后序遍历的非递归算法如下:
void PostOrder2(BiTree T){
InitStack(S);
p = T;
r = NULL;
while(p || !IsEmpty(S)){
if(p){ //走到最左边
push(S, p);
p = p->lchild;
}else{ //向右
GetTop(S, p); //读栈顶元素(非出栈)
//若右子树存在,且未被访问过
if(p->rchild && p->rchild != r){
p = p->rchild; //转向右
push(S, p); //压入栈
p = p->lchild; //再走到最左
}else{ //否则,弹出结点并访问
pop(S, p); //将结点弹出
visit(p->data); //访问该结点
r = p; //记录最近访问过的结点
p = NULL;
}
}
}
}
非递归中序遍历算法
借助栈,我们来分析中序遍历的访问过程:
沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点,此时栈内元素依次为ABD。
栈顶元素出栈并访问:若其右孩子为空,继续执行步骤2;若其右孩子不空,将右子树转执行步骤1。
栈顶D出栈并访问,它是中序序列的第一个结点; D右孩子为空,栈顶B出栈并访问; B右孩子不空,将其右孩子E入栈,E左孩子为空,栈顶E出栈并访问; E右孩子为空,栈顶A出栈并访问; A右孩子不空,将其右孩子C入栈,C左孩子为空,栈顶C出栈并访问。由此得到中序序列DBEAC。
实现代码
void InOrder2(BiTree T){
InitStack(S); //初始化栈S
BiTree p = T; //p是遍历指针
while(p || !IsEmpty(S)){ //栈不空或p不空时循环
if(p){
Push(S, p); //当前节点入栈
p = p->lchild; //左孩子不空,一直向左走
}else{
Pop(S, p); //栈顶元素出栈
visit(p); //访问出栈结点
p = p->rchild; //向右子树走,p赋值为当前结点的右孩子
}
}
}
非递归前序遍历算法
void PreOrder2(BiTree T){
InitStack(S); //初始化栈S
BiTree p = T; //p是遍历指针
while(p || !IsEmpty(S)){ //栈不空或p不空时循环
if(p){
visit(p); //访问出栈结点
Push(S, p); //当前节点入栈
p = p->lchild; //左孩子不空,一直向左走
}else{
Pop(S, p); //栈顶元素出栈
p = p->rchild; //向右子树走,p赋值为当前结点的右孩子
}
}
}
8.堆
概念及结构
如果有一个关键码的集合K=(k0,k1,k2,...,kn-1),把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且满足:ki<=k2i+1且ki<+k2i+2(ki>=k2i+1且ki>=k2i+2)i=0,1,2,3...,则称为小堆或打堆;
性质:堆中某个节点的值总是不大于或不小于其父结点的值;堆总是一颗完全二叉树;
大堆特点:完全二叉树;每个父亲都大于等于孩子,堆顶为最大值;
小堆特点:完全二叉树;每个父亲都小于等于孩子,堆顶为最小值;
堆的逻辑结构为完全二叉树;物理结构为数组,如果父亲的下标为i,则左孩子的下标为2*i+;右孩子下标为2*i+2;
堆作用
1.堆排序;
2.TOPK(N个数中找出最大的或者最小的前K个数);方法1.排序,用排序的弊端,时间复杂度NlogN效率不高;假设N非常大,无法内排序;
方法2,建立K个数来解决,建立小堆;
堆排序
构造小堆方法
void HeapInit(HPDataType* a, int n)
{
//构建堆
int bi = 0;
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {//从最后一个结点调求父亲的下标,最后一个结点下标为n-1,根据公式求父亲下标
AdjustDown(a,i,bi);//一个算法
}
}
向下调整算法
按照完全二叉树的逻辑结构,从最后一个节点的父节点开始,以该节点下标为起始,与自己的两个孩子节点(可能为一个孩子节点)比较大小并根据情况交换位置(大堆比较更大的孩子节点,小堆比较更小的孩子节点),一直调整到根节点,最终得到大堆/小堆
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int AdjustDown(HPDataType* a, int n, int root)
{
int parent = root;//将父亲结点定为root
int child = parent * 2 + 1;//默认孩子为左孩子下标
//循环停止条件child下标大于数组大小n
while (child < n)
{
//如果右孩子的值小于左孩子,则child下标加加,否则不变
if(child+1<n&&a[child + 1] < a[child])
{
child++;
}
//如果孩子结点小于父亲结点的值,则交换.交换后孩子和父亲的下标变为新的孩子和父亲的下标
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
//如果排序结束退出循环
else
{
break;
}
}
}
代码及测试用例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef int HPDataType;
typedef struct Heap {//动态定义数组
HPDataType* _a;
int _size;
int _capacipy;
}Heap;
//定义堆
void HeapInit(HPDataType* a, int n)
{
//构建堆
int bi = 0;
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {//从最后一个结点调求父亲的下标,最后一个结点下标为n-1,根据公式求父亲下标
AdjustDown(a,i,bi);//一个算法
}
}
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int AdjustDown(HPDataType* a, int n, int root)
{
int parent = root;//将父亲结点定为root
int child = parent * 2 + 1;//默认孩子为左孩子下标
//循环停止条件child下标大于数组大小n
while (child < n)
{
//如果右孩子的值小于左孩子,则child下标加加,否则不变
if(child+1<n&&a[child + 1] < a[child])
{
child++;
}
//如果孩子结点小于父亲结点的值,则交换.交换后孩子和父亲的下标变为新的孩子和父亲的下标
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
//如果排序结束退出循环
else
{
break;
}
}
}
int main()
{
int a[] = { 27,15,19,18,28,34,65,49,25,37 };
Heap hp;
HeapInit(a,sizeof(a) / sizeof(HPDataType));//传值过去,()(数组)(数组长度)
for (int d = 0; d < sizeof(a) / sizeof(int); ++d)
{
printf("%d", a[d]);
printf("\n");
}
return 0;
}
9.线索二叉树()
1.线索二叉树原理
遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个结点除外)都有一个直接前驱和直接后继。
传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱或后继。
首先我们要来看看这空指针有多少个呢?对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1 条分支线数,也就是说,其实是存在2n- (n-1) =n+1个空指针域。
由此设想能否利用这些空指针来存放指向其前驱或后继的指针?这样就可以像遍历单链表那样方便地遍历二叉树。引入线索二叉树正是为了加快查找结点前驱和后继的速度。
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
其结点结构如下所示:
2、线索二叉树的结构实现
二叉树的线索存储结构代码如下:
typedef struct ThreadNode{
ElemType data; //数据元素
struct ThreadNode *lchild, *rchild; //左、右孩子指针
int ltag, rtag; //左、右线索标志
}ThreadNode, *ThreadTree;
3、二叉树的线索化
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树,线索化的过程就是在遍历的过程中修改空指针的过程。
10、树、森林与二叉树的转化
在讲树的存储结构时,我们提到了树的孩子兄弟法可以将一棵树用二叉链表进行存储,所以借助二叉链表,树和二叉树可以相互进行转换。从物理结构来看,它们的二叉链表也是相同的,只是解释不太一样而已。 因此,只要我们设定一定的规则,用二叉树来表示树,甚至表示森林都是可以的,森林与二叉树也可以互相进行转换。
1、树转换为二叉树
树转换为二义树的规则:每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,这个规则又称“左孩子右兄弟”。由于根结点没有兄弟,所以对应的二叉树没有右子树。
树转换成二叉树的画法:
在兄弟结点之间加一连线;
对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;
以树根为轴心,顺时针旋转45°。
2、森林转化为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。
森林转换成二叉树的画法:
将森林中的每棵树转换成相应的二叉树;
每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线;
以第一棵树的根为轴心顺时针旋转45°。
11、哈夫曼树和哈夫曼编码
1、哈夫曼树的定义和原理
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度
2、哈夫曼树的构造
先把有权值的叶子结点按照从大到小(从小到大也可以)的顺序排列成一个有序序列。
取最后两个最小权值的结点作为一个新节点的两个子结点,注意相对较小的是左孩子。
用第2步构造的新结点替掉它的两个子节点,插入有序序列中,保持从大到小排列。
重复步骤2到步骤3,直到根节点出现。