树
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个元素的二分查找判定树
- 判定树上每个结点需要的查找次数刚好为该结点所在的层数
- 查找成功时查找次数不会超过判定树的深度
- n个结点的判定树深度为[log2(n)]+1
- ASL(平均查找的次数)=每层数*个数的总和取平均
树的定义
树:n个结点构成的有限集合
-
当n=0是,称为空树
-
当n>0时,称为非空树,其具备以下性质
- 树种有一个称为“根”的结点,用r表示
- 其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一颗树,称为原来树的子树
树与非树
同级结点需满足互不相交,每级结点只能与上一结点有且仅有一条连线,不允许跨越层数
- 子树是不相交的
- 除了根节点外,每个结点有且仅有一个父结点
- 一颗N个结点的树有N-1条边
- 树是保证结点联通的最小的一种连接方式
树的一些基本术语
- 结点的度:结点的子树个数
- 树的度:树的所有结点中最大的度数
- 叶节点:度为0的结点
- 父节点:有子树的结点是其子树的根结点的父节点
- 子节点,如果A结点是B结点的父结点,则称B结点是A结点的子结点,子结点也称孩子结点
- 兄弟结点:具有同一父节点的各结点彼此是兄弟结点
- 路径和路径的长度:路径中所包含边的个数为路径的长度
- 祖先节点:沿树根到某一结点路径上的所有结点都是这个结点的祖先结点
- 子孙结点:某一结点的子树中所有结点是这个结点的子孙
- 结点的层次:规定根结点在1层,其他任一结点的层数是其父节点的层数加1
- 树的深度:树中所有结点中的最大层次是这颗树的深度
树的表示
数组实现
链表实现
儿子-兄弟表示法
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;
};
二叉树的遍历
主要以链式存储为目标
先序遍历:
- 访问根结点
- 先序遍历其左子树
- 先序遍历其右子树
void preordertraversal(bintree bt)
{
if (bt)
{//先访问左边,然后依照就近原则访问右边
cout << bt->data;
preordertraversal(bt->left);
preordertraversal(bt->right);
}
}
中序遍历:
- 中序遍历其左子树
- 访问根结点
- 中序遍历其右子树
void inordertraversal(bintree bt)
{
if (bt)
{
inordertraversal(bt->left);
cout << bt->data;
inordertraversal(bt->right);
}
}
后序遍历:
- 后序遍历其左子树
- 后序遍历其右子树
- 访问根结点
//后序遍历
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
- 建立二叉树2
- 判别是否同构并输出
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));
}