GESP认证C++编程真题解析 | 202606 五级
附上汇总帖:GESP认证C++编程真题解析 | 汇总
单选题
第1题
假设 head != nullptr ,下面是实现单向循环链表在头节点后插入新节点的代码,横线处应填入( )。
struct Node {
int val;
Node* next;
};
void insertAfterHead(Node* head, int x) {
Node* newNode = new Node;
newNode->val = x;
______________________ // 在此处填入代码
}
A.
newNode->next = head;
head->next = newNode;
B.
newNode->next = head->next;
head->next = newNode;
C.
head->next = newNode;
newNode->next = head->next;
D.
newNode->next = head->next;
head = newNode;
【答案】:B
【解析】
在头节点后插入新节点,标准做法是先让新节点指向原头节点的下一个节点,再让头节点指向新节点。选项B正是这两步:newNode->next = head->next 保存了原后继,head->next = newNode 完成插入。选项A让新节点指向head本身,破坏了循环链表原有的后继关系。选项C的写法有顺序错误,先执行head->next = newNode 会丢失原后继节点,之后newNode->next = head->next 让新节点指向自己,形成自环。选项D修改了head指针本身,但head是值传递参数,不会影响外部,且也没有正确建立链接。
第2题
下面代码遍历并输出一个循环单链表,其中 head 指向链表的第一个节点,横线处应填入的是( )。
struct Node {
int val;
Node* next;
};
void printList(Node* head) {
if (head == nullptr) return;
Node* p = head;
_______________________ // 在此处填入代码
cout << endl;
}
A.
while (p != nullptr) {
cout << p->val << " ";
p = p->next;
}
B.
while (p->next != nullptr) {
cout << p->val << " ";
p = p->next;
}
C.
do {
cout << p->val << " ";
p = p->next;
} while (p != head);
D.
for (; p; p = p->next) {
cout << p->val << " ";
}
【答案】:C
【解析】
循环单链表的尾节点 next 指回头节点,不存在nullptr终止,所以不能用p!=nullptr 或p->next != nullptr 作为循环条件,选项A、B、D都会死循环或无法遍历全部节点。正确做法是用 do-while 先访问当前节点再前进,直到回到head为止,选项C符合这一逻辑。由于进入函数前已保证 head非空,do-while 至少执行一次也是合理的。
第3题
双链表结点定义如下,若要删除双链表中的中间结点(非首尾节点) p ,下面写法正确的是( )。
struct Node {
int val;
Node* prev;
Node* next;
};
A.
p->prev->next = p->next;
p->next->prev = p->prev;
delete p;
B.
p->next->prev = p->next;
p->prev->next = p->prev;
delete p;
C.
p->prev = p->next;
p->next = p->prev;
delete p;
D.
p->next->next = p->prev;
p->prev->prev = p->next;
delete p;
【答案】:A
【解析】
删除中间节点,需要让p 的前驱的 next指向p的后继,让p的后继的 prev 指向p的前驱,然后释放p。选项A正是这两步操作,顺序也正确。选项B把前驱和后继都指向了自己,完全错误。选项C只修改了 p 自身的指针,没有修改前驱和后继的指向,删除后链表断裂。选项D修改的是更远节点的指针,与删除操作无关。
第4题
使用如下欧几里得算法求 gcd(105, 45) 时,函数 gcd(a, b) 的递归调用序列正确的是( )。
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
A. gcd(105, 45) -> gcd(45, 60) -> gcd(60, 15) -> gcd(15, 0)
B. gcd(105, 45) -> gcd(45, 15) -> gcd(15, 0)
C. gcd(105, 45) -> gcd(60, 45) -> gcd(15, 45)
D. gcd(105, 45) -> gcd(15, 45) -> gcd(15, 0)
【答案】:B
【解析】
欧几里得算法每一步用gcd(b, a%b)递归。第一步105%45=15,所以调用gcd(45, 15);第二步45%15=0,调用gcd(15, 0);此时b == 0返回15,整个过程结束。选项B完全对应这一序列。选项A出现了gcd(45, 60),这是把a-b 当成了 a%b,属于减法求gcd的写法而非取模。选项C和D的参数顺序或取模结果都有误。
第5题
下面代码实现线性筛(欧拉筛),以筛选出 n 以内的所有素数。横线处的代码应为( )。
vector<int> sieve(int n) {
vector<bool> is_prime(n + 1, true);
vector<int> primes;
if (n >= 0) is_prime[0] = false;
if (n >= 1) is_prime[1] = false;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
primes.push_back(i);
}
for (int j = 0; j < primes.size() && i * primes[j] <= n; j++) {
is_prime[i * primes[j]] = false;
if (________________) break; // 在此处填入代码
}
}
return primes;
}
A. i % primes[j] == 0
B. primes[j] % i == 0
C. i % primes[j] != 0
D. i == primes[j]
【答案】:A
【解析】
线线性筛的核心是让每个合数只被其最小质因子筛掉一次。当 i % primes[j] == 0日时,primes[j]是 i 的最小质因子,此时i * primes[j+1]这个合数的最小质因子也是 primes[j]而非 primes[j+1],它会在之后由更大的 i 乘以primes[j]筛掉,因此必须在此处 break,避免重复筛。选项A正确。选项B方向反了,选项C条件相反会导致提前不break,选项D只在i本身为质数且等于 primes[j]时才break,无法覆盖合数情况。
第6题
下面关于埃氏筛法的说法正确的是( )。
A. 每个合数只会被筛掉一次
B. 从每个素数出发,把它的倍数标记为合数
C. 只能判断一个数是不是偶数
D. 不能求出素数表
【答案】:B
【解析】
埃氏筛法的基本思想就是从每个素数出发,把它的所有倍数标记为合数,最终未被标记的就是素数,选项B正确。选项A描述的是线性筛的特性而非埃氏筛,埃氏筛中一个合数会被多个质因子重复标记。选项C和D显然错误,埃氏筛正是用来求素数表的经典算法。
第7题
下面代码实现了计算 \(x^n\) 的快速幂算法,该算法体现的编程思想是( )。
long long power(long long x, int n) {
if (n == 0) return 1;
long long res = power(x, n / 2);
if (n % 2 == 0) return res * res;
else return res * res * x;
}
A. 枚举
B. 贪心
C. 分治
D. 模拟
【答案】:C
【解析】
快速幂把 \(x^n\) 的问题分解为 \(x^{n/2}\) 的子问题,递归求解后再根据n的奇偶性合并结果,这是典型的分治思想——把大问题拆成结构相同的子问题分别求解再合并。选项A枚举是逐个尝试,选项B贪心是每步取局部最优,选项D模拟是按过程逐步推演,均不符合。
第8题
下面代码用于统计 n 中因子 2 出现了多少次。若 n = 40 ,输出是( )。
int n = 40;
int cnt = 0;
while (n % 2 == 0) {
cnt++;
n /= 2;
}
cout << cnt;
A. 1
B. 2
C. 3
D. 4
【答案】:C
【解析】
\(40=2^3×5\),因子2出现3次。代码执行过程:n=40能被2整除,cnt=1,n=20;n=20能被2整除,cnt=2,n=10;n=10能被2整除,cnt=3,n=5;n=5不能被2整除,循环结束,输出3。
第9题
在一个有序数组中查找第一个大于或等于 x 的元素位置,横线处应填写( )。
int lowerBound(vector<int>& a, int x) {
int l = 0, r = a.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) ________________; // 在此处填入代码
else l = mid + 1;
}
return l;
}
A. r = mid + 1
B. r = mid - 1
C. r = mid
D. l = mid
【答案】:C
【解析】
这是经典的 lower_bound 实现,查找第一个满足 a[mid] >= x 的位置。当a[mid]>=x 时,mid可能就是答案,也可能答案在 mid 左侧,所以要把右边界收缩到mid(包含mid),即 r = mid。选项A答案区间无法缩小,导致死循环;选项B把mid排除在外,可能错过答案。选项D令 l=mid,1,区间缩小的方向不对。最终l == r即为所求位置。
第10题
有若干根木头,长度存于 wood 。每切一刀可以把一段木头分成两段。函数 check(wood, K, x) 返回: 用不超过 K 刀,能否使所有木段长度都不超过 x 。下面代码使用二分答案查找最小可行的 x ,横线处应填( )。
int binary_cut(vector<int>& wood, int K) {
int l = 1;
int r = 0;
for (int len : wood) r = max(r, len);
while (l < r) {
int mid = l + (r - l) / 2;
if (check(wood, K, mid))
________________; // 在此处填入代码
else l = mid + 1;
}
return l;
}
A. r = mid + 1
B. r = mid
C. l = mid
D. r = mid - 1
【答案】:B
【解析】
二分答案求最小可行的x。当check(mid)成立时,说明mid是一个可行的上限,但我们想找更小的可行值,所以把右边界收缩到mid,即r = mid ,保留mid作为候选。当check(mid) 不成立时,mid太小,需要 l = mid +1排除mid。选项A答案区间无法缩小,导致死循环;选项D会错误地排除可行的mid,选项C 用区间缩小的方向不对。
第11题
下面代码段实现了快速排序的划分操作(以首元素为基准),横线处代码应填入( )。
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[low];
int i = low, j = high;
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]);
}
________________; // 在此处填入代码
return i;
}
A. swap(arr[low], arr[high])
B. swap(arr[low], arr[i])
C. swap(arr[i], arr[high])
D. arr[i] = pivot
【答案】:B
【解析】
这是经典的双指针划分(Hoare划分)。循环结束后,i指向的位置左侧都小于等于 pivot,右侧都大于等于pivot,此时需要把基准元素 arr[low] 交换到 i 位置,使基准就位。选项B swap(arr[low], arr[i])正确。选项A交换的是low和high,与划分结果无关。选项C交换 i 和high不对。选项D直接赋值会丢失 arr[low]原本的值(pivot 已保存在变量中,但arr[low]位置会被覆盖,逻辑不完整)。
第12题
下面哪句话最符合归并排序的思想?( )
A. 每次选择最小元素放到前面
B. 将数组分成两半分别排序,再合并两个有序部分
C. 相邻元素两两交换
D. 从左到右把元素插入有序区
【答案】:B
【解析】
归并排序的核心就是分治:先把数组分成两半递归排序,然后把两个有序子数组合并成一个有序数组,选项B准确描述了这一过程。选项A是选择排序,选项C是冒泡排序,选项D是插入排序。
第13题
在对长度为 \(n\)(\(n\ge 1\))的数组进行归并排序的过程中, mergeArray 函数(合并两个有序子数组的操作) 被调用的次数是( )。
const int MAXN = 100005;
int a[MAXN];
int tempArr[MAXN];
void mergeArray(int left, int mid, int right) {
int i = left; // 左半部分起点
int j = mid + 1; // 右半部分起点
int k = left; // 临时数组下标
while (i <= mid && j <= right) {
if (a[i] <= a[j]) {
tempArr[k++] = a[i++];
} else {
tempArr[k++] = a[j++];
}
}
while (i <= mid) {
tempArr[k++] = a[i++];
}
while (j <= right) {
tempArr[k++] = a[j++];
}
for (int p = left; p <= right; p++) {
a[p] = tempArr[p];
}
}
void mergeSort(int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
mergeSort(left, mid);
mergeSort(mid + 1, right);
mergeArray(left, mid, right);
}
A. \(n - 1\)
B. \(logn\)
C. \(nlogn\)
D. \(2n\)
【答案】:A
【解析】
归并排序的递归树有n个叶子(每个单元素是一个有序子数组),每一次 mergeArray 把两个有序子数组合并为一个,合并一次减少一个子数组。从n个子数组合并到1个,恰好需要n-1次合并。也可以从递归树角度理解:内部节点数等于叶子数减1,每个内部节点对应一次 mergeArray调用,所以是n-1次。
第14题
小杨在学校义卖会上负责打包“零食盲盒”。每个盲盒重量不同,快递盒最多承重 limit 克,每个快递盒最多装两个盲盒。为了尽量少用快递盒,他采用如下策略:
(1)每次把最轻的盲盒和最重的盲盒尝试放在一起;
(2)如果两者重量之和不超过 limit ,就一起装;
(3)否则,只能让最重的盲盒单独装一盒。
下面代码用于计算最少需要多少个快递盒,则横线处应填入的是( )。
int minBoxes(vector<int>& w, int limit) {
sort(w.begin(), w.end());
int l = 0, r = w.size() - 1;
int boxes = 0;
while (l <= r) {
if (w[l] + w[r] <= limit) {
__________; // 在此处填入代码
} else {
r--;
}
boxes++;
}
return boxes;
}
A. l++;
B. r--;
C. l++; r--;
D. boxes--;
【答案】:C
【解析】
这是经典的"双指针配对"贪心问题。数组已排序,当最轻和最重两个盲盒重量之和不超过 limit 时,两者一起装入一个盒子,需要同时移动左右两个指针 l++; r--;。当两者超重时,最重的单独装一盒,只移动右指针r--。无论哪种情况,都用掉了一个盒子,所以boxes 每次加1。选项A和B只移动一个指针,会漏掉另一个的配对。选项D修改计数器毫无意义。
第15题
高精度减法中,假设两个高精度数按低位在前存储,且已经保证被减数不小于减数。下面处理借位逻辑代码中横线处应填入( )。
if (a[i] < b[i]) {
a[i + 1]--;
________________;
}
t = a[i] - b[i];
A. a[i] += 10
B. a[i] -= 10
C. b[i] += 10
D. a[i] = a[i+1] + 10
【答案】:A
【解析】
高精度减法中,当被减数当前位 a[i] 小于减数 b[i] 时需要向高位借位。先让高位 a[i+1]--,然后当前位加10变成 a[i] + 10,再减去b[i]就得到正确结果。因此横线处填真 a[i]+=10,选项A正确。选项B和C的方向错误。选项D把a[i]赋值为借位后的高位加10,逻辑不对,当前位应该是在自身基础上加10。
判断题
第1题
数组的存储空间在物理上通常是连续的,而链表的结点可以存储在不连续的内存空间中。
A. 正确
B. 错误
【答案】:A
【解析】
数组在内存中占用一段连续空间,通过下标可直接定位元素。链表的每个节点通过 new 动态分配,内存位置不一定连续,节点之间通过指针连接。这是数组与链表最本质的区别之一。
第2题
带哨兵头尾节点的双向循环链表,在表头插入节点 p ,以下四步操作无论什么顺序执行结果都正确。
① p->next = head->next;
② p->prev = head;
③ head->next->prev = p;
④ head->next = p;
A. 正确
B. 错误
【答案】:B
【解析】
这四步操作的顺序很关键。特别是①必须在④之前执行——因为①中用到了head->next (原后继),如果先执行④把head->next 改成了p,那么①就会让p指向自己。同理③也必须在④之前。正确的顺序通常是①②③④或②①③④。所以"无论什么顺序都正确"的说法是错误的。
第3题
对任意正整数 a 、 b ,以下两种写法的 gcd 函数返回值完全相同。
int gcd1(int a, int b) {
return b ? gcd1(b, a % b) : a;
}
int gcd2(int a, int b) {
while (b) {
int t = b;
b = a % b;
a = t;
}
return a;
}
A. 正确
B. 错误
【答案】:A
【解析】
两个函数都是欧几里得算法的实现,一个是递归版本,一个是迭代版本,逻辑完全等价。
第4题
在归并排序的合并操作中,如下代码片段可以正确地将两个已排序的子数组 L 和 R 合并回原数组 arr 中。
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
vector<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++];
else arr[k++] = R[j++];
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
A. 正确
B. 错误
【答案】:A
【解析】
这是标准的归并排序合并代码。先复制左右两部分到临时数组L和R,再用双指针从前往后依次取较小者写回arr,最后处理剩余元素。其中L[i]<=R[j]保证了稳定性。
第5题
分治法通常将一个规模较大的问题拆分为若干个规模较小、结构相似的子问题,分别求解后再合并子问题的 结果。
A. 正确
B. 错误
【答案】:A
【解析】
这正是分治法的三步:分解、解决、合并。把大问题分成结构相同的子问题,递归求解后合并结果,归并排序和快速排序都是典型的分治应用。
第6题
贪心算法只要每一步选择当前最优解,就一定能得到全局最优解。
A. 正确
B. 错误
【答案】:B
【解析】
贪心算法并不保证对所有问题都能得到全局最优解。只有当问题具有贪心选择性质(局部最优能推出全局最优)时才有效。例如普通的找零问题用贪心可能失败,0/1背包问题贪心也得不到最优解。贪心需要针对具体问题证明其正确性。
第7题
二分查找不仅可以应用于有序数组,也可以在不增加时间复杂度的情况下应用于有序的单链表,因为链表也 支持 \(O(1)\) 时间内的随机访问。
A. 正确
B. 错误
【答案】:B
【解析】
单链表不支持随机访问,要访问第k个元素必须从头节点遍历k步,需要O(k)时间。因此即使在有序链表上做二分查找,每次定位 mid 也要 \(O(n)\) 时间,总复杂度退化为 \(O(n log n)\),远不如在数组上的 \(O(log n)\)。题目说法错误。
第8题
以下函数 f1 的时间复杂度比函数 f2 的更高。
void f1(int n) {
for (int i = 1; i < n; i *= 2);
}
void f2(int n) {
if (n <= 1) return;
f2(n - 1);
f2(n - 1);
}
A. 正确
B. 错误
【答案】:B
【解析】
f1 中 i 每次乘2,循环执行约 \(log_2\ n\) 次,时间复杂度为 \(O(logn)\)。f2是双递归,每次把问题规模减 1 并调用两次,递归树节点数为 \(2^n\) 级别,时间复杂度为 \(O(2^n)\)。f2的复杂度远高于f1,题目说反了。
第9题
唯一分解定理表明,任何一个大于 1 的自然数都可以唯一地分解为若干个质数的乘积,如果不考虑质因数的 顺序,这种分解方式是唯一的。
A. 正确
B. 错误
【答案】:A
【解析】
这是算术基本定理(唯一分解定理)的标准表述。任何大于1的正整数都可以表示为质数的乘积,且在不考虑顺序的情况下表示方式唯一。例如12=2×2×3,没有其他质因数分解方式。
第10题
归并排序和快速排序在平均情况下的时间复杂度均为 \(O(nlogn)\)。但在稳定性方面,归并排序通常是不稳定 的,而快速排序是稳定的。
A. 正确
B. 错误
【答案】:B
【解析】
时间复杂度部分正确,两者平均都是 \(O(nlogn)\)。但稳定性说反了:归并排序是稳定的排序算法(合并时相等元素保持原序),快速排序是不稳定的排序算法(划分时交换可能改变相等元素的相对顺序)。题目把两者的稳定性恰好说反了。
浙公网安备 33010602011771号