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整除"这一性质。
- 数学性质成立
数学定理:一个非负整数能被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) 应始终返回相同的布尔值(即两者等价)。
- 代码逻辑验证性质
比较 isDivisibleBy9(n) == isDigitSumDivisibleBy9(n) 检查两者是否等价。
若等价成立(恒为 true),则意味着:
当 isDivisibleBy9(n) == true(n能被9整除)时,isDigitSumDivisibleBy9(n) 必为 true(数字和能被9整除),这直接验证了目标性质。
- 边界与注意事项
负数输入问题 (代码缺陷):
函数未处理负数(如 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;
}