目录
文章目录
前言
一.树的表达方式
1.树的概念
2.树的结点
3.树的存储结构
01.双亲表示法
顺序表示形式
优缺点说明
02.孩子表示法
03.孩子兄弟表示法
04.非类存储代码演示
二.二叉树
1.树的特点
2.二叉树
01.定义
02.二叉树的性质
03.满二叉树
04.完全二叉树
3.二叉树的存储结构
01.顺序结构
编辑
02.链式存储
03.二叉树的遍历
03.01递归遍历
03.02前序遍历
03.03中序遍历
03.04后序遍历
03.05层序遍历
03.06代码非递归和递归演示
04.扩展二叉树
05.二叉排序树(二叉查找树、二叉搜索树)
05.01性质
05.02构建二叉排序树
05.03 二叉排序树的查找操作
05.04 二叉排序树的插入操作
05.05二叉树的删除操作
05.06 时间复杂度分析
三.二叉平衡树(AVL树)
1.为什么要有平衡二叉树
2.定义
3.平衡因子
01.定义
02.结点结构
4.AVL树插入时的失衡与调整
编辑
01.左旋
02.右旋
5. AVL树的四种插入节点的方式
6. A的左孩子的左子树插入结点(LL)
7.A结点的右孩子的右子树插入结点(RR)
8.A结点的左孩子的右子树插入结点(LR)
9.A结点的右孩子的左子树插入结点(RL)
10.AVL树的四种删除结点方式
01.删除叶子节点
02.如果删除的结点只有左子树或者右子树
03. 删除的结点既有左子树又有右子树
11.代码演示
四.线索二叉树
1.线索化
2. 线索化带来的问题
3.带头结点的线索化
4. 优点和应用
五.最小生成树
1.生成树的定义
2.生成树的属性
3.最小生成树
4.克鲁斯卡尔(Kruskal)算法
5.贪婪算法
01.贪心算法思想
02.贪心算法的基本思路
6.普里姆(Prim)算法
7.最小生成树总结
六.哈夫曼树、哈夫曼编码
1.一些概念
2.应用场景
3.哈夫曼树的构造过程
4.哈夫曼编码
01.定长编码
01.01定长编码的缺陷
02.变长编码
02.01前缀属性
02.02前缀码
5.哈夫曼树的特征
01.哈夫曼编码树是一颗二叉树
02.从根结点到包含字符的叶子结点的路径上获得的叶结点的编码
03.编码均具有前缀属性
6.代码演示
七.树、二叉树和森林的转换
1. 普通树转为二叉树步骤
2.森林转换为二叉树
3.二叉树转为树、森林
八.并查集
1.基本操作
2.树的表现形式
3.查找
4.合并
5.路径压缩
6.洛谷p1551
总结
文章目录
- 前言
- 一.树的表达方式
- 二.二叉树
- 三.二叉平衡树
- 四.线索二叉树
- 五.最小生成树
- 六.哈夫曼树、哈夫曼编码
- 七.树、二叉树和森林的转换
- 八.并查集
- 总结
前言
树是非常常用的数据结构,知识点也非常多,本文也只是较为简单的,像红黑树什么的并没有哈
一.树的表达方式
我们所介绍的树的结构是一种非线性的存储结构。存储的是具有一对多的关系的数据元素的集合。
1.树的概念
图中可以看见一个使用树形结构存储的一个集合,这个集合就是{A,B,C.......}。对于数据A来说,和数据B、C、D有关系。对于数据B来说,和E,F,G有关系。这就是一对多的关系。我们将一对多的关系的集合中的数据元素按照图中的形式进行存储,整个存储形状在逻辑结果上面看,类似于实际生活中倒着的树,所以就将这种结构称之为树形结构。
2.树的结点
结点:使用树结构存储的每一个数据元素都被称为“结点”。例如图中的A就是一个结点。
根结点:有一个特殊的结点,这个结点没有前驱,我们将这种结点称之为根结点。
父结点(双亲结点)、子结点和兄弟结点:对于ABCD四个结点来说,A就是BCD的父结点,也称之为双亲结点。而BCD都是A的子结点,也称之为孩子结点。对于BCD来说,因为他们都有同一个爹,所以它们互相称之为兄弟结点。
叶子结点:如果一个结点没有任何子结点,那么此结点就称之为叶子结点。
结点的度:结点拥有的子树的个数,就称之为结点的度。
树的度:在各个结点当中,度的最大值。为树的度。
树的深度或者高度:结点的层次从根结点开始定义起,根为第一层,根的孩子为第二层。依次类推。
3.树的存储结构
01.双亲表示法
顺序表示形式
//Node结点
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Node {//树种存放的数据int data;//该结点的⽗节点在数组中的下标位置int parent;public Node(int data,int index){this.data = data;this.parent = index;}public int getData() {return data;}public void setData(int data) {this.data = data;}public int getParent() {return parent;}public void setParent(int parent) {this.parent = parent;}
}
//双亲表示法
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class TreeParentDemo {Node node[];int size;//当前元素个数int maxSize;//当前最⼤元素个数public TreeParentDemo(int temp){node = new Node[temp];this.maxSize = temp;this.size = 0;}/*** 构建跟结点* @param data 根节点的数据*/public void insert(int data){//已有根节点 ⽆法继续构建根节点if(size > 1){//提示}Node node = new Node(data,-1);this.node[size] = node;size++;}/*** 插⼊⼦节点* @param data 待插⼊⼦节点的数据* @param index 该⼦节点插⼊到哪个结点下⾯*/public void insertSon(int data,int index){//是否有这个⽗节点 如果没有该⽗节点 那就 提示⼀下Node node = new Node(data,index);this.node[size] = node;size++;}public void show(){for (int i = 0; i < size;i++){System.out.println("数据:" + node[i].getData() + "⽗级结点为:" + node[i].getParent());}}
}
//测试数据
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Test {public static void main(String[] args) {TreeParentDemo demo = new TreeParentDemo(10);demo.insert(1);demo.insertSon(2,0);demo.insertSon(3,0);demo.insertSon(4,0);demo.insertSon(7,1);demo.show();}
}
优缺点说明
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。可如果我们要知道结点的孩子是什么,对不起,请遍历整个结构才行。这真是麻烦,能不能改进一下呢?当然可以。我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1。
对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。甚至是有2个孩子,知道了长子是谁,另一个当然就是次子了。另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那我们怎么办?嗯,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。但如果结点的孩子很多,超过了2个。我们又关注结点的双亲、又关注结点的孩子、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。
02.孩子表示法
03.孩子兄弟表示法
//结点
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Node {//数据int data;//孩⼦指针域Node child;//兄弟指针域Node sibling;public Node(int data){this.data = data;}
}
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class TreeCP {Node root;//新增⼀个临时结点 ⽅便接收定位的数据Node tempNode;public TreeCP(int data){root = new Node(data);}/*** 在指定的位置添加结点* @param data*/public void insert(int data,int temp){//定位到该节点getNode(root,temp);Node node = tempNode;if (node == null){System.out.println("没有结点");} else{if (node.child == null){node.child = new Node(data);}else {Node tempNode = node.child;Node newNode = new Node(data);newNode.sibling = tempNode.sibling;tempNode.sibling = newNode;}}}public void getNode(Node node,int temp){if (node.data == temp){tempNode = node;}//如果我的兄弟结点⾮空,则取出元素if (node.sibling != null){getNode(node.sibling,temp);}if (node.child != null){getNode(node.child,temp);}}
}
//测试
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Test {public static void main(String[] args) {TreeCP treeCP = new TreeCP(1);treeCP.insert(2,1);treeCP.insert(3,1);treeCP.insert(4,2);System.out.println(treeCP.root.child.sibling.data);}
}
这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过firstchild找到此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有做陷的,那怎么办呢?对,如果真的有必要,完全可以再增加一个parent指针域来解决快速查找双亲的问题,这里就不再细谈了。
04.非类存储代码演示
上面的实例都是用了结构体和类,下面我们用更熟悉的非结构体书写
//双亲表示法
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct node{int data;int fi;
}t[1000];
int size;
void initTree(int x){t[1].data=x;t[1].fi=-1;size++;
}
int find(int fx){for(int i=1;i<=size;i++){if(t[i].data==fx){return i;}}return -1;
}
void insert(int x,int fx){size++;t[size].data=x;int fx_i=find(fx);if(fx_i==-1){printf("%d的父节点不存在\n",x);return;}t[size].fi=fx_i;
}
void find_ch(int x){int x_i=find(x);int sum=0;for(int i=1;i<=size;i++){if(t[i].fi==x_i){sum++;printf("%d ",t[i].data);}}if(sum==0)printf("%d没有孩子结点",x);printf("\n");
}
void find_fa(int x){int x_i=find(x);int fa_I=t[x_i].fi;if(fa_i==-1)printf("该节点是根节点,没有父亲节点");elseprintf("%d ",t[fa_i].data);printf("\n");
}
int main()
{int n;//节点的总数scanf("%d",&n);int root;//根节点的数据scanf("%d",&root);initTree(root); int x,fx;//fx是x的父亲 for(int i=2;i<=n;i++){scanf("%d %d",&x,&fx);insert(x,fx);} //寻找x的孩子和父亲 scanf("%d",&x);find_ch(x); find_fa(x); return 0;
}//孩子表示法
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
typedef struct chNode{char data;struct chNode* next;
}chNode;
struct Tree{char data;chNode* first;
}t[1005];
int size;
void initTree(char root){size++;t[size].data=root;t[size].first=NULL;
}
int find(char fx){for(int i=1;i<size;i++){if(t[i].data==fx){return i;}}return -1;
}
void insert(char x,char fx){size++;t[size].data=x;t[size].first=NULL;int fx_I=find(fx);if(fx_i!=-1){chNode* s=(chNode*)malloc(sizeof(chNode));s->data=x;//头插法s->next=t[fx_i].first;t[fx_i].first=s;}
}
void find_ch(char x){int x_i=find(x);chNode* p=t[x_i].first;if(p==NULL){printf("%c没有孩子节点\n",x);return;}while(p!=NULL){printf("%c ",p->data);p=p->next;}printf("\n");
}
void find_fa(char x){chNode* p=NULL;int flag=0;for(int i=1;i<size;i++){p=t[i].first;while(p!=NULL&&p->data!=x)p=p->next;if(p!=NULL&&p->data==x){printf("%c\n",t[i].data);flag=1;break;}}if(flag==0)printf("%c是根节点\n",x);
}
int main()
{int n;//结点个数 scanf("%d",&n);char root;getchar();scanf("%c",&root); initTree(root);char x,fx;for(int i=2;i<=n;i++){getchar();scanf("%c %c",&x,&fx);insert(x,fx);}getchar();scanf("%c",&x);find_ch(x);find_fa(x);return 0; } //孩子兄弟表示法
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//孩子兄弟表示法————二叉链表
typedef struct Node{char data;struct Node* first;struct Node* bro;
}TNode,*TreeList;
TreeList initTree(char root){TNode* s=(TNode*)malloc(sizeof(TNode));s->data=root;s->first=s->bro=NULL;return s;
}
//大问题是:在以r为根节点的树中,找到fx所在的结点
//1.和根结点对比,r->data==fx,reture r; 否则执行2
//2.问题转化为去以r->first为根的子树中找fx find(r->fisrt,fx)
//3.或者 以r->bro为根的子树中找fx find(r->beo,fx)
//递归出口: r==NULL
TNode* find(TreeList r,char fx){if(r==NULL||r->data==fx)return r;if(r->first!=NULL){TNode* ans=find(r->first,fx);if(ans!=NULL&&ans->data==fx)return ans;} if(r->bro!=NULL){TNode* ans=find(r->bro,fx);if(ans!=NULL&&ans->data==fx)return ans;}return NULL;
}
//往以r为根节点的树中,插入数据x,x的父亲数据是fx
void insert(TreeList r,char x,char fx){TNode* f=find(r,fx);if(f==NULL){printf("该父亲结点不存在,插入失败\n");return ;}TNode* s=(TNode*)malloc(sizeof(TNode));s->data=x;if(f->first==NULL){f->first=s;s->first=NULL;s->bro=NULL;}else{TNode* fir=f->first;s->bro=fir->bro;fir->bro=s;s->first=NULL;}
}
int main()
{int n;//结点个数 char root;TreeList r=NULL;scanf("%d",&n);getchar(); scanf("%c",&root);r=initTree(root);char x,fx;for(int i=2;i<=n;i++){getchar(); scanf("%c %c",&x,&fx);insert(r,x,fx);}getchar(); scanf("%c",&x);TNode* p=find(r,x); TNode* fir=p->first;if(fir!=NULL){p=fir;while(p!=NULL){printf("%c ",p->data );p=p->bro;}}}
/*
10
A
B A
C A
D A
E B
F B
G B
H D
I D
J E
D
*/
/*
二.二叉树
1.树的特点
每个结点有零个或多个子结点。
没有父结点的结点称之为根结点
每个非根结点只有一个父结点
除了根结点之外,每个子结点可以分为多个不相交的子树。
2.二叉树
01.定义
二叉树是每个结点最多有两个子树的树结构。也就是说二叉树不允许存在度大于2的树。它有五种最基本的形态:二叉树可以是空集。根可以有空的左子树或者右子树;或者左右子树都是空。其中只有左子树或者右子树的叫做斜树。
02.二叉树的性质
性质 | 内容 |
性质一 | 在二叉树的 i 层上至多有2^(i-1)个结点(i>=1) |
性质二 | 深度为 k 的二叉树至多有 2^(k)-1 个结点(i>=1) |
性质三 | 在一棵二叉树中,除了叶子结点(度为0)之外,就剩下度为2(n2)和1(n1)的结点了。则树的结点总数为T = n0+n1+n2;在⼆叉树中结点总数为T,而连线数为T-1.所以有:n0+n1+n2-1 =2*n2 +n1;最后得到n0 = n2+1; |
性质四 | 具有 n 个结点的完全二叉树的深度为 [log2n] + 1 向下取整 |
性质五 | 如果有一棵有 n 个结点的完全二叉树(其深度为 [log2n] + 1,向下取整)的结点按层次序编号(从第 1 层到第 [log2n] + 1,向下取整层,每层从左到右),则对任一结点 i(1 <= i <= n)有: 1.如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲是结点 [i / 2],向下取整 |
03.满二叉树
满二叉树要求所有的分支结点都存在左右子树,并且所有的叶结点都在同一层上,若满二叉树的层数为 n,则结点数量为 2n-1 个结点,子叶只能出现在最后一层,内部结点的度都为 2,如图所示。
04.完全二叉树
从定义上来说,完全二叉树是满足若对一棵具有 n 个结点的二叉树按层序编号,如果编号为 i 的结点 (1 ≤ i ≤ n)于同样深度的满二叉树中编号为 i 的结点在二叉树的位置相同的二叉树。这样讲有些繁琐,可以理解为完全二叉树生成结点的顺序必须严格按照从上到下,从左往右的顺序来生成结点,如图所示。
因此我们就不难观察出完全二叉树的特点,完全二叉树的叶结点只能存在于最下两层,其中最下层的叶结点只集中在树结构的左侧,而倒数第二层的叶结点集中于树结构的右侧。当结点的度为 1时,该结点只能拥有左子树。
3.二叉树的存储结构
01.顺序结构
由于二叉树的结点至多为2,因此这种性质使得二叉树是可以使用顺序存储结构来描述的。在使用顺序存储结构时我们需要令数组的下标体现结点之间的逻辑关系。我们先来看完全二叉树,如果我们按照从上到下,从左到右的顺序遍历完全二叉树时,顺序是这样的:
那么这个时候我们发现,如果设父结点的序号为k,则子结点的序号非别为2k或者2k+1,子结点的序号和父结点都是相互对应的。因此我们就可以用顺序存储结构来进行描述。如图:
用顺序存储结构描述如图示
那么对于一般的二叉树呢?我们可以利用完全二叉树的编号来实现,在完全二叉树对应的结点是空结点。修改值为null。
如果是一个最特殊的二叉树,对于一颗斜树,我们开辟的空间数远超过实际使用的空间,这样空间就被浪费了。因此顺序存储结构可行,但是不合适。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char data[1005];
int size;
int flag;
int find(char fx){for(int i=1;i<=size;i++){if(data[i]==fx){return i;}}return -1;
}
int main(){int n,fx_i,x_i;char x,fx;scanf("%d",&n);for(int i=0;i<=n;i++)data[i]=' ';getchar();scanf("%c",&x); data[1]=x;size=1;for(int i=2;i<=n;i++){getchar();scanf("%c %c %d",&x,&fx,&flag);fx_i=find(fx);if(fx_i!=-1){if(flag==0)x_i=fx_i*2;elsex_i=fx_i*2+1;data[x_i]=x;size++;}}getchar();scanf("%c",&x);x_i=find(x);printf("孩子节点:%c %c\n",data[2*x_i],data[2*x_i+1]);return 0;
}
02.链式存储
由于二叉树的每个结点最多只能有两个子树,因此我们就不需要使用上述的3种表达法来做。可以直接设置一个结点具有两个指针域与一个数据域,那么这样就可以建好二叉树链表。
结构定义:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct BTNode{char data;struct BTNode* left;struct BTNode* right;
}BTNode,*BTree;
int flag;
BTree initTree(char x){BTNode* r=(BTNode*)malloc(sizeof(BTNode));if(r==NULL){printf("分配失败\n");return NULL;}else{r->data=x;r->left=r->right=NULL;return r;}
}
BTNode* find(BTree ro,char fx){if(ro==NULL||ro->data==fx)return ro;if(ro->left!=NULL){BTNode* f=find(ro->left,fx);if(f!=NULL&&f->data==fx)return f;}if(ro->right!=NULL){BTNode* f=find(ro->right,fx);if(f!=NULL&&f->data==fx)return f;}return NULL;
}
void insert(BTree ro,char x,char fx,int flag){BTNode* f=find(ro,fx);if(f!=NULL){BTNode* s=(BTNode*)malloc(sizeof(BTNode));s->data=x;s->left=s->right=NULL;if(flag==0)f->left=s;else f->right=x;}
}
int main()
{int n;char x,fx;BTree ro=NULL;scanf("%d",&n);getchar();scanf("%c",&x);ro=initTree(x);for(int i=2;i<=n;i++){ getchar();scanf("%c %c %d",&x,&fx,&flag);insert(ro,x,fx,flag);}getchar();scanf("%c",&x);BTNode* p=find(ro,x); if(p!=NULL){if(p->left!=NULL){printf("左%c\n",p->left->data );}if(p->right !=NULL){printf("右%c\n",p->right ->data );}}return 0;
}
03.二叉树的遍历
03.01递归遍历
我们先不急着谈二叉树,我们先来回忆一下斐波那契。我们用斐波那契如何实现递归?
/*** 对于⼀个斐波那契* f(n) = f(n - 2) + f(n - 1)(n >= 2) 其中f(0) = 0,f(1) = 1。*/public static void main(String[] args) {}public int Fibonacci(int n){if (n == 0){return 0;}else if(n == 1){return 1;}else{return Fibonacci(n - 2) + Fibonacci(n - 1);}}
对于这个代码已经不是什么难题了。可能有的同学对于这种递归状态还不是特别理解,只是知道了一个内存的运作情况。那么我们就通过模型来理解。
我们以传入的参数是4来举例:
我们在模拟递归调用的过程当中,和二叉树长的简直一模一样。那么对于二叉树,我们能不能用递归来做一些文章呢?
public void Traverse(Tree tree){if (tree == null){return;}//System.out.println(tree.data);前序遍历Traverse(tree.left);//System.out.println(tree.data);中序遍历Traverse(tree.right);//System.out.println(tree.data);后序遍历
}
那么。根据输出的位置不同,根据输出的数据顺序不同。就分成了不同的遍历方式
03.02前序遍历
先访问根结点,然后左子树,然后右子树。 中左右
03.03中序遍历
从根结点出发,先进入根结点的左子树中序遍历,然后访问根结点,最后访问右子树。左中右
03.04后序遍历
从左到右先叶子后结点的方式进入左右树遍历,最后访问根结点。 左右中。
需要注意的是,无论是什么样的遍历顺序,访问结点都是从根结点开始访问,按照从上到下,从左到右的顺序向下挖掘,分为 3 种顺序主要因为我们需要有一些方式来描述递归遍历的结果,让我们可以抽象二叉树的结构,因此我们就按照输出语句放的位置不同而决定是什么序遍历,所以我这边就将 3种遍历顺序放在一起谈。
03.05层序遍历
层序遍历法不仅直观,而且好理解,但是我们要思考,处于同一层的结点存在于不同子树,按照刚才的递归遍历法我们无法和其他子树产生沟通,那该怎么实现?仔细观察,层序遍历就好像从根结点开始,一层一层向下扩散搜索,这就跟我们队列实现迷宫算法非常类似,因为迷宫算法的不同路径也是无关联的,但是我们是用广度优先搜索的思想可以找到最短路径。
/*** ⼆叉树的遍历* 层次遍历*/public void levelOrder(Node root){Queue<Node> queue = new LinkedList<>();Node current = null;if (root != null){//offer只会返回false 不会报异常//跟结点⼊队queue.offer(root);}//队列不为空 执⾏循环while(!queue.isEmpty()){//poll 返回nullcurrent = queue.poll();System.out.print(current.data + " ");//如果有左节点,就把左节点加⼊if (current.leftChild != null){queue.offer(current.leftChild);}//如果有左节点,就把左节点加⼊if (current.rightChild != null){queue.offer(current.rightChild);}}}
03.06代码非递归和递归演示
递归:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct BTNode{char data;struct BTNode* left;struct BTNode* right;
}BTNode,*BTree;
int flag;
typedef struct qnode{char data;struct qnode* next;
}qnode,*lqueue;
lqueue front,rear;
void initqueue(){qnode* q=(qnode*)malloc(sizeof(qnode));if(q==NULL){printf("队列分配失败\n");return ;}front=rear=q;front->next=NULL;
}
void enqueue(char x){qnode* s=(qnode*)malloc(sizeof(qnode));s->data=x;s->next=NULL;rear->next=s;rear=s;
}
int empty(){if(front->next==NULL)return 1;//空return 0;
}
char dequeue(){if(!empty()){qnode* q=front->next;front->next=q->next;char x=q->data;if(front->next==NULL)rear=front;free(q);q=NULL;return x;}else{printf("队空\n");}
}
BTree initTree(char x){BTNode* r=(BTNode*)malloc(sizeof(BTNode));if(r==NULL){printf("分配失败\n");return NULL;}else{r->data=x;r->left=r->right=NULL;return r;}
}
BTNode* find(BTree ro,char fx)
{//递归if(ro==NULL||ro->data==fx){return ro;} if(ro->left!=NULL){BTNode* f=find(ro->left ,fx); if(f!=NULL&&f->data==fx){return f;}}if(ro->right !=NULL){BTNode* f=find(ro->right ,fx); if(f!=NULL&&f->data==fx){return f;}}return NULL;
}
void insert(BTree ro,char x,char fx,int flag)
{BTNode* f=find(ro,fx); if(f!=NULL){BTNode* s=(BTNode*)malloc(sizeof(BTNode));s->data=x;s->left=s->right=NULL;if(flag==0){f->left=s;}else{f->right=s;}}
}
void levelOrderBTree(BTree ro)
{char x;BTNode* q=NULL;initqueue();//初始化一个队列 if(ro==NULL){printf("空树\n");return ;} enqueue(ro->data);//根节点数据入队while(!empty()) {x=dequeue(); printf("%c ",x);q=find(ro,x);if(q->left!=NULL){enqueue(q->left->data);}if(q->right!=NULL){enqueue(q->right->data);} }
}
void perOrder(BTree ro)
{if(ro==NULL){return;}printf("%c ",ro->data);if(ro->left!=NULL){perOrder(ro->left);}if(ro->right !=NULL){perOrder(ro->right);}
}
void inOrder(BTree ro)
{if(ro==NULL){return;}if(ro->left!=NULL){inOrder(ro->left);}printf("%c ",ro->data);if(ro->right !=NULL){inOrder(ro->right);}
}
void postOrder(BTree ro)
{if(ro==NULL){return;}if(ro->left!=NULL){postOrder(ro->left);}if(ro->right !=NULL){postOrder(ro->right);}printf("%c ",ro->data);
}
int main()
{int n;char x,fx;BTree ro=NULL;scanf("%d",&n);getchar();scanf("%c",&x);ro=initTree(x);for(int i=2;i<=n;i++){ getchar();scanf("%c %c %d",&x,&fx,&flag);insert(ro,x,fx,flag);}levelOrderBTree(ro);//层次遍历 printf("\n");perOrder(ro);//先序遍历 printf("\n");inOrder(ro);//中序遍历 printf("\n");postOrder(ro);//后序遍历return 0;}
/*
9
A
B A 0
E A 1
C B 1
D C 0
F E 1
G F 0
H G 0
K G 1
*/
非递归:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct BTNode{char data;struct BTNode* left;struct BTNode* right;
}BTNode,*BTree;
int flag;//=0 左孩子, =1 右孩子
//---------------------链栈----------------------
//单链表的节点结构:将整个节点入队
typedef struct stackNode{struct BTNode* data;struct stackNode* next;
}sstack;
sstack* s=NULL;
//初始化链栈
void initstack()
{s=(sstack*)malloc(sizeof(sstack));if(s==NULL){printf("栈空间分配失败\n");return ; }s->next=NULL;
}
//入栈
void ppush(BTNode* k)
{//头插sstack* p=(sstack*)malloc(sizeof(sstack));p->data=k;p->next=s->next;s->next=p;}
//判空
int empty()
{if(s->next==NULL){return 1;}return 0;}
//出栈:返回栈顶元素,将其在栈中删除
BTNode* ppop()
{if(empty()==1){printf("栈空\n");return NULL;}sstack* p=s->next;BTNode* k=s->next->data;s->next=p->next;free(p);p=NULL;return k;
}
//-----------------------------------------------------------------
//建树 BTree initTree(char x)
{BTNode* r=(BTNode*)malloc(sizeof(BTNode));if(r==NULL){printf("分配失败\n"); return NULL;}else{r->data=x;r->left = r->right=NULL;return r; }}
BTNode* find(BTree ro,char fx)
{//递归if(ro==NULL||ro->data==fx){return ro;} if(ro->left!=NULL){BTNode* f=find(ro->left ,fx); if(f!=NULL&&f->data==fx){return f;}}if(ro->right !=NULL){BTNode* f=find(ro->right ,fx); if(f!=NULL&&f->data==fx){return f;}}return NULL;}
void insert(BTree ro,char x,char fx,int flag)
{BTNode* f=find(ro,fx); if(f!=NULL){BTNode* s=(BTNode*)malloc(sizeof(BTNode));s->data=x;s->left=s->right=NULL;if(flag==0){f->left=s;}else{f->right=s;}}
}
//--------------------------
//遍历
void perOrder(BTree ro)
{initstack();//初始化一个栈 if(ro==NULL){return;} ppush(ro);while(!empty()){BTNode* node=ppop();printf("%c ",node->data);//访问if(node->right!=NULL){ppush(node->right);} if(node->left !=NULL){ppush(node->left );} }}
void inOrder(BTree ro)
{initstack();//初始化一个栈 if(ro==NULL){return;} BTNode* node=ro;BTNode* k=NULL;while(!empty()||node!=NULL){if(node!=NULL){ppush(node);node=node->left;}else{k=ppop();//取出栈顶K printf("%c ",k->data);node=k->right;} }}
void postOrder(BTree ro)
{initstack();//初始化一个栈 if(ro==NULL){return;} BTNode* node=ro;BTNode* k=NULL;BTNode* pre=NULL;while(!empty()||node!=NULL){if(node!=NULL){ppush(node);node=node->left;}else{k=s->next->data;//栈顶元素 if(k->right!=NULL&&pre!=k->right){node=k->right;} else{k=ppop();printf("%c ",k->data);pre=k;node=NULL;//返回上一个父亲结点 }}}}
int main()
{int n;char x,fx;BTree ro=NULL;scanf("%d",&n);getchar();scanf("%c",&x);ro=initTree(x);for(int i=2;i<=n;i++){ getchar();scanf("%c %c %d",&x,&fx,&flag);insert(ro,x,fx,flag);}//perOrder(ro); //先序遍历--非递归 //inOrder(ro);//中序遍历--非递归 postOrder(ro);//后序遍历--非递归 printf("\n");return 0;}
/*
9
A
B A 0
E A 1
C B 1
D C 0
F E 1
G F 0
H G 0
K G 1
*/
04.扩展二叉树
例如要确定一个二叉树,我们肯定不能只是把结点说明白,还需要把每个结点是否有左右孩子说明白。例如如图所示树结构,我们可以向其中填充结点,使其的所有结点填充完后均具有左右结点,为了表示该结点其实是不存在的,我们需要设置一个标志来表示,例如是“#”,那么这种描述就是拓展二叉树如图所示。
05.二叉排序树(二叉查找树、二叉搜索树)
05.01性质
1、如果他的左子树不空,则左子树上所有结点的值均小于它的根结点的值。
2、若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
3、它的左、右树又分为二叉排序树。
很显然,二叉排序树的定义是一个递归形式的定义,所以对于二叉排序树的操作都是基于递归的形式。二叉排序树既然名字中带有排序二字,这就是它相对于普通二叉树的优势所在了。此时可能还不是很清楚。没关系,我们一步步建立一颗二叉树。
05.02构建二叉排序树
假设,初始状态下,我们有如下的无序序列:
第一步:插入8作为根结点
第二步:插入3,与8做比较,发现比8小,且根结点没有左孩子,则将3插入到8的左孩子。
第三步:插入10,首先与根结点比较,发现比8大,则要将10插入到根结点的右子树;根结点8的右子树为空,则将10作为8的右孩子。
第四步:插入1,首先和根结点做比较,比根结点小,则应该插入到根结点的左子树。在跟根结点的左孩子3比较,发现比3还小,则应该插入3的左孩子。
第五步:插入6,先与根结点8比较,小于8,向左走。在于3比较,大于3,向右走,没有结点,则将6作为3的右孩子。
第六步:插入14,先与8比较,比 8 大,向右走;再与8的右孩子10比较,比10大,向右走,没有结点,则将14作为10的右孩子。
第七步:插入4,先与8比较,发现比8小,向左走,再与3比较,比3大向右走,再与6比较,向左走且没有左孩子,将4作为6的左孩子。
第八步:插入7,先与8比较,发现比8小,向左走,再与3比较,向右走,在与6比较,继续向右走,发现6没有右孩子,则将7作为 6的右孩子插入。
第九步:插入13,先与8比较(大于)向右,再与10比较(大于)向右,再与14比较(小于)向左,发现14的左孩子为空,则将13插入到14的左孩子位置。
二叉排序树的思路出来了,这个时候又有一个问题。我们理解的二叉排序树的构造,可是它的优势在哪里呢?别慌,这个时候我们来看一看,这颗二叉树的中序遍历。对比无序序列,二叉树的中序遍历的结果就是。
05.03 二叉排序树的查找操作
当数组有序了,我们查找元素,直接上查找算法,是不是就极其方便了?但是,如果仅仅是为了让他成为一个有序数列,还要存到数组里。还要再去查找他,那这也太弱了。实际上,我们是直接从二叉树上进行查找的。怎么做呢?二叉排序树的查找操作与二分查找非常相似,我们一起试着查找值为13的结点。首先,访问根结点8。
第二步:根据二叉排序树的左子树均比根结点小,右子树均本根结点大的性质。如果13 > 8,因此值为13的结点可能在根结点8的右子树当中,我们查看根结点的右子结点10;
第三步:与第二步相似,13>10,所以查看结点10的右孩子14.
第四步:根据二叉排序树的左子树均比根结点小,右子树均比根结点大的性质,13 < 14,因此查看 14 的左孩子13,发现刚好和要查找的值相等。
这次,我们就应该明白排序树的作用了。然后,我们就可以看看二叉排序树的查找实现代码。
/**
* 查找结点是否存在
* @param root 从根结点开始遍历查找
* @param key 待查找的结点
* @return
*/
public Boolean search(Node root,int key){while (root != null){if (key == root.data){return true;}else if(key < root.data){root = root.left;}else{root = root.right;}}return false;
}
05.04 二叉排序树的插入操作
对于任意一个待插入的元素 x 都是插入在二叉排序树的叶子结点,问题的关键就是确定插入的位置,原理当然和上面的查找操作一样,从根结点开始进行判断,直到到达叶子结点,则将待插入的元素作为一个叶子结点插入即可,多说无益,直接开始操作。如果我们要在原有的树中插入9,该怎么做呢?
第一步,访问根结点8。
第二步:根据二叉排序树的左子树均比根结点小,右子树均比根结点大的性质, 9 > 8 ,因此值为9的结点应该插入到根结点 8 的右子树当中,我们查看根结点的右子节点10。
第三步:根据二叉排序树的左子树均比根结点小,右子树均比根结点大的性质, 9 < 10 ,因此值为9的结点应该插入到结点 10 的左子树当中,访问结点10的左孩子,发现为空,则将 9 作为 10号结点的左孩子插入。
我们会发现,这和查找的操作也差不多啊。确实差不多,因为插入本身就要先找到插入的位置,再进行插入
public void insert(Node root,int key){//记录⼆叉树的前⼀个结点Node prev = null;while (root != null){prev = root;if (key < root.data){root = root.left;}else if(key > root.data){root = root.right;}else{return;}}if (root == null){root = new Node(key);}else if(key < prev.data){prev.left = new Node(key);} else{prev.right = new Node(key);}
}
05.05二叉树的删除操作
删除操作与查找和插入的操作就不一样啦。我们要开始分情况处理
被删除的结点是叶子结点:
直接从二叉排序树当中移除即可,也不会影响树的结构。
被删除的结点D仅有一个孩子:
如果只有左孩子,没有右孩子,那么只需要把要删除结点的左孩子连接到要删除结点的父亲节点,然后删除D结点就好了;如果只有右孩子,没有左孩子,那么只要将要删除结点D的右孩子重接到要删除结点D的父亲结点。
假设我们要删除值为 14 的结点,其只有一个左孩子结点 13 ,没有右孩子 。
第一步:保存要删除结点 14 的 左孩子结点 13 到临时变量 temp,并删除结点 14;
第二步:将删除结点 14 的父结点 10 的右孩子设置为 temp,即结点 13。
我们再以删除结点 10 为例,再看一下没有左孩子,只有一个右孩子的情况。
第一步:保存要删除结点 10 的右孩子结点 14 到临时变量 temp,并删除结点 10;
第二步:将删除结点 10 的父结点 8 的右孩子设置为 temp,即结点 14。
被删除的结点的左右孩子都在:
这一次我们将图变得复杂一点,给原来结点 10 增加了一个左孩子结点 9 。
对于上面的二叉排序树的中序遍历结果如下所示
现在我们先不考虑二叉排序上的删除操作,而仅在得到的中序遍历结果上进行删除操作。我们以删除中序遍历结果当中的顶点8为例进行说明。
当删除中序遍历结果中的 8 之后,哪一种方式不会改变中序遍历结果的有序性呢?
那我们就要用7或者9来填充,都不会影响数据的有序性,如果对应到我们的二叉排序树上面呢?相当于我们先删除根结点8,然后根结点的左子树当中最大的元素7来替换根结点8的位置,或者用根结点的右子树当中最小的0来替换根结点的位置。那么我们对于删除一颗而叉排序树上一个包含左右子树的结点就是这么做的。
我们下面就来看删除左右孩子都存在的结点是如何实现的,依旧以删除根结点 8 为例。首先我们
一起看用根结点的左子树当中值最大的结点 7 来替换根结点的情况。
第一步:获得待删除结点 8 的左子树当中值最大的结点 7 ,并保存在临时指针变量 temp 当中(这一步可以通过从删除结点的左孩子 3 开始,一个劲地访问右子结点,直到叶子结点为止获得)
第二步:将删除结点 8 的值替换为 7
第三步:删除根结点左子树当中值最大的结点(这一步可能左子树中值最大的结点存在左子结点,而没有右子结点的情况,那么删除就退化成了第二种情况,递归调用即可):
我们再来一起看一下使用删除结点的右子树当中值最小的结点替换删除结点的情况
第一步:查找删除结点 8 的右子树当中值最小的结点,即 9 (先访问删除结点的右子结点 10,然后一直向左走,直到左子结点为空,则得到右子树当中值最小的结点)。
第二步:将删除结点 8 的值替换为 9
第三步:删除根结点右子树当中值最小的结点。
以上就是删除二叉排序树的三种具体情况的分析。那么接下来,看代码
public boolean deleteNode(Node root,int key){if (root == null){return false;}else{if (key == root.data){return delete(root);}else if(key < root.data){return deleteNode(root.left,key);}else{return deleteNode(root.right,key);}}
}
/**
*
*/
public boolean delete(Node node){Node temp = null;/*** 如果右⼦树空,只需要重新连接他的结点* 如果是叶⼦节点,在这⾥也把叶⼦节点删除*/if (node.right == null){temp = node;node = node.left;}else if (node.left == null){temp = node;node = node.right;} else{//左右⼦树都不为空temp = node;Node s = node;/**找到左⼦树的最⼤值*/s = s.left;while(s.right != null){temp = s;s = s.right;}node.data = s.data;if(temp != node){temp.right = s.left;}else{temp.left = s.left;}}return true;
}
05.06 时间复杂度分析
二叉排序树的插入和查找、删除操作的最坏时间复杂度为 O(h),其中 h 是二叉排序树的高度。最极端的情况下,我们可能必须从根结点访问到最深的叶子结点,斜树的高度可能变成n,插入和删除操作的时间复杂度将可能变为O(n) 。下图就是两颗斜树(就相当于单链表)。这也是二叉排序树在进行多次插入操作后可能发生的不平衡问题,也是二叉排序树的缺陷所在,但这依旧不妨碍其作为一个伟大的数据结构。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct BSTNode{int data;struct BSTNode* left;struct BSTNode* right;
}BSTNode,*BSTree;
BSTree initBST(int k){BSTNode* r=(BSTNode*)malloc(sizeof(BSTNode));if(r==NULL)return NULL;r->data=k;r->left=r->right=NULL;return r;
}
BSTree insert_BST(BSTree ro,int x){BSTNode* s=(BSTNode*)malloc(sizeof(BSTNode));s->data=x;s->left=s->right=NULL;while(p!=NULL){if(x<p->data){pre=p;p=p->left;}else{pre=p;p=p->right;}}if(x<pre->data)pre->left=s;elsepre->right=s;return ro;
}
//递归版插入
//往 以ro为根节点的树中插入数据x
// if(x<ro->data) 以ro->left为根节点的子树中插入数据x
//if(x>ro->data) 以ro->right为根节点的子树中插入数据x
//递归出口: if(ro==NULL)创建新结点s,return s; BSTree insert_BST1(BSTree ro,int x)
{if(ro==NULL){BSTNode* s=(BSTNode*)malloc(sizeof(BSTNode));s->data=x;s->left=s->right=NULL;return s;}if(x<ro->data){ro->left=insert_BST1(ro->left,x); return ro;}else{ro->right=insert_BST1(ro->right ,x); return ro;}} //找以ro为根的树中最靠右的节点p
BSTNode* find_p(BSTree ro)
{BSTNode* p=ro;while(p->right!=NULL){p=p->right;}return p;}
//BST删除操作:在以root为根的树中删除数据k
BSTree BST_de(BSTree ro,int k)
{if(k<ro->data ){//问题转化成 在以root->left为根的子树中删除数据kro->left=BST_de(ro->left,k);// return ro;}else if(k>ro->data ){//问题转化成 在以root->right为根的子树中删除数据kro->right =BST_de(ro->right ,k);// return ro;}else{//k==ro->data ro这个节点就是要删除的节点 if(ro->left!=NULL&&ro->right!=NULL){//找到root的左子树中最右的节点pBSTNode* p=find_p(ro->left);ro->data=p->data;ro->left=BST_de(ro->left,p->data);// return ro;} else{//只有一个孩子ch(NULL), BSTNode* ch=NULL; if(ro->left!=NULL){ch=ro->left;}else{ch=ro->right;}free(ro);ro=NULL;return ch;}}return ro;
}
void inOrder(BSTree ro)
{if(ro==NULL){return;}if(ro->left!=NULL){inOrder(ro->left);}printf("%d ",ro->data);if(ro->right !=NULL){inOrder(ro->right);}
}
void Search(BSTree ro,int x)
{BSTNode* p=ro;while(p!=NULL&&p->data !=x){if(x<p->data){p=p->left;}else{p=p->right;}}if(p==NULL){printf("NO\n"); }else{printf("YES\n"); }}
int main()
{int a[105],n;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&a[i]); }BSTree ro=initBST(a[1]);if(ro==NULL){printf("建树失败\n");return 0;}for(int i=2;i<=n;i++){ro=insert_BST1(ro,a[i]);} inOrder(ro);printf("\n");ro=BST_de(ro,10);inOrder(ro);printf("\n");return 0;}/*
9
8 3 10 1 6 14 4 7 13
*/
三.二叉平衡树(AVL树)
AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
1.为什么要有平衡二叉树
二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图示的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。
2.定义
平衡二叉查找树:简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
1、可以是空树。
2、假如不是空树,任何一个节点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。
平衡之意,如天平,即两边的分量大约相同。
如下图
该树就不是平衡二叉树,因为结点60的左右子树高度差超过了1
该图也不是平衡二叉树,因为虽然任何一个结点的左子树和右子树都是平衡二叉树,但是高度之差已经超过1
该图就是一颗平衡二叉树。
3.平衡因子
01.定义
某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子,平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左树比较高,右子树比较高。
02.结点结构
/**
* @author Turing
* @version 1.0.0
* @Description TODO 平衡⼆叉树的结点结构
**/
public class AVLNode {int depth;//深度 计算每个结点的深度 通过深度⽐较是否平衡AVLNode tree;//该节点的⽗节点int data;//该节点的值 可以泛型AVLNode lchild;//左指针AVLNode rchild;//右指针
}
4.AVL树插入时的失衡与调整
在此平衡二叉树中,如果插入一个结点99,树的结构变成:
在上述图中,结点66的左子树高度为1,右子树高度为3,此时平衡因此 -2,树因此失衡。那么以66为父结点的那棵树就叫做最小失衡子树。最小失衡子树:在新插入的节点向上查找,以第一个平衡因子的绝对值超过 1 的节点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋与右旋 。旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边树高,就把那边的树向上旋转。
01.左旋
以上图为例,加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为-2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:
1、结点的右孩子结点替代此结点的位置
2、右孩子的左子树变成该结点的右子树
3、结点本身变成右孩子的左子树
节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 77 ,将节点 77 代替节点 66 的位置右孩子的左子树变为该节点的右子树 —— 节点 77 的左子树为节点 72,将节点 72 挪到节点 66 的右子树位置节点本身变为右孩子的左子树 —— 节点 66 变为了节点 77 的左子树
02.右旋
右旋操作与左旋类似,操作流程为:
1. 节点的左孩子代表此节点
2. 节点的左孩子的右子树变为节点的左子树
3. 将此节点作为左孩子节点的右子树。
5. AVL树的四种插入节点的方式
假设一颗AVL树的某个节点为A,有四种操作会使得A的左右子树高度差大于1,从而破坏AVL树的平衡。平衡二叉树的插入有以下四种情况。
插入方式 | 描述 | 旋转方式 |
LL | 在A结点的左子树根结点的左子树上插入结点而破坏平衡 | 右旋转 |
RR | 在A结点的右子树根结点的右子树上插入结点而破坏平衡 | 左旋转 |
LR | 在A的左子树根结点的右子树上插入结点而破坏平衡 | 先左旋再右旋 |
RL | 在A的右子树根结点的左子树上插入结点而破坏平衡 | 先右旋再左旋 |
1. 在所有的不平衡情况中,都是按照先寻找最小不平衡树,然后寻找所属的不平衡类别,再根据 4
种类别进行固定化程序的操作。
2. LL , LR ,RR ,RL其实已经为我们提供了最后哪个节点作为新的根指明了方向。如 LR 型最后的根节点为原来的根的左孩子的右孩子,RL 型最后的根节点为原来的根的右孩子的左孩子。只要记住这四种情况,可以很快地推导出所有的情况。
6. A的左孩子的左子树插入结点(LL)
只需要执行一次右旋
7.A结点的右孩子的右子树插入结点(RR)
只需要执行一次左旋
8.A结点的左孩子的右子树插入结点(LR)
若 66 的左孩子节点 60的右子树 62插入节点 61,导致节点 66 失衡,如图
此时,66的平衡因子是2。若仍然按照右旋。则变化后就会变成
经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使得树重新平衡。那么这种插入方式需要执行两步操作,使得旋转之后为原来根结点的左孩子的右孩子作为新的根结点。
1、对失衡结点66的左孩子60进行左旋操作,即上述RR情形操作。
2、对失衡结点66做右旋操作,即上述LL情形。
也就是说,经过这两步操作,使得原来根节点的左孩子的右孩子 66节点成为了新的根节点。
9.A结点的右孩子的左子树插入结点(RL)
右孩子插入左结点的过程与左孩子插入有结点的操作类似。也是需要两步操作
右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根节点的右孩子的左孩子作为新的根节点。
(1)对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。
(2)对失衡节点 A 做左旋操作,即上述 RR 情形操作。
也就是说,经过这两步操作,使得原来根节点的右孩子的左孩子75节点成为了新的根节点。
10.AVL树的四种删除结点方式
AVL树的和二叉查找树的删除操作情况一致,都分成了四种情况:
1. 删除叶子结点
2. 删除的结点只有左子树
3. 删除的结点只有右子树
4. 删除的节点既有左子树又有右子树
只不过对于AVL树来说,你要删除一个结点后需要重新检查平衡性并修正。同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对第一个非平衡节点进行修正,而删除操作需要修正所有的非平衡结点。
删除操作的大致步骤如下:
以前三种情况为基础尝试删除节点,并将访问节点入栈。
如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。
再依次检查栈顶节点的平衡状态和修正直到栈空。
对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了
01.删除叶子节点
处理步骤:
1、将该节点直接从树中删除;
2、其父节点的子树高度的变化将导致父节点平衡因子的变化,通过向上检索并推算其父节点是否失衡;
3、如果其父节点未失衡,则继续向上检索推算其父节点的父节点是否失衡…如此反复②的判断,直到根节点 ;如果向上推算过程中发现了失衡的现象,则进行 ④ 的处理;
4、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ② 的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理
02.如果删除的结点只有左子树或者右子树
处理步骤:
1、将左子树(右子树)替代原有节点 C 的位置;
2、节点 C 被删除后,则以 C 的父节点 B 为起始推算点,依此向上检索推算各节点(父、祖先)是否失衡;
3、如果其父节点未失衡,则继续向上检索推算其父节点的父节点是否失衡…如此反复 ② 的判断,直到根节点 ;如果向上推算过程中发现了失衡的现象,则进行 ④ 的处理;
4、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ② 的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理;
03. 删除的结点既有左子树又有右子树
处理步骤:
1、找到被删节点 B 和替代节点 BLR (节点 B 的前继节点或后继节点 —— 在此选择 前继);
2、将替代节点 BLR 的值赋给节点 B ,再把替代节点 BLR 的左孩子 BLRL 替换替代节点 BLR 的位置;
3、以 BLR 的父节点 BL 为起始推算点,依此向上检索推算父节点或祖先节点是否失衡;
4、如果其父节点未失衡,则继续向上检索推算其父节点的父节点是否失衡…如此反复③的判断,直到根节点;如果向上推算过程中发现了失衡的现象,则进行⑤的处理;
5、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ②的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理;
11.代码演示
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
/*AVL树代码*/
//AVL树的结点结构
typedef struct AVLNode{int data;//数据域struct AVLNode* left; struct AVLNode* right;int h;//该节点的高度
}AVLNode,*AVLtree; int max(int a,int b)
{return a>b?a:b;}
//获得ro这个节点的高度
int geth(AVLtree ro)
{if(ro==NULL){return 0;}else{return ro->h;}} AVLtree initAVLtree(int k)
{AVLNode* root=(AVLNode*)malloc(sizeof(AVLNode));//root->data=k;root->left=root->right=NULL;root->h=1;return root;
}
//四种失衡的调整函数
//LL-->以失衡节点x 为中心,右旋
AVLtree LL_rotation(AVLtree x)
{AVLNode* y=x->left;x->left=y->right;y->right=x;x->h=max(geth(x->left),geth(x->right))+1;y->h=max(geth(y->left),geth(y->right))+1;return y;
}
//RR-->以失衡节点x 为中心,左旋
AVLtree RR_rotation(AVLtree x)
{AVLNode* y=x->right;x->right=y->left;y->left=x;x->h=max(geth(x->left),geth(x->right))+1;y->h=max(geth(y->left),geth(y->right))+1;return y;
}
//LR--->以失衡节点x->left为中心,左旋.然后以失衡节点x 为中心,右旋
AVLtree LR_rotation(AVLtree x)
{x->left=RR_rotation(x->left);x= LL_rotation(x);return x;
}
//RL:以x->right为中心进行右旋,再x为中心左旋
AVLtree RL_rotation(AVLtree x)
{x->right=LL_rotation(x->right);x= RR_rotation(x);return x;
}AVLtree insert_AVL(AVLtree root,int k)
{if(root==NULL){AVLNode* s=(AVLNode*)malloc(sizeof(AVLNode));s->data=k;s->left=s->right=NULL;s->h=1;return s;}else if(k<root->data){root->left=insert_AVL(root->left,k);//判断root是否失衡:LL LR if(geth(root->left)-geth(root->right)>1) {AVLNode* l=root->left;if(geth(l->left)>geth(l->right))//if(k < l->data ){//LLroot=LL_rotation(root);}else{//LRroot=LR_rotation(root);}} } else if(k>root->data){root->right =insert_AVL(root->right ,k);//判断root是否失衡:RR RLif(geth(root->right )-geth(root->left )>1) {AVLNode* r=root->right ;if(geth(r->right )>geth(r->left))//if(k > r->data ){//RRroot=RR_rotation(root);}else{//Rlroot=RL_rotation(root);}} }// root->h=max(root->left->h,root->right->h)+1;//bug root->h=max(geth(root->left),geth(root->right))+1;//改正 return root;} //-----------------------------------------------------------------
AVLNode* find(AVLNode* l)
{while(l->right!=NULL){l=l->right;}return l;
}
//在以root为根的树中删除k
AVLtree AVL_de(AVLtree root,int k)
{if(root==NULL){return NULL;}if(k<root->data){root->left=AVL_de(root->left,k);//判断root失衡 if( geth(root->right) - geth(root->left) >1){//RR RLAVLNode* r=root->right;if( geth(r->right) >= geth(r->left) ){//RR root=RR_rotation(root);}else{//RLroot=RL_rotation(root);}} }else if(k>root->data){root->right =AVL_de(root->right ,k);//判断root失衡 LL LRif( geth(root->left) - geth(root->right) >1 ){//LL LRAVLNode* l=root->left;if( geth(l->left) >= geth(l->right) ){root=LL_rotation(root);}else{root=LR_rotation(root);}} }else{if(root->left!=NULL&&root->right!=NULL){AVLNode* maxNode=find(root->left);root->data=maxNode->data;root->left=AVL_de(root->left,maxNode->data);//判断root失衡 if( geth(root->right) - geth(root->left) >1){//RR RLAVLNode* r=root->right;if( geth(r->right) >= geth(r->left) ){//RR root=RR_rotation(root);}else{//RLroot=RL_rotation(root);}} }else{AVLNode* x=root;if(root->left!=NULL){root=root->left; }else{root=root->right; }free(x);x=NULL;return root; }}root->h=max(geth(root->left),geth(root->right))+1;return root;
} //---------------------------------------------------------
void inorder(AVLtree root)
{if(root!=NULL){inorder(root->left );printf("%d %d\n",root->data,root->h);inorder(root->right);}
}
int main()
{int n;int a[105];scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&a[i]);}AVLtree root=initAVLtree(a[1]);for(int i=2;i<=n;i++){root=insert_AVL(root,a[i]);}inorder(root);int k;scanf("%d",&k);root=AVL_de(root,k);inorder(root);return 0;
}
/*
8
1 2 3 4 5 6 7 8
*/
四.线索二叉树
在n个结点的二叉链表中,必定有n+1个空链域。而遍历运算是最重要的,也是最常用的运算方法,之前的无论是递归与非递归的算法实现遍历效率其实都不算高。现有一棵结点数目为n的二叉树,采用二叉链表的形式存储。对于每个结点均有指向左右孩子的两个指针域,而结点为n的二叉树一共有n-1条有效分支路径。那么,则二叉链表中存在2n-(n-1)=n+1个空指针域。那么,这些空指针造成了空间浪费。
此外,当对二叉树进行中序遍历时可以得到二叉树的中序序列。如图所示二叉树的中序遍历结果为DBEAC,可以得知A的前驱结点为E,后继结点为C。但是,这种关系的获得是建立在完成遍历后得到的,那么可不可以在建立二叉树时就记录下前驱后继的关系呢,那么在后续寻找前驱结点和后继结点时将大大提升效率。
1.线索化
现将某结点的空指针域指向该结点的前驱后继,定义规则如下:
若结点的左子树为空,则该结点的左孩子指针指向其前驱结点。
若结点的右子树为空,则该结点的右孩子指针指向其后继结点。
这种指向前驱和后继的指针称为线索。将一棵普通二叉树以某种次序遍历,并添加线索的过程称为线索化。
2. 线索化带来的问题
可以将一棵二叉树线索化为一棵线索二叉树,那么新的问题产生了。我们如何区分一个结点的lchild指针是指向左孩子还是前驱结点呢?
为了解决这一问题,现需要添加标志位ltag,rtag。并定义规则如下:
ltag为0时,指向左孩子,为1时指向前驱
rtag为0时,指向右孩子,为1时指向后继
上述所示的二叉树转变为二叉线索树就是
// 中序线索化
private void inOrderThreadTree(ThreadTreeNode node) {// 如果当前节点为 null ,直接返回if (node == null) {return;}// 处理左⼦树inOrderThreadTree(node.leftNode);// 处理前驱节点// 如果节点左⼦树为 nullif (node.leftNode == null) {// 设置前驱节点node.leftType = 1;node.leftNode = pre;}// 处理前驱的右指针,如果节点右⼦树为 nullif (pre != null && pre.rightNode == null) {// 设置后继节点pre.rightType = 1;// 让前驱节点的右指针指向当前节点pre.rightNode = node;}// 每处理⼀个节点,当前节点是下个节点的前驱节点pre = node;// 处理右⼦树inOrderThreadTree(node.rightNode);
}
遍历
/**
* 中序遍历线索⼆叉树
*/
public void inOrderTraverse() {// 取出根节点ThreadTreeNode node = root;if (root == null) {return;}// 找出最左边结点(最左边结点 leftType 已经变成 1),然后根据线索化,⼀直向右遍历while (node != null && node.leftType == 0) {node = node.leftNode;}System.out.println("值为:" + node.value);// 根据线索化,⼀直向右遍历while (node != null) {node=node.rightNode;if(node!=null){System.out.println("值为:" + node.value);}
}
3.带头结点的线索化
4. 优点和应用
递归遍历需要使用系统栈,非递归遍历需要使用内存中的空间来帮助遍历,而线索化之后就不需要这些辅助了,直接可以像遍历数组一样遍历。线索二叉树核心目的在于加快查找结点的前驱和后继的速度。如果不使用线索的话,当查找一个结点的前驱与后继需要从根节点开始遍历,当然,如果二叉树数据量较小时,可能线索化之后作用不大,但是当数据量很大时,线索化所带来的性能提升就会比较明显。当路由器使用CIDR,选择下一跳的时候,或者转发分组的时候,通常会用最长前缀匹配(最佳匹配)来得到路由表的一行数据,为了更加有效的查找最长前缀匹配,使用了一种层次的数据结构中,通常使用的数据结构为二叉线索。
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
typedef struct BTNode{char data;struct BTNode* left;struct BTNode* right;int lfalg,rflag;//标记左右指针指向的是孩子节点(0),还是前驱后继 (1) //父亲指针
}BTNode,*BTree;
int flag;//=0 左孩子, =1 右孩子
BTNode* pre=NULL;
//----------------------------
//树的相关代码:
BTree initTree(char x)
{BTNode* r=(BTNode*)malloc(sizeof(BTNode));if(r==NULL){printf("分配失败\n"); return NULL;}else{r->data=x;r->left = r->right=NULL;r->lfalg=r->rflag=0;return r; }}
BTNode* find(BTree ro,char fx)
{//递归if(ro==NULL||ro->data==fx){return ro;} if(ro->left!=NULL&&ro->lfalg==0){BTNode* f=find(ro->left ,fx); if(f!=NULL&&f->data==fx){return f;}}if(ro->right !=NULL&&ro->rflag==0){BTNode* f=find(ro->right ,fx); if(f!=NULL&&f->data==fx){return f;}}return NULL;}
void insert(BTree ro,char x,char fx,int flag)
{BTNode* f=find(ro,fx); if(f!=NULL){BTNode* s=(BTNode*)malloc(sizeof(BTNode));s->data=x;s->left=s->right=NULL;s->lfalg=s->rflag=0;if(flag==0){f->left=s;}else{f->right=s;}}
}
//访问函数:加线索
void visit(BTNode* ro)
{if(ro->left==NULL){ro->left=pre;ro->lfalg=1;//打标记 }if(pre!=NULL&&pre->right==NULL){pre->right=ro;pre->rflag=1; //打标记}pre=ro;//更新前驱 } void perOrder(BTree ro)
{if(ro==NULL){return;}//printf("%c ",ro->data);visit(ro);if(ro->left!=NULL&&ro->lfalg==0){perOrder(ro->left);}if(ro->right !=NULL&&ro->rflag==0){perOrder(ro->right);}
}
//中序遍历函数--->加线索
void inOrder(BTree ro)
{if(ro==NULL){return;}if(ro->left!=NULL&&ro->lfalg==0) {inOrder(ro->left);}//printf("%c ",ro->data);//访问 visit(ro); //线索化 if(ro->right !=NULL&&ro->rflag==0){inOrder(ro->right);}
}
void postOrder(BTree ro)
{if(ro==NULL){return;}if(ro->left!=NULL&&ro->lfalg==0){postOrder(ro->left);}if(ro->right !=NULL&&ro->rflag==0){postOrder(ro->right);}//printf("%c ",ro->data);visit(ro);
}
//找中序前驱
BTNode* find_pre(BTree ro,char k)
{BTNode* x=find(ro,k);if(x!=NULL){if(x->lfalg==1){return x->left;}else{if(x->left==NULL){return NULL;}BTNode* p=x->left;while(p->right!=NULL&&p->rflag==0){p=p->right;}return p;} }}
BTNode* find_post(BTree ro,char k)
{BTNode* x=find(ro,k);if(x!=NULL){if(x->rflag ==1){return x->right;}else{if(x->right==NULL){return NULL;}BTNode* p=x->right;while(p->left !=NULL&&p->lfalg ==0){p=p->left ;}return p;} }}
int main()
{int n;char x,fx;BTree ro=NULL;scanf("%d",&n);getchar();scanf("%c",&x);ro=initTree(x);for(int i=2;i<=n;i++){ getchar();scanf("%c %c %d",&x,&fx,&flag);insert(ro,x,fx,flag);}inOrder(ro);//中序线索化 getchar();scanf("%c",&x);BTNode* ans=find_pre(ro,x);if(ans==NULL){printf("不存在\n");} else{printf("%c ",ans->data);}return 0;
}
/*
9
A
B A 0
E A 1
C B 1
D C 0
F E 1
G F 0
H G 0
K G 1
*/
五.最小生成树
1.生成树的定义
一个连通图的生成树是一个极小的连通子图,它包含图中全部的n个顶点,但只有构成一棵树的n-1条边。
可以看到一个包含三个顶点的完全图可以产生3颗生成树。对于包含n个顶点的无向完全图最多包含n的n-2次方颗生成树。
2.生成树的属性
一个连通图可以有多个生成树;
一个连通图的所有生成树都包含相同的顶点个数和边数;
生成树当中不存在环;
移除生成树中的任意一条边都会导致图的不连通,生成树的边最少特性;
在生成树中添加一条边会构成环。
对于包含n个顶点的连通图,生成树包含n个顶点和n-1条边;
对于包含n个顶点的无向完全图最多包含 n^n-2颗生成树。
3.最小生成树
所谓一个带权图的最小生成树,就是原图中边的权值最小的生成树,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。
首先你明白最小生成树是和带权图联系在一起的;如果仅仅只是非带权的图,只存在生成树。其他的,我们看例子解决就好了。
上图中,原来的带权图可以生成左侧的两个最小生成树,这两颗最小生成树的权值之和最小,且包含原图中的所有顶点。看图就是清晰,一下子理解了,但我们又如何从原图得到最小生成树呢?这个确实是个问题,还是个好问题。最小生成树算法有很多,其中最经典的就是克鲁斯卡尔(Kruskal)算法和 普里姆(Prim)算法,也是我们考试、面试当中经常遇到的两个算法。
4.克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔算法(Kruskal)是一种使用贪婪方法的最小生成树算法。该算法初始将图视为森林,图中的每一个顶点视为一棵单独的树。一棵树只与它的邻接顶点中权值最小且不违反最小生成树属性(不构成环)的树之间建立连边。
直接看例子:
第一步:将图中所有的边按照权值进行非降序排列;
第二步:从图中所有的边中选择可以构成最小生成树的边。
1、选择权值最小的边 V4-V7 ,没有环形成,则添加:
2、选择边 V2-V8,没有形成环,则添加:
3、选择边V0 - V1没有形成环,则添加:
4、选择边V0 - V5,没有形成环,添加
5、选择边V1 - V8,没有形成环,添加
6、选择V3 - V7,没有形成环,添加
7、选择V1 - V6,没有形成环,添加
8、选择边V5 - V6添加这条边将导致形成环,舍弃,不添加;
9、选择边V1 - V2添加这条边将导致形成环,舍弃,不添加;
10、选择边V6 - V7,没有成环,添加
此时已经包含了图中顶点个数9减1条边,算法停止。
原来这样也可以,但我有一个问题,我们该如何判断添加一条边后是否形成环呢?
要判断添加⼀条边 X-Y 是否形成环,我们可以判断顶点X在最小树中的终点与顶点Y在最小生成树中的终点是否相同,如果相同则说明存在环路,否则不存环路,从而决定是否添加一条边。
所谓终点,就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最
大顶点"。看下图,我们可以对图中顶点进行排序,排序后的顶点存放在一个数组中,每一个顶点则对应一个下标,同样的我们针对排序后的数组创建一个顶点的终点数组,初始时图中的每一个顶点是一棵树,每一个顶点的终点初始化为自身,我们用0来表示。
回到之前的算法执行过程,我们配合这个终点数组再来一次。
1、选择权值最小的边V4 - V7没有环形成( V4的终点为4, V7的终点为7),则添加,并更新终点数组:此时将4的终点更新为7;
2、选择权值最小的边V2 - V8没有环形成( V2的终点为2, V8的终点为8),则添加,并更新终点数组:此时将2的终点更新为8;
3、选择权值最小的边V0 - V1没有环形成( V0的终点为0, V1的终点为1),则添加,并更新终
点数组:此时将0的终点更新为1;
4、选择权值最小的边V0 - V5没有环形成( V0的终点为1, V1的终点为5),则添加,并更新终
点数组:此时将1的终点更新为5;
5、选择权值最小的边V1 - V8没有环形成( V1的终点为5, V8的终点为8),则添加,并更新终
点数组:此时将5的终点更新为8;
6、选择权值最小的边V3 - V7 没有环形成( V3的终点为3, V7的终点为7),则添加,并更新终
点数组:此时将3的终点更新为7;
7、选择权值最小的边V1 - V6 没有环形成( V1的终点为8, V6的终点为6),则添加,并更新
终点数组:此时将8的终点更新为6;
8、选择权值最小的边V5 - V6 没有环形成( V5的终点为6, V6的终点为6,两个顶点的终点相同,说明添加后会形成环),舍弃,不添加
9、选择权值最小的边V1 - V2 没有环形成( V1的终点为6, V2的终点为6,两个顶点的终点相同,说明添加后会形成环),舍弃,不添加
10、选择权值最小的边V6 - V7没有环形成( V6的终点为6, V7的终点为7),则添加,并更新终点数组:此时将6的终点更新为7;
此时已经包含了图中顶点个数9减1条边,算法停止。
#include <stdio.h>
#include <stdlib.h>
#define INF 65535/*给定一个带权无向图,该图含有n个顶点,e(>=n-1)条边,保证该图连通
求解该图的最小生成树:选出的n-1条边,保证该图仍然连通,并且权值和最小 Kruskal算法:基于贪心/贪婪算法----时间复杂度:排序的时间复杂度O(e^2)+并查集的时间复杂度O(e)
首先对边按照权值升序排序 ---排序算法:任何一种排序算法都行。课上带写用 选择排序 1.选权值 尽可能小 的n-1条边。
选n-1次,每次都选一条权值最小的边即可 2.每次选出一条之后,判断选出的这条边能不能用。
选出的一条边需要连接一个新的点(没有在生成树中的点),
如果这条边没有连新的点,那么这条边连的就是生成树中的两个点,
连接同一棵树中的两个点,就形成环了---看有没有形成环?
判断该边所连接的两个点是不是在同一棵树中
对点建立并查集。 */ //邻接矩阵存图--带权无向图
int g[105][105];//假设最多有100个点
int nv,ne;//边数组
typedef struct {int u,v;//两个端点在邻接矩阵中的下标 int w;//权值
}Edge;
Edge e[5005];
void sorte(int l,int r)
{int minn;Edge tmp;for(int i=l;i<r;i++){minn=i;for(int j=i+1;j<r;j++){if(e[minn].w>e[j].w){minn=j;}}tmp=e[i];e[i]=e[minn];e[minn]=tmp;}printf("按权值升序排列:\n");for(int i=0;i<r;i++){printf("(%d %d) %d\n",e[i].u,e[i].v,e[i].w);} }
int find(int* f,int x)
{if(x==f[x]){return x;}else{return f[x]=find(f,f[x]);}
}
void Kruskal()
{int f[105];//并查集的父亲数组for(int i=0;i<nv;i++){f[i]=i;} printf("以下是组成最小生成树的边:\n");for(int i=0;i<ne;i++){int fu=find(f,e[i].u);int fv=find(f,e[i].v);if(fu!=fv){f[fv]=fu;//合并printf("(%d %d) %d\n",e[i].u,e[i].v,e[i].w); } } }
int main()
{scanf("%d %d",&nv,&ne);//邻接矩阵初始化 for(int i=0;i<nv;i++)for(int j=0;j<nv;j++){if(i==j){g[i][j]=g[j][i]=0;}else{g[i][j]=g[j][i]=INF;}} //s输入边,建图int x,y,wi;for(int i=0;i<ne;i++){scanf("%d %d %d",&x,&y,&wi);g[x][y]=g[y][x]=wi;e[i].u=x;e[i].v=y;e[i].w=wi; } sorte(0,ne); Kruskal();return 0;}/*
9 15
0 1 3
0 5 4
1 6 6
6 5 7
1 2 8
1 8 5
2 8 2
2 3 12
8 3 11
6 3 14
6 7 9
5 4 18
3 7 6
7 4 1
3 4 10
*/
5.贪婪算法
01.贪心算法思想
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心算法的基本要素:
1、贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2、当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
02.贪心算法的基本思路
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1、不能保证求得的最后解是最佳的;
2、不能用来求最大或最小解问题;
3、只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解。
6.普里姆(Prim)算法
普里姆算法在找最小生成树时,将顶点分为两类,一类是在查找的过程中已经包含在生成树中的顶点(假设为 A 类),剩下的为另一类(假设为 B 类)。对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A 类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。所走过的顶点和边就是该连通图的最小生成树。
假如从顶点 V0 出发,顶点V1 、V5的权值分别为3、4,所以对于顶点V0来说,到顶点V1的权值最小,将顶点V1加入到生成树中:
继续分析与顶点V0和V1相邻的所有顶点(包括V2 、V5、V6、V8 ),其中权值最小的为V5, 将V5添加到生成树当中:
继续分析与顶点V0和V1、V5相邻的所有顶点中权值最小的顶点,发现为V8,则添加到生成树当中。
继续分析与生成树中已添加顶点相邻的顶点中权值最小的顶点,发现为V2,则添加到生成树中:
重复上面的过程,直到生成树中包含图中的所有顶点,我们直接看接下来的添加过程:
#include <stdio.h>
#include <stdlib.h>
#define INF 65535
/*最小生成树:选n个点,以及尽量小的n-1条边组成的树
Prim算法:时间复杂度O(|v|^2)---稠密图 ---选点,点的生成树的距离
Kruskal算法 O(ElogE)---稀疏图----选边,判环--并查集
一。分析:
去选点:在选点的过程中,把所有的点分成两类:
1.已经选到生成树中的点
2.还未选到生成树中的点 一个定义:点X到生成树的最小距离dist:就是x到生成树中某点的边的权值(距离), 如果x与生成树中的多个点相连,选权值最小的1.第一个点选谁:任意选一个点
2.进行循环,每次选一个点进入生成树中,循环n-1次 ,每次选距离生成树最近(dist[i]最小)的点。
将某点选入到生成树中之后,用该点更新其他的未选入的点到生成树的距离。
点X到生成树的距离dist:就是x到生成树中某点的边的权值(距离), 如果x与生成树中的多个点相连,选权值最小的
二。代码思路:
维护一个dist[],点到生成树的距离,初始化无穷
1.选起点加到生成树中
2。用起点去更新 与起点直接相连,并且还未加入到生成树中的点的dist
3.n-1次循环:(1)选dist值最小的点(p点)加入生成树,用p点更新 与p点直接相连,并且还未加入到生成树中的点的dist
*/
//邻接矩阵存图--带权无向图
int g[105][105];//假设最多有100个点
int nv,ne;
int dist[105];
int flag[105];
int minn(int x,int y)
{if(x>y)return y;else return x;
}
void prim()
{//初始化dist数组:for(int i=0;i<nv;i++){dist[i]=INF; } dist[0]=0;flag[0]=1;for(int i=1;i<nv;i++){if(flag[i]==0){dist[i]=minn(dist[i],g[i][0]);}}int temp,t; for(int i=1;i<nv;i++)//执行n-1次for循环,选n-1个点 {temp=INF;//最小距离 t=-1;//点的下标 for(int j=0;j<nv;j++){if(flag[j]==0&&dist[j]<temp){temp=dist[j];t=j;}}if(t==-1){break;}flag[t]=1;printf("点:%d,通过权值为 %d 边加入到最小生成树中\n",t,temp);for(int j=0;j<nv;j++){if(flag[j]==0){dist[j]=minn(dist[j],g[j][t]);}}} }
int main()
{scanf("%d %d",&nv,&ne);//邻接矩阵初始化 for(int i=0;i<nv;i++)for(int j=0;j<nv;j++){if(i==j){g[i][j]=g[j][i]=0;}else{g[i][j]=g[j][i]=INF;}} //s输入边,建图int x,y,wi;for(int i=0;i<ne;i++){scanf("%d %d %d",&x,&y,&wi);g[x][y]=g[y][x]=wi;} prim();return 0;}/*
9 15
0 1 3
0 5 4
1 6 6
6 5 7
1 2 8
1 8 5
2 8 2
2 3 12
8 3 11
6 3 14
6 7 9
5 4 18
3 7 6
7 4 1
3 4 10
*/
7.最小生成树总结
最小生成树的问题,简单得理解就是给定一个带有权值的连通图(连通网),从众多的生成树中筛选出权值总和最小的生成树,即为该图的最小生成树。
最经典的两个最小生成树算法:Kruskal 算法与 Prim 算法。两者分别从不同的角度构造最小生成树,Kruskal 算法从边的角度出发,使用贪心的方式选择出图中的最小生成树,而 Prim 算法从顶点的角度出发,逐步找各个顶点上最小权值的边来构建最小生成树的。
最小生成树问题应用广泛,最直接的应用就是网线架设(网络G表示n各城市之间的通信线路网线路(其中顶点表示城市,边表示两个城市之间的通信线路,边上的权值表示线路的长度或造价
六.哈夫曼树、哈夫曼编码
1.一些概念
概念一:什么是结点的路径长度
结点k的路径长度就是从根结点A到结点k的路径上的连接数,也就是我们看到红色连边,也就是3。
概念二:什么是树的路径长度
就是树的每个叶子结点的路径长度之和那么图中树的路径长度之和就是13.
概念三:什么是结点的带权路径长度
结点的路径长度与结点权值的乘积。
比如规定结点K的权重/权值为4,结点K的路径长度为3,那么带权路径长度就是3 * 4 = 12。
概念四:什么是树的带权路径长度 WPL
就是树的所有叶子结点的带权路径长度之和。
2.应用场景
数据压缩的意义不言而喻。谈到数据压缩,就不能不提赫夫曼(Huffman)编码,赫夫曼编码是首个实用的压缩编码方案,即使在今天的许多知名压缩算法里,依然可以见到赫夫曼编码的影子。另外,在数据通信中,用二进制给每个字符进行编码时不得不面对的一个问题是如何使电文总长最短且不产生二义性。根据字符出现频率,利用赫夫曼编码可以构造出一种不等长的二进制,使编码后
的电文长度最短,且保证不产生二义性。当有了以上概念以后,我们就可以用WPL去判断一颗二叉树的性能。WPL的值越小,二叉树的性能越优。
3.哈夫曼树的构造过程
首先,出给一个初始森林:
第一步,森林中选择出两颗结点的权值最小的二叉树
第二步:合并两颗二叉树,增加一个新的结点作为新的二叉树的根,权值为左右孩子的权值之和
第三步:不断的将新的结点跟权值最小的结点相结合
所以说,其实哈夫曼树其实很简单。我们说,哈夫曼编码可以有效的压缩数据(压缩率依赖于数据的特性在20%~~90%不等)。我们要讲哈夫曼编码,继续来看几个名词解释。
4.哈夫曼编码
01.定长编码
像ASCII编码、Unicode编码。ASCII编码每⼀个字符使用8个bit,能够编码256个字符;Unicode编码每个字符占16个bit,能够编码65536个字符,包含所有ASCII编码的字符。假设我们要使用定长编码对由符号A,B,C,D和E构造的消息进行编码,对每个字符编码需要多少位呢?
至少需要3位,2个bit位不够表示五个字符,只能表示4个字符。
如果对DEAACAAAAABA进行编码呢?总共12个字符,每个字符需要3bit,总共需要36位
01.01定长编码的缺陷
浪费空间!对于仅包含ASCII字符的纯文本消息,Unicode使用的空间是ASCII的两倍,两种方式都会造成空间浪费;字符“ a”和“ e”的出现频率比“ q”和“ z”的出现频率高,但是他们都占用了相同位数的空间。要解决定长编码的缺陷,便有了下面的变长编码
02.变长编码
单个编码的长度不一样,可以根据整体出现的频率来调节,出现的频率越高,编码长度越短。变长编码优于定长编码的是,变长编码可以将短编码赋予平均出现频率较高的字符,同一消息的编码长度小于定长编码。这个时候又有一个问题,字符有长有短,我们怎么知道一个字符从哪里开始,又从哪里结束呢?如果位数固定,就没这个问题了。
02.01前缀属性
字符集当中的一个字符编码不是其他字符编码的前缀,则这个字符编码具有前缀属性。所谓前缀,一个编码可以被解码为多个字符,表示不唯一。比如,abcd需要编码表示,设a = 0,b = 10,c =110,d = 11。那么110可以表示c,也可以表示da。不理解没关系,来看个例子。
字符 | 编码 |
P | 000 |
Q | 11 |
R | 01 |
S | 001 |
T | 10 |
000不是11、01、001、10的前缀,000具有前缀属性。11不是000,01,001,10的前缀。比如01001101100010 可以翻译为 RSTQPT。又比如
字符 | 编码 |
P | 0 |
Q | 1 |
R | 01 |
S | 10 |
T | 11 |
编码1110可以解码为QQQP、QTP,QQS和TS,对于任意一个编码,解码的结果不一样。
02.02前缀码
所谓的前缀码,就是没有任何码字是其他码字的前缀。
5.哈夫曼树的特征
01.哈夫曼编码树是一颗二叉树
每片叶子结点都包含一个字符
从结点到其左孩子的路径上标记0
从结点到其右孩子的路径上标记1
02.从根结点到包含字符的叶子结点的路径上获得的叶结点的编码
03.编码均具有前缀属性
每一个叶结点不能出现在到另一个叶结点的路径上
注意:定长编码可以由完整的哈夫曼树表示,并且显然具有前缀属性
有了哈夫曼编码树的特性,我们也知道了哈夫曼树的构造过程,最后我们再来看一个哈夫曼编码和哈夫曼树构造的完整过程
6.代码演示
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
//数组模拟树
typedef struct {int w;//权值int f;//父亲节点的下标int l,r;//左右孩子节点的下标 }HuffmanNode,*HuffmanTree;
void find(HuffmanTree t,int x,int* w1,int* w2)
{//先找最小int minn=0;for(int i=1;i<=x;i++){if(t[i].f==i){minn=i;break;}} for(int i=1;i<=x;i++){if(t[i].f ==i){if(t[i].w<t[minn].w){minn=i;}}}*w1=minn;//最小的根节点的下标保存至W1中//找第二小 for(int i=1;i<=x;i++){if(t[i].f==i&&i!=(*w1)){minn=i;break;}} for(int i=1;i<=x;i++){if(t[i].f==i&&i!=(*w1)){if(t[i].w<t[minn].w){minn=i;}}}*w2=minn;}
HuffmanTree createHuffmanTree(int *wi,int n)
{int m=2*n;//HuffmanTree t=(HuffmanTree)malloc(sizeof(HuffmanNode)*m);for(int i=1;i<m;i++){t[i].f=t[i].l =t[i].r=0;t[i].w=0;}for(int i=1;i<=n;i++){t[i].w=wi[i-1];t[i].f=i;}int w1,w2;//权值最小的两个根节点的下标 for(int i=n+1;i<m;i++){find(t,i-1,&w1,&w2);t[w1].f=t[w2].f=i;t[i].f=i;t[i].w=t[w1].w+t[w2].w;t[i].l=w1;t[i].r=w2;}return t;
}
char** ctreateHuffmanCode(HuffmanTree t,int n)
{char *temp=(char*)malloc(sizeof(char)*n);//0~~n-1char **code=(char**)malloc(sizeof(char*)*n);int start;//一开始放编码的位置 int pos,p;for(int i=1;i<=n;i++){start=n-1;temp[start]='\0';pos=i;p=t[pos].f;//p是pos的父亲 while(t[pos].f!=pos){start--;if(t[p].l==pos){temp[start]='0';}else{temp[start]='1';}pos=p;p=t[pos].f; }code[i-1]=(char*)malloc(sizeof(char)*(n-start));strcpy(code[i-1],&temp[start]);}free(temp);temp=NULL;return code;
}
int main()
{ char s[8] = {'A', 'B', 'C', 'D','E', 'F', 'G', 'H'};int w[8] = {5, 29, 7, 8,14, 23, 3, 11};HuffmanTree tree = createHuffmanTree(w,8);char **code= ctreateHuffmanCode(tree,8); for(int i=0;i<8;i++){printf("%c : %s\n",s[i],code[i]);}return 0;
}
七.树、二叉树和森林的转换
1. 普通树转为二叉树步骤
1、加线,在所有兄弟结点之间加一条连线
2、去线,对树中每个结点,只保留它与第一孩子结点的连线,删除它与其他孩子结点之间的连线
3、层次调整,以树为根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
2.森林转换为二叉树
1、把每棵树都转换为二叉树
2、第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连起来。
3、层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
3.二叉树转为树、森林
二叉树转换为普通树是刚才的逆过程,步骤也就是反过来做而已。
1. 若结点x是其双亲结点y的左孩子,则把x的右孩子,右孩子的右孩子,.....,都与结点y用线连起来。
2. 去掉所有双亲到右孩子之间的连线
3. 调整位置
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有的话就是森林,没有的话就是一棵树
八.并查集
并查集是一种非常精巧而实用的树形数据结构,他主要是处理一些不相交集合的合并和查询的问题。不相交集合,顾名思义,就是两个集合的交集为空集的一些集合。比如1,3,5和2,4,6,他俩的交集为空集。就是不相交集合。像2,3,5和1,3,5就不是不相交集合。使用并查集时,首先会存在一组不相交的动态集合 S={S1, S2,⋯,Sk},一般都会使用一个整数表示集合中的一个元素。每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表。每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为代表一般也是不关心的。我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级
1.基本操作
所以说对于并查集,主要就是有如下的操作:
1、建立一个新的并查集,其中包含s个单元素集合
2、合并集合,把元素x和元素y所在的集合合并,要求x和y所在的集合不相交,如果相交就不合并。
3、找到元素x所在的集合的代表,这个操作也可以用于判断两个元素是否位于同一集合,只要将他们各自的代表比较一下就可以了。
2.树的表现形式
并查集的实现原理也比较简单,就是用树来表示一个集合,树的每个结点都是集合的一个元素,树根对应的那个结点就是该集合的代表
比如上图中的两棵树,就分别对应两个集合,其中第一个集合为a,b,c,d,代表元素是a,第二个集合是e,f,g,代表结合是e。
树的节点表示集合中的元素,指针表示指向父节点的指针,根节点的指针指向自己,表示其没有父节点。沿着每个节点的父节点不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。
现在,假设使用一个足够长的数组来存储树节点,那么 makeSet 要做的就是构造出如下图的森林,其中每个元素都是一个单元素集合,即父节点是其自身:
3.查找
查找操作就是找到i的祖先直接返回
4.合并
比如我们合并(4,3),(3,2),(2,1)。
如果此时我们合并了一万个结点,这个时候,其实从时间复杂度来说,查找的次数就非常非常多了。这个时候,我们就想出来了一个策略,路径压缩。
5.路径压缩
就是在每次查找时,令查找路径上的每个节点都直接指向根节点。
6.洛谷p1551
最后我们用洛谷的题目来熟悉并查集的操作过程
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
//洛谷P1551
int n,m,p;
int f[5005];
int find(int x)
{while(f[x]!=x){x=f[x];}return x;
}
//递归
/*int find(int x)
{if(f[x]==x){return x;} else{//return find(f[x]);//没有路径压缩优化 return f[x]=find(f[x]);//路径压缩优化 }
}
*/
int main()
{int x,y,px,py;scanf("%d %d %d",&n,&m,&p);//初始化f【i】=i; for(int i=1;i<=n;i++){f[i]=i;}for(int i=1;i<=m;i++){scanf("%d %d",&x,&y);px=find(x);py=find(y);f[px]=py;//f[py]=px;}for(int i=1;i<=p;i++){scanf("%d %d",&x,&y);px=find(x);py=find(y);if(px==py){printf("Yes\n");}else{printf("No\n");}}return 0;
}
/**/
总结
树的内容写了我整整三天哎,希望能够给大家带来帮助,谢谢!