哈工大数据结构实验三——图形结构及其应用
哈工大课程实验合集
1.实验要求
步骤
2.实验步骤
2.1 设计BST
先看看BST树是啥。
BST也叫二叉查找树,它本质上也是一棵二叉树。只不过对于二叉树的每个节点来说,节点存储的值大于所有的左子树的节点存储的值,小于所有的右子树的节点存储的值。
有了这个性质,我们利用BST进行查找就很简单,只需要将要查找的值和当前节点存储的值进行比较,等于就查找到,大于这个节点存储的值就往右子树走,否则就往左走,一直走到空节点为止。(可以看到,和折半查找非常类似,但是却比折半查找性能优越的多,本次实验就是需要比较BST查找和折半查找的性能)
2.1.1 设计BST的左右链存储结构
这个很简单,如果你看过我之前写的二叉树的建立、遍历和应用那篇博客的话,相信你对二叉树的存储结构手到拈来。
二叉树的建立、遍历和应用
对于每个节点来说,只需要存储指向左子树和右子树的指针以及该节点存储的值即可。
#include<iostream>
#include<string.h>
#include<fstream>
#include<queue>
#include <cstdlib>
#include<ctime>
using namespace std;
typedef int records;
typedef struct celltype{//BST二叉树的双链结构
records data;//关键字
struct celltype *lchild,*rchild;//左右链
}Node;
typedef records *List;
#define MAX 1024
typedef Node *BST;
2.1.2 BST的建立(插入)
BST插入思路很简单,就是根据BST的性质来进行插入。也就是要保证:对于任意一个节点来说,右子树的所有节点存储的值>大于该节点存储值>该节点左子树所有节点存储的值
我们采用递归的思路进行BST的建立。给定我们要插入的值k,需要插入以节点G为根节点的BST树。
- 如果节点G为空节点,说明以G为根节点的BST树不存在,则直接新建一个节点G,节点G存储的值为k,其左儿子和右儿子都为null,将节点G作为根节点即可。(这也是递归终止条件)
- 如果这个值等于节点G所存储的值,说明BST树已经存储了值k,则直接退出。
- 如果这个值大于节点G存储的值,则递归的插入到以节点G的右儿子为根节点的BST树中。
- 如果这个值小于节点G存储的值,则递归的插入到以节点G的左儿子为根节点的BST树中。
void Insert(BST &G,records k){//BST插入算法
if(G==NULL){//此时应该插入
G=new Node;
G->data =k;
G->lchild =NULL;
G->rchild =NULL;
}
else if(G->data==k){//关键字存在
cout<<"该关键字已经存在,请插入其他的关键字"<<endl;
}
else if(G->data <k){
Insert(G->rchild ,k);
}
else{
Insert(G->lchild ,k);
}
}
2.1.3 BST的查找
查找思路很简单,就像之前说的一样,
-
只需要将要查找的值和当前节点存储的值进行比较,等于就查找到,返回这个节点即可,
-
大于这个节点存储的值就往右子树走
-
否则就往左走,一直走到空节点为止。如果走到了空节点,说明这个值没找到,返回null即可。
为了后续和折半查找的性能进行比较,我们需要记录BST查找某个值k所比较的次数。
Node *Search(BST G,records k){//BST查找
BST p=G;
if(p==NULL){
return p;
}
count++; //比较次数的计数
if(p->data ==k)
return p;
else if(k<p->data)
return (Search(p->lchild,k));
else
return (Search(p->rchild ,k));
}
2.1.4 BST的删除
需要解决以下问题:
- 当删除某个节点之后,这个节点应该由哪个节点来顶替(左子树最大值或者右子树最小值)?
- 顶替之后怎样调整二叉树来保持BST特有的性质?
删除操作比较麻烦且不易理解。跟着思路来,准能懂。
根据删除节点的不同,删除节点执行的操作也不同。主要有三种情况:
-
被删除的节点是叶节点,这种情况我们知道,因为是叶节点,所以没有左子树和右子树,因此也不存在顶替该叶节点的问题,也不存在调整二叉树的问题。因此直接delete即可。
-
当被删除的节点只有一颗左子树或者一颗右子树时,怎么处理呢?
首先需要找出节点来顶替该被删除的节点。考虑两种情况,先以被删除的节点只有左子树为例,我们可以把左子树最右的节点(也就是存储值最大的节点)顶替删除的节点,然后把剩下左子树剩下的节点作为顶替的节点的左子树。但是太麻烦了,我们可以直接把被删除的节点的左儿子顶替掉被删除的节点。一样可以保持BST树的性质。
同理,对于被删除的节点只有右子树来说,我们只要把被删除的右儿子顶替掉被删除的节点即可。
2.4.1.1例子1
如果要删除的节点只有左子树,直接把被删除的节点用该节点的左儿子顶替即可。以删除节点18为例,删除后的子树如下:
调整后的BST树如下:
同理,如果被删除的节点只有右子树,我们只要把被删除的右儿子顶替掉被删除的节点即可
3.当被删除的节点既有左子树又有右子树,怎么处理呢?
这种情况我们就只能用被删除的节点的左子树的最右边的节点去顶替被删除的节点,或者用被删除的节点的右子树的最左边的节点去顶替被删除的节点。然后再调整一下二叉树即可。具体步骤如下:
假设待删除的节点为p,我们用被删除的节点的右子树的最左节点去顶替被删除的节点。
- ①查找结点 p 的右子树上的最左下结点 s(找到被顶替的节点) 及其双亲结点 par
- ②将结点 s 数据域替换到被删结点 p 的数据域(执行顶替操作);
- ③若结点 p 的右孩子无左子树, 则将 s 的右子树接到 par 的右子树上; 否则,将 s 的右子树接到结点 par 的左子树上(调整二叉树);
- ④删除结点 s(因为把节点p的值替换为节点s的值,节点p并没有被删除,而是要删除节点s) ,在DeleteMin函数实现
第③步骤的两种情况如下图所示
2.4.1.2例子2
若结点 p 的右孩子无左子树,则将 s 的右子树接到 par 的右子树上
将 s 的右子树接到 par 的右子树上,调整后的BST树为
若结点 p 的右孩子有左子树,则将 s 的右子树接到结点 par 的左子树上
将 s 的右子树接到结点 par 的左子树上,调整后的BST树为
2.4.1.3 代码
代码如下:
void Delete(BST &G,records k){//删除BST树的关键字为k的节点
if(G!=NULL)
if(G->data<k){
Delete(G->rchild,k);//递归的到右子树去删除
}
else if(G->data >k){
Delete(G->lchild,k);//递归的到左子树去删除
}
else{
if(G->lchild==NULL){
G=G->rchild ;//右链继承
}
else if(G->rchild==NULL){
G=G->lchild ;//左链继承
}
else{
G->data=DeleteMin(G->rchild ); //递归删除
}
}
}
records DeleteMin(BST &G){//删除BST树的最小值的节点并返回该最小值
records tmp;
BST p;
if(G->lchild=NULL){
p=G;
tmp=G->data ;
G=G->rchild ;
delete p;
return tmp;
}
else{
DeleteMin(G->lchild);
}
}
2.4.5 BST排序
BST排序思想:BST树一个重要思想是对于任何一个节点来说,左儿子存储的值 < 该节点存储的值 < 右儿子存储的值 ,如果我们按照从小到大排序,那必然是先访问左儿子、再访问该节点,再访问右儿子。
刚好和中序遍历一模一样
int anc=0;
void Inorder(BST T, List l){//将BST树中序输出到数组l中
if(T==NULL)
return ;
Inorder(T->lchild ,l);
l[anc++]=T->data ;
Inorder(T->rchild ,l);
}
2.2 实现折半查找
折半查找相信不用我多讲了,对于已经排好序(升序)的数组A[low…up],mid = (low + up)/2 .
每次将需要查找的数据k和数组最中间的数A[mid]进行比较,
- k = A[mid],则返回下标,退出。
- k < A[mid] ,up = mid - 1 ,则递归的去数组的左半段去查找
- k > A[mid] ,up = mid +1 ,则递归的去数组的右半段去查找
同时要记录查找的次数
int cc=0;//折半查找次数
records zheban(List L,records k,int up){//折半查找,显示折半查找过程
int low=0;
int mid;
while(low<=up){
cc++; //折半查找计数
cout<<"low:"<<L[low]<<" up:"<<L[up]<<endl;
mid=(low+up)/2;
if(L[mid]==k) return mid;
else if(L[mid]>k) up=mid-1;
else low =mid+1;
}
return -1;
}
records zheban1(List L,records k,int up){//折半查找。不显示折半查找过程
int low=0;
int mid;
while(low<=up){
cc++;
// cout<<"low:"<<L[low]<<" up:"<<L[up]<<endl;
mid=(low+up)/2;
if(L[mid]==k) return mid;
else if(L[mid]>k) up=mid-1;
else low =mid+1;
}
return -1;
}
2.3 实验比较
首先来唠一唠BST查找树和折半查找树的性能比较吧。直观上,BST查找和折半查找原理一样,BST查找把待查找的数据和根节点的数值进行比较,折半查找待查找的数据和最中间的数值进行比较。然后决定下一步往左还是往右走。直观上、二者差不多,没有差别。但是,仔细想一想,会发现:
**对于一个二叉树来说,根节点的左子树和右子树节点个数可以不一样,设置可以相差很大(最大差距时二叉树为一颗斜树),**当为一颗斜树时,时间开销就是O(n),就相当于线性查找了。而最好的情况也才是左右两边的子树平衡,也才O(log n)
**而折半查找左右两边的数据个数都是一样的。**每次都是一半一半查找,所以时间开销稳定在O(log n)
所以,折半查找的时间开销低于BST查找。
本小节咱们就来证明它吧。
2.3.1 生成数据
由上面分析可得,当BST树平衡和不平衡时,和折半查找的效率相差很大。所以,我们需要创建平衡和不平衡的BST树。
生成斜树:我们先看看我们生成BST树的过程,我们利用Insert函数每次插入一个节点,然后再插入一个节点这样来生成BST树。所以,斜树是怎么生成的呢?就是我们插入的数据是升/降序的!以升序为例,每次插入一个数据,都插在上一次插入的节点的右儿子上,这样一直下去就是一颗斜树。
因此,实验需要我们生成的第一组数据是1024个已经排序的数据,就是为了生成一颗斜树
而第二组数据,是将排好序的数据打乱,这样插入过程生成的二叉树就比较均匀,就比较平衡
打乱数据很简单,先获得已经排好序的数据之后,放入到一个数组a中,然后遍历数组每个下标i,每次生成一个随机数w,swap(A[a],A[i])。即可打乱数据。
将排好序的数据存入文件中。
代码如下:
void getdata(){//生成数据
fstream file1,file2;
file1.open("dizen.txt") ;//顺序排列的奇数
int a[MAX],temp,w;
for(int i=0;i<MAX;i++){
a[i]=2*i+1;
file1<<2*i+1<<endl;//把顺序排列的奇数输出到文件中
}
file1.close() ;
file2.open("suiji.txt");//随机排列的奇数
srand(int(time(0)));
for(int i=0;i<MAX;i++){
w=rand()%MAX;//通过交换实现随机数列的生成
temp=a[w];
a[w]=a[i];
a[i]=temp;
}
for(int i=0;i<MAX;i++){
file2<<a[i]<<endl; //把随机排列的奇数输出到文件中
}
file2.close() ;//关闭文件
}
2.3.2 根据两组数据生成BST
我们已经将数据存入到文件中,生成BST可以循环调用Insert函数,因此每次读取文件中的数据,然后调用Insert函数即可。
BST Create1(){//用顺序奇数创建BST树
getdata();
fstream in;
in.open("dizen.txt");//顺序数据
BST p=NULL;
records k;
for(int i=0;i<MAX;i++){
in>>k;
Insert(p,k);
}
in.close() ;
return p;
}
BST Create2(){//用随机奇数创建BST树
getdata();
fstream in;
in.open("suiji.txt");//随机数据
BST p=NULL;
records k;
for(int i=0;i<MAX;i++){
in>>k;
Insert(p,k);//通过插入创建树
}
in.close() ;
return p;
}
2.3.3 计算BST查找成功和查找失败的次数
思路很简单,我们之前的折半查找和BST查找都已经记录了查找次数,所以只要运行函数即可。用变量记录一下查找的次数。
void cishu(BST G){//计算查找次数
float sum1=0,sum2=0;
Node *p;
for(int i=0;i<MAX;i++){
count=0;
p=Search(G,2*i+1);
sum1+=count;// 查找总次数
}
cout<<"查找所有的奇数平均次数为:"<<float(sum1/MAX)<<endl;
for(int i=0;i<MAX;i++){
count=0;
p=Search(G,2*i+1);
sum1+=count;//
}
count=0;
for(int i=0;i<MAX;i++){
count=0;
p=Search(G,2*i);
sum2+=count;//
}
p=Search(G,2048);
sum2+=count;//查找总次数
cout<<"查找所有的偶数平均次数为:"<<float(sum2/(1+MAX))<<endl;
}
2.3.4 计算折半查找查找成功和查找失败的次数
思路同上
void testhalf(){//测试折半查找平均查找长度
int i,sum=0;
float aver;
records a[MAX];
for(int i=0;i<MAX;i++){
a[i]=2*i+1;
}
for(i=0;i<MAX;i++){
cc=0;
zheban1(a,2*i+1,MAX-1);
sum+=cc;
}
aver=(float)sum/MAX;//平均查找总次数
cout<<"折半查找成功平均长度为"<<aver<<endl;
sum=0;anc=0;
for(i=0;i<MAX;i++){
cc=0;
zheban1(a,2*i,MAX-1);
sum+=cc;
}
zheban1(a,2048,MAX-1);
sum+=cc;
aver=(float)sum/(MAX+1);
cout<<"折半查找失败平均长度为"<<aver<<endl;
}
2.3.5 以上实验能否说明:就平均性能而言,BST 的查找与折半查找差不多,为什么?
可以认为,BST查找和折半查找的时间复杂度都是O(logn),但是如果根据有规律的数字建树就会导致BST不能够满足平衡性深度会‘深深浅浅’,当使用特定的数据进行查询的时候就会可能导致性能下降,比如本次实验,用顺序序列建树的时候,就会形成一条链,也就是右斜树,导致每次查找后面的数据性能特别差,对于随机的数进行查找效果仍然很差。但是如果给的数字不是规律的,二是随机的,比如第二组测试数据,我们发现他的平均查找时间性能和折半查找时间性能相差很小,如果我们的数字更随机一点(规律性更差),那么我们有很强的理由认为折半查找和BST查找的时间性能差不多。