链表不具备的特点是( )。
A:链表结点靠 next 指针链接,访问第 i 个元素必须从头结点逐个遍历,无法随机访问;插入删除只改指针、结点动态申请、存储量与结点数成正比,都是链表具备的特点。
双向链表中每个结点有两个指针域 prev 和 next,分别指向该结点的前驱及后继结点。设 p 指向链表中的一个结点,它的前驱结点和后继结点均非空。要删除结点 p,则下述语句中错误的是( )。
A:p->next->prev=p->next 把后继结点的前驱指向自己,p->prev->next=p->prev 把前驱结点的后继也指向自己,两条链都没接到对方;应让 p 的前驱与后继互相连接,如 B 的 p->prev->next=p->next、p->next->prev=p->prev。
假设双向循环链表包含头尾哨兵结点(不存储实际内容),分别为 head 和 tail,链表中每个结点有两个指针域 prev 和 next,分别指向该结点的前驱及后继结点。下面代码实现了一个空的双向循环链表,横线上应填的最佳代码是( )。
01// 链表结点 02template <typename T> 03struct ListNode { 04 T data; 05 ListNode* prev; 06 ListNode* next; 07 // 构造函数 08 explicit ListNode(const T& val = T()) 09 : data(val), prev(nullptr), next(nullptr) {} 10}; 11 12struct LinkedList { 13 ListNode<T>* head; 14 ListNode<T>* tail; 15}; 16 17void InitLinkedList(LinkedList* list) { 18 list->head = new ListNode<T>; 19 list->tail = new ListNode<T>; 20 ____________ // 在此处填入代码 21}
B:空的双向循环链表只有 head、tail 两个哨兵,应令 head->next=tail、tail->prev=head,使二者互相指连成环且中间无数据结点;A 只连 prev,C 的 tail->next 指向 head 但缺 prev 连接,D 把 tail->next 置空破坏了循环。
用以下辗转相除法(欧几里得算法)求 gcd(84, 60) 的步骤中,第二步计算的数是( )。
01int gcd(int a, int b) { 02 int big = a > b ? a : b; 03 int small = a < b ? a : b; 04 if (big % small == 0) { 05 return small; 06 } 07 return gcd(small, big % small); 08}
B:第一步 84%60=24 不为 0,递归调用 gcd(60,24),第二步处理的就是 60 和 24;继续 60%24=12、24%12=0,最终 gcd 为 12。
根据唯一分解定理,下面整数的唯一分解是正确的( )。
D:唯一分解要求所有因子都是质数;30=2×3×5 中 2、3、5 全为质数且乘积为 30。A 的 6、B 的 4、C 的 6 均为合数,没有分解到质数为止。
下述代码实现素数表的线性筛法,筛选出所有小于等于 n 的素数,横线上应填的最佳代码是( )。
01vector<int> sieve_linear(int n) { 02 vector<bool> is_prime(n + 1, true); 03 vector<int> primes; 04 if (n < 2) return primes; 05 is_prime[0] = is_prime[1] = false; 06 for (int i = 2; i <= n / 2; i++) { 07 if (is_prime[i]) 08 primes.push_back(i); 09 for (int j = 0; ____________ ; j++) { // 在此处填入代码 10 is_prime[i * primes[j]] = false; 11 if (i % primes[j] == 0) 12 break; 13 } 14 } 15 for (int i = n / 2 + 1; i <= n; i++) { 16 if (is_prime[i]) 17 primes.push_back(i); 18 } 19 return primes; 20}
C:内层循环既要保证 j 不越出 primes 数组(j<primes.size()),又要保证 i*primes[j]<=n 使 is_prime 下标不越界,两条件缺一不可;A、B 各自只满足其一仍可能越界,D 的 j<=n 完全错误。
在程序运行过程中,如果递归调用的层数过多,会因为( )引发错误。
A:每次函数调用都会在系统栈压入栈帧保存参数、局部变量和返回地址,递归层数过多会耗尽栈空间造成栈溢出;堆、队列、链表空间与递归调用层数无关。
对下面两个函数,说法错误的是( )。
01int factorialA(int n) { 02 if (n <= 1) return 1; 03 return n * factorialA(n - 1); 04} 05 06int factorialB(int n) { 07 if (n <= 1) return 1; 08 int res = 1; 09 for (int i = 2; i <= n; i++) 10 res *= i; 11 return res; 12}
D:factorialB 用 for 循环从 2 累乘到 n,属于迭代实现而非递归;factorialA 靠 return n*factorialA(n-1) 调用自身才是递归,两函数功能相同且复杂度均为 O(n)。
下算法中,( )是不稳定的排序。
A:选择排序每趟将最小元素与未排序区首位交换,交换可能跨越相同值元素,使相等元素的相对顺序改变,故不稳定;冒泡、插入、归并均只相邻交换或按序合并,保持稳定。
考虑以下 C++ 代码实现的快速排序算法,将数据从小到大排序,则横线上应填的最佳代码是( )。
01int partition(vector<int>& arr, int low, int high) { 02 int pivot = arr[high]; // 基准值 03 int i = low - 1; 04 for (int j = low; j < high; j++) { 05 ____________ // 在此处填入代码 06 } 07 swap(arr[i + 1], arr[high]); 08 return i + 1; 09} 10 11// 快速排序 12void quickSort(vector<int>& arr, int low, int high) { 13 if (low < high) { 14 int pi = partition(arr, low, high); 15 quickSort(arr, low, pi - 1); 16 quickSort(arr, pi + 1, high); 17 } 18}
B:Lomuto 分区中 i 指向小于基准区间的末尾,扫描 j 时若 arr[j]<pivot 则先 i++ 再 swap(arr[i],arr[j]),把小数换到左侧;A 判大于方向反,C 先交换后自增位置错,D 只处理相等元素。
若用二分法在 内猜数,最多需要猜( )次。
C:每次猜数把候选区间缩小一半,[1,100] 共 100 个数,2^6=64 不够覆盖、2^7=128 足够,故最多需要 7 次(ceil(log2(100))=7)。
下面代码实现了二分查找算法,在数组 arr 找到目标元素 target 的位置,则横线上能填写的最佳代码是( )。
01int binarySearch(int arr[], int left, int right, int target) { 02 while (left <= right) { 03 ____________ // 在此处填入代码 04 if (arr[mid] == target) 05 return mid; 06 else if (arr[mid] < target) 07 left = mid + 1; 08 else 09 right = mid - 1; 10 } 11 return -1; 12}
A:mid 应为区间中点,left+(right-left)/2 是 (left+right)/2 的防溢出写法,避免 left+right 超过 int 上限,是二分查找的标准最佳写法;B、D 把 mid 设为端点无法对半分区间。
贪心算法的核心特征是( )。
A:贪心的核心是每一步都依据当前状态选择局部最优解,不回溯、不整体规划;回溯对应穷举所有可能,分阶段解决子问题是动态规划思路,且贪心并不总能保证全局最优。
函数 int findMax(int arr[], int low, int high) 计算数组中最大元素,其中数组 arr 从索引 low 到 high,( )正确实现了分治逻辑。
D:递归出口为 low==high 时返回 arr[low],mid 取区间中点后分别递归求左半、右半最大值,再返回两者中较大者;A 只返回 arr[mid],B 求和、C 求积,均不能得到最大值。
小杨编写了一个如下的高精度乘法函数,则横线上应填写的代码为( )。
01vector<int> multiply(vector<int>& a, vector<int>& b) { 02 int m = a.size(), n = b.size(); 03 vector<int> c(m + n, 0); 04 // 逐位相乘,逆序存储 05 for (int i = 0; i < m; i++) { 06 for (int j = 0; j < n; j++) { 07 c[i + j] += a[i] * b[j]; 08 } 09 } 10 // 处理进位 11 int carry = 0; 12 for (int k = 0; k < c.size(); ++k) { 13 ____________ // 在此处填入代码 14 c[k] = temp % 10; 15 carry = temp / 10; 16 } 17 while (c.size() > 1 && c.back() == 0) 18 c.pop_back(); 19 return c; 20}
B:处理第 k 位需把上一位传来的进位 carry 与当前位 c[k] 相加得 temp,再令 c[k]=temp%10、carry=temp/10 传给下一位;A 丢掉了 carry,C、D 的运算方向错误。
要删除单链表中某个结点 p(非尾结点),但不知道头结点,可行的操作是将 p->next 的数据拷贝到 p 的数据,将 p->next 设置为 p->next->next,然后删除 p->next。
对。把后继结点 p->next 的数据拷入 p 覆盖原值,再令 p->next 指向 p->next->next 并删除原后继,等效删除 p 本身;因 p 非尾结点,后继必存在,全程只需 p 一个指针,无需头结点。
链表存储线性表时要求内存中可用存储单元地址是连续的。
错。链表结点用 new 逐个动态分配,各结点在内存中的地址可以任意分散,靠 next/prev 指针把逻辑相邻的结点串起来即可,不要求存储单元地址连续;要求地址连续的是数组。
线性筛相对于埃拉托斯特尼筛法,每个合数只会被它的最小质因数筛去一次,因此效率更高。
对。线性筛在 i%primes[j]==0 时立即 break,保证每个合数只被其最小质因数筛掉一次,时间复杂度 O(n);埃氏筛同一合数会被多个质数重复标记,为 O(n log log n),故线性筛效率更高。
贪心算法通过每一步选择当前最优解,从而一定能获得全局最优解。
错。贪心每步只选当前局部最优,局部最优叠加未必得到全局最优,还需满足贪心选择性质并证明;例如部分背包之外的某些问题按局部贪心会错失全局最优解,故该说法过于绝对。
递归函数必须具有一个终止条件,以防止无限递归。
对。递归必须设终止条件(base case),且每次调用要向出口逼近;若无终止条件,函数会无限调用自身,栈帧不断压栈,最终导致栈溢出程序崩溃。
快速排序算法的时间复杂度与输入是否有序无关,始终稳定为 。
错。快排只有平均复杂度为 O(n log n);当输入已有序且基准总取到最大或最小值时,划分严重失衡,递归深度达 n,退化为 O(n²),故其复杂度与输入有序性有关。
归并排序算法的时间复杂度与输入是否有序无关,始终稳定为 。
对。归并排序始终将区间对半拆分,递归 log n 层,每层两两合并的总代价为 O(n),与输入是否有序无关,所以最好、最坏、平均都是 O(n log n)。
二分查找适用于对无序数组和有序数组的查找。
错。二分查找每次按 arr[mid] 与 target 的大小关系决定进入左半还是右半区间,这要求数组必须有序;无序数组无法据此排除一半元素,只能退化为顺序查找。
小杨有 元去超市买东西,每个商品有各自的价格,每种商品只能买 个,小杨的目标是买到最多数量的商品。小杨采用的策略是每次挑价格最低的商品买,这体现了分治思想。
错。每次挑价格最低的商品买是典型的贪心策略,每步做当前最优选择以买到最多件数;分治是把大问题拆成独立子问题分别求解再合并,与小杨的购买策略无关。
归并排序算法体现了分治算法,每次将大的待排序数组分成大小大致相等的两个小数组,然后分别对两个小数组进行排序,最后对排好序的两个小数组合并成有序数组。
对。归并排序把区间从中点一分为二,递归排序两个子数组,最后用合并操作把两个有序子数组合并,正是分治的分解、求解、合并三步,且每次拆分大小大致相等。