<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>算法 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E7%AE%97%E6%B3%95/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 02 Oct 2025 14:20:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E7%AE%97%E6%B3%95/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《图解算法》学习笔记：一把尺子、一张地图，和算法的直觉</title>
      <link>https://ftmao.com/blog/posts/grokking-algorithms-book-notes/</link>
      <pubDate>Thu, 02 Oct 2025 14:20:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/grokking-algorithms-book-notes/</guid>
      <description>&lt;h1 id=&#34;图解算法学习笔记一把尺子一张地图和算法的直觉&#34;&gt;《图解算法》学习笔记：一把尺子、一张地图，和算法的直觉&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：会写代码但没系统学过算法、一碰到&amp;quot;该用什么数据结构/算法&amp;quot;就凭感觉的新手；也适合学过但忘光了、想快速重建地图的老手。&lt;br&gt;
参考书籍：Aditya Bhargava《图解算法》（Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People）&lt;br&gt;
阅读目标：不追求覆盖每个细节，只提炼全书的骨架——&lt;strong&gt;一把评估工具的尺子（大 O），一张&amp;quot;问题形状 → 工具&amp;quot;的地图&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面一本刻意不完整的算法书&#34;&gt;0. 写在前面：一本刻意&amp;quot;不完整&amp;quot;的算法书&lt;/h2&gt;
&lt;p&gt;算法书的光谱上，两端各有一座大山：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一端是《算法导论》（CLRS）：全面、严谨、数学化，厚得能挡子弹，也厚得让大多数人停在第三章。&lt;/li&gt;
&lt;li&gt;另一端是刷题网站：直接给你题目和答案，见效快，但题目之间的&amp;quot;地形&amp;quot;要自己去悟。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;《图解算法》刻意站在这两端之外。作者 Bhargava 是工程师出身、靠画画解释概念出名（他维护过 illustrated 技术博客），这本书的每一个算法都先配一幅手绘风格的图，再配一段短代码。它覆盖的主题只有十来个，每个都只讲&amp;quot;够用&amp;quot;的深度。&lt;/p&gt;
&lt;p&gt;这不是缺点，是设计。这本书的定位是&lt;strong&gt;第一张地图&lt;/strong&gt;：让你在&amp;quot;问题&amp;quot;和&amp;quot;工具&amp;quot;之间先建立起粗糙但正确的连接，知道某个形状的问题该往哪个方向走。地图粗糙没关系，怕的是手里连地图都没有。&lt;/p&gt;
&lt;p&gt;全书十一章，蒸馏到最后其实是两件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一把尺子&lt;/strong&gt;：大 O 表示法，用来评估任何工具的性能。&lt;br&gt;
&lt;strong&gt;一张地图&lt;/strong&gt;：识别问题的形状，映射到对应的工具——二分查找、分治、散列表、图搜索、贪婪、动态规划。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面按这条主线展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-大-o全书通用的货币&#34;&gt;1. 大 O：全书通用的货币&lt;/h2&gt;
&lt;p&gt;书从二分查找讲起，但二分查找只是引子，真正的主角是&lt;strong&gt;大 O 表示法&lt;/strong&gt;——后面每一章都在花这种&amp;quot;货币&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二分查找本身&lt;/strong&gt;：在有序数组里找元素，每次猜中间值，把搜索空间砍半。100 个元素最多 7 次猜中，40 亿个元素最多 32 次。前提只有一个：&lt;strong&gt;数据必须有序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大 O 说的不是时间，是增长趋势&lt;/strong&gt;。它回答的问题是：&amp;ldquo;输入规模翻倍，运行时间怎么变？&amp;ldquo;书里反复出现的五种：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;大 O&lt;/th&gt;
					&lt;th&gt;名字&lt;/th&gt;
					&lt;th&gt;书里的例子&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;O(log n)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;对数时间&lt;/td&gt;
					&lt;td&gt;二分查找&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;线性时间&lt;/td&gt;
					&lt;td&gt;简单查找（逐个翻）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;O(n log n)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;快速排序（平均）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;O(n^2)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;选择排序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;O(n!)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;旅行商问题的暴力解&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;新手最容易误解的三点，书里都点了名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大 O 是最坏情况&lt;/strong&gt;。简单查找运气好时第一次就命中，但大 O 管的是&amp;quot;倒霉到底&amp;quot;的那个上界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大 O 吃掉了常数，但常数有时很重要&lt;/strong&gt;。快排的常数因子比归并排序小，所以两者同为 &lt;code&gt;O(n log n)&lt;/code&gt;，实践中快排通常更快——这是第四章的一个伏笔。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两种算法的差距会随规模放大&lt;/strong&gt;。&lt;code&gt;O(log n)&lt;/code&gt; 对 &lt;code&gt;O(n)&lt;/code&gt;，在小数据上无所谓，在 40 亿条数据上是 32 次对 40 亿次。评估算法时先问自己：我的数据会涨到多大？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这把尺子从此不离手：后面每拿出一个新工具，第一件事就是量一量它。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-数据结构的地基数组与链表&#34;&gt;2. 数据结构的地基：数组与链表&lt;/h2&gt;
&lt;p&gt;第三章之前，书先补了一块地基：&lt;strong&gt;内存里的数据只有两种基本摆法&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;需要随机访问&amp;lt;br/&amp;gt;按下标直接拿&amp;#34;] --&amp;gt; B[&amp;#34;数组&amp;lt;br/&amp;gt;读取 O(1)&amp;lt;br/&amp;gt;插入删除 O(n)&amp;#34;]
    C[&amp;#34;需要频繁插入删除&amp;lt;br/&amp;gt;顺序遍历为主&amp;#34;] --&amp;gt; D[&amp;#34;链表&amp;lt;br/&amp;gt;读取 O(n)&amp;lt;br/&amp;gt;插入删除 O(1)&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数组&lt;/strong&gt;：元素在内存里连排，知道下标就能直接跳过去；但插入一个元素，后面所有人都得挪窝，而且数组满了还得整体搬家。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链表&lt;/strong&gt;：每个元素存着下一个的地址，插入删除只要改指针；但想找第 5 个，只能从头数过去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选择排序&lt;/strong&gt;在这里出场，不是因为它重要，而是因为它是一个绝佳的&amp;quot;反面教具&amp;rdquo;：每轮扫一遍数组找最小值，n 个元素扫 n 轮，&lt;code&gt;O(n^2)&lt;/code&gt;。它存在的意义是让你亲身体会&amp;quot;为什么 &lt;code&gt;O(n^2)&lt;/code&gt; 不能接受&amp;rdquo;，给后面的快排当对照组。&lt;/p&gt;
&lt;p&gt;这一章的真正收获是一个思维模式：&lt;strong&gt;先说操作模式，再选数据结构&lt;/strong&gt;。读写各半？那就得接受折中（后面散列表给出答案）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-递归与分治把问题缩到基线&#34;&gt;3. 递归与分治：把问题缩到基线&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;递归&lt;/strong&gt;在书里被拆成一句话：每个递归函数都有两部分——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基线条件（base case）&lt;/strong&gt;：什么时候不再调用自己；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归条件（recursive case）&lt;/strong&gt;：什么时候调用自己。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;漏掉基线条件就是死循环。而理解递归的物理载体是&lt;strong&gt;调用栈&lt;/strong&gt;：每次调用往栈上压一层，栈顶永远是当前正在执行的函数。栈好用，但深度失控会爆栈——这是递归的隐形成本。&lt;/p&gt;
&lt;p&gt;递归的最大用处是支撑**分治（Divide and Conquer）**策略。书里给分治的解题套路是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找出尽可能简单的基线条件；&lt;/li&gt;
&lt;li&gt;不断把问题分解（缩小规模），直到触及基线条件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;书的例子很妙：把一块 1680×640 的地均匀分成方块、要求方块尽可能大——每次&amp;quot;切掉一块最大的正方形，对剩下的部分重复同样的操作&amp;quot;，最后收敛到欧几里得算法。分治不是算法，是&lt;strong&gt;想问题的姿势&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;快速排序&lt;/strong&gt;是分治的招牌应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基线条件：数组元素少于 2 个，不用排。&lt;/li&gt;
&lt;li&gt;递归条件：选一个基准值（pivot），把数组分成&amp;quot;比它小&amp;quot;和&amp;quot;比它大&amp;quot;两堆，对两堆分别快排。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;快排的复杂度很讲政治：&lt;strong&gt;平均 &lt;code&gt;O(n log n)&lt;/code&gt;，最坏 &lt;code&gt;O(n^2)&lt;/code&gt;&lt;/strong&gt;——最坏情况出现在每次都选到极端 pivot（比如对已排序数组总选第一个元素）。所以 pivot 的选择就是性能的关键，随机选是最省事的保险。&lt;/p&gt;
&lt;p&gt;记住那个伏笔：快排平均情况的常数比归并排序小，所以它是工程实践的默认选择。&lt;strong&gt;大 O 相同不代表一样快，常数在日复一日地收税。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-散列表用函数换时间&#34;&gt;4. 散列表：用函数换时间&lt;/h2&gt;
&lt;p&gt;如果说数组和链表是两种&amp;quot;认命&amp;quot;的摆法，&lt;strong&gt;散列表（hash table）就是那个&amp;quot;我全都要&amp;quot;的答案&lt;/strong&gt;：平均情况下，查找、插入、删除全是 &lt;code&gt;O(1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它的构造朴素得不像话：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;键 key&amp;#34;] --&amp;gt; B[&amp;#34;散列函数&amp;lt;br/&amp;gt;把任意输入映射成固定数字&amp;#34;]
    B --&amp;gt; C[&amp;#34;数组下标&amp;#34;]
    C --&amp;gt; D[&amp;#34;值 value&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一个合格的散列函数要满足：同样的输入永远得到同样的输出；不同的输入尽量映射到不同位置。于是&amp;quot;查找&amp;quot;从&amp;quot;翻箱倒柜&amp;quot;变成了&amp;quot;算一下地址直接拿&amp;quot;。&lt;/p&gt;
&lt;p&gt;书里的三个经典用法，值得刻进肌肉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找表&lt;/strong&gt;：电话簿、商品价格、DNS 解析——&lt;code&gt;dict&lt;/code&gt;/&lt;code&gt;map&lt;/code&gt; 存在的意义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去重&lt;/strong&gt;：投票站查&amp;quot;这人投过票没有&amp;quot;，把逐个翻名单变成 &lt;code&gt;O(1)&lt;/code&gt; 查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：服务器把算过的结果按 URL 存起来，下次直接给——所有 Web 框架都在这么干。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但散列表不是免费的，书也讲了两条暗线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;冲突（collision）&lt;/strong&gt;：两个键被映射到同一位置。标准解法是&lt;strong&gt;链表法&lt;/strong&gt;——这个位置存一个链表，所有撞车的都挂上去。冲突多了，散列表就退化成了链表，性能从 &lt;code&gt;O(1)&lt;/code&gt; 滑向 &lt;code&gt;O(n)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载因子（load factor）= 元素数 / 位置数&lt;/strong&gt;。负载因子超过约 0.7 就该&lt;strong&gt;扩容&lt;/strong&gt;（换个更大的数组重新散列）。扩容昂贵，但摊销到每次操作上仍然便宜。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程直觉：&lt;strong&gt;散列表的平均 &lt;code&gt;O(1)&lt;/code&gt; 建立在&amp;quot;散列函数好 + 负载受控&amp;quot;两个前提上&lt;/strong&gt;。前提塌了，最坏 &lt;code&gt;O(n)&lt;/code&gt; 在等着你。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-图搜索一广度优先用队列换最短&#34;&gt;5. 图搜索（一）：广度优先，用队列换最短&lt;/h2&gt;
&lt;p&gt;第六章把场景从&amp;quot;数组里找东西&amp;quot;升级到&amp;quot;网络里找关系&amp;quot;：在你的朋友关系网里，谁认识芒果经销商？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图&lt;/strong&gt;就是节点加边，是对&amp;quot;关系&amp;quot;的建模。广度优先搜索（BFS）回答两类问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 A 到 B 有没有路？&lt;/li&gt;
&lt;li&gt;从 A 到 B 的&lt;strong&gt;最短路径&lt;/strong&gt;（经过的边最少）是哪条？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;算法的核心道具是&lt;strong&gt;队列（FIFO）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把起点的朋友全部入队；&lt;/li&gt;
&lt;li&gt;每次从队首取一个人检查：是芒果经销商吗？不是就把他的朋友加到队尾；&lt;/li&gt;
&lt;li&gt;队列为空还找不到，就是不存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么队列保证&amp;quot;最短&amp;quot;？因为你按距离分层推进：一度人脉全部查完，才轮到二度人脉。第一个被找到的目标，一定在最少边数的那条路上。&lt;strong&gt;换栈（LIFO）就变成深度优先，性质全变&lt;/strong&gt;——道具即语义。&lt;/p&gt;
&lt;p&gt;两个工程细节别漏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必须记录&amp;quot;已检查&amp;quot;名单&lt;/strong&gt;（正好用上一章的散列表），否则关系网里有环时会在原地打转。&lt;/li&gt;
&lt;li&gt;检查名单还顺便去重：同一个人被多个人介绍，只查一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;树是特殊的图（没有环、没有指回祖先的边），所以这一章的思维方式覆盖了文件系统、组织结构、依赖关系等大量日常问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-图搜索二狄克斯特拉带权重的最短&#34;&gt;6. 图搜索（二）：狄克斯特拉，带权重的最短&lt;/h2&gt;
&lt;p&gt;BFS 找的是&amp;quot;段数最少&amp;quot;，但现实里每段路的代价不同——换乘 2 次但花 2 小时，和直达但花 3 小时，选哪个？&lt;strong&gt;狄克斯特拉算法（Dijkstra）&lt;strong&gt;处理的就是&lt;/strong&gt;带权图&lt;/strong&gt;的最短路径。&lt;/p&gt;
&lt;p&gt;它的循环只有四步：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;找出当前开销最小&amp;lt;br/&amp;gt;且未处理的节点&amp;#34;] --&amp;gt; B[&amp;#34;检查它的邻居&amp;lt;br/&amp;gt;经由它是否更便宜&amp;#34;]
    B --&amp;gt; C[&amp;#34;更便宜就更新&amp;lt;br/&amp;gt;邻居的开销和父节点&amp;#34;]
    C --&amp;gt; D[&amp;#34;标记该节点已处理&amp;#34;]
    D --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键直觉是&lt;strong&gt;贪心的有序性&lt;/strong&gt;：一个&amp;quot;当前最便宜&amp;quot;的节点一旦被处理，它的最终开销就锁定了——因为不存在更便宜的迂回路线能再改进它（这正是不能有负权边的原因）。&lt;/p&gt;
&lt;p&gt;书明确划了两条边界，比算法本身还值钱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;负权边禁用狄克斯特拉&lt;/strong&gt;，改用贝尔曼-福特算法（Bellman-Ford）。负权边意味着&amp;quot;绕路反而赚钱&amp;quot;，廉价节点处理完后仍可能被改进，四步循环的前提崩了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFS 是狄克斯特拉的特例&lt;/strong&gt;：所有边权重相等时，&amp;ldquo;段数最少&amp;quot;就是&amp;quot;开销最小&amp;rdquo;。学算法时这种&amp;quot;一般与特例&amp;quot;的父子关系，比单独记两个算法省一半脑子。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-贪婪算法局部最优的边界&#34;&gt;7. 贪婪算法：局部最优的边界&lt;/h2&gt;
&lt;p&gt;有些问题不存在快速精确解，&lt;strong&gt;贪婪算法&lt;/strong&gt;的哲学是退而求其次：每步都选当前看起来最好的，拼出一个&amp;quot;足够好&amp;quot;的解。&lt;/p&gt;
&lt;p&gt;书里的两个对比案例是这个哲学的完整两面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;教室调度&lt;/strong&gt;：要在一间教室排最多的课。策略是&amp;quot;每步选结束最早的课&amp;quot;——这里的贪婪恰好就是最优解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背包问题&lt;/strong&gt;：小偷往包里装最值钱的商品，按&amp;quot;单价最高&amp;quot;贪婪地装，装不满，也装不到总价值最大——这里的贪婪只能拿到近似解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同一个策略，一个精确一个近似，差别在问题结构。所以书的真正教益是后半章：&lt;strong&gt;识别 NP 完全问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;旅行商问题、集合覆盖问题这类难题，精确解的复杂度是 &lt;code&gt;O(n!)&lt;/code&gt; 或 &lt;code&gt;O(2^n)&lt;/code&gt;，规模稍大就算不动。书里给的判断流程非常工程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;元素少时算法很快，元素一多就爆炸？可疑。&lt;/li&gt;
&lt;li&gt;&amp;ldquo;所有组合&amp;quot;的字眼出现在解法里？可疑。&lt;/li&gt;
&lt;li&gt;要算所有子集/所有排列才能出答案？基本坐实。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一旦确认撞上 NP 完全问题，&lt;strong&gt;正确动作不是硬算，而是换目标&lt;/strong&gt;：用贪婪算法拿近似解。集合覆盖问题里，&amp;ldquo;每步选覆盖最多未覆盖电台的台&amp;quot;这个贪婪策略，能以 &lt;code&gt;O(n^2)&lt;/code&gt; 的代价拿到足够好的答案。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;完美是优秀的敌人，在 NP 完全问题面前，这句话是数学事实而不是鸡汤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-动态规划把大问题拆成可缓存的小问题&#34;&gt;8. 动态规划：把大问题拆成可缓存的小问题&lt;/h2&gt;
&lt;p&gt;第九章是全书的智力高峰。&lt;strong&gt;动态规划（Dynamic Programming）&lt;strong&gt;的场景是：问题可以拆成子问题，且子问题的最优解能拼出大问题的最优解——那就&lt;/strong&gt;先解子问题、存起来、再组装&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;书用背包问题把整套方法演了一遍：背包容量 4 磅，商品有吉他、音响、笔记本电脑，各有限重和价值，怎么装总价值最大？&lt;/p&gt;
&lt;p&gt;答案是画一张&lt;strong&gt;网格&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;行是商品（逐个加入考虑范围）；&lt;/li&gt;
&lt;li&gt;列是背包容量（1 磅到 4 磅，每个容量是一个子问题）；&lt;/li&gt;
&lt;li&gt;每个单元格填：&amp;ldquo;只考虑前 i 件商品、容量为 j 时，能装的最大价值&amp;rdquo;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;填第 i 行第 j 列时，只有两种选择取最大：不装第 i 件（抄上一行的值），或者装（这件的价值 + 剩余容量在上一行的最优值）。填到右下角，答案自然浮现。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;大问题&amp;lt;br/&amp;gt;容量4 全部商品&amp;#34;] --&amp;gt; B[&amp;#34;子问题网格&amp;lt;br/&amp;gt;每个单元格 = 部分商品 + 部分容量&amp;#34;]
    B --&amp;gt; C[&amp;#34;按行填表&amp;lt;br/&amp;gt;每格取两种选择的最大值&amp;#34;]
    C --&amp;gt; D[&amp;#34;右下角 = 原问题的最优解&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;网格法同样解了&lt;strong&gt;最长公共子串&lt;/strong&gt;（比如拼写检查里 hunish 更像 hunch 还是 fish）和&lt;strong&gt;最长公共子序列&lt;/strong&gt;（diff 工具、生物序列比对的雏形）。两者的网格公式不同，书特意对比：子串要求连续，只在左上角相邻格匹配时加一；子序列允许跳跃，不匹配时取左格和上格的较大者。&lt;/p&gt;
&lt;p&gt;动态规划的使用前提，书里埋了两条硬规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个子问题必须是&lt;strong&gt;离散的、不互相依赖&lt;/strong&gt;的——背包里装两件商品的&amp;quot;组合优惠&amp;rdquo;（比如去巴黎的行程互相抢时间）会毁掉方法；&lt;/li&gt;
&lt;li&gt;它优化的是&lt;strong&gt;某个约束下的某个指标&lt;/strong&gt;，先想清楚约束是什么、指标是什么，再画网格。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新手对动态规划的恐惧，九成来自没见过&amp;quot;网格&amp;quot;这个脚手架。见过一次，DP 就从玄学变成了填表。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;9-k-最近邻从算法走向机器学习的一脚&#34;&gt;9. K 最近邻：从算法走向机器学习的一脚&lt;/h2&gt;
&lt;p&gt;第十章画风突变，讲起了&lt;strong&gt;K 最近邻（KNN）&lt;/strong&gt;——它不是传统意义上的&amp;quot;精确算法&amp;rdquo;，而是通往机器学习的引桥。&lt;/p&gt;
&lt;p&gt;套路分两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;特征抽取&lt;/strong&gt;：把对象变成一组可比较的数值。判断橙子还是柚子，可以抽&amp;quot;大小&amp;quot;和&amp;quot;颜色&amp;quot;两个特征；推荐电影，就把用户口味变成若干维评分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;找邻居算距离&lt;/strong&gt;：新对象来了，找特征空间里最近的 K 个已知对象，分类就看邻居里哪类多，回归就取邻居的平均值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;书顺手点了两条暗线：&lt;strong&gt;特征选得好不好决定一切&lt;/strong&gt;（推荐系统里&amp;quot;喜好&amp;quot;这种主观特征比&amp;quot;年龄&amp;quot;有用得多）；以及&lt;strong&gt;回归&lt;/strong&gt;——不只分类，还能预测数值（比如预测明天面包销量）。第十章还拿 OCR 和垃圾邮件过滤、预测股市当例子，并诚实地泼冷水：股市这种受太多不可测因素影响的对象，KNN 也无能为力。&lt;/p&gt;
&lt;p&gt;这一章不求深，它是在全书结尾处把一扇门推开一条缝：算法世界的下一层是统计学习，特征工程是那层世界的&amp;quot;数据结构&amp;quot;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;10-最后一章的真正价值一张进阶地图&#34;&gt;10. 最后一章的真正价值：一张进阶地图&lt;/h2&gt;
&lt;p&gt;第十一章标题是&amp;quot;接下来如何做&amp;quot;，看似附录，实则是这本书最像&amp;quot;地图&amp;quot;的一章——它把十个更大的主题各用一页纸点了名，告诉你每个是干什么的、什么时候会撞上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二叉查找树&lt;/strong&gt;：有序数据的插入查找都比排序数组灵活，是数据库索引的原型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反向索引&lt;/strong&gt;：搜索引擎的核心——给每个词建一张&amp;quot;出现在哪些网页&amp;quot;的表，正好是散列表思路的工业化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;傅里叶变换&lt;/strong&gt;：把信号拆成频率成分，MP3 压缩、地震预测、音乐识别都靠它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;布隆过滤器与 HyperLogLog&lt;/strong&gt;：海量数据下的概率型判断——&amp;ldquo;这个 URL 我爬过吗？&amp;ldquo;允许极小的误判率，换来指数级的空间节省。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SHA 与局部敏感哈希&lt;/strong&gt;：前者做安全散列和密码存储，后者（如 Simhash）让&amp;quot;相似内容&amp;quot;散列到相近位置，是查重和推荐的底层件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diffie-Hellman 密钥交换&lt;/strong&gt;：双方在不安全的信道上协商出共享密钥，现代加密的基石。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线性规划&lt;/strong&gt;：在给定约束下最大化目标，图论问题是它的特例，运筹学的发动机。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MapReduce 与并行算法&lt;/strong&gt;：数据大到单机装不下时，把计算分发到成百上千台机器；并行排序的最优复杂度也从 &lt;code&gt;O(n log n)&lt;/code&gt; 变成约 &lt;code&gt;O(n)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一章的正确读法不是&amp;quot;学会&amp;rdquo;，而是&lt;strong&gt;建立索引&lt;/strong&gt;：以后在真实工作里撞见&amp;quot;爬虫去重&amp;quot;&amp;ldquo;密码存储&amp;quot;&amp;ldquo;海量计数&amp;quot;这些词，你能想起&amp;quot;好像有个专门的东西解决这个&amp;rdquo;，再回头深查。地图的价值不在精细，在覆盖。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;11-带走的清单&#34;&gt;11. 带走的清单&lt;/h2&gt;
&lt;p&gt;把全书压成一张工位清单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;评估任何工具时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 先问数据规模会涨到多大——小数据上无所谓的差距，大数据上是生死线。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 用大 O 说最坏情况，别用&amp;quot;运气好&amp;quot;的例子安慰自己。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 大 O 相同时，记得问常数因子（快排 vs 归并）；常数在日复一日地收税。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 空间和时间一样要算：递归的调用栈、散列表的扩容、DP 的网格都是空间账单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;看到问题时先认形状&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 数据有序、要查找？二分查找，&lt;code&gt;O(log n)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 要&amp;quot;按 key 秒查 / 去重 / 缓存&amp;rdquo;？散列表，盯紧负载因子。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 关系网里找&amp;quot;最少几步&amp;rdquo;？BFS + 队列 + 已检查名单。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 带代价的图里找&amp;quot;最便宜&amp;quot;？狄克斯特拉，先确认没有负权边。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 问题能拆成独立子问题且子问题可复用？画 DP 网格，先写清约束和优化指标。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 解法里出现&amp;quot;所有组合/所有排列&amp;quot;？多半是 NP 完全，别硬算，贪婪近似走起。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;卡住时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 试试分治：基线条件是什么？能不能把规模砍掉一块再递归？&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 试试换数据结构：很多&amp;quot;算法难题&amp;quot;其实是&amp;quot;数据摆错了地方&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 识别&amp;quot;当前问题是不是某个经典问题的特例&amp;quot;——BFS 之于狄克斯特拉，图论之于线性规划，特例意味着现成解法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;继续学&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 把第十一章当索引：布隆过滤器、反向索引、傅里叶变换、MapReduce，撞见了再深挖。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 想系统化：去啃《算法导论》对应章节，那时你有地图了，不会迷路。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 想练手感：刷题，但每道题先归类&amp;quot;这是什么形状的问题&amp;quot;，再看答案。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这本书两百多页、图比字多，值回票价的理由只有一个：&lt;strong&gt;它把&amp;quot;算法&amp;quot;从一门课变成了一箱工具，并且教会你拿起任何一件工具之前，先看清手里问题的形状。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考书籍：Aditya Bhargava《图解算法》（Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People, Manning, 2016；中译本由人民邮电出版社出版）。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《计算机程序设计艺术》笔记：Knuth 用半个世纪给算法写的一部《史记》</title>
      <link>https://ftmao.com/blog/bookstore/taocp-book-notes/</link>
      <pubDate>Fri, 08 Dec 2023 10:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/taocp-book-notes/</guid>
      <description>&lt;h1 id=&#34;计算机程序设计艺术笔记knuth-用半个世纪给算法写的一部史记&#34;&gt;《计算机程序设计艺术》笔记：Knuth 用半个世纪给算法写的一部《史记》&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：被&amp;quot;要不要读 TAOCP&amp;quot;这个问题折磨过的工程师；也想给正在啃这套书的人一张地图和一份劝退/劝进并存的导读。
参考书籍：Donald E. Knuth《计算机程序设计艺术》（The Art of Computer Programming）：卷 1《基本算法》（1968）、卷 2《半数值算法》（1969）、卷 3《排序与查找》（1973）、卷 4A《组合算法（一）》（2011），以及陆续以分册（fascicle）形式发布的 4B、4C。
阅读目标：不假装自己通读了这套书——说实话，没人通读过——而是把它读成一张&amp;quot;算法领域的地形图&amp;quot;：每卷解决什么问题、Knuth 的方法论是什么、哪些内容今天仍是工程师的硬通货、正确的打开姿势是什么。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一定位这不是教材是算法学科的史记&#34;&gt;一、定位：这不是教材，是算法学科的《史记》&lt;/h2&gt;
&lt;p&gt;先破一个流传最广的误读：TAOCP 不是&amp;quot;编程教程&amp;quot;，甚至不是&amp;quot;算法教材&amp;quot;。它是 Knuth 从 1962 年开始写的一部&lt;strong&gt;算法领域的编年史加分析法典&lt;/strong&gt;——每一种重要算法都给出精确的描述、完整的正确性论证、严格的运行时间分析，以及详尽到令人发指的文献考证。Bill Gates 那句著名的话是对这套书最准确的注脚：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;如果你觉得自己是个足够优秀的程序员，就去读 Knuth 的《计算机程序设计艺术》……如果你能读完它，请一定把简历发给我。&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话的重点不在&amp;quot;读完&amp;quot;，在&amp;quot;读完的人值得收简历&amp;quot;——它说的是这套书的密度。Knuth 自己的写作史也配得上这份密度：1962 年动笔时他以为自己写的是一本&amp;quot;编译器构造&amp;quot;的书，越写发现底料越多，最终膨胀成七卷规划；卷 1 到卷 3 在 1968-1973 年间出齐，然后卷 4 一写就是近四十年，2011 年才出了 4A，至今仍在以分册形式连载。顺带一个工程师会心一笑的插曲：1977 年卷 2 出第二版时，出版商改用照相排版，Knuth 对数学公式的排版质量忍无可忍，于是&lt;strong&gt;自己花了近十年写了 TeX 排版系统&lt;/strong&gt;——全世界数学论文的排版标准，是这套书的副产品。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    TAOCP[&amp;#34;TAOCP 七卷规划&amp;lt;br/&amp;gt;1962 年动笔&amp;#34;] --&amp;gt; V1[&amp;#34;卷 1 基本算法 1968&amp;lt;br/&amp;gt;数学基础 + 数据结构&amp;#34;]
    TAOCP --&amp;gt; V2[&amp;#34;卷 2 半数值算法 1969&amp;lt;br/&amp;gt;随机数 + 高精度算术&amp;#34;]
    TAOCP --&amp;gt; V3[&amp;#34;卷 3 排序与查找 1973&amp;lt;br/&amp;gt;内排序 + 外排序 + 查找&amp;#34;]
    TAOCP --&amp;gt; V4[&amp;#34;卷 4 组合算法&amp;lt;br/&amp;gt;4A 2011 已出&amp;lt;br/&amp;gt;4B/4C 分册连载中&amp;#34;]
    TAOCP --&amp;gt; V5[&amp;#34;卷 5-7 规划中&amp;lt;br/&amp;gt;句法算法 / 语言理论等&amp;#34;]
    V1 -.-&amp;gt;|&amp;#34;排版质量事故&amp;#34;| TEX[&amp;#34;副产品: TeX&amp;lt;br/&amp;gt;全世界数学排版标准&amp;#34;]
    style TEX fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;为什么用-mixmmix-汇编而不是高级语言&#34;&gt;为什么用 MIX/MMIX 汇编，而不是高级语言&lt;/h3&gt;
&lt;p&gt;这是理解全书方法论的一把钥匙。书里所有算法都用一台假想计算机 &lt;strong&gt;MIX&lt;/strong&gt;（后来升级为 64 位 RISC 风格的 &lt;strong&gt;MMIX&lt;/strong&gt;）的机器语言写成。Knuth 给的理由，本质上是三条工程判断：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;要算性能账，就得有一台确定的机器&lt;/strong&gt;。算法分析的核心是精确计算执行次数和时间，而&amp;quot;这条语句花多少时间&amp;quot;在高级语言里是说不清的——编译器、运行时、指令集全在中间隔着。MIX 是一台指令集、时序完全定义好的机器，每条指令几个时钟周期白纸黑字，运行时间的分析因此可以&lt;strong&gt;精确到指令级&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器语言不赶时髦&lt;/strong&gt;。1960 年代的高级语言换得飞快（Algol、Fortran、PL/I……），用任何一种写书都会很快过时；而一台假想的、设计得足够经典的机器，五十年不过时。事实证明他赌对了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逼你看清代价&lt;/strong&gt;。用汇编写，每一次访存、每一次循环、每一条跳转都暴露在纸面上——Knuth 认为程序员必须知道自己写的每一行代码让机器做了什么，这是&amp;quot;程序设计艺术&amp;quot;里&amp;quot;艺术&amp;quot;二字的底线。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代价也是显然的：阅读门槛被这道汇编抬得很高。这也是为什么本文后面会专门谈&amp;quot;怎么读&amp;quot;——这套书的正确打开方式不是从第一页硬啃。&lt;/p&gt;
&lt;p&gt;另一个体现 Knuth 写作态度的细节是&lt;strong&gt;勘误奖励&lt;/strong&gt;：他给每个在书中首次发现错误的人签发一张&amp;quot;十六进制一美元&amp;quot;的支票（0x$1.00 = 2.56 美元）。钱少到可笑，但这张支票在技术圈是荣誉勋章——它宣示的是一种工程文化：&lt;strong&gt;错误不是羞耻，藏着掖着才是；每一版书都比上一版更接近正确。&lt;/strong&gt; 半个世纪几十个印次，这套书可能是人类历史上被校对得最狠的技术著作。&lt;/p&gt;
&lt;p&gt;还有一个容易忽略的风格特征：几乎每一节结尾都有 &lt;strong&gt;&amp;ldquo;History and bibliography&amp;rdquo;（历史与文献）&lt;/strong&gt; 小节，把这个算法从最早的雏形到最新的改进逐年考证，作者、年份、原始论文一个不落。这让 TAOCP 同时是一部&lt;strong&gt;算法史的目录&lt;/strong&gt;——想知道某个思想的最早出处，查这套书通常比查搜索引擎可靠。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二卷-1基本算法算法的定义与数据结构的奠基&#34;&gt;二、卷 1《基本算法》：算法的定义与数据结构的奠基&lt;/h2&gt;
&lt;h3 id=&#34;算法是什么五个特征与欧几里得算法&#34;&gt;算法是什么：五个特征与欧几里得算法&lt;/h3&gt;
&lt;p&gt;卷 1 开篇就干了件&amp;quot;法典&amp;quot;该干的事：给&amp;quot;算法&amp;quot;下定义。Knuth 用**算法 E（欧几里得求最大公约数）**做标本，提炼出算法的五个特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有穷性（Finiteness）&lt;/strong&gt;：算法必须在有限步内终止。（Knuth 特别注明：只满足其余四条而不满足有穷性的，可以叫&amp;quot;计算方法&amp;quot;，比如某些永不终止但仍有用处的反应式过程。）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确切性（Definiteness）&lt;/strong&gt;：每一步都必须精确定义、无歧义——自然语言描述做不到这一点，这是书里坚持用形式化记号的原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输入（Input）&lt;/strong&gt;：有零个或多个输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出（Output）&lt;/strong&gt;：有一个或多个输出，且与输入有确定关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能行性（Effectiveness）&lt;/strong&gt;：每一步都必须足够基本，原则上能用纸笔在有限时间内完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个定义的价值在于它立刻产生了区分能力：菜谱通常不满足确切性（&amp;ldquo;适量盐&amp;rdquo;），死循环程序不满足有穷性，而&amp;quot;掷骰子决定下一步&amp;quot;不满足确切性——都不是算法。&lt;strong&gt;定义不是用来背的，是用来划界的。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;数学基础与-oωθ-的来历&#34;&gt;数学基础与 O/Ω/Θ 的来历&lt;/h3&gt;
&lt;p&gt;卷 1 第一章的数学部分（归纳法、数、幂与对数、和与积、排列、斐波那契、生成函数）常被读者跳过，但里面埋着全书的方法论核心：&lt;strong&gt;渐近分析（asymptotic analysis）&lt;/strong&gt;。几个史实值得澄清：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大 O 记号不是 Knuth 发明的——它来自数论学家 &lt;strong&gt;Bachmann&lt;/strong&gt;（1894 年），经 Landau 推广；&lt;/li&gt;
&lt;li&gt;Knuth 的贡献是把它引入算法分析，并在 1976 年专门写了一篇短文《Big Omicron and Big Omega and Big Theta》，&lt;strong&gt;推广了大 Ω（下界）和大 Θ（紧界）记号&lt;/strong&gt;，结束了当时文献里&amp;quot;什么都叫 O&amp;quot;的混乱局面。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，今天我们写在面试白板上的 O(n log n) 这套语言，其语法规范是这套书（及配套论文）定下来的。Knuth 的态度始终如一：&lt;strong&gt;&amp;ldquo;大约 n 的平方量级&amp;quot;不是分析，&amp;ldquo;3n² + 5n - 7 次 MIX 指令执行&amp;quot;才是分析，O 记号是对前者的严格速写，不是含糊的挡箭牌。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;数据结构栈队列链表与树&#34;&gt;数据结构：栈、队列、链表与树&lt;/h3&gt;
&lt;p&gt;卷 1 第二章是整部书对后世影响最深的部分之一——今天教科书里的数据结构章节目录，几乎就是这一章的子孙：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性结构&lt;/strong&gt;：栈、队列、双端队列，以及链表（含循环链表、双向链表）的完整代数化描述——插入、删除、遍历的每一步都用 MIX 实现并计算代价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树与二叉树&lt;/strong&gt;：形式化定义、三种遍历（先序/中序/后序）、&lt;strong&gt;线索二叉树（threaded tree）&lt;/strong&gt;——利用空指针存放前驱后继信息，让遍历不再需要栈。这是一个漂亮的&amp;quot;空间复用&amp;quot;思想实验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态存储分配与垃圾回收&lt;/strong&gt;：内存池的伙伴系统（buddy system）、首次适配/最佳适配策略、以及垃圾回收的标记思想——1968 年就成体系地写在书里，比多数编程语言拥有 GC 早了一代人。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：读这一章最大的震撼是&amp;quot;年代错位感&amp;rdquo;——我们以为的&amp;quot;现代概念&amp;rdquo;（垃圾回收、智能指针的引用计数思想、内存分配策略的权衡），全部写在 1968 年的书里，连权衡的框架都没变过。所谓新技术，多数是老思想在新硬件上的重排。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三卷-2半数值算法随机数与算得准的艺术&#34;&gt;三、卷 2《半数值算法》：随机数与&amp;quot;算得准&amp;quot;的艺术&lt;/h2&gt;
&lt;p&gt;卷 2 处理一个独特的中间地带：对象是数，但关心的是算法而非数值分析。两大主线是随机数和算术。&lt;/p&gt;
&lt;p&gt;先解释一下这个奇怪的书名：&lt;strong&gt;&amp;ldquo;半数值&amp;rdquo;（seminumerical）&lt;/strong&gt; 指的是这类算法介于&amp;quot;纯数值计算&amp;quot;（数值分析的地盘，关心连续数学与误差界）和&amp;quot;纯符号处理&amp;quot;（编译、树操作的地盘）之间——输入输出是数，但算法的骨架是离散的组合结构。随机数生成、进制转换、多精度算术都属于这一带。&lt;/p&gt;
&lt;h3 id=&#34;随机数不该用随机的方法生成&#34;&gt;随机数：不该用随机的方法生成&lt;/h3&gt;
&lt;p&gt;卷 2 第三章是全书被引用最多的章节之一，开篇那句名言每个工程师都该见过一次：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Random numbers should not be generated by a method chosen at random.&amp;quot;（随机数不该用一个随机选出来的方法生成。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Knuth 用亲身经历佐证这句话：他年轻时曾设计过一个&amp;quot;超级随机&amp;quot;的生成器——程序里随机地跳转、随机地执行不同操作，自以为复杂到不可能有规律；实际一跑，&lt;strong&gt;序列几乎立刻陷入一个短循环或不动点&lt;/strong&gt;。教训被写成定理级别的警告：复杂性不等于随机性，没有理论分析的&amp;quot;看起来乱&amp;quot;都是自欺欺人。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性同余法（LCG）&lt;/strong&gt;：x 的下一个值 = (a·x + c) mod m。全书用几十页分析三个参数怎么选——模数 m、乘数 a、增量 c 的选择决定了周期长度和统计质量，每一个坏选择的下场都有具体展示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统计检验&lt;/strong&gt;：卡方检验、Kolmogorov-Smirnov 检验、以及专门测高维均匀性的&lt;strong&gt;谱检验（spectral test）&lt;/strong&gt;——一个 LCG 可以在一维上完美均匀，在二维平面上却排成几条明显的直线，谱检验就是抓这种&amp;quot;隐藏结构&amp;quot;的。&lt;/li&gt;
&lt;li&gt;这一章的工程翻译到今天依然直接：&lt;strong&gt;不要用 &lt;code&gt;rand()&lt;/code&gt; 做密码学，不要自己发明随机数生成器，用之前先查它的检验记录。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;高精度算术与算得准&#34;&gt;高精度算术与&amp;quot;算得准&amp;rdquo;&lt;/h3&gt;
&lt;p&gt;另一条主线是让计算机突破机器字长地算数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多精度算术&lt;/strong&gt;：加减乘的经典算法之外，重点是&lt;strong&gt;算法 D（多精度除法）&lt;/strong&gt;——除法是四则运算里唯一需要&amp;quot;试商&amp;quot;的，Knuth 给出完整的规范化、试商、校正流程和正确性证明。今天所有大整数库的除法实现，祖师爷都是这一节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进制转换&lt;/strong&gt;：二进制、十进制之间的转换算法及其代价分析——这个在当年是日常刚需。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有理数与浮点运算&lt;/strong&gt;：浮点数的表示、舍入误差分析——&amp;ldquo;浮点算术不是实数算术&amp;quot;这个每个工程师都会踩的坑，书里有完整的误差传播分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多项式算术与幂的快速计算&lt;/strong&gt;：包括快速幂（平方-乘方法）的前身分析，以及乘法复杂度的下潜——Karatsuba 乘法这类&amp;quot;比课本算法快&amp;quot;的分治思路。更进一步是**加法链（addition chain）**问题：计算 x 的 n 次方最少需要几次乘法？&amp;ldquo;反复平方&amp;quot;并不总是最优，而求最优加法链本身至今没有完全解决——又一个&amp;quot;看似简单、实则研究级&amp;quot;的 50 分题栖息地。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：卷 2 是四卷里&amp;quot;最数学&amp;quot;的一卷，但它的工程含义一点不虚：凡是钱、坐标、时间戳参与计算的地方，都在和&amp;quot;算得准&amp;quot;打交道。浮点误差、大数溢出、随机数质量——这三类生产事故的完整理论都在这一卷里，早该被列为后端工程师的体检项目。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四卷-3排序与查找工程师的硬通货&#34;&gt;四、卷 3《排序与查找》：工程师的硬通货&lt;/h2&gt;
&lt;p&gt;四卷里工程密度最高、最值得当工具书常备的一卷。Knuth 把排序和查找这两个&amp;quot;最古老的生产问题&amp;quot;做到了考古学级别的穷尽。&lt;/p&gt;
&lt;h3 id=&#34;内排序一个家族的谱系&#34;&gt;内排序：一个家族的谱系&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    S[&amp;#34;内排序算法家族&amp;#34;] --&amp;gt; I[&amp;#34;插入类&amp;lt;br/&amp;gt;直接插入 / Shell 排序&amp;#34;]
    S --&amp;gt; E[&amp;#34;交换类&amp;lt;br/&amp;gt;冒泡 / 快速排序 Hoare 1962&amp;#34;]
    S --&amp;gt; SE[&amp;#34;选择类&amp;lt;br/&amp;gt;直接选择 / 堆排序 Williams-Floyd&amp;#34;]
    S --&amp;gt; M[&amp;#34;归并类&amp;lt;br/&amp;gt;二路归并 / 自然归并&amp;#34;]
    S --&amp;gt; D[&amp;#34;分布类&amp;lt;br/&amp;gt;计数 / 基数排序&amp;#34;]
    E -.-&amp;gt;|&amp;#34;平均最优&amp;lt;br/&amp;gt;原地性差&amp;#34;| NOTE[&amp;#34;各有适用场景&amp;lt;br/&amp;gt;没有全能冠军&amp;#34;]
    M -.-&amp;gt;|&amp;#34;稳定 + 可外排&amp;#34;| NOTE
    style NOTE fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;书里对每一类的处理都是&amp;quot;描述 + 实现 + 精确分析&amp;quot;三段式，几个今天仍在生产环境活跃的名字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速排序&lt;/strong&gt;（Hoare，1962）：Knuth 给出了平均比较次数的完整推导，以及最坏 O(n²) 的触发条件——&amp;ldquo;已排序输入配最左基准元&amp;quot;这个教科书反面案例就出自这里的分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆排序&lt;/strong&gt;（Williams 提出堆，Floyd 改进）：最坏情况也是 O(n log n)，堆这个数据结构本身后来长成了优先级队列的标准实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;归并排序&lt;/strong&gt;：稳定、可预测，并且是通往外排序的桥。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读这一章时建议始终带着 Knuth 的三个评价维度去看每个算法：&lt;strong&gt;稳定性&lt;/strong&gt;（相等元素的相对次序是否保持）、&lt;strong&gt;原地性&lt;/strong&gt;（额外空间是常数还是随 n 增长）、&lt;strong&gt;适应性&lt;/strong&gt;（输入已接近有序时能否变快）。三个维度没有全能冠军——快排快在常数、堆排赢在最坏情况、归并赢在稳定和可外排——&lt;strong&gt;选型就是在这张三维表里做取舍&lt;/strong&gt;，这也是&amp;quot;没有银弹，只有权衡&amp;quot;在算法层的具体形态。&lt;/p&gt;
&lt;h3 id=&#34;最优排序n-log-n-下界的信息论论证&#34;&gt;最优排序：n log n 下界的信息论论证&lt;/h3&gt;
&lt;p&gt;卷 3 最优雅的一段数学是&lt;strong&gt;基于比较的排序的下界证明&lt;/strong&gt;，值得单独记：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;n 个元素&amp;lt;br/&amp;gt;共有 n! 种可能排列&amp;#34;] --&amp;gt; B[&amp;#34;比较排序 = 决策树&amp;lt;br/&amp;gt;每个内部节点是一次比较&amp;lt;br/&amp;gt;每片叶子对应一种排列&amp;#34;]
    B --&amp;gt; C[&amp;#34;树至少有 n! 片叶子&amp;#34;]
    C --&amp;gt; D[&amp;#34;高度至少为 log2(n!)&amp;#34;]
    D --&amp;gt; E[&amp;#34;Stirling 公式&amp;lt;br/&amp;gt;log2(n!) 约等于&amp;lt;br/&amp;gt;n log2 n - 1.44n&amp;#34;]
    E --&amp;gt; F[&amp;#34;结论:&amp;lt;br/&amp;gt;任何比较排序的最坏情况&amp;lt;br/&amp;gt;下界为 Ω(n log n)&amp;#34;]
    style F fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个论证的力量在于它&lt;strong&gt;不针对任何具体算法&lt;/strong&gt;——它限制的是&amp;quot;比较&amp;quot;这个信息获取方式本身：一次比较最多带来 1 比特信息，而区分 n! 种排列需要 log2(n!) 比特。要快过 n log n，唯一的出路是放弃纯比较——这正是基数排序、计数排序这些&amp;quot;分布类&amp;quot;算法存在的理由。这是我在全书中最喜欢的一段：&lt;strong&gt;用信息论给一类算法封顶，一句话堵死一个方向的无效努力。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;外排序与查找&#34;&gt;外排序与查找&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;外排序&lt;/strong&gt;：当数据放不进内存，瓶颈从比较次数变成读写趟数。多路归并、**置换选择（replacement selection）**生成初始归并段、多相归并——这一整套是磁带时代的遗产，但思想直接活在今天的 LSM-tree（LevelDB/RocksDB 的多路归并 compaction）里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找&lt;/strong&gt;：顺序查找与&lt;strong&gt;二分查找&lt;/strong&gt;（Knuth 照例指出：思路人人会，写对的没几个——边界条件的完整分析在书里；他还考据出二分查找的思路 1946 年就见诸文献，而第一个对所有输入规模都正确的完整算法直到 1962 年才发表，一个&amp;quot;显而易见&amp;quot;的算法花了十六年才写对）；&lt;strong&gt;哈希&lt;/strong&gt;：拉链法、开放寻址、线性探测的平均探查次数分析——线性探测的完整数学分析正是 Knuth 本人在 1962 年做出的，他自称正是这个问题让他决定写这套书；&lt;strong&gt;平衡树&lt;/strong&gt;：AVL 树的旋转与再平衡，多路查找树与 &lt;strong&gt;B 树&lt;/strong&gt;——数据库索引的物理形态；&lt;strong&gt;数字查找树（trie）&lt;/strong&gt;：按位/按字符逐级展开的查找结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五卷-4a组合算法生成一切的艺术&#34;&gt;五、卷 4A《组合算法》：生成一切的艺术&lt;/h2&gt;
&lt;p&gt;隔了 38 年，2011 年出的卷 4A 换了一个世界：从&amp;quot;算数&amp;quot;和&amp;quot;排序&amp;quot;进入&lt;strong&gt;组合对象&lt;/strong&gt;——布尔函数、组合、排列、划分——的领域。这卷的气质和前三卷一脉相承：先把对象定义到无歧义，再讨论&amp;quot;如何系统性地、不重不漏地枚举出全部实例&amp;rdquo;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;布尔函数与按位运算&lt;/strong&gt;（7.1 节）：真值表、按位技巧的宝库——掩码、并行按位计数（population count 的 sideways addition）、&amp;ldquo;x 与 x-1 的按位与可以抹掉最低位的 1&amp;quot;这类惯用法——这一节读起来像高级位操作黑魔法集，但每个技巧都有正确性论证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成所有组合对象&lt;/strong&gt;（7.2.1 节）：全 n 元组与 &lt;strong&gt;Gray 码&lt;/strong&gt;（相邻码字只差一位的编码，枚举时每次只翻转一位）、全排列、全组合、整数划分、集合划分、所有树——每类对象都给出&amp;quot;不重不漏、常数均摊时间&amp;quot;的生成算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回溯思想&lt;/strong&gt;：组合生成的算法骨子里都是&amp;quot;受控的回溯&amp;rdquo;——按字典序推进、到头回退。系统化的回溯框架（Algorithm B）在卷 4B 的 7.2.2 节正式登场；顺带交代一句出版史：4A 出版后 Knuth 继续以分册形式推进，卷 4B 已于 2022 年正式成书，当年那个&amp;quot;卷 4 写了四十年&amp;quot;的段子至此翻篇了一半。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用一张图记住回溯的骨架——所有组合枚举问题都可以投影成同一棵树：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    R[&amp;#34;空解&amp;lt;br/&amp;gt;第 0 层&amp;#34;] --&amp;gt; A[&amp;#34;第 1 位选 a&amp;#34;]
    R --&amp;gt; B[&amp;#34;第 1 位选 b&amp;#34;]
    A --&amp;gt; A1[&amp;#34;第 2 位选 a&amp;lt;br/&amp;gt;...继续深入&amp;#34;]
    A --&amp;gt; A2[&amp;#34;第 2 位选 b&amp;#34;]
    A2 --&amp;gt;|&amp;#34;约束不满足&amp;lt;br/&amp;gt;剪枝回退&amp;#34;| X1[&amp;#34;放弃整棵子树&amp;#34;]
    A1 --&amp;gt; A1L[&amp;#34;到达叶子&amp;lt;br/&amp;gt;输出一个完整解&amp;#34;]
    B --&amp;gt; B1[&amp;#34;...同样的展开&amp;#34;]
    X1 -.-&amp;gt;|&amp;#34;回退到最近分叉&amp;lt;br/&amp;gt;试下一个分支&amp;#34;| A
    style X1 fill:#ffcccc
    style A1L fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心纪律只有两条：&lt;strong&gt;到底就输出，碰壁就回退，回退后试下一个未试过的分支&lt;/strong&gt;——不重不漏靠的就是这条纪律，而不是事后去重。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：卷 4A 对今天的工程师有一个直接映射：&lt;strong&gt;所有&amp;quot;遍历所有可能配置&amp;quot;的需求&lt;/strong&gt;——测试用例生成、组合优化、穷举校验——都是组合生成问题，而&amp;quot;生成&amp;quot;远比&amp;quot;收集后再去重&amp;quot;高效。不重不漏四个字，说起来像小学要求，做起来是算法设计里最硬的纪律之一。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六怎么读这套书习题分级费马大定理与路线图&#34;&gt;六、怎么读这套书：习题分级、费马大定理与路线图&lt;/h2&gt;
&lt;h3 id=&#34;习题分级体系00-到-50-的登山标尺&#34;&gt;习题分级体系：00 到 50 的登山标尺&lt;/h3&gt;
&lt;p&gt;TAOCP 的习题是半本书——Knuth 给每道题标了难度分，这套编号体系本身就是个值得单独记住的设计：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;编号&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;预期投入&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;00&lt;/td&gt;
					&lt;td&gt;看一眼就该会&lt;/td&gt;
					&lt;td&gt;几秒钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;10&lt;/td&gt;
					&lt;td&gt;简单&lt;/td&gt;
					&lt;td&gt;一分钟内&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;20&lt;/td&gt;
					&lt;td&gt;中等&lt;/td&gt;
					&lt;td&gt;一刻钟上下&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;30&lt;/td&gt;
					&lt;td&gt;较难&lt;/td&gt;
					&lt;td&gt;可能需要数小时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;40&lt;/td&gt;
					&lt;td&gt;学期项目级&lt;/td&gt;
					&lt;td&gt;数周&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;50&lt;/td&gt;
					&lt;td&gt;研究级问题&lt;/td&gt;
					&lt;td&gt;出题时可能无人解决&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;编号前可加 M（数学向）或 HM（高等数学向）。最著名的梗是卷 1 里那道 &lt;strong&gt;[M50]&lt;/strong&gt;：题目是&amp;quot;证明费马大定理&amp;rdquo;——Knuth 的意思是&amp;quot;这道题出题时没人会做&amp;rdquo;。1995 年 Wiles 真的证明了它之后，后续印次里这道题的处境变成了书迷间的段子。&lt;strong&gt;50 分题不是作业，是路标：它告诉你学科边界在哪里。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;我的路线图建议&#34;&gt;我的路线图建议&lt;/h3&gt;
&lt;p&gt;对全职工程师，&amp;ldquo;通读&amp;quot;是个伪目标，务实的姿势是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;卷 3 当工具书常备&lt;/strong&gt;：排序和查找是唯一一卷会在日常代码评审、性能分析、系统设计面试中直接变现的。通读一遍，之后按需回查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卷 1 读第二章&lt;/strong&gt;：数据结构部分是&amp;quot;为什么链表这样设计、为什么 GC 这样工作&amp;quot;的第一性原理来源；第一章数学基础按需查，不必顺序啃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卷 2 按需精读&lt;/strong&gt;：写大数、随机、浮点相关代码之前，把对应章节当 pre-mortem 读一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卷 4A 当兴趣读物&lt;/strong&gt;：遇到组合枚举需求（生成测试组合、配置穷举）时按目录取用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;习题只做 20 分以下的&lt;/strong&gt;：10-20 分题保证理解，30 分以上当文献指南——题目本身常附带一整段文献史，不打算做题也值得读题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别按顺序读&lt;/strong&gt;。Knuth 自己也没指望人顺序读：每节相对独立，前置依赖写在节首。把这套书当成&amp;quot;算法领域的百科全书&amp;quot;而不是&amp;quot;课程&amp;rdquo;，是心态上最重要的一步。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七落到工程实践这套书改变了什么&#34;&gt;七、落到工程实践：这套书改变了什么&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;复杂度分析从&amp;quot;感觉&amp;quot;变成了&amp;quot;账&amp;quot;。&lt;/strong&gt; 读过 Knuth 的分析后，再说&amp;quot;这个操作是 O(n)&amp;ldquo;会本能地问：常数是什么？最坏情况和平均情况分别是多少？输入分布假设是什么？这套书最宝贵的遗产不是任何单个算法，而是**&amp;ldquo;性能是可以、也必须被精确计算的&amp;quot;这个信念**——profile 之前的估算、capacity planning 里的量级推演，全是它的日常形态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对&amp;quot;下界&amp;quot;的敬畏。&lt;/strong&gt; n log n 的信息论论证训练了一种思维方式：优化之前先问这个问题的理论下界在哪，离下界还有多远。很多&amp;quot;再优化一下&amp;quot;的努力，看一眼下界就知道是徒劳——&lt;strong&gt;知道哪里没有油水，和知道哪里有油水一样值钱。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机与浮点有了红线意识。&lt;/strong&gt; &amp;ldquo;不自己造随机数生成器&amp;quot;&amp;ldquo;钱不用浮点表示&amp;quot;这类团队规约，以前是被当经验口诀背下来的；读完卷 2 它们变成了有证明的结论，执行的腰杆完全不一样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读旧文献的胆子变大了。&lt;/strong&gt; Knuth 的行文示范了一件事：1960 年代的论文不是&amp;quot;过时的古董&amp;rdquo;，而是很多问题唯一的完整答案所在。排障和选型时敢往回翻原始论文，是这套书给的胆量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对&amp;quot;正确性&amp;quot;的标准被永久抬高了。&lt;/strong&gt; 书里每个算法都带正确性证明或严格论证，读多了会形成一种肌肉记忆：写完一段非平凡逻辑，先问&amp;quot;循环不变式是什么&amp;quot;&amp;ldquo;边界情况枚举全了吗&amp;rdquo;。二分查找十六年才写对的考据，是这条标准最好的反面教材。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TAOCP 是算法学科的编年史加分析法典：七卷规划，卷 1-3 出版于 1968-1973，卷 4A 迟至 2011，副产品是 TeX；Gates 那句&amp;quot;读完发简历&amp;quot;说的是它的密度。&lt;/li&gt;
&lt;li&gt;MIX/MMIX 假想机不是老派怪癖，是方法论：要精确算性能账，就得有一台指令级确定的机器。&lt;/li&gt;
&lt;li&gt;卷 1 立规矩：算法五特征（有穷、确切、输入、输出、能行），数据结构大全，O/Ω/Θ 的语法规范（O 来自 Bachmann，Ω/Θ 由 Knuth 推广）。&lt;/li&gt;
&lt;li&gt;卷 2 管&amp;quot;算得准&amp;rdquo;：随机数不该用随机方法生成（LCG 参数分析、谱检验），多精度算术（算法 D），浮点误差。&lt;/li&gt;
&lt;li&gt;卷 3 是工程师硬通货：排序家族全谱、n log n 下界的信息论论证（比较即信息获取）、外排序思想活在今天的 LSM-tree 里、哈希与 B 树。&lt;/li&gt;
&lt;li&gt;卷 4A 进入组合世界：按位技巧、不重不漏地生成所有组合/排列/划分，回溯的思想雏形。&lt;/li&gt;
&lt;li&gt;读法：卷 3 常备、卷 1 读数据结构、卷 2/4A 按需精读；习题分级 00-50，[M50] 的费马大定理题是学科边界的路标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套书我没读完，大概率也永远不会读完——但这不妨碍它一直在工作：它就立在手边，像一个不说话的考官，每次想偷懒说&amp;quot;差不多 n log n 吧&amp;quot;的时候，抬头就能看见。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Donald E. Knuth, &lt;em&gt;The Art of Computer Programming&lt;/em&gt;, Volume 1: &lt;em&gt;Fundamental Algorithms&lt;/em&gt; (3rd ed., Addison-Wesley, 1997; 初版 1968).&lt;/li&gt;
&lt;li&gt;Donald E. Knuth, &lt;em&gt;The Art of Computer Programming&lt;/em&gt;, Volume 2: &lt;em&gt;Seminumerical Algorithms&lt;/em&gt; (3rd ed., 1997; 初版 1969).&lt;/li&gt;
&lt;li&gt;Donald E. Knuth, &lt;em&gt;The Art of Computer Programming&lt;/em&gt;, Volume 3: &lt;em&gt;Sorting and Searching&lt;/em&gt; (2nd ed., 1998; 初版 1973).&lt;/li&gt;
&lt;li&gt;Donald E. Knuth, &lt;em&gt;The Art of Computer Programming&lt;/em&gt;, Volume 4A: &lt;em&gt;Combinatorial Algorithms, Part 1&lt;/em&gt; (Addison-Wesley, 2011).&lt;/li&gt;
&lt;li&gt;Donald E. Knuth, &amp;ldquo;Big Omicron and Big Omega and Big Theta&amp;rdquo;, &lt;em&gt;ACM SIGACT News&lt;/em&gt;, 1976（O/Ω/Θ 记号规范化的原始短文）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>《编程珠玑》学习笔记：小书大道理，问题求解的完整闭环</title>
      <link>https://ftmao.com/blog/bookstore/programming-pearls-book-notes/</link>
      <pubDate>Tue, 08 Aug 2023 10:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/programming-pearls-book-notes/</guid>
      <description>&lt;h1 id=&#34;编程珠玑学习笔记小书大道理问题求解的完整闭环&#34;&gt;《编程珠玑》学习笔记：小书大道理，问题求解的完整闭环&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：会写代码、刷过一些算法题，但想知道&amp;quot;真实工程里高手是怎么想问题&amp;quot;的程序员。&lt;br&gt;
参考书籍：Jon Bentley《编程珠玑》（Programming Pearls）&lt;br&gt;
阅读目标：提炼全书的问题求解闭环，把几个经典案例变成自己的思维肌肉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面一本中间点的书&#34;&gt;0. 写在前面：一本&amp;quot;中间点&amp;quot;的书&lt;/h2&gt;
&lt;p&gt;如果给程序员的案头书排一条光谱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一端是吴军《数学之美》：讲的是&lt;strong&gt;大思想&lt;/strong&gt;——数学模型如何降维打击真实世界的问题，视野宏大，但离每天的代码有点远。&lt;/li&gt;
&lt;li&gt;另一端是波利亚《怎样解题》：讲的是&lt;strong&gt;纯方法论&lt;/strong&gt;——&amp;ldquo;看清问题、拟定计划、执行、回顾&amp;rdquo;，放之四海皆准，但抽象到几乎不给具体例子。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;《编程珠玑》正好卡在中间：&lt;strong&gt;用一个个小而真的问题，走完&amp;quot;定义 → 估算 → 设计 → 验证 → 调优&amp;quot;的完整闭环&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它的出身就很&amp;quot;工程&amp;quot;：1983 年起，Jon Bentley 在贝尔实验室工作期间，为《ACM 通讯》（Communications of the ACM）主持 &amp;ldquo;Programming Pearls&amp;rdquo; 专栏，每期挑一个看似不起眼的小问题，带着读者从头到尾把它想透、写对、调快。专栏后来结集成书（1986 年初版，2000 年第二版），薄薄两百多页，却影响了整整几代程序员。&lt;/p&gt;
&lt;p&gt;这本书的&amp;quot;珠玑&amp;quot;（pearls）不是奇技淫巧，而是一种工作方式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;把问题定义清楚，用估算摸清规模，设计正确的算法，证明它是对的，最后再谈调优。顺序不能乱。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面我按这条主线，把书里最值得带走的几个案例提炼出来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-全书主线问题求解的完整闭环&#34;&gt;1. 全书主线：问题求解的完整闭环&lt;/h2&gt;
&lt;p&gt;先看一张全景图。这本书十五个专栏（第二版按五部分组织），其实都在打磨同一个闭环：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;1 定义问题&amp;lt;br/&amp;gt;真正的输入输出是什么&amp;#34;] --&amp;gt; B[&amp;#34;2 估算规模&amp;lt;br/&amp;gt;信封背面算一算&amp;#34;]
    B --&amp;gt; C[&amp;#34;3 设计算法&amp;lt;br/&amp;gt;数据结构 + 算法策略&amp;#34;]
    C --&amp;gt; D[&amp;#34;4 程序验证&amp;lt;br/&amp;gt;断言、边界、测试&amp;#34;]
    D --&amp;gt; E[&amp;#34;5 代码调优&amp;lt;br/&amp;gt;测量驱动的优化&amp;#34;]
    E -.-&amp;gt;|&amp;#34;发现假设不对&amp;#34;| A
    C -.-&amp;gt;|&amp;#34;规模估算变了&amp;#34;| B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;闭环是环，不是直线&lt;/strong&gt;。调优时发现瓶颈，可能要回到算法设计；设计时发现规模估计错了，要回到估算；甚至写着写着发现&amp;quot;问题本身定义错了&amp;quot;——回到起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每一步都有&amp;quot;便宜可占&amp;quot;&lt;/strong&gt;。定义清楚了，问题可能消失（位图排序）；估算做对了，方案直接收敛；验证做扎实了，能躲过标准库级别的 bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大部分程序员只在&amp;quot;设计&amp;quot;一环花力气&lt;/strong&gt;，这正是这本书要纠正的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面五个案例，分别对应闭环上的关键环节。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-定义问题--算法设计开篇的旋转数组&#34;&gt;2. 定义问题 + 算法设计：开篇的旋转数组&lt;/h2&gt;
&lt;h3 id=&#34;21-问题&#34;&gt;2.1 问题&lt;/h3&gt;
&lt;p&gt;把一个有 n 个元素的数组向左循环移动 i 位。比如 n=8、i=3：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;原数组：  [1, 2, 3, 4, 5, 6, 7, 8]
左移 3 位：[4, 5, 6, 7, 8, 1, 2, 3]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;约束：尽量少的额外空间（最好 O(1)），时间也要快。&lt;/p&gt;
&lt;h3 id=&#34;22-解法一逐一移动正确但慢&#34;&gt;2.2 解法一：逐一移动（正确但慢）&lt;/h3&gt;
&lt;p&gt;最直觉的做法：把 &lt;code&gt;x[0]&lt;/code&gt; 存到临时变量，整体左移一位，把临时变量放到末尾；重复 i 次。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;移动 1 次：[2, 3, 4, 5, 6, 7, 8, 1]
移动 2 次：[3, 4, 5, 6, 7, 8, 1, 2]
移动 3 次：[4, 5, 6, 7, 8, 1, 2, 3]  ✓&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;代码好写、肯定对，但时间是 O(n·i)——n 和 i 都是百万级时就是万亿次操作，直接出局。&lt;strong&gt;它的价值是当&amp;quot;对照组&amp;quot;：先有一个肯定正确的慢版本，后面所有快版本都可以拿它对拍。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;23-解法二求逆三次全书的第一个啊哈&#34;&gt;2.3 解法二：求逆三次（全书的第一个&amp;quot;啊哈&amp;quot;）&lt;/h3&gt;
&lt;p&gt;把数组看成两段：AB（A 是前 i 个元素，B 是剩下的）。我们要 BA。&lt;/p&gt;
&lt;p&gt;看一个神奇的操作序列（X&amp;rsquo; 表示把 X 反转）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;原数组：  AB
反转 A：  A&amp;#39;B
再反转 B：A&amp;#39;B&amp;#39;
整体反转：(A&amp;#39;B&amp;#39;)&amp;#39; = BA  ✓&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;手摇上面的例子（i=3）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[1, 2, 3 | 4, 5, 6, 7, 8]   反转前 3 个
[3, 2, 1 | 4, 5, 6, 7, 8]   反转后 5 个
[3, 2, 1 | 8, 7, 6, 5, 4]   整体反转
[4, 5, 6, 7, 8, 1, 2, 3]    ✓&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;代码只需一个 reverse 函数加三次调用，O(n) 时间、O(1) 空间，短到几乎不可能写错：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;reverse&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, i&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;reverse&lt;/span&gt;(i, n&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;reverse&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, n&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优雅在哪？&lt;/strong&gt; 旋转是个&amp;quot;整体&amp;quot;操作，我们却一直想逐个元素去搬；而反转是数组上最廉价、最不容易出错的局部操作，三次廉价的局部操作拼出了整体效果。Bentley 借这个例子想说的是：&lt;strong&gt;换个视角看问题，难题会塌缩成 trivial 的三行代码。&lt;/strong&gt; Brian Kernighan 评价这类代码是&amp;quot;既正确又明显地正确&amp;quot;——这是软件里最高级的状态。&lt;/p&gt;
&lt;h3 id=&#34;24-解法三块交换--杂耍算法&#34;&gt;2.4 解法三：块交换 / 杂耍算法&lt;/h3&gt;
&lt;p&gt;还有第三条路：把数组按 gcd(n, i) 分成若干&amp;quot;循环链&amp;quot;，每条链上的元素像杂耍一样依次归位（juggling algorithm）；或者递归地做块交换。时间同样是 O(n)，移动次数比求逆法略少，但代码明显复杂、边界更容易错。&lt;/p&gt;
&lt;p&gt;三种解法摆在一起，就是一堂微型的方法论课：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解法&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;时间&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;额外空间&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;写对的难度&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;角色&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;逐一移动&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n·i)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;极低&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;对照组 / 对拍基准&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;求逆三次&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;低&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;生产代码首选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;块交换/杂耍&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;高&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;炫技 / 面试加分&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：先写&amp;quot;蠢但对&amp;quot;的版本建立信心，再找&amp;quot;巧且短&amp;quot;的版本上线，复杂版本只用于学习。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-重新定义问题位图排序1mb-排-900-万个电话号码&#34;&gt;3. 重新定义问题：位图排序（1MB 排 900 万个电话号码）&lt;/h2&gt;
&lt;p&gt;这是全书第一章 &amp;ldquo;Cracking the Oyster&amp;rdquo;（撬开牡蛎）的核心案例，也是&amp;quot;定义问题&amp;quot;环节最漂亮的一次示范。&lt;/p&gt;
&lt;h3 id=&#34;31-问题来自真实场景&#34;&gt;3.1 问题（来自真实场景）&lt;/h3&gt;
&lt;p&gt;一个文件里有至多 900 万个整数，每个都是 7 位数（即 0 ~ 9,999,999），&lt;strong&gt;没有重复&lt;/strong&gt;。要把它们排序输出。可用内存大约 1 兆字节，磁盘空间充足。&lt;/p&gt;
&lt;h3 id=&#34;32-第一反应归并排序&#34;&gt;3.2 第一反应：归并排序&lt;/h3&gt;
&lt;p&gt;标准答案是多路归并：内存里一次读一批，排序后写回磁盘，再多趟归并。正确、通用，但要好几趟磁盘读写，代码也不少。&lt;/p&gt;
&lt;p&gt;在动手之前，Bentley 追问了一句：&lt;strong&gt;这个问题里有哪些信息被我忽略了？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个数恰好出现一次（无重复）；&lt;/li&gt;
&lt;li&gt;数值范围是已知的、有限的（10^7 种可能）；&lt;/li&gt;
&lt;li&gt;除了排序之外没有其他操作；&lt;/li&gt;
&lt;li&gt;没有关联数据（不像学生记录要跟着学号走）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-关键洞察用位表示存在&#34;&gt;3.3 关键洞察：用&amp;quot;位&amp;quot;表示&amp;quot;存在&amp;quot;&lt;/h3&gt;
&lt;p&gt;如果把这些约束用足，答案呼之欲出：&lt;strong&gt;不存数字本身，只存&amp;quot;这个数出现过没有&amp;quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;用一个 10^7 位的位图（bitmap），第 j 位为 1 表示整数 j 在输入里。10^7 位 = 1,250,000 字节 ≈ 1.19 MB，刚好塞进那点可怜的内存。整个排序变成三趟：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把所有位置 0；&lt;/li&gt;
&lt;li&gt;读入每个整数 i，把第 i 位置 1；&lt;/li&gt;
&lt;li&gt;从头到尾扫一遍位图，把为 1 的位依次输出——&lt;strong&gt;天然有序&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;输入文件&amp;lt;br/&amp;gt;至多 900 万个 7 位整数&amp;lt;br/&amp;gt;无重复&amp;#34;] --&amp;gt; B[&amp;#34;阶段一&amp;lt;br/&amp;gt;10^7 位的位图全部置 0&amp;#34;]
    B --&amp;gt; C[&amp;#34;阶段二&amp;lt;br/&amp;gt;每读到一个整数 i&amp;lt;br/&amp;gt;把第 i 位置 1&amp;#34;]
    C --&amp;gt; D[&amp;#34;阶段三&amp;lt;br/&amp;gt;从 0 到 10^7-1 扫描位图&amp;lt;br/&amp;gt;位为 1 就输出下标&amp;#34;]
    D --&amp;gt; E[&amp;#34;输出文件&amp;lt;br/&amp;gt;已排序&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;没有比较、没有交换、没有归并——&lt;strong&gt;排序问题被转化成了表示问题&lt;/strong&gt;。时间 O(n)，磁盘只顺序读写两遍，代码量远小于归并方案。&lt;/p&gt;
&lt;h3 id=&#34;34-这个案例真正教的东西&#34;&gt;3.4 这个案例真正教的东西&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题的表象 ≠ 问题的本质。&lt;/strong&gt; 嘴上说的是&amp;quot;排序&amp;quot;，实质是&amp;quot;有限集合的表示与枚举&amp;quot;。一旦换了表示法，排序&amp;quot;消失&amp;quot;了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追问约束是最大的杠杆。&lt;/strong&gt; &amp;ldquo;无重复&amp;quot;和&amp;quot;范围有限&amp;quot;这两句话，价值千金。真实工作里，甲方给的条件往往比我们用上的多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思想有长长的影子。&lt;/strong&gt; 位图索引、Bloom Filter（布隆过滤器）、Redis 的 bitmap、Roaring Bitmap，都是&amp;quot;用位表示存在性&amp;quot;这条线上的后代。今天在海量数据去重、用户签到、UV 统计里天天在用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：当值域有限且无重复时，&amp;ldquo;存在性位图&amp;quot;可以把排序、去重、判重全部压成 O(n) 的位操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-算法设计最大子数组和的复杂度演进&#34;&gt;4. 算法设计：最大子数组和的复杂度演进&lt;/h2&gt;
&lt;p&gt;如果说位图排序展示的是&amp;quot;重新定义问题&amp;rdquo;，最大子数组展示的就是闭环里&amp;quot;设计&amp;quot;环节的完整教科书：&lt;strong&gt;同一个问题，四个算法，复杂度连降三级。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;41-问题&#34;&gt;4.1 问题&lt;/h3&gt;
&lt;p&gt;输入一个有 n 个数的数组（有正有负），找出&lt;strong&gt;连续子数组&lt;/strong&gt;中和最大的那个，返回这个最大和。&lt;/p&gt;
&lt;p&gt;小例子：&lt;code&gt;[-2, 11, -4, 13, -5, -2]&lt;/code&gt;，答案是 20（子数组 &lt;code&gt;[11, -4, 13]&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;这个问题不是编造的智力题。它源自布朗大学 Ulf Grenander 做模式识别时遇到的二维问题：在数字图像里找&amp;quot;最像某个模式&amp;quot;的子区域。二维版本太贵，他先把它化简到一维——&lt;strong&gt;化简本身就是设计的一部分&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;42-四个版本的演进&#34;&gt;4.2 四个版本的演进&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;版本一：O(n³) 暴力。&lt;/strong&gt; 枚举所有起点 i 和终点 j，对每对 (i, j) 把中间的数加起来。n=1000 时约 10^9 次加法，已经没法用了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;版本二：O(n²)。&lt;/strong&gt; 注意到 &lt;code&gt;sum(i..j+1) = sum(i..j) + x[j+1]&lt;/code&gt;——内层循环不用从头加，带着上一次的累加和走就行。一次&amp;quot;重用已有计算&amp;rdquo;，干掉一个数量级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;版本三：O(n log n) 分治。&lt;/strong&gt; 把数组从中点劈开。最大子数组只有三种可能：全在左半、全在右半、跨过中点。前两种递归解决；跨中点的答案 = 从中点向左扫描的最大后缀和 + 向右扫描的最大前缀和，O(n) 内搞定。T(n) = 2T(n/2) + O(n)，总共 O(n log n)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;版本四：O(n) 扫描线。&lt;/strong&gt; 从左到右扫一遍，维护&amp;quot;&lt;strong&gt;以当前元素结尾的最大子数组和&lt;/strong&gt;&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ending_here = max(x[i], ending_here + x[i])
so_far      = max(so_far, ending_here)&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;直觉：&lt;strong&gt;一个负数前缀对后面只有拖累&lt;/strong&gt;。当累加和变成负数，果断扔掉，从下一个元素重新开始。手摇上面的例子：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;i  x[i]  ending_here  so_far
0   -2       -2          0   (负数前缀，扔掉)
1   11       11         11
2   -4        7         11
3   13       20         20   ← 答案
4   -5       15         20
5   -2       13         20&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这就是后来大名鼎鼎的 Kadane 算法。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 演进[&amp;#34;同一个问题的四次设计&amp;#34;]
        V1[&amp;#34;版本一 暴力枚举&amp;lt;br/&amp;gt;O(n³)&amp;#34;] --&amp;gt;|&amp;#34;重用累加和&amp;#34;| V2[&amp;#34;版本二 前缀复用&amp;lt;br/&amp;gt;O(n²)&amp;#34;]
        V2 --&amp;gt;|&amp;#34;分而治之&amp;#34;| V3[&amp;#34;版本三 分治&amp;lt;br/&amp;gt;O(n log n)&amp;#34;]
        V3 --&amp;gt;|&amp;#34;负数前缀果断丢弃&amp;#34;| V4[&amp;#34;版本四 扫描线&amp;lt;br/&amp;gt;O(n)&amp;#34;]
    end
    V4 --&amp;gt; R[&amp;#34;n=10^6 时&amp;lt;br/&amp;gt;从&amp;#39;算到宇宙热寂&amp;#39;&amp;lt;br/&amp;gt;到毫秒级&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-真实掌故高手也不是一步到位的&#34;&gt;4.3 真实掌故：高手也不是一步到位的&lt;/h3&gt;
&lt;p&gt;Bentley 在书里交代了这段历史，非常值得记住：Grenander 把一维问题抛给卡内基梅隆的 Michael Shamos，Shamos &lt;strong&gt;一夜之间&lt;/strong&gt;设计出 O(n log n) 的分治算法——当时他们都觉得这大概就是极限了。后来在一次研讨会上，统计学家 Jay Kadane 听了一分钟，当场给出了 O(n) 算法。&lt;/p&gt;
&lt;p&gt;这个故事的两层启示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连 Shamos 这个级别的算法专家，第一反应也不是最优解。&lt;/strong&gt; 算法设计是迭代的，不是顿悟的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每降一个数量级，靠的都是一个新视角&lt;/strong&gt;：重用计算、分治、负数前缀丢弃——而不是更卖力的实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程连接：这个 O(n) 扫描思想今天活在很多地方——股票最大收益、信号处理里的峰值检测、乃至面试最高频题。而&amp;quot;版本二&amp;quot;的累加和技巧，就是&lt;strong&gt;前缀和数组&lt;/strong&gt;，是区间查询类问题的通用武器。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-程序验证二分查找一个藏了九年的标准库-bug&#34;&gt;5. 程序验证：二分查找，一个藏了九年的标准库 bug&lt;/h2&gt;
&lt;p&gt;闭环里最容易被跳过的一环是&amp;quot;验证&amp;quot;。Bentley 用二分查找给了所有自信的程序员一记耳光。&lt;/p&gt;
&lt;h3 id=&#34;51-一个尴尬的事实&#34;&gt;5.1 一个尴尬的事实&lt;/h3&gt;
&lt;p&gt;二分查找的思想小学生都懂：有序数组里每次砍一半。但 Bentley 在贝尔实验室和后来的课程里反复做过同一个实验：请&lt;strong&gt;专业程序员&lt;/strong&gt;（给了充足时间、任意语言、可以用高级伪码）写出正确的二分查找，结果&lt;strong&gt;大约 90% 的人写错&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这不是个案。他在书里引用过一个考据：第一个二分查找的发表版本出现在 1946 年，而第一个&lt;strong&gt;对任意长度数组都正确&lt;/strong&gt;的发表版本，要等到 1962 年。&lt;strong&gt;十六年里，所有人发表的版本都有 bug。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Knuth 在《计算机程序设计艺术》第三卷里说得更直白（大意）：二分查找的思想虽然简单，细节却可能棘手得出乎意料——很多好程序员都在这上面栽过跟头。&lt;/p&gt;
&lt;h3 id=&#34;52-坑都藏在哪&#34;&gt;5.2 坑都藏在哪&lt;/h3&gt;
&lt;p&gt;错的从来不是&amp;quot;砍一半&amp;quot;这个思想，而是边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区间定义不清&lt;/strong&gt;：&lt;code&gt;while (low &amp;lt; high)&lt;/code&gt; 还是 &lt;code&gt;low &amp;lt;= high&lt;/code&gt;？mid 算出来之后，新区间是 &lt;code&gt;[low, mid-1]&lt;/code&gt; 还是 &lt;code&gt;[low, mid]&lt;/code&gt;？两两组合里只有一种自洽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差一位错误（off-by-one）&lt;/strong&gt;：死循环在 n=2 的小数组上，或者漏查最后一个元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回值的语义&lt;/strong&gt;：没找到时返回 -1？还是返回插入位置？调用方怎么处理？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-标准库级别的翻车溢出-bug-藏了九年&#34;&gt;5.3 标准库级别的翻车：溢出 bug 藏了九年&lt;/h3&gt;
&lt;p&gt;2006 年，Google 首席 Java 架构师 Joshua Bloch 在官方博客发了一篇著名的文章《Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken》，自曝家丑：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.util.Arrays.binarySearch&lt;/code&gt; 里的这行代码——&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; mid &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (low &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; high) &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; 2;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;是错的。&lt;/strong&gt; 当 low 和 high 都很大时，&lt;code&gt;low + high&lt;/code&gt; 会溢出成负数，mid 变成负值，抛 ArrayIndexOutOfBoundsException。更扎心的是：这段代码最初正是 Bloch 自己写的，在他的《Effective Java》里也作为范例出现过；同样的 bug 还潜伏在归并排序的实现里，而且&lt;strong&gt;在 JDK 里藏了大约九年&lt;/strong&gt;才被发现——因为要让 low + high 溢出，数组得大到 10^9 量级，当年的机器根本装不下这么大的数组。&lt;/p&gt;
&lt;p&gt;修复方式也很&amp;quot;珠玑&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; mid &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; low &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (high &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; low) &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; 2;        &lt;span style=&#34;color:#75715e&#34;&gt;// 减法会溢出吗？low&amp;lt;=high 时不会&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; mid &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (low &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; high) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; 1;            &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 或者用无符号右移&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-验证环节的正确姿势&#34;&gt;5.4 验证环节的正确姿势&lt;/h3&gt;
&lt;p&gt;Bentley 的对策不是&amp;quot;更聪明&amp;quot;，而是&lt;strong&gt;更机械&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用循环不变式写程序&lt;/strong&gt;：写二分查找前先写下&amp;quot;如果目标存在，它一定在 [low, high] 区间内&amp;quot;，每一轮循环保持这个断言为真，程序的正确性就跟着不变式走，而不是靠感觉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;断言（assert）进代码&lt;/strong&gt;：把不变式直接写成断言，测试期全部打开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小数据穷举对拍&lt;/strong&gt;：n=0、1、2、3 的所有情况手算或穷举，目标值在开头、结尾、中间、不存在各试一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拿慢版本对拍&lt;/strong&gt;：用一个 O(n) 线性查找和二分查找在随机数据上跑几万次，结果必须一致——这就是 2.2 节&amp;quot;蠢但对&amp;quot;版本的真正用途。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：简单算法 + 复杂边界 = 专业坟场。正确性不靠自信，靠不变式和机械验证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-估算的艺术信封背面的算术&#34;&gt;6. 估算的艺术：信封背面的算术&lt;/h2&gt;
&lt;p&gt;闭环里的&amp;quot;估算&amp;quot;环节，Bentley 把它叫做 &lt;strong&gt;back-of-the-envelope calculation&lt;/strong&gt;（信封背面估算）：在餐厅信封背面用粗略数字、心算，三分钟搞清楚问题的规模。它的目的不是精确，而是&lt;strong&gt;在投入大量工作之前，先确认方向是不是错的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;61-为什么要估算&#34;&gt;6.1 为什么要估算&lt;/h3&gt;
&lt;p&gt;回到位图排序的例子：那个&amp;quot;大约 1MB 内存&amp;quot;的约束，配合&amp;quot;至多 900 万个 7 位数&amp;quot;，先算一笔——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果每个数存成 32 位整数：900 万 × 4 字节 = 36 MB，&lt;strong&gt;内存放不下&lt;/strong&gt;，所以&amp;quot;全部读进内存排序&amp;quot;这条路当场枪毙；&lt;/li&gt;
&lt;li&gt;而 10^7 位的位图 ≈ 1.19 MB，&lt;strong&gt;勉强塞得下&lt;/strong&gt;，方案成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两笔心算，直接决定了整个方案的形态。这就是估算的价值：&lt;strong&gt;它花三分钟，替你省掉三天返工。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;62-随身武器一72-法则&#34;&gt;6.2 随身武器一：72 法则&lt;/h3&gt;
&lt;p&gt;钱以年利率 r% 复利增长，大约 72/r 年翻一番。年利率 6%，约 12 年翻倍；利率 12%，约 6 年翻倍。&lt;/p&gt;
&lt;p&gt;这个法则对程序员的用处远超理财——&lt;strong&gt;凡是指数增长的东西都适用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据量每年涨 40%？72/40 ≈ 1.8 年翻一倍，三年后是现在的 5 倍多，现在的存储架构撑得住吗？&lt;/li&gt;
&lt;li&gt;程序每慢 1 倍，意味着规模翻倍时要多等一倍时间——反过来，复杂度从 O(n²) 降到 O(n log n) 在 n 翻倍时省多少，心算就能估出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;63-随身武器二π-秒就是一个纳世纪&#34;&gt;6.3 随身武器二：π 秒就是一个纳世纪&lt;/h3&gt;
&lt;p&gt;Bentley 在书里给了一个极好记的常数：&lt;strong&gt;π 秒 ≈ 一纳世纪（nanocentury，10^-9 个世纪 ≈ 3.155 秒）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;用法：程序每秒跑多少操作、跑一世纪要多久，换算成&amp;quot;纳世纪&amp;quot;立刻有直觉。比如一个操作 1 微秒，跑 10^9 次就是 1000 秒 ≈ 318 个纳世纪 ≈ 6 分钟——心里有没有数，决定你要不要现在就换算法。&lt;/p&gt;
&lt;p&gt;再配几个该背下来的量级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2^10 ≈ 10^3（千），2^20 ≈ 10^6（百万），2^30 ≈ 10^9（十亿）；&lt;/li&gt;
&lt;li&gt;磁盘顺序读比随机读快几个数量级；内存比磁盘快约 10^5 倍；&lt;/li&gt;
&lt;li&gt;一年约 π × 10^7 秒。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;64-随身武器三安全系数与双重估算&#34;&gt;6.4 随身武器三：安全系数与双重估算&lt;/h3&gt;
&lt;p&gt;Bentley 建议的重要习惯：&lt;strong&gt;同一个问题用两种不同路径估算，结果应该对得上；对不上就查到哪里错了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;书里举过估算密西西比河日径流量的例子：一条路径从&amp;quot;流域面积 × 降雨&amp;quot;估，另一条从&amp;quot;河道宽度 × 深度 × 流速&amp;quot;估，两个粗算结果在同一个数量级上吻合，估算就可信。工程里的翻版：系统容量规划时，既从&amp;quot;日活 × 人均请求 × 峰值系数&amp;quot;往下算，也从&amp;quot;单机压测 QPS × 机器数&amp;quot;往上顶，两边对上才算完。&lt;/p&gt;
&lt;p&gt;工程连接：今天这活叫 capacity planning，面试里叫 Fermi 问题（&amp;ldquo;北京有多少个加油站&amp;rdquo;）。内核没变：&lt;strong&gt;粗糙但快速的数字直觉，是工程师防止自己被宏大叙事欺骗的唯一武器。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-代码调优闭环的最后一环也是最危险的一环&#34;&gt;7. 代码调优：闭环的最后一环，也是最危险的一环&lt;/h2&gt;
&lt;h3 id=&#34;71-先立规矩&#34;&gt;7.1 先立规矩&lt;/h3&gt;
&lt;p&gt;Bentley 对调优的态度非常克制，三条规矩：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先正确，再调优。&lt;/strong&gt; 调优引入的复杂度会制造 bug，而一个跑得快但算错的程序毫无价值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先测量，再调优。&lt;/strong&gt; 瓶颈几乎从不在你以为的地方。用 profiler 找到热点，只调那 5% 的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法层级收益 &amp;raquo; 代码层级收益。&lt;/strong&gt; O(n²) 换 O(n log n) 可能快一千倍；循环展开通常只快百分之几十。调优是锦上添花，不是雪中送炭。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;72-书里反复出现的几招&#34;&gt;7.2 书里反复出现的几招&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;缓存（避免重复计算）。&lt;/strong&gt; 如果同一个值被算了很多次，算一次存起来。最现代的版本就是 memoization 和各级缓存体系。书里有个经典场景：频繁计算 &lt;code&gt;c[i][j]&lt;/code&gt; 这类昂贵表达式，而它在循环里其实不变——&lt;strong&gt;把循环不变式提到循环外&lt;/strong&gt;，或者干脆预处理一张表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;哨兵（sentinel）。&lt;/strong&gt; 顺序查找时，先把要查找的值放到数组末尾当&amp;quot;哨兵&amp;quot;，内层循环就可以省掉&amp;quot;是否越界&amp;quot;的判断，比较次数近似减半：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;x[n] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; t;                    &lt;span style=&#34;color:#75715e&#34;&gt;/* 哨兵：保证一定能找到 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; x[i] &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; t; i&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;/* 循环里只剩一个比较 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;查找命中与否分开处理。思想内核：&lt;strong&gt;用一次提前的准备，换掉循环里千万次的检查。&lt;/strong&gt; 今天的 &lt;code&gt;std::string&lt;/code&gt; 以 &lt;code&gt;\0&lt;/code&gt; 结尾、链表的 dummy head 节点，都是哨兵思想的亲戚。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;循环展开。&lt;/strong&gt; 把循环体复制几份，减少循环控制开销。Bentley 同时警告：展开会让代码变长、指令缓存变挤，而且现代编译器自己会做。&lt;strong&gt;手写展开之前，先确认编译器没帮你做，并且 profiler 说这里值得。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试重排。&lt;/strong&gt; 一串 if-else 判断，把命中频率最高的条件放最前面。思想内核和哈夫曼编码一样：高频路径应该最短。&lt;/p&gt;
&lt;h3 id=&#34;73-今天的读者要打个折扣&#34;&gt;7.3 今天的读者要打个折扣&lt;/h3&gt;
&lt;p&gt;这本书写于编译器还笨、内存按 KB 算的年代。今天读调优章节要带两个过滤器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译器已经很强&lt;/strong&gt;：很多手工强度削减、展开，&lt;code&gt;-O2&lt;/code&gt; 会替你做，手写反而碍事；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;瓶颈换了地方&lt;/strong&gt;：当年的敌人是指令数，今天的敌人常常是&lt;strong&gt;缓存未命中和内存墙&lt;/strong&gt;——Bentley 的&amp;quot;缓存重复计算&amp;quot;思想没过时，但主战场从 ALU 挪到了 cache line。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不变的元规则：&lt;strong&gt;测量驱动。&lt;/strong&gt; 不调没测过的代码，不信没数字的优化。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-番外bentley-与-knuth-的那次交锋&#34;&gt;8. 番外：Bentley 与 Knuth 的那次交锋&lt;/h2&gt;
&lt;p&gt;这是计算机科学史上很有名的一段公案，也值得单独记一笔。&lt;/p&gt;
&lt;p&gt;1986 年，Bentley 在自己的专栏里请 Donald Knuth 展示他倡导的 &lt;strong&gt;literate programming&lt;/strong&gt;（文学编程，用 WEB 系统把代码和文档织在一起）。Knuth 领的题目很朴素：读一个文本文件，统计并输出出现次数最多的 k 个单词及次数。&lt;/p&gt;
&lt;p&gt;Knuth 交出了一篇十多页、结构精妙的 WEB 程序，专为这个问题设计了一个巧妙的数据结构（带计数的有序 trie）。Bentley 把它发了出来，同时请 Unix 之父之一的 Doug McIlroy 写书评。McIlroy 的评论比程序本身更出名——他指出 Knuth 程序的优雅毋庸置疑，但&lt;strong&gt;整个问题用 6 行 Unix shell 就能解决&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tr -cs A-Za-z &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;\n&amp;#39;&lt;/span&gt; | tr A-Z a-z | sort | uniq -c | sort -rn | sed &lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;1&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;q&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;而且这 6 行是每个工程师随手可写、可改、可组合的。&lt;/p&gt;
&lt;p&gt;这场交锋没有输家，它留下的启示是双面的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Knuth 展示了&lt;strong&gt;为一个值得的问题量身打造工具与文档&lt;/strong&gt;能达到的深度；&lt;/li&gt;
&lt;li&gt;McIlroy 展示了&lt;strong&gt;用已有简单工具组合，可能比重新发明深刻十倍的轮子更工程&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这正是《编程珠玑》整本书的张力所在：&lt;strong&gt;深度与简单，各自在什么场合赢。&lt;/strong&gt; Bentley 把两篇都发表出来，本身就是&amp;quot;珠玑&amp;quot;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;9-带走的清单&#34;&gt;9. 带走的清单&lt;/h2&gt;
&lt;p&gt;把全书压成一张可以贴在工位上的清单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;想问题时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 先把问题用一句话写清楚：输入、输出、约束。约束里往往藏着解法（位图排序）。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 信封背面先算规模：数据多大、时间预算多少、哪些方案当场出局（36MB vs 1.19MB）。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 问一句&amp;quot;这个问题有没有更简单的等价形式&amp;quot;——旋转数组的三次求逆就是这么来的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;写代码时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 先写&amp;quot;蠢但对&amp;quot;的版本，留作对照组，方便对拍。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 每降一个数量级靠新视角，不靠更卖力：重用计算 → 分治 → 丢弃无用信息。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 写二分查找这类&amp;quot;简单&amp;quot;算法时，先写循环不变式，再写代码；n=0、1、2 手动过一遍。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 记住 &lt;code&gt;(low + high) / 2&lt;/code&gt; 会溢出：用 &lt;code&gt;low + (high - low) / 2&lt;/code&gt;。标准库都翻过车，你没有豁免权。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;调优时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 先 profile，只调热点；调优前后各测一次，没数字不算数。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 优先换算法和数据结构，再考虑代码级技巧。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 哨兵、缓存重复计算、循环不变式外提，这三招便宜又安全；循环展开先问编译器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;作为工程师&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 背上 72 法则和 &amp;ldquo;π 秒 = 纳世纪&amp;rdquo;，让指数增长在你心里有形状。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 重要估算走两条独立路径，对上了才信。&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 读 Knuth vs McIlroy 之争：深刻的方案和简单的方案，各自配得上各自的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这本书两百多页，值回票价的理由只有一个：&lt;strong&gt;它教的不是答案，而是从拿到问题到交付代码之间，那段大多数人凭感觉乱走的路程，其实每一步都有章法。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考书籍：Jon Bentley《编程珠玑》（Programming Pearls, 2nd Edition）；Joshua Bloch, &amp;ldquo;Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken&amp;rdquo;, Google Research Blog, 2006。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《算法与数据结构》教材学习笔记：一张地图看懂全书，附避坑技巧与典型题</title>
      <link>https://ftmao.com/blog/posts/algorithm-data-structures-textbook-notes/</link>
      <pubDate>Sat, 12 Nov 2022 10:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/algorithm-data-structures-textbook-notes/</guid>
      <description>&lt;h1 id=&#34;算法与数据结构教材学习笔记&#34;&gt;《算法与数据结构》教材学习笔记&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：正在学（或补）大学《算法与数据结构》这门课的新人。
阅读目标：看完能对全书有一个整体地图；知道每种结构&amp;quot;什么时候用、怎么实现、容易踩什么坑&amp;quot;；能用简单代码解决典型问题。
代码说明：实现采用 Python（简洁易懂、贴近伪代码），但思路与语言无关。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-开篇这门课到底在讲什么&#34;&gt;0. 开篇：这门课到底在讲什么&lt;/h2&gt;
&lt;p&gt;《算法与数据结构》整本书其实只回答一个问题：&lt;strong&gt;如何把&amp;quot;数据&amp;quot;组织起来，让&amp;quot;操作&amp;quot;更快。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;教材里的任何一章，都可以拆成三句话：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逻辑结构&lt;/strong&gt;：数据之间的关系（线性、树形、图状、集合）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储结构&lt;/strong&gt;：这些关系在内存里怎么放（顺序存储 / 链式存储 / 散列存储）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运算（算法）&lt;/strong&gt;：在这种组织方式上怎么做增删改查，效率如何。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[现实世界的问题] --&amp;gt; B[抽象出逻辑结构]
    B --&amp;gt; C{选存储结构}
    C --&amp;gt; C1[顺序存储&amp;lt;br/&amp;gt;连续内存, 随机访问快]
    C --&amp;gt; C2[链式存储&amp;lt;br/&amp;gt;指针串联, 增删快]
    C --&amp;gt; C3[散列存储&amp;lt;br/&amp;gt;哈希函数, 查找接近O1]
    C --&amp;gt; D[设计运算/算法]
    D --&amp;gt; E[用复杂度评估好坏]
    E --&amp;gt; F[得到可执行的程序]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;新人最容易犯的错误，是上来就背代码。&lt;strong&gt;正确顺序是：先想清楚逻辑关系 → 再选存储方式 → 最后才是写算法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;评判算法好坏的尺子只有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：操作次数随输入规模 n 增长的趋势（O(1) &amp;lt; O(log n) &amp;lt; O(n) &amp;lt; O(n log n) &amp;lt; O(n²) &amp;lt; O(2ⁿ)）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间复杂度&lt;/strong&gt;：额外占用了多少内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑 #0：复杂度看的是&amp;quot;趋势&amp;quot;不是&amp;quot;精确次数&amp;quot;。O(2n+100) 就是 O(n)，常数和低阶项全部扔掉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-全书知识地图&#34;&gt;1. 全书知识地图&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 基础[&amp;#34;数据结构部分&amp;#34;]
        L1[线性表&amp;lt;br/&amp;gt;顺序表/链表]
        L2[栈与队列&amp;lt;br/&amp;gt;受限线性表]
        L3[串与数组&amp;lt;br/&amp;gt;KMP/特殊矩阵]
        L4[树&amp;lt;br/&amp;gt;二叉树/BST/赫夫曼/B树]
        L5[图&amp;lt;br/&amp;gt;遍历/最短路/生成树]
    end
    subgraph 算法[&amp;#34;算法部分&amp;#34;]
        A1[查找&amp;lt;br/&amp;gt;顺序/二分/哈希]
        A2[排序&amp;lt;br/&amp;gt;插入/交换/选择/归并]
        A3[算法思想&amp;lt;br/&amp;gt;递归/分治/贪心/动态规划]
    end
    基础 --&amp;gt; 算法
    L1 --&amp;gt; L2 --&amp;gt; L3 --&amp;gt; L4 --&amp;gt; L5
    L4 --&amp;gt; A1
    L3 --&amp;gt; A2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;学习主线一句话：&lt;strong&gt;线性表是地基，栈队列是线性表的特例，树和图是推广，查找和排序是应用，递归分治等思想贯穿始终。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-线性表一切的起点&#34;&gt;2. 线性表：一切的起点&lt;/h2&gt;
&lt;h3 id=&#34;21-什么时候用哪种&#34;&gt;2.1 什么时候用哪种&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;顺序表（Python 的 list）&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;链表&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;随机访问第 i 个&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;中间插入/删除&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)（要搬数据）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)（找到位置后）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;内存&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;连续，按需自动扩容&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分散，逐个结点申请&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;典型场景&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;频繁按下标读、长度稳定&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;频繁增删、长度不确定&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话场景判断：&lt;strong&gt;&amp;ldquo;读多写少用数组，增删多用链表&amp;rdquo;。&lt;/strong&gt;（Python 的 &lt;code&gt;list&lt;/code&gt; 是动态数组，自带扩容；教材里的链表则要自己定义结点类。）&lt;/p&gt;
&lt;h3 id=&#34;22-单链表结构与操作图解&#34;&gt;2.2 单链表结构与操作图解&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    head[头指针] --&amp;gt; H((头结点))
    H --&amp;gt; A1[a₁] --&amp;gt; A2[a₂] --&amp;gt; A3[a₃] --&amp;gt; NULL[∅ None]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;在结点 &lt;code&gt;p&lt;/code&gt; 之后插入新结点 &lt;code&gt;s&lt;/code&gt;，&lt;strong&gt;顺序不能反&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant s as 新结点s
    participant p as 结点p
    participant n as p.next
    Note over s,n: 第一步: s.next = p.next (先接上后面)
    s-&amp;gt;&amp;gt;n: s 指向 p 的后继
    Note over s,p: 第二步: p.next = s (再让 p 指向 s)
    p-&amp;gt;&amp;gt;s: p 指向 s&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #1：插入时如果先写 &lt;code&gt;p.next = s&lt;/code&gt;，原来 &lt;code&gt;p.next&lt;/code&gt; 指向的结点就丢了（断链）。永远&lt;strong&gt;先接后继，再改前驱&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;23-简单实现单链表反转&#34;&gt;2.3 简单实现：单链表反转&lt;/h3&gt;
&lt;p&gt;链表反转是理解指针（引用）操作的&amp;quot;Hello World&amp;quot;，也是面试必考：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ListNode&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, val&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, next&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;val &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; val
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; next
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;reverse_list&lt;/span&gt;(head: ListNode) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; ListNode:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;原地反转单链表，返回新的头结点&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    prev, cur &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;, head
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; cur:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        nxt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; cur&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;next   &lt;span style=&#34;color:#75715e&#34;&gt;# 1. 先记住下一个，防止断链&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        cur&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; prev  &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 掉头&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prev &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; cur       &lt;span style=&#34;color:#75715e&#34;&gt;# 3. prev 前进&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        cur &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; nxt        &lt;span style=&#34;color:#75715e&#34;&gt;# 4. cur 前进&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; prev          &lt;span style=&#34;color:#75715e&#34;&gt;# prev 停在原尾结点，即新头&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #2：所有链表题，&lt;strong&gt;先建一个&amp;quot;虚拟头结点&amp;quot;（dummy node）&lt;/strong&gt;，可以让&amp;quot;删除头结点&amp;quot;和&amp;quot;删除中间结点&amp;quot;用同一套代码，不用特判，错误率直线下降。
避坑 #3：引用操作完一定要检查边界：对 &lt;code&gt;None&lt;/code&gt; 取 &lt;code&gt;.next&lt;/code&gt; 会报 &lt;code&gt;AttributeError&lt;/code&gt;，循环里每一步都问自己&amp;quot;这个引用可能是 &lt;code&gt;None&lt;/code&gt; 吗&amp;quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-栈与队列加了规矩的线性表&#34;&gt;3. 栈与队列：加了规矩的线性表&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 栈Stack[&amp;#34;栈：后进先出 LIFO&amp;#34;]
        direction TB
        in1[入栈 push] --&amp;gt; top
        top[栈顶 top] --&amp;gt; out1[出栈 pop]
    end
    subgraph 队列Queue[&amp;#34;队列：先进先出 FIFO&amp;#34;]
        direction LR
        rear[队尾 rear&amp;lt;br/&amp;gt;入队] --&amp;gt; q1[...] --&amp;gt; front[队首 front&amp;lt;br/&amp;gt;出队]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;31-场景速查&#34;&gt;3.1 场景速查&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈&lt;/strong&gt;：括号匹配、表达式求值、函数调用栈、DFS、撤销（Undo）、浏览器后退。Python 里直接用 &lt;code&gt;list&lt;/code&gt;，&lt;code&gt;append()&lt;/code&gt; 入栈、&lt;code&gt;pop()&lt;/code&gt; 出栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;队列&lt;/strong&gt;：BFS、消息队列、缓冲区、按层处理（如二叉树层序遍历）。Python 里用 &lt;code&gt;collections.deque&lt;/code&gt;，&lt;code&gt;append()&lt;/code&gt; 入队、&lt;code&gt;popleft()&lt;/code&gt; 出队（&lt;strong&gt;不要用 &lt;code&gt;list.pop(0)&lt;/code&gt;，那是 O(n)&lt;/strong&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断口诀：&lt;strong&gt;&amp;ldquo;后进先出想栈，排队公平想队列&amp;rdquo;&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;32-简单实现括号匹配&#34;&gt;3.2 简单实现：括号匹配&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;is_match&lt;/span&gt;(s: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;判断字符串中的 () [] &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; 是否匹配&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pairs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;)&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;(&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;]&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{&amp;#39;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    stack &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; s:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;([{&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(c)          &lt;span style=&#34;color:#75715e&#34;&gt;# 左括号入栈&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;elif&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;)]}&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; stack:            &lt;span style=&#34;color:#75715e&#34;&gt;# 坑：栈空还来右括号&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pop() &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; pairs[c]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; stack                 &lt;span style=&#34;color:#75715e&#34;&gt;# 坑：别忘了栈里可能剩左括号&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #4：括号匹配的两个经典漏判——&lt;strong&gt;栈空时遇到右括号&lt;/strong&gt;（如 &lt;code&gt;&amp;quot;)(&amp;quot;&lt;/code&gt;）和&lt;strong&gt;遍历完栈非空&lt;/strong&gt;（如 &lt;code&gt;&amp;quot;(((&amp;quot;&lt;/code&gt;）。考试和面试最爱在这两个地方埋雷。
避坑 #5：顺序队列用定长数组实现时会有&amp;quot;假溢出&amp;quot;（rear 到顶但前面有空位），解法是把数组首尾相接做成&lt;strong&gt;循环队列&lt;/strong&gt;，用 &lt;code&gt;(rear+1) % MAXSIZE == front&lt;/code&gt; 判满。（Python 的 &lt;code&gt;deque&lt;/code&gt; 内部已处理好这个问题，但考试按教材的循环队列答。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-串与-kmp让字符串匹配不回退&#34;&gt;4. 串与 KMP：让字符串匹配不回退&lt;/h2&gt;
&lt;p&gt;朴素的字符串匹配：主串指针 i 每次失配都要回退，最坏 O(n·m)。&lt;/p&gt;
&lt;p&gt;KMP 的核心思想一句话：&lt;strong&gt;失配时利用&amp;quot;已匹配部分的前后缀相同&amp;quot;信息，让主串指针 i 永不回退，只移动模式串指针 j&lt;/strong&gt;，复杂度降到 O(n+m)。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[i 指向主串, j 指向模式串] --&amp;gt; B{&amp;#34;s[i] == p[j] ?&amp;#34;}
    B -- 相等 --&amp;gt; C[i++, j++]
    B -- 不等且 j&amp;gt;0 --&amp;gt; D[&amp;#34;j = next[j]&amp;lt;br/&amp;gt;(模式串自己回退, i 不动)&amp;#34;]
    B -- 不等且 j==0 --&amp;gt; E[i++]
    C --&amp;gt; F{j 到达模式串末尾?}
    D --&amp;gt; B
    E --&amp;gt; B
    F -- 是 --&amp;gt; G[匹配成功]
    F -- 否 --&amp;gt; B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #6：&lt;code&gt;next&lt;/code&gt; 数组的定义是&amp;quot;模式串前 j 个字符中，最长的相等真前后缀长度&amp;quot;。很多人背了代码却没理解这句话，换道题就不会推。&lt;strong&gt;手工推 next 数组&lt;/strong&gt;是教材必考题，务必练几次。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-树最重要的非线性结构&#34;&gt;5. 树：最重要的非线性结构&lt;/h2&gt;
&lt;h3 id=&#34;51-二叉树长什么样&#34;&gt;5.1 二叉树长什么样&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A((A 根)) --&amp;gt; B((B))
    A --&amp;gt; C((C))
    B --&amp;gt; D((D))
    B --&amp;gt; E((E))
    C --&amp;gt; F((F))&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三种遍历对同一棵树的不同访问顺序：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;遍历&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;顺序&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;上图结果&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;典型用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;根→左→右&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;A B D E C F&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;复制树、表达式前缀形式&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;中序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;左→根→右&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;D B E A F C&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;BST 中序 = 有序序列&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;后序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;左→右→根&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;D E B F C A&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;释放树、计算目录大小&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;52-简单实现递归遍历&#34;&gt;5.2 简单实现：递归遍历&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TreeNode&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, val&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, left&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;, right&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;val &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; val
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;left &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; left
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;right &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; right
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;preorder&lt;/span&gt;(root: TreeNode):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;前序遍历；把三行的顺序换成 LVR / LRV 就是中序/后序&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; root &lt;span style=&#34;color:#f92672&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:         &lt;span style=&#34;color:#75715e&#34;&gt;# 递归出口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    visit(root)              &lt;span style=&#34;color:#75715e&#34;&gt;# V：访问根&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    preorder(root&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;left)      &lt;span style=&#34;color:#75715e&#34;&gt;# L：遍历左子树&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    preorder(root&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;right)     &lt;span style=&#34;color:#75715e&#34;&gt;# R：遍历右子树&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #7：树的题 90% 能用递归解决，写递归时先写&lt;strong&gt;递归出口&lt;/strong&gt;（&lt;code&gt;root is None&lt;/code&gt;），再写&amp;quot;我对当前结点做什么、把什么交给子树&amp;quot;。想不清就画一棵三层的树手动模拟。另外注意 Python 默认递归深度约 1000，树很深时要改迭代写法或调 &lt;code&gt;sys.setrecursionlimit&lt;/code&gt;。
避坑 #8：**&amp;ldquo;二叉搜索树（BST）中序遍历结果是升序&amp;rdquo;**这条性质是大量题目的钥匙——验证 BST、找第 K 小、找两结点最近公共祖先都靠它。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;53-教材里的三种重要树&#34;&gt;5.3 教材里的三种重要树&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;赫夫曼树&lt;/strong&gt;：带权路径长度最短的树，用于赫夫曼编码（压缩）。构造方法：每次取权值最小的两棵合并（Python 里用 &lt;code&gt;heapq&lt;/code&gt; 实现非常顺手）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二叉搜索树（BST）&lt;/strong&gt;：左子树所有值 &amp;lt; 根 &amp;lt; 右子树所有值。查找/插入/删除平均 O(log n)，&lt;strong&gt;但退化成链表时最坏 O(n)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平衡二叉树（AVL）与 B 树&lt;/strong&gt;：为了解决 BST 退化问题。AVL 靠旋转保持左右子树高度差 ≤ 1；B 树一个结点存多个关键字，矮而胖，&lt;strong&gt;是数据库索引和文件系统的基石&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑 #9：BST 删除有两个孩子的结点时，标准做法是&lt;strong&gt;用中序直接前驱（左子树最大）或中序直接后继（右子树最小）顶替&lt;/strong&gt;，再删除那个顶替者。直接删会把子树搞丢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-图关系最一般的结构&#34;&gt;6. 图：关系最一般的结构&lt;/h2&gt;
&lt;h3 id=&#34;61-怎么存&#34;&gt;6.1 怎么存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;邻接矩阵&lt;/strong&gt;：&lt;code&gt;g[i][j]&lt;/code&gt; 表示 i 到 j 是否有边。适合稠密图，查边 O(1)，空间 O(n²)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邻接表&lt;/strong&gt;：每个顶点挂一个邻居列表。适合稀疏图，空间 O(n+e)，&lt;strong&gt;默认首选&lt;/strong&gt;。Python 里用 &lt;code&gt;dict[int, list[int]]&lt;/code&gt; 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-两种遍历&#34;&gt;6.2 两种遍历&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph DFS[&amp;#34;DFS 深度优先&amp;#34;]
        d1[访问v] --&amp;gt; d2[选一个未访问邻居&amp;lt;br/&amp;gt;递归深入] --&amp;gt; d3[无路可走才回溯]
    end
    subgraph BFS[&amp;#34;BFS 广度优先&amp;#34;]
        b1[v 入队] --&amp;gt; b2[出队并访问] --&amp;gt; b3[所有未访问邻居入队] --&amp;gt; b2
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; collections &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; deque
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dfs&lt;/span&gt;(g: dict, v, visited: set):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;DFS：递归 + visited 集合防环&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    visited&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(v)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    visit(v)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; g[v]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; visited:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            dfs(g, w, visited)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bfs&lt;/span&gt;(g: dict, s):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;BFS：队列 + visited 集合&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    visited &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {s}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    q &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; deque([s])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; q:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        v &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; q&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;popleft()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        visit(v)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; g[v]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; visited:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                visited&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(w)   &lt;span style=&#34;color:#75715e&#34;&gt;# 坑：入队时就标记，不是出队时&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                q&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(w)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #10：图遍历&lt;strong&gt;必须配 visited&lt;/strong&gt;，漏了它遇到环就死循环。而且要在&lt;strong&gt;入队时&lt;/strong&gt;（不是出队时）标记 visited，否则同一结点会被多次入队。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;63-图的经典应用速查&#34;&gt;6.3 图的经典应用速查&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;问题&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;算法&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;一句话说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;无权图最短路径&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;BFS&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;按层扩展，第一次到达即最短&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;带权（非负）最短路&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Dijkstra&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每次挑当前距离最小的点&amp;quot;定稿&amp;quot;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最小生成树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Prim / Kruskal&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;连通所有点的最小代价（布线、修路）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;课程先修顺序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;拓扑排序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;反复找入度为 0 的点输出&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;关键路径&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;AOE 网&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;工程中最长路径决定工期&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;64-简单实现dijkstra-最短路径&#34;&gt;6.4 简单实现：Dijkstra 最短路径&lt;/h3&gt;
&lt;p&gt;思想一句话：&lt;strong&gt;每次从未定稿的点里挑距离最小的点&amp;quot;定稿&amp;quot;，再用它去松弛所有出边&lt;/strong&gt;（好比涟漪一圈圈扩散，最近的点最先确定）。用最小堆（&lt;code&gt;heapq&lt;/code&gt;）把&amp;quot;挑最小&amp;quot;从 O(n) 降到 O(log n)，总复杂度 O((n+e) log n)。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; heapq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dijkstra&lt;/span&gt;(g: dict, s) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; dict:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;g[u] = [(v, w), ...] 表示 u-&amp;gt;v 有一条权值为 w 的边（要求 w &amp;gt;= 0）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    返回 s 到各点的最短距离，不可达的点不在结果中&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dist &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {s: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pq &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, s)]                      &lt;span style=&#34;color:#75715e&#34;&gt;# (当前距离, 顶点)，最小堆&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    done &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; set()                       &lt;span style=&#34;color:#75715e&#34;&gt;# 已&amp;#34;定稿&amp;#34;的点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; pq:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        d, u &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; heapq&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;heappop(pq)       &lt;span style=&#34;color:#75715e&#34;&gt;# 挑出距离最小的点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; u &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; done:                  &lt;span style=&#34;color:#75715e&#34;&gt;# 坑：堆里可能有同一点的过期记录&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        done&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(u)                    &lt;span style=&#34;color:#75715e&#34;&gt;# 定稿：d 就是 s 到 u 的最短距离&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; v, w &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; g&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(u, []):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; v &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; done &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; d &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; dist&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(v, float(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;inf&amp;#39;&lt;/span&gt;)):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                dist[v] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; d &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; w        &lt;span style=&#34;color:#75715e&#34;&gt;# 松弛：找到更短的路&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                heapq&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;heappush(pq, (dist[v], v))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; dist
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 示例：g = {&amp;#39;A&amp;#39;: [(&amp;#39;B&amp;#39;, 5), (&amp;#39;C&amp;#39;, 2)], &amp;#39;B&amp;#39;: [(&amp;#39;C&amp;#39;, 1)], &amp;#39;C&amp;#39;: [(&amp;#39;B&amp;#39;, 3)]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# dijkstra(g, &amp;#39;A&amp;#39;) -&amp;gt; {&amp;#39;A&amp;#39;: 0, &amp;#39;C&amp;#39;: 2, &amp;#39;B&amp;#39;: 5}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;要点解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;松弛&lt;/strong&gt;：如果&amp;quot;经过 u 再到 v&amp;quot;比已知的 v 距离更短，就更新 v 的距离——&lt;code&gt;dist[v] = d + w&lt;/code&gt; 那一行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;懒惰删除&lt;/strong&gt;：堆里的记录可能已被更新（过期），出堆时用 &lt;code&gt;done&lt;/code&gt; 集合跳过，避免入堆记录删不掉的麻烦。&lt;/li&gt;
&lt;li&gt;想要&lt;strong&gt;具体路径&lt;/strong&gt;而不只是距离？再维护一个 &lt;code&gt;prev[v] = u&lt;/code&gt; 前驱字典，最后从终点倒推即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑 #11：&lt;strong&gt;Dijkstra 不能处理负权边&lt;/strong&gt;（&amp;ldquo;定稿&amp;quot;假设一旦成立就被负边打破），有负权要用 Bellman-Ford。这是考研和面试的经典判断题。
避坑 #12：拓扑排序排不完（输出点数 &amp;lt; 总点数）说明&lt;strong&gt;图中有环&lt;/strong&gt;，工程上意味着任务依赖出现了死锁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-查找&#34;&gt;7. 查找&lt;/h2&gt;
&lt;h3 id=&#34;71-三种基本查找&#34;&gt;7.1 三种基本查找&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;方法&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;前提&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;平均复杂度&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顺序查找&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;无&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;数据少、无序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二分查找&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;有序 + 顺序存储&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;静态有序数据&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;散列（哈希）查找&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;建哈希表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;接近 O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;海量数据快速查询&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;72-简单实现二分查找正确写法&#34;&gt;7.2 简单实现：二分查找（正确写法）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;binary_search&lt;/span&gt;(a: list, key) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; int:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;在有序数组 a 中找 key，找到返回下标，否则返回 -1&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    l, r &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, len(a) &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; r:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mid &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (r &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; l) &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 通用写法，见避坑 #13&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; a[mid] &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; key:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; mid
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;elif&lt;/span&gt; a[mid] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; key:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            l &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mid &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            r &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mid &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #13：Python 的整数不会溢出，但 C/Java 里 &lt;code&gt;mid = (l + r) / 2&lt;/code&gt; 在 l、r 很大时会&lt;strong&gt;整数溢出&lt;/strong&gt;；统一写成 &lt;code&gt;l + (r - l) // 2&lt;/code&gt; 是跨语言都安全的好习惯。（Python 工程代码直接用标准库 &lt;code&gt;bisect&lt;/code&gt; 模块，但手写二分是基本功。）
避坑 #14：二分的边界要全程统一——&lt;code&gt;while l &amp;lt;= r&lt;/code&gt; 配 &lt;code&gt;l = mid + 1 / r = mid - 1&lt;/code&gt;（闭区间写法），不要中途混用左闭右开的条件，否则差一错误（off-by-one）必现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;73-哈希的冲突处理&#34;&gt;7.3 哈希的冲突处理&lt;/h3&gt;
&lt;p&gt;哈希查找快的前提是处理好冲突（两个关键字映射到同一位置）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    K[关键字 key] --&amp;gt; H[哈希函数 h key]
    H --&amp;gt; C{位置被占?}
    C -- 否 --&amp;gt; S[直接存放/命中]
    C -- 是 --&amp;gt; M1[开放定址法&amp;lt;br/&amp;gt;向后探测空位]
    C -- 是 --&amp;gt; M2[链地址法&amp;lt;br/&amp;gt;同位置挂链表]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开放定址法&lt;/strong&gt;：冲突了就顺延找下一个空位（教材重点，删除要设&amp;quot;墓碑&amp;quot;标记）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链地址法&lt;/strong&gt;：同位置元素串成链表（工程主流，Python 的 &lt;code&gt;dict&lt;/code&gt; 和 Java 的 &lt;code&gt;HashMap&lt;/code&gt; 都用类似思路）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑 #15：哈希表的效率取决于&lt;strong&gt;装填因子 α = 元素数 / 表长&lt;/strong&gt;。α 越大冲突越多。工程上 α 超过 0.7 左右就该扩容了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-排序教材的重头戏&#34;&gt;8. 排序：教材的重头戏&lt;/h2&gt;
&lt;h3 id=&#34;81-分类地图&#34;&gt;8.1 分类地图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    S[内部排序] --&amp;gt; I[插入类]
    S --&amp;gt; X[交换类]
    S --&amp;gt; C[选择类]
    S --&amp;gt; M[归并类]
    I --&amp;gt; I1[直接插入 O n²]
    I --&amp;gt; I2[希尔排序 O n^1.3]
    X --&amp;gt; X1[冒泡 O n²]
    X --&amp;gt; X2[快速排序 O n log n&amp;lt;br/&amp;gt;平均最快]
    C --&amp;gt; C1[简单选择 O n²]
    C --&amp;gt; C2[堆排序 O n log n]
    M --&amp;gt; M1[归并排序 O n log n&amp;lt;br/&amp;gt;稳定]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;82-一张表记住所有考点&#34;&gt;8.2 一张表记住所有考点&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;排序&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;平均&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;最坏&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;空间&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;稳定?&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;一句话记忆&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;直接插入&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;✅&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;打扑克插牌&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;希尔&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n^1.3)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;❌&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分组插入&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;冒泡&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;✅&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;相邻交换，轻的浮上去&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快速&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;❌&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分治：选定枢轴分两边&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;简单选择&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;❌&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每轮挑最小的放前面&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;堆&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;❌&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;用堆反复取最值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;归并&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;✅&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先分后合，合并两个有序段&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑 #16：&amp;ldquo;稳定&amp;quot;指&lt;strong&gt;相等元素的相对顺序排序后不变&lt;/strong&gt;。常见坑：快排、堆排、简单选择、希尔都&lt;strong&gt;不稳定&lt;/strong&gt;；插入、冒泡、归并稳定。需要稳定性时（比如先按成绩排再按学号排），选归并。（Python 内置的 &lt;code&gt;sorted()&lt;/code&gt; 是 Timsort，稳定，工程直接用它。）
避坑 #17：快速排序在&lt;strong&gt;输入已基本有序 + 选第一个元素做枢轴&lt;/strong&gt;时退化成 O(n²)，实际工程中要随机选枢轴或三数取中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;83-简单实现快速排序&#34;&gt;8.3 简单实现：快速排序&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;quick_sort&lt;/span&gt;(a: list, l: int, r: int):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;快排：分治思想。partition 把枢轴放到最终位置，左边都比它小，右边都比它大&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; r:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    p &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; partition(a, l, r)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    quick_sort(a, l, p &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;# 递归左半&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    quick_sort(a, p &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, r)   &lt;span style=&#34;color:#75715e&#34;&gt;# 递归右半&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;partition&lt;/span&gt;(a: list, l: int, r: int) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; int:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pivot &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a[l]              &lt;span style=&#34;color:#75715e&#34;&gt;# 选首元素做枢轴（教学版）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; r:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; r &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; a[r] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; pivot:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            r &lt;span style=&#34;color:#f92672&#34;&gt;-=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        a[l] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a[r]           &lt;span style=&#34;color:#75715e&#34;&gt;# 小的补到左边&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; l &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; r &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; a[l] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; pivot:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            l &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        a[r] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a[l]           &lt;span style=&#34;color:#75715e&#34;&gt;# 大的补到右边&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    a[l] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pivot              &lt;span style=&#34;color:#75715e&#34;&gt;# 枢轴归位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; l&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;9-算法思想递归分治贪心动态规划&#34;&gt;9. 算法思想：递归、分治、贪心、动态规划&lt;/h2&gt;
&lt;p&gt;教材后半部分的灵魂。四者关系一句话：&lt;strong&gt;递归是手段，分治是&amp;quot;拆开再合并&amp;rdquo;，贪心是&amp;quot;每步选当前最好&amp;rdquo;，动态规划是&amp;quot;记住算过的子问题别重算&amp;quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    P[大问题] --&amp;gt; D{能否拆成&amp;lt;br/&amp;gt;同类子问题?}
    D -- 能, 子问题独立 --&amp;gt; DC[分治&amp;lt;br/&amp;gt;如归并排序/快排]
    D -- 能, 子问题重叠 --&amp;gt; DP[动态规划&amp;lt;br/&amp;gt;存表避免重算]
    P --&amp;gt; G{每步局部最优&amp;lt;br/&amp;gt;能推出全局最优?}
    G -- 能 --&amp;gt; GR[贪心&amp;lt;br/&amp;gt;如赫夫曼/Kruskal]
    G -- 不能 --&amp;gt; DP&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;91-简单实现爬楼梯dp-入门第一题&#34;&gt;9.1 简单实现：爬楼梯（DP 入门第一题）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;climb_stairs&lt;/span&gt;(n: int) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; int:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;一次爬 1 或 2 级台阶，n 级有多少种爬法？&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; n
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    a, b &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;               &lt;span style=&#34;color:#75715e&#34;&gt;# dp[1]=1, dp[2]=2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; _ &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;, n &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        a, b &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; b, a &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; b       &lt;span style=&#34;color:#75715e&#34;&gt;# dp[i] = dp[i-1] + dp[i-2]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; b&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;避坑 #18：写动态规划&lt;strong&gt;先别动手写代码&lt;/strong&gt;，先用一句中文说清&amp;quot;dp[i] 表示什么&amp;quot;（状态定义），再写状态转移方程。这两步想清楚了，代码是机械翻译。直接上手写代码的，十有八九死在边界初始化上。（递归版 DP 记得加 &lt;code&gt;@functools.lru_cache&lt;/code&gt;，否则指数级重复计算。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;10-典型问题速查表&#34;&gt;10. 典型问题速查表&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;典型问题&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;所属章节&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解题钥匙&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;单链表反转 / 找倒数第 K 个结点&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;线性表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;三指针原地反转 / 快慢指针&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;括号匹配、表达式求值&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;栈&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;左括号入栈，右括号出栈比对&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;约瑟夫问题&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;线性表/队列&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;循环链表模拟，或队列反复出队入队&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;汉诺塔&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;递归&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;把 n-1 个盘移到辅助柱 → 移大盘 → 再移回&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;判断/还原二叉树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前序+中序可唯一确定一棵树&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;求树高、叶子数&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;递归：&lt;code&gt;1 + max(左高, 右高)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;赫夫曼编码&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每次合并最小两个权值（&lt;code&gt;heapq&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;迷宫问题&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;栈/DFS&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;栈记录路径，走不通就回溯&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;无权最短路&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;图/BFS&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;BFS 按层扩展&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;课程表能否修完&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;图/拓扑&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;拓扑排序 + 判环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前 K 大元素&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;堆&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;维护大小为 K 的小顶堆（&lt;code&gt;heapq&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;合并 K 个有序数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;归并&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;两两归并或堆&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;11-全书避坑清单考前面试前过一遍&#34;&gt;11. 全书避坑清单（考前/面试前过一遍）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;复杂度只看最高阶，扔掉常数。&lt;/li&gt;
&lt;li&gt;链表插入：先接后继，再改前驱；万能技巧：虚拟头结点。&lt;/li&gt;
&lt;li&gt;括号匹配：查&amp;quot;栈空遇右括号&amp;quot;和&amp;quot;遍历完栈非空&amp;quot;两种漏判。&lt;/li&gt;
&lt;li&gt;循环队列判满：&lt;code&gt;(rear+1) % MAX == front&lt;/code&gt;；队列出队用 &lt;code&gt;deque.popleft()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;KMP 的 next 数组要会手工推。&lt;/li&gt;
&lt;li&gt;树的递归：先写递归出口；注意 Python 递归深度限制。&lt;/li&gt;
&lt;li&gt;记住：BST 中序遍历 = 升序。&lt;/li&gt;
&lt;li&gt;图遍历必须 visited，BFS 在入队时标记。&lt;/li&gt;
&lt;li&gt;Dijkstra 不支持负权边；拓扑排不完 = 有环。&lt;/li&gt;
&lt;li&gt;二分：&lt;code&gt;mid = l + (r - l) // 2&lt;/code&gt;，区间定义全程统一。&lt;/li&gt;
&lt;li&gt;哈希关注装填因子；开放定址法删除要打标记不能真删空。&lt;/li&gt;
&lt;li&gt;快排/堆排/简单选择/希尔不稳定；快排怕有序输入。&lt;/li&gt;
&lt;li&gt;动态规划：先说清 dp[i] 的含义，再写转移方程，最后处理初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;12-学习路线建议&#34;&gt;12. 学习路线建议&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    W1[第1-2周&amp;lt;br/&amp;gt;复杂度+线性表] --&amp;gt; W2[第3-4周&amp;lt;br/&amp;gt;栈/队列/串]
    W2 --&amp;gt; W3[第5-6周&amp;lt;br/&amp;gt;树与二叉树]
    W3 --&amp;gt; W4[第7-8周&amp;lt;br/&amp;gt;图]
    W4 --&amp;gt; W5[第9-10周&amp;lt;br/&amp;gt;查找+排序综合]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三点经验之谈：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每个结构都亲手实现一遍&lt;/strong&gt;。看懂和会写之间隔着一条鸿沟，哪怕写得慢，也要自己敲出链表、栈、二叉树遍历、快排这&amp;quot;四大件&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;画图比读代码快&lt;/strong&gt;。指针题、树题、图题，先在纸上画出结构变化，再翻译成代码，出错率低一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题做不出来别死磕&lt;/strong&gt;。20 分钟没思路就看解析，重点搞懂&amp;quot;人家为什么想到用这个结构&amp;quot;，第二天独立重写一遍。吃透一类题，胜过囫囵刷一百道。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;
&lt;p&gt;《算法与数据结构》这本书看起来章节多，其实主线非常清晰：&lt;strong&gt;数据怎么组织（结构）→ 操作怎么实现（算法）→ 效率怎么评估（复杂度）&lt;/strong&gt;。把这三问套到每一章，全书就从&amp;quot;一堆要背的知识&amp;quot;变成&amp;quot;一张可以推理的地图&amp;quot;。&lt;/p&gt;
&lt;p&gt;希望这份笔记能帮你把地图先装进脑子里，剩下的，就是动手写、动手画、动手刷。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《数据结构与算法》教材笔记（树篇）：从二叉查找树一路推导到 B&#43; 树，为什么数据库索引非它不可</title>
      <link>https://ftmao.com/blog/posts/data-structures-tree-to-btree/</link>
      <pubDate>Wed, 02 Nov 2022 09:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/data-structures-tree-to-btree/</guid>
      <description>&lt;h1 id=&#34;数据结构与算法教材笔记树篇从二叉查找树一路推导到-b-树为什么数据库索引非它不可&#34;&gt;《数据结构与算法》教材笔记（树篇）：从二叉查找树一路推导到 B+ 树，为什么数据库索引非它不可&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：学过一遍数据结构、能写二叉树遍历，但被问到&amp;quot;B 树和红黑树有什么区别、为什么 MySQL 用 B+ 树&amp;quot;就说不囫囵的同学。
姊妹篇：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/algorithm-and-data-structures-basics/&#34;&gt;算法与数据结构入门学习总结&lt;/a&gt;》是&lt;strong&gt;刷题向&lt;/strong&gt;的——讲双指针、滑动窗口、DP 模板，目标是过 LeetCode。这篇是&lt;strong&gt;教材向&lt;/strong&gt;的——讲每种结构&amp;quot;为什么被发明出来&amp;quot;，目标是看懂工程世界。两篇文章覆盖的内容几乎不重叠，建议先读哪篇都行。
实战印证：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mysql-practical-guide/&#34;&gt;MySQL 实战指南&lt;/a&gt;》里讲的索引失效、覆盖索引，《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mongodb-beginner-to-mastery/&#34;&gt;MongoDB 从入门到精通&lt;/a&gt;》里讲的 WiredTiger B 树页、页分裂，底层原理都在这篇里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;0-这篇只讲一件事树是怎么一步步被逼成-b-树的&#34;&gt;0. 这篇只讲一件事：树是怎么一步步被逼成 B+ 树的&lt;/h2&gt;
&lt;p&gt;教材里的结构很多，数组、链表、栈、队列、哈希、图……但刷题篇已经覆盖了大部分，这篇把 70% 的篇幅压在一根主线上——&lt;strong&gt;树的进化链&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;二叉查找树 BST&amp;lt;br/&amp;gt;有序 但会退化&amp;#34;] --&amp;gt;|&amp;#34;痛点: 退化成链表&amp;#34;| B[&amp;#34;AVL 树&amp;lt;br/&amp;gt;严格平衡&amp;#34;]
    B --&amp;gt;|&amp;#34;痛点: 旋转太频繁&amp;lt;br/&amp;gt;写多读少顶不住&amp;#34;| C[&amp;#34;红黑树&amp;lt;br/&amp;gt;近似平衡 工程妥协&amp;#34;]
    C --&amp;gt;|&amp;#34;痛点: 再高也是二叉&amp;lt;br/&amp;gt;树太高 磁盘 I/O 多&amp;#34;| D[&amp;#34;2-3 树&amp;lt;br/&amp;gt;一个节点放多个 key&amp;#34;]
    D --&amp;gt;|&amp;#34;推广&amp;#34;| E[&amp;#34;m 阶 B 树&amp;lt;br/&amp;gt;阶数对齐磁盘页&amp;#34;]
    E --&amp;gt;|&amp;#34;痛点: 内节点存数据&amp;lt;br/&amp;gt;范围扫描难&amp;#34;| F[&amp;#34;B+ 树&amp;lt;br/&amp;gt;内节点纯索引&amp;lt;br/&amp;gt;叶节点链表&amp;#34;]
    style F fill:#ffd699&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每一步都不是&amp;quot;更高级&amp;quot;，而是&lt;strong&gt;上一步在某个具体场景下撑不住了&lt;/strong&gt;。记住这条链上的每个箭头，你就永远不会把 B 树讲成背定义。&lt;/p&gt;
&lt;p&gt;另外教材里还有两个刷题篇没覆盖的点——哈希冲突的两种解法、图的最短路径与最小生成树——放在最后简单补全。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-二叉查找树一切的开始和它的死穴&#34;&gt;1. 二叉查找树：一切的开始，和它的死穴&lt;/h2&gt;
&lt;p&gt;二叉查找树（BST）的规则一句话：左子树所有值 &amp;lt; 根 &amp;lt; 右子树所有值。查找、插入、删除平均都是 O(log n)，看起来很美。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;死穴：它不管自己的形状。&lt;/strong&gt; 按 1, 2, 3, 4, 5, 6, 7 的顺序插入，你会得到：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[1] --&amp;gt; B[2]
    B --&amp;gt; C[3]
    C --&amp;gt; D[4]
    D --&amp;gt; E[5]
    E --&amp;gt; F[6]
    F --&amp;gt; G[7]
    style A fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这棵树退化成了&lt;strong&gt;链表&lt;/strong&gt;，查找变成 O(n)。而&amp;quot;按顺序插入&amp;quot;在真实世界里太常见了——自增 ID、时间戳，天然就是有序的。BST 的平均复杂度是拿&amp;quot;数据随机到达&amp;quot;换来的，工程上没人敢赌这个。&lt;/p&gt;
&lt;p&gt;所以需要&lt;strong&gt;平衡&lt;/strong&gt;：插入删除时主动调整形状，保证树高稳定在 log n。怎么调？教材给了两个答案，一个完美主义，一个实用主义。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-avl-树完美主义的代价&#34;&gt;2. AVL 树：完美主义的代价&lt;/h2&gt;
&lt;p&gt;AVL 的规则：&lt;strong&gt;任意节点的左右子树高度差不超过 1&lt;/strong&gt;。这是最严格的平衡，树高被死死压在约 1.44 log n，查找最快。&lt;/p&gt;
&lt;p&gt;代价是：每次插入删除破坏平衡时，要做&lt;strong&gt;旋转&lt;/strong&gt;来修复。四种失衡形态：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph LL[&amp;#34;LL 型：左孩子的左子树插入&amp;#34;]
        direction LR
        L1[&amp;#34;  30        右旋        20&amp;lt;br/&amp;gt;  /           →        /  \&amp;lt;br/&amp;gt;20                 10   30&amp;lt;br/&amp;gt;/&amp;lt;br/&amp;gt;10&amp;#34;]
    end
    subgraph RR[&amp;#34;RR 型：右孩子的右子树插入&amp;#34;]
        direction LR
        R1[&amp;#34;10          左旋          20&amp;lt;br/&amp;gt;  \\          →         /  \&amp;lt;br/&amp;gt;  20                 10   30&amp;lt;br/&amp;gt;    \\&amp;lt;br/&amp;gt;    30&amp;#34;]
    end
    subgraph LRRL[&amp;#34;LR / RL 型：拐弯失衡 → 先转成 LL/RR 再转&amp;lt;br/&amp;gt;（两次旋转）&amp;#34;]
        LR1[&amp;#34;先对子树反向旋转&amp;lt;br/&amp;gt;再对根旋转&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;旋转本身不复杂，麻烦的是频率：&lt;strong&gt;插入最多旋 2 次就停，但删除可能要从底向上旋 O(log n) 次&lt;/strong&gt;。读多写少的场景没问题，写多读少的场景（比如频繁更新的索引）旋转本身就吃掉了平衡带来的收益。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;学习心得&lt;/strong&gt;：我当年把四种旋转的图背得滚瓜烂熟，考试一过全忘了。后来发现根本不用背——&lt;strong&gt;LR 就是&amp;quot;先左旋成 LL，再右旋&amp;quot;&lt;/strong&gt;，四种形态只是&amp;quot;一次还是两次、往哪边转&amp;quot;的组合。理解失衡的方向比记图重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;3-红黑树工程妥协的胜利&#34;&gt;3. 红黑树：工程妥协的胜利&lt;/h2&gt;
&lt;p&gt;红黑树放弃了&amp;quot;严格平衡&amp;quot;，换成一组染色规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个节点非红即黑；&lt;/li&gt;
&lt;li&gt;根和叶子（NIL）是黑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;红色节点的孩子必须是黑&lt;/strong&gt;（不能有两个红挨着）;&lt;/li&gt;
&lt;li&gt;任意节点到其所有后代叶子的路径上，&lt;strong&gt;黑节点数量相同&lt;/strong&gt;（黑高一致）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这四条合起来保证的不是&amp;quot;左右一样高&amp;quot;，而是一个更弱的结论：&lt;strong&gt;最长路径不超过最短路径的 2 倍&lt;/strong&gt;。树高 ≈ 2 log n，比 AVL 略高，但换来的是插入删除平均只需 O(1) 次旋转。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph AVL对比[&amp;#34;AVL vs 红黑树&amp;#34;]
        direction LR
        V1[&amp;#34;AVL&amp;lt;br/&amp;gt;高 1.44·log n&amp;lt;br/&amp;gt;查得快&amp;lt;br/&amp;gt;删最多旋 log n 次&amp;#34;]
        V2[&amp;#34;红黑树&amp;lt;br/&amp;gt;高 2·log n&amp;lt;br/&amp;gt;查得稍慢&amp;lt;br/&amp;gt;旋 O(1) 次收工&amp;#34;]
    end
    V1 -.-&amp;gt;|&amp;#34;读写均衡时&amp;lt;br/&amp;gt;工程选它&amp;#34;| V2
    style V2 fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;红黑树是工程世界的宠儿：Java 的 &lt;code&gt;TreeMap&lt;/code&gt;、Linux 内核的 CFS 调度器、epoll 的就绪队列、C++ 的 &lt;code&gt;std::map&lt;/code&gt;，全是它。&lt;strong&gt;面试被问&amp;quot;为什么用红黑树不用 AVL&amp;quot;，标准答案就是这一句：读多写少用 AVL（查得快），读写均衡用红黑树（调整便宜）。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 1：手撕红黑树是最不值的投入&lt;/strong&gt;
面试准备期我花了一周想手写红黑树的删除——三种 Case、镜像三种、还有双重黑的修复，写完 200 行测试还是漏了一个分支。后来和做了十年基础设施的朋友聊，他说：&amp;ldquo;我工作里一次都没写过红黑树，但我每周都在做红黑树式的决策——&lt;strong&gt;在&amp;rsquo;严格&amp;rsquo;和&amp;rsquo;够用&amp;rsquo;之间选够用&lt;/strong&gt;。&amp;ldquo;那一周的真正收获不是会写了，而是看懂了工程权衡的形状。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;4-2-3-树b-树的真身换个方向解决平衡&#34;&gt;4. 2-3 树：B 树的真身，换个方向解决平衡&lt;/h2&gt;
&lt;p&gt;AVL 和红黑树都是&amp;quot;二叉 + 旋转修复&amp;rdquo;。2-3 树换了个思路：&lt;strong&gt;允许一个节点放多个 key，靠节点分裂来维持平衡&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点分两种：2-节点（1 个 key，2 个孩子）、3-节点（2 个 key，3 个孩子）；&lt;/li&gt;
&lt;li&gt;所有叶子在&lt;strong&gt;同一层&lt;/strong&gt;（绝对平衡，比 AVL 还狠）；&lt;/li&gt;
&lt;li&gt;插入永远发生在叶子：叶子满了（临时变成 3 个 key）就&lt;strong&gt;中间 key 向上分裂&lt;/strong&gt;，父节点跟着调整，可能一路裂到根——树是这样&amp;quot;从下往上长&amp;quot;的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次分裂长这样：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph BEFORE[&amp;#34;插入 7，叶子已有 5 6，临时三 key 超载&amp;#34;]
        B1[&amp;#34;      [9]&amp;lt;br/&amp;gt;  [5,6]  ← 插入7后变 [5,6,7]&amp;#34;]
    end
    subgraph AFTER[&amp;#34;中间 key 6 上提，裂成两个节点&amp;#34;]
        A1[&amp;#34;        [6,  9]&amp;lt;br/&amp;gt;   /     |      \\&amp;lt;br/&amp;gt; [5]   [7]   [...]&amp;#34;]
    end
    BEFORE --&amp;gt; AFTER&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;2-3 树的意义不在它自己（几乎没人直接用），而在于它证明了一件事：&lt;strong&gt;平衡不一定靠旋转，也可以靠&amp;quot;节点容量 + 向上分裂&amp;rdquo;&lt;/strong&gt;。把这个思路的容量从 2 个 key 放大到几百个，就是 B 树。&lt;/p&gt;
&lt;p&gt;顺带一提：&lt;strong&gt;红黑树其实就是 2-3 树的二叉表示&lt;/strong&gt;——把 3-节点的两个 key 用一条红色边连起来，就是红黑树。理解了 2-3 树再回头看红黑树规则，&amp;ldquo;不能两个红挨着&amp;quot;就是&amp;quot;一个 3-节点最多两个 key&amp;rdquo;，瞬间全部通顺。这是教材推导最美的一段。&lt;/p&gt;
&lt;h2 id=&#34;5-b-树为磁盘而生的矮胖树&#34;&gt;5. B 树：为磁盘而生的矮胖树&lt;/h2&gt;
&lt;h3 id=&#34;51-为什么二叉树到了磁盘上就不行了&#34;&gt;5.1 为什么二叉树到了磁盘上就不行了&lt;/h3&gt;
&lt;p&gt;到这里要引入这篇的真正主角：&lt;strong&gt;磁盘 I/O&lt;/strong&gt;。内存里比较一次是纳秒级，磁盘读一次是毫秒级（机械盘）到几十微秒级（SSD），&lt;strong&gt;差着 4~6 个数量级&lt;/strong&gt;。所以磁盘世界的复杂度单位不是&amp;quot;比较次数&amp;quot;，而是&amp;quot;&lt;strong&gt;读了几个磁盘页&lt;/strong&gt;&amp;quot;。&lt;/p&gt;
&lt;p&gt;一棵 100 万个 key 的平衡二叉树，高约 20 层——查一个 key 最坏要 &lt;strong&gt;20 次磁盘 I/O&lt;/strong&gt;。这就是为什么二叉平衡树统治内存、却进不了磁盘。&lt;/p&gt;
&lt;p&gt;B 树的解法简单粗暴：&lt;strong&gt;把每个节点做到一个磁盘页那么大&lt;/strong&gt;（InnoDB 默认 16KB）。一页能放几百个 key，树高立刻塌下来：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph BTREE[&amp;#34;一棵 m 阶 B 树（每个节点 = 一个 16KB 磁盘页）&amp;#34;]
        ROOT[&amp;#34;根页&amp;lt;br/&amp;gt;[k1 | k2 | ... | k400]&amp;lt;br/&amp;gt;1 次 I/O&amp;#34;]
        ROOT --&amp;gt; L2A[&amp;#34;内页&amp;lt;br/&amp;gt;400 个 key&amp;lt;br/&amp;gt;1 次 I/O&amp;#34;]
        ROOT --&amp;gt; L2B[&amp;#34;内页 ...&amp;#34;]
        ROOT --&amp;gt; L2C[&amp;#34;内页 ...&amp;#34;]
        L2A --&amp;gt; L3[&amp;#34;叶页层&amp;lt;br/&amp;gt;每页 400 条记录&amp;lt;br/&amp;gt;1 次 I/O&amp;#34;]
    end
    NOTE[&amp;#34;100 万 key：树高 ≈ 2~3&amp;lt;br/&amp;gt;= 最多 3 次磁盘 I/O&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;算一笔账：每页 400 个 key，两层就是 400 × 400 = 16 万，三层就是 6400 万。&lt;strong&gt;1 亿条数据的索引，3 次 I/O 定位&lt;/strong&gt;——这就是&amp;quot;千万级数据 B 树也就 4 层&amp;quot;（MongoDB 篇里的那句话）的出处。&lt;/p&gt;
&lt;h3 id=&#34;52-m-阶-b-树的定义五条一条都不能少&#34;&gt;5.2 m 阶 B 树的定义（五条，一条都不能少）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;每个节点最多 &lt;strong&gt;m 个孩子&lt;/strong&gt;（m 阶）；&lt;/li&gt;
&lt;li&gt;非根节点至少有 &lt;strong&gt;⌈m/2⌉ 个孩子&lt;/strong&gt;（保证节点不会太空）；&lt;/li&gt;
&lt;li&gt;有 k 个孩子的节点恰好有 &lt;strong&gt;k-1 个 key&lt;/strong&gt;，key 排序，充当孩子之间的&amp;quot;路标&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有叶子在同一层&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;根至少 2 个孩子（除非是叶子）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;插入和删除是 2-3 树逻辑的放大版：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 插入[&amp;#34;插入：叶子满了就分裂&amp;#34;]
        I1[&amp;#34;叶子 key 数 = m&amp;lt;br/&amp;gt;超载&amp;#34;] --&amp;gt; I2[&amp;#34;中间 key 上提&amp;lt;br/&amp;gt;裂成两半&amp;lt;br/&amp;gt;各留 ⌈m/2⌉-1 个&amp;#34;]
        I2 --&amp;gt; I3[&amp;#34;父节点也满？&amp;lt;br/&amp;gt;继续向上裂&amp;lt;br/&amp;gt;最坏裂到根 树高+1&amp;#34;]
    end
    subgraph 删除[&amp;#34;删除：节点太空就借/并&amp;#34;]
        D1[&amp;#34;key 数 &amp;lt; ⌈m/2⌉-1&amp;#34;] --&amp;gt; D2{&amp;#34;兄弟有富余？&amp;#34;}
        D2 --&amp;gt;|有| D3[&amp;#34;向兄弟借一个&amp;lt;br/&amp;gt;（父key下来 兄弟key上去）&amp;#34;]
        D2 --&amp;gt;|没有| D4[&amp;#34;和兄弟合并&amp;lt;br/&amp;gt;父key拉下来&amp;lt;br/&amp;gt;可能连锁合并&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;B 树的树高增长只有一种方式：根节点分裂。&lt;/strong&gt; 这保证了它永远不会退化——这是比 BST 强在根上的地方。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 2：自增主键 vs UUID，B 树结构的真实代价&lt;/strong&gt;
早年在 MySQL 里给一张日志表用了 UUID 做主键，量小的时候没感觉，过了 5000 万行插入速度腰斩再腰斩。原因就在 B 树的插入路径：&lt;strong&gt;自增主键永远往最右的叶子追加&lt;/strong&gt;，页满了裂一次，顺序 I/O；&lt;strong&gt;UUID 是随机落到任意叶子&lt;/strong&gt;，每插一条都可能读一个冷页、裂一个热页，还伴随着页分裂后的空间碎片（页利用率掉到 60% 多）。换回自增 BIGINT 后写入吞吐翻了近 3 倍。这不是玄学，就是 B 树分裂频率的差。&lt;strong&gt;这也是为什么分布式 ID 方案（雪花 ID）都强调&amp;quot;趋势递增&amp;quot;&lt;/strong&gt;——在讨好 B+ 树。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;6-b-树补上最后一刀数据库的终点站&#34;&gt;6. B+ 树：补上最后一刀，数据库的终点站&lt;/h2&gt;
&lt;p&gt;B 树已经很好了，B+ 树只改了两处，却是数据库索引的定论：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph B树[&amp;#34;B 树：key 和数据混在一起&amp;#34;]
        BR[&amp;#34;内节点&amp;lt;br/&amp;gt;[key+data | key+data]&amp;#34;]
        BR --&amp;gt; BL1[&amp;#34;叶节点&amp;lt;br/&amp;gt;key+data&amp;#34;]
        BR --&amp;gt; BL2[&amp;#34;叶节点&amp;lt;br/&amp;gt;key+data&amp;#34;]
        BN1[&amp;#34;一个 key 的数据&amp;lt;br/&amp;gt;可能在内节点&amp;lt;br/&amp;gt;范围查询要跨层&amp;lt;br/&amp;gt;中序遍历上蹿下跳&amp;#34;]
    end
    subgraph BPLUS树[&amp;#34;B+ 树：内节点纯索引，数据全在叶子&amp;#34;]
        PR[&amp;#34;内节点&amp;lt;br/&amp;gt;[key | key | key]&amp;lt;br/&amp;gt;只当路标 不放数据&amp;#34;]
        PR --&amp;gt; PL1[&amp;#34;叶节点&amp;lt;br/&amp;gt;key+data&amp;#34;]
        PR --&amp;gt; PL2[&amp;#34;叶节点&amp;lt;br/&amp;gt;key+data&amp;#34;]
        PR --&amp;gt; PL3[&amp;#34;叶节点&amp;lt;br/&amp;gt;key+data&amp;#34;]
        PL1 --&amp;gt;|&amp;#34;双向链表&amp;#34;| PL2
        PL2 --&amp;gt;|&amp;#34;双向链表&amp;#34;| PL3
        PN1[&amp;#34;范围查询：定位起点后&amp;lt;br/&amp;gt;顺着链表扫&amp;lt;br/&amp;gt;全是顺序 I/O&amp;#34;]
    end
    style BPLUS树 fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两处改动，每一处都打在磁盘场景的七寸上：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;改动一：内节点不存数据，只存 key。&lt;/strong&gt; 同样 16KB 一页，存&amp;quot;key+数据指针&amp;quot;可能只能放 100 个，纯 key 能放 1000 个。阶数 ×10，树再矮一层。而且内节点少且热，&lt;strong&gt;整个上层索引经常能全塞进内存&lt;/strong&gt;，真实查询只有最后一步读叶子才碰磁盘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;改动二：叶子之间串成双向链表。&lt;/strong&gt; &lt;code&gt;WHERE age BETWEEN 20 AND 30&lt;/code&gt;、&lt;code&gt;ORDER BY ... LIMIT&lt;/code&gt; 这类范围扫描，B 树要在层间来回跳（随机 I/O），B+ 树定位到起点后顺着链表扫，&lt;strong&gt;顺序 I/O&lt;/strong&gt;。机械盘时代这是百倍差距，SSD 时代预读和缓存依然偏爱顺序访问。&lt;/p&gt;
&lt;p&gt;再附赠两个由此推出的性质，都是 MySQL 篇里出现过的实战规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;聚簇索引下，叶子就是数据行本身&lt;/strong&gt;，所以按主键查 = 一次定位，而二级索引叶子存的是主键值，要回表再查一次——这就是&amp;quot;覆盖索引为什么快&amp;quot;；&lt;/li&gt;
&lt;li&gt;B+ 树的有序性 + 左前缀路标结构，决定了 &lt;code&gt;LIKE &#39;%abc&#39;&lt;/code&gt; 没法走索引（开头通配，路标失效），&lt;code&gt;LIKE &#39;abc%&#39;&lt;/code&gt; 可以——MySQL 篇那句&amp;quot;会全表扫描&amp;quot;的底层解释就在这。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话记住 B 树 vs B+ 树&lt;/strong&gt;：B 树是&amp;quot;查到什么都是赚&amp;quot;（内节点也可能命中数据），B+ 树是&amp;quot;每次都老实走到叶子，但换来更矮的树和丝滑的范围扫描&amp;quot;。数据库查询里范围扫描是家常便饭，所以 B+ 树赢了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;7-顺手补两个教材盲点&#34;&gt;7. 顺手补两个教材盲点&lt;/h2&gt;
&lt;h3 id=&#34;71-哈希冲突链地址-vs-开放寻址&#34;&gt;7.1 哈希冲突：链地址 vs 开放寻址&lt;/h3&gt;
&lt;p&gt;刷题篇讲过哈希表&amp;quot;用空间换时间&amp;quot;，教材会多问一步：两个 key 撞进同一个桶怎么办？&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 链地址[&amp;#34;链地址法（Java HashMap）&amp;#34;]
        A1[&amp;#34;桶 0 → k1 → k2 → k3&amp;lt;br/&amp;gt;（Java 8 后链表超 8 个转红黑树）&amp;#34;]
    end
    subgraph 开放寻址[&amp;#34;开放寻址法（Redis dict 扩容期 / Python dict）&amp;#34;]
        B1[&amp;#34;桶被占？&amp;lt;br/&amp;gt;按探查序列找下一个空位&amp;lt;br/&amp;gt;线性 +1 / 二次 +i² / 双重哈希&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;取舍：&lt;strong&gt;链地址实现简单、删除容易，但链表指针追逐对 CPU 缓存不友好；开放寻址数据紧凑缓存友好，但负载因子一高（&amp;gt;0.7）探查序列雪崩，且删除要打墓碑标记&lt;/strong&gt;。Java 选前者，Python 选后者，没有谁对谁错，又是&amp;quot;看场景&amp;quot;。&lt;/p&gt;
&lt;h3 id=&#34;72-图的两个经典算法家族&#34;&gt;7.2 图的两个经典算法家族&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最短路径&lt;/strong&gt;：单源无负权用 &lt;strong&gt;Dijkstra&lt;/strong&gt;（贪心 + 优先队列，O(E log V)）；有负权边用 &lt;strong&gt;Bellman-Ford&lt;/strong&gt;（松弛 V-1 轮，顺带能检测负环）；全源用 &lt;strong&gt;Floyd&lt;/strong&gt;（三层循环的 DP，O(V³)，图小图稠才用）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小生成树&lt;/strong&gt;：&lt;strong&gt;Kruskal&lt;/strong&gt; 按边排序 + 并查集判环（适合稀疏图）；&lt;strong&gt;Prim&lt;/strong&gt; 从点出发贪心扩边（适合稠密图）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程里 Dijkstra 出场率最高——导航、网络路由（OSPF）都是它的变体。并查集在刷题篇讲过，这里正好闭环。&lt;/p&gt;
&lt;h2 id=&#34;8-把整条链放回工程世界&#34;&gt;8. 把整条链放回工程世界&lt;/h2&gt;
&lt;p&gt;学完整条进化链，回头看各个系统的选型，会发现全部是这条链上的位置决策：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    MEM[&amp;#34;纯内存 读写均衡&amp;lt;br/&amp;gt;Java TreeMap / epoll / CFS&amp;#34;] --&amp;gt;|红黑树| R1[&amp;#34;近似平衡 旋转便宜&amp;#34;]
    MEM2[&amp;#34;纯内存 读极多&amp;lt;br/&amp;gt;静态字典&amp;#34;] --&amp;gt;|AVL| R2[&amp;#34;严格平衡 查得最快&amp;#34;]
    DISK[&amp;#34;磁盘 点查为主&amp;lt;br/&amp;gt;MongoDB WiredTiger&amp;#34;] --&amp;gt;|B 树| R3[&amp;#34;页对齐 矮胖&amp;lt;br/&amp;gt;内节点也能命中数据&amp;#34;]
    DISK2[&amp;#34;磁盘 点查+海量范围扫描&amp;lt;br/&amp;gt;MySQL InnoDB / 绝大多数关系库&amp;#34;] --&amp;gt;|B+ 树| R4[&amp;#34;叶子链表&amp;lt;br/&amp;gt;范围扫描顺序 I/O&amp;#34;]
    LOG[&amp;#34;写极多 追加型&amp;lt;br/&amp;gt;LevelDB / RocksDB / HBase&amp;#34;] --&amp;gt;|LSM 树| R5[&amp;#34;干脆放弃原地更新&amp;lt;br/&amp;gt;写全部顺序化&amp;#34;]
    style DISK2 fill:#ffd699&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;最后一行 LSM 树是题外话但值得一提：当写压力大到 B+ 树的页分裂和随机写都扛不住时，LevelDB/RocksDB 们干脆换了世界观——&lt;strong&gt;不原地改，只追加&lt;/strong&gt;，把随机写变成顺序写，读的时候多层合并。这是&amp;quot;B 树家族应付不了的场景长出了新物种&amp;quot;的又一例，思路和本篇主线一脉相承：&lt;strong&gt;没有最好的结构，只有最匹配 I/O 模式的结构。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;思考与感悟&#34;&gt;思考与感悟&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 数据结构课的隐藏主线是&amp;quot;权衡&amp;quot;，不是&amp;quot;结构&amp;quot;。&lt;/strong&gt; 教材目录按结构分章，但真正的线索是：每个结构都在某个维度上多要了一点、在另一个维度上让了一步。AVL 要了查询速度让了写入成本，红黑树反过来；B+ 树要了范围扫描让了内节点命中。考试考结构，工作考权衡。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. &amp;ldquo;为什么&amp;quot;比&amp;quot;怎么做&amp;quot;值钱。&lt;/strong&gt; 红黑树的旋转规则我忘光了，但&amp;quot;为什么不能两个红挨着 = 2-3 树节点容量&amp;quot;这个理解我记了十年。背规则应付考试，理解动机应付工作——工作中没人让你手写 B 树分裂，但天天有人问&amp;quot;这个索引为什么这么建&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 磁盘那节课值整门课的一半。&lt;/strong&gt; 很多人学完数据结构，认知还停留在&amp;quot;比较次数&amp;quot;的世界。B 树这一课的真正教学目的，是把你的复杂度单位从&amp;quot;操作次数&amp;quot;切换到&amp;quot;I/O 次数&amp;quot;。这个切换一旦完成，后面学数据库、学存储引擎、学缓存，全是同一件事的不同切面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 这条链还没完。&lt;/strong&gt; B+ 树不是终点——LSM 树在写密集场景抢了它的地盘，跳表在 Redis 里用更简单的实现达到了类似的效果，AI 时代的向量索引（HNSW）干脆用图结构解决相似度搜索。数据结构这门课永远在续写，但写法永远是一样的：&lt;strong&gt;一个新场景出现，旧结构哪里疼，新结构就长在哪里。&lt;/strong&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>算法与数据结构入门学习总结：从零看懂核心原理与 LeetCode 避坑技巧</title>
      <link>https://ftmao.com/blog/posts/algorithm-and-data-structures-basics/</link>
      <pubDate>Thu, 09 Aug 2018 11:04:39 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/algorithm-and-data-structures-basics/</guid>
      <description>&lt;h1 id=&#34;算法与数据结构入门学习总结&#34;&gt;算法与数据结构入门学习总结&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：刚开始刷题、想系统梳理基础的同学。&lt;br&gt;
参考文档：&lt;code&gt;D:\doc\算法\combined_document.md&lt;/code&gt;（《剑指 Offer（专项突破版）》题解合集）。&lt;br&gt;
阅读目标：理解核心原理、记住常用模板、少踩常见坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面为什么要先建立知识地图&#34;&gt;0. 写在前面：为什么要先建立“知识地图”&lt;/h2&gt;
&lt;p&gt;刷题最怕两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拿到题完全没思路&lt;/strong&gt;，只能硬写暴力，时间超了也不知道怎么优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大概知道方向，但细节错一堆&lt;/strong&gt;：数组越界、整数溢出、指针空指针、边界条件漏掉。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这篇博客想解决的就是这两个问题：&lt;strong&gt;先搭好知识框架，再集中火力避坑&lt;/strong&gt;。只要你知道“这道题可能考什么数据结构/算法思想”，就能快速定位解法。&lt;/p&gt;
&lt;h3 id=&#34;01-学习路线图&#34;&gt;0.1 学习路线图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[语言基础] --&amp;gt; B[复杂度分析]
    B --&amp;gt; C[线性结构&amp;lt;br/&amp;gt;数组/链表/栈/队列]
    C --&amp;gt; D[非线性结构&amp;lt;br/&amp;gt;树/堆/图]
    D --&amp;gt; E[高级结构&amp;lt;br/&amp;gt;哈希/并查集/Trie]
    E --&amp;gt; F[基础算法思想]
    F --&amp;gt; G[专项刷题]
    F --&amp;gt; H[面试/竞赛]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;建议学习顺序&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先学 &lt;strong&gt;复杂度分析&lt;/strong&gt;，它是评判代码好坏的尺子。&lt;/li&gt;
&lt;li&gt;再学 &lt;strong&gt;数组、链表、栈、队列&lt;/strong&gt;，它们是后续所有结构的“建筑材料”。&lt;/li&gt;
&lt;li&gt;进入 &lt;strong&gt;树、堆、图&lt;/strong&gt;，理解非线性结构的遍历方式。&lt;/li&gt;
&lt;li&gt;最后系统学习 &lt;strong&gt;算法思想&lt;/strong&gt;：双指针、滑动窗口、二分、前缀和、回溯、动态规划、BFS/DFS。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-复杂度衡量算法的尺子&#34;&gt;1. 复杂度：衡量算法的尺子&lt;/h2&gt;
&lt;h3 id=&#34;11-时间复杂度&#34;&gt;1.1 时间复杂度&lt;/h3&gt;
&lt;p&gt;时间复杂度描述的是&lt;strong&gt;算法运行时间随输入规模增长的趋势&lt;/strong&gt;，不是真正的秒数。&lt;/p&gt;
&lt;p&gt;常见复杂度（从小到大）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;复杂度&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;名称&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;典型场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;常数&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;哈希表查询、数组随机访问&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;对数&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二分查找、倍增&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;线性&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一次遍历&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;线性对数&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快排、归并排序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;平方&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;双重循环、朴素 DP&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(2ⁿ) / O(n!)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;指数/阶乘&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;回溯、全排列&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;估算口诀&lt;/strong&gt;：看循环层数 + 看每次循环内部有没有“折半”操作。&lt;/p&gt;
&lt;h3 id=&#34;12-空间复杂度&#34;&gt;1.2 空间复杂度&lt;/h3&gt;
&lt;p&gt;空间复杂度描述&lt;strong&gt;算法额外占用的内存&lt;/strong&gt;。注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入数组本身不计入额外空间。&lt;/li&gt;
&lt;li&gt;递归调用栈深度也要算空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[分析代码] --&amp;gt; B[有没有开辟新数组/哈希表?]
    A --&amp;gt; C[有没有递归?]
    B --&amp;gt; D[空间 = 数据结构大小]
    C --&amp;gt; E[空间 = 递归最大深度]
    D --&amp;gt; F[取最大值]
    E --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;2-数据结构全景图&#34;&gt;2. 数据结构全景图&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((数据结构与算法))
    线性结构
      数组
      链表
      栈
      队列
    非线性结构
      树
        二叉树
        二叉搜索树
      堆
      图
    集合与映射
      哈希表
      并查集
    字符串专属
      前缀树 Trie
    算法思想
      双指针/滑动窗口
      二分/前缀和
      递归/回溯
      动态规划
      贪心/分治&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;21-各数据结构的核心能力&#34;&gt;2.1 各数据结构的核心能力&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;数据结构&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;核心能力&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;最适合解决的问题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;随机访问 O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;按索引查询、前缀和、双指针&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;链表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;动态增删 O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;反转、合并、环检测&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;栈&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;后进先出 LIFO&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;括号匹配、表达式求值、单调栈&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;队列&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先进先出 FIFO&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;BFS、滑动窗口、单调队列&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;哈希表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快速查找 O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;去重、计数、两数之和&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;树/ BST&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分层、有序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;遍历、最近公共祖先、中序有序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;堆&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快速取最值 O(log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;TopK、合并 K 个有序数组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;图&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;描述多对多关系&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最短路径、拓扑排序、连通性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Trie&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前缀匹配&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;单词前缀、最大异或、自动补全&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-线性结构数组链表栈队列&#34;&gt;3. 线性结构：数组、链表、栈、队列&lt;/h2&gt;
&lt;h3 id=&#34;31-数组最朴素但最万能&#34;&gt;3.1 数组：最朴素但最万能&lt;/h3&gt;
&lt;p&gt;数组的特点是&lt;strong&gt;连续内存 + 随机访问&lt;/strong&gt;。数组题最常见的套路是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;双指针&lt;/strong&gt;：相向（两数之和）、同向（滑动窗口）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前缀和&lt;/strong&gt;：把“区间求和”变成“两个前缀和相减”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序后处理&lt;/strong&gt;：很多题先排序，复杂度就从 O(n²) 降到 O(n log n)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;双指针模板&#34;&gt;双指针模板&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[初始化 left, right] --&amp;gt; B{指针方向?}
    B --&amp;gt;|相向| C[left=0, right=n-1]
    B --&amp;gt;|同向| D[left=0, right=0]
    C --&amp;gt; E[while left &amp;lt; right]
    D --&amp;gt; F[for right in 0..n-1]
    E --&amp;gt; G[根据条件移动 left 或 right]
    F --&amp;gt; H[按需收缩 left 扩展窗口]
    G --&amp;gt; I[更新答案]
    H --&amp;gt; I&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;例题思路串讲&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;题目&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;核心技巧&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;关键记忆点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;排序数组中两数之和&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;相向双指针&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;和大了 &lt;code&gt;right--&lt;/code&gt;，和小了 &lt;code&gt;left++&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;三数之和&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;排序 + 双指针 + 去重&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;外层和内层都要 &lt;code&gt;while&lt;/code&gt; 跳过重复值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;和 ≥ target 的最短子数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;同向双指针/滑动窗口&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;右扩左缩，记录最小长度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;乘积 &amp;lt; K 的子数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;同向双指针&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;满足条件时新增 &lt;code&gt;right-left+1&lt;/code&gt; 个子数组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;和为 K 的子数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前缀和 + 哈希表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;map.put(0, 1)&lt;/code&gt; 必不可少&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0 和 1 个数相同的子数组&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0 变 -1 后转“和为 0”&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;map.put(0, -1)&lt;/code&gt; 处理从开头开始的子数组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二维子矩阵的和&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二维前缀和&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;sum[i+1][j+1] = sum[i][j+1] + rowSum&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;三数之和去重是新人重灾区&lt;/strong&gt;，一定要记住：排序后，如果当前值和上一个值相同，就跳过，否则会产生重复三元组。&lt;/p&gt;
&lt;h3 id=&#34;32-链表虚拟头节点--快慢指针&#34;&gt;3.2 链表：虚拟头节点 + 快慢指针&lt;/h3&gt;
&lt;p&gt;链表不像数组能随机访问，所以核心技巧是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;虚拟头节点（dummy）&lt;/strong&gt;：方便删除头节点，统一代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快慢指针&lt;/strong&gt;：找中点、找环、找倒数第 K 个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反转链表&lt;/strong&gt;：三指针 &lt;code&gt;pre / now / next&lt;/code&gt; 一遍完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 反转链表模板&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; ListNode &lt;span style=&#34;color:#a6e22e&#34;&gt;reverseList&lt;/span&gt;(ListNode head) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ListNode pre &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;, now &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; head;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (now &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ListNode next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; now.&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        now.&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pre;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        pre &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; now;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        now &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; next;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; pre;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;链表常见坑&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;忘记处理空链表或单节点。&lt;/li&gt;
&lt;li&gt;移动指针时把 &lt;code&gt;next&lt;/code&gt; 覆盖了，后面没法继续遍历。&lt;/li&gt;
&lt;li&gt;找中点时快慢指针初始化/终止条件写错（建议画图手推一次）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-栈后进先出--单调栈&#34;&gt;3.3 栈：后进先出 + 单调栈&lt;/h3&gt;
&lt;p&gt;栈的两个高频用法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;辅助计算&lt;/strong&gt;：括号匹配、后缀表达式、进制转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单调栈&lt;/strong&gt;：找“下一个更大/更小元素”、直方图最大矩形。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[遍历元素] --&amp;gt; B{栈顶与新元素关系?}
    B --&amp;gt;|满足单调性| C[压栈]
    B --&amp;gt;|破坏单调性| D[弹出栈顶并计算]
    D --&amp;gt; B
    C --&amp;gt; E[继续遍历]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-队列先进先出--bfs-利器&#34;&gt;3.4 队列：先进先出 + BFS 利器&lt;/h3&gt;
&lt;p&gt;队列最经典的应用是 &lt;strong&gt;BFS（广度优先搜索）&lt;/strong&gt;：一层一层扩展，天然适合“最短路径/最浅结果”。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[起点入队] --&amp;gt; B[while 队列非空]
    B --&amp;gt; C[队首出队]
    C --&amp;gt; D[扩展所有邻居]
    D --&amp;gt; E[未访问的邻居入队]
    E --&amp;gt; B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;4-非线性结构树堆图&#34;&gt;4. 非线性结构：树、堆、图&lt;/h2&gt;
&lt;h3 id=&#34;41-二叉树递归是王道&#34;&gt;4.1 二叉树：递归是王道&lt;/h3&gt;
&lt;p&gt;二叉树题目 90% 都能用递归解决，核心想清楚三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;当前节点要做什么？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归返回什么？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界条件是什么？&lt;/strong&gt;（通常是 &lt;code&gt;node == null&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[处理当前节点] --&amp;gt; B[递归左子树]
    A --&amp;gt; C[递归右子树]
    B --&amp;gt; D[合并左右结果]
    C --&amp;gt; D&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;二叉树遍历&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序：根 → 左 → 右&lt;/li&gt;
&lt;li&gt;中序：左 → 根 → 右（BST 中序有序）&lt;/li&gt;
&lt;li&gt;后序：左 → 右 → 根&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;二叉搜索树（BST）性质&lt;/strong&gt;：左子树所有节点 &amp;lt; 根 &amp;lt; 右子树所有节点。利用这个性质可以 O(h) 查找、插入、删除。&lt;/p&gt;
&lt;h3 id=&#34;42-堆优先队列--动态最值&#34;&gt;4.2 堆：优先队列 = 动态最值&lt;/h3&gt;
&lt;p&gt;堆通常用优先队列实现。核心能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入 O(log n)&lt;/li&gt;
&lt;li&gt;取最值 O(log n)&lt;/li&gt;
&lt;li&gt;看最值 O(1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TopK 问题&lt;/strong&gt;（找前 K 大/小）是堆最经典的面试题。思路：维护一个大小为 K 的小根堆，堆里保存目前最大的 K 个数。&lt;/p&gt;
&lt;h3 id=&#34;43-图bfs-vs-dfs&#34;&gt;4.3 图：BFS vs DFS&lt;/h3&gt;
&lt;p&gt;图是最灵活的数据结构，几乎所有图算法都基于 &lt;strong&gt;BFS&lt;/strong&gt; 或 &lt;strong&gt;DFS&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[图的遍历] --&amp;gt; B{要最短/分层?}
    B --&amp;gt;|是| C[BFS 队列]
    B --&amp;gt;|否| D[DFS 递归/栈]
    C --&amp;gt; E[visited 集合防重复]
    D --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;图的常见题型&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;问题&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;首选算法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;岛屿数量、连通分量&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;DFS / 并查集&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最短路径（无权图）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;BFS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;拓扑排序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Kahn 算法（入度）/ DFS 后序逆序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最小生成树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Kruskal / Prim&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;单源最短路（带权）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Dijkstra / Bellman-Ford&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-高级结构与技巧&#34;&gt;5. 高级结构与技巧&lt;/h2&gt;
&lt;h3 id=&#34;51-哈希表用空间换时间&#34;&gt;5.1 哈希表：用空间换时间&lt;/h3&gt;
&lt;p&gt;哈希表能把“查找”从 O(n) 降到 O(1)。刷题时常见的哈希用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计数&lt;/strong&gt;：统计字符/数字出现次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去重&lt;/strong&gt;：判断元素是否出现过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录位置&lt;/strong&gt;：LRU、两数之和、前缀和下标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;经典设计题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;RandomizedSet&lt;/strong&gt;：数组 + 哈希表，删除时把末尾元素 swap 到被删位置，保证 O(1)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LRU Cache&lt;/strong&gt;：哈希表 + 双向链表，get/put 都把节点移到尾部，超出容量时删头部。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;52-前缀树-trie&#34;&gt;5.2 前缀树 Trie&lt;/h3&gt;
&lt;p&gt;Trie 用于高效处理字符串前缀、自动补全、最大异或等问题。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[root] --&amp;gt; B[a]
    A --&amp;gt; C[b]
    B --&amp;gt; D[p]
    B --&amp;gt; E[p2]
    C --&amp;gt; F[e]
    D --&amp;gt; G[p&amp;lt;br/&amp;gt;isEnd=true]
    E --&amp;gt; H[p&amp;lt;br/&amp;gt;isEnd=true]
    F --&amp;gt; I[e&amp;lt;br/&amp;gt;isEnd=true]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个节点代表一个字符前缀；叶子或标记节点表示完整单词。&lt;/p&gt;
&lt;h3 id=&#34;53-并查集&#34;&gt;5.3 并查集&lt;/h3&gt;
&lt;p&gt;并查集解决“连通性”问题，三个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find(x)&lt;/code&gt;：找到 x 的根。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;union(x, y)&lt;/code&gt;：合并两个集合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connected(x, y)&lt;/code&gt;：判断是否同根。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配合 &lt;strong&gt;路径压缩&lt;/strong&gt; 和 &lt;strong&gt;按秩合并&lt;/strong&gt;，单次操作近似 O(α(n))，可以认为接近 O(1)。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-算法思想把题归类&#34;&gt;6. 算法思想：把题归类&lt;/h2&gt;
&lt;h3 id=&#34;61-二分查找不是只能查有序数组&#34;&gt;6.1 二分查找：不是只能查有序数组&lt;/h3&gt;
&lt;p&gt;二分查找的精髓是：&lt;strong&gt;找到一个可以判定“答案是否可行”的条件&lt;/strong&gt;，然后不断缩小答案区间。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[确定二分对象] --&amp;gt; B[写出 canmid 判定]
    B --&amp;gt; C[初始化 l, r]
    C --&amp;gt; D[while l &amp;lt; r]
    D --&amp;gt; E{canmid?}
    E --&amp;gt;|true| F[答案在左半区&amp;lt;br/&amp;gt;r = mid]
    E --&amp;gt;|false| G[答案在右半区&amp;lt;br/&amp;gt;l = mid + 1]
    F --&amp;gt; D
    G --&amp;gt; D
    D --&amp;gt; H[返回 l]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;二分查找避坑&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环条件写成 &lt;code&gt;l &amp;lt;= r&lt;/code&gt; 还是 &lt;code&gt;l &amp;lt; r&lt;/code&gt;，取决于你要不要包含右端点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mid = l + (r - l) / 2&lt;/code&gt; 比 &lt;code&gt;(l + r) / 2&lt;/code&gt; 更安全，防止整数溢出。&lt;/li&gt;
&lt;li&gt;左/右边界更新要对称：左闭右闭就 &lt;code&gt;l = mid + 1, r = mid - 1&lt;/code&gt;；左闭右开要相应调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-滑动窗口双指针的高级形态&#34;&gt;6.2 滑动窗口：双指针的高级形态&lt;/h3&gt;
&lt;p&gt;滑动窗口适合处理&lt;strong&gt;子数组/子串&lt;/strong&gt;问题，前提是“右指针右移时，窗口内某种性质单调变化”。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[初始化 left=0] --&amp;gt; B[right 从左到右遍历]
    B --&amp;gt; C[扩大窗口 更新状态]
    C --&amp;gt; D{窗口是否合法?}
    D --&amp;gt;|不合法| E[移动 left 收缩窗口]
    D --&amp;gt;|合法| F[更新答案]
    E --&amp;gt; F
    F --&amp;gt; B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;什么时候不能用滑动窗口？&lt;/strong&gt; 当数组不是单调（例如有负数）时，和/积不具备单调性，要改用前缀和 + 哈希表。&lt;/p&gt;
&lt;h3 id=&#34;63-前缀和区间查询的利器&#34;&gt;6.3 前缀和：区间查询的利器&lt;/h3&gt;
&lt;p&gt;前缀和把“区间和”问题变成“差分”：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;sum(i, j) = prefix[j+1] - prefix[i]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;二维前缀和公式：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;prefix[i][j] = matrix[i-1][j-1]
             + prefix[i-1][j] + prefix[i][j-1]
             - prefix[i-1][j-1]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;一维前缀和 + 哈希表&lt;/strong&gt; 可以把“和为 K 的子数组”从 O(n²) 降到 O(n)。&lt;/p&gt;
&lt;h3 id=&#34;64-递归与回溯暴力枚举的艺术&#34;&gt;6.4 递归与回溯：暴力枚举的艺术&lt;/h3&gt;
&lt;p&gt;回溯 = 递归 + 做选择 + 撤销选择。模板：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;backtrack&lt;/span&gt;(路径, 选择列表) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (满足结束条件) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(路径);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (选择 in 选择列表) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        做选择;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        backtrack(路径, 选择列表);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        撤销选择;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[开始 backtrack] --&amp;gt; B{剪枝/终止?}
    B --&amp;gt;|是| C[记录答案]
    B --&amp;gt;|否| D[做选择]
    D --&amp;gt; E[递归下一层]
    E --&amp;gt; F[撤销选择]
    F --&amp;gt; G[尝试下一个选择]
    G --&amp;gt; D
    C --&amp;gt; H[返回]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;回溯常见题型&lt;/strong&gt;：子集、组合、排列、N 皇后、括号生成、复原 IP。&lt;/p&gt;
&lt;h3 id=&#34;65-动态规划找状态转移&#34;&gt;6.5 动态规划：找状态转移&lt;/h3&gt;
&lt;p&gt;DP 是很多新人的噩梦。其实套路很固定：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;定义状态 dp[i] 或 dp[i][j]&amp;#34;] --&amp;gt; B[写状态转移方程]
    B --&amp;gt; C[确定初始状态]
    C --&amp;gt; D[确定遍历顺序]
    D --&amp;gt; E[空间优化可选]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;入门 DP 分类&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;类型&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;特征&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;例题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;线性 DP&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一维数组递推&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;爬楼梯、房屋偷盗、最长递增子序列&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;区间 DP&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;区间 [i,j] 状态&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最长回文子序列、最少回文分割&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;背包 DP&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;选或不选&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分割等和子集、最少硬币&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;路径 DP&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二维网格&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;不同路径、最小路径和&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;状态压缩 DP&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;用二进制表示集合&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;旅行商问题、棋盘覆盖&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;DP 避坑&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态定义不清晰就开始写，写到一半发现转移不了。&lt;/li&gt;
&lt;li&gt;遍历顺序搞反，导致用到还没计算出来的状态。&lt;/li&gt;
&lt;li&gt;忘记初始化 &lt;code&gt;dp[0]&lt;/code&gt; 或边界行/列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;66-贪心每次局部最优&#34;&gt;6.6 贪心：每次局部最优&lt;/h3&gt;
&lt;p&gt;贪心没有通用模板，关键是&lt;strong&gt;证明局部最优能推导出全局最优&lt;/strong&gt;。常见贪心场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;区间问题：按结束时间排序选最多不重叠区间。&lt;/li&gt;
&lt;li&gt;跳跃游戏：每次选能跳最远的位置。&lt;/li&gt;
&lt;li&gt;哈夫曼编码：每次合并最小的两个。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你不确定贪心对不对，建议先用 DP 或回溯验证小例子。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-位运算与整数技巧&#34;&gt;7. 位运算与整数技巧&lt;/h2&gt;
&lt;p&gt;参考文档第一章专门讲整数，里面有很多位运算技巧，面试题也常考。&lt;/p&gt;
&lt;h3 id=&#34;71-常用位运算技巧&#34;&gt;7.1 常用位运算技巧&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;技巧&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;含义&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;例题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;n &amp;amp; (n-1)&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;把 n 最右侧的 1 变成 0&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;统计二进制 1 的个数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;n &amp;amp; (-n)&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;只保留最右侧的 1&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;判断是否是 2 的幂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;a ^ a = 0&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;相同数异或为 0&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;只出现一次的数字&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;a ^ 0 = a&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;与 0 异或不变&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;两数交换&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;72-整数除法倍增法&#34;&gt;7.2 整数除法：倍增法&lt;/h3&gt;
&lt;p&gt;最朴素的除法是不断减，最坏 O(n)。倍增法的思想是：判断被除数能容纳除数的 1 倍、2 倍、4 倍……直到不能容纳，然后减去最大的倍数，重复这个过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复杂度&lt;/strong&gt;：O(log n)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避坑&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Integer.MIN_VALUE / -1&lt;/code&gt; 会溢出。&lt;/li&gt;
&lt;li&gt;为了处理符号，通常把两个数都转成负数计算，最后再加符号。&lt;/li&gt;
&lt;li&gt;累加时要判断是否超过 &lt;code&gt;0xc0000000&lt;/code&gt;（即最小负数的一半），防止溢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-状态压缩用-int-表示集合&#34;&gt;7.3 状态压缩：用 int 表示集合&lt;/h3&gt;
&lt;p&gt;当数据范围只有 26 个小写字母、或 20 个以内的元素时，可以用一个 int 的每一位表示“选/没选”。&lt;/p&gt;
&lt;p&gt;例如“单词长度的最大乘积”中，用 int 的低 26 位表示一个单词包含哪些字母，两个单词的位掩码 &lt;code&gt;&amp;amp;&lt;/code&gt; 为 0 就说明没有公共字母。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-排序算法回顾&#34;&gt;8. 排序算法回顾&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;排序&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;平均时间&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;空间&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;稳定性&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;特点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;冒泡/选择/插入&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n²)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;稳定（插入）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;简单，数据量小可用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快速排序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;不稳定&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;平均最快&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;归并排序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;稳定&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;链表排序、逆序对&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;堆排序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n log n)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(1)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;不稳定&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;TopK、优先队列&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;计数/桶/基数&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(n+k)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;O(k)&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;稳定&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;数据范围有限&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;实战建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组排序直接用语言内置函数（Java &lt;code&gt;Arrays.sort()&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;链表排序适合归并（自顶向下或自底向上）。&lt;/li&gt;
&lt;li&gt;找第 K 大可以用快速选择（平均 O(n)）或堆。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;9-leetcode-刷题闭坑指南重点&#34;&gt;9. LeetCode 刷题闭坑指南（重点）&lt;/h2&gt;
&lt;p&gt;这部分把参考文档里反复出现的“坑”集中整理出来，刷题前先看一遍，能少错很多。&lt;/p&gt;
&lt;h3 id=&#34;91-拿到题后先做什么&#34;&gt;9.1 拿到题后先做什么&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[拿到题目] --&amp;gt; B[读三遍题意]
    B --&amp;gt; C[列出所有边界条件]
    C --&amp;gt; D[手写 2-3 个小例子]
    D --&amp;gt; E[判断数据结构与算法]
    E --&amp;gt; F[写代码]
    F --&amp;gt; G[脑内跑一遍边界]
    G --&amp;gt; H[提交]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;92-整数与边界&#34;&gt;9.2 整数与边界&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;案例&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;整数溢出&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;Integer.MIN_VALUE / -1&lt;/code&gt; 返回 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;越界累加&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;除法倍增时判断 &lt;code&gt;value &amp;gt;= 0xc0000000&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;负数右移&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;负数算术右移最高位永远补 1&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;mid&lt;/code&gt; 溢出&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二分写 &lt;code&gt;mid = l + (r - l) / 2&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;93-数组与字符串&#34;&gt;9.3 数组与字符串&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;案例&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;下标越界&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;right = n&lt;/code&gt; 还是 &lt;code&gt;right = n-1&lt;/code&gt; 要统一&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;左/右边界更新&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;注意是 &lt;code&gt;++left&lt;/code&gt; 还是 &lt;code&gt;left++&lt;/code&gt;（三数之和去重用 &lt;code&gt;nums[++left]&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;前缀和初始化&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;和为 K 子数组必须 &lt;code&gt;map.put(0, 1)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;二维前缀和&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;通常多开一行一列避免边界判断&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;字符串变位词&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;用长度 26/58 的 int 数组模拟哈希表&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;94-链表&#34;&gt;9.4 链表&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解决方案&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;头节点可能被删&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;使用 dummy 节点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;空指针&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;访问 &lt;code&gt;node.next&lt;/code&gt; 前先判空&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;找中点/环&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快慢指针初始化和终止条件画图确认&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;反转丢失 next&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先用临时变量保存 &lt;code&gt;now.next&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;95-递归与回溯&#34;&gt;9.5 递归与回溯&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解决方案&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;终止条件漏写&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先写边界，再写递归体&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;共享状态被污染&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;回溯时记得“撤销选择”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;重复结果&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;排序后跳过相同元素，或用 &lt;code&gt;visited&lt;/code&gt; 数组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;栈溢出&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;深度太大时考虑改迭代 + 栈&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;96-动态规划&#34;&gt;9.6 动态规划&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解决方案&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;状态定义错误&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先用一句话描述“dp[i] 表示什么”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;遍历顺序错误&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;画依赖图，确保计算 dp[i] 时所需状态已算好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;边界没初始化&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;先填第 0 行/列，或 &lt;code&gt;dp[0]&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;空间可以优化&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;滚动数组、一维降维&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;97-二分查找&#34;&gt;9.7 二分查找&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;解决方案&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;循环条件写错&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;根据开闭区间选择 &lt;code&gt;l &amp;lt; r&lt;/code&gt; 或 &lt;code&gt;l &amp;lt;= r&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;边界更新不对称&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;l = mid + 1&lt;/code&gt; 对应 &lt;code&gt;r = mid&lt;/code&gt; 或 &lt;code&gt;r = mid - 1&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;死循环&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;左中点 &lt;code&gt;mid = l + (r-l)/2&lt;/code&gt; 与右中点 &lt;code&gt;mid = l + (r-l+1)/2&lt;/code&gt; 选清楚&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;10-一张图串联所有知识点&#34;&gt;10. 一张图串联所有知识点&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[算法题] --&amp;gt; B{看到什么关键词?}
    B --&amp;gt;|子数组/子串&amp;lt;br/&amp;gt;和/积/去重| C[双指针 / 滑动窗口]
    B --&amp;gt;|区间和/连续和| D[前缀和 + 哈希表]
    B --&amp;gt;|有序 / 最大化最小值| E[二分查找]
    B --&amp;gt;|所有组合/排列/子集| F[回溯]
    B --&amp;gt;|最优子结构| G[动态规划]
    B --&amp;gt;|最短路径/层序| H[BFS]
    B --&amp;gt;|连通/全遍历| I[DFS / 并查集]
    B --&amp;gt;|TopK / 动态最值| J[堆]
    B --&amp;gt;|前缀匹配| K[Trie]
    C --&amp;gt; L[写代码]
    D --&amp;gt; L
    E --&amp;gt; L
    F --&amp;gt; L
    G --&amp;gt; L
    H --&amp;gt; L
    I --&amp;gt; L
    J --&amp;gt; L
    K --&amp;gt; L&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;11-给新人的刷题路线建议&#34;&gt;11. 给新人的刷题路线建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第 1-2 周：数组 + 链表&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;两数之和、三数之和、反转链表、合并两个有序链表、环形链表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 3-4 周：栈 + 队列 + 哈希表&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;有效的括号、最小栈、LRU 缓存、字母异位词。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 5-6 周：树 + 递归&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;二叉树前中后序遍历、二叉树层序遍历、二叉搜索树验证、路径总和。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 7-8 周：回溯 + 动态规划&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;全排列、组合总和、爬楼梯、最长公共子序列、不同路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 9-10 周：图 + 高级结构&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;岛屿数量、课程表（拓扑排序）、数据流第 K 大、实现 Trie。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续：按标签二刷 + 总结错题本&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;刷题原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一遍不要死磕，20 分钟没思路直接看题解，重点理解“为什么这样想”。&lt;/li&gt;
&lt;li&gt;第二天再独立写一遍，一周后回来再写一遍。&lt;/li&gt;
&lt;li&gt;把每道题的“思路关键词”写进错题本，考前只看错题本。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;12-结语&#34;&gt;12. 结语&lt;/h2&gt;
&lt;p&gt;算法和数据结构不是背代码，而是建立**“看到题目 → 识别模式 → 套用模板 → 处理边界”**的条件反射。希望这篇总结能帮你把零散的知识点串成一张网，刷题时少踩坑、多 AC。&lt;/p&gt;
&lt;p&gt;最后送一句话：&lt;strong&gt;慢即是快&lt;/strong&gt;。把一类题真正搞懂，比囫囵吞枣刷一百道更有效。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;参考文档：`（《剑指 Offer（专项突破版）》题解合集）。&lt;br&gt;
本文已整理成博客，欢迎在留言区补充你踩过的坑。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    <item>
      <title>jphotoshop：用 Java Swing 手写一个 Photoshop——架构与算法全解析</title>
      <link>https://ftmao.com/blog/posts/jphotoshop-java-swing-photoshop-clone/</link>
      <pubDate>Fri, 18 May 2018 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/jphotoshop-java-swing-photoshop-clone/</guid>
      <description>&lt;h1 id=&#34;jphotoshop用-java-swing-手写一个-photoshop架构与算法全解析&#34;&gt;jphotoshop：用 Java Swing 手写一个 Photoshop——架构与算法全解析&lt;/h1&gt;
&lt;p&gt;把多年前的一个练手项目整理开源了：&lt;strong&gt;jphotoshop&lt;/strong&gt;，一个用纯 Java Swing 手写的 Photoshop 模拟器。不写不知道，写一个&amp;quot;简化版 PS&amp;quot;几乎把计算机图形学、数据结构和设计模式都过了一遍，这篇文章既是功能介绍，也是一次技术复盘。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开源地址：&lt;a href=&#34;https://gitee.com/soulnew/jphotoshop&#34;&gt;https://gitee.com/soulnew/jphotoshop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Java + Swing（Swing Application Framework / JSR-296），NetBeans Ant 工程&lt;/li&gt;
&lt;li&gt;规模：197 个 Java 文件，约 1.8 万行（自研约 1.4 万行），开发周期约半年&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一功能概览一个简化版-ps能做什么&#34;&gt;一、功能概览：一个&amp;quot;简化版 PS&amp;quot;能做什么&lt;/h2&gt;
&lt;p&gt;UI 形态上和老版 PS 一致：&lt;strong&gt;MDI 多文档界面&lt;/strong&gt;——每张图片一个内部编辑窗口，图层面板、导航缩放面板都是可拖动的浮动内窗。&lt;/p&gt;
&lt;h3 id=&#34;11-图层系统&#34;&gt;1.1 图层系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;多图层编辑：图像图层、&lt;strong&gt;文字图层&lt;/strong&gt;、浮动选区层（拖动选区内容时的临时层）；&lt;/li&gt;
&lt;li&gt;图层面板带 40×30 &lt;strong&gt;实时缩略图&lt;/strong&gt;，支持拖拽换序（基于 Swing TransferHandler 的 DnD）、复制粘贴为新图层；&lt;/li&gt;
&lt;li&gt;透明区域用经典的灰白棋盘格打底。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;12-选区工具7-个&#34;&gt;1.2 选区工具（7 个）&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;工具&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;魔棒&lt;/td&gt;
					&lt;td&gt;容差可调（默认 23），支持多连通区域一次选中&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;色彩范围&lt;/td&gt;
					&lt;td&gt;全图相似色选取&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多边形套索&lt;/td&gt;
					&lt;td&gt;逐点勾勒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;自由套索&lt;/td&gt;
					&lt;td&gt;鼠标轨迹勾勒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;磁性套索&lt;/td&gt;
					&lt;td&gt;局部窗口内吸附边缘（完成度不高）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;贝塞尔选区&lt;/td&gt;
					&lt;td&gt;双击闭合时自动做曲线拟合，生成平滑选区&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;移动工具&lt;/td&gt;
					&lt;td&gt;把选区内容提成浮动层，拖动实时合成，松手落回底层&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选区还支持：&lt;strong&gt;羽化&lt;/strong&gt;（可调半径）、反选、平移/缩放（AffineTransform）、以及标志性的&lt;strong&gt;蚂蚁线动画&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;13-滤镜24-个分-5-大类&#34;&gt;1.3 滤镜（24 个，分 5 大类）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模糊&lt;/strong&gt;：均值平滑、高斯模糊（3×3 高斯核 &lt;code&gt;{1,2,1;2,4,2;1,2,1}/16&lt;/code&gt;）、中值滤波；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锐化&lt;/strong&gt;：Sharpen / SharpenMore（拉普拉斯卷积核 &lt;code&gt;{0,-1,0;-1,5,-1;0,-1,0}&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;艺术&lt;/strong&gt;：曝光过度、冰晶、熔岩、暗调（平方衰减）、漫画等 8 种风格化效果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单&lt;/strong&gt;：反色、怀旧（sepia 矩阵 &lt;code&gt;0.393/0.769/0.189...&lt;/code&gt;）、灰度；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扭曲&lt;/strong&gt;：挤压、球面化、正/反旋转、正弦波浪、摩尔纹——扭曲类全部带&lt;strong&gt;多段滑块实时预览&lt;/strong&gt;，取消可还原。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;14-图像调整与文字&#34;&gt;1.4 图像调整与文字&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HSB 调整&lt;/strong&gt;：先把整图 RGB 转成 HSB 三维 float 数组，色相/饱和度/亮度三个滑块实时调节再转回 RGB；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比度&lt;/strong&gt;调整；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文字工具&lt;/strong&gt;：点击图片任意位置就地出现透明输入框 + 字体选择面板，完成后生成为独立的文字图层。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;15-文件与工程&#34;&gt;1.5 文件与工程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;打开 jpg / png / gif / bmp；&lt;/li&gt;
&lt;li&gt;自定义 &lt;strong&gt;&lt;code&gt;.jps&lt;/code&gt; 工程格式&lt;/strong&gt;（类似 PSD）：整个图层模型实现 &lt;code&gt;Serializable&lt;/code&gt;，用 &lt;code&gt;ObjectOutputStream&lt;/code&gt; 套 &lt;code&gt;GZIPOutputStream&lt;/code&gt; 序列化保存，可以随时回来继续编辑；&lt;/li&gt;
&lt;li&gt;复制 / 剪切 / 粘贴、导航缩放窗口、中文界面。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二架构总览&#34;&gt;二、架构总览&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[&amp;#34;JPhotoShop 入口&amp;lt;br/&amp;gt;EDT 中构建主窗口&amp;#34;] --&amp;gt; B[&amp;#34;JDesktopPane MDI 主框架&amp;#34;]
    B --&amp;gt; C[&amp;#34;EditFrame 编辑窗口&amp;lt;br/&amp;gt;每张图一个&amp;#34;]
    B --&amp;gt; D[&amp;#34;LayerControl 图层面板&amp;#34;]
    B --&amp;gt; E[&amp;#34;ViewSizeFrame 导航/缩放&amp;#34;]
    C --&amp;gt; F[&amp;#34;EditModel&amp;lt;br/&amp;gt;像素模型 + MemoryImageSource 渲染&amp;#34;]
    F --&amp;gt; G[&amp;#34;ImageModel&amp;lt;br/&amp;gt;LinkedList 图层栈&amp;#34;]
    G --&amp;gt; H[&amp;#34;ImagePaintLayer / TextPaintLayer / TempImageLayer&amp;#34;]
    H --&amp;gt; I[&amp;#34;LayerPlug 混合样式&amp;lt;br/&amp;gt;Alpha 合成&amp;#34;]
    C --&amp;gt; J[&amp;#34;ToolManager&amp;lt;br/&amp;gt;工具代理/状态切换&amp;#34;]
    J --&amp;gt; K[&amp;#34;Tool 抽象类&amp;lt;br/&amp;gt;魔棒/套索/贝塞尔/移动/文字...&amp;#34;]
    C --&amp;gt; L[&amp;#34;FilterManager&amp;lt;br/&amp;gt;properties + 反射装配&amp;#34;]
    L --&amp;gt; M[&amp;#34;PicFilter 滤镜即 Action&amp;#34;]
    M --&amp;gt; N[&amp;#34;Math 包&amp;lt;br/&amp;gt;CoverOp 卷积 / FeatherSide 羽化&amp;lt;br/&amp;gt;FindSide 轮廓追踪 / PointConnection 并查集&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;整体分四层：&lt;strong&gt;UI 层&lt;/strong&gt;（MDI 窗口与面板）、&lt;strong&gt;模型层&lt;/strong&gt;（图层栈与像素数据）、&lt;strong&gt;工具/滤镜层&lt;/strong&gt;（操作入口）、&lt;strong&gt;算法层&lt;/strong&gt;（&lt;code&gt;jphotoshop.Math&lt;/code&gt; 包）。工具和滤镜都通过 properties 配置 + 反射装配，下面详细说。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三技术亮点&#34;&gt;三、技术亮点&lt;/h2&gt;
&lt;h3 id=&#34;31-自绘像素管线int--memoryimagesource&#34;&gt;3.1 自绘像素管线：int[][] + MemoryImageSource&lt;/h3&gt;
&lt;p&gt;核心存储没有用 &lt;code&gt;BufferedImage&lt;/code&gt;，而是直接操作 &lt;strong&gt;&lt;code&gt;int[][]&lt;/code&gt; ARGB 二维数组&lt;/strong&gt;——所有算法（卷积、混合、羽化）都在裸数组上做，直观且没有额外封装开销。&lt;/p&gt;
&lt;p&gt;显示侧用了一个当年很典型的 Swing 高性能技巧：&lt;code&gt;MemoryImageSource&lt;/code&gt; + &lt;code&gt;setAnimated(true)&lt;/code&gt;，像素变化后调 &lt;code&gt;newPixels()&lt;/code&gt; &lt;strong&gt;增量刷新&lt;/strong&gt;屏幕图像，避免每次重绘都重建整个 Image 对象。编辑、拖选区、调滤镜滑块时的实时反馈都靠它撑着。&lt;/p&gt;
&lt;h3 id=&#34;32-链式图层合成缓存&#34;&gt;3.2 链式图层合成缓存&lt;/h3&gt;
&lt;p&gt;多图层合成是最频繁的操作，朴素实现是每次重绘把所有层从头合一遍。这里的做法是给每个图层挂一个&amp;quot;&lt;strong&gt;本层与下面所有层合成结果的缓存&lt;/strong&gt; &lt;code&gt;cacheData&lt;/code&gt;&amp;quot;，层与层之间用 &lt;code&gt;backLayer&lt;/code&gt; 引用连成链：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    BG[&amp;#34;背景层&amp;lt;br/&amp;gt;棋盘格&amp;#34;] --&amp;gt;|&amp;#34;合成&amp;#34;| L1[&amp;#34;图层1&amp;lt;br/&amp;gt;cacheData&amp;#34;]
    L1 --&amp;gt;|&amp;#34;合成&amp;#34;| L2[&amp;#34;图层2&amp;lt;br/&amp;gt;cacheData&amp;#34;]
    L2 --&amp;gt;|&amp;#34;合成&amp;#34;| L3[&amp;#34;图层3&amp;lt;br/&amp;gt;cacheData&amp;#34;]
    L3 --&amp;gt; OUT[&amp;#34;最终显示图像&amp;#34;]
    style L2 fill:#f96
    L2 -. &amp;#34;仅从此层向上重建&amp;#34; .-&amp;gt; L3&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;某一层像素变化时，&lt;strong&gt;只从该层向上依次 &lt;code&gt;reBuild&lt;/code&gt;&lt;/strong&gt;，下面的层缓存不动。层数多、图大时，这个收益非常明显。&lt;/p&gt;
&lt;h3 id=&#34;33-手写-alpha-混合&#34;&gt;3.3 手写 Alpha 混合&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AlphaLayerStyle&lt;/code&gt; 实现了逐像素的经典混合公式 &lt;code&gt;C = α·Cf + (1-α)·Cb&lt;/code&gt;，并且处理了&lt;strong&gt;任意偏移（含负坐标裁剪）&lt;/strong&gt;——浮动选区拖到画布边缘外也能正确合成。羽化后的选区掩码是 short 型 alpha 权重，抠图粘贴时按权重衰减，边缘不会出现生硬的锯齿。&lt;/p&gt;
&lt;h3 id=&#34;34-魔棒一条完整的算法链&#34;&gt;3.4 魔棒：一条完整的算法链&lt;/h3&gt;
&lt;p&gt;魔棒是整个项目里算法含量最高的功能，完整链路分三步：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[&amp;#34;用户点击图片某点&amp;#34;] --&amp;gt; B[&amp;#34;Flood Fill 泛洪填充&amp;lt;br/&amp;gt;8邻域 + 容差判断&amp;lt;br/&amp;gt;ΔR+ΔG+ΔB ＜ range&amp;#34;]
    B --&amp;gt; C[&amp;#34;PointConnection 并查集&amp;lt;br/&amp;gt;id 数组 + sz 数组按秩合并&amp;lt;br/&amp;gt;分离多个互不连通的区域&amp;#34;]
    C --&amp;gt; D[&amp;#34;FindSide 8方向轮廓追踪&amp;lt;br/&amp;gt;沿区域边界走一圈&amp;lt;br/&amp;gt;每3步抽稀路径点&amp;#34;]
    D --&amp;gt; E[&amp;#34;GeneralPath 选区路径&amp;#34;]
    E --&amp;gt; F[&amp;#34;蚂蚁线动画&amp;lt;br/&amp;gt;Timer 500ms 切换两条相位差3的虚线&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;泛洪填充用 &lt;code&gt;LinkedList&lt;/code&gt; 当队列，容差判断用曼哈顿距离（&lt;code&gt;|ΔR|+|ΔG|+|ΔB|&lt;/code&gt;），比欧氏距离省开方；&lt;/li&gt;
&lt;li&gt;一次点击可能选中多个互不连通的区域（比如图片里分散的几块蓝天），用&lt;strong&gt;并查集&lt;/strong&gt;把连通域分组，每个域独立生成轮廓；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轮廓追踪&lt;/strong&gt;把面状的 boolean 掩码转成线状的路径点列，这一步直接决定蚂蚁线的平滑程度；&lt;/li&gt;
&lt;li&gt;蚂蚁线动画是两条相位差为 3 的虚线 &lt;code&gt;BasicStroke&lt;/code&gt; 用 500ms 的 &lt;code&gt;Timer&lt;/code&gt; 交替绘制——朴素但效果完全对味。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;35-通用卷积引擎-coverop&#34;&gt;3.5 通用卷积引擎 CoverOp&lt;/h3&gt;
&lt;p&gt;所有卷积类滤镜（模糊、锐化背后是同一个引擎 &lt;code&gt;CoverOp&lt;/code&gt;，提供三种重载：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;全图卷积；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;掩码限定区域卷积&lt;/strong&gt;——滤镜只在选区内生效，这是 PS 的基本体验；&lt;/li&gt;
&lt;li&gt;short alpha 掩码卷积——配合羽化选区做渐变处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RGB 三通道分离卷积 + 边界 clamp，滤镜类只需要给一个卷积核就完事。新增一个模糊变体的成本是十几行代码。&lt;/p&gt;
&lt;h3 id=&#34;36-贝塞尔选区schneider-曲线拟合&#34;&gt;3.6 贝塞尔选区：Schneider 曲线拟合&lt;/h3&gt;
&lt;p&gt;自由手绘的选区轨迹是抖动的折线，直接当选区很难看。贝塞尔选区工具在双击闭合时调用 JHotDraw 的 &lt;code&gt;Bezier.fitCurve&lt;/code&gt;（Schneider 的经典拟合算法），把鼠标点列拟合成平滑的三次贝塞尔曲线段——这就是 PS 里&amp;quot;钢笔工具&amp;quot;的底层原理。&lt;/p&gt;
&lt;h3 id=&#34;37-声明式装配properties--反射的原始插件化&#34;&gt;3.7 声明式装配：properties + 反射的原始&amp;quot;插件化&amp;quot;&lt;/h3&gt;
&lt;p&gt;工具和滤镜都&lt;strong&gt;不写死在代码里&lt;/strong&gt;，而是从 properties 读类名、&lt;code&gt;Class.forName&lt;/code&gt; 反射实例化：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-properties&#34; data-lang=&#34;properties&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# resource/filters.properties&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;filters.Kinds&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;模糊,锐化,艺术效果,简单,扭曲&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;模糊&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;Smooth,GaussBlur,Median&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;加一个新滤镜 = 写一个类 + 改一行配置，菜单自动按分类分组生成。工具栏同理（&lt;code&gt;ToolManager.initTool&lt;/code&gt;）。项目里还专门有一个 &lt;code&gt;plug&lt;/code&gt; 包在探索真正的外部插件机制（虽然只是个雏形）。在 2007 年没有 OSGi、没有 IoC 框架依赖的纯 Swing 工程里，这是最务实的可扩展方案。&lt;/p&gt;
&lt;h3 id=&#34;38-工具架构proxy--state-的混合&#34;&gt;3.8 工具架构：Proxy + State 的混合&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Tool&lt;/code&gt; 抽象类把底层 MouseEvent 翻译成语义化回调（&lt;code&gt;click / pressed / dragged / release / rightClick / clickDouble / move&lt;/code&gt;），子类只关心语义。&lt;code&gt;ToolManager&lt;/code&gt; 本身也是 &lt;code&gt;Tool&lt;/code&gt; 的子类，作为&lt;strong&gt;代理&lt;/strong&gt;把事件转发给当前激活的工具——选中哪个工具，事件流就切到哪个状态机上。每个工具还可以自带 &lt;code&gt;getPlugPanel()&lt;/code&gt; 参数面板，选中时挂到选项栏上，和 PS 的交互完全一致。&lt;/p&gt;
&lt;h3 id=&#34;39-扭曲滤镜的预览-取消模式&#34;&gt;3.9 扭曲滤镜的&amp;quot;预览-取消&amp;quot;模式&lt;/h3&gt;
&lt;p&gt;扭曲类滤镜（球面、旋转、波浪……）都是坐标重映射：对每个目标像素做逆极坐标变换，找它在原图的取样点。为了支持滑块&lt;strong&gt;实时预览又能取消还原&lt;/strong&gt;，&lt;code&gt;ContortFilter&lt;/code&gt; 在打开对话框时先备份 &lt;code&gt;tempData&lt;/code&gt;，滑块每次变化都在备份基础上重算，点取消直接把备份拷回——一个廉价的 Memento 模式。配套的 &lt;code&gt;MultiSlider&lt;/code&gt; 多段滑块控件也是自绘的。&lt;/p&gt;
&lt;h3 id=&#34;310-滤镜即-action&#34;&gt;3.10 滤镜即 Action&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PicFilter extends AbstractAction&lt;/code&gt;，滤镜本身就是可以挂进菜单/工具栏的 Action：&lt;code&gt;actionPerformed&lt;/code&gt; 里自动取当前窗口、当前图层、当前选区掩码，还处理了&lt;strong&gt;选区坐标系到图层坐标系的换算&lt;/strong&gt;（&lt;code&gt;start -= layerStart&lt;/code&gt;），执行完触发 &lt;code&gt;rePaintEdit&lt;/code&gt; 刷新。框架把脏活都干了，滤镜作者只需要关注像素算法。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四诚实的复盘留下了什么遗憾&#34;&gt;四、诚实的复盘：留下了什么遗憾&lt;/h2&gt;
&lt;p&gt;开源的代码就是当年的样子，包括未完成的部分，如实列出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;撤销/重做是空架子&lt;/strong&gt;：&lt;code&gt;UndoRedoManager&lt;/code&gt; 的骨架搭好了，但 &lt;code&gt;propertyChange&lt;/code&gt; 三个分支全是空的——这是当时没啃下来的硬骨头，现在看应该用 Command 模式重做，而不是当初设想的 property 监听方案；&lt;/li&gt;
&lt;li&gt;历史面板、通道面板、外部插件机制都只有雏形；&lt;/li&gt;
&lt;li&gt;磁性套索完成度不高；&lt;/li&gt;
&lt;li&gt;若干小 bug，比如 &lt;code&gt;FindSide&lt;/code&gt; 里找最近点写成了找最远点（&lt;code&gt;if (temp &amp;gt; MinLength)&lt;/code&gt; 应为 &lt;code&gt;&amp;lt;&lt;/code&gt;）、&lt;code&gt;&amp;quot;undo&amp;quot;.endsWith(...)&lt;/code&gt; 应为 &lt;code&gt;startsWith&lt;/code&gt;、几处类名 typo（&lt;code&gt;WaveFiler&lt;/code&gt;、&lt;code&gt;getSize&lt;/code&gt; 拼写）——权当留给读者的&amp;quot;考古彩蛋&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五写在最后&#34;&gt;五、写在最后&lt;/h2&gt;
&lt;p&gt;写 jphotoshop 的半年收获远超预期：并查集、轮廓追踪、卷积、贝塞尔拟合这些算法不再是教科书上的名词，而是要亲手调到能跑的东西；合成缓存、声明式装配、Action 抽象这些设计，也都是被真实需求逼出来的。如果你也想扎实练一次&amp;quot;算法 + 工程&amp;quot;的综合功，手写一个图像编辑器是我能想到的最好题目之一。&lt;/p&gt;
&lt;p&gt;源码在 &lt;a href=&#34;https://gitee.com/soulnew/jphotoshop&#34;&gt;https://gitee.com/soulnew/jphotoshop&lt;/a&gt;，欢迎 Star、Fork，更欢迎把那个空着的 &lt;code&gt;UndoRedoManager&lt;/code&gt; 补上。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
