与数组相比,链表在( )操作上通常具有更高的效率。
C:链表在已知位置插入或删除只需修改相邻节点的指针,与节点个数无关;数组则要移动该位置后的所有元素。随机访问、查找指定元素、遍历这三项数组反而更快。
下面 C++ 代码实现双向链表。函数 is_empty() 判断链表是否为空,如果链表为空返回 true,否则返回 false。横线处不能填写( )。
01// 节点结构体 02struct Node { 03 int data; 04 Node* prev; 05 Node* next; 06}; 07 08// 双向链表结构体 09struct DoubleLink { 10 Node* head; 11 Node* tail; 12 int size; 13 14 DoubleLink() { 15 head = nullptr; 16 tail = nullptr; 17 size = 0; 18 } 19 20 ~DoubleLink() { 21 Node* curr = head; 22 while (curr) { 23 Node* next = curr->next; 24 delete curr; 25 curr = next; 26 } 27 } 28 29 // 判断链表是否为空 30 bool is_empty() const { 31 ____________ 32 } 33};
C:head 是指针不是对象,不能写 head.data,且 data 为 0 也不能表示空链表。head==nullptr、tail==nullptr、size==0 三种判法在空链表时均为真,A、B、D 都可用。
在双向链表尾部增加新节点的 append() 函数中,横线上应填写( )。
01void append(int data) { 02 Node* newNode = new Node(data, nullptr, nullptr); 03 04 if (is_empty()) { 05 head = tail = newNode; 06 } else { 07 ____________ 08 } 09 ++size; 10}
D:先 tail->next=newNode 连接旧尾与新节点,再 newNode->prev=tail 设置新节点前驱,最后 tail=newNode 更新尾指针。A 缺两条指针更新,B 顺序错误,C 先改 tail 导致连错节点。
下列 C++ 代码用循环链表解决约瑟夫问题,即假设 个人围成一圈,从第一个人开始数,每次数到第 个的人就出圈,输出最后留下的那个人的编号。横线上应填写( )。
01struct Node { 02 int data; 03 Node* next; 04}; 05 06Node* createCircularList(int n) { 07 Node* head = new Node(1, nullptr); 08 Node* prev = head; 09 for (int i = 2; i <= n; ++i) { 10 Node* node = new Node(i, nullptr); 11 prev->next = node; 12 prev = node; 13 } 14 prev->next = head; 15 return head; 16} 17 18int fingLastSurvival(int n, int k) { 19 Node* head = createCircularList(n); 20 Node* p = head; 21 Node* prev = nullptr; 22 23 while (p->next != p) { 24 for (int count = 1; count < k; ++count) { 25 prev = p; 26 p = p->next; 27 } 28 ____________ 29 } 30 31 cout << "最后留下的人编号是:" << p->data << endl; 32 delete p; 33 34 return 0; 35}
A:先 prev->next=p->next 让前驱跳过待删节点 p,再 delete p 释放,最后 p=prev->next 从后继继续报数。B、C 在 delete 之后仍访问 p,D 删成了 p 的后继节点,均错。
下列 C++ 代码判断一个正整数是否是质数,说法正确的是( )。
01bool is_prime(int n) { 02 if (n <= 1) 03 return false; 04 if (n == 2 || n == 3 || n == 5) 05 return true; 06 if (n % 2 == 0 || n % 3 == 0 || n % 5 == 0) 07 return false; 08 09 int i = 7; 10 int step = 4; 11 int finish_number = sqrt(n) + 1; 12 13 while (i <= finish_number) { 14 if (n % i == 0) 15 return false; 16 i += step; 17 step = 6 - step; 18 } 19 return true; 20}
C:所有大于 3 的质数必为 6k±1 形式,代码从 i=7 起按步长 4、2 交替只试除这类数,前提成立。A 错在 5 已被提前特判返回 true;B 的 n/2 多余;D 会多试除 2~5,时间不同。
下列 C++ 代码用两种方式求解两个正整数的最大公约数,说法错误的是( )。
01int gcd0(int big, int small) { 02 if (big < small) { 03 swap(big, small); 04 } 05 if (big % small == 0) { 06 return small; 07 } 08 return gcd0(small, big % small); 09} 10 11int gcd1(int big, int small) { 12 if (big < small) { 13 swap(big, small); 14 } 15 for (int i = small; i >= 1; --i) { 16 if (big % i == 0 && small % i == 0) 17 return i; 18 } 19 return 1; 20}
D:i=1 时 big%1 与 small%1 均为 0,循环必然返回 1 作正确兜底,无需改为 i>1。gcd0 每轮取模使规模快速减半为 O(logn),gcd1 从 small 逐个下试为 O(n),A、B、C 均正确。
下面的代码用于判断整数 是否是质数,错误的说法是( )。
01bool is_prime(int n) { 02 if (n <= 1) return false; 03 04 int finish_number = static_cast<int>(sqrt(n)) + 1; 05 for (int i = 2; i < finish_number; ++i) { 06 if (n % i == 0) 07 return false; 08 } 09 return true; 10}
D:线性筛 O(n) 优于埃氏筛 O(nloglogn),说埃氏筛效率最高错误。该代码只判单个数、试除到 sqrt(n);若要连续筛出质数,埃氏筛、线性筛确实更快,A、B、C 均正确。
唯一分解定理描述了关于正整数的什么性质?
B:唯一分解定理指任何大于 1 的整数都能唯一分解为质数乘积。A 是哥德巴赫猜想;C 应写成 gcd×lcm=a×b;D 中 2 是偶质数,均不构成该定理。
下面的 C++ 代码,用于求一系数据中的最大值。有关其算法说法错误的是( )。
01int find_max_recursive(const vector<int>& nums, int left, int right) { 02 if (left == right) 03 return nums[left]; 04 05 int mid = left + (right - left) / 2; 06 int left_max = find_max_recursive(nums, left, mid); 07 int right_max = find_max_recursive(nums, mid + 1, right); 08 09 return max(left_max, right_max); 10} 11 12int find_max(const vector<int>& nums) { 13 if (nums.empty()) { 14 throw invalid_argument("输入数组不能为空"); 15 } 16 return find_max_recursive(nums, 0, nums.size() - 1); 17}
C:find_max_recursive 按 mid 将区间一分为二,递归求左右最大值再取 max,是分治递归实现,不是贪心。D 对:它自上而下递归分解,并非按递推公式迭代。
下面的 C++ 代码,用于求一系数据中的最大值。有关其算法说法错误的是( )。
01int find_max(const vector<int>& nums) { 02 if (nums.empty()) { 03 throw invalid_argument("输入数组不能为空"); 04 } 05 06 int max_value = nums[0]; 07 for (int num : nums) { 08 if (num > max_value) { 09 max_value = num; 10 } 11 } 12 return max_value; 13}
D:迭代版只用 max_value 一个变量,额外空间 O(1);递归分治版递归深度 O(logn) 需栈空间,两者空间复杂度不同。A、B、C 说法均正确。
下面的 C++ 代码用于在升序数组 lst 中查找目标值 target 最后一次出现的位置。相关说法,正确的是( )。
01int binary_search_last_occurrence(const vector<int>& lst, int target) { 02 if (lst.empty()) return -1; 03 04 int low = 0, high = lst.size() - 1; 05 06 while (low < high) { 07 int mid = (low + high + 1) / 2; 08 if (lst[mid] <= target) { 09 low = mid; 10 } else { 11 high = mid - 1; 12 } 13 } 14 15 if (lst[low] == target) 16 return low; 17 else 18 return -1; 19}
A:mid=(low+high+1)/2 上取整,lst[mid]<=target 时令 low=mid,保证向最后一个 target 收敛,全相同数组也能退出并返回末尾。target 过小时返回 -1 而非 0;改 (low+high)/2 会因 low=mid 死循环。
有关下面 C++ 代码的说法,错误的是( )。
01double sqrt_binary(long long n, double epsilon = 1e-10) { 02 if (n < 0) { 03 throw invalid_argument("输入必须为非负整数"); 04 } 05 06 if (n == 0 || n == 1) return n; 07 08 // 阶段 1 09 long long low = 1, high = n; 10 long long k = 0; 11 12 while (low <= high) { 13 long long mid = (low + high) / 2; 14 long long mid_sq = mid * mid; 15 16 if (mid_sq == n) { 17 return mid; 18 } else if (mid_sq < n) { 19 k = mid; 20 low = mid + 1; 21 } else { 22 high = mid - 1; 23 } 24 } 25 26 long long next_k = k + 1; 27 if (next_k * next_k == n) { 28 return next_k; 29 } 30 31 // 阶段 2 32 double low_d = (double)k; 33 double high_d = (double)(k + 1); 34 double mid; 35 36 while (high_d - low_d >= epsilon) { 37 mid = (low_d + high_d) / 2; 38 double mid_sq = mid * mid; 39 40 if (mid_sq < n) { 41 low_d = mid; 42 } else { 43 high_d = mid; 44 } 45 } 46 47 double result = (low_d + high_d) / 2; 48 long long check_int = (long long)(result + 0.5); 49 if (check_int * check_int == n) { 50 return check_int; 51 } 52 53 return result; 54}
D:high_d-low_d 从 1 起每轮折半,约 34 轮即小于 1e-10 退出,double 精度足够,不会死循环。A、B、C 分别对应阶段 1 找整数平方根、阶段 2 二分逼近小数根、check_int 修正浮点误差,均正确。
硬币找零问题中要求找给客户最少的硬币。coins 存储可用硬币规格,单位为角,假设规格都小于 角,且一定有 角规格。amount 为要找零的金额,约定必须为 角的整数倍。输出为每种规格及其数量,按规格从大到小输出,如果某种规格不必要,则输出为 。下面是其实现代码,相关说法正确的是( )。
01const int MAX_COINS = 10; 02int result[MAX_COINS] = {0}; // 假设最多10种面额 03 04int find_coins(const vector<int>& coins, int amount) { 05 sort(coins.begin(), coins.end(), greater<int>()); 06 07 int n = coins.size(); 08 09 for (int i = 0; i < n; ++i) { 10 int coin = coins[i]; 11 int num = amount / coin; 12 result[i] = num; 13 amount -= num * coin; 14 if (amount == 0) break; 15 } 16 17 cout << "找零方案如下:" << endl; 18 for (int i = 0; i < n; ++i) { 19 cout << sorted_coins[i] << "角需要" << result[i] << "枚" << endl; 20 } 21 22 return 0; 23}
A:代码将 coins 降序排序后从大面额尽量多取,是贪心策略。B 错:贪心未必最优,如 coins={1,3,4}、amount=6 时贪心得 4+1+1 共 3 枚,而 3+3 只需 2 枚;更非枚举或分治。
关于下述 C++ 代码的快速排序算法,说法错误的是( )。
01int randomPartition(std::vector<int>& arr, int low, int high) { 02 int random = low + rand() % (high - low + 1); 03 std::swap(arr[random], arr[high]); 04 05 int pivot = arr[high]; 06 int i = low - 1; 07 08 for (int j = low; j < high; j++) { 09 if (arr[j] <= pivot) { 10 i++; 11 std::swap(arr[i], arr[j]); 12 } 13 } 14 std::swap(arr[i + 1], arr[high]); 15 return i + 1; 16} 17 18void quickSort(std::vector<int>& arr, int low, int high) { 19 if (low < high) { 20 int pi = randomPartition(arr, low, high); 21 22 quickSort(arr, low, pi - 1); 23 quickSort(arr, pi + 1, high); 24 } 25}
D:快排靠 swap 交换元素,相等元素的相对顺序可能改变,是不稳定排序。A 中 i 标记小于等于 pivot 的边界,B 随机选 pivot 可避开有序输入的最坏 O(n²),C 平均 O(nlogn),均正确。
小杨编写了一个如下的高精度除法函数,则横线上应填写的代码为( )。
01const int MAXN = 1005; // 最大位数 02struct BigInt { 03 int d[MAXN]; // 存储数字,d[0]是个位,d[1]是十位,... 04 int len; // 数字长度 05 06 BigInt() { 07 memset(d, 0, sizeof(d)); 08 len = 0; 09 } 10}; 11 12// 比较两个高精度数的大小 13int compare(BigInt a, BigInt b) { 14 if(a.len != b.len) return a.len > b.len ? 1 : -1; 15 for(int i = a.len - 1; i >= 0; i--) { 16 if(a.d[i] != b.d[i]) return a.d[i] > b.d[i] ? 1 : -1; 17 } 18 return 0; 19} 20 21// 高精度减法 22BigInt sub(BigInt a, BigInt b) { 23 BigInt c; 24 for(int i = 0; i < a.len; i++) { 25 c.d[i] += a.d[i] - b.d[i]; 26 if(c.d[i] < 0) { 27 c.d[i] += 10; 28 c.d[i+1]--; 29 } 30 } 31 c.len = a.len; 32 while(c.len > 1 && c.d[c.len-1] == 0) c.len--; 33 return c; 34} 35 36// 高精度除法 (a/b, 返回商和余数) 37pair<BigInt, BigInt> div(BigInt a, BigInt b) { 38 BigInt q, r; // q是商,r是余数 39 40 if(compare(a, b) < 0) { // 如果a<b,商为0,余数为a 41 q.len = 1; 42 q.d[0] = 0; 43 r = a; 44 return make_pair(q, r); 45 } 46 47 // 初始化余数r为a的前b.len位 48 r.len = b.len; 49 for(int i = a.len - 1; i >= a.len - b.len; i--) { 50 r.d[i - (a.len - b.len)] = a.d[i]; 51 } 52 53 // 逐位计算商 54 for(int i = a.len - b.len; i >= 0; i--) { 55 // 把下一位加入余数 56 if(r.len > 1 || r.d[0] != 0) { 57 for(int j = r.len; j > 0; j--) { 58 r.d[j] = r.d[j-1]; 59 } 60 ____________ 61 } else { 62 r.d[0] = a.d[i]; 63 r.len = 1; 64 } 65 66 // 计算当前位的商 67 while(compare(r, b) >= 0) { 68 r = sub(r, b); 69 q.d[i]++; 70 } 71 } 72 73 // 确定商的长度 74 q.len = a.len - b.len + 1; 75 while(q.len > 1 && q.d[q.len-1] == 0) q.len--; 76 77 // 处理余数前导零 78 while(r.len > 1 && r.d[r.len-1] == 0) r.len--; 79 80 return make_pair(q, r); 81}
A:把下一位并入余数时先将 r 整体右移一位即 r.d[j]=r.d[j-1],再把 a.d[i] 放入 r.d[0] 并 r.len++。B、C、D 的下标或长度处理都会破坏余数结构。
下面 C++ 代码是用欧几里得算法(辗转相除法)求两个正整数的最大公约数,a 大于 b 还是小于 b 都适用。
01int gcd(int a, int b) { 02 while (b) { 03 int temp = b; 04 b = a % b; 05 a = temp; 06 } 07 return a; 08}
对。当 a<b 时第一轮 a%b=a,经 temp=b、b=a、a=temp 后仍等价于对 (b, a%b) 继续辗转相除,与输入大小顺序无关,最终返回的 a 就是最大公约数。
假设函数 gcd() 函数能正确求两个正整数的最大公约数,则下面的 lcm() 函数能求相应两数的最小公倍数。
01int lcm(int a, int b) { 02 return a * b / gcd(a, b); 03}
对。最小公倍数等于两数乘积除以最大公约数,即 lcm(a,b)=a*b/gcd(a,b),gcd 正确时该式恒成立,能求相应两数的最小公倍数。
下面的 C++ 代码用于输出每个数对应的质因数表,输出形如:{5: [5], 6: [2, 3], 7: [7], 8: [2, 2, 2]}。
01int main() { 02 int n, m; 03 cin >> n >> m; 04 if (n > m) swap(n, m); 05 06 map<int, vector<int>> prime_factor; 07 08 for (int i = n; i <= m; ++i) { 09 int j = 2, k = i; 10 while (k != 1) { 11 if (k % j == 0) { 12 prime_factor[i] = prime_factor[i] + j; 13 k /= j; 14 } else { 15 ++j; 16 } 17 } 18 } 19 20 for (auto& p : prime_factor) { 21 cout << p.first << ": "; 22 for (int v : p.second) 23 cout << v << " "; 24 cout << endl; 25 } 26 27 return 0; 28}
错。prime_factor[i]=prime_factor[i]+j 中 vector<int> 与 int 相加没有运算符重载,代码无法编译;且输出是每行「i: 因子序列」,与题述字典形式也不符。
下面的 C++ 代码实现归并排序。代码在执行时,将输出一次 HERE 字符串,因为 merge() 函数仅被调用一次。
01void merge(std::vector<int>& arr, int left, int mid, int right) { 02 std::vector<int> temp(right - left + 1); 03 04 int i = left; 05 int j = mid + 1; 06 int k = 0; 07 08 while (i <= mid && j <= right) { 09 if (arr[i] <= arr[j]) { 10 temp[k++] = arr[i++]; 11 } else { 12 temp[k++] = arr[j++]; 13 } 14 } 15 16 while (i <= mid) { 17 temp[k++] = arr[i++]; 18 } 19 20 while (j <= right) { 21 temp[k++] = arr[j++]; 22 } 23 24 for (int p = 0; p < k; ++p) { 25 arr[left + p] = temp[p]; 26 } 27} 28 29void mergeSort(std::vector<int>& arr, int left, int right) { 30 if (left >= right) { 31 return; 32 } 33 34 int mid = left + (right - left) / 2; 35 36 mergeSort(arr, left, mid); 37 mergeSort(arr, mid + 1, right); 38 39 std::cout << "HERE"; 40 merge(arr, left, mid, right); 41}
错。mergeSort 对每个非平凡区间递归后都输出 HERE 再调用 merge,n 个元素需归并 n-1 次,HERE 会输出多次,并非 merge 仅被调用一次。
归并排序的最好、最坏和平均时间复杂度均为 。
对。归并排序无论输入有序、逆序还是乱序,都按 mid 对半切分,每层合并总代价 O(n)、共 logn 层,最好、最坏、平均复杂度均为 O(nlogn)。
查字典这个小学生必备技能,可以把字典视为一个已排序的数组。假设小杨要查找一个音首字母为 g 的单词,他首先翻到字典约一半的页数,发现该页的首字母是 m ,由于字母表中 g 位于 m 之前,所以排除字典后半部分,查找范围缩小到前半部分;不断重复上述步骤,直至找到首字母为 g 的页码。这种查字典的一系列操作可看作二分查找。
对。字典按字母序排列可看作有序数组,翻到中间页比较首字母,g 在 m 前则排除后半部分,每次比较排除约一半范围,正是二分查找的操作过程。
求解下图中 点到 点最短路径,其中 到 之间的 可以理解为距离。求解这样的问题常用 Dijkstra 算法,其思路是通过逐步选择当前距离起点最近的节点来求解非负权重图(如距离不能为负值)单源最短路径的算法。从该算法的描述可以看出,Dijkstra 算法是贪心算法。
对。Dijkstra 每轮从未确定集合中选当前距起点最近的节点并固定其最短路,每步取局部最优,是贪心算法;要求边权非负以保证局部最优能推出全局最优。
分治算法将原问题可以分解成规模更小的子问题,使得解决问题的难度降低。但由于分治算法需要将问题进行分解,并且需要将多个子问题的解合并为原问题的解,所以分治算法的效率通常比直接求解原问题的效率低。
错。分治拆分子问题常能降低复杂度,如归并排序 O(nlogn) 优于直接两两比较的 O(n²),合并开销并不抵消收益,效率通常更高而非更低。
函数 puzzle 定义如下,则调用 puzzle(7) 程序会无限递归。
01int puzzle(int n) { 02 if (n == 1) return 1; 03 if (n % 2 == 0) return puzzle(n / 2); 04 return puzzle(3 * n + 1); 05}
错。puzzle(7)→puzzle(22)→puzzle(11)→puzzle(34)→…→puzzle(4)→puzzle(2)→puzzle(1) 返回 1,序列按奇偶分别走 3n+1 与 n/2,最终到达出口,不会无限递归。
如下为线性筛法,用于高效生成素数表,其核心思想是每个合数只被它的最小质因数筛掉一次,时间复杂度为 。
01vector<int> linearSieve(int n) { 02 vector<bool> is_prime(n + 1, true); 03 vector<int> primes; 04 05 for (int i = 2; i <= n; ++i) { 06 if (is_prime[i]) { 07 primes.push_back(i); 08 } 09 10 for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) { 11 is_prime[i * primes[j]] = false; 12 if (i % primes[j] == 0) { 13 break; 14 } 15 } 16 } 17 return primes; 18}
对。内层循环在 i%primes[j]==0 时立即 break,保证每个合数只被其最小质因数 primes[j] 筛一次,外层共 n 轮,时间复杂度为 O(n)。