[{"title":"CI/CD 与 GitHub Actions 自动化部署实战","permalink":"/posts/cicd%E4%B8%8Egithub-actions%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E5%AE%9E%E6%88%98/","summary":"CI/CD（持续集成与持续交付/部署）是一种现代软件工程方法论，通过在代码仓库中自动化执行构建、测试与部署流水线，实现从代码提交到线上生产环境的无人值守闭环。\n1. CI/CD 简介 在传统的网站与前后端项目运维中，最普遍的人工操作流程往往是：本地编写代码 → 本地执行编译构建打包 → 打开 FTP/SFTP 传输软件（如 FileZilla、WinSCP 或服务器文件管理器） → 拖拽上传并覆盖服务器目标目录。\n这一原始模式存在三个显著工程痛点：\n操作机械繁琐：每次细微的代码更新都要手动重复“构建-传输-覆盖”流程，严重打断研发心流。 环境配置耦合：本地开发环境的私有配置（如本地调试域名或测试环境变量）容易在打包时误带入生产，污染线上运行环境。 残留冗余废弃文件：手动文件覆盖仅处理“新增”和“替换”，一旦在本地删除了旧文件或重构了路由目录，服务器目录中残存的废弃历史文件无法自动清理。 CI/CD 正是为了将这一繁琐、易出错的人工运维流程彻底解脱为无人值守的自动化闭环。\n1.1 什么是 CI 与 CD CI/CD 并非单一软件，而是一种敏捷软件工程实践方法，通常由两个紧密衔接的阶段组成：\n阶段 英文全称 核心职责 项目映射举例 CI Continuous Integration\n（持续集成） 代码频繁合并至主干，并在云端隔离环境自动执行语法检查、依赖安装、单元测试与编译打包，尽早暴露冲突与语法缺陷。 代码推送到主分支后，云端自动拉取代码并运行构建命令进行编译与产物完整性验证。 CD Continuous Delivery\n（持续交付） 产物构建测试通过后，自动打包为可随时发布的标准交付包（Release/Artifact），等待人工确认后上线。 编译产物打包存入云端交付物仓库待命，由运维或负责人点击确认发布。 CD Continuous Deployment\n（持续部署） 无任何人工干预，只要前面的集成验证通过，新版本立即自动推向真实的生产服务器，实时生效。 编译产物无需人工点击，直接由云端通过 SSH/rsync 增量覆盖生产服务器的部署目录。 2. GitHub Actions 核心概念 GitHub Actions 是 GitHub 官方集成的 CI/CD 自动化平台。它免去了自建 Jenkins 调度节点的高昂维护成本，直接在代码托管仓库内以 YAML 声明式语法编排流水线。\n2.1 核心组成要素 GitHub Actions 的运行逻辑基于五个核心抽象概念：\n概念/组件 对应配置语法 描述与核心职责 Workflow name / 文件名 工作流，存放在 .github/workflows 目录下，定义一次完整的自动化任务集合。 Event on 触发器，定义何时激活工作流（如 push、pull_request 或手动 workflow_dispatch）。 Jobs jobs 任务单元，由一个或多个 Job 组成。不同 Job 默认并行执行，也可声明依赖串行运行。 Runner runs-on 运行容器/虚拟机，平台动态分配的纯净环境（如 ubuntu-latest），任务完成后自动销毁。 Steps / Action steps / uses 步骤与动作，Job 内部由多个 Step 顺序执行，可直接复用开源社区沉淀的 Action 模块。 ","content":"CI/CD（持续集成与持续交付/部署）是一种现代软件工程方法论，通过在代码仓库中自动化执行构建、测试与部署流水线，实现从代码提交到线上生产环境的无人值守闭环。\n1. CI/CD 简介 在传统的网站与前后端项目运维中，最普遍的人工操作流程往往是：本地编写代码 → 本地执行编译构建打包 → 打开 FTP/SFTP 传输软件（如 FileZilla、WinSCP 或服务器文件管理器） → 拖拽上传并覆盖服务器目标目录。\n这一原始模式存在三个显著工程痛点：\n操作机械繁琐：每次细微的代码更新都要手动重复“构建-传输-覆盖”流程，严重打断研发心流。 环境配置耦合：本地开发环境的私有配置（如本地调试域名或测试环境变量）容易在打包时误带入生产，污染线上运行环境。 残留冗余废弃文件：手动文件覆盖仅处理“新增”和“替换”，一旦在本地删除了旧文件或重构了路由目录，服务器目录中残存的废弃历史文件无法自动清理。 CI/CD 正是为了将这一繁琐、易出错的人工运维流程彻底解脱为无人值守的自动化闭环。\n1.1 什么是 CI 与 CD CI/CD 并非单一软件，而是一种敏捷软件工程实践方法，通常由两个紧密衔接的阶段组成：\n阶段 英文全称 核心职责 项目映射举例 CI Continuous Integration\n（持续集成） 代码频繁合并至主干，并在云端隔离环境自动执行语法检查、依赖安装、单元测试与编译打包，尽早暴露冲突与语法缺陷。 代码推送到主分支后，云端自动拉取代码并运行构建命令进行编译与产物完整性验证。 CD Continuous Delivery\n（持续交付） 产物构建测试通过后，自动打包为可随时发布的标准交付包（Release/Artifact），等待人工确认后上线。 编译产物打包存入云端交付物仓库待命，由运维或负责人点击确认发布。 CD Continuous Deployment\n（持续部署） 无任何人工干预，只要前面的集成验证通过，新版本立即自动推向真实的生产服务器，实时生效。 编译产物无需人工点击，直接由云端通过 SSH/rsync 增量覆盖生产服务器的部署目录。 2. GitHub Actions 核心概念 GitHub Actions 是 GitHub 官方集成的 CI/CD 自动化平台。它免去了自建 Jenkins 调度节点的高昂维护成本，直接在代码托管仓库内以 YAML 声明式语法编排流水线。\n2.1 核心组成要素 GitHub Actions 的运行逻辑基于五个核心抽象概念：\n概念/组件 对应配置语法 描述与核心职责 Workflow name / 文件名 工作流，存放在 .github/workflows 目录下，定义一次完整的自动化任务集合。 Event on 触发器，定义何时激活工作流（如 push、pull_request 或手动 workflow_dispatch）。 Jobs jobs 任务单元，由一个或多个 Job 组成。不同 Job 默认并行执行，也可声明依赖串行运行。 Runner runs-on 运行容器/虚拟机，平台动态分配的纯净环境（如 ubuntu-latest），任务完成后自动销毁。 Steps / Action steps / uses 步骤与动作，Job 内部由多个 Step 顺序执行，可直接复用开源社区沉淀的 Action 模块。 3. SSH 非对称密钥认证机制 自动化部署中最关键的安全环节是：云端的临时虚拟机（Runner）如何安全无感地登录私有云服务器？\n行业标准做法是彻底弃用账号密码交互，采用基于 SSH 的非对称加密认证。\n3.1 公钥与私钥的本质映射 很多初学者容易混淆两者的职责，最直观的物理比喻为：\n公钥 (Public Key)：对应门上的定制锁。可以公开给外界，任何人拿到它只能用来“关门上锁”或“出题验证”，无法用来开锁。在服务器端保存在入站授权白名单文件 ~/.ssh/authorized_keys 中。 私钥 (Private Key)：对应唯一的物理钥匙。必须严格保密，仅钥匙持有者能解密出题方的挑战数据并完成身份验真。 sequenceDiagram autonumber participant 客户端 as 客户端 / GitHub Actions participant 服务器 as 私有云服务器 (Linux) 客户端-\u0026gt;\u0026gt;服务器: 发起连接请求（声明身份） Note over 服务器: 检索 authorized_keys 列表\u0026lt;br/\u0026gt;匹配预先登记的【公钥】 服务器-\u0026gt;\u0026gt;服务器: 生成随机挑战字符串，使用【公钥】加密 服务器-\u0026gt;\u0026gt;客户端: 发送加密后的挑战密文（出题） Note over 客户端: 调取本地【私钥】解密密文 客户端-\u0026gt;\u0026gt;服务器: 回传解密后的原始明文（答题） Note over 服务器: 校验明文一致性 服务器--\u0026gt;\u0026gt;客户端: 验证通过，建立安全通道并执行部署 sequenceDiagram autonumber participant 客户端 as 客户端 / GitHub Actions participant 服务器 as 私有云服务器 (Linux) 客户端-\u0026gt;\u0026gt;服务器: 发起连接请求（声明身份） Note over 服务器: 检索 authorized_keys 列表\u0026lt;br/\u0026gt;匹配预先登记的【公钥】 服务器-\u0026gt;\u0026gt;服务器: 生成随机挑战字符串，使用【公钥】加密 服务器-\u0026gt;\u0026gt;客户端: 发送加密后的挑战密文（出题） Note over 客户端: 调取本地【私钥】解密密文 客户端-\u0026gt;\u0026gt;服务器: 回传解密后的原始明文（答题） Note over 服务器: 校验明文一致性 服务器--\u0026gt;\u0026gt;客户端: 验证通过，建立安全通道并执行部署 Important 安全核心：在整个握手流程中，私钥从未在网络中传输。即使网络流量被全程监听截获，缺乏本地私钥的攻击者也无法破解挑战密文。\n3.2 身份委托机制 在实际操作中，不少人会有疑问：“明明是 GitHub Actions 虚拟机连接服务器，为什么服务器上挂的却是本地电脑生成的公钥？”\n原因在于底层只认数学凭据，不认硬件实体：\n钥匙工坊 (Local PC)：本地电脑作为配钥匙的源头，生成非对称密钥对（私钥与公钥）。 注册信任 (Remote Server)：将公钥部署并追加至服务器大门的授权名单（~/.ssh/authorized_keys）。 凭据托管 (GitHub Secrets)：GitHub Actions 虚拟机属于一次性临时容器，自身无固定身份，因此将本地私钥的安全副本存入代码仓库的加密凭据库（Encrypted Secrets）。 代持验真 (Action Runner)：GitHub Actions 每次执行流水线时，动态载入该私钥作为“委托授权信物”向目标服务器发起握手认证。 3.3 授权密钥 vs 密钥信息 在现代服务器管理（包括各类 Linux 运维面板或系统管理工具）中，SSH 密钥配置通常分为两类，极易混淆：\n名称 底层映射文件 核心职责 业务场景 授权密钥\n(Authorized Keys) ~/.ssh/authorized_keys 谁能登我\n（门上的锁 / 入站白名单） 外部客户端连接本机。必须将部署端对应的公钥填入此处。 密钥信息\n(Key Information) ~/.ssh/id_ed25519 等私钥文件 我去登谁\n（本机的钥匙包 / 出站凭证） 本机作为客户端连接外部，或通过管理终端登录本机。 4. 自动化部署流水线配置实战 一个通用的自动化部署工作流由目标服务器前置依赖、云端 Secrets 凭据库以及代码仓库中的声明式配置文件共同构成。\n4.1 服务器前置依赖安装 部署动作通常依赖目标服务器安装有 rsync 增量同步工具。若服务器属于精简版最小安装系统，需预先安装：\n(1) 系统包管理器安装 Ubuntu / Debian：\n1sudo apt update 2sudo apt install -y rsync CentOS / RHEL：\n1sudo yum install -y rsync (2) 版本验证 1rsync --version 4.2 凭据配置（GitHub Secrets） 在 GitHub 仓库中进入 Settings → Secrets and variables → Actions，新增以下 5 项加密凭据：\n变量名称 含义说明 示例值 SERVER_HOST 服务器公网 IP 地址或域名 192.0.2.1 SERVER_PORT 服务器 SSH 真实监听端口 22（或自定义端口如 624） SERVER_USER 服务器登录用户名 root（或具备部署目录写权限的用户） SERVER_TARGET_DIR 部署产物在服务器上的绝对根路径 /var/www/html/ SERVER_SSH_KEY 用于免密鉴权的 SSH 私钥明文 \u0026mdash;\u0026ndash;BEGIN OPENSSH PRIVATE KEY\u0026mdash;\u0026ndash; \u0026hellip; 4.3 工作流定义 工作流配置文件默认存放路径：.github/workflows/deploy.yml\n1name: Deploy Application to Server # 工作流名称定义 2 3on: 4 push: 5 branches: 6 - main # 监听 main 分支推送事件 7 workflow_dispatch: # 允许在 GitHub 控制台手动触发运行 8 9jobs: 10 deploy: 11 runs-on: ubuntu-latest # 使用 GitHub 托管的最新 Ubuntu 临时容器 12 13 steps: 14 - name: Checkout Repository # 步骤 1：检出代码仓库 15 uses: actions/checkout@v4 # 官方代码检出动作 16 with: 17 submodules: true # 递归拉取 Git 子模块（若存在依赖子模块） 18 fetch-depth: 0 # 拉取完整版本历史，确保提交时间戳等元数据准确 19 20 - name: Setup Build Environment # 步骤 2：配置语言与运行环境（以 Node.js 为例） 21 uses: actions/setup-node@v4 # 运行环境安装动作 22 with: 23 node-version: 20 # 指定构建环境运行版本 24 cache: \u0026#39;npm\u0026#39; # 开启包依赖缓存，提升后续流水线构建速度 25 26 - name: Install Dependencies and Build # 步骤 3：安装依赖并执行编译构建 27 run: | 28 npm ci # 严格根据 lock 文件安装纯净依赖 29 npm run build # 执行生产编译打包，输出静态产物 30 31 - name: Deploy to Server via rsync # 步骤 4：通过 rsync 增量同步至服务器 32 uses: easingthemes/ssh-deploy@main # 基于 SSH 的通用部署动作 33 with: 34 SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} # 部署免密私钥明文 35 ARGS: \u0026#34;-rlgoDzvc -i --delete\u0026#34; # rsync 参数：递归/软链/属组/属主/设备/压缩/校验和比对/差量清理旧文件 36 SOURCE: \u0026#34;dist/\u0026#34; # 源产物构建目录（末尾加斜杠平铺传输内部文件） 37 REMOTE_HOST: ${{ secrets.SERVER_HOST }} # 目标服务器 IP 或域名 38 REMOTE_USER: ${{ secrets.SERVER_USER }} # 目标服务器用户名 39 REMOTE_PORT: ${{ secrets.SERVER_PORT || \u0026#39;22\u0026#39; }} # 目标服务器 SSH 端口 40 TARGET: ${{ secrets.SERVER_TARGET_DIR }} # 目标服务器部署目录绝对路径 5. 高频踩坑与排查指南 在真实的服务器网络与权限环境下，自动化流水线首次部署极易遭遇网络层或认证层的阻断。以下归纳三大高频故障特征与定位手段：\n5.1 故障一：SSH 连接超时（Connection timed out） 1ssh: connect to host 192.0.2.1 port 22: Connection timed out 2rsync: connection unexpectedly closed (0 bytes received so far) [sender] 3rsync error: unexplained error (code 255) at io.c(232) [sender=3.2.7] 根本原因：TCP 三次握手未能建立，网络流量在进入 Linux 操作系统前被边界防火墙或安全组丢弃。 排查路径： SSH 监听端口真实性：现代云服务器或安装了运维面板的主机通常会修改默认 22 端口（如自定义为非标准高位端口）。若 Secret 变量中端口不匹配则必然触发连接超时。 云服务商安全组入方向拦截：阿里云、腾讯云、AWS 等控制台的安全组规则中，若 SSH 端口仅授权了个人电脑的固定白名单 IP，来自 GitHub Actions 云端动态节点的流量会被直接丢弃。 解决方案与安全释疑： 放行端口规则：在云控制台安全组中，将该 SSH 端口的入方向授权来源配置为 0.0.0.0/0（全网开放）。 生产安全性评估：在“已彻底关闭密码认证”、“仅允许 Ed25519 高强度密钥鉴权”且“使用非 22 自定义端口”的前提下，全网开放端口不存在暴力字典破解风险，符合行业安全准则。 5.2 故障二：权限拒绝（Permission denied (publickey)） 1Warning: Permanently added \u0026#39;[192.0.2.1]:624\u0026#39; (ED25519) to the list of known hosts. 2root@192.0.2.1: Permission denied (publickey). 3rsync: connection unexpectedly closed (0 bytes received so far) [sender] 根本原因：网络连通建立成功，但在基于公私钥的身份鉴权阶段被服务器端 sshd 服务拒绝。 排查路径： 公私钥指纹不匹配 (Fingerprint Mismatch)： 通过在工作流中临时插入 ssh -v 探针，打印 GitHub Actions 实际提供的公钥 SHA256 指纹： 1debug1: Offering public key: /home/runner/.ssh/deploy_key ED25519 SHA256:A3SnD59... 比对服务器授权白名单中的公钥指纹。若两者指纹不一致，说明 GitHub Secrets 里填写的私钥与服务器上的公钥并非同源配对产物。 登录用户名错配 (User Mismatch)： 若服务器授权密钥配置在指定用户（如 root 或 deploy）下，而 GitHub Secret 中的 SERVER_USER 误填了其他账号或 GitHub 仓库用户名，sshd 会检索该错误用户家目录下的授权文件，导致拒绝访问。 Ed25519 私钥末尾换行符丢失 (Missing Trailing Newline)： 在浏览器网页文本框中复制粘贴私钥时，末尾的换行符（\\n）极易被表单自动裁剪。缺少尾部换行的 Ed25519 私钥会导致 OpenSSH 解析载入失败。\n推荐在本地终端直接通过管道导出到系统剪贴板： Windows PowerShell： 1Get-Content $HOME\\.ssh\\id_ed25519 -Raw | Set-Clipboard Linux / macOS： 1cat ~/.ssh/id_ed25519 | pbcopy # macOS 2xclip -sel clip \u0026lt; ~/.ssh/id_ed25519 # Linux Linux 文件权限合规检查 (StrictModes)： Linux OpenSSH 默认开启严格模式（StrictModes yes）。若服务器上的 .ssh 目录或授权文件权限过于宽松（如 777 或同组可写），sshd 会为了规避中间人篡改风险而直接忽略该授权文件。\n合规权限基线： 1chmod 700 ~/.ssh 2chmod 600 ~/.ssh/authorized_keys 5.3 故障三：Action 依赖版本无法解析（Unable to resolve action version） 1Error: Unable to resolve action `easingthemes/ssh-deploy@v5`, unable to find version `v5` 根本原因：部分开源 Action 维护者未发布宽泛的大版本浮动标签（Floating Tag，如 @v5）。 解决方案：明确指定具体的语义化固定版本号（如 @v5.1.0）或根据官方文档直接对齐主干分支（如 @main）。 6. 总结 这套基于 GitHub Actions 与 SSH 密钥的自动化部署体系核心聚焦于两项工程价值：\n云端构建解耦 (CI)：通过纯净容器接管依赖拉取与编译打包，彻底摆脱本地环境配置与本地缓存干扰，产出标准化的一致性生产产物。 安全增量同步 (CD)：借助受保护的非对称密钥凭据穿透安全边界，通过 rsync 增量同步与 \u0026ndash;delete 差量清理，彻底达成“代码一键推送到主分支，线上秒级无感更新”的无人值守研发运维体验。 ","date":"2026-09-15","tags":["CI/CD","GitHub Actions"]},{"title":"Nginx配置防盗链","permalink":"/posts/nginx%E9%85%8D%E7%BD%AE%E9%98%B2%E7%9B%97%E9%93%BE/","summary":"我们在日常浏览网页时，偶尔会遇到图片裂开的情况；若试着在新标签页中单独打开该图片的链接，浏览器可能会弹出一个 403 Forbidden（禁止访问） 的错误提示。这通常意味着：当前网页并未在自己的服务器上托管这张图片，而是直接在外链中引用了其他站点的资源。被引用的源站为了保护自身的服务器资源与出口带宽，通过 Nginx 识别出了此类未经授权的外部请求，并主动返回了 403 拒绝访问。\n这种用来防范外部网站直接引用本站静态资源的保护机制，就是我们常说的 Nginx 防盗链（Anti-Hotlinking）。\n1. 原理浅析：HTTP Referer 浏览器在加载网页内的图片、音视频等资源时，遵循 HTTP 协议规范，通常会在请求头中自动附带一个 Referer 字段，用于告知服务端“这个资源请求是从哪个网页发起的”。\n例如，当用户在第三方网站 https://other-site.com 浏览时，页面加载了本站的图片，浏览器向本站发出的请求大致如下：\n1GET /logo.png HTTP/1.1 2Host: your-domain.com 3Referer: https://other-site.com/page.html Nginx 正是通过内置的 ngx_http_referer_module 模块，提取请求头中的 Referer 并与我们设定的白名单进行比对，从而判断请求来源是否合规。\n不过，Referer 可能因为浏览器隐私策略、Referrer-Policy、代理或浏览器扩展而缺失，也可以被客户端伪造。因此，Referer 防盗链适合做粗粒度限制，不应作为身份认证或强安全边界。\n注意事项：不要把 Referer 白名单当成搜索引擎认证。因为搜索引擎抓取图片时不一定会携带 Referer。本例保留 none，已经允许无来源头的正常请求；如果确实遇到特定爬虫无法访问，应先结合实际请求日志排查，再按业务需要增加白名单。仅凭 Referer 域名无法可靠证明请求来自搜索引擎，也不能单独保证 SEO 效果。 2. 解决方案 为了兼顾系统安全与业务体验，针对未经许可的外部引用，Nginx 通常有两种主流的配置策略：\n2.1 直接返回 403 状态码（更轻量、最省带宽） 发现请求来源不合规时立即切断响应，不给对方传输多余数据，这是大多数生产环境的首选方案。\n1# 图片与多媒体资源防盗链配置 2location ~* \\.(gif|jpg|jpeg|png|bmp|webp|avif|svg|mp4|mov)$ { 3 # 仅对 URL 带版本指纹的静态资源使用 immutable 4 # 使用单个 Cache-Control 头，避免与 expires 重复设置缓存策略 5 expires off; 6 add_header Cache-Control \u0026#34;public, max-age=2592000, immutable\u0026#34;; 7 8 # 流量很大时可关闭；如需审计或排查盗链，请保留日志或改用条件日志 9 access_log off; 10 11 # 合法 Referer 白名单配置： 12 # - none: 允许直接在浏览器地址栏打开或从书签访问（无 Referer） 13 # - server_names: 自动匹配本 server 声明的所有合法域名 14 # - *.example.com: 放行 example.com 下的子域名 15 valid_referers none server_names *.example.com; 16 17 # 非法来源直接返回 403 阻断 18 if ($invalid_referer) { 19 return 403; 20 } 21} 核心参数说明：\n","content":"我们在日常浏览网页时，偶尔会遇到图片裂开的情况；若试着在新标签页中单独打开该图片的链接，浏览器可能会弹出一个 403 Forbidden（禁止访问） 的错误提示。这通常意味着：当前网页并未在自己的服务器上托管这张图片，而是直接在外链中引用了其他站点的资源。被引用的源站为了保护自身的服务器资源与出口带宽，通过 Nginx 识别出了此类未经授权的外部请求，并主动返回了 403 拒绝访问。\n这种用来防范外部网站直接引用本站静态资源的保护机制，就是我们常说的 Nginx 防盗链（Anti-Hotlinking）。\n1. 原理浅析：HTTP Referer 浏览器在加载网页内的图片、音视频等资源时，遵循 HTTP 协议规范，通常会在请求头中自动附带一个 Referer 字段，用于告知服务端“这个资源请求是从哪个网页发起的”。\n例如，当用户在第三方网站 https://other-site.com 浏览时，页面加载了本站的图片，浏览器向本站发出的请求大致如下：\n1GET /logo.png HTTP/1.1 2Host: your-domain.com 3Referer: https://other-site.com/page.html Nginx 正是通过内置的 ngx_http_referer_module 模块，提取请求头中的 Referer 并与我们设定的白名单进行比对，从而判断请求来源是否合规。\n不过，Referer 可能因为浏览器隐私策略、Referrer-Policy、代理或浏览器扩展而缺失，也可以被客户端伪造。因此，Referer 防盗链适合做粗粒度限制，不应作为身份认证或强安全边界。\n注意事项：不要把 Referer 白名单当成搜索引擎认证。因为搜索引擎抓取图片时不一定会携带 Referer。本例保留 none，已经允许无来源头的正常请求；如果确实遇到特定爬虫无法访问，应先结合实际请求日志排查，再按业务需要增加白名单。仅凭 Referer 域名无法可靠证明请求来自搜索引擎，也不能单独保证 SEO 效果。 2. 解决方案 为了兼顾系统安全与业务体验，针对未经许可的外部引用，Nginx 通常有两种主流的配置策略：\n2.1 直接返回 403 状态码（更轻量、最省带宽） 发现请求来源不合规时立即切断响应，不给对方传输多余数据，这是大多数生产环境的首选方案。\n1# 图片与多媒体资源防盗链配置 2location ~* \\.(gif|jpg|jpeg|png|bmp|webp|avif|svg|mp4|mov)$ { 3 # 仅对 URL 带版本指纹的静态资源使用 immutable 4 # 使用单个 Cache-Control 头，避免与 expires 重复设置缓存策略 5 expires off; 6 add_header Cache-Control \u0026#34;public, max-age=2592000, immutable\u0026#34;; 7 8 # 流量很大时可关闭；如需审计或排查盗链，请保留日志或改用条件日志 9 access_log off; 10 11 # 合法 Referer 白名单配置： 12 # - none: 允许直接在浏览器地址栏打开或从书签访问（无 Referer） 13 # - server_names: 自动匹配本 server 声明的所有合法域名 14 # - *.example.com: 放行 example.com 下的子域名 15 valid_referers none server_names *.example.com; 16 17 # 非法来源直接返回 403 阻断 18 if ($invalid_referer) { 19 return 403; 20 } 21} 核心参数说明：\nnone：匹配请求头中完全没有 Referer 的情况。比如访客在浏览器地址栏直接粘贴图片链接打开、或从书签访问时，都不会携带 Referer。保留此项能确保正规用户的直接访问不受阻碍。 blocked：匹配存在 Referer，但值不以 http:// 或 https:// 开头的请求。这类值可能来自代理，也可能是客户端伪造；只有明确需要兼容此类代理时才建议加入。 server_names：直接复用当前 server 块中配置的域名列表，避免重复硬编码。 通配符域名：*.example.com 表示放行 example.com 下的子域名。请只填写实际需要信任的域名，避免使用过宽的通配符。 2.2 返回友好的警示占位图（内部改写） 如果业务上希望给外部引用方一个明确的视觉反馈（例如在对方页面展示一张写着“资源来自 xxx，请前往原站查看”的说明图），可以通过内部改写来实现。这样浏览器地址栏仍然保持原始图片 URL，但响应内容替换为占位图。\n1location ~* \\.(gif|jpg|jpeg|png|webp)$ { 2 # 配置合法 Referer 白名单 3 valid_referers none server_names *.example.com; 4 5 # 非法来源通过内部改写重写至占位图 6 if ($invalid_referer) { 7 # 内部改写后重新匹配 location，让下面的精确 location 接管请求 8 rewrite ^ /static/warning/anti_hotlink.png last; 9 } 10 11 # 原始图片可以按业务需要缓存；不要与占位图共用 no-store 策略 12 expires off; 13 add_header Cache-Control \u0026#34;public, max-age=2592000\u0026#34;; 14 access_log off; 15} 16 17# 警告图片单独放行，并禁止缓存，避免占位图被缓存到原始图片 URL 18location = /static/warning/anti_hotlink.png { 19 root /usr/share/nginx/html; # 静态文件根目录 20 expires off; # 禁用 expires 响应头 21 add_header Cache-Control \u0026#34;no-store\u0026#34; always; # 始终禁止客户端与代理缓存 22 access_log off; # 占位图通常关闭独立访问日志 23} 机制说明与注意事项：\n内部改写机制：这里使用的是内部改写，不会向客户端返回 3xx 重定向状态码。last 会让 Nginx 重新进行 location 匹配，因此请求会进入精确匹配的占位图规则，不会再次执行外层防盗链判断。若改成 break，请求会继续留在当前正则 location 中，下面的精确匹配规则不会被重新执行；若确实需要浏览器发生跳转，应改用 return 302 /static/warning/anti_hotlink.png，并相应使用 curl -L 验证。 缓存策略隔离：占位图必须设置 no-store，否则外部请求得到的占位图可能被浏览器或 CDN 按原始图片 URL 缓存，进而影响后续合法访问。 3. 验证与排查 3.1 语法校验与平滑重载 配置修改完成后，建议先执行语法校验并平滑重载：\n1# 检查语法是否有误 2nginx -t 3 4# 平滑重载生效（不中断现有连接） 5nginx -s reload 3.2 使用 curl 模拟请求验证 随后可以用 curl 命令快速验证策略是否如预期生效：\n1# 方案一：模拟本站正常引用（带合法 Referer，期望返回 200） 2curl -I -e \u0026#34;https://www.example.com\u0026#34; https://www.example.com/logo.png 3 4# 方案一：模拟外部未授权引用（带非法 Referer，期望返回 403） 5curl -I -e \u0026#34;https://unauthorized-domain.com\u0026#34; https://www.example.com/logo.png 6 7# 切换到方案二配置后：模拟外部未授权引用（带非法 Referer，期望返回 200，内容为占位图） 8curl -I -e \u0026#34;https://unauthorized-domain.com\u0026#34; https://www.example.com/logo.png 9 10# 方案一或方案二：模拟地址栏直接打开（无 Referer，期望返回 200） 11curl -I https://www.example.com/logo.png ","date":"2026-08-14","tags":["Nginx"]},{"title":"Nginx 快速入门","permalink":"/posts/nginx%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/","summary":" 1. Nginx 简介 在现代 Web 架构中，Nginx 往往是流量到达服务器的第一道门面。它兼具极高的吞吐能力与轻量资源消耗，常用于静态站点分发与后端服务网关接入。\n1.1 什么是 Nginx Nginx 是一款开源、高性能的 HTTP 服务器与反向代理服务器，核心架构基于单 Master 多 Worker 的异步非阻塞事件驱动模型（Linux 环境下使用 epoll）。\n其核心能力：\n静态资源直出：高效托管 HTML、CSS、JS 及各类多媒体文件，直接经由内核零拷贝响应，释放后端算力。 反向代理与流量分发：作为内部集群的前置入口，对外暴露统一端口，无感转发后端请求。 虚拟主机路由：支持基于单个端口配置多个域名（Server Name），实现单机多站点的精细隔离。 平滑热升级：内置优秀的信号处理机制，修改配置与更新二进制无需停机，实现零感知运维。 2. Nginx 安装与启动 2.1 包管理器安装 (1) 系统包管理器（推荐日常使用）\nUbuntu / Debian：\n1sudo apt update 2sudo apt install -y nginx CentOS / RHEL：\n1sudo yum install -y epel-release 2sudo yum install -y nginx macOS：\n","content":" 1. Nginx 简介 在现代 Web 架构中，Nginx 往往是流量到达服务器的第一道门面。它兼具极高的吞吐能力与轻量资源消耗，常用于静态站点分发与后端服务网关接入。\n1.1 什么是 Nginx Nginx 是一款开源、高性能的 HTTP 服务器与反向代理服务器，核心架构基于单 Master 多 Worker 的异步非阻塞事件驱动模型（Linux 环境下使用 epoll）。\n其核心能力：\n静态资源直出：高效托管 HTML、CSS、JS 及各类多媒体文件，直接经由内核零拷贝响应，释放后端算力。 反向代理与流量分发：作为内部集群的前置入口，对外暴露统一端口，无感转发后端请求。 虚拟主机路由：支持基于单个端口配置多个域名（Server Name），实现单机多站点的精细隔离。 平滑热升级：内置优秀的信号处理机制，修改配置与更新二进制无需停机，实现零感知运维。 2. Nginx 安装与启动 2.1 包管理器安装 (1) 系统包管理器（推荐日常使用）\nUbuntu / Debian：\n1sudo apt update 2sudo apt install -y nginx CentOS / RHEL：\n1sudo yum install -y epel-release 2sudo yum install -y nginx macOS：\n1brew install nginx (2) 前端/Node.js 包管理器（npm）\n适合前端本地快速模拟生产路由与接口代理，无需安装系统级服务：\n1npm install -g nginx 2.2 从源码安装（可自定义模块） (1) 下载与解压源码：\n1wget http://nginx.org/download/nginx-1.24.0.tar.gz 2tar -xvzf nginx-1.24.0.tar.gz 3cd nginx-1.24.0 (2) 配置与编译安装：\n1./configure 2make 3sudo make install 2.3 启动与停止服务 操作 命令 启动服务 sudo systemctl start nginx 停止服务 sudo systemctl stop nginx 重启服务 sudo systemctl restart nginx 检查状态 sudo systemctl status nginx 3. 基本命令 命令 描述 nginx -v 查看已安装版本 nginx -V 查看版本及详细编译参数模块 nginx -t 检验配置文件语法与合法性 nginx -s reload 平滑热重载配置（不中断当前连接） nginx -s stop 快速强制终止进程 nginx -s quit 优雅退出（等待请求处理完成） nginx -s reopen 重新打开日志（配合日志轮转切割） 4. 配置文件结构与解析 4.1 配置文件路径 主配置文件：/etc/nginx/nginx.conf（源码编译版通常位于 /usr/local/nginx/conf/nginx.conf） 模块化子配置：/etc/nginx/conf.d/*.conf 默认工作目录：/usr/share/nginx/html 4.2 配置文件结构 Nginx 配置按作用域分层嵌套生效：\n配置块 作用域与核心职责 main 全局顶层配置，控制运行用户、Worker 进程数、系统日志等。 events 网络连接模型控制，配置单个 Worker 的最大连接数及 epoll。 http HTTP 协议核心层，承载 MIME 类型、连接超时、Gzip 及虚拟主机。 server 虚拟主机单元，声明绑定的监听端口与域名。 location 路由规则分发，定义 URI 路径的具体匹配与响应行为。 4.3 配置文件基础示例 默认配置文件路径：/etc/nginx/nginx.conf\n1user nginx; # 运行 Worker 进程的系统用户 2worker_processes auto; # 自动根据 CPU 核心数分配 Worker 进程数 3 4events { 5 worker_connections 1024; # 单个 Worker 进程支持的最大并发连接数 6} 7 8http { 9 include /etc/nginx/mime.types; # 引入 MIME 类型映射文件 10 default_type application/octet-stream; # 默认响应类型 11 sendfile on; # 开启高效文件零拷贝传输机制 12 keepalive_timeout 65; # 长连接超时时间（秒） 13 14 include /etc/nginx/conf.d/*.conf; # 加载 conf.d 目录下的所有模块化子配置 15 16 server { 17 listen 80; # 监听 HTTP 默认 80 端口 18 server_name localhost; # 绑定的域名或主机名 19 20 location / { 21 root /usr/share/nginx/html; # 站点静态资源根目录 22 index index.html index.htm; # 默认首页文件名 23 } 24 } 25} 5. 常见实战应用 在前后端分离架构中，Nginx 最典型的职责组合是：对外托管前端页面，对内反向代理后端接口。\n5.1 反向代理 目标：将所有发往 /api/ 的接口请求，无感转发给后端的业务服务（如 Spring Boot 的 8080 端口），并保留真实的客户端来源信息。\n1server { 2 listen 80; 3 server_name localhost; 4 5 location /api/ { 6 proxy_pass http://127.0.0.1:8080/; # 转发至后端内网端口 7 proxy_set_header Host $host; # 透传客户端原始请求域名 8 proxy_set_header X-Real-IP $remote_addr; # 透传客户端直连 IP 9 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 } 11} 核心说明：\nproxy_pass 尾部斜杠差异： 带 /（如 http://127.0.0.1:8080/）：Nginx 会去除匹配到的 /api/ 前缀再转发。 不带 /（如 http://127.0.0.1:8080）：完整保留 /api/\u0026hellip; 原路径转发。 透传请求头必要性：反向代理重新发起了 TCP 握手，后端默认只能拿到 Nginx 的本地 IP（127.0.0.1）。必须显式配置 proxy_set_header，后端的业务审计、地理位置解析和限流防刷才能拿到真实 IP。 5.2 监听 HTTP 端口 目标：对外开放 HTTP 访问端口，并根据请求携带的 Host 域名将流量路由到对应的站点虚拟主机。\n1server { 2 listen 80; # Nginx 监听 80 端口，浏览器直接访问 http://localhost 即可命中 3 server_name localhost; # 请求的 Host 满足匹配条件时进入该 server 处理 4 5 location / { 6 root /usr/share/nginx/html; 7 index index.html; 8 } 9} 核心说明：\nlisten：指定该虚拟主机绑定的网络端口（如 80、443）或指定网卡 IP（如 127.0.0.1:8080）。 server_name：虚拟主机的域名标识。当单台机器上运行多个站点时，Nginx 会依据 HTTP 请求头中的 Host 字段与 server_name 进行匹配并分发；未匹配上的请求会默认由排在首位或带有 default_server 标记的配置处理。 5.3 静态资源托管 目标：履行 Web 服务器核心职责，直接从本地目录读取并返回网页静态资源；同时利用 try_files 保障单页应用（Vue / React SPA）在前端路由模式下刷新页面不报 404。\n1server { 2 listen 80; 3 server_name localhost; 4 5 location / { 6 root /usr/share/nginx/html; # 指定前端构建产物所在的本地目录 7 index index.html; # 默认首页入口 8 try_files $uri $uri/ /index.html; # Vue/React 路由刷新防 404 兜底 9 } 10} 核心说明：\nWeb 服务器核心职责：负责监听 HTTP 端口，接收浏览器请求并直接经由内核零拷贝响应网页静态内容。 root 根路径：指定静态资源的物理根目录，请求 URI 路径会被完整拼接到 root 目录后进行文件检索。 try_files 运行逻辑：按请求路径执行链路兜底检索：真实静态文件 ($uri) → 同名目录 ($uri/) → 兜底重定向返回 /index.html（交由前端单页路由引擎渲染，彻底杜绝 404）。 6. 总结与延伸 Nginx 凭借单 Master 多 Worker 的事件驱动架构，在现代 Web 体系中牢牢占据着第一道流量关卡的位置。\n这份基础入门配置看似简单，却覆盖了前后端分离架构中最核心的骨架场景：\n静态层：通过文件零拷贝高效直出，利用 try_files 为单页应用提供路由兜底； 代理层：屏蔽内部真实架构细节，无感将 /api 等接口流量分发到后端微服务集群； 运维层：依托信号驱动机制，让每一次 nginx -s reload 都能平滑热更新，保证线上服务永续运行。 延伸阅读：\n掌握了基础的代理与托管后，在真实的公网运营中往往还需要考虑静态资产的带宽保护。如果你正在面临资源盗刷问题，可以继续参阅进阶篇：《Nginx配置防盗链》。\n","date":"2026-08-13","tags":["Nginx"]},{"title":"Git 常用命令速查与协作工作流全指南","permalink":"/posts/git/","summary":" 1. 仓库初始化（Getting a Git Repository） 1.1 工作区 / 暂存区 / 仓库 区域 时机 存储位置 工作区（Working Directory） add 前 磁盘文件系统 暂存区（Staging Area / Index） add 后、commit 前 .git/index 本地仓库（Local Repository） commit 后 .git/objects 远程仓库（Remote Repository） push 后 远端服务器 1.2 初始化与远端配置 命令 说明 示例 git init 将当前目录初始化为 Git 仓库 git init git clone 克隆远端仓库到本地 git clone git@github.com:user/repo.git git remote add origin 为本地仓库绑定远端地址 git remote add origin git@github.com:user/repo.git git remote add upstream 关联上游原始仓库 git remote add upstream git@github.com:original/repo.git git remote set-url 修改已有远端地址 git remote set-url origin git@github.com:user/repo-v2.git git remote -v 查看所有已配置的远端 git remote -v ","content":" 1. 仓库初始化（Getting a Git Repository） 1.1 工作区 / 暂存区 / 仓库 区域 时机 存储位置 工作区（Working Directory） add 前 磁盘文件系统 暂存区（Staging Area / Index） add 后、commit 前 .git/index 本地仓库（Local Repository） commit 后 .git/objects 远程仓库（Remote Repository） push 后 远端服务器 1.2 初始化与远端配置 命令 说明 示例 git init 将当前目录初始化为 Git 仓库 git init git clone 克隆远端仓库到本地 git clone git@github.com:user/repo.git git remote add origin 为本地仓库绑定远端地址 git remote add origin git@github.com:user/repo.git git remote add upstream 关联上游原始仓库 git remote add upstream git@github.com:original/repo.git git remote set-url 修改已有远端地址 git remote set-url origin git@github.com:user/repo-v2.git git remote -v 查看所有已配置的远端 git remote -v 2. 记录变更（Recording Changes） 2.1 暂存与提交 命令 说明 示例 git status 查看工作区与暂存区状态 git status git add \u0026lt;file\u0026gt; 暂存指定文件 git add README.md git add . 暂存全部变动 git add . git diff 查看工作区未暂存的改动 git diff git diff \u0026ndash;staged 查看已暂存但未提交的改动 git diff \u0026ndash;staged git commit -m 提交并附说明 git commit -m \u0026ldquo;feat: 初始化工程结构\u0026rdquo; git commit \u0026ndash;amend 修改最近一次提交（消息或内容） git commit \u0026ndash;amend 2.2 Commit 语义化规范（Conventional Commits） Type 用途 feat 新增功能 fix 修复 Bug docs 文档变更 refactor 代码重构（无功能增减） perf 性能优化 chore 构建/依赖/工具变更 格式：\u0026lt;type\u0026gt;[scope]: \u0026lt;description\u0026gt;，示例：feat(user): 新增头像上传接口\n3. 查看历史（Viewing the Commit History） 3.1 日志与差异 命令 说明 示例 git log 查看完整提交历史 git log git log \u0026ndash;oneline 每条提交一行精简显示 git log \u0026ndash;oneline -10 git log \u0026ndash;graph 图形化展示分支合并历史 git log \u0026ndash;oneline \u0026ndash;graph \u0026ndash;all git show \u0026lt;hash\u0026gt; 查看指定提交的详细改动 git show a1b2c3d 4. 撤销操作（Undoing Things） 4.1 丢弃修改与取消暂存 命令 说明 示例 git restore \u0026lt;file\u0026gt; 丢弃工作区未暂存的改动 git restore style.css git restore . 丢弃工作区全部改动 git restore . git restore \u0026ndash;staged \u0026lt;file\u0026gt; 取消暂存（反向 add） git restore \u0026ndash;staged README.md git rm \u0026ndash;cached 移除追踪，保留本地文件 git rm -r \u0026ndash;cached target/ 4.2 版本回退（reset） 命令 说明 示例 git reset HEAD~1 mixed（默认）：撤销 commit，改动回工作区 git reset HEAD~1 git reset \u0026ndash;soft HEAD~1 soft：撤销 commit，改动保留在暂存区 git reset \u0026ndash;soft HEAD~1 git reset \u0026ndash;hard HEAD~1 hard：彻底抹除 commit + 改动（不可恢复） git reset \u0026ndash;hard HEAD~1 4.3 安全撤销与临时搁置 命令 说明 示例 git revert \u0026lt;hash\u0026gt; 生成反向提交，安全撤销不改历史 git revert a1b2c3d git stash 临时搁置工作区改动 git stash git stash pop 恢复最近一次搁置 git stash pop git cherry-pick \u0026lt;hash\u0026gt; 摘取指定提交到当前分支 git cherry-pick a1b2c3d 5. 远程操作（Working with Remotes） 5.1 推送与拉取 命令 说明 示例 git fetch 拉取远端变更，不合并 git fetch origin git pull 拉取并合并远端（fetch + merge） git pull origin main git push -u 首次推送并绑定追踪分支 git push -u origin main git push 推送已追踪的分支 git push git push \u0026ndash;force-with-lease 安全强推，避免覆盖他人提交 git push \u0026ndash;force-with-lease origin feat/x 6. 分支（Branching） 6.1 分支管理 命令 说明 示例 git branch 列出所有本地分支 git branch git branch -vv 查看本地分支与远端追踪状态 git branch -vv git switch \u0026lt;branch\u0026gt; 切换到已有分支（现代语法） git switch main git switch -c \u0026lt;branch\u0026gt; 创建并切换新分支（现代语法） git switch -c feat/user-profile git checkout -b \u0026lt;branch\u0026gt; 创建并切换新分支（经典语法） git checkout -b feat/user-profile git branch -d \u0026lt;branch\u0026gt; 删除已合并的本地分支 git branch -d feat/user-profile git branch -D \u0026lt;branch\u0026gt; 强制删除未合并分支 git branch -D feat/wip git push origin \u0026ndash;delete \u0026lt;branch\u0026gt; 删除远端分支 git push origin \u0026ndash;delete feat/user-profile git remote prune origin 清理本地残留的已删除远端分支追踪 git remote prune origin 7. 变基（Rebasing） 7.1 变基命令 命令 说明 示例 git merge \u0026ndash;no-ff \u0026lt;branch\u0026gt; 非快进合并，保留完整分支历史 git merge \u0026ndash;no-ff feat/login git rebase \u0026lt;branch\u0026gt; 将当前分支变基到目标分支 git rebase origin/main git rebase \u0026ndash;continue 冲突解决后继续变基 git rebase \u0026ndash;continue git rebase \u0026ndash;abort 放弃变基，恢复原状 git rebase \u0026ndash;abort Important 黄金法则：严禁对公共主分支执行 rebase；仅在个人未合入的特性分支上使用。\n8. GitHub 协作工作流（GitHub Workflow） 8.1 特性分支流水线 1# 1. 从最新主干派生分支 2git switch main \u0026amp;\u0026amp; git pull origin main 3git switch -c feat/user-profile 4 5# 2. 开发 → 提交 6git add . \u0026amp;\u0026amp; git commit -m \u0026#34;feat(user): 新增头像修改接口\u0026#34; 7 8# 3. 同步主干变基（有冲突则解决后 --continue） 9git fetch origin \u0026amp;\u0026amp; git rebase origin/main 10 11# 4. 推送并发起 PR 12git push -u origin feat/user-profile 13 14# 5. 审查期间补充提交 → 安全强推 15git commit -m \u0026#34;fix(user): 修复昵称长度校验\u0026#34; 16git push --force-with-lease origin feat/user-profile 17 18# 6. 合入后清理 19git switch main \u0026amp;\u0026amp; git pull origin main 20git branch -d feat/user-profile \u0026amp;\u0026amp; git remote prune origin 8.2 GitHub CLI（gh）速查 命令 说明 示例 gh auth login 账号认证 gh auth login gh pr create 发起 PR gh pr create \u0026ndash;base main \u0026ndash;head feat/x \u0026ndash;title \u0026ldquo;feat: xxx\u0026rdquo; gh pr list 列出所有 PR gh pr list gh pr view \u0026lt;id\u0026gt; \u0026ndash;web 浏览器打开 PR gh pr view 1 \u0026ndash;web gh pr checkout \u0026lt;id\u0026gt; 检出他人 PR 到本地 gh pr checkout 1 gh pr checks 查看 CI 状态 gh pr checks gh pr merge \u0026lt;id\u0026gt; \u0026ndash;squash Squash 合并 gh pr merge 1 \u0026ndash;squash 8.3 PR 描述模板 1## 概述 2- **目的**：解决了什么问题 / 新增了什么功能 3- **关联 Issue**：Closes #123 4 5## 改动内容 6- [模块名]：改动点 1 7- [模块名]：改动点 2 8 9## 验证 10- [x] 单元测试通过 11- [x] 本地联调通过 12- [ ] 边缘测试完成 13 14## 注意事项 15- **数据库**：是否需执行变更脚本 16- **Breaking Changes**：是否有不兼容调整 ","date":"2026-07-17","tags":["Git","版本控制","工程规范","GitHub CLI"]},{"title":"Docker知识点详解","permalink":"/posts/docker%E7%9F%A5%E8%AF%86%E7%82%B9%E8%AF%A6%E8%A7%A3/","summary":"Docker 是一个基于 Linux 内核能力实现的开源应用容器引擎，依托 命名空间（Namespaces） 与 控制组（cgroups） 实现轻量级进程隔离，支持将应用程序及其依赖项统一打包为标准化容器镜像，实现跨环境的敏捷交付与一致性运行。\n1. 概述与核心机制 1.1 核心特性与虚拟化差异 容器技术与传统虚拟机（VM）在底层实现上有本质区别：\n进程级轻量隔离：容器共享宿主机的操作系统内核，不需要像虚拟机那样虚拟化完整的 Guest OS 与虚拟硬件，秒级启动且系统资源开销极小。 独立的文件系统视图：容器内部拥有独立的用户态文件系统环境（例如 /bin、/etc、/usr、/app），能够独立执行 Linux 用户态程序与服务进程。 环境交付一致性：通过将代码、运行依赖与环境变量整体封装，实现“一次构建，随处运行”，消除开发、测试与生产环境不一致的痛点。 1.2 Windows 与 WSL2 协同运行模式 在 Windows 环境下，Docker Desktop 并非直接运行于 Windows 原生内核之上，而是深度集成并依赖 WSL2（Windows Subsystem for Linux 2）提供的真实 Linux 内核环境：\n架构协同关系：Windows 充当宿主操作系统；WSL2 虚拟机实例提供 Linux 内核调用支持；Docker Desktop 统一管理 Docker 守护进程引擎与图形化控制台。 最佳工程实践：建议将项目代码与挂载文件直接存放在 WSL 的 Linux 本地文件系统内（如 /home/user/\u0026hellip;），避免跨跨系统 9P 文件协议读写带来的显著性能损耗。 2. 架构与核心概念 Docker 采用标准的 C/S（客户端/服务端）架构，客户端工具与后端守护进程通过 REST API、UNIX 套接字或网络接口进行交互。\n2.1 架构角色与核心概念 角色/组件 描述与核心职责 Docker Client 命令行交互工具（docker CLI），负责向服务端分发构建、运行与管理指令。 Docker Daemon (dockerd) 常驻后台的守护进程，负责管理镜像、容器、存储卷与虚拟网络等核心对象。 镜像（Image） 包含了应用程序及其完整运行依赖的只读层叠模板，用于生成容器运行时实例。 容器（Container） 镜像的可运行实例，在镜像只读层之上附加读写层，具备隔离的网络与运行时环境。 镜像仓库（Registry） 集中存储与分发容器镜像的中心服务，分为公有仓库（如 Docker Hub）与私有仓库（如 Harbor）。 Dockerfile 声明镜像构建流程的文本配置文件，逐层定义基础环境、依赖安装与启动参数。 Docker Compose 用于定义和编排多容器协同运行的工具，通过声明式 YAML 文件管理全栈服务。 2.2 Dockerfile 与 Compose 的职责划分 两者处于不同层级的交付链路中，职责边界清晰：\n","content":"Docker 是一个基于 Linux 内核能力实现的开源应用容器引擎，依托 命名空间（Namespaces） 与 控制组（cgroups） 实现轻量级进程隔离，支持将应用程序及其依赖项统一打包为标准化容器镜像，实现跨环境的敏捷交付与一致性运行。\n1. 概述与核心机制 1.1 核心特性与虚拟化差异 容器技术与传统虚拟机（VM）在底层实现上有本质区别：\n进程级轻量隔离：容器共享宿主机的操作系统内核，不需要像虚拟机那样虚拟化完整的 Guest OS 与虚拟硬件，秒级启动且系统资源开销极小。 独立的文件系统视图：容器内部拥有独立的用户态文件系统环境（例如 /bin、/etc、/usr、/app），能够独立执行 Linux 用户态程序与服务进程。 环境交付一致性：通过将代码、运行依赖与环境变量整体封装，实现“一次构建，随处运行”，消除开发、测试与生产环境不一致的痛点。 1.2 Windows 与 WSL2 协同运行模式 在 Windows 环境下，Docker Desktop 并非直接运行于 Windows 原生内核之上，而是深度集成并依赖 WSL2（Windows Subsystem for Linux 2）提供的真实 Linux 内核环境：\n架构协同关系：Windows 充当宿主操作系统；WSL2 虚拟机实例提供 Linux 内核调用支持；Docker Desktop 统一管理 Docker 守护进程引擎与图形化控制台。 最佳工程实践：建议将项目代码与挂载文件直接存放在 WSL 的 Linux 本地文件系统内（如 /home/user/\u0026hellip;），避免跨跨系统 9P 文件协议读写带来的显著性能损耗。 2. 架构与核心概念 Docker 采用标准的 C/S（客户端/服务端）架构，客户端工具与后端守护进程通过 REST API、UNIX 套接字或网络接口进行交互。\n2.1 架构角色与核心概念 角色/组件 描述与核心职责 Docker Client 命令行交互工具（docker CLI），负责向服务端分发构建、运行与管理指令。 Docker Daemon (dockerd) 常驻后台的守护进程，负责管理镜像、容器、存储卷与虚拟网络等核心对象。 镜像（Image） 包含了应用程序及其完整运行依赖的只读层叠模板，用于生成容器运行时实例。 容器（Container） 镜像的可运行实例，在镜像只读层之上附加读写层，具备隔离的网络与运行时环境。 镜像仓库（Registry） 集中存储与分发容器镜像的中心服务，分为公有仓库（如 Docker Hub）与私有仓库（如 Harbor）。 Dockerfile 声明镜像构建流程的文本配置文件，逐层定义基础环境、依赖安装与启动参数。 Docker Compose 用于定义和编排多容器协同运行的工具，通过声明式 YAML 文件管理全栈服务。 2.2 Dockerfile 与 Compose 的职责划分 两者处于不同层级的交付链路中，职责边界清晰：\nDockerfile 聚焦“单镜像定义”：面向单一组件，定义底层镜像选型、文件拷贝、依赖安装与默认入口程序，类似“组装单台计算节点”。 Docker Compose 聚焦“多服务编排”：面向系统级拓扑，定义容器互联、环境变量注入、端口映射、挂载卷以及服务依赖顺序，类似“构建多节点网络拓扑”。 3. 安装与环境验证 3.1 Linux 系统环境安装 (1) Ubuntu / Debian 系列（APT 源安装）：\n1# 1. 更新索引并安装基础前置工具 2sudo apt-get update 3sudo apt-get install -y ca-certificates curl gnupg 4 5# 2. 添加 Docker 官方 GPG 密钥 6sudo install -m 0755 -d /etc/apt/keyrings 7curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 8sudo chmod a+r /etc/apt/keyrings/docker.gpg 9 10# 3. 写入软件源配置 11echo \\ 12 \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \\ 13 $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;) stable\u0026#34; | \\ 14 sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null 15 16# 4. 安装 Docker 核心引擎与组件 17sudo apt-get update 18sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin (2) CentOS / RHEL 系列（YUM / DNF 安装）：\n1# 1. 安装 yum 实用工具包 2sudo yum install -y yum-utils 3 4# 2. 配置官方镜像源仓库 5sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 6 7# 3. 安装 Docker 核心组件 8sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 3.2 版本验证与连通性测试 安装完成后，执行以下命令验证安装状态与引擎运行状况：\n1# 查看客户端与服务端版本详情 2docker --version 3 4# 验证系统信息与运行架构 5docker info 6 7# 运行官方测试容器验证完整拉取与执行链路 8docker run hello-world 4. 服务生命周期与核心配置 4.1 服务生命周期管理 操作 命令 启动 Docker 守护进程 sudo systemctl start docker 停止 Docker 守护进程 sudo systemctl stop docker 重启 Docker 守护进程 sudo systemctl restart docker 查看 Docker 服务状态 sudo systemctl status docker 设置开机自启 sudo systemctl enable docker 禁止开机自启 sudo systemctl disable docker 4.2 守护进程核心配置模板 默认全局配置文件路径：/etc/docker/daemon.json\n1{ 2 \u0026#34;registry-mirrors\u0026#34;: [ 3 \u0026#34;https://registry.docker-cn.com\u0026#34;, 4 \u0026#34;https://mirror.baidubce.com\u0026#34; 5 ], 6 \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, 7 \u0026#34;log-opts\u0026#34;: { 8 \u0026#34;max-size\u0026#34;: \u0026#34;100m\u0026#34;, 9 \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; 10 }, 11 \u0026#34;data-root\u0026#34;: \u0026#34;/var/lib/docker\u0026#34;, 12 \u0026#34;exec-opts\u0026#34;: [ 13 \u0026#34;native.cgroupdriver=systemd\u0026#34; 14 ], 15 \u0026#34;live-restore\u0026#34;: true 16} 配置项说明： registry-mirrors：配置镜像拉取加速地址，缓解公网下载超时问题。 log-driver 与 log-opts：限制容器日志单文件最大容量为 100MB 且最多保留 3 个副本，防止磁盘被日志耗尽。 data-root：指定镜像、容器与卷的本地存储持久化主目录。 exec-opts：将底层 cgroup 驱动设置为 systemd，与 Kubernetes 及高版本 Linux 宿主机规范对齐。 live-restore：在 Docker 守护进程发生重启或热升级时，保持运行中的容器继续工作不中断。 5. 常用命令与基础操作 5.1 镜像与容器常用操作 命令 操作描述 操作分类 docker pull nginx:alpine 从仓库拉取指定标签镜像 镜像检索与下载 docker images 查看本地已下载的镜像列表 镜像清单查看 docker rmi nginx:alpine 依据名称或 ID 删除镜像 镜像删除 docker run -d -p 80:80 \u0026ndash;name mynginx nginx:alpine 映射端口并以后台守护模式运行 容器后台运行 docker ps 查看当前正在运行的容器 容器列表查看 docker ps -a 查看所有容器（含已停止状态） 全部容器查看 docker logs -f mynginx 持续跟踪查看容器控制台输出日志 查看容器日志 docker exec -it mynginx sh 进入正在运行的容器执行 Shell 调试 交互式终端接入 docker stop mynginx \u0026amp;\u0026amp; docker rm mynginx 停止指定容器并清除容器实例 容器停止与删除 5.2 资源清理与状态诊断 命令 作用说明 诊断操作 docker image prune -f 清除因重新构建而无标签的未引用镜像 删除虚悬镜像 docker system prune -f 清理已停止容器、未挂载网络与悬空镜像 深度系统清理 docker top mynginx 查看目标容器内部正执行的系统进程 容器进程查看 docker stats 实时输出各容器的 CPU、内存、网络 IO 统计 资源动态监控 5.3 Docker Compose 核心控制命令 命令 作用说明 操作分类 docker compose up -d 后台拉起并按依赖顺序启动全部服务 启动编排 docker compose up -d \u0026ndash;build 强制重新构建包含 build 声明的镜像并启动 重新构建 docker compose down 停止并移除容器、默认网络，保留持久化卷 停止并清除 docker compose ps 列出当前 Compose 文件定义的服务容器状态 查看编排状态 docker compose logs -f 聚合输出所有服务的标准控制台日志 追踪多服务日志 docker compose config 解析并展开合并当前 YAML 配置文件 配置语法校验 6. 核心运行机制与网络端口流向 6.1 镜像分层与联合文件系统（UnionFS） Docker 镜像由一系列只读层（Read-Only Layers）通过联合挂载技术组合而成：\n写时复制（Copy-on-Write, CoW）：下层镜像层保持不可变只读。当容器启动时，Docker 会在其顶部附加一个薄薄的容器读写层（Container Layer）。容器内任何增删改操作均发生在读写层，底层文件修改时触发复制操作，保障了基础镜像的不可变性与复用性。 存储效率与缓存复用：相同的镜像层在宿主机磁盘上仅需持久化存储一份，构建 Dockerfile 时若某一步骤前置指令未变，构建引擎将直接复用本地缓存层，极大地加快构建效率。 6.2 容器网络模式与端口流向机制 Docker 默认提供多种网络驱动：\nbridge（桥接网络）：默认驱动。每个容器被分配独立网络栈与内部虚拟 IP，通过 docker0 网桥或自定义用户桥接网络互通。 host（主机网络）：容器直接共享宿主机的网络命名空间，完全不隔离端口，效率最高但易产生端口冲突。 none（无网络）：封闭容器网络栈，仅保留本地回环接口，适用于安全计算隔离场景。 在微服务工程实践中，典型业务请求的端口流向存在两种主流路径：\n路径一：宿主机直连访问后端应用 客户端请求 → 宿主机端口 9090 → Docker iptables 端口映射规则转发 → 容器端口 8080 → Spring Boot 应用服务。 路径二：通过 Nginx 边缘网关反向代理 客户端请求 → 宿主机对外端口 80 → Nginx 容器代理层识别 URL → 自定义 Bridge 网络域名解析（http://backend:8080）→ 后端容器端口 8080。 7. 典型实战场景 7.1 单体应用构建：Spring Boot 基础 Dockerfile 目标：将 Java 后端编译产物封装为独立轻量镜像并以后台容器运行。\nDockerfile 默认保存于项目根目录：Dockerfile\n1# 采用精简版 Temurin OpenJDK 基础镜像 2FROM eclipse-temurin:17-jre-alpine 3 4# 设置容器内部默认工作主目录 5WORKDIR /app 6 7# 拷贝编译好的可执行 JAR 包至容器镜像内 8COPY target/*.jar app.jar 9 10# 修正容器系统时区为亚洲上海 11RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\ 12 \u0026amp;\u0026amp; echo \u0026#34;Asia/Shanghai\u0026#34; \u0026gt; /etc/timezone 13 14# 声明应用暴露的通信端口 15EXPOSE 8080 16 17# 配置容器启动运行命令 18ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;-jar\u0026#34;, \u0026#34;app.jar\u0026#34;] 构建与启动执行命令：\n1# 1. 在 Dockerfile 所在目录下触发镜像构建 2docker build -t my-springboot-app:1.0 . 3 4# 2. 映射宿主机 8080 端口并后台启动容器 5docker run -d -p 8080:8080 --name my-app my-springboot-app:1.0 7.2 前后端全栈编排：Docker Compose 综合部署 目标：通过单一 Compose 配置文件协调拉起 MySQL 8.0 数据存储、Spring Boot 后端 API 以及承载 Vue 静态资源的反代 Nginx 前端。\n(1) Nginx 边缘反向代理配置文件：nginx/default.conf\n1server { 2 listen 80; # 监听容器内部 80 端口 3 server_name localhost; # 默认服务器名称 4 5 # 前端构建产物托管 6 location / { 7 root /usr/share/nginx/html; # 静态文件根目录 8 index index.html index.htm; # 默认首页索引 9 try_files $uri $uri/ /index.html; # 支持 Vue 客户端路由模式 10 } 11 12 # 后端 REST 接口反向代理 13 location /api/ { 14 proxy_pass http://backend:8080/; # 借助 Compose 内置 DNS 路由到后端服务 15 proxy_set_header Host $host; # 透传真实主机请求头 16 proxy_set_header X-Real-IP $remote_addr; # 透传客户端实际 IP 17 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 透传链路代理 IP 18 } 19} (2) 多服务综合编排定义文件：docker-compose.yml\n1version: \u0026#34;3.8\u0026#34; 2 3services: 4 # 关系型数据库存储服务 5 db: 6 image: mysql:8.0 # 声明官方 MySQL 镜像版本 7 container_name: library-mysql # 指定容器运行名称 8 restart: always # 宿主机重启或服务异常时自动重启 9 environment: 10 MYSQL_ROOT_PASSWORD: RootPassword123 # 设置 root 根密码 11 MYSQL_DATABASE: library # 初始化创建的业务数据库 12 TZ: Asia/Shanghai # 设置数据库系统时区 13 volumes: 14 - ./mysql/data:/var/lib/mysql # 持久化数据库存储目录 15 - ./mysql/init:/docker-entrypoint-initdb.d # 挂载初始化 SQL 脚本目录 16 networks: 17 - library-net # 接入内部虚拟网络 18 19 # 后端业务微服务 20 backend: 21 image: springboot:latest # 使用本地构建或私服拉取的后端镜像 22 container_name: library-backend # 指定后端容器名称 23 restart: always # 进程退出时自动重启 24 depends_on: 25 - db # 声明先于 backend 启动 db 数据库 26 environment: 27 SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/library?useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;serverTimezone=GMT%2B8 # 连接串使用 db 主机名 28 SPRING_DATASOURCE_USERNAME: root # 数据库连接账号 29 SPRING_DATASOURCE_PASSWORD: RootPassword123 # 数据库连接密码 30 networks: 31 - library-net # 接入同一虚拟通信网络 32 33 # 前端 Web 与反向代理网关 34 nginx: 35 image: nginx:alpine # 采用轻量 Alpine 基础镜像 36 container_name: library-nginx # 指定网关容器名称 37 restart: always # 服务故障时保持重启 38 ports: 39 - \u0026#34;80:80\u0026#34; # 将宿主机 80 端口映射到 Nginx 容器 80 端口 40 volumes: 41 - ./frontend/dist:/usr/share/nginx/html # 挂载前端打包静态资源 42 - ./nginx/default.conf:/etc/nginx/conf.d/default.conf # 挂载自定义站点配置 43 depends_on: 44 - backend # 确保后端拉起后再启动网关 45 networks: 46 - library-net # 共享内部桥接网络 47 48networks: 49 # 自定义桥接通信网络 50 library-net: 51 driver: bridge # 采用标准桥接驱动 (3) 编排部署启动与验证：\n1# 一键拉起并后台运行全部服务 2docker compose up -d 3 4# 检查各微服务运行与端口映射状态 5docker compose ps 6 7# 跟踪查看后端服务启动初始化日志 8docker compose logs -f backend 7.3 镜像交付流：Docker Hub 推送与异地拉取 目标：将本地构建完成的镜像推送至公共仓库，并在远程目标节点拉取部署。\n(1) 镜像打标与推流：\n1# 1. 登录 Docker Hub 凭据 2docker login 3 4# 2. 为目标镜像附加包含命名空间的正式标签 5docker tag springboot:latest yourname/springboot-library:v1.0 6 7# 3. 推送镜像至中心仓库 8docker push yourname/springboot-library:v1.0 (2) 远程生产端拉取与组织结构： 标准生产部署目录推荐规范：\n1project/ 2├── docker-compose.yml # 编排声明文件 3├── nginx/ 4│ └── default.conf # 边缘路由配置 5├── frontend/ 6│ └── dist/ # 前端静态发布包 7└── mysql/ 8 ├── data/ # 宿主机数据持久卷 9 └── init/ 10 └── init.sql # 数据库初始化表结构 远程一键拉取并拉起编排：\n1# 进入工程根目录并启动 2docker compose pull 3docker compose up -d 8. 性能优化与构建最佳实践 8.1 镜像体积精简（多阶段构建） 在工程化实践中，编译环境（如 Maven、JDK）与运行时环境（仅需 JRE）应当分离，利用多阶段构建（Multi-stage builds）可使产物镜像体积缩小 70% 以上：\n1# 阶段一：源码构建阶段 2FROM maven:3.9-eclipse-temurin-17 AS builder 3WORKDIR /build 4COPY pom.xml . 5COPY src ./src 6RUN mvn clean package -DskipTests 7 8# 阶段二：精简运行阶段 9FROM eclipse-temurin:17-jre-alpine 10WORKDIR /app 11COPY --from=builder /build/target/*.jar app.jar 12EXPOSE 8080 13ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;-jar\u0026#34;, \u0026#34;app.jar\u0026#34;] 8.2 运行时资源配额控制 为了防止单个故障容器消耗过多系统资源导致宿主机宕机，建议在 Compose 或启动命令中声明 CPU 与内存上限：\n1services: 2 backend: 3 image: springboot:latest 4 deploy: 5 resources: 6 limits: 7 cpus: \u0026#39;2.0\u0026#39; # 最多使用 2 个 CPU 核心 8 memory: 2048M # 最大内存配额为 2GB 9 reservations: 10 cpus: \u0026#39;0.5\u0026#39; # 预留保障资源为 0.5 核 11 memory: 512M # 预留初始分配为 512MB 9. 常见问题与排错指南 故障现象 根因分析 排查与修复方案 容器启动后立即退出 (Exited 0 或 1) 容器前台没有常驻进程。Docker 依赖主进程（PID 1）生命周期维持运行，进程执行完毕即终止。 检查 ENTRYPOINT/CMD 是否为后台运行命令；若为测试环境可补充 tail -f /dev/null 保持前台等待。 端口已被占用 (bind: address already in use) 宿主机对应端口已被其他宿主机原生服务或其他容器绑定占用。 执行 netstat -tuln | grep \u0026lt;端口号\u0026gt; 或 lsof -i:\u0026lt;端口号\u0026gt; 定位冲突进程，修改 Compose 映射主机端口。 容器间域名无法解析 (Unknown host) 容器使用了默认 bridge 网络，未加入用户自定义网络，默认 bridge 不支持基于容器名的 DNS 服务发现。 将互联服务置于同一个由 networks 显式声明的自定义 bridge 网络中，使用服务名访问。 磁盘空间被持续占满 (No space left on device) 容器日志无限制追加，或残留大量未清理的虚悬镜像与构建缓存。 配置 /etc/docker/daemon.json 中的 log-opts max-size；定期执行 docker system prune -a \u0026ndash;volumes 清理。 WSL2 宿主内存占用失控 (Vmmem 过高) WSL2 默认按宿主机总内存的 50%~80% 动态分配上限，Java 或高负载构建后不会主动释放页面缓存。 在 Windows 用户目录创建 %USERPROFILE%.wslconfig，写入 [wsl2] memory=8GB 显式约束内存。 10. 总结 Docker 通过基于 Linux 命名空间与控制组的轻量级容器化技术，重构了软件工程的交付与运维范式；在微服务与全栈开发实践中，结合 Dockerfile 的不可变镜像定义与 Docker Compose 的声明式服务编排，能够极大简化系统部署流程并确保跨环境运行的一致性与稳定性。\n","date":"2026-06-18","tags":["Docker"]},{"title":"Harness 工程架构深度解析","permalink":"/posts/harness%E5%B7%A5%E7%A8%8B%E6%9E%B6%E6%9E%84%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90/","summary":"从架构视角深入解读 Harness 的设计哲学，帮助工程师建立对现代 CI/CD 平台的认知框架\n一、Harness 到底是什么 一句话：Harness 是一个构建在 Kubernetes 之上的、以 Delegate 为核心执行引擎的、SaaS 化的软件交付平台。\n这句话里有三个关键词，每一个都值得展开。\n1. 构建在 Kubernetes 之上 Harness 的控制平面（Control Plane）是一组微服务，运行在 Kubernetes 集群中。这意味着：\n弹性伸缩是天然能力，不是事后补丁 多租户隔离通过 K8s 的 namespace 和资源配额实现 服务治理依赖 K8s 原生的服务发现和负载均衡 这不是什么了不起的事——几乎所有现代 SaaS 产品都跑在 K8s 上。但关键在于，Harness 把 K8s 的能力向下暴露给了用户：你的 Pipeline 可以直接操作 K8s 集群进行部署，而不是通过一层中间转换。\n2. Delegate 是核心执行引擎（重点） 这是理解 Harness 最关键的一个概念。\nDelegate 是一个运行在你基础设施中的进程，负责执行 Pipeline 中的所有任务。它不是 Harness 帮你跑的——它跑在你的环境里。\n为什么要这样设计？因为软件交付涉及的操作，绝大多数都需要访问内网资源：代码仓库、制品库、K8s 集群、数据库、云账号。如果这些操作全部由 Harness 的 SaaS 控制平面来执行，就意味着你的内网资源必须对 Harness 开放——这在安全合规层面几乎不可接受。\nDelegate 的架构解决了这个问题：\n┌─────────────────────────────────────────────────────┐ │ Harness SaaS控制平面 │ │ (Pipeline 编排、状态管理、UI、API) │ └──────────────────────┬──────────────────────────────┘ │ 出站连接（Delegate 主动拉取任务） │ ← 不需要入站端口暴露 ┌──────────────────────▼──────────────────────────────┐ │ 你的基础设施 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Delegate │ │ Delegate │ │ Delegate │←可部署多个 │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ┌───▼───┐ ┌────▼───┐ ┌────▼───┐ │ │ │K8s集群 │ │代码仓库 │ │云账号 │ │ │ └───────┘ └────────┘ └────────┘ │ └─────────────────────────────────────────────────────┘ 核心设计要点：\n","content":"从架构视角深入解读 Harness 的设计哲学，帮助工程师建立对现代 CI/CD 平台的认知框架\n一、Harness 到底是什么 一句话：Harness 是一个构建在 Kubernetes 之上的、以 Delegate 为核心执行引擎的、SaaS 化的软件交付平台。\n这句话里有三个关键词，每一个都值得展开。\n1. 构建在 Kubernetes 之上 Harness 的控制平面（Control Plane）是一组微服务，运行在 Kubernetes 集群中。这意味着：\n弹性伸缩是天然能力，不是事后补丁 多租户隔离通过 K8s 的 namespace 和资源配额实现 服务治理依赖 K8s 原生的服务发现和负载均衡 这不是什么了不起的事——几乎所有现代 SaaS 产品都跑在 K8s 上。但关键在于，Harness 把 K8s 的能力向下暴露给了用户：你的 Pipeline 可以直接操作 K8s 集群进行部署，而不是通过一层中间转换。\n2. Delegate 是核心执行引擎（重点） 这是理解 Harness 最关键的一个概念。\nDelegate 是一个运行在你基础设施中的进程，负责执行 Pipeline 中的所有任务。它不是 Harness 帮你跑的——它跑在你的环境里。\n为什么要这样设计？因为软件交付涉及的操作，绝大多数都需要访问内网资源：代码仓库、制品库、K8s 集群、数据库、云账号。如果这些操作全部由 Harness 的 SaaS 控制平面来执行，就意味着你的内网资源必须对 Harness 开放——这在安全合规层面几乎不可接受。\nDelegate 的架构解决了这个问题：\n┌─────────────────────────────────────────────────────┐ │ Harness SaaS控制平面 │ │ (Pipeline 编排、状态管理、UI、API) │ └──────────────────────┬──────────────────────────────┘ │ 出站连接（Delegate 主动拉取任务） │ ← 不需要入站端口暴露 ┌──────────────────────▼──────────────────────────────┐ │ 你的基础设施 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Delegate │ │ Delegate │ │ Delegate │←可部署多个 │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ┌───▼───┐ ┌────▼───┐ ┌────▼───┐ │ │ │K8s集群 │ │代码仓库 │ │云账号 │ │ │ └───────┘ └────────┘ └────────┘ │ └─────────────────────────────────────────────────────┘ 核心设计要点：\n出站连接：Delegate 主动向 Harness 控制平面轮询任务，不需要你开放任何入站端口。你的防火墙规则可以保持严格。 任务执行：Delegate 收到任务后，在你的内网环境中执行——访问你的代码仓库、推送到你的 K8s 集群、调用你的云 API。 无状态设计：Delegate 本身不保存状态，可以水平扩展。多个 Delegate 组成一个 Delegate Group，自动负载均衡。 自动升级：Delegate 定期检查新版本并自动更新，不需要手动运维。 这个设计的精妙之处在于：Harness 看到了你的 Pipeline 定义和执行结果，但从未直接触达你的内网资源。所有敏感操作都在你的边界内完成。这满足了大多数企业的安全合规要求。\n3. SaaS 化的软件交付平台 Harness 的默认形态是 SaaS——控制平面由 Harness 团队运维，你只需要部署 Delegate。当然也有 Self-Managed Platform（即私有化部署），但 SaaS 是其主推模型。\n这意味着：\n你不需要运维 CI/CD 基础设施本身（Jenkins Master 的运维痛点彻底消失） 版本升级对用户透明（对比 Jenkins 的插件兼容性噩梦） 多租户天然支持（团队隔离不需要额外配置） 二、Pipeline 的抽象模型 理解了 Delegate，再来看 Harness 的 Pipeline 是怎么组织的。\nHarness 的 Pipeline 采用四层抽象：\nPipeline（流水线） └── Stage（阶段） └── Step（步骤） └── Step Group（步骤组，可选） 每一层的职责：\nPipeline：一次完整的软件交付流程。通常对应一次代码提交从构建到部署的全过程。 Stage：Pipeline 中的一个逻辑阶段。常见类型有 Build（构建）、Deploy（部署）、Approval（审批）、Custom（自定义）。 Step：Stage 中的一个原子操作。比如\u0026quot;执行 Shell 脚本\u0026quot;、\u0026ldquo;推送镜像\u0026rdquo;、\u0026ldquo;滚动更新 K8s Deployment\u0026rdquo;。 Step Group：一组可复用的 Step 集合，类似于函数封装。 这个模型和 Jenkins Pipeline 的 Stage → Step 模型很像，但有一个关键区别：Harness 的每一层抽象都有独立的失败策略、回滚逻辑和条件执行控制。你可以在 Stage 级别定义\u0026quot;如果失败则回滚到上一个版本\u0026quot;，而不是在每个 Step 里写 try-catch。\nInfrastructure Definition：部署目标的声明式描述 Harness 有一个独特的概念叫 Infrastructure Definition，它声明式地描述\u0026quot;这次部署的目标环境是什么\u0026quot;。\n比如，一个 K8s 部署的 Infrastructure Definition 会声明：\n目标集群是哪个（通过 Cloud Provider Connector 关联） 目标 namespace 是什么 Release Name 的前缀是什么 这个设计的好处是：Pipeline 的逻辑和部署目标解耦了。同一个 Pipeline 可以通过切换 Infrastructure Definition，部署到 dev、staging、prod 三个环境，不需要维护三份 Pipeline 配置。\n三、Connector 机制：如何连接外部世界 Harness 需要和很多外部系统交互：GitHub、Docker Hub、AWS、K8s 集群、Slack……这些连接通过 Connector 统一管理。\nConnector 的核心设计原则是凭证与使用分离：\nConnector 定义：存储在 Harness 控制平面，包含连接 URL、认证方式（SSH Key、Token、IAM Role 等）、连接测试逻辑。 实际连接：由 Delegate 在你的内网环境中执行。Connector 的凭证信息会被传递给 Delegate，但连接动作发生在你的网络边界内。 这意味着：你往 Harness 里存的是\u0026quot;怎么连\u0026quot;的描述，而不是让 Harness 直接连。实际的网络握手在你自己的环境里完成。\n常见的 Connector 类型：\nSource Code Connector：GitHub、GitLab、Bitbucket，用于拉取代码 Artifact Connector：Docker Registry、JFrog Artifactory、Nexus，用于推送/拉取构建产物 Cloud Provider Connector：AWS、GCP、Azure，用于调用云 API Kubernetes Cluster Connector：直接关联 K8s 集群的 kubeconfig 或 Service Account Notification Connector：Slack、Email、PagerDuty，用于发送通知 四、Harness vs Jenkins：架构层面的本质差异 很多对比文章停留在\u0026quot;Jenkins 有 1800+ 插件，Harness 没有\u0026quot;这种层面。这没有意义。我们需要从架构层面理解两者的本质差异。\n4.1 执行模型 Jenkins：Master-Agent 架构。Master 负责调度和状态管理，Agent 负责执行。Master 是单点——它挂了，整个 CI/CD 就停了。即使配置了 HA，本质上也是 Active-Passive，资源利用率低。\nHarness：控制平面是分布式微服务，无单点。执行层是 Delegate，无状态且可水平扩展。不存在\u0026quot;Master 挂了全部停摆\u0026quot;的问题。\n4.2 安全模型 Jenkins：Agent 需要能被 Master 访问（入站连接）。如果 Master 在云上、Agent 在内网，你需要打通网络隧道或暴露端口。Jenkins 凭证存储在 Master 的文件系统上，历史上多次出现凭证泄露漏洞。\nHarness：Delegate 主动出站连接，不需要入站端口。凭证存储在 Harness 控制平面（加密），但实际使用凭证的操作在 Delegate 端完成。攻击面显著更小。\n4.3 配置管理 Jenkins：Pipeline 定义用 Jenkinsfile（Groovy DSL）。Groovy 是一门完整的编程语言，这意味着 Jenkinsfile 可以写得极其灵活，也可以写得极其难以维护。没有 Schema 校验，错误在运行时才发现。\nHarness：Pipeline 定义用 YAML。YAML 是声明式的，有明确的 Schema，可以在提交前校验。灵活性不如 Groovy，但一致性和可审计性更好。\n4.4 插件 vs 原生能力 Jenkins：核心是一个调度引擎，几乎所有能力都靠插件实现。插件质量参差不齐，版本兼容性是噩梦（Jenkins 升级大版本时，插件大面积挂掉是常态）。\nHarness：核心能力（CI、CD、Feature Flags、SLO 管理、云成本管理、安全测试编排、IaC 管理、开发者门户、混沌工程）是平台原生的，不是插件。这意味着版本一致性有保障，但灵活性受限——你不能像 Jenkins 那样写一个自定义插件来扩展能力。\n4.5 可观测性 Jenkins：原生可观测性很弱。你需要额外部署 Prometheus + Grafana 来监控 Jenkins 本身，再用 ELK 来聚合构建日志。\nHarness：内置部署可观测性（Service Reliability Management）。Pipeline 的每次执行都有完整的审计日志、执行时间线、失败分析。和外部 APM 工具（Datadog、New Relic）的集成是原生支持的。\n4.6 总结对比 维度 Jenkins Harness 架构 Master-Agent（中心化） 控制平面 + Delegate（分布式） 安全模型 入站连接，凭证存本地 出站连接，凭证加密存储 配置语言 Groovy DSL（图灵完备） YAML（声明式） 能力扩展 插件生态（1800+） 平台原生能力 可观测性 需要额外搭建 内置 运维成本 高（Master 运维、插件管理） 低（SaaS 托管、Delegate 自动升级） 五、从 Jenkins 迁移到 Harness：你需要知道的事 如果你的团队正在考虑从 Jenkins 迁移到 Harness，以下几个点值得提前了解：\n5.1 迁移不是 1:1 翻译 Jenkinsfile 里的 Groovy 逻辑（条件判断、循环、共享库调用）不能直接翻译成 Harness YAML。你需要重新设计 Pipeline 的抽象层次——哪些是 Stage，哪些是 Step Group，哪些逻辑应该下沉到 Delegate 端的脚本里。\n5.2 Harness 不是免费的 Jenkins 是开源免费的（运维成本除外）。Harness SaaS 是按消费计费的——基于构建分钟数、部署次数、Feature Flag 请求数等维度。你需要评估你的使用量对应的费用，而不是假设\u0026quot;换工具就能省钱\u0026quot;。\n5.3 Delegate 的运维成本 虽然 Delegate 会自动升级，但它仍然跑在你的基础设施上。你需要监控它的资源使用（CPU、内存、网络），确保它不会影响你的业务服务。在大规模场景下，Delegate Group 的容量规划是一个需要认真对待的问题。\n5.4 学习曲线是真实的 Harness 的概念模型（Pipeline/Stage/Step/Infrastructure Definition/Connector/Delegate）比 Jenkins 的概念模型（Job/Stage/Step/Agent）更丰富。团队需要时间来建立新的心智模型。\n六、心理认知准备：面对新范式的正确姿态 6.1 认清范式转移的本质 CI/CD 工具的演进不是线性的功能迭代，而是范式转移：\n第一代：脚本时代。Shell 脚本 + Cron，手动编排。 第二代：自动化服务器。Jenkins 为代表，\u0026ldquo;一切皆可插件\u0026rdquo;。 第三代：软件交付平台。Harness 为代表，\u0026ldquo;一切皆是平台能力\u0026rdquo;。 每一代迁移都不是\u0026quot;旧工具的升级\u0026quot;，而是思维方式的改变。从 Jenkins 到 Harness，你需要放弃\u0026quot;什么都能自定义\u0026quot;的执念，接受\u0026quot;平台已经帮你做好了大多数决策\u0026quot;的现实。\n6.2 工程师的三种认知陷阱 陷阱一：工具熟悉度 = 工具优越性\n用惯了 Jenkins 的工程师会本能地认为 Jenkins \u0026ldquo;更好\u0026rdquo;——因为他们已经踩过坑、建立了心智模型、知道怎么绕过问题。但\u0026quot;我熟悉\u0026quot;和\u0026quot;它更好\u0026quot;是两回事。评估新工具时，需要把熟悉度带来的偏见剥离掉。\n陷阱二：灵活性崇拜\n\u0026ldquo;Jenkins 什么都能做\u0026quot;是真的，但\u0026quot;什么都能做\u0026quot;也意味着\u0026quot;什么都要自己做\u0026rdquo;。Harness 的限制性恰恰是它的优势——它帮你做出了大量架构决策，你不需要在 Groovy 脚本里重新发明轮子。\n陷阱三：迁移恐惧\n\u0026ldquo;迁移太麻烦了，不如继续用 Jenkins。\u0026ldquo;这种想法在短期内是理性的，但长期来看，技术债务会累积。Jenkins 的插件维护成本、Master 的运维成本、安全漏洞的修复成本，这些隐性成本往往被低估。\n6.3 建立正确的评估框架 在决定是否迁移到 Harness 之前，建议从以下维度评估：\n安全合规需求：你的行业对安全合规的要求有多严格？Delegate 的出站连接模型是否能显著降低你的合规风险？ 团队规模：小团队（\u0026lt; 10 人）可能不需要 Harness 的全部能力，Jenkins + 云托管方案可能更经济。大团队（\u0026gt; 50 人）的运维成本和协作复杂度会显著放大 Harness 的价值。 多云/混合云需求：如果你的部署目标跨越多个云平台或混合云环境，Harness 的 Connector 和 Infrastructure Definition 模型会大幅降低管理复杂度。 预算：评估 Harness 的定价模型是否适合你的使用模式。高频率、小规模的 CI 场景可能不划算；低频率、高复杂度的 CD 场景价值更大。 6.4 CNCF 平台工程白皮书的启示 CNCF 平台白皮书对平台工程的定义是：\n平台工程是设计和构建工具链与工作流的学科，为云原生时代的软件工程组织提供自助服务能力。平台工程提供集成产品（通常称为\u0026quot;内部开发者平台\u0026rdquo;），贯穿软件生命周期的每个阶段。\nHarness 的设计哲学与此高度一致：它不是一个 CI 工具或 CD 工具，而是一个平台——它把 CI、CD、Feature Flags、SLO 管理、安全测试、成本管理、开发者门户整合在一起，提供统一的自助服务体验。\n理解这一点很重要：你不是在选一个 Jenkins 的替代品，你是在评估一个平台工程战略是否适合你的组织。\n总结 Harness 的架构设计有三个核心支柱：\nDelegate 模型：把执行引擎放在用户环境中，解决了安全合规的根本问题 声明式 Pipeline：用 YAML 替代 Groovy，用 Schema 校验替代运行时错误 平台化整合：把 CI/CD 之外的能力（安全、成本、可靠性）纳入同一个平台 这三个支柱共同构成了一个和 Jenkins 截然不同的软件交付范式。理解这个范式，比记住 Harness 有哪些产品模块重要得多。\n作为工程师，面对范式转移时最有价值的心态是：不预设结论，但带着框架去评估。 这篇文章给你的框架是架构层面的——Delegate 是怎么工作的、Pipeline 是怎么组织的、安全模型是怎么设计的。带着这些认知去试用 Harness，你会比\u0026quot;看功能清单\u0026quot;更快地判断它是否适合你的团队。\n","date":"2026-06-09","tags":["Harness","CI/CD","平台工程","架构设计","Agent Workflow"]},{"title":"Superpowers 使用指南","permalink":"/posts/superpowers%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/","summary":"如果只先记住一个结论，那么可以记这句：\nSuperpowers 不是单纯的“代码增强插件”，而是一套把软件开发最佳实践内化为可执行工作流的方法论插件。\n它的价值不在于“帮你多补几行代码”，而在于通过一组技能系统，把设计、计划、执行、测试、审查、调试和收尾这些环节串成一个更稳定的开发闭环。\n本文会系统回答几个问题：\nSuperpowers 到底是什么 它和普通代码辅助工具有什么本质区别 它的完整工作流怎么跑 什么时候该自动触发，什么时候适合手动调用 使用中最容易踩的坑是什么 1. Superpowers 到底是什么 Superpowers 是一个面向 Claude Code 的开发流程插件。它不是把一堆零散命令简单打包，而是把软件工程中的一整套高质量实践，组织成可自动触发、可组合、可审查的工作流。\n和常见“你提需求，我给代码”的工具相比，它有几个明显不同点：\n它会优先帮助你澄清需求，而不是立刻开写 它强调计划拆解，而不是一次性生成大段实现 它默认引入 TDD、代码审查和系统化调试 它允许通过子代理并行处理任务 它把“验证是否真的完成”看得和“写出代码”同样重要 换句话说，Superpowers 关注的不只是产出代码，而是产出一个更可控、更可验证的开发过程。\n2. 核心理念是什么 Superpowers 背后最重要的不是某个命令，而是它坚持的几条工程原则。\n2.1 测试驱动开发优先 它默认推崇 TDD，也就是经典的 RED -\u0026gt; GREEN -\u0026gt; REFACTOR 循环：\n先写一个失败的测试 再写最少的代码让测试通过 最后在测试保护下重构 这会强迫开发从“想当然实现”切换到“先定义预期行为，再补实现”。\n2.2 系统化优于临时应对 遇到问题时，它更倾向于：\n明确定位问题 收集证据 找根因 用流程和验证收束结果 而不是简单地“试一下这个改法能不能过”。\n2.3 简单性优先于复杂炫技 Superpowers 的方法论倾向非常明确：复杂度不是能力的证明，能把问题拆到足够小、足够清楚，才是真正可维护的工程能力。\n2.4 验证优先于宣称 “已经修好了”“应该可以了”“理论上没问题”这类表述，在它的工作流里都不算真正完成。必须通过测试、审查、验证或复现消除，结果才算站得住。\n3. 它和普通代码辅助工具有什么不同 可以用下面这张表快速理解它的定位：\n维度 普通代码辅助工具 Superpowers 主要目标 更快生成代码 更稳地完成完整开发流程 默认行为 直接根据需求写实现 先设计、再计划、再执行 测试态度 可有可无 默认强制引入 TDD 代码审查 往往依赖人工补充 工作流内建审查机制 调试方式 偏经验式试错 强调系统化根因分析 适用对象 单次编码任务 需要流程质量保证的开发任务 所以，Superpowers 更像是“开发过程的组织者”，而不只是“生成代码的助手”。\n","content":"如果只先记住一个结论，那么可以记这句：\nSuperpowers 不是单纯的“代码增强插件”，而是一套把软件开发最佳实践内化为可执行工作流的方法论插件。\n它的价值不在于“帮你多补几行代码”，而在于通过一组技能系统，把设计、计划、执行、测试、审查、调试和收尾这些环节串成一个更稳定的开发闭环。\n本文会系统回答几个问题：\nSuperpowers 到底是什么 它和普通代码辅助工具有什么本质区别 它的完整工作流怎么跑 什么时候该自动触发，什么时候适合手动调用 使用中最容易踩的坑是什么 1. Superpowers 到底是什么 Superpowers 是一个面向 Claude Code 的开发流程插件。它不是把一堆零散命令简单打包，而是把软件工程中的一整套高质量实践，组织成可自动触发、可组合、可审查的工作流。\n和常见“你提需求，我给代码”的工具相比，它有几个明显不同点：\n它会优先帮助你澄清需求，而不是立刻开写 它强调计划拆解，而不是一次性生成大段实现 它默认引入 TDD、代码审查和系统化调试 它允许通过子代理并行处理任务 它把“验证是否真的完成”看得和“写出代码”同样重要 换句话说，Superpowers 关注的不只是产出代码，而是产出一个更可控、更可验证的开发过程。\n2. 核心理念是什么 Superpowers 背后最重要的不是某个命令，而是它坚持的几条工程原则。\n2.1 测试驱动开发优先 它默认推崇 TDD，也就是经典的 RED -\u0026gt; GREEN -\u0026gt; REFACTOR 循环：\n先写一个失败的测试 再写最少的代码让测试通过 最后在测试保护下重构 这会强迫开发从“想当然实现”切换到“先定义预期行为，再补实现”。\n2.2 系统化优于临时应对 遇到问题时，它更倾向于：\n明确定位问题 收集证据 找根因 用流程和验证收束结果 而不是简单地“试一下这个改法能不能过”。\n2.3 简单性优先于复杂炫技 Superpowers 的方法论倾向非常明确：复杂度不是能力的证明，能把问题拆到足够小、足够清楚，才是真正可维护的工程能力。\n2.4 验证优先于宣称 “已经修好了”“应该可以了”“理论上没问题”这类表述，在它的工作流里都不算真正完成。必须通过测试、审查、验证或复现消除，结果才算站得住。\n3. 它和普通代码辅助工具有什么不同 可以用下面这张表快速理解它的定位：\n维度 普通代码辅助工具 Superpowers 主要目标 更快生成代码 更稳地完成完整开发流程 默认行为 直接根据需求写实现 先设计、再计划、再执行 测试态度 可有可无 默认强制引入 TDD 代码审查 往往依赖人工补充 工作流内建审查机制 调试方式 偏经验式试错 强调系统化根因分析 适用对象 单次编码任务 需要流程质量保证的开发任务 所以，Superpowers 更像是“开发过程的组织者”，而不只是“生成代码的助手”。\n4. 它是怎么工作的 当你描述一个开发需求时，Superpowers 的典型反应通常不是直接产出代码，而是先把整个过程带入一个更可控的结构。\n一个标准链路大致如下：\n先澄清需求与边界 基于确认后的设计拆出计划 将计划拆成足够小、足够清晰的任务 在实现过程中引入 TDD 在阶段之间引入代码审查 在遇到问题时使用系统化调试 最后统一验证、合并或清理分支 这意味着它真正自动化的，不只是“代码生成”，而是“工程节奏”。\n5. 完整开发流程：7 个阶段 Superpowers 的主流程可以理解为 7 个阶段。\n5.1 阶段一：头脑风暴（brainstorming） 触发时机 当你开始描述一个新功能、改造需求或 bug 修复目标时，通常会先进入这一阶段。\n这个阶段做什么 通过提问把模糊需求变清楚 探索可能的技术方案 明确实现边界和验收标准 输出一个可被继续规划的设计基础 它为什么重要 很多返工并不是因为写代码太慢，而是因为一开始问题就没有被定义清楚。brainstorming 的价值就在于尽量把“想做什么”说透，而不是一上来就开写。\n示例 1用户：我想添加用户认证功能 2 3Claude：先一起把需求细化一下： 41. 认证方式是 JWT、Session，还是 OAuth？ 52. 是否需要第三方登录？ 63. 密码强度和找回流程有什么要求？ 7... 5.2 阶段二：使用 Git 工作树（using-git-worktrees） 触发时机 设计被确认之后，就可以进入隔离工作空间的准备阶段。\n这个阶段做什么 创建独立的工作树或分支 在隔离环境中初始化项目 检查当前测试基线是否干净 适合场景 并行开发多个功能 隔离实验性修改 避免直接污染主开发分支 5.3 阶段三：编写计划（writing-plans） 触发时机 当设计已经清晰，并且准备进入实现阶段时，会开始写计划。\n这个阶段做什么 把一个模糊的大目标拆成多个小任务，每个任务通常控制在 2 到 5 分钟可完成的粒度内。\n一个高质量任务通常要包含：\n精确的文件路径 明确的目标 需要补充或修改的代码 具体验证方式 清晰的完成标准 为什么要把任务拆这么细 因为细粒度任务有几个优势：\n更容易验证 更容易回滚 更适合并行执行 更适合做阶段性审查 示例计划结构 1## 任务 1：创建用户模型 2 3- 文件：`src/models/user.py` 4- 验证：运行 `pytest tests/test_user.py` 5- 预期：测试通过 6 7## 任务 2：实现认证端点 8 9- 文件：`src/api/auth.py` 10- 验证：运行 `pytest tests/test_auth.py` 11- 预期：测试通过 5.4 阶段四：执行计划（executing-plans / subagent-driven-development） 两种主要执行方式 方式 A：分批执行（executing-plans） 特点是：\n分批推进 每批执行后保留人工检查点 更适合对关键步骤需要持续确认的任务 方式 B：子代理驱动（subagent-driven-development） 特点是：\n为不同任务启动独立子代理 可以并行处理多个小任务 每个任务都可独立审查 如果任务拆得足够清晰，子代理驱动通常能显著提升吞吐量。\n5.5 阶段五：测试驱动开发（test-driven-development） 这是整个流程里最有“纪律感”的一环。\nSuperpowers 倾向强制执行下面这个循环：\nRED：先写失败测试 GREEN：写最少实现让测试通过 REFACTOR：在测试保护下重构 示例 1def test_user_authentication(): 2 user = authenticate(\u0026#34;admin\u0026#34;, \u0026#34;password123\u0026#34;) 3 assert user.is_authenticated is True 先运行测试，此时它应该失败，因为实现还不存在。\n然后写最小实现让它通过，最后再进行结构优化。\n为什么这个步骤不能省 因为如果一开始就直接写实现，很容易陷入：\n功能看起来跑通了 但行为边界并没有被明确定义 重构时也没有足够保护 5.6 阶段六：请求代码审查（requesting-code-review） Superpowers 把代码审查看成主流程的一部分，而不是“有空再做”的补充动作。\n审查的几个维度 是否符合最初设计 代码质量是否达标 测试是否足够 是否存在明显安全风险 严重程度划分 Critical：必须修复，否则阻断进度 Major：强烈建议修复 Minor：建议改进 Suggestion：可选优化 这个分级很重要，因为它让“审查反馈”从情绪判断变成了更可执行的工程判断。\n5.7 阶段七：完成开发分支（finishing-a-development-branch） 当所有任务完成后，流程还没有结束。最后一个阶段通常会做：\n验证测试是否全部通过 确认是否创建 Pull Request 选择是否合并到主分支 清理工作树和临时分支 这一步的意义在于：不要把“功能写完了”和“开发任务真正闭环了”混为一谈。\n6. 辅助技能：为什么它不只是主流程 7 步 除了主线流程，Superpowers 还内建了几类辅助技能，用来补足真实开发中常见的中断、调试和修复场景。\n6.1 系统化调试（systematic-debugging） 这是最值得重视的辅助技能之一。它强调四个步骤：\n重现问题 缩小范围 找到根因 修复并验证 这套方法最大的价值是：避免一上来就乱改代码，把调试从“碰运气”变成“可解释的分析过程”。\n6.2 完成前验证（verification-before-completion） 这个技能负责确认：\n问题是否真的修复 测试是否覆盖到了关键路径 是否引入了回归 它的作用是防止“看起来修好了，但其实没有彻底解决”。\n6.3 接收代码审查（receiving-code-review） 当审查反馈回来后，这个技能会帮助处理：\n反馈归类 修改请求响应 修复后的再验证 它让“被审查”不再只是被动挨批，而变成一个结构化的改进流程。\n6.4 调度并行代理（dispatching-parallel-agents） 这个技能适合在任务天然可拆、且互相依赖较少时使用。它会帮助：\n划分并发任务 分配子代理 汇总结果 如果任务足够独立，并行执行确实能省下不少时间；但如果任务边界不清，并发反而会制造更多协调成本。\n7. 手动调用技能时怎么用 虽然 Superpowers 会自动触发，但手动调用也很有价值，尤其是在你想明确控制节奏时。\n开发流程相关 1/brainstorming 2/writing-plans 3/executing-plans 4/subagent-driven-development 测试与调试相关 1/test-driven-development 2/systematic-debugging 3/verification-before-completion 审查与分支管理相关 1/requesting-code-review 2/receiving-code-review 3/using-git-worktrees 4/finishing-a-development-branch 5/dispatching-parallel-agents 什么时候适合手动调用 你已经有成熟设计，只想直接进入计划阶段 你只想单独调试某个问题 你需要强行插入一次代码审查 你想控制并行代理的使用时机 8. 一个典型工作流示例 下面用“添加用户认证功能”举一个更完整的例子。\n第一步：提出需求 1我想添加用户认证功能 此时不会立即开写，而是先被引导澄清：\n认证方式是什么 是否需要第三方登录 密码策略如何设计 是否有限流和安全审计要求 第二步：生成设计与计划 在需求明确后，系统可能会生成类似这样的任务拆解：\n任务 1：创建用户模型 任务 2：实现注册接口 任务 3：实现登录接口 任务 4：添加 JWT 中间件 任务 5：补齐测试与错误处理 第三步：执行并审查 每个任务在推进过程中会配套：\n失败测试 最小实现 重构 阶段性审查 第四步：收尾 最后统一做：\n覆盖验证 审查收口 PR 创建或合并 工作树清理 这样一个流程的优点是：它不要求你在脑中同时维护全部复杂度，而是把复杂度分配给了流程本身。\n9. 最常见的误区与陷阱 这类方法论工具最大的风险，不是不会用，而是“自以为在用，实际上一直在绕过核心价值”。\n9.1 误区一：把它当成更强的代码生成器 错误心态通常是：\n直接帮我写完，不用问那么多。\n但这正好绕开了 Superpowers 最值钱的部分：设计澄清、任务拆解、验证与审查。\n9.2 误区二：跳过测试 如果你默认认为“先写代码，测试后面再补”，那实际上已经背离了它的 TDD 核心。\n省掉测试，短期看更快，长期通常意味着：\n更高返工率 更脆弱的重构能力 更容易漏掉边界情况 9.3 误区三：排斥代码审查 很多人会本能地觉得审查拖慢进度，但真正的关键问题如果在这个阶段没被拦住，后面付出的代价往往更大。\nSuperpowers 把审查放进流程，不是为了增加仪式感，而是为了尽量把风险前置。\n9.4 误区四：任务拆得太大 比如下面这种任务描述就很危险：\n1任务 1：实现完整用户系统（预计 2 小时） 问题在于它：\n难验证 难回滚 难并行 难清晰审查 更合理的做法，是拆成多个可独立验证的小任务。\n9.5 误区五：对流程缺乏耐心 很多人会觉得“先花 5-10 分钟澄清需求”是在拖节奏，但真实经验往往相反：前面少花这几分钟，后面多返工几十分钟甚至几小时。\n10. 最佳实践建议 如果你想把 Superpowers 用得顺一点，我会建议优先做到下面几件事。\n10.1 尽量把需求描述具体 例如不要只说：\n1我想做登录功能 更好的表达是：\n1我想做登录功能，要求： 2- 使用 JWT 3- 支持邮箱和手机号登录 4- 密码加密存储 5- 接入 Google 和 GitHub 第三方登录 6- 有速率限制 需求越具体，后续设计与计划质量通常越高。\n10.2 接受设计引导 如果工具在设计阶段提出质疑、追问或建议，不要急着把它看成“阻碍”。很多时候这些追问恰恰是在帮你提前暴露模糊点。\n10.3 信任细粒度任务拆解 2 到 5 分钟粒度的小任务，虽然看起来碎，但它通常更适合：\n快速验证 阶段性交付 并行推进 快速回滚 10.4 把审查当成保护机制 越早发现关键问题，越便宜。尤其是安全、边界和测试覆盖问题，越适合前置解决。\n10.5 合理使用并行代理 并行不等于永远更快。只有当任务天然独立时，并发才是收益项；否则，它很可能只是更快地制造混乱。\n11. 它适合什么，不适合什么 更适合的场景 新功能开发 Bug 修复 代码重构 性能优化 技术债治理 不那么适合的场景 一次性小脚本 很短的临时实验 紧急 hotfix 最后这类场景并不是“不能用”，而是完整流程的收益未必大于成本。\n12. 总结 Superpowers 最核心的价值，不在于它提供了多少命令，而在于它把优秀工程实践从“口头原则”变成了“可执行流程”。\n它做的事情可以概括为四点：\n用流程把质量前置 用自动化降低认知负荷 用任务拆解提升执行稳定性 用测试、审查和验证减少返工 如果你本来就重视 TDD、代码审查、系统化调试和分阶段交付，那么 Superpowers 会像一个很顺手的“流程放大器”；如果你习惯跳过设计、跳过测试、跳过审查，那它给你的第一感受可能不会是更轻松，而是更有纪律。\n但也正因为这种纪律，它更有机会把“会写代码”推进成“能稳定地完成开发工作流”。\n","date":"2026-05-30","tags":["AI","Claude Code","Workflow"]},{"title":"Java核心面试题与八股知识点速记","permalink":"/posts/java%E6%A0%B8%E5%BF%83%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8E%E5%85%AB%E8%82%A1%E7%9F%A5%E8%AF%86%E7%82%B9%E9%80%9F%E8%AE%B0/","summary":"Java 作为现代企业级分布式系统与微服务架构的基石语言，其核心底层机制、集合实现、JVM 运行时原理以及 JUC 并发编程是技术面试与工程实践中最关键的核心基线。本文对高频核心考点进行系统化模块解构与深度剖析，剔除表层快餐记忆，直击底层源码设计与设计权衡。\n1. Java 基础语法与核心机制 1.1 面向对象思想与对象创建 面向对象与面向过程是两种截然不同的程序设计范式，在系统建模与工程演进中承担不同职责：\n面向过程（POP）：核心以算法步骤为中心，将业务需求拆解为按顺序调用的函数序列。其主要优势在于调用开销低、性能高，常用于底层操作系统开发或单片机固件；缺点在于业务逻辑高度耦合，维护与扩展成本高。 面向对象（OOP）：核心以现实世界或业务概念的模型实体为中心，通过类与对象将数据（属性）与行为（方法）高度聚合，具备三大核心特征： (1) 封装：隐藏对象内部实现细节，仅对外暴露安全受控的访问接口，保护内部数据一致性。 (2) 继承：子类复用并扩展父类的属性与方法，建立代码复用机制，同时构建类型多态体系。 (3) 多态：父类引用指向子类具体实现，在程序运行时动态绑定（动态分派）具体调用的实例方法，是实现低耦合与设计模式的基础。 Java 中创建对象实例的底层途径主要包含四种方式：\nnew 关键字：最基础的方式，编译器生成 new 字节码指令分配内存，并调用对应的构造函数完成初始化。 反射机制：通过 Class.newInstance() 或 Constructor.newInstance() 动态获取构造器并创建对象，常用于框架依赖注入与动态代理。 对象克隆：目标类实现 Cloneable 接口并重写 clone() 方法，通过 JVM 底层内存拷贝直接生成新对象，不调用任何构造函数。 反序列化机制：借助 ObjectInputStream 将字节流反序列化为内存对象实例，同样不依赖构造函数执行。 在对象拷贝场景中，浅拷贝与深拷贝存在本质区别：\n浅拷贝（Shallow Copy）：仅克隆基本数据类型的字段值；对于引用类型字段，仅复制对象引用的内存地址，新旧对象依然共享同一个底层堆对象实例。 深拷贝（Deep Copy）：不仅复制基本数据类型，还会递归克隆所有引用对象，在堆内存中完整重建整棵对象关系树，新旧对象完全隔离独立。深拷贝通常通过手动重写 clone()、序列化反序列化（如 Jackson/Kryo）或拷贝构造器实现。 1.2 核心关键字底层剖析 Java 语言关键字直接影响类的继承结构、内存分配以及字段行为：\n(1) final 关键字的作用边界：\n修饰类：表明该类为最终类，不可被任何类继承，其所有成员方法隐式声明为 final（例如 String、Integer 等不可变类，确保安全性与不可篡改性）。 修饰方法：锁定方法逻辑，禁止任何子类重写（Override），但子类依然可以继承使用；JIT 编译器可据此进行内联展开优化。 修饰变量：作为常量使用，必须在声明时或构造器执行完毕前完成显式赋值，赋值后引用地址不可变。若修饰的是引用类型，仅保证指针指向不变，所指向对象内部的状态与属性仍然可变。 (2) static 关键字的加载行为：\n修饰变量与方法：归属于类元信息本身而非特定对象实例。在 JVM 类加载的准备阶段完成空间分配并赋零值，在初始化阶段执行显式赋值，全局仅存在一份内存，可直接通过类名访问。 修饰代码块：静态代码块在类初次被主动使用、执行类构造器方法（clinit）时按书写顺序触发，生命周期内仅执行一次，常用于初始化重量级系统资源。 静态导包：通过 import static 语法直接导入指定类的公共静态成员，调用时无需携带类名前缀。 (3) instanceof 关键字与类型模式匹配：\n","content":"Java 作为现代企业级分布式系统与微服务架构的基石语言，其核心底层机制、集合实现、JVM 运行时原理以及 JUC 并发编程是技术面试与工程实践中最关键的核心基线。本文对高频核心考点进行系统化模块解构与深度剖析，剔除表层快餐记忆，直击底层源码设计与设计权衡。\n1. Java 基础语法与核心机制 1.1 面向对象思想与对象创建 面向对象与面向过程是两种截然不同的程序设计范式，在系统建模与工程演进中承担不同职责：\n面向过程（POP）：核心以算法步骤为中心，将业务需求拆解为按顺序调用的函数序列。其主要优势在于调用开销低、性能高，常用于底层操作系统开发或单片机固件；缺点在于业务逻辑高度耦合，维护与扩展成本高。 面向对象（OOP）：核心以现实世界或业务概念的模型实体为中心，通过类与对象将数据（属性）与行为（方法）高度聚合，具备三大核心特征： (1) 封装：隐藏对象内部实现细节，仅对外暴露安全受控的访问接口，保护内部数据一致性。 (2) 继承：子类复用并扩展父类的属性与方法，建立代码复用机制，同时构建类型多态体系。 (3) 多态：父类引用指向子类具体实现，在程序运行时动态绑定（动态分派）具体调用的实例方法，是实现低耦合与设计模式的基础。 Java 中创建对象实例的底层途径主要包含四种方式：\nnew 关键字：最基础的方式，编译器生成 new 字节码指令分配内存，并调用对应的构造函数完成初始化。 反射机制：通过 Class.newInstance() 或 Constructor.newInstance() 动态获取构造器并创建对象，常用于框架依赖注入与动态代理。 对象克隆：目标类实现 Cloneable 接口并重写 clone() 方法，通过 JVM 底层内存拷贝直接生成新对象，不调用任何构造函数。 反序列化机制：借助 ObjectInputStream 将字节流反序列化为内存对象实例，同样不依赖构造函数执行。 在对象拷贝场景中，浅拷贝与深拷贝存在本质区别：\n浅拷贝（Shallow Copy）：仅克隆基本数据类型的字段值；对于引用类型字段，仅复制对象引用的内存地址，新旧对象依然共享同一个底层堆对象实例。 深拷贝（Deep Copy）：不仅复制基本数据类型，还会递归克隆所有引用对象，在堆内存中完整重建整棵对象关系树，新旧对象完全隔离独立。深拷贝通常通过手动重写 clone()、序列化反序列化（如 Jackson/Kryo）或拷贝构造器实现。 1.2 核心关键字底层剖析 Java 语言关键字直接影响类的继承结构、内存分配以及字段行为：\n(1) final 关键字的作用边界：\n修饰类：表明该类为最终类，不可被任何类继承，其所有成员方法隐式声明为 final（例如 String、Integer 等不可变类，确保安全性与不可篡改性）。 修饰方法：锁定方法逻辑，禁止任何子类重写（Override），但子类依然可以继承使用；JIT 编译器可据此进行内联展开优化。 修饰变量：作为常量使用，必须在声明时或构造器执行完毕前完成显式赋值，赋值后引用地址不可变。若修饰的是引用类型，仅保证指针指向不变，所指向对象内部的状态与属性仍然可变。 (2) static 关键字的加载行为：\n修饰变量与方法：归属于类元信息本身而非特定对象实例。在 JVM 类加载的准备阶段完成空间分配并赋零值，在初始化阶段执行显式赋值，全局仅存在一份内存，可直接通过类名访问。 修饰代码块：静态代码块在类初次被主动使用、执行类构造器方法（clinit）时按书写顺序触发，生命周期内仅执行一次，常用于初始化重量级系统资源。 静态导包：通过 import static 语法直接导入指定类的公共静态成员，调用时无需携带类名前缀。 (3) instanceof 关键字与类型模式匹配：\n经典判定机制：运行时基于 JVM 操作码 instanceof 判断目标对象实际运行时的类型是否为指定类、子类或接口实现，若对象为 null 则直接返回 false，避免抛出空指针异常。 现代模式匹配演进：在现代 Java 版本（Java 14/16+）中，引入模式匹配增强语法，支持在判断类型的同时完成自动安全向下强转（例如 obj instanceof String str），消除了先判断再强制类型转换的样板代码。 1.3 数据类型与包装类机制 Java 为 8 种基础数据类型提供了对应的对象包装器类，二者通过自动装箱与拆箱无缝衔接：\n(1) 自动装箱与拆箱的底层实现：\n装箱（Autoboxing）：基本类型自动转换为包装类对象，编译期本质上将其转换为对应包装类的 valueOf() 静态方法调用（例如 int 装箱为 Integer.valueOf(x)）。 拆箱（Unboxing）：包装类对象自动转换为基本数据类型，编译期转换为对应类型的 xxxValue() 方法调用（例如 Integer 拆箱为 intValue()）。 (2) 包装类常量缓存池机制：\n包装类在内部维护了静态缓存数组，当数值位于缓存区间内时直接返回预先创建好的单例对象，避免频繁在堆中创建销毁实例： Byte、Short、Integer、Long：默认缓存区间为 [-128, 127]。其中 Integer 的上限可通过 JVM 启动参数 -XX:AutoBoxCacheMax 动态调整。 Character：缓存区间为 [0, 127]。 Boolean：内部预置 TRUE 与 FALSE 两个静态只读常量。 Float 与 Double：由于浮点数精度分布无限，不提供常量缓存池机制。 避坑警示：数值在 [-128, 127] 区间内的两个包装类使用双等号判断可能返回 true，但超出范围后判定为 false。生产环境判断对象包装类数值相等必须统一使用 equals() 方法；且包装类拆箱参与运算时，若变量为 null 会直接触发 NullPointerException。 (3) 复合赋值运算中的类型强转：\n表达式 a = a + b 在运算时严格遵循类型提升规则，若 a 为 short、b 为 int，加法结果会自动提升为 int 类型，若不手动强转赋给 short 会产生编译报错。 复合赋值运算符 a += b 内部隐式包含了强制类型转换，等价于 a = (short)(a + b)，编译器自动完成向下类型截断，语法合法但需要警惕潜在的高位精度溢出。 1.4 异常体系与系统错误诊断 Java 所有异常与错误的根基为 Throwable 类，派生出 Error 与 Exception 两大核心分支：\n(1) Exception 与 Error 的定位差异：\nError（系统级严重错误）：表示由 JVM 底层运行时环境触发的严重故障或不可恢复灾难，应用程序无法通过 try-catch 进行补救处理（例如 VirtualMachineError、StackOverflowError、OutOfMemoryError）。出现 Error 时通常意味着运行环境崩溃或资源枯竭，进程应尽快安全终止。 Exception（程序级异常）：表示程序运行过程中可被预见、检测并能够通过代码逻辑捕获修复的异常状况，分为两大子类： 受检异常（Checked Exception）：直接继承自 Exception 且非 RuntimeException 的异常（如 IOException、SQLException）。编译器强制要求开发者显式通过 try-catch 捕获或在方法签名中 throws 抛出，否则编译无法通过。 非受检异常（Unchecked / RuntimeException）：继承自 RuntimeException 的异常（如 NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException）。通常由程序员逻辑缺陷或不严谨的边界校验导致，编译期不强制要求显式捕获。 (2) OOM 与 SOF 的底层根因与排查：\nSOF（StackOverflowError）：栈溢出错误。Java 线程栈是线程私有的，每个方法调用均压入一个栈帧。当方法递归调用层级过深、缺乏基线终止条件或存在方法死循环相互调用时，栈帧深度超过了线程栈所允许的最大深度（可通过 -Xss 参数设置，通常默认 1MB），JVM 将抛出该错误。 OOM（OutOfMemoryError）：内存溢出错误。表示 JVM 内部申请内存时物理资源不足且垃圾收集器无法腾出足够空间。常见形态包括： Java heap space：堆内存溢出，对象创建速度超过 GC 回收速度，或发生内存泄漏（如无序膨胀的静态缓存集合、未关闭的连接池）。 Metaspace / PermGen space：元空间或永久代溢出，通常由运行时加载了过多动态类（CGLIB 动态代理生成类、大量 JSP 动态编译）导致元数据耗尽。 GC overhead limit exceeded：JVM 花费了 98% 以上的运行时间执行垃圾回收，但回收释放的堆空间不足 2%，防止应用在此状态下长时间陷入卡顿。 2. 面向对象进阶与高级特性 2.1 泛型机制与类型擦除 Java 泛型（Generics）在 JDK 5 中引入，旨在增强编译期类型检查，避免冗余的手动强制转换：\n编译期类型安全：在编译阶段对集合元素类型或参数类型进行严格校验，杜绝运行期频繁抛出 ClassCastException。 类型擦除（Type Erasure）：Java 泛型属于伪泛型实现。为了保持与旧版本 JVM 字节码的向后二进制兼容性，Java 编译器在编译期验证类型后，会将所有泛型类型信息全部擦除： (1) 无限定类型的泛型参数直接擦除为 Object（例如 List 编译后转换为原始类型 List，内部存储 Object）。 (2) 带有上界约束的泛型参数擦除为第一个边界类型（例如 擦除为 Number）。 (3) 在访问具体对象时，编译器自动在字节码中插入 checkcast 指令完成向下强制转换，并通过生成桥接方法（Bridge Method）保障多态调用正确。 PECS 原则：在处理集合通配符时遵循 Producer Extends, Consumer Super 黄金准则。若集合仅用于读取数据对外产出，使用上界通配符 \u0026lt;? extends T\u0026gt;；若集合仅用于写入消费外部传入的数据，使用下界通配符 \u0026lt;? super T\u0026gt;。 2.2 反射机制与动态特性 Java 反射机制允许在程序运行状态中，对任意一个未知类，均能完整获取其所有成员属性、构造器与方法，并进行动态调用：\n(1) 获取 Class 对象的途径：\n类名.class 属性：最安全且编译期即可验证，不触发类的初始化阶段（clinit）。 对象.getClass() 方法：通过运行时对象实例动态获取其所属类的运行时类型。 Class.forName(全类名) 静态方法：通过类全限定名动态加载，默认会触发类的加载与初始化，是数据库驱动注册与容器装配的核心方式。 (2) 反射在现代架构中的权衡：\n核心价值：赋予程序高度的动态扩展性与通用性，是 Spring IoC/AOP、ORM 框架（MyBatis/Hibernate）、RPC 序列化框架与单元测试框架的核心基石。 潜在代价与缺陷： 性能损耗：反射调用涉及方法名字符串解析、参数封装与类型动态校验，阻碍了 JIT 编译器的分支预测、内联优化与逃逸分析，执行效率显著低于直接调用。 破坏封装边界：通过 setAccessible(true) 可以强行突破 private 访问控制符的限制，读写对象私有内部状态，存在一定安全性风险。 2.3 Java 引用类型体系 Java 提供了四种生命周期递减的引用模型，为开发者管理堆内存对象生命周期与设计高速缓存提供支持：\n强引用（Strong Reference）：默认常规引用形式（例如 Object obj = new Object()）。只要强引用链从 GC Roots 可达，垃圾收集器就绝不会回收该对象；即便物理内存濒临枯竭，JVM 宁可抛出 OutOfMemoryError 也绝不回收强引用对象。 软引用（Soft Reference）：通过 SoftReference 类封装。当系统内存充足时不会被回收；当垃圾回收后内存依然紧张不足时，GC 会将这部分软引用对象作为第二梯度全量回收。极其适用于构建内存敏感的高速缓存。 弱引用（Weak Reference）：通过 WeakReference 类封装。其存活周期仅能维持到下一次 GC 发生之前。无论当前内存是否充裕，只要垃圾回收器执行扫描，弱引用对象便会被直接回收。典型应用如 ThreadLocalMap 中的 Entry 键，避免线程池生命周期引发的 ClassLoader 或 Key 内存泄漏。 虚引用（Phantom Reference）：通过 PhantomReference 类封装，也被称为幽灵引用。持有虚引用等同于没有任何引用，无法通过 get() 获取实际对象实例。虚引用必须与引用队列（ReferenceQueue）联合使用，当对象被垃圾回收前会放入队列，主要用于监听追踪对象垃圾回收动作并执行堆外直接内存（DirectByteBuffer）的安全释放与资源清理。 2.4 HashCode 与 Equals 设计契约 在基于哈希表实现的集合（如 HashMap、HashSet、ConcurrentHashMap）中，hashCode() 与 equals() 方法构成了对象散列寻址与唯一性判定的核心契约：\nHashCode 散列定位：该方法返回一个 32 位整型哈希码。哈希集合通过特定散列算法计算哈希码对应的桶数组下标，实现接近 O(1) 时间复杂度的快速定位寻址。 重写 Equals 必须重写 HashCode 规范： (1) 若两个对象通过 equals() 判定相等，则调用二者的 hashCode() 方法所返回的哈希码必须严格相等。 (2) 若两个对象的 hashCode() 相等，二者通过 equals() 判定不一定相等（即哈希碰撞 / Hash Collision）。 违背契约引发的生产故障：若自定义对象只重写了 equals() 而未重写 hashCode()，两个业务上属性完全相同的对象可能会返回不同的哈希码。当将其放入 HashSet 或作为 HashMap 的键时，相同的业务对象会被分别散列到不同的哈希桶中，导致集合出现重复键值对，彻底破坏唯一性约束与数据一致性。 3. 核心容器与集合框架深度剖析 3.1 字符串体系对比与不可变设计 Java 字符串处理体系主要由 String、StringBuffer 与 StringBuilder 三大核心类组成：\n(1) 核心特性横向对比矩阵：\n对比维度 String StringBuffer StringBuilder 可变性 严格只读（不可变对象） 内容可变（内部字符数组动态扩容） 内容可变（内部字符数组动态扩容） 线程安全性 线程安全（只读无状态竞争） 线程安全（核心公共方法添加 synchronized） 线程不安全（无并发同步保护） 执行性能 频繁修改产生大量中间对象，开销大 存在加锁与锁释放开销，吞吐中等 无同步开销，字符串拼接吞吐最高 底层实现 JDK 8 为 final char[]；JDK 9+ 为 final byte[] + 编码标记 继承自 AbstractStringBuilder 继承自 AbstractStringBuilder (2) String 不可变性的工程价值：\n字符串常量池（String Pool）复用：只有字符串不可变，不同代码段引用相同字面量时才能安全共享同一块堆内存区域，极大节省堆内存消耗。 HashCode 缓存计算：不可变保证了字符串在初次计算哈希后即可将 hash 成员变量安全缓存起来，后续使用直接读取缓存，无需重复遍历计算，使得 String 作为 HashMap 的 Key 寻址极其高效。 安全性与多线程并发：网络连接参数、文件路径、数据库连接串大多以 String 形式传递，不可变彻底杜绝了被恶意篡改的可能；在多线程环境中无序共享无需进行同步加锁。 3.2 List 容器体系与实现原理 List 接口用于描述有序且允许重复元素的集合，最常见的底层实现为 ArrayList 与 LinkedList：\nArrayList（基于动态数组）： (1) 内存结构与寻址：底层使用连续的 Object[] 数组进行存储，具备极强的局部性缓存命中率。支持下标随机访问，按索引读取（get）与按索引修改（set）时间复杂度均为 O(1)。 (2) 动态扩容机制：默认初始容量为 10（在初次调用 add 添加元素时才进行惰性扩容分配）。当数组填满时，触发扩容逻辑，新容量设定为旧容量的 1.5 倍（即 newCapacity = oldCapacity + (oldCapacity \u0026raquo; 1)）。底层通过 Arrays.copyOf() 与 System.arraycopy() 分配新数组并完成全量元素迁移。 (3) 增删代价：在末尾追加元素效率高；但在数组头部或中间插入、删除元素时，需要将被操作元素之后的所有数据整体向前或向后移动一位，平均时间复杂度为 O(n)。 LinkedList（基于双向链表）： (1) 内存结构：底层由双向链表组成，每个 Node 节点包含前驱指针 prev、后继指针 next 和数据域 item，节点分散分布在堆内存中。 (2) 操作特征：随机访问需要从链表头或尾双向折半遍历寻址，平均时间复杂度为 O(n)；若已知具体节点位置，执行节点的插入与删除仅需调整相邻指针，时间复杂度为 O(1)。由于每个元素都需要额外封装指针对象，整体内存开销较大。 List、Set、Map 体系定位： List：有序序列，保证元素插入顺序，允许存储重复元素，支持索引访问。 Set：无序集合（TreeSet 基于红黑树维持排序，LinkedHashSet 基于哈希与双向链表维持插入序），不允许重复元素，底层大多直接基于 Map 实现（如 HashSet 底层即为 HashMap，以元素作为 Key、以常量 PRESENT 作为 Value）。 Map：基于键值对（Key-Value）映射的模型，Key 保持唯一且最多允许一个 null 键（在 HashMap 中），Value 允许重复。 3.3 Map 体系与 HashMap 底层架构 HashMap 是 Java 面试与工程中使用频率最高的高并发核心数据结构：\n(1) JDK 1.7 与 JDK 1.8 架构演进：\nJDK 1.7 架构：采用“数组 + 链表”结构。在发生哈希碰撞时采用头插法挂载链表。其严重缺陷在于：在多线程并发扩容时，链表节点倒序重排容易形成环形链表死循环，导致 CPU 使用率直接飙升至 100%。 JDK 1.8 架构：重构为“数组 + 链表 + 红黑树”结构。在发生哈希冲突时采用尾插法挂载链表，解决了并发扩容导致的环形死循环问题（但依然存在并发数据覆盖问题，仍然线程不安全）。 (2) 核心参数与红黑树转换条件：\n默认容量与负载因子：默认初始容量为 16，默认负载因子（loadFactor）为 0.75。当 HashMap 中的元素个数超过 threshold = capacity * loadFactor 时触发 2 倍扩容。 树化阈值（TREEIFY_THRESHOLD = 8）：当单个桶内的链表长度达到 8，且当前数组总容量达到或超过 64（MIN_TREEIFY_CAPACITY）时，链表才会转化为红黑树，将寻址效率从 O(n) 提升为 O(log n)。若链表长度达到 8 但数组总容量小于 64，则优先通过 resize() 数组扩容来分散元素，避免过早引入红黑树的维持开销。 反树化阈值（UNTREEIFY_THRESHOLD = 6）：在扩容或删除元素导致红黑树节点数下降至 6 时，红黑树将退化还原为普通单向链表，节约平衡旋转成本。 (3) 数组长度为什么必须保持为 2 的 N 次幂：\n高效计算哈希桶下标：常规对数组长度取模的公式为 index = hash % length。当数组长度 length 恒为 2 的 N 次幂时，数学上具备等价恒等式：hash % length == (length - 1) \u0026amp; hash。CPU 执行按位与（\u0026amp;）运算的吞吐和速度远高于算术取模运算。 保证散列均匀分布：当 length 为 2 的 N 次幂时，length - 1 转换为二进制后低位全部为 1（例如 16 - 1 = 15，二进制为 01111）。执行按位与运算时，寻址下标完全由散列值低位决定；若长度为奇数或非 2 的次幂，其二进制低位必然包含 0，使得某些哈希槽位永远无法被命中，加剧了哈希碰撞。 扩容高低位指针优化：2 倍扩容时，元素的新下标只有两种可能：保持在原位置，或者移动到 原位置 + 旧容量。仅需判断 hash 对应高一位的二进制是 0 还是 1 即可完成双链表拆分，彻底免除了重新计算哈希码的高昂开销。 (4) 扰动函数（Hash Function）设计精髓：\nJDK 1.8 中的哈希扰动函数为：(h = key.hashCode()) ^ (h \u0026raquo;\u0026gt; 16)。 其机制是将 32 位整型哈希值无符号右移 16 位后，与其自身进行异或运算。此举将高 16 位的哈希特征混合并投射到了低 16 位中，在后续数组长度较小时执行 (n - 1) \u0026amp; hash 时，高位特征依然能充分参与运算，显著降低碰撞概率。 3.4 集合安全性与迭代并发控制 (1) HashMap 与 HashTable 的全方位区别：\n线程安全性：HashMap 非线程安全，完全无并发加锁保护；HashTable 属于历史遗留类，通过在全部对外公共方法上添加 synchronized 关键字实现全表加锁，并发吞吐极差。 空值支持：HashMap 允许存在一个 null 键和任意数量的 null 值（null 键固定散列到 0 号桶）；HashTable 键与值均严禁为 null，否则直接抛出 NullPointerException。 继承体系：HashMap 继承自 AbstractMap；HashTable 继承自已经过时的 Dictionary 抽象类。 (2) fail-fast 机制与 ConcurrentModificationException：\n触发机制：在通过迭代器（Iterator）或增强 for 循环遍历集合时，集合内部维护着一个记录结构性修改次数的计数器 modCount。迭代器创建时会将其复制给内部的 expectedModCount。如果在遍历过程中，其他线程或本线程通过非迭代器方法（如 list.remove()、map.put()）修改了集合结构，迭代器的下一次 next() 探测到 modCount != expectedModCount，便会立即抛出 ConcurrentModificationException 异常，即快速失败。 规避与工程选型： 单线程场景下，必须使用迭代器自带的 iterator.remove() 进行安全删除，该方法在移除元素后会同步刷新 expectedModCount。 多线程高并发读写场景下，应使用 java.util.concurrent 包下的并发集合，如 CopyOnWriteArrayList（读写分离、写时复制数组副本）或 ConcurrentHashMap。 4. Java I/O 与对象序列化体系 4.1 I/O 体系分类与设计模式 Java 传统的阻塞式 I/O（BIO）体系按操作单元、数据流向以及功能角色构建了严密的层次模型：\nflowchart TD IO[\u0026#34;Java I/O 核心体系 (java.io)\u0026#34;] IO --\u0026gt; Byte[\u0026#34;字节流 (8-bit 字节数据)\u0026#34;] IO --\u0026gt; Char[\u0026#34;字符流 (16-bit Unicode 字符)\u0026#34;] Byte --\u0026gt; InStream[\u0026#34;输入: InputStream\u0026#34;] Byte --\u0026gt; OutStream[\u0026#34;输出: OutputStream\u0026#34;] Char --\u0026gt; Rdr[\u0026#34;输入: Reader\u0026#34;] Char --\u0026gt; Wtr[\u0026#34;输出: Writer\u0026#34;] InStream --\u0026gt; FIS[\u0026#34;FileInputStream (节点流)\u0026#34;] InStream --\u0026gt; BIS[\u0026#34;BufferedInputStream (包装流/缓冲)\u0026#34;] InStream --\u0026gt; OIS[\u0026#34;ObjectInputStream (对象序列化)\u0026#34;] OutStream --\u0026gt; FOS[\u0026#34;FileOutputStream (节点流)\u0026#34;] OutStream --\u0026gt; BOS[\u0026#34;BufferedOutputStream (包装流/缓冲)\u0026#34;] Rdr --\u0026gt; FR[\u0026#34;FileReader (节点流)\u0026#34;] Rdr --\u0026gt; BR[\u0026#34;BufferedReader (包装流/按行读)\u0026#34;] Rdr --\u0026gt; ISR[\u0026#34;InputStreamReader (转换流: 字节转字符)\u0026#34;] Wtr --\u0026gt; FW[\u0026#34;FileWriter (节点流)\u0026#34;] Wtr --\u0026gt; BW[\u0026#34;BufferedWriter (包装流)\u0026#34;] Wtr --\u0026gt; OSW[\u0026#34;OutputStreamWriter (转换流: 字符转字节)\u0026#34;] flowchart TD IO[\u0026#34;Java I/O 核心体系 (java.io)\u0026#34;] IO --\u0026gt; Byte[\u0026#34;字节流 (8-bit 字节数据)\u0026#34;] IO --\u0026gt; Char[\u0026#34;字符流 (16-bit Unicode 字符)\u0026#34;] Byte --\u0026gt; InStream[\u0026#34;输入: InputStream\u0026#34;] Byte --\u0026gt; OutStream[\u0026#34;输出: OutputStream\u0026#34;] Char --\u0026gt; Rdr[\u0026#34;输入: Reader\u0026#34;] Char --\u0026gt; Wtr[\u0026#34;输出: Writer\u0026#34;] InStream --\u0026gt; FIS[\u0026#34;FileInputStream (节点流)\u0026#34;] InStream --\u0026gt; BIS[\u0026#34;BufferedInputStream (包装流/缓冲)\u0026#34;] InStream --\u0026gt; OIS[\u0026#34;ObjectInputStream (对象序列化)\u0026#34;] OutStream --\u0026gt; FOS[\u0026#34;FileOutputStream (节点流)\u0026#34;] OutStream --\u0026gt; BOS[\u0026#34;BufferedOutputStream (包装流/缓冲)\u0026#34;] Rdr --\u0026gt; FR[\u0026#34;FileReader (节点流)\u0026#34;] Rdr --\u0026gt; BR[\u0026#34;BufferedReader (包装流/按行读)\u0026#34;] Rdr --\u0026gt; ISR[\u0026#34;InputStreamReader (转换流: 字节转字符)\u0026#34;] Wtr --\u0026gt; FW[\u0026#34;FileWriter (节点流)\u0026#34;] Wtr --\u0026gt; BW[\u0026#34;BufferedWriter (包装流)\u0026#34;] Wtr --\u0026gt; OSW[\u0026#34;OutputStreamWriter (转换流: 字符转字节)\u0026#34;] (1) 核心分类维度：\n按处理数据单元：分为字节流（以 InputStream / OutputStream 为抽象基类，以 8 位 byte 为操作单位，适用于图片、音视频、压缩包等所有二进制文件）与字符流（以 Reader / Writer 为抽象基类，以 16 位 char 为操作单位，由 JVM 内部结合字符集进行自动解码，专门用于文本数据）。 按处理角色职责： 节点流（低级流）：直接与特定的物理数据源（文件、内存数组、网络套接字）绑定对接，如 FileInputStream、FileReader。 处理流/包装流（高级流）：封装已有的节点流，通过增强缓冲区或数据格式转换提供更高效、更丰富的功能，如 BufferedInputStream、BufferedReader。 转换流：InputStreamReader 与 OutputStreamWriter 负责架起字节流与字符流之间的桥梁，支持显式指定 UTF-8、GBK 等字符集编码，避免文本读写乱码。 (2) 经典设计模式——装饰器模式（Decorator Pattern）：\nJava I/O 体系是装饰器模式的经典实践。以 BufferedInputStream 为例，它继承自 FilterInputStream 并持有原始 InputStream 引用，在不修改原始底层节点流的基础上，通过在内存中开辟 8KB 缓冲区实现了批量预读，有效减少了频繁发起系统调用的巨大开销。 4.2 序列化机制与 transient 原理 序列化是指将内存中的 Java 对象实例转换为二进制字节序列的过程，以便于持久化存储到磁盘或通过网络传输到远端；反序列化则是其逆向过程。\n(1) Serializable 接口与 serialVersionUID 版本控制：\n目标类必须显式实现 java.io.Serializable 标记接口，否则在调用 ObjectOutputStream.writeObject() 时会直接抛出 NotSerializableException。 serialVersionUID 的核心作用：作为序列化版本的唯一指纹标识。在反序列化时，JVM 会比对二进制字节流中的 serialVersionUID 与本地类加载器中对应类的 serialVersionUID 是否一致。若一致则允许恢复对象；若不一致（例如类新增或删除了字段且未手动指定 UID，导致编译器自动生成的哈希变动），反序列化过程将直接抛出 InvalidClassException。因此生产规范中要求必须显式声明 private static final long serialVersionUID 常量。 (2) transient 关键字的作用与场景：\ntransient 只能修饰类的成员变量，无法修饰类或局部变量。 被 transient 修饰的变量在对象被默认 Java 序列化时会被直接忽略，反序列化重建对象时，该字段将被赋予对应数据类型的默认零值（如引用类型为 null、数值类型为 0）。 典型应用场景： (1) 敏感数据防护：防止密码明文、私钥凭证、身份证号等高危字段被持久化写入磁盘或网络外泄。 (2) 瞬时态派生数据：某些字段仅在本地运行时根据其他数据实时计算得出，无需占用序列化带宽。 (3) 非序列化资源句柄：如 Thread、Socket、InputStream 等与底层操作系统绑定绑死的句柄对象，无法直接跨 JVM 进程恢复。 5. JVM 体系结构与类加载机制 5.1 JVM 运行时数据区与内存划分 根据 Java 虚拟机规范（Java SE 8+），JVM 在运行程序时所管理的内存空间被划分为不同的数据区域：\nflowchart TD subgraph RDA[\u0026#34;JVM 运行时数据区 (Runtime Data Area)\u0026#34;] subgraph Shared[\u0026#34;线程共享区域 (生命周期与 JVM 进程绑定)\u0026#34;] Heap[\u0026#34;Java 堆 (Heap)\\n存放几乎所有对象实例与数组\\n分代模型: 新生代 (Eden, S0, S1) / 老年代\u0026#34;] Meta[\u0026#34;元空间 / 方法区 (Metaspace / Method Area)\\nJDK 8\u0026#43; 移出永久代，使用本地内存\\n存储类元数据、常量池、静态变量\u0026#34;] end subgraph Private[\u0026#34;线程私有区域 (生命周期与线程强绑定)\u0026#34;] PC[\u0026#34;程序计数器 (Program Counter Register)\\n记录当前线程执行字节码指令行号\\n唯一不会发生 OOM 的区域\u0026#34;] VMS[\u0026#34;Java 虚拟机栈 (JVM Stack)\\n方法调用的线程模型，存储栈帧:\\n局部变量表、操作数栈、动态链接、方法出口\u0026#34;] NMS[\u0026#34;本地方法栈 (Native Method Stack)\\n服务于 JVM 底层调用的 Native C/C\u0026#43;\u0026#43; 方法\u0026#34;] end end flowchart TD subgraph RDA[\u0026#34;JVM 运行时数据区 (Runtime Data Area)\u0026#34;] subgraph Shared[\u0026#34;线程共享区域 (生命周期与 JVM 进程绑定)\u0026#34;] Heap[\u0026#34;Java 堆 (Heap)\\n存放几乎所有对象实例与数组\\n分代模型: 新生代 (Eden, S0, S1) / 老年代\u0026#34;] Meta[\u0026#34;元空间 / 方法区 (Metaspace / Method Area)\\nJDK 8\u0026#43; 移出永久代，使用本地内存\\n存储类元数据、常量池、静态变量\u0026#34;] end subgraph Private[\u0026#34;线程私有区域 (生命周期与线程强绑定)\u0026#34;] PC[\u0026#34;程序计数器 (Program Counter Register)\\n记录当前线程执行字节码指令行号\\n唯一不会发生 OOM 的区域\u0026#34;] VMS[\u0026#34;Java 虚拟机栈 (JVM Stack)\\n方法调用的线程模型，存储栈帧:\\n局部变量表、操作数栈、动态链接、方法出口\u0026#34;] NMS[\u0026#34;本地方法栈 (Native Method Stack)\\n服务于 JVM 底层调用的 Native C/C\u0026#43;\u0026#43; 方法\u0026#34;] end end (1) 核心区域定位与堆栈对比：\n堆（Java Heap）：JVM 内存中规模最大的一块，所有线程共享。主要负责分配和存储所有的对象实例与数组。堆是垃圾收集器（GC）的核心管理区域，物理上可以不连续，逻辑上连续。内存耗尽且无法回收扩展时抛出 OutOfMemoryError: Java heap space。 虚拟机栈（JVM Stack）：每个线程创建时即分配独立的私有栈空间。线程中每一个方法的调用与返回对应着一个栈帧（Stack Frame）在栈内的入栈与出栈： 局部变量表：存放编译器可知的 8 种基本数据类型、对象引用（reference）以及 returnAddress。 操作数栈：充当计算操作的工作区，字节码指令在此进行入栈与出栈计算。 动态链接：持有指向运行时常量池中该栈帧所属方法的符号引用，支持运行时动态分派。 方法出口：保存方法正常退出或异常抛出时调用者的恢复现场。 堆与栈的本质区别： 物理与共享性：堆由全局所有线程共享，存储空间大，物理分配复杂；栈为单个线程专有，生命周期同线程，访问速度快。 存储内容：堆存放对象实例本体；栈存放基本数据类型变量与指向堆中对象的引用指针。 异常表现：堆内存耗尽抛出 OOM；栈深度超标抛出 StackOverflowError，栈无法动态扩展时抛出 OOM。 5.2 类的生命周期与全过程剖析 一个类从被加载进虚拟机内存开始，到被卸载出内存为止，其生命周期包含 7 个确定性阶段：\nflowchart LR L1[\u0026#34;1. 加载\\n(Loading)\u0026#34;] --\u0026gt; L2[\u0026#34;2. 验证\\n(Verification)\u0026#34;] L2 --\u0026gt; L3[\u0026#34;3. 准备\\n(Preparation)\u0026#34;] L3 --\u0026gt; L4[\u0026#34;4. 解析\\n(Resolution)\u0026#34;] L4 --\u0026gt; L5[\u0026#34;5. 初始化\\n(Initialization)\u0026#34;] L5 --\u0026gt; L6[\u0026#34;6. 使用\\n(Using)\u0026#34;] L6 --\u0026gt; L7[\u0026#34;7. 卸载\\n(Unloading)\u0026#34;] subgraph Linking[\u0026#34;连接阶段 (Linking)\u0026#34;] L2 L3 L4 end flowchart LR L1[\u0026#34;1. 加载\\n(Loading)\u0026#34;] --\u0026gt; L2[\u0026#34;2. 验证\\n(Verification)\u0026#34;] L2 --\u0026gt; L3[\u0026#34;3. 准备\\n(Preparation)\u0026#34;] L3 --\u0026gt; L4[\u0026#34;4. 解析\\n(Resolution)\u0026#34;] L4 --\u0026gt; L5[\u0026#34;5. 初始化\\n(Initialization)\u0026#34;] L5 --\u0026gt; L6[\u0026#34;6. 使用\\n(Using)\u0026#34;] L6 --\u0026gt; L7[\u0026#34;7. 卸载\\n(Unloading)\u0026#34;] subgraph Linking[\u0026#34;连接阶段 (Linking)\u0026#34;] L2 L3 L4 end (1) 各阶段核心职责：\n加载（Loading）：通过类的全限定名获取定义此类的二进制字节流（可来自 .class 文件、网络、JAR 包或动态生成）；将字节流所代表的静态存储结构转化为方法区的运行时数据结构；在堆内存中生成一个代表该类的 java.lang.Class 对象作为访问入口。 验证（Verification）：连接的第一步，确保 Class 文件的字节流包含的信息符合当前 JVM 规范，不会危害虚拟机自身安全（包含文件格式验证、元数据验证、字节码验证与符号引用验证）。 准备（Preparation）：在方法区（元空间）中为类变量（即 static 修饰的静态变量）分配内存并设置初始零值（如 int 赋 0，引用类型赋 null）。此阶段不会执行任何 Java 代码，显式赋值发生在后续初始化阶段；但若为 static final 常量，在此阶段就会直接赋予代码中指定的常量值。 解析（Resolution）：将常量池内的符号引用（Symbolic References）替换为直接引用（Direct References，直接指向内存目标的指针、相对偏移量或间接句柄）。 初始化（Initialization）：真正开始执行类中编写的 Java 业务代码。JVM 负责组织并执行编译器自动生成的类构造器方法（clinit），完成静态变量的显式赋值与静态代码块中逻辑的执行。类初始化具备严格的线程安全锁机制保证只执行一次。 使用（Using）与卸载（Unloading）：正常执行对象调用；当类对应的所有实例均已回收、类加载器已被回收且 Class 对象没有在任何地方被引用时，类方可被 GC 卸载。 5.3 类加载器与双亲委派模型 JVM 类加载器体系采用层次化的双亲委派架构，保证基础核心类库的执行安全：\n(1) 三层内置类加载器：\n启动类加载器（Bootstrap ClassLoader）：由 C++ 编写（在 Java 中获取为 null），负责加载 JAVA_HOME/lib 目录下或被 -Xbootclasspath 参数指定的 JVM 核心核心类库（如 rt.jar、java.lang.*）。 扩展/平台类加载器（Extension / Platform ClassLoader）：负责加载 JAVA_HOME/lib/ext 目录或由系统变量 java.ext.dirs 指定位置的扩展组件。 应用程序类加载器（Application ClassLoader）：也称系统类加载器，负责加载用户类路径（Classpath）上所有的第三方依赖与自定义类，是普通项目中默认的类加载器。 (2) 双亲委派机制运行模型：\n当一个类加载器收到类加载请求时，它首先不会自己尝试去加载该类，而是把请求按层级向上委托给父类加载器； 只有当父类加载器反馈自己无法完成加载（其搜索范围内未找到目标类）时，子加载器才会尝试自己去其对应的路径中进行检索与加载。 双亲委派的价值：确保 Java 基础核心类的唯一性与安全性。例如用户即便自定义了一个恶意篡改后的 java.lang.String 类，加载请求也会一路委托至 Bootstrap ClassLoader，最终加载的是 JDK 原生核心 String，有效防止核心 API 被破坏替代。 (3) 打破双亲委派模型的经典场景：\nTomcat Web 容器：每个 Web 应用拥有独立的 WebAppClassLoader，它优先加载自身 WEB-INF/classes 和 WEB-INF/lib 下的应用类，只有无法加载时才向上委托给 SharedClassLoader。目的是实现同一容器内不同 Web 应用之间相同类库不同版本的完全隔离。 SPI 机制（如 JDBC Driver 驱动加载）：核心接口 java.sql.Driver 位于 rt.jar 由 Bootstrap ClassLoader 加载，但各数据库厂商的具体实现（如 mysql-connector-java）位于 Classpath 中。核心层通过引入线程上下文类加载器（Thread Context ClassLoader），让高层父加载器逆向委派底层的 ApplicationClassLoader 去加载驱动实现。 6. JVM 垃圾回收与性能调优 6.1 对象存活判定与 GC Roots 垃圾回收器在回收内存前，必须精准断定堆中的哪些对象仍然“存活”，哪些对象已经“死亡”：\n引用计数法（Reference Counting）：每个对象维护一个引用计数器，被引用一次计数加 1，失效减 1，为 0 则可回收。该算法极其简单高效，但其致命缺陷是无法解决对象间的相互循环引用（A 引用 B，B 引用 A，两对象引用计数均无法为 0，导致内存泄漏），主流 JVM 均未采用该方案。 可达性分析算法（Reachability Analysis）：主流商用虚拟机采用的标准方案。以一系列被称为“GC Roots”的根对象作为起始节点集合，自顶向下搜索所经过的引用链（Reference Chain）。如果一个对象到任何 GC Roots 均没有任何引用链相连，则判定为不可达，标记为可回收。 哪些对象可以作为合法的 GC Roots： (1) 虚拟机栈（栈帧中的局部变量表）引用的对象实例（如正在执行的方法中的参数与局部变量）。 (2) 方法区中类静态属性引用的对象（如类的 static 成员变量）。 (3) 方法区中常量引用的对象（如常量池中 StringTable 字符串引用的对象）。 (4) 本地方法栈中 JNI（即 Native 方法）引用的对象。 (5) Java 虚拟机内部的系统引用（如系统类加载器、基本数据类型的 Class 对象）。 (6) 所有被 synchronized 关键字持有、充当互斥同步锁的对象。\n6.2 经典垃圾收集算法对比 JVM 垃圾收集经历了数十年的演进，不同阶段与场景适配了不同的回收算法：\n收集算法 核心执行原理 核心优势 主要缺陷 典型应用场景 标记-清除 (Mark-Sweep) 先通过可达性分析标记所有可回收对象，再统一扫描清除 算法基础，无需移动对象内存地址 产生大量不连续的内存碎片，大对象申请时易提前触发 Full GC CMS 收集器的并发清理阶段 标记-复制 (Mark-Copy) 将内存划分为对等两块，每次仅使用一块；回收时将存活对象复制到另一块，全量清空原区域 无碎片，按顺序分配内存速度极快 存在内存浪费；存活率高时对象复制成本陡增 新生代 GC（如 Eden 与 Survivor 8:1:1 架构） 标记-整理 (Mark-Compact) 标记存活对象，将其全部向内存一端整体移动压紧，随后直接清理掉边界之外的内存 彻底消除碎片，无额外内存折半损耗 移动存活对象需要全程挂起线程并更新指针，STW（Stop The World）停顿较长 老年代 GC（如 Serial Old、Parallel Old） 分代收集理论 (Generational) 根据对象存活周期不同将堆划分为新生代（Young）与老年代（Old），结合前三种算法针对性组合 综合性能最优，实现吞吐量与停顿的平衡 架构相对复杂，涉及跨代引用记录（卡表 Card Table） 绝大多数传统分代收集器 6.3 生产环境 JVM 核心调优参数 合理的 JVM 参数配置是保障高并发系统低延迟与稳定运行的基础，常用核心参数规约如下：\n(1) 堆内存与空间分配规范：\n-Xms：设置初始堆内存大小。生产环境必须将 -Xms 与 -Xmx（最大堆大小）设置为完全相同的值，彻底避免系统在内存波动时动态向操作系统扩容或缩容所带来的性能抖动与开销。 -Xmx：设置最大可用堆内存。通常根据宿主机或容器可用物理内存的 50% ~ 70% 进行配置，预留充足内存给操作系统、元空间及直接堆外内存。 -Xmn：设置新生代（Young Generation）绝对大小。 -XX:SurvivorRatio=8：设置新生代中 Eden 区与两个 Survivor 区（From/To）的容量比例，默认 8 表示 Eden : From : To = 8 : 1 : 1。 -XX:MetaspaceSize 与 -XX:MaxMetaspaceSize：设置元空间的初始阈值与最大上限。建议显式限制最大上限（如 256M 或 512M），防止无限制占用操作系统物理内存。 (2) 经典收集器指定与调试辅助：\n-XX:+UseG1GC：显式开启垃圾优先收集器（G1 GC），推荐在堆内存大于 4G 以上的现代服务器中作为默认选择。 -XX:MaxGCPauseMillis=200：设定期望的单次 GC 最大停顿时间（默认 200ms），G1 会基于该软目标动态调整回收的 Region 数量。 -XX:+HeapDumpOnOutOfMemoryError：配置当 JVM 发生 OOM 崩溃时，自动导出当前时刻的内存二进制堆转储快照（HPROF 文件）。 -XX:HeapDumpPath=/data/logs/dump.hprof：显式指定堆转储文件的落地路径，是后续通过 MAT（Memory Analyzer Tool）精准定位内存泄漏根因的生命线。 7. 多线程基础与线程生命周期 7.1 进程与线程本质差异 操作系统在支持多任务并发执行时，对进程与线程有着明确的边界划分：\n进程（Process）：操作系统进行资源分配和保护的基本独立单位。每个进程在操作系统内核中拥有独立的地址空间、内存代码段、文件描述符与系统资源句柄。进程间相互隔离，通过 IPC（套接字、管道、共享内存）通信，故障崩溃互不影响。 线程（Thread）：CPU 进行独立调度和执行的基本单位，也被称为轻量级进程（LWP）。一个进程内部可以包含多个并发执行的线程，这些线程共享所属进程的堆空间、方法区及打开的文件句柄，但每个线程拥有独立的程序计数器、虚拟机栈和本地方法栈。 并发与并行： 并发（Concurrency）：指在单核 CPU 上，多个线程通过时间片快速轮转交替执行，在宏观时间尺度上表现为“同时发生”。 并行（Parallelism）：指在多核心 CPU 上，多个线程分别分配到不同的物理 CPU 核心上，在同一微观时刻真正做到“同时运行”。 7.2 线程创建与启停控制 (1) 实现多线程的四种途径与选型：\n继承 Thread 类：重写 run() 方法。由于 Java 仅支持单继承，继承 Thread 类会直接锁死子类的继承扩展通道，工程实践中极少推荐。 实现 Runnable 接口：重写 run() 方法并传入 Thread 实例启动。将执行任务与线程调度生命周期彻底解耦，遵循单一职责设计。 实现 Callable 接口：结合 FutureTask 使用，重写 call() 方法。相比 Runnable，Callable 能够向调用方返回异步计算结果，且允许在方法签名中显式抛出异常，通过 Future.get() 进行阻塞获取。 基于线程池（ThreadPoolExecutor）：生产环境绝对强制使用的方案。通过池化复用已创建的线程，杜绝无限制随意创建线程导致的系统崩溃。 (2) start() 与 run() 方法的底层区别：\n调用 start()：通知 JVM 创建并启动操作系统层面的全新内核线程。该线程进入就绪状态（Runnable），一旦获取 CPU 时间片后由 JVM 调度自动异步执行其绑定的 run() 方法。 直接调用 run()：仅仅是在当前调用者线程（如 main 线程）内同步执行该对象的一个普通方法，完全没有任何新线程生成，无法达成多线程异步并发效果。 (3) 优雅终止正在运行的线程：\n废弃的 stop() 方法：已被 JDK 标记淘汰。因为 stop() 会强行立即剥夺线程运行权限并无条件释放所有监视器锁，极易破坏共享数据的原子状态，引发数据严重损坏。 标准中断机制（Interrupt）：通过协作中断标志位实现优雅停止。 (1) 外部调用 thread.interrupt() 发送中断信号，将线程内部的中断状态标志置为 true。 (2) 目标线程在业务循环中定期调用 Thread.currentThread().isInterrupted() 探测自身状态，发现中断后主动退出循环并收尾释放资源。 (3) 若线程当前因调用 sleep()、wait() 或 join() 正处于阻塞状态，收到中断信号时会抛出 InterruptedException 异常并自动清除中断标志位，业务 catch 块中必须重新恢复中断（Thread.currentThread().interrupt()）以保证中断链条完整。 7.3 线程间通信与条件等待机制 多线程协作处理数据时，主要依托共享内存与内置条件等待队列完成通信：\n(1) wait() 与 notify() 的底层约定：\n必须在 synchronized 同步块/方法中调用：调用 wait() 或 notify() 前，线程必须成功持有该对象的互斥监视器锁（Monitor）。若未在同步块中直接调用，JVM 将直接抛出 IllegalMonitorStateException 运行时异常。 防范唤醒丢失（Lost Wakeup）：如果允许无锁调用，一个线程在检查条件满足（如队列为空）与实际调用 wait() 之间，可能发生 CPU 线程切换，此时另一线程生产了数据并立即发送 notify()。由于等待线程尚未真正进入等待队列，该唤醒信号将永久丢失，随后等待线程陷入无期限阻塞假死。 (2) notify() 与 notifyAll() 的精准区别：\nnotify()：仅从当前对象的等待队列（WaitSet）中随机唤醒一个等待线程进入锁竞争队列（EntryList）。由于唤醒具有不确定性，在多个线程等待不同状态条件的复杂场景下，如果被唤醒的线程无法推进业务，可能导致整个系统所有线程陷入死锁或饥饿。 notifyAll()：唤醒当前对象等待队列中的全部线程，所有被唤醒的线程共同参与锁竞争。尽管会产生瞬时的惊群竞争开销，但在多条件协作时能确保正确的处理线程被推进，彻底消除信号丢失死锁隐患。生产实践中应强制优先使用 notifyAll()，且 wait() 调用必须包裹在 while 循环中进行条件判断，防止虚假唤醒（Spurious Wakeup）。 7.4 线程状态转换与休眠对比 线程的 sleep() 与 wait() 经常在面试中作为连环考点出现，其底层执行语义存在本质区别：\n对比维度 Thread.sleep() Object.wait() 所属归属 Thread 类的静态原生方法 Object 类的最终原生实例方法 锁释放行为 只让出 CPU 时间片，绝不释放持有的锁 完全释放当前对象锁，让出 CPU 执行权 执行上下文 可在任何代码逻辑中自由调用 必须在对应对象的 synchronized 代码块内调用 恢复唤醒条件 达到指定休眠时间后由操作系统自动苏醒 必须等待其他线程显式调用 notify/notifyAll 唤醒 使用意图 暂停执行、限流或定时轮询间歇 线程间协作、状态条件等待与信号通知 8. 并发核心关键字与底层原语 8.1 线程安全三大特性与 JMM 规范 在多核心处理器架构下，Java 内存模型（JMM）规范了多线程访问共享内存的交互准则。线程安全的核心防线围绕三大物理特性建立：\n原子性（Atomicity）：一个或一系列操作在 CPU 执行过程中，要么全部执行完毕且不被任何外部因素中断，要么全部不执行。基本类型的简单读写具备原子性，但复合操作（如 i++ 涉及读取、修改、写回三步）非原子操作，需依赖 synchronized、Lock 或 CAS 原语保障。 可见性（Visibility）：当一个线程修改了共享变量的值，其他核心上运行的线程能够立即感知到该最新值。每个线程拥有自己私有的 CPU 寄存器与工作内存（L1/L2 Cache 高速缓存），变量修改若未及时同步回主内存（Main Memory），便会产生可见性失效。 有序性（Ordering）：程序代码执行的先后顺序，在编译器与处理器优化下可能被指令重排序（Instruction Reordering）。在单线程内保证串行语义一致（as-if-serial），但在多线程并发交叉执行时重排会导致严重逻辑混乱。JMM 通过定义 happens-before 规则确立了天然的有序性屏障。 8.2 volatile 关键字底层原理 volatile 是 Java 提供的最轻量级同步机制，具备两大核心能力：\n(1) 保障内存可见性：\n当一个共享变量被 volatile 修饰时，JMM 规定： 线程写入 volatile 变量时，JIT 生成的汇编代码会在末尾追加一个带有 Lock 前缀的指令。 Lock 前缀指令强制将当前处理器缓存行的数据立即刷新回写至主内存。 触发多核 CPU 的**缓存一致性协议（如 MESI 协议）**与总线嗅探机制，将其他处理器核心中缓存了该变量地址的缓存行标记为无效状态（Invalid）。后续其他核心读取该变量时，被迫重新从主内存中拉取最新数据。 (2) 禁止指令重排序与内存屏障：\n为了实现 volatile 的内存语义，编译器在生成字节码时，会在指令序列中插入特定的内存屏障（Memory Barrier）： 写操作前插入 StoreStore 屏障，禁止上面的普通写与 volatile 写重排。 写操作后插入 StoreLoad 屏障，禁止下面的 volatile 读写重排。 读操作后插入 LoadLoad 与 LoadStore 屏障，禁止下面的读写操作与 volatile 读重排。 经典生产场景——DCL 双重检查锁定单例： 1public class Singleton { 2 // 必须增加 volatile 关键字防止指令重排序 3 private static volatile Singleton instance; 4 5 public static Singleton getInstance() { 6 if (instance == null) { // 第一次检查: 规避无谓加锁开销 7 synchronized (Singleton.class) { 8 if (instance == null) { // 第二次检查: 确保单例唯一性 9 instance = new Singleton(); // 关键行: 包含三步指令 10 } 11 } 12 } 13 return instance; 14 } 15} 对象初始化过程 new Singleton() 在字节码层面包含三步：(1) 分配堆内存空间；(2) 执行构造函数初始化成员；(3) 将 instance 引用指向已分配的内存。若不加 volatile，CPU 可能重排序为 1 → 3 → 2。当线程 A 执行完 3 尚未执行 2 时，线程 B 触发第一次检查发现 instance != null，直接返回了一个尚未完成初始化的半成品对象，调用其方法引发崩溃。\n8.3 synchronized 底层原理与锁膨胀机制 synchronized 是基于 JVM 内置监视器（Monitor）实现的互斥同步锁。在 JDK 6 之前属于直接调度操作系统内核互斥量（Mutex）的重量级锁，后续引入了自适应自旋、锁消除、锁粗化以及轻量级锁与偏向锁的锁膨胀优化。\nflowchart LR NoLock[\u0026#34;无锁状态\\n(未发生竞争)\u0026#34;] --\u0026gt; BiasedLock[\u0026#34;偏向锁\\n(单线程专享，记录 ThreadID)\u0026#34;] BiasedLock --\u0026gt; LightLock[\u0026#34;轻量级锁\\n(轻度交替竞争，CAS 复制锁记录)\u0026#34;] LightLock --\u0026gt; HeavyLock[\u0026#34;重量级锁\\n(激烈竞争/自旋失败，升级为 Monitor 阻塞)\u0026#34;] BiasedLock -. 产生线程争用 .-\u0026gt; LightLock LightLock -. 自适应自旋失败 .-\u0026gt; HeavyLock flowchart LR NoLock[\u0026#34;无锁状态\\n(未发生竞争)\u0026#34;] --\u0026gt; BiasedLock[\u0026#34;偏向锁\\n(单线程专享，记录 ThreadID)\u0026#34;] BiasedLock --\u0026gt; LightLock[\u0026#34;轻量级锁\\n(轻度交替竞争，CAS 复制锁记录)\u0026#34;] LightLock --\u0026gt; HeavyLock[\u0026#34;重量级锁\\n(激烈竞争/自旋失败，升级为 Monitor 阻塞)\u0026#34;] BiasedLock -. 产生线程争用 .-\u0026gt; LightLock LightLock -. 自适应自旋失败 .-\u0026gt; HeavyLock (1) 锁升级与状态演化全链路：\nMark Word 对象头：Java 对象在堆内存中由对象头、实例数据与对齐填充组成。对象头内的 Mark Word（32位或64位）用最后特定标志位记录当前对象的锁状态。 无锁 → 偏向锁：大部分场景下锁不仅不存在多线程竞争，而且总是由同一线程多次获得。当线程访问同步块时，直接通过 CAS 将自己的 Thread ID 记录在对象的 Mark Word 中。之后该线程再次进入同步块无需任何加锁解锁操作。 偏向锁 → 轻量级锁：一旦有第二个线程尝试竞争该锁，偏向锁立即被撤销并升级为轻量级锁。竞争线程在各自栈帧中建立锁记录（Lock Record），通过 CAS 尝试将对象头 Mark Word 替换为指向自身栈帧锁记录的指针。成功则获取轻量级锁；未成功则执行自适应自旋等待（自旋次数依据上一同类锁的成功率动态决定），避免立即陷入内核态挂起。 轻量级锁 → 重量级锁：当自旋达到上限次数，或竞争线程数较多时，轻量级锁膨胀为重量级锁。Mark Word 指针指向底层的 ObjectMonitor 监视器对象，未竞争到锁的线程进入内核态阻塞挂起，操作系统在线程状态切换时带来昂贵的上下文切换开销。 (2) JIT 编译器的锁优化：\n锁消除（Lock Elimination）：JIT 编译器在即时编译时通过逃逸分析（Escape Analysis），若证实一段同步代码块中的锁对象仅在当前线程内部被访问、绝不可能逃逸到其他线程，便会将该锁操作直接在机器码中彻底消除。 锁粗化（Lock Coarsening）：当 JIT 探测到一系列连续的操作都在对同一个对象反复加锁和释放锁（例如在循环体内执行锁操作），编译器会将整个加锁同步范围放大到整个操作序列之外，合并为一次大范围加锁，消除频繁加锁开销。 8.4 ReentrantLock 与 AQS 同步原语 (1) synchronized 与 ReentrantLock 深度技术对比：\n实现层级：synchronized 是 JVM 语言层面的内置关键字，异常时由 JVM 保证自动释放锁；ReentrantLock 是 J.U.C（java.util.concurrent）包下的纯 Java API 类，必须在 finally 块中显式调用 unlock() 释放锁。 中断响应能力：ReentrantLock 支持响应中断的锁获取方式（lockInterruptibly()），而 synchronized 在等待锁时不可被中断。 超时尝试机制：ReentrantLock 提供了 tryLock(timeout) 机制，在指定时间内获取不到锁则优雅放弃，有效打破死锁中的请求与保持条件。 公平锁支持：synchronized 仅支持非公平锁；ReentrantLock 构造器支持传入布尔参数自由选择公平锁（按 FIFO 顺序排队）或非公平锁（允许插队抢占，默认非公平以获得更高吞吐）。 多条件分支：ReentrantLock 可绑定多个 Condition 对象，实现精准定向唤醒特定条件线程组，而 synchronized 仅拥有单一隐式条件队列。 (2) AQS（AbstractQueuedSynchronizer）核心架构与工作机制：\nAQS 是整个 J.U.C 体系的灵魂基石（ReentrantLock、Semaphore、CountDownLatch 均基于其派生）。 三大核心要素： (1) 同步状态（volatile int state）：使用 volatile 保证内存可见性，记录同步资源状态。通过 CAS 操作（compareAndSetState）保证状态修改的原子性。以 ReentrantLock 为例，state = 0 表示无锁；state \u0026gt; 0 表示已被锁定，数值代表同一个线程重入锁的次数。 (2) FIFO 双向等待队列（CLH 变体队列）：当线程通过 CAS 获取 state 失败后，AQS 会将当前线程封装成一个 Node 节点，通过尾插 CAS 机制安全加入双向链表尾部，并调用 LockSupport.park() 将该线程阻塞挂起。 (3) 模板方法设计模式：AQS 封装了所有底层的入队、出队、阻塞、唤醒及状态维护逻辑，仅将资源获取与释放的钩子方法暴露给子类实现： 独占模式（Exclusive）：子类重写 tryAcquire() 与 tryRelease()（如 ReentrantLock）。 共享模式（Shared）：子类重写 tryAcquireShared() 与 tryReleaseShared()（如 Semaphore、CountDownLatch）。 8.5 并发集合演进与死锁排查 (1) ConcurrentHashMap 架构演化：\nJDK 1.7 架构：采用**分段锁（Segment）**设计。底层由 Segment 数组组成，每个 Segment 继承自 ReentrantLock 并管理若干个 HashEntry 桶。并发度严格受限于 Segment 数量（默认 16），不同分段之间可以完全并发读写。 JDK 1.8 架构：彻底抛弃分段锁，回归单一 Node 数组 + 链表 + 红黑树结构。并发控制采用 CAS 乐观机制 + synchronized 细粒度锁定： 写入元素时，若目标数组下标为空，直接通过 CAS 尝试写入首节点，全程无锁。 若目标位置已有节点，仅对当前桶的头节点对象（Head Node）施加 synchronized 锁，将锁粒度极致压缩到单个哈希槽位。并发度直接等价于数组容量大小，吞吐量相较分段锁提升数倍。 SynchronizedMap 的落后性：SynchronizedMap 仅仅是用一个全局互斥对象通过包装器将 Map 的所有操作全部串行化，本质与 HashTable 一样，多线程下性能断崖式下跌。 (2) 死锁产生原因与破除防御策略：\n产生死锁的四个必要条件： (1) 互斥条件：资源在同一时刻只能被一个线程独占持有。 (2) 请求与保持条件：一个线程因请求其他被占用的资源而阻塞时，对自己已经持有的资源保持不放。 (3) 不剥夺条件：线程已获得的资源在未使用完毕之前，不能被外界强行剥夺，只能由其自身显式释放。 (4) 循环等待条件：发生死锁时，必然存在一个由多个线程和资源组成的头尾相接的环形等待链条（例如线程 A 持有资源 1 等待资源 2，线程 B 持有资源 2 等待资源 1）。 生产环境破除与防范： 避免嵌套锁与按照全局固定顺序申请锁，打破循环等待链。 使用 ReentrantLock.tryLock(timeout) 设定获取锁的最大超时阈值，超时未获得则主动释放已占有的资源，打破请求与保持条件。 借助 JDK 内置命令行工具 jstack -l pid 或 Arthas 实时扫描并检测系统线程死锁状态。 9. 线程池体系与生产最佳实践 9.1 线程池核心优势与管理价值 在企业高并发系统中，频繁通过 new Thread() 随意创建线程是导致系统 OOM 与 CPU 耗尽的主要隐患。线程池（ThreadPoolExecutor）带来了核心工程收益：\n降低资源消耗：通过重复利用已经创建好的常驻线程，规避了频繁创建和销毁线程所带来的系统调用与内存开销。 提高响应速度：当外部请求到达时，任务无需等待新线程创建即可直接复用空闲核心线程立即执行，降低请求响应延迟。 提高线程的可管理性：线程是操作系统的稀缺系统资源。线程池允许对并发任务与线程资源进行统一的容量限制、负载监控、日志埋点与饱和限流保护。 9.2 ThreadPoolExecutor 核心参数与运转流程 生产构建线程池必须深入掌握其 7 大核心构造参数与任务流转生命周期：\nflowchart TD Submit[\u0026#34;外部提交新任务 execute(task)\u0026#34;] --\u0026gt; CheckCore{\u0026#34;当前运行线程数 \u0026lt; corePoolSize ?\u0026#34;} CheckCore -- 是 --\u0026gt; CreateCore[\u0026#34;创建核心工作线程 (Core Thread)\\n并立即执行该任务\u0026#34;] CheckCore -- 否 --\u0026gt; CheckQueue{\u0026#34;阻塞队列 (workQueue) 是否未满 ?\u0026#34;} CheckQueue -- 是 --\u0026gt; EnQueue[\u0026#34;将任务暂存入阻塞队列\\n等待核心线程空闲时拉取\u0026#34;] CheckQueue -- 否 --\u0026gt; CheckMax{\u0026#34;当前运行线程数 \u0026lt; maximumPoolSize ?\u0026#34;} CheckMax -- 是 --\u0026gt; CreateNonCore[\u0026#34;创建非核心临时工作线程\\n并立即执行该任务\u0026#34;] CheckMax -- 否 --\u0026gt; Reject[\u0026#34;触发饱和拒绝策略 (RejectedExecutionHandler)\\n执行预设降级逻辑\u0026#34;] flowchart TD Submit[\u0026#34;外部提交新任务 execute(task)\u0026#34;] --\u0026gt; CheckCore{\u0026#34;当前运行线程数 \u0026lt; corePoolSize ?\u0026#34;} CheckCore -- 是 --\u0026gt; CreateCore[\u0026#34;创建核心工作线程 (Core Thread)\\n并立即执行该任务\u0026#34;] CheckCore -- 否 --\u0026gt; CheckQueue{\u0026#34;阻塞队列 (workQueue) 是否未满 ?\u0026#34;} CheckQueue -- 是 --\u0026gt; EnQueue[\u0026#34;将任务暂存入阻塞队列\\n等待核心线程空闲时拉取\u0026#34;] CheckQueue -- 否 --\u0026gt; CheckMax{\u0026#34;当前运行线程数 \u0026lt; maximumPoolSize ?\u0026#34;} CheckMax -- 是 --\u0026gt; CreateNonCore[\u0026#34;创建非核心临时工作线程\\n并立即执行该任务\u0026#34;] CheckMax -- 否 --\u0026gt; Reject[\u0026#34;触发饱和拒绝策略 (RejectedExecutionHandler)\\n执行预设降级逻辑\u0026#34;] (1) 7 大核心构造参数说明：\ncorePoolSize：核心线程数。线程池中长期维持存活的最小工作线程数量，即使它们处于空闲状态也不会被回收（除非显式设置了 allowsCoreThreadTimeOut 为 true）。 maximumPoolSize：最大线程数。线程池允许容纳的最大工作线程上限。 keepAliveTime：非核心空闲线程的存活保持时间。当线程池中的线程数量超过核心线程数时，多余的空闲非核心线程在等待新任务时的最大闲置超时阈值。 unit：存活保持时间的物理时间单位（如 TimeUnit.SECONDS）。 workQueue：存放等待执行任务的阻塞队列（BlockingQueue）。 threadFactory：线程创建工厂。用于定制线程名称前缀、统一设置线程优先级以及捕获未处理异常的 UncaughtExceptionHandler。 handler：饱和拒绝策略。当任务队列已满且工作线程总数已达到最大线程数时触发的任务降级保护。 (2) execute() 与 submit() 的差异：\nexecute(Runnable command)：用于提交无需关注返回结果的任务，无法获取返回值，若任务抛出未捕获异常会在子线程控制台直接打印错误。 submit(Callable/Runnable task)：用于提交需要关注结果或执行状态的任务，返回一个 Future 对象。主线程可通过 Future.get() 获取计算返回值或捕获任务执行期间抛出的业务异常。 9.3 内置线程池隐患与饱和拒绝策略 (1) 阿里巴巴 Java 开发手册为什么严禁使用 Executors 快捷工厂：\nFixedThreadPool 与 SingleThreadExecutor：底层使用的阻塞队列为无界队列 LinkedBlockingQueue（其默认最大容量为 Integer.MAX_VALUE）。在外部请求激增且任务处理缓慢时，任务会在队列中无限堆积，最终直接引发内存耗尽崩溃（OOM）。 CachedThreadPool：底层允许的最大线程数为 Integer.MAX_VALUE。当瞬时并发请求暴涨时，线程池会无限制疯狂创建操作系统内核线程，瞬间压垮宿主机 CPU 与系统线程句柄配额，导致系统死锁或 OOM。 (2) JDK 内置 4 种饱和拒绝策略机制：\nAbortPolicy（默认策略）：直接抛出 RejectedExecutionException 运行时异常，阻止系统继续接收该任务，强制调用方进行异常处理。 CallerRunsPolicy（调用者运行策略）：由提交该任务的调用方线程（如 Controller 主线程）直接在本地同步执行该任务。该策略能够有效反向对调用方施加背压（Backpressure），阻缓调用端提交任务的速度，但会暂时阻塞主业务线程。 DiscardOldestPolicy（丢弃最老任务策略）：悄悄丢弃队列头部等待时间最长的一个任务，并将当前被拒绝的新任务重新尝试提交进队列。 DiscardPolicy（静默丢弃策略）：直接静默丢弃当前任务，不抛出任何异常，不记录任何警告日志。适用于可容忍部分丢弃的无状态上报指标业务。 生产自定义策略建议：在微服务实际落地中，通常建议重写 RejectedExecutionHandler，打印带有全局 TraceID 的详细告警日志，收集报警指标，并将溢出的任务序列化写入外部高可用消息队列（Kafka/RabbitMQ）或本地兜底持久化文件进行异步补偿重试。 9.4 核心线程数与容量调优实践 线程池参数绝不能随意凭空设定，需结合服务器硬件算力与业务任务的运行特性进行分类容量计算：\n(1) 经典任务特征估算基线：\nCPU 密集型任务（如数据加解密、复杂算法运算、压缩编码）：此类任务的大部分时间都在消耗 CPU 算力，线程上下文切换开销极大。 推荐核心线程数配置为：CPU 核心数 + 1（预留 1 个额外线程是防止因偶尔的缺页中断等暂停让出 CPU，保证算力打满）。 I/O 密集型任务（如数据库 SQL 查询、调用外部微服务 RPC、读写网络与本地磁盘）：此类任务的大部分时间都在等待网络与磁盘 I/O 响应，CPU 处于空闲等待状态。 经典理论公式：核心线程数 = CPU 核心数 * (1 + 线程等待时间 / 线程执行时间)。 粗略经验基线：一般配置为 2 * CPU 核心数。 (2) 动态线程池运维建议：\n静态配置的线程池难以完美应对瞬息万变的线上真实流量洪峰。推荐结合配置中心（如 Nacos / Apollo）实现动态线程池（借助 ThreadPoolExecutor 提供的 setCorePoolSize() 与 setMaximumPoolSize() 原生方法），配合 Prometheus + Grafana 对线程池活跃度、队列排队深度与拒绝次数进行全方位大盘监控与动态热调整。 ","date":"2026-05-03","tags":["Java","面试","JVM","并发编程"]},{"title":"Kafka集成与Spring Boot实战详解","permalink":"/posts/kafka%E9%9B%86%E6%88%90%E4%B8%8Espring-boot%E5%AE%9E%E6%88%98%E8%AF%A6%E8%A7%A3/","summary":"Apache Kafka 是一个高吞吐、分布式的流处理平台与开源消息引擎，依托 追加日志（Commit Log）、零拷贝（Zero-Copy） 与 分区并发模型 实现海量事件的持久化存储与低延迟流转。\n本文深入介绍 Spring Boot 与 Kafka 的工程集成方案，涵盖本地环境准备、核心依赖配置、生产者与消费者开发、JSON 对象序列化以及分布式事务消息等实践。\n1. 什么是 Kafka Apache Kafka 是一个分布式事件流平台，核心基于发布与订阅（Pub/Sub）模式构建，具备高并发、高吞吐、可水平扩展与高容错等企业级特征。\n1.1 核心特性 发布订阅与消息持久化：基于追加日志（Commit Log）组织数据，所有写入均为磁盘顺序 I/O，支持消息长期持久化与多组独立消费。 实时流式数据处理：支持毫秒级流式数据接入与处理，可与 Flink、Spark Streaming 等大数据计算框架深度协同。 高吞吐与水平扩展：通过分区（Partition）将主题数据分散在多个 Broker 节点，支持集群无损横向伸缩与并行高并发读写。 高可用与多副本机制：提供 Leader 与 Follower 多副本冗余，配合 ISR（In-Sync Replicas）机制保障节点故障时数据不丢失。 1.2 核心应用场景 在分布式与微服务架构中，Kafka 通常充当事件中枢或数据传输管道，广泛应用于以下业务场景：\n微服务间异步解耦：将非核心链路业务（如短信通知、权益发放、统计分析）解耦为异步事件驱动，降低服务间调用耦合与故障扩散风险。 海量日志收集与监控：作为 ELK（Elasticsearch + Logstash + Kibana）或 ClickHouse 的前置缓冲区，汇聚全链路访问日志与系统监控指标。 流数据管道与同步：捕获关系型数据库变更数据（CDC），实时同步并分发至搜索引擎、数据仓库或缓存集群。 流量削峰填谷：在秒杀或大促等突发流量高峰期暂存高并发请求，下游根据自身承载能力匀速拉取消费，保护核心数据库。 2. 环境准备 在开始集成开发之前，请确保本地或服务器已完成基础运行环境与服务的安装部署。\n2.1 依赖环境要求 JDK：JDK 1.8 或更高版本（推荐 JDK 17 / 21）。 Spring Boot：Spring Boot 2.7.x 或 3.x。 构建工具：Maven 3.6+ 或 Gradle 7+。 Kafka 服务端：Kafka 2.8+（含内置 Zookeeper 或 KRaft 模式）。 2.2 启动本地 Kafka 与 Zookeeper (1) 访问 Kafka 官方下载页面 获取二进制解压包，解压后进入程序根目录。\n","content":"Apache Kafka 是一个高吞吐、分布式的流处理平台与开源消息引擎，依托 追加日志（Commit Log）、零拷贝（Zero-Copy） 与 分区并发模型 实现海量事件的持久化存储与低延迟流转。\n本文深入介绍 Spring Boot 与 Kafka 的工程集成方案，涵盖本地环境准备、核心依赖配置、生产者与消费者开发、JSON 对象序列化以及分布式事务消息等实践。\n1. 什么是 Kafka Apache Kafka 是一个分布式事件流平台，核心基于发布与订阅（Pub/Sub）模式构建，具备高并发、高吞吐、可水平扩展与高容错等企业级特征。\n1.1 核心特性 发布订阅与消息持久化：基于追加日志（Commit Log）组织数据，所有写入均为磁盘顺序 I/O，支持消息长期持久化与多组独立消费。 实时流式数据处理：支持毫秒级流式数据接入与处理，可与 Flink、Spark Streaming 等大数据计算框架深度协同。 高吞吐与水平扩展：通过分区（Partition）将主题数据分散在多个 Broker 节点，支持集群无损横向伸缩与并行高并发读写。 高可用与多副本机制：提供 Leader 与 Follower 多副本冗余，配合 ISR（In-Sync Replicas）机制保障节点故障时数据不丢失。 1.2 核心应用场景 在分布式与微服务架构中，Kafka 通常充当事件中枢或数据传输管道，广泛应用于以下业务场景：\n微服务间异步解耦：将非核心链路业务（如短信通知、权益发放、统计分析）解耦为异步事件驱动，降低服务间调用耦合与故障扩散风险。 海量日志收集与监控：作为 ELK（Elasticsearch + Logstash + Kibana）或 ClickHouse 的前置缓冲区，汇聚全链路访问日志与系统监控指标。 流数据管道与同步：捕获关系型数据库变更数据（CDC），实时同步并分发至搜索引擎、数据仓库或缓存集群。 流量削峰填谷：在秒杀或大促等突发流量高峰期暂存高并发请求，下游根据自身承载能力匀速拉取消费，保护核心数据库。 2. 环境准备 在开始集成开发之前，请确保本地或服务器已完成基础运行环境与服务的安装部署。\n2.1 依赖环境要求 JDK：JDK 1.8 或更高版本（推荐 JDK 17 / 21）。 Spring Boot：Spring Boot 2.7.x 或 3.x。 构建工具：Maven 3.6+ 或 Gradle 7+。 Kafka 服务端：Kafka 2.8+（含内置 Zookeeper 或 KRaft 模式）。 2.2 启动本地 Kafka 与 Zookeeper (1) 访问 Kafka 官方下载页面 获取二进制解压包，解压后进入程序根目录。\n(2) 启动 Zookeeper 协调服务进程：\n1# 启动 Zookeeper 协调服务进程 2bin/zookeeper-server-start.sh config/zookeeper.properties (3) 启动 Kafka Broker 服务节点：\n1# 启动 Kafka 服务节点 2bin/kafka-server-start.sh config/server.properties Note 默认配置下，Zookeeper 服务监听 2181 端口，Kafka Broker 服务监听 9092 端口。若需在 Linux 后台常驻运行，可在命令首部增加 nohup 并在命令末尾追加 \u0026amp;。\n3. Kafka 集成 Spring Boot Spring Kafka（spring-kafka）对底层的 kafka-clients 进行了封装，提供了模板化发送工具 KafkaTemplate 以及声明式监听注解 @KafkaListener。\n3.1 添加依赖 配置文件默认路径：pom.xml\n1\u0026lt;dependencies\u0026gt; 2 \u0026lt;!-- Spring Kafka 核心起步依赖 --\u0026gt; 3 \u0026lt;dependency\u0026gt; 4 \u0026lt;groupId\u0026gt;org.springframework.kafka\u0026lt;/groupId\u0026gt; 5 \u0026lt;artifactId\u0026gt;spring-kafka\u0026lt;/artifactId\u0026gt; 6 \u0026lt;/dependency\u0026gt; 7 \u0026lt;!-- Spring Boot Web 起步依赖（用于暴露 RESTful 调试接口） --\u0026gt; 8 \u0026lt;dependency\u0026gt; 9 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 10 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 11 \u0026lt;/dependency\u0026gt; 12\u0026lt;/dependencies\u0026gt; 3.2 配置 Kafka 连接 配置文件默认路径：src/main/resources/application.yml\n1spring: 2 kafka: 3 bootstrap-servers: localhost:9092 # Kafka Broker 节点集群连接地址 4 producer: 5 key-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息键序列化类 6 value-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息体序列化类 7 acks: 1 # 生产者应答机制（0: 不等待, 1: Leader写入成功, all: ISR全副本写入） 8 retries: 3 # 发送失败重试次数 9 batch-size: 16384 # 批量发送阈值大小（字节） 10 properties: 11 linger.ms: 1 # 批量发送等待缓冲延迟（毫秒） 12 consumer: 13 group-id: test-group # 默认消费者组 ID 14 auto-offset-reset: earliest # 无初始位移或位移越界时的策略（earliest: 从头消费, latest: 从最新消费） 15 enable-auto-commit: true # 启用自动位移提交 16 auto-commit-interval: 1000 # 自动提交位移间隔周期（毫秒） 17 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息键反序列化类 18 value-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息体反序列化类 19 template: 20 default-topic: test-topic # 默认发送目标主题 4. 生产者开发 通过 Spring Kafka 提供的 KafkaTemplate 组件，开发者可以便捷地向指定 Topic 投递同步或异步消息。\n4.1 定义生产者服务 创建生产者业务类，封装消息发送逻辑并挂载异步回调：\n1package com.example.kafka.service; 2 3import org.slf4j.Logger; 4import org.slf4j.LoggerFactory; 5import org.springframework.kafka.core.KafkaTemplate; 6import org.springframework.kafka.support.SendResult; 7import org.springframework.stereotype.Service; 8 9import java.util.concurrent.CompletableFuture; 10 11@Service 12public class KafkaProducerService { 13 14 private static final Logger log = LoggerFactory.getLogger(KafkaProducerService.class); 15 16 private final KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate; 17 18 // 依赖注入 KafkaTemplate 19 public KafkaProducerService(KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate) { 20 this.kafkaTemplate = kafkaTemplate; 21 } 22 23 /** 24 * 发送字符串消息并异步监听发送结果 25 * 26 * @param topic 目标主题名称 27 * @param message 待发送的消息内容 28 */ 29 public void sendMessage(String topic, String message) { 30 CompletableFuture\u0026lt;SendResult\u0026lt;String, String\u0026gt;\u0026gt; future = kafkaTemplate.send(topic, message); 31 future.whenComplete((result, ex) -\u0026gt; { 32 if (ex == null) { 33 log.info(\u0026#34;消息发送成功: topic={}, partition={}, offset={}\u0026#34;, 34 result.getRecordMetadata().topic(), 35 result.getRecordMetadata().partition(), 36 result.getRecordMetadata().offset()); 37 } else { 38 log.error(\u0026#34;消息发送失败: topic={}, error={}\u0026#34;, topic, ex.getMessage(), ex); 39 } 40 }); 41 } 42} 4.2 编写 API 接口与发送验证 (1) 编写 REST Controller 控制器暴露触发入口：\n1package com.example.kafka.controller; 2 3import com.example.kafka.service.KafkaProducerService; 4import org.springframework.web.bind.annotation.PostMapping; 5import org.springframework.web.bind.annotation.RequestMapping; 6import org.springframework.web.bind.annotation.RequestParam; 7import org.springframework.web.bind.annotation.RestController; 8 9@RestController 10@RequestMapping(\u0026#34;/kafka\u0026#34;) 11public class KafkaProducerController { 12 13 private final KafkaProducerService kafkaProducerService; 14 15 public KafkaProducerController(KafkaProducerService kafkaProducerService) { 16 this.kafkaProducerService = kafkaProducerService; 17 } 18 19 /** 20 * HTTP POST 发送测试消息 21 */ 22 @PostMapping(\u0026#34;/send\u0026#34;) 23 public String sendMessage(@RequestParam String topic, @RequestParam String message) { 24 kafkaProducerService.sendMessage(topic, message); 25 return \u0026#34;Message sent: \u0026#34; + message; 26 } 27} (2) 使用 curl 发起 HTTP 请求测试消息发送：\n1curl -X POST \u0026#34;http://localhost:8080/kafka/send?topic=test-topic\u0026amp;message=HelloKafka\u0026#34; 5. 消费者开发 Spring Kafka 提供了声明式注解 @KafkaListener，后台自动维护消费线程池、分区拉取与位移提交。\n5.1 定义消费者逻辑 创建消费者服务类，使用 @KafkaListener 注解声明监听的目标主题和消费者组：\n1package com.example.kafka.consumer; 2 3import org.apache.kafka.clients.consumer.ConsumerRecord; 4import org.slf4j.Logger; 5import org.slf4j.LoggerFactory; 6import org.springframework.kafka.annotation.KafkaListener; 7import org.springframework.stereotype.Service; 8 9@Service 10public class KafkaConsumerService { 11 12 private static final Logger log = LoggerFactory.getLogger(KafkaConsumerService.class); 13 14 /** 15 * 监听指定 Topic 的消息 16 * 17 * @param record 消费记录对象，包含元数据信息 18 */ 19 @KafkaListener(topics = \u0026#34;test-topic\u0026#34;, groupId = \u0026#34;test-group\u0026#34;) 20 public void listen(ConsumerRecord\u0026lt;String, String\u0026gt; record) { 21 log.info(\u0026#34;接收到 Kafka 消息: topic={}, partition={}, offset={}, key={}, value={}\u0026#34;, 22 record.topic(), 23 record.partition(), 24 record.offset(), 25 record.key(), 26 record.value()); 27 } 28} 5.2 测试消费者 (1) 使用 curl 再次向 test-topic 发送测试消息：\n1curl -X POST \u0026#34;http://localhost:8080/kafka/send?topic=test-topic\u0026amp;message=HelloKafkaConsumer\u0026#34; (2) 观察 Spring Boot 应用控制台输出，确认消息被正常接收与消费：\n1接收到 Kafka 消息: topic=test-topic, partition=0, offset=1, key=null, value=HelloKafkaConsumer 6. Kafka 高级功能 在生产实践中，除了简单的纯文本消息，系统往往需要传输复杂的 Java 实体对象，或在跨组件操作中保证消息发送与本地业务的原子性。\n6.1 自定义消息对象的序列化 默认情况下 Kafka 采用字符串序列化器。通过集成 Jackson 与 Spring Kafka 提供的 JsonSerializer / JsonDeserializer，可直接在生产者与消费者之间流转 Java DTO 实体。\n(1) 引入 Jackson 与 Lombok 依赖（配置文件默认路径：pom.xml）：\n1\u0026lt;!-- Jackson 核心依赖（用于 JSON 序列化与反序列化） --\u0026gt; 2\u0026lt;dependency\u0026gt; 3 \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; 4 \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; 5\u0026lt;/dependency\u0026gt; 6\u0026lt;!-- Lombok 依赖（用于消除实体模型样板代码） --\u0026gt; 7\u0026lt;dependency\u0026gt; 8 \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; 9 \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; 10 \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; 11\u0026lt;/dependency\u0026gt; (2) 定义消息实体传输模型 User：\n1package com.example.kafka.entity; 2 3import lombok.AllArgsConstructor; 4import lombok.Data; 5import lombok.NoArgsConstructor; 6 7import java.io.Serializable; 8 9@Data 10@NoArgsConstructor 11@AllArgsConstructor 12public class User implements Serializable { 13 14 private static final long serialVersionUID = 1L; 15 16 private String name; 17 private int age; 18} (3) 配置 JSON 序列化与信任包规则（配置文件默认路径：src/main/resources/application.yml）：\n1spring: 2 kafka: 3 producer: 4 key-serializer: org.apache.kafka.common.serialization.StringSerializer # 键序列化器 5 value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 值序列化器：Spring 提供的 JSON 序列化工具 6 consumer: 7 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 键反序列化器 8 value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer # 值反序列化器：Spring 提供的 JSON 反序列化工具 9 properties: 10 spring.json.trusted.packages: \u0026#34;*\u0026#34; # 反序列化信任包路径，\u0026#34;*\u0026#34; 表示允许反序列化所有包路径下的对象 (4) 生产者发送实体对象：\n1package com.example.kafka.service; 2 3import com.example.kafka.entity.User; 4import org.springframework.kafka.core.KafkaTemplate; 5import org.springframework.stereotype.Service; 6 7@Service 8public class UserProducerService { 9 10 private final KafkaTemplate\u0026lt;String, Object\u0026gt; kafkaTemplate; 11 12 public UserProducerService(KafkaTemplate\u0026lt;String, Object\u0026gt; kafkaTemplate) { 13 this.kafkaTemplate = kafkaTemplate; 14 } 15 16 /** 17 * 发送结构化实体对象 18 */ 19 public void sendUserMessage(String topic, User user) { 20 kafkaTemplate.send(topic, user); 21 } 22} (5) 消费者接收实体对象：\n1package com.example.kafka.consumer; 2 3import com.example.kafka.entity.User; 4import org.slf4j.Logger; 5import org.slf4j.LoggerFactory; 6import org.springframework.kafka.annotation.KafkaListener; 7import org.springframework.stereotype.Service; 8 9@Service 10public class UserConsumerService { 11 12 private static final Logger log = LoggerFactory.getLogger(UserConsumerService.class); 13 14 /** 15 * 自动反序列化为 User 实体对象 16 */ 17 @KafkaListener(topics = \u0026#34;user-topic\u0026#34;, groupId = \u0026#34;test-group\u0026#34;) 18 public void listenUser(User user) { 19 log.info(\u0026#34;成功接收到结构化对象: {}\u0026#34;, user); 20 } 21} 6.2 Kafka 事务支持 Kafka 支持基于两阶段提交的跨分区事务消息，确保一组消息要么全部写入成功，要么全部放弃，常用于实现生产消费之间的原子流转。\n(1) 开启生产者事务支持（配置文件默认路径：src/main/resources/application.yml）：\n1spring: 2 kafka: 3 producer: 4 transaction-id-prefix: trans- # 事务 ID 前缀，配置后自动开启 Kafka 事务管理器 (2) 在生产者业务中执行事务发送：\n1package com.example.kafka.service; 2 3import org.springframework.kafka.core.KafkaTemplate; 4import org.springframework.stereotype.Service; 5import org.springframework.transaction.annotation.Transactional; 6 7@Service 8public class TransactionalProducerService { 9 10 private final KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate; 11 12 public TransactionalProducerService(KafkaTemplate\u0026lt;String, String\u0026gt; kafkaTemplate) { 13 this.kafkaTemplate = kafkaTemplate; 14 } 15 16 /** 17 * 方式一：基于 executeInTransaction 显式事务回调 18 */ 19 public void sendInTransaction(String topic, String message) { 20 kafkaTemplate.executeInTransaction(operations -\u0026gt; { 21 operations.send(topic, message); 22 return true; 23 }); 24 } 25 26 /** 27 * 方式二：基于 Spring 的 @Transactional 声明式事务管理 28 */ 29 @Transactional(rollbackFor = Exception.class) 30 public void sendWithAnnotation(String topic, String message) { 31 kafkaTemplate.send(topic, message); 32 // 若后续业务逻辑抛出未捕获异常，Kafka 事务将自动中止回滚 33 } 34} Tip 开启事务后，消费者端的 isolation.level 默认是 read_uncommitted。如需过滤未提交或已中止的脏消息，需将消费者配置项 isolation.level 设置为 read_committed。\n7. 总结 Spring Kafka 极大地简化了与 Apache Kafka 的集成流程，提升了分布式流数据处理与消息通信的研发效率。\n7.1 核心功能回顾 (1) 生产者开发：使用 KafkaTemplate 发送同步与异步消息，配合回调函数监听消息分区、Offset 及发送状态。\n(2) 消费者开发：使用 @KafkaListener 声明式监听目标主题与消费组，由后台线程池自动管理分区拉取与位移提交。\n(3) 高级功能支持：集成 Jackson 序列化器实现复杂 Java 对象的透明序列化流转，并通过 transaction-id-prefix 开启分布式事务保障消息发送原子性。\n7.2 生产落地建议 位移提交模式：对于金融支付与核心业务链路，建议将自动提交改为手动确认（AckMode.MANUAL_IMMEDIATE），彻底消除消息丢失隐患。 死信队列与容错：针对业务消费异常与重试耗尽场景，引入死信队列（DLT / Dead Letter Queue）与告警机制，防止消费链路级联阻塞。 集群高可用配置：在生产部署中将生产者 acks 设置为 all，并结合 Broker 端的 min.insync.replicas 策略，确保分区多副本强一致落盘。 ","date":"2026-04-26","tags":["Kafka","SpringBoot"]},{"title":"RabbitMQ集成与Spring Boot实战详解","permalink":"/posts/rabbitmq%E9%9B%86%E6%88%90%E4%B8%8Espring-boot%E5%AE%9E%E6%88%98%E8%AF%A6%E8%A7%A3/","summary":"RabbitMQ 是一个基于 AMQP（Advanced Message Queuing Protocol） 协议构建、由 Erlang 语言编写的高性能企业级消息中间件，依托 交换机（Exchange）、绑定键（Routing Key） 与 灵活的路由分发机制 实现分布式架构下高并发、高解耦与高可靠的消息流转。\n本文深入介绍 Spring Boot 与 RabbitMQ 的工程集成方案，涵盖本地环境准备、核心依赖配置、拓扑结构集中声明、生产者与消费者开发、手动确认与限流以及死信延时队列等实践。\n1. 什么是 RabbitMQ RabbitMQ 是一个支持多协议、功能完备的企业级分布式消息引擎，基于 Erlang 语言的高并发 Actor 模型与轻量级进程机制构建，具备低延迟、高可靠性保障、灵活的路由策略与完善的可视化管理能力。\n1.1 核心特性 灵活的路由模型与消息持久化：基于交换机（Exchange）与绑定（Binding）解耦生产与消费端，支持 Direct、Topic、Fanout 等多种路由策略，并提供交换机、队列与消息三层持久化机制。 完善的可靠性投递与确认机制：提供生产者发布确认（Publisher Confirm）、返回监听（Return Callback）与消费者手动应答（Manual Ack），保障消息在全链路传输过程中不丢失。 流量削峰与服务限流（QoS Prefetch）：支持在 Channel 通道级别与消费者级别配置 prefetchCount 预取阈值，下游根据自身承载能力拉取并处理消息，防止突发流量压垮消费者。 丰富的死信机制与延迟任务支持：通过死信交换机（DLX / Dead Letter Exchange）与队列/消息 TTL 组合，优雅解决消费异常兜底、重试收容与延时任务调度需求。 1.2 核心应用场景 在分布式与微服务架构中，RabbitMQ 通常充当解耦中枢与缓冲管道，广泛应用于以下业务场景：\n微服务间异步解耦：将核心业务与非核心流程（如注册成功后的邮件发送、短信通知、优惠券发放）解耦为异步事件驱动，降低服务间调用时延与系统雪崩风险。 流量削峰填谷：在秒杀大促、抢购活动等短时瞬时流量冲击下充当缓冲区，将瞬时高并发请求暂存至队列中，后端服务按照既定消费速率匀速处理，保护底层数据库。 延时任务与超时订单取消：利用队列的 x-message-ttl 配合死信交换机（DLX），或集成延时交换机插件（rabbitmq_delayed_message_exchange），实现未支付订单 30 分钟超时自动关单、定时状态同步等延迟处理场景。 异常重试与死信收容：对于消费端业务异常、重试耗尽或非法报文，自动转发至死信队列（DLQ）进行隔离归档，配合告警通知并提供人工排查补偿机制。 2. 环境准备 在开始集成开发之前，请确保本地或服务器已完成基础运行环境与服务的安装部署。\n2.1 依赖环境要求 JDK：JDK 1.8 或更高版本（推荐 JDK 17 / 21）。 Spring Boot：Spring Boot 2.7.x 或 3.x。 构建工具：Maven 3.6+ 或 Gradle 7+。 RabbitMQ 服务端：RabbitMQ 3.8+（需匹配 Erlang/OTP 对应运行时版本）。 2.2 启动本地 RabbitMQ 与服务管理 (1) Windows 服务运维命令（需以管理员权限打开 PowerShell）：\n","content":"RabbitMQ 是一个基于 AMQP（Advanced Message Queuing Protocol） 协议构建、由 Erlang 语言编写的高性能企业级消息中间件，依托 交换机（Exchange）、绑定键（Routing Key） 与 灵活的路由分发机制 实现分布式架构下高并发、高解耦与高可靠的消息流转。\n本文深入介绍 Spring Boot 与 RabbitMQ 的工程集成方案，涵盖本地环境准备、核心依赖配置、拓扑结构集中声明、生产者与消费者开发、手动确认与限流以及死信延时队列等实践。\n1. 什么是 RabbitMQ RabbitMQ 是一个支持多协议、功能完备的企业级分布式消息引擎，基于 Erlang 语言的高并发 Actor 模型与轻量级进程机制构建，具备低延迟、高可靠性保障、灵活的路由策略与完善的可视化管理能力。\n1.1 核心特性 灵活的路由模型与消息持久化：基于交换机（Exchange）与绑定（Binding）解耦生产与消费端，支持 Direct、Topic、Fanout 等多种路由策略，并提供交换机、队列与消息三层持久化机制。 完善的可靠性投递与确认机制：提供生产者发布确认（Publisher Confirm）、返回监听（Return Callback）与消费者手动应答（Manual Ack），保障消息在全链路传输过程中不丢失。 流量削峰与服务限流（QoS Prefetch）：支持在 Channel 通道级别与消费者级别配置 prefetchCount 预取阈值，下游根据自身承载能力拉取并处理消息，防止突发流量压垮消费者。 丰富的死信机制与延迟任务支持：通过死信交换机（DLX / Dead Letter Exchange）与队列/消息 TTL 组合，优雅解决消费异常兜底、重试收容与延时任务调度需求。 1.2 核心应用场景 在分布式与微服务架构中，RabbitMQ 通常充当解耦中枢与缓冲管道，广泛应用于以下业务场景：\n微服务间异步解耦：将核心业务与非核心流程（如注册成功后的邮件发送、短信通知、优惠券发放）解耦为异步事件驱动，降低服务间调用时延与系统雪崩风险。 流量削峰填谷：在秒杀大促、抢购活动等短时瞬时流量冲击下充当缓冲区，将瞬时高并发请求暂存至队列中，后端服务按照既定消费速率匀速处理，保护底层数据库。 延时任务与超时订单取消：利用队列的 x-message-ttl 配合死信交换机（DLX），或集成延时交换机插件（rabbitmq_delayed_message_exchange），实现未支付订单 30 分钟超时自动关单、定时状态同步等延迟处理场景。 异常重试与死信收容：对于消费端业务异常、重试耗尽或非法报文，自动转发至死信队列（DLQ）进行隔离归档，配合告警通知并提供人工排查补偿机制。 2. 环境准备 在开始集成开发之前，请确保本地或服务器已完成基础运行环境与服务的安装部署。\n2.1 依赖环境要求 JDK：JDK 1.8 或更高版本（推荐 JDK 17 / 21）。 Spring Boot：Spring Boot 2.7.x 或 3.x。 构建工具：Maven 3.6+ 或 Gradle 7+。 RabbitMQ 服务端：RabbitMQ 3.8+（需匹配 Erlang/OTP 对应运行时版本）。 2.2 启动本地 RabbitMQ 与服务管理 (1) Windows 服务运维命令（需以管理员权限打开 PowerShell）：\n1# 启动 RabbitMQ Windows 服务 2Start-Service rabbitmq 3 4# 查看 RabbitMQ 服务运行状态 5Get-Service RabbitMQ 6 7# 停止 RabbitMQ 服务 8net stop rabbitmq 9 10# 开启 RabbitMQ 服务 11net start rabbitmq (2) 基于 Docker 快速拉取并启动集成管理控制台的镜像节点：\n1# 拉取并启动带有 Web 管理插件的 RabbitMQ 容器 2docker run -d --name rabbitmq \\ 3 -p 5672:5672 \\ 4 -p 15672:15672 \\ 5 -e RABBITMQ_DEFAULT_USER=guest \\ 6 -e RABBITMQ_DEFAULT_PASS=guest \\ 7 rabbitmq:3-management Note RabbitMQ 服务默认监听 5672 端口（AMQP 客户端通信端口）与 15672 端口（Web 可视化管理控制台）。在浏览器访问 http://localhost:15672，默认登录账号为 guest，密码为 guest。\n3. RabbitMQ 集成 Spring Boot Spring AMQP（spring-rabbit）对底层的 amqp-client 进行了深度封装，提供了连接工厂 ConnectionFactory、拓扑构建工具（ExchangeBuilder、QueueBuilder、BindingBuilder）、模板化发送组件 RabbitTemplate 以及声明式监听注解 @RabbitListener。\n3.1 添加依赖 配置文件默认路径：pom.xml\n1\u0026lt;dependencies\u0026gt; 2 \u0026lt;!-- Spring Boot AMQP 核心起步依赖 --\u0026gt; 3 \u0026lt;dependency\u0026gt; 4 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 5 \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; 6 \u0026lt;/dependency\u0026gt; 7 \u0026lt;!-- Spring Boot Web 起步依赖（用于暴露 RESTful 调试接口） --\u0026gt; 8 \u0026lt;dependency\u0026gt; 9 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 10 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 11 \u0026lt;/dependency\u0026gt; 12 \u0026lt;!-- Lombok 依赖（用于消除实体模型样板代码） --\u0026gt; 13 \u0026lt;dependency\u0026gt; 14 \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; 15 \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; 16 \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; 17 \u0026lt;/dependency\u0026gt; 18\u0026lt;/dependencies\u0026gt; 3.2 配置 RabbitMQ 连接 配置文件默认路径：src/main/resources/application.yml\n1spring: 2 rabbitmq: 3 host: localhost # RabbitMQ 服务节点连接地址 4 port: 5672 # AMQP 通信端口 5 username: guest # 认证用户名 6 password: guest # 认证密码 7 virtual-host: / # 虚拟主机路径 8 publisher-confirm-type: correlated # 开启发布确认模式（correlated: 异步回调关联, simple: 同步等待, none: 关闭） 9 publisher-returns: true # 开启发布返回机制（消息无法路由到队列时触发回调） 10 template: 11 mandatory: true # 消息无法路由时强制回退给生产者，配合 publisher-returns 使用 12 listener: 13 simple: 14 acknowledge-mode: manual # 消息应答模式（manual: 手动应答, auto: 容器自动应答, none: 不应答） 15 prefetch: 10 # 每个消费者未确认消息的最大预取上限（削峰限流） 4. 生产者开发 在传统的原生 AMQP 开发中，生产者与消费者常常需要各自重复声明交换机与队列拓扑。在 Spring Boot 中，推荐通过配置类统一声明拓扑与发送模板，消除冗余并实现集中治理。\n4.1 集中拓扑配置与核心参数解析 (1) 编写集中配置类 RabbitMQConfig 声明交换机、队列与绑定关系，并定制 RabbitTemplate 与监听器容器工厂： 配置文件默认路径：src/main/java/com/example/rabbitmq/config/RabbitMQConfig.java\n1package com.example.rabbitmq.config; 2 3import org.springframework.amqp.core.AcknowledgeMode; 4import org.springframework.amqp.core.Binding; 5import org.springframework.amqp.core.BindingBuilder; 6import org.springframework.amqp.core.DirectExchange; 7import org.springframework.amqp.core.ExchangeBuilder; 8import org.springframework.amqp.core.Queue; 9import org.springframework.amqp.core.QueueBuilder; 10import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; 11import org.springframework.amqp.rabbit.connection.ConnectionFactory; 12import org.springframework.amqp.rabbit.core.RabbitTemplate; 13import org.springframework.context.annotation.Bean; 14import org.springframework.context.annotation.Configuration; 15 16@Configuration 17public class RabbitMQConfig { 18 19 public static final String EXCHANGE = \u0026#34;task.exchange\u0026#34;; 20 public static final String QUEUE = \u0026#34;task.queue\u0026#34;; 21 public static final String ROUTING_KEY = \u0026#34;task.key\u0026#34;; 22 23 /** 24 * 声明持久化 Direct 交换机 25 */ 26 @Bean 27 public DirectExchange taskExchange() { 28 return ExchangeBuilder.directExchange(EXCHANGE).durable(true).build(); 29 } 30 31 /** 32 * 声明持久化标准队列 33 */ 34 @Bean 35 public Queue taskQueue() { 36 return QueueBuilder.durable(QUEUE).build(); 37 } 38 39 /** 40 * 将队列通过路由键绑定到指定交换机 41 */ 42 @Bean 43 public Binding taskBinding() { 44 return BindingBuilder.bind(taskQueue()).to(taskExchange()).with(ROUTING_KEY); 45 } 46 47 /** 48 * 定制手动确认监听器容器工厂 49 */ 50 @Bean 51 public SimpleRabbitListenerContainerFactory manualAckListenerContainerFactory(ConnectionFactory connectionFactory) { 52 SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); 53 factory.setConnectionFactory(connectionFactory); 54 factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); 55 factory.setPrefetchCount(10); 56 return factory; 57 } 58 59 /** 60 * 定制 RabbitTemplate 模板组件 61 */ 62 @Bean 63 public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { 64 RabbitTemplate template = new RabbitTemplate(connectionFactory); 65 template.setMandatory(true); 66 return template; 67 } 68} (2) 架构演进与核心底层拓扑参数解析：\n原生写法缺陷与集中声明优势：\n在原生 AMQP 开发中，生产者与消费者常常借助工厂模式操纵中间件（ConnectionFactory → Connection → Channel），双方各自重复执行 exchangeDeclare、queueDeclare 与 queueBind。一旦两端声明参数（如 durable、x-message-ttl）不一致，Broker 将直接拒绝连接抛出 PRECONDITION_FAILED 协议错误。Spring Boot 通过 @Bean 托管拓扑生命周期，在应用启动建立连接时自动完成交换机与队列的幂等声明与绑定，职责清晰且杜绝了参数漂移。\n核心拓扑参数规约速查：\nAPI / 方法名 核心参数与属性键 规约说明与工程场景 exchangeDeclare alternate-exchange, x-delayed-type 声明备用交换机与延时交换机底层插件路由类型 queueDeclare x-message-ttl, x-expires 队列级消息存活时间（毫秒）与队列空闲删除周期 queueDeclare x-dead-letter-exchange, x-dead-letter-routing-key 关联死信交换机名称与转发时使用的死信路由键 queueDeclare x-max-length, x-overflow 队列最大堆积消息条数上限与溢出策略（如 reject-publish-dlx） queueDeclare x-queue-type, x-single-active-consumer 队列存储架构（classic / quorum / stream）与单活消费者配置 queueBind x-match headers 交换机匹配策略（all: 全匹配, any: 任一匹配） basicPublish mandatory, deliveryMode, expiration 路由失败退回标记、消息持久化模式（2）与单条消息 TTL 毫秒 basicQos prefetchCount, global 消费者未确认消息上限与限流生效范围（Channel / 消费者） 4.2 编写生产者服务与发送验证 (1) 创建生产者业务服务类，封装消息发送逻辑并挂载属性配置：\n1package com.example.rabbitmq.service; 2 3import com.example.rabbitmq.config.RabbitMQConfig; 4import org.slf4j.Logger; 5import org.slf4j.LoggerFactory; 6import org.springframework.amqp.core.MessageDeliveryMode; 7import org.springframework.amqp.rabbit.core.RabbitTemplate; 8import org.springframework.stereotype.Service; 9 10@Service 11public class TaskProducerService { 12 13 private static final Logger log = LoggerFactory.getLogger(TaskProducerService.class); 14 15 private final RabbitTemplate rabbitTemplate; 16 17 public TaskProducerService(RabbitTemplate rabbitTemplate) { 18 this.rabbitTemplate = rabbitTemplate; 19 } 20 21 /** 22 * 发送普通持久化消息 23 * 24 * @param message 待发送的消息内容 25 */ 26 public void sendMessage(String message) { 27 rabbitTemplate.convertAndSend( 28 RabbitMQConfig.EXCHANGE, 29 RabbitMQConfig.ROUTING_KEY, 30 message, 31 msg -\u0026gt; { 32 // 设置消息投递模式为持久化（deliveryMode = 2） 33 msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); 34 return msg; 35 } 36 ); 37 log.info(\u0026#34;消息发送成功: exchange={}, routingKey={}, body={}\u0026#34;, RabbitMQConfig.EXCHANGE, RabbitMQConfig.ROUTING_KEY, message); 38 } 39 40 /** 41 * 发送带有过期时间（TTL）的延时消息 42 * 43 * @param message 待发送的消息内容 44 * @param delayMillis 过期延迟毫秒数 45 */ 46 public void sendExpiringMessage(String message, long delayMillis) { 47 rabbitTemplate.convertAndSend( 48 RabbitMQConfig.EXCHANGE, 49 RabbitMQConfig.ROUTING_KEY, 50 message, 51 msg -\u0026gt; { 52 msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); 53 // 设置单条消息的 TTL 过期时间 54 msg.getMessageProperties().setExpiration(String.valueOf(delayMillis)); 55 return msg; 56 } 57 ); 58 log.info(\u0026#34;延时消息发送成功: delay={}ms, body={}\u0026#34;, delayMillis, message); 59 } 60} (2) 编写 REST Controller 控制器暴露触发入口：\n1package com.example.rabbitmq.controller; 2 3import com.example.rabbitmq.service.TaskProducerService; 4import org.springframework.web.bind.annotation.PostMapping; 5import org.springframework.web.bind.annotation.RequestMapping; 6import org.springframework.web.bind.annotation.RequestParam; 7import org.springframework.web.bind.annotation.RestController; 8 9@RestController 10@RequestMapping(\u0026#34;/rabbitmq\u0026#34;) 11public class TaskProducerController { 12 13 private final TaskProducerService taskProducerService; 14 15 public TaskProducerController(TaskProducerService taskProducerService) { 16 this.taskProducerService = taskProducerService; 17 } 18 19 /** 20 * HTTP POST 发送测试消息 21 */ 22 @PostMapping(\u0026#34;/send\u0026#34;) 23 public String sendMessage(@RequestParam String message) { 24 taskProducerService.sendMessage(message); 25 return \u0026#34;Message sent: \u0026#34; + message; 26 } 27 28 /** 29 * HTTP POST 发送延时过期测试消息 30 */ 31 @PostMapping(\u0026#34;/send-expiring\u0026#34;) 32 public String sendExpiringMessage(@RequestParam String message, @RequestParam(defaultValue = \u0026#34;10000\u0026#34;) long delayMillis) { 33 taskProducerService.sendExpiringMessage(message, delayMillis); 34 return \u0026#34;Expiring message sent (\u0026#34; + delayMillis + \u0026#34;ms): \u0026#34; + message; 35 } 36} (3) 使用 curl 发起 HTTP 请求测试消息发送：\n1curl -X POST \u0026#34;http://localhost:8080/rabbitmq/send?message=HelloRabbitMQ\u0026#34; 5. 消费者开发 Spring AMQP 提供了声明式注解 @RabbitListener，后台自动维护消费监听线程池、通道维护与异常重试。结合手动确认机制，可精准掌控消息应答时机。\n5.1 定义消费者逻辑 创建消费者组件，监听目标队列并基于 Channel 执行显式 basicAck 或 basicNack 应答：\n1package com.example.rabbitmq.consumer; 2 3import com.example.rabbitmq.config.RabbitMQConfig; 4import com.rabbitmq.client.Channel; 5import org.slf4j.Logger; 6import org.slf4j.LoggerFactory; 7import org.springframework.amqp.core.Message; 8import org.springframework.amqp.rabbit.annotation.RabbitListener; 9import org.springframework.stereotype.Component; 10 11import java.io.IOException; 12 13@Component 14public class TaskConsumer { 15 16 private static final Logger log = LoggerFactory.getLogger(TaskConsumer.class); 17 18 /** 19 * 监听指定队列并基于通道手动确认消息 20 * 21 * @param body 消息正文字符串 22 * @param message 原始消息对象（包含属性与 DeliveryTag） 23 * @param channel AMQP 信道对象（用于执行 ACK / NACK / REJECT） 24 */ 25 @RabbitListener( 26 queues = RabbitMQConfig.QUEUE, 27 containerFactory = \u0026#34;manualAckListenerContainerFactory\u0026#34; 28 ) 29 public void onMessage(String body, Message message, Channel channel) throws IOException { 30 long deliveryTag = message.getMessageProperties().getDeliveryTag(); 31 try { 32 log.info(\u0026#34;接收到 RabbitMQ 消息: tag={}, body={}\u0026#34;, deliveryTag, body); 33 // 执行业务处理... 34 35 // 手动应答：multiple=false 表示仅确认当前单条消息 36 channel.basicAck(deliveryTag, false); 37 log.info(\u0026#34;消息确认成功: tag={}\u0026#34;, deliveryTag); 38 } catch (Exception e) { 39 log.error(\u0026#34;业务处理异常，拒绝消息: tag={}, error={}\u0026#34;, deliveryTag, e.getMessage(), e); 40 // 拒绝消息：multiple=false, requeue=false 不重新入队（直接丢弃或进入死信队列） 41 channel.basicNack(deliveryTag, false, false); 42 } 43 } 44} 5.2 测试消费者 (1) 使用 curl 向 /rabbitmq/send 发起消息发送请求：\n1curl -X POST \u0026#34;http://localhost:8080/rabbitmq/send?message=HelloRabbitMQConsumer\u0026#34; (2) 观察 Spring Boot 应用控制台输出，确认消息被正常接收并执行手动 ACK 确认：\n1接收到 RabbitMQ 消息: tag=1, body=HelloRabbitMQConsumer 2消息确认成功: tag=1 6. RabbitMQ 高级功能 在生产实践中，保障消息系统的高可靠、高可用与弹性调度是架构设计的核心。RabbitMQ 提供了完善的死信交换机（DLX）体系、超时过期策略以及工程化可靠性机制。\n6.1 死信队列（DLX）与延迟任务 死信交换机（Dead Letter Exchange，简称 DLX）是 RabbitMQ 处理异常报文、实现延迟队列与故障兜底的关键组件。\n(1) 触发消息进入死信队列的四种核心场景：\n消费者否定应答且不重回队列：消费者显式调用 channel.basicReject 或 channel.basicNack 且将 requeue 设置为 false。 消息或队列 TTL 到期：消息在队列中存活时间超过预设的 x-message-ttl 毫秒数，或单条消息的 expiration 到期。 队列容量超限：队列中积压的消息数量达到 x-max-length，或总字节数达到 x-max-length-bytes，根据 x-overflow 溢出策略丢弃或转发头部消息。 重新入队失败：由于队列已被删除、限流拒绝等极端原因导致消息重新入队操作失败。 Tip 针对消息级设置 TTL（expiration），RabbitMQ 仅在消息流转到队列头部即将被消费时才会检查是否过期。若队列堆积严重或无消费活动，过期消息可能长期滞留在正常队列中。生产环境推荐在声明业务队列时通过 x-message-ttl 参数配置统一队列存活时间，确保过期瞬间即可被 Broker 自动投递到死信交换机。\n(2) 死信基础设施与业务队列配置： 创建专门的死信配置类 DeadLetterConfig：\n1package com.example.rabbitmq.config; 2 3import org.springframework.amqp.core.Binding; 4import org.springframework.amqp.core.BindingBuilder; 5import org.springframework.amqp.core.DirectExchange; 6import org.springframework.amqp.core.ExchangeBuilder; 7import org.springframework.amqp.core.Queue; 8import org.springframework.amqp.core.QueueBuilder; 9import org.springframework.context.annotation.Bean; 10import org.springframework.context.annotation.Configuration; 11 12import java.util.HashMap; 13import java.util.Map; 14 15@Configuration 16public class DeadLetterConfig { 17 18 public static final String BUSINESS_EXCHANGE = \u0026#34;biz.exchange\u0026#34;; 19 public static final String BUSINESS_QUEUE = \u0026#34;biz.queue\u0026#34;; 20 public static final String BUSINESS_ROUTING_KEY = \u0026#34;biz.key\u0026#34;; 21 22 public static final String DLX_EXCHANGE = \u0026#34;dlx.exchange\u0026#34;; 23 public static final String DLX_QUEUE = \u0026#34;dlx.queue\u0026#34;; 24 public static final String DLX_ROUTING_KEY = \u0026#34;dlx.key\u0026#34;; 25 26 /** 27 * 1. 声明死信交换机 28 */ 29 @Bean 30 public DirectExchange deadLetterExchange() { 31 return ExchangeBuilder.directExchange(DLX_EXCHANGE).durable(true).build(); 32 } 33 34 /** 35 * 2. 声明死信队列 36 */ 37 @Bean 38 public Queue deadLetterQueue() { 39 return QueueBuilder.durable(DLX_QUEUE).build(); 40 } 41 42 /** 43 * 3. 绑定死信队列到死信交换机 44 */ 45 @Bean 46 public Binding deadLetterBinding() { 47 return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DLX_ROUTING_KEY); 48 } 49 50 /** 51 * 4. 声明正常业务队列，并配置死信转发参数 52 */ 53 @Bean 54 public Queue businessQueue() { 55 Map\u0026lt;String, Object\u0026gt; args = new HashMap\u0026lt;\u0026gt;(); 56 // 声明死信交换机 57 args.put(\u0026#34;x-dead-letter-exchange\u0026#34;, DLX_EXCHANGE); 58 // 声明死信路由键 59 args.put(\u0026#34;x-dead-letter-routing-key\u0026#34;, DLX_ROUTING_KEY); 60 // 声明队列级消息存活时间（TTL = 30000ms，即 30 秒） 61 args.put(\u0026#34;x-message-ttl\u0026#34;, 30000); 62 // 声明队列最大消息条数 63 args.put(\u0026#34;x-max-length\u0026#34;, 10000); 64 // 声明超限溢出策略：拒绝发布并转发到死信 65 args.put(\u0026#34;x-overflow\u0026#34;, \u0026#34;reject-publish-dlx\u0026#34;); 66 67 return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build(); 68 } 69 70 /** 71 * 5. 声明业务交换机 72 */ 73 @Bean 74 public DirectExchange businessExchange() { 75 return ExchangeBuilder.directExchange(BUSINESS_EXCHANGE).durable(true).build(); 76 } 77 78 /** 79 * 6. 绑定业务队列到业务交换机 80 */ 81 @Bean 82 public Binding businessBinding() { 83 return BindingBuilder.bind(businessQueue()).to(businessExchange()).with(BUSINESS_ROUTING_KEY); 84 } 85} (3) 编写死信队列消费者监听器：\n1package com.example.rabbitmq.consumer; 2 3import com.example.rabbitmq.config.DeadLetterConfig; 4import com.rabbitmq.client.Channel; 5import org.slf4j.Logger; 6import org.slf4j.LoggerFactory; 7import org.springframework.amqp.core.Message; 8import org.springframework.amqp.rabbit.annotation.RabbitListener; 9import org.springframework.stereotype.Component; 10 11import java.io.IOException; 12 13@Component 14public class DeadLetterConsumer { 15 16 private static final Logger log = LoggerFactory.getLogger(DeadLetterConsumer.class); 17 18 /** 19 * 监听死信队列，处理超时未支付、业务异常与最终兜底消息 20 */ 21 @RabbitListener( 22 queues = DeadLetterConfig.DLX_QUEUE, 23 containerFactory = \u0026#34;manualAckListenerContainerFactory\u0026#34; 24 ) 25 public void onDeadLetterMessage(String body, Message message, Channel channel) throws IOException { 26 long deliveryTag = message.getMessageProperties().getDeliveryTag(); 27 try { 28 log.warn(\u0026#34;死信队列收到待补偿消息: tag={}, body={}\u0026#34;, deliveryTag, body); 29 // 执行补偿处理逻辑（如超时关单、释放库存、告警归档）... 30 31 channel.basicAck(deliveryTag, false); 32 } catch (Exception e) { 33 log.error(\u0026#34;死信补偿处理异常: tag={}, error={}\u0026#34;, deliveryTag, e.getMessage(), e); 34 // 补偿仍失败时拒绝确认，交由人工运维或死信二次归档 35 channel.basicNack(deliveryTag, false, false); 36 } 37 } 38} 6.2 消息可靠性与工程化实践 在金融级与高可用分布式系统中，保障消息全链路不丢失与消费幂等性是核心工程准则。\n(1) 生产端全链路防丢（Confirm 与 Return 机制）：\nPublisher Confirm 发布确认：设置 publisher-confirm-type 为 correlated，在 RabbitTemplate 中配置 ConfirmCallback 异步监听回调，确认 Broker 已成功持久化落盘。 Publisher Return 退回机制：当消息发送至交换机但无法依据 Routing Key 路由至任何队列时，若开启 mandatory 为 true 并配置 ReturnsCallback 回调，Broker 会将无法路由的消息原路返回给生产者，杜绝消息静默丢失。 (2) 消费端幂等性与流量治理：\n至少一次（At-Least-Once）语义与幂等设计：RabbitMQ 遵循“至少一次”投递保证，在网络抖动或服务重启时可能发生重复投递。消费端必须采用“业务唯一键”（如订单号 orderId）结合 Redis 分布式锁、MySQL 去重表（防重唯一索引）实现消费幂等性。 QoS Prefetch 限流调优：在手动 ACK 模式下合理设置 prefetchCount（通常推荐 10 到 50），避免单个消费节点拉取过多消息导致内存溢出，实现按需匀速拉取与平滑削峰。 (3) 消息顺序性与高可用队列架构：\n局部有序保障：RabbitMQ 仅保证“单队列、单消费者”下的绝对顺序性。若多消费者并发读取同一队列，消息将因线程调度差异产生乱序。对强保序场景，需根据业务分区键哈希路由到特定专属队列，或配置 x-single-active-consumer 启用单活消费者模式。 Quorum Queue 仲裁队列高可用：在生产集群中，逐步淘汰基于镜像队列（Mirrored Queues）的旧模式，采用基于 Raft 共识协议的仲裁队列（Quorum Queues，声明参数 x-queue-type: quorum），提供更强的多副本一致性与故障自动转移能力。 事务与分布式补偿模式：对于涉及跨库与第三方接口的核心链路，结合本地消息表（Transactional Outbox）与 Saga 补偿编排模式，确保本地数据库事务与消息发送的最终一致性。 7. 总结 Spring AMQP 为 Spring Boot 应用接入 RabbitMQ 提供了高度成熟、开箱即用的组件支持，兼顾了消息流转的灵活性与严苛的可靠性要求。\n7.1 核心功能回顾 (1) 集中拓扑声明：通过 @Configuration 配置类统一托管 Exchange、Queue 与 Binding，消除原生客户端重复声明样板代码，与 Spring 容器生命周期解耦协同。\n(2) 生产与手动确认：基于 RabbitTemplate 模板发送持久化与延时消息，消费端利用 @RabbitListener 配合 manualAckListenerContainerFactory 与 channel.basicAck 实现确定性消费回执。\n(3) 死信与弹性流转：通过 x-dead-letter-exchange、x-dead-letter-routing-key 与 TTL 机制无缝搭建死信与延时调度闭环，支撑异常补偿与业务解耦。\n7.2 生产落地建议 确认机制与限流策略：生产环境必须严格启用生产者 Publisher Confirm 与消费者手动 ACK，并针对各消费节点性能精准调优 prefetchCount，杜绝消息丢失与消费堆积。 死信重试与兜底收容：严禁在消费端异常时盲目将 requeue 设为 true 触发死循环消费；务必配置死信队列（DLQ）或阶梯延时重试策略，配合告警监控实现全流程闭环。 幂等性与高可用架构：消费端必须基于业务唯一键建立防重幂等机制；集群部署优先采用基于 Raft 协议的 Quorum Queue 仲裁队列，保障极端节点宕机时数据多副本强一致。 ","date":"2026-04-23","tags":["RabbitMQ","SpringBoot"]},{"title":"LLM Wiki：面向 Agent 工程的知识编译层","permalink":"/posts/llm-wiki-%E9%9D%A2%E5%90%91agent%E5%B7%A5%E7%A8%8B%E7%9A%84%E7%9F%A5%E8%AF%86%E7%BC%96%E8%AF%91%E5%B1%82/","summary":"如果只先记住一个结论，那么可以记这句：\nLLM wiki 不是“让 AI 帮你整理笔记”，而是把项目知识从原始资料提升为 Agent 可消费、可引用、可演化的知识中间层。\n这个判断的关键在于，问题并不只是“资料有没有被存下来”，而是“资料是否已经被整理成 Agent 可以稳定工作的形状”。在这件事上，RAG 和 LLM wiki 处理的根本不是同一层问题。Andrej Karpathy 提出的 llm-wiki.md 范式 给出的也不是一个笔记软件教程，而是一种把知识从原始材料编译成可维护工作上下文的思路。\n本文想讨论的是三个问题：\nLLM wiki 到底是什么 它和 RAG 的边界到底在哪里 如果要把它落到工程实践里，最小实现应该长什么样 1. Agent 真正缺的不是“更多文档”，而是知识编译层 很多仓库并不缺文档。\nREADME、ADR、issue、PR、聊天记录、代码注释、发布记录，甚至临时会议纪要，往往已经足够多。问题在于，这些材料大多是围绕“人怎么理解系统”自然长出来的，而不是围绕“Agent 应该如何进入系统、命名概念、定位事实、拼装上下文”组织出来的。\n这会带来几个很典型的摩擦：\n资料很多，但没有稳定入口 术语存在，但定义分散甚至漂移 同一个问题需要每次重新检索和重新拼接上下文 原始资料适合追溯事实，不适合直接充当工作层 所以 LLM wiki 试图解决的，不是“把一堆东西再存一遍”，而是另一件更具体的事：\n它的价值不在存储，而在预编译；不在收集，而在把知识变成可持续复用的中间表示。\nKarpathy 在原始 gist 里把这件事描述得很清楚：系统并不直接依赖原始材料回答问题，而是先把原始材料经过整理，沉淀成持续更新的 wiki 层，并由 schema 约束其结构与工作流，再把它作为后续查询和工作的主上下文层。来源\n2. 与 RAG 的绝对区分：一个是运行时召回，一个是离线知识编译 很多讨论会把 LLM wiki 和 RAG 混在一起，但这两者并不是同义替换。\nRAG 的经典定义来自 Lewis 等人的论文 Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks：模型在生成前，先从外部知识源检索相关内容，再把检索结果并入生成过程。论文链接\n","content":"如果只先记住一个结论，那么可以记这句：\nLLM wiki 不是“让 AI 帮你整理笔记”，而是把项目知识从原始资料提升为 Agent 可消费、可引用、可演化的知识中间层。\n这个判断的关键在于，问题并不只是“资料有没有被存下来”，而是“资料是否已经被整理成 Agent 可以稳定工作的形状”。在这件事上，RAG 和 LLM wiki 处理的根本不是同一层问题。Andrej Karpathy 提出的 llm-wiki.md 范式 给出的也不是一个笔记软件教程，而是一种把知识从原始材料编译成可维护工作上下文的思路。\n本文想讨论的是三个问题：\nLLM wiki 到底是什么 它和 RAG 的边界到底在哪里 如果要把它落到工程实践里，最小实现应该长什么样 1. Agent 真正缺的不是“更多文档”，而是知识编译层 很多仓库并不缺文档。\nREADME、ADR、issue、PR、聊天记录、代码注释、发布记录，甚至临时会议纪要，往往已经足够多。问题在于，这些材料大多是围绕“人怎么理解系统”自然长出来的，而不是围绕“Agent 应该如何进入系统、命名概念、定位事实、拼装上下文”组织出来的。\n这会带来几个很典型的摩擦：\n资料很多，但没有稳定入口 术语存在，但定义分散甚至漂移 同一个问题需要每次重新检索和重新拼接上下文 原始资料适合追溯事实，不适合直接充当工作层 所以 LLM wiki 试图解决的，不是“把一堆东西再存一遍”，而是另一件更具体的事：\n它的价值不在存储，而在预编译；不在收集，而在把知识变成可持续复用的中间表示。\nKarpathy 在原始 gist 里把这件事描述得很清楚：系统并不直接依赖原始材料回答问题，而是先把原始材料经过整理，沉淀成持续更新的 wiki 层，并由 schema 约束其结构与工作流，再把它作为后续查询和工作的主上下文层。来源\n2. 与 RAG 的绝对区分：一个是运行时召回，一个是离线知识编译 很多讨论会把 LLM wiki 和 RAG 混在一起，但这两者并不是同义替换。\nRAG 的经典定义来自 Lewis 等人的论文 Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks：模型在生成前，先从外部知识源检索相关内容，再把检索结果并入生成过程。论文链接\nRAG 的重点天然落在运行时：\n找不找得到 召回的片段准不准 噪音高不高 排序和相关性好不好 而 LLM wiki 的关注点并不在“这一问检索到什么”，而在“平时有没有把知识整理成一个稳定的工作骨架”。\n这里可以直接用一张表来区分说明：\n维度 RAG LLM wiki 主要工作阶段 运行时 离线持续维护 核心目标 找回相关片段 组织成稳定知识骨架 输入形态 原始文档切片 经整理后的主题页、术语页、决策页 主要优化对象 召回率、相关性 可导航性、可引用性、一致性 失败模式 没召回 / 召回噪音 结构陈旧 / 术语漂移 / 入口失效 更像什么 检索系统 知识编译系统 一句话说，RAG 更像是在 query time 做“找”，而 LLM wiki 更像是在 authoring time 做“整理”和“压缩”。\nKarpathy 在 gist 里讲得很直接：不要只在 query time 从 raw documents 里临时捞片段，而是让 LLM 持续维护一个位于你和原始资料之间的 persistent wiki。这个动作本质上就是先把知识整理成稳定结构，再用这层结构服务后续工作，而不是每次都直接在原始材料里临时搜索。来源\n这也是为什么更适合把 LLM wiki 理解成知识层的 build step，而不是检索层的补丁。\n3. 关键不在载体，而在知识形状 LLM wiki 很容易被误解成“用 Obsidian 记笔记”“用 Notion 建文档”“用 Hugo 发布知识库”。这些都可以是载体，但都不是重点。\n哪怕都是 Markdown 文件，面向博客的写法和面向 Agent 的写法也完全不同。\n普通文档更关心的是：\n人类读起来顺不顺 章节结构清不清楚 能不能从头到尾顺着看 而 LLM wiki 还必须满足额外约束：\n页面能不能被独立引用 概念有没有稳定命名 页面之间有没有显式关系 知识能不能被逐步重写，而不是只追加 Obsidian 官方文档一直强调链接、反向链接、图谱和知识库组织能力，例如 internal links、backlinks、graph view 这些能力，本质上都是在增强“知识节点之间的关系可见性”。官方帮助文档 这和 LLM wiki 的思路是相通的，但 LLM wiki 还会再向前一步：它不只要求可读，还要求这些页面天然适合被模型抽取、拼接、引用和重写。\n所以问题不在“你是不是用了 wiki 工具”，而在“你的页面是不是构成了稳定知识节点”。\n4. 一个工程可用的 LLM wiki，最小结构其实只有三层 如果严格按照 Karpathy 的原始范式来看，最小结构应该收缩成三层，而不是先发明一套很重的目录体系。\n最小结构就是：\n1raw/ 2schema 3wiki/ 这里三层各自的职责非常明确：\nraw/：原始资料层，放文章、论文、图片、数据文件等。它是事实来源层，默认不可变，LLM 只读不改。 schema：规则层，通常就是 AGENTS.md、CLAUDE.md 这类文件。它负责告诉 LLM 这套 wiki 怎么组织、有哪些命名约定、如何 ingest、如何 query、如何 lint。 wiki/：知识层，由 LLM 生成和维护的 markdown 页面集合。摘要页、实体页、概念页、比较页、总览页、综合分析页都在这里。 这是原始 gist 里最重要的架构判断之一：原始资料、知识页、维护规则，必须分层。来源\n继续往下拆，这三层里真正决定上限的，往往反而不是 wiki/，而是 raw/ 和 schema。\n对 raw/，重点应该放在数据清洗和人工审核上。无论是自己总结的随记、技术文档、学习笔记、心得，还是外部文章、PDF、Word、Markdown、截图、数据表，只要准备进入这套系统，都更适合先被视为原始知识材料，而不是直接可用知识。这里最怕的是脏数据、重复信息、低质量摘录、未经确认的引用、格式噪音和上下文残缺。因为 raw/ 作为最底层，决定了后面所有 synthesis 的上界；如果地基层就松，后面的 wiki 写得再漂亮，也只是把偏差结构化。\n所以，raw/ 这一层不是“收集得越多越好”，而是“清洗得越干净越好”。进入 raw/ 的材料，应该尽量经过明确整理和人工审核，至少保证来源可追溯、内容可读、格式可处理、重复可识别、噪音可控。\n而 schema 的重要性，很多时候甚至会被低估。schema 不只是一个说明 LLM 怎么写 wiki 的配置文件，它本质上是在约束 LLM 的边界，控制每一轮循环的误差，规定它如何 ingest、如何建立 cross-references、如何回写、如何做 lint、如何处理冲突、何时该保守、何时可以改写。它真正解决的问题不是“格式统一”，而是“这套系统如何在多轮迭代中维持 cross-reference quality 和整体 consistency”。\n再说得具体一点，schema 要回答的其实是这些问题：\n每一轮 ingest 的标准动作是什么 更新一个 source 时允许触碰哪些页面 新信息和旧结论冲突时应该如何处理 query 产出的分析什么时候允许回写进 wiki 哪些页面必须保守，哪些页面可以积极重构 lint 应该检查哪些一致性问题 如果这些规则不清楚，LLM 每一轮虽然都在“工作”，但系统的熵其实是在增加的。旧结论和新增信息之间的冲突、命名漂移、cross-references 退化、页间不一致，都会慢慢侵蚀整个 wiki 的可信度。可以把 schema 理解成一套控制循环误差和管理新增熵的机制，它的目标不是让 LLM 更自由，而是让它更稳定。\n在这三层之下，index.md 和 log.md 是两个非常关键的特殊文件：\nindex.md 是内容导向的目录，帮助 LLM 和人快速找到相关页面 log.md 是时间导向的追加日志，记录 ingest、query、lint 等动作 再往下扩展成 entities/、concepts/、systems/ 之类的目录，当然可以做，但那已经是某个具体领域的实例化，不是这个范式本身的最小要求。\n所以如果只讲核心，可以把它概括成一句话：\nLLM wiki 的最小骨架不是一堆分类目录，而是 raw、schema、wiki 三层分离。\n5. 页面不是“文档”，而是知识节点 这是 LLM wiki 和普通知识库教程最不一样的地方。\n在普通文档系统里，一页文章写得顺、写得全，通常就已经够用了。但在 LLM wiki 里，一页首先是一个可以被引用、组合和追踪的知识节点，而且这页默认是由 LLM 持续维护的，不是主要靠人手工慢慢改出来的。\n落到写法上，页面最好尽量满足这些原则：\n一页只承载一个稳定主题 页面标题必须是可复用的引用名 先写定义、边界、关系，再写叙述性背景 显式写出相关页、前置概念、依赖系统和常见误解 让页面可以被独立抽出，而不是必须通读整篇才能理解 差页面和好页面的区别通常很直接。\n差页面往往是：\n流水账 混合多个主题 没有链接 没有边界 读完之后仍然不知道它在知识图里属于哪里 好页面通常具备这些特征：\n主题单一 命名稳定 关系显式 可以被独立引用 被抽取进上下文后仍然保留语义完整性 这也是为什么 LLM wiki 里的页面更像“可组合单元”，而不是“仅供线性阅读的章节”。至于 wiki 本身为什么应该是 persistent、compounding、interlinked artifact，Karpathy 在原始 gist 里已经讲得很完整，这里不再重复扩写，重点只放在这套结构真正落地时，哪些地方最容易失真。\n6. Agent 工作流闭环：Ingest、Query、Lint、Rewrite 如果只把 LLM wiki 当成一个静态文件夹，它很快就会失效。真正有用的是把它变成一条持续运行的知识维护流水线。\n一个比较自然的最小闭环是四步：\n6.1 Ingest 从 raw/ 提取事实，更新 wiki 页面，而不是把原始资料直接塞进上下文。\n这一步做的不是复制粘贴，而是：\n抽概念 归术语 拆主题 建链接 标记来源 按照 gist 的描述，一个 source 进来之后，LLM 不只是写一页摘要，还可能同时更新 index.md、相关实体页、概念页，以及 log.md。也就是说，ingest 不是“收录一份文件”，而是“把新知识并入现有 wiki”。来源\n6.2 Query 后续回答、分析、规划，优先基于 wiki 层展开；原始资料只作为追溯证据层。\n这样做的好处是，上下文不再是一次次临时拼装的碎片，而是来自同一套稳定骨架。并且 gist 里另一个很重要的点是，好的 query 结果不该只留在聊天记录里，它本身也可以回写成新的 wiki 页面，让分析结果继续沉淀。来源\n6.3 Lint 定期检查结构健康度，例如：\n断链 重复页 命名冲突 过时页 空入口 孤儿页 已经被新来源推翻但没更新的旧结论 被频繁提到但还没有单独页面的重要概念 Karpathy 在 gist 里专门把 lint 作为一个动作提出，这一点非常关键，因为知识系统和代码系统一样，也会腐化。来源\n6.4 Rewrite 随着任务推进，持续重写结构，让 wiki 跟着系统演化，而不是无限追加新页面。\n真正的维护不是“越写越多”，而是“该折叠的折叠，该合并的合并，该改名的改名”。\n所以这套东西的关键判断应该是：\nLLM wiki 不是静态知识库，而是一条持续运行的知识维护流水线。\n而这条流水线能不能长期稳定运转，最终看的还是两件事：raw/ 有没有把好底层质量关，schema 有没有把好多轮循环的一致性关。\n7. 为什么这套东西在今天才真正可行 如果把时间拉长一点看，LLM wiki 并不是完全凭空冒出来的新想法。\nVannevar Bush 在 1945 年的文章 As We May Think 里提出过著名的 memex 概念，强调通过关联路径组织个人知识，而不是只靠线性归档。原文链接\n这个方向并不新，真正长期卡住的，是维护成本。\n过去的问题不是没人想到链接化知识系统，而是：\n资料整理太费人 命名统一太费人 跨页归并太费人 持续重写太费人 而 LLM 真正改变的一点，不只是“能生成文字”，而是它显著压低了这些知识维护动作的边际成本：摘要、归并、重写、抽术语、补链接、生成索引页、维护跨页一致性，这些以前昂贵的操作，现在终于可以低成本持续执行。\n所以 LLM wiki 不是旧 wiki 的换皮，而是旧知识系统在新维护成本条件下的再成立。Karpathy 的 gist 之所以让人眼前一亮，也正是因为它把这件事从模糊愿景落到了可执行结构上。来源\n8. 失败模式与反模式：它什么时候会退化成垃圾层 任何知识系统只要开始维护，就会面临腐化问题。LLM wiki 也不例外。\n最常见的失败模式大概有这几类：\n8.1 不区分 raw 和 wiki 如果什么都往 wiki/ 里塞，而不是先把原始资料留在 raw/、把整理后的知识放进 wiki/，信息密度很快就会失控，后面所有页面都会变成冗长噪音。\n8.2 没有术语页 同一个概念今天叫 workflow state，明天叫 task context，后天又叫 execution memory，Agent 没法稳定对齐，知识骨架会从内部崩掉。\n8.3 当前任务态和长期知识混写 任务临时结论、调试痕迹、一次性 workaround，如果直接写进稳定概念页，后面会持续污染判断。\n8.4 页面之间没有显式关系 只有孤立页面，没有 related pages、前置概念、依赖系统，这样的知识库看起来很多，实际仍然不可导航。\n8.5 只增不改 知识库一旦只追加、不折叠、不重写，就会像没有重构的代码一样越来越不可用。\n8.6 把 schema 当装饰，把 wiki 当真理源 如果 schema 没有持续演化，LLM 很快就会退化成一个泛化聊天机器人，而不是一个有纪律的 wiki maintainer。更具体地说，一旦没有人去约束每轮循环的动作、更新范围、冲突处理和链接策略，系统表面上看起来还在增长，实际上 cross-reference quality、inbound links 和跨页 consistency 已经在悄悄下降。反过来，如果把 wiki 当成最终事实本身，也会出问题。代码、测试、运行行为、生产数据，依旧是更高优先级的事实源。\n也就是说，LLM wiki 最危险的状态不是“没有写”，而是“写得很多，但结构已经失真”。\n9. 适用场景：它是给长期系统准备的 LLM wiki 并不是任何项目都值得上。\n它更适合这些场景：\n长期维护代码库 多模块系统 研究型或架构型项目 高频使用 Agent 参与设计、编码、调试、审查的工作流 它不太适合这些场景：\n一次性脚本 临时 demo 变化太快且没有沉淀意愿的项目 对上下文一致性没有要求的低复杂度工作 简单说，如果系统还没有复杂到需要知识骨架，那么引入 LLM wiki 只会制造额外维护成本。\n工程基础设施从来都不是越多越好，而是只在复杂度真正出现时再引入。\n10. 把它理解成知识层的 build step 如果要给全文做一个最短总结，可以把 LLM wiki 理解成知识层的 build step。\n它不是文档层的附件，也不是一个“更会记笔记”的工具，而是一层位于原始资料和运行时检索之间的知识编译层：\n原始资料是真实来源 schema 是维护规则 RAG 是运行时检索机制 LLM wiki 是两者之间的知识编译层 真正有区分度的地方，从来不是“也有知识库”，而是是否把知识组织成了 Agent 可以稳定工作的形状。\n当 Agent 开始参与真实工程工作流时，知识库就不再只是备忘录，而会逐渐演化成上下文基础设施。\n参考资料 Andrej Karpathy, LLM Wiki gist Patrick Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks Vannevar Bush, As We May Think Obsidian, Official Help Yunfan Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey ","date":"2026-04-04","tags":["wiki","agent","LLM","RAG","knowledge"]},{"title":"LeetCode Hot 100 题解合集","permalink":"/posts/leetcode-hot-100-%E9%A2%98%E8%A7%A3%E5%90%88%E9%9B%86/","summary":"索引 哈希 双指针 滑动窗口 子串 普通数组 矩阵 链表 二叉树 图论 回溯法 二分查找 栈 堆 贪心算法 动态规划 多维动态规划 技巧 哈希 难度 题号 题目 题解 简单 1 两数之和 题解 中等 49 字母异位词分组 题解 中等 128 最长连续序列 题解 双指针 难度 题号 题目 题解 简单 283 移动零 题解 中等 11 盛最多水的容器 题解 中等 15 三数之和 题解 滑动窗口 难度 题号 题目 题解 中等 3 无重复字符的最长子串 题解 中等 438 找到字符串中所有字母异位词 题解 子串 难度 题号 题目 题解 中等 560 和为 K 的子数组 题解 普通数组 难度 题号 题目 题解 中等 53 最大子数组和 题解 中等 56 合并区间 题解 中等 189 轮转数组 题解 中等 238 除自身以外数组的乘积 题解 矩阵 难度 题号 题目 题解 中等 73 矩阵置零 题解 中等 54 螺旋矩阵 题解 中等 48 旋转图像 题解 中等 240 搜索二维矩阵 II 题解 链表 难度 题号 题目 题解 简单 160 相交链表 题解 简单 206 反转链表 题解 简单 234 回文链表 题解 简单 141 环形链表 题解 简单 21 合并两个有序链表 题解 中等 142 环形链表 II 题解 中等 2 两数相加 题解 中等 19 删除链表的倒数第 N 个结点 题解 中等 24 两两交换链表中的节点 题解 中等 148 排序链表 题解 中等 146 LRU 缓存 题解 中等 138 随机链表的复制 题解 二叉树 难度 题号 题目 题解 简单 94 二叉树的中序遍历 题解 简单 104 二叉树的最大深度 题解 简单 226 翻转二叉树 题解 简单 543 二叉树的直径 题解 简单 108 将有序数组转换为二叉搜索树 题解 中等 102 二叉树的层序遍历 题解 中等 98 验证二叉搜索树 题解 中等 230 二叉搜索树中第 K 小的元素 题解 中等 199 二叉树的右视图 题解 中等 114 二叉树展开为链表 题解 中等 105 从前序与中序遍历序列构造二叉树 题解 中等 437 路径总和 III 题解 中等 236 二叉树的最近公共祖先 题解 图论 难度 题号 题目 题解 中等 200 岛屿数量 题解 中等 994 腐烂的橘子 题解 中等 207 课程表 题解 中等 208 实现 Trie (前缀树) 题解 回溯法 难度 题号 题目 题解 中等 46 全排列 题解 中等 78 子集 题解 中等 17 电话号码的字母组合 题解 中等 39 组合总和 题解 中等 22 括号生成 题解 中等 79 单词搜索 题解 中等 131 分割回文串 题解 二分查找 难度 题号 题目 题解 简单 35 搜索插入位置 题解 中等 74 搜索二维矩阵 题解 中等 34 在排序数组中查找元素的第一个和最后一个位置 题解 中等 33 搜索旋转排序数组 题解 中等 153 寻找旋转排序数组中的最小值 题解 栈 难度 题号 题目 题解 简单 20 有效的括号 题解 中等 155 最小栈 题解 中等 394 字符串解码 题解 堆 难度 题号 题目 题解 中等 215 数组中的第K个最大元素 题解 中等 347 前 K 个高频元素 题解 贪心算法 难度 题号 题目 题解 简单 121 买卖股票的最佳时机 题解 中等 55 跳跃游戏 题解 中等 45 跳跃游戏 II 题解 中等 763 划分字母区间 题解 动态规划 难度 题号 题目 题解 简单 70 爬楼梯 题解 简单 118 杨辉三角 题解 中等 198 打家劫舍 题解 中等 279 完全平方数 题解 中等 322 零钱兑换 题解 中等 139 单词拆分 题解 中等 300 最长递增子序列 题解 中等 152 乘积最大子数组 题解 中等 416 分割等和子集 题解 多维动态规划 难度 题号 题目 题解 中等 62 不同路径 题解 中等 64 最小路径和 题解 中等 5 最长回文子串 题解 中等 1143 最长公共子序列 题解 中等 72 编辑距离 题解 技巧 难度 题号 题目 题解 简单 136 只出现一次的数字 题解 简单 169 多数元素 题解 中等 75 颜色分类 题解 中等 31 下一个排列 题解 中等 287 寻找重复数 题解 哈希 方法代码 核心作用 代码示例 / 返回值 hashmap.keys() 获取字典中所有的键 (Key) [\u0026lsquo;aet\u0026rsquo;, \u0026lsquo;ant\u0026rsquo;] hashmap.values() 获取字典中所有的值 (Value) [[\u0026rsquo;eat\u0026rsquo;, \u0026rsquo;tea\u0026rsquo;], [\u0026rsquo;tan\u0026rsquo;]] hashmap.items() 同时获取所有的键和值，常用于循环 for key, val in hashmap.items(): hashmap.get(key, default) 安全获取值，若键不存在则返回设定的默认值 hashmap.get(\u0026ldquo;aet\u0026rdquo;, []) hashmap[key] = value 新增或修改指定的键值对 hashmap[\u0026ldquo;aet\u0026rdquo;] = [\u0026ldquo;eat\u0026rdquo;] hashmap.pop(key) 删除指定的键，并返回它对应的值 words = hashmap.pop(\u0026ldquo;aet\u0026rdquo;) hashmap.clear() 清空字典里的所有内容 hashmap.clear() hashmap.update(dict2) 将另一个字典的数据合并到当前字典中 hashmap.update(other_dict) key in hashmap 判断某个键是否在字典中 (返回布尔值) if \u0026ldquo;aet\u0026rdquo; in hashmap: len(hashmap) 统计并返回字典中键值对的总数量 count = len(hashmap) 1. 两数之和 题目描述 给定一个整数数组nums和一个整数目标值target，请你在该数组中找出和为目标值target的那两个整数，并返回它们的数组下标。 你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。\n","content":"索引 哈希 双指针 滑动窗口 子串 普通数组 矩阵 链表 二叉树 图论 回溯法 二分查找 栈 堆 贪心算法 动态规划 多维动态规划 技巧 哈希 难度 题号 题目 题解 简单 1 两数之和 题解 中等 49 字母异位词分组 题解 中等 128 最长连续序列 题解 双指针 难度 题号 题目 题解 简单 283 移动零 题解 中等 11 盛最多水的容器 题解 中等 15 三数之和 题解 滑动窗口 难度 题号 题目 题解 中等 3 无重复字符的最长子串 题解 中等 438 找到字符串中所有字母异位词 题解 子串 难度 题号 题目 题解 中等 560 和为 K 的子数组 题解 普通数组 难度 题号 题目 题解 中等 53 最大子数组和 题解 中等 56 合并区间 题解 中等 189 轮转数组 题解 中等 238 除自身以外数组的乘积 题解 矩阵 难度 题号 题目 题解 中等 73 矩阵置零 题解 中等 54 螺旋矩阵 题解 中等 48 旋转图像 题解 中等 240 搜索二维矩阵 II 题解 链表 难度 题号 题目 题解 简单 160 相交链表 题解 简单 206 反转链表 题解 简单 234 回文链表 题解 简单 141 环形链表 题解 简单 21 合并两个有序链表 题解 中等 142 环形链表 II 题解 中等 2 两数相加 题解 中等 19 删除链表的倒数第 N 个结点 题解 中等 24 两两交换链表中的节点 题解 中等 148 排序链表 题解 中等 146 LRU 缓存 题解 中等 138 随机链表的复制 题解 二叉树 难度 题号 题目 题解 简单 94 二叉树的中序遍历 题解 简单 104 二叉树的最大深度 题解 简单 226 翻转二叉树 题解 简单 543 二叉树的直径 题解 简单 108 将有序数组转换为二叉搜索树 题解 中等 102 二叉树的层序遍历 题解 中等 98 验证二叉搜索树 题解 中等 230 二叉搜索树中第 K 小的元素 题解 中等 199 二叉树的右视图 题解 中等 114 二叉树展开为链表 题解 中等 105 从前序与中序遍历序列构造二叉树 题解 中等 437 路径总和 III 题解 中等 236 二叉树的最近公共祖先 题解 图论 难度 题号 题目 题解 中等 200 岛屿数量 题解 中等 994 腐烂的橘子 题解 中等 207 课程表 题解 中等 208 实现 Trie (前缀树) 题解 回溯法 难度 题号 题目 题解 中等 46 全排列 题解 中等 78 子集 题解 中等 17 电话号码的字母组合 题解 中等 39 组合总和 题解 中等 22 括号生成 题解 中等 79 单词搜索 题解 中等 131 分割回文串 题解 二分查找 难度 题号 题目 题解 简单 35 搜索插入位置 题解 中等 74 搜索二维矩阵 题解 中等 34 在排序数组中查找元素的第一个和最后一个位置 题解 中等 33 搜索旋转排序数组 题解 中等 153 寻找旋转排序数组中的最小值 题解 栈 难度 题号 题目 题解 简单 20 有效的括号 题解 中等 155 最小栈 题解 中等 394 字符串解码 题解 堆 难度 题号 题目 题解 中等 215 数组中的第K个最大元素 题解 中等 347 前 K 个高频元素 题解 贪心算法 难度 题号 题目 题解 简单 121 买卖股票的最佳时机 题解 中等 55 跳跃游戏 题解 中等 45 跳跃游戏 II 题解 中等 763 划分字母区间 题解 动态规划 难度 题号 题目 题解 简单 70 爬楼梯 题解 简单 118 杨辉三角 题解 中等 198 打家劫舍 题解 中等 279 完全平方数 题解 中等 322 零钱兑换 题解 中等 139 单词拆分 题解 中等 300 最长递增子序列 题解 中等 152 乘积最大子数组 题解 中等 416 分割等和子集 题解 多维动态规划 难度 题号 题目 题解 中等 62 不同路径 题解 中等 64 最小路径和 题解 中等 5 最长回文子串 题解 中等 1143 最长公共子序列 题解 中等 72 编辑距离 题解 技巧 难度 题号 题目 题解 简单 136 只出现一次的数字 题解 简单 169 多数元素 题解 中等 75 颜色分类 题解 中等 31 下一个排列 题解 中等 287 寻找重复数 题解 哈希 方法代码 核心作用 代码示例 / 返回值 hashmap.keys() 获取字典中所有的键 (Key) [\u0026lsquo;aet\u0026rsquo;, \u0026lsquo;ant\u0026rsquo;] hashmap.values() 获取字典中所有的值 (Value) [[\u0026rsquo;eat\u0026rsquo;, \u0026rsquo;tea\u0026rsquo;], [\u0026rsquo;tan\u0026rsquo;]] hashmap.items() 同时获取所有的键和值，常用于循环 for key, val in hashmap.items(): hashmap.get(key, default) 安全获取值，若键不存在则返回设定的默认值 hashmap.get(\u0026ldquo;aet\u0026rdquo;, []) hashmap[key] = value 新增或修改指定的键值对 hashmap[\u0026ldquo;aet\u0026rdquo;] = [\u0026ldquo;eat\u0026rdquo;] hashmap.pop(key) 删除指定的键，并返回它对应的值 words = hashmap.pop(\u0026ldquo;aet\u0026rdquo;) hashmap.clear() 清空字典里的所有内容 hashmap.clear() hashmap.update(dict2) 将另一个字典的数据合并到当前字典中 hashmap.update(other_dict) key in hashmap 判断某个键是否在字典中 (返回布尔值) if \u0026ldquo;aet\u0026rdquo; in hashmap: len(hashmap) 统计并返回字典中键值对的总数量 count = len(hashmap) 1. 两数之和 题目描述 给定一个整数数组nums和一个整数目标值target，请你在该数组中找出和为目标值target的那两个整数，并返回它们的数组下标。 你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。\n示例：\n输入：nums = [2,7,11,15], target = 9 输出：[0,1] 解释：因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 核心思路 该题目可以通过暴力枚举的方法计算出满足nums[i]+nums[j]=target的i和j，但是时间复杂度过高，因此采用哈希表的方法实现。 遍历nums数组中的每一个元素，若target-nums[i]不存在于哈希表中，则将num[i]作为key，i作为value插入哈希表。【可以避免nums中相同元素相加为target】 时间复杂度：由于哈希表查询的时间复杂度为O(1)，因此，题目的复杂度取决于遍历nums数组，所以为O(n)\n代码 1class Solution(object): 2 def twoSum(self, nums, target): 3 \u0026#34;\u0026#34;\u0026#34; 4 :type nums: List[int] 5 :type target: int 6 :rtype: List[int] 7 \u0026#34;\u0026#34;\u0026#34; 8 hashmap={} 9 #查询complement是否存在于hashmap中，若不存在，则将nums[i]插入hashmap 10 for i in range(len(nums)): 11 complement=target-nums[i] 12 if complement in hashmap: 13 return [i,hashmap[complement]] 14 hashmap[nums[i]]=i 15 return [] Tip 值为key，索引为value，创建一个空的哈希表，在遍历数组时，每次计算目标值与当前元素的“差值”。如果该差值已存在于哈希表中，说明数组中已存在与当前元素相加等于目标值的元素，直接返回两者的索引；如果该差值不存在，则将当前元素及其索引存入哈希表，供后续遍历的元素进行匹配。最后返回空数组兜底\n49. 字母异位词分组 题目描述 给你一个字符串数组，请你将字母异位词组合在一起。可以按任意顺序返回结果列表。\n字母异位词是由重新排列源单词的所有字母得到的一个新单词。\n示例：\n输入：strs = [\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;tan\u0026#34;, \u0026#34;ate\u0026#34;, \u0026#34;nat\u0026#34;, \u0026#34;bat\u0026#34;] 输出：[[\u0026#34;bat\u0026#34;],[\u0026#34;nat\u0026#34;,\u0026#34;tan\u0026#34;],[\u0026#34;ate\u0026#34;,\u0026#34;eat\u0026#34;,\u0026#34;tea\u0026#34;]] 核心思路 可以通过哈希表存储由相同字母组成的单词列表。 核心思想是寻找唯一标识。由于互为字母异位词的单词字母组成完全相同，我们将单词按字母顺序重新排序，就能得到它们的统一“身份证”。利用哈希表 Key 天然唯一的特性，将这个排序后的字符串作为 Key，就能自动把属于同一类的原始单词聚合到同一个列表（Value）中。\n创建一个空的哈希表。 对于 strs 中的每一个单词： 将其按字典序排序，得到 key。 若 key 不在哈希表中，则将 key 加入哈希表，并初始化对应的值为包含该未排序单词的列表。 若 key 已在哈希表中，将未排序单词添加到哈希表中 key 对应的列表中。 这样就能高效地将相同字母组成的单词分组。\n代码 1class Solution: 2 def groupAnagrams(self, strs: List[str]) -\u0026gt; List[List[str]]: 3\t# 创建一个defaultdict对象，默认值为空列表 4 hashmap = defaultdict(list) 5 6 for word in strs: 7 # 对每个单词进行排序，作为哈希表的key 8 #sorted()对任何“可迭代对象”排序(字符串、列表、元组、字典） 9 #list.sort()，list的专属方法 10 sortedWord = \u0026#34;\u0026#34;.join(sorted(word)) 11 # 将未排序的单词加入到对应的key的列表中 12 hashmap[sortedWord].append(word) 13 14 # 直接返回哈希表中所有value的列表 15 return list(hashmap.values()) Tip 首先利用 defaultdict(list) 创建一个 value 默认为空数组的 hashmap，接着遍历 strs 中的每一个 word，将其按字母排序得到 sortedword。由于 hashmap 的 key 具有天然去重的唯一性，我们将 sortedword 作为 key，把原始的 word 依次追加存入对应的 value 数组中，这样互为字母异位词的单词就会因为排序后的 key 完全相同而被自动归类到同一个数组里，最后只需直接提取 hashmap 中的所有 values（即 list(hash_map.values())），即可得到最终分好组的二维数组。\n128. 最长连续序列 题目描述 给定一个未排序的整数数组nums，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。\n请你设计并实现时间复杂度为O(n)的算法解决此问题。\n示例：\n输入：nums = [100,4,200,1,3,2] 输出：4 解释：最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 核心思路 题目要求设计一个时间复杂度为 O(n) 的算法。如果直接排序后再遍历统计，时间复杂度至少为 O(n log n)，不符合要求。因此，我们可以利用**哈希集合（Set）**能够在 O(1) 时间内完成元素查找的特性来解决。\n转入哈希集合（去重 + 加速查找）： 将数组 nums 转换成哈希集合 num_set，一方面自动去除了重复数字，另一方面使得判断某个数字是否存在的时间复杂度降为 O(1)。 寻找序列起点（核心剪枝技巧）： 遍历集合中的每一个数字 num。如果对每个数字都盲目往后枚举 num + 1, num + 2...，会产生大量重复计算，最坏情况下时间复杂度会退化到 O(n²)。 关键判断：只有当 num - 1 not in num_set 时，才说明 num 是一个连续序列的起点（最左端）。 若 num - 1 存在于集合中，说明 num 只是某个更长序列的中间元素，它一定会在遍历到该序列真正的起点时被统计到，因此当前可以直接跳过。 顺藤摸瓜向后查找： 一旦确认 num 是序列起点，就令 current_num = num、current_length = 1，通过 while current_num + 1 in num_set 不断向后查找连续递增的数字并累加长度。 维护最大长度： 每次找完一条完整的连续序列后，使用 max_length = max(max_length, current_length) 更新全局最长序列长度。 时间复杂度：O(n)。虽然外层 for 循环内部嵌套了 while 循环，但由于只有每个连续序列的“起点”才会进入内层 while 循环，集合中的每个数字最多只会被内层循环访问一次，因此整体时间复杂度严格为 O(n)。 空间复杂度：O(n)。需要使用哈希集合 num_set 存储去重后的数组元素。 代码 1class Solution: 2 def longestConsecutive(self, nums: list[int]) -\u0026gt; int: 3 # 1. 把数组转换成哈希集合，自动去重且查找时间复杂度降为 O(1) 4 num_set = set(nums) 5 max_length = 0 6 7 # 2. 遍历集合中的每一个数字 8 for num in num_set: 9 # 3. 判断当前数字是不是一个“序列的开头” 10 # 只有当 num - 1 不在集合中时，num 才是一个完整序列的最左端 11 if num - 1 not in num_set: 12 current_num = num 13 current_length = 1 14 15 # 4. 从这个开头开始，顺藤摸瓜往后找连续的数字 16 while current_num + 1 in num_set: 17 current_num += 1 18 current_length += 1 19 20 # 5. 更新最长序列的长度 21 max_length = max(max_length, current_length) 22 23 return max_length Tip 先将数组转为 set 去重并实现 O(1) 查找。本题最精妙的地方在于只从连续序列的起点开始向后枚举：通过 num - 1 not in num_set 判断当前数是否为序列的最左端。如果是起点，再进入 while 循环顺藤摸瓜统计长度；若不是起点则直接跳过。这样保证了每个数字最多被访问两次，从而实现真正的 O(n) 时间复杂度。\n双指针 283. 移动零 题目描述 给定一个数组nums，编写一个函数将所有0移动到数组的末尾，同时保持非零元素的相对顺序。 请注意，必须在不复制数组的情况下原地对数组进行操作。\n示例\n输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0] 核心思路 采用类似快排的划分思想，以非0数为基准，左边为非零数，右边为0，通过zeroindex记录第一个为0的元素的下标。然后循环遍历nums数组，当nums[i]!=0时，nums[i]和nums[zeroindex]进行交换，然后zeroindex++ 时间复杂度：O(n)，空间复杂度：O(1)\n代码 1class Solution: 2 def moveZeroes(self, nums: List[int]) -\u0026gt; None: 3 zeroindex=-1 4 for i in range(len(nums)): 5 if nums[i]==0 and zeroindex==-1: 6 zeroindex=i 7 elif nums[i]!=0 and zeroindex!=-1: 8 nums[zeroindex],nums[i]=nums[i],nums[zeroindex] 9 zeroindex+=1 10 return nums 11. 盛最多水的容器 题目描述 给定一个长度为n的整数数组height。有n条垂线，第i条线的两个端点是(i, 0)和(i, height[i])。\n找出其中的两条线，使得它们与x轴共同构成的容器可以容纳最多的水。\n返回容器可以储存的最大水量。\n说明：你不能倾斜容器。\n示例：\n输入：[1,8,6,2,5,4,8,3,7] 输出：49 解释：图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下，容器能够容纳水（表示为蓝色部分）的最大值为49。 核心思路 采用双指针的方法实现。\n初始化指针和变量： start指向height数组的最左端。 end指向height数组的最右端。 max_volume初始化为0。 循环计算盛水容积，并更新指针： 当start \u0026lt; end时，重复以下步骤： 计算当前容积：volume = min(height[start], height[end]) * (end - start) 更新最大容积： 若volume \u0026gt; max_volume，则max_volume = volume 移动较小值对应的指针： 若height[start]小于height[end]，则start += 1 否则，end -= 1 循环结束时，max_volume即为最大容积。 代码 1class Solution: 2 def maxArea(self, height: List[int]) -\u0026gt; int: 3 start, end = 0, len(height) - 1 4 max_volume = 0 5 6 while start \u0026lt; end: 7 volume = min(height[start], height[end]) * (end - start) 8 max_volume = max(max_volume, volume) 9 10 if height[start] \u0026lt; height[end]: 11 start += 1 12 else: 13 end -= 1 14 15 return max_volume 15. 三数之和 题目描述 给你一个整数数组nums，判断是否存在三元组[nums[i], nums[j], nums[k]]满足i != j、i != k且j != k，同时还满足nums[i] + nums[j] + nums[k] == 0。请你返回所有和为0且不重复的三元组。\n注意：答案中不可以包含重复的三元组。\n示例：\n输入：nums = [-1,0,1,2,-1,-4] 输出：[[-1,-1,2],[-1,0,1]] 解释： nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意，输出的顺序和三元组的顺序并不重要。 核心思路 特殊情况处理：如果数组为 null 或者数组长度小于 3，直接返回空结果。 对数组进行排序。 遍历排序后的数组： 如果当前数 nums[i] 大于 0，因为数组已经排序，所以后面的数都大于 0，无法找到三个数的和为 0，因此直接返回空列表。 如果当前数与前一个数相同，跳过以避免重复解。 设定两个指针，左指针 L 初始化为 i+1，右指针 R 初始化为数组末尾。 在 L \u0026lt; R 的情况下，计算 nums[i] + nums[L] + nums[R] 的和： 如果和为 0，记录这个三元组，并移动 L 和 R 指针，跳过重复元素。 如果和大于 0，说明 nums[R] 太大，右指针左移。 如果和小于 0，说明 nums[L] 太小，左指针右移。 复杂度分析：\n时间复杂度：排序需要 O(nlog n)，遍历数组和双指针查找的时间复杂度为 O(n^2)，总体复杂度为 O(n^2) 空间复杂度：O(1) 代码 1class Solution: 2 def threeSum(self, nums: List[int]) -\u0026gt; List[List[int]]: 3 if len(nums) \u0026lt; 3: return [] 4 nums.sort() 5 result = [] 6 7 for i in range(len(nums)): 8 if nums[i] \u0026gt; 0: break 9 if i \u0026gt; 0 and nums[i] == nums[i - 1]: continue 10 11 L, R = i + 1, len(nums) - 1 12 while L \u0026lt; R: 13 total = nums[i] + nums[L] + nums[R] 14 if total == 0: 15 result.append([nums[i], nums[L], nums[R]]) 16 while L \u0026lt; R and nums[L] == nums[L + 1]: 17 L += 1 18 while L \u0026lt; R and nums[R] == nums[R - 1]: 19 R -= 1 20 L += 1 21 R -= 1 22 elif total \u0026lt; 0: 23 L += 1 24 else: 25 R -= 1 26 27 return result 滑动窗口 3. 无重复字符的最长子串 题目描述 给定一个字符串s，请你找出其中不含有重复字符的最长子串 的长度。\n示例：\n输入: s = \u0026#34;abcabcbb\u0026#34; 输出: 3 解释: 因为无重复字符的最长子串是 `\u0026#34;abc\u0026#34;`，所以其长度为 3。 核心思路 目标是找到字符串中最长的无重复字符的子串。\n初始化： 我们定义两个指针start和end，用于表示滑动窗口的起始和结束位置。 一个哈希集合（HashSet）用来存储当前窗口内的字符，从而帮助我们检测是否有重复字符。 滑动窗口的操作： 扩展窗口：将end指针向右移动，即增大窗口的范围。 检查重复：每次移动end后，我们检查新加入窗口的字符s[end]。 如果s[end]已经存在于集合中，说明出现了重复字符，此时需要缩小窗口。 为了缩小窗口，我们移动start指针，直到没有重复字符为止。在移动start的过程中，需要将移出窗口的字符从集合中删除。 更新最大长度： 在窗口内没有重复字符时，计算当前窗口的长度（end - start + 1），并更新记录的最大长度。 继续上述过程，直到end指针遍历完整个字符串。 复杂度分析： - 时间复杂度为 O(n)，因为每个字符在最坏情况下只会被访问两次（一次被加入集合，一次被移出集合）。\n代码 1class Solution: 2 def lengthOfLongestSubstring(self, s: str) -\u0026gt; int: 3 if not s: 4 return 0 5 6 # 初始化指针和结果变量 7 start, max_length = 0, 0 8 char_index_map = {} # 用于存储字符及其最后出现的位置 9 10 for end in range(len(s)): 11 if s[end] in char_index_map and char_index_map[s[end]] \u0026gt;= start: 12 # 如果当前字符已经存在于字典中且其索引在start之后或等于start，说明有重复字符， 13 # 需要移动start指针，以排除重复字符 14 start = char_index_map[s[end]] + 1 15 16 # 更新字符的最新索引位置 17 char_index_map[s[end]] = end 18 19 # 更新最长无重复子串的长度 20 max_length = max(max_length, end - start + 1) 21 22 return max_length 438. 找到字符串中所有字母异位词 题目描述 给定两个字符串s和p，找到s中所有p的异位词的子串，返回这些子串的起始索引。不考虑答案输出的顺序。\n异位词指由相同字母重排列形成的字符串（包括相同的字符串）。\n示例：\n输入：s = \u0026#34;cbaebabacd\u0026#34;, p = \u0026#34;abc\u0026#34; 输出：[0,6] 解释： 起始索引等于 0 的子串是 \u0026#34;cba\u0026#34;, 它是 \u0026#34;abc\u0026#34; 的异位词。 起始索引等于 6 的子串是 \u0026#34;bac\u0026#34;, 它是 \u0026#34;abc\u0026#34; 的异位词。 核心思路 【方法一：滑动窗口 + 计数器】\n初始化计数器： 初始化 p_count 和 s_count 各需要 O(n) 时间，其中 n 是字符串 p 的长度。 滑动窗口过程： 每次移动窗口时，添加新字符和移除老字符的操作都是 O(1) 时间复杂度。 比较两个字典是否相等（s_count == p_count）的时间复杂度是 O(1)，因为字典的键数固定为 26（即字符集大小）。 因此，这种方法的总时间复杂度为 O(m + (m-n+1) * 1) = O(m) ，其中 m 是字符串 s 的长度。\n【方法二：滑动窗口 + 排序】\n初始化排序： 对字符串 p 进行排序需要 O(n log n) 时间。 滑动窗口过程： 每次移动窗口时，对滑动窗口内的字符串进行排序，时间复杂度是 O(n log n)，其中 n 是窗口大小（即 p 的长度）。 比较两个排序后的字符串的时间复杂度是 O(n)。 因此，这种方法的总时间复杂度为 O((m-n+1) * (n log n + n)) = O((m-n+1) * n log n)，其中 m 是字符串 s 的长度。\n代码 【方法一：滑动窗口 + 计数器】\n1class Solution: 2 def findAnagrams(self, s: str, p: str) -\u0026gt; List[int]: 3 # 初始化 p 的字符计数器和滑动窗口的字符计数器 4 p_count = Counter(p) 5 s_count = Counter(s[:len(p)-1]) 6 7 result = [] 8 # 遍历字符串 s，从索引 len(p)-1 到 len(s)-1 9 #窗口头指针:i-len(p)+1 10 #窗口尾指针:i 11 for i in range(len(p)-1, len(s)): 12 # 将新的字符加入当前滑动窗口的计数器中 13 s_count[s[i]] += 1 14 15 # 如果当前窗口的字符计数器与 p 的字符计数器相同，则记录起始索引 16 if s_count == p_count: 17 result.append(i - len(p) + 1) 18 19 # 移除当前窗口左侧即将滑出字符的计数 20 s_count[s[i - len(p) + 1]] -= 1 21 # 如果某个字符的计数变为零，将其从计数器中删除 22 if s_count[s[i - len(p) + 1]] == 0: 23 del s_count[s[i - len(p) + 1]] 24 25 return result 子串 560. 和为 K 的子数组 题目描述 给你一个整数数组nums和一个整数k，请你统计并返回该数组中和为k的子数组的个数。\n子数组是数组中元素的连续非空序列。\n示例：\n输入：nums = [1,1,1], k = 2 输出：2 核心思路 首先，我们了解问题要求是找到和为 k 的子数组。这意味着我们需要计算很多子数组的和。如果使用暴力方法逐个计算所有可能的子数组和，将会导致时间复杂度非常高，达到 O(n^2) 或更高。在大型数据集上，这种方法效率极低。\n前缀和（Prefix Sum） 为了解决上述问题，可以引入前缀和的概念。前缀和是从数组的起点到当前位置的元素总和。有了前缀和，任意子数组的和可以通过两个前缀和之差快速计算出来：如果我们知道从起点到第 j 个位置的前缀和 prefix_sum_j 和从起点到第 i 个位置的前缀和 prefix_sum_i，那么从 i+1 到 j 的子数组的和为 prefix_sum_j - prefix_sum_i。\n哈希表优化查找 为了快速查找某个前缀和是否存在，以及其出现次数，我们可以使用哈希表。当遍历数组时，我们记录每个前缀和及其出现的次数。这样我们就能快速判断之前是否有某个前缀和使得当前前缀和减去它等于 k。\n解题步骤的推导\n初始化：\nprefix_sum = 0: 表示初始的前缀和。 prefix_sum_count = {0: 1}: 初始化哈希表，表示前缀和为0的情况出现一次。这一步很重要，它处理了当从数组开头到某个位置子数组和恰好为 k 的情况。 遍历数组：\n对每个元素，更新当前前缀和 prefix_sum。 检查 prefix_sum - k 是否在 prefix_sum_count 中：如果在，说明存在之前的一个前缀和，使得这段区间的和为 k，于是增加计数器。 将当前前缀和加入或更新到 prefix_sum_count 中。 代码 1class Solution: 2 def subarraySum(self, nums: List[int], k: int) -\u0026gt; int: 3 prefix_sum = 0 # 初始化前缀和 4 prefix_sum_count = {0: 1} # 初始化哈希表，包含前缀和为0的情况 5 count = 0 # 初始化计数器 6 7 for num in nums: 8 prefix_sum += num # 更新当前前缀和 9 count += prefix_sum_count.get(prefix_sum - k, 0) # 如果存在符合条件的前缀和，则增加计数 10 prefix_sum_count[prefix_sum] = prefix_sum_count.get(prefix_sum, 0) + 1 # 更新当前前缀和出现次数 11 12 return count # 返回计数结果 普通数组 53. 最大子数组和 题目描述 给你一个整数数组nums，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。\n子数组是数组中的一个连续部分。\n示例：\n输入：nums = [-2,1,-3,4,-1,2,1,-5,4] 输出：6 解释：连续子数组[4,-1,2,1] 的和最大，为6 。 核心思路 动态规划思路（Kadane算法）\nKadane算法是一种线性时间复杂度O(n)的动态规划算法，用来解决最大子数组和问题。该算法的核心在于通过迭代数组，计算以每个位置结尾的子数组的最大和，并实时更新全局最大和。\n初始条件： 使用一个变量current_sum来表示当前子数组的和。 使用另一个变量max_sum来记录找到的最大和。 初始化current_sum和max_sum为数组的第一个元素，因为单独一个元素也是一个子数组。 迭代数组： 从第二个元素开始遍历数组。 对于每个元素nums[i]，current_sum = max(nums[i], current_sum + nums[i])。 如果current_sum + nums[i]比nums[i]大，则意味着延续前面的子数组是有益的。 否则，从当前元素重新开始一个新的子数组。 更新全局最大和：max_sum = max(max_sum, current_sum)，这样可以确保max_sum永远是我们迄今为止找到的最大子数组和。 返回结果： 最后，max_sum就是所求的具有最大和的连续子数组的和。 代码 1class Solution: 2 def maxSubArray(self, nums: List[int]) -\u0026gt; int: 3 # 初始化当前子数组和和最大子数组和为第一个元素 4 current_sum = max_sum = nums[0] 5 6 # 从第二个元素开始遍历数组 7 for num in nums[1:]: 8 # 当前子数组和的计算 9 current_sum = max(num, current_sum + num) 10 # 更新全局最大和 11 max_sum = max(max_sum, current_sum) 12 13 return max_sum 56. 合并区间 题目描述 以数组intervals表示若干个区间的集合，其中单个区间为intervals[i] = [starti, endi]。请你合并所有重叠的区间，并返回一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间。\n示例：\n输入：intervals = [[1,3],[2,6],[8,10],[15,18]] 输出：[[1,6],[8,10],[15,18]] 解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. 核心思路 排序：首先按照每个区间的起始值对intervals进行排序。 初始化结果列表：创建一个空列表merged用于存储最终的合并结果。 遍历区间： 如果merged为空或者当前区间与merged的最后一个区间不重叠，将当前区间加入merged。 否则，合并当前区间与merged的最后一个区间。 代码 1class Solution: 2 def merge(self, intervals: List[List[int]]) -\u0026gt; List[List[int]]: 3 # 按起始值排序区间 4 intervals.sort(key=lambda x: x[0]) 5 merged_intervals = [] 6 7 for current in intervals: 8 if not merged_intervals or merged_intervals[-1][1] \u0026lt; current[0]: 9 merged_intervals.append(current) 10 else: 11 merged_intervals[-1][1] = max(merged_intervals[-1][1], current[1]) 12 13 return merged_intervals 189. 轮转数组 题目描述 给定一个整数数组nums，将数组中的元素向右轮转k个位置，其中k是非负数。\n示例：\n输入：nums = [1,2,3,4,5,6,7], k = 3 输出：[5,6,7,1,2,3,4] 解释： 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4] 核心思路 【方法一】\n计算有效的旋转步数： 通过k = k % n，确保旋转步数不超过数组长度。 切片操作重新排列数组： 使用nums[-k:]获取数组末尾k个元素。 使用nums[:-k]获取数组前面部分。 将这两个部分拼接起来形成新的数组顺序，并赋值给原数组nums[:]。 时间复杂度：(O(n))。切片操作需要遍历整个数组，因此时间复杂度为 (O(n))。 空间复杂度：(O(n))。虽然没有显式地使用额外的数据结构，但切片操作会产生临时数组，占用额外的空间。 【方法二】\n整体翻转数组：将整个数组完全反转。 翻转前k个元素：将前k个元素再次反转。 翻转后n-k个元素：将后n-k个元素再次反转。 时间复杂度：(O(n))。每次翻转操作需要遍历部分或者整个数组，总共三次翻转，因此时间复杂度为 (O(n))。 空间复杂度：(O(1))。只使用了常数个额外空间用于变量存储，没有使用额外的数据结构。 代码 【方法一】\n1class Solution: 2 def rotate(self, nums: List[int], k: int) -\u0026gt; None: 3 n = len(nums) 4 k = k % n # 防止 k 超过数组长度 5 nums[:] = nums[-k:] + nums[:-k] 【方法二】\n1class Solution: 2 def rotate(self, nums: List[int], k: int) -\u0026gt; None: 3 n = len(nums) 4 k = k % n # 防止 k 超过数组长度 5 6 def reverse(start: int, end: int) -\u0026gt; None: 7 while start \u0026lt; end: 8 nums[start], nums[end] = nums[end], nums[start] 9 start += 1 10 end -= 1 11 12 reverse(0, n - 1) 13 reverse(0, k - 1) 14 reverse(k, n - 1) 238. 除自身以外数组的乘积 题目描述 给你一个整数数组nums，返回 数组answer，其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。\n题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32 位整数范围内。\n请不要使用除法，且在O(n)时间复杂度内完成此题。\n示例：\n输入：nums = [1,2,3,4] 输出：[24,12,8,6] 核心思路 我们需要找到一个不包含nums[i]的数组乘积，并且要求时间复杂度为O(n)且不使用除法。可以通过前缀积和后缀积的方法来实现。\n计算前缀积：创建一个与输入数组等长的结果数组answer。首先将answer初始化为全1，然后遍历一遍数组，将每个位置的值替换为该位置之前所有元素的乘积。 计算后缀积：在同一个结果数组中从后向前遍历，同时维护一个变量suffix_product表示从当前元素到最后一个元素的乘积。将answer中的每个元素与suffix_product相乘，然后更新suffix_product。 通过上述方法，我们可以在一次遍历中完成前缀积的计算，另一遍遍历中完成后缀积的计算，从而满足O(n)的时间复杂度要求。同时，借助结果数组本身来存储计算结果，实现了O(1)的空间复杂度（不包括输出数组）。\n代码 1class Solution: 2 def productExceptSelf(self, nums: List[int]) -\u0026gt; List[int]: 3 n = len(nums) 4 answer = [1] * n # 初始化结果数组，全1 5 # 计算前缀积并存储在 answer 中 6 for i in range(1, n): 7 answer[i] = answer[i - 1] * nums[i - 1] 8 9 suffix_product = 1 # 初始化后缀积为1 10 # 计算后缀积，并直接更新 answer 11 for i in range(n - 1, -1, -1): # 从后往前遍历数组 12 answer[i] *= suffix_product # 将当前后缀积乘以 answer 中对应位置的值 13 suffix_product *= nums[i] # 更新后缀积 14 15 return answer 矩阵 73. 矩阵置零 题目描述 给定一个m×n的矩阵，如果一个元素为0，则将其所在行和列的所有元素都设为0。请使用原地算法。\n示例：\n输入：matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出：[[1,0,1],[0,0,0],[1,0,1]] 核心思路 要解决这个问题，可以使用矩阵的第一行和第一列作为标记，来记录哪些行和哪些列需要被设为0。这样可以避免使用额外的空间。\n检查第一行和第一列是否有0：首先检查矩阵的第一行和第一列中是否有0，因为这两个位置会被用来标记其他行和列。\n使用第一行和第一列作为标记：遍历矩阵的其余部分（从第二行和第二列开始），如果某个元素是0，就将该元素所在的行的第一个元素和列的第一个元素设为0，作为标记。\n根据标记设置0：再次遍历矩阵（从第二行和第二列开始），如果某个元素所在的行的第一个元素或列的第一个元素是0，就将该元素设为0。\n处理第一行和第一列：最后，根据第一步中记录的信息，决定是否将第一行和第一列全部设为0。\n代码 1class Solution: 2 def setZeroes(self, matrix: List[List[int]]) -\u0026gt; None: 3 m, n = len(matrix), len(matrix[0]) 4 5 # 检查第一行是否有0 6 first_row_has_zero = any(matrix[0][j] == 0 for j in range(n)) 7 # 检查第一列是否有0 8 first_col_has_zero = any(matrix[i][0] == 0 for i in range(m)) 9 10 # 使用第一行和第一列作为标记 11 for i in range(1, m): 12 for j in range(1, n): 13 if matrix[i][j] == 0: 14 matrix[i][0] = 0 # 标记第i行需要变为0 15 matrix[0][j] = 0 # 标记第j列需要变为0 16 17 # 根据标记设置0 18 for i in range(1, m): 19 for j in range(1, n): 20 if matrix[i][0] == 0 or matrix[0][j] == 0: 21 matrix[i][j] = 0 22 23 # 处理第一行，如果第一行原来存在0，则将第一行全部变为0 24 if first_row_has_zero: 25 for j in range(n): 26 matrix[0][j] = 0 27 28 # 处理第一列，如果第一列原来存在0，则将第一列全部变为0 29 if first_col_has_zero: 30 for i in range(m): 31 matrix[i][0] = 0 54. 螺旋矩阵 题目描述 给你一个m行n列的矩阵matrix，请按照顺时针螺旋顺序，返回矩阵中的所有元素。\n示例：\n输入：matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出：[1,2,3,6,9,8,7,4,5] 核心思路 这道题可以通过模拟螺旋遍历的方式来解决。核心思想是按照顺时针方向逐步缩小遍历的范围，直到遍历完整个矩阵。\n初始化边界：定义四个边界变量 top, bottom, left, right，分别表示当前未遍历的矩阵的上、下、左、右边界。\n循环遍历：\n从左到右：遍历 left 到 right 的顶部行，然后将 top 下移一行。 从上到下：遍历 top 到 bottom 的右侧列，然后将 right 左移一列。 从右到左：遍历 right 到 left 的底部行，然后将 bottom 上移一行。 从下到上：遍历 bottom 到 top 的左侧列，然后将 left 右移一列。 终止条件：当 top 超过 bottom 或 left 超过 right 时，遍历结束。\n通过这种方式，可以确保每个元素只被访问一次，并且按照顺时针螺旋顺序返回所有元素。\n代码 1class Solution: 2 def spiralOrder(self, matrix: List[List[int]]) -\u0026gt; List[int]: 3 if not matrix or not matrix[0]: 4 return [] 5 6 m, n = len(matrix), len(matrix[0]) 7 top, bottom, left, right = 0, m - 1, 0, n - 1 8 result = [] 9 10 while top \u0026lt;= bottom and left \u0026lt;= right: 11 # 从左到右遍历顶部行 12 for i in range(left, right + 1): 13 result.append(matrix[top][i]) 14 top += 1 # 顶部行已遍历，下移一行 15 16 # 从上到下遍历右侧列 17 for i in range(top, bottom + 1): 18 result.append(matrix[i][right]) 19 right -= 1 # 右侧列已遍历，左移一列 20 21 # 检查是否还有剩余的行和列 22 if top \u0026lt;= bottom: 23 # 从右到左遍历底部行 24 for i in range(right, left - 1, -1): 25 result.append(matrix[bottom][i]) 26 bottom -= 1 # 底部行已遍历，上移一行 27 28 if left \u0026lt;= right: 29 # 从下到上遍历左侧列 30 for i in range(bottom, top - 1, -1): 31 result.append(matrix[i][left]) 32 left += 1 # 左侧列已遍历，右移一列 33 34 return result 48. 旋转图像 题目描述 给定一个_n_×_n_的二维矩阵matrix表示一个图像。请你将图像顺时针旋转 90 度。\n你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。\n示例：\n输入：matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出：[[7,4,1],[8,5,2],[9,6,3]] 核心思路 确定层数：\n对于一个 n × n 的矩阵，旋转层数为 n // 2 逐层旋转：\n每次选择一个层，从 top 行开始，按顺时针方向交换四个角上的元素 具体元素交换：\n对于每个需要旋转的层和边界：\nmatrix[top][i] 表示当前层的上边元素，存储到临时变量 temp 中。 左边元素 matrix[bottom - offset][top] 移到上边。 下边元素 matrix[bottom][bottom - offset] 移到左边。 右边元素 matrix[i][bottom] 移到下边。 最后将存储的 temp 移到右边。 代码 1class Solution: 2 def rotate(self, matrix: List[List[int]]) -\u0026gt; None: 3 n = len(matrix) 4 5 # 逐层旋转 6 for layer in range(n // 2): 7 # 当前层的边界 8 top, bottom = layer, n - 1 - layer 9 10 for i in range(top, bottom): 11 # 计算偏移量 12 offset = i - top 13 14 # 顺时针旋转四个位置 15 16 # 保存上边元素 17 temp = matrix[top][i] 18 # 左 -\u0026gt; 上 19 matrix[top][i] = matrix[bottom - offset][top] 20 # 下 -\u0026gt; 左 21 matrix[bottom - offset][top] = matrix[bottom][bottom - offset] 22 # 右 -\u0026gt; 下 23 matrix[bottom][bottom - offset] = matrix[i][bottom] 24 # 上 -\u0026gt; 右 25 matrix[i][bottom] = temp 240. 搜索二维矩阵 II 题目描述 编写一个高效的算法来搜索m×n矩阵matrix中的一个目标值target。该矩阵具有以下特性：\n每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例：\n输入：matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5 输出：true 核心思路 从矩阵的右上角开始搜索，这样每一步都可以通过比较大小来确定下一步的方向。详细步骤如下：\n从右上角开始：我们选择从矩阵的右上角（即第一行的最后一个元素）开始。这个位置有一个特殊的性质：\n如果当前元素比目标值 target 大，我们可以往左移动，因为同一行左边的元素更小。 如果当前元素比目标值 target 小，我们可以向下移动，因为同一列下面的元素更大。 不断缩小搜索空间：根据上面的逻辑，我们每次都可以抛弃一整行或一整列，因此每次比较都有效地缩小了搜索范围。\n终止条件：如果找到了目标值，返回 True。如果搜索越界（即行列索引超出矩阵范围），则返回 False，表示未找到目标值。\n代码 1class Solution: 2 def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: 3 # 获取矩阵的行数和列数 4 if not matrix or not matrix[0]: 5 return False 6 rows, cols = len(matrix), len(matrix[0]) 7 8 # 从右上角开始 9 row, col = 0, cols - 1 10 11 while row \u0026lt; rows and col \u0026gt;= 0: 12 if matrix[row][col] == target: 13 return True 14 elif matrix[row][col] \u0026gt; target: 15 # 如果当前元素比目标大，向左移动 16 col -= 1 17 else: 18 # 如果当前元素比目标小，向下移动 19 row += 1 20 21 return False 链表 160. 相交链表 题目描述 给你两个单链表的头节点headA和headB，请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点，返回null。 图示两个链表在节点c1开始相交，题目数据保证整个链式结构中不存在环。\n注意，函数返回结果后，链表必须保持其原始结构。\n示例： 输入：intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出：Intersected at \u0026#39;8\u0026#39; 解释：相交节点的值为8（注意，如果两个链表相交则不能为0）。 从各自的表头开始算起，链表 A 为 [4,1,8,4,5]，链表 B 为 [5,6,1,8,4,5]。 在 A 中，相交节点前有 2 个节点；在 B 中，相交节点前有 3 个节点。 请注意相交节点的值不为 1，因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说，它们在内存中指向两个不同的位置，而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点，B 中第四个节点) 在内存中指向相同的位置。 核心思路 假设两个链表分别为A和B，并且它们在某一点相交。设A的长度为m，B的长度为n，交点之前的部分长度分别为a和b，交点之后的部分长度为c。\n如果两个链表没有交点，那么indexA和indexB最终都会到达None，从而退出循环。 如果有交点，由于两个指针都会遍历完自己的链表后再遍历对方的链表，因此它们会在交点处相遇。 代码 1class Solution: 2\tdef getIntersectionNode(self, headA: ListNode, headB: ListNode) -\u0026gt; Optional[ListNode]: 3\tindexA, indexB = headA, headB 4\twhile indexA != indexB: 5\tindexA = indexA.next if indexA else headB 6\tindexB = indexB.next if indexB else headA 7\treturn indexA 206. 反转链表 题目描述 给你单链表的头节点head，请你反转链表，并返回反转后的链表。\n示例： 输入：head = [1,2,3,4,5] 输出：[5,4,3,2,1] 核心思路 反转链表的基本思路是遍历链表，并将每个节点的next指针从指向它的下一个节点改为指向前一个节点。为了做到这一点，我们需要维护三个指针：\nprev：指向当前节点的前一个节点。 current：指向当前节点。 next_node：指向当前节点的下一个节点。 代码 1class Solution: 2 def reverseList(self, head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 if not head: 4 return None 5 # 初始化前驱节点和当前节点 6 prev = None 7 current = head 8 # 遍历链表，反转指针 9 while current: 10 next_node = current.next 11 # 反转指针 12 current.next = prev 13 # 移动前驱节点和当前节点 14 prev = current 15 current = next_node 16 17 # 返回新的头节点（即原链表的最后一个节点） 18 return prev 234. 回文链表 题目描述 给你一个单链表的头节点head，请你判断该链表是否为回文链表。如果是，返回true；否则，返回false。\n示例：\n输入：head = [1,2,2,1] 输出：true 核心思路 方法一：链表转化为数组 可以遍历链表，并将其中的元素存储在数组中，然后使用首尾双指针判断数组是否为回文数组。 时间复杂度：O(n)，空间复杂度：O(n) 【方法二】 为了降低空间复杂度，改变链表结构，将链表后半部分及进行逆序，然后和链表前半部分的数据进行比较，判断是否为回文。\n步骤一：使用快慢指针寻找到链表中间位置。初始化慢指针slow和快指针fast为链表头指针head，slow每次移动一位，fast每次移动两位，最终在fast为空时，slow指针指向链表的中间位置，将链表分为了两个部分。在接下来的步骤中，会对这两个部分是否为回文进行判断。 步骤二：以slow为头节点，将slow指向的后半部分指针进行逆序排序。【参考反转链表】 步骤三：将反转后的链表和以head节点为首的链表前半部分按照次序进行比较，若出现不相等的值，则返回False；反之最终返回True\n代码 1class Solution: 2 def isPalindrome(self, head: Optional[ListNode]) -\u0026gt; bool: 3 if not head or not head.next: 4 return True 5 # 步骤一：使用快慢指针寻找到链表中间位置 6 slow, fast = head, head 7 while fast and fast.next: 8 slow = slow.next 9 fast = fast.next.next 10 # 步骤二：对于链表后半部分进行反转 11 def reverseLink(head): 12 prev = None 13 current = head 14 while current: 15 next_node = current.next 16 current.next = prev 17 prev = current 18 current = next_node 19 return prev 20 slow = reverseLink(slow) 21 # 步骤三：判断是否为回文链表 22 while slow: 23 if slow.val != head.val: 24 return False 25 slow, head = slow.next, head.next 26 return True 141. 环形链表 题目描述 给你一个链表的头节点head，判断链表中是否有环。\n如果链表中有某个节点，可以通过连续跟踪next指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数pos来表示链表尾连接到链表中的位置（索引从 0 开始）。注意：pos不作为参数进行传递。仅仅是为了标识链表的实际情况。\n如果链表中存在环，则返回true。 否则，返回false。\n示例：\n输入：head = [3,2,0,-4], pos = 1 输出：true 解释：链表中有一个环，其尾部连接到第二个节点。 核心思路 采用快慢指针的方式，设置slow和fast指针：\n初始化：slow=fast=head 循环：当slow，fast和fast.next均不为None时，令slow=slow.next,fast=fast.next.next，若slow=fast，则存在环形链表；若slow和fast中存在None，则不存在环形链表。\n代码 1class Solution: 2 def hasCycle(self, head: Optional[ListNode]) -\u0026gt; bool: 3 if not head: 4 return False 5 slow, fast=head, head 6 while slow and fast and fast.next: 7 slow, fast=slow.next, fast.next.next 8 if slow==fast: 9 return True 10 return False 21. 合并两个有序链表 题目描述 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。\n示例：\n输入：l1 = [1,2,4], l2 = [1,3,4] 输出：[1,1,2,3,4,4] 核心思路 创建哑节点：使用dummy作为合并后链表的头节点的前驱，简化头部处理。 初始化当前指针：用current指向当前合并链表的最后一个节点。 迭代合并：比较list1和list2的值，选择较小的节点接入合并链表，并移动相应的指针。 处理剩余部分：将未遍历完的链表直接接入合并链表的末尾。 返回结果：返回dummy.next，即合并后的链表头节点。 代码 1class Solution: 2 def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 # 创建一个哑节点作为合并后链表的头节点的前驱节点 4 dummy = ListNode(-1) 5 current = dummy 6 7 # 当两个链表都不为空时，进行迭代合并 8 while list1 and list2: 9 if list1.val \u0026lt;= list2.val: 10 # 如果list1的值较小或相等，将其节点接入合并链表 11 current.next = list1 12 list1 = list1.next 13 else: 14 # 如果list2的值较小，将其节点接入合并链表 15 current.next = list2 16 list2 = list2.next 17 # 移动当前指针到合并链表的最后一个节点 18 current = current.next 19 20 # 将未遍历完的链表直接接入合并链表的末尾 21 current.next = list1 if list1 else list2 22 23 # 返回合并后的链表头节点 24 return dummy.next 142. 环形链表 II 题目描述 给定一个链表的头节点 head，返回链表开始入环的第一个节点。如果链表无环，则返回null。\n如果链表中有某个节点，可以通过连续跟踪next指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数pos来表示链表尾连接到链表中的位置（索引从 0 开始）。如果pos是-1，则在该链表中没有环。注意：pos不作为参数进行传递，仅仅是为了标识链表的实际情况。\n不允许修改链表。\n示例：\n输入：head = [3,2,0,-4], pos = 1 输出：返回索引为 1 的链表节点 解释：链表中有一个环，其尾部连接到第二个节点。 核心思路 快慢指针用来检测链表中是否存在环。一旦快慢指针相遇，说明链表中存在环。接下来的关键是如何找到环的起始节点。\n检测环：\n使用两个指针slow和fast，slow每次移动一步，fast每次移动两步。 如果fast和slow相遇，则说明链表中存在环。 找到环的起始节点：\n当fast和slow相遇时，将其中一个指针（例如slow）重置到链表的头部。 然后，两个指针都每次移动一步，直到它们再次相遇。这个相遇点就是环的起始节点。 原理：\n假设链表的头部到环的起始节点的距离为a，环的长度为b。当slow和fast相遇时，slow走了a + b * k步，fast走了a + b * m步，其中m \u0026gt; k。因为fast的速度是slow的两倍，所以有： [ 2(a + b * k) = a + b * m ] 简化后得到： [ a = (m - 2k) * b ] 这说明a是环长度b的整数倍。因此，当slow从头开始，fast从相遇点开始，每次移动一步时，它们会在环的起始节点相遇。\n代码 1class Solution: 2 def detectCycle(self, head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 if not head or not head.next: 4 return None 5 6 slow = head 7 fast = head 8 9 # 检测是否有环 10 while fast and fast.next: 11 slow = slow.next 12 fast = fast.next.next 13 if slow == fast: 14 break 15 else: 16 return None 17 18 # 找到环的起始节点 19 slow = head 20 while slow != fast: 21 slow = slow.next 22 fast = fast.next 23 24 return slow 2. 两数相加 题目描述 给你两个非空的链表，表示两个非负的整数。它们每位数字都是按照逆序的方式存储的，并且每个节点只能存储一位数字。\n请你将两个数相加，并以相同形式返回一个表示和的链表。\n你可以假设除了数字 0 之外，这两个数都不会以 0开头。\n示例：\n输入：l1 = [2,4,3], l2 = [5,6,4] 输出：[7,0,8] 解释：342 + 465 = 807 核心思路 初始化结果链表和进位变量：\n创建一个虚拟头节点 dummy，用于简化边界条件处理。 初始化进位变量 carry 为 0。 遍历两个链表：\n使用两个指针 p1 和 p2 分别指向链表 L1 和 L2 的头节点。 创建一个指针 current 指向 dummy，用于构建结果链表。 逐位相加：\n在 p1 或 p2 不为空时，执行以下操作： 计算当前位的和 sum = (p1.val if p1 else 0) + (p2.val if p2 else 0) + carry。 更新进位 carry = sum // 10。 创建一个新节点，节点值为 sum % 10，并将其连接到结果链表的末尾。 移动 current 指针到新节点。 如果 p1 不为空，移动 p1 指针到下一个节点。 如果 p2 不为空，移动 p2 指针到下一个节点。 处理剩余的进位：\n如果遍历完两个链表后，carry 仍不为 0，则在结果链表的末尾新增一个节点，节点值为 carry。 返回结果链表：\n返回 dummy.next，即结果链表的头节点。 代码 1class Solution: 2 def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 # 创建虚拟头节点，用于简化链表操作 4 dummy = ListNode(0) 5 current = dummy 6 carry = 0 7 8 # 遍历两个链表，直到两者都为空 9 while l1 or l2: 10 # 计算当前位的和，考虑进位 11 sum_val = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry 12 carry = sum_val // 10 # 更新进位 13 current.next = ListNode(sum_val % 10) # 创建新节点，存储当前位的值 14 current = current.next # 移动 current 指针到新节点 15 16 # 移动 l1 和 l2 指针到下一个节点 17 if l1: 18 l1 = l1.next 19 if l2: 20 l2 = l2.next 21 22 # 如果最后有进位，需要在结果链表的末尾新增一个节点 23 if carry \u0026gt; 0: 24 current.next = ListNode(carry) 25 26 # 返回结果链表的头节点，即虚拟头节点的下一个节点 27 return dummy.next 19. 删除链表的倒数第 N 个结点 题目描述 给你一个链表，删除链表的倒数第n个结点，并且返回链表的头结点。\n示例：\n输入：head = [1,2,3,4,5], n = 2 输出：[1,2,3,5] 核心思路 使用快慢指针解决删除链表的倒数第n个结点的问题是一个非常高效的方法。这里的关键在于如何让两个指针在链表中保持一定的距离，从而当一个指针到达链表末尾时，另一个指针正好指向需要删除的结点的前一个结点。\n初始化指针：\n创建一个虚拟头结点dummy，它的next指向原链表的头结点head。这样做可以简化边界条件的处理，尤其是当需要删除的结点是头结点时。 初始化两个指针fast和slow，都指向dummy。 移动快指针：\n先移动fast指针n步。这样fast和slow之间的距离就是n。 同时移动两个指针：\n当fast指针到达链表末尾时（即fast.next为None），slow指针正好指向需要删除的结点的前一个结点。 这是因为fast和slow之间的距离始终保持为n，所以当fast到达末尾时，slow就在倒数第n个结点的前一个位置。 删除结点：\n修改slow的next指针，使其跳过需要删除的结点，即slow.next = slow.next.next。 返回结果：\n返回dummy.next，即新的链表头结点。 代码 1class Solution: 2 def removeNthFromEnd(self, head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: 3 # 创建虚拟头结点，简化边界条件处理 4 dummy = ListNode(0, head) 5 6 # 初始化快慢指针 7 fast = slow = dummy 8 9 # 移动快指针 n 步 10 for _ in range(n): 11 fast = fast.next 12 13 # 同时移动两个指针，直到快指针到达链表末尾 14 while fast.next: 15 fast = fast.next 16 slow = slow.next 17 18 # 删除结点 19 slow.next = slow.next.next 20 21 # 返回新的头结点 22 return dummy.next 24. 两两交换链表中的节点 题目描述 给你一个链表，两两交换其中相邻的节点，并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题（即，只能进行节点交换）。\n示例：\n输入：head = [1,2,3,4] 输出：[2,1,4,3] 核心思路 解决这个问题的一个有效方法是使用迭代，通过维护几个指针来交换链表中的节点。\n创建一个虚拟头节点\n为了简化边界情况的处理，可以在原链表的头部添加一个虚拟头节点（dummy node），这样可以方便地处理原链表头部的节点交换。 初始化指针\nprev指向虚拟头节点，用于记录当前需要交换的两个节点的前一个节点。 current指向链表的头节点，即需要交换的第一个节点。 next指向当前节点的下一个节点，即需要交换的第二个节点。 迭代交换节点 在循环中，每次交换两个相邻的节点，并更新指针：\n将prev的next指向next（即第二个节点）。 将current的next指向next.next（即第二个节点的下一个节点）。 将next的next指向current（即将第二个节点的next指向第一个节点）。 更新prev和current指针，以便处理下一对节点。 返回新的头节点\n当所有节点都交换完成后，返回虚拟头节点的next，即新的链表头节点。 代码 1class Solution: 2 def swapPairs(self, head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 # 创建虚拟头节点，方便处理链表头部的交换 4 dummy = ListNode(0) 5 dummy.next = head 6 prev = dummy 7 8 while head and head.next: 9 # 定义需要交换的两个节点 10 first = head 11 second = head.next 12 13 # 交换节点 14 # 1. 将 prev 的 next 指向 second 15 prev.next = second 16 # 2. 将 first 的 next 指向 second 的 next 17 first.next = second.next 18 # 3. 将 second 的 next 指向 first 19 second.next = first 20 21 # 更新指针，准备处理下一对节点 22 prev = first 23 head = first.next 24 25 # 返回新的链表头节点 26 return dummy.next 148. 排序链表 题目描述 给你链表的头结点head，请将其按升序排列并返回排序后的链表。\n示例： 输入：head = [4,2,1,3] 输出：[1,2,3,4] 核心思路 归并排序（Merge Sort）的时间复杂度为 O(nlog n)，空间复杂度为 O(1)（如果使用自底向上的方法）。\n找到链表的中间节点：使用快慢指针法，快指针每次移动两步，慢指针每次移动一步，当快指针到达链表末尾时，慢指针正好在中间。 分割链表：将链表从中间节点分成两个子链表。 递归排序：对两个子链表分别进行归并排序。 合并排序后的子链表：将两个有序的子链表合并成一个有序的链表。 代码 1class Solution: 2 def sortList(self, head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: 3 if not head or not head.next: 4 return head 5 6 # 找到链表的中间节点 7 def find_mid(head: ListNode) -\u0026gt; ListNode: 8 slow, fast = head, head.next 9 while fast and fast.next: 10 slow = slow.next 11 fast = fast.next.next 12 return slow 13 14 # 合并两个有序链表 15 def merge(l1: ListNode, l2: ListNode) -\u0026gt; ListNode: 16 dummy = ListNode() 17 current = dummy 18 while l1 and l2: 19 if l1.val \u0026lt; l2.val: 20 current.next = l1 21 l1 = l1.next 22 else: 23 current.next = l2 24 l2 = l2.next 25 current = current.next 26 if l1: 27 current.next = l1 28 if l2: 29 current.next = l2 30 return dummy.next 31 32 # 找到中间节点并分割链表 33 mid = find_mid(head) 34 right = mid.next 35 mid.next = None 36 left = head 37 38 # 递归排序 39 left = self.sortList(left) 40 right = self.sortList(right) 41 42 # 合并排序后的子链表 43 return merge(left, right) 146. LRU 缓存 题目描述 请你设计并实现一个满足LRU (最近最少使用) 缓存约束的数据结构。\n实现LRUCache类：\nLRUCache(int capacity)以正整数作为容量capacity初始化 LRU 缓存 int get(int key)如果关键字key存在于缓存中，则返回关键字的值，否则返回-1。 void put(int key, int value)如果关键字key已经存在，则变更其数据值value；如果不存在，则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity，则应该逐出最久未使用的关键字。 函数get和put必须以O(1)的平均时间复杂度运行。\n示例：\n输入 [\u0026#34;LRUCache\u0026#34;, \u0026#34;put\u0026#34;, \u0026#34;put\u0026#34;, \u0026#34;get\u0026#34;, \u0026#34;put\u0026#34;, \u0026#34;get\u0026#34;, \u0026#34;put\u0026#34;, \u0026#34;get\u0026#34;, \u0026#34;get\u0026#34;, \u0026#34;get\u0026#34;] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废，缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废，缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4 核心思路 LRU（最近最少使用）缓存是一种常见的缓存策略，用于管理有限的内存资源。当缓存满时，它会优先淘汰最近最少使用的数据项。这个题目要求你设计一个数据结构来实现 LRUCache 类，该类需要支持get和put操作，并且这些操作的时间复杂度都是 O(1)。\n双向链表 + 哈希表\n双向链表：用于维护缓存中的键值对顺序，最近使用的节点放在链表的头部，最久未使用的节点放在链表的尾部。 哈希表：用于快速查找节点，哈希表的键是缓存的键，值是对应的链表节点。 操作细节\nget操作： 如果键存在，返回对应的值，并将该节点移动到链表的头部（表示最近使用）。 如果键不存在，返回 -1。 put操作： 如果键已存在，更新其值，并将该节点移动到链表的头部。 如果键不存在，插入新的键值对到链表的头部。 如果插入后缓存超过容量，移除链表尾部的节点（最久未使用的节点）。 代码 1class LRUCache: 2 def __init__(self, capacity: int): 3 # 使用 OrderedDict 来维护键值对的顺序 4 self.data = OrderedDict() 5 # 初始化缓存的容量 6 self.capacity = capacity 7 8 def get(self, key: int) -\u0026gt; int: 9 # 如果键存在于缓存中 10 if key in self.data: 11 # 将该键值对移动到有序字典的末尾（表示最近使用） 12 self.data.move_to_end(key) 13 # 返回对应的值 14 return self.data[key] 15 # 如果键不存在于缓存中，返回 -1 16 return -1 17 18 def put(self, key: int, value: int) -\u0026gt; None: 19 # 如果键已经存在于缓存中 20 if key in self.data: 21 # 更新该键的值 22 self.data[key] = value 23 else: 24 # 如果缓存已满 25 if len(self.data) \u0026gt;= self.capacity: 26 # 移除最久未使用的键值对（有序字典的头部） 27 self.data.popitem(last=False) 28 # 插入新的键值对 29 self.data[key] = value 30 # 将该键值对移动到有序字典的末尾（表示最近使用） 31 self.data.move_to_end(key) 138. 随机链表的复制 题目描述 给你一个长度为n的链表，每个节点包含一个额外增加的随机指针random，该指针可以指向链表中的任何节点或空节点。\n构造这个链表的深拷贝。深拷贝应该正好由n个全新节点组成，其中每个新节点的值都设为其对应的原节点的值。新节点的next指针和random指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点。\n例如，如果原链表中有X和Y两个节点，其中X.random --\u0026gt; Y。那么在复制链表中对应的两个节点x和y，同样有x.random --\u0026gt; y。\n返回复制链表的头节点。\n用一个由n个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]表示：\nval：一个表示Node.val的整数。 random_index：随机指针指向的节点索引（范围从0到n-1）；如果不指向任何节点，则为null。 你的代码只接受原链表的头节点head作为传入参数。\n示例： 输入：head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出：[[7,null],[13,0],[11,4],[10,2],[1,0]] 核心思路 复制每个节点并插入到原节点后面：\n首先遍历原链表，对于每个节点，创建一个新的节点，并将其插入到当前节点和下一个节点之间。这样，原链表A -\u0026gt; B -\u0026gt; C变成A -\u0026gt; A\u0026rsquo; -\u0026gt; B -\u0026gt; B\u0026rsquo; -\u0026gt; C -\u0026gt; C\u0026rsquo;，其中A\u0026rsquo;,B\u0026rsquo;,C\u0026rsquo;是新复制的节点。 设置新节点的随机指针：\n再次遍历链表，这次设置每个新节点的random指针。由于新节点紧跟在原节点后面，原节点的random指针指向的节点后面就是新节点的random指针应该指向的节点。例如，如果原节点A的random指针指向B，那么新节点A\u0026rsquo;的random指针应该指向B\u0026rsquo;。 分离两个链表：\n最后，我们需要将原链表和新链表分开。遍历链表，将新节点从原链表中分离出来，恢复原链表的结构，同时构建新链表。 代码 1class Solution: 2 def copyRandomList(self, head: \u0026#39;Optional[Node]\u0026#39;) -\u0026gt; \u0026#39;Optional[Node]\u0026#39;: 3 if not head: 4 return None 5 6 # Step 1: 创建新节点并插入到原节点后面 7 curr = head 8 while curr: 9 # 创建新节点，并将其值设为当前节点的值 10 new_node = Node(curr.val, curr.next, None) 11 # 将新节点插入到当前节点和下一个节点之间 12 curr.next = new_node 13 # 移动到下一个原节点 14 curr = new_node.next 15 16 # Step 2: 设置新节点的随机指针 17 curr = head 18 while curr: 19 # 如果当前节点有随机指针 20 if curr.random: 21 # 新节点的随机指针应该指向原节点随机指针对应的新节点 22 curr.next.random = curr.random.next 23 # 移动到下一个原节点 24 curr = curr.next.next 25 26 # Step 3: 分离两个链表 27 old_head = head 28 new_head = head.next 29 curr_old = old_head 30 curr_new = new_head 31 32 while curr_old: 33 # 恢复原链表的结构 34 curr_old.next = curr_old.next.next if curr_old.next else None 35 # 构建新链表的结构 36 curr_new.next = curr_new.next.next if curr_new.next else None 37 # 移动到下一个原节点 38 curr_old = curr_old.next 39 # 移动到新链表的下一个新节点 40 curr_new = curr_new.next 41 42 return new_head 二叉树 94. 二叉树的中序遍历 题目描述 给定一个二叉树的根节点root，返回_它的中序遍历_。\n示例：\n输入：root = [1,null,2,3] 输出：[1,3,2] 核心思路 【方法一：递归】 中序遍历的顺序是先遍历左子树，然后访问根节点，最后遍历右子树。\n主方法：\n初始化一个空列表 result 用于存储遍历结果。 调用递归辅助方法 inorder，传入根节点和结果列表。 返回结果列表。 递归辅助方法 inorder：\n检查当前节点是否为空，如果为空则返回。 递归遍历当前节点的左子树。 将当前节点的值添加到结果列表中。 递归遍历当前节点的右子树。 返回结果列表。 【方法二：栈】 初始化一个栈和一个结果列表。\n使用一个循环，只要当前节点或栈不为空，就继续遍历。\n在循环中，将当前节点的所有左子节点压入栈中，直到没有左子节点为止。\n弹出栈顶节点，访问该节点并将它的值加入结果列表。\n将当前节点更新为刚访问节点的右子节点，继续遍历右子树。\n代码 【方法一：递归】\n1class Solution: 2 def inorderTraversal(self, root: Optional[TreeNode]) -\u0026gt; List[int]: 3 result = [] # 初始化一个空列表来存储遍历结果 4 self.inorder(root, result) # 调用辅助递归函数进行中序遍历 5 return result # 返回遍历结果列表 6 7 def inorder(self, root, result): 8 if not root: # 如果当前节点为空，直接返回 9 return 10 self.inorder(root.left, result) # 递归遍历左子树 11 result.append(root.val) # 将当前节点的值加入结果列表 12 self.inorder(root.right, result) # 递归遍历右子树 13 return result # 返回结果列表（虽然这个返回值在主方法中没有用到） 【方法二：栈】\n1class Solution: 2 def inorderTraversal(self, root: Optional[TreeNode]) -\u0026gt; List[int]: 3 stack, result = [], [] # 初始化栈和结果列表 4 5 while root or stack: # 当根节点不为空或栈不为空时循环 6 while root: # 遍历到当前子树的最左节点 7 stack.append(root) # 将当前节点入栈 8 root = root.left # 移动到左子节点 9 10 root = stack.pop() # 弹出栈顶节点（最左节点） 11 result.append(root.val) # 将弹出节点的值加入结果列表 12 root = root.right # 移动到右子节点 13 14 return result # 返回结果列表 104. 二叉树的最大深度 题目描述 给定一个二叉树root，返回其最大深度。 二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。\n示例：\n输入：root = [3,9,20,null,null,15,7] 输出：3 核心思路 【方法一：递归】 终止条件：如果当前节点（root）为空，说明我们已经到达了树的末端，这时候返回高度为0。 递归拆解：对每个节点，二叉树的最大深度等于其左子树和右子树中较大的那个深度再加上1。\n【方法二：层序遍历】 初始检查：如果根节点为空，则直接返回深度0。 初始化：使用一个队列来进行广度优先搜索（BFS），将根节点放入队列，并初始化最大深度为1。 BFS遍历：\n每次从队列中取出当前层的所有节点，检查它们的左子节点和右子节点，如果存在则加入队列。 每处理完一层后，增加最大深度计数器。 结束条件：当队列为空时，表示已遍历完所有节点，返回最大深度。 代码 【方法一：递归】\n1class Solution: 2 def maxDepth(self, root: Optional[TreeNode]) -\u0026gt; int: 3 if not root: 4 return 0 5 return max(self.maxDepth(root.left)+1,self.maxDepth(root.right)+1) 【方法二：广度优先搜索】\n1class Solution: 2 # 使用广度优先搜索 (BFS) 计算最大深度 3 def maxDepth(self, root: Optional[TreeNode]) -\u0026gt; int: 4 if not root: 5 return 0 # 空树的深度为0 6 7 maxDepth = 1 # 初始最大深度为1（至少有根节点） 8 queue = deque() # 创建一个队列来进行BFS遍历 9 queue.append(root) # 将根节点添加到队列中 10 11 while len(queue): # 当队列不为空时，继续遍历 12 length = len(queue) # 获取当前层的节点数量 13 for i in range(length): 14 head = queue.popleft() # 弹出当前层的第一个节点 15 if head.left is not None: # 如果左子节点存在，加入队列 16 queue.append(head.left) 17 if head.right is not None: # 如果右子节点存在，加入队列 18 queue.append(head.right) 19 if len(queue) == 0: # 如果队列为空，说明已遍历完所有节点 20 break 21 maxDepth += 1 # 每处理完一层，最大深度加1 22 23 return maxDepth # 返回最终的最大深度 226. 翻转二叉树 题目描述 给你一棵二叉树的根节点root，翻转这棵二叉树，并返回其根节点。\n示例： 输入：root = [4,2,7,1,3,6,9] 输出：[4,7,2,9,6,3,1] 核心思路 【方法一：DFS（递归）】\n检查根节点是否为空：如果根节点为空，则直接返回，因为空树不需要翻转。 递归翻转左右子树：递归调用翻转函数，先翻转左子树，再翻转右子树。确保每棵子树都被正确翻转。 交换左右子节点：在递归处理完左右子树之后，将当前节点的左子节点和右子节点进行交换。 返回翻转后的根节点：最后返回翻转后的根节点，以保证整棵树从上到下都是翻转后的结构。 【方法二：BFS（队列）】\n构造队列queue，对于root根节点，若root为空，则直接返回root；若root不为空，则将其入队。\n在队列不为空时，开始循环弹出队列队首元素。head=queue.pop(）。然后交换head 的左右节点，若head的左右节点不为空，则将其入队。\n最后，返回root。\n代码 【方法一：DFS（递归）】\n1class Solution: 2 def invertTree(self, root: Optional[TreeNode]) -\u0026gt; Optional[TreeNode]: 3 def exchange(root): 4 if not root: 5 return 6 root.left,root.right=root.right,root.left 7 exchange(root.left) 8 exchange(root.right) 9 exchange(root) 10 return root 【方法二：BFS（队列）】\n1class Solution: 2 def invertTree(self, root: Optional[TreeNode]) -\u0026gt; Optional[TreeNode]: 3 if not root: 4 return root 5 # 初始化队列并将根节点入队 6 queue = deque([root]) 7 # 当队列不为空时，进行广度优先遍历 8 while queue: 9 # 弹出队首元素 10 head = queue.popleft() 11 # 交换左右子节点 12 head.left, head.right = head.right, head.left 13 # 将非空的左右子节点分别入队 14 if head.left: 15 queue.append(head.left) 16 if head.right: 17 queue.append(head.right) 18 # 返回反转后的树的根节点 19 return root 543. 二叉树的直径 题目描述 给你一棵二叉树的根节点，返回该树的直径。 二叉树的直径是指树中任意两个节点之间最长路径的长度。这条路径可能经过也可能不经过根节点root。 两节点之间路径的长度由它们之间边数表示。\n示例： 输入：root = [1,2,3,4,5] 输出：3 解释：3 ，取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。 核心思路 在每个节点计算通过该节点的左子树深度和右子树深度。 计算通过该节点的路径长度（左子树深度 + 右子树深度）。 更新全局最大直径。 返回当前节点的深度（即 max(左子树深度, 右子树深度) + 1）。 代码 1class Solution: 2 def diameterOfBinaryTree(self, root: Optional[TreeNode]) -\u0026gt; int: 3 maxDiameter = 0 4 5 def depth(node: TreeNode) -\u0026gt; int: 6 nonlocal maxDiameter 7 if not node: 8 return 0 9 leftDepth = depth(node.left) 10 rightDepth = depth(node.right) 11 # 更新最大直径 12 maxDiameter = max(maxDiameter, leftDepth + rightDepth) 13 14 # 返回该节点的深度 15 return max(leftDepth, rightDepth) + 1 16 17 depth(root) 18 return maxDiameter 108. 将有序数组转换为二叉搜索树 题目描述 给你一个整数数组nums，其中元素已经按升序排列，请你将其转换为一棵平衡 二叉搜索树。\n平衡二叉树是一种特殊的二叉搜索树，其中任意节点的左右子树高度差不超过1。它可以保证在最坏情况下的查找、插入和删除操作的时间复杂度都是 O(log n) 级别。 平衡二叉树常用的实现方式有红黑树、AVL树、Treap等。\n示例： 输入：nums = [-10,-3,0,5,9] 输出：[0,-3,9,-10,null,5] 解释：[0,-10,5,null,-3,null,9] 也将被视为正确答案 核心思路 选择中间元素作为根节点：升序数组的中间元素自然地成为根节点，因为它可以将数组分成两部分，左边部分的所有元素都小于它，右边部分的所有元素都大于它。 递归构建左右子树： 对于根节点的左子树，从数组的开始位置到中间位置-1的部分，递归执行相同的过程。 对于根节点的右子树，从中间位置+1到数组的结束位置的部分，递归执行相同的过程。 停止条件：当子数组为空时，返回None。 代码 1class Solution: 2 def sortedArrayToBST(self, nums: List[int]) -\u0026gt; Optional[TreeNode]: 3 \u0026#34;\u0026#34;\u0026#34; 4 将排序数组转换为高度平衡的二叉搜索树。 5 6 :param nums: 排序数组 7 :return: 二叉搜索树的根节点 8 \u0026#34;\u0026#34;\u0026#34; 9 def helper(left: int, right: int) -\u0026gt; Optional[TreeNode]: 10 \u0026#34;\u0026#34;\u0026#34; 11 辅助函数，递归地将子数组转换为BST。 12 13 :param left: 子数组的左边界 14 :param right: 子数组的右边界 15 :return: 当前子数组对应的BST的根节点 16 \u0026#34;\u0026#34;\u0026#34; 17 if left \u0026gt; right: 18 return None # 如果左边界大于右边界，返回None，表示当前子数组为空 19 20 # 选择中间元素作为根节点 21 mid = (left + right) // 2 22 root = TreeNode(nums[mid]) 23 24 # 递归构建左子树和右子树 25 root.left = helper(left, mid - 1) 26 root.right = helper(mid + 1, right) 27 28 return root 29 30 # 从整个数组开始递归构建BST 31 return helper(0, len(nums) - 1) 102. 二叉树的层序遍历 题目描述 给你二叉树的根节点root，返回其节点值的层序遍历。 （即逐层地，从左到右访问所有节点）。\n示例： 输入：root = [3,9,20,null,null,15,7] 输出：[[3],[9,20],[15,7]] 核心思路 初始化：\n创建一个结果列表result，用于存储每一层的节点值。 创建一个双端队列queue，并将根节点加入队列。 遍历队列：\n当队列不为空时，进行以下步骤： 记录当前层的节点数level_size。 创建一个空列表current_level，用于存储当前层的节点值。 遍历当前层的所有节点： 从队列中取出一个节点node。 将node的值加入current_level。 如果node有左子节点，将左子节点加入队列。 如果node有右子节点，将右子节点加入队列。 将current_level加入result。 返回结果：\n遍历结束后，返回result。 代码 1class Solution: 2 def levelOrder(self, root: Optional[TreeNode]) -\u0026gt; List[List[int]]: 3 if not root: 4 return [] # 如果根节点为空，直接返回空列表 5 6 result = [] # 用于存储最终的层序遍历结果 7 queue = deque([root]) # 初始化队列，将根节点加入队列 8 9 while queue: 10 level_size = len(queue) # 记录当前层的节点数 11 current_level = [] # 用于存储当前层的节点值 12 13 for _ in range(level_size): 14 node = queue.popleft() # 从队列中取出一个节点 15 current_level.append(node.val) # 将节点值加入当前层的列表 16 17 if node.left: 18 queue.append(node.left) # 如果节点有左子节点，将左子节点加入队列 19 if node.right: 20 queue.append(node.right) # 如果节点有右子节点，将右子节点加入队列 21 22 result.append(current_level) # 将当前层的节点值列表加入结果列表 23 24 return result # 返回最终的层序遍历结果 98. 验证二叉搜索树 题目描述 给你一个二叉树的根节点root，判断其是否是一个有效的二叉搜索树。\n有效二叉搜索树定义如下：\n节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。 示例： 输入：root = [2,1,3] 输出：true 核心思路 验证一个二叉树是否为二叉搜索树（BST）的关键在于确保每个节点的值都在一个特定的范围内。具体来说： - 对于每个节点，其左子树的所有节点值必须小于该节点的值。 - 对于每个节点，其右子树的所有节点值必须大于该节点的值。\n可以通过递归的方法来实现这一点，同时传递一个范围给每个递归调用，确保当前节点的值在这个范围内。\n定义一个辅助函数：\nisValidBST，它接受三个参数：当前节点node、当前节点值的下限min_val和上限max_val。 递归终止条件：\n如果当前节点为空，返回True，因为空树是有效的BST。 如果当前节点的值不在min_val和max_val之间，返回False。 递归调用：\n递归检查左子树，左子树的值必须小于当前节点的值，所以传递min_val和node.val作为新的范围。 递归检查右子树，右子树的值必须大于当前节点的值，所以传递node.val和max_val作为新的范围。 返回结果：只有当左右子树都是有效的BST时，当前节点才是有效的BST。\n代码 1class Solution: 2\tdef isValidBST(root: TreeNode) -\u0026gt; bool: 3\tdef helper(node, min_val=float(\u0026#39;-inf\u0026#39;), max_val=float(\u0026#39;inf\u0026#39;)): 4\t# 如果当前节点为空，返回 True，因为空树是有效的BST 5\tif not node: 6\treturn True 7\t8\t# 检查当前节点的值是否在指定的范围内 9\tif not (min_val \u0026lt; node.val \u0026lt; max_val): 10\treturn False 11\t12\t# 递归检查左子树 13\t# 左子树的所有节点值必须小于当前节点的值 14\tleft_valid = helper(node.left, min_val, node.val) 15\t16\t# 递归检查右子树 17\t# 右子树的所有节点值必须大于当前节点的值 18\tright_valid = helper(node.right, node.val, max_val) 19\t20\t# 只有当左右子树都满足条件时，当前节点才是有效的BST 21\treturn left_valid and right_valid 22\t23\t# 初始调用，范围是负无穷到正无穷 24\treturn helper(root) 230. 二叉搜索树中第 K 小的元素 题目描述 给定一个二叉搜索树的根节点root，和一个整数k，请你设计一个算法查找其中第k小的元素（从 1 开始计数）。\n示例： 输入：root = [3,1,4,null,2], k = 1 输出：1 核心思路 在二叉搜索树（BST）中，左子树的所有节点值都小于根节点的值，右子树的所有节点值都大于根节点的值。利用这一性质，可以通过中序遍历来按升序访问所有节点。\n中序遍历：中序遍历二叉搜索树会得到一个有序列表（从小到大）。 提前终止：在遍历到第 k 个节点时，可以直接返回该节点的值，而不需要遍历完整棵树。 代码 1class Solution: 2 def kthSmallest(self, root: Optional[TreeNode], k: int) -\u0026gt; int: 3 stack = [] 4 current = root 5 6 while current or stack: 7 # 先遍历左子树，将所有左子节点压入栈中 8 while current: 9 stack.append(current) 10 current = current.left 11 12 # 从栈中弹出一个节点，访问该节点 13 current = stack.pop() 14 k -= 1 # 每访问一个节点，k 减 1 15 16 # 如果 k 为 0，说明当前节点是第 k 小的元素，直接返回 17 if k == 0: 18 return current.val 19 20 # 继续遍历右子树 21 current = current.right 199. 二叉树的右视图 题目描述 给定一个二叉树的根节点root，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。\n示例： 输入：[1,2,3,null,5,null,4] 输出：[1,3,4] 核心思路 我们需要逐层遍历二叉树，记录每层最右侧的节点，在每一层中，我们只关心最右侧的节点。\n初始化：\n创建一个队列，将根节点加入队列。 创建一个结果列表，用于存储从右侧看到的节点值。 层次遍历：\n使用一个while循环，只要队列不为空，就继续处理。 在每次循环开始时，记录当前队列的长度，这个长度表示当前层的节点数。 处理当前层：\n使用一个for循环，遍历当前层的所有节点。 在每次迭代中，从队列中取出一个节点。 如果是当前层的最后一个节点，将其值加入结果列表。 将当前节点的左子节点和右子节点依次加入队列。 返回结果：\n最终，结果列表中存储的就是从右侧看到的节点值。 代码 1class Solution: 2 def rightSideView(self, root: Optional[TreeNode]) -\u0026gt; List[int]: 3 if not root: 4 return [] # 如果根节点为空，返回空列表 5 6 result = [] # 用于存储从右侧看到的节点值 7 queue = deque([root]) # 初始化队列，将根节点加入队列 8 9 while queue: 10 # 当前层的节点数 11 level_length = len(queue) 12 13 for i in range(level_length): 14 # 从队列中取出一个节点 15 node = queue.popleft() 16 17 # 如果是当前层的最后一个节点，将其值加入结果列表 18 if i == level_length - 1: 19 result.append(node.val) 20 21 # 将当前节点的子节点加入队列 22 if node.left: 23 queue.append(node.left) 24 if node.right: 25 queue.append(node.right) 26 27 return result # 返回从右侧看到的节点值 114. 二叉树展开为链表 题目描述 给你二叉树的根结点root，请你将它展开为一个单链表：\n展开后的单链表应该同样使用TreeNode，其中right子指针指向链表中下一个结点，而左子指针始终为null。 展开后的单链表应该与二叉树先序遍历顺序相同。 示例： 输入：root = [1,2,5,3,4,null,6] 输出：[1,null,2,null,3,null,4,null,5,null,6] 核心思路 将二叉树展开为单链表的问题可以通过递归或迭代的方法来解决。最优的解题思路通常是利用后序遍历的思想，因为我们需要先处理子节点再处理父节点。\n后序遍历：由于我们需要先处理子节点再处理父节点，所以使用后序遍历（左-右-根）的思想。 调整指针：在遍历过程中，将当前节点的右子树接到左子树的最右节点的右子树上，然后将左子树变为右子树，左子树置为空。 递归调用：对每个节点都进行上述操作，直到遍历完整棵树。 代码 1class Solution: 2 def flatten(self, root: Optional[TreeNode]) -\u0026gt; None: 3 if not root: 4 return 5 6 # 先递归展开左右子树 7 self.flatten(root.left) 8 self.flatten(root.right) 9 10 # 如果有左子树 11 if root.left: 12 # 保存当前的右子树 13 right_subtree = root.right 14 15 # 将左子树移动到右子树的位置 16 root.right = root.left 17 root.left = None # 清空左子树 18 19 # 找到右子树的最右节点 20 current = root 21 while current.right: 22 current = current.right 23 24 # 将保存的原右子树接到最右节点 25 current.right = right_subtree 105. 从前序与中序遍历序列构造二叉树 题目描述 给定两个整数数组preorder和inorder，其中preorder是二叉树的先序遍历，inorder是同一棵树的中序遍历，请构造二叉树并返回其根节点。\n示例：\n输入：preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 输出：[3,9,20,null,null,15,7] 核心思路 理解先序和中序遍历的特点： - 先序遍历的第一个元素是树的根节点。 - 中序遍历中，根节点左边的部分是左子树，右边的部分是右子树。\n递归构建二叉树： - 使用先序遍历的第一个元素创建根节点。 - 在中序遍历中找到这个根节点的位置，这样可以确定左子树和右子树的范围。 - 递归地构建左子树和右子树。\n具体实现： - 定义一个递归函数buildTree，该函数接受先序遍历和中序遍历的子数组范围作为参数。 - 在每次递归调用中，使用先序遍历的第一个元素创建根节点，并在中序遍历中找到该根节点的位置。 - 根据根节点的位置，划分出左子树和右子树的范围，并递归构建左子树和右子树。\n代码 1class Solution: 2 def buildTree(self, preorder: List[int], inorder: List[int]) -\u0026gt; Optional[TreeNode]: 3 # 如果输入的先序或中序遍历为空，则返回 None 4 if not preorder or not inorder: 5 return None 6 7 # 先序遍历的第一个元素是根节点 8 root_val = preorder[0] 9 root = TreeNode(root_val) 10 11 # 在中序遍历中找到根节点的位置 12 root_index = inorder.index(root_val) 13 14 # 递归构建左子树 15 # 先序遍历中，根节点后的前 `root_index` 个元素是左子树的先序遍历 16 # 中序遍历中，根节点前的 `root_index` 个元素是左子树的中序遍历 17 root.left = self.buildTree(preorder[1:1 + root_index], inorder[:root_index]) 18 19 # 递归构建右子树 20 # 先序遍历中，从 `1 + root_index` 到末尾的元素是右子树的先序遍历 21 # 中序遍历中，从 `root_index + 1` 到末尾的元素是右子树的中序遍历 22 root.right = self.buildTree(preorder[1 + root_index:], inorder[root_index + 1:]) 23 24 # 返回构建的根节点 25 return root 437. 路径总和 III 题目描述 给定一个二叉树的根节点root，和一个整数targetSum，求该二叉树里节点值之和等于targetSum的路径的数目。\n路径不需要从根节点开始，也不需要在叶子节点结束，但是路径方向必须是向下的（只能从父节点到子节点）。\n示例：\n输入：root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8 输出：3 解释：和等于 8 的路径有 3 条，如图所示。 核心思路 这个问题可以使用递归结合前缀和的方式来求解。下面是详细步骤：\n前缀和的概念：\n前缀和是指从树的根节点到当前节点的路径上的所有节点的值之和。 我们使用一个哈希表（字典）来记录遍历过程中所有前缀和出现的次数。通过计算前缀和，我们可以快速查找某段路径的和是否等于 targetSum。 核心思路：\n遍历树时，我们要检查以当前节点为终点，有没有一条路径的和等于 targetSum。 我们可以通过计算当前路径和 currSum，再检查哈希表中是否有一个前缀和（即 currSum - targetSum）存在。 如果存在，说明从该前缀和之后的部分路径，其和就是 targetSum。 具体步骤\n用一个递归函数遍历二叉树，每到一个节点： 计算当前节点到根节点的路径和 currSum。 在哈希表中查找是否有 currSum - targetSum 的前缀和，存在说明从某段路径和等于 targetSum。 将当前路径和 currSum 加入哈希表，并将其出现次数增加1。 递归地遍历该节点的左子树和右子树。 返回时，将哈希表中当前路径和 currSum 的计数减1，避免影响其他路径。 代码 1class Solution: 2 def pathSum(self, root: Optional[TreeNode], targetSum: int) -\u0026gt; int: 3 def dfs(node, currSum): 4 if not node: 5 return 0 6 7 # 更新当前路径和 8 currSum += node.val 9 10 # 计算从某个祖先节点到当前节点的路径和是否等于 targetSum 11 # 即 (currSum - targetSum) 是否在 prefix_sum_count 中存在 12 count = prefix_sum_count.get(currSum - targetSum, 0) 13 14 # 更新前缀和字典，记录当前路径和出现的次数 15 prefix_sum_count[currSum] = prefix_sum_count.get(currSum, 0) + 1 16 17 # 递归遍历左子树和右子树 18 count += dfs(node.left, currSum) 19 count += dfs(node.right, currSum) 20 21 # 回溯：移除当前节点对应的路径和，以便不影响其他分支的路径计数 22 prefix_sum_count[currSum] -= 1 23 24 return count 25 26 # 前缀和哈希表，记录每个路径和出现的次数 27 # 初始值为 {0: 1}，表示从根节点到当前节点的路径和为0的路径有一条（即空路径） 28 prefix_sum_count = {0: 1} 29 30 return dfs(root, 0) 236. 二叉树的最近公共祖先 题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。\n百度百科中最近公共祖先的定义为：“对于有根树 T 的两个节点 p、q，最近公共祖先表示为一个节点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”\n示例 1：\n输入：root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出：3 解释：节点 5 和节点 1 的最近公共祖先是节点 3 。 核心思路 要找到二叉树中节点 p 和 q 的最近公共祖先（LCA）。有几个关键点： 1. 如果当前节点是 p 或 q，那么它就是一个祖先节点。 2. 如果 p 和 q 分别位于当前节点的左右子树，那么当前节点就是它们的最近公共祖先。 3. 如果两个节点都位于左子树或都位于右子树，则继续在对应子树中寻找。\n递归基：\n如果当前节点是 None，直接返回 None；如果当前节点是 p 或 q，直接返回当前节点。 递归查找左右子树：\n在左子树和右子树递归查找 p 和 q。 返回结果：\n如果左右子树都找到值，说明 p 和 q 分别在左右子树，当前节点是最近公共祖先。 如果只有左子树找到结果，返回左子树结果。 如果只有右子树找到结果，返回右子树结果。 代码 1class Solution: 2 def lowestCommonAncestor(self, root: \u0026#39;TreeNode\u0026#39;, p: \u0026#39;TreeNode\u0026#39;, q: \u0026#39;TreeNode\u0026#39;) -\u0026gt; \u0026#39;TreeNode\u0026#39;: 3 4 # 如果当前节点为空，说明已经到达叶子节点的底部，返回 None 5 # 如果当前节点是 p 或 q，说明我们找到了其中一个节点，返回当前节点 6 if not root or root == p or root == q: 7 return root 8 9 # 在左子树中递归查找 p 和 q，返回在左子树中的结果 10 left = self.lowestCommonAncestor(root.left, p, q) 11 12 # 在右子树中递归查找 p 和 q，返回在右子树中的结果 13 right = self.lowestCommonAncestor(root.right, p, q) 14 15 # 如果左子树和右子树都找到了 p 或 q，那么当前节点 root 就是最近公共祖先 16 # 因为 p 和 q 分别在当前节点的左右子树中 17 if left and right: 18 return root 19 20 # 如果只有左子树找到结果（right 为 None），说明 p 和 q 都在左子树中 21 # 返回左子树的结果 22 # 如果只有右子树找到结果（left 为 None），说明 p 和 q 都在右子树中 23 # 返回右子树的结果 24 return left if left else right 图论 200. 岛屿数量 题目描述 给你一个由1（陆地）和0（水）组成的的二维网格，请你计算网格中岛屿的数量。\n岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。\n此外，你可以假设该网格的四条边均被水包围。\n示例：\n输入：grid = [ [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;] ] 输出：1 核心思路 使用DFS处理岛屿数量问题时更加直观和简洁。\n遍历网格：我们需要遍历整个网格中的每一个单元格。 遇到陆地时：如果遇到一个值为 \u0026lsquo;1\u0026rsquo; 的单元格，表示这是一个岛屿的一部分。 标记岛屿：使用DFS从这个单元格开始，将所有与之相连的陆地标记为 \u0026lsquo;0\u0026rsquo;，以避免重复计数。 计数岛屿：每次启动DFS时，表示发现了一个新的岛屿，因此计数器加一。 代码 1class Solution: 2 def numIslands(self, grid: List[List[str]]) -\u0026gt; int: 3 # 如果网格为空，直接返回0 4 if not grid: 5 return 0 6 7 # 获取网格的行数和列数 8 rows, cols = len(grid), len(grid[0]) 9 count = 0 10 11 def dfs(r: int, c: int): 12 \u0026#34;\u0026#34;\u0026#34; 13 使用深度优先搜索 (DFS) 标记从 (r, c) 开始的所有相连的陆地。 14 \u0026#34;\u0026#34;\u0026#34; 15 # 检查边界条件，如果超出边界或者当前单元格是水，则返回 16 if r \u0026lt; 0 or r \u0026gt;= rows or c \u0026lt; 0 or c \u0026gt;= cols or grid[r][c] == \u0026#39;0\u0026#39;: 17 return 18 19 # 将当前单元格标记为已访问（即变为 \u0026#39;0\u0026#39;） 20 grid[r][c] = \u0026#39;0\u0026#39; 21 22 # 递归访问上下左右四个方向 23 dfs(r + 1, c) # 下 24 dfs(r - 1, c) # 上 25 dfs(r, c + 1) # 右 26 dfs(r, c - 1) # 左 27 28 # 遍历整个网格 29 for r in range(rows): 30 for c in range(cols): 31 # 如果当前单元格是陆地（\u0026#39;1\u0026#39;），则发现了一个新的岛屿 32 if grid[r][c] == \u0026#39;1\u0026#39;: 33 count += 1 34 # 使用DFS标记整个岛屿 35 dfs(r, c) 36 37 return count 994. 腐烂的橘子 题目描述 在给定的m x n网格grid中，每个单元格可以有以下三个值之一：\n值0代表空单元格； 值1代表新鲜橘子； 值2代表腐烂的橘子。 每分钟，腐烂的橘子周围4 个方向上相邻的新鲜橘子都会腐烂。\n返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能，返回-1。\n示例： 输入：grid = [[2,1,1],[1,1,0],[0,1,1]] 输出：4 核心思路 这道题是一道经典的 “多源最短路径” 问题，可以通过广度优先搜索（BFS）来解决。\n初始化队列：首先遍历整个网格，找到所有初始时腐烂的橘子，并将它们的位置加入到队列中。同时，统计新鲜橘子的数量。 广度优先搜索（BFS）：使用队列进行BFS，每次处理队列中的所有腐烂橘子，将它们周围的新鲜橘子变为腐烂橘子，并将新腐烂的橘子加入队列。每处理完一轮，时间增加1分钟。 检查结果：当队列为空时，检查是否还有新鲜橘子。如果没有新鲜橘子，返回经过的分钟数；如果有，返回-1。 代码 1class Solution: 2 def orangesRotting(self, grid: List[List[int]]) -\u0026gt; int: 3 if not grid: 4 return -1 5 6 rows, cols = len(grid), len(grid[0]) 7 fresh_count = 0 8 queue = deque() 9 10 # 初始化队列和新鲜橘子计数 11 for r in range(rows): 12 for c in range(cols): 13 if grid[r][c] == 2: 14 queue.append((r, c)) # 将所有初始腐烂的橘子加入队列 15 elif grid[r][c] == 1: 16 fresh_count += 1 # 统计新鲜橘子的数量 17 18 # 定义四个方向：上下左右 19 directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] 20 minutes_passed = 0 # 记录时间 21 22 # 广度优先搜索 23 while queue and fresh_count \u0026gt; 0: 24 minutes_passed += 1 # 每处理完一轮，时间增加1分钟 25 for _ in range(len(queue)): 26 r, c = queue.popleft() # 从队列中取出一个腐烂的橘子 27 for dr, dc in directions: 28 nr, nc = r + dr, c + dc # 计算相邻的单元格位置 29 if 0 \u0026lt;= nr \u0026lt; rows and 0 \u0026lt;= nc \u0026lt; cols and grid[nr][nc] == 1: 30 # 如果相邻单元格是新鲜橘子，将其变为腐烂橘子 31 grid[nr][nc] = 2 32 fresh_count -= 1 # 新鲜橘子数量减少1 33 queue.append((nr, nc)) # 将新腐烂的橘子加入队列 34 35 # 检查是否还有新鲜橘子 36 if fresh_count == 0: 37 return minutes_passed # 如果没有新鲜橘子，返回经过的分钟数 38 else: 39 return -1 # 如果还有新鲜橘子，返回-1 207. 课程表 题目描述 你这个学期必须选修numCourses门课程，记为0到numCourses - 1。\n在选修某些课程之前需要一些先修课程。 先修课程按数组prerequisites给出，其中prerequisites[i] = [ai, bi]，表示如果要学习课程ai则必须先学习课程bi。\n例如，先修课程对[0, 1]表示：想要学习课程0，你需要先完成课程1。 请你判断是否可能完成所有课程的学习？如果可以，返回true；否则，返回false。\n示例：\n输入：numCourses = 2, prerequisites = [[1,0]] 输出：true 解释：总共有 2 门课程。学习课程 1 之前，你需要完成课程 0 。这是可能的。 核心思路 这个问题的核心是判断是否有依赖冲突。你可以把课程想象成一系列任务，部分任务必须先完成其他任务才能开始。如果出现循环依赖，就意味着有些任务永远无法完成（例如任务 A 依赖任务 B，B 又依赖 A），所以问题的关键就是要检查这些循环依赖是否存在。\n建立依赖关系：\n假设有很多课程，每门课可能需要先修另一门课。我们可以用一个图来表示这些课程之间的依赖关系。 图中的每个节点就是一门课，边则表示“先修关系”（从一门课指向另一门先修课）。 找到没有依赖的课程：\n如果一门课不需要任何先修课（没有依赖），我们可以直接开始学这门课。 依次完成这些课程：\n我们先学完没有依赖的课程，然后看看哪些课程现在已经可以学了（它们之前依赖的课程已经完成了）。 依次重复这个过程，直到所有课程都学完，或者发现有些课程永远学不了（说明有循环依赖）。 检查是否有循环依赖：\n如果我们可以把所有课程都顺利完成，那就没有循环依赖，可以学完所有课程。 如果有些课程永远无法学完，那就有循环依赖，无法完成所有课程。 代码 1class Solution: 2 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -\u0026gt; bool: 3 # 1. 初始化邻接表和入度表 4 # 邻接表（graph）：记录每门课的后续课，即哪些课依赖于这门课 5 # 入度表（in_degree）：记录每门课有多少门前置课（依赖的课程） 6 graph = {i: [] for i in range(numCourses)} # 每门课的后续课程列表 7 in_degree = {i: 0 for i in range(numCourses)} # 每门课的前置课个数 8 9 # 2. 构建图结构和入度表 10 # 遍历所有的前置课程关系 [a, b]，表示要先学课程 b 再学课程 a 11 for course, prereq in prerequisites: 12 graph[prereq].append(course) # 课程 b 指向课程 a，表示 a 依赖 b 13 in_degree[course] += 1 # 课程 a 的前置课程数加 1 14 15 # 3. 找到所有没有前置课程的课程（即入度为 0 的课程） 16 queue = deque([course for course in in_degree if in_degree[course] == 0]) 17 18 # 记录已经学完的课程数量 19 completed_courses = 0 20 21 # 4. 进行广度优先搜索（BFS） 22 while queue: 23 current_course = queue.popleft() # 从队列中取出一门可以学习的课程 24 completed_courses += 1 # 这门课算作已经学完 25 26 # 对这门课的后续课程进行处理 27 for next_course in graph[current_course]: 28 in_degree[next_course] -= 1 # 该后续课的前置课程少了一门 29 if in_degree[next_course] == 0: # 如果该课的前置课程已经全部学完 30 queue.append(next_course) # 将其加入队列 31 32 # 5. 判断是否所有课程都学完了 33 return completed_courses == numCourses # 如果已学完的课程数量等于总课程数，则返回 True 208. 实现 Trie (前缀树) 题目描述 Trie（发音类似 \u0026ldquo;try\u0026rdquo;）或者说前缀树是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景，例如自动补全和拼写检查。\n请你实现 Trie 类：\nTrie()初始化前缀树对象。 void insert(String word)向前缀树中插入字符串word。 boolean search(String word)如果字符串word在前缀树中，返回true（即，在检索之前已经插入）；否则，返回false。 boolean startsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix，返回true；否则，返回false。 示例：\n输入： [\u0026#34;Trie\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;search\u0026#34;, \u0026#34;search\u0026#34;, \u0026#34;startsWith\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;search\u0026#34;] [[], [\u0026#34;apple\u0026#34;], [\u0026#34;apple\u0026#34;], [\u0026#34;app\u0026#34;], [\u0026#34;app\u0026#34;], [\u0026#34;app\u0026#34;], [\u0026#34;app\u0026#34;]] 输出： [null, null, true, false, true, null, true] 解释： Trie trie = new Trie(); trie.insert(\u0026#34;apple\u0026#34;); trie.search(\u0026#34;apple\u0026#34;); // 返回 True trie.search(\u0026#34;app\u0026#34;); // 返回 False trie.startsWith(\u0026#34;app\u0026#34;); // 返回 True trie.insert(\u0026#34;app\u0026#34;); trie.search(\u0026#34;app\u0026#34;); // 返回 True 核心思路 Trie（字典树）简介：\nTrie 是一种树形结构，专门用于高效存储和查找字符串集合，常用于字典中的单词搜索。 每个节点代表一个字符，路径代表某个单词的前缀。 最常见的操作有插入一个单词、搜索某个单词是否存在、以及判断某个前缀是否存在。 init 方法：\n初始化字典树的根节点。用一个字典（dict）来存储，根节点是空字典 {}。 insert 方法：\n逐个字符插入到字典树中，从根节点开始。如果当前字符已经存在，则移动到下一节点；否则就创建一个新的字典节点。 在单词的最后一个字符插入后，添加一个特殊标记符（#），表示该节点代表一个完整单词的结束。 search 方法：\n用于搜索某个单词是否在 Trie 中存在。 逐个字符从根节点开始查找，如果某个字符不在节点中，直接返回 False，表示没有这个单词。 如果能遍历完整个单词，并且最后一个字符的节点有结束标记符（#），说明这个单词存在。 startsWith 方法：\n检查是否存在以某个前缀开头的单词。 逐个字符检查，如果当前字符在 Trie 中存在，就继续；否则返回 False，表示没有该前缀。 如果可以遍历完整个前缀，返回 True，表示存在这个前缀。 代码 1class Trie: 2 def __init__(self): 3 # 初始化一个空的字典树根节点，根节点本身也是一个字典 4 self.root = {} 5 6 def insert(self, word: str) -\u0026gt; None: 7 # 从根节点开始逐个插入字符 8 node = self.root 9 for char in word: 10 # 如果当前字符不在当前节点中，添加这个字符 11 if char not in node: 12 node[char] = {} 13 node = node[char] 14 # 插入完成后，在末尾标记此位置为单词结束 15 node[\u0026#39;#\u0026#39;] = True # \u0026#39;#\u0026#39; 是结束标记符，表示这个路径为一个完整单词 16 17 def search(self, word: str) -\u0026gt; bool: 18 # 从根节点开始搜索字符 19 node = self.root 20 for char in word: 21 # 如果字符不在节点中，则表示字典树中不存在该单词 22 if char not in node: 23 return False 24 node = node[char] 25 # 判断最后的节点是否有单词结束标记 26 return \u0026#39;#\u0026#39; in node 27 28 def startsWith(self, prefix: str) -\u0026gt; bool: 29 # 从根节点开始逐个字符检查前缀 30 node = self.root 31 for char in prefix: 32 # 如果前缀中的字符不在字典树中，返回 False 33 if char not in node: 34 return False 35 node = node[char] 36 # 如果可以走完前缀，说明存在该前缀 37 return True 回溯法 46. 全排列 题目描述 给定一个不含重复数字的数组nums，返回其_所有可能的全排列_。你可以按任意顺序返回答案。\n示例：\n输入：nums = [1,2,3] 输出：[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] 核心思路 回溯算法是用来解决组合、排列、子集等问题的有效方法。它通过在解的空间中一步步探索可能的解，试图找到符合要求的解集。如果发现某个路径不能达成目标，就回到上一步，尝试其他路径。\n初始化：\n首先，我们准备一个空的列表 result，用来存储所有符合条件的解，比如全排列。 还需要一个 path 列表，用来存储当前路径上的元素（即正在构造的一个解）。 递归函数（backtrack）：\n这是回溯算法的核心。它负责一步步地往下探索并做出选择。 终止条件： 当 path 的长度等于原数组的长度时（就是已经用完了所有元素），说明我们找到了一个完整的解。这时就把 path 加入 result。 选择与递归： 在每一步，我们从剩余的候选元素（比如原数组的未使用元素）中选一个，放到 path 中。 然后递归调用 backtrack，继续从剩下的元素中选择。 回溯操作： 当递归返回后（说明已经完成了当前路径的探索），我们要把刚才添加的元素从 path 中移除，回到上一步。这样可以进行下一次选择。 调用递归函数：\n初始调用时，path 是空的，剩余的候选元素就是输入的数组。递归会一步步填满 path，直到找到所有可能的排列。 返回结果：\n最后返回 result，它包含了所有符合条件的解。 代码 【回溯解法】\n1class Solution: 2 def permute(self, nums: List[int]) -\u0026gt; List[List[int]]: 3 def backtrack(path, remaining): 4 # 当剩余元素为空时，说明已经生成了一个完整的排列 5 if not remaining: 6 result.append(path[:]) # 将当前路径的副本添加到结果中（使用 path[:] 防止引用问题） 7 return 8 # 遍历剩余的每一个元素，尝试将其加入当前排列路径 9 for i in range(len(remaining)): 10 # 选择：将 remaining[i] 添加到路径中 11 path.append(remaining[i]) 12 # 递归：将元素 i 从剩余元素中移除，继续生成剩下的排列 13 # remaining[:i] + remaining[i+1:] 表示除了第 i 个元素的其余元素 14 backtrack(path, remaining[:i] + remaining[i+1:]) 15 # 回溯：撤销选择，将刚加入的元素从路径中移除，尝试其他选择 16 path.pop() 17 18 result = [] # 存储所有生成的排列结果 19 backtrack([], nums) # 初始时路径为空，剩余元素为 nums 20 return result # 返回所有排列结果 【暴力递归解法】\n1class Solution: 2 def permute(self, nums): 3 # 如果数组为空，返回一个空列表 4 if len(nums) == 0: 5 return [] 6 # 如果数组只有一个元素，返回它自己 7 if len(nums) == 1: 8 return [nums] 9 10 result = [] 11 # 遍历数组中的每一个元素 12 for i in range(len(nums)): 13 # 当前选择的元素 14 current = nums[i] 15 # 剩余的元素（除了当前元素之外的部分） 16 remaining = nums[:i] + nums[i+1:] 17 18 # 对剩余元素递归生成所有排列 19 for p in self.permute(remaining): 20 # 将当前元素加入到递归生成的排列前面 21 result.append([current] + p) 22 23 return result 78. 子集 题目描述 给你一个整数数组nums，数组中的元素互不相同。返回该数组所有可能的子集（幂集）。\n解集不能包含重复的子集。你可以按任意顺序返回解集。\n示例：\n输入：nums = [1,2,3] 输出：[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] 核心思路 使用了迭代的方法来生成一个数组的所有子集。具体来说，通过逐步扩展当前已有的子集来生成新的子集。\n初始化结果列表：\n初始化结果列表result，其中包含一个空子集。这是因为在任何数组的子集中，空集总是存在的。 遍历每个元素：\n遍历输入数组nums中的每个元素num。对于每个元素，我们都会生成新的子集并将其添加到结果列表中。 生成新的子集：\n对于当前结果列表result中的每个子集subset，生成一个新的子集，该子集是在原子集的基础上加上当前元素num。 将新生成的子集直接添加到结果列表result中。extend方法会将可迭代对象中的所有元素添加到列表的末尾。 返回结果：\n返回包含所有子集的结果列表result。 代码 1class Solution: 2 def subsets(self, nums: List[int]) -\u0026gt; List[List[int]]: 3 result = [[]] 4 5 # 遍历 nums 数组中的每个元素 6 for num in nums: 7 # 使用列表推导式生成新的子集并直接添加到结果集中 8 result.extend(subset + [num] for subset in result) 9 10 return result 17. 电话号码的字母组合 题目描述 给定一个仅包含数字2-9的字符串，返回所有它能表示的字母组合。答案可以按任意顺序返回。\n给出数字到字母的映射如下（与电话按键相同）。注意 1 不对应任何字母。\n示例：\n输入：digits = \u0026#34;23\u0026#34; 输出：[\u0026#34;ad\u0026#34;,\u0026#34;ae\u0026#34;,\u0026#34;af\u0026#34;,\u0026#34;bd\u0026#34;,\u0026#34;be\u0026#34;,\u0026#34;bf\u0026#34;,\u0026#34;cd\u0026#34;,\u0026#34;ce\u0026#34;,\u0026#34;cf\u0026#34;] 核心思路 这个问题的核心是将数字和字母映射，根据给定的数字字符串生成所有可能的字母组合。\n映射关系：首先，将每个数字映射到对应的字母，类似于手机键盘上的按键。\n组合构建：从给定的数字字符串中的每个数字开始，逐步构建字母组合。每次处理一个数字时，将这个数字对应的所有字母依次加到已经生成的所有组合的末尾。\n迭代更新：初始时，组合是空的 [\u0026rsquo;\u0026rsquo;]，然后遍历每个数字，逐步构建新的组合，直到所有数字都被处理完。每处理一个数字，结果集就会扩展，加入所有可能的新组合。\n最终结果：当所有数字都处理完后，生成的列表就是所有可能的字母组合。\n代码 1class Solution: 2 def letterCombinations(self, digits: str) -\u0026gt; List[str]: 3 if not digits: 4 return [] 5 6 digit_to_letters = { 7 \u0026#39;2\u0026#39;: \u0026#39;abc\u0026#39;, \u0026#39;3\u0026#39;: \u0026#39;def\u0026#39;, \u0026#39;4\u0026#39;: \u0026#39;ghi\u0026#39;, \u0026#39;5\u0026#39;: \u0026#39;jkl\u0026#39;, 8 \u0026#39;6\u0026#39;: \u0026#39;mno\u0026#39;, \u0026#39;7\u0026#39;: \u0026#39;pqrs\u0026#39;, \u0026#39;8\u0026#39;: \u0026#39;tuv\u0026#39;, \u0026#39;9\u0026#39;: \u0026#39;wxyz\u0026#39; 9 } 10 11 result = [\u0026#39;\u0026#39;] 12 13 for digit in digits: 14 # 重新构建结果列表，每个结果加上当前数字对应的字母 15 new_result = [] 16 for letter in digit_to_letters[digit]: 17 for combination in result: 18 new_result.append(combination + letter) 19 result = new_result 20 21 return result 39. 组合总和 题目描述 给你一个无重复元素的整数数组candidates和一个目标整数target，找出candidates中可以使数字和为目标数target的 所有不同组合，并以列表形式返回。你可以按任意顺序返回这些组合。\ncandidates中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同，则两种组合是不同的。\n对于给定的输入，保证和为target的不同组合数少于150个。\n示例：\n输入：candidates = [2,3,6,7], target = 7 输出：[[2,2,3],[7]] 解释： 2 和 3 可以形成一组候选，2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选， 7 = 7 。 仅有这两种组合。 核心思路 通过递归不断尝试所有可能的组合，最终找到那些和等于目标数的组合。\n递归探索：从数组 candidates 中选择一个元素开始尝试，不断向当前组合中添加元素，直到总和等于目标数 target。如果总和超过目标数，就回退（回溯）。\n剪枝优化：为了避免重复计算，每次递归时，我们只考虑当前元素及其之后的元素（避免重复组合）。\n重复选择元素：因为题目允许一个数字可以被无限制重复使用，因此递归时可以再次选择当前元素。\n代码 1class Solution: 2 def combinationSum(self, candidates, target): 3 result = [] 4 5 # 回溯函数，参数分别是当前组合路径、当前的总和、候选元素的起始索引 6 def backtrack(remaining, path, start): 7 # 如果剩余的数为 0，说明找到了一组合法的组合，加入结果集 8 if remaining == 0: 9 result.append(path[:]) 10 return 11 # 如果剩余数小于 0，说明当前组合不合法，直接返回 12 if remaining \u0026lt; 0: 13 return 14 15 # 从当前元素开始，尝试每个候选元素 16 for i in range(start, len(candidates)): 17 # 选择当前元素，递归搜索 18 path.append(candidates[i]) 19 # 递归调用，remaining 减去当前元素，start 保持不变以允许重复选择同一元素 20 backtrack(remaining - candidates[i], path, i) 21 # 回溯：撤销当前选择 22 path.pop() 23 24 # 调用回溯函数，初始路径为空，目标是 target，从第 0 个元素开始 25 backtrack(target, [], 0) 26 27 return result 22. 括号生成 题目描述 数字n代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且有效的括号组合。\n示例：\n输入：n = 3 输出：[\u0026#34;((()))\u0026#34;,\u0026#34;(()())\u0026#34;,\u0026#34;(())()\u0026#34;,\u0026#34;()(())\u0026#34;,\u0026#34;()()()\u0026#34;] 核心思路 条件判断：\n有效的括号 有两个关键条件： 左括号的数量不能超过 n，即在递归过程中，我们不能放置超过 n 个左括号。 右括号的数量不能超过已经放置的左括号数量，保证当前组合是有效的。 递归终止条件：\n当生成的字符串的长度等于 2 * n，说明所有括号对都放置完毕，我们将当前生成的字符串加入结果列表中。 回溯过程：\n每次递归时，我们有两个选择： 如果左括号还没放满（left \u0026lt; n），我们可以放一个左括号 \u0026lsquo;(\u0026rsquo;。 如果右括号数量小于左括号（right \u0026lt; left），可以放一个右括号 \u0026lsquo;)\u0026rsquo;。 放置一个括号后，继续递归构建剩下的组合；一旦递归完成后，回溯到之前的状态，尝试其他可能。 代码 1class Solution: 2 def generateParenthesis(self, n: int) -\u0026gt; List[str]: 3 def backtrack(path, left, right): 4 # 如果生成的括号长度达到了 2 * n，说明所有括号对已经放置完毕 5 if len(path) == 2 * n: 6 result.append(\u0026#34;\u0026#34;.join(path)) # 将生成的括号组合加入结果列表 7 return 8 9 # 如果左括号数量还没有达到 n，继续放置左括号 10 if left \u0026lt; n: 11 path.append(\u0026#39;(\u0026#39;) # 放置左括号 12 backtrack(path, left + 1, right) # 递归 13 path.pop() # 回溯，撤销放置的左括号 14 15 # 如果右括号数量小于左括号数量，继续放置右括号 16 if right \u0026lt; left: 17 path.append(\u0026#39;)\u0026#39;) # 放置右括号 18 backtrack(path, left, right + 1) # 递归 19 path.pop() # 回溯，撤销放置的右括号 20 21 result = [] 22 backtrack([], 0, 0) # 初始状态：空路径，0 左括号，0 右括号 23 return result 79. 单词搜索 题目描述 给定一个m x n二维字符网格board和一个字符串单词word。如果word存在于网格中，返回true；否则，返回false。\n单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。\n示例： 输入：board = [[\u0026#34;A\u0026#34;,\u0026#34;B\u0026#34;,\u0026#34;C\u0026#34;,\u0026#34;E\u0026#34;],[\u0026#34;S\u0026#34;,\u0026#34;F\u0026#34;,\u0026#34;C\u0026#34;,\u0026#34;S\u0026#34;],[\u0026#34;A\u0026#34;,\u0026#34;D\u0026#34;,\u0026#34;E\u0026#34;,\u0026#34;E\u0026#34;]], word = \u0026#34;ABCCED\u0026#34; 输出：true 核心思路 搜索起点： 从网格中的每个位置作为起点，尝试查找单词的第一个字母。 递归查找： 从匹配到的第一个字母开始，递归地查找下一个字母。 每个递归步骤中，我们会尝试向四个方向（上下左右）移动，寻找下一个字母。 回溯： 如果找到匹配的单词，返回 true。 如果当前路径无法匹配到单词，回溯并尝试其他可能的路径。 终止条件： 如果匹配到了单词的所有字母，返回 true。 如果当前字符不匹配或越界，则停止当前路径的搜索。 代码 1class Solution: 2 def exist(self, board: List[List[str]], word: str) -\u0026gt; bool: 3 rows, cols = len(board), len(board[0]) 4 5 # 定义回溯函数，参数为当前搜索的行、列以及当前匹配到的字符索引 6 def backtrack(r, c, index): 7 if index == len(word): 8 return True 9 10 # 如果越界，或者当前单元格的字符不匹配，返回 False 11 if r \u0026lt; 0 or r \u0026gt;= rows or c \u0026lt; 0 or c \u0026gt;= cols or board[r][c] != word[index]: 12 return False 13 14 # 暂时标记这个单元格为已访问，避免重复使用 15 temp, board[r][c] = board[r][c], \u0026#39;#\u0026#39; 16 17 # 四个方向进行探索：上、下、左、右 18 found = ( 19 backtrack(r + 1, c, index + 1) or # 向下 20 backtrack(r - 1, c, index + 1) or # 向上 21 backtrack(r, c + 1, index + 1) or # 向右 22 backtrack(r, c - 1, index + 1) # 向左 23 ) 24 25 # 回溯：恢复当前单元格的值 26 board[r][c] = temp 27 28 return found 29 30 # 遍历每一个网格中的位置，尝试寻找单词的第一个字母 31 for i in range(rows): 32 for j in range(cols): 33 # 如果从某个位置找到了单词，直接返回 True 34 if backtrack(i, j, 0): 35 return True 36 37 # 如果遍历完所有的起点都没有找到，返回 False 38 return False 131. 分割回文串 题目描述 给你一个字符串s，请你将s分割成一些子串，使每个子串都是回文串。返回s所有可能的分割方案。\n示例：\n输入：s = \u0026#34;aab\u0026#34; 输出：[[\u0026#34;a\u0026#34;,\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;],[\u0026#34;aa\u0026#34;,\u0026#34;b\u0026#34;]] 核心思路 选择分割点：\n从字符串的起始位置开始，每次选择一个分割点，将字符串分割成一个前缀（子串）和剩余的部分。 检查前缀是否是回文串，如果是，则递归处理剩余的部分。 递归和回溯：\n对于每个分割点，如果当前子串是回文串，则递归地处理剩余部分。 一旦递归到字符串的末尾，说明已经完成了一次有效的分割，将结果加入最终解集中。 回溯时，撤销上一步的选择，继续尝试新的分割方案。 回文检查：\n使用一个辅助函数来判断一个字符串是否是回文。 代码 1class Solution: 2 def partition(self, s: str) -\u0026gt; List[List[str]]: 3 def is_palindrome(sub: str) -\u0026gt; bool: 4 # 判断子串是否是回文 5 return sub == sub[::-1] 6 7 def backtrack(start: int, path: List[str]): 8 # 递归终止条件：如果已经到达字符串末尾，表示完成了一次有效分割 9 if start == len(s): 10 result.append(path[:]) 11 return 12 13 # 尝试从 start 开始的每一个位置分割字符串 14 for end in range(start + 1, len(s) + 1): 15 # 获取当前子串 s[start:end] 16 substring = s[start:end] 17 18 # 如果当前子串是回文，继续递归处理剩余部分 19 if is_palindrome(substring): 20 path.append(substring) # 选择当前子串 21 backtrack(end, path) # 递归处理剩余部分 22 path.pop() # 回溯，撤销选择 23 24 result = [] 25 backtrack(0, []) 26 return result 二分查找 35. 搜索插入位置 题目描述 给定一个排序数组和一个目标值，在数组中找到目标值，并返回其索引。如果目标值不存在于数组中，返回它将会被按顺序插入的位置。 请必须使用时间复杂度为O(log n)的算法。\n示例：\n输入：nums = [1,3,5,6], target = 5 输出：2 输入：nums = [1,3,5,6], target = 7 输出：4 核心思路 初始化：设置两个指针left和right分别指向数组的起始和结束位置。 二分查找： 计算中间索引mid = left + (right - left) // 2。 如果nums[mid]等于目标值target，返回mid。 如果nums[mid]大于目标值，更新right为mid - 1。 如果nums[mid]小于目标值，更新left为mid + 1。 返回插入位置：如果没有找到目标值，循环结束后left即为目标值将要被插入的位置。 代码 1class Solution: 2 def searchInsert(self, nums: List[int], target: int) -\u0026gt; int: 3 left, right = 0, len(nums) - 1 4 while left \u0026lt;= right: 5 mid = left + (right - left) // 2 6 if nums[mid] == target: 7 return mid 8 elif nums[mid] \u0026gt; target: 9 right = mid - 1 10 else: 11 left = mid + 1 12 return left 74. 搜索二维矩阵 题目描述 给你一个满足下述两条属性的m x n整数矩阵：\n每行中的整数从左到右按非严格递增顺序排列。 每行的第一个整数大于前一行的最后一个整数。 给你一个整数target，如果target在矩阵中，返回true；否则，返回false。\n示例：\n输入：matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 输出：true 核心思路 从矩阵的右上角开始搜索，这样每一步都可以通过比较大小来确定下一步的方向。详细步骤如下：\n从右上角开始：我们选择从矩阵的右上角（即第一行的最后一个元素）开始。这个位置有一个特殊的性质：\n如果当前元素比目标值 target 大，我们可以往左移动，因为同一行左边的元素更小。 如果当前元素比目标值 target 小，我们可以向下移动，因为同一列下面的元素更大。 不断缩小搜索空间：根据上面的逻辑，我们每次都可以抛弃一整行或一整列，因此每次比较都有效地缩小了搜索范围。\n终止条件：如果找到了目标值，返回 True。如果搜索越界（即行列索引超出矩阵范围），则返回 False，表示未找到目标值。\n代码 1class Solution: 2 def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: 3 # 获取矩阵的行数和列数 4 if not matrix or not matrix[0]: 5 return False 6 rows, cols = len(matrix), len(matrix[0]) 7 8 # 从右上角开始,rows行数，cols列数 9 row, col = 0, cols - 1 10 11 while row \u0026lt; rows and col \u0026gt;= 0: 12 if matrix[row][col] == target: 13 return True 14 elif matrix[row][col] \u0026gt; target: 15 # 如果当前元素比目标大，向左移动 16 col -= 1 17 else: 18 # 如果当前元素比目标小，向下移动 19 row += 1 20 21 return False 34. 在排序数组中查找元素的第一个和最后一个位置 题目描述 给你一个按照非递减顺序排列的整数数组nums，和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。\n如果数组中不存在目标值target，返回[-1, -1]。\n你必须设计并实现时间复杂度为O(log n)的算法解决此问题。\n示例：\n输入：nums = [5,7,7,8,8,10], target = 8 输出：[3,4] 核心思路 要实现时间复杂度为 O(log n) 的算法解决这个问题，可以使用二分查找。 具体来说，我们需要两次二分查找：一次找到目标值的开始位置，另一次找到目标值的结束位置。\n找到目标值的开始位置：\n使用二分查找，但当找到目标值时，不立即返回，而是继续向左查找，直到找到最左边的目标值。 找到目标值的结束位置：\n使用二分查找，但当找到目标值时，不立即返回，而是继续向右查找，直到找到最右边的目标值。 处理不存在目标值的情况：\n如果在第一次查找中没有找到目标值，直接返回 [-1, -1]。 代码 1class Solution: 2 def searchRange(nums, target): 3 def binary_search(nums, target, find_first): 4 left, right = 0, len(nums) - 1 5 result = -1 6 while left \u0026lt;= right: 7 mid = (left + right) // 2 8 if nums[mid] == target: 9 result = mid 10 if find_first: 11 right = mid - 1 # 寻找左边界 12 else: 13 left = mid + 1 # 寻找右边界 14 elif nums[mid] \u0026lt; target: 15 left = mid + 1 16 else: 17 right = mid - 1 18 return result 19 20 start = binary_search(nums, target, True) 21 end = binary_search(nums, target, False) 22 23 return [start, end] if start != -1 else [-1, -1] 33. 搜索旋转排序数组 题目描述 整数数组nums按升序排列，数组中的值互不相同。\n在传递给函数之前，nums在预先未知的某个下标k（0 \u0026lt;= k \u0026lt; nums.length）上进行了旋转，使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]（下标从 0 开始计数）。例如，[0,1,2,4,5,6,7]在下标3处经旋转后可能变为[4,5,6,7,0,1,2]。\n给你旋转后的数组nums和一个整数target，如果nums中存在这个目标值target，则返回它的下标，否则返回-1。\n你必须设计一个时间复杂度为O(log n)的算法解决此问题。\n示例：\n输入：nums = [4,5,6,7,0,1,2], target = 0 输出：4 核心思路 初始化：定义两个指针left和right分别指向数组的起始和结束位置。 中间值：计算中间位置mid。 比较： 如果nums[mid]等于target，返回mid。 如果nums[left]到nums[mid]是有序的： 检查target是否在这个有序区间内。 如果在，调整right指针；否则，调整left指针。 如果nums[mid]到nums[right]是有序的： 检查target是否在这个有序区间内。 如果在，调整left指针；否则，调整right指针。 循环：重复上述步骤，直到left超过right。 返回结果：如果循环结束仍未找到target，返回 -1。 代码 1class Solution: 2 def search(self, nums: List[int], target: int) -\u0026gt; int: 3 left, right = 0, len(nums) - 1 4 5 while left \u0026lt;= right: 6 mid = (left + right) // 2 7 8 # 如果找到目标值，直接返回 9 if nums[mid] == target: 10 return mid 11 12 # 判断左半部分是否有序 13 if nums[left] \u0026lt;= nums[mid]: 14 # 如果目标值在左半部分的有序区间中 15 if nums[left] \u0026lt;= target \u0026lt; nums[mid]: 16 right = mid - 1 # 缩小右边界，继续在左半部分查找 17 else: 18 left = mid + 1 # 否则，目标值可能在右半部分 19 # 否则右半部分有序 20 else: 21 # 如果目标值在右半部分的有序区间中 22 if nums[mid] \u0026lt; target \u0026lt;= nums[right]: 23 left = mid + 1 # 缩小左边界，继续在右半部分查找 24 else: 25 right = mid - 1 # 否则，目标值可能在左半部分 26 27 return -1 153. 寻找旋转排序数组中的最小值 题目描述 已知一个长度为n的数组，预先按照升序排列，经由1到n次旋转后，得到输入数组。例如，原数组nums = [0,1,2,4,5,6,7]在变化后可能得到：\n若旋转4次，则可以得到[4,5,6,7,0,1,2] 若旋转7次，则可以得到[0,1,2,4,5,6,7] 注意，数组[a[0], a[1], a[2], ..., a[n-1]]旋转一次的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]。\n给你一个元素值互不相同的数组nums，它原来是一个升序排列的数组，并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素。\n你必须设计一个时间复杂度为O(log n)的算法解决此问题。\n示例：\n输入：nums = [3,4,5,1,2] 输出：1 解释：原数组为 [1,2,3,4,5] ，旋转 3 次得到输入数组。 核心思路 首先，假设原数组是严格递增的，并且经过了旋转，导致一部分数组无序。为了理解，我们需要明确 有序部分和 无序部分 的定义： - 有序部分：在旋转后，仍然保持元素从小到大排列的区间。 - 无序部分：由于旋转导致其中部分元素的顺序被打乱的区间（即最小值所在的区间）。\n在旋转后的数组中，如果整个数组仍然是有序的（没有旋转或者旋转了完整长度），那么最小值就是数组的第一个元素。如果数组被旋转了，则会出现 无序部分，最小值一定出现在这个无序部分中。\n通过 中间元素 nums[mid] 和数组最右端元素 nums[right] 来判断： 1. nums[mid] \u0026gt; nums[right]： - 如果 mid 元素大于 right 元素，说明 mid 元素所在的区域是无序的。因为 nums[mid] 本应该小于 nums[right]，但由于旋转，顺序被破坏了。 - 在这种情况下，最小元素一定在 mid 右边。这是因为 nums[mid] \u0026gt; nums[right]，所以在右边部分才有可能找到比 nums[right] 更小的元素。 2. nums[mid] \u0026lt; nums[right]： - 如果 mid 元素小于 right 元素，说明 mid 到 right 的这部分是有序的，也就是说，nums[mid] 是这一段中的最小值。 - 在这种情况下，最小值可能在 mid 左边（包括 mid 本身）。因为右边已经有序，最小值不会在右边，而可能在左边。\n代码 1class Solution: 2 def findMin(self, nums: List[int]) -\u0026gt; int: 3 left, right = 0, len(nums) - 1 4 5 while left \u0026lt; right: 6 mid = (left + right) // 2 7 8 # 如果中间值大于最右边的值，说明最小值在右半部分 9 if nums[mid] \u0026gt; nums[right]: 10 left = mid + 1 11 # 否则，最小值在左半部分（包括 mid 自己） 12 else: 13 right = mid 14 15 # 当 left == right 时，找到了最小值 16 return nums[left] 栈 20. 有效的括号 题目描述 给定一个只包括'('，')'，'{'，'}'，'['，']'的字符串s，判断字符串是否有效。\n有效字符串需满足：\n左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。 示例：\n输入：s = \u0026#34;()\u0026#34; 输出：true 输入：s = \u0026#34;()[]{}\u0026#34; 输出：true 核心思路 遍历s字符串：\n当s[i]为左括号时，将s[i]压栈\n当s[i]为右括号时，分为两种情况：\n若栈为空，则栈顶元素无法弹出与s[i]匹配，返回False； 若栈不为空，则弹出栈顶元素与s[i]匹配。 当栈顶元素与s[i]匹配时，则继续遍历字符串中下一个元素； 当栈顶元素和s[i]不匹配时，则返回False 时间复杂度：由于压栈和弹栈操作的时间复杂度均为O(1)，因此本题的时间复杂度取决于对字符串的遍历，所以为O(n)\n代码 1class Solution: 2 def isValid(self, s: str) -\u0026gt; bool: 3 stack=[] 4 bracketsMatch={\u0026#34;(\u0026#34;:1,\u0026#34;[\u0026#34;:2,\u0026#34;{\u0026#34;:3,\u0026#34;}\u0026#34;:4,\u0026#34;]\u0026#34;:5,\u0026#34;)\u0026#34;:6} 5 for i in range(len(s)): 6 #若为左括号，则入栈 7 if bracketsMatch[s[i]]\u0026lt;=3: 8 stack.append(s[i]) 9 else:#若为右括号 10 #首先，若栈此时为空，则return false 11 if len(stack)==0: 12 return False 13 else: 14 if bracketsMatch[s[i]]+bracketsMatch[stack.pop()]!=7: 15 return False 16 return True if len(stack)==0 else False 155. 最小栈 题目描述 设计一个支持push，pop，top操作，并能在常数时间内检索到最小元素的栈。\n实现MinStack类:\nMinStack()初始化堆栈对象。 void push(int val)将元素val推入堆栈。 void pop()删除堆栈顶部的元素。 int top()获取堆栈顶部的元素。 int getMin()获取堆栈中的最小元素。 示例：\n输入： [\u0026#34;MinStack\u0026#34;,\u0026#34;push\u0026#34;,\u0026#34;push\u0026#34;,\u0026#34;push\u0026#34;,\u0026#34;getMin\u0026#34;,\u0026#34;pop\u0026#34;,\u0026#34;top\u0026#34;,\u0026#34;getMin\u0026#34;] [[],[-2],[0],[-3],[],[],[],[]] *输出： [null,null,null,null,-3,null,0,-2] 解释： MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --\u0026gt; 返回 -3. minStack.pop(); minStack.top(); --\u0026gt; 返回 0. minStack.getMin(); --\u0026gt; 返回 -2. 核心思路 代码 1class MinStack: 2 3 def __init__(self): 4 self.stack = [] 5 self.min_stack = [] 6 7 def push(self, val: int) -\u0026gt; None: 8 self.stack.append(val) 9 if not self.min_stack or val \u0026lt;= self.min_stack[-1]: 10 self.min_stack.append(val) 11 12 def pop(self) -\u0026gt; None: 13 val = self.stack.pop() 14 if val == self.min_stack[-1]: 15 self.min_stack.pop() 16 17 def top(self) -\u0026gt; int: 18 return self.stack[-1] 19 20 def getMin(self) -\u0026gt; int: 21 return self.min_stack[-1] 394. 字符串解码 题目描述 给定一个经过编码的字符串，返回它解码后的字符串。\n编码规则为:k[encoded_string]，表示其中方括号内部的encoded_string正好重复k次。注意k保证为正整数。\n你可以认为输入字符串总是有效的；输入字符串中没有额外的空格，且输入的方括号总是符合格式要求的。\n此外，你可以认为原始数据不包含数字，所有的数字只表示重复的次数k，例如不会出现像3a或2[4]的输入。\n示例：\n输入：s = \u0026#34;3[a]2[bc]\u0026#34; 输出：\u0026#34;aaabcbc\u0026#34; 核心思路 栈的数据结构非常适合处理嵌套的情况。\n初始化栈：创建一个空栈，用于存储数字（重复次数）和字符串。\n遍历字符串：\n如果当前字符是数字，则计算该数字（可能有多位）。 如果当前字符是[，将之前计算的数字压入栈中，并重置数字。 如果当前字符是字母，则将其添加到当前正在构建的字符串中。 如果当前字符是]，则从栈中弹出最近的数字和字符串，并将当前构建的字符串重复该数字次，然后将结果添加到前一个部分的字符串中。 返回结果：遍历结束后，最终构建的字符串即为解码后的字符串。\n代码 1class Solution: 2 def decodeString(self, s: str) -\u0026gt; str: 3 # 初始化栈、当前数字和当前字符串 4 stack = [] # 用于存储之前的字符串和重复次数 5 current_num = 0 # 当前数字，用于表示字符串重复的次数 6 current_string = \u0026#34;\u0026#34; # 当前构建的字符串 7 8 # 遍历给定的字符串 s 中的每个字符 9 for char in s: 10 if char.isdigit(): 11 # 如果遇到数字字符，则更新当前数字 12 # 处理连续的数字字符，将它们组合成一个完整的数字 13 current_num = current_num * 10 + int(char) 14 elif char == \u0026#39;[\u0026#39;: 15 # 如果遇到开括号 \u0026#39;[\u0026#39;，将当前字符串和当前数字保存到栈中 16 # 并重置 current_string 和 current_num，准备处理括号内部的子字符串 17 stack.append((current_string, current_num)) 18 current_string, current_num = \u0026#34;\u0026#34;, 0 # 重置当前字符串和数字 19 elif char == \u0026#39;]\u0026#39;: 20 # 如果遇到闭括号 \u0026#39;]\u0026#39;，从栈中弹出之前的字符串和数字 21 # 生成重复的字符串，将其与之前的字符串连接起来 22 prev_string, num = stack.pop() # 弹出上一个字符串和对应的重复次数 23 current_string = prev_string + num * current_string # 重复当前字符串并与之前的字符串拼接 24 else: 25 # 如果是普通的字母字符，则将其追加到当前字符串中 26 current_string += char 27 28 # 返回最终构建的解码字符串 29 return current_string 题目描述 给定一个整数数组temperatures，表示每天的温度，返回一个数组answer，其中answer[i]是指对于第i天，下一个更高温度出现在几天后。如果气温在这之后都不会升高，请在该位置用0来代替。\n示例：\n输入：temperatures = [73,74,75,71,69,72,76,73] 输出：[1,1,4,2,1,1,0,0] 核心思路 通过单调栈（Monotonic Stack）来高效解决。具体来说，我们可以利用一个栈来存储温度的索引，这样可以方便地计算天数差。\n初始化：\n创建一个结果数组answer，长度与temperatures相同，初始值为0。 创建一个空的栈stack，用于存储温度的索引。 遍历温度数组：\n对于每个温度temperatures[i]： 如果栈不为空且当前温度temperatures[i]大于栈顶索引对应的温度temperatures[stack[-1]]，则： 弹出栈顶索引index。 计算天数差i - index，并将结果存入answer[index]。 将当前索引i压入栈。 返回结果：\n遍历完成后，栈中剩余的索引对应的结果已经默认为0，表示之后没有更高的温度。 代码 1class Solution: 2 def dailyTemperatures(self, temperatures: List[int]) -\u0026gt; List[int]: 3 n = len(temperatures) # 获取温度列表的长度 4 answer = [0] * n # 初始化答案列表，初始值为 0，表示没有比当前温度高的日子 5 stack = [] # 栈，用来存储尚未找到更高温度的索引 6 7 for i in range(n): 8 # 使用 while 循环检查栈顶元素对应的温度是否小于当前温度 9 # 如果是，则说明当前温度高于栈顶对应的温度，我们可以计算等待的天数 10 while stack and temperatures[i] \u0026gt; temperatures[stack[-1]]: 11 index = stack.pop() # 栈顶的温度索引 12 answer[index] = i - index # 计算等待的天数，并将其存入答案列表 13 14 # 将当前温度的索引压入栈中，以便后续处理 15 stack.append(i) 16 17 return answer 堆 215. 数组中的第K个最大元素 题目描述 给定整数数组nums和整数k，请返回数组中第k个最大的元素。\n请注意，你需要找的是数组排序后的第k个最大的元素，而不是第k个不同的元素。 你必须设计并实现时间复杂度为O(n)的算法解决此问题。\n示例：\n输入：[3,2,1,5,6,4], k = 2 输出：5 核心思路 维护大小为 k 的最小堆：\n在遍历数组 nums 时，我们使用一个堆，且堆的大小始终保持为 k。这是关键点：堆的大小总是 k。 堆的特点是堆顶元素是堆中最小的元素。因此，堆中的元素始终是当前遍历过的元素中最大的 k 个元素，而堆顶元素将是这些元素中最小的，也就是整个数组中第 k 大的元素。 堆操作的流程：\n每次遇到一个新的元素 num 时，我们将它加入到最小堆中。 然后检查堆的大小，如果堆的大小超过 k，我们会移除堆顶元素。因为堆顶是最小的元素，这样做的目的是确保堆中只保留最大的 k 个元素。 最终的结果：\n当遍历完所有元素后，最小堆中包含的是数组 nums 中最大的 k 个元素，而堆顶的最小元素正好是这 k 个元素中最小的一个，也就是整个数组中的第 k 大元素。 时间复杂度：对于每个元素插入堆的时间复杂度是 O(log⁡k)，我们有 n 个元素，因此总体的时间复杂度为 O(nlog⁡k)。 空间复杂度：堆的大小始终为 k，因此空间复杂度为 O(k)。 代码 【最小堆】\n1class Solution: 2 def findKthLargest(self, nums: list[int], k: int) -\u0026gt; int: 3 # 使用大小为 k 的最小堆 4 min_heap = [] 5 6 for num in nums: 7 heapq.heappush(min_heap, num) 8 # 如果堆的大小超过 k，弹出堆顶最小值 9 if len(min_heap) \u0026gt; k: 10 heapq.heappop(min_heap) 11 12 # 堆顶即为第 k 大的元素 13 return min_heap[0] 【快速选择】\n1class Solution: 2 def findKthLargest(self, nums: List[int], k: int) -\u0026gt; int: 3 def quick_select(nums, k): 4 \u0026#34;\u0026#34;\u0026#34; 5 Quickselect 函数：通过递归在数组中找到第 k 大的元素。 6 通过选择一个 pivot（枢轴），将数组分成两部分： 7 - `larger`: 所有大于 pivot 的元素 8 - `smaller`: 所有小于 pivot 的元素 9 然后根据 k 判断在哪部分递归继续查找。 10 \u0026#34;\u0026#34;\u0026#34; 11 # 随机选择一个 pivot 来避免最坏情况 12 pivot = random.choice(nums) 13 14 # 分成三部分：比 pivot 大的、比 pivot 小的和等于 pivot 的 15 larger = [num for num in nums if num \u0026gt; pivot] 16 smaller = [num for num in nums if num \u0026lt; pivot] 17 18 # k \u0026lt;= len(larger) 意味着我们正在寻找的第 k 大元素在 larger 部分中。 19 if k \u0026lt;= len(larger): 20 return quick_select(larger, k) 21 22 # 如果 k 大于 len(larger) + len(equal)，我们需要在 smaller 部分中递归查找 23 # 由于我们已经排除了 larger 和 equal 部分的元素 24 # 因此是在剩下的 smaller 部分寻找第 k - (len(larger) + len(equal)) 大的元素。 25 if k \u0026gt; len(nums) - len(smaller): 26 return quick_select(smaller, k - (len(nums) - len(smaller))) 27 28 # 否则，pivot 就是第 k 大的元素 29 return pivot 30 31 return quick_select(nums, k) 347. 前 K 个高频元素 题目描述 给你一个整数数组nums和一个整数k，请你返回其中出现频率前k高的元素。你可以按任意顺序返回答案。\n示例：\n输入：nums = [1,1,1,2,2,3], k = 2 输出：[1,2] 核心思路 使用哈希表计数：我们可以遍历数组，记录每个数字出现的次数，使用哈希表（字典）来存储每个数字的频率。\n获取前 k 个高频元素：我们可以通过一些方式从频率中找到前 k 个出现频率最高的元素。最直接的方式是使用堆或直接排序。\n代码 【Counter + 最小堆】时间复杂度：O(n log k)\n1class Solution: 2 def topKFrequent(self, nums: list[int], k: int) -\u0026gt; list[int]: 3 # 1. 使用 Counter 统计每个数字的出现频率 4 count = Counter(nums) 5 6 # 2. 构建一个最小堆，大小为 k 7 # heapq 可以维护一个最小堆，堆中存储的是 (频率, 数字) 8 min_heap = [] 9 10 for num, freq in count.items(): 11 # 将当前元素及其频率加入堆中 12 heapq.heappush(min_heap, (freq, num)) 13 # 如果堆的大小超过 k，移除频率最低的元素 14 if len(min_heap) \u0026gt; k: 15 heapq.heappop(min_heap) 16 17 # 3. 从最小堆中提取出频率最高的 k 个元素（此时堆中正好有 k 个元素） 18 return [num for freq, num in min_heap] 【Counter + 排序】时间复杂度：O(n log n)\n1class Solution: 2 def topKFrequent(self, nums: list[int], k: int) -\u0026gt; list[int]: 3 # 1. 使用 Counter 统计每个数字的出现频率 4 count = Counter(nums) 5 6 # 2. 使用 most_common 方法找到频率最高的 k 个元素 7 # most_common(k) 返回频率最高的前 k 个元素 8 result = [] 9 for num, freq in count.most_common(k): 10 result.append(num) 11 12 return result 贪心算法 121. 买卖股票的最佳时机 题目描述 给定一个数组prices，它的第i个元素prices[i]表示一支给定股票第i天的价格。\n你只能选择某一天买入这只股票，并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。\n返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回0。\n示例：\n输入：[7,1,5,3,6,4] 输出：5 解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。 核心思路 【贪心算法】\n保持最低价格：记录到目前为止的最低股价 min_price。 计算最大利润： 对于每一天的股价，计算如果在这一天卖出股票的利润，即 prices[i] - min_price。 不断更新最大利润 max_profit。 总结：通过维护一个最低买入价和当前最大利润，每天更新这两个数值来计算最大可能利润。\n代码 1class Solution(object): 2 def maxProfit(self, prices): 3 if len(prices) == 1: 4 return 0 5 min_price = prices[0] 6 max_profit = 0 7 for i in range(1, len(prices)): 8 min_price = min(min_price, prices[i]) 9 max_profit = max(max_profit, prices[i] - min_price) 10 11 return max_profit 55. 跳跃游戏 题目描述 给你一个非负整数数组nums，你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标，如果可以，返回true；否则，返回false。\n示例：\n输入：nums = [2,3,1,1,4] 输出：true 解释：可以先跳 1 步，从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 核心思路 贪心策略：每次在当前位置，计算从该位置能够到达的最远位置，并更新一个变量 max_reachable 来记录能够到达的最远下标。\n遍历数组：从第一个下标开始遍历数组：\n如果当前下标i超过了max_reachable，这意味着无法从之前的任何位置跳到当前位置，因此直接返回False。 否则，更新max_reachable为max(max_reachable, i + nums[i])，即在当前位置i，尝试跳跃nums[i]步，更新最远可达位置。 如果在遍历过程中，max_reachable已经大于或等于最后一个下标，说明可以到达最后一个下标，返回True。 时间复杂度：该算法只需遍历数组一次，因此时间复杂度为 O(n)。这是最优的时间复杂度，因为我们至少需要查看每个元素一次以确定是否可以到达最后一个位置。 空间复杂度：该算法只使用了常数个额外变量，因此空间复杂度为 O(1)。\n代码 1class Solution: 2 def canJump(self, nums: List[int]) -\u0026gt; bool: 3 max_reachable = 0 4 for i in range(len(nums)): 5 if i \u0026gt; max_reachable: 6 return False 7 max_reachable = max(max_reachable, i + nums[i]) 8 return True 45. 跳跃游戏 II 题目描述 给定一个长度为n的0 索引整数数组nums。初始位置为nums[0]。\n每个元素nums[i]表示从索引i向前跳转的最大长度。换句话说，如果你在nums[i]处，你可以跳转到任意nums[i + j]处：\n0 \u0026lt;= j \u0026lt;= nums[i] i + j \u0026lt; n 返回到达nums[n - 1]的最小跳跃次数。生成的测试用例可以到达nums[n - 1]。\n示例：\n输入：nums = [2,3,1,1,4] 输出：2 解释：跳到最后一个位置的最小跳跃数是 2。 核心思路 贪心策略：在每一步中，我们尝试跳到当前能够到达的最远位置，并在需要的时候增加跳跃次数。\n变量初始化：\nmax_reach：表示从当前及之前的位置能够到达的最远下标。 steps：表示当前跳跃次数下能够到达的最远位置。 jumps：记录跳跃次数，初始为 1，因为至少需要一次跳跃。 遍历数组：\n从索引 1 开始遍历（因为起始位置已被初始化考虑）。 如果当前索引i超过了steps，这意味着需要进行一次新的跳跃才能继续前进，因此增加jumps并更新steps为max_reach。 每次更新max_reach为max(max_reach, i + nums[i])，以确保记录从当前位置能够到达的最远位置。 跳跃条件：\n通过比较i和steps来决定是否增加跳跃次数。 steps的更新保证了每次跳跃后能够尽可能远地前进。 时间复杂度：该算法只需遍历数组一次，因此时间复杂度为 O(n)。 空间复杂度：该算法只使用了常数个额外变量，因此空间复杂度为 O(1)。\n代码 1class Solution: 2 def jump(self, nums: List[int]) -\u0026gt; int: 3 if len(nums) == 1: # 如果数组只有一个元素，不需要跳跃 4 return 0 5 6 max_reach = nums[0] # 当前能够达到的最远位置 7 steps = nums[0] # 当前步数能够达到的最远位置 8 jumps = 1 # 跳跃次数 9 10 for i in range(1, len(nums)): 11 if i \u0026gt; steps: # 如果当前位置超出了当前步数能够达到的最远位置 12 jumps += 1 13 steps = max_reach # 更新当前步数能够达到的最远位置 14 15 max_reach = max(max_reach, i + nums[i]) # 更新当前能够达到的最远位置 16 17 return jumps 763. 划分字母区间 题目描述 给你一个字符串s。我们要把这个字符串划分为尽可能多的片段，同一字母最多出现在一个片段中。\n注意，划分结果需要满足：将所有划分结果按顺序连接，得到的字符串仍然是s。\n返回一个表示每个字符串片段的长度的列表。\n示例：\n输入：s = \u0026#34;ababcbacadefegdehijhklij\u0026#34; 输出：[9,7,8] 解释： 划分结果为 \u0026#34;ababcbaca\u0026#34;、\u0026#34;defegde\u0026#34;、\u0026#34;hijhklij\u0026#34; 。 每个字母最多出现在一个片段中。像 \u0026#34;ababcbacadefegde\u0026#34;, \u0026#34;hijhklij\u0026#34; 这样的划分是错误的，因为划分的片段数较少。 核心思路 记录最后出现位置：\n先遍历字符串s，记录每个字符最后一次出现的位置。这样可以知道每个字符的活动范围。 划分片段：\n用两个变量start和end来表示当前片段的起始和结束位置。 再次遍历字符串s： 对于每个字符s[i]，我们更新end为该字符的最后出现位置和当前end的最大值。 如果当前索引i等于end，这意味着从start到end的片段可以独立出来，因为所有在这个片段中的字符都不会在后面出现。 记录结果：\n当确定一个片段时，计算其长度end - start + 1，并将其加入结果列表。 更新start为i + 1，继续寻找下一个片段。 代码 动态规划 70. 爬楼梯 题目描述 假设你正在爬楼梯。需要n阶你才能到达楼顶。\n每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢？\n示例：\n输入：n = 2 输出：2 解释：有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 核心思路 定义状态：用数组dp[i]表示爬到第i个台阶的方法数。 初始化： 爬 0 个台阶只有一种方法（即不动），所以dp[0] = 1。 爬 1 个台阶也只有一种方法（直接爬上去），所以dp[1] = 1。 状态转移方程： 要爬到第i个台阶，可以从第i-1个台阶爬 1 步到达，也可以从第i-2个台阶爬 2 步到达。 因此，dp[i] = dp[i-1] + dp[i-2]。 时间复杂度： 由于每个状态只依赖前两个状态，因此时间复杂度为O(n)。 代码 1class Solution: 2 def climbStairs(self, n: int) -\u0026gt; int: 3 dp = [0] * (n + 1) 4 dp[0], dp[1] = 1, 1 5 for i in range(2, n + 1): 6 dp[i] = dp[i - 1] + dp[i - 2] 7 return dp[n] 118. 杨辉三角 题目描述 给定一个非负整数numRows，生成「杨辉三角」的前numRows行。\n在「杨辉三角」中，每个数是它左上方和右上方的数的和。\n示例：\n输入: numRows = 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 核心思路 初始化： 创建一个长度为numRows的二维列表triangle。 每行i都初始化为有i+1个元素的列表，其中第一个和最后一个元素都设为1，因为杨辉三角的两侧边界上的元素都是1。 填充中间部分： 从第三行开始（即索引为2），对每一行的中间元素进行动态规划计算。 对于triangle[i][j]，它等于上一行的两个元素之和：triangle[i-1][j-1] + triangle[i-1][j]。 返回结果： 最后返回整个triangle。 代码 1class Solution: 2 def generate(self, numRows: int) -\u0026gt; List[List[int]]: 3 if numRows \u0026lt;= 0: 4 return [] 5 # Initialize the first row 6 triangle = [[1]] 7 8 for i in range(1, numRows): 9 row = [1] * (i + 1) # First step: initialize current row with 1s 10 for j in range(1, i): # Second step: fill the middle elements 11 row[j] = triangle[i-1][j-1] + triangle[i-1][j] 12 triangle.append(row) 13 14 return triangle 198. 打家劫舍 题目描述 你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。\n给定一个代表每个房屋存放金额的非负整数数组，计算你不触动警报装置的情况下，一夜之内能够偷窃到的最高金额。\n示例 ：\n输入：[1,2,3,1] 输出：4 解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。 核心思路 这个问题可以使用动态规划方法来解决。动态规划的核心思想是将复杂的问题分解成更小的子问题，并利用这些子问题的解来构建原问题的解。\n定义状态： 设dp[i]表示偷窃到第i个房屋时能够获得的最大金额。 状态转移方程： 对于每个房屋i，有两种选择： 不偷这个房屋，那么dp[i] = dp[i-1]。 偷这个房屋，那么dp[i] = dp[i-2] + nums[i]，因为不能偷相邻的房屋。 因此，状态转移方程为：dp[i] = max(dp[i-1], dp[i-2] + nums[i])。 初始条件： dp[0] = nums[0]，因为只有一个房屋时，只能偷这个房屋。 dp[1] = max(nums[0], nums[1])，因为有两个房屋时，可以选择偷其中一个金额较大的房屋。 优化空间复杂度： 由于每次计算dp[i]只依赖于dp[i-1]和dp[i-2]，可以使用两个变量来代替整个数组，从而将空间复杂度从O(n)优化到O(1)。 代码 1class Solution: 2 def rob(self, nums: List[int]) -\u0026gt; int: 3 if not nums: 4 return 0 5 if len(nums) == 1: 6 return nums[0] 7 8 # 初始化前两个状态 9 prev1 = nums[0] 10 prev2 = max(nums[0], nums[1]) 11 12 # 从第三个房屋开始计算 13 for i in range(2, len(nums)): 14 # 当前状态 15 current = max(prev2, prev1 + nums[i]) 16 # 更新前两个状态 17 prev1 = prev2 18 prev2 = current 19 20 return prev2 279. 完全平方数 题目描述 给你一个整数n，返回_和为n的完全平方数的最少数量_。\n完全平方数是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9和16都是完全平方数，而3和11不是。\n示例：\n输入：n = 12 输出：3 解释：12 = 4 + 4 + 4 核心思路 这道题的本质是找到一个整数n可以被分解成最少的完全平方数之和。我们可以通过定义一个数组dp来存储每个值的最优解，从而逐步构建出最终的解。\n定义状态： dp[i] 表示 凑成 i 这个数时，最少需要几个完全平方数的和。 初始化： dp[0] = 0，因为凑成 0 这个数不需要任何数。 其他dp[i]初始值可以设为正无穷大（表示还未找到解）。 状态转移： 对于每个i，我们可以通过减去一个完全平方数jj来得到i，其中jj \u0026lt;= i。 当 i = 12 时，我们可以尝试减去 1², 2², 3²，因为 1², 2², 3² 都是小于 12 的完全平方数。 如果用 1²，我们就要看 dp[12 - 1] 的值； 如果用 2²，我们就要看 dp[12 - 4] 的值； 如果用 3²，我们就要看 dp[12 - 9] 的值。 状态转移方程为：dp[i] = min(dp[i], dp[i - jj] + 1)，其中1表示我们使用了一个完全平方数jj。 结果： 最终dp[n]就是我们要找的答案。 代码 1class Solution: 2 def numSquares(self, n: int) -\u0026gt; int: 3 4 # 创建一个长度为 n+1 的数组 dp 5 dp = [float(\u0026#39;inf\u0026#39;)] * (n + 1) 6 7 # 初始化 dp[0] 为 0，因为凑成 0 不需要任何数 8 dp[0] = 0 9 10 # 从 1 开始，逐步计算 dp[i] 的最优解 11 for i in range(1, n + 1): 12 # 尝试每一个小于 i 的平方数 j*j 13 j = 1 14 while j * j \u0026lt;= i: 15 dp[i] = min(dp[i], dp[i - j * j] + 1) 16 j += 1 17 18 # 最终答案在 dp[n] 19 return dp[n] 322. 零钱兑换 题目描述 给你一个整数数组coins，表示不同面额的硬币；以及一个整数amount，表示总金额。\n计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回-1。\n你可以认为每种硬币的数量是无限的。\n示例：\n输入：coins = [1, 2, 5], amount = 11 输出：3 解释：11 = 5 + 5 + 1 核心思路 定义状态： dp[i] 表示 凑成金额 i 时，最少需要硬币的个数。 初始化： dp[0] = 0，因为凑成 0 元不需要任何硬币。 其他dp[i]初始值可以设为正无穷大（表示还未找到解）。 状态转移： 对于每一个金额 i，我们要检查所有的硬币面额 coins 中，能否通过减去某个硬币来得到更少的硬币数量。 状态转移方程为：dp[i] = min(dp[i], dp[i - coin] + 1)，其中1表示我们使用了一个 coin。 结果： 最终dp[amount]就是我们要找的答案。 代码 1class Solution: 2 def coinChange(self, coins: List[int], amount: int) -\u0026gt; int: 3 # 初始化dp数组，dp[i]表示凑成i元最少需要的硬币数量 4 dp = [float(\u0026#39;inf\u0026#39;)] * (amount + 1) 5 dp[0] = 0 # 凑成0元需要0个硬币 6 7 # 从1元开始，逐步计算每个金额的最优解 8 for i in range(1, amount + 1): 9 # 遍历所有硬币面额 10 for coin in coins: 11 if i - coin \u0026gt;= 0: # 如果当前硬币可以用来凑成i 12 dp[i] = min(dp[i], dp[i - coin] + 1) 13 14 # 如果dp[amount]仍然是无穷大，说明无法凑成该金额 15 return dp[amount] if dp[amount] != float(\u0026#39;inf\u0026#39;) else -1 139. 单词拆分 题目描述 给你一个字符串s和一个字符串列表wordDict作为字典。如果可以利用字典中出现的一个或多个单词拼出s则返回true。\n注意：不要求字典中出现的单词全部都使用，并且字典中的单词可以重复使用。\n示例：\n输入：s = \u0026#34;leetcode\u0026#34;, wordDict = [\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;] 输出：true 解释：返回 true 因为 \u0026#34;leetcode\u0026#34; 可以由 \u0026#34;leet\u0026#34; 和 \u0026#34;code\u0026#34; 拼接成。 核心思路 定义状态： 用一个布尔数组 dp 来表示 dp[i] 为 True 表示 前 i 个字符可以用 wordDict 中的单词拼接而成， dp[i] 为 False 则表示不能拼接。 初始化： dp[0] = True，因为空字符串可以通过不使用任何单词构成。 其他dp[i]初始值为 False。 状态转移： 对于每个 i，我们要检查之前的每个位置 j 切分出来的字串是否在 wordDict 中，如果 dp[j] 是 True 且 s[j:i] 在 wordDict 中，那么 dp[i] 也设置为 True。 状态转移方程为：dp[i] = dp[j] and (s[j:i] ∈ wordDict) 其中 0≤j\u0026lt;i。 结果： 最终 dp[len(s)] 就表示整个字符串 s 是否能被拆分成字典中的单词。 代码 1class Solution: 2 def wordBreak(self, s: str, wordDict: List[str]) -\u0026gt; bool: 3 # 将 wordDict 转换为集合，方便查找 4 word_set = set(wordDict) 5 # 创建一个长度为 len(s) + 1 的数组 dp，初始为 False 6 dp = [False] * (len(s) + 1) 7 # 初始化 dp[0] 为 True，因为空字符串是可以构成的 8 dp[0] = True 9 10 # 遍历字符串的每一个位置 i 11 for i in range(1, len(s) + 1): 12 # 对于每个位置 i，尝试从 j 切分 13 for j in range(i): 14 # 如果 dp[j] 为 True 且 s[j:i] 在字典中 15 if dp[j] and s[j:i] in word_set: 16 dp[i] = True 17 break # 已经找到一个方案，不需要继续切分 18 19 # 最终结果在 dp[len(s)] 20 return dp[len(s)] 300. 最长递增子序列 题目描述 给你一个整数数组nums，找到其中最长严格递增子序列的长度。\n子序列是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。\n示例：\n输入：nums = [10,9,2,5,3,7,101,18] 输出：4 解释：最长递增子序列是 [2,3,7,101]，因此长度为 4 。 核心思路 初始化： tails = []。这个数组最终会保存每个长度的递增子序列的最小结尾值。 如果 tails[2] = 7，表示存在一个长度为 3 的递增子序列，其结尾元素为 7。 为什么关心最小结尾元素？因为最小的结尾使得后续的序列更有可能继续延展，从而形成更长的递增子序列。 定义 binary_search 函数：二分查找用来找到 tails 中第一个大于等于目标值 target 的位置。该函数返回的 left 就是我们需要插入或替换的索引位置。 遍历 nums 数组： 对于每个 num，在 tails 中通过二分查找找到第一个大于等于 num 的位置 idx。 如果 num 比 tails 中所有元素都大，即 idx == len(tails)，则将 num 添加到 tails 末尾。 如果找到了合适的位置 idx，则替换掉 tails[idx]，表示找到了一个更小的结尾元素，使得当前长度的递增子序列更有潜力继续延展。 最终返回 tails 数组的长度：tails 的长度即为最长递增子序列的长度。 示例\n以输入 [10, 9, 2, 5, 3, 7, 101, 18] 为例：\n初始化 tails = []。 遍历第一个元素 10： tails 是空的，所以将 10 添加到 tails 中：tails = [10]。 遍历第二个元素 9： 9 小于 10，所以用 9 替换 tails[0]，得到 tails = [9]。 遍历第三个元素 2： 2 小于 9，所以用 2 替换 tails[0]，得到 tails = [2]。 遍历第四个元素 5： 5 大于 2，将 5 添加到 tails 末尾：tails = [2, 5]。 遍历第五个元素 3： 3 大于 2 但小于 5，用 3 替换 tails[1]，得到 tails = [2, 3]。 遍历第六个元素 7： 7 大于 3，将 7 添加到 tails 末尾：tails = [2, 3, 7]。 遍历第七个元素 101： 101 大于 7，将 101 添加到 tails 末尾：tails = [2, 3, 7, 101]。 遍历最后一个元素 18： 18 小于 101 但大于 7，用 18 替换 tails[3]，得到 tails = [2, 3, 7, 18]。 最终 tails 数组的长度为 4，因此最长递增子序列的长度为 4。\n贪心策略：通过维护一个 tails 数组，确保对于每个可能长度的递增子序列，其结尾元素尽可能小，以便后续元素更容易接上。 二分查找：在 tails 中找到可以插入或替换的位置，用二分查找加速这个过程，从而将整体时间复杂度优化到 O(nlogn)。\n代码 1class Solution: 2 def lengthOfLIS(self, nums: List[int]) -\u0026gt; int: 3 # tails 数组记录每个长度的递增子序列的最小结尾值 4 tails = [] 5 6 # 手动实现二分查找，用于找到 tails 中第一个大于等于 target 的位置 7 def binary_search(tails, target): 8 left, right = 0, len(tails) - 1 9 while left \u0026lt;= right: 10 mid = left + (right - left) // 2 11 if tails[mid] \u0026lt; target: 12 left = mid + 1 13 else: 14 right = mid - 1 15 return left 16 17 for num in nums: 18 # 找到 tails 中第一个大于等于 num 的位置 19 idx = binary_search(tails, num) 20 21 # 如果没有找到合适的位置，说明 num 大于 tails 中的所有元素 22 if idx == len(tails): 23 tails.append(num) 24 else: 25 # 用 num 替换掉 tails[idx]，使得结尾元素尽可能小 26 tails[idx] = num 27 28 # 最终 tails 数组的长度就是最长递增子序列的长度 29 return len(tails) 152. 乘积最大子数组 题目描述 给你一个整数数组nums，请你找出数组中乘积最大的非空连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。\n测试用例的答案是一个32位整数。\n示例：\n输入：nums = [2,3,-2,4] 输出： 6 解释：子数组 [2,3] 有最大乘积 6。 核心思路 该题的关键在于处理正负数的影响，因为负数的乘积可能会使最小值变成最大值。因此，需要特别关注如何维护最大值和最小值。\n为了在遍历数组的过程中随时保持最大和最小的乘积，关键是要同时记录当前位置之前的最大乘积和最小乘积。原因如下： - 当前的数字如果是正数，我们希望它乘以之前的最大乘积，得到一个更大的值。 - 当前的数字如果是负数，那么我们希望它乘以之前的最小乘积，因为负负得正，可能得到更大的值。 因此，我们需要在每一步同时跟踪最大乘积和最小乘积，并通过更新它们来保证遍历结束时可以得到全局的最大乘积。\n定义状态 max_prod[i]：表示以 nums[i] 结尾的子数组的最大乘积。 min_prod[i]：表示以 nums[i] 结尾的子数组的最小乘积（负数的可能性）。 初始化： max_prod[0] = nums[0]：因为以第一个元素开始的子数组的最大乘积就是它自己。 min_prod[0] = nums[0]：同理，以第一个元素开始的子数组的最小乘积也是它自己。 状态转移方程： 如果 nums[i] 是正数，则 max_prod[i] = max(max_prod[i-1] * nums[i], nums[i])。 如果 nums[i] 是负数，则 max_prod[i] = max(min_prod[i-1] * nums[i], nums[i])，因为负数会反转最大和最小的关系。 代码 1class Solution: 2 def maxProduct(self, nums: List[int]) -\u0026gt; int: 3 # 初始化最大乘积和最小乘积 4 current_max = current_min = global_max = nums[0] 5 6 # 从第二个元素开始遍历 7 for i in range(1, len(nums)): 8 num = nums[i] 9 temp_max = current_max 10 11 # 更新 current_max 和 current_min 12 current_max = max(num, current_max * num, current_min * num) 13 current_min = min(num, temp_max * num, current_min * num) 14 15 # 更新全局最大值 16 global_max = max(global_max, current_max) 17 18 return global_max 416. 分割等和子集 题目描述 给你一个只包含正整数的非空数组nums。请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。\n示例：\n输入：nums = [1,5,11,5] 输出：true 解释：数组可以分割成 [1, 5, 5] 和 [11] 。 核心思路 这道题是一个经典的 0/1 背包问题，给定一个数组 nums，我们需要判断是否能够将它分割成两个子集，使得两个子集的元素和相等。 我们可以将问题转换为：是否能从数组中挑选出若干个元素，使得它们的和等于整个数组总和的一半。\n假设数组的总和为 S，如果 S 是奇数，那么无法平分为两个相等的子集，直接返回 False。 如果 S 是偶数，那么我们的问题就变成了：是否能从数组中找到一个子集，使得该子集的和等于 S / 2。\n我们可以把问题看作是一个 0/1 背包问题： - 背包容量是 S / 2，即我们需要找的子集的目标和。 - 每个数组元素代表可以选择或不选择的一件物品，物品的重量就是数组中的元素值。 目标是判断是否存在一种选择方案，能够让这些元素的和恰好等于 S / 2。\n用一个动态规划的数组 dp 来解决这个问题，其中 dp[i] 表示是否能找到一个子集，使得这个子集的和等于 i。\n状态定义：\n定义一个布尔数组 dp ，其中 dp[j] 表示能否从数组中找到和为 j 的子集。 状态转移：\n对于每个数组中的数字 num，我们从右向左遍历 dp 数组，检查能否通过选择这个 num 来更新 dp[j]。 具体来说，dp[j] 可以由 dp[j - num] 推导出来，表示如果之前存在和为 j - num 的子集，那么加上 num 后就可以得到和为 j 的子集。 因此，状态转移方程为： dp[j] = dp[j] or dp[j − num] 初始化：\ndp[0] = True，表示和为 0 的子集肯定是存在的（即不选任何元素）。 结果：\n在遍历完数组 nums 后，检查 dp[S/2] 是否为 True，如果是，则表示存在子集和为 S/2，即可以分割为两个和相等的子集。 代码 1class Solution: 2 def canPartition(self, nums: List[int]) -\u0026gt; bool: 3 total_sum = sum(nums) 4 5 # 如果总和是奇数，无法平分成两个子集 6 if total_sum % 2 != 0: 7 return False 8 9 target = total_sum // 2 10 11 # 初始化 dp 数组，dp[j] 表示是否能找到和为 j 的子集 12 dp = [False] * (target + 1) 13 dp[0] = True # 和为0的子集肯定存在 14 15 # 遍历数组中的每个数字 16 for num in nums: 17 # 从 target 到 num 进行逆序遍历，防止重复使用同一个数字 18 for j in range(target, num - 1, -1): 19 dp[j] = dp[j] or dp[j - num] 20 21 # 返回是否能找到和为 target 的子集 22 return dp[target] 多维动态规划 62. 不同路径 题目描述 一个机器人位于一个m x n网格的左上角 （起始点在下图中标记为 “Start” ）。\n机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。\n问总共有多少条不同的路径？\n示例： 输入：m = 3, n = 7 输出：28 核心思路 机器人要完成整个移动，必须走 m-1 次向下和 n−1 次向右。因此，总共需要走的步数是 m+n−2 步。 因此，问题可以看作从 m+n−2 个位置中选择 m−1 个位置向下，剩下的向右。\n这就是组合数问题，公式为：C(m+n−2,m−1) = (m+n−2)! / (m−1)!(n−1)!\n代码 class Solution: def uniquePaths(self, m: int, n: int) -\u0026gt; int: return math.comb(m + n - 2, m - 1) 64. 最小路径和 题目描述 给定一个包含非负整数的_m_x_n_网格grid，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。\n说明：每次只能向下或者向右移动一步。\n示例： 输入：grid = [[1,3,1],[1,5,1],[4,2,1]] 输出：7 解释：因为路径 1→3→1→1→1 的总和最小。 核心思路 定义状态：\n设 dp[i][j] 为到达位置 (i, j) 的最小路径和。 状态转移方程：\n从左上角 (0, 0) 开始，移动到 (i, j) 只能从上方 (i-1, j) 或左方 (i, j-1) 移动过来。 因此，状态转移方程为：dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) 需要注意边界条件： 当 i = 0 时，只能从左边移动：dp[0][j] = dp[0][j-1] + grid[0][j] 当 j = 0 时，只能从上边移动：dp[i][0] = dp[i-1][0] + grid[i][0] 初始化：\ndp[0][0] = grid[0][0]，即起点的值。 计算：\n通过双重循环遍历整个 grid，根据状态转移方程填充 dp 数组。 结果：\n最终结果为 dp[m-1][n-1]，即到达右下角的最小路径和。 代码 1class Solution: 2 def minPathSum(self, grid: List[List[int]]) -\u0026gt; int: 3 m = len(grid) 4 n = len(grid[0]) 5 6 # 创建一个二维数组 dp，用于存储到达每个点的最小路径和 7 dp = [[0] * n for _ in range(m)] 8 9 dp[0][0] = grid[0][0] 10 11 # 填充第一行（只能从左边移动） 12 for j in range(1, n): 13 dp[0][j] = dp[0][j - 1] + grid[0][j] 14 15 # 填充第一列（只能从上面移动） 16 for i in range(1, m): 17 dp[i][0] = dp[i - 1][0] + grid[i][0] 18 19 # 填充剩余的 dp 数组 20 for i in range(1, m): 21 for j in range(1, n): 22 # 当前点的最小路径和 = 当前点的值 + 上方和左方的最小路径和 23 dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]) 24 25 return dp[m - 1][n - 1] 5. 最长回文子串 题目描述 给你一个字符串s，找到s中最长的回文子串。\n示例：\n输入：s = \u0026#34;babad\u0026#34; 输出：\u0026#34;bab\u0026#34; 解释：\u0026#34;aba\u0026#34; 同样是符合题意的答案。 核心思路 回文字符串是对称的，我们可以从每个字符或者字符之间的缝隙作为中心，向两边扩展，检查最长的回文子串。 具体地，对于每个可能的中心，向两边扩展，直到不再是回文子串为止。维护当前发现的最长回文子串。\n代码 1class Solution: 2 def longestPalindrome(self, s: str) -\u0026gt; str: 3 if len(s) \u0026lt; 2: 4 return s 5 6 # 定义一个函数，从中心向两边扩展，找到最长的回文子串 7 # 参数 left 和 right 分别是左右边界 8 def expandAroundCenter(s, left, right): 9 # 当左右边界在有效范围内，且 s[left] == s[right] 时，继续向外扩展 10 while left \u0026gt;= 0 and right \u0026lt; len(s) and s[left] == s[right]: 11 left -= 1 # 向左扩展 12 right += 1 # 向右扩展 13 # 返回扩展后的回文子串的实际左右边界（因为最后一次循环会多减/加一，所以要 +1 和 -1） 14 return left + 1, right - 1 15 16 # 初始化回文子串的起始和结束索引 17 start, end = 0, 0 18 19 # 遍历字符串的每一个字符，以其作为中心进行扩展 20 for i in range(len(s)): 21 # 第一种情况：以 s[i] 作为中心，检查长度为奇数的回文子串 22 l1, r1 = expandAroundCenter(s, i, i) 23 # 第二种情况：以 s[i] 和 s[i+1] 之间的空隙作为中心，检查长度为偶数的回文子串 24 l2, r2 = expandAroundCenter(s, i, i + 1) 25 26 # 如果奇数长度的回文子串比当前记录的最长回文子串更长，更新 start 和 end 27 if r1 - l1 \u0026gt; end - start: 28 start, end = l1, r1 29 # 如果偶数长度的回文子串比当前记录的最长回文子串更长，更新 start 和 end 30 if r2 - l2 \u0026gt; end - start: 31 start, end = l2, r2 32 33 # 返回最长回文子串，使用最终的 start 和 end 索引 34 return s[start:end + 1] 1143. 最长公共子序列 题目描述 给定两个字符串text1和text2，返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列，返回0。\n一个字符串的子序列是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。\n例如，\u0026quot;ace\u0026quot;是\u0026quot;abcde\u0026quot;的子序列，但\u0026quot;aec\u0026quot;不是\u0026quot;abcde\u0026quot;的子序列。 两个字符串的公共子序列是这两个字符串所共同拥有的子序列。\n示例：\n输入：text1 = \u0026#34;abcde\u0026#34;, text2 = \u0026#34;ace\u0026#34; 输出：3 解释：最长公共子序列是 \u0026#34;ace\u0026#34; ，它的长度为 3 。 核心思路 定义动态规划表（DP表）：\n我们定义一个二维数组 dp，其中 dp[i][j] 表示字符串 text1 的前 i 个字符和字符串 text2 的前 j 个字符的最长公共子序列长度。 dp[i][j] 记录了到目前为止，text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列长度。 状态转移方程：\n如果 text1[i-1] == text2[j-1]，这说明 text1 和 text2 的当前字符相同，我们可以把这个公共字符加入最长公共子序列，因此：dp[i][j] = dp[i−1][j−1] + 1 如果 text1[i-1] != text2[j-1]，则 dp[i][j] = max⁡(dp[i−1][j],dp[i][j−1]) 这表示我们要么忽略 text1 的当前字符，要么忽略 text2 的当前字符，取两种情况中较大的子序列长度。 边界条件：\n当 i == 0 或 j == 0 时，dp[i][j] 都初始化为 0，因为一个空字符串与任何字符串的公共子序列长度为 0。 最终结果：\n填充完 DP 表后，dp[m][n]（其中 m 是 text1 的长度，n 是 text2 的长度）就是最长公共子序列的长度。 代码 1class Solution: 2 def longestCommonSubsequence(self, text1: str, text2: str) -\u0026gt; int: 3 # 获取两个字符串的长度 4 m, n = len(text1), len(text2) 5 6 # 初始化 DP 表，dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的最长公共子序列长度 7 dp = [[0] * (n + 1) for _ in range(m + 1)] 8 9 # 填充 DP 表 10 for i in range(1, m + 1): 11 for j in range(1, n + 1): 12 if text1[i - 1] == text2[j - 1]: 13 # 当前字符相同，公共子序列长度加1 14 dp[i][j] = dp[i - 1][j - 1] + 1 15 else: 16 # 当前字符不相同，取两个可能情况的最大值 17 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) 18 19 # 返回最终的最长公共子序列长度 20 return dp[m][n] 72. 编辑距离 题目描述 给你两个单词word1和word2，请返回将word1转换成word2所使用的最少操作数。 你可以对一个单词进行如下三种操作：\n插入一个字符 删除一个字符 替换一个字符 示例：\n输入：word1 = \u0026#34;horse\u0026#34;, word2 = \u0026#34;ros\u0026#34; 输出：3 解释： horse -\u0026gt; rorse (将 \u0026#39;h\u0026#39; 替换为 \u0026#39;r\u0026#39;) rorse -\u0026gt; rose (删除 \u0026#39;r\u0026#39;) rose -\u0026gt; ros (删除 \u0026#39;e\u0026#39;) 核心思路 定义 DP 表：我们定义一个二维数组 dp，其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数。\n边界条件：\n如果 i == 0，即 word1 是空字符串，那么我们需要 j 次插入操作来将 word2 拼出来，故 dp[0][j] = j。 如果 j == 0，即 word2 是空字符串，那么我们需要 i 次删除操作来将 word1 清空，故 dp[i][0] = i。 状态转移方程：\n如果 word1[i-1] == word2[j-1]，表示两个字符相同，不需要任何操作：dp[i][j] = dp[i-1][j-1]。 如果 word1[i-1] != word2[j-1]，表示两个字符不同，需要执行一次操作（插入、删除、替换），我们取三种操作中最小的那个： dp[i][j] = min⁡(dp[i−1][j]+1, dp[i][j−1]+1, dp[i−1][j−1]+1) 删除操作：dp[i-1][j] + 1（从 word1 删除一个字符） 插入操作：dp[i][j-1] + 1（在 word1 插入一个字符） 替换操作：dp[i-1][j-1] + 1（将 word1 的当前字符替换成 word2 的字符） 最终结果：dp[m][n]，其中 m 是 word1 的长度，n 是 word2 的长度。\n代码 1class Solution: 2 def minDistance(self, word1: str, word2: str) -\u0026gt; int: 3 m, n = len(word1), len(word2) 4 dp = [[0] * (n + 1) for _ in range(m + 1)] 5 6 for i in range(1, m + 1): 7 dp[i][0] = i # 从 word1[0:i] 变成空字符串，执行 i 次删除操作 8 for j in range(1, n + 1): 9 dp[0][j] = j # 从空字符串变成 word2[0:j]，执行 j 次插入操作 10 11 for i in range(1, m + 1): 12 for j in range(1, n + 1): 13 if word1[i - 1] == word2[j - 1]: 14 dp[i][j] = dp[i - 1][j - 1] 15 else: 16 # 如果不同，取插入、删除、替换操作的最小值，并加1表示一次操作 17 dp[i][j] = min(dp[i - 1][j] + 1, # 删除 18 dp[i][j - 1] + 1, # 插入 19 dp[i - 1][j - 1] + 1) # 替换 20 21 # 返回最终的最少操作次数 22 return dp[m][n] 技巧 136. 只出现一次的数字 题目描述 给你一个非空整数数组nums，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。\n你必须设计并实现线性时间复杂度的算法来解决此问题，且该算法只使用常量额外空间。\n示例：\n输入：nums = [2,2,1] 输出：1 核心思路 异或运算的性质\n任何数与0异或为其本身：a ⊕ 0 = a 任何数与其自身异或为0：a ⊕ a = 0 利用异或运算性质，我们可以在O(n)时间复杂度和O(1)空间复杂度内找到只出现一次的数字。 因为对于任意两个相同的数，异或后结果为0。而0与任何数异或结果为该数本身。所以，所有成对出现的数在异或后都抵消为0，最终剩下的就是那个只出现一次的数。\n具体步骤如下：\n初始化变量result为0。 遍历数组，对每个元素执行异或运算并更新result。 最终，result的值即为只出现一次的那个数字。 代码 1class Solution: 2 def singleNumber(self, nums: List[int]) -\u0026gt; int: 3 result = 0 4 for num in nums: 5 result ^= num 6 return result 169. 多数元素 题目描述 给定一个大小为n的数组nums，返回其中的多数元素。多数元素是指在数组中出现次数大于⌊ n/2 ⌋的元素。\n你可以假设数组是非空的，并且给定的数组总是存在多数元素。\n示例：\n输入：nums = [3,2,3] 输出：3 核心思路 【方法一：哈希表】\n记录次数： 使用一个哈希表counts来记录每个元素出现的次数。 遍历数组nums，对每个元素进行统计。 如果元素已经在哈希表中，则其对应的计数器加1； 如果不在，则初始化该元素的计数器为1。 查找多数元素： 遍历哈希表中的所有键值对，找到出现次数大于 [n/2] 的元素，并返回该元素。 时间复杂度：O(n)，因为我们需要遍历数组一次来填充哈希表，然后再遍历哈希表一次来找到多数元素。 空间复杂度：O(n)，因为最坏情况下，需要存储数组中所有不同的元素的计数。\n【方法二：摩尔投票】\n候选者和计数器： 维护一个变量candidate用于跟踪当前的候选多数元素，以及一个count计数器来表示candidate在目前遇到的元素中的净出现次数。 遍历数组： 如果count为 0，说明我们需要更换候选者，将当前元素设为新的候选者，并将count设为 1。 如果当前元素等于candidate，则count加 1。 如果当前元素不等于candidate，则count减 1。 最终候选者： 遍历完成后，candidate所指的元素就是数组的多数元素。 时间复杂度：O(n)，因为我们只需要一次遍历数组。 空间复杂度：O(1)，只使用了常数级别的额外空间。\n代码 【方法一：哈希表】\n1class Solution: 2\tdef majorityElement(self, nums: List[int]) -\u0026gt; int: 3\tcounts = {} 4\tfor num in nums: 5\tif num in counts: 6\tcounts[num] += 1 7\telse: 8\tcounts[num] = 1 9 10\tfor key, value in counts.items(): 11\tif value \u0026gt; len(nums) // 2: 12\treturn key 【方法二：摩尔投票】\n1class Solution: 2\tdef majorityElement(self, nums: List[int]) -\u0026gt; int: 3\tcandidate = None 4\tcount = 0 5\t6\tfor num in nums: 7\tif count == 0: 8\tcandidate = num 9\tcount += (1 if num == candidate else -1) 10\t11\treturn candidate 75. 颜色分类 题目描述 给定一个包含红色、白色和蓝色、共n个元素的数组nums，原地对它们进行排序，使得相同颜色的元素相邻，并按照红色、白色、蓝色顺序排列。\n我们使用整数0、1和2分别表示红色、白色和蓝色。\n必须在不使用库内置的 sort 函数的情况下解决这个问题。\n示例：\n输入：nums = [2,0,2,1,1,0] 输出：[0,0,1,1,2,2] 核心思路 初始化指针：\nlow指针用于标记红色（0）的边界，初始为 0。 mid指针用于遍历数组，初始为 0。 high指针用于标记蓝色（2）的边界，初始为数组的最后一个索引。 遍历数组：\n当mid指针小于等于high指针时，检查nums[mid]的值： 如果nums[mid] == 0：交换nums[low]和nums[mid]，然后将low和mid都加 1。 如果nums[mid] == 1：mid加 1。 如果nums[mid] == 2：交换nums[mid]和nums[high]，然后将high减 1。注意这里 mid不变，因为交换后需要再次检查mid位置的值。 代码 1class Solution: 2 def sortColors(self, nums: List[int]) -\u0026gt; None: 3 low, mid, high = 0, 0, len(nums) - 1 4 5 while mid \u0026lt;= high: 6 if nums[mid] == 0: 7 nums[low], nums[mid] = nums[mid], nums[low] 8 low += 1 9 mid += 1 10 elif nums[mid] == 1: 11 mid += 1 12 else: # nums[mid] == 2 13 nums[mid], nums[high] = nums[high], nums[mid] 14 high -= 1 31. 下一个排列 题目描述 整数数组的一个排列 就是将其所有成员以序列或线性顺序排列。\n例如，arr = [1,2,3]，以下这些都可以视作arr的排列：[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1]。 整数数组的下一个排列是指其整数的下一个字典序更大的排列。更正式地，如果数组的所有排列根据其字典顺序从小到大排列在一个容器中，那么数组的下一个排列就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列，那么这个数组必须重排为字典序最小的排列（即，其元素按升序排列）。\n例如，arr = [1,2,3]的下一个排列是[1,3,2]。 类似地，arr = [2,3,1]的下一个排列是[3,1,2]。 而arr = [3,2,1]的下一个排列是[1,2,3]，因为[3,2,1]不存在一个字典序更大的排列。 给你一个整数数组nums，找出nums的下一个排列。\n必须原地修改，只允许使用额外常数空间。\n示例：\n输入：nums = [1,2,3] 输出：[1,3,2] 核心思路 找到第一个升序对： 从数组的末尾开始，找到第一个nums[i] \u0026lt; nums[i + 1]的位置i。 如果找不到这样的i，说明整个数组是降序的，直接反转整个数组即可得到最小排列。 找到比nums[i]大的最小数： 从数组的末尾开始，找到第一个nums[j] \u0026gt; nums[i]的位置j。 交换nums[i]和nums[j]。 反转剩余部分： 反转从i + 1到数组末尾的部分，使其变为升序。 代码 1class Solution: 2 def nextPermutation(self, nums: List[int]) -\u0026gt; None: 3 # Step 1: Find the first decreasing element from the right 4 i = len(nums) - 2 5 while i \u0026gt;= 0 and nums[i] \u0026gt;= nums[i + 1]: 6 i -= 1 7 8 if i \u0026gt;= 0: # If such an element is found 9 # Step 2: Find the element just larger than nums[i] from the right 10 j = len(nums) - 1 11 while nums[j] \u0026lt;= nums[i]: 12 j -= 1 13 # Step 3: Swap elements at i and j 14 nums[i], nums[j] = nums[j], nums[i] 15 16 # Step 4: Reverse the elements from i+1 to end 17 nums[i + 1:] = reversed(nums[i + 1:]) 287. 寻找重复数 题目描述 给定一个包含n + 1个整数的数组nums，其数字都在[1, n]范围内（包括1和n），可知至少存在一个重复的整数。\n假设nums只有一个重复的整数，返回这个重复的数。\n你设计的解决方案必须不修改数组nums且只用常量级O(1)的额外空间。\n示例：\n输入：nums = [1,3,4,2,2] 输出：2 核心思路 将数组视为一个链表，其中每个元素的值指向下一个节点的索引。由于存在重复的数字，因此链表中必然存在环。\n初始化快慢指针：\n使用两个指针，slow和fast。初始时，它们都指向数组的第一个元素。 寻找相遇点：\n让slow每次移动一步，而fast每次移动两步。 由于存在重复的数字，fast和 slow最终会在环中相遇。 找到环的入口：\n将slow重置到数组的起始位置，而fast保持在相遇点。 这时，让slow和fast每次都移动一步。 当它们再次相遇时，相遇点即为环的入口，也就是重复数字所在的位置。 代码 1class Solution: 2 def findDuplicate(self, nums: List[int]) -\u0026gt; int: 3 # Step 1: Initialize the slow and fast pointers 4 slow = nums[0] 5 fast = nums[0] 6 7 # Step 2: Move slow pointer by 1 step and fast pointer by 2 steps 8 # until they meet inside the cycle 9 while True: 10 slow = nums[slow] 11 fast = nums[nums[fast]] 12 if slow == fast: 13 break 14 15 # Step 3: Find the entrance to the cycle 16 slow = nums[0] 17 while slow != fast: 18 slow = nums[slow] 19 fast = nums[fast] 20 21 return slow 内容来源声明 本博文收录的题目描述、核心思路与 Python 题解代码均为原作者成果,本人只对内容进行了格式整理与排版,不对其创作归属作声明。\n内容整理自开源项目 LeetCode_Hot100_Python,在此向原作者表示诚挚感谢。 若内容存在不妥或涉及侵权,请联系我删改。\n","date":"2026-03-16","tags":["算法","LeetCode"]}]