CCF编程能力等级认证GESP—C++5级—20240907
单选题(每题 2 分,共 30 分)
1、下面关于链表和数组的描述,错误的是( )。
A. 数组大小固定,链表大小可动态调整。
B. 数组支持随机访问,链表只能顺序访问。
C. 存储相同数目的整数,数组比链表所需的内存多。
D. 数组插入和删除元素效率低,链表插入和删除元素效率高。
正确答案:C
考察知识点:数组与链表的特性对比
解析:
选项A:数组在声明时需指定大小(静态数组)或动态分配后大小固定,链表通过指针动态增减节点,大小可动态调整,正确。
选项B:数组支持通过索引随机访问(O(1)),链表需从头遍历(O(n)),正确。
选项C:数组存储元素时无需额外指针,链表每个节点需额外存储next/prev指针(如双向链表每个节点多2个指针),因此存储相同数量整数时,链表内存开销更大,C错误。
选项D:数组插入/删除需移动元素(O(n)),链表仅需修改指针(O(1),已知前驱节点时),正确。
2、通过( )操作,能完成在双向循环链表结点 p 之后插入结点 s 的功能(其中 next 域为结点的直接后继, prev 域为结点的直接前驱)
A. p->next->prev = s; s->prev = p; p->next = s; s->next = p->next;
B. p->next->prev = s; p->next = s; s->prev = p; s->next = p->next;
C. s->prev = p; s->next = p->next; p->next = s; p->next->prev = s;
D. s->next = p->next; p->next->prev = s; s->prev = p; p->next = s;
正确答案:D
考察知识点:双向循环链表的插入操作
解析:
插入目标:在节点p之后插入节点s,需维护4个指针:
s->next = p->next(s的后继是p原来的后继);
p->next->prev = s(p原来后继的前驱是s);
p->next = s(p的后继是s)。
s->prev = p(s的前驱是p);
3、对下面两个函数,说法错误的是( )。
int sumA(int n){
int res = 0;
for(int i = 1; i <= n; i++){
res += i;
}
return res;
}
int sumB(int n) {
if(n==1)
return 1;
int res = n + sumB(n - 1);
return res;
}
A. sumA体现了迭代的思想。
B. SumB采用的是递归方式。
C. SumB函数比SumA的时间效率更高。
D. 两个函数的实现的功能相同。
正确答案:C
考察知识点:迭代与递归的效率对比
解析:
sumA:for循环实现累加,时间复杂度O(n),空间复杂度O(1)(仅用局部变量res)。
sumB:递归实现,时间复杂度O(n)(调用n次),空间复杂度O(n)(递归栈深度n)。
效率对比:递归因函数调用开销(栈帧创建、参数传递),时间效率低于迭代,C错误。
功能一致性:两者均计算1+2+…+n,功能相同。
4、有如下函数 fun ,则 fun(20, 12) 的返回值为( )。
int fun(int a, int b) {
if (a % b == 0)
return b;
else
return fun(b, a % b);
}
A. 20
B. 12
C. 4
D. 2
正确答案:C
考察知识点:欧几里得算法(辗转相除法)
解析:
函数功能:求a和b的最大公约数(GCD),递归公式GCD(a,b)=GCD(b,a%b),终止条件a%b==0时返回b。
计算过程:
fun(20,12) → 20%12=8 → fun(12,8)
fun(12,8) → 12%8=4 → fun(8,4)
fun(8,4) → 8%4=0 → 返回4
5、下述代码实现素数表的埃拉托斯特尼筛法,筛选出所有小于等于 n 的素数,则横线上应填的最佳代码是( )。
void sieve_Eratosthenes(int n) {
vector<bool> is_prime(n + 1, true);
vector<int> primes;
for(int i = 2; i * i <= n; i++){
if (is_prime[i]) {
primes.push_back(i);
________________________________ {
is_prime[j] = false;
}
}
}
for(int i = sqrt(n) + 1; i <= n; i++){
if (is_prime[i]) {
primes.push_back(i);
}
}
return primes;
}
A. for (int j = i; j <= n; j++)
B. for (int j = i * i; j <= n; j++)
C. for (int j = i * i; j <= n; j += i)
D. for (int j = i; j <= n; j += i)
正确答案:C
考察知识点:埃氏筛法的优化
解析:
埃氏筛核心:标记所有小于等于n的非素数,从i=2开始,将i的倍数标记为非素数。
优化点:
无需从i=2开始标记倍数,从i * i开始(因为i的倍数k * i(k<i)已被更小的素数标记过)。
步长为i(j += i),确保标记所有i的倍数。
选项C:for (j = i * i; j <= n; j += i)符合优化逻辑,正确。
6、下述代码实现素数表的线性筛法,筛选出所有小于等于 n 的素数,则横线上应填的代码是( )。
vector<int> sieve_linear(int n) {
vector<bool> is_prime(n + 1, true);
vector<int> primes;
for (int i = 2; i <= n / 2; i++) {
if (is_prime[i])
primes.push_back(i);
________________________________ {// 在此处填入代码
is_prime[i * primes[j]] = 0;
if (i % primes[j] == 0)
break;
}
}
for (int i = n / 2 + 1; i <= n; i++) {
if (is_prime[i])
primes.push_back(i);
}
return primes;
A. for (int j = 0; j < primes.size() && i * primes[j] <= n; j++)
B. for (int j = 1; j < primes.size() && i * j <= n; j++)
C. for (int j = 2; j < primes.size() && i * primes[j] <= n; j++)
D. 以上都不对
正确答案:A
考察知识点:线性筛法的内层循环
解析:
线性筛核心:每个合数仅被其最小质因子标记,内层循环遍历已找到的素数primes[j],且需满足i * primes[j] <=n。
循环条件:j < primes.size() && i * primes[j] <=n,确保不越界且遍历所有相关素数。
选项A:符合上述条件,正确。选项B中j从1开始会遗漏最小质因子2,选项C步长错误。
7、下面函数可以将 n 的所有质因数找出来,其时间复杂度是( )。
#include <iostream>
#include <vector>
vector<int> get_prime_factors(int n) {
vector<int> factors;
while(n % 2 == 0){
factors.push_back(2);
n /= 2;
}
for(int i = 3; i * i <= n; i += 2){
while (n % i == 0) {
factors.push_back(i);
n /= i;
}
}
if(n > 2){
factors.push_back(n);
}
return factors;
}
A. O ( n 2 ) O(n^2) O(n2)
B. O ( n l o g n ) O(nlogn) O(nlogn)
C. O ( n l o g n ) O(\sqrt{n}logn) O(nlogn)
D. O ( n ) O(n) O(n)
正确答案:C
考察知识点:质因数分解的时间复杂度
解析:
算法逻辑:
处理偶因数(O(logn)次除法);
处理奇因数,i从3到sqrt(n),步长2(循环次数 O ( n ) ) O(\sqrt{n})) O(n));
每次除法操作将n缩小至少一个因子,除法总次数O(logn)。
总时间复杂度: O ( n l o g n ) O(\sqrt{n}logn) O(nlogn),C正确。
8、现在用如下代码来计算 x n x^n xn (n个x相乘),其时间复杂度为( )。
double quick_power(double x, unsigned n) {
if (n == 0) return 1;
if (n == 1) return x;
return quick_power(x, n / 2) * quick_power(x, n / 2) * ((n & 1) ? x : 1);
}
A. O ( n ) O(n) O(n)
B. O ( n 2 ) O(n^2) O(n2)
C. O ( l o g n ) O(logn) O(logn)
D. O ( n l o g n ) O(nlogn) O(nlogn)
正确答案:A
考察知识点:递归快速幂的时间复杂度
解析:
代码问题:quick_power(x,n/2)*quick_power(x,n/2)会导致重复计算(两次调用quick_power(x,n/2))。
时间复杂度分析:
递归树深度O(logn),但每层节点数翻倍,总调用次数O(2^logn)=O(n),时间复杂度O(n)。
正确快速幂应缓存quick_power(x,n/2)的结果,时间复杂度O(logn),但题目代码未优化,故A正确。
9、假设快速排序算法的输入是一个长度为n的已排序数组,且该快速排序算法在分治过程总是选择第一个元素作为基准元素。下面选项( )描述的是在这种情况下的快速排序行为。
A. 快速排序对于此类输入的表现最好,因为数组已经排序。
B. 快速排序对于此类输入的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn) 。
C. 快速排序对于此类输入的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
D. 快速排序无法对此类数组进行排序,因为数组已经排序。
正确答案:C
考察知识点:快速排序的最差情况
解析:
已排序数组+首元素为基准:每次分区将数组分为[1个元素,n-1个元素],递归深度O(n),每层比较O(n)次,总时间复杂度O(n²),C正确。
误区:选项B中O(nlogn)是平均情况,非最差情况;选项A“表现最好”错误,此时是最差情况。
10、考虑以下C++代码实现的归并排序算法:
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int L[n1], R[n2];
for (int i = 0; i < n1; i++)
L[i] = arr[left + i];
for (int j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
}
else {
arr[k] = R[j];
j++;
}
k++;
}
while(i<n1){
arr[k] = L[i];
i++;
k++;
}
while(j<n2){
arr[k] = R[j];
j++;
k++;
}
}
void merge_sort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
对长度为 n 的数组 arr ,挑用函数 merge_sort(a, 0, n-1) ,在排序过程中 merge 函数的递归调用次数大约是 ( )。
A. O ( 1 ) O(1) O(1)
B. O ( n ) O(n) O(n)
C. O ( l o g n ) O(logn) O(logn)
D. O ( n l o g n ) O(nlogn) O(nlogn)
正确答案:B
考察知识点:归并排序的递归调用次数
解析:
归并排序流程:将数组二分至长度1,再合并。merge_sort的调用次数为O(n)(每层调用次数为n/2^k,总层数logn,总和n)。
merge函数调用次数:与merge_sort相同,每层合并次数为n/2^k,总次数O(n),B正确。
11、现在有 n 个人要过河,每只船最多载2人,船的承重为100kg。下列代码中,数组 weight 中保存有 n 个人 的体重(单位为kg),已经按从小到大排好序,代码输出过河所需要的船的数目,采用的思想为( )。
int i, j;
int count = 0;
for(i = 0, j = n - 1; i < j; j--){
if (weight[i] + weight[j] <= 100) {
i++;
}
count++;
}
printf("过河的船数:%d\n", count);
A. 枚举算法
B. 贪心算法
C. 迭代算法
D. 递归算法
正确答案:B
考察知识点:贪心算法的应用
解析:
算法逻辑:双指针i(最轻)和j(最重),若两人重量和≤100,则同乘一船(i++),否则最重者单独乘船(j- -),每步选择局部最优解(尽可能两人同乘),符合贪心算法思想,B正确。
12、关于分治算法,以下哪个说法正确?
A. 分治算法将问题分成子问题,然后分别解决子问题,最后合并结果。
B. 归并排序不是分治算法的应用。
C. 分治算法通常用于解决小规模问题。
D. 分治算法的时间复杂度总是优于O(nlog(n)) 。
正确答案:A
考察知识点:分治算法的定义
解析:
选项A:分治算法三步骤(分解→解决→合并),正确。
选项B:归并排序是典型分治算法(分解为左右两半,排序后合并),错误。
选项C:分治适用于大规模问题(如n=1e5),小规模问题直接解决,错误。
选项D:分治算法时间复杂度不一定优于O(nlogn),如普通分治求最大值时间复杂度O(n),错误。
13、根据下述二分查找法,在排好序的数组 1,3,6,9,17,31,39,52,61,79 中查找数值 31 ,循环 while (left <= right) 执行的次数为( )。
int binary_search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1; // 如果找不到目标元素,返回-1
}
A. 1
B. 2
C. 3
D. 4
正确答案:C
考察知识点:二分查找的循环次数
解析:
数组:[1,3,6,9,17,31,39,52,61,79](长度10,索引0-9),目标31。
查找过程:
循环次数 | left | right | mid | nums[mid] | 操作 |
---|---|---|---|---|---|
1 | 0 | 9 | 4 | 17 | 17<31 → left=5 |
2 | 5 | 9 | 7 | 52 | 52>31 → right=6 |
3 | 5 | 6 | 5 | 31 | 找到目标 → 返回 |
循环执行次数=3,C正确。
14、以下关于高精度运算的说法错误的是( )。
A. 高精度计算主要是用来处理大整数或需要保留多位小数的运算。
B. 大整数除以小整数的处理的步骤可以是,将被除数和除数对齐,从左到右逐位尝试将除数乘以某个数,通过 减法得到新的被除数,并累加商。
C. 高精度乘法的运算时间只与参与运算的两个整数中长度较长者的位数有关。
D. 高精度加法运算的关键在于逐位相加并处理进位。
正确答案:C
考察知识点:高精度运算的特性
解析:
选项C:高精度乘法需将一个数的每一位与另一个数的每一位相乘,时间复杂度为O(lenA * lenB)(lenA、lenB为两个数的位数),与“仅与较长者位数有关”矛盾,错误。
其他选项:高精度处理大整数/小数(A正确);除法从高位到低位逐位计算(B正确);加法需处理进位(D正确)。
15、当n = 7时,下面函数的返回值为( )。
int fun(int n) {
if (n == 1) return 1;
else if (n >= 5) return n * fun(n - 2);
else return n * fun(n - 1);
A. 105
B. 840
C. 210
D. 420
正确答案:C
考察知识点:递归函数的返回值计算
解析:
fun(1)=1
fun(2)=2*fun(1)=2*1=2
fun(3)=3*fun(2)=3*2=6
fun(4)=4*fun(3)=4*6=24
fun(5)=5*fun(3)=5*6=30
fun(6)=6*fun(4)=6*24=144
fun(7)=7*fun(5)=7*30=210
判断题(每题 2 分,共 20 分)
1、在操作系统中,需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU将切换到下 一个进程。这种循环操作可以通过环形链表来实现。
正确答案:正确
考察知识点:环形链表的应用
解析:进程循环调度需首尾相连的结构,环形链表(尾节点next指向头节点)可通过current = current->next实现循环切换,正确。
2、找出自然数 n 以内的所有质数,常用算法有埃拉托斯特尼(埃氏)筛法和线性筛法,其中线性筛法效率更高。
正确答案:正确
考察知识点:筛法效率对比
解析:埃氏筛时间复杂度O(n log log n),线性筛每个合数仅被标记一次(O(n)),效率更高,正确。
3、唯一分解定理表明任何一个大于 1 的整数都可以唯一地分解为素数之和。
正确答案:错误
考察知识点:唯一分解定理
解析:唯一分解定理指“大于1的整数可唯一分解为素数乘积”,而非“素数之和”(如12=2×2×3,而非2+2+3),错误。
4、贪心算法通过每一步选择局部最优解,从而一定能获得最优解。
正确答案:错误
考察知识点:贪心算法的局限性
解析:贪心算法仅选择局部最优,可能无法全局最优(如币种[25,10,5,1]找30元,贪心选25+1×5=6枚,最优解为10×3=3枚),错误。
5、快速排序和归并排序的平均时间复杂度均为O(nlogn),且都是稳定排序。
正确答案:错误
考察知识点:排序算法的稳定性
解析:
快速排序:交换时可能改变相等元素的相对顺序(不稳定);
归并排序:合并时若相等元素先取左数组元素,可保持稳定(稳定);
两者平均时间复杂度均为O(nlogn),但“都是稳定排序”错误。
6、插入排序的时间复杂度总是比快速排序低。
正确答案:错误
考察知识点:排序算法时间复杂度对比
解析:插入排序时间复杂度O(n²),快速排序平均O(nlogn),当n较大时插入排序效率更低。
7、引入分治策略往往可以提升算法效率。一方面,分治策略减少了操作数量;另一方面,分治后有利于系统的 并行优化。
正确答案:正确
考察知识点:分治策略的优势
解析:分治通过分解问题减少操作数(如归并排序O(nlogn)优于冒泡O(n²)),且子问题可并行计算(如MapReduce)。
8、二分查找要求被搜索的序列是有序的,否则无法保证正确性。
正确答案:正确
考察知识点:二分查找的前提
解析:二分查找通过比较mid元素与target的大小调整查找范围,若数组无序,比较结果无法指导范围调整。
9、在C++语言中,递归的实现方式通常会占用更多的栈空间,可能导致栈溢出。
正确答案:正确
考察知识点:递归的栈空间开销
解析:递归需为每层调用分配栈帧(存储参数、返回地址等),递归深度过大(如n=1e5)会导致栈溢出,正确。
10、对于已经定义好的标准数学函数 sin(x) ,应用程序中的语句 y=sin(sin(x)); 是一种递归调用。
正确答案:错误
一、递归调用的核心特征
递归调用需满足 “函数直接或间接调用自身”,即函数在执行过程中,其内部代码显式或隐式地调用了函数本身。例如:
// 递归函数示例:计算n的阶乘
int factorial(int n) {
if (n == 0) return 1;
return n * factorial(n - 1); // 函数内部调用自身
}
这里 factorial 函数直接调用了 factorial(n-1),属于典型递归。
二、sin(sin(x)) 是嵌套调用,而非递归
1.函数调用流程
sin(sin(x)) 的执行过程是 两次独立的 sin 函数调用,而非 sin 调用自身:
内层调用:计算 sin(x),得到结果 a(假设为某个数值,如 sin(π/2) = 1);
外层调用:计算 sin(a),得到最终结果 y(如 sin(1) ≈ 0.8415)。
2.关键区别:是否“自身调用自身”
递归调用:函数的 实现内部 包含对自身的调用(如上述阶乘函数)。
嵌套调用:函数的 参数 是另一个函数的返回值,但被调用的函数本身并未调用自身。
sin 函数的标准实现(如 C 语言 math.h 中的 sin)是通过数学公式(如泰勒级数)计算正弦值,其内部 不会调用 sin 函数本身,因此 sin(sin(x)) 只是两次嵌套的普通函数调用,而非递归。
三、反例:若 sin 是递归函数
假设 sin 函数的实现如下(仅为举例,非真实实现):
double sin(double x) {
if (x < 1e-6) return x; // 终止条件
return 3 * sin(x/3) - 4 * sin(x/3) * sin(x/3) * sin(x/3); // 递归调用自身
}
此时 sin 函数内部调用了 sin(x/3),属于递归函数。但这种递归是 函数实现层面的递归,而非用户代码中 sin(sin(x)) 这种调用方式的递归。
用户代码中的 sin(sin(x)) 是否为递归,取决于 sin 函数自身是否调用自身,而非用户是否嵌套调用该函数。
编程题 (每题 25 分,共 50 分)
小杨的武器
【问题描述】
小杨有n种不同的武器,他对第 种武器的初始熟练度为
c
i
c_i
ci。
小杨会依次参加m场战斗,每场战斗小杨只能且必须选择一种武器使用,假设小杨使用了第i种武器参加了第j场 战斗,战斗前该武器的熟练度为
c
i
′
c_{i}^{'}
ci′,则战斗后小杨对该武器的熟练度会变为
c
i
′
+
a
j
c_{i}^{'} + a_j
ci′+aj。需要注意的是,
a
j
a_j
aj可能是正 数,0或负数,这意味着小杨参加战斗后对武器的熟练度可能会提高,也可能会不变,还有可能降低。
小杨想请你编写程序帮他计算出如何选择武器才能使得m场战斗后,自己对n种武器的熟练度的最大值尽可能大。
【输入描述】
第一行包含两个正整数n,m,含义如题面所示。
第二行包含 个正整数
c
1
,
c
2
,
.
.
.
,
c
n
c_1, c_2, ..., c_n
c1,c2,...,cn,代表小杨对武器的初始熟练度。
第三行包含 个正整数
a
1
,
a
2
,
.
.
.
,
a
m
a_1, a_2, ..., a_m
a1,a2,...,am,代表每场战斗后武器熟练度的变化值。
【输出描述】
输出一个整数,代表m场战斗后小杨对m种武器的熟练度的最大值最大是多少。
【样例输入 1】
2 2
9 9
1 -1
【样例输出 1】
10
一种最优的选择方案为,第一场战斗小杨选择第一种武器,第二场战斗小杨选择第二种武器。
子任务编号 | 数据点占比 | n | m |
---|---|---|---|
1 | 20% | =1 | < = 1 0 5 <=10^5 <=105 |
2 | 20% | < = 1 0 5 <=10^5 <=105 | = 2 |
3 | 60% | < = 1 0 5 <=10^5 <=105 | < = 1 0 5 <=10^5 <=105 |
对于全部数据,保证有 1 < = n , m < = 1 0 5 , − 1 0 4 < = c i , a i < = 1 0 4 1 <=n,m <= 10^5, -10^4 <=c_i,a_i <= 10^4 1<=n,m<=105,−104<=ci,ai<=104。
挑战怪物
【问题描述】
小杨正在和一个怪物战斗,怪物的血量为h,只有当怪物的血量恰好为0时小杨才能够成功击败怪物。 小杨有两种攻击怪物的方式:
物理攻击。假设当前为小杨第i次使用物理攻击,则会对怪物造成
2
i
−
1
2^{i-1}
2i−1点伤害。
魔法攻击。小杨选择任意一个质数x(x不能超过怪物当前血量),对怪物造成x点伤害。由于小杨并不擅长魔法,他只能使用至多一次魔法攻击。
小杨想知道自己能否击败怪物,如果能,小杨想知道自己最少需要多少次攻击。
【输入描述】
第一行包含一个正整数t,代表测试用例组数。
接下来是t组测试用例。对于每组测试用例,第一行包含一个正整数h,代表怪物血量。
【输出描述】
对于每组测试用例,如果小杨能够击败怪物,输出一个整数,代表小杨需要的最少攻击次数,如果不能击败怪物, 输出-1。
【样例输入 1】
3
6
188
9999
【样例输出 1】
2
4
-1
对于第一组测试用例,一种可能的最优方案为,小杨先对怪物使用魔法攻击,选择质数5造成5点伤害,之后对怪 物使用第1次物理攻击,造成 2 1 − 1 = 1 2^{1-1} = 1 21−1=1点伤害,怪物血量恰好为0,小杨成功击败怪物。
子任务编号 | 数据点占比 | t | h |
---|---|---|---|
1 | 20% | <=5 | <=10 |
2 | 20% | <=10 | <= 100 |
3 | 60% | <=10 | < = 1 0 5 <=10^5 <=105 |
对于全部数据,保证有 1 < = t < = 10 , 1 < = h < = 1 0 5 1 <= t <= 10, 1 <= h <= 10^5 1<=t<=10,1<=h<=105。