树和二叉树

154 阅读18分钟

基本概念

非线性数据结构,以分支关系定义的层次结构

非空树的特点:

  • 有且仅有一个根结点
  • 没有后继的节点称为"叶子结点"(或终端结点)
  • 有后继的结点称为"分支结点"(或非终端结点)
  • 除了根结点外,任何一个结点都有且仅有一个前驱

结点之间的关系描述

  • 祖先结点
  • 子孙结点
  • 双亲结点(父结点)
  • 孩子结点
  • 兄弟结点
  • 堂兄弟结点
  • 路径:只能从上往下

结点、树的属性描述

  • 深度:结点的层次--从上往下数
  • 高度:从下往上数
  • 树的高度(深度):总共多少层
  • 结点的度:有几个孩子(分支),非叶子结点的度>0,叶子结点的度=0
  • 树的度:各结点的读的最大值

有序树、无序树

  • 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
  • 无序树:逻辑上看,树中结点的各子树从左至右是无次序的,可以互换

森林

  • 森林是m(m≥0)课互不相交的树的集合
  • 森林和树的相互转换

常见考点

  • 结点数 = 总度数 + 1
  • 度为m的树和m叉树的区别
  • 度为m的树第i层至多有【m的(i-1)次方】个结点(i≥1),m叉树第i曾至多有这些
  • 高度为h的m差树至多有【(m^h-1)/m-1】个结点
  • 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有【h+m-1】个结点
  • 具有n个结点的m叉树的最小高度为【logm(n(m-1)+1)】

基本操作

//definition给出树T的定义
//cur_e是T中某个结点


InitTree(&T):构造空树T
DestroyTree(&T):销毁树T
CreateTree(&T,definition):按definition定义构造树T
ClaerTree(&T):将树T清为空树
TreeEmpty(T):若树T为空树,则返回TRUE,否则FALSE
TreeDepth(T):返回树的深度
Root(T):返回树的根
Value(T,cur_e):返回cur_e的值
Assign(T,cur_e,value):结点cur_e赋值给value
Parent(T,cur_e):若cur_e是T的非根结点,则返回它的双亲,否则函数值为"空"
LeftChild(T,cur_e):若cur_e是T的非叶子结点,则返回它的最左孩子,否则返回"空"
RightSibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则函数值为"空"
InsertChild(T,cur_e):插入c为T中p指结点的第i个子树
DeleteChild(&T,&p,i):删除T中p所指结点的第i个子树
TraverseTree(T,Visit()):按某种次序对T的每个结点调用函数visit()一次且多次,一旦visit()失败,则操作失败

二叉树

基本概念

二叉树是n(n≥0)个结点的有限集合

  • 每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)
  • 二叉树的子树有左右之分,其次序不能任意颠倒

满二叉树

  • 一颗高度为h,且含有2^h-1个结点的二叉树
  • 只有最后一层有叶子结点
  • 不存在度为1的结点
  • 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为i/2(向下取整)

完全二叉树

  • 概念:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应
  • 只有最后两层可能有叶子结点
  • 最多只有一个度为1的结点
  • 同满二叉树的i编号
  • i≤n/2为分支结点,i≥n/2为叶子结点
  • 如果某结点只有一个孩子,那么一定是左孩子

二叉排序树
一颗二叉树或者是空二叉树,或者是具有如下性质的二叉树:

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字
  • 左子树和右子树又各是一颗二叉排序树

平衡二叉树

树上任一结点的左子树和右子树的深度之差不超过1

常见考点

  • 设非空二叉树中读为0、1、2的结点个数分别为n₀、n₁、n₂,则n₀=n₂+1(叶子结点比二分支结点多一个)
  • 完全二叉树的常考性质
    • 具有n个结点的完全二叉树的高度h为log₂(n+1)或log₂n+1
    • 对于完全二叉树,可以由结点数n推出度为0、1和2的节点个数为n₀、n₁和n₂

基本操作

InitBiTree(&T):构造空二叉树T
DestroyBiTree(&T):销毁二叉树T
CreateBiTree(&T,definition)
CreateTree(&T,definition):按definition定义构造二叉树T
ClaerBiTree(&T):将树T清为空树

BiTreeEmpty(T):若树、二叉T为空树,则返回TRUE,否则FALSE
BiTreeDepth(T):返回树的深度
Root(T):返回树的根
Value(T,e):返回e的值
Assign(T,&e,value):结点e赋值给value
Parent(T,e):若cur_e是T的非根结点,则返回它的双亲,否则函数值为"空"

LeftChild(T,e):返回e的左孩子,若e无左孩子,则返回"空"
RightChild(T,e):返回e的右孩子,若e无右孩子,则返回"空"
LeftSibling(T,e):返回e的左兄弟,若e是T的左孩子或无左兄弟,则返回"空"
RightSibling(T,e):返回e的右兄弟,若e是T的右孩子或无右兄弟,则返回"空"

InsertChild(T,p,LR,c):根据LR为0或1,插入c为T中所指结点的左或右子树。p所指节点的原有左或右子树则成为c的右子树
DeleteChild(T,p,LR):根据LR为0或1,删除T中p所指结点的左或右子树

PreOrderTraversers(T,Visit()):先序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败
InOrderTraverse(T,Visit()):中序遍历T
PostOrderTraverse(T,Visit()):后序遍历
LevelOrderTraverse(T,Visit()):层序遍历

存储结构

顺序存储

一定要把二叉树的结点编号与完全二叉树对应起来,只适合存储完全二叉树

#define MaxSize 100
struct TreeNode{
    ElemType value;  //结点中的数据元素
    bool isEmpty;    //结点是否为空
}

/*
    TreeNode t[MaxSize];
    定义一个长度为MaxSize的数组t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点  
    可以让第一个位置孔雀,保证数组下标和结点编号一致
    
    
    初始化时所有结点标记为空
    for(int i = 0;i <MaxSize;i++){
        t[i].isEmpty = true;
    }
    
    基本操作:
        i的左孩子:2i
        i的右孩子:2i+1
        i的父结点:i/2
        i所在层次:log₂(n+1)或log₂n+1
*/

链式存储

找到指定节点p的左右孩子简单,找父结点困难

typedef struct BiTNode{
    ElemType data;      //数据域
    struct BiTNode *lchild,*rchild;//左、右孩子指针
}BiTNode,*BiTree;

//n个结点的二叉链表共有n+1个空链域

基本操作

遍历:按照某种次序把所有结点都访问一遍

二叉树的递归特性:

  • 要么是空二叉树
  • 要么是由“根+左+右”组成的二叉树
//先序遍历
void PreOrder(BiTree T){
    if(T != NULL){
        visit(T);
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}
//中序遍历
void InOrder(BiTree T) {
    if (T != NULL) {
	InOrder(T->lchild);
	visit(T);
	InOrder(T->rchild);
    }
}
//后序遍历
void PostOrder(BiTree T) {
    if (T != 0) {
        PostOrder(T->lchild);
	PostOrder(T->rchild);
	visit(T);
}

//求树的深度
int treeDepth(BiTree T) {
    if (T == 0)
	return 0;
    else {
	int l = treeDepth(T->lchild);
	int r = treeDepth(T->rchild);
	//树的深度 = Max(l,r) + 1
	return l > r ? l + 1 : r + 1;
    }
}

/*
    最简单的visit()函数是:
    Status PrintElement(TElemTyppe e){ //输出元素e的值
        printf(e);                     //实际使用时加格式串
        return OK;
    }
*/

二叉树的层序遍历

  • 初始化一个辅助队列
  • 根结点入队
  • 若队列非空,则队头结点出队,访问该结点,并将其左右孩子插入队尾(如果有的话)
  • 重复上一步直至队列为空
//链队列结点
typedef struct LinkNode{
    BiTNode * data;
    struct LinkNode* next;
}LinkNode;

typedef struct{
    LinkNode *front,*rear;		//队头队尾
}LinkQueue;


void LeveOrder(BiTree T) {
    LinkQueue(Q);
    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);	//右孩子入队
    }
}

线索二叉树

作用

在有n个结点的二叉链表中必定存在n+1个空链域,利用这些空链域来存放结点的前驱和后继的信息。
规定:

  • 若结点有左子树,则其lchild域指针指示其左孩子,否则令lchild域指示其前驱
  • 若结点有右子树,则其rchild域指针指示其右孩子,否则令rchild域指示其前驱
  • 避免混淆,增加两个标志域LTag,RTag
  • "对于相应的tag为1的结点来说,找相应的前驱和后继非常方便"
  • "对于存在左右孩子的结点,如何找前驱和后继呢?"
  1. 线索链表:以这种结点结构构成的二叉链表作为二叉树的存储结构
  2. 线索:指向结点前驱和后继的指针
  3. 线索二叉树:加上线索的二叉树
  4. 线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程

线索二叉树的存储结构

//线索二叉树
typedef struct ThreadNode {
    ElemType data;
    struct ThreadNode* lchild, * rchild;
    int ltag, rtag;		//左、右线索标志
}ThreadNode,*ThreadTree;

基本操作

中序前驱

//辅助全局变量,用于查找结点p的前驱
BiTNode *p;           //p指向目标结点
BiTNode *pre = NULL;  //指向当前访问结点的前驱
BiTNode *final = NULL;//用于记录最终结果

//土方法:在中序遍历的过程中使用pre指向当前结点的前驱结点
void findPre(BiTree T){
    if (T != NULL){
        findPre(T->lchild);
        visit(T);
        findPre(T->rchild);
    }
}

void visit(BiTNode* q){
    if (q == p1)	//当前访问结点刚好是结点p
        final = pre1;	//找到p的前驱
    else
        pre1 = q;	//pre指向当前访问的结点
}


//中序遍历二叉树,一边遍历一边线索化
ThreadNode *pre = NULL;

void InThread(ThreadTree T){
    if (T != NULL){
        InThread(T->lchild);
        threadvisit(T);
        InThread(T->rchild);
    }
}
void threadvisit(ThreadNode* q) {
    if (q->lchild == NULL) {		//左子树为空,建立前驱线索
        q->lchild = pre;
        q->ltag = 1;
    }
    if (pre != NULL && pre->rchild == NULL) {
        pre->rchild = q;		//建立前驱结点的后继线索
        pre->rtag = 1;
    }
    pre = q;
}

//中序线索化二叉树T
void CreateInThread(ThreadTree T) {
    ThreadTree pre = NULL;
    if (T != NULL){             //非空二叉树才能线索化
        InThread(T, pre);       //中序线索化二叉树
        pre->rchild = NULL;	//处理遍历的最后一个结点
        pre->rtag = 1;
        
        /*
            中序遍历的最后一个结点的右孩子指针必为空
            if(pre->rchild == NULL)
                pre->rtag = 1;
        */
    }
}



//王道
void InThreadWD(ThreadTree p, ThreadTree &pre){
    if (p != 0){
        InThreadWD(p->lchild,pre);
        if (p->lchild == NULL){
            p->lchild = pre;
            p->ltag = 1;
        }
        if (p->rchild == NULL && pre != NULL){
            p->rchild = p;
            p->rtag = 1;
        }
        pre = p;	            //标记当前结点成为刚刚访问过的结点
        InThreadWD(p->rchild, pre); //递归,线索化右子树
    }
}

先序线索化

void PreThread(ThreadTree p,ThreadTree &pre){
    if (p != NULL) {
        //Previsit(T);
        if (p->lchild = NULL) {		//左子树为空,建立前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if (pre != NULL && pre->rchild == NULL){
            pre->rchild = p;		//建立前驱线结点的后继线索
            pre->rtag = 1;
        }
        pre = p;
        if(p->ltag == 0)		//lchild 不是前驱线索
            PreThread(p->lchild,pre);
        PreThread(p->rchild,pre);
    }
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T){
    ThreadTree pre = NULL;
    if (T != NULL) {
        PreThread(T, pre);
    if (T->rchild == NULL)	//处理遍历的最后一个结点
        pre->rtag = 1;
    }
}

后序线索化

void PostThread(ThreadTree p,ThreadTree &pre) {
    if (p != NULL) {
        PostThread(p->lchild,pre);
        PostThread(p->rchild,pre);
        //visit()
        if (p->lchild == NULL) {
            p->lchild = pre;
            p->ltag = 1;
        }
        if (pre != NULL && pre->rchild == NULL) {
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
    }
}
void CreatePostThread(ThreadTree T) {
    ThreadTree pre = NULL;
    if (T != NULL) {
        PostThread(T, pre);
        if (pre->rchild = NULL)
            pre->rtag = 1;
    }
}

线索二叉树找前驱/后继

中序线索二叉树

找后继

/*
    1、若 p->rtag == 1,则next = p->rchild
    2、若p->rtag == 0,【左中右】该点存在右孩子,则后继为右子树的最左结点
*/
//找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode* Firstnode(ThreadNode* p) {
    //循环找到最左下结点
    while (p->rtag == 1)
        p = p->rchild;
    return p;
}
//
ThreadNode* Nextnode(ThreadNode* p){
    if (p->rtag == 0)
        return Firstnode(p->rchild);
    else
        return p->rchild;
}


//对中序线索二叉树进行中序遍历(利用线索实现的非递归运算)--O(1)
void Inorder(ThreadNode *T){
    for(ThreadNode *p = Firstnode(T);p != NULL;p = Nextnode(p))
        visit(p);
}

找前驱

/*
    找指定结点*p的中序前驱pre
    1、若p->ltag == 1,则pre = p->lchild
    2、若p->ltag == 0,【左根右】该点存在左孩子,则前驱为左子树的最右结点
*/
//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode* Lastnode(ThreadNode* p){
    //循环找到最右下结点(不一定是叶结点)
    while (p->rtag == 0)
        p = p->rchild;
    return p;
}
//找前驱
ThreadNode* Prenode(ThreadNode* p){
    if (p->ltag == 0)
        return Lastnode(p->lchild);
    else
        return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode* T){
    for (ThreadNode* p = Lastnode(T); p != NULL; p = Prenode(p))
        visit(p);
}

先序线索二叉树

找先序后继

/*
    1、若p->rtag == 1,则p->next = p->rchild;
    2、若p->rtag == 0,【根左右】
        如果有左孩子,则先序后继为左孩子
        如果无左孩子,则先序后继为右孩子
*/

ThreadNode* Nextnode(ThreadNode *p){
    if(p->rtag == 1)
        p->next = p->rchild;
    if(p->rtag == 0){
        if(p->ltag == 0)
            return p->lchild;
        else
            return p->rchild;
    /*
        if(p->ltag == 0)
            return p->next = p->lchild;
        else
            return p->next = p->rchild;
    */
}

找先序前驱

/*
    【根左右】p的左右孩子结点在先序线索二叉树中无论如何都只能是p的后继
    不可能在左右子树中找到前驱
        方法1:从头开始先序遍历
        方法2:如果能找到p的父结点,则根据父结点进行找后继的操作得到p的前驱
            Ⅰ、p是左孩子---p的父结点即为其前驱
            Ⅱ、p是右孩子且左兄弟为空---p的父结点即为其前驱
            Ⅲ、p为右孩子且左兄弟非空---左兄弟子树最后一个被先序遍历的结点(左兄弟子树的最右结点)
*/

TreeNode * findPrePre(TreeNode *p){
    if (p->ltag == 1)
        return p->lchild;
    //如果p有左孩子,pre--三叉链表父结点
    TreeNode *pre = p->pre;
    if (!pre)
        return pre;
    if (pre->lchild == p)
        return pre;
    pre = pre->lchild;
    while (pre->rtag == 0)
        pre = pre->rchild;
    return pre;
}

后序线索二叉树

找后序前驱

/*
    若p->ltag == 1,则pre = p->lchild;
    若p->ltag == 0,【左右根】
        有右孩子,后序前驱为右孩子
        无右孩子,后序前驱为左孩子
*/

ThreadNode *findBackPre(TreeNode *p){
    if(p->rtag == 0)
        return p->rchild;
    else
        return p->lchild;
}

找后序后继

/*
    若p->rtag == 1,则next = p->rchild
    若p->rtag == 0,必有右孩子,【左右根】后序遍历中左右子树只可能是前驱不可能是后继
        方法1:从头开始后序遍历
        方法2:如果能找到父结点,则根据父结点进行找后继的操作得到p的后继
            Ⅰ、p是右孩子---父结点为后继
            Ⅱ、p是左孩子且右兄弟树为空---父结点为后继
            Ⅲ、p是左孩子且右兄弟树不空---右兄弟子树中第一个被后序遍历的结点(右兄弟子树的最左结点)
            Ⅳ、如果p是根节点,则p没有后继结点
        
*/

TreeNode *findBackBack(TreeNode *p){
    if(p->rtag ==1)
        return p->rchild;
    TreeNode *pre = p->pre;
    if(!pre || pre->rchild == p || pre->rtag == 1)
        return pre;
    pre = pre->rchild;
    while(pre->ltag == 0)
        pre = pre->lchild;
    while(pre->rtag == 0)
        pre = pre->rchild;
    return pre;
}

逻辑结构

双亲表示法(顺序存储)

  • 每个结点中保存指向双亲的"指针"
  • 根结点固定存储在0,-1表示没有双亲
  • 优:查指定结点的双亲很方便
  • 缺:查指定结点的孩子只能从头遍历
  • 空数据导致遍历更慢
typedef struct {	//树的结点定义
    ElemType data;	//数据元素
    int parent;		//双亲位置域,int型变量,其实是双亲结点在数组中存放的位置下标
}PTNode;
typedef struct {	//树的类型定义
    PTNode nodes[MAX_TREE_SIZE];//双亲表示
    int n;		//结点数
}PTree;


/*
    增:新增数据元素无需按逻辑上的次序存储,直接添加即可
    删:删除后更改PTree中的结点数 n-1
        PLAN1:直接将parent设置为-1
        PLAN2:将尾部数据移动,填充这个空白
        【注】若删除的并非叶子结点,则需要找到删除结点的子节点一并删除
*/

孩子表示法(顺序+链式存储)

  • 顺序存储各个结点,每个结点中保存孩子链表头指针
struct CTNode{
    int child;           //孩子结点在数组中的位置
    struct CTNode *next;//下一个孩子
};
typedef struct{
    ElemType data;
    struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
    CTBox nodes[MAX_TREE_SIZE];
    int n,r;    //结点数和根的位置
}CTree;

孩子兄弟表示法(链式存储)

  • 可以使用熟悉的二叉树操作来处理树
  • 树与二叉树的相互转换,本事是用孩子表示法存储树
  • 森林中各个树的根结点之间视为兄弟关系
typedef struct CSNode{
    ElemType data;   //数据域
    struct CSNode *firschild;   //第一个孩子
    struct CSNode *nextsibling; //右兄弟指针
}CSNode,*CSTree;

//在存储角度上,和二叉链表相同,将firschild看作左指针,nextsibling看作右指针

树、森林的遍历

树的遍历

先根遍历

若树非空,先访问根结点,再依次对每棵子树进行先根遍历

void PreOrder(TreeNode *R){
    if(R != NULL){
        visit(R);     //访问根结点
        while(R还有下一个子树T)
            PreOrder(T);    //先根遍历下一棵子树
    }
}

后根遍历---深度优先遍历

若树非空,先依次对每棵子树进行后根遍历,最后再访问根节点

void PostOrder(TreeNode *R){
    if(R != NULL){
        while(R还有下一个子树T)
            PostOrder(T);    //先根遍历下一棵子树
        visit(R);     //访问根结点
    }
}

层次遍历(用队列实现)---广度优先遍历

  • 若树非空,则根结点入队
  • 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
  • 重复上一步直到队列为空

森林的遍历

森林是m(m≥0)棵互不相交的树的集合。每棵树去掉根结点后,其各个子树又组成森林

先序遍历

若森林非空

  • 访问森林中第一棵树的根结点
  • 先序遍历第一棵树中根结点的子树森林
  • 先序遍历除去第一棵树之后剩余的树狗曾的森林

效果等同于依次对各个树进行先根遍历

中序遍历

若森林非空

  • 中序遍历森林中第一棵树的根结点的子树森林
  • 访问中第一棵树的根结点
  • 中序遍历除去第一棵树之后剩余的树狗曾的森林

效果等同于依次对各个树进行后根遍历==【依次对二叉树的中序遍历】

中序遍历

若森林非空

  • 中序遍历森林中第一棵树的根结点的子树森林
  • 访问中第一棵树的根结点
  • 中序遍历除去第一棵树之后剩余的树狗曾的森林

效果等同于依次对各个树进行后根遍历==【依次对二叉树的中序遍历】

哈夫曼树(最优二叉树)

  • 结点的权:有某种现实含义的数值(如:表示结点的重要性等)
  • 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该节点上权值的乘积
  • 树的带权路径长度:树中所有叶结点的带权路径长度之和
  • 构造哈夫曼树
    • 构造森林
    • 构造新结点,从森林中选取权值最小的两棵树作为左右子树,将这个树放入森林
    • 循环
    • 直至剩一个
  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
  2. 哈夫曼树的结点总数为2n-1
  3. 哈夫曼树中不存在度为1的结点
  4. 哈夫曼树并不唯一,但WPL必然相同且为最优

哈夫曼编码

将字符频次作为字符结点权值,构造哈夫曼树,即可得哈夫曼编码,可用于数据压缩

  • 固定长度编码--每个字符用相等长度的二进制位表示
  • 可变长度编码--允许对不同字符用不等长的二进制为表示
  • 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
typedef struct{
    unsigned int weight;
    unsigned int parent,lchild,rchild;
}HTNode,*HuffmanTree;
typedef char * * HuffmanCode;
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n){
    //w存放n个字符的权值(均>0),构造哈夫曼树HT,并求出n个字符的哈夫曼编码HC
    if(n <= 1)
        return
    m = 2 * n - 1;
    HT = (HuffmanTree)malloc((m+1) * sizeof(HYNode));  //0号单元未用
    for(p=HT+1,i=1;i<=n;++i,++p,++w)
        *p = {*w,0,0,0};
    for(; i<=m;++i,++p)
        *p = {0,0,0,0};
    for(i=n+1;i<=m,++i){    //建哈夫曼树
    //在HT[1...i-1]选择parent为0且weight最小的两个结点,其序号分别为s1和s2;
        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;
    }
    //从叶子到根逆向求每个字符的哈夫曼编码
    HC = (HuffmanCode)malloc((n+1) * sizeof(char *)); //分配n个字符编码的头指针向量
    cd = (char *)malloc(n*sizeof(char));  //分配求编码的工作空间
    cd[n-1] = "\0";     //编码结束符
    for(i=1;i<=n;++i){  //逐个字符求哈夫曼编码
        start = n - 1;  //编码结束符位置
        for(c=i,f=HT[s1].parent;f!=0;c=f,f=HT[f].parent)   //从叶子到根逆向求编码
            if(HT[f].lchild == c)
                cd[--start] = "0";
            else
                cd[--start] = "1";
        HC[i] = (char *)malloc((n-start) * sizeof(char));  //为第i个字符编码分配空间
        strcpy(HC[i],&cd[start]);       //从cd赋复制编码(串)到HC
    }
    free(cd);   //释放工作空间
}//HuffmanCoding