数据结构
一、线性数据结构
1.1 栈
基本概念
栈是一种后进先出(LIFO)的数据结构,支持在栈顶进行插入和删除操作。
基本操作
push(x):将元素 \(x\) 压入栈顶pop():弹出栈顶元素top():返回栈顶元素empty():判断栈是否为空
时间复杂度
所有操作均为 \(O(1)\)
应用场景
- 表达式求值(中缀转后缀)
- 括号匹配
- 函数调用栈
- 浏览器前进后退
例题1.1:括号匹配
问题描述:给定一个只包含 '('、')'、'['、']'、'{'、'}' 的字符串,判断字符串是否有效。
输入格式:一个字符串 \(s\),长度不超过 \(10^5\)。
输出格式:如果字符串有效,输出 "YES",否则输出 "NO"。
解题思路:
- 遍历字符串,遇到左括号则入栈
- 遇到右括号时,检查栈顶是否匹配
- 最后检查栈是否为空
- 时间复杂度:\(O(n)\)
例题1.2:直方图中最大矩形
问题描述:给定 \(n\) 个非负整数,表示直方图中每个柱子的高度。每个柱子的宽度为 1。求直方图中最大矩形的面积。
输入格式:
- 第一行整数 \(n\)
- 第二行 \(n\) 个整数 \(h_1, h_2, ..., h_n\)
输出格式:一个整数,表示最大矩形面积。
解题思路:
- 使用单调栈维护递增的高度
- 对于每个柱子,找到它左边和右边第一个比它矮的柱子
- 以当前柱子为高的最大矩形宽度为右边界-左边界-1
- 时间复杂度:\(O(n)\)
1.2 队列
基本概念
队列是一种先进先出(FIFO)的数据结构,支持在队尾插入,队头删除。
基本操作
push(x):将元素 \(x\) 插入队尾pop():删除队头元素front():返回队头元素empty():判断队列是否为空
时间复杂度
所有操作均为 \(O(1)\)
应用场景
- BFS遍历
- 缓存管理
- 任务调度
- 滑动窗口
例题1.3:滑动窗口最大值
问题描述:给定一个数组 \(nums\) 和滑动窗口的大小 \(k\),窗口从最左边滑动到最右边,每次向右滑动一个位置。求每个窗口中的最大值。
输入格式:
- 第一行两个整数 \(n, k\)
- 第二行 \(n\) 个整数 \(nums_1, nums_2, ..., nums_n\)
输出格式:\(n-k+1\) 个整数,表示每个窗口的最大值。
解题思路:
- 使用双端队列维护递减序列
- 队列中存储下标,保证队头元素在窗口内
- 每次窗口滑动,移除队头过期元素,加入新元素时移除比它小的元素
- 时间复杂度:\(O(n)\)
1.3 链表
基本概念
链表是由节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。
类型
- 单向链表
- 双向链表
- 循环链表
基本操作
- 插入:\(O(1)\)(已知位置)
- 删除:\(O(1)\)(已知位置)
- 查找:\(O(n)\)
例题1.4:LRU缓存
问题描述:设计并实现一个 LRU(最近最少使用)缓存机制。
操作要求:
get(key):如果密钥存在则获取值,否则返回 -1put(key, value):如果密钥不存在则插入,当缓存达到容量时删除最久未使用的数据
解题思路:
- 使用哈希表+双向链表
- 哈希表实现 \(O(1)\) 查找
- 双向链表维护访问顺序
- 最近访问的节点移到链表头部
- 时间复杂度:所有操作 \(O(1)\)
二、树形数据结构
2.1 二叉树
基本概念
二叉树是每个节点最多有两个子树的树结构,子树有左右之分。
遍历方式
- 前序遍历:根→左→右
- 中序遍历:左→根→右
- 后序遍历:左→右→根
- 层序遍历:按层遍历
特殊二叉树
- 完全二叉树
- 满二叉树
- 二叉搜索树(BST)
- 平衡二叉树(AVL)
例题2.1:二叉搜索树操作
问题描述:实现二叉搜索树的插入、删除、查找操作。
操作要求:
- 插入:插入新节点,保持BST性质
- 删除:删除指定节点,保持BST性质
- 查找:查找指定值是否存在
- 查找第k小元素
解题思路:
- 插入:递归找到合适位置插入
- 删除:分三种情况处理(无子节点、一个子节点、两个子节点)
- 查找:二分查找
- 第k小:中序遍历计数
- 时间复杂度:平均 \(O(\log n)\),最坏 \(O(n)\)
2.2 堆
基本概念
堆是一种完全二叉树,满足堆性质:
- 最大堆:父节点值 ≥ 子节点值
- 最小堆:父节点值 ≤ 子节点值
基本操作
push(x):插入元素,\(O(\log n)\)pop():删除堆顶,\(O(\log n)\)top():获取堆顶,\(O(1)\)empty():判断是否为空,\(O(1)\)
应用场景
- 优先队列
- 堆排序
- Top-K问题
- Dijkstra算法
例题2.2:合并果子
问题描述:有 \(n\) 堆果子,每堆果子重量为 \(a_i\)。每次可以合并任意两堆果子,消耗体力为两堆果子重量之和。求将所有果子合并成一堆的最小体力消耗。
输入格式:
- 第一行整数 \(n\)
- 第二行 \(n\) 个整数 \(a_1, a_2, ..., a_n\)
输出格式:一个整数,表示最小体力消耗。
解题思路:
- 使用最小堆(优先队列)
- 每次取出最小的两堆合并,将合并后的结果放回堆中
- 重复直到只剩一堆
- 时间复杂度:\(O(n \log n)\)
2.3 并查集
基本概念
并查集用于处理不相交集合的合并与查询问题。
基本操作
find(x):查找元素所在集合的代表元素union(x, y):合并两个元素所在集合- 路径压缩:使树更扁平,提高查找效率
- 按秩合并:小树合并到大树
时间复杂度
- 平均:近似 \(O(1)\)
- 最坏:\(O(\alpha(n))\),其中 \(\alpha(n)\) 是反阿克曼函数
应用场景
- 连通分量
- Kruskal算法
- 动态连通性
- 离线查询
例题2.3:朋友圈
问题描述:有 \(n\) 个人,给定 \(m\) 个朋友关系。朋友关系具有传递性。求朋友圈的数量。
输入格式:
- 第一行两个整数 \(n, m\)
- 接下来 \(m\) 行,每行两个整数 \(u, v\) 表示 \(u\) 和 \(v\) 是朋友
输出格式:一个整数,表示朋友圈数量。
解题思路:
- 使用并查集维护朋友关系
- 初始时每个人都是一个独立集合
- 对于每个朋友关系,合并两人所在集合
- 最后统计不同集合的数量
- 时间复杂度:\(O(m \alpha(n))\)
三、高级树形结构
3.1 线段树
基本概念
线段树是一种二叉树,每个节点代表一个区间,用于处理区间查询和更新问题。
支持操作
- 单点修改:\(O(\log n)\)
- 区间查询:\(O(\log n)\)
- 区间修改:\(O(\log n)\)(需要懒标记)
结构特点
- 叶子节点:长度为1的区间
- 内部节点:左右子区间的并集
- 完全二叉树结构
例题3.1:区间最大值
问题描述:给定 \(n\) 个数的序列,支持两种操作:
- 将第 \(i\) 个数修改为 \(x\)
- 查询区间 \([l, r]\) 的最大值
输入格式:
- 第一行两个整数 \(n, m\)
- 第二行 \(n\) 个整数
- 接下来 \(m\) 行,每行表示一个操作
输出格式:对于每个查询操作,输出结果。
解题思路:
- 建立线段树,每个节点存储区间最大值
- 单点修改:更新叶子节点,向上更新父节点
- 区间查询:递归查询,合并左右子树结果
- 时间复杂度:每次操作 \(O(\log n)\)
例题3.2:区间加与区间求和
问题描述:给定 \(n\) 个数的序列,支持两种操作:
- 区间 \([l, r]\) 中每个数加上 \(x\)
- 查询区间 \([l, r]\) 的和
解题思路:
- 使用带懒标记的线段树
- 懒标记记录区间需要加的值,不下传到叶子节点
- 查询和更新时根据懒标记进行计算
- 时间复杂度:每次操作 \(O(\log n)\)
3.2 树状数组
基本概念
树状数组(Fenwick Tree)是一种支持单点修改和前缀查询的数据结构。
核心操作
update(i, x):将第 \(i\) 个元素加上 \(x\)query(i):查询前 \(i\) 个元素的和
特点
- 代码简洁
- 常数小
- 空间复杂度 \(O(n)\)
- 不支持区间修改(可通过差分支持)
时间复杂度
所有操作 \(O(\log n)\)
例题3.3:逆序对数量
问题描述:给定一个整数序列,求逆序对的数量。
输入格式:
- 第一行整数 \(n\)
- 第二行 \(n\) 个整数
输出格式:一个整数,表示逆序对数量。
解题思路:
- 离散化:将原数组映射到 \([1, n]\) 的区间
- 从后往前遍历,用树状数组统计每个数后面有多少个比它小的数
- 时间复杂度:\(O(n \log n)\)
3.3 平衡树
基本概念
平衡树是一种自平衡的二叉搜索树,保证树的高度为 \(O(\log n)\)。
常见类型
- AVL树:严格平衡,旋转操作多
- 红黑树:近似平衡,工程常用
- Treap:随机平衡,实现简单
- Splay:通过伸展操作平衡
- 替罪羊树:基于重构的平衡树
支持操作
- 插入、删除、查找:\(O(\log n)\)
- 前驱、后继:\(O(\log n)\)
- 第k大:\(O(\log n)\)
- 排名:\(O(\log n)\)
例题3.4:普通平衡树
问题描述:实现一种数据结构,支持以下操作:
- 插入 \(x\)
- 删除 \(x\)
- 查询 \(x\) 的排名
- 查询排名为 \(k\) 的数
- 求 \(x\) 的前驱
- 求 \(x\) 的后继
解题思路:
- 使用Treap或Splay实现
- 每个节点存储值、子树大小、随机优先级
- 通过旋转维护平衡性
- 时间复杂度:所有操作 \(O(\log n)\)
四、字符串数据结构
4.1 Trie树
基本概念
Trie树(字典树)是一种用于字符串检索的多叉树结构。
特点
- 根节点不包含字符
- 从根到某一节点的路径表示一个字符串
- 节点存储额外信息(如出现次数、是否为单词结尾)
应用场景
- 字符串检索
- 前缀匹配
- 自动补全
- 词频统计
例题4.1:最大异或对
问题描述:给定 \(n\) 个整数 \(a_1, a_2, ..., a_n\),求两个数异或的最大值。
输入格式:
- 第一行整数 \(n\)
- 第二行 \(n\) 个整数
输出格式:一个整数,表示最大异或值。
解题思路:
- 将每个数看作32位二进制串
- 建立二进制Trie树
- 对于每个数,在Trie中找使异或值最大的路径
- 时间复杂度:\(O(n \log C)\),其中 \(C\) 是值域
4.2 后缀数组
基本概念
后缀数组是对字符串所有后缀进行排序后得到的数组。
核心概念
- \(SA[i]\):排名第 \(i\) 的后缀起始位置
- \(rank[i]\):以 \(i\) 开头的后缀的排名
- \(height[i]\):\(SA[i]\) 和 \(SA[i-1]\) 的最长公共前缀
应用场景
- 最长重复子串
- 不同子串数量
- 字符串匹配
- 回文子串
例题4.2:不同子串个数
问题描述:给定一个字符串,求所有不同子串的个数。
输入格式:一个字符串 \(s\)
输出格式:一个整数,表示不同子串个数。
解题思路:
- 构建后缀数组和height数组
- 所有子串个数为 \(\frac{n(n+1)}{2}\)
- 重复子串个数为 \(\sum_{i=1}^n height[i]\)
- 不同子串个数 = 总子串数 - 重复子串数
- 时间复杂度:\(O(n \log n)\)
4.3 自动机
类型
- KMP自动机:单模式匹配
- AC自动机:多模式匹配
- 后缀自动机:子串相关问题
例题4.3:关键词搜索
问题描述:给定一个文本串 \(T\) 和 \(n\) 个模式串 \(P_i\),求每个模式串在文本串中出现的次数。
输入格式:
- 第一行文本串 \(T\)
- 第二行整数 \(n\)
- 接下来 \(n\) 行,每行一个模式串
输出格式:\(n\) 行,每行一个整数表示出现次数。
解题思路:
- 构建AC自动机
- 将模式串插入Trie树,构建失败指针
- 在文本串上匹配,统计出现次数
- 时间复杂度:\(O(\sum |P_i| + |T|)\)
五、可持久化数据结构
5.1 可持久化线段树
基本概念
可持久化线段树(主席树)可以访问所有历史版本,每次修改创建新节点。
特点
- 空间复杂度:\(O(n + m \log n)\)
- 每次修改只创建 \(O(\log n)\) 个新节点
- 支持区间第k大查询
例题5.1:区间第k小
问题描述:给定 \(n\) 个数的序列,\(m\) 次询问,每次询问区间 \([l, r]\) 中第 \(k\) 小的数。
输入格式:
- 第一行两个整数 \(n, m\)
- 第二行 \(n\) 个整数
- 接下来 \(m\) 行,每行三个整数 \(l, r, k\)
输出格式:对于每个询问,输出结果。
解题思路:
- 离散化原数组
- 建立权值线段树的可持久化版本
- 第 \(i\) 个版本表示前 \(i\) 个数建立的线段树
- 区间 \([l, r]\) 的信息 = 版本 \(r\) - 版本 \(l-1\)
- 在权值线段树上二分查找第k小
- 时间复杂度:\(O((n+m) \log n)\)
5.2 可持久化Trie
应用场景
- 区间最大异或
- 区间mex查询
- 可持久化字典
例题5.2:区间最大异或
问题描述:给定 \(n\) 个数的序列,\(m\) 次询问,每次询问区间 \([l, r]\) 中选一个数与 \(x\) 异或的最大值。
输入格式:
- 第一行两个整数 \(n, m\)
- 第二行 \(n\) 个整数
- 接下来 \(m\) 行,每行三个整数 \(l, r, x\)
输出格式:对于每个询问,输出结果。
解题思路:
- 建立可持久化Trie,第 \(i\) 个版本表示前 \(i\) 个数
- 对于查询 \([l, r]\),使用版本 \(r\) 减去版本 \(l-1\) 得到区间Trie
- 在区间Trie中找与 \(x\) 异或最大的数
- 时间复杂度:\(O((n+m) \log C)\)
六、特殊数据结构
6.1 单调栈/队列
基本概念
栈/队列中元素保持单调性,用于解决滑动窗口、下一个更大元素等问题。
例题6.1:接雨水
问题描述:给定 \(n\) 个非负整数表示每个柱子的高度,计算按此排列的柱子能接多少雨水。
输入格式:\(n\) 个整数
输出格式:一个整数,表示雨水总量。
解题思路:
- 单调栈法:栈中存储递减的高度
- 当当前高度大于栈顶时,可以形成凹槽接水
- 计算凹槽宽度和高度,累加雨水
- 时间复杂度:\(O(n)\)
6.2 分块
基本概念
将序列分成若干块,块内暴力,块间预处理,平衡复杂度。
特点
- 简单易实现
- 灵活性高
- 时间复杂度:\(O(\sqrt{n})\) 级别
例题6.2:区间加法与区间求和
问题描述:给定 \(n\) 个数的序列,支持区间加法和区间求和。
解题思路:
- 将序列分成 \(\sqrt{n}\) 大小的块
- 每个块维护块内和、加法标记
- 区间操作:
- 完整块:更新标记和块内和
- 不完整块:暴力更新
- 查询时考虑标记
- 时间复杂度:\(O(\sqrt{n})\) 每次操作
6.3 树套树
基本概念
在树状数组或线段树的每个节点上再建立一棵树,用于解决二维问题。
常见类型
- 树状数组套线段树
- 线段树套平衡树
- 二维线段树
例题6.3:二维数点
问题描述:平面上有 \(n\) 个点,\(m\) 次询问,每次询问矩形内点的数量。
输入格式:
- 第一行两个整数 \(n, m\)
- 接下来 \(n\) 行,每行两个整数 \((x, y)\) 表示点坐标
- 接下来 \(m\) 行,每行四个整数 \((x_1, y_1, x_2, y_2)\) 表示矩形
输出格式:对于每个询问,输出结果。
解题思路:
- 离散化 \(y\) 坐标
- 建立树状数组套线段树
- 树状数组维护 \(x\) 维度,每个节点维护 \(y\) 维度的线段树
- 查询时在树状数组上求前缀和
- 时间复杂度:\(O((n+m) \log^2 n)\)
总结
数据结构选择指南
| 问题类型 | 推荐数据结构 | 时间复杂度 |
|---|---|---|
| 单点修改,区间查询 | 树状数组/线段树 | \(O(\log n)\) |
| 区间修改,区间查询 | 线段树(带懒标记) | \(O(\log n)\) |
| 第k大查询 | 平衡树/主席树 | \(O(\log n)\) |
| 字符串匹配 | AC自动机/KMP | \(O(n+m)\) |
| 区间最值 | 线段树/ST表 | \(O(\log n)/O(1)\) |
| 离线查询 | 莫队算法 | \(O(n\sqrt{n})\) |
| 二维问题 | 树套树/二维树状数组 | \(O(\log^2 n)\) |
学习建议
- 掌握基础:先熟练掌握数组、链表、栈、队列等基础结构
- 理解原理:理解每个数据结构的设计思想和适用场景
- 熟练模板:掌握线段树、树状数组、平衡树等常用数据结构的实现
- 灵活应用:学会根据问题特点选择合适的数据结构
- 进阶技巧:学习可持久化、树套树、分块等高级技术
常见考点
- 数据结构的选择与组合
- 空间复杂度的优化
- 时间复杂度的分析
- 边界条件的处理
- 代码实现的细节
数据结构是OI竞赛的核心内容,需要大量的练习来掌握。建议从基础题开始,逐步提高难度,最终能够解决复杂的数据结构问题。

浙公网安备 33010602011771号