PTA 实验2-1 求链式线性表的倒数第 m 项(C++版)
PTA 实验2-1 求链式线性表的倒数第 m 项(C++版)
zhangzhang实验2-1 求链式线性表的倒数第 m 项(C++版)
分数 20
作者 刘凯源
单位 华东师范大学
请设计时间和空间上都尽可能高效的算法,求链式存储的线性表的倒数第m(>0)个元素。
函数接口定义:
1 | template <typename ElemType> |
其中LinkedList结构定义如下
1 | template <typename ElemType> |
函数接口定义中,ElemType是用户定义的数据类型,例如 int、double 或者 char等。函数 Find 的功能是,返回带空头结点的单链表 list 的倒数第 m 个元素的值,并且不得对 list 做任何改变。如果这样的元素不存在,则返回一个错误标志 ERROR(这个标志的值由裁判程序定义)。
裁判测试程序样例:
1 | int main() |
输入样例:
在这里给出一组输入。例如:
1 | 5 |
输出样例:
在这里给出相应的输出。例如:
1 | 4 |
我的答案(部分正确)
1 | template <typename ElemType> |
| 测试点 | 提示 | 内存(KB) | 用时(ms) | 结果 | 得分 | |
|---|---|---|---|---|---|---|
| 0 | 同sample,m小于长度,返回的元素在中间 | 560 | 2 | 答案正确 | 10 / 10 | |
| 1 | m等于长度 | 336 | 2 | 答案正确 | 2 / 2 | |
| 2 | 最小m | 512 | 2 | 答案正确 | 1 / 1 | |
| 3 | m大于长度,空链表 | 556 | 2 | 答案错误 | 0 / 1 | |
| 4 | 大规模数据,用于效率比较 | 4296 | 18 | 答案正确 | 6 / 6 |
Ai答案(正确代码)
1 | template <typename ElemType> |
Summary
该题思路
这段代码采用双指针(快慢指针)法查找链表倒数第 m 项,核心思路:
- 定义两个指针 fast(快指针)和 slow(慢指针),均从第一个数据节点(head_->next_)出发。
- 快指针 fast 先向前移动 m 步:
- 若移动前 m 步时 fast 为空,说明链表长度小于 m,抛出异常(如 “The list has fewer than m elements.”)。
- 若 m ≤ 0,属于无效输入,抛出异常(如 “m must be a positive integer.”)。
- 快慢指针同步向前移动,直到快指针 fast 为空。
- 此时慢指针 slow 指向倒数第 m 个节点,返回其数据。
该方法仅需一次遍历,时间复杂度 O (n),空间复杂度 O (1),高效且不修改原链表。
问题:
一、自定义异常结构体
1 | struct ListException // 异常处理 |
这段代码是自定义异常结构体的实现,用于封装链表操作中的错误信息;而throw的执行逻辑是抛出一个该结构体的实例,而非单纯执行括号里的内容。下面分两部分详细解释:
1.struct ListException 的用法与效果
它的核心作用是将错误信息 “打包”,让异常抛出时能携带具体的错误描述(比如 “m 值无效”“链表为空”),而非仅告知 “发生了错误”。
1. 1关键语法与作用
| 语法 / 成员 | 作用 |
|---|---|
std::string error_ |
存储具体错误信息的变量,比如字符串 “m must be a positive integer”。 |
explicit ListException(...) |
带参构造函数,用于初始化 error_;explicit 禁止隐式类型转换(避免意外错误)。 |
std::move(std::move(s)) |
两次std::move是冗余写法(实际一次即可),核心是将参数s的内存 “转移” 给error_,避免字符串复制,提高效率(尤其当错误信息较长时)。 |
1.2.如何达到 “携带错误信息” 的效果
当链表操作出错时,通过 throw ListException("具体错误描述") 创建一个 ListException 自定义异常结构体的实例 —— 此时构造函数会把括号里的错误字符串,通过 std::move 存入实例的 error_ 变量中;之后外部代码用 catch 捕获这个实例,就能通过 实例.error_ 读取到具体错误信息(比如裁判程序中 cout << e.error_ 输出错误)。
2.throw 执行逻辑:不是 “执行括号里的东西”,而是 “抛出括号里的实例”
throw ListException("错误信息") 的执行过程分两步:
- 创建实例:先执行
ListException("错误信息"),调用构造函数生成一个ListException结构体对象,对象的error_已存入 “错误信息”。 - 抛出实例:
throw会暂停当前函数执行,将这个刚创建的ListException实例 “抛” 给上层代码;上层若有try-catch(ListException e),就会捕获这个实例,后续通过e访问错误信息(如e.error_)。
简单说:throw 括号里是 “要抛出的异常对象”,执行 throw 时会先创建这个对象,再将它传递给 catch 块处理。
3.总结
ListException是 “错误信息的容器”,通过构造函数打包错误描述;throw ListException("xxx")是 “抛出装有错误信息的容器实例”,让上层代码能精准捕获并处理具体错误;- 最终达到 “异常不仅能被检测,还能告知具体错误原因” 的效果。
二、 LinkedList<ElemType>
LinkedList 中的 <ElemType> 是模板参数的显式指定,核心原因是 LinkedList 是一个类模板(而非普通类),必须通过模板参数确定具体的类类型后,才能访问它的成员函数 Find。
1. 先明确前提:LinkedList 是 “类模板”,不是普通类
代码开头的 template class LinkedList 定义了 LinkedList 是一个类模板—— 它是一个 “类的蓝图”,本身不对应具体的类,需要传入一个具体类型(比如 int、double)才能生成实际可用的类。
举个例子:
- 当传入
int时,会生成LinkedList这个具体的类(可理解为 “存储 int 类型的链表类”); - 当传入
double时,会生成LinkedList这个具体的类(“存储 double 类型的链表类”); - 而
LinkedList本身不是一个能直接使用的类,无法直接写LinkedList::Find。
2. 为什么成员函数定义要写 LinkedList::Find
Find 是 LinkedList 类模板的成员函数,当在类模板外部定义成员函数时(比如你的代码中 Find 是在 class LinkedList 外部实现的),必须明确告诉编译器:“这个 Find 函数属于哪个具体的类模板实例”。
这里的 <ElemType> 就是 将成员函数与类模板的模板参数关联起来
“我定义的 Find 函数,是属于‘以 ElemType 为模板参数的 LinkedList 类’的成员函数”—— 这样编译器才能正确匹配类模板的成员,比如访问类内的 ListNode (其data_类型也是ElemType)head_等成员。
3.总结
LinkedList是类模板,需要<模板参数>才能生成具体类;- 外部定义成员函数时,
LinkedList::是 “语法规定”,用于绑定函数与类模板的参数,确保编译器能识别函数所属的类模板实例,避免类型混淆。


