ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据结构与算法分析Java版习题答案详解:递归、复杂度与链表实战

数据结构与算法分析Java版习题答案详解:递归、复杂度与链表实战 简介这是《数据结构与算法分析Java语言描述》第三版配套的习题答案文档面向正在学习数据结构与算法的计算机专业学生、考研备考者以及需要巩固Java算法基础的开发者。内容覆盖教材第一章的核心练习包含英文原版习题的完整解答过程资源包内含1个docx文档大小约1.52MB方便阅读、批注与打印。已有2429人学习下载。文档详细解答了文件处理、递归算法、数学归纳法证明、数列求和、模运算及大O符号估计等经典题目例如processFile方法的递归实现、ones(int n)的二进制位数求和以及对对数性质的归纳证明通过逐题研读可深入理解算法分析中的数学推导方法与复杂度估计思路。1. 数据结构与算法分析Java语言描述第三版习题答案一份能直接照跑的解题参考数据结构与算法分析Java语言描述第三版是很多软件专业学生的噩梦来源——教材好买课后题答案却只能靠同学口口相传网上流传的版本还经常缺章少页。这份docx把前三章的习题解答完整整理了出来从递归函数、数学归纳法证明到复杂度排序、链表节点交换覆盖的正是最容易被卡住的章节。它不是给每道题贴一句结论而是给出能直接编译运行的Java代码和完整推导过程适合正在啃课本的在校生、准备Java面试的开发者以及想自己动手验证算法结论的自学者。如果你也被一道归纳题卡了两小时这份答案能省下大把翻论坛的时间。2. 第一章答案拆解递归写法、数学归纳法与数列求和实用技巧2.1 递归两件套processFile与ones(int n)习题1.4讲的是带#include指令的文件递归处理。它的解法是一段典型的递归下降逻辑processFile( fileName ): 打开 fileName 逐行读取 如果当前行是 #include SomeFile: 递归调用 processFile( SomeFile ) 处理当前文件内容 关闭文件这段描述说明了文件I/O里最常见的递归场景嵌套包含的头文件无法用单层循环解决因为包含深度不确定而递归天然匹配这个“未知深度”的树形结构。更关键的是题目里提到的自引用检测——维护一个“尚未结束处理”的文件列表进入递归前先查这个列表重复出现就说明存在循环包含得立刻终止。实际做代码静态扫描时这个列表通常用HashSet存文件绝对路径遇到重复路径直接跳过否则会无限递归栈溢出。习题1.5是另一个更纯粹的递归例子计算一个整数二进制表示中1的个数public static int ones(int n) { if (n 2) { return n; } return n % 2 ones(n / 2); }逻辑说明n % 2取出最低位n / 2把整数右移一位递归累加每一位的值。以ones(13)为例13的二进制是110113%21递归66%20递归33%21递归112直接返回1合计10113。这个递归深度等于二进制位数所以时间复杂度是O(log n)。参数说明n按非负整数设计。如果你把负数传进来Java的/2向零取整-1%2结果是-1而不是1返回值会违背“二进制中1的个数”的定义同样的逻辑用Python跑取余结果又不一样。这就是跨语言移植的第一个坑——递归的数学语义在Java里要谨慎对待负数边界。2.2 数学归纳法从log X X到斐波那契上界习题1.7(a)要证明log X X对X0恒成立。答案用的归纳结构值得反复看先验证0X≤1区间此时log X≤0显然小于X再验证1X≤2log X≤1≤X然后假设pX≤2p区间成立推导2pY≤4p区间log Y 1 log(Y/2)而Y/2落在(p, 2p]内由归纳假设log(Y/2) Y/2所以log Y 1 Y/2 ≤ Y/2 Y/2 Y。这个“区间倍增、区间覆盖”的思路比直接对X放缩更严谨因为log X在接近0处是负无穷直接证明很麻烦。习题1.11(b)的斐波那契上界F_k φ^kφ为黄金比例也用了归纳法但技巧性更强。关键观察是φ满足φ 1 φ²变形得φ⁻¹ φ⁻² 1。归纳步骤F_{k1} F_k F_{k-1} φ^k φ^(k-1) φ^(k1)(φ⁻¹ φ⁻²) φ^(k1)第三节习题1.10的模运算题还展示了快速幂的数学基础2⁴16≡1 (mod 5)所以2¹⁰⁰(2⁴)²⁵≡1²⁵≡1 (mod 5)。这种“先找循环节再取模”的手法在后面学快速幂算法时会反复遇到。2.3 错位相减从4/3到20/27的数列求和推导习题1.8是这份答案里最容易让人看得一头雾水的地方因为docx排版把公式拆散了。理顺之后其实是标准的错位相减链。先看基础公式Σ_{i0}^∞ 1/4^i 4/3基于这个结果求S Σ_{i1}^∞ i/4^i。列两个等式S 1/4 2/16 3/64 … 4S 1 2/4 3/16 4/64 …用4S - S错位相减左边得3S右边除了首项1之外剩下的就是Σ_{i1}^∞ 1/4^i 1/3。所以3S 1 1/3 4/3S 4/9。继续求Σ_{i1}^∞ i²/4^i同样设它为S做4S - S后得到3S 1 3/4 5/16 7/64 …把右边拆成1 Σ(2i1)/4^i再利用Σi/4^i 4/9和Σ1/4^i 1/3算出3S 1 8/9 1/3 20/9S 20/27。习题1.12的平方和、立方和公式同样可以用归纳法或直接代数推导完成答案里给出的是归纳版本平方和Σ(2i-1)N²立方和Σi³(N(N1)/2)²。这里有个经验遇到这种被排版打乱的公式先按题目编号回到教材原题确认求和符号的上下标再按“列等式、错位相减、拆项”三步走基本都能复原。文档里的推导对错判断标准很简单——最终结果代回去能不能自洽。3. 第二章算法分析答案详解复杂度排序、运行时间推算与循环陷阱3.1 复杂度函数排序记住这张表习题2.1给出一串函数要求按增长率从小到大排列。答案的排序是序号函数说明12/N, 37常数级2/N随N增大趋近于02√N低于任何多项式N^kk0.5时3N线性4N log log N比线性略高5N log N, N log(N²)同阶log(N²)2logN常数可忽略6N log²N比N log N高一个log因子7N^1.5介于N log²N和N²之间8N², N² logN平方级9N³立方级102^(N/2), 2^N指数级判断这类排序题核心规则只有三条多项式之间比最高次幂对数函数比任何正次幂增长慢指数函数比任何多项式增长快。最容易错的是N log N和N log(N²)很多人以为后者更快其实差一个常数2渐近上完全同阶。排序结果怎么验证直接取N10⁶代进去算近似值或者画一张双对数坐标图立刻能看出哪几个函数挤在一起。面试里问“N log N和N log(N²)谁增长快”是经典陷阱答案是“一样快”。3.2 运行时间推算从0.5ms到62.5ms的直觉建立习题2.11假设某算法处理当前输入耗时0.5ms输入规模扩大5倍问各复杂度下的新耗时复杂度计算新耗时O(N)0.5 × 52.5 msO(N log N)0.5 × 5 × (log5N / logN)略大于2.5 msO(N²)0.5 × 2512.5 msO(N³)0.5 × 12562.5 ms这里的O(N log N)项没法给精确倍数因为log5N/logN和N有关。N越小这个比值越大N趋近无穷时它趋近1。实际工作中我一般估算成“比线性倍率大一点点”如果面试官追问就给出这个公式。习题2.12反过来问时间限制扩大12000倍输入规模能扩大多少答案分别是线性扩大12000倍平方级扩大√12000≈109.54倍立方级扩大12000^(1/3)≈22.9倍N log N级别答案是“约425000”——但要注意这不是固定倍率它依赖基线N。假设基线N100N log N约664时间扩大12000倍后要解M log M ≈ 664×12000M才约等于425000。理解这点比背数字重要因为真实系统的性能估算永远依赖当前基线的位置。3.3 六段代码复杂度循环层数相乘为什么翻车习题2.7给了六段循环嵌套代码答案是严肃的算法分析训练。快速说结论代码段外层中层内层实际复杂度IN无无O(N)IINN无O(N²)IIINNNO(N³)IVN/2N无O(N²)VNi²jO(N⁵)VINNNO(N⁴)最值得展开的是VI。它的三层循环每一层都到N但内层有一句if条件只有条件成立才进入最内层的O(N)操作。答案说这个条件总共只成立O(N²)次所以总时间是O(N²)×O(N)O(N⁴)而不是O(N⁵)。这就是“循环层数相乘”的局限——它只在所有分支等概率进入时成立。遇到带条件的内层循环必须统计条件成立次数而不是直接数循环层数。这道题在面试里也常变体出现一个看起来O(N³)的三重循环因为一个if (ij k)硬生生变成O(N²)考察的就是你会不会用条件频率代替循环深度。3.4 随机排列与有序矩阵搜索从洗牌到线性扫描习题2.8讨论三种生成随机排列的算法。第一种逐位随机生成重复就重试期望复杂度O(N² log N)第二种用集合维护已选元素降到O(N log N)第三种是Fisher-Yates洗牌的变体线性完成。答案特别指出如果洗牌算法里把swap(a[i], a[randint(i, n-1)])改成randint(0, n-1)均匀性会立刻被破坏。证明方式很巧妙——N3时randint(0,2)共有27种等概率交换序列而6个排列无法被27整除所以排列不可能是等概率的。习题2.27是有序矩阵搜索矩阵每行每列都递增从右上角开始当前值大于目标就左移小于目标就下移最多NM次比较。这是一道高频面试题比二分查找更考察“利用有序性”的思维。习题2.28的最大差值问题则是一维扫描的经典——维护扫描到当前位置前的最小值用当前值减最小值更新答案一趟O(N)收工。这两道题在答案里都给了完整推演建议直接背下来当模板。4. 第三章链表答案实战printLots、节点交换、交集并集与Josephus问题4.1 printLots(L, P)同步推进两个迭代器习题3.1要求打印链表L中由位置列表P指定的所有元素且P中的位置是递增的。答案用两个迭代器同步推进避免了每次从头遍历链表import java.util.Iterator; import java.util.List; 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); while (start itemP iterL.hasNext()) { start; itemL iterL.next(); } System.out.println(itemL); } }逻辑说明iterP每次取一个位置itemPiterL在内层循环里推进itemP - start步把start更新到当前位置。两个迭代器都只往一个方向走总体代价是O(|L| |P|)而不是O(|L|×|P|)。要注意P必须递增如果P是无序列表这个算法直接失效得改用哈希表缓存下标。边界条件需要注意当itemP超过链表长度时内层循环会因iterL.hasNext()为false而退出此时itemL是链表的最后一个节点会被重复打印。严格的做法是在内层循环结束后判断start itemP是否成立不成立就说明位置越界。答案省略了这个保护实际复用时要自己加。4.2 swapWithNext单链表与双链表的指针重连习题3.2要求交换相邻两个节点。单链表的做法是操作待交换节点的前驱beforeppublic static void swapWithNext(Node beforep) { Node p beforep.next; Node afterp p.next; // p和afterp假定都不为null p.next afterp.next; beforep.next afterp; afterp.next p; }双链表版本则需要同时维护prev和next答案的写法是public static void swapWithNext(Node p) { Node beforep p.prev; Node afterp p.next; p.next afterp.next; beforep.next afterp; afterp.next p; p.next.prev p; p.prev afterp; afterp.prev beforep; }逻辑说明双链表交换的头疼之处在于四个方向都要断链。p.next afterp.next先把p的后继接到afterp后面p.next.prev p把p的新后继的prev指回pafterp.prev beforep让afterp的prev指向beforep之后beforep.next afterp和afterp.next p完成剩余两处连接。顺序可以微调但最后一步一定是补p.prev afterp否则p的prev还指着afterp下次遍历会死循环。两个版本都没做空指针检查。如果在链表的tail上调用双链表版本afterp.next是nullp.next.prev p会空指针。真正放进生产代码前必须加判空或者约定“禁止对尾节点调用此方法”。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; } else { itemL2 iterL2.hasNext() ? iterL2.next() : null; } } }逻辑说明compareTo返回负值说明L1的元素小链表L1推进正值说明L2的小链表L2推进相等则收入结果集、两边同时推进。整个过程和归并排序的merge阶段一模一样。并集版本只在分支处理上不同相等时加一个并推进两边不等时加较小的那个再推进对应链表。这里有个容易被忽略的细节extends Comparable? super AnyType这个泛型约束不是装饰它允许AnyType自身实现Comparable也允许它的父类实现Comparable。实际工程里如果你定义了一个子类继承字符串包装类这个约束能在编译期就兜住类型不匹配。题目答案里反复出现这个签名值得在IDE里敲一遍体会它的作用。4.4 Josephus问题从M mod N到反向搜索习题3.6的Josephus问题是链表章节的压轴题N个人围成一圈每数到M就删除问最后留下谁。最朴素的做法是每次都从头遍历M步总复杂度O(N·M)。答案给出两个实用改进。第一个改进是M mod N当M大于当前剩余人数时绕圈多走的完整圈数没有意义取模可以避免大量无效遍历。第二个改进是反向搜索当M超过剩余人数的一半时从当前位置往前找N - M步等价于往后找M步。这两个改进叠加后最坏时间复杂度是O(N·min(M, N))但多数情况下远快于这个上界。实际写的时候还要补一层边界处理M1时直接删头节点即可O(1)N1时连删除都不用做。典型的翻车现场是把M mod N写错成M % size但size在删除过程中不断变化取模时机不同结果就不同。正确做法是每次删除前用最新size取模而不是只在开始时取一次。5. 避坑指南习题答案在学习与复现时最容易翻车的四个细节5.1 公式乱码与排版拆解现象docx里出现“4S  1  2  3 .442”这种断行乱码连求和符号都被拆成碎片。原因原文档排版时混用了公式编辑器域代码和普通文本不同软件渲染时对上下标、分数线的支持不一致尤其用在线预览或文本编辑器打开时几乎必乱。解决用Office或WPS打开不要用记事本类的工具。遇到乱码公式根据题号回到教材原题对照按“列出等式、错位相减、代入已知结果”三步自己重推一遍比盯着乱码猜要快得多。5.2 泛型代码缺import导致编译失败现象直接复制3.1节的printLots代码javac报“找不到List”或“找不到Iterator”。原因答案文档省略了import java.util.*。这在竞赛代码里常见但直接照抄到IDE里必然编译失败。解决开头补全三行import java.util.List;、import java.util.Iterator;、import java.util.ListIterator;。如果你在Eclipse或IDEA里多数情况下快捷键自动补全但建议理解每个import对应哪个接口面试时被问到能说清楚。5.3 printLots越界与非法位置输入现象L有5个元素P里传了位置100程序不报错打印出链表的最后一个元素。原因内层while (start itemP iterL.hasNext())在链表耗尽时静默退出itemL停在最后一个节点上。题目默认P中位置合法但实际传参没有这种保证。解决在内层循环结束后检查start itemP不相等直接抛IllegalArgumentException或者在调用前用L.size()和P的每个元素做一次合法性校验。5.4 循环复杂度直接相乘导致误判现象看到三层嵌套循环就写O(N³)看到更深的嵌套直接写O(N⁵)结果2.7(VI)的正确答案是O(N⁴)。原因循环层数相乘只在无条件进入内层时成立遇到if分支就必须统计成立次数。很多人栽在“内层O(N²)操作对外层每次循环都执行”的错误假设上。解决逐层分析。先确认最内层语句是否每次都被执行统计条件成立的总次数再乘以单次执行代价。养成“先算执行次数再算单次代价”的习惯复杂度分析就不容易翻车。6. 把习题答案变成面试素材验证、改写与追问拿到这份答案最高效的用法不是“看”而是“跑”。我习惯把每段Java代码复制到IDE里写一个main方法做单元验证。比如ones(int n)用标准库的Integer.bitCount(n)做基准循环0到1000逐个断言public static void main(String[] args) { for (int i 0; i 1000; i) { int expected Integer.bitCount(i); int actual ones(i); if (expected ! actual) { System.out.println(Mismatch at i); } } }这一步能把“我以为我懂了”变成“我确认它是对的”。跑通之后再想两个追问ones的递归深度是多少对int输入最多递归31次。能不能改成循环当然能while (n 0) { count n % 2; n / 2; }。能不能更快用n (n - 1)逐次消掉最低位的1循环次数等于1的个数而非位数。这三个追问在Java面试里出现的频率不低答案文档只给了递归版后面两层要靠自己推。另一类面试高频点是复杂度排序表。建议按3.1节的表格默写三遍直到能在一分钟内从2/N排到2^N。遇到“N log N和N log(N²)谁快”这种送命题能脱口而出“同阶”才算过关。最后是链表题的改写。把printLots改成用L.get(itemP)的随机访问版本对比一下性能差异你就明白为什么题目强调“链表上不能用下标”。把swapWithNext加空指针保护再补一个单元测试覆盖“尾部节点交换抛异常”的场景——这些改动让教材答案变成了生产级代码面试官问到边界处理时你也能接得住。从那以后我拿到任何一份习题答案都强制自己先盲写一遍再对照把所有代码跑起来验证过才敢说“会了”。这份docx虽然排版乱、公式散但核心推导和代码都是对的值得花一下午消化。希望帮到你。本文还有配套的精品资源点击获取
返回列表