<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>大O on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%A4%A7o/</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/%E5%A4%A7o/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>
    
  </channel>
</rss>
