DS博客作业05-查找
0.PTA得分截图
1.1本周学习总结
查找算法的评价指标(ASL)
关键字的平均查找次数,也叫做平均搜索长度(ASL)。一般计算某查找算法中查找成功和查找不成功的平均搜索长度来评判某算法的执行效率:如果ASL越大,其时间性能越差,反之,则时间性能越好,其计算公式为:
例如:
静态查找
仅作查询和检索操作的查找表,有顺序查找,二分查找,分块查找等;
顺序查找
从表的一端开始,逐个扫描,将扫描到的关键字和给定的k值比较,若相等则查找成功,若扫描结束后,未找到关键字的记录,则查找失败。
具体代码
ASL分析
-
成功:从表的一端开始查找,按顺序扫描,每个数据的查找次数依次递增1,且每个位置上数据出现的概率都相等,所以第i个数据的查找次数ci为i,查找成功的ASL为:
-
不成功:若要查找一个不存在于顺序表中的数据时,一定要遍历完整个顺序表之后才能确定改数据不存在于顺序表中,故查找不成功时的ASL为:
二分查找(适用于有序序列)
设n个元素的数组a已经有序,用low和high两个变量来表示查找的区间,即在a[low]~a[high]中去查找用户输入的值x,和区间中间位置的元素a[mid] (mid=(low+high)/2)作比较,如果相等则找到,算法结束;如果是大于则修改区间(数组为升序数列:low=mid+1;数组为降序数列:high=mid-1);如果是小于也修改区间(数组为升序的改为:high=mid-1,;数组为降序数列:low=mid+1);一直到x=a[mid] (找到该值在数组中的位置)或者是low>high(在数组中找不到x的值)时算法终止;
具体代码
ASL分析
为了更好的计算该算法的ASL,以当前查找区域的中间位置上的记录作为根节点,左子表作为根节点的左子树,右子表作为根节点的右子树,建立一棵二分查找的判定树,我们以下图为例,圆形为内部结点,矩形为序列中不存在的数据范围,为外部结点。计算下列有序序列的ASL:
- 成功:如果查找成功,每个数据查找的次数为该节点在二叉树中所在的高度
所以查找成功的ASL为: - 不成功:比较到圆形节点就结束了,矩形节点只是为了方便我们阅读而设置的,虚构的节点,并不存在,故不成功的比较次数并不是矩形节点在二叉树中的高度,而是比较到矩形的上层节点就结束了。
所以查找不成功的ASL为:
总结得出二分查找查找成功或不成功的平均查找长度均为(h为判定树高度):,和顺序查找对比,二分查找的ASL比顺序查找小,二分查找的查找效率比较高。
二叉排序树
定义
二叉排序树又称二叉搜索树。在一棵二叉排序树中,若根节点的左子树不为空,那么左子树上所有的结点都小于根结点;若根节点的右子树不为空,那么右子树上所有结点都小于根节点。且根结点的左右子树叶都是二叉排序树。二叉排序树中所有的关键字都是唯一的。二叉树还有特殊的性质:按中序遍历可以得到一个递增有序序列。
结构体定义
typedef struct node
{
KeyType key;//关键字;
InfoType data;//其他数据域;
struct node*lchild,*rchild;//左右孩子指针;
}BSTNode,*BSTree;
二叉树插入和创建操作
插入操作思路
在二叉排序树中插入一个关键字为k的结点要保证插入后仍然满足BST性质,其插入过程为:
- 若二叉排序树bt为空,则创建一个key域作为k的结点,将它作为根结点;
- 若不为空,则与根节点的关键字作比较
- 若相等,则说明树中已经存在该关键字,无需插入,直接返回;
- 若比根节点的值小,就进入根节点的左子树中,否则进入右子树;
该结点插入后一定为叶子结点。
具体代码
创建二叉排序树
创建一棵二叉排序树是从一个空树开始的,每插入一个关键字,就调用一次插入算法把它插入到当前已经生成的二叉排序树中。
具体代码
查找操作
思路
递归查找,查找思路和普通二叉树不同,因为二叉搜索树的左子树中所有值都比根节点小,右子树中所有值都比根节点大,根据这一特点,我们可以将待查找数据和当前的根节点进行比较,如果待查找数据比较小,就有选择的进入左子树进行查找,不用像之前普通二叉树的查找一样,不仅要进左子树还要进右子树中查找。
BSTNode* BSTNode * SearchBST(BSTNode* bt,KeyType k)
{
若bt为NULL或者bt->key==k ,返回bt;
若bt->key<k,递归查找左子树;
若bt->key>k,递归查找右子树;
}
代码实现
拓展
若不仅要找到关键字为k的结点,还要找到其双亲节点,形参中应该再添加两个指针变量,一个用于保存待查找节点的双亲指针father,一个保存遍历过程中当前节点的双亲指针f————主要目的是辅助寻找待查找节点的双亲指针,初始化为NULL,代码如下:
ASL分析
和折半查找的判定树类似,二叉排序树中的结点作为内部结点,可以添加相应的外部结点。具有n个内部结点的二叉排序树,其外部结点个数为n+1。和折半查找不同的是,用折半查找法查找长度为n的有序表,其判定树是唯一的,而含有n个元素的二叉排序树却不唯一。对于含有同样一组元素的表,由于元素插入的先后次序不同,构成的二叉排序树形态和高度可能也不同。在成功和不成功的条件下,他们的平均查找长度也不相同。例如:
由此可见二叉排序树的平均查找长度和二叉排序树的形态有关。下面我们来计算一棵二叉排序树的ASL:
删除操作
思路
二叉排序树中删除一个节点后,其二叉树应该仍然保持BST性质。删除操作必须先进行查找,如果找到,则删除的节点p分为三种情况:
- 情况1:p节点为叶子结点,直接删去该结点,双亲节点相应指针域修改为NULL;
- 情况2:p结点只有左/右子树,有其左子树或者右子树代替他,双亲节点相应的指针域修改;
- 情况3:p结点既有左子树也有右子树。根据二叉排序树的特点,可以从其左子树中选择关键字最大的结点r(最右结点),用结点r的值代替结点p的值(结点值替换),保存r结点的左孩子,再删去r结点。也可以从右子树中选择关键字最小的结点l(最左结点),用结点l的值代替结点p的值(结点替换),保存l结点的右孩子,再删去结点l。
伪代码
int DelteBST(BSTNode* &bt, KeyType k)
{
if(bt->key < k) 递归左子树查找k;
else if(bt->key > k) 递归右子树查找k;
else if(bt->key == k)
若bt为叶子结点, 删除bt;
若bt只有左子树,保存bt的左孩子node,删除bt,bt=node;
若bt只有右子树,保存bt的右孩子node,删除bt,bt=node;
若bt有左右孩子
递归查找bt左子树最右孩子r;
bt->key=r->key;//没有删除结点,只是替换数据;
保存r左孩子node,删除r,r=node;
end if
}
代码实现
平衡二叉树(AVL树)
概念:
二叉排序树中查找的操作执行时间与树形有关,在最坏情况下执行的时间为O(n)(单边树时)。为了提高查找效率,在既能保持BST性质,又能保证树的高度在任何情况下为log2,这样在查找过程中,即使是最坏情况下,执行的时间也还是O(log2),这样的二叉树称为平衡二叉树。
一棵平衡二叉树中,每个结点的左,右子树高度最多相差1。在算法中,我们通过平衡因子来具体实现平衡二叉树的定义。一个结点的平衡因子是该结点左子树的高度减去右子树的高度(或者右子树的高度减去左子树的高度)。如果一棵二叉树中的所有结点平衡因子的绝对值都不超过1,那么这颗二叉树就是平衡二叉树。
插入结点和调整过程
若向平衡二叉树中插入一个新结点(总是作为叶子结点插入)后破坏了平衡性,首先从该新插入的结向根结点方向查找第一个失衡的结点,然后进行调整。调整的操作可以归纳为下列四种情况:
LL调整
若在结点A的左孩子的左子树上插入一个结点导致结点A失衡,进行LL调整:将A结点的左孩子B上升代替结点A成为根节点,A结点作为B结点的右孩子,而B结点的原来右子树成为结点A的左子树。
RR调整
若在结点A的右孩子的右子树上插入一个结点导致结点A失衡,进行RR调整:将A结点右孩子B上升代替结点A成为根节点,A作为B结点左孩子,而B的原来左子树成为结点A的右子树。
LR调整
若在结点A的左孩子的右子树上插入一个结点导致结点A失衡,进行LR调整:将A结点的左孩子B上的右孩子C上升代替结点A成为根节点,A作为C的右孩子,B作为C的左孩子,C原来的左子树作为B的右子树,C原来的右子树作为A的左子树。
RL调整
若在结点A的右孩子的左子树上插入一个结点导致结点A失衡,进行RL调整:将A结点的右孩子B上的左孩子C上升代替结点A 成为根节点,A作为C的左孩子,B作为C的右孩子,C原来的左子树作为A的右子树,C原来的右子树作为B的左子树。
根据某个序列构造一棵AVL树
注意:
1.LL型调整和RR型调整时选择失衡点的L或者R孩子上升替换,LR和RL是选择失衡点的相应LR或者RL孙子上升替换的。
2.如果有多个失衡点,应该以失衡的最小子树(离插入位置最近的,以平衡因子绝对值大于1的结点为根节点的子树)进行调整,例如:
B-树
特性
BST和AVL树都是内存中,适用小数据量。每个节点放一个关键字,树的高度比较大。B-树一个节点可以放多个关键字,降低树的高度。可放外存,适合大数据量查找,一棵m阶B-树符合以下特性:
1.每个结点至多m个孩子结点,至多有m-1个关键字,除根节点外,其他节点至少有m/2个孩子结点,至少有m/2-1个关键字;
2.若根节点不是叶子结点,根节点至少两个孩子结点;
3.叶子结点一定都在最后一层,叶子结点下面还有一层是虚设的外部结点————就是表示查找失败的结点,不含任何信息,在计算B-树高度时要计入最底层的外部结点;
4.每个结点的结构为:
- n为关键字个数,n+1为孩子指针;
- 结点中按关键字大小顺序排列,ki < ki+1;
- pi为该节点的孩子指针,满足:
- p0指向的结点中关键字小于k0;
- pi指向的结点中关键字大于等于[ki,ki+1];
- p0指向的关键字>kn。
结构体定义
/*B-树存储结构*/
typedef int KeyType;
typedef struct node
{
int keymun;//当前结点拥有的关键字个数;
struct node *parent;//双亲结点指针
struct node *ptr[MAXM];//孩子结点指针数组;
}BTNode
/*查找结果返回类型*/
typedef struct
{
BTnode *pt;//指向找的结点的指针;
int i;//在结点中的关键字序号;
int tag;//标志查找成功或者失败;
}Result;
B-树的查找
在一棵B-树上顺序查找关键字,将k于根结点中的key[i]比较:
1.若k==key[i],则查找成功;
2.若k < key[1]:,则沿指针ptr[0]所指的子树继续查找;
3.若key[i]<k<key[i+1],则沿着指针ptr[i]所指的子树进行查找;
4.若k>key[i],则沿着指针ptr[i]所指的子树进程查找;
5.查找某个结点,若相应指针为空,落入一个外部结点,表示查找失败;类似判断树和二叉排序树的查找。
B-树的插入
往B-树中插入一个结点,插入位置必定在叶子结点层,但是因为B-树中对关键字个数有限制,所以,插入情况分为以下两种:
1.关键字个数n<m-1,不用调整;
2.关键字个数n=m-1,进行“结点分裂”
如果分裂完之后,中间关键字进入的结点中关键字个数达到n=m-1,就继续进行“结点分裂”,例:
B-树的删除
B-树的删除和插入考虑相反,还要考虑删除的是叶子结点还是非叶子结点。
非叶子结点删除
1.从Pi子树节点借调最大或者最小关键字key代替删除结点;
2.pi子树中删除key;
3.若子树节点关键字个数 < m/2-1,重复步骤1;
4.若删除关键字为叶子结点层,按叶子结点删除操作。
叶子结点删除
情况1:结点b关键字个数 > m/2-1, 直接删除;
情况2:结点b关键字个数 = m/2-1, 兄弟结点关键字个数大于 m/2-1,于是我们可以向兄弟结点借一个关键字;
- 步骤1:兄弟结点最小关键字上移至双亲;
- 步骤2:双亲节点大于删除关键字的关键字下移至删除结点。
情况3:结点b关键字个数 = m/2-1,兄弟结点关键字个数 = m/2-1,兄弟结点没有关键字可以借:
- 步骤1:删除关键字
- 步骤2:兄弟结点及删除结点,双亲节点中分割二者的关键字合并成一个新的叶子结点
- 步骤3:若双亲节点的关键字个数 < m/2-1 ,则进行步骤2
B-树的应用
B-树经常被用于对于检索时间要求苛刻的场合,比如:
1.B-树索引是数据库中存取和查找文件(称为记录或者键值)的一种方法;
2.硬盘中的结点也是B-树结构,B-树利用多个分支(称为子树)的结点,减少获取记录是所经历的结点数,从而达到节省存取时间的目的;
B+树
特性
一棵m阶b+树满足条件:
1.每个分支节点至多有m棵子树;
2.根结点或者没有子树,或者至少两棵子树;
3.除根结点,其他每个分支结点至少有m/2棵子树;
4.有n棵子树的结点有n个关键字;
5.所有叶子结点包含全部关键字及指向相应记录的指针;
- 叶子结点按关键字大小顺序链接;
- 叶子结点时直接指向数据文件的记录;
6.所有分支结点(可以看成是分块索引的索引表),包含子节点最大关键字及指向子节点的指针;
B+树的查找
方法1.因为所有的叶子结点链接起来成为一个线性链表,可直接总最小关键字开始进行顺序查找所有叶子节点;
方法2.从B+树的根结点开始出发,一直找到叶结点为止;
B+树和B-树之间的差别
1.叶子结点关键字个数n不同,n取值范围不同:
- B+树中:一个结点n个孩子对应n个关键字,取值范围为:m/2~m, 根结点为1~m;
- B-树中:一个结点n个孩子对应n-1个关键字,取值范围为:m/2-1 ~ m-1 , 根结点为1~m-1。
2.叶子结点不一样,B+树中所有叶子结点包含了所有的关键字,B-树中叶子结点包含的关键字于其他结点包含的关键字是不重复的;
3.B+树中所有非叶子结点仅起到索引作用,而在B-树中,每个关键字对应一个记录的存储地址;
4.通常在B+树上有两个头指针,一个指向根结点,另一个指向关键字最小的叶子结点,所有叶子结点链接成一个不定长的线性链表。
哈希表
概念
哈希表又称为散列表,是除顺序表存储结构,链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构,其基本思路是,设要存储的元素个数为n,设置一个长度为m的连续内存单元,以每个元素的关键字ki(i取值为0~n-1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或下标),并把该元素存储在这个内存单元中,这个地址也称为哈希地址。这样构造的线性存储结构就是哈希表。
在建哈希表示可能存在两个关键字虽然不相同,但是其哈希地址却一样的情况,这种现象叫做哈希冲突。通常把具有不同关键字却有相同哈希地址的元素称为同义词。
哈希表的构造
直接定址法
直接定址法是以关键字k本身或关键字加上某个数值常量C作为哈希链地址的方法,哈希函数为:h(k)=k+c,例如:
优势:计算简单,不可能有冲突发生;
缺点:关键字分布不连续,将造成内存单元的大量浪费。
除留取余数法
用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址,h(k)=k%p(这里的p最好为不大于m的质数,减少冲突的可能性),例如
优势:计算简单,适用范围广泛;
缺点:容易发生哈希冲突。如果在上面的例子中插入14:h(14)=14%7=0 于7发生了哈希冲突。
数字分析法
只适用与所有关键字都已知的情况,并需要对关键字中的每一位的取值分布情况进行分析.
哈希冲突的解决
开放定址法
在出现哈希冲突时,再哈希表中找到一个新的空闲位置存放元素,例如:要存放关键字k,d=h(k),而地址d已经被其他元素占用了,那么就在d地址附近寻找空闲位置进行填入。开放定址法分为线性探测和平方探测等
线性探测
线性探测是从发生冲突的地址d0开始,依次探测d0的下一个地址(当到达哈希表表尾时,下一个探测地址为表首地址0),直到找到一个空闲的位置单元为止,探测序列为:d=(d+i)%m,所以,我们在设置哈希表的长度时一定要大等于元素个数,保证每个数据都能找到一个空闲单元插入:
- 思路
/*d为地址,h()为哈希函数,ht为哈希表,m为哈希表长度*/
d = h(k);
if(ht[d]为空)
ht[d].key= k;
else
while(ht[d]不为空)
d=(d+1)%m;//注意这里除的是哈希表长度!!不是p;
end while
end if
下列我们举个例子更好的理解线性探测:
设哈希表的长度为m=13,采用除留余数法(p=13)加线性探测法建立关键字集合{16,74,60,43,54,90,46,31,29,88,77}的哈希表。
- 优势:解决冲突简单;
- 缺点:容易产生堆积问题。
平方探测法
如果发生冲突的地址为d0,平方探测法的探测序列为:
- 优势:是一种很好的处理冲突的方法,可以避免出现堆积问题;
- 缺点:不一定能探测到哈希表上所有的单元,但最少能探测到一半单元。
拉链法
拉链法是把所有的同义词用单链表链接起来的方法,所有哈希地址为i元素对应的结点构成一个单链表,哈希表地址空间为0~m-1,地址为i的单元是一个指向对应单链表的头结点。这种方法中,哈希表的每个单元中存放的不再是元素本身,而是相应同一词单链表的首结点指针。
思路
d=h(k);
动态申请新空间,设置新结点p,将关键字保存;
/*头插法插入哈希链中*/
p->next=ht[d].next;
ht[d].next=p;
优势:
- 处理冲突简单,无堆积现象,非同义词不会发生冲突;
- 结点空间时动态申请的,空间利用效率高;
- 在链表中,删除结点操作方便。
用开放地址法构造哈希表的运算算法
结构体定义
#define NULLKEY -1 //定义空关键字值
#define DELKEY -2 //定义被删关键字字值
typedef int KeyType;
typedef struct
{
KeyType key;//关键字域
int count;//探测次数域
}HashTable;//哈希表单元类型
插入及建表算法
在建表时,首先要将表中各元素的关键字清空,使其地址为开址为开放的;然后调用插入算法将给定的关键字序列依次插入表中。在插入算法中,求出关键字k的哈希函数值adr,若该位置可以直接放置(即adr位置的关键字为NULLKEY或者DELKEY),将其放入;否则出现冲突,采用线性探测法在表中找到一个开放地址,将k插入。
具体代码
删除操作
在采用开放地址法处理冲突的哈希表上执行删除操作,不能简单地将被删元素的空间置为空,否则将截断在它之后填入哈希表同义词元素的查找路径,这是因为在各种开放地址中,空地址单元都是查找失败的条件。因此只能在删除元素上做删除标记DELKEY,而不能真正删除元素。
具体代码
查找算法
哈希表查找过程和建表过程相似。假设查找关键字k,根据建表时采用的哈希函数h计算出地址,若表中该地址单元不为空且该地址的关键字不为k,则按建表时采用的处理方法查找下一个地址(下面用线性探测法),反复下去直到某个地址单元关键字和k相等(查找成功),或者该地址单元为空为止。
ASL分析
- 成功:查找一个关键字所需要比较的次数为对应的探测次数,故成功的ASL为
- 不成功:在哈希表中查找某个关键字,其哈希地址为d,然后从下标为d的地址单元开始查找,如果该关键字不存在,会一直往后遍历到一个空的位置才会停止,如:
故不成功的ASL为:
例:
用拉链法构造的哈希表运算
结构体定义
typedef int KeyType;//关键字类型
typedef struct node
{
KeyType key;//关键字
struct node* next;//下一个结点指针;
}HNode,*HashTable
插入及建表算法
建表过程就是先将ha[i]的所有头结点都置为空,然后调用插入算法依次插入关键字,插入用头插法。
具体代码
删除算法
如果要在哈希表中删除关键字为k的结点,首先在单链表ha[h(k)]中找到对应的结点q,通过前驱结点preq来删除他。不同于开放地址法构建的哈希表,在这里可以直接删除结点。
具体代码
查找算法
在哈希表中查找关键字为k的结点,只要再单链表ha[h(k)]中找到对应的结点q,并累计关键字的比较次数。当前q为空时表示查找不成功。
具体代码
ASL分析
-
成功:在哈希表中查找存在的某个关键字,对应的结点应该在单链表ha[h(k)]中,其探测次数为在单链表中的位置,故ASL成功为:
-
不成功为:在哈希表中查找不存在的关键字k时,需要在单链表ha[h(k)]中遍历所有的结点,判定到空指针才停止,所以ASL不成功为:
例:
1.2.谈谈你对查找的认识及学习体会。
1.学习二叉排序树的时候,感觉是之前学习过的内容,所以有些不太重视,在做pta的题目时,总是惯性思维,运用解决普通树的问题来解决二叉排序树的问题,但是其实由于二叉排序树的结构特性,存在着更加快捷的做法,这样会使算法的执行效率大大提高,所以在解决二叉排序树相关问题时,要注意结合二叉排序树的结构特性来解题;;
2.平衡二叉树中调整的操作要特别记住,如果出现多个失衡点时,一定要先调整最小失衡子树!LL调整和RR调整中是孩子上升成为根节点,LR调整和RL调整是孙子结点上升成为根节点;
3.B-树的删除操作和插入操作一定要区分清楚!在删除操作中对叶子结点和非叶子结点的操作也有所不同,要在处理叶子结点删除问题时特别注意的是:如果进行合并之后,双亲节点的关键字小于Min,这个时候要继续用合并的方法,而不去向孩子结点借调。这里是我刚开始接触时,一直混淆的地方;
4.哈希表在上个学期就有稍微接触过,感觉是一个很方便的结构,但是我在做pta时候,经常超时,要么在快要超时的边缘徘徊,反观同学们的运行时间,比我的都要少很多。后来发现原因是因为在设置哈希函数时,也就是要计算哈希地址时,我要么设置的p不合适,要么就是直接就用直接定址法(因为真的很方便),这样下来我的运行时间就比同学的要多很多。这里还有一个要注意的是计算ASL,在计算ASL不成功的时候,我总是用哈希表的长度m作为分母,这个是错误的!!(我已经栽了两次跟头,事不过三,一定一定要记住!),应该是以p作为分母!!。
2.PTA题目介绍
2.1 是否完全二叉搜索树
2.1.1 该题的设计思路
- 二叉搜索树的建立:由题面我们可以看到,题面要求我们左子树键值大,右子树键值小,故我们在建立二叉搜索树时,要注意如果待插入结点比当前结点小的话进右子树,比当前结点大进左子树;
- 完全二叉树:若一棵二叉树的高度为h,那么1到h-1层的结个数都达到最大,而最后一层结点连续且都集中在最左边。
- 完全二叉树的层次遍历特点:完全二叉树在进行层次遍历时,如果遍历到一个结点只有左孩子,或者左右孩子均为空时,那么剩余未遍历的结点,全都是叶子结点。
- 算法的时间复杂度为:O(n);
2.1.2 该题的伪代码
void InserBST(BinTree*& BST,KeyType k)//往二叉树中插入结点k
{
if(BST==NULL) 建立新结点;
else if(k<BST->key) 递归进入右子树;
else 递归进入左子树
}
/*层次遍历二叉树并判断是否为完全二叉树*/
bool Level_DispBST(BinTree* BST)
{
定义变量end表示是否已经找到第一个叶子结点或者第一个只有左孩子结点;
(如果是,则接下来未遍历的结点应该都是叶子结点)
定义flag保存判断结果;
若根结点BST不为空,入队,并输出;
while(队不为空)
出队一个结点p;
若结点p的左,右孩子不为空,依次输出,并入队;
若在剩余节点都应该是叶子结点的情况下p为非叶子节点
不是完全二叉树,flag=flase;
若结点p只有右孩子,没有左孩子
不是完全二叉树,flag=flase;
若结点p为叶子结点或者为第一个只有左孩子的结点
修改end,表示接下来应该都是叶子结点;
end while
return flag;
}
具体代码
2.1.3 PTA提交列表
1.答案错误(26分):我起初的做法是计算每一层的结点个数,如果在遍历到最后一层之前就提前出现某层结点个数没有达到最大就判定为不是完全二叉树,这样的做法忽略判断最后一层的结点是否都在最左边,有可能有不连续的情况。于是我又加入了一个变量用于判断最后一层是否出现缺口(就是不连续)。
2.答案错误(27分):我在26分的基础上只修改了最后一层的结点不连续的问题,对完全二叉树的结构理解错误,没有判断如果最后一层结点都在最右边的情况。
3.答案错误(19分):并没有发现自己对完全二叉树的结构理解错误,添加了一条错误的判断条件;
4.答案错误(27分):删去19分添加的错误判断条件,发现对完全二叉树理解错误,于是重新寻找解题思路,找到完全二叉树的层次遍历规律。
2.1.4 本题设计的知识点
1.完全二叉树的结构:除了最后一层,其他所有层的结点个数应该都为最大,最后一层结点应该都在最左边且连续,不能在最右边;
2.二叉树的层次遍历:和之前在树中学习一样,运用队列辅助实现;
3.二叉搜索树的插入操作:边查找边插入,算法中的根节点指针要用引用类型,这样才能将改变后的值传回给实参。
2.2 二叉搜索树的最近公共祖先
2.2.1 该题的设计思路
-
由先序遍历序列建立二叉搜索树:由一个先序遍历来建一棵二叉树是不够的,但是我们要建的是二叉搜索树,我们可以知道二叉搜索树的中序遍历是一个递增序列,所以我们可以对先序序列进行从小到大的排序,得到中序序列,再结合我们在树中学习的方法进行建树。但在后续学习中我们发现,如果按某个序列依次往二叉排序树中插入数据,那么这个序列就为该二叉排序树的先序序列,发现这个规律后,我们就可以直接进行插入建树;
-
我们在寻找最近的公共祖先时,要先判断这两个结点是否存在于二叉排序树中,递归查找;
-
若两个结点u,v都在二叉树中,我们自上而下进行遍历,由于二叉排序树的特性,我们可以根据当前遍历到的结点来判断结点u,v的位置,从而判断公共祖先的位置。通过观察我们可以发现,当我们遍历到结点root时:
- u和v分别分布在该结点的左右子树中,那么当前结点root必为u,v的最近公共祖先;
- root为u,v其中一个结点,那么root便为其中另一个节点的祖先;
- 若u,v都在root的左/右子树中,那么他们的最近公共祖先一定在root的左/右子树中;
-
算法时间复杂度为:O(log2n);
2.2.2 该题的伪代码
int main()
{
创建二叉排序树;
for i=0 to m //寻找m对结点的公共祖先;
在二叉排序树中寻找u,v结点:FindNode()函数
若有结点不存在,输出结果;
若都存在,寻找公共祖先,输出结果:Find_ClosestAncestor()函数;
end for
}
BinTree *FindNode(BinTree*root ,int k)
{
若root为NULL或者root->data==k,return root;
若root->data>k ,递归进入左子树查找;
若root->data<k ,递归进入右子树查找;
}
BinTree* Find_ClosestAncestor(BinTree*root ,int u, int v)
{
若root的值都大于两个数据, 说明公共祖先在root的左子树中
递归进入左子树:return Find_ClosestAncestor(root->lchild,u,v);
若root的值都小于两个数据,说明公共祖先在root的右子树中
递归进入右子树:return Find_ClosestAncestor(root->rchidl,u,v);
若root的值处于两个数据之间或者root的值等于其中一个结点的值
说明找到公共祖先,return root;
}
具体代码
2.2.3 PTA提交列表
1.部分正确(18分):我起初用的不是上面的方法,之前学习过在普通二叉树中寻找公共祖先:也是使用递归,不同的是左右子树都要进入,递归结束条件为找到u,v结点,然后返回u,v的指针,如果在某个结点的左右子树中分别返回了u,v指针,那么就返回当前结点为认为是u,v的公共祖先,其他情况一律返回左右子树中不为空的指针,最后得到结果为u,v的公共祖先。这样的做法因为左右子树都要进入,运行效率下降,所以最后两个测试点运行超时。
2.部分正确(22分):这里本来想不写查找函数,直接用map容器来验证u,v是否存在于二叉树中。但是map容器的内部是一颗红黑树,我们在代码上只是短短的呈现出一句代码,就加入了某个结点,但是其内部要经过许多操作插入,还要平衡二叉树,所耗费的时间还是比较多的。
3.部分正确(4分):查找函数写错了,缺少了root为u,v结点的判断。
2.2.4 本题设计的知识点
1.二叉排序树:由于二叉排序树的结构特性,我们可以根据二叉排序树的先序遍历序列中的数据逐个插入建树。
2.map容器:map容器的内部实质上是一颗红黑树,当我们插入一个数据时,其内部不仅要进行插入操作,还要进行对二叉树的平衡操作。
3.pta中所有涉及二叉排序树的题目,我起初开始做的时候都是用比较普遍的方法————用于解决普通二叉树的相关问题,忘记去观察二叉排序树的结构特性,以及其中隐藏的规律,导致整个思路非常复杂,所以在解决相关问题时,还是要结合相关特性,找到更好的解决方法。
2.3 (自建倒排索引表) 基于词频的文件相似度
2.3.1 该题的设计思路
- 每个文件的内容用哈希链保存起来,我是借鉴字典的设计,将内容中的每个单词都按首字母进行保存起来,也就是把单词的首字母在字母表中的位置当成哈希地址。
- 提取内容中的单词:用string作为保存单词的类型,这里要注意题目中说明:大小写不同的单词也被当做是同一个单词,所以在提取单词时,我将所有的小写字母都转换为大写字母,方便后面的对比;
- 计算重复率:去单词数量比较小的文件作为模板,提取模板中的每一个单词去另外一个文件相对应的地址去寻找,如果模板中所有单词都遍历完毕,就提前退出循环,计算重复率。重复率的计算为:相同单词数量/(两个文件总的单词数量-相同单词的数量)。
计算代码的时间复杂度:最坏情况为比较的两个文件中所有单词的首字母都相等,且没有一个单词重复,则时间复杂度为O(n*m),n,m分别为两个文件的单词个数。
2.3.2 该题的伪代码
结构体定义:
int main()
{
定义一个文件列表list保存所有文件;
初始化文件列表;
for i=0 to n
提取每个文件的内容;
end for
for i=0 to m
比较两个文件的单词数量大小,单词数量小的作为模板,计算两文件的重复率;
输出结果;
end for
}
void InserFile(File& F,HashNode* word)//根据单词的首字母插入文件的相对应位置
{
计算该单词应该插入的位置:ard = word->key[0]-'A';
遍历单链F.content[ard]上的单词
若有重复内容 return;
若遍历完毕没有找到重复内容
头插法插入,并使该文件的单词数量+1;
}
void GetFile(FileTable& list, int number)//获取每个文件内容,且将每个小写字母都转换为大写
{
while(1)
若str为'#' ,说明已经到文件内容末尾,break;
while(未遍历到字符串str末尾)
开辟新空间,用于保存新单词word;
while(为字母)
if 单词长度小于10,可以继续加入字母
若为小写字母:word->key += (str[i++]-'a'+'A');
若为大写字母:word->key += str[i++];
else
直接舍弃,i++;
end while
若单词长度小于3,则为不合法单词,舍弃:delete word;
i++;//跳过非字母字符;
end while
end while
}
double File_Similarity(File*file1,File*file2)//计算两文件的重复率
{
定义count保存模板file1的单词个数
定义same记录两文件重复的单词数量,all保存两文件总的单词个数;
for i=0 to 26 //遍历模板的哈希表
p=file1->countent[i].next;//提取其中一个单链表遍历
while(p不为空)
count--;//表示模板中的有一个单词已经遍历过
遍历另一个文件中相对应的位置总寻找是否存在和p相同的单词
若存在 same++;
p=p->next;//遍历单链中下一个单词;
end while
if count==0
表示模板文件中所有单词都遍历完 ,提前退出对模板的遍历,break;
end if
end for
}
具体代码
2.3.3 PTA提交列表
1.部分正确:如果文本中出现重复单词,应该只记录一次,我没有考虑这一点,导致哈希表的单链中存在多个重复单词,计算重复率时出现偏差,后来在插入函数中添加了当前这个单词是否已经在该文本中出现过的判断。还有一个问题就是最大数据时,运行超时,于是我在计算重复率时设置了变量count表示某文件的单词个数,在遍历该文件的哈希链时,计算该文件已经遍历的单词个数,如果所有单词都已经遍历过,但是哈希链还未遍历完时,就提前推出循环。还有一个在进行计算重复率之前,将单词数量少的文件做为模板,提取模板中每个的单词去另外一个文件相应地址中寻找是否有相同的单词,这样可以提高算法效率。
2.如果发现运行时间刚刚好是卡这点通过的,一定要多试几次,有可能是运气好(因为之前在7-5的时候明明是相同的代码,有时可以通过,有时运行超时,结果很不稳定,后来稍微修改了一下才稳定下来)。
2.3.4 本题设计的知识点
1.拉链法构造哈希表:这里运用到了拉链法构造哈希表来保存文件内容,其结构感觉和上一章学习的邻接表很像,也是数组和链表的相结合。
2.头插法插入链表:又接触到了链表的内容,即使到了后面的章节,链表的出场率还是很高,所以对链表的掌握一定要非常熟练。头插法要先修改待插入结点的next指针,保存一下链表中头结点后面的内容,再修改头结点的next指针,使之指向待插入结点;
3.给串添加内容的操作:因为我们用的是串str来吸收文件内容,为了使后续的比较操作更加简便,单词类型设置为string。对结点名称添加内容,这里使用的是+=操作。