DS博客作业05--查找
0.PTA得分截图
查找题目集总得分,请截图,截图中必须有自己名字。题目至少完成总题数的2/3,否则本次作业最高分5分。没有全部做完扣1分。
1.本周学习总结(0-4分)
1.1 总结查找内容
在生活中我们经常要运用查找的内容。比如说你要在百度上搜索你想要的内容,你要在数据库中查找某个人的身份信息,比如你要进行查重等等等。对数据进行存储是一部分,对数据进行查找调用又是一部分重要的内容。查找表可分为两类:
- 静态查找:仅作查询和检索操作的查找表。
- 动态查找:有时在查询之后,还需要将“查询”结果进行操作,也就是边查找边操作;
1.1.1查找的性能指标ASL。
我们把关键字的平均比较次数,平均搜索长度称为ASL(Average Search Length)。指的是进行查找操作的时候,需要查找的平均次数。计算公式为:
其中,n表示记录的个数,ASL是要每个数据都进行考虑的,所以是累加和的过程。pi表示查找第i个记录的概率 ( 通常认为pi =1/n ),ci表示找到第i个记录所需的比较次数;考察ASL主要是考它成功时怎么计算和不成功时怎么计算。
1.1.2顺序查找、二分查找的成功ASL和不成功ASL。
(1)顺序查找
顺序查找是我们之前学过的查找方式。它的操作步骤如下:从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定的值进行比较,如果扫描到的关键字与所给定的值相等的话,说明查找成功;如果整个顺序表都被扫描完了,仍然没有匹配到给定的值,说明这个顺序表里面没有该值。
//代码如如下:
int SeqSearch(SeqList R,int n,KeyType k)
{ int i=0;
while (i<n && R[i].key!=k)
i++;
if (i>=n) //未找到返回0
return 0;
else
return i+1;//找到返回逻辑序号i+1
}
对顺序表的ASL的分析。
- ASL(success)=n*(n+1)/2/n=(n+1)/2;
- 如果查找失败就相当于遍历整个顺序表,所以ASL(unsuccess)=n;
(2)二分查找
顺序查找需要对数据一个一个进行比对,时间复杂度比较高。二分查找也称为折半查找,顾名思义,每次取整个数据的中间值与给定值进行比较,从而确定给定值所在的大致区间,一步步缩小范围,进行查找。要注意,给定的数据必须是有序数列才能进行二分查找。
//代码如下:
int BinSearch(SeqList R,int n,KeyType k)
{ int low=0,high=n-1,mid;
while (low<=high) //当前区间存在元素时循环
{ mid=(low+high)/2;
if (R[mid].key==k)//查找成功
return mid+1;
if (k<R[mid].key)
high=mid-1;
else
low=mid+1
}
return 0;
}
- 查找成功时
1.1.3二叉搜索树
接下来我们要学习二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树:
-
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
-
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。如果将二叉搜索树进行中序遍历的话,你会发现的得到的结果是一个有序序列。 -
二叉树的查找需要从根节点开始比较,如果大于根节点,寻找他的右子树;如果小于根节点则寻找它左子树。重复上诉的步骤,直到找到关键字或者找不到。
比如说,在下面这颗二叉搜索树中寻找61这个结点,对根节点进行比较,大于根节点,搜索右子树,不断向下搜索,直到找到了该结点。
关于二叉搜索树来说,成功与不成功的ASL都是根据具体情况来讲的。比如:
这棵树 -
ASL(success)=(1+22+33+42+52)/10=3.2;计算成功时的ASL,注意每一层有几个结点,就要搜索几次。分母是节点的总个数。
-
ASL(unsuccess)=(12+43+24+45)/11;
以下是关于二叉搜索树的代码层面的操作
//结构体定义:
typedef struct node
{ KeyType key; //关键字项
InfoType data; //其他数据域
struct node *lchild,*rchild; //左右孩子指针
} BSTNode,*BSTree;
//二叉搜索树建立:
BSTNode *CreatBST(KeyType A[],int n) //返回树根指针
{ BSTNode *bt=NULL; //初始时bt为空树
int i=0;
while (i<n)
{
InsertBST(bt,A[i]); //将A[i]插入二叉排序树T中
i++;
}
return bt; //返回建立的二叉排序树的根指针
}
//插入操作:
int InsertBST(BSTree &p,KeyType k)
{ if (p==NULL) //原树为空
{
p=new BSTNode;
p->key=k;p->lchild=p->rchild=NULL;
retrurn 1;
}
else if (k==p->key) //相同关键字的节点0
return 0;
else if (k<p->key)
return InsertBST(p->lchild,k); //插入到左子树
else
return InsertBST(p->rchild,k); //插入到右子树
}
//查找操作:
BSTNode *SearchBST(BSTNode *bt,KeyType k)
{ if (bt==NULL || bt->key==k)
return bt;
if (k<bt->key) return SearchBST(bt->lchild,k);
else return SearchBST(bt->rchild,k);
}
//删除操作:
int DeleteBST(BSTree &bt,KeyType k)
{ if (bt==NULL) return 0; //空树删除失败
else
{ if(k<bt->key) return DeleteBST(bt->lchild);
//递归在左子树中删除为k的节点
else if(k>bt->key) return DeleteBST(bt->rchild);
//递归在右子树中删除为k的节点
else
{ Delete(bt); //删除*bt节点
return 1;
}
}
}
1.1.4AVL树的定义及4种调整做法。
之前学习二叉搜索树的时候,我们知道数据插入的顺序不同的时候,得到的树结构也是不一样的。但是对于查找来说,不同的树查找到效率是不一样的。比如:
插入的顺序不同,得到的树结构不同,前者ASL=(1+22+32)/5=2.2,后者ASL=(1+2+3+4+5)/5=3,可以知道前者对于差照操作来说还是比较友好的。通过观察两图我们可以发现,第一张图的树的层次没有第二张图多,结点更是均匀地分布在树的每一层,所以平均查找长度较小。由此我们提出一个新的概念:平衡二叉树(AVL树)。顾名思义,要让二叉树尽可能地保持形态上的均衡。这里再要引进一个概念:平衡因子。平衡因子值该节点左右子树地高度差。在平衡二叉树中,每个节点地平衡因子的绝对值都是不大于1的,大于1的结点都要进行调整。
//定义AVL树的节点的类型如下:
typedef struct node //记录类型
{ KeyType key; //关键字项
int bf; //增加的平衡因子
InfoType data; //其他数据域
struct node *lchild,*rchild;//左右孩子指针
} BSTNode;
我们上面说过,如果平衡状态被打破的时候是要进行调整的。这里的调整主要分为4类调整。
(1)LL平衡调整
步骤:若在A的左子树的左子树上插入结点,使A失衡,平衡因子2,需要进行一次顺时针旋转。
- 1.A的左孩子B右上旋转作为A的根节点
- 2.A节点右下旋转称为B的右孩子
- 3.B原右子树称为A左子树
(2)RR平衡调整
步骤:若在A的左子树的右子树上插入结点,使A的平衡因子从1增加至2,(以插入的结点C为旋转轴),先C进行逆时针旋转,A再顺时针旋转。
- C向上旋转到A的位置,A作为C右孩子
- C原左孩子作为B的右孩子
- C原右孩子作为A的左孩子
(3)LR平衡调整
步骤:若在A的左子树的右子树上插入结点,使A的平衡因子从1增加至2,(以插入的结点C为旋转轴),先C进行逆时针旋转,A再顺时针旋转。
- C向上旋转到A的位置,A作为C右孩子
- C原左孩子作为B的右孩子
- C原右孩子作为A的左孩子
(4)RL平衡调整
步骤:若在A的右子树的左子树上插入结点,使A的平衡因子从-1增加至-2,(以插入的结点C为旋转轴),先进行顺时针旋转,再逆时针旋转。
- C向上旋转到A的位置,A作为C左孩子
- C原左孩子作为A的右孩子
- C原右孩子作为B的左孩子
根据上面学过的内容我们知道,将树变成一颗平衡二叉树之后,查找的平均查找长度就会降低很多,大概达到O(log2n)。
1.1.5 B-树和B+树
之前我们学的二叉树一个结点中只能存一个数据,但是这样就是对空间的一种浪费。B树的出现可以一个节点可放多个关键字,降低树的高度。可放外存,适合大数据量查找。如数据库中数据。其中B-树又称为多路平衡查找树。
B-树
- 定义:一棵m阶B-树或者是一棵空树,或者是满足下列要求的m叉树:
- 每个节点至多m个孩子节点(至多有m-1个关键字)
- 除根节点外,其他节点至少有m/2个孩子节点(即至少有m/2-1个关键字);
- 若根节点不是叶子节点,根节点至少两个孩子节点
实际上,由于每个节点要存储多个信息,结点的结构是长这个样子的:
其中n表示关键字的个数,n+1表示孩子指针。节点中的关键字是按大小顺序进行排序的。
- 非根节点最少有(m/2)取上界个孩子,最多有m个孩子。关键字个数,最大最小都是孩子个数减一即可。
- 根节点最少有两个孩子,最多也是要有m个孩子;
//B-树结点结构体定义
#define MAXM 10 //定义B-树的最大的阶数
typedef int KeyType; //KeyType为关键字类型
typedef struct node //B-树节点类型定义
{ int keynum; //节点当前拥有的关键字的个数
KeyType key[MAXM]; //[1..keynum]存放关键字,[0]不用
struct node *parent; //双亲节点指针
struct node *ptr[MAXM];//孩子节点指针数组[0..keynum]
} BTNode;
B-树的特点:
-
1.B-树是所有结点的平衡因子均等于0的多路查找树。所有
外部结点都在同一层上。 -
2.在计算B-树的高度时,需要计入最底层的外部结点
-
3.外部结点就是失败结点,指向它的指针为空,不含有任何信息,是虚设的。一棵B树中总有n个关键字,则外部结点个数为n+1。
接下来,我们要根据B-树的特点对其进行操作 -
B-树的插入:在查找不成功之后,需进行插入。关键字插入的位置必定在叶子结点层,有下列几种情况:
- 该结点的关键字个数n<m-1,不修改指针;
- 该结点的关键字个数 n=m-1,则需进行“结点分裂”
要记住:如果没有双亲结点,新建一个双亲结点,树的高度增加一层。如果有双亲结点,将ki插入到双亲结点中。
-
B-树的删除:和插入的考虑相反,
- 如果结点中关键字的个数>m/2-1,直接删除
- 如果结点中关键字的个数=m/2-1要从其左(或右)兄弟结点“借调”关键字
- 若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必须进行结点的“合并”。
删除的时候要注意区分,是在叶子节点层上删除关键字还是非叶子节点层上删除关键字。
-
非叶子节点删除:在非叶子结点上删除关键字ki
- 1.从pi子树节点借调最大或最小关键字key代替ki。
- 2.pi子树中删除key
- 3.若子树节点关键字个数< m/2-1,重复步骤1
- 4.若删除关键字为叶子结点层,按叶子结点删除操作法
-
叶子节点删除:
- (1)假如b结点的关键字个数大于Min,说明删去该关键字后该结点仍满足B树的定义,则可直接删去该关键字。
- (2)假如b结点的关键字个数等于Min,说明删去关键字后该结点将不满足B树的定义。若可以从兄弟结点借。
- 兄弟结点最小关键字上移双亲结点
- 双亲结点大于删除关键字的关键字下移删除结点
- (3)结点的关键字个数等Min,兄弟节点关键字个数也等于Min
- 1.删除关键字
- 2.兄弟节点及删除关键字节点、双亲结点中分割二者关键字合并一个新叶子结点
- 3.若双亲结点关键字个数<=Min,重复2
B+树
索引文件组织中,经常使用B-树的变形B+树。B+树是大型索引文件的标准组织方式。
一棵m阶B+树满足下列条件:
- 每个分支节点至多有m棵子树。
- 根节点或者没有子树,或者至少有两棵子树
- 除根节点,其他每个分支节点至少有m/2棵子树
- 有n棵子树的节点有n个关键字。
- 所有叶子节点包含全部关键字及指向相应记录的指针
- 叶子节点按关键字大小顺序链接
- 叶子节点是直接指向数据文件中的记录。
- 所有分支节点(可看成是分块索引的索引表)
- 包含子节点最大关键字及指向子节点的指针。
B+树的查找:
- 包含子节点最大关键字及指向子节点的指针。
- 直接从最小关键字开始进行顺序查找所有叶节点链接成的线性链表。
- 从B+树的根节点出发一直找到叶节点为止。
m阶的B+树和m阶的B-树的差异:
- (1)非根结点关键字个数n不同、n取值范围不同
- B+树中:一个节点n个孩子则对应n个关键字。取值范围:m/2≤n≤m,根节点是1≤n≤m;
- B-树中:一个节点n个孩子则对应n-1个关键字。取值范围: m/2-1≤n≤m-1 ,根节点 1≤n≤m-1
根节点孩子至少为2.
- (2)叶子结点不一样
- B+树所有叶子节点包含了全部关键字
- B-树叶子节点包含的关键字与其他节点包含的关键字是不重复的。
- (3)B+树中所有非叶子节点仅起到索引的作用,而在B-树中,每个关键字对应一个记录的存储地址
- (4)通常在B+树上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,所有叶子节点链接成一个不定长的线性链表。
1.1.6散列查找。哈希表和哈希链2种构造方法、相关AVL计算。
至今我们已经学习过许多种查找方法,比如顺序查找,二分查找,二叉搜索树,B树等等,但是,如果遇见了数据量比较多的情况,上述方法就比较麻烦了。接下来我们要学习的内容或许对这些问题能很好的解决。
哈希表
哈希表,又称散列表,是除顺序表存储结构、链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。哈希表主要适用于记录关键字与存储地址存在某种函数关系的数据。比如说,每一个成员都按照学号进行存储,哈希的思想就是将数据通过函数的处理,变成容易储存方便查找的结构形式。这里就要引出几个概念了。
- 哈希函数:把关键字进行一定的处理得到方便存储的数据,存放在相应的哈希地址中,这相当于是一个加工的过程。
- 哈希表:存储数据记录的长度为m(m≥n)的连续内存单元。实际上也是线性表结构。
我们知道,虽然是用哈希方式进行存储,但是由于哈希函数选择的问题,会使不同的关键字经过函数处理后得到相同的结果,存在了同一个地址上,这样哦我们在查找的时候就会遇见麻烦,到底选哪一个数据呢?
我们称这种状态为哈希冲突,在哈希表存储结构中哈希冲突是很常见得,可见哈希结构也并不是完美的。哈希表设计主要需要解决哈希冲突。实际中哈希冲突是难以避免的,主要与3个因素有关: - (1)哈希表的长度,这里是与装填因子有关,装填因子α=存储的记录个数/哈希表的大小=n/m , α越小,冲突可能性就越小; α越大(最大可取1),冲突的可能性就越大。控制在0.6~0.9的范围内。
- (2)与选取得哈希函数有关,函数选的好冲突就会少;
- (3)与解决冲突的办法有关;
哈希函数的构造方法
(1)直接定址法:直接定址法是以关键字k本身或关键字加上某个数值常量c作为哈希地址的方法。直接定址法的哈希函数h(k)为:h(k)=k+c。这种方法是不会出现哈希冲突的,但是,对于数据的格式要求太大了,不适用于所有的情况,并且,如果数据分布不连续的话,会造成内春单元的浪费。这实际上是解决方法上面的差异。
(2)除留余数法:除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址的方法。哈希函数h(k)为:h(k)=k mod p (mod为求余运算,p≤m)这里的p最好是接近于哈希表长度的素数,这样会更大程度上减少哈希冲突的概率。 这是我们最常见的方法。
哈希冲突的解决方法:
当我们遇见冲突的时候,不要慌,真正的勇士敢于直面惨淡的人生以及惨痛的现实。有问题解决它就完事了。接下来要讲一下哈希冲突的解决方法。
(1)开放定址法:冲突时寻找新的空闲哈希地址。那么如何找呢?
-
线性探测法:线性探查法的数学递推描述公式为: d0=h(k) di=(di-1+1) mod m (1≤i≤m-1)。遇见冲突将元素向后移动一个位置看看该地址是否为空,空的话就占了它,记录移动次数,不空的话继续向下搜索,直到找到位置。
-
平方探测法:平方探查法的数学描述公式为: d0=h(k) di=(d0± i2) mod m (1≤i≤m-1),平方探查法是一种较好的处理冲突的方法,可以避免出现堆积现象。它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元。
查找哈希表的ASL
查找哈希表的时候,成功时的ASL和之前学习的内容无较大的区别。不成功时的ASL是这样子做的:假设查找m对应哈希地址为adr,查找h(adr)。查找不成功2种可能:
- 1.h(adr)==NULL,探测一次
- 2.h(adr)≠NULL,但是h(adr)!=key,继续探测adr++,直到 h(adr)==NULL
也就是如果找不见的话会继续后移,直到找间哈希表空的区域。
ASL(success)=(18+21+4*1)/10=1.4;
ASL(unsuccess)=(2+1+10+9+8+7+6+5+4+3+2+1+3)/13=4.692;
哈希表上的运算
//哈希表结构体定义:
#define MaxSize 100
#define NULLKEY -1
#define DELKEY -2
typedef char * InfoType ;
typedef struct{
int key;// 关键字域
InfoType data;//其他数据域
int count; //探查次数
}HashTable[MaxSize];
//哈希表的建立:
int InsertHT(HashTable ha,int p,int k,int &n){
int adr,i;
adr=k % p;
if(adr==NULLKEY || adr==DELKEY) //地址为空,可插入数据
{ ha[adr].key=k;ha[adr].count=1;}
else
{ i=1;
while(ha[adr].key!=NULLKEY && ha[adr].key!=DELKEY)
{
adr=(adr+1) % m;
i++;}//查找插入位置
ha[adr].key=k;ha[adr].count=i; //找到插入位置
}
n++;
}
//哈希表查找运算:
int SearchHT(HashTable ha,int p,int k)
{
int i=0,adr;
adr=k % p;
while(ha[adr].key!=NULLKEY && ha[adr].key!=k)
adr=(adr+1) % m;//探查下一个地址
if(ha[adr].key==NULLKEY) return -1;//地址为空,找不到
if(ha[adr].key==k) return adr; //找到关键字k
else return -1;
}
//哈希表的删除:
int DeleteHT(HashTable ha,int p,int k,int &n)
{ int adr;
adr=SearchHT(ha,p,k);
if(adr!=-1){
ha[adr].key=DELKEY;//逻辑删除,该元素贴删除标记
n--;
return 1; }
else return 0;
}
拉链法解决哈希冲突:
除了上述说的顺序存储结构,也可以用链式存储结构来解决哈希冲突问题。有点类似之前学的邻接表的结构:拉链法是把所有的同义词用单链表链接起来的方法。
这样子做是不是比之前的方法简单明了。
//有关哈希表运算的代码:
#include<stdlib.h>
#include<math.h>
struct HashTable;
struct ListNote;
typedef struct HashTable *HashTbl;
typedef struct ListNote *Position;
typedef Position List;
int Hash(int key,int tablesize);
int NextPrime(int x);
HashTbl InitalizeTable(int TableSize);
void DestroyTable(HashTbl H);
Position Find(int key,HashTbl H);
void Insert(int key, HashTbl H);
void Delete(int key,HashTbl H);
struct HashTable{
int TableSize;
Position *TheList;
};
struct ListNote{
int element;
Position next;
};
int Hash(int key,int tablesize){
return key%tablesize;
}
int NextPrime(int x){
int flag;
while(1){
flag = 0;
int i;
int n = sqrt((float)x);
for(i = 2 ;i <= n;i++){
if(x % i == 0){
flag = 1;
break;
}
}
if(flag == 0)
return x;
else
x++;
}
}
HashTbl InitalizeTable(int TableSize){
if(TableSize <= 0){
printf("散列大小有问题\n");
return NULL;
}
HashTbl table = (HashTbl)malloc(sizeof(struct HashTable));
if(table == NULL)
printf("分配失败");
table->TableSize = NextPrime(TableSize);
table->TheList = (Position*)malloc(sizeof(List) * table->TableSize);
if(table->TheList == NULL)
printf("分配失败");
table->TheList[0] = (Position)malloc(table->TableSize*sizeof(struct ListNote));
if(table->TheList == NULL)
printf("分配失败");
int i;
for(i = 0;i < table->TableSize;i++){
table->TheList[i] = table->TheList[0] + i;
table->TheList[i]->next = NULL;
}
return table;
}
Position Find(int key,HashTbl H){
Position p;
List L = H->TheList[Hash(key,H->TableSize)];
p = L->next;
while(p != NULL && p->element != key)
p = p->next;
if(p == NULL)
return L;
else
return p;
}
void Insert(int key,HashTbl H){
Position p,NewCell;
p = Find(key,H);
if(p->element != key){
NewCell = (Position)malloc(sizeof(struct ListNote));
if(NewCell == NULL)
printf("分配失败");
else{
p = H->TheList[Hash(key,H->TableSize)];
NewCell->next = p->next;
p->next = NewCell;
NewCell->element = key;
}
}
else
printf("已经存在该值了\n");
}
void Delete(int key,HashTbl H){
Position p ,NewCell;
p = Find(key,H);
if(p->element == key){
NewCell = H->TheList[Hash(key,H->TableSize)];
while(NewCell->next != p)
NewCell = NewCell->next;
NewCell->next = p->next;
free(p);
}
else
printf("没有该值");
}
int main(){
HashTbl table = InitalizeTable(10);
Position p = NULL;
p = Find(10,table);
printf("%d\n",p->element);
Insert(55,table);
Insert(90,table);
Insert(35,table);
Insert(33,table);
p = Find(55,table);
printf("%d\n",p->element);
p = Find(33,table);
printf("%d\n",p->element);
Delete(33,table);
Delete(44,table);
system( "pause" );
return 0 ;
}
1.2.谈谈你对查找的认识及学习体会。
查找这部分内容和之前学的知识有很大一部分的联系,相当于复习C语言学过的内容,比较简单,也好上手进行操作。但是,每一种查找方法的发明都不是我们看课本就能学到这么简单。刚开始学习快速排序和堆排序的时候,我盯着代码看了很长时间,但是还是理解不了他的操作过程。直到听过老师讲解,用动画的方式进行演示之后,才明白其中的精妙之处。然后感叹发明者新奇的脑洞。。这部分的代码量比较少,但是需要考虑的地方还是蛮多的。这个学期已经快结束了,感觉过的真快。美好的大一生活这样就要结束了。从一个字都打不利索的菜鸟进阶为能用所学的知识解决一定问题的菜鸟也是一种巨大的进步呢。。接触代码的世界让我更加相信作为一个平凡人的力量,代码改变世界对吧,这是一个平凡人能做的努力吧。继续加油。
2.PTA题目介绍(0--6分)
自选3题,必须是编程题,必须包含知识点:
二叉搜索树
哈希表
2.1 题目1:是否完全二叉搜索树
2.1.1 该题的设计思路
题面已经交代很清楚了,该题主要分为两部分,层次遍历二叉树,并且判断该树是否为完全二叉搜索树。那么思路就很清晰了,首先建树(要注意题目要求是左边值大,右边值小),然后层次遍历,然后判断。建树以及层次遍历之前都讲过,所以该题的重点是判断部分。根据完全二叉树的特点,叶子节点都集中在树的最后一层和倒数第二层,并且左孩子节点要优先满足。所以我的想法是,将最后一层的左右节点都放到一个队列中,将队列的元素一个一个出队,如果遇见结点为NULL,但是队列中仍然还有元素的话,说明不是完全二叉树。
- 时间复杂度O(n);
- 空间复杂度O(n);
2.1.2 该题的伪代码
#include<iostream>
#include <queue>
using namespace std;
typedef int ElementType;
typedef struct TNode* Position;
typedef Position BinTree;
struct TNode
{
ElementType Data;
BinTree Left;
BinTree Right;
};
BinTree Insert(BinTree BST, ElementType X);//插入;
void Level_traverse(BinTree BST);//层次遍历;
void Judge(BinTree BST);//判断是否为完全二叉树;
int main()
{
定义整型变量 n, val,i表示输入数据个数,以及数据;
定义一个根节点BinTree BST = NULL;
输入数据数量 n;
循环输入数据
{
cin >> val;
BST = Insert(BST, val);
}
Level_traverse(BST); 层次遍历;
Judge(BST)判断是否为完全二叉树;
return 0;
}
BinTree Insert(BinTree BST, ElementType X)
{
若根节点为空,左右孩子赋为空,根节点赋值为X
若根节点不为空,插入值比根节点小做右孩子BST->Right = Insert(BST->Right, X); 大作左孩子;BST->Left = Insert(BST->Left, X);
return BST;
}
void Level_traverse(BinTree BST)
{
定义队列q, p;
while (队列不为空)
{
每一层结点入队,输出;
}
}
void Judge(BinTree BST)//判断是否为完全二叉树;
{
BinTree p;
定义一个队列 q;
根节点入队q.push(BST);
将每一层每个节点分别入队出队,直到队列中只存在最后一层元素;
while (队不为空)
{
当遇见结点为NULL,但是队列中仍然有元素,输出“NO”
}
否则输出“YES”;
}
2.1.3 PTA提交列表
提交列表截图。并介绍代码编写中碰到问题及解决方法。
Q:内存超限。因为判断是否为完全二叉树的代码也是要得到该树层次遍历最后一层的结果,所以我就想是否能通过只写一个函数,就完成层次遍历和判断的内容。结果是不可以的。层次遍历的时候每得到一层就要把上一层输出,我没有办法保存最后一层的数据,所以做不出结果来。
A:解决办法就是,写成两个函数。虽然重复的内容多了一点。但是比较容易做,并且不会出错。
Q:部分正确。因为当时没有看清楚题,它这棵树是左大右小,所以建树的时候就出了问题;
A:改代码,如果插入节点比根节点小,作为右孩子;插入节点比根节点大,作为左孩子;
2.1.4 本题设计的知识点
- 本题考察了完全二叉搜索树的建立,层次遍历的操作;
- 根据完全二叉搜索树的特点,判断一棵树是否为完全二叉搜索树,利用的队列的知识;
2.2 题目2: 整型关键字的散列映射
2.2.1 该题的设计思路
题面分析。分析题面数据如何表达。
该题主要考察的是利用除留余数法将数据放入哈希表中并且用线性探测法解决冲突。
该题的关键主要有两个方面:
- 利用一个visited数组来保存某个地址是否存在数据,即是否发生了哈希冲突;
- 利用Num数组来记录输入的数据是否与之前的数据是相同的,如果相同直接输出结果,而不需要再次访问哈希表。
这种做法的时间复杂度为O(n^2),主要是发生冲突时解决冲突需要两层循环;空间复杂度为O(n)。
2.2.2 该题的伪代码
文字+代码简要介绍本题思路
#include<iostream>
using namespace std;
#define MAXN 2000
定义全局变量 visited[MAXN] = { 0 }表示是否访问过
定义全局变量Num[1000000] = {-1}标记该数字是否出行过
int main()
{
定义整型变量 n, p,i,index,num;//输入数据个数和p,数据在哈希表中存储的下标,输入的数据;
输入n, p;
循环输入数据
{
输入数据 num;
除留余数法计算下标index = num % p;
如果不是第一个数据,控制格式输出;
若该数据在哈希表中存在
{
输出<< Num[num];
continue;
}
如果得到的下标的位置在哈希表中为空
{
标记访问过visited[index] = 1;
将下标值赋给Num[num] = index;
输出 index;
}
else//也就是不为空的时候,线性探测法
{
for( j = 1 to j < p)
{
定义整型变量temp = (num + j) % p;
如果得到的下标的位置在哈希表中为空
{
标记visited[temp] = 1;
Num[num] = temp;
输出 << temp;
break;
}
}
}
}
return 0;
}
2.2.3 PTA提交列表
提交列表截图。并介绍代码编写中碰到问题及解决方法。
Q:答案部分正确主要是有一个测试点,有重复关键字的处理没有做好,做之前没有考虑这种情况。
A:解决办法就是再定义一个全局变量Num用来记录数据是否重复出现,当NUM[num]值被改变之后,遇见输出就先判断一下是否为重复数据,是的话直接输出,减少了多次访问带来的代码上的繁琐。
2.1.4 本题设计的知识点
- 练习了除留余数法的代码如何操作;
- 复习了全局变量如何使用以及如何处理重复数据情况;
- 练习了线性探测法的代码操作;
2.3 题目:(哈希链) 航空公司VIP客户查询
2.3.1 该题的设计思路
该题主要考察的是哈希链的知识,涉及到如何建链,如何用除留余数法,如何选取数据等方面知识。由于本题是要对身份证这个数据进行处理,数据量比较庞大,如何选取数据就是比较关键的问题。这里主要选取身份证的后5位来表示某个人的信息,但是根据常识我们知道身份证最后一位是0--9还有x(x位罗马数字表示10)所以,解决了数据的选取,问题就和之前整型关键字的散列映射问题解决办法相似了。
思路:
- 首先获得每一位乘客的身份证信息,这里取身份证的后5位,每个身份证信息都用string存储,所以取数据的时候要进行数据的转换。
- 建立哈希链,利用除留余数法得到身份证数据所在的下标,利用头插法插入数据;
- 建立Search函数来进行数据的搜索;
本题时间复杂度为O(n^2)空间复杂度为O(n);
2.3.2 该题的伪代码
文字+代码简要介绍本题思路
#include<iostream>
#define HashSize 20000
#define IDLAST 17
#define MAXSIZE 20
using namespace std;
typedef struct HashNode
{
string ID;
int distance;
struct HashNode* next;
}HashNode, * HashChain;
void CreateHash(HashChain HC[], int n, int k);
void Insert(HashChain HC[], int k);
int GetID(string ID);
void Search(HashChain HC[], string ID);
int main()
{
定义整型变量 n, k, m,i;分别表示乘客数,最低里程数,待查寻人数;
定义身份证信息string ID;
定义哈希链HashChain* HC;
动态申请空间HC = new HashChain[HashSize];
ID.resize(MAXSIZE);
输入乘客数量,最低里程数;cin >> n >> k;
创建哈希链CreateHash(HC, n, k);
输入查询人数;
for (i = 0 to i < m)
{
输入待查寻ID;
Search(HC, ID);
}
return 0;
}
void Search(HashChain HC[], string ID)
{
定义下标 index;
定义结构体变量HashChain p;
除留余数法的下标index = GetID(ID) % HashSize;
p = HC[index]->next;
while (该链表不为空)
{
if (发现ID相同)
{
输出里程数;
return;
}
p = p->next;
}
循环结束仍未发现相同,输出“NO”;
}
void CreateHash(HashChain HC[], int n, int k)
{
定义 i;
for (i = 0 to i < HashSize)
{
为HC[i]申请空间;
HC[i]->next = NULL;
}
for (i = 0 to i < n) 插入数据建链Insert(HC, k);
}
void Insert(HashChain HC[], int k)
{
定义string ID;
定义整型变量distance, index表示路程数,下标;
定义结构体变量HashChain node, p;
ID.resize(MAXSIZE);
输入ID,distance);
若distance小于标准里程数distance = k;
除留余数法获得下标index = GetID(ID) % HashSize;
p = HC[index]->next;
循环寻找该点是否已经插入
若(p == NULL)
{
node = new HashNode;
node->ID = ID;
node->distance = distance;
node->next = HC[index]->next;
HC[index]->next = node;
}
else增加改乘客的行驶路程p->distance += distance;
}
int GetID(string ID)
{
定义最后五位数 lastID = 0;//最后5位数;
int i;
for (i = 13 to i < IDLAST)
由于是字符型数据,对每一位都要进行转化lastID = lastID * 10 + (ID[i] - '0');
if (最后一位ID为 'x') lastID = lastID * 10 + 10;
else lastID = lastID * 10 + (ID[i] - '0');
return lastID;
}
2.3.3 PTA提交列表
提交列表截图。并介绍代码编写中碰到问题及解决方法。
- 该题看起来比较难,代码量也是比较大的,但是真正做起来,多半是复习了之前学过的知识。比如链表的建立,头插法,除留余数法的运用,数据类型的转换等。
- 本题要处理的数据为身份证信息,数据量是比较庞大的,选取合适的存储方式会使数据的查找更加的方便快捷。同时,也要有点生活常识,比如身份证最后4位是每个人的辨别信息,最后一位表示的是性别,单数为男性,双数为女性,还会出现古罗马数字x,知道这些常识后就会将所有情况都考虑到,防止遗漏。
2.3.4 本题设计的知识点
- 哈希链表的建立,除留余数法的应用;
- 复习了头插法的代码,以及数据类型的转化;
- 了解了处理多数据的方法;