<?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/%E4%BB%A3%E7%A0%81%E8%B0%83%E4%BC%98/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 08 Aug 2023 10:40:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E4%BB%A3%E7%A0%81%E8%B0%83%E4%BC%98/index.xml" rel="self" type="application/rss+xml" /><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>
    
  </channel>
</rss>
