目录
一、树
定义
树: 是 n 个结点的有限集(n≥0),其中n=0时为空树;在任意一棵非空树中:
(1) 有且仅有一个特定的称为根结点 ( root ) 的结点;
(2) 当 n > 1 时,其他结点可分为若干个互不相交的子集,每一个子集本身又是一棵树,称为根的子树。(递归定义)
基本术语
结点: 包含一个数据元素及若干个指向其子树的分支
结点的度: 结点所拥有的子树的数目
叶子结点(终端结点): 度为0的结点
分支结点(非终端结点): 度不为0的结点
树的度: 树内各结点的度的最大值
孩子和双亲: 结点的子树的根称为该结点 的孩子,该结点称为孩子的双亲
兄弟: 同一个双亲的孩子之间互称兄弟
路径:从根结点开始,到达某结点p所经过的所有结点成为结点p的层次路径(有且只有一条)。
祖先: 是从根到该结点所经分支上的所有结点
子孙: 以某结点为根的子树中的任一结点都称为该结点的子孙
结点的层次: 根结点的层次为1,根的孩子结点的层次为2,以此类推
即:从根节点到最底层结点的层数。
堂兄弟: 双亲在同一层的结点互称为堂兄弟
树的深度(高度): 树中结点的最大层次
有序树和无序树: 树中结点的各子树从左至右是有次序的,称为有序树,否则称无序树(叶子结点序可以互换)。
森林: 是n(n>=0)棵互不相交的树的集合
分类
一般树:
二叉树 :
两类特殊的二叉树:
满二叉树:指的是深度为k且含有2k-1个结点的二叉树。 所有有分支结点都有左、右子树. 可对满二叉树的结点进行连续编号,若规定从根结点开始,按“自上而下、自左至右”的原则进行。 (即:在不增加树的层数前提下,无法再添加结点的二叉树。)
完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。
完全二叉树度为1的结点有0或1个。
(只删除满二叉树最底层,最右边连续若干个节点形成的二叉树)
森林 :
树的应用
树是数据库中数据组织一种重要形式
操作系统子父进程的关系本身就是一棵树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树
二、二叉树
定义
把满足以下两个条件的树型结构叫做二叉树(Binary Tree):(1)每个结点的度都不大于 2;(每个结点最多有两棵子树,可以没有子树、可以有一棵子树、可 以有两棵子树。)
(2)每个结点的孩子结点次序不能任意颠倒。
二叉树的性质
性质 1 :层节点数
在二叉树的第 i 层上至多有2i-1 个结点。 (i≥1)
性质 2 :节点总数
深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)
性质 3 :叶子节点数量
对任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1
N=n0+n1+n2;N-1=n1+2n2;
性质4:完全二叉树的深度
具有 n 个结点的完全二叉树的深度为 (log2n)+1
性质5:完全二叉树的残缺性
若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 【i/2】 的结点为其双亲结点;
若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
二叉树的存储结构
连续存储【完全二叉树】
用一组连续的存储单元存储二叉树的数据元素,以结点存储的相对位置表示结点之间的关系。即按照完全二叉树的顺序存储。
对非完全二叉树在其与对应完全二叉树的缺位元素用0或者NULL填补。因此存储非完全二叉树会造成存储空间的浪费。
#define MAX_TREE_SIZE 100 // 二叉树的最大结点数
typedef TElemType
SqBiTree[MAX_TREE_SIZE]; // 0号单元存储根结点
SqBiTree bt;
优点:查找某个结点的子结点、父节点速度快
缺点:耗内存空间大
*链式存储
#include <stdio.h>
#include <stdlib.h>
#define MAX 20
typedef char TElemType;
typedef int Status;
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
一般树的存储
双亲表示法
求父结点方便
孩子表示法
求子结点方便
双亲表示法
求父结点和子结点都很方便
二叉树表示法
普通树转化为二叉树方法:
设法保证任意结点的
左指针域指向它的第一个孩子,
右指针域指向它的兄弟。
(一个普通树转化为二叉树一定没有右子树)
森林的存储
森林->树->二叉树
*二叉树遍历
(1)先序遍历(DLR)操作过程
若二又树为空,则空操作,否则依次执行如下3个操作:
访问根结点;
按先序遍历左子树:
按先序遍历右子树;
先序遍历:A、B、D、F、G、C、E、H
第一个是根节点:A
void PreOrder(BiTree T)
{
if (T == NULL)
{
return;
}
printf("%c", T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
(2)中序遍历(LDR)操作过程
若二叉树为空,则空操作,否则依次执行如下3个操作:
按中序遍历左子树:
访问根结点;
按中序遍历右子树;左中右顺序
中序遍历:B、F、D、G、A、C、E、H
左边是左子树,右边是右子树
void InOrder(BiTree T)
{
if (T == NULL)
{
return;
}
InOrder(T->lchild);
printf("%c", T->data);
InOrder(T->rchild);
}
(3)后序遍历(LRD)操作过程
若二叉树为空,则空操作,否则依次执行如下3个操作:
按后序遍历左子树:
按后序遍历右子树;
访问根结点。
后序遍历:F、G、D、B、H、E、C、A
最后一个是根节点:A
void PostOrder(BiTree T)
{
if (T == NULL)
{
return;
}
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%c", T->data);
}
观察特点发现,已知先序中序或后序中序可以还原二叉树。
(4)层次遍历
设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,依次执行下面操作:(1) 队列不空,出队列,取队头元素。(2) 访问该元素所指结点。(3) 若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。此过程不断进行,当队列为空时,二叉树的层次遍历结束。
层次遍历:A、B、C、D、E、F、G、H
/*层次遍历二叉树 T,从第一层开始,每层从左到右遍历*/
void LevelOrder(BiTree T)
{
BiTree Queue[MAX],b;
/*用一维数组表示队列,front 和rear 分别表示队首和队尾指针*/
int front, rear;front=rear=0;
if(T)/*若树非空*/
{
/*根结点入队列*/
Queue[rear++]=T;
while (front!=rear)/*当队列非空*/
{
b=Queue[front++]; /*队首元素出队列,并访问这个结点*/
printf("%2c", b->data);
if (b->lchild!=NULL)
Queue[rear++]=b->lchild;/*左子树非空,则入队列*/
if (b->rchild!=NULL)
Queue[rear++]=b->rchild;/*右子树非空,则入队列*/
}
}
}
*建立二叉树
/#*先序创建二叉树*/
void CreateBiTree(BiTree *T)
{
char ch;
ch=getchar();
if (ch=='#');
(*T)=NULL;/*#代表空指针*/
else
{
(*T)=(BiTree) malloc(sizeof(BiTNode));
(*T)->data=ch;
/*申请结点*/
*生成根结点*/
CreateBiTree(&(*T)->lchild);
CreateBiTree(&(*T)->rchild);
/*构造左子树*/
/*构造右子树*/
}
}
二叉树的应用
查询二叉树中某个结点
Status Preorder (BiTree T, ElemType x, BiTree &p)
{
// 若二叉树中存在和 x 相同的元素,则 p 指向该结点并返回 OK,
// 否则返回 FALSE
if(T)
{
if (T->data==x) { p = T; return TRUE,}
else
{
if ( Preorder(T->lchild, x, p) ) return TRUE;
else return( Preorder(T->rchild, x, p) ) ;
}
}
}
统计二叉树中叶子结点的个数
void CountLeaf (BiTree T, int &count){
if ( T ) {
if ((!T->lchild)&& (!T->rchild))
count++; // 对叶子结点计数
CountLeaf( T->lchild, count);
CountLeaf( T->rchild, count);
} // if
} // CountLeaf
求深度
/*求二叉树的深度*/
int depth(BiTree T)
{
int depl, dep2;
if (T=-NULL) return 0;
else
{
dep1=depth(T->lchild);
dep2=depth(T->rchild):
}
return dep1>dep2?dep1+1:dep2+1;
}
递归删除以值为x的结点为根的子树
struct Node* deleteSubtree(struct Node* root, char x) {
if (root == NULL) {
return NULL;
}
if (root->data == x) {
// 释放以当前结点为根的子树空间
free(root->left);
free(root->right);
free(root);
return NULL;
}
// 递归处理左子树和右子树
root->left = deleteSubtree(root->left, x);
root->right = deleteSubtree(root->right, x);
return root;
}
根据先序和中序,确定二叉树
BiNode *Bicreat_1(char *preStr, char *inStr, int num)
{
if (num == 0)
return NULL;
struct BiNode *bt = NULL;
char root;
root = preStr[0];
int rootPos = 0;
while (inStr[rootPos] != root && rootPos < num)
{//找根结点在中序遍历中的位置,用于分割
rootPos++;
}
int leftchild = rootPos, rightchild = num - rootPos - 1;
//根结点下左右子树的长度,用于定位
bt = (struct BiNode *)malloc(sizeof(BiNode));
if (bt != NULL) {
bt->data = root;
bt->lchild = Bicreat_1(&preStr[1], &inStr[0], leftchild);
//用下标分别在前序,中序中定位左子树
bt->rchild = Bicreat_1(&preStr[leftchild + 1], &inStr[leftchild + 1], rightchild);
//用下标分别在前序,中序中定位右子树
}
return bt;
}
根据后序和中序,建立二叉树
BiNode *Bicreat_2(char *postStr, char *inStr, int num)
{
if (num == 0)
return NULL;
struct BiNode *bt = NULL;
char root;
root = postStr[num - 1];
int rootPos = 0;
while (inStr[rootPos] != root && rootPos < num)
{//找根结点在中序遍历中的位置,用于分割
rootPos++;
}
int leftchild = rootPos, rightchild = num - rootPos - 1;
//根结点下左右子树的长度,用于定位
bt = (struct BiNode *)malloc(sizeof(BiNode));
if (bt != NULL) {
bt->data = root;
bt->Lchild = Bicreat_2(&postStr[0], &inStr[0], leftchild);
//用下标分别在后序,中序中定位左子树
bt->rchild = Bicreat_2(&postStr[leftchild], &inStr[leftchild + 1], rightchild);
//用下标分别在后序,中序中定位右子树
}
return bt;
}
层次遍历建立二叉树
typedef struct QueueNode {
TreeNode *data;
struct QueueNode *next;
} QueueNode;
typedef struct Queue {
QueueNode *front;
QueueNode *rear;
} Queue;
void initQueue(Queue *q) {
q->front = q->rear = NULL;
}
void enqueue(Queue *q, TreeNode *data) {
QueueNode *newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = data;
newNode->next = NULL;
if (q->rear == NULL) {
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
}
TreeNode* dequeue(Queue *q) {
if (q->front == NULL) {
return NULL;
}
QueueNode *temp = q->front;
TreeNode *data = temp->data;
q->front = q->front->next;
if (q->front == NULL) {
q->rear = NULL;
}
free(temp);
return data;
}
TreeNode* buildBinaryTree(int *arr, int n) {
if (n <= 0) {
return NULL;
}
TreeNode *root = (TreeNode*)malloc(sizeof(TreeNode));
root->data = arr[0];
root->left = root->right = NULL;
Queue q;
initQueue(&q);
enqueue(&q, root);
int i = 1;
while (i < n) {
TreeNode *parent = dequeue(&q);
if (arr[i] != -1) {
TreeNode *leftChild = (TreeNode*)malloc(sizeof(TreeNode));
leftChild->data = arr[i];
leftChild->left = leftChild->right = NULL;
parent->left = leftChild;
enqueue(&q, leftChild);
}
i++;
if (i < n && arr[i] != -1) {
TreeNode *rightChild = (TreeNode*)malloc(sizeof(TreeNode));
rightChild->data = arr[i];
rightChild->left = rightChild->right = NULL;
parent->right = rightChild;
enqueue(&q, rightChild);
}
i++;
}
return root;
}
线索二叉树
线索化是在遍历过程修改空指针域的过程
现作如下规定:
若结点有左子树,则其LChild 域指向其左孩子,否则LChild 域指向其前驱结点;
若结点有右子树,则其 RChild 域指向其右孩子,否则 RChild 域指向其后继
三、树和森林
树、森林与二叉树的转换
树转换为二叉树的步骤如下
1.加线。在所有兄弟结点之间加一条连线。
2.去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3.层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
方法2--普通树转化为二叉树方法:
设法保证任意结点的
左指针域指向它的第一个孩子,
右指针域指向它的兄弟。
(一个普通树转化为二叉树一定没有右子树)
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。森林转换为二叉树步骤如下:
1.把每个树转换为二叉树。
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
二叉树转换为树是树转换为二叉树的逆过程,也就是反过来做而已。步骤如下:
1.加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点……哈,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2.去线。删除原二叉树中所有结点与其右孩子结点的连线。
3.层次调整。使之结构层次分明。
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。那么如果是转换成森林,步骤如下:
1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。
2.再将每棵分离后的二叉树转换为树即可。
哈夫曼树
构造哈夫曼树
[思路和算法]: 赫夫曼算法:(一叶一树,自底向上,最小合并,逐步合并) (1) 根据给定的n个权值{w1,w2,w3,...,wn}构成n棵二叉树的集合F={T1,T2,T3,...,Tn},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均为空。 (2) 在集合F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,新二叉树的根结点的权值为其左右子树上根结点的权值之和。 (3) 在集合F中删除这两棵树,同时将新得到的二叉树加入F中。 (4) 重复步骤(2)、(3),直到F中只含一棵树为止,这棵树就是一棵赫夫曼树。 根据赫夫曼算法可知:赫夫曼树没有度为1的结点。
带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
前缀编码
前缀编码:对于不等长编码,若任一个字符的编码都不是另一个字符的编码的前缀,则这种编码称为前缀编码。
哈夫曼编码
赫夫曼编码的算法思路: (1)把字符出现的频率作为权值,根据这些权值构造一棵赫夫曼树。 (2)约定在赫夫曼树中,左分支表示字符‘0’,右分支表示字符‘1’, 则从根结点到叶子结点的路径上的分支字符组成的字符串即为该叶子 结点字符的编码。
哈夫曼树满足两条性质:
性质1 哈夫曼树是前缀编码。
性质2 哈夫曼树是最有前缀编码。