CSP-S 初赛数据结构知识文档(2)
2.2.3 数据结构
1. 线性结构
双端栈 【5】
- 特点:两个栈共享存储空间,可从两端进行push/pop操作
- 应用:特定场景下的高效操作
cpp
#define MAX 1000
int stack1[MAX], top1 = -1; // 左栈
int stack2[MAX], top2 = -1; // 右栈
void push1(int x) { stack1[++top1] = x; } // 左栈入栈
void push2(int x) { stack2[++top2] = x; } // 右栈入栈
int pop1() { return stack1[top1--]; } // 左栈出栈
int pop2() { return stack2[top2--]; } // 右栈出栈
双端队列 【5】
- 特点:两端都可进行插入和删除操作
- 时间复杂度 :所有操作 O ( 1 ) O(1) O(1)
cpp
int dq[MAX * 2], head = MAX, tail = MAX; // 中间开始存储
void push_front(int x) { dq[--head] = x; } // 前端插入
void push_back(int x) { dq[tail++] = x; } // 后端插入
int pop_front() { return dq[head++]; } // 前端删除
int pop_back() { return dq[--tail]; } // 后端删除
单调队列 【5】
- 特点:维护队列元素的单调性
- 应用:滑动窗口最值问题
cpp
int nums[MAX], q[MAX]; // q存储下标
int n, k; // 数组长度和窗口大小
void maxSlidingWindow() {
int head = 0, tail = 0;
for (int i = 0; i < n; i++) {
// 维护单调递减队列
while (head < tail && nums[q[tail-1]] <= nums[i]) tail--;
q[tail++] = i;
// 移除窗口外的元素
if (q[head] <= i - k) head++;
// 输出窗口最大值
if (i >= k - 1) printf("%d ", nums[q[head]]);
}
}
优先队列 【6】
- 特点:元素按优先级出队
- 实现:通常用二叉堆实现
cpp
int heap[MAX], size = 0; // 最小堆
void push(int x) {
heap[++size] = x;
// 上浮操作
for (int i = size; i > 1 && heap[i] < heap[i/2]; i /= 2)
swap(heap[i], heap[i/2]);
}
int pop() {
int res = heap[1]; // 堆顶元素
heap[1] = heap[size--];
// 下沉操作
for (int i = 1, j; i * 2 <= size; i = j) {
j = i * 2;
if (j + 1 <= size && heap[j+1] < heap[j]) j++;
if (heap[i] <= heap[j]) break;
swap(heap[i], heap[j]);
}
return res;
}
ST表(Sparse Table) 【6】
- 特点:静态区间查询,支持幂等性操作
- 预处理 : O ( n log n ) O(n \log n) O(nlogn)
- 查询 : O ( 1 ) O(1) O(1)
cpp
int st[MAX][20], log2[MAX]; // st[i][j]表示区间[i,i+2^j-1]的最小值
void init() {
// 预处理对数
log2[1] = 0;
for (int i = 2; i <= n; i++) log2[i] = log2[i/2] + 1;
// 动态规划预处理ST表
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
st[i][j] = min(st[i][j-1], st[i+(1<<(j-1))][j-1]);
}
int query(int l, int r) {
int k = log2[r - l + 1]; // 区间长度对数
return min(st[l][k], st[r-(1<<k)+1][k]); // 两个重叠区间取min
}
2. 集合与森林
并查集 【6】
- 特点:高效处理不相交集合的合并与查询
- 优化:路径压缩 + 按秩合并
cpp
int parent[MAX], rank[MAX]; // parent存储父节点,rank存储秩
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]); // 路径压缩
}
void unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
// 按秩合并
if (rank[x] < rank[y]) parent[x] = y;
else {
parent[y] = x;
if (rank[x] == rank[y]) rank[x]++;
}
}
树的孩子兄弟表示法 【6】
- 特点:用二叉链表表示多叉树
- 优点:统一了树和二叉树的存储结构
cpp
struct TreeNode {
int val;
int firstChild; // 第一个孩子节点下标
int nextSibling; // 下一个兄弟节点下标
} tree[MAX];
3. 特殊树
二叉堆 【6】
- 特点:完全二叉树,满足堆性质
- 操作 :插入 O ( log n ) O(\log n) O(logn),删除 O ( log n ) O(\log n) O(logn),取最值 O ( 1 ) O(1) O(1)
cpp
// 实现同优先队列
树状数组 【6】
- 特点:高效处理前缀和与单点更新
- 应用:逆序对计数等
cpp
int tree[MAX], n; // tree数组维护前缀和
int lowbit(int x) { return x & -x; } // 获取最低位的1
void update(int i, int delta) {
while (i <= n) {
tree[i] += delta;
i += lowbit(i); // 向上更新父节点
}
}
int query(int i) {
int res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i); // 向前查询前驱
}
return res;
}
线段树 【6】
- 特点:完全二叉树结构,支持区间查询和更新
- 空间 : O ( 4 n ) O(4n) O(4n)
cpp
int arr[MAX], tree[MAX * 4]; // tree数组存储线段树
// 建树
void build(int node, int start, int end) {
if (start == end) {
tree[node] = arr[start]; // 叶子节点
} else {
int mid = (start + end) / 2;
build(2*node, start, mid); // 左子树
build(2*node+1, mid+1, end); // 右子树
tree[node] = tree[2*node] + tree[2*node+1]; // 合并子节点
}
}
// 单点修改
void modify(int node, int start, int end, int idx, int val) {
if (start == end) {
arr[idx] = tree[node] = val; // 找到目标位置
} else {
int mid = (start + end) / 2;
if (idx <= mid) modify(2*node, start, mid, idx, val);
else modify(2*node+1, mid+1, end, idx, val);
tree[node] = tree[2*node] + tree[2*node+1]; // 更新父节点
}
}
// 区间查询
int query(int node, int start, int end, int l, int r) {
if (r < start || end < l) return 0; // 区间无交集
if (l <= start && end <= r) return tree[node]; // 完全包含
int mid = (start + end) / 2;
return query(2*node, start, mid, l, r) + // 查询左子树
query(2*node+1, mid+1, end, l, r); // 查询右子树
}
字典树(Trie树) 【6】
- 特点:多叉树结构,高效存储和查询字符串
- 应用:前缀匹配、词频统计
cpp
int trie[MAX][26], cnt = 1; // trie树节点
bool isEnd[MAX]; // 标记单词结束
void insert(char *s) {
int p = 0;
for (int i = 0; s[i]; i++) {
int c = s[i] - 'a';
if (!trie[p][c]) trie[p][c] = cnt++; // 新建节点
p = trie[p][c];
}
isEnd[p] = true; // 标记单词结束
}
bool search(char *s) {
int p = 0;
for (int i = 0; s[i]; i++) {
int c = s[i] - 'a';
if (!trie[p][c]) return false; // 不存在该字符
p = trie[p][c];
}
return isEnd[p]; // 检查是否是完整单词
}
笛卡尔树 【7】
- 特点:中序遍历为原序列,满足堆性质
- 构建 : O ( n ) O(n) O(n),使用单调栈
cpp
int stk[MAX], top = 0;
struct Node { int val, l, r; } tree[MAX];
void build(int *a, int n) {
for (int i = 0; i < n; i++) {
int k = top;
// 维护单调栈
while (k > 0 && a[stk[k-1]] > a[i]) k--;
// 设置父节点关系
if (k > 0) tree[stk[k-1]].r = i;
if (k < top) tree[i].l = stk[k];
stk[top = k++] = i; // 压入栈
}
}
平衡树 【8】
- 特点:保持树平衡,保证操作效率
- 类型:AVL、Treap、Splay等
cpp
// Treap实现(树堆)
struct Node {
int key, pri, size; // key为键值,pri为随机优先级
int l, r; // 左右子节点
} nodes[MAX];
int root, cnt; // 根节点和节点计数
// 更新节点大小
void update(int p) {
nodes[p].size = nodes[nodes[p].l].size + nodes[nodes[p].r].size + 1;
}
// 按key分裂
void split(int p, int key, int &x, int &y) {
if (!p) { x = y = 0; return; }
if (nodes[p].key <= key) {
x = p;
split(nodes[p].r, key, nodes[p].r, y);
} else {
y = p;
split(nodes[p].l, key, x, nodes[p].l);
}
update(p);
}
// 合并两个Treap
int merge(int x, int y) {
if (!x || !y) return x | y;
// 按优先级合并
if (nodes[x].pri > nodes[y].pri) {
nodes[x].r = merge(nodes[x].r, y);
update(x);
return x;
} else {
nodes[y].l = merge(x, nodes[y].l);
update(y);
return y;
}
}
4. 常见图
稀疏图 【5】
- 特点:边数远少于完全图
- 表示:邻接表更节省空间
cpp
int head[MAX], to[MAX], nxt[MAX], cnt = 1; // 邻接表
void addEdge(int u, int v) {
to[cnt] = v;
nxt[cnt] = head[u]; // 头插法
head[u] = cnt++;
}
偶图(二分图) 【6】
- 特点:顶点可分为两个不相交集合
- 判定:二分图染色法
cpp
int color[MAX]; // 0和1表示两种颜色
bool isBipartite(int u) {
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (color[v] == -1) {
color[v] = color[u] ^ 1; // 染相反颜色
if (!isBipartite(v)) return false;
} else if (color[v] == color[u]) {
return false; // 颜色冲突
}
}
return true;
}
欧拉图 【6】
- 特点:包含欧拉回路或欧拉路径
- 判定 :
- 欧拉回路:所有顶点度数为偶
- 欧拉路径:恰好两个顶点度数为奇
cpp
int degree[MAX]; // 记录每个顶点的度数
bool isEulerian() {
int odd = 0;
for (int i = 1; i <= n; i++)
if (degree[i] % 2) odd++;
return odd == 0 || odd == 2;
}
有向无环图 【6】
- 特点:无环,可拓扑排序
- 应用:任务调度、依赖关系
cpp
// 拓扑排序 (见后续实现)
连通图与强连通图 【7】
- 强连通分量:任意两点互相可达的最大子图
- 算法:Kosaraju、Tarjan
cpp
// Kosaraju算法
int vis[MAX], scc[MAX], scnt; // scc记录强连通分量编号
void dfs1(int u) {
vis[u] = 1;
for (int i = head[u]; i; i = nxt[i])
if (!vis[to[i]]) dfs1(to[i]);
stk[++top] = u; // 按结束时间入栈
}
void dfs2(int u) {
scc[u] = scnt; // 标记强连通分量
for (int i = rhead[u]; i; i = rnxt[i])
if (!scc[rto[i]]) dfs2(rto[i]);
}
void kosaraju() {
// 第一次DFS确定结束顺序
for (int i = 1; i <= n; i++)
if (!vis[i]) dfs1(i);
// 第二次DFS在逆图上处理
while (top) {
int u = stk[top--];
if (!scc[u]) { scnt++; dfs2(u); }
}
}
双连通图 【7】
- 割点:删除后图连通分量增加
- 算法:Tarjan算法
cpp
int dfn[MAX], low[MAX], idx;
bool cut[MAX]; // 标记割点
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++idx;
int child = 0;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (!dfn[v]) {
child++;
tarjan(v, u);
low[u] = min(low[u], low[v]);
// 判断割点
if (low[v] >= dfn[u]) cut[u] = true;
} else if (v != fa) {
low[u] = min(low[u], dfn[v]);
}
}
// 根节点特殊处理
if (fa == 0 && child == 1) cut[u] = false;
}
5. 哈希表
数值哈希函数构造 【5】
- 常用方法 :
- 除法哈希: h ( k ) = k m o d m h(k) = k \mod m h(k)=kmodm
- 乘法哈希: h ( k ) = ⌊ m ( k A m o d 1 ) ⌋ h(k) = \lfloor m(kA \mod 1) \rfloor h(k)=⌊m(kAmod1)⌋, A ≈ ( 5 − 1 ) / 2 A \approx (\sqrt{5}-1)/2 A≈(5 −1)/2
cpp
int hash(int key) {
return (key % MOD + MOD) % MOD; // 处理负数
}
字符串哈希函数构造 【6】
- 多项式哈希:
H ( S ) = ∑ i = 0 n − 1 s [ i ] ⋅ p i m o d m H(S) = \sum_{i=0}^{n-1} s[i] \cdot p^i \mod m H(S)=i=0∑n−1s[i]⋅pimodm
cpp
#define BASE 131
#define MOD 1000000007
char s[MAX];
int h[MAX], p[MAX]; // h存储哈希值,p存储BASE的幂
void init() {
p[0] = 1;
h[0] = s[0];
for (int i = 1; s[i]; i++) {
p[i] = (long long)p[i-1] * BASE % MOD;
h[i] = ((long long)h[i-1] * BASE + s[i]) % MOD;
}
}
int getHash(int l, int r) {
if (l == 0) return h[r];
// 计算子串哈希
return ((h[r] - (long long)h[l-1] * p[r-l+1] % MOD) + MOD) % MOD;
}
哈希冲突的常用处理方法 【6】
- 链地址法:冲突元素组成链表
- 开放定址法:线性探测、平方探测等
cpp
// 链地址法
struct Node {
int key, val;
Node *next;
} *hashTable[MOD];
void insert(int key, int val) {
int idx = hash(key);
Node *p = new Node{key, val, hashTable[idx]};
hashTable[idx] = p;
}
int find(int key) {
int idx = hash(key);
for (Node *p = hashTable[idx]; p; p = p->next)
if (p->key == key) return p->val;
return -1;
}
// 开放定址法
int hashTable[MOD];
int find(int key) {
int idx = hash(key);
while (hashTable[idx] != -1 && hashTable[idx] != key)
idx = (idx + 1) % MOD; // 线性探测
return idx;
}
复杂度总结
数据结构 | 查询 | 插入 | 删除 | 空间 |
---|---|---|---|---|
树状数组 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) | - | O ( n ) O(n) O(n) |
线段树 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) | O ( n ) O(n) O(n) |
并查集 | O ( α ( n ) ) O(\alpha(n)) O(α(n)) | O ( α ( n ) ) O(\alpha(n)) O(α(n)) | O ( α ( n ) ) O(\alpha(n)) O(α(n)) | O ( n ) O(n) O(n) |
哈希表 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
平衡树 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) | O ( n ) O(n) O(n) |
注: α ( n ) \alpha(n) α(n)为反阿克曼函数,通常小于5
CSP-S 提高组初赛复习大纲
更新时间:2025 年 9 月 18 日 23:21:08
- 2.2.1 基础知识与编程环境 & C++ 程序设计
- 2.2.3 数据结构
- 2.2.4 算法
- 2.2.5 数学与其他