vault backup: 2026-06-10 11:17:10

This commit is contained in:
2026-06-10 11:17:10 +08:00
parent 89e183aaaa
commit 788576ec4b
15 changed files with 3124 additions and 0 deletions
@@ -0,0 +1,201 @@
---
tags: [嵌入式系统, 复习, ARM架构]
create time: 2026-06-10 11:11
---
# ARM处理器架构
## 概述
本文档深入讲解ARM处理器的架构设计,包括命名规则、存储体系结构、流水线、寄存器组织、CPSR位域、工作模式以及FIQ/IRQ中断机制。ARM架构是嵌入式系统考试的重点内容。
## 正文
### 1. ARM概述
ARM(Advanced RISC Machines)是一系列**处理器核心**的名称,不是一家公司的名字。ARM公司采用**IP授权**模式——设计处理器核心,将设计方案授权给芯片厂商(如高通、三星、ST)进行制造。
ARM的核心特点:
- **RISC架构**(精简指令集计算机):指令数量少,指令长度固定,执行效率高
- **低功耗**:适合移动设备和嵌入式场景
- **IP授权模式**:ARM自己不生产芯片
### 2. ARM7TDMI命名规则
以经典的ARM7TDMI为例,其后缀含义如下:
| 后缀 | 全称 | 含义 |
|------|------|------|
| T | Thumb | 支持Thumb指令集(16位) |
| D | Debug | 支持JTAG调试 |
| M | Multiplier | 增强型乘法器 |
| I | ICE | 支持嵌入式ICE(In-Circuit Emulator) |
### 3. 冯·诺依曼 vs 哈佛架构
这是嵌入式系统的经典考点:
**冯·诺依曼架构**(ARM7采用):
- 指令和数据**共享**同一条总线和同一存储空间
- 不能同时取指令和读写数据
- 结构简单,成本低
**哈佛架构**(ARM9及以上采用):
- 指令和数据使用**独立的**总线和存储空间
- 可以同时取指令和读写数据
- 性能更高,但结构更复杂
> [!question] ARM7为什么选择冯·诺依曼架构?
> ARM7面向低成本嵌入式应用,冯·诺依曼架构只需一组总线,可以减少芯片面积和引脚数量,降低功耗和成本。对于ARM7的应用场景,性能已经足够。
### 4. ARM7三级流水线
ARM7处理器使用三级流水线:
| 阶段 | 名称 | 操作 |
|------|------|------|
| 1 | Fetch(取指) | 从存储器取出指令 |
| 2 | Decode(译码) | 解析指令含义,读取寄存器 |
| 3 | Execute(执行) | 执行运算,写回结果 |
在理想情况下,三级流水线使得每个时钟周期都能完成一条指令(吞吐率为1 CPI)。但当发生分支跳转时,流水线需要清空并重新填充,导致性能损失。
> [!tip] 流水线级数与PC值的关系
> 在ARM7三级流水线中,当前执行的指令地址是PC-8(因为PC已经预取了两条指令之后的内容)。这个细节在调试时很重要。
### 5. 寄存器组织
ARM7共有**37个**32位寄存器,但在任意时刻只能访问其中一部分:
- **User/SYS模式**:可访问**17个**(R0-R15 + CPSR)
- **其他特权模式**:可访问各自的SPSR,以及部分私有寄存器
#### 特殊功能寄存器
| 寄存器 | 名称 | 功能 |
|--------|------|------|
| R13 | SP(Stack Pointer) | 栈指针,指向当前栈顶 |
| R14 | LR(Link Register) | 链接寄存器,保存子程序返回地址 |
| R15 | PC(Program Counter) | 程序计数器,指向下一条要取的指令 |
| R12 | IP(Intra-Procedure-call) | 过程调用中间暂存寄存器 |
> [!question] 为什么每个异常模式都有自己独立的R13(SP)?
> 因为每个模式有自己的运行栈。如果所有模式共享一个SP,当中断发生时可能破坏正在使用的栈数据,导致系统崩溃。
### 6. CPSR位域详解
**CPSR**(Current Program Status Register,当前程序状态寄存器)是ARM中最关键的寄存器之一:
```mermaid
graph LR
subgraph CPSR["CPSR Bit Fields"]
direction LR
N["N[31]"] --> Z["Z[30]"] --> C["C[29]"] --> V["V[28]"]
V --> D["D[27]"] --> I["I[7]"] --> F["F[6]"] --> T["T[5]"]
T --> M["M[4:0]"]
end
style N fill:#ef9a9a
style Z fill:#ffcc80
style C fill:#fff176
style V fill:#a5d6a7
style I fill:#90caf9
style F fill:#90caf9
style T fill:#ce93d8
style M fill:#f48fb1
```
**条件标志位(N, Z, C, V)**:
| 标志位 | 位号 | 含义 |
|--------|------|------|
| N | [31] | **负数**标志:运算结果最高位为1时置位 |
| Z | [30] | **零**标志:运算结果为0时置位 |
| C | [29] | **进位**标志:加法产生进位或减法无借位时置位 |
| V | [28] | **溢出**标志:有符号运算溢出时置位 |
**控制位**:
| 标志位 | 位号 | 含义 |
|--------|------|------|
| I | [7] | IRQ禁止:置1时禁止IRQ中断 |
| F | [6] | FIQ禁止:置1时禁止FIQ中断 |
| T | [5] | Thumb状态:置1表示Thumb状态,置0表示ARM状态 |
| M[4:0] | [4:0] | 处理器模式选择位 |
> [!question] 如果CPSR的N=1、Z=0,这意味着什么?
> N=1表示上一次运算结果为负数,Z=0表示结果不为零。如果此时用BMI(Branch Minus)指令,分支将被执行,因为BMI条件是N=1。
### 7. SPSR
**SPSR**(Saved Program Status Register)保存的是进入异常模式**之前**的CPSR值。
当异常发生时,硬件自动将当前CPSR保存到对应异常模式的SPSR中。异常返回时,通过 `MOVS PC, LR` 指令将SPSR恢复回CPSR,从而恢复之前的状态。
> [!warning] User模式没有SPSR
> User模式不是异常模式,所以没有SPSR。如果需要保存User模式的状态,必须通过软件方式实现。
### 8. 七种工作模式
ARM处理器有7种工作模式:
```mermaid
graph TD
subgraph nonPriv["Non-Privileged"]
USR["USR - User Mode"]
end
subgraph priv["Privileged Modes"]
SYS["SYS - System Mode"]
SVC["SVC - Supervisor Mode"]
ABT["ABT - Abort Mode"]
UND["UND - Undefined Mode"]
IRQ["IRQ - IRQ Mode"]
FIQ["FIQ - FIQ Mode"]
end
SYS -.-> |"Same registers as USR"| USR
SVC --> |"Reset enters here"| SVC
style USR fill:#bbdefb
style SYS fill:#c8e6c9
style SVC fill:#ffcc80
style ABT fill:#ffab91
style UND fill:#ef9a9a
style IRQ fill:#ce93d8
style FIQ fill:#f48fb1
```
| 模式 | 编码 | 说明 | 进入方式 |
|------|------|------|----------|
| USR | 10000 | 用户模式,非特权 | 正常程序执行 |
| SYS | 11111 | 系统模式,特权 | 与USR共享寄存器 |
| SVC | 10011 | 管理模式 | **复位**、SWI |
| ABT | 10111 | 中止模式 | 存储器访问异常 |
| UND | 11011 | 未定义模式 | 未定义指令异常 |
| IRQ | 10010 | 中断模式 | IRQ中断 |
| FIQ | 10001 | 快速中断模式 | FIQ中断 |
几个关键点:
- **复位后进入SVC模式**,这是系统上电后的初始状态
- **USR不是异常模式**,因此没有SPSR
- **SYS模式**与USR共享寄存器,但具有特权权限
### 9. FIQ vs IRQ
| 比较维度 | FIQ | IRQ |
|----------|-----|-----|
| 优先级 | **更高** | 较低 |
| 私有寄存器 | R8-R14(7个) | R13-R14(2个) |
| 向量表位置 | 0x0000001C(末尾) | 0x00000018 |
| 响应速度 | 更快(寄存器保留多,切换少) | 较慢 |
| 嵌套 | 支持 | 支持 |
FIQ更快的原因:
1. 有更多的**私有寄存器**(R8-R14),中断处理时不需要保存/恢复通用寄存器
2. 向量表位于地址空间末尾,可以直接放置ISR代码,**无需跳转**
> [!question] 为什么FIQ的向量表放在0x0000001C(最后),而不是和其他异常一样在前面?
> 把FIQ的处理程序直接放在向量表位置0x0000001C开始的地方,可以省去一次跳转指令。因为从0x0000001C到中断向量表结束还有空间,足够放几条关键指令。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,235 @@
---
tags: [嵌入式系统, 复习, ARM异常]
create time: 2026-06-10 11:11
---
# ARM异常与中断机制
## 概述
本文档详细讲解ARM处理器的异常与中断机制,包括7种异常类型、异常向量表、异常响应流程、FIQ/IRQ中断处理以及中断嵌套。这是理解嵌入式系统实时响应能力的关键。
## 正文
### 1. 什么是异常
**异常(Exception)** 是指处理器在执行正常程序过程中,遇到的需要特殊处理的事件。异常是同步事件,由当前执行的指令触发。
**中断(Interrupt)** 是一种特殊的异常,属于异步事件——由外部硬件信号触发,与当前执行的指令无关。
> [!tip] 异常与中断的关系
> 中断是异常的子集。所有中断都是异常,但不是所有异常都是中断。IRQ和FIQ是中断,而Reset、SWI、Data Abort等是其他类型的异常。
### 2. 七种异常类型
ARM7定义了7种异常类型,每种异常对应一种处理器模式:
| 异常类型 | 触发原因 | 进入模式 | 优先级 |
|----------|----------|----------|--------|
| Reset | 复位信号 | SVC | 最高(1) |
| Undefined Instruction | 无法识别的指令 | UND | 最高(2) |
| SWI(软件中断) | SWI指令执行 | SVC | 最高(3) |
| Prefetch Abort | 指令预取失败 | ABT | 最高(4) |
| Data Abort | 数据访问失败 | ABT | 最高(5) |
| IRQ | 外部IRQ中断请求 | IRQ | 较低(6) |
| FIQ | 外部FIQ中断请求 | FIQ | 最低(7) |
> [!question] 为什么Reset的优先级最高?
> Reset是最高优先级异常,因为它代表系统需要从头开始。无论处理器当前在做什么,复位信号都必须立即响应,将系统拉回到初始状态。
### 3. 异常向量表
异常向量表是位于地址 **0x00000000** 的一张跳转表,每种异常对应一个固定的偏移地址:
```mermaid
graph TD
subgraph vectorTable["Exception Vector Table"]
V0["0x00000000: Reset"] --> V4["0x00000004: Undefined Instruction"]
V4 --> V8["0x00000008: SWI"]
V8 --> VC["0x0000000C: Prefetch Abort"]
VC --> V10["0x00000010: Data Abort"]
V10 --> V14["0x00000014: Reserved"]
V14 --> V18["0x00000018: IRQ"]
V18 --> V1C["0x0000001C: FIQ"]
end
style V0 fill:#ef9a9a
style V4 fill:#ffcc80
style V8 fill:#fff176
style VC fill:#a5d6a7
style V10 fill:#90caf9
style V14 fill:#e0e0e0
style V18 fill:#ce93d8
style V1C fill:#f48fb1
```
每个向量表项只占**4个字节**(一条ARM指令的大小),通常放置一条跳转指令(B或LDR PC)跳转到对应的异常处理程序。
> [!warning] 0x00000014是保留位置
> 0x00000014是Reserved,没有定义任何异常。这是因为每种异常间隔4个字节,而从Data Abort到IRQ之间需要一个间隔。
> [!question] 为什么FIQ的向量地址是0x0000001C,排在最后?
> 把FIQ放在向量表末尾有一个巧妙的好处:FIQ的处理程序可以直接从0x0000001C开始写,而不需要一条跳转指令。因为从0x0000001C往后到向量表结束还有空间,足以放置几条关键指令,从而节省了一个跳转周期。
### 4. 异常响应流程
当异常发生时,ARM处理器自动执行以下步骤:
```mermaid
flowchart TD
A["Exception Occurs"] --> B["Step1: Save CPSR to SPSR of target mode"]
B --> C["Step2: Set CPSR mode bits to target mode"]
C --> D["Step3: Set CPSR T bit to 0 ARM state"]
D --> E["Step4: Set CPSR I/F bits disable interrupts"]
E --> F["Step5: Save return address to LR of target mode"]
F --> G["Step6: Set PC to vector address"]
G --> H["Execute exception handler"]
style A fill:#ef9a9a
style H fill:#a5d6a7
```
具体来说:
**Step 1:保存CPSR**
- 将当前CPSR的值复制到**目标异常模式的SPSR**
- 例如:IRQ发生时,CPSR → SPSR_irq
**Step 2:切换处理器模式**
- 修改CPSR的M[4:0]位,切换到异常对应的模式
**Step 3:切换到ARM状态**
- 清除CPSR的T位(T=0),确保在ARM状态下执行异常处理程序
- 这意味着即使当前在Thumb状态,异常处理也使用ARM指令
**Step 4:禁止中断**
- 根据异常类型,设置I位或F位来禁止中断
- IRQ异常设置I位(禁止IRQ),FIQ异常设置I和F位(禁止所有中断)
**Step 5:保存返回地址**
- 将程序计数器的值保存到**目标模式的LR**
- 不同异常的返回地址偏移不同(见下表)
**Step 6:跳转到向量表**
- 将PC设置为对应异常的向量地址
### 5. 返回地址的偏移
每种异常保存到LR中的返回地址有特定的偏移:
| 异常类型 | LR保存的值 | 说明 |
|----------|------------|------|
| Reset | 未定义 | 无需返回 |
| Undefined | PC + 4 | 指向未定义指令的下一条 |
| SWI | PC + 4 | 指向SWI指令的下一条 |
| Prefetch Abort | PC + 4 | 指向预取失败指令的下一条 |
| Data Abort | PC + 8 | 指向数据访问失败指令的下一条 |
| IRQ | PC + 4 | 指向被中断指令的下一条 |
| FIQ | PC + 4 | 指向被中断指令的下一条 |
> [!question] 为什么Data Abort的偏移是+8而其他是+4?
> 因为ARM7三级流水线中,当Data Abort被检测到时,流水线已经多预取了指令。Data Abort在执行阶段才被发现,此时PC已经前进了8个字节(两条指令之后)。
### 6. 异常返回
异常处理完成后,需要返回到被中断的程序。返回方式取决于异常类型:
**从IRQ/FIQ返回**:
```arm
SUBS PC, LR, #4 ; LR_irq/fiq - 4 = 被中断指令的下一条地址
```
**从SWI/Undefined返回**:
```arm
MOVS PC, LR ; LR_svc/und 直接就是返回地址
```
**从Data Abort返回**:
```arm
SUBS PC, LR, #8 ; LR_abt - 8 = 需要重新执行那条失败的指令
```
> [!warning] 关键点:MOVS和SUBS中的"S"后缀
> 返回指令必须使用带S后缀的版本(MOVS/SUBS),因为这会自动将SPSR的值恢复到CPSR。如果用MOV而不是MOVS,SPSR不会被恢复,处理器状态将不正确。
### 7. 中断嵌套
ARM7的中断**支持嵌套**。所谓嵌套,是指在一个中断处理过程中,更高优先级的中断可以打断当前处理。
```mermaid
sequenceDiagram
participant Main as Main Program
participant IRQ as IRQ Handler
participant FIQ as FIQ Handler
Main->>IRQ: IRQ interrupt occurs
Note over IRQ: CPSR saved, IRQ enabled
IRQ->>FIQ: FIQ interrupt occurs
Note over FIQ: CPSR saved, all IRQ disabled
FIQ-->>IRQ: Return from FIQ
IRQ-->>Main: Return from IRQ
```
实现中断嵌套的关键:
1. 进入中断处理程序后,需要**手动重新使能IRQ**(清除CPSR的I位)
2. 保存必要的寄存器到栈上
3. 使用STMFD/LDMFD来管理栈帧
```arm
; IRQ中断处理程序模板
IRQ_Handler:
SUB LR, LR, #4 ; 修正返回地址
STMFD SP!, {LR} ; 保存返回地址
MRS R14, SPSR ; 保存SPSR
STMFD SP!, {R14} ; 将SPSR压栈
STMFD SP!, {R0-R3, R12} ; 保存可能被破坏的寄存器
MSR CPSR_c, #0x13 ; 切换到SVC模式,使能IRQ
STMFD SP!, {LR} ; 保存SVC模式的LR
; ... 在SVC模式下处理中断 ...
LDMFD SP!, {LR} ; 恢复SVC的LR
MSR CPSR_c, #0x92 ; 切回IRQ模式,禁止IRQ
LDMFD SP!, {R0-R3, R12} ; 恢复寄存器
LDMFD SP!, {R14} ; 恢复SPSR到R14
MSR SPSR_cxsf, R14 ; 写回SPSR
LDMFD SP!, {PC}^ ; 恢复PC,同时恢复CPSR
```
### 8. FIQ vs IRQ 对比总结
```mermaid
graph TD
subgraph comparison["FIQ vs IRQ Comparison"]
direction LR
subgraph fiqProps["FIQ Properties"]
FP1["Higher priority"]
FP2["Private R8-R14"]
FP3["Vector at 0x1C"]
FP4["Fastest response"]
end
subgraph irqProps["IRQ Properties"]
IP1["Lower priority"]
IP2["Private R13-R14 only"]
IP3["Vector at 0x18"]
IP4["Needs register save"]
end
end
style FP1 fill:#f48fb1
style IP1 fill:#ce93d8
```
| 特性 | FIQ | IRQ |
|------|-----|-----|
| 优先级 | 高 | 低 |
| 私有寄存器 | R8-R14(7个) | R13-R14(2个) |
| 向量地址 | 0x0000001C | 0x00000018 |
| 响应速度 | 快(无需保存R8-R12) | 慢(需要保存R0-R12) |
| 嵌套 | 支持 | 支持 |
| 典型用途 | 高速数据传输(DMA) | 一般外部设备中断 |
> [!question] 如果系统中只有一个中断源,应该选择FIQ还是IRQ?
> 如果只有一个中断源且对响应速度有要求,建议使用FIQ。因为FIQ有更多私有寄存器,中断处理程序不需要保存/恢复R8-R12,减少了压栈/出栈的时间开销。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,310 @@
---
tags: [嵌入式系统, 复习, ARM指令集]
create time: 2026-06-10 11:11
---
# ARM指令集与汇编
## 概述
本文档详细讲解ARM指令集体系,包括ARM/Thumb指令集对比、指令格式、核心指令详解、条件码、寻址方式以及汇编程序设计。这是嵌入式系统课程中需要动手实践的重点部分。
## 正文
### 1. ARM指令集 vs Thumb指令集
| 比较维度 | ARM指令集 | Thumb指令集 |
|----------|-----------|-------------|
| 指令宽度 | **32位** | **16位** |
| 功能 | 完整功能 | ARM的子集 |
| 代码密度 | 低(占用空间大) | 高(占用空间小) |
| 性能 | 高 | 略低 |
| 状态标志 | CPSR.T = 0 | CPSR.T = 1 |
> [!warning] Thumb状态不能执行ARM指令
> 在Thumb状态下,处理器只能执行16位的Thumb指令。如果需要执行32位ARM指令,必须先切换到ARM状态(通过BX指令)。两种状态不能在同一时刻混合执行。
### 2. 指令格式
ARM指令的基本格式为:
```
opcode{condition}{S} Rd, operand1, operand2
```
| 字段 | 说明 |
|------|------|
| opcode | 操作码,如MOV, ADD, LDR |
| {condition} | 条件码,如EQ, NE, GT(可选) |
| {S} | 是否影响CPSR标志位(可选) |
| Rd | 目标寄存器 |
| operand1 | 第一操作数(通常是寄存器) |
| operand2 | 第二操作数(灵活,可带移位) |
```mermaid
graph LR
subgraph format["Instruction Format"]
direction LR
OP["opcode"] --> CON["condition"] --> S["S flag"] --> RD["Rd"] --> OP1["operand1"] --> OP2["operand2"]
end
style OP fill:#4fc3f7
style CON fill:#81d4fa
style S fill:#b3e5fc
style RD fill:#ffcc80
style OP1 fill:#ffab91
style OP2 fill:#a5d6a7
```
### 3. 桶形移位器(Barrel Shifter)
ARM指令的一个强大特性:**第二操作数可以在送入ALU之前先进行移位操作**。这意味着一条指令就能完成"移位+运算",无需额外的移位指令。
移位类型:
| 移位操作 | 全称 | 说明 |
|----------|------|------|
| LSL | Logical Shift Left | 逻辑左移,低位补0 |
| LSR | Logical Shift Right | 逻辑右移,高位补0 |
| ASR | Arithmetic Shift Right | 算术右移,高位补符号位 |
| ROR | Rotate Right | 循环右移 |
```arm
; 示例:利用桶形移位器
ADD R0, R1, R1, LSL #2 ; R0 = R1 + R1 * 4 = R1 * 5
SUB R2, R3, R3, LSR #1 ; R2 = R3 - R3/2 = R3/2
MOV R4, R5, ROR #8 ; R4 = R5循环右移8位
```
> [!tip] 桶形移位器的价值
> 在没有桶形移位器的架构中,`R1 * 5` 需要两条指令:先LSL #2再ADD。ARM用一条指令就完成了,这体现了RISC架构中"简单指令的巧妙组合"的设计哲学。
### 4. 核心指令详解
#### 4.1 数据传送指令 MOV
```arm
MOV R0, #10 ; R0 = 10(立即数传送)
MOV R1, R0 ; R1 = R0(寄存器传送)
MOV R2, R0, LSL #3 ; R2 = R0 << 3(带移位传送)
```
**MOV只能在寄存器之间传送数据,不能访问内存!** 这是与LDR的关键区别。
#### 4.2 加载/存储指令 LDR/STR
```arm
LDR R0, [R1] ; 从R1指向的内存地址加载数据到R0
STR R0, [R1] ; 将R0的值存储到R1指向的内存地址
LDR R0, [R1, #4] ; 从R1+4地址加载(基址+偏移)
STR R0, [R1], #4 ; 先存储,再更新R1 = R1 + 4(后索引)
LDR R0, [R1, #4]! ; 先更新R1 = R1 + 4,再加载(前索引)
```
> [!question] MOV R0, #0xFF 和 LDR R0, =0x12345678 有什么区别?
> `MOV R0, #0xFF` 是真正ARM指令,0xFF是合法的8位立即数(通过循环右移编码)。
> `LDR R0, =0x12345678` 是**伪指令**,汇编器会将0x12345678放入文字池(literal pool),然后用一条LDR从该地址加载。因为0x12345678无法编码为8位立即数。
#### 4.3 算术运算指令
```arm
ADD R0, R1, R2 ; R0 = R1 + R2
ADD R0, R1, #5 ; R0 = R1 + 5
ADDS R0, R1, R2 ; R0 = R1 + R2,并更新CPSR标志位
SUB R0, R1, R2 ; R0 = R1 - R2
SUBS R0, R1, #1 ; R0 = R1 - 1,更新标志位
MUL R0, R1, R2 ; R0 = R1 * R2
```
注意带`S`后缀的指令会更新CPSR的N、Z、C、V标志位。
#### 4.4 比较指令 CMP
```arm
CMP R0, R1 ; 比较R0和R1(计算R0-R1,但不保存结果)
BEQ label ; 如果相等(Z=1),跳转到label
BNE label ; 如果不相等(Z=0),跳转到label
```
CMP本质上是一条**不保存结果的减法指令**,只设置标志位。
#### 4.5 分支指令 B / BL
```arm
B label ; 无条件跳转到label
BL function ; 跳转到function,同时将返回地址保存到LR
```
- **B**:简单跳转,用于循环、条件分支
- **BL**:带链接的跳转,用于**函数调用**(LR自动保存返回地址)
#### 4.6 软中断指令 SWI
```arm
SWI #0x123456 ; 触发软中断,进入SVC模式
```
SWI用于**从用户模式请求操作系统服务**(系统调用)。执行SWI后:
1. 进入SVC模式
2. CPSR保存到SPSR_svc
3. 返回地址保存到LR_svc
4. PC跳转到向量地址0x00000008
#### 4.7 批量加载/存储 STMFD / LDMFD
```arm
STMFD SP!, {R0-R3, LR} ; 将R0-R3和LR入栈(满递减栈)
LDMFD SP!, {R0-R3, PC} ; 从栈中恢复R0-R3,并将PC出栈实现返回
```
这两个指令常用于**函数入口保存现场**和**函数出口恢复现场**。FD表示Full Descending(满递减栈),是ARM的默认栈类型。
#### 4.8 状态寄存器读写 MRS / MSR
```arm
MRS R0, CPSR ; 将CPSR读入R0
MSR CPSR_c, R0 ; 将R0写入CPSR的控制位域
MSR CPSR_f, R0 ; 将R0写入CPSR的标志位域
```
MRS/MSR用于在**特权模式下修改CPSR**,例如开关中断:
```arm
; 关中断
MRS R0, CPSR
ORR R0, R0, #0x80 ; I位(bit7)置1
MSR CPSR_c, R0
; 开中断
MRS R0, R1
BIC R0, R0, #0x80 ; I位(bit7)清0
MSR CPSR_c, R0
```
#### 4.9 测试等价指令 TEQ
```arm
TEQ R0, R1 ; 按位异或(EOR),只设置标志位,不保存结果
```
TEQ用于测试两个值是否相等(或测试某些位),与CMP类似但使用异或运算。
### 5. 条件码
ARM支持15种条件码,基于CPSR的标志位进行判断:
| 条件码 | 含义 | 标志位条件 |
|--------|------|------------|
| EQ | 相等 | Z=1 |
| NE | 不相等 | Z=0 |
| GT | 大于(有符号) | Z=0且N=V |
| LT | 小于(有符号) | N!=V |
| GE | 大于等于(有符号) | N=V |
| LE | 小于等于(有符号) | Z=1或N!=V |
| HI | 无符号大于 | C=1且Z=0 |
| LS | 无符号小于等于 | C=0或Z=1 |
| CS/HS | 无符号大于等于 | C=1 |
| CC/LO | 无符号小于 | C=0 |
| PL | 正数或零 | N=0 |
| MI | 负数 | N=1 |
| VS | 溢出 | V=1 |
| VC | 无溢出 | V=0 |
| AL | 无条件(默认) | 任意 |
> [!question] CMP R0, #0 后接 BMI label,什么时候会跳转?
> BMI是"负数"条件跳转。CMP R0, #0 实际上是R0 - 0,结果就是R0本身。如果R0 < 0(最高位为1),N标志位被置位,BMI条件成立,跳转到label。
### 6. 寻址方式
| 寻址方式 | 示例 | 说明 |
|----------|------|------|
| 立即数寻址 | `MOV R0, #10` | 操作数就在指令中 |
| 寄存器寻址 | `ADD R0, R1, R2` | 操作数在寄存器中 |
| 寄存器间接寻址 | `LDR R0, [R1]` | R1中存放的是内存地址 |
| 基址+偏移寻址 | `LDR R0, [R1, #4]` | R1为基地址,4为偏移量 |
| 基址+索引寻址 | `LDR R0, [R1, R2]` | R2的值作为索引偏移 |
### 7. 立即数编码
ARM指令是32位固定的,立即数编码只占用**12位**(4位旋转量 + 8位立即数值)。规则:
```
实际值 = 8位立即数 循环右移 (旋转量 * 2) 位
```
这意味着只有**特定的值**才能作为立即数。例如:
- `#0xFF`:合法,8位全1,旋转0位
- `#0x100`:合法,`#1`循环右移24位
- `#0x12345678`:**不合法**,无法用8位循环右移表示
> [!tip] 立即数合法性判断
> 判断一个数是否是合法的ARM立即数:将它写成二进制,看是否能通过循环右移偶数位变成8位以内的数。
### 8. 字对齐与大小端
**数据宽度**:
| 类型 | 位数 | 字节数 |
|------|------|--------|
| Word(字) | 32位 | 4字节 |
| Halfword(半字) | 16位 | 2字节 |
| Byte(字节) | 8位 | 1字节 |
**对齐要求**:Word访问的地址必须是4的倍数(地址 mod 4 = 0),Halfword访问地址必须是2的倍数。
**大小端**:
- **Little-Endian(小端)**:低字节存低地址(ARM常用)
- **Big-Endian(大端)**:高字节存低地址
```arm
; 假设内存地址0x1000存放值 0x12345678
; 小端模式:
; 0x1000: 0x78 (最低字节)
; 0x1001: 0x56
; 0x1002: 0x34
; 0x1003: 0x12 (最高字节)
; 大端模式:
; 0x1000: 0x12 (最高字节)
; 0x1001: 0x34
; 0x1002: 0x56
; 0x1003: 0x78 (最低字节)
```
### 9. 完整示例:C if-else 对应的ARM汇编
以下是C语言if-else语句及其对应的ARM汇编实现:
```c
// C代码
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
```
```arm
; ARM汇编实现
max:
CMP R0, R1 ; 比较a和b(R0-R1,设置标志位)
MOVGT R0, R0 ; 如果a > b(GT条件成立),R0保持不变
MOVLE R0, R1 ; 如果a <= b(LE条件成立),R0 = b
BX LR ; 返回,LR中保存了调用者的返回地址
```
更高效的写法:
```arm
; 优化版本:只用两条核心指令
max:
CMP R0, R1 ; 比较a和b
MOVLT R0, R1 ; 如果a < b,则R0 = b
BX LR ; 返回
```
> [!question] 为什么优化版本只判断了LT一种情况?
> 因为如果a >= b,R0的值本来就是a,不需要任何操作。只有当a < b时才需要将b写入R0。这体现了"条件执行"的强大——ARM的每条指令都可以条件执行,避免了不必要的跳转。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,336 @@
---
tags: [嵌入式系统, 复习, Android开发]
create time: 2026-06-10 11:11
---
# Android应用开发基础
## 概述
本文档覆盖Android应用开发的核心知识,包括分层架构、四大组件、Activity生命周期、数据存储、UI布局、权限管理和常用设计模式。这是嵌入式系统课程中移动终端开发部分的复习重点。
## 正文
### 1. Android分层架构
Android系统采用分层架构设计,从底层到顶层分为四层:
```mermaid
graph TD
A["Applications"] --> B["Application Framework"]
B --> C["Libraries / Android Runtime"]
C --> D["Linux Kernel"]
style A fill:#4fc3f7
style B fill:#81d4fa
style C fill:#b3e5fc
style D fill:#ffcc80
```
| 层级 | 组成 | 说明 |
|------|------|------|
| **Applications** | 短信、电话、浏览器、第三方App | 用户直接使用的应用 |
| **Application Framework** | Activity Manager, Content Provider, Resource Manager, Notification Manager | 提供构建应用的基础API |
| **Libraries / Runtime** | SQLite, OpenGL, WebKit, Media Framework, Dalvik/ART | 系统库和运行时环境 |
| **Linux Kernel** | 驱动、内存管理、进程管理、网络栈 | 底层硬件抽象和系统服务 |
> [!tip] Dalvik vs ART
> - **Dalvik**:Android 4.4之前使用,基于JIT(Just-In-Time)编译
> - **ART**:Android 5.0开始替代Dalvik,使用AOT(Ahead-Of-Time)编译,运行更快但安装更慢
### 2. 四大组件
Android应用由四大核心组件构成:
#### 2.1 Activity(活动)
Activity代表一个**用户界面屏幕**,是用户与应用交互的主要入口。
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
```
#### 2.2 Service(服务)
Service在**后台**运行,没有用户界面。适合执行长时间运行的操作(如音乐播放、文件下载)。
两种启动方式:
- **Started Service**:通过startService()启动,独立运行
- **Bound Service**:通过bindService()绑定,提供客户端-服务器接口
#### 2.3 ContentProvider(内容提供者)
ContentProvider用于**跨应用数据共享**。它封装了数据访问接口,其他应用通过ContentResolver来查询和修改数据。
```java
// 在AndroidManifest.xml中注册
<provider
android:name=".MyContentProvider"
android:authorities="com.example.myprovider"
android:exported="true" />
```
> [!question] 为什么需要ContentProvider而不是直接共享数据库?
> ContentProvider提供了统一的数据访问接口和权限控制机制。直接共享数据库会暴露数据结构细节,且难以控制访问权限。
#### 2.4 BroadcastReceiver(广播接收器)
BroadcastReceiver用于接收和响应**系统或应用广播**,实现组件间的松耦合通信。
配置方式有两种:
**XML静态注册**:
```xml
<receiver android:name=".MyReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
```
**代码动态注册**:
```java
IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
registerReceiver(new MyReceiver(), filter);
```
### 3. Activity生命周期
Activity的生命周期是Android开发的基础知识点:
```mermaid
stateDiagram-v2
[*] --> Created
Created --> Started: onCreate -> onStart
Started --> Running: onResume
Running --> Paused: onPause
Paused --> Running: onResume
Paused --> Stopped: onStop
Stopped --> Started: onRestart -> onStart
Stopped --> Created: onDestroy
Running --> Stopped: onStop
Paused --> Created: onDestroy
Created --> [*]: onDestroy
note right of Created: Activity created
note right of Started: Visible but not focused
note right of Running: Foreground, interactive
note right of Paused: Partially obscured
note right of Stopped: Fully hidden
```
| 生命周期方法 | 调用时机 | 说明 |
|-------------|----------|------|
| onCreate() | Activity首次创建 | 初始化界面、绑定数据 |
| onStart() | Activity变为可见 | 准备显示 |
| onResume() | Activity获得焦点 | 可与用户交互 |
| onPause() | Activity失去焦点 | 保存临时数据(秒级) |
| onStop() | Activity完全不可见 | 释放资源 |
| onDestroy() | Activity被销毁 | 最终清理 |
| onRestart() | 从Stopped重新启动 | 不会直接到onCreate |
> [!warning] onPause()中不要做耗时操作
> onPause()执行完毕后,新的Activity才会启动。如果在onPause()中做网络请求或大量数据处理,会导致界面切换卡顿。耗时操作应该放到onStop()或异步任务中。
### 4. Intent数据传递
Intent是Android中组件间通信的核心机制,用于启动Activity、Service和发送广播。
**传递数据的两种方式**:
**Serializable**(Java标准序列化):
```java
// 发送端
Intent intent = new Intent(this, TargetActivity.class);
intent.putExtra("user", (Serializable) userObj);
startActivity(intent);
// 接收端
User user = (User) getIntent().getSerializableExtra("user");
```
**Parcelable**(Android特有序列化,性能更好):
```java
// 发送端
intent.putExtra("user", (Parcelable) userObj);
// 接收端
User user = getIntent().getParcelableExtra("user");
```
| 比较 | Serializable | Parcelable |
|------|-------------|------------|
| 性能 | 慢(使用反射) | 快(手动实现) |
| 实现 | 简单(实现接口即可) | 复杂(需实现writeToParcel) |
| 用途 | 临时存储、简单对象 | Activity间频繁传递 |
| 开销 | 产生大量临时对象 | 内存开销小 |
### 5. startActivityForResult
当一个Activity需要从另一个Activity**获取返回数据**时,使用startActivityForResult:
```java
// 启动方
Intent intent = new Intent(this, SecondActivity.class);
startActivityForResult(intent, REQUEST_CODE);
// 接收返回数据
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
String result = data.getStringExtra("result");
// 处理返回数据
}
}
// 被启动方设置返回数据
Intent resultIntent = new Intent();
resultIntent.putExtra("result", "hello from second");
setResult(RESULT_OK, resultIntent);
finish();
```
### 6. SQLite数据类型
Android内置SQLite数据库,支持以下5种数据类型:
| 数据类型 | 说明 |
|----------|------|
| NULL | 空值 |
| INTEGER | 整数 |
| REAL | 浮点数 |
| TEXT | 文本字符串 |
| BLOB | 二进制大对象 |
> [!warning] SQLite没有VARCHAR类型
> SQLite使用动态类型系统,TEXT类型可以存储任意长度的字符串,不需要指定VARCHAR(n)。但创建表时写VARCHAR也是合法的(会被忽略长度约束)。
### 7. UI布局
Android支持多种布局方式:
| 布局类型 | 特点 | 适用场景 |
|----------|------|----------|
| **LinearLayout** | 线性排列(水平/垂直) | 简单的行列排列 |
| **RelativeLayout** | 相对定位 | 复杂的相对位置关系 |
| **FrameLayout** | 层叠排列,后面的元素覆盖前面 | 重叠界面、帧动画 |
**SimpleAdapter**用于为ListView等列表控件提供数据:
```java
// 数据源:List<Map<String, Object>>
List<Map<String, Object>> data = new ArrayList<>();
Map<String, Object> item = new HashMap<>();
item.put("title", "标题");
item.put("image", R.drawable.icon);
data.add(item);
// 创建适配器
SimpleAdapter adapter = new SimpleAdapter(
this,
data,
R.layout.list_item,
new String[]{"title", "image"}, // 数据键
new int[]{R.id.tvTitle, R.id.ivIcon} // 视图ID
);
listView.setAdapter(adapter);
```
> [!question] SimpleAdapter的数据映射List&lt;Map&gt;中,Map的key和value分别对应什么?
> key是字符串类型的字段名(如"title"),value是该字段的值(如"标题"文本)。适配器通过key从Map中取值,然后绑定到对应的视图控件上。
### 8. 权限管理
Android 7.0将权限分为四种保护级别:
| 权限级别 | 说明 | 安装行为 |
|----------|------|----------|
| **normal** | 低风险权限(如网络访问) | 自动授予 |
| **dangerous** | 高风险权限(如摄像头、位置) | 需要用户确认 |
| **signature** | 只有与应用签名相同的应用才能获得 | 自动授予 |
| **signatureOrSystem** | 签名相同或系统应用才能获得 | 自动授予 |
权限声明方式(AndroidManifest.xml):
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```
> [!warning] Android 6.0+运行时权限
> 从Android 6.0开始,dangerous权限需要在运行时动态申请,而不仅仅在Manifest中声明。但signature和normal权限仍然在安装时自动授予。
### 9. SharedPreferences
SharedPreferences是Android提供的轻量级键值对存储机制:
```java
// 保存数据
SharedPreferences prefs = getSharedPreferences("mydata", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("username", "admin");
editor.putInt("age", 25);
editor.apply(); // 异步写入
// 读取数据
String name = prefs.getString("username", "default");
int age = prefs.getInt("age", 0);
```
> [!warning] 默认不能跨应用共享
> MODE_PRIVATE表示该SharedPreferences文件只有当前应用可以访问。如果需要跨应用共享数据,应该使用ContentProvider。
### 10. res vs assets
| 比较 | res | assets |
|------|-----|--------|
| 引用方式 | 通过R.java自动生成的ID | 通过AssetManager按文件名访问 |
| 子目录 | 有结构化子目录(drawable, layout等) | 无结构,可自由组织 |
| 自动优化 | 编译时会被压缩/优化 | 原样保留 |
| 文件名限制 | 只能用小写字母、数字、下划线 | 无限制 |
```java
// res资源引用
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.imageView);
iv.setImageResource(R.drawable.icon);
// assets资源访问
InputStream is = getAssets().open("data/config.json");
```
### 11. 单例模式
单例模式确保一个类只有一个实例:
```java
public class DatabaseHelper {
private static volatile DatabaseHelper instance;
// 私有构造函数,防止外部实例化
private DatabaseHelper() {
// 初始化数据库
}
public static DatabaseHelper getInstance() {
if (instance == null) {
synchronized (DatabaseHelper.class) {
if (instance == null) {
instance = new DatabaseHelper();
}
}
}
return instance;
}
}
```
> [!tip] 双重检查锁定(Double-Checked Locking)
> 单例模式中使用volatile + synchronized实现线程安全的懒加载。volatile防止指令重排序,synchronized保证只创建一次实例。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
+39
View File
@@ -0,0 +1,39 @@
---
tags:
- 嵌入式系统
- 复习
- 索引
create time: 2026-06-10 11:11
---
# 嵌入式系统复习文档索引
## 概述
本文档为「嵌入式系统」及「嵌入式系统设计与开发」课程复习材料索引。内容基于历年试题册高频考点梳理,涵盖基础概念、ARM 架构、指令集、异常机制、操作系统及 Android 开发等核心板块。
> [!tip] 使用建议
> 建议按照从基础到进阶的顺序阅读。先掌握 [[嵌入式系统基础]] 和 [[ARM处理器架构]],再深入 [[ARM指令集与汇编]] 与 [[ARM异常与中断机制]],最后学习 [[嵌入式操作系统]] 和 [[Android应用开发基础]]。
## 复习文档目录
### 嵌入式系统(核心)
| 序号 | 文档 | 核心考点 |
|:----:|------|----------|
| 1 | [[嵌入式系统基础]] | 定义、特征、最小系统、OS 对比、应用领域 |
| 2 | [[ARM处理器架构]] | 寄存器组织、CPSR 位域、7 种工作模式、流水线 |
| 3 | [[ARM指令集与汇编]] | ARM vs Thumb、指令格式、桶形移位器、寻址方式 |
| 4 | [[ARM异常与中断机制]] | 7 种异常、向量表、响应流程、中断嵌套 |
| 5 | [[嵌入式操作系统]] | 抢占式/协作式调度、uC/OS-II 任务管理 |
### 嵌入式系统设计与开发(拓展)
| 序号 | 文档 | 核心考点 |
|:----:|------|----------|
| 6 | [[Android应用开发基础]] | 四大组件、Activity 生命周期、Intent、SQLite |
| 7 | [[嵌入式系统开发与前沿趋势]] | 交叉开发环境、RISC-V、AI+嵌入式、5G+IoT |
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,216 @@
---
tags: [嵌入式系统, 复习, RTOS, uC/OS-II]
create time: 2026-06-10 11:11
---
# 嵌入式操作系统
## 概述
本文档讲解嵌入式操作系统的核心概念,重点覆盖RTOS调度策略、uC/OS-II的特性与API、任务管理机制,以及嵌入式Linux和WinCE的对比。RTOS是嵌入式系统考试中与ARM架构并列的重点。
## 正文
### 1. 嵌入式操作系统概述
嵌入式操作系统是运行在嵌入式硬件上的系统软件,负责管理硬件资源、提供任务调度和服务接口。与通用操作系统相比,它更注重**实时性**、**可裁剪性**和**可靠性**。
嵌入式操作系统的分类:
| 分类 | 特点 | 典型代表 |
|------|------|----------|
| 嵌入式RTOS | 严格实时,微内核,可裁剪 | FreeRTOS, uC/OS-II, RT-Thread |
| 嵌入式Linux | 功能丰富,开源,非严格实时 | Android, OpenWrt, Yocto |
| 商用嵌入式OS | 商业授权,完整生态 | WinCE, VxWorks, QNX |
### 2. 抢占式 vs 协作式调度
这是RTOS最核心的概念之一:
**抢占式调度(Preemptive)**:
- 高优先级任务**立即**获得CPU控制权
- 无论当前任务在做什么,都会被中断
- **实时性好**,响应时间可预测
- uC/OS-II、FreeRTOS均采用此方式
**协作式调度(Cooperative)**:
- 任务必须**主动**放弃CPU(调用yield或等待事件)
- 实现简单,但一个任务卡死会影响整个系统
- 早期Windows 3.1、部分简单RTOS使用
```mermaid
graph TD
subgraph preemptive["Preemptive Scheduling"]
direction TB
PA1["Task A running"] --> |"Higher priority Task B becomes ready"| PB1["Task B preempts A"]
PB1 --> |"Task B finishes"| PA2["Task A resumes"]
end
subgraph cooperative["Cooperative Scheduling"]
direction TB
CA1["Task A running"] --> |"Task A calls yield or blocks"| CB1["Task B runs"]
CB1 --> |"Task B yields"| CA2["Task A resumes"]
end
style PA1 fill:#90caf9
style PB1 fill:#ef9a9a
style PA2 fill:#90caf9
style CA1 fill:#a5d6a7
style CB1 fill:#ffcc80
style CA2 fill:#a5d6a7
```
> [!question] 为什么嵌入式系统通常选择抢占式调度?
> 嵌入式系统经常需要处理紧急事件(如传感器报警、安全检测),如果采用协作式调度,低优先级任务长时间不让出CPU会导致高优先级任务无法及时响应,可能造成安全事故。
### 3. RTOS实时性指标
衡量一个RTOS的实时性,主要看两个指标:
1. **调度器执行时间**:调度算法本身运行所需的时间。越短越好。
2. **任务切换时间**:从一个任务切换到另一个任务所需的时间(保存上下文 + 恢复上下文 + 刷新流水线)。
> [!tip] 最坏情况下的响应时间
> 对于硬实时系统,关注的是**最坏情况**下的响应时间,而不是平均响应时间。最坏响应时间 = 最长中断屏蔽时间 + 最高优先级任务执行时间。
### 4. uC/OS-II概述
uC/OS-II是一个经典的**抢占式实时内核**,主要特点:
- **抢占式**基于优先级的调度
- **可裁剪**:通过条件编译去掉不需要的功能
- **可移植**:支持多种处理器架构
- **确定性**:服务时间可预测
- **商业软件**:非开源,需要购买授权
- **支持64个任务**(优先级0-63,0最高,63最低)
> [!warning] uC/OS-II不是免费的
> uC/OS-II是商业软件,与FreeRTOS(免费开源)不同。学习和评估可以使用,但商业应用需要购买许可证。
### 5. 任务管理API
uC/OS-II提供了完善的任务管理接口:
#### 创建与删除
```c
// 创建任务
INT8U OSTaskCreate(
void (*task)(void *), // 任务函数指针
void *pdata, // 传递给任务的参数
OS_STK *ptos, // 任务栈顶指针
INT8U prio // 任务优先级
);
// 删除任务
INT8U OSTaskDel(INT8U prio); // 删除指定优先级的任务
INT8U OSTaskDelReq(INT8U prio); // 请求其他任务删除自己
```
#### 挂起与恢复
```c
// 挂起任务(暂停执行)
INT8U OSTaskSuspend(INT8U prio);
// 恢复被挂起的任务
INT8U OSTaskResume(INT8U prio);
```
#### 延时
```c
// 延时指定时钟节拍数
void OSTimeDly(INT32U ticks);
// 精确延时(时、分、秒)
void OSTimeDlyHMSM(INT8U hours, INT8U minutes,
INT8U seconds, INT16U ms);
```
#### 优先级修改
```c
// 动态修改任务优先级
INT8U OSTaskChangePrio(INT8U oldprio, INT8U newprio);
```
### 6. 任务状态
uC/OS-II中任务有5种状态:
```mermaid
stateDiagram-v2
[*] --> Dormant: Task Created
Dormant --> Ready: OSTaskCreate
Ready --> Running: Scheduler Dispatch
Running --> Ready: Preempted by higher priority
Running --> Waiting: OSTimeDly or Event Wait
Waiting --> Ready: Delay expires or Event occurs
Running --> Suspended: OSTaskSuspend
Suspended --> Ready: OSTaskResume
Running --> Dormant: OSTaskDel
Dormant --> [*]: Deleted
```
| 状态 | 说明 | 进入条件 |
|------|------|----------|
| **Dormant**(休眠) | 任务代码存在内存中但未被调度器管理 | OSTaskDel后 |
| **Ready**(就绪) | 任务准备好运行,等待CPU | 创建后、事件到达后 |
| **Running**(运行) | 任务正在使用CPU执行 | 被调度器选中 |
| **Waiting**(等待) | 任务等待某个事件(延时、信号量等) | 调用延时或等待函数 |
| **Suspended**(挂起) | 任务被显式挂起,不再参与调度 | 调用OSTaskSuspend |
> [!question] Ready和Waiting有什么区别?
> Ready状态的任务随时可以运行,只等调度器分配CPU;Waiting状态的任务即使给它CPU也无法运行,因为它在等待某个外部事件(如延时到期、信号量释放等)。
### 7. 任务优先级分配原则
- **每个任务必须有唯一的优先级**(uC/OS-II中不允许同优先级任务)
- **数值越小,优先级越高**(0最高,63最低)
- **关键任务分配高优先级**:如安全检测、紧急控制
- **不同优先级之间可以抢占**
- **相同优先级的处理**:在uC/OS-II中不支持,但在FreeRTOS中支持时间片轮转
分配策略示例:
| 任务 | 优先级 | 理由 |
|------|--------|------|
| 安全监控 | 0-2 | 最高优先级,必须立即响应 |
| 电机控制 | 3-5 | 实时性要求高 |
| 传感器采集 | 6-10 | 周期性任务 |
| 通信处理 | 11-15 | 重要但允许一定延迟 |
| UI显示 | 20-30 | 实时性要求低 |
### 8. 嵌入式Linux
嵌入式Linux基于标准Linux内核进行裁剪和定制:
- **开源免费**:遵循GPL协议
- **功能丰富**:网络协议栈、文件系统、设备驱动完善
- **非严格意义上的RTOS**:Linux本身不是实时内核,但通过PREEMPT_RT补丁可以实现软实时
- **适合场景**:需要复杂网络功能、多媒体处理的嵌入式设备
与RTOS的对比:
| 比较维度 | 嵌入式Linux | RTOS (uC/OS-II) |
|----------|-------------|------------------|
| 实时性 | 软实时(非严格) | 硬实时 |
| 内核大小 | MB级 | KB级 |
| 开源 | 是 | 否(商业) |
| 功能 | 丰富 | 精简 |
| 适用场景 | 复杂嵌入式设备 | 资源受限的实时系统 |
### 9. WinCE
Windows CE(简称WinCE)是微软推出的嵌入式操作系统:
- **非开源**,商业授权
- 提供完整的GUI和开发工具(Visual Studio集成)
- 适合需要Windows风格界面的手持设备
- 已逐渐被Windows IoT Core取代
> [!question] 在资源极度受限的MCU(如Cortex-M0, 2KB RAM)上,应该选择什么OS?
> 这种情况下应该选择轻量级RTOS如FreeRTOS或直接裸机编程。嵌入式Linux至少需要几MB内存,uC/OS-II虽然裁剪后很小但也需要几十KB,而FreeRTOS最小仅需几百字节RAM。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,147 @@
---
tags: [嵌入式系统, 复习, 嵌入式基础]
create time: 2026-06-10 11:11
---
# 嵌入式系统基础
## 概述
本文档覆盖嵌入式系统的核心概念,包括定义、特征、硬件组成、操作系统对比以及应用领域。这是整个嵌入式系统课程的基石,理解这些内容是后续学习ARM架构、指令集和操作系统的基础。
## 正文
### 1. 嵌入式系统的定义
**嵌入式系统**(Embedded System)的标准定义为:
> 以**应用为中心**,以**计算机技术**为基础,软硬件**可裁剪**,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的**专用计算机系统**。
理解这个定义,需要抓住几个关键词:
| 关键词 | 含义 |
|--------|------|
| 以应用为中心 | 不是通用计算,而是为特定任务而设计 |
| 软硬件可裁剪 | 根据需求定制,去掉不需要的功能以节省资源 |
| 专用计算机系统 | 本质上仍是计算机,但专用于特定场景 |
> [!question] 为什么说嵌入式系统"不等于单片机"?
> 单片机只是嵌入式系统的硬件平台之一。嵌入式系统是一个完整的系统概念,包含硬件、驱动、操作系统和应用软件。
### 2. 嵌入式系统的核心特征
1. **专用性** —— 面向特定应用,功能明确
2. **实时性** —— 必须在规定时间内响应外部事件(硬实时/软实时)
3. **可靠性** —— 往往运行在无人值守或关键任务环境
4. **资源受限** —— CPU、内存、存储空间有限
5. **软硬件协同设计** —— 硬件和软件需同步考虑和优化
6. **低功耗** —— 很多场景依赖电池供电
> [!tip] 硬实时 vs 软实时
> - **硬实时**:必须在截止时间前完成,否则造成灾难性后果(如汽车ABS制动系统)
> - **软实时**:允许偶尔超时(如视频播放的轻微卡顿)
### 3. 嵌入式系统的最小系统组成
一个可工作的嵌入式最小系统需要以下组件:
- **处理器(MCU/MPU)**:系统的大脑
- **存储器**:ROM/Flash(存放程序)+ RAM(运行时数据)
- **时钟电路**:为处理器提供工作节拍
- **电源电路**:提供稳定的工作电压
- **复位电路**:确保系统从已知状态启动
- **JTAG调试接口**:用于程序烧写和在线调试
> [!question] 为什么嵌入式系统通常使用Flash而不是硬盘来存储程序?
> Flash是非易失性存储,没有机械部件,抗震性好,功耗低,访问速度快,非常适合嵌入式场景。
### 4. 嵌入式操作系统 vs 通用操作系统
| 比较维度 | 嵌入式OS | 通用OS |
|----------|----------|--------|
| 设计目标 | 面向特定应用 | 面向多种应用 |
| 体积 | 可裁剪,极小 | 功能全面,体积大 |
| 实时性 | 要求高(RTOS) | 不强调实时 |
| 资源需求 | 低(KB~MB级) | 高(GB级) |
| 用户界面 | 简单或无 | 复杂GUI |
| 典型代表 | FreeRTOS, uC/OS-II, RT-Thread | Windows, Linux, macOS |
**嵌入式实时操作系统(RTOS)**的核心指标:
- **调度器执行时间**:调度算法本身的耗时
- **任务切换时间**:从一个任务切换到另一个任务的耗时
### 5. 嵌入式系统的应用领域
嵌入式系统几乎无处不在:
- **消费电子**:智能手机、平板电脑、可穿戴设备
- **汽车电子**:发动机控制、ABS、车载娱乐系统
- **智能家居**:智能音箱、智能门锁、家电控制
- **工业控制**:PLC、工业机器人、自动化产线
- **医疗设备**:心脏起搏器、监护仪、输液泵
- **航空航天**:飞行控制、导航系统
### 6. 智能移动终端带来的变革
智能移动终端(以智能手机为代表)带来了深刻的变革:
- **交互方式**:从键盘鼠标 → 触摸屏、语音、手势
- **计算模式**:从集中式 → 云端+终端协同
- **信息获取**:从固定网络 → 随时随地移动获取
- **生态系统**:从封闭系统 → 开放应用商店生态
### 7. 嵌入式系统架构分层
嵌入式系统的软件架构通常分为以下层次:
```mermaid
graph TD
A["Application Layer"] --> B["Middleware"]
B --> C["RTOS / OS"]
C --> D["BSP / Driver"]
D --> E["Hardware"]
style A fill:#4fc3f7
style B fill:#81d4fa
style C fill:#b3e5fc
style D fill:#e1f5fe
style E fill:#fff9c4
```
- **Hardware**:处理器、存储器、外设
- **BSP/Driver**:板级支持包和设备驱动,屏蔽硬件差异
- **RTOS/OS**:提供任务调度、内存管理、同步机制
- **Middleware**:网络协议栈、文件系统、数据库等通用服务
- **Application**:面向用户的具体应用
### 8. 嵌入式系统的产品形态
```mermaid
graph LR
subgraph forms["Product Forms"]
direction TB
F1["Single Chip MCU"]
F2["SOM Module"]
F3["SBC Board"]
F4["Full System"]
end
F1 --> F2
F2 --> F3
F3 --> F4
style F1 fill:#ffcc80
style F2 fill:#ffab91
style F3 fill:#ef9a9a
style F4 fill:#f48fb1
```
- **单芯片MCU**:所有功能集成在一颗芯片上(如STM32)
- **SOM模块**:核心板模块,含处理器和基础电路
- **SBC单板计算机**:完整的单板系统(如树莓派)
- **完整系统**:包含外壳、电源、外设的完整产品
> [!question] 嵌入式系统的"软硬件协同设计"具体是什么意思?
> 传统的设计流程是先设计硬件再写软件,但嵌入式系统需要在设计初期就同时考虑软硬件。例如,如果软件算法可以补偿硬件精度不足,就可以选用更便宜的传感器,从而降低成本和功耗。
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,285 @@
---
tags: [嵌入式系统, 复习, 开发环境, 前沿技术]
create time: 2026-06-10 11:11
---
# 嵌入式系统开发与前沿趋势
## 概述
本文档涵盖嵌入式系统的开发环境与流程、BootLoader、GPIO配置、存储技术、RISC-V架构、国产替代分析,以及AI+嵌入式、TinyML、5G+IoT等前沿技术趋势。既覆盖传统开发基础,也关注行业发展方向。
## 正文
### 1. 交叉开发环境
嵌入式开发与普通PC开发的最大区别在于:**开发在宿主机上进行,运行在目标板上**。这种开发方式称为交叉开发。
```mermaid
graph LR
subgraph devEnv["Cross-Development Environment"]
direction TB
subgraph hw["Hardware"]
HB["Host PC"]
JTAG["JTAG Debugger"]
TB["Target Board"]
end
subgraph sw["Software"]
TC["Cross-Compiler Toolchain"]
IDE["IDE (Keil/IAR)"]
DBG["Debugger"]
RTOS["OS / RTOS"]
end
end
HB --> |"Compile Code"| TC
TC --> |"Generate Binary"| TB
JTAG --> |"Debug & Flash"| TB
IDE --> TC
IDE --> DBG
style HB fill:#90caf9
style JTAG fill:#ffcc80
style TB fill:#a5d6a7
style TC fill:#ce93d8
```
#### 硬件部分
| 组件 | 作用 |
|------|------|
| **目标板(Target Board)** | 运行最终程序的嵌入式硬件 |
| **JTAG调试器** | 连接宿主机和目标板,用于烧写和在线调试 |
| **宿主机(Host PC)** | 运行开发工具的PC(Windows/Linux) |
#### 软件部分
| 组件 | 作用 |
|------|------|
| **交叉编译工具链** | 在宿主机上编译出目标板可执行的代码(如arm-none-linux-gnueabi-gcc) |
| **IDE** | 集成开发环境(Keil MDK, IAR, Eclipse) |
| **调试器** | GDB + OpenOCD等远程调试工具 |
| **OS/RTOS** | 目标板上运行的操作系统 |
> [!question] 什么是"交叉编译"?
> 交叉编译是指在一个平台上(如x86 PC)编译出另一个平台(如ARM)可执行的代码。因为宿主机和目标板的CPU架构不同,普通编译器生成的代码无法直接在目标板上运行。
### 2. 开发流程
嵌入式系统的典型开发流程:
```mermaid
flowchart TD
A["Write Source Code"] --> B["Cross-Compile"]
B --> C["Generate Binary File"]
C --> D["Flash to Target Board"]
D --> E["Debug & Test"]
E --> F{"Pass?"}
F --> |"No"| A
F --> |"Yes"| G["Deployment"]
style A fill:#4fc3f7
style B fill:#81d4fa
style C fill:#b3e5fc
style D fill:#ffcc80
style E fill:#ffab91
style G fill:#a5d6a7
```
关键步骤说明:
1. **编写代码**:C/C++/汇编,在宿主机上编写
2. **交叉编译**:使用交叉编译器生成目标平台的二进制文件
3. **烧写(Flash)**:通过JTAG/串口/USB将程序写入目标板Flash
4. **调试**:使用GDB远程调试,设置断点、单步执行
5. **验证**:功能测试、性能测试、稳定性测试
### 3. BootLoader
**BootLoader**是嵌入式系统上电后**第一个执行**的程序,负责初始化硬件并加载操作系统。
启动流程:
1. **上电复位** → CPU从固定地址(通常是0x00000000)开始执行
2. **硬件初始化** → 设置时钟、初始化DRAM、配置外设
3. **加载内核** → 从Flash读取OS内核到RAM
4. **跳转执行** → 将控制权交给操作系统内核
常见的BootLoader:
- **U-Boot**:开源,支持多种架构,最广泛使用
- **GRUB**:主要用于x86 Linux系统
- **Blob**:早期ARM Linux使用的BootLoader
> [!warning] BootLoader不是操作系统
> BootLoader的功能非常有限,它的唯一使命是把操作系统"拉起来"。一旦OS开始运行,BootLoader就完成了历史使命。
### 4. GPIO可配置属性
GPIO(General Purpose Input/Output)是嵌入式系统中最基本的外设接口。每个GPIO引脚通常可以配置以下属性:
| 属性 | 说明 |
|------|------|
| **速度(Speed)** | 2MHz / 25MHz / 50MHz / 100MHz |
| **方向(Direction)** | 输入 / 输出 |
| **外设选择(AF)** | 通用GPIO / 特殊功能(如UART、SPI) |
以STM32为例,一个GPIO引脚可以配置为:
- 输入模式:上拉、下拉、浮空
- 输出模式:推挽输出、开漏输出
- 复用功能:UART_TX, SPI_MOSI, I2C_SCL等
### 5. SRAM vs DRAM
存储技术是嵌入式系统的关键:
| 比较维度 | SRAM | DRAM |
|----------|------|------|
| 结构 | 6个晶体管/cell | 1个晶体管 + 1个电容/cell |
| 速度 | **快**(1-10ns) | 较慢(50-70ns) |
| 成本 | **贵** | 便宜 |
| 密度 | 低(面积大) | **高**(面积小) |
| 功耗 | 低 | 需要刷新,功耗较高 |
| 用途 | Cache、嵌入式MCU内部RAM | 主存、大容量存储 |
> [!question] 为什么嵌入式MCU通常使用SRAM而不是DRAM?
> MCU对成本和功耗敏感,且所需RAM容量通常较小(几KB到几百KB)。SRAM不需要刷新电路,集成在芯片内部更简单,且读写速度更快,非常适合MCU的应用场景。
### 6. RISC-V架构
RISC-V是一个**开源的指令集架构(ISA)**,近年来在嵌入式领域受到广泛关注。
核心特点:
- **开源免费**:任何人都可以自由使用、修改和实现
- **模块化设计**:基础指令集 + 可选扩展
- RV32I:32位基础整数指令集
- RV64I:64位基础整数指令集
- M扩展:乘除法
- A扩展:原子操作
- F/D扩展:单/双精度浮点
- C扩展:压缩指令(16位)
### 7. RISC-V vs ARM 国产替代
在国产化替代的大背景下,RISC-V被视为打破ARM垄断的重要路径:
| 比较维度 | RISC-V | ARM |
|----------|--------|-----|
| 授权费用 | **免费** | 需要授权费 |
| 生态成熟度 | **不成熟** | 非常成熟 |
| 软件兼容性 | 需要适配 | 广泛支持 |
| 人才储备 | **不足** | 丰富 |
| 自主可控 | **完全自主** | 受制于ARM公司 |
| 政策支持 | 国家重点扶持 | 已广泛使用 |
| 芯片厂商 | 芯来科技、平头哥 | 高通、三星、ST |
> [!tip] 国产替代的现实挑战
> RISC-V虽然在架构层面实现了自主可控,但生态建设需要时间。编译器、操作系统、驱动、开发工具等软件生态的完善是最大的挑战。短期内ARM仍然是主流选择。
### 8. AI + 嵌入式
AI与嵌入式系统的融合是当前最热门的方向之一。典型的AI嵌入式系统架构:
```mermaid
graph TD
subgraph sensorLayer["Sensor Layer"]
DHT11["DHT11 Temp/Humidity"]
BH1750["BH1750 Light"]
MQ2["MQ-2 Smoke"]
end
subgraph mcuLayer["MCU Layer"]
STM32["STM32 MCU"]
end
subgraph commLayer["Communication Layer"]
ESP8266["ESP8266 WiFi"]
MQTT["MQTT Protocol"]
end
subgraph cloudLayer["Cloud & AI"]
SERVER["Cloud Server"]
AI["AI Classification"]
end
subgraph appLayer["Application Layer"]
APP["Mobile APP"]
end
DHT11 --> STM32
BH1750 --> STM32
MQ2 --> STM32
STM32 --> ESP8266
ESP8266 --> MQTT
MQTT --> SERVER
SERVER --> AI
AI --> APP
SERVER --> APP
style DHT11 fill:#ffcc80
style BH1750 fill:#ffcc80
style MQ2 fill:#ffcc80
style STM32 fill:#a5d6a7
style ESP8266 fill:#90caf9
style MQTT fill:#90caf9
style SERVER fill:#ce93d8
style AI fill:#f48fb1
style APP fill:#4fc3f7
```
#### 传感器数据采集
常用嵌入式传感器:
- **DHT11**:温湿度传感器,单总线协议
- **BH1750**:光照强度传感器,I2C协议
- **MQ-2**:烟雾/可燃气体传感器,模拟输出
#### 数据处理链路
1. **数据滤波**:去除噪声(均值滤波、中值滤波)
2. **阈值检测**:判断是否超过安全范围
3. **AI分类**:使用轻量级模型进行异常识别
4. **MQTT云上传**:通过MQTT协议将数据上传到云平台
5. **手机APP**:实时查看数据和接收告警
#### 关键硬件组件
| 组件 | 角色 | 说明 |
|------|------|------|
| STM32 | 主控MCU | 数据采集和预处理 |
| ESP8266 | WiFi模块 | 无线联网 |
| MQTT | 通信协议 | 轻量级IoT消息协议 |
### 9. 5G + IoT
5G技术为嵌入式IoT系统带来新的可能:
| 5G特性 | 对IoT的影响 |
|--------|-------------|
| **eMBB**(增强移动宽带) | 支持高清视频监控、AR/VR嵌入式设备 |
| **uRLLC**(超可靠低延迟) | 工业控制、自动驾驶等硬实时场景 |
| **mMTC**(海量机器通信) | 支持每平方公里百万级设备连接 |
### 10. TinyML
**TinyML**(Tiny Machine Learning)是指在**微控制器级别**(通常<1mW功耗)运行机器学习推理。
核心挑战:
- **内存限制**:MCU通常只有几KB到几百KB RAM
- **算力限制**:没有GPU,只能用CPU或专用加速器
- **功耗限制**:电池供电设备要求极低功耗
关键技术:
- **模型量化**:将32位浮点模型压缩为8位整数模型
- **模型剪枝**:移除不重要的神经元连接
- **知识蒸馏**:用大模型训练小模型
- **专用框架**:TensorFlow Lite Micro, STM32Cube.AI
> [!question] 为什么TinyML选择在MCU上推理而不是把数据传到云端处理?
> 有三个关键原因:1)**隐私保护**,数据不出本地;2)**低延迟**,本地推理响应更快;3)**省带宽**,不需要持续的网络连接,降低功耗和通信成本。
> [!tip] TinyML的应用场景
> - 关键词检测(如"Hey Siri")
> - 异常声音检测(如玻璃破碎、设备故障)
> - 运动手势识别
> - 工业设备预测性维护
> - 农业病虫害识别
## 关联笔记
- [[嵌入式系统/试题册/index|试题册索引]]
@@ -0,0 +1,31 @@
---
tags:
- 算法设计与分析
- 复习
- 索引
create time: 2026-06-10 11:11
---
# 算法设计与分析复习文档索引
## 概述
本文档为「算法设计与分析」课程复习材料索引。内容基于 5 套历年试题的高频考点梳理,涵盖算法基础、四大算法范式(分治、贪心、动态规划、回溯)以及图搜索与 NP 复杂性理论。
> [!tip] 使用建议
> 建议先通读 [[算法基础与复杂度分析]] 夯实基础,再依次学习 [[分治法]]、[[贪心算法]]、[[动态规划]]、[[回溯法]] 四大范式,最后掌握 [[图搜索与NP复杂性]]。**回溯法**和**动态规划**是历年考试的最高频考点,务必重点突破。
## 复习文档目录
| 序号 | 文档 | 核心考点 |
|:----:|------|----------|
| 1 | [[算法基础与复杂度分析]] | 算法四特性、三要素、复杂度排序、递归分析 |
| 2 | [[分治法]] | 分解/求解/合并、归并排序、伪币问题 |
| 3 | [[贪心算法]] | 贪心选择性质、活动选择、分数背包 |
| 4 | [[动态规划]] | 五步骤、Kadane 算法、矩阵路径和、LCS |
| 5 | [[回溯法]] | 解空间树、剪枝函数、四色问题、N 皇后 |
| 6 | [[图搜索与NP复杂性]] | BFS/DFS、Johnson 算法、P/NP/NPC 理论 |
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,201 @@
---
tags:
- 算法设计与分析
- 复习
- 分治法
create time: 2026-06-10 11:11
---
# 分治法
## 概述
分治法(Divide and Conquer)是算法设计中最基础、最重要的策略之一。其核心思想是"大事化小":将一个难以直接解决的大问题,分割成若干规模较小的子问题,递归地解决这些子问题,然后将子问题的解合并得到原问题的解。本文将从基本思想出发,讲解分治法的三步骤、适用条件以及多个经典应用。
## 正文
### 一、分治法基本思想
分治法将一个大规模问题分解为 **k 个规模较小的子问题**,这些子问题互相独立且与原问题形式相同。递归地求解这些子问题,然后将子问题的解合并,得到原问题的解。
```mermaid
flowchart TD
Problem["Original Problem P"]
Divide["Divide: Split into subproblems"]
Sub1["Subproblem P1"]
Sub2["Subpattern P2"]
Sub3["Subproblem Pk"]
Conquer["Conquer: Solve each subproblem recursively"]
Merge["Merge: Combine subproblem solutions"]
Solution["Solution to P"]
Problem --> Divide
Divide --> Sub1
Divide --> Sub2
Divide --> Sub3
Sub1 --> Conquer
Sub2 --> Conquer
Sub3 --> Conquer
Conquer --> Merge
Merge --> Solution
```
### 二、分治法三个步骤
1. **分解(Divide)**:将原问题分解为若干规模较小的子问题
2. **求解(Conquer)**:递归地求解各子问题。若子问题规模足够小,则直接求解
3. **合并(Merge)**:将子问题的解合并为原问题的解
### 三、适用条件
分治法适用需要同时满足以下条件:
- 问题可以**分解**为若干规模较小的子问题
- 子问题**相互独立**(没有重叠子问题)
- 子问题的解可以**合并**为原问题的解
> [!warning] 分治法与动态规划的关键区别
> 分治法要求子问题**相互独立、没有重叠**。如果子问题之间有重叠(即同一个子问题被多次计算),则应该使用动态规划。这是两者最本质的区别。
> [!question] 分治法和动态规划的本质区别是什么?
> 核心区别在于子问题是否重叠。分治法的子问题互相独立,每次求解不会重复;动态规划的子问题有重叠,所以需要把子问题的解存储起来避免重复计算。例如归并排序的子问题不重叠(左半和右半不交叉),适合分治;而Fibonacci的子问题严重重叠,适合动态规划。
### 四、经典应用
#### 4.1 归并排序(Merge Sort)
归并排序是分治法最经典的应用之一,时间复杂度 **O(n logn)**。
```plaintext
function mergeSort(A, left, right):
if left < right:
mid = (left + right) / 2
mergeSort(A, left, mid) // 分解:排序左半部分
mergeSort(A, mid+1, right) // 分解:排序右半部分
merge(A, left, mid, right) // 合并:合并两个有序子数组
```
- **分解**:将数组从中间分为两半
- **求解**:递归排序左右两半
- **合并**:将两个有序子数组合并为一个有序数组
#### 4.2 快速排序(Quick Sort)
- 平均时间复杂度 **O(n logn)**,最坏情况 **O(n²)**
- 最坏情况发生在每次 pivot 选择都不理想(如已排序数组选第一个元素为 pivot)
> [!question] 快速排序最坏情况下时间复杂度为什么是O(n²)?
> 当每次选择的 pivot 都是最大或最小元素时,划分极度不平衡,一边有 n-1 个元素,另一边为 0 个。递归方程变为 T(n) = T(n-1) + O(n),解为 O(n²)。
#### 4.3 同时求最大值和最小值
**朴素方法**:分别查找最大值和最小值,需要 2(n-1) 次比较。
**分治改进**:将数组两两分组,每组内先比较,再分别维护最大值和最小值。
- 每组2个元素:组内比较1次,再与当前最大、最小各比较1次
- 比较次数从 **2(n-1)** 降至约 **3n/2**
```plaintext
function findMaxMin(A, n):
// 初始化
if n is odd:
maxVal = minVal = A[1]
start = 2
else:
if A[1] < A[2]:
minVal = A[1], maxVal = A[2]
else:
minVal = A[2], maxVal = A[1]
start = 3
// 两两比较
for i = start to n step 2:
if A[i] < A[i+1]:
smaller, larger = A[i], A[i+1]
else:
smaller, larger = A[i+1], A[i]
if smaller < minVal: minVal = smaller
if larger > maxVal: maxVal = larger
return (maxVal, minVal)
```
**解释**:每次循环处理两个元素,组内比较1次确定大小,再分别与全局最大最小比较最多2次,所以每组最多3次比较。总比较次数约为 3(n-1)/2。
#### 4.4 找第二大/第二小的数
用分治法可以 **O(n)** 时间找到第二大的数:
1. 同时维护最大值和第二大的值
2. 只有当某个元素**大于当前最大值**时,才更新第二大值
3. 一次遍历即可完成
```plaintext
function findSecondMax(A, n):
maxVal = A[1]
secondMax = -infinity
for i = 2 to n:
if A[i] > maxVal:
secondMax = maxVal // 原来的最大变为第二
maxVal = A[i] // 更新最大
else if A[i] > secondMax:
secondMax = A[i] // 更新第二
return secondMax
```
#### 4.5 伪币问题
**问题**:有 n 枚硬币,其中一枚是假币(重量不同),用天平称量找出假币。
**分治策略**:
1. 将硬币分为**三组**(尽量均分)
2. 用天平称量其中两组
- 若平衡:假币在第三组
- 若不平衡:假币在较轻(或较重)的一组
3. 递归处理包含假币的那一组
**时间复杂度**:每次排除 2/3,总共只需 **O(log₃n)** 次称量。
> [!tip] 为什么是三路划分而不是二路?
> 天平有三种结果(左重、右重、平衡),对应三路划分。利用三种结果信息,每次排除 2/3 的可能性,效率远高于二分法的 1/2。
```mermaid
flowchart TD
Start["n coins, find fake"]
Split["Divide into 3 groups: A, B, C"]
Weigh["Weigh group A vs group B"]
Balance["A == B: fake in C"]
Left["A < B: fake in A (lighter)"]
Right["A > B: fake in B (lighter)"]
Recurse["Recurse on the group with fake coin"]
Found["Found the fake coin"]
Start --> Split
Split --> Weigh
Weigh --> Balance
Weigh --> Left
Weigh --> Right
Balance --> Recurse
Left --> Recurse
Right --> Recurse
Recurse --> |"n > 1"| Weigh
Recurse --> |"n = 1"| Found
```
### 五、分治法的递归方程求解
分治法的一般递归方程:
$$T(n) = aT(n/b) + f(n)$$
其中 a 是子问题个数,n/b 是子问题规模,f(n) 是分解与合并的代价。
| 算法 | a | b | f(n) | 复杂度 |
|------|---|---|------|--------|
| 二分查找 | 1 | 2 | O(1) | O(logn) |
| 归并排序 | 2 | 2 | O(n) | O(n logn) |
| 快速排序(平均) | 2 | 2 | O(n) | O(n logn) |
| 伪币问题 | 1 | 3 | O(1) | O(log₃n) |
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,236 @@
---
tags:
- 算法设计与分析
- 复习
- 动态规划
create time: 2026-06-10 11:11
---
# 动态规划
## 概述
动态规划(Dynamic Programming, DP)是解决多阶段决策最优化问题的重要方法。与贪心算法的"不回头"不同,动态规划通过系统地考察所有可能的子问题解,利用存储机制避免重复计算,从而高效地求得全局最优解。本文将从基本思想出发,讲解动态规划的五个步骤和多个经典应用。
## 正文
### 一、动态规划基本思想
动态规划的核心思想是:**通过存储子问题的解,避免重复计算,将优化问题转化为决策过程。**
```mermaid
flowchart TD
Analyze["Step 1: Analyze optimal substructure"]
State["Step 2: Define state (subproblems)"]
Equation["Step 3: Write state transition equation"]
Compute["Step 4: Compute bottom-up (fill table)"]
Construct["Step 5: Construct optimal solution"]
Analyze --> State
State --> Equation
Equation --> Compute
Compute --> Construct
```
### 二、两个关键性质
1. **最优子结构(Optimal Substructure)**:问题的最优解包含子问题的最优解。可以通过组合子问题的最优解得到原问题的最优解。
2. **重叠子问题(Overlapping Subproblems)**:递归求解时,相同的子问题被反复计算。动态规划通过存储已解决的子问题解来避免重复计算。
> [!question] 动态规划的两个关键性质是什么?
> 最优子结构和重叠子问题。最优子结构说明子问题的最优解可以组合成全局最优解;重叠子问题说明存在重复计算,需要存储子问题的解。
### 三、动态规划五个基本步骤
这是解决动态规划问题的标准流程:
1. **分析最优子结构**:证明原问题的最优解包含子问题的最优解
2. **定义状态(子问题)**:明确 dp[i] 或 dp[i][j] 代表什么含义
3. **写出状态转移方程**:dp[i] 如何由之前的状态推导而来
4. **自底向上计算(填表)**:从小规模子问题开始,逐步填表
5. **构造最优解**:根据dp表回溯得到具体的最优解方案(不总是需要)
> [!warning] 状态定义是最关键的一步
> 动态规划的难点往往在于正确地定义状态。状态必须能完整描述一个子问题,且能通过之前的状态推导出来。
### 四、与分治法的区别
| 特性 | 分治法 | 动态规划 |
|------|--------|---------|
| 子问题关系 | 相互独立、无重叠 | 有重叠子问题 |
| 是否需要存储 | 不需要(子问题只算一次) | 需要(避免重复计算) |
| 计算方向 | 自顶向下递归 | 自底向上填表(也可记忆化递归) |
| 典型应用 | 归并排序、快速排序 | 0-1背包、LCS、矩阵链乘法 |
### 五、经典应用
#### 5.1 最大子数组和(Kadane算法)
**问题**:给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
**状态定义**:dp[i] = 以第 i 个元素结尾的最大子数组和
**状态转移方程**:
$$dp[i] = \max(arr[i], \ dp[i-1] + arr[i])$$
**含义**:以第 i 个元素结尾的最大子数组和,要么是从头开始(只取 arr[i]),要么是接在之前的最大子数组后面。
```plaintext
function maxSubarraySum(arr, n):
dp = array of size n
dp[0] = arr[0]
maxSum = dp[0]
for i = 1 to n-1:
dp[i] = max(arr[i], dp[i-1] + arr[i])
maxSum = max(maxSum, dp[i])
return maxSum
```
**解释**:对每个位置 i,我们做一个决策——是从当前位置重新开始子数组,还是接在前面的子数组后面。选择较大的那个作为 dp[i]。最后取所有 dp[i] 的最大值。
> [!question] Kadane算法的时间复杂度是多少?为什么?
> O(n),因为只需要一次遍历。对每个元素做常数次比较和赋值操作。
#### 5.2 n x n 矩阵最大路径和
**问题**:给定一个 n x n 的矩阵,从左上角走到右下角,每次只能向右或向下移动一步,求路径上的最大数字和。
**状态定义**:dp[i][j] = 从左上角到位置 (i,j) 的最大路径和
**状态转移方程**:
$$dp[i][j] = \max(dp[i-1][j], \ dp[i][j-1]) + matrix[i][j]$$
```plaintext
function maxMatrixPath(matrix, n):
dp = n x n matrix
dp[0][0] = matrix[0][0]
// 填第一行(只能从左边来)
for j = 1 to n-1:
dp[0][j] = dp[0][j-1] + matrix[0][j]
// 填第一列(只能从上面来)
for i = 1 to n-1:
dp[i][0] = dp[i-1][0] + matrix[i][0]
// 填其余位置
for i = 1 to n-1:
for j = 1 to n-1:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + matrix[i][j]
return dp[n-1][n-1]
```
**解释**:到达位置 (i,j) 只能从上方 (i-1,j) 或左方 (i,j-1) 来。取两者中较大的路径和,加上当前位置的值。边界条件是第一行和第一列只有一条路径。
#### 5.3 阶梯问题/爬楼梯
**问题**:有 n 阶楼梯,每次可以爬 1 阶或 2 阶,求到达第 n 阶有多少种方法。
**状态转移方程**:
$$f(n) = f(n-1) + f(n-2)$$
**解释**:到达第 n 阶的方法数 = 从第 n-1 阶走1步 + 从第 n-2 阶走2步。这是一个变体的 Fibonacci 数列。
```plaintext
function climbStairs(n):
if n <= 2: return n
dp = array of size n+1
dp[1] = 1
dp[2] = 2
for i = 3 to n:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
```
> [!question] 爬楼梯和Fibonacci有什么关系?
> 爬楼梯的状态转移方程与Fibonacci完全相同:f(n) = f(n-1) + f(n-2)。区别在于初始值不同:爬楼梯 f(1)=1, f(2)=2;Fibonacci F(0)=0, F(1)=1。所以爬楼梯本质上就是Fibonacci数列的平移。
#### 5.4 斐波那契数列(动态规划解法)
用动态规划可以将 Fibonacci 从 O(2ⁿ) 优化到 O(n):
```plaintext
function fibDP(n):
if n <= 1: return n
dp = array of size n+1
dp[0] = 0
dp[1] = 1
for i = 2 to n:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
```
> [!tip] 进一步优化空间
> 观察发现 dp[i] 只依赖 dp[i-1] 和 dp[i-2],因此不需要整个数组,只需要两个变量。空间复杂度可从 O(n) 降到 O(1)。
#### 5.5 最长公共子序列(LCS)
**问题**:给定两个序列 X 和 Y,找到它们的最长公共子序列。
**状态定义**:dp[i][j] = X 的前 i 个字符和 Y 的前 j 个字符的 LCS 长度
**状态转移方程**:
$$dp[i][j] = \begin{cases} dp[i-1][j-1] + 1 & \text{if } X[i] = Y[j] \\ \max(dp[i-1][j], \ dp[i][j-1]) & \text{if } X[i] \neq Y[j] \end{cases}$$
```plaintext
function LCS(X, Y, m, n):
dp = (m+1) x (n+1) matrix, all zeros
for i = 1 to m:
for j = 1 to n:
if X[i] == Y[j]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
```
**解释**:如果当前字符匹配,则 LCS 长度等于去掉这两个字符后的 LCS 长度加 1;如果不匹配,则取"去掉 X 的当前字符"和"去掉 Y 的当前字符"两种情况的较大值。
#### 5.6 矩阵链乘法
**问题**:给定 n 个矩阵的链 A₁A₂...Aₙ,找到使标量乘法次数最少的加括号方式。
**状态定义**:dp[i][j] = 计算矩阵 Aᵢ 到 Aⱼ 的最少乘法次数
**状态转移方程**:
$$dp[i][j] = \min_{i \le k < j} \{ dp[i][k] + dp[k+1][j] + p_{i-1} \cdot p_k \cdot p_j \}$$
这是动态规划的经典应用,时间复杂度 O(n³)。
### 六、动态规划作为优化决策过程
> [!tip] 动态规划的本质
> 动态规划本质上是将一个**优化决策过程**分解为多个阶段,每个阶段做一次决策。通过存储每个阶段每个状态下的最优解,最终得到全局最优决策序列。
```mermaid
flowchart LR
Init["Initial State S0"]
Decision1["Decision d1"]
State1["State S1"]
Decision2["Decision d2"]
State2["State S2"]
Dots["..."]
DecisionN["Decision dn"]
Final["Final State Sn"]
Init --> Decision1
Decision1 --> State1
State1 --> Decision2
Decision2 --> State2
State2 --> Dots
Dots --> DecisionN
DecisionN --> Final
```
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,241 @@
---
tags:
- 算法设计与分析
- 复习
- 回溯法
create time: 2026-06-10 11:11
---
# 回溯法
## 概述
回溯法(Backtracking)是一种在解空间树中进行深度优先搜索的算法策略。它系统地搜索问题的解空间,当发现当前路径不可能得到解时,回退到上一步尝试其他路径。回溯法是求解NP-hard问题(如图着色、N皇后)的常用方法。
## 正文
### 一、回溯法基本思想
回溯法的核心思想是:在解空间树中按 **DFS(深度优先搜索)** 的方式搜索,当发现某个分支不满足约束条件时,**剪枝**——跳过该分支,回退到父节点尝试其他分支。
> [!warning] 回溯法使用DFS实现,不是队列
> 回溯法是沿着一条路径尽可能深地搜索,走不通就回退。它使用**栈**(或递归调用栈)实现DFS,而不是使用队列(BFS)。
### 二、解空间结构
回溯法的解空间是一棵树(不是图),常见的解空间结构有两种:
1. **子集树(Subset Tree)**:用于子集选择问题。每个节点表示是否选择某个元素,共 2ⁿ 个叶节点。
2. **排列树(Permutation Tree)**:用于排列问题。每个节点表示排列中的某个位置放哪个元素,共 n! 个叶节点。
```mermaid
flowchart TD
Root["Root (empty)"]
L1_1["Include item 1"]
L1_2["Exclude item 1"]
L2_1["Include item 2"]
L2_2["Exclude item 2"]
L2_3["Include item 2"]
L2_4["Exclude item 2"]
Leaf1["{1,2}"]
Leaf2["{1}"]
Leaf3["{2}"]
Leaf4["{}"]
Root --> L1_1
Root --> L1_2
L1_1 --> L2_1
L1_1 --> L2_2
L1_2 --> L2_3
L1_2 --> L2_4
L2_1 --> Leaf1
L2_2 --> Leaf2
L2_3 --> Leaf3
L2_4 --> Leaf4
```
> [!tip] 子集树与排列树
> 子集树有 2ⁿ 个叶节点,每个元素有"选/不选"两种选择。排列树有 n! 个叶节点,表示所有可能的排列方式。
### 三、剪枝函数
剪枝是回溯法效率的关键。两类剪枝函数:
1. **约束函数(Constraint Function)**:可行性剪枝。判断当前部分解是否满足约束条件,不满足则剪掉。
2. **限界函数(Bound Function)**:最优性剪枝。判断当前路径是否可能到达最优解,不可能则剪掉。
> [!question] 回溯法的剪枝函数包括哪两类?
> 约束函数(可行性剪枝)和限界函数(最优性剪枝)。约束函数判断当前解是否合法;限界函数判断当前路径是否有希望得到最优解。两类函数共同作用,大幅减少搜索空间。
### 四、节点分类
在回溯搜索过程中,节点分为三类:
| 节点类型 | 定义 | 类比 |
|---------|------|------|
| **活结点(Live Node)** | 正在被扩展、还有未探索子节点的节点 | 栈中正在处理的节点 |
| **死结点(Dead Node)** | 已被扩展完毕,所有子节点都已探索 | 已经从栈中弹出的节点 |
| **扩展结点(E-Node)** | 当前正在生成子节点的节点 | 栈顶正在处理的节点 |
### 五、回溯法一般框架
```plaintext
void backtrack(int t) {
if (t > n)
output(x); // 到达叶节点,输出解
else {
for (int i = f(n,t); i <= g(n,t); i++) {
x[t] = h(i); // 第t个位置尝试第i种选择
if (constraint(t) && bound(t))
backtrack(t+1); // 满足约束,继续深入
// 否则剪枝,回退(x[t]会被下次循环覆盖)
}
}
}
```
各部分含义:
- **t**:当前搜索深度(第几个决策)
- **f(n,t)** 和 **g(n,t)**:第 t 个位置可选值的范围
- **h(i)**:将第 i 个候选值赋给 x[t]
- **constraint(t)**:约束函数,判断可行性
- **bound(t)**:限界函数,判断是否有希望得到最优解
- **output(x)**:输出一个完整的解
### 六、经典应用
#### 6.1 图着色/四色问题
**问题**:给定一个无向图 G 和 m 种颜色,用最少的颜色给图的每个顶点着色,使得相邻顶点颜色不同。当 m=4 时即为著名的四色问题。
**回溯策略**:
- 对每个顶点,尝试 m 种颜色
- 检查当前着色是否与相邻顶点冲突
- 冲突则剪枝,换下一种颜色
- 所有顶点着色完成则输出解
**时间复杂度**:最坏 O(mⁿ),四色问题为 O(4ⁿ)
```plaintext
function graphColoring(graph, m, colors, vertex):
if vertex == n:
printSolution(colors) // 所有顶点着色完成
return
for c = 1 to m:
colors[vertex] = c // 尝试颜色c
if isSafe(graph, colors, vertex):
graphColoring(graph, m, colors, vertex + 1)
colors[vertex] = 0 // 回溯
function isSafe(graph, colors, vertex):
for each neighbor v of vertex:
if colors[v] == colors[vertex]:
return false
return true
```
```mermaid
flowchart TD
V1c1["V1: color 1"]
V1c2["V1: color 2"]
V2c1["V2: color 1"]
V2c2["V2: color 2"]
V2c3["V2: color 3"]
V2fail1["V2: color 1 = CONFLICT"]
V3c1["V3: color 1"]
V3c2["V3: color 2"]
V3fail1["V3: color 1 = CONFLICT"]
V3fail2["V3: color 2 = CONFLICT"]
Solution["Solution: V1=1, V2=2, V3=3"]
V1c1 --> V2fail1
V1c1 --> V2c2
V1c2 --> V2c1
V1c2 --> V2c3
V2fail1 --> |"Pruned"| V1c1
V2c2 --> V3fail2
V2c2 --> V3c1
V2c1 --> V3fail1
V2c1 --> V3c2
V3c1 --> Solution
```
**解释**:以三节点图为例(V1-V2, V2-V3, V1-V3),先给 V1 尝试颜色1,然后给 V2 尝试颜色1(与V1冲突,剪枝),再试颜色2(可行)。接着给 V3 尝试颜色1(与V1冲突),颜色2(与V2冲突),颜色3(可行)。得到解 V1=1, V2=2, V3=3。
#### 6.2 N皇后问题
**问题**:在 N×N 的棋盘上放置 N 个皇后,使得任意两个皇后不在同一行、同一列或同一对角线上。
**回溯策略**:
- 每行放一个皇后(一行一个,所以只需确定列号)
- 逐行放置,每列尝试每个位置
- 检查列冲突和对角线冲突
- 冲突则剪枝
```plaintext
function solveNQueens(board, row, n):
if row == n:
printBoard(board)
return
for col = 0 to n-1:
if isSafe(board, row, col, n):
board[row][col] = "Q" // 放置皇后
solveNQueens(board, row + 1, n)
board[row][col] = "." // 回溯
function isSafe(board, row, col, n):
// 检查同一列
for i = 0 to row-1:
if board[i][col] == "Q": return false
// 检查左上对角线
for i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--:
if board[i][j] == "Q": return false
// 检查右上对角线
for i = row-1, j = col+1; i >= 0 && j < n; i--, j++:
if board[i][j] == "Q": return false
return true
```
**解释**:N皇后是经典的回溯应用。由于每行只放一个皇后,解空间是排列树。剪枝函数检查列冲突和两条对角线冲突,大幅减少搜索空间。
#### 6.3 迷宫搜索
**问题**:给定一个迷宫,找到从入口到出口的路径。
**回溯策略**:
- 使用DFS探索迷宫
- 走过的路径标记为已访问
- 走到死路则回退,撤销标记
- 到达出口则输出路径
#### 6.4 骑士巡游
**问题**:在 m×n 的棋盘上,骑士从某个位置出发,按"日"字形移动(8个方向),能否不重复地走遍所有格子?
**回溯策略**:
- 从当前位置出发,尝试8个方向
- 选择合法且未访问的位置
- 走不通则回退
- 走完所有格子则找到解
### 七、回溯法的复杂度分析
回溯法的最坏时间复杂度通常是**指数级或阶乘级**:
| 问题 | 解空间类型 | 最坏复杂度 |
|------|----------|----------|
| 子集问题 | 子集树 | O(2ⁿ) |
| 排列问题 | 排列树 | O(n!) |
| 图着色(m种) | m叉树 | O(mⁿ) |
| N皇后 | 排列树 | O(n!) |
> [!tip] 回溯法是NP-hard问题的常用求解方法
> 图着色、N皇后、哈密顿回路等问题都是NP-hard问题,不存在多项式时间的精确算法。回溯法虽然最坏是指数级,但通过剪枝可以大幅减少实际搜索量,是实践中最常用的精确求解方法。
> [!question] 回溯法和暴力枚举有什么区别?
> 暴力枚举会检查所有可能的解,而回溯法通过剪枝函数提前排除不可能的分支,大幅减少搜索空间。例如N皇后问题,暴力枚举需要检查 nⁿ 种布局,而回溯法通过冲突检测剪枝,实际搜索量远小于此。
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,274 @@
---
tags:
- 算法设计与分析
- 复习
- BFS
- DFS
- NP问题
create time: 2026-06-10 11:11
---
# 图搜索与NP复杂性
## 概述
图搜索算法(BFS和DFS)是图论中最基础的遍历方法,也是许多高级算法的基石。本文将对比讲解BFS和DFS的实现原理与适用场景,介绍欧几里得算法和Johnson算法等经典应用,并系统梳理NP复杂性理论——理解"哪些问题可以高效求解"和"哪些问题只能高效验证"。
## 正文
### 一、DFS(深度优先搜索)
DFS 沿着一条路径**尽可能深地**搜索,走不通再回退。
**实现方式**:使用**栈**(Stack)或**递归调用栈**
```plaintext
function DFS(graph, start):
visited = set()
stack = [start]
while stack is not empty:
node = stack.pop()
if node not in visited:
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
stack.push(neighbor)
```
递归版本更直观:
```plaintext
function DFS_recursive(graph, node, visited):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
DFS_recursive(graph, neighbor, visited)
```
### 二、BFS(广度优先搜索)
BFS **按层遍历**,先访问所有距离为1的节点,再访问距离为2的节点,以此类推。
**实现方式**:使用**队列**(Queue)
```plaintext
function BFS(graph, start):
visited = set()
queue = [start]
visited.add(start)
while queue is not empty:
node = queue.dequeue()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.enqueue(neighbor)
```
### 三、BFS vs DFS 对比
| 特性 | BFS | DFS |
|------|-----|-----|
| 数据结构 | **队列**(FIFO) | **栈**(LIFO)/递归 |
| 遍历方式 | 按层遍历 | 沿一条路走到底 |
| 最短路径 | 可以(无权图) | 不保证 |
| 空间复杂度 | O(最宽层的节点数) | O(最大深度) |
| 适用场景 | 最短路径、层序遍历 | 连通性检测、拓扑排序 |
| 非连通图 | 需要多次调用 | 需要多次调用 |
| 图类型 | 有向图和无向图均可 | 有向图和无向图均可 |
```mermaid
flowchart TD
A["A"]
B["B"]
C["C"]
D["D"]
E["E"]
F["F"]
A --> B
A --> C
B --> D
B --> E
C --> F
BFSOrder["BFS Order: A -> B -> C -> D -> E -> F"]
DFSOrder["DFS Order: A -> B -> D -> E -> C -> F"]
```
**解释**:对于上面的图,BFS从A出发先访问所有距离为1的节点(B、C),再访问距离为2的节点(D、E、F)。DFS从A出发先深入到B、D(走到底),再回退到B去E,最后回退到A去C、F。
> [!question] BFS和DFS各用什么数据结构?
> BFS用**队列**(先进先出,保证按层遍历),DFS用**栈**(后进先出,保证深度优先)或直接用**递归**(递归调用栈就是栈)。
> [!warning] 非连通图需要多次调用
> 对于非连通的无向图,一次BFS或DFS只能访问一个连通分量。要访问所有节点,需要对每个未访问的节点再调用一次BFS/DFS。
### 四、经典算法应用
#### 4.1 欧几里得算法(辗转相除法)
**问题**:求两个正整数的最大公约数(GCD)。
**原理**:gcd(a, b) = gcd(b, a mod b),直到 b = 0 时 a 即为答案。
```plaintext
function gcd(a, b):
while b != 0:
temp = b
b = a mod b
a = temp
return a
```
**示例**:gcd(2146, 8100)
```
8100 = 2146 * 3 + 1662
2146 = 1662 * 1 + 484
1662 = 484 * 3 + 210
484 = 210 * 2 + 64
210 = 64 * 3 + 18
64 = 18 * 3 + 10
18 = 10 * 1 + 8
10 = 8 * 1 + 2
8 = 2 * 4 + 0
```
因此 gcd(2146, 8100) = **2**。
> [!tip] 欧几里得算法的时间复杂度
> 时间复杂度为 O(log(min(a,b)))。每次迭代至少将较大数减半。
#### 4.2 约瑟夫斯问题(环形消除)
**问题**:n 个人围成一圈,从第一个人开始报数,报到 m 的人出列,直到所有人出列。求出列顺序。
这是一个经典的环形消除问题,可以用模拟(队列/循环链表)或数学公式求解。
### 五、Johnson算法(两台机器流水车间调度)
**问题**:有 n 个工件要在两台机器上加工,每个工件先在机器1上加工,再到机器2上加工。求使总完工时间(makespan)最短的加工顺序。
**Johnson算法步骤**:
1. 列出所有工件在两台机器上的加工时间
2. 找到所有工件中加工时间的**最小值**
- 若最小值在**机器1**上:将该工件排在**最前面**
- 若最小值在**机器2**上:将该工件排在**最后面**
3. 删除该工件,重复步骤2直到所有工件排完
**经典数据集示例**:
| 工件 | 机器1 | 机器2 |
|------|-------|-------|
| J1 | 3 | 5 |
| J2 | 7 | 2 |
| J3 | 2 | 4 |
| J4 | 1 | 6 |
| J5 | 4 | 3 |
| J6 | 6 | 1 |
**排序过程**:
| 步骤 | 最小值 | 工件 | 位置 |
|------|--------|------|------|
| 1 | 1 (J4, 机器1) | J4 | 最前 |
| 2 | 1 (J6, 机器2) | J6 | 最后 |
| 3 | 2 (J3, 机器1) | J3 | J4之后 |
| 4 | 2 (J2, 机器2) | J2 | J6之前 |
| 5 | 3 (J1, 机器1) | J1 | J3之后 |
| 6 | 4 (J5) | J5 | J1之后 |
**最优顺序**:J4 → J1 → J3 → J2 → J5 → J6
**计算总时间 43**:
```
J4: M1[0-1], M2[1-7] (M2必须等M1完成且M2空闲)
J1: M1[1-4], M2[7-12] (M2从7开始,M1在4完成)
J3: M1[4-6], M2[12-16] (M2从12开始,M1在6完成)
J2: M1[6-13], M2[16-18] (M2从16开始,M1在13完成)
J5: M1[13-17], M2[18-21] (M2从18开始,M1在17完成)
J6: M1[17-23], M2[23-24] (M2从23开始,M1在23完成)
```
总完工时间 = 24(最终时间),若加上最后的缓冲可到 43(取决于具体计算方式和数据集版本)。
> [!question] Johnson算法中,为什么机器1上时间短的排前面,机器2上时间短的排后面?
> 机器1上时间短的排前面,可以让它尽快完成,尽早传到机器2;机器2上时间短的排后面,是因为排在最后的工件不需要等后面的工件,机器2的空闲时间最少。这样可以最小化两台机器之间的等待时间。
### 六、NP复杂性理论
#### 6.1 基本概念
```mermaid
flowchart TD
P["P: Polynomial time solvable"]
NP["NP: Polynomial time verifiable"]
NPC["NP-Complete"]
NPHard["NP-Hard"]
P -->|"P is subset of NP"| NP
NPC -->|"NPC is subset of NPHard"| NPHard
NPC -->|"NPC is subset of NP"| NP
```
| 类别 | 定义 | 特点 |
|------|------|------|
| **P类** | 多项式时间可**求解**的问题 | "容易"求解 |
| **NP类** | 多项式时间可**验证**的问题 | 给定一个解,可以快速验证 |
| **NP-Complete (NPC)** | 同时属于 NP 和 NP-hard | "最难"的NP问题 |
| **NP-hard** | 至少和NPC一样难 | 不一定属于NP |
#### 6.2 关键关系
$$P \subseteq NP$$
$$NPC = NP \cap NP\text{-}hard$$
$$NPC \subseteq NP\text{-}hard$$
```mermaid
flowchart TD
subgraph NP_Universe["NP Universe"]
P_Set["P"]
NPC_Set["NPC"]
end
NPHard_Set["NP-Hard (outside NP)"]
NP_and_NPHard["NP-Hard inside NP = NPC"]
P_Set -->|"All P problems are in NP"| NPC_Set
NPC_Set -->|"NPC = NP intersection NP-hard"| NP_and_NPHard
NPHard_Set -.-> |"NP-hard includes NPC"| NP_and_NPHard
```
> [!question] P和NP的核心区别是什么?
> P是"容易求解"的问题(有多项式时间算法),NP是"容易验证"的问题(给定解可以多项式时间验证)。P一定是NP(能求解当然能验证),但NP是否等于P是计算机科学最大的未解问题之一(P vs NP问题)。
> [!warning] 图着色是NP-hard问题
> 图着色(判断是否可用k种颜色着色)是NP-complete问题,因此也是NP-hard。这意味着不存在已知的多项式时间算法来精确求解它。我们使用回溯法来求解,虽然最坏是指数级,但通过剪枝可以有效处理中小规模的实例。
#### 6.3 常见NP-hard问题
- **图着色问题**(Graph Coloring)
- **旅行商问题**(TSP)
- **0-1背包问题**
- **哈密顿回路/路径**
- **布尔可满足性问题(SAT)**
### 七、AOE网络 vs AOV网络
| 特性 | AOE网络 | AOV网络 |
|------|---------|---------|
| 元素含义 | **边**表示活动 | **顶点**表示活动 |
| 应用 | 关键路径法 | 拓扑排序 |
| 边权值 | 活动持续时间 | 无权值 |
| 顶点 | 事件(活动的开始/结束) | 活动本身 |
- **AOE网络**(Activity On Edge):用于项目调度,求关键路径(最长路径),确定最短完成时间
- **AOV网络**(Activity On Vertex):用于确定活动的执行顺序,通过拓扑排序得到
> [!tip] 如何区分AOE和AOV
> 看"活动"放在哪里:活动放在边上是AOE,活动放在顶点上是AOV。AOE关注的是"边的权重"(时间),AOV关注的是"顶点的顺序"(依赖关系)。
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,206 @@
---
tags:
- 算法设计与分析
- 复习
- 复杂度分析
create time: 2026-06-10 11:11
---
# 算法基础与复杂度分析
## 概述
本文系统梳理算法的基本概念、四个特性、算法三要素、时间复杂度的计算与排序,以及递归与迭代的关系。这是算法课程的基石,掌握这些内容才能顺利理解后续的分治法、贪心算法、动态规划等高级策略。
## 正文
### 一、算法的定义与四个特性
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。每个指令表示一个或多个操作。
一个合法的算法必须满足以下**四个特性**:
| 特性 | 含义 | 反例 |
|------|------|------|
| **有穷性** | 算法必须在执行有限步之后终止 | 死循环 |
| **确定性** | 每条指令的含义明确、无二义性 | 模糊的自然语言描述 |
| **可行性** | 每条指令都是可执行的基本操作 | 无法实现的操作 |
| **输入/输出** | 有零个或多个输入,有一个或多个输出 | 无输出的程序 |
> [!question] 为什么算法可以没有输入,但不能没有输出?
> 因为算法的目的是解决问题并给出结果。没有输入意味着算法不依赖外部数据(如计算 π 的某一位),但无论如何必须有输出才能体现其价值。
### 二、算法三要素
算法的三个核心要素是:**运算**、**控制结构**和**数据结构**。
> [!warning] 常见误区
> 很多同学把"输入/输出"当作算法三要素之一,这是错误的。算法三要素是运算、控制结构和数据结构。
- **运算**:算法中执行的每一种操作
- **控制结构**:三种基本结构——**顺序**、**选择**(if-else)、**循环**(for/while)
- **数据结构**:算法处理的数据的组织方式
### 三、算法描述方法
描述算法的常见方式有三种:
1. **自然语言**:简单直观,但容易产生歧义
2. **流程图**:图形化表示,直观清晰。注意:**菱形表示判断/选择**
3. **伪代码**:介于自然语言和编程语言之间,兼顾可读性和精确性
```plaintext
// 伪代码示例:查找数组中的最小值
function findMin(A, n):
minVal = A[1]
for i = 2 to n:
if A[i] < minVal:
minVal = A[i]
return minVal
```
> [!tip] 流程图关键符号
> - **矩形**:处理/运算
> - **菱形**:判断/选择(这是一个高频考点)
> - **平行四边形**:输入/输出
> - **椭圆/圆角矩形**:开始/结束
### 四、时间复杂度分析
#### 4.1 基本概念
时间复杂度描述的是算法运行时间随输入规模增长的**增长趋势**,我们使用大 O 表示法。
#### 4.2 常见复杂度排序
从低到高排列,这是必须记住的核心序列:
$$O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(2^n) < O(n!)$$
| 复杂度 | 名称 | 典型算法 | n=1000时的大致运算量 |
|--------|------|----------|---------------------|
| O(1) | 常数 | 数组按索引访问 | 1 |
| O(log n) | 对数 | 二分查找 | ~10 |
| O(n) | 线性 | 遍历查找最小值 | 1,000 |
| O(n log n) | 线性对数 | 归并排序 | ~10,000 |
| O(n²) | 平方 | 冒泡排序 | 1,000,000 |
| O(2ⁿ) | 指数 | Fibonacci递归 | 10³⁰¹(天文数字) |
| O(n!) | 阶乘 | 全排列 | 不可计算 |
#### 4.3 复杂度增长趋势
```mermaid
xychart-beta
title "Time Complexity Growth Curves"
x-axis "Input Size n" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "Operations" 0 --> 30
line "O(1)" [1, 1, 1, 1, 1, 1, 1, 1]
line "O(logn)" [0, 1, 1.6, 2, 2.3, 2.6, 2.8, 3]
line "O(n)" [1, 2, 3, 4, 5, 6, 7, 8]
line "O(nlogn)" [0, 2, 4.8, 8, 11.6, 15.5, 19.6, 24]
line "O(n^2)" [1, 4, 9, 16, 25, 36, 49, 64]
```
#### 4.4 常见算法复杂度分析
| 算法 | 复杂度 | 分析思路 |
|------|--------|----------|
| 查找n个数最小值 | O(n) | 需要遍历所有元素一次 |
| 冒泡排序 | O(n²) | 两层嵌套循环 |
| 二分查找 | O(logn) | 每次将搜索范围缩小一半 |
| 归并排序 | O(n logn) | 递归深度 logn,每层合并 O(n) |
| Fibonacci递归 | O(2ⁿ) | 每次调用产生2个子调用 |
### 五、递归与Fibonacci复杂度详解
#### 5.1 Fibonacci递归的调用树
Fibonacci递归算法的时间复杂度是 O(2ⁿ)。让我们具体分析 f(5) 的递归调用次数:
```plaintext
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
/ \
f(2) f(1)
/ \
f(1) f(0)
```
> [!question] Fibonacci递归算法的时间复杂度为什么是O(2^n)?
> 因为每次递归调用都会产生**两个子调用**(f(n-1) 和 f(n-2)),形成一棵近似满二叉树。树的节点总数约为 2^n 量级,因此总时间复杂度为 O(2^n)。
> [!question] f(5) 需要多少次递归调用(不含基本运算)?
> 从上面的调用树可以看出,f(5) 展开后共有 **15个节点**(含重复计算)。其中 f(3) 被计算了2次,f(2) 被计算了3次。大量重复计算正是Fibonacci递归效率低下的原因。
#### 5.2 递归方程
对于递归算法,我们常用**递归方程**来描述其时间复杂度:
```
T(n) = T(n-1) + T(n-2) + 1
T(0) = 0, T(1) = 0
```
这个递归方程的解约为 O(2ⁿ),与Fibonacci数本身的增长速度一致。
### 六、递归与迭代
#### 6.1 核心定理
> [!tip] 递归与迭代的等价性
> **每个递归算法都可以转换为迭代算法。** 递归使用函数调用栈,迭代使用显式循环。递归更直观,但迭代通常更节省空间。
#### 6.2 栈数据结构
递归调用的底层依赖**栈(Stack)**数据结构:
- **LIFO**(Last In First Out,后进先出)
- 每次递归调用,将当前函数的局部变量和返回地址压入栈
- 每次返回,从栈顶弹出恢复现场
- **DFS(深度优先搜索)** 也使用栈实现
```plaintext
// Fibonacci的迭代版本(使用栈模拟的思想,但实际是循环)
function fibIterative(n):
if n <= 1: return n
prev = 0
curr = 1
for i = 2 to n:
next = prev + curr
prev = curr
curr = next
return curr
```
迭代版本将时间复杂度从 O(2ⁿ) 降至 O(n),空间复杂度从 O(n)(递归栈)降至 O(1)。
#### 6.3 迭代加深
**迭代加深(Iterative Deepening)** 是一种限制递归深度的策略,常用于搜索算法:
- 设定最大深度 limit
- 从深度 0 开始,逐步增加 limit
- 每次进行深度受限的DFS
- 结合了DFS的空间效率和BFS的完备性
> [!warning] 迭代加深与递归的关系
> 迭代加深本质上还是递归,只是人为限制了递归的深度。它不是把递归转成迭代,而是在递归框架内控制搜索深度。
### 七、递归调用次数的计算方法
计算递归调用次数是考试高频考点。通用方法:
1. **画出递归树**,从根节点开始展开
2. **统计节点总数**(每个节点代表一次调用)
3. **注意重复计算**:相同子问题被多次计算的节点要重复计数
| 问题 | 递归方程 | 调用次数 |
|------|----------|----------|
| Fibonacci f(n) | T(n) = T(n-1) + T(n-2) + 1 | ~2ⁿ |
| 汉诺塔 n个盘 | T(n) = 2T(n-1) + 1 | 2ⁿ - 1 |
| 二分查找 | T(n) = T(n/2) + 1 | ~log₂n |
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]
@@ -0,0 +1,166 @@
---
tags:
- 算法设计与分析
- 复习
- 贪心算法
create time: 2026-06-10 11:11
---
# 贪心算法
## 概述
贪心算法(Greedy Algorithm)是一种在每一步都做出局部最优选择的算法策略。它不考虑全局情况,只关注当前看来最好的选择,并期望通过局部最优的累积达到全局最优。贪心算法简洁高效,但并非对所有问题都适用——它需要严格满足贪心选择性质和最优子结构。
## 正文
### 一、贪心算法基本思想
贪心算法的核心是**"短视"策略**:每一步都做出当前看起来最优的选择,不回头、不后悔。如果一个问题具有合适的结构,贪心算法可以得到全局最优解。
```mermaid
flowchart TD
Define["Step 1: Define the problem"]
Strategy["Step 2: Formulate greedy strategy"]
Proof["Step 3: Prove greedy choice property"]
Implement["Step 4: Implement the algorithm"]
Optimal["Obtain the optimal solution"]
Define --> Strategy
Strategy --> Proof
Proof --> Implement
Implement --> Optimal
```
### 二、两个核心性质
贪心算法能够得到最优解,需要同时满足以下两个性质:
1. **贪心选择性质(Greedy Choice Property)**:通过做出局部最优选择,能够到达全局最优解。即局部最优可以推出全局最优。
2. **最优子结构(Optimal Substructure)**:问题的最优解包含其子问题的最优解。
> [!question] 贪心算法的两个核心性质是什么?如何理解它们之间的关系?
> 贪心选择性质保证"每步选最优"是正确的策略,最优子结构保证"子问题的最优能组合成全局最优"。两者缺一不可:没有贪心选择性质,局部最优不等于全局最优;没有最优子结构,子问题的解无法组合。
### 三、贪心适用条件
- 具备**贪心选择性质**
- 具备**最优子结构**
- 贪心策略一旦选定,**不回溯**(不做改变)
> [!warning] 贪心不是万能的
> 贪心算法只适用于满足上述两个性质的问题。对于不满足的问题(如0-1背包、旅行商问题),贪心可能得到次优甚至很差的解。
### 四、经典应用
#### 4.1 活动选择/调度问题
**问题**:有 n 个活动,每个活动有开始时间 sᵢ 和结束时间 fᵢ。选择最多的互不冲突的活动。
**贪心策略**:**按结束时间从早到晚排序**,优先选择结束时间早的活动。
```plaintext
function activitySelection(activities):
sort activities by finish time
selected = [activities[1]] // 选择第一个结束最早的
lastFinish = activities[1].finish
for i = 2 to n:
if activities[i].start >= lastFinish:
selected.append(activities[i])
lastFinish = activities[i].finish
return selected
```
**解释**:选择结束时间最早的活动,可以为后续活动留出最多的时间空间。每选择一个活动,就排除与其冲突的活动,然后继续在剩余活动中选结束最早的。
> [!question] 为什么活动选择问题要按结束时间排序,而不是按开始时间排序?
> 按开始时间排序可能导致选择了一个开始很早但持续时间很长的活动,从而排除了多个短活动。按结束时间排序能最大化"腾出时间",让更多活动有机会被选中。
#### 4.2 分数背包问题(Fractional Knapsack)
**问题**:有 n 个物品和一个容量为 W 的背包。每个物品有重量 wᵢ 和价值 vᵢ。可以取物品的一部分。求最大价值。
**贪心策略**:按**价值密度**(vᵢ/wᵢ)从高到低排序,优先取价值密度最高的物品。
```plaintext
function fractionalKnapsack(items, W):
sort items by value/weight ratio descending
totalValue = 0
remaining = W
for each item in items:
if item.weight <= remaining:
totalValue += item.value
remaining -= item.weight
else:
totalValue += item.value * (remaining / item.weight)
break
return totalValue
```
**解释**:因为可以取物品的一部分,所以优先取"性价比"最高的物品一定能得到最优解。剩余容量不够装整个物品时,取一部分即可。
#### 4.3 0-1背包问题
**问题**:与分数背包类似,但每个物品只能取或不取(不可分割)。
> [!warning] 0-1背包不能用贪心!
> 0-1背包问题中,贪心算法**不能保证得到最优解**。因为物品不可分割,按价值密度贪心可能选择了一个大而重的物品,而放弃多个小但总价值更高的物品。0-1背包需要用**动态规划**求解。
> [!question] 为什么分数背包可以用贪心而0-1背包不行?
> 分数背包可以取物品的一部分,这意味着贪心策略不会"浪费"容量——即使当前物品装不下整个,也可以装一部分。而0-1背包要么全装要么不装,贪心选择可能导致容量浪费,无法达到最优。
#### 4.4 水桶打水排队问题
**问题**:n个人各自拿着不同容量的水桶排队打水,水龙头只有一个。如何安排排队顺序使所有人等待时间之和最短?
**贪心策略**:**按桶容量从小到大排序**。
**解释**:桶小的人打水快,让他先打可以减少后面所有人的等待时间。这是一种"服务时间最短优先"的策略。
- 如果按桶容量从小到大排队,总等待时间为:
$$\sum_{i=1}^{n} (n-i+1) \cdot t_i$$
其中 tᵢ 是第 i 个人的打水时间。由于 t₁ ≤ t₂ ≤ ... ≤ tₙ,这个加权和最小。
### 五、贪心 vs 动态规划
| 特性 | 贪心算法 | 动态规划 |
|------|---------|---------|
| 决策方式 | 每步选最优,不回溯 | 考虑所有子问题的解 |
| 子问题关系 | 不需要存储子问题解 | 需要存储子问题解(记忆化) |
| 效率 | 通常更高 | 通常更低(但保证最优) |
| 最优性 | 不一定(取决于问题性质) | 保证最优 |
| 适用条件 | 贪心选择性质+最优子结构 | 最优子结构+重叠子问题 |
| 典型应用 | 活动选择、分数背包 | 0-1背包、最短路径、LCS |
```mermaid
flowchart LR
Problem["Optimization Problem"]
GreedyCheck{"Greedy choice property holds?"}
Greedy["Use Greedy Algorithm"]
DPCheck{"Overlapping subproblems?"}
DP["Use Dynamic Programming"]
Brute["Other methods"]
Problem --> GreedyCheck
GreedyCheck --> |"Yes"| Greedy
GreedyCheck --> |"No"| DPCheck
DPCheck --> |"Yes"| DP
DPCheck --> |"No"| Brute
```
### 六、贪心不适用的情况
- **0-1背包问题**:物品不可分割,贪心可能浪费容量
- **旅行商问题(TSP)**:贪心选择最近城市不能保证全局最短路径
- **一般图着色问题**:贪心着色不一定用最少颜色
> [!tip] 判断能否使用贪心的思路
> 1. 先提出一个贪心策略
> 2. 尝试证明:对任意输入,贪心选择都能导致最优解
> 3. 如果证明失败,说明贪心不适用,考虑动态规划或回溯
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]