从零开始的数据结构与算法(C)(3)

三.树和二叉树


树的特点:

①.树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点。

②.树中所有结点可用有零个或多个后继结点。

树的基本术语

结点的度:树中每个结点具有的子树个数称为结点的度。

树的度:树中所有结点度的最大值称为树的度。

叶子结点:度为0的结点,或者称为终端结点。

分支结点:度大于0的结点称为分支结点或者非终端结点。

孩子,双亲结点:一个结点的后继称为该结点的孩子结点,这个结点称为它孩子的双亲结点。

子孙结点:一个结点的所有子树中的结点称之为该结点的子孙结点。

祖先结点:从某个结点到达树根结点的路径上通过的所有结点称为祖先结点。

兄弟结点:具有同一双亲的结点互相称为兄弟结点。

结点层数:树具有一种层次结构,根结点为第一层,其孩子结点为第二层,以此类推。

树的深度:树中所有结点的最大层数称为树的深度。

有序树和无序树;如果一棵树中结点的各子树从左到右是有次序的,即若交换了结点各子树的相对位置,则构成不同的树,称这个树为有序树,否则为无序树。

森林 :有限棵不相交的树的集合称为森林。

1.判定树

(1).八枚硬币问题:

问题描述:设存在八枚硬币,分别表示为a,b,c,d,e,f,g,h,其中有一枚且仅有一枚是伪造的,其重量可能比正常硬币轻,也可能比正常硬币重。先需以天平为工具,用量最少的比较次数判断假币的轻和重。

在这里插入图片描述

如图所示:进行思路分析:

将a,b,c分为一组,d,e,f分为一组,g,h分为一组。

比较a+b+c与d,e,f:

1.若a+b+c较大,则假币在上述硬币之中:

​ 进一步拿掉某一个硬币,并且交换两边的一个硬币:比较a+e?b+d:

​ 若前者较大,则假币存在于a与d之间。

​ 若二者相等,则假币存在于c与f之间。

​ 若后者较大,则假币存在于e与b之间。

2.若a+b+c=d+e+f时,则假币存在于取掉的g,h之间。

3.若d+e+f较大,则假币存在于上述六枚硬币之间:

​ 进一步拿掉某一个硬币,并交换两边的一个硬币:比较a+e?b+d:

​ 若前者较大,则假币存在于e与b之间。

​ 若二者相等,则假币存在于c与f之间。

​ 若后者较大,则假币存在于a与d之间。

算法设计

/*
	@Param arr[]:存储八枚硬币质量的数组
*/
void eightcoins(int arr[]){
    int abc = arr[0]+arr[1]+arr[2];
    int def = arr[3]+arr[4]+arr[5];
    int a = arr[0];
    int b = arr[1];
    int c = arr[2];
    int d = arr[3];
    int e = arr[4];
    int f = arr[5];
    int g = arr[6];
    int h = arr[7];
    if(abc > def){
        if(a+e > b+d){
            //假币存在于a与d之间
            compare(a,d,g,0,3);
        }else if(a+e == b+d){
            //假币存在于c与f之间
            compare(c,f,g,2,5);
        }else{
            //假币存在于b与e之间
            compare(b,e,g,1,4);
        }
    }else if(abc == def){
        //假币存在于g与h之间
        if(a == g){
            //假币是h
            print(h,g,7);
        }else{
            //假币是g
            print(g,h,6);
        }
    }else{
        if(a+e > b+d){
            //假币存在于b与e之间
            compare(b,e,g,1,4);
        }else if(a+e == b+d){
            //假币在c与f之间
            compare(c,f,g,2,5);
        }else{
            //假币在a与d之间
            compare(a,d,g,0,3);
        }
    }
}

/*
	@Param x,y:要比较质量的硬币
	@Param real:表示真实的硬币质量
	@Param index1,index2:比较两枚硬币的下标
*/
void prepare(int x,int y,int real,int index1,int index2){
    if(x == real){
        //说明y是假币
        print(y,real,index2);
    }else{
        //说明x是假币
        print(x,real,index1);
    }
}
/*
	@Param false:假的硬币的质量
	@Param true:真的硬币的质量
	@Param index:假币的下标
*/
void print(int false,int true,int index){
    if(false > true){
        printf("假币是第%d个硬币,且质量较重。",index+1);
    }else{
        printf("假币是第%d个硬币,且质量较轻。",index+1);
    }
}

2.二叉树

(1).二叉树的性质

性质1:一棵非空二叉树的第i层有2 ^ (i-1)个结点。

性质2:一棵深度为k的二叉树中,最多有2^k-1个结点。

性质3:对于一棵非空二叉树,如果叶子结点数为n0 ,度数为2的结点数为n。

n0 = n + 1

性质4:具有n个结点的完全二叉树的深度为log2(n)+1
性质5:对于一个有n个结点的完全二叉树,如果按照从上到下,从左到右从1开始对结点进行编号,对于序号为i的结点,有如下性质:
①.如果i>1,序号为i的结点的父结点序号为i/2
②.如果2i≤n,则序号为i的结点的左子结点序号为2i
③.如果2i+1≤n,则序号为i的结点的右子结点序号为2i+1

区分完全二叉树与满二叉树

完全二叉树指从上至下,从左至右一次填满无空缺的二叉树,只允许存在左不存在右,不允许存在右不存在左。

满二叉树指每层都必须填满的二叉树。

(2).二叉树的顺序存储

适用于满二叉树和完全二叉树。其余二叉树使用会造成空间浪费。

//采用数组实现,0号单元存放根结点
#define MAXNODE			//二叉树的最大结点数
typedef elemtype SqBiTree(MAXNODE);//0号单元存放根结点

(3).二叉树的链式存储

二叉链表的存储:

typedef struct BiTNode{
    elemtype data;
    struct BiTNode *left,*right;
}BiTNode,*BiTree;

(4).二叉树的遍历

Ⅰ.dfs遍历

所谓dfs,即Depth First Search,深度优先搜索。

其由分为三种,分别为 前序遍历中序遍历后序遍历

&前序遍历

前序遍历的遍历顺序为"根,左,右"。

递归实现:

/*
	Param bt:指向目标二叉树的指针
*/
void PreOrder(BiTree bt){
    //空树就无需遍历
    if(bt == NULL){return;}
    printf("%3c",bt->data);
    //此处做输出操作,也可改为其他操作比如便利得到的数据存入集合
    PreOrder(bt->left);//访问左子结点
    PreOrder(bt->right);//访问右子结点
}

循环实现

void NRPreOrder(BiTree bt)
{
    BiTree stack[MAXNODE],curr;//curr代表当前结点
    int top;
    //bt不存在,无需遍历
    if(bt==NULL) return;
    top=0;
    curr=bt;
    while(!(curr==NULL&&top==0))
    {
        while(curr!=NULL)
        {
            printf("%3c",curr->data);
            if(top<MAXNODE-1)
            {
                stack[top]=curr;
                top++;
            }
            else return;
            curr=curr->left;
        }
        if(top<=0) {return;}
            top--;
            curr=stack[flag];
            curr=curr->rchild;
    }
}
&中序遍历

中序遍历的遍历顺序是 左根右

递归实现

void InOrderOut(BiTree bt)
{
    if(bt==NULL) return;
    InOrderOut(bt->left);
    printf("%3c",bt->data);
    InOrderOut(bt->right);
}

循环实现

void NRInOrder(BiTree bt)
{
    BiTNode *curr=bt;
	int top=0;
	BiTNode *stack[30];
	while(NULL!=curr||top>0)
	{
		while(NULL!=curr)
		{
			stack[top++]=curr;
			curr=curr->left;
		}
		top--;
		curr=stack[top];
		printf("%3c",curr->data);
		curr=curr->right;
	}    
}
&后序遍历

后续遍历的遍历顺序为 左右根

递归实现

void PostOrder(BiTree bt)
{
    if(bt==NULL) return;
    PostOrder(bt->left);
    PostOrder(bt->right);
    printf("%3c",bt->data);
}

循环实现

void NRPostOrder(BiTree bt)
{
    BiTNode *curr=bt;
	BiTNode *stack[30];
	int top=0;
	BiTNode *have_visited=NULL; 
	while(NULL!=curr||top>0)
	{
		while(NULL!=curr)
		{
			stack[top++]=curr;
			curr=curr->left;		
		}
		curr=stack[top-1];
		if(NULL==curr->right||have_visited==curr->right)
		{
			printf("%3c",curr->data);
			top--;
			have_visited=curr;
			curr=NULL;
		}
		else
		{
			curr=curr->right;
		}
	}

    
}
Ⅱ.bfs遍历

所谓bfs即为 Breadth First Search广度优先搜索。

&层序遍历
void LevelOrder(BiTree bt)
{
    BiTree Queue[MAXNODE];
    int Front1,a;
    if(bt==NULL) return;
    Front1=-1;
    a=0;
    Queue[a]=bt;
    while(Front1!=a)
    {
        Front1++;
        printf("%3c",Queue[Front1]->data);
        if(Queue[Front1]->left!=NULL)
        {
            a++;
            Queue[a]=Queue[Front1]->left;
        }
        if(Queue[Front1]->right!=NULL)
        {
            a++;
            Queue[a]=Queue[Front1]->right;
        }
    }
}

(5).二叉树,树,森林的转换

树转换为二叉树

①树中所有相邻兄弟结点之间加一条连线。

②对树中每个结点,只保留它与第一个孩子之间的连线,删去它与其他孩子结点之间的连线。

③以树的根结点为轴心,将整棵树顺时针旋转一定角度即可。

森林转换为二叉树

①将森林中的每棵树都转换为二叉树。

②第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一个二叉树根结点的右子结点,当所有二叉树连起来后,得到的二叉树就是目标二叉树。

二叉树转换为树和森林

①若某结点是其双亲的左子结点,则把该结点的右子结点,右子结点的右子结点…都与该结点的双亲结点用线连起来。

②删除原二叉树中所有的双亲结点和右孩子结点的连线。

总结:转换过程中始终抓住,左子结点是子,右子结点是兄弟。

(6).二叉搜索树

又名 二叉排序树。其结点规律满足 左子结点的值都小于该结点,右子结点的值都大于该结点

抽象类型设计

typedef struct BSTree
{
    datatype data;
    BSTree *left;
    BSTree *right;
}BSTree;

Ⅰ.初始化二叉搜索树
/*
	@Param *T:指向目标二叉搜索树
*/
void MakeEmpty(BSTree *T){
    if(T!=NULL){
        MakeEmpty(T->left);//初始化其左子结点
        MakeEmpty(T->right);//初始化其右子结点
        free(T);//销毁自己
    }
}
Ⅱ.返回树中值为x的结点指针

算法思路

①若搜索树为空,查找失败,返回NULL。

②如果x比当前结点大,递归向右子结点。

如果x比当前结点小,递归向左子结点。

如果x与当前结点值相等,则返回该结点。

/*
	@Param *T:指向目标二叉搜索树
	@Param x:查找的值x
	@Return:返回找到的值为x的结点指针
*/
BSTree * Find(BSTree *T,datatype x){
    //如果当前结点为空
    if(T == NULL){
        return NULL;
    }
    if(x > T->data){
        return Find(T->right,x);
    }else if(x < T->data){
        return Find(T->left,x);
    }else{
        return T;
    }
}
Ⅲ.查找树中最大元素与最小元素所在结点
/*
	@Param *T:指向目标二叉搜索树
	@Return:返回最值所在的结点
	(递归算法)
*/
BSTree * FindMin(BSTree *T){
    //当前结点为空
    if(T == NULL){
        return NULL;
    }
    if(T->left==NULL){
        return T;
    }
    return FindMin(T->left);
}

/*
	@Param *T:指向目标二叉搜索树
	@Return:返回最值所在的结点
	(非递归算法)
*/
BSTree * FindMax(BSTree *T){
    if(T == NULL){
        return NULL;
    }
    BSTree *curr = T;
    while(T->left != NULL){
        curr = curr->right;
    }
    return T;
}
Ⅳ.插入一个结点

算法思路:先定位再插入

/*
	@Param *T:指向插入的目标树
	@Param x:插入结点的值
	@Return:返回新的结点指针
*/
BSTree * insert(BSTree *T,datatype x){
    //先考虑树为空的情况
    if(T == NULL){
        T = malloc(sizeof(BSTree));
        T->data = x;
        T->left = NULL;
        T->right = NULL;
        return T;
    }
    if(x < T->data){
        T->left = insert(T->left,x);
    }else if(x > T->data){
        T->right = insert(T->right,x);
    }
    return T;
}
Ⅴ.删除一个结点

算法思路

①如果当前结点为空,直接返回NULL。

②如果删除的是叶子结点,将其父结点的对应指针域删除即可。

如果删除的是度为1的结点,调整双亲指针后绕过该结点删除。

如果删除的是度为2的结点,找到其右子树的最小元素作为替代。

/*
	@Param *T:指向删除的目标树
	@Param x:要删除结点的值
	@Return:返回当前的结点指针
*/
BSTree * delete(BSTree * T,datatype x){
    BSTree *p;
    //考虑树为空的情况
    if(T == NULL){
        return NULL;
    }
    if(x < T->data){
        T->left = delete(T->left,x);
    }else if(x > T->data){
        T->right = delete(T->right,x);
    }else{
        //程序到此处表示结点已找到,需分情况进行元素删除
        //删除结点为度为2的结点时
        if(T->left&&T->right){
            p = FindMin(T->right);//找到T的右子树种的最小结点
            T->data = p->data;
            T->right = delete(T->right,T->data);
        }else{//度为0或者1的情况
            p = T;
            if(T->right==NULL){
                T = T->left;
            }else if(T->left==NULL){
                T = T->right;
            }
            free(p);
        }
    }
    return T;
}

3.一些关于树的例题

a.一棵完全二叉树上有1001个结点,其中叶子结点的个数是【501】。

解析:最后一个结点的编号为1001,根据完全二叉树性质,其父节点应为【1001/2 = 500】,即非叶子结点有500个,所以叶子结点有501个。

b.一个具有1025个结点的二叉树的高h为【11到1025之间】。

解释:此题需要分类讨论

如果是完全二叉树,一个10层高的二叉树最多有2^10-1=1023个结点,所以1025个结点有11层。

如果是一个结点占一层,则最多可以有1025层。

c.深度为h的满m叉树的第k层有【m^(k-1)】个结点。(1=<k<=h)

d.哈夫曼树不一定是完全二叉树。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值