(C++)数据结构课程笔记6/9 - 树和二叉树

92 阅读22分钟

§6 - 树和二叉树

1 - 树的定义

相关概念

树(Tree)是 n(n≥0) 个结点的有限集。当 n=0 时称为空树。在任意一棵非空树中,有且仅有一个称为根(Root)的结点,其余的结点可分为 m(m≥0) 个互不相交的有限集 T1,T2,…,Tm,其中每一个集合又成为一棵树,并且称为根的子树。同理,每一棵子树又可以分为若干个互不相交的有限集。

树的分类
  • 无序树
  • 有序树
基本术语
  • 结点(Node):包含一个数据元素及若干个指向其子树的分支
  • 度(Degree):结点的度是一个结点拥有的子树数目;树的度是一棵树上所有结点度的最大值
  • 叶子结点:度为零的结点
  • 分支结点:度大于零的结点
  • 结点之间的关系:孩子、双亲、兄弟、堂兄弟、祖先、子孙
  • 深度 / 结点的层次:从根结点到该结点所经过的路径长度加 1
  • 高度:从该结点向下到某个叶子结点所经过的最长路径长度加 1
  • 树的深度:树中叶子结点具有的最大深度
  • 树的高度:树中根结点具有的高度
延展概念

森林是 m(m≥0) 棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。所以任何一棵非空树是一个二元组 Tree = (root, F)(root 是根结点;F 是子树森林)。

抽象数据类型

ADT Tree {
	数据对象D:
		D是具有相同特性的数据元素的集合。
	数据关系R:
		若D为空集:
			则称为空树。
		否则:
        	1.在D中存在唯一的称为根的数据元素root;
        	2.当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一棵子集本身又是一棵符合本定义的树,称为根root的子树。
	基本操作:
		查找类
			Root(T)                       // 求根结点
			Value(T,e)                    // 求当前结点的元素值
			Parent(T,e)                   // 求当前结点的双亲结点
			LeftChild(T,e)                // 求当前结点的最左孩子
			RightSibling(T,e)             // 求当前结点的右兄弟
			TreeEmpty(T)                  // 判空
			TreeDepth(T)                  // 求深度
			TraverseTree(T,Visit())       // 遍历
		插入类
			InitTree(&T)                  // 初始化树
			CreateTree(&T,definition)     // 按定义构造树
			Assign(&T,e,value)            // 给当前结点赋值
			InsertChild(&T,&p,i,c)        // 将以c为根的树插入为结点p的第i棵子树
		删除类
			ClearTree(&T)                 // 清空树
			DestroyTree(&T)               // 销毁树
			DeleteChild(&T,&p,i)          // 删除结点p的第i棵子树
} ADT Tree

2 - 二叉树的定义与性质

相关概念

二叉树(Binary tree)是 n(n≥0) 个结点的有限集合。这个集合或是空集,或是由一个根结点以及两棵互不相交的左子树右子树所组成。左子树和右子树分别又是一棵二叉树。

抽象数据类型

ADT Binary tree {
	数据对象D:
		D是具有相同特性的数据元素的集合。
	数据关系R:
		若D为空集:
			则称为空树。
		否则:
        	1.在D中存在唯一的称为根的数据元素root;
        	2.当n>1时,其余结点可分为2个互不相交的有限集,称为根root的左子树和右子树,左子树和右子树本身又是一棵符合本定义的二叉树。
	基本操作:
		查找类
			Root(T)                       // 求根结点
			Value(T,e)                    // 求当前结点的元素值
			Parent(T,e)                   // 求当前节点的双亲结点
			LeftChild(T,e)
			RightChild(T,e)               // 求当前结点的孩子结点
			LeftSibling(T,e)
			RightSibling(T,e)             // 求当前结点的左右兄弟
			BiTreeEmpty(T)                // 判空
			BiTreeDepth(T)                // 求深度
			PreOrderTraverse(T,Visit())   // 先序遍历
			InOrderTraverse(T,Visit())    // 中序遍历
			PostOrderTraverse(T,Visit())  // 后序遍历
			LevelOrderTraverse(T,Visit()) // 层序遍历
		插入类
			InitBiTree(&T)                // 初始化二叉树
			CreateBiTree(&T,definition)   // 按定义构造二叉树
			Assign(&T,e,value)            // 给当前结点赋值
			InsertChild(&T,&p,LR,c)       // 将以c为根的二叉树插入为结点p的左或右子树
		删除类
			ClearBiTree(&T)               // 清空二叉树
			DestroyBiTree(&T)             // 销毁二叉树
			DeleteChild(&T,&p,LR)         // 删除结点p的左或右子树
} ADT Binary tree

重要性质

1.二叉树的第 i(i≥1) 层至多有 2i12^{i-1} 个结点

2.深度为 k(k≥1) 的二叉树至多含 2k12^k-1 个结点

3.若二叉树含有 n0 个叶子结点和 n2 个度为 2 的结点,则有关系式 n0=n2+1n_0=n_2+1

证明:

​ 结点总数 n=n0+n1+n2n=n_0+n_1+n_2

​ 分支总数 b=n1+2n2=n1b=n_1+2n_2=n-1

​ 得 n0=n2+1n_0=n_2+1

铺垫:二叉树的分类
  • 一般的二叉树

  • 满二叉树:深度为 k 且含有 2k12^k-1 个结点的二叉树

  • 完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应

4.具有 n 个结点的完全二叉树的深度为 log2n+1⌊log_2n⌋+1

5.若对含 n 个结点的完全二叉树从上至下、从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:

  • 若 i=1,则该结点是根结点,无双亲;否则其双亲结点编号是 ⌊i/2⌋
  • 若 2i>n,则该结点无左孩子;否则其左孩子结点编号是 2i
  • 若 2i+1>n,则该结点无右孩子;否则其右孩子结点编号是 2i+1

3 - 二叉树的存储

二叉树的顺序存储表示

用一组地址连续的存储单元从上至下、从左至右存储二叉树上的结点元素。

  • 完全二叉树容易用顺序存储表示
  • 对于一般的二叉树,可以参照完全二叉树,将对应的完全二叉树中存在但该二叉树中不存在的结点数据元素置空
#define MAX_TREE_SIZE 100

typedef T SqBiTree[MAX_TREE_SIZE]; // 1号单元存储根结点

SqBiTree bt;

二叉树的链式存储表示

两个指针域(二叉链表)

typedef struct BiTNode {
	T data;
	struct BiTNode *lchild,*rchild; // 左右孩子指针
} BiTNode,*BiTree;

BiTree bt;
三个指针域(三叉链表)

typedef struct BiTNode {
    T data;
    struct BiTNode *lchild,*rchild,*parent; // 左右孩子指针和双亲指针
} BiTNode,*BiTree;

BiTree bt;
一个指针域(双亲链表)

// 静态链表实现
#define MAX_TREE_SIZE 100

typedef struct {
    T data;
    int parent; // 双亲指针
    int LRTag;
} BiTNode;

typedef struct{
    BiTNode nodes[MAX_TREE_SIZE];
    int num_node; // 结点数目
    int root; // 根结点的位置
} BiTree;

BiTree bt;

4 - 二叉树的遍历

DFS

  • 分为先根序、中根序、后根序三种遍历顺序,每种遍历顺序又有递归实现和非递归实现
  • 以下 9 种算法,时间复杂度都为 O(n),空间复杂度都为 O(n)
  • 示例图:

先根序

算法描述:若二叉树为空树,则空操作;否则,

  1. 访问根结点
  2. 先根序遍历左子树
  3. 先根序遍历右子树

示例:示例图二叉树遍历顺序为 - + a * b - c d / e f

递归实现:

void PreOrderTraverse(BiTree T,void(*visit)(T &e)) {
    if (T) {
		visit(T->data); // 访问根结点
        PreOrderTraverse(T->lchild,visit); // 先根序遍历左子树
        PreOrderTraverse(T->rchild,visit); // 先根序遍历右子树
    }
}

非递归实现:

先根序遍历时结点输出顺序是根、左孩子和右孩子,根据这个特点可以使用一个栈实现非递归的先根序遍历,右孩子、左孩子和根依次入栈。

// 第一种方式
void PreOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Stack S;
    InitStack(S);
    if (T)
        Push(S,T);
    while (!StackEmpty(S)) {
        BiTree t=Pop(S);
        visit(t->data); // 访问根结点
        if (t->rchild)
            Push(S,t->rchild); // 右子树入栈
        if (t->lchild)
            Push(S,t->lchild); // 左子树入栈
    }
}

// 第二种方式
void GoFarLeft(BiTree T,void(*visit)(T &e),Stack &S) {
    while (T) {
    	visit(T->data); // 找到最左下的结点,沿途结点立即访问
    	if (T->rchild)
        	Push(S,T->rchild); // 沿途右子树自顶向下入栈
        T=T->lchild;
    }
}

void PreOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Stack S;
    InitStack(S);
    while (T) {
        GoFarLeft(T,visit,S);
        if (!StackEmpty(S))
            T=Pop(S);
        else
            break;
    }
}
中根序

算法描述:若二叉树为空树,则空操作;否则,

  1. 中根序遍历左子树
  2. 访问根结点
  3. 中根序遍历右子树

示例:示例图二叉树遍历顺序为 a + b * c - d - e / f

递归实现:

void InOrderTraverse(BiTree T,void(*visit)(T &e)) {
    if (T) {
        InOrderTraverse(T->lchild,visit); // 中根序遍历左子树
        visit(T->data); // 访问根结点
        InOrderTraverse(T->rchild,visit); // 中根序遍历右子树
    }
}

非递归实现:

中根序遍历时结点输出顺序是左孩子、根和右孩子,根据这个特点可以使用一个栈实现非递归的中根序遍历,右孩子、根和左孩子依次入栈。

// 第一种方式
BiTree GoFarLeft(BiTree T,Stack &S) {
    if (!T)
        return NULL;
    while (T->lchild) { // 找到最左下的结点
        Push(S,T); // 沿途结点入栈,最左下的结点没有入栈
        T=T->lchild;
    }
    return T; // 返回最左下的结点
}

void InOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Stack S;
    InitStack(S);
    BiTree t=GoFarLeft(T,S);
    while (t) {
        visit(t->data);
        if (t->rchild)
            t=GoFarLeft(t->rchild,S);
        else if (!StackEmpty(S))
            t=Pop(S);
        else
            break;
    }
}

// 第二种方式
void GoFarLeft(BiTree &T,Stack &S) { // 注意:T需要引用传递
    while (T) {
        Push(S,T);
        T=T->lchild;
    }
}

void InOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Stack S;
    InitStack(S);
    while (1) {
        if (T)
        	GoFarLeft(T,S);
        else if (!StackEmpty(S)) {
            T=Pop(S);
            visit(T->data);
            T=T->rchild;
        }
        else
            break;
    }
}
后根序

算法描述:若二叉树为空树,则空操作;否则,

  1. 后根序遍历左子树
  2. 后根序遍历右子树
  3. 访问根结点

示例:示例图二叉树遍历顺序为 a b c d - * + e f / -

递归实现:

void PostOrderTraverse(BiTree T,void(*visit)(T &e)) {
    if (T) {
        PostOrderTraverse(T->lchild,visit); // 后根序遍历左子树
        PostOrderTraverse(T->rchild,visit); // 后根序遍历右子树
        visit(T->data); // 访问根结点
    }
}

非递归实现:

需要一个标记区别回到根结点时是从左子树返回还是从右子树返回的。

// 第一种方式
void PostOrderTraverse(BiTree T,void(*visit)(T &e)) {
    BiTree S[stacksize];
    int tag[stacksize],top=-1;
    do {
        while (T) { // 一路向左
            S[++top]=T;
            tag[top]=0;
            T=T->lchild;
        }
		while (top>=0&&tag[top]==1) { // 左右子树都访问过
            visit(S[top]->data);
            top--;
        }
        T=S[top];
        if (top>=0&&tag[top]==0) { // 扫描右子树
            T=T->rchild;
            tag[top]=1;
        }
    } while (T!=NULL||top>=0);
}

// 第二种方式
void PostOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Stack S;
    InitStack(S);
    BiTree p=T,r=NULL; // p记录当前访问结点,r记录上一个访问到的结点
    while (p||!StackEmpty(S)) {
        if (p) { // 一路向左
            Push(S,p);
            p=p->lchild;
        }
        else {
            p=GetTop(S);
            if (p->rchild&&p->rchild!=r) { // 扫描右子树
                p=p->rchild;
                Push(S,p);
                p=p->lchild;
            }
            else { // 左右子树都访问过
                Pop(S);
                visit(p->data);
                r=p;
                p=NULL;
            }
        }
    }
}

BFS(层次遍历)

  • 时间复杂度为 O(n),空间复杂度为 O(n)
void LevelOrderTraverse(BiTree T,void(*visit)(T &e)) {
    Queue q;
    InitQueue(q);
    if (T) {
        visit(T->data);
        EnQueue(q,T);
    }
    while (!QueueEmpty(q)) {
        T=GetHead(q);
        DeQueue(q);
        if (T->lchild) {
            visit(T->lchild->data);
            EnQueue(q,T->lchild);
        }
        if (T->rchild) {
            visit(T->rchild->data);
            EnQueue(q,T->rchild);
        }
    }
}

遍历算法的应用举例

遍历算法是二叉树的重要算法,二叉树的很多问题都可以用遍历算法的思想解决,包括:求(叶子)结点个数、求深度、建立二叉树、删除二叉树、交换左右子树、将完全二叉树的顺序存储表示转换为二叉链表表示等等。

求(叶子)结点个数

遍历二叉树,在遍历过程中查找叶子结点并计数。由此,需要增添一个 count 参数,并将算法中 visit 的操作改为:若是叶子,则计数器增 1。

// 第一种方式
// 定义 cnt,visit 操作为:若是叶子结点,则 cnt++(先根序、中根序、后根序、层次遍历都可以)
void CountLeaf(BiTree T,int &count) { // count初值为0
    if (T) {
        if (!T->lchild&&!T->rchild)
            count++;
        CountLeaf(T->lchild,count);
        CountLeaf(T->rchild,count);
    }
}

// 第二种方式
int CountLeaf(BiTree T) {
    if (!T)
        return 0;
    else if (!T->lchild&&!T->rchild)
        return 1;
    else
        return CountLeaf(T->lchild)+CountLeaf(T->rchild);
}
求深度

由二叉树深度的定义可知,二叉树的深度为其左、右子树深度的较大值加 1。由此,需要将算法中 visit 的操作改为:分别求得左、右子树深度的较大值,然后加 1。

int BiTreeDepth(BiTree T) {
    int depthval;
    if (!T)
        depthval=0;
    else {
        int depthLeft=BiTreeDepth(T->lchild);
        int depthRight=BiTreeDepth(T->rchild);
        depthval=1+(depthLeft>depthRight?depthLeft:depthRight); // 只能后根序
    }
    return depthval;
}
建立二叉树
  1. 约定以字符串(先根序、带空结点)的形式表示一棵二叉树

    以'#'表示空结点

    例如:

    • 空树:用字符串"#"表示

    • 只含根结点(a)的二叉树:用字符串"a##"表示

    • 一般情况:

      ​ a /
      b c /
      d e

      用字符串"abd##e##c##"表示

    void CreateBiTree(BiTree &T) {
        char ch=getchar();
        if (ch=='#')
            T=NULL;
        else {
            if (!(T=(BiTree)malloc(sizeof(BiTNode))))
                exit(1);
            T->data=ch;
            CreateBiTree(T->lchild);
            CreateBiTree(T->rchild);
        }
    }
    
  2. 约定以先序序列和中序序列同时给出的形式表示一棵二叉树

    已知二叉树的先序序列中序序列可以唯一确定一棵二叉树

    例如,先序序列为 ABECDFGHIJ,中序序列为 EBCDAFHIGJ,建树过程如下:

    int Search(char in[],char ch) {
        int i=0;
        for (;i<strlen(in);i++)
            if (in[i]==ch)
                break;
        return i==strlen(in)?-1:i;
    }
    
    void CreateBiTree(BiTree &T,char pre[],char in[],int ps,int is,int n) {
        // pre[ps...ps+n-1]为二叉树的先序序列,in[is...is+n-1]为二叉树的中序序列,ps为先序序列的开始位置,is为中序序列的开始位置,n为序列长度
        if (!n)
            T=NULL;
        else {
            int k=Search(in,pre[ps]); // 查询先序序列中的第一个字符在中序序列中的位置
            if (k==-1)
                T=NULL;
            else {
                T=(BiTree)malloc(sizeof(BiTNode));
                T->data=pre[ps];
                if (k==is)
                    T->lchild=NULL; // 先序序列中的第一个字符在中序序列中也是第一个字符,表示没有左子树
                else
                    CreateBiTree(T->lchild,pre,in,ps+1,is,k-is);
                if (k==is+n-1)
                    T->rchild=NULL; // 先序序列中的第一个字符在中序序列中是最后一个字符,表示没有右子树
                else
                    CreateBiTree(T->rchild,pre,in,ps+1+(k-is),k+1,n-(k-is)-1); // 注意这里ps,is,n三个参数
            }
        }
    }
    

    已知二叉树的后序序列中序序列,也可以唯一确定一棵二叉树

    已知二叉树的先序序列后序序列不能唯一确定一棵二叉树

  3. 约定以表达式的形式表示一棵二叉树

    操作数为叶子结点,算符为分支结点

    例如:

    // 由前缀表达式建树
    // 与由字符串(先根序、带空结点)建树类似
    void CreateExpBiTree(BiTree &T) {
        char ch=getchar();
        if (!(T=(BiTree)malloc(sizeof(BiTNode))))
    		exit(1);
       	T->data=ch;
        if (ch>='a'&&ch<='z')
            T->lchild=T->rchild=NULL;
        else {
            CreateBiTree(T->lchild);
            CreateBiTree(T->rchild);
        }
    }
    
    // 由原表达式建树
    // 算法思想与栈的应用举例中表达式求值类似
    /*
    使用两个栈:一个存放叶子结点或子树,一个存放运算符。
    步骤:
    	1.读入一个字符;
    	2.如果是操作数,建叶子结点入树栈;
    	3.如果是运算符,与运算符栈的栈顶元素比较优先级:
    		(1)若当前的优先级高,则入运算符栈;
    		(2)若优先级相同(左右括号),则弹出运算符栈栈顶元素(左括号);
    		(3)若栈顶的优先级高,则弹出运算符栈栈顶元素(根),从树栈中弹出两个元素(右左孩子)建子树,新建的子树入树栈,继续比较至情况(1)(2)。
    */
    // 定义树栈(类似OPND)和运算符栈(OPTR)
    TreeStack OPND;
    OptrStack OPTR;
    // 建叶子结点
    void CrtNode(char ch) {
        BiTree T=(BiTree)malloc(sizeof(BiTNode));
        T->data=ch;
        T->lchild=T->rchild=NULL;
        Push(OPND,T);
    }
    // 建子树
    void CrtSubtree(char ch) {
        BiTree T=(BiTree)malloc(sizeof(BiTNode));
        T->data=ch;
        BiTree rc=Pop(OPND);
        T->rchild=rc;
        BiTree lc=Pop(OPND);
        T->lchild=lc; // 左子树先入栈,右子树后入栈,先弹出的是右子树
        Push(OPND,T);
    }
    // 栈顶和当前运算符比较
    char Precede(char stop,char c) {
        switch (stop) {
            case '+':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '-':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '*':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '/':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '(':
                switch (c) {
                    case '+':return '<';
                    case '-':return '<';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '=';
                }
            case ')':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '#':
            	switch (c) {
            		case '+':return '<';
                    case '-':return '<';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case '#':return '=';
    			}
        }
    }
    // 判断是否是操作数
    int isOperand(char c) {
        return c>='a'&&c<='z'?1:0;
    }
    void CreateExpBiTree(BiTree &T,char exp[]) { // exp[strlen(exp)]='#';
        InitStack(OPND);
        InitStack(OPTR);
        Push(OPTR,'#');
        char* p=exp;
        char ch=*p;
        while (ch!='#'||GetTop(OPTR)!='#') {
            if (isOperand(ch)) {
                CrtNode(ch);
                ch=*(++p);
            }
            else switch (Precede(GetTop(OPTR),ch)) {
                case '<':
                    Push(OPTR,ch);
                    ch=*(++p);
                    break;
                case '=':
                    Pop(OPTR);
                    ch=*(++p);
                    break;
                case '>':
                    CrtSubtree(Pop(OPTR));
                    break;
            }
        }
        T=GetTop(OPND);
    }
    

线索二叉树

提出背景

当以二叉链表作为存储结构时,只能找到结点的左右孩子信息,而不能直接得到结点在任一序列的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到。如何把遍历过程中得到的结点的前驱和后继信息保存下来?

**方法一:**在每个结点上增加两个指针域 fwd 和 bkwd,分别指示结点在任一次序遍历时得到的序列的前驱和后继信息。但是这种方法大大降低了存储密度。

**方法二:**对于一个有 n 个结点的二叉链表,一共有 2n 个指针域,只有 n-1 条分支(除头结点,每一条分支都指向一个结点),也就是存在 2n-(n-1) = n+1 个空指针。这些指针域只是白白浪费空间,因此,可以利用这些空链域来存放结点的前驱和后继信息。

这种利用空链域来存放结点的前驱和后继信息的二叉树称为线索二叉树,以某种次序遍历二叉树使其变为线索二叉树的过程称为线索化

实现方法

为了区分指针域指示的是左右孩子信息还是前驱和后继信息,在二叉链表的结点中增加两个标志域 LTag 和 RTag,其中:

  • LTag = 0,lchild 域指示结点的左孩子
  • LTag = 1,lchild 域指示结点的前驱
  • RTag = 0,rchild 域指示结点的右孩子
  • RTag = 1,rchild 域指示结点的后继
typedef enum{Link,Thread} Pointer; // Link:指针指示左右孩子,值为0;Thread:线索指示前驱后继,值为1

typedef struct ThrBiTNode {
    T data;
    struct ThrBiTNode *lchild,*rchild;
    Pointer LTag,RTag;
} ThrBiTNode,*ThrBiTree;
// 中根序遍历二叉树的线索化(二叉树已建好)
/*
约定:
1.添加头结点,其lchild指针指向根结点,rchild指针指向访问的最后一个结点
2.中根序遍历访问的第一个结点的lchild指针和最后一个结点的rchild指针均指向头结点
思考:
1.中根序遍历访问的第一个结点是?
左子树上处于“最左下”的结点。
2.以中根序线索化后的二叉链表中结点的后继是?
若无右子树,则为后继线索所指结点;否则为对其右子树进行中根序遍历时访问的第一个结点
*/
// 线索化的过程即为在遍历过程中修改空指针的过程。为了记下遍历过程中访问结点的先后关系,附设一个指针pre指向当前访问的结点的前驱。

// 对以p为根的非空二叉树进行中序线索化
void Threading(ThrBiTree p,ThrBiTree &pre) {
    if (p) {
        Threading(p->lchild,pre); // 左子树线索化
        if (!p->lchild) { // 左孩子空,建前驱线索
            p->LTag=Thread;
            p->lchild=pre;
        }
        if (!pre->rchild) { // 前驱的右孩子空,建后继线索
            pre->RTag=Thread;
            pre->rchild=p;
        }
        pre=p; // 保持pre指向p的前驱
        Threading(p->rchild,pre); // 右子树线索化
    }
}

void InOrderThreading(ThrBiTree &h,ThrBiTree T) {
    // h指向头结点,T指向根结点
    // 重点看h->lchild,h->rchild,第一个结点的lchild,最后一个结点的rchild四个指针指向
    // h需要引用传递,因为要为h申请堆上内存,如果是值传递,函数结束空间就被释放
    if (!(h=(ThrBiTree)malloc(sizeof(ThrBiTNode))))
        exit(1);
    h->LTag=Link; // 中根序遍历访问的第一个结点是左子树上处于“最左下”的结点,当根节点没有左子树时,如何定义“最左下”?就是这句代码
    h->RTag=Thread;
    h->rchild=h; // 右指针回指
    if (!T) h->lchild=h; // 若二叉树空,左指针回指
    else {
        h->lchild=T;
        ThrBiTree pre=h;
        Threading(T,pre);
        pre->RTag=Thread;
        pre->rchild=h;
        h->rchild=pre;
    }
}
// 中根序遍历线索二叉树的非递归算法
void InOrderTraverse_Thr(ThrBiTree T,void(*visit)(T &e)) { // T指向头结点
    ThrBiTree p=T->lchild; // p指向根结点
    while (p!=T) { // 空树或遍历结束时,p==T
        while (p->LTag==Link)
            p=p->lchild; // 一路向左,找第一个结点
        visit(p->data);
        while (p->RTag==Thread&&p->rchild!=T) {
            p=p->rchild;
            visit(p->data);
        }
        p=p->rchild;
    }
}

5 - 树的存储

双亲表示法

每个结点只有一个双亲(除根结点),所以用一组连续空间存储树的结点,并在每个结点中附设一个指示其双亲结点的位置域。

// 结点结构
typedef struct PTNode {
    T data;
    int parent; // 双亲位置域
} PTNode;

// 树结构
typedef struct {
    PTNode nodes[MAX_TREE_SIZE];
    int r,n; // 根结点的位置和结点个数
} PTree;

双亲表示法容易求结点的双亲,但是求结点的孩子时需要遍历整个结构。

孩子表示法

每个结点有多个孩子(除叶子结点),所以用一组连续空间存储树的结点,但是每个结点需要多个指示其孩子结点的位置域。

方法一

设树的度为 d,每个结点直接开 d 个孩子位置域。操作简单,但是由于树中很多结点的度小于 d,空间较浪费。对于一棵有 n 个结点、度为 k 的树,一共有 n*k 个指针域,只有 n-1 条分支(除头结点,每一条分支都指向一个结点),也就是存在 nk-(n-1) = n(k-1)+1 个空指针。

方法二

根据每个结点的度为其申请数量不同的孩子位置域。节省空间,但是操作复杂。

方法三

把每个结点的孩子结点排列起来成为一个线性表(以单链表作为存储结构,叶子结点的链表为空表),而每个结点又组成一个线性表(为了便于查找,可采用顺序存储结构)。

typedef struct CTNode {
    int child;
    struct CTNode* next;
} *ChildPtr;

typedef struct {
    T data;
    ChildPtr h; // 孩子链表的头指针
} CTBox;

typedef struct {
    CTBox nodes[MAX_TREE_SIZE];
    int r,n; // 根结点的位置和结点个数
} CTree;

孩子表示法容易求结点的孩子,但是求结点的双亲时需要遍历整个结构。

带双亲的孩子链表表示法

结合了双亲表示法和孩子表示法的优点,容易求结点的双亲和孩子。

孩子-兄弟表示法(二叉链表表示法)

以二叉链表作为树的存储结构,链表中结点的两个链域分别指向该结点的第一个孩子下一个兄弟。给定一棵树,可以找到唯一一棵二叉树与之对应。任何一棵树所对应的二叉树,其右子树必为空。

typedef struct CSNode {
    T data;
    struct CSNode *firstchild,*nextsibling;
} CSNode,*CSTree;

二叉链表表示法便于实现各种树的操作。例如,若要访问某个结点的第 i 个孩子,则只要先从该结点的 firstchild 域找到第 1 个孩子,然后沿着孩子结点的 nextsibling 域连续走 i-1 步。

带度数的层序表示(顺序存储表示)

6 - 树的遍历

树的遍历有先根序遍历后根序遍历层次遍历三种方式。

二叉链表所存树的遍历

上图中:

  • 树的先根序遍历次序:ABEFCDGHIJK,也是对应二叉树的先根序遍历次序
  • 树的后根序遍历次序:EFBCIJKHGDA,也是对应二叉树的中根序遍历次序
  • 树的层次遍历次序:ABCDEFGHIJK

使用二叉链表表示法存储树,可以沿用二叉树的遍历算法。

遍历算法的应用举例

求深度
int TreeDepth(CSTree T) {
    if (!T)
        return 0;
    else {
        int h1=TreeDepth(T->firstchild);
        int h2=TreeDepth(T->nextsibling);
        return h1+1>h2?(h1+1):h2;
    }
}
建树
  1. 约定以表示边的二元组 (parent, child) 的形式表示一棵树

    以二元组 (parent, child) 的形式自上而下、自左而右依次输入树的各边,建立树的孩子-兄弟链表

    例如:

    ​ a / |
    b c d ​ /
    e f | g

    输入为 #aabacadcecfeg##

    void CreateTree(CSTree &T) {
    	T=NULL; // 健壮性:只输入##
    	SqQueue Q;
    	InitQueue(Q);
    	char fa,ch;
    	CSTree r;
    	for(scanf("%c%c",&fa,&ch);ch!='#';scanf("%c%c",&fa,&ch)) {
    		CSTree p=(CSTree)malloc(sizeof(CSNode));
    		p->data=ch;
    		p->firstchild=NULL;
    		p->nextsibling=NULL;
    		EnCycque(Q,p);
    		if (fa=='#')
    			T=p;
    		else {
    			CSTree s=GetHead(Q);
    			while (s->data!=fa) {
    				DeCycque(Q);
    				s=GetHead(Q);
    			}
    			if (!(s->firstchild)) {
    				s->firstchild=p;
    				r=p;
    			}
    			else {
    				r->nextsibling=p;
    				r=p;
    			}
    		}
    	}
    }
    
  2. 约定以先序序列和后序序列同时给出的形式表示一棵树

    对应以先序序列和中序序列同时给出的形式表示一棵二叉树

7 - 哈夫曼树

相关概念

  • 路径:两个结点之间的分支
  • 结点的路径长度:从根结点到该结点的路径上分支的数目
  • 树的路径长度:树中所有结点的路径长度之和
  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和:WPL(T)=(wklk)WPL(T)=\sum(w_k*l_k)
  • 哈夫曼树 / 最优二叉树:在所有含 n 个叶子结点、并带相同权值的二叉树中,必定存在一棵其带权路径长度取最小值的树,称为哈夫曼树

构造方法

例如:已知权值 W = {5, 6, 2, 9, 7}

重要性质

有 n 个叶子结点的哈夫曼树的结点总数为 2n-1

证明:m 叉哈夫曼树只有度为 m 和度为 0 的结点,故二叉哈夫曼树只有度为 2 和度为 0 的结点,故结点总数为 n0+n2n_0+n_2。又对于每个度为 2 的结点都有 2 个分支,度为 0 的结点没有分支,故结点总数为 2n2+12n_2+1(加上根结点)。故 n0+n2=2n2+1n_0+n_2=2n_2+1,得到 n2=n01n_2=n_0-1,故总结点数为 2n012n_0-1

重要应用:最优前缀编码

前缀编码

对字符集进行二进制编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀

例如:设有 a, b, c, d 需要编码表示,若 a=0, b=10, c=110, d=11,则 110 表示的可以是 c 或 da,出现这种情况是因为 d 的编码是 c 的编码的前缀

最优前缀编码

利用哈夫曼树可以构造最优前缀编码,即使得所传电文的总长度最短,也称为哈夫曼编码

构造方法

以 n 种字符出现的频率作权,设计一棵哈夫曼树。约定左分支表示字符 0,右分支表示字符 1,则从根结点到叶子结点的路径的分支上 01 字符组成的字符串就是该叶子结点字符的编码