对如下定义的循环单链表,横线处填写( )。
01// 循环单链表的结点 02struct Node { 03 int data; // 数据域 04 Node* next; // 指针域 05 06 Node(int d) : data(d), next(nullptr) {} 07}; 08 09// 创建一个只有一个结点的循环单链表 10Node* createList(int value) { 11 Node* head = new Node(value); 12 head->next = head; 13 return head; 14} 15 16// 在循环单链表尾部插入新结点 17void insertTail(Node* head, int value) { 18 Node* p = head; 19 while (p->next != head) { 20 p = p->next; 21 } 22 Node* node = new Node(value); 23 node->next = head; 24 p->next = node; 25} 26 27// 遍历并输出循环单链表 28void printList(Node* head) { 29 if (head == nullptr) return; 30 31 Node* p = head; 32 ____________ // 在此处填入代码 33 cout << endl; 34}
C:循环链表尾结点的 next 指回 head,p 永远非空,须用 do-while 先输出 head 再后移,直到 p 回到 head 停止;A/B/D 的退出条件依赖 nullptr 永不成立,会死循环。
区块链技术是比特币的基础。在区块链中,每个区块指向前一个区块,构成链式列表,新区块只能接在链尾,不允许在中间插入或删除。下面代码实现插入区块添加函数,则横线处填写( )。
01//区块(节点) 02struct Block { 03 int index; // 区块编号(高度) 04 string data; // 区块里保存的数据 05 Block* prev; // 指向前一个区块 06 07 Block(int idx, const string& d, Block* p) : index(idx), data(d), prev(p) {} 08}; 09 10// 区块链 11struct Blockchain { 12 Block* tail; 13 14 // 初始化 15 void init() { 16 tail = new Block(0, "Genesis Block", nullptr); 17 } 18 19 // 插入新区块 20 void addBlock(const string& data) { 21 ____________ // 在此处填入代码 22 } 23 24 // 释放内存 25 void clear() { 26 Block* cur = tail; 27 while (cur != nullptr) { 28 Block* p = cur->prev; 29 delete cur; 30 cur = p; 31 } 32 tail = nullptr; 33 } 34};
B:新区块 index 应为 tail->index+1,prev 指向当前 tail 再接在链尾,然后 tail=newBlock 更新链尾;A 把 tail 退回 newBlock->prev,C、D 的 prev 指向 tail->prev 都断开了链。
下面关于单链表和双链表的描述中,正确的是( )。
01struct DNode { 02 int data; 03 DNode* prev; 04 DNode* next; 05}; 06 07// 在双链表中删除指定节点 08void deleteNode(DNode* node) { 09 if (node->prev) { 10 node->prev->next = node->next; 11 } 12 if (node->next) { 13 node->next->prev = node->prev; 14 } 15 delete node; 16} 17 18struct SNode { 19 int data; 20 SNode* next; 21}; 22 23// 在单链表中删除指定节点 24void deleteSNode(SNode* head, SNode* node) { 25 SNode* prev = head; 26 while (prev->next != node) { 27 prev = prev->next; 28 } 29 prev->next = node->next; 30 delete node; 31}
C:双链表 deleteNode 用 node->prev 与 node->next 直接改前后指针,O(1);单链表 deleteSNode 需 while 循环从 head 找到 node 的前驱 prev 才能断开,O(n)。
假设我们有两个数 和 ,它们对模 同余,即 。以下哪个值不可能是 ?
D:a≡b(mod m) 等价于 m 整除 a−b=38−14=24,24 的约数有 1、2、3、4、6、8、12、24,其中 3、4、6 均可,9 不能整除 24,故 m 不可能是 9。
下面代码实现了欧几里得算法。下面有关说法,错误的是( )。
01int gcd1(int a, int b) { 02 return b == 0 ? a : gcd1(b, a % b); 03} 04 05int gcd2(int a, int b) { 06 while (b != 0) { 07 int temp = b; 08 b = a % b; 09 a = temp; 10 } 11 return a; 12}
D:gcd1 每层递归都压栈保存现场,额外空间与调用开销均大于 gcd2 的迭代循环,a 较大时不可能更快;A、B、C 分别正确描述递归、迭代与辅助空间,故选 D。
唯一分解定理描述的内容是( )。
B:算术基本定理——任何大于 1 的整数都能唯一分解为质数幂的乘积,合数自然成立;A 是未证实的哥德巴赫猜想,C 中 gcd 与 lcm 公式颠倒,D 忽略了偶素数 2。
下述代码实现素数表的线性筛法,筛选出所有小于等于 的素数,则横线上应填的代码是( )。
01vector<int> linear_sieve(int n) { 02 vector<bool> is_prime(n +1, true); 03 vector<int> primes; 04 05 is_prime[0] = is_prime[1] = 0; //0和1两个数特殊处理 06 for (int i = 2; i <= n; ++i) { 07 if (is_prime[i]) { 08 primes.push_back(i); 09 } 10 ____________ { // 在此处填入代码 11 is_prime[ i * primes[j] ] = 0; 12 if (i % primes[j] == 0) 13 break; 14 } 15 } 16 17 return primes; 18}
A:j 从 0 扫到 primes.size()-1,并以 i×primes[j]≤n 限制越界;用 if(i%primes[j]==0) break 保证每个合数只被其最小质因子筛一次,这正是线性筛 O(n) 的关键。
下列关于排序的说法,正确的是( )。
B:归并合并时左半元素优先取出,相等元素相对顺序不变,稳定;快排划分时交换破坏稳定性,插入排序稳定,冒泡只交换相邻元素、无需额外数组属原地排序。
下面代码实现了归并排序。下述关于归并排序的说法中,不正确的是( )。
01void merge(vector<int>& arr, vector<int>& temp, int l, int mid, int r) { 02 int i = l, j = mid + 1, k = l; 03 while (i <= mid && j <= r) { 04 if (arr[i] <= arr[j]) temp[k++] = arr[i++]; 05 else temp[k++] = arr[j++]; 06 } 07 while (i <= mid) temp[k++] = arr[i++]; 08 while (j <= r) temp[k++] = arr[j++]; 09 for (int p = l; p <= r; p++) arr[p] = temp[p]; 10} 11 12void mergeSort(vector<int>& arr, vector<int>& temp, int l, int r) { 13 if (l >= r) return; 14 int mid = l + (r - l) / 2; 15 mergeSort(arr, temp, l, mid); 16 mergeSort(arr, temp, mid + 1, r); 17 merge(arr, temp, l, mid, r); 18}
C:归并排序最坏、平均时间都是 O(n log n),不存在最坏 O(n²);它合并时需 O(n) 的 temp 辅助数组,稳定且对大规模数据有最坏复杂度保证,故 A、B、D 正确,不正确的是 C。
下述 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}
C:以首元素 arr[low] 为枢轴,当数组已有序或逆序时每次划分只分出单个元素,partition 累计约 n² 次比较且递归深度 n,故最坏时间复杂度 O(n²)。
下面代码尝试在有序数组中查找第一个大于等于 的元素位置。如果没有大于等于 的元素,返回 arr.size()。以下说法正确的是( )。
01int lower_bound(vector<int>& arr, int x) { 02 int l = 0, r = arr.size(); 03 while(l < r) { 04 int mid = l + (r - l) / 2; 05 if(arr[mid] >= x) r = mid; 06 else l = mid + 1; 07 } 08 return l; 09}
A:这是左闭右开写法,l<r、满足条件时 r=mid,正确返回第一个 arr[mid]≥x 的位置;若全部小于 x,l 会推进到 arr.size() 后退出循环,正合题意。
小杨要把一根长度为 的木头切成 段,使得每段长度小于等于 。已知每切一刀只能把一段木头分成两段,他用二分法找到满足条件的最小 ( 为正整数),则横线处应填写( )。
01// 判断:在不超过 K 次切割内,是否能让每段长度 <= x 02bool check(int L, int K, int x) { 03 int cuts = (L - 1) / x; 04 return cuts <= K; 05} 06 07// 二分查找最小可行的 x 08int binary_cut(int L, int K) { 09 int l = 1, r = L; 10 while (l < r) { 11 int mid = l + (r - l) / 2; 12 ____________ // 在此处填入代码 13 } 14 return l; 15} 16 17int main() { 18 int L = 10; // 木头长度 19 int K = 2; // 最多切 K 刀 20 21 cout << binary_cut(L, K) << endl; 22 return 0; 23}
A:check 为真说明 x 可行但可再小,r=mid 收缩;为假则 l=mid+1。代入 L=10、K=2:x=4 时 cuts=(10−1)/4=2 可行,x=3 时 cuts=3 不可行,输出 4。
下面给出了阶乘计算的两种方式。以下说法正确的是( )。
01int factorial1(int n) { 02 if (n <= 1) return 1; 03 return n * factorial1(n - 1); 04} 05 06int factorial2(int n) { 07 int acc = 1; 08 while (n > 1) { 09 acc = n * acc; 10 n = n - 1; 11 } 12 return acc; 13}
A:两函数都只执行 n−1 次乘法,时间同为 O(n);但 factorial1 递归需 O(n) 栈空间,factorial2 只用 acc 变量为 O(1),故 B、C 错,D 的 O(2^n) 无依据。
给定有 个任务,每个任务有截止时间和利润,每个任务耗时 个时间单位,必须在截止时间前完成,且每个时间槽最多做 个任务。为了在规定时间内获得最大利润,可以采用贪心策略,即按利润从高到低排序,尽量安排,则横线处应填写( )。
01struct Task { 02 int deadline; //截止时间 03 int profit; //利润 04}; 05 06void sortByProfit(vector<Task>& tasks) { 07 sort(tasks.begin(), tasks.end(), 08 [](const Task& a, const Task& b) { 09 return a.profit > b.profit; 10 }); 11} 12 13int maxProfit(vector<Task>& tasks) { 14 sortByProfit(tasks); 15 16 int maxTime = 0; 17 for (auto& t : tasks) { 18 maxTime = max(maxTime, t.deadline); 19 } 20 21 vector<bool> slot(maxTime + 1, false); 22 int totalProfit = 0; 23 24 for (auto& task : tasks) { 25 for (int t = task.deadline; t >= 1; t--) { 26 if (!slot[t]) { 27 ____________ // 在此处填入代码 28 break; 29 } 30 } 31 } 32 33 return totalProfit; 34}
A:找到 deadline 前最近的空槽 t 后,置 slot[t]=true 占住槽位防止复用,并把 totalProfit += task.profit 累加;B 置 false 会重复占用,C/D 用赋值会漏掉前面任务的利润。
下面代码实现了对两个数组表示的正整数的高精度加法(数组低位在前),则横线上应填写( )。
01vector<int> add(vector<int> a, vector<int> b) { 02 vector<int> c; 03 int carry = 0; 04 05 for (int i = 0; i < a.size() || i < b.size(); i++) { 06 if (i < a.size()) carry += a[i]; 07 if (i < b.size()) carry += b[i]; 08 ____________ // 在此处填入代码 09 } 10 if (carry) c.push_back(carry); 11 12 return c; 13}
B:carry 累加 a[i]、b[i] 后,本位数字为 carry%10 存入 c,进位 carry/=10 留给下一轮;循环结束若 carry 非零再补最高位。A 把商当本位,D 未取模,均错。
数组和链表都是线性表。链表的优点是插入删除不需要移动元素,并且能随机查找。
错。链表插入删除只需修改指针、不移动元素,这点正确;但链表不能随机查找,访问第 k 个结点必须从 head 开始逐个遍历,O(1) 随机访问是数组按下标取值的特性,故后半句错误。
假设函数 gcd() 函数能正确求两个正整数的最大公约数,则下面的 lcm(a, b) 函数能正确找到两个正整数 和 的最小公倍数。
01int lcm(int a, int b) { 02 return a / gcd(a, b) * b; 03}
对。最小公倍数公式 lcm=a×b÷gcd(a,b),先算 a/gcd(a,b) 得整数再乘 b,结果正确且避免 a×b 先溢出;当 gcd=1 时 lcm=a×b 也成立。
在单链表中,已知指针 指向要删除的结点(非尾结点),想在 删除 ,可行做法是用 p->next 覆盖 的值与 next,然后删除 p->next。
对。把 p->next 结点的 data 和 next 复制给 p,再 delete p->next,效果等价于删掉 p 所指结点且 O(1);因 p 非尾结点,p->next 必存在,此技巧不能用于尾结点。
在求解所有不大于 的素数时,线性筛法(欧拉筛)都应当优先于埃氏筛法使用,因为线性筛法的时间复杂度为 ,低于埃氏筛法的 。
错。线性筛理论 O(n) 虽优于埃氏筛 O(n log log n),但埃氏筛实现简单、常数小,n 不大时实际更快更常用,故不能断言任何情况都应优先采用线性筛。
二分查找仅适用于有序数据。若输入数据无序,当仅进行一次查找时,为了使用二分而排序通常不划算。
对。二分查找要求数据有序,无序时无法根据中点值与 x 的大小决定舍弃哪一侧;仅查一次时先排序要 O(n log n),超过直接线性扫描的 O(n),故不划算。
通过在数组的第一个、最中间和最后一个这 个数据中选择中间值作为枢轴(比较基准),快速排序算法可降低落入最坏情况的概率。
对。取首、中、尾三数中值作枢轴,可避免输入有序或逆序时以端点作枢轴导致的极不平衡划分,从而降低快排落入最坏情况 O(n²) 的概率。
贪心算法在每一步都做出当前看来最优的局部选择,并且一旦做出选择就不再回溯;而分治算法将问题分解为若干子问题分别求解,再将子问题的解合并得到原问题的解。
对。贪心每一步都做当前最优的局部选择且选择后不再回溯;分治把原问题分解为若干相互独立的子问题分别求解,再合并各子问题的解得到原问题的解,两句话分别准确刻画了两种算法的核心思想。
以下 fib 函数计算第 项斐波那契数(fib(0)=0,fib(1)=1),其时间复杂度为 。
01int fib(int n) { 02 if (n <= 1) return n; 03 return fib(n-1) + fib(n-2); 04}
错。fib(n) 递归会同时调用 fib(n-1) 与 fib(n-2),大量子问题被重复计算,调用次数按斐波那契数增长即 O(2^n) 指数级,不是 O(n);要降到 O(n) 需记忆化或改递推。
递归函数一定要有终止条件,否则可能会造成栈溢出。
对。递归若无终止条件会无限调用自身,每次调用都在系统栈压入新栈帧,深度无界,最终栈空间耗尽即发生栈溢出,故递归必须有终止条件。
使用贪心算法解决问题时,通过对每一步求局部最优解,最终一定能找到全局最优解。
错。局部最优组合起来不一定得到全局最优,须证明贪心选择性质与最优子结构;如 0-1 背包按价值密度贪心只能得次优解,故不能保证一定找到全局最优解。