0.PTA得分截图
1.本周学习总结
1.1 总结查找内容
1.1.1 查找的性能指标ASL
1.查找的定义:给定一个值k,在含有n个元素的表中找出关键字等于k的元素。若找到,则查找成功,返回该元素的信息或该元素在表中的位置;否则查找失败,返回相关的指示信息。
2.查找有内查找和外查找之分:
- 若整个查找过程都在内存中进行,则称之为内查找。
- 若查找过程的需要访问外存,则称之为外查找。
3.在查找运算中时间主要花费在关键字的比较上,把平均需要和给定值k进行比较的关键字次数称为平均查找长度,简称ASL。
其定义为:
其中,n是查找表中元素的个数。pi是查找第i个元素的概率,通常假设每个元素的查找概率相等,此时pi=1/n(1≤i≤n),ci是找到第i个元素所需的关键字比较次数。
4.ASL分为查找成功情况下的ASL(成功)和查找不成功情况下的ASL(不成功)。
5.ASL是衡量查找算法性能好坏的重要指标。一个查找算法的ASL越大,其时间性能越差;反之,一个查找算法的ASL越小,其时间性能越好。
6.顺序查找中的ASL
- 顺序查找的基本思路是从表的一端向另一端逐个将元素的关键字和给定值k比较,若相等,则查找成功,给出该元素在查找表中的位置;若整个查找表扫描结束后仍未找到关键字等于k的元素,则查找失败。
- 算法如下:
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
}
- 顺序查找方法查找成功的平均比较次数约为表长的一半:
- 当待查找元素不在查找表中时,也就是扫描整个表都没有找到时,即比较了n次,查找失败:
7.折半查找中的ASL
-
折半查找又称二分查找,是一种效率较高的查找方法。但是,折半查找要求线性表是有序表,即表中的元素按关键字有序。
-
基本思路:
- 设R[low...high]是当前的查找区间,首先确定该区间的中点位置mid=⌊(low+ high)/2⌋, 然后将待查的k值与R[mid].key比较:
- (1)若k=R[mid].key,则查找成功并返回该元素的逻辑序号。
- (2)若k<R[mid].key,则由表的有序性可知R[mid...high].key均大于k,因此若表中存在关键字等于k的元素,则该元素必定是在位置mid左边的子表R[low...mid-1]中,故新的查找区间是左子表R[low...mid-1]。
- (3)若k> R[mid].key,则关键字为k的元素必在mid的右子表R[mid+ 1...high]中,即新的查找区间是右子表R[mid+1...high]。
- 下一次查找是针对新的查找区间进行的。
- 设R[low...high]是当前的查找区间,首先确定该区间的中点位置mid=⌊(low+ high)/2⌋, 然后将待查的k值与R[mid].key比较:
-
算法如下:
int BinSearch(RecType R[],int n,KeyType k)
{
int low=0,high=n-1,mid;
while (low<=high)//当前区间存在元素时循环
{
mid=(low+high)/2;
if (R[mid].key==k)//查找成功返回其逻辑序号mid+1
return mid+1;
if (k<R[mid].key)//继续在R[low...mid-1]中查找
high=mid-1;
else
low=mid+1;//继续在R[mid+1...high]中查找
}
return 0;//未找到时返回0(查找失败)
}
-
内部结点都能查找成功:
-
查找不成功的是空的外部结点。
-
举例分析:给11个数据元素有序表(2,3,10,15,20,25,28,29,30,35,40)采用折半查找。则ASL成功和不成功分别是多少?
- 成功时的平均查找长度(圆圈)为:
- 不成功的平均查找长度(方框)为:
1.1.2 动态查找:二叉搜索树
1.二叉搜索树的定义:二叉搜索树,又名二叉查找树、二叉排序树。其定义为二叉排序树或者是空树,或者是满足以下性质的二叉树:
- (1)若它的左子树不空,则左子树上的所有结点的值均小于它的根结点的值。
- (2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
- (3)它的左、右子树叶分别是二叉排序树。
2.二叉排序树的创建
-
二叉排序树的创建是从空的二叉树开始的,每输入一个结点,经过查找操作,将新的结点插入到当前二叉树的合适位置。
-
算法步骤:
- (1)将二叉排序树bt初始化为空树。
- (2)读入一个关键字为a[i]的结点。
- (3)如果读入的关键字a[i]不是输入结束的标志,则循环执行以下操作:
- 将此结点插入二叉排序树bt中。
- 读入一个关键字为a[i]的结点。
-
具体代码:
BSTree CreatBST(KeyType a[],int n)//返回树根指针
{
BSTree bt=NULL;//初始时bt为空树
int i=0;
while (i<n)
{
InsertBST(bt,a[i]);//将a[i]插入二叉排序树T中
i++;
}
return bt;//返回建立的二叉排序树的根指针
}
3.二叉排序树的插入
-
二叉排序树的插入操作是以查找为基础的。要将一个关键字值为k的结点插入到二叉排序树中,则需要从根结点向下查找,当树中不存在关键字值等于k的结点时才进行插入。新插入的结点一定是一个新添加的叶子结点,并且查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
-
算法步骤:
- (1)若二叉排序树为空,则待插入结点*p作为根结点插入到空树中。
- (2)若二叉排序树非空,则将k与根结点的关键字p->key进行比较
- 若k小于p->key,则将值插入到左子树
- 若k大于p->key,则将值插入到右子树
-
具体代码:
int InsertBST(BSTree &p,KeyType k)
{
if (p==NULL)//原树为空
{
p=new BSTNode;
p->key=k;
p->lchild=p->rchild=NULL;
return 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);//插入到右子树
}
4.二叉排序树的查找
-
算法步骤:
- (1)若二叉排序树为空,则查找失败,返回空指针。
- (2)若二叉排序树非空,则将给定值key与根结点的关键字T->data.key进行比较:
- 若key等于T->data.key,则查找成功,返回根结点地址。
- 若key小于T->data.key,则递归查找左子树。
- 若key大于T->data.key,则递归查找右子树。
-
具体代码:
BSTree SearchBST(BSTree 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);//在右子树中递归查找
}
5.二叉排序树的删除
-
被删除的二叉搜索树的结点可能是任何结点,删除结点后,要根据其位置不同修改其双亲结点及相关结点的指针,以保持二叉树的特性。
-
删除操作必须首先进行查找,假设在查找结束时p指向要删除的结点。删除过程分为以下几种情况:
- (1)若p结点是叶子结点,直接删去该结点。这是最简单的删除结点的情况。
- (2)若p结点只有左子树而无右子树。根据二叉排序树的特点,可以直接将其左孩子替代结点p(结点替换)。
- (3)若p结点只有右子树而无左子树。根据二叉排序树的特点,可以直接将其右孩子替代结点p(结点替换)。
- (4)若p结点同时存在左、右子树。根据二叉排序树的特点,可以从其左子树中选择关键字最大的结点r,用结点r的值替代结点p的值(结点值替换),并删除结点r(由于r结点一定是没有右子树的,删除它属于情况(2)),其原理是用中序前驱替代被删结点。也可以从其右子树中选择关键字最小的结点r,用结点r的值替代结点p的值(结点值替换),而且将它删除(由于r结点一定是没有左子树的,删除它属于情况(3)),其原理是用中序后继替代被删结点。
-
具体代码:
int DeleteBST(BSTree &bt,KeyType k)//在bt删除关键字为k的结点
{
if (bt==NULL) return 0;//空树删除失败
else
{
if (k<bt->key)
return DeleteBST(bt->lchild,k);//递归在左子树中删除为k的结点
else if (k>bt->key)
return DeleteBST(bt->rchild,k);//递归在右子树中删除为k的结点
else//相等
{
Delete(bt);//调用Delete(bt)函数删除bt结点
return 1;
}
}
}
void Delete(BSTree &p)//从二叉排序树中删除p结点
{
BSTree q;
if (p->rchild==NULL)//结点p没有右子树(含为叶子结点)的情况
{
q=p;
p=p->lchild;//用其左孩子结点替换它
free(q);
}
else if (p->lchild==NULL)//p结点没有左子树的情况
{
q=p;
p=p->rchild;//用其右孩子结点替换它
free(q);
}
else//p结点既有左子树又有右子树的情况
Delete1(p,p->lchild);
}
void Delete1(BSTree p,BSTree &r)//被删结点p有左、右子树,r指向其左孩子
{
BSTree q;
if (r->rchild!=NULL)//递归找r的最右下结点
Delete1(p,r->rchild);
else//r指向最右下结点(它没有右子树)
{
p->key=r->key;//将结点r的值存放到结点p中(结点值替代)
p->data=r->data;
q=r;//删除结点r
r=r->lchild;//即用结点r的左孩子替代它
free(q);//释放结点q的空间
}
}
1.1.3 AVL树
1.定义:一棵二叉树,它的左子树和右子树都是AVL树,且左右子树的高度之差的绝对值不超过1,则称此二叉树为平衡二叉树(AVL)。
2.在算法中,通过平衡因子来具体实现上述平衡二叉树的定义。一个结点的平衡因子是该结点左子树的高度减去右子树的高度(或者该结点右子树的高度减去左子树的高度)。平衡因子的取值为1、0或-1。
3.失衡调整:
-
(1)LL型调整
- 由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下图是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。
- LL型调整的一般形式如下图所示,表示在A的左孩子B的左子树BL(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡(h表示子树的深度)。这种情况调整如下:
- ①将A的左孩子B提升为新的根结点;
- ②将原来的根结点A降为B的右孩子;
- ③各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。
- 由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下图是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。
-
(2)RR型调整
- 由于在A的右孩子(R)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。
- RR型调整的一般形式如下图所示,表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡(h表示子树的深度)。这种情况调整如下:
- ①将A的右孩子B提升为新的根结点;
- ②将原来的根结点A降为B的左孩子;
- ③各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。
- 由于在A的右孩子(R)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。
-
(3)LR型调整
- 由于在A的左孩子(L)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。下图是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
- LR型调整的一般形式如下图所示,表示在A的左孩子B的右子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡(h表示子树的深度)。这种情况调整如下:
- ①将C的右孩子B提升为新的根结点;
- ②将原来的根结点A降为C的右孩子;
- ③各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。
- 由于在A的左孩子(L)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。下图是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
-
(4)RL型调整
- 由于在A的右孩子(R)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
- RL型调整的一般形式如下图所示,表示在A的右孩子B的左子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡(h表示子树的深度)。这种情况调整如下:
- ①将C的右孩子B提升为新的根结点;
- ②将原来的根结点A降为C的左孩子;
- ③各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。
- 由于在A的右孩子(R)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
-
时间复杂度为O(log2n)。
1.1.4 B-树和B+树
1.B-树
-
B-树是一种多路搜索树(并不一定是二叉的),B-树,即为B树。
-
一个 m 阶的B树满足以下条件:
-
每个结点至多拥有m棵子树;
-
根结点至少拥有两颗子树(存在子树的情况下),根结点至少有一个关键字;
-
除了根结点以外,其余每个分支结点至少拥有 m/2 棵子树;
-
所有的叶结点都在同一层上,B树的叶子结点可以看成是一种外部节点,不包含任何信息;
-
有 k 棵子树的分支结点则存在 k-1 个关键码,关键码按照递增次序进行排列;
-
除根结点以外的所有结点的关键字个数n需要满足⌈m/2⌉-1 <= n <= m-1;
-
-
B-树的插入:
-
通过搜索找到对应的结点进行插入,那么根据即将插入的结点的数量可以分为下面几种情况:
-
如果该结点的关键字个数没有到达到m-1个,那么直接插入即可;
-
如果该结点的关键字个数已经到达了m-1个,那么根据B树的性质显然无法满足,需要将其进行分裂。分裂的规则是该结点分成两半,将中间的关键字进行提升,加入到父亲结点中,但是这又可能存在父亲结点也满员的情况,则不得不向上进行回溯,甚至是要对根结点进行分裂,那么整棵树都加了一层。
-
-
- B-树的删除:
-
同样的,我们需要先通过搜索找到相应的值,存在则进行删除,需要考虑删除以后的情况:
-
如果该结点拥有的关键字数量仍然满足B树性质,则不做任何处理;
-
如果该结点在删除关键字以后不满足B树的性质(关键字没有到达⌈m/2⌉-1的数量),则需要向兄弟结点借关键字,这又分为兄弟结点的关键字数量是否足够的情况。
-
如果兄弟结点的关键字足够借给该结点,则将父亲结点的关键字下移,兄弟结点的关键字上移;
-
如果兄弟结点的关键字在借出去以后也无法满足B树性质,即之前兄弟结点的关键字的数量为⌈m/2⌉-1,借的一方的关键字数量为⌈m/2⌉-2的情况,那么我们可以将该结点合并到兄弟结点中,合并之后的子结点数量少了一个,则需要将父亲结点的关键字下放,如果父亲结点不满足性质,则向上回溯。
-
-
-
- B+树
- B+树的定义:B+树是B-树的变体,也是一种多路搜索树。其定义基本与B-树相同。
- B+树的特点:
- 非叶子结点的子树指针与关键字个数相同;
- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
- 为所有叶子结点增加一个链指针;
- 所有关键字都在叶子结点出现。
- B树和B+树的区别
-
由于B+树和B树具有不同的存储结构所造成的区别,以m阶树为例:
-
关键字的数量不同;B+树中分支结点有m个关键字,其叶子结点也有m个,其关键字只是起到了一个索引的作用,虽然B树也有m个子结点,但是其只拥有m-1个关键字。
-
存储的位置不同;B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。
-
分支结点的构造不同;B+树的分支结点存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。
-
查询不同;B树在找到具体的数值以后就结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径,其高度是相同的,相对来说更加的稳定;
-
区间访问:B+树的叶子结点会按照顺序建立起链状指针,可以进行区间访问。
-
-
1.1.5 散列查找
1.散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。建立了关键字与存储位置的映射关系,公式为:存储位置 = f(关键字)
,这里把这种对应关系f称为散列函数,又称为哈希(Hash)函数。
2.采用散列技术将记录存在在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么,关键字对应的记录存储位置称为散列地址。
3.散列技术既是一种存储方法也是一种查找方法。散列技术的记录之间不存在什么逻辑关系,它只与关键字有关,因此,散列主要是面向查找的存储结构。
4.哈希表,又称散列表,是除顺序表存储结构、链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。
5.哈希冲突:对于两个关键字分别为ki和kj(i≠j)的记录,有ki≠kj,但h(ki)=h(kj)。把这种现象叫做哈希冲突(同义词冲突)。
6.哈希表构造方法:
-
直接定址法
-
所谓直接定址法就是说,取关键字的某个线性函数值为散列地址,即H(key)=key或H(key )=a×key +b(a,b为常数)
-
优点:简单、均匀,也不会产生冲突。
-
缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。
-
除留余数法
-
此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:h(k) =k mod p (mod为求余运算,p ≤ m)
-
mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可以再折叠、平方取中后再取模。
-
很显然,本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。
-
根据前辈们的经验,若散列表的表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
-
-
数字分析法
-
适合于所有关键字值都已知的情况,并需要对关键字中每一位的取值分布情况进行分析。
-
数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。
-
-
处理散列冲突的方法
- 在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。市场会碰到两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象称为冲突。出现冲突将会造成查找错误,因此可以通过精心设计散列函数让冲突尽可能的少,但是不能完全避免。
- 开放定址法
- 所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
- 链地址法
- 将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。
- 链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。
- 公共溢出区法
- 为所有冲突的关键字建立一个公共的溢出区来存放,重新找个地址。
- 在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
-
用几种不同的方法解决冲突时哈希表的平均查找长度
-
Hash表的“查找成功的ASL”和“查找不成功的ASL”(ASL指的是平均查找时间)
-
关键字序列:(7、8、30、11、18、9、14)
-
散列函数: H(Key) = (key x 3) MOD 7
-
装载因子: 0.7
-
处理冲突:线性探测再散列法
-
查找成功的ASL计算方法:
因为现在的数据是7个,填充因子是0.7。所以数组大小=7/0.7=10,即写出来的散列表大小为10,下标从0~9。
-
第一个元素7,带入散列函数,计算得0。
第二个元素8,带入散列函数,计算得3。
第三个元素30,带入散列函数,计算得6。
第四个元素11,带入散列函数,计算得5。
第五个元素18,带入散列函数,计算得5;此时和11冲突,使用线性探测法,得7。
第六个元素9,带入散列函数,计算得6;此时和30冲突,使用线性探测法,得8。
第七个元素14,带入散列函数,计算得0;此时和7冲突,使用线性探测法,得1。
所以散列表为:
❀所以查找成功的计算:
如果查找7,则需要查找1次。
如果查找8,则需要查找1次。
如果查找30,则需要查找1次。
如果查找11,则需要查找1次。
如果查找18,则需要查找3次:第一次查找地址5,第二次查找地址6,第三次查找地址7,查找成功。
如果查找9,则需要查找3次:第一次查找地址6,第二次查找地址7,第三次查找地址8,查找成功。
如果查找地址14,则需要查找2次:第一次查找地址0,第二次查找地址1,查找成功。
所以,查找成功ASL=(1+2+1+1+1+3+3)/ 7 = 12/7
❀在已知上面散列表的基础上,如果要查找key为4的关键字。根据散列函数可以计算Hash(key)=Hash(4)=5。此时在地址为5的地方取出那个数字,发现key=11,不等于4。这就说明在装填的时候会发生冲突。根据冲突处理方法,会继续检测地址为6的值,发现key=30,依然不等。这个时候到了地址为6,但是依然没有找到。那么就说明根本就没有key=4这个关键字,说明本次查找不成功。
所以查找不成功的计算:
查找地址为0的值所需要的次数为3,
查找地址为1的值所需要的次数为2,
查找地址为2的值所需要的次数为1,
查找地址为3的值所需要的次数为2,
查找地址为4的值所需要的次数为1,
查找地址为5的值所需要的次数为5,
查找地址为6的值所需要的次数为4。
所以,查找不成功ASL=(3+2+1+2+1+5+4)/ 7 = 18/7
- 哈希链
- 哈希链是一种顺序和链式相结合的存储结构。
- 哈希链的创建
void CreateHashChain(HashChain HC[], int n, int k)
{
int i;
for (i = 0;i < HashSize;i++)
{
HC[i] = new HashNode;
HC[i]->next = NULL;
}
for (i = 0;i < n;i++)
Insert(HC, k);
}
- 哈希链中数据的插入
void Insert(HashChain HC[], int k)
{
string ID;
int distance;
int index;
HashChain node;
HashChain p;
ID.resize(MAXSIZE);
scanf("%s%d", &ID[0], &distance);
if (distance < k) distance = k;
index = GetID(ID) % HashSize;
p = HC[index]->next;
while (p != NULL)
{
if (p->ID == ID) break;
else p = p->next;
}
if (p == NULL)
{
node = new HashNode;
node->ID = ID;
node->flight_distance = distance;
node->next = HC[index]->next;
HC[index]->next = node;
}
else p->flight_distance += distance;
}
- 哈希链的查找
void Search(HashChain HC[], string ID)
{
int index;
HashChain p;
index = GetID(ID) % HashSize;
p = HC[index]->next;
while (p != NULL)
{
if (p->ID == ID)
{
printf("%d", p->flight_distance);
return;
}
p = p->next;
}
printf("No Info");
}
1.2 谈谈你对查找的认识及学习体会
- 查找是我们在生活中很常见的一种行为,所以它的概念我们是很清楚的。但是有些概念我们还不是很清楚,就比如平衡二叉树的失衡调整,对于LL或RR比较清楚,但是对于LR或者RL就很容易弄错,即使借用画图的方法也理不清思路。
- 而且这一章节的小细节很多,容易让人混淆,就比如说关于各种查找方式对应的查找效率ASL的计算,查找的方法有很多,但是它们的计算方法不大相同,很容易弄混淆,要搞清楚这些计算首我们还先得理清楚这些查找的原理,然后再记忆其ASL的计算方式。
- 对于这一部分的内容好像对于代码要求不高,但是涉及的内容更广更全面了。在写这部分的PTA的时候,我们往往需要用到STL容器,比如有map、set等等,但是这部分老师一般是提一下,还需要我们自己去网上找相关的知识,多多储备知识。
2.PTA题目介绍
2.1 7-1 是否完全二叉搜索树
- 题目
将一系列给定数字顺序插入一个初始为空的二叉搜索树(定义为左子树键值大,右子树键值小),你需要判断最后的树是否一棵完全二叉树,并且给出其层序遍历的结果。
输入格式:
输入第一行给出一个不超过20的正整数N;第二行给出N个互不相同的正整数,其间以空格分隔。
输出格式:
将输入的N个正整数顺序插入一个初始为空的二叉搜索树。在第一行中输出结果树的层序遍历结果,数字间以1个空格分隔,行的首尾不得有多余空格。第二行输出YES,如果该树是完全二叉树;否则输出NO。
输入样例1:
9
38 45 42 24 58 30 67 12 51
输出样例1:
38 45 24 58 42 30 12 67 51
YES
输入样例2:
8
38 24 12 45 58 67 42 51
输出样例2:
38 45 24 58 42 12 67 51
NO
- 代码
2.1.1 该题的设计思路
-
题面分析
- 首先这道题先要输入N,代表接下来要将N个数构建一棵二叉搜索树;然后接下来就是输入这N个数。
- 在构建二叉搜索树时,需要注意的是题干中的“定义为左子树键值大,右子树键值小”,也就是比父结点大的数在左孩子处,比父结点小的数在右孩子处,和我们平时构建的不一样;构建时还需要按照给定数字顺序来构建。
- 在输出时,需要层次遍历二叉搜索树,然后将遍历的结果输出,注意空格问题;当该二叉搜索树是完全二叉树(若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k层所有的结点都连续集中在最左边,这就是完全二叉树)时,输出“YES”;其他情况输出“NO”。
-
图文介绍
(1)完全二叉树:若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k层所有的结点都连续集中在最左边,这就是完全二叉树。
(2)样例1构建的二叉搜索树
层次遍历结果:38 45 24 58 42 30 12 67 51,符合完全二叉树,输出YES
(3)样例2构建的二叉搜索树
层次遍历结果:38 45 24 58 42 12 67 51,第三层的12没有兄弟,但是接下来还有第四层,所以不符合完全二叉树,输出NO
- 复杂度分析
- 建立二叉搜索树的时间复杂度为O(log2n),层次遍历二叉树的时间复杂度为O(n),所以总的时间复杂度为O(log2n)
2.1.2 该题的伪代码
tree[100]存储各结点的值
num为结点的数值
N为输入的数目
int main()
{
i为遍历序号
j记录结点的总数量
flag记录输出的个数
输入一个不超过20的正整数N
初始化树的所有结点为-1
for (i = 0 to i < N,i++)//输入N个互不相同的正整数
{
输入正整数num
调用函数AddTree()加入到二叉搜索树中,从1开始才能符合后面的结点间的2*i和2*i+1的关系
}
while (flag 小于 N)
{
while (tree[j]为 - 1)
{
该结点没有存入的数值,去下一个结点
}
if (flag不是0)
{
输出tree[j],且前面需要输出空格
}
else
{
直接输出tree[j]
}
j++,flag++
}
if (j 等于 N + 1)
{
换行输出YES
}
else
{
换行输出NO
}
}
void AddTree(int i, int d)
{
if (tree[i] 等于 - 1)
{
将输入的数字存入该结点
返回
}
if (d 大于 该结点tree[i])
{
函数递归找其左孩子继续判断,左子树与根节点位置的关系是2* i
}
else
{
函数递归找其右孩子继续判断,右子树与根节点位置关系是2* i + 1
}
}
2.1.3 PTA提交列表
- memset头文件应该是string.h,记成stdlib.h了,所以导致了编译错误
- 最后输出YES或者NO时的判断由于j++是放在输出后面的,所以应该是j等于N+1,不是j等于N
2.1.4 本题涉及的知识点
- memset函数:
- memset函数是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值,这个函数通常为新申请的内存做初始化工作。
- 本题我所使用的是memset()的深刻内涵是:用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化。例如:memset(a,'\0',sizeof(a));
- memset函数在头文件:#include<string.h>中
- 运用了创建二叉树时左子树与根节点位置的关系是
2*i
、右子树与根节点位置关系是2*i+1
的关系 - 判断是否是完全二叉树时通过j将遍历过的结点的数量记录下来,然后进行判断,且层次遍历直接按照下标顺序来判断即可
2.2 7-2 二叉搜索树的最近公共祖先
- 题目
给定一棵二叉搜索树的先序遍历序列,要求你找出任意两结点的最近公共祖先结点(简称 LCA)。
输入格式:
输入的第一行给出两个正整数:待查询的结点对数 M(≤ 1000)和二叉搜索树中结点个数 N(≤ 10000)。随后一行给出 N 个不同的整数,为二叉搜索树的先序遍历序列。最后 M 行,每行给出一对整数键值 U 和 V。所有键值都在整型int范围内。
输出格式:
对每一对给定的 U 和 V,如果找到 A 是它们的最近公共祖先结点的键值,则在一行中输出 LCA of U and V is A.。但如果 U 和 V 中的一个结点是另一个结点的祖先,则在一行中输出 X is an ancestor of Y.,其中 X 是那个祖先结点的键值,Y 是另一个键值。如果 二叉搜索树中找不到以 U 或 V 为键值的结点,则输出 ERROR: U is not found. 或者 ERROR: V is not found.,或者 ERROR: U and V are not found.。
输入样例:
6 8
6 3 1 2 5 4 8 7
2 5
8 7
1 9
12 -3
0 8
99 99
输出样例:
LCA of 2 and 5 is 3.
8 is an ancestor of 7.
ERROR: 9 is not found.
ERROR: 12 and -3 are not found.
ERROR: 0 is not found.
ERROR: 99 and 99 are not found.
- 代码
2.2.1 该题的设计思路
-
题面分析
- 题目先给出待查询的结点对数M,以及二叉搜索树中结点个数N;然后给出这N个结点的先序序列,所以我们要先根据该序列建立二叉搜索树;然后题目M对整数键值U和V,想要找这两个结点的最近公共祖先结点。
- 在这个过程中分为几种情况:
- 一是未找到U和V这两个结点,而其中又可以分为三种情况:①是二叉搜索树中找不到以 U 和 V 为键值的结点,输出
ERROR: U and V are not found.
②是二叉搜索树中找不到以 U 为键值的结点,输出ERROR: U is not found.
③是二叉搜索树中找不到以 V 为键值的结点,输出ERROR: V is not found.
; - 二是如果U和V中的一个结点是另一个结点的祖先,比如U是V的祖先或者V是U的祖先,那么就输出
X is an ancestor of Y.
,其中 X 是那个祖先结点的键值,Y 是另一个键值; - 三就是皆大欢喜,找到U和V的最近公共祖先结点的键值,那么就直接输出
LCA of U and V is A.
。
- 一是未找到U和V这两个结点,而其中又可以分为三种情况:①是二叉搜索树中找不到以 U 和 V 为键值的结点,输出
-
图文介绍
- 根据题目所给的先序遍历的顺序建立二叉搜索树。
- 根据题目所给的先序遍历的顺序建立二叉搜索树。
-
复杂度分析
- 前序创建二叉树,时间复杂度为O(n)。
2.2.2 该题的伪代码
int main()
{
待查询的结点对数 M(≤ 1000)
二叉搜索树中结点个数 N(≤ 10000)
给定的 U 和 V,找到它们的最近公共祖先结点
根结点root
x,y分别记录U是否是V的祖先和V是否是U的祖先
输入的第一行先结点对数M,后二叉搜索树中的结点个数N
for (i = 1 to i <= N,i++)
{
给出 N 个不同的整数
将其标记在树上
}
先序创建二叉树
for (i = 1 to i <= M,i++)
{
给出整数键值 U 和 V
if (二叉搜索树中找不到以 U 和 V 为键值的结点)
{
输出:ERROR: Uand V are not found.
}
else if (找不到以 U 为键值的结点)
{
输出:ERROR: U is not found.
}
else if (找不到以 V 为键值的结点)
{
输出:ERROR: V is not found.
}
else
{
Find_F()函数判断U是否是V的祖先和V是否是U的祖先
if (U是V的祖先)
{
输出:U is an ancestor of V.
}
else if (V是U的祖先)
{
输出:V is an ancestor of U.
}
else
{
Find_POS()函数找U的根结点
Find_LCA()函数从V向上找根结点,判断是否有与U的根结点相同的
输出:LCA of U and V is A.
}
}
}
}
node* create(int lchild, int rchild)//建立二叉搜索树
{
根结点root
if (lchild 大于 rchild)
{
返回 NULL
}
递归创建左孩子
if (判断root->lchild是否为空)
{
标记为root->lchild的father节点
}
递归创建右孩子
if (判断root->rchild是否为空)
{
标记为root->rchild的father节点
}
返回root值
}
int Find(node* root, int x)//在以root为根的子树中是否查找到x
{
if (root存在)
{
if (找到x)
{
返回1
}
if (x小于该结点)
{
Find()函数递归左孩子
}
else
{
x大于该结点,Find()函数递归右孩子
}
}
}
int Find_F(node* root, int u, int v)//u是否是v的祖先
{
if (root存在)
{
if (root->data 等于 u)
{
if (在u的子树中找到了v)
说明u是v的祖先,返回1
否则,返回0
}
if (u 小于 root->data)
Find_F()左孩子继续递归判断
else
Find_F()右孩子继续递归判断
}
返回0
}
2.2.3 PTA提交列表
- 这道题的情况比较多,刚开始的时候没有注意到U和V都不存在的情况,只以为有一个不存在,审题不仔细。
- 后来关于U是V的祖先或者V是U的祖先这一部分,我的思路是判断一个数的祖先,如果这个数的祖先里有等于另一个数的话,就可以返回的,但是我这个代码的外面没有判断root已经没有了这个判断条件,导致了程序的一直运行,一直卡着。最后加了if(root)的判断,才可以结束。
2.2.4 本题涉及的知识点
- 二叉搜索树
- 这道题运用到了二叉搜索树的一个性质,即:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
- 根据先序序列创建二叉树
- map容器的使用
2.3 7-5(哈希链) 航空公司VIP客户查询
- 题目
不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
输入格式:
输入首先给出两个正整数N(≤10^5)和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:18位身份证号码(空格)飞行里程。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤10^5),随后给出M行查询人的身份证号码。
输出格式:
对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出No Info。每个查询结果占一行。
输入样例:
4 500
330106199010080419 499
110108198403100012 15000
120104195510156021 800
330106199010080419 1
4
120104195510156021
110108198403100012
330106199010080419
33010619901008041x
输出样例:
800
15000
1000
No Info
- 代码
2.3.1 该题的设计思路
-
题面分析
- 首先理解题目数据:
- N表示的是接下来有N条飞行记录,但是这N条飞行记录的持有者都是会员。
- K表示的是会员的飞行里程的最低要求,其中需要注意的是低于K公里的航班也按K公里累积,即:比如规定最低里程K为20时,当某客户飞行里程为5,按照20算;飞行里程为20时,即为20;飞行里程超过20时,就按实际飞行里程计算。
- M表示的是要查询M个人的里程累积值,但是前提是这个人是会员,也就是前面所给的N条飞行记录里是否有这个人的身份证号码;如果他不是会员,即飞行记录里没有他,就输出No Info;格式上要注意每个查询结果占一行。
- 然后分析所给示例:
- 题目中首先给了4条飞行记录,以及最低飞行里程要求为500公里。
- 第一个和第二个要查询的人都是会员,且飞行里程都超过了500,所以输出其实际的飞行里程就可以。
- 而第三个要查询的人,4条记录中的第一条和第四条是这个人的,且都没有达到最低要求500,所以相当于是两个500,也就是身份证号为330106199010080419的这个人相当于累计了1000公里的飞行里程,所以输出的是1000。
- 第四个人,在前面的飞行记录中没有他的身份证号码,也就是说他不是会员,所以要No Info。
- 首先理解题目数据:
-
解题思路
- 因为身份证号码有18位,所以要使用字符串来记录身份证号码,减少代码运行的时间。
- 利用map容器可以将身份证号码与其对应的里程数匹配,查找时更加方便。
- 未满足最低飞行里程要求,即≤K时,对应人的飞行里程直接加K值。
-
复杂度分析
- 查询时需要遍历存储的会员,所以时间复杂度为O(n)。
2.3.2 该题的伪代码
N为某航空公司全体会员的飞行记录,N(≤10^5)
K是最低里程,K(≤500),航程低于K公里的航班也按K公里累积
M是查询人数,M(≤10^5)
road为每条记录对应的飞行里程
字符串str存储18位的身份证号码
map容器mp,表示关键字为身份证号码str,搜索指向int里程
循环变量i初始化为0
输入正整数N和K
for (i = 0 to i < N,i++)
{
输入身份证号码及其飞行里程
if (达不到最低飞行里程K)
{
road变为K
}
mp对应身份证号码的int型叠加其飞行里程
}
输入要查询的人数M
for (i = 0 to i < M,i++)
{
输入要查询的身份证号码
if (mp[str]为1)
找到,输出里程数
else
未找到,不是会员,输出"No Info"
}
2.3.3 PTA提交列表
- 编译错误:编译错误的主要原因还是由于使用了一些平时不太常用的函数,比如map等等,使用头文件不熟练导致的错误。
- 部分正确:部分答案正确主要是后两个测试点运行超时了,不过答案是正确的,所以我减少了循环的嵌套,最近感觉好多题我做出来都是运行超时,绝大多数都不知道要怎么减少时间,不清楚哪里比较耗时,我还需要多观察总结。
2.3.4 本题涉及的知识点
- 使用了map函数
- map是c++的一个标准容器,提供了一对一的关系,在本题中可以很方便地将身份证号码与飞行里程对应起来。
- 本题我使用的是类似于map<int, string> personnel; 这样就定义了一个用int作为索引,并拥有相关联的指向string的指针。
- PS:【C++map函数的用法】 [https://blog.csdn.net/YC1101/article/details/79311029]
- 运用到了哈希链
- 哈希链,又名散列链,是一个双向链表。
- 具体方法是由用户选择一个随机数,然后对其进行多次散列运算,把每次散列运算的结果组成一个序列。