叶子结点(无后继)
树是由n >= 0个结点组成的有穷集合(不妨用符号D表示)以及结点之间关系组成的集合构成的结构,记为T。
当n=0时,称T为空树。
在任何一棵非空的树中,有一个特殊的结点t(属于D),称之为该树的根结点
其余结点D–{t}被分割成m>0个不相交的子集D1, D2, … ,Dm,其中,每一个子集Di分别构成一棵树,称之为t的子树。
有且仅有一个结点没有前驱结点,该结点为树的根结点;
除了根结点外,每个结点有且仅有一个直接前驱结点
包括根结点在内,每个结点可以有多个后继结点
文氏图表示法(教材P144)
凹入表示法(教材P144)
嵌套括号法(广义表表示法)
借助自然界中一棵倒置的树的形状来表示数据元素之间层次关系的方法。
结点的度:该结点拥有的子树的数目
树的度:树中结点度的最大值
叶结点:度为0的点(终端结点)
分支结点:度非0的点(非终端结点)
树的层次:根节点为第一层,若某结点在第i层,则其孩子结点(若存在)为第i-1层
树的深度/高度:树中结点所处的最大层次数
路径:对于树中任意两个结点di和dj,若在树中存在一个结点序列d1,d2, … di, …,dj,使得di是di+1的双亲(1≤i<j),则称该结点序列是从di到dj的一条路径。
从根结点到树中其余结点均分别存在一条唯一路径
路径的长度为:路径结点数-1。
祖先与子孙:若树中结点d到ds存在一条路径,则称d是ds的祖先,ds是d的子孙。
一个结点的祖先是从根结点到该结点路径上所经过的所有结点;而一个结点的子孙则是以该结点为根的子树上的所有其他结点。
树林(森林):m >= 0 棵不相交的树组成的树的集合。
树的有序性:若树中结点的子树的相对位置不能随意改变, 则称该树为有序树,否则称该树为无序树。
结点间关系:结点的子树的根称为该结点的孩子(child),相应地,该结点称为孩子结点的父结点(或双亲,parent)。同一个双亲的孩子之间互称兄弟。
顺序存储结构
链式存储结构(居多)
主要取决于要对树进行何种操作
无论采用何种存储结构,需要存储的信息有:
链结点的构造: data child1 child2 … childn
缺点:存储空间比较浪费
对于具有n个结点且度为k的树,空指针域的数目是多少?n(k-1) + 1
链结点的构造: data k(结点的度) child1 child2 … childn
缺点:对树的操作不方便
链结点的构造:data child parent brother
data 为数据域;
child 为指针域,指向该结点的第1个孩子结点;
parent 为指针域,指向该结点的双亲结点;
brother 为指针域,指向右边第一个兄弟结点。
二叉树是n >= 0个结点的有穷集合D与D上关系的集合R构成的结构,记为T。
当n=0时,称T为空二叉树
否则,它为包含了一个根结点以及两棵不相交的、分别称之为左子树与右子树的二叉树。
度为2 的树是二叉树。×
度为2 的有序树是二叉树。×
子树有严格的左、右之分且度≤2的树是二叉树
具有三个结点的二叉树:5种形态
具有三个结点的树:2种形态
若一棵二叉树中的结点,或者为叶结点,或者具有两棵非空子树,并且叶结点都集中在二叉树的最下面一层。这样的二叉树为满二叉树。
若一棵二叉树中只有最下面两层的结点的度可以小于2,并且最下面一层的结点(叶结点)都依次排列在该层从左至右的位置上。这样的二叉树为完全二叉树。
具有n个结点的非空二叉树共有(n - 1)个分支。
除了根结点以外,每个结点有且仅有一个双亲结点,即每个结点与其双亲结点之间仅有一个分支存在, 因此,具有n个结点的非空二叉树的分支总数为n–1。
非空二叉树的第i 层最多有2^(i–1)个结点(i >= 1)。
证明采用归纳法
深度为 h 的非空二叉树最多有[(2^h) –1]个结点。
深度为 h 的完全二叉树至少[2^(h-1)]个结点
若非空二叉树有n0个叶结点,有n2个度为2的结点,则 n0 = n2 + 1
推论:n0=n2+2n3+3n4+ … +(m–1)n(m) +1(P230,习题7-4)
具有n个结点的非空完全二叉树的深度为h=[log2(n)] + 1.
若对具有n个结点的完全二叉树按照层次从上到下,每层从左到右的顺序进行编号, 则编号为i的结点具有以下性质:
当i=1,则编号为i的结点为二叉树的根结点;
若i>1,则编号为i 的结点的双亲的编号为[i/2];
若2i>n,则编号为i 的结点无左子树;
若2i <= n,则编号为i 的结点的左孩子的编号为2i;
若2i+1>n,则编号为i 的结点无右子树;
若2i+1 <= n,则编号为i 的结点的右孩子的编号为2i+1。
6a. 若对具有n个结点的完全二叉树按照层次从上到下,每层从左到右的顺序进行编号,则编号为i的结点具有以下性质:
当i = 0,则编号为i的结点为二叉树的根结点;
若i>0,则编号为i 的结点的双亲的编号为[(i-1)/2];
若2i+1 ≥ n,则编号为i 的结点无左子树;
若2i+1
若2i+2 ≥ n,则编号为i 的结点无右子树;
若2i+2
INITIAL(T) 初始(创建)一棵二叉树.
ROOT(T)或ROOT(x) 求二叉树T的根结点, 或求结点x
所在二叉树的根结点。
PARENT(T,x) 求二叉树T中结点x的双亲结点。
若x是二叉树的根结 点,或二叉树中不存在结点x,则返回“空”
LCHILD(T,x)或RCHILD(T,x) 分别求二叉树T中结点x的左孩子结点或右孩子结点。
LDELETE(T,x)或RDELETE(T,x) 分别删除二叉树T中以结点x为根的左子树或右子树。
TRAVERSE(T) 按照某种次序(或原则)依次访问二叉树T中各个结点,得到由该二叉树的所有结点组成的序列。
LAYER(T,x) 求二叉树中结点x所处的层次
DEPTH(T) 求二叉树T的深度
DESTROY(T) 销毁一棵二叉树。
删除T的所有结点,并释放结点空间,使之成为一棵空二叉树
根据完全二叉树的性质6 ,对于深度为h的完全二叉树, 将树中所有结点的数据信息按照编号的顺序依次存储到一维数组BT[0…2h-2]中,由于编号与数组下标一一对应,该数组就是该完全二叉树的顺序存储结构。
在C中,对于一个下标为i的结点:
其父结点下标为:(i - 1)/2
其子结点下标为:2i+1,2i+2
对于一般二叉树, 只须在二叉树中“添加”一些实际上二叉树中并不存在的“虚结点” (可以认为这些结点的数据信息为空),使其在形式上成为一棵“完全二叉树”, 然后按照完全二叉树的顺序存储结构的构造方法将所有结点的数据信息依次存放于数组BT[0…2h -2]中。
顺序存储结构比较适合满二叉树,或者接近于满二叉树的完全二叉树,对于一些称为"退化二叉树"的二叉树,顺序存储结构的空间开销浪费的缺点表现比较突出。
.顺序存储结构便于结点的检索(由双亲查子、由子查双亲)。
顺序存储结构由于需要事先分配存储空间,对于动态数据容易溢出。
深度+1,结点数会增加一倍之多
data 为数据域
left 与right 分别为指向左、右子树的指针域
data 为数据域
parent为指向双亲结点的指针;
left和right 分别为指向左、右孩子结点的指针
这种结构的最大好处是当找到一个结点时,可以很方便的得到其所有祖先结点,或得到从根到该结点的路径。
struct node {
Datatype data;
struct node *left , *right;
};
typedef struct node BTNode;
typedef struct node *BTNodeptr;
BTNodeptr T, p, q;
按照一定的顺序(原则)对二叉树中每一个结点都访问一次(仅访问一次),得到一个由该二叉树的所有结点组成的序列,这一过程称为二叉树的遍历。
常用的遍历方法:
L表示遍历左子树,R表示遍历右子树,D表示访问根结点
若被遍历的二叉树非空, 则
访问根结点
以前序遍历原则遍历根节点的左子树
以前序遍历原则遍历根结点的右子树
void perorder(BTNodeptr t)
{
if(t!=NULL){
VISIT(t); /* 访问t指向结点 */
preorder(t->left);
preorder(t->right);
}
}
若被遍历的二叉树非空, 则
以前序遍历原则遍历根节点的左子树
访问根结点
以前序遍历原则遍历根结点的右子树
void inorder(BTNodeptr t)
{
if(t!=NULL){
inordert->left);
VISIT(t); /* 访问t指向结点 */
inorder(t->right);
}
}
若被遍历的二叉树非空, 则
以前序遍历原则遍历根节点的左子树
以前序遍历原则遍历根结点的右子树
访问根节点
void postorder(BTNodeptr t)
{
if(t!=NULL){
postorder(t->left);
postorder(t->right);
VISIT(t); /* 访问t指向结点 */
}
}
若被遍历的二叉树非空,则按照层次从上到下,每一层从左到右依次访问结点
对二叉树进行层次遍历的时间复杂度均为O(n)
#define NodeNum 100
void layerorder(BTNodeptr t)
{
BTNodeptr queue[NodeNum], p;
int front, rear;
if(t!=NULL){
queue[0]=t;
front=0;
rear=0;
while(front <= rear){ /* 若队列不空 */
p=queue[front++];
VISIT(p); /* 访问p指结点 */
if(p->left!=NULL) /* 若左孩子非空 */
queue[++rear]=p->left;
if(p->right!=NULL) /* 若右孩子非空 */
queue[++rear]=p->right;
}
}
}
void layerorder(BTNodeptr t)
{
BTNodeptr p;
if(t!=NULL){
enQueue(t);
while(!isEmpty()){
p = deQueue();
VISIT(p);
if(p->lchild!=NULL)
enQueue(p->left);
if(p->rchild!=NULL)
enQueue(p->right);
}
}
}
利用前序序列和中序序列恢复二叉树 √
利用中序序列和后序序列恢复二叉树 √
利用前序序列和后序序列恢复二叉树 ×
在前序序列中确定根,到中序序列中分左右。
在后序序列中确定根,到中序序列中分左右。
前序遍历:
类似转换后的二叉树的前序遍历
后序遍历
类似转换后的二叉树的中序遍历!!!!!
#define MAXD 3 //树的度
struct node{
Datatype data;
struct node *next[MAXD];
};
typedef struct node TNode;
typedef struct node *TNodeptr;
void DFStree(TNodeptr t)
{
int i;
if(t!=NULL){
VISIT(t); /* 访问t指向结点 */
for(i=0;i<MAXD; i++)
if(t->next[i] != NULL)
DFStree(t->next[i]);
}
}
#define MAXD 3 //树的度
struct node{
Datatype data;
struct node *next[MAXD];
};
typedef struct node TNode;
typedef struct node *TNodeptr;
void BFStree(TNodeptr t)
{
TNodeptr p; int i;
if(t!=NULL){
enQueue(t);
while(!isEmpty()){ /* 若队列不空 */
p = deQueue();
VISIT(p);
for(i=0; i<MAXD; i++) /* 依次访问p指向的子结点 */
if( p->next[i] != NULL)
enQueue(p);
}
}
}
//二叉树的拷贝:
BTNodeptr copyTree(BTNodeptr src)
{
BTNodeptr obj;
if(src == NULL)
obj = NULL;
else {
obj = (BTNodeptr) malloc(sizeof(BTNode));
obj->data = src->data;
obj->left = copyTree(src->left);
obj->right = copyTree(src->right);
}
return obj;
}
//二叉树的删除
void destoryTree(BTNodeptr p)
{
if(p != NULL)
{
destoryTree(p->left);
destoryTree(p->right);
free(p);
p = NULL;
}
}
//二叉树的高度
int max(x,y)
{
if(x >y)
return x;
else
return y;
}
int heightTree(BTNodeptr p)
{
if(p == NULL)
return 0;
else
return 1 + max(heightTree(p->left) , heightTree(p->right));
}
注意:
对于具有n个结点的二叉树,二叉链表中空的指针域数目为n+1
((2n(指针域总数))- (n-1(已用指针域数))) = n+1
利用二叉链表中空的指针域指出结点在某种遍历序列中的直接前驱或直接后继。指向前驱和后继的指针称为线索,加了线索的二叉树称为线索二叉树。
利用链结点的:
空的左指针域:存放该结点的直接前驱的地址
空的右指针域:存放该结点的直接后继的地址
非空的指针域:仍然存放结点的左孩子或右孩子的地址
p->lbit = 0 表示p->left为指向直接前驱的线索
1 表示p->left为指向左孩子的指针
p->rbit = 0 表示p->right为指向直接后继的线索
1 表示p->right为指向右孩子的指针
不改变链结点的构造,而是在作为线索的地址前加一个负号
“负地址”:表示线索
“正地址”:表示指针。
struct node{
Datatype data;
struct node *left, *right;
char lbit, rbit;
};
typedef struct node TBTNode;
typedef struct node *TBTNodeptr;
在中序线索二叉树中确定地址为x的结点的直接后继
void torder(TBTNodeptr head)//中序遍历-非递归
{
TBTNodeptr p = head;
while(1){
p = insucc(p);
if(p == head)
break;
VISIT(p);
}
}
TBTNodeptr insucc(TBTNodeptr x)//确定x的直接后继
{
TBTNodeptr s;
s = x->right;
if(x->rbit == 1)
while ( s->lbit == 1)
s = s->left;
return(s);
}
prior: 指向前一次访问结点
p: 指向当前访问结点
若当前访问的结点的左指针域为空,则它指向prior指的结点;同时置lbit为0, 否则,置lbit为1
若prior所指结点的右指针域为空,则它指向当前访问的结点。同时置rbit为0, 否则,置rbit为1。
p=NULL标志遍历结束
遍历结束时,将prior->right指向头结点,并置prior->rbit为0
/* 中序遍历进行中序线索化*/
/* piror是一个全局变量,初始时,piror指向树head结点*/
TBTNodeptr piror;
TBTNodeptr threading(TBTNodeptr root)
{
TBTNodeptr head;
head = (TBTNodeptr)malloc(sizeof(TBTNode));
head->left = root; head->right = head; head->lbit = head->rbit=1;
piror = head;
inThreading(root);
piror->right = head; piror->rbit = 0;
return head;
}
void inThreading(TBTNodeptr p)
{
if(p != NULL) {
inThreading(p->left) //递归左子树线索化
if(p->left == NULL) { //没有左孩子
p->lbit = 0; //前驱线索
p->left = prior; //左孩子指针指向前驱
}
else p->lbit = 1;
if( prior->right == NULL) { //前驱没有右孩子
prior->rbit = 0; //后继线索
prior->right = p; //前驱右孩子指向后继
}
else prior->rbit = 1;
prior = p; //保持prior指向p的前驱
inThreading(p->right); //递归右子树线索化
}
}
#define NodeNum 100 /* 定义二叉树中结点最大数目*/ */
TBTNodeptr inthread(TBTNodeptr t)
{
TBTNodeptr head, p=t, prior, stack[NodeNum];
int top=-1;
head=(TBTNoteptr)malloc(sizeof(TBNode)); /* 申请线索二叉树的头结点空间 */
head->left=t;
head->right=head;
head->lbit=1;
prior=head; /* 假设中序序列的第1个结点的“前驱”为头结点 */
do{
for(; p!=NULL; p=p->left) /* p移到左孩子结点 */
stack[++top]=p; /* p指结点的地址进栈 */
p=stack[top--]; /* 退栈 */
/***访问一个结点***/
if(p->left==NULL){ /* 若当前访问结点的左孩子为空 */
p->left=prior; /* 当前访问结点的左指针域指向前一次访问结点 */
p->lbit=0; /* 当前访问结点的左标志域置0(表示地址为线索) */
}
else
p->lbit=1; /* 当前访问结点的左标志域置1(表示地址为指针) */
if(prior->right==NULL)
{ /* 若前一次访问的结点的右孩子为空 */
prior->right=p; /* 前一次访问结点的右指针域指向当前访问结点 */
prior->rbit=0; /* 前一次访问结点的右标志域置0(表示地址为线索) */
}
else
prior->rbit=1; /* 前一次访问结点的右标志域置1(表示地址为指针) */
prior=p; /* 记录当前访问的结点的地址 */
/***访问一个结点***/
p=p->right; /* p移到右孩子结点 */
}
while(!(p==NULL && top==-1));
prior->right=head; /* 设中序序列的最后结点的后继为头结点 */
prior->rbit=0; /* prior指结点的右标志域置0(表示地址为线索) */
return head; /* 返回线索二叉树的头结点指针 */
其实线索化二叉树等于将一棵二叉树转变成了一个双向链表,这为二叉树结点的插入、删除和查找带来了方便
在实际问题中,如果所用的二叉树需要经常遍历或查找结点时需要访问结点的前驱和后继,则采用线索二叉树结构是一个很好的选择
将二叉树线索化可以实现不用栈的树深度优先遍历算法。
二叉查找树或者为空二叉树, 或者为具有以下性质的二叉树:
若根结点的左子树不空,则左子树上所有结点的值都小于根结点的值;
若根结点的右子树不空,则右子树上所有结点的值都大于或等于根结点的值。
每一棵子树分别也是二叉查找树
设K=( k1, k2, k3, … , kn )为具有n个数据元素的序列。从序列的第一个元素开始,依次取序列中的元素,每取一个元素ki,按照下述原则将ki插入到二叉树中:
功能:将一个数据元素item插入到根指针为root的二叉排序树中
#include
typedef int Datatype;
struct node {
Datatype data;
struct node *left, *right;
};
typedef struct node BTNode, *BTNodeptr;
BTNodeptr insertBST(BTNodeptr p, Datatype item)
int main()
{
int i, item;
BTNodeptr root=NULL;
for(i=0; i<10; i++){ //构造一个有10个元素的BST树
scanf(“%d”, &item);
root = insertBST(root, item);
}
return 0;
}
BTNodeptr insertBST(BTNodeptr p, Datatype item)
{
if(p == NULL)
{
p = (BTNodeptr)malloc(sizeof(BTNode));
p->data = item;
p->left = p->right = NULL;
}
else if( item < p->data)
p->left = insertBST(p->left, item);
else if( item > p->data)
p->right = insertBST(p->right,item);
else
do-something; //树中存在该元素
return p;
}
功能:将一个数据元素item插入到根指针为root的二叉排序树中
BTNodeptr Root=NULL; //Root是一个全局变量
void sortTree(Datatype k[ ], int n)//建立二叉查找树的(主)算法
{
int i;
for(i = 0; i < n; i ++)
insertBST(k[i]);//调用插入算法
return ;
}
void insertBST( Typedata item)//插入算法
{
BTNodeptr p, q;
//建立一个新的结点
p=(BTNodeptr)malloc(sizeof(BTNode));
p->data=item;
p->left=NULL;
p->right=NULL;
if(Root==NULL)
Root=p;
else
{
q=Root;
while(1)
{
/* 比较值的大小 */
/* 小于向左,大于向右 */
if(item<q->data)
{
if(q->left==NULL)
{
q->left=p;//插入结点
break;
}
else q=q->left;
}
else if (item>q->data)
{
if(q->right==NULL)
{
q->right=p;//插入结点
break;
}
else q=q->right;
}
else
{
/* do-something */
}
}
}
}
被删除结点为叶结点,则直接删除。
被删除结点无左子树,则用右子树的根结点,取代被删除结点。
被删除结点无右子树,则用左子树的根结点,取代被删除结点。
被删除结点的左、右子树都存在,则用被删除结点的右子树中值最小的结点(或被删除结点的左子树中值最大的结点)取代被删除结点。
如果删除的次数不多,则通常使用的策略是懒惰删除。
若查找成功,给出被查找元素所在结点的地址;
若查找失败,给出信息NULL。
二叉树采用二叉链式存储结构
BTNodeptr searchBST(BTNodeptr t , Datatype key)
{
BTNodeptr p = t;
while(p != NULL)
{
if(key == p->data)
return p; /* 查找成功 */
if(key > p->data)
p=p->right; /* 将p移到右子树的根结点 */
else
p=p->left; /* 将p移到左子树的根结点 */
}
return NULL; /* 查找失败 */
}
BTNodeptr searchBST( BTNodeptr t , Datatype key )
{
if(t != NULL)
{
if(key == t->data)
return t; /* 查找成功 */
if(key > t->data) /* 查找T的右子树 */
return searchBST(t->right, key);
else /* 查找T的左子树 */
return searchBST(t->left, key);
}
else
return NULL; /* 查找失败 */
平均查找长度ASL—— 确定一个元素在树中位置所需要进行的元素间的比较次数的期望值(平均值)。
如果被插入的元素序列是随机序列,或者序列的长度较小,采用“逐点插入法”建立二叉查找树可以接受。如果建立的二叉查找树中出现结点子树的深度之差较大时(即产生不平衡),就有必要采用其他方法建立二叉查找树,即建立所谓“平衡二叉树”。
二叉查找树的缺陷:树的形态无法预料、随意性大。得到的可能是一个不平衡的树,即树的深度差很大。丧失了利用二叉树组织数据带来的好处。
平衡二叉树又称AVL树。它或者是一棵空树,或者是具有下列性质的二叉树:
它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
若将二叉树的平衡因子定义为该结点左子树深度减去右子树深度,则平衡二叉树上所有结点的平衡因子只可能是-1、0和1。
例子:已知二叉查找树采用二叉链表存储结构,根结点地址为T,请写一非递归算法,打印数据信息为item的结点的所有祖先结点(从根结点到该结点的所有分支上经过的结点)。(设该结点存在祖先结点)
BTNodeptr searchBST(BTNodeptr t, Datatype item)
{
BTNodeptr p = t;
while(p != NULL){
if(item == p->data)
return(p); /* 查找成功 */
if(item > p->data)
{
printf(“%d ”,p->data);
p = p->right; /* 将p 移到右子树的根结点 */
}
else
{
printf(“%d ”,p->data);
p=p->left; /* 将p 移到左子树的根结点 */
}
}
return NULL ; /* 查找失败*/ /* 无用语句 */
}
设一个栈保存路径结点或设一个指向父结点的指针
void perorder(BTNodeptr t , Datatype item)
{
if(t != NULL){
push(t);
if(item == t->data)
弹出栈中所有元素;
preorder(t->left);
preorder(t->right);
pop();
}
}
对于输入序列无序、且需要频繁进行查找、插入和删除操作的动态表(如词频统计中的单词表),如何选取数据结构和算法即能兼顾插入和删除效率,又能较高效率地实现查找?
二叉查找树是很好的选择
有序顺序表(数组)
有序链表
二叉查找树
#include
#include
#define MAXWORD 100
struct tnode{
char word[MAXWORD];
int count;
struct tnode *left,*right;
}; //BST,单词树结构
int getword(FILE *bfp,char *w);
struct tnode *addtree(struct tnode *p,char *w);
void treeprint( struct tnode *p);
int main()
{
char filename[32], word[MAXWORD];
FILE *bfp;
struct tnode *root=NULL;//BST树根节点指针
scanf(“%s”, filename);
if((bfp = fopen(filename, “r”)) == NULL){ //打开一个文件
fprintf(stderr, “%s can’t open!\n”,filename);
return -1;
}
while( getword(bfp,word) != EOF) //从文件中读入一个单词
root = addtree(root, word);
treeprint(root); //遍历输出单词树
return 0;
}
堆是一种特殊类型的二叉树,具有以下两个性质:
满足上面两个性质定义的是大顶堆(max heap)(或小顶堆min heap)。这意味着大顶堆的根节点包含了最大的元素,小顶堆的根节点包含了最小的元素
由于堆是一个完全树,一般采用数组实现,对于一个下标为i的结点:
堆结构的最大好处是元素查找、插入和删除效率高(O(log2n))
堆的主要应用:
堆是一棵完全二叉树,二叉树中任何一个分支结点的值都大于或者等于它的孩子结点的值,并且每一棵子树也满足堆的特性。
堆的基本操作:insert(插入)和delete(删除)
heapInsert(e)
{
将e放在堆的末尾;
while e 不是根 && e > parent(e)
e 与其父节点交换
}
heapDelete() //取堆顶(树根)元素
{
从根节点提取元素;
将最后一个叶节点中的元素放到要删除的元素位置 ;
删除最后一个叶节点;
//根的两个子树都是堆
p = 根节点 ;
while p 不是叶节点 && p < 它的任何子节点
p与其较大的子节点交换
}
堆的构造有两种方法:自顶向下(John Williams提出)和自底向上(Robert Floyd提出)
优先队列(Priority queue):与传统队列不同的是下一个服务对象是队列中优先级最高的元素。优先队列常用的实现方式是用堆,其最大好处是管理元素的效率高(O(log2 (N) )。
优先队列是计算机中常用的一种数据结构,如操作系统中进程调度就是基于优先队例。
堆排序(Heap sort):一种基于堆的高效(O(nlog2 n))的排序算法。
表达式树是一种特殊类型的树,其叶结点是操作数(operand),而其它结点为操作符(operator):
表达式树的最大好处是表达式没有括号,计算时也不用考虑运算符优先级。
主要应用:编译器用来处理程序中的表达式
表达式树是这样一种树,非叶节点为操作符,叶节点为操作数,对其进行遍历可计算表达式的值。由后缀表达式生成表达式树的方法如下:
从左至右从后缀表达式中读入一个符号:
重复步骤1,直到后缀表达式处理完。
若给具有m个叶结点的二叉树的每个叶结点赋予一个权值,则该二叉树的带权路径长度定义为:
WPL=(i从1到m求和)w(i)· l(i)
给定一组权值,构造出的具有最小带权路径长度的二叉树称为哈夫曼树。
权值越大的叶结点离根结点越近,权值越小的叶结点离根结点越远;
这样的二叉树WPL最小
无度为1的结点
哈夫曼树不是惟一的
在信息和网络时代,数据的压缩解压是一个非常重要的技术,现有的数据压缩和解压技术很多是基于Huffman的研究之上发展而来。
如何得到使电文总长最短的编码——哈夫曼编码
设:
则:电文总长度为:(i从1到n求和)wi · li (二叉树的带权路径长度)
~ | 按位取反,如~5 |
---|---|
& | 按位“与” |
I | 按位“或” |
^ | 按位“异或” |
>> | 按位右移,低位移出,对int为算术右移(高位补符号位),对unsigned为逻辑右移(高位补0) |
<< | 按位左移,左移,高位移出,低位补0 |
编写一个函数getbits(unsigned x, unsigned p, unsigned n),返回一个整型变量x从位置p开始的n位
unsigned getbits(unsigned x, unsigned p, unsigned n)
{
return ( ( x >> (p + 1 – n)) & ~(~0 << n));
}
在按位运算中:
//首先定义如下结构:
#define MAXSIZE 32
struct cc { //字符及出现次数结构
char c;
int count;
};
struct tnode { //Huffman树结构
struct cc ccount; //字符及出现次数
struct tnode *left,*right; //树的左右节点指针
struct tnode *next; //一个有序链表的节点指针
};
struct tnode *Head=NULL; //一个有序链表的头节点,也是最后Huffman树的根节点
char Huffman[MAXSIZE]; //用于生成Huffman编码
char HCode[128][MAXSIZE]; //字符的Huffman编码,Hcode[0]为文件结束符的编码
//例如:Hcode['a']表示字符a的Huffman编码串。
//第一步为了生成Huffman树,首先根据字符统计结果生成一个有序链表:
struct tnode *p;
for(i = 0 ; i < 128 ; i ++)
{
if(ccount[i].count != 0)
{
p = (struct tnode *)malloc(sizeof(struct tnode));
p->ccount = ccount[i];
p->left = p->right = p->next = NULL;
insertSortLink(p);
}
}
//第二步按Huffman树生成算法,由有序表构造Huffman树:
while(Head->next != NULL)
{
p = (struct tnode *)malloc(sizeof(struct tnode));
p ->ccount.count = Head->ccount.count + Head->next->ccount.count;
p->left = Head; /*将新树的根结点加入到有序结点链表中*/
p->right = Head->next;
p->next = NULL;
Head = Head->next->next;
insertSortLink(p);
}
//第三步遍历(前序)Huffman树,为每个叶结点生成Huffman编码:
void createHCode(struct tnode *p,char code, int level)
{
if(level != 0)
Huffman[level-1] = code;
if(p->left == NULL && p->right == NULL)
{
Huffman[level] ='\0' ;
strcpy(HCode[p->ccount.c], Huffman);
}
else
{
createHCode(p->left, '0', level+1);
createHCode(p->right, '1', level+1);
}
}
//字符频率统计:
struct cc ccount[128];
while( (c=fgetc(fp)) != EOF)
{
ccount[c].c=c; ccount[c] .count++;
}
//第四步:根据Huffman编码,遍历源文件,生成相应压缩文件:
void madeHZIP(FILE *src, FILE *obj)
{
unsigned char *pc,hc=0;
int c=0,i=0;
fseek(src,0, SEEK_SET); //从src文件头开始
do {
c=fgetc(src) ; //依次获取源文件中每个字符
if (c == EOF) c=0; //源文件结束
for(pc = HCode[c]; *pc != '\0'; pc++) //转换为huffman码
{
hc = (hc << 1) | (*pc-'0'); i++;
if(i==8) //每满8位输出一个字节
{
fputc(hc,obj); i = 0;
}
}
if(c==0 && i!=0) //处理文件结束时不满一个字节的情况
{
while(i++<8) hc = (hc << 1);
fputc(hc,obj);
}
}while( c ); //c=0时文件结束
}
说明:
每个树节点可以有两个以上的子节点,称为m阶多叉树,或称为m叉树。
多叉树通常用于大数据的快速检索和信息更新。本课程将在查找(searching)一讲中介绍下面多叉树的应用:
#define MAXD 3 //树的度
struct node{
Datatype data;
struct node *next[MAXD];
};
typedef struct node TNode;
typedef struct node *TNodeptr;
void DFStree(TNodeptr t)
{
int i;
if(t != NULL){
VISIT(t); /* 访问t指向结点 */
for(i = 0;i < MAXD ; i ++)
if(t->next[i] != NULL)
DFStree(t->next[i]);
}
}
void BFStree(TNodeptr t)
{
TNodeptr p;
int i;
if(t != NULL){
enQueue(t);
while(!isEmpty()){ /* 若队列不空 */
p = deQueue();
VISIT(p);
for(i=0; i<MAXD; i++) /* 依次访问p指向的子结点 */
if( p->next[i] != NULL)
enQueue(p);
}
}
}
树的构造取决于树的定义(即结点之间的组成关系),也依赖于输入数据的组织方式
自顶向下构造法
结点插入法:按照树结点组成规则,找到插入位置,依次插入结点
如:BST树构造、堆构造(常用)
基本原理:查找(插入位置) → 插入
按层构造法:通常利用一个队来依次按层构造树(参考BFS算法)。
如输入数据按层组织
自底向上构造法
按照树结点的组成规则依次自底向上构造,这类方法通常要用到栈或队等数据结构,如,表达式树构造(用到栈)、Huffman树构造(用到有序表或优先队列)
谨慎使用递归,因为它的简洁可能会掩盖它的低效率。
许多安全关键软件,如航空航天中的控制软件,限制使用递归。
viod inorder(BTNodeptr t)
{
BTNodeptr p;
p = t;
while( p != NULL || !isEmpty())//“或者”,满足其一即可
{
if(p != NULL)
{
push(p);
p = p->left;
}
else
{
p = pop();
VISIT(p);
p = p->right;
}
}
}
void inorder(BTNodeptr t)
{
BTNodeptr stack[M], p=t;
int top=-1;
if(t!=NULL)
do{
for(; p!=NULL; p=p->left)
stack[++top]=p;
p=stack[top– –];
VISIT(p);
p=p->right;
}while(!(p==NULL && top==-1));
}
已知具有n个结点的完全二叉树采用顺序存储结构,结点的数据信息依次存放于一维数组BT[0…n-1]中,写出中序遍历二叉树的非递归算法:
设置一个栈,保存遍历过程中结点的位置;
设置一个变量,初始时给出根结点的位置;
1. 若变量所指位置上的结点存在,则将该变量所指位置进栈,然后将该变量移到左孩子;(即 i = 2*i + 1)
2. 若变量所指位置上的结点不存在,则从栈中退出栈顶元素送变量,访问该变量位置上的结点,然后将该变量移到右孩子:(即 i = 2*i + 2 )
直到变量所指位置上结点不存在,同时堆栈也为空。
#define MaxSize 100
void inorder(Datatype bt[],int n)
{
int stack[MaxSize],i,top=-1;
i=0;
if(n>=0)
{
do{
while(i<n)
{
stack[++top]=i; /* bt[i]的位置i进栈*/
i=i*2+1; /* 找到i的左孩子的位置 */
}
i=STACK[top--]; /* 退栈*/
VISIT(bt[i]); /* 访问结点bt[i] */
i=i*2+2; /* 找到i的右孩子的位置*/
}while(!(i>n-1 && top==-1));
}
}