假设 head != nullptr,下面是实现单向循环链表在头节点后插入新节点的代码,横线处应填入( )。
01struct Node { 02 int val; 03 Node* next; 04}; 05 06void insertAfterHead(Node* head, int x) { 07 Node* newNode = new Node; 08 newNode->val = x; 09 ______________________ // 在此处填入代码 10}
B:应填 newNode->next = head->next; head->next = newNode。先让新节点指向原头节点的后继,再把头节点 next 改为新节点;若顺序颠倒,head->next 已被覆盖,newNode 会指向自身,D 改 head 指针更是错误。
下面代码遍历并输出一个循环单链表,其中 head 指向链表的第一个节点,横线处应填入的是( )。
01struct Node { 02 int val; 03 Node* next; 04}; 05 06void printList(Node* head) { 07 if (head == nullptr) return; 08 Node* p = head; 09 _______________________ // 在此处填入代码 10 cout << endl; 11}
C:用 do-while 先输出首节点再让 p=p->next,直到 p 回到 head 结束,单元素链表也只输出一次;A、B、D 都依赖 p 出现 nullptr 退出,循环链表无空指针会无限循环。
双链表结点定义如下,若要删除双链表中的中间结点(非首尾节点)p,下面写法正确的是( )。
01struct Node { 02 int val; 03 Node* prev; 04 Node* next; 05};
A:先让 p 的前驱的 next 指向 p 的后继,再让 p 的后继的 prev 指向 p 的前驱,随后 delete p 释放结点;B、D 改错相邻结点指针,C 只改 p 自身,链表仍断开。
使用如下欧几里得算法求 gcd(105, 45) 时,函数 gcd(a, b) 的递归调用序列正确的是( )。
01int gcd(int a, int b) { 02 return b == 0 ? a : gcd(b, a % b); 03}
B:105%45=15,故序列为 gcd(105,45)→gcd(45,15)→gcd(15,0),b=0 时返回 a=15;A 中 gcd(45,60) 及 C、D 都未按 a%b 正确更新参数,序列错误。
下面代码实现线性筛(欧拉筛),以筛选出 以内的所有素数。横线处的代码应为( )。
01vector<int> sieve(int n) { 02 vector<bool> is_prime(n + 1, true); 03 vector<int> primes; 04 if (n >= 0) is_prime[0] = false; 05 if (n >= 1) is_prime[1] = false; 06 for (int i = 2; i <= n; ++i) { 07 if (is_prime[i]) { 08 primes.push_back(i); 09 } 10 for (int j = 0; j < primes.size() && i * primes[j] <= n; j++) { 11 is_prime[i * primes[j]] = false; 12 if (________________) break; // 在此处填入代码 13 } 14 } 15 return primes; 16}
A:线性筛的关键是 i%primes[j]==0 时 break,保证每个合数只被其最小质因子筛一次;B、C、D 会提前 break 漏筛或重复标记,破坏线性性质。
下面关于埃氏筛法的说法正确的是( )。
B:埃氏筛从每个素数出发,把它的 2 倍、3 倍等倍数标记为合数,剩余未标记的即素数;A 错,同一合数会被多个质因子重复标记,这正是它与线性筛的区别。
下面代码实现了计算 的快速幂算法,该算法体现的编程思想是( )。
01long long power(long long x, int n) { 02 if (n == 0) return 1; 03 long long res = power(x, n / 2); 04 if (n % 2 == 0) return res * res; 05 else return res * res * x; 06}
C:power(x,n) 先递归求 power(x,n/2) 再平方,n 为奇数时多乘一次 x,把 n 减半拆成子问题再合并结果,属典型分治思想,复杂度降为 O(logn)。
下面代码用于统计 n 中因子 2 出现了多少次。若 n = 40,输出是( )。
01int n = 40; 02int cnt = 0; 03while (n % 2 == 0) { 04 cnt++; 05 n /= 2; 06} 07cout << cnt;
C:n=40 连续除以 2:40→20→10→5,共三次进入 n%2==0 分支,cnt 累加到 3 后 n=5 为奇数循环停止,输出 3,即 40=2³×5 中因子 2 的指数。
在一个有序数组中查找第一个大于或等于 x 的元素位置,横线处应填写( )。
01int lowerBound(vector<int>& a, int x) { 02 int l = 0, r = a.size(); 03 while (l < r) { 04 int mid = l + (r - l) / 2; 05 if (a[mid] >= x) ________________; // 在此处填入代码 06 else l = mid + 1; 07 } 08 return l; 09}
C:a[mid]>=x 时答案可能在 mid 或更左,令 r=mid 保留 mid 候选并缩小区间;r=mid+1 会跳过 mid,r=mid-1 可能漏掉答案,l=mid 在 l<r 下会死循环。
有若干根木头,长度存于 wood。每切一刀可以把一段木头分成两段。函数 check(wood, K, x) 返回:用不超过 K 刀,能否使所有木段长度都不超过 x。下面代码使用二分答案查找最小可行的 x,横线处应填( )。
01int binary_cut(vector<int>& wood, int K) { 02 int l = 1; 03 int r = 0; 04 for (int len : wood) r = max(r, len); 05 while (l < r) { 06 int mid = l + (r - l) / 2; 07 if (check(wood, K, mid)) 08 ________________; // 在此处填入代码 09 else l = mid + 1; 10 } 11 return l; 12}
B:check(wood,K,mid) 为真说明 mid 可行,最小可行值在 [l,mid] 内,令 r=mid 保留 mid;为假才 l=mid+1,循环结束时 l 即最小可行的 x,与最小化答案的二分模板一致。
下面代码段实现了快速排序的划分操作(以首元素为基准),横线处代码应填入( )。
01int partition(vector<int>& arr, int low, int high) { 02 int pivot = arr[low]; 03 int i = low, j = high; 04 while (i < j) { 05 while (i < j && arr[j] >= pivot) j--; 06 while (i < j && arr[i] <= pivot) i++; 07 if (i < j) swap(arr[i], arr[j]); 08 } 09 ________________; // 在此处填入代码 10 return i; 11}
B:循环结束时 i 指向基准 pivot 的最终落点,swap(arr[low],arr[i]) 把首元素基准换回 i,使左半 ≤pivot、右半 ≥pivot;A、C 换的不是基准位置,D 直接赋值会丢失原元素。
下面哪句话最符合归并排序的思想?( )
B:归并排序先递归把数组对半分成两个子数组分别排序,再把两个有序子段合并成整体有序数组;A 是选择排序、C 是冒泡排序、D 是插入排序的描述。
在对长度为 ()的数组进行归并排序的过程中,mergeArray 函数(合并两个有序子数组的操作)被调用的次数是( )。
01const int MAXN = 100005; 02int a[MAXN]; 03int tempArr[MAXN]; 04 05void mergeArray(int left, int mid, int right) { 06 int i = left; // 左半部分起点 07 int j = mid + 1; // 右半部分起点 08 int k = left; // 临时数组下标 09 10 while (i <= mid && j <= right) { 11 if (a[i] <= a[j]) { 12 tempArr[k++] = a[i++]; 13 } else { 14 tempArr[k++] = a[j++]; 15 } 16 } 17 18 while (i <= mid) { 19 tempArr[k++] = a[i++]; 20 } 21 22 while (j <= right) { 23 tempArr[k++] = a[j++]; 24 } 25 26 for (int p = left; p <= right; p++) { 27 a[p] = tempArr[p]; 28 } 29} 30 31void mergeSort(int left, int right) { 32 if (left >= right) { 33 return; 34 } 35 36 int mid = left + (right - left) / 2; 37 mergeSort(left, mid); 38 mergeSort(mid + 1, right); 39 mergeArray(left, mid, right); 40}
A:mergeSort 递归树有 n 个叶子(单个元素),每合并一次两个有序段就形成一个内部节点,二叉树内部节点数为叶子数减一,故 mergeArray 恰好被调用 n-1 次。
小杨在学校义卖会上负责打包“零食盲盒”。每个盲盒重量不同,快递盒最多承重 limit 克,每个快递盒最多装两个盲盒。为了尽量少用快递盒,他采用如下策略:
()每次把最轻的盲盒和最重的盲盒尝试放在一起;
()如果两者重量之和不超过 limit,就一起装;
()否则,只能让最重的盲盒单独装一盒。
下面代码用于计算最少需要多少个快递盒,则横线处应填入的是( )。
01int minBoxes(vector<int>& w, int limit) { 02 sort(w.begin(), w.end()); 03 int l = 0, r = w.size() - 1; 04 int boxes = 0; 05 while (l <= r) { 06 if (w[l] + w[r] <= limit) { 07 __________; // 在此处填入代码 08 } else { 09 r--; 10 } 11 boxes++; 12 } 13 return boxes; 14}
C:w[l]+w[r]<=limit 时最轻与最重同盒,两指针都向中间移动,即 l++ 且 r--;否则最重单独装只 r--,每轮 boxes 加 1,正好对应题目策略 2 和 3。
高精度减法中,假设两个高精度数按低位在前存储,且已经保证被减数不小于减数。下面处理借位逻辑代码中横线处应填入( )。
01if (a[i] < b[i]) { 02 a[i + 1]--; 03 ________________; 04} 05t = a[i] - b[i];
A:a[i]<b[i] 需向高位借位,a[i+1]-- 表示高位减 1,再 a[i]+=10 使本位够减,随后 t=a[i]-b[i] 才正确;B、C、D 的借位方向或加减对象都错了。
数组的存储空间在物理上通常是连续的,而链表的结点可以存储在不连续的内存空间中。
对。数组在内存中按下标顺序连续存放,可凭首地址加偏移随机访问;链表结点靠 next 指针串联,可散落在不连续的内存块中,这正是两者存储结构的本质区别。
带哨兵头尾节点的双向循环链表,在表头插入节点 p,以下四步操作无论什么顺序执行结果都正确。
① p->next = head->next; ② p->prev = head; ③ head->next->prev = p; ④ head->next = p;
错。④ head->next=p 会覆盖原首节点,若此前未执行 ① 保存原 head->next,p->next 将指向 p 自身形成自环;必须保证 ①、③ 先于 ④ 执行,顺序不能任意。
对任意正整数 a、b,以下两种写法的 gcd 函数返回值完全相同。
01int gcd1(int a, int b) { 02 return b ? gcd1(b, a % b) : a; 03} 04 05int gcd2(int a, int b) { 06 while (b) { 07 int t = b; 08 b = a % b; 09 a = t; 10 } 11 return a; 12}
对。gcd1 用 b 非零时递归调用 gcd1(b,a%b) 实现辗转相除;gcd2 用 while(b) 迭代,循环体 t=b、b=a%b、a=t 与递归参数交换完全一致,两种写法对任意正整数返回值相同。
在归并排序的合并操作中,如下代码片段可以正确地将两个已排序的子数组 L 和 R 合并回原数组 arr 中。
01void merge(int arr[], int left, int mid, int right) { 02 int n1 = mid - left + 1; 03 int n2 = right - mid; 04 vector<int> L(n1), R(n2); 05 for (int i = 0; i < n1; i++) L[i] = arr[left + i]; 06 for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j]; 07 int i = 0, j = 0, k = left; 08 while (i < n1 && j < n2) { 09 if (L[i] <= R[j]) arr[k++] = L[i++]; 10 else arr[k++] = R[j++]; 11 } 12 while (i < n1) arr[k++] = L[i++]; 13 while (j < n2) arr[k++] = R[j++]; 14}
对。L 装入 arr[left..mid]、R 装入 arr[mid+1..right],i、j 双指针取较小者写入 arr[k],再用两个 while 抄回剩余元素,且 L[i]<=R[j] 取左半保证稳定,合并正确。
分治法通常将一个规模较大的问题拆分为若干个规模较小、结构相似的子问题,分别求解后再合并子问题的结果。
对。分治法把规模较大的问题拆成若干个规模较小、结构相似的子问题分别递归求解,再把子结果合并成原问题的解,归并排序、快速排序都是典型应用。
贪心算法只要每一步选择当前最优解,就一定能得到全局最优解。
错。贪心只保证每步局部最优,不保证全局最优,需额外证明贪心选择性质与最优子结构;如 0-1 背包每步贪拿单位价值最高的物品就得不到最优解,是反例。
二分查找不仅可以应用于有序数组,也可以在不增加时间复杂度的情况下应用于有序的单链表,因为链表也支持 时间内的随机访问。
错。单链表没有 O(1) 随机访问能力,每次取中间结点都要从头遍历 O(n) 个结点,二分查找会退化;二分查找的前提是数组式下标随机访问,链表只能用快慢指针等顺序手段。
以下函数 f1 的时间复杂度比函数 f2 的更高。
01void f1(int n) { 02 for (int i = 1; i < n; i *= 2); 03} 04 05void f2(int n) { 06 if (n <= 1) return; 07 f2(n - 1); 08 f2(n - 1); 09}
错。f1 的循环变量 i 每轮乘 2,约执行 log₂n 次,复杂度 O(logn);f2 满足 T(n)=2T(n-1),规模减 1 却加倍调用,复杂度 O(2ⁿ),f2 远高于 f1,说法相反。
唯一分解定理表明,任何一个大于 的自然数都可以唯一地分解为若干个质数的乘积,如果不考虑质因数的顺序,这种分解方式是唯一的。
对。唯一分解定理:任何大于 1 的自然数都能唯一写成质数幂的乘积 n=p1^e1·p2^e2·…,不考虑质因数顺序时分解唯一,如 12=2²×3。
归并排序和快速排序在平均情况下的时间复杂度均为 。但在稳定性方面,归并排序通常是不稳定的,而快速排序是稳定的。
错。平均 O(nlogn) 的说法正确,但稳定性说反了:归并排序合并时取 L[i]<=R[j] 能保持相等元素相对顺序,是稳定排序;快速排序划分时交换会打乱相等元素顺序,不稳定。