数据结构与算法分析C++第四版参考答案的高效运用与代码改造指南
简介《数据结构与算法分析C语言描述第四版》参考答案与源码包面向正在学习数据结构和算法的C开发者、计算机专业学生及考研备考者。内容覆盖数组、链表、栈、队列、树、图等经典结构以及排序、查找、最短路径、最小生成树等核心算法并基于C11及后续标准实现。压缩包共100个文件其中63个cpp源码文件对应各章练习题的完整实现22个h头文件支撑数据结构封装12个docx文档提供答案解析与思路说明另有少量辅助文件整体仅4.65MB便于随身携带查看。已有3725人学习下载适合配合原书逐题练习、对照验证和调试。读者可借此理解算法的时间与空间复杂度分析掌握STL、智能指针等现代C特性并借助源码中的测试用例培养调试能力为后续软件开发打下扎实基础。1. 数据结构与算法分析C语言描述第四版参考答案这本书的答案到底该怎么用备考数据结构的人几乎人手一本《数据结构与算法分析C语言描述第四版》。这本书习题量大、难度跨度也大偏偏很多读者拿到的“参考答案”要么只覆盖零散几章要么和第二版、第三版题号对不上。于是“数据结构与算法分析C语言描述第四版参考答案”这几个字就成了很多人反复搜索、下载、硬盘里存了好几个版本却始终没真正用起来的资源。我的看法很直接参考答案的正确用法不是当“答案”抄而是当“对照表”——先自己写写不下去再看看完合上书重写一遍。这篇文章不会给你变出一个万能答案包但会把“拿到答案之后怎么用”这条路走通包括怎么在 VS Code 里把答案代码跑起来、怎么把书上的类模板改成能复用的算法库以及对答案过程中最容易翻车的几个坑。适合正在备战考研数据结构、期末复习或者想系统补 C 数据结构功底的人。2. 把参考答案当“镜子”用先理清第四版的习题结构与自查顺序2.1 第四版与旧版的差别为什么你手里的答案经常对不上题号Weiss 这本书从第二版到第四版章节顺序和习题编号都有调整。网上流传的许多“参考答案”实际是第三版教辅或某位老师早年整理的第二版题解被反复复制转载后文件名写成了第四版。你拿着它做第四版习题前几章可能还能对上做到树、图部分题号和题目内容就开始错位这是很多读者觉得“答案没用”的第一个原因。验证一份答案是否对应第四版我一般拿第 3 章的“表、栈与队列”和第 7 章的“排序”做试金石。第四版里这两个章节的习题量、难度层级和代码题占比都很稳定如果第 3 章的第 8 题、第 7 章的第 10 题题目内容和课本对不上这份答案基本上就可以放弃。还有一个更快的办法看它有没有包含“不相交集”这一章的题解。第四版把不相交集独立成章而旧版往往只是附录里的一个小节如果答案里完全没有这一章的代码基本可以判断版本不匹配。判断答案质量时我会看三个细节第一哈希表冲突处理的实现用的是分离链接法还是线性探测法两种都常见的版本但第四版正文以分离链接为主第二二叉堆的下滤操作第四版强调“空穴”下滤好的答案会体现这个抽象过程第三图算法里是否使用优先队列实现 Dijkstra第四版明确用了二叉堆优化。三个细节有两项符合这份答案基本可信一项都不符合就算文件名写着第四版也建议弃用。2.2 按章节给习题归类哪些题值得手写哪些题只配看答案我的习惯是把这本书的习题分成“动手默写类”和“原理推演类”两类分类标准很简单这道题能不能直接变成面试手撕代码或者能不能直接变成数据结构实验报告里的核心代码。能就手写不能就看答案、梳理思路。章节核心内容建议投入方式第 1 章 数学预备级数、对数、模运算看答案推导一遍第 2 章 算法分析大 O 计算、递归求解手写复杂度推导第 3 章 表、栈与队列链表、双向链表、栈、队列必须手写第 4 章 树BST、AVL、伸展树必须手写第 5 章 散列哈希函数、冲突处理看答案 手写核心代码第 6 章 优先队列二叉堆、左式堆必须手写第 7 章 排序插入、希尔、堆排序、归并、快排必须手写这是重头戏第 8 章 不相交集并查集看答案 默写第 9 章 图拓扑排序、最短路径、最小生成树必须手写邻接表版第 10 章 算法设计技巧分治、动态规划、贪心重思路看答案第 11 章 摊还分析势能法、记账法看答案推导第 12 章 高级结构红黑树、后缀数组、跳跃表按需阅读这个分类对应到考试场景里很直接。考研数据结构和期末复习选择题考的是原理应用题考的是堆排序、归并排序、Dijkstra 这些“有标准流程”的东西代码题考的就是第 3、4、6、7、9 章的核心结构。把这些章的习题当成数据结构习题集整章刷比零散对答案有效得多。第 1、2、11 章这类偏数学推导的章节不需要在 IDE 里跑通代码用纸笔把答案的推导步骤遮住重推一遍就够了。2.3 自查流程从“看懂答案”到“关上答案能默写”把答案看懂和把答案内化成自己的代码能力中间隔着至少三遍复现。我平时带人的时候会要求按这个流程走第一遍完全不看答案自己动手写写不出来就翻教材正文把教材示例当成“活字典”但不用参考答案第二遍写完一套题后再翻开参考答案逐行对照重点标记三样东西——数据结构选型、边界条件处理、复杂度数得对不对第三遍合上答案把刚才对照时发现的问题抽出来单独默写。对照时不要只看“结果对不对”要看“边界长得像不像”。同一道题参考答案里循环是while (i n)你写的是while (i n)可能测试数据都过但它俩在空表、单节点表上的行为完全不同。我习惯在代码边上打三个标记边界标记B、复杂度标记O()、结构选型标记S。默写时只重复这三类关键行不用从第一行抄到最后一行的代码——你所需要的是思路主线清晰而不是把答案代码背下来。做完一遍之后还有一步把默写出来的代码喂给 3 到 5 组边界数据。比如栈的 pop 在空栈时行为、快排对已经有序的数组表现、删除 BST 节点时左右子树都为空的情况。这一步是“看懂答案”和“真正会用答案”的分水岭也是最容易被跳过的一步。很多人真题做了两三遍问起来都说“我看过答案了懂了”一到考场还是写不出来问题就出在这一步被省掉了。3. 在 VS Code 里把参考答案跑起来最小 C 工程配置与模板类编译3.1 装好编译器并配置任务vscode 配置 C/C 环境的最小步骤很多人拿到答案代码双击运行系统提示缺少运行库就以为装个 Visual C Redistributable 就能搞定。这里有个非常常见的误解运行库只是让已经编译好的 exe 跑起来它不给你编译器。真正要编译运行参考答案你需要装的是 MinGW-w64 或者 MSVC它们才是把.cpp变成.exe的工具。我平时在 Windows 上常用 MinGW-w64装完之后先打开命令行验证。g --version g -stdc17 -Wall -Wextra -g main.cpp -o main ./main这段命令的作用是第一行确认编译器在 PATH 里第二行用 C17 标准编译main.cpp-Wall和-Wextra开启全部常见警告-g生成调试信息-o main指定输出文件名第三行直接运行。如果你连g --version都报错说明编译器没装或没加进 PATH此时先别急着调试代码把环境问题解决。在 VS Code 里跑通单个文件还需要一个构建任务。.vscode/tasks.json里配置g命令CtrlShiftB 就能触发编译。这个文件基本是固定的直接套用即可核心参数解释如下command: g指定编译工具args里的${file}指当前打开的源文件${fileDirname}\\${fileBasenameNoExtension}.exe是指把可执行文件生成到当前文件同目录下名字与源文件相同。这里要注意args 数组里字符串的书写格式Windows 下路径分隔符建议写成\\。{ version: 2.0.0, tasks: [ { label: C 编译当前文件, type: shell, command: g, args: [ -stdc17, -Wall, -Wextra, -g, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true }, problemMatcher: [$gcc] } ] }这套配置的最大价值是让答案代码“一键编译”。参考答案代码往往不是单个文件用这个 task 编译单文件够用但到了多文件阶段就得上 CMake 了。另外-stdc17一定要加否则很多答案用到 C11 之后的新特性如果auto、移动构造、智能指针会被编译器拒绝。加上-Wall -Wextra后答案代码里常见的未初始化变量、比较符号写反这些问题编译器会直接报警告这比你自己盯着屏幕找高效得多。3.2 把答案代码拆成 .h .cpp模板类不能按普通类的方式分离编译参考答案里的很多数据结构都是用类模板写的比如template typename T class Stack。遇到这种代码新手最常见的操作是复制粘贴到main.cpp里编译顺利通过就完事了。但如果你想把答案里的类单独拆成Stack.h和Stack.cpp然后只保留main.cpp里的调用代码经常会看到“未定义引用”之类的链接错误。这不是程序逻辑错了是模板类不能像普通类那样分离编译。原因在于模板是“实例化”的编译器只有在看到Stackint s这样的具体类型时才知道要为int生成一套代码。如果把模板的定义放在.h实现放在.cpp那么main.cpp里包含的只是头文件根本没看到模板实现代码编译器也就无从生成Stackint的成员函数实现。常见的解决方案有两种一是把模板实现也放进.h文件这是最推荐的做法二是在.cpp末尾显式实例化所有需要的类型例如template class Stackint;但这样只能帮你覆盖到少数几个具体类型。// Stack.h #ifndef STACK_H #define STACK_H #include vector template typename T class Stack { public: void push(const T value) { data_.push_back(value); } T pop() { T top data_.back(); data_.pop_back(); return top; } bool empty() const { return data_.empty(); } private: std::vectorT data_; }; #endif这是把整个模板类的定义和实现都写在同一个头文件里。push的参数是const T避免了无谓的拷贝pop返回T调用者先拿到栈顶元素再让 vector 缩容。这里有个设计细节值得体会没有把pop写成void pop()因为那样拿不到被弹出的元素也没有写成T pop()再让你调用empty()做判断而是让调用方自己保证栈非空。参考答案里如果读到了不一样的写法建议改成这种工程里更安全。// main.cpp #include Stack.h #include iostream int main() { Stackint s; s.push(3); s.push(7); while (!s.empty()) { std::cout s.pop() std::endl; } return 0; }这段代码验证的是“模板定义在头文件、实例化在调用方”这种模式。Stackint只出现在main.cpp里编译器在编译main.cpp时已经通过#include Stack.h看到了全部实现所以能够实例化出完整的int版本成员函数。如果你尝试把push和pop的实现挪到Stack.cpp再编译这段main.cpp就会在链接阶段报错。这个坑我后面第 5 章还会展开讲。3.3 用 CMake 管理多文件把一本习题集当成一个工程来组织当你手上的参考答案从“一个文件”变成“几十个文件”时VS Code 的编译当前文件任务就不够用了。我一般会把整本习题集组织成一个 CMake 工程每个章节一个目录每个习题对应一个可执行目标。这样每次做新增习题只需要在 CMakeLists 里加一行不用为每个文件单写编译命令跑整个章节的题目也方便。cmake_minimum_required(VERSION 3.16) project(DSA_Answers CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(ch03_stack exercises/ch03/stack_main.cpp) target_compile_options(ch03_stack PRIVATE -Wall -Wextra) add_executable(ch04_tree exercises/ch04/bst_main.cpp) target_compile_options(ch04_tree PRIVATE -Wall -Wextra)这段 CMakeLists 做的事情是声明项目名、要求 C17 标准、为第 3 章和第 4 章的习题分别建立可执行目标。-Wall -Wextra通过target_compile_options挂到具体目标上保证两个目标的行为一致。你在解一道题的时候不用管其他章的代码是否编译通过因为目标之间是独立的。用 CMake 还有个实际好处很多答案代码在别的机器上跑得好好的轮到你就崩最后查出原因是编译器标准不一样。有了 CMake 之后-stdc17被固定写在构建脚本里换机器、换系统出来的行为基本一致。再配合CTest这种测试框架还能给每个习题挂上测试用例。数据结构实验报告如果要求你“提交工程源码”CMake 结构也比一坨单文件代码显得正规得多。构建时用cmake -S . -B build生成构建目录然后cmake --build build编译就这么两条命令。4. 从参考答案到自用算法库把书上的类模板改成能干活的样子4.1 排序算法参数怎么设比较器、递归阈值与稳定性答案里最常见的排序实现是教材原样代码能跑但性能和安全边界一般。尤其是快速排序如果每次固定取第一个元素作为 pivot遇到已经有序的数组复杂度会退化到 O(n²)。把参考答案改造成自用算法库之前我强烈建议先给快速排序做三个改造三数取中、小数组切换插入排序、支持自定义比较器。template typename T, typename Compare std::lessT void quickSort(std::vectorT arr, int left, int right, Compare comp Compare()) { const int CUTOFF 16; if (right - left 1 CUTOFF) { insertionSort(arr, left, right, comp); return; } int pivotIndex medianOfThree(arr, left, right, comp); T pivot arr[pivotIndex]; std::swap(arr[pivotIndex], arr[right]); int i left; int j right - 1; while (true) { while (i right comp(arr[i], pivot)) i; while (j left comp(pivot, arr[j])) j--; if (i j) std::swap(arr[i], arr[j]); else break; } std::swap(arr[i], arr[right]); quickSort(arr, left, i - 1, comp); quickSort(arr, i 1, right, comp); }这段代码里最关键的两个参数是Compare comp和CUTOFF。Compare作为仿函数传入默认用std::lessT这样元素按从小到大排序std::sort的语义一样可以无缝替换 STL 接口CUTOFF控制数组规模小到一定程度时切换插入排序因为插入排序在小数组上比快速排序快得多实际项目里 16 到 32 都是合理取值我一般用 16。medianOfThree是在left、mid、right三个位置取中位数缓解了最坏情况。注意 partition 之后的条件是j left不写否则越界访问回把你整懵。堆排序在答案里出现的频率也很高但很多答案的堆排序写成了自上而下“建堆”再逐个删除最大元素这个流程没问题只有一点常被忽略元素比较直接用了和没接比较器。当你需要逆序排序或者排序的是自定义类型时就得改代码。工程里我一般给排序算法统一接Compare让调用方用std::greaterT切逆序。这是把参考答案变成自用库的第一步抽掉硬编码的比较逻辑。4.2 把树形结构做成模板深拷贝、析构与拷贝赋值三件套树是参考答案里最容易“跑得起来但只要一拷贝就崩”的结构。因为树节点是用裸指针串起来的编译器合成的默认拷贝构造函数只会把根指针的值复制一份结果两个对象共用同一棵树的节点。修改其中一个另一个跟着变对象销毁时两块内存重复释放直接 double free。template typename T class BinarySearchTree { private: struct Node { T data; Node* left; Node* right; }; Node* root; Node* clone(Node* t) const { if (!t) return nullptr; Node* newNode new Node{t-data, clone(t-left), clone(t-right)}; return newNode; } void destroy(Node* t) { if (!t) return; destroy(t-left); destroy(t-right); delete t; } public: BinarySearchTree() : root(nullptr) {} BinarySearchTree(const BinarySearchTree rhs) : root(clone(rhs.root)) {} BinarySearchTree operator(const BinarySearchTree rhs) { if (this ! rhs) { destroy(root); root clone(rhs.root); } return *this; } ~BinarySearchTree() { destroy(root); } };我把每次写树的答案时都会加的“安全三件套”放出来了。clone递归复制每一个节点包括节点携带的数据和子树指针destroy递归释放整棵树拷贝构造函数、拷贝赋值运算符、析构函数三处分别调用它们。这里operator用的是“先销毁旧树再克隆新树”的策略配合if (this ! rhs)防止自我赋值如果你在实现里看到有人省略了这个判断那几乎是隐含着坑。new Node{t-data, clone(t-left), clone(t-right)}是 C11 的聚合初始化写法录答案那会儿还没有这样写能跑但风格略旧改造时顺手把它统一起来。这组代码看起来简单但它直接决定了你能不能安全地以“值”的方式传递一棵树。如果参考答案里只给了插入和删除没给拷贝和析构千万不要直接拿进自己的工程否则只要出现一次函数参数按值传递的情况就会在函数返回时炸掉。4.3 图的存储与遍历邻接表改成 vector 版避开指针地狱图算法是参考答案里最容易“能在书上看懂、一上机就崩”的部分。原因很直接很多老答案用链表数组存邻接表手写节点指针插入边时还要malloc或者new稍微漏一条边没插好数组越界和野指针就全来了。现代 C 写图我建议全套换掉用vectorvectorpairint, int或者vectorlistEdge。struct Edge { int to; int weight; }; vectorvectorEdge buildGraph(int n, const vectortupleint, int, int edges, bool directed false) { vectorvectorEdge adj(n); for (const auto [u, v, w] : edges) { adj[u].push_back({v, w}); if (!directed) { adj[v].push_back({u, w}); } } return adj; }这里vectorvectorEdge的规模由顶点数n决定每个顶点对应一个边列表。buildGraph接收边的三元组列表directed参数控制是否要同时插入反向边。用vector替代裸指针之后不再需要手动管理链表节点的生命周期函数返回时内存自动释放这是现代 C 最直观的安全感提升。注意这里顶点编号假定从 0 开始如果你手里的答案全是从 1 开始的处理旁白n1个顶点并把adj.resize(n1)这是图题目最容易踩的索引坑。有了邻接表之后Dijkstra 用优先队列实现。优先队列的元素是“距离 顶点编号”的 pair每次取堆顶扩展和书里的思路一模一样唯一要注意的是pair默认按第一维排序所以把距离放在第一个分量位置。vectorint dijkstra(const vectorvectorEdge adj, int src) { const int INF 1e9; vectorint dist(adj.size(), INF); priority_queuepairint, int, vectorpairint, int, greater pq; dist[src] 0; pq.push({0, src}); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); if (d dist[u]) continue; for (const Edge e : adj[u]) { if (dist[u] e.weight dist[e.to]) { dist[e.to] dist[u] e.weight; pq.push({dist[e.to], e.to}); } } } return dist; }d dist[u]这个判断是惰性删除的关键同一个顶点可能被重复入队很多次只有最新的、距离最小的那次才有效旧记录直接跳过。参考答案大多会用visited数组来标记“已经确定最短路的顶点”那样其实也能跑但前提是边权必须非负而且写起来更啰嗦。优先队列写法在负权图上会出问题但题目一般都会注明边权为正。改造到这里图和树的代码就已经从“教材里能跑的样本”变成“自己数据结构实验报告里能直接用的模块”了。5. 对答案时最常翻车的 5 个坑现象、原因与解决办法5.1 坑一两份答案都能跑结果一个对一个错现象同一道题网上找到了两份第四版参考答案逻辑看起来一模一样但用同一组测试数据跑一个输出正确一个输出错误。很多人的第一反应是“自己看错了”陷入反复对照的泥潭。原因两份答案对“空结构”的处理不一样。常见差异在pop遇到空栈是返回值还是直接崩溃、删除 BST 的叶子节点时如何更新父节点指针、以及快排的终止条件写i j还是i j。这些差异在常见小数据下不体现一旦遇到规模为 0 或 1 的输入就暴露出来。解决用边界数据逼它现形。我给每个答案代码都会准备一组“最小攻击数据”栈就测空栈和单元素栈树就测空树和单节点树图就测单顶点无自环的图。如果你使用的编译器支持加上-fsanitizeaddress再编译运行未定义行为会直接报错不会让你在结果层面纠结半天。5.2 坑二模板类编译报链接错误找不到函数实现现象main.cpp编译通过链接时报undefined reference to Stackint::pop()在 Linux 下表现为undefined reference字样在 Windows 下的 Visual Studio 里是 LNK2019。原因参考答案里的模板类被粗暴地拆成了.h和.cpp实现代码在.cpp里。模板类分离编译编译器在编译main.cpp时看不到模板实现自然生成不了具体的Stackint版本。这不是答案逻辑错了是工程组织方式不对。解决把模板成员函数的定义全部挪进头文件或者把.cpp里的模板实现#include到main.cpp的末尾。更省事的方法是所有代码都写进同一个文件里跑但对于要长期维护的数据结构库来说统一放在头文件是标准做法。5.3 坑三递归写排序和遍历数据一多就栈溢出现象快排或二叉树析构函数在数据量大时程序崩溃。快排在随机数据下没问题但测试数据包含有序序列时崩溃概率直线上升。原因是递归深度失控。原因参考答案里的快排如果固定取第一个或最后一个元素做 pivot遇到已经有序的数组每次切分只能分出一个元素递归深度达到 O(n)栈空间不足直接爆。同理二叉树的析构如果递归调用树退化成链表时也会爆栈。解决快排按我在 4.1 节写的方式改造——三数取中 小数组切换插入排序。插入排序的时间复杂度虽然是 O(n²)但它的递归深度是 0用它处理小数组能把最坏情况的递归深度限制在 O(log n)。二叉树析构稍微麻烦一点递归式destroy在树退化时也会爆栈我一般会在深拷贝场景优先用 Morris 遍历思路或显式栈迭代。5.4 坑四浅拷贝导致 double free程序退出时才崩溃现象程序运行中一切正常但 main 函数return 0时弹出pointer being freed was not allocated或double free or corruption。排错半天崩溃点不在业务代码里在程序收尾阶段。原因某个类比如二叉树、图里用了裸指针没有写拷贝构造函数和拷贝赋值运算符。默认拷贝是浅拷贝把指针的值直接复制导致两个对象的指针指向同一内存块。程序结束时第一个对象析构释放内存第二个对象析构再次释放触发 double free。解决用动态内存的结构全部补上“交换三件套”或“复制三件套”。拷贝构造函数负责深拷贝拷贝赋值运算符负责先清理旧资源再深拷贝析构函数负责释放资源。如果你用的是 C11 之后的编译器更省事的方案是内部改用shared_ptr或unique_ptr让指针自己管理生命周期。参考答案里没写这些成员函数其实常见它默认使用者只用一个对象不复制。5.5 坑五答案代码在别人机器上跑得好好的到你这一编译就报错现象同一份代码网上作者说“编译运行均通过”你这边报一堆语法错误或中文注释乱码或程序启动后行为不对。常见的报错包括stray \342 in program、default_delete... undeclared等等。原因这类问题多半不是代码逻辑问题而是环境差异。第一源文件编码不同Visual Studio 默认用 GBK 保存中文字符串MinGW 默认按 UTF-8 解析中文注释就会被识别成非法字符第二编译器标准太老答案里用到了 C11 之后的语法但编译命令没有加-stdc11或-stdc17第三未初始化变量部分老编译器对未初始化变量的宽容度不同-Wall只报警告但换个平台就变成神秘行为。解决打开 VS Code把文件编码改成 UTF-8一劳永逸方式是在.vscode/settings.json里加files.encoding: utf8。编译命令统一加-stdc17 -Wall -Wextra -g让所有警告暴露出来。如果程序行为还是不对劲优先检查所有局部变量尤其是int型变量有没有初始化。让“换一台机器就出问题”的原因从“玄学”变成“可复现的检查清单”这门课才真正学会了。6. 用断言和随机测试给参考答案“验货”把“看答案”变成“审代码”6.1 给核心结构写一个“最小测试驱动”的工具到了这一步你手里的参考答案应该已经能编译、能运行、也能用自己的数据验证了。但“能跑样例”和“代码正确”之间还有差距我习惯给核心数据结构挂一个随机测试的壳子用断言检查结果。以排序为例#include cassert #include algorithm #include random #include vector int main() { std::mt19937 rng(42); for (int n 0; n 100; n) { std::vectorint arr(n); for (int x : arr) x (int)(rng() % 1000); std::vectorint expected arr; std::sort(expected.begin(), expected.end()); quickSort(arr, 0, (int)arr.size() - 1); assert(arr expected 排序结果与标准库不一致); } return 0; }mt19937是确定种子的随机数引擎种子固定意味着每次运行的数据一样方便回放错误assert(arr expected)把排序结果和std::sort做对比一是验证正确性二是可以发现快排实现里是否越界或破坏了数据。assert在发布模式下会失效所以这只作为开发阶段工具。验证通过之后再跑一遍 0、1、2、100、10000 个元素的规模看耗时和递归深度。数据结构算法不是“看答案看懂”而是“用数据验过”才算真的成为自己的能力。这个习惯我保留了很多年。现在再看到一份号称“第四版参考答案”的文件我不会急着下载或抄代码而是先搭一个这样的测试台把它的核心代码跑一遍再决定值不值得深入读。你手里的答案是教材代码还是网上东拼西凑的版本用这种方式验一下心里就有底了。希望你也能把“找答案”的过程换成“造测试工程”的过程这门课的价值会翻倍。希望帮到你。本文还有配套的精品资源点击获取