树(C语言)(待更)

目录

简单认识几种二叉树和它们的性质

二叉树的存储和遍历

二叉树的创建

由遍历引申出的问题

线索二叉树

树和森林

哈夫曼树

常用点的深度讲解


简单认识几种二叉树和它们的性质

什么是二叉树?

      - 就是有一棵树,它的节点最多有两个分支,可以没有分支,可以有一个,也可以有两个。如果它有两个分支,那么它的左右分支是有顺序的,左边的就叫左子树,右边的就叫右子树,左右不能颠倒。如果它只有一个分支,那你也要说清楚这个分支到底是左子树还是右子树。

      - 二叉树必须有根吗?不一定,二叉树可以没有根,也就是一个节点都没有,是被人连根拔起的树。

      - 二叉树好像和树差不多,就是比一般的树特殊,那二叉树是树的特殊情况吗?不是!二叉树不是树的特殊情况,他们是两个概念。因为二叉树得说明左右子树,哪怕只有一个分支。但是一般的树就不区分这个,只有一个树枝就不区分左右了。

二叉树有哪些性质?

      1、第 i 层最多有2^(i - 1)个节点。(i >= 1)

      2、深度为 k 的二叉树最多有2^k  - 1个节点。(k >= 1)

      3、叶子数为n0,度为2的节点个数为n2,那么n0 = n2 + 1。

什么是满二叉树?

      - 就是这个二叉树是饱和的,每一层有最多的节点,它的叶子都在最底层,整棵树有k层,就有2^k - 1个节点。

什么是完全二叉树?

      - 你可以想象这里有一棵满二叉树,你觉得它枝叶太多,想剪掉一部分。那么你从这棵树的右下角开始剪:先剪最下面的一层,从右到左,剪完一层,觉得不够,再剪上一层,也是从右到左...这样你总是能保持这棵树是完全二叉树。

      - 它用数学语言描述就是:对任一结点,如果其右子树的最大层次为 L,则其左子树的最大层次必为 L 或 L + 1。

完全二叉树有什么性质?

      1、具有n个节点的完全二叉树的深度为logn + 1。

      2 、假设完全二叉树有n个节点,每个节点按序编号,对于任一节点 i

            1)i = 1,则节点是根。i > 1,该节点的双亲节点是[i / 2] ([ ]代表向下取整)

            2)2i > n,节点i 是叶子节点。一个节点如果有左孩子,左孩子节点为2i。

            3)2i + 1 > n,节点 i 没有右孩子。 一个节点如果右有孩子,有孩子节点为 2i + 1。

 

二叉树的存储和遍历

二叉树怎么存进计算机呢?

      - 在计算机中存储某种逻辑结构一般采用顺序结构或者链式结构。我们先试试用顺序结构存储二叉树。以数组来模拟顺序存储,二叉树的节点是被我们按顺序编号的,那就按编号顺序将节点存储在数组里。但是这样存储的缺点也显而易见。

      - 我们进而采用链式存储,每个节点的结构为:左孩子节点地址 - 数据 - 右孩子孩子节点地址

typedef struct BiTNode { // 结点结构
    TElemType data;
    struct BiTNode *lchild, *rchild; // 左右孩子指针                                    
} BiTNode, *BiTree;

二叉树怎么遍历?

      - 三种方式:先序遍历、中序遍历、后序遍历。

      - 先序:就是每个节点都按照数据、左孩子节点地址、右孩子节点的顺序遍历。中序:每个节点都按照左孩子节点地址、数据、右孩子节点的顺序遍历。后序:就是每个节点都按照左孩子节点地址、右孩子节点、数据的顺序遍历。

//用递归的形式

   //先序:
 void preorder (BiTNode *root)  {
          if (root!=NULL)   { 
              printf("%d", root->data); //访问根结点
              preorder(root->Lchild); //先序遍历根的左子树
              preorder(root->Rchild); //先序遍历根的右子树
          }//if
    }//preorder

   //中序:
 void preorder (BiTNode *root)  {
          if (root!=NULL)   { 
              preorder(root->Lchild); //先序遍历根的左子树
              printf("%d", root->data); //访问根结点
              preorder(root->Rchild); //先序遍历根的右子树
          }//if
    }//preorder

   //后序:
 void preorder (BiTNode *root)  {
          if (root!=NULL)   { 
              preorder(root->Lchild); //先序遍历根的左子树
              preorder(root->Rchild); //先序遍历根的右子树
              printf("%d", root->data); //访问根结点
          }//if
    }//preorder

怎么通过遍历顺序确定一棵二叉树?(深刻理解递归遍历)

      - 给出先序遍历和中序遍历的一串数字或字母让你确定一棵二叉树,例如先序:ABCDEFGHI,后序:BCAEDGHFI,确定一棵二叉树。

      - 要想推出二叉树结构,就要理解遍历中的归律。我在尝试很多种情况后发现几个有用的规律:

 

二叉树的创建

怎么方便的创建一个二叉树呢?

     - 为了避免手动的一个一个创建节点,那就必须用到循环或递归的方式创建节点。下面我们介绍用递归的方式创建节点。

     - 思路:获取要给节点赋的值;给出递归终点条件;创建节点并赋值;创建左节点(递归);创建右节点(递归)。

BiTNode *CreateBiTree() // 创建任意二叉树(先序)
{
	BiTNode *T;
	int x;
	printf("Enter data(-1 for no data):"); // 输入-1表示该节点为空
	scanf("%d", &x);

	if (x == -1) // 递归终止条件
		return NULL;

	T = (BiTree)malloc(sizeof(BiTNode)); // 创建节点并赋值
	T->data = x;

	printf("Enter left child of %d:", x); // 创建左子树
	T->Lchild = CreateBiTree();

	printf("Enter right child of %d:", x); // 创建右子树
	T->Rchild = CreateBiTree();

	return T;
}//CreateBiTree

 

由遍历引申出的问题

怎么确定二叉树的叶子节点个数?

      - 遍历中可以访问到每个节点,我们在其中加入判断条件,当节点没有孩子就是叶子节点,计数器加一。

int count = 0; // 全局变量,充当计数器

void CountLeaf(BiTNode *T) // 求叶子节点个数
{
	if (T)
	{
		if ((!T->Lchild) && (!T->Rchild))
			count++;
		CountLeaf(T->Lchild);
		CountLeaf(T->Rchild);
	}//if
}//CountLeaf

怎么确定二叉树的深度?

     - 递归遍历左子树得到左子树的深度,再递归遍历右子树得到右子树的深度。取最大值加一,就是某个节点的深度。

int leftDepth = 0, rightDepth = 0; // 设置两个全局变量

int BiTreeDepth(BiTNode *T)
{
	if (!T) // 节点为空,depth为0
		return 0;
	leftDepth = BiTreeDepth(T->Lchild); // 遍历左子树
	rightDepth = BiTreeDepth(T->Rchild); // 遍历右子树
	depth = max(leftDepth, rightDepth) + 1; // 取最大值加一
	return depth;
}//BiTreeDepth

 

线索二叉树

线索二叉树怎么来的?

     - 为了提高找某一节点的效率产生了线索二叉树。普通的二叉树在寻找某个节点需要从父到子逐个遍历。要是能直接找到一个节点的父节点和它的兄弟节点就会方便很多。线索二叉树就是这样的。

线索二叉树的存储结构?

typedef enum PointerTag{ Link, Thread } PointerTag;
// Link == 0 代表指针 Thread == 1 代表线索

typedef struct BiThrNode{
    TElemType data;
    struct BiThrNode *lchild, *rchild; // 左右指针
    PointerTag LTag, RTag; // 左右标志
}BiThrNode, *BiThrTree;

     - 存储结构相比于原来的二叉树多了两个枚举类型的值。这两个值就是为了充分利用节点空间而设计的。一个有左孩子没有右孩子的节点,它的右孩子指针域是空的。为了充分利用,我们让这个空的右孩子指针域指向该节点的后继。后继又是什么呢?假设我们把二叉树的中序遍历结果写出为acdgh,c的后继就是d,d的后继就是g。

     - 所以,在线索二叉树中,我们规定:节点有孩子,对应的指针域就指向它的孩子,标志域写为0;若是左孩子指针域有空闲,那么这个空间就指向它的前驱,标志域写为1;相对应的,若是右孩子指针域有空闲,那么这个空间就指向它的后继,标志域写为1。

     - 下面二叉树的中序遍历的结果为:a + b × c - d - e / f,那么节点 a 的左孩子指针域和 f 的右孩子指针域为空,应该让其空闲区域指向前驱后继,但是a没有前驱,f没有后继,那么我们新建一个节点T,让a的左域和f的右域去指向T。同时节点T的右域指向f,左域指向二叉树的根节点。这样,一个完整的“环”就构成了。这构成的就是中序线索二叉树。类似的还有先序线索二叉树和后序线索而二叉树,是根据何种的遍历序列决定的。对于先序或后序线索二叉树,可能没有T节点。请大家自行尝试,即可发现规律。

怎么遍历线索二叉树?

     - 中序找后继法

Status Visit(TElemType e)
{
    if(!e)
        return ERROR;
    printf("%c ",e);
    return OK;
}

// 中序遍历二叉线索树 
Status InOrderTraverse_Thr(BiThrTree T,int (* Visit)(TElemType e))
{   //T指向头结点,头结点的左链lchild指向根节点,可参见线索化算法。
    //中序遍历二叉线索树T的非递归算法,对每个数据元素调用函数Visit。
    BiThrTree p;
    p = T->lchild;	//p指向根节点
    while (p != T) {//空树或遍历结束时,p == T
        while (p->LTag == Link)
            p = p->lchild;	
        if(!Visit(p->data))	//访问左子树为空的结点
            return ERROR;
        while (p->RTag == Thread && p->rchild != T)
        {
            p = p->rchild;
            Visit(p->data);	//访问后继结点  这里为啥不判定失败?
        }
        p = p->rchild;
    }
    return OK;
}//InOrderTraverse_Thr

怎么创建线索二叉树?

char arr3[] = {'-','+','a','\0','\0','*','b','\0','\0','-','c','\0','\0','d','\0','\0','/','e','\0','\0','f','\0','\0'};
int arr_i = 0;

// 按照先序遍历创建线索二叉树
Status CreateBiTree(BiThrTree *T) {
    char ch;
    ch = arr3[arr_i++];
    if(ch == '\0') {
        (*T) = NULL;
    }else{
        if(!((*T) = (BiThrNode *)malloc(sizeof(BiThrNode))))
            exit(OVERFLOW);
        (*T)->data = ch;			//生成根节点
        (*T)->LTag = Link;			//生成时给线索标志域赋初值
        (*T)->RTag = Link;			//默认都是Link
        CreateBiTree(&((*T)->lchild));//构造左子树
        CreateBiTree(&((*T)->rchild));//构造右子树
    }
    return OK;
}

怎么将普通的二叉树线索化?

// 线索化左子树和右子树   
// 此处也可以把pre设置为全局变量,那就将InOrderThreading函数中的pre定义去掉
void InThreading(BiThrTree p,BiThrTree *pre)
{
    if(p){
        InThreading(p->lchild, pre);	//左子树线索化
        if(!p->lchild) {    	//若p指向结点没有左孩子,线索化前驱
            p->LTag = Thread;
            p->lchild = (*pre);	//前驱线索化
        }
        if(!(*pre)->rchild){		//若p指向结点没有右孩子,线索化后继
            (*pre)->RTag = Thread;
            (*pre)->rchild = p;	//后继线索化
        }
        (*pre) = p;				//保持pre指向p的前驱
        InThreading(p->rchild, pre);	//右子树线索化
    }
}

// 中序线索化 
Status InOrderThreading(BiThrTree *Thrt,BiThrTree T)
{   // 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。
    BiThrTree pre = NULL;
    if(!((*Thrt) = (BiThrNode *)malloc(sizeof(BiThrNode))))
        exit(OVERFLOW);
    (*Thrt)->LTag = Link;	(*Thrt)->RTag = Thread;	//建头结点
    (*Thrt)->data = '#';
    (*Thrt)->rchild = (*Thrt);						//右指针回指
    if(!T) {
        (*Thrt)->lchild = (*Thrt);					//若二叉树为空,则左指针回指(就是指向自己)
    }else {
        (*Thrt)->lchild = T;
        pre = (*Thrt);
        InThreading(T, &pre);						//中序遍历进行中序线索化
        pre->rchild = (*Thrt);	pre->RTag = Thread;	//最后一个结点线索化
        (*Thrt)->rchild = pre;
    }
    return OK;
}

树和森林

树的存储结构

     - 双亲表示法

     - 孩子表示法

          - 多重链表

          - 孩子链表

     - 孩子兄弟表示法

树与二叉树的转换

     - 将树转换成二叉树

     - 将二叉树转换成树

     - 将森林转换成二叉树

     - 将二叉树转换成森林

哈夫曼树

什么是哈夫曼树?

基本概念

哈夫曼树构造方法

哈夫曼编码译码

常用点的知识讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值