树是N个结点的有限集合,N=0,为空树。树是一种逻辑结构,同时也是一种分层结构。树的定义是递归的:
每个结点只有一个前驱但是可以有0个或多个后继
树适合表示具有层次关系的数据结构。
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
n个结点的二叉链表中含有n+1个空指针域
void PreOrder(BiTree T){
if(T!=NULL)
{
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏的情况下,二叉树是有n个结点且深度为n的单支树,时间复杂度是O(n)
非递归形式:
//中序遍历
/*
将根节点及其左子树全部入栈操作,但不进行访问,当结点为空,停止入栈;
访问栈顶元素作为当前结点并出栈,如果当前结点有右子树则遍历访问其右子树
栈顶元素上一个元素就是当前元素的父节点
*/
void InOrder2(BiTree T){
InitStack(S);
BiTree p = T;
while(p||!IsEmpty(s))
{
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
visit(p->data);
p=p->rchild;
}
}
}
//先序遍历
/*
当栈不为空或当前结点不为空时,执行操作;
取栈顶元素为当前结点,并出栈,如果当前结点有右子树,遍历其右子树
*/
void PreOrder2(BiTree T)
{
BiTree p=T;
while(p||IsEmpty(s))
{
if(p){
visit(p->data);
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
p=p->rchild;
}
}
}
//后序遍历
/*后序遍历和前面的有所区别,要后序遍历需要节点访问两遍,所以需要一个专门的count部分记录当前节点被访问的次数*/
typedef struct NewNode{
BiTree t;
int count;
}Node,*pNode;
void PostOrder2(BiTree t){
if(T == nullptr)
return nullptr;
Node p;
p.count = 0;
p.t = t;
InitStack(S);
Push(S,p);
while(!IsEmpty(S)){
Node q;
Pop(S,q);
if(q.count == 0)
{
q.count ++;
Push(S,q);
if(q.t.lchild != nullptr)
{
q.t = q.t.lchild;
Push(S,q);
}//开始第一次一定是先访问左节点此时count=0,在第一次进入的时候判断是否有左节点可以进入
}
else if (q.count ==1 )//当count是1的时候,已经访问一次了该访问右节点了
{
q.count ++;
Push(S,q);
if(q.t.rchild!=nullptr)
{ q.t = q.t.rchild;
Push(S,q);
}
}
else if(q.count == 2)//计数两次可以访问了
{
visit(q.t.data);
}
}
}
思路:使用层次遍历需要一个队列,首先将根节点入队,然后判断当前根节点是否有左右子树,如果有左右子树则按左右的顺序进入,按照正常的出队的顺序弹出
void LevelOrder(BiTree t){
InitQueue(Q)
BiTree p;
EnQueue(Q,p);
//不用再设置一个变量更新p没有用
while(!IsEmpty(Q)){
DeQueue(Q,p);
visit(p->data);
if(p->lchild!=nullptr)
{
//temp =temp-> lchild;
EnQueue(Q,p->lchild);
}
if(p->rchild!=nullptr)
{
//temp = temp -> rchild;
EnQueue(Q,p->rchild);
}
}
}
对于一棵已知树求节点的双亲、求结点的孩子结点、求二叉树的深度、求二叉树的叶子节点个数、判断两棵二叉树是否相同
/*
结构说明;
data:数据域
lchild:ltag=1指向结点的前驱;ltag=0指向结点的左子树
rchild:rtag=1指向结点的后继;rtag=0指向结点的右子树
*/
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
由这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表;指向结点前驱和后继的指针叫做线索;加上线索的二叉树叫做线索二叉树;对线索二叉树的某种次序的遍历过程叫线索化
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=nullptr){
InThread(p->lchild,pre);
if(p->lchild==nullptr){
p->lchild = pre;
p->ltag = 1;
}
if(pre->rchild==nullptr&&pre!=nullptr)
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild,pre);
}
}
void CreateInThread(ThreadTree T){
ThreadTree pre = nullptr;
if(T!=nullptr){
InThread(T,pre);
pre->rchild = nullptr;
pre->rtag=1;
}
}
也会在二叉树的线索链表上添加一个头节点,其lchild域的指针指向二叉树的根节点,其rchild域的指针指向中序遍历是访问的最后一个节点。反之,第一个结点的lchild域和最后一个节点的rchild域均指向头节点。(建立一个双向线索链表,可以从第一个节点起后继遍历,也可以从最后一个结点起顺前驱遍历)
/*
可以利用线索二叉树实现二叉树遍历的非递归算法
a/如果rtag=1 则rchild指向中序后继
b/如果rtag=0 则中序后继应该是以当前节点为根的子树的中根序列的首节点
*/
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag == 1) return p->rchild;
return Firstnode(p->rchild);
}
//主函数
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=nullptr;p=Nextnode(T))
visit(p);
}
树的存储方式有很多种,既可以采用链式存储也可采用顺序存储,关键在于能够唯一的反映出树中各结点之间的逻辑关系。常用三种表示方法如下:
# define MaxSize 100
//树的结点定义
typedef struct{
ElemType data;
int parent;
}PTNode;
//树的类型定义
typedef struct {
PTNode nodes[MaxSize];//双亲表示
int n;//结点数
}PTree;
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
缺点:查找双亲结点比较麻烦,如果为每个结点增设一个parent域指向其父节点,则查找父节点也很方便。
树和二叉树都可以用二叉链表表示,可以由二叉链表导出二者的对应关系:给定一棵树️唯一一棵二叉树与之对应。
从物理结构上说,树的孩子兄弟表示法与二叉树的二叉链表表示法相同.可以用同一种结构的不同解释将一棵树转化为二叉树。
没有中序遍历,一个节点有多个子树,没有办法看哪个是中间的。
如果树非空,则先访问根结点,再按从左到右的顺序遍历根结点的每一棵子树,其访问顺序与这棵树的对应的二叉树的先序遍历顺序相同。
若树非空,则按从左到右的顺序遍历根节点的每一棵子树,之后再访问根结点的每一棵子树,之后再访问跟节点,其访问顺序和这棵树对应的二叉树的中序遍历顺序相同。
层次遍历与二叉树的层次遍历思想基本相同,即按层序依次访问各结点。
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
树的顺序表示:
并查集是一种简单的集合表示,支持三种操作:
Union(S,Root1,Root2):
把集合S中子集合Root2并入子集合Root1中。要求Root1和Root2互不相交,否则不执行合并Find(S,x):
查找集合S中单元素x所在的子集合并返回该子集合的名字Initial(S):
将集合S中的每一个元素都初始化为只有一个元素的集合使用双亲表示作为并查集的存储结构,每个子集合以一棵树表示,所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内,通常用数组元素的下标表示元素名,根结点的下标代表子集合名,根结点的双亲结点为负数。
两个子集合合并,只需将其中一个子集合的根结点的双亲指针指向另一个集合的根结点即可
#define SIZE 100
int UFSets[SIZE];
//并查集的初始化操作
void Initial(int S[]){
for (int i=0;i<size;i++){
S[i]=-1;
}
}
//Find操作
int Find(int S[],int x){
while(S[x]>=0)
x = S[x];
return x;
}
//Union操作
void Union(int S[],int Root1,int Root2){
//要求Root1和Root2是不同的,且表示子集合的名字
S[Root2]=Root1;
}
. 定义:
- 若左子树非空,则左子树上所有结点的关键字值均小于根结点的关键字值
- 若右子树非空,则右子树上所有结点的关键字值均大于根结点的关键字值
- 左右子树本身也分别是一棵二叉排序树
对二叉排序树进行中序遍历可以得到一个递增的有序序列
思路:
从根结点开始,沿某一个分支逐层向下进行比较。如果二叉排序树非空,将给定值与根节点的关键字比较,若相等则查找成功;若不等,如果根节点大于给定关键字值,在根结点的左子树中查找;如果根结点小于给定关键字值,在根结点的右子树中寻找。
/*
函数功能:查找函数返回指向关键字值为key的结点指针,若不存在,则返回null
p:指向被查找结点的双亲,用于插入和删除操作中
*/
BSTNode *BST_Search(BiTree T,ElemType key,BSTNode *&p){
p=nullptr;
while(T!=nullptr&&key!=T->data){
p=T;
if(key<T->data) T = T->lchild;
else T = T->rchild;
}
return T;
}
二叉排序树作为一种动态集合,其特点是树的构造不是一次生成的,而是在查找过程中不存在关键字等于给定值的结点时再进行插入。插入的新节点一定是某个叶节点。
int BST_Insert(BiTree &T,KeyType k){
if(T == nullptr)
{
T = (BiTree)malloc(sizeof(BSTNode);
T->key = k;
T->lchild = T->rchild = nullptr;
return 1;
}
else if(k == T->key)
return 0;
else if(k < T->key)
BST_Insert(T->lchild,k);
else{
BST_Insert(T->rchild,k);
}
}
二叉树的构造实际上就是从输入的第一个元素开始构建排序二叉树。所以只需要一个循环调用二叉树插入部分即可。
void Creat_BST(BiTree &T,KeyType str[],int n){
T = nullptr;
int i = 0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
思路:把删除结点从存储二叉排序树的链表中取下来,将因删除而断开的链表重新连接,但是要保留二叉排序树本来的性质。
如果是删除考虑以下三种情况:(二叉树中结点也就这三种位置情况)
思考:若在二叉排序树中删除并插入某结点,得到的二叉排序树是否和原来的相同?
如果删除的结点是叶结点的话,得到的二叉树和原来的相同;
如果不是叶节点的话,由于插入节点一定是插入在二叉树的叶节点部分,而删除的时候结点是二叉树的非叶节点,所以二者一定不同。
二叉排序树的高度为H,则复杂度为O(H)
如果二叉树退化(输入有序序列形成单链表),复杂度为O(n)
从查找过程上看,二叉排序树与二分查找相似;就平均时间性能来看,二叉排序树上的查找和二分查找差不多。但二分查找的判定树唯一,二叉排序树不唯一
。相同的关键字插入顺序不同,得到的二叉树不同。
【二叉排序树的构造是为了查找比较,所以和二分查找的最终目的是一样的,但是二叉排序树在插入和删除结点只需要更改指针的指向。插入和修改时,二叉排序树O(logN)
,二分查找O(N)
】
当有序表是静态查找表时,使用顺序表作为存储结构,采用二分查找实现查找操作;
当有序表时动态查找表时,使用二叉排序树作为逻辑结构。
在插入和删除时保证任意结点的左右子树的高度差的绝对值不超过1,这样的二叉树称为二叉平衡树。
平衡因子:结点左子树和右子树的高度差(-1,1,0)
(一般用左节点-右节点)
思路:首先要检查其插入路径上的结点是否因为这次操作导致了不平衡,如果导致不平衡,首先应该找到插入路径上离插入节点最近的平衡因子绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点之间的位置关系,使其重新达到平衡。(从下往上一点点调整)
每次调整的都是最小不平衡子树——在插入路径上离插入节点最近的平衡因子的绝对值大于1 的结点作为根的子树。
根据失去平衡后进行调整的规律归纳为以下4种情况:
LL、RR、RL、LR
左孩子的左子树插入新结点:
(1)A结点向下旋转变成B结点的右子树
(2)B结点代替A变成根结点
(3)原B结点的右子树变成A结点的左子树
右孩子的左子树插入新结点:
先将A结点的右孩子B的左子树的根结点C向右上旋转到(右上旋转包括根结点的替换和替换上去的结点的有一个子树被拆下换到原来根结点的子树部分)B结点位置,然后再将C结点旋转到左上替换到A结点的位置。
左孩子的右子树插入新结点:
同上。
O(logN)
,也是平衡二叉树的平均查找长度内通路长度:从根到每个内节点的路径长度之和
外通路长度:从根到每个外结点的路径长度之和
带权路径长度:从树的根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权长度:树中所有带权路径长度之和
WPL = 0;
for( int i=1;i<=n;i++)
WPL = WPL+wi*Li;
最优二叉树/哈夫曼树:WPL最小的二叉树
扩充二叉树:每当二叉树中出现空子树时,就增加特殊的结点——空树叶,由此生成的二叉树。
哈夫曼算法:
哈夫曼编码:将每个出现的字符作为一个独立的结点,其权值为它出现的频度,构造出对应的哈夫曼树。将字符的边编码解释为从根到该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”
⚠️每个结点的权值都是其所有直接子节点之和
⚠️构造的哈夫曼树不唯一,但是各哈夫曼树的带权路径相同且为最优