leetcode 写题的笔记
编码问题记录
- 你的代码问题在于:每次拼接字符串时都创建了新对象:
1 | new_str = new_str + c; // ← 每次都创建新字符串,效率极低! |
这会导致大量的内存分配和拷贝操作。应该使用 push_back() 或 += 操作符:
LeetCode笔记
1. 队列与栈
1.1 (单调队列) LeetCode.239. 滑动窗口最大值
题目介绍
1 | 题目描述 |
示例:
1 | 示例 1: |
1 | 示例 2: |
提示:
- $1 \le \text{nums.length} \le 10^5$
- $-10^4 \le \text{nums}[i] \le 10^4$
- $1 \le k \le \text{nums.length}$
📝 单调队列 (Monotonic Queue)
核心定义:持续维护队列里面的元素单调递增或单调递减的队列。
💡 如何维护单调递减队列?
- 维护原则:新元素入队前,清除队尾所有比它小的元素。
- 根本目的:确保队列头部始终是当前窗口的最大值。
- 底层逻辑:若新元素
x大于队尾元素y,则y在x离开窗口前永无出头之日(既小又老),故可直接删除y。
📊 演示范例
输入数组:vec = {1, 3, 1, -1, 2}
(注:在此维护下,队列始终保持递减状态)
| 操作 | 动作描述 | 队列状态 (队首 -> 队尾) | 当前最大值 |
|---|---|---|---|
push(1) |
队空,直接入队 | [1] |
1 |
push(3) |
3 > 1,弹出 1,入队 3 |
[3] |
3 |
push(1) |
1 < 3,直接入队 |
[3, 1] |
3 |
push(-1) |
-1 < 1,直接入队 |
[3, 1, -1] |
3 |
push(2) |
2 > -1 弹出 -1,2 > 1 弹出 1,2 < 3 入队 2 |
[3, 2] |
3 |
- 注意:以上演示暂未考虑滑动窗口大小。如果滑动窗口划走的元素恰好是队列的头元素,则需要把头元素移除。
- 核心洞察:我们本来就不需要维护当前位置之前、且比当前元素更小的元素,因为在当前元素被替换掉之前,那个更小的元素就已经被“干掉”了。
💻 核心代码实现
1. 单调队列类封装
1 | class monotonic_queue { |
2. 滑动窗口构建逻辑 (Main)
1 | int main() { |
1.2 (优先级队列(最大最小堆)) LeetCode.347 前 K 个高频元素
题目介绍
| 类别 | 难度 | 喜欢 | 不喜欢 |
|---|---|---|---|
| 算法 (Algorithms) | 中等 (Medium) 65.72% | 2184 | - |
标签: 哈希表 数组 分治 桶排序 计数 快速选择 排序 堆(优先队列)
题目描述
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例:
示例 1:
1 | 输入: nums = [1,1,1,2,2,3], k = 2 |
示例 2:
1 | 输入: nums = [1], k = 1 |
提示:
- $1 \le \text{nums.length} \le 10^5$
- $-10^4 \le \text{nums}[i] \le 10^4$
- $k$ 的取值范围是 $[1, \text{数组中不相同的元素的个数}]$
- 题目数据保证答案唯一,换句话说,数组中前 $k$ 个高频元素的集合是唯一的
进阶:
你所设计算法的时间复杂度 必须 优于 $O(n \log n)$ ,其中 $n$ 是数组大小。
📝 优先级队列 (Priority Queue)
核心定义:基于堆 (Heap) 数据结构实现的队列。元素出队顺序不由入队顺序决定,而是由元素的优先级(通常指大小)决定。
💡 如何维护最大堆(默认优先队列)?
- 核心特性:
- 非先进先出:元素入队顺序不影响出队顺序。
- 自动排序:每次插入或删除元素后,内部会自动调整结构,确保堆顶元素始终是优先级最高(最大或最小)的元素。
- 底层实现:默认使用
std::vector作为底层容器,通过算法维护堆性质。
- 维护原则:新元素入队后,通过“上浮”操作调整位置,确保父节点始终大于等于子节点。
- 底层逻辑:堆是一种完全二叉树结构。对于最大堆,任何节点的值都大于或等于其子节点的值。因此,根节点(
top())必然是全局最大值。插入时只需在末尾添加并向上调整,删除时只需将末尾元素移至根部并向下调整,效率极高。
⚙️ 核心参数与复杂度
1. 模板定义
1 | template< |
T:存储元素的类型。Container:底层容器,默认为std::vector<T>。Compare:比较规则,默认为std::less<T>(即最大堆)。
2. 时间复杂度
- 插入 (
push): O(logN) - 删除堆顶 (
pop): O(logN) - 获取堆顶 (
top): O(1)
3. 常用成员函数
| 函数 | 描述 | 复杂度 |
|---|---|---|
push(const T& val) |
插入元素并调整堆 | |
pop() |
删除堆顶元素 | |
top() |
返回堆顶元素的引用(不删除) | |
empty() |
判断队列是否为空 | |
size() |
返回元素个数 |
📊 演示范例
输入数组:vec = {1, 3, 1, -1, 2}
(注:这里演示的是 priority_queue<int> 最大堆的入队逻辑)
| 操作 | 动作描述 | 堆内部结构 (逻辑上的完全二叉树) | 堆顶 (top) |
|---|---|---|---|
push(1) |
堆空,直接入队 | [1] |
1 |
push(3) |
3 > 1,3 上浮成为根,1 下沉 |
[3, 1] |
3 |
push(1) |
1 < 3,1 作为左子节点,无需上浮 |
[3, 1, 1] |
3 |
push(-1) |
-1 < 1,-1 作为右子节点,无需上浮 |
[3, 1, 1, -1] |
3 |
push(2) |
2 > -1,2 上浮替换 -1;2 > 1? 否,停止 |
[3, 2, 1, -1, 1] |
3 |
pop() |
移除堆顶 3,将末尾 1 移至根,向下调整 | [2, 1, 1, -1] |
2 |
- 注意:
priority_queue底层通常由vector实现,它不保证非堆顶元素的有序性,只保证堆顶是极值。
🛠️ 进阶:底层容器 Container 的选择(参数2)
std::priority_queue 的第二个模板参数 Container 指定了底层用于存储数据的容器。它必须支持随机访问迭代器以及 back(), push_back(), pop_back() 操作。
- 场景 A:追求极致性能(保持默认 vector)
在 99% 的情况下,不要修改这个参数。std::vector的连续内存布局使得 CPU 缓存命中率极高,对于堆这种频繁访问父子节点的数据结构,vector的性能通常优于deque。 - 场景 B:避免大规模扩容时的停顿(使用 deque)
如果数据量极大且不可预测,std::vector在容量不足时会重新分配内存并复制所有元素,这可能导致短暂的延迟抖动。std::deque按需分配小块内存,不会发生整体复制。
💻 核心代码实战
1. 基础最大堆与最小堆
1 |
|
2. 经典应用:前 K 个高频元素(也就是本题的问题)
1 | // 场景3:解决 "前 K 个高频元素" (结合 Hash Map) |
评论