文章目录
树
之前遇到的链表、栈、队列和串都是一对一的数据结构,现在着重研究一下这种一对多的数据结构——“树”。
1. 树的定义
树 是n(n>=0)个结点的有限集。
n = 0称为空树。
在任意一颗非空树中:
1. 有且仅有一个特定的称为根的结点;
2. 当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集T1、T2···Tm,其中每一个集合本身又是一棵树,称为根的子树。
1.1 结点的分类
- 度:结点拥有的子树数;
- 叶结点或终端结点:度为0的结点;
- 非终端结点或分支结点:度不为0;
- 内部结点:除根结点之外的分支结点;
- 树的度是树内各结点的度的最大值。
1.2 结点间的关系
- 孩子:结点的子树的根;
- 双亲:父母同体;
- 兄弟:同一个双亲的孩子;
- 祖先:从根到该结点所经分支上的所有节点;
- 子孙:以某结点为根的子树中的任一结点。
1.3 树的其他相关概念
- 结点的层次从根开始定义起,根为第一层,根的孩子为第二层,依此类推;
- 双亲在同一层的结点互为堂兄弟;
- 树中结点的最大层次为树的深度或高度;
- 如果将树中结点的各子树看成从左向右是有次序的,不能互换的,则称该书为有序树,否则为无序树;
- 森林是m(m >= 0)棵互不相交的树的集合。
总结:线性结构与树结构的:
线性结构 | 树结构 |
---|---|
第一个数据元素:无前驱 | 根结点:无双亲,唯一 |
最后一个数据元素:无后继 | 叶结点:无孩子,可以多个 |
中间元素:一个前驱一个后继 | 中间结点:一个双亲多个孩子 |
2. 树的抽象数据类型
Operation
InitTree(*T):构造空树T
DestoryTree(*T):销毁树T
CreateTree(*T,definition):按definition中给出树的定义来构造树
ClearTree(*T):若树T存在,则将树T清位空树
TreeEmpty(T):若T为空树,返回true,否则返回false
TreeDepth(T):返回T的深度
Root(T):返回T的根结点
Value(T,cur_e):cur_e是树T中的一个结点,返回此结点的值
Assign(T,cur_e,value):给树T的cur_e赋值为value
Parent(T,cur_e):若cur_e是树T的非根结点,则返回它的双亲,否则返回空
LiftChild(T,cur_e):若cur_e是树T的非根结点,则返回它的最左孩子,否则返回空
RightSibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则返回空
InsertChild(*T,*p,i,c):其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交, 操作结果为插入c为树T中p指结点的第i棵子树
DeleteChild(*T,*p,i):其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点 的第i棵子树
3. 树的存储结构
之前接触过顺序存储和链式存储,现在要将两者结合起来,就有了三种不同的表示法:双亲表示法、孩子表示法和孩子兄弟表示法。
3.1 双亲表示法
除了根节点外,其余每个结点都有且仅有一个双亲。
所以 双亲表示法 就是用一组连续存储空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在数组的位置。
//树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100
typedef int TElemType;//树结点的数据类型,目前暂定为整型
typedef struct PTNode//结点结构
{
TElemType data;
int parent;
}PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];//结点数组
int r,n;//根的位置和结点数
}PTree;
在双亲表示法存储结构中,我们可以根据结点的parent指针很容易的找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根;如果我们想知道结点的孩子是什么,则需要遍历整个结构。
3.2 孩子表示法
把每个结点的孩子结点排列起来,以单链表为存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成了一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示:
为此,需要设置两种结点结构:
- 孩子链表的孩子结点
其中,child是数据域,用来存储某个结点在表头数组中的下标;next是指针域,用来存储指向某结点的下一个孩子结点的指针
- 表头数组的表头结点
其中,data是数据域,存储某结点的数据信息;firstchild是头指针域,存储该结点的孩子链表的头指针
//树的孩子表示法结构定义
#define MAX_TREE_SIZE 100
typedef struct CTNode//孩子结点
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct//表头结构
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct//树结构
{
CTBox nodes[MAX_TREE_SIZE];//结点数组
int r,n;//根的位置和结点数
}CTree;
3.3 孩子兄弟表示法
任意一棵树, 它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
//树的孩子兄弟表示法结构定义
typedef struct CSNode{
TElemtype data;
struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;
4. 二叉树的定义
二叉树 是 n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
4.1 二叉树的特点
二叉树的特点有:
-
每个结点最多有两棵子树,所以二叉树**不存在度大于2的结点。**
-
左子树和右子树是有顺序的,次序不能任意颠倒。
-
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态:
- 空二叉树;
- 只有一个根结点;
- 根结点只有左子树;
- 根结点只有右子树;
- 根结点既有左子树又有右子树。
4.2 特殊二叉树
4.2.1 斜树
左斜树:所有结点都只有左子树;
右斜树:所有结点都只有右子树。
斜树的结点个数和二叉树的深度相同。
线性表结构可以理解为是树的一种极其特殊的表现形式。
4.2.2 满二叉树
满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
特点:
- 叶子只能出现在最下一层,出现在其他层就不可能达成平衡。
- 非叶子结点的度一定是2。
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。
4.2.3 完全二叉树
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i (1 <= i <=n) 的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同。
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。
完全二叉树的特点:
- 叶子结点只能出现在最下两层;
- 最下层的叶子一定集中在左部连续位置;
- 倒数二层,若有叶子结点,一定都在右部集中位置;
- 如果结点度为1,则该结点只有左孩子,即不存在右孩子。
5. 二叉树的性质
5.1 性质一
在二叉树的第i层上至多有2^(i-1)个结点(i>=1)。
5.2 性质二
深度为 k 的二叉树至多有2^k - 1个结点(k >= 1)。
5.3 性质三
对任何一棵二叉树T,如果其终端结点数 (叶子结点数) 为n0,度为2的结点数为n2,则 n0 = n2 + 1。
5.4 性质四
具有n个结点的完全二叉树的深度为[log 2 ^n] +1 ([x]表示不大于x的最大整数)。
5.5 性质五
如果对一棵有n个结点的完全二叉树(其深度为[log 2 ^n] +1 )的结点按照层序编号(从第一层到第
[log 2 ^n] +1层,每层从左到右),对任一结点 i (1 <= i <= n) 有:
- 如果i = 1,则结点 i 是二叉树的根,无双亲;如果i >1,则其双亲是结点[i/2];
- 如果2i > n,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子是结点 2i;
- 如果 2i+1>n,则结点 i 无右孩子;否则其右孩子是结点 2i+1.
6. 二叉树的存储结构
6.1 二叉树的顺序存储结构
用一维数组存储二叉树的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
但对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中,就会造成存储空间的浪费。
6.2 二叉链表
由于二叉树每个结点最多有两个孩子,所以为他设计一个数据域和两个指针域,如下图所示:
data 是数据域,lchild 和 rchild都是指针域,分别存放指向左孩子和右孩子的指针。
//二叉树的二叉链表结点结构定义
typedef struct BiTNode//结点定义
{
TElemType data;//结点数据
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
还可以再添加一个指向双亲的指针域,那样就称之为三叉链表。
7. 遍历二叉树
7.1 遍历原理
二叉树的遍历 是指从 根结点 出发,按照 某种次序 依次 访问二叉树中的 所有节点 ,使得 每个结点 被访问 一次且仅被 访问一次。
7.2 遍历方法
7.2.1 前序遍历
若二叉树为空,则空操作返回;
否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
//二叉树的前序遍历递归算法
void PreOrderTraverse(BiTree T)
{
if(T == NULL)
return;
printf("%c",T->data);//显示结点数据,可以更改为其他对结点操作
PreOrderTraverse(T->lchild);//再先序遍历左子树
PreOrderTraverse(T->rchild);//最后先序遍历右子树
}
7.2.2 中序遍历
若二叉树为空,则空操作返回;
否则从根结点开始(注意并不是先访问根结点),中序遍历根节点的左子树,然后是访问根结点,最后中序访问右子树。
//二叉树的中序遍历递归算法
void InOrderTraverse(BiTree T)
{
if(T == NULL)
return;
InOrderTraverse(T->lchild);//中序遍历左子树
printf("%c",T->data);//显示结点数据,可以更改为其他对结点的操作
InOrderTraverse(T->rchild);//最后中序遍历右子树
}
7.2.3 后序遍历
若二叉树为空,则空操作返回;
否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
//二叉树的后序遍历递归算法
void PostOrderTraverse(BiTree T)
{
if(T == NULL)
return;
PostOrderTraverse(T->lchild);//先后序遍历左子树
PostOrderTraverse(T->rchild);//再后序遍历右子树
printf("%c",T->data);//显示结点数据,可以更改为其他对结点操作
}
7.2.4 层序遍历
若二叉树为空,则空操作返回;
否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
void LevelOrder(BiTree T)
{
InitQueue(Q);//初始化辅助队列
BiTree p;
EnQueue(Q,T);//将根结点入队
while(!IsEmpty(Q))//队列不空则循环
{
DeQueue(Q,p);//队头结点出队
visit(p);//访问出队结点
if(p->lchild != NULL)
{
EnQueue(Q,p->lchild);//左子树不空,则左子树根结点入队
}
if(p->rchild != NULL)
{
EnQueue(Q,p->rchild);//右子树不空,则右子树根结点入队
}
}
}
8. 二叉树的建立
//按前序输入二叉树中结点的值(一个字符)
//#表示空树,构造二叉链表表示二叉树T
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch == '#')
*T = NULL;
else
{
*T = (BiTree) malloc (sizeof(BiTNode));
if(!*T)
exit (OVERFLOW);
(*T)->data = ch;//生成根结点
CreateBiTree(&(*T)->lchild);//构造左子树
CreateBiTree(&(*T)->rchild);//构造右子树
}
}
9. 线索二叉树
9.1 原理
遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个结点除外)都有一个直接前驱和直接后继。
传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱或后继。
对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1 条分支线数,也就是说,其实是存在2n- (n-1) =n+1个空指针域。
由此设想能否利用这些空指针来存放指向其前驱或后继的指针?这样就可以像遍历单链表那样方便地遍历二叉树。引入线索二叉树正是为了加快查找结点前驱和后继的速度。
指向前驱和后继的指针称为线索,
加上线索的二叉链表称为线索链表,
相应的二叉树就称为线索二叉树。
结点结构:
其中,
- ltag 为0时,指向该结点的左孩子,为1时指向该结点的前驱;
- rtag 为0时,指向该结点的右孩子,为1时指向该结点的后继。
因此,对于上图的二叉链表可以修改为下图:
9.2 线索二叉树的结构实现
结点结构:
struct ThreadNode
{
DataType data;
ThreadNode *lchild,*rchild;
int ltag,rtag;
}
9.2.1 中序线索链表的建立——构造函数
建立二叉链表 -----> 遍历二叉树,将空指针改为线索
伪代码:
-
建立二叉链表,将每个结点的左右标志置为0;
-
遍历二叉链表,建立线索;
-
如果二叉链表p为空,则空操作返回;
-
对p的左子树建立线索;
-
对根结点p建立线索;
-
若p没有左孩子,则为p加上前驱线索;
-
若pre没有右孩子,则为pre加上后继线索;
-
令pre指向刚刚访问的结点p;
-
-
-
对p的右子树建立线索。
//中序序列:D G B A E C F
//中序遍历二叉链表,p 为正在访问的结点,pre 为刚访问的结点
//最后p回溯到最早的结点,然后程序处理结束
void ThreadBiTree::inThread(ThreadNode *p)
{
static ThreadNode *pre = NULL;//**????pre定义为静态变量????**
if(p == NULL)
return;
inThread(p->lchild);//左子树线索化
if(p->lchild == NULL)//建立p的前驱线索
{
p->ltag = 1;
p->lchild = pre;
}
if(pre != NULL && pre->rchild == NULL)//建立pre的后继线索
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p;//pre指向p的前驱
inThread(p->rchild);//右子树线索化
}
pre 定义为静态变量 的原因:此递归程序需要保留上一次结果。
pre的其他定义方法:
- 定义为全局变量(C/C++语言)【不推荐】
- pre 定义成属性(C++/java语言)
9.2.2 中序线索链表查找后继
ThreadNode * ThreadBiTree::next(ThreadNode *p)
{
if(p->rtag == 1)//右标志位为1,可直接得后继结点
q = p->rchild;
else{ //右标志位为0,则要找到右子树最左下角的结点
q = p->rchild;
while(q->ltag == 0)//查找最左下结点的位置
q = q->lchild;
}
return q;
}
9.2.3 中序线索链表的遍历算法
void ThreadBiTree::inOrder()
{
ThreadNode *p;
if(root == NULL)
return;
p = root;
while(p->ltag == 0)//查找中序遍历的第一个结点p
p = p->lchild;
visit(p->data);//访问第一个结点
while(p->rchild != NULL)
{
p = next(p);
visit(p->data);
}
}
10. 二叉树遍历的非递归算法
10.1 前序遍历
二叉树前序遍历的非递归算法的关键:
在前序遍历完某结点的整个左子树后,如何找到该结点的右子树的根指针。
解决方法:
在访问完该结点后,将该结点的指针保存在栈中,以便以后能通过它找到该结点的右子树。
在前序遍历中,设要遍历二叉树的根指针为root,则有两种可能:
(1)若root != NULL,则表明?如何处理?
(2)若root = NULL,则表明?如何处理?
伪代码:
- 栈s初始化;
- 循环直到p为空且栈s为空;
- 当p不空时循环
- 访问p->data;
- 将指针p的值保存到栈中;
- 继续遍历p的左子树
- 如果栈s不空,则
- 将栈顶元素弹出至p;
- 准备遍历p的右子树。
- 当p不空时循环
void BiTree::preOrder()
{
Stack s;//建立栈s
p = root;//令p为根结点
while(p != NULL || !s.isEmpty())//当p为空且栈s为空时退出循 //环
{
while(p != NULL)//一直访问并移动指针p,直到遇到最左叶子的
//左指针,结束循环
{
visit(p->data);//访问根结点
s.push(p);//将指针的结点压入栈中
p = p->lchild;//遍历左子树
}
if(!s.isEmpty())//栈不为空
{
p = s.pop();//根结点(栈顶元素)出栈
p = p->rchild;//遍历右子树
}
}
}
10.2 中序遍历
void BiTree::inOrder()
{
Stack s;
p = root;
while(p != NULL || !s.isEmpty)//仅当p为空且栈为空时退出循环
{
while(p != NULL){//一直移动指针p,直到遇到最左叶子的左指针,结束循环
s.push(p);//将指针p的值压入栈中
p = p->child;//遍历左子树
}
if(!s.isEmpty()){
p = s.pop();//根结点出栈
visit(p->data);//访问根结点
p = p->child;//遍历右子树
}
}
}
10.3 后序遍历
在后序遍历过程中,结点要入两次栈,出两次栈:
- 第一次出栈:只遍历完左子树,该结点不出栈,利用栈顶结点找到它的右子树,准备遍历它的右子树;
- 第二次出栈:遍历完右子树,将该结点出栈,并访问它。因此,为了区别同一个结点的两次出栈,设置标志flag。
栈元素类型定义如下:
struct element{
BiNode *ptr;
int flag;//1表示第一次出栈,2表示第二次出栈
}
伪代码:
void BiTree::postOrder()
{
Stack s;
p = root;
while(p != NULL || !s.isEmpty()){//仅当p为空且栈为空时退出循环
if(p != NULL){//第一次入栈,访问左子树
elem.ptr = p;
elem.flag = 1;//标志flag为1,表示第一次入栈
s.push(elem);//第一次入栈
p = p->lchild;//访问左孩子
}
else{
elem = s.pop();//出栈
p = elem.ptr;//p指向当前要处理的点
if(elem.flag == 1){
//flag == 1,表示只访问过左子树,未访问右
elem.flag = 2;//标志flag为2,表示第二次入栈
s.push(elem);//第二次入栈
p = p->rchild;//访问右孩子
}
else{
//flag == 2,左右子树均访问过
visit(p->data);//访问该结点
p = NULL;//访问后,p赋为空,确保下次循环时继续出栈(回退到上一级)
}
}
}
}
11. 树、森林和二叉树的转换
11.1 树与二叉树之间的对应关系
11.1.1 树 转换为 二叉树
树:兄弟关系 -----> 二叉树: 双亲和右孩子
树:双亲和长子 -----> 二叉树:双亲和左孩子
1. 加线:兄弟加线;
2. 去线:保留双亲与第一个孩子连线,删去与其他孩子连线;
3. 层次调整:顺时针转动,使之层次分明。
转换前后:
树的前序遍历等价于二叉树的前序遍历;
树的后序遍历等价于二叉树的中序遍历;
11.2 森林转换为二叉树
- 将森林中的每棵树转换成二叉树;
- 从第二棵二叉树开始,依次把 后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子 ,当所有二叉树连起来后,此时所得到的二叉树就是由森林转换得到的二叉树。
11.3 二叉树转换为树或森林
- 加线:若某结点 x 是其双亲 y 的左孩子,则把结点x的右孩子、右孩子的右孩子、······,都与结点y用线连起来;
- 去线:删去原二叉树中所有的双亲结点与右孩子结点的连线;
- 层次调整:整理由1.2.步得到的树或森林,使之层次分明。
11.4 森林的遍历
- 前序(根)遍历:前序遍历森林即为前序遍历森林的每一棵树;
- 后序(根)遍历:后序遍历森林即为后序遍历森林的每一棵树;
12. 哈夫曼树
12.1 相关概念
- 叶子结点的权值:对叶子结点赋予的一个有意义的数量值;
- 二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点的权值的乘积之和,记为:
12.2 哈夫曼树的定义
给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
12.3 哈夫曼树的特点
- 权值越大的叶子结点越靠近根结点,权值越小的叶子结点越远离根结点;
- 只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点;
12.4 哈夫曼树的算法思想
-
初始化
由给定的n个权值{w1,w2,···,wn}构造n棵只有一个根结点的二叉树,从而得到一个二叉树集合F = {T1,T2,…,Tn};
-
选取与合并
在F中选取根结点的权值最小的两棵二叉树分别作为左、右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和;
-
删除与加入
在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中;
-
重复
重复2. 3 .两步。当集合F中只剩下一棵二叉树时。这棵二叉树便是哈尔曼树。
例:
W = {2,4,5,3} 哈尔曼树的构造过程
12.5 哈夫曼算法的存储结构
设置一个数组huffTree[2n-1]保存哈尔曼树中各点的信息,数组元素的结点结构:
- weight: 权值域,保存该结点的权值;
- lchild:指针域,结点的左孩子结点在数组中的下标
- rchild:指针域,结点的右孩子结点在数组中的下标
- parent:指针域,该结点的双亲结点在数组中的下标
为什么存储数组的元素个数为2n-1?
解答: 根据叶子结点数计算整棵哈尔曼树需要的结点数 :
****在一棵二叉树中,如果叶子结点数为n0,度为2的结点数为n2,则有:n0 = n2 + 1****
因此,有n2 = n0 - 1 。
在哈尔曼树中,只有出度为 0 和 2 的结点。
因此,n = n0 + n2 = n0 + (n0 -1) = 2 * n0 - 1
12.6 代码
伪代码:
- 数组huffTree初始化,所有元素结点的双亲、左右孩子都置为-1;
- 数组huffTree的前n个元素的权值置给定值w[n];
- 进行n-1次合并;
- 在二叉树集合中选取两个权值最小的根结点,其下标分别是i1,i2;
- 将二叉树i1,i2合并成一棵新的二叉树k
void HuffmanTree(HTNode huffTree[],int w[],int n)
{
for(int i = 0;i < 2*n - 1;i ++){//初始化
huffTree[i].parent = -1;
huffTree[i].lchild = -1;
huffTree[i].rchild = -1;
}
for(int i = 0;i < n; i++)//初始化前n个结点的权值
huffTree[i].weight = w[i];
for(int k = n;k < 2*n - 1;k++){//构建哈尔曼树
select(huffTree,k,i1,i2);//找到parent为-1的最小和次小的结点
huffTree[k].weight = huffTree[i1].weight + huffTree[i2].weight ;
huffTree[i1].weight = k;
huffTree[i2].weight = k;
huffTree[k].lchild = i1;
huffTree[k].rchild = i2;
}
}
13. 哈夫曼编码
编码:给每一个对象标记一个二进制位串来表示一组对象。例:ASCII,指令系统。
编码分类:
- 等长编码:表示一组对象的二进制位串的长度相等;
- 不等长编码:表示一组对象的二进制位串的长度不相等。
所以就有了前缀编码的概念。
前缀编码:一组编码中任一编码都不是其他任何一个编码的前缀。
前缀编码保证了在编码时不会有多种可能。
哈夫曼编码方案:
从叶子到根逆向求每个字符的哈夫曼编码。
void huffmanCoding(HTNode huffTree[],char* huffCode[],int n)
{
temp = new char[n];//定义工作空间,存储临时产生的编码串
temp[n-1] = '\0';
for(int i = 0;i < n;i ++){//遍历哈夫曼数组,生成哈夫曼编码
start = n - 1;
pos = i;//pos记录正在处理的当前位置
parent = huffTree[i].parent;//找到父结点
while(parent != -1){
if(huffTree[parent].lchild == pos)//判断当前是左孩子还是右孩子
temp[--start] = '0';
else
temp[--start] = '1';
pos = parent;//当前位置移到父结点
parent = huffTree[parent].parent;//更新父结点
}
huffCode[i] = new char[n - start];//建立哈夫曼编码实际需要的内存空间
strcpy(huffCode[i],&temp[start]);//临时存储的哈夫曼编码存储到huffCode中
}
delete temp;//s
}