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题

对任意正整数 ab ,以下两种写法的 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题

在归并排序的合并操作中,如下代码片段可以正确地将两个已排序的子数组 LR 合并回原数组 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)\)。但稳定性说反了:归并排序是稳定的排序算法(合并时相等元素保持原序),快速排序是不稳定的排序算法(划分时交换可能改变相等元素的相对顺序)。题目把两者的稳定性恰好说反了。

编程题

题解:洛谷 P17010 [GESP202606 五级] 排排坐

题解:洛谷 P17011 [GESP202606 五级] 晚宴

posted @ 2026-07-18 21:09  团爸讲算法  阅读(10)  评论(0)    收藏  举报