
简介《数据结构与算法分析Java语言描述》第三版教材的配套习题答案文档适合正在系统学习数据结构和算法、需要用习题检验自身理解的Java学习者与备考学生。文档覆盖文件处理与递归调用、递归算法设计如求整数二进制表示中1的个数、数学归纳法证明、等比数列求和、模运算与指数定律、大O符号估计时间复杂度等核心内容针对第一章多道典型题目给出完整解答和推导过程能够帮助读者理解算法分析中的证明思路与递归实现技巧并将数学推理应用到实际编程场景中。资源为单个docx文件压缩包约1.52MB内容排版清晰、公式完整题目步骤逐条展开便于对照教材逐题研读、查漏补缺或快速复习。目前已有2431人学习下载是一份实用且热度较高的数据结构学习辅助材料尤其适合考研复试、期末考试或日常自学时的强化练习可作为教材配套的答案速查与思路复盘工具。1. 习题答案里藏着机制为什么改一行循环边界复杂度就翻了两个数量级《数据结构与算法分析Java语言描述》第三版的这份习题答案最反直觉的地方在第二章的 2.7 题同一段三层循环代码只把内层循环的上界从 N 换成 j按循环头相乘估算复杂度立刻从 O(N³) 跳到 O(N⁵)紧接着的 (VI) 小题又告诉你多加一个 if 判断连 N⁵ 都能被砍回 O(N⁴)。一般人翻开答案只想核对结果但这份 docx 真正的价值在于它把递归为什么能收敛、对数底数为什么无所谓、循环嵌套为什么不能直接相乘、数学归纳法怎么变成可执行代码这些“机制”全部拆到了纸面上。搞懂这些等于把“会用 Java 写链表”升级成“知道算法为什么能 work”。2. 递归与数学归纳法同一套推理两种载体第一章的习题里最扎眼的是两组东西递归代码和归纳证明。很多人把它们当两门课——编程归编程数学归数学。但 1.4、1.5、1.7、1.11 这几道题放在一起看答案几乎在明说递归就是运行时的归纳法归纳法就是纸面上的递归。2.1 ones(int n) 拆开看每一层只处理一个二进制位习题 1.5 的答案是三行代码public static int ones( int n ) { if( n 2 ) return n; return n % 2 ones( n / 2 ); }这个函数统计整数 n 的二进制表示里有多少个 1。以 n 13二进制 1101为例调用链是ones(13) → 1 ones(6) ones(6) → 0 ones(3) ones(3) → 1 ones(1) ones(1) → 1每一层只做两件事用n % 2取出当前最低位用n / 2把剩下的高位右移一位交给下一次调用。递归深度是 ⌊log₂n⌋每层常数时间整体 O(log n)。这里的关键机制叫“信任跳跃”验证递归正确性不需要把整条调用链展开只需要确认两件事——基例正确n 2 时 n 就是 0 或 1 的个数以及归纳步正确假设ones(n/2)已经算对那么n % 2 ones(n/2)必然算对。这和数学归纳法的结构完全同构基例就是归纳基础递归调用就是归纳假设return n % 2 ones(n / 2)这一行就是归纳步。唯一不同的是数学归纳法在纸上证明“对所有 n 成立”递归在机器上把这条证明链实例化成一个接一个的栈帧。2.2 processFile 与 #include递归背后的显式文件栈习题 1.4 讨论了一个更隐蔽的递归场景——文件包含。答案的核心是一个带递归的processFilevoid processFile( String fileName ) { // 打开 fileName做处理…… if ( 发现一行 #include SomeFile ) { processFile( SomeFile ); // 同函数的“自我复制”参数各占一个栈帧 } // 处理完后关闭文件 }如果不做任何防护一个文件#include自己就会像ones(n)缺少基例一样无限压栈直到系统栈溢出——这正是编译器报 nested include too deeply 的机制原型。习题答案给的解法是维护一张“尚未处理完的文件列表”每次递归前先查这张表发现文件已经在表里就拒绝再次进入。这个设计的本质是把隐式的系统调用栈显式化为一个数据结构。系统栈本身就在记录“谁调用了谁”但它只记录路径不记录“集合”而检测环需要的是“集合”——当前递归链上所有文件名的集合。把栈换成显式列表后你就能在入栈前做一次包含判断。真实工程里的依赖解析器、构建工具的循环依赖检测用的都是同一招DFS 遍历时维护一个“在栈上”的 visited 集合遇到已在集合里的节点就报告环。2.3 斐波那契上界证明中的 φ归纳假设如何逼出黄金比习题 1.11(b) 证明斐波那契数列 Fₙ φⁿφ 为黄金比时最关键的一步是观察到 φ 1 φ²等价变形为 φ⁻¹ φ⁻² 1。归纳步是这样合拢的假设 Fₖ φᵏ 和 Fₖ₋₁ φᵏ⁻¹ 都成立那么Fₖ₊₁ Fₖ Fₖ₋₁ φᵏ φᵏ⁻¹ (φ⁻¹ φ⁻²) φᵏ⁺¹ φᵏ⁺¹这个证明的机制在于归纳步想把两个上界加在一起多出来的系数必须被“吸收”掉而 φ 恰好满足 φ⁻¹ φ⁻² 1于是系数精确抵消。这不是运气而是在设计递推不变量——找一个“加上前两项仍然不破界”的数列作为上界。这道题同时埋了一个伏笔这个证明给出的上界是 φⁿ阶数是指数的所以朴素递归计算斐波那契数列必然是指数时间。这个结论会在第 5 章变成一条真实的踩坑记录。3. 大O记号底数、增长率与循环分析的三个细节第二章的答案一眼看过去全是排序和不等式2.1 的增长率表、2.4 的洛必达法则、2.5 的振荡函数反例。这些题的目的不是比大小而是训练一种“看见表达式就估算增长率”的肌肉记忆。3.1 N log N 与 N log(N²) 同阶对数的底到底重不重要习题 2.1 的答案给了一串函数按增长率排序并且特别注明“N log N 和 N log(N²) 以相同速率增长”。原因一句话就能说清log(N²) 2 log N系数 2 是常数会被大 O 记号吸收。更一般地换底公式 log_a N (log_a b) · (log_b N) 说明任意两个底的对数只差一个常数倍所以复杂度表里从来不写对数的底。很多初学者以为 log₂N 比 log₁₀N “快”但在大 O 眼里它们是同一个函数。习题 2.4 用洛必达法则证明了一个更强的结论logᵏN o(N) 对任意正整数 k 成立。这个结论是“多项式比对数快”的严格版本。实际分析时它还有个直接用途当你看到 O(N log N) 和 O(N) 犹豫不决时记住 log N 的增长在工程规模内几乎就是个“缓慢的常数放大器”——N 10⁶ 时 log₂N 约 20N 10⁹ 时约 30翻三倍数据量只多 50% 的对数开销。3.2 2.7(V)三重循环为什么是 O(N⁵) 而不是 O(N³)习题 2.7 给出五段循环代码要求估算运行时间。(V) 的结构是这样的for ( int i 0; i N; i ) for ( int j 0; j i * i; j ) for ( int k 0; k j; k ) sum;如果只看三层循环嵌套会下意识猜 O(N³)。但 j 的上界是 i²最坏能到 N²k 的上界是 j最坏也能到 N²外层 i 有 N 次。乘起来就是 N · N² · N² O(N⁵)。答案里直接说“运行时间正比于 N·N²·N²”。这里的机制是循环嵌套的复杂度上界 各层循环上界的乘积但上界本身可能依赖外层变量。分析时不能只看“几层循环”要看“每一层最多迭代多少次”并且按最坏情况把依赖链串起来。这道题和 3.1 节配合起来看就是“先建立增长率直觉再打破‘循环层数即复杂度’的错误直觉”的两步训练。3.3 2.7(VI)if 分支如何把 O(N⁵) 砍成 O(N⁴)紧接着的 (VI) 小题是前一段代码加了一个 if 条件常见的实现思路是这样的for ( int i 0; i N; i ) for ( int j 0; j i * i; j ) if ( j % i 0 ) for ( int k 0; k j; k ) sum;教科书答案明确说if 语句本身最多执行 N³ 次但条件为真的次数只有 O(N²)对每个 i恰好有 i 个 j 满足条件所以最内层循环体实际只执行 O(N²) 次每次开销 O(j²) O(N²)总复杂度 O(N⁴) 而非 N⁵。答案里那句话值得反复读“multiplying loop sizes can occasionally give an overestimate”——按循环上界相乘会偶尔高估。这个例子的机制价值在于区分“最坏上界”和“期望开销”。循环头的迭代次数是硬上界但条件分支的真值频率会把实际执行次数拉低。分析真实代码时正确姿势是数“最内层语句实际执行了多少次”而不是数“最内层循环头被进入多少次”。我在实战中见过不止一次照着循环层数相乘估出 O(N⁵)结果压测数据出来接近 O(N⁴)就是因为漏算了分支条件。4. 数学工具是算法的原型级数、模幂与输入规模第一章的 1.8 数列求和、1.10 模运算第二章的 2.20 到 2.24 快速幂——这些题看起来像纯数学其实是后面算法设计的零件。习题答案把它们一步步展开恰好暴露了“数学技巧 → 算法优化”的翻译过程。4.1 差分法求 Σ i/4ⁱ生成函数的雏形习题 1.8(b) 要求计算 S 1/4 2/16 3/64 … 即 S Σᵢ₌₁^∞ i/4ⁱ。答案用的手法是差分把等式两边乘 4 再相减。4S 1 2/4 3/16 4/64 … S 1/4 2/16 3/64 … ————————————————————————————— 3S 1 1/4 1/16 … 4/3右边变成一个纯几何级数直接套用 (a) 小题的结论 Σᵢ₌₀^∞ 1/4ⁱ 4/3得到 S 4/9。(c) 小题对 Σ i²/4ⁱ 用同样的手法一次差分后变成 Σ (2i1)/4ⁱ再拆成两个已知级数最后得 20/27。机制上这是生成函数方法的雏形把带多项式系数的级数通过“乘 x 再相减”逐次降幂直到变成纯几何级数。为什么算法分析需要这个因为期望运行时间经常归结为这类级数——比如随机快速排序的期望比较次数就涉及 Σ i·(某种概率)最终都要用差分或求导技巧求和。习题答案把这套推导一步步写出来比直接给结论更能训练“看到无穷级数先想差分”的条件反射。4.2 2¹⁰⁰ ≡ 1 (mod 5)模周期与快速幂的同一思想习题 1.10 的证明非常短2⁴ 16 ≡ 1 (mod 5) 2¹⁰⁰ (2⁴)²⁵ ≡ 1²⁵ ≡ 1 (mod 5)机制是模运算下的周期性底数 2 在模 5 下的幂从 2⁴ 开始进入周期轨道周期为 4。更一般地说任意底数 a 在模 m 下的幂序列最终会落入一个环环长由 a 的乘法阶决定。这个“分组取模”的思想直接翻译成算法就是快速幂——习题 2.22 到 2.24 围绕它出了一组题。一个标准的取模快速幂实现是public static long modPow( long base, long exp, long mod ) { long result 1 % mod; base % mod; while ( exp 0 ) { if ( ( exp 1 ) 1 ) // 当前二进制位是 1累乘 result result * base % mod; base base * base % mod; // 每一步平方对应“2^4 看成一组” exp 1; } return result; }这个 while 循环每轮把指数减半正好是把指数按二进制展开2¹⁰⁰ 2⁶⁴ · 2³² · 2⁴其中 64、32、4 对应 100 的二进制位。习题 2.24 给出的乘法次数公式 ⌊log N⌋ b(N) − 1b(N) 为二进制中 1 的个数就是这个循环的精确计数。拿 100 验证⌊log₂100⌋ 6b(100) 3乘法次数 6 3 − 1 8和手算一致。数学题里的“分组”在代码里就是“位运算分支”。4.3 习题 2.20为什么 40 位数字比 20 位慢约平方倍2.20 的答案给出了一串递进结论试除法判断素数的复杂度是 O(√N)关键在 (c) 到 (e)——如果用二进制位数 B 表示输入规模则 B O(log N)于是 √N O(2^(B/2))。这意味着一个 20 位二进制数能快速测试40 位二进制数需要的时间大约是前者的平方。机制的深层含义是衡量输入规模应该用“输入编码的长度”而不是“数值本身”。用 B 做规模指标时试除法就是 O(2^(B/2))——指数时间而用 N 做规模指标时它看着像 O(√N)——多项式时间。同一个算法换了度量标准就换了复杂度类别这就是计算复杂性理论里“伪多项式算法”概念的源头输入数值增长一倍位长只加一代价却要平方。习题 2.12 的答案给了更实用的换算表时间预算放大 12000 倍O(N) 算法能处理 120 万规模O(N²) 算法只能处理约 42.5 万O(N³) 算法只剩约 2.3 万。这张表的机制是反解方程C·Nᵏ 12000·C·N₀ᵏ。日常评估“换台快 10 倍的机器能多算多大输入”时这套换算直接可用。5. 避坑笔记递归、边界、随机性的五个翻车现场以下五条坑全部能从习题答案反推出来。每一条我都在真实代码里见过对应的翻车版本。5.1 递归不收敛基例正确不等于调用链终止现象用朴素递归算 F(50) 的斐波那契数等了几分钟不出结果甚至栈溢出。原因基例写对了但每次调用分裂成两个子调用调用树节点数按 φⁿ 增长。第 2.3 节证明的 φⁿ 上界在这里恰好就是灾难本身——证明告诉你增长是指数的工程上就应当避开。解决改成自底向上递推或者用数组做记忆化。判断一个递归能不能用先画一行式每次递归调用后问题规模是否严格缩小、是否只会产生一个子问题。像 F(n−1) F(n−2) 这种一拆二的直接默认指数级改用迭代。5.2 二分查找的 mid 死循环low1, high2 时不进展现象二分查找代码在某两个相邻值之间反复递归永远不返回。习题 2.31 和 2.33 专门为这个现象出了两道题答案原话是“if low 1, high 2, then mid 1, and the recursive call does not make progress”。原因mid 向下取整后等于 low。如果代码逻辑把“目标在 [mid, high]”这种区间交给下一次递归新区间还是 [1, 2]规模没有缩小。解决写递归前先问“这次调用一定比上次小吗”。常用方案是mid low (high - low) / 2配合high mid - 1收缩边界或者保证任何分支都让 high − low 严格减小。5.3 单向链表交换相邻节点直接改 p 就断链现象实现相邻节点交换后链表出现环或者丢掉一段。原因习题 3.2(a) 的答案有个反直觉的细节——交换函数的参数是beforep前驱节点而不是要交换的第一个节点p。单向链表节点只存 next不知道 prev要交换 p 和 afterp必须先让前驱的 next 指向 afterp否则前驱仍然指着 p链就断了。答案里四行重链的顺序不能改p.next afterp.next; // 先把 p 接到后一段 beforep.next afterp; // 前驱跳过 p指向 afterp afterp.next p; // afterp 回头指向 p解决凡是涉及单向链表相邻节点操作画三节点图标清楚“谁还指着谁”。每一步修改前确认被覆盖的 next 是否还有别的地方需要保留。5.4 复杂度高估内层 if 把 N⁵ 砍成 N⁴现象写完三层循环按循环头相乘估出 O(N⁵)实际压测曲线却接近 O(N⁴)。原因习题 2.7(VI) 的答案已经点破——if 语句最多执行 N³ 次但条件为真的次数只有 O(N²)最内层语句的实际执行次数是“条件为真的次数 × 每次开销”不是循环变量乘积。解决分析循环时先数“最内层语句执行了多少次”再数“循环头被进入多少次”。两者不一致时取前者为实际复杂度后者只当最坏上界。5.5 洗牌看起来随机但不均匀27 种随机序列对 6 种排列现象用循环随机交换数组元素洗出来的结果“看起来乱”但统计频次有明显偏置。原因习题 2.8(a) 给了一个绝妙的反例。对 N 3如果每次随机下标都从 0..N−1 取三次交换共有 3³ 27 种等概率的随机数序列而排列只有 3! 6 种。27 不能被 6 整除鸽巢原理保证某些排列必然比另一些更频繁。解决用从后向前、每次只与当前位置之前的元素交换的标准洗牌法。验证一个随机算法是否等概率最简单的手段就是数“随机数序列总数能否被排列总数整除”——这比跑一万次统计快得多。6. 接口与取舍为什么参考答案坚持用迭代器第三章的链表习题答案里几乎所有操作都用Iterator和ListIterator而不是下标访问。这不是风格偏好是被 LinkedList 的复杂度逼出来的设计取舍。3.4 的 intersection 是典型例子两个有序链表各持一个迭代器比较当前元素小的往后走相等则收集并同时双走。每轮迭代至少推进一个指针总步数是 O(|L1| |L2|)。如果把内层改成contains嵌套查找每次查找 O(|L2|)整体 O(|L1|·|L2|)同一个功能差出一个数量级。归并的线性来源于“两个序列都只扫一遍”而只有迭代器能廉价地做到这一点——LinkedList 的 get(i) 要步行 i 步循环里反复 get(i) 就是 O(N²)。所以教材在 3.1 的 printLots 里故意用两个迭代器并行推进在 3.4/3.5 里坚持用 listIterator都是在示范同一个原则列表接口把“游标”而不是“下标”作为第一公民。链表擅长的是顺序访问接口设计就应该让顺序访问成为最自然的写法把随机访问的代价留在 get(i) 的步行里。3.6 的约瑟夫环答案更进一步——M 很大时先取模 M mod NM 超过 N/2 时反向走 N − M 步这是对“结构在变化”的尊重每删除一个节点绕圈的步数就得重新约简。我一般会这么理解这份答案的设计意图它故意保留“可以更快但更复杂”的版本是为了让人看到抽象与性能之间永远是交易。迭代器换来通用性却让随机访问变贵取模换来常数因子却让逻辑多一个分支。从那以后我每次 review 链表和递归代码都强制走一遍四步基例是否收敛、边界是否严格缩小、指针是否还有别处在用、复杂度数的是不是最内层语句。希望帮到你。本文还有配套的精品资源点击获取