以下哪种情况使用链表比数组更合适?
B:链表结点靠 next 指针相连,无需连续存储,在中间或开头插入删除只改指针,而数组要移动后续元素。读多写少、随机访问、要求连续存储都更适合数组。
函数 removeElements 删除单链表中所有结点值等于 val 的结点,并返回新的头结点,其中链表头结点为 head,则横线处填写( )。
01// 结点结构体 02struct Node { 03 int val; 04 Node* next; 05 Node() : val(0), next(nullptr) {} 06 Node(int x) : val(x), next(nullptr) {} 07 Node(int x, Node *next) : val(x), next(next) {} 08}; 09 10Node* removeElements(Node* head, int val) { 11 Node dummy(0, head); // 哑结点,统一处理头结点 12 Node* cur = &dummy; 13 while (cur->next) { 14 if (cur->next->val == val) { 15 ____________ // 在此填入代码 16 } 17 else { 18 cur = cur->next; 19 } 20 } 21 return dummy.next; 22}
C:先令 del 指向 cur->next 即待删结点,把 cur->next 改为 del->next 跳过它,再 delete del。D 先 delete 后访问 del->next 属悬垂指针,A、B 均未真正摘除结点。
函数 hasCycle 采用 Floyd 快慢指针法判断一个单链表中是否存在环,链表的头节点为 head,即用两个指针在链表上前进:slow 每次走 步,fast 每次走 步,若存在环,fast 终会追上 slow(相遇);若无环,fast 会先到达 nullptr,则横线上应填写( )。
01struct Node { 02 int val; 03 Node *next; 04 Node(int x) : val(x), next(nullptr) {} 05}; 06 07bool hasCycle(Node *head) { 08 if (!head || !head->next) 09 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:slow 沿 next 走 1 步、fast 走 2 步,即 slow=slow->next 与 fast=fast->next->next。其余选项让 fast 依赖 slow 的位移,步数或指向均不正确。
函数 isPerfectNumber 判断一个正整数是否为完全数(该数是否即等于它的真因子之和),则横线上应填写( )。一个正整数 的真因子包括所有小于 的正因子,如 的真因子为 。
01bool isPerfectNumber(int n) { 02 if(n <= 1) return false; 03 int sum = 1; 04 for(int i = 2; ____________; i++) { 05 if(n % i == 0) { 06 sum += i; 07 if(i != n/i) sum += n/i; 08 } 09 } 10 return sum == n; 11}
B:sum 从 1 起,i 从 2 试到 i*i<=n,命中因子时同时累加 i 与 n/i,i 与 n/i 相等只加一次;28 可验算得 sum=28。其余条件会把因子 i 与 n/i 重复累加。
以下代码计算两个正整数的最大公约数(GCD),横线上应填写( )。
01int gcd0(int a, int b) { 02 if (a < b) { 03 swap(a, b); 04 } 05 while(b != 0) { 06 int temp = a % b; 07 a = b; 08 b = temp; 09 } 10 return ____________; 11}
B:以 12、8 为例:temp=a%b=4,a=8、b=4;再算 temp=0,a=4、b=0,while(b!=0) 退出时余数为 0,最大公约数留在 a 中,故返回 a。
函数 sieve 实现埃拉托斯特尼筛法(埃氏筛),横线处应填入( )。
01vector<bool> sieve(int n) { 02 vector<bool> is_prime(n+1, true); 03 is_prime[0] = is_prime[1] = false; 04 for(int i = 2; i <= n; i++) { 05 if(is_prime[i]) { 06 for(int j = ____________; j <= n; j += i) { 07 is_prime[j] = false; 08 } 09 } 10 } 11 return is_prime; 12}
D:内层从 ii 开始筛,因小于 i² 的合数必含更小质因子、已被筛掉;从 i 开始会把素数自身标成合数,从 i+1、i2 开始则要么漏筛要么多筛。
函数 linearSieve 实现线性筛法(欧拉筛),横线处应填入( )。
01vector<int> linearSieve(int n) { 02 vector<bool> is_prime(n+1, true); 03 vector<int> primes; 04 for(int i = 2; i <= n; i++) { 05 if(is_prime[i]) primes.push_back(i); 06 for(int p : primes) { 07 if(p * i > n) break; 08 is_prime[p * i] = false; 09 if(____________) break; 10 } 11 } 12 return primes; 13}
A:欧拉筛用 primes 中的质数 p 标记 i 的倍数,当 i%p==0 时 p 已是 i 的最小质因子,继续用更大质数 q 标记的 iq 也必被 p 标记,故 break。
关于埃氏筛和线性筛的比较,下列说法错误的是( )。
B:线性筛理论 O(n) 确比埃氏筛 O(nloglogn) 优,但 n≤10^7 的常见范围内埃氏筛实现简单、常数小,实测往往更快,故 B 的结论错误,D 正确。
唯一分解定理描述的是( )。
B:唯一分解定理指每个大于 1 的整数都能唯一写成素数幂的乘积(不计顺序),如 12=2²×3。A 的任意素数、C、D 的素数定义均非定理内容。
给定一个 的矩阵 matrix,矩阵的每一行和每一列都按升序排列。函数 countLE 返回矩阵中第 小的元素,则两处横线上应分别填写( )。
01// 统计矩阵中 <= x 的元素个数:从左下角开始 02int countLE(const vector<vector<int>>& matrix, int x) { 03 int n = (int)matrix.size(); 04 int i = n - 1, j = 0, cnt = 0; 05 while (i >= 0 && j < n) { 06 if (matrix[i][j] <= x) { 07 cnt += i + 1; 08 ++j; 09 } 10 else { 11 --i; 12 } 13 } 14 return cnt; 15} 16 17int kthSmallest(vector<vector<int>>& matrix, int k) { 18 int n = (int)matrix.size(); 19 int lo = matrix[0][0]; 20 int hi = matrix[n - 1][n - 1]; 21 while (lo < hi) { 22 int mid = lo + (hi - lo) / 2; 23 if (countLE(matrix, mid) >= k) { 24 ____________ // 在此处填入代码 25 } else { 26 ____________ // 在此处填入代码 27 } 28 } 29 return lo; 30}
C:countLE(mid)>=k 说明第 k 小元素不超过 mid,收上界 hi=mid;否则在 mid 右侧,lo=mid+1。A 的 hi=mid-1 可能跳过答案,B 永不收敛。
下述 C++ 代码实现了快速排序算法,下面说法错误的是( )。
01int partition(vector<int>& arr, int low, int high) { 02 int i = low, j = high; 03 int pivot = arr[low]; // 以首元素为基准 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 swap(arr[i], arr[low]); 10 return i; 11} 12 13void quickSort(vector<int>& 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}
D:partition 以 arr[low] 为 pivot,必须先右后左找可交换元素;若先从左往右,i 自 pivot 处必前进,最后 swap(arr[i],arr[low]) 会把 pivot 放错位置。
下述 C++ 代码实现了归并排序算法,则横线上应填写( )。
01void merge(vector<int> &nums, int left, int mid, int right) { 02 // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right] 03 vector<int> tmp(right - left + 1); 04 int i = left, j = mid + 1, k = 0; 05 while (i <= mid && j <= right) { 06 if (nums[i] <= nums[j]) 07 tmp[k++] = nums[i++]; 08 else 09 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 22void mergeSort(vector<int> &nums, int left, int right) { 23 if (left >= right) 24 return; 25 int mid = (left + right) / 2; 26 mergeSort(nums, left, mid); 27 mergeSort(nums, mid + 1, right); 28 merge(nums, left, mid, right); 29}
D:前两个 while 分别耗尽左、右子数组,第三个 while 把右子数组 [mid+1,right] 剩余元素拷入 tmp,条件 j<=right 才不漏最后一个元素。
假设你是一家电影院的排片经理,只有一个放映厅。你有一个电影列表 movies,其中 movies[i] = [start_i, end_i] 表示第 部电影的开始和结束时间。请你找出最多能安排多少部不重叠的电影,则横线上应分别填写的代码为( )。
01int maxMovies(vector<vector<int>>& movies) { 02 if (movies.empty()) return 0; 03 sort(movies.begin(), movies.end(), [](const vector<int>& a, const vector<int>& b) { 04 return ____________; // 在此处填入代码 05 }); 06 int count = 1; 07 int lastEnd = movies[0][1]; 08 for (int i = 1; i < movies.size(); i++) { 09 if (movies[i][0] >= lastEnd) { 10 count++; 11 ____________ = movies[i][1]; // 在此处填入代码 12 } 13 } 14 return count; 15}
B:活动安排贪心按结束时间升序排序,lastEnd 记上一场结束时间,movies[i][0]>=lastEnd 就安排并把 lastEnd 更新为 movies[i][1];按开始时间排序不能保证最优。
给定一个整数数组 nums,下面代码找到一个具有最大和的连续子数组,并返回该最大和。则下面说法错误的是( )。
01int crossSum(vector<int>& nums, int left, int mid, int right) { 02 int leftSum = INT_MIN, rightSum = INT_MIN; 03 int sum = 0; 04 for (int i = mid; i >= left; i--) { 05 sum += nums[i]; 06 leftSum = max(leftSum, sum); 07 } 08 sum = 0; 09 for (int i = mid + 1; i <= right; i++) { 10 sum += nums[i]; 11 rightSum = max(rightSum, sum); 12 } 13 return leftSum + rightSum; 14} 15 16int helper(vector<int>& 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 26int maxSubArray(vector<int>& nums) { 27 return helper(nums, 0, nums.size() - 1); 28}
B:该代码是分治:helper 递归拆左右半,取 leftMax、rightMax、crossSum 三者最大值;crossSum 从 mid 向两侧扩展求跨中线最大和,总复杂度 O(nlogn),不是贪心。
给定一个由非负整数组成的数组 digits,表示一个非负整数的各位数字,其中最高位在数组首位,且 digits 不含前导 (除非是 本身)。下面代码对该整数执行 操作,并返回结果数组,则横线上应填写( )。
01vector<int> plusOne(vector<int>& digits) { 02 for (int i = (int)digits.size() - 1; i >= 0; --i) { 03 if (digits[i] < 9) { 04 digits[i] += 1; 05 return digits; 06 } 07 ____________ // 在此处填入代码 08 } 09 digits.insert(digits.begin(), 1); 10 return digits; 11}
A:从最低位 digits[i] 起,遇 9 置 0 继续进位,某位小于 9 则加 1 后直接返回;全 9 才走到 insert 前插 1。置 9、置 1、置 10 都无法正确进位。
基于下面定义的函数,通过判断 isDivisibleBy9(n) == isDigitSumDivisibleBy9(n) 代码可验算如果一个数能被 整除,则它的各位数字之和能被 整除。
01bool isDivisibleBy9(int n) { 02 return n % 9 == 0; 03} 04 05bool isDigitSumDivisibleBy9(int n) { 06 int sum = 0; 07 string numStr = to_string(n); 08 for (char c : numStr) { 09 sum += (c - '0'); 10 } 11 return sum % 9 == 0; 12}
对。10 的幂模 9 余 1,故 n 与各位数字之和模 9 同余,isDivisibleBy9(n) 与 isDigitSumDivisibleBy9(n) 对所有 n 结果一致,用 == 比较可互相验算。
假设函数 gcd() 能正确求两个正整数的最大公约数,则下面的 findMusicalPattern(4, 6) 函数返回 。
01void findMusicalPattern(int rhythm1, int rhythm2) { 02 int commonDivisor = gcd(rhythm1, rhythm2); 03 int patternLength = (rhythm1 * rhythm2) / commonDivisor; 04 return patternLength; 05}
错。gcd(4,6)=2,patternLength=4×6÷2=12,算的是两数最小公倍数而非 2;且函数声明为 void 却 return patternLength,本就不能返回数值。
下面递归实现的斐波那契数列的时间复杂度为 。
01long long fib_memo(int n, long long memo[]) { 02 if (n <= 1) return n; 03 if (memo[n] != -1) return memo[n]; 04 memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo); 05 return memo[n]; 06} 07 08int main() { 09 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 数组被 fill_n 全部置 -1,每个 n 只算一次即缓存,之后直接返回 memo[n],总工作量 O(n),不是朴素递归的 O(2^n)。
链表通过更改指针实现高效的结点插入与删除,但结点访问效率低、占用内存较多,且对缓存利用不友好。
对。链表结点靠指针链接、内存不连续,插入删除只改指针因而高效,但按序访问只能逐个跳 next,且每个结点多存指针、对缓存不友好,描述准确。
二分查找依赖数据的有序性,通过循环逐步缩减一半搜索区间来进行查找,且仅适用于数组或基于数组实现的数据结构。
对。二分查找每次比较 mid 处元素后把区间缩减一半,前提是数据有序,且需支持 O(1) 随机访问;链表只能顺序访问,无法直接二分。
线性筛关键是“每个合数只会被最小质因子筛到一次”,因此为 。
对。线性筛中当 i%p==0 即 break,此时 p 是 i 的最小质因子,保证每个合数只被其最小质因子标记一次,标记总次数与 n 同阶,故复杂度为 O(n)。
快速排序和归并排序都是稳定的排序算法。
错。归并排序合并时相等元素保持原相对顺序,是稳定的;快速排序的划分 swap 可能改变相等元素相对顺序,不稳定,两者稳定性不同。
下面代码采用分治算法求解标准 柱汉诺塔问题,时间复杂度为 。
01void move(vector<int> &src, vector<int> &tar) { 02 int pan = src.back(); 03 src.pop_back(); 04 tar.push_back(pan); 05} 06 07void dfs(int n, vector<int> &src, vector<int> &buf, vector<int> &tar) { 08 if (n == 1) { 09 move(src, tar); 10 return; 11 } 12 dfs(n - 1, src, tar, buf); 13 move(src, tar); 14 dfs(n - 1, buf, src, tar); 15} 16 17void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) { 18 int n = A.size(); 19 dfs(n, A, B, C); 20}
错。设移动 n 个盘耗时为 T(n),T(n)=2T(n-1)+1,解得 T(n)=2^n−1 次 move,时间复杂度 O(2^n),并非 O(nlogn)。
所有递归算法都可以转换为迭代算法。
对。任何递归都能用显式栈模拟调用过程改写为迭代,如用 stack 保存参数与返回点,只是代码可能更繁琐,理论上不存在不可迭代化的递归。
贪心算法总能得到全局最优解。
错。贪心只在满足最优子结构且局部最优能推出全局最优时成立,如 1、4、5 元硬币凑 8 元贪心取 5+1+1+1 得 4 枚,而 4+4 只要 2 枚。