GESP C++ 五级真题(2024年12月)题解
解释:答案选C。
选项A:数组需要预先分配一块连续的内存空间,当数据数量不确定时,为了能存储足够的数据,可能需要申请一个较大的数组,这样就可能会浪费空间。而链表是动态数据结构,其大小可以根据需要动态调整,所以当数据数量不确定时,链表比较合适,选项 A 正确。
选项B:在链表中访问节点,因为没有像数组那样的索引,需要从表头开始逐个节点遍历,平均时间复杂度为为O(n)。选项 B 正确。
选项C:链表插入和删除元素(在已知位置插入或删除)的时间复杂度为O(1)(如果是在给定值的位置插入或删除,需要先查找该位置,查找的时间复杂度为O(n)。与之对应的,数组插入和删除元素(中间位置)通常需要移动大量元素,时间复杂度为O(n),所以选项 C 错误。
选项D:链表的节点在内存中是分散存储的,各个节点之间通过指针连接在一起,选项 D 正确。
2.解释:答案选C。
在循环单链表中,每个节点的 next
指针指向下一个节点,而最后一个节点的 next
指针则指向第一个节点,从而形成一个循环。因此,正确答案是C。
3.解释:答案选A。
我们知道删除中间节点和尾节点代码可以统一为: cur->next = cur->next->next;
。要想删除头节点也使用同样的代码,只需要在本来的头节点之前再插入一个节点作为新的头节点,让 cur
指向新的头节点。也就是第11、12、13行代码的作用,选项A的代码是13行应该填充的代码。
需要注意的是,在完成删除造作后,需要使head指向真正的头节点,也就是将创建的虚拟头节点删除,也就是第26、27、28行的代码所起的作用。
解释:答案选D。
选项A,正确。两个函数都实现了计算斐波那契数列的功能。
选项B,正确。 fibA{}
采用的是递推方式。
选项C,正确。 fibB{}
采用的是递归方式。
选项D,错误。 fibA{}
的时间复杂度为O(n),而 fibB{}
的时间复杂度可以简单的近似为O(2n),精确表达式见下图;但绝对不是选项中的O(n2)。
解释:答案选C。
调用函数 gcd(24,36)
, a
为 24, b
为 36, big
为 36, small
为 24, if(big%small == 0)
不成立,进一步调用函数 gcd(small, big%small)
,也即 gcd(24, 12)
。
调用函数 gcd(24,12)
, a
为 24, b
为 12, big
为 24, small
为 12, if(big%small == 0)
成立,返回 small
,也即 12。
6.解释:答案选D。
选项A、B、C、D,都可得到正确答案,但本题的要求是填写最佳代码。
为了找到所有的质因数,首先处理 2 这个特殊的质数,然后从 3 开始检查奇数。因为偶数已经被处理,所以可以跳过偶数,只检查奇数,检查到 i * i <= n
即可。因此,选项B、D优于选项A、C。
再来比较选项B、D。选项D的循环步长为 2 ,也就是只考虑了奇数情况,因为除去 2 以外的偶数肯定不是质数,选项D的效率要比选项B高。答案选D。
7.解释:答案选B。
埃氏筛的基本思想是标记法。题干代码的思路如下——
假设要找出不超过 n 的所有素数,首先创建一个从 2 到 n 的连续整数序列。首先,从最小的素数 2 开始,将的 2 倍数(除了 2 本身)标记为合数,即这些数不是素数。接着,找到下一个未被标记的数,这个数一定是素数(因为比它小的数都已经筛选过了),然后将这个素数的倍数标记为合数。如此反复,直到遍历完所有小于等于 √n(根号n) 的数。
选项A:埃筛的时间复杂度为 O(nloglogn) ,具体求法可以参考本公众号里的文章,素数的筛法专题。
选项B:素数是从小往大找的,因此当 i 是素数时,其 i-1 , i-2 ……的倍数已经被比 i 小的质数标记过了,从 i*i 开始可减少重复标记。选项B正确。
选项C、D:埃氏筛是为了找出所有的素数,素数(也称为质数)是指在大于 1 的自然数中,除了 1 和它自身外,不能被其他自然数整除的数。素数除了 2 以外都是奇数,但奇数不都是素数,例如,奇数 9 就不是素数,调用函数 sieve_Eratosthenes(10)
,返回值应包含 2,3,5,7 。因此,选项C、D都不对。
8.解释:答案选A。
线性筛,或称欧拉筛,其核心思想是每个合数只被它的最小质因数筛去。线性筛使用一个数组来标记数是否为素数,同时维护一个素数列表。从 2 开始遍历到 n,如果当前数是素数就加入素数列表。对于每个数 i,遍历已经找到的素数列表,将与素数相乘得到的数标记为合数。关键在于,当能被当前素数整除时,就停止标记,因为后面的合数会由更大的数和更小的质因数去筛掉。
选项A,线性筛的时间复杂度为O(n),这是因为每个数最多只被筛选一次,所以总的时间复杂度是线性的,故正确。
选项B,线性筛的每个合数只应被它的最小质因数筛去,故错误。
选项C,线性筛法和埃拉托色尼筛法的实现思路不同,前者是通过最小质因子标记合数,而后者是通过所有质数的倍数标记合数,故错误。
选项D,错误。
9.解释:答案选A。
快速排序是一种基于分治策略的高效排序算法。它的基本思想是选择一个基准元素(pivot),将数组分为两部分:小于等于基准元素的部分和大于基准元素的部分。然后对这两部分分别进行快速排序,直到整个数组有序。
选项A,快速排序每次划分都会得到一个更小规模的问题,更小的问题用同样的策略解决,使用递归解决,故正确。
选项B、D,速排序在最好情况和平均情况下,时间复杂度为O(nlogn),最坏情况(数组有序时)下,时间复杂度为O(n2),故错误。
选项C,快速排序是一种不稳定的排序算法,故错误。
10.解释:答案选B。
归并排序是一种基于分治策略的排序算法。它的基本思想是将一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并成一个有序的数组。这个过程是递归进行的,直到子数组的长度为 1,此时数组自然就是有序的。
选项A,归并排序是一种稳定的排序算法,故错误。
选项B,归并排序在最优、最差和平均情况下的时间复杂度都为O(nlogn),故正确。
选项C,归并排序不是原地排序算法,因为在合并过程中需要创建一个大小与这两个子数组长度之和相等的临时数组;在最坏情况下,也就是整个数组的长度为时,需要一个大小为 n 的临时数组,空间复杂度为O(n),故错误。
选项D,归并排序输出结果应该是有序的,故错误。
11.解释:答案选C。
二分算法/二分查找,是一种用于在有序数组中查找特定元素的高效算法。它的基本思想是每次比较中间元素与目标元素,如果中间元素等于目标元素,则查找成功;如果中间元素大于目标元素,则在数组的左半部分继续查找;如果中间元素小于目标元素,则在数组的右半部分继续查找。这个过程不断重复,直到找到目标元素或者确定目标元素不存在。
选项A,正确,函数采用二分查找,每次计算搜索当前搜索区间的中点,然后根据中点的元素值排除一半搜索区间。
选项B,正确,函数采用递归求解,每次问题的规模减小一半。
选项C,错误,若数组中不包含该元素,则递归终止条件 if(left > right)
成立,递归会终止并返回 -1 。
选项B,正确,代码求解问题的规模每次递归都减少一半,算法时间复杂度为 O(logn)。
12.解释:答案选B。
代码思路:利用二分查找查找某个数值在数组中第一次出现的下标,找不到返回 -1。循环中,如果条件 target <= nums[middle]
成立,说明目标值可能在左半部分(包括 middle
),因此需要更新 right
。否则,目标值在右半部分,需要更新 left
。为子确保找到左边界,当条件 target <= nums[middle]
成立,应将 right
更新为 middle
而不是 middle-1
,以防止跳过潜在的左边界位置。
选项A,错误, right=middle-1
,可能会错过左边界(当左边界恰好是
middle
)。
选项B,正确,当条件 target <= nums[middle]
成立时,说明左边界在 left
到 middle
这个区间内,为了缩小搜索范围,需要将 right
更新为 middle
,这样下一次循环就会在更小的区间内继续查找左边界。
选项C,错误, right=middle+1
,会导致搜索区间错误,有可能永远找不到左边界。
选项D,错误。
13.解释:答案选A。
贪心算法是一种在每一步选择中都采取当前状态下的最优决策(局部最优解),希望以此来获得全局最优解的算法策略。它并不考虑整体的最优解,而是通过做出一系列贪心选择来逐步构建最终的解决方案。
每个孩子最多只能给一块饼干,饼干的尺寸大于等于孩子的胃口时,孩子才能得到满足。且小杨的目标是尽可能满足越多数量的孩子。思考贪心策略:考虑从饼干尺寸最大的开始匹配孩子胃口值(当然,从孩子胃口值最小的开始匹配饼干尺寸也是一种可行的思路,这里以从大饼干开始举例)。之所以选择从大饼干开始,是因为大饼干更有可能满足胃口较大的孩子,先使用大饼干去满足相对大胃口的孩子,能使得在后续匹配中,剩余的小饼干有更多机会去满足那些胃口较小的孩子,从而有望满足更多数量的孩子,符合尽可能满足更多孩子这一目标。
那么,小杨首先需要把饼干、孩子胃口进行排序,第2、3行代码实现了该功能,进行了升序排序。因此后续对饼干和孩子胃口的数组要从后往前遍历。
第8行:如果下标为 index 的饼干可以满足下标为 i 的孩子,则用来记录能满足的孩子数目的变量 result 自增 1,同时 index 自减 1。
选项A的代码,符合题目要求。
14.解释:答案选D。
分治算法(Divide - and - Conquer),是一种基于递归思想的算法策略。它的基本思想是将一个复杂的问题分解为若干个规模较小、相互独立且与原问题形式相同的子问题,然后分别求解这些子问题,最后将子问题的解合并起来得到原问题的解。归并排序和快速排序都采用了分治思想。
冒泡排序(Bubble Sort),是一种简单的排序算法。冒泡排序不采用分治思想,它的基本思想是通过多次遍历数组,每次将最大(或较小)的元素移动到末尾,最终实现序列有序。冒泡排序的思路跟分治无关。
15.解释:答案选B。
选项A:这个代码未对数组a表示的整数,小于b表示的整数进行处理,因此在这种情况下,返回结果错误。
选项B:正确。
选项C:代码的时间复杂度为O(a.size())。
选项D:第21、22、23行的代码对结果为 0 的情况进行了处理,如果结果为 0 ,结果数组中只会存一个 0 。
1.解释:错误。
单链表不仅支持在表头进行插入和删除操作,也可以在表中指定位置(通过遍历找到相应节点后)以及表尾进行插入和删除操作,只是在表头操作相对更便捷,时间复杂度为O(1),而在其他位置操作通常需要先遍历找到对应位置,时间复杂度与链表长度有关。
2.解释:正确。
线性筛法相对于埃拉托斯特尼筛法,每个合数只会被它的最小质因数筛去一次,因此避免了重复标记,效率更高。线性筛法的时间复杂度为O(n),而埃拉托斯特尼筛法的时间复杂O(nloglogn) 。3.解释:错误。
3.解释:错误
唯一分解定理(也称为算术基本定理)表明,任何一个大于1的整数都可以唯一地表示为若干个质数的乘积,本题中若干个不同的质数”表述错误。
4.解释:错误。
贪心算法通过每一步选择局部最优解,但并不总能保证得到全局最优解。只有在某些特定问题中,贪心算法才能确保获得最优解。因此,题目中的说法是不正确的。
5.解释:正确。
递归算法需要一个明确的结束条件(基准情况)来停止递归调用,否则会导致函数不断调用自身,最终引发栈溢出错误。结束条件确保递归过程在某个时刻终止,从而避免无限递归。
6.解释:错误。
快速排序和归并排序的平均时间复杂度均为O(nlogn) ,但它们在稳定性上有所不同。归并排序是稳定排序,而快速排序不是稳定排序,因为元素相同的两个记录可能会因为交换而改变顺序。
7.解释:错误。
快速排序的平均时间复杂度为O(nlogn) ,但在最坏情况下(例如,每次选取的基准都是最大或最小元素),时间复杂度为O(n2)。插入排序的平均和最坏情况时间复杂度均为O(n2)。因此,快速排序并不总是比插入排序快。
8.解释:正确。
二分查找要求能够直接访问数组的中间元素,这在数组中可以通过索引实现,但链表不支持随机访问,需要从头遍历到指定位置,效率低。因此,二分查找适用于数组而不适合链表。
9.解释:正确。
对于题干中的有序数组进行二分查找元素 19 时,第一次比较中间元素(假设取中间位置向下取整,为 56),发现 19 小于 56,然后在左半部分 {5 , 13, 19, 21, 37}中 继续查找,第二次比较中间元素(此时为 19),所以比较次数是 2 次。
10.解释:正确。
递归函数每次调用自身时,系统会在调用栈上分配内存,以保存局部变量、返回地址和其他信息。这些开销使得递归通常比迭代更耗费内存空间,因为每一次递归调用都需要额外的栈帧,而迭代则只需一个固定的栈帧。
思路:
可以考虑质因数分解,使得最后每一个奇妙数字以及它们的乘积是 n 的因数。
奇妙数字的定义:x=pa。
所以在质因数分解的过程中,我们统计每个质因数有多少,然后统计可以分解成多少个奇妙数字。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>using namespace std;using ll = long long;
ll n, ans;// cnt 为指数
ll solve(ll cnt) {ll res = 0;ll tmp = 1;while (cnt >= tmp) { // 每次 - tmp 是使乘积为 n cnt -= tmp;tmp++;res++;}return res;
} int main() {scanf("%lld", &n);for (ll i = 2; i * i <= n; i++) { // 质因数分解 if (n % i == 0) {ll cnt = 0;while (n % i == 0) {cnt++;n /= i;}ans += solve(cnt);}} if (n != 1) // 没有分解彻底,还有一个质数 ans += solve(1); // 指数为 1 printf("%lld", ans);return 0;
}
具体思路见代码注释。
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int n, m;
vector<LL> wp[1005];//wp[i]是第i种武器的强化材料按照花费从小到大排序的vector
LL check(int tar)
{LL sum = 0, now = wp[1].size();//now为第一种武器目前有多少种材料vector<LL> rem;//第1种武器的材料种类为tar,那么其他的最多为tar-1,先把多余的都锻造掉//从头开始,也就是从小到大依次锻造for(int i = 2; i <= n; i++)if(!wp[i].empty()){int numi = max((int)wp[i].size() - tar + 1, 0);//第i种武器的材料减少多少才能为tar-1now += numi;//第一种武器材料数更新for(int j = 0; j <= numi - 1; j++)sum += wp[i][j];//花费的金币数for(int j = numi; j <= (int)wp[i].size() - 1; j++)rem.push_back(wp[i][j]);//如果还有剩余,放入rem中}if(now < tar)//如果还不够的话,从rem数组里从小取,取够为止{sort(rem.begin(), rem.end());for(LL ele : rem){sum += ele;now++;if(now == tar) break;}}return sum;
}
int main(){cin >> n >> m;for(int i = 1; i <= m; i++){int p, c;cin >> p >> c;wp[p].push_back(c);//花费进vector}for(int i = 1; i <= n; i++)if(!wp[i].empty())//第i种武器材料数不为0,按照花费从小到大排序sort(wp[i].begin(), wp[i].end());LL ans = 1e18;//最小花费初始化,记得开longlong//枚举适配第1种武器的强化材料种类数为i时的最小花费//第1种武器的材料种类最小值为当前值和1取最大for(int i = max((int)wp[1].size(), 1); i <= m; i++)ans = min(ans, check(i));cout << ans;return 0;
}