树:n(n≥0)个结点的有限集。在任何一棵非空树中:
1.有且仅有一个特定称为根的结点
2.n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中Ti称为树的子树。
树的度
树中各结点的度的最大值
树的深度
(高度):树中叶子结点所在的最大层次
数据元素及若干指向其子树的分支
节点的度
节点拥有子树的个数
节点的层次
设根结点的层次为1,第l层结点的层次为L,则第l层结点的子树根结点的层次为L+1
节点的关系
孩子(结点)
结点子树的根称为该结点的孩子
双亲(结点)
结点是其孩子的双亲
祖先(结点)
从根到该结点所经分支上的所有结点
以该结点为根的子树中的任一结点
同一双亲的孩子互称兄弟
其双亲在同一层的结点互称堂兄弟
节点的分类
叶子
(终端结点):度为零的结点
非终端结点
(分支结点):度不为零的结点
层次性
递归性
InitTree(&T); CreateTree(&T,definition);
ClearTree(&T); TreeEmpty(T);
TreeDepth(T); Root(T);
Value(T,cur_e); Assign(T, &cur_e, value);
Parent(T, cur_e);
LeftChild(T,cur_e);
RightSibling(T,cur_e);
InsertChild(&T,&p,i,c); DeleteChild(&T,&p,i);
TraverseTree(T,visit());
DestroyTree(&T);
每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)
二叉树的子树有左右之分,其次序不能任意颠倒,分别称为左子树和右子树,左子树和右子树的根称为其双亲的左、右孩子
满二叉树
一棵深度为k且正好有2k-1个结点的二叉树称为满二叉树
完全二叉树
深度为k、有n个结点的二叉树中的各结点与深度为k的满二叉树编号从1到n的结点一一对应
约定编号从根结点开始,自上而下,从左到右
所有的叶子结点都出现在最大的2层上
任一结点,若其右子树最大高度为L,则其左子树最大高度为L或L+1
性质1:在二叉树的第i层上至多有2i-1个结点(i≥1)。
采用归纳法证明此性质。
当i=1时,只有一个根结点,2i-1=20 =1,命题成立。
假设第i-1层上至多有2i-2个结点。
由于二叉树每个结点的度最大为2,故在第i层上最大结点数为第i-1层上最大结点数的二倍,即2×2i-2=2i-1。
命题得证。
性质2:深度为k的二叉树至多有2k-1个结点(k≥1).
每一层最大节点数之和
性质3:对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
设二叉树中度为i的结点数为ni,二叉树中总结点数为n,则有:
n=n0+n1+n2 (1)
从二叉树中的分支数B看,除根结点外,其余结点都有一个进入分支,则有:n=B+1。
由于这些分支都是由度为1和2的结点射出的,所以有:
B=n1+2n2
n=B+1=n1+2n2+1 (2)
综合式(1)和(2)得到: n0+n1+n2=n1+2n2+1
n0=n2+1
性质4:具有n个结点的完全二叉树的深度为log2n(下取整) +1
假设此二叉树的深度为k,则根据性质2及完全二叉树的定义可得: 2k-1-1< n ≤ 2k-1
即: 2k-1 ≤ n < 2k
取对数得: k-1≤log2n<k, 即:log2n<k≤log2n+1
∵ k是整数 ∴ k= log2n(下取整) +1
若对一棵有n个结点的完全二叉树结点进行顺序编号,对任一结点i(1≤i≤n),有:
(1)若i=1,则i是二叉树的根,无双亲;若i>1,则其双亲编号是i/2(下取整)
(2)若2i>n,则i为叶子结点,无左孩子;否则,其左孩子编号是2i。
(3)若2i+1>n,则i无右孩子;否则,其右孩子编号是2i+1。
顺序存储结构:适用于完全二叉树
特点
特点:用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树的结点。此时可利用性质5很方便地求出结点之间的关系。
缺点
对一般二叉树,可将其每个结点与完全二叉树上同一位置上的结点对照,存储在一维数组的相应分量中。可能对存储空间造成极大的浪费。
C语言描述
#define MAX_TREE_SIZE 100
typedef ElemType SqBiTree[MAX_TREE_SIZE+1];
//0号单元空闲,1号单元存储根结点
二叉链表
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
- 基于二叉链表的基本操作
- 遍历二叉树
按某条搜索路径巡访二叉树中的每一个结点,使每一个结点均被访问一次且仅被访问一次。
- 基本遍历操作
- 先序遍历
先序遍历
访问根结点;
先序遍历左子树;
先序遍历右子树;
- 递归
- 算法分析
若二叉树为空,则空操作,否则
1访问根节点
2先序遍历左子树
3先序遍历右子树
- 代码实现
Status PreorderTraverse(BiTree T, Status(*visit)(ElemType))
{
if (T != NULL)//二叉树非空
{
if (visit(T->data))//访问节点,且访问成功
{
if (PreorderTraverse(T->lchild, visit))//先序遍历左子树
{
if (PreorderTraverse(T->rchild, visit))//先序遍历右子树
{
return OK;
}
}
}
return ERROR;
}
else return OK;
}
//二叉树先序遍历算法
- 非递归
- 算法分析
1将根节点入栈并访问,
2依次将左孩子入栈,直到没有左孩子,将栈顶的空指针弹出,
3将栈顶节点出栈并将其右孩子入栈访问,重复2
- 代码实现
void PreorderTraverse1(BiTree T, Status(*visit)(ElemType))
{
SqStack S;//建立栈
BiTree p;
InitSqStack(S);
SqPush(S, T);//根节点入栈
while (SqStackEmpty(S) == FALSE)//栈不空时
{
while (GetTopSq(S, p) && p)//当栈顶指针不为空
{
visit(p->data);//访问栈顶指针的数据
SqPush(S, p->lchild);//将左孩子入栈
}
SqPop(S, p);//弹出栈顶的空指针
if (SqStackEmpty(S) == FALSE)
{
SqPop(S, p);
SqPush(S, p->rchild);
}
}
}//先序遍历的非递归算法
- 中序遍历
中序遍历
中序遍历左子树;
访问根结点;
中序遍历右子树;
- 递归
- 算法分析
若二叉树为空,则空操作,否则
1先序遍历左子树
2访问根节点
3先序遍历右子树
- 代码实现
Status InorderTraverse(BiTree T, Status(*visit)(ElemType))
{
if (T != NULL)
{
if (InorderTraverse(T->lchild, visit))
{
if (visit(T->data))
{
if (InorderTraverse(T->rchild, visit))
{
return OK;
}
}
}
return ERROR;
}
else return OK;
}
//二叉树中序遍历算法
- 非递归
- 算法分析
1将根节点入栈并访问,
2依次将左孩子入栈,直到没有左孩子,将栈顶的空指针弹出,
3将栈顶节点出栈访问并将其右孩子入栈,重复2
- 代码实现
void InorderTraverse1(BiTree T, Status(*visit)(ElemType))
{
SqStack S;
InitSqStack(S); SqPush(S, T);
BiTree p;
while (SqStackEmpty(S) == FALSE)
{
while (GetTopSq(S, p) && p)
SqPush(S, p->lchild);
SqPop(S, p);
if (!SqStackEmpty(S))
{
SqPop(S, p);
visit(p->data);
SqPush(S, p->rchild);
}
}
} //中序遍历非递归算法
- 后序遍历
后序遍历
后序遍历左子树;
后序遍历右子树;
访问根节点
- 递归
- 算法分析
若二叉树为空,则空操作,否则
1先序遍历左子树
2先序遍历右子树
3访问根节点
- 代码实现
Status PostorderTraverse(BiTree T, Status(*visit)(ElemType))
{
if (T != NULL)
{
if (PostorderTraverse(T->lchild, visit))
{
if (PostorderTraverse(T->rchild, visit))
{
if (visit(T->data))
{
return OK;
}
}
}
return ERROR;
}
else return OK;
}
//二叉树后序遍历算法
- 非递归
- 算法分析
1构造空栈.定义两个指针r和p,r记录栈顶节点,p记录已经访问过的节点.
2依次将左孩子入栈,弹出空指针,如果访问过的节点是栈顶节点的右孩子,就访问栈顶节点更新p并出栈。更新栈顶r。
3将r的右孩子入栈,重复2
- 代码实现
void PostorderTraverse1(BiTree T, Status(*visit)(ElemType))
{
SqStack S;
BiTree p;
BiTree r;
int i;
InitSqStack(S); SqPush(S, T);
while (!SqStackEmpty(S))
{
while (GetTopSq(S, p) && p)
SqPush(S, p->lchild);//依次将左孩子入栈
SqPop(S, p);//将栈顶的空指针出栈
while ((i = GetTopSq(S, r)) && r->rchild == p)//如果从右孩子返回,访问节点
{
visit(r->data);
SqPop(S, p);
}
if (i) SqPush(S, r->rchild);//右孩子入栈
}
} //后序遍历的非递归算法
- 层序遍历
- 算法分析
1构造队列,队头指针p
2根节点非空则入队
3队列不空则出队访问,并将非空的左右孩子入队
- 代码实现
void LevelorderTraverse(BiTree T, Status(*visit)(ElemType))
{
BiTree p;
p = T;
SqQueue Q;
InitSqQueue(Q);
if (p) EnSqQueue(Q, p);
while (!SqQueueEmpty(Q)) // 队列不空
{
DeQueue(Q, p); visit(p->data);
if (p->lchild) EnSqQueue(Q, p->lchild);
if (p->rchild) EnSqQueue(Q, p->rchild);
}
} //层序遍历二叉树
- 复杂度分析
- 递归
- 算法复杂度分析
- 时间复杂度
根据公式T(n)=2T(n/2)+1=2(2T(n/4)+1)+1=2^logn+2^(logn-1)+...+2+1 ~= n,所以时间复杂度为O(n)。
- 空间复杂度
空间复杂度与系统堆栈有关,系统栈需要记住每个节点的值,所以空间复杂度为O(n)。
- 优缺点
- 优点
算法描述简单,系统维护栈,自己不用考虑空间的管理。遍历方式单一,不利于后续的扩展操作。
- 缺点
遍历方式单一,不利于后续的扩展操作。便于在遍历时进行其他操作,遍历操作灵活。
- 非递归
- 算法复杂度分析
- 时间复杂度
每个节点遍历一次,所以时间复杂度为O(n),n为结点数。
- 空间复杂度
由于不管是先序遍历还是中序遍历以及后序遍历,我们都需要利用一个辅助栈来进行每个节点的存储打印,所以每个节点都要进栈和出栈,不过是根据那种遍历方式改变的是每个节点的进栈顺序,空间复杂度为O(n),n为结点数。
- 优缺点
- 优点
便于在遍历时进行其他操作,遍历操作灵活。
- 缺点
算法较复杂,自己维护栈易出错。
/*-------------------------------------*/
- 遍历操作的应用
- 先序创建二叉树
- 算法分析
首先读入数据,如果数据不是终止符则申请空间创建根节点,如果是终止符则创建空树,接着先序创建左子树,然后先序创建右子树。
- 代码实现
Status CreateBiTree(BiTree& T)
{
ElemType ch;
scanf("%c", &ch);
if (ch == '#') T = NULL;//空指针标志
else
{
if ((T = (BiTree)malloc(sizeof(BiTNode))) == NULL)//创建失败,返回错误值
{
exit(OVERFLOW);
}
else
{
T->data = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
return OK;
}
//先序创建一棵二叉树,由指针T指向其根结点的指针
- 求二叉树中叶子结点数目
- 算法分析
通过可变参数i传出叶子节点的数量,i初始化为0。如果树不为空,判断是否时叶子节点,如果是叶子,计数加一。然后对左子树计数,对右子树计数。
- 代码实现
Status POLeafNodeNum(int& i, BiTree& T)//通过可变参数i传出叶子节点的数量,i初始化为0
{
if (T != NULL)//T为空时结束
{
if (T->lchild == NULL && T->rchild == NULL)//递归的出口、叶子节点的判断依据:无左右孩子
i++;
POLeafNodeNum(i, T->lchild);//对左子树计数
POLeafNodeNum(i, T->rchild);//对右子树计数
}
return OK;
}
// 求二叉树中叶子结点的数目
- 求二叉树深度
- 算法分析
空树的深度为0,书的深度为左右子树深度的最大值加一。
- 代码实现
int depth(BiTree T)
{
if (T == NULL) return 0;//空树的深度为零
int depthl, depthr;
depthl = depth(T->lchild);
depthr = depth(T->rchild);
return (depthl > depthr ? depthl : depthr) + 1;//树的深度为左右子树的深度最大值加1
}
//利用遍历求二叉树的深度
- 判断二叉树是否相似
- 算法分析
T1 是空树,T2是空树,则相似
T1 是空树,T2不是空树则不相似
T1 是非空树,T2是空树,则不相似
T1、T2 是非空树,左右子树相似则相似,否则不相似
- 代码实现
Status SimilarTree(BiTree& T1, BiTree& T2)
{
if (T1 == NULL)
{
if (T2 == NULL) return TRUE;// T1 是空树,T2是空树,则相似
else return FALSE;// T1 是空树,T2不是空树则不相似
}
else
{
if (T2 == NULL) return FALSE;// T1 是非空树,T2是空树,则不相似
else // T1、T2 是非空树
{
if (SimilarTree(T1->lchild, T2->lchild) && SimilarTree(T1->rchild, T2->rchild))
return TRUE;//左右子树相似则相似
else
return FALSE;
}
}
}
//判断两棵二叉树是否相似
三叉链表
typedef struct TriTNode{
ElemType data;
struct TriTNode *lchild,*rchild,*parent;
}TriTNode,*TriTree;
//增加了指向双亲节点的指针
目的
无法直接得到结点在任意序列中的前驱和后继信息,这种信息只能在遍历的动态过程中才能得到
保存这种在遍历过程中得到的信息:
(1)在每个结点上增加两个指针域fwd和bkwd,分别指向结点在按照某种顺序遍历时得到的前驱和后继。
缺点:存储密度低
(2)在有n个结点的二叉链表中必定存在n+1个空链域,可用它们存放结点的前驱和后继。
若结点有左子树,lchild指向左孩子,否则指向其前驱
若结点有右子树,rchild指向右孩子,否则指向其后继
充分利用二叉链表的空指针域,保存动态遍历过程中得到的结点前驱和后继的信息
术语
线索
指向结点前驱或后继的指针
线索二叉树
加上线索的二叉树
线索化
对二叉树以某种次序遍历使其变为线索二叉树的过程
树的双亲表示法
实现
用结构数组存放树的结点,每个分量含两个域:
数据域:存放结点本身信息
双亲域:指示本结点的双亲结点在数组中位置,根节点为-1
特点
找孩子节点需要遍历整个数组。
找双亲容易,找孩子困难
树的多重链表表示
实现
多重链表:每个结点有多个指针域,分别指向其孩子结点。
特点
1.结点同构:结点的指针个数相等,为树的度D.操作简便,浪费空间
2结点不同构:结点指针个数不等,为该结点的度d.链表中无空指针,无空间浪费,但操作不便
树的孩子链表表示
实现
每个结点的孩子组织成一个单链表,用结构数组存放树的结点,每个分量含两个域:
数据域:存放结点本身信息
指针域:指向每个孩子链表的首元结点
(类似有向图的邻接表存储)
特点
便于涉及孩子结点操作的实现
不适用于求结点的双亲
树的孩子兄弟表示法
实现
树的孩子兄弟表示法: 链表中结点的两个指针域分别指向该结点的第一个孩子和下一个兄弟。
二叉链表存储
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode,*CSTree;
- 特点
优点:
易于找结点孩子
便于导出树与二叉树的转换关系
缺点:
不易查找结点的双亲
破坏了树的层次
- 转化方法
- 树->二叉树
步骤1:加线-在兄弟之间加一连线
步骤2:抹线-对每个结点,除了其第1个孩子外,去除其与其余孩子之间的关系
步骤3:旋转-以树的根结点为轴心,将整树顺时针转45°
- 二叉树->树
步骤1:加线-若p结点是双亲结点的左孩子,将p的右孩子以及沿右分支找到的所有右孩子与p的双亲连线。
步骤2:抹线-抹掉原二叉树中双亲与右孩子之间的连线
步骤3:将结点按层次排列,形成树结构
- 遍历
- 先根遍历
若树不空,先访问根结点,再依次先根遍历各棵子树。
对应二叉链表的先序遍历。
- 后根遍历
若树不空,先依次后根遍历各棵子树,再访问根结点
对应二叉链表的中序遍历
- 层次遍历
若树不空,自上而下自左至右访问树中每个结点
森林&二叉树
森林和二叉树的转换规则
森林->二叉树
设森林F={T1,T2,……Tm},二叉树B=(root,LBT,RBT)
森林转化成二叉树的规则
若F为空(m = 0),B为空;
若F不空(m ≠0),
B的根root(B)是F中第一棵树T1的根root(T1);
左子树LBT从T1根结点的子树森林 {T11, T12, …, T1m }转换而来;
右子树RBT是从森林F’={T2, T3, …, Tn} 转换而来。
二叉树->森林
二叉树转换为森林的规则
若B为空, F为空;
若B非空,
则F中第一棵树T1的根为二叉树的根root(B);
T1根的子树森林F1= (T11, T12, …, T1m)由B的左子树LBT转换而来;
F 中除 T1 外其余树组成的森林F’={ T2, T3, …, Tn } 由B的右子树 RBT 转换而来。
遍历
先序遍历(对应二叉树的先序遍历)
先根遍历森林中的每一棵树:
访问第一棵树的根。
先根遍历第一棵树中根结点的子树森林。
先根遍历除去第一棵树之后剩余的树构成的森林。
中序遍历(对应二叉树的中序遍历)
后根遍历森林中的每一棵树:
中序遍历第一棵树中根结点的子树森林。
访问第一棵树的根。
中序遍历除去第一棵树之后剩余的树构成的森林。
路径
从树中一个结点到另一个结点之间的分支。
路径长度
路径上的分支数目。
树的路径长度
从树根到每个结点的路径长度之和。
结点的带权路径长度
从该结点到树根的路径长度与结点上的权值的乘积。
树的带权路径长度WPL(叶子)
树中所有叶子结点的带权路径长度之和。
已知n个权值{w1,w2,…wn},构造一棵有n个叶子结点的二叉树,第i个叶子结点的权值是wi,则其中带权路径长度最小的二叉树称为赫夫曼树(Huffman tree)或最优二叉树。
Huffman树的构造
步骤1:根据给定的n个权值{w1, w2, …, wn},构造n棵二叉树的集合F = {T1, T2, …, Tn},Ti(1≤i≤n)只有一个带权值wi的根结点,其左、右子树均为空。
步骤2:在F中选取两棵根结点权值最小的二叉树,分别作为左、右子树构造一棵新二叉树。置新二叉树的根结点的权值为其左、右子树上根结点的权值之和。
步骤3: 在F中删去这两棵二叉树,把新的二叉树加入F 。
步骤4: 重复步骤2和步骤3 直到F中仅剩下一棵树为止。这棵树就是Huffman树。
赫夫曼树的存储结构
一棵有n个叶子结点的Huffman树共有2n-1个结点,可存储在长度为2n-1的一维数组中。
typedef struct
{
unsigned int weight;
unsigned int parent, lchild, rchild;
}HTNode,*HuffmanTree;
Status CreateHuffmanTree(HuffmanTree& HT, int* x, int n)
{
int *w=x;
int m, i;
HuffmanTree p;
if (n <= 1) return ERROR;
m = 2 * n - 1;
HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));//0号单元未用
for (p = HT + 1, i = 1; i <= n; ++i, ++p, ++w)
{
p->weight = *w;
p->lchild = 0;
p->rchild = 0;
p->parent = 0;
}
for (; i <= m; ++i, ++p)
{
p->weight = 0;
p->lchild = 0;
p->rchild = 0;
p->parent = 0;
}
int s1, s2;
//s1是最小的,s2是次小的
for (i = n + 1; i <= m; ++i)
{
Select(HT, i - 1, s1, s2);
HT[s1].parent = i; HT[s2].parent = i;
HT[i].lchild = s1; HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
return OK;
}
void Select(HuffmanTree HT, int n, int& s1, int& s2)
{
int min1 = 1000, min2 = 1000;
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0)
{
if (HT[i].weight <= min1)
{
min2 = min1;
min1 = HT[i].weight;
s2 = s1;
s1 = i;
}
else if (HT[i].weight > min1 && HT[i].weight <= min2)
{
min2 = HT[i].weight;
s2 = i;
}
else
{
continue;
}
}
else
{
continue;
}
}
}
类型
固定长度编码
每个被编码对象的码长相等。
优点:码长等长,易于解码;
缺点:被编码信息总码长较长;
不等长编码
每个被编码对象的码长不等。
优点:总码长较短;
缺点:不易解码,解码容易产生二义性
前缀编码
任一字符的编码不是其它字符编码的前缀
Huffman编码
码长最短的二进制前缀编码:以n种字符出现频率为权,设计一棵Huffman树,由此得到的编码称为Huffman编码
Status HuffmanCoding(HuffmanCode& HC, HuffmanTree HT, int n)
{
HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));//和Huffman树的叶子节点对应,0号下标不存储编码
char* cd;//临时存放编码
cd = (char*)malloc(n * sizeof(char));//有n个叶子节点的树的深度不超过n-1
cd[n - 1] = '\0';//最后一个字符存放字符串结尾标志
int start;//标记编码的起始下标
for (int i = 1; i <= n; ++i)//循环为每一个叶子编码
{
int f;//存放双亲下标
int c;//存放孩子下标
start = n - 1; //起始下标初始化为n-1
for (c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent)//从叶子节点向根节点
if (HT[f].lchild == c)
cd[--start] = '0';//左孩子为0,更新起始下标
else
cd[--start] = '1';//右孩子为1,更新起始下标
HC[i] = (char*)malloc((n - start) * sizeof(char)); //为编码分配空间
strcpy(HC[i], &cd[start]);//复制到已分配的空间
}//for
free(cd);
return OK;
} //HuffmanCoding
- Huffman解码算法
Status HuffmanDeCoding(HuffmanTree HT, char* decode, int n, char* key,char* ch)//decode是待解码字符串,n是字符个数,key是解码结果,ch是字符集合
{
int j = 0;
int k = 2 * n - 1;
for (int i = 0; i < strlen(key); i++)
{
if (key[i] == '0')
{
k = HT[k].lchild;
}
else
{
k = HT[k].rchild;
}
if (HT[k].lchild == 0 || HT[k].rchild == 0)
{
decode[j] = ch[k - 1];
j++;
k = 2 * n - 1;
}
}
decode[j] = '\0';
return OK;
}