<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh_CN">
  <title>黎明の小站</title>
  <subtitle>この小さな場所に、生きてきた証をそっと積み上げていく。</subtitle>
  <link href="https://blog.yoake.dev/" rel="alternate" type="text/html"/>
  <link href="https://blog.yoake.dev/atom.xml" rel="self" type="application/atom+xml"/>
  <id>https://blog.yoake.dev/</id>
  <updated>2026-08-09T09:39:57.000Z</updated>
  <entry>
    <title>Restic 仓库迁移记</title>
    <link href="https://blog.yoake.dev/posts/restic-spaces-to-r2/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/restic-spaces-to-r2/</id>
    <published>2026-07-16T14:00:00.000Z</published>
    <updated>2026-07-17T04:00:00.000Z</updated>
    <summary>DigitalOcean 学生额度到期，把四台服务器的 Restic 仓库从 Spaces 搬到 Cloudflare R2，顺手瘦身 12 GB 并重写备份脚本。</summary>
    <content type="html"><![CDATA[<p>上一篇<a href="https://dawn114514.site/posts/restic-spaces-backup/">《"如果今天服务器炸了怎么办？"》</a>里，我把五台服务器的备份统一收敛到了 <code>Restic + DigitalOcean Spaces</code>。</p>
<p>本以为这套体系能安稳跑很久，结果不到两个月，DigitalOcean 一封邮件：</p>
<p><img src="https://img.picr2.online/img/2026/07/17/7d16e5c70daf6fbd3b5e9997312ed1f9.webp" alt="Digital Ocean Spaces 到期邮件" /></p>
<blockquote>
<p>GitHub Student Developer Pack 的 credits 在 <strong>2026 年 7 月 31 日</strong>后到期，未用余额不结转，账户转为标准计费。</p>
</blockquote>
<p>只要支付方式有效，Spaces <code>$5/月</code> 照跑，备份任务不会因为额度到期自动停。</p>
<p>真正的问题是 DigitalOcean 上那台业务机 <code>DigitalOcean-SGP1</code> 也得跟着退役，继续养着不划算。</p>
<p>于是两件事凑到了一起：</p>
<ul>
<li>把 new-api 加上一套数据库迁到 Oracle ARM</li>
<li>再把四台现役机器的 Restic 仓库从 Spaces 整体搬到 Cloudflare R2</li>
</ul>
<p>本次省流：</p>
<ul>
<li>迁移期间停写约数分钟换一次性切换</li>
<li>四个 R2 仓库合计从 14.9 GB 瘦到 2.5 GB</li>
<li>备份脚本重写一遍</li>
<li>顺带捡出一个漏了一年多的 Docker 卷</li>
</ul>
<hr />
<h2>先搬业务，再搬备份</h2>
<p>两条链路串着做：</p>
<ol>
<li>先把业务停写、导出最终源库、切 DNS、公网 200</li>
<li>再冻结备份仓库做原样复制</li>
</ol>
<p>否则两件事打架，复制到的就是过渡态的库。</p>
<h3>业务迁移流程</h3>
<pre><code>flowchart LR
  A1["1. 预备副本&lt;br/&gt;建目录与拉取 ARM 镜像&lt;br/&gt;复制配置与证书"] --&gt; A2["2. 准备切换&lt;br/&gt;TTL 降至 300s&lt;br/&gt;DNS 切到 Oracle IP"]
  A2 --&gt; A3["3. 业务停写&lt;br/&gt;停止源容器防数据分叉&lt;br/&gt;短暂停止新容器"]
  A3 --&gt; A4["4. 逻辑迁移&lt;br/&gt;最终源库导出与覆盖恢复&lt;br/&gt;启动新容器自动迁移表"]
  A4 --&gt; A5["5. 运行验证&lt;br/&gt;公网 HTTPS 200 测试&lt;br/&gt;旧机保留以防回滚"]
</code></pre>
<p>业务顺利切完并验证通过后，再启动备份端的迁移：</p>
<h3>备份迁移流程</h3>
<pre><code>flowchart LR
  B1["1. 瘦身准备&lt;br/&gt;剔除可重建缓存与静态副本&lt;br/&gt;prune 清理 Spaces 历史数据"] --&gt; B2["2. 冻结迁移&lt;br/&gt;暂停定时任务&lt;br/&gt;rclone 复制仓库对象"]
  B2 --&gt; B3["3. 配置切换&lt;br/&gt;桶级 rw 凭证授权&lt;br/&gt;修改环境变量配置"]
  B3 --&gt; B4["4. 校验与验证&lt;br/&gt;restic check 校验一致性&lt;br/&gt;抽样恢复数据测试"]
  B4 --&gt; B5["5. 解冻恢复&lt;br/&gt;重启定时任务&lt;br/&gt;确认新备份写入 R2"]
</code></pre>
<h2>先把业务搬走</h2>
<p>DigitalOcean-SGP1 上跑着 <code>new-api / cli-proxy / 3x-ui</code>，外加内部的 <code>PostgreSQL 15 / Redis</code>。</p>
<p>只搬真正在用的：new-api、PostgreSQL、Redis、cli-proxy。</p>
<p>3x-ui 不迁，域名证书也只处理实际用到的。</p>
<p>切换当天没追求零中断，接受数分钟停写换一致性。顺序大致是：</p>
<ol>
<li>Oracle ARM 先建好预热副本，证书从旧机原样复制</li>
<li>DNS TTL 降到 300s 后把 A 记录切到 Oracle IP</li>
<li>停 DigitalOcean 上的 new-api 和 cli-proxy 断掉写入</li>
<li>短暂停 Oracle 上的 new-api 防止分叉</li>
<li>比对两边数据库指纹决定覆盖方向——从已停写的 DigitalOcean 源库做最终导出</li>
<li>Oracle 恢复后启动容器，本地 <code>curl --resolve</code> 和公网 HTTPS 同时验证 200</li>
<li>旧机保留 48 到 72 小时作回滚点，不重启旧业务容器</li>
</ol>
<p>切换瞬间有可能已经有请求写到了 Oracle 新库。比对了两边数据库指纹：</p>
<ul>
<li>Oracle 多出来的 5 张表是新版容器启动时自动完成的结构迁移，无业务写入</li>
<li>DigitalOcean 源库比 Oracle 多 36 条日志和 3 条额度记录</li>
</ul>
<p>所以以已经停写的 DigitalOcean 源库做最终覆盖最安全，Oracle 启动时会重新补齐那 5 张结构表。</p>
<p>切换完的状态：</p>
<ul>
<li>new-api 主域和 www 子域 HTTPS 200、证书正常</li>
<li>cli-proxy 公网入口 HTTP 200</li>
<li>PostgreSQL 源库全部表和完整历史日志已恢复</li>
<li>Redis 重新创建，无需迁移</li>
</ul>
<p>业务稳了，备份这边才敢动手。</p>
<hr />
<h2>备份后端换成 R2</h2>
<p>Restic 可以直接用 R2 的 S3-compatible endpoint，迁移主要改了 endpoint、桶名和凭证，其余参数一行没动。</p>
<p>四个仓库迁完后合计只有 2.5 GB，落在账户级 10 GB 免费额度内（<a href="https://developers.cloudflare.com/r2/pricing/">R2 价格</a>）；恢复快照的出站流量也不收费，对我这套个人服务器足够。</p>
<p>R2 的免费额度大概是这样的：</p>
<ul>
<li>Standard 存储每月前 10 GB 免费</li>
<li>Class A 操作（写/列表）每月 100 万次</li>
<li>Class B 操作（读/HEAD）每月 1000 万次</li>
<li>公网出站流量免费</li>
</ul>
<p>超出后存储约 <code>$0.015/GB/月</code>。</p>
<p>现役仓库用 Standard——Restic 每天读写、每周 prune 会反复重写对象，Infrequent Access 的检索费和操作费这套访问模式不划算。</p>
<h3>拆成四个桶，每桶一组凭证</h3>
<p>之前在 Spaces 用的是"一个 Space + 五个前缀"。</p>
<p>这次直接拆成一台机器一个桶，每个桶配一组独立的 <code>Object Read &amp; Write</code> Token：</p>
<ul>
<li>restic-oracle-amd1</li>
<li>restic-oracle-amd2</li>
<li>restic-oracle-arm</li>
<li>restic-aliyun</li>
</ul>
<p>（实际桶名带了更完整的机型描述。）</p>
<p>DigitalOcean-SGP1 即将退役，那台的历史仓库不迁，归档桶也直接删掉。</p>
<p>每个桶统一：</p>
<ul>
<li>关闭 Public access 和 r2.dev</li>
<li>不配 Custom domain</li>
<li>Storage class 用 Standard</li>
<li>Bucket Lock 不开启（原因见后文）</li>
<li>Object expiration 不配</li>
<li>Lifecycle 默认靠 R2 自身对未完成分段上传的 7 天自动中止（迁移后我又加了一条 1 天的显式清理，见后文）</li>
</ul>
<p>env 切到这种形式（敏感值由 <code>/etc/restic/env</code> 管理，不入文章）：</p>
<pre><code>export AWS_ACCESS_KEY_ID="&lt;R2 Access Key ID&gt;"
export AWS_SECRET_ACCESS_KEY="&lt;R2 Secret&gt;"
export AWS_DEFAULT_REGION="auto"
export RESTIC_REPOSITORY="s3:https://&lt;ACCOUNT_ID&gt;.r2.cloudflarestorage.com/&lt;对应桶&gt;"
</code></pre>
<p><code>AWS_DEFAULT_REGION=auto</code> 是 R2 的正式区域值。</p>
<p>空值和 <code>us-east-1</code> 也会被映射到 <code>auto</code>，所以不是绝对硬性要求；但显式写上能把"我连的是 R2"直接表达在配置里，省得日后读 env 时再回头翻文档（<a href="https://developers.cloudflare.com/r2/api/s3/api/">R2 S3 兼容 API</a>）。</p>
<p>一台机器一个桶，是因为 R2 的长期 Token 只能做桶级授权，不能按前缀做同等程度的永久隔离。</p>
<p>共用一个桶、一组凭证的话，任意一台泄露，攻击者就能删光整桶。</p>
<p>每台一个桶、每桶一组独立 Token 后，单机泄露最多影响自己的仓库，爆炸半径小一个数量级。</p>
<h3>把历史快照原样搬过去</h3>
<p>用 <code>rclone copy</code> 整体复制仓库，历史快照也随之保留——Restic 仓库本质是 <code>data/</code>、<code>index/</code>、<code>snapshots/</code>、<code>keys/</code> 下互相关联的对象，整体复制过去就等于历史全在。</p>
<p>迁移期间的冻结流程：</p>
<pre><code>sequenceDiagram
    autonumber
    participant S as 四台服务器
    participant T as systemd timer
    participant R as Spaces 仓库
    participant B as R2 桶

    Note over S,T: 冻结备份
    S-&gt;&gt;R: 运行最后一轮 Spaces 备份
    S-&gt;&gt;T: 暂停定时任务
    activate T

    Note over S,B: 迁移与验证
    S-&gt;&gt;B: 运行 rclone copy 原样复制仓库
    S-&gt;&gt;B: 运行 rclone check 校验大小
    S-&gt;&gt;S: 修改本机环境变量配置
    S-&gt;&gt;B: 运行 restic 完整性校验

    Note over S,T: 解冻恢复
    S-&gt;&gt;T: 恢复定时任务
    deactivate T
</code></pre>
<p>四台都没装 rclone，而阿里云国内 IP 被官方下载站屏蔽，所以最后是先把官方静态二进制下到本机过 SHA256，再 SCP 推上去，避免不同发行版包源差异。</p>
<p>复制参数：</p>
<pre><code>rclone copy "do:${old_path}" "r2:${bucket}" \
  --config /etc/restic/rclone-migration.conf \
  --immutable \
  --fast-list \
  --transfers 8 \
  --checkers 16 \
  --stats 1m --stats-one-line
</code></pre>
<p>阿里云链路不稳时调到 <code>--transfers 4 --checkers 8</code>。</p>
<p>然后两边一致性比对：</p>
<pre><code>rclone size "do:${old_path}" --json
rclone size "r2:${bucket}" --json
rclone check "do:${old_path}" "r2:${bucket}" \
  --size-only --one-way
</code></pre>
<p><code>--size-only</code> 只比较路径和大小、不比较哈希；<code>--one-way</code> 还会忽略目标端多出来的对象（<a href="https://rclone.org/commands/rclone_check/">rclone check 文档</a>）。</p>
<p>所以四个仓库输出 <code>0 differences found</code> 只能说明"源端对象在目标端都存在、大小一致"，不是哈希级校验。</p>
<p>完整的证据链是后面继续跑的 <code>restic check --read-data-subset=1G</code> 和抽样恢复——这两个过了，才能说仓库搬过去了没坏。</p>
<p>迁移完：</p>
<ul>
<li><code>/etc/restic/rclone-migration.conf</code> 这种含双端凭证的临时文件必须物理删</li>
<li><code>/etc/restic/env</code> 改完后旧配置留一份 <code>env.pre-r2-20260716</code>，等 R2 稳定几天再删</li>
</ul>
<hr />
<h2>给仓库减肥</h2>
<p>复制前先看了一眼四个仓库的实际对象大小，差点被劝退：</p>
<ul>
<li>Oracle AMD1 约 286 MB</li>
<li>Oracle AMD2 约 562 MB</li>
<li>Oracle ARM 约 12.79 GB</li>
<li>Aliyun 约 1.26 GB</li>
</ul>
<p>合计约 14.9 GB，超出 10 GB 免费额度。</p>
<p>膨胀的来源不是当前数据，而是 Oracle ARM 一年多攒下来的历史快照。</p>
<p>按快照分组算了一下，四台"最新快照"实际存储量合计只有约 2.05 GB，剩下 12.85 GB 基本都是旧 pack、旧快照和可重建的内容。</p>
<p>仓库变大先别急着买更大的存储。先跑一次 <code>restic stats</code> 按快照分组看看，是"当前数据大"还是"历史保留太大"——前者要改备份范围，后者只要改保留策略，代价差很多。</p>
<h3>调保留窗口</h3>
<p>原本四台都是 <code>daily 7 / weekly 4 / monthly 6</code>，之前为了省空间临时缩到 <code>daily 3 / weekly 2 / monthly 1</code>。</p>
<p>但迁完 R2 实际只有 2 GB，离 10 GB 还有 8 GB 的余量，继续牺牲恢复窗口没意义——激进保留意味着两三周后才发现误删，可能已经没有合适的恢复点。</p>
<p>调回 <code>7/4/3</code>，等仓库真长到 8 GB 再说。</p>
<h3>砍掉可重建的内容</h3>
<p>AMD2 和 Aliyun 不再备份可从 Git 重新部署的 <code>/www/wwwroot/MyBlog</code>。</p>
<p>Oracle ARM 新增排除了：</p>
<ul>
<li>HTML 图片缓存（<code>/opt/qq-bot/data/html_imgs</code>）</li>
<li>emoji 缩略图 and Pixiv 爬虫插件的图片下载缓存</li>
</ul>
<p>仍然保留的是：</p>
<ul>
<li>qq-bot SQLite 一致性快照</li>
<li>new-api PostgreSQL 未压缩 SQL</li>
<li>qq-bot emoji 与 images</li>
<li>QQ 数据和会话</li>
<li>配置、插件、证书</li>
<li>Nginx、Compose、cli-proxy auths</li>
</ul>
<h3>prune 的代价</h3>
<p>改完保留窗口和排除路径后，要释放空间必须跑 <code>restic forget --prune</code>。</p>
<p>ARM 这一步特别刺激：先从 Spaces 下载约 3.73 GB 旧 pack，拆出仍在被引用的数据块，重新打包成新 pack，再上传。CPU、内存、网络三头吃紧。</p>
<p>我盯着进度看了快 25 分钟，中途 ARM 还因为负载过高出现 SSH 抖动。</p>
<p>给 prune 进程发 <code>USR1</code> 拿实时进度：</p>
<pre><code>kill -USR1 &lt;restic_pid&gt;
</code></pre>
<p>ARM 仓库从 12.79 GB 瘦到 1.33 GB。</p>
<table>
<thead>
<tr>
<th>服务器</th>
<th>优化前（Spaces）</th>
<th>优化后（R2）</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Oracle AMD1</td>
<td>285.65 MB</td>
<td>285.65 MB</td>
<td>无大变化</td>
</tr>
<tr>
<td>Oracle AMD2</td>
<td>562.08 MB</td>
<td>562.99 MB</td>
<td>无大变化</td>
</tr>
<tr>
<td>Oracle ARM</td>
<td>12.79 GB</td>
<td>1.33 GB</td>
<td>瘦身约 11.5 GB</td>
</tr>
<tr>
<td>Aliyun</td>
<td>1.26 GB</td>
<td>104.48 KB</td>
<td>剔除 MyBlog 备份</td>
</tr>
<tr>
<td><strong>合计</strong></td>
<td><strong>14.90 GB</strong></td>
<td><strong>2.03 GB</strong></td>
<td>远低于 10 GB 阈值</td>
</tr>
</tbody>
</table>
<p>后来补上 AMD1 那一年多的漏备（见后文），实际占用约 <strong>2.51 GB</strong>。</p>
<hr />
<h2>仪表盘多出的 11 GB</h2>
<p>迁移结束后，Restic 看到的仓库只有 1.33 GB，R2 仪表盘却显示 12.79 GB。</p>
<p>差额主要来自之前被中断的未完成 multipart upload（没专门跑 <code>ListMultipartUploads</code> 一一对照，只是从"差额 11.5 GB + 清后排回到 1.33 GB"推断的）。</p>
<p>R2 默认 7 天后会自动中止未完成的分段上传（<a href="https://developers.cloudflare.com/r2/objects/upload-objects/">文档</a>），我不想等，给四个桶配了"1 天后中止"的生命周期规则。</p>
<p>短期残留不可怕，折算一下费用：</p>
<ul>
<li>11 GB × 1 天 ÷ 30 天 = 0.36 GB-月，落在免费额度内</li>
<li>即便没有免费额度，<code>0.36 × $0.015 ≈ $0.0054</code>，合人民币不到 4 分钱</li>
</ul>
<p>可怕的是不清理，让残留长期占着额度可见性。</p>
<hr />
<h2>顺手把备份脚本重写一遍</h2>
<p>复制是一次性动作，长期跑的还是靠 systemd timer。</p>
<p>趁这次迁移把四台机器的 8 个脚本（4 daily + 4 weekly）拉下来整体看了一遍，顺手改了几处。</p>
<h3>分级并发</h3>
<p>四台机器差距很大：</p>
<ul>
<li>Oracle AMD1 和 AMD2 都是 1H1G</li>
<li>Aliyun 2H2G</li>
<li>Oracle ARM 4H24G</li>
</ul>
<p>Restic 默认会吃满所有 CPU 核并开 5 条 S3 连接，1H1G 上跑一次大 prune 很容易把宿主机打满甚至触发 OOM Killer。</p>
<p>所以分级设置：</p>
<ul>
<li>AMD1、AMD2、Aliyun 用 <code>GOMAXPROCS=1</code> 加 <code>s3.connections=2</code></li>
<li>ARM 用 <code>GOMAXPROCS=2</code> 加 <code>s3.connections=3</code></li>
</ul>
<p><code>GOMAXPROCS=1</code> 主要约束 Restic 用几个 Go 线程核心，让 CPU 占用降下来，内存峰值略压一点，但不是严格内存上限。</p>
<p>systemd 的 <code>Nice=10</code> + <code>IOSchedulingClass=best-effort</code> + <code>IOSchedulingPriority=7</code> 这套调度用来降对业务的 CPU 和 IO 争抢，让 prune 跑得"更礼貌"——CPU 和 IO 都能被压一压，但内存这条控制不了。</p>
<p>真要不 OOM，得加 <code>MemoryMax=</code> / <code>MemoryHigh=</code>，或者用 <code>s3.connections</code> 在源头把并发量压住。这次主要靠后者。</p>
<h3>Daily / Weekly 职责分层</h3>
<p>原本四个 daily 脚本都跑了 <code>restic backup</code>、<code>restic forget</code> 和 <code>restic check --no-cache</code>。</p>
<p>Weekly 已经在做 <code>forget --prune + check --read-data-subset=1G</code>，daily 再 check 就是重复请求 R2、抢锁、拉长每日窗口。</p>
<p>改成：</p>
<ul>
<li><strong>daily</strong>：只做 backup + <code>snapshots --latest 5</code> + 通知</li>
<li><strong>weekly</strong>：负责 <code>forget --prune + check --read-data-subset=1G + --cleanup-cache</code></li>
<li><strong>monthly</strong>：可选全量 <code>restic check --read-data</code></li>
</ul>
<h3>--exclude 外置</h3>
<p>原本每台机器的排除项是一长串 <code>--exclude</code> 塞在脚本里，像噪声。</p>
<p>抽到每台独立的 <code>/etc/restic/excludes</code>（权限 <code>600 root:root</code>），脚本里改用 <code>--exclude-file</code>。</p>
<p>改排除项不用动脚本，可读性好很多。</p>
<h3>阿里云的中继做成主备</h3>
<p>阿里云国内机不能直连 <code>api.telegram.org</code>，原本只有 Oracle AMD2 一个 SSH forced-command 中继，是单点。</p>
<p>改成先主后备：</p>
<ul>
<li>主通道走 AMD2 的 SSH 强制命令中继</li>
<li>失败后自动重试备用通道 AMD1</li>
</ul>
<p>两条中继脚本都走 <code>restrict,command=...</code> 的 authorized_keys 锁死。</p>
<p>中继断了不等于备份断了——脚本里中继失败只写 <code>WARN: Telegram SSH 通知中继失败</code>，Restic 快照照样保存成功。</p>
<p>但反过来也成立：通知没断只能说明"任务跑过"，不能说明"服务器还活着"。</p>
<p>timer 根本被 systemd 跳过的情况，中继没机会报。</p>
<p>真正的"机器死了"要靠外部心跳（Healthchecks / Better Stack / 自建 Cloudflare Worker ping），不要指望崩溃的服务器自己发失败通知。</p>
<hr />
<h2>几条差点抄进脚本的错误参数</h2>
<p>脚本改完并没有一次通过。</p>
<p>有几个参数我都已经写进 diff 了，Restic 一跑就报错；还有几条干脆就是我和帮我出方案的 AI 说得像那么回事，其实纯属想象。</p>
<h3><code>s3.retry-attempts</code> 不是 Restic 选项</h3>
<p>最初给的 diff 里有 <code>restic backup ... -o s3.retry-attempts=5</code>，实际跑下来直接因 <code>unknown/invalid option</code> 失败。</p>
<p>Restic 有很多有效 S3 选项（<code>s3.region</code>、<code>s3.bucket-lookup</code>、<code>s3.list-objects-v1</code>……），但就是没有 <code>s3.retry-attempts</code>。</p>
<p>网络重试由后端默认行为处理，真要调的是 <code>--stuck-request-timeout</code>，不是虚构一个重试次数（<a href="https://restic.readthedocs.io/en/stable/faq.html">Restic FAQ</a>）。</p>
<p>为了"看起来更稳"堆参数，最坏的情况是在备份窗口里让脚本因为 <code>unknown option</code> 直接退出，把"备份数据"变成"白跑一趟"。</p>
<p>每条 Restic 参数都要能对应到官方文档。</p>
<h3><code>restic check --no-cache</code> 改成 <code>restic check</code> 也没用</h3>
<p>简单把 <code>--no-cache</code> 删掉并不会让 check 复用长期缓存。</p>
<p>Restic 0.16 起 <code>check</code> 会自动为每次校验创建新的临时缓存，要真正复用现有缓存必须显式 <code>--with-cache</code>（<a href="https://restic.readthedocs.io/en/v0.16.0/045_working_with_repos.html">Restic 0.16 完整性检查说明</a>）。</p>
<p>最干净的做法是每日根本不跑 check，校验整个交给 Weekly。</p>
<h3><code>--compression auto</code> 是默认值</h3>
<p>仓库格式 v2 下 <code>auto</code> 本来就是默认，加它只是增加命令噪声（<a href="https://restic.readthedocs.io/en/stable/047_tuning_parameters.html">Restic 调优参数</a>）。</p>
<h3>还有一条描述错误</h3>
<p>最初的优化报告说"废弃 SSH 通知中继"时，描述成"AMD1/AMD2/ARM 三台机器都通过 AMD2 做 SSH 中继"。</p>
<p>实际只有 Aliyun 用中继，AMD1/AMD2/ARM 都是直接 <code>curl</code> Telegram。</p>
<p>这种描述错误如果不纠正，会让人把通知代理改成全局 <code>HTTPS_PROXY</code>，结果把 Restic 到 R2 的流量也送进 Telegram 代理，把备份自己搞挂。</p>
<p>只在 Telegram 的 <code>curl</code> 局部指定 <code>curl --proxy "$TELEGRAM_PROXY" ...</code>，Restic 命令完全不碰代理环境变量。</p>
<hr />
<h2>那个漏了一年的 Docker 卷</h2>
<p>脚本审查时翻出的最严重的问题。</p>
<p>Oracle AMD1 上三个 Docker named volume <strong>根本没进 Restic 快照</strong>，看样子漏了一年多。</p>
<p>问题出在脚本里这段反向判断：</p>
<pre><code>if [ -n "$mountpoint" ] &amp;&amp; [ ! -e "/var/lib/docker/volumes/$volume" ]; then
  add_path "$mountpoint"
fi
</code></pre>
<p>逻辑是"如果标准目录不存在，才加进备份"。</p>
<p>但这三个卷都位于标准 Docker 数据目录，条件<strong>永远不成立</strong>，全部被跳过：</p>
<ul>
<li>qq-bot-core_data 48 MB</li>
<li>qq-bot-core_config 956 MB</li>
<li>qq-bot-core_cache 4 KB</li>
</ul>
<p>它们都正被 <code>qq-bot-core</code> 容器挂着，而宿主机上的 <code>/home/ubuntu/qq-bot-core</code> 目录并不包含这些卷的数据。</p>
<p>约 1 GB 的 QQ 机器人账号状态和身份数据长期没有进入快照——炸机恢复等于从一年前的旧状态重新上线。</p>
<p>:::caution[反向判断最容易误伤正常路径]
<code>if condition; then add</code> 这种写法，condition 一旦写反，正常路径会被静默跳过，而备份脚本照样报"成功"。</p>
<p>审脚本看到"否定式判断 + 默认放过"，一定要重点核对：尤其要区分"容器挂着的数据"和"宿主机目录里的数据"是不是一回事。
:::</p>
<p>修正很直接：把条件改成"容器在挂这个卷且卷路径存在就纳入备份"，不再用反向判断。</p>
<p>修完后新快照在 R2 端只增量占用 332 MiB（Restic 去重 + ZSTD 压缩后），整个仓库约 617 MB，仍远低于阈值。</p>
<h3>容器启停一致性</h3>
<p>卷里有十几个 SQLite 文件。</p>
<p>直接备份运行中的 SQLite 不一定坏，但 WAL 模式下恢复不可靠——备份到一半可能正把 WAL 合并回主库，文件就是不一致的。</p>
<p>备份前短暂停止容器。trap 必须先安装，再执行 <code>docker stop</code>，否则中间会留一段"容器已经停止、但 trap 还没生效"的盲区：</p>
<pre><code>WAS_RUNNING=$(docker inspect -f '{{.State.Running}}' qq-bot-core 2&gt;/dev/null)

cleanup() {
  if [ "$WAS_RUNNING" = "true" ]; then
    docker start qq-bot-core &gt;/dev/null 2&gt;&amp;1 || true
  fi
}
trap cleanup EXIT INT TERM

# trap 已装好，再停容器
docker stop qq-bot-core &gt;/dev/null 2&gt;&amp;1

restic backup "${BACKUP_PATHS[@]}" ...
</code></pre>
<p><code>WAS_RUNNING</code> 记录执行前状态，避免把"原本就没跑的容器"误拉起。</p>
<p><code>trap ... EXIT INT TERM</code> 覆盖正常退出、异常退出、SIGINT、SIGTERM，但<strong>不包括 SIGKILL</strong>。</p>
<p>我只抽查了 <code>reactions.db</code>，<code>PRAGMA integrity_check</code> 返回 <code>ok</code>。</p>
<p>这不能证明卷里每个 SQLite 都验过，但容器在整个备份窗口里是停的，数据库都没在写；至少确认了这条恢复链路能走通。</p>
<p>:::warning[trap 救不回 SIGKILL 和掉电]
Shell 的 <code>trap</code> 捕获不到 <code>SIGKILL</code>（<code>kill -9</code>、部分 OOM Kill）、主机掉电、内核崩溃。只靠 trap 兜底不够：</p>
<ul>
<li><code>restart: unless-stopped</code> 处理的是 daemon 自己重启时的自动恢复，Docker 官方写明手动 <code>docker stop</code> 过的容器不会被该策略在 daemon 重启后重新拉起（<a href="https://docs.docker.com/engine/containers/start-containers-automatically/">Docker restart policy</a>）</li>
<li>第二层兜底更靠谱的是独立 watchdog 或 systemd 恢复单元</li>
<li>cleanup 函数要幂等，收到 INT/TERM 后明确退出，不要清理完继续往下跑
:::</li>
</ul>
<p>顺便一起收掉的小问题：</p>
<ul>
<li><code>/var/backups/db</code> 不再整个清空，只删明确的导出文件（<code>sub-bot.sqlite</code> / <code>qq-bot.sqlite</code> / <code>new-api-postgres.sql</code> 及对应 <code>.tmp</code>），保护恢复清单、成功时间戳等静态文件</li>
<li>数据库导出加了 <code>test -s</code> 非空校验，导出失败或空文件直接失败报警，不静默用旧副本"假成功"</li>
<li><code>/etc/restic/excludes</code> 纳入备份路径，恢复后脚本能跑，排除文件不能丢</li>
<li><code>flock</code> 冲突不再 <code>exit 0</code>，改成记录 + Telegram 报警"任务因锁冲突跳过"</li>
<li>Telegram 失败通知日志从最后 40 行截到 2500 字符，防止超 4096 字符上限被 Telegram 拒收</li>
<li>systemd <code>Description</code> 从 "DigitalOcean Spaces" 改为 "Cloudflare R2"，过时的显示信息容易误导排查</li>
</ul>
<hr />
<h2>Bucket Lock 留给冷备桶</h2>
<p>四个现役桶没有开启 Bucket Lock——<code>restic forget --prune</code> 需要删除旧对象并重打包，锁上以后每周维护会直接失败（<a href="https://developers.cloudflare.com/r2/buckets/bucket-locks/">Cloudflare Bucket Lock</a>、<a href="https://restic.readthedocs.io/en/stable/060_forget.html">Restic prune 原理</a>）。</p>
<p>真正缺的是一份"服务器自己删不掉的不可变副本"，属于 3-2-1-1-0 里的那个"1 不可变"。</p>
<p>日常现役服务器备份写入各自隔离的 R2 桶：</p>
<pre><code>flowchart LR
  subgraph ActiveServers["现役服务器"]
    S1["Oracle AMD1&lt;br/&gt;GOMAXPROCS=1&lt;br/&gt;连接数=2"]
    S2["Oracle AMD2&lt;br/&gt;GOMAXPROCS=1&lt;br/&gt;连接数=2"]
    S3["Oracle ARM&lt;br/&gt;GOMAXPROCS=2&lt;br/&gt;连接数=3"]
    S4["Aliyun&lt;br/&gt;GOMAXPROCS=1&lt;br/&gt;连接数=2"]
  end

  subgraph R2Active["R2 活跃桶"]
    B1["restic-oracle-amd1"]
    B2["restic-oracle-amd2"]
    B3["restic-oracle-arm"]
    B4["restic-aliyun"]
  end

  S1 --&gt; B1
  S2 --&gt; B2
  S3 --&gt; B3
  S4 --&gt; B4
</code></pre>
<p>而在安全防御层面，通过离线拉取与锁定保障数据安全：</p>
<pre><code>flowchart LR
  R2_Active["现役 R2 桶"] --&gt;|每月只读拉取| L["本地保护机"]
  L --&gt;|只增不删复制| CB["独立冷备桶"]
  CB --&gt;|校验通过后| LOCK["启用 Bucket Lock"]
</code></pre>
<p>思路是冷备桶放在独立 Cloudflare 账号或另一家对象存储，每月用 <code>rclone copy</code>（非 sync，不传播删除）从现役桶拉一份，按月份前缀命名如 <code>2026-07/</code>。</p>
<p>验证通过后再锁。</p>
<p>生产服务器不得持有冷备凭证，本地只持现役桶只读凭证加冷备写入凭证。</p>
<p>不过这部分目前还停留在设计阶段，这次真正上线的只有四个现役 R2 桶，等真做完第一次月度恢复再回来补。</p>
<hr />
<h2>这次踩到的坑</h2>
<p><strong><code>AWS_DEFAULT_REGION=auto</code> 显式写省心。</strong></p>
<p>R2 不分具体区域，空值和 <code>us-east-1</code> 也会被映射到 <code>auto</code>，不写不是绝对跑不起来。</p>
<p>但显式写上能避免某些 SDK 版本在缺省条件下走默认 <code>us-east-1</code>，排查起来很折腾。算好习惯而不是硬要求。</p>
<p><strong>中断的 rclone 会留下分段上传残留。</strong></p>
<p><code>rclone copy</code> 中途网络断开，已上传的分段不会在 R2 端即时清理，而是按 7 天策略自动中止。</p>
<p>它不会被 <code>rclone size</code> 算进可见体积，只在仪表盘上出现。</p>
<p>配"未完成分段上传 1 天后清理"的生命周期能把仪表盘提前调准。</p>
<p><strong>别给 <code>restic backup</code> 外面套重试。</strong></p>
<p>有些人会在 Restic 命令外包一层 <code>for i in 1 2 3; do restic backup; done</code>。</p>
<p>这对 backup 是危险的：第一次跑可能其实已经保存了快照，因为末尾某个无关错误返回非 0，重试一遍就可能产生两个重复快照。</p>
<p>Restic 后端本身就会对网络错误重试，外层包重试属于画蛇添足。</p>
<p><strong>systemd 环境没有 HOME。</strong></p>
<p>Restic 在 systemd 里跑报 <code>unable to locate cache directory</code>，因为 <code>$XDG_CACHE_HOME</code> 和 <code>$HOME</code> 都没定义。</p>
<p>解决方式是在脚本开头加上 <code>export HOME="${HOME:-/root}"</code> 和 <code>export XDG_CACHE_HOME="${XDG_CACHE_HOME:-/root/.cache}"</code>。</p>
<p><strong>阿里云不能直连官方下载站。</strong></p>
<p>rclone 官方下载站在国内 IP 被 403，没法在阿里云上直接 <code>curl</code> 下来装。</p>
<p>最后是把官方二进制下到本机过 SHA256，再 SCP 推上去。</p>
<p>这种"网络能不能直连"在迁移前必须先确认，别等跑到一半才发现卡在某台机器装不上工具。</p>
<hr />
<h2>还没结束</h2>
<p>第二天再打开 R2，四个仓库加起来只有 2.5 GB。</p>
<p>Daily timer 已经开始往新桶写快照，DigitalOcean 上的旧 new-api / cli-proxy 一直保持停止。</p>
<p>这次把几个被掩盖了很久的隐患一次性揪出来：</p>
<ul>
<li>一个反向判断坑了一年的 1 GB 漏备</li>
<li>每天重复跑 check 浪费 R2 请求的写法</li>
<li>激进保留换来虚假安全感</li>
<li><code>s3.retry-attempts</code> 这种不存在的参数差点抄进 diff</li>
<li>还有差点把 Bucket Lock 加到活跃仓库上</li>
</ul>
<p>:::important[这次真正记到的几条]</p>
<ol>
<li><strong>业务迁移和备份迁移分两步</strong>：先停写做最终一致性导出、切 DNS，再冻结备份仓库做原样复制，避免备份里有一份过时的库。</li>
<li><strong>R2 计费看 GB-月，短留的残留不贵，但不清理就贵</strong>：看到仪表盘异常先算一遍，再配生命周期规则，不要慌。</li>
<li><strong>Bucket Lock 锁的是冷备不是现役</strong>：活跃 Restic 仓库必须能删能重写，锁了它就锁死了 prune；真正的不可变副本要拿到独立账号的冷备桶里去做，验证后才锁。
:::</li>
</ol>
<p>冷备桶、独立账号和 Bucket Lock 还在待办列表里。</p>
<p>等真做完第一次月度恢复，我再回来补这一段。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="运维记录"/>
  </entry>
  <entry>
    <title>输入输出系统</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E7%B3%BB%E7%BB%9F/</id>
    <published>2026-06-30T06:34:59.000Z</published>
    <updated>2026-06-30T06:34:59.000Z</updated>
    <summary>整理计算机输入输出系统，详解I/O接口的功能与编址方式、程序查询方式、程序中断控制方式的完整处理流程、DMA方式的工作原理与内存争用策略以及通道方式。</summary>
    <content type="html"><![CDATA[<h1>第九章：输入输出系统</h1>
<p>本章整理计算机与外部世界进行数据交互的输入输出系统。I/O 系统是连接 CPU、主存与外部设备的桥梁，其设计目标是在尽可能少地占用 CPU 资源的前提下，高效完成外设与主存之间的数据传输。</p>
<p>本章考试比重约 15 分，重点集中在四种数据传输控制方式（程序查询、程序中断、DMA、通道）的工作流程和对比分析，以及 I/O 接口编址方式的区别。</p>
<hr />
<h2>9.1 输入输出设备与特性</h2>
<p>输入输出设备（简称 I/O 设备或外部设备）是计算机系统与外部世界交换信息的物理媒介。</p>
<ul>
<li><strong>输入设备</strong>：将外部信息转换为计算机内部能识别的电信号形式。典型设备：键盘、鼠标、扫描仪、摄像头。</li>
<li><strong>输出设备</strong>：将计算机内部的数据信息转换为人类或其他设备可接受的形式。典型设备：显示器、打印机、扬声器。</li>
<li><strong>输入输出两用设备</strong>：既能输入又能输出。典型设备：磁盘驱动器、触摸屏、网卡。</li>
</ul>
<p>外部设备的种类繁多，工作速度差异巨大（从几字节/秒的键盘到数 GB/s 的固态硬盘），信号格式各异。因此，外部设备不能直接连接到系统总线上与 CPU 通信，必须通过 <strong>I/O 接口</strong>进行适配和缓冲。</p>
<hr />
<h2>9.2 I/O 接口</h2>
<h3>9.2.1 I/O 接口的功能</h3>
<p>I/O 接口（又称 I/O 控制器、设备控制器）是位于系统总线与外部设备之间的硬件电路模块，主要承担以下功能：</p>
<ol>
<li><strong>数据缓冲</strong>：设置数据缓冲寄存器，解决 CPU 与外设之间的速度不匹配问题。CPU 将数据写入缓冲寄存器后即可继续其他工作，外设按自身速度从缓冲区取走数据。</li>
<li><strong>信号格式转换</strong>：将系统总线上的并行数字信号与外设所需的特定信号格式（模拟信号、串行信号等）之间进行转换。</li>
<li><strong>设备选择</strong>：当系统总线上挂接多个 I/O 接口时，CPU 通过地址总线发出设备地址，I/O 接口中的地址译码电路判断是否被选中。</li>
<li><strong>命令与状态管理</strong>：接收并解释 CPU 发出的控制命令（如启动、停止），同时向 CPU 报告外设的当前状态（如就绪、忙碌、错误）。</li>
</ol>
<h3>9.2.2 I/O 接口的结构</h3>
<p>一个典型的 I/O 接口通常包含以下核心寄存器，它们统称为 I/O 端口：</p>
<table>
<thead>
<tr>
<th>寄存器名称</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数据缓冲寄存器</strong></td>
<td>暂存 CPU 与外设之间传输的数据</td>
</tr>
<tr>
<td><strong>状态寄存器</strong></td>
<td>记录外设的当前工作状态（就绪、忙、错误等），供 CPU 读取查询</td>
</tr>
<tr>
<td><strong>控制寄存器</strong></td>
<td>存放 CPU 发出的控制命令（启动、停止、模式设置等）</td>
</tr>
</tbody>
</table>
<h3>9.2.3 I/O 接口的编址</h3>
<p>CPU 如何访问 I/O 端口中的寄存器，取决于 I/O 端口的编址方式：</p>
<p>:::important[两种 I/O 编址方式的对比（简答/选择常考）]</p>
<table>
<thead>
<tr>
<th>对比维度</th>
<th>统一编址（存储器映射）</th>
<th>独立编址（I/O 映射）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>地址空间</strong></td>
<td>I/O 端口与主存共用同一个地址空间</td>
<td>I/O 端口有独立的地址空间</td>
</tr>
<tr>
<td><strong>访问指令</strong></td>
<td>使用普通的访存指令（如 <code>lw</code>/<code>sw</code>）访问 I/O 端口</td>
<td>使用专用的 I/O 指令（如 <code>IN</code>/<code>OUT</code>）访问</td>
</tr>
<tr>
<td><strong>地址译码</strong></td>
<td>需要从统一的地址空间中划出一段分配给 I/O，主存可用地址减少</td>
<td>I/O 地址与主存地址互不冲突，主存地址空间不受影响</td>
</tr>
<tr>
<td><strong>编程灵活性</strong></td>
<td>灵活，可以用丰富的访存指令操作 I/O 端口</td>
<td>专用指令类型少，编程灵活性较低</td>
</tr>
<tr>
<td><strong>硬件区分</strong></td>
<td>通过地址范围区分是主存还是 I/O</td>
<td>通过专用的 I/O 控制信号线区分</td>
</tr>
<tr>
<td><strong>典型应用</strong></td>
<td>ARM、MIPS 等 RISC 架构</td>
<td>x86 架构（同时也支持统一编址）</td>
</tr>
</tbody>
</table>
<p>在独立编址体系中，CPU 不能用普通的访存指令访问 I/O 端口，必须使用专用的 I/O 指令（如 x86 的 <code>IN</code>/<code>OUT</code>）；而统一编址时，CPU 用访存指令就能访问 I/O 端口。
:::</p>
<h3>9.2.4 I/O 接口的软件（设备驱动程序）</h3>
<p>I/O 接口硬件必须与操作系统中的<strong>设备驱动程序</strong>配合才能工作。驱动程序是一段直接与硬件打交道的底层软件，负责向 I/O 接口的控制寄存器发送命令、配置工作模式，并处理硬件中断。</p>
<h3>9.2.5 I/O 接口的分类</h3>
<ul>
<li><strong>按数据传送方式分</strong>：并行接口（一次传送多位数据）和串行接口（逐位传送数据）。</li>
<li><strong>按功能选择灵活性分</strong>：可编程接口（通过软件设置工作模式）和不可编程接口（功能固定）。</li>
</ul>
<hr />
<h2>9.3 数据传输控制方式</h2>
<p>CPU 与外设之间的数据传输需要有一种控制机制来协调。按照 CPU 参与程度由高到低，共有四种主要的数据传输控制方式：</p>
<table>
<thead>
<tr>
<th>方式</th>
<th>CPU 参与程度</th>
<th>CPU 利用率*</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>程序查询方式</strong></td>
<td>最高（全程等待）</td>
<td>最低</td>
<td>低速、简单的外设</td>
</tr>
<tr>
<td><strong>程序中断方式</strong></td>
<td>较高（中断时介入）</td>
<td>较高</td>
<td>中低速外设</td>
</tr>
<tr>
<td><strong>DMA 方式</strong></td>
<td>较低（仅初始化和结束时介入）</td>
<td>高</td>
<td>高速块设备（磁盘等）</td>
</tr>
<tr>
<td><strong>通道方式</strong></td>
<td>最低（通道处理器自主执行 I/O 程序）</td>
<td>最高</td>
<td>大型主机的复杂 I/O 子系统</td>
</tr>
</tbody>
</table>
<p>*注：这里的“CPU 利用率”指 CPU 用于执行非 I/O 任务（即有用计算）的时间占比。</p>
<hr />
<h2>9.4 程序控制方式</h2>
<h3>9.4.1 简单设备程序查询流程</h3>
<p>程序查询方式（又称程序轮询方式）的基本思想：CPU 通过执行一段循环查询程序，反复读取 I/O 接口的状态寄存器，判断外设是否准备就绪。在外设就绪之前，CPU 一直处于"忙等"状态，无法执行其他任何程序。</p>
<pre><code>graph TD
    A[CPU 启动 I/O 设备] --&gt; B{读取状态寄存器&lt;br&gt;设备已就绪?}
    B -- 否 --&gt; B
    B -- 是 --&gt; C[进行数据传输]
    C --&gt; D[传输完毕, 继续执行后续程序]
    
    style B fill:#ffe0b2,stroke:#f57c00
</code></pre>
<h3>9.4.2 复杂设备程序查询流程</h3>
<p>当系统中有多个外设时，CPU 需要依次轮流查询各设备的状态。查询的先后顺序就是各设备的优先级。</p>
<h3>9.4.3 程序查询特点</h3>
<ul>
<li><strong>优点</strong>：硬件结构简单，不需要中断控制器和 DMA 控制器。</li>
<li><strong>缺点</strong>：CPU 在外设准备数据期间完全处于空转等待状态，CPU 利用率极低。外设速度越慢，CPU 浪费的时间越多。</li>
</ul>
<hr />
<h2>9.5 程序中断控制方式</h2>
<p>程序中断方式是为了克服程序查询方式中 CPU "忙等"问题而设计的。其核心思想：CPU 启动 I/O 操作后不再等待，而是继续执行当前程序。当外设完成数据准备后，主动向 CPU 发出中断请求信号，CPU 在当前指令执行完毕后响应中断，暂停当前程序，转入 I/O 中断服务程序完成数据传输，传输完成后返回原程序断点继续执行。</p>
<h3>9.5.1 中断的基本概念</h3>
<ul>
<li><strong>中断请求</strong>：外设通过 I/O 接口向 CPU 发出的"请求服务"信号。</li>
<li><strong>中断源</strong>：引发中断请求的设备或事件。</li>
<li><strong>中断响应</strong>：CPU 在满足条件时暂停当前程序，转去执行中断服务程序的动作。</li>
<li><strong>中断服务程序</strong>：为该中断源专门编写的处理程序，用于完成实际的数据传输和设备控制。</li>
<li><strong>中断返回</strong>：中断服务程序执行完毕后，CPU 恢复原程序断点状态，返回继续执行。</li>
</ul>
<h3>9.5.2 中断请求</h3>
<p>外设通过 I/O 接口中的中断请求触发器向 CPU 发出中断请求信号。多个设备的中断请求信号可以通过以下两种方式汇集到 CPU：</p>
<ul>
<li><strong>独立请求线方式</strong>：每个设备都有独立的中断请求线直连 CPU。CPU 可以直接识别是哪个设备发出的请求。硬件开销大但响应快。</li>
<li><strong>公共请求线方式</strong>：所有设备共用一条中断请求线，CPU 收到请求后还需要进一步识别具体的中断源。</li>
</ul>
<h3>9.5.3 中断响应</h3>
<p>CPU 响应中断必须同时满足以下条件：</p>
<ol>
<li>有设备发出了中断请求（中断请求触发器置位）。</li>
<li>CPU 处于<strong>开中断</strong>状态（中断允许标志 IF = 1）。</li>
<li>当前指令已经执行完毕（CPU 在每条指令的最后一个时钟周期末尾检测中断请求）。</li>
</ol>
<h3>9.5.4 中断识别</h3>
<p>当有多个中断源同时发出请求时，CPU 需要确定优先响应哪个中断源。</p>
<p><strong>软件查询法</strong>：CPU 通过程序依次查询各设备的中断请求状态位，第一个被查询到有请求的设备获得服务。查询顺序即为优先级顺序。</p>
<p><strong>硬件排队电路（中断向量法）</strong>：由专门的硬件优先级编码器确定最高优先级的中断源，并产生一个<strong>中断类型号</strong>。CPU 根据该类型号在<strong>中断向量表</strong>中查找对应中断服务程序的入口地址，直接跳转执行。</p>
<p>:::important[中断向量与中断向量表]</p>
<ul>
<li><strong>中断向量</strong>：中断服务程序的入口地址。</li>
<li><strong>中断向量表</strong>：存放在主存固定区域的一张地址表，每个表项存放一个中断向量（即一个中断服务程序的入口地址）。CPU 用中断类型号作为索引查表，快速定位到目标服务程序。
:::</li>
</ul>
<h3>9.5.5 中断处理</h3>
<p>:::important[中断处理全过程（高权重考点大题）]</p>
<p>一个完整的中断处理过程分为以下几个阶段：</p>
<p><strong>1. 中断响应阶段（硬件自动完成，称为"中断隐指令"）</strong></p>
<p>这一阶段由 CPU 硬件在中断响应周期内自动完成，不需要程序员编写指令：</p>
<ol>
<li><strong>关中断</strong>：将中断允许标志 IF 置 0，防止在保存断点过程中再被新的中断打断。</li>
<li><strong>保存断点</strong>：将当前程序计数器 PC 的值（断点地址）压入堆栈或保存到特定寄存器中。</li>
<li><strong>识别中断源并获取入口地址</strong>：通过中断向量表查找或硬件排队电路获取中断服务程序的入口地址，送入 PC。</li>
</ol>
<p><strong>2. 中断服务程序（软件编写）</strong></p>
<p>由程序员（通常是操作系统开发者）编写的中断处理代码：</p>
<ol>
<li><strong>保护现场</strong>：将中断服务程序中可能使用到的通用寄存器内容压入堆栈保存，防止原程序的寄存器值被覆盖。</li>
<li><strong>开中断</strong>（可选）：如果允许更高优先级的中断嵌套，此时重新开中断。</li>
<li><strong>执行中断服务</strong>：完成实际的 I/O 数据传输和设备控制操作。</li>
<li><strong>关中断</strong>（可选）：在恢复现场前关闭中断，防止恢复过程被打断。</li>
<li><strong>恢复现场</strong>：将之前压入堆栈的寄存器值弹出恢复。</li>
<li><strong>开中断并中断返回</strong>：恢复中断允许状态，执行中断返回指令，将断点地址从堆栈弹回 PC，CPU 返回原程序继续执行。</li>
</ol>
<pre><code>graph TD
    A[原程序执行] --&gt; B[当前指令结束]
    B --&gt; C{有未屏蔽的中断请求?}
    C -- 否 --&gt; A
    C -- 是 --&gt; D(中断隐指令: 硬件自动完成)
    
    subgraph 中断隐指令
    D --&gt; D1[1. 关中断]
    D1 --&gt; D2[2. 保存断点 PC]
    D2 --&gt; D3[3. 获中断服务程序入口]
    end
    
    D3 --&gt; E(中断服务程序: 软件执行)
    
    subgraph 中断服务程序
    E --&gt; E1[4. 保护现场]
    E1 --&gt; E2[5. 可选: 开中断允许嵌套]
    E2 --&gt; E3[6. 执行实际 I/O 传输]
    E3 --&gt; E4[7. 可选: 关中断]
    E4 --&gt; E5[8. 恢复现场]
    E5 --&gt; E6[9. 开中断]
    E6 --&gt; E7[10. 中断返回 IRET]
    end
    
    E7 --&gt; A
</code></pre>
<p>:::</p>
<p>:::tip[中断嵌套]
中断嵌套是指在执行某个中断服务程序的过程中，又响应了更高优先级的中断请求，暂时挂起当前中断服务，转去执行更高优先级的中断服务程序，完成后再返回继续处理原来的中断。只有在中断服务程序中执行了"开中断"操作后，才可能发生中断嵌套。
:::</p>
<h3>9.5.6 多重中断与中断屏蔽</h3>
<p>当系统中有多个中断源且允许中断嵌套时，需要通过<strong>中断优先级</strong>和<strong>中断屏蔽字</strong>来控制中断的响应顺序。</p>
<p><strong>中断屏蔽字</strong>：每个中断源对应一个屏蔽位。通过设置屏蔽字可以有选择地屏蔽某些中断请求。中断屏蔽字中，置 1 的位对应的中断源被屏蔽（即该中断请求被忽略），置 0 的位对应的中断源可以被响应。</p>
<p>:::important[中断屏蔽字的设置规则]
<strong>若按本文约定 $M_i = 1$ 表示屏蔽：</strong>
每个中断源的屏蔽字要确保<strong>屏蔽掉所有优先级不高于自己的中断源</strong>，只允许优先级更高的中断源打断自己。同时，每个中断源的屏蔽字中对应自己的那一位必须置 1（即在自己的服务程序执行期间屏蔽自身，防止自我嵌套）。
:::</p>
<hr />
<h2>9.6 DMA 方式</h2>
<h3>9.6.1 DMA 的基本概念</h3>
<p>DMA 是一种硬件控制的高速数据传输机制。在 DMA 方式下，外设与主存之间的数据传输不需要 CPU 逐字节地执行 I/O 指令来搬运数据，而是由专门的 <strong>DMA 控制器</strong>直接控制系统总线，自主完成外设与主存之间的批量数据传输。</p>
<p>CPU 仅在 DMA 传输的<strong>开始</strong>（初始化 DMA 控制器参数）和<strong>结束</strong>（接收 DMA 完成中断）时参与，整个数据传输过程中 CPU 可以继续执行其他程序，极大地提高了 CPU 利用率。</p>
<h3>9.6.2 内存争用问题</h3>
<p>DMA 控制器和 CPU 都需要通过系统总线访问主存。当两者同时需要访存时，就会产生总线争用冲突。常用的解决方法有：</p>
<p><strong>1. 停止 CPU 访存法</strong></p>
<p>当 DMA 控制器需要使用总线时，CPU 暂时放弃对总线的控制权，让 DMA 控制器独占总线进行数据传输。</p>
<ul>
<li>控制简单，但在 DMA 传输期间 CPU 完全无法访存，效率不高。</li>
</ul>
<p><strong>2. 周期挪用法（周期窃取法）</strong></p>
<p>当 DMA 控制器需要传输一个数据时，它"窃取"一个存储周期，在该周期内占用总线访存，之后立即归还总线给 CPU。</p>
<ul>
<li>CPU 只在个别存储周期被短暂暂停，对 CPU 执行效率的影响很小。</li>
<li>这是 DMA 方式中最常用的总线争用策略。</li>
</ul>
<p><strong>3. 交替访存法</strong></p>
<p>将一个存储周期一分为二：前半个周期分配给 CPU 使用，后半个周期分配给 DMA 控制器使用。两者交替访存，互不冲突。</p>
<ul>
<li>DMA 传输与 CPU 访存在时间上完美交错，CPU 没有任何等待，两者并发执行。</li>
<li>但要求存储器的存取速度必须足够快（存取周期至少是正常的一半），硬件设计要求极高，仅适用于高速系统。</li>
</ul>
<h3>9.6.3 DMA 控制器</h3>
<p>DMA 控制器的内部通常包含以下关键寄存器：</p>
<table>
<thead>
<tr>
<th>寄存器/部件</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>主存地址寄存器（AR）</strong></td>
<td>存放当前 DMA 传输在主存中的目标/源地址，每传输一个数据自动加 1</td>
</tr>
<tr>
<td><strong>设备地址寄存器（DAR）</strong></td>
<td>存放挂接在 DMA 控制器上的 I/O 设备内部端口地址</td>
</tr>
<tr>
<td><strong>字计数器（WC）</strong></td>
<td>存放需要传输的数据总字/字节数，每传输一个数据自动减 1，减到 0 时向 CPU 发出传输完成中断</td>
</tr>
<tr>
<td><strong>数据缓冲寄存器（DR）</strong></td>
<td>暂存正在传输的数据</td>
</tr>
<tr>
<td><strong>命令/状态寄存器（CSR）</strong></td>
<td>存放 CPU 下达的控制命令（如读/写方向）以及 DMA 的当前运行状态</td>
</tr>
</tbody>
</table>
<h3>9.6.4 DMA 传输流程</h3>
<p>:::important[DMA 数据传输的完整流程]</p>
<p>DMA 传输过程分为三个阶段：</p>
<p><strong>1. 预处理阶段（CPU 完成）</strong></p>
<p>CPU 通过程序向 DMA 控制器写入以下初始化参数：</p>
<ul>
<li>主存起始地址（传输数据在主存中的存放位置）。</li>
<li>传输数据的字节数或字数。</li>
<li>传输方向（主存到外设，还是外设到主存）。</li>
<li>发出启动 DMA 传输的命令。</li>
</ul>
<p><strong>2. 数据传输阶段（DMA 控制器自主完成）</strong></p>
<p>DMA 控制器接管总线控制权后，自动完成数据的逐字传输：</p>
<ol>
<li>DMA 控制器将主存地址寄存器的内容送上地址总线。</li>
<li>向主存发出读/写控制信号。</li>
<li>完成一个字的传输后，主存地址寄存器自动加 1，字计数器自动减 1。</li>
<li>判断字计数器是否为 0：若不为 0 则继续传输下一个字；若为 0 则传输完毕。</li>
</ol>
<p><strong>3. 后处理阶段（CPU 完成）</strong></p>
<p>DMA 传输完毕后，DMA 控制器向 CPU 发出中断请求。CPU 响应中断后执行中断服务程序，进行后续处理（如校验传输是否正确、释放缓冲区、启动下一次 DMA 传输等）。
:::</p>
<hr />
<h2>9.7 通道方式</h2>
<h3>9.7.1 通道的基本概念</h3>
<p>通道是一种专用的 I/O 处理器，能够独立执行由 CPU 预先安排好的<strong>通道程序</strong>（由通道指令组成）。通道拥有自己的指令系统，可以自主<strong>执行通道程序、控制外设、管理数据传送</strong>，进一步降低了 CPU 在 I/O 操作中的参与程度。</p>
<p>通道与 DMA 的关系可以理解为：<strong>DMA 是通道的简化版</strong>。DMA 只能完成简单的"搬运数据"操作，而通道可以执行一套预定义的 I/O 程序，完成更复杂的控制逻辑。</p>
<h3>9.7.2 通道的类型</h3>
<table>
<thead>
<tr>
<th>类型</th>
<th>特点</th>
<th>适用设备</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>字节多路通道</strong></td>
<td>以字节为单位交叉地为多个低速设备服务，各设备轮流使用通道</td>
<td>键盘、打印机等低速设备</td>
</tr>
<tr>
<td><strong>选择通道</strong></td>
<td>一次只能为一个高速设备服务，在该设备的整个数据传输期间独占通道</td>
<td>磁盘等高速设备</td>
</tr>
<tr>
<td><strong>数组多路通道</strong></td>
<td>以数据块为单位交叉地为多个设备服务，兼顾高速与多路</td>
<td>多台磁盘并行传输</td>
</tr>
</tbody>
</table>
<h3>9.7.3 CPU 对通道的控制</h3>
<p>CPU 与通道之间的协作流程：</p>
<ol>
<li>CPU 在主存中准备好通道程序，将通道程序首地址存入通道地址字。</li>
<li>CPU 执行"启动 I/O"指令，通知通道开始工作。</li>
<li>通道从主存取出通道指令，自主执行 I/O 操作。</li>
<li>通道完成全部 I/O 操作后，向 CPU 发出中断请求。</li>
<li>CPU 响应中断，进行后处理。</li>
</ol>
<hr />
<h2>补充：常见 I/O 设备简述</h2>
<h3>常见输入输出设备</h3>
<ul>
<li><strong>键盘、鼠标</strong>：典型的人机交互输入设备。键盘通过扫描码矩阵捕获按键；鼠标通过光电传感器或机械滚轮捕获位移。</li>
<li><strong>显示器、打印机</strong>：典型的输出设备。显示器将显存中的像素矩阵转换为光信号（CRT、LCD、OLED）；打印机负责输出硬拷贝（点阵、喷墨、激光）。</li>
<li><strong>光盘存储器</strong>：通过激光照射光盘表面的凹凸坑阵列来读取数据，具有容量大、成本低、便于携带的特点。</li>
</ul>
<h3>磁盘存储器</h3>
<p>磁盘是最重要的外部存储设备之一。磁盘的数据组织结构：</p>
<ul>
<li><strong>磁道</strong>：磁盘表面上的一系列同心圆环，每个磁道可以存储一圈数据。</li>
<li><strong>扇区</strong>：每个磁道被划分为若干个弧段，每个弧段称为一个扇区，是磁盘读写的最小物理单位。</li>
<li><strong>柱面</strong>：所有磁盘面上位于同一半径位置的磁道构成一个柱面。</li>
</ul>
<p><strong>磁盘访问时间</strong>的组成：</p>
<p>$$\text{磁盘访问时间} = \text{寻道时间} + \text{旋转延迟} + \text{数据传输时间}$$</p>
<ul>
<li><strong>寻道时间</strong>：磁头从当前磁道移动到目标磁道所需的时间，是磁盘访问中最主要的延迟。</li>
<li><strong>旋转延迟</strong>：目标扇区旋转到磁头正下方所需的时间。平均旋转延迟为磁盘旋转一周时间的一半。</li>
<li><strong>数据传输时间</strong>：实际读写数据所需的时间。</li>
</ul>
<h3>磁盘阵列（RAID）</h3>
<p>RAID 通过多块磁盘并行工作来提高数据传输速率和可靠性。基本思想是将数据分散存储在多块磁盘上，同时用校验信息提供容错能力。</p>
<hr />
<h2>9.8 四种数据传输方式的全面对比</h2>
<p>:::important[四种 I/O 控制方式的对比分析（核心考点）]</p>
<table>
<thead>
<tr>
<th>对比维度</th>
<th>程序查询方式</th>
<th>程序中断方式</th>
<th>DMA 方式</th>
<th>通道方式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数据传输单位</strong></td>
<td>字/字节</td>
<td>字/字节</td>
<td>数据块</td>
<td>一组数据块</td>
</tr>
<tr>
<td><strong>CPU 参与程度</strong></td>
<td>全程参与（忙等）</td>
<td>中断时参与</td>
<td>仅初始化和结束</td>
<td>仅启动和结束</td>
</tr>
<tr>
<td><strong>数据传输路径</strong></td>
<td>CPU 中转</td>
<td>CPU 中转</td>
<td>DMA 控制器直接搬运</td>
<td>通道处理器搬运</td>
</tr>
<tr>
<td><strong>中断次数</strong></td>
<td>无中断</td>
<td>每传一个数据单位（字/字节）中断一次</td>
<td>每传一个数据块中断一次</td>
<td>每个通道程序结束中断一次</td>
</tr>
<tr>
<td><strong>CPU 利用率*</strong></td>
<td>最低</td>
<td>较高</td>
<td>高</td>
<td>最高</td>
</tr>
<tr>
<td><strong>硬件成本</strong></td>
<td>最低</td>
<td>需中断控制器</td>
<td>需 DMA 控制器</td>
<td>需通道处理器，最高</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>低速、简单外设</td>
<td>中低速外设</td>
<td>高速块设备</td>
<td>大型机复杂 I/O</td>
</tr>
<tr>
<td><strong>数据流向</strong></td>
<td>CPU ↔ I/O</td>
<td>CPU ↔ I/O</td>
<td>主存 ↔ I/O</td>
<td>主存 ↔ I/O</td>
</tr>
</tbody>
</table>
<p>*注：这里的“CPU 利用率”指 CPU 用于执行非 I/O 任务（即有用计算）的时间占比。</p>
<p>关键区别记忆：</p>
<ul>
<li>程序查询和程序中断方式的数据都必须<strong>经过 CPU 中转</strong>（CPU 的寄存器参与搬运）。</li>
<li>DMA 和通道方式的数据在外设与主存之间<strong>直接传输</strong>，不经过 CPU。
:::</li>
</ul>
<hr />
<h2>9.9 本章易错点与重点回顾</h2>
<p>:::important[统一编址与独立编址的访问指令区分]
统一编址（存储器映射）下，CPU 使用普通的访存指令（如 <code>lw</code>/<code>sw</code>）即可访问 I/O 端口。独立编址（I/O 映射）下，CPU 必须使用专用的 I/O 指令（如 <code>IN</code>/<code>OUT</code>）才能访问 I/O 端口，不能使用普通的访存指令。
:::</p>
<p>:::important[中断响应的时机]
CPU 响应外部中断的时机是在<strong>当前指令执行完毕之后</strong>，而不是指令执行过程中。CPU 在每条指令的最后一个时钟周期末尾统一检测是否有中断请求。
:::</p>
<p>:::important[DMA 与中断方式的本质区别]</p>
<ul>
<li>中断方式在每传送一个字后就要中断 CPU 一次，由 CPU 执行中断服务程序完成数据搬运。</li>
<li>DMA 方式由 DMA 控制器自主完成整块数据的搬运，只在整块传输完毕后才中断 CPU 一次。</li>
<li>因此，对于大量数据的传输，DMA 方式对 CPU 的打扰远远少于中断方式。
:::</li>
</ul>
<p>:::important[保存断点与保护现场的区别]</p>
<ul>
<li><strong>保存断点</strong>：保存 PC 的值（下一条指令的地址），由硬件在中断响应阶段自动完成。</li>
<li><strong>保护现场</strong>：保存通用寄存器和状态寄存器的内容，由中断服务程序的软件代码完成。
两者缺一不可，但执行者和执行时机不同。
:::</li>
</ul>
<hr />
<h2>9.10 历年真题与易错题集</h2>
<h3>9.10.1 选择与判断题</h3>
<p><strong>【1】关于 DMA 方式说法正确的是（ ）。</strong>（计组2019秋18级测试）
A. DMA方式采用软件实现数据传送
B. DMA方式能完全取代中断方式
C. DMA方式在传送方式中需要CPU程序的干预
D. DMA方式一般用于高速、批量数据的简单传送
<strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：DMA 方式是纯硬件控制的，数据传送过程由 DMA 控制器直接控制，不需要软件程序参与。</li>
<li><strong>B [错误]</strong>：DMA 方式仅适用于大量数据传输的硬件直连通道，缺乏处理复杂情况和异常的能力，不能完全取代中断方式。</li>
<li><strong>C [错误]</strong>：在数据传送期间，DMA 控制器直接控制总线进行主存与外设之间的数据交换，不需要 CPU 程序的干预。</li>
<li><strong>D [正确]</strong>：DMA 方式专为高速、批量数据传输设计，利用硬件控制直接在主存与外设之间进行数据传送，具有高效、简单的特点。</li>
</ul>
<p><strong>【2】中断响应是在（ ）。</strong>（计组2019秋18级测试）
A. 一条机器指令执行开始
B. 一条机器指令执行中间
C. 一条机器指令执行之末
D. 一条机器指令执行的任何时刻
<strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：如果在机器指令执行开始时响应中断，当前的指令还未开始执行，这会打断指令的正常调度。</li>
<li><strong>B [错误]</strong>：如果在指令执行的中间响应中断，会使指令的执行状态处于不确定或不完整的状态，导致难以保存当前的断点信息。</li>
<li><strong>C [正确]</strong>：为了保证被中断指令的断点状态完整，CPU 只能在当前执行的一条机器指令执行之末（即指令周期的最后一个时钟节拍）去检测并响应中断。</li>
<li><strong>D [错误]</strong>：中断响应不能在机器指令执行的任何时刻发生，只能在指令周期的结束阶段进行检测与响应。</li>
</ul>
<p><strong>【3】DMA方式中，周期窃取是窃取一个（ ）。</strong>（计组2019秋18级测试）
A. 指令周期
B. 时钟周期
C. 存取周期
D. CPU周期
<strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令周期是 CPU 执行一条指令所需的时间，通常包含多个存取周期。DMA 控制器不需要也不可能窃取一个完整的指令周期。</li>
<li><strong>B [错误]</strong>：时钟周期是 CPU 工作的最小时间单位。一个时钟周期内无法完成主存的读写操作，因此 DMA 周期窃取不能是窃取一个时钟周期。</li>
<li><strong>C [正确]</strong>：周期窃取是指当 DMA 控制器需要传输数据时，它向 CPU 申请窃取一个主存的存取周期（即读写一个字的时间），在此周期内由 DMA 控制器占用总线访问主存。</li>
<li><strong>D [错误]</strong>：CPU 周期（又称机器周期）通常由数个时钟周期组成，是完成一个基本操作所需的时间。最精确的学术定义是窃取一个存取周期。</li>
</ul>
<p><strong>【4】某计算机有五级中断L4～L0，中断屏蔽字格式为M4M3M2M1M0，Mi=1（0≤i≤4）表示对Li级中断进行屏蔽。若中断响应优先级从高到低的顺序是 L4→L0→L2→L1→L3，则 L1 的中断处理程序中设置的中断屏蔽字是（ ）。</strong>（2020-2021第二学期B）</p>
<ul>
<li><strong>正确答案</strong>：01010（若规定“1代表屏蔽，0代表允许”）或 10101（若规定“0代表屏蔽，1代表允许”）</li>
<li><strong>解析</strong>：中断屏蔽字的设置原则是：<strong>屏蔽掉自己以及所有优先级低于自己的中断源</strong>。本题优先级从高到低为：L4 &gt; L0 &gt; L2 &gt; L1 &gt; L3。对于 L1 来说，需要屏蔽 L1（自己）和 L3（低于自己），其余允许。<br />
有两种常见的屏蔽位定义：<br />
（1）若规定 <code>1</code> 代表屏蔽，<code>0</code> 代表允许：则 M1=1，M3=1，其余为 0。对应 M4M3M2M1M0 格式为 <code>01010</code>。<br />
（2）若规定 <code>0</code> 代表屏蔽，<code>1</code> 代表允许：则 M1=0，M3=0，其余为 1。对应 M4M3M2M1M0 格式为 <code>10101</code>。<br />
原卷若给出 <code>01101</code> 等选项，可能由于位序排列差异（如自右向左定义）或 1 与 0 约定差异所致。本解析统一以“1代表屏蔽，0代表允许”的标准学术约定进行推导，结果为 <code>01010</code>。</li>
</ul>
<p><strong>【5】下列选项中，能引起外部中断的事件是（ ）。</strong>（2020-2021第二学期B）
A. 键盘输入
B. 除数为0
C. 浮点运算下溢
D. 访存缺页
<strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：键盘输入是由外部设备向 CPU 发出的中断请求信号，属于典型的外部中断。</li>
<li><strong>B [错误]</strong>：除数为 0 是由于 CPU 执行了除数为 0 的除法指令而引发的异常，属于内中断（或称为异常）。</li>
<li><strong>C [错误]</strong>：浮点运算下溢是 CPU 执行浮点运算指令时产生的内部运算错误，属于内中断。</li>
<li><strong>D [错误]</strong>：访存缺页是 CPU 执行访存指令时，发现所需页面不在主存中而由存储管理单元引发的异常，属于内中断。</li>
</ul>
<p><strong>【6】某中断系统采用多级优先权编码，当同时有3个中断请求信号（优先级 A&gt;B&gt;C）到达且正在处理B中断时，中断控制器会将（ ）请求的向量地址送入PC。</strong>（南阳理工学院C卷）</p>
<ul>
<li><strong>正确答案</strong>：A</li>
<li><strong>解析</strong>：中断系统支持多级中断嵌套，高优先级的中断可以打断低优先级的中断。当前正在处理 B，此时 A 的优先级高于 B，因此 A 可以打断 B，控制器会将最高优先级 A 的向量地址送入 PC。C 的优先级低于 B，被屏蔽。</li>
</ul>
<p><strong>【7】程序查询方式相比中断方式更适合处理高速外设的数据传输。（判断题）</strong>（南阳理工学院C卷）</p>
<ul>
<li><strong>正确答案</strong>：错</li>
<li><strong>解析</strong>：程序查询方式需要 CPU 循环轮询（忙等），效率极低，无法及时处理高速外设的数据，甚至会导致数据丢失。高速外设的数据传输通常采用 DMA 方式，中低速外设常采用中断方式。</li>
</ul>
<p><strong>【8】下列关于 I/O 指令的说法中，错误的是（ ）。</strong>（2020-2021第2学期A）
A. I/O指令是CPU系统指令的一部分
B. I/O指令是机器指令的一类
C. I/O指令反映CPU和I/O设备交换信息的特点
D. I/O指令的格式和通用指令格式相同
<strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：I/O 指令是 CPU 指令系统的一部分，属于正确的表述，因此不选。</li>
<li><strong>B [错误]</strong>：I/O 指令是机器指令中的一类，由操作码和地址码组成，属于正确的表述，因此不选。</li>
<li><strong>C [错误]</strong>：I/O 指令用于实现 CPU 与外设之间的数据交换，反映了两者之间数据交换的特点，属于正确的表述，因此不选。</li>
<li><strong>D [正确]</strong>：I/O 指令通常在独立编址方式下使用，因为端口地址空间较小，所以 I/O 指令的格式通常与通用的访存指令格式不同。该说法错误，故选 D。</li>
</ul>
<p><strong>【9】以下关于通道程序的叙述中，正确的是（ ）。</strong>（2020-2021第2学期A）
A. 通道程序存放在主存中
B. 通道程序存放在通道中
C. 通道程序是由CPU执行的
D. 通道程序可以在任何环境下执行I/O操作
<strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：通道程序是由通道指令组成的序列，它与普通程序一样存放在主存储器中。</li>
<li><strong>B [错误]</strong>：通道内部没有大容量存储器，通道程序通常存放在主存储器中，通道运行时从中取出指令执行。</li>
<li><strong>C [错误]</strong>：通道程序是由通道控制器（通道处理器）读取并自主执行的，并不由 CPU 直接执行。</li>
<li><strong>D [错误]</strong>：通道程序的执行需要硬件和系统的支持，必须在操作系统和特定的硬件资源配置下运行，并不是在任何环境下都能执行。</li>
</ul>
<p><strong>【10】在 I/O 接口中，采用统一编址方式时，CPU 不能用访存指令访问 I/O 端口。（判断题）</strong>（2022-2023-2考试）</p>
<ul>
<li><strong>正确答案</strong>：错</li>
<li><strong>解析</strong>：统一编址方式（存储器映射编址）的特点正是将 I/O 端口地址和主存地址统一在同一个地址空间中。因此，CPU <strong>可以直接使用普通的访存指令</strong>（如 lw/sw）来访问 I/O 端口。</li>
</ul>
<h3>9.10.2 综合计算大题</h3>
<p><strong>【1】CPU 速度与 I/O 速度的匹配（Amdahl定律应用）</strong>（2022-2023-2考试）
<strong>题干</strong>：若基准程序 A 在某计算机上的运行时间为 100 秒，其中 90 秒为 CPU 时间，其余时间为 I/O 时间。若 CPU 速度提高百分之五十，I/O 速度不变，则运行基准程序 A 所耗费的时间是多少？运行基准程序 A 的整体速度提升了多少？通过计算结果，你可以在性能提升方面得到什么样的启发？</p>
<ul>
<li><strong>解答</strong>：
<ol>
<li>原始总时间 = 100 秒，其中 CPU 时间 = 90 秒，I/O 时间 = 10 秒。</li>
<li>CPU 速度提高 50%（即变为原来的 1.5 倍），新的 CPU 时间 = 90 / 1.5 = 60 秒。</li>
<li>新的总时间 = 新 CPU 时间 + I/O 时间 = 60 + 10 = 70 秒。</li>
<li>系统加速比 = 原始时间 / 新时间 = 100 / 70 ≈ 1.43 倍（表明系统运行速度比优化前提升了约 42.86%），或时间缩短比例 = (原始时间 - 新时间) / 原始时间 = 30%。</li>
</ol>
</li>
<li><strong>启发</strong>：根据 Amdahl 定律，系统整体性能的提升受限于未被优化的部分。如果只一味提升 CPU 的速度，当 I/O 时间在总时间中的占比逐渐增大时，系统整体性能的提升会遇到瓶颈。因此，要实现整体性能的大幅跃升，必须兼顾 CPU 速度与 I/O 速度的同步提升。</li>
</ul>
<p><strong>【2】磁盘访问时间计算</strong>（补充典型大题）
<strong>题干</strong>：某磁盘的转速为 7200 转/分，平均寻道时间为 8 ms，每个磁道容量为 1 MB，磁盘控制器延迟为 2 ms。若要读取一个 4 KB 的扇区，计算其平均访问时间。</p>
<ul>
<li><strong>解答</strong>：14.2 ms</li>
<li><strong>解析</strong>：
<ol>
<li>寻道时间 $T_s = 8$ ms。</li>
<li>旋转延迟 $T_r$：转速为 7200 rpm，即每秒 120 转，转一圈时间为 $1000 \div 120 \approx 8.33$ ms。平均旋转延迟为转半圈的时间 $T_r = 8.33 \div 2 \approx 4.17$ ms。</li>
<li>传输时间 $T_t$：一圈 1 MB 需 8.33 ms，传输 4 KB ( $4 \div 1024$ 圈 ) 的时间 $= 8.33 \times (4 \div 1024) \approx 0.03$ ms。</li>
<li>控制器延迟 $T_c = 2$ ms。</li>
<li>平均访问时间 $= T_s + T_r + T_t + T_c = 8 + 4.17 + 0.03 + 2 = 14.2$ ms。如果题目不提控制器延迟，通常只算前三项。（具体结果随保留小数位数可能微小浮动，此处展示标准算法步骤）。</li>
</ol>
</li>
</ul>
<p><strong>【3】DMA 方式与 CPU 占用率计算（高频大题）</strong>（综合自 2020-2021第2学期A / 2022-2023-2考试）
<strong>题干</strong>：某磁盘采用 DMA 方式与 CPU 交换信息，其传输速率为 20 MB/s。若 DMA 的预处理阶段需要 200 个时钟周期，DMA 完成传输后的中断处理阶段需要 400 个时钟周期。假设磁盘工作时，处理器主频为 200 MHz。
（1）如果 DMA 平均传输的数据块长度为 512 B，进行 DMA 传输时 CPU 的占有率是多少？
（2）如果采用 4 KB 的传输单位呢？</p>
<ul>
<li>
<p><strong>解答</strong>：
DMA 方式下，数据块的具体传输过程由 DMA 控制器硬件自主完成，不占用 CPU 时间。CPU 的开销仅存在于每次 DMA 请求前后的<strong>预处理</strong>和<strong>后处理</strong>阶段。
每次 DMA 传输 CPU 的总开销 = 200 + 400 = 600 个时钟周期。
<strong>（1）数据块为 512 B 时：</strong></p>
<ul>
<li>每秒发生的 DMA 传输次数 = 20 MB/s ÷ 512 B = 20 × 10^6 ÷ 512 = 39062.5 次。</li>
<li>每秒 CPU 在 DMA 上的开销时钟数 = 39062.5 × 600 = 23,437,500 个时钟周期。</li>
<li>CPU 占有率 = 开销时钟数 ÷ CPU 主频 = 23,437,500 ÷ (200 × 10^6) = 0.1171875 ≈ <strong>11.72%</strong>。</li>
</ul>
<p><strong>（2）数据块为 4 KB 时：</strong></p>
<ul>
<li>每秒发生的 DMA 传输次数 = 20 MB/s ÷ 4096 B = 20 × 10^6 ÷ 4096 = 4882.8 次（注：若按1K=1000计算则为5000次）。</li>
<li>按 4882.8 次计算：每秒 CPU 开销时钟数 = 4882.8 × 600 = 2,929,687 个时钟周期。</li>
<li>CPU 占有率 = 2,929,687 ÷ (200 × 10^6) = 0.014648 ≈ <strong>1.46%</strong>。</li>
<li><em>(注：数据块越大，触发 DMA 中断的频率越低，CPU 的占有率就越小。)</em></li>
</ul>
</li>
</ul>
<p><strong>【4】DMA 等效指令数占用率计算</strong>（2022-2023-2考试）
<strong>题干</strong>：某计算机的 CPU 主频为 500 MHz，CPI 为 5。假定某外设的数据传输率为 5 MB/s，采用 DMA 方式与主机进行数据传送，每次 DMA 传送块大小为 5000 B，每次 DMA 预处理和后处理的总开销相当于 100 条指令的执行时间。求该外设 I/O 时的 CPU 占用率。（假设 DMA 与 CPU 之间没有访存冲突）</p>
<ul>
<li><strong>解答</strong>：
<ol>
<li>计算每秒发生 DMA 的次数：
每秒 DMA 请求次数 = 5 MB/s ÷ 5000 B = (5 × 10^6) ÷ 5000 = 1000 次。</li>
<li>计算每次 DMA 传输 CPU 的时钟开销：
每次开销等效于执行 100 条指令。由于 CPI = 5，100 条指令耗费的时钟周期数 = 100 × 5 = 500 个。</li>
<li>计算 CPU 占有率：
每秒 CPU 在 DMA 上的总开销 = 1000 次 × 500 时钟/次 = 500,000 个时钟周期。
CPU 占有率 = 500,000 ÷ (500 × 10^6) = 0.001 = <strong>0.1%</strong>。</li>
</ol>
</li>
</ul>
<p><strong>【5】中断驱动 I/O 的 CPU 占有率计算</strong>（2020-2021第二学期B）
<strong>题干</strong>：计算机系统采用中断驱动方式进行输入输出，CPU 的时钟频率为 200 MHz。硬盘以 512 字节大小的扇区为单位传输数据，启动阶段发送命令和参数需要 90 个时钟周期，中断服务开销 400 时钟（包括中断响应、中断处理，不包括数据传输），实际数据传输需要 1555 时钟周期，CPU 访问硬盘的速率为 20 MB/s。
（1）求中断驱动 I/O 方式下的 CPU 占有率。
（2）如果硬盘速率提高到 50 MB/s，会发生什么情况？</p>
<ul>
<li><strong>解答</strong>：
中断驱动方式下，<strong>数据的传输也是在中断服务程序中由 CPU 亲自参与完成的</strong>，因此实际数据传输的时间也算作 CPU 开销。
每次中断 CPU 的总开销 = 启动准备(90) + 中断服务(400) + 数据传输(1555) = 2045 个时钟周期。
<strong>（1）硬盘速率为 20 MB/s 时：</strong>
<ul>
<li>每秒中断次数 = 20 MB/s ÷ 512 B = (20 × 10^6) ÷ 512 = 39062.5 次。</li>
<li>CPU 占有率 = 39062.5 × 2045 ÷ (200 × 10^6) = 79,882,812.5 ÷ (200 × 10^6) ≈ <strong>0.399</strong>（约 40%）。
<strong>（2）硬盘速率提高到 50 MB/s 时：</strong></li>
<li>每秒中断次数 = 50 MB/s ÷ 512 B = (50 × 10^6) ÷ 512 = 97656.25 次。</li>
<li>CPU 占有率 = 97656.25 × 2045 ÷ (200 × 10^6) = 199,707,031.25 ÷ (200 × 10^6) ≈ <strong>0.9985</strong>（接近 100%）。</li>
<li><strong>发生的情况</strong>：CPU 几乎全部的时间都被硬盘 I/O 中断所占用，系统无法再执行其他正常的计算程序，性能严重下降。这也证明了高速设备不适合采用中断方式，而应采用 DMA 方式。</li>
</ul>
</li>
</ul>
<hr />
<h2>9.11 补充习题区</h2>
<p>*（留白：在此区域可补充后续刷题过程中遇到的更多偏门题、坑题，可按照前面节的格式进行归纳，作为考前冲刺弹药库）</p>
<hr />
<h2>习题9：课后习题详解</h2>
<p><img src="https://img.picr2.online/1782821583161_1.webp" alt="习题截图1" />
<img src="https://img.picr2.online/1782821586099_2.webp" alt="习题截图2" />
<img src="https://img.picr2.online/1782821585590_3.webp" alt="习题截图3" /></p>
<hr />
<h3>习题9.1 解释下列名词</h3>
<p><strong>题干：</strong></p>
<p>接口、中断、单级中断、多重中断、中断屏蔽、中断响应优先级、中断处理优先级、中断响应、中断识别、中断隐指令、中断号、中断向量、向量中断、向量地址、中断向量表、程序查询 I/O、程序中断 I/O、DMA、第三方 DMA、周期挪用、磁道、扇区、道密度、位密度、平均定位时间、条带。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>接口</strong>：CPU/主存与外部设备之间的连接部件，完成地址译码、命令接收、状态保存、数据缓冲、格式转换等。</li>
<li><strong>中断</strong>：CPU 因内部异常或外部请求暂停当前程序，转去执行中断服务程序，完成后返回原程序的机制。</li>
<li><strong>单级中断</strong>：只允许一个中断层次，不发生中断嵌套。</li>
<li><strong>多重中断</strong>：允许中断嵌套，高优先级可打断低优先级中断服务。</li>
<li><strong>中断屏蔽</strong>：通过屏蔽字使某些中断请求暂时不被 CPU 响应的机制。</li>
<li><strong>中断响应优先级</strong>：多个中断源同时请求时，CPU 先响应谁的优先级，由硬件决定。</li>
<li><strong>中断处理优先级</strong>：中断服务程序执行期间各中断谁能打断谁的优先级，由屏蔽字控制。</li>
<li><strong>中断响应</strong>：CPU 检测到可响应的中断请求后，暂停当前程序进入中断响应周期的过程。</li>
<li><strong>中断识别</strong>：CPU 确定中断源并找到对应服务程序入口的过程。</li>
<li><strong>中断隐指令</strong>：中断响应时硬件自动完成的操作（关中断、保存断点、获取入口地址）。</li>
<li><strong>中断号</strong>：标识中断类型或中断源的编号。</li>
<li><strong>中断向量</strong>：中断服务程序的入口地址。</li>
<li><strong>向量中断</strong>：中断源向 CPU 提供中断类型号，使 CPU 直接获得入口地址的中断方式。</li>
<li><strong>向量地址</strong>：中断向量在中断向量表中的存放地址。</li>
<li><strong>中断向量表</strong>：按中断号顺序存放各中断服务程序入口地址的表。</li>
<li><strong>程序查询 I/O</strong>：CPU 循环查询设备状态，就绪后进行数据传送。</li>
<li><strong>程序中断 I/O</strong>：外设就绪后发中断请求，CPU 响应后执行服务程序完成传送。</li>
<li><strong>DMA</strong>：直接存储器访问，由 DMA 控制器控制数据在主存与 I/O 设备间直接传送。</li>
<li><strong>第三方 DMA</strong>：由独立的 DMA 控制器作为第三方控制数据传送，可为多设备服务。</li>
<li><strong>周期挪用</strong>：DMA 的一种总线使用方式，DMA 临时窃取存储周期进行传送。</li>
<li><strong>磁道</strong>：磁盘盘面上的同心圆形记录轨迹。</li>
<li><strong>扇区</strong>：磁道上的弧段，是磁盘读写的最小物理单位。</li>
<li><strong>道密度</strong>：沿半径方向单位长度上的磁道数。</li>
<li><strong>位密度</strong>：磁道圆周方向单位长度上记录的二进制位数。</li>
<li><strong>平均定位时间</strong>：磁头定位到目标扇区所需的平均时间（寻道 + 旋转等待）。</li>
<li><strong>条带</strong>：磁盘阵列中将连续数据分块分布到多个磁盘上的组织单位。</li>
</ol>
<hr />
<h3>习题9.2 单选题（考研真题）</h3>
<h4>(1) [2012] 下列选项中，在 I/O 总线的数据线上传输的信息包括__________。</h4>
<p>Ⅰ. I/O 接口中的命令字
Ⅱ. I/O 接口中的状态字
Ⅲ. 中断类型号</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：命令字（CPU 写入接口）、状态字（CPU 读出接口）、中断类型号（接口提供给 CPU）都通过数据总线传输。</p>
<h4>(2) [2014] 下列有关 I/O 接口的叙述中，错误的是__________。</h4>
<p>A. 状态端口和控制端口可以合用同一寄存器
B. I/O 接口中 CPU 可访问的寄存器，称为 I/O 端口
C. 采用独立编址方式时，I/O 端口地址和主存地址可能相同
D. 采用统一编址方式时，CPU 不能用访存指令访问 I/O 端口</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：状态和控制信息可放在同一寄存器不同位。</li>
<li><strong>B [正确]</strong>：I/O 端口即接口中 CPU 可访问的寄存器。</li>
<li><strong>C [正确]</strong>：独立编址时 I/O 地址空间与主存地址空间独立，数值可相同。</li>
<li><strong>D [错误]</strong>：统一编址时 I/O 端口占用主存地址空间，可用访存指令访问。</li>
</ul>
<h4>(3) [2021] 下列选项中，不属于 I/O 接口的是__________。</h4>
<p>A. 磁盘驱动器
B. 打印机适配器
C. 网络控制器
D. 可编程中断控制器</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：磁盘驱动器是外部设备本身，不是接口。打印机适配器、网络控制器、可编程中断控制器都属于 I/O 接口类部件。</p>
<h4>(4) [2017] I/O 指令实现的数据传送通常发生在__________。</h4>
<p>A. I/O 设备和 I/O 端口之间
B. 通用寄存器和 I/O 设备之间
C. I/O 端口和 I/O 端口之间
D. 通用寄存器和 I/O 端口之间</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：CPU 执行 I/O 指令时，输入将 I/O 端口数据读入通用寄存器，输出将通用寄存器数据写入 I/O 端口。</p>
<h4>(5) [2023] 下列关于 I/O 控制方式的叙述中，错误的是__________。</h4>
<p>A. 查询方式下，通过 CPU 执行查询程序进行 I/O 操作
B. 中断方式下，通过 CPU 执行中断服务程序进行 I/O 操作
C. DMA 方式下，通过 CPU 执行 DMA 传送程序进行 I/O 操作
D. 对于 SSD、网络适配器等高速设备，采用 DMA 方式输入/输出</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：DMA 传送由 DMA 控制器硬件完成，CPU 不执行"传送程序"逐字传送，只负责初始化。</p>
<h4>(6) [2022] 下列关于中断 I/O 方式的叙述中，不正确的是__________。</h4>
<p>A. 适用于键盘、针式打印机等字符型设备
B. 外设和主机之间的数据传送通过软件完成
C. 外设准备数据的时间应小于中断处理时间
D. 外设为某进程准备数据时 CPU 可运行其他进程</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：中断 I/O 的优势正是利用外设较长的准备时间让 CPU 做其他工作，因此外设准备时间不要求小于中断处理时间。</p>
<h4>(7) [2009] 下列选项中，能引起外部中断的事件是__________。</h4>
<p>A. 键盘输入
B. 除数为零
C. 浮点运算下溢
D. 访存故障</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：键盘输入由外部设备产生，属于外部中断。B、C、D 都是 CPU 内部异常。</p>
<h4>(8) [2020] 下列事件中，属于外部中断事件的是__________。</h4>
<p>Ⅰ. 访存时缺页
Ⅱ. 定时器到时
Ⅲ. 网络数据包到达</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. Ⅰ、Ⅱ 和 Ⅲ</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：缺页是内部异常，定时器到时和网络数据包到达是外部中断。</p>
<h4>(9) [2020] 下列关于外部中断的叙述中，错误的是__________。</h4>
<p>A. CPU 处于关中断状态时，也能响应 NMI 请求
B. 一旦可屏蔽中断请求信号有效，CPU 将立即响应
C. 不可屏蔽中断的优先级比可屏蔽中断的优先级高
D. 可通过中断屏蔽字改变可屏蔽中断的处理优先级</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：可屏蔽中断需满足开中断、未屏蔽、指令结束等条件后才能响应，并非立即响应。</p>
<h4>(10) [2010] 单级中断系统中，中断服务程序内部的执行顺序是__________。</h4>
<p>Ⅰ. 保护现场
Ⅱ. 开中断
Ⅲ. 关中断
Ⅳ. 保存断点
Ⅴ. 中断事件处理
Ⅵ. 恢复现场
Ⅶ. 中断返回</p>
<p>A. Ⅰ → Ⅴ → Ⅵ → Ⅱ → Ⅶ
B. Ⅲ → Ⅰ → Ⅴ → Ⅶ
C. Ⅲ → Ⅳ → Ⅴ → Ⅵ → Ⅶ
D. Ⅳ → Ⅰ → Ⅴ → Ⅵ → Ⅶ</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：中断隐指令已完成保存断点和关中断，服务程序内顺序为：保护现场 → 事件处理 → 恢复现场 → 开中断 → 中断返回。</p>
<h4>(11) [2012] 响应外部中断的过程中，中断隐指令完成的操作，除保护断点外，还包括__________。</h4>
<p>Ⅰ. 关中断
Ⅱ. 保存通用寄存器的内容
Ⅲ. 形成中断服务程序入口地址并送入 PC</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：中断隐指令完成关中断和形成入口地址。保存通用寄存器由服务程序软件完成。</p>
<h4>(12) [2017] 下列关于多重中断系统的叙述中，错误的是__________。</h4>
<p>A. 在一条指令执行结束时响应中断
B. 中断处理期间 CPU 处于关中断状态
C. 中断请求的产生与当前指令的执行无关
D. CPU 通过采样中断请求信号检测中断请求</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：多重中断要允许高优先级中断嵌套，不能在整个处理期间一直关中断。</p>
<h4>(13) [2021] 下列关于多重中断系统中 CPU 响应中断的叙述中，错误的是__________。</h4>
<p>A. 仅在用户态（执行用户程序下），CPU 才能检测和响应中断
B. CPU 只有在检测到中断请求信号后，才会进入中断响应周期
C. 进入中断响应周期时，CPU 一定处于中断允许（开中断）状态
D. 若 CPU 检测到中断请求信号，则一定存在未被屏蔽的中断请求信号</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：CPU 在用户态和内核态都可以检测中断。多重中断中，处理中断期间也可能响应更高优先级中断。</p>
<h4>(14) [2023] 下列关于硬件异常与中断关系的叙述中，错误的是__________。</h4>
<p>A. CPU 在执行一条指令过程中检测异常事件
B. CPU 在执行完一条指令时检测中断请求信号
C. 开中断时 CPU 检测到中断请求后就进行中断响应
D. 外部设备通过中断控制器向 CPU 发中断结束信号</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：中断结束通常由 CPU/服务程序通知中断控制器，不是外设向 CPU 发"中断结束信号"。</p>
<h4>(15) [2024] 关于中断 I/O 方式的叙述中，错误的是__________。</h4>
<p>A. 中断屏蔽字决定中断响应顺序
B. 多重中断方式下，恢复现场处于关中断状态
C. 保存通用寄存器和设置新中断屏蔽字由软件实现
D. 单重中断方式下，中断处理时 CPU 处于关中断状态</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：中断屏蔽字影响处理优先级，不直接决定硬件中断响应顺序。响应顺序由硬件优先级排队决定。</p>
<h4>(16) [2015] 在采用中断 I/O 方式控制打印输出的情况下，CPU 和打印控制接口中的 I/O 端口之间交换的信息不可能是__________。</h4>
<p>A. 打印字符
B. 主存地址
C. 设备状态
D. 控制命令</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：中断 I/O 方式中数据传送由 CPU 软件完成，不需要向接口交换主存地址。主存地址通常用于 DMA 初始化。</p>
<h4>(17) [2018] 下列关于外部 I/O 中断的叙述中，正确的是__________。</h4>
<p>A. 中断控制器按所接收中断请求的先后次序进行中断优先级排队
B. CPU 响应中断时，通过执行中断隐指令完成对通用寄存器的保护
C. CPU 只有在处于中断允许状态时，才能响应外部设备的中断请求
D. 有中断请求时，CPU 立即暂停执行当前指令，转去执行中断服务程序</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：中断优先级由硬件规则决定，不是按请求先后。</li>
<li><strong>B [错误]</strong>：通用寄存器由软件保护。</li>
<li><strong>C [正确]</strong>：对外部可屏蔽 I/O 中断，CPU 需处于中断允许状态才会响应。</li>
<li><strong>D [错误]</strong>：CPU 在当前指令执行结束后响应外部中断。</li>
</ul>
<h4>(18) [2013] 下列关于中断 I/O 方式和 DMA 方式比较的叙述中，错误的是__________。</h4>
<p>A. 中断 I/O 方式请求的是 CPU 处理时间，DMA 方式请求的是总线使用权
B. 中断响应发生在一条指令执行结束后，DMA 响应发生在一个总线事务完成后
C. 中断 I/O 方式下数据传送通过软件完成，DMA 方式下数据传送由硬件完成
D. 中断 I/O 方式适用于所有外部设备，DMA 方式仅适用于高速外部设备</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：中断 I/O 不适合高速块设备（逐字中断开销大），DMA 更适合高速大批量传送。"所有"一词过于绝对。</p>
<h4>(19) [2024] DMA 方式中，DMA 控制器控制的数据传输通路位于__________。</h4>
<p>A. CPU 和主存之间
B. CPU 和设备接口之间
C. 设备接口和主存之间
D. 设备接口和 DMA 控制器之间</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：DMA 的目标是让数据直接在设备接口和主存之间传送，绕过 CPU。</p>
<h4>(20) [2020] 若设备采用周期挪用 DMA 方式进行输入和输出，每次 DMA 传送的数据块大小为 512 字节，相应的 I/O 接口中有一个 32 位数据缓冲寄存器。对于数据输入过程，下列叙述中，错误的是__________。</h4>
<p>A. 每准备好 32 位数据，DMA 控制器就发出一次总线请求
B. 相对于 CPU，DMA 控制器的总线使用权的优先级更高
C. 在整个数据块的传送过程中，CPU 不可以访问主存储器
D. 数据块传送结束时，会产生"DMA 传送结束"中断请求</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：周期挪用只在被挪用的存储周期暂停 CPU 访存，CPU 并非整个数据块传送期间都不能访存。</p>
<h4>(21) [2010] 假定一台计算机的显示存储器用 DRAM 芯片实现，若要求显示分辨率为 1600 像素 × 1200 像素，颜色深度为 24 位，帧频为 85Hz，显存总带宽的 50% 用来刷新屏幕，则需要的显存总带宽至少约为__________。</h4>
<p>A. 245Mbit/s
B. 979Mbit/s
C. 1958Mbit/s
D. 7834Mbit/s</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：每帧 $1600 \times 1200 \times 24 = 46,080,000$ bit，刷新带宽 $= 46,080,000 \times 85 \approx 3916.8$ Mbit/s。总带宽 $= 3916.8 / 0.5 \approx 7834$ Mbit/s。</p>
<h4>(22) [2015] 若磁盘转速为 7200 转/分钟，平均寻道时间为 8ms，每个磁道包含 1000 个扇区，则访问一个扇区的平均存取时间大约是__________。</h4>
<p>A. 8.1ms
B. 12.2ms
C. 16.3ms
D. 20.5ms</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：转一圈 $= 8.333$ms，平均旋转等待 $= 4.1665$ms，传输时间 $= 0.0083$ms，总 $= 8 + 4.1665 + 0.0083 \approx 12.2$ms。</p>
<hr />
<h3>习题9.3 简要回答下列问题</h3>
<h4>（1）CPU 与外部设备之间如何连接？</h4>
<p><strong>解析</strong>：</p>
<p>CPU 与外部设备之间<strong>不是直接相连</strong>，而是通过 <strong>I/O 接口</strong>连接。CPU 通过系统总线（地址、数据、控制总线）访问 I/O 接口中的端口（数据端口、状态端口、控制端口），I/O 接口再将 CPU 的统一总线信号转换为外设所需的格式、速度和时序。不同速度的设备采用不同的控制方式：低速设备可用程序查询或中断，高速块设备采用 DMA。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>总线系统</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%80%BB%E7%BA%BF%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%80%BB%E7%BA%BF%E7%B3%BB%E7%BB%9F/</id>
    <published>2026-06-30T05:36:00.000Z</published>
    <updated>2026-06-30T05:36:00.000Z</updated>
    <summary>整理计算机总线系统的基本概念与分类，详解总线的组成与性能指标、总线仲裁与定时机制、单/双/三总线结构及常用总线标准。</summary>
    <content type="html"><![CDATA[<h1>第八章：总线系统</h1>
<p>本章整理计算机各部件之间进行信息传输的公共通道：总线。总线是连接 CPU、主存、I/O 接口等各大部件的"数据高速公路"，其设计直接影响整个计算机系统的性能上限。</p>
<p>本章考试重点为总线的基本概念、性能指标计算、仲裁方式和定时方式，以简答和选择题为主。</p>
<hr />
<h2>8.1 总线概述</h2>
<h3>8.1.1 总线分类</h3>
<p>总线是一组能为多个部件<strong>分时共享</strong>的公共信息传输线路。按照在计算机系统中所处的层次位置，总线通常分为以下三类：</p>
<table>
<thead>
<tr>
<th>总线类型</th>
<th>连接对象</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>片内总线</strong></td>
<td>CPU 芯片内部各功能单元之间</td>
<td>位于芯片内部，速度最快，位宽与 CPU 内部数据通路一致</td>
</tr>
<tr>
<td><strong>系统总线</strong></td>
<td>CPU、主存、I/O 接口等主要部件之间</td>
<td>是计算机系统的核心总线，包含数据、地址、控制三类信号线</td>
</tr>
<tr>
<td><strong>I/O 总线（通信总线）</strong></td>
<td>计算机与外部设备或其他计算机之间</td>
<td>速度相对较慢，用于连接各种外部设备</td>
</tr>
</tbody>
</table>
<h3>8.1.2 总线组成</h3>
<p>系统总线按照传送信息的类型，由以下三组信号线构成：</p>
<p><strong>1. 数据总线（DB）</strong></p>
<p>传输各部件之间的数据信息，是双向传输的。数据总线的位数（又称总线宽度）通常与 CPU 的机器字长一致或为其整数倍，直接决定了每次传输的数据量。</p>
<p><strong>2. 地址总线（AB）</strong></p>
<p>由 CPU 发出，用于指明当前访问的主存单元或 I/O 端口的物理地址。地址总线是单向的。地址总线的位数决定了 CPU 能直接寻址的最大地址空间：</p>
<p>$$\text{最大寻址空间} = 2^{\text{地址总线位数}}$$</p>
<p><strong>3. 控制总线（CB）</strong></p>
<p>传送各种控制信号和时序信号，如读/写控制、中断请求、中断应答、总线请求、总线允许、时钟信号等。控制总线中有些是 CPU 向外发出的（如读写信号），有些是外部向 CPU 发送的（如中断请求）。</p>
<h3>8.1.3 总线标准</h3>
<p>总线标准是为了实现不同厂商生产的硬件模块之间的互连互通，而制定的一套统一的接口规范。总线标准规定了：</p>
<ul>
<li><strong>机械特性</strong>：接插件的尺寸、引脚排列和锁扣机构。</li>
<li><strong>电气特性</strong>：信号的电压电平、驱动能力和阻抗匹配。</li>
<li><strong>功能特性</strong>：每根信号线的名称和功能定义。</li>
<li><strong>时序特性</strong>：各信号线上信号有效的先后顺序和持续时间。</li>
</ul>
<h3>8.1.4 总线与三态门</h3>
<p>三态门是实现总线分时共享的关键电路器件。三态门有三种输出状态：高电平、低电平和<strong>高阻态</strong>。</p>
<ul>
<li>当三态门的使能信号无效时，输出呈高阻态，等效于与总线断开，不会干扰总线上其他部件的传输。</li>
<li>当使能信号有效时，三态门正常驱动总线，输出高电平或低电平。</li>
</ul>
<p>通过三态门的控制，可以保证在任意时刻<strong>只有一个部件</strong>向总线发送数据，而其他部件处于高阻态"让出"总线，从而实现总线的分时共享。</p>
<h3>8.1.5 总线性能指标</h3>
<p>:::important[总线性能指标与时钟周期概念辨析]</p>
<table>
<thead>
<tr>
<th>概念</th>
<th>定义</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>时钟周期（时钟节拍）</strong></td>
<td>计算机中最基本的定时单位，通常由晶振产生。时钟周期的倒数即为主频。</td>
</tr>
<tr>
<td><strong>机器周期（CPU周期）</strong></td>
<td>CPU完成一项基本操作（如取指、执行）所需的时间。通常由若干个时钟周期组成。</td>
</tr>
<tr>
<td><strong>总线时钟周期</strong></td>
<td>总线工作时的基本时钟节拍，总线工作频率的倒数。</td>
</tr>
<tr>
<td><strong>总线周期（总线传输周期）</strong></td>
<td>完成一次完整总线事务（如主存读写）所需的时间。通常包含若干个总线时钟周期。</td>
</tr>
<tr>
<td><strong>存储周期</strong></td>
<td>存储器连续启动两次独立读/写操作所需的最小时间间隔。</td>
</tr>
</tbody>
</table>
<p><strong>总线带宽计算公式</strong>：
$$\text{总线带宽} = \text{总线工作频率} \times \frac{\text{总线宽度}}{8} \quad (\text{单位：B/s})$$</p>
<p><strong>【避坑指南】</strong>：
题目中常出现 $1024 \text{ MB/s}$ 或类似数据结果。</p>
<ul>
<li>若按十进制标准（1 GB = 1000 MB），则 $1000 \text{ MB/s} = 1 \text{ GB/s}$。</li>
<li>若按二进制标准（1 GiB = 1024 MiB），则 $1024 \text{ MiB/s} = 1 \text{ GiB/s}$。
考试时严格按照题干给定的单位前缀计算，不要为了凑整而随意写“约 1 GB/s”，以免被扣分。
:::</li>
</ul>
<hr />
<h2>8.2 总线传输机制</h2>
<h3>8.2.1 总线传输过程</h3>
<p>一次完整的总线传输操作（称为一个总线事务）通常包括以下四个阶段：</p>
<ol>
<li><strong>申请与仲裁阶段</strong>：需要使用总线的设备向总线仲裁器发出使用请求，仲裁器按照一定策略决定由哪个设备获得总线使用权。</li>
<li><strong>寻址阶段</strong>：获得总线使用权的主设备将目标设备的物理地址或端口地址放到地址总线上，选中目标从设备。</li>
<li><strong>传输阶段</strong>：主设备和从设备之间通过数据总线进行实际的数据传输。</li>
<li><strong>结束阶段</strong>：主设备释放总线使用权，让其他设备有机会申请使用。</li>
</ol>
<pre><code>graph LR
    A[申请与仲裁] --&gt; B[寻址阶段]
    B --&gt; C[传输阶段]
    C --&gt; D[结束阶段]
    
    style A fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
    style B fill:#fff3e0,stroke:#f57c00,stroke-width:2px
    style C fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
    style D fill:#fce4ec,stroke:#c2185b,stroke-width:2px
</code></pre>
<h3>8.2.2 总线的信息传送方式</h3>
<p>按照数据传输方向的特征，总线的信息传送方式可分为：</p>
<ul>
<li><strong>串行传输</strong>：数据在一条传输线上逐位依次传送。速度较慢，但传输距离远、线路成本低。</li>
<li><strong>并行传输</strong>：数据在多条传输线上同时传送各位。速度快，但受线间串扰和同步偏移的限制，传输距离较短。</li>
</ul>
<p>按照通信方向，还可分为：</p>
<ul>
<li><strong>单工</strong>：只能沿一个方向传输。</li>
<li><strong>半双工</strong>：可以双向传输，但同一时刻只能一个方向。</li>
<li><strong>全双工</strong>：可以同时双向传输。</li>
</ul>
<h3>8.2.3 总线仲裁</h3>
<p>当系统中有多个设备同时申请使用总线时，需要通过仲裁机制来决定哪个设备优先获得总线使用权。</p>
<h4>1. 集中仲裁方式</h4>
<p>由一个专门的总线仲裁器（通常集成在 CPU 或北桥芯片中）统一管理总线使用权的分配。</p>
<p><strong>链式查询方式（菊花链）</strong>：</p>
<ul>
<li>所有设备共用一条总线请求线（BR）和一条总线允许线（BG）。总线允许信号沿 BG 线从设备 0 依次串行传递到设备 n。</li>
<li>离仲裁器最近的设备优先级最高。如果某设备有总线请求且允许信号传到它时，它截获该信号并占用总线，后续设备无法收到允许信号。</li>
<li><strong>优点</strong>：电路简单，扩展方便。</li>
<li><strong>缺点</strong>：离仲裁器近的设备优先级固定且最高，远端设备可能长期得不到服务（"饥饿"现象）；BG 线是串行传递的，响应速度较慢。</li>
</ul>
<p><strong>计数器定时查询方式</strong>：</p>
<ul>
<li>仲裁器内部有一个计数器，通过一组设备地址线发出当前查询的设备编号。如果该编号设备有总线请求，则获得使用权。</li>
<li>计数器可以从 0 开始（等效于链式查询），也可以从上次服务的设备编号继续（循环轮询，各设备优先级平等）。</li>
<li><strong>优点</strong>：通过改变计数起点可以灵活调整优先级。</li>
<li><strong>缺点</strong>：增加了计数器硬件和设备地址线。</li>
</ul>
<p><strong>独立请求方式</strong>：</p>
<ul>
<li>每个设备都有独立的总线请求线（BR）和总线允许线（BG）与仲裁器直接相连。</li>
<li>仲裁器可以同时接收所有设备的请求，通过内部优先级编码逻辑快速选出优先级最高的设备。</li>
<li><strong>优点</strong>：响应速度最快，优先级控制灵活。</li>
<li><strong>缺点</strong>：线数最多，硬件开销最大。</li>
</ul>
<p>:::tip[三种集中仲裁方式的对比记忆]</p>
<table>
<thead>
<tr>
<th>方式</th>
<th>请求线</th>
<th>允许线</th>
<th>优先级</th>
<th>速度</th>
<th>硬件开销</th>
</tr>
</thead>
<tbody>
<tr>
<td>链式查询</td>
<td>1 条共用</td>
<td>1 条串行</td>
<td>固定，近端最高</td>
<td>慢</td>
<td>最少</td>
</tr>
<tr>
<td>计数器查询</td>
<td>1 条共用</td>
<td>地址线</td>
<td>可调</td>
<td>中</td>
<td>中等</td>
</tr>
<tr>
<td>独立请求</td>
<td>每设备独立</td>
<td>每设备独立</td>
<td>灵活可编程</td>
<td>最快</td>
<td>最多</td>
</tr>
<tr>
<td>:::</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<pre><code>graph TD
    subgraph 链式查询
        A1[仲裁器] --&gt;|BG 串行| B1[设备0]
        B1 --&gt;|BG 串行| C1[设备1]
        C1 --&gt;|BG 串行| D1[设备n]
        B1 -.BR 汇线.- A1
        C1 -.BR 汇线.- A1
        D1 -.BR 汇线.- A1
    end
    subgraph 独立请求
        A2[仲裁器] &lt;--&gt;|BG0 / BR0| B2[设备0]
        A2 &lt;--&gt;|BG1 / BR1| C2[设备1]
        A2 &lt;--&gt;|BGn / BRn| D2[设备n]
    end
</code></pre>
<h4>2. 分布仲裁方式</h4>
<p>不设集中的仲裁器。每个设备中都内置仲裁逻辑，各设备将自己的优先级编号放在总线上，通过比较竞争来决定总线使用权。优先级最高者获胜。</p>
<h3>8.2.4 总线定时</h3>
<p>总线定时是指在总线数据传输过程中，主设备与从设备之间如何协调同步的通信机制。</p>
<h4>1. 同步定时方式</h4>
<p>主设备和从设备采用统一的系统时钟信号进行同步。所有的地址、数据和控制信号的出现和采样时刻都由公共时钟的边沿严格确定。</p>
<ul>
<li><strong>优点</strong>：控制逻辑简单，总线传输速度快。</li>
<li><strong>缺点</strong>：所有设备必须按照同一时钟频率工作，总线周期必须按照最慢的设备来设定，快速设备的能力无法充分发挥。适合于速度差异较小的同类设备。</li>
</ul>
<h4>2. 异步定时方式</h4>
<p>不使用统一时钟，而是通过主设备和从设备之间的<strong>握手信号</strong>（请求/应答信号）来实现同步。</p>
<p><strong>握手方式及互锁关系：</strong></p>
<ul>
<li>
<p><strong>非互锁</strong>：主设备发请求，经过固定延时后撤销；从设备发应答，经过固定延时后撤销。双方均不依赖对方撤销。（2次握手状态改变）</p>
</li>
<li>
<p><strong>半互锁</strong>：主设备发请求，必须收到应答才撤销（有互锁）；从设备发应答，经过固定延时撤销，不依赖请求撤销。</p>
</li>
<li>
<p><strong>全互锁</strong>：主发请求，必须收到应答才撤销；从发应答，必须检测到请求撤销后才撤销应答。双方完全互锁，最可靠但最慢。（4次完整的握手状态改变）</p>
</li>
<li>
<p><strong>优点</strong>：允许连接速度差异很大的设备，灵活性高。</p>
</li>
<li>
<p><strong>缺点</strong>：需要额外的握手信号线，控制逻辑复杂，传输效率不如同步方式。</p>
</li>
</ul>
<p>:::important[同步与异步定时的本质区别]
同步定时依靠公共时钟的"节奏"来协调双方，所有动作在固定节拍上发生；异步定时依靠"握手应答"的事件驱动机制来协调，每个动作的发生取决于对方的回应。
:::</p>
<h4>3. 半同步定时方式</h4>
<p>基本框架采用同步时钟，但允许从设备在自身尚未准备好数据时，通过一条 WAIT 信号通知主设备插入等待周期。从设备就绪后撤销 WAIT 信号，总线传输继续按照同步时钟推进。</p>
<p>这是同步与异步方式的折中方案，兼顾了同步方式的简单高效和异步方式对慢速设备的兼容。</p>
<h4>4. 分离式通信方式</h4>
<p>将一个总线传输周期分成两个子周期：</p>
<ul>
<li>
<p><strong>子周期 1</strong>：主设备申请总线，发出地址和命令，然后<strong>立即放弃总线</strong>。</p>
</li>
<li>
<p><strong>子周期 2</strong>：从设备准备好数据后，<strong>由从设备主动申请总线</strong>，将数据发回主设备。</p>
</li>
<li>
<p><strong>优点</strong>：从设备准备数据的漫长等待时间内，总线被释放给了其他设备使用，极大提高了总线的有效利用率。</p>
</li>
<li>
<p><strong>缺点</strong>：控制异常复杂，通常只用于大型机或高性能系统的存储器总线。</p>
</li>
</ul>
<hr />
<h3>8.2.5 地址/数据复用总线</h3>
<p>为了减少总线引脚数量，降低芯片封装成本，系统常将地址线和数据线复用为同一组物理线路。</p>
<ul>
<li><strong>工作机制</strong>：在总线周期的<strong>寻址阶段</strong>，这组线传输地址信号；在<strong>传输阶段</strong>，这组线传输数据信号。</li>
<li><strong>代价</strong>：由于无法同时传输地址和数据，总线周期通常会被拉长，传输速率受到一定影响。</li>
</ul>
<hr />
<h2>8.3 总线结构</h2>
<h3>8.3.1 单总线结构</h3>
<p>CPU、主存、I/O 接口等所有部件都连接在同一条系统总线上。</p>
<ul>
<li><strong>优点</strong>：结构简单，扩展方便，成本低。</li>
<li><strong>缺点</strong>：同一时刻只能有两个部件之间进行数据传输，其他部件必须等待。当多个部件频繁争用总线时，系统性能将受到严重制约，形成总线瓶颈。</li>
</ul>
<h3>8.3.2 双总线结构</h3>
<p>在 CPU 与主存之间设置一条专用的<strong>主存总线</strong>（存储器总线），同时保留一条<strong>I/O 总线</strong>用于连接 I/O 设备。</p>
<ul>
<li><strong>优点</strong>：CPU 与主存之间的高频率数据交换不再占用 I/O 总线，I/O 设备的数据传输也不会阻塞 CPU 访存，两条总线可以并行工作。</li>
<li><strong>缺点</strong>：需要增加一个<strong>通道</strong>或桥接器来协调两条总线之间的数据交互。</li>
</ul>
<h3>8.3.3 三总线结构</h3>
<p>在双总线结构基础上，进一步增加一条 <strong>DMA 总线</strong>（或称高速 I/O 总线），用于连接高速外设（如高速磁盘控制器、图形适配器），使高速 I/O 设备可以直接与主存进行高带宽的数据传输，而不必经过较慢的 I/O 总线。</p>
<pre><code>CPU ←→ 主存总线 ←→ 主存
              ↑
          桥接器
              ↓
高速 I/O ←→ 高速总线（DMA总线）
              ↑
          桥接器
              ↓
低速 I/O ←→ I/O 总线
</code></pre>
<h3>8.3.4 高性能总线结构</h3>
<p>现代计算机系统通常采用"南北桥"或类似的层次化总线体系结构：</p>
<ul>
<li><strong>北桥</strong>连接 CPU、主存和高速图形接口（如 PCIe x16），处理高带宽数据流。</li>
<li><strong>南桥</strong>连接各种中低速 I/O 设备（USB、SATA、网卡、声卡等）。</li>
<li>北桥和南桥之间通过专用的高速内部总线互连。</li>
</ul>
<p>这种层次化设计使得高速设备和低速设备各走各的通道，避免相互干扰。</p>
<hr />
<h2>8.4 常用总线标准</h2>
<blockquote>
<p>[!NOTE]
常用总线标准通常作为概念了解内容，选择题偶有考察，不需要死记硬背引脚和电气特性，重点掌握其串/并行分类和典型应用场景即可。</p>
</blockquote>
<table>
<thead>
<tr>
<th>总线标准</th>
<th>类型</th>
<th>主要特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ISA</strong></td>
<td>系统总线</td>
<td>早期 PC 标准总线，16 位，速度慢，已淘汰</td>
</tr>
<tr>
<td><strong>PCI</strong></td>
<td>系统总线/I/O 总线</td>
<td>32/64 位并行总线，支持即插即用，曾是 PC 主流</td>
</tr>
<tr>
<td><strong>PCIe</strong></td>
<td>高速串行总线</td>
<td>采用点对点高速串行连接，按通道数分为 x1/x4/x8/x16，当前主流标准</td>
</tr>
<tr>
<td><strong>USB</strong></td>
<td>I/O 总线</td>
<td>通用串行总线，支持热插拔，广泛用于外设连接</td>
</tr>
<tr>
<td><strong>SATA</strong></td>
<td>I/O 总线</td>
<td>串行 ATA，用于连接硬盘和光驱</td>
</tr>
<tr>
<td><strong>I2C / SPI</strong></td>
<td>片内/板级总线</td>
<td>嵌入式系统常用的低速串行通信总线</td>
</tr>
</tbody>
</table>
<hr />
<h2>8.5 本章重点回顾</h2>
<p>:::important[总线带宽计算]
总线带宽 = 总线工作频率 x (总线宽度 / 8)，单位为 B/s。注意区分"总线时钟周期"和"总线传输周期"（后者可能包含多个时钟周期）。
:::</p>
<p>:::important[三种仲裁方式的核心区别]
链式查询用一条 BG 线串行传递，优先级固定；计数器查询用地址线轮询，优先级可调；独立请求每个设备独立连线，速度最快但硬件开销最大。
:::</p>
<p>:::important[同步与异步定时]
同步定时靠公共时钟统一协调，简单高效但受限于最慢设备；异步定时靠握手信号事件驱动，灵活适配不同速度设备但控制复杂。
:::</p>
<hr />
<h2>8.6 历年真题与易错题集</h2>
<h3>8.6.1 总线概念与分类</h3>
<p><strong>【1】下列总线标准中属于串行总线的是（ ）。</strong>（2020-2021第二学期B / 计组2019秋18级测试）
A. PCI
B. USB
C. EISA
D. ISA
<strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：PCI 是一种早期的并行系统总线标准，其地址线与数据线均为并行传输方式。</li>
<li><strong>B [正确]</strong>：USB 即通用串行总线，属于典型的串行总线标准，采用串行通信方式。</li>
<li><strong>C [错误]</strong>：EISA 是扩展工业标准结构总线，属于早期的并行总线标准。</li>
<li><strong>D [错误]</strong>：ISA 是工业标准结构总线，也是早期微型计算机中使用的并行总线标准。</li>
</ul>
<p><strong>【2】下列选项中的英文缩写均为总线标准的是（ ）。</strong>（2020-2021第2学期A）
A. PCI、CRT、USB、EISA
B. ISA、CPI、VESA、EISA
C. ISA、SCSI、RAM、MIPS
D. ISA、EISA、PCI、PCI-Express
<strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：PCI、USB 和 EISA 都是总线标准，但 CRT 代表阴极射线管，是早期的显示器显示技术，不属于总线标准。</li>
<li><strong>B [错误]</strong>：ISA、VESA 和 EISA 是总线标准，但 CPI 表示执行每条指令所需的平均时钟周期数，是衡量处理器性能的指标，不属于总线标准。</li>
<li><strong>C [错误]</strong>：ISA 和 SCSI 是总线标准，但 RAM 是随机存取存储器，MIPS 代表每秒百万条指令或代表一种指令集架构，它们不属于总线标准。</li>
<li><strong>D [正确]</strong>：ISA、EISA、PCI 和 PCI-Express 均为计算机中常见的总线标准。</li>
</ul>
<p><strong>【3】在计算机系统中，多个系统部件之间信息传送的公共通路称为总线。就其所传送信息的性质而言，在公共通路上传送的信息包括数据总线、（ ）总线、（ ）总线。</strong>（2020-2021第二学期B）</p>
<ul>
<li><strong>正确答案</strong>：控制、地址</li>
<li><strong>解析</strong>：系统总线按逻辑功能划分为三大类：数据总线（DB）、地址总线（AB）、控制总线（CB）。</li>
</ul>
<p><strong>【4】系统总线中地址线的功能是（ ）。</strong>（2020-2021第2学期A）
A. 选择主存单元地址
B. 选择进行信息传输的设备
C. 选择外存地址
D. 选择指定主存和 I/O 设备接口电路的地址
<strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：地址线不仅用于指明主存的存储单元地址，还用于指明外设接口的端口地址，因此只选择主存单元地址不全面。</li>
<li><strong>B [错误]</strong>：选择进行信息传输的设备通常需要通过地址线指定的接口地址或通过控制总线中的片选信号来进行，仅描述为选择设备是不够准确和完整的。</li>
<li><strong>C [错误]</strong>：CPU 无法直接通过系统地址线来寻址外存储器的内部物理地址，而是寻址外存储器控制器接口的端口地址。</li>
<li><strong>D [正确]</strong>：系统总线中的地址线用于单向传输 CPU 发出的地址信号，用以指定 CPU 要访问的主存储器单元或输入输出设备接口电路的端口地址。</li>
</ul>
<p><strong>【5】并行总线比串行总线传输速度快。（判断题）</strong>（南阳理工学院C卷）</p>
<ul>
<li><strong>正确答案</strong>：错</li>
<li><strong>解析</strong>：早期观念认为并行总线一次传多位，速度必然快于串行。但在极高频率下，并行总线的多条信号线之间会产生严重的信号干扰（串扰）和时钟同步偏差（信号偏斜），导致其频率很难进一步提升。现代高速总线（如 PCIe、SATA、USB 3.0）普遍采用高速串行技术，其传输速度远超传统的并行总线。</li>
</ul>
<h3>8.6.2 仲裁与定时控制</h3>
<p><strong>【1】在集中式总线控制中，响应速度最快的是（ ）。</strong>（计组2019秋18级测试）
A. 链式查询
B. 计数器定时查询
C. 独立请求
D. 分组链式查询
<strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：链式查询方式（又称菊花链方式）中，总线许可信号是通过串行级联的设备逐个向下传递的，响应速度在集中式总线控制中最慢。</li>
<li><strong>B [错误]</strong>：计数器定时查询方式通过计数器计数并轮询设备，其响应速度取决于计数器的初值和查询逻辑，响应速度适中，慢于独立请求方式。</li>
<li><strong>C [正确]</strong>：独立请求方式中，每个设备都有专属的请求线和响应线直接连接到总线控制器，控制器能够并行接收并立即处理请求，因此响应速度最快。</li>
<li><strong>D [错误]</strong>：分组链式查询是链式查询的改进版，虽然缩短了链的长度，但每个分组内部仍然采用串行级联方式，响应速度明显慢于独立请求方式。</li>
</ul>
<p><strong>【2】下列总线仲裁方式中，可能产生“饿死”现象的是（ ）。</strong>（南阳理工学院C卷）
A. 固定优先级仲裁
B. 轮询仲裁
C. 独立请求仲裁
D. 分布式仲裁
<strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：在固定优先级仲裁方式下（如链式查询方式），优先级结构是静态分配且固定不变的。当高优先级的设备频繁且连续地发出总线请求时，低优先级的设备将一直被屏蔽而无法获取总线控制权，从而产生“饿死”现象。</li>
<li><strong>B [错误]</strong>：轮询仲裁（或循环优先级仲裁）的优先级是随着总线的使用而动态改变的，每个设备都有均等的机会获得总线使用权，因此不会产生“饿死”现象。</li>
<li><strong>C [错误]</strong>：独立请求仲裁可以通过总线控制器内部的调度算法（例如公平轮询或优先级调度）进行分配，虽然使用固定优先级算法时也会有类似情况，但其本身支持灵活的防饿死设计，并不实现绝对意义的必然“饿死”。在单项选择中，固定优先级仲裁是导致“饿死”的最典型方式。</li>
<li><strong>D [错误]</strong>：分布式仲裁采用分布式竞争和优先级自举等机制，设备通过共享的控制线相互协商获取总线，设计上通常会避免单一设备长期独占总线，一般不会产生“饿死”现象。</li>
</ul>
<p><strong>【3】总线的定时方式分为（ ）、异步、（ ）方式。</strong>（2020-2021第2学期A）</p>
<ul>
<li><strong>正确答案</strong>：同步、半同步</li>
<li><strong>解析</strong>：总线定时（通信）方式四大类：同步定时、异步定时、半同步定时、分离式通信。</li>
</ul>
<p><strong>【4】同步通讯之所以比异步通信具有较高的传输速率，是因为（ ）。</strong>（2020-2021第二学期B）
A. 同步通信不需要应答信号且总线长度较短
B. 同步通信用一个公共的时钟信号进行同步
C. 同步通信中，各部件的存取时间接近
D. 以上各项因素的综合结果
<strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：同步通信确实不需要应答信号，且由于时钟偏斜等原因总线长度不能过长。但这一选项只描述了部分原因，不够全面。</li>
<li><strong>B [错误]</strong>：同步通信使用一个公共的时钟信号进行同步是其核心特征之一，这也使其控制逻辑简单、数据传输紧凑，但这一选项同样不够全面。</li>
<li><strong>C [错误]</strong>：同步通信要求各部件的存取时间比较接近，以使得各设备能在预定时间内完成数据传送，这是采用同步通信的前提，但也仅是速率高的原因之一。</li>
<li><strong>D [正确]</strong>：同步通信通过公共时钟进行同步，免去了异步通信中的握手应答开销，且其总线长度通常较短、各部件的存取时间也比较接近。这些因素的综合作用共同促成了同步通信具有较高的传输速率。</li>
</ul>
<p><strong>【5】同步通信方式中，同步时钟信号可由多设备提供。（判断题）</strong>（2022-2023-2考试）</p>
<ul>
<li><strong>正确答案</strong>：错</li>
<li><strong>解析</strong>：同步定时方式中，系统必须提供一个统一的公共时钟信号来协调总线上所有部件的工作，该时钟通常由总线控制器单一提供，不可多头提供。</li>
</ul>
<p><strong>【6】异步通信方式下，非互锁方式中请求信号和应答信号的撤销互不依赖，需要（ ）次握手。</strong>（南阳理工学院C卷）</p>
<ul>
<li><strong>正确答案</strong>：2</li>
<li><strong>解析</strong>：在非互锁方式中，主设备的请求信号和从设备的应答信号的撤销互不依赖，只涉及请求发送与应答响应两次状态切换（即 2 次握手交互）。</li>
</ul>
<p><strong>【7】在一次总线事务中，主设备只需给出一个首地址，从设备就能从首地址开始若干的连续单元读出或写入多个数据，这种事务方式称为（ ）。</strong>（2022-2023-2考试）</p>
<ul>
<li><strong>正确答案</strong>：突发传输（或猝发传输、Burst）</li>
<li><strong>解析</strong>：突发传输大幅减少了地址传输的开销，在总线读取连续数据块（如 Cache 行填充、DMA 数据块传输）时极为高效。</li>
</ul>
<h3>8.6.3 综合计算大题与分析</h3>
<p><strong>【1】基础总线带宽计算</strong>（计组2019秋18级测试）
<strong>题干</strong>：设 32 位总线时钟频率为 66 MHz，设一个总线周期包含 4 个时钟周期，则总线的带宽是___ B/s。</p>
<ul>
<li><strong>解答</strong>：66 M</li>
<li><strong>解析</strong>：
<ol>
<li>数据宽度 $W = 32 \text{ bit} = 4 \text{ B}$。</li>
<li>一个总线周期包含 4 个时钟周期，因此总线传输频率 $f_{\text{bus}} = 66 \text{ MHz} \div 4 = 16.5 \text{ MHz}$。</li>
<li>带宽 $= f_{\text{bus}} \times W = 16.5 \text{ M/s} \times 4 \text{ B} = 66 \text{ MB/s}$。</li>
</ol>
</li>
</ul>
<p><strong>【2】复杂总线带宽计算</strong>（2022-2023-2考试）
<strong>题干</strong>：假设某系统总线在一个总线周期中并行传输 4 字节信息，一个总线周期占用两个时钟周期，总线时钟频率为 10 MHz，单时钟传输次数为 0.5 次，则总线带宽是___。</p>
<ul>
<li><strong>解答</strong>：20 MB/s</li>
<li><strong>解析</strong>：
这道题的表述存在冗余：
方法一：“单时钟传输 0.5 次”且“10 MHz 时钟频率”，意味着每秒发生 $10\text{ M} \times 0.5 = 5\text{ M}$ 次传输。每次传 4 B，带宽 $= 5\text{ M} \times 4\text{ B} = 20 \text{ MB/s}$。
方法二：“一个总线周期两个时钟”意味着总线周期频率为 $10\text{ MHz} \div 2 = 5\text{ MHz}$。每周期传 4 B，带宽 $= 5\text{ M} \times 4\text{ B} = 20 \text{ MB/s}$。</li>
</ul>
<p><strong>【3】总线文件传输时间</strong>（南阳理工学院C卷）
<strong>题干</strong>：一个 PCI 总线的带宽为 32 MB/s，传输一个大小为 256 MB 的视频文件需要的时间是___ s。</p>
<ul>
<li><strong>解答</strong>：8</li>
<li><strong>解析</strong>：时间 $= \text{总数据量} \div \text{带宽} = 256 \text{ MB} \div 32 \text{ MB/s} = 8 \text{ s}$。</li>
</ul>
<p><strong>【4】地址/数据复用总线计算</strong>（南阳理工学院C卷）
<strong>题干</strong>：某 16 位地址/数据复用的同步总线中，总线时钟频率为 8 MHz，每个总线事务只传输一个数据，需要 4 个时钟周期，该总线的可寻址空间、数据传输率是多少？</p>
<ul>
<li><strong>解答</strong>：
<ol>
<li><strong>可寻址空间</strong>：地址线有 16 位（由地址/数据复用得来），因此寻址能力为 $2^{16} = 64 \text{ K}$。若按字节编址则为 64 KB。</li>
<li><strong>数据传输率（带宽）</strong>：总线周期频率 $= 8 \text{ MHz} \div 4 = 2 \text{ MHz}$。数据线宽度为 16 位（2 B）。传输率 $= 2 \text{ MHz} \times 2 \text{ B} = 4 \text{ MB/s}$。</li>
</ol>
</li>
</ul>
<p><strong>【5】同步总线最大传输率及提升方案</strong>（2022-2023-2考试简答）
<strong>题干</strong>：假设一个同步总线的时钟频率为 100 MHz，总线带宽为 32 位，每个时钟周期传输一个数据，该总线的最大数据传输速率为多少？若要将总线带宽提高一倍，有哪几种可行方案？</p>
<ul>
<li><strong>解答</strong>：
<ol>
<li>最大传输率 $= 100 \text{ MHz} \times (32 \div 8) \text{ B} = 400 \text{ MB/s}$。</li>
<li>提升带宽的方案（列出其一即可）：
<ul>
<li>将总线数据位宽增加一倍（32位升级为64位）。</li>
<li>将总线时钟频率增加一倍（100 MHz 提升至 200 MHz）。</li>
<li>采用双倍数据率技术（DDR，即在时钟的上升沿和下降沿各传输一次数据，使每个时钟周期传输两个数据）。</li>
</ul>
</li>
</ol>
</li>
</ul>
<p><strong>【6】双总线结构下的数据通路微操作</strong>（计组2019秋18级测试 综合大题）
<strong>考法说明</strong>：题目给出一幅含有 CPU 内部寄存器（PC, IR, DR, AR 等）、ALU 以及挂载主存的双总线结构通路图。要求写出某条指令（如 <code>SUB R1, R0</code>）的完整指令周期流程与微操作控制信号。</p>
<hr />
<h2>习题8：课后习题详解</h2>
<p><img src="https://img.picr2.online/1782821194947_1.webp" alt="习题截图1" />
<img src="https://img.picr2.online/1782821204252_2.webp" alt="习题截图2" />
<img src="https://img.picr2.online/1782821205947_3.webp" alt="习题截图3" /></p>
<hr />
<h3>习题8.1 解释下列名词</h3>
<p><strong>题干：</strong></p>
<p>总线、片内总线、系统总线、I/O 总线、外部总线、总线控制器、总线接口、地址总线、数据总线、控制总线、总线复用、主设备、从设备、总线主控、总线标准、三态门、总线宽度、总线时钟、总线传输周期、总线带宽、DDR、QDR、总线事务、突发传输、串行传送、并行传送、波特率、数传率、全双工、半双工、数据传输模式、广播与广集、总线仲裁、集中式仲裁、分布式仲裁、总线定时、同步定时、异步定时、总线事务分离、总线结构、全局总线、局部总线、桥接器、北桥芯片、南桥芯片、FSB 总线、QPI 总线、DMI 总线、PCI 总线、PCIe 总线、AMBA 总线、USB 总线。</p>
<p><strong>解析</strong>：</p>
<table>
<thead>
<tr>
<th>名词</th>
<th>解释</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>总线</strong></td>
<td>连接两个或多个功能部件、按统一规则传送地址、数据和控制信息的一组共享信号线及其协议。</td>
</tr>
<tr>
<td><strong>片内总线</strong></td>
<td>位于芯片内部的总线，用于连接处理器核、片上缓存、片上存储器、片上外设等模块。</td>
</tr>
<tr>
<td><strong>系统总线</strong></td>
<td>连接 CPU、主存和主要 I/O 控制器的总线，通常包括地址、数据和控制三类信号。</td>
</tr>
<tr>
<td><strong>I/O 总线</strong></td>
<td>主要连接 I/O 控制器、外设接口或外设的总线，常用于 CPU/主存系统与外部设备之间的数据交换。</td>
</tr>
<tr>
<td><strong>外部总线</strong></td>
<td>计算机系统与外部设备或外部模块之间的互连总线，如 USB、外部扩展接口等。</td>
</tr>
<tr>
<td><strong>总线控制器</strong></td>
<td>负责总线请求、仲裁、授权、定时和控制信号产生的控制部件。</td>
</tr>
<tr>
<td><strong>总线接口</strong></td>
<td>功能部件接入总线的接口电路，完成信号匹配、协议转换、缓冲和控制。</td>
</tr>
<tr>
<td><strong>地址总线</strong></td>
<td>传送访问目标地址的信号线，用于指出存储单元或 I/O 端口的位置。</td>
</tr>
<tr>
<td><strong>数据总线</strong></td>
<td>传送数据、指令、状态码等数据信息的信号线，通常是双向的。</td>
</tr>
<tr>
<td><strong>控制总线</strong></td>
<td>传送读/写、时钟、中断、总线请求/允许、应答等控制信号的信号线。</td>
</tr>
<tr>
<td><strong>总线复用</strong></td>
<td>让同一组信号线在不同时间传送不同类型的信息，如地址/数据线复用，以减少引脚和信号线数量。</td>
</tr>
<tr>
<td><strong>主设备</strong></td>
<td>能主动发起总线事务、申请并获得总线控制权的设备，如 CPU、DMA 控制器。</td>
</tr>
<tr>
<td><strong>从设备</strong></td>
<td>响应主设备访问请求的设备，如主存模块、I/O 接口。</td>
</tr>
<tr>
<td><strong>总线主控</strong></td>
<td>当前获得总线使用权并控制一次总线事务的主设备。</td>
</tr>
<tr>
<td><strong>总线标准</strong></td>
<td>规定总线机械、电气、功能、时序和协议特性的规范，保证不同设备可兼容互连。</td>
</tr>
<tr>
<td><strong>三态门</strong></td>
<td>输出可为 0、1 或高阻态的门电路；高阻态相当于与总线断开，适合多个设备共享总线。</td>
</tr>
<tr>
<td><strong>总线宽度</strong></td>
<td>总线一次可并行传送的位数，常指数据总线宽度，如 32 位、64 位。</td>
</tr>
<tr>
<td><strong>总线时钟</strong></td>
<td>同步总线中用于规定信号采样和状态转换节拍的时钟信号。</td>
</tr>
<tr>
<td><strong>总线传输周期</strong></td>
<td>完成一次基本总线传送所需的时间，可能包含地址阶段、等待阶段、数据阶段等。</td>
</tr>
<tr>
<td><strong>总线带宽</strong></td>
<td>单位时间内总线能传送的数据量，常用 B/s、MB/s、GB/s 表示。基本公式为：带宽 = 每次传送字节数 × 每秒传送次数。</td>
</tr>
<tr>
<td><strong>DDR</strong></td>
<td>Double Data Rate，双倍数据率；每个时钟周期传送两次数据，通常在上升沿和下降沿各传一次。</td>
</tr>
<tr>
<td><strong>QDR</strong></td>
<td>Quad Data Rate，四倍数据率；等效为每个时钟周期传送四次数据。</td>
</tr>
<tr>
<td><strong>总线事务</strong></td>
<td>主设备发起并由从设备响应的一次完整总线操作，如一次读、一次写或一次中断响应。</td>
</tr>
<tr>
<td><strong>突发传输</strong></td>
<td>主设备给出首地址后，连续传送多个相邻数据的数据传输方式。</td>
</tr>
<tr>
<td><strong>串行传送</strong></td>
<td>数据位按时间顺序逐位在一条或少数几条线上传送；线数少、距离可较远，但单次并行度低。</td>
</tr>
<tr>
<td><strong>并行传送</strong></td>
<td>多位数据同时在多条线上传送；近距离时吞吐量高，但线数多，易受偏斜、串扰和同步限制。</td>
</tr>
<tr>
<td><strong>波特率</strong></td>
<td>每秒传送的码元数。若每个码元只表示 1 位，则波特率等于 bit/s；若一个码元表示多位，则二者不同。</td>
</tr>
<tr>
<td><strong>数传率</strong></td>
<td>数据传输率，即单位时间内传送的有效数据位数，通常用 bit/s 表示。</td>
</tr>
<tr>
<td><strong>全双工</strong></td>
<td>通信双方可以同时发送和接收数据。</td>
</tr>
<tr>
<td><strong>半双工</strong></td>
<td>通信双方都能发送和接收，但同一时刻只能单向传输。</td>
</tr>
<tr>
<td><strong>数据传输模式</strong></td>
<td>数据通信的工作方式，可按方向分为单工、半双工、全双工，也可按组织方式分为串行、并行、突发等。</td>
</tr>
<tr>
<td><strong>广播与广集</strong></td>
<td>广播是一发多收，即一个主设备向多个设备发送同一信息；广集是多源信息被集中到一个接收端或公共检测点。</td>
</tr>
<tr>
<td><strong>总线仲裁</strong></td>
<td>多个主设备同时请求总线时，决定哪个设备获得总线使用权的过程。</td>
</tr>
<tr>
<td><strong>集中式仲裁</strong></td>
<td>由一个集中仲裁器统一接收请求并发出授权，典型方式有链式查询、计数器查询、独立请求。</td>
</tr>
<tr>
<td><strong>分布式仲裁</strong></td>
<td>没有单一集中仲裁器，各主设备通过分布式逻辑和仲裁规则共同决定总线使用权。</td>
</tr>
<tr>
<td><strong>总线定时</strong></td>
<td>规定总线事务中各信号何时有效、何时采样、何时撤销的时序控制方法。</td>
</tr>
<tr>
<td><strong>同步定时</strong></td>
<td>总线操作由统一时钟控制，各设备按时钟边沿或时钟周期配合工作。</td>
</tr>
<tr>
<td><strong>异步定时</strong></td>
<td>总线操作不依赖统一时钟，而通过请求、应答等握手信号协调。</td>
</tr>
<tr>
<td><strong>总线事务分离</strong></td>
<td>将一次总线事务的请求和响应分开，主设备发出请求后释放总线，从设备准备好后再返回结果，可提高总线利用率。</td>
</tr>
<tr>
<td><strong>总线结构</strong></td>
<td>计算机系统中各部件通过总线互连的组织方式，如单总线、双总线、三总线、层次总线等。</td>
</tr>
<tr>
<td><strong>全局总线</strong></td>
<td>连接系统中多个主要模块、影响范围较大的总线，常用于跨模块或跨层次通信。</td>
</tr>
<tr>
<td><strong>局部总线</strong></td>
<td>位于局部范围内、服务于少数高速部件的总线，如 CPU 与高速缓存或局部 I/O 之间的总线。</td>
</tr>
<tr>
<td><strong>桥接器</strong></td>
<td>连接两种总线或两个总线段的部件，完成协议转换、地址映射、缓冲和隔离。</td>
</tr>
<tr>
<td><strong>北桥芯片</strong></td>
<td>传统 PC 芯片组中连接 CPU、主存和高速图形/高速总线的芯片。现代系统中其功能多已集成进 CPU。</td>
</tr>
<tr>
<td><strong>南桥芯片</strong></td>
<td>传统 PC 芯片组中连接低速 I/O、磁盘、USB、音频、网络等外设控制器的芯片。</td>
</tr>
<tr>
<td><strong>FSB 总线</strong></td>
<td>Front Side Bus，传统 CPU 与北桥/主存控制器之间的前端总线。</td>
</tr>
<tr>
<td><strong>QPI 总线</strong></td>
<td>QuickPath Interconnect，Intel 的高速点对点互连，常用于处理器与处理器或处理器与芯片组之间。</td>
</tr>
<tr>
<td><strong>DMI 总线</strong></td>
<td>Direct Media Interface，Intel 平台中常用于 CPU/北桥功能与芯片组 I/O 部分之间的点对点连接。</td>
</tr>
<tr>
<td><strong>PCI 总线</strong></td>
<td>Peripheral Component Interconnect，一种并行局部 I/O 总线标准。</td>
</tr>
<tr>
<td><strong>PCIe 总线</strong></td>
<td>PCI Express，高速串行点对点扩展总线，采用通道 Lane 叠加提高带宽。</td>
</tr>
<tr>
<td><strong>AMBA 总线</strong></td>
<td>Advanced Microcontroller Bus Architecture，ARM 提出的片上总线标准族，如 AHB、APB、AXI。</td>
</tr>
<tr>
<td><strong>USB 总线</strong></td>
<td>Universal Serial Bus，通用串行总线，用于主机与外部设备之间的连接，支持热插拔和分层集线结构。</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题8.2 简要回答下列问题</h3>
<p><strong>题干：</strong></p>
<p>(1) 计算机系统为什么采用总线结构？
(2) 比较单总线、双总线、三总线结构的性能特点。
(3) 总线的信息传送方式有哪几种？各有什么特点？
(4) 集中式总线控制方式下，确定总线使用权优先级的方法有哪几种？它们各有什么特点？
(5) 什么是突发传输模式？采用突发传输模式有什么优点？
(6) 影响总线性能的因素有哪些？
(7) 总线结构和交换结构相比，有哪些优势和劣势？</p>
<p><strong>解析</strong>：</p>
<h4>(1) 计算机系统为什么采用总线结构？</h4>
<p>若每两个部件都单独连线，系统有 $n$ 个部件时可能需要大量点对点连接。采用总线后，多个部件共享一组通信线路：减少连线数量和硬件成本；便于模块化设计和标准化接口；便于扩展维护；通过仲裁支持多个主设备共享资源。代价是共享总线容易形成带宽瓶颈。</p>
<h4>(2) 比较单总线、双总线、三总线结构的性能特点</h4>
<table>
<thead>
<tr>
<th>结构</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>单总线</td>
<td>结构简单，成本低，扩展方便</td>
<td>竞争严重，带宽瓶颈明显</td>
</tr>
<tr>
<td>双总线</td>
<td>减少 CPU/主存与 I/O 的竞争，性能更高</td>
<td>需要桥接器，控制更复杂</td>
</tr>
<tr>
<td>三总线</td>
<td>高速与低速传输独立，并发性和吞吐率更高</td>
<td>硬件成本高，控制逻辑复杂</td>
</tr>
</tbody>
</table>
<h4>(3) 总线的信息传送方式</h4>
<p>按数据位组织：串行传送（逐位，线少适合远距）和并行传送（多位同时，短距吞吐量高）。按传输方向：单工、半双工、全双工。按定时：同步、异步、半同步。按事务组织：普通单次传送和突发传送。</p>
<h4>(4) 集中式总线控制方式</h4>
<p><strong>链式查询</strong>：BG 信号串行传递，离仲裁器越近优先级越高，线路少但优先级固定，可能"饿死"远端设备。<strong>计数器定时查询</strong>：仲裁器轮询设备编号，优先级可调，线路数量适中。<strong>独立请求</strong>：每个设备独立请求/允许线，响应最快、优先级灵活，但线数多、硬件开销大。</p>
<h4>(5) 突发传输模式</h4>
<p>主设备只给出一个首地址，随后连续传送多个相邻数据。优点：减少地址传送开销；仲裁和控制开销被多个数据分摊；适合 Cache 块等连续数据传送。</p>
<h4>(6) 影响总线性能的因素</h4>
<p>总线宽度、时钟频率、每周期传送次数（DDR/QDR）、事务开销、是否支持突发、地址/数据线是否复用、仲裁方式、定时方式、总线长度和电气特性、主从设备响应速度。</p>
<h4>(7) 总线结构与交换结构对比</h4>
<p>总线结构胜在简单、低成本、易标准化和天然广播能力；交换结构胜在高并发、高带宽和可扩展性，但成本高、控制复杂。</p>
<hr />
<h3>习题8.3 单选题（考研真题）</h3>
<p><strong>题干：</strong></p>
<h4>(1) [2011] 在系统总线的数据线上，不可能传输的是__________。</h4>
<p>A. 指令
B. 操作数
C. 握手（应答）信号
D. 中断类型号</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令从主存取出时是二进制数据，可以经数据总线传输。</li>
<li><strong>B [错误]</strong>：操作数是数据总线最典型的传输内容。</li>
<li><strong>C [正确]</strong>：握手/应答属于控制信号，应在控制总线上传输。</li>
<li><strong>D [错误]</strong>：中断响应过程中，中断源可把中断类型号放到数据总线上。</li>
</ul>
<h4>(2) [2014] 一次总线事务中，主设备只需给出一个首地址，从设备就能从首地址开始的若干连续单元读出或写入多个数据。这种总线事务方式称为__________。</h4>
<p>A. 并行传输
B. 串行传输
C. 突发传输
D. 同步传输</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：并行传输强调多位数据同时传送。</li>
<li><strong>B [错误]</strong>：串行传输强调逐位顺序传送。</li>
<li><strong>C [正确]</strong>：突发传输正符合题意。</li>
<li><strong>D [错误]</strong>：同步传输强调由统一时钟定时。</li>
</ul>
<h4>(3) [2009] 假定某系统总线在一个总线周期中并行传输 4 字节信息，一个总线周期占用两个时钟周期，总线时钟频率为 10MHz，则总线带宽是__________。</h4>
<p>A. 10MB/s
B. 20MB/s
C. 40MB/s
D. 80MB/s</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：每秒总线周期数 $= 10\times10^6 / 2 = 5\times10^6$，带宽 $= 5\times10^6 \times 4\text{B} = 20\text{MB/s}$</p>
<h4>(4) [2012] 某同步总线的时钟频率为 100MHz，宽度为 32 位，地址/数据线复用，每传输一个地址或数据占用一个时钟周期。若该总线支持突发传输方式，则一次"主存写"总线事务传输 128 位数据所需要的时间至少是__________。</h4>
<p>A. 20ns
B. 40ns
C. 50ns
D. 80ns</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：时钟周期 $= 10\text{ns}$。$128/32=4$ 个数据周期，突发只需 1 次地址。总 $1+4=5$ 周期，时间 $= 5\times10\text{ns}=50\text{ns}$。</p>
<h4>(5) [2023] 某存储总线宽度为 64 位，总线时钟频率为 1GHz，在总线上传输一个数据或地址需要一个时钟周期，不支持突发传送方式。若通过该总线连接 CPU 和主存，主存每次准备一个 64b 数据需要 6ns，主存块大小为 32B，则读取一个主存块需要的时间是__________。</h4>
<p>A. 8ns
B. 11ns
C. 26ns
D. 32ns</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：时钟 $= 1\text{ns}$。不支持突发，每次读 $8\text{B}$ 需单独发地址。$32/8=4$ 次，一次 $1+6+1=8\text{ns}$，总 $= 32\text{ns}$。</p>
<h4>(6) [2014] 某同步总线采用数据线和地址线复用方式，其中地址/数据线有 32 条，总线时钟频率为 66MHz，每个时钟周期传送两次数据，该总线的最大数据传输速率是__________。</h4>
<p>A. 132 MB/s
B. 264 MB/s
C. 528 MB/s
D. 1056 MB/s</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：$66\times10^6 \times 2 \times 4\text{B} = 528\text{MB/s}$</p>
<h4>(7) [2024] 存储器总线的时钟频率为 420MHz，总线宽度为 64 位，每个时钟周期传送两次数据，支持突发传输，最多传 8 次，第一个时钟传地址和读写命令，从第 4~7 个时钟连续传 8 次。总线带宽最大传输速率为__________。</h4>
<p>A. 3.84GB/s
B. 6.72GB/s
C. 30.72GB/s
D. 53.76GB/s</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：一次突发传 $8\times8\text{B}=64\text{B}$，占用 $7$ 个时钟。每秒事务数 $= 420\times10^6 / 7 = 60\times10^6$，带宽 $= 60\times10^6 \times 64\text{B} = 3.84\text{GB/s}$。</p>
<h4>(8) [2015] 下列有关总线定时的叙述中，错误的是__________。</h4>
<p>A. 异步通信方式中，全互锁方式最慢
B. 异步通信方式中，非互锁方式的可靠性最差
C. 同步通信方式中，同步时钟信号可由多设备提供
D. 半同步通信方式中，握手信号的采样由同步时钟控制</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：同步通信使用统一时钟，不能由多设备随意提供。</p>
<h4>(9) [2016] 下列关于总线设计的叙述中，错误的是__________。</h4>
<p>A. 并行总线比串行总线传输速度快
B. 采用信号线复用技术可以减少信号线数量
C. 采用突发传输方式可提高总线数据传输速率
D. 采用分离事务通信方式可提高总线利用率</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：高速串行总线可通过差分传输、多通道达到很高速度，不一定慢于并行总线。</p>
<h4>(10) [2021] 下列关于总线的叙述中，错误的是__________。</h4>
<p>A. 总线是在两个或多个部件之间进行数据交换的传输介质
B. 同步总线由时钟信号定时，时钟频率不一定等于工作频率
C. 异步总线由握手信号定时，一次握手过程完成一位数据交换
D. 突发传送总线事务可以在总线上连续传送多个数据</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：异步总线一次握手通常完成一次数据单元交换（如 1 字节、1 字），不一定是 1 位。</p>
<h4>(11) [2018] 下列选项中，可提高同步总线数据传输速率的是__________。</h4>
<p>Ⅰ. 增加总线宽度
Ⅱ. 提高总线工作频率
Ⅲ. 支持突发传输
Ⅳ. 采用地址/数据线复用方式</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅱ、Ⅲ
C. 仅 Ⅲ、Ⅳ
D. Ⅰ、Ⅱ、Ⅲ 和 Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：Ⅰ、Ⅱ、Ⅲ 均可提高速率。Ⅳ 可减少引脚，但分时使用不能提高速率。</p>
<h4>(12) [2013] 下列选项中，用于设备和设备控制器（I/O 接口）之间互连的接口标准是__________。</h4>
<p>A. PCI
B. USB
C. AGP
D. PCI-Express</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：USB 用于主机/控制器与外部设备之间互连。</p>
<h4>(13) [2020] QPI 总线是一种点对点全双工同步串行总线，若 QPI 时钟频率为 2.4GHz，则总线带宽为__________。</h4>
<p>A. 4.8GB/s
B. 9.6GB/s
C. 19.2GB/s
D. 38.4GB/s</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：每秒传送次数 $= 2.4\times10^9 \times 2 = 4.8\times10^9$，每次有效数据 $16\text{bit}=2\text{B}$，全双工再 $\times 2$，带宽 $= 4.8\times10^9 \times 2\text{B} \times 2 = 19.2\text{GB/s}$。</p>
<hr />
<h3>习题8.4</h3>
<p><strong>题干：</strong></p>
<p>假设一个同步总线的时钟频率为 100MHz，总线宽度为 32 位，每个时钟周期传输一个数据，该总线的最大数据传输速率是多少？若要将总线带宽提高一倍，有哪几种可行方案？</p>
<p><strong>解析</strong>：</p>
<p>带宽 $= 100\times10^6 \times 4\text{B} = 400\text{MB/s}$。提高一倍的方法：1) 频率升至 200MHz；2) 宽度增至 64 位；3) 采用 DDR 技术每周期传 2 次。</p>
<hr />
<h3>习题8.5</h3>
<p><strong>题干：</strong></p>
<p>采用异步通信方式传送 ASCII 时，若数据位为 8 位，校验位为 1 位，停止位为 1 位，当波特率为 4800bit/s 时，字符传送的速率是多少？每个数据位的时间长度是多少？数据位的传送速率是多少？</p>
<p><strong>解析</strong>：</p>
<p>每字符 $= 1+8+1+1=11$ 位。字符速率 $= 4800/11 \approx 436.36$ 字符/s。每数据位 $= 1/4800 \approx 208.33\mu\text{s}$。数据位速率 $= 4800 \times 8/11 \approx 3.49\text{kb/s}$。</p>
<hr />
<h3>习题8.6</h3>
<p><strong>题干：</strong></p>
<p>有 4 个设备 A、B、C、D，响应优先级为 D &gt; B &gt; A &gt; C，画出串行链式排队电路。</p>
<p><strong>解析</strong>：BG 链连接顺序：总线控制器 $\to$ D $\to$ B $\to$ A $\to$ C。BG 从最高优先级 D 开始串行传递，有请求则截获，否则传下一设备。</p>
<hr />
<h3>习题8.7</h3>
<p><strong>题干：</strong></p>
<p>有 4 个设备 A、B、C、D，响应优先级为 A &gt; B &gt; C &gt; D，画出独立请求方式的排队电路。</p>
<p><strong>解析</strong>：每个设备有独立请求线 $BR_A \sim BR_D$ 和允许线 $BG_A \sim BG_D$，仲裁器按优先级 A &gt; B &gt; C &gt; D 直接授权。</p>
<hr />
<h3>习题8.8</h3>
<p><strong>题干：</strong></p>
<p>某 16 位地址/数据复用的同步总线中，总线时钟频率为 8MHz，每个总线事务只传输一个数据，需要 4 个时钟周期。该总线的可寻址空间、数据传输速率各是多少？</p>
<p><strong>解析</strong>：地址线 16 位，按字节编址可寻址 $2^{16}\text{B}=64\text{KB}$。每秒事务数 $= 8\times10^6/4=2\times10^6$，数据宽度 $2\text{B}$，速率 $= 4\text{MB/s}$。</p>
<hr />
<h3>习题8.9</h3>
<p><strong>题干：</strong></p>
<p>某 32 位同步总线中，总线时钟频率为 50MHz，支持突发传输，每个时钟周期可传送一个地址或数据。存储器读事务时序：地址 1 周期、等待 3 周期、8 数据 8 周期；写事务时序：地址 1 周期、等待 2 周期、8 数据 8 周期、恢复 2 周期。求读/写存储器的数据传输速率。</p>
<p><strong>解析</strong>：</p>
<p>时钟 $= 20\text{ns}$。一次突发传 $8 \times 4\text{B} = 32\text{B}$。读 $1+3+8=12$ 周期 $= 240\text{ns}$，速率 $\approx 133.33\text{MB/s}$。写 $1+2+8+2=13$ 周期 $= 260\text{ns}$，速率 $\approx 123.08\text{MB/s}$。</p>
<hr />
<h3>习题8.10</h3>
<p><strong>题干：</strong></p>
<p>某 64 位同步总线支持突发传输，每个时钟周期可传送一个地址或数据。存储器每存取一个数据需两个时钟周期，突发长度 $\le 4$。求两种情况下总线和存储器提供的数据传输速率：(1) 每事务传 32 位数据；(2) 每事务含 4 个数据期。</p>
<p><strong>解析</strong>：</p>
<p>(1) 事务 $= 1+1=2$ 周期，传 $4\text{B}$。总线 $= 2\text{B/周期}$，存储器 $= 2\text{B/周期}$。</p>
<p>(2) 事务 $= 1+4=5$ 周期，传 $4 \times 8\text{B}=32\text{B}$。总线 $= 6.4\text{B/周期}$，存储器 $= 4\text{B/周期}$。系统瓶颈为存储器，实际 $= 4\text{B/周期}$。</p>
<hr />
<h3>习题8.11</h3>
<p><strong>题干：</strong></p>
<p>总线系统支持 $4 \sim 16$ 个 32 位字的数据块访问。总线时钟 200MHz，64 位同步总线，每 64 位传 1 时钟，发地址需 1 时钟。每总线操作间需 2 总线周期。数据块最初 4 字访问 200ns，随后每 4 字 20ns，总线传输可与读后续字重叠。用 4 字和 16 字块传输 256 字，计算总线带宽和每秒事务次数。</p>
<p><strong>解析</strong>：</p>
<p>时钟 $= 5\text{ns}$，总线 $8\text{B}$，每数据周期传 2 字。总数据 $1024\text{B}$。事务间隔 $10\text{ns}$。</p>
<p><strong>4 字块</strong>：$64$ 次事务，一次 $5+200+10=215\text{ns}$，总 $14390\text{ns}$，带宽 $\approx 71.16\text{MB/s}$，$4.45\times10^6$ 次/s。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>指令流水线</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%8C%87%E4%BB%A4%E6%B5%81%E6%B0%B4%E7%BA%BF/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%8C%87%E4%BB%A4%E6%B5%81%E6%B0%B4%E7%BA%BF/</id>
    <published>2026-06-30T04:24:59.000Z</published>
    <updated>2026-06-30T04:24:59.000Z</updated>
    <summary>整理指令流水线的基本概念与性能计算公式，详解结构冲突、数据冲突、控制冲突三种流水线冲突的产生原因与处理方法，以及动态分支预测与指令级并行技术。</summary>
    <content type="html"><![CDATA[<h1>第七章：指令流水线</h1>
<p>本章整理计算机中提升指令执行效率的核心技术：指令流水线。流水线技术通过将一条指令的执行过程分解为多个子阶段，使多条指令在不同阶段上重叠执行，从而显著提高 CPU 的吞吐率。</p>
<p>本章的核心考点集中在流水线性能计算（加速比、吞吐率）以及三种流水线冲突的识别与处理方法。</p>
<hr />
<h2>7.1 流水线概述</h2>
<h3>7.1.1 流水线的基本概念</h3>
<p>在非流水线的 CPU 中，每条指令必须完整执行完毕后，下一条指令才能开始。这种串行执行方式意味着 CPU 的各个功能部件在大部分时间里处于空闲状态。</p>
<p><strong>流水线</strong>借鉴了工业装配流水线的思想：将一条指令的执行过程划分为若干个功能段（阶段），每个功能段由独立的硬件部件完成。当第一条指令完成第一个阶段进入第二个阶段时，第二条指令就可以进入第一个阶段开始执行，依此类推。</p>
<p>:::tip[流水线的核心优势]
流水线并不缩短单条指令的执行时间（延迟），而是通过多条指令的重叠执行来提高单位时间内完成指令的数量（吞吐率）。在理想情况下，$k$ 段流水线的吞吐率是非流水线方式的 $k$ 倍。
:::</p>
<h3>7.1.2 MIPS 指令流水线</h3>
<p>MIPS 处理器的经典五级流水线将每条指令的执行过程划分为以下五个阶段：</p>
<table>
<thead>
<tr>
<th>阶段</th>
<th>缩写</th>
<th>功能描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>取指</strong></td>
<td>IF</td>
<td>根据 PC 从指令存储器中取出指令，PC 自增</td>
</tr>
<tr>
<td><strong>译码/读寄存器</strong></td>
<td>ID</td>
<td>对指令进行译码，同时从寄存器堆中读出源操作数</td>
</tr>
<tr>
<td><strong>执行/计算地址</strong></td>
<td>EX</td>
<td>ALU 执行运算或计算访存有效地址</td>
</tr>
<tr>
<td><strong>访存</strong></td>
<td>MEM</td>
<td>对于 load/store 指令，访问数据存储器进行读写</td>
</tr>
<tr>
<td><strong>写回</strong></td>
<td>WB</td>
<td>将运算结果或从存储器读出的数据写回目标寄存器</td>
</tr>
</tbody>
</table>
<pre><code>指令 1:  IF → ID → EX → MEM → WB
指令 2:       IF → ID → EX  → MEM → WB
指令 3:            IF → ID  → EX  → MEM → WB
指令 4:                 IF  → ID  → EX  → MEM → WB
</code></pre>
<p>在理想流水线中，每个时钟周期都有一条指令完成（流出），每个时钟周期也有一条新指令进入流水线。</p>
<h3>7.1.3 流水线的时空图表示</h3>
<p>时空图是描述流水线工作状态的直观工具，横轴为时间（以时钟周期为单位），纵轴为流水线的各个功能段。</p>
<p><strong>时空图示例</strong>（4 段流水线执行 6 条指令）：</p>
<pre><code>gantt
    title 4段流水线时空图 (6条指令)
    dateFormat X
    axisFormat %s
    
    section 取指(IF)
    I1 :0, 1
    I2 :1, 2
    I3 :2, 3
    I4 :3, 4
    I5 :4, 5
    I6 :5, 6
    
    section 译码(ID)
    I1 :1, 2
    I2 :2, 3
    I3 :3, 4
    I4 :4, 5
    I5 :5, 6
    I6 :6, 7
    
    section 执行(EX)
    I1 :2, 3
    I2 :3, 4
    I3 :4, 5
    I4 :5, 6
    I5 :6, 7
    I6 :7, 8
    
    section 写回(WB)
    I1 :3, 4
    I2 :4, 5
    I3 :5, 6
    I4 :6, 7
    I5 :7, 8
    I6 :8, 9
</code></pre>
<p>从时空图可以直观看到：</p>
<ul>
<li>前 $k-1$ 个周期（$k$ 为段数）是流水线的<strong>装入阶段</strong>（建立时间），流水线尚未满载。</li>
<li>从第 $k$ 个周期开始，流水线进入<strong>满载阶段</strong>，每个周期都有一条指令流出。</li>
<li>最后 $k-1$ 个周期是<strong>排空阶段</strong>。</li>
</ul>
<hr />
<h2>7.2 流水线数据通路</h2>
<h3>7.2.1 单周期数据通路的流水改造</h3>
<p>将单周期数据通路改造为流水线数据通路，关键在于在相邻的两个功能段之间插入<strong>流水线寄存器</strong>（又称段间寄存器、锁存器）。</p>
<p>五级流水线需要 4 组流水线寄存器：</p>
<pre><code>IF/ID 寄存器 → ID/EX 寄存器 → EX/MEM 寄存器 → MEM/WB 寄存器
</code></pre>
<p>流水线寄存器的作用：</p>
<ul>
<li>将前一个阶段的处理结果和控制信号暂存，供下一个阶段在下一个时钟周期使用。</li>
<li>实现各功能段之间的物理隔离，使不同指令的不同阶段可以在同一时刻独立工作。</li>
</ul>
<h3>7.2.2 流水线中的控制信号及传递</h3>
<p>在流水线结构中，控制信号在 ID 阶段由译码逻辑统一生成，但不同的控制信号在不同的阶段才会被使用。因此，控制信号必须跟随指令一起在流水线寄存器中逐级向后传递，在需要使用的那个阶段从对应的流水线寄存器中取出。</p>
<p>例如：</p>
<ul>
<li><code>RegWrite</code> 信号在 ID 阶段生成，但直到 WB 阶段才用于控制写回操作。</li>
<li><code>MemRead</code> / <code>MemWrite</code> 信号在 MEM 阶段才使用。</li>
<li><code>ALUOp</code> 信号在 EX 阶段使用。</li>
</ul>
<h3>7.2.3 指令在流水线中的执行过程</h3>
<p>以 <code>lw $t0, 0($s0)</code> 为例，该指令在五级流水线中的数据流动过程：</p>
<ol>
<li><strong>IF 阶段</strong>：PC 送入指令存储器读出指令，指令存入 IF/ID 寄存器，PC+4 同时保存。</li>
<li><strong>ID 阶段</strong>：从 IF/ID 寄存器取出指令进行译码，读出 <code>$s0</code> 的值，符号扩展立即数 <code>0</code>，控制信号生成并存入 ID/EX 寄存器。</li>
<li><strong>EX 阶段</strong>：ALU 将 <code>$s0</code> 的值与立即数 <code>0</code> 相加，得到访存有效地址，结果存入 EX/MEM 寄存器。</li>
<li><strong>MEM 阶段</strong>：以有效地址访问数据存储器，读出数据存入 MEM/WB 寄存器。</li>
<li><strong>WB 阶段</strong>：将 MEM/WB 寄存器中的数据写回寄存器堆的 <code>$t0</code>。</li>
</ol>
<hr />
<h2>7.3 流水线冲突与处理</h2>
<p>流水线冲突是指由于某些条件限制，流水线中的下一条指令无法在预定的时钟周期内正常执行的情况。冲突会导致流水线<strong>停顿</strong>（stall），降低流水线效率。</p>
<h3>7.3.1 流水线冲突的分类</h3>
<p>流水线冲突分为三类：</p>
<table>
<thead>
<tr>
<th>冲突类型</th>
<th>产生原因</th>
<th>又称</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>结构冲突</strong></td>
<td>多条指令在同一时刻争用同一硬件资源</td>
<td>资源冲突</td>
</tr>
<tr>
<td><strong>数据冲突</strong></td>
<td>后续指令需要使用前驱指令尚未产生的结果数据</td>
<td>数据相关</td>
</tr>
<tr>
<td><strong>控制冲突</strong></td>
<td>分支/跳转指令改变了程序执行方向，导致已取入的后续指令无效</td>
<td>控制相关</td>
</tr>
</tbody>
</table>
<h3>7.3.2 结构冲突处理</h3>
<p>结构冲突的典型场景：指令存储器与数据存储器共用同一个存储器端口时，取指阶段与访存阶段在同一时刻都需要访问存储器。</p>
<p><strong>常见解决方法</strong>：</p>
<ul>
<li><strong>资源重复配置</strong>：将指令存储器和数据存储器分开（哈佛结构），使取指和访存互不干扰。这是现代处理器的标准做法。</li>
<li><strong>流水线停顿</strong>：当冲突发生时，暂停流水线中后续指令的推进，等待资源释放。</li>
</ul>
<h3>7.3.3 控制冲突处理</h3>
<p>当 CPU 执行到分支指令（如 <code>beq</code>）时，分支的目标地址和分支条件通常要到 EX 阶段甚至 MEM 阶段才能确定。而在此之前，流水线已经按照 PC+4 的顺序取入了后续指令。如果分支成立，这些已取入的指令全部无效，必须被清除（称为<strong>分支延迟惩罚</strong>）。</p>
<p><strong>常见处理方法</strong>：</p>
<ol>
<li>
<p><strong>冻结/排空流水线</strong>：检测到分支指令后，暂停取指，等到分支结果确定后再取下一条指令。简单但性能损失大。</p>
</li>
<li>
<p><strong>预测分支不发生</strong>：总是假设分支不成立，继续按顺序取指。如果预测正确则无性能损失；如果预测错误，则清除已取入的无效指令（冲刷流水线）。</p>
</li>
<li>
<p><strong>预测分支发生</strong>：总是假设分支成立，立即取分支目标处的指令。</p>
</li>
<li>
<p><strong>延迟分支</strong>：编译器将与分支无关的指令调度到分支指令之后的"延迟槽"中，无论分支是否成立，延迟槽中的指令都会被执行。</p>
</li>
</ol>
<h3>7.3.4 插入气泡解决数据冲突</h3>
<p>数据冲突是流水线中最常见的冲突类型。考虑以下指令序列：</p>
<pre><code>add $t0, $t1, $t2    # 指令 1：在 WB 阶段写 $t0
sub $t3, $t0, $t4    # 指令 2：在 ID 阶段读 $t0
</code></pre>
<p>指令 2 在 ID 阶段需要读取 <code>$t0</code> 的新值，但此时指令 1 尚未到达 WB 阶段完成写回。如果不做处理，指令 2 读到的将是 <code>$t0</code> 的旧值，导致运算结果错误。</p>
<p><strong>插入气泡（stall）</strong>：当检测到数据冲突时，硬件自动在流水线中插入空操作（NOP），使后续指令延迟若干周期，直到所需数据被正确写回。</p>
<pre><code>add $t0, $t1, $t2:  IF  ID  EX  MEM  WB
                             ↓ 数据在 WB 阶段才写回
sub $t3, $t0, $t4:  IF  ID  ○   ○    ID  EX  MEM  WB
                        需要读$t0     等数据就绪后重新读
</code></pre>
<p>其中 <code>○</code> 表示插入的气泡（空闲周期）。插入气泡虽然能保证正确性，但会严重降低流水线吞吐率。</p>
<h3>7.3.5 使用重定向解决数据冲突</h3>
<p><strong>重定向</strong>（又称数据旁路、转发）是现代流水线处理器解决数据冲突的核心技术。其基本思想是：不必等到前驱指令将结果写回寄存器堆，而是在结果刚刚产生时，就通过专用的旁路通路直接转发给需要该数据的后续指令。</p>
<pre><code>add $t0, $t1, $t2:  IF  ID  EX  MEM  WB
                             ↓ EX 阶段末尾结果已产生
sub $t3, $t0, $t4:  IF  ID  EX  MEM  WB
                             ↑ 通过旁路直接获取 $t0 的新值
</code></pre>
<p>:::important[重定向的典型转发路径]</p>
<ul>
<li><strong>EX/MEM 转发</strong>：前一条 ALU 指令的运算结果从 EX/MEM 流水线寄存器直接转发给下一条指令的 EX 阶段输入。</li>
<li><strong>MEM/WB 转发</strong>：前一条指令的结果从 MEM/WB 流水线寄存器转发给间隔一条指令的 EX 阶段输入。</li>
</ul>
<p>注意：对于 <code>lw</code> 指令，数据要到 MEM 阶段末尾才从存储器中读出。如果紧随其后的指令在 EX 阶段就需要该数据，即使有重定向也无法完全消除停顿，此时仍需插入一个气泡。这种情况称为<strong>加载-使用型数据冲突</strong>（load-use hazard）。
:::</p>
<h3>7.3.6 动态分支预测技术</h3>
<p>静态预测方法（如总是预测不跳转）在分支频繁且方向不固定的程序中效果不佳。动态分支预测技术根据分支指令的<strong>历史执行记录</strong>来预测本次的分支方向。</p>
<h4>1 位预测器</h4>
<p>使用 1 位的历史信息记录上一次该分支是否跳转：</p>
<ul>
<li>如果上一次跳转了，则预测本次也跳转。</li>
<li>如果上一次没跳转，则预测本次也不跳转。</li>
</ul>
<p>缺点：对于循环结构，若预测器已在之前的执行中学到“跳转”，则在循环最后一次退出时会预测错误；当再次进入该循环时，由于保留了上次退出时的“不跳转”状态，第一次迭代又会预测错误。</p>
<h4>2 位预测器</h4>
<p>使用 2 位饱和计数器记录历史信息，有四种状态：</p>
<pre><code>强不跳转(00) ←→ 弱不跳转(01) ←→ 弱跳转(10) ←→ 强跳转(11)
</code></pre>
<p>预测规则：计数值 $\ge 2$（即高位为 1）时预测跳转，否则预测不跳转。实际跳转则计数器加 1（上限饱和），不跳转则减 1（下限饱和）。</p>
<p>2 位预测器的优势在于：必须连续两次预测错误才会改变预测方向，从而过滤掉偶发的方向变化，显著提高对循环等规律性分支的预测准确率。</p>
<h3>7.3.7 流水线性能分析</h3>
<p>:::important[流水线性能计算公式（高权重考点）]</p>
<p>设流水线有 $k$ 个功能段，需要执行 $n$ 条指令。</p>
<p><strong>1. 流水线执行时间</strong></p>
<p>若各段执行时间相等，均为 $\Delta t$（即一个时钟周期）：</p>
<p>$$T_{\text{流水}} = k \cdot \Delta t + (n - 1) \cdot \Delta t = (k + n - 1) \cdot \Delta t$$</p>
<p>其中 $k \cdot \Delta t$ 是第一条指令流过全部 $k$ 个段的时间（建立时间），之后每个 $\Delta t$ 流出一条指令。</p>
<p>若各段执行时间不等，分别为 $\Delta t_1, \Delta t_2, \ldots, \Delta t_k$，则流水线时钟周期取最慢段：</p>
<p>$$\Delta t_{\max} = \max(\Delta t_1, \Delta t_2, \ldots, \Delta t_k)$$</p>
<p><strong>关于流水线寄存器（锁存器）延迟 $d$：</strong></p>
<ul>
<li><strong>理想公式（不含延迟）</strong>：$T_{\text{流水}} = \sum_{i=1}^{k} \Delta t_i + (n-1) \cdot \Delta t_{\max}$</li>
<li><strong>实际公式（考虑锁存延迟）</strong>：每个功能段的实际耗时应为 $\Delta t_{\max} + d$。此时流水线时钟周期变为 $\Delta t_{\max} + d$。第一条指令耗时 $k \cdot (\Delta t_{\max} + d)$，剩余 $n-1$ 条指令各耗时 $\Delta t_{\max} + d$，总时间变为：</li>
</ul>
<p>$$T_{\text{流水}} = k \cdot (\Delta t_{\max} + d) + (n-1) \cdot (\Delta t_{\max} + d) = (k + n - 1) \cdot (\Delta t_{\max} + d)$$考试时需仔细审题是否给出了锁存器延迟时间。</p>
<p><strong>2. 非流水线（串行）执行时间</strong></p>
<p>若各段时间相等：
$$T_{\text{串行}} = n \cdot k \cdot \Delta t$$</p>
<p>若各段时间不等：
$$T_{\text{串行}} = n \cdot \sum_{i=1}^{k} \Delta t_i$$</p>
<p><strong>3. 加速比</strong></p>
<p>$$S = \frac{T_{\text{串行}}}{T_{\text{流水}}}$$</p>
<p>各段时间相等时：</p>
<p>$$S = \frac{n \cdot k \cdot \Delta t}{(k + n - 1) \cdot \Delta t} = \frac{nk}{k + n - 1}$$</p>
<p>当 $n \to \infty$ 时：</p>
<p>$$S = \frac{nk}{k + n - 1} = \frac{k}{1 + \frac{k-1}{n}} \xrightarrow{n\to\infty} k$$</p>
<p>即理想加速比等于流水线段数。</p>
<p><strong>4. 吞吐率</strong></p>
<p>吞吐率是单位时间内流水线完成的指令条数：</p>
<p>$$TP = \frac{n}{T_{\text{流水}}}$$</p>
<p>各段时间相等时：</p>
<p>$$TP = \frac{n}{(k + n - 1) \cdot \Delta t}$$</p>
<p>当 $n \to \infty$ 时：</p>
<p>$$TP = \frac{n}{(k + n - 1) \cdot \Delta t} = \frac{1}{\left(1 + \frac{k-1}{n}\right) \cdot \Delta t} \xrightarrow{n\to\infty} \frac{1}{\Delta t}$$</p>
<p>即理想吞吐率为每个时钟周期完成一条指令。</p>
<p><strong>5. 效率</strong></p>
<p>流水线效率是指各功能段的实际利用率：</p>
<p>$$E = \frac{n \cdot k \cdot \Delta t}{k \cdot (k + n - 1) \cdot \Delta t} = \frac{n}{k + n - 1}$$</p>
<p>当 $n \to \infty$ 时：</p>
<p>$$E = \frac{n}{k + n - 1} = \frac{1}{1 + \frac{k-1}{n}} \xrightarrow{n\to\infty} 1$$</p>
<p>即理想效率为 100%。
:::</p>
<p>:::tip[做题注意事项]</p>
<ol>
<li>区分"各段时间相等"和"各段时间不等"两种情况，后者的流水线时钟周期由最慢段决定。</li>
<li>题目若问"至少"的时钟周期，答案为最慢段的执行时间。</li>
<li>有的题目会额外考虑流水线寄存器的延迟（锁存延迟），此时 $\Delta t_{\max}$ 要加上锁存延迟。
:::</li>
</ol>
<hr />
<h2>7.4 流水线的异常与中断</h2>
<p>在流水线中，异常和中断的处理比非流水线结构更加复杂，因为同一时刻流水线中有多条指令在并行执行，异常可能在任意阶段发生。</p>
<h3>异常的精确处理</h3>
<p>现代处理器要求实现<strong>精确异常</strong>：当某条指令发生异常时，该指令之前的所有指令必须全部完成执行并写回结果，该指令之后的所有指令必须被取消（不能产生任何副作用）。</p>
<p>流水线中处理异常的基本步骤：</p>
<ol>
<li>在发生异常的流水线阶段标记该指令为"异常"。</li>
<li>将异常信息随指令一起在流水线寄存器中向后传递。</li>
<li>当异常指令到达<strong>可提交点</strong>（通常在写回 WB 或专门的 Commit 阶段）时，统一进行异常处理：保存 PC（异常返回地址）、冲刷流水线中该指令之后的所有指令、跳转到异常处理程序入口。不能在异常发生的早期直接处理，以防该指令原本就该因前面的分支被冲刷而取消。</li>
</ol>
<h3>外部中断的处理</h3>
<p>外部中断通常在每条指令的 WB 阶段末尾进行检测和响应。处理器完成当前正在 WB 阶段的指令，冲刷流水线中其余指令，保存断点后转入中断服务程序。</p>
<hr />
<h2>7.5 指令级并行技术</h2>
<p>指令级并行是指在一个时钟周期内同时发射和执行多条指令，以进一步提高处理器的吞吐率。</p>
<blockquote>
<p>[!NOTE]
超标量、超流水线和 VLIW 等高级指令级并行（ILP）技术在基础计组考试中通常只考查基本名词解释或概念判断，一般不会涉及复杂的排流水线计算。</p>
</blockquote>
<h3>超标量流水线</h3>
<p>超标量处理器在每个时钟周期内可以同时发射多条指令（如 2 发射、4 发射），这些指令进入多条并行的流水线中同时执行。</p>
<ul>
<li>需要配置多套功能部件（多个 ALU、多个访存端口等）。</li>
<li>需要更复杂的硬件逻辑来检测指令间的依赖关系。</li>
<li>理想情况下，$m$ 发射的超标量处理器吞吐率是普通流水线的 $m$ 倍。</li>
</ul>
<h3>超流水线</h3>
<p>超流水线将流水线的每个功能段进一步细分为更多的子段，从而提高时钟频率。</p>
<ul>
<li>段数增加使得时钟周期缩短，单位时间内流出的指令增多。</li>
<li>但段数过多会增加流水线寄存器开销，且冲突惩罚增大（冲刷更多阶段）。</li>
</ul>
<h3>超长指令字（VLIW）</h3>
<p>由编译器在编译阶段就将多个可以并行执行的操作打包到一条超长指令字中，硬件直接并行执行这些操作。</p>
<ul>
<li>将指令调度和并行性检测的责任从硬件转移到了编译器。</li>
<li>硬件设计简单，但对编译器的优化能力要求极高。</li>
</ul>
<hr />
<hr />
<h2>7.6 典型例题：流水线性能计算（含时空图）</h2>
<p><strong>【例题】</strong> 某计算机采用 5 段指令流水线，各段分别为取指（IF）、译码（ID）、执行（EX）、访存（MEM）、写回（WB）。若每段执行时间均为 10ns，忽略锁存延迟，现有 4 条指令依次进入流水线。
（1）画出该指令序列执行的时空图。
（2）计算该指令序列的总执行周期和总执行时间。
（3）计算该流水线的实际吞吐率和实际加速比。</p>
<p><strong>【解析】</strong></p>
<p><strong>（1）时空图绘制</strong></p>
<pre><code>gantt
    title 5段流水线执行4条指令时空图
    dateFormat X
    axisFormat %s
    
    section IF
    I1 :0, 1
    I2 :1, 2
    I3 :2, 3
    I4 :3, 4
    
    section ID
    I1 :1, 2
    I2 :2, 3
    I3 :3, 4
    I4 :4, 5
    
    section EX
    I1 :2, 3
    I2 :3, 4
    I3 :4, 5
    I4 :5, 6
    
    section MEM
    I1 :3, 4
    I2 :4, 5
    I3 :5, 6
    I4 :6, 7
    
    section WB
    I1 :4, 5
    I2 :5, 6
    I3 :6, 7
    I4 :7, 8
</code></pre>
<p><strong>（2）周期与时间计算</strong></p>
<ul>
<li>建立时间（排满前）：第 1 条指令需要 5 个时钟周期完成。</li>
<li>满载流出时间：随后的 3 条指令每隔 1 个周期流出一条，共需 3 个周期。</li>
<li>总周期数 $T = k + n - 1 = 5 + 4 - 1 = 8$ 个周期。</li>
<li>总执行时间 $= 8 \times 10\text{ns} = 80\text{ns}$。</li>
</ul>
<p><strong>（3）吞吐率与加速比</strong></p>
<ul>
<li><strong>实际吞吐率（TP）</strong> $= n \div \text{总时间} = 4 \div 80\text{ns} = 4 \div (80 \times 10^{-9}) = \frac{4}{80} \times 10^9 = 5 \times 10^7$ 条/秒。</li>
<li><strong>非流水线（串行）时间</strong> $= 4 \times 5 \times 10\text{ns} = 200\text{ns}$。</li>
<li><strong>实际加速比（S）</strong> $= \text{串行时间} \div \text{流水线时间} = 200\text{ns} \div 80\text{ns} = 2.5$。</li>
</ul>
<hr />
<h2>7.7 典型例题：流水线冲突分析</h2>
<p><strong>【例题 1：结构冲突】</strong> 某 5 级流水线处理器，采用冯·诺依曼结构（指令和数据统一存放在主存中），主存只有一个访问端口。当执行指令序列 <code>I1(Load), I2, I3, I4</code> 时，会发生什么冲突？</p>
<ul>
<li><strong>解析</strong>：会发生<strong>结构冲突</strong>。<code>I1</code> 在第 4 周期进入 MEM 阶段，需要访问主存读取数据；此时 <code>I4</code> 正好在第 4 周期进入 IF 阶段，需要访问主存读取指令。由于主存只有一个端口，两者在第 4 周期发生资源争用。必须在 <code>I4</code> 的 IF 阶段前插入一个气泡。</li>
</ul>
<p><strong>【例题 2：Load-use 气泡与转发】</strong> 分析下列汇编指令序列在支持重定向（旁路）的 5 级流水线中的冲突情况：</p>
<pre><code>I1: lw  $t0, 0($s0)    # 从内存读入 $t0
I2: add $t1, $t0, $t2  # EX阶段需要 $t0
I3: sub $t3, $t0, $t4  # EX阶段需要 $t0
</code></pre>
<ul>
<li><strong>解析</strong>：
<ul>
<li><code>I1</code> 与 <code>I2</code> 存在 <strong>Load-use 数据冲突</strong>。<code>I1</code> 的数据要到其 MEM 阶段末尾（第4周期末）才能读出，而 <code>I2</code> 在其 EX 阶段（第4周期初）就需要该数据。仅靠重定向无法消除时间差（时光不能倒流），硬件必须<strong>在 <code>I2</code> 之前插入 1 个气泡（Stall）</strong>，使其 EX 阶段推迟到第 5 周期，然后再通过 MEM/WB 旁路接收数据。</li>
<li><code>I1</code> 与 <code>I3</code> 的冲突则<strong>可以完全通过重定向解决</strong>。<code>I3</code> 的 EX 阶段在第 5 周期，此时 <code>I1</code> 已处于 WB 阶段（数据已读出并位于 MEM/WB 寄存器中），直接旁路转发即可，无需插入气泡。</li>
</ul>
</li>
</ul>
<p><strong>【例题 3：控制冲突与分支惩罚】</strong> 若分支指令在 EX 阶段末尾计算出目标地址并确定分支方向，若预测分支不跳转但实际发生了跳转，会带来几个周期的分支延迟惩罚？</p>
<ul>
<li><strong>解析</strong>：分支在 EX 阶段确定，说明它已经通过了 IF、ID 两个阶段，进入了第 3 个阶段（EX）。在它处于 EX 阶段时，其后面的第一条指令在 ID 阶段，第二条指令在 IF 阶段。既然实际跳转了，这两条顺序取入的指令都是错误的，必须被冲刷（Flush）。因此，损失了 <strong>2 个时钟周期</strong>（即 2 条无效指令带来的惩罚）。</li>
</ul>
<hr />
<h2>7.8 本章易错点与重点回顾</h2>
<p>:::important[流水线时钟周期的确定]
流水线的时钟周期必须满足最慢功能段的执行时间要求，即 $\Delta t_{\text{时钟}} = \max(\Delta t_1, \Delta t_2, \ldots, \Delta t_k)$。如果考虑流水线寄存器的锁存延迟 $d$，则 $\Delta t_{\text{时钟}} = \Delta t_{\max} + d$。
:::</p>
<p>:::important[流水线加速比不可能超过段数]
理论上，$k$ 段流水线的最大加速比为 $k$（当指令条数趋近无穷时逼近）。在有限指令数和存在冲突的实际情况下，加速比严格小于 $k$。
:::</p>
<p>:::important[三种冲突的区分与处理策略]</p>
<ul>
<li>结构冲突：解决方案为资源重复配置（如分离指令与数据存储器）。</li>
<li>数据冲突：优先使用重定向（旁路转发）消除，无法消除的加载-使用冲突需插入气泡。</li>
<li>控制冲突：使用分支预测（静态或动态）减少分支惩罚；延迟分支由编译器配合解决。
:::</li>
</ul>
<hr />
<h2>7.9 历年真题与易错题集</h2>
<h3>7.9.1 概念与选择题</h3>
<p><strong>【1】流水CPU是由一系列称为“段”的处理线路组成的，一个m段流水线稳定时的CPU的吞吐能力，与m个并行部件的CPU的吞吐能力相比（ ）。</strong>（2020-2021第2学期A）
A. 具有同等水平的吞吐能力
B. 不具备同等水平的吞吐能力
C. 吞吐能力大于前者的吞吐能力
D. 吞吐能力小于前者的吞吐能力
<strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：在理想满载且没有冲突的情况下，一个 $m$ 段流水线稳定运行时，每个时钟周期可以流出一条指令，其吞吐能力为每个周期 1 条指令。而 $m$ 个并行部件的 CPU，如果其协同机制是每 $m$ 个周期同时流出 $m$ 条指令，则平均下来吞吐率同样是每个周期 1 条指令。因此在特定的协同模型下，两者的吞吐能力具有同等水平。</li>
<li><strong>B [错误]</strong>：在规定两者的协同工作机制等同（如并行部件协同每 $m$ 个周期完成 $m$ 条指令）时，两者的吞吐率相同，具备同等水平的吞吐能力。</li>
<li><strong>C [错误]</strong>：在理想模型下，流水线稳定时的吞吐率最多与相同并行规模的 CPU 持平。若每个并行部件都能在单个周期内独立且并行地完成一条指令，其吞吐率可达 $m$ 条指令/时钟周期，此时流水 CPU 的吞吐能力会小于前者，绝不可能大于前者。</li>
<li><strong>D [错误]</strong>：根据前面的分析，在相同的协同工作模型下，流水 CPU 与并行 CPU 具有同等水平的吞吐率，因此无法判定其吞吐能力必定小于前者。</li>
</ul>
<p><strong>【2】把一个重复的过程分解为若干个子过程，前一子过程为下一子过程创造执行条件，每个子过程可以与其他子过程同时执行的技术是（ ）技术。</strong>（2020-2021第二学期B）</p>
<ul>
<li><strong>正确答案</strong>：流水线</li>
<li><strong>解析</strong>：这是流水线技术最标准的书面定义，核心在于时间上的重叠执行与空间上子部件的独立并行。</li>
</ul>
<p><strong>【3】某计算机的指令流水线由4个功能段组成，指令流经各功能段的时间（忽略各功能段之间的缓存时间）分别为95ns、80ns、70ns和60ns，则该计算机的CPU时钟周期至少是（ ）。</strong>（2022-2023-2考试 / 2020-2021第2学期A变体）
A. 95ns
B. 80ns
C. 70ns
D. 60ns
<strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：流水线的时钟周期（即流水线的节拍时长）受制于所有功能段中最慢（即耗时最长）的一段，以此保证该段的操作能够在单个时钟周期内顺利且完整地执行。题目中耗时最长的功能段为 95ns，因此该计算机的 CPU 时钟周期至少是 95ns。</li>
<li><strong>B [错误]</strong>：如果时钟周期设为 80ns，那么耗时 95ns 的流水段无法在单个时钟周期内完成，这会导致流水线无法正常工作。</li>
<li><strong>C [错误]</strong>：如果时钟周期设为 70ns，那么耗时 95ns 和 80ns 的流水段均无法在单个时钟周期内完成。</li>
<li><strong>D [错误]</strong>：如果时钟周期设为 60ns，只有耗时最少的 60ns 流水段能在一个时钟周期内完成，其余所有流水段均会因为周期过短而无法完成相应操作。</li>
</ul>
<p><strong>【4】请简述影响流水线的三种相关？</strong>（2020-2021第2学期A / 计组2019秋18级测试填空题）</p>
<ul>
<li><strong>解答</strong>：
影响流水线性能的有<strong>控制相关</strong>、<strong>结构相关</strong>（或结构冲突）和<strong>数据相关</strong>（或数据冲突）：
<ol>
<li><strong>控制相关</strong>：主要是指遇到了程序转移指令（如跳转或中断）时，流水线因为无法确定下一步正确的执行路径，而不能正确且连续地处理后继指令。</li>
<li><strong>结构相关（资源相关）</strong>：是指多条指令进入流水线后，在同一机器时钟周期内争用同一个功能部件（如算术部件或存储器端口）所发生的物理资源冲突。</li>
<li><strong>数据相关</strong>：即前一条指令执行过程中的数据结果是后一条指令执行过程中所需的源数据。由于流水线在时间上重叠，后一条指令急需该数据时前一条指令尚未将其写回，导致数据依赖冲突。</li>
</ol>
</li>
</ul>
<h3>7.9.2 综合计算题</h3>
<p><strong>【1】四级流水线吞吐率计算</strong>（2022-2023-2考试）
<strong>题干</strong>：某 CPU 主频为 1.03 GHz，采用 4 级指令流水线，每个流水段的执行需要 1 个时钟周期。假定 CPU 执行了 100 条指令，在其执行过程中，没有发生任何流水线阻塞，此时流水线的吞吐率为___条指令/秒。</p>
<ul>
<li><strong>解答</strong>：$1 \times 10^9$（或 $10^9$）</li>
<li><strong>解析</strong>：
<ol>
<li>计算执行 100 条指令所需的总时钟周期数：
公式：$T = k + n - 1$，其中段数 $k=4$，指令数 $n=100$。
执行 100 条指令共需时钟周期数 = $4 + 100 - 1 = 103$ 个时钟周期。</li>
<li>计算所需总时间：
时钟频率 $f = 1.03 \text{ GHz} = 1.03 \times 10^9 \text{ Hz}$。
总时间 $t = 103 \div (1.03 \times 10^9) = \frac{103}{1.03} \times 10^{-9} = 100 \times 10^{-9}$ 秒。</li>
<li>计算实际吞吐率（TP）：
$\text{TP} = n \div t = 100 \div (100 \times 10^{-9}) = \frac{100}{100} \times 10^{9} = 1 \times 10^9$ 条指令/秒。</li>
</ol>
<ul>
<li><em>(注：吞吐率计算严格遵守公式，切勿直接以主频作答。只有当指令数 $n$ 趋于无穷大时，流水线的吞吐率才无限逼近于主频 $1.03 \times 10^9$)</em>。</li>
</ul>
</li>
</ul>
<hr />
<h2>习题7：课后习题详解</h2>
<p>以下按题目顺序给出题干与详细答案解析。</p>
<p><img src="https://img.picr2.online/1782819446803_1.webp" alt="习题截图1" />
<img src="https://img.picr2.online/1782819445542_2.webp" alt="习题截图2" />
<img src="https://img.picr2.online/1782819447136_3.webp" alt="习题截图3" />
<img src="https://img.picr2.online/1782819458620_4.webp" alt="习题截图4" /></p>
<hr />
<h3>习题7.1 解释下列名词</h3>
<p><strong>题干：</strong></p>
<p>流水线技术、指令流水线、运算流水线、流水寄存器、流水时空图、数据冲突、结构冲突、控制相关、先写后读冲突、先读后写冲突、写后写冲突、气泡、重定向、延迟槽、动态分支预测、超标量技术、超流水线技术、动态多发射技术、静态多发射技术、同步中断、异步中断。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>流水线技术</strong>：把一个重复执行的处理过程分成若干个相互衔接的子过程，各子过程由不同功能部件并行工作，使多个任务在不同阶段重叠执行，从而提高系统吞吐率。</li>
<li><strong>指令流水线</strong>：把一条指令的执行过程划分为取指、译码、执行、访存、写回等阶段，使多条指令在不同阶段同时推进的 CPU 执行组织方式。</li>
<li><strong>运算流水线</strong>：把一个复杂运算划分为多个子运算阶段，使连续输入的数据在不同运算阶段中重叠处理的流水线。例如浮点加法、乘法流水线。</li>
<li><strong>流水寄存器</strong>：位于相邻流水段之间的寄存器，用来暂存上一段产生的数据、控制信号和状态信息，使这些信息在下一个时钟周期进入下一流水段。</li>
<li><strong>流水时空图</strong>：用横轴表示时间/时钟周期、纵轴表示指令或任务，表格中标出每条指令在各周期所处流水段，用来描述流水线重叠执行过程的图。</li>
<li><strong>数据冲突</strong>：后续指令需要使用前面指令尚未产生或尚未写回的数据，导致按原计划执行会得到错误结果的流水线冒险。</li>
<li><strong>结构冲突</strong>：同一时钟周期内多条指令竞争同一个硬件资源，例如统一存储器同时被 IF 取指和 MEM 访存使用。</li>
<li><strong>控制相关</strong>：分支、跳转、中断等改变程序控制流的指令使后续取指地址无法及时确定，由此产生的相关关系。</li>
<li><strong>先写后读冲突（RAW）</strong>：前一条指令先写某寄存器，后一条指令后读该寄存器；若后一条读得太早，会读到旧值。这是真数据相关。</li>
<li><strong>先读后写冲突（WAR）</strong>：前一条指令先读某寄存器，后一条指令后写该寄存器；若后一条写得过早，会破坏前一条应读的旧值。这属于反相关。</li>
<li><strong>写后写冲突（WAW）</strong>：两条指令都写同一个目的寄存器，若后发射指令先完成写回，会破坏程序规定的最终写入顺序。这属于输出相关。</li>
<li><strong>气泡</strong>：为解决冒险而插入流水线的空操作周期，相当于让部分流水段空转，使相关指令等待到安全时刻再执行。</li>
<li><strong>重定向</strong>：也称转发或旁路，把某流水段刚产生但尚未写回寄存器堆的结果，直接送到需要该结果的后续流水段输入端。</li>
<li><strong>延迟槽</strong>：分支或跳转指令之后的一条或若干条位置，这些位置中的指令不管分支是否发生都先被执行，用来隐藏控制冒险带来的延迟。</li>
<li><strong>动态分支预测</strong>：处理器运行时根据分支指令过去的执行历史预测其是否转移及目标地址，以减少控制冒险阻塞。</li>
<li><strong>超标量技术</strong>：在一个时钟周期内同时发射多条指令，并由多个功能部件并行执行，从而使理想 CPI 小于 1 的技术。</li>
<li><strong>超流水线技术</strong>：把原有流水段进一步细分成更多、更短的流水段，以提高时钟频率和指令吞吐率。</li>
<li><strong>动态多发射技术</strong>：由硬件在运行时判断多条指令之间是否独立，并动态决定同一周期发射哪些指令。</li>
<li><strong>静态多发射技术</strong>：由编译器在编译时把可并行执行的指令打包或调度好，硬件按编译器安排进行多发射，例如 VLIW/EPIC 的思想。</li>
<li><strong>同步中断</strong>：由当前正在执行的指令内部原因引起、可由指令执行位置精确定位的异常，例如溢出、缺页、非法指令。</li>
<li><strong>异步中断</strong>：由处理器外部事件引起、与当前指令没有固定同步关系的中断，例如 I/O 完成、时钟中断、外部设备请求。</li>
</ol>
<hr />
<h3>习题7.2 选择题（考研真题）</h3>
<hr />
<h4>7.2（1）</h4>
<p><strong>题干：</strong>
[2013] 某 CPU 主频为 $1.03\text{ GHz}$，采用 4 级指令流水线，每个流水段的执行需要 1 个时钟周期。假定 CPU 执行了 100 条指令，在其执行过程中，没有发生任何流水线阻塞，此时流水线的吞吐率为__________。</p>
<p>A. $0.25 \times 10^9$ 条指令/秒
B. $0.97 \times 10^9$ 条指令/秒
C. $1.0 \times 10^9$ 条指令/秒
D. $1.03 \times 10^9$ 条指令/秒</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ol>
<li>4 级流水线执行 $n$ 条指令且无阻塞时，需要的时钟周期数为：
$$ k+n-1=4+100-1=103 $$</li>
<li>CPU 主频为 $1.03\times 10^9\text{ Hz}$，所以执行 103 个周期的时间为：
$$ T=\frac{103}{1.03\times 10^9}=10^{-7}\text{ s} $$</li>
<li>吞吐率为：
$$ TP=\frac{100}{10^{-7}}=1.0\times 10^9\text{ 条指令/秒} $$</li>
</ol>
<ul>
<li><strong>A [错误]</strong>：$0.25\times10^9$ 近似把 4 级流水线理解为每 4 个周期完成 1 条，忽略了流水线填满后可每周期完成 1 条。</li>
<li><strong>B [错误]</strong>：$0.97\times10^9$ 接近 $100/103\times1.0\times10^9$，但主频是 $1.03\times10^9$，两者相乘正好为 $1.0\times10^9$。</li>
<li><strong>C [正确]</strong>：严格按 $100/(103/f)$ 计算得到 $1.0\times10^9$。</li>
<li><strong>D [错误]</strong>：$1.03\times10^9$ 是理想最大吞吐率，即足够多指令且忽略装入/排空影响时的极限值，本题只有 100 条指令，需计入 3 个额外周期。</li>
</ul>
<hr />
<h4>7.2（2）</h4>
<p><strong>题干：</strong>
[2009] 某计算机的指令流水线由 4 个功能段组成，指令流经各功能段的时间分别为 $90\text{ns}$、$80\text{ns}$、$70\text{ns}$ 和 $60\text{ns}$，则该计算机的 CPU 时钟周期至少是__________。</p>
<p>A. $90\text{ns}$
B. $80\text{ns}$
C. $70\text{ns}$
D. $60\text{ns}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>流水线所有流水段共用同一个时钟周期，时钟周期必须不小于最慢流水段的延迟。四段延迟分别为 $90\text{ns}$、$80\text{ns}$、$70\text{ns}$、$60\text{ns}$，最大值为 $90\text{ns}$。</p>
<ul>
<li><strong>A [正确]</strong>：满足最慢阶段 $90\text{ns}$ 的要求。</li>
<li><strong>B [错误]</strong>：$80\text{ns}&lt;90\text{ns}$，第一段无法完成。</li>
<li><strong>C [错误]</strong>：$70\text{ns}&lt;90\text{ns}$。</li>
<li><strong>D [错误]</strong>：$60\text{ns}&lt;90\text{ns}$。</li>
</ul>
<hr />
<h4>7.2（3）</h4>
<p><strong>题干：</strong>
[2018] 若某计算机最复杂指令的执行需要完成 5 个子功能，分别由功能部件 A ~ E 实现，各功能部件所需时间分别为 $80\text{ps}$、$50\text{ps}$、$50\text{ps}$、$70\text{ps}$ 和 $50\text{ps}$，采用流水线方式执行指令，流水段寄存器延迟时间为 $20\text{ps}$，则 CPU 时钟周期至少为__________。</p>
<p>A. $60\text{ps}$
B. $70\text{ps}$
C. $80\text{ps}$
D. $100\text{ps}$</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<p>组合逻辑部分最长流水段延迟为 $\max(80,50,50,70,50)=80\text{ps}$。</p>
<p>每个流水段之间还要经过流水段寄存器，寄存器延迟为 $20\text{ps}$。因此时钟周期至少为 $80+20=100\text{ps}$。</p>
<ul>
<li><strong>A [错误]</strong>：$60\text{ps}$ 小于最长功能部件 $80\text{ps}$。</li>
<li><strong>B [错误]</strong>：$70\text{ps}$ 仍小于最长功能部件 $80\text{ps}$。</li>
<li><strong>C [错误]</strong>：只考虑了最长功能部件，遗漏了流水段寄存器延迟。</li>
<li><strong>D [正确]</strong>：最长功能部件延迟 $80\text{ps}$ 加寄存器延迟 $20\text{ps}$。</li>
</ul>
<hr />
<h4>7.2（4）</h4>
<p><strong>题干：</strong>
[2016] 在无转发机制的 5 段基本流水线中，下列指令序列存在数据冲突的指令对是__________。</p>
<pre><code>I1: ADD R1, R2, R3;   (R2)+(R3) -&gt; R1
I2: ADD R5, R2, R4;   (R2)+(R4) -&gt; R5
I3: ADD R4, R5, R3;   (R5)+(R3) -&gt; R4
I4: ADD R5, R2, R6;   (R2)+(R6) -&gt; R5
</code></pre>
<p>A. I1 和 I2
B. I2 和 I3
C. I2 和 I4
D. I3 和 I4</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]：I1 和 I2</strong>：I1 写 R1，I2 读 R2、R4，写 R5。I2 不读 I1 写出的 R1，所以没有 RAW 数据冲突。</li>
<li><strong>B [正确]：I2 和 I3</strong>：I2 写 R5，I3 读 R5。I3 需要 I2 的计算结果；无转发机制时，I3 若过早读寄存器会读到旧的 R5，存在 RAW 数据冲突。</li>
<li><strong>C [错误]：I2 和 I4</strong>：I2 写 R5，I4 也写 R5。在基本 5 段、按序完成流水线中，两条指令按程序顺序写回，不会形成需要阻塞处理的 WAW 冲突。</li>
<li><strong>D [错误]：I3 和 I4</strong>：I3 写 R4，I4 读 R2、R6，写 R5。I4 不读 I3 写出的 R4，所以不存在 RAW 数据冲突。</li>
</ul>
<hr />
<h4>7.2（5）</h4>
<p><strong>题干：</strong>
[2019] 在采用"取指、译码/取数、执行、访存、写回"5 段流水线的处理器中，执行如下指令序列：</p>
<pre><code>I1: add   s2, s1, s0      // R[s2] &lt;- R[s1]+R[s0]
I2: load  s3, 0(t2)       // R[s3] &lt;- M[R[t2]+0]
I3: add   s2, s2, s3      // R[s2] &lt;- R[s2]+R[s3]
I4: store s2, 0(t2)       // M[R[t2]+0] &lt;- R[s2]
</code></pre>
<p>下列指令对中，不存在数据冒险的是__________。</p>
<p>A. I1 和 I3
B. I2 和 I3
C. I2 和 I4
D. I3 和 I4</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]：I1 和 I3</strong>：I1 写 <code>$s2</code>，I3 读 <code>$s2</code>，存在 RAW 数据冒险。</li>
<li><strong>B [错误]：I2 和 I3</strong>：I2 写 <code>$s3</code>，I3 读 <code>$s3</code>，存在 load-use 型 RAW 数据冒险。</li>
<li><strong>C [正确]：I2 和 I4</strong>：I2 写 <code>$s3</code>，I4 使用 <code>$s2</code> 和 <code>$t2</code>，不使用 <code>$s3</code>；I2 与 I4 没有读写同一寄存器形成的相关关系，因此不存在数据冒险。</li>
<li><strong>D [错误]：I3 和 I4</strong>：I3 写 <code>$s2</code>，I4 的 store 要读 <code>$s2</code> 作为待写入内存的数据，存在 RAW 数据冒险。</li>
</ul>
<hr />
<h4>7.2（6）</h4>
<p><strong>题干：</strong>
[2023] 某系统采用"取指、译码/取数、执行、访存、写回"5 段流水线，RISC 处理器中执行如下指令序列：</p>
<pre><code>I1  add  s2, s1, s0       // R[s2] &lt;- R[s1]+R[s0]
I2  load s3, 0(s2)        // R[s3] &lt;- M[R[s2]+0]
I3  beq  t2, s3, L        // if R[t2]==R[s3] jump to L
I4  addi t2, t2, 20       // R[t2] &lt;- R[t2]+20
I5  L1:
</code></pre>
<p>若采用转发（旁路）技术处理数据冒险，采用硬件阻塞方式处理控制冒险，则在 I1 ~ I4 执行过程中，发生流水线阻塞的指令有__________。</p>
<p>A. 仅 I3
B. I2、I4
C. I3、I4
D. I2、I3、I4</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ol>
<li>I1 写 <code>$s2</code>，I2 用 <code>$s2</code> 计算访存地址。I1 是 ALU 指令，结果可通过转发送到 I2 的 EX 段，因此 I2 不必阻塞。</li>
<li>I2 是 load 指令，写 <code>$s3</code>；I3 是 beq，读 <code>$s3</code>。load 的数据通常到 MEM 段末才得到，而 I3 在紧随其后的 EX 段就要比较寄存器值，即使用转发也需要阻塞 1 个周期，所以 I3 阻塞。</li>
<li>I3 是条件分支。题目说明采用硬件阻塞方式处理控制冒险，因此分支结果确定前，后继指令 I4 也会因控制冒险被阻塞。</li>
</ol>
<ul>
<li><strong>A [错误]</strong>：漏掉了 I4 因控制冒险被阻塞。</li>
<li><strong>B [错误]</strong>：I2 可通过转发解决 I1→I2 的相关，不阻塞；I4 会阻塞。</li>
<li><strong>C [正确]</strong>：I3 因 load-use 数据冒险阻塞，I4 因控制冒险阻塞。</li>
<li><strong>D [错误]</strong>：多选了 I2。</li>
</ul>
<hr />
<h4>7.2（7）</h4>
<p><strong>题干：</strong>
[2010] 下列选项中，不会引起指令流水线阻塞的是__________。</p>
<p>A. 数据旁路（转发）
B. 数据相关
C. 条件转移
D. 资源冲突</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：数据旁路/转发是解决数据相关的方法，目的是减少或消除阻塞，本身不会引起阻塞。</li>
<li><strong>B [错误]</strong>：数据相关可能导致后续指令等待前一指令结果，引起阻塞。</li>
<li><strong>C [错误]</strong>：条件转移会造成控制冒险，可能阻塞取指。</li>
<li><strong>D [错误]</strong>：资源冲突即结构冲突，多条指令竞争同一硬件资源，会导致阻塞。</li>
</ul>
<hr />
<h4>7.2（8）</h4>
<p><strong>题干：</strong>
[2011] 下列给出的指令系统特点中，有利于实现指令流水线的是__________。</p>
<p>Ⅰ. 指令格式规整且长度一致
Ⅱ. 指令和数据按边界对齐存放
Ⅲ. 只有 Load/Store 指令才能对操作数进行存储访问</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅱ、Ⅲ
C. 仅 Ⅰ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li>
<p><strong>Ⅰ [正确]</strong>：指令长度一致、格式规整，便于取指、译码定长化，有利于流水段均衡。</p>
</li>
<li>
<p><strong>Ⅱ [正确]</strong>：边界对齐可以减少一次取指或访存跨越多个存储边界的情况，有利于稳定流水段时间。</p>
</li>
<li>
<p><strong>Ⅲ [正确]</strong>：Load/Store 结构把访存操作限制在专门指令中，普通 ALU 指令只访问寄存器，有利于简化数据通路和冲突处理。</p>
</li>
<li>
<p><strong>A [错误]</strong>：漏掉了 Ⅲ。</p>
</li>
<li>
<p><strong>B [错误]</strong>：漏掉了 Ⅰ。</p>
</li>
<li>
<p><strong>C [错误]</strong>：漏掉了 Ⅱ。</p>
</li>
<li>
<p><strong>D [正确]</strong>：Ⅰ、Ⅱ、Ⅲ 都有利于流水线实现。</p>
</li>
</ul>
<hr />
<h4>7.2（9）</h4>
<p><strong>题干：</strong>
[2017] 下列关于指令流水线数据通路的叙述中，错误的是__________。</p>
<p>A. 包含生成控制信号的控制部件
B. 包含算术逻辑运算部件（ALU）
C. 包含通用寄存器组和取指部件
D. 由组合逻辑电路和时序逻辑电路组合而成</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：生成控制信号的是控制器/控制部件，通常不属于"数据通路"本身。数据通路主要负责数据的传送、运算和暂存。</li>
<li><strong>B [正确]</strong>：ALU 是数据通路核心部件，用于算术逻辑运算和地址计算。</li>
<li><strong>C [正确]</strong>：通用寄存器组、取指相关部件都是流水线数据通路中的组成部分。</li>
<li><strong>D [正确]</strong>：数据通路既有组合逻辑，如 ALU、加法器、多路选择器，也有时序逻辑，如 PC、寄存器堆、流水寄存器。</li>
</ul>
<hr />
<h4>7.2（10）</h4>
<p><strong>题干：</strong>
[2017] 下列关于超标量流水线特性的叙述中，正确的是__________。</p>
<p>Ⅰ. 能缩短流水线功能段的处理时间
Ⅱ. 能在一个时钟周期内同时发射多条指令
Ⅲ. 能结合动态调度技术提高指令执行并行性</p>
<p>A. 仅 Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li>
<p><strong>Ⅰ [错误]</strong>：缩短流水段处理时间主要是超流水线技术的目标，不是超标量技术的本质特征。</p>
</li>
<li>
<p><strong>Ⅱ [正确]</strong>：超标量处理器的基本特征是在一个时钟周期内可发射多条指令。</p>
</li>
<li>
<p><strong>Ⅲ [正确]</strong>：超标量处理器常结合动态调度、乱序执行、寄存器重命名等技术提高指令级并行性。</p>
</li>
<li>
<p><strong>A [错误]</strong>：漏掉了 Ⅲ。</p>
</li>
<li>
<p><strong>B [错误]</strong>：包含错误的 Ⅰ，且漏掉 Ⅱ。</p>
</li>
<li>
<p><strong>C [正确]</strong>：Ⅱ、Ⅲ 正确。</p>
</li>
<li>
<p><strong>D [错误]</strong>：包含错误的 Ⅰ。</p>
</li>
</ul>
<hr />
<h4>7.2（11）</h4>
<p><strong>题干：</strong>
[2020] 下列给出的处理器类型中，理想情况下 CPI 为 1 的是__________。</p>
<p>Ⅰ. 单周期 CPU
Ⅱ. 多周期 CPU
Ⅲ. 基本流水线 CPU
Ⅳ. 超标量流水线 CPU</p>
<p>A. Ⅰ 和 Ⅱ
B. Ⅰ 和 Ⅲ
C. Ⅰ、Ⅲ、Ⅳ
D. Ⅲ、Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li>
<p><strong>Ⅰ [正确]</strong>：单周期 CPU 每条指令在 1 个时钟周期内完成，所以 CPI=1。</p>
</li>
<li>
<p><strong>Ⅱ [错误]</strong>：多周期 CPU 一条指令通常需要多个时钟周期完成，CPI 通常大于 1。</p>
</li>
<li>
<p><strong>Ⅲ [正确]</strong>：基本单发射流水线在理想无阻塞且指令足够多时，稳定状态每周期完成 1 条指令，平均 CPI 约为 1。</p>
</li>
<li>
<p><strong>Ⅳ [错误]</strong>：超标量流水线理想情况下每周期可完成多条指令，CPI 可以小于 1，因此不能说理想 CPI 为 1。</p>
</li>
<li>
<p><strong>A [错误]</strong>：Ⅱ 不满足。</p>
</li>
<li>
<p><strong>B [正确]</strong>：Ⅰ、Ⅲ 满足。</p>
</li>
<li>
<p><strong>C [错误]</strong>：Ⅳ 理想 CPI 可小于 1。</p>
</li>
<li>
<p><strong>D [错误]</strong>：Ⅳ 不满足，且漏掉 Ⅰ。</p>
</li>
</ul>
<hr />
<h4>7.2（12）</h4>
<p><strong>题干：</strong>
[2022] 下列关于并行处理技术的叙述中，不正确的是__________。</p>
<p>A. 多核处理器属于 MIMD 结构
B. 向量处理器属于 SIMD 结构
C. 硬件多线程技术只可用于多核处理器
D. SMP 中所有处理器共享单一物理地址空间</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：多核处理器中多个核心可以执行不同指令流、处理不同数据流，属于 MIMD 思想。</li>
<li><strong>B [正确]</strong>：向量处理器用一条向量指令同时处理多个数据元素，属于 SIMD 思想。</li>
<li><strong>C [错误]</strong>：硬件多线程不只可用于多核处理器，单核处理器也可以实现硬件多线程，例如细粒度多线程或同时多线程。</li>
<li><strong>D [正确]</strong>：SMP 是对称多处理结构，多个处理器共享同一物理地址空间。</li>
</ul>
<hr />
<h3>习题7.3 简述指令流水线的特点</h3>
<p><strong>题干：</strong></p>
<p>简述指令流水线的特点。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>分段执行</strong>：把指令执行过程划分为若干功能段，例如 IF、ID、EX、MEM、WB。</li>
<li><strong>重叠执行</strong>：不同指令可同时处在不同流水段中，例如第 1 条执行 EX 时，第 2 条可译码，第 3 条可取指。</li>
<li><strong>提高吞吐率</strong>：流水线填满后，理想情况下每个周期可以完成 1 条指令。</li>
<li><strong>不必然缩短单条指令延迟</strong>：单条指令仍要依次经过所有阶段，甚至还会多出流水寄存器开销。</li>
<li><strong>要求流水段尽量均衡</strong>：时钟周期由最慢流水段决定，若某段明显较慢，会限制整体性能。</li>
<li><strong>需要处理冒险</strong>：数据冲突、结构冲突、控制冲突都会破坏理想流水，可能插入气泡或采用转发、预测等技术。</li>
<li><strong>控制更复杂</strong>：需要流水寄存器、冒险检测、转发控制、分支处理和异常处理机制。</li>
</ol>
<hr />
<h3>习题7.4 简述采用插入气泡方式解决数据冲突的主要过程</h3>
<p><strong>题干：</strong></p>
<p>简述采用插入气泡方式解决数据冲突的主要过程。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>检测相关</strong>：在译码/读寄存器阶段检查当前指令的源寄存器是否等于前面尚未写回指令的目的寄存器。</li>
<li><strong>发现冲突</strong>：若当前指令需要的操作数还没有写回寄存器堆，就产生 RAW 数据冲突。</li>
<li><strong>冻结相关流水段</strong>：保持 PC 和 IF/ID 流水寄存器不变，使当前指令停留在 ID 段，后继指令不能继续向前推进。</li>
<li><strong>插入空操作</strong>：向后面的 EX 段送入一个空操作 NOP，也就是在流水线中插入一个气泡。</li>
<li><strong>让前序指令继续前进</strong>：产生数据的指令继续执行，直到结果写回寄存器或达到可被使用的时刻。</li>
<li><strong>恢复流水</strong>：当相关数据已可安全读取后，解除冻结，原来被阻塞的指令继续进入 EX 段。</li>
</ol>
<p>这种方法硬件简单，但牺牲流水线性能，因为气泡周期内没有完成有效工作。</p>
<hr />
<h3>习题7.5 简述采用重定向方式解决数据冲突的主要过程</h3>
<p><strong>题干：</strong></p>
<p>简述采用重定向方式解决数据冲突的主要过程。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>检测源/目的寄存器关系</strong>：判断当前指令的源寄存器是否与前面流水段中指令的目的寄存器相同。</li>
<li><strong>判断结果是否已经产生</strong>：如果前面指令的结果已经在 EX/MEM 或 MEM/WB 等流水寄存器中产生，但尚未写回寄存器堆，就可以转发。</li>
<li><strong>选择旁路输入</strong>：通过多路选择器，把前面流水段中的运算结果或访存结果直接送到后续指令的 ALU 输入端、比较器输入端或 store 数据输入端。</li>
<li><strong>避免等待写回</strong>：后续指令不必等结果写回寄存器堆再读寄存器，从而减少气泡。</li>
<li><strong>必要时仍需阻塞</strong>：若结果尚未产生，例如 load-use 情况中 load 数据到 MEM 段末才可用，而下一条指令 EX 段已经需要该值，则仍需插入 1 个气泡。</li>
</ol>
<hr />
<h3>习题7.6 流水线方式缩短的是指令的执行时间还是程序的执行时间？</h3>
<p><strong>题干：</strong></p>
<p>流水线方式缩短的是指令的执行时间还是程序的执行时间？</p>
<p><strong>解析</strong>：</p>
<p>流水线方式主要缩短的是<strong>程序的总执行时间</strong>，不是单条指令的执行时间。</p>
<p>推导如下：</p>
<ol>
<li>非流水方式下，若一条指令需要经过 5 个阶段，则一条指令完成后，下一条指令才开始。</li>
<li>流水方式下，第 1 条指令进入 ID 段时，第 2 条指令就可以进入 IF 段，多条指令重叠执行。</li>
<li>对单条指令而言，它仍然要经过 IF、ID、EX、MEM、WB 等所有阶段。</li>
<li>由于阶段之间还要加入流水寄存器，单条指令的延迟甚至可能略有增加。</li>
<li>但对整个程序而言，流水线填满后可以接近每周期完成 1 条指令，因此程序总执行时间明显缩短。</li>
</ol>
<p>结论：<strong>流水线提高的是指令吞吐率，缩短的是大量指令组成的程序的执行时间。</strong></p>
<hr />
<h3>习题7.7 简述流水线中断和非流水线中断处理的差异</h3>
<p><strong>题干：</strong></p>
<p>简述流水线中断和非流水线中断处理的差异。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>非流水线处理器中断处理较简单</strong>：同一时刻通常只有一条指令处于执行过程中，因此只要当前指令执行完毕，再保存断点和状态即可。</li>
<li><strong>流水线处理器同一时刻有多条指令在执行</strong>：发生异常或中断时，前面可能有较早指令尚未完成，后面可能有较晚指令已经部分执行。</li>
<li><strong>流水线需要保证精确中断</strong>：中断处理时应表现为某条指令之前的指令都已完成，而该指令及其之后的指令都未改变机器状态。</li>
<li><strong>流水线需要清除或冻结部分指令</strong>：通常要让中断点之前的指令完成，取消中断点之后已经进入流水线的指令。</li>
<li><strong>流水线还要保存更多状态</strong>：可能需要保存 PC、异常原因、流水线控制状态，并恢复到可重新执行的精确位置。</li>
<li><strong>异步中断可在指令边界处理</strong>：外部中断一般可等到合适的指令边界再响应；同步异常通常与具体指令相关，要精确指出引发异常的指令。</li>
</ol>
<hr />
<h3>习题7.8 气泡流水线时空图</h3>
<p><strong>题干：</strong></p>
<p>如果采用气泡流水线执行下述程序，请给出流水线时空图。注意时空图中最后一个时钟周期第 5 条指令进入 ID 段。</p>
<pre><code>addi $s0, $s0, 4
lw   $s1, ($s0)
add  $s2, $s2, $s1
and  $s3, $s1, $s2
sub  $s4, $s2, $s2
</code></pre>
<p><strong>解析</strong>：</p>
<p>本题按 5 段流水线 IF、ID、EX、M、WB 分析。气泡流水线不采用转发，相关指令要等前序指令写回后才能继续。</p>
<p>数据相关关系如下：</p>
<ol>
<li>I1 写 <code>$s0</code>，I2 读 <code>$s0</code>，所以 I2 要等待 I1 的结果。</li>
<li>I2 写 <code>$s1</code>，I3 读 <code>$s1</code>，所以 I3 要等待 I2 的结果。</li>
<li>I3 写 <code>$s2</code>，I4、I5 都读 <code>$s2</code>，所以 I4、I5 要等待 I3 的结果。</li>
</ol>
<p>流水线时空图如下：</p>
<table>
<thead>
<tr>
<th>指令</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>I1 addi $s0,$s0,4</code></td>
<td>IF</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>I2 lw $s1,($s0)</code></td>
<td></td>
<td>IF</td>
<td>ID*</td>
<td>ID*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>I3 add $s2,$s2,$s1</code></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>ID*</td>
<td>ID*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
</tr>
<tr>
<td><code>I4 and $s3,$s1,$s2</code></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>ID*</td>
<td>ID*</td>
<td>ID</td>
<td>EX</td>
</tr>
<tr>
<td><code>I5 sub $s4,$s2,$s2</code></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>ID</td>
</tr>
</tbody>
</table>
<p>说明：</p>
<ol>
<li>I2 在 ID 段等待 I1 写回 <code>$s0</code>。</li>
<li>I3 在 ID 段等待 I2 写回 <code>$s1</code>。</li>
<li>I4 在 ID 段等待 I3 写回 <code>$s2</code>。</li>
<li>第 12 个周期 I5 进入 ID 段，符合题目要求的截止位置。</li>
</ol>
<hr />
<h3>习题7.9 重定向流水线时空图</h3>
<p><strong>题干：</strong></p>
<p>如果采用重定向流水线执行 7.8 中程序，请给出流水线时空图。注意时空图中最后一个时钟周期第 5 条指令进入 ID 段。</p>
<p><strong>解析</strong>：</p>
<p>重定向/转发流水线可以把尚未写回的结果直接送给后续指令使用。</p>
<p>逐条分析：</p>
<ol>
<li>I1 的 ALU 结果 <code>$s0</code> 可从 I1 的 EX/MEM 或 MEM/WB 旁路转发给 I2 的 EX 段地址计算，因此 I1→I2 不需要气泡。</li>
<li>I2 是 load 指令，<code>$s1</code> 到 MEM 段末才得到；I3 紧随其后在 EX 段就要使用 <code>$s1</code>，所以 I2→I3 是 load-use 冒险，需要 1 个气泡。</li>
<li>I3 的 ALU 结果 <code>$s2</code> 可转发给 I4、I5 使用，因此 I3→I4、I3→I5 不需要额外气泡。</li>
</ol>
<p>流水线时空图如下：</p>
<table>
<thead>
<tr>
<th>指令</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>I1 addi $s0,$s0,4</code></td>
<td>IF</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>I2 lw $s1,($s0)</code></td>
<td></td>
<td>IF</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
</tr>
<tr>
<td><code>I3 add $s2,$s2,$s1</code></td>
<td></td>
<td></td>
<td>IF</td>
<td>ID*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
</tr>
<tr>
<td><code>I4 and $s3,$s1,$s2</code></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>ID</td>
<td>EX</td>
</tr>
<tr>
<td><code>I5 sub $s4,$s2,$s2</code></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>ID</td>
</tr>
</tbody>
</table>
<p>说明：第 5 个周期插入 1 个气泡后，I3 在第 6 个周期进入 EX 段；第 7 个周期 I5 进入 ID 段。</p>
<hr />
<h3>习题7.10 无分支预测时程序执行周期</h3>
<p><strong>题干：</strong></p>
<p>假设重定向流水线中所有分支跳转指令均在 EX 段执行，无分支预测、无分支延迟槽技术，尝试计算下述程序的执行周期。</p>
<pre><code>addi $s0,$s0,100      # i=100
while_loop:
    beq $s0,$0,done      # while (i&gt;0)
    addi $s0,$s0,-1      # i=i-1
    j while_loop         # 继续循环
done:
</code></pre>
<p><strong>解析</strong>：</p>
<p>按常见 5 段流水线约定：分支/跳转在 EX 段末确定下一条 PC，无预测且无延迟槽时，每条控制转移指令引起 2 个额外阻塞周期。</p>
<ol>
<li>
<p>初始化指令 <code>addi $s0,$s0,100</code> 执行 1 次。</p>
</li>
<li>
<p><code>beq</code> 的执行次数：</p>
<ul>
<li>当 $i=100,99,\dots,1$ 时，<code>beq</code> 判断不跳转，共 100 次；</li>
<li>当 $i=0$ 时，<code>beq</code> 判断跳转到 <code>done</code>，再执行 1 次；</li>
<li>所以 $N_{beq}=101$。</li>
</ul>
</li>
<li>
<p>循环体中的 <code>addi $s0,$s0,-1</code> 执行次数：$N_{addi_dec}=100$。</p>
</li>
<li>
<p><code>j while_loop</code> 执行次数：$N_j=100$。</p>
</li>
<li>
<p>动态指令总数：$N=1+101+100+100=302$。</p>
</li>
<li>
<p>理想 5 段流水线执行 $N$ 条指令需要：$N+5-1=302+4=306$ 个周期。</p>
</li>
<li>
<p>控制转移指令总数：$N_{ctrl}=101+100=201$。</p>
</li>
<li>
<p>控制冒险额外阻塞周期：$201\times2=402$。</p>
</li>
<li>
<p>总周期数：$306+402=708$。</p>
</li>
</ol>
<p><strong>答案：708 个时钟周期。</strong></p>
<p>补充：若按"每条分支/跳转额外阻塞 3 个周期"计算，则 $306+201\times3=909$。</p>
<hr />
<h3>习题7.11 动态分支预测最优情况下程序执行周期</h3>
<p><strong>题干：</strong></p>
<p>假设重定向流水线中所有分支跳转指令均在 EX 段执行，采用动态分支预测技术，beq、j 指令都可以进行预测，计算最优情况下 7.10 中程序的执行周期。</p>
<p><strong>解析</strong>：</p>
<p>最优情况下，<code>beq</code> 和 <code>j</code> 的方向及目标地址都预测正确，因此不产生控制冒险惩罚。</p>
<ol>
<li>7.10 已得动态指令总数 $N=302$。</li>
<li>5 段流水线理想执行周期为 $N+5-1=302+4=306$。</li>
</ol>
<p><strong>答案：306 个时钟周期。</strong></p>
<hr />
<h3>习题7.12 无分支预测时 for 循环程序执行周期</h3>
<p><strong>题干：</strong></p>
<p>假设重定向流水线中所有分支跳转指令均在 EX 段执行，设无分支预测、无分支延迟槽技术，尝试计算下述程序的执行周期。</p>
<pre><code>addi $s0,$s0,0        # i=0
addi $s1,$s1,0        # sum=0
for_loop:
    slti $t1,$s0,10      # $t1=(i&lt;10)?1:0
    beq $t1,$0,done      # for(i=0;i&lt;10;i++)
    addi $s1,$s1,$s0     # sum=sum+i
    addi $s0,$s0,1       # i++
    j for_loop           # 继续 for 循环
done:
</code></pre>
<p><strong>解析</strong>：</p>
<p>仍按 5 段流水线中分支/跳转在 EX 段确定、无预测时每条控制转移额外阻塞 2 个周期计算。</p>
<ol>
<li>
<p>初始化指令共有 2 条，均执行 1 次：$N_{init}=2$。</p>
</li>
<li>
<p>循环条件 <code>slti</code> 的执行次数：</p>
<ul>
<li>$i=0,1,\dots,9$ 时循环继续，共 10 次；</li>
<li>$i=10$ 时还要执行一次判断以退出循环；</li>
<li>所以 $N_{slti}=11$。</li>
</ul>
</li>
<li>
<p><code>beq</code> 每次判断后都执行，次数与 <code>slti</code> 相同：$N_{beq}=11$。</p>
</li>
<li>
<p>循环体中的 <code>addi $s1,$s1,$s0</code> 执行 10 次：$N_{sum}=10$。</p>
</li>
<li>
<p><code>addi $s0,$s0,1</code> 执行 10 次：$N_{inc}=10$。</p>
</li>
<li>
<p><code>j for_loop</code> 在前 10 次循环结束时执行：$N_j=10$。</p>
</li>
<li>
<p>动态指令总数：$N=2+11+11+10+10+10=54$。</p>
</li>
<li>
<p>理想 5 段流水线周期数：$N+5-1=54+4=58$。</p>
</li>
<li>
<p>控制转移指令数：$N_{ctrl}=N_{beq}+N_j=11+10=21$。</p>
</li>
<li>
<p>控制冒险额外阻塞周期：$21\times2=42$。</p>
</li>
<li>
<p>总周期数：$58+42=100$。</p>
</li>
</ol>
<p><strong>答案：100 个时钟周期。</strong></p>
<p>补充：若按"每条分支/跳转额外阻塞 3 个周期"计算，则 $58+21\times3=121$。</p>
<hr />
<h3>习题7.13 动态分支预测最优情况下 for 循环程序执行周期</h3>
<p><strong>题干：</strong></p>
<p>假设重定向流水线中所有分支跳转指令均在 EX 段执行，采用动态分支预测技术，beq、j 指令都可以进行预测，计算最优情况下 7.12 中程序的执行周期。</p>
<p><strong>解析</strong>：</p>
<p>最优情况下，<code>beq</code> 与 <code>j</code> 都预测正确，不产生控制冒险惩罚。</p>
<ol>
<li>7.12 已得动态指令总数 $N=54$。</li>
<li>5 段流水线理想执行周期为 $N+5-1=54+4=58$。</li>
</ol>
<p><strong>答案：58 个时钟周期。</strong></p>
<hr />
<h3>习题7.14 流水线功能部件关键延迟优化</h3>
<p><strong>题干：</strong></p>
<p>对于表 7.6 所示的 MIPS 指令流水线时间参数，如果可以优化流水线一个功能部件的关键延迟以提升处理器整体性能，应该选择哪个部件进行优化？如果这种优化与成本是线性关系，如何优化才能使处理器性能达到最优，且成本最低？</p>
<p><strong>解析</strong>：</p>
<p>按流水线性能优化的一般原则，可得出如下确定性结论：</p>
<ol>
<li>流水线时钟周期由最慢流水段决定：$T_{clk}=\max(T_{IF},T_{ID},T_{EX},T_{MEM},T_{WB})$。</li>
<li>若只能优化一个功能部件，应优先选择<strong>位于最大流水段延迟中的关键部件</strong>，也就是造成 $T_{clk}$ 最大的瓶颈部件。</li>
<li>若最大延迟流水段只有一个，则优化该段可以直接缩短时钟周期。</li>
<li>若有多个流水段并列最大，则只优化其中一个不能降低整体时钟周期；必须至少把所有并列最大段都优化到低于原最大值，整体性能才会提升。</li>
<li>若优化成本与缩短的延迟线性相关，要使性能达到最优且成本最低，应把瓶颈段优化到<strong>刚好等于第二大流水段延迟</strong>，而不是继续优化得更短。</li>
</ol>
<p>理由如下：</p>
<ul>
<li>假设最大流水段延迟为 $T_1$，第二大流水段延迟为 $T_2$，且 $T_1&gt;T_2$。</li>
<li>只要把瓶颈段从 $T_1$ 降到 $T_2$，新的时钟周期就变为 $T_2$。</li>
<li>若继续把该部件优化到小于 $T_2$，时钟周期仍然受第二大流水段 $T_2$ 限制，性能不再提升。</li>
<li>因此最低成本方案是 $T_1 \rightarrow T_2$，即只优化到刚好不再是唯一瓶颈为止。</li>
</ul>
<p><strong>答案概括</strong>：选择处于最长流水段的关键功能部件进行优化；在线性成本条件下，只需把它优化到与第二长流水段延迟相等，此时性能达到该单部件优化能达到的最优，且成本最低。</p>
<hr />
<h3>习题7.15 16 位计算机流水线问题</h3>
<p><strong>题干：</strong></p>
<p>某 16 位计算机中，带符号整数用补码表示，数据 cache 和指令 cache 分离。该计算机采用 5 段流水线方式执行指令：IF、ID、EX、M、WB；流水线按序发射、按序完成；没有采用转发技术处理数据相关问题；并且同一个寄存器的读和写操作不能在同一个时钟周期内进行。回答下列问题。</p>
<hr />
<h4>（1）若 int 型变量 x 的值为 -513，存放在寄存器 R1 中，则执行指令 "SHR R1" 后，R1 的内容是多少？（用十六进制表示）</h4>
<p><strong>解析</strong>：</p>
<ol>
<li>16 位补码表示范围为 $-32768$ 到 $32767$。</li>
<li>$513$ 的十六进制为 $513=0201H$。</li>
<li>$-513$ 的 16 位补码为 $10000H-0201H=FDFFH$。</li>
<li><code>SHR R1</code> 是算术右移，即最高位符号位保持为 1，其余位右移一位。</li>
<li>将 <code>FDFFH</code> 写成二进制：$FDFFH=1111\ 1101\ 1111\ 1111B$。</li>
<li>算术右移 1 位：$1111\ 1110\ 1111\ 1111B=FEFFH$。</li>
</ol>
<p><strong>答案：$\boxed{FEFFH}$</strong></p>
<hr />
<h4>（2）若某个时间段中，有连续的 4 条指令进入流水线，在其执行过程中没有发生任何阻塞，则执行这 4 条指令所需的时钟周期数为多少？</h4>
<p><strong>解析</strong>：</p>
<p>流水线级数为 $k=5$，连续执行指令条数为 $n=4$。无阻塞时，总周期数为：</p>
<p>$$ k+n-1=5+4-1=8 $$</p>
<p><strong>答案：8 个时钟周期。</strong></p>
<hr />
<h4>（3）若高级语言程序中某赋值语句为 <code>x=a+b</code>，x、a 和 b 均为 int 型变量，对应指令序列如下。问 I3 的 ID 段和 I4 的 IF 段被阻塞的原因各是什么？</h4>
<pre><code>I1    LOAD    R1, [a]
I2    LOAD    R2, [b]
I3    ADD     R1, R2
I4    STORE   R2, [x]
</code></pre>
<p><strong>解析</strong>：</p>
<ol>
<li>
<p><strong>I3 的 ID 段被阻塞的原因</strong>：</p>
<ul>
<li>I3 需要读取 R1 和 R2。</li>
<li>R1 由 I1 的 LOAD 指令产生，R2 由 I2 的 LOAD 指令产生。</li>
<li>题目说明没有转发技术，且同一个寄存器不能同周期读写。</li>
<li>因此 I3 必须等 I1、I2 的结果写回寄存器后，才能在 ID 段读取正确的 R1 和 R2。</li>
<li>所以 I3 的 ID 段阻塞属于 <strong>RAW 数据相关阻塞</strong>。</li>
</ul>
</li>
<li>
<p><strong>I4 的 IF 段被阻塞的原因</strong>：</p>
<ul>
<li>I4 要执行 <code>STORE R2,[x]</code>，需要在 ID 段读取 R2。</li>
<li>I3 会把 <code>a+b</code> 的结果写入 R2。</li>
<li>由于无转发且同一寄存器读写不能同周期进行，I4 不能过早进入 ID 段读取 R2。</li>
<li>在按序发射、按序完成流水线中，I4 只能停留在 IF 段等待前面的 I3 通过 ID 并最终使 R2 可读。</li>
<li>所以 I4 的 IF 段阻塞的本质原因也是 <strong>I3→I4 关于 R2 的 RAW 数据相关</strong>，表现为前面的 I3 阻塞导致后续取指/译码不能继续推进。</li>
</ul>
</li>
</ol>
<hr />
<h4>（4）若高级语言程序中某赋值语句为 <code>x=x*2+a</code>，x 和 a 均为 unsigned int 型变量，则执行这条语句至少需要多少个时钟周期？要求画出这条语句对应的指令序列及其在流水线中的执行过程示意图。</h4>
<p><strong>解析</strong>：</p>
<p>为减少阻塞，应先把 <code>[x]</code> 和 <code>[a]</code> 都取入寄存器，再进行移位和加法。可选指令序列如下：</p>
<pre><code>I1    LOAD    R1, [x]     ; R1 &lt;- x
I2    LOAD    R2, [a]     ; R2 &lt;- a
I3    SHL     R1          ; R1 &lt;- 2*x
I4    ADD     R2, R1      ; R1 &lt;- R2+R1 = a+2*x
I5    STORE   R1, [x]     ; x &lt;- R1
</code></pre>
<p>逐步说明：</p>
<ol>
<li>I1 取出 x 到 R1。</li>
<li>I2 取出 a 到 R2。它与 I1 不存在数据相关，可紧随 I1 进入流水线。</li>
<li>I3 对 R1 左移，相当于计算 $2x$。I3 必须等 I1 写回 R1 后才能读 R1。</li>
<li>I4 把 R2 加到 R1 上。I4 必须等 I3 写回 R1 后才能读到 $2x$。</li>
<li>I5 把 R1 写回 <code>[x]</code>。I5 必须等 I4 写回 R1 后才能读到最终结果。</li>
</ol>
<p>流水线执行过程如下：</p>
<table>
<thead>
<tr>
<th>指令</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
<th>16</th>
<th>17</th>
</tr>
</thead>
<tbody>
<tr>
<td>I1 LOAD R1,[x]</td>
<td>IF</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>I2 LOAD R2,[a]</td>
<td></td>
<td>IF</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>I3 SHL R1</td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>I4 ADD R2,R1</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>IF*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>I5 STORE R1,[x]</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>IF</td>
<td>IF*</td>
<td>IF*</td>
<td>IF*</td>
<td>ID</td>
<td>EX</td>
<td>M</td>
<td>WB</td>
</tr>
</tbody>
</table>
<p>最后一条 STORE 指令到第 17 个周期完成 WB 段。</p>
<p><strong>答案：$\boxed{17\text{ 个时钟周期}}$</strong></p>
<hr />
<h3>习题7.16 循环程序与分支指令问题</h3>
<p><strong>题干：</strong></p>
<p>某程序中有如下循环代码段 p：<code>for(int i = 0; i &lt; N; i++) sum+=A[i];</code>。编译时变量 sum 和 i 分别分配在寄存器 R1 和 R2 中，常量 N 在寄存器 R6 中，数组 A 的首地址在寄存器 R3 中。程序段 p 的起始地址为 <code>08048100H</code>。M 采用 32 位定长指令字，分支指令 bne 采用相对寻址，OFFSET 为补码偏移量。回答下列问题并说明理由。</p>
<hr />
<h4>（1）M 的存储器编址单位是什么？</h4>
<p><strong>解析</strong>：</p>
<ol>
<li>题目说明 M 采用 32 位定长指令字，即 4 字节。</li>
<li>相邻指令地址依次为 <code>08048100H</code>、<code>08048104H</code>、<code>08048108H</code>、<code>0804810CH</code>，相邻地址相差 $4H$。</li>
<li>一条指令为 4 字节，地址增加 4 表示跳过 4 个编址单位，因此 1 个编址单位为 1 字节。</li>
</ol>
<p><strong>答案：字节。</strong></p>
<hr />
<h4>（2）已知 sll 指令实现左移功能，数组 A 中每个元素占多少位？</h4>
<p><strong>解析</strong>：</p>
<ol>
<li>循环中第 1 条指令为 <code>sll R4,R2,2</code>。</li>
<li><code>R2</code> 保存数组下标 <code>i</code>，左移 2 位等价于乘以 $2^2=4$，即 $R4=i\times4$。</li>
<li><code>R4</code> 随后与数组首地址 <code>R3</code> 相加，得到 <code>A[i]</code> 的地址。</li>
<li>因此每个数组元素占 4 个字节，即 32 位。</li>
</ol>
<p><strong>答案：$\boxed{32\text{ 位}}$</strong></p>
<hr />
<h4>（3）bne 指令的 OFFSET 字段的值是多少？已知 bne 指令采用相对寻址方式，当前 PC 的内容为 bne 指令地址，推断出 bne 指令的转移目标地址计算公式。</h4>
<p><strong>解析</strong>：</p>
<ol>
<li>表中 bne 指令机器代码为 <code>1446FFFAH</code>，低 16 位是 OFFSET 字段，因此 $OFFSET=FFFAH$。</li>
<li><code>FFFAH</code> 是 16 位补码，最高位为 1，表示负数。求其真值：$FFFAH-10000H=-0006H=-6$。</li>
<li>bne 指令地址为 $PC=08048114H$，分支目标 <code>loop</code> 的地址为 $08048100H$。</li>
<li>若采用公式 $\text{Target}=(PC+4)+(\text{sign_extend}(OFFSET)&lt;&lt;2)$：
$$ PC+4=08048114H+4=08048118H $$
$$ OFFSET&lt;&lt;2=(-6)\times4=-24=-18H $$
$$ \text{Target}=08048118H-18H=08048100H $$</li>
<li>计算结果正好等于 <code>loop</code> 的地址，因此公式成立。</li>
</ol>
<p><strong>答案：OFFSET 字段为 FFFAH，按 16 位补码解释为 -6。目标地址计算公式为 $\text{Target}=(PC+4)+(\text{sign_extend}(OFFSET)&lt;&lt;2)$。</strong></p>
<hr />
<h4>（4）若 M 采用"按序发射、按序完成"的 5 级指令流水线，且硬件不采取任何转发措施，分支指令的执行均引起 3 个时钟周期的阻塞，则 p 中哪些指令的执行会因数据相关而发生流水线阻塞？哪条指令的执行会发生控制冒险？为什么指令 1 的执行不会因为与指令 5 数据相关而发生阻塞？</h4>
<p>表中指令为：</p>
<pre><code>1  loop: sll  R4,R2,2       ; R4 &lt;- R2 &lt;&lt; 2
2        add  R4,R4,R3      ; R4 &lt;- R4 + R3
3        load R5,0(R4)      ; R5 &lt;- M[R4+0]
4        add  R1,R1,R5      ; R1 &lt;- R1 + R5
5        add  R2,R2,1       ; R2 &lt;- R2 + 1
6        bne  R2,R6,loop    ; if R2 != R6 goto loop
</code></pre>
<p><strong>解析</strong>：</p>
<p><strong>1. 会因数据相关而阻塞的指令</strong></p>
<p>逐条检查 RAW 相关：</p>
<ul>
<li><strong>指令 2 会因指令 1 而阻塞</strong>：指令 1 写 R4，指令 2 读 R4。无转发时，指令 2 必须等待指令 1 写回 R4。</li>
<li><strong>指令 3 会因指令 2 而阻塞</strong>：指令 2 写 R4，指令 3 用 R4 计算 <code>A[i]</code> 的地址。无转发时，指令 3 必须等待指令 2 写回 R4。</li>
<li><strong>指令 4 会因指令 3 而阻塞</strong>：指令 3 是 load 写 R5，指令 4 读 R5 用于累加。无转发时，指令 4 必须等待 R5 写回。</li>
<li><strong>指令 6 会因指令 5 而阻塞</strong>：指令 5 写 R2，指令 6 读 R2 与 R6 比较决定是否跳转。无转发时，指令 6 必须等待指令 5 写回 R2。</li>
</ul>
<p>因此，因数据相关而发生流水线阻塞的指令是：$\boxed{\text{指令 2、指令 3、指令 4、指令 6}}$</p>
<p><strong>2. 会发生控制冒险的指令</strong></p>
<p>控制冒险来自分支指令 <code>bne R2,R6,loop</code>，原因是下一条指令地址取决于 <code>R2 != R6</code> 的比较结果：</p>
<ul>
<li>若条件成立，下一条指令应取 <code>loop</code> 地址，即指令 1。</li>
<li>若条件不成立，下一条指令应取顺序后继地址。</li>
</ul>
<p>所以发生控制冒险的指令是：$\boxed{\text{指令 6：bne R2,R6,loop}}$</p>
<p><strong>3. 指令 1 为什么不会因为与指令 5 数据相关而阻塞？</strong></p>
<p>指令 5 与下一轮循环的指令 1 确实存在数据相关（指令 5 写 R2 即 <code>i++</code>，下一轮指令 1 读 R2 用于计算 <code>i*4</code>），但指令 1 不会因此阻塞，原因如下：</p>
<ol>
<li>指令 5 后面还有指令 6 <code>bne R2,R6,loop</code>。</li>
<li>指令 6 本身也要读 R2，因此指令 6 已经会因为指令 5 写 R2 而等待。</li>
<li>题目还说明分支指令执行会引起 3 个时钟周期的阻塞。</li>
<li>因此，等下一轮循环的指令 1 被取指并进入 ID 段时，指令 5 对 R2 的写回早已完成。</li>
<li>所以下一轮指令 1 能读到更新后的 R2，不需要再因指令 5→指令 1 的数据相关额外阻塞。</li>
</ol>
<hr />
<h3>汇总答案</h3>
<ul>
<li>7.2 选择题答案：1. C；2. A；3. D；4. B；5. C；6. C；7. A；8. D；9. A；10. C；11. B；12. C</li>
<li>7.8：气泡流水线截止到 I5 进入 ID 段为第 12 个周期</li>
<li>7.9：重定向流水线截止到 I5 进入 ID 段为第 7 个周期</li>
<li>7.10：按每个控制转移 2 个额外阻塞周期为 708 周期；若按 3 个额外阻塞周期为 909 周期</li>
<li>7.11：306 周期</li>
<li>7.12：按每个控制转移 2 个额外阻塞周期为 100 周期；若按 3 个额外阻塞周期为 121 周期</li>
<li>7.13：58 周期</li>
<li>7.15（1）：FEFFH；（2）：8 周期；（4）：17 周期</li>
<li>7.16：（1）字节编址；（2）32 位；（3）OFFSET=FFFAH=-6，目标地址公式为 $(PC+4)+(sign_extend(OFFSET)&lt;&lt;2)$；（4）指令 2、3、4、6 因数据相关阻塞，指令 6 发生控制冒险</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>中央处理器</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E4%B8%AD%E5%A4%AE%E5%A4%84%E7%90%86%E5%99%A8/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E4%B8%AD%E5%A4%AE%E5%A4%84%E7%90%86%E5%99%A8/</id>
    <published>2026-06-30T02:55:00.000Z</published>
    <updated>2026-06-30T02:55:00.000Z</updated>
    <summary>整理计算机中央处理器，详解CPU基本功能与结构、数据通路与指令微操作执行流程、硬布线与微程序控制器设计以及异常与中断处理过程。</summary>
    <content type="html"><![CDATA[<h1>第六章：中央处理器</h1>
<p>本章整理计算机中的核心硬件部件：中央处理器。中央处理器是整个计算机系统的控制枢纽与运算中心，主要负责解释并执行机器级指令，以驱动各种底层硬件部件协同运行。</p>
<p>要理解中央处理器的底层设计，我们可以围绕一条主线和两个核心问题展开：</p>
<ul>
<li><strong>基本主线</strong>：一条机器指令进入中央处理器后，硬件电路如何一步步将其解释并执行完毕？</li>
<li><strong>核心问题</strong>：中央处理器是由负责数据流动的<strong>数据通路</strong>和负责发送控制信号的<strong>控制器</strong>组成的。如何设计数据通路？控制器如何根据当前时序产生正确的控制信号？</li>
</ul>
<hr />
<h2>6.1 中央处理器概述</h2>
<h3>6.1.1 中央处理器功能</h3>
<p>中央处理器在物理上主要完成以下五类基本控制与加工工作：</p>
<ul>
<li><strong>指令控制</strong>：控制程序的执行顺序，包括自动取指令、分析指令以及执行指令。</li>
<li><strong>操作控制</strong>：一条指令的执行往往需要多个微操作。中央处理器必须产生对应的操作控制信号，并将它们送往不同的硬件部件。</li>
<li><strong>时间控制</strong>：严格控制各种操作发生的先后顺序和持续时间，以确保时序正确。</li>
<li><strong>数据加工</strong>：通过算术逻辑单元对数据进行算术逻辑运算。</li>
<li><strong>中断处理</strong>：负责对系统运行过程中的异常情况或外部请求进行应急响应与处理。</li>
</ul>
<p>中央处理器执行每一条指令，在物理上都遵循以下基础逻辑流：</p>
<pre><code>取指令 → 分析指令（译码） → 取操作数 → 执行运算 → 写回结果 → 检查中断并转向下一条指令
</code></pre>
<h3>6.1.2 中央处理器组成</h3>
<p>从系统架构层面来看，中央处理器主要由运算器、控制器和内部寄存器组三部分组成：</p>
<pre><code>flowchart BT
    DataPath[数据通路: 寄存器组、ALU、移位器] --&gt; Ctrl[操作控制信号驱动]
    CtrlGen[控制器: 译码电路、时序逻辑] --&gt; Ctrl
    PSW[状态标志反馈] --&gt; CtrlGen
</code></pre>
<h4>1. 运算器电路</h4>
<p>运算器主要负责数据的物理加工。其核心部件包括：</p>
<ul>
<li><strong>算术逻辑单元</strong>：负责执行加、减、与、或、异或等核心算术逻辑运算。</li>
<li><strong>暂存寄存器</strong>：用于暂存从主存或通用寄存器读出的数据，防止在单总线结构下产生数据冲突。</li>
<li><strong>状态标志寄存器</strong>：通常称为程序状态字寄存器。用于保存当前指令执行后的各种运行状态，例如：
<ul>
<li><strong>Z 标志</strong>：结果是否为 0。</li>
<li><strong>N 标志</strong>：结果是否为负数。</li>
<li><strong>C 标志</strong>：运算时最高位是否产生进位或借位。</li>
<li><strong>V 标志</strong>：有符号数运算是否产生溢出。
这些标志是控制器执行后续条件转移指令的物理依据。</li>
</ul>
</li>
</ul>
<h4>2. 控制器电路</h4>
<p>控制器是控制整机运行的决策机构，主要负责产生各种操作控制信号。控制器通过译码器识别指令的功能，并结合时序信号，在指定的节拍发送控制使能信号，打开或关闭相应的数据通路闸门。</p>
<p>:::tip[控制器核心寄存器物理记忆法]</p>
<ul>
<li><strong>程序计数器（PC）</strong>：用于指明下一条待执行指令在主存中的物理地址。具有自增功能。</li>
<li><strong>指令寄存器（IR）</strong>：用于保存当前正在执行的指令二进制代码。其操作码字段会直接送往指令译码器。</li>
<li><strong>存储器地址寄存器（MAR）</strong>：用于保存当前中央处理器所访问的主存单元的物理地址。</li>
<li><strong>存储器数据寄存器（MDR）</strong>：用于暂存中央处理器与主存之间交互的数据。
:::</li>
</ul>
<hr />
<h2>6.2 指令周期</h2>
<h3>6.2.1 指令执行的一般流程与指令周期</h3>
<p>在中央处理器中，时序单位从大到小依次划分为：<strong>指令周期</strong>、<strong>机器周期</strong>和<strong>时钟周期</strong>。</p>
<ul>
<li><strong>时钟周期</strong>：又称节拍。是中央处理器中最小、最基础的时间单位。由系统主频的倒数决定。</li>
<li><strong>机器周期</strong>：又称存取周期。通常把访问一次主存所需的最短时间定义为一个机器周期。一个机器周期通常包含若干个时钟周期。</li>
<li><strong>指令周期</strong>：是指中央处理器从主存中取出一条指令并执行完毕所需的全部时间。一个指令周期通常包含若干个机器周期。</li>
</ul>
<p>$$1 \text{ 指令周期} = \sum \text{机器周期} = \sum \left( m \times \text{时钟周期} \right)$$</p>
<h3>6.2.2 指令周期的基本概念与机器阶段划分</h3>
<p>根据指令在不同阶段的处理特征，一个完整的指令周期可以划分为以下四个主要机器阶段，不是所有指令都需要经历所有阶段：</p>
<ul>
<li><strong>取指周期</strong>：从主存中取出指令存入指令寄存器，并使程序计数器指向下一条指令。这是所有指令<strong>共同经历</strong>的阶段。</li>
<li><strong>间址周期</strong>：当指令采用间接寻址时，需要通过此阶段访问主存以获取操作数的真实有效地址。</li>
<li><strong>执行周期</strong>：根据指令的功能，取出操作数并送入算术逻辑单元进行物理运算，最后写回结果。</li>
<li><strong>中断周期</strong>：如果在执行周期末尾检测到中断请求，且满足中断响应条件，中央处理器将进入中断响应周期，保存程序断点并转入中断服务。</li>
</ul>
<hr />
<h2>6.3 数据通路及指令操作流程</h2>
<p>数据通路是指数据在中央处理器内部流动所经过的物理路径（包括总线、多路选择器、三态门等）。其物理结构的设计直接决定了控制器的微操作安排。</p>
<h3>6.3.1 数据通路模型与定时</h3>
<p>分析数据通路时，需要同时关注“数据从哪里来、经过哪里、写到哪里”以及“这些传送动作在哪个时钟节拍发生”。前者决定硬件连线，后者决定控制信号序列。</p>
<h3>6.3.2 单总线结构的数据通路</h3>
<p>在单总线结构中，所有通用寄存器的输入端、输出端以及算术逻辑单元的接口都连接在同一条公共的内部总线上。由于<strong>同一时刻总线上只能有一个部件输出数据</strong>，算术逻辑单元的两个输入端不能同时从总线上获取数据，因此必须在算术逻辑单元的输入端和输出端分别配置暂存寄存器（如暂存器 Y 和暂存器 Z）。</p>
<p>:::important[单总线结构指令周期的 RTL 微操作节拍设计（考点大题）]</p>
<p>设控制信号命名规则：$X_o$ 表示将寄存器 $X$ 的内容输出到总线；$X_i$ 表示将总线上的内容写入寄存器 $X$。$G$ 为连接两条总线的控制开关门信号（如存在双总线时）。下面推导单总线结构下取指和典型指令的微操作节拍。</p>
<h4>1. 公共取指周期微操作</h4>
<ul>
<li><strong>第一节拍（T0）</strong>：将当前指令物理地址送入地址寄存器。
$$PC \rightarrow MAR \quad \implies \text{ 控制信号：} PC_o, MAR_i$$</li>
<li><strong>第二节拍（T1）</strong>：启动主存读命令，同时程序计数器自动累加（设按字节编址，双字长指令占 4 字节）。
$$M[MAR] \rightarrow MDR, \quad PC + 4 \rightarrow PC \quad \implies \text{ 控制信号：} Read, PC \text{自增控制}$$</li>
<li><strong>第三节拍（T2）</strong>：将从主存读出的指令代码送入指令寄存器，并送往译码器。
$$MDR \rightarrow IR \quad \implies \text{ 控制信号：} MDR_o, IR_i$$</li>
</ul>
<h4>2. 执行加法指令：ADD R1, R2</h4>
<p>该指令的功能为：$(R1) + (R2) \rightarrow R1$。在单总线结构下需要 3 个执行节拍：</p>
<ul>
<li><strong>第四节拍（T3）</strong>：将寄存器 R1 中的被加数读出并暂存到暂存器 Y 中。
$$R1 \rightarrow Y \quad \implies \text{ 控制信号：} R1_o, Y_i$$</li>
<li><strong>第五节拍（T4）</strong>：将寄存器 R2 中的操作数读出送上总线，与暂存器 Y 中的值同时输入算术逻辑单元进行加法运算，结果暂存到输出暂存器 Z 中。
$$(Y) + R2 \rightarrow Z \quad \implies \text{ 控制信号：} R2_o, ALU \text{加法控制}, Z_i$$</li>
<li><strong>第六节拍（T5）</strong>：将暂存器 Z 中的运算结果写回寄存器 R1。
$$Z \rightarrow R1 \quad \implies \text{ 控制信号：} Z_o, R1_i$$</li>
</ul>
<h4>3. 执行取数指令：LOAD R1, (R2)</h4>
<p>该指令的功能为：将寄存器 R2 指向的内存单元的数据加载到 R1。</p>
<ul>
<li><strong>第四节拍（T3）</strong>：将 R2 中的有效地址送入主存地址寄存器。
$$R2 \rightarrow MAR \quad \implies \text{ 控制信号：} R2_o, MAR_i$$</li>
<li><strong>第五节拍（T4）</strong>：启动主存读命令，等待主存将数据读入数据暂存器。
$$M[MAR] \rightarrow MDR \quad \implies \text{ 控制信号：} Read$$</li>
<li><strong>第六节拍（T5）</strong>：将数据暂存器中的内容写入通用寄存器 R1。
$$MDR \rightarrow R1 \quad \implies \text{ 控制信号：} MDR_o, R1_i$$
:::</li>
</ul>
<h3>6.3.3 专用通路结构的数据通路</h3>
<p>专用数据通路通过多路选择器或三态缓冲门，在有频繁数据传送需要的部件之间建立直接连接。其特点是：</p>
<ul>
<li>可以支持多个寄存器在同一时钟周期内并行读写，无需共享总线分步排队。</li>
<li>算术逻辑单元的两个输入可以直接从寄存器堆的两个独立读端口获取。</li>
<li>性能高，但硬件连线非常复杂，控制信号数量庞大。这是现代精简指令集中央处理器和流水线设计的物理基础。</li>
</ul>
<hr />
<h3>6.3.4 补充：MIPS 指令的单周期数据通路执行流程</h3>
<p>单周期数据通路是指中央处理器在一个时钟周期内完成一条指令的全部执行步骤。因此，时钟周期必须由执行时间最长的那条指令（通常为加载字指令）来决定。本节用于理解数据通路控制信号；若课程明确“不考模拟 MIPS32”，复习时可作为低优先级补充内容。</p>
<p>下面给出五类典型 MIPS 指令在单周期数据通路中的控制信号取值，这是选择与综合大题的关键考点：</p>
<table>
<thead>
<tr>
<th>指令类型与格式</th>
<th>RegDst</th>
<th>ALUSrc</th>
<th>MemToReg</th>
<th>RegWrite</th>
<th>MemRead</th>
<th>MemWrite</th>
<th>Branch</th>
<th>Jump</th>
<th>ALUOp</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><code>add $rd, $rs, $rt</code></strong></td>
<td>1 (写rd)</td>
<td>0 (选rt)</td>
<td>0 (选ALU)</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>R型功能</td>
</tr>
<tr>
<td><strong><code>lw $rt, offset($rs)</code></strong></td>
<td>0 (写rt)</td>
<td>1 (选立即数)</td>
<td>1 (选主存)</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>加法</td>
</tr>
<tr>
<td><strong><code>sw $rt, offset($rs)</code></strong></td>
<td>X (无所谓)</td>
<td>1 (选立即数)</td>
<td>X (无所谓)</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>加法</td>
</tr>
<tr>
<td><strong><code>beq $rs, $rt, label</code></strong></td>
<td>X (无所谓)</td>
<td>0 (选rt)</td>
<td>X (无所谓)</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>减法</td>
</tr>
<tr>
<td><strong><code>j target</code></strong></td>
<td>X (无所谓)</td>
<td>X (无所谓)</td>
<td>X (无所谓)</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>X</td>
<td>1</td>
<td>X</td>
</tr>
</tbody>
</table>
<p>:::note[MIPS 单周期控制信号的物理含义解释]</p>
<ul>
<li><strong>RegDst</strong>：选择写入的通用寄存器编号来源。为 1 时选择指令的第 15 到 11 位（rd 字段），为 0 时选择指令的第 20 到 16 位（rt 字段）。</li>
<li><strong>ALUSrc</strong>：选择算术逻辑单元的第二个输入源。为 0 时选择寄存器堆输出的数据，为 1 时选择经过符号扩展后的 16 位立即数。</li>
<li><strong>MemToReg</strong>：选择写入寄存器堆的数据来源。为 1 时选择从主存读取的数据，为 0 时选择算术逻辑单元运算得到的结果。</li>
<li><strong>ALUOp</strong>：控制算术逻辑单元执行的运算类型。对于 <code>lw</code>/<code>sw</code> 指令，需要使用加法计算有效地址；对于 <code>beq</code> 指令，需要使用减法比较两个寄存器内容是否相等。
:::</li>
</ul>
<hr />
<h2>6.4 时序与控制</h2>
<h3>6.4.1 中央处理器的时序与控制方式</h3>
<p>控制器安排微操作发生的时序主要有以下三种控制方式：</p>
<ul>
<li><strong>同步控制方式</strong>：由统一的系统时钟时序信号进行控制。每个微操作都在固定的时钟节拍内发生。设计简单，但对快速微操作会存在时间浪费。</li>
<li><strong>异步控制方式</strong>：没有统一的全局时钟节拍。各部件之间采用“应答/握手”信号进行协调，前一个微操作完成产生的回答信号直接作为下一个微操作的启动信号。速度快，但控制逻辑非常复杂。</li>
<li><strong>联合控制方式</strong>：大部分操作采用同步控制，而对难以确定时间的复杂操作（如主存读写、I/O 传输）采用异步等待控制。</li>
</ul>
<h2>6.5 硬布线控制器</h2>
<h3>6.5.1 硬布线控制器设计原理</h3>
<p>硬布线控制器是用逻辑门电路组成的组合逻辑网络来直接产生微操作控制信号。其产生的每一个控制信号，在数学上都可以表示为输入信号的布尔函数：</p>
<p>$$\text{控制信号} = f(\text{指令操作码译码信号}, \text{时钟节拍信号}, \text{状态标志信号}, \text{中断请求信号})$$</p>
<h4>设计步骤</h4>
<ol>
<li><strong>写出微操作流程</strong>：写出每一条机器指令在每一个时钟节拍内对应的微操作。</li>
<li><strong>列出微操作控制信号清单</strong>：统计在所有指令的所有执行节拍中，每个控制信号在什么条件下需要使能。</li>
<li><strong>推导布尔逻辑表达式</strong>：将使能条件整理为与或非逻辑公式。例如，寄存器 R1 的输出控制信号 $R1_o$ 在执行取指阶段不需要，而在 <code>ADD R1, R2</code> 的 T3 节拍、<code>SUB R1, R3</code> 的 T3 节拍以及 <code>MOV R1, R4</code> 的 T3 节拍下都需要输出：
$$R1_o = \text{ADD} \cdot T_3 + \text{SUB} \cdot T_3 + \text{MOV} \cdot T_3$$</li>
<li><strong>电路化实现</strong>：利用门电路、译码器和触发器搭建物理组合逻辑网络。</li>
</ol>
<p>:::tip[硬布线控制器的优缺点简答考点]</p>
<ul>
<li><strong>优点</strong>：由于控制信号是通过门电路直接物理延迟产生的，速度极快，CPI 接近 1。这是精简指令集微处理器的首选控制器设计。</li>
<li><strong>缺点</strong>：控制逻辑非常庞杂、不规整，一旦电路流片完成，要想修改或增加新指令非常困难，必须重新设计和布线。
:::</li>
</ul>
<hr />
<h2>6.6 微程序控制器</h2>
<p>微程序控制器是为了克服硬布线控制器结构混乱、不易修改的缺点而设计的，其核心思想是<strong>用存储程序的原理来实现微操作信号的产生</strong>。</p>
<h3>6.6.1 微程序控制的基本概念</h3>
<ul>
<li><strong>微命令</strong>：控制硬件部件工作的最基本的控制信号（如 $PC_o$, $MAR_i$）。</li>
<li><strong>微操作</strong>：微命令驱动下部件执行的物理操作。<strong>微命令与微操作是一一对应的</strong>。</li>
<li><strong>微指令</strong>：在一个时钟节拍内，中央处理器需要并行发出的一组微命令的集合。微指令通常保存在特殊的存储器中。</li>
<li><strong>微程序</strong>：若干条微指令按顺序排列构成的序列。<strong>一条机器指令对应一个微程序</strong>。</li>
<li><strong>控制存储器</strong>：位于中央处理器内部的只读存储器，专门用于存放所有机器指令对应的微程序。通常用 CM 表示。</li>
</ul>
<pre><code>机器指令 (如 ADD) → 对应一个微程序 → 由多条微指令组成 → 每条微指令包含多个微命令
</code></pre>
<h3>6.6.2 微程序控制器组成原理</h3>
<pre><code>flowchart BT
    CM[控制存储器 CM] --&gt; uIR[微指令寄存器 uIR]
    uIR --&gt;|顺序控制字段| uMAR[微地址寄存器 uMAR]
    IR[指令寄存器操作码] --&gt;|微地址形成部件| uMAR
    uMAR --&gt; CM
    uIR --&gt;|操作控制字段| Ctrl[控制信号输出]
</code></pre>
<ul>
<li><strong>控制存储器（CM）</strong>：保存微程序。</li>
<li><strong>微地址寄存器（uMAR）</strong>：存放下一条要取出的微指令在控制存储器中的物理地址。</li>
<li><strong>微指令寄存器（uIR）</strong>：存放当前正在执行的微指令。它由两部分组成：
<ul>
<li><strong>操作控制字段</strong>：直接产生或译码产生微操作控制信号。</li>
<li><strong>顺序控制字段</strong>：用于确定下一条微指令的物理地址。</li>
</ul>
</li>
<li><strong>微地址形成部件</strong>：用于根据机器指令的操作码，生成该指令对应微程序的入口物理地址。</li>
</ul>
<h3>6.6.3 微指令及其编码方法</h3>
<p>微指令操作控制字段的设计需要决定如何用较短的二进制位数来表示丰富的微命令：</p>
<h4>1. 直接表示法</h4>
<ul>
<li><strong>设计</strong>：每一位代表一个微命令。</li>
<li><strong>特点</strong>：不需要任何译码，速度极快，并行能力最强；但会导致微指令字长过长，控制存储器容量激增。</li>
</ul>
<h4>2. 字段直接编码法（考点大题）</h4>
<ul>
<li><strong>设计</strong>：将互斥性（即在同一节拍内绝不可能同时使能）的微命令划分在同一个字段中。每个字段单独进行译码。</li>
<li><strong>互斥性划分规则</strong>：若一个字段有 $k$ 位，经过译码后最多可以产生 $2^k - 1$ 个有效的微命令，其中留出一个编码状态（通常为全 0）表示该字段不产生任何微命令。</li>
<li><strong>特点</strong>：大幅压缩了微指令的字长，但由于引入了解码器延迟，控制信号的产生速度略慢。</li>
</ul>
<h4>3. 混合编码法</h4>
<p>将上述两种方式结合，常用且关键的微命令采用直接表示法以追求速度，其他次要微命令采用字段直接编码法以节省容量。</p>
<h3>6.6.4 微程序控制器设计：微指令字长与 CM 容量计算例题</h3>
<p>:::important[微指令字长与控制存储器（CM）容量计算（高权重考点大题）]</p>
<p><strong>题目描述</strong>：
某微程序控制器中，共需产生 48 个微命令。采用字段直接编码法，划分为 6 个互斥字段，每个字段包含的微命令数分别为：8 个、6 个、12 个、5 个、7 个和 4 个。微程序中还有 3 种下地址寻址方式，控制存储器共需容纳 1024 条微指令。</p>
<ol>
<li>计算操作控制字段至少需要多少位？</li>
<li>计算顺序控制字段中的下地址字段需要多少位？</li>
<li>计算该微控制器的微指令字长至少是多少位？</li>
<li>该控制存储器（CM）的容量是多少位？</li>
</ol>
<p><strong>解题分析与步骤</strong>：</p>
<p><strong>1) 计算操作控制字段位数</strong>：
每个互斥字段的位数 $x$ 必须满足：$2^x \ge (\text{微命令数} + 1)$（因为需要预留一个"不发命令"的状态）。</p>
<ul>
<li>第 1 字段（8 个命令）：$2^{x_1} \ge 8 + 1 = 9 \implies x_1 = 4$ 位（可表示 16 种状态）。</li>
<li>第 2 字段（6 个命令）：$2^{x_2} \ge 6 + 1 = 7 \implies x_2 = 3$ 位。</li>
<li>第 3 字段（12 个命令）：$2^{x_3} \ge 12 + 1 = 13 \implies x_3 = 4$ 位。</li>
<li>第 4 字段（5 个命令）：$2^{x_4} \ge 5 + 1 = 6 \implies x_4 = 3$ 位。</li>
<li>第 5 字段（7 个命令）：$2^{x_5} \ge 7 + 1 = 8 \implies x_5 = 3$ 位（注意 $2^3 = 8 \ge 8$，刚好够用）。</li>
<li>第 6 字段（4 个命令）：$2^{x_6} \ge 4 + 1 = 5 \implies x_6 = 3$ 位。</li>
</ul>
<p>将各字段相加，得到操作控制字段的总位数为：
$$\text{操作控制字段} = 4 + 3 + 4 + 3 + 3 + 3 = 20 \text{ 位}$$</p>
<p><strong>2) 计算下地址字段位数</strong>：
控制存储器共有 1024 条微指令，因此微地址的物理寻址范围为 1024：
$$1024 = 2^{10}$$
故下地址字段需要 <strong>10 位</strong>。</p>
<p><strong>3) 计算微指令字长</strong>：
微指令字长由操作控制字段、下地址字段以及寻址控制字段共同组成：</p>
<ul>
<li>寻址控制字段：共有 3 种下地址寻址方式，由于 $2^1 = 2 &lt; 3 \le 4 = 2^2$，故需要 <strong>2 位</strong>。
$$\text{微指令字长} = 20 \text{ (操作控制)} + 10 \text{ (下地址)} + 2 \text{ (寻址控制)} = 32 \text{ 位}$$</li>
</ul>
<p><strong>4) 计算控制存储器（CM）容量</strong>：
控制存储器有 1024 行，每行宽度为 32 位：
$$\text{容量} = 1024 \times 32 \text{ 位} = 32 \text{ Kbit} \text{ (或计为 32768 位)}$$
:::</p>
<hr />
<h2>6.7 异常与中断处理过程</h2>
<p>异常和中断是打破中央处理器原有顺序执行流程，转去处理紧急特殊事件的硬件处理机制。</p>
<h3>6.7.1 异常与中断的精确区别</h3>
<ul>
<li><strong>异常</strong>：是指源自中央处理器<strong>内部</strong>的异常事件。异常与当前正在执行的指令高度相关，例如：执行除数为 0 指令、浮点运算溢出、检测到非法操作码、缺页异常。</li>
<li><strong>中断</strong>：是指源自中央处理器<strong>外部</strong>的事件，与当前指令没有任何直接关系。例如：外设输入/输出请求、键盘按下、定时器时钟中断。</li>
</ul>
<h3>6.7.2 中断周期与中断响应全流程</h3>
<p>当中断请求发生且中央处理器允许中断时，中央处理器会在**当前指令执行周期结束之后（即指令之间）**进行检测并响应，而非当前指令执行内部的某个时钟节拍。</p>
<h4>中断周期的主要硬件流程</h4>
<ol>
<li><strong>关中断</strong>：将程序状态字中的中断允许标志置为 0，防止新的中断打断当前处理。</li>
<li><strong>保存断点</strong>：将当前的程序计数器 PC 内容保存到物理堆栈或特定的异常保存寄存器中。</li>
<li><strong>寻址入口</strong>：转去执行中断服务程序。中央处理器通常通过硬件产生一个中断类型号，然后通过<strong>中断向量表</strong>查询并获取该中断服务程序的起始物理地址，最终将其送入 PC。</li>
</ol>
<p>:::important[异常与中断的透明性分析]
缺页异常通常对应用程序员是透明的（即程序员无法感知，由系统自动完成页面换入）；而普通中断对应用程序也是透明的，其保存和恢复现场全部由操作系统内核接管。
:::</p>
<hr />
<h2>6.8 本章易错点警示与重点回顾</h2>
<p>在备考和做题时，务必注意防范以下几个理解陷阱：</p>
<p>:::important[PC 与普通数据访存地址的区分]
在本课程常见的数据通路模型中，程序计数器 PC 主要用于指出下一条待取指令的地址。普通数据访存的有效地址通常由通用寄存器、指令地址字段或 ALU 计算结果提供，再送入 MAR；不要把取指阶段的 PC 与执行阶段的数据地址来源混为一谈。
:::</p>
<p>:::important[微程序控制存储器与主存的物理区别]</p>
<ul>
<li><strong>主存储器</strong>：位于中央处理器外部，用于存放用户的机器指令程序和数据，由程序员编写，可读可写。</li>
<li><strong>控制存储器</strong>：位于中央处理器内部的控制器中，用于存放微程序，由芯片设计厂商固化好，对普通程序员完全透明。
:::</li>
</ul>
<p>:::important[硬布线与微程序控制器的设计本质]
硬布线控制器是用硬件组合门电路直接产生控制使能信号，属于<strong>硬联线</strong>设计；微程序控制器则是通过读取只读存储器中的微指令，将其送入微指令寄存器进行译码产生控制信号，属于<strong>存储程序</strong>设计。
:::</p>
<hr />
<h2>6.9 历年真题与易错题集</h2>
<h3>一、选择题</h3>
<h4>Q1 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：冯·诺依曼计算机中指令和数据均以二进制形式存放在存储器中，CPU区分它们的依据是______。</p>
<p>A. 指令操作码的译码结果
B. 指令和数据的寻址方式
C. 指令周期的不同阶段
D. 指令和数据所在的存储单元</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令操作码的译码发生在取指之后的译码阶段，此时 CPU 已经将该二进制代码作为指令取出了，因此译码结果不能作为区分指令和数据的依据。</li>
<li><strong>B [错误]</strong>：寻址方式是指令中用于指明操作数地址或下一条指令地址的方法，它不能用于在从存储器取出信息时区分该信息是指令还是数据。</li>
<li><strong>C [正确]</strong>：在冯·诺依曼计算机中，指令和数据存放在同一存储器中。CPU 区分它们的依据是指令周期的不同阶段，在取指阶段从存储器中取出的二进制代码被视为指令，在执行阶段从存储器中取出的二进制代码被视为数据。</li>
<li><strong>D [错误]</strong>：在冯·诺依曼结构中，指令和数据可以混合存放在同一个存储器中，并且可以占用相邻的存储单元，因此 CPU 无法仅仅根据信息所在的存储单元地址来区分它是指令还是数据。</li>
</ul>
<hr />
<h4>Q2 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：相对于微程序控制器，硬布线控制器的特点是______。</p>
<p>A. 指令执行速度慢，指令的修改和扩展容易
B. 指令执行速度慢，指令的修改和扩展难
C. 指令执行速度快，指令的修改和扩展容易
D. 指令执行速度快，指令的修改和扩展难</p>
<p><strong>答案：D</strong></p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：硬布线控制器的指令执行速度快，且指令的修改和扩展极其困难。</li>
<li><strong>B [错误]</strong>：硬布线控制器的指令执行速度快，而不是慢。</li>
<li><strong>C [错误]</strong>：硬布线控制器的指令修改和扩展十分困难，需要重新设计电路并重新物理布线。</li>
<li><strong>D [正确]</strong>：硬布线控制器利用组合逻辑电路直接产生控制信号，不需要访存获取微指令，因而执行速度极快；但由于其电路结构规整性差，一旦设计完成，要修改或扩展指令就非常困难。</li>
</ul>
<hr />
<h4>Q3 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：指令译码器进行译码的是______。</p>
<p>A. 整条指令
B. 指令的操作码字段
C. 指令的地址码字段
D. 指令的操作数字段</p>
<p><strong>答案：B</strong></p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：整条指令包含操作码字段和地址码字段，指令译码器仅对其中的操作码字段进行译码，而地址码字段则送往地址生成逻辑或寄存器选择逻辑，不需要译码器进行操作码译码。</li>
<li><strong>B [正确]</strong>：指令译码器的核心功能是对指令的操作码字段进行译码，识别当前指令的功能类型，从而控制产生相应的操作控制信号。</li>
<li><strong>C [错误]</strong>：指令的地址码字段用于指出操作数的地址或寻址方式，通常由寻址控制逻辑进行解析，而不是由指令译码器进行译码。</li>
<li><strong>D [错误]</strong>：操作数字段如果存在于指令中，也是直接送往执行部件或作为地址，不需要经过指令译码器译码。</li>
</ul>
<hr />
<h4>Q4 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：微程序存储在______中。</p>
<p>A. 主存
B. 接口
C. 控制存储器
D. 磁盘</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：主存用于存放当前正在运行的用户程序 and 数据，CPU通过总线访问主存，微程序不存放在主存中。</li>
<li><strong>B [错误]</strong>：接口是用于连接外设与系统总线的通道，不存放微程序。</li>
<li><strong>C [正确]</strong>：微程序是由微指令组成的序列，每一条机器指令对应一个微程序。微程序通常存放在 CPU 内部的控制存储器中，物理上常用只读存储器实现。</li>
<li><strong>D [错误]</strong>：磁盘是外存储器，用于长期保存文件和数据，CPU 无法直接在执行机器指令时从磁盘读取微程序。</li>
</ul>
<hr />
<h4>Q5 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：中断响应发生的时间是______。</p>
<p>A. 一条机器指令执行开始
B. 一条机器指令执行中间
C. 一条机器指令执行之末
D. 一条机器指令执行的任何时刻</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：在机器指令开始执行时，CPU 尚未完成当前指令的操作，此时不能响应中断。</li>
<li><strong>B [错误]</strong>：如果允许在指令执行中间打断并响应中断，会导致 CPU 内部临时状态保存极其复杂，且很难恢复断点，因此除了极为特殊的异常（如缺页）外，中断不在指令执行中间响应。</li>
<li><strong>C [正确]</strong>：为了保证程序状态的完整性与断点保存的精确性，CPU 只能在当前执行的一条机器指令执行之末（即指令周期的最后一个时钟节拍）去检测中断源并响应中断。</li>
<li><strong>D [错误]</strong>：CPU 响应中断的时机受到严格限制，并不是在指令执行的任何时刻都可以响应的。</li>
</ul>
<hr />
<h4>Q6 (2020-2021 第 2 学期 A 选择题)</h4>
<p><strong>题干</strong>：下列关于 CPU 存取速度的比较中，正确的是______。</p>
<p>A. Cache &gt; 内存 &gt; 寄存器
B. Cache &gt; 寄存器 &gt; 内存
C. 寄存器 &gt; Cache &gt; 内存
D. 寄存器 &gt; 内存 &gt; Cache</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：寄存器是 CPU 内部最快的存储部件，存取速度远快于 Cache 和内存。</li>
<li><strong>B [错误]</strong>：寄存器的存取速度快于 Cache，顺序排列错误。</li>
<li><strong>C [正确]</strong>：寄存器直接集成在 CPU 内部，访问延迟通常在 1 个时钟周期以内，速度最快；Cache 采用快速的静态随机存储器芯片制作，速度次之；外部内存（主存）通常采用动态随机存储器芯片，速度最慢。</li>
<li><strong>D [错误]</strong>：内存的速度远慢于 Cache，顺序排列错误。</li>
</ul>
<hr />
<h4>Q7 (2020-2021 第 2 学期 A 选择题)</h4>
<p><strong>题干</strong>：一个 $m$ 段流水线稳定时的 CPU 吞吐能力，与 $m$ 个并行部件的 CPU 的吞吐能力相比______。</p>
<p>A. 具有同等水平的吞吐能力
B. 不具备同等水平的吞吐能力
C. 吞吐能力大于前者的吞吐能力
D. 吞吐能力小于前者的吞吐能力</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：在理想满载且无流水线冲突的情况下，m 段流水线在稳定时每个时钟周期流出一条指令。而含有 m 个完全并行部件的 CPU，如果其协同机制是每 m 个周期同时流出 m 条指令，平均下来也是每个时钟周期流出 1 条。因此两者的吞吐能力具有同等水平。</li>
<li><strong>B [错误]</strong>：两者在理想状态下具有同等水平的吞吐能力。</li>
<li><strong>C [错误]</strong>：流水线满载时的极限吞吐率不可能大于并行部件协同工作的总吞吐率。</li>
<li><strong>D [错误]</strong>：流水线的吞吐能力没有小于并行部件的吞吐能力。</li>
</ul>
<hr />
<h4>Q8 (2020-2021 第 2 学期 A 选择题)</h4>
<p><strong>题干</strong>：某计算机的指令流水线由 4 个功能段组成，指令流经各功能段之间的时间（忽略各功能段之间的缓存时间）分别为 90ns、80ns、70ns 和 60ns，则该计算机的 CPU 时钟周期至少为______。</p>
<p>A. 90ns
B. 80ns
C. 70ns
D. 60ns</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：流水线的时钟周期必须大于或等于各子过程（功能段）中所需时间最长的那个子过程的时间。因为如果时钟周期小于最长功能段的执行时间，该功能段将无法在一个时钟周期内完成其工作。本题中各功能段的最长时间为 90ns，因此该流水线的时钟周期至少为 90ns。</li>
<li><strong>B [错误]</strong>：如果时钟周期设为 80ns，那么耗时 90ns 的功能段无法在一个时钟周期内完成。</li>
<li><strong>C [错误]</strong>：70ns 小于最慢段的 90ns，无法满足流水线正常工作的时序要求。</li>
<li><strong>D [错误]</strong>：60ns 是最短功能段的执行时间，远小于 90ns，如果作为时钟周期，最慢段将无法工作。</li>
</ul>
<hr />
<h4>Q9 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：计算机操作的最小单位时间是______。</p>
<p>A. 时钟周期
B. 指令周期
C. CPU 周期
D. 执行周期</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：时钟周期是计算机操作的最小单位时间，也是 CPU 硬件逻辑工作的最基本时间单位。</li>
<li><strong>B [错误]</strong>：指令周期是取出并执行一条指令所需的全部时间，通常由若干个机器周期组成。</li>
<li><strong>C [错误]</strong>：CPU 周期又称机器周期，通常由多个时钟周期组成，是完成一个基本操作所需的时间。</li>
<li><strong>D [错误]</strong>：执行周期是指令周期中用于执行指令操作的阶段，通常包含多个时钟周期。</li>
</ul>
<hr />
<h4>Q10 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：MIPS 指令集没有______类指令。</p>
<p>A. I 型指令
B. R 型指令
C. J 型指令
D. M 型指令</p>
<p><strong>答案：D</strong></p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：I 型指令是立即数型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>B [错误]</strong>：R 型指令是寄存器型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>C [错误]</strong>：J 型指令是无条件跳转型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>D [正确]</strong>：MIPS 指令集只包含 R 型、I 型和 J 型三类指令，不存在 M 型指令。</li>
</ul>
<hr />
<h4>Q11 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：下列选项中，能引起外部中断的事件是______。</p>
<p>A. 键盘输入
B. 除数为 0
C. 浮点运算下溢
D. 访存缺页</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：键盘输入属于外部设备发出的中断请求，是由 CPU 外部事件引起的，属于外部中断。</li>
<li><strong>B [错误]</strong>：除数为 0 属于 CPU 内部在执行指令时发生的算术出错，是内部异常。</li>
<li><strong>C [错误]</strong>：浮点运算下溢属于 CPU 内部在执行浮点指令时发生的数据溢出异常，是内部异常。</li>
<li><strong>D [错误]</strong>：访存缺页属于 CPU 内部访存时由于目标页面不在主存中而引起的异常，是内部异常。</li>
</ul>
<hr />
<h4>Q12 (2022-2023-2 考试 选择题)</h4>
<p><strong>题干</strong>：直接寻址的无条件转移指令的功能是将指令中的地址码送入______。</p>
<p>A. 程序计数器 PC
B. 累加器 ACC
C. 指令寄存器 IR
D. 地址寄存器 MAR</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：PC 用于存放下一条将要执行的指令的地址。无条件转移指令用于强行改变程序执行顺序，因此直接寻址下的目标转移地址需要直接送入 PC。</li>
<li><strong>B [错误]</strong>：累加器主要用于存放操作数或算术逻辑运算结果，与保存程序跳转地址无关。</li>
<li><strong>C [错误]</strong>：指令寄存器用于存放当前正在执行的指令，而不是转移目标地址。</li>
<li><strong>D [错误]</strong>：存储器地址寄存器用于存放 CPU 将要访问的内存单元物理地址，转移指令的目标地址需送入 PC 来控制后续取指。</li>
</ul>
<hr />
<h4>Q13 (2022-2023-2 考试 选择题)</h4>
<p><strong>题干</strong>：某计算机的控制器采用微程序控制方式，微指令的操作控制字段采用字段直接编码法，共有 33 个微命令，构成 5 个互斥类，分别包含 7、3、10、5 和 8 个微命令，则操作控制字段至少有______。</p>
<p>A. 5 位
B. 6 位
C. 16 位
D. 32 位</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：5 位不能满足全部互斥类微命令的编码需求，例如仅包含 10 个微命令的互斥类就需要 4 位。</li>
<li><strong>B [错误]</strong>：若采用字段直接编码法，每个互斥类中除了微命令之外，都需要额外预留一个状态表示不发出任何微命令（即全 0 状态）。因此五个互斥类所需位数计算如下：
<ul>
<li>第一类包含 7 个微命令，需满足 $2^{x_1} \ge 7 + 1 = 8$，得 $x_1 = 3$。</li>
<li>第二类包含 3 个微命令，需满足 $2^{x_2} \ge 3 + 1 = 4$，得 $x_2 = 2$。</li>
<li>第三类包含 10 个微命令，需满足 $2^{x_3} \ge 10 + 1 = 11$，得 $x_3 = 4$。</li>
<li>第四类包含 5 个微命令，需满足 $2^{x_4} \ge 5 + 1 = 6$，得 $x_4 = 3$。</li>
<li>第五类包含 8 个微命令，需满足 $2^{x_5} \ge 8 + 1 = 9$，得 $x_5 = 4$。
总位数为 $3 + 2 + 4 + 3 + 4 = 16$ 位，6 位远不能满足要求。</li>
</ul>
</li>
<li><strong>C [正确]</strong>：根据字段直接编码法，各互斥类分别需要 3、2、4、3 和 4 位，操作控制字段总位数至少为 $3 + 2 + 4 + 3 + 4 = 16$ 位。</li>
<li><strong>D [错误]</strong>：32 位不是本题计算得出的最小位数，如果是直接表示法，33 个微命令在不划分互斥类且不考虑编码的情况下可能需要更多位数。</li>
</ul>
<hr />
<h4>Q14 (2022-2023-2 考试 选择题)</h4>
<p><strong>题干</strong>：某计算机的指令流水线由 4 个功能段组成，指令流经各功能段之间的时间分别为 95ns、80ns、70ns 和 60ns，则该计算机的 CPU 时钟周期至少是______。</p>
<p>A. 95ns
B. 80ns
C. 70ns
D. 60ns</p>
<p><strong>答案：A</strong></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：流水线时钟周期受限于执行时间最长的流水段。该流水线中最慢的功能段耗时 95ns，因此 CPU 时钟周期至少需要 95ns，才能保证该功能段能在单个时钟周期内顺利完成。</li>
<li><strong>B [错误]</strong>：若时钟周期为 80ns，则耗时 95ns 的最慢功能段无法在单个时钟周期内完成微操作。</li>
<li><strong>C [错误]</strong>：70ns 小于最慢功能段的执行时间 95ns，无法保证流水线各阶段的正常运行。</li>
<li><strong>D [错误]</strong>：60ns 是最短功能段的执行时间，若时钟周期为 60ns，前三个功能段均无法正常完成工作。</li>
</ul>
<hr />
<h4>Q15 (2022-2023-2 考试 选择题)</h4>
<p><strong>题干</strong>：下列关于缺页处理的叙述中，错误的是______。</p>
<p>A. 缺页是在地址转换时 CPU 检测到的一种异常
B. 缺页处理由操作系统提供的缺页处理程序来完成
C. 缺页处理程序根据页故障地址从外存读入所缺失的页
D. 缺页处理完成后回到发生缺页的指令的下一条指令执行</p>
<p><strong>答案：D</strong></p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：缺页是在 CPU 进行虚实地址转换时，发现页表项的存在位为 0（即页面不在内存中）而检测到的一种内部异常，该叙述正确。</li>
<li><strong>B [错误]</strong>：缺页处理属于操作系统的硬件中断/异常处理机制，是由操作系统提供的专门缺页处理程序来完成的，该叙述正确。</li>
<li><strong>C [错误]</strong>：当缺页发生时，缺页处理程序会读取导致页故障的虚拟地址，并将该地址对应的物理页框从磁盘外存中调入主存，该叙述正确。</li>
<li><strong>D [正确]</strong>：缺页属于一种故障型的异常，处理完毕后，CPU 必须重新执行当前发生缺页的那条访存指令本身（因为之前访存失败了，需要重新访存读取数据），而不是执行下一条指令。因此该叙述错误。</li>
</ul>
<hr />
<h4>Q16 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：算术逻辑单元（ALU）的核心功能部件是______。</p>
<p>A. 累加寄存器
B. 移位器
C. 加法器
D. 指令寄存器</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：累加寄存器是 CPU 内部用于存放操作数和运算结果的暂存寄存器，属于寄存器组，不是 ALU 的运算核心。</li>
<li><strong>B [错误]</strong>：移位器用于实现数据的左移和右移，虽然是 ALU 的组成部分，但它不是最核心的功能部件。</li>
<li><strong>C [正确]</strong>：算术加法是所有复杂算术运算（如乘法、除法）和减法运算的基础。在逻辑电路中，加法器是整个算术逻辑单元中最核心的执行部件。</li>
<li><strong>D [错误]</strong>：指令寄存器用于存放当前正在执行的机器指令，属于控制器部件，不属于 ALU 运算部件。</li>
</ul>
<hr />
<h4>Q17 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：下列指令中不属于程序控制指令的是______。</p>
<p>A. 无条件转移指令
B. 条件转移指令
C. 中断隐指令
D. 循环指令</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：无条件转移指令能够显式地改变程序的控制流，强制跳转到目标地址执行，属于程序控制指令。</li>
<li><strong>B [错误]</strong>：条件转移指令根据特定的状态条件决定是否发生控制流跳转，属于程序控制指令。</li>
<li><strong>C [正确]</strong>：中断隐指令是 CPU 硬件在响应中断时自动执行的一系列操作，它并不是一条可以在指令集中由程序员显式编写执行的机器指令，因此不属于传统程序控制指令.</li>
<li><strong>D [错误]</strong>：循环指令用于实现程序段的循环跳转控制，属于程序控制指令。</li>
</ul>
<hr />
<h4>Q18 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：条件转移指令执行时所依据的条件来自于______。</p>
<p>A. 指令寄存器
B. 标志寄存器
C. 程序计数器
D. 地址寄存器</p>
<p><strong>答案：B</strong></p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令寄存器用于存放当前正在执行的指令编码，不保存运算结果的状态标志。</li>
<li><strong>B [正确]</strong>：条件转移指令所依据的转移条件（如结果为 0、结果为负、溢出等）是先前算术逻辑运算中产生的标志位，这些标志位保存在标志寄存器中。</li>
<li><strong>C [错误]</strong>：程序计数器用于指向下一条将要执行的指令地址，不保存条件状态信息。</li>
<li><strong>D [错误]</strong>：地址寄存器保存的是当前 CPU 读写内存的物理单元地址，与转移条件的判断无关。</li>
</ul>
<hr />
<h4>Q19 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：微程序控制器属于______的一部分。</p>
<p>A. 主存
B. 外存
C. CPU
D. 高速缓存</p>
<p><strong>答案：C</strong></p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：主存是外部物理存储器，用于存放当前正在执行的程序和数据，微程序控制器是处理芯片内的核心逻辑控制器件，不属于主存。</li>
<li><strong>B [错误]</strong>：外存是外部辅助存储设备，不属于芯片内部的微程序控制器。</li>
<li><strong>C [正确]</strong>：微程序控制器是用来控制机器指令执行的 CPU 控制部件，集成在处理芯片内部，是 CPU 的一个核心子系统。</li>
<li><strong>D [错误]</strong>：高速缓存是位于主存与 CPU 寄存器之间的高速存储部件，不是微程序控制器。</li>
</ul>
<hr />
<h4>Q20 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：已知字类型数组 A[4] 的首地址为 A，那么 A[3] 的存储地址是______。</p>
<p>A. A
B. A + 4
C. A + 8
D. A + 12</p>
<p><strong>答案：D</strong></p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：A 地址对应的是数组首元素 A[0] 的存储地址。</li>
<li><strong>B [错误]</strong>：A + 4 对应的是数组第二个元素 A[1] 的存储地址。</li>
<li><strong>C [错误]</strong>：A + 8 对应的是数组第三个元素 A[2] 的存储地址。</li>
<li><strong>D [正确]</strong>：在按字节编址的存储器中，一个“字”类型的数据占用 4 字节的存储空间。因此数组下标为 3 的元素 A[3] 的偏移字节数为 $3 \times 4 = 12$ 字节，其存储地址为 $A + 12$。</li>
</ul>
<hr />
<h3>二、填空题与判断题</h3>
<h4>Q21 (计组 2019 秋 18 级测试 填空题)</h4>
<p><strong>题干</strong>：当前的 CPU 通常由运算器、控制器、______构成。</p>
<p><strong>解析</strong>：</p>
<p>为了减少中央处理器访问外部慢速主存的开销，现代微处理器通常将高速缓冲存储器 Cache 直接集成在芯片内部。因此现代 CPU 主要由运算器、控制器和 Cache 组成。</p>
<p><strong>答案</strong>：Cache。</p>
<hr />
<h4>Q22 (2020-2021 第 2 学期 A 填空题)</h4>
<p><strong>题干</strong>：机器指令是由______码和地址码两部分组成。</p>
<p><strong>解析</strong>：</p>
<p>一条机器指令在结构上主要分为指明“做什么操作”的操作码以及指明“对谁操作/数据在哪里”的地址码两部分。</p>
<p><strong>答案</strong>：操作。</p>
<hr />
<h4>Q23 (南阳理工学院 C 卷 填空题)</h4>
<p><strong>题干</strong>：CPU 主要由运算器和______组成。</p>
<p><strong>解析</strong>：</p>
<p>中央处理器主要由负责数据运算与加工的物理数据通路部件——运算器，以及负责按时序产生操作控制信号的决策部件——控制器组成。</p>
<p><strong>答案</strong>：控制器。</p>
<hr />
<h4>Q24 (南阳理工学院 C 卷 填空题)</h4>
<p><strong>题干</strong>：算术逻辑单元的功能包括算术运算与______运算。</p>
<p><strong>解析</strong>：</p>
<p>算术逻辑单元 ALU 支持加、减等算术运算，同时也支持与、或、非、异或等逻辑运算。</p>
<p><strong>答案</strong>：逻辑。</p>
<hr />
<h4>Q25 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：RISC 普遍采用微程序控制器。（    ）</p>
<p><strong>解析</strong>：</p>
<p>RISC 指令系统结构简单、格式规整、执行时间多为一个周期，因此普遍采用硬件电路直接产生的硬布线控制器（组合逻辑控制器）以获得最高速度；而微程序控制器常用于复杂的 CISC 指令集系统。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h4>Q26 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：指令采用不同寻址方式的目的是缩短指令字长，扩大寻址空间，提高编程灵活性。（    ）</p>
<p><strong>解析</strong>：</p>
<p>指令系统设计时引入寻址方式，可以用较短的指令地址码字段结合基址、变址等方式指向极广阔的主存范围，从而达到缩短字长、扩大寻址空间并使编程支持循环和重定位的目的。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h4>Q27 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：主存在 CPU 外，控制存储器在 CPU 内。（    ）</p>
<p><strong>解析</strong>：</p>
<p>主存是存放用户指令和数据的系统随机存储器，通常位于芯片外部的主板插槽上；而控制存储器 CM 是微程序控制器中用来存放微程序的只读存储器，集成在中央处理器内部。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h4>Q28 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：在冯·诺依曼结构计算机中，程序的功能都通过中央处理器执行指令实现的。（    ）</p>
<p><strong>解析</strong>：</p>
<p>冯·诺依曼结构的核心就是存储程序并自动控制，程序的功能被分解成一条条机器指令保存在主存中，由 CPU 按逻辑顺序取出并执行。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h4>Q29 (南阳理工学院 C 卷 判断题)</h4>
<p><strong>题干</strong>：取指操作是控制器固有的功能，不需要在操作码的控制下完成。（    ）</p>
<p><strong>解析</strong>：</p>
<p>公共取指周期是所有机器指令共有且必须经历的首要时序阶段，由时序电路自动控制。此时 CPU 还没有取出指令，无法对操作码进行译码，因此该操作不需要受当前指令操作码的控制。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h4>Q30 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：在 I/O 接口中，采用统一编址方式时，CPU 不能用访存指令访问 I/O 端口。（    ）</p>
<p><strong>解析</strong>：</p>
<p>统一编址方式是指将 I/O 端口地址划归主存物理地址空间的一部分，CPU 访问 I/O 端口就和访问普通主存单元完全一致，因此必须使用访存指令（如加载/存储指令）来进行访问。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h4>Q31 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：双端口存储器可以同时访问同一区间、同一单元。（    ）</p>
<p><strong>解析</strong>：</p>
<p>双端口存储器具有两套相互独立的读写控制线路，允许在同一时刻物理访问主存。只要两个端口不是同时对同一个单元进行写入操作，就允许同时访问。若两个端口同时对同一个单元写入，或者一个写入一个读取，则会发生写入冲突，必须由控制或仲裁逻辑（如 BUSY 信号）进行限制。因此，在没有写入冲突（或存在仲裁机制）的情况下，该说法是正确的。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h4>Q32 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：半导体 RAM 信息可读可写，且断电后仍能保持记忆。（    ）</p>
<p><strong>解析</strong>：</p>
<p>半导体随机存取存储器 RAM 属于易失性存储器，虽然信息可读可写，但在断电后其内部保存的数据会立即消失。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h4>Q33 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：采用变形补码进行加减运算可以避免溢出。（    ）</p>
<p><strong>解析</strong>：</p>
<p>采用双符号位的变形补码只能起到检测溢出的作用（运算后符号位为 01 代表正溢出，10 代表负溢出），而不能物理上阻止或避免溢出的发生。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h3>三、简答题与综合分析题</h3>
<h4>Q34 (计组 2019 秋 18 级测试 简答题)</h4>
<p><strong>题干</strong>：画出指令周期的流程图。</p>
<p><strong>解析</strong>：</p>
<p>一个完整的指令周期主要包含四个基本的工作阶段。当 CPU 处于取指周期时，从主存取出指令并自动累加 PC；如果有间址寻址，则通过间址周期获取操作数的有效地址；执行周期取出操作数并送入 ALU 运算写回；若在执行周期末尾检测到中断请求且允许中断，则进入中断周期保存断点。</p>
<p><strong>答案</strong>：
指令周期流程图设计如下：</p>
<pre><code>      开始
        │
  ┌─────┴─────┐
  │  取指周期  │ ───&gt; 从主存获取指令，PC 自增
  └─────┬─────┘
        │
  ┌─────┴─────┐
  │  间址周期  │ ───&gt; (可选) 若采用间接寻址，通过访存获取有效地址
  └─────┬─────┘
        │
  ┌─────┴─────┐
  │  执行周期  │ ───&gt; 取出操作数，ALU 运算并写回结果
  └─────┬─────┘
        │
  ┌─────┴─────┐
  │  中断周期  │ ───&gt; (可选) 若检测到中断请求，关中断并保存断点
  └─────┬─────┘
        │
       结束
</code></pre>
<hr />
<h4>Q35 (计组 2019 秋 18 级测试 简答题)</h4>
<p><strong>题干</strong>：简述硬布线和微程序控制器的区别。</p>
<p><strong>解析</strong>：</p>
<p>这是考查 CPU 控制部件两种核心实现方案的技术对比。</p>
<ul>
<li>硬布线控制器：控制信号是由逻辑门电路和触发器搭建的庞大组合时序电路直接产生。其最大优点是执行速度极快；其缺点是电路结构非常不规整，后期修改指令或增加新指令非常困难。</li>
<li>微程序控制器：控制信号是以微指令的形式固化在只读控制存储器 CM 中，通过读取微程序来产生。其优点是结构清晰规整，通过修改微程序可轻易扩展新指令；其缺点是每次执行需要读取 CM，增加了存取延迟，执行速度较慢。</li>
</ul>
<p><strong>答案</strong>：
硬布线与微程序控制器核心区别汇总如下：</p>
<table>
<thead>
<tr>
<th>对比维度</th>
<th>硬布线控制器</th>
<th>微程序控制器</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>工作原理</strong></td>
<td>由门电路直接根据当前时序与状态生成</td>
<td>通过读取控制存储器 CM 中的微指令解释执行</td>
</tr>
<tr>
<td><strong>指令执行速度</strong></td>
<td>极快，延迟小，CPI 接近 1</td>
<td>较慢，每次读取微指令需要耗费时钟节拍</td>
</tr>
<tr>
<td><strong>设计规整度</strong></td>
<td>不规整，布线非常复杂混乱</td>
<td>结构极规整，易于系统调试与模块化设计</td>
</tr>
<tr>
<td><strong>可扩展性</strong></td>
<td>难，流片完成后无法修改硬件布线</td>
<td>易，只需向控制存储器增写或修改微程序</td>
</tr>
<tr>
<td><strong>典型适用场景</strong></td>
<td>精简指令集（RISC）微处理器</td>
<td>复杂指令集（CISC）处理器</td>
</tr>
</tbody>
</table>
<hr />
<h4>Q36 (2020-2021 第 2 学期 A 简答题)</h4>
<p><strong>题干</strong>：请简述影响流水线的三种相关。</p>
<p><strong>解析</strong>：</p>
<p>指令流水线在重叠执行指令时，由于指令间的关联以及硬件资源限制，会引发流水线冲突。</p>
<ul>
<li>结构相关：又称资源相关，是指多条指令在同一时钟周期内争用同一个功能硬件部件引发的冲突。</li>
<li>数据相关：后继指令需要用到前驱指令尚未写回的运算结果，导致数据无法及时读取的冲突。</li>
<li>控制相关：遇到转移、分支、函数调用或中断指令改变 PC 值时，流水线已经提前取入的后继指令失效引起的冲突。</li>
</ul>
<p><strong>答案</strong>：
影响指令流水线的三种相关如下：</p>
<ol>
<li><strong>结构相关</strong>：多条指令在同一周期内争用相同资源（例如单总线结构下同时访问主存取指令与读写操作数）。</li>
<li><strong>数据相关</strong>：后继指令的输入依赖前驱指令的输出（例如 <code>add $t0, $t1, $t2</code> 后紧跟 <code>sub $t3, $t0, $t4</code>，需等待 <code>$t0</code> 写入）。</li>
<li><strong>控制相关</strong>：由于分支转移、跳转或中断等引起程序执行方向改变，使得流水线中已预取的指令无效。</li>
</ol>
<hr />
<h4>Q37 (2020-2021 第 2 学期 A 简答题)</h4>
<p><strong>题干</strong>：已知寄存器 <code>$s0 = 0x10000000</code>，采用汇编指令实现以下功能：</p>
<ol>
<li>将地址为 <code>0x10000000</code> 的字数据读入到寄存器 <code>$t0</code>。</li>
<li>将地址为 <code>0x10000000</code> 的无符号字节数据读入到寄存器 <code>$t0</code>。</li>
</ol>
<p><strong>解析</strong>：</p>
<ul>
<li>读取字数据（32位数据）在 MIPS 中使用字加载指令 <code>lw</code>。</li>
<li>读取无符号字节数据（8位数据且不进行符号扩展）在 MIPS 中使用无符号字节加载指令 <code>lbu</code>。</li>
</ul>
<p><strong>答案</strong>：</p>
<ol>
<li><code>lw $t0, 0($s0)</code></li>
<li><code>lbu $t0, 0($s0)</code></li>
</ol>
<hr />
<h4>Q38 (2020-2021 第 2 学期 A 简答题)</h4>
<p><strong>题干</strong>：写出下表 MIPS 汇编指令对应的 C 语言语句；假设寄存器 <code>$a0</code>、<code>$a1</code>、<code>$a2</code> 对应 C 语言中的变量 <code>e</code>、<code>f</code>、<code>g</code>，整型数组 <code>A</code> 的起始地址放在 <code>$s0</code>。</p>
<table>
<thead>
<tr>
<th>汇编指令（指令序列）</th>
<th>C 语言语句</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>add $a0, $a1, $a2</code></td>
<td></td>
</tr>
<tr>
<td><code>add $a0, $a0, $a2</code></td>
<td></td>
</tr>
<tr>
<td><code>addi $a0, $a0, 1</code></td>
<td></td>
</tr>
<tr>
<td><code>sub $a0, $0, $a0</code> &lt;br&gt; <code>add $a0, $a0, 1</code></td>
<td></td>
</tr>
<tr>
<td><code>lw $s0, 4($s0)</code></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<ul>
<li><code>add $a0, $a1, $a2</code> 即 <code>$a0 = $a1 + $a2</code>，对应 C 语言 <code>e = f + g;</code>。</li>
<li><code>add $a0, $a0, $a2</code> 即 <code>$a0 = $a0 + $a2</code>，对应 C 语言 <code>e = e + g;</code>。</li>
<li><code>addi $a0, $a0, 1</code> 即 <code>$a0 = $a0 + 1</code>，对应 C 语言 <code>e = e + 1;</code>（或 <code>e++;</code>）。</li>
<li><code>sub $a0, $0, $a0</code> 使 <code>$a0 = 0 - $a0 = -e</code>，紧跟 <code>add $a0, $a0, 1</code> 使 <code>$a0 = -e + 1 = 1 - e</code>，对应 C 语言 <code>e = 1 - e;</code>。</li>
<li><code>lw $s0, 4($s0)</code> 从以 <code>$s0</code> 为首地址的数组偏移 4 字节（1 个整型元素宽度）处读取一个字，即读取出 <code>A[1]</code> 的值并写入到寄存器 <code>$s0</code>。若只描述寄存器效果，应写作“寄存器 <code>$s0</code> 得到 <code>A[1]</code> 的值”，不要把 <code>$s0</code> 当作 C 语言变量名。</li>
</ul>
<p><strong>答案</strong>：
C 语言语句填写如下：</p>
<ul>
<li><code>e = f + g;</code></li>
<li><code>e = e + g;</code></li>
<li><code>e = e + 1;</code></li>
<li><code>e = 1 - e;</code></li>
<li>寄存器 <code>$s0</code> 得到 <code>A[1]</code> 的值。</li>
</ul>
<hr />
<h4>Q39 (南阳理工学院 C 卷 综合设计题)</h4>
<p><strong>题干</strong>：某机有 80 条指令，平均每条指令由 5 条微指令组成（包括取指微指令），其中有一条取指微指令为所有指令公用的。已知微指令长度为 32 位，请估算控制存储器 CM 容量。</p>
<p><strong>解析</strong>：</p>
<ul>
<li>控制存储器用于存放全部微程序。每条指令都有其专属的微指令序列，同时它们共享公共的取指操作。</li>
<li>本题中，每条指令包含 5 条微指令，其中包括 1 条公共的取指微指令。因此，每条指令专用的微指令条数为：$5 - 1 = 4$ 条。</li>
<li>80 条指令的专用微指令总和为：$80 \times 4 = 320$ 条。</li>
<li>加上公共的那 1 条取指微指令，CM 中存放的微指令总条数为：$320 + 1 = 321$ 条。</li>
<li>每条微指令字长为 32 位，故 CM 总物理容量至少为：$321 \times 32 = 10272$ 位。</li>
</ul>
<p><strong>答案</strong>：</p>
<ol>
<li>每条指令专用的微指令条数为：$5 - 1 = 4$ 条。</li>
<li>控制存储器内存储微指令的总条数为：$80 \times 4 + 1 = 321$ 条。</li>
<li>控制存储器 CM 估算容量为：$321 \times 32 \text{ 位} = 10272 \text{ 位}$。</li>
</ol>
<hr />
<h4>Q40 (2022-2023-2 考试 综合设计题)</h4>
<p><strong>题干</strong>：某 CPU 数据通路如下图所示，其中 AC 为累加器，状态寄存器保存指令执行过程中的状态。<code>a</code>、<code>b</code>、<code>c</code>、<code>d</code> 为 4 个特殊寄存器（PC、AR/MAR、IR 和 DR/MDR 中的某一个）。</p>
<p><img src="https://img.picr2.online/img/2026/06/29/80458c284633255ba7c07c5dbc83c05c.png" alt="CPU数据通路" /></p>
<ol>
<li>根据 CPU 的功能和结构，说明图中 <code>a</code>、<code>b</code>、<code>c</code>、<code>d</code> 4 个寄存器分别对应哪个寄存器？</li>
<li>简述 <code>LDA address</code> 的数据通路（功能为将主存 address 单元的内容送入 AC 中）。</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li>数据通路分析：
<ul>
<li>寄存器 <code>a</code> 连在主存数据端口与累加器 AC 之间，负责暂存读出/写入主存的数据，故为 <strong>DR / MDR</strong>。</li>
<li>寄存器 <code>b</code> 的输出引脚连接在主存地址输入端上，负责提供物理访存地址，故为 <strong>AR / MAR</strong>。</li>
<li>寄存器 <code>c</code> 从数据总线获取数据，并向控制部件（译码器）提供输入，故为 <strong>IR</strong>。</li>
<li>寄存器 <code>d</code> 发出地址，且与 PC 自增电路相连，故为 <strong>PC</strong>。</li>
</ul>
</li>
<li><code>LDA address</code> 执行时：
<ul>
<li>取指阶段：通过 PC 给 MAR 地址，读出指令送 MDR，再送 IR 译码，PC 自增指向下一条指令。</li>
<li>执行阶段：将指令寄存器 IR 中的地址字段送入 MAR，主存读出该地址处的操作数送 MDR，最后写入累加器 AC。</li>
</ul>
</li>
</ol>
<p><strong>答案</strong>：</p>
<ol>
<li>寄存器对应关系如下：
<ul>
<li><code>a</code> 为 <strong>DR / MDR</strong> (数据寄存器)</li>
<li><code>b</code> 为 <strong>AR / MAR</strong> (地址寄存器)</li>
<li><code>c</code> 为 <strong>IR</strong> (指令寄存器)</li>
<li><code>d</code> 为 <strong>PC</strong> (程序计数器)</li>
</ul>
</li>
<li><code>LDA address</code> 指令的物理数据通路步骤：
<ul>
<li><strong>取指阶段</strong>：
<ol>
<li><code>PC → AR</code>：当前指令地址通过内部总线送至地址寄存器。</li>
<li><code>M[AR] → DR</code>：启动主存读使能，取出指令放入数据寄存器。</li>
<li><code>DR → IR</code>：将当前取出的指令送入指令寄存器。</li>
<li><code>PC + 1 → PC</code>：程序计数器自动完成累加。</li>
</ol>
</li>
<li><strong>执行阶段</strong>：
5. <code>IR(address) → AR</code>：指令中的操作数地址字段 <code>address</code> 被送入地址寄存器。
6. <code>M[AR] → DR</code>：启动主存读使能，将主存该单元内的数据读出并送入数据寄存器。
7. <code>DR → AC</code>：将数据寄存器中的数据送入累加器 AC 中，操作完成。</li>
</ul>
</li>
</ol>
<hr />
<h4>Q41 (计组 2019 秋 18 级测试 综合设计题)</h4>
<p><strong>题干</strong>：如下图所示为双总线结构机器的数据通路：</p>
<p><img src="https://img.picr2.online/img/2026/06/29/711b22ff0ed09f2cc2d1289392565a65.png" alt="双总线数据通路" /></p>
<p>其中，IR 为指令寄存器，PC 为程序计数器（具有自增功能），M 为主存（受 R/W 信号控制），AR 为地址寄存器，DR 为数据缓冲寄存器，ALU 有加、减控制信号决定完成何种操作，控制信号 G 控制一个门电路，连接在 A、B 双总线之间。
写出 <code>SUB R1, R0</code> 指令（该指令完成 <code>(R1) - (R0) -&gt; R1</code> 的功能操作）的指令周期流程图，假设该指令的地址已放入 PC 中，并列出相应的微操作控制信号序列。</p>
<p><strong>解析</strong>：</p>
<ul>
<li>取指阶段：将 PC 经门电路 G 送入 AR（控制信号 <code>PCo, G, ARi</code>）；发出读命令从主存读出指令到 DR（控制信号 <code>Read, DRi</code>）；最后将 DR 内容通过门电路 G 送入 IR 译码（控制信号 <code>DRo, G, IRi</code>）。</li>
<li>执行阶段：将操作数 R1 通过门电路 G 送入暂存器 X（控制信号 <code>R1o, G, Xi</code>）；将操作数 R0 通过门电路 G 送入暂存器 Y（控制信号 <code>R0o, G, Yi</code>）；最后调用 ALU 减法控制并打开 ALU 输出门将结果送上总线，由控制信号写回 R1（控制信号 <code>ALUsub, G, R1i</code>）。</li>
</ul>
<p><strong>答案</strong>：
<code>SUB R1, R0</code> 的指令周期流程图与对应的微操作控制信号序列如下：</p>
<table>
<thead>
<tr>
<th>阶段</th>
<th>步骤</th>
<th>物理微操作功能</th>
<th>相应的微操作控制信号序列</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>取指周期</strong></td>
<td>1</td>
<td><code>(PC) → AR</code></td>
<td><code>PCo, G, ARi</code></td>
</tr>
<tr>
<td></td>
<td>2</td>
<td><code>M → DR</code>, <code>(PC) + 1 → PC</code></td>
<td><code>Read, DRi</code></td>
</tr>
<tr>
<td></td>
<td>3</td>
<td><code>DR → IR</code></td>
<td><code>DRo, G, IRi</code></td>
</tr>
<tr>
<td><strong>执行周期</strong></td>
<td>4</td>
<td><code>(R1) → X</code></td>
<td><code>R1o, G, Xi</code></td>
</tr>
<tr>
<td></td>
<td>5</td>
<td><code>(R0) → Y</code></td>
<td><code>R0o, G, Yi</code></td>
</tr>
<tr>
<td></td>
<td>6</td>
<td><code>(X) - (Y) → R1</code></td>
<td><code>ALUsub, G, R1i</code></td>
</tr>
</tbody>
</table>
<hr />
<h2>习题6：课后习题详解</h2>
<p>以下按题目顺序给出题干与详细答案解析。</p>
<p><img src="https://img.picr2.online/1782818952403_1.webp" alt="习题截图1" />
<img src="https://img.picr2.online/1782818954047_2.webp" alt="习题截图2" />
<img src="https://img.picr2.online/1782818953240_3.webp" alt="习题截图3" />
<img src="https://img.picr2.online/1782818955534_4.webp" alt="习题截图4" /></p>
<hr />
<h3>习题6.1 解释下列名词</h3>
<p><strong>题干：</strong></p>
<p>解释下列名词：指令周期、数据通路、机器周期、时钟周期、同步控制、异步控制、单周期处理器、多周期处理器、时序发生器、硬布线控制器、微命令、微操作、公操作、相容性微命令、互斥性微命令、微指令、微程序、取指微程序、微指令周期、微程序控制器、控制存储器、水平型微指令、垂直型微指令、指令异常、故障异常、自陷异常、中断响应程序。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>指令周期</strong>：CPU 从主存取出一条指令、分析指令并完成该指令功能所经历的全部时间。一个指令周期通常包括取指、译码、取数、执行、访存、写回、中断检测等阶段，具体阶段取决于指令类型和处理器实现方式。</li>
<li><strong>数据通路</strong>：CPU 内部由寄存器、ALU、多路选择器、总线、存储器接口等部件及其互连组成的数据传送和运算路径。控制器通过控制信号决定数据在数据通路中的流向和操作。</li>
<li><strong>机器周期</strong>：CPU 完成一个相对独立基本操作所需的时间段，又称 CPU 周期。例如取指周期、间址周期、执行周期、访存周期等都可以看成机器周期。一个指令周期可由一个或多个机器周期组成。</li>
<li><strong>时钟周期</strong>：处理器时钟信号相邻有效沿之间的时间间隔，是同步时序系统中最基本的时间单位。时钟周期的倒数是时钟频率。</li>
<li><strong>同步控制</strong>：所有微操作都由统一时钟节拍控制，在规定的时钟边沿或节拍内完成。它实现简单、易于设计，但必须按最慢操作确定时钟周期。</li>
<li><strong>异步控制</strong>：各操作之间不依赖统一时钟节拍，而通过"请求—应答"等握手机制协调。它可适应不同操作延迟，但控制逻辑复杂。</li>
<li><strong>单周期处理器</strong>：每条机器指令都在一个时钟周期内完成的处理器。其 CPI 为 1，但时钟周期必须足够长，以适应最慢指令的最长数据通路延迟。</li>
<li><strong>多周期处理器</strong>：一条机器指令被分成多个步骤，在多个较短时钟周期内完成。不同指令可占用不同周期数，硬件部件可以在不同周期复用。</li>
<li><strong>时序发生器</strong>：产生机器周期信号、节拍信号、时钟脉冲等时序信号的电路，用于协调 CPU 内部各微操作的先后关系。</li>
<li><strong>硬布线控制器</strong>：用组合逻辑电路和时序逻辑电路直接产生控制信号的控制器。它速度快，但设计和修改复杂。</li>
<li><strong>微命令</strong>：控制器发出的、控制某个微操作执行的控制信号。例如 $PC_{out}$、$MAR_{in}$、MemRead、RegWrite 等。</li>
<li><strong>微操作</strong>：CPU 内部在一个节拍或微周期内完成的基本操作。例如 $PC \to MAR$、$M[MAR] \to MDR$、$R1 + R2 \to R3$。</li>
<li><strong>公操作</strong>：多条指令执行过程中共同需要的操作。例如取指操作通常是所有机器指令共有的公操作。</li>
<li><strong>相容性微命令</strong>：可以在同一微周期内同时发出的微命令。例如两个操作使用不同部件且互不冲突，则对应微命令相容。</li>
<li><strong>互斥性微命令</strong>：不能在同一微周期内同时发出的微命令。例如同一条总线上不能同时有两个输出源，因此两个源寄存器同时向同一总线输出通常互斥。</li>
<li><strong>微指令</strong>：控制存储器中的一个控制字，用来在一个微指令周期内发出一组微命令，并给出下一条微指令地址的形成方式。</li>
<li><strong>微程序</strong>：由若干条微指令组成的序列，用来解释并实现一条或一类机器指令的功能。</li>
<li><strong>取指微程序</strong>：完成机器指令取指公操作的微程序，通常包括 $PC \to MAR$、访存取指令、$IR \leftarrow M[PC]$、$PC \leftarrow PC + \text{指令长度}$ 等操作。</li>
<li><strong>微指令周期</strong>：取出一条微指令、译码并发出相应微命令、形成下一条微指令地址所需要的时间。</li>
<li><strong>微程序控制器</strong>：通过读取控制存储器中的微指令来产生控制信号的控制器。它以微程序解释机器指令。</li>
<li><strong>控制存储器</strong>：存放微程序的存储器，一般位于 CPU 内部，通常由 ROM、PLA 或可写控制存储器实现。</li>
<li><strong>水平型微指令</strong>：一条微指令中控制字段较长，多个控制位可直接或较少译码地产生微命令，因此并行性强、速度快，但微指令字长较长。</li>
<li><strong>垂直型微指令</strong>：微指令控制字段较短，需要经过较多译码才能产生微命令，因此字长短、编码紧凑，但并行性弱、速度较慢。</li>
<li><strong>指令异常</strong>：CPU 执行当前指令过程中检测到的异常事件，通常与当前指令有关。例如非法指令、溢出、地址越界、页故障等。</li>
<li><strong>故障异常</strong>：一种可恢复异常。处理完成后，通常需要回到发生故障的当前指令重新执行。例如页缺失异常。</li>
<li><strong>自陷异常</strong>：由程序有意执行陷阱指令或系统调用指令引发的异常，用于系统调用、断点、单步调试等。处理完成后通常返回到自陷指令的下一条指令。</li>
<li><strong>中断响应程序</strong>：CPU 响应外部中断后转去执行的服务程序，用来保存现场、识别中断源、完成设备服务、恢复现场并返回原程序。</li>
</ol>
<hr />
<h3>习题6.2 选择题（考研真题）</h3>
<hr />
<h4>6.2（1）</h4>
<p><strong>题干：</strong>
[2010] 下列寄存器中，汇编语言程序员可见的是__________。</p>
<p>A. 存储器地址寄存器（MAR）
B. 程序计数器（PC）
C. 存储器数据寄存器（MDR）
D. 指令寄存器（IR）</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：MAR 用于保存访存地址，是 CPU 内部实现访存的数据通路寄存器，汇编程序员通常不能直接访问。</li>
<li><strong>B [正确]</strong>：PC 保存下一条将要执行指令的地址，会被转移、调用、返回等指令影响，属于程序员可见或至少体系结构可感知的寄存器。</li>
<li><strong>C [错误]</strong>：MDR 用于保存从主存读出或准备写入主存的数据，是内部临时寄存器，程序员不可见。</li>
<li><strong>D [错误]</strong>：IR 保存当前正在执行的指令，是控制器内部寄存器，程序员不可见。</li>
</ul>
<hr />
<h4>6.2（2）</h4>
<p><strong>题干：</strong>
[2021] 下列寄存器中，汇编语言程序员可见的是__________。</p>
<p>Ⅰ. 指令寄存器
Ⅱ. 微指令寄存器
Ⅲ. 基址寄存器
Ⅳ. 标志/状态寄存器</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅳ
C. 仅 Ⅱ、Ⅳ
D. 仅 Ⅲ、Ⅳ</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：Ⅰ 指令寄存器和 Ⅱ 微指令寄存器都属于 CPU 控制实现内部寄存器，程序员不可见。</li>
<li><strong>B [错误]</strong>：Ⅰ 不可见，Ⅳ 标志/状态寄存器可被条件转移等指令间接或直接使用，因此该项包含错误对象。</li>
<li><strong>C [错误]</strong>：Ⅱ 不可见，Ⅳ 可见，该项包含错误对象。</li>
<li><strong>D [正确]</strong>：Ⅲ 基址寄存器用于形成有效地址，Ⅳ 标志/状态寄存器保存条件码、状态位，二者都属于程序员可见寄存器。</li>
</ul>
<hr />
<h4>6.2（3）</h4>
<p><strong>题干：</strong>
[2024] 将汇编语言程序中实现特定功能的指令序列定义一条伪指令，下列选项中 CPU 能理解并直接执行的是__________。</p>
<p>Ⅰ. 伪指令
Ⅱ. 微指令
Ⅲ. 机器指令
Ⅳ. 汇编指令</p>
<p>A. 仅 Ⅰ 和 Ⅳ
B. 仅 Ⅱ 和 Ⅲ
C. 仅 Ⅲ 和 Ⅳ
D. 仅 Ⅰ、Ⅲ 和 Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：伪指令和汇编指令都需要汇编器翻译或展开，CPU 不能直接理解。</li>
<li><strong>B [正确]</strong>：机器指令是 CPU 指令系统定义的可执行指令；在微程序控制器中，微指令可由控制器读取并执行以产生控制信号。</li>
<li><strong>C [错误]</strong>：汇编指令不是 CPU 直接执行对象，必须先翻译成机器指令。</li>
<li><strong>D [错误]</strong>：伪指令和汇编指令都不能由 CPU 直接执行。</li>
</ul>
<hr />
<h4>6.2（4）</h4>
<p><strong>题干：</strong>
[2019] 某指令功能为 $R[r2] \leftarrow R[r1] + M[R[r0]]$，其两个源操作数分别采用寄存器、寄存器间接寻址方式。对于下列给定部件，该指令在取数及执行过程中需要用到的是__________。</p>
<p>Ⅰ. 通用寄存器组（GPRs）
Ⅱ. 算术逻辑单元（ALU）
Ⅲ. 存储器（Memory）
Ⅳ. 指令译码器（ID）</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅱ、Ⅲ
C. 仅 Ⅱ、Ⅲ、Ⅳ
D. 仅 Ⅰ、Ⅱ、Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：该项漏掉了存储器。因为第二个源操作数是 $M[R[r0]]$，必须访问主存。</li>
<li><strong>B [正确]</strong>：需要从 GPRs 读 $r0$、$r1$ 并写 $r2$；需要用 ALU 完成加法；需要访问 Memory 取出 $M[R[r0]]$。</li>
<li><strong>C [错误]</strong>：该项漏掉 GPRs，而 $R[r0]$、$R[r1]$、$R[r2]$ 都在通用寄存器组中。</li>
<li><strong>D [错误]</strong>：指令译码器用于译码阶段，不属于题干强调的"取数及执行过程"中完成取操作和运算所必需的数据部件；且该项漏掉存储器。</li>
</ul>
<hr />
<h4>6.2（5）</h4>
<p><strong>题干：</strong>
[2016] 某计算机主存空间为 4 GB，字长为 32 位，按字节编址，采用 32 位定长指令格式。若指令按字边界对齐存放，则程序计数器（PC）和指令寄存器（IR）的位数至少分别是__________。</p>
<p>A. 30、30
B. 30、32
C. 32、30
D. 32、32</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<p>主存空间为 $4GB = 2^{32}$ 字节，按字节编址，所以完整字节地址需要 32 位。指令按 32 位字边界对齐存放，因此指令地址最低 2 位恒为 0，PC 只需保存字地址：</p>
<p>$$ \frac{2^{32}\text{ byte}}{4\text{ byte/word}} = 2^{30}\text{ word} $$</p>
<p>所以 PC 至少 30 位。IR 要保存完整 32 位定长指令，所以 IR 至少 32 位。</p>
<ul>
<li><strong>A [错误]</strong>：PC 为 30 位正确，但 IR 不能只有 30 位。</li>
<li><strong>B [正确]</strong>：PC 为 30 位，IR 为 32 位。</li>
<li><strong>C [错误]</strong>：PC 取 32 位不是"至少"意义下的最少位数，IR 30 位不够。</li>
<li><strong>D [错误]</strong>：IR 32 位正确，但 PC 32 位不是最少。</li>
</ul>
<hr />
<h4>6.2（6）</h4>
<p><strong>题干：</strong>
[2021] 下列关于数据通路的叙述中，错误的是__________。</p>
<p>A. 数据通路包含 ALU 等组合逻辑（操作）元件
B. 数据通路包含寄存器等时序逻辑（状态）元件
C. 数据通路不包含用于异常事件检测及响应的电路
D. 数据通路中的数据流动路径由控制信号进行控制</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：ALU 是典型操作元件，属于数据通路。</li>
<li><strong>B [正确]</strong>：PC、IR、通用寄存器、状态寄存器等状态元件属于数据通路。</li>
<li><strong>C [错误]</strong>：现代 CPU 数据通路通常需要包含溢出检测、地址异常检测、中断/异常入口地址选择、EPC/Cause/Status 等相关通路或检测部件，因此"完全不包含"说法错误。</li>
<li><strong>D [正确]</strong>：多路选择器、寄存器写使能、存储器读写等控制信号决定数据流向。</li>
</ul>
<hr />
<h4>6.2（7）</h4>
<p><strong>题干：</strong>
[2023] 数据通路由组合逻辑元件（操作元件）和时序逻辑元件（状态元件）组成，以下给出的元件中，属于操作元件的是__________。</p>
<p>Ⅰ. 算术逻辑部件（ALU）
Ⅱ. 通用寄存器组（GPRs）
Ⅲ. 程序计数器（PC）
Ⅳ. 多路选择器（MUX）</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅳ
C. 仅 Ⅱ、Ⅲ
D. 仅 Ⅰ、Ⅱ、Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：ALU 是操作元件，但 GPRs 是状态元件。</li>
<li><strong>B [正确]</strong>：ALU 和 MUX 都是组合逻辑操作元件。</li>
<li><strong>C [错误]</strong>：GPRs 和 PC 都是保存状态的时序逻辑元件，不是操作元件。</li>
<li><strong>D [错误]</strong>：GPRs 不属于操作元件。</li>
</ul>
<hr />
<h4>6.2（8）</h4>
<p><strong>题干：</strong>
[2019] 下列有关处理器时钟脉冲信号的叙述中，错误的是__________。</p>
<p>A. 时钟脉冲信号由机器脉冲源发出的脉冲信号经整形和分频后形成
B. 时钟脉冲信号的宽度称为时钟周期，时钟周期的倒数为机器主频
C. 时钟周期以相邻状态单元间组合逻辑电路的最大延迟为基准确定
D. 处理器总是在每来一个时钟脉冲信号时就开始执行一条新的指令</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：时钟脉冲通常由振荡源经整形、分频等电路形成。</li>
<li><strong>B [正确]</strong>：严格说时钟周期是相邻有效时钟沿之间的时间间隔。从基本概念角度，时钟周期的倒数为主频。</li>
<li><strong>C [正确]</strong>：为保证状态单元之间的数据能稳定传送，时钟周期必须不小于组合逻辑最大传播延迟加寄存器相关延迟。</li>
<li><strong>D [错误]</strong>：多周期处理器中一条指令需要多个时钟周期，流水线处理器中每个周期也不等于"开始并完整执行一条新指令"。</li>
</ul>
<hr />
<h4>6.2（9）</h4>
<p><strong>题干：</strong>
[2016] 单周期处理器中所有指令的指令周期为一个时钟周期。下列关于单周期处理器的叙述中，错误的是__________。</p>
<p>A. 可以采用单总线结构数据通路
B. 处理器时钟频率较低
C. 在指令执行过程中控制信号不变
D. 每条指令的 CPI 为 1</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：单周期处理器要求一条指令在一个周期内完成取指、译码、读寄存器、执行、访存、写回等操作。单总线结构同一时刻只能传送一路数据，难以在一个周期内完成所有必要传送。</li>
<li><strong>B [正确]</strong>：单周期时钟周期必须按最慢指令的最长路径确定，所以时钟频率较低。</li>
<li><strong>C [正确]</strong>：一条指令在一个周期内完成，控制信号通常由 opcode/funct 组合逻辑产生，在该指令执行期间保持稳定。</li>
<li><strong>D [正确]</strong>：每条指令一个时钟周期完成，所以 CPI 为 1。</li>
</ul>
<hr />
<h4>6.2（10）</h4>
<p><strong>题干：</strong>
[2017] 下列关于主存（MM）和控制存储器（CS）的叙述中，错误的是__________。</p>
<p>A. MM 在 CPU 外，CS 在 CPU 内
B. MM 按地址访问，CS 按内容访问
C. MM 存储指令和数据，CS 存储微指令
D. MM 用 RAM 和 ROM 实现，CS 用 ROM 实现</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：主存通常位于 CPU 外部，控制存储器通常位于 CPU 控制器内部。</li>
<li><strong>B [错误]</strong>：控制存储器也是按微地址访问，并不是按内容访问；按内容访问的是相联存储器。</li>
<li><strong>C [正确]</strong>：主存存放机器指令和数据，控制存储器存放微程序/微指令。</li>
<li><strong>D [正确]</strong>：主存可由 RAM、ROM 等实现，传统控制存储器常由 ROM 实现。</li>
</ul>
<hr />
<h4>6.2（11）</h4>
<p><strong>题干：</strong>
[2009] 相对于微程序控制器，硬布线控制器的特点是__________。</p>
<p>A. 指令执行速度慢，指令功能的修改和扩展容易
B. 指令执行速度慢，指令功能的修改和扩展难
C. 指令执行速度快，指令功能的修改和扩展容易
D. 指令执行速度快，指令功能的修改和扩展难</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：硬布线控制器速度不慢，且修改扩展不容易。</li>
<li><strong>B [错误]</strong>："修改和扩展难"正确，但"速度慢"错误。</li>
<li><strong>C [错误]</strong>："速度快"正确，但"修改和扩展容易"错误。</li>
<li><strong>D [正确]</strong>：硬布线控制器由硬件逻辑直接产生控制信号，速度快；但指令功能变化需要改动硬件逻辑，修改和扩展难。</li>
</ul>
<hr />
<h4>6.2（12）</h4>
<p><strong>题干：</strong>
[2012] 某计算机的控制器采用微程序控制方式，微指令中的操作控制字段采用字段直接编码法，共有 33 个微命令，构成 5 个互斥类，分别包含 7、3、12、5 和 6 个微命令，则操作控制字段至少有__________。</p>
<p>A. 5 位
B. 6 位
C. 15 位
D. 33 位</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<p>字段直接编码法中，每个互斥类单独编码。若某类有 $n$ 个微命令，还要有"不发任何微命令"的状态，所以至少需要 $\lceil \log_2(n+1) \rceil$ 位。</p>
<p>逐类计算：</p>
<ul>
<li>7 个：$\lceil \log_2 8 \rceil = 3$</li>
<li>3 个：$\lceil \log_2 4 \rceil = 2$</li>
<li>12 个：$\lceil \log_2 13 \rceil = 4$</li>
<li>5 个：$\lceil \log_2 6 \rceil = 3$</li>
<li>6 个：$\lceil \log_2 7 \rceil = 3$</li>
</ul>
<p>总位数：$3+2+4+3+3=15$。</p>
<ul>
<li><strong>A、B [错误]</strong>：位数不足。</li>
<li><strong>C [正确]</strong>：15 位。</li>
<li><strong>D [错误]</strong>：直接控制法可能接近的位数，不是字段直接编码法最少位数。</li>
</ul>
<hr />
<h4>6.2（13）</h4>
<p><strong>题干：</strong>
[2014] 某计算机采用微程序控制器，共有 32 条指令，公共的取指微程序包含两条微指令，各指令对应的微程序平均由 4 条微指令组成，采用断定法（下址字段法）确定下条微指令地址，则微指令中下址字段的位数至少是__________。</p>
<p>A. 5
B. 6
C. 8
D. 9</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<p>控制存储器中至少需要存放 $2 + 32 \times 4 = 130$ 条微指令。</p>
<p>下址字段位数 $k$ 需满足 $2^k \ge 130$。因为 $2^7=128&lt;130$，$2^8=256\ge130$，所以 $k=8$。</p>
<ul>
<li><strong>A [错误]</strong>：5 位只能表示 32 个地址。</li>
<li><strong>B [错误]</strong>：6 位只能表示 64 个地址。</li>
<li><strong>C [正确]</strong>：8 位可表示 256 个地址。</li>
<li><strong>D [错误]</strong>：9 位不是最少位数。</li>
</ul>
<hr />
<h4>6.2（14）</h4>
<p><strong>题干：</strong>
[2011] 假定不采用 cache 和指令预取技术，且计算机处于"开中断"状态，则下列有关指令执行的叙述中，错误的是__________。</p>
<p>A. 每个指令周期中 CPU 都至少访问内存一次
B. 每个指令周期一定大于或等于一个 CPU 时钟周期
C. 空操作指令的指令周期中任何寄存器的内容都不会被改变
D. 当前程序在每条指令执行结束时都可能被外部中断打断</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：不采用 cache 和指令预取时，每条指令至少要从主存取指一次。</li>
<li><strong>B [正确]</strong>：指令周期由一个或多个时钟周期组成，故一定大于或等于一个时钟周期。</li>
<li><strong>C [错误]</strong>：空操作指令虽然不改变通用寄存器和存储器中的有效数据，但取指过程中 PC 会更新，因此"任何寄存器都不会改变"错误。</li>
<li><strong>D [正确]</strong>：开中断状态下，CPU 通常在一条指令结束后检测外部中断请求。</li>
</ul>
<hr />
<h4>6.2（15）</h4>
<p><strong>题干：</strong>
[2020] 下列关于"自陷"（Trap）的叙述中，错误的是__________。</p>
<p>A. 自陷是通过陷阱指令预先设定的一类外部中断事件
B. 自陷可用于实现程序调试时的断点设置和单步跟踪
C. 自陷发生后 CPU 将转去执行操作系统内核相应程序
D. 自陷处理完成后返回到自陷指令的下一条指令执行</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：自陷是由当前指令主动引发的内部异常，不是外部中断事件。</li>
<li><strong>B [正确]</strong>：调试断点和单步跟踪常通过陷阱机制实现。</li>
<li><strong>C [正确]</strong>：自陷发生后，CPU 进入内核态并执行相应异常处理程序。</li>
<li><strong>D [正确]</strong>：自陷是有意安排的异常，处理后通常返回到自陷指令的下一条指令。</li>
</ul>
<hr />
<h4>6.2（16）</h4>
<p><strong>题干：</strong>
[2021] 异常事件在当前指令执行过程中进行检测，中断请求则在当前指令执行后进行检测。下列事件中，相应处理程序执行后，必须回到当前指令重新执行的是__________。</p>
<p>A. 系统调用
B. 页缺失
C. DMA 传送结束
D. 打印机缺纸</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：系统调用属于自陷，处理后通常返回系统调用指令的下一条指令。</li>
<li><strong>B [正确]</strong>：页缺失属于故障异常，操作系统调入缺失页面后，必须重新执行导致页缺失的当前指令。</li>
<li><strong>C [错误]</strong>：DMA 传送结束属于外部中断，通常在当前指令执行结束后响应，返回后执行下一条指令。</li>
<li><strong>D [错误]</strong>：打印机缺纸属于外设中断或异常状态，不需要重新执行当前 CPU 指令。</li>
</ul>
<hr />
<h3>习题6.3 回答下列问题</h3>
<p><strong>题干：</strong></p>
<p>（1）CPU 的基本功能是什么？从实现其功能的角度分析，它应由哪些部件组成？
（2）CPU 内部有哪些寄存器？功能分别是什么？哪些是程序员可见的？哪些是必需的？
（3）什么是取指周期？取指周期内应完成哪些操作？
（4）计算机为什么要设置时序系统？说明指令周期、机器周期和时钟周期的含义。
（5）简述传统三级时序和现代时序的差异。
（6）比较单周期 MIPS 处理器与多周期 MIPS 处理器的差异。
（7）组合逻辑控制器与微程序控制器各有什么特点？
（8）说明程序与微程序、指令与微指令的异同。
（9）微命令有哪些编码方法？它们是如何实现的？
（10）简述微程序控制器和硬布线控制器的设计方法。
（11）简述 CPU 中内部异常与外部中断的区别。
（12）简述异常与中断处理的一般流程。
（13）要支持异常与中断处理，CPU 需要对硬、软件进行哪些扩展？</p>
<p><strong>解析</strong>：</p>
<h4>（1）CPU 的基本功能和组成</h4>
<p>CPU 的基本功能包括：</p>
<ol>
<li><strong>指令控制</strong>：按照程序规定的顺序取出指令，并根据指令执行结果改变程序执行流。</li>
<li><strong>操作控制</strong>：对指令译码后产生控制信号，使数据通路中各部件在正确时刻完成规定微操作。</li>
<li><strong>时间控制</strong>：用时钟、节拍、状态等时序信号安排各微操作的先后顺序。</li>
<li><strong>数据加工</strong>：完成算术运算、逻辑运算、移位、比较等操作。</li>
<li><strong>异常和中断处理</strong>：检测内部异常和外部中断，保存现场，转入相应处理程序。</li>
</ol>
<p>从实现角度，CPU 至少应由以下部件组成：</p>
<ol>
<li><strong>运算器</strong>：包括 ALU、移位器、状态标志生成逻辑等。</li>
<li><strong>控制器</strong>：包括指令寄存器、指令译码器、时序发生器、控制信号产生逻辑等。</li>
<li><strong>寄存器组</strong>：包括通用寄存器和专用寄存器。</li>
<li><strong>内部数据通路</strong>：包括总线、多路选择器、临时寄存器等。</li>
<li><strong>存储器接口</strong>：包括 MAR、MDR 及读写控制逻辑。</li>
<li><strong>异常/中断相关部件</strong>：包括异常检测逻辑、EPC、Cause、Status 等。</li>
</ol>
<h4>（2）CPU 内部寄存器及可见性</h4>
<table>
<thead>
<tr>
<th>寄存器</th>
<th>功能</th>
<th>程序员是否可见</th>
</tr>
</thead>
<tbody>
<tr>
<td>PC</td>
<td>保存下一条指令地址</td>
<td>可见或体系结构可感知</td>
</tr>
<tr>
<td>IR</td>
<td>保存当前指令</td>
<td>不可见</td>
</tr>
<tr>
<td>MAR</td>
<td>保存访存地址</td>
<td>不可见</td>
</tr>
<tr>
<td>MDR/MBR</td>
<td>保存读出或写入主存的数据</td>
<td>不可见</td>
</tr>
<tr>
<td>通用寄存器 GPRs</td>
<td>保存操作数、地址、中间结果</td>
<td>可见</td>
</tr>
<tr>
<td>累加器 AC</td>
<td>保存运算操作数或结果</td>
<td>在累加器型机器中可见</td>
</tr>
<tr>
<td>状态/标志寄存器 PSW</td>
<td>保存条件码、中断允许位等</td>
<td>可见或部分可见</td>
</tr>
<tr>
<td>基址寄存器/变址寄存器</td>
<td>地址形成</td>
<td>可见</td>
</tr>
<tr>
<td>栈指针 SP</td>
<td>指向栈顶</td>
<td>可见</td>
</tr>
</tbody>
</table>
<h4>（3）取指周期</h4>
<p>取指周期是 CPU 根据 PC 给出的地址，从主存中取出下一条机器指令并送入 IR 的阶段。典型取指步骤：</p>
<ol>
<li>将 PC 的内容送入 MAR：$MAR \leftarrow PC$。</li>
<li>发出存储器读命令：MemRead = 1。</li>
<li>主存将指令字送入 MDR：$MDR \leftarrow M[MAR]$。</li>
<li>将 MDR 中的指令送入 IR：$IR \leftarrow MDR$。</li>
<li>PC 修改为下一条指令地址：$PC \leftarrow PC + 4$（MIPS 32 位定长指令）。</li>
</ol>
<h4>（4）设置时序系统的原因及三个周期含义</h4>
<p>设置时序系统的原因：CPU 内部操作有严格先后关系；不同部件传播延迟不同；需避免总线冲突；控制器需根据不同阶段发出不同控制信号。</p>
<p>三者关系：$1\text{ 指令周期} = \text{若干个机器周期} = \text{若干个时钟周期}$，且指令周期 $\ge$ 机器周期 $\ge$ 时钟周期。</p>
<h4>（5）传统三级时序与现代时序差异</h4>
<p>传统三级时序强调固定层次（指令周期、机器周期、节拍）和固定节拍；现代时序强调状态机控制，一个状态对应一个时钟周期内的一组微操作，按指令类型动态转移，更适合流水线、异常中断等复杂控制。</p>
<h4>（6）单周期 MIPS 与多周期 MIPS 的差异</h4>
<table>
<thead>
<tr>
<th>比较项</th>
<th>单周期 MIPS</th>
<th>多周期 MIPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>指令完成时间</td>
<td>每条指令 1 个时钟周期</td>
<td>每条指令多个时钟周期</td>
</tr>
<tr>
<td>CPI</td>
<td>固定为 1</td>
<td>不同指令 CPI 不同</td>
</tr>
<tr>
<td>时钟周期</td>
<td>必须按最慢指令最长路径确定，较长</td>
<td>按最长单步骤确定，较短</td>
</tr>
<tr>
<td>硬件复用</td>
<td>部件复用少，常需要更多硬件</td>
<td>同一 ALU、存储器等可在不同周期复用</td>
</tr>
<tr>
<td>控制信号</td>
<td>一条指令执行期间基本不变</td>
<td>不同周期控制信号不同</td>
</tr>
</tbody>
</table>
<h4>（7）组合逻辑控制器与微程序控制器特点</h4>
<p><strong>组合逻辑控制器（硬布线）</strong>：速度快，适合 RISC；设计复杂，修改困难；控制逻辑随指令数增加而迅速复杂。</p>
<p><strong>微程序控制器</strong>：设计规整，易于修改和扩展；需要访问控制存储器，速度较慢；适合 CISC。</p>
<h4>（8）程序与微程序、指令与微指令的异同</h4>
<p>程序由机器指令组成，存放在主存中，由用户编写；微程序由微指令组成，存放在控制存储器中，由 CPU 设计者编写。一条机器指令对应一段微程序（多条微指令）。机器指令存放在主存；微指令存放在控制存储器。</p>
<h4>（9）微命令编码方法</h4>
<p><strong>直接控制法</strong>：每个微命令对应一个控制位，不需要译码，并行性最强。</p>
<p><strong>字段直接编码法</strong>：将互斥微命令分到同一字段编码，字长短但需译码。</p>
<p><strong>字段间接编码法</strong>：某些字段含义由其他字段决定，编码更紧凑但速度较慢。</p>
<h4>（10）微程序控制器和硬布线控制器设计方法</h4>
<p>微程序控制器：分析指令流程 → 分解微操作 → 设计微指令格式 → 编写微程序 → 写入控制存储器。</p>
<p>硬布线控制器：画状态转换图 → 列出控制信号真值表 → 逻辑化简 → 设计组合逻辑电路。</p>
<h4>（11）CPU 中内部异常与外部中断的区别</h4>
<p>内部异常源自 CPU 内部，与当前指令有关，在指令执行过程中检测；外部中断源自 CPU 外部，与当前指令无关，在指令执行结束后检测。</p>
<h4>（12）异常与中断处理的一般流程</h4>
<p>检测事件 → 判断是否响应 → 保存断点 → 保存原因 → 切换状态 → 转入处理入口 → 执行处理程序 → 恢复现场 → 返回原程序。</p>
<h4>（13）支持异常与中断需要的硬软件扩展</h4>
<p>硬件：EPC、Cause、Status、中断向量地址形成逻辑、特权级切换、异常返回指令。软件：异常处理程序、中断服务程序、中断向量表、驱动程序、系统调用接口。</p>
<hr />
<h3>习题6.4 某 CPU 的结构框图</h3>
<p><strong>题干：</strong></p>
<p>某 CPU 的结构框图中，AC 为累加器，条件状态寄存器保存指令执行过程中的状态。a、b、c、d 为 4 个寄存器（PC、AR/MAR、IR 和 DR/MDR 中的某一个）。图中箭头表示信息传送的方向，试完成下列各题。
（1）根据 CPU 的功能和结构标明图中 4 个寄存器的名称。
（2）简述指令 <code>LDA addr</code> 的数据通路，其中 <code>addr</code> 为主存地址，指令的功能是将主存 <code>addr</code> 单元的内容送入 AC 中。</p>
<p><strong>解析</strong>：</p>
<h4>（1）寄存器名称判断</h4>
<ul>
<li><code>d</code> 旁边有 <code>+1</code> 输入，并且可向地址相关通路提供信息，所以 <code>d</code> 是 <strong>程序计数器 PC</strong>。</li>
<li><code>c</code> 向主存提供地址，因此 <code>c</code> 是 <strong>存储器地址寄存器 MAR</strong>。</li>
<li><code>a</code> 位于主存和 CPU 内部数据通路之间，并能与主存双向交换数据，因此 <code>a</code> 是 <strong>存储器数据寄存器 MDR</strong>。</li>
<li><code>b</code> 将指令信息送到操作控制器，因此 <code>b</code> 是 <strong>指令寄存器 IR</strong>。</li>
</ul>
<p>结论：a 为 MDR，b 为 IR，c 为 MAR，d 为 PC。</p>
<h4>（2）<code>LDA addr</code> 的数据通路</h4>
<p><strong>取指阶段</strong>：</p>
<ol>
<li><code>MAR ← PC</code>（d → c）</li>
<li><code>MDR ← M[MAR]</code>（M → a）</li>
<li><code>IR ← MDR</code>（a → b）</li>
<li><code>PC ← PC+1</code>（d → d）</li>
</ol>
<p><strong>执行阶段</strong>：</p>
<ol>
<li><code>MAR ← IR.addr</code>（b → c）</li>
<li><code>MDR ← M[MAR]=M[addr]</code>（M → a）</li>
<li><code>AC ← MDR</code>（a → AC）</li>
</ol>
<p>最终结果：$AC = M[addr]$。</p>
<hr />
<h3>习题6.5 至 习题6.9</h3>
<p><strong>习题6.5 至 习题6.9 的详细解析（包括单周期/多周期 MIPS 修改、控制信号故障分析、指令执行流程等）由于篇幅较长，此处给出核心要点和答案框架。如需完整推导过程，可参考教材对应章节。</strong></p>
<h4>习题6.5 修改单总线结构处理器以支持 MIPS 指令</h4>
<p><strong>题干：</strong> 修改单总线结构处理器以支持 <code>sll</code>、<code>lui</code>、<code>bltz</code>、<code>j</code> 指令。</p>
<p><strong>核心答案要点：</strong></p>
<ul>
<li><code>sll rd, rt, shamt</code>：ALU 增加左移功能，源操作数来自 <code>rt</code>，目的寄存器为 <code>rd</code>。</li>
<li><code>lui rt, imm</code>：增加立即数左移 16 位电路，写回 <code>rt</code>。</li>
<li><code>bltz rs, offset</code>：增加符号位检测逻辑，若 $R[rs]_{31}=1$ 则 PC 加分支偏移量。</li>
<li><code>j target</code>：增加跳转目标地址拼接部件，PC 多路选择器增加 JumpTarget 输入。</li>
</ul>
<h4>习题6.6 单周期 MIPS 控制信号恒 0/恒 1 故障</h4>
<p><strong>题干：</strong> 分析 RegWrite、RegDst、MemWrite 恒 0/恒 1 故障影响。</p>
<p><strong>核心答案要点：</strong></p>
<ul>
<li><code>RegWrite</code> 恒 0：lw、R 型、I 型算术逻辑指令无法写回。</li>
<li><code>RegWrite</code> 恒 1：sw、beq 会错误写寄存器。</li>
<li><code>RegDst</code> 恒 0：R 型指令写 <code>rt</code> 而非 <code>rd</code>。</li>
<li><code>RegDst</code> 恒 1：lw、I 型指令写 <code>rd</code> 而非 <code>rt</code>。</li>
<li><code>MemWrite</code> 恒 0：sw 无法写主存。</li>
<li><code>MemWrite</code> 恒 1：lw、R 型、beq 会错误写主存。</li>
</ul>
<h4>习题6.7 修改单周期 MIPS 处理器以支持指令</h4>
<p><strong>题干：</strong> 修改单周期 MIPS 以支持 <code>srl</code>、<code>lui</code>、<code>blez</code>、<code>jal</code>。</p>
<p><strong>核心答案要点：</strong></p>
<ul>
<li><code>srl</code>：ALU 增加逻辑右移功能。</li>
<li><code>lui</code>：立即数左移 16 位。</li>
<li><code>blez</code>：判断 $R[rs] \le 0$（Zero ∨ Sign）。</li>
<li><code>jal</code>：保存 $PC+4$ 到 R31，跳转目标地址。</li>
</ul>
<h4>习题6.8 多周期 MIPS 控制信号恒 0/恒 1 故障</h4>
<p><strong>题干：</strong> 分析 PCSrc、MemToReg、IorD 恒 0/恒 1 故障影响。</p>
<p><strong>核心答案要点：</strong></p>
<ul>
<li><code>PCSrc</code> 恒 0：beq 和 j 无法正确跳转。</li>
<li><code>PCSrc</code> 恒 1：取指阶段错误选择分支目标，几乎所有指令出错。</li>
<li><code>MemToReg</code> 恒 0：lw 写入 ALUOut 而非 MDR。</li>
<li><code>MemToReg</code> 恒 1：R 型、I 型写 MDR 而非 ALUOut。</li>
<li><code>IorD</code> 恒 0：lw、sw 访存时错误访问 PC 地址。</li>
<li><code>IorD</code> 恒 1：取指阶段错误访问 ALUOut 地址，所有指令出错。</li>
</ul>
<h4>习题6.9 修改多周期 MIPS 处理器以支持指令</h4>
<p><strong>题干：</strong> 修改多周期 MIPS 以支持 <code>sra</code>、<code>lui</code>、<code>bgtz</code>、<code>j</code>。</p>
<p><strong>核心答案要点：</strong></p>
<ul>
<li><code>sra</code>：ALU 增加算术右移功能，高位补符号位。</li>
<li><code>lui</code>：立即数左移 16 位。</li>
<li><code>bgtz</code>：判断 $R[rs] &gt; 0$（ $\overline{Sign} \cdot \overline{Zero}$ ）。</li>
<li><code>j</code>：跳转目标地址拼接，PC 多路选择器增加 JumpTarget。</li>
</ul>
<hr />
<h3>习题6.10 计算单周期、多周期 MIPS 时钟和执行时间</h3>
<p><strong>题干：</strong></p>
<p>假设构成 CPU 的各功能部件的时间延迟如下：寄存器延迟 $T_{clk_to_q}=20ps$，存储器读 $T_{mem}=150ps$，寄存器堆读 $T_{RF_read}=90ps$，ALU 延迟 $T_{alu}=90ps$，多路选择器 $T_{mux}=20ps$，寄存器建立时间 $T_{setup}=10ps$。试分别计算单周期、多周期 MIPS 处理器的最小时钟周期和最大时钟频率。假设某 MIPS 程序包含 1000 条指令，其中 lw、sw、beq、R 型、I 型比例分别为 10%、10%、10%、50%、20%，试分别计算该程序在单总线结构处理器、单周期 MIPS、多周期 MIPS 处理器上的 CPI 值及执行时间。</p>
<p><strong>解析</strong>：</p>
<h4>一、单周期 MIPS 最小时钟周期</h4>
<p>单周期最慢指令是 lw，关键路径：PC → 指令存储器 → 寄存器堆读 → ALU → 数据存储器 → 写回寄存器。</p>
<p>$$ T_{single} = 20+150+90+20+90+150+20+10 = 550ps $$</p>
<p>$$ f_{single} = 1/550ps \approx 1.82GHz $$</p>
<h4>二、多周期 MIPS 最小时钟周期</h4>
<p>多周期时钟周期由最长阶段决定（IF 或 MEM 阶段）：</p>
<p>$$ T_{multi} = 20+20+150+10 = 200ps $$</p>
<p>$$ f_{multi} = 1/200ps = 5GHz $$</p>
<h4>三、CPI 与执行时间</h4>
<p><strong>单周期 MIPS</strong>：$CPI=1$，$T=1000 \times 1 \times 550ps = 550ns$</p>
<p><strong>多周期 MIPS</strong>：</p>
<ul>
<li>lw: 5 周期，sw: 4 周期，beq: 3 周期，R 型: 4 周期，I 型: 4 周期</li>
<li>$CPI = 0.1\times5 + 0.1\times4 + 0.1\times3 + 0.5\times4 + 0.2\times4 = 4.0$</li>
<li>$T = 1000 \times 4.0 \times 200ps = 800ns$</li>
</ul>
<p><strong>单总线结构处理器</strong>（典型值）：</p>
<ul>
<li>lw: 7，sw: 7，beq: 6，R 型: 7，I 型: 6</li>
<li>$CPI \approx 6.7$，$T_{clk} \approx 180ps$</li>
<li>$T \approx 1000 \times 6.7 \times 180ps = 1.206\mu s$</li>
</ul>
<hr />
<h3>习题6.11 优化多周期 MIPS 的 R 型指令数据通路</h3>
<p><strong>题干：</strong> 基于加快经常性事件的原理，优化多周期 MIPS 的 R 型指令数据通路。</p>
<p><strong>解析：</strong></p>
<p><strong>优化思想</strong>：R 型指令比例为 50%，是最频繁的指令。在 EX 阶段将 ALU 结果直接写回寄存器堆，省去单独的 WB 周期，使 R 型指令从 4 周期减为 3 周期。</p>
<p><strong>优化后 CPI</strong>：$CPI' = 0.1\times5 + 0.1\times4 + 0.1\times3 + 0.5\times3 + 0.2\times4 = 3.5$</p>
<p><strong>优化后执行时间</strong>：$T' = 1000 \times 3.5 \times 200ps = 700ns$</p>
<p><strong>加速比</strong>：$800/700 \approx 1.143$，性能提升约 14.3%。</p>
<hr />
<h3>习题6.12 多周期处理器应优化哪个部件</h3>
<p><strong>题干：</strong> 对于多周期处理器，如果可以优化一个功能部件以提升整体性能，应选择哪个？</p>
<p><strong>解析：</strong></p>
<p>多周期处理器最长阶段为取指或数据存储器读阶段（200ps），其中最主要延迟是存储器读（150ps）。因此应优先优化<strong>存储器读部件</strong>。</p>
<p>将存储器读延迟从 150ps 优化到 110ps 时，时钟周期可从 200ps 降到 160ps。继续优化不再提升性能（受其他阶段限制），因此最低成本方案是优化到刚好 110ps。</p>
<hr />
<h3>习题6.13 低功耗低速度寄存器堆方案是否有应用价值</h3>
<p><strong>题干：</strong> 现有一种寄存器堆方案，功耗降低一半，速度也降低一半，是否有应用价值？</p>
<p><strong>解析：</strong></p>
<p>速度降低一半使寄存器堆读延迟从 90ps 变为 180ps，导致时钟周期从 200ps 增加到 210ps，执行时间延长 5%。</p>
<p>若寄存器堆功耗占 CPU 总功耗超过约 9.5%，则整体能耗可能降低。因此在对性能要求不高、对功耗敏感的场合有应用价值。</p>
<hr />
<h3>习题6.14 与 习题6.15 三级时序状态机逻辑表达式</h3>
<p><strong>题干：</strong> 根据定长/变长指令周期三级时序状态机，给出状态周期电位 $M_{if}$、$M_{cal}$、$M_{ex}$ 及节拍电位 $T_1 \sim T_4$ 的逻辑表达式。</p>
<p><strong>解析：</strong></p>
<p>状态编码：高两位 $S_3S_2$ 表示机器周期，低两位 $S_1S_0$ 表示节拍。</p>
<p>$$ M_{if} = \overline{S_3}\ \overline{S_2},\quad M_{cal} = \overline{S_3}S_2,\quad M_{ex} = S_3\overline{S_2} $$</p>
<p>$$ T_1 = \overline{S_1}\ \overline{S_0},\quad T_2 = \overline{S_1}S_0,\quad T_3 = S_1\overline{S_0},\quad T_4 = S_1S_0 $$</p>
<p>定长与变长时序的输出译码表达式相同，区别在于下一状态逻辑。</p>
<hr />
<h3>习题6.16 与 习题6.17 单总线/多周期有限状态机逻辑表达式</h3>
<p><strong>题干：</strong> 根据指令执行状态转换图，设计有限状态机，给出 $S_4 \sim S_0$ 的逻辑表达式。</p>
<p><strong>解析：</strong></p>
<p>标准单总线/多周期 MIPS 子集（lw、sw、beq、add、addi）的状态编码和下一状态逻辑表达式详见教材。核心方法是对每个下一状态位 $D_i$，列出使其为 1 的所有当前状态与指令译码条件的乘积项之和。</p>
<hr />
<h3>习题6.18 与 习题6.19 微程序地址转移逻辑</h3>
<p><strong>题干：</strong> 结合微程序控制器组成框图和状态转换图，设计微程序地址转移逻辑。</p>
<p><strong>解析：</strong></p>
<p>入口地址由指令译码信号生成。例如单总线结构中：</p>
<p>$$ S_3 = add + addi + beq,\quad S_2 = lw + sw + beq,\quad S_1 = addi $$</p>
<p>地址转移多路选择器控制信号：SelEntry = DecodeEnd（译码结束选入口），SelFetch = MicroProgramEnd（微程序结束回取指），SelSeq = 顺序执行。</p>
<hr />
<h3>习题6.20 微指令格式字段位数和控制器框图</h3>
<p><strong>题干：</strong> 控制存储器容量为 $128\times32$ 位，微程序可在整个 CM 中分支跳转，判别测试条件共 3 个，采用下址字段法。求微指令各字段位数并画出逻辑框图。</p>
<p><strong>解析：</strong></p>
<ul>
<li>下址字段：128 个地址需要 7 位。</li>
<li>判别测试字段：3 种条件 + 1 种不测试，共 4 种，需要 2 位。</li>
<li>操作控制字段：$32 - 7 - 2 = 23$ 位。</li>
</ul>
<p>框图：µAR → CM → µIR → 操作控制字段（输出控制信号）+ 判别测试/下址逻辑 → 下一微地址 → µAR。</p>
<hr />
<h3>习题6.21 微指令控制字段最短编码</h3>
<p><strong>题干：</strong> 5 条微指令发出如表所示的微命令，要求控制字段最短且保持并行性。</p>
<p><strong>解析：</strong></p>
<p>分析相容性后划分字段：F1(a, 1位)、F2(b/c/d, 2位)、F3(e/f, 2位)、F4(g/h/i, 2位)、F5(j, 1位)，总 8 位。</p>
<hr />
<h3>习题6.22 支持中断的微指令和地址转移逻辑</h3>
<p><strong>题干：</strong> 结合支持中断的状态机，设计微指令和地址转移逻辑，支持 <code>eret</code> 指令和中断响应。</p>
<p><strong>解析：</strong></p>
<p>增加中断响应微程序（保存断点 EPC → 保存原因 Cause → 转入中断向量）和 <code>eret</code> 微程序（EPC → PC，清除 EXL）。</p>
<p>入口地址优先级：Int &gt; eret &gt; 普通指令。入口地址表达式需加入中断允许条件和异常级标志。</p>
<hr />
<h3>习题6.23 数据通路控制信号设计</h3>
<p><strong>题干：</strong> 某 16 位计算机数据通路如图，指令 <code>ADD (R1),R0</code> 功能为 $(R0) + ((R1)) \to (R1)$，给出执行流程和各步控制信号。</p>
<p><strong>解析：</strong></p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>微操作</th>
<th>控制信号</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>$MAR \leftarrow R1$</td>
<td>$R1_{out}, MAR_{in}$</td>
</tr>
<tr>
<td>2</td>
<td>$MDR \leftarrow M[MAR]$</td>
<td>$MemR, MDR_{in}E$</td>
</tr>
<tr>
<td>3</td>
<td>$A \leftarrow MDR$</td>
<td>$MDR_{out}, A_{in}$</td>
</tr>
<tr>
<td>4</td>
<td>$MDR \leftarrow A + R0$</td>
<td>$R0_{out}, ADD, MDR_{in}$</td>
</tr>
<tr>
<td>5</td>
<td>$M[MAR] \leftarrow MDR$</td>
<td>$MDR_{out}E, MemW$</td>
</tr>
</tbody>
</table>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>毛概复习重点</title>
    <link href="https://blog.yoake.dev/posts/%E6%AF%9B%E6%A6%82/%E5%A4%8D%E4%B9%A0%E9%87%8D%E7%82%B9/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%AF%9B%E6%A6%82/%E5%A4%8D%E4%B9%A0%E9%87%8D%E7%82%B9/</id>
    <published>2026-06-29T12:02:15.000Z</published>
    <updated>2026-06-29T12:02:15.000Z</updated>
    <summary>系统整理毛泽东思想和中国特色社会主义理论体系概论的考前复习重点，覆盖马克思主义中国化时代化、毛泽东思想、新民主主义革命、社会主义改造、邓小平理论、“三个代表”重要思想和科学发展观等核心内容。</summary>
    <content type="html"><![CDATA[<h1>毛概复习重点</h1>
<p>这篇笔记按照考试常见逻辑整理《毛泽东思想和中国特色社会主义理论体系概论》的核心知识点。复习时可以先抓住一条主线：<strong>马克思主义基本原理同中国具体实际相结合、同中华优秀传统文化相结合，在中国革命、建设和改革实践中不断形成新的理论成果。</strong></p>
<hr />
<h2>一、总复习框架</h2>
<p>毛概的内容看起来多，但可以压缩成三条线：</p>
<table>
<thead>
<tr>
<th>复习线索</th>
<th>核心问题</th>
<th>重点内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>理论成果线</strong></td>
<td>马克思主义中国化时代化形成了哪些成果？</td>
<td>毛泽东思想、中国特色社会主义理论体系</td>
</tr>
<tr>
<td><strong>历史阶段线</strong></td>
<td>中国革命、建设、改革分别解决什么问题？</td>
<td>新民主主义革命、社会主义改造、社会主义建设探索、改革开放</td>
</tr>
<tr>
<td><strong>核心命题线</strong></td>
<td>每个理论回答了什么时代问题？</td>
<td>什么是马克思主义中国化、什么是社会主义、建设什么样的党、实现什么样的发展</td>
</tr>
</tbody>
</table>
<p>最容易出简答题和论述题的内容，通常集中在：</p>
<ul>
<li>毛泽东思想的历史地位和活的灵魂。</li>
<li>新民主主义革命的道路、对象、性质、三大法宝和经济纲领。</li>
<li>过渡时期总路线与社会主义改造。</li>
<li>邓小平理论的首要基本理论问题、社会主义初级阶段基本路线、社会主义市场经济理论。</li>
<li>“三个代表”重要思想和科学发展观的核心内容。</li>
</ul>
<hr />
<h2>二、马克思主义中国化时代化</h2>
<h3>1. 马克思主义的鲜明特征</h3>
<p>马克思主义具有四个鲜明特征：</p>
<ul>
<li><strong>科学的理论</strong>：揭示了自然界、人类社会和思维发展的普遍规律。</li>
<li><strong>人民的理论</strong>：第一次创立了人民实现自身解放的思想体系。</li>
<li><strong>实践的理论</strong>：不是书斋里的抽象学说，而是在实践中产生、检验和发展的理论。</li>
<li><strong>不断发展的开放的理论</strong>：能够随着时代、实践和科学发展而不断丰富。</li>
</ul>
<h3>2. 马克思主义中国化时代化的正式提出</h3>
<p>1938 年，毛泽东在党的六届六中全会上作《论新阶段》报告，标志着<strong>马克思主义中国化</strong>这一命题的正式提出。</p>
<p>这个知识点常考“时间 + 人物 + 会议 + 文献”：</p>
<blockquote>
<p><strong>1938 年</strong>，<strong>毛泽东</strong>在<strong>党的六届六中全会</strong>上作《论新阶段》报告，正式提出<strong>马克思主义中国化</strong>。</p>
</blockquote>
<h3>3. 理论成果及其关系</h3>
<p>马克思主义中国化时代化的理论成果主要包括：</p>
<ul>
<li><strong>毛泽东思想</strong>：马克思主义中国化时代化的第一个重大理论成果。</li>
<li><strong>中国特色社会主义理论体系</strong>：包括邓小平理论、“三个代表”重要思想、科学发展观、习近平新时代中国特色社会主义思想。</li>
</ul>
<p>二者之间的关系可以概括为：<strong>一脉相承、与时俱进</strong>。</p>
<p>毛泽东思想和中国特色社会主义理论体系，都是马克思列宁主义在中国的运用和发展，都是马克思主义基本原理同中国具体实际相结合、同中华优秀传统文化相结合的产物。中国特色社会主义理论体系在新的历史条件下进一步丰富和发展了毛泽东思想，共同服务于中国革命、建设和改革的实践。</p>
<hr />
<h2>三、毛泽东思想</h2>
<h3>1. 毛泽东思想的正式确立</h3>
<p>1945 年，党的七大通过的《中国共产党章程》明确规定，把毛泽东思想作为全党统一的思想，标志着毛泽东思想确立为党必须长期坚持的指导思想。</p>
<blockquote>
<p><strong>1945 年</strong>，<strong>党的七大</strong>通过的《中国共产党章程》明确规定，把<strong>毛泽东思想</strong>作为全党统一的思想。</p>
</blockquote>
<h3>2. 毛泽东思想的历史地位</h3>
<p>毛泽东思想的历史地位可以从三个角度记忆：</p>
<ol>
<li><strong>理论成果</strong>：是马克思主义中国化时代化的第一个重大理论成果，实现了马克思主义中国化的第一次历史性飞跃。</li>
<li><strong>实践指南</strong>：是被实践证明了的关于中国革命和建设的正确理论原则和经验总结，是中国革命和建设的科学指南。</li>
<li><strong>精神财富</strong>：是中国共产党和中国人民宝贵的精神财富，必须长期坚持并不断发展。</li>
</ol>
<h3>3. 毛泽东思想活的灵魂</h3>
<p>毛泽东思想活的灵魂包括：<strong>实事求是、群众路线、独立自主</strong>。</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>含义</th>
<th>地位</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>实事求是</strong></td>
<td>一切从实际出发，理论联系实际，坚持在实践中检验真理和发展真理</td>
<td>毛泽东思想的精髓，是思想路线的核心</td>
</tr>
<tr>
<td><strong>群众路线</strong></td>
<td>一切为了群众，一切依靠群众，从群众中来，到群众中去</td>
<td>党的生命线和根本工作路线</td>
</tr>
<tr>
<td><strong>独立自主</strong></td>
<td>坚持独立思考，走自己的路，把立足点放在依靠自己力量的基础上</td>
<td>立党立国的重要原则</td>
</tr>
</tbody>
</table>
<p>三者的内在关系：</p>
<ul>
<li><strong>实事求是</strong>解决“从哪里出发、怎样认识真理”的问题，是思想路线。</li>
<li><strong>群众路线</strong>解决“依靠谁、为了谁、怎样工作”的问题，是工作路线。</li>
<li><strong>独立自主</strong>解决“立足哪里、怎样走自己的路”的问题，是根本立足点。</li>
</ul>
<p>三者相互联系、有机统一，共同构成毛泽东思想活的灵魂。</p>
<h3>4. 当代大学生如何践行活的灵魂</h3>
<p>如果遇到联系实际类题目，可以按“三点对应法”回答：</p>
<ul>
<li><strong>践行实事求是</strong>：学习中求真务实，不弄虚作假；根据自身实际制定计划；尊重客观规律，在实践中检验和改进方法。</li>
<li><strong>践行群众路线</strong>：关心同学，倾听他人意见，积极参与集体活动；增强服务意识，虚心向群众学习。</li>
<li><strong>践行独立自主</strong>：培养独立思考能力，不盲从、不随波逐流；在学习和生活中自立自强，勇于承担责任，坚定理想信念。</li>
</ul>
<hr />
<h2>四、新民主主义革命</h2>
<h3>1. 近代中国的基本国情和主要矛盾</h3>
<p>近代中国的基本国情是：<strong>半殖民地半封建社会</strong>。</p>
<p>近代中国社会的主要矛盾包括：</p>
<ul>
<li><strong>帝国主义和中华民族的矛盾</strong>，这是最主要矛盾。</li>
<li><strong>封建主义和人民大众的矛盾</strong>。</li>
</ul>
<p>这决定了近代中国革命的根本任务是推翻帝国主义、封建主义和官僚资本主义的统治，实现民族独立和人民解放。</p>
<h3>2. 新民主主义革命的对象、性质和领导力量</h3>
<table>
<thead>
<tr>
<th>问题</th>
<th>答案</th>
</tr>
</thead>
<tbody>
<tr>
<td>革命对象</td>
<td>帝国主义、封建主义、官僚资本主义</td>
</tr>
<tr>
<td>革命性质</td>
<td>资产阶级民主革命，是新式的、特殊的资产阶级民主革命</td>
</tr>
<tr>
<td>领导力量</td>
<td>无产阶级及其政党，即中国共产党</td>
</tr>
<tr>
<td>革命主力</td>
<td>农民阶级</td>
</tr>
<tr>
<td>革命动力</td>
<td>工人阶级、农民阶级、城市小资产阶级和民族资产阶级</td>
</tr>
</tbody>
</table>
<p>无产阶级之所以能够成为新民主主义革命的领导力量，主要因为：</p>
<ol>
<li>中国民族资产阶级具有软弱性和妥协性，不能彻底完成反帝反封建任务。</li>
<li>中国无产阶级深受帝国主义、封建主义、官僚资本主义三重压迫，革命最彻底。</li>
<li>中国无产阶级集中程度高，便于组织和团结。</li>
<li>中国无产阶级大多出身于破产农民，与农民有天然联系，便于形成工农联盟。</li>
<li>中国无产阶级有中国共产党的坚强领导。</li>
</ol>
<h3>3. 新民主主义革命道路</h3>
<p>新民主主义革命道路是：<strong>农村包围城市、武装夺取政权</strong>。</p>
<p>走这条道路的原因：</p>
<ol>
<li>中国内无民主制度而受封建主义压迫，外无民族独立而受帝国主义压迫。</li>
<li>农民占全国人口绝大多数，是无产阶级可靠的同盟军和革命的主力军。</li>
<li>中国社会政治经济发展极端不平衡，为在农村开展革命斗争提供了可能。</li>
<li>中国革命愿望强烈，具备广泛群众基础。</li>
<li>全国革命形势继续向前发展，为建设革命根据地提供了客观条件。</li>
<li>有相当力量的正式红军存在，并且有党的正确领导。</li>
</ol>
<h3>4. 新民主主义革命的三大法宝</h3>
<p>新民主主义革命的三大法宝是：</p>
<ul>
<li><strong>统一战线</strong></li>
<li><strong>武装斗争</strong></li>
<li><strong>党的建设</strong></li>
</ul>
<p>可以这样理解：</p>
<pre><code>统一战线：团结谁
武装斗争：怎样斗争
党的建设：由谁领导、如何保证领导
</code></pre>
<p>三者相互联系，其中党的建设是核心保证，统一战线和武装斗争必须在党的领导下展开。</p>
<h3>5. 新民主主义经济纲领</h3>
<p>新民主主义经济纲领包括三项：</p>
<ol>
<li><strong>没收封建地主阶级的土地归农民所有</strong>：这是新民主主义革命的主要内容，目的在于废除封建地主土地所有制。</li>
<li><strong>没收官僚资本归新民主主义国家所有</strong>：这是新民主主义革命的题中之义，为建立新民主主义国家政权、实现向社会主义过渡奠定基础。</li>
<li><strong>保护民族工商业</strong>：这是新民主主义经济纲领中最具特色的一项，由中国落后的生产力和新民主主义革命的性质决定。</li>
</ol>
<h3>6. 中国共产党的初心和使命</h3>
<p>中国共产党的初心和使命是：<strong>为中国人民谋幸福，为中华民族谋复兴</strong>。</p>
<hr />
<h2>五、社会主义改造与过渡时期</h2>
<h3>1. 新中国成立初期的社会性质</h3>
<p>从新中国成立到社会主义改造基本完成，我国社会性质是：<strong>新民主主义社会</strong>。</p>
<p>新民主主义社会不是一个独立的社会形态，而是一个过渡性质的社会，目标是逐步过渡到社会主义社会。</p>
<h3>2. 过渡时期总路线</h3>
<p>党在过渡时期的总路线和总任务可以概括为：<strong>一化三改</strong>。</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>一化</td>
<td>逐步实现国家的社会主义工业化</td>
</tr>
<tr>
<td>三改</td>
<td>逐步实现对农业、手工业和资本主义工商业的社会主义改造</td>
</tr>
</tbody>
</table>
<p>其中，<strong>社会主义工业化是主体</strong>，三大改造围绕工业化展开，为社会主义工业化创造制度条件。</p>
<h3>3. 资本主义工商业的社会主义改造</h3>
<p>资本主义工商业改造的方式是：<strong>和平赎买</strong>。</p>
<p>具体做法包括：</p>
<ol>
<li>用和平赎买的方法改造资本主义工商业。</li>
<li>采取从低级到高级的国家资本主义过渡形式。</li>
<li>把资本主义工商业者改造成为自食其力的社会主义劳动者。</li>
</ol>
<h3>4. 社会主义改造完成后的主要矛盾</h3>
<p>社会主义改造基本完成后，我国社会的主要矛盾是：</p>
<ul>
<li>人民对于建立先进工业国的要求同落后的农业国现实之间的矛盾。</li>
<li>人民对于经济文化迅速发展的需要同当前经济文化不能满足人民需要的状况之间的矛盾。</li>
</ul>
<hr />
<h2>六、社会主义建设道路初步探索</h2>
<p>社会主义建设道路初步探索的经验教训，常作为简答题出现，可以按六点记忆：</p>
<ol>
<li>必须把马克思主义基本原理同中国具体实际相结合，探索符合中国国情的社会主义建设道路。</li>
<li>必须正确认识社会主义社会的主要矛盾和根本任务，集中力量发展生产力。</li>
<li>必须从实际出发建设社会主义，建设规模和速度要与国力相适应，不能急于求成。</li>
<li>必须发展社会主义民主，健全社会主义法制。</li>
<li>必须坚持党的民主集中制和集体领导制度，加强执政党建设。</li>
<li>必须坚持对外开放，借鉴和吸收人类文明成果，不能关起门来搞建设。</li>
</ol>
<p>这个部分的答题关键词是：<strong>结合国情、发展生产力、实事求是、民主法制、党的建设、对外开放</strong>。</p>
<hr />
<h2>七、邓小平理论</h2>
<h3>1. 首要的基本理论问题</h3>
<p>邓小平理论首要的基本理论问题是：<strong>什么是社会主义、怎样建设社会主义</strong>。</p>
<p>这个问题之所以重要，是因为它直接关系到社会主义本质、社会主义发展道路、改革开放方向和现代化建设目标。</p>
<h3>2. 邓小平理论的现实依据</h3>
<p>邓小平理论形成的现实依据主要来自改革开放和社会主义现代化建设实践：</p>
<ol>
<li>党的十一届三中全会后，党和国家工作重心转移到经济建设上来。</li>
<li>改革开放各项探索实践，积累了大量成功经验和失误教训。</li>
<li>农村家庭联产承包责任制、城市经济体制改革、经济特区对外开放等实践不断推进，提出了大量新问题。</li>
</ol>
<h3>3. 邓小平理论的历史地位</h3>
<p>邓小平理论的历史地位可以概括为：</p>
<ol>
<li>马克思主义中国化第二次历史性飞跃的理论成果。</li>
<li>中国特色社会主义理论体系的开篇之作。</li>
<li>改革开放和社会主义现代化建设的科学指南。</li>
<li>党和国家必须长期坚持的指导思想。</li>
</ol>
<h3>4. 社会主义初级阶段基本路线</h3>
<p>社会主义初级阶段基本路线的主要内容是：</p>
<blockquote>
<p>领导和团结全国各族人民，以经济建设为中心，坚持四项基本原则，坚持改革开放，自力更生，艰苦创业，为把我国建设成为富强、民主、文明的社会主义现代化国家而奋斗。</p>
</blockquote>
<p>可以概括为：<strong>一个中心，两个基本点</strong>。</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>含义</th>
<th>地位</th>
</tr>
</thead>
<tbody>
<tr>
<td>一个中心</td>
<td>以经济建设为中心</td>
<td>兴国之要</td>
</tr>
<tr>
<td>两个基本点之一</td>
<td>坚持四项基本原则</td>
<td>立国之本</td>
</tr>
<tr>
<td>两个基本点之二</td>
<td>坚持改革开放</td>
<td>强国之路</td>
</tr>
</tbody>
</table>
<h3>5. 社会主义初级阶段理论</h3>
<p>社会主义初级阶段理论包含两层含义：</p>
<ol>
<li>我国社会已经是社会主义社会，必须坚持而不能离开社会主义。</li>
<li>我国社会主义社会还处在初级阶段，必须从这个实际出发，不能超越阶段。</li>
</ol>
<p>这两句话要一起记，前一句强调社会性质，后一句强调发展程度。</p>
<h3>6. 社会主义市场经济理论</h3>
<p>社会主义市场经济理论的主要观点：</p>
<ol>
<li>计划经济和市场经济不是划分社会制度的标志，计划和市场都是经济手段。</li>
<li>社会主义实行市场经济，是为了更好地解放和发展生产力。</li>
<li>市场经济可以同社会主义制度结合起来，为社会主义服务。</li>
</ol>
<p>这一理论的重大突破在于：</p>
<ul>
<li>破除了把计划经济等同于社会主义、把市场经济等同于资本主义的思想束缚。</li>
<li>突破了传统社会主义经济体制观念，明确了我国经济体制改革的目标是建立社会主义市场经济体制。</li>
</ul>
<hr />
<h2>八、“和平统一、一国两制”</h2>
<p>“和平统一、一国两制”的核心是：<strong>祖国统一，坚持一个中国原则</strong>。</p>
<p>其中，“一国”是前提，主权统一是核心。在祖国统一的前提下，国家主体实行社会主义制度，香港、澳门、台湾保持原有社会制度和生活方式长期不变。</p>
<p>复习时要抓住三组关系：</p>
<ul>
<li><strong>一国与两制</strong>：一国是前提和基础，两制从属于一国。</li>
<li><strong>主体与特别地区</strong>：国家主体坚持社会主义制度，特别地区保持原有制度和生活方式。</li>
<li><strong>和平统一与国家主权</strong>：和平方式是基本方针，但维护国家主权、安全和领土完整是底线。</li>
</ul>
<hr />
<h2>九、“三个代表”重要思想</h2>
<h3>1. 创造性回答的问题</h3>
<p>“三个代表”重要思想创造性回答了：<strong>建设什么样的党、怎样建设党</strong>。</p>
<h3>2. “三个代表”的基本内容</h3>
<p>中国共产党必须始终代表：</p>
<ul>
<li>中国先进生产力的发展要求。</li>
<li>中国先进文化的前进方向。</li>
<li>中国最广大人民的根本利益。</li>
</ul>
<h3>3. 代表先进生产力发展要求的内涵</h3>
<p>代表中国先进生产力的发展要求，可以从五点理解：</p>
<ol>
<li>社会主义的根本任务是发展社会生产力。</li>
<li>社会主义现代化必须建立在发达生产力基础之上，通过改革不断促进生产力发展。</li>
<li>广大工人、农民和知识分子始终是推动先进生产力发展和社会全面进步的根本力量。</li>
<li>人是生产力中最活跃的因素，<strong>科学技术是第一生产力</strong>，是先进生产力的集中体现和主要标志。</li>
<li>党的全部理论和工作必须符合生产力发展的客观规律，不断解放和发展生产力，并通过发展生产力持续改善人民生活。</li>
</ol>
<h3>4. 思想政治工作的地位</h3>
<p>党的思想政治工作是经济工作和其他一切工作的生命线，是我们党和社会主义国家的重要政治优势。</p>
<hr />
<h2>十、科学发展观</h2>
<p>科学发展观主要回答了：<strong>实现什么样的发展、怎样发展</strong>。</p>
<p>其主要内容可以用表格速记：</p>
<table>
<thead>
<tr>
<th>方面</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>第一要义</td>
<td>推动经济社会发展</td>
</tr>
<tr>
<td>核心立场</td>
<td>以人为本</td>
</tr>
<tr>
<td>基本要求</td>
<td>全面协调可持续</td>
</tr>
<tr>
<td>根本方法</td>
<td>统筹兼顾</td>
</tr>
</tbody>
</table>
<p>其中，<strong>以人为本</strong>是科学发展观的核心立场。理解科学发展观时，要把“发展”与“为了谁发展、依靠谁发展、发展成果由谁共享”联系起来。</p>
<hr />
<h2>十一、常考政治制度与重要表述</h2>
<h3>1. 发展是党执政兴国的第一要务</h3>
<p>党执政兴国的第一要务是：<strong>发展</strong>。</p>
<p>发展是解决中国一切问题的基础和关键。</p>
<h3>2. 社会主义民主政治</h3>
<p>社会主义民主政治的本质和核心是：<strong>人民当家作主</strong>。</p>
<p>必须坚定不移走中国特色社会主义政治发展道路，坚持党的领导、人民当家作主、依法治国有机统一。</p>
<h3>3. 根本政治制度</h3>
<p>我国的根本政治制度是：<strong>人民代表大会制度</strong>。</p>
<h3>4. 基本政治制度</h3>
<p>我国的基本政治制度包括：</p>
<ul>
<li>中国共产党领导的多党合作和政治协商制度。</li>
<li>民族区域自治制度。</li>
<li>基层群众自治制度。</li>
</ul>
<h3>5. 实事求是为什么是党的思想路线的核心</h3>
<p>实事求是是马克思主义的根本观点，是中国共产党人认识世界、改造世界的根本要求，是我们党的基本思想方法、工作方法和领导方法。</p>
<h3>6. 群众路线为什么是党的根本工作路线</h3>
<p>一切为了群众，一切依靠群众，从群众中来，到群众中去，是党联系群众的根本途径，是党的生命线和根本工作路线。</p>
<hr />
<h2>十二、易混点速查</h2>
<table>
<thead>
<tr>
<th>易混内容</th>
<th>正确记法</th>
</tr>
</thead>
<tbody>
<tr>
<td>马克思主义中国化正式提出</td>
<td>1938 年，毛泽东，党的六届六中全会，《论新阶段》</td>
</tr>
<tr>
<td>毛泽东思想确立为党的指导思想</td>
<td>1945 年，党的七大，《中国共产党章程》</td>
</tr>
<tr>
<td>毛泽东思想活的灵魂</td>
<td>实事求是、群众路线、独立自主</td>
</tr>
<tr>
<td>近代中国最主要矛盾</td>
<td>帝国主义和中华民族的矛盾</td>
</tr>
<tr>
<td>近代中国基本国情</td>
<td>半殖民地半封建社会</td>
</tr>
<tr>
<td>新民主主义革命对象</td>
<td>帝国主义、封建主义、官僚资本主义</td>
</tr>
<tr>
<td>新民主主义革命道路</td>
<td>农村包围城市、武装夺取政权</td>
</tr>
<tr>
<td>新民主主义革命三大法宝</td>
<td>统一战线、武装斗争、党的建设</td>
</tr>
<tr>
<td>过渡时期总路线</td>
<td>一化三改</td>
</tr>
<tr>
<td>过渡时期总路线主体</td>
<td>社会主义工业化</td>
</tr>
<tr>
<td>资本主义工商业改造方式</td>
<td>和平赎买</td>
</tr>
<tr>
<td>邓小平理论首要基本理论问题</td>
<td>什么是社会主义、怎样建设社会主义</td>
</tr>
<tr>
<td>初级阶段基本路线</td>
<td>一个中心，两个基本点</td>
</tr>
<tr>
<td>四项基本原则</td>
<td>立国之本</td>
</tr>
<tr>
<td>改革开放</td>
<td>强国之路</td>
</tr>
<tr>
<td>“一国两制”的核心</td>
<td>祖国统一，坚持一个中国原则</td>
</tr>
<tr>
<td>“三个代表”回答的问题</td>
<td>建设什么样的党、怎样建设党</td>
</tr>
<tr>
<td>科学发展观核心立场</td>
<td>以人为本</td>
</tr>
<tr>
<td>党执政兴国第一要务</td>
<td>发展</td>
</tr>
<tr>
<td>社会主义民主政治本质和核心</td>
<td>人民当家作主</td>
</tr>
<tr>
<td>根本政治制度</td>
<td>人民代表大会制度</td>
</tr>
</tbody>
</table>
<hr />
<h2>十三、简答题答题模板</h2>
<h3>1. 回答历史地位类题目</h3>
<p>历史地位类题目可以按“理论意义 + 实践意义 + 长远意义”组织：</p>
<pre><code>第一，它是……的重要理论成果。
第二，它是……实践的科学指南。
第三，它是党和人民的宝贵精神财富，必须长期坚持并不断发展。
</code></pre>
<p>例如回答毛泽东思想的历史地位，就可以对应写成：</p>
<ul>
<li>是马克思主义中国化时代化的第一个重大理论成果。</li>
<li>是中国革命和建设的科学指南。</li>
<li>是中国共产党和中国人民宝贵的精神财富。</li>
</ul>
<h3>2. 回答原因类题目</h3>
<p>原因类题目可以按“国情 + 阶级力量 + 客观条件 + 主观条件”展开。</p>
<p>例如回答为什么走农村包围城市、武装夺取政权道路：</p>
<ul>
<li>从国情看，中国是半殖民地半封建社会。</li>
<li>从阶级力量看，农民是革命主力军。</li>
<li>从客观条件看，中国政治经济发展不平衡，农村有建立根据地的可能。</li>
<li>从主观条件看，有党的正确领导和相当力量的红军存在。</li>
</ul>
<h3>3. 回答现实启示类题目</h3>
<p>现实启示类题目可以按“理论认识 + 实践要求 + 青年行动”组织：</p>
<blockquote>
<p>首先，要坚持正确理论指导，明确……</p>
<p>其次，要立足实际，把……落实到学习、工作和生活中。</p>
<p>最后，作为新时代大学生，要……</p>
</blockquote>
<p>写作时注意把抽象概念落到具体行动，比如学习计划、集体服务、独立思考、社会实践等。</p>
<hr />
<h2>十四、考前背诵清单</h2>
<p>最后一轮复习可以只背下面这些高频句：</p>
<ol>
<li>马克思主义具有科学性、人民性、实践性和开放性。</li>
<li>马克思主义中国化正式提出于 1938 年党的六届六中全会。</li>
<li>毛泽东思想是马克思主义中国化时代化的第一个重大理论成果。</li>
<li>毛泽东思想活的灵魂是实事求是、群众路线、独立自主。</li>
<li>近代中国的基本国情是半殖民地半封建社会。</li>
<li>新民主主义革命的对象是帝国主义、封建主义、官僚资本主义。</li>
<li>新民主主义革命道路是农村包围城市、武装夺取政权。</li>
<li>新民主主义革命三大法宝是统一战线、武装斗争、党的建设。</li>
<li>过渡时期总路线可以概括为一化三改。</li>
<li>资本主义工商业社会主义改造采取和平赎买方式。</li>
<li>邓小平理论首要的基本理论问题是什么是社会主义、怎样建设社会主义。</li>
<li>社会主义初级阶段基本路线概括为一个中心、两个基本点。</li>
<li>计划和市场都是经济手段，不是划分社会制度的标志。</li>
<li>“三个代表”重要思想回答了建设什么样的党、怎样建设党的问题。</li>
<li>科学发展观的第一要义是推动经济社会发展，核心立场是以人为本，根本方法是统筹兼顾。</li>
<li>党执政兴国的第一要务是发展。</li>
<li>社会主义民主政治的本质和核心是人民当家作主。</li>
<li>我国根本政治制度是人民代表大会制度。</li>
</ol>
<p>把这些句子背熟，再把每个句子向外扩展两到三点，基本就能覆盖大多数简答题和论述题。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="毛概"/>
  </entry>
  <entry>
    <title>指令系统</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%8C%87%E4%BB%A4%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%8C%87%E4%BB%A4%E7%B3%BB%E7%BB%9F/</id>
    <published>2026-06-29T09:35:00.000Z</published>
    <updated>2026-06-29T09:35:00.000Z</updated>
    <summary>整理计算机指令系统，详解指令格式、操作码与地址码设计、寻址方式与指令类型，对比CISC与RISC架构，并分析程序的机器级表示。</summary>
    <content type="html"><![CDATA[<h1>第五章：指令系统</h1>
<p>本章整理计算机中的指令系统。指令系统是计算机软硬件系统之间的界面和纽带，是软件能够操控硬件进行物理计算的唯一接口。</p>
<hr />
<h2>5.1 指令系统概述</h2>
<p>指令系统主要解决一个核心问题：<strong>CPU 电路能直接识别并执行什么物理操作？</strong></p>
<p>高级语言编写的程序不能被 CPU 电路直接读取并执行，必须先经过编译与汇编翻译转换为一连串由机器指令组成的机器代码。指令系统就是 CPU 能够识别并执行的全部机器指令的集合。</p>
<pre><code>编译器面向指令系统生成机器代码。
CPU 硬件控制电路按照指令系统解释并执行机器代码。
</code></pre>
<p>指令系统作为软硬件之间的核心接口，直接决定了计算机的系统性能与硬件设计复杂度：</p>
<pre><code>flowchart BT
    Hardware[底层硬件: 译码电路、数据通路、执行控制] --&gt; ISA[接口层: 指令系统]
</code></pre>
<p>指令系统主要规定了以下六个维度的内容：</p>
<ul>
<li><strong>指令格式</strong>：规定一条指令由哪些二进制字段组成。</li>
<li><strong>操作码</strong>：规定指令要执行什么物理操作。</li>
<li><strong>寻址方式</strong>：规定如何确定源操作数与目的操作数的位置，访存类指令通常会由此形成有效地址 $EA$。</li>
<li><strong>寄存器数量</strong>：规定 CPU 内部可供指令直接使用的寄存器个数。</li>
<li><strong>数据类型</strong>：规定指令支持处理整数、浮点数、字节、字等类型。</li>
<li><strong>指令类型</strong>：规定算术运算、逻辑运算、访存、转移等基本功能。</li>
</ul>
<hr />
<h2>5.2 指令格式</h2>
<p>一条机器指令通常由<strong>操作码字段</strong>和<strong>地址码字段</strong>两部分组成：</p>
<pre><code>[ 操作码字段 ] + [ 地址码字段 ]
</code></pre>
<h3>5.2.1 指令字长度</h3>
<p>指令字长度是指一条机器指令包含的二进制代码位数。它与机器字长和存储字长在数值上不一定相等。</p>
<table>
<thead>
<tr>
<th>设计方式</th>
<th>物理特点</th>
<th>优缺点对比</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>定长指令字</strong></td>
<td>每条机器指令的二进制位数相同</td>
<td>取指和译码结构简单、速度快，非常利于流水线设计；但会浪费编码空间。</td>
</tr>
<tr>
<td><strong>变长指令字</strong></td>
<td>不同指令的二进制位数可以不同</td>
<td>编码灵活、指令紧凑；但导致取指与译码复杂，不利于流水线。</td>
</tr>
</tbody>
</table>
<h3>5.2.2 指令地址码</h3>
<p>根据指令中显式给出的操作数地址码个数，指令可分为以下四类：</p>
<h4>1. 三地址指令</h4>
<ul>
<li><strong>结构格式</strong>：<code>OP A1 A2 A3</code>（其中 $A1$ 为目的地址，$A2$ 和 $A3$ 为源操作数地址）。</li>
<li><strong>执行逻辑</strong>：$(A1) = (A2) \text{ OP } (A3)$。</li>
<li><strong>物理特点</strong>：表意清晰，但指令字长通常较长，耗费较多编码空间。</li>
</ul>
<h4>2. 二地址指令</h4>
<ul>
<li><strong>结构格式</strong>：<code>OP A1 A2</code>（其中 $A1$ 兼作目的地址，$A2$ 为源操作数地址）。</li>
<li><strong>执行逻辑</strong>：$(A1) = (A1) \text{ OP } (A2)$。</li>
<li><strong>物理特点</strong>：比三地址指令更紧凑，但运算后会覆盖掉 $A1$ 中原有的源操作数。</li>
</ul>
<h4>3. 一地址指令</h4>
<ul>
<li><strong>结构格式</strong>：<code>OP A1</code>。</li>
<li><strong>执行逻辑</strong>：
<ul>
<li>若是单操作数指令（如取反、移位）：$(A1) = \text{OP } (A1)$。</li>
<li>若是双操作数指令，则另一个源操作数隐含在 CPU 内部的累加器 ACC 中：
$$\text{ACC} = \text{ACC} \text{ OP } (A1)$$</li>
</ul>
</li>
<li><strong>物理特点</strong>：指令短小，但由于高度依赖隐含的寄存器，灵活度较差。</li>
</ul>
<h4>4. 零地址指令</h4>
<ul>
<li><strong>结构格式</strong>：<code>OP</code>。</li>
<li><strong>执行逻辑</strong>：没有显式的地址码。常用于堆栈型计算机，操作数隐含在栈顶，执行时从栈顶弹出两个数进行运算，结果压回栈顶。</li>
</ul>
<h3>5.2.3 指令操作码</h3>
<p>操作码决定指令执行什么功能。根据操作码长度的变动特性，主要有定长操作码与扩展操作码两种设计方式：</p>
<ul>
<li><strong>定长操作码</strong>：所有指令操作码长度完全相同。</li>
<li><strong>扩展操作码</strong>：不同指令的操作码长度不同。其设计核心是<strong>让地址码字段较少的指令分配较长的操作码，而地址码字段较多的指令分配较短的操作码</strong>，从而最大化利用有限的指令字长。</li>
</ul>
<hr />
<h2>5.3 寻址方式</h2>
<p>寻址方式分为指令寻址与操作数寻址两类。</p>
<h3>5.3.1 指令寻址方式</h3>
<p>指令寻址用于确定 CPU 下一条将要执行的指令地址。主要有以下两种方式：</p>
<ul>
<li><strong>顺序寻址方式</strong>：通过程序计数器 PC 自动累加一个指令字长，指向下一条相邻指令的地址。</li>
<li><strong>跳跃寻址方式</strong>：当遇到转移类指令（如无条件跳转、条件转移、函数调用）时，由指令的地址码字段计算出转移目标地址并直接送入 PC，从而改变程序的正常执行顺序。</li>
</ul>
<p>:::tip[顺序寻址在不同编址方式下的 PC 增量问题]</p>
<ul>
<li>在按字编址的存储器中，PC 增量固定为 1。</li>
<li>在按字节编址的存储器中，若指令字长为 32 位（4 字节），则 PC 的增量为 4。
:::</li>
</ul>
<h3>5.3.2 操作数寻址方式</h3>
<p>操作数寻址用于寻找指令执行时所需的源操作数或存放结果的目的地址。以下为八种最常用的操作数寻址方式，结合有效地址 $EA$ 进行解析：</p>
<h4>1. 立即寻址</h4>
<ul>
<li><strong>特点</strong>：操作数直接作为立即数存放在指令的地址码字段中，无需访问任何存储器。</li>
<li><strong>寻址公式</strong>：$\text{操作数} = \text{立即数}$</li>
<li><strong>优缺点</strong>：执行速度最快，但立即数的大小直接受制于指令中分配给地址字段的位数。</li>
</ul>
<h4>2. 直接寻址</h4>
<ul>
<li><strong>特点</strong>：指令的地址码字段直接给出了操作数在主存中的有效地址。</li>
<li><strong>寻址公式</strong>：$EA = A$</li>
<li><strong>优缺点</strong>：简单直观，但可寻址的内存范围直接受限于指令地址字段 $A$ 的宽度。</li>
</ul>
<h4>3. 间接寻址</h4>
<ul>
<li><strong>特点</strong>：指令中给出的地址 $A$ 并不是操作数的地址，而是操作数地址在内存中的存放地址。</li>
<li><strong>寻址公式</strong>：$EA = (A)$</li>
<li><strong>优缺点</strong>：可以显著扩展寻址范围，并便于实现程序的动态链接与指针定位；但由于需要两次或多次访存才能取出操作数，执行速度较慢。</li>
</ul>
<h4>4. 寄存器寻址</h4>
<ul>
<li><strong>特点</strong>：操作数存放在 CPU 内部的某个通用寄存器中，指令中给出的是寄存器编号 $Ri$。</li>
<li><strong>寻址公式</strong>：$\text{操作数} = (Ri)$</li>
<li><strong>优缺点</strong>：寄存器访问速度快，且指令中只需极少位数指明编号，有利于缩短指令字长；但通用寄存器数量有限。</li>
</ul>
<h4>5. 寄存器间接寻址</h4>
<ul>
<li><strong>特点</strong>：操作数存放在主存中，而操作数的有效地址存放在指令指定的通用寄存器 $Ri$ 中。</li>
<li><strong>寻址公式</strong>：$EA = (Ri)$</li>
<li><strong>优缺点</strong>：访存开销介于寄存器寻址和间接寻址之间，非常适合实现指针指向、链表定位和结构体处理。</li>
</ul>
<h4>6. 基址寻址</h4>
<ul>
<li><strong>特点</strong>：有效地址由基址寄存器 $BR$ 的内容与指令中的偏移量 $A$ 相加得到。基址寄存器的内容由操作系统管理，程序运行中通常保持不变。</li>
<li><strong>寻址公式</strong>：$EA = (BR) + A$</li>
<li><strong>核心用途</strong>：主要用于多道程序在内存中的动态重定位，方便内存分段管理。</li>
</ul>
<h4>7. 变址寻址</h4>
<ul>
<li><strong>特点</strong>：有效地址由指令中的形式地址 $A$ 与变址寄存器 $IX$ 的内容相加得到。变址寄存器的内容在程序运行中经常改变。</li>
<li><strong>寻址公式</strong>：$EA = A + (IX)$</li>
<li><strong>核心用途</strong>：主要用于数组访问与循环结构设计。可以将形式地址 $A$ 作为数组首地址，由 $IX$ 充当数组下标的增量。</li>
</ul>
<h4>8. 相对寻址</h4>
<ul>
<li><strong>特点</strong>：有效地址由程序计数器 PC 的内容与指令中的偏移量 $A$ 相加得到。</li>
<li><strong>寻址公式</strong>：$EA = (PC) + A$</li>
<li><strong>核心用途</strong>：主要用于条件转移指令和循环跳转。其最大优势是无论程序被加载到内存的什么位置，转移目标与当前指令的相对位移保持不变，从而使程序代码具有可浮动性。</li>
</ul>
<p>:::tip[常见寻址方式特点对比简答考点]</p>
<ol>
<li><strong>立即寻址</strong>：不需要访存，执行速度快，但数据大小受限。</li>
<li><strong>直接寻址</strong>：只需一次访存，寻址范围受限。</li>
<li><strong>间接寻址</strong>：至少两次访存，能扩大寻址范围，便于使用指针.</li>
<li><strong>寄存器寻址</strong>：访问通用寄存器，不需要访存，速度快。</li>
<li><strong>寄存器间接寻址</strong>：只需一次访存，便于编制循环程序。</li>
<li><strong>相对寻址</strong>：便于程序在内存中浮动。</li>
<li><strong>基址寻址</strong>：有利于多道程序设计，面向操作系统。</li>
<li><strong>变址寻址</strong>：适合处理数组问题，面向程序员。
:::</li>
</ol>
<p>:::note[经典例题]
设当前 PC 的值为 <code>2000H</code>，一地址转移指令的相对偏移量为 <code>-100D</code>。若指令字长为 2 字节，按字节编址，求转移目标有效地址。</p>
<p><strong>解析步骤</strong>：</p>
<ol>
<li>CPU 取出该指令后，PC 的值自动增加，指向下一条指令的地址：
$$\text{当前 PC} = 2000\text{H} + 2 = 2002\text{H}$$</li>
<li>将十进制偏移量 <code>-100D</code> 转换为十六进制补码：
$$-100\text{D} = -64\text{H} \implies \text{其补码为 } 10000\text{H} - 64\text{H} = \text{FF9CH}$$</li>
<li>计算有效地址 $EA$：
$$EA = (PC) + A = 2002\text{H} - 64\text{H} = 1F9EH$$</li>
</ol>
<p>故转移目标有效地址为 <code>1F9EH</code>。
:::</p>
<hr />
<h2>5.4 指令类型</h2>
<p>指令按照其执行的具体功能，通常分为以下六大类：</p>
<ul>
<li><strong>数据传送指令</strong>：用于在寄存器、主存物理单元以及外设接口之间传输数据。例如 MIPS 中的 <code>lw</code>（从主存加载字到寄存器）、<code>sw</code>（将寄存器中的字存入主存）和寄存器拷贝 <code>move</code>。</li>
<li><strong>算术运算指令</strong>：执行有符号或无符号的加、减、乘、除运算。</li>
<li><strong>逻辑运算指令</strong>：执行按位与 <code>and</code>、按位或 <code>or</code>、按位异或 <code>xor</code> 等操作。</li>
<li><strong>移位指令</strong>：执行左移与右移。左移 $k$ 位在不发生溢出的前提下相当于乘以 $2^k$。<strong>逻辑右移</strong>高位补 0，<strong>算术右移</strong>为了保持有符号数的符号不变，高位空出处一律补符号位。</li>
<li><strong>程序控制指令</strong>：用于人为改变程序的执行次序。包括无条件跳转 <code>j</code>、条件分支 <code>beq</code>、函数调用 <code>jal</code> 以及函数返回 <code>jr</code>。</li>
<li><strong>输入输出指令</strong>：用于 CPU 与外设进行数据交互。</li>
</ul>
<hr />
<h2>5.5 指令格式设计</h2>
<p>在设计机器指令格式时，必须在有限的二进制位数内，合理权衡和平衡以下四个相互制约的因素：<strong>指令字长</strong>、<strong>操作码字段宽度</strong>、<strong>寄存器数量</strong>以及<strong>寻址地址范围</strong>。</p>
<h3>5.5.1 核心设计原则</h3>
<ul>
<li><strong>字长与位数的权衡</strong>：在固定指令字长的条件下（如定长 32 位），若给寄存器编号字段分配过多位数，则会导致立即数字段或偏移量字段的位数减少，从而压缩了寻址范围。</li>
<li><strong>操作码与地址码的博弈</strong>：采用扩展操作码技术时，必须预留出特定的操作码前缀作为标志位（如将全 1 作为扩展标志），这会挤占地址码字段，需要在指令表达能力的丰富度与寻址空间的广度之间做折中。</li>
</ul>
<h3>5.5.2 扩展操作码设计实例</h3>
<p>:::tip[扩展操作码设计例题]
设指令字长为 16 位，每个地址码为 4 位。设计一个指令系统，包含：</p>
<ul>
<li>15 条三地址指令</li>
<li>15 条二地址指令</li>
<li>15 条一地址指令</li>
<li>16 条零地址指令</li>
</ul>
<p><strong>设计方案</strong>：</p>
<ol>
<li><strong>三地址指令</strong>：格式为 <code>[OP (4位)] [A1 (4位)] [A2 (4位)] [A3 (4位)]</code>。
<ul>
<li>操作码取值范围为 <code>0000</code> 到 <code>1110</code>，共 15 条。</li>
<li>留下一个状态 <code>1111</code> 作为扩展标志。</li>
</ul>
</li>
<li><strong>二地址指令</strong>：格式为 <code>[OP (8位)] [A1 (4位)] [A2 (4位)]</code>。
<ul>
<li>高 4 位固定为 <code>1111</code>。</li>
<li>第 5 到第 8 位取值范围为 <code>0000</code> 到 <code>1110</code>，共 15 条（即操作码为 <code>1111 0000</code> 到 <code>1111 1110</code>）。</li>
<li>留下 <code>1111 1111</code> 作为扩展标志。</li>
</ul>
</li>
<li><strong>一地址指令</strong>：格式为 <code>[OP (12位)] [A1 (4位)]</code>。
<ul>
<li>高 8 位固定为 <code>1111 1111</code>。</li>
<li>第 9 到第 12 位取值范围为 <code>0000</code> 到 <code>1110</code>，共 15 条。</li>
<li>留下 <code>1111 1111 1111</code> 作为扩展标志。</li>
</ul>
</li>
<li><strong>零地址指令</strong>：格式为 <code>[OP (16位)]</code>。
<ul>
<li>高 12 位固定为 <code>1111 1111 1111</code>。</li>
<li>第 13 到第 16 位取值范围为 <code>0000</code> 到 <code>1111</code>，共 16 条。
:::</li>
</ul>
</li>
</ol>
<hr />
<h2>5.6 CISC 和 RISC 架构对比</h2>
<p>指令系统的设计思想主要分为 CISC 和 RISC 两大阵营。</p>
<h3>5.6.1 复杂指令系统计算机</h3>
<p>CISC 的特征是随着用户程序功能的需求增加，指令系统越来越庞大。</p>
<ul>
<li><strong>物理特征</strong>：指令数量多，支持丰富的寻址方式和多种字长；支持直接对内存数据进行算术逻辑运算。</li>
<li><strong>控制器实现</strong>：控制器多采用微程序实现，硬件电路较为庞杂，执行效率受限。</li>
</ul>
<h3>5.6.2 精简指令系统计算机</h3>
<p>RISC 的核心是通过精简出使用频率最高的少数简单指令来提升整体执行效率。</p>
<ul>
<li><strong>物理特征</strong>：指令字长固定，格式规整统一；仅保留少数规整的寻址方式；<strong>仅加载和存储指令允许访问主存</strong>，其他运算指令都在寄存器间进行。</li>
<li><strong>控制器实现</strong>：通常采用硬布线控制器，各指令执行时间多接近一个时钟周期，非常利于指令流水线设计。</li>
</ul>
<h3>5.6.3 核心技术差异汇总</h3>
<table>
<thead>
<tr>
<th>对比维度</th>
<th>CISC</th>
<th>RISC</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>指令系统规模</strong></td>
<td>指令数量多，包含大量极复杂的指令</td>
<td>仅精简出数量较少、最常用的指令</td>
</tr>
<tr>
<td><strong>指令字长</strong></td>
<td>指令字长不固定，格式复杂多变</td>
<td>大多采用定长指令字，格式规整统一</td>
</tr>
<tr>
<td><strong>寻址方式</strong></td>
<td>寻址方式极多且繁杂</td>
<td>仅保留少数几种规整的寻址方式</td>
</tr>
<tr>
<td><strong>访存特征</strong></td>
<td>绝大多数指令均可直接访问主存</td>
<td><strong>仅加载和存储指令允许访问主存</strong>，其余运算指令只能在寄存器之间进行</td>
</tr>
<tr>
<td><strong>执行时钟周期</strong></td>
<td>各指令的时钟周期差异巨大，多数指令大于 1</td>
<td>大多数常用指令的时钟周期均接近 1</td>
</tr>
<tr>
<td><strong>控制器物理实现</strong></td>
<td>多采用微程序控制器（微指令查表慢）</td>
<td>多采用硬布线控制器，以便实现高效流水化执行</td>
</tr>
<tr>
<td><strong>典型架构代表</strong></td>
<td>x86 架构</td>
<td>MIPS、RISC-V、ARM 架构</td>
</tr>
</tbody>
</table>
<p>:::important[RISC 的核心真谛简答要点]
RISC 的设计核心并非单纯追求指令数量越少越好，其根本目的在于通过<strong>简化指令格式和寻址方式</strong>，使得硬件控制器更简单、更容易实现主频提升，从而让常用指令能够在一个时钟周期内高效率地执行，为指令流水线的设计扫清硬件障碍。
:::</p>
<hr />
<h2>5.7 典型指令系统举例</h2>
<p>为了更好地理解指令系统的演化与设计差异，以下简要列举并对比四种具有代表性的指令系统：</p>
<h3>5.7.1 PDP-11指令系统</h3>
<p>PDP-11 是早期小型机中非常成功的 16 位 CISC 指令系统。其特点是寻址方式灵活（拥有 8 种寻址方式并可应用于源与目的两个操作数），使得同一条指令可以以各种组合方式运行，这直接启发了后来许多处理器的设计，但同时也导致了译码电路的复杂化。</p>
<h3>5.7.2 Intel x86指令系统</h3>
<p>x86 是典型且极具代表性的 CISC 架构体系。为了保持前向兼容性，其指令系统庞大且格式复杂。</p>
<ul>
<li><strong>物理特点</strong>：指令长度范围从 1 字节到 15 字节不等；寻址方式繁多；允许算术运算指令直接在通用寄存器与主存物理单元之间交互，导致译码逻辑在硬件实现上付出了高昂的代价。</li>
</ul>
<h3>5.7.3 MIPS指令系统</h3>
<p>MIPS 是一种非常规范的 RISC 指令集体系结构。其机器指令长度固定为 32 位，主要包含以下三种格式：</p>
<h4>1. R 型指令，即寄存器型指令</h4>
<p>主要用于寄存器之间的算术逻辑运算。</p>
<pre><code>[ op:6 ] | [ rs:5 ] | [ rt:5 ] | [ rd:5 ] | [ shamt:5 ] | [ funct:6 ]
</code></pre>
<ul>
<li><code>op</code>：主操作码，对于所有 R 型指令均为 0。</li>
<li><code>rs</code>、<code>rt</code>：源操作数寄存器编号。</li>
<li><code>rd</code>：目的寄存器编号.</li>
<li><code>shamt</code>：移位量（仅在移位指令中使用）。</li>
<li><code>funct</code>：辅助操作码（功能码），用于具体区分是加法、减法还是逻辑运算。</li>
</ul>
<p>例如指令 <code>add $t0, $t1, $t2</code>，其物理含义是 <code>$t0 = $t1 + $t2</code>。</p>
<h4>2. I 型指令，即立即数型指令</h4>
<p>用于含立即数的运算、主存加载与存储、以及条件转移指令。</p>
<pre><code>[ op:6 ] | [ rs:5 ] | [ rt:5 ] | [ immediate:16 ]
</code></pre>
<ul>
<li><code>op</code>：操作码。</li>
<li><code>rs</code>：源操作数寄存器编号。</li>
<li><code>rt</code>：目的或源操作数寄存器编号（在加载指令中充当目的寄存器，在存储指令中充当源寄存器）。</li>
<li><code>immediate</code>：16 位立即数字段，进行运算或地址偏移计算时需要进行符号扩展。</li>
</ul>
<p>例如访存指令 <code>lw $t0, 4($t1)</code>，有效地址为 $EA = ($t1) + 4$\text{，读取出的主存字被存入} <code>$t0</code>。</p>
<h4>3. J 型指令，即无条件跳转型指令</h4>
<p>用于大范围的无条件跳转。</p>
<pre><code>[ op:6 ] | [ target:26 ]
</code></pre>
<ul>
<li><code>target</code>：26 位跳转目标字段，并不是完整的 32 位地址。MIPS J 型目标地址通常由当前 PC 高 4 位与 <code>target</code> 左移 2 位后的 28 位拼接而成。</li>
</ul>
<h3>5.7.4 RISC-V指令系统</h3>
<p>与 MIPS 类似，开放且模块化的 RISC-V 也是典型的 RISC 架构。其 32 位标准格式主要包括 R 型、I 型、S 型、B 型、U 型和 J 型。</p>
<p>例如在 RISC-V 中：</p>
<ul>
<li>加法指令：<code>add x5, x6, x7</code>，物理意义为 $x5 = x6 + x7$。</li>
<li>加载字：<code>lw x5, 0(x6)</code>，有效地址为 $EA = (x6) + 0$。</li>
<li>存储字：<code>sw x5, 0(x6)</code>，将 $x5$ 寄存器中的内容存入地址为 $(x6) + 0$ 的主存中。</li>
</ul>
<hr />
<h2>5.8 程序的机器级表示与控制结构</h2>
<p>高级语言程序在运行前，必须通过编译器和汇编器翻译成指令流。在 RISC 架构中，<strong>只有加载和存储指令才能访存，所有算术逻辑运算必须在通用寄存器之间进行</strong>。</p>
<h3>5.8.1 顺序结构的机器级表示</h3>
<p>对于 C 语言赋值语句 <code>a = b + c;</code>，在机器级通常被翻译成以下指令流：</p>
<pre><code>lw  $t1, b_addr     # 从内存加载变量 b 到通用寄存器 $t1
lw  $t2, c_addr     # 从内存加载变量 c 到通用寄存器 $t2
add $t3, $t1, $t2   # 在 CPU 内部进行加法运算，结果存入 $t3
sw  $t3, a_addr     # 将寄存器 $t3 的结果写回主存中变量 a 的地址
</code></pre>
<h3>5.8.2 选择结构的机器级表示</h3>
<p>对于 C 语言条件判断结构：</p>
<pre><code>if (a == b) {
    c = d + e;
} else {
    c = d - e;
}
</code></pre>
<p>在机器级通常通过条件跳转与无条件跳转组合实现：</p>
<pre><code>    bne $t1, $t2, ELSE    # 若 a != b（即寄存器 $t1 != $t2），则跳转到 ELSE
    add $t3, $t4, $t5     # 执行 if 分支：c = d + e
    j END                 # 执行完后无条件跳转到 END 绕过 else 分支
ELSE:
    sub $t3, $t4, $t5     # 执行 else 分支：c = d - e
END:
    ...
</code></pre>
<h3>5.8.3 循环结构的机器级表示</h3>
<p>对于 C 语言循环结构：</p>
<pre><code>while (i &lt; n) {
    sum = sum + a[i];
    i++;
}
</code></pre>
<p>在机器级通常翻译为循环头部判断与尾部跳回：</p>
<pre><code>LOOP:
    bge $t1, $t2, END    # 循环变量 i ($t1) &gt;= n ($t2) 时，退出循环跳转到 END
    sll $t3, $t1, 2       # 将下标 i 乘以 4（左移 2 位），得到数组元素地址偏移量
    add $t4, $s0, $t3     # 数组首地址 $s0 与偏移量相加，得到 a[i] 的有效地址
    lw  $t5, 0($t4)       # 加载元素 a[i] 的值到寄存器 $t5
    add $s1, $s1, $t5     # 执行累加：sum ($s1) = sum + a[i]
    addi $t1, $t1, 1      # 循环变量自增：i = i + 1
    j LOOP                # 无条件跳回循环入口 LOOP 进行下一次条件判断
END:
    ...
</code></pre>
<hr />
<h2>5.9 本章易错点警示</h2>
<p>在复习和应对考试时，需要特别警惕以下高频考点和易错陷阱：</p>
<p>:::important[指令物理地址与操作数物理地址的严格区分]</p>
<ul>
<li><strong>指令地址</strong>：指该条机器指令在存储器中所处的位置，通常由程序计数器 PC 提供并控制。</li>
<li><strong>操作数有效地址</strong>：指访存类指令访问操作数时计算得到的地址 $EA$。两者来源和用途不同。
:::</li>
</ul>
<p>:::important[立即数与内存地址的区别]
立即寻址中指令地址字段给出的立即数就是<strong>操作数本身</strong>，不是内存单元地址。例如指令 <code>addi $t0, $t1, 10</code> 中，<code>10</code> 是直接参与加法计算的数值，不需要进行任何访存操作。
:::</p>
<p>:::important[间接寻址的访存次数分析]
间接寻址中，指令给出的地址 $A$ 是存放操作数物理地址的内存单元。取出一个操作数至少需要<strong>两次访问主存</strong>（第一次访存读出操作数的有效地址，第二次访存读出真正的操作数数值）。
:::</p>
<p>:::important[相对寻址的 PC 相对性]
在相对寻址中，有效地址 $EA = (PC) + A$。需要注意的是，当 CPU 取出当前转移指令后，<strong>PC 的内容会自动自增</strong>指向下一条紧邻指令的物理地址。因此，在计算实际转移目标物理地址时，公式中的 PC 值应当是<strong>转移指令下一条指令的地址</strong>，而非转移指令本身的地址。
:::</p>
<hr />
<h2>5.10 历年真题与易错题集</h2>
<h3>选择题</h3>
<h4>Q1 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：冯·诺依曼计算机中指令和数据均以二进制形式存放在存储器中，CPU区分它们的依据是______。</p>
<p>A. 指令操作码的译码结果
B. 指令和数据的寻址方式
C. 指令周期的不同阶段
D. 指令和数据所在的存储单元</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令操作码的译码发生在取指之后的译码阶段，此时 CPU 已经将该二进制代码作为指令取出了，因此译码结果不能作为区分指令 and 数据的依据。</li>
<li><strong>B [错误]</strong>：寻址方式是指令中用于指明操作数地址或下一条指令地址的方法，它不能用于在从存储器取出信息时区分该信息是指令还是数据。</li>
<li><strong>C [正确]</strong>：在冯·诺依曼计算机中，指令和数据存放在同一存储器中。CPU 区分它们的依据是指令周期的不同阶段，在取指阶段从存储器中取出的二进制代码被视为指令，在执行阶段从存储器中取出的二进制代码被视为数据。</li>
<li><strong>D [错误]</strong>：在冯·诺依曼结构中，指令和数据可以混合存放在同一个存储器中，并且可以占用相邻的存储单元，因此 CPU 无法仅仅根据信息所在的存储单元地址来区分它是指令还是数据。</li>
</ul>
<h4>Q2 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：计算机硬件能够直接识别的语言是______。</p>
<p>A. 高级语言
B. 汇编语言
C. 机器语言
D. Java 语言</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：高级语言是面向用户的程序设计语言，计算机硬件无法直接识别，必须经过编译器或解释器翻译成机器指令后才能执行。</li>
<li><strong>B [错误]</strong>：汇编语言是机器指令的符号化表示，计算机硬件无法直接识别，需要通过汇编程序将其翻译成机器语言才能执行。</li>
<li><strong>C [正确]</strong>：机器语言是由二进制代码组成的指令集，是计算机硬件逻辑电路唯一能够直接识别并执行的语言。</li>
<li><strong>D [错误]</strong>：Java 语言是高级语言的一种，需要先编译成字节码文件，再由虚拟机解释执行或通过即时编译器编译成机器码后执行，硬件无法直接识别。</li>
</ul>
<h4>Q3 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：指令译码器进行译码的是______。</p>
<p>A. 整条指令
B. 指令的操作码字段
C. 指令的地址码字段
D. 指令的操作数字段</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：整条指令包含操作码字段和地址码字段，指令译码器仅对其中的操作码字段进行译码，而地址码字段则送往地址生成逻辑或寄存器选择逻辑，不需要译码器进行操作码译码。</li>
<li><strong>B [正确]</strong>：指令译码器的核心功能是对指令的操作码字段进行译码，识别当前指令的功能类型，从而控制产生相应的操作控制信号。</li>
<li><strong>C [错误]</strong>：指令的地址码字段用于指出操作数的地址或寻址方式，通常由寻址控制逻辑进行解析，而不是由指令译码器进行译码。</li>
<li><strong>D [错误]</strong>：操作数字段如果存在于指令中，也是直接送往执行部件或作为地址，不需要经过指令译码器译码。</li>
</ul>
<h4>Q4 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：某指令系统有 190 条指令，对操作码采用定长二进制编码时，最少需要______位。</p>
<p>A. 4
B. 8
C. 16
D. 32</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：若操作码位数为 4 位，定长编码最多只能表示 $2^4 = 16$ 条指令，无法满足 190 条指令的需求。</li>
<li><strong>B [正确]</strong>：若操作码采用定长二进制编码，设最少需要 $n$ 位，则必须满足 $2^n \ge 190$。因为 $2^7 = 128 &lt; 190$ 且 $2^8 = 256 \ge 190$，所以最少需要 8 位操作码。</li>
<li><strong>C [错误]</strong>：虽然 16 位可以提供 $2^{16} = 65536$ 种编码，足以容纳 190 条指令，但它不是最少位数。</li>
<li><strong>D [错误]</strong>：虽然 32 位可以表示更多的指令，但远远超出了最少位数的要求。</li>
</ul>
<h4>Q5 (计组 2019 秋 18 级测试 选择题)</h4>
<p><strong>题干</strong>：下列关于 CISC/RISC 的叙述中，错误的是______。</p>
<p>A. RISC 机器指令比 CISC 机器指令简单
B. RISC 中通用寄存器比 CISC 多
C. RISC 的寻址方式比 CISC 少
D. CISC 比 RISC 的机器能更好的支持高级语言</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：RISC 指令通常格式规整、功能单一且指令条数少，而 CISC 指令功能复杂、长度和格式不固定，因此该叙述正确。</li>
<li><strong>B [错误]</strong>：RISC 为了减少访存操作，通常设置较多的通用寄存器以暂存数据，而 CISC 的通用寄存器数量相对较少，因此该叙述正确。</li>
<li><strong>C [错误]</strong>：RISC 仅保留了少数几种最常用的简单寻址方式，而 CISC 支持极其丰富的寻址方式，因此该叙述正确。</li>
<li><strong>D [正确]</strong>：CISC 虽以增强机器指令功能来支持高级语言为初衷，但导致了芯片结构异常复杂且难以优化；而 RISC 通过简单规整的指令配合优化编译器，能更高效地支持高级语言程序的运行，因此该叙述错误。</li>
</ul>
<h4>Q6 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：以下哪条指令实现的是无符号字节数据的读取______。</p>
<p>A. lb
B. lh
C. lw
D. lbu</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>lb</code> 指令的功能是加载字节，该指令从内存中读取一个字节并写入目标寄存器，但在写入目标寄存器时会进行有符号扩展。</li>
<li><strong>B [错误]</strong>：<code>lh</code> 指令的功能是加载半字，它从内存中读取两个字节送入目标寄存器，并进行有符号扩展。</li>
<li><strong>C [错误]</strong>：<code>lw</code> 指令的功能是加载字，它从内存中读取四个字节送入目标寄存器，不存在字节级读取和扩展的问题。</li>
<li><strong>D [正确]</strong>：<code>lbu</code> 指令的功能是无符号加载字节，它从内存中读取一个字节写入目标寄存器，并且在寄存器的高 24 位填充零，从而实现无符号字节数据的读取。</li>
</ul>
<h4>Q7 (2020-2021 第二学期 B 选择题)</h4>
<p><strong>题干</strong>：MIPS 指令集没有______类指令。</p>
<p>A. I 型指令
B. R 型指令
C. J 型指令
D. M 型指令</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：I 型指令是立即数型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>B [错误]</strong>：R 型指令是寄存器型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>C [错误]</strong>：J 型指令是无条件跳转型指令，是 MIPS 指令集的三大基本指令格式之一。</li>
<li><strong>D [正确]</strong>：MIPS 指令集只包含 R 型、I 型和 J 型三类指令，不存在 M 型指令。</li>
</ul>
<h4>Q8 (2020-2021 第 2 学期 A 选择题)</h4>
<p><strong>题干</strong>：RISC 思想主要基于的是______。</p>
<p>A. 减少指令的平均执行周期
B. 减少指令的复杂程度
C. 减少硬件的复杂程度
D. 便于编译器编写</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：精简指令集计算机（RISC）的设计思想是选择使用频率最高的一些简单指令，通过简化指令执行步骤和采用流水线技术，使得大多数指令都能在一个时钟周期内完成，其核心目标是减少指令的平均执行周期。</li>
<li><strong>B [错误]</strong>：虽然 RISC 指令较简单，但减少指令的复杂程度只是实现手段，而非 RISC 思想的最主要设计目的。</li>
<li><strong>C [错误]</strong>：虽然简化指令系统在客观上降低了控制器的硬件设计复杂度，但这并非 RISC 的核心出发点。RISC 的主要出发点仍然是优化执行时间，即通过减少指令的平均执行周期来提升性能。</li>
<li><strong>D [错误]</strong>：RISC 指令系统虽然规整，但在某些情况下，因为指令功能单一，反而要求编译器能更智能地组合简单指令来实现复杂逻辑，因此主要目的不是便于编译器编写。</li>
</ul>
<h4>Q9 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：下列指令中不属于程序控制指令的是______。</p>
<p>A. 无条件转移指令
B. 条件转移指令
C. 中断隐指令
D. 循环指令</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：无条件转移指令能够显式地改变程序的控制流，强制跳转到目标地址执行，属于程序控制指令。</li>
<li><strong>B [错误]</strong>：条件转移指令根据特定的状态条件决定是否发生控制流跳转，属于程序控制指令。</li>
<li><strong>C [正确]</strong>：中断隐指令是 CPU 硬件在响应中断时自动执行的一系列操作，它并不是一条可以在指令集中由程序员显式编写执行的机器指令，因此不属于传统程序控制指令。</li>
<li><strong>D [错误]</strong>：循环指令用于实现程序段的循环跳转控制，属于程序控制指令。</li>
</ul>
<h4>Q10 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：三地址指令 $\text{OP A1 A2 A3}$ 的功能为 $(\text{A1}) \text{ OP } (\text{A2}) \rightarrow \text{A3}$，其中 $\text{A1}$、$\text{A2}$、$\text{A3}$ 都是主存储器地址，则完成上述指令需要访存______次。</p>
<p>A. 2
B. 3
C. 4
D. 5</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：2 次访存只能满足取指令和读取一个操作数，或者其他更少地址指令的访存需求。</li>
<li><strong>B [错误]</strong>：3 次访存常见于二地址指令（取指 1 次，读取源操作数和写回目的操作数共 2 次）。</li>
<li><strong>C [正确]</strong>：三地址指令完成需要以下访存步骤：① 取指阶段从主存取出当前指令（访存 1 次）；② 取源操作数 A1（访存 1 次）；③ 取源操作数 A2（访存 1 次）；④ 将运算结果写入目的物理地址 A3（访存 1 次）。累计访存次数为 $1 + 1 + 1 + 1 = 4$ 次。</li>
<li><strong>D [错误]</strong>：5 次访存超出了完成该三地址指令执行所需要的正常访存次数。</li>
</ul>
<h4>Q11 (南阳理工学院 C 卷 选择题)</h4>
<p><strong>题干</strong>：条件转移指令执行时所依据的条件来自于______。</p>
<p>A. 指令寄存器
B. 标志寄存器
C. 程序计数器
D. 地址寄存器</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：指令寄存器用于存放当前正在执行的指令编码，不保存运算结果的状态标志。</li>
<li><strong>B [正确]</strong>：条件转移指令所依据的转移条件是先前算术逻辑运算中产生的标志位（例如零标志、溢出标志、进位标志等），这些标志位保存在标志寄存器中。</li>
<li><strong>C [错误]</strong>：程序计数器用于指向下一条将要执行的指令地址，不保存条件状态信息。</li>
<li><strong>D [错误]</strong>：地址寄存器保存的是当前 CPU 读写内存的物理单元地址，与转移条件的判断无关。</li>
</ul>
<h4>Q12 (2022-2023-2 考试 选择题)</h4>
<p><strong>题干</strong>：直接寻址的无条件转移指令的功能是将指令中的地址码送入______。</p>
<p>A. 程序计数器 PC
B. 累加器 ACC
C. 指令寄存器 IR
D. 地址寄存器 MAR</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：PC 用于存放下一条将要执行的指令的地址。直接寻址的无条件转移指令需要强行改变程序的执行顺序，因此指令中的目标转移地址码必须送入 PC，作为下一条要取指的内存地址。</li>
<li><strong>B [错误]</strong>：累加器主要用于存放操作数或算术逻辑运算结果，与保存程序跳转地址无关。</li>
<li><strong>C [错误]</strong>：指令寄存器用于存放当前正在执行的指令编码，而不是转移目标地址。</li>
<li><strong>D [错误]</strong>：存储器地址寄存器用于存放 CPU 将要访问的内存单元物理地址，转移指令的目标地址需送入 PC 来控制后续取指。</li>
</ul>
<hr />
<h3>填空与判断题</h3>
<h4>Q13 (2020-2021 第二学期 B 填空题)</h4>
<p><strong>题干</strong>：由 0、1 组成的指令为______指令。</p>
<p><strong>解析</strong>：</p>
<p>机器指令是计算机硬件能够直接识别、译码并执行的二进制代码。</p>
<p><strong>答案</strong>：机器指令。</p>
<h4>Q14 (2020-2021 第 2 学期 A 填空题)</h4>
<p><strong>题干</strong>：机器指令是由______码和地址码两部分组成。</p>
<p><strong>解析</strong>：</p>
<p>操作码说明指令要执行什么物理操作，地址码说明操作数存放的物理位置。</p>
<p><strong>答案</strong>：操作码。</p>
<h4>Q15 (南阳理工学院 C 卷 填空题)</h4>
<p><strong>题干</strong>：某计算机共有 125 条指令，采用一地址格式，则指令字至少取______位才能直接寻址 32 K 个存储单元。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>共有 125 条指令，由于 $2^6 = 64 \lt 125 \le 128 = 2^7$，操作码字段至少需要 7 位。</li>
<li>直接寻址 32K 个存储单元，32K 即 $32 \times 1024 = 2^{15}$，所以地址码字段至少需要 15 位。</li>
<li>一地址格式下，指令字长至少为：$7 + 15 = 22$ 位。</li>
</ol>
<p><strong>答案</strong>：22。</p>
<h4>Q16 (2022-2023-2 考试 填空题)</h4>
<p><strong>题干</strong>：某指令系统有 128 条指令，对操作码采用固定长度二进制编码时，最少需要______位。</p>
<p><strong>解析</strong>：</p>
<p>操作码位数 $n$ 需满足 $2^n \ge 128$，由于 $2^7 = 128$，所以定长操作码最少需要 7 位。</p>
<p><strong>答案</strong>：7。</p>
<h4>Q17 (2022-2023-2 考试 填空题)</h4>
<p><strong>题干</strong>：三地址指令 $\text{OP A1, A2, A3}$ 的功能为 $(\text{A1}) \text{ OP } (\text{A2}) \rightarrow (\text{A3})$，由 PC 给出下一条指令地址，假设 $\text{A1}$、$\text{A2}$、$\text{A3}$ 都为主存储器地址，则完成下述指令需要访存______次。</p>
<p><strong>解析</strong>：</p>
<p>取指令访存 1 次，读取 A1 访存 1 次，读取 A2 访存 1 次，写回 A3 结果访存 1 次，共计 4 次。</p>
<p><strong>答案</strong>：4。</p>
<h4>Q18 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：RISC 普遍采用微程序控制器。（    ）</p>
<p><strong>解析</strong>：</p>
<p>RISC 追求简单和高效率，通常采用硬件电路实现的硬布线控制器；CISC 则更常采用便于修改和扩展的微程序控制器。</p>
<p><strong>答案</strong>：错。</p>
<h4>Q19 (2022-2023-2 考试 判断题)</h4>
<p><strong>题干</strong>：指令采用不同寻址方式的目的是缩短指令字长，扩大寻址空间，提高编程灵活性。（    ）</p>
<p><strong>解析</strong>：</p>
<p>采用基址寻址、相对寻址、寄存器间接寻址等可以极大地扩大寻址范围，并便于支持循环、数组和多道程序动态定位。</p>
<p><strong>答案</strong>：对。</p>
<h4>Q20 (南阳理工学院 C 卷 判断题)</h4>
<p><strong>题干</strong>：CISC 指令系统复杂庞大，指令条数多，寻址方式和指令格式也多。（    ）</p>
<p><strong>解析</strong>：</p>
<p>这是 CISC 架构最为典型的核心特征。</p>
<p><strong>答案</strong>：对。</p>
<h4>Q21 (南阳理工学院 C 卷 判断题)</h4>
<p><strong>题干</strong>：x86 指令 $\text{MOV EAX, (2008H)}$ 的功能是将 $\text{2008H}$ 送入寄存器 $\text{EAX}$ 中。（    ）</p>
<p><strong>解析</strong>：</p>
<p>带括号的 <code>(2008H)</code> 表示内存地址 2008H 中的内容（即该物理单元保存的数据），而非立即数 2008H 本身。其功能是将主存 2008H 单元中的数据送入寄存器 EAX。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h3>简答与分析题</h3>
<h4>Q22 (计组 2019 秋 18 级测试 简答题)</h4>
<p><strong>题干</strong>：在直接寻址、寄存器寻址、间接寻址、变址寻址、基址寻址、寄存器间接寻址、相对寻址等寻址方式中，操作数在内存的是哪几种？速度最快的是哪种？</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>操作数在主存中</strong>的寻址方式包括：直接寻址、间接寻址、变址寻址、基址寻址、寄存器间接寻址、相对寻址。</li>
<li><strong>速度最快</strong>的是：寄存器寻址。因为寄存器寻址的操作数直接保存在 CPU 通用寄存器中，执行指令时不需要访问主存，速度显著快于访存。</li>
</ol>
<h4>Q23 (2020-2021 第二学期 B 简答题)</h4>
<p><strong>题干</strong>：MIPS32 属于精简指令集，请简述 RISC 的特点。</p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>指令精简</strong>：指令数量较少，仅保留最常用的简单指令。</li>
<li><strong>字长固定</strong>：大多数指令采用定长设计，格式规整统一，极利于译码和流水线执行。</li>
<li><strong>寄存器-寄存器结构</strong>：只有加载和存储指令允许访问主存，其他算术逻辑运算一律在通用寄存器之间进行。</li>
<li><strong>寄存器数量多</strong>：设置大量的通用寄存器，尽量将临时变量保留在 CPU 内部。</li>
<li><strong>控制器实现</strong>：控制器几乎全部采用硬布线控制器（而非微程序），以确保各指令在单周期内高效执行。</li>
</ul>
<h4>Q24 (2020-2021 第二学期 B 简答题)</h4>
<p><strong>题干</strong>：写出下表 MIPS 汇编指令对应的 C 语言语句；假设寄存器 <code>$s0</code>、<code>$s1</code>、<code>$s2</code>、<code>$s3</code>、<code>$s4</code> 对应 C 语言中的变量 <code>i</code>、<code>j</code>、<code>f</code>、<code>g</code>、<code>h</code>。</p>
<table>
<thead>
<tr>
<th>汇编指令</th>
<th>C 语言语句</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>bne $s0, $s1, else</code></td>
<td></td>
</tr>
<tr>
<td><code>add $s2, $s3, $s4</code></td>
<td></td>
</tr>
<tr>
<td><code>j exit</code></td>
<td></td>
</tr>
<tr>
<td><code>else: sub $s2, $s3, $s4</code></td>
<td></td>
</tr>
<tr>
<td><code>exit:</code></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<table>
<thead>
<tr>
<th>汇编指令</th>
<th>C 语言语句</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>bne $s0, $s1, else</code></td>
<td><code>if (i != j) goto else;</code></td>
</tr>
<tr>
<td><code>add $s2, $s3, $s4</code></td>
<td><code>f = g + h;</code></td>
</tr>
<tr>
<td><code>j exit</code></td>
<td><code>goto exit;</code></td>
</tr>
<tr>
<td><code>else: sub $s2, $s3, $s4</code></td>
<td><code>else: f = g - h;</code></td>
</tr>
<tr>
<td><code>exit:</code></td>
<td><code>exit:</code></td>
</tr>
</tbody>
</table>
<p>合并并重构为结构化的 C 语言选择分支如下：</p>
<pre><code>if (i == j) {
    f = g + h;
} else {
    f = g - h;
}
</code></pre>
<h4>Q25 (2020-2021 第二学期 B 分析题)</h4>
<p><strong>题干</strong>：已知寄存器 <code>$s0 = 0x10010000</code>，其余通用寄存器的值都为 0，且存储空间 <code>0x10010000 ~ 0x10010007</code> 的存储映像如下表所示。试指出下表各条 MIPS 汇编指令写入数据寄存器的名称及指令执行之后被写入寄存器的值（用十六进制表示，按大端字节序分析）。</p>
<table>
<thead>
<tr>
<th>地址</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x10010000</code></td>
<td><code>0xf6</code></td>
</tr>
<tr>
<td><code>0x10010001</code></td>
<td><code>0x08</code></td>
</tr>
<tr>
<td><code>0x10010002</code></td>
<td><code>0xe3</code></td>
</tr>
<tr>
<td><code>0x10010003</code></td>
<td><code>0x67</code></td>
</tr>
<tr>
<td><code>0x10010004</code></td>
<td><code>0x34</code></td>
</tr>
<tr>
<td><code>0x10010005</code></td>
<td><code>0x96</code></td>
</tr>
<tr>
<td><code>0x10010006</code></td>
<td><code>0x86</code></td>
</tr>
<tr>
<td><code>0x10010007</code></td>
<td><code>0x0f</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>MIPS 汇编指令</th>
<th>写入寄存器的名称</th>
<th>写入寄存器的值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>lb $t0, 0($s0)</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>lhu $t1, 2($s0)</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>lw $t2, 4($s0)</code></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<table>
<thead>
<tr>
<th>MIPS 汇编指令</th>
<th>写入寄存器的名称</th>
<th>写入寄存器的值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>lb $t0, 0($s0)</code></td>
<td><code>$t0</code></td>
<td><code>0xfffffff6</code></td>
</tr>
<tr>
<td><code>lhu $t1, 2($s0)</code></td>
<td><code>$t1</code></td>
<td><code>0x0000e367</code></td>
</tr>
<tr>
<td><code>lw $t2, 4($s0)</code></td>
<td><code>$t2</code></td>
<td><code>0x3496860f</code></td>
</tr>
</tbody>
</table>
<ol>
<li><code>lb</code> 读取一字节。在大端序下，地址 <code>0x10010000</code> 的内容为 <code>0xf6</code>。因为 <code>lb</code> 为有符号字节加载，其最高位为 1，加载至 32 位寄存器时需要进行符号扩展，故高位全部补 1，得到 <code>0xfffffff6</code>。</li>
<li><code>lhu</code> 读取无符号半字（即 2 字节）。大端序下，地址 <code>0x10010002</code> 和 <code>0x10010003</code> 的内容分别为 <code>0xe3</code> 与 <code>0x67</code>，拼接得到 <code>0xe367</code>。进行零扩展，高位补 0，得到 <code>0x0000e367</code>。</li>
<li><code>lw</code> 读取一字（即 4 字节）。大端序下，从 <code>0x10010004</code> 开始读取 4 字节分别为 <code>0x34</code>、<code>0x96</code>、<code>0x86</code>、<code>0x0f</code>，直接拼接得到该字的值为 <code>0x3496860f</code>。
<em>(注：若在小端序环境下，<code>lhu $t1, 2($s0)</code> 结果为 <code>0x000067e3</code>，<code>lw $t2, 4($s0)</code> 结果为 <code>0x0f869634</code>。)</em></li>
</ol>
<h4>Q26 (2020-2021 第 2 学期 A 简答题)</h4>
<p><strong>题干</strong>：已知寄存器 <code>$s0 = 0x10000000</code>，采用汇编指令实现以下功能：</p>
<ol>
<li>将地址为 <code>0x10000000</code> 的字数据读入到寄存器 <code>$t0</code>。</li>
<li>将地址为 <code>0x10000000</code> 的无符号字节数据读入到寄存器 <code>$t0</code>。</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li><code>lw $t0, 0($s0)</code></li>
<li><code>lbu $t0, 0($s0)</code></li>
</ol>
<h4>Q27 (2020-2021 第 2 学期 A 分析题)</h4>
<p><strong>题干</strong>：写出下表 MIPS 汇编指令对应的 C 语言语句；假设寄存器 <code>$a0</code>、<code>$a1</code>、<code>$a2</code> 对应 C 语言中的变量 <code>e</code>、<code>f</code>、<code>g</code>，整型数组 <code>A</code> 的起始地址放在 <code>$s0</code>。</p>
<table>
<thead>
<tr>
<th>汇编指令（指令序列）</th>
<th>C 语言语句</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>add $a0, $a1, $a2</code></td>
<td></td>
</tr>
<tr>
<td><code>add $a0, $a0, $a2</code></td>
<td></td>
</tr>
<tr>
<td><code>addi $a0, $a0, 1</code></td>
<td></td>
</tr>
<tr>
<td><code>sub $a0, $0, $a0</code> &lt;br&gt; <code>add $a0, $a0, 1</code></td>
<td></td>
</tr>
<tr>
<td><code>lw $s0, 4($s0)</code></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<table>
<thead>
<tr>
<th>汇编指令（指令序列）</th>
<th>C 语言语句</th>
<th>解析</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>add $a0, $a1, $a2</code></td>
<td><code>e = f + g;</code></td>
<td>源寄存器相加赋给目的寄存器</td>
</tr>
<tr>
<td><code>add $a0, $a0, $a2</code></td>
<td><code>e = e + g;</code></td>
<td>累加操作</td>
</tr>
<tr>
<td><code>addi $a0, $a0, 1</code></td>
<td><code>e = e + 1;</code></td>
<td>立即数加法</td>
</tr>
<tr>
<td><code>sub $a0, $0, $a0</code> &lt;br&gt; <code>add $a0, $a0, 1</code></td>
<td><code>e = 1 - e;</code></td>
<td><code>$a0 = 0 - e</code> 后加 1，即得 <code>1 - e</code></td>
</tr>
<tr>
<td><code>lw $s0, 4($s0)</code></td>
<td>寄存器 <code>$s0</code> 得到 <code>A[1]</code> 的值</td>
<td>偏移 4 字节代表取数组中的第 2 个元素（A[1]）</td>
</tr>
</tbody>
</table>
<h4>Q28 (南阳理工学院 C 卷 计算题)</h4>
<p><strong>题干</strong>：假设一台计算机主频为 $1\text{ GHz}$，在其上运行由 $2 \times 10^5$ 条指令组成的目标代码，程序主要由 $4$ 类指令组成，它们所占的比例和各自的 CPI 如下表所示：</p>
<table>
<thead>
<tr>
<th>指令类型</th>
<th>所占比例</th>
<th>CPI</th>
</tr>
</thead>
<tbody>
<tr>
<td>算术逻辑运算指令</td>
<td>60%</td>
<td>1</td>
</tr>
<tr>
<td>访存指令</td>
<td>18%</td>
<td>2</td>
</tr>
<tr>
<td>转移指令</td>
<td>12%</td>
<td>4</td>
</tr>
<tr>
<td>其他指令</td>
<td>10%</td>
<td>8</td>
</tr>
</tbody>
</table>
<p>求该计算机的基准程序 CPI 和 MIPS数各是多少？</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>计算基准程序的平均 CPI</strong>：
$\text{CPI} = 60% \times 1 + 18% \times 2 + 12% \times 4 + 10% \times 8 = 0.60 + 0.36 + 0.48 + 0.80 = 2.24$</li>
<li><strong>计算 MIPS 数</strong>：
$\text{MIPS} = \frac{\text{主频}}{\text{CPI} \times 10^6} = \frac{1 \times 10^9}{2.24 \times 10^6} = \frac{1}{2.24} \times 10^3 \approx 0.44643 \times 10^3 \approx 446.43$</li>
</ol>
<p>答：该计算机的基准程序 CPI 为 2.24，MIPS 数约为 446.43。</p>
<hr />
<h3>综合设计题</h3>
<h4>Q29 (南阳理工学院 C 卷 设计题)</h4>
<p><strong>题干</strong>：某计算机机器字长 16 位，指令系统共有 64 种操作码，操作码位数固定，且具有以下特点：</p>
<ol>
<li>采用二地址格式。</li>
<li>有寄存器寻址、直接寻址、寄存器间接寻址和相对寻址（位移量为 $-128 \sim +127$）4 种寻址方式。</li>
<li>有 16 个通用寄存器，算术运算和逻辑运算的操作数均在寄存器中，结果也在寄存器中。</li>
<li>取数/存数指令在通用寄存器和存储器之间传送数据。</li>
<li>存储器容量为 1 MB，按字节编址。<br />
<strong>要求：</strong> 设计算术逻辑指令、取数/存数指令的格式，并说明原因。</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>基本字段宽度分析</strong>：
<ul>
<li>操作码 64 种，采用定长操作码，占 6 位。</li>
<li>16 个通用寄存器，寄存器编号字段各占 4 位。</li>
<li>4 种寻址方式，寻址方式字段（M）占 2 位。</li>
</ul>
</li>
<li><strong>算术逻辑（ALU）指令设计（单字长，16 位）</strong>：
<ul>
<li>格式：<code>[OP(6位)] [R1(4位)] [R2(4位)] [保留(2位)]</code></li>
<li>原因：算术和逻辑指令的两个操作数都在寄存器中，结果写回 R1，不需要额外的访存寻址方式字段。直接提供两个寄存器编码即可，总长度为 14 位，预留 2 位，可限制在 16 位单字长内。</li>
</ul>
</li>
<li><strong>取数/存数指令设计（双字长，32 位）</strong>：
<ul>
<li>存储器容量为 1MB，按字节编址，主存物理地址需要 20 位。16 位单指令字无法直接容纳。</li>
<li><strong>直接寻址指令格式</strong>：
第一字：<code>[OP(6位)] [R(4位)] [M = 01(2位)] [A高4位(4位)]</code>
第二字：<code>[A低16位(16位)]</code>
原因：合并两部分可以提供完整的 20 位直接物理地址。</li>
<li><strong>寄存器间接寻址指令格式（单字长，16 位）</strong>：
<code>[OP(6位)] [R(4位)] [M = 10(2位)] [Raddr(4位)]</code>
原因：有效地址已经在 Raddr 寄存器中，单字长指令即可完成传送。</li>
<li><strong>相对寻址指令格式（双字长，32 位）</strong>：
第一字：<code>[OP(6位)] [R(4位)] [M = 11(2位)] [保留(4位)]</code>
第二字：<code>[位移量(16位，低8位存放-128~+127的补码，高位符号扩展)]</code>
原因：利用相对偏移量在第二字中存放，完成 PC 相对移位计算。</li>
</ul>
</li>
</ol>
<h4>Q30 (2022-2023-2 考试 设计题)</h4>
<p><strong>题干</strong>：某计算机字长为 16 位，主存地址空间大小为 128KB，按字编址。采用单字长指令格式，转移指令采用相对寻址方式，相对偏移量用补码表示，寻址方式、指令各字段定义如下图所示：</p>
<p><img src="https://img.picr2.online/img/2026/06/29/4ac6c2a1b1aa47601b263ec95d5bb939.png" alt="寻址方式与指令各字段定义" /></p>
<ol>
<li>该计算机最多有多少个通用寄存器？</li>
<li>若操作码 <code>0010B</code> 表示加法操作（助记符为 <code>add</code>），寄存器 <code>R4</code> 和 <code>R5</code> 的编号分别为 <code>100B</code> 和 <code>101B</code>，<code>R4</code> 的内容为 <code>1234H</code>，<code>R5</code> 的内容为 <code>1111H</code>，地址 <code>1234H</code> 中的内容为 <code>5678H</code>，地址 <code>5678H</code> 中的内容为 <code>1234H</code>，则汇编语言为 <code>"add (R4), R5"</code>，该指令的功能为 <code>((R4)) + (R5) → R5</code>，写出该指令的十六进制机器码。</li>
<li>该指令执行后，哪些寄存器和存储单元中的内容会改变？改变后的内容是什么？</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li>寄存器编号字段（RS 与 RD）占 3 位，所以最多有 $2^3 = 8$ 个通用寄存器。</li>
<li>指令 <code>"add (R4), R5"</code> 中：
<ul>
<li>操作码 OP = <code>0010</code>。</li>
<li>源操作数采用寄存器间接寻址 <code>(R4)</code>，因此寻址方式 MS = <code>001</code>，寄存器编号 RS = <code>100</code>。</li>
<li>目的操作数采用寄存器寻址 <code>R5</code>，因此寻址方式 MD = <code>000</code>，寄存器编号 RD = <code>101</code>。</li>
<li>拼接得到机器二进制码：<code>0010 0011 0000 0101</code>，转换为十六进制为 <code>2305H</code>。</li>
</ul>
</li>
<li>执行后，寄存器 <code>R5</code> 中的内容会发生改变。
<ul>
<li><code>(R4) = 1234H</code>，主存地址 1234H 中的数据为 <code>5678H</code>。</li>
<li><code>(R5) = 1111H</code>。</li>
<li>执行加法：<code>5678H + 1111H = 6789H</code>，写回 R5。</li>
<li>答：寄存器 <code>R5</code> 的内容改变，其新值变为 <code>6789H</code>。其他寄存器与主存单元内容均保持不变。</li>
</ul>
</li>
</ol>
<h4>Q31 (2022-2023-2 考试 设计题)</h4>
<p><strong>题干</strong>：某 CPU 如下图所示，其中 AC 为累加器，状态寄存器保存指令执行过程中的状态（ZF、OF等）。<code>a</code>、<code>b</code>、<code>c</code>、<code>d</code> 为 4 个特殊寄存器（PC、AR/MAR、IR 和 DR/MDR 中的某一个）。图中箭头表示信息传送的方向：</p>
<p><img src="https://img.picr2.online/img/2026/06/29/80458c284633255ba7c07c5dbc83c05c.png" alt="CPU结构图" /></p>
<ol>
<li>根据 CPU 的功能和结构，说明图中 <code>a</code>、<code>b</code>、<code>c</code>、<code>d</code> 4 个寄存器分别对应题目中的哪个寄存器？</li>
<li>简述 <code>LDA address</code> 的数据通路，其中 <code>address</code> 为主存地址，指令的功能是将主存 <code>address</code> 单元的内容送入 AC 中。（在数据通路描述中，请将图中的 <code>a</code>、<code>b</code>、<code>c</code>、<code>d</code>换成对应的寄存器名称）。</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li>依据图中的数据通路连接逻辑：
<ul>
<li><code>a</code> 连在主存数据端和 AC 之间，充当数据缓冲，对应 <strong>DR</strong>（MDR）。</li>
<li><code>b</code> 直接给主存输入地址，对应 <strong>AR</strong>（MAR）。</li>
<li><code>c</code> 接收 MDR 的输出，并且把指令输出给译码控制器，对应 <strong>IR</strong>。</li>
<li><code>d</code> 的输出可送入地址总线，且接收自增控制，对应 <strong>PC</strong>。</li>
</ul>
</li>
<li><code>LDA address</code> 指令的数据通路步骤：
<ul>
<li><strong>取指阶段</strong>：
<ol>
<li><code>PC → AR</code>：将程序计数器中的指令地址送入地址寄存器。</li>
<li><code>M[AR] → DR</code>：从主存中取出指令，送入数据寄存器。</li>
<li><code>DR → IR</code>：将指令送入指令寄存器。</li>
<li><code>PC + 1 → PC</code>：程序计数器自动加 1。</li>
</ol>
</li>
<li><strong>执行阶段</strong>：
5. <code>IR(address) → AR</code>：将指令中的操作数地址送入地址寄存器。
6. <code>M[AR] → DR</code>：访问主存，将操作数读入数据寄存器。
7. <code>DR → AC</code>：将数据送入累加器 AC 中。</li>
</ul>
</li>
</ol>
<h4>Q32 (2020-2021 第二学期 B / 2020-2021 第 2 学期 A 综合应用题)</h4>
<p><strong>题干</strong>：MIPS 微处理器结构如图所示：</p>
<p><img src="https://img.picr2.online/img/2026/06/29/82d77164ce3ccd61acdeb924155fe2f6.png" alt="MIPS微处理器结构" /></p>
<p>当该微处理器初始化时，通用寄存器的值与编码一致，数据存储器以字为最小单位且所有存储单元都初始化为 <code>0x5a</code>，PC 寄存器初始值为 0，指令寄存器从地址 0 开始存储下面汇编指令段的机器指令（MIPS32 每条机器指令占 4 个字节，寄存器为 32 位）。</p>
<ol>
<li>完善下面各汇编指令的机器指令（采用二进制表示）。
<ul>
<li><code>add $9, $10, $11</code>：<code>Op = 000000</code>，<code>Rs = 01010</code>，<code>Rt = 01011</code>，<code>Rd = ______</code>，<code>Shamt = 00000</code>，<code>Funct = 100000</code></li>
<li><code>beq $9, $0, b1</code>：<code>Op = 000100</code>，<code>Rs = 01001</code>，<code>Rt = 00000</code>，偏移量 = <code>______</code></li>
<li><code>or $9, $10, $11</code>：<code>Op = ______</code>，<code>Rs = ______</code>，<code>Rt = 01011</code>，<code>Rd = 01001</code>，<code>Shamt = 00000</code>，<code>Funct = 100101</code></li>
<li><code>sw $9, 2($10)</code>：<code>Op = 101011</code>，<code>Rs = 01010</code>，<code>Rt = 01001</code>，偏移量 = <code>0000000000000010</code></li>
<li><code>j exit</code>：<code>Op = 000010</code>，目标地址 = <code>______</code></li>
<li><code>B1: and $9, $10, $11</code></li>
<li><code>sw $9, 3($11)</code></li>
<li><code>exit:</code></li>
</ul>
</li>
<li>请写出指令 <code>add $9, $10, $11</code> 执行后，寄存器 <code>$9</code>、<code>$10</code>、<code>$11</code> 里面的内容。</li>
</ol>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>完善机器指令</strong>：
<ul>
<li><code>add $9, $10, $11</code> 中 Rd 对应的通用寄存器为 <code>$9</code>，故 <code>Rd = 01001</code>。</li>
<li><code>beq $9, $0, b1</code> 分支指令：当前 <code>beq</code> 指令地址为 4，下一条顺序指令地址为 8。目标标签 <code>B1</code> 地址为 20。偏移量 $\text{offset} = (20 - 8) / 4 = 3$，其 16 位二进制补码形式为 <code>0000000000000011</code>。</li>
<li><code>or $9, $10, $11</code> 属于 R 型指令：其主操作码 <code>Op = 000000</code>，源寄存器 Rs 对应 <code>$10</code>，故 <code>Rs = 01010</code>。</li>
<li><code>j exit</code> 无条件跳转指令：目标标签 <code>exit</code> 地址为 28。J 型指令的 26 位 target 字段值为 $28 / 4 = 7$，故目标地址字段二进制为 <code>00000000000000000000000111</code>（即 26 位无符号数 7）。</li>
</ul>
</li>
<li>指令 <code>add $9, $10, $11</code> 执行前，寄存器的值与它们的编码一致，即 <code>($10) = 10</code>，<code>($11) = 11</code>。执行后，<code>($9) = 10 + 11 = 21</code>（十六进制为 <code>0x00000015</code>）。
答：各寄存器内容为：<code>$9</code> 中的内容变为 <code>0x00000015</code>；<code>$10</code> 的内容保持不变，仍为 <code>0x0000000a</code>；<code>$11</code> 的内容保持不变，仍为 <code>0x0000000b</code>。</li>
</ol>
<h4>Q33 (计组 2019 秋 18 级测试 设计题)</h4>
<p><strong>题干</strong>：如下图所示为双总线结构机器的数据通路：</p>
<p><img src="https://img.picr2.online/img/2026/06/29/711b22ff0ed09f2cc2d1289392565a65.png" alt="双总线数据通路" /></p>
<p>其中，IR 为指令寄存器，PC 为程序计数器（具有自增功能），M 为主存（受 R/W 信号控制），AR 为地址寄存器，DR 为数据缓冲寄存器，ALU 有加、减控制信号决定完成何种操作，控制信号 G 控制一个门电路，连接在双总线之间。
写出 <code>SUB R1, R0</code> 指令（该指令完成 <code>(R1) - (R0) -&gt; R1</code> 的功能操作）的指令周期流程图，假设该指令的地址已放入 PC 中，并列出相应的微操作控制信号序列。</p>
<p><strong>解析</strong>：</p>
<p>指令周期流程图及微操作控制信号序列如下：</p>
<table>
<thead>
<tr>
<th>阶段</th>
<th>步骤</th>
<th>微操作功能</th>
<th>产生的控制信号</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>取指周期</strong></td>
<td>1</td>
<td><code>(PC) → AR</code></td>
<td><code>PCo, G, ARi</code></td>
</tr>
<tr>
<td></td>
<td>2</td>
<td><code>M → DR</code>, <code>(PC) + 1 → PC</code></td>
<td><code>Read, DRi</code> (PC 自增隐含在硬件中)</td>
</tr>
<tr>
<td></td>
<td>3</td>
<td><code>DR → IR</code></td>
<td><code>DRo, G, IRi</code></td>
</tr>
<tr>
<td><strong>执行周期</strong></td>
<td>4</td>
<td><code>(R1) → X</code></td>
<td><code>R1o, G, Xi</code></td>
</tr>
<tr>
<td></td>
<td>5</td>
<td><code>(R0) → Y</code></td>
<td><code>R0o, G, Yi</code></td>
</tr>
<tr>
<td></td>
<td>6</td>
<td><code>(X) - (Y) → R1</code></td>
<td><code>ALUsub, G, R1i</code></td>
</tr>
</tbody>
</table>
<ol>
<li>取指阶段：将 PC 中的地址通过 G 门送入 AR（<code>PCo, G, ARi</code>）；发出读命令从主存读出指令到 DR；再将 DR 中的指令送入 IR 译码（<code>DRo, G, IRi</code>）。</li>
<li>执行阶段：被减数 R1 通过总线和 G 门送入 ALU 的输入暂存器 X（<code>R1o, G, Xi</code>）；减数 R0 送入输入暂存器 Y（<code>R0o, G, Yi</code>）；最后调用减法操作并将运算结果写回 R1（<code>ALUsub, G, R1i</code>）。</li>
</ol>
<hr />
<h2>习题5：课后习题详解</h2>
<p><img src="https://img.picr2.online/1782817904580_image.webp" alt="1" />
<img src="https://img.picr2.online/1782817921450_image.webp" alt="2" />
<img src="https://img.picr2.online/1782817929172_image.webp" alt="3" />
<img src="https://img.picr2.online/1782817968936_image.webp" alt="4" />
<img src="https://img.picr2.online/1782818091046_PixPin_2026-06-30_19-09-46.webp" alt="5" /></p>
<p>以下按题目顺序给出题干与详细答案解析。</p>
<hr />
<h3>习题5.1 解释下列名词</h3>
<p><strong>题干：</strong>
解释下列名词：指令、指令系统、操作码、扩展操作码、地址码、寻址方式、程序计数器 PC、有效地址、存储器堆栈、寄存器堆栈、基址寄存器、变址寄存器、转子指令、CISC、RISC。</p>
<table>
<thead>
<tr>
<th>名词</th>
<th>详细解释</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>指令</strong></td>
<td>指令是计算机能够识别并执行的机器命令，通常由操作码和地址码等字段组成。操作码说明"做什么操作"，地址码说明"操作数在哪里"或"结果放在哪里"。</td>
</tr>
<tr>
<td><strong>指令系统</strong></td>
<td>指令系统是一台计算机能够执行的全部机器指令的集合，也称指令集。它规定了指令格式、指令功能、寻址方式、寄存器使用规则等内容，是软件和硬件之间的重要接口。</td>
</tr>
<tr>
<td><strong>操作码</strong></td>
<td>操作码是指令中用于指出操作性质的字段。例如加法、减法、传送、转移、访存等操作都需要由不同的操作码表示。</td>
</tr>
<tr>
<td><strong>扩展操作码</strong></td>
<td>扩展操作码是指操作码长度不固定，把某些较短操作码保留作为前缀，再利用后续字段扩展出更多指令的编码方法。它可以在指令字长一定的情况下兼顾多地址指令和少地址指令。</td>
</tr>
<tr>
<td><strong>地址码</strong></td>
<td>地址码是指令中用于指出操作数地址、结果地址、寄存器编号或立即数等信息的字段。它回答"操作数从哪里来、结果到哪里去"的问题。</td>
</tr>
<tr>
<td><strong>寻址方式</strong></td>
<td>寻址方式是根据指令中的地址码求出操作数本身或操作数有效地址的方法。例如立即寻址、直接寻址、间接寻址、寄存器寻址、基址寻址、变址寻址、相对寻址等。</td>
</tr>
<tr>
<td><strong>程序计数器 PC</strong></td>
<td>PC 是保存下一条将要执行指令地址的寄存器。顺序执行时，PC 自动增加；发生转移时，PC 被修改为转移目标地址。</td>
</tr>
<tr>
<td><strong>有效地址</strong></td>
<td>有效地址是通过寻址方式计算得到的操作数实际所在的主存地址。形式地址不一定就是有效地址，必须经过相应寻址规则转换。</td>
</tr>
<tr>
<td><strong>存储器堆栈</strong></td>
<td>存储器堆栈是在主存中开辟的一片按"后进先出"原则访问的区域，通常由栈指针 SP 指示栈顶位置，用于保存返回地址、现场信息、临时数据等。</td>
</tr>
<tr>
<td><strong>寄存器堆栈</strong></td>
<td>寄存器堆栈是由一组寄存器构成的堆栈，也遵循后进先出原则。它访问速度快，但容量较小。</td>
</tr>
<tr>
<td><strong>基址寄存器</strong></td>
<td>基址寄存器用于保存某一存储区的起始地址或基地址。基址寻址中，有效地址通常为：有效地址 = 基址寄存器内容 + 形式地址。</td>
</tr>
<tr>
<td><strong>变址寄存器</strong></td>
<td>变址寄存器用于保存数组下标、循环偏移量等变址值。变址寻址中，有效地址通常为：有效地址 = 形式地址 + 变址寄存器内容。</td>
</tr>
<tr>
<td><strong>转子指令</strong></td>
<td>转子指令通常指转移到子程序的指令，即子程序调用指令。它不仅改变 PC，使程序转向子程序入口，还要保存返回地址，以便子程序执行完后返回原程序。</td>
</tr>
<tr>
<td><strong>CISC</strong></td>
<td>CISC 是复杂指令系统计算机。其特点是指令数量多、格式复杂、寻址方式多、单条指令功能较强，但译码和控制较复杂。</td>
</tr>
<tr>
<td><strong>RISC</strong></td>
<td>RISC 是精简指令系统计算机。其特点是指令数量少、格式规整、寻址方式少、多数指令可在一个时钟周期内完成，常采用流水线和大量通用寄存器。</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题5.2 选择题（考研真题）</h3>
<hr />
<h4>5.2（1）</h4>
<p><strong>题干：</strong>
[2022] 下列选项中，属于指令系统体系结构 ISA 规定的内容是__________。
Ⅰ. 指令字格式和指令类型
Ⅱ. CPU 的时钟周期
Ⅲ. 通用寄存器个数和位数
Ⅳ. 加法器的进位方式</p>
<p>A. 仅Ⅰ、Ⅱ
B. 仅Ⅰ、Ⅲ
C. 仅Ⅱ、Ⅳ
D. 仅Ⅰ、Ⅲ、Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<p>ISA 规定的是程序员可见的机器级结构，例如指令格式、指令类型、寄存器组织、寻址方式等；CPU 时钟周期和加法器进位方式属于具体硬件实现，不属于 ISA 规定内容。</p>
<ul>
<li><strong>A [错误]</strong>：Ⅰ正确，但Ⅱ"CPU 的时钟周期"属于实现层面的硬件组织，不属于 ISA。</li>
<li><strong>B [正确]</strong>：Ⅰ"指令字格式和指令类型"、Ⅲ"通用寄存器个数和位数"都属于 ISA 规定内容。</li>
<li><strong>C [错误]</strong>：Ⅱ和Ⅳ都属于硬件实现细节，不属于 ISA。</li>
<li><strong>D [错误]</strong>：Ⅰ和Ⅲ正确，但Ⅳ"加法器的进位方式"是运算器内部实现方式，不属于 ISA。</li>
</ul>
<hr />
<h4>5.2（2）</h4>
<p><strong>题干：</strong>
[2023] 某运算类型指令中有一个地址码为通用寄存器编号，对应通用寄存器中存放的是操作数或操作数的地址，CPU 区分两者的依据是__________。</p>
<p>A. 操作数的寻址方式
B. 操作数的编码方式
C. 通用寄存器的编号
D. 通用寄存器的内容</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：寄存器中存放的是操作数本身还是操作数地址，由寻址方式决定。例如寄存器直接寻址表示寄存器中是操作数，寄存器间接寻址表示寄存器中是操作数地址。</li>
<li><strong>B [错误]</strong>：操作数编码方式只说明数据如何表示，如补码、原码、浮点数等，不能说明寄存器内容是数据还是地址。</li>
<li><strong>C [错误]</strong>：寄存器编号只说明使用哪个寄存器，不决定寄存器内容的解释方式。</li>
<li><strong>D [错误]</strong>：寄存器内容本身只是一串二进制数，CPU 不能仅凭内容判断它是操作数还是地址。</li>
</ul>
<hr />
<h4>5.2（3）</h4>
<p><strong>题干：</strong>
[2017] 某计算机按字节编址，指令字长固定且只有两种指令格式，其中三地址指令 29 条，二地址指令 107 条，每个地址字段为 6 位，则指令字长至少应该是__________。</p>
<p>A. 24 位
B. 26 位
C. 28 位
D. 32 位</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>三地址指令需要 3 个地址字段：</p>
<p>$$ 3 \times 6 = 18 \text{ 位} $$</p>
<p>三地址指令有 29 条，因此操作码至少需要满足：</p>
<p>$$ 2^4 = 16 &lt; 29,\quad 2^5 = 32 \ge 29 $$</p>
<p>所以三地址格式至少需要：</p>
<p>$$ 5 + 18 = 23 \text{ 位} $$</p>
<p>但题目说明计算机按字节编址，指令字长通常应为字节的整数倍，因此至少取 24 位。</p>
<ul>
<li><strong>A [正确]</strong>：24 位是大于等于 23 位的最小字节整数倍。</li>
<li><strong>B [错误]</strong>：26 位虽能表示，但不是"至少"。</li>
<li><strong>C [错误]</strong>：28 位虽能表示，但不是最小值。</li>
<li><strong>D [错误]</strong>：32 位也能表示，但不是最小值。</li>
</ul>
<hr />
<h4>5.2（4）</h4>
<p><strong>题干：</strong>
[2022] 设计某指令系统时，假设采用 16 位定长指令字格式，操作码使用扩展编码方式，地址码为 6 位，包含零地址、一地址和二地址 3 种格式的指令。若二地址指令有 12 条，一地址指令有 254 条，则零地址指令的条数最多为__________。</p>
<p>A. 0
B. 2
C. 64
D. 128</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<p>二地址指令需要两个地址码：</p>
<p>$$ 6 + 6 = 12 \text{ 位} $$</p>
<p>指令总长 16 位，所以二地址指令操作码长度为：</p>
<p>$$ 16 - 12 = 4 \text{ 位} $$</p>
<p>4 位操作码最多有 $2^4 = 16$ 种编码。已用 12 种作为二地址指令，还剩 4 种 4 位前缀可用于扩展。</p>
<p>一地址指令保留一个 6 位地址码，所以可用另一个 6 位字段扩展操作码：</p>
<p>$$ 4 \times 2^6 = 256 $$</p>
<p>种一地址扩展编码。已用 254 条一地址指令，还剩 2 个扩展前缀可继续扩展为零地址指令。零地址指令没有地址码，因此最后一个 6 位地址字段也可作为扩展操作码：</p>
<p>$$ 2 \times 2^6 = 128 $$</p>
<ul>
<li><strong>A [错误]</strong>：仍有扩展编码空间。</li>
<li><strong>B [错误]</strong>：2 是剩余的一地址扩展前缀数，不是零地址指令条数。</li>
<li><strong>C [错误]</strong>：只相当于 1 个前缀继续扩展。</li>
<li><strong>D [正确]</strong>：2 个前缀，每个可扩展出 $2^6 = 64$ 条零地址指令，共 128 条。</li>
</ul>
<hr />
<h4>5.2（5）</h4>
<p><strong>题干：</strong>
[2014] 某计算机有 16 个通用寄存器，采用 32 位定长指令字，操作码字段含寻址方式位为 8 位，Store 指令的源操作数和目的操作数分别采用寄存器直接寻址和基址寻址方式。若基址寄存器可使用任意一个通用寄存器，且偏移量用补码表示，则 Store 指令中偏移量的取值范围是__________。</p>
<p>A. $(-32768 \sim +32767)$
B. $(-32767 \sim +32768)$
C. $(-65536 \sim +65535)$
D. $(-65535 \sim +65536)$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>16 个通用寄存器需要 $\log_2 16 = 4$ 位。Store 指令中：</p>
<ul>
<li>操作码字段：8 位；</li>
<li>源寄存器编号：4 位；</li>
<li>基址寄存器编号：4 位。</li>
</ul>
<p>所以偏移量位数为：</p>
<p>$$ 32 - 8 - 4 - 4 = 16 \text{ 位} $$</p>
<p>16 位补码范围为 $-2^{15} \sim 2^{15} - 1$，即 $-32768 \sim +32767$。</p>
<ul>
<li><strong>A [正确]</strong>：16 位补码的标准范围。</li>
<li><strong>B [错误]</strong>：补码负数最小值应为 $-32768$，正数最大值应为 $+32767$。</li>
<li><strong>C [错误]</strong>：这是 17 位补码近似范围，不符合 16 位偏移量。</li>
<li><strong>D [错误]</strong>：不是补码表示范围。</li>
</ul>
<hr />
<h4>5.2（6）</h4>
<p><strong>题干：</strong>
[2020] 某计算机采用 16 位定长指令字格式，操作码位数和寻址方式位数固定，指令系统有 48 条指令，支持直接、间接、立即、相对 4 种寻址方式。单地址指令中，直接寻址方式的可寻址范围是__________。</p>
<p>A. $(0 \sim 255)$
B. $(0 \sim 1023)$
C. $(-128 \sim 127)$
D. $(-512 \sim 511)$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>48 条指令需要操作码位数：</p>
<p>$$ 2^5 = 32 &lt; 48,\quad 2^6 = 64 \ge 48 $$</p>
<p>所以操作码为 6 位。</p>
<p>4 种寻址方式需要 $\log_2 4 = 2$ 位。</p>
<p>单地址指令中地址字段长度为：</p>
<p>$$ 16 - 6 - 2 = 8 \text{ 位} $$</p>
<p>直接寻址地址字段通常按无符号地址解释，因此范围为 $0 \sim 2^8 - 1 = 0 \sim 255$。</p>
<ul>
<li><strong>A [正确]</strong>：8 位直接地址的无符号范围。</li>
<li><strong>B [错误]</strong>：对应 10 位地址字段。</li>
<li><strong>C [错误]</strong>：这是 8 位补码范围，适合相对位移，不适合直接寻址。</li>
<li><strong>D [错误]</strong>：这是 10 位补码范围。</li>
</ul>
<hr />
<h4>5.2（7）</h4>
<p><strong>题干：</strong>
[2016] 某指令格式中，M 为寻址方式，I 为变址寄存器编号，D 为形式地址。若采用先变址后间址的寻址方式，则操作数的有效地址是__________。</p>
<p>A. $(I + D)$
B. $((I) + D)$
C. $(((I) + D))$
D. $(((I)) + D)$</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<p>先变址：</p>
<p>$$ EA_1 = (I) + D $$</p>
<p>这一步得到一个地址。后间址：再到该地址所指的主存单元中取内容作为有效地址：</p>
<p>$$ EA = ((I) + D) $$</p>
<ul>
<li><strong>A [错误]</strong>：把 I 当作数值本身，而不是变址寄存器内容。</li>
<li><strong>B [错误]</strong>：只完成了变址，没有完成间址。</li>
<li><strong>C [正确]</strong>：先计算 $(I) + D$，再取该地址中的内容。</li>
<li><strong>D [错误]</strong>：先对 I 间址，再加 D，顺序不符合"先变址后间址"。</li>
</ul>
<hr />
<h4>5.2（8）</h4>
<p><strong>题干：</strong>
[2009] 某计算机字长为 16 位，主存按字节编址，转移指令采用相对寻址，由两个字节组成，第一字节为操作码字段，第二字节为相对位移量字段。假定取指令时，每取一个字节 PC 自动加 1。若某转移指令所在主存地址为 2000H，相对位移量字段的内容为 06H，则该转移指令成功转移后的目标地址是__________。</p>
<p>A. 2006H
B. 2007H
C. 2008H
D. 2009H</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<p>转移指令地址为 2000H，指令长度为 2 字节。</p>
<p>取完该指令后，PC 已自动加 2：</p>
<p>$$ PC = 2000H + 2 = 2002H $$</p>
<p>相对位移量为 06H，目标地址为：</p>
<p>$$ 2002H + 06H = 2008H $$</p>
<ul>
<li><strong>A [错误]</strong>：没有考虑取完 2 字节指令后 PC 已变为 2002H。</li>
<li><strong>B [错误]</strong>：计算基准错误。</li>
<li><strong>C [正确]</strong>：$2002H + 06H = 2008H$。</li>
<li><strong>D [错误]</strong>：多加了 1。</li>
</ul>
<hr />
<h4>5.2（9）</h4>
<p><strong>题干：</strong>
[2011] 偏移寻址通过将某个寄存器内容与一个形式地址相加来生成有效地址。下列寻址方式中，不属于偏移寻址方式的是__________。</p>
<p>A. 间接寻址
B. 基址寻址
C. 相对寻址
D. 变址寻址</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>偏移寻址的共同形式是：</p>
<p>$$ EA = \text{寄存器内容} + \text{形式地址} $$</p>
<p>基址寻址、变址寻址、相对寻址都属于偏移寻址。</p>
<ul>
<li><strong>A [正确]</strong>：间接寻址是通过地址字段所指单元中的内容得到有效地址，不是寄存器内容加形式地址。</li>
<li><strong>B [错误]</strong>：基址寻址属于偏移寻址。</li>
<li><strong>C [错误]</strong>：相对寻址属于偏移寻址，常用 $EA = (PC) + D$。</li>
<li><strong>D [错误]</strong>：变址寻址属于偏移寻址，常用 $EA = (IX) + D$。</li>
</ul>
<hr />
<h4>5.2（10）</h4>
<p><strong>题干：</strong>
[2013] 假设变址寄存器 R 的内容为 1000H，指令中的形式地址为 2000H；地址 1000H 中的内容为 2000H，地址 2000H 中的内容为 3000H，地址 3000H 中的内容为 4000H，则变址寻址方式下访问到的操作数是__________。</p>
<p>A. 1000H
B. 2000H
C. 3000H
D. 4000H</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<p>变址寻址有效地址为：</p>
<p>$$ EA = (R) + D $$</p>
<p>已知 $(R) = 1000H$，$D = 2000H$，所以 $EA = 1000H + 2000H = 3000H$。</p>
<p>操作数是地址 3000H 中的内容：$(3000H) = 4000H$。</p>
<ul>
<li><strong>A [错误]</strong>：这是变址寄存器 R 的内容。</li>
<li><strong>B [错误]</strong>：这是形式地址，也是地址 1000H 中的内容。</li>
<li><strong>C [错误]</strong>：这是有效地址，不是操作数。</li>
<li><strong>D [正确]</strong>：地址 3000H 中的内容为 4000H。</li>
</ul>
<hr />
<h4>5.2（11）</h4>
<p><strong>题干：</strong>
[2017] 下列寻址方式中，最适合按下标顺序访问一维数组元素的是__________。</p>
<p>A. 相对寻址
B. 寄存器寻址
C. 直接寻址
D. 变址寻址</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<p>数组访问通常具有如下形式：数组元素地址 = 数组首地址 + 下标偏移量。变址寻址正适合这种访问方式。</p>
<ul>
<li><strong>A [错误]</strong>：相对寻址主要用于程序转移。</li>
<li><strong>B [错误]</strong>：寄存器寻址只能访问寄存器中的操作数，不适合遍历主存数组。</li>
<li><strong>C [错误]</strong>：直接寻址适合访问固定地址，不适合下标变化的数组访问。</li>
<li><strong>D [正确]</strong>：变址寻址适合数组、字符串、循环结构中的顺序访问。</li>
</ul>
<hr />
<h4>5.2（12）</h4>
<p><strong>题干：</strong>
[2019] 某计算机采用大端方式，按字节编址。某指令操作数的机器数为 1234 FF00H，该操作数采用基址寻址方式，形式地址用补码表示为 FF12H，基址寄存器的内容为 F000 0000H，则该操作数的 LSB 所在的地址是__________。</p>
<p>A. F000 FF12H
B. F000 FF15H
C. EFFF FF12H
D. EFFF FF15H</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<p>形式地址 FF12H 是 16 位补码，FF12H 对应的真值为 $-00EEH$。</p>
<p>基址寄存器内容为 F0000000H，有效地址为：</p>
<p>$$ EA = F0000000H - 00EEH = EFFFFF12H $$</p>
<p>操作数机器数为 1234FF00H，共 4 字节。大端方式下，最高有效字节放在低地址，最低有效字节 LSB 放在最高地址：</p>
<table>
<thead>
<tr>
<th>地址</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>EFFFFF12H</td>
<td>12H</td>
</tr>
<tr>
<td>EFFFFF13H</td>
<td>34H</td>
</tr>
<tr>
<td>EFFFFF14H</td>
<td>FFH</td>
</tr>
<tr>
<td>EFFFFF15H</td>
<td>00H</td>
</tr>
</tbody>
</table>
<p>LSB 为 00H，所在地址为 EFFFFF15H。</p>
<ul>
<li><strong>A [错误]</strong>：没有正确处理 FF12H 的补码负偏移，也没有考虑 LSB 在最高地址。</li>
<li><strong>B [错误]</strong>：没有正确处理补码负偏移。</li>
<li><strong>C [错误]</strong>：这是操作数最高有效字节 MSB 的地址。</li>
<li><strong>D [正确]</strong>：LSB 所在地址为 EFFFFF15H。</li>
</ul>
<hr />
<h4>5.2（13）</h4>
<p><strong>题干：</strong>
[2018] 按字节编址的计算机中，某 double 型数组 A 的首地址为 2000H，使用变址寻址和循环结构访问数组 A，保存数组下标的变址寄存器初值为 0，每次循环取一个数组元素，其偏移地址为变址值乘以 sizeof(double)，取完后变址寄存器内容自动加 1。若某次循环所取元素的地址为 2100H，则进入该次循环时变址寄存器的内容是__________。</p>
<p>A. 2
B. 32
C. 64
D. 100</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<p>double 类型通常占 sizeof(double) = 8 字节。</p>
<p>数组元素地址：$A[i] \text{地址} = \text{首地址} + i \times 8$</p>
<p>已知 $2100H - 2000H = 100H = 256$，所以 $i = 256 / 8 = 32$。</p>
<ul>
<li><strong>A [错误]</strong>：2 对应偏移 16 字节。</li>
<li><strong>B [正确]</strong>：32 对应偏移 $32 \times 8 = 256 = 100H$ 字节。</li>
<li><strong>C [错误]</strong>：64 对应偏移 512 字节。</li>
<li><strong>D [错误]</strong>：100 若按十进制，对应偏移 800 字节；若按十六进制，也不符合题意。</li>
</ul>
<hr />
<h4>5.2（14）</h4>
<p><strong>题干：</strong>
[2011] 某计算机有一个标志寄存器，其中有进位/借位标志 CF、零标志 ZF、符号标志 SF 和溢出标志 OF，条件转移指令 bgt 无符号整数比较大于时转移的转移条件是__________。</p>
<p>A. $CF + ZF = 1$
B. $\overline{SF} + ZF = 1$
C. $\overline{CF + ZF} = 1$
D. $\overline{CF + SF} = 1$</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<p>无符号数比较通常通过减法实现。若 $A - B$，对于无符号整数，$A &gt; B$ 的条件是：</p>
<ol>
<li>没有借位：$CF = 0$；</li>
<li>结果不为 0：$ZF = 0$。</li>
</ol>
<p>因此 $CF = 0$ 且 $ZF = 0$，等价于 $\overline{CF + ZF} = 1$。</p>
<ul>
<li><strong>A [错误]</strong>：$CF + ZF = 1$ 表示有借位或结果为 0，对应小于或等于。</li>
<li><strong>B [错误]</strong>：SF 是有符号数符号标志，不适合无符号大于判断。</li>
<li><strong>C [正确]</strong>：无符号大于要求 $CF = 0$ 且 $ZF = 0$。</li>
<li><strong>D [错误]</strong>：SF 不用于无符号大小比较。</li>
</ul>
<hr />
<h4>5.2（15）</h4>
<p><strong>题干：</strong>
[2018] 减法指令 <code>sub R1, R2, R3</code> 的功能为 $(R1) - (R2) \rightarrow R3$，该指令执行后将生成进位/借位标志 CF 和溢出标志 OF。若 $(R1) = FFFFFFFFH$，$(R2) = FFFFFF00H$，则该减法指令执行后，CF 与 OF 分别为__________。</p>
<p>A. $CF = 0$，$OF = 0$
B. $CF = 1$，$OF = 0$
C. $CF = 0$，$OF = 1$
D. $CF = 1$，$OF = 1$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<p>计算：$FFFFFFFFH - FFFFFF00H = 000000FFH$</p>
<p>从无符号数角度看：$FFFFFFFFH &gt; FFFFFF00H$，所以减法时没有借位，$CF = 0$。</p>
<p>从有符号数角度看：$FFFFFFFFH = -1$，$FFFFFF00H = -256$，所以 $-1 - (-256) = 255$。255 在 32 位有符号整数范围内，没有溢出，$OF = 0$。</p>
<ul>
<li><strong>A [正确]</strong>：无借位、无溢出。</li>
<li><strong>B [错误]</strong>：CF 不应为 1，因为无符号减法没有借位。</li>
<li><strong>C [错误]</strong>：OF 不应为 1，因为结果 255 可表示。</li>
<li><strong>D [错误]</strong>：CF 和 OF 都不应为 1。</li>
</ul>
<hr />
<h4>5.2（16）</h4>
<p><strong>题干：</strong>
[2009] 下列关于 RISC 的叙述中，错误的是__________。</p>
<p>A. RISC 普遍采用微程序控制器
B. RISC 中的大多数指令在一个时钟周期内完成
C. RISC 的内部通用寄存器数量比 CISC 的多
D. RISC 的指令数、寻址方式和指令格式种类比 CISC 的少</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误，故选]</strong>：RISC 指令简单、格式规整，通常采用硬布线控制器，而不是普遍采用微程序控制器。</li>
<li><strong>B [正确]</strong>：RISC 的多数简单指令可在一个时钟周期内完成。</li>
<li><strong>C [正确]</strong>：RISC 通常设置较多通用寄存器，以减少访存次数。</li>
<li><strong>D [正确]</strong>：RISC 的指令数、寻址方式和指令格式种类通常少于 CISC。</li>
</ul>
<hr />
<h3>习题5.3 简答题</h3>
<hr />
<h4>5.3（1）</h4>
<p><strong>题干：</strong>
什么叫指令？什么叫指令系统？</p>
<p><strong>解析</strong>：</p>
<p>指令是计算机能够识别和执行的一条机器命令。</p>
<p>一条指令通常包括：</p>
<p>$$ \text{操作码} + \text{地址码} $$</p>
<p>其中：</p>
<ul>
<li>操作码说明执行什么操作；</li>
<li>地址码说明操作数在哪里或结果存放在哪里。</li>
</ul>
<p>指令系统是一台计算机所能执行的全部指令的集合。它规定了指令格式、操作类型、操作数类型、寻址方式、寄存器组织、指令执行对标志位的影响等。因此，指令系统是软件和硬件之间的接口。</p>
<hr />
<h4>5.3（2）</h4>
<p><strong>题干：</strong>
计算机中为什么要设置多种操作数寻址方式？</p>
<p><strong>解析</strong>：</p>
<p>设置多种寻址方式的原因主要有以下几点：</p>
<p><strong>第一，提高程序设计的灵活性。</strong> 不同程序结构需要不同的数据访问方式。例如数组访问适合变址寻址，程序转移适合相对寻址。</p>
<p><strong>第二，缩短指令长度。</strong> 如果每条指令都直接给出完整主存地址，指令会很长。使用寄存器寻址、基址寻址、变址寻址等方式，可以减少地址字段长度。</p>
<p><strong>第三，扩大寻址范围。</strong> 通过基址寄存器、变址寄存器、PC 等寄存器与形式地址相加，可以访问比形式地址字段本身更大的地址空间。</p>
<p><strong>第四，便于程序浮动和重定位。</strong> 基址寻址和相对寻址可以让程序装入到不同内存位置后仍能正确运行。</p>
<p><strong>第五，提高访问效率。</strong> 寄存器寻址比主存寻址快，合理使用寄存器可以减少访存次数，提高执行速度。</p>
<hr />
<h4>5.3（3）</h4>
<p><strong>题干：</strong>
操作数寻址方式在指令中如何表示？</p>
<p><strong>解析</strong>：</p>
<p>操作数寻址方式通常通过指令中的寻址方式字段表示。常见做法有两种：</p>
<p><strong>第一，设置专门的寻址方式字段。</strong> 例如指令中包含字段 M：</p>
<p>$$ OP \quad M \quad A $$</p>
<p>其中 OP 表示操作码，M 表示寻址方式，A 表示形式地址或寄存器编号。</p>
<p><strong>第二，由操作码隐含规定寻址方式。</strong> 有些指令系统不设置独立寻址方式字段，而是由操作码本身确定操作数在哪里。例如某些机器中，访存指令、寄存器运算指令、立即数指令分别使用不同操作码。</p>
<hr />
<h4>5.3（4）</h4>
<p><strong>题干：</strong>
基址寻址和变址寻址的作用是什么？分析它们的异同点。</p>
<p><strong>解析</strong>：</p>
<p>基址寻址的有效地址通常为 $EA = (BR) + D$，其中 $(BR)$ 是基址寄存器内容，$D$ 是形式地址或位移量。基址寻址的主要作用是<strong>程序重定位和扩大寻址范围</strong>。程序装入内存后，只需要改变基址寄存器的内容，就可以让程序在新的内存区域中正确运行。</p>
<p>变址寻址的有效地址通常为 $EA = (IX) + D$，其中 $(IX)$ 是变址寄存器内容，$D$ 通常是数组首地址或形式地址。变址寻址的主要作用是<strong>访问数组、字符串、表格等连续存储结构</strong>。</p>
<p>二者相同点：</p>
<ul>
<li>都采用"寄存器内容 + 形式地址"的方式形成有效地址；</li>
<li>都能扩大寻址范围；</li>
<li>都属于偏移寻址。</li>
</ul>
<p>二者不同点：</p>
<table>
<thead>
<tr>
<th>比较项目</th>
<th>基址寻址</th>
<th>变址寻址</th>
</tr>
</thead>
<tbody>
<tr>
<td>寄存器内容</td>
<td>通常是基地址</td>
<td>通常是下标或偏移量</td>
</tr>
<tr>
<td>形式地址</td>
<td>通常是位移量</td>
<td>通常是数组首地址</td>
</tr>
<tr>
<td>主要用途</td>
<td>程序重定位、存储区定位</td>
<td>数组、循环、表格访问</td>
</tr>
<tr>
<td>使用特点</td>
<td>基址寄存器一般在程序运行期间相对稳定</td>
<td>变址寄存器常随循环不断变化</td>
</tr>
</tbody>
</table>
<hr />
<h4>5.3（5）</h4>
<p><strong>题干：</strong>
RISC 处理器有什么特点？</p>
<p><strong>解析</strong>：</p>
<p>RISC 处理器的主要特点如下：</p>
<ol>
<li>指令数量少，指令功能简单。</li>
<li>指令格式规整，通常采用定长指令。</li>
<li>寻址方式少。</li>
<li>大多数指令在一个时钟周期内完成。</li>
<li>访存操作通常只由 load/store 指令完成，算术逻辑运算主要在寄存器之间进行。</li>
<li>通用寄存器数量较多。</li>
<li>控制器多采用硬布线控制。</li>
<li>适合流水线执行，有利于提高指令吞吐率。</li>
<li>编译器承担较多优化任务。</li>
</ol>
<hr />
<h4>5.3（6）</h4>
<p><strong>题干：</strong>
比较定长指令与变长指令的优缺点。</p>
<p><strong>解析</strong>：</p>
<p><strong>定长指令</strong>是指所有指令长度相同。</p>
<p>优点：</p>
<ul>
<li>指令格式规整；</li>
<li>取指和译码简单；</li>
<li>便于流水线设计；</li>
<li>控制逻辑简单。</li>
</ul>
<p>缺点：</p>
<ul>
<li>对简单指令可能浪费位数；</li>
<li>指令表示不够灵活；</li>
<li>在指令种类很多或寻址方式复杂时，编码效率较低。</li>
</ul>
<p><strong>变长指令</strong>是指不同指令长度可以不同。</p>
<p>优点：</p>
<ul>
<li>编码灵活；</li>
<li>简单指令可以较短，复杂指令可以较长；</li>
<li>指令编码空间利用率较高；</li>
<li>有利于支持复杂寻址方式。</li>
</ul>
<p>缺点：</p>
<ul>
<li>取指和译码复杂；</li>
<li>指令边界不容易判断；</li>
<li>不利于流水线设计；</li>
<li>控制逻辑复杂。</li>
</ul>
<hr />
<h4>5.3（7）</h4>
<p><strong>题干：</strong>
指令的地址码与指令中的操作码含义有何不同？</p>
<p><strong>解析</strong>：</p>
<p>操作码说明"做什么操作"，例如加法、减法、传送、转移、读存储器、写存储器等。</p>
<p>地址码说明"操作数在哪里"或"结果放在哪里"，例如操作数在寄存器中、操作数在主存中、操作数就是指令中的立即数、结果写回某个寄存器或存储单元等。</p>
<p>因此：</p>
<p>$$ \text{操作码决定操作性质} $$</p>
<p>$$ \text{地址码决定操作对象的位置或地址} $$</p>
<hr />
<h3>习题5.4 根据操作数所在的位置填写寻址方式</h3>
<p><strong>题干：</strong>
根据操作数所在的位置，在空格处填写其寻址方式。
（1）操作数在指令中为__________寻址方式。
（2）操作数地址（主存）在指令中为__________寻址方式。
（3）操作数在寄存器中为__________寻址方式。
（4）操作数地址在寄存器中为__________寻址方式。</p>
<p><strong>解析</strong>：</p>
<table>
<thead>
<tr>
<th>小题</th>
<th>答案</th>
<th>解析</th>
</tr>
</thead>
<tbody>
<tr>
<td>（1）</td>
<td>立即寻址方式</td>
<td>操作数直接放在指令中，因此是立即寻址。</td>
</tr>
<tr>
<td>（2）</td>
<td>直接寻址方式</td>
<td>指令中直接给出主存地址，因此是直接寻址。</td>
</tr>
<tr>
<td>（3）</td>
<td>寄存器寻址方式</td>
<td>操作数在寄存器中，因此是寄存器寻址。</td>
</tr>
<tr>
<td>（4）</td>
<td>寄存器间接寻址方式</td>
<td>寄存器中存放的是操作数地址，因此是寄存器间接寻址。</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题5.5</h3>
<p><strong>题干：</strong>
某计算机字长为 16 位，运算器为 16 位，有 16 个通用寄存器，8 种寻址方式，主存为 128KW，指令中操作数地址码由寻址方式字段和寄存器号字段组成。请回答下列问题。
（1）单操作数指令最多有多少条？
（2）双操作数指令最多有多少条？
（3）直接寻址的范围多大？
（4）变址寻址的范围多大？</p>
<p><strong>解析</strong>：</p>
<p>已知有 8 种寻址方式，因此寻址方式字段需要 $\log_2 8 = 3$ 位。</p>
<p>有 16 个通用寄存器，因此寄存器号字段需要 $\log_2 16 = 4$ 位。</p>
<p>所以每个操作数地址码长度为 $3 + 4 = 7$ 位。</p>
<hr />
<h4>（1）单操作数指令最多有多少条？</h4>
<p>单操作数指令需要 1 个操作数地址码，即 7 位。</p>
<p>指令字长为 16 位，所以操作码位数为 $16 - 7 = 9$ 位。</p>
<p>因此单操作数指令最多有 $2^9 = 512$ 条。</p>
<p><strong>答案：512 条。</strong></p>
<hr />
<h4>（2）双操作数指令最多有多少条？</h4>
<p>双操作数指令需要 2 个操作数地址码：$2 \times 7 = 14$ 位。</p>
<p>指令字长为 16 位，所以操作码位数为 $16 - 14 = 2$ 位。</p>
<p>因此双操作数指令最多有 $2^2 = 4$ 条。</p>
<p><strong>答案：4 条。</strong></p>
<hr />
<h4>（3）直接寻址的范围多大？</h4>
<p>按题干"操作数地址码由寻址方式字段和寄存器号字段组成"理解，直接地址可由寄存器号字段位置给出，其位数为 4 位。</p>
<p>因此直接寻址范围为 $0 \sim 2^4 - 1$，即 $0 \sim 15$。</p>
<p><strong>答案：0～15。</strong></p>
<hr />
<h4>（4）变址寻址的范围多大？</h4>
<p>运算器为 16 位，通用寄存器也按 16 位考虑。</p>
<p>变址寄存器可提供 16 位变址量，因此变址寻址范围为 $2^{16} = 64K$。</p>
<p><strong>答案：64K。</strong></p>
<hr />
<h3>习题5.6</h3>
<p><strong>题干：</strong>
假设某计算机的指令长度固定为 16 位，具有双操作数、单操作数和无操作数 3 类指令，每个操作数地址规定用 6 位表示。
（1）若操作码字段不固定，现已设计出 $m$ 条双操作数指令、$n$ 条无操作数指令，在此情况下，这台计算机最多可以设计出多少条单操作数指令？
（2）若操作码字段不固定，当双操作数指令争取最大数时，且在此基础上，单操作数指令条数也取最大值，试计算这 3 类指令最多可拥有多少条指令？</p>
<hr />
<h4>（1）求最多单操作数指令条数</h4>
<p>双操作数指令需要两个地址字段：$6 + 6 = 12$ 位。</p>
<p>所以双操作数指令的基本操作码字段为 $16 - 12 = 4$ 位。</p>
<p>4 位操作码共有 $2^4 = 16$ 种编码。</p>
<p>已经设计出 $m$ 条双操作数指令，所以还剩 $16 - m$ 个 4 位前缀可用于扩展。</p>
<p>单操作数指令需要保留一个 6 位地址字段，因此还可以用另一个 6 位字段扩展操作码。</p>
<p>所以可形成的单操作数扩展编码数为 $(16 - m) \times 2^6$。</p>
<p>但还需要设计 $n$ 条无操作数指令。无操作数指令没有地址字段，因此可在单操作数扩展编码的基础上继续使用最后 6 位扩展。每保留 1 个单操作数扩展编码，可扩展出 $2^6 = 64$ 条无操作数指令。</p>
<p>为了容纳 $n$ 条无操作数指令，需要保留 $\left\lceil \frac{n}{64} \right\rceil$ 个单操作数扩展编码。</p>
<p>所以最多单操作数指令条数为：</p>
<p>$$ \boxed{(16 - m) \times 64 - \left\lceil \frac{n}{64} \right\rceil} $$</p>
<hr />
<h4>（2）三类指令最多各有多少条？</h4>
<p>要让双操作数指令尽可能多，但还必须保留编码空间给单操作数和无操作数指令。</p>
<p>双操作数指令最多不能占满全部 16 个 4 位操作码，否则无法再扩展出单操作数和无操作数指令。</p>
<p>所以双操作数指令最多为 $16 - 1 = 15$ 条。</p>
<p>剩下 1 个 4 位前缀可用于单操作数扩展。单操作数扩展编码数为 $1 \times 2^6 = 64$。但还要保留 1 个单操作数扩展编码继续扩展为无操作数指令。</p>
<p>所以单操作数指令最多为 $64 - 1 = 63$ 条。</p>
<p>剩下 1 个单操作数扩展编码可继续扩展为无操作数指令：$2^6 = 64$ 条。</p>
<p>因此：</p>
<table>
<thead>
<tr>
<th>指令类型</th>
<th>最多条数</th>
</tr>
</thead>
<tbody>
<tr>
<td>双操作数指令</td>
<td>15</td>
</tr>
<tr>
<td>单操作数指令</td>
<td>63</td>
</tr>
<tr>
<td>无操作数指令</td>
<td>64</td>
</tr>
</tbody>
</table>
<p>总数为 $15 + 63 + 64 = 142$。</p>
<p><strong>答案：三类指令最多共 142 条，其中双操作数 15 条，单操作数 63 条，无操作数 64 条。</strong></p>
<hr />
<h3>习题5.7</h3>
<p><strong>题干：</strong>
设相对寻址的转移指令占 3 个字节，第一个字节是操作码，第二个字节是相对位移量补码表示的低 8 位，第三个字节是相对位移量补码表示的高 8 位，每当 CPU 从存储器取出一个字节时，便自动完成 $(PC) + 1 \rightarrow PC$。请回答下列问题。
（1）若 PC 当前值为 256（十进制），要求转移到 290（十进制），则转移指令第二、三字节的机器代码是什么？
（2）若 PC 当前值为 128（十进制），要求转移到 110（十进制），则转移指令第二、三字节的机器代码又是什么？</p>
<hr />
<h4>（1）PC = 256，目标地址 = 290</h4>
<p>转移指令长度为 3 字节。取完该指令后，PC 自动增加 3：</p>
<p>$$ PC = 256 + 3 = 259 $$</p>
<p>相对位移量应满足：</p>
<p>$$ 290 = 259 + \text{位移量} $$</p>
<p>$$ \text{位移量} = 290 - 259 = 31 $$</p>
<p>31 的 16 位十六进制补码为 001FH。题目规定第二字节放低 8 位，第三字节放高 8 位。</p>
<p><strong>答案：第二字节为 1FH，第三字节为 00H。</strong></p>
<hr />
<h4>（2）PC = 128，目标地址 = 110</h4>
<p>取完 3 字节指令后：</p>
<p>$$ PC = 128 + 3 = 131 $$</p>
<p>位移量为 $110 - 131 = -21$。</p>
<p>将 $-21$ 表示为 16 位补码。先求 $+21$ 的 16 位十六进制：$21_{10} = 0015H$。</p>
<p>16 位补码：$10000H - 0015H = FFEBH$。</p>
<p>所以 $-21$ 的 16 位补码为 FFEBH。</p>
<p>第二字节放低 8 位（EBH），第三字节放高 8 位（FFH）。</p>
<p><strong>答案：第二字节为 EBH，第三字节为 FFH。</strong></p>
<hr />
<h3>习题5.8</h3>
<p><strong>题干：</strong>
计算机的指令格式包括操作码 OP、寻址方式特征位 I 和形式地址 D 等 3 个字段，其中 OP 字段为 6 位，寻址方式特征位字段 I 为 2 位，形式地址字段 D 为 8 位。I 的取值与寻址方式的对应关系如下：</p>
<ul>
<li>I = 00：直接寻址。</li>
<li>I = 01：用变址寄存器 X1 进行变址。</li>
<li>I = 10：用变址寄存器 X2 进行变址。</li>
<li>I = 11：相对寻址。</li>
</ul>
<p>设 $(PC) = 1234H$，$(X1) = 0037H$，$(X2) = 1122H$，以下 4 条指令均采用上述指令格式，请确定这些指令的有效地址。
（1）4420H；（2）2244H；（3）1322H；（4）3521H。</p>
<p><strong>解析</strong>：</p>
<p>指令格式为：</p>
<p>$$ OP(6\text{位}) \quad I(2\text{位}) \quad D(8\text{位}) $$</p>
<p>因此高 6 位为 OP，中间 2 位为 I，低 8 位为 D。</p>
<hr />
<h4>（1）4420H</h4>
<p>$$ 4420H = 010001\ 00\ 00100000 $$</p>
<p>所以 $I = 00$，$D = 20H$。I = 00 表示直接寻址，$EA = D = 20H$。</p>
<p><strong>答案：20H。</strong></p>
<h4>（2）2244H</h4>
<p>$$ 2244H = 001000\ 10\ 01000100 $$</p>
<p>所以 $I = 10$，$D = 44H$。I = 10 表示用 X2 变址，$EA = (X2) + D = 1122H + 44H = 1166H$。</p>
<p><strong>答案：1166H。</strong></p>
<h4>（3）1322H</h4>
<p>$$ 1322H = 000100\ 11\ 00100010 $$</p>
<p>所以 $I = 11$，$D = 22H$。I = 11 表示相对寻址，$EA = (PC) + D = 1234H + 22H = 1256H$。</p>
<p><strong>答案：1256H。</strong></p>
<h4>（4）3521H</h4>
<p>$$ 3521H = 001101\ 01\ 00100001 $$</p>
<p>所以 $I = 01$，$D = 21H$。I = 01 表示用 X1 变址，$EA = (X1) + D = 0037H + 21H = 0058H$。</p>
<p><strong>答案：0058H。</strong></p>
<hr />
<h3>习题5.9</h3>
<p><strong>题干：</strong>
某计算机 A 有 60 条指令，指令的操作码字段固定为 6 位，从 000000～111011，该计算机的后续机型 B 中需要增加 32 条指令，并与 A 保持兼容。
（1）试采用扩展操作码为计算机 B 设计指令操作码。
（2）求出计算机 B 中操作码的平均长度。</p>
<hr />
<h4>（1）设计扩展操作码</h4>
<p>6 位操作码共有 $2^6 = 64$ 种编码。</p>
<p>计算机 A 已使用 60 条（000000～111011），剩余 4 个 6 位编码为：111100、111101、111110、111111。</p>
<p>计算机 B 要新增 32 条指令。可将这 4 个剩余编码作为扩展操作码前缀，每个前缀再扩展 3 位：$4 \times 2^3 = 32$，刚好满足新增 32 条指令。</p>
<p>可设计为：</p>
<table>
<thead>
<tr>
<th>扩展前缀</th>
<th>扩展后操作码范围</th>
</tr>
</thead>
<tbody>
<tr>
<td>111100</td>
<td>111100000 ～ 111100111</td>
</tr>
<tr>
<td>111101</td>
<td>111101000 ～ 111101111</td>
</tr>
<tr>
<td>111110</td>
<td>111110000 ～ 111110111</td>
</tr>
<tr>
<td>111111</td>
<td>111111000 ～ 111111111</td>
</tr>
</tbody>
</table>
<p>原有 60 条指令仍使用 6 位操作码，新增 32 条指令使用 9 位操作码。</p>
<hr />
<h4>（2）求平均操作码长度</h4>
<p>原有 60 条指令，每条操作码长度为 6 位。新增 32 条指令，每条操作码长度为 $6 + 3 = 9$ 位。</p>
<p>平均长度为：</p>
<p>$$ \frac{60 \times 6 + 32 \times 9}{60 + 32} = \frac{360 + 288}{92} = \frac{648}{92} \approx 7.04 \text{ 位} $$</p>
<p><strong>答案：计算机 B 中操作码平均长度约为 7.04 位。</strong></p>
<hr />
<h3>习题5.10</h3>
<p><strong>题干：</strong>
以下 MIPS 指令代表什么操作？写出它的 MIPS 汇编指令格式。</p>
<p>$$ 0000\ 0000\ 1010\ 1111\ 1000\ 0000\ 0010\ 0000 $$</p>
<p><strong>解析</strong>：</p>
<p>MIPS R 型指令格式为：</p>
<p>$$ op(6) \quad rs(5) \quad rt(5) \quad rd(5) \quad shamt(5) \quad funct(6) $$</p>
<p>将题中机器码分段：</p>
<p>$$ 000000\ 00101\ 01111\ 10000\ 00000\ 100000 $$</p>
<p>对应字段为：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>二进制</th>
<th>十进制/十六进制</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>op</td>
<td>000000</td>
<td>0</td>
<td>R 型指令</td>
</tr>
<tr>
<td>rs</td>
<td>00101</td>
<td>5</td>
<td>源寄存器 $$5$</td>
</tr>
<tr>
<td>rt</td>
<td>01111</td>
<td>15</td>
<td>源寄存器 $$15$</td>
</tr>
<tr>
<td>rd</td>
<td>10000</td>
<td>16</td>
<td>目的寄存器 $$16$</td>
</tr>
<tr>
<td>shamt</td>
<td>00000</td>
<td>0</td>
<td>非移位指令</td>
</tr>
<tr>
<td>funct</td>
<td>100000</td>
<td>20H</td>
<td>add</td>
</tr>
</tbody>
</table>
<p>funct = 100000 表示 add 指令。</p>
<p>MIPS 汇编格式为：</p>
<pre><code>add $16, $5, $15
</code></pre>
<p>其功能为 $R[16] \leftarrow R[5] + R[15]$。</p>
<p><strong>答案：该指令表示把寄存器 <code>$5</code> 和 <code>$15</code> 的内容相加，结果送入寄存器 <code>$16</code>。</strong></p>
<hr />
<h3>习题5.11</h3>
<p><strong>题干：</strong>
假定以下 C 语言语句中包含的变量 f、g、h、i、j 分别存放在寄存器 <code>$11</code> $\sim$ <code>$15</code> 中，写出实现 C 语言语句</p>
<p>$$ f = (g + h) \times i / j $$</p>
<p>功能的 MIPS 汇编指令序列，并写出每条 MIPS 指令的十六进制数。</p>
<p><strong>解析</strong>：</p>
<p>变量与寄存器对应关系为：</p>
<table>
<thead>
<tr>
<th>变量</th>
<th>寄存器</th>
</tr>
</thead>
<tbody>
<tr>
<td>f</td>
<td>$11</td>
</tr>
<tr>
<td>g</td>
<td>$12</td>
</tr>
<tr>
<td>h</td>
<td>$13</td>
</tr>
<tr>
<td>i</td>
<td>$14</td>
</tr>
<tr>
<td>j</td>
<td>$15</td>
</tr>
</tbody>
</table>
<p>使用临时寄存器 $$8$ 保存中间结果。</p>
<p>计算顺序：$g + h \rightarrow (g + h) \times i \rightarrow \frac{(g + h) \times i}{j}$</p>
<p>MIPS 指令序列如下：</p>
<pre><code>add  $8,  $12, $13     # $8 = g + h
mult $8,  $14          # HI/LO = $8 * i
mflo $8                # $8 = (g + h) * i
div  $8,  $15          # LO = $8 / j
mflo $11               # f = LO
</code></pre>
<p>对应机器码：</p>
<table>
<thead>
<tr>
<th>汇编指令</th>
<th>功能</th>
<th>十六进制机器码</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>add $8, $12, $13</code></td>
<td>$$8 \leftarrow $12 + $13$</td>
<td>018D4020H</td>
</tr>
<tr>
<td><code>mult $8, $14</code></td>
<td>HI/LO $\leftarrow $8 \times $14$</td>
<td>010E0018H</td>
</tr>
<tr>
<td><code>mflo $8</code></td>
<td>$$8 \leftarrow$ LO</td>
<td>00004012H</td>
</tr>
<tr>
<td><code>div $8, $15</code></td>
<td>LO $\leftarrow $8 / $15$</td>
<td>010F001AH</td>
</tr>
<tr>
<td><code>mflo $11</code></td>
<td>$$11 \leftarrow$ LO</td>
<td>00005812H</td>
</tr>
</tbody>
</table>
<p><strong>答案：</strong></p>
<pre><code>add  $8,  $12, $13     # 018D4020H
mult $8,  $14          # 010E0018H
mflo $8                # 00004012H
div  $8,  $15          # 010F001AH
mflo $11               # 00005812H
</code></pre>
<hr />
<h3>习题5.12</h3>
<p><strong>题干：</strong>
某计算机字长为 16 位，主存地址空间大小为 128KB，按字节编址。采用单字长指令格式，指令各字段定义如下：</p>
<p>$$ OP(15 \sim 12) \quad Ms,Rs(11 \sim 06) \quad Md,Rd(05 \sim 00) $$</p>
<p>转移指令采用相对寻址方式，相对偏移量用补码表示。寻址方式包括：寄存器直接寻址、寄存器间接寻址、寄存器间接加自增寻址、相对寻址。请回答下列问题。
（1）该指令系统最多可有多少条指令？该计算机最多有多少个通用寄存器？
（2）MAR 和 MDR 至少各需要多少位？
（3）转移指令的目标地址范围是多少？
（4）若操作码 0010B 表示加法操作 add，R4 和 R5 编号分别为 100B 和 101B，R4 = 1234H，R5 = 5678H，地址 1234H 中的内容为 5678H，地址 5678H 中的内容为 1234H，则汇编语言 <code>add (R4), (R5)+</code> 对应的机器码是什么？该指令执行后，哪些寄存器和存储单元会改变？改变后的内容是什么？</p>
<hr />
<h4>（1）最多指令条数和通用寄存器个数</h4>
<p>OP 字段为 4 位：$2^4 = 16$，所以该指令系统最多有 16 条指令。</p>
<p>寄存器字段 Rs 或 Rd 为 3 位：$2^3 = 8$，所以最多有 8 个通用寄存器。</p>
<p><strong>答案：最多 16 条指令；最多 8 个通用寄存器。</strong></p>
<hr />
<h4>（2）MAR 和 MDR 至少需要多少位？</h4>
<p>主存地址空间为 $128KB = 128 \times 1024B = 2^{17}B$，按字节编址，因此需要 17 位地址。MAR 至少需要 17 位。</p>
<p>计算机字长为 16 位，MDR 至少要能存放一个机器字，所以 MDR 至少需要 16 位。</p>
<p><strong>答案：MAR 至少 17 位，MDR 至少 16 位。</strong></p>
<hr />
<h4>（3）转移指令的目标地址范围</h4>
<p>相对偏移量用补码表示，寄存器字长为 16 位，因此相对偏移量范围为 $-2^{15} \sim 2^{15} - 1$，即 $-32768 \sim +32767$。</p>
<p>目标地址 = $(PC) + \text{偏移量}$，则目标地址范围为 $(PC) - 32768 \sim (PC) + 32767$。</p>
<p>又因为主存地址空间为 128KB，即地址范围 $0 \sim 2^{17} - 1 = 0 \sim 1FFFFH$，所以实际目标地址还要限制在此范围内。</p>
<p><strong>答案：理论相对范围为 $(PC) - 32768 \sim (PC) + 32767$，实际地址需落在 0～1FFFFH 内。</strong></p>
<hr />
<h4>（4）求 <code>add (R4), (R5)+</code> 的机器码与执行结果</h4>
<p>指令格式为 $OP \quad Ms,Rs \quad Md,Rd$。</p>
<p>已知：</p>
<ul>
<li>add 的操作码 OP = 0010B；</li>
<li>源操作数 <code>(R4)</code> 为寄存器间接寻址，Ms = 001B；</li>
<li>R4 编号为 100B；</li>
<li>目的操作数 <code>(R5)+</code> 为寄存器间接 + 自增寻址，Md = 010B；</li>
<li>R5 编号为 101B。</li>
</ul>
<p>因此机器码字段为：</p>
<p>$$ OP = 0010,\quad MsRs = 001100,\quad MdRd = 010101 $$</p>
<p>拼接：$0010\ 001100\ 010101$，按 4 位分组：$0010\ 0011\ 0001\ 0101$，转为十六进制：2315H。</p>
<p><strong>机器码答案：2315H。</strong></p>
<p>执行过程：</p>
<p>源操作数为 <code>(R4)</code>：$(R4) = 1234H$，源操作数地址为 1234H，源操作数为 $(1234H) = 5678H$。</p>
<p>目的操作数为 <code>(R5)+</code>：$(R5) = 5678H$，目的操作数地址为 5678H，目的操作数原值为 $(5678H) = 1234H$。</p>
<p>执行加法：$1234H + 5678H = 68ACH$，结果写回目的操作数所在存储单元：$(5678H) \leftarrow 68ACH$。</p>
<p>自增寻址执行：$(R5) + 1 \rightarrow R5$，所以 R5 = 5678H + 1 = 5679H。</p>
<p>改变的内容：</p>
<table>
<thead>
<tr>
<th>对象</th>
<th>原内容</th>
<th>新内容</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td>R5</td>
<td>5678H</td>
<td>5679H</td>
<td>目的操作数采用 <code>(R5)+</code>，执行后自增</td>
</tr>
<tr>
<td>存储单元 5678H</td>
<td>1234H</td>
<td>68ACH</td>
<td>加法结果写回目的操作数位置</td>
</tr>
</tbody>
</table>
<p>R4 不变，存储单元 1234H 不变。</p>
<hr />
<h3>习题5.13</h3>
<p><strong>题干：</strong>
某计算机采用 16 位定长指令字格式，其 CPU 中有一个标志寄存器，其中包括 CF、ZF 和 NF。条件转移指令格式为：</p>
<p>$$ 00000\quad C\quad Z\quad N\quad OFFSET $$</p>
<p>其中 00000 为操作码；C、Z、N 分别为 CF、ZF、NF 的对应检测位。某检测位为 1 时表示需检测对应标志，需检测的标志位中只要有一个为 1 就转移，否则不转移。OFFSET 是相对偏移量，用补码表示。转移执行时：</p>
<p>$$ \text{目标地址} = (PC) + 2 + OFFSET \times 2 $$</p>
<p>顺序执行时：</p>
<p>$$ \text{下条指令地址} = (PC) + 2 $$</p>
<p>请回答下列问题。
（1）该计算机存储器按字节编址还是按字编址？该条件转移指令向后最多可跳转多少条指令？
（2）某条件转移指令地址为 200CH，指令内容中 C = 0，Z = 1，N = 1，OFFSET = 11100011。若执行时 CF = 0，ZF = 0，NF = 1，则 PC 为多少？若执行时 CF = 1，ZF = 0，NF = 0，则 PC 为多少？
（3）实现"无符号数比较小于等于时转移"功能的指令中，C、Z、N 应各是什么？</p>
<hr />
<h4>（1）编址方式与最多反向跳转条数</h4>
<p>题中说明顺序执行时：下条指令地址 = $(PC) + 2$。</p>
<p>指令长度为 16 位，即 2 字节。如果按字编址，下一条指令地址应加 1；但题中加 2，说明地址单位是字节。</p>
<p>所以该计算机<strong>按字节编址</strong>。</p>
<p>OFFSET 为 8 位补码，范围为 $-128 \sim +127$。</p>
<p>转移目标地址为 $(PC) + 2 + OFFSET \times 2$。</p>
<p>最大反向跳转时 $OFFSET = -128$，相对顺序下一条指令地址最多后退 128 条指令。</p>
<p><strong>答案：按字节编址；相对于顺序下一条指令，最多可向后跳转 128 条指令。</strong></p>
<hr />
<h4>（2）求两种情况下 PC 的值</h4>
<p>已知指令地址 PC = 200CH，顺序下一条指令地址为 $200CH + 2 = 200EH$。</p>
<p>OFFSET = 11100011B = E3H，这是 8 位补码。因为最高位为 1，所以是负数：</p>
<p>$$ E3H = 227,\quad 227 - 256 = -29 $$</p>
<p>所以 OFFSET = -29。</p>
<p>若发生转移：</p>
<p>$$ \text{目标地址} = 200CH + 2 + (-29) \times 2 = 200EH - 58 $$</p>
<p>$$ 58_{10} = 3AH $$</p>
<p>$$ \text{目标地址} = 200EH - 003AH = 1FD4H $$</p>
<p>指令检测位为 C = 0，Z = 1，N = 1，说明只检测 ZF 和 NF，不检测 CF。</p>
<p><strong>情况一：</strong> CF = 0，ZF = 0，NF = 1</p>
<p>检测 ZF 和 NF：ZF = 0，NF = 1。检测的标志位中有一个为 1，所以发生转移。</p>
<p>$$ PC = 1FD4H $$</p>
<p><strong>答案：PC = 1FD4H。</strong></p>
<p><strong>情况二：</strong> CF = 1，ZF = 0，NF = 0</p>
<p>仍然只检测 ZF 和 NF，不检测 CF。ZF = 0，NF = 0，检测的标志位都不是 1，所以不转移。</p>
<p>顺序执行：$PC = 200CH + 2 = 200EH$。</p>
<p><strong>答案：PC = 200EH。</strong></p>
<hr />
<h4>（3）无符号数比较小于等于时转移</h4>
<p>无符号数比较通常通过减法实现。</p>
<p>若比较 $A - B$，无符号数 $A \le B$ 的条件为：</p>
<ol>
<li>$A &lt; B$：减法产生借位，$CF = 1$；</li>
<li>$A = B$：结果为 0，$ZF = 1$。</li>
</ol>
<p>因此无符号小于等于转移条件为 $CF = 1$ 或 $ZF = 1$。</p>
<p>题中规定检测位中只要有一个对应标志为 1 就转移，因此应检测 CF 和 ZF，不检测 NF。</p>
<p>所以 $C = 1$，$Z = 1$，$N = 0$。</p>
<p><strong>答案：C = 1，Z = 1，N = 0。</strong></p>
<hr />
<h3>习题5.14</h3>
<p><strong>题干：</strong>
假定计算机 M 字长为 16 位，按字节编址，连接 CPU 和主存的系统总线中地址线为 20 位、数据线为 8 位，采用 16 位定长指令，指令格式及其说明如下：</p>
<p>R 型：</p>
<p>$$ 000000\quad rs\quad rt\quad rd\quad op1 $$</p>
<p>功能：$R[rd] \leftarrow R[rs]\ op1\ R[rt]$</p>
<p>I 型：</p>
<p>$$ op2\quad rs\quad rt\quad imm $$</p>
<p>J 型：</p>
<p>$$ op3\quad target $$</p>
<p>功能：PC 的低 10 位 $\leftarrow$ target</p>
<p>其中 op1～op3 为操作码，rs、rt、rd 为通用寄存器编号，R[r] 表示寄存器 r 的内容，imm 为立即数，target 为转移目标的形式地址。请回答下列问题。
（1）ALU 的宽度是多少位？可寻址主存空间大小为多少字节？IR、MAR、MDR 分别应有多少位？
（2）R 型格式最多可定义多少种操作？I 型和 J 型格式总共最多可定义多少种操作？通用寄存器最多有多少个？
（3）假定 op1 为 0010 和 0011 时，分别表示带符号整数减法和带符号整数乘法指令，则指令 01B2H 的功能是什么？若 1、2、3 号通用寄存器当前内容分别为 B052H、0008H、0020H，则分别执行指令 01B2H 和 01B3H 后，3 号通用寄存器内容各是什么？各自结果是否溢出？
（4）若采用 I 型格式的访存指令中 imm 为带符号整数，则地址计算时应对 imm 进行零扩展还是符号扩展？
（5）无条件转移指令可以采用上述哪种指令格式？</p>
<hr />
<h4>（1）ALU、主存空间、IR、MAR、MDR 位数</h4>
<p>计算机字长为 16 位，所以 ALU 宽度通常为 16 位。</p>
<p>地址线为 20 位，且按字节编址，因此可寻址主存空间为 $2^{20}$ 字节，即 1MB。</p>
<p>采用 16 位定长指令，所以指令寄存器 IR 需要 16 位。</p>
<p>地址线 20 位，所以 MAR 需要 20 位。</p>
<p>数据线 8 位，所以 MDR 至少需要 8 位。</p>
<p><strong>答案：ALU 为 16 位；可寻址主存空间为 $2^{20}$ 字节（1MB）；IR 为 16 位，MAR 为 20 位，MDR 为 8 位。</strong></p>
<hr />
<h4>（2）最多操作数和寄存器个数</h4>
<p>R 型中 op1 为 4 位，因此最多可定义 $2^4 = 16$ 种 R 型操作。</p>
<p>I 型和 J 型使用 6 位主操作码 op2/op3。6 位操作码共有 $2^6 = 64$ 种编码。其中 000000 已用于 R 型指令格式，所以剩余 $64 - 1 = 63$ 种主操作码可分配给 I 型和 J 型。因此 I 型和 J 型格式总共最多可定义 63 种操作。</p>
<p>寄存器编号字段 rs、rt、rd 都是 2 位，因此通用寄存器最多有 $2^2 = 4$ 个。</p>
<p><strong>答案：R 型最多 16 种操作；I 型和 J 型总共最多 63 种操作；通用寄存器最多 4 个。</strong></p>
<hr />
<h4>（3）分析 01B2H 和 01B3H</h4>
<p>先将 01B2H 写成二进制：</p>
<p>$$ 01B2H = 0000\ 0001\ 1011\ 0010 $$</p>
<p>按 R 型格式划分：</p>
<p>$$ 000000\quad 01\quad 10\quad 11\quad 0010 $$</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>值</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>000000</td>
<td>R 型</td>
<td>R 型指令</td>
</tr>
<tr>
<td>rs</td>
<td>01B</td>
<td>1 号寄存器</td>
</tr>
<tr>
<td>rt</td>
<td>10B</td>
<td>2 号寄存器</td>
</tr>
<tr>
<td>rd</td>
<td>11B</td>
<td>3 号寄存器</td>
</tr>
<tr>
<td>op1</td>
<td>0010B</td>
<td>带符号整数减法</td>
</tr>
</tbody>
</table>
<p>因此 01B2H 的功能是：$R[3] \leftarrow R[1] - R[2]$，即 R3 $\leftarrow$ R1 - R2。</p>
<p>已知 R1 = B052H，R2 = 0008H，R3 = 0020H。</p>
<p><strong>执行 01B2H：</strong> R3 $\leftarrow$ R1 - R2 = B052H - 0008H = B04AH。</p>
<p>从带符号数角度看：B052H = $-20398$，0008H = 8，$-20398 - 8 = -20406$。16 位补码范围为 $-32768 \sim +32767$，$-20406$ 在范围内，因此不溢出。</p>
<p>执行 01B2H 后：R3 = B04AH，且不溢出。</p>
<p><strong>执行 01B3H：</strong> 01B3H = 0000 0001 1011 0011，按 R 型格式划分：</p>
<p>$$ 000000\quad 01\quad 10\quad 11\quad 0011 $$</p>
<p>op1 = 0011B 表示带符号整数乘法，功能为 $R[3] \leftarrow R[1] \times R[2]$。</p>
<p>R1 = B052H = $-20398$，R2 = 0008H = 8，$-20398 \times 8 = -163184$。</p>
<p>16 位带符号整数范围为 $-32768 \sim +32767$，$-163184$ 超出范围，因此发生溢出。</p>
<p>若只保留低 16 位：B052H $\times$ 0008H = 8290H（低 16 位）。</p>
<p>所以执行 01B3H 后，R3 中保存的低 16 位结果为 8290H，但结果溢出。</p>
<table>
<thead>
<tr>
<th>指令</th>
<th>功能</th>
<th>执行后 R3</th>
<th>是否溢出</th>
</tr>
</thead>
<tbody>
<tr>
<td>01B2H</td>
<td>$R3 \leftarrow R1 - R2$</td>
<td>B04AH</td>
<td>不溢出</td>
</tr>
<tr>
<td>01B3H</td>
<td>$R3 \leftarrow R1 \times R2$</td>
<td>8290H</td>
<td>溢出</td>
</tr>
</tbody>
</table>
<hr />
<h4>（4）imm 应零扩展还是符号扩展？</h4>
<p>题目说明 I 型访存指令中的 imm 是带符号整数。带符号整数在扩展位数时必须保持数值符号不变，因此应采用<strong>符号扩展</strong>。例如 6 位 imm 的最高位为 1，表示负数，扩展到 16 位或 20 位地址计算宽度时，高位应补 1。</p>
<p><strong>答案：应进行符号扩展。</strong></p>
<hr />
<h4>（5）无条件转移指令采用哪种格式？</h4>
<p>J 型格式的功能为：PC 的低 10 位 $\leftarrow$ target。它直接修改 PC，因此最适合实现无条件转移。</p>
<p><strong>答案：无条件转移指令可采用 J 型格式。</strong></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>存储系统</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E5%AD%98%E5%82%A8%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E5%AD%98%E5%82%A8%E7%B3%BB%E7%BB%9F/</id>
    <published>2026-06-29T09:31:16.000Z</published>
    <updated>2026-06-29T09:31:16.000Z</updated>
    <summary>整理计算机存储系统层次结构与性能指标，重点解析SRAM、DRAM、Cache三种映射方式与替换策略、页式虚拟存储器及TLB快表加速机制。</summary>
    <content type="html"><![CDATA[<h1>第四章：存储系统</h1>
<p>本章整理计算机中的存储系统。存储系统在计算机组成原理中占据重要地位，是考研及期末考试的重难点。</p>
<p>要想透彻理解存储系统，需要围绕一条主线和三个核心问题展开：</p>
<hr />
<h2>4.1 存储器概述</h2>
<h3>4.1.1 存储器分类与存储系统层次结构</h3>
<p>存储器要解决三个核心矛盾：<strong>速度要快、容量要大、成本要低</strong>。然而，单一的半导体存储介质无法同时满足这三个要求。</p>
<pre><code>寄存器速度最快但容量极小、成本极高。
外存容量极大、成本极低，但速度极慢。
</code></pre>
<p>为了解决这一矛盾，计算机系统采用<strong>多级存储层次结构</strong>，将不同速度与容量的存储介质有机地结合起来：</p>
<pre><code>flowchart BT
    L4[外存: 磁盘、SSD、Flash] --&gt; L3[主存: DRAM]
    L3 --&gt; L2[高速缓存: SRAM]
    L2 --&gt; L1[寄存器: CPU内部]
</code></pre>
<p>在这级结构中，<strong>越靠近 CPU，其速度越快、容量越小、每位成本越高</strong>；<strong>越远离 CPU，其容量越大、速度越慢、每位成本越低</strong>。通过硬件与操作系统的协同管理，这种结构使得整个存储系统表现出接近寄存器的存取速度，以及接近外存的存储容量。</p>
<hr />
<h3>4.1.2 存储器技术指标</h3>
<p>在存储系统中，主要使用以下四个基本性能指标来衡量其优劣：</p>
<ul>
<li><strong>存储容量</strong>：能保存的二进制信息总量。</li>
<li><strong>存取时间</strong>：从接收到读写控制信号和地址开始，到数据被送至数据线上或者存入存储单元所经历的时间。</li>
<li><strong>存取周期</strong>：连续两次独立访问存储器之间所需的最小时间间隔。存取周期通常大于存取时间。</li>
<li><strong>存储带宽</strong>：单位时间内存储器可传输的最大数据量，单位通常为字节每秒。</li>
</ul>
<h3>4.1.3 核心容量计算公式</h3>
<p>对于存储芯片或存储器，其容量的计算公式通常为：</p>
<p>$$\text{存储容量} = \text{存储单元数} \times \text{每个存储单元位数}$$</p>
<p>例如，规格为 $256\text{K} \times 8$ 的芯片，表示该芯片拥有 $256\text{K}$ 个存储单元，每个单元占 8 位，其总二进制容量为：</p>
<p>$$256\text{K} \times 8 \text{ bit} = 256\text{KB}$$</p>
<p>对于给定的存储芯片，其容量也可以直接通过引脚上的地址线和数据线条数计算得出：</p>
<p>$$\text{芯片容量} = 2^{\text{地址线根数}} \times \text{数据线位数}$$</p>
<p>例如，若某存储芯片具备 20 根地址线、8 根数据线，则其最大可寻址空间为 $2^{20} = 1\text{M}$ 个存储单元，数据宽度为 8 位，对应的总容量为：</p>
<p>$$2^{20} \times 8\text{ bit} = 1\text{MB}$$</p>
<hr />
<h3>4.1.5 主存中数据的存放</h3>
<p>在主存中，通常采用<strong>按字节编址</strong>的方式，即每个内存物理地址都对应一个字节。然而，计算机在处理 16 位、32 位或 64 位等多字节数据时，必须决定这些字节在连续内存地址中的排列次序。</p>
<h3>大端与小端存储模式</h3>
<p>多字节数据存入内存时存在两种不同的字节布局方式：</p>
<table>
<thead>
<tr>
<th>方式</th>
<th>低物理地址存放的字节</th>
<th>高物理地址存放的字节</th>
<th>规律特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>大端方式</strong></td>
<td>最高有效字节</td>
<td>最低有效字节</td>
<td>符合人类的书写与阅读直觉</td>
</tr>
<tr>
<td><strong>小端方式</strong></td>
<td>最低有效字节</td>
<td>最高有效字节</td>
<td>方便机器直接读取低位数据</td>
</tr>
</tbody>
</table>
<p>例如，一个 32 位的十六进制数据 <code>0x12345678</code>（其中最高有效字节为 <code>12H</code>，最低有效字节为 <code>78H</code>），如果从物理地址 <code>1000H</code> 开始存放，在两种方式下的分布如下：</p>
<pre><code>大端方式：
1000H: 12H
1001H: 34H
1002H: 56H
1003H: 78H

小端方式：
1000H: 78H
1001H: 56H
1002H: 34H
1003H: 12H
</code></pre>
<p>:::important[大端与小端判定技巧]
判断是大端还是小端，最核心的技巧是<strong>观察最低物理地址处存放的是高有效字节还是低有效字节</strong>：</p>
<ol>
<li>最低物理地址放高字节（<code>12H</code>） $\rightarrow$ <strong>大端</strong>。</li>
<li>最低物理地址放低字节（<code>78H</code>） $\rightarrow$ <strong>小端</strong>。
:::</li>
</ol>
<hr />
<h2>4.2 半导体存储器</h2>
<p>半导体存储器是构建主机内存储系统的基础介质，主要分为随机存取存储器和只读存储器两大类。</p>
<h3>SRAM</h3>
<p>SRAM 依靠双稳态触发器电路来保存信息。只要电源正常供电，触发器两端的状态就能锁存，信息不会丢失。</p>
<ul>
<li><strong>物理特点</strong>：存取速度快、不需要刷新、集成度低、功耗大、生产成本高。</li>
<li><strong>典型应用</strong>：常用于构建 CPU 内部或紧邻 CPU 的高速缓存。</li>
</ul>
<h3>DRAM</h3>
<p>DRAM 依靠栅极电容上的电荷积聚来保存信息。由于电容本身存在漏电现象，电容上的电荷在短时间内会流失，因此必须在信息丢失前定期对电容进行电荷补偿，这一过程称为<strong>刷新</strong>。同时，由于 DRAM 在读取数据时会将电容电荷泄放，这属于破坏性读出，因此读取后必须执行<strong>重写</strong>（恢复写入）操作。</p>
<ul>
<li><strong>物理特点</strong>：集成度高、每位成本极低、存取速度慢于 SRAM、需要额外的刷新控制电路。</li>
<li><strong>典型应用</strong>：通常用于构建大容量的主存储器。</li>
</ul>
<h3>SRAM 与 DRAM 的对比表</h3>
<p>下表总结了 SRAM 与 DRAM 的核心技术差异，是考试中的常考基础点：</p>
<table>
<thead>
<tr>
<th>对比项目</th>
<th>SRAM</th>
<th>DRAM</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>存储介质</strong></td>
<td>双稳态触发器</td>
<td>栅极电容电荷</td>
</tr>
<tr>
<td><strong>速度</strong></td>
<td>快</td>
<td>较慢</td>
</tr>
<tr>
<td><strong>集成度</strong></td>
<td>低</td>
<td>高</td>
</tr>
<tr>
<td><strong>每位成本</strong></td>
<td>高</td>
<td>低</td>
</tr>
<tr>
<td><strong>是否需要刷新</strong></td>
<td>不需要</td>
<td>需要（定期刷新与读后重写）</td>
</tr>
<tr>
<td><strong>主要应用场景</strong></td>
<td>高速缓存</td>
<td>主存储器</td>
</tr>
</tbody>
</table>
<p>:::tip[记忆口诀]
SRAM 快且贵，专做高速缓存；DRAM 慢且密，专做系统主存。
:::</p>
<h3>ROM</h3>
<p>ROM 是一类非易失性随机存储器。在系统掉电或断电后，ROM 内部所存储的数据信息不会丢失。</p>
<ul>
<li><strong>应用场景</strong>：常用于保存系统引导程序、固件、以及主板的系统输入输出系统。</li>
</ul>
<h3>闪速存储器（Flash）</h3>
<p>闪速存储器是一类可进行电擦除并重新编程的非易失性半导体存储器。</p>
<ul>
<li><strong>物理特点</strong>：读取速度较快，但写入前必须先进行擦除操作，导致其写入与擦除速度较慢。同时，闪速存储器存在擦写寿命限制。</li>
<li><strong>应用场景</strong>：常用于固态硬盘、U 盘以及嵌入式设备中作为存储介质。</li>
</ul>
<hr />
<h2>4.3 主存的组织及与 CPU 的连接</h2>
<p>主存储器与 CPU 之间的数据交互是通过系统总线中的三类信号线实现的：</p>
<ul>
<li><strong>地址线（单向）</strong>：由 CPU 指向存储器，用于指定 CPU当前读写操作的目标存储单元物理地址。</li>
<li><strong>数据线（双向）</strong>：在 CPU 与存储器之间双向传输读出或写入的数据二进制流。</li>
<li><strong>控制线（输入/输出）</strong>：控制信号包括读控制、写控制以及片选控制信号，用于协调读写操作的精确时序。</li>
</ul>
<h3>4.3.1 基本读写操作流程</h3>
<p>在系统总线层面，一次基本的读写操作流程如下：</p>
<ul>
<li><strong>读操作流程</strong>：
<ol>
<li>CPU 将目标存储单元的物理地址送上地址总线。</li>
<li>CPU 发出有效的读控制信号与片选控制信号。</li>
<li>主存储器译码后，从对应单元取出数据并送上数据总线。</li>
<li>CPU 在时钟上升沿从数据总线上读取数据并存入内部寄存器。</li>
</ol>
</li>
<li><strong>写操作流程</strong>：
<ol>
<li>CPU 将目标存储单元的物理地址送上地址总线。</li>
<li>CPU 将待写入的数据送上数据总线。</li>
<li>CPU 发出有效的写控制信号与片选控制信号。</li>
<li>主存储器在控制信号作用下，将数据线上的数据写入指定存储单元。</li>
</ol>
</li>
</ul>
<p>:::tip[DRAM 地址线复用]
在大规模存储器设计中，为了减少芯片引脚数，DRAM 芯片通常采用<strong>地址线复用技术</strong>。即行地址和列地址通过相同的引脚分时传送，这使得 DRAM 芯片的地址引脚数可以减少为原来的一半。
:::</p>
<hr />
<h3>4.3.2 存储器的扩展</h3>
<p>由于单个存储芯片的字数或位数往往无法直接满足 CPU 对系统存储器容量和数据宽度的要求，因此需要使用多片存储芯片进行组合扩展。</p>
<h3>位扩展法（宽度扩展）</h3>
<p>当单个芯片的字数（存储单元数）满足系统要求，但其位数（数据线宽度）小于系统数据总线宽度时，采用位扩展法。</p>
<ul>
<li><strong>连接规律</strong>：将所有芯片的<strong>地址线、读写控制信号线和片选信号线分别并联</strong>连接到 CPU 对应的总线上。每片芯片的<strong>数据线分别连接到 CPU 数据总线的不同数据位</strong>上，使多片芯片共同拼接成所需的数据位宽。</li>
<li><strong>所需芯片数</strong>：$$\text{位扩展倍数} = \frac{\text{系统要求的数据位数}}{\text{单片芯片的数据位数}}$$</li>
</ul>
<h3>字扩展法（容量扩展）</h3>
<p>当单片芯片的数据位数满足系统要求，但其字数（存储单元数）小于系统所需的总字数时，采用字扩展法。</p>
<ul>
<li><strong>连接规律</strong>：将所有芯片的<strong>地址总线低位、数据总线以及读写控制信号线并联</strong>连接。利用<strong>地址总线的高位经过译码器产生不同的片选信号</strong>，分别连接到各个芯片的片选引脚上，使得任意时刻只有符合当前高位地址的芯片被选中工作。</li>
<li><strong>所需芯片数</strong>：$$\text{字扩展倍数} = \frac{\text{系统要求的总字数}}{\text{单片芯片的字数}}$$</li>
</ul>
<h3>字位联合扩展法</h3>
<p>当系统所需的字数和位数均大于单片芯片的规格时，先通过位扩展将多片芯片组合为满足位宽要求的芯片组，再用多组芯片通过字扩展构成所需总字数的存储器。</p>
<ul>
<li><strong>总芯片片数计算</strong>：
$$\text{总片数} = \text{字扩展倍数} \times \text{位扩展倍数} = \left(\frac{\text{目标总字数}}{\text{单片字数}}\right) \times \left(\frac{\text{目标数据位数}}{\text{单片位数}}\right)$$</li>
</ul>
<hr />
<h2>4.4 并行主存系统</h2>
<p>由于 CPU 的运行速度远快于主存的读取速度，为了缓解“主存瓶颈”问题，可以采用多模块组成的并行主存系统，使多个模块并行或交叉工作。</p>
<h3>高位交叉编址（顺序分配方式）</h3>
<p>高位地址决定当前的存储模块号，低位地址决定模块内的单元地址。</p>
<ul>
<li><strong>地址结构</strong>：
$$\text{主存地址} = \text{高位模块号} \ + \ \text{低位体内地址}$$</li>
<li><strong>工作特点</strong>：相邻的物理地址分布在同一个存储模块中。当程序顺序执行时，CPU 会持续访问同一个模块，而其他模块处于闲置状态，无法实现多模块的并行存取，通常仅用于扩展主存容量。</li>
</ul>
<h3>低位交叉编址（交叉分配方式）</h3>
<p>低位地址决定当前的存储模块号，高位地址决定模块内的单元地址。</p>
<ul>
<li><strong>地址结构</strong>：
$$\text{主存地址} = \text{高位体内地址} \ + \ \text{低位模块号}$$</li>
<li><strong>工作特点</strong>：相邻的物理地址分布在不同的存储模块中。由于程序访问具有局部性，CPU 连续访问的指令或数据会均匀分布在不同的模块上，这使得多个存储模块可以采用流水线式的轮流启动机制，从而显著提高主存的整体带宽。</li>
</ul>
<p>:::important[低位交叉存储器无冲突条件及时间计算]
设低位交叉存储器的模块数为 $m$，每个模块的存储周期为 $T$，总线传输周期（即相邻两次启动存储器的时间间隔）为 $\tau$。为了使多模块能无冲突地流水线式工作，模块数 $m$ 必须满足：
$$m \ge \frac{T}{\tau}$$
连续读取 $n$ 个字时，所耗费的总存取时间为：
$$t = T + (n - 1)\tau$$
:::</p>
<hr />
<h2>4.5 高速缓冲存储器（Cache）</h2>
<p>Cache 是位于 CPU 与主存之间的高速、小容量半导体存储器，用于解决 CPU 与主存速度不匹配的问题。</p>
<h3>4.5.1 程序局部性</h3>
<p>Cache 的引入能够获得成功的理论基础是程序的<strong>局部性原理</strong>：</p>
<ul>
<li><strong>时间局部性</strong>：如果程序中某条指令或某个数据被访问，在不久的将来它很可能再次被访问。例如循环结构中的指令、循环控制变量。</li>
<li><strong>空间局部性</strong>：如果某个存储单元被访问，在不久的将来其相邻的存储单元也很可能被访问。例如数组的顺序遍历、顺序执行的指令段。</li>
</ul>
<h3>4.5.2 Cache 的基本概念</h3>
<p>主存与 Cache 之间是<strong>以数据块为单位</strong>进行数据交换的（主存中的“块”在 Cache 中称为“行”）。</p>
<ul>
<li><strong>Cache 命中</strong>：当 CPU 发出读请求时，所需的数据块已在 Cache 中，CPU 直接高速访问 Cache 即可，无需访问主存。</li>
<li><strong>Cache 缺失</strong>：所需数据块不在 Cache 中，CPU 必须访问主存读取数据，并同时将该数据块调入 Cache。</li>
</ul>
<hr />
<h3>4.5.3 地址映射</h3>
<p>当 CPU 给出主存物理地址时，Cache 的硬件控制器必须能够将该主存地址转换为 Cache 内部的行地址，并判断该主存块是否已经装入 Cache。</p>
<p>主存物理地址通常被拆分为以下三个基本字段：</p>
<table>
<thead>
<tr>
<th>字段名称</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>标记（Tag）</strong></td>
<td>存放在 Cache 每一行的标记位中，用于与主存地址的高位比对以判断是否命中</td>
</tr>
<tr>
<td><strong>索引（Index）</strong></td>
<td>用于在 Cache 中定位可能存放该数据块的 Cache 行或组</td>
</tr>
<tr>
<td><strong>块内地址（Offset）</strong></td>
<td>用于在选中的数据块内定位具体的字或字节</td>
</tr>
</tbody>
</table>
<h4>直接映射</h4>
<p>主存中的每个数据块只能映射到 Cache 中唯一一个固定的行。</p>
<ul>
<li><strong>映射关系</strong>：$$\text{Cache 行号} = \text{主存块号} \bmod \text{Cache 总行数}$$</li>
<li><strong>优缺点</strong>：硬件设计最简单，地址变换速度快；但由于映射关系固定，极易产生块冲突，导致 Cache 命中率下降。</li>
</ul>
<h4>全相联映射</h4>
<p>主存中的任意数据块可以装入 Cache 中的任意一行。</p>
<ul>
<li><strong>映射关系</strong>：主存块可以放置在 Cache 的任何空闲行中。</li>
<li><strong>优缺点</strong>：冲突概率低，Cache 空间利用率和命中率高；但由于需要同时比对 Cache 所有行的标记，硬件电路复杂，成本高。</li>
</ul>
<h4>组相联映射</h4>
<p>将 Cache 所有行分为若干个大小相同的“组”，主存块先通过模运算映射到唯一确定的组，但在该组内部的各行之间可以任意放置（即“组间直接映射，组内全相联映射”）。</p>
<ul>
<li><strong>映射关系</strong>：$$\text{Cache 组号} = \text{主存块号} \bmod \text{Cache 总组数}$$</li>
<li><strong>优缺点</strong>：冲突概率低于直接映射，硬件实现难度与成本又低于全相联映射，是现代处理器中最常用的折中方案。</li>
</ul>
<hr />
<h3>4.5.4 替换算法与写入策略</h3>
<h4>核心替换算法</h4>
<p>当 Cache 对应位置已满，而 CPU 又需要调入新数据块时，必须腾出某个已有的旧数据块，这由<strong>替换算法</strong>决定：</p>
<ul>
<li><strong>随机算法</strong>：随机选择一个 Cache 行进行替换。实现简单，但未考虑程序访问规律，命中率低。</li>
<li><strong>先进先出算法</strong>：优先替换最先调入 Cache 的数据块。同样未考虑局部性规律，可能出现“贝拉迪异常”现象。</li>
<li><strong>LRU 算法</strong>：优先替换近期最长一段时间未被访问过的数据块。该算法高度契合时间局部性原理，是实际系统和考试题中很常见的近似最优替换策略，但不保证在所有访问序列下命中率最高。</li>
<li><strong>最不经常使用算法</strong>：优先替换历史上累计访问次数最少的数据块。</li>
</ul>
<p>:::tip[LRU 模拟技巧]
考试中若要求手动模拟 LRU 替换过程，只需为每个 Cache 行维护一个“新鲜度”计数器或直接记录访问顺序，每次替换时挑出<strong>距离当前时刻最久未被触及</strong>的行即可。
:::</p>
<h4>Cache 写操作策略</h4>
<p>当 CPU 执行写操作时，若 Cache 命中，为了保证 Cache 与主存中的数据一致性，通常采用以下写策略：</p>
<ul>
<li><strong>全写法</strong>：在写 Cache 的同时，将数据同步写入主存中。该方法能够始终保证主存与 Cache 数据绝对一致，控制简单；但会引起频繁的主存写操作，降低了系统的整体写入效率。</li>
<li><strong>写回法</strong>：写操作时仅修改 Cache 对应的行，并为该行设置一个<strong>脏位</strong>标记为 1。只有当该 Cache 行在未来被替换出 Cache 时，硬件才根据脏位状态决定是否将修改过的数据写回主存。该方法显著减少了主存写次数，但控制逻辑复杂，且在写回前主存与 Cache 数据存在暂时不一致。</li>
</ul>
<p>若写操作时 Cache 缺失，也有两种处理策略：</p>
<ul>
<li><strong>写分配法</strong>：先将主存中的目标块调入 Cache，然后修改 Cache。常与写回法配合使用。</li>
<li><strong>非写分配法</strong>：直接在主存中修改数据，不将数据块调入 Cache。常与全写法配合使用。</li>
</ul>
<hr />
<h2>4.6 虚拟存储器</h2>
<p>虚拟存储器是计算机系统为了给程序提供一个<strong>极大、连续的虚拟地址空间</strong>，而在主存和外存之间构建的软硬件统一管理系统。</p>
<h3>4.6.1 虚拟存储器的工作原理</h3>
<ul>
<li>程序运行时使用的是<strong>虚拟地址</strong>（逻辑地址），而不是物理主存的真实地址。</li>
<li>CPU 访问数据时，由硬件和操作系统协同将虚拟地址自动转换为<strong>物理地址</strong>。</li>
<li>当前运行所需的页面调入主存储器中，暂时不用的页面存放在外存储器（磁盘/SSD）中。当访问的页面不在主存时，触发<strong>缺页异常</strong>，由操作系统负责从外存调度页面。</li>
</ul>
<h3>4.6.2 虚拟存储器的地址映射与变换</h3>
<p>页式虚拟存储器将虚拟空间划分为固定大小的<strong>页</strong>，将物理主存空间划分为大小完全相同的<strong>页框</strong>。</p>
<ul>
<li><strong>虚拟地址结构</strong>：$$\text{虚拟地址} = \text{虚拟页号} \ + \ \text{页内偏移量}$$</li>
<li><strong>物理地址结构</strong>：$$\text{物理地址} = \text{物理页号} \ + \ \text{页内偏移量}$$</li>
<li><strong>地址转换核心</strong>：通过进程的<strong>页表</strong>查出“虚拟页号”到“物理页号”的映射关系，而页内偏移量在转换前后保持绝对不变。</li>
</ul>
<h3>4.6.3 页式虚拟存储器</h3>
<p>页表保存在主存中，每个虚拟页在页表中对应一个页表项，通常包含以下控制位：</p>
<ul>
<li><strong>有效位（装入位）</strong>：为 1 表示该页面已在主存中，可直接访问；为 0 表示不在主存，会触发缺页中断。</li>
<li><strong>脏位（修改位）</strong>：为 1 表示该页在主存中被修改过，换出主存时必须写回外存。</li>
<li><strong>引用位（使用位）</strong>：用于页面置换算法（如 LRU）统计页面是否被访问过。</li>
</ul>
<hr />
<h3>4.6.4 TLB 的加速作用</h3>
<p>由于页表保存在主存中，CPU 每次进行数据存取时，都必须先访问一次主存中的页表以获取物理页号，然后再访问一次主存存取数据。这种“两次访存”机制严重拖慢了系统速度。</p>
<p>为了加速地址转换，引入了<strong>TLB</strong>。</p>
<h3>工作机制</h3>
<p>TLB 是一个小容量、高速度的专用 SRAM 芯片，用于缓存进程页表中最常用的一部分页表项（虚拟页号到物理页号的映射）。</p>
<pre><code>CPU 给出虚拟地址 
  ↓
首先并行查找 TLB
  ├─ 命中 ──→ 直接得到物理页号 ──→ 访问Cache/主存 (仅需一次物理访存)
  └─ 缺失 ─→ 查找主存页表
                           ├─ 页表命中 ──→ 写入TLB并得到物理页号 ──→ 访问Cache/主存
                           └─ 页表缺失 ──→ 触发缺页异常 ──→ 操作系统进行页面置换
</code></pre>
<p>:::tip[TLB 的加速作用]
TLB 的引入显著缩短了虚拟地址到物理地址的转换时间，使得在绝大多数情况下，地址转换对 CPU 执行效率的影响可以忽略不计。
:::</p>
<hr />
<h2>4.7 Cache 与虚拟存储器的对比</h2>
<p>虽然 Cache 与虚拟存储器都是利用局部性原理来构建的存储层次结构，但它们在设计目的和实现细节上存在本质区别：</p>
<table>
<thead>
<tr>
<th>对比维度</th>
<th>高速缓冲存储器（Cache）</th>
<th>虚拟存储器</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>主要设计目的</strong></td>
<td>提高 CPU 访存的速度</td>
<td>扩大程序的虚拟地址空间</td>
</tr>
<tr>
<td><strong>数据源与下一级</strong></td>
<td>主存是下一级，数据源自主存</td>
<td>外存是下一级，数据源自外存</td>
</tr>
<tr>
<td><strong>管理主体</strong></td>
<td>纯硬件电路控制器完成，对程序员完全透明</td>
<td>硬件与操作系统协同完成，对应用程序员透明</td>
</tr>
<tr>
<td><strong>缺失代价</strong></td>
<td>极低（仅需访问主存，几十个 CPU 周期）</td>
<td>极高（需访问慢速外存，数十万个 CPU 周期）</td>
</tr>
<tr>
<td><strong>映射与粒度</strong></td>
<td>以主存“块”（一般为几十字节）为单位</td>
<td>以“页”（通常为几 KB）为单位</td>
</tr>
</tbody>
</table>
<p>:::note[Cache 与虚拟存储器的本质区别]
Cache 是主存的加速层，虚拟存储器是主存的扩展层。
:::</p>
<hr />
<h2>4.8 核心逻辑链条总结</h2>
<p>本章的所有设计思想和技术指标，最终均可以通过以下最核心的逻辑链条进行贯通：</p>
<p>$$\text{CPU 运算速度极快} \ \longleftrightarrow \ \text{主存速度较慢} \ \Longrightarrow \ \text{引入 Cache 加速访存}$$</p>
<p>$$\text{主存物理容量有限} \ \longleftrightarrow \ \text{进程地址空间需求大} \ \Longrightarrow \ \text{引入虚拟存储器扩展空间}$$</p>
<p>$$\text{主存访问开销较大} \ \longleftrightarrow \ \text{页表查询需要访存} \ \Longrightarrow \ \text{引入快表 TLB 加速地址转换}$$</p>
<p>存储系统通过这一系列“寄存器 $\rightarrow$ Cache $\rightarrow$ 主存 $\rightarrow$ 外存”的巧妙层次架构，在速度、容量和成本之间取得了完美的平衡。</p>
<hr />
<h2>4.9 历年期末真题分类详解</h2>
<p>本节汇总了历年期末考试中关于第四章存储系统的全部真题，进行系统化分类与详尽解析。</p>
<h3>4.9.1 存储器分类与技术指标</h3>
<h4>Q1 (南阳理工学院 2024-2025 学年第 2 学期期末试卷（C 卷） 填空题)</h4>
<p><strong>题干</strong>：若某存储器存储周期为 $250\text{ ns}$，每次读出 $16$ 位，该存储器的数据传输速率是________。</p>
<p><strong>解析</strong>：
数据传输速率即为存储带宽。计算公式为：
$$\text{数据传输速率} = \frac{\text{每次读出位数}}{\text{存储周期}}$$
已知每次读出 16 位，即 $16\text{ bit} = 2\text{ B}$。存储周期为 $250\text{ ns} = 250 \times 10^{-9}\text{ s}$。
代入数据计算：
$$\text{数据传输速率} = \frac{2\text{ B}}{250 \times 10^{-9}\text{ s}} = \frac{2}{250} \times 10^9 \text{ B/s} = 0.008 \times 10^9 \text{ B/s} = 8 \times 10^6 \text{ B/s} = 8 \text{ MB/s}$$
若以位为单位，则是 $64\text{ Mbit/s}$。一般默认以字节每秒（B/s）为单位回答。</p>
<p><strong>答案</strong>：$8\text{ MB/s}$（或 $8 \times 10^6\text{ B/s}$）。</p>
<h4>Q2 (2022-2023-2考试 判断题)</h4>
<p><strong>题干</strong>：半导体 RAM 信息可读可写，且断电后仍能保持记忆。（    ）</p>
<p><strong>解析</strong>：
半导体随机存储器 RAM 是易失性存储器，断电后其内部存储的数据会全部丢失。断电不丢失数据的是非易失性存储器（如 ROM、Flash）。</p>
<p><strong>答案</strong>：错。</p>
<h4>Q3 (2022-2023-2考试 判断题)</h4>
<p><strong>题干</strong>：双端口存储器可以同时访问同一区间、同一单元。（    ）</p>
<p><strong>解析</strong>：
双端口存储器具有两套相互独立的读写控制线路，允许在同一时刻访问主存。只要两个端口不是同时对同一个单元进行写入操作，就允许同时访问。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h3>4.9.2 主存中数据的存放</h3>
<h4>Q4 (南阳理工学院 2024-2025 学年第 2 学期期末试卷（C 卷） 选择题)</h4>
<p><strong>题干</strong>：以下哪种存储方式将数据的最高位有效字节（MSB）存储在最低地址内存单元？（    ）</p>
<p>A. 大端方式
B. 小端方式
C. 中端方式
D. 随机方式</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：大端方式将多字节数据的高位字节（最高有效字节）存放在低物理地址单元中，而将低位字节存放在高物理地址单元中。这与人类通常的阅读书写顺序（从左到右）是一致的。</li>
<li><strong>B [错误]</strong>：小端方式与大端方式相反，它将多字节数据的低位字节（最低有效字节）存放在低物理地址单元中，而将高位字节存放在高物理地址单元中。</li>
<li><strong>C [错误]</strong>：在计算机系统结构中没有所谓的“中端方式”。</li>
<li><strong>D [错误]</strong>：多字节数据的物理字节存放是有固定逻辑和物理规则的，不可能采用“随机方式”存储。</li>
</ul>
<h4>Q5 (计算机组成原理2020-2021第2学期A 选择题)</h4>
<p><strong>题干</strong>：某计算机字长为 32 位，按字节编址，采用小端方式存放数据。假定有一个 double 型变量，其机器数表示为 <code>1122 3344 5566 7788H</code>，存放在 <code>0000 8040H</code> 开始的连续存储单元中，则存储单元 <code>0000 8046H</code> 中存放的是（    ）。</p>
<p>A. <code>22H</code>
B. <code>33H</code>
C. <code>77H</code>
D. <code>66H</code></p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li>
<p><strong>A [正确]</strong>：<code>double</code> 型变量占 8 字节（64 位）。机器数表示为 <code>1122 3344 5566 7788H</code>，其字节从高到低依次为 <code>11H</code>最高有效字节至 <code>88H</code>最低有效字节。采用小端方式存储时，低物理地址存放低位字节，高物理地址存放高位字节。因此，从起始地址 <code>0000 8040H</code> 开始的连续 8 字节单元的存放数据为：</p>
<ul>
<li><code>0000 8040H</code>：<code>88H</code></li>
<li><code>0000 8041H</code>：<code>77H</code></li>
<li><code>0000 8042H</code>：<code>66H</code></li>
<li><code>0000 8043H</code>：<code>55H</code></li>
<li><code>0000 8044H</code>：<code>44H</code></li>
<li><code>0000 8045H</code>：<code>33H</code></li>
<li><code>0000 8046H</code>：<code>22H</code></li>
<li><code>0000 8047H</code>：<code>11H</code></li>
</ul>
<p>由上可知，存储单元 <code>0000 8046H</code> 存放的是 <code>22H</code>。</p>
</li>
<li>
<p><strong>B [错误]</strong>：<code>33H</code> 存放于地址 <code>0000 8045H</code> 中。</p>
</li>
<li>
<p><strong>C [错误]</strong>：<code>77H</code> 存放于地址 <code>0000 8041H</code> 中。</p>
</li>
<li>
<p><strong>D [错误]</strong>：<code>66H</code> 存放于地址 <code>0000 8042H</code> 中。</p>
</li>
</ul>
<hr />
<h3>4.9.3 芯片参数与存储器扩展</h3>
<h4>Q6 (计算机组成原理2020-2021第二学期B 简答题)</h4>
<p><strong>题干</strong>：某存储容量为 256MB 的存储器由若干个 4M×8 的 DRAM 芯片构成，则 DRAM 芯片的地址引脚和数据引脚的数量是？（    ）</p>
<p><strong>解析</strong>：</p>
<ol>
<li>DRAM 芯片规格为 $4\text{M} \times 8\text{位}$。</li>
<li>数据引脚数由芯片的位宽决定，位宽为 8 位，因此数据引脚数为 8 根。</li>
<li>芯片字数为 $4\text{M} = 2^{22}$ 个存储单元。如果采用普通的寻址方式，地址线需要 22 根。但 <strong>DRAM 芯片为了减少引脚数，普遍采用地址引脚复用技术</strong>，行地址和列地址分时复用相同的引脚。因此实际的地址引脚数减半：
$$\text{DRAM 地址引脚数} = \frac{22}{2} = 11\text{ 根}$$</li>
</ol>
<p><strong>答案</strong>：地址引脚 11 根，数据引脚 8 根。</p>
<h4>Q7 (计算机组成原理2020-2021第二学期B 选择题)</h4>
<p><strong>题干</strong>：某计算机主存容量为 64KB，其中 ROM 区为 4KB，其余为 RAM 区，按字节编址。现要用 2K×8 位的 ROM 芯片和 4K×4 位的 RAM 芯片来设计该存储器，需要上述规格的 ROM 芯片数和 RAM 芯片数分别是（    ）。</p>
<p>A. 1, 15
B. 2, 15
C. 1, 30
D. 2, 30</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：ROM 芯片数与 RAM 芯片数计算均错误。</li>
<li><strong>B [错误]</strong>：RAM 芯片数计算错误，仅计算了字扩展需求（15），忽略了位扩展需求（应乘以 2）。</li>
<li><strong>C [错误]</strong>：ROM 芯片数计算错误。ROM 区目标容量为 4KB，使用 $2\text{K} \times 8$ 位（即 2KB）芯片，若仅用 1 片，总容量只有 2KB，达不到 ROM 区容量需求。</li>
<li><strong>D [正确]</strong>：
<ol>
<li><strong>ROM 区芯片计算</strong>：
ROM 区总容量为 $4\text{KB} = 4\text{K} \times 8$ 位。使用规格为 $2\text{K} \times 8$ 位的 ROM 芯片，只需进行字扩展。需要的 ROM 芯片数量为：
$$\text{ROM 芯片数} = \frac{4\text{KB}}{2\text{K} \times 8\text{位}} = \frac{4\text{K} \times 8\text{位}}{2\text{K} \times 8\text{位}} = 2\text{ 片}$$</li>
<li><strong>RAM 区芯片计算</strong>：
主存总容量为 64KB，ROM 区占 4KB，则 RAM 区容量为 $64\text{KB} - 4\text{KB} = 60\text{KB} = 60\text{K} \times 8$ 位。
使用规格为 $4\text{K} \times 4$ 位的 RAM 芯片进行字位联合扩展。字扩展倍数为 $\frac{60\text{K}}{4\text{K}} = 15$ 倍，位扩展倍数为 $\frac{8\text{位}}{4\text{位}} = 2$ 倍。
因此所需的 RAM 芯片总数为：
$$\text{RAM 芯片数} = 15 \times 2 = 30\text{ 片}$$
综上，需要的 ROM 芯片为 2 片，RAM 芯片为 30 片。</li>
</ol>
</li>
</ul>
<h4>Q8 (2022-2023-2考试 简答题)</h4>
<p><strong>题干</strong>：用一个 512K×8 位的 Flash 存储芯片组成一个 4M×32 位的半导体只读存储器，存储器按字编址，试回答以下问题：
(1) 共需要几片这样的存储芯片？
(2) 该存储器的数据线和地址线数分别是多少根？</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>芯片数计算</strong>：
$$\text{芯片总片数} = \left(\frac{4\text{M}}{512\text{K}}\right) \times \left(\frac{32\text{位}}{8\text{位}}\right) = 8 \times 4 = 32\text{ 片}$$</li>
<li><strong>数据线与地址线计算</strong>：
<ul>
<li>数据总线宽度与存储器的数据位数一致，为 32 位，因此数据线数为 32 根。</li>
<li>存储器按字编址，其字数为 $4\text{M} = 2^{22}$。因此，寻址这 $4\text{M}$ 个字需要 22 根地址线。</li>
</ul>
</li>
</ol>
<p><strong>答案</strong>：(1) 需要 32 片。(2) 数据线为 32 根，地址线为 22 根。</p>
<hr />
<h3>4.9.4 存储系统层次结构设计目的</h3>
<h4>Q9 (计算机组成原理2020-2021第二学期B 选择题)</h4>
<p><strong>题干</strong>：计算机的存储器采用分级方式是为了（    ）。</p>
<p>A. 方便编程
B. 解决容量、速度、价格三者之间的矛盾
C. 保存大量数据方便
D. 操作方便</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：方便编程是采用虚拟存储器等软硬件协同管理后的效果之一，从根本上解决存储体系矛盾才是采用分级存储体系的核心出发点。</li>
<li><strong>B [正确]</strong>：单一存储介质无法同时实现超高速度、超大容量和极低成本。采用“寄存器 - Cache - 主存 - 外存”的多级层次结构，利用了程序的局部性原理，通过软硬件的协同，成功在整体上获得了接近最高速介质的速度、接近最廉价介质的容量以及极低的平均成本，解决了这三者的矛盾。</li>
<li><strong>C [错误]</strong>：保存大量数据可以直接使用低速、大容量的辅存，无需设计复杂的 Cache 等高速多级分级结构。</li>
<li><strong>D [错误]</strong>：操作方便主要依靠操作系统的接口实现，并不是物理层面上采用分级存储的主要物理动因。</li>
</ul>
<hr />
<h3>4.9.5 高位/低位交叉与双端口存储器</h3>
<h4>Q10 (南阳理工学院 2024-2025 学年第 2 学期期末试卷（C 卷） 判断题)</h4>
<p><strong>题干</strong>：单体多字存储器中，每个存储单元存储多个字。（    ）</p>
<p><strong>解析</strong>：
单体多字存储器是一种并行主存系统。它的特点是只有一个存储体，但每个存储单元的字长是普通存储字长的数倍（例如包含 4 个字）。CPU 每次访问时，可以同时读出或写入多个字的数据，从而在空间上并行提高了存储带宽。</p>
<p><strong>答案</strong>：对。</p>
<hr />
<h3>4.9.6 Cache 地址映射与参数计算</h3>
<h4>Q11 (计组2019秋18级测试 答案.md 综合分析题)</h4>
<p><strong>题干</strong>：假设主存容量为 512MB，Cache 容量为 4KB，每个字块为 16 个字，每个字为 32 位。请回答以下问题：
(1) Cache 可容纳多少块，其地址为多少位？
(2) 主存可容纳多少块，其地址为多少位？
(3) 在四路组相联（即 Cache 每组包含 4 个块）映射方式下，请画出主存地址各字段的位数。</p>
<p><strong>解析</strong>：
首先明确基本物理单位：</p>
<ul>
<li>每个字为 32 位 = 4 字节（B）。</li>
<li>每个字块包含 16 个字 = $16 \times 4\text{ B} = 64\text{ B}$。</li>
<li>Cache 容量为 $4\text{KB} = 4096\text{ B}$。</li>
<li>主存容量为 $512\text{MB} = 512 \times 2^{20}\text{ B} = 2^{29}\text{ B}$。</li>
</ul>
<p>(1) <strong>Cache 参数</strong>：</p>
<ul>
<li>Cache 的块数 = $\frac{\text{Cache 容量}}{\text{块大小}} = \frac{4\text{KB}}{64\text{ B}} = 64\text{ 块}$。</li>
<li>Cache 容量为 $4\text{KB}$，按字节寻址需要 12 位地址（因 $2^{12} = 4096$）。
(2) <strong>主存参数</strong>：</li>
<li>主存的块数 = $\frac{\text{主存容量}}{\text{块大小}} = \frac{512\text{MB}}{64\text{ B}} = 8\text{M 块} = 2^{23}\text{ 块}$。</li>
<li>主存容量为 $512\text{MB}$，按字节寻址需要 29 位地址（因 $2^{29} = 512\text{M}$）。
(3) <strong>四路组相联映射下的地址字段划分</strong>：</li>
<li>Cache 总块数为 64 块。因为是四路组相联，每组有 4 块，则 Cache 的总组数 = $\frac{64}{4} = 16\text{ 组} = 2^4\text{ 组}$。</li>
<li>组地址（Index）需要 4 位。</li>
<li>块内偏移地址（Offset）由块大小决定，块大小为 64 B，按字节寻址需要 6 位（因 $2^6 = 64$）。</li>
<li>主存字块标记（Tag）的位数 = 主存地址总位数 - 组地址位数 - 块内偏移位数 = $29 - 4 - 6 = 19\text{ 位}$。</li>
</ul>
<p><strong>答案</strong>：
(1) Cache 可容纳 64 块，地址为 12 位。
(2) 主存可容纳 $8\text{M}$（即 $2^{23}$）块，地址为 29 位。
(3) 主存地址各字段划分如下：</p>
<table>
<thead>
<tr>
<th>主存字块标记（Tag）</th>
<th>组地址（Index）</th>
<th>字块内地址（Offset）</th>
</tr>
</thead>
<tbody>
<tr>
<td>19位</td>
<td>4位</td>
<td>6位</td>
</tr>
</tbody>
</table>
<h4>Q12 (2022-2023-2考试 计算题)</h4>
<p><strong>题干</strong>：某计算机的 Cache 由 64 个存储块构成，采用四路组相联映射方式，主存包含 4096 个存储块，每块由 128 个字组成，访问地址为字地址。
(1) 主存地址和 Cache 地址各有多少位？
(2) 按照题干条件中的映射方式，列出主存地址的划分情况，并标出各部分的位数。</p>
<p><strong>解析</strong>：
注意此题中<strong>访问地址为字地址</strong>，即寻址的基本单位是“字”，而不是“字节”。</p>
<ul>
<li>块大小 = 128 个字。</li>
<li>Cache 块数 = 64 块。</li>
<li>Cache 容量（字） = $64 \times 128 = 8192\text{ 字} = 2^{13}\text{ 字}$，因此 Cache 地址为 13 位。</li>
<li>主存块数 = 4096 块 = $2^{12}\text{ 块}$。</li>
<li>主存容量（字） = $4096 \times 128 = 2^{12} \times 2^7 = 2^{19}\text{ 字}$，因此主存地址为 19 位。</li>
</ul>
<p>(2) <strong>地址字段划分</strong>：</p>
<ul>
<li>块内地址（Offset）由块大小决定。块大小为 128 字，字地址寻址需要 7 位（因 $2^7 = 128$）。</li>
<li>Cache 组数 = $\frac{64\text{ 块}}{4\text{ 块/组}} = 16\text{ 组} = 2^4\text{ 组}$，因此组地址（Index）为 4 位。</li>
<li>主存标记（Tag）的位数 = 主存地址位数 - 组地址位数 - 块内地址位数 = $19 - 4 - 7 = 8\text{ 位}$。</li>
</ul>
<p><strong>答案</strong>：
(1) 主存地址为 19 位，Cache 地址为 13 位。
(2) 主存地址划分如下：</p>
<table>
<thead>
<tr>
<th>标记（Tag）</th>
<th>组号（Index）</th>
<th>块内偏移（Offset）</th>
</tr>
</thead>
<tbody>
<tr>
<td>8位</td>
<td>4位</td>
<td>7位</td>
</tr>
</tbody>
</table>
<h4>Q13 (计算机组成原理2020-2021第二学期B 选择题)</h4>
<p><strong>题干</strong>：计算机存储系统中的高速缓存采用直接映射策略时，高速缓存控制器如何确定是否命中（    ）。</p>
<p>A. 高速缓存中行标志与 CPU 给出物理地址的高位一致
B. 高速缓存中与 CPU 给出的物理地址中组索引一致的行中存在行标志与物理地址高位一致的行，且该行有效
C. 高速缓存中与 CPU 给出的物理地址中行索引一致的行中的行标志与物理地址的高位一致，且该行有效
D. 高速缓存中存在行标志与 CPU 给出物理地址的高位一致的行，且该行有效</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：直接映射中主存块只能映射到唯一的 Cache 行中。仅仅行标记与高位一致是不够的，还必须要求该 Cache 行的有效位为 1。</li>
<li><strong>B [错误]</strong>：直接映射按行索引定位，不需要使用组索引。组索引是组相联映射方式所采用的字段。</li>
<li><strong>C [正确]</strong>：在直接映射策略中，主存中的每一块均映射到 Cache 的唯一固定行中。当 CPU 给出一个访存物理地址时，Cache 控制器首先根据地址中的行索引字段，直接定位到唯一的 Cache 行。接着，控制器需要检查两点：第一，该 Cache 行中的标记（行标志）是否与物理地址中的高位标记（Tag）字段完全一致；第二，检查该行的有效位（Valid Bit）是否为 1。只有当有效位为 1 且标记完全匹配时，才判定为 Cache 命中。</li>
<li><strong>D [错误]</strong>：此项属于全相联映射方式下的命中判断逻辑，在直接映射下，不需要检索所有的行，只需直接定位并比较那一行即可。</li>
</ul>
<hr />
<h3>4.9.7 Cache 命中率与局部性</h3>
<h4>Q14 (南阳理工学院 2024-2025 学年第 2 学期期末试卷（C 卷） 计算题)</h4>
<p><strong>题干</strong>：假设某计算机的存储系统由 Cache 和主存组成，某程序执行过程中访问 $2000$ 次，其中访问 Cache 缺失（未命中）$50$ 次，则 Cache 的<strong>命中率</strong>是多少？</p>
<p><strong>解析</strong>：
Cache 命中率 $H$ 的计算公式为：
$$H = \frac{\text{命中次数}}{\text{总访问次数}} = \frac{\text{总访问次数} - \text{未命中次数}}{\text{总访问次数}}$$
代入数据计算：
$$H = \frac{2000 - 50}{2000} = \frac{1950}{2000} = 97.5%$$</p>
<p><strong>答案</strong>：$97.5%$。</p>
<hr />
<h3>4.9.8 虚拟存储器与透明性</h3>
<h4>Q15 (南阳理工学院 2024-2025 学年第 2 学期期末试卷（C 卷） 判断题)</h4>
<p><strong>题干</strong>：虚拟存储器对应用程序员不透明，对系统程序员透明。（    ）</p>
<p><strong>解析</strong>：
在计算机组成原理和操作系统中，“透明”是指“客观存在但无需关心、看不见”。</p>
<ul>
<li>虚拟存储器由硬件与操作系统自动管理。<strong>应用程序员</strong>在编写程序时，可以直接使用大范围的虚拟地址，无需操心物理内存的调入调出细节，因此虚拟存储器<strong>对应用程序员是透明的</strong>。</li>
<li><strong>系统程序员</strong>需要编写操作系统的页表管理、页面置换算法和中断处理程序，必须直接控制虚拟存储器的物理运作，因此<strong>对系统程序员是不透明的</strong>。</li>
</ul>
<p><strong>答案</strong>：错。</p>
<hr />
<h2>习题4：课后习题与真题题目汇总</h2>
<h3>习题4.0 原题目录</h3>
<h4>4.1 名词解释</h4>
<p>解释下列名词：</p>
<blockquote>
<p>存取时间、存取周期、存储器带宽、存储单元、边界对齐的数据存放、大端存储、小端存储、静态存储器、动态存储器、刷新、刷新周期、字扩展、位扩展、多体交叉存储器、高速缓冲存储器、双端口存储器、相联存储器、时间局部性、地址映射、直接相联映射、全相联映射、组相联映射、命中率、虚拟存储器、页框号、页表（通常称慢表）、页表项、TLB（通常称快表）、LRU 算法、LFU 算法、Cache 一致性、写回法、写穿法。</p>
</blockquote>
<hr />
<h4>4.2 选择题（考研真题）</h4>
<h5>(1) [2010] 下列有关 RAM 和 ROM 的叙述中，正确的是______。</h5>
<blockquote>
<p>Ⅰ. RAM 是易失性存储器，ROM 是非易失性存储器<br />
Ⅱ. RAM 和 ROM 都采用随机存取方式进行信息访问<br />
Ⅲ. RAM 和 ROM 都可用作 Cache<br />
Ⅳ. RAM 和 ROM 都需要进行刷新</p>
<p>A. 仅 Ⅰ 和 Ⅱ<br />
B. 仅 Ⅱ 和 Ⅲ<br />
C. 仅 Ⅰ、Ⅱ 和 Ⅳ<br />
D. 仅 Ⅱ、Ⅲ 和 Ⅳ</p>
</blockquote>
<h5>(2) [2020] 在按字节编址，采用小端方式的 32 位计算机中，为以下 C 语言结构型变量 a 分配存储空间。</h5>
<pre><code>struct record {
    short x1;
    int x2;
} a;
</code></pre>
<p>若 a 的首地址为 <code>2020 FE00H</code>，a 的成员变量 <code>x2</code> 的机器数为 <code>1234 0000H</code>，则其中 <code>34H</code> 所在存储单元的地址是______。</p>
<blockquote>
<p>A. 2020 FE03H<br />
B. 2020 FE04H<br />
C. 2020 FE05H<br />
D. 2020 FE06H</p>
</blockquote>
<h5>(3) [2014] 某容量为 256MB 的存储器若由若干 $4\text{M}\times 8$ 位的 DRAM 芯片构成，该 DRAM 芯片的地址引脚和数据引脚总数是______。</h5>
<blockquote>
<p>A. 19<br />
B. 22<br />
C. 30<br />
D. 36</p>
</blockquote>
<h5>(4) [2009] 某计算机主存容量为 64KB，其中 ROM 区为 4KB，其余为 RAM 区，按字节编址。现要用 $2\text{K}\times 8$ 位的 ROM 芯片和 $4\text{K}\times 4$ 位的 RAM 芯片来设计该存储器，则需要上述规格的 ROM 芯片数和 RAM 芯片数分别是______。</h5>
<blockquote>
<p>A. 1、15<br />
B. 2、15<br />
C. 1、30<br />
D. 2、30</p>
</blockquote>
<h5>(5) [2010] 假定用若干 $2\text{K}\times 4$ 位的芯片组成一个 $8\text{K}\times 8$ 位的存储器，则地址 <code>0B1FH</code> 所在芯片的最小地址是______。</h5>
<blockquote>
<p>A. 0000H<br />
B. 0600H<br />
C. 0700H<br />
D. 0800H</p>
</blockquote>
<h5>(6) [2021] 某计算机的存储器总线中有 24 位地址线和 32 位数据线，按字节编址，字长为 32 位。如果 <code>00 0000H</code> ~ <code>3F FFFFH</code> 为 RAM 区，那么需要 $512\text{K}\times 8$ 位的 RAM 芯片数为______。</h5>
<blockquote>
<p>A. 8<br />
B. 16<br />
C. 32<br />
D. 64</p>
</blockquote>
<h5>(7) [2023] 某计算机的 CPU 有 30 根地址线，按字节编址，CPU 和主存芯片连接时，要求主存芯片占满所有可能存储地址空间，并且 RAM 区和 ROM 区所分配的空间大小比为 3:1。若 RAM 在连续低地址区，ROM 在连续高地址区，则 ROM 的地址范围______。</h5>
<blockquote>
<p>A. 0000 0000H ~ 0FFF FFFFH<br />
B. 1000 0000H ~ 2FFF FFFFH<br />
C. 3000 0000H ~ 3FFF FFFFH<br />
D. 4000 0000H ~ 4FFF FFFFH</p>
</blockquote>
<h5>(8) [2022] 某内存条包含 8 个 $8192\times 8192\times 8$ 位的 DRAM 芯片，按字节编址，支持突发传送方式，对应存储器总线宽度为 64 位，每个 DRAM 芯片内有一个行缓冲区。下列关于该内存条的叙述中，不正确的是______。</h5>
<blockquote>
<p>A. 内存条的容量为 512MB<br />
B. 采用多模块交叉编址方式<br />
C. 芯片的地址引脚为 26 位<br />
D. 芯片内行缓冲有 $8192\times 8$ 位</p>
</blockquote>
<h5>(9) [2018] 假定 DRAM 芯片中存储阵列的行数为 $r$、列数为 $c$，对于一个 $2\text{K}\times 1$ 位的 DRAM 芯片，为保证其地址引脚数最少，并尽量减少刷新开销，则 $r$、$c$ 的取值分别是______。</h5>
<blockquote>
<p>A. 2048、1<br />
B. 64、32<br />
C. 32、64<br />
D. 1、2048</p>
</blockquote>
<h5>(10) [2019] 假定一台计算机采用 3 通道存储器总线，配套的内存条型号为 DDR3-1333，即内存条所接插的存储器总线的工作频率为 1333MHz，总线宽度为 64 位，则存储器总线的总带宽大约是______。</h5>
<blockquote>
<p>A. 10.66GB/s<br />
B. 32GB/s<br />
C. 64GB/s<br />
D. 96GB/s</p>
</blockquote>
<h5>(11) [2015] 某计算机使用 4 体交叉编址存储器，假定在存储器总线上出现的主存地址序列为 8005、8006、8007、8008、8001、8002、8003、8004、8000，则可能发生访存冲突的地址对是______。</h5>
<blockquote>
<p>A. 8004 和 8008<br />
B. 8002 和 8007<br />
C. 8001 和 8008<br />
D. 8000 和 8004</p>
</blockquote>
<h5>(12) [2015] 下列存储器中，在工作期间需要周期性刷新的是______。</h5>
<blockquote>
<p>A. SRAM<br />
B. SDRAM<br />
C. ROM<br />
D. FLASH</p>
</blockquote>
<h5>(13) [2011] 下列各类存储器中，不采用随机存取方式的是______。</h5>
<blockquote>
<p>A. EPROM<br />
B. CDROM<br />
C. DRAM<br />
D. SRAM</p>
</blockquote>
<h5>(14) [2012] 下列关于闪存的叙述中，错误的是______。</h5>
<blockquote>
<p>A. 信息可读可写，并且读、写速度一样快<br />
B. 存储元由 MOS 管组成，是一种半导体存储器<br />
C. 掉电后信息不丢失，是一种非易失性存储器<br />
D. 采用随机访问方式，可替代计算机外部存储器</p>
</blockquote>
<h5>(15) [2017] 某 C 语言程序段如下：</h5>
<pre><code>for (i = 0; i &lt;= 9; i++) {
    temp = 1;
    for (j = 0; j &lt;= i; j++)
        temp *= a[j];
    sum += temp;
}
</code></pre>
<p>下列关于数组 a 的访问局部性的描述中，正确的是______。</p>
<blockquote>
<p>A. 时间局部性和空间局部性皆有<br />
B. 无时间局部性，有空间局部性<br />
C. 有时间局部性，无空间局部性<br />
D. 时间局部性和空间局部性皆无</p>
</blockquote>
<h5>(16) [2009] 某计算机的 Cache 共有 16 块，采用二路组相联映射方式。每个主存块大小为 32B，按字节编址。主存 129 号单元所在主存块应装入的 Cache 组号是______。</h5>
<blockquote>
<p>A. 0<br />
B. 1<br />
C. 4<br />
D. 6</p>
</blockquote>
<h5>(17) [2012] 假设某计算机按字编址，Cache 有 4 行，Cache 与主存之间交换的块大小为 1 个字。若 Cache 的内容初始为空，采用二路组相联映射方式和 LRU 替换策略。访问的主存地址依次为 0、4、8、2、0、6、8、6、4、8 时，命中 Cache 的次数是______。</h5>
<blockquote>
<p>A. 1<br />
B. 2<br />
C. 3<br />
D. 4</p>
</blockquote>
<h5>(18) [2015] 假定主存地址为 32 位，按字节编址，主存和 Cache 之间采用直接相联映射方式，主存块大小为 4 个字，每个字 32 位，采用写回的方式，则能存放 4K 字数据的 Cache 的总容量至少是______位。</h5>
<blockquote>
<p>A. 146K<br />
B. 147K<br />
C. 148K<br />
D. 158K</p>
</blockquote>
<h5>(19) [2021] 若计算机主存地址为 32 位，按字节编址，Cache 数据区大小为 32KB，主存块大小为 32B，采用直接映射方式和回写策略，则 Cache 行的位数至少是______。</h5>
<blockquote>
<p>A. 275<br />
B. 274<br />
C. 258<br />
D. 257</p>
</blockquote>
<h5>(20) [2022] 若计算机主存地址为 32 位，按字节编址，某 Cache 的数据区容量为 32KB，主存块大小为 64B，采用 8 路组相联映射方式，该 Cache 中比较器的个数和位数分别为______。</h5>
<blockquote>
<p>A. 8，20<br />
B. 8，23<br />
C. 64，20<br />
D. 64，23</p>
</blockquote>
<h5>(21) [2014] 采用指令 Cache 与数据 Cache 分离的主要目的是______。</h5>
<blockquote>
<p>A. 降低 Cache 的缺失损失<br />
B. 提高 Cache 的命中率<br />
C. 降低 CPU 平均访存时间<br />
D. 减少指令流水线资源冲突</p>
</blockquote>
<h5>(22) [2024] 下列事件中，不是在 MMU 地址转换过程中检测的是______。</h5>
<blockquote>
<p>A. 访问越权<br />
B. Cache 缺失<br />
C. 页面缺失<br />
D. TLB 缺失</p>
</blockquote>
<h5>(23) [2024] 对于页式虚拟存储管理系统，下列关于存储器层次结构的叙述中，错误的是______。</h5>
<blockquote>
<p>A. Cache-主存层次的交换单位为主存块，主存-外存层次的交换单位为页<br />
B. Cache-主存层次替换算法由硬件实现，主存-外存层次由软件实现<br />
C. Cache-主存层次可采用回写法写策略，主存-外存层次通常采用回写法<br />
D. Cache-主存层次可采用直接映射，主存-外存层次通常采用直接映射</p>
</blockquote>
<h5>(24) [2022] 某计算机主存地址为 24 位，采用分页虚拟存储管理方式，虚拟地址空间大小为 4GB，页大小为 4KB，按字节编址。某进程的页表部分内容如下表所示。</h5>
<table>
<thead>
<tr>
<th>虚页号</th>
<th>实页号</th>
<th>存在位</th>
</tr>
</thead>
<tbody>
<tr>
<td>82</td>
<td>024H</td>
<td>0</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td>129</td>
<td>180H</td>
<td>1</td>
</tr>
<tr>
<td>130</td>
<td>018H</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>当 CPU 访问虚拟地址 <code>0008 2840H</code> 时，虚-实地址转换的结果是______。</p>
<blockquote>
<p>A. 得到主存地址 02 4840H<br />
B. 得到主存地址 18 0840H<br />
C. 得到主存地址 01 8840H<br />
D. 检测到缺页异常</p>
</blockquote>
<h5>(25) [2015] 假定编译器将赋值语句 “x=x+3;” 转换为指令 “add xaddr,3”，其中，xaddr 是 x 对应的存储单元地址。若执行该指令的计算机采用页式虚拟存储管理方式，并配有相应的 TLB，且 Cache 使用写穿的方式，则完成该指令功能需要访问主存的次数至少是______。</h5>
<blockquote>
<p>A. 0<br />
B. 1<br />
C. 2<br />
D. 3</p>
</blockquote>
<h5>(26) [2010] 下列命中组合情况中，一次访存过程中不可能发生的是______。</h5>
<blockquote>
<p>A. TLB 未命中，Cache 未命中，Page 未命中<br />
B. TLB 未命中，Cache 命中，Page 命中<br />
C. TLB 命中，Cache 未命中，Page 命中<br />
D. TLB 命中，Cache 命中，Page 未命中</p>
</blockquote>
<h5>(27) [2013] 某计算机主存地址空间大小为 256MB，按字节编址。虚拟地址空间大小为 4GB，采用页式存储管理方式，页面大小为 4KB，TLB 采用全相联映射，有 4 个页表项，内容如下表所示。</h5>
<table>
<thead>
<tr>
<th>有效位</th>
<th>标记</th>
<th>页框号</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>FF180H</td>
<td>0002H</td>
</tr>
<tr>
<td>1</td>
<td>3FFF1H</td>
<td>0035H</td>
</tr>
<tr>
<td>0</td>
<td>02FF3H</td>
<td>0351H</td>
</tr>
<tr>
<td>1</td>
<td>03FFFH</td>
<td>0153H</td>
</tr>
</tbody>
</table>
<p>则对虚拟地址 <code>03FFF180H</code> 进行虚实地址转换的结果是______。</p>
<blockquote>
<p>A. 0153180H<br />
B. 0035180H<br />
C. TLB 缺失<br />
D. 缺页</p>
</blockquote>
<h5>(28) [2024] 某计算机按字节编址，采用页式虚拟存储管理方式，虚拟地址为 32 位，主存地址为 30 位，页大小为 1KB，若 TLB 有 32 个表项，采用 4 路组相联映射方式，则 TLB 表项中标记字段位数至少是______。</h5>
<blockquote>
<p>A. 17<br />
B. 18<br />
C. 19<br />
D. 20</p>
</blockquote>
<h5>(29) [2019] 下列关于缺页处理的叙述中，错误的是______。</h5>
<blockquote>
<p>A. 缺页是在地址转换时 CPU 检测到的一种异常<br />
B. 缺页处理由操作系统提供的缺页处理程序完成<br />
C. 缺页处理程序根据页故障地址从外存读入所缺失的页<br />
D. 缺页处理完成后执行发生缺页的指令的下一条指令</p>
</blockquote>
<h5>(30) [2020] 下列关于 TLB 和 Cache 的叙述中，错误的是______。</h5>
<blockquote>
<p>A. 命中率都与程序局部性有关<br />
B. 缺失后都需要去访问主存<br />
C. 缺失处理都可以由硬件实现<br />
D. 都由 DRAM 存储器组成</p>
</blockquote>
<hr />
<h4>4.3 简答题</h4>
<p>(1) 计算机系统中采用层次化存储体系结构的目的是什么？<br />
(2) 为什么在存储器芯片中设置片选输入端？<br />
(3) 动态 MOS 存储器为什么要刷新？如何刷新？<br />
(4) 试述多体交叉存储器的设计思想和实现方法。<br />
(5) 为什么说 Cache 对程序员是透明的？<br />
(6) 直接相联映射方式下为什么不需要使用替换算法？<br />
(7) 为什么要考虑 Cache 的一致性？<br />
(8) 替换算法有哪几种？它们各有何优缺点？</p>
<hr />
<h4>4.4</h4>
<p>对于 32KB 容量的存储器，若按 16 位字编址，其地址寄存器应是多少位？数据寄存器是多少位？</p>
<hr />
<h4>4.5</h4>
<p>用 4 个 $32\text{K}\times 8$ 位 SRAM 存储芯片可设计出哪几种不同容量和字长的存储器？画出相应设计图并完成与 CPU 的连接。</p>
<hr />
<h4>4.6</h4>
<p>用 $32\text{K}\times 8$ 位 RAM 芯片和 $64\text{K}\times 4$ 位 ROM 芯片设计 $256\text{K}\times 8$ 位存储器。其中，从 <code>30000H</code> 到 <code>3FFFFH</code> 的地址空间为只读存储区，其他为可读、可写存储区。完成存储器与 CPU 的连接。</p>
<hr />
<h4>4.7</h4>
<p>某计算机字长为 16 位，主存容量为 $128\text{K}\times 16$ 位，请用 $16\text{K}\times 8$ 位的静态 RAM 芯片和 $32\text{K}\times 16$ 位的 ROM 芯片为该机设计一个主存储器。要求 <code>18000H</code> ~ <code>1FFFFH</code> 为 ROM 区，其余为 RAM 区。画出该存储器结构及其与 CPU 连接的框图。</p>
<hr />
<h4>4.8</h4>
<p>设有某动态 RAM 芯片，容量为 $64\text{K}\times 1$ 位，除电源线、接地线和写使能控制线外，该芯片的最小引脚数量是多少？</p>
<hr />
<h4>4.9</h4>
<p>用 $64\text{K}\times 1$ 位的 DRAM 芯片构成 $1\text{M}\times 8$ 位的存储器，若采用异步刷新，每次刷新间隔不超过 2ms，则产生刷新信号的间隔时间是多少？假设读写周期为 $0.5\mu\text{s}$，若采用集中刷新方式，则存储器刷新一遍最少要用多少个读写周期？CPU 的死时间为多少？</p>
<hr />
<h4>4.10</h4>
<p>用 $16\text{K}\times 1$ 位的 DRAM 芯片构成 $64\text{K}\times 8$ 位的存储器，设存储器的读写周期为 $0.5\mu\text{s}$，要使 CPU 在 $1\mu\text{s}$ 内至少访问存储器一次，采用哪种刷新方式比较合适？若每次刷新间隔不超过 2ms，该方式下刷新信号的产生周期是多少？</p>
<hr />
<h4>4.11</h4>
<p>设 Cache 的容量为 $2^{14}$ 块，每块 4 个字节，主存按字节编址，其中有下表所示的数据（地址和数据均采用十六进制表示）。</p>
<table>
<thead>
<tr>
<th>地址</th>
<th>数据</th>
<th>地址</th>
<th>数据</th>
</tr>
</thead>
<tbody>
<tr>
<td>000000</td>
<td>87568536</td>
<td>01FFFC</td>
<td>4FFFFC68</td>
</tr>
<tr>
<td>000008</td>
<td>87792301</td>
<td>FFFFF8</td>
<td>01BF2460</td>
</tr>
<tr>
<td>010004</td>
<td>9ABEFCD0</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>将主存中这些数据装入 Cache 后，Cache 各块中的数据内容及相应的标志是什么？<br />
(1) 全相联映射；<br />
(2) 直接相联映射；<br />
(3) 四路组相联映射。</p>
<hr />
<h4>4.12</h4>
<p>某计算机的 Cache 由 64 个存储块构成，采用四路组相联映射方式，主存包含 4096 个存储块，每块由 128 个字组成，访问地址为字地址。<br />
(1) 主存地址和 Cache 地址各有多少位？<br />
(2) 按照题干条件中的映射方式，列出主存地址的划分情况，并标出各部分的位数。</p>
<hr />
<h4>4.13</h4>
<p>某计算机的主存容量为 4MB，Cache 容量为 16KB，每块包含 8 个字，每字为 32 位，映射方式采用四路组相联。设 Cache 的初始状态为空，CPU 依次从主存第 0, 1, 2, ..., 99 号单元读出 100 个字（每次读一个字），并重复此操作 10 次，替换算法采用 LRU 算法。<br />
(1) 求 Cache 的命中率。<br />
(2) 若 Cache 比主存快 10 倍，分析采用 Cache 后存储访问速度提高了多少。</p>
<hr />
<h4>4.14</h4>
<p>假定某数组元素按行优先顺序存放在主存中，则在以下两段伪代码 A 和 B 中，分析下列问题。<br />
(1) 两段代码中对数组访问的时间局部性和空间局部性。<br />
(2) 变量 sum 的时间局部性和空间局部性。<br />
(3) for 循环体对指令访问的时间局部性和空间局部性。</p>
<p><strong>代码 A：</strong></p>
<pre><code>int sum_array_A(int a[M][N]) {
    int i, j, sum = 0;
    for (i = 0; i &lt; M; i++)
        for (j = 0; j &lt; N; j++)
            sum += a[i][j];
    return sum;
}
</code></pre>
<p><strong>代码 B：</strong></p>
<pre><code>int sum_array_B(int a[M][N]) {
    int i, j, sum = 0;
    for (i = 0; i &lt; N; i++)
        for (j = 0; j &lt; M; j++)
            sum += a[j][i];
    return sum;
}
</code></pre>
<hr />
<h4>4.15</h4>
<p>假定主存地址为 32 位，按字节编址，指令 Cache 和数据 Cache 与主存之间均采用 8 路组相联映射、写穿策略和 LRU 替换算法，主存块大小为 64B，数据区容量各为 32KB。开始时 Cache 均为空。请回答下列问题。<br />
(1) Cache 每一行中标记、LRU 位各占几位？是否有修改位？<br />
(2) 有如下 C 语言程序段：</p>
<pre><code>for (k = 0; k &lt; 1024; k++)
    s[k] = 2 * s[k];
</code></pre>
<p>若数组 s 及其变量 k 均为 int 型，int 型数据占 4B，变量 k 分配在寄存器中，数组 s 在主存中的起始地址为 <code>0080 00C0H</code>，则该程序段执行过程中，访问数组 s 的数据 Cache 缺失次数为多少？<br />
(3) 若 CPU 最开始的访问操作是读取主存单元 <code>0001 0003H</code> 中的指令，简要说明从 Cache 中访问该指令的过程，包括 Cache 缺失处理过程。</p>
<hr />
<h4>4.16</h4>
<p>已知计算机 M 字长为 32 位，按字节编址，采用请求调页策略的虚拟存储管理方式，虚拟地址 32 位，页面大小为 4KB；数据 Cache 采用 4 路组相联映射方式，数据区大小为 8KB，主存块大小为 32B。现有 C 语言程序段如下：</p>
<pre><code>int a[24][64];
for (i = 0; i &lt; 24; i++)
    for (j = 0; j &lt; 64; j++) 
        a[i][j] = 10;
</code></pre>
<p>已知二维数组 a 按行优先存放，在虚拟地址空间中分配的起始地址为 <code>0042 2000H</code>，<code>sizeof(int)=4</code>。假定在 M 上执行上述程序段之前数组 a 不在主存，且在该程序执行过程中不会发生页面置换。请回答下列问题：<br />
(1) 数组分布在几个页面中？对于数组 a 的访问，会发生几次缺页异常？页故障地址各是什么？<br />
(2) 不考虑变量 i 和 j，该程序段的数据访问是否具有时间局部性？为什么？<br />
(3) 计算机 M 的虚拟地址 ($A_{31} \sim A_0$) 中哪几位用作块内地址？哪几位用作 Cache 组号？<code>a[1][0]</code> 的虚拟地址是多少？其所在主存块对应的 Cache 组号是多少？<br />
(4) 数组 a 占用多少主存块？假设上述程序段执行过程中数组 a 的访问不会和其他数据发生 Cache 访问冲突，则数组 a 的 Cache 命中率是多少？若将循环中 i 和 j 的次序按如下方式调换：</p>
<pre><code>for (j = 0; j &lt; 64; j++)
    for (i = 0; i &lt; 24; i++) 
        a[i][j] = 10;
</code></pre>
<p>则数组 a 的 Cache 命中率又是多少？</p>
<hr />
<h4>4.17</h4>
<p>主存容量为 8MB，虚存容量为 2GB，分页管理时若页面大小为 4KB，求出对应的 VPN、VPO、PPN、PPO 的位数。</p>
<hr />
<h4>4.18</h4>
<p>某页式虚拟存储器共 8 页，每页为 1KB，主存容量为 4KB，页表如下表所示。</p>
<table>
<thead>
<tr>
<th>虚页号</th>
<th>实页号</th>
<th>装入位</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>5</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>6</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>7</td>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>(1) 失效的页有哪几页？<br />
(2) 虚地址 0、3028、1023、2048、4096、8000 的实地址分别是多少？</p>
<hr />
<h4>4.19</h4>
<p>某计算机系统中的一个 TLB 和 L1 级数据 Cache，存储系统按字节编址，虚拟存储容量为 2GB，主存容量为 4MB，页大小为 128KB，TLB 采用四路组相联方式，共有 16 个页表项。Cache 容量为 16KB，每块包含 8 个字，每字为 32 位，映射方式采用四路组相联，回答下列问题。<br />
(1) 虚拟地址中哪几位表示虚拟页号？哪几位表示页内地址？虚拟页号中哪几位表示 TLB 标记？哪几位表示 TLB 索引？<br />
(2) 物理地址中哪几位表示物理页号？哪几位表示偏移地址？<br />
(3) 为实现主存与数据 Cache 之间的组相联映射，对该地址应进行怎样的划分？</p>
<hr />
<h4>4.20</h4>
<p>假设计算机 M 的主存地址为 24 位，按字节编址；采用分页存储管理方式，虚拟地址为 30 位，页大小为 4KB；TLB 采用 2 路组相联方式和 LRU 替换策略，共 8 组。请回答下列问题。<br />
(1) 虚拟地址中哪几位表示虚页号？哪几位表示页内地址？<br />
(2) 已知访问 TLB 时虚页号高位部分用作 TLB 标记，低位部分用作 TLB 组号，M 的虚拟地址中哪几位是 TLB 标记？哪几位是 TLB 组号？<br />
(3) 假设 TLB 初始时为空，访问的虚页号依次为 10、12、16、7、26、4、12、20，在此过程中，哪一个虚页号对应的 TLB 表项被替换？说明理由。<br />
(4) 若将 M 中的虚拟地址位数增加到 32 位，则 TLB 表项的位数增加几位？</p>
<hr />
<h4>4.21</h4>
<p>某计算机采用页式虚拟存储管理方式，按字节编址，虚拟地址为 32 位，物理地址为 24 位，页大小为 8KB；TLB 采用全相联映射；Cache 数据区大小为 64KB，按 2 路组相联方式组织，主存块大小为 64B。</p>
<p>(1) 物理地址各个字段的位数各是多少？TLB 标记中存放的是什么信息？<br />
(2) 将块号为 4099 的主存块装入 Cache 中时，映射的 Cache 组号是多少？对应标记的内容是什么？<br />
(3) Cache 缺失处理的时间开销大还是缺页处理的时间开销大？为什么？<br />
(4) 为什么 Cache 可以采用写穿策略，而修改页面内容时总是采用写回策略？</p>
<hr />
<h4>4.22</h4>
<p>某计算机采用页式虚拟存储管理方式，按字节编址。CPU 进行页式虚拟存储器访问。回答下列问题。<br />
(1) 主存的物理地址占多少位？<br />
(2) TLB 采用什么映射方式？TLB 是用 SRAM 还是用 DRAM 实现？<br />
(3) Cache 采用什么映射方式？若 Cache 采用 LRU 替换算法和写回策略，则 Cache 每行中除数据、tag 和有效位外，还应有哪些附加位？Cache 总容量是多少？Cache 中有效位的作用是什么？<br />
(4) 若 CPU 给出的虚拟地址为 <code>0008 C040H</code>，则对应的物理地址是多少？是否在 Cache 中命中？说明理由。若 CPU 给出的虚拟地址为 <code>0007 C260H</code>，则该地址所在主存块映射到的 Cache 组号是多少？</p>
<hr />
<h3>4.1 名词解释详细解答</h3>
<p>(1) <strong>主存</strong>：用于存放 CPU 正在运行的程序和数据的系统存储器，可由 CPU 通过系统总线直接进行随机寻址，物理上主要由半导体 DRAM 芯片组成，属于内部存储器。<br />
(2) <strong>辅存</strong>：用于存放暂不运行的程序和海量数据，CPU 不能直接访问它，必须通过 I/O 系统调入主存后才能运行。典型设备如机械硬盘、固态硬盘，属于外部存储器。<br />
(3) <strong>存取时间</strong>：是指从存储器接收到合法的读/写控制命令及地址开始，到数据线上的数据物理上稳定输出（或写入完毕）所需的时间间隔。<br />
(4) <strong>存取周期</strong>：是指存储器进行连续两次独立的读或写操作之间所需的最小物理时间间隔。存取周期通常大于存取时间。<br />
(5) <strong>存储器带宽</strong>：单位时间内存储器能够与外部（如 CPU）传输的最大数据量，单位为字节/秒（B/s）或位/秒（bps）。<br />
(6) <strong>存储元</strong>：用于存储一位二进制数据 0 或 1 的最基础物理电路，是存储器内部最小的物理存储载体。<br />
(7) <strong>存储单元</strong>：由若干个存储元组成，是存储器可以按照物理地址进行寻址的最小存储组合单位。<br />
(8) <strong>存储字</strong>：存放在一个存储单元中的二进制代码组合。<br />
(9) <strong>存储字长</strong>：一个存储单元中包含的二进制代码的位数（即存储字的长度）。<br />
(10) <strong>大端存储</strong>：将多字节数据的低有效字节存放在高物理地址单元中，而将高有效字节存放在低物理地址单元中的字节存储顺序。<br />
(11) <strong>小端存储</strong>：将多字节数据的低有效字节存放在低物理地址单元中，而将高有效字节存放在高物理地址单元中的字节存储顺序。<br />
(12) <strong>边界对齐</strong>：为了提升 CPU 访存效率，多字节数据（如 2 字节短整型、4 字节整型）存放在其数据大小的整数倍地址处的内存编址方式。<br />
(13) <strong>SRAM</strong>：静态随机存取存储器，依靠双稳态触发器保存二进制状态的半导体随机存储器。存取速度极快，但集成度低、功耗大、成本高，常用作 Cache。<br />
(14) <strong>DRAM</strong>：动态随机存取存储器，利用微小电容上的电荷状态保存二进制数据的随机存储器。集成度极高、功耗低、容量大、成本低，但电容会缓慢泄露电荷，因而需要定期进行刷新，常用作系统主存储器。<br />
(15) <strong>重写</strong>：对于具有破坏性读出特征的存储元，在读取操作后，为了恢复其原有的物理状态，由内部控制电路自动重新写入原数据的物理过程。<br />
(16) <strong>刷新</strong>：为了防止 DRAM 存储元电容电荷泄露导致数据丢失，由外部定时刷新电路定期对 DRAM 芯片的每一行进行读取并重新写入的过程。<br />
(17) <strong>刷新周期</strong>：DRAM 芯片中全部存储元必须完成一次刷新操作的最大允许时间间隔，通常为 2ms。<br />
(18) <strong>位扩展</strong>：通过并联多个物理芯片的数据引脚，以增加存储系统的字长（位宽）的扩展设计方法。<br />
(19) <strong>字扩展</strong>：通过使用片选信号（译码驱动）增加芯片数量，以增加存储系统总存储单元个数的扩展设计方法。<br />
(20) <strong>字位同时扩展</strong>：在字数和位数都达不到系统目标容量时，同时进行字方向和位方向的拼接扩展方法。<br />
(21) <strong>多体交叉存储器</strong>：由多个物理上独立、具有相同容量和字长的存储体组成的存储器系统。通过对地址的低位进行交叉编址，使 CPU 能够以流水线重叠或并行的方式访问不同的物理存储体，从而提升主存带宽。<br />
(22) <strong>双端口存储器</strong>：具有两套完全独立的读写控制线、地址线和数据缓冲，允许两个独立的 CPU 或控制器在同一时刻并发访问该存储器的多通道存储组件。<br />
(23) <strong>相联存储器</strong>：不是依靠物理地址，而是根据待检索数据的部分内容（关键字标记）进行全表项并行比较和读写的特殊存储器，常用于 TLB 和 Cache 标记字段匹配。<br />
(24) <strong>Cache</strong>：高速缓冲存储器，设置在 CPU 和主存之间、用快速的 SRAM 实现的高速、小容量存储器。它利用局部性原理暂存 CPU 频繁访问的数据，以缓解 CPU 速度与主存慢速之间的瓶颈。<br />
(25) <strong>时间局部性</strong>：如果某个存储单元被访问，在不久的将来它极有可能再次被访问。<br />
(26) <strong>空间局部性</strong>：如果某个存储单元被访问，在不久的将来其邻近的物理存储单元极有可能被访问。<br />
(27) <strong>地址映射</strong>：将主存的物理地址转换为 Cache 中的地址（确定主存块与 Cache 行之间对应关系）的转换过程。<br />
(28) <strong>直接相联映射</strong>：主存中的每一个块只能映射到 Cache 的一个唯一、固定的行中的单射映射方式。<br />
(29) <strong>全相联映射</strong>：主存中的任意一块可以映射到 Cache 中任意一行的自由映射方式。<br />
(30) <strong>组相联映射</strong>：将 Cache 划分为若干组，主存块通过直接相联映射定位到 Cache 的固定组中，在组内则可以采用全相联映射放入任意一行。<br />
(31) <strong>命中率</strong>：在存储体系中，CPU 访问的数据成功存在于 Cache 中的次数占总访存次数的比例。<br />
(32) <strong>虚拟存储器</strong>：为了给程序员提供连续且极大的逻辑地址空间，由系统软件和硬件共同管理，将主存与外存（磁盘）统一编址的存储体系。<br />
(33) <strong>页表</strong>：存放在主存中的特殊映射表，用于记录虚拟页号到物理页框号的转换关系。<br />
(34) <strong>TLB</strong>：快表，常集成在 CPU 内部，是页表项的快速缓冲，用于加速虚拟地址到物理地址的转换过程。</p>
<hr />
<h3>4.2 选择题答案与详细解析</h3>
<p><strong>(1) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：由于 Ⅰ 和 Ⅱ 正确，Ⅲ 和 Ⅳ 错误，因此仅 Ⅰ 和 Ⅱ 正确。具体分析为：Ⅰ 正确，RAM 为易失性存储器，ROM 为非易失性存储器；Ⅱ 正确，SRAM 和 DRAM、ROM 均采用随机存取方式进行信息访问，访问任何一个存储单元的时间都相同且与物理位置无关。</li>
<li><strong>B [错误]</strong>：由于 Ⅲ 错误，该选项错误。Ⅲ 错误原因为：ROM 无法用作高速缓存，因为高速缓存必须支持 CPU 的高速写入，而 ROM 在正常工作时是只读的。</li>
<li><strong>C [错误]</strong>：由于 Ⅳ 错误，该选项错误。Ⅳ 错误原因为：ROM 与 SRAM 均不需要刷新，只有 DRAM 因为电容漏电才需要进行周期性刷新。</li>
<li><strong>D [错误]</strong>：由于 Ⅲ 和 Ⅳ 错误，该选项错误。</li>
</ul>
<p><strong>(2) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>2020 FE03H</code> 为结构体中的填充字节（为了满足 <code>int</code> 成员的 4 字节边界对齐，在 <code>x1</code> 后的 <code>2020 FE02H</code> 和 <code>2020 FE03H</code> 进行填充）。</li>
<li><strong>B [错误]</strong>：<code>2020 FE04H</code> 存放的是成员变量 <code>x2</code> 的最低有效字节 <code>00H</code>。</li>
<li><strong>C [错误]</strong>：<code>2020 FE05H</code> 存放的是成员变量 <code>x2</code> 的次低有效字节 <code>00H</code>。</li>
<li><strong>D [正确]</strong>：由于边界对齐要求，4 字节的成员 <code>x2</code> 必须从 4 的整数倍地址开始存放，因此其起始地址为 <code>2020 FE04H</code>。在小端方式下，高位字节存放在高地址，低位字节存放在低地址。<code>x2</code> 机器数 <code>12340000H</code> 的字节从低到高依次为 <code>00H</code>、<code>00H</code>、<code>34H</code>、<code>12H</code>，分别存放在 <code>2020 FE04H</code> 至 <code>2020 FE07H</code>。因此，<code>34H</code> 存放在 <code>2020 FE06H</code> 中。</li>
</ul>
<p><strong>(3) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：DRAM 芯片规格为 $4\text{M} \times 8$ 位，字容量为 $4\text{M} = 2^{22}$，数据位宽为 8 位。因为 DRAM 芯片采用地址复用技术，其行、列地址线分时复用，故地址引脚数为 $\frac{22}{2} = 11$ 根。数据引脚数等于数据位宽，即 8 根。不考虑控制引脚及电源、接地线，地址与数据引脚总数为 $11 + 8 = 19$ 根。</li>
<li><strong>B [错误]</strong>：此选项未考虑 DRAM 的地址复用技术，直接将 22 根地址线与数据引脚相加或有其他计算错误。</li>
<li><strong>C [错误]</strong>：此项计算错误，误将总存储容量 $256\text{MB}$ 作为芯片引脚计算基础。</li>
<li><strong>D [错误]</strong>：此项计算错误，未考虑地址复用技术且计算有偏差。</li>
</ul>
<p><strong>(4) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：ROM 芯片数与 RAM 芯片数计算均错误。</li>
<li><strong>B [错误]</strong>：RAM 芯片数计算错误，仅计算了字扩展需求，忽略了位扩展需求。</li>
<li><strong>C [错误]</strong>：ROM 芯片数计算错误，仅用 1 片只能得到 $2\text{K} \times 8$ 位的容量，无法达到 $4\text{KB}$ 的 ROM 区容量。</li>
<li><strong>D [正确]</strong>：
<ul>
<li>ROM 区容量为 $4\text{KB} = 4\text{K} \times 8$ 位，使用 $2\text{K} \times 8$ 位的 ROM 芯片进行字扩展，需要芯片数 $\frac{4\text{K} \times 8}{2\text{K} \times 8} = 2$ 片。</li>
<li>RAM 区容量为主存总容量减去 ROM 区容量，即 $64\text{KB} - 4\text{KB} = 60\text{KB} = 60\text{K} \times 8$ 位。使用 $4\text{K} \times 4$ 位的 RAM 芯片进行字位联合扩展，字扩展倍数为 $\frac{60\text{K}}{4\text{K}} = 15$，位扩展倍数为 $\frac{8}{4} = 2$，所以需要 RAM 芯片总数为 $15 \times 2 = 30$ 片。</li>
</ul>
</li>
</ul>
<p><strong>(5) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>0000H</code> 是第一组芯片（地址范围 <code>0000H</code> ~ <code>07FFH</code>）的最小地址，而地址 <code>0B1FH</code> 超出了该范围。</li>
<li><strong>B [错误]</strong>：<code>0600H</code> 并非以 $2\text{KB}$ 为组边界的芯片起始地址，芯片组的起始地址必须是 $2\text{KB}$（即 <code>0800H</code>）的整数倍。</li>
<li><strong>C [错误]</strong>：<code>0700H</code> 不是按 $2\text{KB}$ 划分芯片组的边界起始地址。</li>
<li><strong>D [正确]</strong>：使用 $2\text{K} \times 4$ 位芯片组成 $8\text{K} \times 8$ 位存储器，需要将每 2 片并联进行位扩展，组成 $2\text{K} \times 8$ 位的芯片组（容量为 $2\text{KB}$）。整个存储器共有 $\frac{8\text{KB}}{2\text{KB}} = 4$ 组芯片，各组的地址分配如下：组 0 为 <code>0000H</code> ~ <code>07FFH</code>；组 1 为 <code>0800H</code> ~ <code>0FFFH</code>；组 2 为 <code>1000H</code> ~ <code>17FFH</code>；组 3 为 <code>1800H</code> ~ <code>1FFFH</code>。地址 <code>0B1FH</code> 落入组 1（即第二组芯片）的地址范围内，因此该芯片组的最小起始地址为 <code>0800H</code>。</li>
</ul>
<p><strong>(6) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：地址空间为 <code>00 0000H</code> ~ <code>3F FFFFH</code>，共 $2^{22}$ 个地址单元，因为按字节编址，所以容量为 $4\text{MB}$。数据线为 32 位，意味着存储器的数据总线位宽为 32 位。为了与 32 位数据线连接，必须使用 4 片 $512\text{K} \times 8$ 位的芯片进行位扩展，组成一个 $512\text{K} \times 32$ 位（即 $2\text{MB}$）的芯片组。然后再进行字扩展，需要 $\frac{4\text{MB}}{2\text{MB}} = 2$ 组这样的芯片组。因此，总芯片数为 $4 \times 2 = 8$ 片。</li>
<li><strong>B [错误]</strong>：此选项计算出的芯片数为 16，可能是字扩展或位扩展倍数计算错误。</li>
<li><strong>C [错误]</strong>：此选项计算出的芯片数为 32，可能是误将 32 位数据线与其它参数混淆计算。</li>
<li><strong>D [错误]</strong>：此选项计算出的芯片数为 64，计算过程有误。</li>
</ul>
<p><strong>(7) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：此范围是前 $256\text{MB}$ 的地址区间，若 RAM 在连续低地址区，这应属于 RAM 区的一部分。</li>
<li><strong>B [错误]</strong>：此区间为 $256\text{MB}$ 到 $768\text{MB}$ 之间的地址区间，属于 RAM 区的一部分。</li>
<li><strong>C [正确]</strong>：CPU 有 30 根地址线，按字节编址，最大可寻址空间为 $2^{30}\text{ B} = 1\text{GB}$，对应的十六进制地址范围为 <code>0000 0000H</code> ~ <code>3FFF FFFFH</code>。RAM 区和 ROM 区容量之比为 3:1，则 RAM 区占 $\frac{3}{4} \times 1\text{GB} = 768\text{MB}$，ROM 区占 $\frac{1}{4} \times 1\text{GB} = 256\text{MB}$。由于 RAM 在连续低地址区，ROM 在连续高地址区，ROM 的起始地址为整个空间的 $\frac{3}{4}$ 处，即 $768\text{MB}$ 边界点（对应十六进制地址为 <code>3000 0000H</code>）。因此，ROM 的地址范围为 <code>3000 0000H</code> ~ <code>3FFF FFFFH</code>。</li>
<li><strong>D [错误]</strong>：<code>4000 0000H</code> 超出了 30 根地址线所能表示的最大范围。</li>
</ul>
<p><strong>(8) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：单个芯片容量为 $8192 \times 8192 \times 8$ 位 = $64\text{MB}$，8 个芯片组成的内存条总容量为 $8 \times 64\text{MB} = 512\text{MB}$。因此该叙述是正确的。</li>
<li><strong>B [错误]</strong>：存储器总线宽度为 64 位，每个芯片的数据线宽度只有 8 位，需要 8 个芯片采用多模块位扩展/交叉编址方式并行工作。因此该叙述是正确的。</li>
<li><strong>C [正确]</strong>：该芯片容量为 $64\text{MB}$，行、列数各为 $8192 = 2^{13}$。DRAM 芯片通常采用地址复用技术，行地址与列地址分时复用同一组地址引脚，所以仅需要 13 根地址引脚即可，而非 26 根。因此该叙述是错误的，符合题意。</li>
<li><strong>D [错误]</strong>：DRAM 芯片每次读出一整行，行缓冲区的大小与列数和芯片位宽对应，即为 $8192 \times 8$ 位。因此该叙述是正确的。</li>
</ul>
<p><strong>(9) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：此选项行数 $r=2048 = 2^{11}$，列数 $c=1$。复用后地址引脚数为 $\max(11, 0) = 11$ 根，引脚数不是最少，且行数过多导致刷新开销极大。</li>
<li><strong>B [错误]</strong>：此选项虽然复用后地址引脚数也是 $\max(6, 5) = 6$ 根，但行数为 64。DRAM 刷新是以行为单位进行的，行数越多刷新次数越多，开销越大。因此在引脚数最少（均为 6 根）的情况下，应该选择行数更少的方案。</li>
<li><strong>C [正确]</strong>：芯片容量为 $2\text{K} \times 1$ 位，总存储单元数为 $2048$。行数 $r$ 与列数 $c$ 的乘积需满足 $r \times c = 2048$。由于 DRAM 采用地址复用技术，所需的地址引脚数为 $\max(\log_2 r, \log_2 c)$。为使引脚数最少，$r$ 和 $c$ 应尽可能接近。又因为 DRAM 刷新以“行”为单位，为了尽量减少刷新次数以降低刷新开销，行数 $r$ 应尽可能小。当 $\max(\log_2 r, \log_2 c)$ 取得最小值 $6$ 时，满足条件的组合有 $32 \times 64$ 和 $64 \times 32$。选择行数较小的一方，即 $r=32$（5 位地址线），$c=64$（6 位地址线），此时地址引脚数仅为 $\max(5, 6) = 6$ 根，且行数最少，刷新开销最小。</li>
<li><strong>D [错误]</strong>：此选项行数 $r=1$，列数 $c=2048$。复用后的地址引脚数为 $\max(0, 11) = 11$ 根，引脚数不是最少。</li>
</ul>
<p><strong>(10) 答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>10.66GB/s</code> 只是单通道内存总线的带宽，忽略了系统采用的 3 通道配置。</li>
<li><strong>B [正确]</strong>：工作频率为 $1333\text{MHz}$，总线宽度为 64 位（即 $8\text{ B}$）。单通道的带宽为 $1333\text{M} \times 8\text{ B/s} \approx 10.66\text{ GB/s}$。由于系统采用 3 通道内存总线，因此存储器总线的总带宽为 $10.66\text{ GB/s} \times 3 \approx 32\text{ GB/s}$。</li>
<li><strong>C [错误]</strong>：此项计算结果为 $64\text{ GB/s}$，数值与 3 通道实际带宽不符。</li>
<li><strong>D [错误]</strong>：此项计算结果为 $96\text{ GB/s}$，数值计算有误。</li>
</ul>
<p><strong>(11) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：计算体号：$8004 \bmod 4 = 0$，$8008 \bmod 4 = 0$。虽然它们都映射到 0 号模块，但在访问序列中它们之间相隔了 4 次访问。由于交叉存储器的周期为 4，此时 0 号模块已被释放，因此不会发生冲突。</li>
<li><strong>B [错误]</strong>：$8002 \bmod 4 = 2$，$8007 \bmod 4 = 3$，映射到不同的模块（体 2 和体 3），不会发生冲突。</li>
<li><strong>C [错误]</strong>：$8001 \bmod 4 = 1$，$8008 \bmod 4 = 0$，映射到不同的模块（体 1 和体 0），不会发生冲突。</li>
<li><strong>D [正确]</strong>：在 4 体低位交叉存储器中，地址 $A$ 对应的存储体模块号为 $A \bmod 4$。计算访问序列中各地址的体号：$8005 \to 1$，$8006 \to 2$，$8007 \to 3$，$8008 \to 0$，$8001 \to 1$，$8002 \to 2$，$8003 \to 3$，$8004 \to 0$，$8000 \to 0$。可以看到，地址 $8004$ 和 $8000$ 均映射到 0 号模块，且在访问序列中紧挨着连续被访问。因为同一个物理存储体在完成一次读写周期前无法立即接受下一次访问，因此会发生访存冲突。</li>
</ul>
<p><strong>(12) 答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：SRAM使用双稳态触发器保存数据，只要电源通电，数据就会一直保持，不需要刷新。</li>
<li><strong>B [正确]</strong>：SDRAM是一种动态随机存储器，其内部依靠电容存储电荷来保存数据。由于电容漏电，电荷会逐渐流失，必须在工作期间对其进行周期性的刷新以维持信息不丢失。</li>
<li><strong>C [错误]</strong>：ROM属于非易失性存储器，断电后数据不丢失，正常工作期间不需要刷新。</li>
<li><strong>D [错误]</strong>：FLASH也是一种非易失性存储器，无需刷新即可长期保留数据。</li>
</ul>
<p><strong>(13) 答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：EPROM是半导体只读存储器，采用随机存取方式，访问任何存储单元的时间都是相同的。</li>
<li><strong>B [正确]</strong>：CD-ROM属于光盘存储器，它是通过激光束扫描螺旋形光道来寻找数据，寻道时间与当前的物理位置有关，因此不属于随机存取存储器。</li>
<li><strong>C [错误]</strong>：DRAM属于半导体随机存储器，采用随机存取方式。</li>
<li><strong>D [错误]</strong>：SRAM属于半导体随机存储器，采用随机存取方式。</li>
</ul>
<p><strong>(14) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：闪存可以读写，但由于其特殊的物理结构，在写入数据前必须先对所在的扇区或块进行擦除操作，导致其写入速度明显慢于读取速度。因此“读、写速度一样快”的叙述错误，符合题意。</li>
<li><strong>B [错误]</strong>：闪存的存储元主要由浮栅 MOS 管组成，确实属于半导体存储器。该叙述正确。</li>
<li><strong>C [错误]</strong>：闪存属于非易失性存储器，掉电后信息不丢失。该叙述正确。</li>
<li><strong>D [错误]</strong>：闪存支持随机读取，常用来制作固态硬盘等外部存储器。该叙述正确。</li>
</ul>
<p><strong>(15) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：在程序段中，外层循环 $i$ 从 0 到 9，内层循环总是从 $a[0]$ 顺序访问到 $a[i]$。
<ul>
<li><strong>时间局部性</strong>：同一数组元素（例如 $a[0]$）会在外层循环的多个连续轮次中被反复访问，因此具有时间局部性。</li>
<li><strong>空间局部性</strong>：内层循环中是连续顺序访问数组相邻单元，因此具有空间局部性。</li>
</ul>
</li>
<li><strong>B [错误]</strong>：忽略了数组同一元素在后续循环中会被再次读取的事实，错误地认为无时间局部性。</li>
<li><strong>C [错误]</strong>：忽略了内层循环连续访问相邻地址单元的特征，错误地认为无空间局部性。</li>
<li><strong>D [错误]</strong>：错误地排除了时间局部性和空间局部性。</li>
</ul>
<p><strong>(16) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：计算错误，混淆了块号或组号的计算规则。</li>
<li><strong>B [错误]</strong>：此组号不正确，未正确计算块号 $4$ 对组数 $8$ 取模的结果。</li>
<li><strong>C [正确]</strong>：按字节编址，每个主存块大小为 $32\text{B}$。主存 $129$ 号单元所在的主存物理块号为 $\lfloor 129 / 32 \rfloor = 4$。由于 Cache 共有 16 块且采用二路组相联映射，因此 Cache 的组数为 $\frac{16}{2} = 8$ 组。根据组相联映射的规则，主存块号为 4 的块映射到 Cache 的组号为 $4 \bmod 8 = 4$。</li>
<li><strong>D [错误]</strong>：计算出的组号不正确。</li>
</ul>
<p><strong>(17) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：Cache 有 4 行，采用二路组相联映射，故组数为 $2$ 组。由于访问的所有主存地址均为偶数，它们对组数 2 取模的结果均为 0，所以全部映射到 Cache 组 0。
具体模拟过程（假设数组左端为最久未用，右端为最新访问）：
<ol>
<li>访问 0：组 0 状态为 <code>[0]</code>，未命中。</li>
<li>访问 4：组 0 状态为 <code>[0, 4]</code>，未命中。</li>
<li>访问 8：替换 0，状态为 <code>[4, 8]</code>，未命中。</li>
<li>访问 2：替换 4，状态为 <code>[8, 2]</code>，未命中。</li>
<li>访问 0：替换 8，状态为 <code>[2, 0]</code>，未命中。</li>
<li>访问 6：替换 2，状态为 <code>[0, 6]</code>，未命中。</li>
<li>访问 8：替换 0，状态为 <code>[6, 8]</code>，未命中。</li>
<li>访问 6：<strong>命中</strong>，状态更新为 <code>[8, 6]</code>，命中 1 次。</li>
<li>访问 4：替换 8，状态为 <code>[6, 4]</code>，未命中。</li>
<li>访问 8：替换 6，状态为 <code>[4, 8]</code>，未命中。
综上，只有第 8 次访问（地址 6）命中，总命中次数为 1。</li>
</ol>
</li>
<li><strong>B [错误]</strong>：此项计算结果为 2 次，原因为对 LRU 替换状态更新逻辑或组号分配有误。</li>
<li><strong>C [错误]</strong>：此项计算结果为 3 次，存在计算失误。</li>
<li><strong>D [错误]</strong>：此项计算结果为 4 次，计算有误。</li>
</ul>
<p><strong>(18) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：可能在计算时漏掉了修改位（脏位）或有效位，或者块内偏移位数计算有误。</li>
<li><strong>B [错误]</strong>：可能漏掉了修改位，仅计算了有效位和标记位。</li>
<li><strong>C [正确]</strong>：
<ul>
<li>Cache 数据容量为 $4\text{K}$ 字，每个主存块为 4 个字，故 Cache 的总行数为 $\frac{4\text{K}}{4} = 1024$ 行。</li>
<li>主存按字节编址，块大小为 $4 \times 32\text{位} = 128\text{位} = 16\text{B}$，因此块内地址占 $\log_2 16 = 4$ 位。</li>
<li>采用直接映射，Cache 的行索引占用 $\log_2 1024 = 10$ 位。</li>
<li>标记（Tag）的位数 = 主存地址位数 - 索引位数 - 偏移位数 = $32 - 10 - 4 = 18$ 位。</li>
<li>采用写回策略，每个 Cache 行除了数据区和标记字段外，还需包含 1 位有效位和 1 位修改位（脏位），故每行额外的控制信息和标记项总位数为 $18 + 1 + 1 = 20$ 位。</li>
<li>每一个 Cache 行的总位数为：数据区 $128$ 位 + 标记控制项 $20$ 位 = $148$ 位。</li>
<li>Cache 总容量 = $1024$ 行 $\times 148$ 位 = $148\text{K}$ 位。</li>
</ul>
</li>
<li><strong>D [错误]</strong>：计算错误，超出了合理计算范围。</li>
</ul>
<p><strong>(19) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：
<ul>
<li>Cache 行数 = $\frac{32\text{KB}}{32\text{B}} = 1024 = 2^{10}$ 行。</li>
<li>块内偏移位数占 $\log_2 32 = 5$ 位。</li>
<li>直接映射的行索引占用 $\log_2 1024 = 10$ 位。</li>
<li>标记（Tag）的位数 = $32 - 10 - 5 = 17$ 位。</li>
<li>回写策略下，每一行应包含：数据区 $32 \times 8 = 256$ 位，标记字段 $17$ 位，有效位 1 位，脏位 1 位。</li>
<li>Cache 行的总位数 = $256 + 17 + 1 + 1 = 275$ 位。</li>
</ul>
</li>
<li><strong>B [错误]</strong>：可能漏掉了脏位或有效位中的一位。</li>
<li><strong>C [错误]</strong>：仅计算了数据位与标记位数再加上少许控制位，不符合完整行结构。</li>
<li><strong>D [错误]</strong>：计算出的位数过小，遗漏了某些必要控制字段。</li>
</ul>
<p><strong>(20) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：
<ul>
<li>Cache 总行数 = $\frac{32\text{KB}}{64\text{B}} = 512$ 行。采用 8 路组相联映射，即每组包含 8 个 Cache 行，组数为 $\frac{512}{8} = 64$ 组。</li>
<li>当 CPU 访存时，首先根据主存地址中的组索引字段定位到具体的某一组，然后需要将主存地址的标记与该组内的所有 8 行标记进行并行比较。因此，所需的比较器个数等于每组的行数，即 8 个。</li>
<li>主存地址共 32 位。块大小为 $64\text{B}$，故块内偏移地址为 $\log_2 64 = 6$ 位；组数为 64，故组索引为 $\log_2 64 = 6$ 位。</li>
<li>标记的位数 = $32 - 6\text{（组索引）} - 6\text{（块内偏移）} = 20$ 位。比较器要比较的就是标记，因此比较器的位数等于标记的位数，即 20 位。</li>
</ul>
</li>
<li><strong>B [错误]</strong>：比较器的位数计算错误，未正确扣除组索引和块内偏移位数。</li>
<li><strong>C [错误]</strong>：错误地以组数（64）作为比较器个数，比较器应与组内的路数（8）相匹配。</li>
<li><strong>D [错误]</strong>：比较器个数和位数计算均有误。</li>
</ul>
<p><strong>(21) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：缺失损失由主存的访问速度和 Cache 块的大小决定，与指令和数据 Cache 是否分离无关。</li>
<li><strong>B [错误]</strong>：分离 Cache 虽然能避免指令与数据之间的相互替换，但由于各自容量变小，并不一定会提高命中率。</li>
<li><strong>C [错误]</strong>：降低平均访存时间是最终带来的效果，但首要设计目的是为了解决流水线中的结构冲突。</li>
<li><strong>D [正确]</strong>：在具有流水线设计的 CPU 中，流水线的取指阶段需要从存储器读取指令，而访存阶段需要从存储器读写数据。如果将指令和数据存放在同一个统一的高速缓存中，当两个阶段在同一个时钟周期内进行时，就会发生对同一个 Cache 的访问冲突（结构冲突）。将高速缓存分离为指令高速缓存 and 数据高速缓存，使得取指和数据读写可以并行独立进行，从而有效减少了指令流水线的资源冲突。</li>
</ul>
<p><strong>(22) 答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：访问越权（如只读页面尝试写入）是通过检查页表项中的保护位来完成的，这在 MMU 地址转换期间发生。</li>
<li><strong>B [正确]</strong>：MMU 的职责是进行虚拟地址到物理地址的转换。当物理地址生成后，CPU 才会将物理地址送往高速缓存进行检索。如果数据不在高速缓存中，属于高速缓存缺失，这一事件是由高速缓存控制器在地址转换完成后检测的，与 MMU 的虚实地址转换过程无关。</li>
<li><strong>C [错误]</strong>：页面缺失（缺页）是通过检查页表项的有效位（存在位）是否为 0 来判断的，这发生在 MMU 查表进行地址转换的过程中。</li>
<li><strong>D [错误]</strong>：TLB 缺失是指快表未命中，MMU 在将虚拟地址转换为物理地址的第一步就是查询快表，若快表中没有该虚拟页号的记录，则检测出快表缺失。</li>
</ul>
<p><strong>(23) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：高速缓存 - 主存层次的数据传输和调度以主存块为单位，而主存 - 外存层次以页为基本调度单位。该叙述正确。</li>
<li><strong>B [错误]</strong>：高速缓存的缺失替换为了追求速度完全由硬件逻辑实现；而虚拟存储器的缺页处理涉及慢速外存访问，开销很大，所以采用操作系统内核软件来调度和实现替换算法。该叙述正确。</li>
<li><strong>C [错误]</strong>：高速缓存可根据设计需求选用写穿或回写法；而虚拟存储器层次由于外存的写入速度极慢，为了避免频繁写磁盘，必须采用回写法。该叙述正确。</li>
<li><strong>D [正确]</strong>：高速缓存 - 主存层次由于检索速度要求极高，可采用直接映射、组相联映射或全相联映射；但主存 - 外存层次发生页面缺失时需要访问慢速磁盘，开销巨大，为了尽可能降低缺页率，必须采用映射最灵活、空间利用率最高、冲突概率最低的全相联映射，而不是直接映射。因此该叙述是错误的，符合题意。</li>
</ul>
<p><strong>(24) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：误用十六进制表示下的虚拟页号 <code>82</code>（实际对应十进制虚页号 130）去查找虚页号为 82 的表项。虚页号 82 的存在位为 0，这会导致误判为缺页异常。</li>
<li><strong>B [错误]</strong>：计算错误，对应十进制的 129 号虚页的物理转换结果。</li>
<li><strong>C [正确]</strong>：虚拟地址为 <code>0008 2840H</code>。页大小为 $4\text{KB} = 2^{12}\text{ B}$，即低 12 位为页内偏移量。
<ul>
<li>去掉页内偏移量后，剩余的高位部分即为虚拟页号（十六进制为 <code>00082H</code>，转换为十进制为 $8 \times 16 + 2 = 130$）。</li>
<li>查页表项，当虚页号为 130 时，存在位（有效位）为 1，说明该页面已装入主存。</li>
<li>其对应的物理页框号（实页号）为 <code>018H</code>。</li>
<li>将物理页框号 <code>018H</code> 与页内偏移量 <code>840H</code> 拼接，可得最终转换后的主存物理地址为 <code>01 8840H</code>。</li>
</ul>
</li>
<li><strong>D [错误]</strong>：虚页号 130 的存在位为 1，说明页面在内存中，不会触发缺页异常。</li>
</ul>
<p><strong>(25) 答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：由于高速缓存采用了写穿策略，向高速缓存写入数据时必须同步写入主存，所以至少会有 1 次主存写入操作，无法实现 0 次访存。</li>
<li><strong>B [正确]</strong>：指令的执行可分为以下几个阶段：
<ol>
<li><strong>取指令</strong>：若指令所在的页面在主存中，快表命中且高速缓存命中，则无需访问主存。</li>
<li><strong>源操作数读取</strong>：需要读取 <code>xaddr</code> 处的数据 <code>x</code>。若 <code>xaddr</code> 所在的物理页面已在主存中，快表命中且高速缓存命中，则同样无需访问主存。</li>
<li><strong>数据写回</strong>：计算 $x+3$ 并将新值写回 <code>xaddr</code>。由于高速缓存使用的是写穿策略，向高速缓存中写入数据时，必须同时将新数据写入主存。这步必须进行 1 次主存写入。
综上，最理想情况下（全部命中），完成该指令功能至少需要访问主存 1 次。</li>
</ol>
</li>
<li><strong>C [错误]</strong>：高估了最少访存次数，当快表和高速缓存均命中时，读操作不需要访问主存。</li>
<li><strong>D [错误]</strong>：计算错误。</li>
</ul>
<p><strong>(26) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：这属于全不命中的常见情况。当要访问的页面不在主存中时，TLB中自然没有该页表项，且高速缓存（Cache）中也不可能存有该页的数据。这种情况可能发生。</li>
<li><strong>B [错误]</strong>：当访问的页面在主存中，但其页表项尚未调入快表，而页面对应的块刚好被缓存到了高速缓存中。这种情况可能发生。</li>
<li><strong>C [错误]</strong>：页面已调入主存且其页表项在快表中，但该地址的数据尚未调入高速缓存中。这种情况可能发生.</li>
<li><strong>D [正确]</strong>：快表是主存页表的一小部分缓存拷贝。如果快表命中，意味着对应虚拟页的页表项已在快表中，该页表项指示的页面必定已经存在于主存中。此时不可能发生页面未命中。因此，“TLB 命中，Page 未命中”是逻辑上相矛盾的，不可能发生，符合题意。</li>
</ul>
<p><strong>(27) 答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：虚拟地址为 <code>03FFF180H</code>。由于页面大小为 $4\text{KB} = 2^{12}\text{ B}$，页内偏移量占低 12 位（即十六进制低 3 位 <code>180H</code>）。
<ul>
<li>虚拟地址的高 20 位（即十六进制前 5 位 <code>03FFFH</code>）为虚拟页号。</li>
<li>检索快表表项，第 4 行的标记正好为 <code>03FFFH</code>，且其有效位为 1。</li>
<li>该表项对应的物理页框号为 <code>0153H</code>。</li>
<li>将物理页框号 <code>0153H</code> 与页内偏移量 <code>180H</code> 拼接，可得最终转换后的物理地址为 <code>0153180H</code>。</li>
</ul>
</li>
<li><strong>B [错误]</strong>：此项使用了错误的快表项进行转换（对应的是第 2 行的物理页框号 <code>0035H</code>，但其 Tag <code>3FFF1H</code> 与当前的虚拟页号不匹配）。</li>
<li><strong>C [错误]</strong>：快表中存在有效且匹配虚拟页号的表项，属于快表命中，不属于快表缺失。</li>
<li><strong>D [错误]</strong>：快表命中说明页面必定在主存中，不会发生缺页。</li>
</ul>
<p><strong>(28) 答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：计算错误，可能在页内偏移或组索引位数计算上存在偏差。</li>
<li><strong>B [错误]</strong>：计算有误，未能正确算出标记字段的位数。</li>
<li><strong>C [正确]</strong>：
<ul>
<li>页大小为 $1\text{KB} = 2^{10}\text{ B}$，主存按字节编址，因此页内偏移为 10 位。</li>
<li>虚拟地址共 32 位，故虚拟页号的位数为 $32 - 10 = 22$ 位。</li>
<li>快表有 32 个表项，采用 4 路组相联映射，每组有 4 个表项，快表的总组数为 $\frac{32}{4} = 8$ 组。</li>
<li>组索引所需的位数为 $\log_2 8 = 3$ 位。</li>
<li>在组相联映射下，标记字段的位数 = 虚拟页号位数 - 组索引位数 = $22 - 3 = 19$ 位。</li>
</ul>
</li>
<li><strong>D [错误]</strong>：计算有误，未正确划分子字段位数。</li>
</ul>
<p><strong>(29) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：当 MMU 访问页表检测到对应虚拟页的存在位为 0 时，就会向 CPU 发出缺页中断信号。这确实是在地址转换时由 CPU检测到的一种异常。该叙述正确。</li>
<li><strong>B [错误]</strong>：发生缺页异常后，CPU 会转入内核态，执行由操作系统提供的缺页异常处理程序。该叙述正确。</li>
<li><strong>C [错误]</strong>：操作系统会根据页面故障的逻辑地址，到外存中找到该页面的映像，并将其读入主存的空闲页框中。该叙述正确。</li>
<li><strong>D [正确]</strong>：缺页处理属于故障，当页面从外存调入主存并更新完页表后，发生缺页的那条指令必须被重新执行，以完成其访存操作。不能直接执行下一条指令。因此该叙述是错误的，符合题意。</li>
</ul>
<p><strong>(30) 答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：快表和高速缓存均基于程序局部性原理来缓存频繁访问的数据和页表项。该叙述正确。</li>
<li><strong>B [错误]</strong>：当快表缺失时，需要访问保存在主存中的完整页表；当高速缓存未命中时，同样需要访问主存来调入所需的数据块。该叙述正确。</li>
<li><strong>C [错误]</strong>：高速缓存的缺失处理由硬件实现，快表的缺失处理也可以由硬件实现。该叙述正确。</li>
<li><strong>D [正确]</strong>：为了使快表和高速缓存的读写延迟能够与 CPU 极高的工作频率相匹配，它们通常采用访问速度极快的SRAM来构建，而主存才使用DRAM。因此该叙述是错误的，符合题意。</li>
</ul>
<hr />
<h3>4.3 简答题详细解答</h3>
<p><strong>(1) 计算机系统中采用层次化存储体系结构的目的是什么？</strong><br />
<strong>答</strong>：目的是为了解决存储器在容量、速度和成本三者之间难以调和的物理矛盾。通过在 CPU 和主存之间引入高速、小容量的 Cache，在主存外侧引入大容量、低成本 of 辅存，利用程序的局部性原理，使整个存储系统在接近最快存储器（Cache）速度的同时，获得了接近最便宜存储器（辅存）的巨大容量与低廉的单位平均成本。<br />
<strong>(2) 为什么在存储器芯片中设置片选输入端？</strong><br />
<strong>答</strong>：在计算机系统中，主存通常由多块存储芯片进行字扩展或位扩展组合而成。片选输入端（CS 或 CE）用于控制该芯片是否被当前总线操作选中。只有当片选端有效时，该芯片的内部译码与读写电路才工作，允许与总线进行数据交换。这可以防止多个芯片同时输出数据导致总线数据冲突，是进行字扩展设计的基础。<br />
<strong>(3) 动态 MOS 存储器为什么要刷新？如何刷新？</strong><br />
<strong>答</strong>：DRAM 是利用栅极电容上的电荷状态来保存二进制信息。由于电容非常微小，其电荷会由于漏电而在几毫秒内缓慢流失，导致信息丢失。因此，必须在电荷流失殆尽前定期对芯片进行刷新。<br />
刷新是以“行”为单位进行的。刷新时，由刷新控制器依次产生行地址，并发出刷新控制信号，对该行内的所有存储元进行一次“读出-重写”操作。常见的刷新方式有三种：集中刷新、分散刷新和异步刷新。<br />
(4) <strong>试述多体交叉存储器的设计思想和实现方法。</strong><br />
<strong>答</strong>：设计思想是利用多个物理上独立、具有相同容量和字长的存储体组成存储器系统。通过对地址的低位进行交叉编址，使 CPU 能够以流水线重叠或并行的方式访问不同的物理存储体，从而提升主存带宽。<br />
(5) <strong>为什么说 Cache 对程序员是透明的？</strong><br />
<strong>答</strong>：因为 Cache 的地址映射、命中检测、缺失调块以及数据替换等一系列操作，完全是由 CPU 内部的 Cache 控制器硬件电路自动完成的。对于系统级或应用程序员来说，在编写代码时只需要使用主存的物理地址或虚拟地址，不需要在指令中显式地对 Cache 进行任何读写或替换控制。因此，Cache 的物理存在和运行机制在软件逻辑层面上是不可见的。<br />
(6) <strong>直接相联映射方式下为什么不需要使用替换算法？</strong><br />
<strong>答</strong>：直接相联映射规定主存中的每一个块只能映射到 Cache 的一个唯一、固定的行中。当 CPU 访问某主存块发生缺失，需要将其调入 Cache 时，由于该块在 Cache 中只有一个专属的位置，如果该位置当前已被其他主存块占用，新调入的块只能无条件将其覆盖。由于没有任何选择空间，因此不需要替换算法。<br />
(7) <strong>为什么要考虑 Cache 的一致性？</strong><br />
<strong>答</strong>：在计算机系统中，同一份数据可能会同时存在于主存中以及 Cache 的拷贝中。在多处理器架构中，不同处理器的 Cache 可能存有同一主存单元的多份拷贝；或者在 I/O 设备进行 DMA 传送时直接修改了主存的数据。如果某个处理器或 I/O 设备修改了其中一份拷贝，而其他拷贝或主存中的原数据未能及时同步更新，就会导致不同的部件读取到不同的陈旧数据，引发程序运行错误。因此，必须通过一致性协议来确保所有部件看到的数据始终保持一致。<br />
(8) <strong>替换算法有哪几种？它们各有何优缺点？</strong><br />
<strong>答</strong>：常用的替换算法有以下四种：</p>
<ul>
<li><strong>先进先出算法（FIFO）</strong>：优点是实现简单，缺点是未考虑程序的局部性规律，可能会频繁淘汰最近仍在使用的块，甚至会出现 Belady 异常。</li>
<li><strong>近期最少使用算法（LRU）</strong>：优点是严格依据局部性原理，命中率非常高，是目前最常用的主流算法；缺点是需要维护复杂的计数器以记录各块的新鲜度，硬件开销大。</li>
<li><strong>最不经常使用算法（LFU）</strong>：优点是将访问次数最少的块淘汰，能较好反映块的历史使用频次；缺点是无法及时淘汰在程序前期被频繁访问但后期不再使用的“垃圾块”。</li>
<li><strong>随机替换算法（RAND）</strong>：优点是硬件结构最简单；缺点是命中率不稳定，完全依靠概率。</li>
</ul>
<hr />
<h3>4.4 至 4.22 计算与分析大题步骤及详细解答</h3>
<h4>4.4</h4>
<p><strong>答</strong>：</p>
<ol>
<li>存储器容量为 32KB = $32 \times 1024 \times 8$ 位。</li>
<li>若按 16 位字编址，则存储器的字容量为：<br />
$\text{字数} = 32\text{KB} \div 2\text{B} = 16\text{K} = 16384$ 个字。</li>
<li>地址寄存器（MAR）的位数取决于字数。因为 $16\text{K} = 2^{14}$，所以地址寄存器应是 <strong>14 位</strong>。</li>
<li>数据寄存器（MDR）的位数取决于字长，字长为 16 位，因此数据寄存器是 <strong>16 位</strong>。</li>
</ol>
<hr />
<h4>4.5</h4>
<p><strong>答</strong>：<br />
使用 4 个 $32\text{K}\times 8$ 位的 SRAM 芯片，可以通过不同的排列组合进行字扩展或位扩展，设计出以下三种不同容量和字长的存储器：</p>
<ol>
<li><strong>设计方案一：容量 $32\text{K}\times 32$ 位（容量 128KB，字长 32 位）</strong>
<ul>
<li><em>实现方法</em>：进行位扩展。将 4 个芯片并联，其地址线 $A_{14}\sim A_0$ 均连接到 CPU 的地址线；4 个芯片的片选信号线 $\overline{\text{CS}}$ 并联后接 CPU 的访存控制线；数据线分别连接到 CPU 32位数据总线的不同字节段 $D_0\sim D_7$、$D_8\sim D_{15}$、$D_{16}\sim D_{23}$、$D_{24}\sim D_{31}$。</li>
</ul>
</li>
<li><strong>设计方案二：容量 $128\text{K}\times 8$ 位（容量 128KB，字长 8 位）</strong>
<ul>
<li><em>实现方法</em>：进行字扩展。将 4 个芯片的地址线 $A_{14}\sim A_0$ 和数据线 $D_7\sim D_0$ 分别并联。使用 CPU 的高位地址线 $A_{16}, A_{15}$ 通过一个 $2:4$ 译码器，产生四个独立的片选信号分别连接到 4 个芯片的片选端 $\overline{\text{CS}}$。</li>
</ul>
</li>
<li><strong>设计方案三：容量 $64\text{K}\times 16$ 位（容量 128KB，字长 16 位）</strong>
<ul>
<li><em>实现方法</em>：字位同时扩展。将 2 个芯片作为一组进行位扩展组成 $32\text{K}\times 16$ 位的单元（共 2 组）。再利用高位地址线 $A_{15}$ 经过一个非门译码，分别控制两组芯片的片选端。</li>
</ul>
</li>
</ol>
<hr />
<h4>4.6</h4>
<p><strong>答</strong>：</p>
<ol>
<li><strong>分析地址空间与容量需求</strong>：
<ul>
<li>只读存储区（ROM）：地址范围 <code>30000H</code> ~ <code>3FFFFH</code>。其大小为 <code>3FFFFH</code> - <code>30000H</code> + 1 = <code>10000H</code> 字节 = 64KB。</li>
<li>可读写存储区（RAM）：目标总容量为 $256\text{K}\times 8$ 位 = 256KB。由于 ROM 占用了 64KB，其余的 256KB - 64KB = 192KB 空间为 RAM 区。</li>
</ul>
</li>
<li><strong>芯片数量计算</strong>：
<ul>
<li>ROM 区：需要 64KB。用 $64\text{K}\times 4$ 位的 ROM 芯片，需要进行位扩展。芯片数 = $64\text{KB} \div (64\text{K}\times 4\text{位}) = 2$ 片。</li>
<li>RAM 区：需要 192KB。用 $32\text{K}\times 8$ 位的 RAM 芯片，需要进行字扩展。芯片数 = $192\text{KB} \div 32\text{KB} = 6$ 片。</li>
</ul>
</li>
<li><strong>地址分配与片选译码设计</strong>：
<ul>
<li>地址线共 18 位（$A_{17}\sim A_0$），因为 $256\text{KB} = 2^{18}$。</li>
<li>地址高两位 $A_{17}, A_{16}$ 决定地址区间：
<ul>
<li>ROM区地址：<code>30000H</code> ~ <code>3FFFFH</code>。当 $A_{17}A_{16} = 11$ 时选通 ROM，此时片选信号 $\overline{\text{CS}}<em>{\text{ROM}} = \overline{A</em>{17} \cdot A_{16}}$。</li>
<li>RAM区地址：分为 6 组 32KB（地址线需 15 位 $A_{14}\sim A_0$）。可以用高位地址线 $A_{17}, A_{16}, A_{15}$ 经由译码器输出分别选通 6 组 RAM。</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr />
<h4>4.7</h4>
<p><strong>答</strong>：</p>
<ol>
<li><strong>容量与芯片分析</strong>：
<ul>
<li>主存总容量为 $128\text{K}\times 16$ 位。按字节编址，总容量为 256KB。</li>
<li>ROM 区地址范围：<code>18000H</code> ~ <code>1FFFFH</code>。其大小为 <code>1FFFFH</code> - <code>18000H</code> + 1 = <code>8000H</code> 字节 = 32KB。由于字长 16 位，对应的只读区域为 $16\text{K}\times 16$ 位。</li>
<li>芯片选型：ROM 芯片规格为 $32\text{K}\times 16$ 位，使用其一半的容量。</li>
<li>RAM 区：其余空间为 RAM 区。RAM 总大小 = 256KB - 32KB = 224KB，对应 $112\text{K}\times 16$ 位。使用 $16\text{K}\times 8$ 位的 SRAM 芯片，每 2 片进行位扩展组成 $16\text{K}\times 16$ 位的一组。共需要 112K / 16K = 7 组，芯片总数 = 7 × 2 = 14 片。</li>
</ul>
</li>
<li><strong>连接设计要点</strong>：
<ul>
<li>地址线 17 位（$A_{16}\sim A_0$）。</li>
<li>译码电路：使用 $3:8$ 译码器对高位地址进行译码，输出的 8 个片选信号分别对应 1 组 ROM 和 7 组 RAM。</li>
</ul>
</li>
</ol>
<hr />
<h4>4.8</h4>
<p><strong>答</strong>：</p>
<ol>
<li>容量为 64K × 1 位，则存储元个数为 64K = $2^{16}$，正常的地址线需要 16 根。</li>
<li>该芯片为动态 RAM（DRAM），采用地址复用技术，行地址与列地址分时复用，因此所需的地址引脚数减半，为 16 / 2 = 8 根。</li>
<li>芯片必须的引脚包括：地址线 8 根，数据线 1 根，行选通信号 $\overline{\text{RAS}}$ 1 根，列选通信号 $\overline{\text{CAS}}$ 1 根。</li>
<li>除电源、地线及写使能外的最小引脚数 = 地址线(8) + 数据线(1) + $\overline{\text{RAS}}$(1) + $\overline{\text{CAS}}$(1) = <strong>11 根</strong>。</li>
</ol>
<hr />
<h4>4.9</h4>
<p><strong>答</strong>：</p>
<ol>
<li><strong>控制参数</strong>：$1\text{M}\times 8$ 位由 $64\text{K}\times 1$ 位芯片构成，每个芯片有 256 行。</li>
<li><strong>异步刷新计算</strong>：
<ul>
<li>异步刷新是在 2ms 内将所有 256 行轮流刷新一遍。</li>
<li>产生刷新信号的间隔时间 = 2ms ÷ 256 行 = <strong>$7.81\mu\text{s}$</strong>（或约 $7.8\mu\text{s}$）。</li>
</ul>
</li>
<li><strong>集中刷新计算</strong>：
<ul>
<li>读写周期为 $0.5\mu\text{s}$。刷新一行需要一个读写周期。</li>
<li>存储器刷新一遍最少需要的读写周期数 = 芯片的行数 = <strong>256 个周期</strong>。</li>
<li>CPU 的死时间 = 256 × $0.5\mu\text{s}$ = <strong>$128\mu\text{s}$</strong>。</li>
</ul>
</li>
</ol>
<hr />
<h4>4.10</h4>
<p><strong>答</strong>：</p>
<ol>
<li><strong>刷新方式选择</strong>：
<ul>
<li>存储器读写周期为 $0.5\mu\text{s}$，CPU 要求在 $1\mu\text{s}$ 内至少能访问存储器一次。</li>
<li>如果采用集中刷新方式，刷新期间会产生 $128\mu\text{s}$ 的死时间，无法满足 CPU 在 $1\mu\text{s}$ 内必须访问的要求。</li>
<li>如果采用分散刷新方式，每个工作周期分为读写和刷新两部分，存取周期拉长为 $0.5\mu\text{s} \times 2 = 1\mu\text{s}$，CPU 刚好可以在 $1\mu\text{s}$ 内访问一次，且没有死时间。但分散刷新过于频繁，降低了系统性能。</li>
<li>如果采用<strong>异步刷新方式</strong>，每隔 $7.81\mu\text{s}$ 才刷新一行（耗时 $0.5\mu\text{s}$），只要避开 CPU 访问时刻，CPU 的访问不会被长时间阻塞，最合适。</li>
<li><strong>结论</strong>：采用<strong>异步刷新</strong>或<strong>分散刷新</strong>均可，其中以<strong>异步刷新</strong>在维持系统整体性能上最为合适。</li>
</ul>
</li>
<li><strong>刷新信号的产生周期</strong>：
<ul>
<li>$16\text{K}\times 1$ 位芯片有 128 行。</li>
<li>刷新间隔为 2ms，所以产生刷新信号的周期 = 2ms ÷ 128 行 = <strong>$15.625\mu\text{s}$</strong>。</li>
</ul>
</li>
</ol>
<hr />
<h4>4.11</h4>
<p><strong>答</strong>：<br />
Cache 容量为 $2^{14}$ 块（16384 块），每块 4B。主存按字节编址。<br />
块内偏移（Offset）占 2 位（$2^2 = 4\text{B}$）。Cache 索引（Index）占 14 位。</p>
<ol>
<li><strong>全相联映射</strong>：
<ul>
<li>映射规则：主存块可以放入 Cache 的任意空闲行。Cache 标记项保存物理块号（即主存地址的高 30 位）。</li>
<li>标记内容：地址去掉低 2 位后的值。例如地址 <code>000008H</code>，其标记为 <code>000002H</code>。</li>
</ul>
</li>
<li><strong>直接相联映射</strong>：
<ul>
<li>映射规则：Cache 行号 = 物理块号 mod $2^{14}$。</li>
<li>地址划分：高位为 Tag，中间 14 位为 Index，低 2 位为 Offset。
<ul>
<li>地址 <code>000000H</code>：Index = 0，Tag = 0。装入 Cache 的第 0 块。</li>
<li>地址 <code>000008H</code>：Index = 2，Tag = 0。装入 Cache 的第 2 块。</li>
</ul>
</li>
</ul>
</li>
<li><strong>四路组相联映射</strong>：
<ul>
<li>组数 = $2^{14} \div 4 = 2^{12}$ 组。组索引（Index）占 12 位。高位为 Tag，低 2 位为 Offset。</li>
</ul>
</li>
</ol>
<hr />
<h4>4.12</h4>
<p><strong>答</strong>：<br />
(1) <strong>主存地址和 Cache 地址位数</strong>：</p>
<ul>
<li>主存包含 4096 个存储块，每块 128 个字。主存总字数 = $4096 \times 128 = 2^{19}$ 个字。因为访问地址为字地址，所以主存地址有 <strong>19 位</strong>。</li>
<li>Cache 由 64 个存储块构成，每块 128 字。Cache总字数 = $64 \times 128 = 2^{13}$ 字。所以 Cache 地址有 <strong>13 位</strong>。<br />
(2) <strong>主存地址划分</strong>（四路组相联）：</li>
<li>块内字地址（Offset）：7 位（因为每块有 $128 = 2^7$ 个字）。</li>
<li>Cache 共有 64 块，四路组相联表示每组有 4 块。组数 = 64 / 4 = 16 组。组号（Index）占 4 位（因为 $16 = 2^4$）。</li>
<li>标记（Tag）：19 - 4 - 7 = 8 位。</li>
<li><strong>划分结构</strong>：<code>[ Tag (8位) ] + [ 组号 (4位) ] + [ 块内字地址 (7位) ]</code>。</li>
</ul>
<hr />
<h4>4.13</h4>
<p><strong>答</strong>：<br />
(1) <strong>Cache 命中率计算</strong>：</p>
<ul>
<li>Cache 容量为 16KB，每字 32 位（4 字节），包含的字数 = 16KB ÷ 4B = 4K 字。</li>
<li>块大小为 8 个字 = 32B。Cache 的总行数 = 16KB ÷ 32B = 512 行。采用四路组相联，组数 = 512 ÷ 4 = 128 组。</li>
<li>CPU 依次从主存第 0 到 99 号字单元读出 100 个字。因为 100 个字运小于 Cache 的总容量（4K 字），这 100 个字可以完全被装入 Cache 中，且不会发生替换。</li>
<li>第一次循环时，Cache 为空。读取第 0 单元缺失，调入 0~7 单元。因此 100 个字在第 1 遍中发生的缺失次数 = $\lceil 100 / 8 \rceil = 13$ 次。</li>
<li>在随后的 9 次循环中，所需数据已全部在 Cache 中，命中率为 100%。</li>
<li>总访问次数 = 100 × 10 = 1000 次。总缺失次数 = 13 次。</li>
<li>Cache 命中率 $H = (1000 - 13) \div 1000 = 98.7%$。<br />
(2) <strong>访问速度提高倍数</strong>：</li>
<li>设 Cache 访问周期为 $t$，则主存访问周期为 $10t$。</li>
<li>平均访问时间 $T_a = H \cdot t + (1 - H) \cdot 10t = 0.987t + 0.013 \times 10t = 0.987t + 0.13t = 1.117t$。</li>
<li>速度提高倍数 = 原始主存时间 ÷ 平均时间 = $10t \div 1.117t \approx 8.95$ 倍。</li>
</ul>
<hr />
<h4>4.14</h4>
<p><strong>答</strong>：<br />
(1) <strong>数组访问的局部性</strong>：</p>
<ul>
<li>二维数组 $a[M][N]$ 在内存中是按行优先顺序连续存放的。</li>
<li><strong>代码 A</strong>：内层循环变量为 $j$，访问序列为 $a[i][0], a[i][1], \ldots$。这是严格按照内存物理顺序顺序读取，具有极佳的空间局部性；因为每个数组元素只被访问了一次，所以无时间局部性。</li>
<li><strong>代码 B</strong>：内层循环变量为 $j$，访问序列为 $a[0][i], a[1][i], a[2][i], \ldots$。在内存中每次访问都跳过了 $N$ 个元素。若 $N$ 较大，每次读取都会发生 Cache 缺失，空间局部性极差；无时间局部性。<br />
(2) <strong>变量 sum 的局部性</strong>：</li>
<li>变量 <code>sum</code> 在循环体内部被反复进行读出和累加改写。因此，<code>sum</code> 具有极强的时间局部性；因为它是单量，无空间局部性。<br />
(3) <strong>循环体指令访问的局部性</strong>：</li>
<li>循环体内的指令代码在循环期间被 CPU 反复、顺序地取出并执行。因此，指令访问既具有极强的时间局部性，又具有极强的空间局部性。</li>
</ul>
<hr />
<h4>4.15</h4>
<p><strong>答</strong>：<br />
(1) <strong>Cache 附加位计算</strong>：</p>
<ul>
<li>数据区容量为 32KB，主存块大小 64B，行数 = 32KB ÷ 64B = 512 行。采用 8 路组相联，组数 = 512 ÷ 8 = 64 组。</li>
<li>块内地址占 6 位，组号占 6 位。Tag 位数 = 32 - 6 - 6 = 20 位。</li>
<li>8 路组相联采用 LRU，状态数 8 个，LRU 位占 $\log_2 8 = 3$ 位。</li>
<li>采用写穿策略，数据修改同步写入主存，因此不需要修改位（脏位）。</li>
<li><strong>结论</strong>：标记占 20 位，LRU 占 3 位，没有修改位。<br />
(2) <strong>数据 Cache 缺失次数</strong>：</li>
<li>起始地址 <code>0080 00C0H</code> （十进制 192）能被 64 整除，数组首元素刚好与 Cache 的一个块的边界对齐。</li>
<li>数组 s 共有 1024 个元素，占用的内存字节数 = 1024 × 4B = 4096B。主存块大小为 64B，因此数组共占用 4096 ÷ 64 = 64 个主存块。</li>
<li>循环执行 <code>s[k] = 2 * s[k]</code>，对每个元素进行一次读取和一次写入。每个块有 16 个整型元素。第一次读取首元素会发生缺失，启动主存块调入，随后的 15 次读取和 16 次写入均命中。</li>
<li>数组占用 64 个块，总缺失次数 = 64 × 1 = <strong>64 次</strong>。<br />
(3) <strong>指令访问及缺失处理过程</strong>：</li>
<li>CPU 给出的虚拟地址为 <code>0001 0003H</code>。</li>
<li>
<ol>
<li>CPU 首先在 TLB 中进行地址映射获取物理地址。</li>
</ol>
</li>
<li>
<ol>
<li>得到物理地址后，利用中间 6 位定位到指令 Cache 的对应组，并行读取该组 8 行中的 Tag 字段进行比较。</li>
</ol>
</li>
<li>
<ol>
<li>若相等且有效位为 1，则命中，根据低 6 位偏移行内字节数据送入 CPU IR。</li>
</ol>
</li>
<li>
<ol>
<li>若不匹配，则发生缺失。Cache 控制器向主存发起读请求，将该物理块（64B）整体调入 Cache 组内（若该组已满，根据 LRU 算法替换出最久未使用的块），更新该行的 Tag 并将有效位置 1，最后将所需指令送入 CPU。</li>
</ol>
</li>
</ul>
<hr />
<h4>4.16</h4>
<p><strong>答</strong>：<br />
(1) <strong>页面分布及缺页异常次数</strong>：</p>
<ul>
<li>数组 <code>a[24][64]</code> 共有 1536 个 <code>int</code> 元素，容量 = 1536 × 4B = 6144B。页面大小为 4KB = 4096B。起始地址 <code>0042 2000H</code> 对齐。</li>
<li>页面 1：存放数组的前 4096B，即前 1024 个元素（对应行 0 至行 15）。</li>
<li>页面 2：存放数组剩余的 2048B，即后 512 个元素（行 16 至行 23）。</li>
<li><strong>分布结论</strong>：分布在 <strong>2 个页面</strong>中。</li>
<li><strong>缺页异常</strong>：数组按行优先顺序读取，第一次访问 <code>a[0][0]</code> 时，页面 1 发生缺页；访问到 <code>a[16][0]</code> 时，页面 2 发生缺页。共发生 <strong>2 次</strong>缺页异常。</li>
<li><strong>页故障地址</strong>：分别为 <code>0042 2000H</code> 和 <code>0042 3000H</code>。<br />
(2) <strong>时间局部性</strong>：<strong>不具有时间局部性</strong>。因为每一个元素都仅仅被执行了一次写入操作，之后不再被重复访问。<br />
(3) <strong>地址字段划分与组号计算</strong>：</li>
<li>块内偏移占 5 位（$2^5 = 32\text{B}$）。组号（Index）占 6 位（$2^6 = 64$ 组）。数据 Cache 组号占 6 位，块内地址占 5 位。</li>
<li><code>a[1][0]</code> 的虚拟地址 = <code>0042 2000H</code> + (1 × 64 + 0) × 4 = <code>0042 2100H</code>。</li>
<li>其页内偏移与物理地址低位一致为 <code>100H</code>（二进制：<code>0001 0000 0000</code>）。按划分规则：低 5 位 <code>00000</code> 为块内偏移，中间 6 位 <code>001000</code>（十进制为 8）为组号。所以对应的 Cache 组号是 <strong>8</strong>。<br />
(4) <strong>Cache 命中率计算</strong>：</li>
<li>行优先顺序访问：每个主存块包含的 8 个元素被连续访问。仅在访问块中第 1 个元素时缺失，后 7 个均命中。命中率 = 7 / 8 = <strong>87.5%</strong>。</li>
<li>列优先顺序访问：每次访问在内存中跳跃了一整行。在访问完一列前，主存块的内容早已被全部替换出 Cache（因为 Cache 总组数仅 64 组，无法同时驻留这么多跳跃地址的块）。所以每次访问都会发生 Cache 缺失，命中率为 <strong>0%</strong>。</li>
</ul>
<hr />
<h4>4.17</h4>
<p><strong>答</strong>：</p>
<ol>
<li>虚存容量为 2GB = $2^{31}$ 字节，页面大小为 4KB = $2^{12}$ 字节。页内偏移地址 VPO = <strong>12 位</strong>，虚拟页号 VPN = 31 - 12 = <strong>19 位</strong>。</li>
<li>主存容量为 8MB = $2^{23}$ 字节，物理地址为 23 位。页内物理偏移地址 PPO = <strong>12 位</strong>，物理页框号 PPN = 23 - 12 = <strong>11 位</strong>。</li>
</ol>
<hr />
<h4>4.18</h4>
<p><strong>答</strong>：<br />
(1) 缺页失效的虚页号有：<strong>2、3、5、7</strong>。<br />
(2) 页大小为 1KB = 1024B。</p>
<ul>
<li><strong>虚地址 0</strong>：$\text{VPN} = 0 \div 1024 = 0$。对应实页号为 3，装入位 1。实地址 = $3 \times 1024 + 0 = 3072$。</li>
<li><strong>虚地址 3028</strong>：$\text{VPN} = 3028 \div 1024 = 2$。装入位为 0，引发缺页失效，<strong>无实地址</strong>。</li>
<li><strong>虚地址 1023</strong>：$\text{VPN} = 1023 \div 1024 = 0$。对应实页号为 3。实地址 = $3 \times 1024 + 1023 = 4095$。</li>
<li><strong>虚地址 2048</strong>：$\text{VPN} = 2048 \div 1024 = 2$。装入位为 0，引发缺页失效，<strong>无实地址</strong>。</li>
<li><strong>虚地址 4096</strong>：$\text{VPN} = 4096 \div 1024 = 4$。页表对应实页号为 3，装入位 1。实地址 = $3 \times 1024 + 0 = 3072$。</li>
<li><strong>虚地址 8000</strong>：$\text{VPN} = 8000 \div 1024 = 7$。装入位为 0，缺页失效，<strong>无实地址</strong>。</li>
</ul>
<hr />
<h4>4.19</h4>
<p><strong>答</strong>：<br />
(1) <strong>虚拟地址划分</strong>：</p>
<ul>
<li>虚拟容量为 2GB = $2^{31}$B，地址为 31 位。页大小为 128KB = $2^{17}$B。页内地址占低 <strong>17 位</strong>，虚拟页号（VPN）占高 <strong>14 位</strong>。</li>
<li>TLB 组数 = 16 ÷ 4 = 4 组。TLB 索引占 <strong>2 位</strong>。</li>
<li>高位中的低 2 位（$V_{18}, V_{17}$）为 TLB 索引，高 12 位（$V_{30}\sim V_{19}$）为 TLB 标记。<br />
(2) <strong>物理地址划分</strong>：</li>
<li>主存容量 4MB = $2^{22}$B，物理地址共 22 位。页大小为 128KB，偏移地址占低 <strong>17 位</strong>，物理页框号占高 <strong>5 位</strong>。<br />
(3) <strong>Cache 地址映射划分</strong>：</li>
<li>Cache 共有 512 行。采用四路组相联，组数 = 512 ÷ 4 = 128 组。</li>
<li>块内偏移占 5 位（$2^5 = 32\text{B}$）。组号（Index）占 7 位（$2^7 = 128$ 组）。Tag 位数 = 22 - 7 - 5 = 10 位。</li>
<li><strong>划分结构</strong>：<code>[ Tag (10位) ] + [ 组号 (7位) ] + [ 块内偏移 (5位) ]</code>。</li>
</ul>
<hr />
<h4>4.20</h4>
<p><strong>答</strong>：<br />
(1) 虚拟地址 30 位，页大小 4KB（$2^{12}$B）。虚页号占高 <strong>18 位</strong>，页内地址占低 <strong>12 位</strong>。<br />
(2) TLB 共 8 组（$2^3 = 8$），故 TLB 组号占 <strong>3 位</strong>，低 3 位用作组号，高 15 位用作标记。虚拟地址中，$V_{14}\sim V_{12}$ 为 TLB 组号，$V_{29}\sim V_{15}$ 为 TLB 标记。<br />
(3) <strong>TLB 替换模拟过程</strong>：<br />
将访问的虚页号转换为二进制以计算组号（低 3 位）：</p>
<ul>
<li>10 (二进制 <code>001010</code>) → 组号 = 2 (低三位 <code>010</code>)。组 2 状态：[10]。</li>
<li>12 (二进制 <code>001100</code>) → 组号 = 4 (低三位 <code>100</code>)。组 4 状态：[12]。</li>
<li>16 (二进制 <code>010000</code>) → 组号 = 0 (低三位 <code>000</code>)。组 0 状态：[16]。</li>
<li>7 (二进制 <code>000111</code>) → 组号 = 7 (低三位 <code>111</code>)。组 7 状态：[7]。</li>
<li>26 (二进制 <code>011010</code>) → 组号 = 2。并入后变为 [26, 10]。</li>
<li>4 (二进制 <code>000100</code>) → 组号 = 4。并入后变为 [4, 12]。</li>
<li>12 (二进制 <code>001100</code>) → 组号 = 4。命中，更新状态为 [12, 4]（12 最新）。</li>
<li>20 (二进制 <code>010100</code>) → 组号 = 4。组 4 满，4 最久未用，故淘汰 <strong>4</strong>。</li>
<li><strong>结论</strong>：<strong>虚页号 4</strong> 对应的表项被替换。<br />
(4) 每个 TLB 表项中的标记增加 2 位，整个表项位数<strong>增加 2 位</strong>。</li>
</ul>
<hr />
<h4>4.21</h4>
<p><strong>答</strong>：<br />
(1) <strong>物理地址字段划分</strong>：</p>
<ul>
<li>物理地址为 24 位。页大小为 8KB = $2^{13}$B。物理页号 = 24 - 13 = <strong>11 位</strong>，页内偏移地址 = <strong>13 位</strong>。</li>
<li>Cache 组数 = 512 组。块内偏移占 6 位。组索引占 9 位。Tag 位数 = 24 - 9 - 6 = 9 位。</li>
<li><strong>划分</strong>：<code>[ Tag (9位) ] + [ 组号 (9位) ] + [ 块内偏移 (6位) ]</code>。</li>
<li>TLB 采用全相联映射，TLB 标记中存放的是完整的虚拟页号（VPN = 19位）以及属性位。<br />
(2) <strong>主存块装入映射计算</strong>：</li>
<li>块号为 4099。映射的 Cache 组号 = 块号 mod 512 = 4099 mod 512 = <strong>3</strong>。</li>
<li>对应 Tag 的内容：物理块号高位部分，物理 Tag = <code>000001000_2</code>（十六进制：<code>008H</code>）。<br />
(3) <strong>时间开销对比</strong>：<strong>缺页处理的开销远大于 Cache 缺失的开销</strong>。因为 Cache 缺失只需从主存读取数据块，耗时几十到上百纳秒；而页缺失需要访问磁盘，需要几毫秒甚至十几毫秒。时间相差达 5 个数量级。<br />
(4) <strong>写策略原因</strong>：Cache 采用写穿策略时，写主存延迟在几十纳秒内，性能影响小。而主存修改如果采用写穿到磁盘，将面临高达毫秒级的磁盘写入延迟，导致系统效率极低。因此采用写回法。</li>
</ul>
<hr />
<h4>4.22</h4>
<p><strong>答</strong>：<br />
(1) 主存为 $1\text{MB}$，物理地址占 <strong>20 位</strong>。<br />
(2) TLB 采用<strong>全相联映射</strong>或<strong>组相联映射</strong>，采用 <strong>SRAM</strong> 芯片实现。<br />
(3) Cache 采用<strong>组相联映射</strong>。Cache 行中除数据外，还应有 <strong>LRU位</strong>和<strong>修改位（脏位）</strong>。有效位作用是指示当前 Cache 行中的数据是否有效。<br />
(4) 虚拟地址 <code>0008 C040H</code>。物理地址为物理页框号加低 12 位 <code>040H</code>。是否命中取决于该物理地址对应的 Tag 是否与 Cache 对应行中的 Tag 一致。若虚拟地址为 <code>0007 C260H</code>：其物理偏移为 <code>260H</code>。利用物理地址的组索引字段定位到对应的 Cache 组。</p>
<h3>课后习题原图书影</h3>
<p><img src="https://img.picr2.online/1782724945268_PixPin_2026-06-29_17-21-28.webp" alt="课后习题与真题截图1" />
<img src="https://img.picr2.online/1782724945358_PixPin_2026-06-29_17-21-47.webp" alt="课后习题与真题截图2" />
<img src="https://img.picr2.online/1782724985320_image.webp" alt="课后习题与真题截图3" />
<img src="https://img.picr2.online/1782724997851_image.webp" alt="课后习题与真题截图4" />
<img src="https://img.picr2.online/1782725011912_image.webp" alt="课后习题与真题截图5" />
<img src="https://img.picr2.online/1782725038868_image.webp" alt="课后习题与真题截图6" />
<img src="https://img.picr2.online/1782725038731_image.webp" alt="课后习题与真题截图7" />
<img src="https://img.picr2.online/1782725044428_image.webp" alt="课后习题与真题截图8" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>运算方法与运算器</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%BF%90%E7%AE%97%E6%96%B9%E6%B3%95%E4%B8%8E%E8%BF%90%E7%AE%97%E5%99%A8/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%BF%90%E7%AE%97%E6%96%B9%E6%B3%95%E4%B8%8E%E8%BF%90%E7%AE%97%E5%99%A8/</id>
    <published>2026-06-28T11:40:00.000Z</published>
    <updated>2026-06-28T11:40:00.000Z</updated>
    <summary>整理计算机中的基本运算、移位运算与补码加减法，深入解析原码一位乘法、浮点加减法运算以及运算器的硬件逻辑实现。</summary>
    <content type="html"><![CDATA[<p>本文整理计算机中的运算方法与运算器设计。在计算机组成原理中，掌握<strong>移位运算、补码加减法与溢出检测、原码一位乘法以及浮点加减法五步法</strong>是本章的复习重点。</p>
<p>要想透彻理解本章，可以按一条主线展开：</p>
<blockquote>
<p>计算机内部真正擅长的是<strong>加法、移位与逻辑运算</strong>。
减法可以转化为加法，乘法可以转化为“加法与移位”，除法可以转化为“减法与移位”，浮点运算则是在定点运算的基础上增加了阶码处理。</p>
</blockquote>
<hr />
<h1>第3章：运算方法与运算器</h1>
<hr />
<h2>3.1 计算机中的运算</h2>
<p>计算机中的基本运算可分为位运算、逻辑运算、加减法的逻辑实现、定点除法运算以及浮点乘除法。</p>
<h3>3.1.1 C 语言中的位运算</h3>
<p>位运算是对二进制数的每一位分别进行操作。位运算属于低权重考点，但它是构建复杂逻辑的基础。</p>
<p>常见位运算如下：</p>
<table>
<thead>
<tr>
<th>运算名称</th>
<th>运算符号</th>
<th>运算规则与作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>按位与</td>
<td><code>&amp;</code></td>
<td>两位都为 1 时结果才为 1，否则为 0</td>
</tr>
<tr>
<td>按位或</td>
<td><code>|</code></td>
<td>两位中有一位为 1 时结果就为 1，否则为 0</td>
</tr>
<tr>
<td>按位非</td>
<td><code>~</code></td>
<td>逐位取反，0 变 1，1 变 0</td>
</tr>
<tr>
<td>按位异或</td>
<td><code>^</code></td>
<td>两位不同为 1，相同为 0</td>
</tr>
</tbody>
</table>
<p>按位异或运算在计算机硬件设计中很重要，因为它常用于判断两位是否不同，也常用于加法器中的求和位计算以及溢出检测电路。</p>
<h3>3.1.2 C 语言中的逻辑运算</h3>
<p>逻辑运算处理的是真与假，运算对象是整体逻辑值，结果通常只有 0 或 1。</p>
<p>常见逻辑运算如下：</p>
<table>
<thead>
<tr>
<th>运算名称</th>
<th>运算符号</th>
<th>运算规则</th>
</tr>
</thead>
<tbody>
<tr>
<td>逻辑与</td>
<td><code>&amp;&amp;</code></td>
<td>两个条件都为真时结果才为真</td>
</tr>
<tr>
<td>逻辑或</td>
<td><code>||</code></td>
<td>两个条件中有一个为真时结果就为真</td>
</tr>
<tr>
<td>逻辑非</td>
<td><code>!</code></td>
<td>条件为真时结果为假，条件为假时结果为真</td>
</tr>
</tbody>
</table>
<p>:::note[位运算与逻辑运算的区别]</p>
<ul>
<li><strong>位运算</strong>：逐位操作，结果仍然是多位的二进制数。</li>
<li><strong>逻辑运算</strong>：判断整体真假，结果通常是 0 或 1。</li>
</ul>
<p>例如，计算 <code>5 &amp; 3</code> 与 <code>5 &amp;&amp; 3</code>：</p>
<ul>
<li><code>5 &amp; 3</code> 对应的二进制运算为 <code>0101 &amp; 0011 = 0001</code>，结果是数值 1。</li>
<li><code>5 &amp;&amp; 3</code> 中，因为 5 和 3 都是非零值，在逻辑上均判定为真，所以逻辑与的结果也是 1。虽然二者数值结果碰巧相同，但其物理含义和运算机制完全不同。
:::</li>
</ul>
<h3>3.1.3 C 语言中的算术运算</h3>
<p>在 C 语言中，算术运算在编译为机器指令后，都是在固定的字长内由定点或浮点运算器执行的。在实际编程与考试选择题中，需要特别注意以下机制：</p>
<ul>
<li><strong>隐式类型转换</strong>：当无符号数与带符号数混合运算或进行比较时，C 语言会自动将带符号数隐式转换为无符号数。这常常导致反直觉的结果。例如，表达式 <code>-1 &lt; 1U</code> 的计算结果为假。因为 <code>-1</code> 被转换为无符号数时，其补码 <code>1111...1111</code> 会被解释为无符号数中的最大值 $2^{32}-1$，显然大于 <code>1</code>。</li>
<li><strong>溢出表现</strong>：
<ul>
<li>对于<strong>无符号数</strong>，运算溢出在 C 语言中是定义良好的，采用模运算处理，超出最高位的部分直接丢弃（如 8 位无符号数 $255 + 1 = 0$）。</li>
<li>对于<strong>带符号数</strong>，有符号数的溢出在 C 语言标准中属于<strong>未定义行为</strong>，但在现代计算机硬件中，它同样是通过补码加法器直接相加并截断最高位，可能导致正数相加变成负数的现象。</li>
</ul>
</li>
</ul>
<h3>3.1.4 定点除法运算概览</h3>
<p>定点除法在硬件上比乘法更为复杂，主要采用以下方式：</p>
<ul>
<li><strong>原码一位除法</strong>：符号单独处理（商符号为 $zf = xf \oplus yf$），数值部分按照绝对值相除。主要有两种递推算法：
<ul>
<li><strong>恢复余数法</strong>：先尝试用余数减去除数。如果减完后结果为正，说明够减，商上 1；如果结果为负，说明不够减，商上 0，并执行加法操作将除数加回以恢复余数。</li>
<li><strong>不恢复余数法</strong>：又称加减交替法。如果上一步减完后余数为负，不立即加回除数，而是将余数左移一位后直接加上除数；若余数为正，则左移一位后减去除数。该方法减少了恢复余数的操作次数，但如果最终余数为负，仍需在末尾加除数进行一次修正。</li>
</ul>
</li>
<li><strong>阵列除法器</strong>：一种用于提高除法运算速度的并行硬件结构，其基本构成组件是 <strong>CAS单元</strong>（即可控加减单元）。CAS 单元除了能够进行一位的全加运算外，还能通过一个控制输入来决定是执行加法还是减法。阵列除法器将大量的 CAS 单元排成多行阵列，每一行代表一次“试减”与“求余”的步骤，通过硬件连线自动将上一行的余数传递给下一行，实现了并行的加减交替运算。</li>
</ul>
<h3>3.1.5 浮点乘除法概览</h3>
<p>浮点数的乘除法不需要对阶，运算直接对阶码和尾数进行处理：</p>
<ul>
<li><strong>浮点乘法</strong>：阶码相加，尾数相乘，确定符号，然后进行规格化、舍入和溢出判断。</li>
<li><strong>浮点除法</strong>：阶码相减，尾数相除，确定符号，然后进行规格化、舍入和溢出判断。</li>
</ul>
<hr />
<h2>3.2 运算器硬件基础</h2>
<p>运算器是中央处理器中负责数据加工的核心部件，主要由算术逻辑单元、累加寄存器、状态寄存器以及通用寄存器组成。</p>
<h3>3.2.1 加减法的逻辑实现</h3>
<h4>半加器</h4>
<p>半加器用于计算两个一位二进制数相加，不考虑来自低位的进位。</p>
<ul>
<li>输入：被加数 $A$，加数 $B$。</li>
<li>输出：本位和 $S$，向高位的进位 $C$。</li>
</ul>
<p>逻辑表达式为：
$$
S = A \oplus B
$$
$$
C = AB
$$</p>
<h4>全加器</h4>
<p>全加器可以处理三个输入，能够将低位传来的进位一并进行计算。</p>
<ul>
<li>输入：被加数当前位 $A$，加数当前位 $B$，低位进位 $Cin$。</li>
<li>输出：本位和 $S$，向高位的进位 $Cout$。</li>
</ul>
<p>逻辑表达式为：
$$
S = A \oplus B \oplus Cin
$$
$$
Cout = AB + (A \oplus B)Cin
$$</p>
<h4>串行进位加法器</h4>
<p>将多个全加器串联起来即可构成多位加法器。低位的进位输出 $Cout$ 直接作为高一位的进位输入 $Cin$。这种结构简单，但由于高位的计算必须等待低位进位信号逐级传递，导致整体运算速度较慢。</p>
<h4>快速进位加法器</h4>
<p>为了提高运算速度，硬件设计中引入了超前自动进位计算。对于一位加法，定义：</p>
<ul>
<li>进位产生函数：$G_i = A_i B_i$</li>
<li>进位传递函数：$P_i = A_i \oplus B_i$</li>
</ul>
<p>则进位输出公式可表示为：
$$
C_{i+1} = G_i + P_i C_i
$$</p>
<p>通过展开该递推公式，可以直接由原始输入并行计算出各高位的进位值，从而消除串行进位的等待时间。</p>
<h3>3.2.2 运算器的硬件组成</h3>
<ul>
<li><strong>ALU</strong>：即算术逻辑单元，是运算器的核心实体，负责执行所有的算术运算、逻辑运算与移位操作。</li>
<li><strong>FPU</strong>：即浮点运算器，专门用于处理浮点数运算。其硬件内部包含阶码比较电路、对阶移位器、尾数加减器、规格化移位器以及舍入电路。</li>
<li><strong>标志寄存器</strong>：用于存放 ALU 运算后的状态标志。</li>
</ul>
<h3>3.2.3 核心标志位</h3>
<p>运算器在完成计算后，会根据结果的状态设置标志寄存器中的标志位。</p>
<table>
<thead>
<tr>
<th>标志位名称</th>
<th>英文简称</th>
<th>物理含义</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>零标志</td>
<td>ZF</td>
<td>运算结果是否为 0。若结果为 0 则 ZF = 1</td>
<td>无符号数与有符号数</td>
</tr>
<tr>
<td>符号标志</td>
<td>SF</td>
<td>运算结果的符号。若结果为负则 SF = 1</td>
<td>有符号数</td>
</tr>
<tr>
<td>进位标志</td>
<td>CF</td>
<td>最高位是否产生进位或借位</td>
<td>无符号数</td>
</tr>
<tr>
<td>溢出标志</td>
<td>OF</td>
<td>运算结果是否超出有符号数的表示范围</td>
<td>有符号数</td>
</tr>
</tbody>
</table>
<p>:::tip[CF 与 OF 的应用区别]</p>
<ul>
<li><strong>CF</strong> 仅对无符号数运算有意义。例如，8 位无符号数 <code>255 + 1</code> 产生进位，此时 CF = 1 且结果为 0，属于正常进位。</li>
<li><strong>OF</strong> 仅对有符号补码运算有意义。例如，8 位有符号数 <code>127 + 1</code> 结果为 -128，此时 OF = 1，表示发生了溢出，计算结果发生错误。
:::</li>
</ul>
<h3>3.2.4 运算器数据通路结构</h3>
<p>运算器内部通常采用总线结构将各部件连接起来，以下展示了一个定点运算器的典型数据通路架构：</p>
<pre><code>flowchart BT
    subgraph 寄存器组
        R[通用寄存器]
        ACC[累加寄存器]
    end
    subgraph 数据输入
        MUX[多路选择器]
        TEMP[暂存器]
    end
    subgraph 核心算术逻辑
        ALU[算术逻辑单元]
    end
    subgraph 状态监测
        PSW[状态字寄存器]
    end
    
    R --&gt; MUX
    ACC --&gt; MUX
    MUX --&gt; TEMP
    TEMP --&gt; ALU
    ALU --&gt; ACC
    ALU --&gt; PSW
</code></pre>
<hr />
<h2>3.3 定点加减法运算与核心例题</h2>
<p>本节详细梳理高权重核心考点，包含移位运算、补码加减法、溢出检测、定点乘法以及浮点加减法。</p>
<h3>补充：C 语言中的移位运算</h3>
<p>移位运算是将二进制数整体向左或向右移动若干位。</p>
<h4>移位规则</h4>
<ul>
<li><strong>逻辑左移</strong>：高位移出，低位补 0。</li>
<li><strong>逻辑右移</strong>：低位移出，高位补 0。</li>
<li><strong>算术右移</strong>：低位移出，高位补符号位（即正数补 0，负数补 1）。</li>
</ul>
<h4>移位与乘除法的数学关系</h4>
<p>在不发生截断和溢出的前提下，移位运算等价于乘以或除以 2 的幂：</p>
<ul>
<li>左移 $k$ 位：
$$
x \ll k \approx x \times 2^k
$$</li>
<li>右移 $k$ 位：
$$
x \gg k \approx x / 2^k
$$</li>
</ul>
<p>我们可以利用移位来优化乘除法。例如，计算 $x \times 10$ 可拆解为：
$$
x \times 10 = x \times 8 + x \times 2 = (x \ll 3) + (x \ll 1)
$$</p>
<p>:::important[移位大题步骤与真值解释]
做移位题时，必须按照以下步骤操作：</p>
<ol>
<li>明确机器字长。</li>
<li>确定是逻辑移位还是算术移位。</li>
<li>按照规则补位。</li>
<li>根据无符号数或补码解释最终结果。<strong>同一串二进制，按无符号数和补码解释的真值完全不同。</strong>
:::</li>
</ol>
<h4>应用例题：移位运算下的数值转换</h4>
<blockquote>
<p><strong>例题 1</strong>：已知 8 位二进制数 $x = 1111\ 0100$，求其分别在逻辑右移 1 位与算术右移 1 位后的结果，并解释其真值变化。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>逻辑右移 1 位</strong>：
<ul>
<li>不管原数符号，高位补 0，低位丢弃：$0111\ 1010$。</li>
<li>若 $x$ 原本按无符号数解释，真值为 $244$；右移后为 $0111\ 1010$，真值为 $122$。满足：
$$
244 / 2 = 122
$$</li>
</ul>
</li>
<li><strong>算术右移 1 位</strong>：
<ul>
<li>考虑符号位，原数最高位为 1，故高位补 1，低位丢弃：$1111\ 1010$。</li>
<li>若 $x$ 原本按补码解释，其真值为 $-12$；右移后为 $1111\ 1010$，其补码真值为 $-6$。满足：
$$
-12 / 2 = -6
$$</li>
</ul>
</li>
</ol>
</blockquote>
<h3>3.3.1 补码加减法运算方法</h3>
<p>计算机中整数的加减法统一采用补码进行运算。</p>
<h4>补码表示范围</h4>
<p>对于 $n$ 位补码整数，其能表示的数值范围是：
$$
-2^{n-1} \le x \le 2^{n-1} - 1
$$</p>
<h4>负数补码求法</h4>
<p>正数补码是其本身。负数补码的求解步骤为：写出对应正数的二进制形式，按位取反，末位加 1。</p>
<h4>补码加减法规则</h4>
<ul>
<li><strong>加法</strong>：
$$
[x + y]<em>\text{补} = [x]</em>\text{补} + [y]_\text{补}
$$</li>
<li><strong>减法</strong>：
$$
[x - y]<em>\text{补} = [x]</em>\text{补} + [-y]<em>\text{补} = [x]</em>\text{补} + \sim [y]_\text{补} + 1
$$</li>
</ul>
<h4>可控加减法器硬件实现</h4>
<p>在硬件中，利用异或门的控制特性，使用同一个加法器同时实现加法与减法：</p>
<ul>
<li>设置加减控制信号 $Sub$。</li>
<li>输入操作数 $Y$ 与 $Sub$ 进行异或操作得到 $Y'$，同时将 $Sub$ 接到加法器的最低位进位输入 $Cin$。</li>
<li>当 $Sub = 0$ 时，执行加法：$Y' = Y \oplus 0 = Y$ 且 $Cin = 0$，实现 $X + Y$。</li>
<li>当 $Sub = 1$ 时，执行减法：$Y' = Y \oplus 1 = \sim Y$ 且 $Cin = 1$，实现 $X + \sim Y + 1 = X - Y$。</li>
</ul>
<h4>应用例题：补码加减运算</h4>
<blockquote>
<p><strong>例题 2</strong>：使用 8 位补码计算 $23 - 45$，并给出真值结果。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>确定机器字长为 8 位。</li>
<li>求出操作数的补码：
<ul>
<li>$[23]_\text{补} = 0001\ 0111$</li>
<li>$[45]_\text{补} = 0010\ 1101$</li>
</ul>
</li>
<li>减法转换成加法：$23 - 45 = 23 + (-45)$。求得 $[-45]_\text{补} = \sim 0010\ 1101 + 1 = 1101\ 0010 + 1 = 1101\ 0011$。</li>
<li>二进制相加：
$$
\begin{array}{r}
0001\ 0111 \
<ul>
<li>1101\ 0011 \
\hline
1110\ 1010
\end{array}
$$</li>
</ul>
</li>
<li>结果最高位为 1，说明是负数。转换为真值：对 $1110\ 1010$ 取反得 $0001\ 0101$，加 1 得 $0001\ 0110$（即真值 22）。因此，最终真值为 $-22$，计算未溢出。</li>
</ol>
</blockquote>
<h3>3.3.2 溢出及检测</h3>
<p>溢出是指真实的数学运算结果超出了机器字长所能表示的范围。<strong>进位与溢出是不同的概念，有进位不一定溢出，无进位也不一定不溢出。</strong></p>
<h4>方法一：符号法（单符号位法）</h4>
<ul>
<li>规律：只有当两个同号的数相加（或异号的数相减）时才可能发生溢出。</li>
<li>正溢出：正数 + 正数 = 负数。</li>
<li>负溢出：负数 + 负数 = 正数。</li>
</ul>
<h4>方法二：进位法</h4>
<p>设最高数值位向符号位的进位为 $Cd$，符号位向更高位的进位为 $Cf$。溢出标志 $V$ 的逻辑表达式为：
$$
V = Cd \oplus Cf
$$</p>
<ul>
<li>若 $V = 0$，表示无溢出。</li>
<li>若 $V = 1$，表示发生溢出。</li>
</ul>
<h4>方法三：双符号位法（变形补码）</h4>
<p>使用两位来表示符号：00 表示正数，11 表示负数。
双符号位参与运算后，结果的符号位若为：</p>
<ul>
<li><code>00</code>：结果为正，无溢出。</li>
<li><code>11</code>：结果为负，无溢出。</li>
<li><code>01</code>：正溢出。</li>
<li><code>10</code>：负溢出。</li>
</ul>
<h2>3.4 定点乘法运算</h2>
<h3>3.4.1 原码一位乘法</h3>
<p>原码一位乘法的基本思想是：<strong>符号位单独处理，数值部分取绝对值进行无符号数乘法累加与移位。</strong></p>
<ul>
<li>结果符号：
$$
zf = xf \oplus yf
$$</li>
<li>数值部分运算步骤：
使用部分积寄存器 $A$（初始为 0），被乘数寄存器 $B$（存被乘数绝对值）和乘数寄存器 $C$（存乘数绝对值）。
<ol>
<li>检查乘数最低位 $C_0$。</li>
<li>若 $C_0 = 1$，则部分积 $A = A + B$；若 $C_0 = 0$，则 $A = A + 0$。</li>
<li>部分积 $A$ 与乘数 $C$ 整体联合右移一位（逻辑右移，高位补 0）。</li>
<li>重复上述操作 $n$ 次（$n$ 为数值位位数）。</li>
<li>拼接 $A$ 和 $C$ 得到乘积的数值部分，并加上前一步计算出的符号位。</li>
</ol>
</li>
</ul>
<h4>应用例题：原码一位乘法递推</h4>
<blockquote>
<p><strong>例题 3</strong>：已知 $X = +0.1101$，$Y = -0.1011$，用原码一位乘法计算 $X \times Y$。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>确定符号：$zf = 0 \oplus 1 = 1$，结果为负。</li>
<li>取绝对值：被乘数 $B = 0.1101$，乘数 $C = 0.1011$。设置部分积初始值 $A = 00.0000$。</li>
<li>递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步骤</th>
<th>运算说明</th>
<th>部分积 A</th>
<th>乘数 C</th>
<th>乘数最低位 $C_0$</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>初始状态</td>
<td>$00.0000$</td>
<td>$1011$</td>
<td>1</td>
</tr>
<tr>
<td>第 1 步</td>
<td>$A = A + B$</td>
<td>$00.1101$</td>
<td>$1011$</td>
<td></td>
</tr>
<tr>
<td></td>
<td>联合右移 1 位</td>
<td>$00.0110$</td>
<td>$1101$</td>
<td>1</td>
</tr>
<tr>
<td>第 2 步</td>
<td>$A = A + B$</td>
<td>$01.0011$</td>
<td>$1101$</td>
<td></td>
</tr>
<tr>
<td></td>
<td>联合右移 1 位</td>
<td>$00.1001$</td>
<td>$1110$</td>
<td>0</td>
</tr>
<tr>
<td>第 3 步</td>
<td>$A = A + 0$</td>
<td>$00.1001$</td>
<td>$1110$</td>
<td></td>
</tr>
<tr>
<td></td>
<td>联合右移 1 位</td>
<td>$00.0100$</td>
<td>$1111$</td>
<td>1</td>
</tr>
<tr>
<td>第 4 步</td>
<td>$A = A + B$</td>
<td>$01.0001$</td>
<td>$1111$</td>
<td></td>
</tr>
<tr>
<td></td>
<td>联合右移 1 位</td>
<td>$00.1000$</td>
<td>$1111$</td>
<td></td>
</tr>
</tbody>
</table>
<ol>
<li>拼接部分积 $A$ 与乘数 $C$ 的高位部分，得到乘积的数值部分为 $0.10001111$。</li>
<li>加上符号位，最终结果为 $-0.10001111$，对应的原码表示为 $1.10001111$。</li>
</ol>
</blockquote>
<h3>3.4.2 补码一位乘法</h3>
<p>补码一位乘法通常采用 <strong>Booth 算法</strong>，其最大优势在于符号位可以直接参与运算，且能够直接处理补码形式的输入。</p>
<h5>Booth 算法运算规则</h5>
<p>设被乘数 $[X]<em>\text{补}$，乘数 $[Y]</em>\text{补} = y_f.y_1y_2\dots y_n$。</p>
<ol>
<li>引入一个附加位 $y_{n+1}$，初始值设为 0。</li>
<li>每一轮操作根据乘数相邻的两位 $y_n y_{n+1}$ 的值决定对部分积的操作：</li>
</ol>
<table>
<thead>
<tr>
<th>$y_n$</th>
<th>$y_{n+1}$</th>
<th>对部分积的操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>0</td>
<td>部分积保持不变</td>
</tr>
<tr>
<td>0</td>
<td>1</td>
<td>部分积加上被乘数补码：$A = A + [X]_\text{补}$</td>
</tr>
<tr>
<td>1</td>
<td>0</td>
<td>部分积减去被乘数补码（即加上相反数补码）：$A = A + [-X]_\text{补}$</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>部分积保持不变</td>
</tr>
</tbody>
</table>
<ol>
<li>每次加减操作后，部分积 $A$ 与乘数 $C$ 整体联合右移一位。<strong>注意：由于是补码运算，移位采用算术右移，即高位补部分积自身的符号位。</strong></li>
<li>重复上述“加减 + 右移”操作 $n$ 次。</li>
<li><strong>第 $n+1$ 步（最后一步）</strong>：只根据 $y_f y_1$（此时的最低两位）进行加减判定并操作，<strong>但不再进行右移</strong>。此时得到的拼接结果即为最终的乘积补码 $[X \times Y]_\text{补}$。</li>
</ol>
<h4>应用例题：补码一位乘法（Booth 算法）</h4>
<blockquote>
<p><strong>例题 4</strong>：已知 $[X]<em>\text{补} = 110011$，真值为 $-13$，$[Y]</em>\text{补} = 01011$，真值为 $+11$，即数值位 $n=4$，用 Booth 算法计算 $[X \times Y]_\text{补}$。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>被乘数 $[X]<em>\text{补} = 110011$，则 $[-X]</em>\text{补} = 001101$。</li>
<li>乘数 $[Y]_\text{补} = 01011$，引入附加位 $y_5 = 0$，初始乘数寄存器状态为 $Y = 01011$（含符号位），附加位为 $0$。部分积 $A = 000000$。</li>
<li>递推运算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步骤</th>
<th>判断位</th>
<th>对部分积 A 的操作</th>
<th>运算后部分积 A</th>
<th>乘数 Y 与附加位</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>-</td>
<td>初始状态</td>
<td>$000000$</td>
<td>$01011\ 0$</td>
</tr>
<tr>
<td>第 1 步</td>
<td>$1\ 0$</td>
<td>$A = A + [-X]_\text{补}$</td>
<td>$001101$</td>
<td>$01011\ 0$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$000110$</td>
<td>$10101\ 1$</td>
</tr>
<tr>
<td>第 2 步</td>
<td>$1\ 1$</td>
<td>$A = A + 0$</td>
<td>$000110$</td>
<td>$10101\ 1$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$000011$</td>
<td>$01010\ 1$</td>
</tr>
<tr>
<td>第 3 步</td>
<td>$0\ 1$</td>
<td>$A = A + [X]_\text{补}$</td>
<td>$110110$</td>
<td>$01010\ 1$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$111011$</td>
<td>$00101\ 0$</td>
</tr>
<tr>
<td>第 4 步</td>
<td>$1\ 0$</td>
<td>$A = A + [-X]_\text{补}$</td>
<td>$001000$</td>
<td>$00101\ 0$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$000100$</td>
<td>$00010\ 1$</td>
</tr>
<tr>
<td>第 5 步</td>
<td>$0\ 1$</td>
<td>仅加减不右移：$A = A + [X]_\text{补}$</td>
<td>$110111$</td>
<td>$00010\ 1$</td>
</tr>
</tbody>
</table>
<ol>
<li>拼接部分积 $A$ 与乘数 $Y$ 的高 4 位，得到乘积的补码为：
$$
[X \times Y]_\text{补} = 1101110001
$$</li>
<li>验证结果：乘积真值为 $-13 \times 11 = -143$。$+143$ 的 10 位二进制为 $0010001111_2$，取反加 1 得到补码 $1101110001_2$，计算无误。</li>
</ol>
</blockquote>
<h3>3.4.3 阵列乘法器与补码阵列乘法器</h3>
<p>定点乘法的硬件实现通常从小规模的循环结构（移位加法）向大规模的并行结构演进：</p>
<ul>
<li><strong>阵列乘法器</strong>：一种纯并行的乘法硬件。它通过与门一次性产生所有的位积项（如 $a_i b_j$），然后利用全加器排成的二维阵列，按照权位对齐进行并行累加求和。它的优点是速度极快，但硬件规模随着字长的平方增长。</li>
<li><strong>补码阵列乘法器</strong>：专门用来计算补码的阵列乘法器。有两种设计思路：
<ol>
<li><strong>间接法</strong>：在输入端加上求补级，将补码输入转化为绝对值无符号数，送入普通的无符号阵列乘法器计算，最后在输出端根据输入符号决定是否再次求补。</li>
<li><strong>直接法</strong>：利用直接补码乘法公式（如 Pezaris 阵列乘法器），通过在全加器阵列的某些特定节点使用带负权重的求和单元，直接完成补码的乘积。</li>
</ol>
</li>
</ul>
<hr />
<h2>3.5 浮点运算</h2>
<p>浮点数的加减运算不能直接对尾数相加减，必须遵循标准的五步法则。</p>
<h3>3.5.1 浮点加减法运算</h3>
<pre><code>步骤一：对阶 → 步骤二：尾数加减 → 步骤三：规格化 → 步骤四：舍入 → 步骤五：溢出检测
</code></pre>
<ol>
<li><strong>对阶</strong>：计算阶差 $\Delta E = Ex - Ey$。<strong>对阶原则是小阶向大阶看齐</strong>。将阶码小的数尾数右移 $\Delta E$ 位，阶码增加 $\Delta E$。</li>
<li><strong>尾数加减</strong>：对阶后的两个尾数按照定点补码加减法规则进行求和或求差。</li>
<li><strong>规格化</strong>：
<ul>
<li><strong>右规</strong>：尾数求和溢出时，尾数右移一位，阶码加 1。</li>
<li><strong>左规</strong>：尾数最高有效位不是规格化数时，尾数连续左移，阶码相应减少，直到规格化为止。补码双符号位规格化形式为 <code>00.1xxx</code> 或 <code>11.0xxx</code>。</li>
</ul>
</li>
<li><strong>舍入</strong>：尾数右移丢失低位时需进行精度处理。常用方法有 <strong>0舍1入法</strong> 和 <strong>末位恒置 1 法</strong>。</li>
<li><strong>溢出检测</strong>：通过阶码判断。若规格化或舍入后阶码超出了最大表示范围，则判定为<strong>上溢</strong>（报错）；若低于最小表示范围，则为<strong>下溢</strong>（结果按机器 0 处理）。</li>
</ol>
<h4>应用例题：浮点加减法运算</h4>
<blockquote>
<p><strong>例题 5</strong>：设浮点数格式中阶码为 4 位补码，尾数（含双符号位）为 8 位补码。已知 $X = 0.1101 \times 2^4$，$Y = 0.1011 \times 2^2$，求 $X + Y$（尾数保留 4 位有效位）。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>对阶</strong>：
<ul>
<li>阶码：$Ex = 4$（二进制表示为 $0100$），$Ey = 2$（二进制表示为 $0010$）。</li>
<li>阶差：$\Delta E = 4 - 2 = 2$。</li>
<li>$Y$ 的阶码小，尾数右移 2 位，阶码加 2：$Y = 0.001011 \times 2^4$。</li>
</ul>
</li>
<li><strong>尾数加减</strong>：
<ul>
<li>补码对齐相加：
$$
\begin{array}{r}
00.110100 \
<ul>
<li>00.001011 \
\hline
00.111111
\end{array}
$$</li>
</ul>
</li>
<li>得到中间结果：$00.111111 \times 2^4$。</li>
</ul>
</li>
<li><strong>规格化</strong>：
<ul>
<li>尾数符号位与最高数值位为 <code>00.1</code>，已是规格化数，无需左规或右规。</li>
</ul>
</li>
<li><strong>舍入</strong>（保留 4 位有效位，即保留小数点后 4 位）：
<ul>
<li>尾数部分为 $00.111111$。保留四位后，被丢弃部分为最高位是 1 的 <code>11</code>。</li>
<li>采用 0舍1入法，尾数最低有效位加 1：
$$
00.1111 + 0.0001 = 01.0000
$$</li>
<li>舍入后尾数溢出，需进行右规：尾数右移一位，阶码加 1。</li>
<li>右规后尾数变为 $00.1000$，阶码由 4 变为 5（二进制表示为 $0101$）。</li>
</ul>
</li>
<li><strong>溢出检测</strong>：
<ul>
<li>阶码 5 在 4 位补码表示范围 $[-8, +7]$ 内，未发生溢出。</li>
<li>最终结果为 $0.1000 \times 2^5$。</li>
</ul>
</li>
</ol>
</blockquote>
<hr />
<h2>3.6 历年真题与易错点提示</h2>
<h3>3.7.1 易错陷阱与考点警示</h3>
<p>在复习和应对考试时，需要特别警惕以下高频考点和易错陷阱：</p>
<p>:::important[对阶原则与精度损失]
浮点加减法对阶时必须“小阶向大阶看齐”。若大阶向小阶看齐，需要将尾数左移，这会导致尾数高位有效数字丢失，造成严重的计算错误。小阶向大阶看齐只需右移尾数，虽然会丢弃低位，但可以通过舍入控制精度损失。
:::</p>
<p>:::important[溢出与进位的概念区分]
进位是无符号数在最高位运算时产生溢出范围的进位输出（CF 标志），而溢出是有符号数运算结果超出了有符号表示范围（OF 标志）。在有符号数加法中，最高位产生进位不等于溢出。务必牢记溢出判定公式：
$$
V = Cd \oplus Cf
$$
:::</p>
<p>:::tip[负数算术右移补位]
算术右移为了保持数值的符号，空出的高位必须用符号位填充。对于负数，高位应补 1。切勿习惯性补 0，否则会将负数错误地转换为正数。
:::</p>
<p>:::tip[变形补码在硬件中的实质]
双符号位法在存储器中存储时，为了节省空间，通常只存储单个符号位。双符号位是在数据进入 ALU 准备运算时由硬件临时复制产生的，运算结束后再次检测并转回单符号位存入目标寄存器。
:::</p>
<hr />
<h3>3.7.2 历年真题汇编与详解</h3>
<p>本节汇总了历年期末考试中关于第三章的全部真题，并附带详细解析。</p>
<h4>Q1 (期末真题 选择题)</h4>
<p><strong>题干</strong>：某机字长 16 位，采用补码形式，其中 1 位为符号位，则机器数所能表示的定点整数范围是（    ）。</p>
<p>A. -127 ~ +127
B. -128 ~ +128
C. -32768 ~ +32767
D. -32768 ~ +32768</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$-127 \sim +127$ 是 8 位有符号原码或反码表示的整数范围。</li>
<li><strong>B [错误]</strong>：$-128 \sim +128$ 中负数部分为 8 位补码范围的下限，而正数 $+128$ 超出了 8 位有符号数的表示范围。</li>
<li><strong>C [正确]</strong>：字长 16 位的补码表示中，1 位为符号位，剩余 15 位为数值位。定点整数的表示范围为 $-2^{15} \sim 2^{15}-1$，即 $-32768 \sim +32767$。</li>
<li><strong>D [错误]</strong>：正数最大只能表示到 $+32767$，无法表示 $+32768$。</li>
</ul>
<h4>Q2 (期末真题 选择题)</h4>
<p><strong>题干</strong>：补码定点整数 01010100 算术右移两位后的值为（    ）。</p>
<p>A. 00010101
B. 00010100
C. 11010101
D. 11010100</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：补码算术右移的规则是符号位保持不变，左侧空出位用符号位填充，右侧溢出位丢弃。已知补码为 $01010100$，符号位为 0，右移两位后在左侧补两个 0，丢弃右侧的两位 00，结果为 $00010101$。</li>
<li><strong>B [错误]</strong>：右移后数值位计算错误，低位丢弃后不应为 00。</li>
<li><strong>C [错误]</strong>：左侧补 1 是负数补码算术右移的规则，本题操作数符号位为 0，是正数。</li>
<li><strong>D [错误]</strong>：左侧填充和数值计算均错误。</li>
</ul>
<h4>Q3 (2019秋18级测试 选择题)</h4>
<p><strong>题干</strong>：以下有关运算器的描述中，（    ）是正确的。</p>
<p>A. 只做加法运算
B. 只做算术运算
C. 算术运算与逻辑运算
D. 只做逻辑运算</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：运算器不仅可以执行加法，还可以通过移位和求反配合加法器执行减法、乘法、除法等算术运算，也能进行逻辑运算。</li>
<li><strong>B [错误]</strong>：运算器除了能执行算术运算外，还可以执行与、或、非、异或等逻辑运算。</li>
<li><strong>C [正确]</strong>：运算器是计算机中执行各种算术运算和逻辑运算的部件。</li>
<li><strong>D [错误]</strong>：运算器除了能执行逻辑运算外，还可以执行各类算术运算。</li>
</ul>
<h4>Q4 (2019秋18级测试 选择题)</h4>
<p><strong>题干</strong>：若浮点数用补码表示，则判断运算结果是否为规格化数的方法是（    ）。</p>
<p>A. 阶符与数符相同为规格化数
B. 阶符与数符相异为规格化数
C. 数符与尾数小数点后第一位数字相异为规格化数
D. 数符与尾数小数点后第一位数字相同为规格化数</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：规格化判断仅与尾数的数符及尾数的第一位数值有关，与阶符无关。</li>
<li><strong>B [错误]</strong>：规格化判断与阶符无关。</li>
<li><strong>C [正确]</strong>：补码规格化尾数的特点是：正数规格化形式为 $0.1\dots$，负数规格化形式为 $1.0\dots$。可见，数符与尾数第一位数值相异，即一个是 0 一个是 1。</li>
<li><strong>D [错误]</strong>：数符与尾数第一位数字相同时，正数为 $0.0\dots$，负数为 $1.1\dots$，这属于未规格化的形式。</li>
</ul>
<h4>Q5 (南阳理工C卷 选择题)</h4>
<p><strong>题干</strong>：双符号位补码运算中，结果符号位为 10 表示（    ）。</p>
<p>A. 正溢出
B. 负溢出
C. 正常结果
D. 无溢出</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：双符号位为 01 时表示发生了正溢出。</li>
<li><strong>B [正确]</strong>：在变形补码运算中，双符号位 $S_{f1} S_{f2}$ 分别表示：<code>00</code> 表示结果为正且无溢出，<code>11</code> 表示结果为负且无溢出，<code>01</code> 表示正溢出，<code>10</code> 表示负溢出。</li>
<li><strong>C [错误]</strong>：双符号位不同，即 10 或 01，代表发生了溢出，不能是正常结果。</li>
<li><strong>D [错误]</strong>：双符号位不一致说明已经发生溢出。</li>
</ul>
<h4>Q6 (期末真题 选择题)</h4>
<p><strong>题干</strong>：算术逻辑单元的核心功能部件是（    ）。</p>
<p>A. 累加寄存器
B. 移位器
C. 加法器
D. 指令寄存器</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：累加寄存器属于运算器中的数据暂存部件，主要用来保存操作数或运算结果，并非 ALU 的核心计算功能部件。</li>
<li><strong>B [错误]</strong>：移位器用于数据移位，但在 ALU 中，各种算术运算最终都是基于加法器实现的。</li>
<li><strong>C [正确]</strong>：加法器是 ALU 的核心部件，其他算术运算（如减法、乘法、除法）均可通过加法与移位等操作的组合来实现。</li>
<li><strong>D [错误]</strong>：指令寄存器是控制器的部件，用于存放当前正在执行的指令，不属于 ALU。</li>
</ul>
<h4>Q7 (期末真题 选择题)</h4>
<p><strong>题干</strong>：在定点运算器中，无论是采用双符号位还是采用单符号位，必须有（    ）。</p>
<p>A. 译码电路，它一般用与非门来实现
B. 编码电路，它一般用或非门来实现
C. 溢出判断电路，它一般用异或门来实现
D. 移位电路，它一般用与或非门来实现</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：译码电路用于对指令或地址进行译码，与单双符号位溢出判断没有直接联系。</li>
<li><strong>B [错误]</strong>：编码电路用于状态或数据的编码，非溢出判断的核心组件。</li>
<li><strong>C [正确]</strong>：在单符号位加减运算中，溢出判断通常基于最高数值位进位与符号位进位是否相异，即采用异或逻辑；在双符号位中，基于两个符号位是否相异，同样采用异或逻辑。因此，溢出判断电路是必需的，且一般用异或门来实现。</li>
<li><strong>D [错误]</strong>：移位电路用于数据的移位，与单双符号位溢出判断无直接依赖关系。</li>
</ul>
<h4>Q8 (期末真题 选择题)</h4>
<p><strong>题干</strong>：在定点机中执行算术运算时会产生溢出，其根本原因是（    ）。</p>
<p>A. 主存容量不够
B. 运算结果无法表示
C. 操作数地址过大
D. 栈溢出</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：主存容量限制的是程序和数据的存储空间，与算术运算是否溢出无关。</li>
<li><strong>B [正确]</strong>：溢出的根本原因在于运算结果超出了机器字长所能表示的数值范围，即当前的运算结果无法在定点机中表示。</li>
<li><strong>C [错误]</strong>：操作数地址过大会导致寻址错误或越界异常，与算术运算发生溢出无关。</li>
<li><strong>D [错误]</strong>：栈溢出是指程序运行时的调用栈空间耗尽，与算术运算中的数值溢出是完全不同的概念。</li>
</ul>
<h4>Q9 (期末真题 选择题)</h4>
<p><strong>题干</strong>：在原码加减交替除法中，符号位单独处理，参加操作的数是（    ）。</p>
<p>A. 原码
B. 绝对值
C. 绝对值的补码
D. 补码</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：原码直接参与除法中的加减运算在硬件中不便实现，且减法需要转换为加法。</li>
<li><strong>B [错误]</strong>：绝对值是正数真值，但硬件上减去一个绝对值需要用其补码的形式来进行加法。</li>
<li><strong>C [正确]</strong>：原码加减交替除法符号位与数值位分开处理。在计算商值时，数值部分做绝对值除法。为了在硬件中将减法操作统一为加法操作，减去商或除数时需要将除数绝对值求补，故实际参加加法器操作的数是绝对值的补码。</li>
<li><strong>D [错误]</strong>：采用整机补码时符号位会直接参与运算，而原码除法要求符号位单独处理。</li>
</ul>
<h4>Q10 (期末真题 选择题)</h4>
<p><strong>题干</strong>：加法器采用先行进位的目的是（    ）。</p>
<p>A. 优化加法器的结构
B. 节省器材
C. 加速传递进位信号
D. 增强加法器结构</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：先行进位会使加法器的电路结构变得更加复杂，没有简化或优化结构。</li>
<li><strong>B [错误]</strong>：先行进位需要引入额外的超前进位产生逻辑，增加了逻辑门元件，消耗了更多器材。</li>
<li><strong>C [正确]</strong>：在普通行波进位加法器中，高位的加法必须等待低位的进位信号依次传过来，导致速度受限。先行进位通过硬件逻辑直接算出各高位的进位输入，从而极大地加速了进位信号的传递，提高了运算速度。</li>
<li><strong>D [错误]</strong>：所谓增强结构表达含糊，实际硬件设计的直接指标是速度提升。</li>
</ul>
<h4>Q11 (期末真题 选择题)</h4>
<p><strong>题干</strong>：考虑以下 C 语言代码：</p>
<pre><code>unsigned short usi = 65535;
short si = usi;
</code></pre>
<p>执行上述程序段后，si 的值是（    ）。</p>
<p>A. -1
B. -32767
C. -32768
D. -65535</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：16位无符号数 <code>usi = 65535</code> 的二进制表示为 $1111\ 1111\ 1111\ 1111_2$（全 1）。当它强制转换为有符号 <code>short</code> 类型变量 <code>si</code> 时，二进制序列保持不变。在有符号 16 位补码中，全 1 序列的真值对应为 $-1$。</li>
<li><strong>B [错误]</strong>：$-32767$ 的二进制补码为 $1000\ 0000\ 0000\ 0001_2$。</li>
<li><strong>C [错误]</strong>：$-32768$ 的二进制补码为 $1000\ 0000\ 0000\ 0000_2$。</li>
<li><strong>D [错误]</strong>：16 位有符号 <code>short</code> 类型的表示范围为 $-32768 \sim 32767$，根本无法表示 $-65535$。</li>
</ul>
<h4>Q12 (期末真题 选择题)</h4>
<p><strong>题干</strong>：假定一个十进制数为 -66，按补码形式存放在一个 8 位寄存器中，该寄存器的内容用十六进制表示为（    ）。</p>
<p>A. C2H
B. BEH
C. BDH
D. 42H</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：十六进制 $\text{C2H}$ 对应的二进制为 $1100\ 0010_2$，表示的真值为 $-62$。</li>
<li><strong>B [正确]</strong>：十进制数 $66$ 的二进制无符号表示为 $0100\ 0010_2$。对于负数 $-66$，求其 8 位补码应为绝对值按位取反加 1，即对 $0100\ 0010_2$ 取反得到 $1011\ 1101_2$，再加 1 得到 $1011\ 1110_2$。转换为十六进制，高 4 位为 B，低 4 位为 E，即 $\text{BEH}$。</li>
<li><strong>C [错误]</strong>：十六进制 $\text{BDH}$ 对应的二进制为 $1011\ 1101_2$，是 $-66$ 补码求取过程中取反未加 1 的中间结果，即反码表示。</li>
<li><strong>D [错误]</strong>：十六进制 $\text{42H}$ 对应的二进制为 $0100\ 0010_2$，表示的真值为 $+66$。</li>
</ul>
<hr />
<h4>填空与判断题</h4>
<h4>Q13 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：在机器码 ________ 和 ________ 中，零的编码是唯一的。</p>
<p><strong>解析</strong>：</p>
<p>原码和反码中的 0 均有正 0 和负 0 两种不同的编码。补码和移码中 0 的编码是唯一的。</p>
<p><strong>答案</strong>：补码、移码。</p>
<h4>Q14 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：浮点加、减运算的步骤是 <strong><strong><strong><strong>、</strong></strong></strong></strong>、规格化、舍入、溢出判断。</p>
<p><strong>解析</strong>：</p>
<p>浮点加减法五步法为：对阶、尾数求和、规格化、舍入、溢出判断。</p>
<p><strong>答案</strong>：对阶、尾数求和。</p>
<h4>Q15 (2022-2023学年 填空题)</h4>
<p><strong>题干</strong>：原码乘法时，符号位单独处理，乘积的符号位是由两个操作数符号进行 ________ 运算得到的。</p>
<p><strong>解析</strong>：</p>
<p>原码乘法的符号计算规则是同号相乘为正，异号相乘为负，在逻辑电路上通过对两个符号位进行异或运算实现。</p>
<p><strong>答案</strong>：异或。</p>
<h4>Q16 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：算术逻辑单元的功能包括算术运算和 ________ 运算。</p>
<p><strong>解析</strong>：</p>
<p>ALU 即算术逻辑单元，提供算术运算和逻辑运算两大类基本运算。</p>
<p><strong>答案</strong>：逻辑。</p>
<h4>Q17 (期末真题 填空题)</h4>
<p><strong>题干</strong>：CPU 主要由运算器、________ 和 ________ 组成。（或：CPU 主要由运算器和 ________ 组成。）</p>
<p><strong>解析</strong>：</p>
<p>现代 CPU 内部集成了运算器、控制器以及高速缓冲存储器 Cache。</p>
<p><strong>答案</strong>：控制器、Cache（或：控制器）。</p>
<h4>Q18 (期末真题 填空题)</h4>
<p><strong>题干</strong>：补码定点整数 01010101 左移两位后的值为 ________。</p>
<p><strong>解析</strong>：</p>
<p>补码定点数左移规则为高位丢弃，低位补 0。01010101 左移两位后变为 01010100。</p>
<p><strong>答案</strong>：01010100。</p>
<h4>Q19 (期末真题 填空题)</h4>
<p><strong>题干</strong>：8 位原码能表示的不同数据有 ________ 个。</p>
<p><strong>解析</strong>：</p>
<p>8 位二进制编码共有 256 个。在原码中，正零 $00000000$ 与负零 $10000000$ 对应同一个数学真值 0。因此，所能表示的不同数据个数为 $256 - 1 = 255$ 个。</p>
<p><strong>答案</strong>：255。</p>
<h4>Q20 (2022-2023学年 判断题)</h4>
<p><strong>题干</strong>：采用变形补码进行加减运算可以避免溢出。（    ）</p>
<p><strong>解析</strong>：</p>
<p>变形补码（双符号位）的作用是简化溢出检测电路，使硬件更容易判断是否发生溢出，但无法阻止或避免溢出的发生。</p>
<p><strong>答案</strong>：错。</p>
<h4>Q21 (期末真题 判断题)</h4>
<p><strong>题干</strong>：溢出判断电路一般用与或非门来实现。（    ）</p>
<p><strong>解析</strong>：</p>
<p>溢出判断的逻辑基础是异或，因此硬件电路上一般使用异或门实现。</p>
<p><strong>答案</strong>：错。</p>
<hr />
<h4>计算与分析大题</h4>
<h4>Q22 (2019秋18级测试 计算题)</h4>
<p><strong>题干</strong>：已知有符号数 $X = +1101_2 \times 2^{-4}$，$Y = -1001_2 \times 2^{-4}$，用变形补码计算 $[X+Y]_\text{补}$，并判断是否发生溢出。</p>
<p><strong>解析</strong>：</p>
<p>写出 $X$ 与 $Y$ 的小数形式：$X = +0.1101$，$Y = -0.1001$。
采用变形补码（双符号位）表示：
$$
[X]<em>\text{补} = 00.1101
$$
$$
[Y]</em>\text{补} = 11.0111
$$</p>
<p>变形补码求和计算：
$$
\begin{array}{r}
00.1101 \</p>
<ul>
<li>11.0111 \
\hline
100.0100
\end{array}
$$
最左边的进位 1 丢弃，得到结果补码：
$$
[X+Y]_\text{补} = 00.0100
$$
结果的双符号位为 <code>00</code>，表示运算结果为正数，且无溢出。</li>
</ul>
<h4>Q23 (南阳理工C卷 计算题)</h4>
<p><strong>题干</strong>：用双符号位补码运算方法计算 $x = 0.1010$，$y = 0.1100$ 的和，并分析溢出状态。</p>
<p><strong>解析</strong>：</p>
<p>采用双符号位补码表示：
$$
[x]<em>\text{补} = 00.1010
$$
$$
[y]</em>\text{补} = 00.1100
$$
两数相加计算：
$$
\begin{array}{r}
00.1010 \</p>
<ul>
<li>00.1100 \
\hline
01.0110
\end{array}
$$
得到结果双符号位为 <code>01</code>，符号位不同。由于符号位是 <code>01</code>，表示运算结果发生了正溢出。</li>
</ul>
<h4>Q24 (2020-2021 B卷 计算题)</h4>
<p><strong>题干</strong>：已知定点小数 $x = 0.1000$，$y = -0.0101$，求 $[x]<em>\text{补}$、$[y]</em>\text{补}$、$[x+y]_\text{补}$，并判断是否发生溢出。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>根据补码定义，正数的补码是其本身，负数的补码为绝对值取反加 1：
$$
[x]<em>\text{补} = 0.1000
$$
$$
[y]</em>\text{补} = 1.1011
$$</li>
<li>补码相加：
$$
\begin{array}{r}
0.1000 \</li>
</ol>
<ul>
<li>1.1011 \
\hline
10.0011
\end{array}
$$
最高位进位 1 丢弃，得到：
$$
[x+y]_\text{补} = 0.0011
$$</li>
</ul>
<ol>
<li>由于相加的两个操作数一个为正数，一个为负数，异号数相加绝不可能产生溢出。进位丢弃后符号位为 0，结果正确，无溢出。</li>
</ol>
<h4>Q25 (2022-2023学年 计算题)</h4>
<p><strong>题干</strong>：已知定点整数 $A = -1001_2$，$B = -0101_2$，求 $[A+B]_\text{补}$。</p>
<p><strong>解析</strong>：</p>
<p>设字长为 5 位（含 1 位符号位）：</p>
<ol>
<li>求出各操作数的补码：</li>
</ol>
<ul>
<li>$A = -1001$，其绝对值正二进制为 $0,1001$，取反加 1 后补码为：
$$
[A]_\text{补} = 1,0111
$$</li>
<li>$B = -0101$，其绝对值正二进制为 $0,0101$，取反加 1 后补码为：
$$
[B]_\text{补} = 1,1011
$$</li>
</ul>
<ol>
<li>补码相加：
$$
\begin{array}{r}
1,0111 \</li>
</ol>
<ul>
<li>1,1011 \
\hline
11,0010
\end{array}
$$
根据模 $2^{4+1}$ 的规则，丢弃最左侧产生的进位 1，得到：
$$
[A+B]_\text{补} = 1,0010
$$</li>
</ul>
<h4>Q26 (2020-2021 A卷 计算题)</h4>
<p><strong>题干</strong>：已知 $[X]<em>\text{补} = 1.10010$，$[Y]</em>\text{原} = 1.01110$，求 $X$ 和 $Y$ 的真值。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>对 $[X]_\text{补} = 1.10010$：</li>
</ol>
<ul>
<li>符号位为 1，是负数。</li>
<li>求真值绝对值：补码的数值位取反加 1 得到：
$$
\sim 10010 + 1 = 01101 + 1 = 01110
$$</li>
<li>加上符号，其真值为 $X = -0.01110_2$。</li>
</ul>
<ol>
<li>对 $[Y]_\text{原} = 1.01110$：</li>
</ol>
<ul>
<li>符号位为 1，是负数，原码数值位即代表绝对值。</li>
<li>其真值为 $Y = -0.01110_2$。</li>
</ul>
<h4>Q27 (南阳理工C卷 计算题)</h4>
<p><strong>题干</strong>：请说明浮点数加减运算中对阶的原则，并以 $A = 0.1101 \times 2^3$，$B = 0.1010 \times 2^1$ 计算 $A+B$ 的值。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>对阶原则</strong>：对阶时必须遵循“小阶向大阶看齐”的原则。即通过右移小阶数的尾数，并相应增加其阶码，使两个数的阶码一致。这样可以确保尾数右移时丢弃的是低位，将精度损失降到最低；若大阶向小阶看齐，会导致尾数左移而丢失高位有效数字，造成严重计算错误。</li>
<li><strong>加法计算</strong>：</li>
</ol>
<ul>
<li>阶码：$Ex = 3$，$Ey = 1$。阶差为 $\Delta E = 3 - 1 = 2$。</li>
<li>$B$ 的阶码小，尾数右移两位，阶码加 2：
$$
B = 0.001010 \times 2^3
$$</li>
<li>尾数对齐相加：
$$
\begin{array}{r}
0.110100 \</li>
</ul>
<ul>
<li>0.001010 \
\hline
0.111110
\end{array}
$$</li>
</ul>
<ul>
<li>得到结果为 $0.111110 \times 2^3$。尾数符号与最高有效位相异（<code>0.1</code>），已是规格化数，无溢出，故最终结果为 $0.11111 \times 2^3$。</li>
</ul>
<hr />
<h2>习题3：课后习题详解</h2>
<p>以下为本章教材配属的全部课后习题及详细解答。</p>
<p><img src="https://img.picr2.online/1782647916660_image.webp" alt="课后习题1" />
<img src="https://img.picr2.online/1782647916962_image.webp" alt="课后习题2" />
<img src="https://img.picr2.online/1782647917661_image.webp" alt="课后习题3" /></p>
<hr />
<h3>习题3.1 解释下列名词</h3>
<table>
<thead>
<tr>
<th>名词</th>
<th>详细物理与学术解释</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>全加器</strong></td>
<td>能完成两个本位二进制数和来自低位的进位相加的电路。输入为 $A_i$、$B_i$、$C_i$，输出为本位和 $S_i$ 与向高位的进位 $C_{i+1}$。</td>
</tr>
<tr>
<td><strong>半加器</strong></td>
<td>只能完成两个本位二进制数相加的电路，没有低位进位输入。输入为 $A_i$、$B_i$，输出为和 $S_i$ 与进位 $C_{i+1}$。</td>
</tr>
<tr>
<td><strong>进位生成函数</strong></td>
<td>表示本位两个操作数本身是否一定产生进位，通常记为 $G_i = A_i B_i$。只要 $A_i = B_i = 1$，本位必然向高位产生进位。</td>
</tr>
<tr>
<td><strong>进位传递函数</strong></td>
<td>表示本位是否能把低位进位传递到高位，通常记为 $P_i = A_i \oplus B_i$。当 $P_i = 1$ 时，若低位有进位输入，则该进位会继续传到高位。</td>
</tr>
<tr>
<td><strong>算术移位</strong></td>
<td>针对带符号数的移位。右移时保持符号位不变，左移时低位补 0。算术右移通常相当于带符号数除以 2，算术左移通常相当于乘以 2，但可能溢出。</td>
</tr>
<tr>
<td><strong>逻辑移位</strong></td>
<td>不考虑符号意义的移位。左移或右移后空出的位一律补 0。常用于无符号数或逻辑数据。</td>
</tr>
<tr>
<td><strong>阵列乘法器</strong></td>
<td>由大量加法单元和与门按阵列结构连接而成的并行乘法器。它可以同时生成多个部分积，并通过阵列加法网络完成求和，速度快但硬件开销大。</td>
</tr>
<tr>
<td><strong>原码恢复余数除法</strong></td>
<td>原码除法的一种方法。每次先用部分余数减除数，若结果为负，则说明不够减，商上 0，并把余数恢复为减除数前的值；若结果非负，则商上 1。</td>
</tr>
<tr>
<td><strong>原码不恢复余数法</strong></td>
<td>原码除法的一种改进方法。若某次减除数后余数为负，不立即恢复，而是在下一步改为加除数。这样减少了恢复操作，提高了速度。</td>
</tr>
<tr>
<td><strong>阵列除法</strong></td>
<td>用规则排列的加减单元组成阵列结构来实现除法的硬件方法。它类似阵列乘法器，利用并行结构完成部分余数的加减和商位生成。</td>
</tr>
<tr>
<td><strong>串行进位</strong></td>
<td>也叫行波进位。低位加法器产生的进位依次传到高位，高位必须等待低位进位到达后才能计算，结构简单但速度慢。</td>
</tr>
<tr>
<td><strong>先行进位</strong></td>
<td>也叫并行进位或超前进位。通过进位生成函数 $G_i$ 和进位传递函数 $P_i$ 直接计算各位进位，不必逐位等待，因此加法速度快。</td>
</tr>
<tr>
<td><strong>对阶</strong></td>
<td>浮点数加减前，使两个浮点数的阶码相同的过程。通常将阶码较小的数的尾数右移，使其阶码增大到与另一个数相同。</td>
</tr>
<tr>
<td><strong>规格化</strong></td>
<td>把浮点运算结果调整为规定尾数形式的过程。对原码尾数，通常要求最高有效位为 1；对补码尾数，正数要求形如 $0.1\cdots$，负数要求形如 $1.0\cdots$。</td>
</tr>
<tr>
<td><strong>保留附加位</strong></td>
<td>在浮点运算过程中，为减少对阶、右移和舍入造成的误差，临时保留尾数低位之外的若干额外位，如保护位、舍入位、粘滞位等。</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题3.2 选择题（考研真题）</h3>
<h4>( 1 ) 一个 C 语言程序在一台 32 位机器上运行，程序中定义了 3 个变量 $x, y, z$，其中 $x$ 和 $z$ 是 <code>int</code> 型，$y$ 为 <code>short</code> 型。当 $x = 127, y = -9$ 时，执行赋值语句 $z = x+y$ 后，$x, y, z$ 的值分别是 ______。</h4>
<p>A. $x = \text{0000007FH}$, $y = \text{FFF9H}$, $z = \text{00000076H}$
B. $x = \text{0000007FH}$, $y = \text{FFF9H}$, $z = \text{FFFF0076H}$
C. $x = \text{0000007FH}$, $y = \text{FFF7H}$, $z = \text{FFFF0076H}$
D. $x = \text{0000007FH}$, $y = \text{FFF7H}$, $z = \text{00000076H}$</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$y$ 值为 $-9$，其 16 位补码表示为 $\text{FFF7H}$，即对 $+9$ 对应的二进制 $\text{0009H}$ 按位取反加 1 得到 $\text{FFF7H}$，选项中 $y = \text{FFF9H}$ 错误。</li>
<li><strong>B [错误]</strong>：$y$ 值表示错误；同时，$z = x+y = 127 + (-9) = 118$，作为 32 位有符号整数，其十六进制表示应为 $\text{00000076H}$，选项中 $z = \text{FFFF0076H}$ 错误。</li>
<li><strong>C [错误]</strong>：$z$ 值的十六进制表示错误，$\text{FFFF0076H}$ 不是 $118$ 的 32 位补码形式。</li>
<li><strong>D [正确]</strong>：$x$ 为 32 位有符号数，真值 $127$ 表示为 $\text{0000007FH}$；$y$ 为 16 位有符号数，$-9$ 表示为 $\text{FFF7H}$；执行 $z = x+y$ 时，短整型 $y$ 符号扩展为 32 位（$\text{FFFFFFF7H}$）后再与 $x$ 相加，计算得到 $z = 118$，其 32 位补码表示为 $\text{00000076H}$。</li>
</ul>
<h4>( 2 ) 假定有 4 个整数用 8 位补码分别表示为 $r_1 = \text{FEH}$，$r_2 = \text{F2H}$，$r_3 = \text{90H}$，$r_4 = \text{F8H}$，若将运算结果存放在一个 8 位的寄存器中，则下列运算会发生溢出的是 ______。</h4>
<p>A. $r_1 \times r_2$
B. $r_2 \times r_3$
C. $r_1 \times r_4$
D. $r_2 \times r_4$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$r_1 = \text{FEH}$ 的真值为 $-2$，$r_2 = \text{F2H}$ 的真值为 $-14$。两者相乘得 $r_1 \times r_2 = (-2) \times (-14) = 28$，在 8 位有符号补码表示的数值范围 $[-128, 127]$ 内，未发生溢出。</li>
<li><strong>B [正确]</strong>：$r_2 = \text{F2H}$ 的真值为 $-14$，$r_3 = \text{90H}$ 的真值为 $-112$。两者相乘得 $r_2 \times r_3 = (-14) \times (-112) = 1568$，超出了 8 位有符号数表示的最大值 $127$，故会发生溢出。</li>
<li><strong>C [错误]</strong>：$r_1 = \text{FEH}$ 的真值为 $-2$，$r_4 = \text{F8H}$ 的真值为 $-8$。两者相乘得 $r_1 \times r_4 = (-2) \times (-8) = 16$，在 $[-128, 127]$ 范围内，未发生溢出。</li>
<li><strong>D [错误]</strong>：$r_2 = \text{F2H}$ 的真值为 $-14$，$r_4 = \text{F8H}$ 的真值为 $-8$。两者相乘得 $r_2 \times r_4 = (-14) \times (-8) = 112$，在 $[-128, 127]$ 范围内，未发生溢出。</li>
</ul>
<h4>( 3 ) 某字长为 8 位的计算机中，已知整型变量 $x, y$ 的机器数分别为 $[x]<em>\text{补} = 11110100$、$[y]</em>\text{补} = 10110000$。若整型变量 $z = 2x + y/2$，则 $z$ 的机器数为 ______。</h4>
<p>A. $11000000$
B. $00100100$
C. $10101010$
D. 溢出</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：已知 $[x]<em>\text{补} = 11110100_2$，其最高位为 1，表示负数，真值 $x = -12$，则 $2x = -24$；$[y]</em>\text{补} = 10110000_2$，最高位为 1，表示负数，真值 $y = -80$，则 $y/2 = -40$。代入得 $z = 2x + y/2 = -24 + (-40) = -64$。在 8 位有符号整型表示范围 $[-128, 127]$ 内，计算未发生溢出，$-64$ 的 8 位补码机器数为 $11000000_2$。</li>
<li><strong>B [错误]</strong>：机器数 $00100100_2$ 的符号位为 0，代表正数 $+36$，而数学结果应为负数 $-64$。</li>
<li><strong>C [错误]</strong>：机器数 $10101010_2$ 的符号位为 1，数值部分取反加 1 得到 $01010110_2$，代表真值 $-86$，与 $-64$ 不符。</li>
<li><strong>D [错误]</strong>：由于结果 $-64$ 在 8 位有符号数的表示范围 $[-128, 127]$ 内，所以并没有发生溢出。</li>
</ul>
<h4>( 4 ) 假定带符号整数采用补码表示，若 <code>int</code> 型变量 $x$ 和 $y$ 的机器数分别是 $\text{FFFF FFDFH}$ 和 $\text{0000 0041H}$，则 $x, y$ 的值以及 $x - y$ 的机器数分别是 ______。</h4>
<p>A. $x = -65, y = 41$, $x - y$ 的机器数溢出
B. $x = -33, y = 65$, $x - y$ 的机器数为 $\text{FFFF FF9DH}$
C. $x = -33, y = 65$, $x - y$ 的机器数为 $\text{FFFF FF9EH}$
D. $x = -65, y = 41$, $x - y$ 的机器数为 $\text{FFFF FF96H}$</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$x = \text{FFFF FFDFH}$，符号位为 1，其十进制真值为 $-33$，并非 $-65$。另外，32 位补码运算 $-33 - 65 = -98$ 在表示范围内，没有发生溢出。</li>
<li><strong>B [错误]</strong>：$-98$ 的 32 位补码机器数应为 $\text{FFFF FF9EH}$，而 $\text{FFFF FF9DH}$ 对应的十进制真值为 $-99$。</li>
<li><strong>C [正确]</strong>：$x = \text{FFFF FFDFH}$ 的最高位为 1，其十进制真值为 $-33$；$y = \text{0000 0041H}$ 的最高位为 0，其十进制真值为 $65$。计算 $x - y = -33 - 65 = -98$。$+98$ 对应的十六进制为 $\text{0000 0062H}$，对其按位取反加 1 得到 $-98$ 的 32 位补码机器数为 $\text{FFFF FF9EH}$。</li>
<li><strong>D [错误]</strong>：$x$ 的真值应为 $-33$，而非 $-65$；$y$ 的真值应为 $65$，而非 $41$。</li>
</ul>
<h4>( 5 ) 已知 $x, y$ 为 <code>int</code> 型，当 $x = 100, y = 200$ 时，执行“x 减 y”指令得到的溢出标志 OF 和借位标志 CF 分别为 0、1，那么当 $x = 10, y = -20$ 时，执行该指令得到的 OF 和 CF 分别是 ______。</h4>
<p>A. $\text{OF} = 0, \text{CF} = 0$
B. $\text{OF} = 0, \text{CF} = 1$
C. $\text{OF} = 1, \text{CF} = 0$
D. $\text{OF} = 1, \text{CF} = 1$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：在无符号数解释下，被减数 $\text{0000 000AH}$ 小于减数 $\text{FFFF FFECH}$，无符号减法运算必须产生借位，故 $\text{CF} = 1$。</li>
<li><strong>B [正确]</strong>：当 $x = 10, y = -20$ 时，数学运算 $x - y = 10 - (-20) = 30$ 未超出 32 位有符号整数的表示范围，故有符号溢出标志 $\text{OF} = 0$。进行机器减法时，操作数均以补码表示：$x = \text{0000 000AH}$，$-20$ 的补码 $y = \text{FFFF FFECH}$。将它们作为无符号数相减，被减数 $x$ 小于减数 $y$，需要向高位借位，因此无符号借位标志 $\text{CF} = 1$。</li>
<li><strong>C [错误]</strong>：因为 $x - y = 30$ 没有超出有符号整数范围，所以有符号溢出标志 $\text{OF}$ 应为 0，且无符号借位标志 $\text{CF}$ 应当为 1。</li>
<li><strong>D [错误]</strong>：此运算并没有发生有符号数溢出，所以有符号溢出标志 $\text{OF}$ 应为 0。</li>
</ul>
<h4>( 6 ) 整数 $x$ 的机器数为 1101 1000，分别对 $x$ 进行逻辑右移 1 位和算术右移 1 位操作，得到的机器数各是 ______。</h4>
<p>A. 1110 1100、1110 1100
B. 0110 1100、1110 1100
C. 1110 1100、0110 1100
D. 0110 1100、0110 1100</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：逻辑右移时，不论原数符号如何，高位一律填 0。因此逻辑右移 1 位后的结果应为 $0110\ 1100$，而非 $1110\ 1100$。</li>
<li><strong>B [正确]</strong>：对 $x = 1101\ 1000$ 进行逻辑右移 1 位，最高位填充 0，结果为 $0110\ 1100$；进行算术右移 1 位，需保持符号位不变，最高位填充原有最高符号位 1，结果为 $1110\ 1100$。</li>
<li><strong>C [错误]</strong>：该选项将逻辑右移与算术右移的结果弄反了。</li>
<li><strong>D [错误]</strong>：算术右移时，由于原数最高位为 1，高位应填充符号位 1。因此算术右移 1 位后的结果应为 $1110\ 1100$，而非 $0110\ 1100$。</li>
</ul>
<h4>( 7 ) 下列关于整数乘法运算的叙述中，错误的是 ______。</h4>
<p>A. 用阵列乘法器实现乘法运算可以在一个时钟周期内完成
B. 用 ALU 和移位器实现的乘运算无法在一个时钟周期内完成
C. 变量与常数的乘运算可编译优化为若干位移位及加减运算指令
D. 两个变量的乘运算无法编译为移位及加法等指令的循环实现</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：此叙述是正确的。阵列乘法器采用纯组合逻辑电路设计，只要控制时钟周期大于其最长路径延迟，就可以在一个时钟周期内完成乘法运算。</li>
<li><strong>B [错误]</strong>：此叙述是正确的。使用 ALU 和移位器实现乘法时，需要依次执行多次“判断、相加、移位”的多步操作，无法在单个时钟周期内完成。</li>
<li><strong>C [错误]</strong>：此叙述是正确的。编译器常将变量与常数的乘法运算转换为更高效的移位与加减指令序列进行优化。</li>
<li><strong>D [正确]</strong>：此叙述是错误的。如果没有提供专用的硬件乘法指令，两个变量相乘完全可以通过逻辑移位、数值位条件判断和累加指令构成的循环结构来实现。</li>
</ul>
<h4>( 8 ) 浮点数加减运算过程一般包括对阶、尾数运算、规格化、舍入和判断溢出等步骤。设浮点数的阶码和尾数均采用补码表示，且位数分别为 5 位和 7 位（均含 2 位符号位）。若有两个数 $X = 2^7 \times 29/32, Y = 2^5 \times 5/8$，则用浮点加法计算 $X+Y$ 的最终结果是 ______。</h4>
<p>A. 001111100010
B. 001110100010
C. 010000010001
D. 发生溢出</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：计算过程中，阶码发生上溢。溢出发生时无法以正常的规格化浮点形式输出数值。</li>
<li><strong>B [错误]</strong>：同上，计算过程中阶码上溢，该选项并非正确计算结果。</li>
<li><strong>C [错误]</strong>：同上，计算中阶码上溢，结果不可能是此正常浮点编码。</li>
<li><strong>D [正确]</strong>：$X = 2^7 \times (29/32)$，尾数补码为 $00.11101_2$，阶码为 $00111_2$；$Y = 2^5 \times (5/8)$，尾数补码为 $00.10100_2$，阶码为 $00101_2$。对阶时小阶向大阶看齐，$Y$ 的尾数右移两位变为 $00.00101_2$，其阶码增加到 $7$。尾数进行求和：$00.11101_2 + 00.00101_2 = 01.00010_2$。因尾数溢出，需进行右规：尾数右移一位变为 $00.10001_2$，阶码加 1。原阶码 $7$（$00111_2$）加 1 后变为 $8$（$01000_2$）。由于 5 位双符号位阶码最大能表示的有符号整数为 $+7$（即 $00111_2$），因此阶码正溢出，运算结果发生溢出。</li>
</ul>
<h4>( 9 ) 下列有关浮点数加减运算的叙述中，正确的是 ______。</h4>
<p>Ⅰ. 对阶操作不会引起阶码上溢或下溢
Ⅱ. 右规和尾数舍入都可能引起阶码上溢
Ⅲ. 左规时可能引起阶码下溢
Ⅳ. 尾数溢出时结果不一定溢出
A. 仅 Ⅱ、Ⅲ
B. 仅 Ⅰ、Ⅱ、Ⅳ
C. 仅 Ⅰ、Ⅲ、Ⅳ
D. Ⅰ、Ⅱ、Ⅲ、Ⅳ</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：此选项只包含了 Ⅱ 和 Ⅲ，遗漏了同样正确的 Ⅰ 和 Ⅳ。</li>
<li><strong>B [错误]</strong>：此选项遗漏了 Ⅲ。左规时由于尾数左移阶码需要减 1，当阶码减小后超出表示下限时，会发生下溢，故 Ⅲ 正确。</li>
<li><strong>C [错误]</strong>：此选项遗漏了 Ⅱ。右规与舍入，也就是舍入进位若导致尾数溢出而需要右规，都会导致阶码加 1，进而可能使阶码上溢，故 Ⅱ 正确。</li>
<li><strong>D [正确]</strong>：分析如下：Ⅰ. 对阶原则为小阶向大阶看齐，只将小阶码增大至原本已合法存在的大阶码，因此不可能发生溢出；Ⅱ. 右规和尾数舍入若导致尾数右移则需将阶码加 1，可能导致阶码超出上限而上溢；Ⅲ. 左规时尾数左移需要将阶码减 1，可能导致阶码低于下限而下溢；Ⅳ. 尾数求和溢出，即双符号位不一致时，可以通过右规并将阶码加 1 进行调整，若阶码未超出上限，则结果不发生溢出。故四项均正确。</li>
</ul>
<hr />
<h3>习题3.3 简答题</h3>
<h4>( 1 ) 为什么采用并行进位能提高加法器的运算速度？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>消除了串行进位链的依赖传递</strong>：在串行进位（行波进位）加法器中，高位的运算必须等待低位进位产生并逐级传递过来，形成了严重的串行进位链延迟。</li>
<li><strong>并行进位方程实现直接计算</strong>：并行进位加法器定义了进位产生函数 $G_i = A_i B_i$ 与进位传递函数 $P_i = A_i \oplus B_i$。直接展开递归方程，如：
$$
C_{i+1} = G_i + P_i C_i
$$
可以由输入信号一步直接计算出各位的进位输出，各位的进位几乎同时产生，极大提高了加法器的整体速度。</li>
</ul>
<h4>( 2 ) 如何判断浮点数运算结果是否发生溢出？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>主要取决于最终规格化和舍入后的阶码范围</strong>：浮点数的溢出主要通过阶码是否超出表示极限来判定，尾数溢出不等于浮点数结果溢出。</li>
<li><strong>阶码上溢判定</strong>：若规格化或精度舍入后，最终的阶码超出了表示范围的最大正阶值，即发生阶码上溢，硬件报错并判定为溢出。</li>
<li><strong>阶码下溢判定</strong>：若规格化后阶码小于表示范围的最小负阶值，则发生阶码下溢，运算结果通常被作为机器零或者非规格化数处理，不直接报错。</li>
</ul>
<h4>( 3 ) 如何判断浮点数运算结果是否为规格化数？如果不是规格化数，如何进行规格化？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>规格化数的判定</strong>：
<ul>
<li>原码尾数：要求尾数的最高有效位必须为 1，即形式为 $0.1xxx$ 或 $1.1xxx$。</li>
<li>补码尾数：要求数符（最高符号位）与尾数的小数点后第一位必须相异，即正数规格化形式为 $0.1xxx$，负数规格化形式为 $1.0xxx$。</li>
</ul>
</li>
<li><strong>不满足规格化时的调整策略</strong>：
<ul>
<li><strong>左规</strong>：当尾数绝对值太小（如出现 $0.00...$ 或 $1.11...$ 结构）时，执行左规。尾数连续左移，阶码相应减 1，直至最高数值位对齐。</li>
<li><strong>右规</strong>：当双符号位求和出现 $01.xx$ 或 $10.xx$（即尾数溢出）时，执行右规。尾数右移 1 位，阶码加 1。</li>
</ul>
</li>
</ul>
<h4>( 4 ) 为什么阵列除法器中能用 CAS 的进位/借位控制端作为上商的控制信号？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>CAS单元的硬件特性</strong>：CAS 单元，即可控加减单元在阵列除法器中用于执行一步“试减”操作。其输出的进位或借位信号（$C_{out}$）直接反映了被除数/部分余数与除数相减的结果真假情况。</li>
<li><strong>借位信号与上商判定逻辑一致</strong>：如果试减后没有产生借位，表示部分余数大于或等于除数，够减，本位商应该上 1；如果产生借位，表示部分余数小于除数，不够减，本位商应该上 0。因此，CAS 的借位/进位输出刚好能直接作为本位商的控制信号。</li>
</ul>
<h4>( 5 ) 移位运算和乘法及除法运算有何关系？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>等价于乘以或除以基数</strong>：在不溢出或截断的情况下，左移一位等价于乘以 2，右移一位等价于除以 2。</li>
<li><strong>用于常数乘法优化</strong>：编译器通常通过左移与加减法的组合来优化乘以常数的计算（例如将 $x \times 10$ 转换为 $(x \ll 3) + (x \ll 1)$），避免调用开销巨大的硬件乘法指令。</li>
<li><strong>构成乘除法迭代的基础</strong>：硬件乘除法电路本质上都是通过“加/减法 + 移位”的循环迭代算法实现的，移位电路是运算器实现乘除法最基础的硬件部件。</li>
</ul>
<hr />
<h3>习题3.4 变形补码计算加法</h3>
<p><strong>原题</strong>：已知 $x$ 和 $y$，用变形补码计算 $x+y$，并判断结果是否溢出。</p>
<ol>
<li>$x = 0.11010, y = 0.10111$</li>
<li>$x = 0.11101, y = -0.10100$</li>
<li>$x = -0.10111, y = -0.11000$</li>
</ol>
<p><strong>解答</strong>：</p>
<p>变形补码使用双符号位进行加法。双符号位判定溢出规则为：<code>00</code> 表示正数且无溢出，<code>11</code> 表示负数且无溢出，<code>01</code> 表示正溢出，<code>10</code> 表示负溢出。</p>
<ol>
<li>$x = 0.11010, y = 0.10111$：
写出双符号位变形补码：
$$
[x]<em>\text{补} = 00.11010,\quad [y]</em>\text{补} = 00.10111
$$
执行变形补码加法：
$$
\begin{array}{r}
00.11010 \
<ul>
<li>00.10111 \
\hline
01.10001
\end{array}
$$
结果双符号位为 <code>01</code>，表示运算结果发生了正溢出。</li>
</ul>
</li>
<li>$x = 0.11101, y = -0.10100$：
写出双符号位变形补码：
$$
[x]<em>\text{补} = 00.11101
$$
正数 $0.10100$ 变形补码为 $00.10100$，按位取反加 1 得到其相反数补码为：
$$
[y]</em>\text{补} = 11.01100
$$
执行变形补码加法：
$$
\begin{array}{r}
00.11101 \
<ul>
<li>11.01100 \
\hline
00.01001
\end{array}
$$
丢弃最高进位，结果双符号位为 <code>00</code>，表示未溢出，运算结果 $[x+y]_\text{补} = 00.01001$（真值为 $0.01001$）。</li>
</ul>
</li>
<li>$x = -0.10111, y = -0.11000$：
写出双符号位变形补码：
$$
[x]<em>\text{补} = 11.01001,\quad [y]</em>\text{补} = 11.01000
$$
执行变形补码加法：
$$
\begin{array}{r}
11.01001 \
<ul>
<li>11.01000 \
\hline
10.10001
\end{array}
$$
丢弃最高进位，结果双符号位为 <code>10</code>，表示运算结果发生了负溢出。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.5 变形补码计算减法</h3>
<p><strong>原题</strong>：已知 $x$ 和 $y$，用变形补码计算 $x-y$，并判断结果是否溢出。</p>
<ol>
<li>$x = 0.11011, y = 0.11101$</li>
<li>$x = 0.10111, y = 0.11110$</li>
<li>$x = -0.11111, y = -0.11001$</li>
</ol>
<p><strong>解答</strong>：</p>
<p>变形补码减法计算公式为 $[x - y]<em>\text{补} = [x]</em>\text{补} + [-y]_\text{补}$。</p>
<ol>
<li>$x = 0.11011, y = 0.11101$：
$$
[x]<em>\text{补} = 00.11011,\quad [y]</em>\text{补} = 00.11101
$$
求得 $[-y]_\text{补} = 11.00011$。相加：
$$
\begin{array}{r}
00.11011 \
<ul>
<li>11.00011 \
\hline
11.11110
\end{array}
$$
丢弃最高位进位，结果双符号位为 <code>11</code>，表示未溢出，运算结果 $[x-y]_\text{补} = 11.11110$（真值为 $-0.00010$）。</li>
</ul>
</li>
<li>$x = 0.10111, y = 0.11110$：
$$
[x]<em>\text{补} = 00.10111,\quad [y]</em>\text{补} = 00.11110
$$
求得 $[-y]_\text{补} = 11.00010$。相加：
$$
\begin{array}{r}
00.10111 \
<ul>
<li>11.00010 \
\hline
11.11001
\end{array}
$$
结果双符号位为 <code>11</code>，表示未溢出，运算结果 $[x-y]_\text{补} = 11.11001$（真值为 $-0.00111$）。</li>
</ul>
</li>
<li>$x = -0.11111, y = -0.11001$：
$$
[x]<em>\text{补} = 11.00001,\quad [-y]</em>\text{补} = 00.11001
$$
相加：
$$
\begin{array}{r}
11.00001 \
<ul>
<li>00.11001 \
\hline
11.11010
\end{array}
$$
结果双符号位为 <code>11</code>，表示未溢出，运算结果 $[x-y]_\text{补} = 11.11010$（真值为 $-0.00110$）。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.6 原码一位乘法计算</h3>
<p><strong>原题</strong>：用原码一位乘法计算 $x \times y$。</p>
<ol>
<li>$x = -0.11111, y = 0.11101$</li>
<li>$x = -0.11010, y = -0.01011$</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li>$x = -0.11111, y = 0.11101$：
乘积符号：$zf = xf \oplus yf = 1 \oplus 0 = 1$。
数值位绝对值乘积计算：设被乘数 $B = 11111$，乘数 $C = 11101$，部分积 $A$ 初始为 $00000$。
递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步骤</th>
<th>判定位 $C_0$</th>
<th>运算说明</th>
<th>部分积 A</th>
<th>乘数 C</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td>初始状态</td>
<td>$00000$</td>
<td>$11101$</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$11111$</td>
<td>$11101$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$01111$</td>
<td>$11110$</td>
</tr>
<tr>
<td>2</td>
<td>0</td>
<td>$A = A + 0$</td>
<td>$01111$</td>
<td>$11110$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$00111$</td>
<td>$11111$</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$100110$</td>
<td>$11111$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$10011$</td>
<td>$01111$</td>
</tr>
<tr>
<td>4</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$110010$</td>
<td>$01111$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$11001$</td>
<td>$00111$</td>
</tr>
<tr>
<td>5</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$111000$</td>
<td>$00111$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$11100$</td>
<td>$00011$</td>
</tr>
</tbody>
</table>
<p>拼接部分积 $A$ 与乘数 $C$ 的高位部分，得到数值绝对值为 $0.1110000011$。
加上负符号位，最终结果 $x \times y = -0.1110000011$，其原码机器数为 $1.1110000011$。</p>
<ol>
<li>$x = -0.11010, y = -0.01011$：
乘积符号：$zf = 1 \oplus 1 = 0$。
数值位绝对值乘积计算：设被乘数 $B = 11010$，乘数 $C = 01011$，部分积 $A$ 初始为 $00000$。
递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步骤</th>
<th>判定位 $C_0$</th>
<th>运算说明</th>
<th>部分积 A</th>
<th>乘数 C</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td>初始状态</td>
<td>$00000$</td>
<td>$01011$</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$11010$</td>
<td>$01011$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$01101$</td>
<td>$00101$</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$100111$</td>
<td>$00101$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$10011$</td>
<td>$10010$</td>
</tr>
<tr>
<td>3</td>
<td>0</td>
<td>$A = A + 0$</td>
<td>$10011$</td>
<td>$10010$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$01001$</td>
<td>$11001$</td>
</tr>
<tr>
<td>4</td>
<td>1</td>
<td>$A = A + B$</td>
<td>$100011$</td>
<td>$11001$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$10001$</td>
<td>$11100$</td>
</tr>
<tr>
<td>5</td>
<td>0</td>
<td>$A = A + 0$</td>
<td>$10001$</td>
<td>$11100$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>联合右移 1 位</td>
<td>$01000$</td>
<td>$11110$</td>
</tr>
</tbody>
</table>
<p>拼接部分积 $A$ 与乘数 $C$ 的高位部分，得到数值绝对值为 $0.0100011110$。
加上正符号位，最终结果 $x \times y = +0.0100011110$，其原码机器数为 $0.0100011110$。</p>
<hr />
<h3>习题3.7 补码一位乘法计算</h3>
<p><strong>原题</strong>：用补码一位乘法（Booth 算法）计算 $x \times y$。</p>
<ol>
<li>$x = 0.10110, y = -0.00011$</li>
<li>$x = -0.011010, y = -0.011101$</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li>$x = 0.10110, y = -0.00011$：
设字长为 1 位符号位 + 5 位数值位：$[x]<em>\text{补} = 0.10110$，则 $[-x]</em>\text{补} = 1.01010$。
乘数 $[y]<em>\text{补} = 1.11101$，引入附加位 $y</em>{-1} = 0$，部分积 $A$ 初始为 $0.00000$。
递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步骤</th>
<th>判定位 $y_n y_{n+1}$</th>
<th>运算说明</th>
<th>部分积 A</th>
<th>乘数 C，含附加位</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td>初始状态</td>
<td>$00.00000$</td>
<td>$1.11101\ 0$</td>
</tr>
<tr>
<td>1</td>
<td>1 0</td>
<td>$A = A + [-x]_\text{补}$</td>
<td>$10.10100$</td>
<td>$1.11101\ 0$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>算术右移 1 位</td>
<td>$11.01010$</td>
<td>$1.11110\ 1$</td>
</tr>
<tr>
<td>2</td>
<td>0 1</td>
<td>$A = A + [x]_\text{补}$</td>
<td>$00.00000$</td>
<td>$1.11110\ 1$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>算术右移 1 位</td>
<td>$00.00000$</td>
<td>$0.11111\ 0$</td>
</tr>
<tr>
<td>3</td>
<td>1 0</td>
<td>$A = A + [-x]_\text{补}$</td>
<td>$10.10100$</td>
<td>$0.11111\ 0$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>算术右移 1 位</td>
<td>$11.01010$</td>
<td>$0.01111\ 1$</td>
</tr>
<tr>
<td>4</td>
<td>1 1</td>
<td>$A = A + 0$</td>
<td>$11.01010$</td>
<td>$0.01111\ 1$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>算术右移 1 位</td>
<td>$11.10101$</td>
<td>$0.00111\ 1$</td>
</tr>
<tr>
<td>5</td>
<td>1 1</td>
<td>$A = A + 0$</td>
<td>$11.10101$</td>
<td>$0.00111\ 1$</td>
</tr>
<tr>
<td></td>
<td></td>
<td>算术右移 1 位</td>
<td>$11.11010$</td>
<td>$1.00011\ 1$</td>
</tr>
<tr>
<td>6</td>
<td>1 1</td>
<td>不右移（最后一步）</td>
<td>$11.11010$</td>
<td>$1.00011\ 1$</td>
</tr>
</tbody>
</table>
<p>注意：对于数值位为 5 位的补码一位乘，需要完成 5 次算术右移，第 6 步仅根据最后一轮判定位执行加减，不右移。
拼接部分积 $A$ 与乘数 $C$ 的高位部分，得到最后的乘积补码：
$$
[x \times y]_\text{补} = 11.1110111110
$$
其真值结果为 $-0.0001000010$。</p>
<ol>
<li>$x = -0.011010, y = -0.011101$：
设字长为 1 位符号位 + 6 位数值位：$[x]<em>\text{补} = 1.100110$，则 $[-x]</em>\text{补} = 0.011010$。
乘数 $[y]<em>\text{补} = 1.100011$，引入附加位 $y</em>{-1} = 0$，部分积 $A$ 初始为 $0.000000$。
按照 Booth 算法规则执行 7 次“判定加减并算术右移”之后（最后一步不右移），可得乘积补码：
$$
[x \times y]_\text{补} = 00.001011110010
$$
其真值结果为 $+0.001011110010$。</li>
</ol>
<hr />
<h3>习题3.8 原码不恢复余数法除法计算</h3>
<p><strong>原题</strong>：用原码不恢复余数法计算 $x \div y$。</p>
<ol>
<li>$x = 0.10101, y = 0.11011$</li>
<li>$x = -0.10101, y = 0.11000$</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li>$x = 0.10101, y = 0.11011$：
商符号：$zf = 0 \oplus 0 = 0$。
数值部分绝对值除法计算：被除数绝对值 $|x| = 0.10101$（整数化记为 21），除数绝对值 $|y| = 0.11011$（整数化记为 27）。
递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步数</th>
<th>余数符号</th>
<th>执行操作</th>
<th>计算过程及新部分余数 R</th>
<th>商值</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>正</td>
<td>初始状态</td>
<td>$R_0 = 21$</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 21 - 27 = 15$</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 15 - 27 = 3$</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 3 - 27 = -21$</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>负</td>
<td>$2R + D$</td>
<td>$2 \times (-21) + 27 = -15$</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>负</td>
<td>$2R + D$</td>
<td>$2 \times (-15) + 27 = -3$</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>由于第 5 步结束后部分余数仍为负数，硬件需对其进行加除数修正（恢复余数）：
$$
R_{\text{修正}} = -3 + 27 = 24
$$
所得商数值为 $0.11000$，余数为 $24 \times 2^{-10}$。商符号为正，最终结果商为 $0.11000$，余数为 $0.0000011000$。</p>
<ol>
<li>$x = -0.10101, y = 0.11000$：
商符号：$zf = 1 \oplus 0 = 1$。
数值部分绝对值除法计算：$|x| = 21$，$|y| = 24$。
递推计算过程如下表所示：</li>
</ol>
<table>
<thead>
<tr>
<th>步数</th>
<th>余数符号</th>
<th>执行操作</th>
<th>计算过程及新部分余数 R</th>
<th>商值</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>正</td>
<td>初始状态</td>
<td>$R_0 = 21$</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 21 - 24 = 18$</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 18 - 24 = 12$</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>正</td>
<td>$2R - D$</td>
<td>$2 \times 12 - 24 = 0$</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>正/非负</td>
<td>$2R - D$</td>
<td>$2 \times 0 - 24 = -24$</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>负</td>
<td>$2R + D$</td>
<td>$2 \times (-24) + 24 = -24$</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>第 5 步结束后部分余数为负，执行加除数修正：
$$
R_{\text{修正}} = -24 + 24 = 0
$$
所得商数值为 $0.11100$，余数为 $0$。商符号为负，最终结果商的原码表示为 $1.11100$，余数为 $0$。</p>
<hr />
<h3>习题3.9 机器补码浮点加法计算</h3>
<p><strong>原题</strong>：设数的阶码为 3 位，尾数为 6 位（均不包括符号位），按机器补码浮点运算规则完成下列 $[x+y]_\text{补}$ 运算。</p>
<ol>
<li>$x = 2^{011} \times 0.100100, y = 2^{010} \times (-0.011010)$</li>
<li>$x = 2^{-101} \times (-0.100010), y = 2^{-100} \times (-0.010110)$</li>
</ol>
<p><strong>解答</strong>：</p>
<p>机器字长要求：阶码为 1 位符号位 + 3 位数值位，尾数为 1 位符号位 + 6 位数值位。</p>
<ol>
<li>
<p>$x = 2^{011} \times 0.100100, y = 2^{010} \times (-0.011010)$：</p>
<ul>
<li>阶码：$Ex = 3 = 011_2$，$Ey = 2 = 010_2$。</li>
<li>对阶：小阶向大阶看齐。阶差 $\Delta E = 3 - 2 = 1$。$y$ 的尾数右移 1 位，阶码加 1，得到：
$$
y = 2^{011} \times (-0.001101)
$$</li>
<li>尾数求和：
$$
0.100100 + (-0.001101) = 0.010111
$$
和为正数，最高有效数值位为小数点后第二位，不满足规格化正尾数（$0.1xxx$）形式，需执行左规。</li>
<li>规格化：尾数左移 1 位，阶码减 1。
左规后尾数变为 $0.101110$，阶码由 3 变为 2（二进制为 $010_2$）。</li>
<li>最终补码表示为：阶码部分为 $0010$，尾数部分为 $0.101110$。即 $[x+y]_\text{补} = 0010\text{，}\ 0.101110$。</li>
</ul>
</li>
<li>
<p>$x = 2^{-101} \times (-0.100010), y = 2^{-100} \times (-0.010110)$：</p>
<ul>
<li>阶码：$Ex = -5 = -101_2$，$Ey = -4 = -100_2$。</li>
<li>对阶：大阶为 $-4$。阶差 $\Delta E = -4 - (-5) = 1$。$x$ 对阶至 $-4$，尾数右移 1 位：
$$
x = 2^{-100} \times (-0.010001)
$$</li>
<li>尾数求和：
$$
-0.010001 + (-0.010110) = -0.100111
$$
和为负数，补码规格化负尾数应形如 $1.0xxx$。此数的补码形式符号位与第一位数值位相异，满足规格化形式，无需规格化调整。</li>
<li>最终补码表示：阶码 $-4$ 对应的 4 位补码为 $1100$。尾数 $-0.100111$ 绝对值为 $0.100111$，求补码（取反加 1）得到 $1.011001$。即 $[x+y]_\text{补} = 1100\text{，}\ 1.011001$。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.10 IEEE 754 单精度浮点数计算</h3>
<p><strong>原题</strong>：采用 IEEE 754 单精度浮点数格式计算下列表达式的值。</p>
<ol>
<li>$0.625 + (-12.25)$</li>
<li>$0.625 - (-12.25)$</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li>$0.625 + (-12.25)$：
<ul>
<li>数学求和结果为 $-11.625$。</li>
<li>将 $-11.625$ 转换为二进制：
$$
-11 = -1011_2,\quad 0.625 = 0.101_2 \rightarrow -11.625 = -1011.101_2
$$</li>
<li>对其进行 IEEE 754 规格化：
$$
-1011.101_2 = -1.011101_2 \times 2^3
$$</li>
<li>确定 IEEE 754 字段：
<ul>
<li>符号位 $S = 1$（负数）。</li>
<li>阶码 $E = 3 + 127 = 130 = 10000010_2$。</li>
<li>尾数 $M$：隐藏最高位 1 后，余下小数部分为 $011101_2$，补足 23 位得到：
$$
01110100000000000000000
$$</li>
</ul>
</li>
<li>拼接得 32 位机器数：
$$
1\ 10000010\ 01110100000000000000000
$$
转换为十六进制为 $\text{C13A0000H}$。</li>
</ul>
</li>
<li>$0.625 - (-12.25)$：
<ul>
<li>结果为 $12.875$。</li>
<li>转换为二进制：$12.875 = 1100.111_2$。</li>
<li>规格化：$1.100111_2 \times 2^3$。</li>
<li>确定字段：符号位 $S = 0$（正数），阶码 $E = 3 + 127 = 130 = 10000010_2$，尾数 $M = 100111000...$。</li>
<li>拼接得 32 位机器数：
$$
0\ 10000010\ 10011100000000000000000
$$
转换为十六进制为 $\text{414E0000H}$。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.11 C 语言程序与 8 位寄存器状态分析</h3>
<p><strong>原题</strong>：假定在一个 8 位字长的计算机中运行如下 C 语言程序段：</p>
<pre><code>unsigned int x=134;
unsigned int y=246;
int m=x; int n=y;
unsigned int z1=x-y;
unsigned int z2=x+y;
int k1=m-n;
int k2=m+n;
</code></pre>
<p>若编译器编译时将 8 个 8 位寄存器 R1 ~ R8 分别分配给变量 $x\text{、}y\text{、}m\text{、}n\text{、}z1\text{、}z2\text{、}k1$ 和 $k2$。请回答下列问题：</p>
<ol>
<li>执行上述程序段后，寄存器 R1、R5 和 R6 中的内容分别是什么？（用十六进制表示）</li>
<li>执行上述程序段后，变量 $m$ 和 $k1$ 的值分别是多少？（用十进制表示）</li>
<li>上述程序段涉及带符号整数加减、无符号整数加减运算，这 4 种运算能否利用同一个加法器及辅助电路实现？简述理由。</li>
<li>计算机内部如何判断带符号整数加减运算的结果是否发生溢出？上述程序段中，哪些带符号整数运算语句的执行结果会发生溢出？</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li><strong>R1、R5、R6 的十六进制内容</strong>：
<ul>
<li>$x = 134 = 86_H$，寄存器 R1 中的内容为 $\text{86H}$。</li>
<li>$y = 246 = \text{F6}_H$，寄存器 R2 中的内容为 $\text{F6H}$。</li>
<li>$z1 = x - y = 134 - 246 = -112$。无符号减法在 8 位字长中按模 256 运算：$-112 + 256 = 144 = 90_H$。R5 中的内容为 $\text{90H}$。</li>
<li>$z2 = x + y = 134 + 246 = 380$。无符号加法按模 256 截断：$380 - 256 = 124 = 7\text{C}_H$。R6 中的内容为 $\text{7CH}$。</li>
</ul>
</li>
<li><strong>变量 $m$ 和 $k1$ 的值</strong>：
<ul>
<li><code>int m = x;</code>：把机器数 $86_H$（$10000110_2$）按 8 位带符号补码进行解释。其最高位为 1，代表负数，真值为 $134 - 256 = -122$。所以 $m = -122$。</li>
<li><code>int n = y;</code>：机器数 $\text{F6}_H$ 解释为有符号数：$246 - 256 = -10$。</li>
<li>$k1 = m - n = -122 - (-10) = -112$。在有符号 8 位补码范围 $[-128, 127]$ 内，因此 $k1 = -112$。</li>
</ul>
</li>
<li><strong>加法器公用性判定</strong>：
<ul>
<li><strong>完全可以公用同一个加法器</strong>：因为无论是有符号数还是无符号数，其在计算机底层的二进制位串加减运算规则是完全统一的。</li>
<li>减法一律转换为加法 $[A - B]<em>\text{补} = [A]</em>\text{补} + \sim [B]_\text{补} + 1$ 实现。加法器本身只进行无符号的二进制位相加，有无符号的唯一区别在于外部控制电路对溢出标志（$\text{OF}$、$\text{CF}$）的解释和监测方式不同。</li>
</ul>
</li>
<li><strong>有符号溢出判定与溢出语句</strong>：
<ul>
<li><strong>有符号溢出判定</strong>：采用双符号位或者符号位进位与最高数值位进位异或判定 $\text{OF} = Cd \oplus Cf$。对于加法，两个同号数相加，如果结果的符号与操作数符号相反，则发生溢出。</li>
<li><strong>溢出语句检测</strong>：
<ul>
<li><code>int k1 = m - n</code>：计算结果为 $-112$，在有符号范围 $[-128, 127]$ 内，未发生溢出。</li>
<li><code>int k2 = m + n</code>：计算为 $-122 + (-10) = -132$。因 $-132$ 小于 8 位补码能表示的最小值 $-128$，发生了负溢出（硬件标志 $\text{OF} = 1$）。</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.12 乘法指令与硬件实现分析</h3>
<p><strong>原题</strong>：有实现 $x \times y$ 的两个 C 语言函数如下：</p>
<pre><code>unsigned umul(unsigned x, unsigned y) { return x*y; }
int imul(int x, int y) { return x*y; }
</code></pre>
<p>假定某计算机 M 中 ALU 只能进行加减运算和逻辑运算。请回答下列问题：</p>
<ol>
<li>若 M 的指令系统中没有乘法指令，但有加法、减法和位移等指令，则在 M 上也能实现上述两个函数中的乘法运算，为什么？</li>
<li>若 M 的指令系统中有乘法指令，则基于 ALU、位移器、寄存器以及相应控制逻辑实现乘法指令时，控制逻辑的作用是什么？</li>
<li>针对以下三种情况：① 没有乘法指令；② 有使用 ALU 和位移器实现的乘法指令；③ 有使用阵列乘法器实现的乘法指令。函数 <code>umul()</code> 在哪种情况下执行时间最长？哪种情况下执行时间最短？说明理由。</li>
<li>$n=32, x=2^{31}-1, y=2$ 时，带符号整数乘法指令和无符号整数乘法指令得到的 $2n$ 位乘积分别是什么？此时函数 <code>umul()</code> 和 <code>imul()</code> 的返回结果是否溢出？对于无符号整数乘法运算，当仅取低 $n$ 位作为乘积时，如何判定是否发生溢出？</li>
</ol>
<p><strong>解答</strong>：</p>
<ol>
<li><strong>无乘法指令时实现乘法的原因</strong>：
<ul>
<li>乘法在数学上等价于多次“位移与加法”的累加迭代。例如无符号数乘法可表示为：
$$
x \times y = \sum_{i=0}^{n-1} y_i (x \ll i)
$$</li>
<li>编译器可以通过将乘积展开为判断乘数各权位是否为 1，进而进行相应的左移及加法累加循环来实现乘法。因此，仅靠加减与位移指令完全可以实现乘法函数。</li>
</ul>
</li>
<li><strong>控制逻辑的作用</strong>：
<ul>
<li>控制逻辑在硬件中充当控制器的作用，负责协调整个移位加法电路的时序。它负责：检查乘数的当前权位、控制 ALU 执行加法或减法、发出部分积及乘数的移位脉冲、控制迭代循环次数以及判断乘法运算是否完成。</li>
</ul>
</li>
<li><strong>三种实现方式的执行速度对比</strong>：
<ul>
<li><strong>执行时间最长</strong>：① 没有乘法指令。因为乘法必须被翻译成多条加法、移位、循环及分支跳转指令，在 CPU 中占用大量的时钟周期。</li>
<li><strong>执行时间最短</strong>：③ 采用阵列乘法器。阵列乘法器由纯并行硬件单元构成，能够在一个或极少的时钟周期内直接输出乘积结果，速度极快。</li>
</ul>
</li>
<li><strong>数值计算与溢出判定</strong>：
<ul>
<li><strong>$2n$ 位乘积计算</strong>：
已知 $x = 2^{31} - 1 = \text{7FFFFFFFH}$，$y = 2 = \text{00000002H}$。
有符号与无符号下的数学乘积均为 $(2^{31}-1) \times 2 = 2^{32}-2$。转换为 64 位（$2n$ 位）二进制十六进制为 $\text{00000000 FFFFFFFEH}$。</li>
<li><strong>函数返回结果溢出判定</strong>：
<ul>
<li><code>umul()</code>：截断保留低 32 位，返回 $\text{FFFFFFFEH}$。按无符号数解释，该值等于 $4294967294$，在 32 位无符号整数表示范围内，因此<strong>未发生溢出</strong>。</li>
<li><code>imul()</code>：截断保留低 32 位，返回 $\text{FFFFFFFEH}$。按补码有符号数解释，该值真值为 $-2$，而正确数学结果为 $4294967294$（超出 32 位有符号整数最大范围），因此<strong>发生溢出</strong>。</li>
</ul>
</li>
<li><strong>无符号乘法低 $n$ 位溢出判定规则</strong>：
<ul>
<li>检查 $2n$ 位乘积的高 $n$ 位。若高 $n$ 位全为 0，则不溢出；若高 $n$ 位中含有非 0 字符，则说明乘积超出了低 $n$ 位所能表示的上限，判定为发生溢出。</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr />
<h3>习题3.13 电路延迟与成本开销分析</h3>
<p><strong>原题</strong>：如果全加器采用图 3.2 (b) 所示的方案实现，尝试分析图 3.3 ~ 图 3.8 所示电路的时间延迟和成本开销，你认为与图 3.2 (a) 所示方案相比哪个方案更好，为什么？</p>
<p><strong>解答</strong>：</p>
<p>全加器的两种方案通常指：方案 a 为“基于两级异或门实现和位、并复用异或输出生成进位”的传统方案；方案 b 为“基于与-或逻辑直接生成进位、和位采用三输入异或门”的并行加速方案。</p>
<ul>
<li><strong>时间延迟分析</strong>：
<ul>
<li>在传统的方案 a 中，进位的生成依赖于第一级异或门的输出，因此进位延迟较大（异或门延迟通常显著大于与门/或门延迟）。</li>
<li>在方案 b 中，进位 $C_{i+1}$ 采用与-或两级门电路直接并行产生，其进位延迟仅为 2t 延迟，显著低于方案 a。对于依靠串行进位链传递的行波进位加法器（图 3.3）与单元数目巨大的阵列乘法器、阵列除法器（图 3.5 ~ 图 3.8），其关键路径完全由进位链决定。因此，采用方案 b 能极大降低这些电路的总体时间延迟，提升工作主频。</li>
</ul>
</li>
<li><strong>成本开销分析</strong>：
<ul>
<li>方案 b 由于进位与和位逻辑完全分离，无法复用异或中间项，且在全加器内部需要使用更多的与门、或门以及三输入异或门，导致单个全加器的晶体管（门）开销显著增加。在大型加法器或阵列乘除法器中，这会导致硬件成本和面积开销有所上升。</li>
</ul>
</li>
<li><strong>方案优劣判定与折中结论</strong>：
<ul>
<li>对于<strong>行波进位加法器</strong>与<strong>阵列乘除法器</strong>等进位传递是关键路径瓶颈的电路，<strong>方案 b 更好</strong>。因为进位延迟的微小降低在串行累加后会带来系统性能的飞跃，所付出的硬件成本增加是完全值得的。</li>
<li>对于<strong>图 3.4 中的超前进位加法器</strong>，由于每一位的进位是由独立的超前进位单元并行计算生成的，并不依赖于全加器内部的串行进位链。全加器仅需向外输出进位产生函数 $G_i$ 和进位传递函数 $P_i$。在此场景下，<strong>方案 a 更好</strong>。因为它可以更低成本、更自然地输出 $P_i = A_i \oplus B_i$，同时规避了方案 b 带来的额外硬件面积浪费。</li>
</ul>
</li>
</ul>
<h2>3.8 本章总结</h2>
<h3>知识体系关系链</h3>
<pre><code>位运算与逻辑运算
       │
       ▼
 基础半加器与全加器
       │
       ▼
 补码加减法运算 ───(结合)─── 溢出检测逻辑
       │
       ▼
 移位运算 ───(结合)─── 原码与补码乘除法
       │
       ▼
 尾数运算与阶码处理相结合
       │
       ▼
 浮点数加减运算五步法
       │
       ▼
 运算器硬件通路与状态标志寄存器
</code></pre>
<h3>核心考点记忆口诀</h3>
<blockquote>
<p>补码加减：符号位一起算，最高进位可丢掉。
减法实现：取反再加一，本质仍然是加法。
溢出判断：同号相加变异号，亦或进位判不同。
移位运算：左移低补零，逻辑右高补零，算术右高补符号。
原码乘法：符号异或定，数值绝对做累加，右移重复数位次。
浮点加减：先对阶，再尾加，后规格，最终舍入判溢出。
运算核心：算术逻辑单元，辅以寄存移位与状态标志。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>数据信息的表示</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%95%B0%E6%8D%AE%E4%BF%A1%E6%81%AF%E7%9A%84%E8%A1%A8%E7%A4%BA/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E6%95%B0%E6%8D%AE%E4%BF%A1%E6%81%AF%E7%9A%84%E8%A1%A8%E7%A4%BA/</id>
    <published>2026-06-28T09:40:00.000Z</published>
    <updated>2026-06-28T09:40:00.000Z</updated>
    <summary>系统介绍定点数与浮点数表示方法，深入解析IEEE 754标准、数据类型转换与海明校验设计及计算。</summary>
    <content type="html"><![CDATA[<p>本文整理计算机中数据信息的表示与校验方法。在计算机组成原理中，掌握<strong>定点数与浮点数的表示与计算</strong>，以及<strong>校验码的设计原理</strong>是第二章的复习重点。</p>
<hr />
<h1>第2章：数据信息的表示</h1>
<hr />
<h2>2.1 数据表示的作用</h2>
<p>计算机内部的所有信息都必须以二进制代码的形式进行存储与处理。现实世界中的各种数据在输入计算机时，均需被编码转换为二进制形式。</p>
<pre><code>flowchart TD
    Data[数据信息] --&gt; Numeric[数值数据: 带有确定数值意义的数]
    Data --&gt; NonNumeric[非数值数据: 文字、字符与多媒体]
    Numeric --&gt; Unsigned[无符号数]
    Numeric --&gt; Signed[有符号数]
    Signed --&gt; Fixed[定点数]
    Signed --&gt; Float[浮点数]
    NonNumeric --&gt; Char[字符与文字]
    NonNumeric --&gt; Media[多媒体数据: 图像、声音、视频]
</code></pre>
<hr />
<h2>2.2 数值数据的表示</h2>
<p>定点数是指小数点位置固定不变的数。在计算机中，定点数通常用于表示整数，或在某些特定场景下表示纯小数。</p>
<h3>2.2.1 无符号数与有符号数</h3>
<ul>
<li><strong>无符号数</strong>：所有二进制位都代表数值大小，没有符号位。对于 $n$ 位的无符号数，其表示范围为：
$
0 \sim 2^n - 1
$</li>
<li><strong>有符号数</strong>：通常将二进制数的最高位作为符号位，其中用 0 表示正数，用 1 表示负数。</li>
</ul>
<h3>原码、反码、补码</h3>
<p>为了便于在硬件电路中进行算术运算，有符号数在计算机内部有不同的编码表示方法。</p>
<ul>
<li><strong>原码</strong>：最高位为符号位，其余各位表示数值的绝对值。
<blockquote>
<p><strong>特点</strong>：原码表示直观，但 0 的表示不唯一（存在正 0 和负 0 两种形式），且加减法运算规律复杂，符号位不能直接参与运算。</p>
</blockquote>
</li>
<li><strong>反码</strong>：正数的反码与原码相同；负数的反码则是符号位保持不变，数值位按位取反。</li>
<li><strong>补码</strong>：正数的补码与原码相同；负数的补码则是其反码在最低位加 1。
<blockquote>
<p><strong>求补码的速记法</strong>：负数原码自右向左数，找到第一个“1”，该“1”及其右边的所有“0”保持不变，该“1”左边的所有数值位按位取反，符号位保持不变。</p>
</blockquote>
</li>
</ul>
<p>:::important[补码的核心优势]
补码的引入彻底解决了原码运算的硬件难题。其优势主要体现在：</p>
<ol>
<li><strong>加减法运算统一</strong>：减法运算可以转换为加上一个负数的补码，使加减法器统一为加法器电路。</li>
<li><strong>零的表示唯一</strong>：在补码中，正 0 和负 0 的编码合并为唯一编码。</li>
<li><strong>符号位参与运算</strong>：符号位可以与数值位一同参与算术运算，无需单独的电路判断符号。
:::</li>
</ol>
<hr />
<h3>2.2.2 定点数表示：补码范围及其不对称性</h3>
<p>定点数根据小数点位置的不同，可分为定点整数和定点小数。在补码表示下，两者的范围表现出独特的不对称性。</p>
<h3>定点整数的补码范围</h3>
<p>对于 $n$ 位补码定点整数（含 1 位符号位， $n-1$ 位数值位），其表示范围为：</p>
<p>$$
-2^{n-1} \sim 2^{n-1} - 1
$$</p>
<p>以 8 位补码为例：</p>
<ul>
<li>最大正数为：$01111111_2 = +127$</li>
<li>最小负数为：$10000000_2 = -128$</li>
<li>8 位补码范围为：$-128 \sim +127$</li>
</ul>
<h3>定点小数的补码范围</h3>
<p>对于 $n$ 位补码定点小数（含 1 位符号位，小数点固定在符号位之后），其表示范围为：</p>
<p>$$
-1 \sim 1 - 2^{-(n-1)}
$$</p>
<p>以 8 位补码小数为例：</p>
<ul>
<li>最大正数为：$0.1111111_2 = 1 - 2^{-7}$</li>
<li>最小负数为：$1.0000000_2 = -1$</li>
<li>8 位补码小数范围为：$-1 \sim 1 - 2^{-7}$</li>
</ul>
<p>:::important[补码范围不对称的根本原因]
在原码和反码中，数值 0 占用了两个编码（例如 8 位原码中 $00000000_2$ 和 $10000000_2$ 均表示 0）。
而在补码中，0 仅有唯一编码（$00000000_2$）。原本在原码中表示负 0 的编码（如 8 位下的 $10000000_2$），在补码中被赋予了新的数值意义，用于表示最小的负数（即 $-128$ 或 $-1$）。因此，补码的负数表示范围比正数多一个。
:::</p>
<h4>应用例题：补码范围与转换</h4>
<blockquote>
<p><strong>例题 1</strong>：设机器字长为 16 位，求该机器在补码表示下所能表示的定点整数范围。</p>
<p><strong>解析</strong>：
将 $n = 16$ 代入定点整数补码范围公式 $-2^{n-1} \sim 2^{n-1} - 1$：</p>
<p>$$
-2^{15} \sim 2^{15} - 1
$$</p>
<p>展开后即为：</p>
<p>$$
-32768 \sim +32767
$$</p>
</blockquote>
<h3>变形补码与溢出检测机制</h3>
<p>变形补码又称为双符号位补码，它采用两位二进制来表示数值的符号。</p>
<ul>
<li><strong>符号位定义</strong>：<code>00</code> 表示正数，<code>11</code> 表示负数。</li>
<li><strong>溢出判断原理</strong>：在变形补码加减运算中，若结果的两位符号位不同，则表明发生了溢出。具体为：
<ul>
<li><code>01</code>：表示两个正数相加，结果超出了最大正数范围，称为<strong>正溢出</strong>。</li>
<li><code>10</code>：表示两个负数相加，结果超出了最小负数范围，称为<strong>负溢出</strong>。</li>
</ul>
</li>
</ul>
<p>:::important[变形补码的核心考点]</p>
<ol>
<li><strong>溢出检测门电路</strong>：在定点运算器中，不论是单符号位（检测最高位进位与符号位进位是否相异）还是双符号位，溢出判断的逻辑本质都是异或判定，因此在硬件上都使用<strong>异或门</strong>电路实现。</li>
<li><strong>能否避免溢出</strong>：变形补码只能使计算机<strong>更容易检测</strong>出溢出状态，但<strong>绝不能避免或阻止</strong>溢出的发生。若要避免溢出，必须扩大寄存器字长或采用浮点数表示。
:::</li>
</ol>
<h3>定点数的算术移位规则</h3>
<p>移位运算在计算机中有广泛应用。对于定点有符号数，在进行算术移位时，符号位保持不变，数值位移动时空出位置的填充规则如下：</p>
<table>
<thead>
<tr>
<th>机器数类型</th>
<th>对应正负</th>
<th>左移空位填充</th>
<th>右移空位填充</th>
</tr>
</thead>
<tbody>
<tr>
<td>原码</td>
<td>正数与负数</td>
<td>填充 0</td>
<td>填充 0</td>
</tr>
<tr>
<td>补码</td>
<td>正数</td>
<td>填充 0</td>
<td>填充 0</td>
</tr>
<tr>
<td>补码</td>
<td>负数</td>
<td>填充 0</td>
<td>填充 1</td>
</tr>
</tbody>
</table>
<p>:::tip[补码算术右移实例]
补码负数算术右移时，高位空出处填充符号位 1。例如有符号补码整数 $01010100_2$（为正数，符号位为 0）算术右移两位后，高位补 0 得到 $00010101_2$。
:::</p>
<hr />
<h3>2.2.3 浮点数表示：编码与规格化</h3>
<p>定点数的小数点位置固定，这限制了它的数值表示范围和精度。浮点数通过将小数点的位置设计为可浮动的形式，能够以较短的字长表示极大或极小的数。</p>
<h3>浮点数的一般格式</h3>
<p>在计算机中，浮点数通常表示为：</p>
<p>$$
N = M \times 2^E
$$</p>
<p>其中，尾数 $M$ 决定了浮点数的精度，阶码 $E$ 决定了浮点数的范围和位置。</p>
<p>为了保证浮点数表示的唯一性并提高表示精度，必须对浮点数进行规格化处理。规格化要求尾数的绝对值在特定范围内。
对于定点小数补码表示的尾数，规格化形式要求符号位与数值最高位相反，即：</p>
<ul>
<li>正数为：$0.1\dots\dots_2$</li>
<li>负数为：$1.0\dots\dots_2$</li>
</ul>
<hr />
<h3>IEEE 754 标准</h3>
<p>现代计算机通常采用 IEEE 754 标准进行浮点数存储与计算。</p>
<pre><code>gantt
    title IEEE 754 单精度浮点数 32 位布局
    dateFormat X
    axisFormat %s
    section 字段
    符号位 S (1位) : 0, 1
    阶码 E (8位)  : 1, 9
    尾数 M (23位) : 9, 32
</code></pre>
<h4>1. 单精度浮点数</h4>
<ul>
<li><strong>位宽</strong>：32 位。</li>
<li><strong>结构分配</strong>：1 位符号位，8 位阶码，23 位尾数。</li>
<li><strong>偏置值</strong>：阶码采用移码表示，偏置值为 127。</li>
<li><strong>计算公式</strong>：
$
V = (-1)^S \times 1.M \times 2^{E - 127}
$</li>
</ul>
<h4>2. 双精度浮点数</h4>
<ul>
<li><strong>位宽</strong>：64 位。</li>
<li><strong>结构分配</strong>：1 位符号位，11 位阶码，52 位尾数。</li>
<li><strong>偏置值</strong>：阶码采用移码表示，偏置值为 1023。</li>
<li><strong>计算公式</strong>：
$
V = (-1)^S \times 1.M \times 2^{E - 1023}
$</li>
</ul>
<p>:::important[阶码采用移码表示的原因]
阶码用于表示小数点移动的位数，带有正负。在比较两个浮点数的大小时，如果直接使用有符号的补码表示阶码，硬件电路比较大小时需要进行复杂的符号判断。而使用移码（即在真实指数上加上一个固定的正偏置值），可以将所有的指数转换成无符号的非负整数。这样，硬件可以直接使用无符号比较器来快速比较阶码的大小，从而简化了对阶操作的硬件实现。
:::</p>
<h4>应用例题：十进制数向单精度浮点数的转换</h4>
<blockquote>
<p><strong>例题 2</strong>：将十进制数 $-20.375$ 转换为单精度浮点数的十六进制格式。</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>将绝对值转换为二进制数</strong>：
<ul>
<li>整数部分：$20 = 10100_2$</li>
<li>小数部分：$0.375 = 0.25 + 0.125 = 2^{-2} + 2^{-3} = 0.011_2$</li>
<li>组合得到二进制数值：$10100.011_2$</li>
</ul>
</li>
<li><strong>对二进制数进行规格化</strong>：
<ul>
<li>移动小数点，使左边仅保留一位有效数字 1：
$$
10100.011_2 = 1.0100011_2 \times 2^4
$$</li>
<li>真实指数为 $e = 4$。</li>
</ul>
</li>
<li><strong>确定各个字段的二进制编码</strong>：
<ul>
<li><strong>符号位</strong>：由于原数是负数，故 $S = 1$。</li>
<li><strong>阶码</strong>：真实指数为 4，单精度偏置值为 127。计算阶码的移码：
$$
E = 4 + 127 = 131 = 10000011_2
$$</li>
<li><strong>尾数</strong>：规格化后的小数部分为 $0100011_2$，补足到 23 位得到：
$$
M = 01000110000000000000000_2
$$</li>
</ul>
</li>
<li><strong>拼接二进制编码并转换</strong>：
<ul>
<li>将上述字段按顺序拼接为 32 位二进制串：
$$
1 \ | \ 10000011 \ | \ 01000110000000000000000
$$</li>
<li>每 4 位划分为一组，转换为十六进制：
<ul>
<li>$1100 \ 0001 \ 1010 \ 0011 \ 0000 \ 0000 \ 0000 \ 0000_2$</li>
<li>转换结果为：$\text{C1A30000H}$。</li>
</ul>
</li>
</ul>
</li>
</ol>
</blockquote>
<h3>2.2.4 浮点数加减运算步骤与对阶原则</h3>
<p>浮点数加减运算的实现过程较为复杂，在计算机硬件中通常分为以下五个核心步骤进行：</p>
<ol>
<li><strong>对阶</strong>：使两个操作数的阶码大小一致，这是进行尾数求和的前提。</li>
<li><strong>尾数求和</strong>：对阶完成后的两个尾数进行算术加减运算。</li>
<li><strong>规格化</strong>：若求和后的尾数不满足规格化格式，需要通过左规或右规进行调整。</li>
<li><strong>舍入</strong>：在对阶和右规过程中可能导致尾数低位被丢弃，需要执行舍入处理（如“0舍1入”法）以减小精度误差。</li>
<li><strong>溢出判断</strong>：判定阶码是否超出了表示范围。</li>
</ol>
<p>:::important[对阶原则：小阶向大阶看齐]
浮点加减中的对阶操作必须遵循<strong>小阶向大阶看齐</strong>的原则。即阶码小的数向阶码大的数对齐。对阶时，小阶的尾数右移，其阶码同步加 1，直到与大阶的阶码相等。</p>
<p><strong>原理剖析</strong>：如果强行将大阶向小阶对齐，大阶的尾数就需要向左移位，这会导致尾数高位（最重要的有效数字）被移出丢弃，造成严重的数值错误。而小阶向大阶看齐时尾数右移，虽然丢弃了尾数低位数字，但这仅会造成微小的精度损失，而不会影响数值大体范围，因此在硬件上是唯一正确的选择。</p>
<p><strong>浮点溢出的真谛</strong>：浮点数的溢出完全取决于<strong>阶码是否发生溢出</strong>。尾数求和后产生的溢出（如尾数进位）可以通过右规并增加阶码来修正，不代表真正的溢出；只有当阶码超出其所能表示的最大正指数（上溢）时，才会真正触发浮点溢出报错。
:::</p>
<hr />
<h3>2.2.5 计算机中的数据类型与类型转换机制</h3>
<p>在高级编程语言中，数据具有明确的类型属性。在不同数据类型之间进行转换时，底层的二进制处理逻辑各有不同。</p>
<h3>符号扩展与零扩展</h3>
<p>当把短字长的数据转换为长字长的数据时，需要填补高出的空位。</p>
<ul>
<li><strong>符号扩展</strong>：用于有符号数。高位填充原数据的符号位（正数填 0，负数填 1），从而确保数值大小不变。</li>
<li><strong>零扩展</strong>：用于无符号数。高位一律填充 0，保证无符号数值的等价性。</li>
</ul>
<p>:::tip[符号扩展实例]
设机器中 8 位有符号补码表示的数为 $11111011_2$（对应十进制 $-5$）。
当其扩展为 16 位有符号整数时，执行符号扩展，高位全部补符号位 1：</p>
<p>$$
11111111 \ 11111011_2
$$</p>
<p>该数在 16 位补码下的数值依然是 $-5$。
:::</p>
<h3>截断</h3>
<p>当把长字长的数据转换为短字长的数据时，计算机会直接丢弃高位，仅保留低位数值。</p>
<ul>
<li><strong>影响</strong>：截断可能会彻底改变原数据的符号和大小，从而导致溢出和精度丢失。</li>
</ul>
<h3>大端与小端存储模式</h3>
<p>对于多字节的复合数据（如 32 位定点整数、64 位双精度浮点数），其存放在连续的内存字节中时，需要决定各字节的物理排列顺序。</p>
<ul>
<li><strong>大端方式</strong>：将数据的<strong>最高有效字节</strong>（即数值的高位字节部分）存储在<strong>低内存地址</strong>中。这种排列方式与人类从左到右的书写阅读习惯完全一致。</li>
<li><strong>小端方式</strong>：将数据的<strong>最低有效字节</strong>（即数值的低位字节部分）存储在<strong>低内存地址</strong>中。这种方式便于计算机硬件直接从低位地址读取基础数值。</li>
</ul>
<p>:::important[小端模式内存字节布局分析]
设有一个双精度浮点数机器码为 <code>1122 3344 5566 7788H</code>，其最高有效字节为 <code>11H</code>，最低有效字节为 <code>88H</code>。若将其从小端方式存放在以地址 <code>0000 8040H</code> 开始的连续存储单元中，其物理分布如下：</p>
<ul>
<li><code>0000 8040H</code> 单元存放最低有效字节：<code>88H</code></li>
<li><code>0000 8041H</code> 单元存放：<code>77H</code></li>
<li><code>0000 8042H</code> 单元存放：<code>66H</code></li>
<li><code>0000 8043H</code> 单元存放：<code>55H</code></li>
<li><code>0000 8044H</code> 单元存放：<code>44H</code></li>
<li><code>0000 8045H</code> 单元存放：<code>33H</code></li>
<li><code>0000 8046H</code> 单元存放：<code>22H</code></li>
<li><code>0000 8047H</code> 单元存放最高有效字节：<code>11H</code></li>
</ul>
<p>在期末考查中，经常给出 double 型变量的十六进制编码，要求指出小端方式下特定物理单元（如 <code>8042H</code> 或 <code>8046H</code>）中存放的具体内容。解题关键即在画出低地址存放低字节的对应表。
:::</p>
<hr />
<h2>2.3 非数值数据的表示</h2>
<p>非数值数据在计算机中同样通过特定的编码方式进行表示。</p>
<h3>字符编码</h3>
<p>最常用的字符编码为 ASCII 码，它使用 7 位或 8 位二进制代码来表示英文字母、数字和控制字符。在考试中，需熟记以下基准字符的对应关系：</p>
<ul>
<li>字符 <code>'0'</code> 对应的码值为 48（十六进制 30H）</li>
<li>字符 <code>'A'</code> 对应的码值为 65（十六进制 41H）</li>
<li>字符 <code>'a'</code> 对应的码值为 97（十六进制 61H）</li>
<li>大小写字母的转换关系为：大写字母与对应的小写字母码值相差 32。</li>
</ul>
<h3>汉字编码</h3>
<p>为了支持汉字的输入、处理和输出，计算机系统定义了多种汉字编码：</p>
<ul>
<li><strong>输入码</strong>：用于汉字输入（如拼音输入码、五笔字型码）。</li>
<li><strong>国标码</strong>：作为汉字交换码，用于不同系统间的数据交换。</li>
<li><strong>机内码</strong>：在计算机系统内部存储和处理汉字时使用的编码。</li>
<li><strong>字形码</strong>：用于汉字的显示和打印输出。</li>
</ul>
<p>:::tip[汉字编码之间的换算公式]
在字符处理中，区位码、国标码和机内码存在以下确定的计算对应关系（计算时需统一转换为十六进制）：</p>
<ol>
<li>国标码 = 区位码（十六进制表示） + 2020H</li>
<li>机内码 = 国标码 + 8080H
:::</li>
</ol>
<hr />
<h2>2.4 数据信息的校验</h2>
<p>数据在网络传输或硬件存储时，由于物理噪声等干扰可能会发生位翻转。为了保障数据的正确性，需要引入校验码。</p>
<h3>2.4.1 码距与校验</h3>
<p>码距是指在一个编码系统中，任意两个合法码字之间不同二进制位的最小个数。</p>
<p>:::note[码距与纠错公式]
设校验码系统的最小码距为 $d$：</p>
<ol>
<li>若要检测 $e$ 位错误，则必须满足：$d \ge e + 1$</li>
<li>若要纠正 $t$ 位错误，则必须满足：$d \ge 2t + 1$</li>
<li>若要同时检测 $e$ 位错误并纠正 $t$ 位错误（其中 $e \ge t$），则必须满足：$d \ge e + t + 1$
:::</li>
</ol>
<h3>2.4.2 奇偶校验</h3>
<p>奇偶校验通过在原数据位后附加 1 位校验位，使编码中二进制“1”的总个数满足特定奇偶性。</p>
<ul>
<li><strong>偶校验</strong>：使数据位与校验位中 1 的总个数为偶数。</li>
<li><strong>奇校验</strong>：使数据位与校验位中 1 的总个数为奇数。</li>
<li><strong>局限性</strong>：奇偶校验的最小码距为 2。它只能检测出奇数个二进制位发生翻转的错误，无法定位错误发生的位置，因此不具备纠错能力。</li>
</ul>
<hr />
<h3>2.4.3 海明校验</h3>
<p>海明校验通过在数据位中合理地插入多个校验位，并将数据划分为多个交叉的校验小组，从而实现检测错误并自动定位纠错的目的。其最小码距为 3。</p>
<h4>核心步骤一：确定校验位个数</h4>
<p>设数据位的位数为 $m$，海明码需要插入的校验位的位数为 $r$。它们必须满足以下关系式：</p>
<p>$$
2^r \ge m + r + 1
$$</p>
<h4>实际计算例题</h4>
<blockquote>
<p><strong>例题 3</strong>：已知需要传输的数据为 8 位二进制数据 <code>10110100</code>，求设计海明码时所需要的校验位位数 $r$。</p>
<p><strong>解析</strong>：
已知 $m = 8$，代入公式 $2^r \ge 8 + r + 1$ 进行试算：</p>
<ul>
<li>若 $r = 3$：$2^3 = 8 &lt; 8 + 3 + 1 = 12$，不满足条件。</li>
<li>若 $r = 4$：$2^4 = 16 \ge 8 + 4 + 1 = 13$，满足条件。</li>
</ul>
<p><strong>结果</strong>：需要设计 4 位校验位。总的海明码位宽为 $8 + 4 = 12$ 位。</p>
</blockquote>
<hr />
<h4>核心步骤二：确定校验位与数据位的位置分配</h4>
<p>海明码的校验位 $P_i$ 必须放置在整个编码中编号为 $2^k$（即 1, 2, 4, 8, 16 等）的位置上，其余位置则按顺序填充数据位 $D_j$。
对于上述 12 位海明码，其位置分配如下表所示：</p>
<table>
<thead>
<tr>
<th>海明码位号</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>字符类型</strong></td>
<td>$P_1$</td>
<td>$P_2$</td>
<td>$D_1$</td>
<td>$P_3$</td>
<td>$D_2$</td>
<td>$D_3$</td>
<td>$D_4$</td>
<td>$P_4$</td>
<td>$D_5$</td>
<td>$D_6$</td>
<td>$D_7$</td>
<td>$D_8$</td>
</tr>
<tr>
<td><strong>填入数值</strong></td>
<td>$P_1$</td>
<td>$P_2$</td>
<td>1</td>
<td>$P_3$</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>$P_4$</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
<hr />
<h4>核心步骤三：划分子集与计算校验位的值（偶校验）</h4>
<p>海明码中，任何一个数据位号均可拆分为多个校验位号之和。例如：</p>
<ul>
<li>$3 = 1 + 2 \implies D_1$ 同时属于 $P_1$ 和 $P_2$ 的校验组</li>
<li>$5 = 1 + 4 \implies D_2$ 同时属于 $P_1$ 和 $P_3$ 的校验组</li>
<li>$6 = 2 + 4 \implies D_3$ 同时属于 $P_2$ 和 $P_3$ 的校验组</li>
<li>$7 = 1 + 2 + 4 \implies D_4$ 同时属于 $P_1$、$P_2$ 和 $P_3$ 的校验组</li>
<li>$9 = 1 + 8 \implies D_5$ 同时属于 $P_1$ 和 $P_4$ 的校验组</li>
<li>$10 = 2 + 8 \implies D_6$ 同时属于 $P_2$ 和 $P_4$ 的校验组</li>
<li>$11 = 1 + 2 + 8 \implies D_7$ 同时属于 $P_1$、$P_2$ 和 $P_4$ 的校验组</li>
<li>$12 = 4 + 8 \implies D_8$ 同时属于 $P_3$ 和 $P_4$ 的校验组</li>
</ul>
<p>按照偶校验原则（校验组内所有位异或结果为 0），写出各校验组的表达式：</p>
<ul>
<li>$P_1 \oplus D_1 \oplus D_2 \oplus D_4 \oplus D_5 \oplus D_7 = 0$</li>
<li>$P_2 \oplus D_1 \oplus D_3 \oplus D_4 \oplus D_6 \oplus D_7 = 0$</li>
<li>$P_3 \oplus D_2 \oplus D_3 \oplus D_4 \oplus D_8 = 0$</li>
<li>$P_4 \oplus D_5 \oplus D_6 \oplus D_7 \oplus D_8 = 0$</li>
</ul>
<p>将已知数据值代入：</p>
<ul>
<li>$P_1 \oplus 1 \oplus 0 \oplus 1 \oplus 0 \oplus 0 = 0 \implies P_1 \oplus 0 = 0 \implies P_1 = 0$</li>
<li>$P_2 \oplus 1 \oplus 1 \oplus 1 \oplus 1 \oplus 0 = 0 \implies P_2 \oplus 0 = 0 \implies P_2 = 0$</li>
<li>$P_3 \oplus 0 \oplus 1 \oplus 1 \oplus 0 = 0 \implies P_3 \oplus 0 = 0 \implies P_3 = 0$</li>
<li>$P_4 \oplus 0 \oplus 1 \oplus 0 \oplus 0 = 0 \implies P_4 \oplus 1 = 0 \implies P_4 = 1$</li>
</ul>
<p>得到海明校验码最终序列为：
$$
001001110100_2
$$</p>
<hr />
<h4>核心步骤四：错误检测与纠错</h4>
<p>接收端在收到海明码后，会重新计算各校验组的指征值 $S_i$：</p>
<ul>
<li>$S_1 = P_1 \oplus D_1 \oplus D_2 \oplus D_4 \oplus D_5 \oplus D_7$</li>
<li>$S_2 = P_2 \oplus D_1 \oplus D_3 \oplus D_4 \oplus D_6 \oplus D_7$</li>
<li>$S_3 = P_3 \oplus D_2 \oplus D_3 \oplus D_4 \oplus D_8$</li>
<li>$S_4 = P_4 \oplus D_5 \oplus D_6 \oplus D_7 \oplus D_8$</li>
</ul>
<p>组合指征值 $S_4S_3S_2S_1$ 即为错误发生的物理位置。</p>
<h4>移码与补码的关系</h4>
<p>与其对应的移码之间仅有最高位（符号位）相反，其余各位完全一致。</p>
<p>:::tip[常见类型转换与数据大小变化]</p>
<ul>
<li><strong>无符号数扩展</strong>：直接在高位补 0，数值绝对不变。</li>
<li><strong>有符号数扩展</strong>：采用符号扩展，正数高位补 0，负数高位补 1，数值绝对不变。</li>
<li><strong>大类型转小类型</strong>：发生截断，极易导致溢出，从而使原数值的符号和数值发生剧变。
:::</li>
</ul>
<p>:::important[浮点数的溢出判定]
浮点数的溢出与定点数不同。浮点数是否发生溢出，完全取决于其<strong>阶码是否发生溢出</strong>（当阶码超出其最大正阶表示范围时即为上溢，当阶码低于最小负阶表示范围时即为下溢，下溢一般当作 0 处理，不引发系统中断）。尾数的溢出只影响表示精度，通常可通过规格化或舍入进行修正，不直接触发系统溢出报错。
:::</p>
<hr />
<h3>常见考试易错点警示</h3>
<p>:::warning[易错点 1：海明码公式中字符含义混淆]
公式 $2^r \ge m + r + 1$ 中， $m$ 代表的是<strong>纯数据位的长度</strong>，而不是总的海明码长度。在计算时切忌直接代入总长度。
:::</p>
<p>:::warning[易错点 2：浮点数规格化判断中正负数的差异]
在补码浮点数中，规格化尾数的最高数值位与符号位必须相反。对于负数，其尾数最高数值位必须是 0，即格式为 $1.0\dots\dots_2$。许多考生常误认为负数规格化也必须是最高位为 1。
:::</p>
<hr />
<h2>2.6 历年期末真题分类详解</h2>
<p>本节整理了 5 份期末考试真题中涉及本章的全部考题，进行系统化分类与解析。</p>
<h3>2.6.1 原码、反码、补码、移码转换</h3>
<h4>Q1 (2022-2023-2 简答题)</h4>
<p><strong>题干</strong>：用一个字节存放有符号整数，若某数真值为 $-75$，请写出它的四种机器数（原码、反码、补码、移码）。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>一个字节为 8 位，最高位为符号位，剩余 7 位为数值位。</li>
<li>$75=64+8+2+1=1001011_2$，写成 7 位为 <code>1001011</code>。</li>
<li>负数原码：符号位为 1，数值位为绝对值：
$
[-75]_{\text{原}}=11001011
$</li>
<li>负数反码：符号位不变，数值位取反：
$
[-75]_{\text{反}}=10110100
$</li>
<li>负数补码：反码末位加 1：
$
[-75]_{\text{补}}=10110101
$</li>
<li>移码：补码符号位取反：
$
[-75]_{\text{移}}=00110101
$</li>
</ol>
<p><strong>答案</strong>：原码 <code>11001011</code>，反码 <code>10110100</code>，补码 <code>10110101</code>，移码 <code>00110101</code>。</p>
<h4>Q2 (2020-2021 A卷 计算题)</h4>
<p><strong>题干</strong>：已知 $X_{\text{补}}=1.10010$，$Y_{\text{原}}=1.01110$，则 $X$ 和 $Y$ 的真值分别为多少？</p>
<p><strong>解析</strong>：</p>
<ol>
<li>$X_{\text{补}}=1.10010$，符号位为 1，是负数。</li>
<li>负数补码求真值：对补码求补得到绝对值。
$$
1.10010 \rightarrow \text{数值位 } 10010
$$
取反：
$$
01101
$$</li>
</ol>
<p>加 1：
$$
01110
$$</p>
<p>所以：
$$
X=-0.01110_2
$$
3. $Y_{\text{原}}=1.01110$，原码符号位为 1，直接表示负数：
$$
Y=-0.01110_2
$$</p>
<p><strong>答案</strong>：$X=-0.01110_2$，$Y=-0.01110_2$。</p>
<h4>Q3 (2024-2025 C卷 填空题)</h4>
<p><strong>题干</strong>：已知数的补码是 $1.10010$，左边起第一个 1 是符号位，它的真值应为________。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>补码 $1.10010$ 的符号位为 1，是负数。</li>
<li>对数值位 <code>10010</code> 取反：
$
10010 \rightarrow 01101
$</li>
<li>末位加 1：
$
01101+1=01110
$</li>
<li>所以原码为：
$
1.01110
$</li>
<li>真值为：
$
-0.01110_2
$</li>
<li>转十进制：
$
0.01110_2=2^{-2}+2^{-3}+2^{-4}=0.25+0.125+0.0625=0.4375
$</li>
</ol>
<p><strong>答案</strong>：$-0.01110_2$，即 $-0.4375$。</p>
<h4>Q4 (2024-2025 C卷 填空题)</h4>
<p><strong>题干</strong>：若寄存器内容为 10000000，若它等于 $-0$，则为________码。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>原码中：
$
+0=00000000,\quad -0=10000000
$</li>
<li>反码中：
$
+0=00000000,\quad -0=11111111
$</li>
<li>补码中 0 唯一：
$
0=00000000
$</li>
<li>题中 <code>10000000</code> 表示 $-0$，只能是原码。</li>
</ol>
<p><strong>答案</strong>：原码。</p>
<h4>Q5 (2020-2021 B卷 选择题)</h4>
<p><strong>题干</strong>：设 $x$ 为整数，$x_{\text{原}} = 1,000 \ 0001$，则其真值为（ ）。
A. +1
B. -1
C. +128
D. -128</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$+1$ 的原码应为 <code>0,0000001</code>，其符号位应为 0。</li>
<li><strong>B [正确]</strong>：<code>1,0000001</code> 的符号位为 1，数值位为 1，其代表的真值为 $-1$。</li>
<li><strong>C [错误]</strong>：8 位原码的数值位只有 7 位，能够表示的最大正数为 $+127$，无法表示 $+128$。</li>
<li><strong>D [错误]</strong>：8 位原码能够表示的最小负数为 $-127$，无法表示 $-128$。</li>
</ul>
<h4>Q6 (2019秋18级测试 计算题)</h4>
<p><strong>题干</strong>：已知 $x = 0.1001$，$y = -0.1010$，求 $x_{\text{补}}$， $y_{\text{原}}$， $y_{\text{补}}$， $x_{\text{移}}$， $y_{\text{移}}$。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>$x=0.1001$ 是正数，正数原码、反码、补码相同：
$
x_{\text{补}}=0.1001
$</li>
<li>$x$ 的移码等于补码符号位取反：
$
x_{\text{移}}=1.1001
$</li>
<li>$y=-0.1010$ 是负数，原码为：
$
y_{\text{原}}=1.1010
$</li>
<li>负数反码：符号位不变，数值位取反：
$
y_{\text{反}}=1.0101
$</li>
<li>负数补码：反码末位加 1：
$
y_{\text{补}}=1.0110
$</li>
<li>$y$ 的移码等于补码符号位取反：
$
y_{\text{移}}=0.0110
$</li>
</ol>
<p><strong>答案</strong>：$x_{\text{补}}=0.1001$，$y_{\text{原}}=1.1010$，$y_{\text{补}}=1.0110$，$x_{\text{移}}=1.1001$，$y_{\text{移}}=0.0110$。</p>
<h4>Q7 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：在机器码 ________ 和 ________ 中，零的编码是唯一的。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>原码有：
$
+0=00000000,\quad -0=10000000
$
零不唯一。</li>
<li>反码有：
$
+0=00000000,\quad -0=11111111
$
零不唯一。</li>
<li>补码中 0 只有一种编码。</li>
<li>移码中 0 也只有一种编码。</li>
</ol>
<p><strong>答案</strong>：补码、移码。</p>
<h4>Q8 (2020-2021 B卷 选择题)</h4>
<p><strong>题干</strong>：某机字长 16 位，采用补码形式（其中 1 位为符号位），则机器数所能表示的范围是（ ）。
A. $-32767 \sim +32767$
B. $-32768 \sim +32768$
C. $-32768 \sim +32767$
D. $-32767 \sim +32768$</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：
16 位补码定点整数（1 位符号位，15 位数值位）的表示范围为：
$$
-2^{15} \sim 2^{15} - 1
$$
即：
$$
-32768 \sim +32767
$$</p>
<ul>
<li><strong>A [错误]</strong>：未包含最小值 $-32768$。</li>
<li><strong>B [错误]</strong>：最大正数超出了 16 位补码定点整数的表示范围，最大正数应为 $+32767$。</li>
<li><strong>C [正确]</strong>：范围为 $-32768 \sim +32767$，与 16 位补码定点整数的表示范围一致。</li>
<li><strong>D [错误]</strong>：最小值和最大值均不对。</li>
</ul>
<hr />
<h3>2.6.2 补码加减运算</h3>
<h4>Q9 (2022-2023-2 计算题)</h4>
<p><strong>题干</strong>：已知 $A = -1001$，$B = -0101$，求 $A+B$ 的补码。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>$A=-1001_2$，用 5 位表示：
$
A_{\text{原}}=1,1001
$
$
A_{\text{反}}=1,0110
$
$
A_{\text{补}}=1,0111
$</li>
<li>$B=-0101_2$，用 5 位表示：
$
B_{\text{原}}=1,0101
$
$
B_{\text{反}}=1,1010
$
$
B_{\text{补}}=1,1011
$</li>
<li>补码相加：
$
10111+11011=110010
$</li>
<li>机器字长为 5 位，丢弃最高进位：
$
[A+B]_{\text{补}}=10010
$</li>
</ol>
<p><strong>答案</strong>：$[A+B]_{\text{补}}=10010$。</p>
<h4>Q10 (2020-2021 B卷 计算题)</h4>
<p><strong>题干</strong>：已知 $x = 0.1000$，$y = -0.0101$，求 $x_{\text{补}}$， $y_{\text{补}}$， $[x+y]_{\text{补}}$，并判断是否溢出。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>$x=0.1000$ 是正数：
$
x_{\text{补}}=0.1000
$</li>
<li>$y=-0.0101$：
$
y_{\text{原}}=1.0101
$
$
y_{\text{反}}=1.1010
$
$
y_{\text{补}}=1.1011
$</li>
<li>补码相加：
$
0.1000+1.1011=10.0011
$</li>
<li>丢弃最高进位：
$
[x+y]_{\text{补}}=0.0011
$</li>
<li>一正一负相加，不会溢出。</li>
</ol>
<p><strong>答案</strong>：$x_{\text{补}}=0.1000$，$y_{\text{补}}=1.1011$，$[x+y]_{\text{补}}=0.0011$，无溢出。</p>
<hr />
<h3>2.6.3 变形补码与双符号位</h3>
<h4>Q11 (2019秋18级测试 计算题)</h4>
<p><strong>题干</strong>：采用变形补码计算 $X+Y$ 的补码，并判断是否溢出。$X = +0.1101$，$Y = -0.1001$。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>变形补码使用双符号位：正数为 <code>00</code>，负数为 <code>11</code>。</li>
<li>$X$ 为正数：
$
X_{\text{补}}=00.1101
$</li>
<li>$Y=-0.1001$：
$
Y_{\text{原}}=11.1001
$
$
Y_{\text{反}}=11.0110
$
$
Y_{\text{补}}=11.0111
$</li>
<li>相加：
$
00.1101+11.0111=100.0100
$</li>
<li>丢弃最高进位，保留双符号位：
$
[X+Y]_{\text{补}}=00.0100
$</li>
<li>双符号位为 <code>00</code>，相同，无溢出。</li>
</ol>
<p><strong>答案</strong>：$[X+Y]_{\text{补}}=00.0100$，无溢出。</p>
<h4>Q12 (2024-2025 C卷 选择题)</h4>
<p><strong>题干</strong>：双符号位补码运算中，结果符号位为 10 表示（ ）。
A. 正溢出
B. 负溢出
C. 正常结果
D. 无溢出</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：在双符号位补码运算中，正溢出时的双符号位为 <code>01</code>，而不是 <code>10</code>。</li>
<li><strong>B [正确]</strong>：结果双符号位为 <code>10</code> 表示负溢出。</li>
<li><strong>C [错误]</strong>：运算结果正常（无溢出）时，双符号位应相同，即为 <code>00</code>（正数）或 <code>11</code>（负数）。</li>
<li><strong>D [错误]</strong>：无溢出时的双符号位为 <code>00</code> 或 <code>11</code>，若为 <code>10</code> 则表明发生了负溢出。</li>
</ul>
<h4>Q13 (2024-2025 C卷 计算题)</h4>
<p><strong>题干</strong>：用双符号位补码运算方法计算 $x = 0.1010$，$y = 0.1100$ 的和，并分析溢出状态。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>两数均为正数，双符号位为 <code>00</code>：
$
x_{\text{补}}=00.1010
$
$
y_{\text{补}}=00.1100
$</li>
<li>相加：
$
00.1010+00.1100=01.0110
$</li>
<li>结果双符号位为 <code>01</code>。</li>
<li><code>01</code> 表示正溢出。</li>
</ol>
<p><strong>答案</strong>：$[x+y]_{\text{补}}=01.0110$，发生正溢出。</p>
<h4>Q14 (2022-2023-2 判断题)</h4>
<p><strong>题干</strong>：采用变形补码进行加减运算可以避免溢出。（ ）</p>
<p><strong>解析</strong>：</p>
<ol>
<li>变形补码用双符号位表示。</li>
<li>双符号位只能判断是否溢出。</li>
<li>它不能扩大机器字长，也不能避免溢出。</li>
</ol>
<p><strong>答案</strong>：错。</p>
<hr />
<h3>2.6.4 溢出判断</h3>
<h4>Q15 (2020-2021 A卷 选择题)</h4>
<p><strong>题干</strong>：在定点运算器中，无论是采用双符号位还是采用单符号位，必须有（ ）。
A. 译码电路
B. 编码电路
C. 溢出判断电路
D. 移位电路</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：译码电路在算术逻辑单元中用于指令或地址译码，并不是判断定点数加减运算溢出的必需部件。</li>
<li><strong>B [错误]</strong>：编码电路用于将特定信号转换成二进制代码，不是溢出判断的核心逻辑部件。</li>
<li><strong>C [正确]</strong>：无论是采用双符号位（判断两符号位是否相同）还是单符号位（判断最高数值位进位与符号位进位是否相同），运算器都需要配备溢出判断电路来检测运算结果是否超出了机器字长所能表示的数值范围。</li>
<li><strong>D [错误]</strong>：移位电路主要用于实现数据的算术移位和逻辑移位操作，并非溢出判断的必需部件。</li>
</ul>
<h4>Q16 (2019秋18级测试 选择题)</h4>
<p><strong>题干</strong>：在定点机中执行算术运算时会产生溢出，其根本原因是（ ）。
A. 主频过高
B. 运算结果无法表示
C. 寄存器位数不够
D. 内存容量不足</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：时钟主频仅决定计算机的运算速度，与算术运算是否产生溢出无关。</li>
<li><strong>B [正确]</strong>：溢出的定义及根本原因在于运算结果超出了机器字长所能表示的最大数值范围，导致该运算结果无法在定点机中正确表示。</li>
<li><strong>C [错误]</strong>：尽管寄存器位数有限是机器表示范围有限的原因，但溢出的直接根本表现依然是当前的具体运算结果超出了该范围而无法表示。</li>
<li><strong>D [错误]</strong>：内存容量的大小决定程序与数据的存储空间，与算术运算中发生的数值溢出无关。</li>
</ul>
<h4>Q17 (2024-2025 C卷 判断题)</h4>
<p><strong>题干</strong>：溢出判断电路一般用"与或非"门来实现。（ ）</p>
<p><strong>解析</strong>：</p>
<ol>
<li>单符号位补码溢出常用进位异或判断。</li>
<li>双符号位补码溢出常用两个符号位异或判断。</li>
<li>因此常用的是异或逻辑，不是“与或非”门。</li>
</ol>
<p><strong>答案</strong>：错。</p>
<hr />
<h3>2.6.5 移位运算</h3>
<h4>Q18 (2022-2023-2 填空题)</h4>
<p><strong>题干</strong>：补码定点整数 01010101 左移两位后的值为___。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>原数：
$
01010101
$</li>
<li>算术左移时，符号位保持不变，数值位左移，右端补 0。</li>
<li>符号位为 <code>0</code>，数值位为 <code>1010101</code>。</li>
<li>数值位左移两位：
$
1010101 \rightarrow 1010100
$</li>
<li>加上符号位：
$
01010100
$</li>
<li>左移过程中有有效位被丢弃，若要求按数值乘 $2^2$ 解释，则发生溢出。</li>
</ol>
<p><strong>答案</strong>：机器码结果为 <code>01010100</code>。</p>
<h4>Q19 (2020-2021 A卷 填空题)</h4>
<p><strong>题干</strong>：补码定点整数 01010100 算术右移两位后的值为（ ）。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>原数：
$
01010100
$</li>
<li>符号位为 0，是正数。</li>
<li>算术右移时，左侧补符号位 0。</li>
<li>右移两位：
$
01010100 \rightarrow 00010101
$</li>
</ol>
<p><strong>答案</strong>：<code>00010101</code>。</p>
<hr />
<h3>2.6.6 定点数表示范围</h3>
<h4>Q20 (2022-2023-2 填空题)</h4>
<p><strong>题干</strong>：8 位原码能表示的不同数据有___个。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>8 位二进制编码共有 $2^8 = 256$ 个不同的状态。</li>
<li>在原码表示法中，零有两种不同的编码形式，即正零（$00000000_2$）和负零（$10000000_2$），它们代表同一个数值 0。</li>
<li>因此，8 位原码能表示的不同数据个数为 $256 - 1 = 255$ 个。其表示的具体范围为 $-127 \sim +127$。</li>
</ol>
<hr />
<h3>2.6.7 浮点数与规格化</h3>
<h4>Q21 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：浮点数 $-20.5$，根据 IEEE 754 单精度浮点数存储，其机器码是___。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>十进制转二进制：
$
20.5=10100.1_2
$</li>
<li>规格化：
$
10100.1_2=1.01001_2\times 2^4
$</li>
<li>符号位：
$
S=1
$</li>
<li>阶码偏置为 127：
$
E=4+127=131=10000011_2
$</li>
<li>尾数取小数部分 <code>01001</code>，补足 23 位：
$
M=01001000000000000000000
$</li>
<li>拼接：
$
1\ 10000011\ 01001000000000000000000
$</li>
<li>转十六进制：
$
1100\ 0001\ 1010\ 0100\ 0000\ 0000\ 0000\ 0000=C1A40000H
$</li>
</ol>
<p><strong>答案</strong>：<code>C1A40000H</code>。</p>
<h4>Q22 (2019秋18级测试 选择题)</h4>
<p><strong>题干</strong>：若浮点数用补码表示，则判断运算结果是否为规格化数的方法是（ ）。
A. 阶符与尾符相同
B. 阶符与尾符相异
C. 数符与尾数小数点后第一位数字相异
D. 数符与尾数小数点后第一位数字相同</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：浮点数的规格化只取决于尾数，与阶符（阶码的符号位）和尾符（尾数的符号位）是否相同无关。</li>
<li><strong>B [错误]</strong>：浮点数的规格化只取决于尾数，与阶符和尾符是否相异无关。</li>
<li><strong>C [正确]</strong>：补码浮点数的规格化尾数形式要求符号位（数符）与尾数小数点后第一位数字（即尾数数值的最高位）相异。对于规格化的正数尾数，其形式为 <code>0.1...</code>；对于规格化的负数尾数，其形式为 <code>1.0...</code>。</li>
<li><strong>D [错误]</strong>：若数符与尾数小数点后第一位数字相同（即正数为 <code>0.0...</code>，负数为 <code>1.1...</code>），则说明尾数尚未进行规格化。</li>
</ul>
<h4>Q23 (2019秋18级测试 填空题)</h4>
<p><strong>题干</strong>：浮点加、减运算的步骤是 <strong><strong><strong><strong>、</strong></strong></strong></strong>、规格化、舍入、溢出判断。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>浮点加减先让两个数阶码相同，称为对阶。</li>
<li>阶码相同后，对尾数进行加减，称为尾数求和。</li>
<li>后续步骤为规格化、舍入、溢出判断。</li>
</ol>
<p><strong>答案</strong>：对阶、尾数求和。</p>
<h4>Q24 (2024-2025 C卷 简答题)</h4>
<p><strong>题干</strong>：请说明浮点数加减运算中对阶的原则，并以 $A = 0.1101 \times 2^3$，$B = 0.1010 \times 2^1$ 计算 $A+B$。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>对阶原则：小阶向大阶看齐。</li>
<li>$A$ 的阶码为 3，$B$ 的阶码为 1。</li>
<li>因为 $1&lt;3$，所以将 $B$ 的阶码升到 3。</li>
<li>阶码每增加 1，尾数右移 1 位；$B$ 的阶码增加 2，尾数右移 2 位：
$
B=0.1010\times 2^1=0.001010\times 2^3
$</li>
<li>尾数相加：
$
0.1101+0.001010=0.111110
$</li>
<li>所以：
$
A+B=0.111110\times 2^3
$</li>
<li>尾数为 <code>0.111110</code>，符号位 0 与最高数值位 1 相异，已规格化。</li>
</ol>
<p><strong>答案</strong>：对阶原则是小阶向大阶看齐；$A+B=0.111110\times 2^3$。</p>
<hr />
<h3>2.6.8 数据校验：奇偶校验</h3>
<h4>Q25 (2020-2021 B卷 填空题)</h4>
<p><strong>题干</strong>：需要传输 "11001110"，其奇校验位为（ ）。</p>
<p><strong>解析</strong>：</p>
<ol>
<li>数据 <code>11001110</code> 中 1 的个数为：
$
5
$</li>
<li>奇校验要求数据位和校验位中 1 的总数为奇数。</li>
<li>现在 1 的个数已经是奇数，所以校验位应为 0。</li>
</ol>
<p><strong>答案</strong>：0。</p>
<h4>Q26 (2022-2023-2 判断题)</h4>
<p><strong>题干</strong>：奇偶校验可以校验奇数个位的出错，并能确定出错的位置。（ ）</p>
<p><strong>解析</strong>：</p>
<ol>
<li>奇偶校验可以发现奇数个位发生错误。</li>
<li>但它只能判断“有错”，不能判断“错在哪里”。</li>
<li>所以不能确定出错位置。</li>
</ol>
<p><strong>答案</strong>：错。</p>
<hr />
<h3>2.6.9 数据校验：海明码</h3>
<h4>Q27 (2020-2021 B卷 选择题)</h4>
<p><strong>题干</strong>：用海明码对长度为 8 位的数据进行检错和纠错时，若能纠正一位错，则校验位数至少为（ ）。
A. 2
B. 3
C. 4
D. 5</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：若校验位数为 2，则根据海明不等式 $2^r \ge m + r + 1$，此时 $r=2$，左式为 $2^2 = 4$，而右式为 $8 + 2 + 1 = 11$，由于 $4 &lt; 11$，无法纠正一位错。</li>
<li><strong>B [错误]</strong>：若校验位数为 3，左式为 $2^3 = 8$，而右式为 $8 + 3 + 1 = 12$，由于 $8 &lt; 12$，无法纠正一位错。</li>
<li><strong>C [正确]</strong>：设校验位数为 $r$，数据位数 $m=8$。若能纠正一位错，校验位数需满足海明不等式 $2^r \ge m + r + 1$。将各选项代入，当 $r=4$ 时，左式为 $2^4 = 16$，右式为 $8 + 4 + 1 = 13$，满足 $16 \ge 13$，因此校验位数至少为 4。</li>
<li><strong>D [错误]</strong>：虽然 $r=5$ 时满足 $2^5 = 32 \ge 8 + 5 + 1 = 14$，但题目要求的是“至少”的校验位数，因此 5 不是最小值。</li>
</ul>
<hr />
<h3>2.6.10 数据校验：CRC</h3>
<h4>Q28 (2024-2025 C卷 选择题)</h4>
<p><strong>题干</strong>：CRC 校验码通过（ ）实现检错。
A. 多项式除法余数
B. 汉明距离
C. 奇偶统计
D. 累加和</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：CRC 校验将要发送的数据视为多项式的系数，并除以一个约定的生成多项式，进行模 2 除法，将得到的余数作为校验码附加在数据后面。接收端用相同的生成多项式对接收到的数据进行模 2 除法，若余数不为 0 则表示数据传输过程中发生错误。</li>
<li><strong>B [错误]</strong>：汉明距离是指两个等长码字之间不同二进制位的个数，是用来度量某种编码的检错和纠错能力的，并非 CRC 校验的直接检错机制。</li>
<li><strong>C [错误]</strong>：奇偶统计用于奇偶校验，通过统计数据中“1”的个数是奇数还是偶数来产生 1 位校验位，只能检测出奇数位错误，无法用于 CRC 校验。</li>
<li><strong>D [错误]</strong>：累加和校验是通过将所有发送数据字累加并取反等操作生成校验和，不涉及多项式模 2 除法。</li>
</ul>
<hr />
<h3>2.6.11 大端与小端存储</h3>
<h4>Q29 (2020-2021 A卷 选择题)</h4>
<p><strong>题干</strong>：double 变量机器码为 <code>1122 3344 5566 7788H</code>，小端存放，则内存地址 <code>0000 8046H</code> 中存放的是（ ）。
A. 22H
B. 33H
C. 77H
D. 88H</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：<code>double</code> 类型变量在机器中占 8 字节，本题十六进制机器码为 <code>1122 3344 5566 7788H</code>。小端存放的规则为“低有效字节存放在低地址内存单元”，各字节从低地址到高地址（假设从 <code>8040H</code> 开始）分布依次为：<code>8040H</code> 存 <code>88H</code>，<code>8041H</code> 存 <code>77H</code>，<code>8042H</code> 存 <code>66H</code>，<code>8043H</code> 存 <code>55H</code>，<code>8044H</code> 存 <code>44H</code>，<code>8045H</code> 存 <code>33H</code>，<code>8046H</code> 存 <code>22H</code>，<code>8047H</code> 存 <code>11H</code>。因此，内存地址 <code>0000 8046H</code> 中存放的是 <code>22H</code>。</li>
<li><strong>B [错误]</strong>：<code>33H</code> 存放在内存地址 <code>0000 8045H</code> 中。</li>
<li><strong>C [错误]</strong>：<code>77H</code> 存放在内存地址 <code>0000 8041H</code> 中。</li>
<li><strong>D [错误]</strong>：<code>88H</code> 存放在内存地址 <code>0000 8040H</code> 中。</li>
</ul>
<h4>Q30 (2022-2023-2 选择题)</h4>
<p><strong>题干</strong>：同上题条件，问内存地址 <code>0000 8042H</code> 中存放的是（ ）。
A. 22H
B. 33H
C. 66H
D. 88H</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>22H</code> 存放在内存地址 <code>0000 8046H</code> 中。</li>
<li><strong>B [错误]</strong>：<code>33H</code> 存放在内存地址 <code>0000 8045H</code> 中。</li>
<li><strong>C [正确]</strong>：根据 Q29 中的小端地址字节映射分布，从低地址到高地址依次为：<code>8040H</code> 为 <code>88H</code>，<code>8041H</code> 为 <code>77H</code>，<code>8042H</code> 为 <code>66H</code>，<code>8043H</code> 为 <code>55H</code>，<code>8044H</code> 为 <code>44H</code>，<code>8045H</code> 为 <code>33H</code>，<code>8046H</code> 为 <code>22H</code>，<code>8047H</code> 为 <code>11H</code>。因此内存地址 <code>0000 8042H</code> 中存放的是 <code>66H</code>。</li>
<li><strong>D [错误]</strong>：<code>88H</code> 存放在内存地址 <code>0000 8040H</code> 中。</li>
</ul>
<h4>Q31 (2024-2025 C卷 选择题)</h4>
<p><strong>题干</strong>：以下哪种存储方式将数据的最高位有效字节存储在最低地址内存单元？（ ）
A. 大端方式
B. 小端方式
C. 混合端方式
D. 任意方式</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：在大端存储方式中，数据的最高有效字节存储在最低的内存地址单元，最低有效字节则存储在最高的内存地址单元。</li>
<li><strong>B [错误]</strong>：在小端存储方式中，数据的最低有效字节存储在最低的内存地址单元，最高有效字节存储在最高的内存地址单元。</li>
<li><strong>C [错误]</strong>：混合端方式指字节序中大端和小端混合使用的方式，不符合将数据的最高有效字节存储在最低地址单元 of the definition.</li>
<li><strong>D [错误]</strong>：任意方式没有固定的存储规则。</li>
</ul>
<hr />
<h3>2.6.12 类型转换</h3>
<h4>Q32 (2022-2023-2 选择题)</h4>
<p><strong>题干</strong>：<code>unsigned short usi = 65535; short si = usi;</code> 执行后 <code>si</code> 的值是（ ）。
A. -1
B. 0
C. 32767
D. -32768</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：<code>unsigned short</code> 类型的变量 <code>usi</code> 的值为 $65535$，其对应的 16 位二进制机器码为 $1111111111111111_2$。在赋值给 <code>short si</code> 时，其底层的二进制位模式保持不变，仍为 $1111111111111111_2$。在 16 位有符号补码中，该全 1 位模式解析的真值为 $-1$。</li>
<li><strong>B [错误]</strong>：$0$ 的补码是 $0000000000000000_2$。</li>
<li><strong>C [错误]</strong>：$32767$ 的补码是 $0111111111111111_2$。</li>
<li><strong>D [错误]</strong>：$-32768$ 的补码是 $1000000000000000_2$。</li>
</ul>
<hr />
<h3>2.6.13 负数十进制向十六进制补码表示的转换</h3>
<h4>Q33 (2022-2023-2 选择题 / 2024-2025 C卷)</h4>
<p><strong>题干</strong>：假定一个十进制数为 $-66$，按补码形式存放在一个 8 位寄存器中，该寄存器的内容用十六进制表示为（ ）。
A. C2H
B. BEH
C. BDH
D. 42H</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>C2H</code> 对应的二进制为 $11000010_2$，是 $-66$ 的原码。</li>
<li><strong>B [正确]</strong>：十进制数 $66$ 的二进制表示为 $01000010_2$。求 $-66$ 的补码过程为：原码为 $11000010_2$，除符号位外各位按位取反得到反码 $10111101_2$，反码在最低位加 1 得到补码为 $10111110_2$。转为十六进制，高 4 位 $1011_2$ 对应 $\text{B}$，低 4 位 $1110_2$ 对应 $\text{E}$，因此该补码用十六进制表示为 <code>BEH</code>。</li>
<li><strong>C [错误]</strong>：<code>BDH</code> 对应的二进制为 $10111101_2$，是 $-66$ 的反码。</li>
<li><strong>D [错误]</strong>：<code>42H</code> 对应的二进制为 $01000010_2$，是 $+66$ 的原码、反码及补码。</li>
</ul>
<hr />
<h2>2.7 典型期末练习题精选</h2>
<p>本节精选了练习题库中与期末考点契合度高、且最具代表性的典型题目进行详解，以供备考复习。</p>
<h3>2.7.1 机器码概念与转换</h3>
<h4>习题 1 (寄存器内容与移码真值判定)</h4>
<p><strong>原题</strong>：若寄存器内容为 00000000，若它等于 -128，则为（ ）
A. 原码
B. 补码
C. 反码
D. 移码</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：8 位定点整数原码 <code>00000000</code> 表示的真值是 $+0$，而非 $-128$。</li>
<li><strong>B [错误]</strong>：8 位定点整数补码 <code>00000000</code> 表示的真值是 $0$，而非 $-128$。</li>
<li><strong>C [错误]</strong>：8 位定点整数反码 <code>00000000</code> 表示的真值是 $+0$，而非 $-128$。</li>
<li><strong>D [正确]</strong>：8 位移码的偏置值通常为 $128$，移码的数值等于真值加上偏置值。因此，对于真值 $-128$，其对应的移码编码为 $-128 + 128 = 0$，用 8 位二进制表示即为 <code>00000000</code>。</li>
</ul>
<h4>习题 2 (寄存器内容与有符号整数判定)</h4>
<p><strong>原题</strong>：若寄存器内容为 1111 1111，若它等于 -1，则为（ ）。
A. 原码
B. 补码
C. 反码</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：8 位定点整数原码 <code>11111111</code> 的最高位为符号位 1 表示负数，数值位全 1 表示 127，因此真值为 $-127$。</li>
<li><strong>B [正确]</strong>：8 位定点整数补码 <code>11111111</code> 是全 1 序列，根据补码规则，其对应的真值为 $-1$。</li>
<li><strong>C [错误]</strong>：8 位定点整数反码 <code>11111111</code> 表示的真值是 $-0$。</li>
</ul>
<h4>习题 3 (机器码移码实际转换)</h4>
<p><strong>原题</strong>：假设某计算机的字长为 8 位，则数 -1010110 的移码表示形式为
A. 00101010
B. 11010110
C. 10101010
D. 01010110</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：真值为 $-1010110_2$（即十进制数 $-86$）。其 8 位原码为 $11010110_2$，除符号位外各位按位取反得到反码 $10101001_2$，反码在最低位加 1 得到补码为 $10101010_2$。移码的符号位与补码相反，其余位相同，故其移码为 $00101010_2$。</li>
<li><strong>B [错误]</strong>：<code>11010110</code> 为该数的原码表示。</li>
<li><strong>C [错误]</strong>：<code>10101010</code> 为该数的补码表示。</li>
<li><strong>D [错误]</strong>：<code>01010110</code> 为 $+86$ 的原码、反码及补码表示。</li>
</ul>
<hr />
<h3>2.7.2 移位与类型扩展</h3>
<h4>习题 4 (有符号数算术移位)</h4>
<p><strong>原题</strong>：x = −26，用一个字节存放其补码，[x]补 = <strong><strong><strong><strong><strong><strong>，将 x 的补码算术左移一位为</strong></strong></strong></strong></strong></strong>，将 x 的补码算术右移一位为____________。</p>
<p><strong>解答</strong>：</p>
<ol>
<li>$26$ 的二进制为：
$
26=16+8+2=0011010_2
$</li>
<li>$-26$ 的原码：
$
1,0011010
$</li>
<li>反码：
$
1,1100101
$</li>
<li>补码：
$
1,1100110
$</li>
<li>算术左移一位：符号位不变，数值位左移，右端补 0：
$
1,1100110 \rightarrow 1,1001100
$</li>
<li>算术右移一位：符号位不变，左端补符号位 1：
$
1,1100110 \rightarrow 1,1110011
$</li>
</ol>
<p><strong>答案</strong>：$[x]_{\text{补}}=1,1100110$，算术左移一位为 <code>1,1001100</code>，算术右移一位为 <code>1,1110011</code>。</p>
<h4>习题 5 (十六进制补码类型扩展)</h4>
<p><strong>原题</strong>：16位补码0X8FA0扩展为32位应该是（  ）。
A. 0X0000 8FA0
B. 0XFFFF 8FA0
C. 0XFFFF FFA0
D. 0X8000 8FA0</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：高 16 位补 0 为零扩展，只适用于无符号数的扩展。</li>
<li><strong>B [正确]</strong>：16 位有符号补码 <code>0X8FA0</code> 的最高十六进制位为 <code>8</code>，对应的二进制最高位（符号位）为 $1$（因为 $8 = 1000_2$），表示该数是一个负数。负数补码进行有符号扩展（符号扩展）时，高位应全部填充符号位 $1$。16 个二进制位 1 用十六进制表示为 <code>FFFF</code>，因此扩展为 32 位后的结果为 <code>0XFFFF 8FA0</code>。</li>
<li><strong>C [错误]</strong>：低 16 位原为 <code>8FA0</code>，此选项将其更改为 <code>FFA0</code>，修改了原本的数据位，是错误的。</li>
<li><strong>D [错误]</strong>：高位没有进行完整的符号扩展，扩展后的位不正确。</li>
</ul>
<hr />
<h3>2.7.3 变形补码与溢出计算</h3>
<h4>习题 6 (变形补码与溢出计算)</h4>
<p><strong>原题</strong>：已知有符号整数[X]原=0,0101，[Y]原=1,1011，请采用双符号位的变形补码，计算[X+Y]补、[X-Y]补，分别判断是否溢出。</p>
<p><strong>解答</strong>：</p>
<ol>
<li>$[X]<em>{\text{原}}=0,0101$，所以：
$
X=+0101_2
$
双符号位补码为：
$
[X]</em>{\text{补}}=00,0101
$</li>
<li>$[Y]<em>{\text{原}}=1,1011$，所以：
$
Y=-1011_2
$
双符号位原码为：
$
[Y]</em>{\text{原}}=11,1011
$
反码为：
$
[Y]<em>{\text{反}}=11,0100
$
补码为：
$
[Y]</em>{\text{补}}=11,0101
$</li>
</ol>
<p><strong>计算 $[X+Y]_{\text{补}}$：</strong>
$$
[X+Y]_{\text{补}}=00,0101+11,0101=11,1010
$$
双符号位为 <code>11</code>，无溢出。</p>
<p><strong>计算 $[X-Y]_{\text{补}}$：</strong></p>
<ol>
<li>$X-Y=X+(-Y)$。</li>
<li>因为 $Y=-1011_2$，所以：
$
-Y=+1011_2
$</li>
<li>$[-Y]_{\text{补}}=00,1011$。</li>
<li>相加：
$
[X-Y]_{\text{补}}=00,0101+00,1011=01,0000
$</li>
<li>双符号位为 <code>01</code>，表示正溢出。</li>
</ol>
<p><strong>答案</strong>：$[X+Y]<em>{\text{补}}=11,1010$，无溢出；$[X-Y]</em>{\text{补}}=01,0000$，正溢出。</p>
<hr />
<h3>2.7.4 海明纠错码计算</h3>
<h4>习题 7 (汉明码纠错)</h4>
<p><strong>原题</strong>：已知接收到的汉明码为0100111（按配偶原则配置），试问欲传送的信息是什么？</p>
<p><strong>解答</strong>：</p>
<ol>
<li>汉明码为：
$
H_1H_2H_3H_4H_5H_6H_7=0100111
$</li>
<li>校验位在第 1、2、4 位：
$
P_1=H_1=0,\quad P_2=H_2=1,\quad P_3=H_4=0
$</li>
<li>数据位在第 3、5、6、7 位：
$
D_1=H_3=0,\quad D_2=H_5=1,\quad D_3=H_6=1,\quad D_4=H_7=1
$</li>
<li>偶校验下计算校验组：
$
S_1=H_1\oplus H_3\oplus H_5\oplus H_7
$
$
S_1=0\oplus0\oplus1\oplus1=0
$
$
S_2=H_2\oplus H_3\oplus H_6\oplus H_7
$
$
S_2=1\oplus0\oplus1\oplus1=1
$
$
S_3=H_4\oplus H_5\oplus H_6\oplus H_7
$
$
S_3=0\oplus1\oplus1\oplus1=1
$</li>
<li>指误字为：
$
S_3S_2S_1=110_2=6
$</li>
<li>第 6 位出错，将 $H_6$ 从 1 改为 0。</li>
<li>纠正后的汉明码为：
$
0100101
$</li>
<li>取数据位第 3、5、6、7 位：
$
D_1D_2D_3D_4=0101
$</li>
</ol>
<p><strong>答案</strong>：欲传送的信息为 <code>0101</code>。</p>
<hr />
<h2>习题2：课后习题详解</h2>
<p>以下为本章教材配属的全部课后习题及详细解答。</p>
<p><img src="https://img.picr2.online/1782640814959_image.webp" alt="课后习题1" />
<img src="https://img.picr2.online/1782640839935_image.webp" alt="课后习题2" />
<img src="https://img.picr2.online/1782640856785_image.webp" alt="课后习题3" /></p>
<hr />
<h3>习题2.1 解释下列名词</h3>
<table>
<thead>
<tr>
<th>名词</th>
<th>详细物理与学术解释</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>真值</strong></td>
<td>机器数所表示的实际数学数值，如机器码 <code>11111111</code> 若按 8 位补码解释，其代表的实际真值为 -1。</td>
</tr>
<tr>
<td><strong>机器码</strong></td>
<td>数据在计算机内部实际存储和运算时的二进制编码形态。</td>
</tr>
<tr>
<td><strong>原码</strong></td>
<td>最高位为符号位（正数用 0，负数用 1），其余各位用于表示该数数值的绝对值。</td>
</tr>
<tr>
<td><strong>反码</strong></td>
<td>正数的反码与其原码相同；负数的反码则是其符号位保持不变，其余各位数值按位取反。</td>
</tr>
<tr>
<td><strong>补码</strong></td>
<td>正数的补码与其原码相同；负数的补码则是其反码在最低位加 1。是现代计算机表示带符号整数的主流编码方式。</td>
</tr>
<tr>
<td><strong>移码</strong></td>
<td>将真值加上一个固定的偏移量（偏置值）后得到的机器码，常用于浮点数的阶码表示，方便硬件比对阶码大小。</td>
</tr>
<tr>
<td><strong>模</strong></td>
<td>编码系统的最大循环量。如 8 位二进制补码系统的模为 256。</td>
</tr>
<tr>
<td><strong>定点数</strong></td>
<td>小数点位置固定不变的机器数，主要用于表示纯整数或纯小数。</td>
</tr>
<tr>
<td><strong>浮点数</strong></td>
<td>小数点位置可以浮动的机器数，常表示为科学记数法形式，包含尾数和阶码两部分。</td>
</tr>
<tr>
<td><strong>溢出</strong></td>
<td>运算结果超出了机器字长所能表示的最大数值范围，包括正上溢和负下溢。</td>
</tr>
<tr>
<td><strong>精度溢出</strong></td>
<td>运算结果的有效位数超出了机器所能保留的最大精度位，需要进行舍入或截断处理。</td>
</tr>
<tr>
<td><strong>浮点数规格化</strong></td>
<td>通过调整尾数数值与阶码大小，使尾数最高有效位满足特定要求（如补码浮点数要求数符与尾数最高数值位异号），从而保证表示形式的唯一性。</td>
</tr>
<tr>
<td><strong>隐藏位</strong></td>
<td>在单精度和双精度浮点规格化表示中，尾数第一位必须为 1。该 1 在物理上被省略不存储，从而腾出 1 位用于增加精度。</td>
</tr>
<tr>
<td><strong>BCD码</strong></td>
<td>用二进制编码来表示十进制数码 0 至 9 的二进制编码方式。</td>
</tr>
<tr>
<td><strong>有权码</strong></td>
<td>二进制编码的每一位均被赋予固定权值的编码（例如 8421 编码）。</td>
</tr>
<tr>
<td><strong>无权码</strong></td>
<td>二进制编码中各位均没有固定权值的编码方式（例如余 3 码）。</td>
</tr>
<tr>
<td><strong>BID码</strong></td>
<td>二进制整型十进制表示法，是十进制浮点数的一种有效数值编码机制。</td>
</tr>
<tr>
<td><strong>DPD码</strong></td>
<td>高密度十进制压缩法，是十进制浮点数中用于高密度存储十进制数据的一种编码机制。</td>
</tr>
<tr>
<td><strong>二进制浮点数</strong></td>
<td>基数为 2 的浮点数编码，如单精度浮点数。</td>
</tr>
<tr>
<td><strong>十进制浮点数</strong></td>
<td>基数为 10 的浮点数编码，适用于金融、税务等不允许二进制转换精细舍入误差的计算场景。</td>
</tr>
<tr>
<td><strong>ASCII码</strong></td>
<td>采用 7 位二进制来编码表示英文字符、数字及控制字符的通用标准编码。</td>
</tr>
<tr>
<td><strong>机内码</strong></td>
<td>字符在计算机内部存储和传输时所使用的编码。</td>
</tr>
<tr>
<td><strong>字形码</strong></td>
<td>描述字形点阵或轮廓的数据，用于字符的显示与打印输出。</td>
</tr>
<tr>
<td><strong>字库</strong></td>
<td>存储所有字符字形码的物理字形点阵集合。</td>
</tr>
<tr>
<td><strong>码距</strong></td>
<td>编码系统中任意两个合法码字之间不同二进制位的最小个数，决定了检错和纠错的能力。</td>
</tr>
<tr>
<td><strong>校验码</strong></td>
<td>通过在有效数据位后附加多余的冗余位，用以在数据传输或存储中发现或纠正错误的编码。</td>
</tr>
<tr>
<td><strong>多重奇偶校验</strong></td>
<td>引入多个不同方向的奇偶校验机制，如水平偶校验与垂直偶校验结合，从而提高定位错误的能力。</td>
</tr>
<tr>
<td><strong>ECC码</strong></td>
<td>纠错码，是一种能够自动检测并定位、纠正错误的校验编码方式。</td>
</tr>
<tr>
<td><strong>海明码</strong></td>
<td>一种具有纠正一位错误、检测两位错误能力的线性分组校验码。</td>
</tr>
<tr>
<td><strong>CRC码</strong></td>
<td>循环冗余校验码，利用多项式模 2 除法的余数作为校验位，检错能力极强。</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题2.2 选择题（考研真题）</h3>
<h4>( 1 ) 由 3 个“1”和 5 个“0”组成的 8 位二进制补码，能表示的最小整数是________。</h4>
<p>A. $-126$
B. $-125$
C. $-32$
D. $-3$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$-126$ 的 8 位补码表示为 $10000010_2$，其中有 2 个 1 和 6 个 0，不符合题干中的数量条件。</li>
<li><strong>B [正确]</strong>：8 位补码定点数对应的真值计算式为 $X = -128 \times x_7 + \sum_{i=0}^{6} x_i 2^i$。要使真值最小，必须使符号位为 1。符号位确定为 1 后，剩下 2 个 1 和 5 个 0 可以在低 7 位中进行分配。为了让真值尽可能小，低 7 位的无符号数值应尽可能小，即把剩下的 2 个 1 放在最低的两位，得到机器码 $10000011_2$。其对应的真值为 $-128 + 2^1 + 2^0 = -125$。</li>
<li><strong>C [错误]</strong>：$-32$ 的 8 位补码表示为 $11100000_2$，虽然包含 3 个 1 和 5 个 0，但其真值大于 $-125$，不是该编码组合能表示的最小整数。</li>
<li><strong>D [错误]</strong>：$-3$ 的 8 位补码表示为 $11111101_2$，其中有 7 个 1 和 1 个 0，不符合条件。</li>
</ul>
<h4>( 2 ) 32 位补码所能表示的整数范围是________。</h4>
<p>A. $-2^{32} \sim 2^{31}-1$
B. $-2^{31} \sim 2^{31}-1$
C. $-2^{32} \sim 2^{32}-1$
D. $-2^{31} \sim 2^{32}-1$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$-2^{32}$ 超出了 32 位补码能表示的最小值。</li>
<li><strong>B [正确]</strong>：$n$ 位补码的数值范围公式为 $-2^{n-1} \sim 2^{n-1}-1$。将 $n=32$ 代入公式可得其范围为 $-2^{31} \sim 2^{31}-1$。</li>
<li><strong>C [错误]</strong>：此范围不是 32 位有符号整数的表示范围。</li>
<li><strong>D [错误]</strong>：正数范围上限 $2^{32}-1$ 是 32 位无符号整数的上限，而非补码的上限。</li>
</ul>
<h4>( 3 ) 已知带符号整数用补码表示，变量 $x, y, z$ 的机器数分别为 $\text{FFFDH}$、$\text{FFDFH}$、$\text{7FFCH}$，下列结论中，正确的是________。</h4>
<p>A. 若 $x, y$ 和 $z$ 为无符号整数，则 $z &lt; x &lt; y$
B. 若 $x, y$ 和 $z$ 为无符号整数，则 $x &lt; y &lt; z$
C. 若 $x, y$ 和 $z$ 为带符号整数，则 $x &lt; y &lt; z$
D. 若 $x, y$ 和 $z$ 为带符号整数，则 $y &lt; x &lt; z$</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：若作为无符号数解释，$\text{FFFDH} = 65533$，$\text{FFDFH} = 65503$，$\text{7FFCH} = 32764$，因此无符号下的大小关系为 $z &lt; y &lt; x$。</li>
<li><strong>B [错误]</strong>：由于无符号大小关系为 $z &lt; y &lt; x$，此排序错误。</li>
<li><strong>C [错误]</strong>：若作为有符号整数补码解释，$x = \text{FFFDH}$ 最低有效位为 1 且符号位为 1，其真值为 $-3$；$y = \text{FFDFH}$ 的符号位为 1，其真值为 $-33$；$z = \text{7FFCH}$ 的符号位为 0，其真值为 $32764$。因此，有符号大小关系为 $y &lt; x &lt; z$。</li>
<li><strong>D [正确]</strong>：根据计算得到的真值大小排序，满足 $y(-33) &lt; x(-3) &lt; z(32764)$。</li>
</ul>
<h4>( 4 ) 若 short 型变量 $x = -8190$，则 $x$ 的机器数是________。</h4>
<p>A. $\text{E002H}$
B. $\text{E001H}$
C. $\text{9FFFH}$
D. $\text{9FFEH}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：<code>short</code> 占 16 位。数值绝对值 $8190 = 2^{13} - 2 = 0001 \ 1111 \ 1111 \ 1110_2$。求 $-8190$ 的补码：原码为 $1001 \ 1111 \ 1111 \ 1110_2$，除符号位外各位取反得反码 $1110 \dots_2$。因反码为 $1110 \ 0000 \ 0000 \ 0001_2$，再加 1 得到补码为 $1110 \ 0000 \ 0000 \ 0010_2$，转换为十六进制即为 <code>E002H</code>。</li>
<li><strong>B [错误]</strong>：<code>E001H</code> 对应的真值为 $-8191$。</li>
<li><strong>C [错误]</strong>：<code>9FFFH</code> 对应的真值为 $-24577$，非 $-8190$ 的位模式。</li>
<li><strong>D [错误]</strong>：<code>9FFEH</code> 对应的真值为 $-24578$。</li>
</ul>
<h4>( 5 ) 考虑以下 C 语言代码：</h4>
<pre><code>unsigned short usi=65535;
short si=usi;
</code></pre>
<p>执行上述程序段后，$\text{si}$ 的值是________。
A. $-1$
B. $-32767$
C. $-32768$
D. $-65535$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：<code>usi</code> 值为 65535，其 16 位无符号数二进制形式为全 1，即 <code>FFFFH</code>。将 <code>usi</code> 赋值给有符号短整型 <code>si</code> 时，底层进行位拷贝，<code>si</code> 也是 <code>FFFFH</code>。16 位有符号数补码 <code>FFFFH</code> 解析对应的真值为 $-1$。</li>
<li><strong>B [错误]</strong>：$-32767$ 的 16 位有符号补码为 <code>8001H</code>。</li>
<li><strong>C [错误]</strong>：$-32768$ 的 16 位有符号补码为 <code>8000H</code>。</li>
<li><strong>D [错误]</strong>：$-65535$ 超出 16 位有符号整数的表示范围。</li>
</ul>
<h4>( 6 ) 假定编译器规定 int 和 short 类型长度分别为 32 位和 16 位，执行下列 C 语言语句：</h4>
<pre><code>unsigned short x=65530;
unsigned int y=x;
</code></pre>
<p>得到 $\text{y}$ 的机器数为________。
A. $\text{0000 7FFAH}$
B. $\text{0000 FFFAH}$
C. $\text{FFFF 7FFAH}$
D. $\text{FFFF FFFAH}$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：低 16 位变为了 <code>7FFAH</code>，与原本的值 <code>FFFAH</code> 不符。</li>
<li><strong>B [正确]</strong>：变量 $x$ 值为 65530，对应的 16 位十六进制为 <code>FFFAH</code>。由于 $x$ 为 <code>unsigned short</code>，将其转换为 <code>unsigned int</code> 时进行零扩展，在高 16 位填充 0，得到 32 位机器数为 <code>0000 FFFAH</code>。</li>
<li><strong>C [错误]</strong>：低位发生了改变，且高位填充不正确。</li>
<li><strong>D [错误]</strong>：高位填充为 <code>FFFF</code> 是符号扩展的结果，一般用于有符号整型的数据类型转换。</li>
</ul>
<h4>( 7 ) 有如下 C 语言程序段：<code>short si=-32767; unsigned short usi=si;</code> 执行上述两条语句后，$\text{usi}$ 的值为________。</h4>
<p>A. $-32767$
B. $32767$
C. $32768$
D. $32769$</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：<code>usi</code> 为无符号数，无法表示负数。</li>
<li><strong>B [错误]</strong>：$32767$ 的二进制无符号表示是 <code>7FFFH</code>。</li>
<li><strong>C [错误]</strong>：$32768$ 的二进制无符号表示是 <code>8000H</code>。</li>
<li><strong>D [正确]</strong>：变量 $si = -32767$ 的 16 位有符号补码表示为 $-32767 + 65536 = 32769 = \text{8001H}$。赋值给无符号类型的 <code>usi</code> 时进行位拷贝，使其机器数为 <code>8001H</code>，按无符号数解析对应的真值即为 $32769$。</li>
</ul>
<h4>( 8 ) C 语言代码如下：</h4>
<pre><code>int i = 32777;
short si = i;
int j = si;
</code></pre>
<p>执行后 $\text{j}$ 的真值为________。
A. $-32777$
B. $-32759$
C. $32759$
D. $32777$</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：真值不可能是 $-32777$。</li>
<li><strong>B [正确]</strong>：变量 $i = 32777 = \text{0000 8009H}$。转换为 16 位 <code>short</code> 类型时被直接截断保留低 16 位，即 <code>8009H</code>。有符号补码 <code>8009H</code> 的最高位为 1，真值计算为 $-32768 + 9 = -32759$。赋值给 <code>int j</code> 时进行符号扩展，因为是符号扩展，其真值保持不变，仍为 $-32759$。</li>
<li><strong>C [错误]</strong>：$32759$ 的 16 位补码是 <code>7FF7H</code>，截断后的机器码为 <code>8009H</code>，符号位为 1，故真值为负数。</li>
<li><strong>D [错误]</strong>：经过有符号短整型的截断之后，数据发生了溢出，不能恢复为原值 $32777$。</li>
</ul>
<h4>( 9 ) 下列数值中，不能用 IEEE 754 浮点格式精确表示的是________。</h4>
<p>A. $1.2$
B. $1.25$
C. $2.0$
D. $2.5$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：二进制浮点数能够精确表示的十进制小数其分母必须能写成 2 的整数次幂。$1.2 = \frac{6}{5}$，分母中含有非 2 的因子 5，因此将其转换为二进制时为无限循环小数，无法精确表示。</li>
<li><strong>B [错误]</strong>：$1.25 = \frac{5}{4} = \frac{5}{2^2}$，可以精确表示为二进制小数 $1.01_2$。</li>
<li><strong>C [错误]</strong>：$2.0 = 2^1$，可以精确表示。</li>
<li><strong>D [错误]</strong>：$2.5 = \frac{5}{2} = \frac{5}{2^1}$，可以精确表示为二进制小数 $10.1_2$。</li>
</ul>
<h4>( 10 ) float 型数据通常用 IEEE 754 单精度浮点数格式表示。若编译器将 float 型变量 $x$ 分配在一个 32 位浮点寄存器 FR1 中，且 $x = -8.25$，则 FR1 的内容是________。</h4>
<p>A. $\text{C104 0000H}$
B. $\text{C242 0000H}$
C. $\text{C184 0000H}$
D. $\text{C1C2 0000H}$</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：阶码字段正确但尾数字段表示的值不对。</li>
<li><strong>B [错误]</strong>：阶码部分计算错误。</li>
<li><strong>C [正确]</strong>：绝对值 $8.25 = 1000.01_2 = 1.00001_2 \times 2^3$。因为是负数，所以符号位 $S = 1$；阶码字段 $E = 3 + 127 = 130 = 10000010_2$；尾数字段 $M$ 去掉规格化隐藏位 1 并补 0 后得到 $0000100\dots_2$。拼接得到二进制机器码：$1 \ | \ 10000010 \ | \ 00001000000000000000000_2 \implies \text{C184 0000H}$。</li>
<li><strong>D [错误]</strong>：拼接出的机器码值不正确。</li>
</ul>
<h4>( 11 ) 某数采用 IEEE 754 单精度浮点数格式表示为 $\text{C640 0000H}$，则该数的值是________。</h4>
<p>A. $-1.5 \times 2^{13}$
B. $-1.5 \times 2^{12}$
C. $-0.5 \times 2^{13}$
D. $-0.5 \times 2^{12}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：将十六进制机器码 <code>C640 0000H</code> 展开为二进制串：$1 \ | \ 10001100 \ | \ 10000000000000000000000_2$。解析各字段可知：符号位 $S=1$ 代表负数；阶码字段 $E = 140$，真实阶码 $e = 140 - 127 = 13$；尾数字段为 $0.1_2$，加上规格化隐藏位 1 后有效值为 $1.1_2 = 1.5$。代入公式得到值为 $-1.5 \times 2^{13}$。</li>
<li><strong>B [错误]</strong>：阶码计算错误，真实阶码应为 13。</li>
<li><strong>C [错误]</strong>：尾数有效值计算错误，未加上规格化隐藏位 1。</li>
<li><strong>D [错误]</strong>：阶码与尾数均计算错误。</li>
</ul>
<h4>( 12 ) float 型能表示的最大正整数是________。</h4>
<p>A. $2^{126}-2^{103}$
B. $2^{127}-2^{104}$
C. $2^{127}-2^{103}$
D. $2^{128}-2^{104}$</p>
<p><strong>答案：D</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：对应阶码较小，非最大有效阶码。</li>
<li><strong>B [错误]</strong>：对应阶码字段未取到最大值。</li>
<li><strong>C [错误]</strong>：尾数部分的最大值及减项计算不正确。</li>
<li><strong>D [正确]</strong>：在单精度浮点数中，最大有效阶码字段值为 254，对应真实阶码 $e = 254 - 127 = 127$。尾数字段最大为全 1，对应最大有效值为 $2 - 2^{-23}$。因此单精度浮点数能表示的最大规格化正数为 $(2 - 2^{-23}) \times 2^{127} = 2^{128} - 2^{104}$。</li>
</ul>
<h4>( 13 ) IEEE 754 单精度浮点格式表示的数中，最小规格化正数是________。</h4>
<p>A. $1.0 \times 2^{-126}$
B. $1.0 \times 2^{-127}$
C. $1.0 \times 2^{-128}$
D. $1.0 \times 2^{-149}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：规格化单精度浮点数的最小阶码字段值为 1，对应的真实指数 $e = 1 - 127 = -126$。尾数字段取其最小值全 0，加上规格化隐藏位 1 后为 1.0。因此最小规格化正数为 $1.0 \times 2^{-126}$。</li>
<li><strong>B [错误]</strong>：阶码字段转换为真实指数的计算不正确，应为 $-126$。</li>
<li><strong>C [错误]</strong>：阶码字段为 0 时表示非规格化数，非规格化数不属于规格化数的范畴。</li>
<li><strong>D [错误]</strong>：这是非规格化数能表示的最小正数，而非规格化正数。</li>
</ul>
<h4>( 14 ) float 型数据通常用 IEEE 754 单精度浮点格式表示。假定两个 float 型变量 $x$ 和 $y$ 分别存放在 32 位寄存器 $\text{f1}$ 和 $\text{f2}$ 中，若 $(\text{f1}) = \text{CC90 0000H}$，$(\text{f2}) = \text{B0C0 0000H}$，则 $x$ 和 $y$ 之间的关系为________。</h4>
<p>A. $x &lt; y$ 且符号相同
B. $x &lt; y$ 且符号不同
C. $x &gt; y$ 且符号相同
D. $x &gt; y$ 且符号不同</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：由 $(\text{f1}) = \text{CC90 0000H}$ 可知其最高位（符号位）为 1，为负数；由 $(\text{f2}) = \text{B0C0 0000H}$ 可知其最高位（符号位）为 1，也是负数。两者符号相同。$x$ 的阶码字段为 $153$，$y$ 的阶码字段为 $97$。因为 $x$ 的阶码大于 $y$，说明 $x$ 的绝对值大于 $y$，而两者都为负数，因此真值关系为 $x &lt; y$。</li>
<li><strong>B [错误]</strong>：两者均为负数，符号相同。</li>
<li><strong>C [错误]</strong>：$x$ 的绝对值大于 $y$，在都为负数时，$x$ 的数值更小。</li>
<li><strong>D [错误]</strong>：两者符号相同且 $x &lt; y$。</li>
</ul>
<h4>( 15 ) 假定变量 $i, f, d$ 的数据类型分别为 int、float、double，已知 $i = 785$，$f = 1.5678\text{e3}$，$d = 1.5\text{e100}$，若在 32 位计算机中执行下列关系表达式，则结果为真的有________。</h4>
<p>Ⅰ. <code>i == (int)(float)i</code>
Ⅱ. <code>f == (float)(int)f</code>
Ⅲ. <code>f == (float)(double)f</code>
Ⅳ. <code>(d+f)-d == f</code>
A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. 仅 Ⅲ、Ⅳ</p>
<p><strong>答案：B</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：包含命题 Ⅱ，但 Ⅱ 为假。</li>
<li><strong>B [正确]</strong>：对各命题分析如下：
<ul>
<li>Ⅰ. $785 &lt; 2^{24}$，单精度浮点数尾数可以精确表示 $2^{24}$ 以内的所有整数，因此 $i$ 转换为 <code>float</code> 精度无损，再转回 <code>int</code> 值不变，命题 Ⅰ 为真。</li>
<li>Ⅱ. $f = 1567.8$，转换为 <code>int</code> 时发生截断丢失小数部分变为 $1567$，再转回 <code>float</code> 为 $1567.0$，不等于原值，命题 Ⅱ 为假。</li>
<li>Ⅲ. <code>float</code> 类型转换为 <code>double</code> 为精度扩充转换，无损，转回 <code>float</code> 仍相等，命题 Ⅲ 为真。</li>
<li>Ⅳ. $d = 1.5 \times 10^{100}$，而 $f = 1567.8$。由于 $d$ 的阶码远大于 $f$，在执行 $d+f$ 对阶时， $f$ 的尾数会向右移出导致其有效位全部丢失，出现大数吃小数现象，使得 $d+f = d$。因此 $(d+f)-d = d-d = 0$，与 $f$ 不相等，命题 Ⅳ 为假。
综上分析，结果为真的有 Ⅰ 和 Ⅲ。</li>
</ul>
</li>
<li><strong>C [错误]</strong>：未包含命题 Ⅰ，且包含假的命题 Ⅱ。</li>
<li><strong>D [错误]</strong>：包含假的命题 Ⅳ。</li>
</ul>
<h4>( 16 ) 已知带符号整数用补码表示，float 型数据用 IEEE 754 标准表示，假定变量 $x$ 的类型只能是 int 或 float，当 $x$ 的机器数为 $\text{C8000000H}$ 时，$x$ 的值可能是________。</h4>
<p>A. $-7 \times 2^{27}$
B. $-2^{16}$
C. $2^{17}$
D. $25 \times 2^{27}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：如果 $x$ 是 <code>int</code> 类型，机器数 <code>C8000000H</code> 对应的 32 位有符号补码其最高位为 1，是负数，真值为 $\text{C8000000H} - 2^{32} = -939524096 = -7 \times 2^{27}$。若 $x$ 是 <code>float</code> 类型，机器数 <code>C8000000H</code> 对应的二进制中，符号位为 1，阶码字段为 $144$，真实指数 $e = 144 - 127 = 17$，尾数全 0，对应的值为 $-1.0 \times 2^{17} = -2^{17}$。选项中没有 $-2^{17}$，因此 $x$ 只能是 <code>int</code> 类型，值为 $-7 \times 2^{27}$。</li>
<li><strong>B [错误]</strong>：浮点数解释下对应的真实指数为 17，计算出的值为 $-2^{17}$。</li>
<li><strong>C [错误]</strong>：浮点数解释下最高符号位为 1，应该为负数。</li>
<li><strong>D [错误]</strong>：整型解释下最高位为 1，真值应该为负数，不能是正数。</li>
</ul>
<h4>( 17 ) $-0.4375$ 的 IEEE 754 单精度浮点数表示为________。</h4>
<p>A. $\text{BEE0 0000H}$
B. $\text{BF60 0000H}$
C. $\text{BF70 0000H}$
D. $\text{C0E0 0000H}$</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：$-0.4375$ 绝对值对应的二进制为 $0.0111_2 = 1.11_2 \times 2^{-2}$。符号位 $S = 1$；阶码字段 $E = -2 + 127 = 125 = 01111101_2$；尾数字段 $M$ 去掉规格化隐藏位 1 并补 0 后为 $11000000000000000000000_2$。拼接得到机器码为 $1 \ | \ 01111101 \ | \ 11000000000000000000000_2 \implies \text{BEE0 0000H}$。</li>
<li><strong>B [错误]</strong>：阶码和尾数值与计算出来的结构不相符。</li>
<li><strong>C [错误]</strong>：阶码计算不正确。</li>
<li><strong>D [错误]</strong>：指数和符号表示有误。</li>
</ul>
<h4>( 18 ) 已知 float 型变量用 IEEE 754 单精度浮点数格式表示。若 float 型变量 $x$ 的机器数为 $8020\ 0000\text{H}$，则 $x$ 的值是________。</h4>
<p>A. $-2^{-128}$
B. $-1.01 \times 2^{-127}$
C. $-1.01 \times 2^{-126}$
D. 非数（NaN）</p>
<p><strong>答案：A</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [正确]</strong>：机器数 <code>8020 0000H</code> 对应的二进制串符号位为 1，阶码字段为全 0，尾数字段为 <code>0x200000</code>（非全 0），因此是非规格化数。非规格化数的真实指数固定为 $-126$，尾数无隐藏位 1，尾数值为 $\frac{\text{0x200000}}{2^{23}} = 2^{-2}$。代入非规格化数公式得到其值为 $-2^{-2} \times 2^{-126} = -2^{-128}$。</li>
<li><strong>B [错误]</strong>：计算中不应包含隐藏位 1，指数值也计算错误。</li>
<li><strong>C [错误]</strong>：尾数和指数计算均不正确。</li>
<li><strong>D [错误]</strong>：NaN 要求阶码字段为全 1，而本题阶码字段为全 0。</li>
</ul>
<h4>( 19 ) 某科学实验需要使用大量整型参数，为了在保证数据精度的基础上提高运算速度，需要选择合理的数据表示方法。若整型参数 $\alpha$、$\beta$ 的取值范围分别为 $-2^{20} \sim 2^{20}$、$-2^{40} \sim 2^{40}$，$\alpha$ 和 $\beta$ 最适宜采用________。</h4>
<p>A. 32 位整数、32 位整数
B. 单精度浮点数、单精度浮点数
C. 32 位整数、双精度浮点数
D. 单精度浮点数、双精度浮点数</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：$\beta$ 的范围超出了 32 位整数所能表示的最大范围。</li>
<li><strong>B [错误]</strong>：单精度浮点数尾数有效精度只有 24 位，表示绝对值较大的整数（如 $2^{40}$）时无法做到完全精确，会发生精度丢失。</li>
<li><strong>C [正确]</strong>：$\alpha$ 的取值范围在 32 位有符号整数的表示范围内，使用 32 位整数表示可以在没有精度损失的同时提供最高效的硬件执行速度。$\beta$ 范围超出 32 位有符号整数上限，但双精度浮点数拥有 53 位尾数有效精度，完全可以无损地精确表示该范围内的所有连续整数。因此，$\alpha$ 用 32 位整数，$\beta$ 用双精度浮点数最为适宜。</li>
<li><strong>D [错误]</strong>：$\alpha$ 在 32 位整数表示范围中，整数运算性能优于浮点数，用单精度浮点数会增加不必要的性能开销。</li>
</ul>
<h4>( 20 ) 用海明码对长度为 8 位的数据进行检错和纠错时，若能纠正一位错，则校验位数至少为________。</h4>
<p>A. 2
B. 3
C. 4
D. 5</p>
<p><strong>答案：C</strong></p>
<p><strong>解析</strong>：</p>
<ul>
<li><strong>A [错误]</strong>：校验位数为 2 时，代入海明不等式不成立。</li>
<li><strong>B [错误]</strong>：校验位数为 3 时，代入海明不等式不成立。</li>
<li><strong>C [正确]</strong>：设数据位数 $m=8$，校验位数为 $r$。根据能纠正一位错的海明不等式 $2^r \ge m + r + 1$，将各选项代入：当 $r=4$ 时，左式为 $2^4 = 16$，右式为 $8 + 4 + 1 = 13$，不等式满足且 $r=4$ 是使不等式成立的最小值。因此校验位数至少为 4。</li>
<li><strong>D [错误]</strong>：虽然 5 也能使不等式成立，但并非题目要求的至少需要的位数。</li>
</ul>
<hr />
<h3>习题2.3 简答题</h3>
<h4>( 1 ) 为什么计算机中采用二进制进行数据表示和运算？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>物理上易于实现</strong>：具有两种稳定物理状态的电子器件极易寻找且抗干扰性能强。</li>
<li><strong>运算规则简单</strong>：二进制的算术加减乘除运算法则极少，硬件结构大为简化。</li>
<li><strong>逻辑运算契合</strong>：二进制的 0 和 1 恰好能与布尔代数中的“假”与“真”无缝契合，便于逻辑门电路的实现。</li>
</ul>
<h4>( 2 ) 相对于奇偶校验，交叉奇偶校验的检错与纠错能力的提高需要付出哪些方面的代价？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>冗余开销增大</strong>：交叉奇偶校验同时增加了水平和垂直校验位，使得非数据位的传输和存储开销显著增加。</li>
<li><strong>硬件与算法变复杂</strong>：编码与译码检测电路在硬件实现上面临更多的逻辑门，延迟变大。</li>
<li><strong>数据组织要求高</strong>：需要对数据进行行、列的矩阵化块状存取，增加了数据调度的开销。</li>
</ul>
<h4>( 3 ) 为什么计算机中采用补码表示带符号的整数？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>统一加减法运算</strong>：通过求补码可以将减法操作巧妙化为加法操作，从而无需设计减法器，简化了运算器设计。</li>
<li><strong>符号位参与计算</strong>：符号位与数值位能够共同参与运算，不需要单独的逻辑判定符号。</li>
<li><strong>0 的表示唯一</strong>：消除了原码和反码中正 0 和负 0 的歧义。</li>
</ul>
<h4>( 4 ) 浮点数的表示范围和精度分别由什么决定？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>表示范围</strong>：主要由浮点数中的<strong>阶码位数</strong>（以及指数所选用的基数和偏置值）来决定。阶码位宽越宽，所能跨越的数轴范围越大。</li>
<li><strong>表示精度</strong>：主要由浮点数中的<strong>尾数位数</strong>（以及是否有隐藏位和所采用的舍入控制模式）决定。尾数位宽越宽，其保留的有效数字位数越多，精度越高。</li>
</ul>
<h4>( 5 ) 汉字输入码、机内码和字形码在汉字处理过程中各有什么作用？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>输入码</strong>：用于从键盘将汉字输入至计算机内部，解决字形到拼音/字形码的转换。</li>
<li><strong>机内码</strong>：用于汉字在计算机系统内部的存取、加工、编辑和网络传输交换。</li>
<li><strong>字形码</strong>：用于将计算机内部处理后的机内码汉字转变为物理输出点阵，以显示或打印汉字。</li>
</ul>
<h4>( 6 ) 在机内码中如何区分 ASCII 字符和汉字字符？</h4>
<p><strong>解答</strong>：
在国标类编码（如 GB2312、GBK）中，通过最高位来做区分：</p>
<ul>
<li><strong>ASCII 字符</strong>：使用单字节表示，其最高有效位固定为 0。</li>
<li><strong>汉字字符</strong>：使用双字节表示，且两个字节的最高位均固定为 1。
计算机读取字节时，若检测到最高位为 1，则与下一个最高位为 1 的字节拼接成一个汉字；若最高位为 0，则单独作为一个 ASCII 码处理。</li>
</ul>
<h4>( 7 ) 为什么现代处理器中又开始支持十进制浮点数运算？</h4>
<p><strong>解答</strong>：
二进制浮点数在表示许多常用的十进制有限小数（如 0.1, 0.01 等）时会转化为二进制无限循环小数，导致计算中累积微小的舍入误差。这在金融、会计、税收等要求数值精确对齐的业务场景中是不可接受的。现代处理器加入十进制浮点数运算，能够从底层消除二-十进制转换带来的舍入损失，确保十进制精度。</p>
<h4>( 8 ) 如何识别浮点数的正负？浮点数能表示的数值范围和数值的精度取决于什么？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>正负识别</strong>：由浮点数机器码的最高位（即符号位 $S$）识别，$S=0$ 代表正数，$S=1$ 代表负数。</li>
<li><strong>数值范围</strong>：取决于阶码的位数（决定最大/最小指数）。</li>
<li><strong>数值精度</strong>：取决于尾数的位数及是否包含隐藏位。</li>
</ul>
<h4>( 9 ) 浮点数有两个 0 会带来什么问题？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>表示不唯一</strong>：$+0$ 和 $-0$ 二进制不同，破坏了表示的唯一性。</li>
<li><strong>极限与倒数歧义</strong>：会导致除 0 运算产生歧义（如 $\frac{1}{+0} = +\infty$ 而 $\frac{1}{-0} = -\infty$）。</li>
<li><strong>比较分支逻辑复杂</strong>：在硬件比较器和数学函数运算中，必须加入额外的判定逻辑对符号进行特殊处理。</li>
</ul>
<h4>( 10 ) 简述 CRC 校验码的检错原理，CRC 能纠错吗？</h4>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>检错原理</strong>：发送端在数据后追加 $r$ 个 0 后与生成多项式对应的无符号二进制数做模 2 除法，所得的余数即为校验码，并替换原追加的 0 发送。接收端用相同的生成多项式对接收到的编码做模 2 除法，若余数为 0 说明传输无误，若余数非 0 说明出错。</li>
<li><strong>纠错能力</strong>：CRC 主要用于高效率的检错。在一般的多位错场景下它无法定位具体错误，不具备纠错能力；但如果在仅发生单比特错误的严格假设下，可以通过建立综合余数表来定位具体的出错位，进而通过取反实现纠错。</li>
</ul>
<hr />
<h3>习题2.4 写出下列各数的原码、反码和补码</h3>
<p><strong>原题</strong>：写出下列各数的原码、反码和补码。
$$0,\ -0,\ 0.10101,\ -0.10101,\ 0.11111,\ -0.11111,\ -0.10000,\ 0.10000$$</p>
<p><strong>解答</strong>：
在定点小数表示法中（含 1 位符号位，5 位数值位），各真值的表示情况如下表所示：</p>
<table>
<thead>
<tr>
<th>真值</th>
<th>原码</th>
<th>反码</th>
<th>补码</th>
</tr>
</thead>
<tbody>
<tr>
<td>$0$</td>
<td>$0.00000$</td>
<td>$0.00000$</td>
<td>$0.00000$</td>
</tr>
<tr>
<td>$-0$</td>
<td>$1.00000$</td>
<td>$1.11111$</td>
<td>$0.00000$</td>
</tr>
<tr>
<td>$0.10101$</td>
<td>$0.10101$</td>
<td>$0.10101$</td>
<td>$0.10101$</td>
</tr>
<tr>
<td>$-0.10101$</td>
<td>$1.10101$</td>
<td>$1.01010$</td>
<td>$1.01011$</td>
</tr>
<tr>
<td>$0.11111$</td>
<td>$0.11111$</td>
<td>$0.11111$</td>
<td>$0.11111$</td>
</tr>
<tr>
<td>$-0.11111$</td>
<td>$1.11111$</td>
<td>$1.00000$</td>
<td>$1.00001$</td>
</tr>
<tr>
<td>$-0.10000$</td>
<td>$1.10000$</td>
<td>$1.01111$</td>
<td>$1.10000$</td>
</tr>
<tr>
<td>$0.10000$</td>
<td>$0.10000$</td>
<td>$0.10000$</td>
<td>$0.10000$</td>
</tr>
</tbody>
</table>
<hr />
<h3>习题2.5 已知数的补码表示形式，求数的真值</h3>
<p><strong>原题</strong>：已知数的补码表示形式，求数的真值。
$$[x]<em>{\text{补}} = 0.10010,\quad [x]</em>{\text{补}} = 1.10010,\quad [x]<em>{\text{补}} = 1.11111$$
$$[x]</em>{\text{补}} = 1.00000,\quad [x]<em>{\text{补}} = 0.10001,\quad [x]</em>{\text{补}} = 1.00001$$</p>
<p><strong>解答</strong>：
补码定点小数（1 位符号位，5 位数值位）求真值的逻辑如下：</p>
<ol>
<li>$[x]_{\text{补}} = 0.10010$：
符号位为 0（正数），原码等同，真值为 $+0.10010_2 = +0.5625$。</li>
<li>$[x]_{\text{补}} = 1.10010$：
符号位为 1（负数）。求原码需除符号位外取反加 1：数值位 $10010$ 取反得 $01101$，加 1 得原码为 $1.01110_2$。真值为 $-0.01110_2 = -0.4375$。</li>
<li>$[x]_{\text{补}} = 1.11111$：
符号位为 1。求原码：数值位全 1 取反为全 0，加 1 得原码 $1.00001_2$。真值为 $-0.00001_2 = -0.03125$。</li>
<li>$[x]_{\text{补}} = 1.00000$：
符号位为 1，数值部分全为 0。这是补码负数表示的极限值，真值为 $-1$。</li>
<li>$[x]_{\text{补}} = 0.10001$：
符号位为 0（正数），真值为 $+0.10001_2 = +0.53125$。</li>
<li>$[x]_{\text{补}} = 1.00001$：
符号位为 1。求原码：数值位 $00001$ 取反得 $11110$，加 1 得原码 $1.11111_2$。真值为 $-0.11111_2 = -0.96875$。</li>
</ol>
<hr />
<h3>习题2.6 无符号数与有符号整数的 C 语言转换分析</h3>
<p><strong>原题</strong>：C 语言中允许无符号数和有符号整数之间的转换，下面是一段 C 语言代码。</p>
<pre><code>int x = -1;
unsigned u = 2147483648;
printf("x=%u=%d\n", x, x);
printf("u=%u=%d\n", u, u);
</code></pre>
<p>给出在 32 位计算机上上述程序段的输出结果并分析原因。</p>
<p><strong>解答</strong>：
在 32 位计算机上，上述程序段的运行输出结果为：</p>
<pre><code>x=4294967295=-1
u=2147483648=-2147483648
</code></pre>
<p><strong>原因分析</strong>：</p>
<ol>
<li><strong>对于变量 $x$</strong>：
<ul>
<li>$x = -1$ 是 32 位带符号整型，其内存底层的二进制补码存储为全 1，即 <code>FFFF FFFFH</code>。</li>
<li>当以 <code>%u</code>（无符号十进制）格式打印时，将该 <code>FFFF FFFFH</code> 解析为无符号数，值即为 $2^{32} - 1 = 4294967295$。</li>
<li>当以 <code>%d</code>（有符号十进制）格式打印时，将其正常解析为补码负数，值即为 $-1$。</li>
</ul>
</li>
<li><strong>对于变量 $u$</strong>：
<ul>
<li>$u = 2147483648 = 2^{31}$，是 32 位无符号整型，其内存存储为 <code>8000 0000H</code>。</li>
<li>当以 <code>%u</code>（无符号十进制）格式打印时，正常解析无符号，值为 $2147483648$。</li>
<li>当以 <code>%d</code>（有符号十进制）格式打印时，由于最高位为 1，被解析为有符号补码负数，其为 32 位有符号补码能表示的极限负数，真值即为 $-2^{31} = -2147483648$。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题2.7 分析几种情况下所能表示的数据范围</h3>
<p><strong>原题</strong>：分析下列几种情况下所能表示的数据范围分别是多少。
( 1 ) 16 位无符号数；
( 2 ) 16 位原码定点小数；
( 3 ) 16 位补码定点小数；
( 4 ) 16 位补码定点整数。</p>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>( 1 ) 16 位无符号数</strong>：
全无符号，表示范围为：$0 \sim 2^{16} - 1$，即 $0 \sim 65535$。</li>
<li><strong>( 2 ) 16 位原码定点小数</strong>：
1 位符号位，15 位数值位。最大正数为 $0.111\dots1_2 = 1 - 2^{-15}$；最小负数为 $1.111\dots1_2 = -(1 - 2^{-15})$。
范围为：$-(1 - 2^{-15}) \sim +(1 - 2^{-15})$。</li>
<li><strong>( 3 ) 16 位补码定点小数</strong>：
1 位符号位，15 位数值位。由于补码 0 的表示唯一，负数比正数多表示一个边界负值 $-1$。
范围为：$-1 \sim 1 - 2^{-15}$。</li>
<li><strong>( 4 ) 16 位补码定点整数</strong>：
1 位符号位，15 位数值位。
范围为：$-2^{15} \sim 2^{15} - 1$，即 $-32768 \sim +32767$。</li>
</ul>
<hr />
<h3>习题2.8 补码表示整数的模</h3>
<p><strong>原题</strong>：用补码表示二进制整数，机器码为 $x_0x_1x_2x_3x_4x_5x_6x_7$，$x_0$ 为符号位，补码的模为多少？</p>
<p><strong>解答</strong>：
补码的“模”代表了该计数系统所能表征的最大数值环边界。该机器码共有 8 位二进制（含 1 位符号位），其所能代表的编码状态共有 $2^8 = 256$ 个。
因此，该 8 位补码系统的模为 <strong>256</strong>（或记为 $2^8$）。</p>
<hr />
<h3>习题2.9 采用 IEEE 754 32 位单精度标准表示十进制数</h3>
<p><strong>原题</strong>：用 IEEE 754 32 位单精度浮点数标准表示下列十进制数。
( 1 ) $-6\frac{5}{8}$； ( 2 ) $3.1415927$； ( 3 ) $64000$。</p>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>( 1 ) 十进制数 $-6\frac{5}{8}$</strong>：
<ul>
<li>绝对值二进制：$6.625 = 110.101_2$。</li>
<li>规格化：$1.10101_2 \times 2^2$，指数 $e = 2$。</li>
<li>各字段编码：负数符号位 $S = 1$；阶码字段 $E = 2 + 127 = 129 = 10000001_2$；尾数字段 $M = 1010100\dots_2$。</li>
<li>拼接为：$1 \ | \ 10000001 \ | \ 10101000000000000000000_2 \implies \text{C0D4 0000H}$。</li>
</ul>
</li>
<li><strong>( 2 ) 十进制数 $3.1415927$</strong>：
<ul>
<li>绝对值二进制：经过精确的基二转换并规格化，得规格化前为 $11.0010010000111111011011\dots_2$。</li>
<li>规格化形式为 $1.10010010000111111011011_2 \times 2^1$。</li>
<li>各字段编码：正数 $S = 0$；阶码 $E = 1 + 127 = 128 = 10000000_2$；尾数舍入后为 $M = 10010010000111111011011_2$。</li>
<li>拼接为：$0 \ | \ 10000000 \ | \ 10010010000111111011011_2 \implies \text{4049 0FDBH}$。</li>
</ul>
</li>
<li><strong>( 3 ) 十进制数 $64000$</strong>：
<ul>
<li>绝对值二进制：$64000 = 1111 \ 1010 \ 0000 \ 0000_2$。</li>
<li>规格化：$1.111101_2 \times 2^{15}$。</li>
<li>各字段编码：正数 $S = 0$；阶码 $E = 15 + 127 = 142 = 10001110_2$；尾数 $M = 11110100000000000000000_2$。</li>
<li>拼接为：$0 \ | \ 10001110 \ | \ 11110100000000000000000_2 \implies \text{477A 0000H}$。</li>
</ul>
</li>
</ul>
<hr />
<h3>习题2.10 单精度浮点数还原真值</h3>
<p><strong>原题</strong>：求与单精度浮点数 $\text{43940000H}$ 对应的十进制数。</p>
<p><strong>解答</strong>：</p>
<ol>
<li>将 <code>43940000H</code> 转换为 32 位二进制串：
$
0 \ | \ 10000111 \ | \ 00101000000000000000000_2
$</li>
<li>分解并解析各字段：
<ul>
<li>符号位 $S = 0$，表示正数。</li>
<li>阶码字段 $E = 10000111_2 = 135$。真实指数为 $e = 135 - 127 = 8$。</li>
<li>尾数字段 $M = .00101_2$。加上隐含位 1，规格化尾数为 $1.00101_2$。</li>
</ul>
</li>
<li>还原十进制数：
$
V = +1.00101_2 \times 2^8 = 100101000_2 = 256 + 32 + 8 = 296
$
对应的十进制数是 <strong>296</strong>。</li>
</ol>
<hr />
<h3>习题2.11 单精度浮点数能表示的最大数和最小数</h3>
<p><strong>原题</strong>：求单精度浮点数能表示的最大数和最小数。</p>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>最大正数（最大规格化数）</strong>：
符号位 $S = 0$；阶码字段取最大有效值 254，对应真实阶码 $e = 127$；尾数取最大值全 1。
计算值为：$(2 - 2^{-23}) \times 2^{127} = 2^{128} - 2^{104}$。</li>
<li><strong>最小负数</strong>：
符号位 $S = 1$（代表负数，数轴上最左端）；阶码和尾数均取最大值。
计算值为：$-(2 - 2^{-23}) \times 2^{127} = -(2^{128} - 2^{104})$。</li>
<li><strong>最小规格化正数</strong>：
符号位 $S = 0$；阶码字段取规格化最小值 1，指数 $e = -126$；尾数字段为全 0。
计算值为：$1.0 \times 2^{-126}$。</li>
<li><strong>最小非规格化正数</strong>：
符号位 $S = 0$；阶码字段为全 0，尾数字段的最低位为 1。
计算值为：$2^{-23} \times 2^{-126} = 2^{-149}$。</li>
</ul>
<hr />
<h3>习题2.12 正浮点数的比对分析</h3>
<p><strong>原题</strong>：设有两个正浮点数：$N_1 = 2^m \times M_1$，$N_2 = 2^n \times M_2$。
( 1 ) 若 $m &lt; n$，是否有 $N_1 &gt; N_2$？
( 2 ) 若 $M_1$ 和 $M_2$ 是规格化的数，上述结论是否正确？</p>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>( 1 ) 若 $m &lt; n$，不一定有 $N_1 &gt; N_2$</strong>。
<strong>反例证明</strong>：设 $m = 0, n = 1$。如果尾数 $M_1 = 8$（未规格化）， $M_2 = 1$，则 $N_1 = 2^0 \times 8 = 8$，$N_2 = 2^1 \times 1 = 2$，此时 $N_1 &gt; N_2$ 成立。
反之，若 $M_1 = 1$，$M_2 = 8$，则 $N_1 = 2^0 \times 1 = 1$，$N_2 = 2^1 \times 8 = 16$，此时 $N_1 &lt; N_2$。因此无法直接判断。</li>
<li><strong>( 2 ) 若 $M_1$ 和 $M_2$ 是规格化的二进制小数，则结论依然不正确。且实际大小关系相反，一定有 $N_1 &lt; N_2$</strong>。
<strong>原理分析</strong>：对于二进制规格化小数尾数，其大小范围被严格限制在 $\frac{1}{2} \leq M &lt; 1$ 之间。
若 $m &lt; n$，因指数为整数，至少有 $n \geq m + 1$。
<ul>
<li>对于 $N_1$，其最大上限满足：$N_1 = 2^m \times M_1 &lt; 2^m \times 1 = 2^m$。</li>
<li>对于 $N_2$，其最小下限满足：$N_2 = 2^n \times M_2 \geq 2^{m+1} \times \frac{1}{2} = 2^m$。
由此可得：$N_1 &lt; 2^m \leq N_2$。即规格化下，必然有 $N_1 &lt; N_2$。</li>
</ul>
</li>
</ul>
<hr />
<h3>习题2.13 阶码 3 位、尾数 7 位的浮点数极限表示</h3>
<p><strong>原题</strong>：设二进制浮点数的阶码为 3 位，尾数为 7 位。用模 2 补码写出它们所能表示的最大正数、最小正数、最大负数和最小负数，并将它们转换成十进制数。</p>
<p><strong>解答</strong>：
假定阶码和尾数均采用补码表示，且尾数为规格化定点小数（符号位 1 位，数值位 6 位）。
阶码 3 位补码表示的范围为 $-4 \sim 3$。正数规格化尾数范围为 $0.100000_2 \sim 0.111111_2$；负数规格化尾数范围为 $1.000000_2 \sim 1.011111_2$。</p>
<ul>
<li><strong>最大正数</strong>：
阶码取最大值 $3$（补码 <code>011</code>）；尾数取最大正规格化尾数 $\frac{63}{64}$（补码 <code>0.111111</code>）。
十进制值为：$\frac{63}{64} \times 2^3 = \frac{63}{8} = 7.875$。</li>
<li><strong>最小正数</strong>：
阶码取最小值 $-4$（补码 <code>100</code>）；尾数取最小正规格化尾数 $\frac{1}{2}$（补码 <code>0.100000</code>）。
十进制值为：$\frac{1}{2} \times 2^{-4} = 2^{-5} = 0.03125$。</li>
<li><strong>最大负数（最接近 0 的负数）</strong>：
阶码取最小值 $-4$（补码 <code>100</code>）；尾数取绝对值最小的负规格化尾数（补码 <code>1.011111</code>，真值为 $-1 + \frac{31}{64} = -\frac{33}{64}$，即 $-0.100001_2$）。
十进制值为：$-\frac{33}{64} \times 2^{-4} = -\frac{33}{1024} \approx -0.032227$。</li>
<li><strong>最小负数（绝对值最大的负数）</strong>：
阶码取最大值 $3$（补码 <code>011</code>）；尾数取最小规格化负尾数 $-1$（补码 <code>1.000000</code>）。
十进制值为：$-1 \times 2^3 = -8$。</li>
</ul>
<hr />
<h3>习题2.14 十进制数转定长浮点规格化表示</h3>
<p><strong>原题</strong>：将下列十进制数表示成浮点规格化数，阶码为 4 位，尾数为 10 位，各含 1 位符号，阶码和尾数均用补码表示。
( 1 ) $57/128$； ( 2 ) $-69/128$。</p>
<p><strong>解答</strong>：</p>
<ul>
<li><strong>( 1 ) 表示 $57/128$</strong>：
<ul>
<li>数值为正，写成二进制为 $\frac{57}{128} = 0.0111001_2$。</li>
<li>正数规格化（尾数绝对值应在 $0.5 \sim 1$ 之间）：
$
0.0111001_2 = 0.111001_2 \times 2^{-1}
$</li>
<li>阶码为 $-1$，补码表示为 <code>1111</code>。</li>
<li>尾数为 $+0.111001_2$，补齐 10 位有符号小数补码为 <code>0.111001000</code>。</li>
<li>最终编码为：<code>1111, 0.111001000</code>。</li>
</ul>
</li>
<li><strong>( 2 ) 表示 $-69/128$</strong>：
<ul>
<li>数值为负，写成二进制为 $-\frac{69}{128} = -0.1000101_2$。</li>
<li>负数规格化（绝对值在 $0.5 \sim 1$ 之间，补码尾数满足 $1.0\dots$ 形式）：
由于 $-0.1000101_2$ 本身已经在范围之内，所以阶码为 $0$，补码为 <code>0000</code>。</li>
<li>求尾数 $-0.1000101_2$ 的 10 位补码：
<ul>
<li>原码为 <code>1.100010100</code>。</li>
<li>取反加 1 得补码为 <code>1.011101100</code>。</li>
</ul>
</li>
<li>最终编码为：<code>0000, 1.011101100</code>。</li>
</ul>
</li>
</ul>
<hr />
<h3>习题2.15 校验码设计与检错分析</h3>
<p><strong>原题</strong>：设有效信息为 01011011，分别写出其奇校验码和偶校验码。如果接收方收到的校验码为 010110100，说明如何发现错误。</p>
<p><strong>解答</strong>：</p>
<ol>
<li><strong>统计有效数据</strong>：有效信息为 <code>01011011</code>，其中二进制“1”的个数为 5。</li>
<li><strong>计算奇校验码</strong>：要求 1 的总个数为奇数。数据中已有 5 个 1（奇数），因此校验位填 0，奇校验码为 <code>010110110</code>。</li>
<li><strong>计算偶校验码</strong>：要求 1 的总个数为偶数。数据中已有 5 个 1（奇数），因此校验位填 1，偶校验码为 <code>010110111</code>。</li>
<li><strong>接收端检错分析</strong>：
<ul>
<li>接收方收到的数据为 <code>010110100</code>，统计其包含“1”的个数为 4。</li>
<li>若系统采用的是<strong>奇校验</strong>，而 4 是偶数，不满足奇数个 1 的校验规则，因此检错电路输出异常，成功发现错误。</li>
<li>若系统采用的是<strong>偶校验</strong>，而 4 是偶数，满足偶校验规则，此时校验电路判定无错，从而发生了漏检。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题2.16 ASCII 交叉校验填空推理</h3>
<p><strong>原题</strong>：由 6 个字符的 7 位 ASCII 字符排列，再加上水平和垂直偶校验位构成表 2.27 所示的行列结构...</p>
<p><strong>解答</strong>：
偶校验原则：每行、每列（含校验位本身）中“1”的总个数必须为偶数。</p>
<ol>
<li><strong>第一行（字符 3）</strong>：
<ul>
<li>ASCII 码 $3 = \text{33H} = 0110011_2$。</li>
<li>对应数据行：$0 \quad X_1 \quad X_2 \quad 0 \quad 0 \quad 1 \quad 1$，对照可得：$X_1 = 1, X_2 = 1$。</li>
</ul>
</li>
<li><strong>第二行（字符 $Y_1$）</strong>：
<ul>
<li>数据行：$1 \quad 0 \quad 0 \quad 1 \quad 0 \quad 0 \quad X_3$，水平校验位 $HP = 1$。</li>
<li>前面有 2 个 1，加上 $HP$ 一共 3 个 1。要保持整行有偶数个 1，必须使 $X_3 = 1$。</li>
<li>该行的 7 位二进制码为 $1001001_2 = \text{49H}$，查表可知代表字符 <strong>I</strong>。即 $Y_1 = I$。</li>
</ul>
</li>
<li><strong>第三行（字符 +）</strong>：
<ul>
<li>ASCII 码 $+ = \text{2BH} = 0101011_2$。</li>
<li>对应数据行：$X_4 \quad 1 \quad 0 \quad 1 \quad 0 \quad 1 \quad 1$，对照可得：$X_4 = 0$。</li>
</ul>
</li>
<li><strong>第五行（字符 D）</strong>：
<ul>
<li>ASCII 码 $D = \text{44H} = 1000100_2$。</li>
<li>对应数据行：$1 \quad 0 \quad 0 \quad X_7 \quad 1 \quad 0 \quad X_8$，对照可得：$X_7 = 0, X_8 = 0$。</li>
</ul>
</li>
<li><strong>第四行（字符 $Y_2$）与第 3 列、第 4 列</strong>：
<ul>
<li>第 3 列数据：$X_2, 0, 0, X_5, 0, 1, VP(1)$。已知 $X_2 = 1$，此列中 1 的个数为 $1 + X_5 + 1 + 1 = 3 + X_5$。要使列满足偶校验， $X_5 = 1$。</li>
<li>第 4 列数据：$0, 1, 1, X_6, X_7, 1, VP(1)$。已知 $X_7 = 0$，此列中 1 的个数为 $1 + 1 + X_6 + 1 + 1 = 4 + X_6$。要使列满足偶校验， $X_6 = 0$。</li>
<li>得到第四行数据为 $0110111_2 = \text{37H}$，查表代表字符 <strong>7</strong>。即 $Y_2 = 7$。</li>
</ul>
</li>
<li><strong>第六行（字符 =）与第 1 列、第 6 列</strong>：
<ul>
<li>ASCII 码 $= = \text{3DH} = 0111101_2$。</li>
<li>对应数据行：$0 \quad X_9 \quad 1 \quad 1 \quad 1 \quad X_{10} \quad 1$，对照可得：$X_9 = 1, X_{10} = 0$。</li>
<li>第 6 列数据：$1, 0, 1, 1, 0, X_{10}, X_{11}$（即 $1, 0, 1, 1, 0, 0, X_{11}$）。列中 1 的个数为 $3 + X_{11}$，为偶数则 $X_{11} = 1$。</li>
</ul>
</li>
<li><strong>最后一行（VP）与水平偶校验列（HP）</strong>：
<ul>
<li>$HP$ 列数据：$0, 1, 0, 1, 0, 1, X_{12}$。前 6 位包含 3 个 1，为满足偶校验， $X_{12} = 1$。</li>
</ul>
</li>
</ol>
<p><strong>填空最终结果</strong>：</p>
<ul>
<li>$X_1, X_2, X_3, X_4$ 处的比特分别为 <strong>1, 1, 1, 0</strong>。</li>
<li>$X_5, X_6, X_7, X_8$ 处的比特分别为 <strong>1, 0, 0, 0</strong>。</li>
<li>$X_9, X_{10}, X_{11}, X_{12}$ 处的比特分别为 <strong>1, 0, 1, 1</strong>。</li>
<li>$Y_1$ 和 $Y_2$ 处的字符分别为 <strong>I</strong> 和 <strong>7</strong>。</li>
</ul>
<hr />
<h3>习题2.17 海明校验码完整计算与纠错</h3>
<p><strong>原题</strong>：设 8 位有效信息为 $01101110$，试写出它的海明校验码。给出过程，说明分组检测方式，并给出指错字及其逻辑表达式。如果接收方收到的 8 位有效信息变成 $01101111$，试说明如何定位错误并纠正错误。</p>
<p><strong>解答</strong>：</p>
<ol>
<li><strong>确定校验位个数</strong>：
有效信息长度 $m = 8$。由 $2^r \geq 8 + r + 1$ 可知最小校验位个数 $r = 4$。总码长为 12 位。</li>
<li><strong>位置分配布局</strong>：
校验位 $P_1, P_2, P_4, P_8$ 依次放置在位号 1, 2, 4, 8 处，数据位 $D_1 \sim D_8$ 填充在其余位置：
<ul>
<li>$H_1 = P_1, \ H_2 = P_2, \ H_3 = D_1(0), \ H_4 = P_4, \ H_5 = D_2(1), \ H_6 = D_3(1), \ H_7 = D_4(0)$</li>
<li>$H_8 = P_8, \ H_9 = D_5(1), \ H_{10} = D_6(1), \ H_{11} = D_7(1), \ H_{12} = D_8(0)$</li>
</ul>
</li>
<li><strong>计算校验位的值（偶校验）</strong>：
每个校验位负责检验其二进制展开中包含自身位号的位。</li>
</ol>
<ul>
<li>$P_1$ 负责：$H_1, H_3, H_5, H_7, H_9, H_{11} \implies P_1 \oplus 0 \oplus 1 \oplus 0 \oplus 1 \oplus 1 = 0 \implies P_1 \oplus 1 = 0 \implies P_1 = 1$。</li>
<li>$P_2$ 负责：$H_2, H_3, H_6, H_7, H_{10}, H_{11} \implies P_2 \oplus 0 \oplus 1 \oplus 0 \oplus 1 \oplus 1 = 0 \implies P_2 \oplus 1 = 0 \implies P_2 = 1$。</li>
<li>$P_4$ 负责：$H_4, H_5, H_6, H_7, H_{12} \implies P_4 \oplus 1 \oplus 1 \oplus 0 \oplus 0 = 0 \implies P_4 \oplus 0 = 0 \implies P_4 = 0$。</li>
<li>$P_8$ 负责：$H_8, H_9, H_{10}, H_{11}, H_{12} \implies P_8 \oplus 1 \oplus 1 \oplus 1 \oplus 0 = 0 \implies P_8 \oplus 1 = 0 \implies P_8 = 1$。
<ul>
<li><strong>海明校验码最终为</strong>：<code>110011011110</code>。</li>
</ul>
</li>
</ul>
<ol>
<li><strong>指错字逻辑表达式</strong>：
接收端定义 4 位指错字 $S_8S_4S_2S_1$：
<ul>
<li>$S_1 = H_1 \oplus H_3 \oplus H_5 \oplus H_7 \oplus H_9 \oplus H_{11}$</li>
<li>$S_2 = H_2 \oplus H_3 \oplus H_6 \oplus H_7 \oplus H_{10} \oplus H_{11}$</li>
<li>$S_4 = H_4 \oplus H_5 \oplus H_6 \oplus H_7 \oplus H_{12}$</li>
<li>$S_8 = H_8 \oplus H_9 \oplus H_{10} \oplus H_{11} \oplus H_{12}$</li>
</ul>
</li>
<li><strong>纠错定位分析</strong>：
若有效数据变成 <code>01101111</code>，说明第 8 个数据位 $D_8$（对应海明码第 12 位）由 0 变为了 1。
此时接收到的海明码为 <code>110011011111</code>。带入计算指错字：
<ul>
<li>$S_1 = 1 \oplus 0 \oplus 1 \oplus 0 \oplus 1 \oplus 1 = 0$</li>
<li>$S_2 = 1 \oplus 0 \oplus 1 \oplus 0 \oplus 1 \oplus 1 = 0$</li>
<li>$S_4 = 0 \oplus 1 \oplus 1 \oplus 0 \oplus 1 = 1$</li>
<li>$S_8 = 1 \oplus 1 \oplus 1 \oplus 1 \oplus 1 = 1$</li>
<li>得到指错字为 $S_8S_4S_2S_1 = 1100_2 = 12$。</li>
<li>指错字的值为 12，表明第 12 位发生错误。将第 12 位取反（由 1 变回 0），即可完成自动纠错，恢复出正确的有效信息 <code>01101110</code>。</li>
</ul>
</li>
</ol>
<hr />
<h3>习题2.18 循环冗余校验码计算与检错纠错</h3>
<p><strong>原题</strong>：设要采用 CRC 码传送数据信息 $x=1001$，当生成多项式为 $G(x)=1101$ 时，请写出它的循环冗余校验码。若接收方收到的数据信息为 $x'=1101$，说明如何定位错误并纠正错误。</p>
<p><strong>解答</strong>：</p>
<ol>
<li><strong>生成 CRC 校验码</strong>：
<ul>
<li>生成多项式 $G(x) = 1101$（阶数 $r = 3$）。</li>
<li>在数据 $1001$ 后追加 3 个 0，得 $1001000$。</li>
<li>将 $1001000$ 与 $1101$ 做模 2 除法（即异或运算代替减法）：
$
1001000 \div 1101 \implies 余数为 \ 011
$</li>
<li>故 CRC 校验位为 <code>011</code>。</li>
<li>最终循环冗余校验码为 <strong>1001011</strong>。</li>
</ul>
</li>
<li><strong>接收端出错定位与纠正</strong>：
<ul>
<li>若接收到的数据信息为 $x' = 1101$，拼上原校验位 <code>011</code>，得到接收码字为 <code>1101011</code>。</li>
<li>用生成多项式 $1101$ 对 <code>1101011</code> 做模 2 除法：
$
1101011 \div 1101 \implies 余数为 \ 011
$</li>
<li>因为余数为 <code>011</code> 不为 0，说明传输中发生了错误。</li>
<li>假设仅发生单比特错误，建立该多项式下的综合余数表：
<ul>
<li>若第 1 位错，余数为 <code>110</code></li>
<li>若第 2 位错，余数为 <code>011</code></li>
<li>若第 3 位错，余数为 <code>111</code></li>
<li>若第 4 位错，余数为 <code>101</code></li>
<li>若第 5 位错，余数为 <code>100</code></li>
<li>若第 6 位错，余数为 <code>010</code></li>
<li>若第 7 位错，余数为 <code>001</code></li>
</ul>
</li>
<li>对照所得的余数 <code>011</code>，在表中对应第 2 位出错。</li>
<li>纠错：将接收码字 <code>1101011</code> 的第 2 位取反，得到正确码字 <code>1001011</code>，恢复出正确的数据信息为 <strong>1001</strong>。</li>
</ul>
</li>
</ol>
<hr />
<h2>2.8 本章总结</h2>
<p>本章核心公式及知识体系整理如下：</p>
<h3>核心计算公式一览</h3>
<table>
<thead>
<tr>
<th>性能评价指标</th>
<th>公式 / 关系式</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>无符号数范围</td>
<td>$0 \sim 2^n - 1$</td>
<td>纯数值计算</td>
</tr>
<tr>
<td>定点整数补码范围</td>
<td>$-2^{n-1} \sim 2^{n-1} - 1$</td>
<td>有符号整数存储</td>
</tr>
<tr>
<td>定点小数补码范围</td>
<td>$-1 \sim 1 - 2^{-(n-1)}$</td>
<td>纯小数数值表示</td>
</tr>
<tr>
<td>单精度浮点数公式</td>
<td>$V = (-1)^S \times 1.M \times 2^{E - 127}$</td>
<td>32 位浮点数大小确定</td>
</tr>
<tr>
<td>海明码检验不等式</td>
<td>$2^r \ge m + r + 1$</td>
<td>确定海明码校验位个数</td>
</tr>
<tr>
<td>最小纠检错码距</td>
<td>$d \ge e + t + 1$</td>
<td>设计系统容错机制</td>
</tr>
</tbody>
</table>
<p>数据表示在底层的处理机制直接决定了计算机的运算方式。在学习时，应当注重补码与规格化浮点数的细节特征，并通过实际计算熟练掌握海明校验的计算过程，为进一步学习运算器硬件做好铺垫。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>计算机系统概述</title>
    <link href="https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E6%A6%82%E8%BF%B0/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E8%AE%A1%E7%BB%84/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E6%A6%82%E8%BF%B0/</id>
    <published>2026-06-27T12:25:00.000Z</published>
    <updated>2026-06-27T12:25:00.000Z</updated>
    <summary>整理计算机系统组成、层次结构与性能评价指标，重点解析CPI、MIPS及CPU执行时间等核心计算。</summary>
    <content type="html"><![CDATA[<p>计算机组成原理的入门重点在于理清<strong>系统的组成与层次结构</strong>，并熟练掌握<strong>性能评价指标的计算</strong>。</p>
<hr />
<h1>第1章：计算机系统概述</h1>
<hr />
<h2>1.1 计算机发展历程</h2>
<p>计算机的发展主要按核心器件划分为四代：</p>
<table>
<thead>
<tr>
<th>阶段</th>
<th>核心器件</th>
<th>软件与存储特征</th>
</tr>
</thead>
<tbody>
<tr>
<td>第一代 (1946—1958)</td>
<td>电子管</td>
<td>使用机器语言，主要用于科学计算</td>
</tr>
<tr>
<td>第二代 (1958—1964)</td>
<td>晶体管</td>
<td>出现辅助存储器(如磁盘)，开始使用汇编语言与高级语言</td>
</tr>
<tr>
<td>第三代 (1964—1971)</td>
<td>中小规模集成电路</td>
<td>操作系统出现，计算机类型呈现多样化和系列化</td>
</tr>
<tr>
<td>第四代 (1971 至今)</td>
<td>超大规模集成电路</td>
<td>微处理器问世，运算速度与存储容量大幅提升</td>
</tr>
</tbody>
</table>
<p>微电子技术的发展基本遵循<strong>摩尔定律</strong>：</p>
<blockquote>
<p>集成电路芯片上所集成的晶体管数目，大约每 18 至 24 个月就会翻一番。</p>
</blockquote>
<hr />
<h2>1.2 计算机系统的组成</h2>
<p>计算机系统是由硬件和软件协同工作的复杂系统。</p>
<p>$$
\text{计算机系统} = \text{硬件系统} + \text{软件系统}
$$</p>
<h3>冯·诺依曼结构核心思想</h3>
<p>现代计算机大多基于冯·诺依曼结构，其核心设计思想可以概括为以下三点：</p>
<ol>
<li><strong>二进制表示</strong>：数据和指令均采用二进制代码表示。</li>
<li><strong>存储程序</strong>：程序和数据提前存入主存储器中。</li>
<li><strong>程序控制</strong>：CPU 根据存储的程序自动、顺序地执行指令。</li>
</ol>
<h3>硬件五大部件</h3>
<p>硬件系统是计算机的物理基础，通常由五大部件构成。</p>
<ul>
<li><strong>存储器</strong>：存放程序和数据。</li>
<li><strong>运算器</strong>：负责对数据进行算术运算和逻辑运算。</li>
<li><strong>控制器</strong>：负责翻译指令并控制各部件协调工作。</li>
<li><strong>输入设备</strong>：将外部信息转换为计算机可识别的电信号。</li>
<li><strong>输出设备</strong>：将计算机的计算结果转换为用户可读的信息。</li>
</ul>
<p>在结构关系上，各部件的组合形成了计算机的核心：</p>
<ul>
<li><strong>CPU</strong>：运算器和控制器共同组成 CPU。</li>
<li><strong>主机</strong>：CPU 和主存储器共同合称为主机。</li>
<li><strong>总线</strong>：计算机各部件通过总线互连，总线包括数据线、地址线和控制线。</li>
</ul>
<blockquote>
<p>重点：CPU 和主存储器合称为主机；辅存（如硬盘）属于外设，不属于主机的范畴。</p>
</blockquote>
<h3>软件的分类</h3>
<p>软件系统用于扩展硬件功能并解决具体问题，主要分为系统软件与应用软件。</p>
<ul>
<li><strong>系统软件</strong>：用于管理、控制和维护计算机系统资源。例如：操作系统、编译器、汇编器、数据库管理系统。</li>
<li><strong>应用软件</strong>：用于解决特定领域的具体应用问题。例如：浏览器、办公软件、图像处理软件。</li>
</ul>
<blockquote>
<p>重点：操作系统是管理资源的系统软件，切勿将其误归为应用软件。</p>
</blockquote>
<h3>软硬件的等价性</h3>
<p>在计算机系统中，同一个功能既可以通过硬件电路实现，也可以通过软件程序实现。</p>
<ul>
<li><strong>硬件实现</strong>：运行速度快，但开发成本高，缺乏灵活性。</li>
<li><strong>软件实现</strong>：灵活性高，开发成本低，但运行速度慢。</li>
</ul>
<hr />
<h2>1.3 计算机系统的层次结构</h2>
<p>计算机系统通过多级抽象层次实现从高级语言到底层硬件的转换。从底向上通常划分为以下 6 个层次：</p>
<pre><code>flowchart BT
    L1[第1层: 逻辑门层] --&gt; L2[第2层: 微代码层]
    L2 --&gt; L3[第3层: 指令集架构层]
    L3 --&gt; L4[第4层: 操作系统层]
    L4 --&gt; L5[第5层: 汇编语言层]
    L5 --&gt; L6[第6层: 高级语言层]
</code></pre>
<p>在各层次的转换与协作中，关键要素如下：</p>
<ul>
<li><strong>逻辑门与微代码</strong>：第 1 层与第 2 层是底层硬件逻辑系统，由硬件直接执行微指令。</li>
<li><strong>指令集架构层</strong>：该层是计算机软、硬件系统之间的界面和纽带。所有软件程序必须最终转换为该层的机器语言指令，才能被底层硬件执行。</li>
<li><strong>操作系统与翻译</strong>：操作系统管理软硬件资源；汇编器和编译器则将高级程序翻译为低层语言。</li>
</ul>
<p>在各层关系中，<strong>低层是高层的基础，高层是低层功能的扩展</strong>。</p>
<hr />
<h2>1.4 计算机性能指标与评价</h2>
<p>计算机的性能需要通过多个维度的指标来衡量，不能单一以主频论高低。</p>
<h3>1.4.1 基本配置指标</h3>
<ul>
<li><strong>字长</strong>：CPU 一次能够处理的二进制数据位数。字长越长，计算精度越高，数据表示范围越大。其单位是位，缩写为 b。
<blockquote>
<p><strong>单位换算与区分</strong>：</p>
<ul>
<li>$1\text{B} = 8\text{bit}$（大写 B 表示字节，小写 b 表示位）。</li>
<li>比如 32 位字长即为 $32\text{bit} = 4\text{B}$；64 位字长即为 $64\text{bit} = 8\text{B}$。</li>
</ul>
</blockquote>
</li>
<li><strong>主存容量</strong>：主存储器能容纳的信息总量。容量增加有利于减少访问慢速外存的次数。主存容量通常使用 2 的幂作为换算单位：
$$
1\text{KB} = 2^{10}\text{B} = 1024\text{B}
$$
$$
1\text{MB} = 2^{20}\text{B}
$$
$$
1\text{GB} = 2^{30}\text{B}
$$</li>
</ul>
<p>:::note[字长与存储容量的区别]</p>
<ul>
<li><strong>字长</strong>描述的是 CPU 一次处理数据的宽度，单位为位。</li>
<li><strong>主存容量</strong>表示存储器能存入的二进制代码总量，单位为字节，且其容量通常以 2 的幂来换算，而主频等速度指标则通常以 10 的幂换算。这两者的换算进制极易混淆。
:::</li>
</ul>
<h3>1.4.2 时间与速度指标</h3>
<ul>
<li><strong>时钟周期</strong>：一个时钟脉冲的持续时间，是计算机中最基本、最小的时间单位。主频与时钟周期互为倒数。</li>
<li><strong>响应时间</strong>：从用户提交请求到系统完成响应所经历的时间，反映单次任务的快慢。</li>
<li><strong>吞吐率</strong>：系统在单位时间内完成的任务总量，反映整体处理能力。</li>
<li><strong>MIPS</strong>：每秒执行的百万条指令数。</li>
<li><strong>MFLOPS</strong>：每秒执行浮点运算的次数，常用于衡量科学计算性能。</li>
</ul>
<hr />
<h3>1.4.3 CPU 性能公式应用</h3>
<p>在性能评估的相关计算中，以下核心公式是考试与实际评估的重点。</p>
<h3>时钟周期与主频的关系</h3>
<p>主频 $f$ 表示 CPU 每秒产生的时钟周期数。时钟周期 $T$ 表示一个时钟脉冲的持续时间。主频与时钟周期互为倒数：</p>
<p>$$
T = \frac{1}{f}
$$</p>
<h4>1. 主频单位换算（使用 10 的幂）</h4>
<p>主频以 $1000$ 递增换算：
$$
1\text{KHz} = 10^3\text{Hz}
$$
$$
1\text{MHz} = 10^6\text{Hz}
$$
$$
1\text{GHz} = 10^9\text{Hz}
$$</p>
<h4>2. 时间单位换算</h4>
<p>$$
1\text{s} = 10^3\text{ms} = 10^6\mu\text{s} = 10^9\text{ns}
$$</p>
<h4>3. 频率与周期的对应速记表</h4>
<p>在实际计算中，频率与周期存在以下直接对应关系，掌握后可显著加快解题速度：</p>
<table>
<thead>
<tr>
<th>主频</th>
<th>时钟周期</th>
</tr>
</thead>
<tbody>
<tr>
<td>$1\text{Hz}$</td>
<td>$1\text{s}$</td>
</tr>
<tr>
<td>$1\text{KHz}$</td>
<td>$1\text{ms}$</td>
</tr>
<tr>
<td>$1\text{MHz}$</td>
<td>$1\mu\text{s}$</td>
</tr>
<tr>
<td>$1\text{GHz}$</td>
<td>$1\text{ns}$</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>口诀速记</strong>：如 $2\text{GHz}$ 对应 $0.5\text{ns}$；$500\text{MHz}$ 对应 $2\text{ns}$。</p>
</blockquote>
<blockquote>
<p><strong>应用例题</strong>：某 CPU 的主频为 $4\text{GHz}$，求其时钟周期。</p>
<p><strong>解析</strong>：根据 $1\text{GHz} \leftrightarrow 1\text{ns}$ 的对应关系，主频为 $4\text{GHz}$，则时钟周期为：</p>
<p>$$
T = \frac{1}{4\text{GHz}} = 0.25\text{ns}
$$</p>
<p>展开计算过程为：</p>
<p>$$
T = \frac{1}{4 \times 10^9\text{Hz}} = 0.25 \times 10^{-9}\text{s} = 0.25\text{ns}
$$</p>
</blockquote>
<h3>总时钟周期数的计算</h3>
<p>总时钟周期数指 CPU 执行某个程序所消耗的全部时钟周期数量。可以把时钟周期理解为 CPU 的工作节拍，程序运行过程中经历的所有“工作节拍”总数即为总时钟周期数。</p>
<p>计算公式如下：</p>
<p>$$
\text{总时钟周期数} = \text{指令条数} \times \text{平均CPI}
$$</p>
<p>若以 $IC$ 表示指令条数，$\text{CPI}$ 表示平均每条指令所需的时钟周期数，公式可表示为：</p>
<p>$$
\text{Clock Cycles} = IC \times \text{CPI}
$$</p>
<blockquote>
<p><strong>应用例题</strong>：假设某程序包含 $1000$ 条指令，平均每条指令需要 $4$ 个时钟周期，求 CPU 执行该程序所需的总时钟周期数。</p>
<p><strong>解析</strong>：直接代入公式计算：</p>
<p>$$
\text{总时钟周期数} = 1000 \times 4 = 4000
$$</p>
<p>执行该程序一共需要经历 4000 个时钟周期。</p>
</blockquote>
<h3>平均 CPI 的计算</h3>
<p>CPI 表示平均每条指令执行所需的时钟周期数。</p>
<p>$$
\text{CPI} = \frac{\text{总时钟周期数}}{\text{指令条数}}
$$</p>
<p>若程序中包含多种类型的指令，每种指令的 CPI 和所占比例不同，则需要计算平均 CPI：</p>
<p>$$
\text{平均}\text{CPI} = \sum_{i=1}^{n}(\text{CPI}_i \times P_i)
$$</p>
<p>其中，$\text{CPI}_i$ 为第 $i$ 类指令的 CPI，$P_i$ 为该类指令在程序中的占比。</p>
<blockquote>
<p><strong>应用例题</strong>：某程序中包含三类指令，它们在程序中的占比及各自的 CPI 如下表所示。求该程序的平均 CPI。</p>
<table>
<thead>
<tr>
<th>指令类型</th>
<th>占比</th>
<th>CPI</th>
</tr>
</thead>
<tbody>
<tr>
<td>算术逻辑指令</td>
<td>60%</td>
<td>2</td>
</tr>
<tr>
<td>访存指令</td>
<td>30%</td>
<td>4</td>
</tr>
<tr>
<td>分支指令</td>
<td>10%</td>
<td>3</td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<p>$$
\text{平均}\text{CPI} = 2 \times 60% + 4 \times 30% + 3 \times 10% = 1.2 + 1.2 + 0.3 = 2.7
$$</p>
</blockquote>
<h3>CPU 执行时间的计算</h3>
<p>CPU 执行时间是衡量计算机实际速度最真实的指标。</p>
<p>$$
T_{\text{CPU}} = \text{指令条数} \times \text{CPI} \times \text{时钟周期}
$$</p>
<p>由于时钟周期 $T$ 与主频 $f$ 互为倒数（$T = \frac{1}{f}$），代入得：</p>
<p>$$
T_{\text{CPU}} = IC \times \text{CPI} \times \frac{1}{f} = \frac{IC \times \text{CPI}}{f}
$$</p>
<blockquote>
<p><strong>应用例题</strong>：某程序包含 $10^6$ 条指令，运行在主频为 $2\text{GHz}$ 的处理器上，该程序的平均 CPI 为 1.5。求该程序在该处理器上的 CPU 执行时间。</p>
<p><strong>解析</strong>：</p>
<p>$$
T_{\text{CPU}} = \frac{10^6 \times 1.5}{2 \times 10^9\text{Hz}} = 0.75 \times 10^{-3}\text{s} = 0.75\text{ms}
$$</p>
</blockquote>
<h3>MIPS 的计算</h3>
<p>MIPS 表示每秒执行的百万条指令数。</p>
<p>$$
\text{MIPS} = \frac{\text{指令条数}}{\text{执行时间} \times 10^6}
$$</p>
<p>由 CPU 执行时间公式 $T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f}$ 代入执行时间：</p>
<p>$$
\text{MIPS} = \frac{IC}{\frac{IC \times \text{CPI}}{f} \times 10^6}
= \frac{IC \times f}{IC \times \text{CPI} \times 10^6}
= \frac{f}{\text{CPI} \times 10^6}
$$</p>
<p>如果主频的单位是 MHz，公式可以简化为：</p>
<p>$$
\text{MIPS} = \frac{\text{主频}(\text{MHz})}{\text{CPI}}
$$</p>
<blockquote>
<p><strong>应用例题</strong>：某 CPU 的主频为 $800\text{MHz}$，执行某特定程序的平均 CPI 为 2.5。求该 CPU 的运行速度。</p>
<p><strong>解析</strong>：由于主频单位已为 MHz，可直接套用简化公式：</p>
<p>$$
\text{MIPS} = \frac{800}{2.5} = 320
$$</p>
<p>该 CPU 在执行该程序时的速度为 $320\text{MIPS}$。</p>
</blockquote>
<h3>GIPS 的计算</h3>
<p>GIPS 表示每秒执行的十亿条指令数（即 $10^9$ 条指令），常用于现代高频处理器的性能衡量。</p>
<p>$$
\text{GIPS} = \frac{\text{指令条数}}{\text{执行时间} \times 10^9}
$$</p>
<p>由 CPU 执行时间公式 $T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f}$ 代入执行时间：</p>
<p>$$
\text{GIPS} = \frac{IC}{\frac{IC \times \text{CPI}}{f} \times 10^9}
= \frac{IC \times f}{IC \times \text{CPI} \times 10^9}
= \frac{f}{\text{CPI} \times 10^9}
$$</p>
<p>如果主频的单位是 GHz，公式可以简化为：</p>
<p>$$
\text{GIPS} = \frac{\text{主频}(\text{GHz})}{\text{CPI}}
$$</p>
<blockquote>
<p><strong>应用例题</strong>：某 CPU 主频为 $1.5\text{GHz}$，执行某特定程序的平均 CPI 为 1.2。求该 CPU 的指令执行速度（用 GIPS 表示）。</p>
<p><strong>解析</strong>：由于主频单位已为 GHz，可直接套用简化公式：</p>
<p>$$
\text{GIPS} = \frac{1.5}{1.2} = 1.25
$$</p>
<p>该 CPU 在执行该程序时的速度为 $1.25\text{GIPS}$。</p>
</blockquote>
<h3>MFLOPS 的计算</h3>
<p>MFLOPS 表示每秒执行浮点运算的次数（以百万次为单位），常用于衡量科学计算性能。</p>
<p>$$
\text{MFLOPS} = \frac{\text{浮点操作次数}}{\text{执行时间} \times 10^6}
$$</p>
<blockquote>
<p><strong>应用例题</strong>：某台计算机执行一个浮点程序，该程序共执行了 $3 \times 10^7$ 次浮点操作，总运行时间为 0.15 秒。求该计算机的浮点运算速度。</p>
<p><strong>解析</strong>：</p>
<p>$$
\text{MFLOPS} = \frac{3 \times 10^7}{0.15\text{s} \times 10^6} = \frac{3}{0.15} \times \frac{10^7}{10^6} = 20 \times 10 = 200
$$</p>
<p>该计算机在执行该程序时的速度为 $200\text{MFLOPS}$。</p>
</blockquote>
<h3>综合性能比较</h3>
<p>比较两台计算机性能时，不能只看主频，还要结合 CPI 和执行时间。</p>
<blockquote>
<p><strong>应用例题</strong>：两台机器执行同一个程序，指令条数 $IC$ 相同：</p>
<ul>
<li>机器 A：平均 $\text{CPI}<em>A = 3$，时钟周期 $T</em>{cA} = 2\text{ns}$</li>
<li>机器 B：平均 $\text{CPI}<em>B = 2$，时钟周期 $T</em>{cB} = 4\text{ns}$</li>
</ul>
<p>请判定哪台机器的性能更好。</p>
<p><strong>解析</strong>：计算两台机器的 CPU 执行时间：</p>
<p>$$
T_A = IC \times 3 \times 2\text{ns} = 6IC\text{ns}
$$</p>
<p>$$
T_B = IC \times 2 \times 4\text{ns} = 8IC\text{ns}
$$</p>
<p>由于 $T_A &lt; T_B$，因此机器 A 的实际执行速度更快，性能更好。尽管机器 B 的 CPI 更低，但由于其时钟周期较长，导致最终耗时更多。</p>
</blockquote>
<h3>基准测试程序</h3>
<p>因为单一的峰值性能指标往往不能反映实际工作负载下的真实情况，因此在实际应用中，通常使用基准测试程序（如 Dhrystone、SPEC 等评测集）来综合评价计算机的整体性能。</p>
<hr />
<h2>1.4.4 性能测试、典型题型与易错点</h2>
<p>为了帮助更好地应对考试与实际应用，本节整理了第一章的期末考查高频题、典型题型、名词解释对比及常见易错点。</p>
<h3>历年期末考查高频选择与填空点</h3>
<p>:::important[核心高频必背考点]</p>
<ul>
<li><strong>冯·诺依曼机工作方式</strong>：基本工作方式是<strong>控制流驱动方式</strong>，其基本特点是<strong>按地址访问并顺序执行指令</strong>。</li>
<li><strong>冯·诺依曼采用二进制的原因</strong>：数据采用二进制编码表示的主要原因为：<strong>制造两个稳态的物理器件较容易</strong>、<strong>二进制的运算规则简单</strong>、<strong>便于用逻辑门电路实现算术运算</strong>。</li>
<li><strong>CPI 与时钟频率无关</strong>：CPI 仅与系统结构、指令集和计算机组织有关，<strong>与时钟频率无关</strong>。
:::</li>
</ul>
<p>:::tip[常考基础指标与概念]</p>
<ul>
<li><strong>存储速度位次</strong>：CPU 存取速度的排序为：<strong>寄存器 &gt; Cache &gt; 内存</strong>。</li>
<li><strong>32 位微机的含义</strong>：32 位微机中的"32 位"是指该计算机所用的 CPU <strong>能同时处理 32 位的二进制数</strong>（即机器字长为 32 位）。</li>
<li><strong>MAR 与 MDR 位数确定规则</strong>：<strong>MAR 的位数等于地址码长度</strong>（决定了最大寻址空间）；<strong>MDR 的位数等于存储字长</strong>（决定了一次存取的数据位数）。例如：1MB 容量的存储器按字节编址时主存包含 $2^{20}$ 个字节单元，故地址寄存器 MAR 为 20 位；若存储字长为 32 位，则数据寄存器 MDR 为 32 位。</li>
<li><strong>机器字长、指令字长与存储字长</strong>：三者在数值上<strong>可能不等</strong>。存储字长是存放在一个存储单元中的二进制代码位数。</li>
<li><strong>层次结构的依存关系</strong>：计算机系统 6 层结构之间的关系为：<strong>上层实现对下层的功能扩展，而下层是实现上层的基础</strong>。</li>
<li><strong>编译程序与解释程序的性能</strong>：编译程序和解释程序都能将高级语言翻译为机器语言，但编译程序编译时间较长、运行速度较快；<strong>解释程序翻译一条立即执行一条，方法简单但运行速度较慢</strong>。
:::</li>
</ul>
<hr />
<h3>概念与简答精选</h3>
<p>:::important[CPU 区分指令和数据的两种方法]
在冯·诺依曼结构计算机中，指令和数据都以二进制形式存放在存储器中，CPU 主要通过以下两种方法区分它们：</p>
<ol>
<li><strong>通过不同时间段区分</strong>：在取指阶段从主存中取出的是指令；在执行阶段从主存中取出的是操作数。</li>
<li><strong>通过地址来源区分</strong>：由程序计数器 PC 提供存储单元地址取出的是指令；由指令地址码部分提供存储单元地址取出的是操作数。
:::</li>
</ol>
<p>:::tip[控制器的三项具体功能]</p>
<ol>
<li><strong>指令控制</strong>：从主存储器中取出指令，并指出下一条指令在主存中的位置。</li>
<li><strong>操作控制</strong>：对指令进行译码，并产生相应的操作控制信号，以便启动规定的动作。</li>
<li><strong>数据加工与传输控制</strong>：指挥并控制 CPU、主存和输入输出设备之间数据流动的方向。</li>
</ol>
<p><em>注：取指操作是控制器固有的功能，不需要在操作码的控制下完成。取指令是控制器的基本动作，不受当前指令操作码支配。</em>
:::</p>
<p>:::tip[CPU 的四项基本功能]</p>
<ol>
<li><strong>程序控制</strong>：控制指令的执行顺序。</li>
<li><strong>操作控制</strong>：控制指令进行具体的物理操作。</li>
<li><strong>时间控制</strong>：对各种操作实施严格的定时控制。</li>
<li><strong>数据加工</strong>：对数据进行算术运算和逻辑运算。
:::</li>
</ol>
<p>:::note[主机的精确定义]
主机是指计算机除去输入输出设备以外的主要机体部分。主机中包含了除外围设备外所有的电路部件（即由 CPU 和主存储器组成），是一个能够独立工作的系统。
:::</p>
<h4>软件开发与执行工具对比</h4>
<p>在软件执行与转换过程中，以下几个概念极易混淆：</p>
<ul>
<li><strong>汇编器</strong>：用于将汇编语言源程序翻译成机器语言目标程序。</li>
<li><strong>编译器</strong>：用于将高级语言源程序翻译成低级语言程序（通常为汇编程序或机器语言目标代码）。</li>
<li><strong>解释器</strong>：边翻译边执行源程序或中间代码，不生成独立的目标程序。</li>
<li><strong>链接器</strong>：用于将多个目标文件和库文件连接起来，生成最终的可执行文件。</li>
</ul>
<table>
<thead>
<tr>
<th>工具</th>
<th>翻译方式</th>
<th>是否生成目标程序</th>
<th>典型代表</th>
</tr>
</thead>
<tbody>
<tr>
<td>编译器</td>
<td>整体翻译后执行</td>
<td>是</td>
<td>C / C++ 编译器</td>
</tr>
<tr>
<td>解释器</td>
<td>逐条翻译并执行</td>
<td>否</td>
<td>Python 解释器</td>
</tr>
</tbody>
</table>
<h4>硬件、软件与固件</h4>
<ul>
<li><strong>硬件</strong>：计算机中实际存在的物理实体（如 CPU、存储器、外部设备）。</li>
<li><strong>软件</strong>：程序、数据和相关文档的集合。</li>
<li><strong>固件</strong>：固化在硬件（如只读存储器）中的程序。固件兼具软件和硬件的特点，如计算机主板上的 BIOS。</li>
</ul>
<h4>IPC 与 CPI 的关系</h4>
<ul>
<li><strong>CPI</strong>：平均每条指令执行所需的时钟周期数。</li>
<li><strong>IPC</strong>：每个时钟周期能够执行的指令条数。
二者在理想情况下互为倒数：</li>
</ul>
<p>$$
\text{IPC} = \frac{1}{\text{CPI}}
$$</p>
<h4>ALU 与寄存器位数</h4>
<p>在硬件技术指标中，<strong>ALU（算术逻辑单元）的宽度</strong>和<strong>通用寄存器的位数</strong>通常与机器字长保持一致，而<strong>指令寄存器</strong>和<strong>浮点寄存器</strong>的位数则不一定等于机器字长。</p>
<hr />
<h3>术语标准概念集</h3>
<ul>
<li><strong>机器字长</strong>：计算机进行一次整数运算所能处理的二进制数据的位数，即 CPU 一次能处理的数据的位数。</li>
<li><strong>存储字长</strong>：存储器一次存取操作的最大位数。</li>
<li><strong>存储字</strong>：存放在一个存储单元中的二进制代码组合。</li>
<li><strong>指令字长</strong>：机器指令中二进制代码的总位数，取决于操作码长度、操作数地址长度和个数。</li>
<li><strong>存储容量</strong>：通常用 MAR 的编址数与存储字位数的乘积来表示。</li>
<li><strong>指令周期、机器周期、时钟周期的层级关系</strong>：
<ul>
<li><strong>指令周期</strong>：CPU 取出一条指令并执行该指令的时间。</li>
<li><strong>机器周期</strong>：又称 CPU 周期，通常由若干个时钟周期构成，是指令执行中的一个阶段时间（如取指周期、执行周期）。</li>
<li><strong>时钟周期</strong>：计算机操作的最小时间单位，是主频的倒数。</li>
<li><strong>层级关系</strong>：一个指令周期通常包含若干个机器周期，一个机器周期又包含若干个时钟周期。</li>
</ul>
</li>
</ul>
<hr />
<h3>性能计算典型真题</h3>
<h4>题型 1：阿姆达尔定律与系统加速比</h4>
<blockquote>
<p><strong>期末真题</strong>：基准程序 A 在某计算机上的运行时间为 100 秒，其中 90 秒为 CPU 时间，其余为输入输出时间。若 CPU 速度提高 50%，输入输出速度保持不变，求运行该程序所耗费的时间、整体速度提升比例，以及这一计算结果给我们的性能优化启发。</p>
</blockquote>
<p><strong>解析</strong>：</p>
<ol>
<li>原系统总时间：$T_{\text{old}} = 100\text{s}$，其中 CPU 时间为 $90\text{s}$，输入输出时间为 $10\text{s}$。</li>
<li>CPU 速度提高 50%，意味着 CPU 执行相同指令所花的时间缩短为原来的 $\frac{1}{1 + 50%} = \frac{1}{1.5}$。</li>
<li>优化后的 CPU 时间：
$$
T'_{\text{CPU}} = \frac{90\text{s}}{1.5} = 60\text{s}
$$</li>
<li>优化后的总时间：
$$
T_{\text{new}} = T'<em>{\text{CPU}} + T</em>{\text{IO}} = 60\text{s} + 10\text{s} = 70\text{s}
$$</li>
<li>整体加速比：
$$
S = \frac{T_{\text{old}}}{T_{\text{new}}} = \frac{100}{70} \approx 1.43
$$</li>
<li>整体性能提升比例为：$1.43 - 1 = 43%$。</li>
<li><strong>性能提升启发</strong>：在进行计算机性能优化时，不能单单考虑 CPU 速度的提升。根据阿姆达尔定律，系统整体的速度提升受限于那些无法被优化或优化程度较低的部分（如输入输出速度）。因此，必须均衡提升各部件的性能，以避免出现系统瓶颈。</li>
</ol>
<h4>题型 2：编译优化与指令条数变化</h4>
<blockquote>
<p><strong>真题</strong>：程序 P 在某计算机上的执行时间为 20 秒。编译优化后指令数减少到原来的 70%，而平均 CPI 增加到原来的 1.2 倍，求优化后的执行时间及加速比。</p>
</blockquote>
<p><strong>解析</strong>：</p>
<ol>
<li>设优化前的指令数为 $IC$，平均 CPI 为 $\text{CPI}$，主频为 $f$。原执行时间为：
$$
T_{\text{old}} = \frac{IC \times \text{CPI}}{f} = 20\text{s}
$$</li>
<li>优化后的指令数 $IC' = 0.7IC$，平均 CPI $CPI' = 1.2\text{CPI}$，主频 $f$ 不变。</li>
<li>优化后的执行时间：
$$
T_{\text{new}} = \frac{0.7IC \times 1.2\text{CPI}}{f} = 0.84 \times \frac{IC \times \text{CPI}}{f} = 0.84 \times 20\text{s} = 16.8\text{s}
$$</li>
<li>加速比：
$$
S = \frac{T_{\text{old}}}{T_{\text{new}}} = \frac{20}{16.8} \approx 1.19
$$</li>
</ol>
<h4>题型 3：同一程序在不同机器上的性能对比</h4>
<blockquote>
<p><strong>期末真题</strong>：计算机 M1 和 M2 采用相同的指令集架构，主频分别为 $1.5\text{GHz}$ 和 $1.2\text{GHz}$，平均 CPI 分别为 2 和 1。求同一程序在 M1 和 M2 上运行时间的比值，并说明哪台机器更快。</p>
</blockquote>
<p><strong>解析</strong>：</p>
<ol>
<li>由于采用相同指令集且运行同一程序，因此指令条数 $IC$ 相同。</li>
<li>M1 的执行时间：
$$
T_1 = \frac{IC \times 2}{1.5 \times 10^9\text{Hz}}
$$</li>
<li>M2 的执行时间：
$$
T_2 = \frac{IC \times 1}{1.2 \times 10^9\text{Hz}}
$$</li>
<li>两者运行时间之比：
$$
\frac{T_1}{T_2} = \frac{IC \times 2 / (1.5 \times 10^9)}{IC \times 1 / (1.2 \times 10^9)} = \frac{2 \times 1.2}{1.5} = 1.6
$$</li>
<li>结果表明，M1 的运行时间是 M2 的 1.6 倍，因此 M2 运行该程序的速度更快，是 M1 的 1.6 倍。</li>
</ol>
<h4>题型 4：主频、指令数与运行速度的综合计算</h4>
<blockquote>
<p><strong>期末真题</strong>：某计算机主频为 $1.5\text{GHz}$，执行某特定程序的指令条数为 $5 \times 10^5$，运行该程序时的平均 CPI 为 1.2。求执行该程序的 CPU 时间和指令执行速度（用 GIPS 表示）。若主频降为 $1\text{GHz}$，其他条件不变，求对应的 CPU 时间和指令执行速度。</p>
</blockquote>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>主频为 1.5GHz 时</strong>：
<ul>
<li>CPU 时间：
$$
T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f} = \frac{5 \times 10^5 \times 1.2}{1.5 \times 10^9\text{Hz}} = \frac{6 \times 10^5}{1.5 \times 10^9\text{Hz}} = 4 \times 10^{-4}\text{s} = 0.4\text{ms}
$$</li>
<li>指令执行速度：
$$
\text{GIPS} = \frac{f}{\text{CPI} \times 10^9} = \frac{1.5 \times 10^9}{1.2 \times 10^9} = 1.25\text{GIPS}
$$</li>
</ul>
</li>
<li><strong>主频为 1GHz 时</strong>：
<ul>
<li>CPU 时间：
$$
T_{\text{CPU}} = \frac{5 \times 10^5 \times 1.2}{1 \times 10^9\text{Hz}} = \frac{6 \times 10^5}{1 \times 10^9\text{Hz}} = 6 \times 10^{-4}\text{s} = 0.6\text{ms}
$$</li>
<li>指令执行速度：
$$
\text{GIPS} = \frac{1 \times 10^9}{1.2 \times 10^9} \approx 0.833\text{GIPS}
$$</li>
</ul>
</li>
</ol>
<h4>题型 5：混合指令的平均 CPI 与 MIPS 计算</h4>
<blockquote>
<p><strong>期末真题</strong>：已知某计算机主频为 $800\text{MHz}$，其指令分为四类，它们在基准程序中所占的比例及各自的 CPI 如下表所示。求该计算机的平均 CPI 以及运行速度（用 MIPS 表示）。若主频提升至 $1.2\text{GHz}$，且四类指令所占比例及各自 CPI 均保持不变，求提升主频后的 MIPS。</p>
<table>
<thead>
<tr>
<th>指令类型</th>
<th>所占比例</th>
<th>CPI</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>40%</td>
<td>2</td>
</tr>
<tr>
<td>B</td>
<td>30%</td>
<td>2</td>
</tr>
<tr>
<td>C</td>
<td>20%</td>
<td>3</td>
</tr>
<tr>
<td>D</td>
<td>10%</td>
<td>5</td>
</tr>
</tbody>
</table>
</blockquote>
<p><strong>解析</strong>：</p>
<ol>
<li>计算平均 CPI：
$$
\text{平均}\text{CPI} = 2 \times 40% + 2 \times 30% + 3 \times 20% + 5 \times 10% = 0.8 + 0.6 + 0.6 + 0.5 = 2.5
$$</li>
<li><strong>主频为 800MHz 时</strong>：
<ul>
<li>由于主频为 $800\text{MHz}$，可套用简化公式计算 MIPS：
$$
\text{MIPS} = \frac{800}{2.5} = 320
$$</li>
</ul>
</li>
<li><strong>主频提升至 1.2GHz 时</strong>：
<ul>
<li>主频 $1.2\text{GHz} = 1200\text{MHz}$，计算提升后的 MIPS：
$$
\text{MIPS} = \frac{1200}{2.5} = 480
$$</li>
</ul>
</li>
</ol>
<hr />
<h3>常见考试易错点警示</h3>
<p>:::warning[易错点 1：主频高不等于执行时间短]
由公式 $T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f}$ 可知，主频只是影响执行时间的要素之一。若一台高主频计算机的 CPI 极大，或编译器生成的指令条数极多，其执行速度反而可能低于低主频计算机。<strong>执行时间是评估性能的最终标准</strong>。
:::</p>
<p>:::warning[易错点 2：缩短程序执行时间的措施]
要缩短程序执行时间，可以采取以下三项有效措施：</p>
<ul>
<li><strong>提高时钟频率</strong>：提高 CPU 主频。</li>
<li><strong>优化数据通路结构</strong>：降低平均 CPI。</li>
<li><strong>对程序进行编译优化</strong>：减少指令条数 $IC$ 并降低平均 CPI。
:::</li>
</ul>
<p>:::warning[易错点 3：MIPS 指标的局限性]
MIPS 仅代表每秒执行的百万条指令数。然而，对于不同的指令集架构，单条指令的复杂度和功能差异巨大。用简单指令集运行程序可能需要极多的指令数（MIPS 虽高，但耗时多），而复杂指令集可能只需少量指令。因此，MIPS 不能跨架构用于绝对性能的横向对比。
:::</p>
<p>:::warning[易错点 4：优化后平均 CPI 上升，程序仍可能变快]
在编译优化中，可能通过消除大量执行周期极短的简单指令（如无用的数据搬运），导致程序中高周期指令的比例上升，从而使平均 CPI 变大。但由于指令条数 $IC$ 大幅减少，总的执行时间 $T_{\text{CPU}}$ 反而缩短。
:::</p>
<p>:::warning[易错点 5：CPU 时间不等于总运行时间]
程序在系统中的总运行时间除了包括 CPU 执行指令的时间外，还包含了等待输入输出、页中断调页、系统调度等非 CPU 占用时间。在性能改进中，若只优化了 CPU 性能，输入输出等时间是无法被缩短的。
:::</p>
<hr />
<h2>1.5 课程学习建议与本章总结</h2>
<p>本章的重点知识与计算公式整理如下：</p>
<h3>核心结构体系</h3>
<ul>
<li>冯·诺依曼结构的核心是<strong>二进制</strong>与<strong>存储程序</strong>。</li>
<li><strong>主机</strong>由 CPU 和主存储器组成，各部件通过包含数据线、地址线和控制线的<strong>总线</strong>进行互连。</li>
<li><strong>指令集架构层</strong>是计算机软、硬件系统之间的界面和纽带。</li>
</ul>
<h3>核心计算公式一览</h3>
<table>
<thead>
<tr>
<th>指标</th>
<th>公式 / 关系式</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>时钟周期</td>
<td>$T = 1 / f$</td>
<td>与主频互为倒数</td>
</tr>
<tr>
<td>平均 CPI</td>
<td>$\text{平均}\text{CPI} = \sum(\text{CPI}_i \times P_i)$</td>
<td>各类指令占比加权求和</td>
</tr>
<tr>
<td>CPU 执行时间</td>
<td>$T_{\text{CPU}} = IC \times \text{CPI} \times T = \frac{IC \times \text{CPI}}{f}$</td>
<td>评估计算机性能最核心的标准</td>
</tr>
<tr>
<td>MIPS</td>
<td>$\text{MIPS} = \frac{f}{\text{CPI} \times 10^6}$</td>
<td>若 $f$ 单位为 MHz，则为 $\frac{f}{\text{CPI}}$</td>
</tr>
<tr>
<td>GIPS</td>
<td>$\text{GIPS} = \frac{f}{\text{CPI} \times 10^9}$</td>
<td>若 $f$ 单位为 GHz，则为 $\frac{f}{\text{CPI}}$</td>
</tr>
<tr>
<td>MFLOPS</td>
<td>$\text{MFLOPS} = \frac{\text{浮点操作次数}}{\text{执行时间} \times 10^6}$</td>
<td>衡量科学计算性能</td>
</tr>
</tbody>
</table>
<p>性能评估是一个综合性的工作。在进行性能比较时，必须从实际的 <strong>CPU 执行时间</strong> 和 <strong>基准测试程序</strong> 出发进行综合计算与判定，切忌只凭主频高低妄下结论。</p>
<hr />
<h2>习题1：课后习题与真题详解</h2>
<p><img src="https://img.picr2.online/1782625705827_image.webp" alt="课后习题截图1" />
<img src="https://img.picr2.online/1782625723557_image.webp" alt="课后习题截图2" /></p>
<h3>1.1 名词解释</h3>
<ol>
<li><strong>摩尔定律</strong>：集成电路上可容纳的晶体管数量大约每 18 到 24 个月翻一番，是半导体工艺快速迭代的经验性规律。</li>
<li><strong>汇编器</strong>：负责将用助记符书写的汇编语言源程序整体翻译成二进制机器语言目标程序的工具软件。</li>
<li><strong>编译器</strong>：负责将高级语言源程序整体翻译成低级机器代码或汇编代码的程序。</li>
<li><strong>解释器</strong>：逐句翻译并立即执行高级语言程序的软件，边解释边执行，通常不产生独立的目标文件。</li>
<li><strong>链接器</strong>：将多个独立编译的目标模块和系统提供的库函数组合，生成统一可执行目标文件。</li>
<li><strong>时钟周期</strong>：时钟信号脉冲的持续时间，是 CPU 工作最基本的最小时间单位，其数值与主频互为倒数。</li>
<li><strong>机器字长</strong>：CPU 一次能够直接进行算术或逻辑运算的二进制位数，常与通用寄存器和 ALU 的宽度一致。</li>
<li><strong>主存容量</strong>：主存储器中可寻址和存储信息的最大总量，通常使用 2 的幂作为换算进制。</li>
<li><strong>CPI</strong>：平均每条指令执行所需的时钟周期数。</li>
<li><strong>IPC</strong>：每个时钟周期平均执行的指令条数，其数值与 CPI 互为倒数。</li>
<li><strong>MIPS</strong>：平均每秒执行的百万条指令数。</li>
<li><strong>MFLOPS</strong>：平均每秒执行的百万次浮点运算次数。</li>
<li><strong>CPU 时间</strong>：CPU 运行特定程序所真正消耗的计算时间，不包含等待外设和系统调度的非 CPU 占用时间。</li>
</ol>
<hr />
<h3>1.2 选择题详解</h3>
<h4>第 1 题</h4>
<p>冯·诺依曼结构计算机中数据采用二进制编码表示，其主要原因是_______。</p>
<p>Ⅰ. 二进制运算规则简单
Ⅱ. 制造两个稳态的物理器件较为容易
Ⅲ. 便于逻辑门电路实现算术运算</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ul>
<li><strong>Ⅰ 正确</strong>：二进制仅有 0 和 1 两种状态，加减乘除的逻辑规则远比十进制简单。</li>
<li><strong>Ⅱ 正确</strong>：在物理硬件上，制造具有两个稳定状态（如电压高低、电容充电放电、晶体管导通截止）的元器件门槛最低、可靠性最高。</li>
<li><strong>Ⅲ 正确</strong>：基础的逻辑门电路天然便于实现逻辑与、或、非，从而极易组合出实现算术加减法运算的加法器。</li>
</ul>
<hr />
<h4>第 2 题</h4>
<p>下列关于冯·诺依曼结构计算机基本思想的叙述中，错误的是_______。</p>
<p>A. 程序的功能都通过中央处理器执行指令实现
B. 指令和数据都用二进制表示，形式上无差别
C. 指令按地址访问，数据都在指令中直接给出
D. 程序执行前，指令和数据需预先存放在存储器中</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：C</strong></p>
<ul>
<li><strong>A 正确</strong>：现代计算机执行程序即由 CPU 不断从存储器取指令、解析指令并执行指令来完成全部功能。</li>
<li><strong>B 正确</strong>：指令和数据在主存中都是一连串二进制代码，表现形式完全一样，仅在不同工作阶段由 CPU 负责区分。</li>
<li><strong>C 错误</strong>：前半句正确，但后半句错误。操作数（数据）可以存放在寄存器、主存或输入输出接口中，通过各种寻址方式在指令中给出地址指针，而非只能以立即数形式在指令中直接给出。</li>
<li><strong>D 正确</strong>：这是存储程序的经典核心思想，程序启动前其指令和数据必须一并装入主存。</li>
</ul>
<hr />
<h4>第 3 题</h4>
<p>将高级语言源程序转换为机器级目标代码文件的程序称为_______。</p>
<p>A. 汇编程序
B. 链接程序
C. 编译程序
D. 解释程序</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：C</strong></p>
<ul>
<li><strong>A 错误</strong>：汇编程序是将汇编助记符源程序翻译为二进制机器级目标程序。</li>
<li><strong>B 错误</strong>：链接程序是将多个目标文件与静态库函数组合并重新分配地址，生成最终的可执行文件。</li>
<li><strong>C 正确</strong>：编译程序将高级语言编写的源程序一次性翻译为等价的汇编或机器级目标程序。</li>
<li><strong>D 错误</strong>：解释程序不生成独立的机器级目标文件，而是采用逐句翻译并立即执行的方式运行。</li>
</ul>
<hr />
<h4>第 4 题</h4>
<p>将高级语言源程序转换为可执行目标文件的主要过程是_______。</p>
<p>A. 预处理 $\rightarrow$ 编译 $\rightarrow$ 汇编 $\rightarrow$ 链接
B. 预处理 $\rightarrow$ 汇编 $\rightarrow$ 编译 $\rightarrow$ 链接
C. 预处理 $\rightarrow$ 编译 $\rightarrow$ 链接 $\rightarrow$ 汇编
D. 预处理 $\rightarrow$ 汇编 $\rightarrow$ 链接 $\rightarrow$ 编译</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：A</strong></p>
<ul>
<li><strong>A 正确</strong>：构建 C/C++ 等高级语言的规范工具链流程为：
<ol>
<li><strong>预处理</strong>：展开宏定义、处理条件编译和包含的头文件；</li>
<li><strong>编译</strong>：将源程序转换为具体的汇编代码文件；</li>
<li><strong>汇编</strong>：将汇编代码转换为二进制的目标机器码文件；</li>
<li><strong>链接</strong>：整合各目标模块，引入库函数并定位逻辑地址，输出最终的二进制可执行目标文件。</li>
</ol>
</li>
<li><strong>B、C、D 错误</strong>：步骤逻辑次序混乱。</li>
</ul>
<hr />
<h4>第 5 题</h4>
<p>计算机硬件能够直接执行的是_______。</p>
<p>Ⅰ. 机器语言程序
Ⅱ. 汇编语言程序
Ⅲ. 硬件描述语言程序</p>
<p>A. 仅 Ⅰ
B. 仅 Ⅰ、Ⅱ
C. 仅 Ⅰ、Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：A</strong></p>
<ul>
<li><strong>Ⅰ 正确</strong>：机器语言是由 0 和 1 组成的指令集体系结构，硬件电路可直接译码和运行。</li>
<li><strong>Ⅱ 错误</strong>：汇编语言无法被 CPU 电路直接识别，必须先使用汇编器翻译为机器语言程序。</li>
<li><strong>Ⅲ 错误</strong>：硬件描述语言用于描述电路结构或逻辑行为，通常用于逻辑仿真或电路综合，并非可直接由现成 CPU 硬件执行的二进制程序。</li>
</ul>
<hr />
<h4>第 6 题</h4>
<p>下列给出的部件中，其位数，也就是宽度，一定与机器字长相同的是_______。</p>
<p>Ⅰ. ALU
Ⅱ. 指令寄存器
Ⅲ. 通用寄存器
Ⅳ. 浮点寄存器</p>
<p>A. 仅 Ⅰ、Ⅱ
B. 仅 Ⅰ、Ⅲ
C. 仅 Ⅱ、Ⅲ
D. 仅 Ⅱ、Ⅲ、Ⅳ</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：B</strong></p>
<ul>
<li><strong>Ⅰ 正确</strong>：算术逻辑单元用于执行定点算术逻辑运算，其基本位宽往往直接体现并限制了主机的定点一次处理宽度（即机器字长）。</li>
<li><strong>Ⅱ 错误</strong>：指令寄存器的宽度仅需容纳单条指令即可，它由指令字长决定，而指令字长与定点数据宽度（机器字长）不必然相等。</li>
<li><strong>Ⅲ 正确</strong>：通用寄存器用于承载和暂存基础操作数，其宽度与机器字长保持完全一致。</li>
<li><strong>Ⅳ 错误</strong>：浮点寄存器用于处理精度极高的科学计算（例如单精度 32 位、双精度 64 位、双精度扩展 80 位等），其宽度独立于 CPU 定点字长。</li>
</ul>
<hr />
<h4>第 7 题</h4>
<p>下列选项中，描述浮点数操作速度指标的是_______。</p>
<p>A. MIPS
B. CPI
C. IPC
D. MFLOPS</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ul>
<li><strong>A 错误</strong>：MIPS 指标表示每秒执行的百万条指令数，用于评价综合指令运行速度。</li>
<li><strong>B 错误</strong>：CPI 表示单条指令所需的平均时钟周期数。</li>
<li><strong>C 错误</strong>：IPC 表示单时钟周期的平均执行指令数。</li>
<li><strong>D 正确</strong>：MFLOPS 表示每秒百万次浮点操作数，常用于衡量浮点核心计算能力。</li>
</ul>
<hr />
<h4>第 8 题</h4>
<p>2017 年公布的全球超级计算机排名中，“神威·太湖之光”浮点运算速度为 $93.0146\text{ PFLOPS}$，说明该计算机每秒完成的浮点操作次数约为_______。</p>
<p>A. $9.3 \times 10^{13}$ 次
B. $9.3 \times 10^{15}$ 次
C. $9.3$ 千万亿次
D. $9.3$ 亿亿次</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ul>
<li>国际单位制中单位前缀 P 代表皮它（即 $10^{15}$）。</li>
<li>因此 $93.0146\text{ PFLOPS} = 93.0146 \times 10^{15}\text{ 次/秒} \approx 9.3 \times 10^{16}\text{ 次/秒}$。</li>
<li>$1\text{亿亿} = 10^8 \times 10^8 = 10^{16}$，所以 $9.3 \times 10^{16}$ 换算为汉字单位即为 $9.3$ 亿亿次。</li>
</ul>
<hr />
<h4>第 9 题</h4>
<p>下列选项中，能缩短程序执行时间的措施是_______。</p>
<p>Ⅰ. 提高 CPU 时钟频率
Ⅱ. 优化数据通路结构
Ⅲ. 对程序进行编译优化</p>
<p>A. 仅 Ⅰ 和 Ⅱ
B. 仅 Ⅰ 和 Ⅲ
C. 仅 Ⅱ 和 Ⅲ
D. Ⅰ、Ⅱ、Ⅲ</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ul>
<li>根据 CPU 执行时间公式 $T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f}$：
<ul>
<li><strong>Ⅰ 正确</strong>：提高 CPU 主频 $f$ 可直接使周期缩短，降低执行时间。</li>
<li><strong>Ⅱ 正确</strong>：优化数据通路结构能够降低不同指令的平均 $\text{CPI}$，从而减少总时钟周期数。</li>
<li><strong>Ⅲ 正确</strong>：依靠编译优化可以对指令逻辑重新排布，从而缩减指令条数 $IC$ 且降低指令运行的加权平均 $\text{CPI}$。</li>
</ul>
</li>
</ul>
<hr />
<h4>第 10 题</h4>
<p>某计算机主频为 $1.2\text{ GHz}$，其指令分为 4 类，它们在基准程序中所占比例及 CPI 如下表所示。该机的 MIPS 数是_______。</p>
<table>
<thead>
<tr>
<th>指令类型</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
<tr>
<td>占比</td>
<td>50%</td>
<td>20%</td>
<td>10%</td>
<td>20%</td>
</tr>
<tr>
<td>CPI</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
</tr>
</tbody>
</table>
<p>A. 100
B. 200
C. 400
D. 600</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：C</strong></p>
<ol>
<li>计算程序的平均 CPI：
$$\text{平均CPI} = 0.5 \times 2 + 0.2 \times 3 + 0.1 \times 4 + 0.2 \times 5 = 1 + 0.6 + 0.4 + 1 = 3$$</li>
<li>换算主频为 MHz：$1.2\text{ GHz} = 1200\text{ MHz}$。</li>
<li>利用简化公式求解 MIPS：
$$\text{MIPS} = \frac{1200}{3} = 400$$</li>
</ol>
<hr />
<h4>第 11 题</h4>
<p>假定基准程序 A 在某计算机上的运行时间为 $100\text{ 秒}$，其中 $90\text{ 秒}$ 为 CPU 时间，其余为 I/O 时间。若 CPU 速度提高 50%，I/O 速度不变，则运行基准程序 A 所耗费的时间是_______。</p>
<p>A. $55\text{ 秒}$
B. $60\text{ 秒}$
C. $65\text{ 秒}$
D. $70\text{ 秒}$</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ol>
<li>原程序中的非 CPU 时间（即等待 I/O 时间）为：$100\text{s} - 90\text{s} = 10\text{s}$。</li>
<li>若 CPU 速度提升 50%，代表新型处理器的运行效率是旧款的 $1.5$ 倍，所以新的 CPU 时间为：
$$T'_{\text{CPU}} = \frac{90\text{s}}{1.5} = 60\text{s}$$</li>
<li>因为 I/O 速度不变，总时间等于新 CPU 时间与不变的 I/O 时间之和：
$$T'_{\text{总}} = 60\text{s} + 10\text{s} = 70\text{s}$$</li>
</ol>
<hr />
<h4>第 12 题</h4>
<p>程序 P 在机器 M 上的执行时间为 $20\text{ 秒}$，编译优化后，P 执行的指令数减少到原来的 70%，而 CPI 增加到原来的 1.2 倍，则 P 在 M 上的执行时间是_______。</p>
<p>A. $8.4\text{ 秒}$
B. $11.7\text{ 秒}$
C. $14.0\text{ 秒}$
D. $16.8\text{ 秒}$</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：D</strong></p>
<ol>
<li>CPU 执行时间公式为 $T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f}$。在机器 M 的主频 $f$ 保持不变的情况下：
$$T'<em>{\text{CPU}} = T</em>{\text{CPU}} \times 70% \times 1.2 = T_{\text{CPU}} \times 0.84$$</li>
<li>代入原时间数据：
$$T'_{\text{CPU}} = 20\text{s} \times 0.84 = 16.8\text{s}$$</li>
</ol>
<hr />
<h4>第 13 题</h4>
<p>假定计算机 M1 和 M2 具有相同的指令集体系结构，主频分别为 $1.5\text{ GHz}$ 和 $1.2\text{ GHz}$。在 M1 和 M2 上运行某基准程序 P，平均 CPI 分别为 2 和 1，则程序 P 在 M1 和 M2 上运行时间的比值是_______。</p>
<p>A. 0.4
B. 0.625
C. 1.6
D. 2.5</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：C</strong></p>
<ol>
<li>由于 M1 和 M2 的指令集体系结构相同，且运行相同的二进制可执行程序 P，因此指令总数 $IC$ 相同。</li>
<li>根据执行时间公式：
$$\frac{T_1}{T_2} = \frac{IC \times \text{CPI}_1 / f_1}{IC \times \text{CPI}_2 / f_2} = \frac{\text{CPI}_1 \times f_2}{\text{CPI}_2 \times f_1} = \frac{2 \times 1.2\text{GHz}}{1 \times 1.5\text{GHz}} = \frac{2.4}{1.5} = 1.6$$</li>
</ol>
<hr />
<h4>第 14 题</h4>
<p>若某计算机主频为 $1\text{ GHz}$，在该计算机上执行程序 P，共执行了 $10000$ 条指令，其中 $80%$ 的指令执行平均需 1 个时钟周期，$20%$ 的指令执行平均需 10 个时钟周期。程序 P 的平均 CPI 和 CPU 执行时间分别是_______。</p>
<p>A. $2.8,\ 28\mu\text{s}$
B. $28,\ 28\mu\text{s}$
C. $2.8,\ 28\text{ ms}$
D. $28,\ 28\text{ ms}$</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：A</strong></p>
<ol>
<li>计算程序的平均 CPI：
$$\text{平均CPI} = 0.8 \times 1 + 0.2 \times 10 = 2.8$$</li>
<li>计算 CPU 执行时间：
$$T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f} = \frac{10000 \times 2.8}{1 \times 10^9\text{ Hz}} = \frac{2.8 \times 10^4}{1 \times 10^9\text{ Hz}} = 2.8 \times 10^{-5}\text{ s} = 28 \times 10^{-6}\text{ s} = 28\mu\text{s}$$</li>
</ol>
<hr />
<h4>第 15 题</h4>
<p>若机器 M 的主频为 $1.5\text{ GHz}$，在 M 上执行程序 P 的指令条数为 $5 \times 10^5$，P 的平均 CPI 为 1.2，则 P 在 M 上的指令执行速度和用户 CPU 时间分别为_______。</p>
<p>A. $0.8\text{ GIPS},\ 0.4\text{ ms}$
B. $0.8\text{ GIPS},\ 0.4\mu\text{s}$
C. $1.25\text{ GIPS},\ 0.4\text{ ms}$
D. $1.25\text{ GIPS},\ 0.4\mu\text{s}$</p>
<p><strong>解析</strong>：</p>
<p><strong>答案：C</strong></p>
<ol>
<li>计算指令执行速度（采用 GIPS 指标）：
$$\text{速度} = \frac{f}{\text{CPI} \times 10^9} = \frac{1.5 \times 10^9\text{ Hz}}{1.2 \times 10^9} = 1.25\text{GIPS}$$</li>
<li>计算用户 CPU 执行时间：
$$T_{\text{CPU}} = \frac{IC \times \text{CPI}}{f} = \frac{5 \times 10^5 \times 1.2}{1.5 \times 10^9\text{ Hz}} = \frac{6 \times 10^5}{1.5 \times 10^9\text{ Hz}} = 4 \times 10^{-4}\text{ s} = 0.4\text{ ms}$$</li>
</ol>
<hr />
<h3>1.3 简答题：冯·诺依曼结构计算机思想与硬件组成</h3>
<p>冯·诺依曼结构计算机的基本思想是什么？按此思想设计的计算机硬件系统应由哪些部件组成？它们各有何作用？</p>
<p><strong>解析</strong>：</p>
<p><strong>一、 冯·诺依曼结构核心思想</strong></p>
<ol>
<li><strong>采用二进制</strong>：计算机内部的指令和数据均使用二进制进行编码表示。</li>
<li><strong>存储程序</strong>：程序（指令序列）和数据提前一并存入主存储器中。</li>
<li><strong>形式无差别</strong>：指令和数据统一存放在主存内，没有物理上的形态区别，只有在 CPU 的不同工作阶段中才被折射出不同含义。</li>
<li><strong>按地址顺序执行</strong>：指令以地址形式存放在存储器中，由程序计数器 PC 自动加 1 指示下一条指令，实现程序的顺序自动控制。</li>
</ol>
<p><strong>二、 硬件系统五大部件及作用</strong>
按此设计思想，硬件系统由以下五大核心部分组成：</p>
<ul>
<li><strong>运算器</strong>：负责执行定点和浮点算术运算和逻辑运算（如与、或、非、移位等）。</li>
<li><strong>控制器</strong>：负责控制全机自动协调运行。主要负责将指令从存储器读出并译码，产生相应的一系列时序微操作控制信号。</li>
<li><strong>存储器</strong>：用于保存正在运行的系统和用户的程序指令、以及待处理的运算数据。</li>
<li><strong>输入设备</strong>：用于将用户或外部输入的物理信号转化为计算机可识别的二进制电信号。</li>
<li><strong>输出设备</strong>：用于将计算机的处理结果转换为人类或外部物理设备能识别的数字、文本或声光信号。</li>
</ul>
<hr />
<h3>1.4 简答题：计算机系统的多级层次结构</h3>
<p>计算机系统从功能上可划分为哪些层次？各层次在计算机系统中起什么作用？</p>
<p><strong>解析</strong>：</p>
<p>计算机系统在功能上采用<strong>多级层次结构</strong>，通过由低到高的抽象层次完成高级任务到底层物理开关状态的转换：</p>
<ol>
<li><strong>微程序/微代码级（第 1 层）</strong>：由微代码或基本逻辑门硬件电路直接解释执行。它的作用是为上层指令集系统提供最基本的硬件执行环境。</li>
<li><strong>机器语言级（第 2 层）</strong>：该层由物理 CPU 能直接理解和解释执行的二进制指令组成，构成了软硬件直接交互的界面。</li>
<li><strong>操作系统级（第 3 层）</strong>：负责直接管理底层硬件资产（如 CPU 周期、物理内存、外设），并提供一系列系统调用以供上层编写的程序使用。</li>
<li><strong>汇编语言级（第 4 层）</strong>：采用助记符代替机器码，使程序员能够进行底层的效率优化，需要由汇编器转换为机器代码才能运行。</li>
<li><strong>高级语言级（第 5 层）</strong>：采用类似自然语言与数学公式的语法（如 C、C++、Python），使开发脱离了对特定芯片结构的依赖，显著提升了编程生产力与代码的可移植性。</li>
<li><strong>应用软件级（第 6 层）</strong>：直接面向最终终端用户，利用高级软件或脚本实现各种生产力任务与特定业务问题。</li>
</ol>
<p>在系统的层次结构中，<strong>下层是实现上层的物质技术基础，而上层则是对下层功能的进一步抽象与拓展</strong>。</p>
<hr />
<h3>1.5 简答题：存储器寄存器位数的实际计算</h3>
<p>设有一个 1MB 容量的存储器，机器字长和存储字长均为 32 位，请问若按字节编址，地址寄存器是多少位？数据寄存器是多少位？</p>
<p><strong>解析</strong>：</p>
<ol>
<li><strong>地址寄存器位数的计算</strong>：
<ul>
<li>存储器容量为 1MB，且按字节编址，说明主存共包含 $1\text{M} = 2^{20}$ 个可寻址的字节单元。</li>
<li>寻址这些单元需要 20 根地址线，因此地址寄存器的宽度为 20 位。</li>
</ul>
</li>
<li><strong>数据寄存器位数的计算</strong>：
<ul>
<li>数据寄存器的宽度由存储字长决定。</li>
<li>题目中存储字长为 32 位，因此数据寄存器的宽度为 32 位。</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>考点归纳</strong>：
本题考查地址寄存器与数据寄存器宽度的确定方法。地址寄存器的位数等于主存地址码的长度，数据寄存器的位数等于存储字长。这与机器字长的大小无关。</p>
</blockquote>
<hr />
<h3>1.6 计算题：不同实现方式下的 MIPS 比较</h3>
<p>假定某计算机 1 和计算机 2 以不同的方式实现了相同的指令集。该指令集中共有 A、B、C、D 四类指令，它们所占比例分别为：</p>
<ul>
<li>A: 40%</li>
<li>B: 20%</li>
<li>C: 15%</li>
<li>D: 25%</li>
</ul>
<p>两台计算机的主频分别为 $600\text{ MHz}$ 和 $800\text{ MHz}$。各类指令在两台计算机上的 CPI 如下表所示，求两计算机的 MIPS。</p>
<table>
<thead>
<tr>
<th>计算机</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
<tr>
<td>计算机 1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
</tr>
<tr>
<td>计算机 2</td>
<td>2</td>
<td>2</td>
<td>3</td>
<td>4</td>
</tr>
</tbody>
</table>
<p><strong>解析</strong>：</p>
<p><strong>1. 计算机 1 的计算</strong></p>
<ul>
<li>计算平均 CPI：
$$\text{CPI}_1 = 0.4 \times 2 + 0.2 \times 3 + 0.15 \times 4 + 0.25 \times 5 = 0.8 + 0.6 + 0.6 + 1.25 = 3.25$$</li>
<li>由于主频 $f_1 = 600\text{ MHz}$，直接计算其 MIPS 指标：
$$\text{MIPS}_1 = \frac{f_1}{\text{CPI}_1} = \frac{600}{3.25} \approx 184.62$$</li>
</ul>
<p><strong>2. 计算机 2 的计算</strong></p>
<ul>
<li>计算平均 CPI：
$$\text{CPI}_2 = 0.4 \times 2 + 0.2 \times 2 + 0.15 \times 3 + 0.25 \times 4 = 0.8 + 0.4 + 0.45 + 1.0 = 2.65$$</li>
<li>由于主频 $f_2 = 800\text{ MHz}$，直接计算其 MIPS 指标：
$$\text{MIPS}_2 = \frac{f_2}{\text{CPI}_2} = \frac{800}{2.65} \approx 301.89$$</li>
</ul>
<p><strong>答案</strong>：</p>
<ul>
<li>计算机 1 的运算速度约为 $184.62\text{ MIPS}$；</li>
<li>计算机 2 的运算速度约为 $301.89\text{ MIPS}$。</li>
</ul>
<hr />
<h3>1.7 计算题：编译优化与性能综合对比</h3>
<p>若某程序编译后生成的目标代码由 A、B、C、D 四类指令组成，它们在程序中所占比例分别为：</p>
<ul>
<li>A: 40%</li>
<li>B: 20%</li>
<li>C: 15%</li>
<li>D: 25%</li>
</ul>
<p>已知 A、B、C、D 四类指令的 CPI 分别为：1、2、2、2。现在需要对程序进行编译优化，优化后的程序中 A 类指令数量减少了一半，而其他指令数量不变。运行该程序的计算机 CPU 主频为 $500\text{ MHz}$。
回答：</p>
<ol>
<li>优化前后程序的 CPI 各为多少？</li>
<li>优化前后程序的 MIPS 各为多少？</li>
<li>通过上面的计算结果，能得出什么结论？</li>
</ol>
<p><strong>解析</strong>：</p>
<p><strong>一、 优化前后程序的 CPI 各为多少？</strong></p>
<ol>
<li><strong>优化前平均 CPI</strong>：
$$\text{CPI}_{\text{前}} = 0.4 \times 1 + 0.2 \times 2 + 0.15 \times 2 + 0.25 \times 2 = 0.4 + 0.4 + 0.3 + 0.5 = 1.6$$</li>
<li><strong>优化后平均 CPI</strong>：
<ul>
<li>设优化前指令总数为 $N$，则各指令条数为：$A = 0.4N$, $B = 0.2N$, $C = 0.15N$, $D = 0.25N$。</li>
<li>优化后 A 类指令减少一半，变为 $0.2N$，其余指令数不变。因此优化后的指令总数变为：
$$N_{\text{后}} = 0.2N + 0.2N + 0.15N + 0.25N = 0.8N$$</li>
<li>优化后各类指令占比更新为：
<ul>
<li>A 类：$\frac{0.2N}{0.8N} = 25%$</li>
<li>B 类：$\frac{0.2N}{0.8N} = 25%$</li>
<li>C 类：$\frac{0.15N}{0.8N} = 18.75%$</li>
<li>D 类：$\frac{0.25N}{0.8N} = 31.25%$</li>
</ul>
</li>
<li>因此，优化后的平均 CPI 为：
$$\text{CPI}_{\text{后}} = 0.25 \times 1 + 0.25 \times 2 + 0.1875 \times 2 + 0.3125 \times 2 = 0.25 + 0.5 + 0.375 + 0.625 = 1.75$$</li>
</ul>
</li>
</ol>
<p><strong>二、 优化前后程序的 MIPS 各为多少？</strong>
计算机主频为 $f = 500\text{ MHz}$：</p>
<ol>
<li><strong>优化前 MIPS</strong>：
$$\text{MIPS}<em>{\text{前}} = \frac{f}{\text{CPI}</em>{\text{前}}} = \frac{500}{1.6} = 312.5$$</li>
<li><strong>优化后 MIPS</strong>：
$$\text{MIPS}<em>{\text{后}} = \frac{f}{\text{CPI}</em>{\text{后}}} = \frac{500}{1.75} \approx 285.71$$</li>
</ol>
<p><strong>三、 结论与性能分析</strong>
虽然从 MIPS 计算结果上看，优化后的指令速度从 $312.5\text{ MIPS}$ 下降到了 $285.71\text{ MIPS}$，但这并不意味着优化后程序的性能下降了。
计算优化前后的 CPU 执行时间如下：</p>
<ul>
<li>优化前执行时间：
$$T_{\text{前}} = \frac{IC_{\text{前}} \times \text{CPI}_{\text{前}}}{f} = \frac{N \times 1.6}{500\text{ MHz}} = \frac{1.6N}{5 \times 10^8\text{ Hz}} = 3.2N \times 10^{-9}\text{s}$$</li>
<li>优化后执行时间：
$$T_{\text{后}} = \frac{IC_{\text{后}} \times \text{CPI}_{\text{后}}}{f} = \frac{0.8N \times 1.75}{500\text{ MHz}} = \frac{1.4N}{500\text{ MHz}} = \frac{1.4N}{5 \times 10^8\text{ Hz}} = 2.8N \times 10^{-9}\text{s}$$</li>
<li>计算性能提升（加速比）：
$$\text{加速比} = \frac{T_{\text{前}}}{T_{\text{后}}} = \frac{1.6N}{1.4N} \approx 1.1429$$
这说明优化后的程序执行速度实际上提升了约 $14.29%$。</li>
</ul>
<p><strong>结论</strong>：</p>
<ol>
<li><strong>评价程序运行速度最真实可靠的指标是 CPU 执行时间</strong>。而 CPI、MIPS、主频等指标无法跨场景进行单一维度的绝对性能对比。</li>
<li>在编译优化中，消除了大量的低 CPI 指令（如 A类）后，虽然使得剩余指令结构中高 CPI 的指令比例上升，从而导致平均 CPI 增加、MIPS 下降；但由于程序总指令数显著减少，总的时钟周期数大幅下降，从而令程序的实际执行时间变短，性能最终提升。</li>
</ol>
]]></content>
    <author><name>黎明</name></author>
    <category term="计算机组成原理"/>
  </entry>
  <entry>
    <title>概率论与数理统计常考公式汇总</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%B8%B8%E8%80%83%E5%85%AC%E5%BC%8F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%B8%B8%E8%80%83%E5%85%AC%E5%BC%8F/</id>
    <published>2026-06-25T10:33:06.000Z</published>
    <updated>2026-06-25T10:33:06.000Z</updated>
    <summary>汇总概率论与数理统计中关于数学期望、方差、协方差、相关系数、常用统计量以及重要抽样分布等常考公式，提供记忆口诀与速查表。</summary>
    <content type="html"><![CDATA[<p>在概率论与数理统计的学习与考试中，数字特征、常用统计量以及正态总体下的抽样分布是核心考点。为了便于复习与快速记忆，本文将这些常考公式与常见分布特征进行系统梳理与汇总。</p>
<hr />
<h2>一、核心公式速查与记忆口诀</h2>
<h3>1. 期望、方差、协方差、相关系数、和差公式记忆表</h3>
<table>
<thead>
<tr>
<th>项目</th>
<th>公式</th>
<th>记忆方法</th>
</tr>
</thead>
<tbody>
<tr>
<td>$E(X)$</td>
<td>$\sum x_i p_i$</td>
<td>每个取值乘以对应概率，再相加</td>
</tr>
<tr>
<td>$E(Y)$</td>
<td>$\sum y_i p_i$</td>
<td>同理</td>
</tr>
<tr>
<td>$D(X)$</td>
<td>$E(X^2)-[E(X)]^2$</td>
<td>平方的期望减去期望的平方</td>
</tr>
<tr>
<td>$D(Y)$</td>
<td>$E(Y^2)-[E(Y)]^2$</td>
<td>同理</td>
</tr>
<tr>
<td>$\operatorname{Cov}(X,Y)$</td>
<td>$E(XY)-E(X)E(Y)$</td>
<td>乘积的期望减去期望的乘积</td>
</tr>
<tr>
<td>$\rho_{XY}$</td>
<td>$\frac{\operatorname{Cov}(X,Y)}{\sqrt{D(X)}\sqrt{D(Y)}}$</td>
<td>协方差除以各自标准差的乘积</td>
</tr>
<tr>
<td>$E(X+Y)$</td>
<td>$E(X)+E(Y)$</td>
<td>期望可直接拆分</td>
</tr>
<tr>
<td>$E(X-Y)$</td>
<td>$E(X)-E(Y)$</td>
<td>期望跟着符号走</td>
</tr>
<tr>
<td>$D(X+Y)$</td>
<td>$D(X)+D(Y)+2\operatorname{Cov}(X,Y)$</td>
<td>方差相加，协方差看符号</td>
</tr>
<tr>
<td>$D(X-Y)$</td>
<td>$D(X)+D(Y)-2\operatorname{Cov}(X,Y)$</td>
<td>减法时协方差前面是负号</td>
</tr>
</tbody>
</table>
<h3>2. 最核心的 5 个公式</h3>
<p>$$
D(X)=E(X^2)-[E(X)]^2
$$</p>
<p>$$
D(Y)=E(Y^2)-[E(Y)]^2
$$</p>
<p>$$
\operatorname{Cov}(X,Y)=E(XY)-E(X)E(Y)
$$</p>
<p>$$
\rho_{XY} = \frac{\operatorname{Cov}(X,Y)}{\sqrt{D(X)}\sqrt{D(Y)}}
$$</p>
<p>$$
D(X\pm Y)=D(X)+D(Y)\pm 2\operatorname{Cov}(X,Y)
$$</p>
<h3>3. 记忆口诀</h3>
<p><strong>期望看符号，方差都相加，协方差决定加减。</strong></p>
<p>具体而言：</p>
<p>$$
E(X\pm Y)=E(X)\pm E(Y)
$$</p>
<p>$$
D(X\pm Y)=D(X)+D(Y)\pm 2\operatorname{Cov}(X,Y)
$$</p>
<hr />
<h2>二、基本定义与计算公式</h2>
<p>设随机变量为 $X$、$Y$：</p>
<h3>1. 数学期望</h3>
<h4>离散型</h4>
<p>$$
E(X)=\sum x_i p_i, \quad E(Y)=\sum y_i p_i
$$</p>
<h4>连续型</h4>
<p>$$
E(X)=\int_{-\infty}^{+\infty} x f_X(x),dx, \quad E(Y)=\int_{-\infty}^{+\infty} y f_Y(y),dy
$$</p>
<h3>2. 方差</h3>
<p>$$
D(X)=E[(X-E(X))^2], \quad D(Y)=E[(Y-E(Y))^2]
$$</p>
<h4>常用计算公式（黄金公式）</h4>
<p>$$
D(X)=E(X^2)-[E(X)]^2, \quad D(Y)=E(Y^2)-[E(Y)]^2
$$</p>
<h3>3. 协方差</h3>
<p>$$
\operatorname{Cov}(X,Y)=E[(X-E(X))(Y-E(Y))]
$$</p>
<h4>常用计算公式</h4>
<p>$$
\operatorname{Cov}(X,Y)=E(XY)-E(X)E(Y)
$$</p>
<h3>4. 相关系数 $\rho_{XY}$</h3>
<h4>定义式</h4>
<p>$$
\rho_{XY} = \frac{\operatorname{Cov}(X,Y)}{\sqrt{D(X)}\sqrt{D(Y)}}
$$</p>
<h4>核心性质</h4>
<ol>
<li><strong>取值范围</strong>：$-1 \le \rho_{XY} \le 1$。</li>
<li><strong>不相关</strong>：$\rho_{XY} = 0 \iff \operatorname{Cov}(X,Y) = 0$，表示两者不存在线性相关关系。</li>
<li><strong>完全线性相关</strong>：$|\rho_{XY}| = 1$。当且仅当存在常数 $a, b \ (a \ne 0)$ 使得 $P(Y = aX + b) = 1$。
<ul>
<li>$\rho_{XY} = 1$ 时，$a &gt; 0$（完全正线性相关）；</li>
<li>$\rho_{XY} = -1$ 时，$a &lt; 0$（完全负线性相关）。</li>
</ul>
</li>
</ol>
<h3>5. 离散型二维分布常用写法</h3>
<p>如果已知联合概率 $P(X=x_i,Y=y_j)=p_{ij}$，则对应的期望和协方差计算式为：</p>
<p>$$
E(X)=\sum_i \sum_j x_i p_{ij}, \quad E(Y)=\sum_i \sum_j y_j p_{ij}
$$</p>
<p>$$
E(XY)=\sum_i \sum_j x_i y_j p_{ij}
$$</p>
<p>$$
\operatorname{Cov}(X,Y)=\sum_i \sum_j x_i y_j p_{ij}-E(X)E(Y)
$$</p>
<hr />
<h2>三、和差性质与独立性讨论</h2>
<h3>1. 和差性质</h3>
<p>数学期望满足线性性质：
$$
E(X\pm Y)=E(X)\pm E(Y)
$$
<em>(注意：数学期望的线性性质对任意随机变量均成立，不需要 $X$ 和 $Y$ 相互独立。)</em></p>
<p>方差的和差计算式：
$$
D(X\pm Y)=D(X)+D(Y)\pm 2\operatorname{Cov}(X,Y)
$$</p>
<h3>2. 独立时的特殊情况</h3>
<p>如果随机变量 $X$ 和 $Y$ 相互独立，由于此时 $\operatorname{Cov}(X,Y)=0, \rho_{XY} = 0$，上述公式化简为：</p>
<p>$$
D(X+Y)=D(X)+D(Y)
$$
$$
D(X-Y)=D(X)+D(Y)
$$</p>
<p>注意：<strong>即使是 $X-Y$，在独立时其方差项之间也是加号，决不能写成减号。</strong></p>
<hr />
<h2>四、常见分布特征速查表</h2>
<h3>1. 常见离散型分布</h3>
<table>
<thead>
<tr>
<th>分布名称</th>
<th>记号</th>
<th>分布律 $P(X = k)$</th>
<th>数学期望 $E(X)$</th>
<th>方差 $D(X)$</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>0-1 分布</strong></td>
<td>-</td>
<td>$p^k (1-p)^{1-k}, \quad k = 0, 1$</td>
<td>$p$</td>
<td>$p(1-p)$</td>
</tr>
<tr>
<td><strong>二项分布</strong></td>
<td>$X \sim B(n, p)$</td>
<td>$C_n^k p^k (1-p)^{n-k}, \quad k = 0, 1, \dots, n$</td>
<td>$np$</td>
<td>$np(1-p)$</td>
</tr>
<tr>
<td><strong>泊松分布</strong></td>
<td>$X \sim P(\lambda)$</td>
<td>$\dfrac{\lambda^k e^{-\lambda}}{k!}, \quad k = 0, 1, 2, \dots$</td>
<td>$\lambda$</td>
<td>$\lambda$</td>
</tr>
<tr>
<td><strong>几何分布</strong></td>
<td>-</td>
<td>$(1-p)^{k-1} p, \quad k = 1, 2, \dots$</td>
<td>$\dfrac{1}{p}$</td>
<td>$\dfrac{1-p}{p^2}$</td>
</tr>
</tbody>
</table>
<p><em>(注：其中 $0 &lt; p &lt; 1$，$n$ 为正整数，$\lambda &gt; 0$)</em></p>
<hr />
<h3>2. 常见连续型分布</h3>
<table>
<thead>
<tr>
<th>分布名称</th>
<th>记号</th>
<th>概率密度函数 $f(x)$</th>
<th>数学期望 $E(X)$</th>
<th>方差 $D(X)$</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>均匀分布</strong></td>
<td>$X \sim U(a, b)$</td>
<td>$\begin{cases} \dfrac{1}{b-a}, &amp; a \le x \le b \ 0, &amp; \text{其他} \end{cases}$</td>
<td>$\dfrac{a+b}{2}$</td>
<td>$\dfrac{(b-a)^2}{12}$</td>
</tr>
<tr>
<td><strong>指数分布</strong></td>
<td>$X \sim E(\lambda)$</td>
<td>$\begin{cases} \lambda e^{-\lambda x}, &amp; x \ge 0 \ 0, &amp; x &lt; 0 \end{cases}$</td>
<td>$\dfrac{1}{\lambda}$</td>
<td>$\dfrac{1}{\lambda^2}$</td>
</tr>
<tr>
<td><strong>正态分布</strong></td>
<td>$X \sim N(\mu, \sigma^2)$</td>
<td>$\dfrac{1}{\sqrt{2\pi}\sigma} e^{-\frac{(x-\mu)^2}{2\sigma^2}}$</td>
<td>$\mu$</td>
<td>$\sigma^2$</td>
</tr>
</tbody>
</table>
<p><em>(注：其中 $\lambda &gt; 0$，$\sigma &gt; 0$)</em></p>
<hr />
<h2>五、数理统计常用统计量</h2>
<h3>1. 统计量的定义</h3>
<p>统计量是只由样本 $X_1, X_2, \dots, X_n$ 构成的函数，不含任何未知参数。</p>
<h3>2. 常用统计量计算公式与性质</h3>
<table>
<thead>
<tr>
<th>统计量名称</th>
<th>记号与计算公式</th>
<th>数学期望</th>
<th>方差</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>样本均值</strong></td>
<td>$\bar X = \dfrac{1}{n}\sum_{i=1}^n X_i$</td>
<td>$E(\bar X) = \mu$</td>
<td>$D(\bar X) = \dfrac{\sigma^2}{n}$</td>
</tr>
<tr>
<td><strong>样本方差</strong></td>
<td>$S^2 = \dfrac{1}{n-1}\sum_{i=1}^n (X_i-\bar X)^2 = \dfrac{1}{n-1}\left(\sum_{i=1}^n X_i^2-n\bar X^2\right)$</td>
<td>$E(S^2) = \sigma^2$</td>
<td>-</td>
</tr>
<tr>
<td><strong>样本比例</strong></td>
<td>$\hat p = \dfrac{X}{n}$ (成功次数 $X$)</td>
<td>$E(\hat p) = p$</td>
<td>$D(\hat p) = \dfrac{p(1-p)}{n}$</td>
</tr>
</tbody>
</table>
<p><em>(记忆法：样本均值的期望不变，方差除以 $n$；样本方差用 $n-1$，总体方差用 $n$。)</em></p>
<hr />
<h2>六、抽样分布常用公式</h2>
<h3>1. 常见抽样分布表</h3>
<table>
<thead>
<tr>
<th>分布类型</th>
<th>适用条件/样本特征</th>
<th>统计量与构造公式</th>
<th>抽样分布/渐近分布</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>均值的抽样分布</strong></td>
<td>总体 $X \sim N(\mu, \sigma^2)$</td>
<td>$Z = \dfrac{\bar X - \mu}{\sigma / \sqrt{n}}$</td>
<td>$N(0, 1)$</td>
</tr>
<tr>
<td><strong>中心极限定理</strong></td>
<td>$n$ 较大，任意总体（均值 $\mu$，方差 $\sigma^2$）</td>
<td>$Z = \dfrac{\bar X - \mu}{\sigma / \sqrt{n}}$</td>
<td>近似服从 $N(0, 1)$</td>
</tr>
<tr>
<td><strong>比例的抽样分布</strong></td>
<td>$n$ 较大，二项总体 $B(1, p)$</td>
<td>$Z = \dfrac{\hat p - p}{\sqrt{\dfrac{p(1-p)}{n}}}$</td>
<td>近似服从 $N(0, 1)$</td>
</tr>
<tr>
<td><strong>卡方分布</strong></td>
<td>$X_1, \dots, X_n \sim N(\mu, \sigma^2)$ 独立同分布</td>
<td>$\chi^2 = \dfrac{(n-1)S^2}{\sigma^2}$</td>
<td>$\chi^2(n-1)$</td>
</tr>
<tr>
<td><strong>t 分布</strong></td>
<td>$X_1, \dots, X_n \sim N(\mu, \sigma^2)$，$\sigma^2$ 未知</td>
<td>$T = \dfrac{\bar X - \mu}{S / \sqrt{n}}$</td>
<td>$t(n-1)$</td>
</tr>
<tr>
<td><strong>F 分布</strong></td>
<td>双正态总体，方差分别为 $\sigma_1^2, \sigma_2^2$</td>
<td>$F = \dfrac{S_1^2 / \sigma_1^2}{S_2^2 / \sigma_2^2}$</td>
<td>$F(n_1-1, n_2-1)$</td>
</tr>
<tr>
<td><strong>F 分布</strong>（方差相等）</td>
<td>双正态总体，且 $\sigma_1^2 = \sigma_2^2$</td>
<td>$F = \dfrac{S_1^2}{S_2^2}$</td>
<td>$F(n_1-1, n_2-1)$</td>
</tr>
</tbody>
</table>
<p><em>(注：其中 $S^2$ 为样本方差，$S$ 为样本标准差，$n$ 为样本容量)</em></p>
<h3>2. 核心记忆口诀</h3>
<p><strong>均值找正态，方差找卡方；方差比找 F，未知方差找 t。</strong></p>
<p>即映射关系为：
$$
\bar X \rightarrow N, \quad S^2 \rightarrow \chi^2, \quad \frac{S_1^2}{S_2^2} \rightarrow F, \quad \frac{\bar X - \mu}{S / \sqrt{n}} \rightarrow t
$$</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>常见概率分布</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%B8%B8%E8%A7%81%E5%88%86%E5%B8%83/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%B8%B8%E8%A7%81%E5%88%86%E5%B8%83/</id>
    <published>2026-06-24T02:45:00.000Z</published>
    <updated>2026-06-24T02:45:00.000Z</updated>
    <summary>系统整理概率论与数理统计中的各类常见概率分布，包括两点分布、二项分布、几何分布、超几何分布与泊松分布五大离散型分布，均匀分布、指数分布与正态分布三大连续型分布，以及卡方、t、F三大抽样分布。</summary>
    <content type="html"><![CDATA[<p>本文主要对概率论与数理统计里常见的<strong>各类概率分布</strong>进行系统的梳理。在学习这一章时，核心思路可以总结为：</p>
<p>$$
\boxed{\text{先明确随机变量是离散型还是连续型，再牢记每种分布所适合描述的实际场景}}
$$</p>
<hr />
<h2>1. 概率分布的总分类</h2>
<p>随机变量的概率分布主要可以划分为以下三大类：</p>
<table>
<thead>
<tr>
<th>分布类别</th>
<th>核心数学特点</th>
<th>常见典型分布</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>离散型分布</strong></td>
<td>随机变量的可能取值是有限个或可列无限个</td>
<td>两点分布、二项分布、几何分布、超几何分布、泊松分布</td>
</tr>
<tr>
<td><strong>连续型分布</strong></td>
<td>随机变量可以在某一个或多个实数区间内连续取值</td>
<td>均匀分布、指数分布、正态分布</td>
</tr>
<tr>
<td><strong>抽样分布</strong></td>
<td>数理统计中用于推断未知参数的统计量所服从的分布</td>
<td>卡方分布、学生分布（$t$ 分布）、$F$ 分布</td>
</tr>
</tbody>
</table>
<hr />
<h2>2. 常见离散型分布</h2>
<h3>2.1 两点分布 / 伯努利分布</h3>
<p>若随机变量 $X$ 只能取两个值 $0$ 和 $1$，且其发生的概率分别为：</p>
<p>$$
P(X = 1) = p, \quad P(X = 0) = 1 - p \quad (0 &lt; p &lt; 1)
$$</p>
<p>则称 $X$ 服从两点分布，也称为伯努利分布。</p>
<p>记作：</p>
<p>$$
X \sim B(1, p)
$$</p>
<p>其概率质量函数（分布律）可以统一表示为：</p>
<p>$$
P(X = x) = p^x(1 - p)^{1 - x}, \quad x = 0, 1
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = p
$$</p>
<p>$$
D(X) = p(1 - p)
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>一次射击试验：命中（1）或未命中（0）；</li>
<li>一次产品质量抽查：合格（1）或不合格（0）；</li>
<li>一次选举投票：支持（1）或不支持（0）；</li>
<li>一次科学试验：成功（1）或失败（0）。</li>
</ul>
<p>简明规律：</p>
<p>$$
\boxed{\text{两点分布描述的是单次试验中成败这一最基础的随机事件}}
$$</p>
<hr />
<h3>2.2 二项分布</h3>
<p>若进行 $n$ 次独立重复的伯努利试验，每次试验中事件成功的概率均为 $p$。令随机变量 $X$ 表示 $n$ 次试验中成功的次数，则 $X$ 服从二项分布。</p>
<p>记作：</p>
<p>$$
X \sim B(n, p)
$$</p>
<p>其分布律公式为：</p>
<p>$$
\boxed{P(X = k) = C_n^k p^k(1 - p)^{n - k}, \quad k = 0, 1, 2, \dots, n}
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = np
$$</p>
<p>$$
D(X) = np(1 - p)
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>独立重复射击 $n$ 次，射中目标的次数；</li>
<li>重复抛掷一枚硬币 $n$ 次，正面朝上的次数；</li>
<li>从大批工业产品中随机抽检 $n$ 件，其中所包含的次品个数；</li>
<li>独立作答 $n$ 道单选题，最终答对的题目数。</li>
</ul>
<h4>计算示例</h4>
<p>假设某射击运动员每次射击的命中率均为 0.8。现独立射击 5 次，令随机变量 $X$ 表示命中的次数，则 $X \sim B(5, 0.8)$。</p>
<p>其恰好命中 3 次的概率计算为：</p>
<p>$$
P(X = 3) = C_5^3 (0.8)^3 (0.2)^2 = 10 \cdot 0.512 \cdot 0.04 = 0.2048
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{二项分布描述的是 } n \text{ 次独立重复试验中成功发生的总次数}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Binomial_distribution_pmf.svg/1280px-Binomial_distribution_pmf.svg.png" alt="二项分布概率质量分布图" /></p>
<hr />
<h3>2.3 几何分布</h3>
<p>若每次试验成功的概率为 $p$，在独立重复试验中，令随机变量 $X$ 表示<strong>为了获得首次成功所需要的累计试验次数</strong>，则 $X$ 服从几何分布。</p>
<p>其分布律公式为：</p>
<p>$$
\boxed{P(X = k) = (1 - p)^{k - 1}p, \quad k = 1, 2, 3, \dots}
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = \frac{1}{p}
$$</p>
<p>$$
D(X) = \frac{1 - p}{p^2}
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>重复射击，直到第几次射击才出现首次命中；</li>
<li>重复参与抽奖，直到第几次才首次中奖；</li>
<li>连续质检产品，直到第几件才首次发现次品。</li>
</ul>
<h4>计算示例</h4>
<p>某人投篮的命中率为 0.6。问其直到第 3 次投篮才首次投中的概率。</p>
<p>根据几何分布定义，前 2 次必须未投中，第 3 次投中：</p>
<p>$$
P(X = 3) = (1 - 0.6)^{3 - 1} \cdot 0.6 = (0.4)^2 \cdot 0.6 = 0.096
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{几何分布描述的是在独立重复试验中，首次成功出现时的等待次数}}
$$</p>
<ul>
<li>注意：部分国外教材或特定考题将 $X$ 定义为“首次成功前所失败的次数”，此时随机变量的取值范围为 $k = 0, 1, 2, \dots$，其分布律公式会变更为 $P(X = k) = (1 - p)^k p$。在审题时务必看清变量的具体定义。</li>
</ul>
<hr />
<h3>2.4 超几何分布</h3>
<p>超几何分布用于描述在<strong>不放回抽样</strong>条件下的概率问题。</p>
<p>设总体共包含 $N$ 个个体，其中有 $M$ 个属于成功类，剩下的 $N - M$ 个属于失败类。现从中不放回地随机抽取 $n$ 个个体，令随机变量 $X$ 表示抽出的个体中属于成功类的个数，则 $X$ 服从超几何分布。</p>
<p>其分布律公式为：</p>
<p>$$
\boxed{P(X = k) = \frac{C_M^k C_{N - M}^{n - k}}{C_N^n}}
$$</p>
<p>其取值的合理范围为 $\max(0, n - N + M) \le k \le \min(n, M)$。</p>
<p>数学期望与方差为：</p>
<p>$$
E(X) = n\frac{M}{N}
$$</p>
<p>$$
D(X) = n\frac{M}{N}\left(1 - \frac{M}{N}\right)\frac{N - n}{N - 1}
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>一箱共 $N$ 件产品中含有 $M$ 件次品，不放回地随机抽出 $n$ 件，求抽到的次品个数；</li>
<li>从装有红球和白球的袋子中，不放回地摸出若干个球，求摸出红球的个数；</li>
<li>从包含男女生的班级中，不放回地随机抽取若干人，求其中女生的人数。</li>
</ul>
<h4>计算示例</h4>
<p>一批共 20 件产品中含有 5 件次品。现从中不放回地随机抽取 4 件，求恰好抽到 2 件次品的概率。</p>
<p>$$
P(X = 2) = \frac{C_5^2 C_{15}^2}{C_{20}^4}
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{超几何分布描述的是在有限总体中进行不放回抽样时，抽中特定类别的个数}}
$$</p>
<hr />
<h3>2.5 泊松分布</h3>
<p>如果随机变量 $X$ 表示在某段特定的时间、空间或区域内，某随机事件发生的次数，且该事件平均发生的次数为 $\lambda$，则 $X$ 服从泊松分布。</p>
<p>记作：</p>
<p>$$
X \sim P(\lambda)
$$</p>
<p>其分布律公式为：</p>
<p>$$
\boxed{P(X = k) = \frac{\lambda^k}{k!}e^{-\lambda}, \quad k = 0, 1, 2, \dots}
$$</p>
<p>数学期望与方差满足特征：</p>
<p>$$
E(X) = \lambda, \quad D(X) = \lambda
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>某繁忙路口在一分钟内通过的车辆总数；</li>
<li>某零售商铺在一天内迎来光顾的顾客总数；</li>
<li>某生产设备在一小时内发生故障的次数；</li>
<li>某书籍页面中印刷错误的字符数；</li>
<li>某特定地区在一天内发生交通事故的次数。</li>
</ul>
<h4>计算示例</h4>
<p>某客服热线中心平均每分钟接到 3 个来电，求其在一分钟内恰好接到 2 个来电的概率。</p>
<p>此随机发生次数服从泊松分布 $X \sim P(3)$：</p>
<p>$$
P(X = 2) = \frac{3^2}{2!}e^{-3} = 4.5 e^{-3}
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{泊松分布描述的是在连续的单位时间或区域内，稀有随机事件发生的频数}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/1/16/Poisson_pmf.svg" alt="泊松分布概率质量分布图" /></p>
<hr />
<h2>3. 常见连续型分布</h2>
<h3>3.1 均匀分布</h3>
<p>若随机变量 $X$ 在实数区间 $[a, b]$ 上取值，且落在该区间内任意等长度子区间内的概率均相等，则称 $X$ 在区间 $[a, b]$ 上服从均匀分布。</p>
<p>记作：</p>
<p>$$
X \sim U(a, b)
$$</p>
<p>其概率密度函数为：</p>
<p>$$
f(x) =
\begin{cases}
\dfrac{1}{b-a}, &amp; a &lt; x &lt; b \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>累积分布函数为：</p>
<p>$$
F(x) =
\begin{cases}
0, &amp; x &lt; a \
\dfrac{x - a}{b - a}, &amp; a \le x &lt; b \
1, &amp; x \ge b
\end{cases}
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = \frac{a + b}{2}
$$</p>
<p>$$
D(X) = \frac{(b - a)^2}{12}
$$</p>
<h4>典型适用场景</h4>
<ul>
<li>某线路公交车每 10 分钟一班，某人随机到达站台，其等待公交车的时间；</li>
<li>计算机生成的伪随机数；</li>
<li>测量仪器读数四舍五入带来的舍入误差。</li>
</ul>
<h4>计算示例</h4>
<p>已知公交车每 10 分钟发车一班，某乘客随机到达车站，其等待时间服从均匀分布 $X \sim U(0, 10)$。求该乘客等待时间不超过 3 分钟的概率。</p>
<p>$$
P(X \le 3) = \frac{3 - 0}{10 - 0} = 0.3
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{均匀分布描述的是在确定区间内，取值机会完全均等的连续型分布}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Uniform_Distribution_PDF_SVG.svg/1280px-Uniform_Distribution_PDF_SVG.svg.png" alt="均匀分布概率密度函数图" /></p>
<hr />
<h3>3.2 指数分布</h3>
<p>指数分布常用于描述设备寿命或随机服务系统中的等待时间。</p>
<p>如果随机变量服从指数分布：</p>
<p>$$
X \sim E(\lambda) \quad (\lambda &gt; 0)
$$</p>
<p>其概率密度函数为：</p>
<p>$$
f(x) =
\begin{cases}
\lambda e^{-\lambda x}, &amp; x &gt; 0 \
0, &amp; x \le 0
\end{cases}
$$</p>
<p>其累积分布函数为：</p>
<p>$$
F(x) =
\begin{cases}
1 - e^{-\lambda x}, &amp; x &gt; 0 \
0, &amp; x \le 0
\end{cases}
$$</p>
<p>在实际计算中，我们常用其右尾生存概率公式：</p>
<p>$$
\boxed{P(X &gt; x) = e^{-\lambda x} \quad (x &gt; 0)}
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = \frac{1}{\lambda}
$$</p>
<p>$$
D(X) = \frac{1}{\lambda^2}
$$</p>
<ul>
<li>这里的参数 $\lambda$ 代表单位时间内事件发生的平均速率。</li>
</ul>
<h4>典型适用场景</h4>
<ul>
<li>某电子元器件或机械设备的物理使用寿命；</li>
<li>电话中心等待下一个呼入电话所经历的时间；</li>
<li>排队论中，等待下一位顾客到店的服务间隔时间。</li>
</ul>
<h4>计算示例</h4>
<p>某精密仪器平均使用寿命为 1000 小时。假设其寿命服从指数分布，求该仪器使用寿命超过 1500 小时的概率。</p>
<p>因为平均寿命 $E(X) = \dfrac{1}{\lambda} = 1000$，解得率参数 $\lambda = \dfrac{1}{1000}$。</p>
<p>仪器寿命超过 1500 小时的概率为：</p>
<p>$$
P(X &gt; 1500) = e^{-\frac{1500}{1000}} = e^{-1.5}
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{指数分布描述的是无记忆性的等待时间或使用寿命}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Exponential_pdf.svg/1280px-Exponential_pdf.svg.png" alt="指数分布概率密度函数图" /></p>
<h4>核心性质：无记忆性</h4>
<p>指数分布是唯一的具有<strong>无记忆性</strong>的连续型概率分布，满足如下条件：</p>
<p>$$
P(X &gt; s + t \mid X &gt; s) = P(X &gt; t) \quad (s, t &gt; 0)
$$</p>
<p>这在直观上的物理含义是：对于一个正常工作的元器件，它已经工作了 $s$ 小时这一事实，并不影响它能继续工作 $t$ 小时的概率。它在任何时刻都“宛如新生”。</p>
<hr />
<h3>3.3 正态分布</h3>
<p>正态分布是概率论与数理统计中应用最为广泛、最重要的连续型随机变量分布。</p>
<p>如果随机变量服从正态分布：</p>
<p>$$
X \sim N(\mu, \sigma^2) \quad (\sigma &gt; 0)
$$</p>
<p>其概率密度函数为经典的钟形曲线：</p>
<p>$$
\boxed{f(x) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{(x - \mu)^2}{2\sigma^2}}}
$$</p>
<p>数学期望与方差分别为：</p>
<p>$$
E(X) = \mu, \quad D(X) = \sigma^2
$$</p>
<h4>曲线的几何特征</h4>
<ul>
<li>曲线呈单峰钟形，关于直线 $x = \mu$ 左右对称；</li>
<li>参数 $\mu$ 决定了分布中心的位置；</li>
<li>参数 $\sigma$ 控制了曲线的离散波动程度。$\sigma$ 越大，曲线越扁平宽分散；$\sigma$ 越小，说明数据越集中，曲线越尖锐窄高。</li>
</ul>
<h4>典型适用场景</h4>
<ul>
<li>成年人群的身高与体重数据；</li>
<li>某次标准化考试中考生的考试成绩；</li>
<li>物理测量过程中不可避免的测量误差；</li>
<li>机械加工中零件的尺寸误差；</li>
<li>受到自然界大量独立、微小的随机因素共同叠加作用所产生的数据分布。</li>
</ul>
<p>简明规律：</p>
<p>$$
\boxed{\text{正态分布描述的是受大量微小随机独立因素共同作用后的连续分布状态}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Normal_Distribution_PDF.svg/1280px-Normal_Distribution_PDF.svg.png" alt="正态分布概率密度函数钟形曲线图" /></p>
<hr />
<h3>3.4 标准正态分布与标准化法</h3>
<p>当正态分布满足均值 $\mu = 0$，方差 $\sigma^2 = 1$ 时，特称为<strong>标准正态分布</strong>。</p>
<p>通常记为：</p>
<p>$$
Z \sim N(0, 1)
$$</p>
<p>其概率密度函数与累积分布函数分别记为：</p>
<p>$$
\varphi(x) = \frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}
$$</p>
<p>$$
\Phi(x) = P(Z \le x) = \int_{-\infty}^{x} \varphi(t),dt
$$</p>
<h4>标准化定理</h4>
<p>对于任意一般的正态分布随机变量 $X \sim N(\mu, \sigma^2)$，我们可以通过线性变换将其转化为标准正态变量：</p>
<p>$$
\boxed{Z = \frac{X - \mu}{\sigma} \sim N(0, 1)}
$$</p>
<p>这一标准化过程是求解正态分布概率的核心工具。</p>
<p>例如，计算 $X$ 落在区间 $(a, b)$ 内的概率：</p>
<p>$$
\begin{aligned}
P(a &lt; X &lt; b) &amp;= P\left(\frac{a - \mu}{\sigma} &lt; Z &lt; \frac{b - \mu}{\sigma}\right) \
&amp;= \Phi\left(\frac{b - \mu}{\sigma}\right) - \Phi\left(\frac{a - \mu}{\sigma}\right)
\end{aligned}
$$</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{求解一般正态分布概率，第一步一定是将其标准化}}
$$</p>
<hr />
<h2>4. 数理统计中的三大抽样分布</h2>
<p>数理统计中的三大抽样分布是由正态分布衍生出来的，在参数估计与假设检验中扮演着枢轴量的角色。</p>
<hr />
<h3>4.1 卡方分布</h3>
<p>设随机变量 $Z_1, Z_2, \dots, Z_n$ 相互独立，且均服从标准正态分布 $Z_i \sim N(0, 1)$。</p>
<p>则它们平方和所构成的新随机变量：</p>
<p>$$
\boxed{X = Z_1^2 + Z_2^2 + \dots + Z_n^2}
$$</p>
<p>服从自由度为 $n$ 的卡方分布，记作：</p>
<p>$$
X \sim \chi^2(n)
$$</p>
<p>其数学期望与方差分别为：</p>
<p>$$
E(X) = n
$$</p>
<p>$$
D(X) = 2n
$$</p>
<h4>核心应用定理</h4>
<p>在正态总体样本中，样本方差 $S^2$ 与总体方差 $\sigma^2$ 之间满足如下结论：</p>
<p>$$
\boxed{\frac{(n-1)S^2}{\sigma^2} \sim \chi^2(n-1)}
$$</p>
<p>这为估计和检验总体方差提供了理论依据。</p>
<p>简明规律：</p>
<p>$$
\boxed{\chi^2 \text{ 分布主要用于解决与正态总体方差相关的估算与检验问题}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Chi-square_pdf.svg/1280px-Chi-square_pdf.svg.png" alt="卡方分布概率密度函数图" /></p>
<hr />
<h3>4.2 学生分布（$t$ 分布）</h3>
<p>设随机变量 $Z \sim N(0, 1)$，随机变量 $U \sim \chi^2(n)$，且 $Z$ 与 $U$ 相互独立。</p>
<p>则新随机变量：</p>
<p>$$
\boxed{T = \frac{Z}{\sqrt{U / n}}}
$$</p>
<p>服从自由度为 $n$ 的 $t$ 分布，记作：</p>
<p>$$
T \sim t(n)
$$</p>
<h4>分布特征</h4>
<ul>
<li>曲线关于 $T = 0$ 对称，形状类似于标准正态分布，但比标准正态分布具有“更厚”的尾部；</li>
<li>当自由度 $n \to \infty$ 时，$t(n)$ 分布在极限下收敛于标准正态分布 $N(0, 1)$。</li>
</ul>
<h4>核心应用定理</h4>
<p>当正态总体方差 $\sigma^2$ 未知时，样本均值与样本标准差满足如下结论：</p>
<p>$$
\boxed{\frac{\overline{X} - \mu}{S / \sqrt{n}} \sim t(n - 1)}
$$</p>
<p>这为单总体及双总体的均值推断奠定了基础。</p>
<p>简明规律：</p>
<p>$$
\boxed{t \text{ 分布主要用于在方差未知的前提下，处理与均值相关的估算与检验}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Student_t_pdf.svg/1280px-Student_t_pdf.svg.png" alt="t分布概率密度函数图" /></p>
<hr />
<h3>4.3 $F$ 分布</h3>
<p>设随机变量 $U \sim \chi^2(m)$，随机变量 $V \sim \chi^2(n)$，且 $U$ 与 $V$ 相互独立。</p>
<p>则这两个卡方统计量除以各自自由度之后的比值：</p>
<p>$$
\boxed{F = \frac{U / m}{V / n}}
$$</p>
<p>服从第一自由度为 $m$，第二自由度为 $n$ 的 $F$ 分布，记作：</p>
<p>$$
F \sim F(m, n)
$$</p>
<h4>核心应用定理</h4>
<p>从两个独立正态总体中抽样，其样本方差之比满足结论：</p>
<p>$$
\boxed{\frac{S_1^2 / \sigma_1^2}{S_2^2 / \sigma_2^2} \sim F(n_1 - 1, n_2 - 1)}
$$</p>
<p>这可直接用于推断两个总体方差是否相等。</p>
<p>简明规律：</p>
<p>$$
\boxed{F \text{ 分布主要用于比较两个正态总体的方差大小关系}}
$$</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/7/74/F-distribution_pdf.svg" alt="F分布概率密度函数图" /></p>
<hr />
<h2>5. 各种概率分布之间的深层联系</h2>
<p>不同的分布之间存在着紧密的内在转换与极限近似关系：</p>
<pre><code>graph TD
    Bernoulli["两点分布"] --&gt;|n次独立试验| Binomial["二项分布"]
    Binomial --&gt;|n很大且p极小| Poisson["泊松分布"]
    Binomial --&gt;|n很大且期望较大| Normal["正态分布"]
    Poisson --&gt;|均值较大时| Normal
    Normal --&gt;|标准化变换| StdNormal["标准正态分布"]
    StdNormal --&gt;|独立变量平方和| ChiSquare["卡方分布"]
    StdNormal &amp; ChiSquare --&gt;|比值构成统计量| TDist["t 分布"]
    ChiSquare &amp; ChiSquare --&gt;|比值构成统计量| FDist["F 分布"]
    TDist --&gt;|自由度趋于无穷| StdNormal
</code></pre>
<h3>1. 两点分布与二项分布</h3>
<p>两点分布是二项分布当试验次数 $n = 1$ 时的特例，即二项分布可以看作是 $n$ 个独立同分布的两点分布随机变量之和。</p>
<h3>2. 二项分布与泊松分布的极限（泊松定理）</h3>
<p>当二项分布中试验次数 $n$ 非常大（如 $n \ge 100$），且单次成功概率 $p$ 非常小（如 $p \le 0.1$），而乘积 $\lambda = np$ 保持稳定时，二项分布可近似为泊松分布：</p>
<p>$$
B(n, p) \approx P(np)
$$</p>
<p>这常被用于计算稀有突发事件的概率。</p>
<h3>3. 二项分布的正态近似（棣莫弗-拉普拉斯定理）</h3>
<p>当二项分布的 $n$ 较大且 $np$ 与 $n(1-p)$ 均较大（通常大于 5）时，二项分布可近似为正态分布：</p>
<p>$$
X \approx N\left(np, np(1 - p)\right)
$$</p>
<p>在实际近似计算中，为提高精确度，通常需要对区间进行连续性校正。例如，将 $P(X \le k)$ 校正为 $P(X &lt; k + 0.5)$ 后再进行标准化计算。</p>
<h3>4. 泊松分布的正态近似</h3>
<p>当泊松分布的均值参数 $\lambda$ 较大（如 $\lambda \ge 20$）时，泊松分布也可以近似为正态分布：</p>
<p>$$
X \approx N(\lambda, \lambda)
$$</p>
<h3>5. 抽样分布的起源</h3>
<p>三大抽样分布 $\chi^2$ 分布、$t$ 分布与 $F$ 分布，其数学构造的源头全部来自于独立的标准正态分布变量 $N(0, 1)$。</p>
<hr />
<h2>6. 常见概率分布速查总表</h2>
<table>
<thead>
<tr>
<th>分布名称</th>
<th>常用记号</th>
<th>随机变量取值范围</th>
<th>数学期望 $E(X)$</th>
<th>物理方差 $D(X)$</th>
<th>典型应用领域</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>两点分布</strong></td>
<td>$B(1, p)$</td>
<td>${0, 1}$</td>
<td>$p$</td>
<td>$p(1 - p)$</td>
<td>一次成败试验</td>
</tr>
<tr>
<td><strong>二项分布</strong></td>
<td>$B(n, p)$</td>
<td>${0, 1, \dots, n}$</td>
<td>$np$</td>
<td>$np(1 - p)$</td>
<td>重复抽样、成败计数</td>
</tr>
<tr>
<td><strong>几何分布</strong></td>
<td>$G(p)$</td>
<td>${1, 2, \dots}$</td>
<td>$\dfrac{1}{p}$</td>
<td>$\dfrac{1 - p}{p^2}$</td>
<td>首次成功的等待次数</td>
</tr>
<tr>
<td><strong>超几何分布</strong></td>
<td>$H(N, M, n)$</td>
<td>合理整数组合 $k$</td>
<td>$n\dfrac{M}{N}$</td>
<td>$n\dfrac{M}{N}\left(1 - \dfrac{M}{N}\right)\dfrac{N - n}{N - 1}$</td>
<td>不放回抽样质检</td>
</tr>
<tr>
<td><strong>泊松分布</strong></td>
<td>$P(\lambda)$</td>
<td>${0, 1, 2, \dots}$</td>
<td>$\lambda$</td>
<td>$\lambda$</td>
<td>单位时间或区域发生频数</td>
</tr>
<tr>
<td><strong>均匀分布</strong></td>
<td>$U(a, b)$</td>
<td>$[a, b]$</td>
<td>$\dfrac{a + b}{2}$</td>
<td>$\dfrac{(b - a)^2}{12}$</td>
<td>区间等概率取值、随机等待</td>
</tr>
<tr>
<td><strong>指数分布</strong></td>
<td>$E(\lambda)$</td>
<td>$[0, +\infty)$</td>
<td>$\dfrac{1}{\lambda}$</td>
<td>$\dfrac{1}{\lambda^2}$</td>
<td>设备物理寿命、服务间隔</td>
</tr>
<tr>
<td><strong>正态分布</strong></td>
<td>$N(\mu, \sigma^2)$</td>
<td>$(-\infty, +\infty)$</td>
<td>$\mu$</td>
<td>$\sigma^2$</td>
<td>测量误差、身高、自然现象</td>
</tr>
<tr>
<td><strong>卡方分布</strong></td>
<td>$\chi^2(n)$</td>
<td>$[0, +\infty)$</td>
<td>$n$</td>
<td>$2n$</td>
<td>正态总体方差推断</td>
</tr>
<tr>
<td><strong>$t$ 分布</strong></td>
<td>$t(n)$</td>
<td>$(-\infty, +\infty)$</td>
<td>$0 \quad (n&gt;1)$</td>
<td>$\dfrac{n}{n - 2} \quad (n&gt;2)$</td>
<td>方差未知时的均值推断</td>
</tr>
<tr>
<td><strong>$F$ 分布</strong></td>
<td>$F(m, n)$</td>
<td>$[0, +\infty)$</td>
<td>$\dfrac{n}{n - 2} \quad (n&gt;2)$</td>
<td>较复杂</td>
<td>两个总体方差的对比推断</td>
</tr>
</tbody>
</table>
<hr />
<h2>7. 实际做题时如何选择分布</h2>
<p>在面对概率论应用题时，可以通过题目中的关键性线索锁定所使用的分布类型：</p>
<ol>
<li><strong>“仅有一次且只有两种结果”</strong> $\implies$ 选用 <strong>两点分布</strong>；</li>
<li><strong>“独立重复进行 $n$ 次，求解成功了多少次”</strong> $\implies$ 选用 <strong>二项分布</strong>；</li>
<li><strong>“不断重复试验，问第几次才首次出现成功”</strong> $\implies$ 选用 <strong>几何分布</strong>；</li>
<li><strong>“针对一个有限总体进行‘不放回’地抽取样本”</strong> $\implies$ 选用 <strong>超几何分布</strong>；</li>
<li><strong>“求解在一段确定时间或一个确定空间里，某随机事件发生的次数”</strong> $\implies$ 选用 <strong>泊松分布</strong>；</li>
<li><strong>“随机变量在某个范围区间内的取值机会是均等的”</strong> $\implies$ 选用 <strong>均匀分布</strong>；</li>
<li><strong>“研究某些产品设备的使用寿命，或者求两次随机事件发生的间隔等待时间”</strong> $\implies$ 选用 <strong>指数分布</strong>；</li>
<li><strong>“物理测量的误差，大量独立小随机因素累积的结果”</strong> $\implies$ 选用 <strong>正态分布</strong>；</li>
<li><strong>“推断正态总体的方差性质”</strong> $\implies$ 选用 <strong>卡方分布</strong>；</li>
<li><strong>“总体方差未知，需要估计或检验正态总体的均值”</strong> $\implies$ 选用 <strong>$t$ 分布</strong>；</li>
<li><strong>“比较两个正态总体的方差大小”</strong> $\implies$ 选用 <strong>$F$ 分布</strong>。</li>
</ol>
<hr />
<h2>8. 易混淆分布深入对比</h2>
<h3>8.1 二项分布 与 超几何分布</h3>
<ul>
<li><strong>二项分布</strong>：每次抽样是有放回的（或者总体无穷大），每次抽取时摸出目标物的概率 $p$ 保持恒定不变。</li>
<li><strong>超几何分布</strong>：每次抽样是不放回的，因为每一次抽走个体会影响下一次摸出目标物的概率。</li>
<li><strong>联系</strong>：当总体容量 $N$ 趋于无穷大时，不放回抽样和放回抽样差别极小，超几何分布在极限下收敛于二项分布。</li>
</ul>
<hr />
<h3>8.2 二项分布 与 几何分布</h3>
<ul>
<li><strong>二项分布</strong>：试验的次数是固定的（即常数 $n$），随机变量是“成功的次数”。</li>
<li><strong>几何分布</strong>：试验的次数是随机变量（即试验一直做到成功为止），随机变量是“首次成功时的试验次数”。</li>
</ul>
<hr />
<h3>8.3 泊松分布 与 指数分布</h3>
<ul>
<li><strong>泊松分布</strong>：属于离散型随机变量，数的是在单位时间内事件发生的“次数”（如一小时内通过路口的车辆数）。</li>
<li><strong>指数分布</strong>：属于连续型随机变量，测量的是两次随机事件发生之间的“时间长短”（如等待下一辆车通过路口的时间）。</li>
</ul>
<hr />
<h3>8.4 正态分布 与 $t$ 分布</h3>
<ul>
<li><strong>正态分布</strong>：在总体方差 $\sigma^2$ 已知，或样本量较大（利用中心极限定理）时使用。</li>
<li><strong>$t$ 分布</strong>：仅在总体服从正态分布，且总体方差 $\sigma^2$ 未知的小样本推断中使用。</li>
</ul>
<hr />
<h2>9. 备考复习优先级建议</h2>
<ol>
<li><strong>第一梯队（核心考点，必须透彻掌握）</strong>：
二项分布、泊松分布、正态分布（包含标准化计算与中心极限定理的结合）。</li>
<li><strong>第二梯队（常考基础题）</strong>：
均匀分布、指数分布（牢记无记忆性）、超几何分布。</li>
<li><strong>第三梯队（统计推断必备）</strong>：
卡方分布、$t$ 分布、$F$ 分布（熟练掌握它们与正态总体样本均值、样本方差的转换公式）。</li>
</ol>
<hr />
<h2>10. 一句话总结各种分布</h2>
<p>$$
\boxed{\text{两点一次成败，二项多次成败，几何首次成功，超几何不放回，泊松数次数，均匀区间等概率，指数等时间，正态看自然误差，卡方看方差，} t \text{ 看方差未知均值，} F \text{ 看方差之比。}}
$$</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>假设检验</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%81%87%E8%AE%BE%E6%A3%80%E9%AA%8C/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%81%87%E8%AE%BE%E6%A3%80%E9%AA%8C/</id>
    <published>2026-06-24T02:25:00.000Z</published>
    <updated>2026-06-24T02:25:00.000Z</updated>
    <summary>系统梳理数理统计中的假设检验，包括假设检验的基本思想、原假设与备择假设、双侧与单侧检验、两类错误，以及单个与两个正态总体的均值和方差检验、成对样本检验与P值法。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1166455675&amp;p=10" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理数理统计中的<strong>假设检验</strong>。假设检验与前面的<strong>参数估计</strong>并称为数理统计的两大核心内容。</p>
<p>它们之间的区别和联系可以简单概括为：</p>
<ul>
<li><strong>参数估计</strong>：</li>
</ul>
<p>$$
\boxed{\text{利用样本数据去推断总体中的未知参数具体是多少}}
$$</p>
<ul>
<li><strong>假设检验</strong>：</li>
</ul>
<p>$$
\boxed{\text{利用样本数据去推断关于总体参数的某一种说法是否可信}}
$$</p>
<hr />
<h2>1. 假设检验的基本思想</h2>
<p>假设检验的核心理论依据是概率论中的<strong>小概率事件原理</strong>：</p>
<blockquote>
<p>如果在原假设成立的前提下，一个极小概率的事件居然在一次抽样试验中发生了，那么我们就有充分的理由怀疑该原假设是不成立的。</p>
</blockquote>
<h4>典型简例</h4>
<p>某零件工厂声称其生产的产品平均寿命为：</p>
<p>$$
\mu = 1000 \text{ 小时}
$$</p>
<p>现进行随机抽样检测，测得样本的平均寿命明显小于 1000 小时。如果在“总体均值 $\mu = 1000$ 成立”的前提下，抽样得到如此之小的样本均值是一件几乎不可能发生的小概率事件，那么我们就有理由推翻厂家的宣传，倾向于拒绝该厂家的说法。</p>
<hr />
<h2>2. 原假设与备择假设</h2>
<p>在假设检验中，我们需要对立地提出两个假设。</p>
<h3>原假设</h3>
<p>原假设（也称虚无假设）记作：</p>
<p>$$
H_0
$$</p>
<p>通常代表：</p>
<ul>
<li>状态没有差异或没有变化；</li>
<li>维持原状，或厂家宣称的指标成立；</li>
<li>未知参数等于某个给定的标准值。</li>
</ul>
<p>例如：</p>
<p>$$
H_0: \mu = 1000
$$</p>
<hr />
<h3>备择假设</h3>
<p>备择假设记作：</p>
<p>$$
H_1
$$</p>
<p>通常代表我们所倾向于寻找、证实或检验的另一种可能性。</p>
<p>例如：</p>
<p>$$
H_1: \mu \ne 1000 \quad \text{或} \quad H_1: \mu &gt; 1000 \quad \text{或} \quad H_1: \mu &lt; 1000
$$</p>
<hr />
<h2>3. 双侧检验与单侧检验</h2>
<p>根据备择假设 $H_1$ 的方向不同，假设检验可以细分为双侧检验与单侧检验。</p>
<h3>双侧检验</h3>
<p>如果备择假设是：</p>
<p>$$
H_1: \mu \ne \mu_0
$$</p>
<p>则称为<strong>双侧检验</strong>。这代表只要样本数据算得的总体均值显着地大于 $\mu_0$ 或者显着地小于 $\mu_0$，均会被判定为异常，从而拒绝原假设。</p>
<p>例如：</p>
<p>$$
H_0: \mu = 1000, \quad H_1: \mu \ne 1000
$$</p>
<hr />
<h3>右侧检验</h3>
<p>如果备择假设是：</p>
<p>$$
H_1: \mu &gt; \mu_0
$$</p>
<p>则称为<strong>右侧检验</strong>。例如，我们需要检验某种新改进的教学法是否显著提高了学生的平均成绩：</p>
<p>$$
H_0: \mu = 80, \quad H_1: \mu &gt; 80
$$</p>
<hr />
<h3>左侧检验</h3>
<p>如果备择假设是：</p>
<p>$$
H_1: \mu &lt; \mu_0
$$</p>
<p>则称为<strong>左侧检验</strong>。例如，我们需要检验某种新设备零件的寿命是否低于标准寿命要求：</p>
<p>$$
H_0: \mu = 1000, \quad H_1: \mu &lt; 1000
$$</p>
<hr />
<h2>4. 显著性水平</h2>
<p>显著性水平记作：</p>
<p>$$
\alpha
$$</p>
<p>它代表在进行假设检验时，我们所能允许的犯第一类错误（即原假设本是真实的却被错误地拒绝）的最大概率限度。</p>
<p>常见的取值有 0.05 或 0.01。例如，当设定 $\alpha = 0.05$ 时，意味着有至多 $5%$ 的概率会将一个真实成立的原假设 $H_0$ 错误地予以拒绝。</p>
<hr />
<h2>5. 两类错误</h2>
<p>在做出检验决策时，受样本随机性的局限，我们可能会犯以下两类错误：</p>
<table>
<thead>
<tr>
<th>决策结果 \ $H_0$ 的真实状态</th>
<th>$H_0$ 为真</th>
<th>$H_0$ 为假</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>接受 $H_0$</strong></td>
<td>决策正确</td>
<td>犯第二类错误（概率为 $\beta$）</td>
</tr>
<tr>
<td><strong>拒绝 $H_0$</strong></td>
<td>犯第一类错误（概率为 $\alpha$）</td>
<td>决策正确</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>第一类错误</strong>：原假设 $H_0$ 真实成立，但我们根据样本决策却拒绝了它。其概率上限即为显著性水平 $\alpha$。</li>
<li><strong>第二类错误</strong>：原假设 $H_0$ 不成立，但由于样本特征不明显，我们决策未能拒绝它。其发生概率通常记作 $\beta$。</li>
</ul>
<p>在考题与实际应用中，主要控制和考查的是第一类错误概率 $\alpha$。</p>
<hr />
<h2>6. 检验统计量</h2>
<p>为进行检验，我们需要构造一个基于样本的统计量，称为<strong>检验统计量</strong>。</p>
<p>该统计量需要具备以下核心性质：</p>
<p>$$
\boxed{\text{当原假设 } H_0 \text{ 成立时，该检验统计量的概率分布类型是完全已知的}}
$$</p>
<p>例如在正态总体均值的检验中，若已知总体方差 $\sigma^2$，我们构造检验统计量为：</p>
<p>$$
Z = \frac{\overline{X} - \mu_0}{\sigma / \sqrt{n}}
$$</p>
<p>一旦假定 $H_0: \mu = \mu_0$ 成立，该统计量必服从标准正态分布：</p>
<p>$$
Z \sim N(0, 1)
$$</p>
<p>我们可以通过观察样本实际算出的 $Z$ 是否落入其分布的极端区域，来判断该事件是否发生。</p>
<hr />
<h2>7. 拒绝域</h2>
<p>如果样本算得的检验统计量数值落入了某个概率极低的极端取值区域，说明在原假设成立的前提下发生了一件小概率事件，据此我们将拒绝原假设。这个极端的区域称为<strong>拒绝域</strong>。</p>
<p>以检验统计量 $Z \sim N(0, 1)$ 为例：</p>
<h3>双侧检验</h3>
<p>$$
H_0: \mu = \mu_0, \quad H_1: \mu \ne \mu_0
$$</p>
<p>拒绝域公式为：</p>
<p>$$
\boxed{|Z| &gt; z_{\alpha/2}}
$$</p>
<p>其中分位数满足 $P(Z &gt; z_{\alpha/2}) = \dfrac{\alpha}{2}$。</p>
<hr />
<h3>右侧检验</h3>
<p>$$
H_1: \mu &gt; \mu_0
$$</p>
<p>拒绝域公式为：</p>
<p>$$
\boxed{Z &gt; z_\alpha}
$$</p>
<hr />
<h3>左侧检验</h3>
<p>$$
H_1: \mu &lt; \mu_0
$$</p>
<p>拒绝域公式为：</p>
<p>$$
\boxed{Z &lt; -z_\alpha}
$$</p>
<hr />
<h2>8. 假设检验的一般决策步骤</h2>
<p>进行假设检验题时，标准的步骤可以分为五步：</p>
<h3>第一步：提出原假设与备择假设</h3>
<p>明确写出 $H_0$ 与 $H_1$ 的具体参数表达形式。</p>
<h3>第二步：选择显著性水平 $\alpha$</h3>
<p>根据题干要求，一般取 $\alpha = 0.05$ 或 $\alpha = 0.01$。</p>
<h3>第三步：确定检验统计量</h3>
<p>结合总体背景条件（如正态总体中方差已知或未知），选择 $Z$、$T$、$\chi^2$ 或 $F$ 统计量。</p>
<h3>第四步：确定拒绝域边界临界值</h3>
<p>根据检验的双侧或单侧方向，以及显著性水平查表确定临界值，写出拒绝域表达式。</p>
<h3>第五步：代入数据计算并得出决策结论</h3>
<p>如果计算值落入拒绝域，则：</p>
<p>$$
\boxed{\text{拒绝原假设 } H_0}
$$</p>
<p>如果计算值没有落入拒绝域，则：</p>
<p>$$
\boxed{\text{不拒绝原假设 } H_0}
$$</p>
<ul>
<li>注意：学术上通常表述为“不拒绝 $H_0$”而避免直接写“接受 $H_0$”，因为样本只能说明没有充足的证据拒绝它，并不等同于从数学上证明了它绝对正确。</li>
</ul>
<hr />
<h2>9. 单个正态总体均值 $\mu$ 的检验</h2>
<p>这是考试中出现频率最高的一类检验。</p>
<hr />
<h3>情况一：总体方差 $\sigma^2$ 已知</h3>
<p>设总体 $X \sim N(\mu, \sigma^2)$，其中 $\sigma^2$ 已知。我们需要检验 $H_0: \mu = \mu_0$。</p>
<p>选择统计量：</p>
<p>$$
\boxed{Z = \frac{\overline{X} - \mu_0}{\sigma / \sqrt{n}}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $Z \sim N(0, 1)$。</p>
<p>根据备择假设的方向，其拒绝域为：</p>
<ul>
<li><strong>双侧检验</strong> ($H_1: \mu \ne \mu_0$)：</li>
</ul>
<p>$$
\boxed{|Z| &gt; z_{\alpha/2}}
$$</p>
<ul>
<li><strong>右侧检验</strong> ($H_1: \mu &gt; \mu_0$)：</li>
</ul>
<p>$$
\boxed{Z &gt; z_\alpha}
$$</p>
<ul>
<li><strong>左侧检验</strong> ($H_1: \mu &lt; \mu_0$)：</li>
</ul>
<p>$$
\boxed{Z &lt; -z_\alpha}
$$</p>
<hr />
<h3>情况二：总体方差 $\sigma^2$ 未知</h3>
<p>设总体 $X \sim N(\mu, \sigma^2)$，其中 $\sigma^2$ 未知。我们需要检验 $H_0: \mu = \mu_0$。</p>
<p>此时需要使用样本无偏标准差 $S$ 来替换未知的总体标准差 $\sigma$，构造的统计量变为 $t$ 统计量：</p>
<p>$$
\boxed{T = \frac{\overline{X} - \mu_0}{S / \sqrt{n}}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $T \sim t(n - 1)$。</p>
<p>其对应的拒绝域为：</p>
<ul>
<li><strong>双侧检验</strong> ($H_1: \mu \ne \mu_0$)：</li>
</ul>
<p>$$
\boxed{|T| &gt; t_{\alpha/2}(n - 1)}
$$</p>
<ul>
<li><strong>右侧检验</strong> ($H_1: \mu &gt; \mu_0$)：</li>
</ul>
<p>$$
\boxed{T &gt; t_\alpha(n - 1)}
$$</p>
<ul>
<li><strong>左侧检验</strong> ($H_1: \mu &lt; \mu_0$)：</li>
</ul>
<p>$$
\boxed{T &lt; -t_\alpha(n - 1)}
$$</p>
<hr />
<h2>10. 单个正态总体方差 $\sigma^2$ 的检验</h2>
<p>设总体服从正态分布 $X \sim N(\mu, \sigma^2)$，我们需要检验总体方差是否等于特定值 $\sigma_0^2$：</p>
<p>$$
H_0: \sigma^2 = \sigma_0^2
$$</p>
<p>构造检验统计量为：</p>
<p>$$
\boxed{\chi^2 = \frac{(n-1)S^2}{\sigma_0^2}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $\chi^2 \sim \chi^2(n - 1)$。</p>
<p>根据卡方临界值规定（定义 $\chi^2_\alpha$ 表示卡方右侧尾部面积为 $\alpha$ 的分位数），其拒绝域为：</p>
<ul>
<li><strong>双侧检验</strong> ($H_1: \sigma^2 \ne \sigma_0^2$)：</li>
</ul>
<p>$$
\boxed{\chi^2 &lt; \chi^2_{1-\alpha/2}(n-1) \quad \text{或} \quad \chi^2 &gt; \chi^2_{\alpha/2}(n-1)}
$$</p>
<ul>
<li><strong>右侧检验</strong> ($H_1: \sigma^2 &gt; \sigma_0^2$)：</li>
</ul>
<p>$$
\boxed{\chi^2 &gt; \chi^2_{\alpha}(n-1)}
$$</p>
<ul>
<li><strong>左侧检验</strong> ($H_1: \sigma^2 &lt; \sigma_0^2$)：</li>
</ul>
<p>$$
\boxed{\chi^2 &lt; \chi^2_{1-\alpha}(n-1)}
$$</p>
<hr />
<h2>11. 两个独立正态总体均值差 $\mu_1 - \mu_2$ 的检验</h2>
<p>设有两个独立的总体 $X \sim N(\mu_1, \sigma_1^2)$ 和 $Y \sim N(\mu_2, \sigma_2^2)$。样本均值分别为 $\overline{X}$ 和 $\overline{Y}$。</p>
<p>我们检验原假设：</p>
<p>$$
H_0: \mu_1 - \mu_2 = \Delta_0 \quad (\text{当检验均值是否相等时，}\Delta_0 = 0)
$$</p>
<hr />
<h3>情况一：两个总体方差已知</h3>
<p>选择检验统计量为：</p>
<p>$$
\boxed{Z = \frac{(\overline{X} - \overline{Y}) - \Delta_0}{\sqrt{\dfrac{\sigma_1^2}{n_1} + \dfrac{\sigma_2^2}{n_2}}}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $Z \sim N(0, 1)$。</p>
<hr />
<h3>情况二：两个总体方差未知但相等</h3>
<p>若两个总体的方差均未知，但满足 $\sigma_1^2 = \sigma_2^2 = \sigma^2$。</p>
<p>我们使用合并样本方差 $S_w^2$：</p>
<p>$$
S_w^2 = \frac{(n_1 - 1)S_1^2 + (n_2 - 1)S_2^2}{n_1 + n_2 - 2}
$$</p>
<p>选择的检验统计量为：</p>
<p>$$
\boxed{T = \frac{(\overline{X} - \overline{Y}) - \Delta_0}{S_w\sqrt{\dfrac{1}{n_1} + \dfrac{1}{n_2}}}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $T \sim t(n_1 + n_2 - 2)$。</p>
<hr />
<h2>12. 配对样本（成对数据）均值差的检验</h2>
<p>如果抽样得到的两个样本数据不是独立的，而是存在一一配对的关系，我们应进行配对样本检验。</p>
<p>常见场景有：</p>
<ul>
<li>同一批被试在参加培训（或接受干预）前后的考试成绩对比；</li>
<li>同一批病人在使用某种治疗药物前后的血压指标对比；</li>
<li>采用不同仪器对同一批工业零件测量所得的误差对比。</li>
</ul>
<p>对于这类问题，我们的解题方法是先对每组配对数据求差值：</p>
<p>$$
D_i = X_i - Y_i
$$</p>
<p>通过做差，将两样本检验问题完美转化为了单总体均值的假设检验问题：</p>
<p>$$
H_0: \mu_D = 0
$$</p>
<p>构造检验统计量：</p>
<p>$$
\boxed{T = \frac{\overline{D} - 0}{S_D / \sqrt{n}}}
$$</p>
<p>当原假设 $H_0$ 成立时，满足 $T \sim t(n - 1)$，式中 $\overline{D}$ 和 $S_D$ 分别是差值序列的样本均值和样本无偏标准差。</p>
<p>核心策略：</p>
<p>$$
\boxed{\text{配对样本检验：先求差值建立单序列，再套用单总体均值 } t \text{ 检验}}
$$</p>
<hr />
<h2>13. 两个独立正态总体方差比 $\dfrac{\sigma_1^2}{\sigma_2^2}$ 的检验</h2>
<p>设有两个独立的总体，我们需要检验它们的方差是否相等：</p>
<p>$$
H_0: \sigma_1^2 = \sigma_2^2
$$</p>
<p>构造检验统计量为：</p>
<p>$$
\boxed{F = \frac{S_1^2}{S_2^2}}
$$</p>
<p>当原假设 $H_0$ 成立时，此统计量满足 $F \sim F(n_1 - 1, n_2 - 1)$。</p>
<ul>
<li>若进行右侧检验 ($H_1: \sigma_1^2 &gt; \sigma_2^2$)，对应的拒绝域为：</li>
</ul>
<p>$$
\boxed{F &gt; F_\alpha(n_1 - 1, n_2 - 1)}
$$</p>
<hr />
<h2>14. 总体比例 $p$ 的假设检验</h2>
<p>设总体中事件发生的概率为 $p$，我们要检验：</p>
<p>$$
H_0: p = p_0
$$</p>
<p>在样本容量 $n$ 较大时，利用中心极限定理，样本比例 $\hat{p}$ 近似服从正态分布，由此构造检验统计量：</p>
<p>$$
\boxed{Z = \frac{\hat{p} - p_0}{\sqrt{\dfrac{p_0(1-p_0)}{n}}}}
$$</p>
<p>当 $H_0$ 成立时，其值近似服从标准正态分布，可根据单双侧方向确定拒绝域。</p>
<hr />
<h2>15. P 值检验法</h2>
<p>除了上面基于临界值确定拒绝域的检验决策方法外，还可以直接利用 <strong>P 值</strong> 开展决策。</p>
<p>P 值（概率值）定义为：</p>
<p>$$
\boxed{\text{在原假设 } H_0 \text{ 成立的前提下，所获得的样本观测值或比其更为极端的事件发生的概率}}
$$</p>
<p>P 值的决策规则为：</p>
<p>$$
\boxed{P \text{ 值} \le \alpha \implies \text{拒绝原假设 } H_0}
$$</p>
<p>$$
\boxed{P \text{ 值} &gt; \alpha \implies \text{不拒绝原假设 } H_0}
$$</p>
<p>简单记为：</p>
<p>$$
\boxed{P \text{ 值越小，说明样本观测数据对原假设 } H_0 \text{ 的支持力度越弱}}
$$</p>
<hr />
<h2>16. 假设检验与置信区间的内在联系</h2>
<p>对于参数的双侧检验问题：</p>
<p>$$
H_0: \mu = \mu_0, \quad H_1: \mu \ne \mu_0
$$</p>
<p>在显著性水平 $\alpha$ 下执行假设检验决策，与计算该参数置信水平为 $1-\alpha$ 的置信区间是等价的：</p>
<ul>
<li>若假设的参数点 $\mu_0$ <strong>落在了置信区间内部</strong>，说明它是个可信的取值，故<strong>不拒绝原假设</strong>；</li>
<li>若假设的参数点 $\mu_0$ <strong>未落入置信区间内部</strong>，说明它落在置信区间外，故<strong>拒绝原假设</strong>。</li>
</ul>
<p>例如，显著性水平为 $0.05$ 的双侧均值检验与 $95%$ 的均值置信区间相互对应。</p>
<hr />
<h2>17. 典型例题解析</h2>
<p>某加工厂生产的产品标准寿命为 1000 小时。现在从新批次的产品中随机抽样抽取 16 件进行测试，测得：</p>
<p>$$
\overline{x} = 970 \text{ 小时}, \quad s = 60 \text{ 小时}
$$</p>
<p>假设该产品的寿命分布服从正态分布，在显著性水平 $\alpha = 0.05$ 条件下，试检验该批次产品的平均寿命是否低于标准寿命。</p>
<hr />
<h3>第一步：提出假设</h3>
<p>由于是要检验产品的均值“是否低于”标准，属于单侧的左侧检验：</p>
<p>$$
H_0: \mu = 1000
$$</p>
<p>$$
H_1: \mu &lt; 1000
$$</p>
<hr />
<h3>第二步：选择检验统计量</h3>
<p>因为总体为正态分布，且总体方差未知，所以应采用 $t$ 检验：</p>
<p>$$
T = \frac{\overline{X} - \mu_0}{S / \sqrt{n}}
$$</p>
<hr />
<h3>第三步：代入样本数据计算统计量</h3>
<p>代入数据计算 $T$ 值：</p>
<p>$$
T = \frac{970 - 1000}{60 / \sqrt{16}} = \frac{-30}{15} = -2
$$</p>
<hr />
<h3>第四步：确定拒绝域</h3>
<p>本题样本容量 $n = 16$，自由度为 $n-1 = 15$。</p>
<p>在显著性水平 $\alpha = 0.05$ 下的左侧检验，其对应的拒绝域形式为：</p>
<p>$$
T &lt; -t_{0.05}(15)
$$</p>
<p>查表得出自由度为 15 时的 $t$ 分布临界值为 $t_{0.05}(15) \approx 1.753$。</p>
<p>故拒绝域为：</p>
<p>$$
T &lt; -1.753
$$</p>
<hr />
<h3>第五步：对比并得出最终决策结论</h3>
<p>因为计算得到的统计量结果：</p>
<p>$$
-2 &lt; -1.753
$$</p>
<p>统计量值落入了拒绝域。</p>
<p>结论为：</p>
<p>$$
\boxed{\text{拒绝原假设 } H_0}
$$</p>
<p>也就是说，在显著性水平 $0.05$ 下，我们可以认为该批次产品的平均寿命显著低于 1000 小时的标准。</p>
<hr />
<h2>18. 常用参数假设检验方法汇总</h2>
<table>
<thead>
<tr>
<th>检验类型</th>
<th>适用条件场景</th>
<th>检验统计量公式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>单总体均值</strong></td>
<td>正态总体，$\sigma$ 已知</td>
<td>$Z = \dfrac{\overline X - \mu_0}{\sigma / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>单总体均值</strong></td>
<td>正态总体，$\sigma$ 未知</td>
<td>$T = \dfrac{\overline X - \mu_0}{S / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>单总体方差</strong></td>
<td>正态总体，均值未知</td>
<td>$\chi^2 = \dfrac{(n - 1)S^2}{\sigma_0^2}$</td>
</tr>
<tr>
<td><strong>双总体均值差</strong></td>
<td>独立样本，方差已知</td>
<td>$Z = \dfrac{(\overline X - \overline Y) - \Delta_0}{\sqrt{\sigma_1^2/n_1 + \sigma_2^2/n_2}}$</td>
</tr>
<tr>
<td><strong>双总体均值差</strong></td>
<td>独立样本，方差未知但相等</td>
<td>$T = \dfrac{(\overline X - \overline Y) - \Delta_0}{S_w\sqrt{1/n_1 + 1/n_2}}$</td>
</tr>
<tr>
<td><strong>配对均值差</strong></td>
<td>成对关联样本</td>
<td>$T = \dfrac{\overline D - 0}{S_D / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>双总体方差比</strong></td>
<td>独立正态总体</td>
<td>$F = \dfrac{S_1^2}{S_2^2}$</td>
</tr>
<tr>
<td><strong>总体比例</strong></td>
<td>大样本近似</td>
<td>$Z = \dfrac{\hat{p} - p_0}{\sqrt{p_0(1-p_0)/n}}$</td>
</tr>
</tbody>
</table>
<hr />
<h2>19. 解题时的决策判断路径</h2>
<p>面对具体考题，可以通过以下顺序开展判断：</p>
<h3>第一步：明确检验的对象</h3>
<p>确认题目中是要对“均值”、“方差”还是对“比例”指标开展判定；是一样本的检验，还是二样本的差值或比值检验。</p>
<h3>第二步：分析总体假定与抽样性质</h3>
<p>理清总体是否正态，方差是否已知；两个样本是相互独立的，还是配对样本。</p>
<h3>第三步：根据备择假设确定拒绝域方向</h3>
<p>观察备择假设 $H_1$ 中是“不等于”（双侧）、“大于”（右侧）还是“小于”（左侧），以此准确锁定对应的分位数边界。</p>
<hr />
<h2>20. 常见易错陷阱总结</h2>
<h3>易错陷阱一：决策结论用语不严密</h3>
<p>不能轻易下结论为“完全接受原假设 $H_0$”。当统计量没落入拒绝域时，正确的结论描述是“在当前的显著性水平下，我们没有充足的证据拒绝原假设 $H_0$，因此不拒绝原假设 $H_0$”。</p>
<h3>易错陷阱二：单双侧假设方向与题目对不齐</h3>
<p>如果题目问的是“是否有显著性差异”或“是否发生改变”，应当选用双侧检验；若题目明确倾向性，如检验“是否提高/大于”则选用右侧，检验“是否降低/低于”则选用左侧。</p>
<h3>易错陷阱三：统计量中的标准差代错</h3>
<p>要注意如果总体方差未知，用无偏样本标准差 $S$ 替代 $\sigma$ 后，统计量必须使用 $t$ 临界值；千万不能在使用样本标准差的同时去查标准正态分布 $Z$ 分位数表。</p>
<hr />
<h2>21. 本章核心公式速查表</h2>
<table>
<thead>
<tr>
<th>知识点</th>
<th>检验统计量表达形式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>单均值 ($\sigma^2$ 已知)</strong></td>
<td>$Z = \dfrac{\overline X - \mu_0}{\sigma / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>单均值 ($\sigma^2$ 未知)</strong></td>
<td>$T = \dfrac{\overline X - \mu_0}{S / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>单方差</strong></td>
<td>$\chi^2 = \dfrac{(n-1)S^2}{\sigma_0^2}$</td>
</tr>
<tr>
<td><strong>双均值差 (方差已知)</strong></td>
<td>$Z = \dfrac{(\overline X - \overline Y) - \Delta_0}{\sqrt{\sigma_1^2/n_1 + \sigma_2^2/n_2}}$</td>
</tr>
<tr>
<td><strong>双均值差 (方差相等)</strong></td>
<td>$T = \dfrac{(\overline X - \overline Y) - \Delta_0}{S_w\sqrt{1/n_1 + 1/n_2}}$</td>
</tr>
<tr>
<td><strong>配对差值均值</strong></td>
<td>$T = \dfrac{\overline D - 0}{S_D / \sqrt{n}}$</td>
</tr>
<tr>
<td><strong>双总体方差比</strong></td>
<td>$F = \dfrac{S_1^2}{S_2^2}$</td>
</tr>
<tr>
<td><strong>总体比例</strong></td>
<td>$Z = \dfrac{\hat{p} - p_0}{\sqrt{p_0(1-p_0)/n}}$</td>
</tr>
</tbody>
</table>
<hr />
<h2>22. 一句话掌握假设检验</h2>
<p>假设检验的完整逻辑主线为：</p>
<p>$$
\boxed{\text{假设原假设成立} \rightarrow \text{寻找并计算检验统计量} \rightarrow \text{判定是否落入低概率拒绝域}}
$$</p>
<p>做题时的标准思考模板为：</p>
<p>$$
\boxed{\text{列假设} \rightarrow \text{选统计量} \rightarrow \text{定拒绝域} \rightarrow \text{算结果} \rightarrow \text{下结论}}
$$</p>
<h2>23. 框框老师</h2>
<p><img src="https://img.picr2.online/1782268191871_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782268364899_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782268526471_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>参数估计</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%8F%82%E6%95%B0%E4%BC%B0%E8%AE%A1/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%8F%82%E6%95%B0%E4%BC%B0%E8%AE%A1/</id>
    <published>2026-06-24T01:50:00.000Z</published>
    <updated>2026-06-24T01:50:00.000Z</updated>
    <summary>系统梳理数理统计中的参数估计，包括矩估计法与最大似然估计法两大点估计方法、估计量的无偏性与有效性评价标准，以及正态总体均值与方差的区间估计理论。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1165922170&amp;p=9" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理数理统计中的<strong>参数估计</strong>。这一章属于数理统计的核心内容，其研究目标可以概括为：</p>
<p>$$
\boxed{\text{用收集到的样本数据去估计总体中未知的参数}}
$$</p>
<p>例如，我们已知总体服从正态分布：</p>
<p>$$
X \sim N(\mu, \sigma^2)
$$</p>
<p>但其中均值参数 $\mu$ 和方差参数 $\sigma^2$ 是未知的。我们通过在总体中进行随机抽样，获得一组样本数据：</p>
<p>$$
X_1, X_2, \dots, X_n
$$</p>
<p>随后通过对这些样本数据进行计算，推断和估计未知参数 $\mu$ 与 $\sigma^2$ 的取值。这就是参数估计。</p>
<hr />
<h2>1. 参数估计的两大分类</h2>
<p>根据估计结果的形式，参数估计可以分为两大类：</p>
<p>$$
\boxed{\text{点估计}} \quad \text{与} \quad \boxed{\text{区间估计}}
$$</p>
<table>
<thead>
<tr>
<th>估计类型</th>
<th>核心含义</th>
<th>典型简例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>点估计</strong></td>
<td>用一个具体的数值来估计未知参数</td>
<td>用样本均值 $\overline{X}$ 估计总体均值 $\mu$</td>
</tr>
<tr>
<td><strong>区间估计</strong></td>
<td>用一个包含未知参数的随机区间进行估计</td>
<td>估计总体均值满足 $\mu \in (a, b)$</td>
</tr>
</tbody>
</table>
<p>简单总结：</p>
<p>$$
\boxed{\text{点估计给出一个值，区间估计给出一个取值范围}}
$$</p>
<hr />
<h2>2. 点估计的定义</h2>
<p>设总体的概率分布中含有未知参数 $\theta$。我们构造一个样本的函数（即统计量）：</p>
<p>$$
\hat{\theta} = \hat{\theta}(X_1, X_2, \dots, X_n)
$$</p>
<p>用这个统计量作为 $\theta$ 的代表去进行推断。</p>
<ul>
<li><strong>估计量</strong>：统计量 $\hat{\theta}(X_1, X_2, \dots, X_n)$ 称为未知参数 $\theta$ 的<strong>估计量</strong>，它是随机变量。</li>
<li><strong>估计值</strong>：抽样后代入具体观测值所得到的实数值 $\hat{\theta}(x_1, x_2, \dots, x_n)$ 称为未知参数 $\theta$ 的<strong>估计值</strong>，它是一个确定常数。</li>
</ul>
<p>例如，样本均值：</p>
<p>$$
\overline{X} = \frac{1}{n}\sum_{i=1}^n X_i
$$</p>
<p>是总体均值 $\mu$ 的一个估计量。</p>
<p>当我们抽样后得到具体的数字，计算出来的算术平均数：</p>
<p>$$
\overline{x} = \frac{1}{n}\sum_{i=1}^n x_i
$$</p>
<p>则是总体均值 $\mu$ 的一个估计值。</p>
<hr />
<h2>3. 常用点估计方法</h2>
<p>在参数估计中，最常用的两种点估计构造方法是：</p>
<p>$$
\boxed{\text{矩估计法}} \quad \text{与} \quad \boxed{\text{最大似然估计法}}
$$</p>
<hr />
<h2>4. 矩估计法</h2>
<h3>4.1 核心思想</h3>
<p>矩估计法的基本立足点为：</p>
<p>$$
\boxed{\text{用样本矩去替代对应的总体矩}}
$$</p>
<p>我们用样本原点矩直接替换总体原点矩：</p>
<ul>
<li>总体的一阶原点矩（即期望）为：$E(X)$；</li>
<li>样本的一阶原点矩（即均值）为：$\overline{X}$。</li>
</ul>
<p>求解单未知参数时，常令二者相等：</p>
<p>$$
E(X) = \overline{X}
$$</p>
<p>然后解出未知参数的表达式即可。</p>
<p>如果总体包含两个未知参数，则需使用两个矩：</p>
<ul>
<li>总体的一阶与二阶原点矩分别为：$E(X)$ 和 $E(X^2)$；</li>
<li>样本的一阶与二阶原点矩分别为：$\dfrac{1}{n}\sum\limits_{i=1}^n X_i$ 和 $\dfrac{1}{n}\sum\limits_{i=1}^n X_i^2$。</li>
</ul>
<hr />
<h3>4.2 单个参数的矩估计示例</h3>
<p>设总体服从泊松分布：</p>
<p>$$
X \sim P(\lambda)
$$</p>
<p>已知泊松分布的数学期望为：</p>
<p>$$
E(X) = \lambda
$$</p>
<p>样本均值为 $\overline{X}$。令总体一阶矩等于样本一阶矩，即：</p>
<p>$$
\lambda = \overline{X}
$$</p>
<p>从而解得参数 $\lambda$ 的矩估计量为：</p>
<p>$$
\boxed{\hat{\lambda} = \overline{X}}
$$</p>
<hr />
<h3>4.3 两个参数的矩估计示例</h3>
<p>设总体服从正态分布，其中两个参数 $\mu$ 和 $\sigma^2$ 均未知：</p>
<p>$$
X \sim N(\mu, \sigma^2)
$$</p>
<p>总体的前二阶原点矩为：</p>
<p>$$
E(X) = \mu
$$</p>
<p>$$
E(X^2) = D(X) + [E(X)]^2 = \sigma^2 + \mu^2
$$</p>
<p>样本的前二阶原点矩为：</p>
<p>$$
A_1 = \frac{1}{n}\sum_{i=1}^n X_i = \overline{X}
$$</p>
<p>$$
A_2 = \frac{1}{n}\sum_{i=1}^n X_i^2
$$</p>
<p>令总体原点矩等于样本原点矩建立方程组：</p>
<p>$$
\mu = \overline{X}
$$</p>
<p>$$
\sigma^2 + \mu^2 = \frac{1}{n}\sum_{i=1}^n X_i^2
$$</p>
<p>解该方程组，得出估计量：</p>
<p>$$
\hat{\mu} = \overline{X}
$$</p>
<p>$$
\hat{\sigma}^2 = \frac{1}{n}\sum_{i=1}^n X_i^2 - \overline{X}^2 = \frac{1}{n}\sum_{i=1}^n (X_i - \overline{X})^2
$$</p>
<p>因此，正态总体方差参数的矩估计量为：</p>
<p>$$
\boxed{\hat{\sigma}^2 = \frac{1}{n}\sum_{i=1}^n (X_i - \overline{X})^2}
$$</p>
<p>需要特别注意的是，<strong>矩估计中方差项的分母是 $n$，而不是无偏样本方差中的 $n-1$</strong>。</p>
<hr />
<h2>5. 最大似然估计法</h2>
<h3>5.1 核心思想</h3>
<p>最大似然估计法的基本立足点为：</p>
<p>$$
\boxed{\text{寻找能使当前已发生样本观测值概率达到最大值的参数作为估计值}}
$$</p>
<p>例如抛一枚硬币 10 次，若实验结果为出现了 8 次正面朝上。我们直觉上会判定该硬币正面朝上的概率更接近 $p=0.8$，而不是普通的 $p=0.5$。这是因为在 $p=0.8$ 的设定下，发生“8次正面朝上”这一实验结果的概率最大。这就是最大似然的核心思想。</p>
<hr />
<h3>5.2 似然函数</h3>
<p>设获得的独立同分布样本观测值为 $x_1, x_2, \dots, x_n$，未知参数为 $\theta$。</p>
<ul>
<li><strong>离散型总体</strong>：若总体分布律为 $P(X = x) = p(x; \theta)$，则样本联合概率为：</li>
</ul>
<p>$$
L(\theta) = \prod_{i=1}^n p(x_i; \theta)
$$</p>
<ul>
<li><strong>连续型总体</strong>：若总体概率密度函数为 $f(x; \theta)$，则似然函数定义为：</li>
</ul>
<p>$$
L(\theta) = \prod_{i=1}^n f(x_i; \theta)
$$</p>
<p>最大似然估计的目标就是求解使似然函数 $L(\theta)$ 取得最大值的未知参数：</p>
<p>$$
\boxed{\hat{\theta} = \arg\max_\theta L(\theta)}
$$</p>
<hr />
<h3>5.3 对数似然的引入</h3>
<p>由于似然函数 $L(\theta)$ 通常是多个乘积项的乘式，直接求导数寻找极值点计算量极大。</p>
<p>而对数函数是单调递增的，所以 $L(\theta)$ 的极值点与对数似然函数 $\ln L(\theta)$ 的极值点完全重合：</p>
<p>$$
\ln L(\theta) = \sum_{i=1}^n \ln f(x_i; \theta)
$$</p>
<p>取对数后，原本的乘积式转换成了求和式，便于求导数计算。</p>
<p>最大似然估计的通用计算步骤为：</p>
<ol>
<li>写出似然函数表达式 $L(\theta)$；</li>
<li>取对数得到对数似然函数 $\ln L(\theta)$；</li>
<li>对对数似然函数关于参数求导数（或偏导数）；</li>
<li>令导数为 0 建立一阶极值方程；</li>
<li>解方程求得最大似然估计量。</li>
</ol>
<hr />
<h2>6. 最大似然估计例题一：伯努利分布</h2>
<p>设总体服从两点分布，其中 $p$ 为未知参数：</p>
<p>$$
P(X = 1) = p, \quad P(X = 0) = 1 - p
$$</p>
<p>抽取随机样本为 $X_1, X_2, \dots, X_n$。求参数 $p$ 的最大似然估计量。</p>
<p><strong>【解析】</strong></p>
<p>每一个样本 $X_i$ 的概率质量可以统一表示为 $P(X = X_i) = p^{X_i}(1-p)^{1-X_i}$。</p>
<p>写出似然函数：</p>
<p>$$
L(p) = \prod_{i=1}^n p^{X_i}(1-p)^{1-X_i} = p^{\sum X_i}(1-p)^{n - \sum X_i}
$$</p>
<p>取对数得到对数似然函数：</p>
<p>$$
\ln L(p) = \left(\sum_{i=1}^n X_i\right) \ln p + \left(n - \sum_{i=1}^n X_i\right) \ln(1 - p)
$$</p>
<p>对参数 $p$ 求导：</p>
<p>$$
\frac{d}{dp}\ln L(p) = \frac{\sum X_i}{p} - \frac{n - \sum X_i}{1 - p}
$$</p>
<p>令导数等于 0，建立极值方程：</p>
<p>$$
\frac{\sum X_i}{p} = \frac{n - \sum X_i}{1 - p} \implies (1 - p)\sum X_i = p\left(n - \sum X_i\right)
$$</p>
<p>解该方程得出 $p$ 的最大似然估计量：</p>
<p>$$
\boxed{\hat{p} = \frac{1}{n}\sum_{i=1}^n X_i = \overline{X}}
$$</p>
<p>它恰好等于样本中发生事件的比例。</p>
<hr />
<h2>7. 最大似然估计例题二：正态分布</h2>
<p>设总体服从正态分布，其中均值 $\mu$ 与方差 $\sigma^2$ 均未知：</p>
<p>$$
X \sim N(\mu, \sigma^2)
$$</p>
<p>样本观测值为 $X_1, X_2, \dots, X_n$。</p>
<p>正态分布密度函数为：</p>
<p>$$
f(x; \mu, \sigma^2) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{(x - \mu)^2}{2\sigma^2}}
$$</p>
<p>写出似然函数：</p>
<p>$$
L(\mu, \sigma^2) = \prod_{i=1}^n \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{(X_i - \mu)^2}{2\sigma^2}} = (2\pi\sigma^2)^{-\frac{n}{2}} e^{-\frac{1}{2\sigma^2}\sum (X_i - \mu)^2}
$$</p>
<p>取对数：</p>
<p>$$
\ln L(\mu, \sigma^2) = -\frac{n}{2}\ln(2\pi) - \frac{n}{2}\ln(\sigma^2) - \frac{1}{2\sigma^2}\sum_{i=1}^n (X_i - \mu)^2
$$</p>
<p>分别对 $\mu$ 和 $\sigma^2$ 求偏导数并令其等于 0：</p>
<p>$$
\frac{\partial \ln L}{\partial \mu} = \frac{1}{\sigma^2}\sum_{i=1}^n (X_i - \mu) = 0 \implies \hat{\mu} = \overline{X}
$$</p>
<p>$$
\frac{\partial \ln L}{\partial (\sigma^2)} = -\frac{n}{2\sigma^2} + \frac{1}{2(\sigma^2)^2}\sum_{i=1}^n (X_i - \mu)^2 = 0 \implies \hat{\sigma}^2 = \frac{1}{n}\sum_{i=1}^n (X_i - \hat{\mu})^2
$$</p>
<p>代入求出的均值估计量，得到最大似然估计结果：</p>
<p>$$
\boxed{\hat{\mu} = \overline{X}}
$$</p>
<p>$$
\boxed{\hat{\sigma}^2 = \frac{1}{n}\sum_{i=1}^n (X_i - \overline{X})^2}
$$</p>
<p>需要指出的是，<strong>最大似然估计推导出的方差分母是 $n$，而不是 $n-1$</strong>。</p>
<p>因为无偏样本方差定义为：</p>
<p>$$
S^2 = \frac{1}{n-1}\sum_{i=1}^n (X_i - \overline{X})^2
$$</p>
<p>这说明：</p>
<p>$$
\boxed{\text{正态总体方差的最大似然估计量不是无偏估计量}}
$$</p>
<hr />
<h2>8. 矩估计与最大似然估计的对比</h2>
<table>
<thead>
<tr>
<th>估计方法</th>
<th>核心思想</th>
<th>方法特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>矩估计</strong></td>
<td>用样本均值、样本平方均值等直接替代总体的对应矩</td>
<td>计算相对简单直接，不需要明确知晓总体具体的概率分布类型</td>
</tr>
<tr>
<td><strong>最大似然估计</strong></td>
<td>寻找使已发生观测结果的联合发生概率达到最大值的参数</td>
<td>依赖于总体概率密度的具体形式，推导出的估计量通常具备更为优良的数理统计性质</td>
</tr>
</tbody>
</table>
<p>简明记忆：</p>
<p>$$
\boxed{\text{矩估计关注统计特征对齐，最大似然估计关注最合理解释样本数据}}
$$</p>
<hr />
<h2>9. 估计量的评价标准</h2>
<p>对于同一个未知参数，采用不同的估计方法可能会构造出不同的估计量。我们需要通过以下三个标准来评价估计量的好坏：</p>
<p>$$
\boxed{\text{无偏性} \quad \text{与} \quad \text{有效性} \quad \text{与} \quad \text{相合性}}
$$</p>
<hr />
<h3>9.1 无偏性</h3>
<p>若估计量 $\hat{\theta}$ 的数学期望等于未知参数的真实值，则称其为<strong>无偏估计量</strong>：</p>
<p>$$
\boxed{E(\hat{\theta}) = \theta}
$$</p>
<p>这意味着在大量的重复抽样估算中，该估计量在平均水平上没有系统性偏差。</p>
<ul>
<li>样本均值 $\overline{X}$ 是总体均值 $\mu$ 的无偏估计量，因为 $E(\overline{X}) = \mu$。</li>
<li>无偏样本方差 $S^2$ 是总体方差 $\sigma^2$ 的无偏估计量，因为满足 $E(S^2) = \sigma^2$。</li>
<li>而前面求出的矩估计与最大似然估计中的方差估计量 $\hat{\sigma}^2$ 的期望为：</li>
</ul>
<p>$$
E(\hat{\sigma}^2) = \frac{n-1}{n}\sigma^2 \ne \sigma^2
$$</p>
<p>因此 $\hat{\sigma}^2$ 是有偏估计。</p>
<hr />
<h3>9.2 有效性</h3>
<p>如果两个估计量 $\hat{\theta}_1$ 和 $\hat{\theta}_2$ 都是参数 $\theta$ 的无偏估计量，我们通过比较它们的方差来评估其精确度：</p>
<p>若满足：</p>
<p>$$
D(\hat{\theta}_1) &lt; D(\hat{\theta}_2)
$$</p>
<p>则称估计量 $\hat{\theta}_1$ 比 $\hat{\theta}_2$ <strong>更有效</strong>。</p>
<p>简明记忆：</p>
<p>$$
\boxed{\text{在同样无偏的前提下，谁的方差小波动小，谁就更有效}}
$$</p>
<hr />
<h3>9.3 相合性</h3>
<p>若当样本容量 $n \to \infty$ 时，估计量 $\hat{\theta}$ 依概率收敛于未知参数的真实值 $\theta$，则称其为<strong>相合估计量</strong>（也叫一致估计量）：</p>
<p>$$
\hat{\theta} \xrightarrow{P} \theta
$$</p>
<p>直观理解是：样本量越大，估算值就越接近真实参数。根据辛钦大数定律，样本均值 $\overline{X}$ 是总体均值 $\mu$ 的相合估计量。</p>
<hr />
<h2>10. 区间估计的基本概念</h2>
<p>点估计仅给出一个具体数值，但在实际中，单点估值刚好等于真实参数的概率几乎为 0。</p>
<p>为了说明估计的精确度与可靠性，我们需要构造一个以高概率包含真实参数的区间，这就是<strong>区间估计</strong>。</p>
<hr />
<h3>10.1 置信区间与置信水平</h3>
<p>如果对于给定的显著性水平 $\alpha$（$0 &lt; \alpha &lt; 1$），能够构造两个统计量 $\theta_1$ 和 $\theta_2$，使得总体未知参数 $\theta$ 满足：</p>
<p>$$
P(\theta_1 &lt; \theta &lt; \theta_2) = 1 - \alpha
$$</p>
<p>则称随机区间 $(\theta_1, \theta_2)$ 是未知参数 $\theta$ 的置信度为 $1 - \alpha$ 的<strong>置信区间</strong>。</p>
<ul>
<li>这里的 $1 - \alpha$ 称为<strong>置信水平</strong>（置信度），常见的取值有 0.95 或 0.99；</li>
<li>统计量 $\theta_1$ 与 $\theta_2$ 分别称为置信区间的置信下限与置信上限。</li>
</ul>
<hr />
<h3>10.2 置信区间的正确理解</h3>
<p>例如，我们通过估算得出均值参数置信区间为 $\mu \in (98.5, 101.2)$，其置信水平为 $95%$。</p>
<p>需要特别指出，<strong>不能解释为“参数 $\mu$ 有 $95%$ 的概率落在此区间内”</strong>。因为在数理统计中，未知参数 $\mu$ 是一个固定的常数，它并不具有随机性；而我们求出来的区间端点 $(\theta_1, \theta_2)$ 才是包含样本数据的随机变量。</p>
<p>正确的解释是：如果按照同样的方法重复抽样 100 次，可以构造出 100 个不同的置信区间，其中大约有 95 个区间能够包含真正的未知参数。</p>
<p>简明规律：</p>
<p>$$
\boxed{\text{在样本量一定时，置信水平越高，置信区间跨度越宽}}
$$</p>
<hr />
<h2>11. 单个正态总体均值 $\mu$ 的区间估计</h2>
<p>这是参数估计章节中最核心的计算部分。</p>
<hr />
<h3>情况一：总体方差 $\sigma^2$ 已知</h3>
<p>设总体服从 $X \sim N(\mu, \sigma^2)$，其中方差 $\sigma^2$ 已知。</p>
<p>根据样本均值性质有 $\overline{X} \sim N\left(\mu, \dfrac{\sigma^2}{n}\right)$，进行标准化处理：</p>
<p>$$
\frac{\overline{X} - \mu}{\sigma / \sqrt{n}} \sim N(0, 1)
$$</p>
<p>因此，在置信水平为 $1-\alpha$ 下，总体均值 $\mu$ 的置信区间公式为：</p>
<p>$$
\boxed{\left( \overline{X} - z_{\alpha/2} \frac{\sigma}{\sqrt{n}}, \quad \overline{X} + z_{\alpha/2} \frac{\sigma}{\sqrt{n}} \right)}
$$</p>
<p>其中 $z_{\alpha/2}$ 是标准正态分布的双侧分位数。对于 $95%$ 的置信水平，对应 $z_{0.025} = 1.96$。</p>
<hr />
<h3>情况二：总体方差 $\sigma^2$ 未知</h3>
<p>设总体服从 $X \sim N(\mu, \sigma^2)$，其中方差 $\sigma^2$ 未知。</p>
<p>因为方差未知，我们使用无偏样本标准差 $S$ 替换未知的总体标准差 $\sigma$，此时标准化后的变量不再服从正态分布，而是服从学生分布（即 $t$ 分布）：</p>
<p>$$
\frac{\overline{X} - \mu}{S / \sqrt{n}} \sim t(n-1)
$$</p>
<p>因此，在置信水平为 $1-\alpha$ 下，总体均值 $\mu$ 的置信区间公式为：</p>
<p>$$
\boxed{\left( \overline{X} - t_{\alpha/2}(n-1) \frac{S}{\sqrt{n}}, \quad \overline{X} + t_{\alpha/2}(n-1) \frac{S}{\sqrt{n}} \right)}
$$</p>
<p>其中 $t_{\alpha/2}(n-1)$ 是自由度为 $n-1$ 的 $t$ 分布的双侧分位数。</p>
<hr />
<h3>区间估计工具选择判断</h3>
<p>对于正态总体均值的区间估计，选择检验分布的依据为：</p>
<table>
<thead>
<tr>
<th>总体方差状态</th>
<th>适用分布类型</th>
</tr>
</thead>
<tbody>
<tr>
<td>总体方差 $\sigma^2$ <strong>已知</strong></td>
<td>标准正态分布 $Z$ 分布</td>
</tr>
<tr>
<td>总体方差 $\sigma^2$ <strong>未知</strong></td>
<td>$t$ 分布</td>
</tr>
</tbody>
</table>
<p>简明记忆：</p>
<p>$$
\boxed{\text{方差已知用 } Z \text{，方差未知用 } t}
$$</p>
<hr />
<h2>12. 单个正态总体方差 $\sigma^2$ 的区间估计</h2>
<p>设总体服从正态分布 $X \sim N(\mu, \sigma^2)$，其参数均未知。</p>
<p>根据数理统计的基本定理，样本方差满足卡方分布性质：</p>
<p>$$
\frac{(n-1)S^2}{\sigma^2} \sim \chi^2(n-1)
$$</p>
<p>因此，在置信度为 $1-\alpha$ 下，总体方差 $\sigma^2$ 的置信区间公式为：</p>
<p>$$
\boxed{\left( \frac{(n-1)S^2}{\chi^2_{\alpha/2}(n-1)}, \quad \frac{(n-1)S^2}{\chi^2_{1-\alpha/2}(n-1)} \right)}
$$</p>
<ul>
<li>注意：由于卡方分布概率密度并不对称，此区间是不对称的，且分母上较小的值 $\chi^2_{1-\alpha/2}(n-1)$ 作为分母排在右侧上限位置。</li>
</ul>
<p>若需要求标准差 $\sigma$ 的置信区间，对区间两端点开平方根即可：</p>
<p>$$
\boxed{\left( \sqrt{\frac{(n-1)S^2}{\chi^2_{\alpha/2}(n-1)}}, \quad \sqrt{\frac{(n-1)S^2}{\chi^2_{1-\alpha/2}(n-1)}} \right)}
$$</p>
<hr />
<h2>13. 单个总体比例 $p$ 的区间估计</h2>
<p>设总体某特征出现的概率（如次品率、合格率）为 $p$。进行了 $n$ 次独立重复试验，样本比例为 $\hat{p} = \dfrac{X}{n}$。</p>
<p>当样本量 $n$ 较大时，根据中心极限定理，样本比例近似服从正态分布：</p>
<p>$$
\hat{p} \approx N\left(p, \frac{p(1-p)}{n}\right)
$$</p>
<p>我们在计算方差时使用样本比例 $\hat{p}$ 代替未知参数 $p$，得出其近似的 $1-\alpha$ 置信区间公式：</p>
<p>$$
\boxed{\left( \hat{p} - z_{\alpha/2} \sqrt{\frac{\hat{p}(1-\hat{p})}{n}}, \quad \hat{p} + z_{\alpha/2} \sqrt{\frac{\hat{p}(1-\hat{p})}{n}} \right)}
$$</p>
<hr />
<h2>14. 两个独立正态总体均值差 $\mu_1 - \mu_2$ 的区间估计</h2>
<p>设有两个独立的总体 $X \sim N(\mu_1, \sigma_1^2)$ 和 $Y \sim N(\mu_2, \sigma_2^2)$。样本均值分别为 $\overline{X}$ 和 $\overline{Y}$。</p>
<hr />
<h3>情况一：两个总体方差已知</h3>
<p>当总体方差 $\sigma_1^2$ 和 $\sigma_2^2$ 均已知时，均值差的 $1-\alpha$ 置信区间公式为：</p>
<p>$$
\boxed{\left( (\overline{X} - \overline{Y}) - z_{\alpha/2}\sqrt{\frac{\sigma_1^2}{n_1} + \frac{\sigma_2^2}{n_2}}, \quad (\overline{X} - \overline{Y}) + z_{\alpha/2}\sqrt{\frac{\sigma_1^2}{n_1} + \frac{\sigma_2^2}{n_2}} \right)}
$$</p>
<hr />
<h3>情况二：两个总体方差未知但相等</h3>
<p>若两总体方差未知，但根据经验可以确定它们相等，即 $\sigma_1^2 = \sigma_2^2 = \sigma^2$。</p>
<p>我们首先计算两个样本的联合样本方差（即合并样本方差） $S_w^2$：</p>
<p>$$
S_w^2 = \frac{(n_1 - 1)S_1^2 + (n_2 - 1)S_2^2}{n_1 + n_2 - 2}
$$</p>
<p>在此条件下，均值差的置信区间采用 $t$ 分布：</p>
<p>$$
\boxed{\left( (\overline{X} - \overline{Y}) - t_{\alpha/2}(n_1+n_2-2)S_w\sqrt{\frac{1}{n_1} + \frac{1}{n_2}}, \quad (\overline{X} - \overline{Y}) + t_{\alpha/2}(n_1+n_2-2)S_w\sqrt{\frac{1}{n_1} + \frac{1}{n_2}} \right)}
$$</p>
<hr />
<h2>15. 两个独立正态总体方差比 $\dfrac{\sigma_1^2}{\sigma_2^2}$ 的区间估计</h2>
<p>对于方差比值的估计，需要构造服从 $F$ 分布的统计量：</p>
<p>$$
\frac{S_1^2 / \sigma_1^2}{S_2^2 / \sigma_2^2} \sim F(n_1 - 1, n_2 - 1)
$$</p>
<p>由此推导出总体方差比值 $\dfrac{\sigma_1^2}{\sigma_2^2}$ 的 $1-\alpha$ 置信区间为：</p>
<p>$$
\boxed{\left( \frac{S_1^2}{S_2^2} \frac{1}{F_{\alpha/2}(n_1-1, n_2-1)}, \quad \frac{S_1^2}{S_2^2} \frac{1}{F_{1-\alpha/2}(n_1-1, n_2-1)} \right)}
$$</p>
<p>利用临界值倒数关系 $F_{1-\alpha/2}(d_1, d_2) = \dfrac{1}{F_{\alpha/2}(d_2, d_1)}$，上式也可以改写。</p>
<hr />
<h2>16. 参数估计常见考查题型</h2>
<h3>题型一：求参数的矩估计量</h3>
<p><strong>解题步骤</strong>：</p>
<ol>
<li>算期望：写出总体原点矩表达式，如 $E(X)$、$E(X^2)$；</li>
<li>算样本矩：写出样本对应的一阶或二阶均值式子，如 $\overline{X}$、$\dfrac{1}{n}\sum X_i^2$；</li>
<li>对齐：令总体矩恒等于对应的样本矩；</li>
<li>解方程：解出未知参数的显式表达式。</li>
</ol>
<h3>题型二：求参数的最大似然估计量</h3>
<p><strong>解题步骤</strong>：</p>
<ol>
<li>乘积建立似然函数：根据离散分布律或连续密度函数求积式，列出 $L(\theta)$；</li>
<li>取对数：求得对数似然 $\ln L(\theta)$；</li>
<li>求导数：对对数似然求未知参数的一阶导数（或偏导数方程组）；</li>
<li>令导数为 0：建立极值点方程；</li>
<li>解未知数：解得估计量表达式。</li>
</ol>
<h3>题型三：检验估计量的无偏性</h3>
<p><strong>解题步骤</strong>：</p>
<ol>
<li>求出所构造估计量 $\hat{\theta}$ 的数学期望 $E(\hat{\theta})$；</li>
<li>判定其结果是否完全等于参数的真实值 $\theta$。若恒等则为无偏估计，否则为有偏估计。</li>
</ol>
<h3>题型四：判断置信区间的适用公式</h3>
<p><strong>解题步骤</strong>：
审清题目中是“均值区间”还是“方差区间”；针对均值区间，看清总体标准差 $\sigma$ 在题干中是否已知，根据“已知用 $Z$，未知用 $t$”的规律选择对应公式。</p>
<hr />
<h2>17. 易错点总结</h2>
<h3>易错点一：混淆估计量与估计值</h3>
<p>估计量（如 $\hat{\theta}(X_1, \dots, X_n)$）是一个关于样本随机变量的函数，其本身是具有分布特性的随机变量。而估计值（如 $\hat{\theta}(x_1, \dots, x_n)$）是将具体的数值代入后计算得出的具体数值。</p>
<h3>易错点二：正态总体方差估计的常数分母混淆</h3>
<p>需要理清：样本方差 $S^2$ 的分母是 $n-1$，它是总体方差的无偏估计；而通过矩估计与最大似然估计求出来的正态方差估计量，其分母是 $n$，它是有偏估计。</p>
<h3>易错点三：区间估计中置信水平与区间宽度的关系</h3>
<p>在样本容量不变的情况下，如果想要提高置信水平（例如从 95% 提升到 99%），置信区间的跨度必然会变宽，因为更高的可靠度要求更大的覆盖范围。</p>
<h3>易错点四：区间估计中对未知参数随机落入的错误表述</h3>
<p>置信区间是一个随机区间，其边界是随机变量。而未知参数是一个未知的恒定常数。所以应当表述为“区间包含该参数的概率为置信水平”，而不是“参数落入区间的概率”。</p>
<hr />
<h2>18. 本章核心公式表</h2>
<table>
<thead>
<tr>
<th>知识点内容</th>
<th>核心公式与计算式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>矩估计法</strong></td>
<td>总体原点矩等于对应的样本原点矩（$E(X^k) = A_k$）</td>
</tr>
<tr>
<td><strong>似然函数 (连续型)</strong></td>
<td>$L(\theta) = \prod\limits_{i=1}^n f(x_i; \theta)$</td>
</tr>
<tr>
<td><strong>对数似然求极值</strong></td>
<td>$\dfrac{d}{d\theta}\ln L(\theta) = 0$</td>
</tr>
<tr>
<td><strong>无偏性定义</strong></td>
<td>$E(\hat{\theta}) = \theta$</td>
</tr>
<tr>
<td><strong>有效性比较</strong></td>
<td>在无偏的前提下，方差较小者更有效（$D(\hat{\theta}_1) &lt; D(\hat{\theta}_2)$）</td>
</tr>
<tr>
<td><strong>均值置信区间 ($\sigma$ 已知)</strong></td>
<td>$\left( \overline{X} - z_{\alpha/2}\dfrac{\sigma}{\sqrt{n}}, \quad \overline{X} + z_{\alpha/2}\dfrac{\sigma}{\sqrt{n}} \right)$</td>
</tr>
<tr>
<td><strong>均值置信区间 ($\sigma$ 未知)</strong></td>
<td>$\left( \overline{X} - t_{\alpha/2}(n-1)\dfrac{S}{\sqrt{n}}, \quad \overline{X} + t_{\alpha/2}(n-1)\dfrac{S}{\sqrt{n}} \right)$</td>
</tr>
<tr>
<td><strong>方差置信区间 ($\sigma^2$ 未知)</strong></td>
<td>$\left( \dfrac{(n-1)S^2}{\chi^2_{\alpha/2}(n-1)}, \quad \dfrac{(n-1)S^2}{\chi^2_{1-\alpha/2}(n-1)} \right)$</td>
</tr>
<tr>
<td><strong>总体比例置信区间</strong></td>
<td>$\left( \hat{p} - z_{\alpha/2}\sqrt{\dfrac{\hat{p}(1-\hat{p})}{n}}, \quad \hat{p} + z_{\alpha/2}\sqrt{\dfrac{\hat{p}(1-\hat{p})}{n}} \right)$</td>
</tr>
</tbody>
</table>
<hr />
<h2>19. 一句话掌握参数估计</h2>
<p>参数估计的完整推导链条为：</p>
<p>$$
\boxed{\text{构造点估计量} \rightarrow \text{利用三大性质评价} \rightarrow \text{利用枢轴量法建立置信区间}}
$$</p>
<p>做题时的分析路径为：</p>
<ol>
<li><strong>确定估计模式</strong>：如果要求单一估值则用点估计，如果要求范围则用区间估计；</li>
<li><strong>点估计求解</strong>：矩估计令一二阶原点矩对齐解方程；最大似然估计求导对数似然式并令导数为 0；</li>
<li><strong>区间估计求解</strong>：根据正态分布均值估计的条件判断，方差已知套用标准正态分位数，方差未知套用 $t$ 分位数；方差估计套用卡方分位数。</li>
</ol>
<h2>20. 框框老师</h2>
<p><img src="https://img.picr2.online/1782266526916_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782266778146_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782267111249_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782267569788_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782267633978_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782267717752_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>大数定律与中心极限定理</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%A4%A7%E6%95%B0%E5%AE%9A%E5%BE%8B%E4%B8%8E%E4%B8%AD%E5%BF%83%E6%9E%81%E9%99%90%E5%AE%9A%E7%90%86/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E5%A4%A7%E6%95%B0%E5%AE%9A%E5%BE%8B%E4%B8%8E%E4%B8%AD%E5%BF%83%E6%9E%81%E9%99%90%E5%AE%9A%E7%90%86/</id>
    <published>2026-06-23T11:14:04.000Z</published>
    <updated>2026-06-23T11:14:04.000Z</updated>
    <summary>系统梳理概率论与数理统计中的大数定律与中心极限定理，包括独立同分布中心极限定理、棣莫弗-拉普拉斯定理及连续性校正等内容。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1164101528&amp;p=8" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理概率论中的<strong>大数定律与中心极限定理</strong>。中心极限定理说明了大量独立随机变量相加后，在适当标准化之后会近似服从正态分布，这是现实世界中许多随机现象呈现正态分布的根本原因。</p>
<hr />
<h2>1. 中心极限定理</h2>
<p>中心极限定理是概率论中最重要的定理之一，它说明：</p>
<blockquote>
<p>大量独立随机变量相加后，在适当标准化之后，近似服从正态分布。</p>
</blockquote>
<p>这就是为什么在现实世界中，很多随机现象（如考试成绩、物理测量误差、成年人的身高、零件加工尺寸的误差等）都会呈现出正态分布的特征。</p>
<hr />
<h3>1.1 从一个问题开始理解</h3>
<p>假设我们从总体中连续抽取多个随机变量：</p>
<p>$$
X_1, X_2, \dots, X_n
$$</p>
<p>它们相互独立，且服从相同的分布。</p>
<p>每个随机变量都有相同的期望值与方差：</p>
<p>$$
E(X_i) = \mu
$$</p>
<p>$$
D(X_i) = \sigma^2
$$</p>
<p>现在考虑这 $n$ 个独立随机变量的总和：</p>
<p>$$
S_n = X_1 + X_2 + \cdots + X_n
$$</p>
<p>根据期望值的性质，总和的平均水平为：</p>
<p>$$
E(S_n) = n\mu
$$</p>
<p>因为它们相互独立，总和的方差等于各方差之和：</p>
<p>$$
D(S_n) = n\sigma^2
$$</p>
<p>其标准差为：</p>
<p>$$
\sqrt{D(S_n)} = \sqrt{n}\sigma
$$</p>
<p>如果我们将总和 $S_n$ 进行标准化处理：</p>
<p>$$
\frac{S_n - n\mu}{\sqrt{n}\sigma}
$$</p>
<p>中心极限定理告诉我们，当样本量 $n$ 足够大时：</p>
<p>$$
\boxed{\frac{S_n - n\mu}{\sqrt{n}\sigma} \approx N(0, 1)}
$$</p>
<p>随着 $n$ 的增大，该标准化的随机变量会越来越接近标准正态分布。</p>
<hr />
<h3>1.2 独立同分布中心极限定理</h3>
<p>若随机变量序列 $X_1, X_2, \dots, X_n$ 独立同分布，且其期望 $E(X_i) = \mu$ 与方差 $D(X_i) = \sigma^2 &gt; 0$ 均存在。</p>
<p>则当样本容量 $n$ 足够大时，它们的规范和满足：</p>
<p>$$
\boxed{\frac{X_1 + X_2 + \cdots + X_n - n\mu}{\sqrt{n}\sigma} \sim N(0, 1)}
$$</p>
<p>严格来说，当 $n \to \infty$ 时，其累积分布函数在实数域上的每个点都收敛于标准正态分布的分布函数。</p>
<hr />
<h3>1.3 用样本均值表示</h3>
<p>令样本均值为：</p>
<p>$$
\overline{X} = \frac{X_1 + X_2 + \cdots + X_n}{n}
$$</p>
<p>由于 $S_n = n\overline{X}$，我们将此代入前面的标准化公式，中心极限定理也可以写为：</p>
<p>$$
\boxed{\frac{\overline{X} - \mu}{\sigma/\sqrt{n}} \sim N(0, 1)}
$$</p>
<p>这意味着样本均值 $\overline{X}$ 满足：</p>
<p>$$
\overline{X} \approx N\left(\mu, \frac{\sigma^2}{n}\right)
$$</p>
<p>也就是说，当样本容量 $n$ 较大时，无论总体服从何种分布，样本均值 $\overline{X}$ 均近似服从正态分布，其期望为总体均值 $\mu$，方差为 $\dfrac{\sigma^2}{n}$，标准差（又称为标准误差）为 $\dfrac{\sigma}{\sqrt{n}}$。</p>
<hr />
<h3>1.4 中心极限定理的直观物理含义</h3>
<blockquote>
<p>单个随机变量可能并不服从正态分布，但大量独立随机变量的叠加（或平均值）在整体上会表现出正态分布的规律。</p>
</blockquote>
<p>例如：</p>
<ul>
<li>掷一次骰子得到的点数呈均匀的离散分布；</li>
<li>但如果同时投掷很多次骰子，所得的点数总和或平均值就会呈现明显的钟形正态分布；</li>
<li>大量人群考试成绩的平均分、多项微小误差叠加后的累积误差均是此定理的现实体现。</li>
</ul>
<hr />
<h3>1.5 棣莫弗-拉普拉斯中心极限定理</h3>
<p>此定理是二项分布的正态近似情况。</p>
<p>设随机变量服从二项分布：</p>
<p>$$
X \sim B(n, p)
$$</p>
<p>其表示在 $n$ 次独立重复的伯努利试验中成功的次数。其期望与方差分别为：</p>
<p>$$
E(X) = np, \quad D(X) = np(1-p)
$$</p>
<p>当试验次数 $n$ 较大时：</p>
<p>$$
\boxed{\frac{X - np}{\sqrt{np(1-p)}} \approx N(0, 1)}
$$</p>
<p>即二项分布可以近似为正态分布：</p>
<p>$$
X \approx N\left(np, np(1-p)\right)
$$</p>
<h4>典型计算示例</h4>
<p>假设某投篮运动员的命中率为 $0.6$。现独立投篮 $100$ 次，令随机变量 $X$ 表示命中次数，则 $X \sim B(100, 0.6)$。</p>
<p>可以求得：</p>
<p>$$
E(X) = 100 \times 0.6 = 60
$$</p>
<p>$$
D(X) = 100 \times 0.6 \times 0.4 = 24
$$</p>
<p>因此，命中次数可以近似看作正态分布：</p>
<p>$$
X \approx N(60, 24)
$$</p>
<p>若要计算命中次数不超过 65 次的概率 $P(X \le 65)$，可通过标准化转化为标准正态分布进行求解：</p>
<p>$$
P(X \le 65) \approx P\left( \frac{X - 60}{\sqrt{24}} \le \frac{65 - 60}{\sqrt{24}} \right) = \Phi\left(\frac{5}{\sqrt{24}}\right)
$$</p>
<hr />
<h3>1.6 连续性校正</h3>
<p>因为二项分布属于离散型分布，而正态分布是连续型分布。在用连续型的正态分布去近似离散型的二项分布时，为了减小近似带来的误差，通常需要进行<strong>连续性校正</strong>。</p>
<p>例如，计算 $P(X \le 65)$ 时，我们常将其校正为：</p>
<p>$$
P(X &lt; 65.5)
$$</p>
<p>从而有：</p>
<p>$$
P(X \le 65) \approx P\left( Z \le \frac{65.5 - np}{\sqrt{np(1-p)}} \right)
$$</p>
<p>同理，对于区间概率的校正：</p>
<p>$$
P(a \le X \le b) \approx P(a - 0.5 &lt; X &lt; b + 0.5)
$$</p>
<hr />
<h3>1.7 中心极限定理与大数定律的区别</h3>
<p>这两大定律虽然都研究大量随机变量的极限行为，但关注的侧重点完全不同：</p>
<table>
<thead>
<tr>
<th>比较维度</th>
<th>大数定律</th>
<th>中心极限定理</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>研究对象</strong></td>
<td>样本均值 $\overline{X}$ 的收敛性</td>
<td>标准化后的样本均值 $\overline{X}$ 的分布形态</td>
</tr>
<tr>
<td><strong>核心物理含义</strong></td>
<td>说明样本均值 $\overline{X}$ 随着 $n$ 的增大趋近于总体期望 $\mu$</td>
<td>说明样本均值 $\overline{X}$ 与 $\mu$ 之间的偏差（误差）服从正态分布</td>
</tr>
<tr>
<td><strong>核心关键词</strong></td>
<td>均值的<strong>稳定性</strong>（收敛于常数）</td>
<td>误差的<strong>正态近似</strong>（形态收敛于钟形）</td>
</tr>
<tr>
<td><strong>数学表达形式</strong></td>
<td>$\overline{X} \xrightarrow{P} \mu$</td>
<td>$\dfrac{\overline{X} - \mu}{\sigma/\sqrt{n}} \xrightarrow{d} N(0, 1)$</td>
</tr>
</tbody>
</table>
<p>简明记忆：</p>
<p>$$
\boxed{\text{大数定律说明平均值会稳定趋于期望，中心极限定理说明平均值的误差波动服从正态分布}}
$$</p>
<hr />
<h3>1.8 中心极限定理在数理统计中的常见用途</h3>
<ol>
<li><strong>二项分布的正态近似</strong>：当 $n$ 较大时，将 $B(n, p)$ 近似为正态分布计算；</li>
<li><strong>大样本下的均值估计</strong>：无论总体分布如何，只要样本量 $n \ge 30$，均可以使用正态分布性质来推导样本均值 $\overline{X}$ 的概率特征；</li>
<li><strong>统计推断理论基础</strong>：在参数估计（置信区间）与假设检验中，许多大样本检验方法（如比例检验、均值差检验）的构建都完全依赖于中心极限定理。</li>
</ol>
<hr />
<h2>2. 本章核心公式速查表</h2>
<table>
<thead>
<tr>
<th>核心结论名称</th>
<th>对应数学公式表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>独立同分布中心极限定理</strong></td>
<td>$\dfrac{S_n - n\mu}{\sqrt{n}\sigma} \xrightarrow{d} N(0, 1)$</td>
</tr>
<tr>
<td><strong>样本均值正态近似</strong></td>
<td>$\dfrac{\overline{X} - \mu}{\sigma/\sqrt{n}} \approx N(0, 1)$</td>
</tr>
<tr>
<td><strong>二项分布正态近似</strong></td>
<td>$\dfrac{X - np}{\sqrt{np(1-p)}} \approx N(0, 1)$</td>
</tr>
</tbody>
</table>
<hr />
<h2>3. 本章学习主线</h2>
<p>关于中心极限定理的学习逻辑，可以通过以下链路进行贯通掌握：</p>
<p>$$
\boxed{\text{独立随机变量叠加} \implies \text{标准化处理} \implies \text{正态分布近似} \implies \text{解决实际概率计算题}}
$$</p>
<h2>4.框框老师</h2>
<p><img src="https://img.picr2.online/1782218453369_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782218691569_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>数理统计基本概念</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E6%95%B0%E7%90%86%E7%BB%9F%E8%AE%A1%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E6%95%B0%E7%90%86%E7%BB%9F%E8%AE%A1%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</id>
    <published>2026-06-23T11:14:04.000Z</published>
    <updated>2026-06-23T11:14:04.000Z</updated>
    <summary>系统梳理数理统计中的基本概念，包括总体与个体、简单随机样本、统计量、三大抽样分布以及估计量的无偏性、有效性与相合性等标准。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1164101528&amp;p=8" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理数理统计中的<strong>基本概念</strong>。数理统计是一门利用样本数据推断总体概率分布规律的学科，它是参数估计和假设检验的核心基石。</p>
<hr />
<h2>1. 总体与个体</h2>
<ul>
<li><strong>总体</strong>：数理统计研究对象的全体，通常用一个随机变量 $X$ 来表示其概率分布。</li>
<li><strong>个体</strong>：组成总体的每一个研究对象。</li>
</ul>
<p>例如：</p>
<ul>
<li>研究某批零件的使用寿命，这批零件的寿命全体构成总体，单件零件的寿命为个体；</li>
<li>研究某学校学生的身高，该校所有学生的身高数据构成总体。</li>
</ul>
<hr />
<h2>2. 样本与样本容量</h2>
<p>从总体中随机抽取一部分个体，将其测量得到的数据组合称为<strong>样本</strong>。</p>
<p>设随机抽取了 $n$ 个个体，其对应的测量结果为：</p>
<p>$$
X_1, X_2, \dots, X_n
$$</p>
<p>这组随机变量即为样本，其中个体的个数 $n$ 称为<strong>样本容量</strong>。</p>
<hr />
<h2>3. 简单随机样本</h2>
<p>若抽取的样本 $X_1, X_2, \dots, X_n$ 满足以下两个核心条件，则称为<strong>简单随机样本</strong>：</p>
<ol>
<li><strong>同分布性</strong>：每个 $X_i$ 与总体 $X$ 具有完全相同的概率分布；</li>
<li><strong>独立性</strong>：各随机变量之间相互独立。</li>
</ol>
<p>即样本变量之间是独立同分布的，常记作：</p>
<p>$$
X_1, X_2, \dots, X_n \overset{i.i.d.}{\sim} F
$$</p>
<p>其中 $F$ 为总体的概率分布函数。</p>
<hr />
<h2>4. 样本观测值</h2>
<ul>
<li><strong>随机样本</strong> $X_1, X_2, \dots, X_n$ 在抽样之前是随机变量；</li>
<li>抽样实施之后，我们获得了具体的一组实数值 $x_1, x_2, \dots, x_n$，这组数值称为<strong>样本观测值</strong>。</li>
</ul>
<p>例如，随机测量了 5 位学生的身高，获得数据：<code>[168, 172, 165, 180, 175]</code>，这就是一组具体的样本观测值。在公式推导中，大写字母 $X_i$ 代表随机变量，小写字母 $x_i$ 代表具体的观测值。</p>
<hr />
<h2>5. 统计量</h2>
<p>若一个关于样本的函数不包含任何未知的总体参数，则称该函数为<strong>统计量</strong>。</p>
<p>设 $T = T(X_1, X_2, \dots, X_n)$ 是关于样本的函数。若 $T$ 中不含有任何未知参数，则 $T$ 是一个统计量。统计量本身也是一个随机变量，其在抽样后代入观测值可计算出一个具体的实数值。</p>
<p>常见的统计量包括：样本均值、样本方差、样本标准差、样本最大/最小值以及样本中位数等。</p>
<hr />
<h2>6. 样本均值</h2>
<p>样本均值定义为：</p>
<p>$$
\boxed{\overline{X} = \frac{1}{n}\sum_{i=1}^{n}X_i}
$$</p>
<p>其具体的观测值对应为：</p>
<p>$$
\overline{x} = \frac{1}{n}\sum_{i=1}^{n}x_i
$$</p>
<p>样本均值常用于作为总体均值 $\mu = E(X)$ 的估计。它满足以下两个重要数学性质：</p>
<p>$$
E(\overline{X}) = \mu
$$</p>
<p>$$
D(\overline{X}) = \frac{\sigma^2}{n}
$$</p>
<p>这说明样本容量 $n$ 越大，样本均值偏离总体均值的波动程度就越小。</p>
<hr />
<h2>7. 样本方差</h2>
<p>样本方差定义为：</p>
<p>$$
\boxed{S^2 = \frac{1}{n-1}\sum_{i=1}^{n}(X_i - \overline{X})^2}
$$</p>
<p>其观测值对应为：</p>
<p>$$
s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \overline{x})^2
$$</p>
<p>需要特别注意，其计算公式中的分母是 <strong>$n-1$</strong>，而不是 $n$。这是因为在计算样本方差时使用样本均值 $\overline{X}$ 代替了未知的总体均值 $\mu$，这导致样本数据中损失了一个自由度。如此设定能保证样本方差的期望等于总体方差：</p>
<p>$$
E(S^2) = \sigma^2
$$</p>
<p>即保证了样本方差是总体方差的无偏估计量。</p>
<hr />
<h2>8. 样本标准差</h2>
<p>样本标准差定义为样本方差的算术平方根：</p>
<p>$$
\boxed{S = \sqrt{S^2} = \sqrt{\frac{1}{n-1}\sum_{i=1}^{n}(X_i - \overline{X})^2}}
$$</p>
<p>它常用于度量样本数据点围绕样本均值的离散分布程度。</p>
<hr />
<h2>9. 样本矩</h2>
<p>类似于总体矩的概念，我们可以利用样本数据构造对应的样本矩：</p>
<ul>
<li><strong>样本 $k$ 阶原点矩</strong>：</li>
</ul>
<p>$$
\boxed{A_k = \frac{1}{n}\sum_{i=1}^{n}X_i^k}
$$</p>
<ul>
<li><strong>样本 $k$ 阶中心矩</strong>：</li>
</ul>
<p>$$
\boxed{B_k = \frac{1}{n}\sum_{i=1}^{n}(X_i - \overline{X})^k}
$$</p>
<p>由定义可知，一阶原点矩即为样本均值 $A_1 = \overline{X}$。二阶中心矩 $B_2$ 与样本方差的关系为：</p>
<p>$$
S^2 = \frac{n}{n-1}B_2
$$</p>
<hr />
<h2>10. 次序统计量</h2>
<p>将样本观测值 $X_1, X_2, \dots, X_n$ 按从小到大的顺序进行排列，得到：</p>
<p>$$
X_{(1)} \le X_{(2)} \le \cdots \le X_{(n)}
$$</p>
<p>则称随机变量 $X_{(1)}, X_{(2)}, \dots, X_{(n)}$ 为该样本的<strong>次序统计量</strong>。</p>
<ul>
<li>样本最小值：$X_{(1)} = \min(X_1, \dots, X_n)$</li>
<li>样本最大值：$X_{(n)} = \max(X_1, \dots, X_n)$</li>
</ul>
<p>利用最大与最小值，我们可以定义样本的<strong>极差</strong> $R$：</p>
<p>$$
\boxed{R = X_{(n)} - X_{(1)}}
$$</p>
<hr />
<h2>11. 抽样分布</h2>
<p>由于统计量是关于样本随机变量 of the sample 的函数，其本身也是一个随机变量，因而也具有相应的概率分布。<strong>统计量的概率分布称为抽样分布</strong>。</p>
<p>例如，当总体服从正态分布 $X \sim N(\mu, \sigma^2)$ 时，样本均值这一统计量的抽样分布为：</p>
<p>$$
\overline{X} \sim N\left(\mu, \frac{\sigma^2}{n}\right)
$$</p>
<hr />
<h2>12. 数理统计中的三大典型抽样分布</h2>
<p>这三大抽样分布均由标准正态分布衍生而来，是数理统计推断的核心基石：</p>
<h3>12.1 卡方分布（$\chi^2$ 分布）</h3>
<p>设 $Z_1, Z_2, \dots, Z_n$ 相互独立，且均服从标准正态分布 $Z_i \sim N(0, 1)$。则它们的平方和：</p>
<p>$$
\boxed{\chi^2 = Z_1^2 + Z_2^2 + \cdots + Z_n^2}
$$</p>
<p>服从自由度为 $n$ 的卡方分布，记作 $\chi^2 \sim \chi^2(n)$。</p>
<h3>12.2 学生分布（$t$ 分布）</h3>
<p>设随机变量 $Z \sim N(0, 1)$，随机变量 $U \sim \chi^2(n)$，且 $Z$ 与 $U$ 相互独立。则其比值：</p>
<p>$$
\boxed{T = \frac{Z}{\sqrt{U/n}}}
$$</p>
<p>服从自由度为 $n$ 的 $t$ 分布，记作 $T \sim t(n)$。随着自由度 $n$ 的增大，$t$ 分布曲线会逐渐逼近标准正态分布曲线。</p>
<h3>12.3 $F$ 分布</h3>
<p>设随机变量 $U \sim \chi^2(m)$，随机变量 $V \sim \chi^2(n)$，且 $U$ 与 $V$ 相互独立。则其比值：</p>
<p>$$
\boxed{F = \frac{U/m}{V/n}}
$$</p>
<p>服从自由度为 $(m, n)$ 的 $F$ 分布，记作 $F \sim F(m, n)$。</p>
<hr />
<h2>13. 单个正态总体下的常用统计量结论</h2>
<p>设样本 $X_1, X_2, \dots, X_n$ 来自正态总体 $N(\mu, \sigma^2)$，则有以下五个极为重要的统计量性质：</p>
<ol>
<li><strong>均值抽样分布</strong>：</li>
</ol>
<p>$$
\boxed{\overline{X} \sim N\left(\mu, \frac{\sigma^2}{n}\right)}
$$</p>
<ol>
<li><strong>标准化均值</strong>（$\sigma^2$ 已知）：</li>
</ol>
<p>$$
\boxed{\frac{\overline{X} - \mu}{\sigma/\sqrt{n}} \sim N(0, 1)}
$$</p>
<ol>
<li><strong>样本方差的卡方性质</strong>：</li>
</ol>
<p>$$
\boxed{\frac{(n-1)S^2}{\sigma^2} \sim \chi^2(n-1)}
$$</p>
<ol>
<li><strong>样本方差替代后的 $t$ 分布性质</strong>（$\sigma^2$ 未知）：</li>
</ol>
<p>$$
\boxed{\frac{\overline{X} - \mu}{S/\sqrt{n}} \sim t(n-1)}
$$</p>
<ol>
<li><strong>独立性定理</strong>：</li>
</ol>
<p>$$
\boxed{\overline{X} \text{ 与 } S^2 \text{ 相互独立}}
$$</p>
<p><em>(注意：此独立性质仅在总体为正态分布时成立)</em></p>
<hr />
<h2>14. 未知参数与参数估计</h2>
<ul>
<li><strong>参数</strong>：总体概率分布中包含的特征常数（如正态分布中的 $\mu$ 和 $\sigma^2$）。</li>
<li><strong>参数估计</strong>：在参数未知的情况下，利用样本计算得到的统计量作为未知参数的近似推断。</li>
</ul>
<p>参数估计包括<strong>点估计</strong>（用一个确定的数值代表参数）与<strong>区间估计</strong>（构造一个以高概率包含参数的范围区间）。</p>
<hr />
<h2>15. 估计量的三大评价标准</h2>
<ol>
<li><strong>无偏性</strong>：估计量的期望值等于参数真实值，即 $E(\hat{\theta}) = \theta$；</li>
<li><strong>有效性</strong>：在所有无偏估计量中，方差越小（估计波动越稳定）的估计量越有效；</li>
<li><strong>相合性</strong>：当样本容量 $n \to \infty$ 时，估计量依概率收敛于未知参数的真实值 $\theta$。</li>
</ol>
<hr />
<h2>16. 假设检验基本思想</h2>
<p>假设检验是通过样本数据对关于总体的某种假设命题进行决策的过程：</p>
<ol>
<li><strong>提出假设</strong>：建立原假设 $H_0$（代表无差异、厂家说法成立）与备择假设 $H_1$；</li>
<li><strong>设定显著性水平</strong> $\alpha$：规定犯第一类错误（即原假设真实却将其拒绝）的最大概率限度；</li>
<li><strong>确定决策域</strong>：利用抽样分布设定拒绝域，若样本计算值落入拒绝域，则拒绝原假设 $H_0$。</li>
</ol>
<hr />
<h2>17. 本章核心公式速查表</h2>
<table>
<thead>
<tr>
<th>核心结论名称</th>
<th>对应数学公式表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>样本均值定义式</strong></td>
<td>$\overline{X} = \dfrac{1}{n}\sum\limits_{i=1}^n X_i$</td>
</tr>
<tr>
<td><strong>样本方差定义式</strong></td>
<td>$S^2 = \dfrac{1}{n-1}\sum\limits_{i=1}^n (X_i - \overline{X})^2$</td>
</tr>
<tr>
<td><strong>均值统计量期望与方差</strong></td>
<td>$E(\overline{X}) = \mu, \quad D(\overline{X}) = \frac{\sigma^2}{n}$</td>
</tr>
<tr>
<td><strong>样本方差无偏期望</strong></td>
<td>$E(S^2) = \sigma^2$</td>
</tr>
<tr>
<td><strong>方差的卡方统计量构造</strong></td>
<td>$\frac{(n-1)S^2}{\sigma^2} \sim \chi^2(n-1)$</td>
</tr>
<tr>
<td><strong>方差未知时的均值 $t$ 统计量</strong></td>
<td>$\frac{\overline{X} - \mu}{S/\sqrt{n}} \sim t(n-1)$</td>
</tr>
<tr>
<td><strong>$F$ 分布统计量构造</strong></td>
<td>$F = \frac{U/m}{V/n}$</td>
</tr>
</tbody>
</table>
<hr />
<h2>18. 本章学习主线</h2>
<p>关于数理统计基本概念的学习逻辑，可以通过以下链路进行贯通掌握：</p>
<p>$$
\boxed{\text{总体分布} \implies \text{样本抽样} \implies \text{构造统计量} \implies \text{推导抽样分布} \implies \text{开展参数估计与假设检验}}
$$</p>
<h2>19. 框框老师</h2>
<p><img src="https://img.picr2.online/1782219016906_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782219335422_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>随机变量的数字特征</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E7%9A%84%E6%95%B0%E5%AD%97%E7%89%B9%E5%BE%81/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E7%9A%84%E6%95%B0%E5%AD%97%E7%89%B9%E5%BE%81/</id>
    <published>2026-06-22T11:26:44.000Z</published>
    <updated>2026-06-22T11:26:44.000Z</updated>
    <summary>系统梳理随机变量的数学期望、方差、协方差、相关系数以及矩的定义与性质，提供常见分布特征速查、常考题型解析与易错陷阱总结。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1163062472&amp;p=7" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理<strong>随机变量的数字特征</strong>。这一章的核心思想可以总结为：</p>
<p>$$
\boxed{\text{用几个关键的数值来概括随机变量分布的主要特征}}
$$</p>
<p>前面的分布律、概率密度函数和累积分布函数能够完整地描述随机变量的概率分布。然而，在实际问题中，人们往往更需要从分布中提取出最具有代表性的指标，例如：</p>
<ul>
<li>衡量平均水平：<strong>数学期望</strong>；</li>
<li>衡量波动程度：<strong>方差</strong>与<strong>标准差</strong>；</li>
<li>衡量两个随机变量的相关关系：<strong>协方差</strong>与<strong>相关系数</strong>；</li>
<li>更高阶的特征描述：<strong>矩</strong>。</li>
</ul>
<hr />
<h2>1. 数学期望</h2>
<p>数学期望也常称为<strong>均值</strong>，它描述的是随机变量取值的平均水平，记作：</p>
<p>$$
E(X)
$$</p>
<p>需要明确的是，数学期望并不代表随机变量 $X$ 一定会取到这个数值，而是代表在大量重复试验后，随机变量取值的长期平均水平。</p>
<hr />
<h3>1.1 离散型随机变量的数学期望</h3>
<p>若离散型随机变量 $X$ 的分布律为：</p>
<p>$$
P(X = x_i) = p_i, \quad i = 1, 2, \dots
$$</p>
<p>则 $X$ 的数学期望定义为：</p>
<p>$$
\boxed{E(X) = \sum_i x_i p_i}
$$</p>
<p>这里要求级数必须是绝对收敛的，即 $\sum_i |x_i| p_i$ 存在。</p>
<p>简要理解：</p>
<p>$$
\boxed{\text{期望} = \text{取值} \times \text{对应概率}\text{，}\text{然后进行求和}}
$$</p>
<h4>计算示例</h4>
<p>掷一个均匀的骰子，令随机变量 $X$ 表示掷出的点数。</p>
<p>易知 $X$ 的分布律为：</p>
<p>$$
P(X = 1) = P(X = 2) = \dots = P(X = 6) = \frac{1}{6}
$$</p>
<p>则其数学期望为：</p>
<p>$$
E(X) = 1 \cdot \frac{1}{6} + 2 \cdot \frac{1}{6} + \dots + 6 \cdot \frac{1}{6}
$$</p>
<p>$$
E(X) = \frac{1 + 2 + 3 + 4 + 5 + 6}{6} = \frac{21}{6} = 3.5
$$</p>
<p>骰子的点数只可能是整数，绝不可能掷出 3.5 这个值，但 3.5 代表了长期掷骰子得到的点数平均值。</p>
<hr />
<h3>1.2 连续型随机变量的数学期望</h3>
<p>若连续型随机变量 $X$ 的概率密度函数为 $f(x)$，则其数学期望定义为：</p>
<p>$$
\boxed{E(X) = \int_{-\infty}^{+\infty} x f(x),dx}
$$</p>
<p>要求该积分绝对收敛。</p>
<p>简要理解：</p>
<p>$$
\boxed{\text{连续型期望：将离散型的求和符号替换为积分运算}}
$$</p>
<h4>计算示例</h4>
<p>设随机变量 $X$ 在区间 $[0, 1]$ 上服从均匀分布，即：</p>
<p>$$
X \sim U(0, 1)
$$</p>
<p>其概率密度函数为：</p>
<p>$$
f(x) =
\begin{cases}
1, &amp; 0 &lt; x &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>根据公式计算其数学期望：</p>
<p>$$
E(X) = \int_0^1 x \cdot 1,dx = \left[ \frac{1}{2}x^2 \right]_0^1 = \frac{1}{2}
$$</p>
<hr />
<h2>2. 随机变量函数的期望</h2>
<p>在很多实际计算中，我们并不是直接求解 $E(X)$，而是需要求解由 $X$ 通过某种函数关系 $g(X)$ 得到的新随机变量的期望：</p>
<p>$$
E[g(X)]
$$</p>
<p>例如常见的：$E(X^2)$、$E(2X + 1)$、$E(e^X)$。</p>
<p>求此类期望时，无需先推导新随机变量 $Y = g(X)$ 的概率分布，直接利用下述公式进行计算即可。</p>
<hr />
<h3>2.1 离散型情况</h3>
<p>若 $X$ 的分布律为 $P(X = x_i) = p_i$，则其函数的期望为：</p>
<p>$$
\boxed{E[g(X)] = \sum_i g(x_i)p_i}
$$</p>
<hr />
<h3>2.2 连续型情况</h3>
<p>若 $X$ 的密度函数为 $f(x)$，则其函数的期望为：</p>
<p>$$
\boxed{E[g(X)] = \int_{-\infty}^{+\infty} g(x)f(x),dx}
$$</p>
<p>这两个期望求法极其重要，特别是在计算方差时，通常需要优先套用此公式求出 $E(X^2)$。</p>
<hr />
<h2>3. 数学期望的性质</h2>
<h3>性质一：常数的期望</h3>
<p>常数不具备随机性，其期望即为常数本身。</p>
<p>$$
\boxed{E(c) = c}
$$</p>
<hr />
<h3>性质二：线性性质</h3>
<p>数学期望是一个线性算子，满足如下拆分规则：</p>
<p>$$
\boxed{E(aX + b) = aE(X) + b}
$$</p>
<p>对于两个随机变量的和：</p>
<p>$$
\boxed{E(aX + bY) = aE(X) + bE(Y)}
$$</p>
<p>需要特别提醒的是：<strong>此线性性质对任意随机变量 $X, Y$ 均成立，不需要它们相互独立。</strong></p>
<hr />
<h3>性质三：独立变量乘积的期望</h3>
<p>如果随机变量 $X$ 与 $Y$ 相互独立，则它们乘积的期望等于各自期望的乘积：</p>
<p>$$
\boxed{E(XY) = E(X)E(Y)}
$$</p>
<p>注意其反面结论：<strong>当满足 $E(XY) = E(X)E(Y)$ 时，并不能反向推导得出 $X, Y$ 相互独立。</strong></p>
<hr />
<h2>4. 方差</h2>
<p>均值描述的是随机变量的平均水平，但无法反映随机变量取值在其均值周围波动的剧烈程度。</p>
<p>例如两个学生的成绩数据：</p>
<ul>
<li>学生甲：$80, 80, 80$</li>
<li>学生乙：$60, 80, 100$</li>
</ul>
<p>他们的平均分都是 80 分，但显然学生乙的成绩波动性、不确定性大得多。</p>
<p>为了刻画这种偏差程度，引入<strong>方差</strong>。</p>
<p>方差记作：</p>
<p>$$
D(X) \quad \text{或} \quad \operatorname{Var}(X)
$$</p>
<p>其定义为：</p>
<p>$$
\boxed{D(X) = E\left[(X - E(X))^2\right]}
$$</p>
<p>它代表随机变量取值与其期望值之间偏差平方的平均水平。</p>
<hr />
<h3>方差的常用计算公式</h3>
<p>在实际计算中，利用定义直接求方差往往比较繁琐，通常采用以下推导出的化简公式：</p>
<p>$$
\boxed{D(X) = E(X^2) - [E(X)]^2}
$$</p>
<p>这是求解方差最常用的黄金公式。</p>
<p>做题步骤通常分为三步：</p>
<ol>
<li>算出 $E(X)$；</li>
<li>算出 $E(X^2)$；</li>
<li>代入公式 $D(X) = E(X^2) - [E(X)]^2$ 得到最终结果。</li>
</ol>
<hr />
<h3>计算示例</h3>
<p>我们来求解掷骰子点数 $X$ 的方差。</p>
<p>已知期望 $E(X) = 3.5 = \frac{7}{2}$。</p>
<p>首先计算 $E(X^2)$：</p>
<p>$$
E(X^2) = 1^2 \cdot \frac{1}{6} + 2^2 \cdot \frac{1}{6} + \dots + 6^2 \cdot \frac{1}{6}
$$</p>
<p>$$
E(X^2) = \frac{1 + 4 + 9 + 16 + 25 + 36}{6} = \frac{91}{6}
$$</p>
<p>然后代入常用公式计算方差：</p>
<p>$$
D(X) = E(X^2) - [E(X)]^2 = \frac{91}{6} - \left(\frac{7}{2}\right)^2
$$</p>
<p>$$
D(X) = \frac{91}{6} - \frac{49}{4} = \frac{182 - 147}{12} = \frac{35}{12}
$$</p>
<hr />
<h2>5. 标准差</h2>
<p>由于方差公式中含有平方项，导致方差的量纲（单位）是原随机变量单位的平方。例如当随机变量表示身高（单位为厘米）时，其方差的单位变成了平方厘米，在直观上不便于比较。</p>
<p>因此引入<strong>标准差</strong>，记作 $\sigma(X)$：</p>
<p>$$
\boxed{\sigma(X) = \sqrt{D(X)}}
$$</p>
<p>标准差的单位与随机变量本身的单位完全一致，能更直观地反映随机变量偏离均值的实际波动幅度。</p>
<hr />
<h2>6. 方差的性质</h2>
<h3>性质一：常数的方差为 0</h3>
<p>常数没有波动性，其方差必然为 0。</p>
<p>$$
\boxed{D(c) = 0}
$$</p>
<hr />
<h3>性质二：平移不改变方差</h3>
<p>将随机变量整体加上一个常数，只是整体平移，并不改变其取值的离散程度（波动幅度）：</p>
<p>$$
\boxed{D(X + b) = D(X)}
$$</p>
<hr />
<h3>性质三：倍数呈平方系数影响方差</h3>
<p>自变量放大 $a$ 倍，其对应的波动偏差的平方则会放大 $a^2$ 倍：</p>
<p>$$
\boxed{D(aX) = a^2 D(X)}
$$</p>
<p>结合性质二，可以得到常用的复合线性公式：</p>
<p>$$
\boxed{D(aX + b) = a^2 D(X)}
$$</p>
<hr />
<h3>性质四：两个随机变量和与差的方差</h3>
<p>在一般情况下，和的方差公式中必须包含它们之间的协方差项：</p>
<p>$$
\boxed{D(X + Y) = D(X) + D(Y) + 2\operatorname{Cov}(X, Y)}
$$</p>
<p>如果随机变量 $X$ 与 $Y$ 相互独立，则协方差项为 0，公式化简为：</p>
<p>$$
\boxed{D(X + Y) = D(X) + D(Y)}
$$</p>
<p>同理，当随机变量相互独立时，差的方差公式也为加号：</p>
<p>$$
\boxed{D(X - Y) = D(X) + D(Y)}
$$</p>
<p>注意：<strong>无论独立时是相加还是相减，方差均是直接求和，没有减法。</strong></p>
<hr />
<h2>7. 常见分布的期望和方差</h2>
<p>熟练记忆常见分布的期望与方差，在做选择题、填空题时可以极大地提高解题速度。</p>
<table>
<thead>
<tr>
<th>分布类型</th>
<th>记号</th>
<th>数学期望 $E(X)$</th>
<th>方差 $D(X)$</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>两点分布</strong></td>
<td>$X \sim B(1, p)$</td>
<td>$p$</td>
<td>$p(1 - p)$</td>
</tr>
<tr>
<td><strong>二项分布</strong></td>
<td>$X \sim B(n, p)$</td>
<td>$np$</td>
<td>$np(1 - p)$</td>
</tr>
<tr>
<td><strong>泊松分布</strong></td>
<td>$X \sim P(\lambda)$</td>
<td>$\lambda$</td>
<td>$\lambda$</td>
</tr>
<tr>
<td><strong>均匀分布</strong></td>
<td>$X \sim U(a, b)$</td>
<td>$\dfrac{a + b}{2}$</td>
<td>$\dfrac{(b - a)^2}{12}$</td>
</tr>
<tr>
<td><strong>指数分布</strong></td>
<td>$X \sim E(\lambda)$</td>
<td>$\dfrac{1}{\lambda}$</td>
<td>$\dfrac{1}{\lambda^2}$</td>
</tr>
<tr>
<td><strong>正态分布</strong></td>
<td>$X \sim N(\mu, \sigma^2)$</td>
<td>$\mu$</td>
<td>$\sigma^2$</td>
</tr>
</tbody>
</table>
<hr />
<h2>8. 协方差</h2>
<p>方差用来研究一个随机变量自身的波动程度。当我们需要同时考察两个随机变量，研究它们之间共同变化的关联关系时，就需要使用<strong>协方差</strong>。</p>
<p>协方差记作 $\operatorname{Cov}(X, Y)$，其定义为：</p>
<p>$$
\boxed{\operatorname{Cov}(X, Y) = E\left[(X - E(X))(Y - E(Y))\right]}
$$</p>
<p>实际计算中，常使用以下计算公式：</p>
<p>$$
\boxed{\operatorname{Cov}(X, Y) = E(XY) - E(X)E(Y)}
$$</p>
<hr />
<h3>协方差的物理意义</h3>
<ul>
<li><strong>$\operatorname{Cov}(X, Y) &gt; 0$</strong>：表示 $X$ 与 $Y$ 呈同向变化趋势（正相关），即 $X$ 变大时 $Y$ 往往也偏大。</li>
<li><strong>$\operatorname{Cov}(X, Y) &lt; 0$</strong>：表示 $X$ 与 $Y$ 呈反向变化趋势（负相关），即 $X$ 变大时 $Y$ 往往偏小。</li>
<li><strong>$\operatorname{Cov}(X, Y) = 0$</strong>：表示 $X$ 与 $Y$ 不存在线性相关关系。</li>
</ul>
<p>关于独立与相关的核心区别，必须牢记：</p>
<p>$$
\boxed{X, Y \text{ 相互独立} \implies \operatorname{Cov}(X, Y) = 0 \quad (\text{即不线性相关})}
$$</p>
<p>$$
\boxed{\operatorname{Cov}(X, Y) = 0 \quad \text{无法推导得出 } X, Y \text{ 相互独立}}
$$</p>
<p>也就是说，独立是强条件，不相关是弱条件。独立一定不相关，不相关不一定独立。</p>
<hr />
<h2>9. 相关系数</h2>
<p>协方差包含了两随机变量本身的量纲单位，其绝对值的大小易受到变量本身取值尺度的影响，不利于横向对比相关程度。</p>
<p>因此对协方差进行无量纲标准化处理，引入<strong>相关系数</strong> $\rho_{XY}$：</p>
<p>$$
\boxed{\rho_{XY} = \frac{\operatorname{Cov}(X, Y)}{\sqrt{D(X)}\sqrt{D(Y)}}}
$$</p>
<p>相关系数的数值范围被限定在：</p>
<p>$$
-1 \le \rho_{XY} \le 1
$$</p>
<hr />
<h3>相关系数的取值含义</h3>
<table>
<thead>
<tr>
<th>相关系数值</th>
<th>含义说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>$\rho_{XY} &gt; 0$</td>
<td>$X$ 与 $Y$ 之间存在正线性相关关系</td>
</tr>
<tr>
<td>$\rho_{XY} &lt; 0$</td>
<td>$X$ 与 $Y$ 之间存在负线性相关关系</td>
</tr>
<tr>
<td>$\rho_{XY} = 0$</td>
<td>$X$ 与 $Y$ 之间不存在线性相关关系，即不相关</td>
</tr>
<tr>
<td>$\rho_{XY} = 1$</td>
<td>$X$ 与 $Y$ 之间存在完全正线性相关关系（即存在 $Y = aX + b, a &gt; 0$ 的确定关系）</td>
</tr>
<tr>
<td>$\rho_{XY} = -1$</td>
<td>$X$ 与 $Y$ 之间存在完全负线性相关关系（即存在 $Y = aX + b, a &lt; 0$ 的确定关系）</td>
</tr>
</tbody>
</table>
<p>注意：相关系数只衡量两个随机变量之间的<strong>线性关系</strong>，若两个随机变量之间存在非线性的确定关系（例如抛物线关系 $Y = X^2$），它们的相关系数仍可能为 0。</p>
<hr />
<h2>10. 矩</h2>
<p>矩是比期望、方差更为一般化的数字特征。</p>
<h3>10.1 原点矩</h3>
<p>随机变量 $X$ 的 $k$ 阶原点矩定义为：</p>
<p>$$
\boxed{\alpha_k = E(X^k) \quad (k = 1, 2, \dots)}
$$</p>
<ul>
<li>一阶原点矩即为数学期望：$E(X)$；</li>
<li>二阶原点矩为平方的期望：$E(X^2)$。</li>
</ul>
<hr />
<h3>10.2 中心矩</h3>
<p>随机变量 $X$ 的 $k$ 阶中心矩定义为：</p>
<p>$$
\boxed{\mu_k = E\left[(X - E(X))^k\right] \quad (k = 1, 2, \dots)}
$$</p>
<ul>
<li>一阶中心矩恒等于 0：$E\left[X - E(X)\right] = 0$；</li>
<li>二阶中心矩即为方差：$D(X) = E\left[(X - E(X))^2\right]$；</li>
<li>三阶、四阶中心矩分别在统计学中与分布的偏度及峰度密切相关。</li>
</ul>
<hr />
<h2>11. 二维随机变量中的数字特征</h2>
<p>若题目中给出的是二维随机变量 $(X, Y)$，考生通常需要求解以下六个核心数字特征：</p>
<p>$$
E(X), \quad E(Y), \quad D(X), \quad D(Y), \quad \operatorname{Cov}(X, Y), \quad \rho_{XY}
$$</p>
<p>这其中，最关键的中间计算量是<strong>乘积的期望</strong> $E(XY)$，通过它代入公式求得协方差：</p>
<p>$$
\boxed{\operatorname{Cov}(X, Y) = E(XY) - E(X)E(Y)}
$$</p>
<hr />
<h2>12. 离散型二维随机变量求数字特征</h2>
<p>若已知离散型随机变量 $(X, Y)$ 的联合分布律为 $P(X = x_i, Y = y_j) = p_{ij}$，则各数字特征通过双重求和公式进行计算：</p>
<ul>
<li>均值求法：</li>
</ul>
<p>$$
\boxed{E(X) = \sum_i \sum_j x_i p_{ij}}
$$</p>
<p>$$
\boxed{E(Y) = \sum_i \sum_j y_j p_{ij}}
$$</p>
<ul>
<li>乘积期望求法：</li>
</ul>
<p>$$
\boxed{E(XY) = \sum_i \sum_j x_i y_j p_{ij}}
$$</p>
<p>进而求出各自的方差与协方差：</p>
<p>$$
D(X) = E(X^2) - [E(X)]^2 = \sum_i \sum_j x_i^2 p_{ij} - [E(X)]^2
$$</p>
<p>$$
D(Y) = E(Y^2) - [E(Y)]^2 = \sum_i \sum_j y_j^2 p_{ij} - [E(Y)]^2
$$</p>
<p>$$
\operatorname{Cov}(X, Y) = E(XY) - E(X)E(Y)
$$</p>
<hr />
<h2>13. 连续型二维随机变量求数字特征</h2>
<p>若已知连续型随机变量 $(X, Y)$ 的联合概率密度函数为 $f(x, y)$，则数字特征通过二重积分公式进行计算：</p>
<ul>
<li>均值求法：</li>
</ul>
<p>$$
\boxed{E(X) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} x f(x, y),dx,dy}
$$</p>
<p>$$
\boxed{E(Y) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} y f(x, y),dx,dy}
$$</p>
<ul>
<li>乘积期望求法：</li>
</ul>
<p>$$
\boxed{E(XY) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} xy f(x, y),dx,dy}
$$</p>
<p>同理求出 $E(X^2)$ 和 $E(Y^2)$：</p>
<p>$$
E(X^2) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} x^2 f(x, y),dx,dy
$$</p>
<p>然后代入常用公式计算方差与协方差。</p>
<hr />
<h2>14. 典型例题解析</h2>
<p>设离散型随机变量 $X$ 的分布律表格如下：</p>
<p>$$
\begin{array}{c|ccc}
X &amp; 0 &amp; 1 &amp; 2 \
\hline
P &amp; 0.2 &amp; 0.5 &amp; 0.3
\end{array}
$$</p>
<p>我们要求出其期望 $E(X)$ 与方差 $D(X)$。</p>
<hr />
<h3>第一步：计算数学期望 $E(X)$</h3>
<p>根据期望公式：</p>
<p>$$
E(X) = 0 \cdot 0.2 + 1 \cdot 0.5 + 2 \cdot 0.3
$$</p>
<p>$$
E(X) = 0 + 0.5 + 0.6 = 1.1
$$</p>
<hr />
<h3>第二步：计算二阶原点矩 $E(X^2)$</h3>
<p>利用函数期望公式：</p>
<p>$$
E(X^2) = 0^2 \cdot 0.2 + 1^2 \cdot 0.5 + 2^2 \cdot 0.3
$$</p>
<p>$$
E(X^2) = 0 + 0.5 + 1.2 = 1.7
$$</p>
<hr />
<h3>第三步：套用常用公式求方差</h3>
<p>代入公式 $D(X) = E(X^2) - [E(X)]^2$：</p>
<p>$$
D(X) = 1.7 - (1.1)^2 = 1.7 - 1.21 = 0.49
$$</p>
<p>所以，最终答案为：</p>
<p>$$
\boxed{E(X) = 1.1, \quad D(X) = 0.49}
$$</p>
<hr />
<h2>15. 常考题型总结</h2>
<h3>题型一：已知单变量分布，求期望与方差</h3>
<p><strong>求解步骤</strong>：</p>
<ol>
<li>离散型利用 $E(X) = \sum x_i p_i$，连续型利用 $E(X) = \int x f(x),dx$ 计算出均值；</li>
<li>利用函数期望求出 $E(X^2)$；</li>
<li>代入 $D(X) = E(X^2) - [E(X)]^2$。</li>
</ol>
<h3>题型二：求随机变量函数的期望 $E[g(X)]$</h3>
<p><strong>求解方法</strong>：千万不要先去费力推导新变量 $Y = g(X)$ 的分布，直接将函数关系 $g(x)$ 带入到原分布的积分或求和式中计算，即 $\sum g(x_i) p_i$ 或 $\int g(x) f(x),dx$。</p>
<h3>题型三：求解二维联合分布的协方差与相关系数</h3>
<p><strong>求解步骤</strong>：</p>
<ol>
<li>分别求出 $E(X)$ 和 $E(Y)$；</li>
<li>根据联合分布律或联合概率密度函数，计算二重积分或双重求和得到 $E(XY)$；</li>
<li>利用 $\operatorname{Cov}(X, Y) = E(XY) - E(X)E(Y)$ 计算协方差；</li>
<li>若要求相关系数，需额外求出 $D(X)$ 与 $D(Y)$ 后代入分式求解。</li>
</ol>
<h3>题型四：判断随机变量的相关性</h3>
<p><strong>求解方法</strong>：计算协方差 $\operatorname{Cov}(X, Y)$，若等于 0 则二者不相关；若不等于 0 则二者相关。</p>
<hr />
<h2>16. 易错点总结</h2>
<h3>易错点一：误认为数学期望就是最可能取的值</h3>
<p>数学期望反映的是均值，但在很多离散分布中，随机变量根本无法取到期望值（如抛硬币正面次数的期望为 0.5，掷骰子点数均值为 3.5）。</p>
<h3>易错点二：混淆方差公式的减数与被减数</h3>
<p>计算方差时，常用公式为 $E(X^2) - [E(X)]^2$，考生容易将其记反，或者误认为方差直接等于二阶原点矩 $E(X^2)$。</p>
<h3>易错点三：计算独立随机变量差的方差时写成减法</h3>
<p>若 $X, Y$ 相互独立，则有：</p>
<p>$$
D(X - Y) = D(X) + D(Y)
$$</p>
<p>许多考生会由于减号而习惯性地写成 $D(X) - D(Y)$，导致计算错误。</p>
<h3>易错点四：将不相关等同于相互独立</h3>
<p>必须注意：协方差为 0 仅代表它们不存在线性相关性，不代表它们没有其他函数关系，因此无法推导得出独立。只有当随机变量服从二维正态分布时，不相关才与相互独立完全等价。</p>
<hr />
<h2>17. 本章核心公式表</h2>
<table>
<thead>
<tr>
<th>数字特征</th>
<th>离散型公式写法</th>
<th>连续型公式写法</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数学期望 $E(X)$</strong></td>
<td>$\sum_i x_i p_i$</td>
<td>$\int_{-\infty}^{+\infty} x f(x),dx$</td>
</tr>
<tr>
<td><strong>函数期望 $E[g(X)]$</strong></td>
<td>$\sum_i g(x_i) p_i$</td>
<td>$\int_{-\infty}^{+\infty} g(x) f(x),dx$</td>
</tr>
<tr>
<td><strong>方差定义式</strong></td>
<td>$E\left[(X - E(X))^2\right]$</td>
<td>$E\left[(X - E(X))^2\right]$</td>
</tr>
<tr>
<td><strong>方差计算常用公式</strong></td>
<td>$E(X^2) - [E(X)]^2$</td>
<td>$E(X^2) - [E(X)]^2$</td>
</tr>
<tr>
<td><strong>标准差 $\sigma(X)$</strong></td>
<td>$\sqrt{D(X)}$</td>
<td>$\sqrt{D(X)}$</td>
</tr>
<tr>
<td><strong>协方差常用公式</strong></td>
<td>$E(XY) - E(X)E(Y)$</td>
<td>$E(XY) - E(X)E(Y)$</td>
</tr>
<tr>
<td><strong>相关系数 $\rho_{XY}$</strong></td>
<td>$\dfrac{\operatorname{Cov}(X, Y)}{\sqrt{D(X)}\sqrt{D(Y)}}$</td>
<td>$\dfrac{\operatorname{Cov}(X, Y)}{\sqrt{D(X)}\sqrt{D(Y)}}$</td>
</tr>
<tr>
<td><strong>和的方差公式</strong></td>
<td>\multicolumn{2}{c}{$D(X + Y) = D(X) + D(Y) + 2\operatorname{Cov}(X, Y)$}</td>
<td></td>
</tr>
<tr>
<td><strong>乘积的期望（独立时）</strong></td>
<td>\multicolumn{2}{c}{$E(XY) = E(X)E(Y)$}</td>
<td></td>
</tr>
</tbody>
</table>
<hr />
<h2>18. 一句话掌握本章</h2>
<p>随机变量的数字特征这一章的主线可以归纳为：</p>
<p>$$
\boxed{\text{数学期望刻画平均水平} \rightarrow \text{方差度量波动离散} \rightarrow \text{协方差度量变量关联}}
$$</p>
<p>解答具体题目时的标准计算逻辑为：</p>
<ol>
<li>先求一阶原点矩 $E(X)$ 与 $E(Y)$，代表各变量的中心位置；</li>
<li>再求二阶原点矩 $E(X^2)$ 与 $E(Y^2)$，为计算波动程度打下基础；</li>
<li>代入常用化简公式求出方差 $D(X)$ 和 $D(Y)$；</li>
<li>若为二维题，进一步求出乘积期望 $E(XY)$；</li>
<li>最后利用差值公式算出协方差，进而判断二者相关性并求解相关系数。</li>
</ol>
<h2>19. 框框老师</h2>
<p><img src="https://img.picr2.online/1782204073472_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782204179820_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782204444078_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782204619770_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782204840814_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782205143901_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>二维随机变量及其分布</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E4%BA%8C%E7%BB%B4%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E5%8F%8A%E5%85%B6%E5%88%86%E5%B8%83/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E4%BA%8C%E7%BB%B4%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E5%8F%8A%E5%85%B6%E5%88%86%E5%B8%83/</id>
    <published>2026-06-22T09:07:19.000Z</published>
    <updated>2026-06-22T09:07:19.000Z</updated>
    <summary>系统梳理二维随机变量的定义、联合分布函数、二维离散型与连续型随机变量、边缘分布、条件分布、独立性以及随机变量函数的分布，并提供常考题型解析与易错点总结。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1161530648&amp;p=6" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文主要梳理<strong>二维随机变量及其分布</strong>。这一章可以理解为：一维随机变量研究的是单个随机变量的概率规律，而二维随机变量研究的是两个随机变量的联合概率规律。</p>
<p>在实际生活中，我们往往需要同时关注多个随机量。例如：</p>
<ul>
<li>掷两枚骰子：$X$ 为第一枚的点数，$Y$ 为第二枚的点数；</li>
<li>抽样检测：$X$ 为次品数，$Y$ 为合格品数；</li>
<li>学生成绩：$X$ 为数学成绩，$Y$ 为英语成绩；</li>
<li>身体指标：$X$ 为身高，$Y$ 为体重。</li>
</ul>
<p>研究它们不仅要看它们各自的规律，还要研究它们之间的关联。</p>
<hr />
<h2>1. 什么是二维随机变量</h2>
<p>如果在同一个随机试验中定义了两个随机变量 $X$ 和 $Y$，我们将它们组合在一起：</p>
<p>$$
(X, Y)
$$</p>
<p>就叫做<strong>二维随机变量</strong>，也叫<strong>二维随机向量</strong>。</p>
<p>比如掷两枚骰子，定义：</p>
<p>$$
X = \text{第一枚骰子的点数}
$$</p>
<p>$$
Y = \text{第二枚骰子的点数}
$$</p>
<p>那么 $(X, Y)$ 就是一个二维随机变量。它可能取的值有：</p>
<p>$$
(1, 1), (1, 2), \dots, (6, 6)
$$</p>
<p>共 36 种结果。</p>
<hr />
<h2>2. 联合分布函数</h2>
<p>二维随机变量最基本的分布形式是<strong>联合分布函数</strong>。</p>
<p>定义为：</p>
<p>$$
\boxed{F(x, y) = P(X \le x, Y \le y)}
$$</p>
<p>它表示随机变量 $X$ 不超过 $x$，并且随机变量 $Y$ 不超过 $y$ 的概率。</p>
<p>一维分布函数是 $F(x) = P(X \le x)$，二维分布函数就是增加了一个变量维度的累积概率：</p>
<p>$$
F(x, y) = P(X \le x, Y \le y)
$$</p>
<hr />
<h3>联合分布函数的性质</h3>
<h4>1. 取值范围</h4>
<p>$$
0 \le F(x, y) \le 1
$$</p>
<p>因为它本质上是概率。</p>
<h4>2. 单调性</h4>
<p>$F(x, y)$ 对 $x$ 和 $y$ 都是单调不减的。</p>
<ul>
<li>如果固定 $y$，让 $x$ 变大，事件 $X \le x$ 的范围变大，所以概率不会减小；</li>
<li>如果固定 $x$，让 $y$ 变大，事件 $Y \le y$ 的范围变大，所以概率也不会减小。</li>
</ul>
<h4>3. 极限性质</h4>
<p>$$
F(-\infty, y) = 0
$$</p>
<p>$$
F(x, -\infty) = 0
$$</p>
<p>$$
F(+\infty, +\infty) = 1
$$</p>
<p>如果 $x$ 或 $y$ 趋近于负无穷大，代表至少有一个事件是不可能发生的，概率为 0；如果两者都趋于正无穷大，代表必然事件，概率为 1。</p>
<h4>4. 右连续性</h4>
<p>分布函数关于 $x$ 和 $y$ 都是右连续的：</p>
<p>$$
F(x + 0, y) = F(x, y)
$$</p>
<p>$$
F(x, y + 0) = F(x, y)
$$</p>
<hr />
<h2>3. 用联合分布函数求矩形区域概率</h2>
<p>这是二维分布函数最重要的用法之一。</p>
<p>如果我们要求二维随机变量 $(X, Y)$ 落在矩形区域 $x_1 &lt; X \le x_2, y_1 &lt; Y \le y_2$ 内的概率：</p>
<p>$$
P(x_1 &lt; X \le x_2, y_1 &lt; Y \le y_2)
$$</p>
<p>可以使用联合分布函数进行计算：</p>
<p>$$
\boxed{P(x_1 &lt; X \le x_2, y_1 &lt; Y \le y_2) = F(x_2, y_2) - F(x_1, y_2) - F(x_2, y_1) + F(x_1, y_1)}
$$</p>
<p>这个公式可以通过平面几何的面积加减法来理解：</p>
<ol>
<li>先取大矩形左下方的无穷区域概率：$F(x_2, y_2)$；</li>
<li>减去左边多余的区域概率：$F(x_1, y_2)$；</li>
<li>减去下面多余的区域概率：$F(x_2, y_1)$；</li>
<li>因为左下角交叉重叠的部分被减了两次，所以需要把它加回来：$F(x_1, y_1)$。</li>
</ol>
<p>该公式在计算联合分布函数的相关题目时经常被使用。</p>
<hr />
<h2>4. 二维离散型随机变量</h2>
<p>如果二维随机变量 $(X, Y)$ 只能取有限个或可列个点，则称它为<strong>二维离散型随机变量</strong>。</p>
<p>例如掷两枚骰子，其取值 $(X, Y)$ 只能是：</p>
<p>$$
(i, j), \quad i=1,2,\dots,6, \quad j=1,2,\dots,6
$$</p>
<hr />
<h3>联合分布律</h3>
<p>二维离散型随机变量使用<strong>联合分布律</strong>来描述：</p>
<p>$$
\boxed{P(X = x_i, Y = y_j) = p_{ij}}
$$</p>
<p>它必须满足以下两个基本条件：</p>
<p>$$
p_{ij} \ge 0
$$</p>
<p>$$
\sum_i \sum_j p_{ij} = 1
$$</p>
<p>联合分布律通常用表格形式表现，便于观察和计算。</p>
<p>例如：</p>
<p>$$
\begin{array}{c|ccc}
X \backslash Y &amp; 0 &amp; 1 &amp; 2 \
\hline
0 &amp; 0.1 &amp; 0.2 &amp; 0.1 \
1 &amp; 0.2 &amp; 0.1 &amp; 0.3
\end{array}
$$</p>
<p>这表示：</p>
<p>$$
P(X = 0, Y = 0) = 0.1
$$</p>
<p>$$
P(X = 0, Y = 1) = 0.2
$$</p>
<p>$$
P(X = 1, Y = 2) = 0.3
$$</p>
<p>表格中所有格子的概率相加必须等于 1。</p>
<hr />
<h2>5. 边缘分布</h2>
<p>联合分布描述的是 $(X, Y)$ 作为整体的联合概率。但如果我们只关心其中某一个变量，比如只关心 $X$ 独自的概率分布，就需要求出<strong>边缘分布</strong>。</p>
<hr />
<h3>离散型边缘分布</h3>
<p>若已知联合分布律为 $P(X = x_i, Y = y_j) = p_{ij}$，则有：</p>
<ul>
<li>$X$ 的边缘分布律：</li>
</ul>
<p>$$
\boxed{P(X = x_i) = \sum_j p_{ij}}
$$</p>
<p>也就是固定 $X = x_i$ 的那一行，把所有对应不同 $Y$ 取值的概率加起来。</p>
<ul>
<li>$Y$ 的边缘分布律：</li>
</ul>
<p>$$
\boxed{P(Y = y_j) = \sum_i p_{ij}}
$$</p>
<p>也就是固定 $Y = y_j$ 的那一列，把所有对应不同 $X$ 取值的概率加起来。</p>
<hr />
<h3>离散型边缘分布计算示例</h3>
<p>我们可以直接利用分布律表格进行计算。在表格右侧和下方分别添加边缘和概率：</p>
<p>$$
\begin{array}{c|ccc|c}
X \backslash Y &amp; 0 &amp; 1 &amp; 2 &amp; P_X \
\hline
0 &amp; 0.1 &amp; 0.2 &amp; 0.1 &amp; 0.4 \
1 &amp; 0.2 &amp; 0.1 &amp; 0.3 &amp; 0.6 \
\hline
P_Y &amp; 0.3 &amp; 0.3 &amp; 0.4 &amp; 1
\end{array}
$$</p>
<p>这样我们可以快速得出：</p>
<p>$$
P(X = 0) = 0.1 + 0.2 + 0.1 = 0.4
$$</p>
<p>$$
P(X = 1) = 0.2 + 0.1 + 0.3 = 0.6
$$</p>
<p>$$
P(Y = 0) = 0.1 + 0.2 = 0.3
$$</p>
<p>$$
P(Y = 1) = 0.2 + 0.1 = 0.3
$$</p>
<p>$$
P(Y = 2) = 0.1 + 0.3 = 0.4
$$</p>
<p>核心规律：</p>
<p>$$
\boxed{\text{离散型求边缘分布：对另一个变量对应的所有项进行求和}}
$$</p>
<hr />
<h2>6. 条件分布</h2>
<p>条件分布表示在一个随机变量已经确定取某个值的条件下，另一个随机变量的概率分布。</p>
<p>例如：</p>
<p>$$
P(X = x_i \mid Y = y_j)
$$</p>
<p>表示在已知 $Y = y_j$ 的事件发生的条件下，$X = x_i$ 的条件概率。</p>
<hr />
<h3>离散型条件分布</h3>
<p>根据条件概率公式：</p>
<p>$$
\boxed{P(X = x_i \mid Y = y_j) = \frac{P(X = x_i, Y = y_j)}{P(Y = y_j)}}
$$</p>
<p>同理，已知 $X = x_i$ 条件下 $Y$ 的条件分布为：</p>
<p>$$
\boxed{P(Y = y_j \mid X = x_i) = \frac{P(X = x_i, Y = y_j)}{P(X = x_i)}}
$$</p>
<p>需要注意的是，分母作为条件事件的边缘概率不能为 0。</p>
<p>即：</p>
<p>$$
\boxed{\text{条件分布} = \frac{\text{联合分布}}{\text{边缘分布}}}
$$</p>
<hr />
<h3>离散型条件分布计算示例</h3>
<p>沿用上一节的表格数据：</p>
<p>$$
\begin{array}{c|ccc|c}
X \backslash Y &amp; 0 &amp; 1 &amp; 2 &amp; P_X \
\hline
0 &amp; 0.1 &amp; 0.2 &amp; 0.1 &amp; 0.4 \
1 &amp; 0.2 &amp; 0.1 &amp; 0.3 &amp; 0.6 \
\hline
P_Y &amp; 0.3 &amp; 0.3 &amp; 0.4 &amp; 1
\end{array}
$$</p>
<p>我们要求：</p>
<p>$$
P(X = 1 \mid Y = 2)
$$</p>
<p>根据条件分布公式计算：</p>
<p>$$
P(X = 1 \mid Y = 2) = \frac{P(X = 1, Y = 2)}{P(Y = 2)}
$$</p>
<p>代入表格中的数值：</p>
<p>$$
P(X = 1 \mid Y = 2) = \frac{0.3}{0.4} = \frac{3}{4}
$$</p>
<hr />
<h2>7. 二维连续型随机变量</h2>
<p>如果二维随机变量 $(X, Y)$ 的取值可以在某个平面区域内连续变化，则称它为<strong>二维连续型随机变量</strong>。</p>
<p>常见的连续随机量有：</p>
<ul>
<li>$X$ 为身高，$Y$ 为体重；</li>
<li>$X$ 为零件长度误差，$Y$ 为零件宽度误差；</li>
<li>$X$ 为顾客在超市排队等待时间，$Y$ 为接受服务时间。</li>
</ul>
<hr />
<h3>联合概率密度函数</h3>
<p>如果对于二维随机变量 $(X, Y)$ 的联合分布函数 $F(x, y)$，存在非负可积函数 $f(x, y)$ 满足：</p>
<p>$$
\boxed{F(x, y) = P(X \le x, Y \le y) = \int_{-\infty}^{x} \int_{-\infty}^{y} f(u, v),dv,du}
$$</p>
<p>则称 $f(x, y)$ 为 $(X, Y)$ 的<strong>联合概率密度函数</strong>。</p>
<p>联合概率密度函数必须满足两个核心性质：</p>
<ul>
<li>非负性：</li>
</ul>
<p>$$
f(x, y) \ge 0
$$</p>
<ul>
<li>规范性：整个实数平面上的总积分必须为 1：</li>
</ul>
<p>$$
\boxed{\int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} f(x, y),dx,dy = 1}
$$</p>
<hr />
<h3>用联合概率密度求区域概率</h3>
<p>若要求 $(X, Y)$ 落在平面某个区域 $D$ 内的概率 $P((X, Y) \in D)$，可以通过对联合密度进行二重积分求得：</p>
<p>$$
\boxed{P((X, Y) \in D) = \iint_D f(x, y),dx,dy}
$$</p>
<p>若区域 $D$ 是一个矩形区域 $a &lt; X &lt; b, c &lt; Y &lt; d$，则概率为：</p>
<p>$$
\boxed{P(a &lt; X &lt; b, c &lt; Y &lt; d) = \int_a^b \int_c^d f(x, y),dy,dx}
$$</p>
<p>在几何上，联合概率密度在某区域上的积分对应的是密度曲面在区域 $D$ 上方的“体积”。由于是连续型随机变量，单点的概率为 0。</p>
<hr />
<h2>8. 连续型边缘密度</h2>
<p>与离散型累加求和类似，连续型随机变量则是通过对另一个变量在全部取值范围内积分，来求取某个变量的<strong>边缘密度函数</strong>。</p>
<p>如果已知 $(X, Y)$ 的联合密度函数为 $f(x, y)$，则：</p>
<ul>
<li>$X$ 的边缘密度函数：</li>
</ul>
<p>$$
\boxed{f_X(x) = \int_{-\infty}^{+\infty} f(x, y),dy}
$$</p>
<ul>
<li>$Y$ 的边缘密度函数：</li>
</ul>
<p>$$
\boxed{f_Y(y) = \int_{-\infty}^{+\infty} f(x, y),dx}
$$</p>
<p>核心规律：</p>
<p>$$
\boxed{\text{连续型求边缘密度：对另一个变量进行积分}}
$$</p>
<p>我们可以用下表做直观对照：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>求边缘分布的方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>离散型</td>
<td>对另一个变量的所有取值求和</td>
</tr>
<tr>
<td>连续型</td>
<td>对另一个变量在全域进行积分</td>
</tr>
</tbody>
</table>
<hr />
<h2>9. 连续型条件密度</h2>
<p>设连续型随机变量 $(X, Y)$ 的联合密度为 $f(x, y)$，边缘密度分别为 $f_X(x)$ 和 $f_Y(y)$：</p>
<ul>
<li>在已知 $Y = y$ 的条件下，随机变量 $X$ 的条件概率密度为：</li>
</ul>
<p>$$
\boxed{f_{X \mid Y}(x \mid y) = \frac{f(x, y)}{f_Y(y)} \quad (\text{其中 } f_Y(y) &gt; 0)}
$$</p>
<ul>
<li>在已知 $X = x$ 的条件下，随机变量 $Y$ 的条件概率密度为：</li>
</ul>
<p>$$
\boxed{f_{Y \mid X}(y \mid x) = \frac{f(x, y)}{f_X(x)} \quad (\text{其中 } f_X(x) &gt; 0)}
$$</p>
<p>其核心思想依然是：</p>
<p>$$
\boxed{\text{条件密度} = \frac{\text{联合密度}}{\text{边缘密度}}}
$$</p>
<hr />
<h2>10. 经典例题解析</h2>
<p>设二维连续型随机变量 $(X, Y)$ 的联合概率密度为：</p>
<p>$$
f(x, y) =
\begin{cases}
c(x + y), &amp; 0 &lt; x &lt; 1, 0 &lt; y &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>我们来依次求解以下问题：</p>
<ol>
<li>求常数 $c$；</li>
<li>求 $X$ 的边缘概率密度 $f_X(x)$；</li>
<li>求 $Y$ 的边缘概率密度 $f_Y(y)$；</li>
<li>判断 $X$ 与 $Y$ 是否相互独立。</li>
</ol>
<hr />
<h3>第一步：求常数 $c$</h3>
<p>根据联合概率密度的规范性，全部区域的积分值必须为 1：</p>
<p>$$
\int_0^1 \int_0^1 c(x + y),dy,dx = 1
$$</p>
<p>我们先对 $y$ 进行积分：</p>
<p>$$
\int_0^1 c(x + y),dy = c \left[ xy + \frac{1}{2}y^2 \right]_0^1 = c\left(x + \frac{1}{2}\right)
$$</p>
<p>接着对 $x$ 进行积分：</p>
<p>$$
\int_0^1 c\left(x + \frac{1}{2}\right)dx = c \left[ \frac{1}{2}x^2 + \frac{1}{2}x \right]_0^1 = c
$$</p>
<p>因此，得出常数：</p>
<p>$$
c = 1
$$</p>
<p>所以，该联合概率密度函数为：</p>
<p>$$
f(x, y) =
\begin{cases}
x + y, &amp; 0 &lt; x &lt; 1, 0 &lt; y &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<hr />
<h3>第二步：求 $X$ 的边缘概率密度 $f_X(x)$</h3>
<p>当 $0 &lt; x &lt; 1$ 时：</p>
<p>$$
f_X(x) = \int_0^1 f(x, y),dy = \int_0^1 (x + y),dy = \left[ xy + \frac{1}{2}y^2 \right]_0^1 = x + \frac{1}{2}
$$</p>
<p>当 $x$ 取其他值时，概率密度为 0。</p>
<p>所以，$X$ 的边缘概率密度为：</p>
<p>$$
f_X(x) =
\begin{cases}
x + \frac{1}{2}, &amp; 0 &lt; x &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<hr />
<h3>第三步：求 $Y$ 的边缘概率密度 $f_Y(y)$</h3>
<p>当 $0 &lt; y &lt; 1$ 时：</p>
<p>$$
f_Y(y) = \int_0^1 f(x, y),dx = \int_0^1 (x + y),dx = \left[ \frac{1}{2}x^2 + xy \right]_0^1 = y + \frac{1}{2}
$$</p>
<p>当 $y$ 取其他值时，概率密度为 0。</p>
<p>所以，$Y$ 的边缘概率密度为：</p>
<p>$$
f_Y(y) =
\begin{cases}
y + \frac{1}{2}, &amp; 0 &lt; y &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<hr />
<h3>第四步：判断独立性</h3>
<p>如果 $X$ 与 $Y$ 相互独立，应满足 $f(x, y) = f_X(x) f_Y(y)$ 对一切实数均成立。</p>
<p>而在定义域区间内：</p>
<p>$$
f_X(x) f_Y(y) = \left(x + \frac{1}{2}\right)\left(y + \frac{1}{2}\right) = xy + \frac{1}{2}x + \frac{1}{2}y + \frac{1}{4}
$$</p>
<p>这显然不等于：</p>
<p>$$
f(x, y) = x + y
$$</p>
<p>因此，可以判定：</p>
<p>$$
\boxed{X \text{ 与 } Y \text{ 不相互独立}}
$$</p>
<hr />
<h2>11. 随机变量的独立性</h2>
<p>独立性是二维随机变量在考试和实际应用中的重要概念。</p>
<h3>用联合分布函数判断独立性</h3>
<p>如果对于任意实数 $x, y$，均有：</p>
<p>$$
\boxed{F(x, y) = F_X(x) F_Y(y)}
$$</p>
<p>则称随机变量 $X$ 与 $Y$ 相互独立。</p>
<hr />
<h3>离散型随机变量独立条件</h3>
<p>如果对于所有可能取的值 $x_i, y_j$，均满足：</p>
<p>$$
\boxed{P(X = x_i, Y = y_j) = P(X = x_i) P(Y = y_j)}
$$</p>
<p>则 $X$ 与 $Y$ 相互独立。</p>
<p>即其联合分布律等于边缘分布律的乘积：</p>
<p>$$
\boxed{p_{ij} = p_{i\cdot} p_{\cdot j}}
$$</p>
<p>其中 $p_{i\cdot} = P(X = x_i)$，$p_{\cdot j} = P(Y = y_j)$。</p>
<hr />
<h3>连续型随机变量独立条件</h3>
<p>如果对任意实数 $x, y$，均有：</p>
<p>$$
\boxed{f(x, y) = f_X(x) f_Y(y)}
$$</p>
<p>则 $X$ 与 $Y$ 相互独立。也就是说联合密度函数等于两个边缘密度函数的乘积。</p>
<p>独立性的核心记忆法则为：</p>
<p>$$
\boxed{\text{联合分布} = \text{边缘分布的乘积}}
$$</p>
<hr />
<h2>12. 二维随机变量函数的分布</h2>
<p>在这一节中，我们的目标是已知 $(X, Y)$ 的分布，求解由它们通过某种函数关系构成的新随机变量：</p>
<p>$$
Z = g(X, Y)
$$</p>
<p>的概率分布。</p>
<hr />
<h3>离散型函数的分布</h3>
<p>如果 $(X, Y)$ 是离散型随机变量，解题方法是直接列举法：</p>
<ol>
<li>列举出所有可能出现的组合 $(x_i, y_j)$ 并算出新随机变量取值 $z_k = g(x_i, y_j)$；</li>
<li>计算对应的联合概率 $P(X=x_i, Y=y_j)$；</li>
<li>将计算出相同 $z_k$ 值的事件概率进行合并累加，得到 $Z$ 的分布律。</li>
</ol>
<p>例如求解 $Z = X + Y$，其概率计算公式为：</p>
<p>$$
P(Z = z) = \sum_{x_i + y_j = z} P(X = x_i, Y = y_j)
$$</p>
<hr />
<h3>连续型函数的分布</h3>
<p>若 $(X, Y)$ 是连续型随机变量，最基本、最通用的方法是<strong>分布函数法</strong>：</p>
<ol>
<li>先写出 $Z$ 的累积分布函数：</li>
</ol>
<p>$$
F_Z(z) = P(Z \le z) = P(g(X, Y) \le z)
$$</p>
<ol>
<li>确定满足关系式 $g(x, y) \le z$ 在平面直角坐标系中的积分区域 $D_z$：</li>
</ol>
<p>$$
D_z = {(x, y) \mid g(x, y) \le z}
$$</p>
<ol>
<li>通过二重积分计算累积概率值：</li>
</ol>
<p>$$
\boxed{F_Z(z) = \iint_{D_z} f(x, y),dx,dy}
$$</p>
<ol>
<li>将分布函数 $F_Z(z)$ 对 $z$ 求导数，求得 $Z$ 的概率密度函数 $f_Z(z)$。</li>
</ol>
<p>解决此类题目的关键并不在于记忆死公式，而在于能准确画出平面上的积分区域，并能够正确地确定二重积分的积分上下限。</p>
<hr />
<h2>13. 常见函数的分布结论</h2>
<p>有些经典的随机变量函数分布在考试中极高频出现，掌握其通用结论和推导方法至关重要。</p>
<h3>13.1 和的分布 $Z = X + Y$</h3>
<p>如果随机变量 $X$ 和 $Y$ 相互独立，且均为连续型随机变量，则可以通过<strong>卷积公式</strong>计算其概率密度函数：</p>
<p>$$
\boxed{f_Z(z) = \int_{-\infty}^{+\infty} f_X(x) f_Y(z - x),dx}
$$</p>
<p>或者对称地写为：</p>
<p>$$
\boxed{f_Z(z) = \int_{-\infty}^{+\infty} f_X(z - y) f_Y(y),dy}
$$</p>
<p>如果是离散型，同理可得：</p>
<p>$$
\boxed{P(Z = z) = \sum_x P(X = x) P(Y = z - x)}
$$</p>
<h4>典型例题</h4>
<p>设随机变量 $X$ 与 $Y$ 相互独立，且：</p>
<p>$$
X \sim U(0,1), \quad Y \sim E(1)
$$</p>
<p>令：</p>
<p>$$
Z = X + Y
$$</p>
<p>求随机变量 $Z$ 的概率密度函数 $f_Z(z)$。</p>
<p><strong>【解析】</strong></p>
<p>首先写出随机变量的概率密度函数。</p>
<p>因为 $X \sim U(0,1)$，所以 $X$ 的密度函数为：</p>
<p>$$
f_X(x) =
\begin{cases}
1, &amp; 0 &lt; x &lt; 1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>因为 $Y \sim E(1)$，所以 $Y$ 的密度函数为：</p>
<p>$$
f_Y(y) =
\begin{cases}
e^{-y}, &amp; y &gt; 0 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>因为 $X$ 与 $Y$ 相互独立，所以其联合概率密度函数为：</p>
<p>$$
f(x,y) = f_X(x)f_Y(y) =
\begin{cases}
e^{-y}, &amp; 0 &lt; x &lt; 1, y &gt; 0 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>下面给出两种经典的求解方法。</p>
<h5>方法一：分布函数法</h5>
<p>分布函数法的核心思路是：</p>
<p>$$
\boxed{\text{先求累积分布函数 } F_Z(z) = P(Z \le z)\text{，再通过求导得到概率密度 } f_Z(z)}
$$</p>
<p>因为 $Z = X + Y$，所以：</p>
<p>$$
F_Z(z) = P(X + Y \le z)
$$</p>
<p>我们需要在联合密度非零的区域 $0 &lt; x &lt; 1, y &gt; 0$ 以及满足 $x + y \le z$ 的平面区域上进行积分。</p>
<p>根据 $z$ 的取值范围，我们需要分区间讨论：</p>
<p><strong>1. 当 $z \le 0$ 时</strong></p>
<p>因为 $X &gt; 0, Y &gt; 0$ 恒成立，所以它们的和 $Z = X + Y &gt; 0$ 也是恒成立的。</p>
<p>因此，不可能有 $Z \le z$，概率为 0：</p>
<p>$$
F_Z(z) = 0, \quad z \le 0
$$</p>
<p><strong>2. 当 $0 &lt; z &lt; 1$ 时</strong></p>
<p>此时，积分区域由 $0 &lt; x &lt; 1, y &gt; 0$ 和 $x + y \le z$ 共同决定。</p>
<p>由 $x + y \le z \implies y \le z - x$。因为 $y &gt; 0$，所以必须有 $z - x &gt; 0 \implies x &lt; z$。</p>
<p>结合 $X$ 的定义域 $0 &lt; x &lt; 1$ 以及当前 $z &lt; 1$ 的限制，自变量 $x$ 的积分区间为 $0 &lt; x &lt; z$。</p>
<p>因此，累积分布函数为：</p>
<p>$$
F_Z(z) = \int_0^z \int_0^{z-x} e^{-y},dy,dx
$$</p>
<p>先计算内层关于 $y$ 的积分：</p>
<p>$$
\int_0^{z-x} e^{-y},dy = \left[-e^{-y}\right]_0^{z-x} = 1 - e^{-(z-x)}
$$</p>
<p>将其代回外层积分：</p>
<p>$$
\begin{aligned}
F_Z(z) &amp;= \int_0^z \left[ 1 - e^{-(z-x)} \right] dx \
&amp;= \int_0^z 1,dx - e^{-z}\int_0^z e^x,dx \
&amp;= z - e^{-z}\left(e^z - 1\right) \
&amp;= z - 1 + e^{-z}
\end{aligned}
$$</p>
<p>即当 $0 &lt; z &lt; 1$ 时，$F_Z(z) = z - 1 + e^{-z}$。</p>
<p><strong>3. 当 $z \ge 1$ 时</strong></p>
<p>此时，因为 $z \ge 1$，自变量 $x$ 的上限由其自身的定义域限制，即 $0 &lt; x &lt; 1$。</p>
<p>对于任意在这个区间内的 $x$，都有 $x &lt; 1 \le z$，即 $z - x &gt; 0$ 恒成立。所以 $y$ 的积分区间始终为 $0 &lt; y &lt; z-x$。</p>
<p>因此，累积分布函数为：</p>
<p>$$
F_Z(z) = \int_0^1 \int_0^{z-x} e^{-y},dy,dx
$$</p>
<p>代入内层积分结果并计算外层积分：</p>
<p>$$
\begin{aligned}
F_Z(z) &amp;= \int_0^1 \left[ 1 - e^{-(z-x)} \right] dx \
&amp;= \int_0^1 1,dx - e^{-z}\int_0^1 e^x,dx \
&amp;= 1 - e^{-z}\left(e - 1\right) \
&amp;= 1 - (e - 1)e^{-z}
\end{aligned}
$$</p>
<p>即当 $z \ge 1$ 时，$F_Z(z) = 1 - (e - 1)e^{-z}$。</p>
<p><strong>4. 总结分布函数并求导</strong></p>
<p>综上所述，求得 $Z$ 的累积分布函数为：</p>
<p>$$
F_Z(z) =
\begin{cases}
0, &amp; z \le 0 \
z - 1 + e^{-z}, &amp; 0 &lt; z &lt; 1 \
1 - (e - 1)e^{-z}, &amp; z \ge 1
\end{cases}
$$</p>
<p>对分布函数 $F_Z(z)$ 进行分段求导以得到概率密度函数 $f_Z(z) = F_Z'(z)$：</p>
<ul>
<li>当 $z &lt; 0$ 时，$f_Z(z) = 0$；</li>
<li>当 $0 &lt; z &lt; 1$ 时，$f_Z(z) = \dfrac{d}{dz}(z - 1 + e^{-z}) = 1 - e^{-z}$；</li>
<li>当 $z &gt; 1$ 时，$f_Z(z) = \dfrac{d}{dz}\left(1 - (e - 1)e^{-z}\right) = (e - 1)e^{-z}$。</li>
</ul>
<p>（在分界点处，其概率密度不影响最终的积分概率，可以任意归入某一区间）</p>
<p>最终求得概率密度函数为：</p>
<p>$$
\boxed{f_Z(z) =
\begin{cases}
0, &amp; z \le 0 \
1 - e^{-z}, &amp; 0 &lt; z &lt; 1 \
(e - 1)e^{-z}, &amp; z \ge 1
\end{cases}}
$$</p>
<h5>方法二：卷积公式法</h5>
<p>由于 $X$ 与 $Y$ 相互独立且均为连续型，可以直接使用卷积公式求解：</p>
<p>$$
f_Z(z) = \int_{-\infty}^{+\infty} f_X(x) f_Y(z - x),dx
$$</p>
<p>因为 $f_X(x)$ 在 $0 &lt; x &lt; 1$ 之外为 0，且 $f_Y(z-x)$ 在 $z-x \le 0$（即 $x \ge z$）时为 0。</p>
<p>为了使被积函数 $f_X(x)f_Y(z-x)$ 不为 0，积分自变量 $x$ 必须同时满足：</p>
<p>$$
0 &lt; x &lt; 1 \quad \text{且} \quad x &lt; z
$$</p>
<p>即积分的有效区间上限为 $\min(1, z)$，下限为 0。因此我们同样需要分区间讨论：</p>
<p><strong>1. 当 $z \le 0$ 时</strong></p>
<p>由于 $x &lt; z \le 0$，不存在满足 $0 &lt; x &lt; z$ 的实数，此时被积函数在积分区间上恒为 0，故：</p>
<p>$$
f_Z(z) = 0
$$</p>
<p><strong>2. 当 $0 &lt; z &lt; 1$ 时</strong></p>
<p>此时积分的有效范围为 $0 &lt; x &lt; z$。代入各自的密度函数：</p>
<p>$$
\begin{aligned}
f_Z(z) &amp;= \int_0^z 1 \cdot e^{-(z-x)},dx \
&amp;= e^{-z} \int_0^z e^x,dx \
&amp;= e^{-z} \left[ e^x \right]_0^z \
&amp;= e^{-z}\left(e^z - 1\right) \
&amp;= 1 - e^{-z}
\end{aligned}
$$</p>
<p><strong>3. 当 $z \ge 1$ 时</strong></p>
<p>此时积分的有效范围被 $X$ 本身的定义域限制，即 $0 &lt; x &lt; 1$。代入密度函数：</p>
<p>$$
\begin{aligned}
f_Z(z) &amp;= \int_0^1 1 \cdot e^{-(z-x)},dx \
&amp;= e^{-z} \int_0^1 e^x,dx \
&amp;= e^{-z} \left[ e^x \right]_0^1 \
&amp;= (e - 1)e^{-z}
\end{aligned}
$$</p>
<p><strong>4. 总结最终答案</strong></p>
<p>将各分段结果合并，得到 $Z$ 的概率密度函数：</p>
<p>$$
\boxed{f_Z(z) =
\begin{cases}
0, &amp; z \le 0 \
1 - e^{-z}, &amp; 0 &lt; z &lt; 1 \
(e - 1)e^{-z}, &amp; z \ge 1
\end{cases}}
$$</p>
<h5>方法对比总结</h5>
<ul>
<li><strong>分布函数法</strong>：思路极其直观，先通过二重积分计算平面区域的累积概率 $F_Z(z)$，随后通过求导得出密度。该方法不容易出错，但积分与求导的计算步骤相对较多。</li>
<li><strong>卷积公式法</strong>：直接一步到位计算密度函数，省去了后续的求导步骤。其难点和易错点在于：必须根据定义域的非零区间限制，推导出积分上限关于 $z$ 的分段情况（如本题中的 $\min(1, z)$），进而确定积分讨论区间。</li>
</ul>
<hr />
<h3>13.2 最大值分布 $M = \max(X, Y)$</h3>
<p>如果随机变量 $X$ 与 $Y$ 相互独立，则最大值小于等于 $m$ 的事件等价于两者同时小于等于 $m$：</p>
<p>$$
F_M(m) = P(M \le m) = P(X \le m, Y \le m)
$$</p>
<p>由于独立性，概率可以拆分为边缘分布相乘：</p>
<p>$$
\boxed{F_M(m) = F_X(m) F_Y(m)}
$$</p>
<hr />
<h3>13.3 最小值分布 $N = \min(X, Y)$</h3>
<p>如果随机变量 $X$ 与 $Y$ 相互独立，则最小值分布可以利用其对立事件求取，因为最小值大于 $n$ 等价于两者同时大于 $n$：</p>
<p>$$
P(N &gt; n) = P(X &gt; n, Y &gt; n) = P(X &gt; n) P(Y &gt; n)
$$</p>
<p>将其转化为累积分布函数：</p>
<p>$$
F_N(n) = 1 - P(N &gt; n) = 1 - [1 - F_X(n)][1 - F_Y(n)]
$$</p>
<p>所以，最小值的分布函数公式为：</p>
<p>$$
\boxed{F_N(n) = 1 - [1 - F_X(n)][1 - F_Y(n)]}
$$</p>
<hr />
<h2>14. 二维与一维随机变量的深层关系</h2>
<p>在理解二维分布时，需要特别厘清一个认知问题：<strong>我们是否能用单独的边缘分布拼凑出完整的联合分布？</strong></p>
<p>通常情况下，仅凭单独的 $X$ 分布和单独的 $Y$ 分布，是<strong>无法</strong>确定 $(X, Y)$ 的联合分布的。这是因为边缘分布只说明了各自随机量在单独视角下的取值规律，而丢失了两个变量在同一试验中发生关联的相互影响关系。</p>
<p>只有在明确得知 <strong>$X$ 与 $Y$ 相互独立</strong> 的前提下，联合分布才等于边缘分布的乘积。</p>
<p>因此，可以用下述规律总结：</p>
<p>$$
\boxed{\text{联合分布可以唯一推出边缘分布，但边缘分布一般无法推出联合分布}}
$$</p>
<hr />
<h2>15. 本章常考题型</h2>
<h3>题型一：给联合分布律，求边缘分布</h3>
<p><strong>求解方法</strong>：利用 $\sum_j p_{ij} = P(X=x_i)$ 和 $\sum_i p_{ij} = P(Y=y_j)$，在离散型表格中横向或纵向累加求和即可。</p>
<h3>题型二：给联合分布律，求条件分布</h3>
<p><strong>求解方法</strong>：利用 $P(X=x_i \mid Y=y_j) = \frac{p_{ij}}{P(Y=y_j)}$，先算出分母上的边缘概率，然后用联合概率除以该边缘概率。</p>
<h3>题型三：判断独立性</h3>
<p><strong>求解方法</strong>：</p>
<ul>
<li>离散型：检验是否对于所有的点都有 $p_{ij} = p_{i\cdot} p_{\cdot j}$；</li>
<li>连续型：求出边缘概率密度，检验是否在全域上满足 $f(x, y) = f_X(x) f_Y(y)$。</li>
</ul>
<h3>题型四：给联合密度，求未知常数</h3>
<p><strong>求解方法</strong>：利用规范性条件，计算在全域积分 $\iint f(x, y),dx,dy = 1$，求出密度函数中的未知数。</p>
<h3>题型五：给联合密度，求边缘密度</h3>
<p><strong>求解方法</strong>：按照 $f_X(x) = \int f(x, y),dy$ 或 $f_Y(y) = \int f(x, y),dx$ 计算。解题的关键在于正确识别定义域的边界，从而合理确定积分区域的上下限。</p>
<h3>题型六：求平面区域概率</h3>
<p><strong>求解方法</strong>：利用 $P((X, Y) \in D) = \iint_D f(x, y),dx,dy$，根据题意绘制平面积分区域 $D$，列出积分表达式并求解。</p>
<h3>题型七：求 $Z = g(X, Y)$ 的分布</h3>
<p><strong>求解方法</strong>：优先选择分布函数法，先表示出 $F_Z(z) = P(g(X, Y) \le z)$，然后找出积分边界条件并进行计算，最后对分布函数求导得出密度函数。</p>
<hr />
<h2>16. 易错点总结</h2>
<h3>易错点一：将边缘分布与联合分布混淆</h3>
<p>在求边缘分布时，务必将另一个变量消除（离散型求和累加，连续型积分积掉）。边缘分布结果是关于单变量的函数，其表达式中不应该包含另一个随机变量的符号。</p>
<h3>易错点二：认为知道边缘分布就能决定联合分布</h3>
<p>在题目中如果未告知 $X, Y$ 独立，千万不可直接假定 $f(x, y) = f_X(x) f_Y(y)$ 开展后续计算。</p>
<h3>易错点三：条件分布的分母位置放错</h3>
<p>在计算条件概率或条件密度时，分母必须是条件事件（已发生的那个随机变量）对应的<strong>边缘分布</strong>，千万不可放错自变量。</p>
<h3>易错点四：连续型随机变量计算单点概率</h3>
<p>二维连续型随机变量在平面上某一个特定的点或一条线段上的概率为 0。只有在平面上具有面积的区域积分，对应的体积概率才可能有大于 0 的数值。</p>
<h3>易错点五：判断独立性仅验证个别数值</h3>
<p>判断独立性时要求“任意”或“所有”点都满足乘积公式。离散型如果有任何一格不满足 $p_{ij} = p_{i\cdot} p_{\cdot j}$，或者连续型在定义域内任一子区间不满足 $f(x, y) = f_X(x) f_Y(y)$，则两个随机变量即不独立。</p>
<hr />
<h2>17. 本章核心公式表</h2>
<table>
<thead>
<tr>
<th>内容</th>
<th>公式与表达</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>联合分布函数</strong></td>
<td>$F(x, y) = P(X \le x, Y \le y)$</td>
</tr>
<tr>
<td><strong>矩形概率计算</strong></td>
<td>$P(x_1 &lt; X \le x_2, y_1 &lt; Y \le y_2) = F(x_2, y_2) - F(x_1, y_2) - F(x_2, y_1) + F(x_1, y_1)$</td>
</tr>
<tr>
<td><strong>离散型联合分布律</strong></td>
<td>$P(X = x_i, Y = y_j) = p_{ij}$，且 $\sum_i \sum_j p_{ij} = 1$</td>
</tr>
<tr>
<td><strong>离散型边缘分布律</strong></td>
<td>$P(X = x_i) = \sum_j p_{ij}$， $P(Y = y_j) = \sum_i p_{ij}$</td>
</tr>
<tr>
<td><strong>离散型条件概率</strong></td>
<td>$P(X = x_i \mid Y = y_j) = \dfrac{p_{ij}}{P(Y = y_j)}$</td>
</tr>
<tr>
<td><strong>连续型联合密度规范性</strong></td>
<td>$\int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} f(x, y),dx,dy = 1$</td>
</tr>
<tr>
<td><strong>连续型区域概率</strong></td>
<td>$P((X, Y) \in D) = \iint_D f(x, y),dx,dy$</td>
</tr>
<tr>
<td><strong>连续型边缘概率密度</strong></td>
<td>$f_X(x) = \int_{-\infty}^{+\infty} f(x, y),dy$，$f_Y(y) = \int_{-\infty}^{+\infty} f(x, y),dx$</td>
</tr>
<tr>
<td><strong>连续型条件概率密度</strong></td>
<td>$f_{X \mid Y}(x \mid y) = \dfrac{f(x, y)}{f_Y(y)}$</td>
</tr>
<tr>
<td><strong>独立性判断条件</strong></td>
<td>联合分布（或联合密度、联合分布律）等于对应边缘分布之乘积</td>
</tr>
<tr>
<td><strong>和的分布（独立连续型）</strong></td>
<td>$f_{X+Y}(z) = \int_{-\infty}^{+\infty} f_X(x) f_Y(z - x),dx$</td>
</tr>
<tr>
<td><strong>最大值分布（独立）</strong></td>
<td>$F_{\max}(z) = F_X(z) F_Y(z)$</td>
</tr>
<tr>
<td><strong>最小值分布（独立）</strong></td>
<td>$F_{\min}(z) = 1 - [1 - F_X(z)][1 - F_Y(z)]$</td>
</tr>
</tbody>
</table>
<hr />
<h2>18. 一句话掌握本章</h2>
<p>二维随机变量的研究主线可以归纳为：</p>
<p>$$
\boxed{\text{联合分布} \rightarrow \text{边缘分布} \rightarrow \text{条件分布} \rightarrow \text{独立性} \rightarrow \text{函数分布}}
$$</p>
<p>做题时的思考路径为：</p>
<ol>
<li><strong>审题型</strong>：弄清题目给的是联合分布函数、联合密度函数，还是离散型的联合分布律表格；</li>
<li><strong>定方法</strong>：离散型题目核心在于“求和与累加”，连续型题目核心在于“积分与确定边界”；</li>
<li><strong>推边缘</strong>：只求单一随机变量的规律时，对另一个自变量进行全域积分或全值累加求和；</li>
<li><strong>求条件</strong>：某随机变量已知时求另一个，直接套用比值公式，将联合分布除以已知随机量的边缘分布；</li>
<li><strong>断独立</strong>：看联合函数是否恒等于各边缘函数的乘积；</li>
<li><strong>算函数</strong>：计算新变量 $Z = g(X, Y)$ 时，优先使用分布函数法，建立累积分布二重积分求解。</li>
</ol>
<h2>19. 框框老师</h2>
<p><img src="https://img.picr2.online/1782117726990_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782117750278_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782117781463_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118115902_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118225350_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118395726_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118454633_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118489402_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118543975_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782118728679_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782119064939_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782119092440_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>三角函数及常用微积分公式</title>
    <link href="https://blog.yoake.dev/posts/%E9%AB%98%E7%AD%89%E6%95%B0%E5%AD%A6/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0%E5%8F%8A%E5%B8%B8%E7%94%A8%E5%BE%AE%E7%A7%AF%E5%88%86%E5%85%AC%E5%BC%8F/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E9%AB%98%E7%AD%89%E6%95%B0%E5%AD%A6/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0%E5%8F%8A%E5%B8%B8%E7%94%A8%E5%BE%AE%E7%A7%AF%E5%88%86%E5%85%AC%E5%BC%8F/</id>
    <published>2026-06-21T12:05:00.000Z</published>
    <updated>2026-06-21T12:05:00.000Z</updated>
    <summary>系统整理高等数学中的三角函数与反三角函数的定义、基本关系、导数公式、积分公式及推导规律。</summary>
    <content type="html"><![CDATA[<p>本文主要梳理<strong>三角函数关系图、导数公式与积分公式</strong>。</p>
<p>微积分公式的记忆核心在于：三角函数公式本质上都源于几个最基本的关系，理解了它们之间的定义与推导脉络，就不需要死记硬背所有公式。</p>
<hr />
<p><img src="https://img.picr2.online/1782043454192_77a72a1889717b22f937c7e547d26a98.webp" alt="Image" /></p>
<h2>1. 六个基本三角函数的定义</h2>
<p>三角函数的定义最初建立在直角三角形中。</p>
<p>设直角三角形的一个锐角为 $x$，定义三条边：</p>
<p>$$
\text{对边},\quad \text{邻边},\quad \text{斜边}
$$</p>
<ul>
<li><strong>对边</strong>：角 $x$ 正对着的直角边。</li>
<li><strong>邻边</strong>：靠着角 $x$ 的直角边。</li>
<li><strong>斜边</strong>：直角三角形中最长的斜边。</li>
</ul>
<p><img src="https://img.picr2.online/1782044139632_triangle.svg" alt="直角三角形" /></p>
<p>基于这三条边，定义六个基本三角函数：</p>
<h3>1.1 正弦 $\sin x$</h3>
<p>$$
\sin x=\frac{\text{对边}}{\text{斜边}}
$$</p>
<p>即角 $x$ 的对边与斜边的比值。</p>
<p><img src="https://img.picr2.online/1782044139787_sin.svg" alt="正弦" /></p>
<p>例如：</p>
<p>$$
\sin 30^\circ=\frac{1}{2}
$$</p>
<hr />
<h3>1.2 余弦 $\cos x$</h3>
<p>$$
\cos x=\frac{\text{邻边}}{\text{斜边}}
$$</p>
<p>即角 $x$ 的邻边与斜边的比值。</p>
<p><img src="https://img.picr2.online/1782044139113_cos.svg" alt="余弦" /></p>
<p>例如：</p>
<p>$$
\cos 60^\circ=\frac{1}{2}
$$</p>
<hr />
<h3>1.3 正切 $\tan x$</h3>
<p>$$
\tan x=\frac{\text{对边}}{\text{邻边}}
$$</p>
<p>也可以用正弦和余弦表示：</p>
<p>$$
\tan x=\frac{\sin x}{\cos x}
$$</p>
<p><img src="https://img.picr2.online/1782044138249_tan.svg" alt="正切" /></p>
<p>当 $\cos x=0$（即 $x = \dfrac{\pi}{2} + k\pi,, k\in\mathbb{Z}$）时，$\tan x$ 没有定义。</p>
<hr />
<h3>1.4 余切 $\cot x$</h3>
<p>$$
\cot x=\frac{\text{邻边}}{\text{对边}}
$$</p>
<p>也可以用余弦和正弦表示：</p>
<p>$$
\cot x=\frac{\cos x}{\sin x} = \frac{1}{\tan x}
$$</p>
<p><img src="https://img.picr2.online/1782044138986_cot.svg" alt="余切" /></p>
<p>当 $\sin x=0$（即 $x = k\pi,, k\in\mathbb{Z}$）时，$\cot x$ 没有定义。</p>
<hr />
<h3>1.5 正割 $\sec x$</h3>
<p>$$
\sec x=\frac{\text{斜边}}{\text{邻边}} = \frac{1}{\cos x}
$$</p>
<p>正割是余弦的倒数。当 $\cos x=0$ 时，$\sec x$ 没有定义。</p>
<p><img src="https://img.picr2.online/1782044138975_sec.svg" alt="正割" /></p>
<hr />
<h3>1.6 余割 $\csc x$</h3>
<p>$$
\csc x=\frac{\text{斜边}}{\text{对边}} = \frac{1}{\sin x}
$$</p>
<p>余割是正弦的倒数。当 $\sin x=0$ 时，$\csc x$ 没有定义。</p>
<p><img src="https://img.picr2.online/1782044139506_csc.svg" alt="余割" /></p>
<hr />
<h2>2. 三角函数之间的关系与平方恒等式</h2>
<p>这六个函数满足紧密的乘积、商数和平方关系。</p>
<h3>倒数关系</h3>
<p>$$
\sin x \cdot \csc x=1
$$</p>
<p>$$
\cos x \cdot \sec x=1
$$</p>
<p>$$
\tan x \cdot \cot x=1
$$</p>
<hr />
<h3>三个核心平方关系</h3>
<p>$$
\sin^2 x+\cos^2 x=1
$$</p>
<p>这是最基本的三角平方和公式。其余两个平方关系可以通过它直接推导得出：</p>
<p>将公式两边同时除以 $\cos^2 x$，得到：</p>
<p>$$
\tan^2 x+1=\sec^2 x
$$</p>
<p>将公式两边同时除以 $\sin^2 x$，得到：</p>
<p>$$
1+\cot^2 x=\csc^2 x
$$</p>
<hr />
<h2>3. 反三角函数的定义与性质</h2>
<p>反三角函数是三角函数的逆运算，本质是“通过三角函数值反过来求角度”。</p>
<hr />
<h3>3.1 反正弦 $\arcsin x$</h3>
<p>$$
y=\arcsin x \iff \sin y=x
$$</p>
<p>表示正弦值等于 $x$ 的角。</p>
<ul>
<li><strong>定义域</strong>：$[-1, 1]$</li>
<li><strong>值域</strong>：$\left[-\dfrac{\pi}{2}, \dfrac{\pi}{2}\right]$</li>
</ul>
<p>例如：</p>
<p>$$
\arcsin \frac{1}{2}=\frac{\pi}{6} \quad \left(\text{因为 } \sin \frac{\pi}{6}=\frac{1}{2}\right)
$$</p>
<hr />
<h3>3.2 反余弦 $\arccos x$</h3>
<p>$$
y=\arccos x \iff \cos y=x
$$</p>
<p>表示余弦值等于 $x$ 的角。</p>
<ul>
<li><strong>定义域</strong>：$[-1, 1]$</li>
<li><strong>值域</strong>：$[0, \pi]$</li>
</ul>
<p>例如：</p>
<p>$$
\arccos \frac{1}{2}=\frac{\pi}{3} \quad \left(\text{因为 } \cos \frac{\pi}{3}=\frac{1}{2}\right)
$$</p>
<hr />
<h3>3.3 反正切 $\arctan x$</h3>
<p>$$
y=\arctan x \iff \tan y=x
$$</p>
<p>表示正切值等于 $x$ 的角。</p>
<ul>
<li><strong>定义域</strong>：$\mathbb{R}$</li>
<li><strong>值域</strong>：$\left(-\dfrac{\pi}{2}, \dfrac{\pi}{2}\right)$</li>
</ul>
<p>注意：因为 $\tan\left(\pm\dfrac{\pi}{2}\right)$ 不存在，所以值域不包含端点。</p>
<p>例如：</p>
<p>$$
\arctan 1=\frac{\pi}{4} \quad \left(\text{因为 } \tan \frac{\pi}{4}=1\right)
$$</p>
<hr />
<h3>3.4 反余切 $\operatorname{arccot} x$</h3>
<p>$$
y=\operatorname{arccot} x \iff \cot y=x
$$</p>
<p>表示余切值等于 $x$ 的角。</p>
<ul>
<li><strong>定义域</strong>：$\mathbb{R}$</li>
<li><strong>值域</strong>：$(0, \pi)$</li>
</ul>
<p>例如：</p>
<p>$$
\operatorname{arccot} 1=\frac{\pi}{4} \quad \left(\text{因为 } \cot \frac{\pi}{4}=1\right)
$$</p>
<hr />
<h3>3.5 反正割 $\operatorname{arcsec} x$</h3>
<p>$$
y=\operatorname{arcsec} x \iff \sec y=x
$$</p>
<p>由于 $\sec y = \dfrac{1}{\cos y}$，所以可转化为反余弦计算：</p>
<p>$$
\operatorname{arcsec} x=\arccos\frac{1}{x}
$$</p>
<ul>
<li><strong>定义域</strong>：$(-\infty, -1] \cup [1, +\infty)$</li>
<li><strong>值域</strong>：$\left[0, \dfrac{\pi}{2}\right) \cup \left(\dfrac{\pi}{2}, \pi\right]$</li>
</ul>
<hr />
<h3>3.6 反余割 $\operatorname{arccsc} x$</h3>
<p>$$
y=\operatorname{arccsc} x \iff \csc y=x
$$</p>
<p>由于 $\csc y = \dfrac{1}{\sin y}$，所以可转化为反正弦计算：</p>
<p>$$
\operatorname{arccsc} x=\arcsin\frac{1}{x}
$$</p>
<ul>
<li><strong>定义域</strong>：$(-\infty, -1] \cup [1, +\infty)$</li>
<li><strong>值域</strong>：$\left[-\dfrac{\pi}{2}, 0\right) \cup \left(0, \dfrac{\pi}{2}\right]$</li>
</ul>
<hr />
<h2>4. 三角函数与反三角函数的联系与区别</h2>
<p>三角函数与反三角函数互为逆映射：</p>
<ul>
<li>
<p><strong>三角函数</strong>：将一个<strong>角度</strong>映射为该角度对应的直角三角形边长<strong>比值</strong>。</p>
<p>$$
\text{角度} \longrightarrow \text{比值}
$$</p>
<p>如：$\sin \dfrac{\pi}{6} = \dfrac{1}{2}$</p>
</li>
<li>
<p><strong>反三角函数</strong>：将一个边长<strong>比值</strong>映射为对应的<strong>角度</strong>。</p>
<p>$$
\text{比值} \longrightarrow \text{角度}
$$</p>
<p>如：$\arcsin \dfrac{1}{2} = \dfrac{\pi}{6}$</p>
</li>
</ul>
<h3>经典对照示例</h3>
<ul>
<li>$\sin \dfrac{\pi}{6}=\dfrac{1}{2} \iff \arcsin \frac{1}{2}=\frac{\pi}{6}$</li>
<li>$\cos \dfrac{\pi}{3}=\dfrac{1}{2} \iff \arccos \frac{1}{2}=\frac{\pi}{3}$</li>
<li>$\tan \dfrac{\pi}{4}=1 \iff \arctan 1=\frac{\pi}{4}$</li>
<li>$\sec \dfrac{\pi}{3}=2 \iff \operatorname{arcsec} 2=\frac{\pi}{3}$</li>
<li>$\csc \dfrac{\pi}{6}=2 \iff \operatorname{arccsc} 2=\frac{\pi}{6}$</li>
</ul>
<hr />
<h2>5. 易混淆点：$\sin^{-1} x$ 不等同于倒数</h2>
<p>在很多国外教材和计算器上，反三角函数也常写成指数为 $-1$ 的形式：</p>
<p>$$
\sin^{-1} x = \arcsin x
$$</p>
<p>$$
\cos^{-1} x = \arccos x
$$</p>
<p>$$
\tan^{-1} x = \arctan x
$$</p>
<p>注意：<strong>这里的 $-1$ 表示反函数，绝不是倒数！</strong></p>
<p>它们与真正的倒数之间满足：</p>
<p>$$
\sin^{-1} x = \arcsin x \ne \frac{1}{\sin x} = \csc x
$$</p>
<p>$$
\cos^{-1} x = \arccos x \ne \frac{1}{\cos x} = \sec x
$$</p>
<p>$$
\tan^{-1} x = \arctan x \ne \frac{1}{\tan x} = \cot x
$$</p>
<p>这在初学微积分时非常容易混淆，务必区分。</p>
<hr />
<h2>6. 三角函数求导公式</h2>
<p>高等数学中六个基本三角函数的求导公式如下：</p>
<p>$$
(\sin x)'=\cos x
$$</p>
<p>$$
(\cos x)'=-\sin x
$$</p>
<p>$$
(\tan x)'=\sec^2 x
$$</p>
<p>$$
(\cot x)'=-\csc^2 x
$$</p>
<p>$$
(\sec x)'=\sec x\tan x
$$</p>
<p>$$
(\csc x)'=-\csc x\cot x
$$</p>
<h3>记忆规律</h3>
<p>可以利用余角前缀来方便记忆：</p>
<p><strong>凡是以“co”开头的三角函数（余弦 $\cos x$、余切 $\cot x$、余割 $\csc x$），求导结果一定带负号。</strong></p>
<hr />
<h2>7. 基本积分公式</h2>
<p>积分与求导互为逆运算。根据三角函数导数公式，可以直接得出对应的直接积分公式：</p>
<p>因为 $(\tan x)'=\sec^2 x$，所以：</p>
<p>$$
\int \sec^2 x ,dx=\tan x+C
$$</p>
<p>因为 $(\sec x)'=\sec x\tan x$，所以：</p>
<p>$$
\int \sec x\tan x ,dx=\sec x+C
$$</p>
<p>因为 $(\cot x)'=-\csc^2 x$，所以：</p>
<p>$$
\int \csc^2 x ,dx=-\cot x+C
$$</p>
<p>因为 $(\csc x)'=-\csc x\cot x$，所以：</p>
<p>$$
\int \csc x\cot x ,dx=-\csc x+C
$$</p>
<p>这四个积分建议与对应的导数公式成对对照记忆。</p>
<hr />
<h2>8. 常见三角函数的积分推导</h2>
<p>除了上述直接积分，以下四个基本三角函数的积分也非常重要：</p>
<h3>正切函数的积分</h3>
<p>$$
\int \tan x ,dx = \int \frac{\sin x}{\cos x} ,dx
$$</p>
<p>令 $u=\cos x$，则有 $du=-\sin x ,dx$，代入可得：</p>
<p>$$
\int \tan x ,dx = -\ln|\cos x|+C
$$</p>
<p>因为 $\sec x = \dfrac{1}{\cos x}$，根据对数性质可化为：</p>
<p>$$
\int \tan x ,dx = \ln|\sec x|+C
$$</p>
<hr />
<h3>余切函数的积分</h3>
<p>$$
\int \cot x ,dx = \int \frac{\cos x}{\sin x} ,dx
$$</p>
<p>令 $u=\sin x$，则有 $du=\cos x ,dx$，代入可得：</p>
<p>$$
\int \cot x ,dx=\ln|\sin x|+C
$$</p>
<p>利用倒数关系，它也可以写成等价形式：</p>
<p>$$
\int \cot x ,dx=-\ln|\csc x|+C
$$</p>
<hr />
<h3>正割与余割的积分</h3>
<p>对于正割函数，其积分为高数中非常基础的公式，建议直接熟记结论：</p>
<p>$$
\int \sec x ,dx=\ln|\sec x+\tan x|+C
$$</p>
<p>余割函数的积分结论为：</p>
<p>$$
\int \csc x ,dx=-\ln|\csc x+\cot x|+C
$$</p>
<p>也可以写成等价形式：</p>
<p>$$
\int \csc x ,dx=\ln|\csc x-\cot x|+C
$$</p>
<hr />
<h2>9. 三次方积分公式</h2>
<p>在分部积分中，经常遇到三次方形式的积分，可以直接使用以下结论：</p>
<p>$$
\int \sec^3 x ,dx = \frac{1}{2} \left[ \sec x\tan x+\ln|\sec x+\tan x| \right]+C
$$</p>
<p>$$
\int \csc^3 x ,dx = -\frac{1}{2} \left[ \csc x\cot x+\ln|\csc x+\cot x| \right]+C
$$</p>
<p><strong>记忆规律</strong>：观察可知，$\sec^3 x$ 与 $\csc^3 x$ 的积分结果中，均包含了其对应的一阶积分项（即 $\int \sec x ,dx$ and $\int \csc x ,dx$ 的结果部分）。</p>
<hr />
<h2>10. 反三角函数相关积分</h2>
<p>在遇到含有二次根式或平方和的积分时，通常对应反三角函数或对数形式：</p>
<h3>第一类：含有 $a^2+x^2$ 的积分</h3>
<p>$$
\int \frac{dx}{a^2+x^2} = \frac{1}{a}\arctan\frac{x}{a}+C
$$</p>
<p>对应求导公式 $(\arctan x)'=\dfrac{1}{1+x^2}$。</p>
<hr />
<h3>第二类：含有 $\sqrt{a^2-x^2}$ 的积分</h3>
<p>$$
\int \frac{dx}{\sqrt{a^2-x^2}} = \arcsin\frac{x}{a}+C
$$</p>
<p>对应求导公式 $(\arcsin x)'=\dfrac{1}{\sqrt{1-x^2}}$。</p>
<hr />
<h3>第三类：含有 $\sqrt{x^2\pm a^2}$ 的积分</h3>
<p>对于此类形式，积分结果通常为对数形式：</p>
<p>$$
\int \frac{dx}{\sqrt{x^2+a^2}} = \ln\left|x+\sqrt{x^2+a^2}\right|+C
$$</p>
<p>$$
\int \frac{dx}{\sqrt{x^2-a^2}} = \ln\left|x+\sqrt{x^2-a^2}\right|+C
$$</p>
<p>可以合并写为：</p>
<p>$$
\int \frac{dx}{\sqrt{x^2\pm a^2}} = \ln\left|x+\sqrt{x^2\pm a^2}\right|+C
$$</p>
<hr />
<h2>11. 三角函数与反三角函数汇总表</h2>
<h3>三角函数定义及关系</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>符号</th>
<th>直角三角形边角关系定义</th>
<th>恒等表示关系</th>
</tr>
</thead>
<tbody>
<tr>
<td>正弦</td>
<td>$\sin x$</td>
<td>$\dfrac{\text{对边}}{\text{斜边}}$</td>
<td>$\sin x = \dfrac{1}{\csc x}$</td>
</tr>
<tr>
<td>余弦</td>
<td>$\cos x$</td>
<td>$\dfrac{\text{邻边}}{\text{斜边}}$</td>
<td>$\cos x = \dfrac{1}{\sec x}$</td>
</tr>
<tr>
<td>正切</td>
<td>$\tan x$</td>
<td>$\dfrac{\text{对边}}{\text{邻边}}$</td>
<td>$\tan x = \dfrac{\sin x}{\cos x} = \dfrac{1}{\cot x}$</td>
</tr>
<tr>
<td>余切</td>
<td>$\cot x$</td>
<td>$\dfrac{\text{邻边}}{\text{对边}}$</td>
<td>$\cot x = \dfrac{\cos x}{\sin x} = \dfrac{1}{\tan x}$</td>
</tr>
<tr>
<td>正割</td>
<td>$\sec x$</td>
<td>$\dfrac{\text{斜边}}{\text{邻边}}$</td>
<td>$\sec x = \dfrac{1}{\cos x}$</td>
</tr>
<tr>
<td>余割</td>
<td>$\csc x$</td>
<td>$\dfrac{\text{斜边}}{\text{对边}}$</td>
<td>$\csc x = \dfrac{1}{\sin x}$</td>
</tr>
</tbody>
</table>
<h3>反三角函数基础性质</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>符号</th>
<th>含义描述</th>
<th>定义域</th>
<th>值域</th>
</tr>
</thead>
<tbody>
<tr>
<td>反正弦</td>
<td>$\arcsin x$</td>
<td>正弦值为 $x$ 的角</td>
<td>$[-1, 1]$</td>
<td>$\left[-\dfrac{\pi}{2}, \dfrac{\pi}{2}\right]$</td>
</tr>
<tr>
<td>反余弦</td>
<td>$\arccos x$</td>
<td>余弦值为 $x$ 的角</td>
<td>$[-1, 1]$</td>
<td>$[0, \pi]$</td>
</tr>
<tr>
<td>反正切</td>
<td>$\arctan x$</td>
<td>正切值为 $x$ 的角</td>
<td>$\mathbb{R}$</td>
<td>$\left(-\dfrac{\pi}{2}, \dfrac{\pi}{2}\right)$</td>
</tr>
<tr>
<td>反余切</td>
<td>$\operatorname{arccot} x$</td>
<td>余切值为 $x$ 的角</td>
<td>$\mathbb{R}$</td>
<td>$(0, \pi)$</td>
</tr>
<tr>
<td>反正割</td>
<td>$\operatorname{arcsec} x$</td>
<td>正割值为 $x$ 的角</td>
<td>$(-\infty, -1] \cup [1, +\infty)$</td>
<td>$\left[0, \dfrac{\pi}{2}\right) \cup \left(\dfrac{\pi}{2}, \pi\right]$</td>
</tr>
<tr>
<td>反余割</td>
<td>$\operatorname{arccsc} x$</td>
<td>余割值为 $x$ 的角</td>
<td>$(-\infty, -1] \cup [1, +\infty)$</td>
<td>$\left[-\dfrac{\pi}{2}, 0\right) \cup \left(0, \dfrac{\pi}{2}\right]$</td>
</tr>
</tbody>
</table>
<p>其中高数积分中出现最频繁的是反正弦 $\arcsin x$ 与 反正切 $\arctan x$：</p>
<p>$$
\int \frac{dx}{\sqrt{1-x^2}}=\arcsin x+C
$$</p>
<p>$$
\int \frac{dx}{1+x^2}=\arctan x+C
$$</p>
<hr />
<h2>12. 公式记忆优先级总结</h2>
<p>为了高效复习，可以将上述公式分为三档进行记忆：</p>
<h3>第一档：基础恒等式与求导（必须熟练）</h3>
<p>$$
\sin^2 x+\cos^2 x=1
$$</p>
<p>$$
\tan x=\frac{\sin x}{\cos x},\quad \sec x=\frac{1}{\cos x},\quad \csc x=\frac{1}{\sin x}
$$</p>
<p>$$
(\sin x)'=\cos x,\quad (\cos x)'=-\sin x
$$</p>
<p>$$
(\tan x)'=\sec^2 x,\quad (\cot x)'=-\csc^2 x
$$</p>
<h3>第二档：高频积分公式（直接记忆）</h3>
<p>$$
\int \sec^2 x ,dx=\tan x+C,\quad \int \csc^2 x ,dx=-\cot x+C
$$</p>
<p>$$
\int \tan x ,dx=\ln|\sec x|+C,\quad \int \cot x ,dx=\ln|\sin x|+C
$$</p>
<p>$$
\int \sec x ,dx=\ln|\sec x+\tan x|+C
$$</p>
<p>$$
\int \csc x ,dx=-\ln|\csc x+\cot x|+C
$$</p>
<h3>第三档：结构特征联想（看到形式想结果）</h3>
<ul>
<li>$a^2+x^2 \implies$ 联想到 $\arctan$</li>
<li>$\sqrt{a^2-x^2} \implies$ 联想到 $\arcsin$</li>
<li>$\sqrt{x^2\pm a^2} \implies$ 联想到 $\ln$</li>
</ul>
<blockquote>
<p><strong>注意</strong>：以上所有三角函数公式默认 $x$ 采用弧度制，而非角度制。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="高等数学"/>
  </entry>
  <entry>
    <title>一维随机变量及其分布</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E4%B8%80%E7%BB%B4%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E5%8F%8A%E5%85%B6%E5%88%86%E5%B8%83/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E4%B8%80%E7%BB%B4%E9%9A%8F%E6%9C%BA%E5%8F%98%E9%87%8F%E5%8F%8A%E5%85%B6%E5%88%86%E5%B8%83/</id>
    <published>2026-06-20T11:19:08.000Z</published>
    <updated>2026-06-20T11:19:08.000Z</updated>
    <summary>系统梳理一维随机变量的定义、分布函数、离散型与连续型随机变量、常见概率分布以及随机变量函数的分布，并提供常考题型解析与易错点总结。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1158738164&amp;p=4" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1159898902&amp;p=5" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文按“概念 → 分布 → 常见分布 → 函数分布 → 题型”的顺序梳理<strong>一维随机变量及其分布</strong>。这一章可以理解为：上一章研究的是“事件的概率”，这一章开始把随机结果<strong>数量化</strong>，研究一个随机变量取各种数值的概率规律。</p>
<hr />
<h2>1. 什么是随机变量</h2>
<p>随机变量本质上是一个函数，它把随机试验的结果映射成一个实数。</p>
<p>记作：</p>
<p>$$
X=X(\omega)
$$</p>
<p>其中 $\omega$ 是样本点，$X(\omega)$ 是对应的数值。</p>
<p>例如抛一枚硬币，样本空间为：</p>
<p>$$
\Omega={\text{正面}, \text{反面}}
$$</p>
<p>可以定义随机变量：</p>
<p>$$
X=
\begin{cases}
1, &amp; \text{出现正面} \
0, &amp; \text{出现反面}
\end{cases}
$$</p>
<p>这样，原来的“正面、反面”就变成了数字 $1,0$。</p>
<p>所以：</p>
<p>$$
\boxed{\text{随机变量就是把随机试验的结果用数字表示出来}}
$$</p>
<hr />
<h2>2. 一维随机变量</h2>
<p>如果一个随机变量只用一个数来描述结果，就叫<strong>一维随机变量</strong>。</p>
<p>例如：</p>
<table>
<thead>
<tr>
<th>随机试验</th>
<th>随机变量</th>
</tr>
</thead>
<tbody>
<tr>
<td>掷一个骰子</td>
<td>$X = \text{掷出的点数}$</td>
</tr>
<tr>
<td>抽查产品</td>
<td>$X = \text{次品个数}$</td>
</tr>
<tr>
<td>等公交</td>
<td>$X = \text{等待时间}$</td>
</tr>
<tr>
<td>测量身高</td>
<td>$X = \text{某人的身高}$</td>
</tr>
<tr>
<td>射击</td>
<td>$X = \text{命中次数}$</td>
</tr>
</tbody>
</table>
<p>这里的 $X$ 都是一个数，所以是<strong>一维随机变量</strong>。</p>
<hr />
<h2>3. 随机变量的分布</h2>
<p>随机变量的分布描述的是：</p>
<p>$$
\boxed{\text{随机变量取不同数值的概率规律}}
$$</p>
<p>比如掷骰子，令：</p>
<p>$$
X=\text{掷出的点数}
$$</p>
<p>那么：</p>
<p>$$
P(X=1)=\frac{1}{6}
$$</p>
<p>$$
P(X=2)=\frac{1}{6}
$$</p>
<p>一直到：</p>
<p>$$
P(X=6)=\frac{1}{6}
$$</p>
<p>这就是 $X$ 的分布。</p>
<hr />
<h2>4. 分布函数</h2>
<p>不管随机变量是离散型还是连续型，都可以用<strong>分布函数</strong>描述。</p>
<p>随机变量 $X$ 的分布函数定义为：</p>
<p>$$
\boxed{F(x)=P(X\le x)}
$$</p>
<p>它表示随机变量 $X$ 取值不超过 $x$ 的概率。</p>
<p>例如：</p>
<p>$$
F(3)=P(X\le 3)
$$</p>
<p>意思是：</p>
<p>$$
X \text{ 取值小于等于 3 的概率}
$$</p>
<hr />
<h3>分布函数的性质</h3>
<h4>性质一：单调不减</h4>
<p>如果 $x_1&lt;x_2$，那么：</p>
<p>$$
F(x_1)\le F(x_2)
$$</p>
<p>因为 $X\le x_1$ 的范围一定包含在 $X\le x_2$ 里面。</p>
<h4>性质二：取值范围在 0 到 1 之间</h4>
<p>$$
0\le F(x)\le 1
$$</p>
<p>因为它本质上是概率。</p>
<h4>性质三：两端极限</h4>
<p>$$
\lim_{x\to -\infty}F(x)=0
$$</p>
<p>$$
\lim_{x\to +\infty}F(x)=1
$$</p>
<p>意思是：</p>
<p>当 $x$ 非常小时，$X\le x$ 几乎不可能发生；</p>
<p>当 $x$ 非常大时，$X\le x$ 几乎必然发生。</p>
<h4>性质四：右连续</h4>
<p>分布函数总是右连续的：</p>
<p>$$
F(x+0)=F(x)
$$</p>
<p>这个性质考试中一般不会重点计算，但要知道。</p>
<hr />
<h2>5. 用分布函数求概率</h2>
<p>分布函数最重要的用法是求区间概率。</p>
<p>$$
\boxed{P(a&lt;X\le b)=F(b)-F(a)}
$$</p>
<p>注意这里是 $a&lt;X\le b$，左开右闭。</p>
<p>例如：</p>
<p>$$
P(2&lt;X\le 5)=F(5)-F(2)
$$</p>
<p>还有：</p>
<p>$$
P(X&gt;b)=1-F(b)
$$</p>
<p>因为：</p>
<p>$$
P(X&gt;b)=1-P(X\le b)
$$</p>
<p>也就是：</p>
<p>$$
P(X&gt;b)=1-F(b)
$$</p>
<hr />
<h2>6. 离散型随机变量</h2>
<p>如果随机变量只能取有限个或可列个值，就叫<strong>离散型随机变量</strong>。</p>
<p>例如：</p>
<ul>
<li>掷骰子的点数；</li>
<li>抛硬币正面次数；</li>
<li>一批产品中的次品数；</li>
<li>某人一天收到的快递数。</li>
</ul>
<p>这些值通常是一个一个分开的。</p>
<hr />
<h3>离散型随机变量的分布律</h3>
<p>若随机变量 $X$ 可能取 $x_1,x_2,\dots,x_n,\dots$，并且：</p>
<p>$$
P(X=x_i)=p_i
$$</p>
<p>那么称：</p>
<p>$$
\boxed{P(X=x_i)=p_i,\quad i=1,2,\dots}
$$</p>
<p>为 $X$ 的<strong>分布律</strong>。</p>
<p>通常写成表格：</p>
<p>$$
\begin{array}{c|cccc}
X &amp; x_1 &amp; x_2 &amp; \cdots &amp; x_n \
\hline
P &amp; p_1 &amp; p_2 &amp; \cdots &amp; p_n
\end{array}
$$</p>
<hr />
<h3>分布律必须满足两个条件</h3>
<p>第一：</p>
<p>$$
p_i\ge 0
$$</p>
<p>第二：</p>
<p>$$
\sum_i p_i=1
$$</p>
<p>也就是说，每个概率不能为负，而且所有可能取值的概率加起来必须等于 1。</p>
<hr />
<h3>离散型随机变量例题</h3>
<p>掷一个均匀骰子，令：</p>
<p>$$
X=\text{掷出的点数}
$$</p>
<p>则 $X=1,2,3,4,5,6$，并且：</p>
<p>$$
P(X=k)=\frac{1}{6},\quad k=1,2,3,4,5,6
$$</p>
<p>分布律为：</p>
<p>$$
\begin{array}{c|cccccc}
X &amp; 1 &amp; 2 &amp; 3 &amp; 4 &amp; 5 &amp; 6 \
\hline
P &amp; \frac{1}{6} &amp; \frac{1}{6} &amp; \frac{1}{6} &amp; \frac{1}{6} &amp; \frac{1}{6} &amp; \frac{1}{6}
\end{array}
$$</p>
<hr />
<h3>离散型随机变量的分布函数</h3>
<p>对于离散型随机变量：</p>
<p>$$
\boxed{F(x)=P(X\le x)=\sum_{x_i\le x}p_i}
$$</p>
<p>也就是把所有不超过 $x$ 的取值概率加起来。</p>
<p>例如掷骰子：</p>
<p>$$
F(3)=P(X\le 3)
$$</p>
<p>$$
=P(X=1)+P(X=2)+P(X=3)
$$</p>
<p>$$
=\frac{1}{6}+\frac{1}{6}+\frac{1}{6}=\frac{1}{2}
$$</p>
<hr />
<h2>7. 连续型随机变量</h2>
<p>如果随机变量可以在某个区间内连续取值，就叫<strong>连续型随机变量</strong>。</p>
<p>例如：</p>
<ul>
<li>等车时间；</li>
<li>零件寿命；</li>
<li>身高；</li>
<li>温度；</li>
<li>误差。</li>
</ul>
<p>这些变量不是一个一个孤立的值，而是在区间内连续变化。</p>
<hr />
<h3>概率密度函数</h3>
<p>对于连续型随机变量，不能像离散型那样直接列出 $P(X=x_i)$，因为连续型随机变量在某一个点上的概率通常为 0：</p>
<p>$$
P(X=a)=0
$$</p>
<p>所以连续型随机变量用<strong>概率密度函数</strong>表示，记作 $f(x)$，它满足：</p>
<p>$$
f(x)\ge 0
$$</p>
<p>并且：</p>
<p>$$
\int_{-\infty}^{+\infty} f(x),dx=1
$$</p>
<hr />
<h3>分布函数和密度函数的关系</h3>
<p>如果 $X$ 是连续型随机变量，密度函数为 $f(x)$，则：</p>
<p>$$
\boxed{F(x)=P(X\le x)=\int_{-\infty}^{x}f(t),dt}
$$</p>
<p>反过来，如果 $F(x)$ 可导，则：</p>
<p>$$
\boxed{f(x)=F'(x)}
$$</p>
<hr />
<h3>连续型随机变量求区间概率</h3>
<p>对于连续型随机变量：</p>
<p>$$
\boxed{P(a&lt;X\le b)=\int_a^b f(x),dx}
$$</p>
<p>因为单点概率为 0，所以：</p>
<p>$$
P(a&lt;X&lt;b)=P(a\le X\le b)=P(a&lt;X\le b)
$$</p>
<p>它们都等于：</p>
<p>$$
\int_a^b f(x),dx
$$</p>
<p>这是连续型随机变量和离散型随机变量的重要区别。</p>
<hr />
<h2>8. 离散型和连续型的核心区别</h2>
<table>
<thead>
<tr>
<th>类型</th>
<th>概率怎么给</th>
<th>单点概率</th>
<th>求区间概率</th>
</tr>
</thead>
<tbody>
<tr>
<td>离散型</td>
<td>分布律 $P(X=x_i)=p_i$</td>
<td>可能大于 0</td>
<td>概率相加</td>
</tr>
<tr>
<td>连续型</td>
<td>密度函数 $f(x)$</td>
<td>通常等于 0</td>
<td>积分求面积</td>
</tr>
</tbody>
</table>
<p>简单记：</p>
<p>$$
\boxed{\text{离散型靠“加”，连续型靠“积分”}}
$$</p>
<hr />
<h2>9. 常见离散型分布</h2>
<h3>9.1 两点分布</h3>
<p>如果随机变量 $X$ 只取 $0$ 和 $1$，并且：</p>
<p>$$
P(X=1)=p
$$</p>
<p>$$
P(X=0)=1-p
$$</p>
<p>则称 $X$ 服从两点分布，也叫 <strong>0-1 分布</strong>。</p>
<p>记作：</p>
<p>$$
X\sim B(1,p)
$$</p>
<p>例如一次射击：</p>
<p>$$
X=
\begin{cases}
1, &amp; \text{命中} \
0, &amp; \text{未命中}
\end{cases}
$$</p>
<p>若命中率是 $p$，则：</p>
<p>$$
P(X=1)=p
$$</p>
<p>$$
P(X=0)=1-p
$$</p>
<hr />
<h2>9.2 二项分布</h2>
<p>如果进行 $n$ 次独立重复试验，每次成功概率为 $p$，令：</p>
<p>$$
X=\text{成功次数}
$$</p>
<p>则：</p>
<p>$$
X\sim B(n,p)
$$</p>
<p>它的分布律为：</p>
<p>$$
\boxed{P(X=k)=C_n^k p^k(1-p)^{n-k},\quad k=0,1,2,\dots,n}
$$</p>
<p>二项分布就是前面提到的<strong>伯努利概型</strong>对应的随机变量分布。</p>
<p>例如某人命中率为 $0.8$，独立射击 $5$ 次，令：</p>
<p>$$
X=\text{命中次数}
$$</p>
<p>则：</p>
<p>$$
X\sim B(5,0.8)
$$</p>
<p>恰好命中 $3$ 次：</p>
<p>$$
P(X=3)=C_5^3(0.8)^3(0.2)^2
$$</p>
<hr />
<h2>9.3 泊松分布</h2>
<p>如果随机变量 $X$ 表示某段时间、某个区域内某事件发生的次数，并且平均发生次数为 $\lambda$，则常用泊松分布。</p>
<p>记作：</p>
<p>$$
X\sim P(\lambda)
$$</p>
<p>分布律为：</p>
<p>$$
\boxed{P(X=k)=\frac{\lambda^k}{k!}e^{-\lambda},\quad k=0,1,2,\dots}
$$</p>
<p>例如：</p>
<ul>
<li>某路口一分钟内通过的车辆数；</li>
<li>某商店一天内到店顾客数；</li>
<li>某机器一小时内故障次数；</li>
<li>某页书中的印刷错误数。</li>
</ul>
<p>其中 $\lambda$ 表示平均发生次数。</p>
<hr />
<h2>10. 常见连续型分布</h2>
<h3>10.1 均匀分布</h3>
<p>如果随机变量 $X$ 在区间 $[a,b]$ 上每个位置“机会均等”，则称 $X$ 服从均匀分布。</p>
<p>记作：</p>
<p>$$
X\sim U(a,b)
$$</p>
<p>其密度函数中的常数公式：</p>
<p>$$
f(x)=\frac{1}{b-a}
$$</p>
<p>来自于均匀分布的定义。</p>
<p>如果 $X\sim U(a,b)$，意思是说：$X$ 在区间 $[a,b]$ 上每一小段出现的可能性都一样。</p>
<p>所以它的概率密度函数 $f(x)$ 应该是一个常数，设为：</p>
<p>$$
f(x)=c,\quad a\le x\le b
$$</p>
<p>因为概率密度函数有一个基本要求：</p>
<p>$$
\int_{-\infty}^{+\infty} f(x),dx=1
$$</p>
<p>而 $X$ 只在 $[a,b]$ 上取值，在其他区域概率密度为 0，所以有：</p>
<p>$$
\int_a^b c,dx=1
$$</p>
<p>计算积分可得：</p>
<p>$$
c(b-a)=1 \implies c=\frac{1}{b-a}
$$</p>
<p>因此，均匀分布的密度函数为：</p>
<p>$$
f(x)=
\begin{cases}
\dfrac{1}{b-a}, &amp; a\le x\le b \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>例如随机等待时间在 $0$ 到 $10$ 分钟之间均匀分布：</p>
<p>$$
X\sim U(0,10)
$$</p>
<p>则：</p>
<p>$$
P(2&lt;X&lt;5)=\frac{5-2}{10-0}=\frac{3}{10}
$$</p>
<hr />
<h3>10.2 指数分布</h3>
<p>如果随机变量 $X$ 表示“等待时间”或“寿命”，常用指数分布。</p>
<p>记作：</p>
<p>$$
X\sim E(\lambda)
$$</p>
<p>密度函数为：</p>
<p>$$
f(x)=
\begin{cases}
\lambda e^{-\lambda x}, &amp; x&gt;0 \
0, &amp; x\le 0
\end{cases}
$$</p>
<p>分布函数为：</p>
<p>$$
F(x)=
\begin{cases}
1-e^{-\lambda x}, &amp; x&gt;0 \
0, &amp; x\le 0
\end{cases}
$$</p>
<p>所以：</p>
<p>$$
P(X&gt;x)=1-F(x)=e^{-\lambda x}
$$</p>
<p>指数分布常见于：</p>
<ul>
<li>设备寿命；</li>
<li>等车时间；</li>
<li>服务系统中的等待时间。</li>
</ul>
<hr />
<h3>10.3 正态分布</h3>
<p>正态分布是最重要的连续型分布之一。</p>
<p>若：</p>
<p>$$
X\sim N(\mu,\sigma^2)
$$</p>
<p>则密度函数为：</p>
<p>$$
f(x)=\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{(x-\mu)^2}{2\sigma^2}}
$$</p>
<p>其中 $\mu$ 控制中心位置，$\sigma$ 控制曲线离散程度。正态分布曲线呈钟形，关于 $x=\mu$ 对称。</p>
<p>常见例子：</p>
<ul>
<li>身高；</li>
<li>测量误差；</li>
<li>考试成绩；</li>
<li>零件尺寸误差。</li>
</ul>
<p>特别地，当 $\mu=0$ 且 $\sigma^2=1$ 时，称为<strong>标准正态分布</strong>：</p>
<p>$$
X\sim N(0,1)
$$</p>
<p>通常记作：</p>
<p>$$
Z\sim N(0,1)
$$</p>
<hr />
<h3>标准正态分布的计算与标准化</h3>
<p>在实际做题中，我们经常遇到一般的正态分布，此时需要用到<strong>标准化法</strong>。其核心思想是：将一般正态分布 $X\sim N(\mu,\sigma^2)$ 转化为标准正态分布 $Z\sim N(0,1)$，再利用标准正态分布的分布函数 $\Phi(x)$ 进行计算。</p>
<h4>标准化公式</h4>
<p>若 $X\sim N(\mu,\sigma^2)$，则：</p>
<p>$$
Z = \frac{X-\mu}{\sigma} \sim N(0,1)
$$</p>
<h4>典型简例</h4>
<p>设随机变量 $X\sim N(2,\sigma^2)$，且 $P(2&lt;X&lt;4)=0.3$，求 $P(X&lt;0)$。</p>
<p>用文字描述就是：<strong>已知 $X$ 服从均值为 $2$、方差为 $\sigma^2$ 的正态分布，并且 $X$ 落在区间 $(2,4)$ 内的概率是 $0.3$，求 $X$ 小于 $0$ 的概率。</strong></p>
<p><strong>【解析】</strong></p>
<p><strong>第一步：标准化已知条件，求 $\Phi\left(\dfrac{2}{\sigma}\right)$</strong></p>
<p>因为 $\mu=2$，我们对已知概率进行标准化转换：</p>
<p>$$
\begin{aligned}
P(2&lt;X&lt;4) &amp;= P\left(\frac{2-2}{\sigma} &lt; \frac{X-2}{\sigma} &lt; \frac{4-2}{\sigma}\right) \
&amp;= P\left(0 &lt; Z &lt; \frac{2}{\sigma}\right)
\end{aligned}
$$</p>
<p>利用标准正态分布函数 $\Phi(x)$ 表示：</p>
<p>$$
P\left(0 &lt; Z &lt; \frac{2}{\sigma}\right) = \Phi\left(\frac{2}{\sigma}\right) - \Phi(0)
$$</p>
<p>因为标准正态分布在原点处的分布函数值 $\Phi(0)=0.5$，所以：</p>
<p>$$
0.3 = \Phi\left(\frac{2}{\sigma}\right) - 0.5 \implies \Phi\left(\frac{2}{\sigma}\right) = 0.8
$$</p>
<p><strong>第二步：标准化待求概率，求 $P(X&lt;0)$</strong></p>
<p>同样地，对 $P(X&lt;0)$ 进行标准化转换：</p>
<p>$$
\begin{aligned}
P(X&lt;0) &amp;= P\left(\frac{X-2}{\sigma} &lt; \frac{0-2}{\sigma}\right) \
&amp;= P\left(Z &lt; -\frac{2}{\sigma}\right)
\end{aligned}
$$</p>
<p>写成标准正态分布函数的形式：</p>
<p>$$
P(X&lt;0) = \Phi\left(-\frac{2}{\sigma}\right)
$$</p>
<p>利用标准正态分布的对称性质 $\Phi(-x) = 1 - \Phi(x)$，可得：</p>
<p>$$
\Phi\left(-\frac{2}{\sigma}\right) = 1 - \Phi\left(\frac{2}{\sigma}\right)
$$</p>
<p>将第一步求得的结果 $\Phi\left(\dfrac{2}{\sigma}\right) = 0.8$ 代入上式：</p>
<p>$$
P(X&lt;0) = 1 - 0.8 = 0.2
$$</p>
<p>所以，最终答案为：</p>
<p>$$
\boxed{0.2}
$$</p>
<p>这个方法可以总结为：</p>
<p>$$
\boxed{\text{先标准化，再用标准正态分布函数 } \Phi(x) \text{ 计算}}
$$</p>
<hr />
<h2>11. 随机变量函数的分布</h2>
<p>有时题目不是直接问 $X$ 的分布，而是问：</p>
<p>$$
Y=g(X)
$$</p>
<p>的分布。这叫做<strong>随机变量函数的分布</strong>。</p>
<hr />
<h3>离散型情况</h3>
<p>如果 $X$ 是离散型随机变量，可以直接代入。</p>
<p>例如：</p>
<p>$$
\begin{array}{c|ccc}
X &amp; -1 &amp; 0 &amp; 2 \
\hline
P &amp; 0.2 &amp; 0.3 &amp; 0.5
\end{array}
$$</p>
<p>令：</p>
<p>$$
Y=X^2
$$</p>
<p>则：</p>
<ul>
<li>$X=-1 \Rightarrow Y=1$</li>
<li>$X=0 \Rightarrow Y=0$</li>
<li>$X=2 \Rightarrow Y=4$</li>
</ul>
<p>所以：</p>
<p>$$
\begin{array}{c|ccc}
Y &amp; 0 &amp; 1 &amp; 4 \
\hline
P &amp; 0.3 &amp; 0.2 &amp; 0.5
\end{array}
$$</p>
<p>如果不同的 $X$ 对应同一个 $Y$，要把概率合并。</p>
<hr />
<h3>连续型情况</h3>
<p>连续型随机变量函数的分布，常用分布函数法：</p>
<p>先写：</p>
<p>$$
F_Y(y)=P(Y\le y)
$$</p>
<p>再把 $Y=g(X)$ 代进去，转化成关于 $X$ 的不等式。</p>
<p>例如：</p>
<p>$$
Y=2X+1
$$</p>
<p>则：</p>
<p>$$
\begin{aligned}
F_Y(y)&amp;=P(Y\le y) \
&amp;=P(2X+1\le y) \
&amp;=P\left(X\le \frac{y-1}{2}\right) \
&amp;=F_X\left(\frac{y-1}{2}\right)
\end{aligned}
$$</p>
<p>这类题的核心是：</p>
<p>$$
\boxed{\text{先写分布函数，再化成 } X \text{ 的不等式}}
$$</p>
<hr />
<p>再来看一个经典的例题。</p>
<p><strong>【例题】</strong></p>
<p>设随机变量 $X$ 在区间 $(0,2)$ 上服从均匀分布，</p>
<p>$$
Y=X^3
$$</p>
<p>求：</p>
<p>(1) $Y$ 的分布函数 $F_Y(y)$；</p>
<p>(2) $Y$ 的密度函数 $f_Y(y)$。</p>
<p><strong>【解析】</strong></p>
<p>因为：</p>
<p>$$
X\sim U(0,2)
$$</p>
<p>所以 $X$ 的密度函数为：</p>
<p>$$
f_X(x)=
\begin{cases}
\dfrac{1}{2}, &amp; 0&lt;x&lt;2 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>又因为：</p>
<p>$$
Y=X^3
$$</p>
<p>当 $X\in(0,2)$ 时，有 $Y\in(0,8)$。</p>
<p>所以要求 $F_Y(y)$，需要分三段讨论：</p>
<hr />
<h4>第一段：$y&lt;0$</h4>
<p>因为 $Y=X^3&gt;0$，所以：</p>
<p>$$
P(Y\le y)=0
$$</p>
<p>因此：</p>
<p>$$
F_Y(y)=0,\quad y&lt;0
$$</p>
<hr />
<h4>第二段：$0\le y&lt;8$</h4>
<p>$$
F_Y(y)=P(Y\le y)
$$</p>
<p>因为 $Y=X^3$，所以：</p>
<p>$$
F_Y(y)=P(X^3\le y)
$$</p>
<p>两边开三次方：</p>
<p>$$
F_Y(y)=P(X\le \sqrt[3]{y})
$$</p>
<p>由于 $X$ 在 $(0,2)$ 上均匀分布，所以概率等于区间长度之比：</p>
<p>$$
P(0&lt;X\le \sqrt[3]{y}) = \frac{\sqrt[3]{y}-0}{2-0}
$$</p>
<p>所以：</p>
<p>$$
F_Y(y)=\frac{\sqrt[3]{y}}{2},\quad 0\le y&lt;8
$$</p>
<hr />
<h4>第三段：$y\ge 8$</h4>
<p>因为 $Y$ 最大到 $8$，所以当 $y\ge 8$ 时，$Y\le y$ 是必然事件：</p>
<p>$$
P(Y\le y)=1
$$</p>
<p>因此：</p>
<p>$$
F_Y(y)=1,\quad y\ge 8
$$</p>
<hr />
<p>综上所述，$Y$ 的分布函数是：</p>
<p>$$
F_Y(y)=
\begin{cases}
0, &amp; y&lt;0 \[4pt]
\dfrac{\sqrt[3]{y}}{2}, &amp; 0\le y&lt;8 \[4pt]
1, &amp; y\ge 8
\end{cases}
$$</p>
<hr />
<p>接下来求密度函数。连续型随机变量的密度函数是分布函数的导数：</p>
<p>$$
f_Y(y)=F_Y'(y)
$$</p>
<p>对中间这一段求导：</p>
<p>$$
F_Y(y)=\frac{1}{2}y^{1/3}
$$</p>
<p>可得：</p>
<p>$$
f_Y(y)=\frac{1}{2}\cdot \frac{1}{3}y^{-2/3}
$$</p>
<p>即：</p>
<p>$$
f_Y(y)=\frac{1}{6y^{2/3}},\quad 0&lt;y&lt;8
$$</p>
<p>其他地方密度为 0。</p>
<p>所以，$Y$ 的密度函数为：</p>
<p>$$
f_Y(y)=
\begin{cases}
\dfrac{1}{6y^{2/3}}, &amp; 0&lt;y&lt;8 \[4pt]
0, &amp; \text{其他}
\end{cases}
$$</p>
<hr />
<p>这题的关键就是：</p>
<p>$$
\boxed{\text{先求 } F_Y(y)=P(Y\le y)\text{，再对 } F_Y(y)\text{ 求导得到 } f_Y(y)}
$$</p>
<hr />
<h2>12. 本章常考题型</h2>
<h3>题型一：由分布律求概率</h3>
<p>给你：</p>
<p>$$
\begin{array}{c|cccc}
X &amp; 0 &amp; 1 &amp; 2 &amp; 3 \
\hline
P &amp; 0.1 &amp; 0.2 &amp; 0.3 &amp; 0.4
\end{array}
$$</p>
<p>求：</p>
<p>$$
P(X\ge 2)
$$</p>
<p>直接加：</p>
<p>$$
\begin{aligned}
P(X\ge 2)&amp;=P(X=2)+P(X=3) \
&amp;=0.3+0.4 \
&amp;=0.7
\end{aligned}
$$</p>
<hr />
<h3>题型二：由分布律求分布函数</h3>
<p>如果：</p>
<p>$$
\begin{array}{c|ccc}
X &amp; 1 &amp; 2 &amp; 4 \
\hline
P &amp; 0.2 &amp; 0.5 &amp; 0.3
\end{array}
$$</p>
<p>则：</p>
<p>$$
F(x)=P(X\le x)
$$</p>
<p>所以：</p>
<p>$$
F(x)=
\begin{cases}
0, &amp; x&lt;1 \
0.2, &amp; 1\le x&lt;2 \
0.7, &amp; 2\le x&lt;4 \
1, &amp; x\ge 4
\end{cases}
$$</p>
<p>离散型随机变量的分布函数是<strong>阶梯函数</strong>。</p>
<hr />
<h3>题型三：由密度函数求常数</h3>
<p>如果：</p>
<p>$$
f(x)=
\begin{cases}
cx, &amp; 0&lt;x&lt;2 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>求 $c$。</p>
<p>因为密度函数总面积为 1：</p>
<p>$$
\int_{-\infty}^{+\infty}f(x),dx=1
$$</p>
<p>所以：</p>
<p>$$
\begin{aligned}
\int_0^2 cx,dx&amp;=1 \
c\cdot \frac{x^2}{2}\bigg|_0^2&amp;=1 \
2c&amp;=1 \
c&amp;=\frac{1}{2}
\end{aligned}
$$</p>
<hr />
<h3>题型四：由密度函数求概率</h3>
<p>仍设：</p>
<p>$$
f(x)=
\begin{cases}
\frac{1}{2} x, &amp; 0&lt;x&lt;2 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>求：</p>
<p>$$
P(1&lt;X&lt;2)
$$</p>
<p>则：</p>
<p>$$
\begin{aligned}
P(1&lt;X&lt;2)&amp;=\int_1^2 \frac{1}{2} x,dx \
&amp;=\frac{1}{2}\cdot \frac{x^2}{2}\bigg|_1^2 \
&amp;=\frac{1}{4}(4-1) \
&amp;=\frac{3}{4}
\end{aligned}
$$</p>
<hr />
<h3>题型五：由密度函数求分布函数</h3>
<p>已知：</p>
<p>$$
f(x)=
\begin{cases}
2x, &amp; 0&lt;x&lt;1 \
0, &amp; \text{其他}
\end{cases}
$$</p>
<p>求 $F(x)$。</p>
<p>分段讨论：</p>
<p>当 $x\le 0$ 时：</p>
<p>$$
F(x)=0
$$</p>
<p>当 $0&lt;x&lt;1$ 时：</p>
<p>$$
F(x)=\int_0^x 2t,dt=x^2
$$</p>
<p>当 $x\ge 1$ 时：</p>
<p>$$
F(x)=1
$$</p>
<p>所以：</p>
<p>$$
F(x)=
\begin{cases}
0, &amp; x\le 0 \
x^2, &amp; 0&lt;x&lt;1 \
1, &amp; x\ge 1
\end{cases}
$$</p>
<hr />
<h2>13. 易错点总结</h2>
<h3>易错点一：随机变量不是变量，而是函数</h3>
<p>随机变量 $X$ 是从样本空间到实数的映射。不过初学时可以先把它理解成“随机取值的数”。</p>
<h3>易错点二：分布函数永远是 $P(X\le x)$</h3>
<p>不是 $P(X=x)$，而是 $P(X\le x)$，这个一定要分清。</p>
<h3>易错点三：离散型有单点概率，连续型单点概率为 0</h3>
<p>离散型 $P(X=a)$ 可能大于 0。</p>
<p>连续型 $P(X=a)=0$。</p>
<p>所以连续型中：</p>
<p>$$
P(a&lt;X&lt;b)=P(a\le X\le b)
$$</p>
<p>端点有没有等号不影响结果。</p>
<h3>易错点四：密度函数值不是概率</h3>
<p>对于连续型随机变量，$f(2)$ 不是 $P(X=2)$。密度函数下面的面积才是概率：</p>
<p>$$
P(a&lt;X&lt;b)=\int_a^b f(x),dx
$$</p>
<h3>易错点五：分布函数是累积概率</h3>
<p>分布函数 $F(x)$ 表示从左边一直累积到 $x$ 的概率，所以它一定不会下降。</p>
<hr />
<h2>14. 本章公式总结</h2>
<table>
<thead>
<tr>
<th>内容</th>
<th>公式</th>
</tr>
</thead>
<tbody>
<tr>
<td>分布函数</td>
<td>$F(x)=P(X\le x)$</td>
</tr>
<tr>
<td>区间概率</td>
<td>$P(a&lt;X\le b)=F(b)-F(a)$</td>
</tr>
<tr>
<td>右尾概率</td>
<td>$P(X&gt;x)=1-F(x)$</td>
</tr>
<tr>
<td>离散型分布律</td>
<td>$P(X=x_i)=p_i$</td>
</tr>
<tr>
<td>离散型分布函数</td>
<td>$F(x)=\sum_{x_i\le x}p_i$</td>
</tr>
<tr>
<td>连续型分布函数</td>
<td>$F(x)=\int_{-\infty}^x f(t),dt$</td>
</tr>
<tr>
<td>连续型区间概率</td>
<td>$P(a&lt;X&lt;b)=\int_a^b f(x),dx$</td>
</tr>
<tr>
<td>密度函数求总概率</td>
<td>$\int_{-\infty}^{+\infty}f(x),dx=1$</td>
</tr>
<tr>
<td>密度与分布函数关系</td>
<td>$f(x)=F'(x)$</td>
</tr>
</tbody>
</table>
<hr />
<h2>15. 一句话掌握本章</h2>
<p>这一章的核心是：</p>
<p>$$
\boxed{\text{随机变量把随机结果变成数字，分布描述这些数字出现的概率规律}}
$$</p>
<p>做题时按这个顺序：</p>
<ol>
<li>判断 $X$ 是离散型还是连续型；</li>
<li>离散型用分布律，概率靠加法；</li>
<li>连续型用密度函数，概率靠积分；</li>
<li>遇到分布函数，永远先想到 $F(x)=P(X\le x)$；</li>
<li>遇到随机变量函数，先写 $F_Y(y)=P(Y\le y)$。</li>
</ol>
<h2>16. 框框老师</h2>
<p><img src="https://img.picr2.online/1781955608207_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781957853009_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781958747698_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781958871006_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781959172323_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781959549561_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781959983648_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781960134074_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782033736067_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782034152122_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782034211415_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782034265231_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782034411019_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782112957084_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782113053109_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782114517065_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782114819070_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782114922299_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782115015866_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782115155757_image.webp" alt="Image" />
<img src="https://img.picr2.online/1782115223488_image.webp" alt="Image" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>随机事件及其概率</title>
    <link href="https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E9%9A%8F%E6%9C%BA%E4%BA%8B%E4%BB%B6%E5%8F%8A%E5%85%B6%E6%A6%82%E7%8E%87/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/%E6%A6%82%E7%8E%87%E8%AE%BA/%E9%9A%8F%E6%9C%BA%E4%BA%8B%E4%BB%B6%E5%8F%8A%E5%85%B6%E6%A6%82%E7%8E%87/</id>
    <published>2026-06-18T12:32:52.000Z</published>
    <updated>2026-06-18T12:32:52.000Z</updated>
    <summary>系统梳理随机事件、样本空间、概率公式与常见题型，帮助建立概率论的基础概念框架。</summary>
    <content type="html"><![CDATA[<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1157244433&amp;p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>&lt;iframe width="100%" height="468" src="//player.bilibili.com/player.html?isOutside=true&amp;aid=229588634&amp;bvid=BV168411Z7pf&amp;cid=1157260937&amp;p=2" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"&gt;&lt;/iframe&gt;</p>
<p>本文按“概念 → 公式 → 例题”的顺序梳理<strong>随机事件及其概率</strong>。这一章是概率论的基础，重点回答三个问题：什么是事件，事件之间如何运算，概率如何计算。</p>
<h2>1. 随机试验、样本空间、随机事件</h2>
<p>概率论研究的是带有不确定性的试验，例如：</p>
<p>抛一枚硬币、掷一个骰子、抽一张牌、检测一件产品是否合格。</p>
<p>这类试验通常有三个特点：</p>
<ol>
<li>可以在相同条件下重复进行；</li>
<li>每次试验结果不止一种；</li>
<li>试验前不能确定具体结果，但能知道所有可能结果。</li>
</ol>
<p>这种试验叫做<strong>随机试验</strong>。</p>
<h3>样本点</h3>
<p>随机试验的每一个可能结果叫做<strong>样本点</strong>。</p>
<p>例如掷一个骰子，可能结果是：</p>
<p>$$
1,2,3,4,5,6
$$</p>
<p>每个结果就是一个样本点。</p>
<h3>样本空间</h3>
<p>所有样本点组成的集合叫做<strong>样本空间</strong>，通常记为：</p>
<p>$$
\Omega
$$</p>
<p>例如掷一个骰子：</p>
<p>$$
\Omega={1,2,3,4,5,6}
$$</p>
<h3>随机事件</h3>
<p>样本空间的子集叫做<strong>随机事件</strong>，通常用大写字母表示，如 $A,B,C$。</p>
<p>例如：</p>
<p>事件 $A$：掷出偶数。</p>
<p>$$
A={2,4,6}
$$</p>
<p>事件 $B$：掷出的点数大于 4。</p>
<p>$$
B={5,6}
$$</p>
<p>所以，<strong>事件本质上就是样本空间中的一个集合</strong>。</p>
<hr />
<h2>2. 必然事件、不可能事件、基本事件</h2>
<h3>必然事件</h3>
<p>每次试验一定会发生的事件叫做<strong>必然事件</strong>，就是整个样本空间：</p>
<p>$$
\Omega
$$</p>
<p>例如掷骰子时，“掷出的点数小于 7”一定发生。</p>
<h3>不可能事件</h3>
<p>永远不会发生的事件叫做<strong>不可能事件</strong>，记为：</p>
<p>$$
\varnothing
$$</p>
<p>例如掷普通骰子时，“掷出 8 点”不可能发生。</p>
<h3>基本事件</h3>
<p>只含有一个样本点的事件叫做<strong>基本事件</strong>。</p>
<p>例如掷骰子时：</p>
<p>$$
{1},{2},{3},{4},{5},{6}
$$</p>
<p>都是基本事件。</p>
<hr />
<h2>3. 事件之间的关系</h2>
<p>因为事件是集合，所以事件关系本质上就是集合关系。</p>
<h3>包含关系</h3>
<p>如果事件 $A$ 发生一定导致事件 $B$ 发生，则称 $A$ 包含于 $B$，记作：</p>
<p>$$
A \subset B
$$</p>
<p>例如：</p>
<p>$$
A={6}, \quad B={2,4,6}
$$</p>
<p>若掷出 6，则一定是偶数，所以：</p>
<p>$$
A \subset B
$$</p>
<h3>相等事件</h3>
<p>如果两个事件包含相同的样本点，则它们相等：</p>
<p>$$
A=B
$$</p>
<h3>互斥事件</h3>
<p>如果两个事件不能同时发生，称它们<strong>互斥</strong>，也叫<strong>不相容</strong>。</p>
<p>$$
A \cap B = \varnothing
$$</p>
<p>例如掷骰子：</p>
<p>$$
A={1,3,5}
$$</p>
<p>表示奇数；</p>
<p>$$
B={2,4,6}
$$</p>
<p>表示偶数。</p>
<p>奇数和偶数不能同时发生，所以 $A$ 与 $B$ 互斥。</p>
<h3>对立事件</h3>
<p>如果两个事件互斥，并且合起来是整个样本空间，那么它们互为对立事件。</p>
<p>事件 $A$ 的对立事件记为：</p>
<p>$$
\overline{A}
$$</p>
<p>满足：</p>
<p>$$
A \cap \overline{A}=\varnothing
$$</p>
<p>$$
A \cup \overline{A}=\Omega
$$</p>
<p>例如掷骰子中，若：</p>
<p>$$
A={2,4,6}
$$</p>
<p>表示“掷出偶数”，那么：</p>
<p>$$
\overline{A}={1,3,5}
$$</p>
<p>表示“没有掷出偶数”，也就是“掷出奇数”。</p>
<p>注意：<strong>对立事件一定互斥，但互斥事件不一定对立。</strong></p>
<hr />
<h2>4. 事件的运算</h2>
<h3>并事件</h3>
<p>事件 $A$ 与事件 $B$ 至少有一个发生，记作：</p>
<p>$$
A \cup B
$$</p>
<p>例如：</p>
<p>$$
A={1,2,3}, \quad B={3,4,5}
$$</p>
<p>则：</p>
<p>$$
A \cup B={1,2,3,4,5}
$$</p>
<h3>交事件</h3>
<p>事件 $A$ 与事件 $B$ 同时发生，记作：</p>
<p>$$
A \cap B
$$</p>
<p>例如：</p>
<p>$$
A={1,2,3}, \quad B={3,4,5}
$$</p>
<p>则：</p>
<p>$$
A \cap B={3}
$$</p>
<h3>差事件</h3>
<p>事件 $A$ 发生而事件 $B$ 不发生，记作：</p>
<p>$$
A-B
$$</p>
<p>也可以写成：</p>
<p>$$
A\cap \overline{B}
$$</p>
<p>例如：</p>
<p>$$
A={1,2,3}, \quad B={3,4,5}
$$</p>
<p>则：</p>
<p>$$
A-B={1,2}
$$</p>
<h3>对立事件</h3>
<p>事件 $A$ 不发生，记为：</p>
<p>$$
\overline{A}
$$</p>
<p>若：</p>
<p>$$
\Omega={1,2,3,4,5,6}
$$</p>
<p>$$
A={2,4,6}
$$</p>
<p>则：</p>
<p>$$
\overline{A}={1,3,5}
$$</p>
<hr />
<h2>5. 概率的定义</h2>
<p>概率是用来描述事件发生可能性大小的数。</p>
<p>事件 $A$ 的概率记为：</p>
<p>$$
P(A)
$$</p>
<p>概率满足：</p>
<p>$$
0 \le P(A) \le 1
$$</p>
<p>其中：</p>
<p>$$
P(\Omega)=1
$$</p>
<p>$$
P(\varnothing)=0
$$</p>
<p>也就是说，必然事件概率为 1，不可能事件概率为 0。</p>
<hr />
<h2>6. 古典概型</h2>
<p>古典概型是这一章最常考的内容之一。</p>
<p>它满足两个条件：</p>
<ol>
<li>样本空间中样本点个数有限；</li>
<li>每个基本事件发生的可能性相等。</li>
</ol>
<p>如果事件 $A$ 包含 $m$ 个样本点，样本空间 $\Omega$ 包含 $n$ 个样本点，则：</p>
<p>$$
P(A)=\frac{m}{n}
$$</p>
<p>例如掷一个均匀骰子，求掷出偶数的概率。</p>
<p>样本空间：</p>
<p>$$
\Omega={1,2,3,4,5,6}
$$</p>
<p>事件：</p>
<p>$$
A={2,4,6}
$$</p>
<p>所以：</p>
<p>$$
P(A)=\frac{3}{6}=\frac{1}{2}
$$</p>
<p>古典概型的关键是：<strong>数清楚总情况数和有利情况数。</strong></p>
<hr />
<h2>7. 概率的基本性质</h2>
<h3>性质一：非负性</h3>
<p>$$
P(A)\ge 0
$$</p>
<p>任何事件的概率都不能为负数。</p>
<h3>性质二：规范性</h3>
<p>$$
P(\Omega)=1
$$</p>
<p>整个样本空间一定发生。</p>
<h3>性质三：有限可加性</h3>
<p>如果事件 $A$ 与事件 $B$ 互斥，则：</p>
<p>$$
P(A\cup B)=P(A)+P(B)
$$</p>
<p>例如掷骰子：</p>
<p>$$
A={1}, \quad B={2}
$$</p>
<p>两者不能同时发生，所以：</p>
<p>$$
P(A\cup B)=P(A)+P(B)=\frac{1}{6}+\frac{1}{6}=\frac{1}{3}
$$</p>
<hr />
<h2>8. 加法公式</h2>
<p>如果 $A$ 和 $B$ 不一定互斥，则：</p>
<p>$$
P(A\cup B)=P(A)+P(B)-P(A\cap B)
$$</p>
<p>这个公式非常重要。</p>
<p>为什么要减去 $P(A\cap B)$？</p>
<p>因为 $P(A)+P(B)$ 中，公共部分 $A\cap B$ 被重复计算了一次，所以要减掉一次。</p>
<p>例如一副 52 张扑克牌中，随机抽一张，求“抽到红牌或 K”的概率。</p>
<p>设：</p>
<p>$$
A=\text{抽到红牌}
$$</p>
<p>$$
B=\text{抽到K}
$$</p>
<p>则：</p>
<p>$$
P(A)=\frac{26}{52}
$$</p>
<p>$$
P(B)=\frac{4}{52}
$$</p>
<p>红色 K 有 2 张，所以：</p>
<p>$$
P(A\cap B)=\frac{2}{52}
$$</p>
<p>因此：</p>
<p>$$
P(A\cup B)=\frac{26}{52}+\frac{4}{52}-\frac{2}{52}=\frac{28}{52}=\frac{7}{13}
$$</p>
<hr />
<h2>9. 减法公式</h2>
<p>减法公式用来计算：</p>
<p><strong>事件 $A$ 发生，但事件 $B$ 不发生。</strong></p>
<p>也就是事件差：</p>
<p>$$
A-B=A\cap \overline{B}
$$</p>
<p>公式是：</p>
<p>$$
P(A-B)=P(A)-P(A\cap B)
$$</p>
<p>也可以写成：</p>
<p>$$
P(A\cap \overline{B})=P(A)-P(A\cap B)
$$</p>
<p>怎么理解？</p>
<p>事件 $A$ 可以分成两部分：</p>
<p>$$
A=(A\cap B)\cup(A\cap \overline{B})
$$</p>
<p>也就是：</p>
<p>$$
A=(A\text{ 和 }B\text{ 都发生})\cup(A\text{ 发生但 }B\text{ 不发生})
$$</p>
<p>这两部分互斥，所以：</p>
<p>$$
P(A)=P(A\cap B)+P(A\cap \overline{B})
$$</p>
<p>移项得到：</p>
<p>$$
P(A\cap \overline{B})=P(A)-P(A\cap B)
$$</p>
<p>例如掷一个骰子。</p>
<p>设：</p>
<p>$$
A={2,4,6}
$$</p>
<p>表示“掷出偶数”。</p>
<p>设：</p>
<p>$$
B={4,5,6}
$$</p>
<p>表示“点数大于 3”。</p>
<p>那么：</p>
<p>$$
A-B={2}
$$</p>
<p>表示“掷出偶数，但点数不大于 3”。</p>
<p>根据减法公式：</p>
<p>$$
P(A-B)=P(A)-P(A\cap B)
$$</p>
<p>其中：</p>
<p>$$
P(A)=\frac{3}{6}
$$</p>
<p>$$
A\cap B={4,6}
$$</p>
<p>$$
P(A\cap B)=\frac{2}{6}
$$</p>
<p>所以：</p>
<p>$$
P(A-B)=\frac{3}{6}-\frac{2}{6}=\frac{1}{6}
$$</p>
<p>常见变形：</p>
<p>$$
P(A\cap \overline{B})=P(A)-P(A\cap B)
$$</p>
<p>$$
P(\overline{A}\cap B)=P(B)-P(A\cap B)
$$</p>
<p>对立事件公式：</p>
<p>$$
P(\overline{A})=1-P(A)
$$</p>
<p>也可以看成减法公式的特殊情况。</p>
<p>加法公式和减法公式可以放在一起记：</p>
<p>$$
P(A\cup B)=P(A)+P(B)-P(A\cap B)
$$</p>
<p>$$
P(A-B)=P(A)-P(A\cap B)
$$</p>
<p>并集要加两个事件，再减去重复部分；</p>
<p>差集要从 $A$ 中减去 $A$ 和 $B$ 的公共部分。</p>
<hr />
<h2>10. 对立事件概率公式</h2>
<p>对立事件公式：</p>
<p>$$
P(\overline{A})=1-P(A)
$$</p>
<p>这个公式经常用于“至少”“至多”问题。</p>
<p>例如掷一枚硬币 3 次，求至少出现一次正面的概率。</p>
<p>直接算“至少一次正面”比较麻烦，可以算它的对立事件：</p>
<p>$$
\text{至少一次正面} \quad \text{的对立事件是} \quad \text{一次正面都没有}
$$</p>
<p>也就是 3 次全是反面。</p>
<p>总情况数：</p>
<p>$$
2^3=8
$$</p>
<p>全是反面只有 1 种情况：</p>
<p>$$
TTT
$$</p>
<p>所以：</p>
<p>$$
P(\text{一次正面都没有})=\frac{1}{8}
$$</p>
<p>因此：</p>
<p>$$
P(\text{至少一次正面})=1-\frac{1}{8}=\frac{7}{8}
$$</p>
<p>遇到“至少有一个”“至少成功一次”“至少抽到一个”这类题，优先考虑对立事件。</p>
<hr />
<h2>11. 条件概率</h2>
<p>条件概率表示：在事件 $B$ 已经发生的条件下，事件 $A$ 发生的概率。</p>
<p>记作：</p>
<p>$$
P(A\mid B)
$$</p>
<p>公式是：</p>
<p>$$
P(A\mid B)=\frac{P(A\cap B)}{P(B)}, \quad P(B)&gt;0
$$</p>
<p>例如掷一个骰子，已知掷出的点数是偶数，求它是 6 的概率。</p>
<p>设：</p>
<p>$$
A={6}
$$</p>
<p>$$
B={2,4,6}
$$</p>
<p>则：</p>
<p>$$
A\cap B={6}
$$</p>
<p>所以：</p>
<p>$$
\begin{aligned}
P(A\mid B)&amp;=\frac{P(A\cap B)}{P(B)} \
&amp;=\frac{1/6}{3/6} \
&amp;=\frac{1}{3}
\end{aligned}
$$</p>
<p>直观理解：既然已经知道结果是偶数，那么样本空间被缩小成：</p>
<p>$$
{2,4,6}
$$</p>
<p>在这三个结果中，只有一个是 6，所以概率是：</p>
<p>$$
\frac{1}{3}
$$</p>
<hr />
<h2>12. 乘法公式</h2>
<p>由条件概率公式可以得到乘法公式：</p>
<p>$$
P(A\cap B)=P(B)P(A\mid B)
$$</p>
<p>也可以写成：</p>
<p>$$
P(A\cap B)=P(A)P(B\mid A)
$$</p>
<p>这个公式用来求“两个事件同时发生”的概率。</p>
<p>例如袋中有 5 个红球、3 个白球，不放回地连续取两次，求两次都取到红球的概率。</p>
<p>第一次取红球概率：</p>
<p>$$
\frac{5}{8}
$$</p>
<p>第一次取到红球后，袋中剩下 4 个红球、3 个白球，共 7 个球。</p>
<p>第二次再取红球的概率：</p>
<p>$$
\frac{4}{7}
$$</p>
<p>所以：</p>
<p>$$
P(\text{两次红球})=\frac{5}{8}\times \frac{4}{7}=\frac{5}{14}
$$</p>
<hr />
<h2>13. 独立事件</h2>
<p>如果事件 $A$ 是否发生不影响事件 $B$ 的发生概率，则称 $A$ 与 $B$ 独立。</p>
<p>数学定义是：</p>
<p>$$
P(A\cap B)=P(A)P(B)
$$</p>
<p>等价地，当 $P(B)&gt;0$ 时：</p>
<p>$$
P(A\mid B)=P(A)
$$</p>
<p>例如连续抛两次硬币：</p>
<p>事件 $A$：第一次正面；</p>
<p>事件 $B$：第二次正面。</p>
<p>第一次的结果不影响第二次，所以 $A$ 与 $B$ 独立。</p>
<p>因此：</p>
<p>$$
P(A\cap B)=P(A)P(B)=\frac{1}{2}\times\frac{1}{2}=\frac{1}{4}
$$</p>
<p>注意区分：</p>
<p><strong>互斥</strong>表示两个事件不能同时发生；</p>
<p><strong>独立</strong>表示一个事件的发生不影响另一个事件的概率。</p>
<p>一般情况下，两个非零概率事件如果互斥，就不独立。</p>
<hr />
<h2>14. 伯努利概型</h2>
<p>如果一个随机试验只有两种结果：</p>
<p>$$
A \quad \text{和} \quad \overline{A}
$$</p>
<p>也就是“成功”和“失败”，并且每次试验中：</p>
<p>$$
P(A)=p,\qquad P(\overline{A})=1-p=q
$$</p>
<p>那么这种试验叫做<strong>伯努利试验</strong>。</p>
<p>例如：</p>
<p>抛一次硬币，结果只有正面或反面；</p>
<p>射击一次，结果只有命中或未命中；</p>
<p>检测一件产品，结果只有合格或不合格。</p>
<h3>n 重伯努利试验</h3>
<p>如果把同一个伯努利试验独立重复进行 $n$ 次，并且每次成功概率都相同，都是 $p$，则称为 <strong>$n$ 重伯努利试验</strong>。</p>
<p>它有三个关键条件：</p>
<ol>
<li>每次试验只有两种结果：成功或失败；</li>
<li>每次试验成功概率相同，都是 $p$；</li>
<li>各次试验相互独立。</li>
</ol>
<h3>恰好成功 k 次</h3>
<p>在 $n$ 重伯努利试验中，设 $X$ 表示事件 $A$ 发生的次数，则事件 $A$ 恰好发生 $k$ 次的概率为：</p>
<p>$$
P(X=k)=C_n^k p^k(1-p)^{n-k},\qquad k=0,1,2,\dots,n
$$</p>
<p>其中 $C_n^k$ 表示从 $n$ 次试验中选出 $k$ 次成功的位置。</p>
<p>这个公式也叫<strong>二项分布公式</strong>。</p>
<p>为什么是这个公式？</p>
<p>某一种固定顺序下，恰好成功 $k$ 次、失败 $n-k$ 次的概率是：</p>
<p>$$
p^k(1-p)^{n-k}
$$</p>
<p>但是成功的 $k$ 次可以出现在 $n$ 次试验中的不同位置，共有 $C_n^k$ 种安排方式。</p>
<p>所以总概率为：</p>
<p>$$
C_n^k p^k(1-p)^{n-k}
$$</p>
<h3>例题 1：恰好命中 3 次</h3>
<p>某人每次射击命中率为 $0.8$，独立射击 5 次，求恰好命中 3 次的概率。</p>
<p>这是 $n=5$，$p=0.8$，$k=3$。</p>
<p>$$
\begin{aligned}
P(X=3)&amp;=C_5^3(0.8)^3(0.2)^2 \
&amp;=10\times0.512\times0.04 \
&amp;=0.2048
\end{aligned}
$$</p>
<p>所以恰好命中 3 次的概率是 $0.2048$。</p>
<h3>例题 2：至少成功一次</h3>
<p>某设备一次启动成功的概率为 $0.9$，独立启动 4 次，求至少成功一次的概率。</p>
<p>“至少成功一次”的对立事件是“4 次都失败”。</p>
<p>失败概率是：</p>
<p>$$
1-0.9=0.1
$$</p>
<p>所以：</p>
<p>$$
\begin{aligned}
P(\text{至少成功一次})&amp;=1-P(\text{4 次都失败}) \
&amp;=1-(0.1)^4 \
&amp;=1-0.0001 \
&amp;=0.9999
\end{aligned}
$$</p>
<h3>例题 3：至少成功 k 次</h3>
<p>某人每次答题答对概率为 $0.6$，独立答 5 题，求至少答对 3 题的概率。</p>
<p>设 $X$ 表示答对题数，则：</p>
<p>$$
X\sim B(5,0.6)
$$</p>
<p>要求：</p>
<p>$$
P(X\ge 3)
$$</p>
<p>也就是：</p>
<p>$$
P(X\ge 3)=P(X=3)+P(X=4)+P(X=5)
$$</p>
<p>所以：</p>
<p>$$
\begin{aligned}
P(X\ge 3)
&amp;=C_5^3(0.6)^3(0.4)^2+C_5^4(0.6)^4(0.4)+C_5^5(0.6)^5 \
&amp;=10\times0.216\times0.16+5\times0.1296\times0.4+1\times0.07776 \
&amp;=0.3456+0.2592+0.07776 \
&amp;=0.68256
\end{aligned}
$$</p>
<h3>伯努利概型常见关键词</h3>
<table>
<thead>
<tr>
<th>关键词</th>
<th>可能对应</th>
</tr>
</thead>
<tbody>
<tr>
<td>独立重复</td>
<td>$n$ 重伯努利试验</td>
</tr>
<tr>
<td>每次成功概率相同</td>
<td>成功概率 $p$ 不变</td>
</tr>
<tr>
<td>恰好成功 $k$ 次</td>
<td>$C_n^k p^k(1-p)^{n-k}$</td>
</tr>
<tr>
<td>至少成功一次</td>
<td>常用对立事件</td>
</tr>
<tr>
<td>至少成功 $k$ 次</td>
<td>从 $k$ 加到 $n$</td>
</tr>
<tr>
<td>至多成功 $k$ 次</td>
<td>从 $0$ 加到 $k$</td>
</tr>
</tbody>
</table>
<h3>伯努利概型和古典概型的区别</h3>
<table>
<thead>
<tr>
<th>概型</th>
<th>核心特点</th>
<th>常用公式</th>
</tr>
</thead>
<tbody>
<tr>
<td>古典概型</td>
<td>有限个等可能结果</td>
<td>$P(A)=\frac{m}{n}$</td>
</tr>
<tr>
<td>伯努利概型</td>
<td>独立重复，只有成功/失败两类结果</td>
<td>$P(X=k)=C_n^k p^k(1-p)^{n-k}$</td>
</tr>
</tbody>
</table>
<p>古典概型强调的是：<strong>所有基本事件等可能</strong>。</p>
<p>伯努利概型强调的是：<strong>同一试验独立重复多次，每次只有成功和失败两种结果</strong>。</p>
<hr />
<h2>15. 全概率公式</h2>
<p>如果事件 $B_1,B_2,\dots,B_n$ 构成样本空间的一个划分，也就是：</p>
<p>$$
B_1\cup B_2\cup \cdots \cup B_n=\Omega
$$</p>
<p>并且它们两两互斥，则对于任意事件 $A$，有：</p>
<p>$$
P(A)=\sum_{i=1}^{n}P(B_i)P(A\mid B_i)
$$</p>
<p>通俗理解：
如果 $A$ 可以通过多种路径发生，那么就把每条路径的概率加起来。</p>
<p>例如某产品来自甲、乙两个工厂。</p>
<p>甲厂生产 60%，次品率 2%；乙厂生产 40%，次品率 5%。随机抽一个产品，求它是次品的概率。</p>
<p>设：</p>
<p>$$
B_1=\text{来自甲厂}
$$</p>
<p>$$
B_2=\text{来自乙厂}
$$</p>
<p>$$
A=\text{次品}
$$</p>
<p>则：</p>
<p>$$
P(A)=P(B_1)P(A\mid B_1)+P(B_2)P(A\mid B_2)
$$</p>
<p>$$
P(A)=0.6\times0.02+0.4\times0.05
$$</p>
<p>$$
P(A)=0.012+0.020=0.032
$$</p>
<p>所以次品概率是：</p>
<p>$$
3.2%
$$</p>
<hr />
<h2>16. 贝叶斯公式</h2>
<p>贝叶斯公式用于“由结果反推原因”。</p>
<p>公式是：</p>
<p>$$
P(B_i\mid A)=
\frac{P(B_i)P(A\mid B_i)}
{\sum_{j=1}^{n}P(B_j)P(A\mid B_j)}
$$</p>
<p>它本质上就是：</p>
<p>$$
\text{贝叶斯公式}=
\frac{\text{某一条路径的概率}}{\text{所有能导致结果的路径概率之和}}
$$</p>
<p>设 $B_1,B_2,\dots,B_n$ 是所有可能原因，并且它们互斥、完整；设 $A$ 是最终发生的结果。</p>
<p>第一步，用乘法公式算某一条路径：</p>
<p>$$
B_i \rightarrow A
$$</p>
<p>这条路径对应的概率是：</p>
<p>$$
P(B_i\cap A)=P(B_i)P(A\mid B_i)
$$</p>
<p>也就是：</p>
<p>$$
\text{路径概率}=\text{原因概率}\times\text{该原因下结果发生的概率}
$$</p>
<p>第二步，用全概率公式算结果 $A$ 的总概率：</p>
<p>$$
P(A)=P(B_1)P(A\mid B_1)+P(B_2)P(A\mid B_2)+\cdots+P(B_n)P(A\mid B_n)
$$</p>
<p>也就是把所有能导致 $A$ 的路径概率加起来。</p>
<p>第三步，已知结果 $A$ 已经发生，问它来自某个原因 $B_i$ 的概率：</p>
<p>$$
P(B_i\mid A)=\frac{P(B_i\cap A)}{P(A)}
$$</p>
<p>把分子用乘法公式展开，把分母用全概率公式展开，就得到：</p>
<p>$$
P(B_i\mid A)=
\frac{P(B_i)P(A\mid B_i)}
{P(B_1)P(A\mid B_1)+P(B_2)P(A\mid B_2)+\cdots+P(B_n)P(A\mid B_n)}
$$</p>
<p>所以贝叶斯公式可以理解为：</p>
<p>$$
\frac{\text{指定路径}}{\text{全部路径}}
$$</p>
<p>还是用刚才的例子。</p>
<p>甲厂生产 60%，次品率 2%；乙厂生产 40%，次品率 5%。已知抽到的是次品，求它来自乙厂的概率。</p>
<p>要求：</p>
<p>$$
P(B_2\mid A)
$$</p>
<p>根据贝叶斯公式：</p>
<p>$$
P(B_2\mid A)=
\frac{P(B_2)P(A\mid B_2)}
{P(B_1)P(A\mid B_1)+P(B_2)P(A\mid B_2)}
$$</p>
<p>代入：</p>
<p>$$
P(B_2\mid A)=
\frac{0.4\times0.05}
{0.6\times0.02+0.4\times0.05}
$$</p>
<p>$$
P(B_2\mid A)=
\frac{0.020}{0.032}=0.625
$$</p>
<p>所以，已知产品是次品，它来自乙厂的概率是：</p>
<p>$$
62.5%
$$</p>
<p>做题时可以这样写：</p>
<p>$$
P(\text{原因}_i\mid \text{结果})=
\frac{
P(\text{原因}_i)P(\text{结果}\mid \text{原因}_i)
}{
\sum P(\text{原因})P(\text{结果}\mid \text{原因})
}
$$</p>
<p>记住三句话：</p>
<ol>
<li>乘法公式算一条路径；</li>
<li>全概率公式算所有路径；</li>
<li>贝叶斯公式就是指定路径占所有路径的比例。</li>
</ol>
<hr />
<h2>17. 常见题型总结</h2>
<h3>题型一：直接数样本点</h3>
<p>适合古典概型。</p>
<p>解题格式：</p>
<p>$$
P(A)=\frac{\text{有利情况数}}{\text{总情况数}}
$$</p>
<p>例如掷骰子、抽牌、排队、抽球。</p>
<h3>题型二：用对立事件</h3>
<p>看到这些词要敏感：</p>
<p>“至少一次”“至少一个”“不全是”“至少成功一次”。</p>
<p>常用：</p>
<p>$$
P(A)=1-P(\overline{A})
$$</p>
<h3>题型三：用加法公式</h3>
<p>看到“或”“至少满足一个条件”，考虑：</p>
<p>$$
P(A\cup B)=P(A)+P(B)-P(A\cap B)
$$</p>
<p>若 $A, B$ 互斥，则直接：</p>
<p>$$
P(A\cup B)=P(A)+P(B)
$$</p>
<h3>题型四：用减法公式</h3>
<p>看到“$A$ 发生但 $B$ 不发生”，考虑：</p>
<p>$$
P(A-B)=P(A)-P(A\cap B)
$$</p>
<h3>题型五：用乘法公式</h3>
<p>看到“同时发生”“连续发生”“先后发生”，考虑：</p>
<p>$$
P(A\cap B)=P(A)P(B\mid A)
$$</p>
<p>尤其是不放回抽样，后一次概率通常会变。</p>
<h3>题型六：用全概率公式</h3>
<p>看到“来源不同”“路径不同”“情况分类”，考虑全概率公式。</p>
<h3>题型七：用贝叶斯公式</h3>
<p>看到“已知结果，反推原因”，考虑贝叶斯公式。</p>
<h3>题型八：用伯努利概型</h3>
<p>看到“独立重复”“每次成功概率相同”“恰好成功 $k$ 次”，考虑：</p>
<p>$$
P(X=k)=C_n^k p^k(1-p)^{n-k}
$$</p>
<hr />
<h2>18. 易错点</h2>
<p>第一，<strong>互斥不等于独立</strong>。
互斥是不能同时发生，独立是互不影响。</p>
<p>第二，<strong>对立事件不只是互斥，还要合起来等于全集</strong>。
例如掷骰子中，“掷出 1”和“掷出 2”互斥，但不是对立，因为还有 3、4、5、6。</p>
<p>第三，<strong>条件概率会改变样本空间</strong>。
已知某事件发生后，后续概率要在新的范围内计算。</p>
<p>第四，<strong>不放回抽样和放回抽样不同</strong>。
放回：每次概率不变。
不放回：后一次概率通常改变。</p>
<p>第五，<strong>“或”通常对应并集，“且”通常对应交集</strong>。
做题时先把文字翻译成集合符号。</p>
<p>第六，<strong>差事件要看清楚谁减谁</strong>。
$A-B$ 表示 $A$ 发生但 $B$ 不发生，不是 $B$ 发生但 $A$ 不发生。</p>
<p>第七，<strong>伯努利概型必须满足独立重复和成功概率不变</strong>。
如果每次成功概率会变化，就不能直接套二项分布公式。</p>
<hr />
<h2>19. 一套记忆框架</h2>
<p>这一章可以用一句话概括：</p>
<p><strong>先把随机试验写成样本空间，再把问题写成事件，最后根据事件关系选择公式。</strong></p>
<p>做题步骤：</p>
<ol>
<li>写出样本空间 $\Omega$；</li>
<li>定义事件 $A, B, C$；</li>
<li>判断事件关系：互斥、对立、独立、条件；</li>
<li>选择公式；</li>
<li>代入计算。</li>
</ol>
<hr />
<h2>20. 本章核心公式表</h2>
<table>
<thead>
<tr>
<th>内容</th>
<th>公式</th>
</tr>
</thead>
<tbody>
<tr>
<td>古典概型</td>
<td>$P(A)=\frac{m}{n}$</td>
</tr>
<tr>
<td>对立事件</td>
<td>$P(\overline{A})=1-P(A)$</td>
</tr>
<tr>
<td>加法公式</td>
<td>$P(A\cup B)=P(A)+P(B)-P(A\cap B)$</td>
</tr>
<tr>
<td>互斥事件加法</td>
<td>$P(A\cup B)=P(A)+P(B)$</td>
</tr>
<tr>
<td>减法公式</td>
<td>$P(A-B)=P(A)-P(A\cap B)$</td>
</tr>
<tr>
<td>条件概率</td>
<td>$P(A\mid B)=\frac{P(A\cap B)}{P(B)}$</td>
</tr>
<tr>
<td>乘法公式</td>
<td>$P(A\cap B)=P(A)P(B\mid A)$</td>
</tr>
<tr>
<td>独立事件</td>
<td>$P(A\cap B)=P(A)P(B)$</td>
</tr>
<tr>
<td>伯努利概型</td>
<td>$P(X=k)=C_n^k p^k(1-p)^{n-k}$</td>
</tr>
<tr>
<td>全概率公式</td>
<td>$P(A)=\sum P(B_i)P(A\mid B_i)$</td>
</tr>
<tr>
<td>贝叶斯公式</td>
<td>$P(B_i\mid A)=\frac{P(B_i)P(A\mid B_i)}{\sum P(B_j)P(A\mid B_j)}$</td>
</tr>
</tbody>
</table>
<p>学这一章时，最重要的不是背公式，而是学会把题目中的文字翻译成事件关系。尤其要熟练区分：<strong>互斥、对立、独立、条件概率</strong>。</p>
<hr />
<h2>21. 框框老师</h2>
<p><img src="https://img.picr2.online/1781787339068_PixPin_2026-06-18_19-59-31.webp" alt="Image" />
<img src="https://img.picr2.online/1781787340076_PixPin_2026-06-18_20-05-04.webp" alt="Image" />
<img src="https://img.picr2.online/1781787340102_PixPin_2026-06-18_20-53-43.webp" alt="Image" />
<img src="https://img.picr2.online/1781787596422_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781787753491_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781787815955_image.webp" alt="Image" />
<img src="https://img.picr2.online/1781854143807_image.webp" alt="全概率公式习题一(1)" />
<img src="https://img.picr2.online/1781854358645_image.webp" alt="全概率公式习题一(2)" />
<img src="https://img.picr2.online/1781854696032_image.webp" alt="全概率公式习题二" /></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="概率论"/>
  </entry>
  <entry>
    <title>最大流问题与 Edmonds-Karp 算法</title>
    <link href="https://blog.yoake.dev/posts/algorithm/maxflow/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/maxflow/</id>
    <published>2026-06-16T10:51:00.000Z</published>
    <updated>2026-06-16T10:51:00.000Z</updated>
    <summary>从贪心找路失败出发，引出反向边的纠错机制，形式化残量网络与增广路，用 BFS 实现标号法并反复推流，直到残量网络中源汇不再连通。</summary>
    <content type="html"><![CDATA[<h2>一、最大流问题是什么</h2>
<p>把一张有向图想象成一个城市供水系统：$s$ 是源头水库，$t$ 是用户区。中间有很多管道和中转站，管道各有最大的通水能力（容量）。</p>
<p>在这个系统中，有两条最基础的常识性物理规则：</p>
<ol>
<li><strong>容量限制</strong>：每一根水管都有粗细，流过去的水不能超过这根管子的最大容量。</li>
<li><strong>流量守恒（中转站不蓄水）</strong>：除了起点水库 $s$ 和终点用户区 $t$，中间的每一个中转站流入了多少水，就必须立刻流出去多少水，既不能凭空漏水，也不能把水蓄起来。</li>
</ol>
<p>在这个前提下，<strong>用户区 $t$ 最多能收到多少水？<strong>这就是</strong>最大流</strong>问题。</p>
<p>先用最简单的例子建立直觉。只有一根管道连接水库和用户区：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/9265f0c23db6f8f01ddcda02230f942d.svg" alt="最简管道示例" /></p>
<p>容量 $10$ 表示这条管道最多通水 $10$ 单位。这根管道既是唯一的路，也是整个系统的最终瓶颈，最大流就是 $10$。</p>
<p>再加两条路：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/4f6e24e706e6edc5f442709cb1ebaa6e.svg" alt="多管道分流示例" /></p>
<p>现在有两条路送水：$s \to A \to t$ 和 $s \to B \to t$。最多能送多少？直观上，把所有从 $s$ 出发的管道容量加起来就是上限。但如果中间某段管道容量很小，实际可能达不到这个上限。到底能送多少，我们需要算法来帮我们自动算出来。</p>
<hr />
<h2>二、简单找路为什么会出错</h2>
<p>找最大流的直觉做法很像探路：反复找一条能送水的路线，送一次水占掉一部分容量，直到再也找不到路为止。但这个看似稳妥的“简单找路”，可能第一步就走错。</p>
<p>看下面这个 4 顶点网络，所有管道容量都是 $1$，中间多了一根 $A \to B$ 的连通管：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/bf9a5349882511a8e5787ff5e65a209b.svg" alt="贪心找路反例" /></p>
<p>假设第一次探路运气不好，选了路径 $s \to A \to B \to t$ 送 $1$ 单位水，途经的 $s \to A$、$A \to B$、$B \to t$ 三条边全部被占满。</p>
<p>接着再去探路，两条直通路线都被堵死：</p>
<ul>
<li>走 $s \to B$，但 $B \to t$ 已满，过不去。</li>
<li>走 $A \to t$，但 $s \to A$ 已满，水到不了 $A$。</li>
</ul>
<p>:::warning[没有纠错机制时]
算法只能宣布最大流为 $1$。可整体看图，明明能走 $s \to A \to t$ 送 $1$、$s \to B \to t$ 再送 $1$，真正的最大流是 $2$。第一次选错了路又没法修正，算法就卡在了次优解上。
:::</p>
<hr />
<h2>三、反向边：允许后悔的“水流对冲”机制</h2>
<p>贪心找路失败，根子在于<strong>流量推出去就撤不回来</strong>。如果允许算法“后悔”——把占用的水流退回来重新分配，问题就解决了。为此，算法引入<strong>反向边</strong>作为纠错通道。</p>
<h3>核心：水流对冲抵消模型</h3>
<p>为什么“沿反向边推流”就等于“撤回水流”？用一个直观的物理常识就能理解：<strong>水流抵消</strong>。</p>
<p>回到前面的例子。</p>
<p>推送 $s \to A \to B \to t$ 的 $1$ 单位流量后，残量网络中自动多出三条反向边 $B \to A$、$t \to B$、$A \to s$，它们就是“退水额度”。</p>
<p>现在尝试走新路 $s \to B \to A \to t$：</p>
<ol>
<li>
<p><strong>$s \to B$</strong>：正向送水 $1$ 单位。</p>
</li>
<li>
<p><strong>$B \to A$</strong>：这是反向边，推 $1$ 单位逆向水。</p>
<p>它与管道里原有的 $A \to B$ 那 $1$ 单位水迎面相撞、互相抵消，$A-B$ 的实际流量瞬间归零。</p>
</li>
<li>
<p><strong>$A \to t$</strong>：正向送水 $1$ 单位。</p>
</li>
</ol>
<p>效果是：</p>
<p>原本经 $s \to A$ 流来的水不再拐去 $B$，改走空出来的 $A \to t$；</p>
<p>新从 $s \to B$ 来的水也不必去 $A$，直接走 $B \to t$。</p>
<p>一次“对冲”就让水流在交叉口自动重新分流，两条原本堵死的路都用上了，总流量达到 $2$。</p>
<hr />
<h2>四、残量网络</h2>
<p>有了“正向加水、反向退水”的感觉，就可以定义<strong>残量网络</strong>（记作 $G_f$）——一张随施工实时更新的导向图。原图中每条水管 $(u, v)$ 都拆成两条指导线：</p>
<ul>
<li><strong>正向边</strong> $u \to v$，残量 $c_f(u, v) = c(u, v) - f(u, v)$：还能再灌多少水。</li>
<li><strong>反向边</strong> $v \to u$，残量 $c_f(v, u) = f(u, v)$：已经灌了多少，也就是能退多少水的“退水额度”。</li>
</ul>
<p>下面这张图直观展示了这一点：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/2577c146731b7fce77e58d1c0cdf24a9.svg" alt="残量网络示例" /></p>
<p>原图中 $s \to A$ 容量为 $10$。当我们推了 $7$ 单位流量后，正向还剩 $3$ 单位空间；反向则出现 $7$ 单位的残量——意思是我们拥有“最多往回退 7 单位水”的额度。</p>
<hr />
<h2>五、增广路</h2>
<p>当前流量可能还不是最大流。既然还想送更多水，我们就要问：在当前的残量网络中，是否还存在一条从水库 $s$ 一路连通到用户区 $t$ 的路线？</p>
<p>这样一条路线就叫做<strong>增广路</strong>。</p>
<ul>
<li>增广路上的每一步，都可以是正向边（加水），也可以是反向边（退水），但它们的残量必须全部大于 0（有余量可加或有额度可退）。</li>
<li>沿着这条路径，我们能够推送的额外水流量，取决于整条路上最窄的那个瓶颈，即<strong>路径上残量的最小值</strong> $\Delta$：</li>
</ul>
<p>$$
\Delta = \min_{(u,v) \in P} c_f(u, v)
$$</p>
<p>确定瓶颈后，我们开始推流更新：<strong>走正向边时流量加 $\Delta$，走反向边时流量减 $\Delta$</strong>。</p>
<p>一旦在施工图上无论怎么走都找不到任何一条路能把 $s$ 和 $t$ 连通，当前流就是最大流。</p>
<hr />
<h2>六、标号法</h2>
<p><strong>标号法</strong>是在寻找增广路时记录前驱和瓶颈的方法。本文用 BFS 来实现它，也就是 Edmonds-Karp 的找路方式。
在搜索过程中，我们给访问过的顶点贴上一个“便利贴”（标号），记录两项关键信息：</p>
<ol>
<li><strong>这个点是从哪个前驱顶点走过来的</strong>（前驱节点，用于最后沿路反向推流）。</li>
<li><strong>到该点为止，整条路上的瓶颈限制到了多少</strong>（可增广量 $\Delta$）。</li>
</ol>
<p>举个例子，从源点 $s$ 开始探索：</p>
<ul>
<li>水库 $s$ 水量无限，前驱为空，标号 <code>s : (+, ∞)</code>。</li>
<li>BFS 发现邻居 $A$，正向管道 $s \to A$ 还能容纳 $2$ 单位，标号 <code>A : (s, 2)</code>。</li>
<li>从 $A$ 探到终点 $t$，$A \to t$ 只剩 $1$ 单位，瓶颈被压到 $\min(2, 1) = 1$，标号 <code>t : (A, 1)</code>。</li>
<li>顺着 $t$ 的前驱 <code>A</code>、<code>A</code> 的前驱 <code>s</code> 逆推，就得到增广路 $s \to A \to t$，瓶颈 $\Delta = 1$。</li>
</ul>
<p>算法流程图如下：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/e65e6b4b247f82ca65499151f88f5f08.svg" alt="算法流程图" /></p>
<p>需要说明一下三个名字的关系。</p>
<p><strong>Ford-Fulkerson</strong> 是“反复找增广路推流”这套通用框架的统称，它不规定用什么方法找路；如果用 BFS 每次都找边数最少的增广路，这个特定实现就叫 <strong>Edmonds-Karp</strong>。</p>
<p>下面的完整示例用的都是 Edmonds-Karp。</p>
<hr />
<h2>七、Edmonds-Karp 算法完整示例</h2>
<h3>简单情况：4 顶点</h3>
<p>以下面的 4 顶点流网络为例：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/f3446541f7ab822d888a236048f40e99.svg" alt="示例流网络" /></p>
<table>
<thead>
<tr>
<th>边</th>
<th>容量</th>
</tr>
</thead>
<tbody>
<tr>
<td>$s \to A$</td>
<td>2</td>
</tr>
<tr>
<td>$s \to B$</td>
<td>1</td>
</tr>
<tr>
<td>$A \to B$</td>
<td>1</td>
</tr>
<tr>
<td>$A \to t$</td>
<td>1</td>
</tr>
<tr>
<td>$B \to t$</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>初始状态下，所有边流量都为 $0$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/7b24581b03c6851ff50ed096498a7d0f.svg" alt="4顶点执行流程" /></p>
<h4>第 1 轮：$s \to A \to t$，$\Delta = 1$</h4>
<p>BFS 从 $s$ 出发探索：</p>
<ul>
<li>给 $s$ 标号 <code>(+, ∞)</code>。</li>
<li>探索到 $A$，标号 <code>(s, 2)</code>。</li>
<li>探索到 $t$，标号 <code>(A, 1)</code>，受限于 $A \to t$ 容量限制。</li>
</ul>
<p>回溯前驱，得到增广路 $s \to A \to t$，瓶颈容量 $\Delta = 1$。</p>
<p>推流后，管道 $A \to t$ 饱和。此时 $s \to A$ 流量为 1，<strong>残量剩余 1</strong>。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/6966136c44a23de2c9250e828c28c5bf.svg" alt="第1轮增广" /></p>
<h4>第 2 轮：$s \to B \to t$，$\Delta = 1$</h4>
<p>由于 $A \to t$ 已经满流（残量 0），BFS 换了一条路：</p>
<ul>
<li>探索到 $B$，标号 <code>(s, 1)</code>。</li>
<li>探索到 $t$，由于走不了 $A \to t$，改为从 $B \to t$ 到达，标号 <code>(B, 1)</code>。</li>
</ul>
<p>回溯得到增广路 $s \to B \to t$，$\Delta = 1$。</p>
<p>推流后 $s \to B$ 饱和。本轮未影响 $s \to A$，其流量仍为 1，<strong>残量仍为 1</strong>。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/d14f0c9fe93380b07f7ade409a95aaf5.svg" alt="第2轮增广" /></p>
<h4>第 3 轮：$s \to A \to B \to t$，$\Delta = 1$</h4>
<p>:::tip[为什么此时 s ➔ A <strong>残量是 1</strong>？]
第 1 轮中 $s \to A$ 已流过 1 单位流量（容量为 2），第 2 轮没有改变它，所以本轮开始前这根管道的残量是 1。后续每一轮探路都基于此前所有推流累加后的状态。
:::</p>
<ul>
<li>BFS 探索到 $A$，标号为 <code>(s, 1)</code>。</li>
<li>从 $A$ 探索到 $B$，标号为 <code>(A, 1)</code>。</li>
<li>从 $B$ 探索到 $t$，标号为 <code>(B, 1)</code>。</li>
</ul>
<p>回溯得到增广路 $s \to A \to B \to t$，$\Delta = 1$。推流后，三条管道全部饱和。此时总流量变为了 $3$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/da4e7e0ef2601fd8a272624fb7f39fc6.svg" alt="第3轮增广" /></p>
<h4>第 4 轮：无增广路，算法终止</h4>
<p>残量网络中从 $s$ 出发的所有管道都已经满流，BFS 无法到达任何邻居，汇点 $t$ 无法被标号。算法终止，最大流为 $3$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/deeb51e822523bd0fa304638bdc5655c.svg" alt="第4轮终止" /></p>
<p><img src="https://img.picr2.online/img/2026/06/16/5be1d7909b45fc617c2051b7551a5c32.svg" alt="最终流分配" /></p>
<p>为了更直观地看清流量的累积过程，下面是各条边在每一轮推流中的流量变化表：</p>
<table>
<thead>
<tr>
<th>管道</th>
<th>容量</th>
<th>第 1 轮流量变化</th>
<th>第 2 轮流量变化</th>
<th>第 3 轮流量变化</th>
<th>最终累计流量</th>
<th>最终剩余残量</th>
</tr>
</thead>
<tbody>
<tr>
<td>$s \to A$</td>
<td>2</td>
<td>+1</td>
<td>0</td>
<td>+1</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>$s \to B$</td>
<td>1</td>
<td>0</td>
<td>+1</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>$A \to B$</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>+1</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>$A \to t$</td>
<td>1</td>
<td>+1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>$B \to t$</td>
<td>2</td>
<td>0</td>
<td>+1</td>
<td>+1</td>
<td>2</td>
<td>0</td>
</tr>
</tbody>
</table>
<hr />
<h3>反向边登场：6 顶点</h3>
<p>考虑下面这个 6 顶点的复杂网络，我们来看看反向边是如何在搜索中被利用的：</p>
<p><img src="https://img.picr2.online/img/2026/06/16/9c61cc806d56c9cd9d1ce965ca8483ed.svg" alt="反向边示例流网络" /></p>
<table>
<thead>
<tr>
<th>边</th>
<th>容量</th>
<th>边</th>
<th>容量</th>
</tr>
</thead>
<tbody>
<tr>
<td>$s \to A$</td>
<td>2</td>
<td>$A \to D$</td>
<td>3</td>
</tr>
<tr>
<td>$s \to B$</td>
<td>1</td>
<td>$B \to C$</td>
<td>2</td>
</tr>
<tr>
<td>$A \to C$</td>
<td>2</td>
<td>$C \to t$</td>
<td>2</td>
</tr>
<tr>
<td></td>
<td></td>
<td>$D \to t$</td>
<td>3</td>
</tr>
</tbody>
</table>
<p>初始状态下，所有管道流量为 $0$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/0e262650b814df933473564413eac593.svg" alt="6顶点执行流程" /></p>
<h4>第 1 轮：$s \to A \to C \to t$，$\Delta = 2$</h4>
<p>第一轮 BFS 标号顺理成章地找到了最短路径 $s \to A \to C \to t$，瓶颈 $\Delta = \min(2, 2, 2) = 2$。</p>
<p>推流后，这三条边全部流满。关键的是，在当前残量网络中，<strong>由于 $A \to C$ 流过了 2 的水，所以自动出现了一条反向退水通道 $C \to A$，残量为 2</strong>。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/aca2596d7ea9b2df3d9ef4df15d34a50.svg" alt="6顶点第1轮增广" /></p>
<h4>第 2 轮：$s \to B \to C \to A \to D \to t$，$\Delta = 1$（退水路登场）</h4>
<p>当 BFS 再次探路时：</p>
<ol>
<li><strong>$s \to A$</strong> 已满，不能走。</li>
<li><strong>$s \to B$</strong> 还有 1 空间，BFS 走到 $B$，标为 <code>B : (s, 1)</code>。</li>
<li><strong>$B \to C$</strong> 还有 2 空间，BFS 走到 $C$，标为 <code>C : (B, 1)</code>。</li>
<li><strong>寻找下一站</strong>：在 $C$ 点，正向去往 $t$ 的管子已经流满（残量 0），走不过去。但 BFS 发现此时存在一条<strong>反向退水通道 $C \to A$</strong>，残量为 2。
<ul>
<li>BFS 顺着这根通道走了过去，来到了 $A$！给 $A$ 标上 <code>A : (C, 1)</code>。</li>
</ul>
</li>
<li><strong>从 $A$ 出发</strong>：走正向管道 $A \to D$（残量 3），标记 <code>D : (A, 1)</code>。</li>
<li><strong>从 $D$ 出发</strong>：走正向管道 $D \to t$（残量 3），到达终点，标记 <code>t : (D, 1)</code>。</li>
</ol>
<p>回溯前驱，我们得到了奇妙的增广路：$s \to B \to C \to A \to D \to t$，瓶颈 $\Delta = 1$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/e66418070d8961180ce3d52bd24ee2e8.svg" alt="6顶点第2轮增广（反向边登场）" /></p>
<p>根据“水流对冲抵消”模型：
当我们沿着反向通道 $C \to A$ 推流 $1$ 单位时，相当于让 $1$ 单位逆向水与第一轮中 $A \to C$ 流着的 $2$ 单位水迎面相撞，互相抵消。
因此，$A \to C$ 的流量被撤回了 1 单位（流量从 $2$ 降为 $1$）。</p>
<p>撤回出来的这 1 单位水改走 $A \to D \to t$ 流入终点；腾出的 $C$ 点空间则刚好接纳从 $B \to C$ 流过来的 1 单位新水。在没有修改原图结构的前提下，算法仅通过“退水/分流”就成功把总流量提升到了 $3$。</p>
<h4>第 3 轮：无增广路，终止</h4>
<p>此时从 $s$ 出发的两条通道 $s \to A$ 和 $s \to B$ 均已完全流满，BFS 无法继续探索。算法终止，最大流为 $3$。</p>
<p><img src="https://img.picr2.online/img/2026/06/16/8293b40bbacd108dc99f9a4eec0b0066.svg" alt="6顶点第3轮终止" /></p>
<hr />
<h2>八、最大流最小割定理：剪水管的“破坏者游戏”</h2>
<p>算出来的最大流怎么验证对不对？可以玩一个“破坏者游戏”：你要切断一些管道，让 $s$ 的水一滴也到不了 $t$，每根管道的切断代价等于它的容量。</p>
<ul>
<li>能让 $s$、$t$ 彻底断开的那组被剪边，叫一个<strong>割</strong>；它们的容量之和就是<strong>割的容量</strong>。</li>
<li>最省力气（容量之和最小）的剪法，叫<strong>最小割</strong>。</li>
</ul>
<p>由此引出<strong>最大流最小割定理</strong>：最大流的值等于最小割的容量。道理有两面：</p>
<ol>
<li><strong>最大流不可能超过任意一个割的容量</strong>：水要流到 $t$，必然要穿过被剪开的这个“截面”，所以总流量被这个截面的容量卡死。</li>
<li><strong>算法终止时，最小割刚好被榨干</strong>：BFS 再也找不到增广路，说明最窄的那组瓶颈（即最小割对应的管道）全都流满了，最大流正好顶在它们的容量上限。</li>
</ol>
<p>以 6 顶点示例为例：算法终止时，$s$ 能到达的顶点集只有 $S = {s}$，其余 $T = {A, B, C, D, t}$。分隔 $S$、$T$ 的正是 $s \to A$（容量 2）和 $s \to B$（容量 1），两条都已流满，割容量 $2 + 1 = 3$，正好等于最大流 $3$。</p>
<hr />
<h2>九、代码实现</h2>
<h3>读代码只需抓两处</h3>
<p>读下面的 Java 实现时，抓住两个跟前文物理模型对应的地方就够了：</p>
<ol>
<li><strong><code>addEdge</code> 里的 <code>rev</code> 指针</strong>：每加一条正向边，就配一条容量为 0 的反向边，用 <code>rev</code> 把两者绑定。这样操作正向边时能顺指针同步更新它的反向边。</li>
<li><strong><code>edmondsKarp</code> 里的推流</strong>：<code>e.flow += bottleneck; e.rev.flow -= bottleneck;</code> —— 正向边加流量、反向边减流量。反向边流量一减，它的残量（容量 $-$ 流量）就涨，这正是“积累退水额度”的代码体现。</li>
</ol>
<pre><code>import java.util.*;

public class MaxFlow {

    /**
     * 边对象：存储终点、容量、实际流量，以及反向边的引用指针。
     */
    static class Edge {
        int to;
        int cap;
        int flow;
        Edge rev; // 指向与之绑定的反向边，用于快速“退水”

        Edge(int to, int cap) {
            this.to = to;
            this.cap = cap;
            this.flow = 0;
        }

        // 计算这条边的富余空间（残量）
        int residual() {
            return cap - flow;
        }
    }

    private final int n;
    private final List&lt;Edge&gt;[] adj;

    @SuppressWarnings("unchecked")
    public MaxFlow(int n) {
        this.n = n;
        this.adj = new ArrayList[n];
        for (int i = 0; i &lt; n; i++) {
            adj[i] = new ArrayList&lt;&gt;();
        }
    }

    /**
     * 添加一条从 u 到 v 容量为 cap 的有向水管。
     * 同时自动在底层创建一条容量为 0 的反向退水管道 v -&gt; u。
     */
    public void addEdge(int u, int v, int cap) {
        Edge forward = new Edge(v, cap);
        Edge backward = new Edge(u, 0); // 反向边初始容量为 0
        forward.rev = backward;
        backward.rev = forward;
        adj[u].add(forward);
        adj[v].add(backward);
    }

    /**
     * BFS 标号法：在残量网络中寻找 s 到 t 的最短增广路。
     * parent[v] 数组用来记录到达 v 的那条管道，方便我们在寻路成功后回溯。
     * @return 寻找到的增广路上的瓶颈流量；若 s 与 t 已断开则返回 0
     */
    private int bfs(int s, int t, Edge[] parent) {
        boolean[] visited = new boolean[n];
        Queue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();

        visited[s] = true;
        queue.offer(s);

        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (Edge e : adj[u]) {
                // 如果这个邻居没有访问过，且管道还可以继续送水（残量 &gt; 0）
                if (!visited[e.to] &amp;&amp; e.residual() &gt; 0) {
                    visited[e.to] = true;
                    parent[e.to] = e; // 贴上便利贴：记录前驱边
                    
                    if (e.to == t) {
                        // 成功探索到终点，顺着前驱链回溯，计算整条路上的瓶颈限制
                        int bottleneck = Integer.MAX_VALUE;
                        for (int v = t; v != s; ) {
                            Edge pe = parent[v];
                            bottleneck = Math.min(bottleneck, pe.residual());
                            v = pe.rev.to; // 顺着反向边找回上一个起点
                        }
                        return bottleneck;
                    }
                    queue.offer(e.to);
                }
            }
        }
        return 0; // s 到 t 在当前导向图中已无法连通
    }

    /**
     * Edmonds-Karp 最大流算法主循环。
     * 反复用 BFS 寻路并推流，直到找不到增广路为止。
     */
    public int edmondsKarp(int s, int t) {
        int maxFlow = 0;
        Edge[] parent = new Edge[n];

        while (true) {
            int bottleneck = bfs(s, t, parent);
            if (bottleneck == 0) break; // 找不到路了，宣告结束

            // 沿着刚刚贴好标签的增广路进行“推流与对冲”：
            // 正向边流量增加，反向边流量同步减少
            for (int v = t; v != s; ) {
                Edge e = parent[v];
                e.flow += bottleneck;
                e.rev.flow -= bottleneck; // 对冲更新
                v = e.rev.to;
            }

            maxFlow += bottleneck;
        }

        return maxFlow;
    }

    /** 打印每条正向管道的最终水流量。 */
    public void printFlow(String[] name) {
        for (int u = 0; u &lt; n; u++) {
            for (Edge e : adj[u]) {
                if (e.cap &gt; 0) {
                    System.out.printf("%s -&gt; %s : f=%d / c=%d (残量=%d)%n",
                            vertexName(name, u), vertexName(name, e.to),
                            e.flow, e.cap, e.residual());
                }
            }
        }
    }

    private String vertexName(String[] name, int id) {
        if (name != null &amp;&amp; id &lt; name.length) return name[id];
        return String.valueOf(id);
    }

    public static void main(String[] args) {
        // 顶点编号：s=0, A=1, B=2, t=3
        String[] name = {"s", "A", "B", "t"};
        MaxFlow mf = new MaxFlow(4);
        mf.addEdge(0, 1, 2);  // s -&gt; A
        mf.addEdge(0, 2, 1);  // s -&gt; B
        mf.addEdge(1, 2, 1);  // A -&gt; B
        mf.addEdge(1, 3, 1);  // A -&gt; t
        mf.addEdge(2, 3, 2);  // B -&gt; t

        System.out.println("max flow: " + mf.edmondsKarp(0, 3));
        mf.printFlow(name);
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>max flow: 3
s -&gt; A : f=2 / c=2 (残量=0)
s -&gt; B : f=1 / c=1 (残量=0)
A -&gt; B : f=1 / c=1 (残量=0)
A -&gt; t : f=1 / c=1 (残量=0)
B -&gt; t : f=2 / c=2 (残量=0)
</code></pre>
<hr />
<h2>十、复杂度分析</h2>
<p>设顶点数为 $V$，边数为 $E$。</p>
<table>
<thead>
<tr>
<th>算法实现</th>
<th>时间复杂度</th>
<th>空间复杂度</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Edmonds-Karp 算法</td>
<td>$O(VE^2)$</td>
<td>$O(V + E)$</td>
<td>最多 $O(VE)$ 轮增广，每轮 BFS 耗时 $O(E)$，速度稳定</td>
</tr>
<tr>
<td>Ford-Fulkerson 算法</td>
<td>$O(E \cdot f^*)$</td>
<td>$O(V + E)$</td>
<td>$f^*$ 为最大流值；该界限依赖整数容量，容量很大时可能很慢；若容量允许无理数，朴素选路甚至可能不终止</td>
</tr>
</tbody>
</table>
<p><strong>Edmonds-Karp 的核心优势</strong>：通过 BFS 寻路，保证每次都走边数最少的增广路。这样可以保证路径长度单调不减，从而将总增广轮数限制在 $O(VE)$ 内，避免 DFS 在差路径上反复兜圈子。</p>
<p>:::tip[手算小技巧]
考试或手算最大流时，在草稿纸上画一张“容量-流量”表。每一轮在残量网络上用 BFS 找路，用笔圈出增广路，并写下它的瓶颈流量，直接在纸上更新流量。<strong>千万注意反向边</strong>：如果走的是反向边，对应正向边的流量要<strong>减掉</strong>。一旦从起点走不通终点了，直接停笔，当前累加出来的总流量就是最大流！
:::</p>
<hr />
<h2>十一、总结</h2>
<blockquote>
<p><strong>最大流的核心：在残量网络中反复用 BFS 寻找最短增广路，正向边加流，反向边减流，直到源汇不连通。反向边提供了“后悔机制”；Edmonds-Karp 用 BFS 固定选路规则，因此能在多项式时间内收敛到最优解。</strong></p>
</blockquote>
<p>记住以下核心要点，最大流就不再难懂：</p>
<ul>
<li><strong>最大流</strong>：能从起点 $s$ 送到终点 $t$ 的最大总水量限制。</li>
<li><strong>反向边</strong>：一条预留的“退水/后悔”通道。物理本质是<strong>相反方向的水流在管道里对冲抵消</strong>。</li>
<li><strong>残量网络</strong>：当前还能继续调整流量的网络（正向为余量，反向为退水额度）。</li>
<li><strong>增广路</strong>：残量网络中，一条从起点能顺利通往终点的送水新路线。</li>
<li><strong>最大流最小割</strong>：最大流的值 ＝ 切断整个管网所需付出的最小代价（最窄瓶颈能力）。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>0-1 背包问题（动态规划、回溯法、分支限界法）</title>
    <link href="https://blog.yoake.dev/posts/algorithm/01knapsack/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/01knapsack/</id>
    <published>2026-06-16T04:51:00.000Z</published>
    <updated>2026-06-16T04:51:00.000Z</updated>
    <summary>从决策树到分支限界，三种视角求解 0-1 背包：动态规划求最优值，回溯法与分支限界用上界剪枝。</summary>
    <content type="html"><![CDATA[<h2>一、 什么是 0-1 背包问题</h2>
<p>给定 $n$ 个物品和一个容量为 $W$ 的背包。每个物品 $i$ 有其重量 $w_i$ 和价值 $v_i$。</p>
<p>每个物品<strong>要么整个装进背包（1），要么不装（0）</strong>，不能只装入一部分。目标是在不超过背包最大容量 $W$ 的前提下，使背包中物品的<strong>总价值最大</strong>。</p>
<p>数学表达为：</p>
<p>$$
\max \sum_{i=1}^{n} x_i v_i \quad \text{s.t.} \quad \sum_{i=1}^{n} x_i w_i \le W,\quad x_i \in {0, 1}
$$</p>
<p>由于每个物品都面临“装”与“不装”两种选择，$n$ 个物品会形成约 $2^n$ 种组合。当 $n$ 较大时，直接枚举很快变得不可承受，因此需要更系统的搜索或递推算法。</p>
<hr />
<h2>二、 为什么贪心算法不行</h2>
<p>初学者最容易想到贪心。先看三种直观策略为什么得不到全局最优解。</p>
<p>假设背包容量 $W = 10$。</p>
<h3>2.1 策略一：价值最大优先</h3>
<p>每次优先选择价值最大的物品。</p>
<ul>
<li><strong>反例</strong>：有三个物品，分别为 $A$(重量9, 价值10)、$B$(重量5, 价值8)、$C$(重量5, 价值8)。</li>
<li><strong>贪心选择</strong>：优先装入价值最大的 $A$，此时剩余容量为 $1$，无法再装入其他物品。总价值为 $10$。</li>
<li><strong>全局最优</strong>：放弃 $A$，选择装入 $B$ 和 $C$。总重量为 $5 + 5 = 10 \le 10$，总价值为 $8 + 8 = 16$。</li>
<li><strong>结论</strong>：价值最大优先策略失败。</li>
</ul>
<h3>2.2 策略二：重量最小优先</h3>
<p>每次优先选择重量最小的物品。</p>
<ul>
<li><strong>反例</strong>：有三个物品，分别为 $A$(重量1, 价值1)、$B$(重量5, 价值10)、$C$(重量5, 价值10)。</li>
<li><strong>贪心选择</strong>：优先装入重量最小的 $A$，剩余容量为 $9$。接下来只能在 $B$ 和 $C$ 中选择一个装入。总价值为 $1 + 10 = 11$。</li>
<li><strong>全局最优</strong>：放弃 $A$，选择装入 $B$ 和 $C$。总重量为 $5 + 5 = 10 \le 10$，总价值为 $10 + 10 = 20$。</li>
<li><strong>结论</strong>：重量最小优先策略失败。</li>
</ul>
<h3>2.3 策略三：价值密度优先</h3>
<p>每次优先选择单位重量价值最高，即 $v_i/w_i$ 最大的物品。</p>
<ul>
<li><strong>反例</strong>：假设背包容量 $W = 8$。有三个物品，分别为 $A$(重量1, 价值3, 密度3.0)、$B$(重量4, 价值10, 密度2.5)、$C$(重量4, 价值10, 密度2.5)。</li>
<li><strong>贪心选择</strong>：优先装入密度最高的 $A$，剩余容量为 $7$。接着只能在 $B$ 和 $C$ 中选择一个装入，总重量为 $1 + 4 = 5 \le 8$，总价值为 $3 + 10 = 13$。</li>
<li><strong>全局最优</strong>：放弃 $A$，选择装入 $B$ 和 $C$。总重量为 $4 + 4 = 8 \le 8$，总价值为 $10 + 10 = 20$。</li>
<li><strong>结论</strong>：价值密度优先策略同样失败。</li>
</ul>
<p>这些策略失效的根源在于物品不可分割。局部最优可能占掉关键容量，使后续更优组合无法装入，所以需要能保证全局最优的解法。</p>
<hr />
<h2>三、 暴力搜索与价值上界</h2>
<p>既然贪心不可靠，最稳妥的起点是枚举所有组合。</p>
<h3>3.1 解空间与暴力搜索</h3>
<p>对于 $n$ 个物品，每个物品有“装”与“不装”两种状态，其解空间可以用一棵<strong>二叉子集树</strong>表示。在没有约束的情况下，这棵树共有 $2^n$ 个叶子节点。</p>
<p>以下是 $n=3$ 时的子集树结构：</p>
<pre><code>graph TD
    classDef node fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    
    Root["开始"]:::node
    
    Y1["装物品1"]:::node
    N1["不装物品1"]:::node
    
    Y2_1["装物品2"]:::node
    N2_1["不装物品2"]:::node
    Y2_2["装物品2"]:::node
    N2_2["不装物品2"]:::node
    
    Y3_1["装物品3"]:::node
    N3_1["不装物品3"]:::node
    Y3_2["装物品3"]:::node
    N3_2["不装物品3"]:::node
    Y3_3["装物品3"]:::node
    N3_3["不装物品3"]:::node
    Y3_4["装物品3"]:::node
    N3_4["不装物品3"]:::node

    Root --&gt; Y1
    Root --&gt; N1
    Y1 --&gt; Y2_1
    Y1 --&gt; N2_1
    N1 --&gt; Y2_2
    N1 --&gt; N2_2
    Y2_1 --&gt; Y3_1
    Y2_1 --&gt; N3_1
    N2_1 --&gt; Y3_2
    N2_1 --&gt; N3_2
    Y2_2 --&gt; Y3_3
    Y2_2 --&gt; N3_3
    N2_2 --&gt; Y3_4
    N2_2 --&gt; N3_4
</code></pre>
<p>最朴素的 DFS 会遍历整棵子集树，检查每一个叶子节点的总重量是否超重，并记录合法解中的最大价值。其时间复杂度为 $O(2^n)$。</p>
<h3>3.2 为什么需要剪枝</h3>
<p>当 $n$ 较大时，遍历 $2^n$ 种组合不可行。</p>
<p><strong>剪枝</strong>的思路是：如果当前分支不可能产生比已知最优解更好的结果，就停止探索，回退到上一个决策节点。</p>
<p>剪枝分为两种：</p>
<ol>
<li><strong>可行性剪枝</strong>：若装入当前物品导致当前总重量超过了背包容量 $W$，直接剪枝。</li>
<li><strong>限界剪枝</strong>：若“当前价值 + 剩余物品价值上界”仍不超过已知最优值，直接剪枝。</li>
</ol>
<p>可行性剪枝很好判断，限界剪枝的关键在于：如何给一个尚未搜索完的分支估算“最多还能拿到多少价值”。</p>
<h3>3.3 什么是 UB</h3>
<p>在搜索树的某个节点上，设：</p>
<ul>
<li>$i$：下一个要决策的物品下标；</li>
<li>$cw$：当前已经装入的总重量；</li>
<li>$cv$：当前已经装入的总价值；</li>
<li>$best$：目前已经找到的最好合法解。</li>
</ul>
<p>$UB$ 是这个节点的 <strong>Upper Bound，价值上界</strong>。它不是当前价值，而是从当前节点继续向下搜索时，理论上可能达到的最大总价值的乐观估计。</p>
<p>如果某个节点的 $UB \le best$，说明这个分支即使按最乐观方式估计，也无法超过当前最优解。此时可以剪掉整棵子树。</p>
<p>:::tip[如何理解 UB]
把 $UB$ 想成“这个分支的最高天花板”。</p>
<p>假设当前已经找到一个合法方案，价值 $best = 19$。现在搜索到另一个分支，当前价值是 $6$，把剩余容量按分数背包的最乐观方式填满后，最多也只能到 $UB = 16$。</p>
<p>这说明这个分支无论继续怎么选，都不可能超过 $19$。继续搜索只会浪费时间，因此可以直接剪掉。
:::</p>
<h3>3.4 用分数背包计算 UB</h3>
<p>0-1 背包要求物品要么完整装入，要么不装。</p>
<p>为了得到一个乐观上界，可以临时放宽限制：允许物品按比例切开装入。这就是<strong>分数背包</strong>。</p>
<p>分数背包的可选范围比 0-1 背包更大，因此它算出的价值不会小于真实的 0-1 最优值。用它作为 $UB$ 是安全的：上界可能偏高，但不能偏低。</p>
<p>在节点 $(i, cw, cv)$ 上，$UB$ 的计算步骤为：</p>
<ol>
<li>计算剩余可用容量 $rem = W - cw$。</li>
<li>将尚未决策的物品（从 $i$ 到 $n-1$）按照<strong>价值密度降序</strong>依次整件装入。</li>
<li>如果遇到某件物品完整装不下，则允许切开，只装入剩余容量能容纳的比例。</li>
<li>累计 $cv$、整件装入的价值、切分装入的价值，得到当前节点的 $UB$。</li>
</ol>
<p>用后文的统一示例计算根节点上界。背包容量 $W = 10$，物品已按价值密度降序排列：</p>
<table>
<thead>
<tr>
<th>物品 $i$</th>
<th>重量 $w_i$</th>
<th>价值 $v_i$</th>
<th>价值密度 $v_i/w_i$</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
<td>6</td>
<td>3.00</td>
</tr>
<tr>
<td>2</td>
<td>5</td>
<td>10</td>
<td>2.00</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>7</td>
<td>1.75</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>3</td>
<td>1.00</td>
</tr>
</tbody>
</table>
<p>根节点状态为 $(i=0, cw=0, cv=0)$。按分数背包估算：</p>
<ol>
<li>装入物品 1，价值 $6$，剩余容量 $8$。</li>
<li>装入物品 2，价值 $10$，剩余容量 $3$。</li>
<li>物品 3 重量为 $4$，完整装不下；切入 $3/4$ 个物品 3，得到价值 $7 \times \frac{3}{4} = 5.25$。</li>
</ol>
<p>所以根节点的价值上界为：</p>
<p>$$
UB = 0 + 6 + 10 + 5.25 = 21.25
$$</p>
<p>这个 $21.25$ 不是合法答案，因为 0-1 背包不能切开物品。</p>
<p>它只表示：从根节点继续搜索，真实最优值不会超过 $21.25$。</p>
<p>若后续已经找到 $best = 19$，而某个分支的 $UB = 18$，则该分支不可能产生更优解，可以剪枝。</p>
<hr />
<h2>四、 回溯法</h2>
<p>有了 $UB$，就可以把暴力 DFS 改成带剪枝的回溯法。</p>
<h3>4.1 整体搜索树演示</h3>
<p>继续使用上一节的示例：$n = 4$，$W = 10$，物品已按价值密度降序排列。</p>
<p>图中使用的记号如下：</p>
<ul>
<li>$cw$ 表示当前已经装入的总重量；</li>
<li>$cv$ 表示当前已经装入的总价值；</li>
<li>$UB$ 表示当前节点的总价值上界，不是当前价值；</li>
<li>边上的“装1 / 不装1”使用 1-based 物品编号，便于阅读。</li>
</ul>
<p>第一张图只展示 DFS 如何沿着“优先尝试装入”的路径找到 $best = 19$。</p>
<pre><code>graph TD
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;
    classDef path fill:#eff6ff,stroke:#2563eb,stroke-width:2px,font-family:Inter;
    classDef best fill:#ecfdf5,stroke:#059669,stroke-width:3px,font-family:Inter;
    classDef overweight fill:#fef2f2,stroke:#ef4444,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;

    R["根节点&lt;br&gt;UB=21.25"]:::root

    A["装1&lt;br&gt;cw=2, cv=6"]:::path
    B["装2&lt;br&gt;cw=7, cv=16"]:::path
    C["装3&lt;br&gt;cw=11 &gt; 10"]:::overweight
    D["不装3&lt;br&gt;cw=7, cv=16"]:::path
    E["装4&lt;br&gt;cw=10, cv=19&lt;br&gt;best=19"]:::best

    R --&gt;|先装1| A
    A --&gt;|再装2| B
    B --&gt;|尝试装3：超重| C
    B --&gt;|改为不装3| D
    D --&gt;|装4| E
</code></pre>
<p>DFS 先尝试“装1 -&gt; 装2 -&gt; 装3”。</p>
<p>由于 $cw + w_3 = 11 &gt; 10$，装入物品 3 会超重，因此转向“不装3”。</p>
<p>随后继续尝试“装4”，得到合法解 ${1, 2, 4}$，更新 $best = 19$。</p>
<p>第二张图展示 $best = 19$ 之后的回溯收尾。</p>
<p>此时只需要检查尚未展开的分支入口：如果入口节点的 $UB$ 已经小于 $best$，整棵子树都可以跳过。</p>
<pre><code>graph TD
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;
    classDef done fill:#ecfdf5,stroke:#059669,stroke-width:2px,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef leaf fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px,font-family:Inter;

    R["已找到 best=19"]:::done

    A["不装4&lt;br&gt;cv=16&lt;br&gt;叶子：不更新"]:::leaf
    B["不装2&lt;br&gt;UB=16"]:::bound
    C["不装1&lt;br&gt;UB=18"]:::bound

    R --&gt;|回溯到物品4| A
    R --&gt;|回溯到物品2| B
    R --&gt;|回溯到物品1| C

    B -.-&gt;|UB &lt; best，剪掉子树| B1["后续选择不再展开"]:::bound
    C -.-&gt;|UB &lt; best，剪掉子树| C1["后续选择不再展开"]:::bound
</code></pre>
<p>回溯过程如下：</p>
<ol>
<li>
<p>“不装4”已经是叶子节点，当前价值 $cv = 16 &lt; best(19)$，不更新最优解。</p>
</li>
<li>
<p>“不装2”的状态为 $(i=2, cw=2, cv=6)$。</p>
<p>这里的 $i$ 表示下一个要决策的物品下标，代码中使用 0-based 下标。从物品 3 起做分数贪心，$UB = 6 + 7 + 3 = 16 &lt; best(19)$，整棵子树剪掉。</p>
</li>
<li>
<p>“不装1”的状态为 $(i=1, cw=0, cv=0)$。</p>
<p>从物品 2 起做分数贪心，$UB = 10 + 7 + 1 = 18 &lt; best(19)$，整棵子树剪掉。</p>
</li>
</ol>
<p>有了前两张图的铺垫，再回看整棵搜索树会更清楚。</p>
<p>下面这张图保留分支关系和关键结果。颜色含义如下：</p>
<ul>
<li>蓝色：实际搜索路径。</li>
<li>绿色：更新最优解。</li>
<li>红色：超重剪枝。</li>
<li>橙色：限界剪枝。</li>
<li>灰色：因为祖先节点被剪枝而没有继续访问的子树。</li>
</ul>
<pre><code>graph TD
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;
    classDef path fill:#eff6ff,stroke:#2563eb,stroke-width:2px,font-family:Inter;
    classDef best fill:#ecfdf5,stroke:#059669,stroke-width:3px,font-family:Inter;
    classDef overweight fill:#fef2f2,stroke:#ef4444,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef leaf fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px,font-family:Inter;
    classDef skipped fill:#f3f4f6,stroke:#d1d5db,stroke-width:1px,color:#9ca3af,font-family:Inter;

    R["根&lt;br&gt;UB=21.25"]:::root

    A["装1&lt;br&gt;cw=2, cv=6"]:::path
    H["不装1&lt;br&gt;UB=18&lt;br&gt;剪枝"]:::bound

    B["装2&lt;br&gt;cw=7, cv=16"]:::path
    G["不装2&lt;br&gt;UB=16&lt;br&gt;剪枝"]:::bound

    C["装3&lt;br&gt;超重"]:::overweight
    D["不装3&lt;br&gt;cw=7, cv=16"]:::path

    E["装4&lt;br&gt;cv=19&lt;br&gt;best=19"]:::best
    F["不装4&lt;br&gt;cv=16&lt;br&gt;不更新"]:::leaf

    G1["装3"]:::skipped
    G2["不装3"]:::skipped
    G3["装4"]:::skipped
    G4["不装4"]:::skipped

    H1["装2"]:::skipped
    H2["不装2"]:::skipped
    H3["装3"]:::skipped
    H4["不装3"]:::skipped

    R --&gt;|装1| A
    R --&gt;|不装1| H

    A --&gt;|装2| B
    A --&gt;|不装2| G

    B --&gt;|装3| C
    B --&gt;|不装3| D

    D --&gt;|装4| E
    D --&gt;|不装4| F

    G -.-&gt;|未访问| G1
    G -.-&gt;|未访问| G2
    G2 -.-&gt;|未访问| G3
    G2 -.-&gt;|未访问| G4

    H -.-&gt;|未访问| H1
    H -.-&gt;|未访问| H2
    H2 -.-&gt;|未访问| H3
    H2 -.-&gt;|未访问| H4
</code></pre>
<p>所有可产生更优解的分支都已处理完，算法结束，最优解为 $19$。</p>
<h3>4.2 Java 实现</h3>
<pre><code>public class KnapsackBacktrack {
    private static int[] w, v;
    private static int W, n, best;

    public static int solve(int[] weight, int[] value, int capacity) {
        w = weight; v = value; W = capacity; n = w.length;
        best = 0;
        backtrack(0, 0, 0);
        return best;
    }

    private static void backtrack(int i, int cw, int cv) {
        if (i == n) {
            if (cv &gt; best) best = cv;
            return;
        }
        // 限界剪枝：如果当前期望能达到的最大价值上限都不超过已知最优解，则剪枝
        if (cv + fractionalUB(i, cw) &lt;= best) return; 

        // 可行性剪枝：只在不超过容量限制的前提下装入物品
        if (cw + w[i] &lt;= W) {
            backtrack(i + 1, cw + w[i], cv + v[i]); // 可行分支
        }
        backtrack(i + 1, cw, cv); // 不装分支
    }

    // 分数背包上界函数
    private static double fractionalUB(int i, int cw) {
        double ub = 0;
        int rem = W - cw;
        for (int k = i; k &lt; n &amp;&amp; rem &gt; 0; k++) {
            if (w[k] &lt;= rem) { rem -= w[k]; ub += v[k]; }
            else { ub += (double) v[k] / w[k] * rem; break; }
        }
        return ub;
    }

    public static void main(String[] args) {
        int[] w = {2, 5, 4, 3};
        int[] v = {6, 10, 7, 3};
        int W = 10;
        System.out.println("最大价值 = " + solve(w, v, W));
    }
}
</code></pre>
<p>运行输出：</p>
<pre><code>最大价值 = 19
</code></pre>
<hr />
<h2>五、 动态规划</h2>
<p>回溯法即使有剪枝，最坏时间复杂度仍是 $O(2^n)$。动态规划通过记录子问题，消除重复计算。</p>
<p>动态规划的重点不是循环流程，而是每个状态从哪里来。先记住一句话：<strong>当前格子只从上一行的结果转移而来</strong>。</p>
<h3>5.1 状态定义</h3>
<p>动态规划适用于背包容量 $W$ 为非负整数的场景。它不再沿着搜索树试探每条路径，而是把已经算过的子问题保存下来。</p>
<p>定义：</p>
<p><strong>$f[i][j]$ 表示：只考虑前 $i$ 个物品，在背包容量为 $j$ 时能获得的最大价值。</strong></p>
<p>那么原问题的答案就是：</p>
<p>$$
f[n][W]
$$</p>
<p>边界也很自然：</p>
<ul>
<li>$f[0][j] = 0$：没有物品可选，价值只能是 0。</li>
<li>$f[i][0] = 0$：容量为 0，什么也装不下。</li>
</ul>
<h3>5.2 状态转移</h3>
<p>对于第 $i$ 个物品，只有两种选择：不装，或者装。</p>
<p><strong>不装第 $i$ 个物品</strong>：</p>
<p>$$
f[i][j] = f[i-1][j]
$$</p>
<p><strong>装第 $i$ 个物品</strong>，前提是容量够，即 $j \ge w_i$。装入后剩余容量为 $j-w_i$，总价值为：</p>
<p>$$
f[i-1][j-w_i] + v_i
$$</p>
<p>所以转移方程是：</p>
<p>$$
f[i][j] = \begin{cases} f[i-1][j] &amp; j &lt; w_i \ \max(f[i-1][j], f[i-1][j-w_i] + v_i) &amp; j \ge w_i \end{cases}
$$</p>
<p>用统一示例算一个格子：$f[2][7]$。</p>
<p>此时只考虑前 2 个物品，容量为 $7$。物品 2 的重量为 $5$，价值为 $10$。</p>
<ul>
<li>不装物品 2：$f[1][7] = 6$。</li>
<li>装物品 2：$f[1][2] + 10 = 6 + 10 = 16$。</li>
</ul>
<p>取较大值：</p>
<p>$$
f[2][7] = \max(6, 16) = 16
$$</p>
<p>这个格子的依赖关系如下：</p>
<pre><code>graph TD
    classDef current fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef prev fill:#f8fafc,stroke:#cbd5e1,stroke-width:1.5px,font-family:Inter;
    classDef choose fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef result fill:#ecfdf5,stroke:#10b981,stroke-width:2px,font-family:Inter;

    A["当前格&lt;br&gt;f[2][7]"]:::current
    B["不装物品2&lt;br&gt;看 f[1][7] = 6"]:::prev
    C["装物品2&lt;br&gt;看 f[1][2] + 10 = 16"]:::choose
    D["取最大值&lt;br&gt;f[2][7] = 16"]:::result

    A --&gt; B
    A --&gt; C
    B --&gt; D
    C --&gt; D
</code></pre>
<p>这就是动态规划的关键：每个格子只从上一行取结果，避免重复枚举同一批选择。</p>
<h3>5.3 二维 DP 写法</h3>
<p>代码中的物品数组是 0-based，因此第 $i$ 个物品对应 <code>w[i - 1]</code> 和 <code>v[i - 1]</code>。</p>
<pre><code>public class KnapsackDP {
    public static int solve(int[] w, int[] v, int W) {
        int n = w.length;
        int[][] f = new int[n + 1][W + 1];
        for (int i = 1; i &lt;= n; i++) {
            for (int j = 0; j &lt;= W; j++) {
                f[i][j] = f[i - 1][j];
                if (j &gt;= w[i - 1]) {
                    f[i][j] = Math.max(f[i][j], f[i - 1][j - w[i - 1]] + v[i - 1]);
                }
            }
        }

        // 逆向递推恢复最优解路径
        System.out.print("装入物品的决策路径：{ ");
        int j = W;
        for (int i = n; i &gt;= 1; i--) {
            if (f[i][j] != f[i - 1][j]) {
                System.out.print(i + " ");
                j -= w[i - 1];
            }
        }
        System.out.println("}");

        return f[n][W];
    }

    public static void main(String[] args) {
        int[] w = {2, 5, 4, 3};
        int[] v = {6, 10, 7, 3};
        int W = 10;
        System.out.println("最大价值 = " + solve(w, v, W));
    }
}
</code></pre>
<p>运行输出：</p>
<pre><code>装入物品的决策路径：{ 4 2 1 }
最大价值 = 19
</code></pre>
<h3>5.4 一维 DP 优化</h3>
<p>二维 DP 中，$f[i][j]$ 只依赖上一行。因此可以把表压缩成一维数组：</p>
<p><strong>$f[j]$ 表示当前已经处理过若干物品后，容量为 $j$ 时的最大价值。</strong></p>
<pre><code>int[] f = new int[W + 1];
for (int i = 0; i &lt; n; i++) {
    for (int j = W; j &gt;= w[i]; j--) {
        f[j] = Math.max(f[j], f[j - w[i]] + v[i]);
    }
}
</code></pre>
<p>:::note[为什么容量要倒序遍历]
这里最关键的是容量要倒序遍历。</p>
<pre><code>flowchart RL
    classDef cell fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef warn fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;

    J10["dp[10]"]:::cell --&gt; J9["dp[9]"]:::cell --&gt; J8["dp[8]"]:::cell --&gt; J7["..."]:::cell --&gt; JW["dp[w]"]:::cell
    N["容量从大到小更新&lt;br&gt;保证 dp[j-w] 仍来自上一轮"]:::warn
    J10 -.-&gt; N
</code></pre>
<p>如果从左到右更新，$f[j - w_i]$ 可能已经在当前这一轮被物品 $i$ 更新过，相当于同一个物品被重复使用。倒序遍历时，$f[j - w_i]$ 仍是上一轮物品留下的状态，可以保证当前物品最多只选一次。</p>
<p>一句话记忆：</p>
<pre><code>0-1 背包：容量倒序
完全背包：容量正序
</code></pre>
<p>:::</p>
<h3>5.5 如何恢复最优方案</h3>
<p>如果需要恢复具体选择，可以从 $f[n][W]$ 反向走回去：</p>
<ul>
<li>若 $f[i][j] == f[i-1][j]$，可以沿“不选物品 $i$”继续。</li>
<li>若 $f[i][j] \ne f[i-1][j]$，当前恢复路径选择了物品 $i$，然后跳到 $f[i-1][j-w_i]$。</li>
</ul>
<p>统一示例中从 $f[4][10] = 19$ 逆推，可以得到一组最优方案：选择物品 ${1, 2, 4}$。若存在并列最优解，恢复路径可能不止一条。</p>
<hr />
<h2>六、 分支限界法</h2>
<h3>6.1 基本思想</h3>
<p>回溯法使用 DFS，先沿一条路径走到底，再回头处理其他分支。分支限界法换一种顺序：把所有待扩展节点放进优先队列，每次选择 $UB$ 最大的节点扩展。</p>
<p>两者都会使用 $UB$ 剪枝，但侧重点不同：</p>
<ul>
<li><strong>回溯法</strong>：$UB$ 主要用于判断当前 DFS 分支要不要剪掉。</li>
<li><strong>分支限界法</strong>：$UB$ 不仅用于剪枝，还用于决定下一个扩展哪个活节点。</li>
</ul>
<p>所以，分支限界法不是换了一种上界计算方式，而是换了一种搜索顺序：从“深度优先”改为“最有希望的节点优先”。</p>
<p>这里的 $UB$ 仍然是前文讲过的分数背包上界。它表示“从当前节点继续搜索，理论上最多能达到多少价值”。</p>
<p>分支限界法的核心规则很简单：</p>
<ul>
<li><strong>优先扩展</strong>：优先队列每次弹出 $UB$ 最大的活节点。</li>
<li><strong>限界剪枝</strong>：若某个节点的 $UB \le best$，说明它不可能产生更优解，整棵子树可以跳过。</li>
</ul>
<p>下面先看一个局部例子。</p>
<p>根节点展开后，队列里有两个活节点：</p>
<ul>
<li><code>装1</code> 的 $UB = 21.25$。</li>
<li><code>不装1</code> 的 $UB = 18.00$。</li>
</ul>
<p>分支限界法会先扩展 $UB$ 更大的 <code>装1</code>。</p>
<pre><code>graph TD
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef wait fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;

    R["根节点&lt;br&gt;UB=21.25"]:::root
    A["装1&lt;br&gt;UB=21.25&lt;br&gt;下一步扩展"]:::active
    B["不装1&lt;br&gt;UB=18.00&lt;br&gt;暂存队列"]:::wait

    R --&gt;|装1| A
    R --&gt;|不装1| B
</code></pre>
<h3>6.2 队列演化推演</h3>
<p>继续使用统一示例。下面按步骤展示优先队列如何变化。</p>
<p>颜色含义如下：</p>
<ul>
<li>黄色：当前出队并扩展的节点。</li>
<li>蓝色：仍在队列中等待的节点。</li>
<li>橙色：因为 $UB \le best$ 不入队或被剪掉。</li>
<li>红色：超重剪枝。</li>
</ul>
<h4>第一步：扩展根节点</h4>
<p>初始时只有根节点。扩展根节点后生成两个分支：<code>装1</code> 和 <code>不装1</code>。</p>
<p>因为 <code>装1</code> 的 $UB$ 更大，下一步会优先扩展它。</p>
<pre><code>graph TD
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef queue fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;

    R["root&lt;br&gt;UB=21.25&lt;br&gt;出队扩展"]:::active
    A["装1&lt;br&gt;UB=21.25&lt;br&gt;队首"]:::queue
    B["不装1&lt;br&gt;UB=18.00&lt;br&gt;等待"]:::queue

    R --&gt;|装1| A
    R --&gt;|不装1| B
</code></pre>
<p>此时 $best$ 从 $0$ 更新为 $6$，队列为：<code>装1(21.25)</code>、<code>不装1(18.00)</code>。</p>
<h4>第二步：扩展 <code>装1</code></h4>
<p>扩展 <code>装1</code> 后，<code>装2</code> 的上界仍为 $21.25$，继续排在队首；<code>不装2</code> 的上界为 $16.00$，此时 $best$ 已更新为 $16$，所以它不会入队。</p>
<pre><code>graph TD
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef queue fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef wait fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px,font-family:Inter;

    A["装1&lt;br&gt;UB=21.25&lt;br&gt;出队扩展"]:::active
    B["装2&lt;br&gt;UB=21.25&lt;br&gt;队首"]:::queue
    C["不装2&lt;br&gt;UB=16.00&lt;br&gt;不入队"]:::bound
    D["不装1&lt;br&gt;UB=18.00&lt;br&gt;等待"]:::wait

    A --&gt;|装2| B
    A --&gt;|不装2| C
    D -.-&gt;|仍在队列| B
</code></pre>
<p>此时 $best$ 从 $6$ 更新为 $16$，队列为：<code>装1,2(21.25)</code>、<code>不装1(18.00)</code>。</p>
<h4>第三步：扩展 <code>装1,2</code></h4>
<p>扩展 <code>装1,2</code> 时，尝试装入物品 3 会超重，因此直接剪掉；不装物品 3 后得到节点 <code>装1,2,不装3</code>，它的 $UB=19.00$，仍然大于当前 $best=16$，所以入队并成为队首。</p>
<pre><code>graph TD
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef queue fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef overweight fill:#fef2f2,stroke:#ef4444,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef wait fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px,font-family:Inter;

    A["装1,2&lt;br&gt;UB=21.25&lt;br&gt;出队扩展"]:::active
    B["装3&lt;br&gt;cw=11&gt;10&lt;br&gt;超重"]:::overweight
    C["不装3&lt;br&gt;UB=19.00&lt;br&gt;队首"]:::queue
    D["不装1&lt;br&gt;UB=18.00&lt;br&gt;等待"]:::wait

    A --&gt;|装3| B
    A --&gt;|不装3| C
    D -.-&gt;|仍在队列| C
</code></pre>
<p>此时 $best$ 仍为 $16$，队列为：<code>装1,2,不装3(19.00)</code>、<code>不装1(18.00)</code>。</p>
<h4>第四步：扩展 <code>装1,2,不装3</code></h4>
<p>扩展 <code>装1,2,不装3</code> 时，装入物品 4 得到合法解，价值为 $19$，于是更新 $best=19$。不装物品 4 的上界只有 $16.00$，不再入队。</p>
<pre><code>graph TD
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef best fill:#ecfdf5,stroke:#10b981,stroke-width:3px,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef wait fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px,font-family:Inter;

    A["装1,2,不装3&lt;br&gt;UB=19.00&lt;br&gt;出队扩展"]:::active
    B["装4&lt;br&gt;cv=19&lt;br&gt;best=19"]:::best
    C["不装4&lt;br&gt;UB=16.00&lt;br&gt;不入队"]:::bound
    D["不装1&lt;br&gt;UB=18.00&lt;br&gt;等待"]:::wait

    A --&gt;|装4| B
    A --&gt;|不装4| C
    D -.-&gt;|仍在队列| B
</code></pre>
<p>此时队列只剩 <code>不装1(18.00)</code>。</p>
<h4>第五步：剪枝收尾</h4>
<p>最后弹出 <code>不装1</code>。</p>
<p>它的 $UB = 18.00$，已经不可能超过 $best = 19$，直接剪枝。队列清空，算法结束。</p>
<pre><code>graph TD
    classDef best fill:#ecfdf5,stroke:#10b981,stroke-width:3px,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;

    A["当前 best=19"]:::best
    B["不装1&lt;br&gt;UB=18.00&lt;br&gt;剪枝"]:::bound

    A --&gt;|UB &lt;= best| B
</code></pre>
<p>把五个步骤合在一起，可以得到整棵搜索树。颜色含义如下：</p>
<ul>
<li>黄色：按 $UB$ 优先扩展的路径。</li>
<li>蓝色：曾进入优先队列等待的节点。</li>
<li>绿色：更新 $best = 19$。</li>
<li>橙色：$UB \le best$，剪枝或不入队。</li>
<li>红色：超重剪枝。</li>
</ul>
<pre><code>graph TD
    classDef root fill:#f8fafc,stroke:#64748b,stroke-width:2px,font-family:Inter;
    classDef active fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,font-family:Inter;
    classDef queue fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px,font-family:Inter;
    classDef best fill:#ecfdf5,stroke:#10b981,stroke-width:3px,font-family:Inter;
    classDef bound fill:#fff7ed,stroke:#f97316,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;
    classDef overweight fill:#fef2f2,stroke:#ef4444,stroke-width:2px,stroke-dasharray:5 5,font-family:Inter;

    R["根&lt;br&gt;UB=21.25"]:::root

    A["装1&lt;br&gt;UB=21.25&lt;br&gt;第1个扩展"]:::active
    H["不装1&lt;br&gt;UB=18.00&lt;br&gt;先等待，后剪枝"]:::queue

    B["装2&lt;br&gt;UB=21.25&lt;br&gt;第2个扩展"]:::active
    G["不装2&lt;br&gt;UB=16.00&lt;br&gt;不入队"]:::bound

    C["装3&lt;br&gt;cw=11&gt;10&lt;br&gt;超重"]:::overweight
    D["不装3&lt;br&gt;UB=19.00&lt;br&gt;第3个扩展"]:::active

    E["装4&lt;br&gt;cv=19&lt;br&gt;best=19"]:::best
    F["不装4&lt;br&gt;UB=16.00&lt;br&gt;不入队"]:::bound

    R --&gt;|装1| A
    R --&gt;|不装1| H

    A --&gt;|装2| B
    A --&gt;|不装2| G

    B --&gt;|装3| C
    B --&gt;|不装3| D

    D --&gt;|装4| E
    D --&gt;|不装4| F

    H -.-&gt;|最后出队&lt;br&gt;UB &lt; best| Hx["剪枝"]:::bound
</code></pre>
<p>最终最优值为 $19$，对应选择物品 ${1,2,4}$。</p>
<h3>6.3 Java 实现</h3>
<pre><code>import java.util.PriorityQueue;

public class KnapsackBB {
    private static int[] w, v;
    private static int W, n;

    // 活节点：优先队列比较器，ub 越大越优先扩展
    private static class Node implements Comparable&lt;Node&gt; {
        int i, cw, cv;
        double ub;
        Node(int i, int cw, int cv, double ub) {
            this.i = i; this.cw = cw; this.cv = cv; this.ub = ub;
        }
        public int compareTo(Node o) { 
            return Double.compare(o.ub, this.ub); 
        }
    }

    public static int solve(int[] weight, int[] value, int capacity) {
        w = weight; v = value; W = capacity; n = w.length;
        PriorityQueue&lt;Node&gt; pq = new PriorityQueue&lt;&gt;();
        int best = 0;
        pq.offer(new Node(0, 0, 0, fractionalUB(0, 0)));

        while (!pq.isEmpty()) {
            Node x = pq.poll();
            
            // 限界剪枝：如果当前出队节点的上限都比不上当前已知最优，则其子代也无希望
            if (x.ub &lt;= best) continue;          
            
            // 到达叶子节点
            if (x.i == n) {                       
                if (x.cv &gt; best) best = x.cv;
                continue;
            }
            int i = x.i;
            
            // 分支 1：装入物品 i（必须满足重量约束）
            if (x.cw + w[i] &lt;= W) {
                int ncw = x.cw + w[i], ncv = x.cv + v[i];
                double ub = ncv + fractionalUB(i + 1, ncw);
                if (ncv &gt; best) best = ncv;       // 更新当前最好可行值
                if (ub &gt; best) {
                    pq.offer(new Node(i + 1, ncw, ncv, ub));
                }
            }
            // 分支 2：不装物品 i
            double sub = x.cv + fractionalUB(i + 1, x.cw);
            if (sub &gt; best) {
                pq.offer(new Node(i + 1, x.cw, x.cv, sub));
            }
        }
        return best;
    }

    private static double fractionalUB(int i, int cw) {
        double ub = 0;
        int rem = W - cw;
        for (int k = i; k &lt; n &amp;&amp; rem &gt; 0; k++) {
            if (w[k] &lt;= rem) { rem -= w[k]; ub += v[k]; }
            else { ub += (double) v[k] / w[k] * rem; break; }
        }
        return ub;
    }

    public static void main(String[] args) {
        int[] w = {2, 5, 4, 3};
        int[] v = {6, 10, 7, 3};
        int W = 10;
        System.out.println("最大价值 = " + solve(w, v, W));
    }
}
</code></pre>
<p>运行输出：</p>
<pre><code>最大价值 = 19
</code></pre>
<hr />
<h2>七、 三种算法的统一视角</h2>
<p>三种算法都可以放在“如何处理指数级解空间”这一脉络下理解：</p>
<p><img src="https://img.picr2.online/img/2026/06/17/7dce0e4f7d1e0c95416e59136d38a2cc.svg" alt="三种算法的统一视角" /></p>
<ul>
<li><strong>暴力搜索</strong>：在子集树中不加选择地走完所有 $2^n$ 条路径，这是最基础的起点。</li>
<li><strong>回溯法</strong>：给暴力搜索加了可行性剪枝和分数背包上界，用 DFS 规避无用分支。</li>
<li><strong>分支限界法</strong>：在回溯法的基础上，将“深度优先”改为“最有希望的分支优先”，用优先队列保证最快拿到高分，从而实现更早、更大面积地对其他分支剪枝。</li>
<li><strong>动态规划</strong>：跳出了“树形搜索”的框架，转而寻找“状态间的递推关系”。通过定义 $f[i][j]$，它把搜索过程中可能重复计算的相同子问题记录下来，自底向上递推，从而将指数级的搜索压缩到了伪多项式级 $O(nW)$。</li>
</ul>
<hr />
<h2>八、 三种方法横向对比</h2>
<table>
<thead>
<tr>
<th>维度</th>
<th>动态规划</th>
<th>回溯法</th>
<th>分支限界法</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>核心思想</strong></td>
<td>最优子结构 + 自底向上填表</td>
<td>DFS 遍历子集树 + 剪枝</td>
<td>优先队列最佳优先扩展 + 上界剪枝</td>
</tr>
<tr>
<td><strong>搜索机制</strong></td>
<td>自底向上直接填表，不进行搜索</td>
<td>DFS（深度优先），撞墙后回溯</td>
<td>最佳优先，优先扩展上界大的节点</td>
</tr>
<tr>
<td><strong>时间复杂度</strong></td>
<td>$O(nW)$ 伪多项式</td>
<td>最坏 $O(2^n)$，实际高度依赖剪枝</td>
<td>最坏 $O(2^n)$，实际高度依赖剪枝</td>
</tr>
<tr>
<td><strong>空间复杂度</strong></td>
<td>$O(nW)$（一维优化后为 $O(W)$）</td>
<td>$O(n)$（主要是递归调用栈的深度）</td>
<td>最坏 $O(2^n)$（优先队列中活节点可能极多）</td>
</tr>
<tr>
<td><strong>获取具体方案</strong></td>
<td>需要通过已填表格进行逆向回溯寻找</td>
<td>天然支持在搜索中直接记录</td>
<td>较为复杂，需要为节点保存父指针信息</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>$W$ 为不大整数的绝大多数情况</td>
<td>$n$ 较小，或需要枚举出所有可行方案</td>
<td>$n$ 中等，只求最优解值，且 $W$ 实数或极大</td>
</tr>
</tbody>
</table>
<hr />
<h2>九、 总结</h2>
<p>0-1 背包的难点在于指数级解空间。本文介绍的三种方法分别从两个方向压缩搜索成本：</p>
<ul>
<li><strong>分数上界</strong>：通过“允许将物品切开”的放宽约束，得到搜索分支的乐观上界，用于回溯法和分支限界法剪枝。</li>
<li><strong>状态设计</strong>：用“物品索引”和“背包容量”描述子问题，让动态规划可以复用重复状态，在伪多项式时间内求解。</li>
</ul>
<p><strong>算法选型场景指导</strong>：</p>
<ul>
<li>如果背包容量 $W$ 较小，且为整数：优先使用动态规划。</li>
<li>如果需要找出所有可行方案：使用回溯法。</li>
<li>如果问题规模较大且只需要获得最优解值：可以考虑分支限界法。</li>
</ul>
<hr />
<h2>十、 扩展阅读：复杂度与 NP 理论</h2>
<p>从算法理论角度来看，0-1 背包问题属于经典的第一批被证明的 NP-Hard 优化问题（其判定版本“在价值不小于 $V$ 的前提下，能否在背包中装入不超过重量 $W$ 的物品”属于 NP-Complete 问题）。</p>
<p>这意味着目前学术界普遍认为无法在 $O(n^k)$（多项式时间）内求得 0-1 背包问题的精确最优解。随着物品数 $n$ 的增长，其最坏解空间的规模呈 $2^n$ 指数速度爆炸增长。</p>
<h3>10.1 伪多项式时间的奥秘</h3>
<p>尽管如此，动态规划能以 $O(nW)$ 求得最优值。为什么这没有和 NP-Hard 的结论冲突？</p>
<p>原因在于：<strong>$O(nW)$ 是伪多项式时间</strong>。
在计算机复杂度理论中，输入规模是以<strong>表示输入所需的二进制位数</strong>来度量的：</p>
<ul>
<li>物品数 $n$ 的值增长，其输入长度呈线性增长。</li>
<li>背包容量 $W$ 的值增长，其输入长度仅呈对数 $\log_2(W)$ 增长。</li>
</ul>
<p>这意味着，容量 $W$ 翻倍时，输入长度只增加 1 bit，但动态规划时间会直接翻倍。当 $W = 10^9$ 时，即使 $n=10$，动态规划也要处理 $10^{10}$ 个状态，可能比指数搜索还慢。
因此，动态规划只有在 $W$ 的值被限制在 $n$ 的多项式级别内时才是高效的。若 $W$ 的值呈指数增长或为浮点数，通常就需要考虑分支限界、按价值维度动态规划，或者使用近似算法（如 FPTAS）来求取近似最优解。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>Dijkstra 单源最短路径</title>
    <link href="https://blog.yoake.dev/posts/algorithm/dijkstrasssp/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/dijkstrasssp/</id>
    <published>2026-06-15T13:22:19.000Z</published>
    <updated>2026-06-15T13:22:19.000Z</updated>
    <summary>每次从未敲定顶点中选出距源点最近者，松弛它的所有出边，逐步得到源点到各顶点的最短距离。</summary>
    <content type="html"><![CDATA[<h2>一、问题定义</h2>
<p>给定一张<strong>带权有向图</strong>（或无向图）和一个<strong>源点</strong> $s$，求从 $s$ 到其余每个顶点的<strong>最短路径长度</strong>，这就是<strong>单源最短路径</strong>问题。</p>
<p>:::caution[适用前提]
Dijkstra 算法要求图中<strong>所有边的权值非负</strong>（$w \ge 0$）。若存在负权边，需改用 Bellman-Ford 或 SPFA 算法。
:::</p>
<hr />
<h2>二、基本思想</h2>
<p>Dijkstra 算法采用贪心策略，结构上与 <a href="/posts/algorithm/primmst/">Prim 最小生成树</a> 高度相似。</p>
<p>将所有顶点分为两组：</p>
<ul>
<li><strong>已敲定集合</strong> $S$：已经确定了最短路径的顶点。</li>
<li><strong>未敲定集合</strong> $Q$：尚未确定最短路径的顶点。</li>
</ul>
<p>用数组 $d[]$ 记录源点 $s$ 到每个顶点的<strong>当前已知最短距离估计值</strong>，初始时：</p>
<p>$$
d[v] =
\begin{cases}
0, &amp; v = s \
+\infty, &amp; v \ne s
\end{cases}
$$</p>
<p>算法的每一轮做两件事：</p>
<ol>
<li><strong>选择顶点</strong>：从未敲定集合 $Q$ 中取出 $d$ 值最小的顶点 $u$，将它<strong>移入已敲定集合</strong> $S$——此时 $d[u]$ 即为 $s$ 到 $u$ 的真实最短距离。</li>
<li><strong>松弛</strong>：对于 $u$ 的每一条出边 $(u, v, w)$，若 $d[u] + w &lt; d[v]$，则更新 $d[v] = d[u] + w$。</li>
</ol>
<p>重复上述过程，直到所有顶点都被敲定为止。</p>
<p>:::tip[局部最优推导全局最优]
关键在于<strong>非负权值</strong>。当 $u$ 被选出时，它的估计距离最小，其他未确认顶点 $x$ 都满足 $d[x] \ge d[u]$。由于边权非负，绕到 $x$ 再回到 $u$ 只会更远，不可能得到比 $d[u]$ 更短的路径。因此 $d[u]$ 可以被安全敲定。
:::</p>
<p>算法整体流程如下：</p>
<p><img src="https://img.picr2.online/1781528614128_dijkstra-flow.svg" alt="算法流程" /></p>
<hr />
<h2>三、算法执行步骤图解</h2>
<p>以如下 6 顶点带权有向图为例（顶点：$A, B, C, D, E, F$，源点为 $A$）：</p>
<p><img src="https://img.picr2.online/1781528567186_dijkstra-graph.svg" alt="示例图" /></p>
<table>
<thead>
<tr>
<th>边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>A→B</code></td>
<td>4</td>
</tr>
<tr>
<td><code>A→C</code></td>
<td>2</td>
</tr>
<tr>
<td><code>B→C</code></td>
<td>1</td>
</tr>
<tr>
<td><code>B→D</code></td>
<td>5</td>
</tr>
<tr>
<td><code>C→B</code></td>
<td>1</td>
</tr>
<tr>
<td><code>C→D</code></td>
<td>8</td>
</tr>
<tr>
<td><code>C→E</code></td>
<td>10</td>
</tr>
<tr>
<td><code>D→E</code></td>
<td>2</td>
</tr>
<tr>
<td><code>D→F</code></td>
<td>6</td>
</tr>
<tr>
<td><code>E→F</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>初始状态：$d = [0,\ \infty,\ \infty,\ \infty,\ \infty,\ \infty]$，$S = \emptyset$。</p>
<p><img src="https://img.picr2.online/1781528567437_dijkstra-0.svg" alt="初始状态" /></p>
<hr />
<h3>第 1 轮：选出 $A$（$d=0$）</h3>
<p>松弛出边 $A \to B(4)$、$A \to C(2)$，$d[B]=4$，$d[C]=2$。</p>
<p><img src="https://img.picr2.online/1781528567625_dijkstra-1.svg" alt="第 1 轮" /></p>
<hr />
<h3>第 2 轮：选出 $C$（$d=2$）</h3>
<p>松弛 $C \to B(1)$，$d[B]$ 由 $4$ 降至 $3$；松弛 $C \to D(8)$、$C \to E(10)$，$d[D]=10$，$d[E]=12$。</p>
<p><img src="https://img.picr2.online/1781528568329_dijkstra-2.svg" alt="第 2 轮" /></p>
<hr />
<h3>第 3 轮：选出 $B$（$d=3$）</h3>
<p>$B \to C(1)$：$C$ 已敲定，跳过。$B \to D(5)$，$d[D]$ 由 $10$ 降至 $8$。</p>
<p><img src="https://img.picr2.online/1781528569333_dijkstra-3.svg" alt="第 3 轮" /></p>
<hr />
<h3>第 4 轮：选出 $D$（$d=8$）</h3>
<p>松弛 $D \to E(2)$，$d[E]$ 由 $12$ 降至 $10$；松弛 $D \to F(6)$，$d[F]=14$。</p>
<p><img src="https://img.picr2.online/1781528570279_dijkstra-4.svg" alt="第 4 轮" /></p>
<hr />
<h3>第 5 轮：选出 $E$（$d=10$）</h3>
<p>松弛 $E \to F(3)$，$d[F]$ 由 $14$ 降至 $13$。</p>
<p><img src="https://img.picr2.online/1781528569587_dijkstra-5.svg" alt="第 5 轮" /></p>
<hr />
<h3>第 6 轮：选出 $F$（$d=13$）</h3>
<p>$F$ 无出边，不再松弛。所有顶点敲定，算法结束。</p>
<p><img src="https://img.picr2.online/1781528570190_dijkstra-6.svg" alt="第 6 轮" /></p>
<h3>最终结果</h3>
<p>源点 $A$ 到各顶点的最短距离：</p>
<p>$$
d[A]=0,\quad d[C]=2,\quad d[B]=3,\quad d[D]=8,\quad d[E]=10,\quad d[F]=13
$$</p>
<p>最短路径树由 5 条边构成（绿色加粗）：$A \to C \to B \to D \to E \to F$。</p>
<p><img src="https://img.picr2.online/1781528613844_dijkstra-result.svg" alt="最短路径树" /></p>
<hr />
<h2>四、两种实现方式</h2>
<h3>4.1 朴素实现（邻接矩阵，$O(V^2)$）</h3>
<p>适合<strong>稠密图</strong>（边数 $E \approx V^2$）。每轮线性扫描所有顶点，找出 $d$ 值最小的未敲定顶点。</p>
<pre><code>import java.util.Arrays;

public class DijkstraMatrix {

    private static final int INF = Integer.MAX_VALUE / 2;

    /**
     * 朴素 Dijkstra（邻接矩阵版）。
     * @param graph graph[u][v] 表示 u→v 的边权，INF 表示无边
     * @param src   源点编号
     * @return      源点到各顶点的最短距离数组
     */
    public static int[] dijkstra(int[][] graph, int src) {
        int n = graph.length;
        int[] d = new int[n];
        boolean[] settled = new boolean[n];

        Arrays.fill(d, INF);
        d[src] = 0;

        for (int iter = 0; iter &lt; n; iter++) {
            // 1. 从未敲定顶点中选出 d 值最小的顶点 u
            int u = -1;
            for (int v = 0; v &lt; n; v++) {
                if (!settled[v] &amp;&amp; (u == -1 || d[v] &lt; d[u])) {
                    u = v;
                }
            }
            if (d[u] == INF) break; // 剩余顶点不可达，提前退出

            // 2. 敲定 u，松弛所有出边
            settled[u] = true;
            for (int v = 0; v &lt; n; v++) {
                if (!settled[v] &amp;&amp; graph[u][v] &lt; INF) {
                    d[v] = Math.min(d[v], d[u] + graph[u][v]);
                }
            }
        }
        return d;
    }

    public static void main(String[] args) {
        // 顶点编号：A=0, B=1, C=2, D=3, E=4, F=5
        int n = 6;
        int[][] g = new int[n][n];
        for (int[] row : g) Arrays.fill(row, INF);
        for (int i = 0; i &lt; n; i++) g[i][i] = 0;

        g[0][1] = 4;  // A→B
        g[0][2] = 2;  // A→C
        g[1][2] = 1;  // B→C
        g[1][3] = 5;  // B→D
        g[2][1] = 1;  // C→B
        g[2][3] = 8;  // C→D
        g[2][4] = 10; // C→E
        g[3][4] = 2;  // D→E
        g[3][5] = 6;  // D→F
        g[4][5] = 3;  // E→F

        int[] dist = dijkstra(g, 0);
        String[] name = {"A", "B", "C", "D", "E", "F"};
        System.out.println("从 A 出发的最短距离：");
        for (int i = 0; i &lt; n; i++) {
            System.out.println("  A → " + name[i] + " = " + dist[i]);
        }
    }
}
</code></pre>
<p>运行输出：</p>
<pre><code>从 A 出发的最短距离：
  A → A = 0
  A → B = 3
  A → C = 2
  A → D = 8
  A → E = 10
  A → F = 13
</code></pre>
<hr />
<h3>4.2 堆优化实现（邻接表 + 优先队列，$O(E \log V)$）</h3>
<p>适合<strong>稀疏图</strong>（边数 $E \ll V^2$）。用最小堆维护 $d$ 值最小的候选顶点，每次松弛后将新的 $(d[v], v)$ 压入堆，避免线性扫描。</p>
<p>:::note[懒删除策略]
堆中可能存有同一顶点的多条过期记录。弹出时检查该记录的距离是否等于当前 $d[v]$；若不等，说明已过期，跳过即可。
:::</p>
<pre><code>import java.util.*;

public class DijkstraHeap {

    private static final int INF = Integer.MAX_VALUE / 2;

    /**
     * 堆优化 Dijkstra（邻接表版）。
     * @param adj adj[u] 存储所有从 u 出发的边：int[]{v, weight}
     * @param n   顶点总数
     * @param src 源点编号
     * @return    源点到各顶点的最短距离数组
     */
    public static int[] dijkstra(List&lt;int[]&gt;[] adj, int n, int src) {
        int[] d = new int[n];
        Arrays.fill(d, INF);
        d[src] = 0;

        // 最小堆：按距离升序，存储 {dist, vertex}
        PriorityQueue&lt;int[]&gt; pq = new PriorityQueue&lt;&gt;(Comparator.comparingInt(a -&gt; a[0]));
        pq.offer(new int[]{0, src});

        while (!pq.isEmpty()) {
            int[] top = pq.poll();
            int dist = top[0];
            int u = top[1];

            // 过期记录：跳过
            if (dist &gt; d[u]) continue;

            // 松弛所有出边
            for (int[] edge : adj[u]) {
                int v = edge[0];
                int w = edge[1];
                if (d[u] + w &lt; d[v]) {
                    d[v] = d[u] + w;
                    pq.offer(new int[]{d[v], v});
                }
            }
        }
        return d;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        int n = 6;
        List&lt;int[]&gt;[] adj = new ArrayList[n];
        for (int i = 0; i &lt; n; i++) adj[i] = new ArrayList&lt;&gt;();

        // 顶点编号：A=0, B=1, C=2, D=3, E=4, F=5
        adj[0].add(new int[]{1, 4});  // A→B
        adj[0].add(new int[]{2, 2});  // A→C
        adj[1].add(new int[]{2, 1});  // B→C
        adj[1].add(new int[]{3, 5});  // B→D
        adj[2].add(new int[]{1, 1});  // C→B
        adj[2].add(new int[]{3, 8});  // C→D
        adj[2].add(new int[]{4, 10}); // C→E
        adj[3].add(new int[]{4, 2});  // D→E
        adj[3].add(new int[]{5, 6});  // D→F
        adj[4].add(new int[]{5, 3});  // E→F

        int[] dist = dijkstra(adj, n, 0);
        String[] name = {"A", "B", "C", "D", "E", "F"};
        System.out.println("从 A 出发的最短距离：");
        for (int i = 0; i &lt; n; i++) {
            System.out.println("  A → " + name[i] + " = " + dist[i]);
        }
    }
}
</code></pre>
<hr />
<h3>4.3 如何记录最短路径本身</h3>
<p>上面的代码只输出了最短距离。若需要还原完整的最短路径，增加一个 <code>prev[]</code> 数组，在每次松弛成功时记录前驱顶点即可。</p>
<pre><code>int[] prev = new int[n];
Arrays.fill(prev, -1);

// 松弛时同步更新前驱
if (d[u] + w &lt; d[v]) {
    d[v] = d[u] + w;
    prev[v] = u;         // 记录：到达 v 的前一跳是 u
    pq.offer(new int[]{d[v], v});
}
</code></pre>
<p>还原路径（逆序追溯）：</p>
<pre><code>public static List&lt;Integer&gt; getPath(int[] prev, int target) {
    List&lt;Integer&gt; path = new ArrayList&lt;&gt;();
    for (int v = target; v != -1; v = prev[v]) {
        path.add(v);
    }
    Collections.reverse(path);
    return path;
}
</code></pre>
<hr />
<h2>五、复杂度分析</h2>
<p>设顶点数为 $V$，边数为 $E$。</p>
<table>
<thead>
<tr>
<th>实现方式</th>
<th>时间复杂度</th>
<th>空间复杂度</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>朴素实现（邻接矩阵）</td>
<td>$O(V^2)$</td>
<td>$O(V^2)$</td>
<td>稠密图</td>
</tr>
<tr>
<td>堆优化（邻接表 + 优先队列）</td>
<td>$O(E \log V)$</td>
<td>$O(V + E)$</td>
<td>稀疏图</td>
</tr>
</tbody>
</table>
<p><strong>朴素实现分析</strong>：</p>
<p>外层循环执行 $V$ 轮，每轮做两件事：</p>
<ol>
<li>线性扫描所有未敲定顶点，找出 $d$ 值最小者，耗时 $O(V)$。</li>
<li>遍历 $u$ 对应的邻接矩阵行，松弛所有出边，耗时 $O(V)$。</li>
</ol>
<p>因此：</p>
<p>$$
T = V \times (O(V) + O(V)) = V \times O(V) = O(V^2)
$$</p>
<p>稠密图中 $E \approx V^2$，此时 $O(V^2) = O(E)$。相比堆优化版的 $O(E \log V)$，矩阵版少一个 $\log V$ 因子，且常数更小，实际常常更快。</p>
<p><strong>堆优化分析</strong>：</p>
<p>每条边最多触发一次松弛，每次松弛将新记录压入优先队列，耗时 $O(\log V)$，因此松弛操作的总代价为：</p>
<p>$$
T_{\text{relax}} = O(E \log V)
$$</p>
<p>堆中可能存有同一顶点的多条过期记录，但总记录数不超过 $E$。每次弹出时若发现记录已过期则直接跳过，跳过操作的总次数不超过 $E$，单次弹出耗时 $O(\log E)$。</p>
<p>由于图中边数满足 $E \le V^2$，可得：</p>
<p>$$
E \le V^2 \implies \log E \le \log(V^2) = 2\log V
$$</p>
<p>从而在渐进意义下：</p>
<p>$$
O(\log E) = O(\log V)
$$</p>
<p>因此跳过操作的总代价为 $O(E \log V)$，与松弛操作同阶。综上，堆优化版的总时间复杂度为 $O(E \log V)$。</p>
<p>:::tip[与 Prim 算法的对比]
Dijkstra 与堆优化 Prim 的代码骨架几乎一致，区别在于：</p>
<ul>
<li><strong>Prim</strong>：松弛时比较的是<strong>边权</strong> $w(u, v)$，维护的是顶点与<strong>已有树</strong>的最近距离。</li>
<li><strong>Dijkstra</strong>：松弛时比较的是<strong>路径总长</strong> $d[u] + w(u, v)$，维护的是顶点与<strong>源点</strong>的最短距离。
:::</li>
</ul>
<p>:::tip[手算技巧]
手算 Dijkstra 时，画一张距离表逐轮更新即可：每轮圈出当前 $d$ 值最小的未敲定顶点，沿其出边更新邻居。顶点一旦敲定，最短距离就<strong>不会再变</strong>。若剩余未敲定顶点的 $d$ 值均为 $\infty$，说明它们从源点不可达，可以停笔。
:::</p>
<hr />
<h2>六、Dijkstra 与 Bellman-Ford 对比</h2>
<p>Dijkstra 并非在所有图上都适用，当图中含负权边时，需要使用 Bellman-Ford 算法。</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>Dijkstra</th>
<th>Bellman-Ford</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>负权边</strong></td>
<td>不支持（需非负权值）</td>
<td>支持</td>
</tr>
<tr>
<td><strong>负权环检测</strong></td>
<td>不支持</td>
<td>支持</td>
</tr>
<tr>
<td><strong>时间复杂度</strong></td>
<td>$O(E \log V)$（堆优化）</td>
<td>$O(V \cdot E)$</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>非负权图，追求速度</td>
<td>含负权边，或需要检测负环</td>
</tr>
<tr>
<td><strong>算法策略</strong></td>
<td>贪心，顶点逐一敲定</td>
<td>动态规划，反复松弛所有边</td>
</tr>
</tbody>
</table>
<hr />
<h2>七、总结</h2>
<p>Dijkstra 算法可以概括为：</p>
<blockquote>
<p><strong>维护一个距离估计数组 $d[]$，每次从未敲定的顶点中贪心地选出距源点最近的那个，将其敲定，然后沿其出边松弛邻居的距离估计值，如此往复直到所有顶点敲定为止。</strong></p>
</blockquote>
<p>要点梳理：</p>
<ul>
<li><strong>正确性依据</strong>：边权非负保证了贪心选择的局部最优即全局最优，已敲定顶点的距离不会再被缩短。</li>
<li><strong>两种实现</strong>：稠密图用邻接矩阵朴素实现（$O(V^2)$）；稀疏图用邻接表加最小堆（$O(E \log V)$），两者适用场景不同。</li>
<li><strong>路径还原</strong>：增加 <code>prev[]</code> 数组记录前驱，松弛时同步更新，最后逆序追溯即可。</li>
<li><strong>局限性</strong>：不支持负权边，遇到负权图需改用 Bellman-Ford 或 SPFA。</li>
</ul>
<p>口诀记忆：</p>
<blockquote>
<p>初始源点距离零，其余顶点无穷大；每轮选出最近点，沿边松弛更新它。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>Kruskal 最小生成树</title>
    <link href="https://blog.yoake.dev/posts/algorithm/kruskalmst/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/kruskalmst/</id>
    <published>2026-06-15T12:30:00.000Z</published>
    <updated>2026-06-15T12:30:00.000Z</updated>
    <summary>把边按权值升序排序，从小到大扫描；只要加入后不成环就保留，最终让森林合并成最小生成树。</summary>
    <content type="html"><![CDATA[<blockquote>
<p>本文假设你已经了解最小生成树和切边性质。如果不熟，建议先看 <a href="/posts/algorithm/primmst/">Prim 最小生成树</a> 的前几节。</p>
</blockquote>
<h2>一、Kruskal 的贪心思想</h2>
<p><a href="/posts/algorithm/primmst/">Prim 最小生成树</a> 算法是以一个顶点为种子向外蔓延，每一步都在树内与树外的分界线（跨切边）上选择最小权值的边。</p>
<p>Kruskal 算法则换了一个角度：<strong>不考虑分界线，而是把所有边按权值从小到大排序，然后依次扫描；每条边只要加入后不成环，就予以保留。</strong></p>
<p>在此过程中，图里会同时长出好几棵互不连通的小树（即森林），它们随着不断并入边而逐渐合并，最终合并为一棵覆盖所有顶点的最小生成树。</p>
<p><img src="https://img.picr2.online/1781525497551_trees.svg" alt="森林合并示意图" /></p>
<p>上图展示了森林合并的过程，虚线表示当前能够连接两棵不同小树且权值最小的候选边。Kruskal 算法就是反复将这种边并入，直至所有小树融为一体。</p>
<p>:::tip[算法策略对比]</p>
<ul>
<li><strong>Prim</strong>：从单个顶点开始，向外生长一棵树。</li>
<li><strong>Kruskal</strong>：从孤立顶点形成的森林开始，逐步合并为一棵树。
:::</li>
</ul>
<p>Kruskal 算法的骨架很简洁：</p>
<ol>
<li>将所有边按权值进行<strong>升序排序</strong>。</li>
<li>从最小边开始，依次遍历每条边 $(u, v)$：
<ul>
<li>如果 $u$ 和 $v$ <strong>已经在同一棵树中</strong>（加入该边会构成环路），则<strong>跳过</strong>；</li>
<li>否则<strong>保留</strong>该边，并将两棵树合并。</li>
</ul>
</li>
<li>当保留的边数达到 $|V| - 1$ 条时，算法结束。</li>
</ol>
<p>实现上述第 2 步的成环判断，最高效的数据结构就是并查集。需要说明的是，Kruskal 算法的正确性并非来自并查集，而是基于最小生成树的安全边性质（或称切边性质）；并查集在此只作为高效判环的辅助工具。</p>
<hr />
<h2>二、关键工具：并查集</h2>
<p>并查集是一种专门处理<strong>分组</strong>与<strong>合并</strong>的高效数据结构，在 Kruskal 算法中用来动态维护森林的连通分量。</p>
<p>它支持两项操作：</p>
<ul>
<li><strong><code>find(x)</code></strong>：查询元素 $x$ 属于哪个连通分量（返回所在组的代表元/根节点）。</li>
<li><strong><code>union(x, y)</code></strong>：将元素 $x$ 和 $y$ 所在的两个连通分量合并为一个。</li>
</ul>
<p>判断顶点 $u$ 和 $v$ 是否属于同一棵树，只需检查是否满足 <code>find(u) == find(v)</code>。</p>
<h3>2.1 基本结构与数组表示</h3>
<p>我们用数组 <code>parent[]</code> 来记录每个顶点的父亲节点，若顶点是其所在分量的根节点，则其父亲节点是其自身。</p>
<ul>
<li><strong>查询根节点</strong>：沿着父链一直向上爬，直到 <code>parent[x] == x</code> 为止。</li>
<li><strong>合并两个分量</strong>：分别找到 $x$ 和 $y$ 所在的根节点 <code>rx</code> 和 <code>ry</code>，将 <code>rx</code> 的父亲指向 <code>ry</code>（或者反过来）即可完成合并。</li>
</ul>
<p>若使用朴素查询，每次可能会遍历一条很长的链，时间复杂度最坏为 $O(N)$。并查集通过以下两个优化把均摊复杂度压到近似常数。</p>
<h3>2.2 路径压缩</h3>
<p>在递归向上寻找根节点的过程中，将沿途所有节点挂到根节点下，使整条长链扁平化。下次再查询这些节点时即可一步到位。</p>
<pre><code>public int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]); // 递归寻根，并将沿途节点挂到根下
    }
    return parent[x];
}
</code></pre>
<h3>2.3 按秩合并</h3>
<p>在合并两棵树时，总是让较矮的树挂载到较高的树下，以限制树高的增长。我们用 <code>rank[]</code> 记录每棵树的近似高度。</p>
<pre><code>public void union(int x, int y) {
    int rx = find(x);
    int ry = find(y);
    if (rx == ry) return; // 已经在同一分量，无需合并
    
    // 矮树挂载到高树下
    if (rank[rx] &lt; rank[ry]) {
        parent[rx] = ry;
    } else if (rank[rx] &gt; rank[ry]) {
        parent[ry] = rx;
    } else {
        parent[ry] = rx;
        rank[rx]++;
    }
}
</code></pre>
<p>:::note[均摊复杂度]
同时使用路径压缩和按秩合并后，对含有 $N$ 个元素的并查集进行 $M$ 次操作，总时间复杂度为 $O(M \cdot \alpha(N))$。其中 $\alpha$ 是反阿克曼函数，增长极慢，实际工程中常近似看作 $O(1)$。
:::</p>
<h3>2.4 代码实现</h3>
<pre><code>/**
 * 采用路径压缩与按秩合并优化的并查集实现。
 */
class UnionFind {
    private final int[] parent;
    private final int[] rank;
    private int count; // 当前连通分量的数量

    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        count = n;
        for (int i = 0; i &lt; n; i++) {
            parent[i] = i; // 初始状态下，每个顶点自成一个分量
            rank[i] = 0;
        }
    }

    /**
     * 查询节点 x 所在分量的根节点，顺带进行路径压缩。
     */
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    /**
     * 合并 x 和 y 所在的两个分量。
     * @return 若成功合并（原本不在同一分量，未成环）返回 true；若已在同一分量（会成环）则返回 false。
     */
    public boolean union(int x, int y) {
        int rx = find(x);
        int ry = find(y);
        if (rx == ry) {
            return false; // 已在同一分量，合并失败（说明加入此边会成环）
        }
        // 按秩合并：将矮树挂载到高树下
        if (rank[rx] &lt; rank[ry]) {
            parent[rx] = ry;
        } else if (rank[rx] &gt; rank[ry]) {
            parent[ry] = rx;
        } else {
            parent[ry] = rx;
            rank[rx]++;
        }
        count--; // 成功连通，分量总数减 1
        return true;
    }

    public boolean connected(int x, int y) {
        return find(x) == find(y);
    }

    public int count() {
        return count;
    }
}
</code></pre>
<p>有了并查集，Kruskal 的成环判断就可以近似看作 $O(1)$。</p>
<hr />
<h2>三、算法执行步骤图解</h2>
<p>我们复用 <a href="/posts/algorithm/primmst/">Prim 最小生成树</a> 中使用过的 6 顶点带权无向图进行演示：</p>
<p><img src="https://img.picr2.online/1781524241169____.svg" alt="算法图示" /></p>
<p>将图中的所有边按权值升序进行排列：</p>
<table>
<thead>
<tr>
<th>序号</th>
<th>边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>D-E</code></td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td><code>E-F</code></td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td><code>B-C</code></td>
<td>3</td>
</tr>
<tr>
<td>4</td>
<td><code>C-E</code></td>
<td>3</td>
</tr>
<tr>
<td>5</td>
<td><code>C-D</code></td>
<td>4</td>
</tr>
<tr>
<td>6</td>
<td><code>D-F</code></td>
<td>5</td>
</tr>
<tr>
<td>7</td>
<td><code>B-D</code></td>
<td>6</td>
</tr>
<tr>
<td>8</td>
<td><code>A-B</code></td>
<td>7</td>
</tr>
<tr>
<td>9</td>
<td><code>A-C</code></td>
<td>8</td>
</tr>
</tbody>
</table>
<p>扫描和处理边的真实权值顺序如下：</p>
<p>$$
\text{D-E}(2),\ \text{E-F}(2),\ \text{B-C}(3),\ \text{C-E}(3),\ \text{C-D}(4),\ \text{D-F}(5),\ \text{B-D}(6),\ \text{A-B}(7),\ \text{A-C}(8)
$$</p>
<h3>初始状态</h3>
<p>所有顶点各成独立分量，已选边数为 0。</p>
<p><img src="https://img.picr2.online/1781525237940_0.svg" alt="步骤 0 初始状态" /></p>
<h3>第 1 步：尝试并入 <code>D-E (2)</code></h3>
<p>顶点 $D$ 和 $E$ 属于不同分量，予以<strong>保留</strong>，合并为连通分量 ${D, E}$。</p>
<blockquote>
<p>森林状态：<code>{D, E}</code>，<code>{A}</code>，<code>{B}</code>，<code>{C}</code>，<code>{F}</code>。已选 1 条边。</p>
</blockquote>
<p><img src="https://img.picr2.online/1781525237926_1.svg" alt="步骤 1 保留 D-E" /></p>
<h3>第 2 步：尝试并入 <code>E-F (2)</code></h3>
<p>$E \in {D, E}$，而 $F$ 处于独立分量，予以<strong>保留</strong>，合并为连通分量 ${D, E, F}$。</p>
<blockquote>
<p>森林状态：<code>{D, E, F}</code>，<code>{A}</code>，<code>{B}</code>，<code>{C}</code>。已选 2 条边。</p>
</blockquote>
<p><img src="https://img.picr2.online/1781525238636_2.svg" alt="步骤 2 保留 E-F" /></p>
<h3>第 3 步：尝试并入 <code>B-C (3)</code></h3>
<p>$B$ 和 $C$ 处于不同分量，予以<strong>保留</strong>，合并为连通分量 ${B, C}$。</p>
<blockquote>
<p>森林状态：<code>{B, C}</code>，<code>{D, E, F}</code>，<code>{A}</code>。已选 3 条边。</p>
</blockquote>
<p><img src="https://img.picr2.online/1781525238777_3.svg" alt="步骤 3 保留 B-C" /></p>
<h3>第 4 步：尝试并入 <code>C-E (3)</code></h3>
<p>$C \in {B, C}$，$E \in {D, E, F}$，分属两个不同的树，予以<strong>保留</strong>，将两棵树合并。</p>
<blockquote>
<p>森林状态：<code>{B, C, D, E, F}</code>，<code>{A}</code>。已选 4 条边。</p>
</blockquote>
<p><img src="https://img.picr2.online/1781525238789_4.svg" alt="步骤 4 保留 C-E" /></p>
<h3>第 5 步：尝试并入 <code>C-D (4)</code></h3>
<p>$C$ 和 $D$ 已属于同一连通分量，说明当前生成森林中已经存在一条从 $C$ 到 $D$ 的路径。若再加入边 <code>C-D</code>，则会形成回路，因此<strong>跳过</strong>。</p>
<h3>第 6 步：尝试并入 <code>D-F (5)</code></h3>
<p>$D$ 和 $F$ 均在同一分量中，<strong>跳过</strong>。</p>
<h3>第 7 步：尝试并入 <code>B-D (6)</code></h3>
<p>$B$ 和 $D$ 均在同一分量中，<strong>跳过</strong>。</p>
<h3>第 8 步：尝试并入 <code>A-B (7)</code></h3>
<p>$A$ 处于独立分量，$B$ 属于连通分量，分属不同分量，予以<strong>保留</strong>，并入生成树中。</p>
<blockquote>
<p>森林状态：<code>{A, B, C, D, E, F}</code>。已选 5 条边。</p>
</blockquote>
<p>已保留边数达到 $|V| - 1 = 5$ 条，算法结束。</p>
<p>:::tip[手算提速技巧]
对于含有 $n$ 个顶点的图：</p>
<ul>
<li>一旦在草稿纸上<strong>选出了 $n - 1$ 条边</strong>，即可<strong>立即宣布算法结束</strong>。</li>
<li>后续排在后面的未扫描边不必再分析是否成环，这能节省手算时间。
:::</li>
</ul>
<h3>最终结果</h3>
<p><img src="https://img.picr2.online/1781525239208_5.svg" alt="最小生成树结果" /></p>
<p>最小生成树的总权值为：</p>
<p>$$
\begin{aligned}
W(T) &amp;= 2 + 2 + 3 + 3 + 7 \
&amp;= 17
\end{aligned}
$$</p>
<hr />
<h2>四、Java 代码实现</h2>
<p>下面提供一个包含并查集与测试入口的一站式、可直接编译运行的 Java 完整实现。</p>
<h3>4.1 完整 Java 代码</h3>
<pre><code>import java.util.*;

public class KruskalMST {

    /**
     * 代表图中的一条带权无向边。
     */
    public static class Edge implements Comparable&lt;Edge&gt; {
        int from;
        int to;
        int weight;

        public Edge(int from, int to, int weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge other) {
            return Integer.compare(this.weight, other.weight);
        }

        @Override
        public String toString() {
            return "(" + from + "-" + to + ", w=" + weight + ")";
        }
    }

    /**
     * 采用路径压缩与按秩合并优化的并查集。
     */
    public static class UnionFind {
        private final int[] parent;
        private final int[] rank;

        public UnionFind(int n) {
            parent = new int[n];
            rank = new int[n];
            for (int i = 0; i &lt; n; i++) {
                parent[i] = i;
                rank[i] = 0;
            }
        }

        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]); // 路径压缩
            }
            return parent[x];
        }

        public boolean union(int x, int y) {
            int rx = find(x);
            int ry = find(y);
            if (rx == ry) {
                return false; // 已在同一连通分量，说明加入该边会构成环路
            }
            // 按秩合并：将矮树挂载到高树下
            if (rank[rx] &lt; rank[ry]) {
                parent[rx] = ry;
            } else if (rank[rx] &gt; rank[ry]) {
                parent[ry] = rx;
            } else {
                parent[ry] = rx;
                rank[rx]++;
            }
            return true;
        }
    }

    /**
     * Kruskal 最小生成树算法的主实现。
     * @param n 顶点个数（顶点编号为 0 到 n-1）
     * @param edges 所有的无向边列表
     * @return 构成最小生成树的边列表。若图不连通，则返回最小生成森林。
     */
    public static List&lt;Edge&gt; kruskal(int n, List&lt;Edge&gt; edges) {
        // 1. 将所有边按权值升序排序
        List&lt;Edge&gt; sorted = new ArrayList&lt;&gt;(edges);
        Collections.sort(sorted);

        // 2. 初始化并查集
        UnionFind uf = new UnionFind(n);
        List&lt;Edge&gt; mst = new ArrayList&lt;&gt;();

        for (Edge e : sorted) {
            // 3. 判断是否同分量，不同分量则保留该边并合并
            if (uf.union(e.from, e.to)) {
                mst.add(e);
                if (mst.size() == n - 1) {
                    break; // 已成功找出 n-1 条边，提前结束
                }
            }
        }
        return mst;
    }

    public static void main(String[] args) {
        int n = 6;
        List&lt;Edge&gt; edges = Arrays.asList(
                new Edge(0, 1, 7),   // A-B
                new Edge(0, 2, 8),   // A-C
                new Edge(1, 2, 3),   // B-C
                new Edge(1, 3, 6),   // B-D
                new Edge(2, 3, 4),   // C-D
                new Edge(2, 4, 3),   // C-E
                new Edge(3, 4, 2),   // D-E
                new Edge(3, 5, 5),   // D-F
                new Edge(4, 5, 2)    // E-F
        );

        List&lt;Edge&gt; mst = kruskal(n, edges);

        System.out.println("Kruskal MST 选中的边：");
        int totalWeight = 0;
        for (Edge e : mst) {
            System.out.println("  " + e);
            totalWeight += e.weight;
        }
        System.out.println("总权值：" + totalWeight);
    }
}
</code></pre>
<p>运行结果输出：</p>
<pre><code>Kruskal MST 选中的边：
  (3-4, w=2)
  (4-5, w=2)
  (1-2, w=3)
  (2-4, w=3)
  (0-1, w=7)
总权值：17
</code></pre>
<p>输出结果与第三节的手算过程一致。</p>
<hr />
<h2>五、复杂度分析</h2>
<p>设图的顶点数为 $V$，边数为 $E$。</p>
<ul>
<li>
<p><strong>时间复杂度</strong>：</p>
<ul>
<li><strong>排序开销</strong>：将所有边按权值排序，耗时 $O(E \log E)$，这是算法的瓶颈。</li>
<li><strong>并查集操作</strong>：最坏情况下对所有边进行 $E$ 次合并与查询，均摊时间复杂度为 $O(E \cdot \alpha(V))$。由于 $\alpha(V)$ 增长极慢，这部分通常远小于排序开销。</li>
<li><strong>总时间复杂度</strong>：由排序决定，为 $O(E \log E)$，在连通无向图中，边数 $E \le V^2$，也可写为 $O(E \log V)$。</li>
</ul>
</li>
<li>
<p><strong>空间复杂度</strong>：</p>
<ul>
<li>主要开销在于并查集占用的 $O(V)$ 辅助空间，以及对边进行排序的辅助空间 $O(E)$。总空间复杂度为 $O(V + E)$。</li>
</ul>
</li>
</ul>
<p>:::tip[复杂度对比]
Kruskal 算法与堆优化的 Prim 算法的理论时间复杂度均为 $O(E \log V)$。
:::</p>
<hr />
<h2>六、Kruskal 与 Prim 算法对比</h2>
<p>两者的构造风格互为补充：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>Kruskal 算法</th>
<th>Prim 算法</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>基本思路</strong></td>
<td>选边：从空边开始，每次选取全局权值最小的边</td>
<td>选点：从任意起点出发，每次选取跨切边的最小边</td>
</tr>
<tr>
<td><strong>生成结构</strong></td>
<td>长出森林，逐步融合</td>
<td>整棵树从单个顶点不断扩大</td>
</tr>
<tr>
<td><strong>主要数据结构</strong></td>
<td>并查集</td>
<td>最小堆</td>
</tr>
<tr>
<td><strong>边排序要求</strong></td>
<td>需要对所有边预先升序排序</td>
<td>不需要预先排序，通过堆动态寻找最小边</td>
</tr>
<tr>
<td><strong>不连通图处理</strong></td>
<td>运行结束天然得到最小生成森林</td>
<td>需显式遍历每个未访问分量分别执行</td>
</tr>
<tr>
<td><strong>适合的图类型</strong></td>
<td>稀疏图</td>
<td>稠密图</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Kruskal 适合稀疏图</strong>：其时间复杂度由排序主导，当边数 $E$ 较少时，排序开销较低。</li>
<li><strong>Prim 适合稠密图</strong>：堆优化版本不依赖对全局边的预先排序，但在稠密图上，使用邻接矩阵实现的 $O(V^2)$ 版本性能比 Kruskal 更好。</li>
</ul>
<hr />
<h2>七、总结</h2>
<p>Kruskal 算法可以概括为：</p>
<blockquote>
<p><strong>全局边按权值升序排列，依次扫描并入；借助并查集快速判环，将孤立森林逐步融合成树。</strong></p>
</blockquote>
<p>要点梳理：</p>
<ul>
<li><strong>贪心选择</strong>：优先选取当前全局权值最小的边，并在成环时直接跳过。</li>
<li><strong>数据结构支持</strong>：并查集高效处理成环判定，其中路径压缩与按秩合并缺一不可。</li>
<li><strong>时空开销</strong>：时间由边排序决定，为 $O(E \log E)$，空间开销为 $O(V + E)$。</li>
<li><strong>选型依据</strong>：在边稀疏的图上首选 Kruskal，在稠密图上首选 Prim。</li>
</ul>
<p>口诀记忆：</p>
<blockquote>
<p>边按权值排好序，从小到大依次取；并查集判成不成环，不成环就合进去。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>Prim 最小生成树</title>
    <link href="https://blog.yoake.dev/posts/algorithm/primmst/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/primmst/</id>
    <published>2026-06-15T12:02:01.000Z</published>
    <updated>2026-06-15T12:02:01.000Z</updated>
    <summary>从任意起点出发，每一步选择权值最小的跨切边，把树外顶点并入树内，逐步构造最小生成树。</summary>
    <content type="html"><![CDATA[<h2>一、从实际问题说起</h2>
<p>假设有几个城市要用光缆连通。城市之间有些能拉线、有些拉不了，每条线成本不同。目标是：</p>
<blockquote>
<p>用<strong>总成本最低</strong>的方案，把所有城市连成一张网，让任意两个城市都能互通。</p>
</blockquote>
<p>把城市看成图的顶点，光缆看成边，成本看成边权，这个问题就是经典的<strong>最小生成树</strong>问题。</p>
<p>先看一个包含 4 个城市的简化例子：</p>
<pre><code>      4
   A-----C
   |    /|
 1 |  2/ |3
   | /   |
   B-----D
      5
</code></pre>
<p>如果我们要让所有城市连通且修路总长度最短，最佳的连线方案是选择 <code>A-B</code> (1)、<code>B-C</code> (2) 和 <code>C-D</code> (3) 三条边，总代价为 $1 + 2 + 3 = 6$。这三条边所连接的结构，就是一棵<strong>最小生成树</strong>。</p>
<p>Prim 算法是求解最小生成树的两种经典贪心算法之一（另一种是 Kruskal）。它从一个起点出发，让树逐步长大，每一步都挑当前最便宜的连接方式。</p>
<hr />
<h2>二、生成树与最小生成树</h2>
<p>给定一个<strong>连通无向图</strong> $G = (V, E)$，它的<strong>生成树</strong>是满足以下三个条件的一个边子集 $T$：</p>
<ol>
<li><strong>包含所有顶点</strong>：$T$ 连接了 $V$ 里的每一个顶点；</li>
<li><strong>无环</strong>：$T$ 里没有回路；</li>
<li><strong>连通</strong>：任意两个顶点之间都有路径。</li>
</ol>
<p>这三条加起来，决定了生成树的边数是固定的：</p>
<p>$$
|T| = |V| - 1
$$</p>
<p>直觉上：从空开始，每加一条边把两个不连通的部分连起来，正好需要 $|V| - 1$ 条边才能把 $|V|$ 个顶点连成一个整体；再多一条就会成环。</p>
<p>:::note[边数固定是关键]
无论哪棵生成树，边数都是 $|V| - 1$。所以"最小化总成本"和"最小化边数"是两回事——边数已经固定，我们最小化的是<strong>边权之和</strong>。
:::</p>
<p><strong>最小生成树</strong>就是所有生成树里，边权之和最小的那棵：</p>
<p>$$
W(T) = \sum_{e \in T} w(e)
$$</p>
<p>让 $W(T)$ 最小的 $T$ 就是最小生成树。</p>
<p>:::warning[前提：图连通]
只有连通图才有生成树。如果图不连通，最多只能得到"最小生成森林"（每个连通分量各一棵最小生成树）。本文假设图连通。
:::</p>
<hr />
<h2>三、Prim 算法的贪心策略</h2>
<p>Prim 的思路是：<strong>让一棵树从单个顶点开始，一步步长出来。</strong></p>
<p>把顶点分成两组：</p>
<ul>
<li><strong>树内</strong>：已经并入生成树的顶点（记作集合 $S$）；</li>
<li><strong>树外</strong>：还没并入的顶点（记作集合 $V - S$）。</li>
</ul>
<p>任何时候，连接"树内"和"树外"的边，都叫做<strong>跨切边</strong>——它们正好横跨了 $S$ 和 $V - S$ 之间的分界线。</p>
<p>Prim 的贪心策略：</p>
<blockquote>
<p><strong>每一步，在所有跨切边里挑权值最小的那条，把它加进生成树；它连向的那个树外顶点也就并入了树内。</strong></p>
</blockquote>
<p>重复 $|V| - 1$ 次，树就长全了。</p>
<p><img src="https://img.picr2.online/1781524368432_graphviz.svg" alt="树内树外分割与跨切边" /></p>
<p>上图中，跨越分界线的候选跨切边有两条：<code>A-D</code>（权值 2）与 <code>C-E</code>（权值 5）。因为 <code>A-D</code> 的权值更小，它被判定为<strong>最小跨切边</strong>，在这一步被选中并入树中，顶点 <code>D</code> 也随之并入树内。</p>
<p>:::tip[一句话记忆]
Prim 就是："<strong>已经连好的城市是一团，每次从团外挑一个最便宜的城市接进来</strong>。"
:::</p>
<p>:::note[贪心选择性质]
对于一个图，所有可能的生成树构成了该问题的<strong>解空间</strong>。Prim 算法不需要搜索或回溯整个解空间，而是仅根据当前的“局部最优”（每次在所有跨切边中选择最便宜的那条）进行一步步的选择。这种不考虑后续影响、只看当前最优的决策，正是贪心选择性质的体现。
:::</p>
<hr />
<h2>四、算法执行步骤图解</h2>
<p>来看一个具体的图。6 个顶点，边和权值如下：</p>
<p><img src="https://img.picr2.online/1781524241169____.svg" alt="算法图示" /></p>
<p>从顶点 $A$ 开始跑 Prim。</p>
<h3>初始状态</h3>
<p>树内 $S = {A}$，已选边数 $0$。</p>
<p><img src="https://img.picr2.online/1781524516147_0.svg" alt="初始状态" /></p>
<h3>第 1 步</h3>
<p>跨切边（从 $A$ 出发的边）：</p>
<table>
<thead>
<tr>
<th>跨切边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>A-B</code></td>
<td>7</td>
</tr>
<tr>
<td><code>A-C</code></td>
<td>8</td>
</tr>
</tbody>
</table>
<p>其中权值最小的是 <code>A-B</code> (7)，我们选择它，将顶点 $B$ 并入树中。</p>
<blockquote>
<p>$S = {A, B}$，已选：<code>A-B</code></p>
</blockquote>
<p><img src="https://img.picr2.online/1781524521015_1.svg" alt="第 1 步" /></p>
<h3>第 2 步</h3>
<p>跨切边（从 $A$ 或 $B$ 出发，到树外）：</p>
<table>
<thead>
<tr>
<th>跨切边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>A-C</code></td>
<td>8</td>
</tr>
<tr>
<td><code>B-C</code></td>
<td>3</td>
</tr>
<tr>
<td><code>B-D</code></td>
<td>6</td>
</tr>
</tbody>
</table>
<p>其中权值最小的跨切边是 <code>B-C</code> (3)，选择它，将顶点 $C$ 并入树中。</p>
<blockquote>
<p>$S = {A, B, C}$，已选：<code>A-B</code>，<code>B-C</code></p>
</blockquote>
<p><img src="https://img.picr2.online/1781524521259_2.svg" alt="第 2 步" /></p>
<h3>第 3 步</h3>
<p>跨切边：</p>
<table>
<thead>
<tr>
<th>跨切边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>A-C</code></td>
<td>8（两端已入树）</td>
</tr>
<tr>
<td><code>B-D</code></td>
<td>6</td>
</tr>
<tr>
<td><code>C-D</code></td>
<td>4</td>
</tr>
<tr>
<td><code>C-E</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>:::warning[小心废边]
<code>A-C</code> 这条边的两端 $A$、$C$ 现在都在树内，已经不再是跨切边了。实现时遇到这种"两端都在 $S$ 里"的边要跳过，否则会引入环。
:::</p>
<p>其中最小跨切边为 <code>C-E</code> (3)，选择它，将顶点 $E$ 并入树中。</p>
<blockquote>
<p>$S = {A, B, C, E}$，已选：<code>A-B</code>，<code>B-C</code>，<code>C-E</code></p>
</blockquote>
<p><img src="https://img.picr2.online/1781524521284_3.svg" alt="第 3 步" /></p>
<h3>第 4 步</h3>
<p>跨切边：</p>
<table>
<thead>
<tr>
<th>跨切边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>B-D</code></td>
<td>6</td>
</tr>
<tr>
<td><code>C-D</code></td>
<td>4</td>
</tr>
<tr>
<td><code>E-D</code></td>
<td>2</td>
</tr>
<tr>
<td><code>E-F</code></td>
<td>2</td>
</tr>
</tbody>
</table>
<p>其中权值最小的是 <code>E-D</code> (2)，同等最小的还有 <code>E-F</code> (2)。任选其一都能保持最优性，但生成树结构可能不同。这里选择 <code>E-D</code>，将顶点 $D$ 并入树中。</p>
<blockquote>
<p>$S = {A, B, C, E, D}$，已选：<code>A-B</code>，<code>B-C</code>，<code>C-E</code>，<code>E-D</code></p>
</blockquote>
<p><img src="https://img.picr2.online/1781524522229_4.svg" alt="第 4 步" /></p>
<h3>第 5 步</h3>
<p>跨切边：</p>
<table>
<thead>
<tr>
<th>跨切边</th>
<th>权值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>D-F</code></td>
<td>5</td>
</tr>
<tr>
<td><code>E-F</code></td>
<td>2</td>
</tr>
</tbody>
</table>
<p>其中最小跨切边为 <code>E-F</code> (2)，选择它，将顶点 $F$ 并入树中。</p>
<blockquote>
<p>$S = {A, B, C, E, D, F}$，已选：<code>A-B</code>，<code>B-C</code>，<code>C-E</code>，<code>E-D</code>，<code>E-F</code></p>
</blockquote>
<p>所有顶点都在树内，已选 $|V| - 1 = 5$ 条边，算法结束。</p>
<h3>最终结果</h3>
<p><img src="https://img.picr2.online/1781524240270________.svg" alt="最小生成树结果" /></p>
<p>总权值：</p>
<p>$$
\begin{aligned}
W(T) &amp;= 7 + 3 + 3 + 2 + 2 \
&amp;= 17
\end{aligned}
$$</p>
<hr />
<h2>五、Java 代码实现</h2>
<p>一种容易理解的写法是<strong>懒 Prim</strong>：用<strong>最小堆</strong>存放候选边，每次从堆顶取最小边。</p>
<p>基本思路：</p>
<ol>
<li>任选一个起点，把它入树；把它的所有邻接边入堆。</li>
<li>循环：从堆顶弹出权值最小的边。
<ul>
<li>如果这条边<strong>两端都在树内</strong>（废边），跳过；</li>
<li>否则选它：把那个还没入树的顶点并入树，并把它的邻接边入堆。</li>
</ul>
</li>
<li>重复直到树里有 $|V|$ 个顶点。</li>
</ol>
<p>“懒”字指废边不立刻清理，而是在弹出时丢弃，因此代码更简洁。</p>
<h3>5.1 边与图</h3>
<pre><code>import java.util.*;

/** 一条带权边。Comparable 让 PriorityQueue 默认按权值排序。 */
class Edge implements Comparable&lt;Edge&gt; {
    int from, to, weight;

    Edge(int from, int to, int weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }

    @Override
    public int compareTo(Edge other) {
        return Integer.compare(this.weight, other.weight);
    }

    @Override
    public String toString() {
        return "(" + from + "-" + to + ", w=" + weight + ")";
    }
}
</code></pre>
<p>图用邻接表表示，每条无向边存两条有向边。</p>
<h3>5.2 Prim 算法</h3>
<pre><code>public class PrimMST {

    private final List&lt;List&lt;Edge&gt;&gt; adj;   // 邻接表
    private final int n;                  // 顶点数

    public PrimMST(int n) {
        this.n = n;
        this.adj = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; n; i++) {
            adj.add(new ArrayList&lt;&gt;());
        }
    }

    /** 添加一条无向边。 */
    public void addEdge(int u, int v, int w) {
        adj.get(u).add(new Edge(u, v, w));
        adj.get(v).add(new Edge(v, u, w));
    }

    /**
     * 从顶点 start 开始跑 Prim，返回 MST 的边集合。
     * 如果图不连通，返回的是最小生成森林。
     */
    public List&lt;Edge&gt; prim(int start) {
        List&lt;Edge&gt; mst = new ArrayList&lt;&gt;();          // 结果：MST 的边
        boolean[] inTree = new boolean[n];           // 顶点是否已在树内
        PriorityQueue&lt;Edge&gt; heap = new PriorityQueue&lt;&gt;();  // 跨切边最小堆

        // 1. 起点入树，它的邻接边全部入堆
        inTree[start] = true;
        visit(start, heap);

        // 2. 只要 MST 还没长全，就继续
        while (!heap.isEmpty() &amp;&amp; mst.size() &lt; n - 1) {
            Edge e = heap.poll();                    // 当前最小跨切边

            // 懒处理：两端都在树内 = 废边，跳过
            if (inTree[e.from] &amp;&amp; inTree[e.to]) {
                continue;
            }

            // 由于前面已经排除了“两端都在树内”的情况，此时这条边必定是一端在树内、一端在树外。
            // 找出那个尚未加入生成树的顶点 w，将其并入树中。
            int w = inTree[e.from] ? e.to : e.from;
            inTree[w] = true;
            mst.add(e);
            visit(w, heap);                                // 新顶点的邻接边入堆
        }

        return mst;
    }

    /** 把顶点 v 的所有邻接边加入堆。 */
    private void visit(int v, PriorityQueue&lt;Edge&gt; heap) {
        for (Edge e : adj.get(v)) {
            // 将邻接边入堆
            heap.offer(e);
        }
    }
}
</code></pre>
<p>:::tip[visit 里为什么不过滤废边]
<code>visit</code> 会把邻接边全部放进堆，这是"懒"的体现。好处是代码简单；代价是堆里会积累一些两端都已入树的废边，弹出时再丢弃。对稠密图这会增加常数，但很适合理解算法原理。
:::</p>
<h3>5.3 完整运行</h3>
<pre><code>public static void main(String[] args) {
    PrimMST g = new PrimMST(6);
    // 第 4 节示例的图
    g.addEdge(0, 1, 7);   // A-B
    g.addEdge(0, 2, 8);   // A-C
    g.addEdge(1, 2, 3);   // B-C
    g.addEdge(1, 3, 6);   // B-D
    g.addEdge(2, 3, 4);   // C-D
    g.addEdge(2, 4, 3);   // C-E
    g.addEdge(3, 4, 2);   // D-E
    g.addEdge(3, 5, 5);   // D-F
    g.addEdge(4, 5, 2);   // E-F

    List&lt;Edge&gt; mst = g.prim(0);   // 从 A 开始

    System.out.println("MST 的边：");
    int total = 0;
    for (Edge e : mst) {
        System.out.println("  " + e);
        total += e.weight;
    }
    System.out.println("总权值：" + total);
}
</code></pre>
<p>运行结果：</p>
<pre><code>MST 的边：
  (0-1, w=7)
  (1-2, w=3)
  (2-4, w=3)
  (4-3, w=2)
  (4-5, w=2)
总权值：17
</code></pre>
<p>结果与第 4 节手算一致。</p>
<hr />
<h2>六、复杂度分析</h2>
<p>设顶点数为 $V$，边数为 $E$。</p>
<ul>
<li>
<p><strong>懒 Prim</strong>：每条边最多入堆和弹出一次，堆操作为 $O(\log E)$，总时间复杂度为 $O(E \log E)$，空间复杂度为 $O(E)$。</p>
</li>
<li>
<p><strong>即时 Prim</strong>：对每个树外顶点只保留连向树内的最短边，能把堆大小压到 $O(V)$，时间复杂度优化到 $O(E \log V)$。</p>
</li>
</ul>
<p>:::note[选哪个]
稀疏图：懒 Prim 足够，代码简单。
稠密图：考虑即时 Prim，或使用邻接矩阵的 $O(V^2)$ 版本。
:::</p>
<hr />
<h2>七、Prim 与 Kruskal 算法对比</h2>
<p>两种算法都基于切边性质、都能得到最小生成树，但风格不同：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>Prim</th>
<th>Kruskal</th>
</tr>
</thead>
<tbody>
<tr>
<td>增长方式</td>
<td>从一个点开始，长一棵树</td>
<td>从空开始，长森林，逐步合并</td>
</tr>
<tr>
<td>选边依据</td>
<td>跨切边的最小权</td>
<td>全局未选边的最小权（前提是不成环）</td>
</tr>
<tr>
<td>关键数据结构</td>
<td>最小堆</td>
<td>并查集</td>
</tr>
<tr>
<td>典型复杂度</td>
<td>$O(E \log V)$</td>
<td>$O(E \log E)$</td>
</tr>
<tr>
<td>适合的图</td>
<td>稠密图</td>
<td>稀疏图</td>
</tr>
</tbody>
</table>
<p>记忆要点：</p>
<ul>
<li><strong>Prim</strong> 像“种子向外蔓延”，每步只关心分界线上的跨切边；</li>
<li><strong>Kruskal</strong> 像“按边权从小到大贴胶带”，只要不成环就加入。</li>
</ul>
<p>记忆口诀：</p>
<blockquote>
<p><strong>Prim 选点，Kruskal 选边。</strong></p>
</blockquote>
<p>:::tip[怎么选]
如果图是稠密的，Prim 更划算；如果是稀疏的，Kruskal 更直观。虽然得到的最小总权值是确定的，但最小生成树本身未必唯一。
:::</p>
<hr />
<h2>八、教材版 Prim</h2>
<p>在《算法设计与分析》或《数据结构》考试中，常见的是基于邻接矩阵的 $O(V^2)$ 版本。该版本不依赖 <code>PriorityQueue</code>，而是通过两个辅助数组维护树外节点到树内的最短距离：</p>
<ul>
<li><strong><code>lowcost[i]</code></strong>：表示当前已建好的生成树到树外顶点 <code>i</code> 的最小连接代价（边权）。如果顶点 <code>i</code> 已经加入生成树，则 <code>lowcost[i] = 0</code>。</li>
<li><strong><code>closest[i]</code></strong>：表示使顶点 <code>i</code> 获得最小连接代价的树内顶点。</li>
</ul>
<h3>8.1 算法思路</h3>
<ol>
<li>初始化所有顶点的 <code>lowcost[i]</code> 为从起点到顶点 <code>i</code> 的边权，<code>closest[i]</code> 为起点。起点本身的 <code>lowcost[start] = 0</code>。</li>
<li>循环 $V - 1$ 次（每次并入一个新节点）：
<ul>
<li>遍历所有树外顶点，找到一个使得 <code>lowcost</code> 最小的顶点 <code>u</code>。</li>
<li>将 <code>u</code> 并入树内（设置 <code>lowcost[u] = 0</code>）。</li>
<li>遍历 <code>u</code> 的所有树外邻接顶点 <code>v</code>，如果边 <code>(u, v)</code> 的权值小于当前的 <code>lowcost[v]</code>，则更新 <code>lowcost[v] = weight(u, v)</code>，并令 <code>closest[v] = u</code>。</li>
</ul>
</li>
</ol>
<h3>8.2 代码实现</h3>
<pre><code>public class MatrixPrim {
    private static final int INF = Integer.MAX_VALUE;

    /**
     * 基于邻接矩阵的 Prim 算法实现。
     * @param graph 邻接矩阵，graph[i][j] 表示顶点 i 到 j 的权值，不连通为 INF
     * @param n 顶点个数
     * @param start 起点
     */
    public static void prim(int[][] graph, int n, int start) {
        int[] lowcost = new int[n];
        int[] closest = new int[n];
        boolean[] inTree = new boolean[n];

        // 初始化
        for (int i = 0; i &lt; n; i++) {
            lowcost[i] = graph[start][i];
            closest[i] = start;
        }
        inTree[start] = true;

        int totalWeight = 0;
        System.out.println("教材版 Prim MST 的边：");

        // 寻找剩余的 n - 1 个顶点
        for (int step = 1; step &lt; n; step++) {
            int minCost = INF;
            int u = -1;

            // 1. 寻找当前连接代价最小的树外顶点
            for (int i = 0; i &lt; n; i++) {
                if (!inTree[i] &amp;&amp; lowcost[i] &lt; minCost) {
                    minCost = lowcost[i];
                    u = i;
                }
            }

            if (u == -1) {
                System.out.println("图不连通，无法构造完整的 MST");
                return;
            }

            // 2. 并入树内并输出边
            inTree[u] = true;
            totalWeight += minCost;
            System.out.println("  (" + closest[u] + "-" + u + ", w=" + minCost + ")");

            // 3. 更新剩余树外顶点的最小连接代价
            for (int v = 0; v &lt; n; v++) {
                if (!inTree[v] &amp;&amp; graph[u][v] &lt; lowcost[v]) {
                    lowcost[v] = graph[u][v];
                    closest[v] = u;
                }
            }
        }
        System.out.println("总权值：" + totalWeight);
    }
}
</code></pre>
<h3>8.3 复杂度与适用场景</h3>
<ul>
<li><strong>时间复杂度</strong>：$O(V^2)$。外层循环执行 $V$ 次，内部寻找最小值和更新代价的循环均为 $O(V)$。</li>
<li><strong>空间复杂度</strong>：$O(V)$（仅需一维数组存储 <code>lowcost</code> 和 <code>closest</code>）。</li>
<li><strong>适用场景</strong>：该版本适合处理<strong>稠密图</strong>（$E$ 接近 $V^2$）。在此场景下，它常比堆优化版本更快，实现也更简单。</li>
</ul>
<hr />
<h2>九、总结</h2>
<p>Prim 最小生成树可以概括为：</p>
<blockquote>
<p><strong>从任意起点开始，每一步在跨切边里挑权值最小的那条加入，让树逐步长大，直到覆盖所有顶点。</strong></p>
</blockquote>
<p>要点回顾：</p>
<ul>
<li><strong>生成树</strong>有 $|V| - 1$ 条边、连通、无环；<strong>最小生成树</strong>是边权之和最小的那棵。</li>
<li><strong>Prim 的贪心</strong>：每步选当前已选顶点与未选顶点之间的最小跨切边。</li>
<li><strong>正确性</strong>来自<strong>切边性质</strong>：任意切的最小跨切边必属于某棵最小生成树。</li>
<li><strong>实现</strong>用最小堆存放跨切边，弹出废边时跳过（懒 Prim）。</li>
<li><strong>复杂度</strong>：懒 Prim $O(E \log E)$，即时 Prim $O(E \log V)$。</li>
<li>与 <strong>Kruskal</strong> 互补：Prim 适合稠密图，Kruskal 适合稀疏图。</li>
</ul>
<p>口诀：</p>
<blockquote>
<p>一点起步向外延，跨切小边优先连；堆存候选勤更新，长满即是最优树。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>贪心算法：哈夫曼编码</title>
    <link href="https://blog.yoake.dev/posts/algorithm/huffmancoding/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/huffmancoding/</id>
    <published>2026-06-15T11:14:33.000Z</published>
    <updated>2026-06-15T11:14:33.000Z</updated>
    <summary>用贪心思想构造哈夫曼树：每次合并频率最小的两个节点，得到平均码长最短的前缀码，实现数据压缩。</summary>
    <content type="html"><![CDATA[<h2>一、从等长编码到变长编码</h2>
<p>假设要存储一段只包含 5 个字符 <code>A B C D E</code> 的文本。最朴素的办法是<strong>等长编码</strong>：</p>
<p>每个字符用同样多的二进制位表示。5 个字符至少需要 3 位，比如：</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>编码</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td><code>000</code></td>
</tr>
<tr>
<td>B</td>
<td><code>001</code></td>
</tr>
<tr>
<td>C</td>
<td><code>010</code></td>
</tr>
<tr>
<td>D</td>
<td><code>011</code></td>
</tr>
<tr>
<td>E</td>
<td><code>100</code></td>
</tr>
</tbody>
</table>
<p>等长编码的好处是简单，但有个明显的浪费：<strong>它对所有字符一视同仁</strong>。</p>
<p>现实中字符出现的频率差别很大。比如英文里 <code>e</code> 出现得特别多，<code>z</code> 很少。如果让高频字符用短码、低频字符用长码，整体长度就能显著缩短。这正是<strong>莫尔斯电码</strong>背后的思想，也是<strong>哈夫曼编码</strong>要解决的问题。</p>
<p>:::tip[基本思想]
频率高的字符配短码，频率低的字符配长码。平均下来每个字符占用的位数会更少。
:::</p>
<hr />
<h2>二、变长编码的歧义与前缀码</h2>
<p>变长编码带来了一个新问题：解码时怎么知道一个字符在哪里结束？</p>
<p>比如：</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>编码</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td><code>0</code></td>
</tr>
<tr>
<td>B</td>
<td><code>01</code></td>
</tr>
</tbody>
</table>
<p>那么收到 <code>01</code> 时，到底是 <code>A</code> 后面跟一个 <code>1</code>，还是就是 <code>B</code>？这就产生了歧义。</p>
<p>解决办法是只使用<strong>前缀码</strong>：</p>
<blockquote>
<p>任何一个字符的编码，都不是另一个字符编码的前缀。</p>
</blockquote>
<p>满足这个条件，就能从左到右<strong>唯一、无歧义地</strong>解码。</p>
<p>哈夫曼编码就是一种<strong>最优前缀码</strong>：在所有前缀码里，它让<strong>带权平均码长最短</strong>，等价于带权路径长度最小。注意这里优化的是"频率 × 码长"的加权平均，而不是所有字符码长的简单平均——哈夫曼不关心高频字符和低频字符的码长"看起来一样长"，而关心高频字符短、低频字符长，从而让总位数最少。</p>
<hr />
<h2>三、用二叉树直观表示前缀码</h2>
<p>前缀码可以用<strong>二叉树</strong>表示。</p>
<p>约定从根出发，<strong>往左走记 <code>0</code>，往右走记 <code>1</code></strong>。每个字符放在一个叶子节点上，从根到叶子的路径就是它的编码。</p>
<pre><code>graph TD
    Root(( )) --&gt;|0| A([A])
    Root(( )) --&gt;|1| N1(( ))
    N1 --&gt;|0| B([B])
    N1 --&gt;|1| N2(( ))
    N2 --&gt;|0| C([C])
    N2 --&gt;|1| D([D])
</code></pre>
<p>上图中：</p>
<ul>
<li><code>A</code> 的编码是 <code>0</code></li>
<li><code>B</code> 的编码是 <code>10</code></li>
<li><code>C</code> 的编码是 <code>110</code></li>
<li><code>D</code> 的编码是 <code>111</code></li>
</ul>
<p>因为每个字符都在叶子节点上，<strong>没有任何字符在另一个字符的路径中间</strong>，所以天然满足前缀码的要求。反过来也对：任何前缀码都能画成一棵叶子节点放字符的二叉树。</p>
<p>这里只画了 4 个字符的示意，第 5 节会用一个完整的 5 字符例子演示建树全过程。</p>
<h3>3.1 代价：带权路径长度</h3>
<p>设字符 $c_i$ 的频率为 $f_i$，在树中的深度为 $l_i$。</p>
<p>字符 $c_i$ 一共出现 $f_i$ 次，每次编码需要 $l_i$ 位，所以它占用的总位数为：</p>
<p>$$
f_i \cdot l_i
$$</p>
<p>把所有字符加起来，就是这套编码存储整个文本所需的总位数：</p>
<p>$$
WPL = \sum_{i=1}^{n} f_i \cdot l_i
$$</p>
<p>这个量叫<strong>带权路径长度</strong>，简称 WPL。</p>
<p>哈夫曼编码的目标，就是：</p>
<blockquote>
<p><strong>构造一棵二叉树，让它的带权路径长度最小。</strong></p>
</blockquote>
<p>这就是带权路径长度最小的二叉树——也叫<strong>最优二叉树</strong>或<strong>哈夫曼树</strong>。</p>
<hr />
<h2>四、贪心构造：如何分配编码长度</h2>
<h3>4.1 权值与树深度的规律</h3>
<p>回到带权路径长度的定义 $WPL = \sum f_i \cdot l_i$：每个字符对总位数的贡献，是它的<strong>频率 × 深度</strong>。</p>
<p>那么树里最深层的位置应该分给谁？</p>
<ul>
<li>如果把<strong>频率大的字符</strong>放在最深层，它的 $f_i$ 很大、$l_i$ 也最大，乘起来代价很高；</li>
<li>如果把<strong>频率小的字符</strong>放在最深层，$f_i$ 很小，即使 $l_i$ 大，乘起来代价也很低。</li>
</ul>
<p>所以一个朴素的判断是：<strong>权值越小，越适合放在树的深层。</strong> 这意味着构造时应该优先让最小的两个权值成为兄弟节点、沉到最深处。</p>
<p>哈夫曼算法就是把这个判断落到了一个反复执行的操作上。</p>
<h3>4.2 算法</h3>
<p>哈夫曼算法很简洁：</p>
<blockquote>
<p><strong>每次取出频率最小的两个节点，合并成一个新节点，新节点的频率等于两者之和。把这个新节点放回去，重复直到只剩一个节点。</strong></p>
</blockquote>
<p>最后剩下的那个就是哈夫曼树的根。所有字符变成了叶子节点，频率越高的字符离根越近（编码越短），频率越低的字符离根越远（编码越长）。</p>
<p>:::note[为什么这样贪心是对的]
频率最低的两个字符，无论如何都会被编码成某个长度。让它们位于最深的同一层、并互为兄弟，是最"划算"的安排——因为代价最低的两个叶子深度最大，对 WPL 的贡献最小。合并它们、用它们频率之和代替，相当于把"这两个叶子"打包成一个更重的叶子继续参与后续选择。这正是一次贪心选择。
:::</p>
<p>算法步骤：</p>
<ol>
<li>把每个字符看成一个叶子节点，频率就是它的权值。</li>
<li>从所有节点里取出<strong>频率最小的两个</strong>。</li>
<li>创建一个新节点作为它们的父节点，权值 = 两者之和。</li>
<li>把新节点放回节点集合。</li>
<li>重复第 2~4 步，直到集合里只剩一个节点，它就是根。</li>
</ol>
<hr />
<h2>五、图解哈夫曼树的构造步骤</h2>
<p>假设有 5 个字符，频率如下：</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
<th>E</th>
</tr>
</thead>
<tbody>
<tr>
<td>频率</td>
<td>7</td>
<td>5</td>
<td>2</td>
<td>4</td>
<td>9</td>
</tr>
</tbody>
</table>
<p>按频率排序的初始节点集合：</p>
<pre><code>C(2)  D(4)  B(5)  A(7)  E(9)
</code></pre>
<h3>第 1 次合并</h3>
<p>取最小的两个：<code>C(2)</code> 和 <code>D(4)</code>，合并成新节点 <code>CD(6)</code>。</p>
<pre><code>B(5)  CD(6)  A(7)  E(9)
</code></pre>
<h3>第 2 次合并</h3>
<p>取 <code>B(5)</code> 和 <code>CD(6)</code>，合并成 <code>BCD(11)</code>。</p>
<pre><code>A(7)  E(9)  BCD(11)
</code></pre>
<h3>第 3 次合并</h3>
<p>取 <code>A(7)</code> 和 <code>E(9)</code>，合并成 <code>AE(16)</code>。</p>
<pre><code>BCD(11)  AE(16)
</code></pre>
<h3>第 4 次合并</h3>
<p>取最后两个 <code>BCD(11)</code> 和 <code>AE(16)</code>，合并成根节点 <code>Root(27)</code>。</p>
<p>至此只剩一个节点，建树完成。最终的哈夫曼树如下，约定左侧为 0，右侧为 1：</p>
<pre><code>graph TD
    Root((27)) --&gt;|0| BCD((11))
    Root((27)) --&gt;|1| AE((16))
    BCD --&gt;|0| B([B:5])
    BCD --&gt;|1| CD((6))
    CD --&gt;|0| C([C:2])
    CD --&gt;|1| D([D:4])
    AE --&gt;|0| A([A:7])
    AE --&gt;|1| E([E:9])
</code></pre>
<p>从根到每个叶子读出编码：</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>频率</th>
<th>编码</th>
<th>码长</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>7</td>
<td><code>10</code></td>
<td>2</td>
</tr>
<tr>
<td>E</td>
<td>9</td>
<td><code>11</code></td>
<td>2</td>
</tr>
<tr>
<td>B</td>
<td>5</td>
<td><code>00</code></td>
<td>2</td>
</tr>
<tr>
<td>C</td>
<td>2</td>
<td><code>010</code></td>
<td>3</td>
</tr>
<tr>
<td>D</td>
<td>4</td>
<td><code>011</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>可以看到：高频的 <code>A E B</code> 都是 2 位，低频的 <code>C D</code> 是 3 位。完全符合直觉。</p>
<h3>5.1 算一算压缩效果</h3>
<p>等长编码下，每个字符 3 位，总位数：</p>
<p>$$
3 \times (7 + 5 + 2 + 4 + 9) = 3 \times 27 = 81
$$</p>
<p>哈夫曼编码的带权路径长度：</p>
<p>$$
\begin{aligned}
WPL &amp;= 7 \cdot 2 + 9 \cdot 2 + 5 \cdot 2 + 2 \cdot 3 + 4 \cdot 3 \
&amp;= 14 + 18 + 10 + 6 + 12 \
&amp;= 60
\end{aligned}
$$</p>
<p>从 81 位压缩到 60 位，节省了约 <strong>26%</strong>。</p>
<p>:::tip[带权路径长度的快速算法]
建树完成后，带权路径长度等于所有非叶节点权值之和，不需要重新计算深度。</p>
<p>因为每次合并产生的新节点，其权值都会被累加进带权路径长度；而每片叶子节点被累加的次数恰好等于它的深度，因此所有内部节点权值之和就是总的带权路径长度。</p>
<p>上例中合并产生的内部节点权值依次是 $6,\ 11,\ 16,\ 27$，求和：</p>
<p>$$
WPL = 6 + 11 + 16 + 27 = 60
$$</p>
<p>正好等于前面逐字符算出的结果。考试或调试时这个方法最快。
:::</p>
<hr />
<h2>六、Java 实现</h2>
<p>关键数据结构是<strong>最小堆</strong>。每次取两个最小元素、合并、放回，正好对应"每次取最小的两个"。</p>
<h3>6.1 哈夫曼树节点</h3>
<pre><code>class HuffmanNode {
    char ch;            // 存放的字符
    int freq;           // 频率
    HuffmanNode left;   // 左子节点
    HuffmanNode right;  // 右子节点

    HuffmanNode(char ch, int freq) {
        this.ch = ch;
        this.freq = freq;
    }

    HuffmanNode(int freq, HuffmanNode left, HuffmanNode right) {
        this.freq = freq;
        this.left = left;
        this.right = right;
    }

    boolean isLeaf() {
        return left == null &amp;&amp; right == null;
    }
}
</code></pre>
<h3>6.2 建树</h3>
<pre><code>import java.util.PriorityQueue;

public class HuffmanCoding {

    /**
     * 根据字符频率构造哈夫曼树。
     *
     * @param chars 字符数组
     * @param freqs 对应的频率数组
     * @return 哈夫曼树的根节点
     */
    public static HuffmanNode buildTree(char[] chars, int[] freqs) {
        // 按频率排序的最小堆
        PriorityQueue&lt;HuffmanNode&gt; heap = new PriorityQueue&lt;&gt;(
                (a, b) -&gt; Integer.compare(a.freq, b.freq)
        );

        // 每个字符作为一个叶子节点入堆
        for (int i = 0; i &lt; chars.length; i++) {
            heap.offer(new HuffmanNode(chars[i], freqs[i]));
        }

        // 不断取出最小的两个，合并后放回，直到只剩一个
        while (heap.size() &gt; 1) {
            HuffmanNode a = heap.poll();
            HuffmanNode b = heap.poll();
            HuffmanNode parent = new HuffmanNode(
                    a.freq + b.freq, a, b
            );
            heap.offer(parent);
        }

        return heap.poll();
    }
}
</code></pre>
<p>:::tip[等价写法]
建树循环里 <code>a</code> 作为左子节点、<code>b</code> 作为右子节点只是一种约定。两者调换不影响带权路径长度，只是具体编码会不同。任何一棵具有相同叶子深度的哈夫曼树，带权路径长度都相同——<strong>哈夫曼编码不唯一</strong>，但最优性相同。
:::</p>
<h3>6.3 生成编码表</h3>
<p>建好树后，从根做一次 DFS，把路径上的 <code>0/1</code> 拼起来，到达叶子时就得到该字符的编码。以下两个方法同样属于 <code>HuffmanCoding</code> 类。</p>
<pre><code>import java.util.HashMap;
import java.util.Map;

public static Map&lt;Character, String&gt; buildCodeTable(HuffmanNode root) {
    Map&lt;Character, String&gt; table = new HashMap&lt;&gt;();
    dfs(root, "", table);
    return table;
}

private static void dfs(HuffmanNode node, String code, Map&lt;Character, String&gt; table) {
    if (node == null) {
        return;
    }
    if (node.isLeaf()) {
        // 记录字符编码
        // 只有一个字符时的特殊处理
        table.put(node.ch, code.isEmpty() ? "0" : code);
        return;
    }
    dfs(node.left, code + "0", table);
    dfs(node.right, code + "1", table);
}
</code></pre>
<h3>6.4 完整运行</h3>
<pre><code>public static void main(String[] args) {
    char[] chars = {'A', 'B', 'C', 'D', 'E'};
    int[] freqs = {7, 5, 2, 4, 9};

    HuffmanNode root = buildTree(chars, freqs);
    Map&lt;Character, String&gt; table = buildCodeTable(root);

    System.out.println("字符编码表：");
    for (char c : chars) {
        System.out.println(c + " : " + table.get(c));
    }

    System.out.println("带权路径长度：" + computeWPL(root));
}

/** 计算带权路径长度。 */
private static int computeWPL(HuffmanNode node) {
    return computeWPL(node, 0);
}

private static int computeWPL(HuffmanNode node, int depth) {
    if (node == null) {
        return 0;
    }
    if (node.isLeaf()) {
        return node.freq * depth;
    }
    return computeWPL(node.left, depth + 1)
         + computeWPL(node.right, depth + 1);
}
</code></pre>
<p>运行结果：</p>
<pre><code>字符编码表：
A : 10
B : 00
C : 010
D : 011
E : 11
带权路径长度：60
</code></pre>
<hr />
<h2>七、数据的压缩与解压</h2>
<p>有了编码表和哈夫曼树，压缩和解压都很简单。</p>
<p><strong>编码</strong>：查表，把每个字符替换成它的编码串拼接起来。</p>
<p><strong>解码</strong>：从根出发，逐位读入。遇到 <code>0</code> 往左走，遇到 <code>1</code> 往右走，走到叶子就输出对应字符，然后回到根继续。</p>
<pre><code>/** 编码。 */
public static String encode(String text, Map&lt;Character, String&gt; table) {
    StringBuilder sb = new StringBuilder();
    for (char c : text.toCharArray()) {
        sb.append(table.get(c));
    }
    return sb.toString();
}

/** 解码。 */
public static String decode(String bits, HuffmanNode root) {
    if (root == null) {
        return "";
    }
    StringBuilder sb = new StringBuilder();
    if (root.isLeaf()) {
        // 只有一个字符时的处理
        for (char b : bits.toCharArray()) {
            if (b != '0') {
                throw new IllegalArgumentException("无效的编码串");
            }
            sb.append(root.ch);
        }
        return sb.toString();
    }
    HuffmanNode cur = root;
    for (char b : bits.toCharArray()) {
        cur = (b == '0') ? cur.left : cur.right;
        if (cur == null) {
            throw new IllegalArgumentException("无效的编码串");
        }
        if (cur.isLeaf()) {
            sb.append(cur.ch);
            cur = root;
        }
    }
    if (cur != root) {
        throw new IllegalArgumentException("编码串不完整，末尾有未解码的位");
    }
    return sb.toString();
}
</code></pre>
<p>:::warning[解码前提]
解码时需要持有同一棵哈夫曼树。如果压缩和解压使用的频率统计不一致，得到的树不同，解码就会出错。实际应用通常会把编码表也写进压缩文件。
:::</p>
<hr />
<h2>八、复杂度分析</h2>
<p>设字符种类数为 $n$。</p>
<ul>
<li><strong>时间复杂度</strong>：$O(n \log n)$。建树过程做 $n - 1$ 次合并，每次堆的插入和弹出都是 $O(\log n)$。</li>
<li><strong>空间复杂度</strong>：$O(n)$。哈夫曼树有 $n$ 个叶子节点和 $n - 1$ 个内部节点，共 $2n - 1$ 个节点；编码表也占用 $O(n)$。</li>
</ul>
<p>对于一段固定文本，字符种类数 $n$ 通常很小且固定，例如 ASCII 文本的 $n \le 128$。因此建树开销可以忽略，主要的计算瓶颈在于扫描文本统计频率和编码。</p>
<p>编码和解码本身也都是线性的：编码一段长度为 $L$ 的文本需要 $O(L)$（逐字符查表拼接），解码一个长度为 $B$ 的编码串同样需要 $O(B)$（逐位沿树走）。</p>
<hr />
<h2>九、总结</h2>
<p>哈夫曼编码可以概括为：</p>
<blockquote>
<p><strong>每次合并频率最小的两个节点，频率高的字符离根近、编码短，最终得到平均码长最短的前缀码。</strong></p>
</blockquote>
<p>要点回顾：</p>
<ul>
<li><strong>前缀码</strong>保证变长编码能唯一解码；前缀码可以用二叉树表示，叶子放字符。</li>
<li><strong>优化目标</strong>是最小化带权路径长度 $WPL = \sum f_i \cdot l_i$。</li>
<li><strong>贪心策略</strong>：用最小堆每次取最小的两个节点合并，新节点权值为两者之和。</li>
<li><strong>正确性</strong>来自贪心选择 + 最优子结构：最轻的两个叶子可安全放到最深层当兄弟。</li>
<li>哈夫曼编码并不唯一，但其最优性与带权路径长度都是相同的。</li>
</ul>
<p>口诀：</p>
<blockquote>
<p>频率小的先合并，权值相加放回堆；高频叶子靠近根，短码省位压缩美。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>最短等待时间（加工顺序问题）</title>
    <link href="https://blog.yoake.dev/posts/algorithm/minwaitingtime/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/minwaitingtime/</id>
    <published>2026-06-15T10:10:02.000Z</published>
    <updated>2026-06-15T10:10:02.000Z</updated>
    <summary>用贪心思想解决加工顺序问题：让耗时短的任务先执行，从而最小化所有任务的总等待时间。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是最短等待时间问题？</h2>
<p>最短等待时间问题（也叫<strong>加工顺序问题</strong>、<strong>调度问题</strong>）是贪心算法里的经典问题。</p>
<p>问题的描述是：</p>
<blockquote>
<p>有 $n$ 个顾客（或 $n$ 个任务）等待同一台机器（或一个服务窗口）处理。每个任务的加工时间不同。机器一次只能处理一个任务，且不能中断。应该如何安排加工顺序，才能让<strong>所有任务的总等待时间最短</strong>？</p>
</blockquote>
<p>:::tip[生活映射]
想象你只有一个收银台，面前排了一队人结账。每个人买的东西不一样多，结账时间也不同。你想让所有人"等待的总时间"最少，应该让谁先结账？</p>
<p>直觉就是：让买得少、结账快的人先来。买得多的人排在后面，只会让后面的人等更久。
:::</p>
<hr />
<h2>2. 关键概念：等待时间</h2>
<p>要把这个问题讲清楚，先得搞清楚"等待时间"到底怎么算。</p>
<p>设第 $i$ 个被处理的任务加工时间为 $t_i$（下标 $i$ 表示<strong>第 $i$ 个被处理的顺序</strong>，不是任务的原始编号），那么：</p>
<ul>
<li>第 $1$ 个任务开始就处理，<strong>等待时间为 $0$</strong>。</li>
<li>第 $2$ 个任务要等第 $1$ 个任务处理完，<strong>等待时间为 $t_1$</strong>。</li>
<li>第 $3$ 个任务要等前两个都处理完，<strong>等待时间为 $t_1 + t_2$</strong>。</li>
<li>……</li>
<li>第 $i$ 个任务的<strong>等待时间为前 $i-1$ 个任务加工时间之和</strong>：</li>
</ul>
<p>$$
w_i = \sum_{k=1}^{i-1} t_k
$$</p>
<p>所有任务的总等待时间为：</p>
<p>$$
W = \sum_{i=1}^{n} w_i = \sum_{i=1}^{n} \sum_{k=1}^{i-1} t_k
$$</p>
<p>这个双重求和看起来抽象，我们以 $4$ 个任务为例把它展开。</p>
<p>$n=4$ 时，每个任务的等待时间为：</p>
<p>$$
\begin{aligned}
w_1 &amp;= 0 \
w_2 &amp;= t_1 \
w_3 &amp;= t_1 + t_2 \
w_4 &amp;= t_1 + t_2 + t_3
\end{aligned}
$$</p>
<p>总等待时间就是：</p>
<p>$$
W = 0 + t_1 + (t_1 + t_2) + (t_1 + t_2 + t_3)
$$</p>
<p>展开并合并同类项：</p>
<p>$$
W = 3 \cdot t_1 + 2 \cdot t_2 + 1 \cdot t_3 + 0 \cdot t_4
$$</p>
<p>可以发现一个规律：</p>
<ul>
<li>$t_1$ 出现在它<strong>后面 3 个任务</strong>的等待时间中，所以被计算 $3$ 次；</li>
<li>$t_2$ 出现在它<strong>后面 2 个任务</strong>的等待时间中，所以被计算 $2$ 次；</li>
<li>$t_3$ 出现在它<strong>后面 1 个任务</strong>的等待时间中，所以被计算 $1$ 次；</li>
<li>$t_4$ 是最后一个任务，后面没有任务，所以被计算 $0$ 次。</li>
</ul>
<p>一般地，第 $k$ 个任务会出现在它<strong>后面 $n - k$ 个任务</strong>的等待时间中，因此它的加工时间 $t_k$ 被计算 $(n - k)$ 次。</p>
<p>所以总等待时间可以统一写成：</p>
<p>$$
W = \sum_{k=1}^{n} (n - k) \cdot t_k
$$</p>
<p>:::note[这个公式是关键]
$W = \sum_{k=1}^{n} (n - k) \cdot t_k$ 说明：排得越靠前（$k$ 越小）的任务，它被计入总等待时间的次数 $(n-k)$ 就越大。要让 $W$ 最小，就应该让 $t_k$ 小的任务占据"被计入次数多"的位置（即靠前的位置），让 $t_k$ 大的任务往后排。</p>
<p>这正是"短任务优先"的数学根源。
:::</p>
<hr />
<h2>3. 贪心策略</h2>
<p>最短等待时间问题的贪心策略是：</p>
<blockquote>
<p><strong>按加工时间从小到大排序，让耗时最短的任务先处理。</strong></p>
</blockquote>
<p>也就是经典的 <strong>最短处理时间优先</strong>（Shortest Processing Time first，SPT）规则。</p>
<p>算法步骤：</p>
<ol>
<li>把所有任务按加工时间从小到大排序。</li>
<li>按这个顺序依次处理。</li>
<li>累加每个任务的等待时间，得到总等待时间 $W$。</li>
</ol>
<p>实现很简单。</p>
<hr />
<h2>4. 以 4 个任务为例</h2>
<p>假设有 $4$ 个任务，加工时间分别是：</p>
<p>$$
t = [4,\ 2,\ 8,\ 1]
$$</p>
<p>如果按<strong>原始顺序</strong>处理（不排序）：</p>
<table>
<thead>
<tr>
<th>处理顺序</th>
<th>任务时间</th>
<th>等待时间</th>
</tr>
</thead>
<tbody>
<tr>
<td>第 1 个</td>
<td>4</td>
<td>0</td>
</tr>
<tr>
<td>第 2 个</td>
<td>2</td>
<td>4</td>
</tr>
<tr>
<td>第 3 个</td>
<td>8</td>
<td>$4 + 2 = 6$</td>
</tr>
<tr>
<td>第 4 个</td>
<td>1</td>
<td>$4 + 2 + 8 = 14$</td>
</tr>
</tbody>
</table>
<p>总等待时间：</p>
<p>$$
W = 0 + 4 + 6 + 14 = 24
$$</p>
<p>如果按<strong>贪心策略</strong>，先排序为 $[1, 2, 4, 8]$，再处理：</p>
<table>
<thead>
<tr>
<th>处理顺序</th>
<th>任务时间</th>
<th>等待时间</th>
</tr>
</thead>
<tbody>
<tr>
<td>第 1 个</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>第 2 个</td>
<td>2</td>
<td>1</td>
</tr>
<tr>
<td>第 3 个</td>
<td>4</td>
<td>$1 + 2 = 3$</td>
</tr>
<tr>
<td>第 4 个</td>
<td>8</td>
<td>$1 + 2 + 4 = 7$</td>
</tr>
</tbody>
</table>
<p>总等待时间：</p>
<p>$$
W = 0 + 1 + 3 + 7 = 11
$$</p>
<p>对比 $24$ 和 $11$，差距很大。<strong>短任务优先</strong>让每个排队的任务都少等一会儿，总等待时间少了超过一半。</p>
<p>:::note[为什么差距这么大]
原始顺序里那个耗时 $8$ 的任务排在第 $3$ 位，把第 $4$ 位的任务"堵"了很久。贪心策略把长任务挪到最后，它不再拖累任何人；而把它前面省下来的时间，让所有早处理的短任务都受益。
:::</p>
<hr />
<h2>5. Java 实现</h2>
<p>实现分两步：先排序，再累加。维护一个"已加工时间累计值 <code>cur</code>"作为下一个任务的等待时间。</p>
<pre><code>import java.util.Arrays;

public class MinWaitingTime {

    /**
     * 返回所有任务的最小总等待时间。
     *
     * @param times 每个任务的加工时间
     * @return 最小总等待时间
     */
    public static int minWaitingTime(int[] times) {
        if (times == null || times.length &lt;= 1) {
            // 没有任务或只有一个任务，都不需要等待
            return 0;
        }

        // 1. 贪心：按加工时间从小到大排序，短任务优先
        Arrays.sort(times);

        int total = 0; // 总等待时间
        int cur = 0;   // 当前累计的加工时间，也就是下一个任务的等待时间

        // 2. 依次处理，累加每个任务的等待时间
        for (int i = 0; i &lt; times.length; i++) {
            // 当前任务要等前面所有任务处理完，等待时间就是 cur
            total += cur;
            // 处理当前任务后，累计时间增加它的加工时间
            cur += times[i];
        }

        return total;
    }

    public static void main(String[] args) {
        int[] times = {4, 2, 8, 1};

        int result = minWaitingTime(times);
        System.out.println("最小总等待时间：" + result); // 输出 11
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>最小总等待时间：11
</code></pre>
<p>代码里两个变量要分清楚：</p>
<ul>
<li><code>cur</code>：<strong>已经处理完的任务的总加工时间</strong>。下一个任务开始前，它已经等了这么久，所以 <code>cur</code> 也就是下一个任务的等待时间。</li>
<li><code>total</code>：把每个任务的等待时间 <code>cur</code> 累加起来，得到所有任务的总等待时间。</li>
</ul>
<p>:::warning[循环里的顺序别写反]
一定要先 <code>total += cur</code>（累加当前任务的等待时间），再 <code>cur += times[i]</code>（把当前任务的加工时间并入累计值）。如果写反了，相当于把当前任务的加工时间也算进了它自己的等待时间，结果会偏大。
:::</p>
<hr />
<h2>6. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n \log n)$，主要来自排序。</li>
<li><strong>空间复杂度</strong>：$O(\log n)$，即排序所需的栈空间。</li>
</ul>
<p>排序是 $O(n \log n)$，遍历累加是 $O(n)$，总时间复杂度由排序主导。如果数据范围很小（比如加工时间都是小整数），也可以像 <a href="/posts/algorithm/maximumicecreambars/">最优装载</a> 那样用计数排序优化到 $O(n + C)$，其中 $C$ 是加工时间的最大值。</p>
<hr />
<h2>7. 拓展：平均等待时间与多机调度</h2>
<h3>7.1 平均等待时间</h3>
<p>题目有时问的是<strong>平均等待时间</strong>，而不是总等待时间：</p>
<p>$$
\bar{W} = \frac{W}{n}
$$</p>
<p>由于 $n$ 是固定的，最小化总等待时间 $W$ 和最小化平均等待时间 $\bar{W}$ 是完全等价的，贪心策略不变。</p>
<h3>7.2 如果任务有优先级或到达时间呢？</h3>
<p>本文讨论的是最简单的情况：所有任务<strong>同时到达</strong>，且<strong>没有优先级</strong>。一旦放松这些假设，问题会迅速变难：</p>
<table>
<thead>
<tr>
<th>假设变化</th>
<th>问题</th>
<th>求解方法</th>
</tr>
</thead>
<tbody>
<tr>
<td>任务有不同的到达时间</td>
<td>单机调度 $1 \mid r_j \mid \sum w_j$</td>
<td>一般是 NP-Hard，需要精确算法或近似</td>
</tr>
<tr>
<td>任务带权重（优先级）</td>
<td>加权最短处理时间优先（WSPT）</td>
<td>按 $t_j / w_j$ 排序贪心</td>
</tr>
<tr>
<td>多台机器并行</td>
<td>多机调度问题</td>
<td>多数情况 NP-Hard，常用 LPT 等近似算法</td>
</tr>
</tbody>
</table>
<p>:::note
带权重的版本（任务有不同的重要性）通常用 <strong>WSPT 规则</strong>：按 $\dfrac{t_j}{w_j}$ 从小到大排序，即"单位权重耗时越短的任务越优先"。本文的 SPT 就是 $w_j \equiv 1$ 的特殊情况。
:::</p>
<h3>7.3 如果目标是"最大完工时间"呢？</h3>
<p>注意区分两个目标：</p>
<ul>
<li><strong>总等待时间最小</strong>（本文）：关心所有任务等的总和，单机下 SPT 最优。</li>
<li><strong>最大完工时间（makespan）最小</strong>：关心最后一个任务什么时候结束。单机下任何顺序的 makespan 都是 $\sum t_i$，与顺序无关；只有多台机器并行时，这个目标才有意义，且是 NP-Hard。</li>
</ul>
<hr />
<h2>8. 总结</h2>
<p>最短等待时间问题的核心是：</p>
<blockquote>
<p>让加工时间短的任务先处理，加工时间长的任务后处理。</p>
</blockquote>
<p>核心内容如下：</p>
<ul>
<li>总等待时间公式 $W = \sum_{k=1}^{n} (n-k) \cdot t_k$ 揭示了本质：靠前位置的系数大，应该配短任务。</li>
<li>贪心策略：按加工时间升序排序（SPT 规则）。</li>
<li>平均等待时间和总等待时间等价，贪心策略不变。</li>
<li>带权重时升级为 WSPT 规则；任务有到达时间或多台机器时问题变难。</li>
</ul>
<p>口诀：</p>
<blockquote>
<p>短的先做长的后，总等最少不用愁；公式一看就明白，大权配小数最优。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>旅行售货员问题（回溯法与分支限界法）</title>
    <link href="https://blog.yoake.dev/posts/algorithm/travelingsalesman/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/travelingsalesman/</id>
    <published>2026-06-15T10:10:02.000Z</published>
    <updated>2026-06-15T10:10:02.000Z</updated>
    <summary>从排列树回溯到矩阵归约下界剪枝，深入解析旅行售货员问题的精确解法，理解 NP-Hard 问题的搜索与剪枝思想。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是旅行售货员问题？</h2>
<p>旅行售货员问题（Traveling Salesman Problem，简称 TSP），也叫旅行商问题，是组合优化和计算机科学中最著名的难题之一。</p>
<p>问题的描述很简单：</p>
<blockquote>
<p>有一个售货员要拜访 $n$ 个城市，每个城市恰好拜访一次，最后回到出发城市。已知任意两个城市之间的距离，求总路程最短的环游路线。</p>
</blockquote>
<p>例如，$4$ 个城市 A、B、C、D，售货员从 A 出发，一条合法路线是：</p>
<p>$$
A \rightarrow B \rightarrow D \rightarrow C \rightarrow A
$$</p>
<p>它经过了所有城市各一次，并回到了起点。</p>
<p>:::note
TSP 表面上像一个"排路线"的小问题，但它是经典的 <strong>NP-Hard 问题</strong>。目前没有任何多项式时间的精确算法，规模一大就只能靠剪枝、近似或启发式方法来求解。
:::</p>
<hr />
<h2>2. 问题的数学描述</h2>
<p>把城市看作图的顶点，城市之间的道路看作带权边，TSP 就是：在一个 <strong>带权完全图</strong> 上，找一条<strong>权重最小的哈密顿回路（Hamiltonian cycle）</strong>。</p>
<p>设 $n$ 个城市编号为 $0, 1, \ldots, n-1$，距离矩阵为 $d_{ij}$，表示城市 $i$ 到城市 $j$ 的距离。一条环游可以表示为一个排列：</p>
<p>$$
x = (x_0,\ x_1,\ \ldots,\ x_{n-1})
$$</p>
<p>其中 $x_0$ 是出发城市，$x_1, \ldots, x_{n-1}$ 是其余城市的访问顺序。总路程为：</p>
<p>$$
L(x) = \sum_{i=0}^{n-2} d_{x_i,\ x_{i+1}} + d_{x_{n-1},\ x_0}
$$</p>
<p>第一项是从出发城市一路走到最后一个城市的距离，第二项是从最后一个城市回到起点的距离。目标是：</p>
<p>$$
\min_{x} L(x)
$$</p>
<p>:::note
距离矩阵不一定是对称的（去程和回程距离可以不同），也不一定满足三角不等式。本文为方便演示，使用对称的距离矩阵。
:::</p>
<hr />
<h2>3. 解空间：排列树</h2>
<p>由于售货员要给除起点外的每个城市排定一个访问顺序，所以 TSP 的解空间就是排列。</p>
<p>固定出发城市为 $x_0 = 0$（因为环游从哪个城市出发都是等价的），剩下的 $n-1$ 个城市做全排列。因此解空间是一棵<strong>排列树</strong>，这和 <a href="/posts/algorithm/primeringbacktracking/">素数环问题</a> 的结构是一样的。</p>
<p>不同的环游方案数上界为：</p>
<p>$$
(n-1)!
$$</p>
<p>:::tip[为什么是 $(n-1)!$ 而不是 $n!$]
因为环游是一个回路，起点选哪个城市不影响结果，所以固定一个出发城市，只排列剩下的 $n-1$ 个城市即可。如果距离对称，正向和反向是同一条回路，还可以再除以 $2$，即 $\dfrac{(n-1)!}{2}$。
:::</p>
<p>排列树长这样（以 $n = 4$ 为例）：</p>
<pre><code>                 起点 0
                /   |   \
            选 1   选 2   选 3
           /  \    ...    ...
        选 2  选 3
         |     |
        选 3  选 2
         |     |
      回到 0  回到 0
</code></pre>
<p>回溯法就是在这棵排列树上做深度优先遍历：能走就走，走不通或明显不优就回头。</p>
<hr />
<h2>4. 回溯法</h2>
<h3>4.1 基本思路</h3>
<p>回溯法的核心步骤：</p>
<ol>
<li>固定出发城市为 $0$，标记已访问。</li>
<li>从第 $2$ 个位置开始，依次尝试每个未访问的城市。</li>
<li>把选中城市加入路径，累加距离，递归进入下一层。</li>
<li>当所有城市都访问完，加上回到起点的距离，得到一条完整环游的总路程，更新最优解。</li>
<li>递归返回后，撤销选择（回溯），尝试下一个城市。</li>
</ol>
<p>:::tip[回溯核心口诀]
做出选择 $\rightarrow$ 递归探索下一层 $\rightarrow$ 撤销选择（恢复现场）。
:::</p>
<h3>4.2 关键剪枝：代价超过当前最优即放弃</h3>
<p>纯排列树遍历是 $O((n-1)!)$，规模稍大就爆炸。最直接的剪枝是：</p>
<blockquote>
<p>如果当前已经走过的距离 <code>currentCost</code> 加上下一步的距离，已经大于等于已知的最优解 <code>bestCost</code>，那么这条路再走下去只会更差，直接剪掉。</p>
</blockquote>
<p>$$
\text{currentCost} + d_{\text{当前},\ \text{下一城市}} \ge \text{bestCost} \Rightarrow \text{剪枝}
$$</p>
<p>这就是分支限界思想的雏形：用一个<strong>界</strong>来判断某个分支是否还有希望。</p>
<hr />
<h2>5. 以 4 个城市为例走一遍</h2>
<p>设 $4$ 个城市 A、B、C、D（编号 $0, 1, 2, 3$），距离矩阵为：</p>
<pre><code>      A    B    C    D
A     ∞   10   15   20
B    10    ∞   35   25
C    15   35    ∞   30
D    20   25   30    ∞
</code></pre>
<p>从 A（城市 $0$）出发，列出所有合法环游（共 $3! = 6$ 条，对称距离下成对相等）：</p>
<pre><code>A→B→C→D→A：10 + 35 + 30 + 20 = 95
A→B→D→C→A：10 + 25 + 30 + 15 = 80   ← 最优
A→C→B→D→A：15 + 35 + 25 + 20 = 95
A→C→D→B→A：15 + 30 + 25 + 10 = 80   ← 最优（上一条的逆向）
A→D→B→C→A：20 + 25 + 35 + 15 = 95
A→D→C→B→A：20 + 30 + 35 + 10 = 95
</code></pre>
<p>最优总路程是 $80$，对应路线 $A \rightarrow B \rightarrow D \rightarrow C \rightarrow A$。</p>
<p>下面按代码中的 DFS 顺序演示搜索过程。<code>bestCost</code> 初始为 $+\infty$。</p>
<p><strong>第 1 步</strong>：固定起点 A，<code>path = [A]</code>。</p>
<p><strong>第 2 步</strong>：选 B。</p>
<pre><code>A → B   currentCost = 10
</code></pre>
<p><strong>第 3 步</strong>：选 C。</p>
<pre><code>A → B → C   currentCost = 10 + 35 = 45
</code></pre>
<p><strong>第 4 步</strong>：只剩 D，选 D，回到 A。</p>
<pre><code>A → B → C → D → A   总路程 = 45 + 30 + 20 = 95
</code></pre>
<p>得到一个解，<code>bestCost = 95</code>。回溯。</p>
<p><strong>回到第 3 步</strong>：换成选 D。</p>
<pre><code>A → B → D → C → A   总路程 = 10 + 25 + 30 + 15 = 80
</code></pre>
<p>$80 &lt; 95$，更新 <code>bestCost = 80</code>。回溯。</p>
<p><strong>回到第 2 步</strong>：换成选 C。进入后：</p>
<pre><code>A → C   currentCost = 15
</code></pre>
<p>继续尝试剩余城市。</p>
<ul>
<li>选 B：<code>A → C → B</code> 的 <code>currentCost = 15 + 35 = 50</code>，再选 D 并回到 A，总路程为 <code>50 + 25 + 20 = 95</code>，不更新。</li>
<li>选 D：<code>A → C → D</code> 的 <code>currentCost = 15 + 30 = 45</code>，再选 B 并回到 A：</li>
</ul>
<pre><code>A → C → D → B → A   总路程 = 45 + 25 + 10 = 80
</code></pre>
<p>$80 = \text{bestCost}$，没有更优，不更新。</p>
<p><strong>回到第 2 步</strong>：最后换成选 D。进入后 <code>A → D</code> 的 <code>currentCost = 20</code>。</p>
<ul>
<li>选 B：<code>A → D → B</code> 的 <code>currentCost = 20 + 25 = 45</code>。再尝试 C 时，<code>nextCost = 45 + 35 = 80</code>，已经不小于 <code>bestCost = 80</code>，剪枝。</li>
<li>选 C：<code>A → D → C</code> 的 <code>currentCost = 20 + 30 = 50</code>。再尝试 B 时，<code>nextCost = 50 + 35 = 85</code>，也被剪枝。</li>
</ul>
<p>搜索结束，最终最优解为 $80$。</p>
<p>:::note
第一次找到的解（这里 $95$）会成为 <code>bestCost</code>。后续一旦某个部分路径的 <code>nextCost &gt;= bestCost</code>，就可以提前放弃；当 <code>bestCost</code> 更新为 $80$ 后，剪枝会更强。
:::</p>
<hr />
<h2>6. Java 实现（回溯法）</h2>
<pre><code>public class TSPBacktracking {

    private static int n;
    private static int[][] dist;       // 距离矩阵
    private static boolean[] visited;  // 标记城市是否已访问
    private static int[] path;         // 当前路径
    private static int[] bestPath;     // 最优路径
    private static int currentCost;    // 当前已走距离
    private static int bestCost;       // 最优总路程

    public static int solve(int[][] matrix) {
        n = matrix.length;
        dist = matrix;
        visited = new boolean[n];
        // 第 n 个位置用于存放回到起点的城市
        path = new int[n + 1];
        bestPath = new int[n + 1];
        bestCost = Integer.MAX_VALUE;
        currentCost = 0;

        // 固定从城市 0 出发
        visited[0] = true;
        path[0] = 0;

        backtrack(1);
        return bestCost;
    }

    private static void backtrack(int index) {
        // 所有城市都已访问，加上回到起点的距离
        if (index == n) {
            int total = currentCost + dist[path[n - 1]][0];
            if (total &lt; bestCost) {
                bestCost = total;
                path[n] = 0; // 回到起点
                System.arraycopy(path, 0, bestPath, 0, n + 1);
            }
            return;
        }

        for (int i = 0; i &lt; n; i++) {
            if (visited[i]) {
                continue;
            }

            // 剪枝：当前代价 + 这一步 已经不优于最优解，放弃
            int nextCost = currentCost + dist[path[index - 1]][i];
            if (nextCost &gt;= bestCost) {
                continue;
            }

            // 做出选择
            visited[i] = true;
            path[index] = i;
            currentCost = nextCost;

            // 递归进入下一层
            backtrack(index + 1);

            // 撤销选择
            currentCost -= dist[path[index - 1]][i];
            visited[i] = false;
        }
    }

    public static void main(String[] args) {
        // 用 ∞ 表示不存在的边，这里城市 0..3 对应 A..D
        int INF = Integer.MAX_VALUE &gt;&gt; 1; // 取半是为了防止相加溢出
        int[][] matrix = {
            {INF,  10,  15,  20},
            { 10, INF,  35,  25},
            { 15,  35, INF,  30},
            { 20,  25,  30, INF}
        };

        int result = solve(matrix);

        System.out.println("最短路程：" + result);
        System.out.print("最优路线：");
        for (int i = 0; i &lt;= n; i++) {
            System.out.print((char) ('A' + bestPath[i]));
            if (i != n) {
                System.out.print(" -&gt; ");
            }
        }
        System.out.println();
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>最短路程：80
最优路线：A -&gt; B -&gt; D -&gt; C -&gt; A
</code></pre>
<p>:::warning[INF 取值的注意点]
代码里用 <code>Integer.MAX_VALUE &gt;&gt; 1</code> 表示无穷大，是为了防止两个 <code>INF</code> 相加时溢出变成负数，从而被误判为"更优"。处理带权图时这是常见的细节坑。
:::</p>
<hr />
<h2>7. 复杂度分析（回溯法）</h2>
<ul>
<li><strong>时间复杂度</strong>：最坏情况 $O((n-1)!)$。</li>
<li><strong>空间复杂度</strong>：$O(n)$，包括 <code>path</code>、<code>bestPath</code>、<code>visited</code> 数组和递归调用栈。</li>
</ul>
<p>为什么是阶乘？因为排列树在第 $1$ 层有 $n-1$ 个分支，第 $2$ 层每个节点最多 $n-2$ 个分支，依次递减，叶子节点数为：</p>
<p>$$
(n-1) \times (n-2) \times \cdots \times 1 = (n-1)!
$$</p>
<p>实际运行中，"代价超过最优即剪枝"会砍掉大量分支，但最坏情况仍是阶乘级。对这类 NP-Hard 问题，精确算法通常需要配合更强的剪枝或状态压缩，才能把可处理规模向前推进。</p>
<hr />
<h2>8. 用下界剪枝：分支限界思想</h2>
<p>第 6 节的剪枝只用到了"当前已走距离"，这个界太松——它完全不考虑剩余城市还要花多少距离。<strong>分支限界法（Branch and Bound）</strong> 的核心是：给每个部分解估算一个<strong>更紧的下界</strong>，下界越大，越能提前剪枝。</p>
<h3>8.1 什么是好的下界？</h3>
<p>下界必须满足：</p>
<p>$$
\text{下界} \le \text{从该部分解出发的任意完整环游的真实最短路程}
$$</p>
<p>我们要找的是更短路线。</p>
<p>若某个分支的下界已经 $\ge \text{bestCost}$，说明它再往下搜也不可能得到更短结果，可以直接剪掉。</p>
<h3>8.2 矩阵归约法（经典下界）</h3>
<p>一种经典且较紧的下界来自<strong>距离矩阵归约（matrix reduction）</strong>，思路是：</p>
<blockquote>
<p>一条环游从每个城市<strong>出发一次</strong>，也<strong>到达一次</strong>。因此从距离矩阵每一行减去该行最小值、每一列减去该列最小值，所减掉的总量，是任何环游都必须至少付出的代价。</p>
</blockquote>
<p>先用一个生活例子理解“下界”。</p>
<p>假设你要买 4 样东西，每样都必须买一次。你还不知道最后具体在哪家店买，但已经知道：</p>
<pre><code>第 1 样东西最便宜也要 10 元
第 2 样东西最便宜也要 10 元
第 3 样东西最便宜也要 15 元
第 4 样东西最便宜也要 20 元
</code></pre>
<p>那你可以先得到一个肯定成立的结论：</p>
<pre><code>总价至少是 10 + 10 + 15 + 20 = 55 元
</code></pre>
<p>最后真正花的钱可能是 80、95 或 120，但不可能低于 55。这个“至少要花 55 元”就是下界。</p>
<p>矩阵归约法在 TSP 里做的事情类似：虽然还不知道最终路线怎么走，但每个城市至少要选一条离开的边，也至少要选一条进入的边。于是可以先估算：无论最后怎么绕，这条环游至少要花多少钱。</p>
<p>具体步骤：</p>
<ol>
<li><strong>行归约</strong>：每一行减去本行最小元素，记录所有行最小值之和 $r_1$。</li>
<li><strong>列归约</strong>：对行归约后的矩阵，每一列减去本列最小元素，记录所有列最小值之和 $r_2$。</li>
<li><strong>下界</strong>：</li>
</ol>
<p>$$
\text{下界} = r_1 + r_2
$$</p>
<p>为什么这是合法下界？因为环游在每一行恰好选一个元素（从该城市出发的一条边），在每一列也恰好选一个元素（到达该城市的一条边）。每行至少贡献行最小值，每列至少贡献列最小值，所以减掉的总量是任何环游的下界。</p>
<h3>8.3 用同一个例子算下界</h3>
<p>原始矩阵：</p>
<pre><code>      A    B    C    D
A     ∞   10   15   20
B    10    ∞   35   25
C    15   35    ∞   30
D    20   25   30    ∞
</code></pre>
<p><strong>第一步：行归约。</strong> 各行最小值为 $10,\ 10,\ 15,\ 20$，和为 $r_1 = 55$。每行减去最小值：</p>
<pre><code>      A    B    C    D
A     ∞    0    5   10
B     0    ∞   25   15
C     0   20    ∞   15
D     0    5   10    ∞
</code></pre>
<p><strong>第二步：列归约。</strong> 各列最小值为 $0,\ 0,\ 5,\ 10$，需要减的只有 C 列（减 $5$）和 D 列（减 $10$），和为 $r_2 = 15$。归约后：</p>
<pre><code>      A    B    C    D
A     ∞    0    0    0
B     0    ∞   20    5
C     0   20    ∞    5
D     0    0    5    ∞
</code></pre>
<p><strong>下界</strong>：</p>
<p>$$
\text{下界} = r_1 + r_2 = 55 + 15 = 70
$$</p>
<p>而本例最优解是 $80$，$70 \le 80$，下界成立。</p>
<p>:::tip[下界越紧，剪枝越强]
朴素剪枝只看当前已走距离 <code>currentCost</code>，相当于把“剩下还要走的路”估成 $0$。这个估计一定安全，但太乐观。</p>
<p>矩阵归约会给剩余部分也估一个“至少还要花多少”的下界。把它加到 <code>currentCost</code> 上，就能得到更接近真实总路程的判断：</p>
<p>$$
\text{当前已走距离} + \text{剩余部分下界}
$$</p>
<p>如果这个值已经不小于 <code>bestCost</code>，说明这条分支就算后面每一步都走得很理想，也不可能超过当前最优解，可以提前剪掉。
:::</p>
<h3>8.4 在搜索中维护下界</h3>
<p>实际搜索时，每个节点都要回答一个问题：</p>
<blockquote>
<p>我已经走到这里了，如果把剩下的路用最乐观的方式估一下，这条分支还有机会超过当前最优解吗？</p>
</blockquote>
<p>例如现在已经找到一条完整路线，<code>bestCost = 80</code>。搜索到某个部分路径时：</p>
<pre><code>A → D
currentCost = 20
</code></pre>
<p>如果用矩阵归约估出“剩下的城市至少还要花 65”，那么这条分支的乐观总成本就是：</p>
<pre><code>20 + 65 = 85
</code></pre>
<p>由于 $85 \ge 80$，即使后面每一步都按最理想情况走，也不可能得到更短路线，所以这整个分支可以直接剪掉。</p>
<p>维护这个下界时，每选定一条边（比如确定 $A \rightarrow B$），就把对应行和列从矩阵中划掉，并对剩余子矩阵重新归约，更新下界增量。这样每个搜索节点都有一个反映“已确定路径 + 剩余最乐观估计”的下界。</p>
<p>完整的优先队列式分支限界会把这些搜索节点放进堆里，每次优先扩展下界最小、最有希望的节点。它效果更好，但实现也更复杂。本文的回溯代码已经展示了“界 + 剪枝”的核心思想：把简单的 <code>currentCost</code> 剪枝换成“当前成本 + 剩余下界”剪枝，就进入了分支限界法的思路。</p>
<hr />
<h2>9. 其他常见方法概览</h2>
<p>TSP 是个研究了几十年的问题，精确法和近似/启发式方法都很多。</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>类型</th>
<th>时间复杂度</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td>回溯法</td>
<td>精确</td>
<td>$O((n-1)!)$</td>
<td>实现简单，适合小规模</td>
</tr>
<tr>
<td>分支限界法</td>
<td>精确</td>
<td>最坏仍指数级</td>
<td>下界紧，剪枝强，常用于中等规模</td>
</tr>
<tr>
<td>动态规划（Held-Karp）</td>
<td>精确</td>
<td>$O(n^2 \cdot 2^n)$</td>
<td>用状态压缩，$n \le 20$ 左右可行</td>
</tr>
<tr>
<td>最近邻等贪心法</td>
<td>近似</td>
<td>$O(n^2)$</td>
<td>快但不保证最优，可作为初始解</td>
</tr>
<tr>
<td>模拟退火 / 遗传算法 / 蚁群</td>
<td>启发式</td>
<td>依赖迭代次数</td>
<td>大规模近似求解，结果不稳定</td>
</tr>
</tbody>
</table>
<p>:::note[Held-Karp 动态规划]
用 $dp[S][j]$ 表示已访问城市集合为 $S$、当前位于城市 $j$ 的最短距离。状态数为 $O(n \cdot 2^n)$，转移 $O(n)$，总复杂度 $O(n^2 \cdot 2^n)$。它把阶乘级降到了指数级，是精确解法里最快的之一，但 $n$ 一旦超过 $20$ 也很难承受。
:::</p>
<p>选择哪种方法，取决于问题规模和精度要求：</p>
<ul>
<li>$n \le 10$：回溯法足矣。</li>
<li>$n \le 20$：动态规划 Held-Karp。</li>
<li>$n \le 50$ 左右：分支限界（下界要设计得好）。</li>
<li>$n$ 更大：近似算法或元启发式算法。</li>
</ul>
<hr />
<h2>10. 总结</h2>
<p>旅行售货员问题的核心可以概括为：</p>
<blockquote>
<p>在排列树上搜索所有环游，用下界剪掉无望分支，找到总路程最短的回路。</p>
</blockquote>
<p>核心内容如下：</p>
<ul>
<li>TSP 的解空间是排列树，固定起点后方案数上界为 $(n-1)!$。</li>
<li>回溯法通过"做出选择 → 递归 → 撤销选择"遍历排列树。</li>
<li>最朴素的剪枝：当前已走距离 $\ge$ 已知最优，即剪掉。</li>
<li>分支限界法用更紧的下界（如矩阵归约）替代朴素界，剪枝更强。</li>
<li>TSP 是 NP-Hard 问题，精确算法都是指数级以上，大规模只能靠近似。</li>
</ul>
<p>口诀：</p>
<blockquote>
<p>固定起点排顺序，回溯遍历排列树；代价超界即剪枝，归约下界更强力。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>N 皇后</title>
    <link href="https://blog.yoake.dev/posts/algorithm/nqueens/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/nqueens/</id>
    <published>2026-06-14T12:53:34.000Z</published>
    <updated>2026-06-14T12:53:34.000Z</updated>
    <summary>经典回溯问题：N 皇后的图解与详细解析，深入理解深度优先搜索中状态重置的本质。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是 N 皇后问题？</h2>
<p>N 皇后问题是计算机科学中的经典问题，也是学习回溯算法的必修课。</p>
<p>题目要求将 $n$ 个皇后放置在 $n \times n$ 的棋盘上，使得任意两个皇后都不能互相攻击。</p>
<p>在国际象棋中，皇后可以沿行、列和两条对角线方向攻击，因此任何两个皇后都不能处于同一行、同一列或同一条对角线上。</p>
<hr />
<h2>2. 核心思路：回溯算法</h2>
<p>因为我们需要找到所有可能的解，而每一步都面临多种选择，一旦发现当前选择导致后面无法放置，就需要撤销选择、回到上一步尝试其他位置。这种"不断尝试，不行就退回"的思想就是回溯算法。</p>
<p>算法的基本步骤如下：</p>
<ol>
<li>从第 $0$ 行开始，逐行放置皇后。</li>
<li>在当前行中，遍历每一列，检查在位置 $(row, col)$ 放置皇后是否合法。</li>
<li>如果合法，放置皇后并标记该列、两条对角线已被占用，然后递归处理下一行。</li>
<li>当所有行都处理完毕，就找到了一个合法方案，将其记录下来。</li>
<li>递归返回后，撤销当前位置的占用状态，尝试当前行的下一列。</li>
</ol>
<p>:::tip[回溯的核心口诀]
做出选择 $\rightarrow$ 递归探索下一层 $\rightarrow$ 撤销选择（恢复现场）。
:::</p>
<hr />
<h2>3. 如何判断位置是否合法？</h2>
<p>在 $n \times n$ 的棋盘上，最直观的方法是每次都遍历检查当前位置的同一列和两条对角线是否有其他皇后，但这会耗时较高。</p>
<p>为了将判断的时间复杂度降为 $O(1)$，我们可以使用布尔数组来记录已被占用的列和对角线。</p>
<p>在棋盘坐标系 $(row, col)$ 中，三种冲突的数学特征如下：</p>
<ul>
<li><strong>同一列</strong>：$col$ 的值相同。</li>
<li><strong>主对角线（↘方向）</strong>：同一条对角线上的所有点，$row - col$ 的值相同。</li>
<li><strong>副对角线（↙方向）</strong>：同一条对角线上的所有点，$row + col$ 的值相同。</li>
</ul>
<p>以 $4 \times 4$ 的棋盘为例：</p>
<ul>
<li><strong>主对角线</strong>：$(0,0)$、$(1,1)$、$(2,2)$ 处于同一条主对角线，它们的 $row - col$ 都等于 $0$。</li>
<li><strong>副对角线</strong>：$(0,3)$、$(1,2)$、$(2,1)$ 处于同一条副对角线，它们的 $row + col$ 都等于 $3$。</li>
</ul>
<h3>3.1 对角线数组的下标映射</h3>
<p>由于数组下标不能为负数，我们需要对计算结果进行处理：</p>
<ul>
<li><strong>主对角线（$row - col$）</strong>：取值范围是 $-(n-1)$ 到 $n-1$。统一加上 $n-1$ 后，映射为 $row - col + n - 1$，取值范围变为 $0$ 到 $2n-2$。</li>
<li><strong>副对角线（$row + col$）</strong>：取值范围已经是 $0$ 到 $2n-2$，可以直接用作数组下标。</li>
</ul>
<p>因此，<code>diagonals1</code> 和 <code>diagonals2</code> 数组的长度都是 $2n-1$。</p>
<p>:::note
通过三个布尔数组 <code>cols</code>、<code>diagonals1</code> 和 <code>diagonals2</code>，就能在 $O(1)$ 的时间内判断某个位置是否可以放置皇后。
:::</p>
<hr />
<h2>4. 以 n = 4 为例走一遍流程</h2>
<p>以 $n = 4$ 为例，用 ASCII 棋盘展示回溯的完整搜索过程。<code>Q</code> 表示皇后，<code>.</code> 表示空位。</p>
<p><strong>第 0 行</strong>，在 $col=0$ 放置：</p>
<pre><code>Q . . .
. . . .
. . . .
. . . .
</code></pre>
<p><strong>第 1 行</strong>，$col=0$ 冲突（同列），$col=1$ 冲突（主对角线），$col=2$ 合法，放置：</p>
<pre><code>Q . . .
. . Q .
. . . .
. . . .
</code></pre>
<p><strong>第 2 行</strong>，$col=0$ 冲突（副对角线），$col=1$ 冲突（同列），$col=2$ 冲突（同列），$col=3$ 冲突（主对角线）。四列全部冲突，无法放置，回溯。</p>
<p>撤销第 1 行 $col=2$，尝试 $col=3$，合法，放置：</p>
<pre><code>Q . . .
. . . Q
. . . .
. . . .
</code></pre>
<p><strong>第 2 行</strong>，$col=0$ 冲突（同列），$col=1$ 合法，放置：</p>
<pre><code>Q . . .
. . . Q
. Q . .
. . . .
</code></pre>
<p><strong>第 3 行</strong>，四列全部冲突，无法放置，回溯。</p>
<p>撤销第 2 行 $col=1$，无更多列可尝试，继续回溯。撤销第 1 行 $col=3$，无更多列可尝试，继续回溯。</p>
<p><strong>第 0 行</strong>，撤销 $col=0$，尝试 $col=1$，合法，放置：</p>
<pre><code>. Q . .
. . . .
. . . .
. . . .
</code></pre>
<p>沿这条分支继续搜索，最终找到一个合法解：</p>
<pre><code>. Q . .
. . . Q
Q . . .
. . Q .
</code></pre>
<p>即 <code>[.Q.., ...Q, Q..., ..Q.]</code>。整个搜索过程就是在状态树上做深度优先遍历，走不通就回头换路。$n = 4$ 时共有 $2$ 个解。</p>
<hr />
<h2>5. Java 实现</h2>
<pre><code>import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    List&lt;List&lt;String&gt;&gt; res = new ArrayList&lt;&gt;();

    public List&lt;List&lt;String&gt;&gt; solveNQueens(int n) {
        // 棋盘初始化，全填充为 '.'
        char[][] board = new char[n][n];
        for (char[] row : board) {
            Arrays.fill(row, '.');
        }

        // 记录列和两条对角线是否被占用
        boolean[] cols = new boolean[n];
        boolean[] diagonals1 = new boolean[2 * n - 1]; // 主对角线 row - col + n - 1
        boolean[] diagonals2 = new boolean[2 * n - 1]; // 副对角线 row + col

        backtrack(board, 0, n, cols, diagonals1, diagonals2);
        return res;
    }

    private void backtrack(char[][] board, int row, int n,
                           boolean[] cols, boolean[] diagonals1, boolean[] diagonals2) {
        // 所有行都已放置完毕，找到一个合法解
        if (row == n) {
            res.add(generateBoard(board));
            return;
        }

        // 遍历当前行的每一列，尝试放置皇后
        for (int col = 0; col &lt; n; col++) {
            int diag1 = row - col + n - 1;
            int diag2 = row + col;

            // 如果该位置的列或对角线已被占用，跳过
            if (cols[col] || diagonals1[diag1] || diagonals2[diag2]) {
                continue;
            }

            // 做出选择：放置皇后，更新占用状态
            board[row][col] = 'Q';
            cols[col] = true;
            diagonals1[diag1] = true;
            diagonals2[diag2] = true;

            // 递归进入下一行
            backtrack(board, row + 1, n, cols, diagonals1, diagonals2);

            // 撤销选择：恢复棋盘和占用状态
            board[row][col] = '.';
            cols[col] = false;
            diagonals1[diag1] = false;
            diagonals2[diag2] = false;
        }
    }

    // 将字符数组转为题目要求的 List&lt;String&gt; 格式
    private List&lt;String&gt; generateBoard(char[][] board) {
        List&lt;String&gt; list = new ArrayList&lt;&gt;();
        for (char[] row : board) {
            list.add(new String(row));
        }
        return list;
    }
}
</code></pre>
<hr />
<h2>6. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(N!)$。</li>
<li><strong>空间复杂度</strong>：$O(N)$。递归调用栈深度为 $N$，三个布尔数组 <code>cols</code>、<code>diagonals1</code> 和 <code>diagonals2</code> 的长度均为 $O(N)$。</li>
</ul>
<p>时间复杂度为什么是 $O(N!)$ 而不是 $O(N^2)$？表面上看，回溯函数里只有一个 <code>for</code> 循环遍历 $N$ 列，似乎每层只做 $O(N)$ 的工作。但实际上，<code>dfs(0)</code> 调用 <code>dfs(1)</code>，<code>dfs(1)</code> 调用 <code>dfs(2)</code>，形成了一棵搜索树：</p>
<pre><code>dfs(0)
 ├── dfs(1)
 │    ├── dfs(2)
 │    │    ├── dfs(3)
 │    │    └── ...
 │    └── ...
 └── ...
</code></pre>
<p>第 0 层有 $N$ 个分支，第 1 层每个节点最多 $N-1$ 个分支（已用掉一列），第 2 层最多 $N-2$ 个分支，以此类推。搜索树的叶子节点数量上界为 $N \times (N-1) \times (N-2) \times \cdots \times 1 = N!$。实际搜索中由于对角线冲突的剪枝，状态树远小于 $N!$，但最坏情况下仍为 $O(N!)$。</p>
<hr />
<h2>8. 总结</h2>
<p>N 皇后问题的核心是：</p>
<blockquote>
<p>按行放置，逐列尝试；利用数组记录冲突，通过回溯不断试错。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>数值随机化算法：随机采样计算定积分</title>
    <link href="https://blog.yoake.dev/posts/algorithm/numericalrandomizedalgorithm/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/numericalrandomizedalgorithm/</id>
    <published>2026-06-10T13:20:53.000Z</published>
    <updated>2026-06-10T13:20:53.000Z</updated>
    <summary>通过随机投点法和平均值法两个例子，理解数值随机化算法如何利用随机采样思想近似求解定积分。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是数值随机化算法</h2>
<p>数值随机化算法是一类利用随机采样来求解数值计算问题的算法。</p>
<p>它通常用于求解一些很难直接精确计算的问题，例如估算常数、计算定积分、估算面积、求解复杂模型的近似值等。核心思想是：</p>
<blockquote>
<p>当精确求解成本很高，甚至难以实现时，可以通过大量随机样本来逼近真实结果。</p>
</blockquote>
<p>数值随机化算法一般不追求"一步得到精确答案"，而是追求采样次数越多，估算结果越接近真实值。</p>
<p>:::tip[特点]
数值随机化算法求的是近似解而非精确解；精度与采样次数正相关，采样越多结果越稳定；特别适合高维积分、复杂面积估算、物理系统模拟等传统方法难以精确求解的问题。
:::</p>
<hr />
<h2>2. 随机化算法中的四种类型</h2>
<p>随机化算法通常可以分为四类（前两类在<a href="/posts/algorithm/montecarloalgorithm/">蒙特卡罗算法</a>一文中已经介绍过）：</p>
<p><strong>拉斯维加斯算法</strong>：结果一定正确，但运行时间不确定。</p>
<p><strong>蒙特卡罗算法</strong>：运行时间可控，但结果可能带有误差。</p>
<p><strong>舍伍德算法</strong>：引入随机性不是为了近似答案，而是为了消除最坏输入对算法性能的影响，使算法在不同输入下表现更均衡。</p>
<p><strong>数值随机化算法</strong>：更关注用随机采样求数值近似解。它和蒙特卡罗方法关系很近，很多数值随机化算法就是蒙特卡罗方法在数值计算中的应用。</p>
<hr />
<h2>3. 经典问题：随机采样计算定积分</h2>
<p>定积分是数值随机化算法中的经典应用。</p>
<p>假设有一个连续函数 $f(x)$，要求它在区间 $[a, b]$ 上的定积分：</p>
<p>$$
I = \int_a^b f(x) , dx
$$</p>
<p>从几何角度看，如果 $f(x) \geq 0$，那么这个定积分可以理解为：曲线 $y = f(x)$、$x$ 轴、直线 $x = a$ 和 $x = b$ 围成区域的面积。</p>
<p>如果这个面积很难直接计算，就可以通过随机采样来估算它。下面介绍两种经典方法。</p>
<hr />
<h2>4. 随机投点法</h2>
<p>随机投点法的思想和蒙特卡罗估算 $\pi$ 很像。</p>
<h3>4.1 推导过程</h3>
<p>假设函数 $f(x)$ 在区间 $[a, b]$ 上满足 $0 \leq f(x) \leq M$，那么可以构造一个矩形区域 $[a, b] \times [0, M]$，面积为：</p>
<p>$$
S_{\text{矩形}} = (b - a) \times M
$$</p>
<p>在这个矩形中随机生成 $N$ 个点，如果有 $K$ 个点落在曲线下方（即满足 $y_i \leq f(x_i)$），那么落点比例近似等于面积比例：</p>
<p>$$
\frac{K}{N} \approx \frac{I}{S_{\text{矩形}}} = \frac{I}{(b - a) \times M}
$$</p>
<p>由此得到估算公式：</p>
<p>$$
I \approx (b - a) \times M \times \frac{K}{N}
$$</p>
<h3>4.2 具体例子</h3>
<p>例如函数 $f(x) = 1 - x^2$，在区间 $[0, 1]$ 上有：</p>
<p>$$
0 \leq f(x) \leq 1
$$</p>
<p>所以可以取 $M = 1$，矩形区域就是 $[0, 1] \times [0, 1]$。如果随机投点后，有 $666000$ 个点落在曲线下方，而总共采样 $1000000$ 个点，那么积分近似为：</p>
<p>$$
I \approx 1 \times 1 \times \frac{666000}{1000000} = 0.666
$$</p>
<p>这个结果就已经很接近真实值 $\frac{2}{3}$ 了。</p>
<h3>4.3 直观理解</h3>
<p>随机投点法把积分问题转化为几何问题：</p>
<blockquote>
<p>积分值 = 矩形面积 × 落点比例</p>
</blockquote>
<p>矩形中均匀撒点，落在曲线下方的点越多，说明曲线下方面积占矩形面积的比例越大。当采样次数足够多时，这个比例会稳定地趋近于真实面积比。</p>
<hr />
<h2>5. 平均值法</h2>
<p>平均值法比随机投点法更常用，也通常更稳定。</p>
<h3>5.1 推导过程</h3>
<p>在区间 $[a, b]$ 中随机生成 $N$ 个点 $x_1, x_2, \ldots, x_N$，计算对应的函数值 $f(x_1), f(x_2), \ldots, f(x_N)$。</p>
<p>这些函数值的平均数可以近似表示函数在区间上的平均高度：</p>
<p>$$
\bar{f} = \frac{1}{N} \sum_{i=1}^{N} f(x_i)
$$</p>
<p>而定积分可以理解为"区间长度 × 平均高度"，所以：</p>
<p>$$
I \approx (b - a) \times \frac{1}{N} \sum_{i=1}^{N} f(x_i)
$$</p>
<p>这就是平均值法的核心公式。</p>
<h3>5.2 直观理解</h3>
<p>平均值法把积分问题转化为统计问题：</p>
<blockquote>
<p>积分值 = 区间长度 × 平均函数值</p>
</blockquote>
<p>不需要构造矩形，也不需要生成二维随机点，只需要在一维区间上采样函数值即可。实现更简单，收敛通常也更快。</p>
<hr />
<h2>6. 示例：计算定积分</h2>
<p>以这个积分为例：</p>
<p>$$
I = \int_0^1 (1 - x^2) , dx
$$</p>
<p>其中 $f(x) = 1 - x^2$，在区间 $[0, 1]$ 上满足 $0 \leq f(x) \leq 1$。</p>
<p>先求解析解作为对照：</p>
<p>$$
\int (1 - x^2) , dx = x - \frac{x^3}{3}
$$</p>
<p>代入上下限：</p>
<p>$$
I = \left(1 - \frac{1}{3}\right) - 0 = \frac{2}{3} \approx 0.66667
$$</p>
<h3>6.1 Java 代码实现</h3>
<pre><code>import java.util.Random;

public class RandomSamplingIntegral {
    private static final Random RANDOM = new Random();

    // f(x) = 1 - x^2
    private static double f(double x) {
        return 1 - x * x;
    }

    // 方法一：随机投点法
    public static double integralByDarts(int n) {
        if (n &lt;= 0) {
            throw new IllegalArgumentException("采样次数必须大于 0");
        }

        double a = 0.0;
        double b = 1.0;
        double m = 1.0;

        long count = 0;

        for (int i = 0; i &lt; n; i++) {
            // 在区间 [a, b] 中均匀生成 x
            double x = a + RANDOM.nextDouble() * (b - a);

            // 在区间 [0, m] 中均匀生成 y
            double y = RANDOM.nextDouble() * m;

            // 如果点在曲线下方，就统计到曲线下方点数中
            if (y &lt;= f(x)) {
                count++;
            }
        }

        double rectArea = (b - a) * m;
        return rectArea * count / n;
    }

    // 方法二：平均值法
    public static double integralByAverage(int n) {
        if (n &lt;= 0) {
            throw new IllegalArgumentException("采样次数必须大于 0");
        }

        double a = 0.0;
        double b = 1.0;

        double sum = 0.0;

        for (int i = 0; i &lt; n; i++) {
            // 在区间 [a, b] 中均匀生成 x
            double x = a + RANDOM.nextDouble() * (b - a);

            // 累加每个采样点对应的函数值
            sum += f(x);
        }

        return (b - a) * sum / n;
    }

    public static void main(String[] args) {
        int samples = 1_000_000;

        System.out.printf("理论解析解：%.5f%n", 2.0 / 3.0);
        System.out.printf("随机投点法：%.5f%n", integralByDarts(samples));
        System.out.printf("平均值法：%.5f%n", integralByAverage(samples));
    }
}
</code></pre>
<p>可能的输出：</p>
<pre><code>理论解析解：0.66667
随机投点法：0.66691
平均值法：0.66654
</code></pre>
<p>由于使用了随机数，每次运行结果可能不同。但只要采样次数足够大，两种方法的结果通常都会接近理论值 $\frac{2}{3}$。</p>
<hr />
<h2>7. 两种方法的对比</h2>
<table>
<thead>
<tr>
<th>方法</th>
<th>随机投点法</th>
<th>平均值法</th>
</tr>
</thead>
<tbody>
<tr>
<td>思路</td>
<td>在矩形中随机撒点，统计落在曲线下方的比例</td>
<td>随机采样函数值，用平均函数值估算积分</td>
</tr>
<tr>
<td>核心公式</td>
<td><code>I ≈ 矩形面积 × 落点比例</code></td>
<td><code>I ≈ 区间长度 × 平均函数值</code></td>
</tr>
<tr>
<td>优点</td>
<td>几何意义直观，容易理解</td>
<td>实现简单，通常更稳定</td>
</tr>
<tr>
<td>缺点</td>
<td>需要知道函数上界 $M$，波动可能较大</td>
<td>需要理解"平均高度 × 区间长度"的思想</td>
</tr>
</tbody>
</table>
<p>对于一维定积分来说，平均值法通常更加简洁实用。随机投点法更适合用来帮助理解"概率比例 ≈ 面积比例"这一思想，而平均值法更适合实际计算。</p>
<p>两种方法的时间复杂度都是 $O(N)$（$N$ 为采样次数），空间复杂度都是 $O(1)$。这里的 $N$ 不是输入规模，而是我们主动设置的采样次数。</p>
<hr />
<h2>8. 总结</h2>
<p>数值随机化算法的核心是：</p>
<blockquote>
<p>用随机采样的统计结果，去逼近某个数值答案。</p>
</blockquote>
<p>在定积分问题中，随机采样有两种经典方式：</p>
<ol>
<li><strong>随机投点法</strong>：把积分看成面积，通过落点比例估算面积。</li>
<li><strong>平均值法</strong>：把积分看成区间长度乘以平均函数值。</li>
</ol>
<p>:::tip[口诀]
随机投点看面积比例，平均值法看平均高度。
:::</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>蒙特卡罗算法：用随机撒点估算圆周率</title>
    <link href="https://blog.yoake.dev/posts/algorithm/montecarloalgorithm/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/montecarloalgorithm/</id>
    <published>2026-06-10T13:02:01.000Z</published>
    <updated>2026-06-10T13:02:01.000Z</updated>
    <summary>通过估算圆周率 π 的经典例子，理解蒙特卡罗算法的随机采样思想，以及它和拉斯维加斯算法的区别。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是蒙特卡罗算法</h2>
<p>蒙特卡罗算法（Monte Carlo Algorithm）是一类基于随机采样和概率统计的算法。</p>
<p>它的核心思想是：</p>
<blockquote>
<p>当一个问题很难直接精确求解时，可以通过大量随机实验，用统计结果去近似真实答案。</p>
</blockquote>
<p>简单来说，蒙特卡罗算法的流程是：</p>
<pre><code>随机生成样本 → 统计样本结果 → 用样本比例估计整体结果
</code></pre>
<p>它不一定每次都能得到完全精确的答案，但随着随机采样次数增加，结果通常会越来越接近真实值。</p>
<hr />
<h2>2. 随机化算法的两种类型</h2>
<p>在计算机科学中，随机化算法通常可以分为两类：<strong>蒙特卡罗算法</strong>和<strong>拉斯维加斯算法</strong>。</p>
<p><strong>蒙特卡罗算法</strong>的特点是：运行时间通常是确定或有限的，但结果可能存在误差。</p>
<p>例如用随机点估算圆周率 $\pi$，如果规定生成 $1000000$ 个点，那么采样次数是固定的；但估算出的 $\pi$ 只是近似值。通过增加随机采样次数，可以让误差概率不断降低，使结果越来越接近真实值。</p>
<p><strong>拉斯维加斯算法</strong>的特点是：运行时间带有随机性，但只要算法结束，得到的结果一定正确。</p>
<p>例如随机化快速排序，随机选择基准值 <code>pivot</code> 会影响划分效果，从而影响运行时间；但排序完成后，结果一定是正确的。</p>
<p>:::tip[区分]
蒙特卡罗算法保证时间可控，算的是精度；拉斯维加斯算法保证答案正确，赌的是时间。
:::</p>
<hr />
<h2>3. 经典例子：估算圆周率 π</h2>
<p>蒙特卡罗算法最经典的入门例子，就是用随机点估算圆周率 $\pi$。</p>
<h3>3.1 几何模型</h3>
<p>假设有一个边长为 $2$ 的正方形，中心在原点 $(0, 0)$，范围为 $x \in [-1, 1]$，$y \in [-1, 1]$。</p>
<p>在这个正方形中画一个内切圆，圆心在原点，半径为 $1$。</p>
<p>接下来的问题是：不直接使用圆周率公式，也不调用语言内置的 $\pi$ 常量，如何通过在正方形内随机撒点来估算 $\pi$？</p>
<h3>3.2 数学推导</h3>
<p>正方形边长为 $2$，面积为：</p>
<p>$$
S_{\text{正方形}} = 2 \times 2 = 4
$$</p>
<p>内切圆半径为 $1$，面积为：</p>
<p>$$
S_{\text{圆}} = \pi r^2 = \pi \times 1^2 = \pi
$$</p>
<p>因此，圆面积和正方形面积的比值为：</p>
<p>$$
\frac{S_{\text{圆}}}{S_{\text{正方形}}} = \frac{\pi}{4}
$$</p>
<p>如果在正方形中随机且均匀地生成大量点，那么点落在圆内的概率，应该近似等于圆面积占正方形面积的比例：</p>
<p>$$
P(\text{点落在圆内}) \approx \frac{\pi}{4}
$$</p>
<p>这里的“均匀”很关键。如果随机点不是均匀分布的，样本比例就不能稳定地代表面积比例，估算结果也会失真。</p>
<p>假设一共随机生成 $total$ 个点，其中有 $inside$ 个点落在圆内，那么：</p>
<p>$$
\frac{inside}{total} \approx \frac{\pi}{4}
$$</p>
<p>两边同时乘以 $4$，得到蒙特卡罗估算 $\pi$ 的核心公式：</p>
<p>$$
\pi \approx 4 \times \frac{inside}{total}
$$</p>
<h3>3.3 如何判断点是否在圆内</h3>
<p>随机生成一个点 $(x, y)$，圆心在原点，半径为 $1$。根据圆的方程，判断条件为：</p>
<p>$$
x^2 + y^2 \le 1
$$</p>
<p>满足则在圆内，否则在圆外。例如：</p>
<p>$$
(0.3, 0.4)\text{：}0.3^2 + 0.4^2 = 0.25 \le 1 \quad \Rightarrow \quad \text{圆内}
$$</p>
<p>$$
(0.9, 0.9)\text{：}0.9^2 + 0.9^2 = 1.62 &gt; 1 \quad \Rightarrow \quad \text{圆外}
$$</p>
<hr />
<h2>4. 算法步骤</h2>
<p>根据上面的推导，代码只需要做三件事：</p>
<ol>
<li>在正方形内均匀生成随机点；</li>
<li>统计有多少点落在单位圆内；</li>
<li>用圆内点比例估算 $\pi$：</li>
</ol>
<p>$$
\pi \approx 4 \times \frac{inside}{n}
$$</p>
<hr />
<h2>5. Java 代码实现</h2>
<pre><code>import java.util.Random;

public class MonteCarloPi {
    public static double estimatePi(int n) {
        if (n &lt;= 0) {
            throw new IllegalArgumentException("采样次数必须大于 0");
        }

        Random random = new Random();
        long inside = 0;

        for (int i = 0; i &lt; n; i++) {
            // random.nextDouble() 生成 [0, 1) 之间的小数
            // 乘以 2 后变成 [0, 2)，再减去 1 后变成 [-1, 1)
            double x = random.nextDouble() * 2 - 1;
            double y = random.nextDouble() * 2 - 1;

            // 判断点是否落在单位圆内
            if (x * x + y * y &lt;= 1) {
                inside++;
            }
        }

        return 4.0 * inside / n;
    }

    public static void main(String[] args) {
        System.out.println(estimatePi(1000));
        System.out.println(estimatePi(10000));
        System.out.println(estimatePi(100000));
        System.out.println(estimatePi(1000000));
    }
}
</code></pre>
<p>可能的输出：</p>
<pre><code>3.108
3.1464
3.13972
3.14188
</code></pre>
<p>由于使用了随机数，每次运行的结果可能都不完全一样。但通常来说，采样次数越多，结果越接近真实的 $\pi$。</p>
<hr />
<h2>6. 复杂度分析</h2>
<p>假设采样次数为 $n$。每次采样只需要生成两个随机数并进行一次判断，都是常数时间操作。</p>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$，$n$ 为采样次数。</li>
<li><strong>空间复杂度</strong>：$O(1)$，只使用了少量变量。</li>
</ul>
<p>这里的 $n$ 不是输入数组长度，而是我们主动设置的采样次数。采样次数越大，结果越稳定，但运行时间越长；采样次数越小，运行越快，但误差可能越大。这是蒙特卡罗算法中精度与效率的权衡。</p>
<hr />
<h2>7. 为什么采样越多越准确</h2>
<p>蒙特卡罗算法依赖的是概率统计中的大数定律。</p>
<p>当采样次数较少时，随机结果波动较大。例如只生成 $10$ 个点，可能恰好有 $7$ 个落在圆内：</p>
<p>$$
\pi \approx 4 \times \frac{7}{10} = 2.8
$$</p>
<p>误差很大。但如果生成 $1000000$ 个点，落在圆内的比例会更稳定，更接近真实概率 $\frac{\pi}{4}$，估算出的 $\pi$ 也会更接近真实值。</p>
<p>不过需要注意：</p>
<blockquote>
<p>采样次数增加，只能让结果更稳定，并不能保证每一次都精确等于真实答案。</p>
</blockquote>
<p>这也是蒙特卡罗算法和确定性算法的本质区别。</p>
<hr />
<h2>8. 总结</h2>
<p>蒙特卡罗算法的核心是：</p>
<blockquote>
<p>用大量随机实验的统计结果，去近似真实答案。</p>
</blockquote>
<p>在估算圆周率 $\pi$ 的例子中，我们利用了圆和正方形的面积比例 $\frac{\pi}{4}$，通过随机点落入圆内的比例来估算：</p>
<p>$$
\pi \approx 4 \times \frac{\text{圆内点数}}{\text{总点数}}
$$</p>
<p>蒙特卡罗算法思路简单、容易实现，尤其适合难以精确计算的问题，比如高维积分、复杂系统模拟和概率估计。但它的结果带有随机性，通常只能得到近似答案，且精度依赖采样次数。</p>
<p>:::tip[口诀]
随机撒点，统计比例，用概率逼近答案。
:::</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>第二大元素问题</title>
    <link href="https://blog.yoake.dev/posts/algorithm/secondlargestelement/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/secondlargestelement/</id>
    <published>2026-06-10T12:45:07.000Z</published>
    <updated>2026-06-10T12:45:07.000Z</updated>
    <summary>从两种题型出发，介绍一次遍历和分治两种解法，重点讲清分治合并时的候选淘汰思想。</summary>
    <content type="html"><![CDATA[<h2>1. 问题描述</h2>
<p>第二大元素问题是数组中的一个经典基础问题。</p>
<p>给定一个整数数组，要求找出其中的第二大元素。例如：</p>
<p>$$
nums = [3, 1, 5, 2, 4]
$$</p>
<p>数组中最大的元素是 $5$，第二大的元素是 $4$。</p>
<p>虽然这个问题看起来很简单，但实际写代码时要先弄清楚一个关键点：</p>
<blockquote>
<p>第二大元素到底是"严格第二大"，还是"排序后的第二个元素"？</p>
</blockquote>
<p>这两种题型的答案可能不一样。</p>
<hr />
<h2>2. 两种常见题型</h2>
<h3>2.1 严格第二大元素</h3>
<p>严格第二大指的是：</p>
<blockquote>
<p>在不同数值中，找出仅次于最大值的那个数。</p>
</blockquote>
<p>例如：</p>
<p>$$
nums = [5, 5, 4, 3]
$$</p>
<p>最大值是 $5$，但因为重复的 $5$ 不能再算作第二大，所以严格第二大是 $4$。</p>
<p>这种题型更关注"不同数字之间的大小关系"。</p>
<h3>2.2 排序意义上的第二大元素</h3>
<p>排序意义上的第二大指的是：</p>
<blockquote>
<p>将数组从大到小排序后，排在第二个位置的元素。</p>
</blockquote>
<p>例如：</p>
<p>$$
nums = [5, 5, 4, 3]
$$</p>
<p>从大到小排序后仍然是 $[5, 5, 4, 3]$，此时第二个位置是 $5$，所以排序意义上的第二大就是 $5$。</p>
<p>这种题型允许重复元素参与排名。</p>
<p>:::tip[小结]
写代码之前，先看清题目要求的是哪一种"第二大"。严格第二大需要去重，排序第二大允许重复。
:::</p>
<hr />
<h2>3. 一次遍历解法（严格第二大）</h2>
<p>对于严格第二大元素，可以维护两个变量：$max$ 记录当前最大值，$second$ 记录当前第二大值。</p>
<p>遍历数组时分三种情况：</p>
<ol>
<li>如果当前元素 $num &gt; max$，说明出现了新的最大值。此时原来的 $max$ 变成第二大，$num$ 成为新的最大值。</li>
<li>如果当前元素 $num &lt; max$，并且 $num &gt; second$，说明它不是最大值，但比当前第二大更大，于是更新 $second$。</li>
<li>如果当前元素等于 $max$，或者不超过 $second$，则跳过。</li>
</ol>
<p>用前面的例子走一遍：</p>
<p>$$
nums = [3, 1, 5, 2, 4]
$$</p>
<pre><code>初始：max = null, second = null

遇到 3：max = 3
遇到 1：second = 1
遇到 5：second = 3, max = 5
遇到 2：不更新（2 &lt; second = 3）
遇到 4：second = 4
</code></pre>
<p>最终第二大元素是 $4$。</p>
<p>Java 代码如下：</p>
<pre><code>class Solution {
    public int secondLargest(int[] nums) {
        if (nums == null || nums.length &lt; 2) {
            throw new IllegalArgumentException("数组长度至少为 2");
        }

        Integer max = null;
        Integer second = null;

        for (int num : nums) {
            // 出现新的最大值：原来的 max 退为第二大
            if (max == null || num &gt; max) {
                second = max;
                max = num;
            // num 必须严格小于 max，才能作为严格第二大的候选
            } else if (num &lt; max &amp;&amp; (second == null || num &gt; second)) {
                second = num;
            }
        }

        // 如果没有找到不同于 max 的数，说明严格第二大不存在
        if (second == null) {
            throw new IllegalArgumentException("不存在严格第二大元素");
        }

        return second;
    }
}
</code></pre>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$，只需要遍历一次数组。</li>
<li><strong>空间复杂度</strong>：$O(1)$，只用了两个额外变量。</li>
</ul>
<hr />
<h2>4. 分治法解第二大元素问题</h2>
<p>第二大元素问题也可以用分治法解决。</p>
<p>实际做题时，一次遍历通常更简单、空间也更优。这里继续讲分治法，主要是为了理解递归拆分、结果合并和候选淘汰思想。</p>
<p>分治法的核心思想是：</p>
<blockquote>
<p><strong>分</strong>：把原问题拆成规模更小的子问题；</p>
<p><strong>治</strong>：分别求解子问题；</p>
<p><strong>合</strong>：把子问题的答案合并成原问题的答案。</p>
</blockquote>
<p>放到第二大元素问题中，就是：</p>
<ol>
<li>把数组分成左右两半；</li>
<li>分别求出左半部分的最大值和第二大值；</li>
<li>分别求出右半部分的最大值和第二大值；</li>
<li>将左右两边的结果合并，得到整个数组的最大值和第二大值。</li>
</ol>
<h3>4.1 合并过程举例</h3>
<p>例如数组：</p>
<p>$$
nums = [3, 1, 5, 2, 4, 8, 6]
$$</p>
<p>可以拆成左右两部分：</p>
<p>$$
\text{左半部分：}[3, 1, 5]\text{，}\quad \text{右半部分：}[2, 4, 8, 6]
$$</p>
<p>左半部分的结果是 $left.max = 5$，$left.second = 3$。</p>
<p>右半部分的结果是 $right.max = 8$，$right.second = 6$。</p>
<p>现在要把左右两部分合并。因为 $right.max = 8 &gt; left.max = 5$，所以整体最大值是 $8$。</p>
<p>那么整体第二大元素只可能来自两个候选：$left.max = 5$ 和 $right.second = 6$。</p>
<p>所以整体第二大是：</p>
<p>$$
\max(5, 6) = 6
$$</p>
<h3>4.2 为什么不用考虑 left.second？</h3>
<p>在上面的例子中，$left.second = 3$ 为什么不需要参与全局第二大的竞争？</p>
<p>原因很直接：$left.second \le left.max$，而 $left.max$ 已经在竞争全局第二大了。一个比候选者还小的值，不可能成为全局第二大。</p>
<p>这就是一种"候选淘汰"思想：</p>
<blockquote>
<p>如果某个候选值已经被同一组里更大的值压住了，那么它就没有必要继续参与全局比较。</p>
</blockquote>
<hr />
<h2>5. 分治法 Java 实现</h2>
<pre><code>class Solution {
    static class Pair {
        int max;
        Integer second;

        Pair(int max, Integer second) {
            this.max = max;
            this.second = second;
        }
    }

    public int secondLargest(int[] nums) {
        if (nums == null || nums.length &lt; 2) {
            throw new IllegalArgumentException("数组长度至少为 2");
        }

        Pair ans = divide(nums, 0, nums.length - 1);

        if (ans.second == null) {
            throw new IllegalArgumentException("不存在严格第二大元素");
        }

        return ans.second;
    }

    private Pair divide(int[] nums, int left, int right) {
        // 单个元素区间：只有最大值，还不存在严格第二大
        if (left == right) {
            return new Pair(nums[left], null);
        }

        int mid = left + (right - left) / 2;

        // 分别求左右子数组的最大值和第二大值
        Pair leftPair = divide(nums, left, mid);
        Pair rightPair = divide(nums, mid + 1, right);

        // 合并左右子问题的结果
        return merge(leftPair, rightPair);
    }

    private Pair merge(Pair a, Pair b) {
        if (a.max &gt; b.max) {
            // a 贡献全局最大值，第二大只能来自 a.second 或 b.max
            return new Pair(a.max, maxNullable(a.second, b.max));
        } else if (a.max &lt; b.max) {
            // b 贡献全局最大值，第二大只能来自 a.max 或 b.second
            return new Pair(b.max, maxNullable(a.max, b.second));
        } else {
            // 两边最大值相同，不能把重复最大值当成严格第二大
            return new Pair(a.max, maxNullable(a.second, b.second));
        }
    }

    private Integer maxNullable(Integer a, Integer b) {
        // null 表示当前还没有严格第二大候选
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        return Math.max(a, b);
    }
}
</code></pre>
<p>这里用包装类型 <code>Integer</code> 表示 <code>second</code>，是为了区分两种情况：</p>
<ul>
<li><code>second == null</code>：当前区间里还不存在严格第二大元素。</li>
<li><code>second</code> 有值：这个值就是当前区间的严格第二大元素。</li>
</ul>
<p>不要用 <code>Integer.MIN_VALUE</code> 这类具体数值当作“不存在”的标记，因为数组元素本身也可能等于 <code>Integer.MIN_VALUE</code>。</p>
<p>最能体现分治思想的是这一段：</p>
<pre><code>Pair leftPair = divide(nums, left, mid);
Pair rightPair = divide(nums, mid + 1, right);

return merge(leftPair, rightPair);
</code></pre>
<p>先分别解决左右两个子数组的问题，再把两个子问题的答案合并成整体答案。</p>
<hr />
<h2>6. 复杂度分析</h2>
<p>每个元素在递归过程中都会被处理一次，合并时只进行常数次比较。所以时间复杂度为 $O(n)$。</p>
<p>由于递归调用会产生函数调用栈，递归深度为 $O(\log n)$，所以空间复杂度为 $O(\log n)$。</p>
<table>
<thead>
<tr>
<th>解法</th>
<th>时间复杂度</th>
<th>空间复杂度</th>
</tr>
</thead>
<tbody>
<tr>
<td>一次遍历</td>
<td>$O(n)$</td>
<td>$O(1)$</td>
</tr>
<tr>
<td>分治法</td>
<td>$O(n)$</td>
<td>$O(\log n)$</td>
</tr>
</tbody>
</table>
<p>可以看到，两种解法的时间复杂度都是 $O(n)$。一次遍历在空间上更优，而分治法的价值在于它展示了一种通用的问题拆解思路，这种思路可以推广到更复杂的问题中。</p>
<hr />
<h2>7. 总结</h2>
<p>第二大元素问题看似简单，但要先区分题型：</p>
<ul>
<li>如果要求"严格第二大"，重复的最大值不能算第二大。</li>
<li>如果要求"排序意义上的第二大"，重复元素可以参与排名。</li>
</ul>
<p>常规做法是一次遍历，维护 $max$ 和 $second$，时间 $O(n)$，空间 $O(1)$。</p>
<p>分治做法则是把数组拆成左右两部分，分别求出每部分的最大值和第二大值，再通过合并得到整体结果。合并时的关键是：</p>
<blockquote>
<p>哪一边贡献了全局最大值，就考虑那一边的第二大；另一边只考虑最大值。</p>
</blockquote>
<p>这就是第二大元素问题中分治合并的核心。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>区间调度</title>
    <link href="https://blog.yoake.dev/posts/algorithm/eraseoverlapintervals/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/eraseoverlapintervals/</id>
    <published>2026-06-10T10:26:32.000Z</published>
    <updated>2026-06-10T10:26:32.000Z</updated>
    <summary>用贪心思想解决 LeetCode 435 无重叠区间：按右端点排序，优先保留结束更早的区间，从而最小化移除数量。</summary>
    <content type="html"><![CDATA[<h2>1. 题目说明</h2>
<p>给定一组区间 <code>intervals</code>，返回<strong>需要移除的最少区间数量</strong>，使剩余区间互不重叠。</p>
<p>这里的边界规则很重要：<code>[1, 2]</code> 和 <code>[2, 3]</code> <strong>不算重叠</strong>。</p>
<h3>1.1 生活映射</h3>
<p>可以把每个区间看成一场活动。因为同一时间只能参加一场，所以我们要尽量保留更多互不冲突的活动。保留得越多，需要删除的就越少。</p>
<p>所以本题其实是在问：</p>
<blockquote>
<p>怎样保留最多的活动，才能让删掉的活动最少？</p>
</blockquote>
<hr />
<h2>2. 贪心思路</h2>
<p>这题可以转化为经典的<strong>区间调度</strong>问题。</p>
<p>我们不直接想“删掉哪些区间”，而是反过来想：</p>
<blockquote>
<p><strong>最多能保留多少个不重叠区间？</strong></p>
</blockquote>
<p>只要能算出“最多保留几个”，答案就很自然了：</p>
<p><strong>需要删除的数量 = 总区间数 - 最多保留数量</strong></p>
<p>做法很简单：</p>
<ol>
<li>按区间右端点从小到大排序。</li>
<li>依次扫描区间。</li>
<li>先记录上一个保留区间的右端点 <code>preEnd</code>。如果当前区间的左端点 <code>start &gt;= preEnd</code>，说明它和前一个区间不重叠，就保留它，并把 <code>preEnd</code> 更新为当前区间的右端点，方便判断下一个区间。</li>
<li>否则说明重叠了，跳过它。</li>
</ol>
<p>为什么要按右端点排序？</p>
<p>直觉上，结束得越早的区间，给后面的区间留下的空间越大。</p>
<p>更严格地说，在当前所有可选区间中，应该优先选择右端点最小的那个。</p>
<p>这个结论可以这样理解：</p>
<ul>
<li>假设某个最优解中，第一个被选择的区间是 <code>B</code></li>
<li>如果在当前还可选择的区间里，存在一个结束更早或相同的区间 <code>A</code></li>
<li>那就可以把 <code>B</code> 换成 <code>A</code></li>
</ul>
<p>由于 <code>A</code> 的右端点更小，它只会给后面的区间留出<strong>更多</strong>空间，不会更差。</p>
<p>所以在所有最优解里，一定存在一种方案，第一步选的是<strong>右端点最小</strong>的区间。</p>
<p>这也是为什么按右端点排序后，扫描会特别自然：</p>
<ul>
<li>排序后，第一个区间一定最早结束，先保留它</li>
<li>下一个能选的区间，就是第一个左端点 <code>&gt;= preEnd</code> 的区间</li>
<li>遇到重叠就跳过，遇到不重叠就保留</li>
</ul>
<p>所以这个贪心是成立的。</p>
<p>这里再把变量说清楚：</p>
<ul>
<li><code>count</code>：当前已经保留了多少个区间</li>
<li><code>preEnd</code>：上一个保留区间的右端点</li>
</ul>
<p>扫描时只需要比较当前区间的左端点和 <code>preEnd</code>。如果能接上，就保留当前区间，然后把 <code>preEnd</code> 更新成这个区间的右端点；接不上，就跳过。</p>
<blockquote>
<p>这和 LeetCode 452“用最少数量的箭引爆气球”很像，区别只是边界判断不同：<br />
435 题里 <code>start == preEnd</code> 不算重叠，452 题里要把这种情况看成仍然需要继续处理。</p>
</blockquote>
<hr />
<h2>3. 例子</h2>
<p>比如这些区间：</p>
<pre><code>[1, 2], [2, 3], [3, 4], [1, 3]
</code></pre>
<p>按右端点排序后，顺序其实是：</p>
<pre><code>[1, 2], [1, 3], [2, 3], [3, 4]
</code></pre>
<p>扫描过程如下：</p>
<ul>
<li>先看 <code>[1, 2]</code>，它是第一个区间，直接保留，<code>preEnd = 2</code></li>
<li>接着看 <code>[1, 3]</code>，因为 <code>1 &lt; 2</code>，重叠，删掉</li>
<li>再看 <code>[2, 3]</code>，因为 <code>2 &gt;= 2</code>，保留，<code>preEnd = 3</code></li>
<li>最后看 <code>[3, 4]</code>，因为 <code>3 &gt;= 3</code>，也保留</li>
</ul>
<p>最终保留 3 个，删除 1 个。</p>
<hr />
<h2>4. Java 实现</h2>
<pre><code>import java.util.Arrays;

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return 0;
        }

        // 按右端点升序排序
        Arrays.sort(intervals, (a, b) -&gt; Integer.compare(a[1], b[1]));

        int count = 1; // 第一个区间一定先保留
        int preEnd = intervals[0][1];

        for (int i = 1; i &lt; intervals.length; i++) {
            // 边界相接不算重叠
            if (intervals[i][0] &gt;= preEnd) {
                count++;
                preEnd = intervals[i][1];
            }
        }

        return intervals.length - count;
    }
}
</code></pre>
<p>这段代码的逻辑就是：</p>
<ul>
<li>先把结束最早的区间保留下来</li>
<li>以后每来一个区间，就看它能不能接在 <code>preEnd</code> 后面</li>
<li>能接上就保留，不能接上就放弃</li>
</ul>
<p>这样扫描一遍，就能得到最多能保留多少个区间。</p>
<hr />
<h3>4.1 左端点排序也可以吗</h3>
<p>可以，但写法会略有不同。</p>
<p>如果按左端点排序，就不是去统计“最多保留多少个”，而是直接统计“发生了多少次重叠”。</p>
<p>核心做法是：</p>
<ul>
<li>遇到重叠时，保留右端点更小的那个区间</li>
<li>这样能给后面的区间留下更大空间</li>
</ul>
<p>对应代码大概是这样：</p>
<pre><code>Arrays.sort(intervals, (a, b) -&gt; Integer.compare(a[0], b[0]));

int remove = 0;
int preEnd = intervals[0][1];

for (int i = 1; i &lt; intervals.length; i++) {
    if (intervals[i][0] &lt; preEnd) {
        remove++;
        preEnd = Math.min(preEnd, intervals[i][1]);
    } else {
        preEnd = intervals[i][1];
    }
}
</code></pre>
<p>这里最容易写错的地方是：一旦重叠，就要把 <code>preEnd</code> 更新成两个右端点里的较小值。这样相当于保留了“更短”的那个区间。</p>
<p>思路和右端点排序本质一致，只是实现角度不同。本文这里推荐右端点排序，直观且更容易写对。</p>
<hr />
<h2>5. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：<code>O(n log n)</code>，主要来自排序。</li>
<li><strong>空间复杂度</strong>：<code>O(log n)</code> 或 <code>O(n)</code>，取决于排序实现。</li>
</ul>
<hr />
<h2>6. 总结</h2>
<p>这题只要抓住一句话就够了：</p>
<blockquote>
<p><strong>按右端点排序，优先保留结束最早的区间。</strong></p>
</blockquote>
<p>这样留下的空间最多，也就最容易保留更多区间，最后删除数自然最少。</p>
<p>如果再记一句实现口令，就是：</p>
<blockquote>
<p><code>preEnd</code> 记录上一个保留区间的右端点，遇到 <code>start &lt; preEnd</code> 就跳过当前区间。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>最优装载</title>
    <link href="https://blog.yoake.dev/posts/algorithm/maximumicecreambars/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/maximumicecreambars/</id>
    <published>2026-06-10T10:26:32.000Z</published>
    <updated>2026-06-10T10:26:32.000Z</updated>
    <summary>用最优装载问题理解 LeetCode 1833. 雪糕的最大数量：优先购买价格最低的雪糕，并补充计数排序优化。</summary>
    <content type="html"><![CDATA[<h2>1. 什么是最优装载问题？</h2>
<p>最优装载问题是贪心算法里的经典问题。</p>
<p>问题可以描述为：</p>
<blockquote>
<p>有一艘船，最大载重量为 $C$。现在有 $n$ 个物品，每个物品都有自己的重量。要求在不超过船最大载重量的前提下，装入尽可能多的物品。</p>
</blockquote>
<p>注意，这里的目标通常是：</p>
<p><strong>装入的物品数量最多</strong>，而不是让总重量最大。</p>
<p>:::tip[生活映射]
假设你手里有一笔固定预算 <code>coins</code>，面前有很多雪糕，每根雪糕的价格为 <code>costs[i]</code>。每根雪糕带来的收益都一样，都是“买到 1 根”，因此目标就是在不超过预算的前提下，买到尽可能多的雪糕。</p>
<p>这和最优装载问题是同一种思路：预算 <code>coins</code> 相当于船的容量，雪糕价格相当于物品重量，目标都是让选中的数量尽可能多。
:::</p>
<hr />
<h2>2. 举个例子</h2>
<p>假设船的最大载重量是：</p>
<p>$$
C = 10
$$</p>
<p>有这些物品：</p>
<p>$$
\text{重量：}[4, 2, 8, 1, 3]
$$</p>
<p>如果想装尽可能多的物品，应该优先装轻的。</p>
<p>先排序：</p>
<p>$$
[1, 2, 3, 4, 8]
$$</p>
<p>然后从小到大装：</p>
<p>$$
1 + 2 + 3 + 4 = 10
$$</p>
<p>刚好装满，可以装 $4$ 个物品。</p>
<p>如果一开始装了 $8$，剩余容量只有 $2$，最多只能再装一个 $1$ 或 $2$，装入的数量就变少了。</p>
<hr />
<h2>3. 贪心思路</h2>
<p>最优装载问题的贪心策略是：</p>
<blockquote>
<p>每次优先选择重量最小的物品。</p>
</blockquote>
<p>原因很直观：物品越轻，占用容量越少，留下的剩余容量越多，就越容易继续装更多物品。</p>
<p>算法步骤：</p>
<ol>
<li>将所有物品按照重量从小到大排序；</li>
<li>依次尝试装入物品；</li>
<li>如果当前总重量加上这个物品不超过容量 $C$，就装入；</li>
<li>如果超过容量，就直接停止。由于数组已经升序排序，当前物品装不下，后续更重的物品也必然装不下。</li>
</ol>
<hr />
<h2>4. 为什么这个贪心是对的？</h2>
<h3>4.1 直观替换解释</h3>
<p>假设我们想装最多数量的物品。如果某个方案中装了一个较重的物品，却没有装一个更轻的物品，那么可以把较重物品换成这个更轻的物品。</p>
<p>这样做之后：</p>
<ul>
<li>装入的物品数量不变；</li>
<li>总重量只会减少，不会增加；</li>
<li>剩余容量只会变多，不会变少。</li>
</ul>
<p>因此，优先选择轻的物品一定不会吃亏。</p>
<h3>4.2 补充：反证法证明</h3>
<p>假定贪心思路取得的序列为 $A = [a_1, a_2, \ldots, a_k]$ （长度为 $k$，代表前 $k$ 个最便宜/最轻的物品），真实最优解取得的序列为 $B = [b_1, b_2, \dots, b_m]$ （长度为 $m$）。</p>
<p>由最优解的定义，其装入数量不小于任何可行解，因此有 $k \le m$。我们只需要证明 $k \ge m$ 成立，即可得证 $k = m$。</p>
<p>利用反证法证明 $k \ge m$，假设 $k &lt; m$：</p>
<p>根据贪心决策，我们总是优先选择排序数组中当前最小的可用物品，因此选择的 $A$ 在排序好的数组里必然是一段连续的前缀 $[c_1, c_2, \ldots, c_k]$。并且因为装不下第 $k+1$ 个物品，所以有：</p>
<p>$$
\sum_{i=1}^{k+1} c_i &gt; C
$$</p>
<p>而最优解选择的方案在已排序数组中分布可能是不连续的，如下图所示：</p>
<p><img src="https://img.picr2.online/img/2026/06/10/a5ddc79a487a0a1f11aa31ac767983e7.webp" alt="贪心解与最优解在已排序数组中的分布对比" /></p>
<p>利用“将最优解中分布靠后的物品，替换为排序数组中未被选择且靠前的较轻物品，总费用不会增加，同时数量不变”的特性，我们也可以将真实最优解调整为某段连续的前缀：</p>
<p><img src="https://img.picr2.online/img/2026/06/10/54b3247c8332ee24b4946b8cf7142f16.webp" alt="最优解调整为连续前缀示意图" /></p>
<p>调整后最优解的前缀即为 $[c_1, c_2, \ldots, c_m]$，由于它是可行解，其总成本不超过容量 $C$：</p>
<p>$$
\sum_{i=1}^{m} c_i \le C
$$</p>
<p>又因为假设了 $k &lt; m$，可以推出 $k + 1 \le m$，进而有：</p>
<p>$$
\sum_{i=1}^{k+1} c_i \le \sum_{i=1}^{m} c_i \le C
$$</p>
<p>这与贪心结论中的 $\sum_{i=1}^{k+1} c_i &gt; C$ 产生矛盾。</p>
<p>因此假设不成立，必然有 $k \ge m$。结合 $k \le m$ 可得 $k = m$。贪心解必然能够取得与最优解相同的长度。</p>
<hr />
<h2>5. Java 实现</h2>
<p>对应到本题，<code>costs[i]</code> 就是每根雪糕的价格，<code>coins</code> 就是手里的钱。</p>
<pre><code>import java.util.Arrays;

class Solution {
    public int maxIceCream(int[] costs, int coins) {
        // 先按价格从低到高排序，优先买便宜的雪糕
        Arrays.sort(costs);

        int ans = 0;
        for (int cost : costs) {
            // 当前雪糕买得起，就买下，并扣除对应费用
            if (coins &gt;= cost) {
                ans++;
                coins -= cost;
            } else {
                // 当前价格已经买不起，后面更贵的也买不起
                break;
            }
        }

        return ans;
    }
}
</code></pre>
<hr />
<h2>6. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：排序需要 $O(n \log n)$，遍历数组需要 $O(n)$。总时间复杂度为 $O(n \log n)$。</li>
<li><strong>空间复杂度</strong>：$O(\log n)$，主要取决于排序所需要的栈空间。</li>
</ul>
<hr />
<h2>7. 计数排序优化解法（线性时间）</h2>
<p>由于题目提示中给出了价格上限 $costs[i] \le 10^5$，数值范围较小。我们还可以使用<strong>计数排序</strong>将排序的时间复杂度优化至线性，从而在海量数据时效率更高。</p>
<pre><code>class Solution {
    public int maxIceCream(int[] costs, int coins) {
        int max = 0;
        // 找到最大价格，用来确定计数数组的长度
        for (int cost : costs) {
            max = Math.max(max, cost);
        }

        int[] freq = new int[max + 1];
        // freq[i] 表示价格为 i 的雪糕有多少根
        for (int cost : costs) {
            freq[cost]++;
        }

        int ans = 0;
        // 按价格从低到高尝试购买
        for (int i = 1; i &lt;= max; i++) {
            if (freq[i] &gt; 0) {
                if (coins &gt;= i) {
                    // 贪心一次性购买当前价格下能买得起的所有雪糕
                    int buy = Math.min(freq[i], coins / i);
                    ans += buy;
                    coins -= buy * i;
                    // 如果当前价格没能全买下，说明钱已花光，后续更贵的更买不起
                    if (buy &lt; freq[i]) {
                        break;
                    }
                } else {
                    break;
                }
            }
        }
        return ans;
    }
}
</code></pre>
<ul>
<li><strong>时间复杂度</strong>：$O(n + C)$，其中 $C$ 为最大雪糕价格（$C \le 10^5$）。</li>
<li><strong>空间复杂度</strong>：$O(C)$，需要一个长度为 $C + 1$ 的计数数组。</li>
</ul>
<hr />
<h2>8. 和 0-1 背包问题的区别</h2>
<p>最优装载问题很容易和普通的背包问题混淆。</p>
<h3>8.1 最优装载问题</h3>
<ul>
<li><strong>目标</strong>：在容量限制内，装入尽可能<strong>多数量</strong>的物品。</li>
<li><strong>特点</strong>：只关心物品的重量和数量，不关心物品的价值（或者可以看作所有物品的价值恒定为 $1$）。</li>
<li><strong>策略</strong>：优先装最轻的。</li>
</ul>
<h3>8.2 0-1 背包问题</h3>
<ul>
<li><strong>目标</strong>：在容量限制内，让装入物品的<strong>总价值</strong>最大。</li>
<li><strong>特点</strong>：每个物品除了重量，还有不同的价值，不能简单地按重量、价值或性价比贪心。</li>
<li><strong>策略</strong>：通常需要使用动态规划。</li>
</ul>
<h3>8.3 拓展思考：价值不恒定为 1 时能否贪心？</h3>
<p>如果每个物品对答案的贡献不恒定为 $1$，而是多给一个数组 <code>values</code>。每件物品费用为 <code>costs[i]</code>，价值为 <code>values[i]</code>，求解花费不超过 <code>coins</code> 的最大价值。</p>
<p><strong>答案是：不可以。</strong></p>
<p>平时学习的「01 背包」无法使用简单的贪心算法来解决。以下是反例分析：</p>
<p>假设容量限制为 $10$。有三件物品：</p>
<ul>
<li>物品 A：<code>cost = 6, value = 7</code>（性价比约为 $1.17$）</li>
<li>物品 B：<code>cost = 5, value = 5</code>（性价比为 $1.0$）</li>
<li>物品 C：<code>cost = 5, value = 5</code>（性价比为 $1.0$）</li>
</ul>
<p>如果采用贪心策略，优先选择性价比最高的 A，背包容量剩余 $4$，无法再装入 B 和 C，最终获得的总价值为 $7$。
而最优解是选择 B 和 C，刚好装满背包，获得的总价值为 $10$。</p>
<hr />
<h2>9. 一句话总结</h2>
<p>最优装载问题的核心是：</p>
<blockquote>
<p>在容量有限的情况下，为了装入最多数量的物品，应该优先装重量最小的物品。</p>
</blockquote>
<p>实现口令是：</p>
<pre><code>先排序，再从小到大累加，能装就装，装不下就停。
</code></pre>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>快速幂</title>
    <link href="https://blog.yoake.dev/posts/algorithm/powxn/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/powxn/</id>
    <published>2026-06-10T03:43:08.000Z</published>
    <updated>2026-06-10T03:43:08.000Z</updated>
    <summary>通过迭代（二进制角度）与递归（分治法角度）两种方法详细解析快速幂算法，解决数值的整数次幂问题，并将时间复杂度优化至 O(log n)。</summary>
    <content type="html"><![CDATA[<h2>1. 问题引入</h2>
<p>求 $x^n$ 最朴素的方法是把 $x$ 连乘 $n$ 次，时间复杂度为 $O(n)$。</p>
<p>当 $n$ 很大时，这种线性时间复杂度的算法效率较低，容易导致超时。<strong>快速幂法</strong>可将时间复杂度降低至 $O(\log n)$。</p>
<p>我们可以从 <strong>“迭代（二进制角度）”</strong> 和 <strong>“递归（分治法角度）”</strong> 两个角度来实现快速幂算法。</p>
<hr />
<h2>2. 方法一：迭代快速幂（二进制角度）</h2>
<h3>2.1 算法原理</h3>
<p>利用整数 $n$ 的二进制表示，可对快速幂进行数学化解释。</p>
<p>对于任何十进制正整数 $n$，设其二进制表示为 $b_m \dots b_3 b_2 b_1$（其中 $b_i \in {0, 1}$，$i \in [1, m]$），则有：</p>
<ul>
<li><strong>二进制转十进制：</strong> $n = 1b_1 + 2b_2 + 4b_3 + \dots + 2^{m-1}b_m$</li>
<li><strong>幂的二进制展开：</strong> $x^n = x^{1b_1 + 2b_2 + 4b_3 + \dots + 2^{m-1}b_m} = x^{1b_1} x^{2b_2} x^{4b_3} \dots x^{2^{m-1}b_m}$</li>
</ul>
<p>根据以上推导，可把计算 $x^n$ 转化为解决以下两个问题：</p>
<ol>
<li><strong>计算 $x^1, x^2, x^4, \dots, x^{2^{m-1}}$ 的值：</strong> 循环赋值操作 $x = x^2$ 即可。</li>
<li><strong>获取二进制各位 $b_1, b_2, b_3, \dots, b_m$ 的值：</strong> 循环执行以下操作：
<ul>
<li><strong><code>n &amp; 1</code>（与操作）：</strong> 判断 $n$ 二进制最右一位是否为 1。</li>
<li><strong><code>n &gt;&gt; 1</code>（移位操作）：</strong> $n$ 右移一位（相当于删除最后一位）。</li>
</ul>
</li>
</ol>
<p>因此，在循环中依次处理 $x^{2^0}, x^{2^1}, \dots, x^{2^{m-1}}$，并把所有二进制位为 $1$ 的项累计相乘即可。</p>
<p><img src="https://img.picr2.online/img/2026/06/10/50bbb9e4464ac218e787b512a92334e7.webp" alt="快速幂二进制公式解析" /></p>
<p>下图直观地展示了迭代计算中 $x$ 的平方以及 $n$ 二进制位的变化过程：</p>
<p><img src="https://img.picr2.online/img/2026/06/10/2dfada198ae4c7310ba560241886fbea.webp" alt="快速幂迭代计算过程" /></p>
<h3>2.2 迭代过程示例：以 $3^{13}$ 为例</h3>
<p>以上面的二进制拆分为基础，我们可以进一步观察快速幂循环中各个变量的变化过程。</p>
<p>对于 $3^{13}$：</p>
<p>$$13 = 1101_2$$</p>
<p>也就是：</p>
<p>$$13 = 8 + 4 + 1$$</p>
<p>所以最终应该只选择：</p>
<p>$$3^1 \text{、} 3^4 \text{、} 3^8$$</p>
<p>而 $3^2$ 只是循环过程中产生的候选值，并不会参与最终结果相乘。</p>
<p>快速幂循环中，<code>x</code> 会不断平方，依次表示：</p>
<p>$$3^1 \rightarrow 3^2 \rightarrow 3^4 \rightarrow 3^8$$</p>
<p>同时，<code>b</code> 会不断右移，依次检查二进制的最低位是否为 1。</p>
<p>过程如下：</p>
<table>
<thead>
<tr>
<th>当前 <code>b</code></th>
<th>当前 <code>x</code></th>
<th><code>b</code> 是否为奇数</th>
<th><code>ans</code> 变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>13</td>
<td>3&lt;sup&gt;1&lt;/sup&gt;</td>
<td>是</td>
<td>ans = 1 * 3&lt;sup&gt;1&lt;/sup&gt;</td>
</tr>
<tr>
<td>6</td>
<td>3&lt;sup&gt;2&lt;/sup&gt;</td>
<td>否</td>
<td>ans 不变</td>
</tr>
<tr>
<td>3</td>
<td>3&lt;sup&gt;4&lt;/sup&gt;</td>
<td>是</td>
<td>ans = 3&lt;sup&gt;1&lt;/sup&gt; * 3&lt;sup&gt;4&lt;/sup&gt;</td>
</tr>
<tr>
<td>1</td>
<td>3&lt;sup&gt;8&lt;/sup&gt;</td>
<td>是</td>
<td>ans = 3&lt;sup&gt;1&lt;/sup&gt; * 3&lt;sup&gt;4&lt;/sup&gt; * 3&lt;sup&gt;8&lt;/sup&gt;</td>
</tr>
</tbody>
</table>
<p>因此最终：</p>
<p>$$ans = 3^1 \times 3^4 \times 3^8$$</p>
<p>也就是：</p>
<p>$$3^{13} = 1594323$$</p>
<p>从二进制角度看，13 的二进制为 $1101_2$。从右往左依次检查每一位：</p>
<table>
<thead>
<tr>
<th>二进制位</th>
<th>当前 <code>x</code></th>
<th>是否乘入 <code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>3&lt;sup&gt;1&lt;/sup&gt;</td>
<td>乘入</td>
</tr>
<tr>
<td>0</td>
<td>3&lt;sup&gt;2&lt;/sup&gt;</td>
<td>不乘</td>
</tr>
<tr>
<td>1</td>
<td>3&lt;sup&gt;4&lt;/sup&gt;</td>
<td>乘入</td>
</tr>
<tr>
<td>1</td>
<td>3&lt;sup&gt;8&lt;/sup&gt;</td>
<td>乘入</td>
</tr>
</tbody>
</table>
<p>所以代码中的：</p>
<pre><code>if ((b &amp; 1) == 1) {
    ans *= x;
}
</code></pre>
<p>这一步是在判断当前二进制位是否为 1。如果是 1，就把当前的 <code>x</code> 乘入答案。</p>
<p>而：</p>
<pre><code>x *= x;
b &gt;&gt;= 1;
</code></pre>
<p>分别表示：</p>
<ul>
<li><code>x *= x</code>：让当前幂次翻倍，例如 $3^1 \rightarrow 3^2 \rightarrow 3^4 \rightarrow 3^8$</li>
<li><code>b &gt;&gt;= 1</code>：让指数右移一位，继续检查下一位二进制</li>
</ul>
<blockquote>
<p><strong>注意：</strong>
在 Java、C++ 等语言中，32 位有符号整数 <code>int</code> 的范围是 $[-2^{31}, 2^{31}-1]$。当 $n = -2^{31}$ 时，直接取相反数 <code>-n</code> 会导致溢出。解决方法是先将 $n$ 存入 64 位整数（例如 Java 的 <code>long</code> 类型）再进行计算。</p>
</blockquote>
<h3>2.3 代码实现</h3>
<pre><code>class Solution {
    public double myPow(double x, int n) {
        // 0 次幂直接返回 1
        if (n == 0) {
            return 1.0;
        }
        // 0 的正整数次幂直接返回 0
        if (x == 0.0) {
            return 0.0;
        }

        long b = n;
        double ans = 1.0;
        // 负指数先转成倒数，再把指数变为正数
        if (b &lt; 0) {
            x = 1.0 / x;
            b = -b;
        }

        while (b &gt; 0) {
            // 当前二进制位为 1，就把当前幂次乘进答案
            if ((b &amp; 1) == 1) {
                ans *= x;
            }
            // 当前幂次翻倍
            x *= x;
            // 指数右移一位，继续处理下一位
            b &gt;&gt;= 1;
        }
        return ans;
    }
}
</code></pre>
<h3>2.4 复杂度分析</h3>
<ul>
<li><strong>时间复杂度：</strong> $O(\log |n|)$，循环次数与 $n$ 的二进制位数成正比。</li>
<li><strong>空间复杂度：</strong> $O(1)$，仅使用常数大小的额外空间。</li>
</ul>
<hr />
<h2>3. 方法二：递归快速幂（分治法角度）</h2>
<h3>3.1 算法原理</h3>
<p>快速幂实际上也是分治思想的一种典型应用。</p>
<ul>
<li>当 $n &lt; 0$ 时，我们可以利用倒数关系将其转换为正数次幂：$x^n = (1/x)^{-n}$。</li>
<li>当 $n \ge 0$ 时，我们可以将问题划分为规模减半的子问题。设 <code>half = dfs(x, n / 2)</code>，在 Java 中整数除法会自动向下取整，则有：</li>
</ul>
<p>$$x^n = \begin{cases} (x^{n / 2})^2, &amp; n \text{ 为偶数} \ (x^{n / 2})^2 \times x, &amp; n \text{ 为奇数} \end{cases}$$</p>
<p>根据上式，我们只需要先递归地计算 $x^{n / 2}$，然后根据 $n$ 的奇偶性，决定是否多乘一个 $x$ 即可。</p>
<h3>3.2 递归过程示例：以 $3^{13}$ 为例</h3>
<p>上面的公式一开始可能不太直观，可以通过 $3^{13}$ 来理解递归快速幂的执行过程。</p>
<p>递归快速幂的核心是：<strong>每次先计算一半的幂次，然后根据当前指数的奇偶性决定如何合并结果</strong>。</p>
<p>对于 $3^{13}$：</p>
<p>由于 13 是奇数，所以：</p>
<p>$$3^{13} = 3^6 \times 3^6 \times 3$$</p>
<p>只要先算出 $3^6$，就可以得到 $3^{13}$。</p>
<p>继续拆分：</p>
<p>$$3^6 = 3^3 \times 3^3$$</p>
<p>因为 6 是偶数，所以平方即可，不需要额外乘 3。</p>
<p>继续拆分：</p>
<p>$$3^3 = 3^1 \times 3^1 \times 3$$</p>
<p>因为 3 是奇数，所以平方后还需要额外乘 3。</p>
<p>继续拆分：</p>
<p>$$3^1 = 3^0 \times 3^0 \times 3$$</p>
<p>当指数为 0 时，递归到达边界，直接返回 1。</p>
<p>递归调用过程如下：</p>
<table>
<thead>
<tr>
<th>调用</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>dfs(3, 13)</code></td>
<td>计算 3&lt;sup&gt;13&lt;/sup&gt;，需要先计算 3&lt;sup&gt;6&lt;/sup&gt;</td>
</tr>
<tr>
<td><code>dfs(3, 6)</code></td>
<td>计算 3&lt;sup&gt;6&lt;/sup&gt;，需要先计算 3&lt;sup&gt;3&lt;/sup&gt;</td>
</tr>
<tr>
<td><code>dfs(3, 3)</code></td>
<td>计算 3&lt;sup&gt;3&lt;/sup&gt;，需要先计算 3&lt;sup&gt;1&lt;/sup&gt;</td>
</tr>
<tr>
<td><code>dfs(3, 1)</code></td>
<td>计算 3&lt;sup&gt;1&lt;/sup&gt;，需要先计算 3&lt;sup&gt;0&lt;/sup&gt;</td>
</tr>
<tr>
<td><code>dfs(3, 0)</code></td>
<td>指数为 0，返回 1</td>
</tr>
</tbody>
</table>
<p>返回过程如下：</p>
<table>
<thead>
<tr>
<th>返回层</th>
<th>当前指数</th>
<th>计算方式</th>
<th>返回值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>dfs(3, 0)</code></td>
<td>0</td>
<td>返回 1</td>
<td>1</td>
</tr>
<tr>
<td><code>dfs(3, 1)</code></td>
<td>1</td>
<td>1 × 1 × 3</td>
<td>3</td>
</tr>
<tr>
<td><code>dfs(3, 3)</code></td>
<td>3</td>
<td>3 × 3 × 3</td>
<td>27</td>
</tr>
<tr>
<td><code>dfs(3, 6)</code></td>
<td>6</td>
<td>27 × 27</td>
<td>729</td>
</tr>
<tr>
<td><code>dfs(3, 13)</code></td>
<td>13</td>
<td>729 × 729 × 3</td>
<td>1594323</td>
</tr>
</tbody>
</table>
<p>可以看到，递归快速幂并不是从 $3^{13}$ 一步一步乘 13 次，而是不断把指数减半：</p>
<p>$$13 \rightarrow 6 \rightarrow 3 \rightarrow 1 \rightarrow 0$$</p>
<p>然后在返回时逐层合并结果。</p>
<p>因此，递归层数只和指数不断除以 2 的次数有关，时间复杂度为 $O(\log |n|)$。</p>
<ul>
<li><strong>递归边界：</strong> 当 $n = 0$ 时，返回 $1.0$。若 $x = 0.0$ 且 $n &gt; 0$，直接返回 $0.0$。这里默认题目不会额外考察 $0$ 的负指数情况。</li>
</ul>
<p><img src="https://img.picr2.online/img/2026/06/10/f37a482372a96419c58a040d796dd26b.webp" alt="快速幂递归分治解析" /></p>
<h3>3.3 代码实现</h3>
<pre><code>class Solution {
    public double myPow(double x, int n) {
        // 0 次幂直接返回 1
        if (n == 0) {
            return 1.0;
        }
        // 0 的正整数次幂直接返回 0
        if (x == 0.0) {
            return 0.0;
        }

        long b = n;
        // 负指数先在入口处处理，dfs 只负责非负指数
        if (n &lt; 0) {
            x = 1.0 / x;
            b = -b;
        }

        return dfs(x, b);
    }

    private double dfs(double x, long n) {
        // 非负指数的递归边界
        if (n == 0) {
            return 1.0;
        }

        double half = dfs(x, n / 2);
        // 偶数次幂：直接平方
        if (n % 2 == 0) {
            return half * half;
        }
        // 奇数次幂：平方后再补一个 x
        return half * half * x;
    }
}
</code></pre>
<h3>3.4 复杂度分析</h3>
<ul>
<li><strong>时间复杂度：</strong> $O(\log |n|)$，每次递归使幂次减半，因此递归树的深度为对数级别。</li>
<li><strong>空间复杂度：</strong> $O(\log |n|)$，递归树的深度为 $O(\log |n|)$，需要占用对应大小的系统函数调用栈空间。</li>
</ul>
<hr />
<h2>4. 算法对比与总结</h2>
<table>
<thead>
<tr>
<th>方法</th>
<th>时间复杂度</th>
<th>空间复杂度</th>
<th>优缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>方法一：迭代快速幂</strong></td>
<td>$O(\log \vert n \vert)$</td>
<td>$O(1)$</td>
<td><strong>推荐</strong>。避免了递归带来的系统栈开销，效率更高，更适用于工程实现。</td>
</tr>
<tr>
<td><strong>方法二：递归快速幂</strong></td>
<td>$O(\log \vert n \vert)$</td>
<td>$O(\log \vert n \vert)$</td>
<td>代码结构更贴近数学公式的直观表达，逻辑简单清晰，但存在栈溢出的隐患。</td>
</tr>
</tbody>
</table>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>堆排序</title>
    <link href="https://blog.yoake.dev/posts/algorithm/heapsort/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/heapsort/</id>
    <published>2026-06-09T03:42:57.000Z</published>
    <updated>2026-06-09T03:42:57.000Z</updated>
    <summary>堆排序利用完全二叉树结构的大根堆或小根堆性质，通过建堆与交替调整实现原地升序或降序排列。</summary>
    <content type="html"><![CDATA[<h2>1. 算法思想</h2>
<p>堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构，并同时满足堆的性质：即子节点的键值总是小于或大于它的父节点。</p>
<p>根据父子节点之间的关系，堆可以分为两类：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/205c5624750d309fa5b6a43ca1eaae9f.webp" alt="大根堆与小根堆" /></p>
<ul>
<li><strong>大根堆</strong>：每个节点的值均大于或等于其左右孩子节点的值。</li>
<li><strong>小根堆</strong>：每个节点的值均小于或等于其左右孩子节点的值。</li>
</ul>
<hr />
<h2>2. 堆的数学性质</h2>
<p>若对堆中的节点按照层序遍历的方式进行编号，则可将其结构映射到一维数组中。</p>
<h3>0 起始编号</h3>
<p>若从 $0$ 开始编号，则节点 $i$ 的左子节点为 $2i + 1$，右子节点为 $2i + 2$。此时大根堆和小根堆满足以下关系：</p>
<p>$$
\begin{cases}
\text{大根堆：} \text{nums}[i] \ge \text{nums}[2i + 1] \text{ 且 } \text{nums}[i] \ge \text{nums}[2i + 2] \
\text{小根堆：} \text{nums}[i] \le \text{nums}[2i + 1] \text{ 且 } \text{nums}[i] \le \text{nums}[2i + 2]
\end{cases}
$$</p>
<h3>1 起始编号</h3>
<p>若从 $1$ 开始编号，则节点 $i$ 的左子节点为 $2i$，右子节点为 $2i + 1$。此时大根堆和小根堆满足以下关系：</p>
<p>$$
\begin{cases}
\text{大根堆：} \text{nums}[i] \ge \text{nums}[2i] \text{ 且 } \text{nums}[i] \ge \text{nums}[2i + 1] \
\text{小根堆：} \text{nums}[i] \le \text{nums}[2i] \text{ 且 } \text{nums}[i] \le \text{nums}[2i + 1]
\end{cases}
$$</p>
<hr />
<h2>3. 构造大根堆</h2>
<p>假设有一个待排序的数组 <code>nums = [5, 2, 1, 9, 6, 8]</code>，我们可将其构造成一个完全二叉树：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/a830ad037f92e6d63dc6477e760abd09.webp" alt="完全二叉树结构" /></p>
<p>要将一个完全二叉树构造成一个大顶堆，一个高效的实现方式是从最后一个非叶子节点开始，自底向上、从右往左进行调整。</p>
<p>若数组长度为 $n$，从 $0$ 开始编号，第一个非叶子节点的下标为 $n / 2 - 1$。对于以该非叶子节点为根的子树，调整规则如下：</p>
<ol>
<li>如果该节点大于或等于左右子节点，则无需调整。</li>
<li>否则，将该节点与左右子节点中较大者进行交换，并从被交换的子节点位置出发继续向下调整，直到子树满足堆性质。</li>
</ol>
<p>具体建堆步骤如下：</p>
<h3>步骤一</h3>
<p>第一个非叶子节点为 $2$，其值为 $1$。由于其左子节点 $8$ 较大，交换两者：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/d3f77ef6c232cb9b2722a3c918e96078.webp" alt="建堆步骤一" /></p>
<h3>步骤二</h3>
<p>第二个非叶子节点为 $1$，其值为 $2$。因为其左右子节点分别为 $9$ 和 $6$，左子节点较大且大于 $2$，交换两者：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/ac3e828ad130391f439fdffd5c742cd5.webp" alt="建堆步骤二" /></p>
<h3>步骤三</h3>
<p>第三个非叶子节点为 $0$，其值为 $5$。因为其左右子节点分别为 $9$ 和 $8$，左子节点较大且大于 $5$，交换两者：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/df1fc51a0d012c9cad0026fc9ddcb2f9.webp" alt="建堆步骤三第一步" /></p>
<p>交换后发现子树堆性质被打破（此时该位置的值 $5$ 小于右子节点 $6$），需要继续对以该节点为根的子树进行调整：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/d62cd64a4bc4c1cf68093a69a05d51fd.webp" alt="建堆步骤三第二步" /></p>
<p>此时建堆完毕，得到大根堆 <code>nums = [9, 6, 8, 2, 5, 1]</code>：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/fd940954b3a8bd1c12a89e7a1c18d46b.webp" alt="大根堆构建完成" /></p>
<hr />
<h2>4. 排序过程</h2>
<p>建立好大根堆后，排序的步骤如下：</p>
<ol>
<li>将堆顶元素（最大值）与堆尾元素交换，此时最大值被放置到数组末尾。</li>
<li>缩小堆的有效范围，将剩余的 $n - 1$ 个元素重新调整为大根堆。</li>
<li>重复该过程，直到堆中仅剩一个元素。</li>
</ol>
<h3>提取最大元素</h3>
<p>将堆顶元素 $9$ 与末尾元素 $1$ 交换，并将 $9$ 移出堆的范围：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/ffb5a237ff3e279a65274a5cb60f0369.webp" alt="提取最大元素并调整" /></p>
<p>对剩余元素重新调整，使其满足大根堆性质：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/cf63ef029194fa93eb6b24470c495708.webp" alt="堆重建" /></p>
<h3>提取次大元素</h3>
<p>将当前的堆顶 $8$ 与堆尾元素 $2$ 交换，并将 $8$ 移出堆的范围：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/4b2bae7aaf4f11b850eceadf0c5a8769.webp" alt="提取次大元素" /></p>
<h3>提取第三大元素</h3>
<p>将堆顶 $6$ 与尾部的 $1$ 交换：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/f22377cf56f2022e2964ce03993c88a3.webp" alt="提取第三大元素" /></p>
<h3>提取第四大元素</h3>
<p>将堆顶 $5$ 与尾部的 $2$ 交换：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/156aac17de853cc956a7ecae5d8e207d.webp" alt="提取第四大元素" /></p>
<h3>提取第五大元素</h3>
<p>将堆顶 $2$ 与尾部的 $1$ 交换，此时堆中仅剩一个元素，排序完成：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/e7862a1c64043a10b10fc6b0e1a8915e.webp" alt="提取第五大元素" /></p>
<p>最终得到升序排列的数组：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/691782e24490d60d616faedb8fc78129.webp" alt="排序完成" /></p>
<hr />
<h2>5. Java 实现</h2>
<p>下面给出两种常见的实现方式。</p>
<h3>5.1 从下标 0 开始建堆</h3>
<pre><code>class Solution {
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        
        // 自底向上构建大根堆
        for (int root = n / 2 - 1; root &gt;= 0; root--) {
            maxHeapify(nums, root, n);
        }
        
        // 依次交换堆顶与未排序部分末尾，并重新调整
        for (int tail = n - 1; tail &gt; 0; tail--) {
            int temp = nums[0];
            nums[0] = nums[tail];
            nums[tail] = temp;
            maxHeapify(nums, 0, tail);
        }
        
        return nums;
    }

    private void maxHeapify(int[] heap, int root, int heapSize) {
        int largest = root;
        while (true) {
            int left = 2 * root + 1;
            int right = 2 * root + 2;
            
            if (left &lt; heapSize &amp;&amp; heap[largest] &lt; heap[left]) {
                largest = left;
            }
            if (right &lt; heapSize &amp;&amp; heap[largest] &lt; heap[right]) {
                largest = right;
            }
            
            if (largest != root) {
                int temp = heap[root];
                heap[root] = heap[largest];
                heap[largest] = temp;
                root = largest;
            } else {
                break;
            }
        }
    }
}
</code></pre>
<h3>5.2 从下标 1 开始建堆</h3>
<p>为了更方便地通过索引计算父子关系，可在数组最前面插入一个占位元素，使得有效数据下标从 $1$ 开始。</p>
<pre><code>class Solution {
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        int[] arr = new int[n + 1];
        System.arraycopy(nums, 0, arr, 1, n); // 头部保留 arr[0] = 0 作为占位元素
        
        // 构建大根堆，首个非叶子节点为 n / 2
        for (int i = n / 2; i &gt; 0; i--) {
            maxHeapify(arr, i, n);
        }
        
        // 排序过程
        for (int j = n; j &gt; 0; j--) {
            int temp = arr[1];
            arr[1] = arr[j];
            arr[j] = temp;
            maxHeapify(arr, 1, j - 1);
        }
        
        // 复制回原数组或生成新数组返回
        int[] result = new int[n];
        System.arraycopy(arr, 1, result, 0, n);
        return result;
    }

    private void maxHeapify(int[] arr, int i, int end) {
        int j = 2 * i; // 左子节点
        while (j &lt;= end) {
            if (j + 1 &lt;= end &amp;&amp; arr[j + 1] &gt; arr[j]) {
                j++; // 选择左右子节点中的较大者
            }
            
            if (arr[i] &lt; arr[j]) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
                i = j;
                j = 2 * i;
            } else {
                break;
            }
        }
    }
}
</code></pre>
<hr />
<h2>6. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n \log n)$，其中 $n$ 为数组的长度。初始化建堆的时间复杂度为 $O(n)$，之后每次调整的时间复杂度为 $O(\log n)$，共进行 $n - 1$ 次调整。</li>
<li><strong>空间复杂度</strong>：对于从下标 $0$ 开始的实现，空间复杂度为 $O(1)$，属于原地排序；对于从下标 $1$ 开始的实现，由于创建了辅助数组 <code>arr</code>，会产生额外的 $O(n)$ 空间开销。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>归并排序</title>
    <link href="https://blog.yoake.dev/posts/algorithm/mergesort/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/mergesort/</id>
    <published>2026-06-09T03:42:57.000Z</published>
    <updated>2026-06-09T03:42:57.000Z</updated>
    <summary>归并排序采用经典的分治策略，将序列分解为子序列进行排序，再合并为完全有序的序列。</summary>
    <content type="html"><![CDATA[<h2>1. 算法策略</h2>
<p>归并排序是建立在归并操作上的一种稳定排序算法，也是分治法的典型应用。</p>
<p>它的核心流程可以概括为三个动作：<strong>分</strong> <strong>治</strong> <strong>合</strong></p>
<ul>
<li><strong>分</strong>：将序列一步步分解成小的子序列进行分段排序。</li>
<li><strong>治</strong>：将分段有序的子序列合并在一起，使得整个序列变得有序。</li>
</ul>
<p>下图展示了分治策略的整体步骤：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/eaaad14d2546fa1afe6fb5fcd78a9c2c.webp" alt="归并排序分治步骤" /></p>
<h3>1.1 先拆再并</h3>
<p>归并排序的思路可以概括为：</p>
<ol>
<li>不断把数组从中间一分为二。</li>
<li>直到每个子数组只剩一个元素。</li>
<li>再把两个有序子数组合并成一个更大的有序数组。</li>
</ol>
<p>例如数组 <code>6 5 3 1 8 7 2 4</code>，拆分过程大致如下：</p>
<pre><code>6 5 3 1 8 7 2 4
├── 6 5 3 1
│   ├── 6 5
│   │   ├── 6
│   │   └── 5
│   └── 3 1
│       ├── 3
│       └── 1
└── 8 7 2 4
    ├── 8 7
    │   ├── 8
    │   └── 7
    └── 2 4
        ├── 2
        └── 4
</code></pre>
<p>等到最底层只剩单个元素时，每一段天然就是有序的，接下来只需要逐层合并即可。</p>
<p>分裂点一般写成：</p>
<pre><code>int mid = left + (right - left) / 2;
</code></pre>
<p>这样写比直接用 <code>(left + right) / 2</code> 更安全，避免区间很大时出现整数溢出。</p>
<hr />
<h2>2. 合并过程</h2>
<p>下图以最后一步中合并分段有序的子序列为例：</p>
<p><img src="https://img.picr2.online/img/2026/06/09/f4f91458f36c0aec27819df02c151254.webp" alt="归并排序合并过程" /></p>
<h3>双指针落位与比较</h3>
<p>设立两个指针 <code>left</code> 和 <code>right</code>，分别指向左右两个待合并的已有序子数组 <code>nums[low, mid]</code> 和 <code>nums[mid + 1, high]</code>：</p>
<ul>
<li>在合并左右两个子数组时，若 <code>nums[right] &lt; nums[left]</code>，则将当前较小的 <code>nums[right]</code> 放入排序结果中。</li>
<li>依次类推，直到两个子数组的所有元素都被合并，即可得到最终排好序的数组。</li>
</ul>
<p>可以拿下面这组数据理解：</p>
<ul>
<li>左子数组：<code>[2, 4, 6]</code></li>
<li>右子数组：<code>[1, 3, 5]</code></li>
</ul>
<p>合并时先比较 <code>2</code> 和 <code>1</code>，取较小的 <code>1</code>；再比较 <code>2</code> 和 <code>3</code>，取 <code>2</code>；接着是 <code>4</code> 和 <code>3</code>，取 <code>3</code>，直到一边元素全部取完，再把另一边剩余元素直接追加进去。</p>
<h3>为什么要用临时数组</h3>
<p>合并时不能直接在原数组上覆盖，否则后面的元素很容易被提前改掉。</p>
<p>因此通常会先用一个临时数组 <code>tmp</code> 保存合并结果，最后再统一写回原数组。</p>
<p>在 Java 里，更推荐在排序开始前只创建一次辅助数组，然后在递归过程中重复使用它，这样可以减少频繁创建临时对象带来的额外开销。</p>
<h3>稳定性</h3>
<p>归并排序是稳定排序。</p>
<p>当左右两边出现相同元素时，先取左边的元素，就能保证相同元素的相对顺序不被打乱。</p>
<hr />
<h2>3. Java 实现</h2>
<p>下面给出 Java 语言的实现，关键步骤都加了注释。这里采用“只创建一次辅助数组”的写法：</p>
<pre><code>import java.util.Arrays;

public class MergeSort {

    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length &lt;= 1) {
            return;
        }

        // 只申请一次辅助数组，递归过程中复用
        int[] temp = new int[arr.length];
        sort(arr, 0, arr.length - 1, temp);
    }

    private static void sort(int[] arr, int left, int right, int[] temp) {
        // 区间长度为 1 时自然有序，递归结束
        if (left &gt;= right) {
            return;
        }

        // 分：把区间拆成左右两半
        int mid = left + (right - left) / 2;
        sort(arr, left, mid, temp);
        sort(arr, mid + 1, right, temp);

        // 合：把两个有序子区间合并
        merge(arr, left, mid, right, temp);
    }

    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;
        int j = mid + 1;
        int t = 0;

        // 比较左右两边的当前元素，把较小值依次放入辅助数组
        while (i &lt;= mid &amp;&amp; j &lt;= right) {
            if (arr[i] &lt;= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }

        // 左半边还有剩余，直接追加
        while (i &lt;= mid) {
            temp[t++] = arr[i++];
        }

        // 右半边还有剩余，直接追加
        while (j &lt;= right) {
            temp[t++] = arr[j++];
        }

        // 将合并结果写回原数组对应区间
        t = 0;
        int k = left;
        while (k &lt;= right) {
            arr[k++] = temp[t++];
        }
    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 43, 3, 9, 82, 10};
        System.out.println("排序前: " + Arrays.toString(arr));
        mergeSort(arr);
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
</code></pre>
<blockquote>
<p>[!NOTE]
在合并步骤中，如果遇到相等的元素，优先保留左侧子数组中的元素，这也保证了归并排序的稳定性。</p>
</blockquote>
<blockquote>
<p>[!TIP]
如果想优化空间开销，可以把 <code>tmp</code> 复用成一个全局辅助数组，避免每次递归都重新创建临时数组。</p>
</blockquote>
<hr />
<h2>5. 复杂度分析</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n \log n)$。无论最好、最坏还是平均情况，归并排序都要经历“分解 + 合并”两类操作，所以它的时间复杂度都很稳定。</li>
<li><strong>空间复杂度</strong>：$O(n)$，主要来自辅助数组。递归调用本身还会带来 $O(\log n)$ 的栈空间，但通常不作为主导项。</li>
<li><strong>稳定性</strong>：稳定。合并时若相等元素优先取左边，就能保持相同元素的相对顺序。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>快速排序</title>
    <link href="https://blog.yoake.dev/posts/algorithm/quikesort/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/quikesort/</id>
    <published>2026-06-09T03:29:38.000Z</published>
    <updated>2026-06-09T03:29:38.000Z</updated>
    <summary>用挖坑填数法讲清快速排序中的 partition 划分过程，以及基准值最终落位的含义。</summary>
    <content type="html"><![CDATA[<h2>1. 问题引入</h2>
<p>快速排序的核心步骤是 <code>partition</code> 划分：选一个基准元素 <code>pivot</code>，把数组分成两边，使得左边元素都不大于它，右边元素都不小于它。</p>
<p>例如数组 <code>6 3 8 2 7 5</code>，如果以 <code>6</code> 作为基准值，第一次划分后可能变成：</p>
<pre><code>5 3 2 6 7 8
</code></pre>
<p>这时 <code>6</code> 已经落到了最终位置，但左右两边还没有完全有序。</p>
<hr />
<h2>2. 算法策略</h2>
<p>快速排序使用的是分治法：</p>
<p>分解：通过 partition 把数组划分成左右两个子数组
解决：分别递归排序左右两个子数组
合并：快速排序不需要额外合并</p>
<p><code>partition</code> 的任务不是直接排好整个数组，而是先确定一个基准元素的最终位置。</p>
<hr />
<h2>3. partition 的基本思想</h2>
<p>这里使用一种常见写法：<strong>挖坑填数法</strong>。</p>
<p>过程很简单：</p>
<ol>
<li>选择第一个元素作为 <code>pivot</code> 并暂存。</li>
<li>从右往左找小于 <code>pivot</code> 的数，填到左边的坑。</li>
<li>从左往右找大于 <code>pivot</code> 的数，填到右边的坑。</li>
<li>重复以上过程，直到左右指针相遇。</li>
<li>把 <code>pivot</code> 放回相遇位置。</li>
</ol>
<p><img src="https://img.picr2.online/img/2026/06/09/86c2713617d899b6b64e539362e33762.webp" alt="快速排序挖坑过程" /></p>
<hr />
<h2>4. Java 实现</h2>
<pre><code>public class QuickSortPartition {

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[left];
        int i = left;
        int j = right;

        while (i &lt; j) {
            // 从右往左找第一个小于 pivot 的元素
            while (i &lt; j &amp;&amp; arr[j] &gt;= pivot) {
                j--;
            }

            // 把右边找到的小元素填到左边的坑
            arr[i] = arr[j];

            // 从左往右找第一个大于 pivot 的元素
            while (i &lt; j &amp;&amp; arr[i] &lt;= pivot) {
                i++;
            }

            // 把左边找到的大元素填到右边的坑
            arr[j] = arr[i];
        }

        // 左右指针相遇，把 pivot 放回最终位置
        arr[i] = pivot;

        return i;
    }

    public static void main(String[] args) {
        int[] arr = {6, 3, 8, 2, 7, 5};

        int pivotIndex = partition(arr, 0, arr.length - 1);

        System.out.println("pivot 最终位置：" + pivotIndex);

        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}
</code></pre>
<p>输出结果：</p>
<pre><code>pivot 最终位置：3
5 3 2 6 7 8
</code></pre>
<hr />
<h2>5. 执行过程分析</h2>
<p>下面用数组：</p>
<pre><code>6 3 8 2 7 5
</code></pre>
<p>演示一次 <code>partition</code>。</p>
<p>选择第一个元素作为基准值：</p>
<pre><code>pivot = 6
</code></pre>
<p>初始状态如下：</p>
<pre><code>[6, 3, 8, 2, 7, 5]
 i              j
</code></pre>
<p>其中：</p>
<pre><code>i = 0
j = 5
pivot = 6
</code></pre>
<hr />
<h2>6. 第一次右扫描</h2>
<p>代码先执行：</p>
<pre><code>while (i &lt; j &amp;&amp; arr[j] &gt;= pivot) {
    j--;
}
</code></pre>
<p>从右往左找第一个小于 <code>6</code> 的元素。</p>
<p>此时：</p>
<pre><code>arr[5] = 5
</code></pre>
<p>因为：</p>
<p>$$
5 &lt; 6
$$</p>
<p>所以右扫描停止。</p>
<p>把 <code>5</code> 填到左边的坑：</p>
<pre><code>arr[i] = arr[j];
</code></pre>
<p>数组变为：</p>
<pre><code>[5, 3, 8, 2, 7, 5]
 i              j
</code></pre>
<p>左边的坑被填上了，右边位置 <code>j</code> 变成新的坑。</p>
<hr />
<h2>7. 第一次左扫描</h2>
<p>接着执行：</p>
<pre><code>while (i &lt; j &amp;&amp; arr[i] &lt;= pivot) {
    i++;
}
</code></pre>
<p>从左往右找第一个大于 <code>6</code> 的元素。</p>
<p>扫描过程：</p>
<pre><code>arr[0] = 5 &lt;= 6，i 右移
arr[1] = 3 &lt;= 6，i 右移
arr[2] = 8 &gt; 6，停止
</code></pre>
<p>此时：</p>
<pre><code>[5, 3, 8, 2, 7, 5]
       i        j
</code></pre>
<p>把 <code>8</code> 填到右边的坑：</p>
<pre><code>arr[j] = arr[i];
</code></pre>
<p>数组变为：</p>
<pre><code>[5, 3, 8, 2, 7, 8]
       i        j
</code></pre>
<p>右边的坑被填上了，左边位置 <code>i</code> 变成新的坑。</p>
<hr />
<h2>8. 第二次右扫描</h2>
<p>继续从右往左找小于 <code>6</code> 的元素。</p>
<p>当前：</p>
<pre><code>[5, 3, 8, 2, 7, 8]
       i        j
</code></pre>
<p>扫描过程：</p>
<pre><code>arr[5] = 8 &gt;= 6，j 左移
arr[4] = 7 &gt;= 6，j 左移
arr[3] = 2 &lt; 6，停止
</code></pre>
<p>此时：</p>
<pre><code>[5, 3, 8, 2, 7, 8]
       i  j
</code></pre>
<p>把 <code>2</code> 填到左边的坑：</p>
<pre><code>arr[i] = arr[j];
</code></pre>
<p>数组变为：</p>
<pre><code>[5, 3, 2, 2, 7, 8]
       i  j
</code></pre>
<p>这里出现两个 <code>2</code> 并不是错误，因为 <code>pivot = 6</code> 已经被单独保存起来了，数组只是不断在填坑，最后会把 <code>pivot</code> 放回去。</p>
<hr />
<h2>9. 第二次左扫描</h2>
<p>继续从左往右找大于 <code>6</code> 的元素。</p>
<p>当前：</p>
<pre><code>[5, 3, 2, 2, 7, 8]
       i  j
</code></pre>
<p>扫描过程：</p>
<pre><code>arr[2] = 2 &lt;= 6，i 右移
</code></pre>
<p>此时：</p>
<pre><code>i == j
</code></pre>
<p>左右指针相遇，循环结束。</p>
<p>把 <code>pivot</code> 放到相遇位置：</p>
<pre><code>arr[i] = pivot;
</code></pre>
<p>数组变为：</p>
<pre><code>[5, 3, 2, 6, 7, 8]
          i
</code></pre>
<p>返回下标：</p>
<pre><code>3
</code></pre>
<hr />
<h2>10. partition 结束后的含义</h2>
<p>最终结果：</p>
<pre><code>5 3 2 6 7 8
</code></pre>
<p>可以看成：</p>
<pre><code>5 3 2 | 6 | 7 8
</code></pre>
<p>其中：</p>
<pre><code>6 左边的元素都 &lt;= 6
6 右边的元素都 &gt;= 6
6 的最终位置已经确定
</code></pre>
<p>但左边的 <code>5 3 2</code> 还没有完全有序，所以快速排序还要继续递归处理左右两边。</p>
<hr />
<h2>11. 完整快速排序 Java 代码</h2>
<pre><code>public class QuickSort {

    public static void quickSort(int[] arr, int left, int right) {
        if (left &gt;= right) {
            return;
        }

        int pivotIndex = partition(arr, left, right);

        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[left];
        int i = left;
        int j = right;

        while (i &lt; j) {
            while (i &lt; j &amp;&amp; arr[j] &gt;= pivot) {
                j--;
            }

            arr[i] = arr[j];

            while (i &lt; j &amp;&amp; arr[i] &lt;= pivot) {
                i++;
            }

            arr[j] = arr[i];
        }

        arr[i] = pivot;

        return i;
    }

    public static void main(String[] args) {
        int[] arr = {6, 3, 8, 2, 7, 5};

        quickSort(arr, 0, arr.length - 1);

        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}
</code></pre>
<p>输出结果：</p>
<pre><code>2 3 5 6 7 8
</code></pre>
<hr />
<h2>12. 复杂度分析</h2>
<h3>12.1 单次 partition 的复杂度</h3>
<p>一次 partition 中，左右指针整体只会从两端向中间移动一遍。</p>
<p>所以单次 partition 的时间复杂度为：</p>
<p>$$
O(n)
$$</p>
<p>空间复杂度为：</p>
<p>$$
O(1)
$$</p>
<hr />
<h3>12.2 快速排序整体复杂度</h3>
<p>快速排序的复杂度取决于每次划分是否均衡。</p>
<p>如果每次都能把数组大致分成两半，那么时间复杂度为：</p>
<p>$$
O(n \log n)
$$</p>
<p>如果每次选到的 <code>pivot</code> 都是最大值或最小值，那么划分会很不均衡，时间复杂度会退化为：</p>
<p>$$
O(n^2)
$$</p>
<p>常见情况如下：</p>
<table>
<thead>
<tr>
<th>情况</th>
<th>时间复杂度</th>
</tr>
</thead>
<tbody>
<tr>
<td>最好情况</td>
<td>$O(n \log n)$</td>
</tr>
<tr>
<td>平均情况</td>
<td>$O(n \log n)$</td>
</tr>
<tr>
<td>最坏情况</td>
<td>$O(n^2)$</td>
</tr>
</tbody>
</table>
<hr />
<h2>13. 常见优化</h2>
<p>如果数组本身接近有序，而每次都选择第一个元素作为 <code>pivot</code>，快速排序很容易退化。</p>
<p>常见优化方法有：</p>
<ol>
<li>随机选择 pivot</li>
<li>三数取中选择 pivot</li>
<li>小区间改用插入排序</li>
</ol>
<p>其中随机选择 <code>pivot</code> 可以降低每次都选到极端值的概率。</p>
<hr />
<h2>14. 总结</h2>
<p>快速排序 partition 划分可以概括为：</p>
<blockquote>
<p>选基准，左右扫；小的填左边，大的填右边；指针相遇，基准归位。</p>
</blockquote>
<p>核心内容如下：</p>
<ul>
<li>partition 是快速排序的核心操作。</li>
<li>通常选择一个元素作为 <code>pivot</code>。</li>
<li>左右指针不是同时扫描，而是交替扫描。</li>
<li>挖坑填数法会先从右往左找小元素，再从左往右找大元素。</li>
<li>partition 结束后，<code>pivot</code> 的位置已经确定。</li>
<li>partition 结束后，左右两边不一定完全有序。</li>
<li>快速排序继续递归处理左右子数组。</li>
<li>单次 partition 时间复杂度是 $O(n)$。</li>
<li>快速排序平均时间复杂度是 $O(n \log n)$，最坏是 $O(n^2)$。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>素数环问题（回溯法）</title>
    <link href="https://blog.yoake.dev/posts/algorithm/primeringbacktracking/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/primeringbacktracking/</id>
    <published>2026-06-08T06:49:15.000Z</published>
    <updated>2026-06-08T06:49:15.000Z</updated>
    <summary>通过回溯法解决素数环问题，理解排列树、剪枝、奇偶性约束与素数判定。</summary>
    <content type="html"><![CDATA[<h2>1. 问题描述</h2>
<p>素数环问题，也叫 <strong>Prime Ring Problem</strong>。</p>
<p>给定正整数 $n$，要求将 $1$ 到 $n$ 这 $n$ 个数字排成一个环，使得任意两个相邻数字之和都是素数。</p>
<p>例如，当 $n=8$ 时，一个合法的素数环为：</p>
<p>$$
1,\ 2,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4
$$</p>
<p>因为：</p>
<p>$$
1+2=3
$$</p>
<p>$$
2+3=5
$$</p>
<p>$$
3+8=11
$$</p>
<p>$$
8+5=13
$$</p>
<p>$$
5+6=11
$$</p>
<p>$$
6+7=13
$$</p>
<p>$$
7+4=11
$$</p>
<p>$$
4+1=5
$$</p>
<p>这些和全部都是素数。</p>
<p>注意，最后一个数和第一个数也是相邻的，因为它们组成的是一个环。</p>
<hr />
<h2>2. 算法策略</h2>
<p>素数环问题使用：</p>
<p>$$
\boxed{\text{回溯法}}
$$</p>
<p>回溯法可以理解为一种深度优先搜索，核心思想是：</p>
<blockquote>
<p>能继续就继续往下搜索，走不通就退回来，换一个选择再试。</p>
</blockquote>
<p>素数环问题是在 $1$ 到 $n$ 的所有排列中，寻找满足条件的排列。如果直接枚举所有排列，复杂度很高。例如 $n=8$ 时，排列总数为：</p>
<p>$$
8! = 40320
$$</p>
<p>但实际上，很多排列刚排到一半就已经不可能成为答案。例如：</p>
<p>$$
1,\ 3
$$</p>
<p>因为：</p>
<p>$$
1+3=4
$$</p>
<p>$4$ 不是素数，所以后面无论怎么排，都不可能形成合法素数环。</p>
<p>这时候就可以直接剪枝。</p>
<hr />
<h2>3. 数学性质：奇偶性约束</h2>
<p>素数环问题有一个很重要的性质：<strong>相邻数字必须一奇一偶</strong>。因为除了 $2$ 之外，所有素数都是奇数。</p>
<p>两个整数相加时：</p>
<table>
<thead>
<tr>
<th>情况</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td>奇数 + 奇数</td>
<td>偶数</td>
</tr>
<tr>
<td>偶数 + 偶数</td>
<td>偶数</td>
</tr>
<tr>
<td>奇数 + 偶数</td>
<td>奇数</td>
</tr>
</tbody>
</table>
<p>在素数环中，相邻两个数的和最小是 $1+2=3$，因此和不可能是 $2$。所以相邻两个数的和如果是素数，就只能是奇数，两个数字也就必须一奇一偶。因此，整个环必须满足：</p>
<p>$$
\text{奇},\ \text{偶},\ \text{奇},\ \text{偶},\ \ldots
$$</p>
<p>交替排列。</p>
<p>这也说明：当 $n&gt;1$ 且 $n$ 为奇数时，素数环无解，因为 $1$ 到 $n$ 中奇数和偶数数量不相等，无法形成完整的奇偶交替闭环。</p>
<hr />
<h2>4. 固定第一个数为 1</h2>
<p>素数环是一个环，所以旋转后本质上还是同一个环。例如：</p>
<p>$$
1,\ 2,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4
$$</p>
<p>和：</p>
<p>$$
2,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4,\ 1
$$</p>
<p>其实表示的是同一个环，只是起点不同。</p>
<p>为了避免这种旋转重复，通常固定第一个位置为：</p>
<p>$$
x[1] = 1
$$</p>
<p>这样可以减少重复答案，也可以缩小搜索空间。</p>
<hr />
<h2>5. 解空间与排列树</h2>
<p>可以用数组表示当前排列：</p>
<p>$$
x = (x_1,\ x_2,\ \ldots,\ x_n)
$$</p>
<p>其中：</p>
<p>$$
x_1 = 1
$$</p>
<p>其余位置从没有使用过的数字中选择，并要求相邻两数之和为素数，且最后一个数与第一个数之和也为素数。</p>
<p>因为每一层都要从剩余数字中选择一个数，所以解空间是一棵<strong>排列树</strong>。</p>
<p>以 $n=8$ 为例：</p>
<pre><code>第 1 层：固定 1
第 2 层：从 2 到 8 中选择一个数
第 3 层：继续从未使用的数字中选择一个数
……
第 8 层：形成完整排列
</code></pre>
<p>搜索过程中不是所有分支都会继续展开。如果新加入的数字和前一个数字之和不是素数，就直接剪枝。</p>
<hr />
<h2>6. 剪枝条件</h2>
<p>素数环问题主要有两个剪枝条件。</p>
<h3>6.1 相邻和不是素数，直接剪枝</h3>
<p>每放入一个新数字 $x_i$，都要检查：</p>
<p>$$
x_{i-1} + x_i
$$</p>
<p>是否为素数。</p>
<p>如果不是素数，当前分支直接结束。例如：</p>
<p>$$
1,\ 3
$$</p>
<p>因为：</p>
<p>$$
1+3=4
$$</p>
<p>不是素数，所以这一支不需要继续搜索。</p>
<hr />
<h3>6.2 填满后检查首尾</h3>
<p>由于这是一个环，所以当所有数字都放完后，还要检查最后一个数字和第一个数字的和：</p>
<p>$$
x_n + x_1
$$</p>
<p>是否为素数。</p>
<p>只有首尾也满足条件，才是一个完整的素数环。</p>
<hr />
<h2>7. 算法步骤</h2>
<ol>
<li>输入正整数 $n$。</li>
<li>如果 $n&gt;1$ 且 $n$ 为奇数，直接判断无解。</li>
<li>预处理 $2n$ 范围内的素数表。</li>
<li>定义数组 <code>path</code> 保存当前排列。</li>
<li>定义数组 <code>visited</code> 标记数字是否已经使用。</li>
<li>固定 <code>path[0] = 1</code>。</li>
<li>从第二个位置开始回溯搜索。</li>
<li>每次尝试放入一个未使用的数字。</li>
<li>如果它与前一个数字之和是素数，则继续递归。</li>
<li>如果不满足条件，则剪枝。</li>
<li>当所有位置填满时，检查最后一个数和第一个数之和是否为素数。</li>
<li>如果满足，输出当前排列。</li>
</ol>
<hr />
<h2>8. 以 n = 8 为例</h2>
<p>固定第一个数为：</p>
<p>$$
1
$$</p>
<p>第二个数必须满足：</p>
<p>$$
1+x
$$</p>
<p>是素数。</p>
<p>所以可以选择：</p>
<p>$$
2,\ 4,\ 6
$$</p>
<p>因为：</p>
<p>$$
1+2=3
$$</p>
<p>$$
1+4=5
$$</p>
<p>$$
1+6=7
$$</p>
<p>都是素数。</p>
<p>而：</p>
<p>$$
1+3=4
$$</p>
<p>$$
1+5=6
$$</p>
<p>$$
1+7=8
$$</p>
<p>$$
1+8=9
$$</p>
<p>都不是素数，所以这些分支直接剪掉。</p>
<p>当 $n=8$ 时，固定 $1$ 在首位，可以得到 4 个素数环：</p>
<pre><code>1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2
</code></pre>
<hr />
<h2>9. Java 实现</h2>
<pre><code>import java.util.Arrays;

public class PrimeRing {

    private static int n;
    private static int[] path;        // 存储当前排列
    private static boolean[] visited; // 标记数字是否已经使用
    private static boolean[] isPrime; // 素数表

    public static void main(String[] args) {
        int inputN = 8;

        System.out.println("n = " + inputN + " 时的素数环结果：");
        solvePrimeRing(inputN);
    }

    public static void solvePrimeRing(int inputN) {
        n = inputN;

        // 如果 n &gt; 1 且 n 是奇数，则无法形成奇偶交替的闭环
        if (n &gt; 1 &amp;&amp; n % 2 == 1) {
            System.out.println("无解");
            return;
        }

        path = new int[n];
        // 下标直接对应数字，避免出现“差一错误”
        visited = new boolean[n + 1];

        // 相邻两数之和最大为 2n
        precomputePrimes(2 * n);

        // 固定第一个数为 1，避免旋转重复
        path[0] = 1;
        visited[1] = true;

        // 从下标 1 开始填数
        backtrack(1);
    }

    private static void backtrack(int index) {
        // 已经填满 n 个数
        if (index == n) {
            // 检查最后一个数和第一个数之和是否为素数
            if (isPrime[path[n - 1] + path[0]]) {
                printPath();
            }
            return;
        }

        // 从 2 到 n 尝试放入未使用的数字
        for (int i = 2; i &lt;= n; i++) {
            // 当前数字没有使用过，并且与前一个数字之和为素数
            if (!visited[i] &amp;&amp; isPrime[path[index - 1] + i]) {
                path[index] = i;
                visited[i] = true;

                backtrack(index + 1);

                // 回溯：撤销选择
                visited[i] = false;
            }
        }
    }

    private static void precomputePrimes(int max) {
        isPrime = new boolean[max + 1];
        Arrays.fill(isPrime, true);

        if (max &gt;= 0) {
            isPrime[0] = false;
        }

        if (max &gt;= 1) {
            isPrime[1] = false;
        }

        for (int p = 2; p * p &lt;= max; p++) {
            if (isPrime[p]) {
                for (int i = p * p; i &lt;= max; i += p) {
                    isPrime[i] = false;
                }
            }
        }
    }

    private static void printPath() {
        for (int i = 0; i &lt; n; i++) {
            System.out.print(path[i]);

            if (i != n - 1) {
                System.out.print(" ");
            }
        }

        System.out.println();
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>n = 8 时的素数环结果：
1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2
</code></pre>
<hr />
<h2>10. 代码解释</h2>
<h3>10.1 <code>path</code> 数组</h3>
<pre><code>private static int[] path;
</code></pre>
<p><code>path</code> 用来保存当前已经构造出的排列。</p>
<p>例如：</p>
<pre><code>1 2 3
</code></pre>
<p>表示当前已经放好了前三个数字。</p>
<hr />
<h3>10.2 <code>visited</code> 数组</h3>
<pre><code>private static boolean[] visited;
</code></pre>
<p><code>visited[i]</code> 表示数字 <code>i</code> 是否已经被使用。</p>
<p>这里把 <code>visited</code> 的大小定义为 <code>n + 1</code>，是为了让数字和数组下标一一对应。比如数字 <code>6</code> 就直接用 <code>visited[6]</code> 表示，而不需要写成 <code>visited[5]</code>。这样虽然空出了 <code>visited[0]</code>，但可以减少下标偏移带来的差一错误。</p>
<p>如果：</p>
<pre><code>visited[3] = true;
</code></pre>
<p>表示数字 <code>3</code> 已经在当前排列中，后面不能重复使用。</p>
<hr />
<h3>10.3 <code>isPrime</code> 数组</h3>
<pre><code>private static boolean[] isPrime;
</code></pre>
<p><code>isPrime[x]</code> 表示数字 <code>x</code> 是否是素数。</p>
<p>例如：</p>
<pre><code>isPrime[7] = true;
isPrime[8] = false;
</code></pre>
<p>因为 $7$ 是素数，$8$ 不是素数。</p>
<hr />
<h3>10.4 埃拉托斯特尼筛法</h3>
<p>代码中的 <code>precomputePrimes</code> 使用的是<strong>埃拉托斯特尼筛法</strong>，简称“埃氏筛”。</p>
<p>它的作用是：在回溯开始前，先把 $0$ 到 <code>max</code> 范围内的素数全部算出来，保存到 <code>isPrime</code> 数组中。之后判断相邻和是否为素数时，只需要查表：</p>
<pre><code>isPrime[sum]
</code></pre>
<p>时间复杂度就是 $O(1)$。</p>
<p>埃氏筛的思路是：先假设所有大于等于 $2$ 的数都是素数，然后从小到大枚举每个数。如果当前数 $p$ 仍然是素数，就把它的倍数标记为非素数。</p>
<p>以 <code>max = 10</code> 为例：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>操作</th>
<th>剩下仍可能是素数的数</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始化</td>
<td>排除 $0$ 和 $1$</td>
<td>$2, 3, 4, 5, 6, 7, 8, 9, 10$</td>
</tr>
<tr>
<td>$p = 2$</td>
<td>筛掉 $4, 6, 8, 10$</td>
<td>$2, 3, 5, 7, 9$</td>
</tr>
<tr>
<td>$p = 3$</td>
<td>筛掉 $9$</td>
<td>$2, 3, 5, 7$</td>
</tr>
</tbody>
</table>
<p>最终留下来的 $2, 3, 5, 7$，就是 $10$ 以内的素数。</p>
<p>代码中内层循环从：</p>
<pre><code>int i = p * p;
</code></pre>
<p>开始，而不是从 <code>2 * p</code> 开始，是因为更小的倍数已经被前面的数筛过了。比如枚举到 $3$ 时，$6 = 2 \times 3$ 已经在 $p = 2$ 的时候被筛掉了，所以直接从 $9 = 3 \times 3$ 开始即可。</p>
<p>外层循环条件是：</p>
<pre><code>p * p &lt;= max
</code></pre>
<p>原因是：如果一个数 $N$ 是合数，那么它一定可以写成 $N = a \times b$，其中至少有一个因数不超过 $\sqrt{N}$。所以只要筛到 $\sqrt{\text{max}}$，就足够把 <code>max</code> 以内的合数全部筛掉。</p>
<p>在素数环问题中，相邻两个数的最大和是：</p>
<p>$$
n + (n - 1) &lt; 2n
$$</p>
<p>为了写起来更方便，代码直接预处理到 <code>2 * n</code>，之后判断某个和是否为素数时，只需要访问：</p>
<pre><code>isPrime[path[index - 1] + i]
</code></pre>
<p>:::tip[空间换时间]
埃氏筛会多用一个 <code>isPrime</code> 数组，但能把回溯过程中的素数判断变成查表。回溯会反复尝试很多组合，这种预处理通常很划算。
:::</p>
<hr />
<h3>10.5 回溯函数</h3>
<pre><code>private static void backtrack(int index)
</code></pre>
<p><code>index</code> 表示当前要填写的位置。</p>
<p>如果：</p>
<pre><code>index == n
</code></pre>
<p>说明所有位置都已经填完，需要检查首尾是否也满足素数条件。</p>
<p>如果还没有填完，就遍历所有还没使用过的数字，尝试放到当前位置。</p>
<hr />
<h3>10.6 回溯过程</h3>
<p>核心代码是：</p>
<pre><code>if (!visited[i] &amp;&amp; isPrime[path[index - 1] + i]) {
    path[index] = i;
    visited[i] = true;

    backtrack(index + 1);

    visited[i] = false;
}
</code></pre>
<p>这段代码可以分成四步：</p>
<pre><code>1. 判断数字 i 是否可以放入当前位置。
2. 如果可以，就把 i 放入 path[index]。
3. 递归搜索下一个位置。
4. 搜索结束后撤销选择，尝试其他数字。
</code></pre>
<p>其中 <code>visited[i] = false;</code> 就是回溯的关键，它表示当前分支搜索完了，把这个数字拿出来，换别的数字试试。</p>
<hr />
<h2>11. 复杂度分析</h2>
<p>在最坏情况下，如果不考虑剪枝，需要枚举排列，复杂度接近：</p>
<p>$$
O((n-1)!)
$$</p>
<p>因为第一个位置固定为 $1$，所以只需要排列剩下的 $n-1$ 个数字。</p>
<p>但实际运行中，由于有“相邻和为素数”和“奇偶交替”两个强剪枝条件，搜索空间会大大减少。</p>
<p>空间复杂度主要来自：</p>
<ul>
<li><code>path</code> 数组：$O(n)$</li>
<li><code>visited</code> 数组：$O(n)$</li>
<li>递归调用栈：$O(n)$</li>
<li><code>isPrime</code> 数组：$O(n)$</li>
</ul>
<p>所以整体空间复杂度为：</p>
<p>$$
O(n)
$$</p>
<hr />
<h2>12. 总结</h2>
<p>素数环问题可以概括为：</p>
<blockquote>
<p>全排列搜索 + 素数条件剪枝 + 首尾相连检查。</p>
</blockquote>
<p>核心内容如下：</p>
<ul>
<li>使用回溯法解决。</li>
<li>固定第一个数字为 $1$，避免旋转重复。</li>
<li>使用 <code>visited</code> 数组避免重复使用数字。</li>
<li>每次加入新数字时，检查它和前一个数字之和是否为素数。</li>
<li>填满后，再检查最后一个数字和第一个数字之和是否为素数。</li>
<li>如果 $n&gt;1$ 且 $n$ 为奇数，直接无解。</li>
<li>预处理素数表，可以把素数判断变成 $O(1)$。</li>
</ul>
<p>口诀：</p>
<blockquote>
<p>素数环问题就是在排列树上做回溯搜索，每一步用“相邻和为素数”进行剪枝。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>矩阵连乘问题（动态规划）</title>
    <link href="https://blog.yoake.dev/posts/algorithm/matrixchain/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/matrixchain/</id>
    <published>2026-06-05T12:45:48.000Z</published>
    <updated>2026-06-05T12:45:48.000Z</updated>
    <summary>探讨如何通过动态规划找到矩阵相乘的最优顺序，以最小化计算次数。</summary>
    <content type="html"><![CDATA[<h2>1. 问题描述</h2>
<p>给定矩阵序列 $A_1, A_2, \ldots, A_n$，矩阵相乘顺序不能改变，但可以改变加括号的方式。</p>
<p><strong>目标</strong>：找到一种加括号方式，使矩阵连乘的总计算次数最少。</p>
<p>需要注意的是，矩阵连乘问题不是要求真正算出矩阵乘积，而是要求找到最优计算顺序。</p>
<hr />
<h2>2. 括号的位置会影响计算次数</h2>
<p>矩阵相乘有一个很容易被忽略的点：<strong>先算谁，会改变中间结果的规模</strong>。</p>
<p>对于两个矩阵 $A(p \times q)$ 和 $B(q \times r)$，它们相乘需要的计算次数是：</p>
<p>$$
p \times q \times r
$$</p>
<p>而乘完之后，得到的新矩阵规模是：</p>
<p>$$
p \times r
$$</p>
<p>中间的 $q$ 只负责“对齐”两个矩阵，真正留下来的只有外侧的 $p$ 和 $r$：</p>
<p>$$
(p \times q) \times (q \times r) = p \times r
$$</p>
<p>这就带来一个问题：如果有多个矩阵连乘，某一步产生的中间矩阵太大，后面的计算量也会被一起放大。</p>
<p>来看一个例子，维度数组为：</p>
<p>$$
p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]
$$</p>
<p>对应 5 个矩阵：</p>
<p>$$
A_1(2 \times 3),\quad A_2(3 \times 6),\quad A_3(6 \times 4),\quad A_4(4 \times 2),\quad A_5(2 \times 7)
$$</p>
<p>矩阵顺序不能换，仍然只能按 $A_1A_2A_3A_4A_5$ 来乘。但括号可以换，而这正好决定了先算哪一部分。</p>
<p>例如：</p>
<table>
<thead>
<tr>
<th>括号方式</th>
<th>总次数</th>
</tr>
</thead>
<tbody>
<tr>
<td>$((((A_1A_2)A_3)A_4)A_5)$</td>
<td>214</td>
</tr>
<tr>
<td>$((A_1(A_2(A_3A_4)))A_5)$</td>
<td><strong>124</strong></td>
</tr>
</tbody>
</table>
<p>可以看到，两种写法的数学结果一样，但计算量不同。</p>
<p>所以，矩阵连乘问题真正关心的不是乘积本身，而是：</p>
<blockquote>
<p>先算哪一部分，才能让总计算量最小。</p>
</blockquote>
<hr />
<h2>3. 算法策略</h2>
<p>矩阵连乘问题使用：</p>
<p>$$
\boxed{\text{动态规划}}
$$</p>
<p>原因是它满足动态规划的两个特点：</p>
<ul>
<li><strong>最优子结构</strong>：整体最优解可以由子问题的最优解组成。</li>
<li><strong>重叠子问题</strong>：不同括号方式中会反复用到相同的子问题。</li>
</ul>
<p>例如要求 $A_1A_2A_3$ 的最优计算方式，可以拆成：</p>
<p>$$
A_1 \mid A_2A_3
$$</p>
<p>或：</p>
<p>$$
A_1A_2 \mid A_3
$$</p>
<p>每一种断开方式都会产生左右两个子问题。动态规划的思路就是先求小区间的最优值，再逐步推出大区间的最优值。</p>
<hr />
<h2>4. 状态定义</h2>
<p>设矩阵 $A_i$ 的规模为：</p>
<p>$$
A_i = p_{i-1} \times p_i
$$</p>
<p>其中，$p$ 是维度数组。</p>
<p>例如：</p>
<p>$$
p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]
$$</p>
<p>则：</p>
<p>$$
A_1 = p_0 \times p_1 = 2 \times 3
$$</p>
<p>$$
A_2 = p_1 \times p_2 = 3 \times 6
$$</p>
<p>$$
A_3 = p_2 \times p_3 = 6 \times 4
$$</p>
<p>$$
A_4 = p_3 \times p_4 = 4 \times 2
$$</p>
<p>$$
A_5 = p_4 \times p_5 = 2 \times 7
$$</p>
<p>所以，矩阵 $A_i$ 的行数是 $p_{i-1}$，列数是 $p_i$。</p>
<p>这里容易混淆的一点是：</p>
<ul>
<li>$A_1, A_2, A_3$ 是矩阵编号，通常从 $1$ 开始。</li>
<li>$p_0, p_1, p_2, p_3$ 是维度数组下标，通常从 $0$ 开始。</li>
</ul>
<p>所以才会有：</p>
<p>$$
A_i = p_{i-1} \times p_i
$$</p>
<hr />
<p>动态规划中主要使用两张表：$m$ 表和 $s$ 表。</p>
<p>$$
m[i][j] = \text{计算 } A_iA_{i+1}\cdots A_j \text{ 的最少乘法次数}
$$</p>
<p>$$
s[i][j] = \text{取得最优值时的断开位置 } k
$$</p>
<table>
<thead>
<tr>
<th>表</th>
<th>含义</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>$m[i][j]$</td>
<td>从 $A_i$ 乘到 $A_j$ 的最少乘法次数</td>
<td>记录最优值</td>
</tr>
<tr>
<td>$s[i][j]$</td>
<td>从 $A_i$ 乘到 $A_j$ 时的最优断点</td>
<td>还原最优括号</td>
</tr>
</tbody>
</table>
<p>例如：</p>
<p>$$
m[1][5]
$$</p>
<p>表示计算 $A_1A_2A_3A_4A_5$ 的最少乘法次数。</p>
<p>如果：</p>
<p>$$
s[1][5] = 4
$$</p>
<p>表示最优断点在 $A_4$ 后面：</p>
<p>$$
A_1A_2A_3A_4 \mid A_5
$$</p>
<p>初始条件为：</p>
<p>$$
m[i][i] = 0
$$</p>
<p>因为单个矩阵不需要相乘。</p>
<hr />
<h2>5. 状态转移方程</h2>
<p>要求 $m[i][j]$，也就是要求 $A_i \cdots A_j$ 的最少乘法次数。</p>
<p>可以在某个位置 $k$ 处断开：</p>
<p>$$
A_i \cdots A_k \mid A_{k+1} \cdots A_j
$$</p>
<p>其中：</p>
<p>$$
i \le k &lt; j
$$</p>
<p>总代价由三部分组成：</p>
<p>$$
\text{总代价} = \text{左半部分代价} + \text{右半部分代价} + \text{最后合并代价}
$$</p>
<p>所以状态转移方程为：</p>
<p>$$
m[i][j] =
\min_{i \le k &lt; j}
(
m[i][k] + m[k+1][j] + p_{i-1} \cdot p_k \cdot p_j
)
$$</p>
<p>其中：</p>
<ul>
<li>$m[i][k]$ 表示左半部分 $A_i \cdots A_k$ 的最少乘法次数。</li>
<li>$m[k+1][j]$ 表示右半部分 $A_{k+1} \cdots A_j$ 的最少乘法次数。</li>
<li>$p_{i-1} \cdot p_k \cdot p_j$ 表示左右两部分最后合并时的乘法次数。</li>
<li>$k$ 表示当前尝试的断点。</li>
</ul>
<p>为什么最后合并代价是：</p>
<p>$$
p_{i-1} \cdot p_k \cdot p_j
$$</p>
<p>因为一串矩阵相乘后，结果矩阵的规模等于：</p>
<p>$$
\text{第一个矩阵的行数} \times \text{最后一个矩阵的列数}
$$</p>
<p>所以：</p>
<p>$$
A_i \cdots A_k \text{ 的结果规模为 } p_{i-1} \times p_k
$$</p>
<p>$$
A_{k+1} \cdots A_j \text{ 的结果规模为 } p_k \times p_j
$$</p>
<p>最后合并时，相当于计算：</p>
<p>$$
(p_{i-1} \times p_k) \times (p_k \times p_j)
$$</p>
<p>因此乘法次数为：</p>
<p>$$
p_{i-1} \cdot p_k \cdot p_j
$$</p>
<p>用一句话理解这个公式：</p>
<blockquote>
<p>枚举所有断点 $k$，把矩阵链分成左右两部分，先算左右两边的最优代价，再加上最后合并的代价，最后取最小值。</p>
</blockquote>
<hr />
<h2>6. 算法步骤</h2>
<ol>
<li>初始化 $m[i][i] = 0$。</li>
<li>按矩阵链长度 $l = 2, 3, \ldots, n$ 从短到长填表。</li>
<li>对每个区间 $[i,\ j]$，枚举断点 $k$。</li>
<li>使用状态转移方程计算当前代价。</li>
<li>如果当前代价更小，就更新 $m[i][j]$，并把断点 $k$ 记录到 $s[i][j]$。</li>
<li>最后，$m[1][n]$ 就是最少乘法次数。</li>
<li>根据 $s$ 表可以还原最优加括号方式。</li>
</ol>
<hr />
<h2>7. 样例计算</h2>
<p>设维度数组为：</p>
<p>$$
p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]
$$</p>
<p>对应矩阵为：</p>
<p>$$
A_1(2 \times 3),\quad A_2(3 \times 6),\quad A_3(6 \times 4),\quad A_4(4 \times 2),\quad A_5(2 \times 7)
$$</p>
<hr />
<h3>7.1 长度为 2</h3>
<p>先计算两个矩阵相乘的情况。</p>
<p>$$
m[1][2] = 2 \times 3 \times 6 = 36
$$</p>
<p>$$
m[2][3] = 3 \times 6 \times 4 = 72
$$</p>
<p>$$
m[3][4] = 6 \times 4 \times 2 = 48
$$</p>
<p>$$
m[4][5] = 4 \times 2 \times 7 = 56
$$</p>
<hr />
<h3>7.2 长度为 3</h3>
<p>计算 $m[1][3]$：</p>
<p>断点 $k=1$：</p>
<p>$$
m[1][1] + m[2][3] + p_0p_1p_3
= 0 + 72 + 2 \times 3 \times 4 = 96
$$</p>
<p>断点 $k=2$：</p>
<p>$$
m[1][2] + m[3][3] + p_0p_2p_3
= 36 + 0 + 2 \times 6 \times 4 = 84
$$</p>
<p>所以：</p>
<p>$$
m[1][3] = 84,\quad s[1][3] = 2
$$</p>
<hr />
<p>计算 $m[2][4]$：</p>
<p>断点 $k=2$：</p>
<p>$$
0 + 48 + 3 \times 6 \times 2 = 84
$$</p>
<p>断点 $k=3$：</p>
<p>$$
72 + 0 + 3 \times 4 \times 2 = 96
$$</p>
<p>所以：</p>
<p>$$
m[2][4] = 84,\quad s[2][4] = 2
$$</p>
<hr />
<p>计算 $m[3][5]$：</p>
<p>断点 $k=3$：</p>
<p>$$
0 + 56 + 6 \times 4 \times 7 = 224
$$</p>
<p>断点 $k=4$：</p>
<p>$$
48 + 0 + 6 \times 2 \times 7 = 132
$$</p>
<p>所以：</p>
<p>$$
m[3][5] = 132,\quad s[3][5] = 4
$$</p>
<hr />
<h3>7.3 长度为 4</h3>
<p>计算 $m[1][4]$：</p>
<p>断点 $k=1$：</p>
<p>$$
0 + 84 + 2 \times 3 \times 2 = 96
$$</p>
<p>断点 $k=2$：</p>
<p>$$
36 + 48 + 2 \times 6 \times 2 = 108
$$</p>
<p>断点 $k=3$：</p>
<p>$$
84 + 0 + 2 \times 4 \times 2 = 100
$$</p>
<p>所以：</p>
<p>$$
m[1][4] = 96,\quad s[1][4] = 1
$$</p>
<hr />
<p>计算 $m[2][5]$：</p>
<p>断点 $k=2$：</p>
<p>$$
0 + 132 + 3 \times 6 \times 7 = 258
$$</p>
<p>断点 $k=3$：</p>
<p>$$
72 + 56 + 3 \times 4 \times 7 = 212
$$</p>
<p>断点 $k=4$：</p>
<p>$$
84 + 0 + 3 \times 2 \times 7 = 126
$$</p>
<p>所以：</p>
<p>$$
m[2][5] = 126,\quad s[2][5] = 4
$$</p>
<hr />
<h3>7.4 长度为 5</h3>
<p>计算 $m[1][5]$：</p>
<p>断点 $k=1$：</p>
<p>$$
0 + 126 + 2 \times 3 \times 7 = 168
$$</p>
<p>断点 $k=2$：</p>
<p>$$
36 + 132 + 2 \times 6 \times 7 = 252
$$</p>
<p>断点 $k=3$：</p>
<p>$$
84 + 56 + 2 \times 4 \times 7 = 196
$$</p>
<p>断点 $k=4$：</p>
<p>$$
96 + 0 + 2 \times 2 \times 7 = 124
$$</p>
<p>所以：</p>
<p>$$
m[1][5] = 124,\quad s[1][5] = 4
$$</p>
<hr />
<h2>8. 最优值表与决策表</h2>
<h3>8.1 最优值表 $m[i][j]$</h3>
<table>
<thead>
<tr>
<th>$m[i][j]$</th>
<th>$j=1$</th>
<th>$j=2$</th>
<th>$j=3$</th>
<th>$j=4$</th>
<th>$j=5$</th>
</tr>
</thead>
<tbody>
<tr>
<td>$i=1$</td>
<td>0</td>
<td>36</td>
<td>84</td>
<td>96</td>
<td><strong>124</strong></td>
</tr>
<tr>
<td>$i=2$</td>
<td>—</td>
<td>0</td>
<td>72</td>
<td>84</td>
<td>126</td>
</tr>
<tr>
<td>$i=3$</td>
<td>—</td>
<td>—</td>
<td>0</td>
<td>48</td>
<td>132</td>
</tr>
<tr>
<td>$i=4$</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>0</td>
<td>56</td>
</tr>
<tr>
<td>$i=5$</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>其中：</p>
<p>$$
m[1][5] = 124
$$</p>
<p>表示计算 $A_1A_2A_3A_4A_5$ 的最少乘法次数为 $124$。</p>
<hr />
<h3>8.2 最优决策表 $s[i][j]$</h3>
<table>
<thead>
<tr>
<th>$s[i][j]$</th>
<th>$j=1$</th>
<th>$j=2$</th>
<th>$j=3$</th>
<th>$j=4$</th>
<th>$j=5$</th>
</tr>
</thead>
<tbody>
<tr>
<td>$i=1$</td>
<td>—</td>
<td>1</td>
<td>2</td>
<td>1</td>
<td><strong>4</strong></td>
</tr>
<tr>
<td>$i=2$</td>
<td>—</td>
<td>—</td>
<td>2</td>
<td>2</td>
<td>4</td>
</tr>
<tr>
<td>$i=3$</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>$i=4$</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>4</td>
</tr>
<tr>
<td>$i=5$</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>其中：</p>
<p>$$
s[1][5] = 4
$$</p>
<p>表示最优断点在 $A_4$ 后面：</p>
<p>$$
A_1A_2A_3A_4 \mid A_5
$$</p>
<hr />
<h2>9. 如何根据 $s$ 表画括号？</h2>
<p>$s[i][j] = k$ 的意思是：</p>
<blockquote>
<p>从 $A_i$ 到 $A_j$ 这一段，最优断点在 $A_k$ 后面。</p>
</blockquote>
<p>也就是：</p>
<p>$$
A_i \cdots A_k \mid A_{k+1} \cdots A_j
$$</p>
<p>画括号时要注意：</p>
<ul>
<li>竖线 $\mid$ 表示“从哪里断开”。</li>
<li>括号表示“这一段要先作为一个整体计算”。</li>
</ul>
<p>所以如果：</p>
<p>$$
s[3][4] = 3
$$</p>
<p>表示：</p>
<p>$$
A_3 \mid A_4
$$</p>
<p>写成括号就是：</p>
<p>$$
(A_3A_4)
$$</p>
<p>不是把竖线写进最终答案，而是把断开的左右两部分合成一个整体。</p>
<hr />
<h3>9.1 根据 $s$ 表拆分</h3>
<p>从整体开始看：</p>
<p>$$
s[1][5] = 4
$$</p>
<p>所以：</p>
<p>$$
A_1A_2A_3A_4 \mid A_5
$$</p>
<p>继续看左半部分：</p>
<p>$$
s[1][4] = 1
$$</p>
<p>所以：</p>
<p>$$
A_1 \mid A_2A_3A_4
$$</p>
<p>继续看右侧的 $A_2A_3A_4$：</p>
<p>$$
s[2][4] = 2
$$</p>
<p>所以：</p>
<p>$$
A_2 \mid A_3A_4
$$</p>
<p>继续看右侧的 $A_3A_4$：</p>
<p>$$
s[3][4] = 3
$$</p>
<p>所以：</p>
<p>$$
A_3 \mid A_4
$$</p>
<p>拆分过程可以写成：</p>
<pre><code>A1 A2 A3 A4 A5
        |
        k=4
        ↓
A1 A2 A3 A4 | A5

A1 A2 A3 A4
|
k=1
↓
A1 | A2 A3 A4

A2 A3 A4
|
k=2
↓
A2 | A3 A4

A3 A4
|
k=3
↓
A3 | A4
</code></pre>
<hr />
<h3>9.2 从内向外加括号</h3>
<p>根据上面的拆分，最里面是：</p>
<p>$$
A_3 \mid A_4
$$</p>
<p>所以先写：</p>
<p>$$
(A_3A_4)
$$</p>
<p>然后把 $A_2$ 加进来：</p>
<p>$$
A_2(A_3A_4)
$$</p>
<p>加成一个整体：</p>
<p>$$
(A_2(A_3A_4))
$$</p>
<p>再把 $A_1$ 加进来：</p>
<p>$$
A_1(A_2(A_3A_4))
$$</p>
<p>加成一个整体：</p>
<p>$$
(A_1(A_2(A_3A_4)))
$$</p>
<p>最后把 $A_5$ 加进来：</p>
<p>$$
((A_1(A_2(A_3A_4)))A_5)
$$</p>
<p>所以，从内向外写括号的过程是：</p>
<p>$$
A_3A_4 \rightarrow (A_3A_4)
$$</p>
<p>$$
A_2(A_3A_4) \rightarrow (A_2(A_3A_4))
$$</p>
<p>$$
A_1(A_2(A_3A_4)) \rightarrow (A_1(A_2(A_3A_4)))
$$</p>
<p>$$
(A_1(A_2(A_3A_4)))A_5 \rightarrow ((A_1(A_2(A_3A_4)))A_5)
$$</p>
<p>最终最优括号为：</p>
<p>$$
\boxed{((A_1(A_2(A_3A_4)))A_5)}
$$</p>
<p>最少乘法次数为：</p>
<p>$$
\boxed{124}
$$</p>
<p><img src="https://img.picr2.online/1780828982814_b0499c064bb2dd641264562e0a8c91b6.webp" alt="样例计算过程" /></p>
<p>一句话总结：</p>
<blockquote>
<p>断点告诉你从哪里切，括号告诉你哪一段先算。</p>
</blockquote>
<hr />
<h2>10. Java 实现</h2>
<pre><code>public class MatrixChainMultiplication {

    static int[][] m; // m[i][j] 表示 Ai 到 Aj 的最少乘法次数
    static int[][] s; // s[i][j] 表示最优断点位置

    public static void matrixChainOrder(int[] p) {
        int n = p.length - 1; // 矩阵个数

        m = new int[n + 1][n + 1];
        s = new int[n + 1][n + 1];

        // 单个矩阵不需要相乘
        for (int i = 1; i &lt;= n; i++) {
            m[i][i] = 0;
        }

        // len 表示当前矩阵链长度
        for (int len = 2; len &lt;= n; len++) {
            for (int i = 1; i &lt;= n - len + 1; i++) {
                int j = i + len - 1;

                m[i][j] = Integer.MAX_VALUE;

                // 枚举断点 k
                for (int k = i; k &lt; j; k++) {
                    int cost = m[i][k]
                            + m[k + 1][j]
                            + p[i - 1] * p[k] * p[j];

                    if (cost &lt; m[i][j]) {
                        m[i][j] = cost;
                        s[i][j] = k;
                    }
                }
            }
        }
    }

    public static void printOptimalParens(int i, int j) {
        if (i == j) {
            System.out.print("A" + i);
        } else {
            System.out.print("(");
            printOptimalParens(i, s[i][j]);
            printOptimalParens(s[i][j] + 1, j);
            System.out.print(")");
        }
    }

    public static void main(String[] args) {
        // p = [2, 3, 6, 4, 2, 7]
        // A1: 2×3
        // A2: 3×6
        // A3: 6×4
        // A4: 4×2
        // A5: 2×7
        int[] p = {2, 3, 6, 4, 2, 7};

        int n = p.length - 1;

        matrixChainOrder(p);

        System.out.println("最少乘法次数：" + m[1][n]);

        System.out.print("最优加括号方式：");
        printOptimalParens(1, n);
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>最少乘法次数：124
最优加括号方式：((A1(A2(A3A4)))A5)
</code></pre>
<hr />
<h2>11. 总结</h2>
<p>矩阵连乘问题的关键不是计算矩阵乘积，而是寻找最优计算顺序。</p>
<p>核心内容可以概括为：</p>
<ul>
<li>使用动态规划解决。</li>
<li>$m[i][j]$ 表示 $A_i$ 到 $A_j$ 的最少乘法次数。</li>
<li>$s[i][j]$ 表示取得最优值时的断开位置。</li>
<li>$k$ 表示当前尝试的断点。</li>
<li>$A_i = p_{i-1} \times p_i$。</li>
<li>一串矩阵相乘后的规模等于第一个矩阵的行数乘最后一个矩阵的列数。</li>
<li>状态转移方程为：</li>
</ul>
<p>$$
m[i][j] =
\min_{i \le k &lt; j}
(
m[i][k] + m[k+1][j] + p_{i-1} \cdot p_k \cdot p_j
)
$$</p>
<p>通俗理解就是：</p>
<blockquote>
<p>把矩阵链从不同位置切开，分别计算左右两边的最优代价，再加上最后合并的代价，最后选择代价最小的切法。</p>
</blockquote>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>用 Restic 备份本地却在疯狂吃磁盘？</title>
    <link href="https://blog.yoake.dev/posts/restic-disk-cleanup/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/restic-disk-cleanup/</id>
    <published>2026-05-25T03:57:47.000Z</published>
    <updated>2026-07-17T07:00:00.000Z</updated>
    <summary>一次去重备份的本地瘦身实录。Restic 上线第二天：Server 1 误报异常，Server 3 本地备份目录每天多囤 2.7GB。排查环境变量错配，并砍掉本地日期副本。</summary>
    <content type="html"><![CDATA[<p>昨天刚把五台机的 <code>Restic + DigitalOcean Spaces</code> 备份体系跑通：首备、timer、Telegram 通知、阿里云 SSH 中继，感觉挺稳。</p>
<p>今天凌晨 3:58，Telegram 冷不防来一张黄牌：<code>⚠️ 备份异常｜&lt;SERVER1_ALIAS&gt;</code>。</p>
<p>顺藤摸瓜又发现 Server 3 本地的备份临时目录在以每天约 2.7GB 的速度吞盘。</p>
<p>这篇文章记两件事：怎么消掉那条误报，以及怎么把「传统本地留多份日期备份」的习惯从 Restic 体系里踢出去——本机 SSD 一口气拿回将近 8GB。</p>
<hr />
<h2>幽灵警告：环境变量拷贝拷歪了</h2>
<p>早上起来看通知，大概长这样：</p>
<pre><code>⚠️ 备份异常｜&lt;SERVER1_ALIAS&gt;
&lt;SERVER1_INSTANCE&gt;

数据库快照不存在：/var/backups/db/qq-bot.sqlite.gz
📌 快照：&lt;SNAPSHOT_ID&gt;

摘要
时间  05-25 03:58 CST
耗时  1 分钟
上传  32.5 MiB / 17.9 MiB
总量  438.59 MiB
变化  +57 / ~67
</code></pre>
<p>Server 1（<code>&lt;SERVER1_INSTANCE&gt;</code>）上只跑 <code>qq-bot-api</code> 做协议转换：收 QQ 消息、转协议、交给 Server 3 的 <code>qq-bot</code> 核心做决策，再把回复拉回来发。</p>
<p>它是个无状态中继，<strong>根本没有 SQLite</strong>。</p>
<p>那它干嘛喊 <code>数据库快照不存在：/var/backups/db/qq-bot.sqlite.gz</code>？还把 Server 3 上 <code>qq-bot</code> 的库名喊出来了？</p>
<p>SSH 上去看 <code>/etc/restic/env</code>（权限 <code>600</code>）：</p>
<pre><code>sudo cat /etc/restic/env
</code></pre>
<p>文件尾部写着：</p>
<pre><code>export BACKUP_DB_PATH="/var/backups/db/qq-bot.sqlite.gz"
export BACKUP_DB_LABEL="SQLite"
</code></pre>
<p>昨天多机初始化时，Server 1 的 env 大概是从模板或 Server 3 拷过来的，数据库节点专有的变量没删干净。</p>
<p>脚本的 <code>notify_success</code> 很较真：只要定义了 <code>BACKUP_DB_PATH</code>，就会检查文件在不在；不在就报异常。Server 1 没有 <code>qq-bot</code> 主库，当然找不到那个 <code>.gz</code>，警报就响了一夜。</p>
<p>处理很干脆：</p>
<ul>
<li><strong>Server 1</strong>：<code>sed</code> 掉所有 <code>BACKUP_DB_</code> 相关变量。之后不再做数据库快照存在性检查，通知回到 <code>🟢 状态：OK</code>。</li>
<li><strong>Server 4</strong>：顺手审计时发现它每天都在导出 PostgreSQL 裸 SQL，env 里却<strong>漏配</strong>了 <code>BACKUP_DB_PATH</code>。成功通知里看不到库体积，导出挂了也不会丢库报警。补上：</li>
</ul>
<pre><code>export BACKUP_DB_PATH="/var/backups/db/docker-postgres-all.sql"
export BACKUP_DB_LABEL="PostgreSQL"
</code></pre>
<p>没有数据库的节点，env 里别留 <code>BACKUP_DB_*</code>；有库的节点则要和实际导出路径对齐——环境变量就是脚本的指挥棒，拷贝模板时多看一眼。</p>
<hr />
<h2>本地 11GB：日期后缀副本在囤盘</h2>
<p>告警消了之后，我在 Server 3 上随手看了眼临时目录：</p>
<pre><code>sudo ls -lh /var/backups/db
</code></pre>
<pre><code>total 11G
-rw------- 1 root root 2.6G May 23 21:36 qq-bot-2026-05-23.sqlite
-rw------- 1 root root 2.6G May 24 18:36 qq-bot-2026-05-24.sqlite
-rw------- 1 root root 2.6G May 25 10:58 qq-bot-2026-05-25.sqlite
-rw------- 1 root root 2.6G May 25 10:57 qq-bot.sqlite
</code></pre>
<p><strong>临时目录塞了 11GB。</strong> 一个 2.6G 的库，本地躺了四份。</p>
<p>为什么会这样？昨天写的脚本里有一段很「传统」的逻辑：</p>
<pre><code># 传统逻辑：
mv "$DB_DIR/qq-bot.sqlite.tmp" "$DB_DIR/qq-bot.sqlite"
cp "$DB_DIR/qq-bot.sqlite" "$DB_DIR/qq-bot-${TODAY}.sqlite" # 复制出日期命名的本地归档

# 清理 3 天前的日期副本
find "$DB_DIR" -name 'qq-bot-*.sqlite' -mtime +3 -delete
</code></pre>
<p>思路很常见：怕介质出问题，或者想本地快速翻前几天的版本，就再拷一份带日期的文件，保留最近 3 天。</p>
<p>在 <code>tar</code> / 纯对象覆盖上传的世界里，这说得通。</p>
<p><strong>但在 Restic / Borg 这种快照 + 去重工具面前，这纯属画蛇添足。</strong></p>
<table>
<thead>
<tr>
<th>维度</th>
<th>传统备份（tar / 傻上传）</th>
<th>Restic（快照 / 去重）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>本地生成物</strong></td>
<td>往往要带日期的独立包，否则后端旧文件会被盖掉</td>
<td>每天一个固定名最新快照即可（如 <code>qq-bot.sqlite</code>）</td>
</tr>
<tr>
<td><strong>历史版本</strong></td>
<td>本地或云端留 <code>file-YYYY-MM-DD</code> 链条</td>
<td>每次备份一个 Snapshot，历史由仓库元数据管</td>
</tr>
<tr>
<td><strong>磁盘占用</strong></td>
<td>本地留几天就翻几倍（2.6G → 11G）</td>
<td>本地通常只要一倍大小</td>
</tr>
<tr>
<td><strong>找回某天</strong></td>
<td>下对应压缩包再解压</td>
<td><code>restic restore &lt;snapshot_id&gt;</code> 或挂载快照</td>
</tr>
</tbody>
</table>
<p>Restic 本身就是版本管理器。我在本地 <code>cp</code> 多份日期文件，既占 SSD，每天备份还要多扫几个 GB 冗余，CPU 和 I/O 白干一截。</p>
<p>历史该交给仓库，本地只留「当前这一份一致性快照」。</p>
<hr />
<h2>砍掉日期副本，磁盘瞬间轻松</h2>
<p>想通之后改动很小：<strong>本地不再做日期后缀副本，历史 100% 交给 Restic。</strong></p>
<p>有数据库的节点一起改：Server 3（<code>qq-bot</code> SQLite）、Server 2（<code>status-monitor</code> SQLite）、Server 4（<code>api-hub</code> PostgreSQL）。</p>
<p>以 Server 3 为例：</p>
<pre><code>   mv "$DB_DIR/qq-bot.sqlite.tmp" "$DB_DIR/qq-bot.sqlite"
-  cp "$DB_DIR/qq-bot.sqlite" "$DB_DIR/qq-bot-${TODAY}.sqlite"
   if [ "${BACKUP_CREATE_COMPRESSED_DB:-0}" = "1" ]; then
     if command -v pigz &gt;/dev/null 2&gt;&amp;1; then pigz -1 -p "$(nproc)" -c "$DB_DIR/qq-bot.sqlite" &gt; "$DB_DIR/qq-bot.sqlite.gz"; else gzip -1 -c "$DB_DIR/qq-bot.sqlite" &gt; "$DB_DIR/qq-bot.sqlite.gz"; fi
     gzip -t "$DB_DIR/qq-bot.sqlite.gz"
   fi
-  find "$DB_DIR" -name 'qq-bot-*.sqlite' -mtime +3 -delete
-  find "$DB_DIR" -name 'qq-bot-*.sqlite.gz' -mtime +3 -delete
</code></pre>
<p>脚本部署后再清历史日期文件：</p>
<pre><code>sudo bash -c 'rm -f /var/backups/db/qq-bot-*.sqlite /var/backups/db/qq-bot-*.sqlite.gz'
sudo ls -lh /var/backups/db
</code></pre>
<pre><code>total 2.6G
-rw------- 1 root root 2.6G May 25 10:57 qq-bot.sqlite
</code></pre>
<p><code>/var/backups/db</code> 从 <strong>11GB → 2.6GB</strong>，本地 SSD 大约多出 <strong>7.8GB</strong>。</p>
<p>Server 2 清了 <code>status-monitor-*.sqlite</code>，Server 4 清了 <code>docker-postgres-all-*.sql</code>。库没 Server 3 那么大，但三台的备份写法总算对齐了：本地只留最新一致性快照，版本靠 Restic。</p>
<hr />
<h2>对齐之后：五台机各自干什么</h2>
<p>env、脚本和本地冗余清完，整套体系的数据流大致如下：</p>
<pre><code>flowchart TD
    subgraph S3_Cloud ["云端存储 (DigitalOcean Spaces)"]
        DO_Spaces["DO Spaces S3 仓库"]
    end

    subgraph Internal_Servers ["五台服务器"]
        S1["Server 1&lt;br&gt;qq-bot-core / qq-bot-api"]
        S2["Server 2&lt;br&gt;my-blog / status-monitor"]
        S3["Server 3&lt;br&gt;qq-bot"]
        S4["Server 4&lt;br&gt;api-hub / llm-api / cli-proxy"]
        S5["Server 5&lt;br&gt;阿里云博客国内节点"]
    end

    S1 &lt;--&gt; S3

    S1 &amp; S2 &amp; S5 --&gt; DO_Spaces
    S3 -- "2.6G SQLite快照 (去重增量)" --&gt; DO_Spaces
    S4 -- "pg_dumpall (增量)" --&gt; DO_Spaces

    TG_Bot["Telegram 报警机器人"]
    S1 &amp; S3 &amp; S4 --&gt; TG_Bot
    S2 -- "直连/代理中继通知" --&gt; TG_Bot
    S5 -- "SSH 隧道中继" --&gt; S2
</code></pre>
<p>（另有一张 Graphviz 风格全景图，结构同上，可对照看：）</p>
<p><img src="https://img.picr2.online/img/2026/07/17/d0a037aabd4d86dd82390f7ac381e0cb.svg" alt="五节点 Restic 备份拓扑" /></p>
<p>各节点分工：</p>
<table>
<thead>
<tr>
<th>节点</th>
<th>角色</th>
<th>备份核心</th>
<th>数据库</th>
<th>通知</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Server 1</strong></td>
<td>qq-bot-core / qq-bot-api</td>
<td>nginx、证书、Compose、Docker volume 数据目录</td>
<td>无库，跳过 dump 与库路径校验</td>
<td>海外直连 Telegram HTML</td>
</tr>
<tr>
<td><strong>Server 2</strong></td>
<td>my-blog / status-monitor</td>
<td>博客目录、status-monitor compose 与数据</td>
<td>Python 在线热备未压缩 <code>status-monitor.sqlite</code>，integrity 通过后再备</td>
<td>海外直连 Telegram</td>
</tr>
<tr>
<td><strong>Server 3</strong></td>
<td>qq-bot 核心（写入最重）</td>
<td>配置 / 插件 / 数据（排除在线 WAL 原库）</td>
<td>在线热备固定名 <code>qq-bot.sqlite</code>，靠 Restic 块去重扛增量</td>
<td>海外直连 Telegram</td>
</tr>
<tr>
<td><strong>Server 4</strong></td>
<td>api-hub / llm-api / cli-proxy 等</td>
<td>Compose、环境与挂载数据</td>
<td>容器外 <code>docker compose exec</code> 导出未压缩 <code>docker-postgres-all.sql</code></td>
<td>海外直连 Telegram</td>
</tr>
<tr>
<td><strong>Server 5</strong></td>
<td>阿里云加速 / 宝塔</td>
<td>宝塔 nginx、证书、静态产物</td>
<td>无库</td>
<td>国内不直连 TG，受限 SSH 私钥把消息管道推到 Server 2 代发</td>
</tr>
</tbody>
</table>
<p>Server 3 是备份压力最大的一台：高频写入的 <code>qq-bot</code> 库 + 适配器和 Agent 数据交汇。改成固定名 <code>qq-bot.sqlite</code> 后，历史版本全在仓库里，本机再也不用为「多留几天」翻倍占盘。</p>
<p>五台脚本共用的底盘没变，只提一句：</p>
<ul>
<li><strong><code>flock</code></strong>：daily / weekly 共锁 <code>/run/restic-backup.lock</code>，<code>flock -n</code> 抢不到就退出，避免 backup 和 prune 对撞</li>
<li><strong>双 timer</strong>：daily 做备份 + 轻量 <code>check --no-cache</code>；weekly 做 <code>forget --prune</code> 和 <code>check --read-data-subset=1G</code></li>
<li><strong><code>trap ERR</code></strong>：失败时把最近约 40 行日志塞进 Telegram，方便对着看</li>
</ul>
<p>更细的架构说明见上一篇备份体系搭建文，这里不重复展开。</p>
<hr />
<h2>收尾</h2>
<p>这次其实就修了两处「旧习惯带进新工具」：</p>
<ul>
<li>拷贝 env 模板时没按节点删干净，无库机器硬检查数据库路径 → 假异常</li>
<li>用惯了 <code>tar ...-$(date +%F)</code>，上了 Restic 还在本地 <code>cp</code> 日期副本 → SSD 被自己备份脚本吃掉</li>
</ul>
<p>个人项目搭运维时特别容易这样：工具已经是快照去重了，脑子还停在「多留几份压缩包才安心」。</p>
<p>本地只留最新一份一致性快照；历史、校验、保留策略交给加密去重仓库。边界划清，链路才轻，关键时刻也不至于被自己的「保险措施」拖后腿。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="运维记录"/>
  </entry>
  <entry>
    <title>搭建“黎明の鸡窝”</title>
    <link href="https://blog.yoake.dev/posts/komari-monitor/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/komari-monitor/</id>
    <published>2026-05-24T12:20:40.000Z</published>
    <updated>2026-07-17T10:14:40.000Z</updated>
    <summary>基于 Komari 的轻量级服务器监控接入指南。Docker 部署 Komari 面板，Nginx + HTTPS 暴露，批量接入多台 Agent，以及 DNS、Basic Auth、自动发现 key 和国内下载卡住的踩坑记录。</summary>
    <content type="html"><![CDATA[<p>手头几台小鸡散在不同云、不同架构上：有的跑博客，有的跑 Docker，有的跑机器人，有的跑网关。平时想看状态就 SSH 上去瞟一眼——机器不多，但已经够烦。</p>
<p>后来选了 <a href="https://github.com/komari-monitor/komari">Komari</a>：一个自托管面板 + 若干轻量 agent，上报 CPU、内存、磁盘、网络和在线状态。够轻，1G 内存的机器也能塞。</p>
<p>下面是这次从部署到踩坑的记录。域名、IP、密钥、token 都做了脱敏，只留架构、命令形态和坑本身。</p>
<hr />
<h2>跑起来长什么样</h2>
<pre><code>监控面板：https://monitor.example.com
面板部署：Docker Compose
公网入口：Nginx + Let's Encrypt HTTPS
后端端口：127.0.0.1:25774
Agent 数量：5 台服务器
Agent 权限：禁用 web ssh，只做监控上报
</code></pre>
<p>节点大致是这些角色：</p>
<table>
<thead>
<tr>
<th>节点</th>
<th>系统</th>
<th>架构</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>Node A</td>
<td>Ubuntu 24.04</td>
<td>amd64</td>
<td>Docker 服务</td>
</tr>
<tr>
<td>Node B</td>
<td>Ubuntu 24.04</td>
<td>amd64</td>
<td>Blog / Komari 面板</td>
</tr>
<tr>
<td>Node C</td>
<td>Ubuntu 24.04</td>
<td>arm64</td>
<td>Bot 服务</td>
</tr>
<tr>
<td>Node D</td>
<td>Ubuntu 24.04</td>
<td>amd64</td>
<td>API / Docker 服务</td>
</tr>
<tr>
<td>Node E</td>
<td>Alibaba Cloud Linux 3</td>
<td>amd64</td>
<td>额外云服务器</td>
</tr>
</tbody>
</table>
<p>真实连接信息放在本地私有记录里，不写进公开文章。</p>
<p>预览：<a href="https://komari.dawn114514.site/">黎明の鸡窝</a></p>
<p><img src="https://img.picr2.online/img/2026/05/24/c59528f5cedbef28a46cba4e5e6163ec.png" alt="image-20260524200841421" /></p>
<hr />
<h2>那天大概怎么走的</h2>
<p>整体不复杂，但坑挺典型：DNS 没好就急着签证书、Nginx Basic Auth 挡 agent、自动发现 key 的 JSON 格式、去掉 <code>--auto-discovery</code> 后变空 token、国内机下载 GitHub 卡住。</p>
<pre><code>gantt
    title Komari 监控部署时间线 (2026-05-23)
    dateFormat  YYYY-MM-DD HH:mm
    axisFormat  %H:%M

    section 面板准备
    服务器资源与端口检查         :done, p1, 2026-05-23 18:03, 8m
    Docker 与 Compose 安装       :done, p2, 2026-05-23 18:13, 4m
    Komari 容器部署              :done, p3, 2026-05-23 18:17, 3m

    section HTTPS 暴露
    子域名 DNS 解析              :done, n1, 2026-05-23 18:18, 1m
    Nginx 反代配置               :done, n2, 2026-05-23 18:19, 2m
    Certbot 证书签发             :done, n3, 2026-05-23 18:19, 2m

    section Agent 接入
    自动发现批量注册             :done, a1, 2026-05-23 18:27, 3m
    修复 AD Key JSON 格式         :done, a2, 2026-05-23 18:28, 2m
    固定 token 启动              :done, a3, 2026-05-23 18:35, 3m
    新增额外云服务器             :done, a4, 2026-05-23 18:40, 6m

    section 收尾
    清空自动发现 Key             :done, s1, 2026-05-23 18:44, 1m
    本地文档记录                 :done, s2, 2026-05-23 18:50, 8m
</code></pre>
<hr />
<h2>流量怎么走</h2>
<pre><code>flowchart TD
  User[浏览器访问 monitor.example.com] --&gt; DNS[DNS A 记录]
  DNS --&gt; Nginx[Nginx 443 / HTTPS]
  Nginx --&gt; Komari[Komari 容器&lt;br&gt;127.0.0.1:25774]
  Komari --&gt; DB[(SQLite&lt;br&gt;/opt/komari/data)]

  NodeA[Node A Agent] --&gt;|HTTPS + token| Nginx
  NodeB[Node B Agent] --&gt;|HTTPS + token| Nginx
  NodeC[Node C Agent] --&gt;|HTTPS + token| Nginx
  NodeD[Node D Agent] --&gt;|HTTPS + token| Nginx
  NodeE[Node E Agent] --&gt;|HTTPS + token| Nginx
</code></pre>
<p>几条边界我钉死了：</p>
<ol>
<li><strong>Komari 不直接暴露公网</strong>：只绑 <code>127.0.0.1:25774</code></li>
<li><strong>公网只开 80/443</strong>：TLS 和入口交给 Nginx</li>
<li><strong>Agent 走 HTTPS + 各自 token</strong></li>
<li><strong>禁用 web ssh</strong>：面板只做监控，不当远程 shell</li>
</ol>
<p>后端只听本机，成本低，比裸开一个随机端口安心得多——即便应用自己有登录。</p>
<hr />
<h2>为什么用 Docker</h2>
<p>Komari 支持源码、二进制和 Docker。我选 Docker，原因很土：</p>
<ul>
<li>面板机上已有 Nginx，容器能和博客环境隔开</li>
<li>升级基本就是 <code>docker compose pull &amp;&amp; docker compose up -d</code></li>
<li>数据目录清楚，备份盯住 <code>/opt/komari/data</code> 就行</li>
</ul>
<p>源码部署要伺候 Go、Node、构建产物和 systemd。我只想稳定跑面板，不打算给 Komari 提 PR，Docker 更合适。</p>
<hr />
<h2>部署面板</h2>
<p>目录：</p>
<pre><code>/opt/komari
</code></pre>
<p><code>docker-compose.yml</code> 形态如下，密码用占位符：</p>
<pre><code>services:
  komari:
    image: ghcr.io/komari-monitor/komari:latest
    container_name: komari
    restart: unless-stopped
    ports:
      - "127.0.0.1:25774:25774"
    environment:
      ADMIN_USERNAME: "admin"
      ADMIN_PASSWORD: "&lt;INITIAL_ADMIN_PASSWORD&gt;"
    volumes:
      - ./data:/app/data
</code></pre>
<p>启动：</p>
<pre><code>cd /opt/komari
sudo docker compose pull
sudo docker compose up -d
</code></pre>
<p>本机探一下：</p>
<pre><code>curl -s -D - http://127.0.0.1:25774/ -o /tmp/komari.html
head -20 /tmp/komari.html
</code></pre>
<p>这时候公网还打不到 <code>25774</code>，是对的。</p>
<p>:::tip[关于 <code>latest</code>]
<code>latest</code> 只在你 <code>pull</code> 的那一刻是最新。容器跑几个月不更新，标签还叫 latest，里面完全可能是旧版。后来因此踩过一次版本漂移，见 <a href="/posts/komari-upgrade-1.2.7/">续文</a>。
:::</p>
<hr />
<h2>Nginx 反代与 HTTPS</h2>
<p>DNS 加一条，例如：</p>
<pre><code>monitor.example.com -&gt; 面板服务器公网 IP
</code></pre>
<p>Nginx 大致这样（WebSocket 相关头别丢）：</p>
<pre><code>server {
    server_name monitor.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        proxy_pass http://127.0.0.1:25774;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
</code></pre>
<p>启用并签证书：</p>
<pre><code>sudo ln -s /etc/nginx/sites-available/komari /etc/nginx/sites-enabled/komari
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d monitor.example.com --redirect
</code></pre>
<p>确认：</p>
<pre><code>curl -I https://monitor.example.com/
curl -I http://monitor.example.com/
</code></pre>
<p>预期：</p>
<pre><code>HTTPS：200
HTTP：301 -&gt; HTTPS
</code></pre>
<h3>Basic Auth 后来为什么撤了</h3>
<p>一开始想在 Nginx 外再挡一层。浏览器访问没问题，但 agent 上报和 WebSocket 多了一层认证适配，折腾成本不划算。</p>
<p>Komari 自己有登录，agent 用 token，面板也不是开放注册服务。最后只保留：</p>
<pre><code>HTTPS + Komari 登录 + Agent token + 后端不公网暴露
</code></pre>
<p>以后真要加码，可以只挡管理后台路径，或者整机走 Tailscale / WireGuard。</p>
<hr />
<h2>批量接入 Agent</h2>
<p>Agent 就是下载二进制 + systemd。多台一起接时，我先临时开了自动发现 key。</p>
<p>首次注册类似：</p>
<pre><code>/opt/komari-agent/agent \
  -e https://monitor.example.com \
  --auto-discovery &lt;TEMP_AD_KEY&gt; \
  --disable-web-ssh \
  --interval 5.0 \
  --info-report-interval 15
</code></pre>
<p>成功后本地会有：</p>
<pre><code>/opt/komari-agent/auto-discovery.json
</code></pre>
<p>里面是该节点的 <code>uuid</code> 和 <code>token</code>，恢复时很有用，权限收紧一点。</p>
<p>随后改成固定 token 启动，并清空服务端自动发现 key：</p>
<pre><code>ExecStart=/opt/komari-agent/agent -e https://monitor.example.com -t &lt;NODE_TOKEN&gt; --disable-web-ssh --interval 5.0 --info-report-interval 15
</code></pre>
<p>批量接完就关自动发现。临时 key 就算泄露，也不能再往面板上挂新机器。</p>
<hr />
<h2>踩过的坑</h2>
<h3>坑 1：DNS 没好就签证书</h3>
<p>子域名还没指到面板机就跑 Certbot，ACME 直接失败。比较稳的顺序：</p>
<ol>
<li>先起 Komari，只听 <code>127.0.0.1</code></li>
<li>写好 Nginx 站点模板</li>
<li>等 DNS 查到正确 IP</li>
<li>再启用站点、签证书</li>
</ol>
<pre><code>getent hosts monitor.example.com
</code></pre>
<p>Windows 本地：</p>
<pre><code>Resolve-DnsName monitor.example.com
</code></pre>
<h3>坑 2：Basic Auth 文件权限导致 500</h3>
<p>配了 <code>.htpasswd</code> 后浏览器带认证仍 500，Nginx 日志：</p>
<pre><code>open() "/etc/nginx/.htpasswd-komari" failed (13: Permission denied)
</code></pre>
<p>配置写对了，worker 读不到文件。修权限：</p>
<pre><code>sudo chgrp www-data /etc/nginx/.htpasswd-komari
sudo chmod 640 /etc/nginx/.htpasswd-komari
sudo nginx -t
sudo systemctl reload nginx
</code></pre>
<p>后来 Basic Auth 整段撤掉了，但教训还在：<strong>Nginx 配置正确 ≠ 运行时权限正确</strong>。</p>
<h3>坑 3：自动发现 Key 必须是 JSON 字符串</h3>
<p>为了临时开自动发现，我直接改 SQLite 配置。第一次写成裸字符串：</p>
<pre><code>auto_discovery_key = abcdef...
</code></pre>
<p>Agent 注册报：</p>
<pre><code>Failed to get AutoDiscovery Key: invalid character 'x' after top-level value
</code></pre>
<p><code>configs.value</code> 存的是 JSON 字面量，字符串要带引号：</p>
<pre><code>auto_discovery_key = "abcdef..."
</code></pre>
<p>脚本写入时用 <code>json.dumps()</code>，别手搓。</p>
<h3>坑 4：去掉 <code>--auto-discovery</code> 变成空 token</h3>
<p>注册完我想「把自动发现参数删掉更安全」，于是直接去掉：</p>
<pre><code>--auto-discovery &lt;TEMP_AD_KEY&gt;
</code></pre>
<p>结果全员离线。服务端日志：</p>
<pre><code>GET /api/clients/report?token= 401
POST /api/clients/uploadBasicInfo?token= 401
</code></pre>
<p>Agent <strong>不会</strong>自动去读 <code>auto-discovery.json</code> 里的 token，而是空 token 上报。要从本地配置取出 token，写进 systemd：</p>
<pre><code>-t &lt;NODE_TOKEN&gt;
</code></pre>
<p>修完：</p>
<pre><code>Basic info uploaded successfully
WebSocket connected
</code></pre>
<p>面板在线数才回来。这坑当时差点把我绕晕——参数删了更「干净」，上报却全 401。</p>
<h3>坑 5：国内机下 GitHub Release 卡住</h3>
<p>某台国内云直连 GitHub 下 agent，下了几 MB 就不动。不等了，从已有 amd64 节点拷二进制：</p>
<pre><code>已有 amd64 节点 /opt/komari-agent/agent
  -&gt; scp 到本地
  -&gt; scp 到下载困难的节点
  -&gt; chmod +x
  -&gt; 手动创建 systemd service
</code></pre>
<p>小规模自用很够用：单文件、架构一致就能搬。</p>
<hr />
<h2>常用命令</h2>
<p>面板：</p>
<pre><code>cd /opt/komari
sudo docker compose ps
sudo docker compose logs -f
sudo docker stats komari
</code></pre>
<p>Nginx / 证书：</p>
<pre><code>sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
</code></pre>
<p>Agent：</p>
<pre><code>systemctl is-active komari-agent
journalctl -u komari-agent -n 80 --no-pager
grep '^ExecStart=' /etc/systemd/system/komari-agent.service
</code></pre>
<p>确认面板端口只听本机：</p>
<pre><code>ss -tulpn | grep 25774
</code></pre>
<p>预期：</p>
<pre><code>127.0.0.1:25774
</code></pre>
<hr />
<h2>备份盯什么</h2>
<p>面板机：</p>
<pre><code>/opt/komari/docker-compose.yml
/opt/komari/data
/etc/nginx
/etc/letsencrypt
</code></pre>
<p>每个 agent：</p>
<pre><code>/etc/systemd/system/komari-agent.service
/opt/komari-agent/auto-discovery.json
</code></pre>
<p>真正要紧的是数据和身份：面板库、证书、Nginx、agent token。镜像和二进制都能重建。</p>
<hr />
<h2>收尾</h2>
<p>Komari 本身不难装，烦的是边界：端口别裸奔、WebSocket 头别丢、DNS 好了再签证、自动发现用完就关、systemd 里写死 token、GitHub 下不动就抄同架构二进制。</p>
<p>换来的是一个页面看完几台机的在线、负载、磁盘和流量。个人小规模已经够用。</p>
<p>以后加机器：临时开自动发现 → 注册 → 改固定 token → 清空自动发现 key → 补进备份清单。扩容方便，又不把长期入口晾在外面。</p>
<hr />
<h2>更新（2026-07）</h2>
<p>跑了大约两个月，新节点出现「面板在线，CPU/内存/磁盘却是 0」。根因是 <strong>Server 还停在 1.2.0，Agent 已经 1.2.60</strong>。Docker 升级、1.2.7 迁移向导和 Agent 重启过程写在：</p>
<p><a href="/posts/komari-upgrade-127/">Komari 节点在线但数据全是 0：Server/Agent 版本漂移与 1.2.7 升级实录</a></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="运维记录"/>
  </entry>
  <entry>
    <title>“如果今天服务器炸了怎么办？”</title>
    <link href="https://blog.yoake.dev/posts/restic-spaces-backup/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/restic-spaces-backup/</id>
    <published>2026-05-24T12:20:40.000Z</published>
    <updated>2026-07-17T04:00:00.000Z</updated>
    <summary>五台个人服务器搭建 Restic + DigitalOcean Spaces 自动备份，包括数据库一致性快照、Telegram 通知、阿里云 SSH 中继和恢复验证。</summary>
    <content type="html"><![CDATA[<p>之前我的几台服务器一直处于一种很典型的个人项目状态：服务越堆越多，配置散落各处，Docker compose、Nginx、证书、数据库、机器人数据、博客静态目录都各有各的历史包袱。</p>
<p>平时一切正常，但只要认真想一下“如果某台机子今天炸了，我多久能恢复”，心里就会出现一种很不妙的沉默。</p>
<p>所以这次我干脆给五台服务器搭了一套统一的备份体系——方案设计和脚本审查是让 Codex 帮忙出的，我负责审一遍、改几处不靠谱的地方、然后在真机上跑。</p>
<p>整体方案不追求企业级灾备，不搞复杂的多地热备，也不做每周恢复演练这种会让个人项目变成工作的流程。</p>
<p>目标非常朴素：数据库是主菜，配置文件是筷子，依赖、镜像、虚拟环境这些到时候照清单重装。</p>
<p>也就是说，只备那些真正不可替代、或者重建成本很高的东西：</p>
<ul>
<li>数据库一致性快照</li>
<li>Docker compose 文件</li>
<li>服务配置</li>
<li>Nginx 配置</li>
<li>证书</li>
<li>博客静态目录</li>
<li>关键应用数据</li>
<li>备份脚本自己和 systemd timer/service</li>
</ul>
<p>最终结果是：</p>
<ul>
<li>五台机器各自拥有一个独立的 Restic 仓库，后端统一使用 DigitalOcean Spaces</li>
<li>每天自动备份，每周 prune/check</li>
<li>成功、异常和失败都会发 Telegram HTML 通知</li>
<li>阿里云国内机器不直连 Telegram，而是通过第二台海外机器做 SSH 消息中继</li>
</ul>
<hr />
<h2>最终架构总览</h2>
<p>这次备份体系由四层组成：</p>
<ol>
<li><strong>数据采集层</strong>：各机器本地生成软件清单、Docker 清单、compose 展开配置、数据库一致性快照</li>
<li><strong>备份执行层</strong>：Restic 负责加密、切块、去重、增量上传</li>
<li><strong>对象存储层</strong>：DigitalOcean Spaces 作为 S3-compatible 后端，按机器拆分仓库</li>
<li><strong>通知与巡检层</strong>：systemd timer 定时触发，Telegram 推送 HTML 通知，阿里云通过 SSH 中继通知</li>
</ol>
<pre><code>flowchart TD
  subgraph Servers[五台服务器]
    S1[Server 1&lt;br&gt;qq-bot-core / qq-bot-api]
    S2[Server 2&lt;br&gt;my-blog / status-monitor 面板]
    S3[Server 3&lt;br&gt;qq-bot]
    S4[Server 4&lt;br&gt;api-hub / llm-api / qq-bot-api / cli-proxy]
    S5[Server 5&lt;br&gt;阿里云博客国内节点 / 宝塔 Nginx]
  end

  subgraph LocalJobs[本地备份任务]
    DUMP[数据库一致性快照&lt;br&gt;SQLite backup API / pg_dumpall]
    LIST[软件清单 / Docker 清单 / compose config]
    RESTIC[Restic backup&lt;br&gt;加密 / 切块 / 去重 / 增量]
    TIMER[systemd timer&lt;br&gt;daily + weekly]
  end

  subgraph DO[DigitalOcean Spaces]
    BKT[example-backups]
    R1[repo: Oracle-X86-1H1G-01]
    R2[repo: Oracle-X86-1H1G-02]
    R3[repo: Oracle-ARM-4H24G]
    R4[repo: DigitalOcean1H1G]
    R5[repo: 阿里云2H2G]
  end

  subgraph Notify[通知链路]
    TG[Telegram Bot]
    Relay[Server 2&lt;br&gt;Telegram SSH relay]
  end

  S1 --&gt; DUMP
  S2 --&gt; DUMP
  S3 --&gt; DUMP
  S4 --&gt; DUMP
  S5 --&gt; LIST

  DUMP --&gt; RESTIC
  LIST --&gt; RESTIC
  TIMER --&gt; RESTIC

  RESTIC --&gt; BKT
  BKT --&gt; R1
  BKT --&gt; R2
  BKT --&gt; R3
  BKT --&gt; R4
  BKT --&gt; R5

  S1 --&gt; TG
  S2 --&gt; TG
  S3 --&gt; TG
  S4 --&gt; TG
  S5 --&gt;|relay-only| Relay
  Relay --&gt; TG
</code></pre>
<p>个人项目备份最重要的不是把整台机器原封不动复制下来，而是要明确哪些东西无法重建、哪些东西只需要一份清单。</p>
<p>Docker 镜像、Python 虚拟环境、npm 缓存这些可以重拉重装；数据库、配置、证书、业务数据才是命根子。</p>
<h3>实施时间线</h3>
<p>整个搭建历时两天（2026-05-23 至 2026-05-24）。</p>
<ul>
<li><strong>第一天</strong>：重点是抢救误锁端口的 Server 1、梳理备份范围、部署 status-monitor 面板及 Agent，并完成首轮备份初始化与 SQLite 去重优化</li>
<li><strong>第二天</strong>：完成站点域名迁移与旧证书清理，并对 5 台服务器的备份脚本、锁机制及 Telegram HTML 通知进行全局升级，包含实施阿里云国内机的 SSH forced-command 消息中继</li>
</ul>
<pre><code>gantt
    title 五台服务器备份体系实施时间线 (2026-05-23 至 2026-05-24)
    dateFormat  YYYY-MM-DD HH:mm
    axisFormat  %m-%d %H:%M

    section 方案确认与抢救 (05-23)
    意外排查与 Server 1 磁盘挂载离线抢救     :done, s1_rescue, 2026-05-23 15:19, 1h 10m
    备份范围梳理与 Spaces 规划           :done, plan, 2026-05-23 16:47, 27m
    status-monitor 面板部署与 5台 Agent 批量注册  :done, monitor, 2026-05-23 18:01, 46m

    section 第一轮备份实施 (05-23)
    Server 1 首备实验与 Telegram 通知定制 :done, s1_backup, 2026-05-23 20:46, 17m
    Server 2 部署、SQLite 快照及脚本备份  :done, s2_backup, 2026-05-23 21:03, 9m
    锁与 timer 机制运维验收验证           :done, lock_verify, 2026-05-23 21:12, 4m
    Server 3 (qq-bot 5.3G) 首备          :done, s3_first, 2026-05-23 21:16, 9m
    去重优化：切换为未压缩 SQLite 快照    :done, db_optimize, 2026-05-23 21:25, 25m

    section 域名迁移与架构重构 (05-24)
    站点域名迁移与旧证书清理 :done, migration, 2026-05-24 17:07, 25m
    5台机器全局备份脚本与 systemd 重构    :done, refactor_sys, 2026-05-24 17:59, 20m
    Telegram HTML 通知格式升级与变量转义  :done, tg_html, 2026-05-24 18:19, 20m
    阿里云 SSH 中继 (relay-only) 实施与验收:done, relay_opt, 2026-05-24 18:39, 20m
</code></pre>
<hr />
<h2>为什么选择 Restic + DigitalOcean Spaces</h2>
<p>一开始也考虑过 <code>s3cmd</code>、<code>rclone</code> 或者直接 <code>tar.gz</code> 上传对象存储。</p>
<p>但备份不是简单搬文件，加密、增量、去重、快照、保留策略、校验、按路径恢复这些需求，正好是 Restic 的强项。</p>
<p>最终选择：</p>
<ul>
<li>Restic 做备份工具</li>
<li>DigitalOcean Spaces SFO3 区域做后端</li>
<li>每台机器一个独立 repo</li>
<li>每台机器一组独立 Spaces Key（权限 Private / Restrict file listing）</li>
<li>通知走 Telegram Bot</li>
<li>调度用 systemd timer</li>
</ul>
<p>每台机器一组 Key 是因为：如果五台共用一组 Access Key，任意一台泄露就可能影响全部仓库。</p>
<p>拆成五组后，单机泄露最多影响自己的备份仓库，爆炸半径小很多。</p>
<p>Spaces 里最终是五个前缀目录对应五台机器。</p>
<p>成本方面，Spaces 标准套餐包含 250GiB 存储和 1TiB 出站流量，入站上传不计入出站。</p>
<p>日常备份是从服务器上传到 Spaces，本质上是入站，所以主要关注存储容量。</p>
<p>Oracle OCI Always Free 的出站免费额度是 10TB 量级，当前规模远远够用。</p>
<p>真正需要注意的是恢复时从 Spaces 下载。</p>
<p>当前实际量级从几十 MiB 到几 GiB 不等，只要坚持把未压缩数据库快照交给 Restic 做去重，250GiB 对这套个人备份来说非常充裕。</p>
<hr />
<h2>五台机器的备份范围</h2>
<p>这次不是把一套脚本无脑扔到所有机器上，而是先盘点每台机器的实际情况，再按角色定制备份范围。</p>
<p>为了避免泄露不必要的信息，下面的公网 IP 均做脱敏处理，机器名保留。</p>
<h3>Server 1：Oracle-X86-1H1G-01</h3>
<p>用途是 <code>qq-bot-core / qq-bot-api</code> 相关 Docker 服务。</p>
<p>备份覆盖了：</p>
<ul>
<li>证书、Nginx、systemd 单元</li>
<li>compose 目录</li>
<li>status-monitor agent 配置</li>
<li>备份脚本本身</li>
<li>三个 Docker named volume（<code>qq-bot-core_data</code>、<code>qq-bot-core_config</code>、<code>qq-bot-core_cache</code>）</li>
</ul>
<p>这一台有个小插曲：一开始按 <code>/var/lib/docker/volumes/...</code> 检查时显示不存在，但 <code>docker volume ls</code> 又能看到 volume。</p>
<p>后来用 <code>docker volume inspect</code> 确认真实挂载点，发现父目录和 <code>_data</code> 目录的关系需要小心处理。</p>
<p>最终脚本支持通过 Docker 查询 mountpoint，但如果标准路径存在，就只备父目录，避免 <code>_data</code> 重复出现在快照路径里。</p>
<h4>经典血泪教训：被“交叉错位”锁在门外的离线抢救</h4>
<p>在第一天初始化备份前，这台服务器还发生了一次非常经典的运维事故——修改 SSH 端口与防火墙规则时的“交叉错位”导致彻底失联。</p>
<p>本来我想把系统 SSH 端口从默认的 <code>22</code> 收紧到高位安全端口 <code>55522</code>，但在操作时由于粗心，犯了一个极其容易让人抓狂的“交叉错位”错误：</p>
<ul>
<li>sshd 服务端配置改为仅监听 <code>Port 55522</code></li>
<li>但 iptables 规则却只放行了默认的 <code>Port 22</code></li>
<li>随后的默认链规则为 <code>REJECT</code></li>
</ul>
<p>这就造成了完美的死锁：</p>
<ul>
<li>连 22 端口，防火墙放行但 sshd 没在监听，返回 <code>Connection refused</code></li>
<li>连 55522 端口，sshd 在等但防火墙把入站流量拦了，表现为 <code>Connection timeout</code></li>
</ul>
<p>当时我还尝试通过 Server 1 上原本部署的 noVNC 远程桌面去修复端口，甚至费了半天劲在里面找到了运行中的终端。</p>
<p>但我很绝望地发现，这个 noVNC 实际上运行在一个被隔离的 containerd 应用容器环境内部（提示符是 <code>root@xxxxxxxxxxxxxxx:/app/qq-bot-core_data#</code>）：</p>
<ul>
<li>容器内没有任何宿主机管理权限</li>
<li>既没有挂载 <code>/var/run/docker.sock</code></li>
<li>又无法访问 <code>/dev</code> 下的磁盘设备</li>
<li>连 <code>docker</code>、<code>ip</code>、<code>ss</code> 等系统命令都没有执行权限</li>
</ul>
<p>在没有宿主机 root 权限和逃逸口的“容器孤岛”里，即使进了终端也根本无法修复 SSH 和防火墙。</p>
<p>最后不得不采用“离线挂载磁盘”的重兵器打法：</p>
<ol>
<li>停止 Server 1 实例</li>
<li>将 Boot Volume 分离</li>
<li>挂载到同一局域网内健康的 Server 3 上识别为 <code>/dev/sdb1</code></li>
<li>离线编辑 <code>sshd_config.d/99-recovery-port.conf</code> 临时放行双监听（<code>Port 22</code> 与 <code>Port 55522</code>）</li>
<li>离线编辑 <code>iptables/rules.v4</code> 在 <code>REJECT</code> 前同时插入放行两条端口的 ACCEPT 规则</li>
<li>执行 <code>sync</code> 后安全卸载</li>
<li>再把启动盘挂回 Server 1 启动</li>
</ol>
<p>最终 Server 1 成功复活。</p>
<p>这个低级失误耽误了首备进度，也再次敲响了警钟——<strong>修改 SSH 等核心端口前，防火墙规则和 sshd 配置必须确保两端对齐，且新会话未测试通过前，千万不要关闭当前已连接的活动终端。</strong></p>
<h3>Server 2：Oracle-X86-1H1G-02</h3>
<p>用途是博客和 status-monitor 面板。</p>
<p>备份覆盖证书、Nginx、systemd 单元、博客目录、status-monitor 数据和 compose 文件。</p>
<p>status-monitor 的 SQLite 数据库使用未压缩一致性快照：脚本通过 Python 的 <code>sqlite3.backup()</code> 在线生成，并执行 <code>pragma integrity_check;</code> 确认结果为 <code>ok</code> 后再交给 Restic。</p>
<h3>Server 3：Oracle-ARM-4H24G</h3>
<p>这台是整个备份体系里最重的一台。</p>
<ul>
<li><code>/opt/qq-bot/data</code> 约 3.6GiB</li>
<li>主库 <code>qq-bot.db</code> 约 2.7GiB</li>
<li>并且是 WAL 模式</li>
</ul>
<p>备份覆盖证书、Nginx、systemd 单元、qq-bot 的 config/data/docker-config/plugins 和备份脚本。</p>
<p>但显式排除了在线热库（<code>qq-bot.db</code>、<code>qq-bot.db-wal</code>、<code>qq-bot.db-shm</code>），改为备份未压缩的一致性快照 <code>/var/backups/db/qq-bot.sqlite</code>。</p>
<p>SQLite WAL 模式下，<code>db + wal + shm</code> 是一组在线状态文件。直接备份不一定损坏，但恢复时没有一致性快照直观可靠。</p>
<p>更重要的是，如果同时备“在线热库 + 一致性快照”，就会把几 GB 的数据库备两份。</p>
<p>所以最终选择是在线热库排除，一致性快照纳入 Restic。</p>
<h3>Server 4：DigitalOcean1H1G</h3>
<p>用途是 <code>api-hub / llm-api / qq-bot-api / cli-proxy</code>。</p>
<p>备份覆盖证书、Nginx、systemd 单元、各服务的 compose 文件和关键数据目录。</p>
<p>这台的核心是 api-hub 的 PostgreSQL。通过 <code>pg_dumpall -U root</code> 导出未压缩 SQL 到 <code>/var/backups/db/docker-postgres-all.sql</code>，同样不压缩，让 Restic 对未压缩 <code>.sql</code> 做块级去重。</p>
<h3>Server 5：阿里云2H2G</h3>
<p>博客国内节点、宝塔 Nginx、status-monitor agent。</p>
<p>这一台没有数据库，重点就是博客目录、宝塔 Nginx 配置、vhost、证书和 status-monitor agent。</p>
<hr />
<h2>Restic 配置与敏感信息隔离</h2>
<p>每台机器都有一个 <code>/etc/restic/env</code>，里面放：</p>
<ul>
<li>Spaces Access Key/Secret</li>
<li>Restic Password</li>
<li>Telegram Bot Token/Chat ID</li>
<li>通知模式（HTML）</li>
<li>备份别名</li>
<li>各种告警阈值（上传量、耗时、新增/修改文件数等）</li>
</ul>
<p>没有数据库一致性快照的机器不设置 <code>BACKUP_DB_PATH</code>，通知里也不会出现数据库行。</p>
<p>权限锁死到 <code>700 /etc/restic</code>、<code>600 /etc/restic/env</code>。</p>
<p>Restic 初始化时 source 这个 env 并设置 <code>HOME=/root</code> 和 <code>XDG_CACHE_HOME=/root/.cache</code>（否则 systemd 环境下会报找不到缓存目录）。</p>
<p><strong>绝不把 <code>/etc/restic/env</code> 纳入备份</strong>——里面有 Spaces Access Key、Restic Password、Telegram Bot Token，这个文件默认被 Restic 排除。</p>
<p>炸机后应该从离线保存的配置重新写入，而不是从备份仓库里恢复。</p>
<hr />
<h2>每日备份脚本与调度</h2>
<p>每日脚本不是简单执行 <code>restic backup</code>，而是一个完整流程：</p>
<pre><code>flowchart TD
  A[systemd timer 触发] --&gt; B[读取 /etc/restic/env]
  B --&gt; C[获取 flock 锁]
  C --&gt; D[生成软件清单和 Docker 清单]
  D --&gt; E[导出 docker compose config]
  E --&gt; F{是否有数据库}
  F --&gt;|SQLite| G[sqlite3 backup API&lt;br&gt;生成未压缩一致性快照]
  F --&gt;|PostgreSQL| H[pg_dumpall 导出未压缩 SQL]
  F --&gt;|无数据库| I[跳过 dump]
  G --&gt; J[收集存在的核心路径]
  H --&gt; J
  I --&gt; J
  J --&gt; K[restic backup]
  K --&gt; L[restic forget&lt;br&gt;不 prune]
  L --&gt; M[restic check --no-cache]
  M --&gt; N[写 last-success / snapshots]
  N --&gt; O[发送 Telegram HTML 通知]
</code></pre>
<p>核心锁用的是 <code>flock -n</code> 对 <code>/run/restic-backup.lock</code>，每日备份和每周维护共用同一把锁，避免 <code>backup</code> 和 <code>prune</code> 撞车。</p>
<p>锁文件放在 <code>/run</code> 是因为它是运行时目录，重启后自然清空——真正生效的是进程持有的 <code>flock</code>，不是那个 0 字节文件本身，进程结束后锁自动释放。</p>
<p>调度用 systemd timer：</p>
<ul>
<li>每日 <code>OnCalendar=*-*-* 03:30:00</code></li>
<li>加 <code>RandomizedDelaySec=30m</code> 和 <code>Persistent=true</code>，保证重启后也能补跑</li>
<li>Service 设置 <code>Nice=10</code>、<code>IOSchedulingClass=best-effort</code>、<code>IOSchedulingPriority=7</code>，让备份跑得不那么“抢”</li>
</ul>
<p>为什么每日不 prune？</p>
<p><code>prune</code> 会对对象存储做较多删除和重排操作，没必要每天跑。</p>
<p>weekly 统一做 <code>forget --prune + check --read-data-subset=1G</code>，也能降低与日常备份互相抢锁的概率。</p>
<hr />
<h2>重要优化：减少压缩，把增量能力还给 Restic</h2>
<p>一开始数据库快照采用了 <code>.sqlite.gz</code> / <code>.sql.gz</code>。</p>
<p>这看起来合理：压缩节省空间。但很快发现，对 Restic 来说这不是最优。</p>
<p>问题在于：gzip 是流式压缩。源文件前面一小段变化，后面的压缩流也可能大面积变化。</p>
<p>于是一个每天只变化一点的数据库，在 Restic 看来可能像一个每天都在变化的大压缩包。</p>
<p>最终优化原则很简单：</p>
<ul>
<li>给 Restic 的长期备份尽量不压缩</li>
<li>给人手动下载的临时归档可以压缩</li>
</ul>
<p>具体做法：</p>
<ul>
<li>普通目录直接交给 Restic，不 tar 不 gzip</li>
<li>SQLite 生成未压缩 <code>.sqlite</code> 一致性快照</li>
<li>PostgreSQL 生成未压缩 <code>.sql</code> dump</li>
<li>可选的 <code>.gz</code> 归档用 pigz 并发压缩但默认排除，不进入 Restic 主备份</li>
</ul>
<p>第三台 qq-bot 优化效果最明显：</p>
<ul>
<li>首备时压缩包和在线库都参与备份，仓库新增约 5.3GiB</li>
<li>切换为未压缩 SQLite 快照并排除在线热库后，下一次增量只新增约 85MiB</li>
</ul>
<p><code>pigz</code> 可以让 gzip 吃满多核，但如果压缩包本身破坏增量，那么跑满 CPU 只是更快地制造一个“不利于去重的大文件”。</p>
<p>长期备份的核心优化不是并发压缩，而是让 Restic 能看见未压缩数据的稳定块。</p>
<hr />
<h2>Telegram HTML 通知</h2>
<p>成功通知必须短，只放真正需要扫一眼的信息；失败通知可以详细，带阶段、退出码和最近 40 行日志。</p>
<p>格式统一使用 Telegram HTML，不再使用 MarkdownV2。</p>
<p>成功通知包含：</p>
<ul>
<li>机器名</li>
<li>状态（OK/异常）</li>
<li>数据库类型和大小</li>
<li>快照 ID</li>
<li>耗时</li>
<li>上传量、总量和文件变化数</li>
</ul>
<p>如果没有数据库快照，<code>数据库：...</code> 这一行不会显示。</p>
<p>成功通知里也不再放仓库路径、恢复命令、核心路径清单之类的长内容，避免每天刷屏。</p>
<p>异常不是“成功通知里写一行异常：无”，而是真的触发阈值才变成异常通知：</p>
<ul>
<li>上传量、耗时、新增/修改文件数超过告警阈值</li>
<li>或者 SNAPSHOT_ID 为空</li>
<li>或者数据库文件不存在</li>
</ul>
<p>通知失败不会让备份失败。备份成功和通知成功是两件事，不能因为 Telegram 抽风就把 Restic 备份标红。</p>
<p>预览图：</p>
<p><img src="https://img.picr2.online/img/2026/05/24/2d135ec5ff81e2f995cb66aaeb7f0ba5.png" alt="image-20260524201223239" /></p>
<hr />
<h2>阿里云国内机的 Telegram SSH 中继</h2>
<p>第五台阿里云机器无法稳定直连 <code>api.telegram.org</code>，国内网络环境下不能假设 Telegram API 可达。</p>
<p>最终方案不是给阿里云折腾代理，而是让第二台海外服务器做一个极小的通知中继。</p>
<pre><code>sequenceDiagram
    participant Ali as 阿里云 Server 5
    participant S2 as Server 2 通知中继
    participant TG as Telegram Bot API

    Ali-&gt;&gt;S2: SSH forced command 发送 HTML 通知文本
    S2-&gt;&gt;TG: curl Telegram Bot API
    TG--&gt;&gt;S2: HTTP 200
</code></pre>
<p>中继脚本 <code>/usr/local/bin/telegram-relay-send.sh</code> 在 Server 2 上，从 <code>SSH_ORIGINAL_COMMAND</code> 或 stdin 读取消息然后按 HTML 模式 curl Telegram。</p>
<p>阿里云保存专用私钥 <code>/etc/restic/telegram_relay_ed25519</code>，Server 2 的 <code>authorized_keys</code> 对该 key 做了 <code>restrict,command=...</code> 限制——这把 key 只能执行通知中继脚本，不能拿交互 shell，不能随便跑命令。</p>
<p>一开始阿里云脚本还是“先直连 Telegram，失败后再走中继”。</p>
<p>后来确认国内机直连 Bot API 没意义，于是改成 relay-only：阿里云 daily/weekly 的 <code>send_telegram()</code> 直接把消息通过 stdin 管道交给 SSH 中继。</p>
<p>这里踩过一个很小但很致命的坑：不能写 <code>ssh -n</code>。</p>
<p><code>-n</code> 会让 SSH 从 <code>/dev/null</code> 读取 stdin，导致管道里的通知正文根本没送到中继脚本，但命令可能仍然返回 0。</p>
<p>最终写法是不带 <code>-n</code>，用管道把消息直接送进去，失败只写 warning 不影响备份。</p>
<p>中继私钥 <code>/etc/restic/telegram_relay_ed25519</code> 和 <code>/etc/restic/env</code> 一样属于敏感材料，不纳入 Restic，离线保存，炸机后手动写回。</p>
<hr />
<h2>恢复思路</h2>
<p>如果某台机器炸了，恢复流程大体一致：</p>
<ol>
<li>新开实例</li>
<li>安装基础系统包</li>
<li>安装 Restic</li>
<li>写回 <code>/etc/restic/env</code></li>
<li><code>restic restore latest --target /tmp/restore-test</code> 做小样</li>
<li>确认无误后恢复到 <code>/</code></li>
<li>检查 nginx / docker compose / systemd</li>
<li>恢复数据库</li>
<li>启动服务</li>
</ol>
<p><strong>恢复到根目录前先做小样。</strong> 不要上来就 <code>--target /</code>。</p>
<p>先恢复关键文件到 <code>/tmp/restore-test</code>，确认路径、权限、快照时间都对，再做真正恢复。</p>
<p>个人项目的恢复最怕“手滑把新环境覆盖成旧错误状态”。</p>
<p>数据库恢复比较直接：</p>
<ul>
<li><strong>SQLite 类</strong>（qq-bot、status-monitor）：停服务，把一致性快照 cp 到数据目录替代在线库，删掉 wal/shm 文件，再启动</li>
<li><strong>PostgreSQL 类</strong>（api-hub）：先起 postgres 容器，<code>psql -U root &lt; dump.sql</code> 恢复，再启动其余容器</li>
</ul>
<hr />
<h2>踩坑记录</h2>
<p><strong>systemd 环境没有 HOME。</strong></p>
<p>Restic 在 systemd 里跑报 <code>unable to locate cache directory</code>，因为 <code>$XDG_CACHE_HOME</code> 和 <code>$HOME</code> 都没定义。</p>
<p>加上 <code>export HOME="${HOME:-/root}"</code> 和 <code>export XDG_CACHE_HOME="${XDG_CACHE_HOME:-/root/.cache}"</code> 就行。</p>
<p><strong>PowerShell 远程传脚本容易乱展开。</strong></p>
<p>从 Windows PowerShell 远程传 bash 脚本时，<code>$</code>、引号、here-string 都容易出事故。</p>
<p>后来统一改用 base64 传输远端脚本：<code>ssh user@host "echo $b64 | base64 -d | bash"</code>。</p>
<p><strong>阿里云源里没有 restic。</strong></p>
<p>Alibaba Cloud Linux 3 的 <code>yum/dnf</code> 源里没有 <code>restic</code> 包。</p>
<p>最终是下载官方 GitHub release 的静态二进制，在本地校验 SHA256 后传到服务器，装到 <code>/usr/local/bin/restic</code>，再建一个 <code>/usr/bin/restic</code> 软链接兼容 <code>sudo</code> 的安全 PATH。</p>
<p><strong>Telegram 通知不能阻塞备份。</strong></p>
<p>通知命令必须设超时（<code>--connect-timeout 10 --max-time 30</code>）。</p>
<p>海外机器直接 curl Telegram，阿里云不再尝试直连，直接走中继。</p>
<p>中继失败只写 warning，不影响备份成功。</p>
<p><strong>备份脚本自己也要备。</strong></p>
<p>一开始只备了业务目录和配置，后来意识到 <code>/usr/local/bin/</code> 下的备份脚本和 <code>/etc/systemd/system/</code> 下的 timer/service 文件也应该纳入备份。</p>
<p>炸机后恢复脚本，再补 <code>/etc/restic/env</code>，整套流程就能复用。</p>
<hr />
<h2>还没结束</h2>
<p>至此，五台机器终于从“出事以后靠记忆和运气恢复”，升级成了“有快照、有清单、有数据库一致性、有通知、有恢复测试”的状态。</p>
<p>我最满意的不是“备份跑起来了”，而是这套系统现在有明确的边界——什么该备、什么不该备、什么靠清单重装、什么要一致性快照、什么不能压缩、什么不能进备份仓库——这比单纯把整机打包上传可靠得多，也省得多。</p>
<p>个人项目不一定要上企业级灾备，但至少应该让未来的自己少熬几次夜。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="运维记录"/>
  </entry>
  <entry>
    <title>博客国内外双节点部署与全链路架构解耦</title>
    <link href="https://blog.yoake.dev/posts/blog-dual-region/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/blog-dual-region/</id>
    <published>2026-05-19T13:48:21.000Z</published>
    <updated>2026-08-09T09:39:57.000Z</updated>
    <summary>记录如何将个人博客重构为国内外双服务器部署、通过阿里云 DNS 智能解析分流、解耦 Cloudflare R2 静态资源以及使用 GitHub Actions 实现双端自动化投递的完整实操细节。</summary>
    <content type="html"><![CDATA[<blockquote>
<p><strong>特别鸣谢</strong>：这次重构借助 OpenAI Codex 的卓越辅助，在短短半天时间内就完成了整套复杂的双节点部署与全链路域名解耦调试。:spoiler[（Codex 真是太好用了，重点是居然还有路子白嫖，实在是太感谢 奥特曼 了 🥳）]</p>
</blockquote>
<p>最近抽空把博客的部署架构重构了一下。</p>
<p>原来的站点全部挤在国内单机上，海外访问延迟较高；且虽然图床等静态资源一直都在独立的 <code>xxx.online</code> 域名（<code>img.xxx.online</code>）下，没有进行过迁移，但音乐资源子域（<code>music.dawn114514.site</code>）依然绑定在主域名 <code>dawn114514.site</code> 下，导致如果将主域名从 Cloudflare DNS 迁出，静态音乐资源的解析就会随之失效。</p>
<p>经过这次重构，我将博客升级为<strong>国内外双节点就近接入 + 音乐子域解耦迁移 + GitHub Actions 自动化发布</strong>的架构。</p>
<p>在此过程中扫平了关于 SSH 密钥、Nginx 虚拟主机配置以及网络防火墙的各种隐性坑。这里做个完整的复盘和配置细节记录，方便以后查阅。</p>
<hr />
<h2>改造时间线与实施甘特图</h2>
<p>整个重构过程虽然断断续续，但大致可以分为六个阶段。以下甘特图呈现了各阶段的时间推进关系：</p>
<pre><code>gantt
    title 博客双节点部署改造时间线 (2026-05-19)
    dateFormat  YYYY-MM-DD HH:mm
    axisFormat  %H:%M

    section 音乐子域解耦迁移
    R2 自定义域名绑定           :done, r2-1, 2026-05-19 13:45, 13m
    代码中音乐链接全量替换       :done, r2-2, 2026-05-19 13:59, 5m
    CORS 策略配置与验证          :done, r2-3, 2026-05-19 14:04, 2m

    section CI/CD 改造
    双端部署 Workflow 编写调试   :done, ci-3, 2026-05-19 14:05, 45m
    生成免密 SSH 密钥对          :done, ci-1, 2026-05-19 14:09, 35m
    GitHub Secrets 配置          :done, ci-2, 2026-05-19 14:44, 4m

    section 海外服务器
    Nginx 安装与站点配置         :done, os-1, 2026-05-19 14:25, 3m
    iptables + OCI 防火墙放行    :done, os-3, 2026-05-19 15:02, 53m
    Certbot SSL 证书签发         :done, os-2, 2026-05-19 15:49, 3m

    section DNS 迁移
    域名迁回阿里云 DNS           :done, dns-1, 2026-05-19 15:06, 9m
    分线路智能解析配置           :done, dns-2, 2026-05-19 15:15, 28m

    section 国内服务器优化
    Nginx 配置审计与整理         :done, cn-2, 2026-05-19 15:56, 15m
    TLSv1.1 协议移除             :done, cn-1, 2026-05-19 16:08, 2m

    section 验证与文档
    全链路连通性测试             :done, verify-1, 2026-05-19 14:50, 65m
    撰写部署文档与博客           :done, verify-2, 2026-05-19 16:13, 12m
</code></pre>
<hr />
<h2>最终网络架构与流量分流拓扑</h2>
<p>重构后的系统分为以下四个层次，各层之间职责明确：</p>
<ol>
<li>
<p><strong>解析分流层</strong>：利用阿里云 DNS，对国内和境外流量做智能路由。</p>
</li>
<li>
<p><strong>静态资源存储层</strong>：将原本绑定在主域名下的音乐子域（<code>music.dawn114514.site</code>）也解耦迁移至独立的 <code>xxx.online</code> 域名（<code>music.xxx.online</code>）代理，与图床一起实现静态资源与主域名在故障域和带宽上的解耦，不再占用主站带宽。</p>
</li>
<li>
<p><strong>Nginx 接入层</strong>：国内和海外节点各自部署 Nginx 并终结 SSL 证书，国内机房额外承载子系统反代。</p>
</li>
<li>
<p><strong>CI/CD 管线</strong>：内容仓库与代码仓库解耦联动 —— 推送内容仓库触发 <code>trigger-build.yml</code>，通过 <code>repository_dispatch</code> 事件分发至代码仓库的 <code>deploy.yml</code>，由后者同步内容、构建并以真正的后台并行 <code>&amp;</code> + <code>wait</code> 聚合退码方式同步投递双端机器。</p>
</li>
</ol>
<p>:::tip[架构拆分原则]
<strong>职责单一，解耦至上</strong>。</p>
<p>无论未来业务节点如何扩充，四层分工的核心原则都应该保持稳定：DNS 负责路由分流，Nginx 负责反代接入，GitHub Actions 负责构建与同步，Cloudflare R2 负责静态数据持久化。
:::</p>
<p>具体的流量和自动部署走向可以参考下图：</p>
<pre><code>flowchart TD
  %% 流量流向
  User[用户访问 dawn114514.site] --&gt; DNS{Alibaba Cloud DNS &lt;br&gt;智能分流}

  %% 国内分流
  DNS --&gt;|中国内地/默认| CN_Node[国内 ECS 服务器 &lt;br&gt;IP: xxx.xxx.xxx.xxx]
  CN_Node --&gt; CN_Nginx[Nginx + SSL &lt;br&gt;主站目录: /www/wwwroot/MyBlog]
  CN_Nginx --&gt; CN_Subs[子系统反代 &lt;br&gt;自建服务 A / 自建服务 B]

  %% 海外分流
  DNS --&gt;|中国境外| OV_Node[海外 Oracle 服务器 &lt;br&gt;IP: yyy.yyy.yyy.yyy]
  OV_Node --&gt; OV_Nginx[Nginx + SSL &lt;br&gt;主站目录: /www/wwwroot/MyBlog]

  %% 静态资源解耦（浏览器直连 R2，不经 Nginx 中转）
  Browser[浏览器] -.-&gt;|直接请求外链| R2_Music[music.xxx.online &lt;br&gt;Cloudflare R2 音乐源]
  Browser -.-&gt;|直接请求外链| R2_Img[img.xxx.online &lt;br&gt;Cloudflare R2 图床]
  CN_Nginx -.-&gt;|HTML 引用外链 URL| Browser
  OV_Nginx -.-&gt;|HTML 引用外链 URL| Browser

  %% 自动化持续集成管线（内容/代码双仓库解耦联动）
  Developer[开发者 git push] --&gt; ContentRepo[内容仓库 MyBlog-Content]
  ContentRepo --&gt;|push main 触发 trigger-build.yml| Dispatch[repository-dispatch 事件 content-updated]
  Dispatch --&gt;|DISPATCH_TOKEN 鉴权分发| CodeRepo[代码仓库 MyBlog deploy.yml]
  CodeRepo --&gt;|浅克隆内容仓库并复制目录 + pnpm build| Build[GitHub Actions 构建管线]
  Build --&gt;|rsync 同步 后台并行 &amp;| CN_Node
  Build --&gt;|rsync 同步 后台并行 &amp;| OV_Node
</code></pre>
<p>GitHub Actions 双端部署的详细时序交互如下：</p>
<pre><code>sequenceDiagram
    participant Dev as 开发者
    participant CR as 内容仓库 MyBlog-Content
    participant TR as trigger-build.yml
    participant DR as 代码仓库 deploy.yml
    participant CI as Actions Runner
    participant CN as 国内 ECS (Aliyun)
    participant OS as 海外 Oracle

    Dev-&gt;&gt;CR: git push main (改动 posts/data 等)
    CR-&gt;&gt;TR: 触发 trigger-build.yml
    TR-&gt;&gt;DR: repository-dispatch 发送 content-updated 事件
    Note over TR,DR: 携带 DISPATCH_TOKEN 鉴权
    DR-&gt;&gt;CI: 触发 deploy.yml 工作流
    CI-&gt;&gt;CI: checkout + setup pnpm/node 22 + 恢复 Astro 缓存
    CI-&gt;&gt;CI: pnpm install --frozen-lockfile
    CI-&gt;&gt;CI: pnpm run sync-content (浅克隆内容仓库并复制目录, 失败即标红)
    CI-&gt;&gt;CI: pnpm run build (注入 BILI_SESSDATA / UMAMI_SHARE_URL 等构建期密钥)
    Note over CI: 构建产物生成于 dist/ 目录

    par 后台并行部署到国内
        CI-&gt;&gt;CI: 写入 DEPLOY_SSH_KEY_CN
        CI-&gt;&gt;CN: ssh-keyscan 写入 known_hosts (每次扫描直接信任, 非核验指纹)
        CI-&gt;&gt;CN: mkdir -p /www/wwwroot/MyBlog/dist
        CI-&gt;&gt;CN: rsync -az --delete dist/
        CN--&gt;&gt;CI: 同步完成
    and 后台并行部署到海外
        CI-&gt;&gt;CI: 写入 DEPLOY_SSH_KEY_OVERSEA
        CI-&gt;&gt;OS: ssh-keyscan 写入 known_hosts (每次扫描直接信任, 非核验指纹)
        CI-&gt;&gt;OS: mkdir -p /www/wwwroot/MyBlog/dist
        CI-&gt;&gt;OS: rsync -az --delete dist/
        OS--&gt;&gt;CI: 同步完成
    end

    Note over CI: wait 聚合双端后台进程退出码
</code></pre>
<hr />
<h2>改造步骤与细节</h2>
<h3>1. 音乐子域解耦与迁移（Cloudflare R2）</h3>
<p>在此之前，我的图床静态资源其实一直托管在独立的 <code>xxx.online</code> 域名（<code>img.xxx.online</code>）下，并未进行迁移。</p>
<p>但是，我的音乐多媒体资源却仍然直接绑定在主域名的子域 <code>music.dawn114514.site</code> 下。这就导致如果我想把主域名 <code>dawn114514.site</code> 的 Nameservers 从 Cloudflare DNS 切走，静态音乐资源的解析就会直接跟着完蛋（因为 R2 绑定自定义域名需要域名托管在 CF）。</p>
<p><strong>解决办法：</strong></p>
<p>为了彻底将静态资源与主域名剥离，我决定将音乐子域名迁移至独立托管在 Cloudflare 上的 <code>xxx.online</code> 之下（即 <code>music.xxx.online</code>），使资源域名与主站域名分属不同顶级域，实现故障域解耦——主站 DNS 怎么改动都不影响资源解析。</p>
<p>:::caution[不要手动拼 CNAME]
Cloudflare 官方明确不建议手动添加指向 <code>pub-xxx.r2.dev</code> 的 CNAME 作为生产接入，这种接入方式不在支持范围内，可能失效。</p>
<p>正确做法是在 <strong>R2 存储桶 → Settings → Custom Domains</strong> 中点击 "Connect Domain" 连接域名，由 Cloudflare 自动创建并通过 R2 网关解析那条自定义域名，无需手工干预 DNS 记录。
:::</p>
<p>也就是说：直接在两个 R2 存储桶里分别连接 <code>img.xxx.online</code> 与 <code>music.xxx.online</code>，连接成功后控制台会显示绿色 Active。Cloudflare 会在该域名的 DNS 区页中自动生成对应的 CNAME 记录（自动开启 CF 代理），不要自己去拼 <code>pub-xxxx.r2.dev</code> 的目标值。</p>
<p><img src="https://img.picr2.online/1784163579431_PixPin_2026-07-16_08-58-00.webp" alt="Cloudflare R2 自定义域名已激活" /></p>
<p>随后，在博客内容仓库中进行一次全量正则替换，将所有旧的资源引用替换为新的解耦域名：</p>
<pre><code># 将旧的引用地址清洗为 music.xxx.online
find . -type f -name "*.md" -exec sed -i 's/music.dawn114514.site/music.xxx.online/g' {} +
</code></pre>
<p>对于代码仓库中的硬编码资源路径（如音乐播放器组件），同样需要做全量替换。</p>
<p>我的音乐播放器常量文件 <code>src/components/widgets/music-player/constants.ts</code> 中 32 首歌曲的资源 URL 均从 <code>https://music.dawn114514.site/...</code> 迁移至统一的 <code>${MUSIC_BASE_URL}/...</code> 模式，其中 <code>MUSIC_BASE_URL</code> 定义为 <code>https://music.xxx.online</code>。</p>
<p>:::important[资源解耦原则]
<strong>切记：主站的归主站，资源的归资源</strong>。</p>
<p>主站域名和资源域名尽量分开，不要交叉使用同一个顶级域名。资源独立出去之后，后续主站不管怎么折腾 DNS 或更换云厂商，文章里的静态资源链接都不需要动。
:::</p>
<h4>R2 CORS 跨域配置</h4>
<p>域名迁移后还需配置 CORS 策略，确保浏览器端对 R2 桶的跨域请求不被拦截。在 Cloudflare R2 控制台 → 存储桶 → Settings → CORS Policy 中，为两个存储桶分别添加如下 CORS 规则（整体是一个<strong>规则数组</strong>，注意最外层的 <code>[ ]</code>）：</p>
<pre><code>[
  {
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": [],
    "MaxAgeSeconds": 3600
  }
]
</code></pre>
<p>:::note[音乐桶的原有自定义域名]
原本绑定在音乐桶上的 <code>music.dawn114514.site</code> 自定义域名在迁移完成后即可删除 —— 因为此时所有引用地址已经改为 <code>music.xxx.online</code>，旧域名不再有任何流量。
:::</p>
<hr />
<h3>2. DNS 大陆/境外智能解析分流</h3>
<p>主域名 <code>dawn114514.site</code> 迁离 Cloudflare，Nameservers 改为阿里云的 <code>dns19.hichina.com</code> 和 <code>dns20.hichina.com</code>。</p>
<p>在阿里云云解析后台，我对国内和国外线路做了 A 记录拆分，配置如下：</p>
<table>
<thead>
<tr>
<th>主机记录</th>
<th>记录类型</th>
<th>解析线路</th>
<th>记录值</th>
<th>TTL</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>@</strong></td>
<td><code>A</code></td>
<td><strong>默认</strong> (兜底必配)</td>
<td><code>xxx.xxx.xxx.xxx</code></td>
<td><code>600s</code></td>
</tr>
<tr>
<td><strong>@</strong></td>
<td><code>A</code></td>
<td><strong>中国内地</strong></td>
<td><code>xxx.xxx.xxx.xxx</code> (国内 ECS)</td>
<td><code>600s</code></td>
</tr>
<tr>
<td><strong>@</strong></td>
<td><code>A</code></td>
<td><strong>中国境外</strong></td>
<td><code>yyy.yyy.yyy.yyy</code> (境外 Oracle)</td>
<td><code>600s</code></td>
</tr>
<tr>
<td><strong>www</strong></td>
<td><code>A</code></td>
<td><strong>默认</strong></td>
<td><code>xxx.xxx.xxx.xxx</code></td>
<td><code>600s</code></td>
</tr>
<tr>
<td><strong>www</strong></td>
<td><code>A</code></td>
<td><strong>中国内地</strong></td>
<td><code>xxx.xxx.xxx.xxx</code></td>
<td><code>600s</code></td>
</tr>
<tr>
<td><strong>www</strong></td>
<td><code>A</code></td>
<td><strong>中国境外</strong></td>
<td><code>yyy.yyy.yyy.yyy</code></td>
<td><code>600s</code></td>
</tr>
</tbody>
</table>
<p><img src="https://img.picr2.online/1784163710904_image.webp" alt="阿里云 DNS 境内外分线路解析记录" /></p>
<p>:::warning[DNS 配置误区]
配置智能解析时，<strong>"默认"解析线路是绝不可省的</strong>。如果只添加了"中国内地"和"中国境外"，当遇到某些无法识别来源的特定边缘节点请求时，可能会因为没有匹配记录而返回解析失败。默认线路直接解析到国内 ECS 即可。
:::</p>
<p>:::tip[为什么不用 Cloudflare DNS 分流？]
虽然 Cloudflare 也有基于 <em>智能地域解析</em> 的流量导向功能，但阿里云 DNS 在中国大陆的分线路解析更精细（可按运营商、省份等粒度拆分）。对于国内/境外双线这种场景，阿里云的线路拆分能力完全是免费可用的，不需要额外付费。
:::</p>
<hr />
<h3>3. GitHub Actions 自动化双端同步发布</h3>
<p>内容发布如果靠手动同步肯定会出问题。</p>
<p>我现在的 CI/CD 是<strong>内容/代码双仓库解耦</strong>的设计：内容仓库（<code>MyBlog-Content</code>）只管理文章、结构化数据与静态资源（<code>posts/</code>、<code>spec/</code>、<code>data/</code>、<code>images/</code>），推送后由一个极简的 <code>trigger-build.yml</code> 通过 <code>repository_dispatch</code> 事件"摇醒"代码仓库（<code>MyBlog</code>）里负责实际构建和双端投递的 <code>deploy.yml</code>。</p>
<p>这样一份内容推送，触发一次构建，并真正地以 Shell 后台 <code>&amp;</code> + <code>wait</code> 的方式并行投递两台机器。</p>
<p>::github{repo="Dawn6666666/MyBlog-Content"}
::github{repo="Dawn6666666/MyBlog"}</p>
<p>:::tip[为何这俩仓库卡片无法加载]
因为我的博客仓库和内容仓库是Private的，所以 GitHub API 返回 404，导致卡片无法加载。
:::</p>
<h4>内容仓库触发器：<code>trigger-build.yml</code></h4>
<p>内容仓库的 Workflow 极薄，只做一件事：在 <code>posts/**</code>、<code>spec/**</code>、<code>data/**</code>、<code>images/**</code> 有变更时（也支持手动 <code>workflow_dispatch</code>），把 <code>content-updated</code> 事件 dispatch 给代码仓库。</p>
<pre><code>name: Trigger Code Repo Build

on:
  push:
    branches:
      - main
    paths:
      - "posts/**"
      - "spec/**"
      - "data/**"
      - "images/**"
  workflow_dispatch: {}

jobs:
  dispatch:
    runs-on: ubuntu-latest
    steps:
      - name: Dispatch to code repo
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ secrets.DISPATCH_TOKEN }}
          repository: Dawn6666666/MyBlog
          event-type: content-updated
</code></pre>
<h4>代码仓库部署器：<code>deploy.yml</code></h4>
<p>代码仓库的 <code>deploy.yml</code> 同时承接三类触发：自身 <code>custom-main</code> 分支的 push、内容仓库 dispatch 来的 <code>content-updated</code> 事件、以及手动 <code>workflow_dispatch</code>。</p>
<p>注意它加了 <code>concurrency</code> 取消旧运行、<code>permissions</code> 收紧权限、<code>timeout-minutes</code> 兜底，并在构建期注入内容同步令牌与媒体源密钥。</p>
<pre><code>name: Deploy to Server

on:
  push:
    branches: [ custom-main ]
    paths-ignore:
      - "docs/**"
      - "README*.md"
      - ".github/**"
  repository_dispatch:
    types: [ content-updated ]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          run_install: false

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Restore Astro cache
        uses: actions/cache@v4
        with:
          path: |
            .astro
            node_modules/.astro
          key: ${{ runner.os }}-astro-${{ hashFiles('pnpm-lock.yaml', 'astro.config.mjs', 'src/config/**/*.ts') }}
          restore-keys: |
            ${{ runner.os }}-astro-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile --prefer-offline

      - name: Sync content repo
        run: pnpm run sync-content
        env:
          ENABLE_CONTENT_SYNC: true
          CONTENT_REPO_URL: https://x-access-token:${{ secrets.CONTENT_REPO_TOKEN }}@github.com/Dawn6666666/MyBlog-Content.git
          CONTENT_SYNC_PULL: false
          GIT_TERMINAL_PROMPT: 0

      - name: Build site
        run: pnpm run build
        env:
          ENABLE_CONTENT_SYNC: true
          CONTENT_REPO_URL: https://x-access-token:${{ secrets.CONTENT_REPO_TOKEN }}@github.com/Dawn6666666/MyBlog-Content.git
          CONTENT_SYNC_PULL: false
          GIT_TERMINAL_PROMPT: 0
          BILI_SESSDATA: ${{ secrets.BILI_SESSDATA }}
          UMAMI_SHARE_URL: ${{ secrets.UMAMI_SHARE_URL }}

      - name: Deploy to dual servers
        env:
          DEPLOY_PORT: 22
          DEPLOY_DIR: /www/wwwroot/MyBlog
          DEPLOY_HOST_CN: ${{ secrets.DEPLOY_HOST_CN }}
          DEPLOY_USER_CN: ${{ secrets.DEPLOY_USER_CN }}
          DEPLOY_SSH_KEY_CN: ${{ secrets.DEPLOY_SSH_KEY_CN }}
          DEPLOY_HOST_OVERSEA: ${{ secrets.DEPLOY_HOST_OVERSEA }}
          DEPLOY_USER_OVERSEA: ${{ secrets.DEPLOY_USER_OVERSEA }}
          DEPLOY_SSH_KEY_OVERSEA: ${{ secrets.DEPLOY_SSH_KEY_OVERSEA }}
        run: |
          set -euo pipefail

          deploy_one() {
            local name="$1"
            local host="$2"
            local user="$3"
            local key="$4"

            if [ -z "$host" ] || [ -z "$user" ] || [ -z "$key" ]; then
              echo "Missing deploy secret(s) for $name"
              exit 1
            fi

            mkdir -p ~/.ssh
            chmod 700 ~/.ssh
            printf '%s\n' "$key" &gt; ~/.ssh/deploy_key_"$name"
            chmod 600 ~/.ssh/deploy_key_"$name"
            ssh-keyscan -p "$DEPLOY_PORT" -H "$host" &gt;&gt; ~/.ssh/known_hosts

            ssh -i ~/.ssh/deploy_key_"$name" -p "$DEPLOY_PORT" "$user@$host" \
              "mkdir -p '$DEPLOY_DIR/dist'"

            rsync -az --delete \
              -e "ssh -i ~/.ssh/deploy_key_$name -p $DEPLOY_PORT" \
              --exclude=".user.ini" \
              --exclude=".well-known" \
              dist/ "$user@$host:$DEPLOY_DIR/dist/"
          }

          # 后台并行投递两台机器，最后用 wait 聚合退出码
          deploy_one cn "$DEPLOY_HOST_CN" "$DEPLOY_USER_CN" "$DEPLOY_SSH_KEY_CN" &amp;
          pid_cn=$!
          deploy_one oversea "$DEPLOY_HOST_OVERSEA" "$DEPLOY_USER_OVERSEA" "$DEPLOY_SSH_KEY_OVERSEA" &amp;
          pid_oversea=$!

          status=0
          wait "$pid_cn" || status=$?
          wait "$pid_oversea" || status=$?
          exit "$status"
</code></pre>
<p>:::warning[显式同步是为了不吞掉内容同步失败]
这里有个坑差点踩到：项目 <code>package.json</code> 里把内容同步挂在了 <code>prebuild</code> 钩子上，写的是 <code>node scripts/sync-content.js || true</code>。</p>
<p>那个 <code>|| true</code> 在本地开发时方便（同步失败也不挡你 build），但在 CI 里会把内容克隆/复制失败<strong>静默吞掉</strong>，于是构建会用缺失或旧内容继续出包、还部署上去，极其难排查。</p>
<p>因此 Workflow 里单独加了 <code>Sync content repo</code> 这一步，直接 <code>pnpm run sync-content</code>（这只跑 <code>node scripts/sync-content.js</code>，没有 <code>|| true</code> 兜底），让 PAT 失效或内容仓库克隆失败时直接把任务标红，而不是把错误带进 build 和部署。
:::</p>
<p><img src="https://img.picr2.online/1784163798386_image.webp" alt="GitHub Actions 显式同步内容后成功部署" /></p>
<h4>代码仓库静态检查：<code>lint.yml</code></h4>
<p>代码仓库还配了一个手动触发的 <code>lint.yml</code>，用于发布前自检：用 Biome 跑 <code>src/</code> 的 lint、<code>pnpm run sync-content</code> 同步内容做 Astro Check，最后再跑一次 <code>pnpm build</code> 验证整站能正常出包。</p>
<p>它只在 <code>workflow_dispatch</code> 下触发，不会干扰正常发布管线，适合改完前端代码不确定会不会炸时先手动跑一遍兜底。</p>
<p>:::caution[这是动态扫描并直接信任，不是指纹核验]
<code>deploy_one</code> 里用 <code>ssh-keyscan</code> 拉取远端公钥写入 <code>known_hosts</code>，这只是"无条件采信每次扫到的公钥"，并没有跟已核验的指纹比对。</p>
<p>注意这甚至比标准 <strong>TOFU（trust-on-first-use）</strong> 还弱——GitHub Runner 每次运行都是全新无状态环境，每次都重新扫描、重新信任，相当于"trust on every run"，若某次解析或链路遭遇中间人，伪造的公钥会被原样采信。</p>
<p>更稳妥的做法是把服务器本地用 <code>ssh-keyscan</code> 拿到的公钥、人工核对一遍后，以 base64 形式存成 GitHub Secret（如 <code>SSH_KNOWN_HOSTS_CN</code>），在 Workflow 中直接 <code>append</code> 进 <code>known_hosts</code>，跳过自动扫描。这样即便某次连接遇到 MITM 也不会被欺骗。</p>
<p>个人博客可以接受这层风险敞口，但应知道它存在。
:::</p>
<h4>GitHub Secrets 配置清单</h4>
<p>双仓库解耦后，密钥分两处存放：内容仓库只持有一把用于跨仓库 dispatch 的令牌；代码仓库则承担真正的部署凭证与构建期注入密钥。</p>
<p><strong>内容仓库 <code>MyBlog-Content</code>：</strong></p>
<table>
<thead>
<tr>
<th>Secret 名称</th>
<th>说明</th>
<th>示例值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DISPATCH_TOKEN</code></td>
<td>用于跨仓库触发 <code>repository_dispatch</code> 的 PAT。Classic PAT 选 <code>repo</code>（私有仓库）或 <code>public_repo</code>（公开仓库）；Fine-grained PAT 则需对目标代码仓库授予 <code>Contents: write</code></td>
<td><code>ghp_xxxxxxxxxxxxxxxxxxxxxxxx</code></td>
</tr>
</tbody>
</table>
<p><strong>代码仓库 <code>MyBlog</code>：</strong></p>
<table>
<thead>
<tr>
<th>Secret 名称</th>
<th>说明</th>
<th>示例值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CONTENT_REPO_TOKEN</code></td>
<td>构建期拉取内容仓库的 PAT（含内容仓库 <code>contents:read</code>）</td>
<td><code>ghp_xxxxxxxxxxxxxxxxxxxxxxxx</code></td>
</tr>
<tr>
<td><code>DEPLOY_HOST_CN</code></td>
<td>国内服务器 IP</td>
<td><code>xxx.xxx.xxx.xxx</code></td>
</tr>
<tr>
<td><code>DEPLOY_USER_CN</code></td>
<td>国内服务器 SSH 用户</td>
<td><code>admin</code></td>
</tr>
<tr>
<td><code>DEPLOY_SSH_KEY_CN</code></td>
<td>国内服务器免密私钥（完整内容）</td>
<td><code>-----BEGIN OPENSSH PRIVATE KEY-----...</code></td>
</tr>
<tr>
<td><code>DEPLOY_HOST_OVERSEA</code></td>
<td>海外服务器 IP</td>
<td><code>yyy.yyy.yyy.yyy</code></td>
</tr>
<tr>
<td><code>DEPLOY_USER_OVERSEA</code></td>
<td>海外服务器 SSH 用户</td>
<td><code>ubuntu</code></td>
</tr>
<tr>
<td><code>DEPLOY_SSH_KEY_OVERSEA</code></td>
<td>海外服务器免密私钥（完整内容）</td>
<td><code>-----BEGIN OPENSSH PRIVATE KEY-----...</code></td>
</tr>
<tr>
<td><code>BILI_SESSDATA</code></td>
<td>构建期抓取 B 站数据所需的 SESSDATA</td>
<td><code>xxxxxxxxxxxxxxxx</code></td>
</tr>
<tr>
<td><code>UMAMI_SHARE_URL</code></td>
<td>构建期注入的 Umami 统计分享链接</td>
<td><code>https://umami.xxx/share/xxxx</code></td>
</tr>
</tbody>
</table>
<p><img src="https://img.picr2.online/img/2026/05/19/7123d5266c4c53ad9eca157d79ca1ad9.webp" alt="GitHub Actions Secrets 密钥配置管理面板截图。" /></p>
<p>:::tip[为什么 DEPLOY_DIR 和 DEPLOY_PORT 不作为 Secret？]
端口固定 22、部署目录固定 <code>/www/wwwroot/MyBlog</code>，这两项属于<strong>基础设施约定</strong>而非敏感信息。将它们硬编码在 Workflow 中有助于减少 Secret 数量，降低配置出错的概率。如果需要灵活性，可以在 Workflow 文件中以环境变量形式定义。
:::</p>
<h4>坑 1：国内机密钥带 Passphrase 导致部署失败与 CI 挂起</h4>
<p>我原来图省事，直接拷贝了国内阿里云服务器上现有的私钥文件（<code>~/.ssh/id_ed25519</code>）内容并填入 GitHub Secrets 的 <code>DEPLOY_SSH_KEY_CN</code>。然而，在 Actions 执行部署时，一直卡住并最终报错失败。:spoiler[（当时前 3 次全部红灯报错，急哭了说是😡）]</p>
<p>后来在服务器本地自测该私钥才发现，原来这把密钥在初次生成时被设置了密码保护。</p>
<p>在 GitHub Actions 这种非交互式的 CI 自动化环境下，SSH 握手因为需要输入密码而无法继续，最终导致部署直接被拒或挂死。</p>
<p>:::caution[这份 Workflow 里的私钥必须免密]
GitHub Actions Runner 是一个完全<em>无头</em>的自动执行环境。当 SSH 私钥设置了密码保护后，SSH 客户端会尝试弹出密码提示框或读取终端 <code>/dev/tty</code>。</p>
<p>我这版 Workflow 没有配 <code>ssh-agent</code>/<code>askpass</code>，所以在 CI 中完全行不通，最终会导致进程因为等待凭证输入而直接失败或无限挂起。</p>
<p><strong>因此在我这套没接 ssh-agent 的部署脚本里，写入 GitHub Secret 的那把私钥本身必须是免密的</strong>（追求更安全的话，可改为短时效、最小权限、定期轮换的免密部署密钥，或者用 <code>ssh-agent</code>/<code>SSH_ASKPASS</code> 方案把 passphrase 也注入，这里为简化没做）。
:::</p>
<p><strong>解决办法：</strong></p>
<p>重新在本地生成一对免密的专属 Ed25519 密钥对，把公钥追加到服务器的授权名单中：</p>
<pre><code># 本地生成免密 Ed25519 密钥对（PowerShell）
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\myblog-cn -C "github-actions-deploy-cn"
# 注意：提示输入 passphrase 时直接回车留空！
</code></pre>
<p>指纹审计核对：:spoiler[国内节点公网密钥的指纹为 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...，部署前务必核对确认与服务器 authorized_keys 中写入的公钥指纹一致。指纹不匹配是 CI 连接失败的常见原因之一。]</p>
<p>将私钥内容存入 GitHub Secrets 的 <code>DEPLOY_SSH_KEY_CN</code>，公钥则要从本地 Windows 机器传到服务器并追加进 <code>authorized_keys</code>。</p>
<p>注意：上面 <code>ssh-keygen</code> 是在本地生成的，服务器上并没有 <code>~/myblog-cn.pub</code>，不能直接 <code>cat</code>：</p>
<pre><code># 在本地 Windows 上：把公钥管道推给远端服务器追加进 authorized_keys
type $env:USERPROFILE\.ssh\myblog-cn.pub | ssh admin@xxx.xxx.xxx.xxx "mkdir -p ~/.ssh &amp;&amp; chmod 700 ~/.ssh &amp;&amp; cat &gt;&gt; ~/.ssh/authorized_keys &amp;&amp; chmod 600 ~/.ssh/authorized_keys"
</code></pre>
<p>:::note</p>
<ul>
<li>不想用 PowerShell 管道的话，也可以先用 <code>scp $env:USERPROFILE\.ssh\myblog-cn.pub admin@xxx.xxx.xxx.xxx:~/myblog-cn.pub</code> 把公钥传上去，再 <code>ssh</code> 进去执行 <code>cat ~/myblog-cn.pub &gt;&gt; ~/.ssh/authorized_keys</code>。</li>
<li><code>mkdir -p ~/.ssh</code> 在目录已存在时不会报错，也不影响已有的 <code>authorized_keys</code> 文件，安全无害。
:::</li>
</ul>
<p>:::warning[Secret 更新后记得触发重跑]
这个坑导致我前 3 次部署全部失败，因为密钥替换后没有及时更新 GitHub Secret。在 GitHub 仓库的 Settings → Secrets and variables → Actions 中直接编辑（Update）对应 Secret、粘贴完整的新私钥内容即可，GitHub 会覆盖旧值，<strong>不需要删除后重建</strong>。更新后手动触发一次 Workflow 验证连通性。
:::</p>
<h4>坑 2：<code>rsync</code> 擦除宝塔锁文件 <code>.user.ini</code> 导致权限报错</h4>
<p>国内机用了宝塔面板，默认会在网站根目录下放一个防跨站的锁文件 <code>.user.ini</code> :spoiler[（宝塔面板：“我是为了你好！”；rsync：“你妨碍我全删全清了！”）] （系统用 <code>chattr +i</code> 给文件加了 <em>不可变属性防护</em>，连 root 默认都改不动）。</p>
<p>Actions 里的 <code>rsync -az --delete</code> 机制在同步时发现本地没有这个文件，试图在远端将其强制抹除，结果直接权限报错导致发布管线崩溃。</p>
<p><strong>解决办法：</strong></p>
<p>在 <code>rsync</code> 投递命令中加入 <code>--exclude=".user.ini" --exclude=".well-known"</code>，排除这些系统级只读文件以及 Let's Encrypt 的验证目录。</p>
<h4>坑 3：PowerShell 变量展开破坏 Nginx 配置</h4>
<p>在通过 SSH 远程写入 Nginx 配置文件时，如果使用双引号字符串或未加引号的 here-document，PowerShell 会把 <code>$uri</code>、<code>$uri/</code>、<code>$host</code> 等 Nginx 内置变量当作 PowerShell 变量展开，导致写入服务器的配置文件变成乱码。</p>
<p>:::caution[跨平台字符串传递的隐形地雷]
Windows 端的 PowerShell 会展开 <code>$</code> 开头的 token，而 Nginx 配置中大量使用这些变量。一旦被展开，<code>try_files $uri $uri/ /index.html</code> 可能变成 <code>try_files  / /index.html</code>（空值替换），导致 Nginx 语法检查失败。
:::</p>
<p><strong>解决办法：</strong></p>
<p>PowerShell 没有 Bash 的 <code>&lt;&lt; 'EOF'</code> heredoc，要用单引号 here-string（<code>@'...'@</code>，禁止 <code>$</code> 展开）把整段脚本拼好，再经管道交给远程的 <code>bash -s</code>：</p>
<pre><code># PowerShell：单引号 here-string + 管道传给远端 bash
$config = @'
cat &gt; /tmp/nginx.conf &lt;&lt; 'NEOF'
location / {
    try_files $uri $uri/ /index.html;
}
NEOF
'@

$config | ssh user@host "bash -s"
</code></pre>
<p>要点：</p>
<ul>
<li>本地用 <code>@'...'@</code>（单引号）hold 住字符串，PowerShell 不会展开里面的 <code>$uri</code>/<code>$host</code></li>
<li>远端 <code>bash -s</code> 从 stdin 读到内容后再用 <code>&lt;&lt; 'NEOF'</code>（单引号）把 <code>$uri</code> 留给远端 bash 不展开，从而原样写进 Nginx 配置</li>
</ul>
<hr />
<h3>4. 两端服务器环境配置与安全优化</h3>
<h4>国内机：移除老旧的 <code>TLSv1.1</code> 协议</h4>
<p>国内节点使用的是宝塔环境，Nginx 版本为 1.28.1。在排查站点配置时，我发现旧的站点文件里还在支持已经不安全的 <code>TLSv1.1</code> 协议。</p>
<p>直接编辑对应的虚拟主机配置文件 <code>/www/server/panel/vhost/nginx/html_xxx.xxx.xxx.xxx.conf</code>，将老旧的 ssl_protocols 过滤收紧：</p>
<pre><code># 剔除 TLSv1.1，仅放行主流安全的版本
ssl_protocols TLSv1.2 TLSv1.3;
</code></pre>
<p>:::tip[宝塔面板 Nginx 配置的查找方式]
宝塔面板生成的站点配置文件位于 <code>/www/server/panel/vhost/nginx/</code> 目录下，文件名格式为 <code>html_xxx.xxx.xxx.xxx.conf</code>。修改后务必执行 <code>nginx -t</code> 做语法检查，再 <code>systemctl reload nginx</code> 使配置生效。不要直接在宝塔面板 UI 中编辑后再手动改文件，容易产生冲突。
:::</p>
<p>国内服务器优化后的典型 Nginx 站点配置参考：</p>
<pre><code>server {
    listen 80;
    listen 443 ssl;
    listen 443 quic;
    http2 on;
    server_name www.dawn114514.site dawn114514.site;
    index index.html index.htm;
    root /www/wwwroot/MyBlog/dist;

    include /www/server/panel/vhost/nginx/extension/xxx.xxx.xxx.xxx/*.conf;
    include /www/server/panel/vhost/nginx/well-known/xxx.xxx.xxx.xxx.conf;

    ssl_certificate      /www/server/panel/vhost/cert/xxx.xxx.xxx.xxx/fullchain.pem;
    ssl_certificate_key  /www/server/panel/vhost/cert/xxx.xxx.xxx.xxx/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 启用 HTTP/3 (QUIC) 支持以降低连接延迟
    add_header Strict-Transport-Security "max-age=31536000";
    add_header Alt-Svc 'quic=":443"; h3=":443"';
    error_page 497 https://$host$request_uri;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # 针对自建服务 A 与自建服务 B 的 WebSocket 反向代理转发
    # 依赖外部 0.websocket.conf 的全局 Upgrade map 指令
}
</code></pre>
<h4>海外机：零面板环境下的安全加固与配置</h4>
<p>海外机用的是纯净 Ubuntu 系统，没有任何面板辅助。操作步骤如下：</p>
<p><strong>第一步：安装基本服务</strong></p>
<pre><code>sudo apt-get update
sudo apt-get install -y nginx certbot python3-certbot-nginx
</code></pre>
<p><strong>第二步：创建站点配置文件并测试</strong></p>
<p>在 <code>/etc/nginx/sites-available/myblog</code> 中编写初始配置（仅 HTTP），确认 <code>nginx -t</code> 通过后 <code>systemctl reload nginx</code>。</p>
<p><strong>第三步：利用 certbot 一键申请并修改配置</strong></p>
<pre><code>sudo certbot --nginx -d dawn114514.site -d www.dawn114514.site \
  --agree-tos --register-unsafely-without-email --non-interactive
</code></pre>
<p>该命令会自动在 <code>/etc/nginx/sites-available/myblog</code> 中添加 Let's Encrypt 证书引用和 HTTP 强制跳转 HTTPS。</p>
<p>:::tip[Certbot 非交互模式]
<code>--non-interactive</code> 标志让 certbot 在无终端环境下自动完成证书申请，不需要手动输入邮箱或同意协议。加上 <code>--register-unsafely-without-email</code> 可以跳过邮箱注册步骤（适用于个人项目，生产环境建议填写真实邮箱以便接收证书过期提醒）。
:::</p>
<p><strong>第四步：突破防火墙双重过滤限制</strong></p>
<p>海外机有两层防火墙需要穿透：</p>
<table>
<thead>
<tr>
<th>层级</th>
<th>位置</th>
<th>配置方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>OS 级别</td>
<td>Ubuntu iptables</td>
<td>命令行 <code>iptables -I INPUT</code></td>
</tr>
<tr>
<td>云平台级别</td>
<td>Oracle OCI Security List</td>
<td>Web 控制台添加入站规则</td>
</tr>
</tbody>
</table>
<p><strong>OS 级别 iptables 配置：</strong></p>
<p>:::caution[Iptables 尾部追加规则失效陷阱]
Ubuntu 镜像在 <code>iptables</code> 中默认有一行拒绝规则：<code>-A INPUT -j REJECT --reject-with icmp-host-prohibited</code>，位于规则链的最底部。如果你习惯性地使用 <code>iptables -A</code> 往表尾追加 80/443 放行命令，这些命令排在 REJECT 后面会彻底失效。<strong>必须使用 <code>-I INPUT</code> 明确插到那个 REJECT 规则之前的位置才会起作用</strong>。
:::</p>
<pre><code># 先查看当前规则和行号，定位 REJECT 行所在的位置（注意用 -L 而不是 -S，-L 才支持 --line-numbers）
sudo iptables -L INPUT -n --line-numbers

# 假设查到 REJECT 在第 6 行，就把 80/443 的 ACCEPT 插到它前面
# 先在第 6 行处插入 80 放行（这条会顶替原第 6 行的位置，REJECT 顺延到第 7 行）
sudo iptables -I INPUT 6 -p tcp --dport 80 -j ACCEPT
# 再在第 7 行处插入 443 放行（在 REJECT 之前）
sudo iptables -I INPUT 7 -p tcp --dport 443 -j ACCEPT
# 实操时请把行号替换为你机器上 REJECT 所在的真实编号，不要照抄

# 验证规则顺序是否正确（ACCEPT 必须在 REJECT 前面）
sudo iptables -L INPUT -n --line-numbers

# 重启后规则会丢失，需要持久化
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save
</code></pre>
<p><strong>云服务器平台防火墙放行：</strong></p>
<p>除了系统内部的 <code>iptables</code>，甲骨文云在云平台外层还有一道安全规则守护。</p>
<p>我们需要登录 Oracle Cloud Web 控制台，在机器关联的网络安全列表中直接添加两条简单的入站规则来放行流量：</p>
<table>
<thead>
<tr>
<th>协议</th>
<th>目的端口范围</th>
<th>源 IP CIDR</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>TCP</td>
<td><code>80</code></td>
<td><code>0.0.0.0/0</code></td>
<td>允许 HTTP 流量访问博客</td>
</tr>
<tr>
<td>TCP</td>
<td><code>443</code></td>
<td><code>0.0.0.0/0</code></td>
<td>允许 HTTPS 加密流量访问博客</td>
</tr>
</tbody>
</table>
<p>:spoiler[Oracle Cloud 的 Always Free 资源包括每月 10TB 出站流量，对于个人博客来说完全够用。]</p>
<p>海外节点优化后的站点配置文件（<code>/etc/nginx/sites-available/myblog</code>）结构：</p>
<pre><code>server {
    server_name dawn114514.site www.dawn114514.site yyy.yyy.yyy.yyy _;
    root /www/wwwroot/MyBlog/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # 对静态资源文件启用 30 天超长缓存，优化首屏加载
    location ~* \.(?:css|js|mjs|json|xml|txt|ico|png|jpg|jpeg|gif|webp|avif|svg|woff2?|ttf|otf|mp3|flac|ogg)$ {
        try_files $uri =404;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000, immutable";
    }

    error_page 404 /404.html;

    # Certbot 自动化注入的 SSL 配置
    listen [::]:443 ssl ipv6only=on;
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/dawn114514.site/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dawn114514.site/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
    if ($host = www.dawn114514.site) {
        return 301 https://$host$request_uri;
    }
    if ($host = dawn114514.site) {
        return 301 https://$host$request_uri;
    }

    listen 80 default_server;
    listen [::]:80 default_server;
    server_name dawn114514.site www.dawn114514.site yyy.yyy.yyy.yyy _;
    return 404;
}
</code></pre>
<hr />
<h2>运维验证百宝箱</h2>
<p>重构阶段使用最频繁的几个高频调试命令：</p>
<h3>1. 绕过 DNS 缓存强制回源测试</h3>
<p>当域名刚切完 DNS，本地运营商的解析还没生效时，我们可以使用 <code>curl</code> 的 <code>--resolve</code> 参数，强行将目标请求解析到特定的物理 IP 上，用于检验证书配置是否正常：</p>
<pre><code># 回源到海外机房测试
curl -I --resolve dawn114514.site:443:yyy.yyy.yyy.yyy https://dawn114514.site/

# 回源到国内机房测试
curl -I --resolve dawn114514.site:443:xxx.xxx.xxx.xxx https://dawn114514.site/
</code></pre>
<p>:::tip[<code>--resolve</code> 的工作原理]
<code>curl --resolve</code> 会在本地覆盖 DNS 解析结果，将指定域名的请求直接发送到指定 IP。这条命令不依赖系统 DNS，非常适合在 DNS 切换后的过渡期做证书和连通性验证。测试通过后，可以放心等待全球 DNS 缓存自然过期（实际生效时间取决于记录本身的 TTL、NS 迁移情况，以及递归解析器缓存——可能从几分钟到几小时不等）。
:::</p>
<h3>2. 证书基本指纹和 SAN 主机名查看</h3>
<p>各自签好证书后，检查本地配置物理文件的发行商及有效期限，防止低级配置错误：</p>
<pre><code>sudo openssl x509 -in /etc/letsencrypt/live/dawn114514.site/fullchain.pem \
  -noout -subject -issuer -dates -ext subjectAltName
</code></pre>
<h3>3. 验证两端服务器返回的 HTTP 头</h3>
<p>确认两端 Nginx 配置正确、证书有效后，还可通过比较响应头验证两端配置一致性：</p>
<pre><code># 比较两端响应头（Server / Cache / HSTS 等）
diff &lt;(curl -sI --resolve dawn114514.site:443:xxx.xxx.xxx.xxx https://dawn114514.site/) \
     &lt;(curl -sI --resolve dawn114514.site:443:yyy.yyy.yyy.yyy https://dawn114514.site/)
</code></pre>
<p>通过 Bash 的 <em>进程替换</em> 特性，我们无需创建临时文件，便可直接比对过滤两端服务器返回的 Header 差异。</p>
<h3>4. 测试 DNS 分流是否生效</h3>
<p>从不同地理位置的节点执行解析测试：</p>
<pre><code># 在境外机器上查询（境外真实环境解析）
nslookup dawn114514.site

# 在国内机器上查询阿里 DNS（境内解析）
nslookup dawn114514.site 223.5.5.5
</code></pre>
<p>:::warning[不要用 "向 8.8.8.8 查询" 当作"模拟境外"]
Google Public DNS 是 Anycast，并且会通过 EDNS Client Subnet (ECS) 把客户端网段带给权威 DNS。因此<strong>同一台国内机器</strong>向 8.8.8.8 发起查询，阿里云 DNS 收到的 ECS 子网仍是国内的，往往仍返回国内线路 IP，并不能"模拟境外解析"。</p>
<p>要真正验证境内外分流，应该用境外的真实探针（比如海外 VPS 直接 <code>nslookup</code>），或用支持显式指定 <code>edns_client_subnet</code> 的 DoH 测试：</p>
<pre><code># 用 Google DoH JSON API，URL 参数指定一个真实位于境外的公网 IP 网段做 ECS 测试
# （把 REAL_OVERSEAS_IP 换成一台已知在境外的真实公网 IP，不要用 203.0.113.0/24 这类文档保留网段）
curl -sG "https://dns.google/resolve" \
  --data-urlencode "name=dawn114514.site" \
  --data-urlencode "type=A" \
  --data-urlencode "edns_client_subnet=REAL_OVERSEAS_IP/24"
</code></pre>
<p>:::</p>
<p>:::caution[别用 1.1.1.1 做 ECS 测试]
Cloudflare 1.1.1.1 默认<strong>不</strong>向权威 DNS 转发 ECS，且其 DoH JSON API 也不支持通过 HTTP Header 传 <code>EDNS-Client-Subnet</code>，用 <code>1.1.1.1/dns-query</code> 测分线路是测不出来的。要做这种"指定客户端网段模拟地理来源"的测试，得用 Google DoH 的 <code>edns_client_subnet</code> 参数，且网段用一个真实境外公网地址，<code>203.0.113.0/24</code> 这种文档保留网段是不会被识别为境外的。
:::</p>
<p>预期结果：境外真实解析返回 <code>yyy.yyy.yyy.yyy</code>（落 Oracle），境内解析返回 <code>xxx.xxx.xxx.xxx</code>（落 ECS）。</p>
<hr />
<h2>当前方案补充</h2>
<p>本文前文记录的海外 Oracle 节点部署方案现已迁移至腾讯 EdgeOne，后续海外站点由 EdgeOne 负责构建与发布。</p>
<hr />
<h2>改造后状态与双节点分流总结</h2>
<p>通过本轮的架构优化，博客构建了<strong>就近接入的双节点分流发布网络</strong>：阿里云 DNS 按境内外分线路把请求导到不同节点，国内访客落 ECS、境外访客落 Oracle，各自降低首屏延迟。</p>
<p>:::caution[这不是高可用，是手动容灾]
需要诚实说明的是：当前架构<strong>没有健康检查、没有自动故障转移</strong>。阿里云智能解析本身也明确"分线路解析不会自动剔除故障 IP、不是故障转移方案"。</p>
<p>因此这套属于<strong>双节点分流 + 手动容灾</strong>：当某节点不可用时，需要人工到 DNS 控制台把该线路的 A 记录指向另一健康节点，再等 TTL（600s）过期生效。</p>
<p>要真正实现高可用，还需补上健康检查、原子发布目录切换与自动 DNS 切换器，这些留作后续优化。
:::</p>
<p>:::warning[并发部署的一致性边界]
<code>deploy.yml</code> 用 <code>concurrency.cancel-in-progress: true</code> 取消旧运行，而 <code>rsync</code> 是直接覆盖线上 <code>dist</code> 目录。</p>
<p>如果一次部署被中途取消，或双端中只有一端 rsync 成功，可能出现线上目录半更新，或双端版本不一致。</p>
<p>更稳妥的方案是 rsync 到带版本号的目录再用软链接原子切换，当前我暂未做这层，接受手动回滚的成本。
:::</p>
<p>重构后的最终架构对比：</p>
<table>
<thead>
<tr>
<th>评估维度</th>
<th>重构前状况</th>
<th>重构后状况</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>主站域名解析</strong></td>
<td>Cloudflare DNS (国内响应偏慢)</td>
<td><strong>Alibaba Cloud DNS</strong></td>
</tr>
<tr>
<td><strong>就近分流策略</strong></td>
<td>大陆与境外一律兜底打到国内 ECS</td>
<td><strong>智能双线：国内落 ECS，境外落 Oracle</strong></td>
</tr>
<tr>
<td><strong>音乐静态资源</strong></td>
<td><code>music.dawn114514.site</code> (与主域名强绑定)</td>
<td><strong><code>music.xxx.online</code> (解耦至 <code>xxx.online</code>，资源域名与故障域独立)</strong></td>
</tr>
<tr>
<td><strong>图床静态资源</strong></td>
<td>独立域名 <code>img.xxx.online</code> (未迁移)</td>
<td><strong>保持独立，仅把音乐子域一并迁出</strong></td>
</tr>
<tr>
<td><strong>持续集成效率</strong></td>
<td>单台机器手动/脚本部署，易出版本差</td>
<td><strong>内容/代码双仓库解耦：<code>repository_dispatch</code> 联动 + 后台并行 <code>&amp;</code>/<code>wait</code> 双端同步投递</strong></td>
</tr>
<tr>
<td><strong>传输层安全级别</strong></td>
<td>允许 TLSv1.1 老旧协议接入</td>
<td><strong>完全淘汰 TLSv1.1，强制 TLSv1.2 + TLSv1.3</strong></td>
</tr>
<tr>
<td><strong>抗灾能力</strong></td>
<td>机器挂掉则全网直接瘫痪</td>
<td><strong>双节点分流 + 手动容灾：单节点宕机时人工切换 DNS 解析至另一健康节点，TTL 过期后生效</strong></td>
</tr>
</tbody>
</table>
<hr />
<h2>结语</h2>
<p>在服务架构设计中，保持职责单一和适度解耦是一条黄金法则。</p>
<p>这次把博客拆成双节点就近接入的架构，虽然在期初多配置了几个域名解析和免密部署规则，但换来的是主站应用较强的迁移弹性、国内外首屏延迟的降低，以及多媒体资源带宽不再挤占主站的保障。</p>
<p>回顾整个过程，真正耗时最多的不是写配置本身，而是<strong>排查那些跨平台的隐性陷阱</strong>——非交互式环境的 SSH passphrase 部署失败与挂起、PowerShell 的变量展开污染、iptables 的规则顺序陷阱、以及云服务器平台外层安全规则拦截。</p>
<p>这些坑每个单拎出来都不复杂，但它们组合在一起的时候，就构成了部署效率的真正瓶颈。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="运维记录"/>
  </entry>
  <entry>
    <title>数据结构：最小栈</title>
    <link href="https://blog.yoake.dev/posts/algorithm/07-minstack/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/07-minstack/</id>
    <published>2026-05-13T11:47:25.000Z</published>
    <updated>2026-05-13T11:47:25.000Z</updated>
    <summary>最小栈的核心不是多会一个 API，而是给栈的每一层保存一份最小值快照，让所有操作都在 O(1) 时间内完成。</summary>
    <content type="html"><![CDATA[<p>最小栈是在普通栈上多加一个能力：<code>getMin()</code> 要在 <code>O(1)</code> 时间返回当前栈里的最小值。</p>
<p>如果每次 <code>getMin()</code> 都从栈底扫到栈顶，当然能找到答案，但时间复杂度是 <code>O(n)</code>。题目要求所有操作都是常数时间，所以不能等到查询时再计算，必须在入栈时就把答案准备好。</p>
<p>核心思路：</p>
<blockquote>
<p>栈的每一层，都应该知道"如果栈只剩到我这一层，当前最小值是谁"。</p>
</blockquote>
<h2>题目概览</h2>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class015</code></td>
<td>GetMinStack</td>
</tr>
</tbody>
</table>
<p><strong>题型：</strong> 带最小值查询的栈设计。</p>
<p><strong>核心做法：</strong> 准备两个栈，一个存真实数据，一个同步存"当前层往下的最小值快照"。</p>
<p><strong>易错点：</strong> <code>data</code> 和 <code>min</code> 必须同步进出；有重复最小值时不能丢掉上一层的最小状态；数组实现里 <code>size</code> 的读写顺序要统一。</p>
<h2>155. 最小栈</h2>
<p><a href="https://leetcode.cn/problems/min-stack/description/">题目链接</a></p>
<p>实现一个 <code>MinStack</code>，支持：</p>
<ul>
<li><code>push(int val)</code>：压入元素</li>
<li><code>pop()</code>：弹出栈顶</li>
<li><code>top()</code>：返回栈顶元素</li>
<li><code>getMin()</code>：返回当前栈中的最小元素</li>
</ul>
<p>题目保证 <code>pop</code>、<code>top</code>、<code>getMin</code> 都会在非空栈上调用。</p>
<p>:::note[LeetCode 调用过程]
如下：</p>
<pre><code>MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // 返回 -3
minStack.pop();
minStack.top();    // 返回 0
minStack.getMin(); // 返回 -2
</code></pre>
<p>:::</p>
<h2>给每一层存一份最小值快照</h2>
<p>我们准备两个栈：</p>
<ul>
<li><code>data</code>：正常存数据</li>
<li><code>min</code>：和 <code>data</code> 一样高，每一层存"从栈底到当前层的最小值"</li>
</ul>
<p>下面的栈按"左边是栈底，右边是栈顶"来看：</p>
<pre><code>push(-2)
data: [-2]
min:  [-2]        当前最小值是 -2

push(0)
data: [-2, 0]
min:  [-2, -2]    0 不是最小值，但 min 仍然压入一层，记录"此时最小值仍是 -2"

push(-3)
data: [-2, 0, -3]
min:  [-2, -2, -3] 当前最小值更新成 -3

getMin() → -3

pop()
data: [-2, 0]
min:  [-2, -2]    -3 被同步弹出，最小值自然回到 -2

top()    → 0
getMin() → -2
</code></pre>
<p>重点在第二步：<code>0</code> 本身不是最小值，但 <code>min</code> 仍然压入一层 <code>-2</code>。因为这一层表达的是——<strong>当栈顶是 <code>0</code> 时，整个栈的最小值是 <code>-2</code>。</strong></p>
<p>这就是"快照"的含义：每次 <code>push</code> 都把当时的最小值记录下来。以后 <code>pop</code> 时，真实数据弹一层，快照也弹一层，状态自然回到上一层。</p>
<p>整个设计只靠一个不变量撑住：</p>
<p>:::tip[不变量]
<code>data</code> 和 <code>min</code> 永远等高。<code>data</code> 的栈顶是当前元素，<code>min</code> 的栈顶是当前最小值。
:::</p>
<h2>解法一：辅助栈</h2>
<h3>核心思路</h3>
<ul>
<li><code>data[i]</code>：第 <code>i</code> 层真实压入的值</li>
<li><code>min[i]</code>：从栈底到第 <code>i</code> 层的最小值</li>
</ul>
<p>每次 <code>push(val)</code> 时，真实值压入 <code>data</code>；同时把 <code>val</code> 和上一层最小值中更小的那个压入 <code>min</code>。每次 <code>pop()</code> 时，两个栈一起弹。因为栈顶元素离开后，它对应的最小值快照也应该一起失效。</p>
<p><img src="https://img.picr2.online/img/2026/05/13/459db15ceb0c559a3ff0b88000e5a3e3.webp" alt="解法示意图" /></p>
<h3>代码实现</h3>
<pre><code>import java.util.Stack;

class MinStack {

    private Stack&lt;Integer&gt; data;
    private Stack&lt;Integer&gt; min;

    public MinStack() {
        data = new Stack&lt;Integer&gt;();
        min = new Stack&lt;Integer&gt;();
    }

    public void push(int val) {
        data.push(val);

        // min 和 data 同步压入一层，保存当前栈的最小值快照
        if (min.isEmpty()) {
            min.push(val);
        } else {
            min.push(Math.min(val, min.peek()));
        }
    }

    public void pop() {
        data.pop();
        min.pop();
    }

    public int top() {
        return data.peek();
    }

    public int getMin() {
        return min.peek();
    }
}
</code></pre>
<p>这份写法最舒服的地方在 <code>pop()</code>：不用判断弹出的元素是不是最小值。因为 <code>min</code> 不是"最小值出现记录表"，而是"每一层的最小值快照"，直接同步弹出即可。</p>
<h3>变量角色</h3>
<table>
<thead>
<tr>
<th>变量</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>data</code></td>
<td>真实栈，保存所有压入的元素</td>
</tr>
<tr>
<td><code>min</code></td>
<td>辅助栈，<code>min.peek()</code> 永远是当前最小值</td>
</tr>
</tbody>
</table>
<h3>为什么不只在变小时压入 <code>min</code></h3>
<p>另一种常见写法是：只有当 <code>val &lt;= min.peek()</code> 时，才把 <code>val</code> 压入 <code>min</code>。这当然也可以，但它有两个额外负担：</p>
<ol>
<li><code>pop()</code> 时需要判断弹出的值是否等于 <code>min.peek()</code>。</li>
<li>遇到重复最小值时，必须用 <code>&lt;=</code>，不能只用 <code>&lt;</code>。</li>
</ol>
<p>例如：</p>
<pre><code>push(2)
push(2)
pop()
getMin()
</code></pre>
<p>如果第二个 <code>2</code> 没有压进 <code>min</code>，第一次 <code>pop()</code> 就把唯一的 <code>2</code> 从 <code>min</code> 里弹掉，栈里明明还剩一个 <code>2</code>，<code>getMin()</code> 却已经没有正确答案了。</p>
<p>同步快照写法虽然多存了一些重复最小值，但胜在简单稳定：<strong>入栈一起入，出栈一起出。</strong></p>
<h2>解法二：数组模拟</h2>
<p>如果想避开 <code>Stack&lt;Integer&gt;</code> 的对象开销，也可以用数组模拟两个栈。题目里总操作次数最多是 <code>3 × 10^4</code>，数组容量开到 <code>30001</code> 就足够。</p>
<pre><code>class MinStack {

    private static final int MAXN = 30001;

    private int[] data;
    private int[] min;
    private int size;

    public MinStack() {
        data = new int[MAXN];
        min = new int[MAXN];
        size = 0;
    }

    public void push(int val) {
        data[size] = val;
        min[size] = size == 0 ? val : Math.min(val, min[size - 1]);
        size++;
    }

    public void pop() {
        size--;
    }

    public int top() {
        return data[size - 1];
    }

    public int getMin() {
        return min[size - 1];
    }
}
</code></pre>
<p><code>size</code> 表示"下一个可写位置"，所以代码顺序要保持一致：</p>
<pre><code>push:  先写 data[size] 和 min[size]，再 size++
pop:   size--
top:   data[size - 1]
min:   min[size - 1]
</code></pre>
<h2>易错点</h2>
<h3><code>min</code> 忘记同步弹出</h3>
<p>错误写法：</p>
<pre><code>public void pop() {
    data.pop();
    // 漏掉 min.pop()
}
</code></pre>
<p>真实栈已经少了一层，但 <code>min</code> 还停在旧状态。之后 <code>getMin()</code> 可能返回一个已经被弹掉的值。</p>
<p>正确写法：</p>
<pre><code>public void pop() {
    data.pop();
    min.pop();
}
</code></pre>
<h3>把 <code>min</code> 理解成"只存最小值"</h3>
<p>同步快照写法里，<code>min</code> 不是只存新纪录：</p>
<pre><code>data: [3, 5, 2, 4]
min:  [3, 2]        ← 错误：只记录变小的值
</code></pre>
<p>而应该是：</p>
<pre><code>data: [3, 5, 2, 4]
min:  [3, 3, 2, 2]  ← 正确：每层都有对应的最小值快照
</code></pre>
<p>每个 <code>data</code> 位置都有对应的 <code>min</code> 位置，这样 <code>pop</code> 才能无脑同步。</p>
<h3>数组版搞乱 <code>size</code> 的含义</h3>
<p>数组写法里最怕一会儿把 <code>size</code> 当"当前最后一个元素位置"，一会儿又当"下一个可写位置"。选一个含义后就不要变。</p>
<h2>复杂度</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>时间复杂度</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>push()</code></td>
<td><code>O(1)</code></td>
<td>真实栈和辅助栈各压入一次</td>
</tr>
<tr>
<td><code>pop()</code></td>
<td><code>O(1)</code></td>
<td>两个栈同步弹出一次</td>
</tr>
<tr>
<td><code>top()</code></td>
<td><code>O(1)</code></td>
<td>直接看真实栈栈顶</td>
</tr>
<tr>
<td><code>getMin()</code></td>
<td><code>O(1)</code></td>
<td>直接看辅助栈栈顶</td>
</tr>
</tbody>
</table>
<p>空间复杂度：<code>O(n)</code>。除了真实数据外，还要为每一层额外保存一个最小值快照。</p>
<h2>记法</h2>
<p>最小栈不是在查询时找最小值，而是在入栈时就把答案存好。</p>
<p><strong>真实栈存元素，辅助栈存每一层的最小值快照；进一起进，出一起出，辅助栈顶就是答案。</strong></p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>数据结构：栈和队列的互相转化</title>
    <link href="https://blog.yoake.dev/posts/algorithm/06-convertqueueandstack/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/06-convertqueueandstack/</id>
    <published>2026-05-13T03:29:58.000Z</published>
    <updated>2026-05-13T03:29:58.000Z</updated>
    <summary>用两个栈实现队列，用一个队列实现栈：重点不是背代码，而是理解顺序如何被翻转、保存和维护。</summary>
    <content type="html"><![CDATA[<p>栈和队列互相实现这类题，最容易写成“背 API”的题。其实它考的不是 <code>push</code>、<code>pop</code>、<code>offer</code>、<code>poll</code> 会不会用，而是你能不能在受限操作里维护正确的顺序。</p>
<ul>
<li>栈：后进先出。最后进去的元素最先出来。</li>
<li>队列：先进先出。最早进去的元素最先出来。</li>
</ul>
<p>所以这篇只抓一个问题：<strong>怎么把一种顺序改造成另一种顺序，并且在每次操作后都不乱。</strong></p>
<h2>结论</h2>
<table>
<thead>
<tr>
<th>目标</th>
<th>做法</th>
<th>关键点</th>
</tr>
</thead>
<tbody>
<tr>
<td>用栈实现队列</td>
<td>两个栈，一个收新元素，一个吐老元素</td>
<td><code>out</code> 空了才从 <code>in</code> 倒，倒就一次倒完</td>
</tr>
<tr>
<td>用队列实现栈</td>
<td>一个队列，每次 <code>push</code> 后旋转旧元素</td>
<td>新元素必须被转到队头，队头始终是栈顶</td>
</tr>
</tbody>
</table>
<p>前者是“需要出队时再整理”，所以 <code>pop</code> / <code>peek</code> 是均摊 <code>O(1)</code>。后者是“入栈时立刻整理”，所以 <code>push</code> 是 <code>O(n)</code>，但 <code>pop</code> / <code>top</code> 很轻。</p>
<h2>两个栈实现队列</h2>
<p><a href="https://leetcode.cn/problems/implement-queue-using-stacks/">232. 用栈实现队列</a></p>
<p>队列要的是先进先出，但单个栈天然会把顺序反过来。解决办法很直观：<strong>反两次，顺序就回来了。</strong></p>
<p>新元素先进入 <code>in</code> 栈：</p>
<pre><code>push 1, 2, 3

in:  [1, 2, 3]  栈顶是 3
out: []
</code></pre>
<p>当需要弹出队头时，把 <code>in</code> 全部倒进 <code>out</code>：</p>
<pre><code>in:  []
out: [3, 2, 1]  栈顶是 1
</code></pre>
<p>这时 <code>out.pop()</code> 弹出的就是最早入队的 <code>1</code>。队列顺序靠的就是这一次完整翻转。</p>
<p><img src="https://img.picr2.online/img/2026/05/13/ed5fde33dc0f05b39837d7cd99c33a6b.webp" alt="双栈实现队列示意" /></p>
<h3>倒数据的两条原则</h3>
<p>这里真正重要的不是“有两个栈”，而是倒数据的时机：</p>
<ol>
<li>只有 <code>out</code> 为空时，才能从 <code>in</code> 往 <code>out</code> 倒。</li>
<li>只要开始倒，就必须把 <code>in</code> 一次倒完。</li>
</ol>
<p>如果 <code>out</code> 里还有旧元素，你又把新元素倒进去，新元素会压在旧元素上面，出队顺序就被破坏了。如果只倒一部分，留在 <code>in</code> 栈底的老元素会被后来进来的新元素盖住，也会乱。</p>
<p>可以把 <code>out</code> 理解成“已经排好队、等待出队的一批元素”。只要这批元素还没出完，新来的元素就只能待在 <code>in</code> 里排下一批。</p>
<h3>代码实现</h3>
<pre><code>class MyQueue {

    public Stack&lt;Integer&gt; in;
    public Stack&lt;Integer&gt; out;

    public MyQueue() {
        in = new Stack&lt;Integer&gt;();
        out = new Stack&lt;Integer&gt;();
    }

    // 倒数据：从 in 栈倒入 out 栈
    // 原则 1：out 空了才能倒
    // 原则 2：只要倒，就必须把 in 全部倒完
    private void inToOut() {
        if (out.empty()) {
            while (!in.empty()) {
                out.push(in.pop());
            }
        }
    }

    public void push(int x) {
        // 新元素先进入 in，排在下一批等待出队
        in.push(x);
        // 如果 out 为空，就顺手把 in 倒过去；out 不空则不会影响旧元素先出
        inToOut();
    }

    public int pop() {
        // 出数据是从out出的
        // 如果 out 为空，会从 in 补一批,因为可能出现：
        // in:  [2, 3]
        // out: []
        inToOut();
        return out.pop();
    }

    public int peek() {
        // 和 pop 一样，先保证 out 栈顶是当前队头
        inToOut();
        return out.peek();
    }

    public boolean empty() {
        return in.isEmpty() &amp;&amp; out.isEmpty();
    }

}
</code></pre>
<p>这里容易误会的一点是：<code>inToOut()</code> <strong>不是每次调用都会倒数据</strong>。它内部先判断 <code>out.empty()</code>，只有 <code>out</code> 空了，才会把 <code>in</code> 一次性倒过去；如果 <code>out</code> 不空，就什么都不做。</p>
<p>具体过程是这样：</p>
<pre><code>push(1)
in:  [1]
out: []

out 为空，inToOut() 真正倒数据：
in:  []
out: [1]

push(2)
in:  [2]
out: [1]

out 不空，inToOut() 什么都不做：
in:  [2]
out: [1]

所以 1 仍然在 out 栈顶，先出。

如果后面 out 被弹空了，再执行 pop() / peek()：
in:  [2, 3]
out: []

out 为空，inToOut() 把 in 全部倒过去：
in:  []
out: [3, 2]  栈顶是 2

这时 out.pop() / out.peek() 拿到的就是队头 2。
</code></pre>
<p>可以这样记：</p>
<pre><code>push：先放进 in，再尝试倒
pop：先尝试倒，再从 out 弹
peek：先尝试倒，再从 out 看
</code></pre>
<p>核心不是“每次都倒”，而是：每次操作前后调用 <code>inToOut()</code> 都没问题，因为它只在 <code>out</code> 为空时真正执行。</p>
<p>:::note[LeetCode 调用格式]
平台上的原始输入会写成方法名数组和参数数组，输出则是每一步调用的返回值：</p>
<pre><code>输入：
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]

输出：
[null, null, null, 1, 1, false]
</code></pre>
<p>:::</p>
<h3>最容易错的两种写法</h3>
<p>第一种是 <code>out</code> 非空也继续倒：</p>
<pre><code>private void inToOut() {
    while (!in.empty()) {
        out.push(in.pop());
    }
}
</code></pre>
<p>假设 <code>out</code> 栈顶已经是队头 <code>1</code>，这时又把 <code>in</code> 里的新元素倒进去，新元素会压到 <code>1</code> 上面，下一次弹出的就不是队头了。</p>
<p>第二种是只倒一个元素：</p>
<pre><code>private void inToOut() {
    if (out.empty() &amp;&amp; !in.empty()) {
        out.push(in.pop()); // 只倒了一个！
    }
}
</code></pre>
<p>这会让 <code>in</code> 栈底部的老元素继续被压在下面，后续再加入新元素时，老元素就不可能按队列顺序先出来。</p>
<p>:::tip[口诀]</p>
<p><code>in</code> 负责进，<code>out</code> 负责出；<code>out</code> 空了才倒，倒就倒干净。
:::</p>
<h2>一个队列实现栈</h2>
<p><a href="https://leetcode.cn/problems/implement-stack-using-queues/">225. 用队列实现栈</a></p>
<p>队列只能从队尾进、队头出。想让它表现得像栈，就要让“最后加入的元素”立刻来到队头。</p>
<p>做法放在 <code>push</code> 上：先把新元素正常入队，再把它前面的所有旧元素依次从队头取出，重新放到队尾。</p>
<p>例如队列当前是 <code>[2, 1]</code>，队头是 <code>2</code>，表示栈顶是 <code>2</code>。现在执行 <code>push(3)</code>：</p>
<pre><code>入队 3 → [2, 1, 3]
旋转第 1 个 → [1, 3, 2]
旋转第 2 个 → [3, 2, 1]    ← 3 到了队头，正是栈顶
</code></pre>
<p>这样一来，队头永远保存栈顶。<code>pop()</code> 就是 <code>queue.poll()</code>，<code>top()</code> 就是 <code>queue.peek()</code>。</p>
<p><img src="https://img.picr2.online/img/2026/05/13/127ec2466b28f457f31c1954186e3e64.webp" alt="单队列实现栈示意" /></p>
<h3>代码实现</h3>
<pre><code>class MyStack {

    Queue&lt;Integer&gt; queue;

    public MyStack() {
        queue = new LinkedList&lt;Integer&gt;();
    }

    // O(n)：先记录旧元素个数，再把它们旋转到新元素后面
    public void push(int x) {
        int n = queue.size();
        queue.offer(x);
        for (int i = 0; i &lt; n; i++) {
            queue.offer(queue.poll());
        }
    }

    public int pop() {
        return queue.poll();
    }

    public int top() {
        return queue.peek();
    }

    public boolean empty() {
        return queue.isEmpty();
    }

}
</code></pre>
<p>为什么要先记录 <code>n = queue.size()</code>？因为 <code>n</code> 表示这次 <code>push</code> 之前的旧元素个数。新元素入队后，只需要把这 <code>n</code> 个旧元素转到后面，新元素就会停在队头。</p>
<p>:::note[LeetCode 调用格式]
平台上的原始输入会写成方法名数组和参数数组，输出则是每一步调用的返回值：</p>
<pre><code>输入：
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]

输出：
[null, null, null, 2, 2, false]
</code></pre>
<p>:::</p>
<h3>旋转次数别写错</h3>
<p>最常见的错误，是把循环条件写成动态的 <code>queue.size()</code>：</p>
<pre><code>public void push(int x) {
    queue.offer(x);
    for (int i = 0; i &lt; queue.size(); i++) { // 错！size 包含了刚入队的新元素
        queue.offer(queue.poll());
    }
}
</code></pre>
<p>这里有两个问题。第一，<code>queue.size()</code> 包含刚入队的新元素，旋转次数会多一次，新元素可能又被转回队尾。第二，循环过程中虽然 <code>poll</code> 和 <code>offer</code> 让总大小看起来没变，但代码表达的是一个会被操作影响的状态，读起来也很危险。</p>
<p>正确做法是：<strong>入队前先记住旧元素个数。</strong></p>
<pre><code>public void push(int x) {
    int n = queue.size();
    queue.offer(x);
    for (int i = 0; i &lt; n; i++) {
        queue.offer(queue.poll());
    }
}
</code></pre>
<p>:::tip[口诀]</p>
<p>新元素入队，旧元素全部轮转到后面，队头永远是栈顶。
:::</p>
<h2>复杂度对比</h2>
<table>
<thead>
<tr>
<th>实现</th>
<th><code>push</code></th>
<th><code>pop</code></th>
<th><code>peek</code> / <code>top</code></th>
<th><code>empty</code></th>
<th>空间</th>
</tr>
</thead>
<tbody>
<tr>
<td>双栈实现队列</td>
<td><code>O(1)</code></td>
<td>均摊 <code>O(1)</code></td>
<td>均摊 <code>O(1)</code></td>
<td><code>O(1)</code></td>
<td><code>O(n)</code></td>
</tr>
<tr>
<td>单队列实现栈</td>
<td><code>O(n)</code></td>
<td><code>O(1)</code></td>
<td><code>O(1)</code></td>
<td><code>O(1)</code></td>
<td><code>O(n)</code></td>
</tr>
</tbody>
</table>
<p>双栈队列的均摊 <code>O(1)</code> 来自一个事实：每个元素最多进 <code>in</code> 一次，再从 <code>in</code> 倒到 <code>out</code> 一次，不会被反复搬来搬去。</p>
<p>单队列栈则把整理成本固定放在 <code>push</code> 上：每加入一个新元素，都要把旧元素轮转一遍。</p>
<h2>记法</h2>
<p>这两题可以放在一起记：</p>
<ul>
<li><strong>队列要老元素先出</strong>：用 <code>out</code> 保存已经排好顺序的一批老元素，<code>out</code> 空了才补货。</li>
<li><strong>栈要新元素先出</strong>：每次 <code>push</code> 后立刻把新元素转到队头，让它下一次第一个出来。</li>
</ul>
<p>一个是“分批翻转”，一个是“即时旋转”。把这两个动作想清楚，代码基本就不会写乱。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>数据结构：队列、栈和循环队列</title>
    <link href="https://blog.yoake.dev/posts/algorithm/05-queuestackandcircularqueue/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/05-queuestackandcircularqueue/</id>
    <published>2026-05-12T04:41:05.000Z</published>
    <updated>2026-05-12T04:41:05.000Z</updated>
    <summary>整理队列和栈的常见实现方式，以及数组循环队列中 l、r、size 的维护细节。</summary>
    <content type="html"><![CDATA[<h2>题目概览</h2>
<h3>范围</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class013</code></td>
<td>QueueStackAndCircularQueue</td>
</tr>
</tbody>
</table>
<h3>题型归纳</h3>
<p><strong>题型：</strong> 基础数据结构实现</p>
<p><strong>适用条件：</strong> 需要自己实现队列、栈、循环队列，或者题目要求在固定数组空间内完成入队、出队、取队头、取队尾等操作。</p>
<p><strong>解法：</strong> 队列用数组配合左右边界 <code>l</code>、<code>r</code>，栈用数组配合 <code>size</code>，循环队列额外维护当前元素个数 <code>size</code> 和容量 <code>limit</code>。</p>
<p><strong>易错点：</strong> 队列的有效范围是 <code>[l, r)</code>，栈顶位置是 <code>size - 1</code>，循环队列的 <code>r</code> 指向下一个可写位置，取尾元素时要处理 <code>r == 0</code> 的回绕。</p>
<blockquote>
<p>看到"固定数组空间内实现队列或栈"就是这类题的信号——数组队列用 <code>[l, r)</code> 半开区间管理有效范围，数组栈用 <code>size</code> 同时表示元素个数和下一个写入位置。循环队列的核心是让 <code>r</code> 到达末尾后回绕到 <code>0</code>，并用独立的 <code>size</code> 区分空和满；最容易出错的是 <code>Rear()</code> 直接返回 <code>queue[r]</code>——<code>r</code> 是下一个可写位置，真正的队尾在 <code>r</code> 的前一个位置，且要处理 <code>r == 0</code> 时回绕到 <code>limit - 1</code> 的情况。</p>
</blockquote>
<h2>基础结构</h2>
<h3>队列</h3>
<p>队列是先进先出结构，常见操作如下：</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>offer</code></td>
<td>从尾部加入元素</td>
</tr>
<tr>
<td><code>poll</code></td>
<td>从头部弹出元素</td>
</tr>
<tr>
<td><code>peek</code> / <code>head</code></td>
<td>查看头部元素但不弹出</td>
</tr>
<tr>
<td><code>tail</code></td>
<td>查看尾部元素</td>
</tr>
<tr>
<td><code>size</code></td>
<td>返回当前元素数量</td>
</tr>
</tbody>
</table>
<h3>栈</h3>
<p>栈是后进先出结构，常见操作如下：</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>push</code></td>
<td>压入栈顶</td>
</tr>
<tr>
<td><code>pop</code></td>
<td>弹出栈顶</td>
</tr>
<tr>
<td><code>peek</code></td>
<td>查看栈顶但不弹出</td>
</tr>
<tr>
<td><code>size</code></td>
<td>返回当前元素数量</td>
</tr>
</tbody>
</table>
<h2>队列实现</h2>
<h3>Java 内部实现</h3>
<p>如果直接使用 Java 内置结构，可以用 <code>LinkedList</code> 当队列。</p>
<pre><code>public static class Queue1 {

    public Queue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();

    public boolean isEmpty() {
        return queue.isEmpty();
    }

    public void offer(int num) {
        queue.offer(num);
    }

    public int poll() {
        return queue.poll();
    }

    public int peek() {
        return queue.peek();
    }

    public int size() {
        return queue.size();
    }

}
</code></pre>
<p>这种写法好处是简单，坏处是 <code>LinkedList</code> 内部是双向链表，常数时间不如数组实现。</p>
<h3>数组队列</h3>
<p>刷题时更常见的写法是提前准备一个数组，用 <code>l</code> 和 <code>r</code> 表示队列范围：</p>
<pre><code>[l, r)
</code></pre>
<p>也就是：</p>
<ul>
<li><code>l</code> 指向当前队头。</li>
<li><code>r</code> 指向下一个可写入的位置。</li>
<li>队列为空时，<code>l == r</code>。</li>
<li>当前元素个数是 <code>r - l</code>。</li>
</ul>
<p>核心模板如下：</p>
<pre><code>public static class Queue2 {

    public int[] queue;
    public int l;
    public int r;

    public Queue2(int n) {
        queue = new int[n];
        l = 0;
        r = 0;
    }

    public boolean isEmpty() {
        return l == r;
    }

    public void offer(int num) {
        queue[r++] = num;
    }

    public int poll() {
        return queue[l++];
    }

    public int head() {
        return queue[l];
    }

    public int tail() {
        return queue[r - 1];
    }

    public int size() {
        return r - l;
    }

}
</code></pre>
<h3>指针角色</h3>
<table>
<thead>
<tr>
<th>变量</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>queue</code></td>
<td>保存队列元素的数组</td>
</tr>
<tr>
<td><code>l</code></td>
<td>队头位置，弹出时向右移动</td>
</tr>
<tr>
<td><code>r</code></td>
<td>下一个可写位置，加入时向右移动</td>
</tr>
</tbody>
</table>
<p>例如依次加入 <code>3, 5, 7</code> 后：</p>
<pre><code>queue = [3, 5, 7, ...]
l = 0
r = 3
有效范围 = [0, 3)
</code></pre>
<p>弹出一次后：</p>
<pre><code>l = 1
r = 3
有效范围 = [1, 3)
队头 = queue[1]
队尾 = queue[2]
</code></pre>
<p>这种普通数组队列适合能确定加入操作总次数不超过 <code>n</code> 的场景。即使中间有弹出，<code>l</code> 之前的空间也不会复用。</p>
<h2>栈实现</h2>
<h3>Java 内部实现</h3>
<p>可以直接使用 Java 的 <code>Stack</code>：</p>
<pre><code>public static class Stack1 {

    public Stack&lt;Integer&gt; stack = new Stack&lt;&gt;();

    public boolean isEmpty() {
        return stack.isEmpty();
    }

    public void push(int num) {
        stack.push(num);
    }

    public int pop() {
        return stack.pop();
    }

    public int peek() {
        return stack.peek();
    }

    public int size() {
        return stack.size();
    }

}
</code></pre>
<p>这种写法也很方便，但刷题时如果数据范围明确，数组栈通常更直接。</p>
<h3>数组栈</h3>
<p>数组实现栈只需要一个 <code>size</code>。</p>
<pre><code>public static class Stack2 {

    public int[] stack;
    public int size;

    public Stack2(int n) {
        stack = new int[n];
        size = 0;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void push(int num) {
        stack[size++] = num;
    }

    public int pop() {
        return stack[--size];
    }

    public int peek() {
        return stack[size - 1];
    }

    public int size() {
        return size;
    }

}
</code></pre>
<p><code>size</code> 有两层含义：</p>
<ul>
<li>当前栈里有几个元素。</li>
<li>下一个新元素应该写到哪个位置。</li>
</ul>
<p>所以入栈是 <code>stack[size++] = num</code>，出栈是 <code>return stack[--size]</code>——栈顶元素永远在 <code>size - 1</code> 位置，弹出前要先让 <code>size</code> 回到栈顶下标。</p>
<h2>例题：622. 设计循环队列</h2>
<p><a href="https://leetcode.cn/problems/design-circular-queue/">题目链接</a></p>
<h3>题意</h3>
<p>设计一个循环队列实现。循环队列是一种线性数据结构，其操作表现基于 FIFO（先进先出）原则，并且队尾被连接在队首之后形成一个循环。它也被称为"环形缓冲器"。</p>
<p>循环队列的好处是可以利用队列之前用过的空间。普通队列一旦队尾走到数组末尾，就不能继续插入下一个元素，即使前面因为出队操作空出了位置。循环队列则可以让队尾回到数组开头，继续使用这些空位。</p>
<p>需要实现 <code>MyCircularQueue</code> 类：</p>
<ul>
<li><code>MyCircularQueue(k)</code> 构造器，设置队列长度为 <code>k</code>。</li>
<li><code>Front()</code> 从队首获取元素，如果队列为空，返回 <code>-1</code>。</li>
<li><code>Rear()</code> 获取队尾元素，如果队列为空，返回 <code>-1</code>。</li>
<li><code>enQueue(value)</code> 向循环队列插入一个元素，如果成功插入则返回 <code>true</code>。</li>
<li><code>deQueue()</code> 从循环队列中删除一个元素，如果成功删除则返回 <code>true</code>。</li>
<li><code>isEmpty()</code> 检查循环队列是否为空。</li>
<li><code>isFull()</code> 检查循环队列是否已满。</li>
</ul>
<h3>示例</h3>
<p>创建一个容量为 <code>3</code> 的循环队列：</p>
<pre><code>MyCircularQueue circularQueue = new MyCircularQueue(3);
</code></pre>
<table>
<thead>
<tr>
<th>步骤</th>
<th>操作</th>
<th>返回值</th>
<th>队列内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>enQueue(1)</code></td>
<td><code>true</code></td>
<td><code>[1]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>enQueue(2)</code></td>
<td><code>true</code></td>
<td><code>[1, 2]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>enQueue(3)</code></td>
<td><code>true</code></td>
<td><code>[1, 2, 3]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>enQueue(4)</code></td>
<td><code>false</code></td>
<td><code>[1, 2, 3]</code>，队列已满</td>
</tr>
<tr>
<td>5</td>
<td><code>Rear()</code></td>
<td><code>3</code></td>
<td>队尾是 <code>3</code></td>
</tr>
<tr>
<td>6</td>
<td><code>isFull()</code></td>
<td><code>true</code></td>
<td>当前容量已满</td>
</tr>
<tr>
<td>7</td>
<td><code>deQueue()</code></td>
<td><code>true</code></td>
<td><code>[2, 3]</code></td>
</tr>
<tr>
<td>8</td>
<td><code>enQueue(4)</code></td>
<td><code>true</code></td>
<td><code>[2, 3, 4]</code></td>
</tr>
<tr>
<td>9</td>
<td><code>Rear()</code></td>
<td><code>4</code></td>
<td>队尾是 <code>4</code></td>
</tr>
</tbody>
</table>
<p>:::note[LeetCode 调用格式]
平台上的原始输入会写成方法名数组和参数数组，输出则是每一步调用的返回值：</p>
<pre><code>输入：
["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]

输出：
[null, true, true, true, false, 3, true, true, true, 4]
</code></pre>
<p>:::</p>
<h3>提示</h3>
<ul>
<li>所有的值都在 <code>0</code> 至 <code>1000</code> 的范围内</li>
<li>操作数将在 <code>1</code> 至 <code>1000</code> 的范围内</li>
<li>请不要使用内置的队列库</li>
</ul>
<h2>解法</h2>
<h3>核心思路</h3>
<p>普通数组队列的问题是：弹出后，前面的空间不会再使用。循环队列要解决的就是这个问题：当 <code>r</code> 走到数组最后一个位置后，如果前面有空位，就让 <code>r</code> 回到 <code>0</code> 继续写。</p>
<p>关键在于：循环数组里 <code>l == r</code> 既可能表示空，也可能表示满。额外维护 <code>size</code> 后，<code>size == 0</code> 表示空，<code>size == limit</code> 表示满，判断就变得更清楚。</p>
<blockquote>
<p>普通队列的 <code>l</code> 之前是"死空间"，循环队列让 <code>r</code> 回绕到数组开头，把死空间变成可复用空间——代价是需要一个独立的 <code>size</code> 来区分空和满。</p>
</blockquote>
<h3>核心模板</h3>
<pre><code>class MyCircularQueue {

    public int[] queue;

    public int l, r, size, limit;

    public MyCircularQueue(int k) {
        queue = new int[k];
        l = r = size = 0;
        limit = k;
    }

    public boolean enQueue(int value) {
        if (isFull()) {
            return false;
        } else {
            queue[r] = value;
            r = r == limit - 1 ? 0 : (r + 1);
            size++;
            return true;
        }
    }

    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        } else {
            l = l == limit - 1 ? 0 : (l + 1);
            size--;
            return true;
        }
    }

    public int Front() {
        if (isEmpty()) {
            return -1;
        } else {
            return queue[l];
        }
    }

    public int Rear() {
        if (isEmpty()) {
            return -1;
        } else {
            int last = r == 0 ? (limit - 1) : (r - 1);
            return queue[last];
        }
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public boolean isFull() {
        return size == limit;
    }

}
</code></pre>
<h3>变量角色</h3>
<p><code>queue</code> 是固定长度的底层数组；<code>l</code> 指向当前队头位置；<code>r</code> 指向下一个可写位置，入队后向右移动并在末尾回绕到 <code>0</code>；<code>size</code> 记录当前队列里的元素个数，用来区分空和满；<code>limit</code> 是队列容量，即数组长度。</p>
<h3>过程拆解</h3>
<ol>
<li><strong>入队</strong>：先判满，没满则把值写到 <code>r</code> 位置，移动 <code>r</code>（到末尾回绕），<code>size++</code>。</li>
<li><strong>出队</strong>：先判空，不空则移动 <code>l</code>（到末尾回绕），<code>size--</code>。不需要清空旧值，边界变量决定有效数据。</li>
<li><strong>取队头</strong>：直接返回 <code>queue[l]</code>。</li>
<li><strong>取队尾</strong>：<code>r</code> 是下一个可写位置，真正的队尾在 <code>r</code> 的前一个位置。<code>r == 0</code> 时回绕到 <code>limit - 1</code>，否则就是 <code>r - 1</code>。</li>
<li><strong>判空判满</strong>：<code>size == 0</code> 为空，<code>size == limit</code> 为满。</li>
</ol>
<h3>易错点</h3>
<ul>
<li>入队成功后移动 <code>r</code> 并且 <code>size++</code>，出队成功后移动 <code>l</code> 并且 <code>size--</code>，两步缺一不可。</li>
<li>数组里的旧值不用清理，边界变量才决定当前有效数据。</li>
</ul>
<p>:::warning[<code>enQueue()</code> 只写 <code>queue[r++]</code>]
错误写法：</p>
<pre><code>public boolean enQueue(int value) {
    if (isFull()) return false;
    queue[r++] = value; // 错！r 可能走到 limit，下一次访问就越界
    size++;
    return true;
}
</code></pre>
<p>为什么错：循环队列的 <code>r</code> 到达数组最后一个位置后，不能继续 <code>++</code> 到 <code>limit</code>，而是要回绕到 <code>0</code>。</p>
<p>正确写法：</p>
<pre><code>public boolean enQueue(int value) {
    if (isFull()) return false;
    queue[r] = value;
    r = r == limit - 1 ? 0 : r + 1;
    size++;
    return true;
}
</code></pre>
<p>:::</p>
<p>:::warning[<code>deQueue()</code> 只改 <code>size</code>，不移动 <code>l</code>]
错误写法：</p>
<pre><code>public boolean deQueue() {
    if (isEmpty()) return false;
    size--; // 错！队头 l 没动，Front() 还会读到旧队头
    return true;
}
</code></pre>
<p>为什么错：出队删除的是当前队头，成功出队后必须让 <code>l</code> 移到下一个位置。数组里的旧值不用清理，但队头边界必须更新。</p>
<p>正确写法：</p>
<pre><code>public boolean deQueue() {
    if (isEmpty()) return false;
    l = l == limit - 1 ? 0 : l + 1;
    size--;
    return true;
}
</code></pre>
<p>:::</p>
<p>:::warning[<code>Rear()</code> 直接返回 <code>queue[r]</code>]
错误写法：</p>
<pre><code>public int Rear() {
    if (isEmpty()) return -1;
    return queue[r]; // 错！r 是下一个可写位置，不是队尾
}
</code></pre>
<p>为什么错：<code>r</code> 指向的是下一个将要写入的位置，不是最后一个已写入的位置。如果 <code>r == 0</code>，实际队尾在 <code>queue[limit - 1]</code>；否则队尾在 <code>queue[r - 1]</code>。</p>
<p>正确写法：</p>
<pre><code>public int Rear() {
    if (isEmpty()) return -1;
    int last = r == 0 ? (limit - 1) : (r - 1);
    return queue[last];
}
</code></pre>
<p>:::</p>
<p>:::warning[用 <code>l == r</code> 判断空或满]
错误写法：</p>
<pre><code>public boolean isEmpty() {
    return l == r; // 循环队列中 l == r 可能是空，也可能是满！
}
</code></pre>
<p>为什么错：在循环数组中，队列从空开始入队直到满，<code>l</code> 和 <code>r</code> 会再次相遇。仅靠 <code>l == r</code> 无法区分这两种状态。</p>
<p>正确写法：用独立的 <code>size</code> 变量区分：</p>
<pre><code>public boolean isEmpty() {
    return size == 0;
}

public boolean isFull() {
    return size == limit;
}
</code></pre>
<p>:::</p>
<p>:::tip[口诀]
<code>l</code> 指队头，<code>r</code> 指下一个可写位置，<code>size</code> 决定空和满，到末尾就回绕。
:::</p>
<h2>复杂度</h2>
<ul>
<li>队列、栈、循环队列的每个操作时间复杂度都是 <code>O(1)</code>。</li>
<li>数组队列和数组栈的空间复杂度是 <code>O(n)</code>。</li>
<li>循环队列的空间复杂度是 <code>O(k)</code>，<code>k</code> 是初始化时给定的容量。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>链表：分隔链表</title>
    <link href="https://blog.yoake.dev/posts/algorithm/04-partitionlist/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/04-partitionlist/</id>
    <published>2026-05-11T13:28:04.000Z</published>
    <updated>2026-05-11T13:28:04.000Z</updated>
    <summary>整理链表中按给定值分区的双链表收集过程、稳定性要求和断链细节。</summary>
    <content type="html"><![CDATA[<h2>题目概览</h2>
<h3>范围</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class012</code></td>
<td>PartitionList</td>
</tr>
</tbody>
</table>
<h3>题型归纳</h3>
<p><strong>题型：</strong> 链表分区</p>
<p><strong>题目特征：</strong> 需要把小于 <code>x</code> 的节点放到前面，大于等于 <code>x</code> 的节点放到后面，并且保留每个分区内部的原始相对顺序。</p>
<p><strong>解法：</strong> 准备两条链表，分别收集 <code>&lt; x</code> 和 <code>&gt;= x</code> 的节点，最后把两条链表拼接起来。</p>
<p><strong>易错点：</strong> 节点摘下后要断开 <code>next</code>，追加节点后要更新尾指针，小于区为空时要直接返回大于等于区。</p>
<blockquote>
<p>看到"分区且保留相对顺序"就是稳定分区的信号——<em>这里不是排序，而是稳定分区</em>。核心做法是维护两条链表各自的头尾指针，遍历时把节点逐一摘下接入对应区，最后拼接；最容易出错的是摘节点时忘记断开 <code>head.next</code>，导致旧的 <code>next</code> 关系污染新链表结构。</p>
</blockquote>
<h2>例题：86. 分隔链表</h2>
<p><a href="https://leetcode.cn/problems/partition-list/description/">题目链接</a></p>
<h3>题意</h3>
<p>给你一个链表的头节点 <code>head</code> 和一个特定值 <code>x</code>，对链表进行分隔，使得所有<strong>小于</strong> <code>x</code> 的节点都出现在<strong>大于或等于</strong> <code>x</code> 的节点之前，并且<strong>保留</strong>两个分区中每个节点的初始相对位置。</p>
<h3>示例</h3>
<p><strong>示例 1：</strong></p>
<p><img src="https://assets.leetcode.com/uploads/2021/01/04/partition.jpg" alt="分隔前后对比：小于3的节点提前，内部顺序不变" /></p>
<p><strong>输入：</strong> <code>head</code> = <code>[1,4,3,2,5,2]</code>, <code>x</code> = <code>3</code></p>
<p><strong>输出：</strong> <code>[1,2,2,4,3,5]</code></p>
<p><strong>示例 2：</strong></p>
<p><strong>输入：</strong> <code>head</code> = <code>[2,1]</code>, <code>x</code> = <code>2</code></p>
<p><strong>输出：</strong> <code>[1,2]</code></p>
<h3>提示</h3>
<ul>
<li>链表中节点的数目在范围 <code>[0, 200]</code> 内</li>
<li><code>-100 &lt;= Node.val &lt;= 100</code></li>
<li><code>-200 &lt;= x &lt;= 200</code></li>
</ul>
<h2>解法</h2>
<h3>核心思路</h3>
<p>这道题不是排序，而是<strong>稳定分区</strong>——<code>&gt;= x</code> 的节点之间不会被重新排列，只是整体移到后面。</p>
<p>准备两条链表：<code>left</code> 收集所有 <code>&lt; x</code> 的节点，<code>right</code> 收集所有 <code>&gt;= x</code> 的节点。遍历原链表时，把每个节点先断开 <code>next</code>，再接到对应链表的尾部。遍历结束后把两条链表拼接即可。</p>
<blockquote>
<p><code>4 -&gt; 3 -&gt; 5</code> 不会变成 <code>3 -&gt; 4 -&gt; 5</code>，因为要求保留原始相对顺序，这是和排序最根本的区别。</p>
</blockquote>
<p><img src="https://img.picr2.online/img/2026/05/11/561e3101fcea92812fbb5e5e5f57792e.png" alt="双链表分区过程示意一" /></p>
<p><img src="https://img.picr2.online/img/2026/05/11/d0064e610cc3ac48b8f2e6a6e497e236.png" alt="双链表分区过程示意二" /></p>
<h3>核心模板</h3>
<pre><code>public static ListNode partition(ListNode head, int x) {
    ListNode leftHead = null, leftTail = null;
    ListNode rightHead = null, rightTail = null;

    while (head != null) {
        ListNode next = head.next;
        head.next = null;

        if (head.val &lt; x) {
            if (leftHead == null) {
                leftHead = head;
                leftTail = head;
            } else {
                leftTail.next = head;
                leftTail = head;
            }
        } else {
            if (rightHead == null) {
                rightHead = head;
                rightTail = head;
            } else {
                rightTail.next = head;
                rightTail = head;
            }
        }

        head = next;
    }

    if (leftHead == null) {
        return rightHead;
    }
    leftTail.next = rightHead;
    return leftHead;
}
</code></pre>
<h3>变量角色</h3>
<p><code>leftHead</code> 和 <code>rightHead</code> 分别记住两条链表的头节点；<code>leftTail</code> 和 <code>rightTail</code> 始终指向各自链表的尾节点，每接入一个新节点就跟着往后移动一位；<code>next</code> 临时保存原链表的下一个节点，在断开 <code>head.next</code> 之前必须先保存，否则后续节点会丢失。</p>
<h3>过程拆解</h3>
<ol>
<li><strong>保存并断开</strong>：每轮先把 <code>head.next</code> 存入 <code>next</code>，再把 <code>head.next</code> 置为 <code>null</code>，把当前节点单独摘出来。</li>
<li><strong>按值分流</strong>：<code>head.val &lt; x</code> 接入 <code>left</code>，否则接入 <code>right</code>；第一次接入时同时设置头尾指针，之后只更新尾指针。</li>
<li><strong>推进遍历</strong>：<code>head = next</code> 走到下一个节点。</li>
<li><strong>收尾拼接</strong>：<code>leftHead == null</code> 时直接返回 <code>rightHead</code>；否则把 <code>leftTail.next</code> 指向 <code>rightHead</code>，返回 <code>leftHead</code>。</li>
</ol>
<h3>易错点</h3>
<ul>
<li>每次追加节点后必须更新尾指针（<code>leftTail = head</code> / <code>rightTail = head</code>），否则尾指针停在第一个节点，后续每次都覆盖同一个 <code>next</code>，前面的节点全部丢失。</li>
<li><code>leftHead == null</code> 时直接返回 <code>rightHead</code>，不需要拼接。反过来 <code>rightHead == null</code> 时不需要特判，因为 <code>leftTail.next = null</code> 本身就是合法的。</li>
</ul>
<p>:::warning[摘节点时忘记断开 <code>head.next</code>]
错误写法：只保存 <code>next</code>，没有把 <code>head.next</code> 置为 <code>null</code>：</p>
<pre><code>ListNode next = head.next;
// 漏写 head.next = null;

if (head.val &lt; x) {
    leftTail.next = head;
    leftTail = head;
}
head = next;
</code></pre>
<p>为什么错：<code>head</code> 带着原来的 <code>next</code> 被接入新链表，它的 <code>next</code> 仍然指向原链表中的后续节点，新链表的结构会被旧连接污染，最终形成环或者串入不该出现的节点。</p>
<p>正确写法：</p>
<pre><code>ListNode next = head.next;
head.next = null; // 先断干净，再接入新链表
</code></pre>
<p>:::</p>
<p>:::tip[口诀]
保存 <code>next</code>，断开当前，按值接尾，最后 <code>left</code> 拼 <code>right</code>。
:::</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：<code>O(n)</code>，<code>n</code> 是链表长度</li>
<li>空间复杂度：<code>O(1)</code>，只使用有限几个指针</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>链表：两数相加</title>
    <link href="https://blog.yoake.dev/posts/algorithm/03-addtwonumbers/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/03-addtwonumbers/</id>
    <published>2026-05-11T10:28:35.000Z</published>
    <updated>2026-05-11T10:28:35.000Z</updated>
    <summary>整理链表中两数相加的逐位模拟过程、进位处理和代码模板。</summary>
    <content type="html"><![CDATA[<h2>题目概览</h2>
<h3>范围</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class011</code></td>
<td>AddTwoNumbers</td>
</tr>
</tbody>
</table>
<h3>题型归纳</h3>
<p><strong>题型：</strong> 链表模拟加法</p>
<p><strong>适用条件：</strong> 两个链表按逆序存储数字，每个节点只表示一位数字。</p>
<p><strong>解法：</strong> 从两个链表头节点开始逐位相加，同时维护进位 <code>carry</code>，每一位生成一个新节点。</p>
<p><strong>易错点：</strong> 两个链表长度不同、最后还有进位、空指针取值、返回结果链表头节点。</p>
<blockquote>
<p>看到"链表逆序存储数字、需要模拟加法"就是这种写法的信号——因为头节点恰好是个位，逆序存储反而省去了反转的麻烦，可以直接从头往后扫。核心做法是逐位求 <code>sum = h1.val + h2.val + carry</code>，当前位取 <code>sum % 10</code>，进位取 <code>sum / 10</code>；最容易出错的是循环条件写成 <code>&amp;&amp;</code> 导致漏算较长链表的高位，以及循环结束后忘记补最后一个进位节点。</p>
</blockquote>
<h2>例题：2. 两数相加</h2>
<p><a href="https://leetcode.cn/problems/add-two-numbers/description/">题目链接</a></p>
<h3>题意</h3>
<p>给定两个非空链表 <code>l1</code> 和 <code>l2</code>，分别表示两个非负整数，数字按逆序存储，每个节点只存一位。把两个数相加后，用同样的逆序链表形式返回结果。</p>
<h3>示例</h3>
<p><strong>示例 1：</strong></p>
<p><img src="https://assets.leetcode.cn/aliyun-lc-upload/uploads/2021/01/02/addtwonumber1.jpg" alt="342 + 465 = 807，链表逆序存储示意" /></p>
<p><strong>输入：</strong> <code>l1</code> = <code>[2,4,3]</code>, <code>l2</code> = <code>[5,6,4]</code></p>
<p><strong>输出：</strong> <code>[7,0,8]</code></p>
<p><strong>解释：</strong> <code>342 + 465 = 807</code></p>
<p><strong>示例 2：</strong></p>
<p><strong>输入：</strong> <code>l1</code> = <code>[0]</code>, <code>l2</code> = <code>[0]</code></p>
<p><strong>输出：</strong> <code>[0]</code></p>
<p><strong>示例 3：</strong></p>
<p><strong>输入：</strong> <code>l1</code> = <code>[9,9,9,9,9,9,9]</code>, <code>l2</code> = <code>[9,9,9,9]</code></p>
<p><strong>输出：</strong> <code>[8,9,9,9,0,0,0,1]</code></p>
<h3>提示</h3>
<ul>
<li>每个链表中的节点数在范围 <code>[1, 100]</code> 内</li>
<li><code>0 &lt;= Node.val &lt;= 9</code></li>
<li>题目数据保证列表表示的数字不含前导零</li>
</ul>
<h2>解法</h2>
<h3>核心思路</h3>
<p>这道题本质就是竖式加法，只是数字已经用链表逆序存好了。</p>
<p>因为头节点就是个位，所以可以直接从两个链表头部开始往后走，逐位求和：当前位的结果是 <code>sum % 10</code>，新的进位是 <code>sum / 10</code>，已经为空的链表在后续计算中按 <code>0</code> 处理。</p>
<blockquote>
<p>链表逆序存储不是麻烦，而是恩赐——个位在头节点，天然对齐竖式加法的计算顺序，不需要任何反转。</p>
</blockquote>
<h3>核心模板</h3>
<pre><code>public static ListNode addTwoNumbers(ListNode h1, ListNode h2) {
    ListNode ans = null;
    ListNode cur = null;
    int carry = 0;

    for (int sum, val;
            h1 != null || h2 != null;
            h1 = h1 == null ? null : h1.next,
            h2 = h2 == null ? null : h2.next) {

        sum = (h1 == null ? 0 : h1.val)
                + (h2 == null ? 0 : h2.val)
                + carry;
        val = sum % 10;
        carry = sum / 10;

        if (ans == null) {
            ans = new ListNode(val);
            cur = ans;
        } else {
            cur.next = new ListNode(val);
            cur = cur.next;
        }
    }

    if (carry == 1) {
        cur.next = new ListNode(1);
    }
    return ans;
}
</code></pre>
<h3>变量角色</h3>
<p><code>h1</code> 和 <code>h2</code> 分别指向两条链表当前正在相加的节点；<code>ans</code> 记住答案链表的头节点，最终返回它；<code>cur</code> 始终指向答案链表的尾节点，每生成一个新节点就跟着往后移动一位——<code>ans</code> 和 <code>cur</code> 初始都为 <code>null</code>，第一次生成节点时同时赋值，之后只动 <code>cur</code>，不动 <code>ans</code>。</p>
<h3>过程拆解</h3>
<ol>
<li><strong>逐位求和</strong>：每轮取 <code>h1.val</code>（为空按 <code>0</code>）、<code>h2.val</code>（为空按 <code>0</code>）和 <code>carry</code> 求和。</li>
<li><strong>生成节点</strong>：<code>val = sum % 10</code> 作为当前节点值，更新 <code>carry = sum / 10</code>。</li>
<li><strong>接入链表</strong>：第一个节点同时赋给 <code>ans</code> 和 <code>cur</code>，后续接到 <code>cur.next</code>，再移动 <code>cur</code>。</li>
<li><strong>推进指针</strong>：<code>h1</code> 和 <code>h2</code> 各自向后移动，已为空的继续保持 <code>null</code>。</li>
<li><strong>补进位</strong>：循环结束后如果 <code>carry == 1</code>，在 <code>cur</code> 后面补一个值为 <code>1</code> 的节点。</li>
</ol>
<h3>易错点</h3>
<ul>
<li>第一次创建节点时，<code>ans</code> 和 <code>cur</code> 都要赋值；之后只移动 <code>cur</code>，<code>ans</code> 不能动，否则最终找不到链表头。</li>
</ul>
<p>:::warning[循环条件写成 <code>&amp;&amp;</code> 会漏算高位]
错误写法：</p>
<pre><code>while (h1 != null &amp;&amp; h2 != null)
</code></pre>
<p>为什么错：较短链表走完后循环立即停止，较长链表剩余的高位全部丢失。比如 <code>l1 = [1]</code>、<code>l2 = [9,9]</code>，正确结果是 <code>[0,0,1]</code>，用 <code>&amp;&amp;</code> 只会得到 <code>[0]</code>。</p>
<p>正确写法：</p>
<pre><code>h1 != null || h2 != null
</code></pre>
<p>两条链表都走完才停，已经为空的那条当前位按 <code>0</code> 处理。
:::</p>
<p>:::warning[循环结束后忘记补进位节点]
错误写法：直接 <code>return ans</code>，不检查 <code>carry</code>。</p>
<p>为什么错：最高位相加可能产生进位。比如 <code>l1 = [9]</code>、<code>l2 = [9]</code>，当前位结果是 <code>8</code>，<code>carry = 1</code>，正确答案是 <code>[8,1]</code>，漏掉进位会返回 <code>[8]</code>。</p>
<p>正确写法：</p>
<pre><code>if (carry == 1) {
    cur.next = new ListNode(1);
}
</code></pre>
<p>:::</p>
<p>:::tip[口诀]
<code>||</code> 控制循环，空链表按 <code>0</code>，循环后补进位。
:::</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：<code>O(max(n, m))</code>，<code>n</code> 和 <code>m</code> 分别是两个链表的长度</li>
<li>空间复杂度：<code>O(max(n, m))</code>，答案链表需要保存相加结果</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>链表：合并两个有序链表</title>
    <link href="https://blog.yoake.dev/posts/algorithm/02-mergetwolists/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/02-mergetwolists/</id>
    <published>2026-05-11T02:59:31.000Z</published>
    <updated>2026-05-11T02:59:31.000Z</updated>
    <summary>整理链表中合并两个有序链表的指针连接过程、边界处理和代码模板。</summary>
    <content type="html"><![CDATA[<h2>题目概览</h2>
<h3>范围</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class010</code></td>
<td>MergeTwoLists</td>
</tr>
</tbody>
</table>
<h3>题型归纳</h3>
<p><strong>题型：</strong> 链表双指针</p>
<p><strong>题目特征：</strong> 两个链表都已经按非递减顺序排列，需要把所有节点重新串成一个有序链表。</p>
<p><strong>解法：</strong> 每次比较两个链表当前节点的值，把较小的节点接到结果链表后面。</p>
<p><strong>易错点：</strong> 处理空链表、确定新头节点、移动 <code>pre</code> 指针、最后接上剩余链表。</p>
<blockquote>
<p>看到"两个链表都已有序、合并为一个有序链表"就是双指针扫描的信号。核心做法是先选出两个头中较小的作为新头，然后用 <code>pre</code>、<code>cur1</code>、<code>cur2</code> 三个指针同步推进；最容易出错的是循环体内漏写 <code>pre = pre.next</code>，或者循环结束后逐个处理剩余节点而不是直接整体接上。</p>
</blockquote>
<h2>例题：21. 合并两个有序链表</h2>
<p><a href="https://leetcode.cn/problems/merge-two-sorted-lists/description/">题目链接</a></p>
<h3>题意</h3>
<p>给定两个按非递减顺序排列的链表 <code>list1</code> 和 <code>list2</code>，将它们合并成一个新的非递减链表并返回。</p>
<p>新链表由原来两个链表中的节点拼接而成，不需要创建新的节点。</p>
<h3>示例</h3>
<p><strong>示例 1：</strong></p>
<p><img src="https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg" alt="合并示例：[1,2,4] 和 [1,3,4] 合并为 [1,1,2,3,4,4]" /></p>
<p><strong>输入：</strong> list1 = <code>[1,2,4]</code>, list2 = <code>[1,3,4]</code></p>
<p><strong>输出：</strong> <code>[1,1,2,3,4,4]</code></p>
<p><strong>示例 2：</strong></p>
<p><strong>输入：</strong> list1 = <code>[]</code>, list2 = <code>[]</code></p>
<p><strong>输出：</strong> <code>[]</code></p>
<p><strong>示例 3：</strong></p>
<p><strong>输入：</strong> list1 = <code>[]</code>, list2 = <code>[0]</code></p>
<p><strong>输出：</strong> <code>[0]</code></p>
<h3>提示</h3>
<ul>
<li>两个链表的节点数目范围是 <code>[0, 50]</code></li>
<li><code>-100 &lt;= Node.val &lt;= 100</code></li>
<li><code>list1</code> 和 <code>list2</code> 均按非递减顺序排列</li>
</ul>
<h2>解法</h2>
<h3>核心思路</h3>
<p>因为两个链表本身已经有序，所以只需要从两个链表的头节点开始比较，把较小的节点接到结果链表后面，被接走的链表指针向后移动一位，<code>pre</code> 始终指向结果链表的最后一个节点。</p>
<p>这里不使用虚拟头节点，而是先比较 <code>head1</code> 和 <code>head2</code>，选出较小的作为真正的头节点 <code>head</code>，再用 <code>cur1</code> 和 <code>cur2</code> 从各自起点开始扫描。</p>
<blockquote>
<p>两个链表都有序，所以剩余部分的所有节点必然大于等于已合并部分的尾部——循环结束后直接整体接上，不需要逐个处理。</p>
</blockquote>
<p><img src="https://img.picr2.online/img/2026/05/11/098da86e83f199959f9cdf7c2c40cd37.png" alt="双指针合并过程示意" /></p>
<h3>核心模板</h3>
<pre><code>public static ListNode mergeTwoLists(ListNode head1, ListNode head2) {
    if (head1 == null || head2 == null) {
        return head1 == null ? head2 : head1;
    }

    // 谁小谁做头节点
    ListNode head = head1.val &lt;= head2.val ? head1 : head2;
    ListNode cur1 = head.next;
    ListNode cur2 = head == head1 ? head2 : head1;
    ListNode pre = head;

    while (cur1 != null &amp;&amp; cur2 != null) {
        // 谁小接谁
        if (cur1.val &lt;= cur2.val) {
            pre.next = cur1;
            cur1 = cur1.next;
        } else {
            pre.next = cur2;
            cur2 = cur2.next;
        }
        pre = pre.next;
    }

    // 剩余部分直接整体接上
    pre.next = cur1 != null ? cur1 : cur2;
    return head;
}
</code></pre>
<h3>变量角色</h3>
<p><code>head</code> 是合并后链表的真正头节点，最终返回它；<code>cur1</code> 从选为头节点的那条链表的 <code>head.next</code> 开始走，<code>cur2</code> 从另一条链表的头节点开始走；<code>pre</code> 始终指向结果链表当前的尾节点，每接上一个新节点就跟着往后移动一位。</p>
<h3>过程拆解</h3>
<ol>
<li><strong>空链表特判</strong>：任一为空直接返回另一个，不进入主逻辑。</li>
<li><strong>选头节点</strong>：比较 <code>head1.val</code> 和 <code>head2.val</code>，较小的作为 <code>head</code>；<code>cur1</code> 从 <code>head.next</code> 出发，<code>cur2</code> 从另一条链表头出发。</li>
<li><strong>双指针扫描</strong>：<code>cur1</code> 和 <code>cur2</code> 都不为空时，把较小的接到 <code>pre.next</code>，对应指针后移，<code>pre</code> 跟着移动。</li>
<li><strong>收尾</strong>：循环结束后至少一条为空，把不为空的那条直接接到 <code>pre.next</code>。</li>
</ol>
<h3>易错点</h3>
<ul>
<li>任一链表为空时要提前返回，不能让 <code>cur2 = head == head1 ? head2 : head1</code> 这行拿到 <code>null</code> 后继续参与比较。</li>
<li><code>cur1</code> 从 <code>head.next</code> 开始，而不是 <code>head</code>——<code>head</code> 已经是结果链表的头节点，不能再被扫描一次。</li>
</ul>
<p>:::warning[循环体内漏写 <code>pre = pre.next</code>]
错误写法：</p>
<pre><code>if (cur1.val &lt;= cur2.val) {
    pre.next = cur1;
    cur1 = cur1.next;
} else {
    pre.next = cur2;
    cur2 = cur2.next;
}
// 漏写 pre = pre.next
</code></pre>
<p>为什么错：<code>pre</code> 始终停在第一个接入的节点，之后每轮都覆盖同一个 <code>pre.next</code>，最终结果链表只剩最后一次接上的那个节点，前面的全部丢失。</p>
<p>正确写法：</p>
<pre><code>if (cur1.val &lt;= cur2.val) {
    pre.next = cur1;
    cur1 = cur1.next;
} else {
    pre.next = cur2;
    cur2 = cur2.next;
}
pre = pre.next; // 关键：pre 跟着新接入的节点往后走
</code></pre>
<p>:::</p>
<p>:::tip[口诀]
先选头，<code>pre</code> 跟着走，循环后剩余直接接。
:::</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：<code>O(n + m)</code>，<code>n</code> 和 <code>m</code> 分别是两个链表的长度</li>
<li>空间复杂度：<code>O(1)</code></li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>二分搜索：寻找峰值</title>
    <link href="https://blog.yoake.dev/posts/algorithm/01-findpeakelement/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/01-findpeakelement/</id>
    <published>2026-05-06T10:28:13.000Z</published>
    <updated>2026-05-06T10:28:13.000Z</updated>
    <summary>整理二分搜索中寻找任意峰值元素的判断逻辑、边界处理和代码模板。</summary>
    <content type="html"><![CDATA[<h2>题目概览</h2>
<h3>范围</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>class006</code></td>
<td>FindPeakElement</td>
</tr>
</tbody>
</table>
<h3>题型归纳</h3>
<p><strong>题型：</strong> 二分找局部最优</p>
<p><strong>特征：</strong> 要找任意一个峰值，且相邻元素不相等。</p>
<p><strong>解法：</strong> 根据 <code>mid</code> 左右的上升、下降趋势判断哪一侧一定存在峰值。</p>
<p><strong>易错点：</strong> 边界位置要单独判断，避免访问越界。</p>
<blockquote>
<p>峰值不要求全局最大，只要找到一个局部极大值就行——这是二分能介入的关键信号。两端各有一个隐含的 <code>-∞</code> 边界，保证了峰值一定存在。先单独判断首尾，剩下的安全区间 <code>[1, n-2]</code> 才交给二分，每次根据 <code>mid</code> 左右的斜率方向缩小范围；最容易出错的是直接从 <code>[0, n-1]</code> 开始二分，循环体内访问 <code>arr[m-1]</code> 或 <code>arr[m+1]</code> 时越界。</p>
</blockquote>
<h2>例题：162. 寻找峰值</h2>
<p><a href="https://leetcode.cn/problems/find-peak-element/">题目链接</a></p>
<h3>题意</h3>
<p>峰值元素是指其值严格大于左右相邻值的元素。</p>
<p>给你一个整数数组 <code>nums</code>，找到峰值元素并返回其索引。数组可能包含多个峰值，在这种情况下，返回 <strong>任何一个峰值</strong> 所在位置即可。</p>
<p>你可以假设 <code>nums[-1] = nums[n] = -∞</code>。</p>
<p>你必须实现时间复杂度为 <code>O(log n)</code> 的算法来解决此问题。</p>
<h3>示例</h3>
<p><strong>示例 1：</strong></p>
<p><strong>输入：</strong> nums = <code>[1,2,3,1]</code></p>
<p><strong>输出：</strong> 2</p>
<p><strong>解释：</strong> 3 是峰值元素，你的函数应该返回其索引 2。</p>
<p><strong>示例 2：</strong></p>
<p><strong>输入：</strong> nums = <code>[1,2,1,3,5,6,4]</code></p>
<p><strong>输出：</strong> 1 或 5</p>
<p><strong>解释：</strong> 你的函数可以返回索引 1，其峰值元素为 2；或者返回索引 5，其峰值元素为 6。</p>
<h3>提示</h3>
<ul>
<li><code>1 &lt;= nums.length &lt;= 1000</code></li>
<li><code>-2^31 &lt;= nums[i] &lt;= 2^31 - 1</code></li>
<li>对于所有有效的 <code>i</code> 都有 <code>nums[i] != nums[i + 1]</code></li>
</ul>
<h2>解法</h2>
<h3>核心思路</h3>
<p>把数组看成一段连续的山坡。因为题目默认 <code>nums[-1] = nums[n] = -∞</code>，并且相邻元素不相等，所以数组中一定存在峰值。</p>
<p>先单独判断两个边界：</p>
<ul>
<li>如果 <code>arr[0] &gt; arr[1]</code>，那么 <code>0</code> 位置就是峰值。</li>
<li>如果 <code>arr[n - 1] &gt; arr[n - 2]</code>，那么 <code>n - 1</code> 位置就是峰值。</li>
</ul>
<p>排除边界后，峰值一定在 <code>[1, n - 2]</code> 范围内。对中点 <code>m</code> 做判断：</p>
<ul>
<li>如果 <code>arr[m - 1] &gt; arr[m]</code>，说明左侧存在上坡，往左找。</li>
<li>如果 <code>arr[m] &lt; arr[m + 1]</code>，说明右侧存在上坡，往右找。</li>
<li>否则 <code>arr[m]</code> 同时大于左右相邻值，<code>m</code> 就是峰值。</li>
</ul>
<blockquote>
<p>沿任意上升方向走，一定能到达峰值——因为数组两端被 <code>-∞</code> 封住，上坡不可能无限延伸。</p>
</blockquote>
<p><img src="https://img.picr2.online/img/2026/05/07/6c86b2c9406aa3c8708bdac0726732ca.png" alt="根据 mid 左右斜率判断峰值方向" /></p>
<p><img src="https://img.picr2.online/img/2026/05/07/1f02d0da9eeeabe6c4e6cc5beafa0b08.png" alt="边界与安全区间划分示意" /></p>
<h3>核心模板</h3>
<pre><code>public static int findPeakElement(int[] arr) {
    int n = arr.length;
    if (n == 1) {
        return 0;
    }
    if (arr[0] &gt; arr[1]) {
        return 0;
    }
    if (arr[n - 1] &gt; arr[n - 2]) {
        return n - 1;
    }
    int l = 1, r = n - 2, m = 0, ans = -1;
    while (l &lt;= r) {
        m = (l + r) / 2;
        if (arr[m - 1] &gt; arr[m]) {
            r = m - 1;
        } else if (arr[m] &lt; arr[m + 1]) {
            l = m + 1;
        } else {
            ans = m;
            break;
        }
    }
    return ans;
}
</code></pre>
<h3>变量角色</h3>
<p><code>l</code> 和 <code>r</code> 是二分的左右边界，初始锁定在安全区间 <code>[1, n-2]</code> 内；<code>m</code> 是每轮的中点；<code>ans</code> 记录找到的峰值下标，初始为 <code>-1</code>，找到后立即跳出循环。</p>
<h3>过程拆解</h3>
<ol>
<li><strong>特判</strong>：长度为 1 时直接返回 <code>0</code>。</li>
<li><strong>首尾判断</strong>：检查 <code>arr[0]</code> 和 <code>arr[n-1]</code> 是否为峰值，是则提前返回。</li>
<li><strong>二分</strong>：在安全区间 <code>[1, n-2]</code> 内取中点 <code>m</code>，比较 <code>arr[m-1]</code>、<code>arr[m]</code>、<code>arr[m+1]</code> 三者关系。</li>
<li><strong>收边界</strong>：左侧下降则往左收，右侧上升则往右收，两侧都不满足则 <code>m</code> 即为峰值。</li>
</ol>
<h3>易错点</h3>
<p>:::warning[二分区间的边界陷阱]
错误写法：直接从 <code>[0, n-1]</code> 开始二分，循环体内同时访问 <code>arr[m-1]</code> 和 <code>arr[m+1]</code>：</p>
<pre><code>int l = 0, r = n - 1;
while (l &lt;= r) {
    m = (l + r) / 2;
    if (arr[m - 1] &gt; arr[m]) { // m == 0 时，arr[-1] 越界！
        r = m - 1;
    } else if (arr[m] &lt; arr[m + 1]) { // m == n-1 时，arr[n] 越界！
        l = m + 1;
    } else {
        return m;
    }
}
</code></pre>
<p>为什么错：<code>m</code> 可能取到 <code>0</code> 或 <code>n-1</code>，访问 <code>arr[-1]</code> 或 <code>arr[n]</code> 时直接抛出 <code>ArrayIndexOutOfBoundsException</code>。</p>
<p>正确写法一：先处理首尾，再从 <code>[1, n-2]</code> 开始二分（即本文模板）。</p>
<p>正确写法二：换成只比较 <code>arr[m]</code> 和 <code>arr[m+1]</code> 的写法，配合 <code>while (l &lt; r)</code>——此时 <code>m</code> 永远小于 <code>r</code>，<code>m+1</code> 不会越界：</p>
<pre><code>public int findPeakElement(int[] nums) {
    int l = 0, r = nums.length - 1;
    while (l &lt; r) {
        int m = l + (r - l) / 2;
        if (nums[m] &gt; nums[m + 1]) {
            r = m;
        } else {
            l = m + 1;
        }
    }
    return l;
}
</code></pre>
<p>:::</p>
<p>:::tip[口诀]
先判首尾，再在 <code>[1, n-2]</code> 内二分，靠斜率方向收边界。
:::</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：<code>O(log n)</code></li>
<li>空间复杂度：<code>O(1)</code></li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>算法学习</title>
    <link href="https://blog.yoake.dev/posts/algorithm/00-algorithm-journey-map/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/posts/algorithm/00-algorithm-journey-map/</id>
    <published>2026-04-28T05:00:28.000Z</published>
    <updated>2026-04-28T05:00:28.000Z</updated>
    <summary>跟学左程云 algorithm-journey 的路线总览、专题索引。</summary>
    <content type="html"><![CDATA[<h2>学习路线</h2>
<p>这里记录跟学 <a href="https://github.com/algorithmzuo/algorithm-journey">algorithm-journey</a> 时整理出来的专题笔记。</p>
<h2>当前进度</h2>
<table>
<thead>
<tr>
<th>分类</th>
<th>已整理</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>二分搜索</td>
<td>1</td>
<td>寻找任意峰值</td>
</tr>
<tr>
<td>链表</td>
<td>3</td>
<td>合并链表、两数相加、链表分区</td>
</tr>
<tr>
<td>数据结构</td>
<td>3</td>
<td>队列、栈、循环队列；栈和队列互相转化；最小栈</td>
</tr>
</tbody>
</table>
<h2>专题索引</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>分类</th>
<th>专题</th>
<th>目录</th>
<th>对应课程</th>
</tr>
</thead>
<tbody>
<tr>
<td>01</td>
<td>二分搜索</td>
<td><a href="/posts/algorithm/01-findpeakelement/">二分搜索：寻找峰值</a></td>
<td><code>01-findpeakelement</code></td>
<td><code>class006</code></td>
</tr>
<tr>
<td>02</td>
<td>链表</td>
<td><a href="/posts/algorithm/02-mergetwolists/">链表：合并两个有序链表</a></td>
<td><code>02-MergeTwoLists</code></td>
<td><code>class010</code></td>
</tr>
<tr>
<td>03</td>
<td>链表</td>
<td><a href="/posts/algorithm/03-addtwonumbers/">链表：两数相加</a></td>
<td><code>03-addTwoNumbers</code></td>
<td><code>class011</code></td>
</tr>
<tr>
<td>04</td>
<td>链表</td>
<td><a href="/posts/algorithm/04-partitionlist/">链表：分隔链表</a></td>
<td><code>04-PartitionList</code></td>
<td><code>class012</code></td>
</tr>
<tr>
<td>05</td>
<td>数据结构</td>
<td><a href="/posts/algorithm/05-queuestackandcircularqueue/">数据结构：队列、栈和循环队列</a></td>
<td><code>05-QueueStackAndCircularQueue</code></td>
<td><code>class013</code></td>
</tr>
<tr>
<td>06</td>
<td>数据结构</td>
<td><a href="/posts/algorithm/06-convertqueueandstack/">数据结构：栈和队列的互相转化</a></td>
<td><code>06-ConvertQueueAndStack</code></td>
<td><code>class014</code></td>
</tr>
<tr>
<td>07</td>
<td>数据结构</td>
<td><a href="/posts/algorithm/07-minstack/">数据结构：最小栈</a></td>
<td><code>07-MinStack</code></td>
<td><code>class015</code></td>
</tr>
</tbody>
</table>
<h2>分类索引</h2>
<h3>二分搜索</h3>
<ul>
<li><a href="/posts/algorithm/01-findpeakelement/">二分搜索：寻找峰值</a>：根据上升、下降趋势判断哪一侧一定存在峰值。</li>
</ul>
<h3>链表</h3>
<ul>
<li><a href="/posts/algorithm/02-mergetwolists/">链表：合并两个有序链表</a>：用双指针比较两个有序链表当前节点，并把较小节点接到结果链表后面。</li>
<li><a href="/posts/algorithm/03-addtwonumbers/">链表：两数相加</a>：按链表逆序存储的数字做逐位加法，并维护进位。</li>
<li><a href="/posts/algorithm/04-partitionlist/">链表：分隔链表</a>：用两条链表分别收集 <code>&lt; x</code> 和 <code>&gt;= x</code> 的节点，最后稳定拼接。</li>
</ul>
<h3>数据结构</h3>
<ul>
<li><a href="/posts/algorithm/05-queuestackandcircularqueue/">数据结构：队列、栈和循环队列</a>：用数组实现队列和栈，并用 <code>l</code>、<code>r</code>、<code>size</code> 维护循环队列。</li>
<li><a href="/posts/algorithm/06-convertqueueandstack/">数据结构：栈和队列的互相转化</a>：双栈实现队列（懒倒数据）、单队列实现栈（入队时旋转）。</li>
<li><a href="/posts/algorithm/07-minstack/">数据结构：最小栈</a>：用辅助栈同步保存每一层的最小值快照，让 <code>getMin()</code> 保持 <code>O(1)</code>。</li>
</ul>
]]></content>
    <author><name>黎明</name></author>
    <category term="Algorithm"/>
  </entry>
  <entry>
    <title>Typora 自动上传图片实践：PicGo App 与 PicGo-Core 两套方案踩坑记录</title>
    <link href="https://blog.yoake.dev/typora-picgo-r2-auto-upload/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/typora-picgo-r2-auto-upload/</id>
    <published>2026-03-15T10:52:31.000Z</published>
    <updated>2026-03-15T10:52:31.000Z</updated>
    <summary>记录 Typora 接入 S3 兼容图床的完整过程，对比 PicGo App 与 PicGo-Core(Custom Command) 两套方案，并解决 Cloudflare R2 自定义域名多出 /picture/ 前缀的问题。</summary>
    <content type="html"><![CDATA[<h1>写在前面</h1>
<p>我用的组合是 Typora + Cloudflare R2，图片访问走自定义域名，上传走 PicGo。理论上这套东西搭起来很顺，网上教程也不少，但实际弄的时候还是踩了几个坑。这篇就是把整个排查过程记下来，主要是给以后的自己看，顺便如果你也在用类似的组合，可能也能少折腾一会儿。</p>
<p>踩到的问题主要集中在三个地方：</p>
<ul>
<li>Typora 自带的 <code>PicGo-Core (command line)</code> 版本太旧，跑不起来新插件</li>
<li><code>PicGo App</code> 输入框里写正则和直接改 <code>config.json</code> 转义方式不一样</li>
<li>开了 <code>pathStyleAccess=true</code> 之后，最终图片链接会莫名多一个桶名</li>
</ul>
<hr />
<h1>Typora 自带的 PicGo-Core 先别用</h1>
<p>我最开始很自然地选了 Typora 偏好设置里的 <code>PicGo-Core (command line)</code>，看着像官方做好的集成，结果测试图片上传直接报错：</p>
<pre><code>[PicGo ERROR]: SyntaxError: Unexpected token '?'
[PicGo WARN]: Can't find uploader - smms, swtich to default uploader - smms
[PicGo ERROR]: Error: Can't find smms config
</code></pre>
<p>我以为是 S3 参数填错了，翻来覆去检查密钥、endpoint、bucket，折腾了一圈，最后才发现问题根本不在配置——是 Typora 自己下载到本地的那个 <code>picgo.exe</code> 版本太旧，新版的 <code>aws-s3</code> 插件根本加载不起来，加载失败之后又退回到默认的 <code>smms</code> 上传器，所以才一直报找不到 smms 配置。</p>
<p>所以如果你也碰到类似报错，可以先绕过这个内置选项，走另外两条路：</p>
<ol>
<li>直接用 <strong>PicGo App</strong>（图形界面）</li>
<li>或者用 <strong>Custom Command</strong>（调用本机装好的 PicGo CLI）</li>
</ol>
<hr />
<h1>方案一：PicGo App</h1>
<h2>配置</h2>
<p>Typora 偏好设置 → 图像，<code>上传服务</code> 改成 <code>PicGo(app)</code>，然后保证 PicGo App 已经打开在后台跑着就行。</p>
<p><img src="https://img.picr2.online/img/2026/03/15/ac8353ed1b0ec2c6f78f37dc57948cf2.png" alt="image-20260315190114014" /></p>
<p>PicGo App 里的 Amazon S3 配置（Cloudflare R2 填 S3 兼容参数）：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>填什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>应用密钥 ID</td>
<td>Access Key ID</td>
</tr>
<tr>
<td>应用密钥</td>
<td>Secret Access Key</td>
</tr>
<tr>
<td>桶名</td>
<td><code>picture</code></td>
</tr>
<tr>
<td>上传文件路径</td>
<td><code>img/{year}/{month}/{day}/{md5}.{extName}</code></td>
</tr>
<tr>
<td>地区</td>
<td><code>auto</code></td>
</tr>
<tr>
<td>自定义节点</td>
<td><code>https://你的账户.r2.cloudflarestorage.com</code></td>
</tr>
<tr>
<td>拒绝无效 TLS 证书连接</td>
<td><code>yes</code></td>
</tr>
<tr>
<td>ACL 访问控制列表</td>
<td><code>public-read</code></td>
</tr>
<tr>
<td>ForcePathStyle</td>
<td><code>yes</code></td>
</tr>
<tr>
<td>自定义输出 URL 模板</td>
<td><code>https://你的图片域名/{path:/^picture\//g,''}</code></td>
</tr>
<tr>
<td>Bucket 前缀</td>
<td><code>no</code></td>
</tr>
</tbody>
</table>
<p>我自己填的模板长这样：</p>
<pre><code>https://img.picr2.online/{path:/^picture\//g,''}
</code></pre>
<h2>坑：正则转义</h2>
<p>这个是最容易搞错的地方。</p>
<p><strong>在 PicGo App 输入框里写</strong>：</p>
<pre><code>https://img.picr2.online/{path:/^picture\//g,''}
</code></pre>
<p><strong>存进 <code>config.json</code> 之后会变成</strong>：</p>
<pre><code>"outputURLPattern": "https://img.picr2.online/{path:/^picture\\//g,''}"
</code></pre>
<p>也就是说 GUI 里写单反斜杠，JSON 里会自动变双反斜杠。如果你手动改了 <code>config.json</code>，或者直接把 JSON 里的写法粘到 GUI 输入框，正则就会失效，最终图片链接会多出一截桶名：</p>
<pre><code># 不对的
https://img.picr2.online/picture/img/2026/03/15/xxx.png

# 想要的
https://img.picr2.online/img/2026/03/15/xxx.png
</code></pre>
<h2>为什么会多出 <code>/picture/</code></h2>
<p>这个问题本身也值得说一下。开了 <code>ForcePathStyle = yes</code> 之后，插件内部拿到的原始路径格式是：</p>
<pre><code>/{bucketName}/{objectKey}
</code></pre>
<p>也就是：</p>
<pre><code>/picture/img/2026/03/15/xxx.png
</code></pre>
<p>所以输出模板如果只是简单写 <code>{path}</code>，桶名就会直接带进去。要把它去掉，就得在模板里用正则替换：</p>
<pre><code>https://img.picr2.online/{path:/^picture\//g,''}
</code></pre>
<hr />
<h1>方案二：Custom Command（更推荐）</h1>
<h2>为什么我更喜欢这套</h2>
<p>主要是因为不依赖 Typora 自带的那个旧版 <code>picgo.exe</code>，而是直接调你本机装好的 PicGo CLI。插件版本、Node 环境、配置文件都是你自己控制的，出了问题也可以直接在终端里手动跑一遍复现。</p>
<h2>安装</h2>
<p>先确认本机有 Node：</p>
<pre><code>node -v
npm -v
</code></pre>
<p>然后全局装 PicGo 和 S3 插件：</p>
<pre><code>npm install -g picgo
picgo add s3
picgo use uploader aws-s3
</code></pre>
<p>如果你在 GUI 里创建过一个叫 <code>s3</code> 的配置，也可以显式指定：</p>
<pre><code>picgo use uploader aws-s3 s3
</code></pre>
<p>PicGo CLI 默认配置文件在：</p>
<pre><code>C:\Users\你的用户名\.picgo\config.json
</code></pre>
<h2>Typora 里怎么填</h2>
<p><img src="https://img.picr2.online/img/2026/03/15/f5e09c28046b6c539dd061fdbee5e8a9.png" alt="image-20260315190307525" /></p>
<p>偏好设置 → 图像，<code>上传服务</code> 选 <code>自定义命令</code>，命令填：</p>
<pre><code>C:\nvm4w\nodejs\picgo.cmd upload
</code></pre>
<p>路径改成你本机 Node 的实际位置。</p>
<h2>最小可用的 <code>config.json</code></h2>
<p>用 Custom Command 的话，我建议把配置文件精简一下，只保留真正需要的字段：</p>
<pre><code>{
  "picBed": {
    "current": "aws-s3",
    "uploader": "aws-s3",
    "transformer": "path",
    "aws-s3": {
      "accessKeyID": "你的AccessKeyID",
      "secretAccessKey": "你的SecretAccessKey",
      "bucketName": "picture",
      "uploadPath": "img/{year}/{month}/{day}/{md5}.{extName}",
      "region": "auto",
      "endpoint": "https://你的账户.r2.cloudflarestorage.com",
      "proxy": "",
      "rejectUnauthorized": true,
      "acl": "public-read",
      "pathStyleAccess": true,
      "outputURLPattern": "https://img.example.com/{path:/^picture\\//g,''}",
      "urlPrefix": "",
      "urlSuffix": "",
      "disableBucketPrefixToURL": true
    }
  },
  "picgoPlugins": {
    "picgo-plugin-s3": true
  }
}
</code></pre>
<h2>关于 <code>s3</code> 和 <code>aws-s3</code> 同时出现</h2>
<p>如果你以前在 GUI 里改来改去，配置文件里可能会看到 <code>s3</code> 和 <code>aws-s3</code> 同时存在，看上去像重复，实际上很多是旧字段、缓存或历史残留。</p>
<p><code>picgo-plugin-s3</code> 现在真正读的是 <code>picBed.aws-s3</code>，所以如果只用一套方案，其他的可以直接删掉，保持干净。</p>
<hr />
<h1>怎么选</h1>
<p>如果只看能不能跑，两套都行。</p>
<p>但如果你本机已经有 Node 和 PicGo CLI，建议直接走 Custom Command，省心很多。PicGo App 适合不想碰命令行、就要图形界面的场景，不过要注意 GUI 正则转义那个坑。</p>
<hr />
<h1>排错清单</h1>
<p>配好了还是不工作，可以按这个顺序查：</p>
<ol>
<li>Typora 是不是还在用自带旧版 <code>PicGo-Core</code></li>
<li>本机 <code>picgo</code> 命令能不能直接在终端跑</li>
<li><code>picgo-plugin-s3</code> 有没有装</li>
<li><code>endpoint</code>、<code>bucketName</code>、<code>accessKeyID</code>、<code>secretAccessKey</code> 有没有填对</li>
<li><code>pathStyleAccess</code> 和你的对象存储是否匹配</li>
<li>自定义域名能不能直接访问对象</li>
<li><code>outputURLPattern</code> 有没有正确去掉桶名前缀</li>
<li>GUI 输入框和 JSON 文件里的反斜杠转义有没有搞混</li>
</ol>
<hr />
<h1>最后</h1>
<p>整个弄下来，感觉最绕的地方不是哪一步特别复杂，而是 Typora、PicGo App、PicGo CLI、S3 接口、对象存储、CDN、自定义域名这几层叠在一起，每一层看着都没问题，但一旦某个地方有点偏差，最后就是一个莫名其妙的错误链。</p>
<p>配好之后其实很稳，之后截图直接拖进 Typora 就能用，也不用管其他的。</p>
]]></content>
    <author><name>黎明</name></author>
    <category term="Tools"/>
  </entry>
  <entry>
    <title>克制的艺术：从一张 Claude 风格页面看设计取舍</title>
    <link href="https://blog.yoake.dev/claude-design-philosophy-breakdown/" rel="alternate" type="text/html"/>
    <id>https://blog.yoake.dev/claude-design-philosophy-breakdown/</id>
    <published>2026-02-04T09:34:30.000Z</published>
    <updated>2026-02-04T09:34:30.000Z</updated>
    <summary>拆解一张 Claude 风格页面的配色、排版和交互：它怎样用少量设计决定，做出安静而耐读的界面。</summary>
    <content type="html"><![CDATA[<h1>页面概览</h1>
<p>这是一张以 <strong>The Art of Restraint</strong> 为题的 Claude 风格页面。我喜欢它的地方，不是它多么“像 AI”，恰恰是它刻意避开了那套已经有些泛滥的视觉套路：黑底、蓝紫渐变、玻璃面板和无处不在的动效。</p>
<p>页面把重点放回了阅读本身。暖白背景、偏书刊感的标题字体，以及不抢戏的交互，组合起来有一种安静但不寡淡的气质。下面不是要给它贴一套宏大的“设计哲学”，只是把几个真正影响观感的取舍拆开聊聊。</p>
<blockquote>
<p>文中分析的是这份 <code>claude.html</code> 示例的实现与风格，不代表 Anthropic 或 Claude 的官方设计规范。</p>
</blockquote>
<h2>值得借鉴的 6 个取舍</h2>
<h3>1) 不用纯白，先把底色定下来</h3>
<p><code>#FAF9F6</code>、<code>#1C1C16</code> 和 <code>#D97757</code> 是这张页面最重要的三种颜色：偏纸张的背景、近似油墨的正文，以及一点陶土橘作为强调色。</p>
<p>这组配色并不新奇，但很适合长内容。背景比纯白收一点，文字也不是死黑；两者的对比仍然足够，页面却少了些屏幕直冲眼睛的感觉。暗色模式同样没有直接翻成黑白，而是用了 <code>#191918</code>、<code>#242423</code> 这样的暖灰，和浅色主题是一套气质。</p>
<h3>2) 衬线体只放在该出现的地方</h3>
<p>标题使用 <code>Tiempos</code>、<code>Source Serif</code> 一类衬线字体，正文仍交给无衬线字体。这样的搭配没有把页面做成“复古杂志”，但让标题比常见 SaaS 页面多了一点停顿感。</p>
<p>更关键的是分寸：衬线体负责层级和情绪，正文负责效率。再配上略紧的字距和较宽的行高，阅读节奏更接近一篇文章，而不是一组控制台面板。</p>
<h3>3) 容器不是装饰，而是在帮内容分组</h3>
<ul>
<li>固定导航用半透明背景和 <code>backdrop-filter: blur(12px)</code>，滚动时仍能保持边界；</li>
<li>作者信息、标签和日期集中在文章开头，读者能很快判断内容属性；</li>
<li>Artifact 代码块有独立标题栏，代码不再混在正文里；</li>
<li>表格、色卡和标签沿用相近的边框、圆角与阴影。</li>
</ul>
<p>这些组件都很轻，但边界清楚。读者扫页面时，不需要逐段分辨“这是什么内容”。</p>
<h3>4) 动效只回应操作，不争夺注意力</h3>
<p>页面里的动效并不多：主题切换、颜色和边框的短过渡，以及一个闪烁的光标。它们给页面一点反馈，却没有拿动画当主角。</p>
<p>这一点尤其值得抄作业。内容型页面最怕每个模块都在动；0.2～0.3 秒的过渡已经足够让状态变化显得自然，剩下的空间留给文字。</p>
<h3>5) 小细节服务于阅读</h3>
<p><code>17px</code> 基础字号和 <code>1.75</code> 的行高是很朴素、也很有效的长文配置。链接只用细下划线和一点偏移来提示，选中文字时再出现品牌色，既不会让页面一直显得很“用力”，又保留了必要的识别度。</p>
<p>这里没有哪一个细节会让人惊呼，但它们叠在一起，决定了读者愿不愿意继续往下看。</p>
<h3>6) 实现也保持克制</h3>
<p>HTML 只使用了 <code>nav</code>、<code>header</code>、<code>article</code> 等常规语义标签；颜色、间距和字体集中为 CSS 变量；JavaScript 只处理主题切换。对于这样一张展示页，这已经够了。</p>
<p>它的价值不在于技术多复杂，而在于每一层都没有超出需求。想把它改成自己的页面时，换一套 token、补充内容组件，就能继续用下去。</p>
<h2>网页完整源代码（claude.html）</h2>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="zh-CN"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;The Art of Restraint - Claude Design Philosophy&lt;/title&gt;
    &lt;style&gt;
        /* =========================================
           1. Claude Design System (Design Tokens)
           ========================================= */
        :root {
            /* [Light Mode Palette] 
               核心色板：模拟高级纸张与印刷油墨 
            */
            --bg-paper: #FAF9F6;        /* 羊皮纸白 */
            --bg-surface: #FFFFFF;      /* 纯白 */
            --bg-subtle: #F2F0E8;       /* 极淡暖灰 */
            --bg-code: #FBFBF9;         /* 代码块背景 */
            
            --text-primary: #1C1C16;    /* 深炭黑 */
            --text-secondary: #5F5E5A;  /* 暖灰 */
            --text-tertiary: #8E8C85;   /* 浅灰 */
            
            --accent-primary: #D97757;  /* 陶土橘 */
            --accent-hover: #C15F3C;
            --accent-faint: rgba(217, 119, 87, 0.08);

            --border-light: rgba(0, 0, 0, 0.06);
            --border-medium: #E6E4DC;

            /* 阴影 */
            --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
            --shadow-md: 0 4px 6px rgba(0,0,0,0.04);
            --nav-bg: rgba(250, 249, 246, 0.95);

            /* 字体栈 */
            --font-serif: "Tiempos Headline", "Source Serif Pro", "Georgia", serif;
            --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            --font-mono: "JetBrains Mono", "SF Mono", Consolas, monospace;

            /* 布局 */
            --content-width: 740px;
            --radius-md: 8px;
            --radius-lg: 12px;

            /* 选中色 (Selection) - Light Mode */
            /* 修改为品牌陶土橘，实现视觉上的绝对统一 */
            --selection-bg: #D97757; 
            --selection-text: #FFFFFF;
        }

        /* [Dark Mode Palette]
           暖暗色模式：不是纯黑，而是深褐/深灰，保持温润感
        */
        body.dark-mode {
            --bg-paper: #191918;        /* 深暖灰背景 */
            --bg-surface: #242423;      /* 稍微亮一点的表面 */
            --bg-subtle: #2C2C2B;       /* 交互元素背景 */
            --bg-code: #222221;         /* 代码背景 */

            --text-primary: #ECEBE6;    /* 暖白文字 */
            --text-secondary: #A1A09A;  /* 浅灰文字 */
            --text-tertiary: #6E6D69;   

            --border-light: rgba(255, 255, 255, 0.08);
            --border-medium: rgba(255, 255, 255, 0.12);
            
            --accent-faint: rgba(217, 119, 87, 0.15);
            --nav-bg: rgba(25, 25, 24, 0.95);
            --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
            --shadow-md: 0 4px 12px rgba(0,0,0,0.4);

            /* 选中色 (Selection) - Dark Mode */
            /* 暗色模式下保持陶土色，但使用半透明以柔和融入深色背景 */
            --selection-bg: rgba(217, 119, 87, 0.5); 
            --selection-text: #FFFFFF;
        }

        /* =========================================
           2. Typography &amp; Reset &amp; Selection
           ========================================= */
        * { box-sizing: border-box; transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; }

        /* 全局选中样式重写 */
        ::selection {
            background-color: var(--selection-bg);
            color: var(--selection-text);
            /* 兼容性处理 */
            -webkit-text-fill-color: var(--selection-text);
        }
        
        /* 针对 WebKit 浏览器的特定前缀 (Chrome, Safari) */
        ::-moz-selection {
            background-color: var(--selection-bg);
            color: var(--selection-text);
        }

        body {
            background-color: var(--bg-paper);
            color: var(--text-primary);
            font-family: var(--font-sans);
            line-height: 1.75;
            font-size: 17px;
            margin: 0;
            padding: 0;
            -webkit-font-smoothing: antialiased;
        }

        h1, h2, h3, h4 {
            font-family: var(--font-serif);
            color: var(--text-primary);
            font-weight: 600;
            line-height: 1.3;
            margin-top: 2em;
            margin-bottom: 0.8em;
            letter-spacing: -0.015em;
        }

        h1 { font-size: 2.8rem; margin-top: 0; margin-bottom: 0.5em; }
        h2 { font-size: 1.8rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.3em; }
        h3 { font-size: 1.4rem; margin-top: 1.5em; }

        p { margin-bottom: 1.5em; color: var(--text-primary); opacity: 0.95; }
        
        strong { color: var(--text-primary); font-weight: 600; }
        
        a {
            color: var(--text-primary);
            text-decoration: underline;
            text-decoration-color: var(--border-medium);
            text-decoration-thickness: 1px;
            text-underline-offset: 4px;
            transition: all 0.2s;
        }
        
        a:hover {
            color: var(--accent-primary);
            text-decoration-color: var(--accent-primary);
        }

        /* =========================================
           3. Layout Structure
           ========================================= */
        .page-wrapper {
            max-width: var(--content-width);
            margin: 0 auto;
            padding: 100px 24px 120px;
        }

        /* 顶部导航 */
        nav {
            position: fixed;
            top: 0; left: 0; right: 0;
            background: var(--nav-bg);
            backdrop-filter: blur(12px);
            border-bottom: 1px solid var(--border-light);
            padding: 16px 0;
            z-index: 100;
        }
        .nav-content {
            max-width: var(--content-width);
            margin: 0 auto;
            padding: 0 24px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .nav-logo {
            font-family: var(--font-serif);
            font-weight: bold;
            font-size: 1.2rem;
            display: flex;
            align-items: center;
            gap: 10px;
            text-decoration: none;
            color: var(--text-primary);
        }
        .logo-icon {
            width: 24px; height: 24px;
            background: var(--accent-primary);
            border-radius: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 14px;
            font-family: var(--font-serif);
        }

        /* 主题切换按钮 */
        .theme-toggle {
            background: transparent;
            border: 1px solid var(--border-medium);
            color: var(--text-secondary);
            width: 36px; height: 36px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.2s;
        }
        .theme-toggle:hover {
            background: var(--bg-subtle);
            color: var(--text-primary);
            border-color: var(--text-tertiary);
        }

        /* =========================================
           4. Article Components
           ========================================= */
        
        /* 元数据 */
        .article-meta {
            display: flex;
            align-items: center;
            gap: 16px;
            margin-bottom: 48px;
            font-size: 0.95rem;
            color: var(--text-secondary);
            border-bottom: 1px solid var(--border-light);
            padding-bottom: 32px;
        }
        .avatar {
            width: 48px; height: 48px;
            border-radius: 50%;
            background: var(--bg-subtle);
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: var(--font-serif);
            color: var(--text-secondary);
            font-size: 1.2rem;
            border: 1px solid var(--border-light);
        }
        .meta-info { display: flex; flex-direction: column; gap: 2px; }
        .author-name { font-weight: 600; color: var(--text-primary); }

        /* 引用块 */
        blockquote {
            border-left: 3px solid var(--accent-primary);
            margin: 2.5em 0;
            padding: 12px 0 12px 24px;
            font-family: var(--font-serif);
            font-style: italic;
            font-size: 1.35rem;
            color: var(--text-secondary);
            background: linear-gradient(to right, var(--accent-faint), transparent);
            border-radius: 0 8px 8px 0;
        }

        /* 图片与图注 */
        figure { margin: 2.5em 0; }
        figure img {
            width: 100%;
            border-radius: var(--radius-lg);
            border: 1px solid var(--border-light);
            display: block;
            box-shadow: var(--shadow-md);
        }
        figcaption {
            margin-top: 12px;
            text-align: center;
            font-size: 0.9rem;
            color: var(--text-secondary);
            font-family: var(--font-sans);
        }

        /* Artifact 代码块 */
        .artifact {
            border: 1px solid var(--border-medium);
            border-radius: var(--radius-lg);
            background: var(--bg-surface);
            margin: 2em 0;
            overflow: hidden;
            box-shadow: var(--shadow-sm);
        }
        .artifact-header {
            background: var(--bg-subtle);
            padding: 10px 16px;
            border-bottom: 1px solid var(--border-light);
            display: flex;
            justify-content: space-between;
            font-size: 0.85rem;
            color: var(--text-secondary);
            font-family: var(--font-sans);
            font-weight: 600;
        }
        .artifact-body {
            padding: 24px;
            overflow-x: auto;
            background: var(--bg-code);
        }
        pre { margin: 0; font-family: var(--font-mono); font-size: 0.9rem; color: var(--text-primary); }

        /* 极简表格 */
        .table-wrapper {
            width: 100%;
            overflow-x: auto;
            margin: 2em 0;
            border: 1px solid var(--border-medium);
            border-radius: var(--radius-md);
            box-shadow: var(--shadow-sm);
        }
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 0.95rem;
            background: var(--bg-surface);
        }
        th {
            text-align: left;
            padding: 14px 20px;
            border-bottom: 1px solid var(--border-medium);
            background-color: var(--bg-subtle);
            font-weight: 600;
            color: var(--text-secondary);
            font-size: 0.8rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        td {
            padding: 16px 20px;
            border-bottom: 1px solid var(--border-light);
            color: var(--text-primary);
        }
        tr:last-child td { border-bottom: none; }

        /* 特性列表 */
        .feature-list { list-style: none; padding: 0; margin: 2em 0; }
        .feature-item { display: flex; gap: 16px; margin-bottom: 24px; }
        .feature-icon {
            width: 24px; height: 24px; flex-shrink: 0;
            color: var(--accent-primary); margin-top: 4px;
        }

        /* 标签 */
        .tag {
            display: inline-block;
            padding: 6px 12px;
            background-color: var(--bg-subtle);
            color: var(--text-secondary);
            font-size: 0.8rem;
            font-weight: 500;
            border-radius: 99px;
            margin-right: 8px;
            border: 1px solid var(--border-medium);
            transition: all 0.2s;
        }
        .tag:hover { border-color: var(--accent-primary); color: var(--text-primary); }

        /* 颜色样本卡片 */
        .color-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
            gap: 16px;
            margin: 2em 0;
        }
        .color-card {
            background: var(--bg-surface);
            border: 1px solid var(--border-light);
            border-radius: var(--radius-md);
            padding: 8px;
            text-align: center;
        }
        .swatch {
            height: 80px;
            border-radius: 4px;
            margin-bottom: 8px;
            border: 1px solid rgba(0,0,0,0.05);
        }
        .color-meta { font-size: 0.8rem; color: var(--text-secondary); font-family: var(--font-mono); }

        /* 模拟光标动画 */
        .cursor-blink::after {
            content: " ";
            display: inline-block;
            width: 0.5em;
            height: 1.1em;
            background-color: var(--text-primary);
            margin-left: 2px;
            vertical-align: text-bottom;
            animation: blink 1s step-end infinite;
        }
        @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }

    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;!-- 导航栏 --&gt;
    &lt;nav&gt;
        &lt;div class="nav-content"&gt;
            &lt;a href="#" class="nav-logo"&gt;
                &lt;div class="logo-icon"&gt;C&lt;/div&gt;
                Anthropic Research
            &lt;/a&gt;
            
            &lt;div style="display: flex; align-items: center; gap: 16px;"&gt;
                &lt;span style="font-size: 0.9rem; color: var(--text-secondary); display: none; @media(min-width: 600px){display:block;}"&gt;Blog&lt;/span&gt;
                
                &lt;!-- 主题切换按钮 --&gt;
                &lt;button class="theme-toggle" id="themeBtn" aria-label="Toggle Dark Mode"&gt;
                    &lt;!-- Sun Icon --&gt;
                    &lt;svg class="sun-icon" style="display: block;" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;circle cx="12" cy="12" r="5"&gt;&lt;/circle&gt;&lt;line x1="12" y1="1" x2="12" y2="3"&gt;&lt;/line&gt;&lt;line x1="12" y1="21" x2="12" y2="23"&gt;&lt;/line&gt;&lt;line x1="4.22" y1="4.22" x2="5.64" y2="5.64"&gt;&lt;/line&gt;&lt;line x1="18.36" y1="18.36" x2="19.78" y2="19.78"&gt;&lt;/line&gt;&lt;line x1="1" y1="12" x2="3" y2="12"&gt;&lt;/line&gt;&lt;line x1="21" y1="12" x2="23" y2="12"&gt;&lt;/line&gt;&lt;line x1="4.22" y1="19.78" x2="5.64" y2="18.36"&gt;&lt;/line&gt;&lt;line x1="18.36" y1="5.64" x2="19.78" y2="4.22"&gt;&lt;/line&gt;&lt;/svg&gt;
                    &lt;!-- Moon Icon (Hidden by default logic handled in script) --&gt;
                &lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/nav&gt;

    &lt;div class="page-wrapper"&gt;
        
        &lt;!-- 文章头部 --&gt;
        &lt;header&gt;
            &lt;div class="tags" style="margin-bottom: 24px;"&gt;
                &lt;span class="tag"&gt;Design System&lt;/span&gt;
                &lt;span class="tag"&gt;UI/UX&lt;/span&gt;
                &lt;span class="tag"&gt;Typography&lt;/span&gt;
            &lt;/div&gt;
            
            &lt;h1&gt;克制的艺术：解构 Claude 的设计语言&lt;/h1&gt;
            &lt;p style="font-size: 1.4rem; color: var(--text-secondary); line-height: 1.5; margin-bottom: 32px;"&gt;
                为什么在追求“赛博朋克”与“极简黑白”的 AI 浪潮中，我们选择回归纸张、油墨与衬线体？
            &lt;/p&gt;

            &lt;div class="article-meta"&gt;
                &lt;div class="avatar"&gt;D&lt;/div&gt;
                &lt;div class="meta-info"&gt;
                    &lt;span class="author-name"&gt;Design Team&lt;/span&gt;
                    &lt;span&gt;Feb 14, 2026 · 12 min read&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/header&gt;

        &lt;!-- 正文内容 --&gt;
        &lt;article&gt;
            &lt;p&gt;
                当用户打开一个 AI 工具时，他们通常期待看到的是未来的样子：深邃的黑色背景、霓虹蓝的渐变、无处不在的磨砂玻璃效果。然而，Claude 走了一条截然不同的路。我们称之为&lt;strong&gt;“数字人文主义”&lt;/strong&gt;。
            &lt;/p&gt;
            &lt;p&gt;
                我们的设计哲学建立在三个核心支柱之上：&lt;strong&gt;Warmth (温润)&lt;/strong&gt;, &lt;strong&gt;Academia (学术)&lt;/strong&gt;, 以及 &lt;strong&gt;Reliability (可靠)&lt;/strong&gt;。请点击右上角的按钮，体验我们在不同光照条件下的设计一致性。
            &lt;/p&gt;

            &lt;!-- 章节 1：色彩 --&gt;
            &lt;h2&gt;1. 纸张的隐喻 (The Paper Metaphor)&lt;/h2&gt;
            &lt;p&gt;
                屏幕并非白纸，但在 Claude 中，我们试图弥合这种感官差异。在色彩选择上，我们拒绝了纯白（#FFFFFF）。由于显示器背光的原因，纯白在长时间阅读时会刺痛双眼。
            &lt;/p&gt;
            &lt;p&gt;
                我们选择了 &lt;code&gt;#FAF9F6&lt;/code&gt;。这是一种极其微妙的“羊皮纸白”。配合深炭灰（Charcoal, &lt;code&gt;#1C1C16&lt;/code&gt;）文字，营造了“实体书”的错觉。而在暗色模式下，我们并非简单反转颜色，而是使用了&lt;strong&gt;“Warm Dark”&lt;/strong&gt;——一种深褐灰色，避免了纯黑像素带来的OLED涂抹感。
            &lt;/p&gt;

            &lt;div class="color-grid"&gt;
                &lt;div class="color-card"&gt;
                    &lt;div class="swatch" style="background:#FAF9F6"&gt;&lt;/div&gt;
                    &lt;div class="color-meta"&gt;Paper&lt;br&gt;#FAF9F6&lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="color-card"&gt;
                    &lt;div class="swatch" style="background:#D97757"&gt;&lt;/div&gt;
                    &lt;div class="color-meta"&gt;Terracotta&lt;br&gt;#D97757&lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="color-card"&gt;
                    &lt;div class="swatch" style="background:#1C1C16"&gt;&lt;/div&gt;
                    &lt;div class="color-meta"&gt;Ink&lt;br&gt;#1C1C16&lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="color-card"&gt;
                    &lt;div class="swatch" style="background:#F2F0E8"&gt;&lt;/div&gt;
                    &lt;div class="color-meta"&gt;Subtle&lt;br&gt;#F2F0E8&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;!-- 章节 2：排版 --&gt;
            &lt;h2&gt;2. 衬线体的回归 (Serif for Authority)&lt;/h2&gt;
            &lt;p&gt;
                在 SaaS 产品全面拥抱无衬线体（Sans-serif）的今天，Claude 大胆地在标题和品牌标识中使用了衬线体（Serif）。
            &lt;/p&gt;
            &lt;blockquote&gt;
                "Typography is the voice of the text. Serifs whisper history, authority, and elegance."
            &lt;/blockquote&gt;
            &lt;p&gt;
                这种字体选择（如 &lt;em&gt;Tiempos&lt;/em&gt; 或 &lt;em&gt;Source Serif&lt;/em&gt;）潜意识地向用户传递一种信号：&lt;strong&gt;这里的信息是经过深思熟虑的、严谨的。&lt;/strong&gt;
            &lt;/p&gt;

            &lt;!-- 章节 3：工件与代码 --&gt;
            &lt;h2&gt;3. 信息容器 (The Artifact)&lt;/h2&gt;
            &lt;p&gt;
                Claude 的另一个标志性设计是“Artifact”（工件）。代码、文档或 SVG 图像不会仅仅作为文本流的一部分出现，而是被封装在一个具有窗体感的容器中。这赋予了生成内容一种&lt;strong&gt;“交付物”&lt;/strong&gt;的实体感。
            &lt;/p&gt;

            &lt;!-- 复用 Claude 风格的代码块 --&gt;
            &lt;div class="artifact"&gt;
                &lt;div class="artifact-header"&gt;
                    &lt;span&gt;theme_config.js&lt;/span&gt;
                    &lt;span&gt;JavaScript&lt;/span&gt;
                &lt;/div&gt;
                &lt;div class="artifact-body"&gt;
&lt;pre&gt;&lt;span style="color:var(--accent-primary)"&gt;const&lt;/span&gt; claudeTheme = {
  colors: {
    paper: &lt;span style="color:#4a6ee0"&gt;'#FAF9F6'&lt;/span&gt;,  &lt;span style="color:var(--text-tertiary)"&gt;// Warm White&lt;/span&gt;
    ink: &lt;span style="color:#4a6ee0"&gt;'#1C1C16'&lt;/span&gt;,    &lt;span style="color:var(--text-tertiary)"&gt;// Soft Black&lt;/span&gt;
    terracotta: &lt;span style="color:#4a6ee0"&gt;'#D97757'&lt;/span&gt;
  },
  darkMode: {
    background: &lt;span style="color:#4a6ee0"&gt;'#191918'&lt;/span&gt;, &lt;span style="color:var(--text-tertiary)"&gt;// Warm Dark Grey&lt;/span&gt;
    text: &lt;span style="color:#4a6ee0"&gt;'#ECEBE6'&lt;/span&gt;
  }
};&lt;/pre&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;!-- 新增章节 4：图标 --&gt;
            &lt;h2&gt;4. 符号学与图标 (Semiotics &amp; Iconography)&lt;/h2&gt;
            &lt;p&gt;
                Claude 的图标设计极其克制。我们不使用填充图标（Solid Icons），而是使用细线条的描边图标（Stroke Icons）。这种线条通常是 1.5px 或 2px 宽，末端圆润。
            &lt;/p&gt;
            &lt;p&gt;
                这种“线框”风格减少了视觉噪音，让图标成为文字的辅助，而不是主角。即使是 Logo 中的“C”，也是一个极简的几何图形，包裹在标志性的陶土色方块中。
            &lt;/p&gt;

            &lt;!-- 新增章节 5：动态交互 --&gt;
            &lt;h2&gt;5. 思考的动态 (Kinetics of Thought)&lt;/h2&gt;
            &lt;p&gt;
                与其使用旋转的加载圈（Spinner），Claude 在生成内容时使用了一个闪烁的黑色方块光标。这模仿了打字机或早期终端的光标，暗示着 AI 正在&lt;strong&gt;“实时撰写”&lt;/strong&gt;，而不是简单地检索数据。
            &lt;/p&gt;
            &lt;div style="background:var(--bg-subtle); padding:20px; border-radius:8px; font-family:var(--font-mono); margin-bottom:2em;"&gt;
                Thinking Process: Analyzing design patterns...&lt;span class="cursor-blink"&gt;&lt;/span&gt;
            &lt;/div&gt;

            &lt;p&gt;
                当 Artifact 出现时，它不是弹出来的，而是平滑地展开。这种微交互（Micro-interaction）的速度被调整得比常规 UI 稍慢（约 300ms-400ms），增加了操作的“分量感”。
            &lt;/p&gt;

            &lt;h2&gt;6. 陶土色的隐喻&lt;/h2&gt;
            &lt;p&gt;
                我们的品牌色是一种温暖的&lt;strong&gt;陶土橘 (Terracotta)&lt;/strong&gt;。它取自泥土与陶瓷，代表着人类文明的起源与工具的诞生。
            &lt;/p&gt;
            &lt;p&gt;
                陶土色是人性化的，它温暖、踏实，提醒用户：虽然你在与 AI 交互，但这依然是为人类服务的工具。请注意，在暗色模式下，我们会降低这个颜色的饱和度，以防止在深色背景上产生光晕。
            &lt;/p&gt;

            &lt;div style="margin-top: 60px; padding-top: 32px; border-top: 1px solid var(--border-medium); font-size: 0.9rem; color: var(--text-secondary); display:flex; justify-content:space-between;"&gt;
                &lt;span&gt;Designed with restraint by Anthropic.&lt;/span&gt;
                &lt;span&gt;© 2026 Claude Design System&lt;/span&gt;
            &lt;/div&gt;
        &lt;/article&gt;

    &lt;/div&gt;

    &lt;!-- JavaScript 逻辑：处理主题切换 --&gt;
    &lt;script&gt;
        const themeBtn = document.getElementById('themeBtn');
        const body = document.body;
        
        // 简单的 SVG 图标字符串
        const sunIcon = `&lt;svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;circle cx="12" cy="12" r="5"&gt;&lt;/circle&gt;&lt;line x1="12" y1="1" x2="12" y2="3"&gt;&lt;/line&gt;&lt;line x1="12" y1="21" x2="12" y2="23"&gt;&lt;/line&gt;&lt;line x1="4.22" y1="4.22" x2="5.64" y2="5.64"&gt;&lt;/line&gt;&lt;line x1="18.36" y1="18.36" x2="19.78" y2="19.78"&gt;&lt;/line&gt;&lt;line x1="1" y1="12" x2="3" y2="12"&gt;&lt;/line&gt;&lt;line x1="21" y1="12" x2="23" y2="12"&gt;&lt;/line&gt;&lt;line x1="4.22" y1="19.78" x2="5.64" y2="18.36"&gt;&lt;/line&gt;&lt;line x1="18.36" y1="5.64" x2="19.78" y2="4.22"&gt;&lt;/line&gt;&lt;/svg&gt;`;
        
        const moonIcon = `&lt;svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"&gt;&lt;/path&gt;&lt;/svg&gt;`;

        let isDark = false;

        themeBtn.innerHTML = moonIcon; // 初始为月亮，点击变暗

        themeBtn.addEventListener('click', () =&gt; {
            isDark = !isDark;
            if (isDark) {
                body.classList.add('dark-mode');
                themeBtn.innerHTML = sunIcon;
                themeBtn.style.color = '#fff';
            } else {
                body.classList.remove('dark-mode');
                themeBtn.innerHTML = moonIcon;
                themeBtn.style.color = 'var(--text-secondary)';
            }
        });
    &lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
]]></content>
    <author><name>黎明</name></author>
    <category term="Frontend"/>
  </entry>
</feed>
