从数组到数据结构:为什么不能只用数组
前面我们已经会用数组存一排数字了。但竞赛题里经常遇到这种情况:你要不断往末尾加元素,还要从开头删元素;或者要在任意位置插入一个数。用普通数组做这些事会非常慢——删第一个元素要把后面所有元素往前挪一格。
这就是数据结构存在的原因:它是一套专门为某种操作优化的存储方式。栈擅长"在末尾加、从末尾删",队列擅长"在末尾加、从开头删",链表擅长"在任意位置插入删除"。竞赛题看到操作模式,就要立刻想到用哪种结构。
选对数据结构,问题就解决了一半。选错了,再聪明的算法也会超时。
- 栈后进先出,像叠盘子,只能从顶上拿。
- 队列先进先出,像排队买票,先来的先服务。
- 链表用指针把节点串起来,插入删除不用挪元素。
- STLC++ 标准库已经实现好了,直接用 stack/queue/list。
栈:先进后出的一摞盘子
想象你往桌子上叠盘子:每次只能从顶上放新盘子(push),也只能从顶上拿盘子(pop)。最后放上去的盘子最先被拿下来——这就是栈(Stack),先进后出(LIFO)。
用数组模拟一个栈:
int stack[1005];
int top = 0; // 栈顶指针,指向下一个空位
void push(int x) {
stack[top++] = x;
}
int pop() {
return stack[--top];
}
bool isEmpty() {
return top == 0;
}
top 是栈顶指针。push 时把 x 放到 stack[top],然后 top 加 1;pop 时先把 top 减 1,再返回 stack[top]。整个栈就两个操作,非常简单。
栈实战:括号匹配问题
经典题:给一个只包含 `(`、`)`、`[`、`]`、`{`、`}` 的字符串,判断括号是否匹配。比如 `()` 匹配,`([)]` 不匹配。
思路:从左到右扫描字符串。遇到左括号就 push 进栈;遇到右括号,看栈顶是不是对应的左括号——是就 pop,不是就说明不匹配。扫描完之后栈必须为空。
#include <stack>
#include <string>
#include <iostream>
using namespace std;
int main() {
string s;
cin >> s;
stack<char> st;
bool ok = true;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
if (st.empty()) { ok = false; break; }
char top = st.top();
if ((c == ')' && top == '(') ||
(c == ']' && top == '[') ||
(c == '}' && top == '{')) {
st.pop();
} else { ok = false; break; }
}
}
if (ok && st.empty()) cout << "YES";
else cout << "NO";
return 0;
}
这是栈最经典的应用。括号匹配、表达式求值、递归调用的函数栈,底层都是同一个结构。
队列:先进先出的一条队伍
排队买票时,先来的人先买到票,后来的人排在队尾。这就是队列(Queue),先进先出(FIFO)。
用数组模拟一个队列,需要两个指针:head 指向队首,tail 指向队尾的下一个空位。
int q[1005];
int head = 0, tail = 0;
void push(int x) {
q[tail++] = x;
}
int pop() {
return q[head++];
}
bool isEmpty() {
return head == tail;
}
入队 push 往 tail 方向加,出队 pop 从 head 方向拿。head 永远跟着 tail 往前走。这种简单实现有个问题:数组越用越短,前面的空间浪费了。竞赛里可以用循环队列解决,入门阶段先用简单版即可。
队列实战:BFS 广度优先搜索入门
队列最经典的应用是 BFS(广度优先搜索)。想象你在一个迷宫里,要找从起点到终点的最短路径。BFS 的思路是:从起点开始,一圈一圈往外扩散,先访问离起点 1 步的所有格子,再访问 2 步的,直到撞到终点。
#include <queue>
using namespace std;
queue<pair<int,int>> q;
int dist[105][105];
bool visited[105][105];
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};
void bfs(int sx, int sy) {
q.push({sx, sy});
visited[sx][sy] = true;
dist[sx][sy] = 0;
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (!visited[nx][ny]) {
visited[nx][ny] = true;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
}
}
这里 `queue<pair<int,int>>` 就是一个存坐标的队列。每次从队首拿出一个格子,把它四周没访问过的邻居放进队尾。先放进去的先处理,保证按距离扩散。
- 队列BFS 的核心容器。
- visited记录已经访问过的格子,避免重复。
- dist记录起点到每个格子的最短距离。
- 方向数组四个方向 dx/dy 循环遍历。
链表:动态拼接的一串节点
数组的缺点是大小固定。如果你不知道要存多少个元素,就要开一个很大的数组浪费空间。链表解决了这个问题:每个节点是一个结构体,里面存数据和一个指向下一个节点的指针。
struct Node {
int data;
Node* next;
};
Node* head = nullptr;
void insert(int x) {
Node* n = new Node;
n->data = x;
n->next = head;
head = n;
}
每次 insert 就在链表头插一个新节点,next 指向原来的 head。链表的好处是插入删除不需要挪动其他元素,只要改指针。缺点是不能像数组那样随机访问第 k 个元素——必须从头一个一个往后找。
竞赛里纯手写链表的题不多,因为 STL 的 `list` 已经实现好了。但理解链表的原理对后面学树和图非常重要——树和图本质上就是更复杂的节点连接结构。
STL 里的 stack、queue 与 list
实际竞赛中,几乎不用自己手写栈队列链表。C++ 标准库 STL 已经提供好了,直接 `#include` 就能用。
| STL 容器 | 对应结构 | 常用操作 |
|---|---|---|
| stack<T> | 栈 | push、pop、top、empty |
| queue<T> | 队列 | push、pop、front、back、empty |
| list<T> | 双向链表 | push_back、push_front、insert、erase |
| deque<T> | 双端队列 | 两端都能 push/pop |
入门阶段重点掌握 stack 和 queue。list 在竞赛里用得少,因为 vector 大多数时候更方便。
学习建议:先按本文手撸一遍数组版栈队列,理解底层原理;再切换到 STL,感受"调包"的爽快感。这样以后遇到 STL 内部的题目(比如让你用两个栈实现队列),你才不会懵。
- 先手写:用数组和指针把栈队列链表写一遍。
- 再用 STL:竞赛实战用 STL 容器。
- 最后学原理:知道 STL 底层是怎么实现的,进阶更快。
数据结构是算法的基石。栈队列链表搞明白,后面学树、图、哈希表都会顺很多。