CCF-GESP 等级考试 2025年9月认证C++五级真题解析

1 单选题(每题 2 分,共 30 分)

第1题 以下哪种情况使用链表比数组更合适?

A. 数据量固定且读多写少 B. 需要频繁在中间或开头插入、删除元素

C. 需要高效随机访问元素 D. 存储空间必须连续

解析:答案B。链表插入、删除的时间复杂度为𝑂(1),查询的时间复杂度为𝑂(𝑛)。数据量固定且读多写少的使用数组比较合适,A错误;需要频繁在中间或开头插入、删除元素,符合链表操作特点,频繁插入、删除效率高,B正确;需要高效随机访问元素的数组效率高,C错误;存储空间必须连续的是数组,链表特点是存储空间可以不连续,D错误。故选B。

第2题 函数 removeElements 删除单链表中所有结点值等于 val 的结点,并返回新的头结点,其中链表头结点为 head ,则横线处填写( )。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│// 结点结构体 2│struct Node { 3│ int val; 4│ Node* next; 5│ 6│ Node() : val(0), next(nullptr) {} 7│ Node(int x) : val(x), next(nullptr) {} 8│ Node(int x, Node *next) : val(x), next(next) {} 9│}; 10│ 11│Node* removeElements(Node* head, int val) { 12│ Node dummy(0, head); // 哑结点,统一处理头结点 13│ Node* cur = &dummy; 14│ while (cur->next) { 15│ if (cur->next->val == val) { 16│ _______________________ // 在此填入代码 17│ 18│ } 19│ else { 20│ cur = cur->next; 21│ } 22│ } 23│ return dummy.next; 24│} |

|----|---------------------------------------------------------------------|----|---------------------------------------------------------------------|
| A. | 1│Node* del = cur; 2│cur = del->next; 3│delete del; | B. | 1│Node* del = cur->next; 2│cur->next = del; 3│delete del; |
| C. | 1│Node* del = cur->next; 2│cur->next = del->next; 3│delete del; | D. | 1│Node* del = cur->next; 2│delete del; 3│cur->next = del->next; |

解析:答案C。正确选项分析

C****选项 ‌是唯一完全正确的实现:

Node* del = cur->next; 创建指针指向待删除结点

cur->next = del->next; 跳过待删除结点建立新连接

delete del; 释放被跳过的结点内存

其他选项问题

Node* del = cur; cur = del->next; 3│delete del;错误地将cur自身删除,导致链表断裂,A错误;Node* del = cur->next; cur->next = del; delete del; 建立无效的cur->next=del连接,未真正跳过待删除结点,B.错误;Node* del = cur->next; 创建指针指向待删除结点,cur->next = del->next; 跳过待删除结点建立新连接,delete del; 释放被跳过的结点内存,C.正确;Node* del = cur->next; delete del; cur->next = del->next; 先delete再访问del->next会导致未定义行为,D错误。故选C。

第3题 函数 hasCycle 采用Floyd快慢指针法判断一个单链表中是否存在环,链表的头节点为 head ,即用两个指针在链表上前进:slow每次走1步,fast每次走 2 步,若存在环,fast终会追上slow(相遇);若无环,fast会先到达nullptr,则横线上应填写( )。

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│struct Node { 2│ int val; 3│ Node *next; 4│ Node(int x) : val(x), next(nullptr) {} 5│}; 6│ 7│bool hasCycle(Node *head) { 8│ if (!head || !head->next) 9│ return false; 10│ Node* slow = head; 11│ Node* fast = head->next; 12│ while (fast && fast->next) { 13│ if (slow == fast) return true; 14│ _______________________ // 在此填入代码 15│ } 16│ return false; 17│} |

|----|----------------------------------------------------|----|----------------------------------------------------|
| A. | 1│slow = slow->next; 2│fast = fast->next->next; | B. | 1│slow = fast->next; 2│fast = slow->next->next; |
| C. | 1│slow = slow->next; 2│fast = slow->next->next; | D. | 1│slow = fast->next; 2│fast = fast->next->next; |

解析:答案A。

slow = slow->next;
fast = fast->next->next;

算法说明

‌初始化‌:慢指针slow从头节点开始,快指针fast从头节点的下一个节点开始

‌移动规则‌:

slow每次移动1步(slow = slow->next)

fast每次移动2步(fast = fast->next->next)

终止条件 ‌:

如果fast或fast->next为空,说明链表无环

如果slow == fast,说明发现环

‌故选A。

第4题 函数 isPerfectNumber 判断一个正整数是否为完全数(该数是否即等于它的真因子之和),则横线上应填写( )。一个正整数 n 的真因子包括所有小于n的正因子,如28的真因子为1, 2, 4, 7, 14。

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│bool isPerfectNumber(int n) { 2│ if(n <= 1) return false; 3│ int sum = 1; 4│ for(int i = 2; ______; i++) { 5│ if(n % i == 0) { 6│ sum += i; 7│ if(i != n/i) sum += n/i; 8│ } 9│ } 10│ return sum == n; 11│} |

A. i <= n B. i*i <= n C. i <= n/2 D. i < n

解析:答案B。真因子不包含该数本身,一个数的最大真因子不会超过其平方根,因此只需遍历到 i*i <= n 即可,能显著提高效率。所以应该填i*i <= n。A包含n本身,错;B相当于i<=,正确;C仅n的一半,错,D虽不错但没有B效率高。故选B。

第5题 以下代码计算两个正整数的最大公约数(GCD),横线上应填写( )。

|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│int gcd0(int a, int b) { 2│ if (a < b) { 3│ swap(a, b); 4│ } 5│ while(b != 0) { 6│ int temp = a % b; 7│ a = b; 8│ b = temp; 9│ } 10│ return ______; 11│} |

A. b B. a C. temp D. a * b

解析:答案B。循环结束时b=0。A、D,错误;C的tmp是中间变量,且退出循环时b=temp=0,故错误。故选B。

第6题 函数 sieve 实现埃拉托斯特尼筛法(埃氏筛),横线处应填入( )。

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│vector sieve(int n) { 2│ vector is_prime(n+1, true); 3│ is_prime[0] = is_prime[1] = false; 4│ for(int i = 2; i <= n; i++) { 5│ if(is_prime[i]) { 6│ for(int j = ______; j <= n; j += i) { 1│ is_prime[j] = false; 1│ } 1│ } 1│ } 10│ return is_prime; 11│} |

A. i B. i+1 C. i*2 D. i*i

解析:答案D。这是埃拉托斯特尼筛法的关键优化:对于每个素数i,其所有小于i的倍数都已经被更小的素数标记过了,所以直接从i*i开始标记即可,避免重复操作。i是质数不能筛除,A错误;i是质数不能保证i+1不是质数,如2、3都是质数,B错误;i*2只是其中一个倍数(如i>2,则i*2已被标记),C错误。故选D。

第7题 函数 linearSieve 实现线性筛法(欧拉筛),横线处应填入( )。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│vector linearSieve(int n) { 2│ vector is_prime(n+1, true); 3│ vector primes; 4│ for(int i = 2; i <= n; i++) { 5│ if(is_prime[i]) primes.push_back(i); 6│ for(int p : primes) { 7│ if(p * i > n) break; 8│ is_prime[p * i] = false; 9│ if(________) break; 10│ } 11│ } 12│ return primes; 13│} |

A. i % p == 0 B. p % i == 0 C. i == p D. i * p == n

解析:答案A。通过线性筛(欧拉筛)法,可以去掉合数,筛选出质数,这是因为:

(1)对于一个合数m,若m=p₁p₂,而p₁和p₂为小于m的质数,则m一定被去掉。否则,则有m=p₁x,p₁是m的最小质因数,取x的最小质因数成立,p₁≤pₓ|x。

(2)在线性筛法中,对于i*p,若存在i'>i使i'*pⱼ=i*pⱼ',则有pⱼ'<pⱼ≤pᵢ,pⱼ'|i*pⱼèpⱼ'|ièpⱼ'≥pᵢ,矛盾,因此不存在这样的i',所以i*pⱼ已被去除。

线性筛 在埃氏筛基础上优化,确保每个合数只被其‌最小质因子 ‌标记一次。

关键步骤:如果i是primes[j]的倍数,即程序中p的倍数,也就是i % p == 0,就停止循环。故选A。

第8题 关于 埃氏筛 和 线性筛 的比较,下列说法错误的是( )。

A. 埃氏筛可能会对同一个合数进行多次标记

B. 线性筛的理论时间复杂度更优,所以线性筛的速度往往优于埃氏筛

C. 线性筛保证每个合数只被其最小质因子筛到一次

D. 对于常见范围(𝑛≤10⁷),埃氏筛因实现简单,常数较小,其速度往往优于线性筛

解析:答案B。埃氏筛‌:从2开始,依次将每个素数的倍数标记为合数。例如,找到素数2后,标记4、6、8、10等为合数;然后找到素数3,标记6、9、12等为合数。这种方法简单直观,但会导致合数被多次标记(如6会被2和3分别标记),故A正确;‌线性筛在埃氏筛基础上优化,确保每个合数只被其‌最小质因子‌标记一次,使时间复杂度为𝑂(𝑛),每个合数仅被标记一次,因此效率比埃氏筛更高,虽然线性筛理论上比埃氏筛快,但实际上差不了多少(线性筛是𝑂(𝑛),埃氏筛是𝑂(𝑛⋅loglog𝑛)),但是如果代码中的数组使用了vector,由于线性筛算法涉及到多次数组存取(vector访问速度比原生数组慢)和内存的动态分配,可能会比埃氏筛慢不少,在实际运行中因为线性筛的实现相对复杂,其常数因子较大,在常见范围(n≤10⁷)内,埃氏筛由于实现简单、常数小,其实际运行速度往往反而更快,故B错误、C正确、D正确。故选B。

第9题 唯一分解定理描述的是( )。

A. 每个整数都能表示为任意素数的乘积

B. 每个大于1的整数能唯一分解为素数幂乘积(忽略顺序)

C. 合数不能分解为素数乘积

D. 素数只有两个因子:1 和自身

解析:答案B。唯一分解定理描述的是:每个大于1的整数能唯一分解为素数幂乘积(忽略顺序)。故选‌B‌。

第10题 给定一个n×n的矩阵matrix ,矩阵的每一行和每一列都按升序排列。函数countLE返回矩阵中第k小的元素,则两处横线上应分别填写( )。

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│// 统计矩阵中 <= x 的元素个数:从左下角开始 2│int countLE(const vector<vector>& matrix, int x) { 3│ int n = (int)matrix.size(); 4│ int i = n - 1, j = 0, cnt = 0; 5│ while (i >= 0 && j < n) { 6│ if (matrix[i][j] <= x) { 7│ cnt += i + 1; 8│ ++j; 9│ } 10│ else { 11│ --i; 12│ } 13│ } 14│ return cnt; 15│} 16│ 17│int kthSmallest(vector<vector>& matrix, int k) { 18│ int n = (int)matrix.size(); 19│ int lo = matrix[0][0]; 20│ int hi = matrix[n - 1][n - 1]; 21│ 22│ while (lo < hi) { 23│ int mid = lo + (hi - lo) / 2; 24│ if (countLE(matrix, mid) >= k) { 25│ ________________ // 在此处填入代码 26│ } else { 27│ ________________ // 在此处填入代码 28│ } 29│ } 30│ return lo; 31│} |

|----|---------------------------------|----|-----------------------------|
| A. | 1│hi = mid - 1; 2│lo = mid + 1; | B. | 1│hi = mid; 2│lo = mid; |
| C. | 1│hi = mid; 2│lo = mid + 1; | D. | 1│hi = mid + 1; 2│lo = mid; |

解析:答案C。在二分查找中,由24得知:countLE(matrix, mid) 返回的是矩阵中<= x的元素个数 >= k 时,说明第 k 小的元素在 [lo, mid] 范围内,因此需要将 hi 更新为 mid(即 hi = mid)。反之,如果矩阵中<= x的元素个数 < k,则第 k 小的元素在 (mid, hi] 范围内,需要将 lo 更新为 mid + 1(即 lo = mid + 1)。故选C。

第11题 下述C++代码实现了快速排序算法,下面说法错误的是( )。

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│int partition(vector& arr, int low, int high) { 2│ int i = low, j = high; 3│ int pivot = arr[low]; // 以首元素为基准 4│ while (i < j) { 5│ while (i < j && arr[j] >= pivot) j--; //从右往左查找 6│ while (i < j && arr[i] <= pivot) i++; //从左往右查找 7│ if (i < j) swap(arr[i], arr[j]); 8│ } 9│ swap(arr[i], arr[low]); 10│ return i; 11│} 12│ 13│void quickSort(vector& arr, int low, int high) { 14│ if (low >= high) return; 15│ int p = partition(arr, low, high); 16│ quickSort(arr, low, p - 1); 17│ quickSort(arr, p + 1, high); 18│} |

A. 快速排序之所以叫"快速",是因为它在平均情况下运行速度较快,常数小、就地排序,实践中通常比归并排序更高效。

B. 在平均情况下,划分的递归层数为log𝑛,每层中的总循环数为𝑛,总时间为𝑛log𝑛。

C. 在最差情况下,每轮划分操作都将长度为𝑛的数组划分为长度0和𝑛-1的两个子数组,此时递归层数达到𝑛,每层中的循环数为𝑛,总时间为(𝑛²)。

D. 划分函数partition中"从右往左查找"与"从左往右查找"的顺序可以交换。

解析:答案‌D‌。快速排序的"从右往左查找"和"从左往右查找"的顺序‌不能随意交换‌。在partition函数中,while (i < j && arr[j] >= pivot) j-- 和 while (i < j && arr[i] <= pivot) i++ 这两行代码的顺序是固定的,必须从右往左先找小于基准值的元素,再从左往右找大于基准值的元素,这样才能正确完成分区。交换顺序会导致分区逻辑错误,无法保证排序的正确性。

快速排序平均性能优异,常数因子小,是实践中高效的排序算法,A正确;平均情况下递归深度为𝑂(log𝑛),每层处理𝑂(𝑛)元素,总时间为𝑂(𝑛log𝑛),B正确;最坏情况下(如已排序数组)递归深度为𝑂(𝑛),每层处理𝑂(𝑛)元素,总时间为𝑂(𝑛²),C正确。故选D。

第12题 下述C++代码实现了归并排序算法,则横线上应填写( )。

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│void merge(vector &nums, int left, int mid, int right) { 2│ // 左子数组区间为[left, mid], 右子数组区间为[mid+1, right] 3│ vector tmp(right - left + 1); 4│ int i = left, j = mid + 1, k = 0; 5│ while (i <= mid && j <= right) { 6│ if (nums[i] <= nums[j]) 7│ tmp[k++] = nums[i++]; 8│ else 9│ tmp[k++] = nums[j++]; 10│ } 11│ while (i <= mid) { 12│ tmp[k++] = nums[i++]; 13│ } 14│ while (________) { // 在此处填入代码 15│ tmp[k++] = nums[j++]; 16│ } 17│ for (k = 0; k < tmp.size(); k++) { 18│ nums[left + k] = tmp[k]; 19│ } 20│} 21│ 22│void mergeSort(vector &nums, int left, int right) { 23│ if (left >= right) 24│ return; 25│ 26│ int mid = (left + right) / 2; 27│ mergeSort(nums, left, mid); 28│ mergeSort(nums, mid + 1, right); 29│ merge(nums, left, mid, right); 30│} |

A. i < mid B. j < right C. i <= mid D. j <= right

解析:答案‌D‌。这里的while (j <= right)是为了处理右子数组(mid+1到right)中可能还有剩余元素的情况。当左子数组(left到mid)已经全部合并完毕,但右子数组还有元素时,需要将这些剩余元素直接复制到临时数组tmp中,以保证合并后的数组是完全有序的。故选D。

第13题 假设你是一家电影院的排片经理,只有一个放映厅。你有一个电影列表movies,其中 movies[i] = [start_i, end_i]表示第i 部电影的开始和结束时间。请你找出最多能安排多少部不重叠的电影,则横线上应分别填写的代码为( )。

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│int maxMovies(vector<vector>& movies) { 2│ if (movies.empty()) return 0; 3│ 4│ sort(movies.begin(), movies.end(), [](const vector& a, const vector& b) { 5│ return ______; // 在此处填入代码 6│ }); 7│ 8│ int count = 1; 9│ int lastEnd = movies[0][1]; 10│ 11│ for (int i = 1; i < movies.size(); i++) { 12│ if (movies[i][0] >= lastEnd) { 13│ count++; 14│ ______ = movies[i][1]; // 在此处填入代码 15│ } 16│ } 17│ 18│ return count; 19│} |

A. a[0] < b[0] 和 lastEnd B. a[1] < b[1] 和 lastEnd

C. a[0] < b[0] 和 movies[i][0] D. a[1] < b[1] 和 movies[i][0]

解析:答案‌B‌。按结束时间排序,所以第5行应该填a[1]<b[1],第14行更新的应该是结束时间,所以应该填lastEnd。故选B。

第14题 给定一个整数数组nums,下面代码找到一个具有最大和的连续子数组,并返回该最大和。则下面说法错误的是( )。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│int crossSum(vector& nums,int left,int mid, int right) { 2│ int leftSum = INT_MIN, rightSum = INT_MIN; 3│ int sum = 0; 4│ for (int i = mid; i >= left; i--) { 5│ sum += nums[i]; 6│ leftSum = max(leftSum, sum); 7│ } 8│ sum = 0; 9│ for (int i = mid + 1; i <= right; i++) { 10│ sum += nums[i]; 11│ rightSum = max(rightSum, sum); 12│ } 13│ return leftSum + rightSum; 14│} 15│ 16│int helper(vector& nums, int left, int right) { 17│ if (left == right) 18│ return nums[left]; 19│ int mid = left + (right - left) / 2; 20│ int leftMax = helper(nums, left, mid); 21│ int rightMax = helper(nums, mid + 1, right); 22│ int crossMax = crossSum(nums, left, mid, right); 23│ return max({leftMax, rightMax, crossMax}); 24│} 25│ 26│int maxSubArray(vector& nums) { 27│ return helper(nums, 0, nums.size() - 1); 28│} |

A. 上述代码采用分治算法实现 B. 上述代码采用贪心算法

C. 上述代码时间复杂度为(𝑛log𝑛) D. 上述代码采用递归方式实现

解析:答案‌B‌。分析算法类型‌:分治和递归是明确的,但‌没有贪心算法的特征 ‌(贪心算法通常基于局部最优选择,而这里是递归分解问题)。所以A正确上述代码没有采用贪心算法‌,B 错误 ‌;分治的标准复杂度为𝑂 ( 𝑛 log 𝑛 )‌C正确 ;helper函数递归调用自身处理子数组,D正确 。故选B。

第15题 给定一个由非负整数组成的数组digits,表示一个非负整数的各位数字,其中最高位在数组首位,且digits不含前导0(除非是0本身)。下面代码对该整数执行+1操作,并返回结果数组,则横线上应填写( )。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│vector plusOne(vector& digits) { 2│ for (int i = (int)digits.size() - 1; i >= 0; --i) { 3│ if (digits[i] < 9) { 4│ digits[i] += 1; 5│ return digits; 6│ } 7│ ________________ // 在此处填入代码 8│ } 9│ digits.insert(digits.begin(), 1); 10│ return digits; 11│} |

A. digits[i] = 0; B. digits[i] = 9;

C. digits[i] = 1; D. digits[i] = 10;

解析:答案A。在加1操作中,当"当前位"为9时需要进位,将其置为0并继续处理前一位。具体分析如下:

1 逻辑 :从"末位"开始遍历(i = digits.size() -- 1),若digits[i] < 9,直接加1返回(如 1234 → 1235);若 digits[i] == 9,加1后变为10,需进位(如 1999 → 2000)。

进位处理 :当前位digits[i] = 0(9+1,"当前位"为0,并进1),继续处理前一位(--i)"+1"。

若所有位都为9(如9999),循环结束后在首位插入1(digits.insert(digits.begin(), 1))。故选A。

2 判断题(每题 2 分,共 20 分)

第1题 基于下面定义的函数,通过判断 isDivisibleBy9(n) == isDigitSumDivisibleBy9(n) 代码可验算如果一个数能被9整除,则它的各位数字之和能被9整除。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│bool isDivisibleBy9(int n) { 2│ return n % 9 == 0; 3│} 4│ 5│bool isDigitSumDivisibleBy9(int n) { 6│ int sum = 0; 7│ string numStr = to_string(n); 8│ for (char c : numStr) { 9│ sum += (c - '0'); 10│ } 11│ return sum % 9 == 0; 12│} |

解析:答案正确‌。若n为非负整数,该代码能有效验算"如果一个数能被9整除,则它的各位数字之和能被9整除"这一性质。

  1. ‌数学性质成立‌

数学定理:一个非负整数能被9整除,当且仅当它的各位数字之和能被9整除(例如:

n=108(108%9=0),数字和=1+0+8=9(9%9=0);

n=100(100%9≠0),数字和=1+0+0=1(1%9≠0))。

因此,isDivisibleBy9(n) 和 isDigitSumDivisibleBy9(n) 应始终返回相同的布尔值(即两者等价)。

  1. ‌代码逻辑验证性质‌

比较 isDivisibleBy9(n) == isDigitSumDivisibleBy9(n) 检查两者是否等价。

若等价成立(恒为 true),则意味着:

当 isDivisibleBy9(n) == true(n能被9整除)时,isDigitSumDivisibleBy9(n) 必为 true(数字和能被9整除),这直接验证了目标性质。

  1. ‌边界与注意事项‌

负数输入问题 ‌(代码缺陷):
函数未处理负数(如 n = -9):isDivisibleBy9(-9) 返回 true(-9 % 9 == 0),但 isDigitSumDivisibleBy9(-9) 计算字符串 "-9" 时,'-' 字符被转换为 -3(ASCII 值差),导致结果错误。

第2题 假设函数 gcd() 能正确求两个正整数的最大公约数,则下面的 findMusicalPattern(4,6) 函数返回2。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│void findMusicalPattern(int rhythm1, int rhythm2) { 2│ int commonDivisor = gcd(rhythm1, rhythm2); 3│ int patternLength = (rhythm1 * rhythm2) / commonDivisor; 4│ return patternLength; 5│} |

解析:答案错误‌。findMusicalPattern(4,6)返回的值(4+6) / gcd(4, 6) = 10 / 2 = 5 ≠ 2。

第3题 下面递归实现的斐波那契数列的时间复杂度为𝑂(2ⁿ)。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│long long fib_memo(int n, long long memo[]) { 2│ if (n <= 1) return n; 3│ if (memo[n] != -1) return memo[n]; 4│ memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo); 5│ return memo[n]; 6│} 7│ 8│int main() { 9│ int n = 40; 10│ long long memo[100]; 11│ fill_n(memo, 100, -1); 12│ long long result2 = fib_memo(n, memo); 13│ return 0; 14│} |

解析:答案错误‌。使用备忘录(memo数组)存储已计算的斐波那契值,避免重复计算。

递归树深度为𝑛,每层常数时间操作(检查memo和计算),总时间复杂度𝑂(𝑛)。空间复杂度𝑂(𝑛),需额外memo数组存储中间结果。通过fill_n初始化memo为-1,标记未计算状态。

main函数测试计算Fibonacci(40),结果输出到控制台。

时间复杂度𝑂(𝑛)推导:每个fib_memo(n)最多计算一次(memo[n] != -1时直接返回)。

递归树深度为n(从fib_memo(n)到fib_memo(0))。每层递归常数时间操作(检查memo和计算)。总计算次数为n(每个n计算一次),忽略递归调用开销。因此时间复杂度为𝑂(𝑛)。

第4题 链表通过更改指针实现高效的结点插入与删除,但结点访问效率低、占用内存较多,且对缓存利用不友好。

解析:答案‌正确 ‌。链表通过指针操作实现高效插入/删除,但访问效率低、内存占用高且缓存利用率差,原因如下:1. ‌高效插入/删除‌,‌时间复杂度 ‌:插入/删除操作只需修改指针(𝑂(1)时间),无需移动元素(如数组)。2. ‌访问效率低,,‌随机访问 ‌:链表无法直接通过索引访问元素(如 arr[i]),需从头遍历(𝑂(𝑛)时间)。‌3. ‌内存占用高‌,‌额外指针开销 ‌:每个节点需存储数据和指针(如 int data; Node* next;),指针占用额外内存(4/8字节)。4. ‌缓存利用不友好‌

缓存利用率差 ‌:链表节点分散存储,访问时需多次内存访问(缓存命中率低,缓存失效)。

第5题 二分查找依赖数据的有序性,通过循环逐步缩减一半搜索区间来进行查找,且仅适用于数组或基于数组实现的数据结构。

解析:答案‌正确 ‌。线性筛关键是"每个合数只会被最小质因子筛到一次",因此为𝑂(𝑛)。

第6题 线性筛关键是"每个合数只会被最小质因子筛到一次",因此为𝑂(𝑛)。

解析:答案‌正确 ‌。线性筛关键是"每个合数只会被最小质因子筛到一次",因此为𝑂(𝑛)。

第7题 快速排序和归并排序都是稳定的排序算法。

解析:答案‌错误 ‌。归并排序是稳定的,但快速排序通常不稳定。‌稳定性‌指的是排序后,相同元素的相对顺序是否保持不变。归并排序在合并两个已排序子数组时,遇到相等的元素会优先选择左侧子数组的元素,从而保持了原始顺序。而快速排序在分区过程中,相等的元素可能会被交换位置,导致相对顺序改变。

第8题 下面代码采用分治算法求解标准3柱汉诺塔问题,时间复杂度为𝑂(𝑛log𝑛)。

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1│void move(vector &src, vector &tar) { 2│ int pan = src.back(); 3│ src.pop_back(); 4│ tar.push_back(pan); 5│} 6│ 7│void dfs(int n, vector &src, vector &buf, vector &tar) { 8│ if (n == 1) { 9│ move(src, tar); 10│ return; 11│ } 12│ 13│ dfs(n - 1, src, tar, buf); 14│ move(src, tar); 15│ dfs(n - 1, buf, src, tar); 16│} 17│ 18│void solveHanota(vector &A, vector &B, vector &C) { 19│ int n = A.size(); 20│ dfs(n, A, B, C); 21│} |

解析:答案‌错误 ‌。该程序的时间复杂度为𝑂(2ⁿ)。程序采用深度优先搜索(DFS)递归方法解决汉诺塔问题。递归函数 dfs 的核心逻辑是:将src上n张盘片中的上面 n-1张盘片从 src 移动到 buf(借助 tar)。剩下最大的第n张盘片从src 直接移动到tar。将buf上n-1张盘片从buf移动到tar(借助src)。‌递推关系‌:递归调用次数满足以下关系式:𝛵(𝑛)=2𝛵(𝑛−1)+1,其中𝛵(𝑛)表示移动𝑛张盘片所需的操作次数。初始条件为 𝛵(1)=1。

𝛵(𝑛)=2𝛵(𝑛−1)+1=2(2𝛵(𝑛−2)+1)+1=2²𝛵(𝑛−2)+2+1=2²(2𝛵(𝑛−3)+1)+2+1=2³𝛵(𝑛−3)+2²+2+1=...

=2ⁿ⁻¹𝛵(1)+2ⁿ⁻²+2ⁿ⁻³+...+2+1

代入初始条件𝛵(1)=1,得:𝛵(𝑛)=2ⁿ⁻¹+(2ⁿ⁻²+2ⁿ⁻³+...+2+1)

等比数列求和公式:2ⁿ⁻¹+2ⁿ⁻²+2ⁿ⁻³+...+2+1 = 2ⁿ-1

𝛵(𝑛)= 2ⁿ-1

高阶项主导,时间复杂度为𝑂(2ⁿ)。

第9题 所有递归算法都可以转换为迭代算法。

解析:答案‌正确 。从理论上讲,只要允许使用栈来模拟函数调用的过程,任何递归程序都能被改写为迭代形式。虽然所有递归都能转换,但实际中这种转换可能非常复杂,尤其是对于深度嵌套或非尾递归的情况,代码会变得难以理解和维护。因此,在算法设计时,通常会优先考虑递归的直观性,除非遇到栈溢出或性能瓶颈,才会考虑进行这种转换。

第10题 贪心算法总能得到全局最优解。

解析:答案‌错误 。贪心算法并不总能得到全局最优解。贪心算法的核心在于每一步都选择当前状态下的局部最优解,并希望以此达到全局最优。但这需要满足两个关键条件:一是问题必须具有‌贪心选择性质‌(即局部最优能导向全局最优),二是必须满足‌最优子结构‌(问题的最优解包含其子问题的最优解)。如果问题不满足这些条件,贪心策略就可能无法得到最优解。

3 编程题(每题 25 分,共 50 分)

3.1 编程题 1

试题名称:数字选取

时间限制:1.0 s

内存限制:512.0 MB

3.1.1题目描述

给定正整数𝑛,现在有1,2,3,...,𝑛共计𝑛个整数。你需要从这𝑛个整数中选取一些整数,使得所选取的整数中任意两个不同的整数均互质(也就是说,这两个整数的最大公因数为1)。请你最大化所选取整数的数量。

例如,当𝑛=9时,可以选择1,5,7,8,9共计5个整数。可以验证不存在数量更多的选取整数的方案。

3.1.2 输入格式

一行,一个正整数𝑛,表示给定的正整数。

3.1.3 输出格式

一行,一个正整数,表示所选取整数的最大数量。

3.1.4 样例

3.1.4.1 输入样例1

|--------|
| 1 | 6 |

3.1.4.2 输出样例1

|--------|
| 1 | 4 |

3.1.4.3 输入样例2

|--------|
| 1 | 9 |

3.1.4.4 输出样例2

|--------|
| 1 | 5 |

3.1.5 数据范围

对于40%的测试点,保证1≤𝑛≤1000。

对于所有测试点,保证1≤𝑛≤10⁵。

3.1.6 编写程序

编程思路:

选择1和所有质数,因为:1与任何数都互质,质数之间都互质(因为它们没有共同的质因子),所以只要求出小于等于n的质数数,再加上1就是答案。

方法一:用普通方法求判断某数x否为质数,只要x能被2~ 整除则不是质数,否则就是质数。

参考程序如下:

cpp 复制代码
#include<iostream>
using namespace std;

int n, ans = 0;

// 判断一个数x是否为质数的函数
bool isPrime(int x) {
    if (x <= 1) return false;  // 1和负数不是质数
    for(int i=2; i*i<=x; i++){  // 遍历2到sqrt(x)
        if (x % i == 0) return false;  // 如果能被2~sqrt(x)的整数整除,不是质数
    }
    return true;  // 都不能整除则是质数
}

int main() {
    cin >> n;  // 输入n
    // 统计1到n中质数的数量
    for (int i=2; i<=n; i++){
        if (isPrime(i)) ans++;
    }
    // 输出结果,质数数量+1(加上数字1,1与任何数的最大公约数为1)
    cout << ans + 1;
    return 0;
}

时间复杂度:𝑂(𝑛) = 𝑂(𝑛¹‧⁵) = 𝑂((10⁵)¹‧⁵) = 𝑂(10⁷‧⁵) < 𝑂(10⁸),不会超时。

方法二:用埃氏筛判断某数x否为质数,当x<10⁷时,埃氏筛比线性(欧拉)筛更有效。

参考程序如下:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 1e5+5;  // n的最大值为10⁵
int n, isPrime[N], cnt=0;

int main() {
         cin >> n;
         isPrime[0] = isPrime[1] = 1; //isPrime[i]=0为质数,=1为倒数
         for(int i=2; i<=n; ++i) {
                   if(!isPrime[i]) {
                            cnt ++;
                            for(int j=2*i; j<=n; j+=i) {
                                     isPrime[j] = 1;
                            }
                   }
         }
         cout << cnt + 1;
         return 0;
}

时间复杂度:𝑂(𝑛loglogn) = 𝑂(10⁵loglog10⁵) ≈𝑂(405395) < 𝑂(10⁶),不会超时。

3.2 编程题2

试题名称:有趣的数字和

时间限制:1.0 s

内存限制:512.0 MB

3.2.1题目描述

如果一个正整数的二进制表示包含奇数个1,那么小A就会认为这个正整数是有趣的。

例如,7的二进制表示为(111)₂,包含1的个数为3个,所以7是有趣的。但是9=(1001)₂包含2个1,所以9不是有趣的。

给定正整数𝑙,𝑟,请你统计满足𝑙≤𝑛≤𝑟 的有趣的整数𝑛之和。

3.2.2 输入格式

一行,两个正整数𝑙,𝑟,表示给定的正整数。

3.2.3 输出格式

一行,一个正整数,表示𝑙,𝑟之间有趣的整数之和。

3.2.4 样例

3.2.4.1 输入样例1

|----------|
| 1 | 3 8 |

3.2.4.2 输出样例1

|---------|
| 1 | 19 |

3.2.4.3 输入样例2

|---------------|
| 1 | 65 36248 |

3.2.4.4 输出样例2

|----------------|
| 1 | 328505490 |

3.2.5 数据范围

对于40%的测试点,保证1≤𝑙≤𝑟≤10⁴。

对于另外30%的测试点,保证𝑙=1并且𝑟=2ᵏ-1,其中𝑘是大于1的正整数。

对于所有测试点,保证1≤𝑙≤𝑟≤10⁹。

3.2.6 提示 由于本题的数据范围较大,整数类型请使用long long 。

3.2.7 编写程序

由于40%的测试点1≤𝑙≤𝑟≤10⁴,10⁴有14位二进制,用移位判断1的个数,最多需要14次,所以总循环次数10⁴*14<10⁶,不会超时。但对100%的测试点1≤𝑙≤𝑟≤10⁹,10⁹有30位二进制,光遍历就会超时,所以要找规律。

方法一:

核心思路是数学规律 + 分块处理 ‌。先观察0~31这32个数:

|---|--------|----|--------|----|--------|----|--------|
| 0 | 0 0000 | 8 | 0 1000 | 16 | 1 0000 | 24 | 1 1000 |
| 1 | 0 0001 | 9 | 0 1001 | 17 | 1 0001 | 25 | 1 1001 |
| 2 | 0 0010 | 10 | 0 1010 | 18 | 1 0010 | 26 | 1 1010 |
| 3 | 0 0011 | 11 | 0 1011 | 19 | 1 0011 | 27 | 1 1011 |
| 4 | 0 0100 | 12 | 0 1100 | 20 | 1 0100 | 28 | 1 1100 |
| 5 | 0 0101 | 13 | 0 1101 | 21 | 1 0101 | 29 | 1 1101 |
| 6 | 0 0110 | 14 | 0 1110 | 22 | 1 0110 | 30 | 1 1110 |
| 7 | 0 0111 | 15 | 0 1111 | 23 | 1 0111 | 31 | 1 1111 |

通过观察可以发现:从0开始𝑛每4个数一组,总有两个数的二进制1的个数为奇数(每组的第2、3个或第1,4个),即每4个连续的数(4n, 4n+1, 4n+2, 4n+3)中,块内总和为(4n+(4n+3))或((4n+1)+(4n+2)),等于8n+3,则从第0块到第n块的和为

(0+3)+(8+3)+...+(8n+3)=8(0+1+...+n)+3(n+1)=4n²+7n+3,如用a表示完整的块数,则a=n+1,则有趣数字的和为4(a-1)²+7(a-1)+3=4(a-1)²+7(a-1)+3=4a²-a,所以有趣数字的和总是4a²-a。利用这个规律,我们可以将问题转化为计算[0, x]范围内的有趣数字和,设f(x)为[0, x]范围内的有趣数字和,那么最终答案就是f(r) - f(l-1)。

具体实现时 ‌,可以定义一个函数f(n)来计算[0, n]范围内的和。对于n,先计算完整4个一组的组数a,其和为4a² - a。然后处理剩余不足4个的数字,单独计算其贡献。注意a个整数块并不包含数4a,只到4a-1,所以剩余不足4个的数字从4a开始。

cpp 复制代码
#include<iostream>
using namespace std;

long long f(long x) {
    long a = x / 4;
    long long sum = 4 * a * a - a; // 前4a个数(0~4a-1)二进制数1为奇数的和

    for (long long i = 4 * a; i <= x; i++) { // 剩余不足4个的数字

        if (__builtin_popcount(i) & 1) sum += i; // __builtin_popcount用于快速计算二进制中1的个数
    }
    return sum;
}

int main() {
    int l, r;    
    cin >> l >> r;
    cout << f(r) - f(l - 1) << endl;
    return 0;
}

时间复杂度接近常数𝑂(1),小于𝑂(log𝑛)。如不知道__builtin_popcount**(** i**)**函数,可以用移位来求,时间复杂度小于𝑂(log𝑛)。参考程序如下:

cpp 复制代码
#include<iostream>
using namespace std;

long long f(long x) {
    long a = x / 4;
    long long sum = 4 * a * a - a;
    for (long long i = 4 * a; i <= x; i++) { // 最多3次
        long j = i, k = j & 1;
        while (j > 0) {        // 最多30次,10⁹转二进制最多30位
            j = j >> 1;
            k += j & 1;
        }
        if (k & 1) sum += i;
    }
    return sum;
}

int main() {
    int l, r;
    cin >> l >> r;
    cout << f(r) - f(l - 1) << endl;
    return 0;
}

方法二:

用位运算实现。参考程序如下:

cpp 复制代码
#include <iostream>
#define int long long
using namespace std;
inline int sum(int n) { // 计算一个数的二进制位数位之和
         if (n == 1) return 1;
         int cnt = n & 1; // 数位之和
         while(n>0) { // 10⁹二进制30位,对所有n不超过30位
                   n = n >> 1;
                   cnt += (n & 1);
         }
         return cnt;
}

inline int f(int n) { //计算[0, n]的有趣数字和
         int b = n >> 2; // b为n整除4
         int a = ((b * b) << 2); // a为最终的答案(4个一组完整区间的和)
         a -= b;           // a=4b²-b
         for (int i = (b << 2); i <= n; i++) { // 不足4个部分,最多3个
                   if (sum(i) & 1) {//二进制数字和为奇数
                            a += i; //加上i
                   }
         }
         return a;
}

signed main() {
         int l, r;
         cin >> l >> r; //输入
         cout << f(r) - f(l - 1) << endl; //输出
         return 0;
}
相关推荐
EXtreme352 小时前
【数据结构】手撕队列(Queue):从FIFO底层原理到高阶应用的全景解析
c语言·数据结构·链表·队列
程序喵大人2 小时前
Duff‘s device
c语言·开发语言·c++
亭上秋和景清2 小时前
qsort函数(快速排序)
数据结构·算法
轻描淡写6062 小时前
二进制存储数据
java·开发语言·算法
laocooon5238578862 小时前
C++ 设计模式概述及常用模式
开发语言·c++·设计模式
爱潜水的小L2 小时前
自学嵌入式day28,文件操作
linux·数据结构·算法
2301_800399722 小时前
误用sizeof()计算指针
算法
咔咔咔的2 小时前
1523. 在区间范围内统计奇数数目
c++
黑客思维者2 小时前
Python自动化测试Pytest/Unittest深度解析与接口测试落地实践
开发语言·python·pytest·unittest