树与二叉树的解析与实例

what`s 树

  • 客观世界中许多事物存在层次关系
  • 分层次组织再管理上具有更高对的效率
  • 层次管理(树)
查找
  • 静态查找:集合中记录是固定的
    • 没有插入和删除操作,只有查找
    • 查字典
  • 动态查找:集合中记录是动态变化的
    • 除查找外,还可能发生插入或删除
静态查找
方法1:顺序查找
  • 数据从1开始存储
  • 哨兵,可以在数组的边界设一个值,无需在每一个循环都判断条件
#include <stdio.h>
#include <stdlib.h>
#include<iostream>

using namespace std;

constexpr auto MAXSIZE = 100;;
typedef int ELement;
struct Lnode {
	ELement ele[MAXSIZE];
	int length;
};
typedef Lnode* list;

//有哨兵的情形

int sequentialSearch(list tb1, ELement k)
{
	int i = 0;
	tb1->ele[0] = k;//建立哨兵
	for (i = tb1->length; tb1->ele[i] != k; i--);
	return i;
}

//无哨兵
int sequentialSearch2(list tb1, ELement k)
{
	int i = 0;
	for (i = tb1->length; i>0&&tb1->ele[i] != k; i--);
	return i;
}


  • 时间复杂性O(n)
方法2:二分查找
  • 时间复杂度为log2(N)

  • 前提条件

    • 假设n个数据元素的关键字满足有序排列
    • 并且是连续存放的(数组),即可使用二分查找
  • 一直取中间,跟中间值进行对比

    //二分查找
    int BinarySearch(list tb, ELement k)
    {
    	int left, right, mid, notfound = -1;
    	left = 1;//初始左边界
    	right = tb->length;//初始右边界
    	while (left <= right)
    	{
    		mid = (left + right) / 2;
    		if (k < tb->ele[mid])
    		{
    			right = mid - 1;//调整右边界
    		}
    		else if (k > tb->ele[mid])
    		{
    			left = mid + 1;//调整左边界
    		}
    		else
    			return mid;
    	}
    	return notfound;
    }
    
11个元素的二分查找判定树

img

  • 判定树上每个结点需要的查找次数刚好为该结点所在的层数
  • 查找成功时查找次数不会超过判定树的深度
  • n个结点的判定树深度为[log2(n)]+1
  • ASL(平均查找的次数)=每层数*个数的总和取平均

树的定义

树:n个结点构成的有限集合

  • 当n=0是,称为空树

  • 当n>0时,称为非空树,其具备以下性质

    • 树种有一个称为“”的结点,用r表示
    • 其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一颗树,称为原来树的子树
树与非树

同级结点需满足互不相交,每级结点只能与上一结点有且仅有一条连线,不允许跨越层数

  • 子树是不相交的
  • 除了根节点外,每个结点有且仅有一个父结点
  • 一颗N个结点的树有N-1条边
  • 树是保证结点联通的最小的一种连接方式
树的一些基本术语
  1. 结点的度:结点的子树个数
  2. 树的度:树的所有结点中最大的度数
  3. 叶节点:度为0的结点
  4. 父节点:有子树的结点是其子树的根结点的父节点
  5. 子节点,如果A结点是B结点的父结点,则称B结点是A结点的子结点,子结点也称孩子结点
  6. 兄弟结点:具有同一父节点的各结点彼此是兄弟结点
  7. 路径和路径的长度:路径中所包含边的个数为路径的长度
  8. 祖先节点:沿树根到某一结点路径上的所有结点都是这个结点的祖先结点
  9. 子孙结点:某一结点的子树中所有结点是这个结点的子孙
  10. 结点的层次:规定根结点在1层,其他任一结点的层数是其父节点的层数加1
  11. 树的深度:树中所有结点中的最大层次是这颗树的深度
树的表示
数组实现
链表实现
儿子-兄弟表示法

element->firstchild

​ ->nextsibling

二叉树就是度为2的一种树,即每个结点指针最多是两个

二叉树的定义

二叉树T:一个有穷的结点集合

  • 这个集合可以为空
  • 若不为空,则它是由根节点和称为其左子树TL和右子树TR的两个不想交的二叉树组成
  • 某种程度上,可以将二叉树理解为度为2的树,但存在左右之分

二叉树具有五种基本形式

  • 空树
  • 只有一个结点
  • 有一个结点,只有左子树
  • 有一个结点,只有右子树
  • 有一个结点,左右子树均存在

特殊二叉树

  • 斜二叉树

  • 完美二叉树(满二叉树)

  • 完全二叉树

    • 完全二叉树是完美二叉树的子集,是在完美二叉树的基础上缺掉部分最底层的结点
二叉树几个重要性质
  • 一个二叉树的第i层的最大结点数为:2^(i-1)
  • 深度为k的二叉树有最大结点总数为2^(k)-1
  • 二叉树的结点可以分为三种类型,叶节点(没有儿子),只有一个儿子,有两个儿子
  • 对任何非空的二叉树,叶节点的个数=度为2的非叶节点个数+1
  • 一个二叉树的结点最多只有两个子结点
二叉树的抽象数据类型及定义

数据类型:一个有穷的结点集合,若不为空,则由根节点和其左、右二叉子树组成

操作集

  • Boolearn isempty(bintree bt);判断bt是否为空
  • void traversal(bintree bt),遍历,按某顺序访问每一个结点
  • bintree creatbintree,创建一个二叉树

常见的遍历方法

  • void preordertraversal(bintree bt);先序–>根、左子树、右子树
  • void inordertraversal(bintree bt);中序–>左子树、根、右子树
  • void postordertraversal(bintree bt);后序–>左子树、右子树、根
  • void levelordertraversal(bintree bt);层次遍历,从上到下、从左到右
二叉树的存储结构
顺序存储结构

完全二叉树:按从上到下、从左到右顺序存储n个结点的完全二叉树的结点父子关系

  • 对结点进行编号
  • 将编号与数组对应起来
  • 非根结点(序号i>1)的父结点的序号是i/2;
  • 结点(序号为i)的左孩子结点的序号是2i(若2i<=n,否则没有左孩子)
  • 结点(序号为i)的右孩子结点的序号是2i+1(若2i+1<=n,否则没有右孩子)
  • 对于一般的二叉树,则将其补充成完全二叉树,相应缺的结点,在数组中留下一个空位即可
链表存储

一般的二叉树每个结点分为三个域,左边的域指向左儿子,中间的域指向数据,右边的域指向右儿子

typedef struct treenode* bintree;
struct treenode {
	ELement data;
	bintree left;
	bintree right;
};
二叉树的遍历

主要以链式存储为目标

先序遍历:
  1. 访问根结点
  2. 先序遍历其左子树
  3. 先序遍历其右子树
void preordertraversal(bintree bt)
{
	if (bt)
	{//先访问左边,然后依照就近原则访问右边
		cout << bt->data;
		preordertraversal(bt->left);
		preordertraversal(bt->right);
	}
}
中序遍历:
  1. 中序遍历其左子树
  2. 访问根结点
  3. 中序遍历其右子树
void inordertraversal(bintree bt)
{
	if (bt)
	{
		inordertraversal(bt->left);
		cout << bt->data;
		inordertraversal(bt->right);
	}
}
后序遍历:
  1. 后序遍历其左子树
  2. 后序遍历其右子树
  3. 访问根结点
//后序遍历
void postordertraversal(bintree bt)
{
	if (bt)
	{
		postordertraversal(bt->left);
		postordertraversal(bt->right);
		cout << bt->data;
	}
}

先序、中序和后序的遍历过程,便利过程中经过结点的路线一样,只是访问各结点的时机不同

二叉树的非递归遍历

直接用堆栈实现,而不用递归

中序遍历非递归遍历算法
  • 碰到一个结点,就把它压入堆栈中,并去遍历他的左子树
  • 当左子树遍历结束后,从栈顶弹出这个结点并访问它
  • 然后按其右指针再去中序遍历该结点的右子树
void inordertravarsal_stack(bintree bt)
{
	bintree t = bt;
	stack s = CreateStack();
	while (t || !IsEmpty(s))
	{
		while (t)
		{
			Push(s, t);
			t = t->left;
		
	}
	if (!IsEmpty(s))
	{
		t = Pop(s);
		cout << t->data;
		t = t->right;

	}
    }
}
先序遍历非递归遍历算法
void preordertravarsal_stack(bintree bt)
{
	bintree t = bt;
	stack s = CreateStack();
	while (t || !IsEmpty(s))
	{
		while (t)
		{
			Push(s, t);
			cout << t->data;
			t = t->left;
		}

	if (!IsEmpty(s))
	{
		t = Pop(s);
		t = t->right;

	}
    }
}
后续遍历非递归算法

先序的访问顺序是root, left, right ,假设将先序左右对调,则顺序变成root, right, left,暂定称之为“反序”。

后序遍历的访问顺序为left, right,root ,刚好是“反序”结果的逆向输出。

我们可以使用双堆栈进行相应的操作,堆栈S用于相应的操作,堆栈Q用于将最终反序的结果进行输出,也就是将root->right->left的顺序压入堆栈Q,最后输出

于是方法如下:

1、反序遍历二叉树,具体方法为:将先序遍历代码中的left 和right 对调即可。数据存在堆栈S和Q中。

2、将print结点改为把当前结点 PUSH 到堆栈Q中。

3、反序遍历完成后,堆栈Q的压栈顺序即为反序遍历的输出结果。 此时再将堆栈Q中的结果pop并print,即为“反序”结果的逆向,也就是后序遍历的结果。

缺点是堆栈Q的深度等于数的结点数,空间占用较大。


void InOrderTraversal( BinTree BT )
{
   BinTree T BT;
   Stack S = CreatStack( MaxSize ); /*创建并初始化堆栈S*/
   Stack Q = CreatStack( MaxSize ); /*创建并初始化堆栈Q,用于输出反向*/
   while( T || !IsEmpty(S) ){
       while(T){ /*一直向右并将沿途结点压入堆栈*/
           Push(S,T);
           Push(Q,T);/*将遍历到的结点压栈,用于反向*/
           T = T->Right;
       }
       if(!IsEmpty(S)){
       T = Pop(S); /*结点弹出堆栈*/
       T = T->Left; /*转向左子树*/
       }
   }
   while( !IsEmpty(Q) ){
       T = Pop(Q);
       printf(“]”, T->Data); /*(访问)打印结点*/
   }
}
层序遍历

二叉树遍历的核心的问题:二维结构的线性化

  • 从结点访问其左,右儿子结点
  • 访问左儿子后,右儿子结点怎么办?
    • 需要一个存储结构保存暂时不访问的结点
    • 存储结构:堆栈、队列

队列实现:遍历从根节点开始,首先将根节点入队,然后执行循环:结点出队、访问该结点、其左右儿子入队

层序基本过程:
  • 先根节点入队
  • 从队列中取出一个元素
  • 访问该元素所指结点
  • 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队
void levelordertraversal(bintree bt)
{
	Queue Q;
	bintree t;
	if (!bt)
		return; ///如果是空树,直接返回
	Q = createQueue(MaxSize);
	Add(Q, bt);
	while (!IsEmpty(Q))
	{
		t = deleteQ(Q);
		cout << t->data;
		if(t->left)
			Add(Q, t->left);
		if (t->right)
			Add(Q, t->right);
	}
}
遍历二叉树的应用
  • 输出二叉树中的叶子结点

  • 在二叉树的遍历算法中增加检测结点“左右子树是否都为空”

void preordertraversal(bintree bt)
{
	if (bt)
	{
		if (!bt->left && !bt->right)
			cout << bt->data;
		cout << bt->data;
		preordertraversal(bt->left);
		preordertraversal(bt->right);
	}
}
  • 求二叉树的高度

二叉树的高度=等于左右两个子树的最大高度+1

(必须知道左右两个子树的高度)->后续遍历

int postordergethigh(bintree bt)
{
	int hl, hr, maxh;
	if (bt)
	{
		postordergethigh(bt->left);
		postordergethigh(bt->right);
		maxh = (hl > hr) ? hl : hr;
		return (maxh + 1);
	}
	else
		return 0;
}
  • 二元运算表达式树及其遍历

叶节点代表运算数,非叶结点代表运算符号

不同的遍历方式得到的表达式不同

先序遍历得到前缀表达式

中序遍历得到中缀表达式

后序表达式得到后缀表达式

中缀表达式会受到运算优先级的影响,故不太准,可以通过在每个子树输出前加个括号,解决上述问题

  • 由两个遍历序列确定二叉树

    • 当给出一个中序序列,无论接下来所给的是前序还是后序都可以确定
    • 但光给前序后序无法唯一确定二叉树
    • 必须要有中序遍历
  • 先序和中序遍历序列确定一颗二叉树

    • 根据先序遍历序列第一个结点确定根节点
    • 根据根节点在中序遍历序列中分割出左右两个子序列
    • 对左子树和右子树分别递归使用相同的方法继续分解

小白专场:树的同构模拟

题目详情:

给定两颗树T1和T2,若t1可以通过若干次左右孩子互换变成t2,则我们称这两棵树是同构的

关键问题:

判断根节点的位置在哪里

求解思路
  • 二叉树的表示
  • 建立二叉树
  • 同构判别

二叉树表示:将二叉树看作完全二叉树

结构数组表示二叉树:静态链表

判别根的有效方法:即看看有没有那个结点的位置没有被指向,则改点为根节点

程序框架搭建
  1. 建立二叉树1
  2. 建立二叉树2
  3. 判别是否同构并输出
main函数
int main()
{
	tree r1, r2;
	r1 =buildtree(t1);
	r2= buildtree(t2);
	if (isomorphic(r1, r2))
	{
		cout << "Yes" << endl;
	}
	else
	{
		cout << "No" << endl;
	}
	return 0;
}
建立存储
tree buildtree(struct Treenode T[])
{
	int n, i,root;
	char cl, cr;
	int check[100];
	cin >> n;
	if (n)
	{
		for (i = 0; i < n; i++)
			check[i] = 0;
		for (i = 0; i < n; i++)
		{
			cin >> T[i].element >> cl >> cr;
			if (cl!='-')
			{
				T[i].left = cl - '0';
				check[T[i].left] = 1;
			}
			else
			{
				T[i].left = Null;
			}

			if (cr != '-')
			{
				T[i].right = cr - '0';
				check[T[i].right] = 1;
			}
			else
			{
				T[i].right = Null;
			}
		}
		for (i = 0; i < n; i++)
		{
			if (!check[i])
				break;
		}
		root = i;

	}
	return root;
}
比对是否同构
int isomorphic(tree r1, tree r2)
{
	if ((r1 == Null) && (r2 == Null))
		return 1;
	if (((r1 == Null) && (r2 != Null))|| ((r1 != Null) && (r2 == Null)))
		return 0;
	if (t1[r1].element != t2[r2].element)
		return 0;
	if ((t1[r1].left==Null)&& (t2[r2].left==Null))
		return isomorphic(t1[r1].right, t2[r2].right);

	if (((t1[r1].left != Null) && (t2[r2].left != Null))&& ((t1[t1[r1].left].element) == (t2[t2[r2].left].element)))
		return (isomorphic(t1[r1].left, t2[r2].left)&&isomorphic(t1[r1].right, t2[r2].right));
	else
		return (isomorphic(t1[r1].left, t2[r2].left) && isomorphic(t1[r1].right, t2[r2].right));
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值