1.树的定义
树的定义:根在上,枝叶在下,树由结点构成
树可以为空集,对于非空树而言,至少有一个根结点,根结点没有前驱结点,其余结点都有唯一的前驱结点
节点的度:一个节点含有的子树个数
叶节点或终端节点:度为0的节点
非终端节点或分支节点:度不为0的节点
双亲结点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
孩子节点或子节点:一个节点还有的子树的根节点
兄弟节点:具有相同父节点的节点
树的度:一棵树中最大节点的度为树的度
节点的层次:从根开始定义,根为第一层,根的子节点为第二层
树的高度或深度:树中节点的最大层次
节点的祖先:从根到该节点所经分支上的所有节点
子孙:以某节点为根的子树中任一节点都为该节点的子孙
森林:有m棵互不相交的多棵树的集合森林
如果一棵树中各个节点的子树次序不重要,可以交换位置,这样的树称为无序树
如果将树中结点的各棵子树看成是从左到右有次序的,这样的树为有序树,从左到右依次为第一子树,第二子树。
2.树的表示
顺序存储表示:
按照满二叉树的节点层次编号,依次存放二叉树中的数据元素
缺点:大小固定,浪费空间比较多
#define MAXTSIZE 100
typedef BTDataType SqBiTree[MAXTSIZE];
SqBiTree bt;//二叉树顺序储存表示
c语言表示:
struct TreeNode
{
int data;
struct TreeNode* child1;
struct TreeNode* child2;
....
};
c++表示:
struct TreeNode
{
int data;
vector<struct TreeNode*>childs;
};
c++用STL的vector容器可以实现动态增容
左孩子右兄弟表示:
不论有多少个子结点
typedef int DataType;
struct Node
{
struct Node* _firstChild1;//第一个孩子结点1
struct Node* _pNextBrother;//指向其下一个兄弟结点
DataType _data;//结点中的数据域
};
每个指针只指向第一个孩子,剩余用兄弟表示,当没有兄弟时,右兄弟指针指向NULL
双亲表示法:
将所有数据储存在一个数组当中,数组的下标指向父亲,只存储父亲的下标
便于涉及双亲的操作,求结点的孩子时需要遍历整棵树
3.二叉树概念及结构
二叉树:度不超过2的树,每个结点最多有两棵子树,即二叉树不存在度大于2的结点
二叉树的子树有左右之分,子树的次序不能颠倒;二叉树可以是空集合,有空的左子树或者右子树
二叉树不是树的特殊情况,二叉树即使只有一颗子树也要区分他是左子树还是右子树。树当节点只有一个孩子时,无需区分左右次序。(也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了),
二叉链表示:
//二叉链
struct BinTreeNode
{
struct BinTreeNode* pLeft;
struct BinTreeNode* pRight;
BTDataType _data;
};
普通二叉树都设置为二叉链
在n个节点的二叉链表中,有n+1个空指针域。n个节点一定有2n个指针域,除根节点之外,每个节点有且仅有一个双亲,所以只会有n-1个节点的指针域存放指针
三叉链表示:
//三叉链
struct BinaryTreeNode
{
struct BinTreeNode* pParent;//指向当前结点的父亲
struct BinTreeNode* pLift;//指向当前结点左孩子
struct BinTreeNode* pRight;//指向当前结点右孩子
BTDataType _data;
};
后续学习到红黑树或者AVL树会使用三叉链
3.1特殊的二叉树
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。即一个二叉树层数为h,且总结点数为(2^h)-1,则其为满二叉树
2^0+2^1+2^2+……+2^(h-1)=N
完全二叉树:一颗二叉树中,只有最下面两层结点的度可以小于2,并且最下层的叶结点集中在靠左的若干位置上
假设树的高度为h:①前h-1层都是满的②最后一层不满,但是最后一层从左往右都是连续的
扩充二叉树:不存在度为1的结点的二叉树称为扩充二叉树,也叫2-树
3.2二叉树的性质
①若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个结点
②若规定根节点的层数为1,则深度为h的二叉树的最大结点数为2^h-1
③对任何一颗二叉树,如果度为0,其叶结点个数为n0,度为2分支结点个数为n2,则有n0=n2+1,即度为0的节点个数比度为2的结点个数多1.
④若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log2(N-1)
⑤具有n个结点的完全二叉树的深度为(
⑥如果对一颗有n个结点的完全二叉树(深度为log2(n)+1)的结点按层序编号,则对任一结点i有
(1)如果i=1,则结点i为二叉树的根,无双亲;如果i>1,则其双亲是结点i/2
(2)如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i
(3)如果2i+1>n,则结点i无右孩子结点;否则,其右孩子是节点2i+1
例题:
1.某二叉树共有399个结点,其中199个度为2的结点,则该二叉树中叶子结点数为(B)
A.不存在这样的二叉树 B.200 C.198 D.199
2.在具有2n个结点的完全二叉树中,叶子结点的个数为(A)
A.n B.n+1 C.n-1 D.n/2
解:
假设度为0的有X0个,度为1的有X1个,度为2的有X2个
X0+X1+X2=2n
X0=X2+1,又因为完全二叉树度为1的最多只有一个,所以X1=1或0
X0+X1+X0-1=2n,通过算式可以得知X1一定为1,如果为0计算结果会出现小数
所以2X0=2n,X0=n
3.一颗完全二叉树的节点数为531个,那么这棵树的高度为(B)
A.11 B.10 C.8 D.12
解:
X0=532/2=266,所以度为0的有266个,度为1的有0个,度为2的有265个
设X为最后一层缺失X个元素,2^h-1-X=531,X范围为[0,2^(h-1)-1].由于X的取值不确定,所以这里将答案依次带入计算尝试。
当h=10时,X最大为512-1=511,2^h-1-X=531可以满足
log2(531)=9.05257,log2(531)+1≈10
4.二叉树的遍历
遍历定义——顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。“访问”的含义很广,可以是对结点作各种处理,如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。
4.1先序,中序,后序遍历
分为先序遍历(VLR),中序遍历(LVR),后序遍历(LRV)
二叉树的先序和中序,中序和后序可以确定唯一一颗二叉树
递归算法
先序递归遍历:如果二叉树为空,则空操作
否则①访问根结点②先序遍历左子树③先序遍历右子树
如上图所示二叉树,其中DEF的子节点为NULL
所以采用先序遍历时,遍历次序为:A,B,D,NULL,NULL,E,NULL,NULL,C,F,NULL,NULL
void PrevOrder(BTNode* root)
{
if (root==NULL)
{
return;
}
printf("%d",root->data);//根结点
PrevOrder(root->left);//左子树
PrevOrder(root->right);//右子树
}
先序遍历递归函数的执行过程:
中序递归遍历:如果二叉树为NULL,则空操作
否则①中序遍历左子树②访问根节点③中序遍历右子树
如上图所示二叉树,其中DEF的子节点为NULL
所以采用中序遍历时,遍历次序为:NULL,D,NULL,B,NULL,E,NULL,A,NULL,C,NULL,F,NULL
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d", root->data);
InOrder(root->right);
}
后序递归遍历:如果二叉树为NULL,则空操作
否则①后序遍历左子树②后序遍历右子树③访问根节点
所以采用后序遍历时,遍历次序为:NULL,NULL,D,NULL,NULL,E,B,NULL,NULL,NULL,F,C,A
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d", root->data);
}
1.某完全二叉树按层次输出(同一层从左到右)的序列为ABCDEFGH,则该完全二叉树的前序序列为()
A.ABDHECFG
B.ABCDEFGH
C.HDBEAFCG
D.HDEBFGCA
2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG,则二叉树根节点为()
A.E
B.F
C.G
D.H
3.设一颗二叉树的中序遍历序列:badce,后序遍历序列:bdeca。则二叉树的前序遍历序列为()
A.adbce
B.decab
C.debac
D.abcde
答案:A A D
非递归算法
递归是程序设计中强有力的工具。递归函数结构清晰,使程序易读。但递归函数也有不可克服的弱点,时间、空间效率较低,运行代价较高,所以在实际使用中,常希望使用它的迭代版本。为了实现非递归遍历算法,需要一个堆栈,作为实现算法的辅助数据结构。堆栈用于存放遍历过程中待处理的任务线索。二叉树是非线性数据结构,遍历过程中访问的每一个结点都有左、右两棵子树,任何时刻程序只能访问其中之一,程序必须保留以后继续访问另一棵子树的“线索”,我们用堆栈来存放这种线索。二叉树遍历的递归算法虽然没有用户定义的栈,但是系统栈承担了此项任务。
中序非递归遍历,基本思想:①建立一个栈②根节点进栈,遍历左子树③根节点出栈,输出根节点,遍历右子树。当栈内为空时,遍历结束。
void MidOrder(BTNode* t) //t为根指针
{
struct BTNode* st[STACKSIZE];//定义指针栈
int top = 0; //置空栈
do {
while (t) //根指针t表示的为非空二叉树
{
if (top == STACKSIZE)
{
exit(-1);//栈已满,退出
}
st[top] = t; //根指针进栈
top++;
t = t->left;//t移向左子树
} //循环结束表示以栈顶元素的指向为根结点的二叉树
//的左子树遍历结束
if (top) //为非空栈
{
top--;
t = st[top]; //弹出根指针
printf("%c", t->data); //访问根结点
t = t->right; //遍历右子树
}
} while (top || t); //父结点未访问,或右子树未遍历
}
4.2层序遍历
层序遍历:
实际上二叉树的前序、中序、后序遍历都属于二叉树的深度优先遍历。
除了先序遍历、中序遍历、后序遍历之外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的根节点,然后从左到右访问第二层上的节点,接着是从左到右访问第三层的节点,以此类推,自上而下,从左到右逐层访问树的结点的过程就是层序遍历,层序遍历为二叉树的广度优先遍历。
采用队列先进先出的特性来实现层序遍历,上一层带下一层的核心思路。
当队列为NULL时遍历就结束了,一层一层的遍历,有时候队列中会同时出现两层的结点
//二叉树的层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//获取队头元素
QueuePop(&q);
printf("%c", front->data);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestory(&q);
}
不论是先序,中序,后序还是层次遍历,时间复杂度都为O(n),n为树结点的个数,空间复杂度最坏情况下为单支,为O(n)同理,有了计算节点个数的前例,可以自己写出类似的计算叶节点的递归函数
5.二叉树的基本运算
头文件:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include"Queue.h"
typedef char BTNDataType;
typedef struct BinaryTreeNode//设定二叉树中的结点结构体
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTNDataType data;
}BTNode;
typedef struct BinaryTree//设定一个二叉树结构体
{
BTNode* root;
}BT;
void TreeCreate(BT* B1);//创建一棵空二叉树B1
BTNode* NewNode(BTNDataType x, BTNode* ln, BTNode* rn);//创建一个新结点,数据域为x,左孩子为ln,右孩子为rn
BTNDataType TreeRoot(BT* B1);//用于获取树的根节点储存数据
void TreeMake(BT* B1, BTNDataType x, BT* lt, BT* rt);//创建一个树B1,数据域为x,左子树为lt,右子树为rt
void NodeDestory(BTNode* root);
void TreeDestory(BT* B1);
//前中后序遍历
void PrevOrder(BTNode* root);
void InOrder(BTNode* root);
void PostOrder(BTNode* root);
void LevelOrder(BTNode* root);//层序遍历
int TreeSize(BTNode* root);//结点个数
int TreeLeafSize(BTNode* root);//叶子结点个数
int TreeKLevelSize(BTNode* root,int k);//第k层节点个数
BTNode* TreeFind(BTNode* root, BTNDataType x);//查找值为x的结点
bool IfTreeComplete(BTNode* root);//判断是否是完全二叉树
int maxDepth(BTNode* root);
队列头文件:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
struct BinaryTreeNode;//加一个前置声明,使其找到二叉树结构体
typedef struct BinaryTreeNode* QDataType;
//typedef BTNode* QDataType;
//为了实现树的广度优先遍历,需要用栈保存树的结点数据
//由于在队列的实现中,将Queue.h头文件放至顶部,在头文件展开的过程中无法找到BTNode的定义
//这时候就产生了相互依赖,队列中需要保存树的结点,而树又要需要队列来解决广度优先遍历
typedef struct QueueNode//创建一个链表
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue//创建一个队列
{
QNode* head;//指向队头
QNode* tail;//指向队尾
}Queue;
void QueueInit(Queue* Q1);
void QueueDestory(Queue* Q1);
void QueuePush(Queue* Q1,QDataType x);//队尾入
void QueuePop(Queue* Q1);//队头出
QDataType QueueFront(Queue* Q1);
QDataType QueueBack(Queue* Q1);
int QueueSize(Queue* Q1);
bool QueueEmpty(Queue* Q1);
函数TreeMak有三个参数:数据元素x,两个二叉树结构体对象lt和rt。lt->root和rt->root分别指向二叉树lt和rt的根节点。使lt和rt成为根节点B1的左,右子树,同时必须使lt与rt自身称为空二叉树。
如果不使lt,rt称为空树,lt与rt将会与新二叉树共享二叉链结点。这种共享结点的现象是十分危险的。一旦应用程序删除或修改了lt和rt所包含的二叉树,而这种修改不是新二叉树所希望的,就会造成混乱。
销毁树需要采用后序遍历的方式,先销毁左子树,再销毁右子树,最后销毁根节点
基础运算函数实现:
#include"Tree.h"
void TreeCreate(BT* B1)
{
B1->root = NULL;
}
BTNode* NewNode(BTNDataType x,BTNode* ln,BTNode* rn)
{
BTNode* p = (BTNode*)malloc(sizeof(BTNode));
p->data = x;
p->left = ln;
p->right = rn;
return p;
}
BTNDataType TreeRoot(BT* B1)
{
if (B1->root)
{
return B1->root->data;//树的根节点的数据域
}
else
{
printf("树为空\n");
exit(-1);
}
}
void TreeMake(BT* B1, BTNDataType x,BT* lt,BT* rt)//传入树B1,数据x,左子树lt,右子树rt
{
if (B1->root||lt==rt)//如果rt==lt,则左右子树共享结点
{
return;
}
B1->root = NewNode(x, lt->root, rt->root);
lt->root = rt->root = NULL;
}
void NodeDestory(BTNode* root)
{
if (!root)
{
return;
}
NodeDestory(root->left);
NodeDestory(root->right);
free(root);
}
void TreeDestory(BT* B1)
{
NodeDestory(B1->root);
}
void PrevOrder(BTNode* root)//先序遍历
{
if (root == NULL)
{
return;
}
printf("%c ", root->data);//根结点
PrevOrder(root->left);//左子树
PrevOrder(root->right);//右子树
}
void InOrder(BTNode* root)//中序遍历
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root)//后序遍历
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d", root->data);
}
//二叉树的层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//获取队头元素
QueuePop(&q);
printf("%c", front->data);
//这里必须先左子树后右子树,从而实现从上向下,从左到右的层序遍历
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestory(&q);
}
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
int TreeLeafSize(BTNode* root)
{
if (root==NULL)
{
return 0;
}
else
{
if (root->left == NULL && root->right == NULL)
{
return 1;
}
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
//计算第k层结点数
int TreeKLevelSize(BTNode* root,int k)
{
if (root==NULL)
{
return 0;
}
if (k==1)
{
return 1;
}
return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
}
BTNode* TreeFind(BTNode* root, BTNDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* lret, * rret;
//先找左树
lret = TreeFind(root->left, x);
if (lret)
{
return lret;
}
//再找右树
rret = TreeFind(root->right, x);
if (rret)
{
return rret;
}
return NULL;
}
//int main()
//{
// BT a, b, x, y, z;
// TreeCreate(&a);
// TreeCreate(&b);
// TreeCreate(&x);
// TreeCreate(&y);
// TreeCreate(&z);
// TreeMake(&y, 'E', &a, &b);
// TreeMake(&z, 'F', &a, &b);
// TreeMake(&x, 'C', &y, &z);
// TreeMake(&y, 'D', &a, &b);
// TreeMake(&z, 'B', &y, &x);
// PrevOrder((&z)->root);
// TreeDestory(&z);
// system("pause");
// return 0;
//}
拓展运算函数实现:
#include"Tree.h"
//判断是否是完全二叉树
bool IfTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//第一次遇到空的时候直接结束循环,进行判断
if (front==NULL)
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//完全二叉树的判断条件是,当层序遍历至第一次遇到空节点时
//之后的所有结点都必须为空
while (!QueueEmpty(&q))
{//循环遍历队列中的所有元素,如果有不是空的结点,则不是完全二叉树
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front!=NULL)
{
QueueDestory(&q);
return false;
}
}
QueueDestory(&q);
return true;
}
//计算二叉树最大深度
int maxDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftdeep = maxDepth(root->left);
int rightdeep = maxDepth(root->right);
return leftdeep > rightdeep ? leftdeep + 1 : rightdeep + 1;
}
6.线索二叉树
为了寻找特定遍历序列中二叉树结点的前驱和后继:①通过遍历寻找--浪费时间②增设前驱、后继指针域--增加了存储负担③利用二叉链中的空指针域
对于一个有n个结点的二叉链表,总共有2n个指针域。n个结点有n-1个孩子,也就是有n-1个指向孩子的指针域和n+1个空指针域
利用二叉链表中的空指针域:如果某个结点的左孩子为空,那么将空的左孩子指针域改为指向前驱;如果某个结点的右孩子为空,则将空的右孩子指针域改为指向后继。这些改变指向的指针称为“线索”
线索二叉树由于指针域指向的改变,为了辨别左右指针域指向孩子还是为线索。在二叉链表的结点结构中增加lTag和rTag两个标志域。Tag为0则指向孩子,Tag为1则指向前驱后继。 lTag为1,则lchild指向前驱结点;rTag为1,则rchild指向后继结点
lTag | lchild | data | rchild | rTag |
0 | B | A | NULL | 1 |
0 | C | B | D | 0 |
1 | NULL | C | 指向B | 1 |
0 | E | D | F | 0 |
1 | 指向B | E | G | 0 |
1 | 指向D | F | 指向A | 1 |
1 | 指向E | G | 指向D | 1 |
最终中序线索二叉树的结构为下图,红色为原本二叉树结构,蓝色为线索,线索的前驱后继指向从中序序列得到
PS:对于所有的先序,中序,后序序列,都有第一个元素没有前驱,最后一个元素没有后继,如果使其默认为NULL是可以的。但为了避免悬空态,可以增设一个头节点。头节点同样具有五个成员,数据域置空。遍历序列中第一个结点的lchild域和最后一个结点的rchild域都指向头节点,从而避免出现NULL。
增设一个头结点:
ltag=0,Ichild指向根结点,
rtag=1,rchild指向遍历序列中最后一个结点
遍历序列中第一个结点的lchild域和最后一个结点的rchild域都指向头节点
7.树,森林与二叉树的转换
7.1树与二叉树的转换
树与二叉树都可以用二叉链表作存储结构,则以二叉链表作为媒介,可以找出树与二叉树之间的关系。
给定一棵树,可以找到唯一的一颗二叉树与之对应。
树转换为二叉树(兄弟相连留长子):
①加线:在兄弟之间加一连线
②抹线:对每个结点,除了其左孩子之外,去除与其他孩子值之间的关系
③旋转:以树的根节点为轴心,将树顺时针旋转45°
二叉树转换为树(左孩右右连双亲,去掉原来右孩线):
①加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子...…沿分支找到的所有右孩子,都与p的双亲用线连起来
②抹线:抹掉原二叉树中双亲与右孩子之间的连线
③调整:将结点按层次排列,形成树结构
7.2森林与二叉树的转换
森林转换成二叉树(树变二叉根相连):
①将各棵树分别转换成二叉树
②将每棵树的根结点用线相连
③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
二叉树转换成森林(去掉全部右孩线,孤立二叉再还原):
①抹线:将二叉树中根节点与其右孩子的连线,以及沿着右分支搜索到的所有右孩子联系全部抹掉,使其变成孤立的二叉树
②还原:将孤立的二叉树还原成树
二叉树转换成森林示意图为上图顺序倒转
8.树和森林的遍历
8.1树的遍历
树的遍历只有三种方式,没有中根遍历是因为一个根结点可以有多个孩子结点,无法规定根结点在中间的位置
先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
层次遍历:若树不空,则自上而下自左至右访问树中每个结点。
8.2森林的遍历
将森林看作由三部分构成:1.森林中第一棵树的根节点2.森林中第一棵树的子树森林3.森林中其他树构成的森林
先序遍历:如果森林树为NULL,则空操作
否则①访问森林中第一棵树的根节点②先序遍历森林中第一棵树的的子树森林③先序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行先根遍历。
中序遍历:如果森林树为NULL,则空操作
否则①中序遍历森林中第一棵树的的子树森林②访问森林中第一棵树的根节点③中序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行后根遍历。
后序遍历:如果森林树为NULL,则空操作
否则①后序遍历森林中第一棵树的的子树森林②后序遍历森林中(除第一棵树之外)其余树构成的森林③访问森林中第一棵树的根节点
后序遍历对于森林中的任意一棵树而言,对该树根节点的访问,被推迟到该树后面的所有的树中的结点都访问完毕才进行,不常用
9.哈夫曼树
9.1哈夫曼树概念
哈夫曼树:具有最小加权路径长度的扩充二叉树
举例:将学生的百分制成绩转换为五分制成绩
<60: E 60-69:D 70-79:C 80-89:B 90-100:A
如图所示可以满足将成绩转换为五分制,成绩<60比较一次,<70比较两次,<80比较三次<90和>90都是比较四次。如果学生成绩数据有10000个,则总共需要比较10000*(1*5%+2*15%+3*40%+4*10%)=31500次。程序的操作时间过长
上图形似单边二叉树,将其转换为
可以得出ABC都只用判断两次,DE都只用判断三次
如果学生成绩数据有10000个,则总共需要比较10000(2*80%+3*20%)=22000次
显然两种判别树的效率是不一样的,哈夫曼算法就是寻找最优二叉树
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
路径:从树中一个结点到另一个结点之间的分支,构成着两个结点间的路径
结点的路径长度:两结点间路径上的分支数
树的路径长度:从树根到每一个结点的路径长度之和
权:将树中结点赋给一个有着某种含义的数值,则称这个数值为该结点的权
结点的带权路径长度:从根节点到该节点之间的路径长度与该节点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和
哈夫曼树:最优树,选择带权路径长度最短的树。满二叉树不一定是哈夫曼树,哈夫曼树中权越大的叶结点离根越近,具有相同带权结点的哈夫曼树不唯一
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
哈夫曼算法步骤:
(1)根据n个给定的权值{w, We,....W}构成n棵二叉树的森林F={71,T2. ..,T},其中 T只有一个带权为w的根结点。构造森林全是根
(2)在F中选取两棵根结点的权值最少的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。选用两小造新树
(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。删除两小添新人
(4)重复(2)(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树
从图中可以得知,哈夫曼树中只有度为0或2的结点,没有度为1的结点。包含n个叶结点的哈夫曼树中总共有2n-1个结点。包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
9.2哈夫曼树实现
采用顺序存储结构--一维结构数组
//哈夫曼树顺序存储结构--一维结构数组
#include<stdio.h>
#include<stdlib.h>
#define MAX 99999
//哈夫曼树中共有2n-1个结点,采用一维结构数组存储,不使用0下标,数组大小为2n
typedef struct
{
int weight;//q权值
int flag;
int parent, left, right;
}HuffmanTree;
//哈夫曼树初始化
void InitHuffmanTree(HuffmanTree** HT,int n)
{
//分配2*n-1个节点空间
(*HT) = (HuffmanTree*)malloc((2*n-1)*sizeof(HuffmanTree));
int i;
printf("输入权值序列\n");
for (int i=1;i<2*n;i++)
{
if (i<n+1)
{
scanf_s("%d", &(*HT)[i].weight);//输入前n个元素的权值
}
else
{
(*HT)[i].weight = MAX;
}
(*HT)[i].flag = 0;
(*HT)[i].parent = 0;
(*HT)[i].left = 0;
(*HT)[i].right = 0;
}
printf("初始化结束\n");
}
// 查询未被访问的最小权值结点下标
int SelectMinWeigth(HuffmanTree* HT, int N)
{
int i, min, temp;
// 查找第一个未访问结点的权值
for (i = 1; i < N+1; i++)
{
if (!HT[i].flag)
{
temp = HT[i].weight;
min = i;
break;
}
}
// 在剩余结点中查询未访问的最小权值
while (i < N+1)
{
if (!HT[i].flag && HT[i].weight < temp)
{
temp = HT[i].weight;
min = i;
break;
}
i++;
}
// 设置该最小权值已被访问
HT[min].flag = 1;
return min;
}
// 构建哈夫曼树
void CreateHuffmanTree(HuffmanTree* HT, int N)
{
int i;
for (i = N+1; i < 2 * N ; i++)
{
// 获取未被访问的两个最小权值下标
int min1 = SelectMinWeigth(HT, i);
int min2 = SelectMinWeigth(HT, i);
// 设置两个最小权值指向的父结点下标
HT[min1].parent = i;
HT[min2].parent = i;
// 设置父结点指向的左右孩子结点下标
HT[i].left = min1;
HT[i].right = min2;
// 设置父结点权值为两个最小权值之和
HT[i].weight = HT[min1].weight + HT[min2].weight;
}
printf("哈夫曼树构建完毕!\n");
}
// 遍历哈夫曼树
void DisplayHuffmanTree(HuffmanTree* HT, int N)
{
int i;
printf("结点\t左孩子\t右孩子\t父结点\t权值\n");
for (i = 1; i < 2 * N ; i++)
{
printf("%d\t%d\t%d\t%d\t", i, HT[i].left, HT[i].right, HT[i].parent);
if (HT[i].weight == MAX)
{
printf("%c\n", '-');
}
else
{
printf("%d\n", HT[i].weight);
}
}
printf("\n");
}
int main()
{
int N;
HuffmanTree* HT;
printf("请输入结点数:");
scanf_s("%d", &N);
InitHuffmanTree(&HT, N);
printf("***********哈夫曼树的初始状态*********\n");
DisplayHuffmanTree(HT, N);
CreateHuffmanTree(HT, N);
printf("***********哈夫曼树的最终状态*********\n");
DisplayHuffmanTree(HT, N);
free(HT);
system("pause");
return 0;
}
9.3哈夫曼编码
字符以某种数字编码形式存储和传输
如果字符集中每个字符的使用频率相等,则固定长度编码的空间效率最高,如ASCLL码采用固定长度编码
如果字符使用频率差异较大,为了提高存储和传输的效率,可采用不等长的编码方式。为了保证编码解码过程的无二义性,要求字符集中任一字符的编码不能是另一字符编码的前缀,符合该特征要求的编码方案也称前缀编码。
方法:
1、统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短))。
2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。3、在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
如果叶结点的权值是待编码的文本中个字符的频率,则编码后文本的长度为即为该哈夫曼树的加权路径长度
1.为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀2.为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。