GESP认证C++编程真题解析 | 202512 五级
附上汇总帖:GESP认证C++编程真题解析 | 汇总
单选题
第1题
对如下定义的循环单链表,横线处填写( )。
// 循环单链表的结点
struct Node {
int data; // 数据域
Node* next; // 指针域
Node(int d) : data(d), next(nullptr) {}
};
// 创建一个只有一个结点的循环单链表
Node* createList(int value) {
Node* head = new Node(value);
head->next = head;
return head;
}
// 在循环单链表尾部插入新结点
void insertTail(Node* head, int value) {
Node* p = head;
while (p->next != head) {
p = p->next;
}
Node* node = new Node(value);
node->next = head;
p->next = node;
}
// 遍历并输出循环单链表
void printList(Node* head) {
if (head == nullptr) return;
Node* p = head;
_______________________ //在此处填入代码
cout << endl;
}
A.
while (p != nullptr){
cout << p->data << " ";
p = p->next;
}
B.
while (p->next != nullptr){
cout << p->data << " ";
p = p->next;
}
C.
do {
cout << p->data << " ";
p = p->next;
} while (p != head);
D.
for(; p; p=p->next){
cout << p->data << " ";
}
【答案】:C
【解析】
循环单链表的遍历需要保证从头结点开始,直到再次回到头结点才结束。选项C保证至少输出依次,并在 p 回到 head 时停止,防止无限循环。
第2题
区块链技术是比特币的基础。在区块链中,每个区块指向前一个区块,构成链式列表,新区块只能接在链尾,不允许在中间插入或删除。下面代码实现插入区块添加函数,则横线处填写( )。
//区块(节点)
struct Block {
int index; // 区块编号(高度)
string data; // 区块里保存的数据
Block* prev; // 指向前一个区块
Block(int idx, const string& d, Block* p) : index(idx), data(d), prev(p) {}
};
// 区块链
struct Blockchain {
Block* tail;
// 初始化
void init() {
tail = new Block(0, "Genesis Block", nullptr);
}
// 插入新区块
void addBlock(const string& data) {
_______________________ //在此处填入代码
}
// 释放内存
void clear() {
Block* cur = tail;
while (cur != nullptr) {
Block* p = cur->prev;
delete cur;
cur = p;
}
tail = nullptr;
}
};
A.
Block* newBlock = new Block(tail->index + 1, data, tail);
tail = newBlock->prev;
B.
Block* newBlock = new Block(tail->index + 1, data, tail);
tail = newBlock;
C.
Block* newBlock = new Block(tail->index + 1, data, tail->prev);
tail = newBlock;
D.
Block* newBlock = new Block(tail->index + 1, data, tail->prev);
tail = newBlock->prev;
【答案】:B
【解析】
插入新区块时,新区间的 prev 指向当前尾区块,并更新 tail 指针指向新结点。选项B:tail->index+1生成新区块编号,prev指向旧 tail,最后 tail=newBlock 更新链尾。
第3题
下面关于单链表和双链表的描述中,正确的是( )。
struct DNode {
int data;
DNode* prev;
DNode* next;
};
// 在双链表中删除指定节点
void deleteNode(DNode* node) {
if (node->prev) {
node->prev->next = node->next;
}
if (node->next) {
node->next->prev = node->prev;
}
delete node;
}
struct SNode {
int data;
SNode* next;
};
// 在单链表中删除指定节点
void deleteSNode(SNode* head, SNode* node) {
SNode* prev = head;
while (prev->next != node) {
prev = prev->next;
}
prev->next = node->next;
delete node;
}
A. 双链表删除指定节点是 \(O(1)\),单链表是 \(O(1)\)
B. 双链表删除指定节点是 \(O(n)\),单链表是 \(O(1)\)
C. 双链表删除指定节点是 \(O(1)\),单链表是 \(O(n)\)
D. 双链表删除指定节点是 \(O(n)\),单链表是 \(O(n)\)
【答案】:C
【解析】
在双链表中,结点含有前驱指针prev和后继指针next,只要知道该结点即可在 \(O(1)\) 时间内调整前驱、后继指向并删除结点。
单链表没有前驱指针,若要删除某结点,必须从链表头开始找到它的前驱结点,需遍历链表,时间复杂度为 \(O(n)\)。
第4题
假设我们有两个数 \(a=38\) 和 \(b=14\),它们对模 \(m\) 同余,即 \(a\equiv b(mod\ m)\)。以下哪个值不可能是 \(m\)?
A. 3
B. 4
C. 6
D. 9
【答案】:D
【解析】
由 \(a\equiv b(mod\ m)\) 可得 \(m\ |\ (a-b)\),\(a - b = 24\),所以 \(m\) 必须是 24 的约数。3、4、6都是24的约数,9不是,选D。
第5题
下面代码实现了欧几里得算法。下面有关说法,错误的是( )。
int gcd1(int a, int b) {
return b == 0 ? a : gcd1(b, a % b);
}
int gcd2(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
A. gcd1() 实现为递归方式。
B. gcd2() 实现为迭代方式。
C. 当 \(a\) 较大时,gcd1() 实现会多次调用自身,需要较多额外的辅助空间。
D. 当 \(a\) 较大时,gcd1() 的实现比 gcd2() 执行效率更高。
【答案】:D
【解析】
gcd1() 使用递归实现,gcd2() 使用迭代时间,A和B正确。
递归在调用栈会为每次调用分配额外空间,因此当 a 较大且递归次数多时,gcd1() 辅助空间消耗也会多,C正确。
递归通常比迭代多了函数调用开销,执行效率并不会更高,甚至会更低,D错误。
第6题
唯一分解定理描述的内容是( )。
A. 任何正整数都可以表示为两个素数的和。
B. 任何大于1的合数都可以唯一分解为有限个质数的乘积。
C. 两个正整数的最大公约数总是等于它们的最小公倍数除以它们的乘积。
D. 所有素数都是奇数。
【答案】:B
【解析】
A是哥德巴赫猜想。
C公式错误,最大公约数与最小公倍数的关系是 \(a\times b = gcd(a, b)\times lcm(a,b)\);
D错误,2是素数也是偶数。
第7题
下述代码实现素数表的线性筛法,筛选出所有小于等于 \(n\) 的素数,则横线上应填的代码是( )。
vector<int> linear_sieve(int n) {
vector<bool> is_prime(n +1, true);
vector<int> primes;
is_prime[0] = is_prime[1] = 0; //0和1两个数特殊处理
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
primes.push_back(i);
}
________________________________ { // 在此处填入代码
is_prime[ i * primes[j] ] = 0;
if (i % primes[j] == 0)
break;
}
}
return primes;
}
A. for (int j = 0; j < primes.size() && i * primes[j] <= n; j++)
B. for(int j = sqrt(n); j <= n && i * primes[j] <= n; j++)
C. for (int j = 1; j <= sqrt(n); j++)
D. for(int j = 1; j < n && i * primes[j] <= n; j++)
【答案】:A
【解析】
线性筛法的内层循环应遍历当前已找到的素数列表,并同时检查乘积不超过n,A正确。
第8题
下列关于排序的说法,正确的是( )。
A. 快速排序是稳定排序
B. 归并排序通常是稳定的
C. 插入排序是不稳定排序
D. 冒泡排序不是原地排序
【答案】:B
【解析】
归并排序、冒泡排序、插入排序都是稳定排序,快速排序不稳定。B正确。
冒泡排序只通过相邻元素交换完成排序,不需要额外空间,是原地排序。D错误。
第9题
下面代码实现了归并排序。下述关于归并排序的说法中,不正确的是( )。
void merge(vector<int>& arr, vector<int>& temp, int l, int mid, int r) {
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j]) temp[k++] = arr[i++];
else temp[k++] = arr[j++];
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= r) temp[k++] = arr[j++];
for (int p = l; p <= r; p++) arr[p] = temp[p];
}
void mergeSort(vector<int>& arr, vector<int>& temp, int l, int r) {
if (l >= r) return;
int mid = l + (r - l) / 2;
mergeSort(arr, temp, l, mid);
mergeSort(arr, temp, mid + 1, r);
merge(arr, temp, l, mid, r);
}
A. 归并排序的平均复杂度是 \(O(nlogn)\)。
B. 归并排序需要 \(O(n)\) 的额外空间。
C. 归并排序在最坏情况的时间复杂度是 \(O(n^2)\)。
D. 归并排序适合大规模数据。
【答案】:C
【解析】
归并排序平均和最快情况下的时间复杂度均为 \(O(nlogn)\),C错误。
归并排序在合并过程中需要一个临时数组存放结果,因此额外空间复杂度为 \(O(n)\),B正确。
由于归并排序稳定且时间复杂度低,适合大规模数据处理,D正确。
第10题
下述C++代码实现了快速排序算法,最坏情况的时间复杂度是( )。
int partition(vector<int>& arr, int low, int high) {
int i = low, j = high;
int pivot = arr[low]; // 以首元素为基准
while (i < j) {
while (i < j && arr[j] >= pivot) j--;
while (i < j && arr[i] <= pivot) i++;
if (i < j) swap(arr[i], arr[j]);
}
swap(arr[i], arr[low]);
return i;
}
void quickSort(vector<int>& arr, int low, int high) {
if (low >= high) return;
int p = partition(arr, low, high);
quickSort(arr, low, p - 1);
quickSort(arr, p + 1, high);
}
A. \(O(n)\)
B. \(O(logn)\)
C. \(O(n^2)\)
D. \(O(nlogn)\)
【答案】:C
【解析】
如果输入是有序或接近有序的数组,快速排序以首元素为基准,将导致每次分区一边为空,另一边为 \(n-1\) 个元素,递归深度达到 \(n\)。在这种情况下,时间复杂度为 \(O(n^2)\)。
第11题
下面代码尝试在有序数组中查找第一个大于等于 x 的元素位置。如果没有大于等于 x 的元素,返回 arr.size()。以下说法正确的是( )。
int lower_bound(vector<int>& arr, int x) {
int l = 0, r = arr.size();
while(l < r) {
int mid = l + (r - l) / 2;
if(arr[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
A. 上述代码逻辑正确
B. 上述代码逻辑错误,while 循环条件应该用 l <= r
C. 上述代码逻辑错误,mid 计算错误
D. 上述代码逻辑错误,边界条件不对
【答案】:A
【解析】
当 arr[mid]>=x 时收缩右边界,否则收缩左边界。结束时 l 正好为目标位置,逻辑正确。
第12题
小杨要把一根长度为 L 的木头切成 K 段,使得每段长度小于等于 x。已知每切一刀只能把一段木头分成两段,他用二分法找到满足条件的最小 x ( x 为正整数),则横线处应填写( )。
// 判断:在不超过 K 次切割内,是否能让每段长度 <= x
bool check(int L, int K, int x) {
int cuts = (L - 1) / x;
return cuts <= K;
}
// 二分查找最小可行的 x
int binary_cut(int L, int K) {
int l = 1, r = L;
while (l < r) {
int mid = l + (r - l) / 2;
________________________________ // 在此处填入代码
}
return l;
}
int main() {
int L = 10; // 木头长度
int K = 2; // 最多切 K 刀
cout << binary_cut(L, K) << endl;
return 0;
}
A.
if (check(L, K, mid))
r = mid;
else
l = mid + 1;
B.
if (check(L, K, mid))
r = mid+1;
else
l = mid + 1;
C.
if (check(L, K, mid))
r = mid + 1;
else
l = mid - 1;
D.
if (check(L, K, mid))
r = mid + 1;
else
l = mid;
【答案】:A
【解析】
若 check(L, K, mid) 为真,可以尝试更小的值,让 r = mid;若为假,则令 l = mid + 1;这样最终 l 会收敛到最小可行的 X,选A。
第13题
下面给出了阶乘计算的两种方式。以下说法正确的是( )。
int factorial1(int n) {
if (n <= 1) return 1;
return n * factorial1(n - 1);
}
int factorial2(int n) {
int acc = 1;
while (n > 1) {
acc = n * acc;
n = n - 1;
}
return acc;
}
A. 上面两种实现方式的时间复杂度相同,都为 \(O(n)\)
B. 上面两种实现方式的空间复杂度相同,都为 \(O(n)\)
C. 上面两种实现方式的空间复杂度相同,都为 \(O(1)\)
D. 函数 factorial1() 的时间复杂度为 \(O(2^n)\),函数 factorial2() 的时间复杂度为 \(O(n)\)
【答案】:A
【解析】
两种实现都要对 n 进行一次逐步递减的乘法运算,调用次数或循环次数都是 n 次,因此时间复杂度均为 \(O(n)\),A正确。
递归实现每次调用会在调用栈上存储局部变量和返回地址,空间复杂度为 \(O(n)\),B错误。
迭代实现只需常量额外空间,空间复杂度为 \(O(1)\),C错误。
递归的时间复杂度不是 \(O(2^n)\),D错误。
第14题
给定有 \(n\) 个任务,每个任务有截止时间和利润,每个任务耗时 1 个时间单位、必须在截止时间前完成,且每个时间槽最多做 1 个任务。为了在规定时间内获得最大利润,可以采用贪心策略,即按利润从高到低排序,尽量安排,则横线处应填写( )。
struct Task {
int deadline; //截止时间
int profit; //利润
};
void sortByProfit(vector<Task>& tasks) {
sort(tasks.begin(), tasks.end(),
[](const Task& a, const Task& b) {
return a.profit > b.profit;
});
}
int maxProfit(vector<Task>& tasks) {
sortByProfit(tasks);
int maxTime = 0;
for (auto& t : tasks) {
maxTime = max(maxTime, t.deadline);
}
vector<bool> slot(maxTime + 1, false);
int totalProfit = 0;
for (auto& task : tasks) {
for (int t = task.deadline; t >= 1; t--) {
if (!slot[t]) {
_______________________ //在此处填入代码
break;
}
}
}
return totalProfit;
}
A.
slot[t] = true;
totalProfit += task.profit;
B.
slot[t] = false;
totalProfit += task.profit;
C.
slot[t] = true;
totalProfit = task.profit;
D.
slot[t] = false;
totalProfit = task.profit;
【答案】:A
【解析】
此题的贪心策略是:按利润从高到低排序任务,并在其截止时间之前找一个空闲时间槽安排。当找到可用的时间槽 t 时,应将时间槽标记为已占用 slot[t]=true,并累加该任务的利润 totalProfit += task.profit。A符合逻辑。
第15题
下面代码实现了对两个数组表示的正整数的高精度加法(数组低位在前),则横线上应填写( )。
vector<int> add(vector<int> a, vector<int> b) {
vector<int> c;
int carry = 0;
for (int i = 0; i < a.size() || i < b.size(); i++) {
if (i < a.size()) carry += a[i];
if (i < b.size()) carry += b[i];
_______________________ //在此处填入代码
}
if (carry) c.push_back(carry);
return c;
}
A.
c.push_back(carry / 10);
carry %= 10;
B.
c.push_back(carry % 10);
carry /= 10;
C.
c.push_back(carry % 10);
D.
c.push_back(carry);
carry /= 10;
【答案】:B
【解析】
高精度加法按位相加并处理进位,本题数组低位在前,因此每次都当前位结果是 carry%10,进位是 carry / 10。B正确。
判断题
第1题
数组和链表都是线性表。链表的优点是插入删除不需要移动元素,并且能随机查找。
A. 正确
B. 错误
【答案】:B
【解析】
链表不能随机访问元素,查找某个特定位置的元素必须从头结点顺序遍历。
第2题
假设函数 gcd() 函数能正确求两个正整数的最大公约数,则下面的 lcm(a,b) 函数能正确找到两个正整数 a 和 b 的最小公倍数。
int lcm(int a, int b) {
return a / gcd(a, b) * b;
}
A. 正确
B. 错误
【答案】:A
【解析】
\(a \times b = gcd(a, b) \times lcm(a, b)\),代码中先计算除再乘,避免大数乘法直接导致溢出,逻辑正确。
第3题
在单链表中,已知指针 p 指向要删除的结点(非尾结点),想在 \(O(1)\) 删除 p ,可行做法是用 p->next 覆盖 p 的值与 next ,然后删除 p->next。
A. 正确
B. 错误
【答案】:A
【解析】
如果要删除的结点 p 已知,但它不是尾节点时,传统做法需要找到它的前驱结点才能修改指针,这通常要从链表头遍历,时间复杂度为 \(O(n)\)。这里有个 \(O(1)\) 的方法:
- 首先,把 p 的下一个结点 p->next 的数据和 p->next 的指向复制到 p。
- 这样 p 看起来就变成了它的下一个结点。
- 然后释放原本的 p->next 结点,相当于直接跳过该结点。
第4题
在求解所有不大于 n 的素数时,线性筛法(欧拉筛)都应当优先于埃氏筛法使用,因为线性筛法的时间复杂度为 \(O(n)\),低于埃氏筛法的 \(O(nloglogn)\)。
A. 正确
B. 错误
【答案】:B
【解析】
小规模 n 时两者复杂度差异微乎其微,并不是所有情况下都优先使用线性筛法。
第5题
二分查找仅适用于有序数据。若输入数据无序,当仅进行一次查找时,为了使用二分而排序通常不划算。
A. 正确
B. 错误
【答案】:A
【解析】
二分查找依赖数据的有序性。如果只是进行一次查找,把数据先排序的时间复杂度为 \(O(nlogn)\),而一次线性查找只需 \(O(n)\),通常排序的成本会超过二分带来的查找效率提升。
第6题
通过在数组的第一个、最中间和最后一个这 3 个数据中选择中间值作为枢轴(比较基准),快速排序算法可降低落入最坏情况的概率。
A. 正确
B. 错误
【答案】:A
【解析】
选择中间大小的值作为枢轴,可以避免枢轴过偏,提升分区平衡性,从而降低进入最坏情况的概率。
第7题
贪心算法在每一步都做出当前看来最优的局部选择,并且一旦做出选择就不再回溯;而分治算法将问题分解为若干子问题分别求解,再将子问题的解合并得到原问题的解。
A. 正确
B. 错误
【答案】:A
【解析】
题述准确地概括了两者的本质区别,答案正确。
第8题
以下 fib 函数计算第 n 项斐波那契数( fib(0)=0 , fib(1)=1 ),其时间复杂度为 \(O(n)\)。
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
A. 正确
B. 错误
【答案】:B
【解析】
使用递归,会重复计算大量相同的字节过,时间复杂度不是 \(O(n)\)。若要达到 \(O(n)\) 的时间复杂度,需要使用循环或带记忆化的递归。
第9题
递归函数一定要有终止条件,否则可能会造成栈溢出。
A. 正确
B. 错误
【答案】:A
【解析】
调用栈的空间是有限的,当压入的栈帧数量超过该空间时,就会发生栈溢出(stack overflow),造成程序崩溃或运行异常。
第10题
使用贪心算法解决问题时,通过对每一步求局部最优解,最终一定能找到全局最优解。
A. 正确
B. 错误
【答案】:B
【解析】
贪心算法只能在满足贪心选择性质和最优子结构的问题中保证最终解是全局最优。对于不满足这些性质的问题,可能得到次优甚至错误的解。
浙公网安备 33010602011771号