Maxw的小站

Maxw学习记录

队列

任何操作系统内核中常见的编程模式是生产者与消费者。实现此模式的最简单方式通常是使用队列,即先进先出(FIFO)。

Linux内核的通用队列实现称为kfifo,代码位于kernel/kfifo.c,头文件为<linux/kfifo.h>。本节讨论2.6.33版本更新后的API(早期版本用法略有不同,编写代码前请确认头文件)。


kfifo

Linux的kfifo与其他队列抽象类似,提供两个核心操作:
- 入队in):将数据写入队列
- 出队out):从队列中读取数据

kfifo对象维护两个偏移量:
- in偏移量:下一次入队的起始位置
- out偏移量:下一次出队的起始位置

规则
1. out偏移量始终≤in偏移量(否则会读取未入队的数据)。
2. 当out == in时,队列为空(无法出队)。
3. 当in到达队列末尾时,需重置队列才能继续入队。


创建队列

动态初始化(常用)
1
2
3
4
5
6
7
8
9
10
int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask);
```
- **功能**:分配大小为`size`字节的队列(`size`必须为2的幂次),内存分配标志为`gfp_mask`(参见第12章“内存管理”)。
- **返回值**:成功返回`0`,失败返回错误码。
- **示例**:
```c
struct kfifo fifo;
int ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if (ret)
return ret; // 初始化失败
自定义缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size);
```
- **功能**:使用用户提供的`buffer`初始化队列,`size`必须为2的幂次。

##### 静态声明(较少用)
```c
DECLARE_KFIFO(name, size); // 声明队列
INIT_KFIFO(name); // 初始化队列
```
- **要求**:`size`必须为2的幂次。

---

#### 数据入队
```c
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
```
- **功能**:从`from`复制`len`字节到队列`fifo`。
- **返回值**:实际入队的字节数(若空间不足,可能小于`len`)。


#### 数据出队
##### 标准出队
```c
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);
```
- **功能**:从队列`fifo`复制最多`len`字节到`to`缓冲区。
- **返回值**:实际出队的字节数。
- **注意**:出队后数据不再保留在队列中。

##### 查看数据(不删除)
```c
unsigned int kfifo_out_peek(struct kfifo *fifo, void *to, unsigned int len, unsigned offset);
```
- **功能**:与`kfifo_out`类似,但**不移动out偏移量**,数据仍可后续读取。
- **参数**:`offset`指定队列中的起始位置(0表示队头)。

---

#### 获取队列信息
1. **获取队列总容量**
```c
static inline unsigned int kfifo_size(struct kfifo *fifo);
  • 返回队列底层缓冲区的总字节数
  1. 获取已入队数据量
    1
    static inline unsigned int kfifo_len(struct kfifo *fifo);
  • 返回当前队列中已存储的字节数
  • (注:内核命名存在不一致性,需特别注意)
  1. 获取剩余空间
    1
    static inline unsigned int kfifo_avail(struct kfifo *fifo);
  • 返回可继续写入的剩余字节数
  1. 队列状态检查
    1
    2
    static inline int kfifo_is_empty(struct kfifo *fifo);
    static inline int kfifo_is_full(struct kfifo *fifo);
  • 返回非零值表示队列为空/满
  • 返回零表示非空/非满

重置与销毁队列

  1. 重置队列
    1
    static inline void kfifo_reset(struct kfifo *fifo);
  • 清空队列所有内容(不释放内存)
  1. 销毁动态分配的队列
    1
    void kfifo_free(struct kfifo *fifo);
  • 释放通过kfifo_alloc()创建的队列
  • 注意:使用kfifo_init()创建的队列需手动释放关联缓冲区

实际应用示例

创建8KB大小的kfifo队列:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/kfifo.h>
struct kfifo my_fifo; // 声明 kfifo 结构体
int ret;
// 分配 8KB 队列
ret = kfifo_alloc(&my_fifo, 8192, GFP_KERNEL); // 8192 = 8 * 1024
if (ret) {
pr_err("Failed to allocate kfifo: error %d\n", ret);
return ret;
}
// 验证大小
pr_info("Created FIFO size: %u bytes\n", kfifo_size(&my_fifo)); // 输出 8192
> 队列总容量 = 8 KB = 8 × 1024 字节 = 8192 字节(环形缓冲区无需减一) > unsigned int大小 = 4 字节(也可能是8字节,可以通过sizeof(unsigned int)获取) > 这样一个队列最多可以容纳8192 字节 / 4 字节 = 2048 个unsigned int

入队操作

1
2
3
4
/* 入队0-31的整数 */
unsigned int i;
for (i = 0; i < 32; i++)
kfifo_in(fifo, &i, sizeof(i));

查看队首元素(不移除)

1
2
3
4
5
unsigned int val;
int ret = kfifo_out_peek(fifo, &val, sizeof(val), 0);
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val); /* 输出: 0 */

完整出队操作

1
2
3
4
5
6
7
8
while (kfifo_avail(fifo)) {
unsigned int val;
int ret = kfifo_out(fifo, &val, sizeof(val));
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val);
}
/* 输出: 0 1 2 ... 31 (严格保持FIFO顺序) */
- 输出顺序为0→31证明是标准的FIFO队列 - 若输出为31→0则变为栈结构(LIFO) - 所有操作均保持原子性,适合生产者-消费者场景 (注:实际开发中通常入队复杂结构体而非基础类型)

链表(Linked Lists)

单链表与双链表

  • 单链表:每个节点只包含指向下一个节点的指针。
    1
    2
    3
    4
    struct list_element {
    void *data; // 数据
    struct list_element *next; // 指向下一个节点
    };
  • 双链表:每个节点包含指向前一个和后一个节点的指针。
    1
    2
    3
    4
    5
    struct list_element {
    void *data; // 数据
    struct list_element *next; // 指向下一个节点
    struct list_element *prev; // 指向前一个节点
    };

循环链表(Circular Linked Lists)

  • 普通链表的最后一个节点的 next 指针通常指向 NULL,表示结束。
  • 循环链表的最后一个节点的 next 指针指向第一个节点,形成环状结构。循环链表可以是单向或双向的。
  • Linux 内核的链表实现本质上是循环双链表,灵活性最高。

链表的遍历

  • 链表的遍历是线性的:从头节点开始,依次通过 next 指针访问每个节点。
  • 非循环链表的最后一个节点 next 为 NULL;循环链表的最后一个节点 next 指向头节点。
  • 双链表可以支持从尾节点向前遍历。
  • 链表适合需要频繁插入、删除和遍历全部元素的场景,不适合随机访问。

Linux 内核链表实现方式

1. 与传统链表的区别

  • 传统链表通常是在数据结构里直接加 nextprev 指针,把结构本身变成链表节点。
  • Linux 内核采用嵌入链表节点的方式:在自定义结构体里嵌入一个 struct list_head 成员,而不是直接用 next/prev 指针。

2. 内核链表节点结构

1
2
3
4
struct list_head {
struct list_head *next;
struct list_head *prev;
};
  • 只包含指向前后节点的指针,不存储数据。

3. 如何使用

  • 在你的结构体里嵌入 struct list_head 成员,例如:
    1
    2
    3
    4
    5
    6
    struct fox {
    unsigned long tail_length;
    unsigned long weight;
    bool is_fantastic;
    struct list_head list; // 链表节点
    };
  • 这样,fox.list.next 指向下一个 fox,fox.list.prev 指向上一个 fox。

4. 链表操作

  • 内核提供了丰富的链表操作函数(如 list_add()),这些函数只操作 list_head,不关心具体数据类型。
  • 通过 container_of 宏,可以从 list_head 指针反查到包含它的结构体:
    1
    2
    3
    4
    #define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))
    #define list_entry(ptr, type, member) \
    container_of(ptr, type, member)
  • list_entry() 可以从链表节点指针获取到完整的结构体数据。

5. 链表初始化

  • 动态分配结构体后,用 INIT_LIST_HEAD(&obj->list) 在运行时初始化链表节点。
    1
    2
    3
    4
    5
    6
    struct fox *red_fox; 
    red_fox = kmalloc(sizeof(*red_fox), GFP_KERNEL);
    red_fox->tail_length = 40;
    red_fox->weight = 6;
    red_fox->is_fantastic = false;
    INIT_LIST_HEAD(&red_fox->list);
  • 静态定义时可用 LIST_HEAD_INIT() 宏在编译时初始化。
    1
    2
    3
    4
    5
    struct fox red_fox = { 
    .tail_length = 40,
    .weight = 6,
    .list = LIST_HEAD_INIT(red_fox.list),
    };

6. 链表头

  • 通常会定义一个专门的 list_head 变量作为链表头,用于管理整个链表:
    1
    static LIST_HEAD(fox_list);
  • 这个链表头本质上也是一个 list_head 节点,但不存储实际数据,只作为入口。

Linux 内核链表的操作方法

  1. 所有链表操作函数都只操作 struct list_head 指针,和具体数据类型无关。

    • 这些函数都定义在 <linux/list.h>,实现为内联函数,效率高。
    • 所有操作都是 O(1) 常数时间,无论链表长度如何,插入、删除等操作速度都一样。
  2. 常用操作函数:

    • 添加节点(底层的循环链表特性,每一个节点都可以填入head)
      • list_add(new, head):把新节点插入到 head 节点之后(将最后一个节点填入head,实现栈)。
      • list_add_tail(new, head):把新节点插入到 head 节点之前(将第一个节点填入head,实现队列)。
    • 删除节点
      • list_del(entry):把 entry 节点从链表中移除,但不释放内存。
      • list_del_init(entry):移除节点并重新初始化它,方便后续复用。
    • 移动节点
      • list_move(list, head):把节点 list 移到 head 节点之后。
      • list_move_tail(list, head):把节点 list 移到 head 节点之前。
    • 拼接链表
      • list_splice(list, head):把 list 指向的链表拼接到 head 节点之后。
      • list_splice_init(list, head):拼接后把原链表重新初始化。
    • 判断链表是否为空
      • list_empty(head):判断链表是否为空,返回非零表示空。
  3. 内部函数优化

    • 如果你已经有了节点的 next 和 prev 指针,可以直接调用内部函数(如 __list_del(prev, next)),省去多余的指针解引用。
    • 这些内部函数一般以双下划线 __ 开头,只有在你已经拿到指针时才建议用。

Linux 内核链表的遍历

完整遍历包含n个节点的链表是O(n)时间复杂度操作。

基础遍历方法

最基础的遍历宏是list_for_each(),接收两个list_head参数: - 第一个参数:指向当前项的指针(需调用者提供的临时变量) - 第二个参数:要遍历的链表头节点

1
2
3
4
struct list_head *p;
list_for_each(p, fox_list) {
/* p指向链表中的某个节点 */
}

但仅获取list_head指针通常无用,我们需要的是包含该链表节点的结构体(前文已讨论)指针。此时应使用list_entry()宏:

1
2
3
4
5
struct list_head *p;
struct fox *f;
list_for_each(p, &fox_list) {
f = list_entry(p, struct fox, list); // 获取包含list_head的fox结构体
}

实用遍历方法

上述方法不够直观,因此内核主要使用list_for_each_entry()宏,该宏自动完成list_entry()转换:

1
list_for_each_entry(pos, head, member)

参数说明: - pos:包含list_head的结构体指针(相当于list_entry()返回值) - head:链表头节点指针(如fox_list) - memberlist_head在结构体中的成员名(如list

示例:

1
2
3
4
struct fox *f;
list_for_each_entry(f, &fox_list, list) {
/* 每次迭代f指向下一个fox结构体 */
}

实际案例(来自内核文件系统通知系统inotify):

1
2
3
4
5
6
7
8
9
10
static struct inotify_watch *inode_find_handle(struct inode *inode, 
struct inotify_handle *ih)
{
struct inotify_watch *watch;
list_for_each_entry(watch, &inode->inotify_watches, i_list) {
if (watch->ih == ih)
return watch;
}
return NULL;
}
此函数遍历inode->inotify_watches链表,查找匹配inotify_handle的节点。

反向遍历

list_for_each_entry_reverse()功能与正向遍历相同,但沿prev指针逆向移动:

1
list_for_each_entry_reverse(pos, head, member)
适用场景: 1. 性能优化:当目标节点更靠近链表尾部时 2. 顺序要求:如实现LIFO栈操作 若无特殊需求,建议使用正向遍历。

安全删除遍历

标准遍历方法在迭代过程中删除节点会导致问题(后续迭代无法获取正确的next/prev指针)。内核提供安全版本:

1
list_for_each_entry_safe(pos, next, head, member)
参数next用于临时存储下一节点指针,确保当前节点可安全删除。

inotify示例

1
2
3
4
5
6
7
8
9
10
11
12
void inotify_inode_is_dead(struct inode *inode)
{
struct inotify_watch *watch, *next;
mutex_lock(&inode->inotify_mutex);
list_for_each_entry_safe(watch, next, &inode->inotify_watches, i_list) {
struct inotify_handle *ih = watch->ih;
mutex_lock(&ih->mutex);
inotify_remove_watch_locked(ih, watch); // 删除watch节点
mutex_unlock(&ih->mutex);
}
mutex_unlock(&inode->inotify_mutex);
}

逆向安全遍历版本:

1
list_for_each_entry_safe_reverse(pos, n, head, member)

注意事项

  1. "safe"宏仅防护循环体内的删除操作,若存在并发操作仍需加锁
  2. 更多链表操作方法详见<linux/list.h>

198.打家劫舍

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int rob(vector<int>& nums) {
vector<int> dp(nums.size()+1, 0);
dp[1] = nums[0];
for(int i = 2; i <= nums.size(); i++){
dp[i] = max(dp[i-2]+nums[i-1], dp[i-1]);
}
return dp[nums.size()];
}
};

213.打家劫舍II

三种情况的动态规划: - 不考虑头尾元素 - 考虑头元素,不考虑尾元素 - 考虑尾元素,不考虑头元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1)return nums[nums.size()-1];
int res_front = rob_range(nums, 0, nums.size()-2);
int res_back = rob_range(nums, 1, nums.size()-1);
int res = max(res_front, res_back);
if(nums.size() >= 3)res = max(res,rob_range(nums, 1, nums.size()-2));
return res;
}
int rob_range(vector<int>& nums, int start, int end){
vector<int> dp(nums.size()+1, 0);
if(start == end)return nums[start];
dp[start] = nums[start];
dp[start+1] = max(nums[start], nums[start+1]);
if((start+1) == end)return dp[start+1];
for(int i = start+2; i<=end; i++){
dp[i] = max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[end];
}
};

337.打家劫舍III

直接使用递归,计算抢当前节点还是不抢当前节点时,对相同的子树进行了多次独立的递归调用。会超时,推荐记忆化: 利用递归,每层返回一个保存当前节点偷还是不偷的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// struct TreeNode{
// int val;
// TreeNode* left;
// TreeNode* right;
// TreeNode(int v) : val(v), left(nullptr), right(nullptr){}
// };
class Solution {
public:
vector<int> rob_tree(TreeNode* root){
if(root == nullptr)return vector<int>{0, 0};
vector<int> left = rob_tree(root->left);
vector<int> right = rob_tree(root->right);

// ↓不抢当前节点时,既可以选择抢子节点,也可选择不抢,可能会有跳过两个节点抢更优的情况
int next_h = max(left[0], left[1])+max(right[0], right[1]);
int this_h = root->val+left[0]+right[0];

return {next_h, this_h};
}
int rob(TreeNode* root) {
vector<int> res = rob_tree(root);
return max(res[0], res[1]);
}
};

TCP连接如何确保可靠性

序列号与确认应答: * 序列号: 发送方为每个字节的数据分配一个唯一的序列号。TCP报文段的首部包含该报文段中第一个数据字节的序列号。 * 确认号: 接收方收到数据后,会发送一个确认报文段。该报文段中的确认号字段表示接收方期望收到的下一个字节的序列号。例如,接收方正确收到了序列号为 1001-2000 的数据,它会发送确认号 2001,表示“我已正确收到序列号 2000 及之前的所有字节,请从序列号 2001 开始发送”。 * 机制: 发送方发送数据后,会等待接收方的确认。如果收到预期的确认,说明数据已成功送达。这是可靠性的基石。

校验和: * 计算: 发送方在发送数据前,会计算报文段(首部和数据)的校验和,并将结果放入首部的校验和字段。 * 验证: 接收方收到报文段后,会使用相同的算法重新计算校验和。 * 丢弃: 如果接收方计算出的校验和与报文段首部中的校验和不匹配,说明数据在传输过程中发生了比特错误(比特翻转)。接收方会丢弃该损坏的报文段,并且不会发送任何确认。 * 触发重传: 由于发送方没有收到确认(见第1、2点),最终会触发超时重传。

流量控制: * 目的: 防止发送方发送数据过快,导致接收方缓冲区溢出而丢失数据。 * 滑动窗口: 接收方通过TCP首部中的窗口大小字段告知发送方自己当前接收缓冲区的可用空间大小。这个值称为接收窗口。 * 发送窗口限制: 发送方维护一个发送窗口,其大小不能超过接收方通告的接收窗口大小。发送窗口内的数据是允许发送但尚未被确认的数据。 * 动态调整: 随着接收方处理数据并释放缓冲区空间,它会通过后续的确认报文段更新其通告的窗口大小,发送方据此调整自己的发送窗口。这确保了发送速率不会超过接收方的处理能力。

拥塞控制: * 目的: 防止发送方发送数据过快,导致网络中间设备(如路由器)的缓冲区溢出,引发网络拥塞和数据包丢失。这是对整个网络的保护机制。 * 核心机制: 发送方维护一个拥塞窗口,它限制了在任何时候可以发送但未被确认的数据量。发送窗口的实际大小是min(接收窗口, 拥塞窗口)

超时重传: * 核心思想: 发送方发送一个报文段后启动一个重传计时器。如果在计时器超时之前没有收到该报文段的确认,发送方就认为该报文段已丢失或损坏,会重新发送该报文段。 * 动态计算超时时间: 超时时间是根据历史数据包往返时间动态计算出来的,称为RTO。这确保了在网络状况变化时也能有效工作。

拥塞控制是怎么实现的

  • 慢启动: 连接开始时,拥塞窗口从一个很小的值开始,并随着每个成功确认的报文段而指数增长(每收到一个ACK,cwnd增加1个MSS),快速探测可用带宽。
  • 拥塞避免: 当拥塞窗口增长到某个阈值时,进入拥塞避免阶段,窗口变为线性增长(每收到一个RTT内的所有ACK,cwnd增加1个MSS),增速放缓。
  • 拥塞检测:
    • 超时: 如果发生超时(表明有严重丢包),阈值被设置为当前拥塞窗口的一半(ssthresh = cwnd / 2),拥塞窗口被重置为1(或一个很小的值),重新进入慢启动。
    • 快速重传与快速恢复: 如果发送方收到3个重复的ACK(表明有单个数据包丢失,但后续数据包接收方还能收到),它立即重传丢失的报文段(快速重传),并将阈值设置为当前拥塞窗口的一半(ssthresh = cwnd / 2),拥塞窗口设置为阈值加3(或类似算法),然后进入快速恢复阶段。在快速恢复阶段,每收到一个重复ACK,拥塞窗口增加1个MSS。当收到一个新数据的ACK时(表明重传成功),退出快速恢复,将拥塞窗口设置为阈值大小,进入拥塞避免阶段。这比超时恢复要快得多。

TCP流量控制是怎么实现的

TCP流量控制的实现核心在于滑动窗口协议(Sliding Window Protocol),其目的是防止发送方发送数据过快导致接收方缓冲区溢出。这是通过接收方动态通告其接收窗口大小来实现的。

  1. 初始通告:
    • 连接建立时(三次握手阶段),接收方在其SYN+ACK报文段中设置窗口大小字段,告知发送方其初始接收缓冲区大小。
  2. 动态窗口通告:
    • 接收方处理数据: 当接收方应用程序从缓冲区读取数据后,缓冲区空间被释放,可用空间增加。
    • 发送更新窗口: 接收方在发送给发送方的任何报文段(包括数据报文段、纯ACK确认报文段)中,都会携带最新的窗口大小值。
    • 即时生效: 发送方收到包含新窗口大小的报文段后,立即更新其对接收方接收窗口的理解。
  3. 发送方行为 - 滑动窗口:
    • 维护状态: 发送方维护三个指针:
      • SND.UNA:最早未确认字节的序列号。
      • SND.NXT:下一个要发送字节的序列号。
      • 发送窗口大小 (swnd)swnd = min(接收方通告的接收窗口, 拥塞窗口)。流量控制关注的是接收窗口部分。
    • 发送约束: 发送方只能发送序列号在 [SND.UNA, SND.UNA + swnd) 范围内的数据。
    • 窗口滑动:
      • 当发送方收到新的ACK确认(推进了SND.UNA),并且接收方通告了新的(可能更大的)窗口大小时,发送窗口会向右“滑动”。
      • 滑动后,SND.NXT可能可以继续发送新的数据(如果可用窗口 > 0)。
  4. 关键操作示例:
    • 假设接收方初始通告rwnd = 4000字节。
    • 发送方发送2000字节(SND.NXT前进2000)。
    • 接收方收到这2000字节,但应用程序只读取了1000字节。此时接收缓冲区:
      • 已用空间 = 1000字节(2000收到 - 1000被读走)
      • 可用空间 = 3000字节(初始4000 - 1000占用)。
      • 接收方在ACK中设置rwnd = 3000
    • 发送方收到ACK和rwnd=3000
      • SND.UNA前进2000(假设ACK确认了前2000字节)。
      • 更新swnd = min(3000, cwnd)
      • 新发送窗口变为 [新SND.UNA, 新SND.UNA + 3000)
      • SND.NXT可能指向新窗口内的位置,允许发送最多3000字节新数据(减去已在传输中的)。
  5. 处理零窗口 - 死锁预防:
    • 问题: 如果接收方缓冲区满,它会通告rwnd = 0。发送方必须立即停止发送数据。但如果之后接收方应用程序读取数据释放了缓冲区,它需要通知发送方rwnd > 0。如果这个通知(携带新rwnd的ACK)丢失了怎么办?双方会陷入死锁:发送方在等待窗口更新,接收方以为发送方知道窗口已打开。
    • 解决方案:零窗口探测:
      • 当发送方收到rwnd = 0时,启动一个持续计时器
      • 计时器超时后,发送方发送一个1字节的探测报文段
      • 接收方收到探测报文段:
        • 如果缓冲区仍满,再次回复rwnd = 0,发送方重置持续计时器。
        • 如果缓冲区已有空间,回复包含当前rwnd > 0的ACK。
      • 探测报文段确保即使窗口更新ACK丢失,死锁也能被打破。

UDP怎么实现可靠传输

(参考QUIC)

HTTPS和HTTP有哪些区别

  1. 加密性
    • HTTP:数据明文传输,容易被窃听和篡改。
    • HTTPS:在 HTTP 基础上增加了 SSL/TLS 加密层,数据传输安全。
  2. 连接建立流程
    • HTTP:TCP 三次握手后即可传输数据。
    • HTTPS:TCP 三次握手后,还需进行 SSL/TLS 握手,协商密钥后才能加密传输数据。
  3. 端口号
    • HTTP:默认端口 80
    • HTTPS:默认端口 443
  4. 证书机制
    • HTTP:不需要证书。
    • HTTPS:需要向 CA 申请数字证书,验证服务器身份,防止伪造。

HTTPS的工作原理(HTTPS建立连接的过程)

  1. 密钥交换
    客户端发起HTTPS请求,服务器发送公钥证书给客户端。

  2. 证书验证
    客户端验证服务器证书是否由受信任的CA签发,并检查证书有效性。

  3. 加密通信协商
    客户端生成一个随机的对称加密密钥,用服务器公钥加密后发送给服务器。

  4. 建立安全连接
    服务器用私钥解密,得到对称加密密钥。此时双方拥有相同密钥,可以进行加密通信。

  5. 数据传输
    双方用对称加密密钥对数据进行加密传输,保证安全性。

  6. 完整性校验
    SSL/TLS协议还会对数据进行完整性校验,防止数据被篡改。

  7. 结束连接
    通信结束后,会话密钥被销毁,避免安全隐患。

TCP和UDP的区别

  1. 连接方式
    • TCP:面向连接,传输前需建立连接(三次握手)。
    • UDP:无连接,直接发送数据。
  2. 可靠性
    • TCP:可靠传输,保证数据顺序和完整性,丢包会重传。
    • UDP:不保证可靠性,可能丢包、乱序、不重传。
  3. 流量与拥塞控制
    • TCP:有流量控制(滑动窗口)和拥塞控制,能根据网络状况调整速率。
    • UDP:没有流量和拥塞控制,发送速率固定。
  4. 报文头部
    • TCP:头部复杂,包含序列号、确认号等。
    • UDP:头部简单,只有基本信息。
  5. 性能开销
    • TCP:机制多,性能开销大,延迟高。
    • UDP:机制简单,开销小,延迟低。
  6. 适用场景
    • TCP:适合需要可靠传输的场景,如网页浏览、文件传输等。
    • UDP:适合对实时性要求高的场景,如语音通话、视频会议等。

new和malloc有什么区别

new和malloc在C++中都用于动态内存分配,但它们之间有几个关键的区别:

语法层面: new是C++的操作符,可以直接用来分配对象或数组。 malloc是一个函数,通常需要包含头文件,并且只分配原始内存。 类型安全: new是类型安全的,它会根据分配的对象类型进行正确的内存分配和构造函数调用。 malloc 不是类型安全的,它只分配原始内存,不调用构造函数。返回类型是 void*,需要强制类型转换为具体的指针类型。 构造与析构: 使用 new 分配的对象在对象生命周期结束时需要使用 delete 来释放,delete 会自动调用对象的析构函数。 使用 malloc 分配的内存需要使用 free 来释放,free 不会自动调用析构函数,因此如果分配的是对象数组,需要手动调用析构函数。 异常安全性: new在分配失败时会抛出std::bad_alloc异常。 malloc在分配失败时返回NULL指针。 管理机制: C++中的new和delete通常由编译器实现,可能包含一些额外的内存管理机制。 C语言的malloc和free由C标准库提供,与编译器无关。 总结来说,new和malloc都是动态内存分配的手段,但new提供了类型安全和构造/析构的自动化,而malloc则提供了更底层的内存分配方式,需要手动管理构造和析构。在C++中,推荐使用new来分配对象,以保持类型安全和自动化的资源管理。

delete 和 free 有什么区别?

delete和free都是用来释放动态分配的内存,但它们有不同的使用方式:

语法:

delete是C++中的关键字,用于释放由new分配的对象。 free是C语言中的函数,通常包含在<stdlib.h>头文件中,用于释放由malloc分配的内存。 对象销毁:

当使用 delete 释放对象内存时,C++ 编译器会自动调用对象的析构函数,释放与对象相关的资源,并执行对象的清理工作。 free 仅释放内存,不调用析构函数。因此,如果使用 malloc 分配了 C++ 对象的内存,需要手动调用析构函数后再调用 free。 数组处理:

如果是数组,C++提供了delete[]来释放整个数组的内存,而C语言中仍然使用free,没有区分单个对象和数组。 返回值:

free 没有返回值,即使内存释放失败,也不会反馈任何信息。

delete 之后,指针会自动置为 nullptr

类型检查:

delete 进行类型检查,确保删除的对象类型与 new 分配时的类型一致。

free 不进行类型检查,因为它只处理 void* 类型的指针。

总结来说,delete和free都是用来释放动态内存的,但它们分别用于C++和C语言中的内存管理。delete适用于C++对象,会自动调用析构函数;而free适用于C语言分配的内存,不涉及对象的析构。

堆区和栈区的区别

堆 (Heap) 和栈 (Stack) 是程序运行时两种不同的内存分配区域

内存分配:

栈 是由编译器自动管理的,用于存储局部变量和函数调用的上下文信息。 栈上的对象在定义它们的块或函数调用结束后自动销毁。 栈的内存分配和释放速度很快,因为栈的内存管理是连续的,不需要搜索空闲内存。 堆 由程序员手动管理的,用于存储动态分配的对象。 堆上的对象需要程序员手动释放,否则可能导致内存泄漏。 堆的内存分配和释放速度通常比栈慢,因为可能需要搜索合适的内存块,并且涉及内存碎片整理。 大小限制:

栈的大小通常有限制,远小于堆的大小,且在不同系统和编译器中可能不同。 堆的大小通常很大,受限于系统可用内存。 使用场景:

栈主要用于存储函数参数、局部变量等。 堆用于存储生存期不受限于单个函数调用的对象,如使用 new 或 malloc 分配的对象。

补充: 如何定义只能在堆上或栈上生成对象的类

在 C++ 中,可以通过控制构造函数和析构函数的访问权限来限制对象只能在堆上或栈上创建。以下是两种实现方式的详细说明和代码示例:

1. 只能在堆上创建对象(禁止栈上创建)

核心原理:私有析构函数 + 智能指针管理生命周期
关键点: - 私有析构函数阻止栈对象自动销毁 - 静态工厂函数返回智能指针 - 智能指针自定义删除器调用 destroy() - destroy() 调用 delete this(需谨慎使用) - 禁用拷贝构造/赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <memory>

class HeapOnly {
public:
// 工厂函数:返回带有自定义删除器的 unique_ptr
static std::unique_ptr<HeapOnly, void(*)(HeapOnly*)> create() {
return {
new HeapOnly(),
[](HeapOnly* p) { p->destroy(); } // 自定义删除器
};
}

// 销毁函数(必须公开)
void destroy() {
delete this; // 调用私有析构函数
}

// 禁用拷贝操作
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;

private:
HeapOnly() = default; // 私有构造
~HeapOnly() = default; // 私有析构
};

// 使用示例
auto obj = HeapOnly::create(); // 只能在堆上创建

2. 只能在栈上创建对象(禁止堆上创建)

核心原理:禁用 new 操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StackOnly {
public:
StackOnly() = default;
~StackOnly() = default;

private:
// 禁用 new 操作符
void* operator new(size_t) = delete;
void* operator new[](size_t) = delete;
void* operator new(size_t, void* p) = delete; // 禁用 malloc + placement new
};

// 使用示例
StackOnly stackObj; // 合法:栈上创建
// auto heapObj = new StackOnly(); // 编译错误:new 被禁用

⚠️ 注意:delete this 是特殊技术,必须确保: - 对象通过 new 创建 - 调用 delete this 后不再访问任何成员 - 通常只用于精确控制生命周期的场景

必做练习

练习1
准备好实验报告

练习2
我们将通过 libc 包装器发起系统调用(标准用户程序调用内核的方式)。系统调用完整列表可通过 man 2 syscalls 查看手册页。
1. 启动树莓派,在终端中脱离 Linux 源码目录新建用户程序目录
2. 创建文件 lib_call.c,编写 C 程序:
- 使用库函数 getuid 读取并打印用户 ID
- 尝试用 setuid 将 ID 设为 0(root)并报告是否成功
- 再次读取并打印用户 ID
3. 通过 man 2 getuidman 2 setuid 查阅:
- 函数调用语法和返回值类型
- 编译所需的头文件
4. 错误检查:若 setuid 失败,输出错误原因(使用 strerror(errno))。
- 相关手册页:man 3 printf, man 3 strerror, man 3 errno
5. 在实验机上编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
   gcc lib_call.c -o lib_call && ./lib_call
```
**提示**:
直接写 `usr_id = getuid()` 实际上**不是直接使用系统调用**,而是调用了**libc库的封装函数**(即 glibc 提供的 getuid),它内部会帮你完成系统调用的参数准备和陷入内核的过程。
如果想**直接使用系统调用**,需要用 `syscall` 函数或者汇编指令,并传入系统调用号(如 `__NR_getuid`),而不是用库函数。例如:


---

**练习3**
从树莓派用户程序目录执行:
1. 通过 sftp 从 `shell.cec.学校简称.edu` 获取 `lib_call.c`
2. 在树莓派上编译并运行程序(分别用普通用户和 `sudo` 权限运行)
**答案**:粘贴两种运行方式的输出,并对比实验机与树莓派的差异(编译/运行等)。

---

**练习4**
修改程序使用原生系统调用接口(`man 2 syscall`):
1. 复制 `lib_call.c` 为 `native_call.c`
2. 将 `getuid/setuid` 替换为 `syscall`
3. 通过 Linux 源码确定 ARM 系统调用号:
- 查看 `arch/arm/include/uapi/asm/unistd.h`
- 查看生成的 `arch/arm/include/generated/uapi/asm/unistd-common.h`
4. **使用常量**(如 `__NR_getuid`)而非硬编码数字
5. 可能需包含头文件 `asm/unistd.h`
6. 在树莓派上编译运行
**答案**:粘贴新程序的输出。

---

**练习5**
为内核添加两个新系统调用(各打印一条内核日志):
**任务清单**
1. 在内核中声明函数原型
2. 实现函数逻辑
3. 分配系统调用号
4. 更新 ARM 系统调用表
5. 更新系统调用总数

**操作步骤**
1. SSH 登录 `shell.cec.学校简称.edu` → `qlogin -q all.q` → 进入 Linux 源码目录
2. **修改前备份文件**(如 `cp syscalls.h syscalls.h.072325`)

**声明函数原型**(在 `include/linux/syscalls.h` 底部添加):
```c
// CSE422 072325
asmlinkage long sys_noargs(void);
asmlinkage long sys_onearg(int arg);
```
**答案**:粘贴添加的函数原型。

---

**练习6**
**实现系统调用函数**:
1. 在 `arch/arm/kernel/` 创建文件:
- `sys_noargs.c`(无参数函数)
- `sys_onearg.c`(带参数函数)
2. `sys_noargs.c` 内容参考[此文件](https://example.com/sys_noargs.c)
3. `sys_onearg.c` 模板:
```c
SYSCALL_DEFINE1(sys_onearg, int, arg) {
printk("Received argument: %d\n", arg);
return 0; // 返回适当值
}
```
**答案**:粘贴 `sys_onearg` 的实现代码。

---

**练习7**
**添加文件到构建系统**:
修改 `arch/arm/kernel/Makefile`:
1. 备份原文件
2. 在 `obj-y` 列表末尾添加(**不在 `\` 后**):
```makefile
obj-y += sys_noargs.o sys_onearg.o
```
**答案**:粘贴修改后的 `obj-y` 行。

---

**练习8**
**更新系统调用表**:
修改 `arch/arm/tools/syscall.tbl`:
1. 备份原文件
2. 在末尾添加(分配新调用号):
```plaintext
456 common sys_noargs sys_noargs
457 common sys_onearg sys_onearg
```
**答案**:粘贴添加的两行内容。

---

**练习9**
**编译并验证新内核**:
1. 在 Linux 源码根目录执行:
```bash
make clean
```
2. 修改本地版本标识:
- 编辑 `.config` 文件
- 更新 `CONFIG_LOCALVERSION` 值(如 `-syscallstudio`)
3. 按实验1/2的步骤编译安装内核
4. 树莓派上运行:
```bash
uname -a
```
**答案**:粘贴 `uname -a` 输出结果。

---

**练习10**
**调用新系统调用**:
1. 通过 sftp 获取 `arch/arm/include/generated/uapi/asm/unistd-common.h`
2. 复制到 GCC 头文件目录:
```bash
sudo cp unistd-common.h /usr/include/arm-linux-gnueabihf/asm
```
3. 创建 `new_call.c`(基于 `native_call.c`):
- 将 `getuid` 调用号替换为 `__NR_sys_noargs`
- 将 `setuid` 调用号替换为 `__NR_sys_onearg`
- 为单参数调用声明变量(勿直接传常量)
4. 编译运行后检查内核日志:
```bash
dmesg
```
**答案**:粘贴 `dmesg` 中包含系统调用输出的日志行。

---

### **需提交的文件**
1. 必做练习答案
2. 源代码:`lib_call.c`, `native_call.c`, `new_call.c`
3. 系统调用实现文件:`sys_noargs.c`, `sys_onearg.c`
4. 修改文件的 diff(对比备份):
```bash
diff -up syscalls.h syscalls.h.072325 > syscalls.h.diff
diff -up syscall.tbl syscall.tbl.072325 > syscall.tbl.diff


可选拓展练习

完成任意拓展练习后简述收获(附问题答案)。

练习1
用汇编接口实现系统调用(参考课堂 ARM 调用流程),使用 GCC 内联汇编(asm 扩展)。
> 提示:详细流程见 man 2 syscall,内核入口代码见 arch/arm/kernel/entry-common.S

练习2
实现读取周期计数器(CCNT)的系统调用:
1. 下载性能监控头文件arch/arm/include/asm
2. 声明函数原型:asmlinkage long sys_read_ccnt(unsigned long long *val)
3. 创建 sys_read_ccnt.c(参考模板
4. 更新 Makefilesyscall.tbl
5. 编写程序连续调用两次,计算调用耗时(周期数)
答案:报告系统调用消耗的周期数。

练习3
修复 read_ccnt 的安全缺陷:
1. 使用 copy_to_user() 替代直接写入用户指针
2. 失败时返回 -EFAULT
答案:粘贴修复后的 sys_read_ccnt 实现代码。


注意:
- 所有内核修改需添加注释 //CSE422 MMDDYY(日期格式:月日年)
- 推荐在实验机开发代码,通过 sftp 同步至树莓派以防崩溃丢失

322. 零钱兑换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <limits>
#include <algorithm> // 包含 std::min 和 std::max
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, std::numeric_limits<int>::max());// 每个amount需要的最少coin数
// 第一遍遍历需要把coins中有的coin数置1,所以dp[0] = 0方便+1
dp[0] = 0;
// 针对每个amount, 遍历减去coin看哪一种最少
for(int i = 0; i<=amount; i++){
for(int j = 0; j< coins.size();j++){
if((i - coins[j]) >= 0 && dp[i - coins[j]] < std::numeric_limits<int>::max()){
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] == std::numeric_limits<int>::max() ? -1 : dp[amount];
}
};

279.完全平方数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int numSquares(int n) {
// 完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍历背包
for (int j = 1; j * j <= i; j++) { // 遍历物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
};

139.单词拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size()+1, false);
dp[0] = true;
for(int i = 1; i<=s.size(); i++){
for(int j = 0; j<i; j++){
string subS = s.substr(j, i-j);
if(wordSet.find(subS) != wordSet.end() && dp[j]){
dp[i] = true;
}
}
}
return dp[s.size()];
}
};

使用 perf 验证性能瓶颈

perf 找系统慢在哪,就像给系统做一次‘全身CT扫描’加‘血液化验’。在我那个操作系统框架项目里,我们特别关心调度算法在任务多到快‘撑爆’的时候表现如何(超负荷场景)。这个过程大概是这样的:

  1. ‘挂号’与‘检查’ (启动Perf & 记录数据):
    • 我先给系统‘挂号’,告诉 perf 我要检查什么:CPU 干了啥、内存怎么用的、任务等了多久、程序内部函数调用了多少次等等。这些信息就是‘检查项目’(perf 事件,如 cycles, instructions, cache-misses, context-switches, sched:sched_stat_runtime, sched:sched_stat_wait 等)。
    • 然后,我让系统开始‘干活’(运行特定负载,模拟超负荷),同时 perf 就像个精密的仪器在旁边‘抽血化验’、‘拍片子’,全程记录下所有关键指标(perf record -e <事件列表> -ag -- sleep 30 或针对特定进程/命令)。
    • 这个负载是精心设计的,包含不同类型(CPU计算狂、慢悠悠的I/O读写手、要求准时完成的‘急脾气’任务)和不同数量的任务,把它们一股脑儿塞给系统,看它啥时候‘顶不住’。
  2. ‘看化验单’与‘读CT片’ (分析Perf报告):
    • 等系统跑完,perf 会生成一份厚厚的‘检查报告’(perf report)。这份报告特别有用:
      • ‘谁最累?’ (CPU时间分布): 报告会列出哪个程序、哪个函数吃掉最多的CPU时间(Overhead 列)。如果某个调度器函数或者内核里处理任务切换/通信(IPC)的函数占比异常高,那它可能就是‘罪魁祸首’。
      • ‘效率高不高?’ (指令效率): 我重点看 CPIIPCCPI 是说CPU执行一条指令平均花了多少个时钟周期,IPC 是反过来(一个周期执行多少条指令)。理想值: 现代CPU在跑得很顺的代码时,IPC 应该接近1甚至更高(比如1.5-2.0+)。瓶颈信号: 如果整个系统或者关键函数的 IPC 掉得很低(比如低于0.5),或者 CPI 飙升(比如>2.0),那CPU可能没在高效干活,而是在‘空等’或者‘干杂活’。
      • ‘等得久不久?’ (调度延迟): 报告里能看到任务从‘准备好’到‘真正上CPU跑’等了多久(sched:sched_stat_wait)。合理范围: 在超负荷前,这个等待时间应该比较稳定且符合预期(比如微秒到毫秒级,看系统要求)。瓶颈信号: 如果这个等待时间在超负荷时急剧增加(比如从毫秒级跳到几十甚至几百毫秒),或者大量时间花在 sched 相关的内核函数上,说明调度器本身或者任务太多成了瓶颈,任务在‘排队等CPU等太久’。
      • ‘找东西慢不慢?’ (缓存失效): cache-misses 事件特别重要。CPU从自己高速缓存(L1/L2/L3)拿数据比去慢悠悠的主内存(RAM)拿快百倍。合理范围: 有一定比例的 cache-misses 是正常的。瓶颈信号: 如果 cache-misses 率非常高(比如LLC Last Level Cache miss rate > 5-10%,具体看CPU和负载),或者某个关键数据结构相关的代码段 cache-misses 特别多,说明CPU经常‘找不到需要的东西’,得跑去慢速内存取,这会严重拖慢速度,是内存访问瓶颈的典型标志。
      • ‘沟通顺畅吗?’ (IPC开销): 在微内核里,任务间沟通(IPC)是大事。我会专门看花在内核处理IPC消息的函数(比如消息复制、权限检查、调度切换)上的时间占比。合理范围: IPC本身就有成本,时间占比应该相对稳定。瓶颈信号: 如果IPC处理时间占比在超负荷时不成比例地暴增,或者 context-switches (上下文切换次数) 高得吓人,说明系统资源大量消耗在‘传话’和‘换人’上了,任务本身的‘正事’反而没时间做,这就是通信开销过大或调度切换太频繁的瓶颈。
  3. ‘病灶’在哪? (我们发现的瓶颈): 在我们的超负荷测试中,perf 的‘化验单’清晰地指出了几个‘病灶’:
    • ‘沟通成本’太高 (IPC开销陡增): 当任务数量超过某个阈值,花在内核处理IPC上的CPU时间占比从正常的 ~15% 一下子跳到 40%+!perf report 显示 ipc_send, ipc_receive 及相关内核锁函数耗时剧增。同时 context-switches 也翻倍了。这说明微内核架构的‘双刃剑’——隔离性好但沟通成本高——在重压下成了主要瓶颈。
    • ‘找东西’越来越慢 (Cache效率下降): LLC cache-misses 率在超负荷时从 ~3% 升高到 12%+。结合 annotate 功能看热点代码,发现是调度器内部的任务队列数据结构访问冲突加剧,导致多个CPU核心频繁互相使对方的缓存失效 (False Sharing),逼得CPU频繁去慢速内存找数据。
    • ‘急脾气’任务被耽误 (调度延迟波动): sched:sched_stat_wait 显示实时任务的等待时间方差(jitter)在超负荷时变得很大,偶尔出现远超截止期限的等待峰值。perf 指向调度器在重载下决策变慢,以及非实时任务过多挤占了CPU资源。
  4. ‘化验单’靠谱吗? (判断数据合理性): 光看一次‘化验’结果不行,得判断数据靠不靠谱:
    • ‘对照实验’: 我会在相同硬件、相同负载下,跑不同的调度算法配置或内核版本,用 perf 收集数据对比。如果瓶颈指标(如IPC耗时、Cache Miss率)的变化趋势一致,且能解释性能差异(如吞吐量下降、延迟上升),数据就合理。
    • ‘符合常识’: 数据要符合计算机原理。比如:
      • CPI 不可能无限低(受限于CPU物理极限)。
      • 一个纯计算任务如果 IPC 很低且 cache-misses 不高,那瓶颈可能在指令依赖或分支预测失败(branch-misses)。
      • 调度延迟不可能为0,总有个基础开销。
    • ‘多指标印证’: 单一指标可能误导。比如高 %sys (内核态CPU占用) 是瓶颈信号,但必须结合 perf report 看具体是内核哪部分耗时。高 cache-misses 是信号,但要看 annotate 确认热点代码位置是否合理。
    • ‘可重复性’: 多次运行测试,关键指标(如平均延迟、最大延迟、瓶颈函数占比)的波动应在小范围内,结果要能稳定复现。
    • ‘符合理论模型’ (效用边界分析): 我们用 perf 采集的响应时间、CPU利用率等数据,输入到调度理论模型(如排队论模型)中计算‘效用边界’。如果模型预测的系统饱和点、性能拐点与 perf 观测到的实际瓶颈出现点(如IPC开销暴增、延迟突变的负载阈值)能吻合,那这些 perf 数据及其揭示的瓶颈就非常可信了。

518. 零钱兑换 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<uint64_t> dp(amount + 1, 0); // 防止相加数据超int
dp[0] = 1;
// 第一轮循环应该将所有j = coins[i]置1
// 想清楚这一点,就可以明确组合数的状态转移方程和排列数不一样
for(int i = 0; i<coins.size(); i++){
int coin = coins[i];
for(int j = coin; j<(amount+1); j++){
dp[j] += dp[j - coins[i]];
}
// cout<<"coins[i]="<<coins[i]<<endl;
// for(int d:dp)cout<<d<<' ';
// cout<<endl;
}

return dp[amount];
}
};

377. 组合总和 Ⅳ

如果求组合数就是外层for循环遍历物品,内层for遍历背包,因为物品处理顺序固定。 如果求排列数就是外层for遍历背包,内层for循环遍历物品,因为每个容量下都重新扫描所有物品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for(int j = 0;j<=target;j++){
for(int i = 0; i<nums.size();i++){
if((j - nums[i])>=0 && dp[j] <= INT_MAX - dp[j - nums[i]]){
dp[j] += dp[j - nums[i]];
}
}
// for(auto d:dp)cout<<d<<' ';
// cout<<endl;
}
return dp[target];
}
};

70. 爬楼梯 (进阶)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n+1, 0);
dp[0] = 1;
for(int i = 0; i<=n; i++){
for(int j = 1; j<=2; j++){
if((i-j) >= 0){
dp[i] += dp[i - j];
}
}
}
return dp[n];
}
};
0%