资讯动态

Java数据结构与算法分析:递归、大O与链表习题答案精讲

发布时间:2026/10/17 14:41:21 来源:尧图企业网站定制
简介《数据结构与算法分析Java语言描述》第三版习题答案文档面向正在学习数据结构与算法、需要核对解题思路的Java开发者及计算机专业学生。内容聚焦教材前几章的经典题目涵盖递归程序设计、数学归纳法证明、文件递归处理、数列求和、模运算与指数定律以及大O符号与对数估计等核心知识点。答案逐题给出推导过程例如通过processFile方法演示嵌套文件递归处理、用ones函数展示二进制位计数递归实现并利用数学归纳法严格证明对数性质帮助读者理解算法分析背后的数学基础。整个资源包共1个docx文件压缩后约1.52MB便携易用。已有2431人学习下载。通过对照答案读者可以检验自身解题方法掌握从问题建模到复杂度分析的系统思路尤其适合备考或自学《数据结构与算法分析》课程时使用。1. 拿到这份Java数据结构与算法分析习题答案先搞懂它值不值得你花时间在 Java 数据结构与算法分析这条路上最磨人的往往不是教材本身而是课后习题没有可靠答案。这份第三版习题答案 docx 覆盖了前三章的核心内容递归与数学归纳法、大 O 复杂度分析、链表与栈队列的手写代码每一题都给出可直接运行的 Java 实现或完整推导过程。它适合两类人一类是正在准备笔试面试、需要快速过一遍经典题型的开发者另一类是自己刷书但卡在证明题和链表题上的学生。我拆完这份材料后最大的感受是答案本身不难难的是你知不知道它为什么要这么写。2. 习题答案里的数学底子递归、归纳法与求和公式怎么变成代码前三章的题看着散其实核心数学工具就四件递归构造、数学归纳法、错位相减、模运算。前两章大量证明题不是考数学本身而是用数学训练你估算复杂度和写递归的能力。这一章把答案里的推导逐个拆开你会发现它们和后面链表代码是同一套思维模式。2.1 递归函数从 ones(int n) 看终止条件和递推关系第 1.5 题要求实现一个函数统计整数 n 的二进制表示里 1 的个数。答案给了很短的一段递归// 统计二进制表示中 1 的个数 public static int ones(int n) { if (n 2) { return n; // 0 - 01 - 1直接返回 } return n % 2 ones(n / 2); // 最低位 剩余高位部分的结果 }逻辑说明递归把问题切成两块——当前最低位贡献的 1 的个数n % 2以及右移一位后的子问题ones(n / 2)两者相加。终止条件是 n 2因为 0 和 1 的答案就是它们自己不需要再拆。参数说明这个函数只对非负 n 有意义如果 n 是负数Java 的%和/运算结果会带上符号实际使用时可以先取绝对值或者改用无符号右移。拿 8 验证一下8 % 2 08 / 2 44 % 2 04 / 2 22 % 2 02 / 2 11 2 返回 1最后结果是 1正确8 1000 二进制。这种每次把问题减半的递归时间是 O(log N)栈深也是 O(log N)。Java 没有自动尾递归优化但这里的递归深度很小完全够用。第 1.4 题把递归用在文件处理上这道题更接近工程场景void processFile(String fileName) { // 打开 fileName逐行处理 for (每一行 line : 文件内容) { if (line 匹配 #include SomeFile) { processFile(SomeFile); // 递归展开被包含文件 } } // 处理完毕关闭文件 }逻辑说明处理主文件时遇到#include指令就把被包含文件当作新的子任务递归调用。这个模式对应实际工程里的配置文件 include 展开、依赖解析器。参数说明fileName 是文件路径递归传入的是被包含文件名文件不存在或嵌套过深时递归会直接抛异常。这题真正的坑在于自引用包含A 包含 B、B 又包含 A 就会无限递归。答案给出的办法是维护一个尚未处理完的文件列表每次递归前先查这个列表发现文件已经在列表中就直接跳过等价于环检测。工程上这个闭环检测必须写不然后悔药都来不及吃。2.2 数学归纳法的论证骨架从区间翻倍到斐波那契第 1.7 题要求证明对 X 0有 log X X自然对数。答案的证明思路不是常见的从 N 推到 N1而是用区间翻倍。先验证基础情况0 X ≤ 1 时成立因为 log X 是负数或 01 X ≤ 2 时也成立。然后假设对所有 p X ≤ 2p 成立考虑任意 2p Y ≤ 4p做代换log Y 1 log(Y/2)而 Y/2 落在 (p, 2p] 区间内正好能用归纳假设再结合 1 Y/2 Y/2 Y/2 Y就完成了证明。这个翻倍区间 (0,2] → (0,4] → (0,8] 一步步覆盖整个正实数轴是归纳法里比较隐蔽的一种构造。斐波那契那道题1.11也是同样的骨架只是换成了数列递推。证明时用到的关键恒等式是 φ 1 φ²也就是 φ⁻¹ φ⁻² 1把 F(k1) F(k) F(k-1) 代入归纳假设后正好用这个恒等式把两项合并成一项得到上界 φ^(k1)。这类证明题的通用步骤可以归纳成四条写清命题 P(N)验证最基础的 N 值假设 P(1) 到 P(k) 都成立用定义把 P(k1) 归结到前面的项。做完 1.7 和 1.11你会发现后面几乎所有复杂度推导用的都是这套骨架。2.3 数列求和的差分技巧等比数列变形题第 1.8 题是数列求和的经典套题核心是错位相减。先看最简单的 (a)S 1 1/4 1/16 ...直接套等比数列求和公式结果是 4/3。(b) 开始变形S 1/4 2/16 3/64 ...也就是 Σ(i / 4^i)。答案用 4S 减 S 的做法S 1/4 2/16 3/64 ... 4S 1 2/4 3/16 ... 3S 1 1/4 1/16 ... 4/3所以 S 4/9。这个技巧把一次项系数变成了常数项让等比公式可以直接用。(c) 再升一阶求 Σ(i² / 4^i)。同样用 4S - S二次项的差会退化成一次项和常数项的组合也就是每减一次幂次降一阶求和对象方法结果Σ 1/4^ii 从 0 开始等比公式4/3Σ i/4^ii 从 1 开始错位相减一次4/9Σ i²/4^i错位相减两次20/27第 1.10 题换了个方向做模运算证明 2^100 mod 5 1。直接算 2^100 会溢出答案用的是先找周期——2^4 1616 mod 5 1所以 2^100 (2^4)^25 ≡ 1^25 1。这其实就是快速幂思想在数论题里的应用后面第 2 章分析快速幂复杂度时又回到了这个思路。3. 算法分析实战大O推导、循环嵌套与运行时间估算第 2 章是整份答案里信息密度最高的一章也是面试最容易直接考的一章。它不像第 1 章那样考数学技巧而是把复杂度分析落到了一段段具体代码上。3.1 增长率排序为什么 N log N 和 N log(N²) 同阶第 2.1 题给了一组函数要求按增长率排序并找出同阶项。答案的结论如下增长率从低到高说明2/N趋向 037常数√N亚线性N线性N log log N接近线性N log N常规排序算法N log(N²)等于 2N log N与 N log N 同阶N log²N这里的 log²N 是 (log N)²N^1.5亚平方N²平方N² log N略高于平方N³立方2^(N/2)指数2^N指数这里最容易踩的坑是把 N log(N²) 当成比 N log N 高阶。实际上 log(N²) 2 log N所以 N log(N²) 2N log N常数因子在渐近分析里可以忽略两者同阶。另一坑是 N log²N 的写法它表示 N × (log N)²不是 (N log N)²增长率比 N log N 高一个 log 因子但远低于 N^1.5。第 2.2 题考大 O 符号的反例答案里给了几组构造T1(N) 2NT2(N) Nf(N) N 时T1 和 T2 都是 O(f)但 T1 - T2 N 不是 o(N)。这说明大 O 加法性质不能随便反推做题时看到相减要特别小心。3.2 循环嵌套复杂度直接乘上界会翻车第 2.7 题给了六段循环要求判断运行时间。答案的结论是从 O(N) 到 O(N⁵)复杂度差异极大其中两道题的乘界法是错的。先看简单形态。一层循环是 O(N)两层嵌套是 O(N²)三层是 O(N³)。但第 (V) 题的循环上限不是常数// 习题 2.7(V) 的典型形态j 的上限是 i²k 的上限是 j for (int i 0; i N; i) { for (int j 0; j i * i; j) { for (int k 0; k j; k) { sum; // 总次数约 N^5 量级 } } }逻辑说明第 2 层循环执行 i² 次第 3 层循环执行 j 次而 j 最大也是 i²所以总次数约等于 N × N² × N² N⁵。参数说明这里的三层上限都依赖外层变量直接套三层就是 N³不成立要先写出每层实际范围。第 (VI) 题是另一个极端它里面有个 if 条件挡住了一部分执行// 习题 2.7(VI) 的典型形态内层实际执行次数远小于循环上界 for (int i 0; i N; i) { for (int j 0; j i; j) { // j i每个 i 恰好 i 次 for (int k 0; k j * j; k) { sum; } } }逻辑说明总次数 Σ(i) Σ(j i) j² ≈ Σ(i) i³ / 3 ≈ N⁴ / 12所以是 O(N⁴)。如果直接乘三层循环上界 N × N × N²会得到 N⁵ 的错误结论。这两道题对比说明分析嵌套循环时必须先找每个变量的实际取值范围再决定是直接乘还是需要逐层求和。3.3 从运行时间反推问题规模第 2.11 和 2.12 题是运行时间的实际估算也是面试里你估计一下这个算法能处理多大输入这类问题的出处。第 2.11 题说一个算法在 N 1000 时耗时 0.5 ms问 N 5000 时耗时多少。答案按复杂度给出不同结论复杂度结论O(N)5 倍时间2.5 msO(N log N)略大于 5 倍比 2.5 ms 稍高O(N²)25 倍时间12.5 msO(N³)125 倍时间62.5 ms第 2.12 题反过来问时间增加到 12000 倍输入规模能扩大多少。对 O(N)规模直接扩大 12000 倍对 O(N²)扩大 √12000 ≈ 110 倍对 O(N³)扩大 ∛12000 ≈ 23 倍。这类估算不需要精确到个位关键是建立平方变慢、立方更慢的直觉。第 2.9 题给了一个更冲击的点生成随机排列的四种算法在输入 100 万时暴力算法约 72 年带查重优化的约 4 小时洗牌法只要 0.7 秒线性法只要 0.03 秒。同一个问题算法选型不同差距是 11 个数量级。这道题的答案有个附带说明如果内存不够线性算法反而会因为缺页中断变得不像线性这也是实测里最常见的翻车原因。4. 链表、栈与队列的参考答案直接可抄的代码与指针细节第 3 章的题目集中在链表操作答案是完整的 Java 代码。这一章的价值在于书里讲的抽象数据结构在这里变成真实的指针调整和迭代器操作。4.1 printLots两个迭代器协同扫描的套路第 3.1 题给两个表 L 和 P要求打印 L 中所有由 P 指定位置上的元素。答案用两个迭代器同步扫描public static AnyType void printLots(ListAnyType L, ListInteger P) { IteratorAnyType iterL L.iterator(); IteratorInteger iterP P.iterator(); AnyType itemL null; Integer itemP 0; int start 0; while (iterL.hasNext() iterP.hasNext()) { itemP iterP.next(); System.out.println(Looking for position itemP); // 把 L 的迭代器推进到目标位置 while (start itemP iterL.hasNext()) { start; itemL iterL.next(); } System.out.println(itemL); } }逻辑说明start 记录 L 迭代器当前已经越过的元素个数每次从当前位置继续推进而不是从头扫描整体复杂度是 O(|L| |P|)不是 O(|L| × |P|)。参数说明P 中的位置必须按升序排列如果 P 是无序的这个双迭代器方案会失效需要先对 P 排序或用哈希表缓存位置。位置值需要保证在 L 的有效范围内否则 itemL 可能一直停在 null 或最后一个元素上。这个两个有序序列同步推进、谁小谁动的写法在后面 intersection 和 union 里还会出现一次是同一种套路。4.2 swapWithNext单链表与双链表的指针调整顺序第 3.2 题要求交换链表相邻两个节点。单链表的答案里入参是前驱节点这是单链表交换的经典设计——因为单链表无法回头必须从要交换两个节点的前一个节点开始操作// 单链表beforep 是待交换两个节点之前的那个节点 // 假设 beforep.next 和 afterp 都不为 null public static void swapWithNext(Node beforep) { Node p beforep.next; // 第一个要交换的节点 Node afterp p.next; // 第二个要交换的节点 p.next afterp.next; // 先把 p 接到后一个节点的后继上 beforep.next afterp; // 前驱指向第二个节点 afterp.next p; // 第二个节点指向第一个 }逻辑说明核心是保存 afterp 的引用后再改指向否则 p.next 一旦被覆盖afterp 就找不到了。参数说明这个函数只适用于相邻节点都存在的情况beforep 是哨兵节点也可以但传入的 beforep 不能是最后一个节点。双链表版本多了一组 prev 指针要维护顺序更严格// 双链表直接传入要交换的第一个节点 p public static void swapWithNext(Node p) { Node beforep p.prev; Node afterp p.next; p.next afterp.next; // 1. p 指向 afterp 的后继 beforep.next afterp; // 2. 前驱指向 afterp afterp.next p; // 3. afterp 指向 p p.next.prev p; // 4. p 的新后继的 prev 指回 p p.prev afterp; // 5. p 的 prev 改为 afterp afterp.prev beforep; // 6. afterp 的 prev 改为 beforep }逻辑说明双链表交换要保证每个被改动节点的两个方向都正确六步缺一不可。参数说明p 不能是头节点或尾节点调用前要检查 p.next 是否为空。这个题是面试高频手写题很多考生死在先改了 p.next再想找 afterp 就找不到了。答案是固定套路直接背熟。4.3 intersection 与 union有序链表的归并双指针第 3.4 和 3.5 题实现两个有序链表的交集和并集答案用的是归并式双指针。以交集为例public static AnyType extends Comparable? super AnyType void intersection(ListAnyType L1, ListAnyType L2, ListAnyType Intersect) { ListIteratorAnyType iterL1 L1.listIterator(); ListIteratorAnyType iterL2 L2.listIterator(); AnyType itemL1 null, itemL2 null; // 先取两个链表的第一个元素 if (iterL1.hasNext() iterL2.hasNext()) { itemL1 iterL1.next(); itemL2 iterL2.next(); } while (itemL1 ! null itemL2 ! null) { int compareResult itemL1.compareTo(itemL2); if (compareResult 0) { Intersect.add(itemL1); // 相等加入结果集 // 两边同时推进 itemL1 iterL1.hasNext() ? iterL1.next() : null; itemL2 iterL2.hasNext() ? iterL2.next() : null; } else if (compareResult 0) { itemL1 iterL1.hasNext() ? iterL1.next() : null; // L1 小了L1 走 } else { itemL2 iterL2.hasNext() ? iterL2.next() : null; // L2 小了L2 走 } } }逻辑说明每个 while 循环只推进一个迭代器谁小谁走相等就同时走。整体时间复杂度 O(|L1| |L2|)空间上只用了两个游标变量。参数说明前提是 L1 和 L2 本身有序如果链表无序这个算法退化必须先排序或者改用哈希表时间复杂度变成 O(|L1| |L2|) 的哈希版本。第 3.6 题的约瑟夫环问题答案里给了两个数学优化M 很大时绕圈走 M 步等价于走 M mod N 步因为每圈 N 个节点M 超过 N/2 时反向走 N - M 步比正向走 M 步更短。再加上每删一个节点链表规模减一最坏复杂度是 O(N × min(M, N))。这道题的答案没有给完整代码但这两个优化点就是实现约瑟夫环的标准手法。5. 避坑与常见问题对答案时最容易误判的五个地方这份答案文档本身质量不低但照着抄的时候以下五个坑我见过不止一个人踩。每条都按现象、原因、解决三步走。5.1 现象照抄 ones 函数结果却对不上A 同学拿着第 1.5 题的答案把n % 2理解成取十进制个位数于是改成n % 10 ones(n / 10)测试 n 9 时得到 1但手算 9 1001 二进制应该有两个 1。原因原题统计的是二进制表示中 1 的个数n % 2取的是最低二进制位n / 2等价于右移一位改成十进制就是另一道题了。解决先确认题意再用 2 的幂次方验证8、16、255 这几个数能快速暴露问题。这类抄答案但抄错位的失误最好的预防手段是抄完立刻在 main 方法里跑断言。5.2 现象1.7 的归纳证明看不懂区间为什么翻倍很多读者卡在为什么要分 0 X ≤ 1 和 1 X ≤ 2 两个基础情况。原因这道题的归纳步骤不是从 N 到 N1而是从 (p, 2p] 推到 (2p, 4p]属于区间翻倍式归纳只验证 X 1 一个点不够必须验证整个基础区间。解决把 p 1、2、4 代入观察覆盖范围变成 (0,4]、(0,8]就能看出所有正数都能被覆盖到。这个套路在证明 log N N 这类不等式时非常常用理解了以后不用死记。5.3 现象复杂度算对了实测运行时间却完全对不上有读者拿第 2.9 题的72 年去验证自己本地的 O(N²) 算法发现 N 10000 时只要几十毫秒于是怀疑答案错了。原因72 年是理论模型下常数因子极大时的估算实际机器的 JIT 编译、CPU 缓存、常数因子都会让小规模表现远好于理论值渐近分析描述的是规模增长形态不是绝对耗时。解决验证复杂度用倍增法输入规模翻倍观察时间是否变成约 4 倍O(N²) 特征或约 8 倍O(N³) 特征同时取多次运行中位数避免 GC 噪音。注意单点耗时不能验证复杂度至少取三个规模点才有说服力。5.4 现象swapWithNext 照着敲链表却断成两截某开发者在实现单链表交换时先执行beforep.next afterp再执行p.next afterp.next结果链表少了节点。原因p.next被覆盖之前没有先保存 afterp或者接线顺序错误导致 p 与 afterp 之间的连接丢失。解决严格按答案顺序执行——先保存 afterp再让 p 指向 afterp 的后继再改 beforep 的 next最后让 afterp 指向 p。建议在纸上画出四个节点每一步对照着改双链表则多检查一遍 prev 方向的四步。5.5 现象2^100 这类题直接循环乘法溢出看到第 1.10 题第一反应是写循环连乘 100 次结果 int 直接溢出。原因2^100 远超 int 和 long 的表示范围题目意图是用模运算消去大数。解决先找模周期2^4 16 ≡ 1 (mod 5)所以 2^100 (2^4)^25 ≡ 1通用做法是快速幂按二进制位累乘。第 2.23 题的答案补充了乘法次数公式⌊log N⌋ b(N) - 1其中 b(N) 是 N 的二进制表示中 1 的个数这在实际工程里就是 BigInteger.modPow 的原理。6. 验证答案的最后一公里测试台、倍增实测与思维卡片6.1 给答案建一个最小测试台链表和递归的答案不要只靠眼睛对建一个最小测试台是最快的验证方式。public static void main(String[] args) { // 用已知结果验证 ones assert ones(0) 0; assert ones(1) 1; assert ones(8) 1; // 1000b assert ones(9) 2; // 1001b assert ones(255) 8; // 11111111b System.out.println(ones 自测通过); // 验证单链表 swapWithNext构造 1 - 2 - 3交换前两个节点 Node head new Node(0); // 哨兵 head.next new Node(1); head.next.next new Node(2); head.next.next.next new Node(3); swapWithNext(head); // 交换 1 和 2 assert head.next.data 2; assert head.next.next.data 1; assert head.next.next.next.data 3; System.out.println(swapWithNext 自测通过); }逻辑说明断言式自测能在几秒内确认答案抄对了还能顺便验证边界情况。参数说明链表题建议只用三到四个节点手画一遍图再跑代码指针类错误在图上比在内存里好找得多。6.2 复杂度复核把答案变成自己的思维卡片复杂度类题目用倍增法做实测复核。大体思路是写一个计时器对 N 1000、2000、4000 分别运行同一算法记录耗时比例。比例在 2 附近说明是 O(N)在 4 附近说明是 O(N²)在 8 附近说明是 O(N³)。每次跑完先做一轮预热再计时取中位数这个习惯能避免 JIT 编译带来的首轮误差也能让第 3 章的结论变成你自己的手感。每道题做完我会在答案旁留三行笔记一行写复杂度结论一行写代码模板一行写证明或推导骨架。第 1.5 题的三行就是O(log N) / n%2 ones(n/2) / 终止条件 n2。这套做法比反复刷题有用得多因为最后进考场的不是答案文档是你脑子里的骨架。从那以后我每次刷这本文集都强制自己先跑一遍测试台再翻答案翻完只对结论不对过程错的地方必须重写一遍。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑