数据结构和算法
本文最后更新于 2025年10月25日 下午
算法
枚举算法
描述:也称为穷举算法,按照问题本身性质,列举出该问题所有可能的解,并在逐一列举的过程中,将它们逐一与目标状态进行比较以得出满足问题要求的解。通常用于求解问题规模比较小的问题,或者作为求解问题的一个子算法。
核心思想:通过列举问题的所有状态,将它们逐一与目标状态进行比较,从而得到满足条件的解,关注枚举的上下界。
优点:简单基本,便于实现,容易调试,正确性容易证明。
流程:
确定枚举对象、枚举的上下界、判断条件。
枚举可能的情况并验证是否是问题的解。
考虑提高效率(缩小状态空间,限制枚举条件,找特殊性质避免重复求解)。
题库:百钱买百鸡,LeetCode78,611,1504,1733,2048,2749。
贪心算法
描述:每一步都做出在当前看来是最好的选择,期望通过局部最优选择来达到全局最优解。
核心思想:局部最优,不可回溯,期望全局最优。
贪心只做局部最优选择,不保证全局最优。
优点:高效,简单直观。
流程:
- 建立数学模型
- 分解问题
- 制定贪心策略
- 求解子问题
- 合并成原问题解
题库:LeetCode134,1792,2598,3350,3397。
回溯算法
描述:通过深度优先(DFS)搜索策略来遍历所有可能的候选解,并在搜索过程中利用剪枝来避免无效搜索,提高效率。
核心思想:
- 路径:从根节点到当前节点所做出的选择序列。
- 选择列表:在当前节点可以做出的所有选择。
- 结束条件:找到一个解或停止。
流程:
- 在当前节点遍历所有选择
- 找到解做出选择并加入当前路径
- 递归进入下一个状态
- 递归返回后,撤销刚才的选择(回溯)
模板:
1 | class Solution { |
可以配合lamdba表达式auto dfs{};
题库:LeetCode 22,39,77。
动态规划
描述:把原问题分解为相对简单的子问题的方式求解复杂问题的方法,能够避免重复计算子问题。
分成记忆化递归(自顶向下)和DP数组(自底向上)两种动态规划。
核心思想:拆分子问题、记住过往、减少重复计算。
流程:
穷举分析
确认边界
- 找出规律,确定最优子结构
- 动态规划问题即是递推问题,当前的决策结果是
f(n),则最有子结构就是要让f(n-k)最优,并且与后面的决策没有关系,能让后面的决策安心使用局部最优解。
- 动态规划问题即是递推问题,当前的决策结果是
- 写出状态转移方程
- 找出规律,确定最优子结构
题库:LeetCode62,120,1277,1504,2327,3186,3494。
位运算算法
大大提高程序的性能,0/1问题时好用。
题库:LeetCode78,2749。
模拟
描述:完全按照题目的描述或者问题的真实发生过程,一步步地用代码复现出来从而得到答案。
核心思想:重视还原,过程再现。
流程:
- 提炼规则
- 设计数据结构
- 分解步骤,模块化
- 注意边界
特点:思路直观,实现繁琐。
题库:LeetCode6,54,3446。
堆算法
描述:特殊的完全二叉树,可以用一维数组存储。
堆的更新和维护:
入堆push(),时间复杂度O(log n)
出堆pop(),时间复杂度O(log n)
访问栈顶top(),时间复杂度O(1)
建堆,时间复杂度O(nlog n)
# 即堆中插入n个元素
应用:优先队列。
题库:LeetCode 1792。
分支限界法
描述:
- 分支:将大问题划分为若干子问题,形成解空间树,每个结点代表一个子问题,每条分支表示对子问题的一种约束或决策。
- 限界:对每个子问题,计算一个上下界,即它可能取得的最优解的范围,如果某个子问题的最优解范围不可能优于当前已知的最优解,就剪枝(丢弃该子问题)。
流程:
- 定义问题的解空间
- 从根结点开始分支,产生子问题
- 对每个子问题计算限界
- 子问题放入候选结点表(队列/堆等)
- 从候选结点表中选择结点展开(搜索策略)
- 结点满足约束并且更优就继续分支,不满足就剪枝
- 重复直到候选结点表为空或者找到全局最优
题库:0-1背包问题。
双指针
描述:使用两个指针(索引)在数据结构中协同工作,一般分为同向指针或者相向指针。
滑动窗口也是一种双指针,左指针收缩窗口,右指针扩展窗口。
流程:
- 数据结构进行排序
- 边界
- 指针移动条件
- 重复元素处理
题库:LeetCode15,16,75,611, 2348。
前缀和
描述:高效处理区间和的技巧。
将区间求和的操作从O(n)优化为O(1),例如,求sum[l,r]的暴力解法是遍历[l,r];而预先处理prefix[i],sum[l,r]即为prefix[r]-prefix[l-1]。
构造时prefix[x]=prefix[x-1]+a[x]。
核心思想:预处理前缀和,换取查询效率;适合于频繁查询但不修改。
题库:LeetCode3147,3346。
差分数组
描述:高效处理区间加法更新的技巧,在区间的边界上做标记,最后通过前缀和恢复原数组。
前缀和的互补技巧。
构造时d[i]=a[i]-a[i-1],恢复时a[i]=a[i-1]+d[i]。
同理,例如将遍历[l,r]的数组区间,每个加x,转换为d[l]+=x和d[r+1]-=x。
题库:LeetCode3346。
结构
折半查找树
即二叉搜索树,Binary Search Tree,BST
每个结点最多有两个孩子,对于任一结点node:
左子树上的所有键小于node->key,右子树上的所有键大于node->key。
若树的高度为h,查找/插入/删除复杂度为O(h),遍历O(n)。
二叉树的遍历方式
包括深度优先遍历(DFS)和广度优先遍历(BFS)
1、深度优先遍历
按访问顺序不同,可分为前序遍历,中序遍历,后序遍历。
(1)前序遍历
顺序:根 -> 左子树 -> 右子树
可以用于复制树。
1 | void preorder(TreeNode* root) { |
(2)中序遍历
顺序:左子树 -> 根 ->右子树
对于二叉搜索树来说,中序遍历的结果就是升序。
1 | void inorder(TreeNode* root) { |
(3)后序遍历
顺序:左子树 -> 右子树 -> 根
删除树节点或者释放内存。
1 | void postorder(TreeNode* root) { |
2、广度优先遍历
又叫层序遍历,从根节点开始逐层从左到右访问。
输出层次结构,求出树的高度、最短路径等。
1 | void levelOrder(TreeNode* root) { |
有向图邻接表
常用的图存储结构,适合存储稀疏图(顶点多,边少)
- 有向图:每条边都有方向,比如
u->v表示从顶点u指向顶点v。 - 邻接表:为图中的每个顶点建立一个链表/动态数组,存储从该顶点出发的所有边。
1 | int n = 4; // 顶点个数 |
STL
ASCII
小写字母的ASCII码=大写字母的ASCII码+32。
正整数向上取整
方法:int div=(a+b-1)/b
如果a整除b,那么加上的b+1还不足以跨越到下一个整数;如果a不整除b,那么跨越下一个整数。
int最小数和最大数
INT_MIN和INT_MAX,分别为-231和231-1。
整数取模的最小非负整数
(x%m+m)%m,x如果为负数,加上m变为正数再取模;x为正数和0无影响。
swap()
格式:void swap(T& a,T& b)
时间复杂度和空间复杂度都是O(1)。
sort()
格式:void sort (RandomAccessIterator first, RandomAccessIterator last);
额外的:ranges::sort(xx)等价于std::(sort.begin(),sort.end())
注意:
- 迭代器范围:
[first, last)是一个左闭右开的区间,例如使用索引和迭代器结合时要用begin()+i和begin()+j+1。 - 性能:时间复杂度为O(nlogn)。
min_element()
格式:min_element(begin(),end())
作用:返回[begin,end)范围内最小的迭代器,可以在前面加解引用符号*,即为返回值。
示例:return *min_element(matrix.begin(),matrix.end())
lower_bound()
格式:auto it = lower_bound(xx.begin(),xx.end(),x)
作用:找到这个容器下第一个大于等于x的元素,并返回迭代器(要求容器有序)。
平均时间复杂度:O(log n)。
clamp()
格式:int x=clamp(value,lo,hi)
作用:返回区间[lo,hi]之间最接近value的值。
即为
min(max(value, lo), hi)
static
作用于局部变量时:使得变量的生命周期延长至整个程序运行期间,而不是随着函数调用结束而销毁。
作用于成员变量时:所有该类的对象共享同一个静态成员变量。
优先队列
特殊的队列,每个元素都有优先级,最先取出优先级最高的元素,在struct{};中提前声明。
核心实现:二叉堆(最大堆priority_queue/最小堆),堆顶是最大/最小元素。
格式:priority_queue<元素类型,底层容器类型,比较函数类型>
# 注:声明忽略后面两个参数时自动按照最大堆排列,即按照从前往后比较的最大元素的排列。
1 | // 初始化,char可以换成自定义的struct |
重载运算符:
# 一般const修饰,重载运算符不必修改对象,引用传递避免不必要的拷贝,提高效率,返回值必须为bool。
1 | // 最小堆,栈顶是优先级最小的,最大堆反之用<。 |
插入:push()(只能接受一个参数,必须是已经构造好的元素)或者emplace()
# 不支持随机访问修改
时间复杂度:插入和删除都是O(log n)。
Vector
长度:size()
在末尾添加一个元素:push_back(数组的一个数据)
直接在末尾构造对象:emplace_back(构造需要的数据),和push_back()区别不大,省去一次拷贝。
# 二维数组添加一维数组同理
释放原有资源并构造新的容器,属性和传进来的容器一致:新的容器=move(数组)
清空所有元素:clear()
是否为空矩阵:empty()
String
长度:length()
获取子字符串:substr(size_t pos,size_t len)
找到子字符或子字符串:find(),返回起始位置
# pos:子字符串的起始位置,默认0;len:提取的字符数量,默认一直提取到末尾
将字符串转换为整数:stoi(const string& s,size_t* idx,int base)
# s:需转换的字符串;idx:可选指针;base:可选的转换的进制
删除最后一个字符:pop_back()
# 字符串不能为空
位运算
计算整数中二进制为1的个数:popcount(T x)
# T:无符号的整数类型,例如unsigned int,unsigned long,uint64_t等;x:整数
模板类
<pair>
定义好的迷你结构体,将两个数据打包成一个整体。
1 | struct pair { |
T1和T2可以为其他不同类型,例如pair<int,string>,经常用于键值对,函数返回值,构造容器等。
<tuple>
固定大小,异质(可以是不同类型)的容器
构造:tuple<T1,T2,T3> myTuple(t1,t2,t3)
访问:T1 first=get<0>(myTuple),也可以修改
支持比较:比较的是字典序
无序/有序关联容器
无序:基于哈希表实现
1、哈希集合:unordered_set<key>
元素是唯一的,检查一个元素是否存在于集合当中
用法:
插入:
insert(key)查找:
find(key),返回的是迭代器,一般配合end()使用进行判断删除:
erase(key)
2、哈希表:unordered_map<key,value>
key是唯一的
用法:
- 插入:
insert({key,value}),或者map[key]=value
注意,
it指向pair<const Key, Value>的对象,不能通过it修改键,但可以修改值,it->first是键,it->second是值。- 访问:
map[key]
- 插入:
3、性能:
- 平均时间复杂度:插入、删除、查找都是O(1),但最坏情况可能是O(n)。
- 元素顺序:无序
有序:基于红黑树实现
红黑树是一种自平衡的二叉搜索树,保证了树的高度始终保持在 O(log n),从而确保了所有主要操作的时间复杂度都是稳定的 O(log n)。
有序集合:set<key>和有序表:map<key,value>
用法同哈希集合和哈希表。
性能:
- 平均时间复杂度:插入、删除、查找都是O(log n)
- 元素顺序:默认按键排列升序
List,Set,Map
| 特性 | List | Set | Map |
|---|---|---|---|
| 顺序 | 有序 | 无序 | 无序 |
| 可重复 | 可以 | 不可以 | 键不重复 |
| 访问方式 | 下标索引 | 无索引 | key查找 |
| 用途 | 保存序列 | 保存唯一元素 | 键值对应关系 |
链表
线性数据结构,链表中的元素在内存中不是连续存放的,通过指针将一系列节点连接起来,形成逻辑上的序列。
节点:
- 数据域:用于存储实际的数据(可以是整数、字符串、对象等)。
- 指针域:一个指向下一个节点的指针。
单向链表:
1 | struct ListNode { |
结构:
- 头节点:链表的第一个节点。通过一个指向头节点的指针(
head)来访问整个链表。如果链表为空,head指针为nullptr。 - 尾节点:链表的最后一个节点。它的
next指针指向nullptr。
方法:
- 创建链表:
1 | // 创建几个节点 |
- 遍历链表:
1 | void printList(ListNode* head) { |
- 头部插入节点(时间复杂度O(1)):
1 | ListNode* insertAtHead(ListNode* head, int val) { |
- 尾部插入节点(时间复杂度O(n)):
1 | ListNode* insertAtTail(ListNode* head, int val) { |
- 删除节点(时间复杂度O(n),已知位置就是O(1)):
1 | ListNode* deleteNode(ListNode* head, int val) { |
注意:
- 内存管理:在 C++ 中使用
new创建的节点,必须使用delete手动释放,否则会导致内存泄漏。 - 指针安全:操作指针时要格外小心,避免访问
nullptr或已删除的内存(野指针)。 - 边界条件:处理空链表、单节点链表等情况时要特别注意。
inline
内联函数,编译器会尝试用函数内的代码替换掉函数调用语句
传统函数调用过程:
- 执行到函数调用语句
- 将返回地址,参数等压入栈
- 跳转到函数体的内存地址开始执行
- 函数执行完毕,将返回值存入指定位置
- 跳转回之前保存的地址,继续执行调用处的下一条指令
这个过程对于功能简单,调用频繁的小函数,这种开销可能比函数本身执行的时间还长。
1 | inline return_type function_name(parameters) { |
优点:
- 减少调用开销
- 避免宏定义的缺点
Lambda表达式
1 | // lambda 递归函数例子1 |
# [&]表示能够访问外部作用域所有变量,this auto&&允许Lambda实现自递归,->int表示返回类型为整数,int & res表示修改这个值同时会修改对应的的memo的值,实现记忆化。