三.树和二叉树
文章目录
树的特点:
①.树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点。
②.树中所有结点可用有零个或多个后继结点。
树的基本术语:
①结点的度:树中每个结点具有的子树个数称为结点的度。
②树的度:树中所有结点度的最大值称为树的度。
③叶子结点:度为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.哈夫曼树不一定是完全二叉树。