<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>hins Yeow&#39;s Blog</title>
  
  
  <link href="https://blog.hinsyeow.org/rss.xml" rel="self"/>
  
  <link href="https://blog.hinsyeow.org/"/>
  <updated>2026-03-17T17:43:02.321Z</updated>
  <id>https://blog.hinsyeow.org/</id>
  
  <author>
    <name>hins-Yeow</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>复杂系统之ERP</title>
    <link href="https://blog.hinsyeow.org/posts/Complex-Systems-ERP/"/>
    <id>https://blog.hinsyeow.org/posts/Complex-Systems-ERP/</id>
    <published>2026-03-01T04:54:10.000Z</published>
    <updated>2026-03-17T17:43:02.321Z</updated>
    
    <content type="html"><![CDATA[<p>一文读懂ERP全流程：从实施落地到价值闭环，企业数字化转型的核心指南</p><p>在数字化浪潮席卷全球的今天，ERP（Enterprise Resource Planning，企业资源计划）系统早已不是大型企业的“专属配置”，而是各类企业实现资源整合、流程标准化、决策智能化的核心支撑。所谓ERP全流程，并非单纯的“软件安装与操作”，而是一套涵盖“前期准备、实施落地、业务联动、优化迭代”的完整管理工程，贯穿企业采购、生产、销售、财务、人力等所有核心环节，实现从“分散管理”到“全局协同”的跨越。本文将从全流程拆解、核心模块联动、实施关键节点、价值体现四个维度，详细解析ERP全流程的核心逻辑与实操要点，帮你全面掌握ERP系统的运行本质。</p><p>一、ERP全流程总览：从规划到迭代的完整闭环</p><p>ERP全流程的核心目标是“打破信息孤岛、整合企业资源、优化业务流程、提升管理效率”，其完整链路可分为前期准备阶段、实施落地阶段、业务运营阶段、优化迭代阶段四大板块，各阶段环环相扣、层层递进，形成“规划-落地-运行-优化”的闭环管理。不同于单一软件的操作流程，ERP全流程更强调“管理思维与信息技术的深度融合”，每一个环节都需兼顾系统配置与企业实际业务场景，避免“为了上系统而上系统”。</p><p>简单来说，ERP全流程的本质是“用系统固化流程、用数据驱动决策”——通过标准化的流程设计，让企业各部门的业务活动可追溯、可管控；通过实时的数据共享，让管理层快速掌握企业运营全貌，实现资源的最优配置。无论是中小企业的进销存+财务一体化，还是大型制造企业的全链路生产管控，ERP全流程的核心逻辑始终一致，只是在模块配置和流程细节上根据企业规模、行业特性进行适配。</p><p>二、ERP全流程详细拆解：四大阶段+核心步骤</p><p>（一）前期准备阶段：筑牢基础，避免实施翻车</p><p>前期准备是ERP实施成功的前提，很多企业上ERP失败，根源在于前期准备不足、需求模糊。该阶段的核心是“明确目标、梳理需求、搭建团队”，具体分为3个关键步骤：</p><ol><li>需求调研与目标明确</li></ol><p>企业需组织各部门（采购、生产、销售、财务、人力等）开展全面调研，明确“为什么上ERP”“上ERP要解决什么问题”“期望达成什么目标”。比如，有的企业核心需求是解决“库存积压与缺货并存”的问题，有的企业则是为了实现“财务与业务数据同步，减少手工记账误差”。调研完成后，需形成详细的需求文档，明确核心需求、次要需求，划分优先级，避免后期实施过程中需求反复变更，导致项目延期、成本超支。</p><p>同时，需结合企业发展战略，设定可量化的实施目标，比如“库存周转率提升30%”“订单交付周期缩短20%”“财务结账效率提升50%”，为后续实施验收提供明确标准。</p><ol start="2"><li>项目团队搭建与责任划分</li></ol><p>ERP实施不是IT部门的“独角戏”，而是需要企业全员参与的系统工程。需搭建由“企业高层、IT部门、各业务部门负责人、核心操作人员”组成的项目组，明确各角色的职责：</p><ul><li><p>企业高层：负责统筹全局，审批项目预算、协调跨部门资源，确保项目顺利推进；</p></li><li><p>IT部门：负责系统安装、配置、技术支持，解决实施过程中的技术难题；</p></li><li><p>业务部门负责人：负责梳理本部门业务流程，配合系统配置，推动部门员工适应新流程；</p></li><li><p>核心操作人员：负责测试系统功能，反馈使用问题，协助开展全员培训。</p></li></ul><p>此外，建议引入专业的ERP实施服务商，借助其经验优化流程设计，规避实施风险——尤其是对于缺乏数字化经验的中小企业，专业服务商的指导能大幅提升实施效率。</p><ol start="3"><li>系统选型与预算规划</li></ol><p>根据企业需求、规模、行业特性，选择适配的ERP系统。目前市场上的ERP系统主要分为三类：通用型ERP（如SAP、Oracle）、行业专用ERP（如制造业的用友U9、零售业的金蝶K3）、中小企业轻量化ERP（如用友畅捷通、管家婆）。选型时需重点关注“系统兼容性、可扩展性、易用性、售后服务”，避免盲目追求“功能全面”而选择超出企业需求的系统，导致成本浪费。</p><p>同时，制定详细的项目预算，涵盖系统采购费、实施服务费、培训费用、后期维护费等，明确预算分配，避免后期出现资金缺口。</p><p>（二）实施落地阶段：从蓝图到落地，核心环节不缺位</p><p>实施落地是ERP全流程的核心环节，也是最复杂、最耗时的阶段，核心是“将企业业务流程转化为系统可实现的功能，完成从‘手工管理’到‘系统管理’的切换”。根据行业通用标准，该阶段可分为6个标准化步骤，遵循“蓝图不确认，不进配置；数据不准确，绝不上线；用户不会用，不能切系统”的原则，确保实施质量。</p><ol><li>业务蓝图设计与确认</li></ol><p>项目组与实施服务商合作，根据前期调研的需求，梳理企业现有业务流程，识别流程中的痛点、冗余环节，设计符合企业实际的“ERP业务蓝图”。蓝图需明确各模块的功能配置、流程节点、数据流转逻辑，比如“采购流程从需求提交到入库结算的全节点、各节点的责任人、数据如何同步到财务模块”。</p><p>蓝图设计完成后，需组织各部门负责人审核确认，签字归档——这是后续系统配置、测试、培训的核心依据，一旦确认，不得随意变更，若确需变更，需走正规的变更流程，避免项目混乱。</p><ol start="2"><li>系统配置与二次开发</li></ol><p>IT部门与实施服务商根据业务蓝图，进行系统配置：包括基础信息设置（如企业组织架构、员工信息、物料编码、供应商信息、客户信息）、流程配置（如采购流程、销售流程、生产流程的节点设置）、权限配置（如不同岗位的系统操作权限，避免越权操作）、报表配置（如库存报表、销售报表、财务报表的格式与数据来源）。</p><p>对于一些特殊行业或企业的个性化需求，若通用ERP系统无法满足，可进行少量二次开发，但需严格控制开发范围和成本，避免过度开发导致系统不稳定、维护难度增加。开发完成后，需进行单元测试，确保每个开发功能可正常使用。</p><ol start="3"><li>数据准备与导入</li></ol><p>数据是ERP系统的“血液”，数据的准确性直接决定ERP系统的运行效果。该步骤的核心是“清理历史数据、规范数据格式、完成数据导入”，需梳理的核心数据包括：</p><ul><li><p>基础数据：物料编码、供应商信息、客户信息、员工信息、会计科目等；</p></li><li><p>期初数据：库存期初数量与金额、财务期初余额、应收应付期初数据、生产工单期初数据等；</p></li><li><p>业务数据：未完成的采购订单、销售订单、生产计划等。</p></li></ul><p>数据准备过程中，需安排专人负责数据清理，剔除重复数据、错误数据，规范数据格式，确保数据的准确性和一致性。数据清理完成后，通过系统的数据导入功能，将数据批量导入ERP系统，导入后需进行数据校验，确认数据无误后，方可进入下一环节。</p><ol start="4"><li>测试与问题优化</li></ol><p>数据导入完成后，进入系统测试阶段，核心是“验证系统功能是否符合业务蓝图，流程是否顺畅，数据是否准确”，测试分为三个层级：</p><ul><li><p>单元测试：针对单个模块、单个功能进行测试，比如“采购模块的订单提交、审核功能，库存模块的入库、出库功能”，确保每个功能可正常使用；</p></li><li><p>集成测试：测试各模块之间的联动性，比如“采购入库后，库存数据是否自动更新，财务模块是否自动生成应付凭证”，确保数据流转顺畅；</p></li><li><p>UAT用户测试：由各部门核心操作人员亲自操作，模拟实际业务场景，测试系统的易用性和实用性，反馈使用过程中遇到的问题（如流程繁琐、操作不便、数据错误等）。</p></li></ul><p>测试过程中，需详细记录问题，由实施服务商和IT部门及时优化调整，反复测试，直到所有问题解决，系统功能完全符合业务需求。</p><ol start="5"><li>全员培训与操作演练</li></ol><p>系统测试通过后，需开展全员培训，确保每个员工都能熟练操作ERP系统——这是系统顺利上线的关键，若员工不会用，即使系统功能再完善，也无法发挥其价值。培训需分岗位、分模块进行，针对不同岗位的工作职责，讲解对应的系统操作流程，比如：</p><ul><li><p>采购人员：讲解采购需求提交、采购订单创建、入库单审核等操作；</p></li><li><p>仓库人员：讲解入库、出库、库存盘点、库存查询等操作；</p></li><li><p>财务人员：讲解凭证生成、账务处理、报表查询等操作。</p></li></ul><p>培训完成后，组织员工进行操作演练，模拟实际业务场景，熟练操作流程，解决员工操作过程中遇到的疑问，确保员工能够独立完成本职工作相关的系统操作。</p><ol start="6"><li>上线切换与并行运行</li></ol><p>培训完成后，进入系统上线切换阶段，通常采用“并行运行”的方式：即同时运行旧的手工管理模式（或旧系统）和新的ERP系统，为期1-2周。并行运行期间，员工需同时在两个模式下开展工作，对比两个模式的结果，确认ERP系统的数据准确性和流程顺畅性。</p><p>并行运行无异常后，正式停止旧模式，全面切换到ERP系统运行。上线初期，需安排实施服务商和IT部门专人值守，及时解决员工操作过程中遇到的问题，处理系统运行中的异常，确保系统稳定运行。上线稳定后，组织项目验收，由各部门负责人确认系统功能、流程符合需求，项目验收通过后，ERP实施落地阶段正式完成。</p><p>（三）业务运营阶段：模块联动，实现全流程管控</p><p>ERP系统正式上线后，进入业务运营阶段，核心是“通过系统实现各部门业务的协同运作，完成从采购、生产、销售到财务、人力的全流程管控”。该阶段的核心是“模块联动、数据共享”，各核心模块相互配合，形成完整的业务闭环，以下结合ERP核心模块，详细解析业务运营流程。</p><ol><li>采购管理流程（供应链起点）</li></ol><p>采购管理是ERP全流程的起点，核心是“按需采购、控制成本、保障供应”，完整流程如下：</p><ol><li><p>需求提交：各部门（如生产部门、仓库部门）根据实际需求，在ERP系统中提交采购需求单，注明物料名称、规格、数量、需求日期等信息；</p></li><li><p>采购计划：采购部门根据采购需求单，结合库存数据，制定采购计划，确定采购数量、供应商、采购价格、交货日期；</p></li><li><p>询价比价：采购部门在系统中发起询价比价，对比多个供应商的报价、资质，确定合适的供应商；</p></li><li><p>采购订单：在系统中创建采购订单，发送给供应商，订单信息自动同步到库存模块和财务模块；</p></li><li><p>到货验收：供应商送货后，仓库部门在系统中核对物料信息，确认无误后，办理入库手续，系统自动更新库存数据；</p></li><li><p>付款结算：财务部门根据采购订单、入库单，在系统中生成应付凭证，审核通过后，安排付款，付款后系统自动更新应付账款数据。</p></li><li><p>库存管理流程（供应链核心）</p></li></ol><p>库存管理的核心是“实时监控库存、优化库存结构、避免积压与缺货”，完整流程如下：</p><ol><li><p>入库管理：采购入库、生产入库、退货入库等，均需在系统中录入入库单，注明物料名称、数量、规格、入库日期，系统自动更新库存数量；</p></li><li><p>出库管理：销售出库、生产领料、退货出库等，录入出库单，系统自动扣减库存数量，实时更新库存余额；</p></li><li><p>库存盘点：定期在系统中发起库存盘点，对比系统库存与实际库存，录入盘点差异，调整系统库存，确保账实一致；</p></li><li><p>库存预警：系统可设置库存上下限，当库存低于下限或高于上限时，自动发出预警，提醒相关人员及时处理（如采购、清库）；</p></li><li><p>库存查询：各部门可根据权限，在系统中查询实时库存数据，为采购、生产、销售决策提供依据。</p></li><li><p>生产管理流程（制造企业核心）</p></li></ol><p>生产管理流程适用于制造企业，核心是“按计划生产、控制生产进度、保障产品质量”，完整流程如下：</p><ol><li><p>生产计划：生产部门根据销售订单、市场需求，在系统中制定生产计划，明确生产产品、数量、生产周期、生产工序；</p></li><li><p>物料需求计划（MRP）：系统根据生产计划、物料清单（BOM）、库存数据，自动计算所需物料的数量、采购时间，生成物料需求计划，同步到采购模块；</p></li><li><p>生产工单：根据生产计划，在系统中创建生产工单，分配生产任务到各生产工序、各班组；</p></li><li><p>生产领料：生产班组根据生产工单，在系统中提交领料申请，仓库部门审核后，发放物料，系统自动扣减库存；</p></li><li><p>生产执行：生产过程中，在系统中记录生产进度、生产工时、产品质量等信息，实时反馈生产状态；</p></li><li><p>成品入库：生产完成后，质检部门在系统中进行质量检验，检验合格后，办理成品入库手续，系统自动更新成品库存；</p></li><li><p>生产核算：系统自动统计生产过程中的物料消耗、人工成本、制造费用，生成生产成本报表，为成本控制提供依据。</p></li><li><p>销售管理流程（供应链终点）</p></li></ol><p>销售管理的核心是“拓展客户、提升销量、及时回款”，完整流程如下：</p><ol><li><p>客户管理：在系统中录入客户信息，建立客户档案，记录客户资质、合作历史、回款情况等，实现客户精细化管理；</p></li><li><p>报价管理：根据客户需求，在系统中生成报价单，注明产品名称、规格、价格、交货期等，提交客户确认；</p></li><li><p>销售订单：客户确认报价后，在系统中创建销售订单，明确订单数量、交货期、付款方式，订单信息自动同步到库存模块和生产模块；</p></li><li><p>发货管理：仓库部门根据销售订单，在系统中生成发货单，安排发货，发货后系统自动扣减库存，同步到财务模块；</p></li><li><p>开票结算：财务部门根据销售订单、发货单，在系统中生成销售发票，提交客户付款，客户付款后，在系统中记录回款信息，更新应收账款数据；</p></li><li><p>售后管理：客户反馈售后问题（如退货、换货），在系统中记录售后信息，安排处理，处理完成后，更新订单状态和库存数据。</p></li><li><p>财务管理流程（全流程核心枢纽）</p></li></ol><p>财务管理是ERP系统的核心枢纽，核心是“实现财务与业务数据同步，精准核算、规范管控”，完整流程如下：</p><ol><li><p>账务处理：系统自动接收采购、销售、库存等模块的业务数据，生成记账凭证（如采购入库生成应付凭证、销售发货生成应收凭证），减少手工记账误差；</p></li><li><p>成本核算：系统自动归集生产、采购、销售过程中的成本费用，进行成本核算，生成成本报表，为成本控制提供依据；</p></li><li><p>应收应付管理：实时监控应收账款、应付账款，跟踪回款进度、付款进度，设置回款预警，避免坏账、逾期付款；</p></li><li><p>固定资产管理：在系统中录入固定资产信息，记录固定资产购置、折旧、报废等情况，自动计提折旧，生成固定资产报表；</p></li><li><p>财务报表：系统自动生成资产负债表、利润表、现金流量表等财务报表，实时反映企业财务状况，为管理层决策提供数据支持；</p></li><li><p>预算管理：设置企业年度预算、部门预算，实时监控预算执行情况，对比实际发生额与预算额，及时调整预算，控制成本。</p></li><li><p>人力资源管理流程（企业支撑）</p></li></ol><p>人力资源管理模块作为ERP系统的重要支撑，核心是“实现员工全生命周期管理，优化人力资源配置”，完整流程如下：</p><ol><li><p>招聘管理：在系统中发布招聘需求，筛选简历，安排面试，录用后录入员工信息，建立员工档案；</p></li><li><p>入职管理：办理员工入职手续，录入员工基本信息、岗位信息、薪资信息，分配系统操作权限；</p></li><li><p>考勤管理：员工打卡记录自动同步到系统，统计考勤数据（如迟到、早退、请假），生成考勤报表；</p></li><li><p>薪资管理：根据员工岗位、考勤、绩效等数据，在系统中计算薪资，生成薪资条，同步到财务模块，安排薪资发放；</p></li><li><p>绩效评估：设置绩效考核指标，定期开展绩效考核，录入考核结果，为员工晋升、薪资调整提供依据；</p></li><li><p>离职管理：办理员工离职手续，更新员工状态，清理系统权限，归档员工档案。</p></li></ol><p>（四）优化迭代阶段：持续完善，实现价值最大化</p><p>ERP全流程并非“一劳永逸”，企业的业务模式、市场环境、发展战略会不断变化，因此，ERP系统需要持续优化迭代，才能始终适配企业发展需求。该阶段的核心是“收集反馈、分析数据、优化流程”，具体分为3个关键步骤：</p><ol><li>常态化反馈与问题收集</li></ol><p>建立常态化的反馈机制，鼓励各部门员工反馈系统使用过程中遇到的问题、流程繁琐的环节、功能缺失的需求，定期整理反馈意见，分类汇总，为优化迭代提供依据。比如，销售部门反馈“销售订单审核流程繁琐，影响效率”，财务部门反馈“报表格式不符合企业需求”，均需及时记录、分析。</p><ol start="2"><li>数据复盘与流程优化</li></ol><p>定期对ERP系统的运行数据进行复盘，分析关键指标（如库存周转率、订单交付周期、财务结账效率），对比实施目标，找出差距，分析原因。同时，梳理现有业务流程，识别冗余环节、瓶颈环节，结合反馈意见，优化流程配置和系统功能，比如简化审核流程、增加个性化报表、优化数据流转逻辑。</p><ol start="3"><li>系统升级与维护</li></ol><p>定期对ERP系统进行升级，安装系统补丁，修复系统漏洞，确保系统稳定运行；同时，根据企业发展需求，新增模块或功能（如新增客户关系管理模块、项目管理模块），拓展系统的可扩展性。此外，加强系统数据备份，防止数据丢失，定期开展系统安全检查，防范安全风险。</p><p>三、ERP全流程实施的关键节点与避坑指南</p><p>（一）三大关键节点（重中之重）</p><p>结合行业实施经验，ERP全流程实施有三个不可逾越的关键节点，直接决定项目成败：</p><ul><li><p>节点一：蓝图确认。业务蓝图是实施的核心依据，必须经过各部门签字确认，避免后期需求反复变更，导致项目延期、成本超支；</p></li><li><p>节点二：数据导入。数据准确性是系统运行的基础，必须严格清理、校验数据，确保数据无误后再导入系统，否则会导致系统数据混乱，无法正常使用；</p></li><li><p>节点三：全员培训。员工是系统的使用者，必须确保全员熟练操作，否则系统无法落地，前期所有投入都会白费。</p></li></ul><p>（二）常见坑点与避坑建议</p><ol><li><p>坑点一：需求模糊，目标不明确。避坑建议：前期调研要全面，明确核心需求和可量化目标，形成详细的需求文档，避免“拍脑袋上系统”；</p></li><li><p>坑点二：高层不重视，跨部门协作不畅。避坑建议：企业高层要亲自统筹，明确各部门职责，建立跨部门协作机制，及时协调解决协作中的问题；</p></li><li><p>坑点三：过度依赖实施服务商，自身团队不参与。避坑建议：企业核心团队要全程参与实施过程，熟悉系统配置和流程设计，避免后期服务商撤离后，无法独立维护系统；</p></li><li><p>坑点四：忽视数据质量，导入错误数据。避坑建议：安排专人负责数据清理和校验，建立数据规范，确保数据的准确性和一致性；</p></li><li><p>坑点五：上线后不优化，系统与业务脱节。避坑建议：建立常态化优化机制，定期复盘数据、收集反馈，持续优化流程和系统功能，让系统始终适配企业业务需求。</p></li></ol><p>四、ERP全流程的核心价值：不止是“管流程”，更是“提效率、降成本、促决策”</p><p>很多企业认为，ERP全流程就是“用系统代替手工记账、管库存”，其实这只是ERP的基础价值。真正的ERP全流程，是通过数字化手段，实现企业资源的全面整合和流程的标准化、智能化，其核心价值体现在三个层面：</p><ol><li>提升运营效率，减少人工成本</li></ol><p>通过系统自动化处理重复性工作（如手工记账、数据统计、订单审核），减少人工操作，降低人为误差，提升工作效率。比如，财务部门无需手工录入采购、销售凭证，系统自动生成，结账效率提升50%以上；仓库部门无需手工盘点库存，系统实时监控库存数据，盘点效率提升60%以上。</p><ol start="2"><li>优化资源配置，降低企业成本</li></ol><p>通过实时的数据共享，管理层可以快速掌握企业资源（物料、资金、人力）的使用情况，优化资源配置，避免浪费。比如，通过库存数据实时监控，避免库存积压，降低库存成本；通过生产计划优化，合理分配生产资源，降低生产成本；通过应收账款跟踪，减少坏账，降低资金占用成本。</p><ol start="3"><li>提供数据支撑，助力科学决策</li></ol><p>ERP系统整合了企业所有核心业务数据，通过报表分析、数据可视化，为管理层提供实时、准确的运营数据，帮助管理层快速识别业务痛点、市场趋势，做出科学的决策。比如，通过销售数据报表，分析不同产品的销量、利润，调整产品结构；通过成本数据报表，分析成本构成，优化成本控制策略。</p><p>五、总结：ERP全流程，是企业数字化转型的必经之路</p><p>ERP全流程不是简单的“软件实施”，而是一套涵盖“前期准备、实施落地、业务运营、优化迭代”的完整管理工程，其核心是“以系统为载体，以流程为核心，以数据为驱动，实现企业资源的全面整合和管理升级”。无论是中小企业的轻量化管理，还是大型企业的全链路管控，ERP全流程都能帮助企业打破信息孤岛、优化业务流程、提升运营效率、降低企业成本。</p><p>需要注意的是，ERP全流程的落地，并非一蹴而就，需要企业高层重视、全员参与、持续优化。只有将ERP系统与企业实际业务深度融合，才能充分发挥其价值，助力企业在数字化转型的道路上稳步前行，实现高质量发展。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;一文读懂ERP全流程：从实施落地到价值闭环，企业数字化转型的核心指南&lt;/p&gt;
&lt;p&gt;在数字化浪潮席卷全球的今天，ERP（Enterprise Resource Planning，企业资源计划）系统早已不是大型企业的“专属配置”，而是各类企业实现资源整合、流程标准化、决策智能化的核心支撑。所谓ERP全流程，并非单纯的“软件安装与操作”，而是一套涵盖“前期准备、实施落地、业务联动、优化迭代”的完整管理工程，贯穿企业采购、生产、销售、财务、人力等所有核心环节，实现从“分散管理”到“全局协同”的跨越。本文将从全流程拆解、核心模块联动、实施关键节点、价值体现四个维度，详细解析ERP全流程的核心逻辑与实操要点，帮你全面掌握ERP系统的运行本质。&lt;/p&gt;
&lt;p&gt;一、ERP全流程总览：从规划到迭代的完整闭环&lt;/p&gt;</summary>
    
    
    
    
    <category term="design" scheme="https://blog.hinsyeow.org/tags/design/"/>
    
  </entry>
  
  <entry>
    <title>网页加载时序控制：解决FOUC闪烁问题</title>
    <link href="https://blog.hinsyeow.org/posts/webpage-loading-timing-control-techniques/"/>
    <id>https://blog.hinsyeow.org/posts/webpage-loading-timing-control-techniques/</id>
    <published>2025-08-07T10:11:21.000Z</published>
    <updated>2026-03-17T17:43:02.321Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在网页开发中，我们经常会遇到**FOUC（Flash of Unstyled Content）**问题，即页面在CSS样式加载完成前，会短暂显示未样式化的内容，造成视觉上的闪烁。本文将详细介绍如何通过时序控制技巧来解决这个问题。</p></blockquote><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><h3 id="什么是FOUC？"><a href="#什么是FOUC？" class="headerlink" title="什么是FOUC？"></a>什么是FOUC？</h3><p>FOUC（Flash of Unstyled Content）是指网页在CSS样式表加载完成前，浏览器会使用默认样式显示HTML内容，导致页面出现短暂的”原始”状态，然后突然变成设计好的样式，造成视觉上的闪烁。</p><h3 id="问题产生的原因"><a href="#问题产生的原因" class="headerlink" title="问题产生的原因"></a>问题产生的原因</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">graph TD</span><br><span class="line">    A[HTML解析] --&gt; B[文本立即显示]</span><br><span class="line">    B --&gt; C[默认样式: 白底黑字]</span><br><span class="line">    C --&gt; D[CSS文件下载]</span><br><span class="line">    D --&gt; E[CSS解析应用]</span><br><span class="line">    E --&gt; F[JavaScript执行]</span><br><span class="line">    F --&gt; G[最终样式显示]</span><br></pre></td></tr></table></figure><p><strong>时序问题</strong>：</p><ol><li><strong>HTML解析阶段</strong>：浏览器立即显示文本内容</li><li><strong>CSS加载延迟</strong>：外部CSS文件需要时间下载和解析</li><li><strong>JavaScript执行延迟</strong>：需要等待DOM加载完成</li></ol><h2 id="解决方案原理"><a href="#解决方案原理" class="headerlink" title="解决方案原理"></a>解决方案原理</h2><h3 id="核心思想：多重保险机制"><a href="#核心思想：多重保险机制" class="headerlink" title="核心思想：多重保险机制"></a>核心思想：多重保险机制</h3><p>我们采用<strong>内联样式 + 外部CSS + JavaScript控制</strong>的三重保险机制：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 1. 内联样式（立即生效） --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.content-title</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-id">#card</span> <span class="selector-class">.card-inner</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.content-subtitle</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.enter</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.arrow</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-tag">h1</span>, <span class="selector-tag">h2</span>, <span class="selector-tag">h3</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: opacity <span class="number">1s</span> ease-in-out, visibility <span class="number">1s</span> ease-in-out;</span></span><br><span class="line"><span class="language-css">    &#125;</span></span><br><span class="line"><span class="language-css">    </span></span><br><span class="line"><span class="language-css">    <span class="comment">/* 确保页面背景始终是深色的 */</span></span></span><br><span class="line"><span class="language-css">    <span class="selector-tag">html</span>, <span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#222325</span> <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">    &#125;</span></span><br><span class="line"><span class="language-css">    </span></span><br><span class="line"><span class="language-css">    <span class="comment">/* 当元素有in类时显示 */</span></span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.content-title</span><span class="selector-class">.in</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-id">#card</span> <span class="selector-class">.card-inner</span><span class="selector-class">.in</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.content-subtitle</span><span class="selector-class">.in</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.enter</span><span class="selector-class">.in</span>,</span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.arrow</span><span class="selector-class">.in</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">opacity</span>: <span class="number">1</span> <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">visibility</span>: visible <span class="meta">!important</span>;</span></span><br><span class="line"><span class="language-css">    &#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-外部CSS文件（双重保险）"><a href="#2-外部CSS文件（双重保险）" class="headerlink" title="2. 外部CSS文件（双重保险）"></a>2. 外部CSS文件（双重保险）</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 防止页面加载时的闪烁 */</span></span><br><span class="line"><span class="selector-class">.content-title</span>,</span><br><span class="line"><span class="selector-id">#card</span> <span class="selector-class">.card-inner</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,</span><br><span class="line"><span class="selector-class">.content-subtitle</span>,</span><br><span class="line"><span class="selector-class">.enter</span>,</span><br><span class="line"><span class="selector-class">.arrow</span>,</span><br><span class="line"><span class="selector-tag">h1</span>, <span class="selector-tag">h2</span>, <span class="selector-tag">h3</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">transition</span>: opacity <span class="number">1s</span> ease-in-out, visibility <span class="number">1s</span> ease-in-out;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.content-title</span><span class="selector-class">.in</span>,</span><br><span class="line"><span class="selector-id">#card</span> <span class="selector-class">.card-inner</span><span class="selector-class">.in</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,</span><br><span class="line"><span class="selector-class">.content-subtitle</span><span class="selector-class">.in</span>,</span><br><span class="line"><span class="selector-class">.enter</span><span class="selector-class">.in</span>,</span><br><span class="line"><span class="selector-class">.arrow</span><span class="selector-class">.in</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">1</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: visible <span class="meta">!important</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 确保页面背景始终是深色的 */</span></span><br><span class="line"><span class="selector-tag">html</span>, <span class="selector-tag">body</span> &#123;</span><br><span class="line">    <span class="attribute">background</span>: <span class="built_in">var</span>(--color-content) <span class="meta">!important</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-JavaScript控制显示"><a href="#3-JavaScript控制显示" class="headerlink" title="3. JavaScript控制显示"></a>3. JavaScript控制显示</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">loadIntro</span>(<span class="params"></span>)&#123;</span><br><span class="line">    <span class="variable language_">document</span>[hiddenProperty] || loadIntro.<span class="property">loaded</span> || (</span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">            $(<span class="string">&quot;.wrap&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>),</span><br><span class="line">            $(<span class="string">&quot;.content-title&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>),</span><br><span class="line">            $(<span class="string">&quot;.enter&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>),</span><br><span class="line">            $(<span class="string">&quot;.arrow&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>),</span><br><span class="line">            <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">                $(<span class="string">&quot;.content-subtitle&quot;</span>).<span class="property">innerHTML</span>=<span class="string">&quot;&lt;span&gt;&quot;</span>+<span class="title function_">_toConsumableArray</span>(subtitle).<span class="title function_">join</span>(<span class="string">&quot;&lt;/span&gt;&lt;span&gt;&quot;</span>)+<span class="string">&quot;&lt;/span&gt;&quot;</span>,</span><br><span class="line">                $(<span class="string">&quot;.content-subtitle&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>)</span><br><span class="line">            &#125;,<span class="number">270</span>)</span><br><span class="line">        &#125;,<span class="number">0</span>),</span><br><span class="line">        loadIntro.<span class="property">loaded</span>=!<span class="number">0</span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="技术要点详解"><a href="#技术要点详解" class="headerlink" title="技术要点详解"></a>技术要点详解</h2><h3 id="1-内联样式优先原则"><a href="#1-内联样式优先原则" class="headerlink" title="1. 内联样式优先原则"></a>1. 内联样式优先原则</h3><p><strong>为什么使用内联样式？</strong></p><ul><li><strong>最高优先级</strong>：内联样式的优先级高于外部CSS文件</li><li><strong>立即生效</strong>：在HTML解析阶段就开始生效，无需等待外部文件加载</li><li><strong>可靠性高</strong>：不依赖网络请求，确保样式立即应用</li></ul><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 内联样式示例 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">    <span class="selector-class">.target-element</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;  <span class="comment">/* 立即隐藏 */</span></span></span><br><span class="line"><span class="language-css">        <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;  <span class="comment">/* 双重保险 */</span></span></span><br><span class="line"><span class="language-css">    &#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-important-的重要性"><a href="#2-important-的重要性" class="headerlink" title="2. !important 的重要性"></a>2. !important 的重要性</h3><p><strong>为什么使用 !important？</strong></p><ul><li><strong>防止覆盖</strong>：确保我们的隐藏规则不会被其他CSS规则覆盖</li><li><strong>优先级保证</strong>：即使在CSS文件加载后，这些规则仍然有效</li></ul><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.target-element</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;  <span class="comment">/* 确保不被覆盖 */</span></span><br><span class="line">    <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-双重隐藏策略"><a href="#3-双重隐藏策略" class="headerlink" title="3. 双重隐藏策略"></a>3. 双重隐藏策略</h3><p><strong>为什么同时使用 opacity 和 visibility？</strong></p><ul><li><strong>opacity: 0</strong>：使元素透明，但仍在文档流中</li><li><strong>visibility: hidden</strong>：完全隐藏元素，不占用空间</li><li><strong>双重保险</strong>：确保元素在各种情况下都被隐藏</li></ul><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.target-element</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">transition</span>: opacity <span class="number">1s</span> ease-in-out, visibility <span class="number">1s</span> ease-in-out;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-渐进式显示控制"><a href="#4-渐进式显示控制" class="headerlink" title="4. 渐进式显示控制"></a>4. 渐进式显示控制</h3><p><strong>JavaScript的时序控制：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. 立即隐藏所有元素</span></span><br><span class="line"><span class="comment">// 2. 等待DOM加载完成</span></span><br><span class="line"><span class="comment">// 3. 逐步添加显示类</span></span><br><span class="line"><span class="comment">// 4. 触发CSS过渡动画</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">loadIntro</span>(<span class="params"></span>)&#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">        <span class="comment">// 给元素添加显示类</span></span><br><span class="line">        $(<span class="string">&quot;.content-title&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>);</span><br><span class="line">        $(<span class="string">&quot;.enter&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>);</span><br><span class="line">        $(<span class="string">&quot;.arrow&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 延迟显示副标题</span></span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">            $(<span class="string">&quot;.content-subtitle&quot;</span>).<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;in&quot;</span>);</span><br><span class="line">        &#125;, <span class="number">270</span>);</span><br><span class="line">    &#125;, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="时序控制流程图"><a href="#时序控制流程图" class="headerlink" title="时序控制流程图"></a>时序控制流程图</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">sequenceDiagram</span><br><span class="line">    participant Browser as 浏览器</span><br><span class="line">    participant HTML as HTML解析</span><br><span class="line">    participant InlineCSS as 内联样式</span><br><span class="line">    participant ExternalCSS as 外部CSS</span><br><span class="line">    participant JS as JavaScript</span><br><span class="line">    </span><br><span class="line">    Browser-&gt;&gt;HTML: 开始解析HTML</span><br><span class="line">    HTML-&gt;&gt;InlineCSS: 立即应用内联样式</span><br><span class="line">    InlineCSS-&gt;&gt;Browser: 元素隐藏，背景深色</span><br><span class="line">    </span><br><span class="line">    Browser-&gt;&gt;ExternalCSS: 下载并解析外部CSS</span><br><span class="line">    ExternalCSS-&gt;&gt;Browser: 继续隐藏规则</span><br><span class="line">    </span><br><span class="line">    Browser-&gt;&gt;JS: DOM加载完成</span><br><span class="line">    JS-&gt;&gt;Browser: 添加显示类</span><br><span class="line">    Browser-&gt;&gt;Browser: 触发CSS过渡动画</span><br><span class="line">    Browser-&gt;&gt;Browser: 元素平滑显示</span><br></pre></td></tr></table></figure><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="1-识别需要控制的元素"><a href="#1-识别需要控制的元素" class="headerlink" title="1. 识别需要控制的元素"></a>1. 识别需要控制的元素</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 常见的需要控制的元素 */</span></span><br><span class="line"><span class="selector-class">.content-title</span>,           <span class="comment">/* 主标题 */</span></span><br><span class="line"><span class="selector-id">#card</span> <span class="selector-class">.card-inner</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,  <span class="comment">/* 卡片标题 */</span></span><br><span class="line"><span class="selector-class">.content-subtitle</span>,        <span class="comment">/* 副标题 */</span></span><br><span class="line"><span class="selector-class">.enter</span>,                   <span class="comment">/* 按钮 */</span></span><br><span class="line"><span class="selector-class">.arrow</span>,                   <span class="comment">/* 箭头 */</span></span><br><span class="line"><span class="selector-tag">h1</span>, <span class="selector-tag">h2</span>, <span class="selector-tag">h3</span>               <span class="comment">/* 所有标题元素 */</span></span><br></pre></td></tr></table></figure><h3 id="2-设置合适的过渡时间"><a href="#2-设置合适的过渡时间" class="headerlink" title="2. 设置合适的过渡时间"></a>2. 设置合适的过渡时间</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.target-element</span> &#123;</span><br><span class="line">    <span class="attribute">transition</span>: opacity <span class="number">1s</span> ease-in-out, visibility <span class="number">1s</span> ease-in-out;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>时间选择考虑因素：</strong></p><ul><li><strong>1秒</strong>：足够平滑，不会让用户等待太久</li><li><strong>ease-in-out</strong>：提供自然的加速和减速效果</li></ul><h3 id="3-背景色立即设置"><a href="#3-背景色立即设置" class="headerlink" title="3. 背景色立即设置"></a>3. 背景色立即设置</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">html</span>, <span class="selector-tag">body</span> &#123;</span><br><span class="line">    <span class="attribute">background</span>: <span class="number">#222325</span> <span class="meta">!important</span>;  <span class="comment">/* 深色背景 */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么重要：</strong></p><ul><li>避免白色背景闪烁</li><li>与最终设计保持一致</li></ul><h2 id="常见问题与解决方案"><a href="#常见问题与解决方案" class="headerlink" title="常见问题与解决方案"></a>常见问题与解决方案</h2><h3 id="问题1：某些元素仍然闪烁"><a href="#问题1：某些元素仍然闪烁" class="headerlink" title="问题1：某些元素仍然闪烁"></a>问题1：某些元素仍然闪烁</h3><p><strong>解决方案：</strong></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 更广泛的元素选择器 */</span></span><br><span class="line"><span class="selector-class">.content-title</span>,</span><br><span class="line"><span class="selector-id">#card</span> <span class="selector-class">.card-inner</span> <span class="selector-tag">header</span> <span class="selector-tag">h1</span>,</span><br><span class="line"><span class="selector-class">.content-subtitle</span>,</span><br><span class="line"><span class="selector-class">.enter</span>,</span><br><span class="line"><span class="selector-class">.arrow</span>,</span><br><span class="line"><span class="selector-tag">h1</span>, <span class="selector-tag">h2</span>, <span class="selector-tag">h3</span>,</span><br><span class="line"><span class="selector-class">.main-content</span> * &#123;  <span class="comment">/* 所有主要内容 */</span></span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="问题2：动画效果不流畅"><a href="#问题2：动画效果不流畅" class="headerlink" title="问题2：动画效果不流畅"></a>问题2：动画效果不流畅</h3><p><strong>解决方案：</strong></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.target-element</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: hidden <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translateY</span>(<span class="number">20px</span>) <span class="meta">!important</span>;  <span class="comment">/* 添加位移 */</span></span><br><span class="line">    <span class="attribute">transition</span>: opacity <span class="number">1s</span> ease-in-out, </span><br><span class="line">                visibility <span class="number">1s</span> ease-in-out,</span><br><span class="line">                transform <span class="number">1s</span> ease-in-out;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.target-element</span><span class="selector-class">.in</span> &#123;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">1</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">visibility</span>: visible <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translateY</span>(<span class="number">0</span>) <span class="meta">!important</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="问题3：移动端性能问题"><a href="#问题3：移动端性能问题" class="headerlink" title="问题3：移动端性能问题"></a>问题3：移动端性能问题</h3><p><strong>解决方案：</strong></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 针对移动端优化 */</span></span><br><span class="line"><span class="keyword">@media</span> (<span class="attribute">max-width</span>: <span class="number">768px</span>) &#123;</span><br><span class="line">    <span class="selector-class">.target-element</span> &#123;</span><br><span class="line">        <span class="attribute">transition</span>: opacity <span class="number">0.5s</span> ease-in-out;  <span class="comment">/* 缩短时间 */</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过这种时序控制技巧，我们成功解决了FOUC闪烁问题：</p><ol><li><strong>内联样式</strong>：确保在HTML解析阶段就隐藏元素</li><li><strong>外部CSS</strong>：提供双重保险，确保隐藏规则持续有效</li><li><strong>JavaScript控制</strong>：在合适的时机触发元素的显示</li><li><strong>平滑过渡</strong>：通过CSS过渡动画提供良好的用户体验</li></ol><p>这种方法的优势在于：</p><ul><li><strong>可靠性高</strong>：多重保险机制确保效果</li><li><strong>用户体验好</strong>：平滑的过渡动画</li><li><strong>兼容性强</strong>：适用于各种浏览器和设备</li><li><strong>维护性好</strong>：代码结构清晰，易于理解和修改</li></ul><p>通过掌握这种时序控制技巧，我们可以为用户提供更加流畅和专业的网页体验。</p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;在网页开发中，我们经常会遇到**FOUC（Flash of Unstyled Content）**问题，即页面在CSS样式加载完成前，会短暂显示未样式化的内容，造成视觉上的闪烁。本文将详细介绍如何通过时序控制技巧来解决这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;问题分析&quot;&gt;&lt;a href=&quot;#问题分析&quot; class=&quot;headerlink&quot; title=&quot;问题分析&quot;&gt;&lt;/a&gt;问题分析&lt;/h2&gt;&lt;h3 id=&quot;什么是FOUC？&quot;&gt;&lt;a href=&quot;#什么是FOUC？&quot; class=&quot;headerlink&quot; title=&quot;什么是FOUC？&quot;&gt;&lt;/a&gt;什么是FOUC？&lt;/h3&gt;</summary>
    
    
    
    
    <category term="web" scheme="https://blog.hinsyeow.org/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>解决&quot;/dev/kvm&quot; is not found问题</title>
    <link href="https://blog.hinsyeow.org/posts/solve-dev-kvm-is-not-found/"/>
    <id>https://blog.hinsyeow.org/posts/solve-dev-kvm-is-not-found/</id>
    <published>2025-07-07T15:59:16.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<p>Docker命令运行失败，错误信息显示系统找不到<code>/dev/kvm</code>设备文件。</p><p>这个问题通常与KVM（内核虚拟机）虚拟化支持有关。</p><h3 id="1-确认主机支持虚拟化"><a href="#1-确认主机支持虚拟化" class="headerlink" title="1. 确认主机支持虚拟化"></a>1. <strong>确认主机支持虚拟化</strong></h3><p>首先检查你的CPU是否支持虚拟化技术：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep -E <span class="string">&#x27;vmx|svm&#x27;</span> /proc/cpuinfo</span><br></pre></td></tr></table></figure><p>如果没有输出，则表示机器的CPU不支持硬件虚拟化，无法运行macOS容器。若有输出，继续下一步。</p><h3 id="2-检查KVM模块是否加载"><a href="#2-检查KVM模块是否加载" class="headerlink" title="2. 检查KVM模块是否加载"></a>2. <strong>检查KVM模块是否加载</strong></h3><p>运行以下命令检查KVM模块是否已加载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsmod | grep kvm</span><br></pre></td></tr></table></figure><ul><li><strong>Intel CPU</strong>：应看到<code>kvm_intel</code>模块。</li><li><strong>AMD CPU</strong>：应看到<code>kvm_amd</code>模块。</li></ul><p>若未加载，手动加载模块：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Intel CPU</span></span><br><span class="line"><span class="built_in">sudo</span> modprobe kvm_intel</span><br><span class="line"></span><br><span class="line"><span class="comment"># AMD CPU</span></span><br><span class="line"><span class="built_in">sudo</span> modprobe kvm_amd</span><br></pre></td></tr></table></figure><h3 id="3-检查-dev-kvm权限"><a href="#3-检查-dev-kvm权限" class="headerlink" title="3. 检查&#x2F;dev&#x2F;kvm权限"></a>3. <strong>检查&#x2F;dev&#x2F;kvm权限</strong></h3><p>确认<code>/dev/kvm</code>文件存在：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -l /dev/kvm</span><br></pre></td></tr></table></figure><ul><li><p>如果文件不存在，可能是虚拟化未启用或KVM驱动未正确安装。</p></li><li><p>如果文件存在但权限不足，添加当前用户到<code>kvm</code>组：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> usermod -aG kvm <span class="variable">$USER</span></span><br></pre></td></tr></table></figure><p>然后重新登录或重启系统使权限生效。</p></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;Docker命令运行失败，错误信息显示系统找不到&lt;code&gt;/dev/kvm&lt;/code&gt;设备文件。&lt;/p&gt;
&lt;p&gt;这个问题通常与KVM（内核虚拟机）虚拟化支持有关。&lt;/p&gt;
&lt;h3 id=&quot;1-确认主机支持虚拟化&quot;&gt;&lt;a href=&quot;#1-确认主机支持虚拟化&quot; class=&quot;headerlink&quot; title=&quot;1. 确认主机支持虚拟化&quot;&gt;&lt;/a&gt;1. &lt;strong&gt;确认主机支持虚拟化&lt;/strong&gt;&lt;/h3&gt;</summary>
    
    
    
    
    <category term="bug" scheme="https://blog.hinsyeow.org/tags/bug/"/>
    
  </entry>
  
  <entry>
    <title>「逻辑学」- 逻辑的目标</title>
    <link href="https://blog.hinsyeow.org/posts/logic-purpose-of-logic/"/>
    <id>https://blog.hinsyeow.org/posts/logic-purpose-of-logic/</id>
    <published>2025-06-12T13:31:11.000Z</published>
    <updated>2026-03-17T17:43:02.321Z</updated>
    
    <content type="html"><![CDATA[<h2 id="逻辑的目标"><a href="#逻辑的目标" class="headerlink" title="逻辑的目标"></a>逻辑的目标</h2><ol><li><p>对有道理的说话做<strong>系统性的研究</strong>。</p></li><li><p>增强说话与思考的能力。</p></li></ol><p>最终目标是让讲话有道理，并能理解他人反驳的理由。比如两个人因为买衣服时颜色的选择而吵架，原因通常是因为对颜色的主观认同，背后可能有客观或受欢迎的道理。争吵是因为假设背后的道理存在客观标准，有人因追求自我而不遵循大众喜好的理由，指出颜色好看的争吵是无谓的争吵，意识到争吵无谓就不要再继续争吵了。</p><h2 id="普通学科与逻辑学不能简单类比"><a href="#普通学科与逻辑学不能简单类比" class="headerlink" title="普通学科与逻辑学不能简单类比"></a>普通学科与逻辑学不能简单类比</h2><ol><li>懂理财才有好晚年? 懂得一些理财知识和方法无疑是有益的。经济学家们也普遍倡导合理理财，通过有效的资产配置实现财富的保值增值。例如，学习一些基本的投资知识，了解股票、基金、债券等不同投资产品的特点和风险，能够帮助我们做出更明智的投资决策。​<br>然而，不懂理财知识并不一定就意味着会有凄凉的晚年。生活中，我们也能看到一些人虽然对复杂的理财知识知之甚少，但他们通过踏实工作、勤俭节约，同样积累了一定的财富，过上了安稳的生活。而且，即使自己不精通理财，也可以通过与专业的理财顾问交流，或者向善于理财的朋友请教，来获取合理的理财建议。这就如同我们不一定非要成为厨师才能品尝到美味佳肴，只要懂得寻找合适的资源，同样能够享受美食带来的愉悦。</li><li>虽然每个人都知道物体会动、看得到光、也会使用电器，但不是每个人都是物理学家。虽然但是懂一些物理原理对安全有好处，至少在电器的安全使用上有些概念。</li><li>虽然每个人都知道食物放久不能吃，但并非都是生物学家。</li></ol><h2 id="类比论证不是有效论证"><a href="#类比论证不是有效论证" class="headerlink" title="类比论证不是有效论证"></a>类比论证不是有效论证</h2><p>虽然都会说话与判断道理，但学逻辑有益沟通。理解类比论证的问题，避免不合理推论。</p><p>生活中，父母常常会拿自己的孩子与别人家的孩子进行类比，这种类比的初衷可能是希望激励孩子进步，但往往会给孩子带来很大的心理压力，甚至引发亲子之间的矛盾。因为每个孩子都是独一无二的，他们有着不同的性格、兴趣爱好和学习方式，简单地将两个孩子进行类比，并不能全面、客观地评价孩子的表现。</p><p>要及时反思自己是否存在不当类比。我们要明白，即使类比论证的推论看起来没有道理，也不能就此判定结论一定错误。在某些情况下，可能只是类比的方式不恰当，但结论本身可能是有一定合理性的。然而，为了避免陷入错误的思维陷阱，我们需要及时反思自己的思维过程，审视是否存在不当类比的情况。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;逻辑的目标&quot;&gt;&lt;a href=&quot;#逻辑的目标&quot; class=&quot;headerlink&quot; title=&quot;逻辑的目标&quot;&gt;&lt;/a&gt;逻辑的目标&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;p&gt;对有道理的说话做&lt;strong&gt;系统性的研究&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;增强说话与思考的能力。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终目标是让讲话有道理，并能理解他人反驳的理由。比如两个人因为买衣服时颜色的选择而吵架，原因通常是因为对颜色的主观认同，背后可能有客观或受欢迎的道理。争吵是因为假设背后的道理存在客观标准，有人因追求自我而不遵循大众喜好的理由，指出颜色好看的争吵是无谓的争吵，意识到争吵无谓就不要再继续争吵了。&lt;/p&gt;</summary>
    
    
    
    
    <category term="thinking" scheme="https://blog.hinsyeow.org/tags/thinking/"/>
    
  </entry>
  
  <entry>
    <title>「逻辑学」- 高效讨论与《罗伯特议事原则》</title>
    <link href="https://blog.hinsyeow.org/posts/high-efficiency-discussion/"/>
    <id>https://blog.hinsyeow.org/posts/high-efficiency-discussion/</id>
    <published>2025-06-10T15:31:11.000Z</published>
    <updated>2026-03-17T17:43:02.321Z</updated>
    
    <content type="html"><![CDATA[<p>如果你有遇到以下讨论场景，一定会被“<strong>逻辑学</strong>”中的方法所吸引：</p><ul><li>公司会议上，大家为方案争得面红耳赤，散会时问题却依然悬而未决；</li><li>朋友聚会聊起热门话题，没说两句就开始互怼，好好的氛围瞬间变得火药味十足。</li></ul><p>本文就结合《罗伯特议事原则》和逻辑学干货，学习如何让讨论既有深度又有结果。</p><h2 id="一、先定规矩：讨论不是-“抢麦”"><a href="#一、先定规矩：讨论不是-“抢麦”" class="headerlink" title="一、先定规矩：讨论不是 “抢麦”"></a>一、先定规矩：讨论不是 “抢麦”</h2><h3 id="围绕主题讨论"><a href="#围绕主题讨论" class="headerlink" title="围绕主题讨论"></a>围绕主题讨论</h3><p>《罗伯特议事原则》的<strong>动议中心原则</strong>是讨论的核心。</p><p>简单来说，讨论得有个明确的主题，所有发言都要围绕具体提议展开。这就好比玩游戏，得先明确任务，队友之间才能默契配合闯关。比如公司要讨论下个月的促销活动，有人抱怨去年活动效果差，有人吐槽预算少，但没人提出具体方案，这就是典型的跑题。正确做法是，发起人先抛出动议：“我提议下个月做满减活动，用优惠券刺激消费，大家觉得可行吗？” 其他人再围绕这个提议发表看法。</p><h3 id="发言完整原则"><a href="#发言完整原则" class="headerlink" title="发言完整原则"></a>发言完整原则</h3><p>生活中总有这样的人，别人话还没说完就急着打断：“你说的不对！我觉得应该……” 这种行为在逻辑学里叫 “<strong>诉诸打断谬误</strong>“，直接剥夺了对方完整表达的机会。《罗伯特议事原则》规定，一人发言时，其他人必须保持安静，等对方说完，下一个人再举手申请发言。这就像接力赛跑，只有等前一棒稳稳交棒，下一棒才能顺利起跑，讨论才能顺畅进行。</p><h2 id="二、讲逻辑：别让情绪左右逻辑"><a href="#二、讲逻辑：别让情绪左右逻辑" class="headerlink" title="二、讲逻辑：别让情绪左右逻辑"></a>二、讲逻辑：别让情绪左右逻辑</h2><p>很多时候，讨论变争吵，是因为我们不小心掉进了逻辑陷阱。以你提议更换办公软件，同事不认同为例：</p><p>1.<strong>人身攻击</strong></p><blockquote><p>回避讨论的话题，转而质疑说话人的身份、动机或地位</p></blockquote><ul><li><p><strong>质疑身份</strong> 回答：”你不会是某office的管理员吧？”</p></li><li><p><strong>质疑动机</strong> 回答：”你收了某office多少钱？”</p></li><li><p><strong>质疑地位</strong> 回答：”你就一文员” | “你上次方案都搞砸了，这次肯定也不行。”</p></li></ul><p>2.<strong>转移话题</strong></p><ul><li>同事：”隔壁家的某office产品也不好用，你怎么不说？”</li></ul><p>3.<strong>以偏概全</strong></p><p>用个例以偏概全的诡辩</p><ul><li>同事：”我身边的人也用某office很久了，也没发现问题”</li></ul><p>4.<strong>偷换概念</strong></p><p>假设有人提出了观点A，诡辩者不直接反驳，而是把观点A偷换成B，然后反驳观点B，让围观者认同观点B是错误的，然后进而让围观者同意观点A也是错误的。通常观点B的是一个明显错误的命题，诡辩者把观点B当成一个靶子来攻击，所以又称为<strong>稻草人诡辩法</strong></p><ul><li>同事：”把某office产品公司弄倒闭了，对大家都没好处”</li></ul><p>5.<strong>诉诸恐惧</strong></p><ul><li>同事：”不好用？那就都别用了”</li></ul><p>6.<strong>诉诸传统</strong></p><ul><li>同事：”我们公司一直用的某office很久了，历史证明这个软件很好用”</li></ul><p>7.<strong>诉诸信心</strong></p><p>采用信心来代替逻辑和证据，跟这类人很难讨论，因为不讲逻辑，缺乏共同的讨论基础</p><ul><li>同事：“我相信某office就是最好的”</li></ul><p>正确做法是针对 “换软件” 这件事理性讨论：“这个软件操作太复杂，我们团队学习成本太高，有没有更简单的选择？”</p><h2 id="三、找共识：讨论的终极目标"><a href="#三、找共识：讨论的终极目标" class="headerlink" title="三、找共识：讨论的终极目标"></a>三、找共识：讨论的终极目标</h2><h3 id="限时限次原则"><a href="#限时限次原则" class="headerlink" title="限时限次原则"></a>限时限次原则</h3><p>《罗伯特议事原则》中的<strong>限时限次原则</strong>，能有效避免无意义的争论。规定每个人的发言时间和对同一议题的发言次数，既能防止有人霸占话语权，又能让讨论更聚焦。就像辩论赛，双方都有固定的发言时间，才能保证比赛公平高效。</p><h3 id="多数裁决原则"><a href="#多数裁决原则" class="headerlink" title="多数裁决原则"></a>多数裁决原则</h3><p>当讨论进入白热化，逻辑学中的<strong>求同存异</strong>策略就显得尤为重要。先把大家达成共识的部分梳理出来，比如讨论旅游目的地，有人想去海边，有人想去爬山，但大家都同意 “选个三天能往返的地方”，这就是共识。基于这个基础再讨论细节，就容易多了。《罗伯特议事原则》还强调<strong>多数裁决原则</strong>，在充分讨论后，尊重多数人的意见，这不是妥协，而是为了推动讨论得出结果，避免陷入僵局。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>回到文章开头的两个举例：</p><ul><li>公司会议上，大家为方案争得面红耳赤，散会时问题却依然悬而未决；<blockquote><p>下次开会前，不妨和同事约定：每人发言不超过 3 分钟，先举手再发言；</p></blockquote></li><li>朋友聚会聊起热门话题，没说两句就开始互怼，好好的氛围瞬间变得火药味十足。<blockquote><p>朋友聚会聊到有争议的话题，试试用 “我理解你的想法，不过我觉得……” 这样的句式开头，减少对立感。</p></blockquote></li></ul><p>正确讨论问题，本质上是一场思维的协作。《罗伯特议事原则》为我们提供规则框架，逻辑学教会我们理性思考，当两者结合，讨论就不再是 “各说各话” 的混乱战场，而是共同解决问题、创造价值的合作舞台。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;如果你有遇到以下讨论场景，一定会被“&lt;strong&gt;逻辑学&lt;/strong&gt;”中的方法所吸引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公司会议上，大家为方案争得面红耳赤，散会时问题却依然悬而未决；&lt;/li&gt;
&lt;li&gt;朋友聚会聊起热门话题，没说两句就开始互怼，好好的氛围瞬间变得火药味十足。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文就结合《罗伯特议事原则》和逻辑学干货，学习如何让讨论既有深度又有结果。&lt;/p&gt;</summary>
    
    
    
    
    <category term="thinking" scheme="https://blog.hinsyeow.org/tags/thinking/"/>
    
  </entry>
  
  <entry>
    <title>Cloudflare Tunnels实现内网穿透Docker桥接网络</title>
    <link href="https://blog.hinsyeow.org/posts/Cloudflare-Tunnels-implementing-internal-penetration-of-Docker-bridge-networks/"/>
    <id>https://blog.hinsyeow.org/posts/Cloudflare-Tunnels-implementing-internal-penetration-of-Docker-bridge-networks/</id>
    <published>2025-04-22T14:56:36.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在现代的网络架构中，通常会使用Docker容器来实现应用的隔离和部署。本文将介绍如何使用Cloudflare Tunnels来实现内网穿透Docker桥接网络，从而实现在没有公网IP的情况下，通过访问公网地址来访问内网中的Docker容器。</p><h2 id="选择Cloudflare-Tunnels的原因"><a href="#选择Cloudflare-Tunnels的原因" class="headerlink" title="选择Cloudflare Tunnels的原因"></a>选择Cloudflare Tunnels的原因</h2><ol><li><strong>免费</strong>：Cloudflare Tunnels是Cloudflare提供的免费服务，无需额外付费。</li><li><strong>安全</strong>：Cloudflare Tunnels采用了先进的加密技术，确保了数据的安全性。</li><li><strong>易用</strong>：Cloudflare Tunnels提供了简单易用的API和命令行工具，方便用户进行配置和管理。</li><li><strong>支持自定义域名</strong>：Cloudflare Tunnels支持自定义域名</li></ol><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>在开始之前，我们需要准备以下工具和环境：</p><ul><li>运行在Docker上的应用程序（需要穿透的服务）</li><li>一个部署在<a href="https://cloudflare.com/">Cloudflare</a>的域名</li></ul><h2 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h2><ol><li><p>访问<a href="https://dash.cloudflare.com/">cloudflare控制台</a>, 选择Zero Trust, 点击Tunnels, 点击Create Tunnels,隧道类型选择Cloudflared, 填写Tunnels名称, 点击Create Tunnels.</p></li><li><p>安装连接器，选择Docker安装，记住命令中的Token。</p></li><li><p>配置公共主机名，选择自定义域名，填写自己的域名即可。<code>服务</code>处请根据自己的需求选择，因为我的应用在部署在本地的8080端口，所以配置类型为HTTP，URL为<code>177.10.0.1:8080</code>，点击Save。（<code>177.10.0.1</code>是应用服务入口的容器的IP地址，实际部署时请根据实际情况修改）</p></li><li><p>打开你的应用程序项目，编辑<code>docker-compose.yml</code>文件(本文以<code>docker-compose.yml为例</code>，其他项目自行修改)。</p></li></ol><p>把<code>cloudflared</code>服务添加到桥接容器的网络中, 如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&quot;1&quot;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">demo-nginx:</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">demo-web</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">    <span class="attr">build:</span></span><br><span class="line">      <span class="attr">context:</span> <span class="string">./</span></span><br><span class="line">      <span class="attr">dockerfile:</span> <span class="string">./Dockerfile</span></span><br><span class="line">    <span class="attr">expose:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080&quot;</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="attr">network:</span></span><br><span class="line">        <span class="attr">ipv4_address:</span> <span class="number">177.10</span><span class="number">.0</span><span class="number">.1</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># 新增的cloudflared服务</span></span><br><span class="line">  <span class="attr">demo-cloudflared:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">cloudflare/cloudflared:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">demo-cloudflared</span></span><br><span class="line">    <span class="attr">command:</span> [ <span class="string">&quot;tunnel&quot;</span>, <span class="string">&quot;--no-autoupdate&quot;</span>, <span class="string">&quot;run&quot;</span>, <span class="string">&quot;--token&quot;</span>, <span class="string">&quot;123456&quot;</span> ]</span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="attr">network:</span></span><br><span class="line">        <span class="attr">ipv4_address:</span> <span class="number">177.10</span><span class="number">.0</span><span class="number">.2</span></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line">  <span class="attr">network:</span></span><br><span class="line">    <span class="attr">ipam:</span></span><br><span class="line">      <span class="attr">driver:</span> <span class="string">default</span></span><br><span class="line">      <span class="attr">config:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">subnet:</span> <span class="string">&#x27;177.10.0.0/16&#x27;</span></span><br></pre></td></tr></table></figure><ol start="5"><li>启动容器，执行以下命令：</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><ol start="6"><li><p>访问公网地址，即可访问到内网中的Docker容器, 访问地址为：<code>https://你的域名</code>。</p><p> 如果遇到访问失败的情况，可以检查以下几点：</p></li></ol><blockquote><ul><li>cloudflared服务是否正常启动</li><li>cloudflared和应用程序是否在同一个网络中</li></ul></blockquote>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;在现代的网络架构中，通常会使用Docker容器来实现应用的隔离和部署。本文将介绍如何使用Cloudflare Tunnels来实现内网穿透Docker桥接网络，从而实现在没有公网IP的情况下，通过访问公网地址来访问内网中的Docker容器。&lt;/p&gt;
&lt;h2 id=&quot;选择Cloudflare-Tunnels的原因&quot;&gt;&lt;a href=&quot;#选择Cloudflare-Tunnels的原因&quot; class=&quot;headerlink&quot; title=&quot;选择Cloudflare Tunnels的原因&quot;&gt;&lt;/a&gt;选择Cloudflare Tunnels的原因&lt;/h2&gt;</summary>
    
    
    
    
    <category term="network" scheme="https://blog.hinsyeow.org/tags/network/"/>
    
  </entry>
  
  <entry>
    <title>「Github」CICD流水线-hexo自动发布到Pages服务</title>
    <link href="https://blog.hinsyeow.org/posts/github-action-deploy/"/>
    <id>https://blog.hinsyeow.org/posts/github-action-deploy/</id>
    <published>2025-02-02T07:44:43.000Z</published>
    <updated>2026-03-17T17:43:02.320Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>以代码的编写到发布的过程，通常需要：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1.创建、编写</span><br><span class="line">2.build</span><br><span class="line">3.发布到仓库</span><br><span class="line">4.更新服务</span><br></pre></td></tr></table></figure><p>在此流程中，存在很多重复的步骤（不重复的地方只有编写的内容）。<br>这些步骤不仅重复繁琐，还对本地环境的依赖比较强。要是有一种方式，可以自动完成这些步骤，那就太棒了。本文将介绍一种基于<code>Github Action</code>的自动化部署方案，并以<code>hexo</code>为例，实现从编写到发布，只需一个上传步骤，即可完成整个流程的快速发布。</p><h2 id="Github-Action"><a href="#Github-Action" class="headerlink" title="Github Action"></a>Github Action</h2><h3 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h3><p><code>Github Action</code>是一个<code>持续集成</code>和<code>持续部署</code>（CI&#x2F;CD）平台，它允许您定义在代码仓库中发生特定事件时运行的任务。且免费、与 GitHub 生态深度集成</p><blockquote><p><code>持续集成</code>：频繁地（一天多次）将代码集成到主干。 每次集成都通过自动化的构建（包括编译，发布，自动化测试）来验证，从而尽早地发现集成错误。许多团队发现这种方法可以显著减少集成问题，并允许团队更快地开发高质量的软件。 这是持续交付和持续部署的基础。</p></blockquote><h3 id="如何使用"><a href="#如何使用" class="headerlink" title="如何使用"></a>如何使用</h3><p><img src="https://img.shields.io/badge/Github%20Action-%E7%82%B9%E5%87%BB%E9%98%85%E8%AF%BB%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3-green" alt="https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;actions"></p><p>官方文档中，有详细介绍，这里就不再赘述。</p><h2 id="快速实战"><a href="#快速实战" class="headerlink" title="快速实战"></a>快速实战</h2><p>这里以<code>hexo</code>为例，通过实战快速入门，实现自动化部署。</p><h3 id="实现目的"><a href="#实现目的" class="headerlink" title="实现目的"></a>实现目的</h3><p>从markdown编写到发布，只需一个上传步骤，即可完成整个流程的快速发布。</p><h3 id="前置条件"><a href="#前置条件" class="headerlink" title="前置条件"></a>前置条件</h3><ul><li>需要的基础：GitHub 账号、仓库、基础命令行知识</li><li>环境准备：无需本地安装工具</li></ul><h3 id="步骤"><a href="#步骤" class="headerlink" title="步骤"></a>步骤</h3><h4 id="1-创建仓库"><a href="#1-创建仓库" class="headerlink" title="1.创建仓库"></a>1.创建仓库</h4><ol><li>登录GitHub账号，创建两个仓库</li></ol><ul><li><p><code>xxx.github.io</code>: 用于存放博客的静态资源，如<code>index.html</code>、<code>css</code>、<code>js</code>等，xxx为github用户名</p></li><li><p><code>blog</code>: 用于存放hexo博客的源代码</p></li></ul><h4 id="2-构建Hexo"><a href="#2-构建Hexo" class="headerlink" title="2.构建Hexo"></a>2.构建Hexo</h4><p><img src="https://img.shields.io/badge/Hexo-%E7%82%B9%E5%87%BB%E9%98%85%E8%AF%BB%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3-green" alt="https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;"></p><p>把构建的hexo项目提交到<code>xxx</code>仓库中</p><p>git init<br>git checkout -b gh-pages<br>git add .<br>git commit -m “init”<br>git remote add origin <a href="https://github.com/hinsyeow/hinsyeow.github.io.git">https://github.com/hinsyeow/hinsyeow.github.io.git</a><br>git push -u origin gh-pages –force #强制覆盖</p><h4 id="3-创建Github-Action"><a href="#3-创建Github-Action" class="headerlink" title="3.创建Github Action"></a>3.创建Github Action</h4><p>返回到github，在<code>xxx</code>仓库中，找到 Settings-&gt;Pages-&gt;Build and deployment，将 source 选择为 GitHub Actions</p><p>随后，在提示</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Use a suggested workflow, browse all workflows, or create your own.</span><br></pre></td></tr></table></figure><p>中选择<code>create your own</code></p><p>创建文件<code>.github/workflows/pages.yml</code></p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Pages</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">gh-pages</span> <span class="comment"># 我们设置的分支是 gh-pages</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="comment"># If your repository depends on submodule, please see: https://github.com/actions/checkout</span></span><br><span class="line">          <span class="attr">submodules:</span> <span class="literal">false</span> <span class="comment">#recursive</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Use</span> <span class="string">Node.js</span> <span class="number">20</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/setup-node@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="comment"># Examples: 20, 18.19, &gt;=16.20.2, lts/Iron, lts/Hydrogen, *, latest, current, node</span></span><br><span class="line">          <span class="comment"># Ref: https://github.com/actions/setup-node#supported-version-syntax</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;20.11.1&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Cache</span> <span class="string">NPM</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/cache@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">node_modules</span></span><br><span class="line">          <span class="attr">key:</span> <span class="string">$&#123;&#123;</span> <span class="string">runner.OS</span> <span class="string">&#125;&#125;-npm-cache</span></span><br><span class="line">          <span class="attr">restore-keys:</span> <span class="string">|</span></span><br><span class="line"><span class="string">            $&#123;&#123; runner.OS &#125;&#125;-npm-cache</span></span><br><span class="line"><span class="string"></span>      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">Pages</span> <span class="string">artifact</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/upload-pages-artifact@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">./public</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">build</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">pages:</span> <span class="string">write</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">github-pages</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">$&#123;&#123;</span> <span class="string">steps.deployment.outputs.page_url</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">to</span> <span class="string">GitHub</span> <span class="string">Pages</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">deployment</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/deploy-pages@v4</span></span><br></pre></td></tr></table></figure><h4 id="3-配置Github-Secrets"><a href="#3-配置Github-Secrets" class="headerlink" title="3.配置Github Secrets"></a>3.配置Github Secrets</h4><blockquote><p>为了安全，我们需要配置Github Secrets，用于存放私密信息，如ssh key等</p></blockquote><p>1.先生成ssh key</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t rsa -b 4096 -C <span class="string">&quot;你的 GitHub 邮箱&quot;</span> -f hexo_deploy_key</span><br></pre></td></tr></table></figure><p>2.在生成的文件夹中找到id_rsa.pub，复制内容，以此为ssh key</p><p>3.在Github仓库的Settings-&gt;Secrets中添加DEPLOY_KEY，值为ssh key，用于提交代码,部署到Github Pages</p><p>到这里，我们就可以通过push代码，自动部署到Github Pages了</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h2&gt;&lt;p&gt;以代码的编写到发布的过程，通常需要：&lt;/p&gt;
&lt;figure class=&quot;highlight text&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1.创建、编写&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2.build&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3.发布到仓库&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4.更新服务&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="github" scheme="https://blog.hinsyeow.org/tags/github/"/>
    
  </entry>
  
  <entry>
    <title>基于Tailscale的异地组网与SSH远程连接实现</title>
    <link href="https://blog.hinsyeow.org/posts/tailscale-ssh-remote-connection/"/>
    <id>https://blog.hinsyeow.org/posts/tailscale-ssh-remote-connection/</id>
    <published>2023-09-04T16:16:33.000Z</published>
    <updated>2023-09-04T16:16:33.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在当今分布式办公与远程协作日益普及的背景下，安全、高效的异地组网与远程连接方案显得尤为重要。Tailscale作为一款基于WireGuard协议的现代VPN解决方案，凭借其简单易用的特性和强大的功能，为用户提供了便捷的组网与远程访问能力。本文将深入探讨如何利用Tailscale实现异地组网，并通过SSH协议进行安全的远程连接。</p><h2 id="一、Tailscale"><a href="#一、Tailscale" class="headerlink" title="一、Tailscale"></a>一、Tailscale</h2><h3 id="1-1-Tailscale简介"><a href="#1-1-Tailscale简介" class="headerlink" title="1.1 Tailscale简介"></a>1.1 Tailscale简介</h3><p><a href="https://tailscale.com/">Tailscale</a> 是一个基于 <a href="https://www.wireguard.com/">WireGuard</a> 协议的现代VPN软件，旨在简化网络连接与管理。与传统VPN不同，Tailscale采用了零配置网络（Zero-Config Networking）技术，无需复杂的路由器配置，即可快速实现设备间的安全通信。其主要功能包括内网穿透、虚拟组网、远程访问等，广泛应用于家庭网络、企业办公和开发测试等场景。</p><h3 id="1-2-技术原理"><a href="#1-2-技术原理" class="headerlink" title="1.2 技术原理"></a>1.2 技术原理</h3><p>Tailscale基于WireGuard协议构建，利用加密隧道技术在设备间建立安全连接。WireGuard是一种快速、现代的VPN协议，具有高性能、低延迟和简洁的设计特点。Tailscale通过自动配置IP地址和路由规则，实现设备间的自动发现与连接，形成一个虚拟的私有网络（Tailnet）。在这个网络中，所有设备都拥有唯一的IP地址，并且可以通过该地址进行直接通信，就像它们处于同一个局域网中一样。</p><h3 id="1-3-优势分析"><a href="#1-3-优势分析" class="headerlink" title="1.3 优势分析"></a>1.3 优势分析</h3><ul><li><strong>简单易用</strong>：无需专业的网络知识，只需在设备上安装Tailscale客户端并登录账号，即可自动加入网络。</li><li><strong>安全可靠</strong>：采用端到端加密技术，确保数据传输的安全性。同时，Tailscale支持多因素认证（MFA）和设备验证，进一步增强了网络的安全性。</li><li><strong>高性能</strong>：基于WireGuard协议的高效实现，Tailscale在网络性能方面表现出色，能够提供低延迟、高带宽的连接体验。</li><li><strong>跨平台支持</strong>：支持Windows、macOS、Linux、iOS、Android等多种操作系统，方便用户在不同设备间进行连接与协作。</li></ul><h2 id="2-Tailscale安装"><a href="#2-Tailscale安装" class="headerlink" title="2. Tailscale安装"></a>2. Tailscale安装</h2><h5 id="客户端安装"><a href="#客户端安装" class="headerlink" title="客户端安装"></a>客户端安装</h5><p>登录<a href="https://tailscale.com/">Tailscale官网</a>，注册账号，然后下载对应的客户端，安装即可。</p><p>安装完成后，打开Tailscale客户端，登录账号，然后登录<a href="https://login.tailscale.com/admin/machines">控制台</a>,就可以看到已经连接的设备了。<br>在控制台上，可以看到每台设备的IP地址，这些IP地址都是在同一个局域网内的，可以直接通过这些IP地址进行内网穿透，组网，远程连接等操作。</p><h4 id="检查网络是否正常"><a href="#检查网络是否正常" class="headerlink" title="检查网络是否正常"></a>检查网络是否正常</h4><p>从某台设备上ping另一台设备的IP地址，如果ping通，说明网络正常。</p><h4 id="服务器sshd安装"><a href="#服务器sshd安装" class="headerlink" title="服务器sshd安装"></a>服务器sshd安装</h4><p>服务器上安装sshd服务，然后启动sshd服务，这样就可以通过ssh远程连接服务器了。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install openssh-server</span><br><span class="line">sudo systemctl start sshd</span><br></pre></td></tr></table></figure><h4 id="客户端ssh连接服务器"><a href="#客户端ssh连接服务器" class="headerlink" title="客户端ssh连接服务器"></a>客户端ssh连接服务器</h4><p>在客户端上，通过ssh连接服务器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ssh username@server_ip</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">username是服务器上的用户名，server_ip是服务器的IP地址, 如果是默认端口，可以不用指定端口号。</span></span><br></pre></td></tr></table></figure><h4 id="VSCode-Remote-SSH"><a href="#VSCode-Remote-SSH" class="headerlink" title="VSCode Remote SSH"></a>VSCode Remote SSH</h4><p>在VSCode中，安装Remote SSH插件，然后通过Remote SSH插件，连接服务器，就可以在本地编辑服务器上的文件了。</p><p>安装以下插件</p><blockquote><p>Remote - SSH<br>Remote - SSH: Editing Configuration Files<br>Remote - SSH: Explorer</p></blockquote><p>在VSCode中，按下F1，输入Remote SSH，选择Remote-SSH: Open Configuration File，然后选择config文件，编辑config文件，添加以下内容</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Host server</span><br><span class="line">    HostName server_ip</span><br><span class="line">    User username</span><br></pre></td></tr></table></figure><h4 id="免密登录"><a href="#免密登录" class="headerlink" title="免密登录"></a>免密登录</h4><p>以下操作，以客户端和服务端都为Windows为例，Linux和MacOS类似。</p><h5 id="客户端生成密钥"><a href="#客户端生成密钥" class="headerlink" title="客户端生成密钥"></a>客户端生成密钥</h5><p>在客户端上，打开PowerShell，输入以下命令，生成密钥</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen</span><br></pre></td></tr></table></figure><h5 id="服务端添加密钥"><a href="#服务端添加密钥" class="headerlink" title="服务端添加密钥"></a>服务端添加密钥</h5><p>拷贝客户端生成的公钥到服务端的.ssh目录下，在authorized_keys文件中追加粘贴。（如果没有authorized_keys文件，可以新建一个）</p><p>然后编辑sshd_config文件，添加以下配置</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">PubkeyAuthentication yes</span><br><span class="line">AuthorizedKeysFile  .ssh/authorized_keys</span><br><span class="line">PasswordAuthentication no  (需要将默认的yes改为no,很重要)</span><br></pre></td></tr></table></figure><p>注释掉以下配置</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">Match Group administrators</span></span><br><span class="line">      #AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys</span><br></pre></td></tr></table></figure><p>重启sshd服务</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">net stop sshd</span><br><span class="line">net start sshd</span><br></pre></td></tr></table></figure><h5 id="客户端免密登录"><a href="#客户端免密登录" class="headerlink" title="客户端免密登录"></a>客户端免密登录</h5><p>编辑客户端的.ssh目录下的config文件，添加IdentityFile配置，内容为客户端的私钥路径。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Host server</span><br><span class="line">    HostName server_ip</span><br><span class="line">    User username</span><br><span class="line">    IdentityFile C:\Users\username\.ssh\id_rsa</span><br></pre></td></tr></table></figure><h4 id="Git保存用户名密码"><a href="#Git保存用户名密码" class="headerlink" title="Git保存用户名密码"></a>Git保存用户名密码</h4><p>在客户端上，打开PowerShell，输入以下命令，保存用户名密码（需要输入过一次用户名密码）</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global credential.helper store </span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">本文详细介绍了如何利用Tailscale实现异地组网，并通过SSH进行远程连接，涵盖从原理到实践的完整流程。</summary>
    
    
    
    
    <category term="network" scheme="https://blog.hinsyeow.org/tags/network/"/>
    
  </entry>
  
  <entry>
    <title>jwt授权认证流程</title>
    <link href="https://blog.hinsyeow.org/posts/jwt-authorization/"/>
    <id>https://blog.hinsyeow.org/posts/jwt-authorization/</id>
    <published>2023-05-14T19:41:21.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<p>JWT 的全称是 JSON Web Token，是一种开放标准（RFC 7519） ，用于在网络应用环境间安全地传输声明。说得通俗一点，它就像是你在某个大型活动中的通行证，上面记录了你的基本信息（比如姓名、身份等），凭借这个通行证，你就能在活动规定的范围内自由通行，享受相应的服务和资源。</p><p>JWT 是一种基于 JSON 的轻量级开放标准，以 JSON 对象的形式存在，由三部分组成，通过点号（.）分隔，分别是头部（Header）、载荷（Payload）和签名（Signature）。在后续的内容中，我们将详细介绍 JWT 的这三个组成部分。</p><h2 id="JWT-长啥样"><a href="#JWT-长啥样" class="headerlink" title="JWT 长啥样"></a>JWT 长啥样</h2><h3 id="（一）头部（Header）"><a href="#（一）头部（Header）" class="headerlink" title="（一）头部（Header）"></a>（一）头部（Header）</h3><p>头部通常由两部分组成：令牌的类型（typ）和签名算法（alg） 。以最常见的 JSON 格式来表示，头部可能是这样的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">     &quot;alg&quot;: &quot;HS256&quot;,</span><br><span class="line"></span><br><span class="line">     &quot;typ&quot;: &quot;JWT&quot;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个例子中，”alg”: “HS256” 表示使用的签名算法是 HMAC SHA256 ，这种算法在保证数据完整性和安全性方面表现出色，就像给你的数据上了一把坚固的锁；”typ”: “JWT” 则明确声明了这是一个 JWT 令牌，让接收方一眼就能识别它的身份。</p><p>为了能在网络中安全传输，头部会被进行 Base64 编码 ，转换后的字符串就像是一种特殊的 “密文”，可以在各种网络环境中稳定传输。例如，上面的 JSON 格式头部经过 Base64 编码后，可能会变成这样一串字符：<code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9</code> 。虽然看起来像乱码，但只要掌握了解码规则，就能还原出原始的头部信息。</p><h3 id="（二）载荷（Payload）"><a href="#（二）载荷（Payload）" class="headerlink" title="（二）载荷（Payload）"></a>（二）载荷（Payload）</h3><p>载荷是 JWT 中真正携带有效信息的部分，它就像是一个包裹，里面装着各种你想要传递的数据。载荷部分也是一个 JSON 对象，包含了许多声明（claims），这些声明可以分为三类：</p><ul><li><p><strong>注册声明</strong>：是一些预定义的声明，虽然不是强制使用，但推荐使用，它们就像是一些通用的标签，方便大家理解和使用。常见的注册声明有：iss（issuer，签发者），就像快递的寄件人，标识这个 JWT 是由谁生成的；exp（expiration time，过期时间），规定了这个 JWT 的有效期限，一旦超过这个时间，JWT 就会失效，就像食品的保质期一样；sub（subject，主题），说明这个 JWT 是关于谁或什么的，比如用户 ID 等；iat（issued at，签发时间），记录了 JWT 的生成时间，方便追踪和管理。</p></li><li><p><strong>公共声明</strong>：是可以由使用 JWT 的各方自定义的声明，但为了避免冲突，建议使用一些大家都认可的名称。比如，我们可以定义一个 “name” 声明来表示用户的姓名，或者用 “role” 声明来表示用户的角色。这些公共声明可以根据具体的业务需求来灵活定义，为 JWT 赋予更多的业务含义。</p></li><li><p><strong>私有声明</strong>：是在特定的应用场景中使用的声明，只有应用的开发者和相关人员知道其含义，就像是一种内部的约定。比如，某个电商应用可能会定义一个 “cart_id” 声明来表示用户购物车的 ID，这个声明对于其他不了解该应用业务的人来说可能毫无意义，但在这个应用内部却非常重要。</p></li></ul><p>以下是一个载荷的示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">     &quot;sub&quot;: &quot;1234567890&quot;,</span><br><span class="line"></span><br><span class="line">     &quot;name&quot;: &quot;John Doe&quot;,</span><br><span class="line"></span><br><span class="line">     &quot;admin&quot;: true,</span><br><span class="line"></span><br><span class="line">     &quot;iat&quot;: 1516239022,</span><br><span class="line"></span><br><span class="line">     &quot;exp&quot;: 1516239022 + 3600</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，”sub” 声明表示用户的 ID 为 “1234567890”；”name” 声明显示用户的姓名是 “John Doe”；”admin”: true 表示该用户具有管理员权限；”iat” 声明记录了 JWT 的签发时间为 1516239022（这是一个时间戳，表示从 1970 年 1 月 1 日 00:00:00 UTC 到签发时间的秒数）；”exp” 声明则规定了 JWT 的过期时间，这里设置为签发时间加上 3600 秒（即 1 小时后过期）。</p><p>与头部类似，载荷也会被 Base64 编码 ，编码后的字符串用于在 JWT 中传输。例如，上述载荷经过 Base64 编码后，可能会变成这样：</p><p><code>eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyKzM2MDAifQ</code></p><p>这样，即使在传输过程中信息被截取，没有正确的解码方式也无法获取其中的有效信息，从而保证了数据的安全性。</p><h3 id="（三）签名（Signature）"><a href="#（三）签名（Signature）" class="headerlink" title="（三）签名（Signature）"></a>（三）签名（Signature）</h3><p>签名是 JWT 的重要组成部分，它的作用是验证 JWT 在传输过程中没有被篡改，并且确保这个 JWT 确实是由合法的签发者生成的，就像文件上的签名盖章，用来证明文件的真实性和完整性。</p><p>要生成签名，需要使用编码后的头部、编码后的载荷、一个密钥（secret）以及头部中指定的签名算法 。以 HS256 算法为例，生成签名的过程如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hmac</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> base64</span><br><span class="line"></span><br><span class="line"><span class="comment"># 假设已经有编码后的头部和载荷</span></span><br><span class="line"></span><br><span class="line">encoded_header = <span class="string">&quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9&quot;</span></span><br><span class="line"></span><br><span class="line">encoded_payload = <span class="string">&quot;eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyKzM2MDAifQ&quot;</span></span><br><span class="line"></span><br><span class="line">secret = <span class="string">&quot;your_secret_key&quot;</span>  <span class="comment"># 密钥，必须保密</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 拼接编码后的头部和载荷</span></span><br><span class="line"></span><br><span class="line">message = encoded_header + <span class="string">&quot;.&quot;</span> + encoded_payload</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用HMAC SHA256算法生成签名</span></span><br><span class="line"></span><br><span class="line">signature = hmac.new(</span><br><span class="line"></span><br><span class="line">       secret.encode(<span class="string">&#x27;utf-8&#x27;</span>),</span><br><span class="line"></span><br><span class="line">       message.encode(<span class="string">&#x27;utf-8&#x27;</span>),</span><br><span class="line"></span><br><span class="line">       hashlib.sha256</span><br><span class="line"></span><br><span class="line">).digest()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 对签名进行Base64编码</span></span><br><span class="line"></span><br><span class="line">encoded_signature = base64.urlsafe_b64encode(signature).rstrip(<span class="string">b&#x27;=&#x27;</span>).decode(<span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(encoded_signature)</span><br></pre></td></tr></table></figure><p>生成的签名会附加在编码后的头部和载荷之后，通过点号（.）分隔，最终形成完整的 JWT 。例如：<code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyKzM2MDAifQ.abcdefghijklmnopqrstuvwxyz123456</code> 。</p><p>在验证 JWT 时，接收方会使用相同的密钥和签名算法，根据接收到的头部和载荷重新计算签名 。如果计算得到的签名与 JWT 中附带的签名一致，就说明 JWT 在传输过程中没有被篡改，是可信的；反之，如果签名不一致，就说明 JWT 可能被恶意修改过，或者是伪造的，接收方就会拒绝这个 JWT。</p><p>这里要特别强调的是，密钥（secret）的保密非常重要 。如果密钥泄露，任何人都可以使用这个密钥生成合法的签名，从而伪造出有效的 JWT，这将严重威胁到系统的安全。所以，在实际应用中，一定要妥善保管密钥，采用安全的存储方式和传输方式，比如使用环境变量来存储密钥，避免将密钥硬编码在代码中。</p><h2 id="三、JWT-认证流程"><a href="#三、JWT-认证流程" class="headerlink" title="三、JWT 认证流程"></a>三、JWT 认证流程</h2><p>现在，我们已经对 JWT 的结构有了深入的了解，接下来就进入 JWT 的实际应用环节 ——JWT 认证流程。这个流程就像是一场精心编排的舞台剧，各个角色（客户端和服务器）按照特定的步骤和规则进行交互，以确保用户能够安全、顺利地访问受保护的资源。下面，我们就来详细解析这场 “舞台剧” 的每一幕。</p><h3 id="（一）用户登录"><a href="#（一）用户登录" class="headerlink" title="（一）用户登录"></a>（一）用户登录</h3><p>想象一下，你来到了一个需要身份验证才能进入的在线系统，比如一个电商平台或者一个办公系统。首先，你会在客户端（通常是网页或者移动应用）的登录界面输入你的用户名和密码 ，就像你在现实生活中出示你的身份证和密码来证明你的身份一样。然后，客户端会将这些登录信息打包成一个请求，发送给服务器 。这个请求就像是一封带着你身份信息的信件，被投递到服务器的 “信箱” 中。</p><p>在前端代码中，使用 JavaScript 的 fetch API 发送登录请求可能像这样：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fetch</span>(<span class="string">&#x27;/api/login&#x27;</span>, &#123;</span><br><span class="line"></span><br><span class="line">     <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line"></span><br><span class="line">     <span class="attr">headers</span>: &#123;</span><br><span class="line"></span><br><span class="line">       <span class="string">&#x27;Content-Type&#x27;</span>: <span class="string">&#x27;application/json&#x27;</span></span><br><span class="line"></span><br><span class="line">     &#125;,</span><br><span class="line"></span><br><span class="line">     <span class="attr">body</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123;</span><br><span class="line"></span><br><span class="line">       <span class="attr">username</span>: <span class="string">&#x27;your_username&#x27;</span>,</span><br><span class="line"></span><br><span class="line">       <span class="attr">password</span>: <span class="string">&#x27;your_password&#x27;</span></span><br><span class="line"></span><br><span class="line">     &#125;)</span><br><span class="line"></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">.<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =&gt;</span> response.<span class="title function_">json</span>())</span><br><span class="line"></span><br><span class="line">.<span class="title function_">then</span>(<span class="function"><span class="params">data</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(data))</span><br><span class="line"></span><br><span class="line">.<span class="title function_">catch</span>(<span class="function"><span class="params">error</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;Error:&#x27;</span>, error));</span><br></pre></td></tr></table></figure><p>在这个示例中，我们使用 fetch 函数向<code>/api/login</code>发送一个 POST 请求，请求头中设置了<code>Content-Type</code>为<code>application/json</code>，表示我们发送的数据是 JSON 格式。请求体中包含了用户输入的用户名和密码，通过<code>JSON.stringify</code>方法将其转换为 JSON 字符串。服务器接收到这个请求后，就会开始验证用户的身份。</p><h3 id="（二）服务器验证身份并生成-JWT"><a href="#（二）服务器验证身份并生成-JWT" class="headerlink" title="（二）服务器验证身份并生成 JWT"></a>（二）服务器验证身份并生成 JWT</h3><p>服务器收到客户端发来的登录请求后，就像是收到了一封需要验证身份的信件，它会打开信件（解析请求），取出里面的用户名和密码 。然后，服务器会根据事先存储在数据库中的用户信息来验证这些凭证是否正确 ，这就好比在现实生活中，工作人员会核对你的身份证和密码是否与档案中的信息一致。</p><p>如果验证通过，恭喜你，你成功证明了自己的身份！服务器就会开始生成 JWT 。生成 JWT 的过程就像是制作一张专属你的通行证，服务器会按照我们前面介绍的 JWT 结构，创建头部、载荷和签名 。头部中会指定签名算法，比如常用的 HS256；载荷中会包含用户的一些基本信息，比如用户 ID、用户名、角色等，还会设置一些声明，如签发时间（iat）和过期时间（exp）；最后，服务器会使用一个密钥（secret）和指定的签名算法来生成签名 ，这个密钥就像是制作通行证的特殊印章，只有拥有这个印章的服务器才能制作出有效的通行证。</p><p>以下是使用 Python 的 PyJWT 库生成 JWT 的伪代码示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> jwt</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> datetime</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">login</span>(<span class="params">username, password</span>):</span><br><span class="line"></span><br><span class="line">       <span class="comment"># 假设这里有一个函数authenticate用于验证用户身份</span></span><br><span class="line"></span><br><span class="line">       user = authenticate(username, password)</span><br><span class="line"></span><br><span class="line">       <span class="keyword">if</span> user:</span><br><span class="line"></span><br><span class="line">           <span class="comment"># 定义载荷</span></span><br><span class="line"></span><br><span class="line">           payload = &#123;</span><br><span class="line"></span><br><span class="line">               <span class="string">&#x27;user_id&#x27;</span>: user.<span class="built_in">id</span>,</span><br><span class="line"></span><br><span class="line">               <span class="string">&#x27;username&#x27;</span>: user.username,</span><br><span class="line"></span><br><span class="line">               <span class="string">&#x27;role&#x27;</span>: user.role,</span><br><span class="line"></span><br><span class="line">               <span class="string">&#x27;iat&#x27;</span>: datetime.datetime.utcnow(),</span><br><span class="line"></span><br><span class="line">               <span class="string">&#x27;exp&#x27;</span>: datetime.datetime.utcnow() + datetime.timedelta(minutes=<span class="number">30</span>)  <span class="comment"># 设置30分钟后过期</span></span><br><span class="line"></span><br><span class="line">           &#125;</span><br><span class="line"></span><br><span class="line">           <span class="comment"># 密钥，必须保密</span></span><br><span class="line"></span><br><span class="line">           secret = <span class="string">&#x27;your_secret_key&#x27;</span></span><br><span class="line"></span><br><span class="line">           <span class="comment"># 生成JWT</span></span><br><span class="line"></span><br><span class="line">           token = jwt.encode(payload, secret, algorithm=<span class="string">&#x27;HS256&#x27;</span>)</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> token</span><br><span class="line"></span><br><span class="line">       <span class="keyword">else</span>:</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><p>在这个示例中，<code>login</code>函数接收用户名和密码作为参数，调用<code>authenticate</code>函数验证用户身份。如果验证通过，就创建一个包含用户信息和声明的载荷，使用指定的密钥和 HS256 算法生成 JWT 并返回。如果验证失败，则返回<code>None</code>。</p><h3 id="（三）返回-JWT-给客户端"><a href="#（三）返回-JWT-给客户端" class="headerlink" title="（三）返回 JWT 给客户端"></a>（三）返回 JWT 给客户端</h3><p>服务器成功生成 JWT 后，就像是制作好了你的专属通行证，它会把这个 JWT 返回给客户端 。这个 JWT 通常会以 JSON 格式返回，就像把通行证放在一个信封里寄回给你。例如：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line"></span><br><span class="line">     <span class="attr">&quot;token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjI3fQ.abcdefghijklmnopqrstuvwxyz123456&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>在这个 JSON 数据中，<code>token</code>字段的值就是生成的 JWT，它由三部分组成，通过点号（.）分隔，分别是编码后的头部、编码后的载荷和签名 。客户端接收到这个 JWT 后，就可以开始使用它来访问受保护的资源了。</p><h3 id="（四）客户端存储-JWT"><a href="#（四）客户端存储-JWT" class="headerlink" title="（四）客户端存储 JWT"></a>（四）客户端存储 JWT</h3><p>客户端收到服务器返回的 JWT 后，就像是收到了一张珍贵的通行证，它需要妥善保管这张通行证，以便在后续的请求中使用 。通常，客户端会将 JWT 存储在本地存储（localStorage）或会话存储（sessionStorage）中 。本地存储就像是你的一个私人保险柜，里面的东西除非你主动清除，否则会一直存在；会话存储则像是一个临时的小盒子，当你关闭浏览器标签页时，里面的东西就会消失。</p><p>在前端代码中，使用 JavaScript 将 JWT 存储在本地存储中的示例如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> jwtToken = <span class="string">&#x27;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjI3fQ.abcdefghijklmnopqrstuvwxyz123456&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">&#x27;token&#x27;</span>, jwtToken);</span><br></pre></td></tr></table></figure><p>在这个示例中，我们使用<code>localStorage.setItem</code>方法将 JWT 存储在本地存储中，键名为<code>token</code>，值为接收到的 JWT。这样，在后续的请求中，我们就可以从本地存储中取出这个 JWT，发送给服务器进行身份验证。需要注意的是，由于本地存储和会话存储中的数据可以被 JavaScript 读取，存在一定的安全风险，所以在实际应用中，要采取一些安全措施，比如使用 HTTPS 来加密传输数据，防止 JWT 被窃取。</p><h3 id="（五）发送带-JWT-的请求"><a href="#（五）发送带-JWT-的请求" class="headerlink" title="（五）发送带 JWT 的请求"></a>（五）发送带 JWT 的请求</h3><p>当客户端需要访问受保护的资源时，就像是你拿着通行证去进入一个特定的区域，它会在请求中带上之前存储的 JWT 。通常，JWT 会被放在请求头的 Authorization 字段中，格式为<code>Bearer &lt;JWT&gt;</code> ，这里的<code>Bearer</code>就像是通行证的类型标识，告诉服务器这是一个 JWT 认证请求。</p><p>在前端代码中，使用 JavaScript 的 fetch API 发送带 JWT 的请求可能像这样：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fetch</span>(<span class="string">&#x27;/api/protected&#x27;</span>, &#123;</span><br><span class="line"></span><br><span class="line">     <span class="attr">method</span>: <span class="string">&#x27;GET&#x27;</span>,</span><br><span class="line"></span><br><span class="line">     <span class="attr">headers</span>: &#123;</span><br><span class="line"></span><br><span class="line">       <span class="string">&#x27;Authorization&#x27;</span>: \<span class="string">`Bearer \$&#123;localStorage.getItem(&#x27;token&#x27;)&#125;\`</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">     &#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&#125;)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.then(response =&gt; response.json())</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.then(data =&gt; console.log(data))</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.catch(error =&gt; console.error(&#x27;Error:&#x27;, error));</span></span><br></pre></td></tr></table></figure><p>在这个示例中，我们向<code>/api/protected</code>发送一个 GET 请求，请求头中设置了<code>Authorization</code>字段，其值为<code>Bearer</code>加上从本地存储中取出的 JWT。服务器接收到这个请求后，就会根据 JWT 来验证用户的身份和权限。</p><h3 id="（六）服务器验证-JWT"><a href="#（六）服务器验证-JWT" class="headerlink" title="（六）服务器验证 JWT"></a>（六）服务器验证 JWT</h3><p>服务器接收到带有 JWT 的请求后，就像是工作人员检查你的通行证是否有效，它会开始验证 JWT 的有效性 。验证过程主要包括以下几个方面：</p><ul><li><p><strong>签名验证</strong>：服务器会使用与生成 JWT 时相同的密钥和签名算法，根据接收到的头部和载荷重新计算签名 。如果计算得到的签名与 JWT 中附带的签名一致，就说明 JWT 在传输过程中没有被篡改，是可信的；反之，如果签名不一致，就说明 JWT 可能被恶意修改过，或者是伪造的，服务器就会拒绝这个 JWT。</p></li><li><p><strong>过期时间验证</strong>：服务器会检查 JWT 载荷中的过期时间（exp）声明 。如果当前时间已经超过了过期时间，说明 JWT 已经失效，服务器会拒绝这个 JWT；只有在当前时间在过期时间之前，JWT 才是有效的。</p></li><li><p><strong>有效载荷验证</strong>：服务器还会检查 JWT 载荷中的其他声明，比如用户 ID、角色等 ，以确保用户具有访问请求资源的权限。例如，如果某个资源只允许管理员访问，而 JWT 中的角色声明不是 “admin”，服务器就会拒绝这个请求。</p></li></ul><p>以下是使用 Python 的 PyJWT 库验证 JWT 的伪代码示例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> jwt</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">protected_route</span>(<span class="params">request</span>):</span><br><span class="line"></span><br><span class="line">       token = request.headers.get(<span class="string">&#x27;Authorization&#x27;</span>).split()\[<span class="number">1</span>]  <span class="comment"># 从请求头中获取JWT</span></span><br><span class="line"></span><br><span class="line">       <span class="keyword">try</span>:</span><br><span class="line"></span><br><span class="line">           <span class="comment"># 密钥，必须保密</span></span><br><span class="line"></span><br><span class="line">           secret = <span class="string">&#x27;your_secret_key&#x27;</span></span><br><span class="line"></span><br><span class="line">           <span class="comment"># 验证JWT</span></span><br><span class="line"></span><br><span class="line">           payload = jwt.decode(token, secret, algorithms=\[<span class="string">&#x27;HS256&#x27;</span>])</span><br><span class="line"></span><br><span class="line">           user_id = payload\[<span class="string">&#x27;user_id&#x27;</span>]</span><br><span class="line"></span><br><span class="line">           <span class="comment"># 继续处理请求，比如根据用户ID获取用户信息等</span></span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> &#123;<span class="string">&#x27;message&#x27;</span>: <span class="string">&#x27;Request processed successfully&#x27;</span>, <span class="string">&#x27;user_id&#x27;</span>: user_id&#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">except</span> jwt.ExpiredSignatureError:</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> &#123;<span class="string">&#x27;error&#x27;</span>: <span class="string">&#x27;Token expired&#x27;</span>&#125;, <span class="number">401</span></span><br><span class="line"></span><br><span class="line">       <span class="keyword">except</span> jwt.InvalidTokenError:</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> &#123;<span class="string">&#x27;error&#x27;</span>: <span class="string">&#x27;Invalid token&#x27;</span>&#125;, <span class="number">401</span></span><br></pre></td></tr></table></figure><p>在这个示例中，<code>protected_route</code>函数从请求头中获取 JWT，使用指定的密钥和 HS256 算法验证 JWT 的有效性。如果验证成功，就可以从载荷中获取用户 ID，并继续处理请求；如果 JWT 过期，会捕获<code>ExpiredSignatureError</code>异常，返回错误信息和 401 状态码；如果 JWT 无效，会捕获<code>InvalidTokenError</code>异常，返回错误信息和 401 状态码。</p><h3 id="（七）访问受保护资源"><a href="#（七）访问受保护资源" class="headerlink" title="（七）访问受保护资源"></a>（七）访问受保护资源</h3><p>如果 JWT 验证通过，就像是你的通行证被工作人员认可，服务器会允许用户访问受保护的资源 。服务器会根据请求的内容，返回相应的资源数据给客户端 。例如，如果你请求的是用户的个人信息，服务器会从数据库中查询出你的个人信息并返回给你；如果你请求的是一个文件，服务器会将文件内容发送给你。</p><p>以下是一个简单的示例，展示如何根据用户 ID 获取用户信息并返回：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">get_user_info</span>(<span class="params">user_id</span>):</span><br><span class="line"></span><br><span class="line">       <span class="comment"># 假设这里有一个函数从数据库中获取用户信息</span></span><br><span class="line"></span><br><span class="line">       user = get_user_from_db(user_id)</span><br><span class="line"></span><br><span class="line">       <span class="keyword">if</span> user:</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> &#123;<span class="string">&#x27;user_info&#x27;</span>: &#123;<span class="string">&#x27;id&#x27;</span>: user.<span class="built_in">id</span>, <span class="string">&#x27;name&#x27;</span>: user.name, <span class="string">&#x27;email&#x27;</span>: user.email&#125;&#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">else</span>:</span><br><span class="line"></span><br><span class="line">           <span class="keyword">return</span> &#123;<span class="string">&#x27;error&#x27;</span>: <span class="string">&#x27;User not found&#x27;</span>&#125;, <span class="number">404</span></span><br></pre></td></tr></table></figure><p>在这个示例中，<code>get_user_info</code>函数接收用户 ID 作为参数，调用<code>get_user_from_db</code>函数从数据库中获取用户信息。如果找到用户，就返回用户信息；如果未找到用户，就返回错误信息和 404 状态码。在实际应用中，<code>protected_route</code>函数验证 JWT 通过后，可以调用<code>get_user_info</code>函数获取用户信息并返回给客户端。</p><h3 id="（八）过期和刷新"><a href="#（八）过期和刷新" class="headerlink" title="（八）过期和刷新"></a>（八）过期和刷新</h3><p>JWT 通常会设置一个过期时间，这是为了增强安全性 。就像你的车票有一个有效期，过了有效期就不能使用了。如果 JWT 过期了，客户端在发送请求时，服务器会拒绝这个请求，提示用户需要重新登录或刷新 JWT 。</p><p>为了避免用户频繁重新登录，客户端可以在 JWT 过期前进行刷新 。刷新 JWT 的过程通常是客户端向服务器发送一个刷新请求，这个请求中会带上旧的 JWT 。服务器验证旧 JWT 的有效性（虽然它可能快过期了，但还在有效期内），如果验证通过，就会生成一个新的 JWT 并返回给客户端 。客户端收到新的 JWT 后，就可以用它来继续访问受保护的资源了。</p><p>在前端代码中，使用 JavaScript 发送刷新 JWT 的请求示例如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fetch</span>(<span class="string">&#x27;/api/refresh&#x27;</span>, &#123;</span><br><span class="line"></span><br><span class="line">     <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line"></span><br><span class="line">     <span class="attr">headers</span>: &#123;</span><br><span class="line"></span><br><span class="line">       <span class="string">&#x27;Authorization&#x27;</span>: \<span class="string">`Bearer \$&#123;localStorage.getItem(&#x27;token&#x27;)&#125;\`</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">     &#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&#125;)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.then(response =&gt; response.json())</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.then(data =&gt; &#123;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">       if (data.token) &#123;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">           localStorage.setItem(&#x27;token&#x27;, data.token);  // 更新本地存储的JWT</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">       &#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&#125;)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.catch(error =&gt; console.error(&#x27;Error:&#x27;, error));</span></span><br></pre></td></tr></table></figure><p>在这个示例中，我们向<code>/api/refresh</code>发送一个 POST 请求，请求头中带上旧的 JWT。服务器接收到请求后，如果验证通过，会返回一个包含新 JWT 的 JSON 数据。客户端接收到数据后，检查是否有<code>token</code>字段，如果有，就更新本地存储中的 JWT。</p><h3 id="（九）退出登录"><a href="#（九）退出登录" class="headerlink" title="（九）退出登录"></a>（九）退出登录</h3><p>当用户选择退出登录时，就像是你主动交出了通行证，客户端可以简单地删除存储的 JWT ，以阻止进一步的访问。在前端代码中，使用 JavaScript 删除本地存储中的 JWT 示例如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="string">&#x27;token&#x27;</span>);</span><br></pre></td></tr></table></figure><p>在这个示例中，我们使用<code>localStorage.removeItem</code>方法删除键名为<code>token</code>的本地存储项，这样就删除了存储的 JWT。当用户再次发送请求时，由于没有有效的 JWT，服务器会拒绝请求，提示用户需要重新登录。</p><h2 id="四、JWT-的优点"><a href="#四、JWT-的优点" class="headerlink" title="四、JWT 的优点"></a>四、JWT 的优点</h2><p>通过前面的介绍，我们已经深入了解了 JWT 的结构和认证流程，现在来总结一下 JWT 在实际应用中展现出的诸多优点。</p><h3 id="（一）无状态性，减轻服务器压力"><a href="#（一）无状态性，减轻服务器压力" class="headerlink" title="（一）无状态性，减轻服务器压力"></a>（一）无状态性，减轻服务器压力</h3><p>JWT 是一种无状态的认证机制 ，这意味着服务器不需要在后端存储任何与 JWT 相关的会话信息。在传统的基于会话（Session）的认证方式中，服务器需要在内存或数据库中保存用户的会话状态，记录用户的登录信息、权限等 。随着用户数量的增加，服务器需要维护大量的会话数据，这会消耗大量的服务器资源，增加服务器的负担，就像一个仓库需要存放大量的物品，占用了很多空间和资源。</p><p>而 JWT 则不同，每个 JWT 都包含了用户身份验证和授权所需的所有信息 。服务器在接收到请求时，只需要验证 JWT 的有效性，而不需要查询额外的会话数据，大大减轻了服务器的压力 。这就好比你去图书馆借书，传统方式是图书馆工作人员需要在本子上记录你借了什么书，而 JWT 方式就像是你自己带着一张借书清单，工作人员只需要核对清单的真实性，不需要再去查找其他记录，这样可以提高效率，减少工作人员的工作量。这种无状态性使得应用更容易扩展，可以轻松地部署到多个服务器上，实现负载均衡，提高系统的性能和可用性。</p><h3 id="（二）自包含性，方便传输"><a href="#（二）自包含性，方便传输" class="headerlink" title="（二）自包含性，方便传输"></a>（二）自包含性，方便传输</h3><p>JWT 具有自包含性，它将用户的身份信息、权限信息以及其他相关声明都包含在一个 JSON 对象中 。这就像是一个包裹，里面装着你需要的所有东西。当 JWT 在客户端和服务器之间传输时，不需要再依赖其他外部资源来获取这些信息 ，因为所有必要的信息都已经包含在 JWT 内部。例如，在一个分布式系统中，不同的服务可能需要验证用户的身份和权限。如果使用 JWT，每个服务都可以独立地验证 JWT，而不需要与其他服务或数据库进行额外的通信来获取用户信息 ，就像每个服务都有一把钥匙，可以直接打开 JWT 这个包裹，获取里面的信息，非常方便快捷。这种自包含性不仅提高了数据传输的效率，还增强了系统的独立性和可维护性，使得各个服务之间的耦合度降低，更易于开发、测试和部署。</p><h3 id="（三）安全性高，防止数据篡改"><a href="#（三）安全性高，防止数据篡改" class="headerlink" title="（三）安全性高，防止数据篡改"></a>（三）安全性高，防止数据篡改</h3><p>JWT 使用数字签名来验证令牌的真实性和完整性 ，这为数据的安全性提供了有力保障。在生成 JWT 时，服务器会使用一个密钥（secret）和指定的签名算法（如 HS256、RS256 等）对头部和载荷进行签名 ，生成的签名就像是文件上的盖章，用来证明文件的真实性。当服务器接收到 JWT 时，会使用相同的密钥和签名算法重新计算签名 ，并与 JWT 中附带的签名进行比较。如果两个签名一致，就说明 JWT 在传输过程中没有被篡改，是可信的；反之，如果签名不一致，就说明 JWT 可能被恶意修改过，或者是伪造的，服务器会拒绝这个 JWT。这就好比你收到一份重要文件，文件上有一个独特的印章，你可以通过验证印章来确认文件是否被篡改。此外，JWT 还可以使用加密算法对载荷进行加密，进一步保护数据的机密性，确保敏感信息在传输过程中不被窃取。</p><h3 id="（四）跨域支持，适应复杂环境"><a href="#（四）跨域支持，适应复杂环境" class="headerlink" title="（四）跨域支持，适应复杂环境"></a>（四）跨域支持，适应复杂环境</h3><p>在现代的 Web 应用开发中，跨域请求是一个常见的场景 。例如，前端应用可能部署在一个域名下，而后端 API 服务部署在另一个域名下，这就需要进行跨域通信。JWT 在跨域场景中表现出色，因为它可以直接在前端存储（如 localStorage、sessionStorage），并且可以很方便地在跨域请求中携带 。客户端在发送跨域请求时，只需要将 JWT 放在请求头的 Authorization 字段中，服务器就可以验证 JWT 的有效性，从而确认用户的身份和权限 。与传统的基于 Cookie 的认证方式相比，JWT 不需要依赖服务器端的 Session 来验证用户身份，避免了跨域 Cookie 带来的安全问题和复杂性 。这就像是你可以带着自己的通行证（JWT）自由穿梭在不同的区域（不同域名），而不用担心因为区域的不同而受到限制，非常适合在复杂的分布式系统和多域名环境中使用。</p><h3 id="（五）可扩展性，满足多样化需求"><a href="#（五）可扩展性，满足多样化需求" class="headerlink" title="（五）可扩展性，满足多样化需求"></a>（五）可扩展性，满足多样化需求</h3><p>JWT 具有良好的可扩展性，可以轻松地与其他身份验证和授权机制集成 ，如 OAuth、OpenID Connect 等。这使得 JWT 能够适应不同的应用场景和业务需求，为用户提供更加灵活和多样化的认证和授权解决方案 。例如，在一个大型的企业级应用中，可能需要同时支持多种登录方式，如用户名密码登录、第三方账号登录（如微信登录、QQ 登录）等。通过将 JWT 与 OAuth 等机制集成，可以实现统一的身份验证和授权管理 ，用户可以使用不同的方式登录，而系统可以通过验证 JWT 来确认用户的身份和权限。此外，JWT 还可以根据应用的需求，在载荷中添加自定义的声明信息 ，这些声明可以用于传递额外的业务数据或权限信息，进一步扩展 JWT 的功能，满足各种复杂的业务逻辑需求。</p><h2 id="五、使用-JWT-的注意事项"><a href="#五、使用-JWT-的注意事项" class="headerlink" title="五、使用 JWT 的注意事项"></a>五、使用 JWT 的注意事项</h2><p>在使用 JWT 为应用程序保驾护航时，还有一些关键的注意事项需要牢记，就像驾驶汽车时要遵守交通规则一样，只有注意这些要点，才能确保 JWT 的安全、有效使用。</p><ul><li><p><strong>保护密钥安全</strong>：密钥（secret）是 JWT 的核心机密，它就像一把万能钥匙，拥有它就能生成和验证有效的 JWT。如果密钥泄露，任何人都可以伪造 JWT，从而获得未授权的访问权限，这将对系统安全造成极大的威胁。因此，一定要将密钥存储在安全的地方，避免将其硬编码在代码中 。可以考虑使用环境变量来存储密钥，这样在部署应用程序时，可以方便地设置和管理密钥，同时也增加了密钥的安全性。例如，在 Node.js 应用中，可以使用<code>dotenv</code>库来加载环境变量，将密钥存储在<code>.env</code>文件中，然后在代码中通过<code>process.env.SECRET_KEY</code>来获取密钥。</p></li><li><p><strong>设置合理过期时间</strong>：JWT 的过期时间（exp）是保证安全性的重要因素 。如果过期时间设置过长，一旦 JWT 被泄露，攻击者就有较长的时间利用它来访问受保护的资源；如果过期时间设置过短，用户可能需要频繁重新登录，这会影响用户体验。因此，需要根据应用程序的实际需求，设置一个合理的过期时间 。一般来说，对于一些安全性要求较高的应用，如金融类应用，可以将过期时间设置得较短，比如几分钟或几小时；对于一些普通的应用，可以将过期时间设置为一天或几天。同时，还可以结合刷新令牌（refresh token）机制，在 JWT 过期前为用户自动刷新令牌，避免用户频繁登录。</p></li><li><p><strong>不存储敏感信息</strong>：虽然 JWT 的载荷部分可以存储各种信息，但不应该在其中存储敏感信息，如用户密码、银行卡号等 。因为 JWT 是可以被解码的，任何人都可以获取载荷中的信息，即使进行了 Base64 编码，也只是一种简单的编码方式，并非加密，很容易被破解。所以，在载荷中只应存储必要的非敏感信息，如用户 ID、用户名、角色等，对于敏感信息，应该在服务器端进行安全存储和管理 。</p></li><li><p><strong>使用 HTTPS 传输</strong>：JWT 在传输过程中可能会被截获，因此使用 HTTPS 协议进行传输至关重要 。HTTPS 通过 SSL&#x2F;TLS 协议对数据进行加密，确保数据在客户端和服务器之间传输时的机密性和完整性 ，可以有效防止 JWT 被窃取和篡改。如果使用 HTTP 协议传输 JWT，攻击者可以通过中间人攻击等手段获取 JWT，进而利用它进行恶意操作。所以，在生产环境中，一定要确保应用程序使用 HTTPS 协议来传输 JWT 。</p></li><li><p><strong>防止重放攻击</strong>：重放攻击是指攻击者截获并重新发送有效的 JWT，以获取未授权的访问权限 。为了防止重放攻击，可以在 JWT 中添加一个唯一的标识符（如 JTI，JWT ID），并在服务器端维护一个已使用 JWT 的黑名单 。当服务器接收到 JWT 时，除了验证签名和过期时间外，还可以检查 JTI 是否在黑名单中，如果在黑名单中，则说明该 JWT 可能是被重放的，服务器可以拒绝这个 JWT。另外，也可以为 JWT 设置一个 “不可使用时间”（nbf，Not Before）声明，表示在这个时间之前 JWT 是无效的，这样可以进一步防止重放攻击 。</p></li><li><p><strong>验证 JWT 来源</strong>：在服务器端验证 JWT 时，不仅要验证签名和过期时间等，还要确保 JWT 是来自合法的客户端 。可以通过验证请求的来源 IP 地址、检查请求头中的其他信息等方式来确认 JWT 的来源是否可信 。例如，在一些应用中，只允许特定 IP 地址段的客户端发送请求，对于其他来源的请求，即使 JWT 有效，也会被拒绝，这样可以有效防止 JWT 被恶意使用。</p></li></ul><h2 id="六、总结"><a href="#六、总结" class="headerlink" title="六、总结"></a>六、总结</h2><p>JWT 授权认证作为现代网络应用中保障安全与便捷访问的关键机制，以其独特的结构和高效的流程，在身份验证领域占据了重要地位。从用户登录时的身份验证，到服务器生成 JWT 并返回给客户端，再到客户端存储 JWT 并在后续请求中携带，以及服务器对 JWT 的验证和对受保护资源的访问控制，每个环节都紧密相扣，确保了只有合法用户能够访问相应的资源。同时，JWT 的优点，如无状态性、自包含性、安全性高、跨域支持和可扩展性，使其成为众多应用开发者的首选认证方式。</p><p>然而，在享受 JWT 带来的便利时，我们也不能忽视使用过程中的注意事项，保护密钥安全、设置合理过期时间、不存储敏感信息、使用 HTTPS 传输以及防止重放攻击等，都是确保 JWT 安全有效使用的关键因素。</p><p>希望通过这篇文章，大家能够对 JWT 授权认证流程有一个全面而清晰的理解。如果你在实际应用中需要实现身份验证和授权功能，不妨尝试使用 JWT，相信它会为你的应用带来更高效、更安全的认证体验。如果你在学习或使用 JWT 的过程中有任何疑问，欢迎在留言区留言交流，让我们一起探讨，共同进步。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;JWT 的全称是 JSON Web Token，是一种开放标准（RFC 7519） ，用于在网络应用环境间安全地传输声明。说得通俗一点，它就像是你在某个大型活动中的通行证，上面记录了你的基本信息（比如姓名、身份等），凭借这个通行证，你就能在活动规定的范围内自由通行，享受相应的服务和资源。&lt;/p&gt;
&lt;p&gt;JWT 是一种基于 JSON 的轻量级开放标准，以 JSON 对象的形式存在，由三部分组成，通过点号（.）分隔，分别是头部（Header）、载荷（Payload）和签名（Signature）。在后续的内容中，我们将详细介绍 JWT 的这三个组成部分。&lt;/p&gt;
&lt;h2 id=&quot;JWT-长啥样&quot;&gt;&lt;a href=&quot;#JWT-长啥样&quot; class=&quot;headerlink&quot; title=&quot;JWT 长啥样&quot;&gt;&lt;/a&gt;JWT 长啥样&lt;/h2&gt;</summary>
    
    
    
    
    <category term="web" scheme="https://blog.hinsyeow.org/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>桌面应用鼠标失焦问题排查</title>
    <link href="https://blog.hinsyeow.org/posts/steal-focus/"/>
    <id>https://blog.hinsyeow.org/posts/steal-focus/</id>
    <published>2022-11-10T15:31:11.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、问题背景"><a href="#一、问题背景" class="headerlink" title="一、问题背景"></a>一、问题背景</h2><p>忙碌的夜晚，我喝着拿铁，坐在电脑旁，在指尖且飞快的敲着键盘的时候，键盘突然打不了字，window窗口栏变灰，应该是窗口失焦了，此时只需要动动鼠标，点击窗口恢复焦点，就可以继续愉悦的敲键盘了。</p><p>此起彼伏，窗口又失去了焦点，双手已然在键盘位只能再去移动鼠标..点击窗口…..</p><p>周而复始几次后，我终于忍不住了。。</p><h2 id="二、问题确认"><a href="#二、问题确认" class="headerlink" title="二、问题确认"></a>二、问题确认</h2><p>问题具有随机性、可复现，在排除了“可能不小心按错键导致失焦”的可能性后。</p><p>我就把问题的关键词确认在 <code>“window11”</code> <code>“窗口失焦”</code>。</p><h2 id="三、SFW-Seach-in-Fxxking-Web-方法"><a href="#三、SFW-Seach-in-Fxxking-Web-方法" class="headerlink" title="三、SFW(Seach in Fxxking Web)方法"></a>三、SFW(Seach in Fxxking Web)方法</h2><p>得到了的关键词，开始了从互联网找答案。</p><p><img src="https://s1.ax1x.com/2022/11/11/z95LUP.png" alt="z95LUP.png"></p><p>通过一番操作后，仍然得不到任何有用的信息。</p><h2 id="四、问题重新分析、猜测"><a href="#四、问题重新分析、猜测" class="headerlink" title="四、问题重新分析、猜测"></a>四、问题重新分析、猜测</h2><p>互联网找不到答案，从问题根源入手。</p><p>由我目前的前置知识得知：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Force Window to Always Be in Focus</span><br><span class="line">// window必然会有一个窗口焦点</span><br></pre></td></tr></table></figure><p>但从我的现象来看，失焦的时刻并没有其他页面弹出，所以猜测焦点可能被后台程序获取了？传说中的<code>&quot;焦点窃取&quot;</code>?</p><p>经过重新分析后，我确定下一步方向：找到窃取焦点的进程。</p><p>去Web查询窗口焦点的资料，发现了一个软件<code>ViewWizard</code>,不仅可以捕捉到窗口的句柄，还能捕捉到窗口的pid。</p><p>但是使用起来没有那么理想：失去焦点时并没有捕捉到。</p><p>所以我找到了一篇博客：用C#实现的window api 获取 当前焦点窗口的信息，根据博客提供的思路和window API ，写出获取窗口句柄的代码。</p><pre><code>https://www.cnblogs.com/mq0036/p/12575627.html</code></pre><p>代码完成后，执行程序，尝试复现问题。果不其然，窗口被一个叫TscShellContainerClass的窗口捕捉了。</p><p><img src="https://s1.ax1x.com/2022/11/11/z95Tud.jpg" alt="z95Tud.jpg"></p><p>再用回软件<code>ViewWizard</code>根据句柄查询PID。</p><p>知道了PID后，再通过命令查询线程的信息：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">taskList|findstr XXXX</span><br></pre></td></tr></table></figure><p><a href="https://imgse.com/i/z95jC8"><img src="https://s1.ax1x.com/2022/11/11/z95jC8.md.png" alt="z95jC8.md.png"></a></p><p>?这不是远程桌面的应用程序吗？他捕捉我焦点干嘛捏？</p><h2 id="五、问题怎么产生、根源"><a href="#五、问题怎么产生、根源" class="headerlink" title="五、问题怎么产生、根源"></a>五、问题怎么产生、根源</h2><ul><li><p>通过process Manage，定位问题发生瞬间的调用信息</p><p>从mstu.exe的调用树已经几乎可以确认是WSL的锅了。</p></li></ul><p><a href="https://imgse.com/i/z95v8S"><img src="https://s1.ax1x.com/2022/11/11/z95v8S.md.png" alt="z95v8S.md.png"></a></p><p>自己平时也不会去用远程桌面，这个功能也是我前段时间为了能使用WSLg才打开的。</p><p>并且问题发生的时刻，我没有主动去开启远程桌面程序，不过我WSLg默认是启动的，还是不明白问题的原因。</p><p>于是在确定了导致问题发生的mstsc,开始<code>SFW</code>。</p><h2 id="六、问题追溯"><a href="#六、问题追溯" class="headerlink" title="六、问题追溯"></a>六、问题追溯</h2><h4 id="线索一：mstsc-exe"><a href="#线索一：mstsc-exe" class="headerlink" title="线索一：mstsc.exe"></a>线索一：mstsc.exe</h4><h1 id="mstsc-exe-stealing-focus-mstsc-exe窃取了焦点"><a href="#mstsc-exe-stealing-focus-mstsc-exe窃取了焦点" class="headerlink" title="mstsc.exe stealing focus(mstsc.exe窃取了焦点)"></a>mstsc.exe stealing focus(mstsc.exe窃取了焦点)</h1><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://answers.microsoft.com/en-us/windows/forum/all/mstscexe-stealing-focus/f9acb6fd-3c95-4e22-8f40-dc41dffbb178</span><br></pre></td></tr></table></figure><p><a href="https://imgse.com/i/z95xgg"><img src="https://s1.ax1x.com/2022/11/11/z95xgg.md.png" alt="z95xgg.md.png"></a></p><p><a href="https://imgse.com/i/z95zvQ"><img src="https://s1.ax1x.com/2022/11/11/z95zvQ.png" alt="z95zvQ.png"></a></p><p>帖子最终结果为官方也不知道。。当事人自己删除了wslg才临时解决问题。。</p><p>不过通过这一线索，基本可以确认问题出现在window 和 SubSystem linux  之间</p><h4 id="线索二：Windows-Subsystem-for-Linux-GUI和mstsc-exe之间的联系"><a href="#线索二：Windows-Subsystem-for-Linux-GUI和mstsc-exe之间的联系" class="headerlink" title="线索二：Windows Subsystem for Linux GUI和mstsc.exe之间的联系"></a>线索二：Windows Subsystem for Linux GUI和mstsc.exe之间的联系</h4><p><img src="https://s1.ax1x.com/2022/11/11/z9IwVI.jpg" alt="z9IwVI.jpg"></p><p>通过搜索得知，微软在<code>WSLg System Distro</code>里加了远程桌面服务套件——<code>FreeRDP</code>，一个支持Windows远程桌面协议（RDP）的服务端.</p><blockquote><p>整个<code>WSLg System Distro</code>、Windows远程桌面客户端（mstsc.exe）的窗口都是对用户隐藏的，猜测是用户开启WSL或者敲完Linux GUI程序名的时候，这些隐藏的组件就已经在后台运行了。具体来说就是上图的<code>WSLGd</code>，微软解释说一个类似守护进程的程序，负责启动<code>Weston</code>、建立RDP通信等工作，如果它们挂了的话还要负责重启它们。</p></blockquote><h3 id="线索三、问题在于window通过RDP向WSL通信时"><a href="#线索三、问题在于window通过RDP向WSL通信时" class="headerlink" title="线索三、问题在于window通过RDP向WSL通信时"></a>线索三、问题在于window通过RDP向WSL通信时</h3><p><img src="https://s1.ax1x.com/2022/11/11/z9IrPf.png" alt="z9IrPf.png"></p><p>通过这一线索和开源的信息得知，当一些内容到shot（剪切板）时，负责RDP的线程发生崩溃。</p><p>最后发现了这个问题的issue：</p><p><a href="https://imgse.com/i/z9Ipuj"><img src="https://s1.ax1x.com/2022/11/11/z9Ipuj.png" alt="z9Ipuj.png"></a></p><p><a href="https://imgse.com/i/z9I9Ds"><img src="https://s1.ax1x.com/2022/11/11/z9I9Ds.png" alt="z9I9Ds.png"></a></p><h3 id="官方解释和结论"><a href="#官方解释和结论" class="headerlink" title="官方解释和结论"></a>官方解释和结论</h3><p>由于从 FreeRDP 线程到 wayland 显示循环线程的多线程切换而导致事件源为 NULL，导致崩溃。</p><p><a href="https://imgse.com/i/z9oAeA"><img src="https://s1.ax1x.com/2022/11/11/z9oAeA.png" alt="z9oAeA.png"></a></p><ul><li>提供了临时解决的方案—关闭WSLg :)</li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、问题背景&quot;&gt;&lt;a href=&quot;#一、问题背景&quot; class=&quot;headerlink&quot; title=&quot;一、问题背景&quot;&gt;&lt;/a&gt;一、问题背景&lt;/h2&gt;&lt;p&gt;忙碌的夜晚，我喝着拿铁，坐在电脑旁，在指尖且飞快的敲着键盘的时候，键盘突然打不了字，window窗口栏变灰，应该是窗口失焦了，此时只需要动动鼠标，点击窗口恢复焦点，就可以继续愉悦的敲键盘了。&lt;/p&gt;
&lt;p&gt;此起彼伏，窗口又失去了焦点，双手已然在键盘位只能再去移动鼠标..点击窗口…..&lt;/p&gt;</summary>
    
    
    
    
    <category term="bug" scheme="https://blog.hinsyeow.org/tags/bug/"/>
    
  </entry>
  
  <entry>
    <title>关于「雪糕刺客」和「消费降级」</title>
    <link href="https://blog.hinsyeow.org/posts/About-Snowflake-and-Consumption-Degradation/"/>
    <id>https://blog.hinsyeow.org/posts/About-Snowflake-and-Consumption-Degradation/</id>
    <published>2022-08-14T14:35:27.000Z</published>
    <updated>2026-03-17T17:43:02.320Z</updated>
    
    <content type="html"><![CDATA[<p>在炎炎夏日，雪糕成为了人们消暑的必备良品。然而，近年来「雪糕刺客」这一现象引发了广泛的关注和讨论。所谓「雪糕刺客」，指的是那些隐藏在冰柜中，看似普通，结账时却价格惊人的高价雪糕。这一现象不仅反映了消费市场的变化，也与当下热议的「消费降级」话题息息相关。</p><h3 id="「雪糕刺客」现象剖析"><a href="#「雪糕刺客」现象剖析" class="headerlink" title="「雪糕刺客」现象剖析"></a>「雪糕刺客」现象剖析</h3><p>笔者认为，根据艾媒咨询发布的《2022年中国雪糕行业发展现状与消费趋势调查分析报告》显示，2021年中国雪糕市场规模达到1600亿元，预计2024年将超过2000亿元。随着市场规模的不断扩大，雪糕产品的种类日益丰富，价格区间也逐渐拉大。一些高端雪糕品牌通过独特的口味、精美的包装和品牌营销，将产品价格提升到了一个新的高度。</p><p>以某知名高端雪糕品牌为例，其部分产品售价高达几十元一支。这些高价雪糕通常定位为中高端消费群体，强调产品的品质、口感和文化内涵。然而，在一些便利店和超市中，这些高价雪糕与普通平价雪糕混放在一起，没有明确的价格标识，导致消费者在结账时才发现价格超出预期，从而产生了「被刺」的感觉。</p><h3 id="「消费降级」背景下的思考"><a href="#「消费降级」背景下的思考" class="headerlink" title="「消费降级」背景下的思考"></a>「消费降级」背景下的思考</h3><p>笔者认为，与此同时，「消费降级」也成为了当下社会的一个热门话题。所谓「消费降级」，并非指消费者降低消费标准，而是更加注重性价比，追求理性消费。在经济环境不稳定、生活成本上升的背景下，消费者对于价格的敏感度逐渐提高，更加倾向于购买物美价廉的商品。</p><p>根据国家统计局发布的数据显示，2022年上半年，全国居民人均消费支出11756元，比上年同期名义增长2.5%，扣除价格因素影响，实际增长0.8%。虽然消费支出仍在增长，但增速有所放缓。这一数据反映出消费者在消费决策时更加谨慎，对于非必要消费的支出有所减少。</p><p>「雪糕刺客」现象与「消费降级」之间存在着一定的关联。一方面，高价雪糕的出现满足了部分消费者对于品质和个性化的需求，是消费升级的一种表现；另一方面，在「消费降级」的大背景下，消费者对于高价商品的接受度逐渐降低，更加注重产品的性价比。因此，笔者认为雪糕企业需要在产品定位和价格策略上进行调整，以适应市场的变化。</p><h3 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h3><p>「雪糕刺客」和「消费降级」是当下消费市场中两个值得关注的现象。笔者认为，它们反映了消费者需求的变化和市场环境的复杂性。对于消费者来说，在购买商品时应更加理性，关注产品的价格和品质；对于企业来说，也应根据市场需求调整产品策略，提供更加符合消费者需求的产品。只有这样，才能实现消费者和企业的双赢。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在炎炎夏日，雪糕成为了人们消暑的必备良品。然而，近年来「雪糕刺客」这一现象引发了广泛的关注和讨论。所谓「雪糕刺客」，指的是那些隐藏在冰柜中，看似普通，结账时却价格惊人的高价雪糕。这一现象不仅反映了消费市场的变化，也与当下热议的「消费降级」话题息息相关。&lt;/p&gt;
&lt;h3 id=&quot;「雪糕刺客」现象剖析&quot;&gt;&lt;a href=&quot;#「雪糕刺客」现象剖析&quot; class=&quot;headerlink&quot; title=&quot;「雪糕刺客」现象剖析&quot;&gt;&lt;/a&gt;「雪糕刺客」现象剖析&lt;/h3&gt;&lt;p&gt;笔者认为，根据艾媒咨询发布的《2022年中国雪糕行业发展现状与消费趋势调查分析报告》显示，2021年中国雪糕市场规模达到1600亿元，预计2024年将超过2000亿元。随着市场规模的不断扩大，雪糕产品的种类日益丰富，价格区间也逐渐拉大。一些高端雪糕品牌通过独特的口味、精美的包装和品牌营销，将产品价格提升到了一个新的高度。&lt;/p&gt;</summary>
    
    
    
    
    <category term="thinking" scheme="https://blog.hinsyeow.org/tags/thinking/"/>
    
  </entry>
  
  <entry>
    <title>01背包组合优化算法</title>
    <link href="https://blog.hinsyeow.org/posts/01Backpack/"/>
    <id>https://blog.hinsyeow.org/posts/01Backpack/</id>
    <published>2021-11-07T15:59:16.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>背包问题（Knapsack problem）是一类经典的组合优化的NP完全（NP-Complete, NPC）问题。该问题可描述为：给定一组物品，每个物品有各自的重量和价值，在背包总重量限制下，如何选择物品以使背包内物品的总价值最大。由于NPC问题不存在多项式时间复杂度的解法，不过借助动态规划，我们能够以伪多项式时间复杂度求解背包问题。</p><p>背包问题主要有以下几种类型：</p><ol><li>01背包问题</li><li>完全背包问题</li><li>多重背包问题</li></ol><h2 id="一、01背包问题"><a href="#一、01背包问题" class="headerlink" title="一、01背包问题"></a>一、01背包问题</h2><h3 id="1-1-问题描述"><a href="#1-1-问题描述" class="headerlink" title="1.1 问题描述"></a>1.1 问题描述</h3><p>一共有 $N$ 件物品，第 $i$（$i$ 从1开始）件物品的重量为 $w[i]$，价值为 $v[i]$。在背包总重量不超过承载上限 $W$ 的情况下，求能够装入背包的物品的最大价值。</p><h3 id="1-2-动态规划解法"><a href="#1-2-动态规划解法" class="headerlink" title="1.2 动态规划解法"></a>1.2 动态规划解法</h3><h4 id="1-2-1-定义状态数组"><a href="#1-2-1-定义状态数组" class="headerlink" title="1.2.1 定义状态数组"></a>1.2.1 定义状态数组</h4><p>根据问题描述，我们定义二维状态数组 $dp[i][j]$，其中 $i$ 表示前 $i$ 件物品，$j$ 表示背包的当前承重。$dp[i][j]$ 表示在前 $i$ 件物品中选择，背包承重为 $j$ 时所能获得的最大价值。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dp[i][j]</span><br></pre></td></tr></table></figure><h4 id="1-2-2-状态转移方程"><a href="#1-2-2-状态转移方程" class="headerlink" title="1.2.2 状态转移方程"></a>1.2.2 状态转移方程</h4><p>首先，将 $dp[0][0 dots W]$ 初始化为0，意味着将前0个物品（即没有物品）装入背包时，最大价值为0。当 $i &gt; 0$ 时，$dp[i][j]$ 有以下两种情况：</p><ol><li><strong>不装入第 $i$ 件物品</strong>：此时最大价值与只考虑前 $i - 1$ 件物品时相同，即 $dp[i - 1][j]$。</li><li><strong>装入第 $i$ 件物品</strong>（前提是 $j geq w[i]$）：装入第 $i$ 件物品后，背包的最大价值为装入前 $i - 1$ 件物品且背包承重为 $j - w[i]$ 时的最大价值加上第 $i$ 件物品的价值，即 $dp[i - 1][j - w[i]] + v[i]$。</li></ol><p>因此，状态转移方程为：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dp[i][j] = Math.max(dp[i - <span class="number">1</span>][j], dp[i - <span class="number">1</span>][j - w[i]] + v[i]) <span class="comment">// j &gt;= w[i]</span></span><br></pre></td></tr></table></figure><h4 id="1-2-3-空间优化"><a href="#1-2-3-空间优化" class="headerlink" title="1.2.3 空间优化"></a>1.2.3 空间优化</h4><p>由状态转移方程可知，$dp[i][j]$ 的值仅与 $dp[i - 1][0 dots j - 1]$ 有关。因此，我们可以使用滚动数组的方法对空间进行优化，将二维数组 $dp$ 优化为一维数组。需要注意的是，为了避免上一层循环的 $dp[0 dots j - 1]$ 被覆盖，$j$ 必须逆向枚举。优化后的伪代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// 01背包问题伪代码(空间优化版)</span><br><span class="line">dp[0...W] = 0</span><br><span class="line">for i = 1...N</span><br><span class="line">    for j = W...w[i] // 必须逆向枚举!!!</span><br><span class="line">        dp[j] = max(dp[j], dp[j - w[i]] + v[i])</span><br></pre></td></tr></table></figure><p>此算法的时间复杂度为 $O(NW)$，空间复杂度为 $O(W)$。由于 $W$ 的值是其位数的幂，所以该时间复杂度为伪多项式时间。</p><h2 id="二、完全背包问题"><a href="#二、完全背包问题" class="headerlink" title="二、完全背包问题"></a>二、完全背包问题</h2><h3 id="2-1-问题描述"><a href="#2-1-问题描述" class="headerlink" title="2.1 问题描述"></a>2.1 问题描述</h3><p>完全背包问题（unbounded knapsack problem）与01背包问题的区别在于，每种物品的数量是无限的。即一共有 $N$ 种物品，每种物品有无限个，第 $i$（$i$ 从1开始）种物品的重量为 $w[i]$，价值为 $v[i]$。在总重量不超过背包承载上限 $W$ 的情况下，求能够装入背包的物品的最大价值。</p><h3 id="2-2-动态规划解法"><a href="#2-2-动态规划解法" class="headerlink" title="2.2 动态规划解法"></a>2.2 动态规划解法</h3><h4 id="2-2-1-定义状态数组"><a href="#2-2-1-定义状态数组" class="headerlink" title="2.2.1 定义状态数组"></a>2.2.1 定义状态数组</h4><p>与01背包问题类似，定义 $dp[i][j]$ 表示将前 $i$ 种物品装入限重为 $j$ 的背包中所能获得的最大价值，其中 $0 leq i leq N$，$0 leq j leq W$。</p><h4 id="2-2-2-状态转移方程"><a href="#2-2-2-状态转移方程" class="headerlink" title="2.2.2 状态转移方程"></a>2.2.2 状态转移方程</h4><p>初始状态同样将 $dp[0][0 dots W]$ 初始化为0。当 $i &gt; 0$ 时，$dp[i][j]$ 有以下两种情况：</p><ol><li><strong>不装入第 $i$ 种物品</strong>：即 $dp[i - 1][j]$，与01背包问题相同。</li><li><strong>装入第 $i$ 种物品</strong>：由于每种物品数量无限，装入第 $i$ 种物品后还可继续装入该种物品，因此状态应转移到 $dp[i][j - w[i]]$，即 $dp[i][j - w[i]] + v[i]$。</li></ol><p>状态转移方程为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]) // j &gt;= w[i]</span><br></pre></td></tr></table></figure><h4 id="2-2-3-空间优化"><a href="#2-2-3-空间优化" class="headerlink" title="2.2.3 空间优化"></a>2.2.3 空间优化</h4><p>与01背包问题不同，完全背包问题空间优化后 $j$ 必须正向枚举，因为状态转移方程中的第二项是 $dp[i]$ 而非 $dp[i - 1]$，需要覆盖之前的值。优化后的伪代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// 完全背包问题思路一伪代码(空间优化版)</span><br><span class="line">dp[0...W] = 0</span><br><span class="line">for i = 1...N</span><br><span class="line">    for j = w[i]...W // 必须正向枚举!!!</span><br><span class="line">        dp[j] = max(dp[j], dp[j - w[i]] + v[i])</span><br></pre></td></tr></table></figure><p>该解法的时间复杂度为 $O(NW)$，空间复杂度为 $O(W)$。</p><h3 id="2-3-另一种思路"><a href="#2-3-另一种思路" class="headerlink" title="2.3 另一种思路"></a>2.3 另一种思路</h3><p>除上述思路外，完全背包问题还可从装入第 $i$ 种物品的数量出发。01背包问题中每种物品只能取0件或1件，而完全背包问题中可选取0件、1件、2件 $cdots$ 直到超过背包限重（$k &gt; j &#x2F; w[i]$）。状态转移方程为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># k为装入第i种物品的件数, k &lt;= j/w[i]</span><br><span class="line">dp[i][j] = max&#123;(dp[i - 1][j - k * w[i]] + k * v[i]) for every k&#125;</span><br></pre></td></tr></table></figure><p>空间优化后，由于状态转移方程中的max项为 $dp[i - 1]$，与01背包问题相同，$j$ 必须逆向枚举。优化后的伪代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">// 完全背包问题思路二伪代码(空间优化版)</span><br><span class="line">dp[0...W] = 0</span><br><span class="line">for i = 1...N</span><br><span class="line">    for j = W...w[i] // 必须逆向枚举!!!</span><br><span class="line">        for k = [0, 1... j/w[i]]</span><br><span class="line">            dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i])</span><br></pre></td></tr></table></figure><p>此方法在计算 $dp[i][j]$ 时并非 $O(1)$ 时间复杂度，因此总的时间复杂度高于第一种思路，为 $O(NWW)$ 级别。</p><h3 id="2-4-转化为01背包问题"><a href="#2-4-转化为01背包问题" class="headerlink" title="2.4 转化为01背包问题"></a>2.4 转化为01背包问题</h3><p>由于01背包问题是最基本的背包问题，我们可以将完全背包问题转化为01背包问题求解。最简单的方法是将第 $i$ 种物品转化为 $W &#x2F; w[i]$ 件费用和价值均不变的物品，然后求解01背包问题。更高效的转化方法是采用二进制思想，将第 $i$ 种物品拆分为重量为 $w_i imes 2^k$、价值为 $v_i imes 2^k$ 的若干件物品，其中 $k$ 取遍满足 $w_i imes 2^k leq W$ 的非负整数。这样可将转换后的物品数量降为对数级别。具体代码见后续模板部分。</p><h2 id="三、多重背包问题"><a href="#三、多重背包问题" class="headerlink" title="三、多重背包问题"></a>三、多重背包问题</h2><h3 id="3-1-问题描述"><a href="#3-1-问题描述" class="headerlink" title="3.1 问题描述"></a>3.1 问题描述</h3><p>多重背包问题（bounded knapsack problem）与前面问题的不同之处在于，每种物品的数量是有限的。即一共有 $N$ 种物品，第 $i$（$i$ 从1开始）种物品的数量为 $n[i]$，重量为 $w[i]$，价值为 $v[i]$。在总重量不超过背包承载上限 $W$ 的情况下，求能够装入背包的物品的最大价值。</p><h3 id="3-2-动态规划解法"><a href="#3-2-动态规划解法" class="headerlink" title="3.2 动态规划解法"></a>3.2 动态规划解法</h3><p>与完全背包问题的第二种思路类似，从装入第 $i$ 种物品的数量出发，可装入0件、1件 $cdots$ $n[i]$ 件（同时需满足不超过背包限重）。状态转移方程为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># k为装入第i种物品的件数, k &lt;= min(n[i], j/w[i])</span><br><span class="line">dp[i][j] = max&#123;(dp[i - 1][j - k * w[i]] + k * v[i]) for every k&#125;</span><br></pre></td></tr></table></figure><p>空间优化后，$j$ 同样必须逆向枚举。优化后的伪代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">// 多重背包问题伪代码(空间优化版)</span><br><span class="line">dp[0...W] = 0</span><br><span class="line">for i = 1...N</span><br><span class="line">    for j = W...w[i] // 必须逆向枚举!!!</span><br><span class="line">        for k = [0, 1... min(n[i], j/w[i])]</span><br><span class="line">            dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i])</span><br></pre></td></tr></table></figure><p>总的时间复杂度约为 $O(NWoverline{n}) &#x3D; O(Wsum_{i}n_i)$ 级别。</p><h3 id="3-3-转化为01背包问题"><a href="#3-3-转化为01背包问题" class="headerlink" title="3.3 转化为01背包问题"></a>3.3 转化为01背包问题</h3><p>采用与完全背包问题类似的思路，可将多重背包问题转化为01背包问题。利用二进制思想将第 $i$ 种物品拆分为 $O(log n_i)$ 件物品，将原问题转化为复杂度为 $O(Wsum_{i}log n_i)$ 的01背包问题，相较于第一种分析方法有较大改进。具体代码见后续模板部分。</p><h3 id="3-4-代码模板"><a href="#3-4-代码模板" class="headerlink" title="3.4 代码模板"></a>3.4 代码模板</h3><p>以下是这三种背包问题的解题模板，方便实际解题时使用，尤其注意其中的二进制优化实现。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">https://tangshusen.me/2019/11/24/knapsack-problem/</span></span><br><span class="line"><span class="comment">01背包, 完全背包, 多重背包模板(二进制优化). </span></span><br><span class="line"><span class="comment">2020.01.04 by tangshusen.</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">用法:</span></span><br><span class="line"><span class="comment">    对每个物品调用对应的函数即可, 例如多重背包:</span></span><br><span class="line"><span class="comment">    for(int i = 0; i &lt; N; i++) </span></span><br><span class="line"><span class="comment">        multiple_pack_step(dp, w[i], v[i], num[i], W);</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">参数:</span></span><br><span class="line"><span class="comment">    dp   : 空间优化后的一维dp数组, 即dp[i]表示最大承重为i的书包的结果</span></span><br><span class="line"><span class="comment">    w    : 这个物品的重量</span></span><br><span class="line"><span class="comment">    v    : 这个物品的价值</span></span><br><span class="line"><span class="comment">    n    : 这个物品的个数</span></span><br><span class="line"><span class="comment">    max_w: 书包的最大承重</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">zero_one_pack_step</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp;dp, <span class="type">int</span> w, <span class="type">int</span> v, <span class="type">int</span> max_w)</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> j = max_w; j &gt;= w; j--) <span class="comment">// 反向枚举!!!</span></span><br><span class="line">        dp[j] = <span class="built_in">max</span>(dp[j], dp[j - w] + v);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">complete_pack_step</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp;dp, <span class="type">int</span> w, <span class="type">int</span> v, <span class="type">int</span> max_w)</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> j = w; j &lt;= max_w; j++) <span class="comment">// 正向枚举!!!</span></span><br><span class="line">        dp[j] = <span class="built_in">max</span>(dp[j], dp[j - w] + v);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 法二: 转换成01背包, 二进制优化</span></span><br><span class="line">    <span class="comment">// int n = max_w / w, k = 1;</span></span><br><span class="line">    <span class="comment">// while(n &gt; 0)&#123;</span></span><br><span class="line">    <span class="comment">//     zero_one_pack_step(dp, w*k, v*k, max_w);</span></span><br><span class="line">    <span class="comment">//     n -= k;</span></span><br><span class="line">    <span class="comment">//     k = k*2 &gt; n ? n : k*2;</span></span><br><span class="line">    <span class="comment">// &#125;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">multiple_pack_step</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp;dp, <span class="type">int</span> w, <span class="type">int</span> v, <span class="type">int</span> n, <span class="type">int</span> max_w)</span></span>&#123;</span><br><span class="line">   <span class="keyword">if</span>(n &gt;= max_w / w) <span class="built_in">complete_pack_step</span>(dp, w, v, max_w);</span><br><span class="line">   <span class="keyword">else</span>&#123; <span class="comment">// 转换成01背包, 二进制优化</span></span><br><span class="line">       <span class="type">int</span> k = <span class="number">1</span>;</span><br><span class="line">       <span class="keyword">while</span>(n &gt; <span class="number">0</span>)&#123;</span><br><span class="line">           <span class="built_in">zero_one_pack_step</span>(dp, w*k, v*k, max_w);</span><br><span class="line">           n -= k;</span><br><span class="line">           k = k*<span class="number">2</span> &gt; n ? n : k*<span class="number">2</span>;</span><br><span class="line">       &#125;</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="四、背包问题的其他变种"><a href="#四、背包问题的其他变种" class="headerlink" title="四、背包问题的其他变种"></a>四、背包问题的其他变种</h2><h3 id="4-1-恰好装满"><a href="#4-1-恰好装满" class="headerlink" title="4.1 恰好装满"></a>4.1 恰好装满</h3><p>在某些背包问题中，存在必须恰好装满背包的限制。此时，基本思路不变，但初始化时有所不同。若没有恰好装满的限制，可将 $dp$ 数组全部初始化为0，因为任何容量的背包都有一个合法解“什么都不装”，其价值为0。若有恰好装满的限制，则应将 $dp[0 dots N][0]$ 初始化为0，其余 $dp$ 值初始化为 $-infty$，因为只有容量为0的背包在不装任何物品时可被“恰好装满”，其他容量的背包初始时没有合法解。</p><h3 id="4-2-求方案总数"><a href="#4-2-求方案总数" class="headerlink" title="4.2 求方案总数"></a>4.2 求方案总数</h3><p>除了求最大价值外，还有一类问题是求装满背包或将背包装至指定容量的方案总数。对于这类问题，只需将状态转移方程中的 <code>max</code> 操作改为 <code>sum</code> 操作，整体思路基本不变。例如，在完全背包问题中，转移方程变为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dp[i][j] = sum(dp[i - 1][j], dp[i][j - w[i]]) // j &gt;= w[i]</span><br></pre></td></tr></table></figure><h3 id="4-3-二维背包"><a href="#4-3-二维背包" class="headerlink" title="4.3 二维背包"></a>4.3 二维背包</h3><p>前面讨论的背包问题的容量限制通常只有一个维度（如重量），而二维背包问题则有两个限制条件（如重量和体积）。选择物品时必须同时满足这两个条件。此类问题的解法与一维背包问题类似，只是 $dp$ 数组需要多开一维。例如，在5.4节的LeetCode题目中会有具体应用。</p><h3 id="4-4-求最优方案"><a href="#4-4-求最优方案" class="headerlink" title="4.4 求最优方案"></a>4.4 求最优方案</h3><p>一般来说，背包问题主要求解最优值。若需要输出达到最优值的具体方案，可以参考动态规划问题输出方案的常规方法：记录每个状态的最优值是由哪种策略推导出来的，然后根据该策略回溯到上一个状态，依次向前推导。以01背包问题为例，我们可以使用另一个数组 $G[i][j]$ 来记录方案。设 $G[i][j] &#x3D; 0$ 表示计算 $dp[i][j]$ 时采用了max操作中的前一项（即 $dp[i - 1][j]$），$G[i][j] &#x3D; 1$ 表示采用了后一项。这分别代表了两种策略：未装入第 $i$ 个物品和装入了第 $i$ 个物品。实际上，我们也可以直接从已经计算好的 $dp[i][j]$ 反推方案：若 $dp[i][j] &#x3D; dp[i - 1][j]$，则说明未选择第 $i$ 个物品，反之则说明选择了。</p><h2 id="五、LeetCode相关题目"><a href="#五、LeetCode相关题目" class="headerlink" title="五、LeetCode相关题目"></a>五、LeetCode相关题目</h2><h3 id="5-1-Partition-Equal-Subset-Sum（分割等和子集）"><a href="#5-1-Partition-Equal-Subset-Sum（分割等和子集）" class="headerlink" title="5.1 Partition Equal Subset Sum（分割等和子集）"></a>5.1 Partition Equal Subset Sum（分割等和子集）</h3><p><a href="https://leetcode.com/problems/partition-equal-subset-sum/">416. Partition Equal Subset Sum（分割等和子集）</a></p><p>题目给定一个只包含正整数的非空数组，要求判断是否可以将该数组分割成两个子集，使得两个子集的元素和相等。由于所有元素的和 $sum$ 已知，若能分割，则两个子集的和都应为 $sum &#x2F; 2$（因此 $sum$ 不能为奇数）。该问题可转化为从数组中选取一些元素，使其和恰好为 $sum &#x2F; 2$。若将数组元素的值视为物品的重量，每件物品的价值均为1，则这是一个恰好装满的01背包问题。</p><p>我们定义空间优化后的状态数组 $dp$，由于是恰好装满，应将 $dp[0]$ 初始化为0，其余元素初始化为 $INT_MIN$，然后按照类似1.2节的伪代码更新 $dp$：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> capacity = sum / <span class="number">2</span>;</span><br><span class="line">vector&lt;<span class="type">int</span>&gt;<span class="built_in">dp</span>(capacity + <span class="number">1</span>, INT_MIN);</span><br><span class="line">dp[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> j = capacity; j &gt;= nums[i - <span class="number">1</span>]; j--)</span><br><span class="line">        dp[j] = <span class="built_in">max</span>(dp[j], <span class="number">1</span> + dp[j - nums[i - <span class="number">1</span>]]);</span><br></pre></td></tr></table></figure><p>更新完成后，若 $dp[sum &#x2F; 2] &gt; 0$，则说明满足题意。</p><p>由于该题最终只需判断是否能进行划分，因此 $dp$ 数组的每个元素可定义为 <code>bool</code> 类型，将 $dp[0]$ 初始化为 <code>true</code>，其余元素初始化为 <code>false</code>，转移方程使用或操作。完整代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">canPartition</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp; nums)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>, n = nums.<span class="built_in">size</span>();</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> &amp;num: nums) sum += num;</span><br><span class="line">    <span class="keyword">if</span>(sum % <span class="number">2</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="type">int</span> capacity = sum / <span class="number">2</span>;</span><br><span class="line">    vector&lt;<span class="type">bool</span>&gt;<span class="built_in">dp</span>(capacity + <span class="number">1</span>, <span class="literal">false</span>);</span><br><span class="line">    dp[<span class="number">0</span>] = <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; i++)</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = capacity; j &gt;= nums[i - <span class="number">1</span>]; j--)</span><br><span class="line">            dp[j] = dp[j] || dp[j - nums[i - <span class="number">1</span>]];</span><br><span class="line">        </span><br><span class="line">    <span class="keyword">return</span> dp[capacity];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>此外，该题还有一种更巧妙、更快的解法，基本思路是使用 <code>bitset</code> 记录所有可能子集的和，详见[我的Github](<a href="https://github.com/ShusenTang/LeetCode/blob/master/solutions/416">https://github.com/ShusenTang/LeetCode/blob/master/solutions/416</a>. Partition Equal Subset Sum.md)。</p></blockquote><h3 id="5-2-Coin-Change（零钱兑换）"><a href="#5-2-Coin-Change（零钱兑换）" class="headerlink" title="5.2 Coin Change（零钱兑换）"></a>5.2 Coin Change（零钱兑换）</h3><p><a href="https://leetcode.com/problems/coin-change/">322. Coin Change</a></p><p>题目给定一个目标金额 $amount$ 和一些硬币面值，假设每种面值的硬币数量无限，求最少需要多少个硬币才能组成目标金额。若将硬币面值视为物品重量，每个硬币的价值均为1，则该问题是一个恰好装满的完全背包问题。不过，这里求的是最少硬币数量，只需将2.2节的状态转移方程中的 <code>max</code> 操作改为 <code>min</code> 操作。由于是恰好装满，除 $dp[0]$ 外，其余元素应初始化为 $INT_MAX$。完整代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">coinChange</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp; coins, <span class="type">int</span> amount)</span> </span>&#123;</span><br><span class="line">    vector&lt;<span class="type">int</span>&gt;<span class="built_in">dp</span>(amount + <span class="number">1</span>, INT_MAX);</span><br><span class="line">    dp[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i &lt;= coins.<span class="built_in">size</span>(); i++)</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = coins[i - <span class="number">1</span>]; j &lt;= amount; j++)&#123;</span><br><span class="line">            <span class="comment">// 下行代码会在 1+INT_MAX 时溢出</span></span><br><span class="line">            <span class="comment">// dp[j] = min(dp[j], 1 + dp[j - coins[i - 1]]); </span></span><br><span class="line">            <span class="keyword">if</span>(dp[j] - <span class="number">1</span> &gt; dp[j - coins[i - <span class="number">1</span>]])</span><br><span class="line">                dp[j] = <span class="number">1</span> + dp[j - coins[i - <span class="number">1</span>]];   </span><br><span class="line">        &#125;</span><br><span class="line">    <span class="keyword">return</span> dp[amount] == INT_MAX ? <span class="number">-1</span> : dp[amount];   </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意，<code>1 + dp[j - coins[i - 1]]</code> 可能会导致溢出，因此代码中采用了另一种写法。</p><blockquote><p>此外，该题还可以通过搜索所有可能的组合并维护一个全局结果 $res$ 来求解，但直接搜索会超时，需要进行精心剪枝，剪枝后可击败99%的提交。详见[我的Github](<a href="https://github.com/ShusenTang/LeetCode/blob/master/solutions/322">https://github.com/ShusenTang/LeetCode/blob/master/solutions/322</a>. Coin Change.md)。</p></blockquote><h3 id="5-3-Target-Sum（目标和）"><a href="#5-3-Target-Sum（目标和）" class="headerlink" title="5.3 Target Sum（目标和）"></a>5.3 Target Sum（目标和）</h3><p><a href="https://leetcode.com/problems/target-sum/">494. Target Sum</a></p><p>题目给定一个非负整数数组和一个目标值，要求给数组中的每个数字前添加正号或负号，使得组成的表达式结果等于目标值 $S$，求满足条件的组合数量。</p><p>假设所有元素的和为 $sum$，添加正号的元素和为 $A$，添加负号的元素和为 $B$，则有 $sum &#x3D; A + B$ 且 $S &#x3D; A - B$，解方程可得 $A &#x3D; (sum + S) &#x2F; 2$。因此，问题转化为从数组中选取一些元素，使其和恰好为 $(sum + S) &#x2F; 2$，这是一个恰好装满的01背包问题，要求计算所有方案数。将1.2节状态转移方程中的 <code>max</code> 操作改为求和操作即可。需要注意的是，由于求的是方案数，$dp$ 数组应全部初始化为0，仅 $dp[0]$ 初始化为1。代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">findTargetSumWays</span><span class="params">(vector&lt;<span class="type">int</span>&gt;&amp; nums, <span class="type">int</span> S)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// for(int &amp;num: nums) sum += num;</span></span><br><span class="line">    sum = <span class="built_in">accumulate</span>(nums.<span class="built_in">begin</span>(), nums.<span class="built_in">end</span>(), <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span>(S &gt; sum || sum &lt; -S) <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// 肯定不行</span></span><br><span class="line">    <span class="keyword">if</span>((S + sum) &amp; <span class="number">1</span>) <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// 奇数</span></span><br><span class="line">    <span class="type">int</span> target = (S + sum) &gt;&gt; <span class="number">1</span>;</span><br><span class="line">    </span><br><span class="line">    vector&lt;<span class="type">int</span>&gt;<span class="built_in">dp</span>(target + <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line">    </span><br><span class="line">    dp[<span class="number">0</span>] = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i &lt;= nums.<span class="built_in">size</span>(); i++)</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = target; j &gt;= nums[i - <span class="number">1</span>]; j--)</span><br><span class="line">            dp[j] = dp[j] + dp[j - nums[i - <span class="number">1</span>]];</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> dp[target];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-4-Ones-and-Zeros（一和零）"><a href="#5-4-Ones-and-Zeros（一和零）" class="headerlink" title="5.4 Ones and Zeros（一和零）"></a>5.4 Ones and Zeros（一和零）</h3><p><a href="https://leetcode.com/problems/ones-and-zeroes/">474. Ones and Zeroes</a></p><p>题目给定一个仅包含0和1的字符串数组，要求从数组中选取尽可能多的字符串，使得这些字符串中包含的0和1的数量分别不超过 $m$ 和 $n$。</p><p>我们将每个字符串视为一个物品，字符串中0和1的数量分别视为两种“重量”，则该问题转化为一个二维01背包问题，背包的两个限重分别为 $m$ 和 $n$，要求背包能装下的物品的最大数量（可将每个物品的价值视为1）。</p><p>我们可以提前计算每个字符串的两个“重量” $w_0$ 和 $w_1$ 并存储在数组中，不过由于每个字符串的这两个值仅使用一次，因此可以在需要时直接计算。完整代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">findMaxForm</span><span class="params">(vector&lt;string&gt;&amp; strs, <span class="type">int</span> m, <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> num = strs.<span class="built_in">size</span>();</span><br><span class="line">    <span class="type">int</span> w0, w1;</span><br><span class="line">    </span><br><span class="line">    vector&lt;vector&lt;<span class="type">int</span>&gt;&gt;<span class="built_in">dp</span>(m + <span class="number">1</span>, <span class="built_in">vector</span>&lt;<span class="type">int</span>&gt;(n + <span class="number">1</span>, <span class="number">0</span>));</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i &lt;= num; i++)&#123;</span><br><span class="line">        w0 = <span class="number">0</span>; w1 = <span class="number">0</span>;</span><br><span class="line">        <span class="comment">// 计算第i - 1个字符串的两个重量</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">char</span> &amp;c: strs[i - <span class="number">1</span>])&#123;</span><br><span class="line">            <span class="keyword">if</span>(c == <span class="string">&#x27;0&#x27;</span>) w0 += <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">else</span> w1 += <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 01背包, 逆向迭代更新dp</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = m; j &gt;= w0; j--)</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> k = n; k &gt;= w1; k--)</span><br><span class="line">                dp[j][k] = <span class="built_in">max</span>(dp[j][k], <span class="number">1</span> + dp[j - w0][k - w1]);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> dp[m][n];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h2&gt;&lt;p&gt;背包问题（Knapsack problem）是一类经典的组合优化的NP完全（NP-Complete, NPC）问题。该问题可描述为：给定一组物品，每个物品有各自的重量和价值，在背包总重量限制下，如何选择物品以使背包内物品的总价值最大。由于NPC问题不存在多项式时间复杂度的解法，不过借助动态规划，我们能够以伪多项式时间复杂度求解背包问题。&lt;/p&gt;
&lt;p&gt;背包问题主要有以下几种类型：&lt;/p&gt;</summary>
    
    
    
    
    <category term="algorithm" scheme="https://blog.hinsyeow.org/tags/algorithm/"/>
    
  </entry>
  
  <entry>
    <title>排序算法解析</title>
    <link href="https://blog.hinsyeow.org/posts/sorting-algorithms/"/>
    <id>https://blog.hinsyeow.org/posts/sorting-algorithms/</id>
    <published>2020-05-17T05:42:21.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<p>在计算机科学领域，排序算法是一类基础且核心的算法，用于将一组数据按照特定顺序排列。不同的排序算法在时间复杂度、空间复杂度和稳定性等方面各有优劣，选择合适的算法对于提升程序性能至关重要。本文将详细介绍七种常见排序算法，包括冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序和堆排序，并对其复杂度进行分析。</p><h2 id="复杂度分析概述"><a href="#复杂度分析概述" class="headerlink" title="复杂度分析概述"></a>复杂度分析概述</h2><p>在评估排序算法时，主要关注以下几个指标：</p><ul><li><strong>时间复杂度</strong>：描述算法执行所需的时间随数据规模增长的变化趋势，通常用大O表示法。</li><li><strong>空间复杂度</strong>：指算法执行过程中所需的额外存储空间。</li><li><strong>稳定性</strong>：如果排序前后相等元素的相对顺序不变，则称该算法是稳定的。</li></ul><table><thead><tr><th>排序算法</th><th>平均时间复杂度</th><th>最好时间复杂度</th><th>最坏时间复杂度</th><th>空间复杂度</th><th>稳定性</th></tr></thead><tbody><tr><td>冒泡排序</td><td>$O(n^2)$</td><td>$O(n)$</td><td>$O(n^2)$</td><td>$O(1)$</td><td>稳定</td></tr><tr><td>选择排序</td><td>$O(n^2)$</td><td>$O(n^2)$</td><td>$O(n^2)$</td><td>$O(1)$</td><td>不稳定</td></tr><tr><td>插入排序</td><td>$O(n^2)$</td><td>$O(n)$</td><td>$O(n^2)$</td><td>$O(1)$</td><td>稳定</td></tr><tr><td>希尔排序</td><td>$O(n^{1.3})$</td><td>$O(n)$</td><td>$O(n^2)$</td><td>$O(1)$</td><td>不稳定</td></tr><tr><td>归并排序</td><td>$O(n log n)$</td><td>$O(n log n)$</td><td>$O(n log n)$</td><td>$O(n)$</td><td>稳定</td></tr><tr><td>快速排序</td><td>$O(n log n)$</td><td>$O(n log n)$</td><td>$O(n^2)$</td><td>$O(log n)$</td><td>不稳定</td></tr><tr><td>堆排序</td><td>$O(n log n)$</td><td>$O(n log n)$</td><td>$O(n log n)$</td><td>$O(1)$</td><td>不稳定</td></tr></tbody></table><h2 id="冒泡排序（Bubble-Sort）"><a href="#冒泡排序（Bubble-Sort）" class="headerlink" title="冒泡排序（Bubble Sort）"></a>冒泡排序（Bubble Sort）</h2><h3 id="算法原理"><a href="#算法原理" class="headerlink" title="算法原理"></a>算法原理</h3><p>冒泡排序是一种简单直观的排序算法，它重复地走访过要排序的数列，一次比较两个元素，如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换，也就是说该数列已经排序完成。</p><h3 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较小且基本有序的情况。</p><h3 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 冒泡排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">bubbleSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> len)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len - <span class="number">1</span>; ++i) &#123;</span><br><span class="line">        <span class="type">bool</span> swapped = <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; len - i - <span class="number">1</span>; ++j) &#123;</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &gt; arr[j + <span class="number">1</span>]) &#123;</span><br><span class="line">                <span class="comment">// 交换相邻元素</span></span><br><span class="line">                <span class="type">int</span> temp = arr[j];</span><br><span class="line">                arr[j] = arr[j + <span class="number">1</span>];</span><br><span class="line">                arr[j + <span class="number">1</span>] = temp;</span><br><span class="line">                swapped = <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果没有发生交换，说明数组已经有序，提前退出</span></span><br><span class="line">        <span class="keyword">if</span> (!swapped) <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">bubbleSort</span>(arr, len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：平均和最坏情况下为 $O(n^2)$，最好情况下（数组已经有序）为 $O(n)$。</li><li><strong>空间复杂度</strong>：$O(1)$，只需要常数级的额外空间。</li><li><strong>稳定性</strong>：稳定，因为只有在相邻元素逆序时才交换，相等元素不会交换位置。</li></ul><h2 id="选择排序（Selection-Sort）"><a href="#选择排序（Selection-Sort）" class="headerlink" title="选择排序（Selection Sort）"></a>选择排序（Selection Sort）</h2><h3 id="算法原理-1"><a href="#算法原理-1" class="headerlink" title="算法原理"></a>算法原理</h3><p>选择排序是一种简单直观的排序算法。它首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置，然后，再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。</p><h3 id="适用场景-1"><a href="#适用场景-1" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较小的情况，对稳定性要求不高。</p><h3 id="代码实现-1"><a href="#代码实现-1" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 选择排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">selectionSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> len)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len - <span class="number">1</span>; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> minIndex = i;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = i + <span class="number">1</span>; j &lt; len; ++j) &#123;</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &lt; arr[minIndex]) &#123;</span><br><span class="line">                minIndex = j;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (minIndex != i) &#123;</span><br><span class="line">            <span class="comment">// 交换元素</span></span><br><span class="line">            <span class="type">int</span> temp = arr[i];</span><br><span class="line">            arr[i] = arr[minIndex];</span><br><span class="line">            arr[minIndex] = temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">selectionSort</span>(arr, len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-1"><a href="#复杂度分析-1" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：无论最好、最坏还是平均情况，时间复杂度均为 $O(n^2)$。</li><li><strong>空间复杂度</strong>：$O(1)$，只需要常数级的额外空间。</li><li><strong>稳定性</strong>：不稳定，例如 <code>[5, 8, 5, 2]</code> 排序后第一个 <code>5</code> 会和 <code>2</code> 交换位置，导致两个 <code>5</code> 的相对顺序改变。</li></ul><h2 id="插入排序（Insertion-Sort）"><a href="#插入排序（Insertion-Sort）" class="headerlink" title="插入排序（Insertion Sort）"></a>插入排序（Insertion Sort）</h2><h3 id="算法原理-2"><a href="#算法原理-2" class="headerlink" title="算法原理"></a>算法原理</h3><p>插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列，对于未排序数据，在已排序序列中从后向前扫描，找到相应位置并插入。</p><h3 id="适用场景-2"><a href="#适用场景-2" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较小且基本有序的情况。</p><h3 id="代码实现-2"><a href="#代码实现-2" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 插入排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">insertionSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> len)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> temp = arr[i];</span><br><span class="line">        <span class="type">int</span> j = i - <span class="number">1</span>;</span><br><span class="line">        <span class="comment">// 从后向前扫描，找到合适的插入位置</span></span><br><span class="line">        <span class="keyword">while</span> (j &gt;= <span class="number">0</span> &amp;&amp; arr[j] &gt; temp) &#123;</span><br><span class="line">            arr[j + <span class="number">1</span>] = arr[j];</span><br><span class="line">            j--;</span><br><span class="line">        &#125;</span><br><span class="line">        arr[j + <span class="number">1</span>] = temp;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">insertionSort</span>(arr, len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-2"><a href="#复杂度分析-2" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：平均和最坏情况下为 $O(n^2)$，最好情况下（数组已经有序）为 $O(n)$。</li><li><strong>空间复杂度</strong>：$O(1)$，只需要常数级的额外空间。</li><li><strong>稳定性</strong>：稳定，因为插入时遇到相等元素不会交换位置。</li></ul><h2 id="希尔排序（Shell-Sort）"><a href="#希尔排序（Shell-Sort）" class="headerlink" title="希尔排序（Shell Sort）"></a>希尔排序（Shell Sort）</h2><h3 id="算法原理-3"><a href="#算法原理-3" class="headerlink" title="算法原理"></a>算法原理</h3><p>希尔排序是插入排序的一种更高效的改进版本。它的基本思想是将待排序的序列分割成若干个子序列，分别对这些子序列进行插入排序，随着增量逐渐减小，子序列的长度逐渐增加，整个序列变得越来越接近有序，最后对整个序列进行一次插入排序。</p><h3 id="适用场景-3"><a href="#适用场景-3" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较大且基本无序的情况。</p><h3 id="代码实现-3"><a href="#代码实现-3" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 希尔排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">shellSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> len)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> gap = len / <span class="number">2</span>; gap &gt; <span class="number">0</span>; gap /= <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = gap; i &lt; len; ++i) &#123;</span><br><span class="line">            <span class="type">int</span> temp = arr[i];</span><br><span class="line">            <span class="type">int</span> j = i;</span><br><span class="line">            <span class="comment">// 对每个子序列进行插入排序</span></span><br><span class="line">            <span class="keyword">while</span> (j &gt;= gap &amp;&amp; arr[j - gap] &gt; temp) &#123;</span><br><span class="line">                arr[j] = arr[j - gap];</span><br><span class="line">                j -= gap;</span><br><span class="line">            &#125;</span><br><span class="line">            arr[j] = temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">shellSort</span>(arr, len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-3"><a href="#复杂度分析-3" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：希尔排序的时间复杂度与增量序列的选择有关，平均情况下约为 $O(n^{1.3})$，最坏情况下为 $O(n^2)$。</li><li><strong>空间复杂度</strong>：$O(1)$，只需要常数级的额外空间。</li><li><strong>稳定性</strong>：不稳定，因为不同子序列的插入操作可能会改变相等元素的相对顺序。</li></ul><h2 id="归并排序（Merge-Sort）"><a href="#归并排序（Merge-Sort）" class="headerlink" title="归并排序（Merge Sort）"></a>归并排序（Merge Sort）</h2><h3 id="算法原理-4"><a href="#算法原理-4" class="headerlink" title="算法原理"></a>算法原理</h3><p>归并排序是采用分治法（Divide and Conquer）的一个非常典型的应用。它的基本思想是将一个序列分成两个子序列，分别对这两个子序列进行排序，然后将排好序的子序列合并成一个最终的有序序列。</p><h3 id="适用场景-4"><a href="#适用场景-4" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较大且对稳定性有要求的情况。</p><h3 id="代码实现-4"><a href="#代码实现-4" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 合并两个有序子数组</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">merge</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> left, <span class="type">int</span> mid, <span class="type">int</span> right)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> n1 = mid - left + <span class="number">1</span>;</span><br><span class="line">    <span class="type">int</span> n2 = right - mid;</span><br><span class="line">    <span class="type">int</span> L[n1], R[n2];</span><br><span class="line">    <span class="comment">// 复制数据到临时数组</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n1; ++i) L[i] = arr[left + i];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; n2; ++j) R[j] = arr[mid + <span class="number">1</span> + j];</span><br><span class="line">    <span class="type">int</span> i = <span class="number">0</span>, j = <span class="number">0</span>, k = left;</span><br><span class="line">    <span class="comment">// 合并临时数组到原数组</span></span><br><span class="line">    <span class="keyword">while</span> (i &lt; n1 &amp;&amp; j &lt; n2) &#123;</span><br><span class="line">        <span class="keyword">if</span> (L[i] &lt;= R[j]) &#123;</span><br><span class="line">            arr[k] = L[i];</span><br><span class="line">            i++;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            arr[k] = R[j];</span><br><span class="line">            j++;</span><br><span class="line">        &#125;</span><br><span class="line">        k++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 复制剩余元素</span></span><br><span class="line">    <span class="keyword">while</span> (i &lt; n1) &#123;</span><br><span class="line">        arr[k] = L[i];</span><br><span class="line">        i++;</span><br><span class="line">        k++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">while</span> (j &lt; n2) &#123;</span><br><span class="line">        arr[k] = R[j];</span><br><span class="line">        j++;</span><br><span class="line">        k++;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 归并排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">mergeSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> left, <span class="type">int</span> right)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (left &lt; right) &#123;</span><br><span class="line">        <span class="type">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line">        <span class="comment">// 递归排序左右子数组</span></span><br><span class="line">        <span class="built_in">mergeSort</span>(arr, left, mid);</span><br><span class="line">        <span class="built_in">mergeSort</span>(arr, mid + <span class="number">1</span>, right);</span><br><span class="line">        <span class="comment">// 合并排好序的子数组</span></span><br><span class="line">        <span class="built_in">merge</span>(arr, left, mid, right);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">mergeSort</span>(arr, <span class="number">0</span>, len - <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-4"><a href="#复杂度分析-4" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：无论最好、最坏还是平均情况，时间复杂度均为 $O(n log n)$。</li><li><strong>空间复杂度</strong>：$O(n)$，主要用于合并操作时的临时数组。</li><li><strong>稳定性</strong>：稳定，在合并过程中，相等元素会保持原有的相对顺序。</li></ul><h2 id="快速排序（Quick-Sort）"><a href="#快速排序（Quick-Sort）" class="headerlink" title="快速排序（Quick Sort）"></a>快速排序（Quick Sort）</h2><h3 id="算法原理-5"><a href="#算法原理-5" class="headerlink" title="算法原理"></a>算法原理</h3><p>快速排序也是采用分治法的一种排序算法。它的基本思想是通过选择一个基准元素，将序列分为两部分，使得左边部分的所有元素都小于等于基准元素，右边部分的所有元素都大于基准元素，然后分别对左右两部分进行递归排序。</p><h3 id="适用场景-5"><a href="#适用场景-5" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较大且基本无序的情况。</p><h3 id="代码实现-5"><a href="#代码实现-5" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 分区函数</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">partition</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> left, <span class="type">int</span> right)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> pivot = arr[right];</span><br><span class="line">    <span class="type">int</span> i = left - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = left; j &lt; right; ++j) &#123;</span><br><span class="line">        <span class="keyword">if</span> (arr[j] &lt; pivot) &#123;</span><br><span class="line">            i++;</span><br><span class="line">            <span class="comment">// 交换元素</span></span><br><span class="line">            <span class="type">int</span> temp = arr[i];</span><br><span class="line">            arr[i] = arr[j];</span><br><span class="line">            arr[j] = temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 将基准元素放到正确的位置</span></span><br><span class="line">    <span class="type">int</span> temp = arr[i + <span class="number">1</span>];</span><br><span class="line">    arr[i + <span class="number">1</span>] = arr[right];</span><br><span class="line">    arr[right] = temp;</span><br><span class="line">    <span class="keyword">return</span> i + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 快速排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">quickSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> left, <span class="type">int</span> right)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (left &lt; right) &#123;</span><br><span class="line">        <span class="type">int</span> pivotIndex = <span class="built_in">partition</span>(arr, left, right);</span><br><span class="line">        <span class="comment">// 递归排序左右子数组</span></span><br><span class="line">        <span class="built_in">quickSort</span>(arr, left, pivotIndex - <span class="number">1</span>);</span><br><span class="line">        <span class="built_in">quickSort</span>(arr, pivotIndex + <span class="number">1</span>, right);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">quickSort</span>(arr, <span class="number">0</span>, len - <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-5"><a href="#复杂度分析-5" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：平均和最好情况下为 $O(n log n)$，最坏情况下（例如数组已经有序）为 $O(n^2)$。</li><li><strong>空间复杂度</strong>：平均情况下为 $O(log n)$，主要是递归调用栈的深度，最坏情况下为 $O(n)$。</li><li><strong>稳定性</strong>：不稳定，在分区过程中可能会改变相等元素的相对顺序。</li></ul><h2 id="堆排序（Heap-Sort）"><a href="#堆排序（Heap-Sort）" class="headerlink" title="堆排序（Heap Sort）"></a>堆排序（Heap Sort）</h2><h3 id="算法原理-6"><a href="#算法原理-6" class="headerlink" title="算法原理"></a>算法原理</h3><p>堆排序是利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构，并同时满足堆积的性质：即子节点的键值或索引总是小于（或者大于）它的父节点。堆排序的基本思想是先将数组构建成一个最大堆（升序排序）或最小堆（降序排序），然后将堆顶元素与最后一个元素交换，再对剩余元素重新调整为堆，重复这个过程直到整个数组有序。</p><h3 id="适用场景-6"><a href="#适用场景-6" class="headerlink" title="适用场景"></a>适用场景</h3><p>数据量较大且对空间复杂度有要求的情况。</p><h3 id="代码实现-6"><a href="#代码实现-6" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调整堆</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">heapify</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> n, <span class="type">int</span> i)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> largest = i;</span><br><span class="line">    <span class="type">int</span> left = <span class="number">2</span> * i + <span class="number">1</span>;</span><br><span class="line">    <span class="type">int</span> right = <span class="number">2</span> * i + <span class="number">2</span>;</span><br><span class="line">    <span class="comment">// 找出左右子节点和根节点中的最大值</span></span><br><span class="line">    <span class="keyword">if</span> (left &lt; n &amp;&amp; arr[left] &gt; arr[largest]) &#123;</span><br><span class="line">        largest = left;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (right &lt; n &amp;&amp; arr[right] &gt; arr[largest]) &#123;</span><br><span class="line">        largest = right;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (largest != i) &#123;</span><br><span class="line">        <span class="comment">// 交换元素</span></span><br><span class="line">        <span class="type">int</span> temp = arr[i];</span><br><span class="line">        arr[i] = arr[largest];</span><br><span class="line">        arr[largest] = temp;</span><br><span class="line">        <span class="comment">// 递归调整子树</span></span><br><span class="line">        <span class="built_in">heapify</span>(arr, n, largest);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 堆排序函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">heapSort</span><span class="params">(<span class="type">int</span> arr[], <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 构建最大堆</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = n / <span class="number">2</span> - <span class="number">1</span>; i &gt;= <span class="number">0</span>; --i) &#123;</span><br><span class="line">        <span class="built_in">heapify</span>(arr, n, i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 依次取出堆顶元素并调整堆</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = n - <span class="number">1</span>; i &gt; <span class="number">0</span>; --i) &#123;</span><br><span class="line">        <span class="comment">// 交换堆顶元素和最后一个元素</span></span><br><span class="line">        <span class="type">int</span> temp = arr[<span class="number">0</span>];</span><br><span class="line">        arr[<span class="number">0</span>] = arr[i];</span><br><span class="line">        arr[i] = temp;</span><br><span class="line">        <span class="comment">// 调整剩余元素为堆</span></span><br><span class="line">        <span class="built_in">heapify</span>(arr, i, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> arr[] = &#123;<span class="number">6</span>, <span class="number">1</span>, <span class="number">5</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>&#125;;</span><br><span class="line">    <span class="type">int</span> len = <span class="built_in">sizeof</span>(arr) / <span class="built_in">sizeof</span>(arr[<span class="number">0</span>]);</span><br><span class="line">    <span class="built_in">heapSort</span>(arr, len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; len; ++i) &#123;</span><br><span class="line">        cout &lt;&lt; arr[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="复杂度分析-6"><a href="#复杂度分析-6" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><ul><li><strong>时间复杂度</strong>：无论最好、最坏还是平均情况，时间复杂度均为 $O(n log n)$。</li><li><strong>空间复杂度</strong>：$O(1)$，只需要常数级的额外空间。</li><li><strong>稳定性</strong>：不稳定，在堆调整过程中可能会改变相等元素的相对顺序。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>不同的排序算法在不同的场景下有各自的优势。在实际应用中，需要根据数据规模、数据初始状态和对稳定性的要求等因素来选择合适的排序算法。以下是一些选择建议：</p><ul><li>当数据量较小时，冒泡排序、选择排序和插入排序都是不错的选择，其中插入排序在数据基本有序时性能较好。</li><li>当数据量较大且基本无序时，希尔排序、快速排序和堆排序的性能相对较好。</li><li>当对稳定性有要求时，归并排序是一个合适的选择。</li></ul><p>希望本文能帮助你深入理解各种排序算法的原理和应用场景，在实际编程中做出更合适的选择。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在计算机科学领域，排序算法是一类基础且核心的算法，用于将一组数据按照特定顺序排列。不同的排序算法在时间复杂度、空间复杂度和稳定性等方面各有优劣，选择合适的算法对于提升程序性能至关重要。本文将详细介绍七种常见排序算法，包括冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序和堆排序，并对其复杂度进行分析。&lt;/p&gt;
&lt;h2 id=&quot;复杂度分析概述&quot;&gt;&lt;a href=&quot;#复杂度分析概述&quot; class=&quot;headerlink&quot; title=&quot;复杂度分析概述&quot;&gt;&lt;/a&gt;复杂度分析概述&lt;/h2&gt;&lt;p&gt;在评估排序算法时，主要关注以下几个指标：&lt;/p&gt;</summary>
    
    
    
    
    <category term="algorithm" scheme="https://blog.hinsyeow.org/tags/algorithm/"/>
    
  </entry>
  
  <entry>
    <title>⌈贪心算法⌋</title>
    <link href="https://blog.hinsyeow.org/posts/greedy-algorithm/"/>
    <id>https://blog.hinsyeow.org/posts/greedy-algorithm/</id>
    <published>2019-11-14T14:43:18.000Z</published>
    <updated>2026-03-17T17:43:02.310Z</updated>
    
    <content type="html"><![CDATA[<h1 id="贪心算法：原理、实现与应用实例"><a href="#贪心算法：原理、实现与应用实例" class="headerlink" title="贪心算法：原理、实现与应用实例"></a>贪心算法：原理、实现与应用实例</h1><h2 id="一、算法描述"><a href="#一、算法描述" class="headerlink" title="一、算法描述"></a>一、算法描述</h2><p>贪心算法是一种在每一步选择中都采取在当前状态下最好或最优（即最有利）的选择，从而希望导致结果是全局最好或最优的算法。它并不从整体最优上加以考虑，所做出的仅是在某种意义上的局部最优解。</p><p>贪心算法没有固定的算法框架，算法设计的关键是贪心策略的选择。必须注意的是，贪心算法不是对所有问题都能得到整体最优解，选择的贪心策略必须具备无后效性，即某个状态以前的过程不会影响以后的状态，只与当前状态有关。</p><h2 id="二、理论基础"><a href="#二、理论基础" class="headerlink" title="二、理论基础"></a>二、理论基础</h2><p>贪心算法的理论基础主要基于贪心选择性质和最优子结构性质。</p><h3 id="1-贪心选择性质"><a href="#1-贪心选择性质" class="headerlink" title="1. 贪心选择性质"></a>1. 贪心选择性质</h3><p>贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择，即贪心选择来达到。这是贪心算法可行的第一个基本要素，也是贪心算法与动态规划算法的主要区别。在动态规划算法中，每步所做的选择往往依赖于相关子问题的解，因而只有在解出相关子问题后，才能做出选择。而在贪心算法中，仅在当前状态下做出最好选择，即局部最优选择，然后再去解做出这个选择后产生的相应的子问题。</p><h3 id="2-最优子结构性质"><a href="#2-最优子结构性质" class="headerlink" title="2. 最优子结构性质"></a>2. 最优子结构性质</h3><p>当一个问题的最优解包含其子问题的最优解时，称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。</p><h2 id="三、优势"><a href="#三、优势" class="headerlink" title="三、优势"></a>三、优势</h2><ol><li><strong>算法简单高效</strong>：贪心算法的实现通常比较简单，代码量少，执行效率高。由于它不需要像动态规划那样保存大量的中间状态，因此空间复杂度较低。</li><li><strong>时间复杂度低</strong>：在很多情况下，贪心算法可以在多项式时间内解决问题，时间复杂度通常为 $O(n)$ 或 $O(nlogn)$，其中 $n$ 是问题的规模。</li><li><strong>容易理解和应用</strong>：贪心策略直观易懂，容易被理解和应用到实际问题中。</li></ol><h2 id="四、Python代码实现"><a href="#四、Python代码实现" class="headerlink" title="四、Python代码实现"></a>四、Python代码实现</h2><h3 id="1-一般贪心算法框架示例"><a href="#1-一般贪心算法框架示例" class="headerlink" title="1. 一般贪心算法框架示例"></a>1. 一般贪心算法框架示例</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 贪心算法一般框架</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">greedy_algorithm</span>(<span class="params">problem_instance</span>):</span><br><span class="line">    solution = []  <span class="comment"># 初始化解决方案</span></span><br><span class="line">    candidates = get_candidates(problem_instance)  <span class="comment"># 获取候选集</span></span><br><span class="line">    <span class="keyword">while</span> candidates <span class="keyword">and</span> <span class="keyword">not</span> is_solution_complete(solution):</span><br><span class="line">        best_candidate = select_best_candidate(candidates)</span><br><span class="line">        <span class="keyword">if</span> is_safe(best_candidate, solution):</span><br><span class="line">            solution.append(best_candidate)</span><br><span class="line">        candidates.remove(best_candidate)</span><br><span class="line">    <span class="keyword">return</span> solution</span><br><span class="line"></span><br><span class="line"><span class="comment"># 以下函数需要根据具体问题实现</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_candidates</span>(<span class="params">problem_instance</span>):</span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">select_best_candidate</span>(<span class="params">candidates</span>):</span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_safe</span>(<span class="params">candidate, solution</span>):</span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_solution_complete</span>(<span class="params">solution</span>):</span><br><span class="line">    <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><h3 id="2-具体实例实现"><a href="#2-具体实例实现" class="headerlink" title="2. 具体实例实现"></a>2. 具体实例实现</h3><h4 id="田忌赛马"><a href="#田忌赛马" class="headerlink" title="田忌赛马"></a>田忌赛马</h4><p>田忌赛马是一个经典的贪心算法应用场景。齐王和田忌各有 $n$ 匹马，每匹马有一个速度值，速度快的马会战胜速度慢的马。田忌可以通过合理安排马的出场顺序来尽可能多地赢得比赛。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">tianji_horse_race</span>(<span class="params">qi_horses, tian_horses</span>):</span><br><span class="line">    qi_horses.sort()</span><br><span class="line">    tian_horses.sort()</span><br><span class="line">    qi_left, qi_right = <span class="number">0</span>, <span class="built_in">len</span>(qi_horses) - <span class="number">1</span></span><br><span class="line">    tian_left, tian_right = <span class="number">0</span>, <span class="built_in">len</span>(tian_horses) - <span class="number">1</span></span><br><span class="line">    win_count = <span class="number">0</span></span><br><span class="line">    <span class="keyword">while</span> qi_left &lt;= qi_right:</span><br><span class="line">        <span class="keyword">if</span> tian_horses[tian_right] &gt; qi_horses[qi_right]:</span><br><span class="line">            <span class="comment"># 田忌最快的马比齐王最快的马快，用田忌最快的马对战齐王最快的马</span></span><br><span class="line">            tian_right -= <span class="number">1</span></span><br><span class="line">            qi_right -= <span class="number">1</span></span><br><span class="line">            win_count += <span class="number">1</span></span><br><span class="line">        <span class="keyword">elif</span> tian_horses[tian_left] &gt; qi_horses[qi_left]:</span><br><span class="line">            <span class="comment"># 田忌最慢的马比齐王最慢的马快，用田忌最慢的马对战齐王最慢的马</span></span><br><span class="line">            tian_left += <span class="number">1</span></span><br><span class="line">            qi_left += <span class="number">1</span></span><br><span class="line">            win_count += <span class="number">1</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="comment"># 其他情况，用田忌最慢的马对战齐王最快的马</span></span><br><span class="line">            <span class="keyword">if</span> tian_horses[tian_left] &lt; qi_horses[qi_right]:</span><br><span class="line">                win_count -= <span class="number">1</span></span><br><span class="line">            tian_left += <span class="number">1</span></span><br><span class="line">            qi_right -= <span class="number">1</span></span><br><span class="line">    <span class="keyword">return</span> win_count</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例</span></span><br><span class="line">qi_horses = [<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line">tian_horses = [<span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>]</span><br><span class="line">result = tianji_horse_race(qi_horses, tian_horses)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;田忌赢得的比赛场数: <span class="subst">&#123;result&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><h4 id="优势洗牌"><a href="#优势洗牌" class="headerlink" title="优势洗牌"></a>优势洗牌</h4><p>给定两个长度相等的整数数组 <code>A</code> 和 <code>B</code>，<code>A</code> 相对于 <code>B</code> 的优势可以通过重新排列 <code>A</code> 中的元素，使得 <code>A</code> 中比 <code>B</code> 中对应位置元素大的个数最多。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> collections <span class="keyword">import</span> deque</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">advantage_count</span>(<span class="params">A, B</span>):</span><br><span class="line">    A.sort()</span><br><span class="line">    sorted_B = <span class="built_in">sorted</span>(<span class="built_in">enumerate</span>(B), key=<span class="keyword">lambda</span> x: x[<span class="number">1</span>])</span><br><span class="line">    result = [-<span class="number">1</span>] * <span class="built_in">len</span>(A)</span><br><span class="line">    remaining = deque()</span><br><span class="line">    j = <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> num <span class="keyword">in</span> A:</span><br><span class="line">        <span class="keyword">if</span> num &gt; sorted_B[j][<span class="number">1</span>]:</span><br><span class="line">            index = sorted_B[j][<span class="number">0</span>]</span><br><span class="line">            result[index] = num</span><br><span class="line">            j += <span class="number">1</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            remaining.append(num)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(result)):</span><br><span class="line">        <span class="keyword">if</span> result[i] == -<span class="number">1</span>:</span><br><span class="line">            result[i] = remaining.popleft()</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例</span></span><br><span class="line">A = [<span class="number">2</span>, <span class="number">7</span>, <span class="number">11</span>, <span class="number">15</span>]</span><br><span class="line">B = [<span class="number">1</span>, <span class="number">10</span>, <span class="number">4</span>, <span class="number">11</span>]</span><br><span class="line">result = advantage_count(A, B)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;优势洗牌后的数组 A: <span class="subst">&#123;result&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a>五、总结</h2><p>贪心算法是一种简单而高效的算法策略，在满足贪心选择性质和最优子结构性质的问题中能够发挥很好的作用。通过合理选择贪心策略，可以在多项式时间内得到问题的近似最优解或全局最优解。但需要注意的是，不是所有问题都适合使用贪心算法，在使用时需要仔细分析问题的特性。在实际应用中，如资源分配、任务调度、图论等领域，贪心算法都有广泛的应用。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;贪心算法：原理、实现与应用实例&quot;&gt;&lt;a href=&quot;#贪心算法：原理、实现与应用实例&quot; class=&quot;headerlink&quot; title=&quot;贪心算法：原理、实现与应用实例&quot;&gt;&lt;/a&gt;贪心算法：原理、实现与应用实例&lt;/h1&gt;&lt;h2 id=&quot;一、算法描述&quot;&gt;&lt;a href=&quot;#一、算法描述&quot; class=&quot;headerlink&quot; title=&quot;一、算法描述&quot;&gt;&lt;/a&gt;一、算法描述&lt;/h2&gt;&lt;p&gt;贪心算法是一种在每一步选择中都采取在当前状态下最好或最优（即最有利）的选择，从而希望导致结果是全局最好或最优的算法。它并不从整体最优上加以考虑，所做出的仅是在某种意义上的局部最优解。&lt;/p&gt;</summary>
    
    
    
    
    <category term="algorithm" scheme="https://blog.hinsyeow.org/tags/algorithm/"/>
    
  </entry>
  
</feed>
