一、线性表基础定义

线性表是n 个数据元素的有限序列,元素之间满足「一对一」的前驱后继关系,除首尾元素外,每个元素有且仅有一个前驱和一个后继。

本文聚焦线性表的四大核心实现:数组 / Vector、栈、队列、链表,从底层原理、工程模板、典型应用、避坑要点四个维度系统梳理,内容均为可直接落地的干货。


二、数组与动态数组(Vector)

1. 底层原理

  • 静态数组:编译期分配连续内存,长度固定,支持 O(1) 随机访问,但无法动态扩容。
  • 动态数组(std::vector):C++ 标准库实现的动态数组,底层为连续内存,支持自动扩容(扩容因子通常为 1.5/2 倍),兼顾随机访问与动态存储。

2. 核心 API 与时间复杂度

操作接口时间复杂度说明
尾插push_back(x)均摊 O (1)扩容时为 O (n),均摊后为常数
尾删pop_back()O(1)仅修改尾指针,无内存释放
随机访问v[i] / at(i)O(1)at() 会做越界检查,[] 无检查
插入(指定位置)insert(pos, x)O(n)需移动后续元素腾出位置
删除(指定位置)erase(pos)O(n)需移动后续元素覆盖删除位置
扩容优化reserve(n)O(1)预分配内存,避免多次扩容开销

3. 工程模板(含二维数组)

#include <vector>
using namespace std;

// 一维vector基础操作
vector<int> v;
v.reserve(100);  // 预分配100个元素空间,优化扩容
v.push_back(1);  // 尾插
v.pop_back();    // 尾删
for (int x : v) { /* 范围for遍历 */ }

// 二维vector(模拟表格/矩阵)
vector<vector<int>> grid(5, vector<int>(5, 0)); // 5×5初始化为0
grid[0][1] = 10; // 随机访问二维元素

4. 典型应用与避坑

  • 应用场景:点名问题、寄包柜模拟、批量数据存储与遍历、矩阵运算。
  • 避坑要点:
    1. 越界访问:v[i] 需保证 i < v.size(),否则行为未定义。
    2. 迭代器失效:insert/erase 操作会使后续迭代器失效,需更新迭代器。
    3. 扩容开销:频繁 push_back 大数据时,务必先 reserve 预分配内存。

三、栈(Stack)

1. 底层原理

栈是 受限线性表,遵循「后进先出(LIFO)」原则,仅允许在 栈顶 进行插入(入栈)和删除(出栈)操作,底层可由数组 / Vector 或链表实现。

2. 核心 API 与时间复杂度

操作接口时间复杂度说明
入栈push(x)O(1)栈顶插入
出栈pop()O(1)栈顶删除,无返回值
取栈顶top()O(1)仅读取栈顶元素
判空empty()O(1)判断栈中是否有元素

3. 手动实现模板(数组版・静态)

const int MAXN = 1005;
int st[MAXN];
int top = 0; // 栈顶指针,初始为0表示空栈

// 入栈
void push(int x) {
    st[++top] = x;
}

// 出栈
void pop() {
    if (!empty()) top--;
}

// 取栈顶
int get_top() {
    return st[top];
}

// 判空
bool empty() {
    return top == 0;
}

4. 动态数组实现栈(Vector・推荐)

优点:无容量上限、自动扩容、代码极简、竞赛 / 教学首选

#include <vector>
using namespace std;

vector<int> st;

// 入栈
void push(int x) {
    st.push_back(x);
}

// 出栈
void pop() {
    if (!st.empty())
        st.pop_back();
}

// 获取栈顶
int top() {
    return st.back();
}

// 判空
bool empty() {
    return st.empty();
}

// 获取元素个数
int size() {
    return st.size();
}

5. 典型应用与避坑

  • 应用场景:
    1. 括号匹配:判断字符串中括号是否成对合法。
    2. 后缀表达式求值:逆波兰表达式的计算核心结构。
    3. 递归转非递归:模拟函数调用栈,实现 DFS 等递归算法。
  • 避坑要点:
    1. 空栈访问:top()/pop() 前必须判断 !empty(),否则程序崩溃。
    2. 栈溢出:数组实现的栈需控制最大容量,避免栈顶指针越界。

四、队列(Queue)

1. 底层原理

队列是 受限线性表,遵循「先进先出(FIFO)」原则,仅允许在 队尾插入(入队)、队头删除(出队),底层可由数组 / Vector 或链表实现。

2. 核心 API 与时间复杂度

操作接口时间复杂度说明
入队push(x)O(1)队尾插入
出队pop()O(1)队头删除,无返回值
取队头front()O(1)读取队头元素
取队尾back()O(1)读取队尾元素
判空empty()O(1)判断队列是否为空

3. 手动实现模板(循环队列版・静态)

const int MAXN = 1005;
int q[MAXN];
int front = 0, rear = 0;

// 入队
bool push(int x) {
    if ((rear + 1) % MAXN == front) return false;
    rear = (rear + 1) % MAXN;
    q[rear] = x;
    return true;
}

// 出队
bool pop() {
    if (empty()) return false;
    front = (front + 1) % MAXN;
    return true;
}

// 判空
bool empty() {
    return front == rear;
}

// 取队头
int get_front() {
    return q[(front + 1) % MAXN];
}

4. 动态数组实现队列(Vector 版)

适合教学演示,直观易懂

#include <vector>
using namespace std;

vector<int> q;

// 队尾入队
void push(int x) {
    q.push_back(x);
}

// 队头出队
void pop() {
    if (!q.empty())
        q.erase(q.begin());
}

// 获取队头
int front() {
    return q[0];
}

// 获取队尾
int back() {
    return q.back();
}

// 判空
bool empty() {
    return q.empty();
}

5. 高效动态队列(deque 版・工程推荐)

#include <deque>
using namespace std;

deque<int> q;

// 入队 O(1)
q.push_back(x);
// 出队 O(1)
q.pop_front();
// 队头
q.front();
// 队尾
q.back();

6. 典型应用与避坑

  • 应用场景:
    1. 广度优先搜索(BFS):树 / 图的层序遍历核心结构。
    2. 模拟排队问题:超市结账、进程调度、消息队列。
    3. 约瑟夫问题:循环队列的经典应用。
  • 避坑要点:
    1. 空队列访问:front()/pop() 前必须判断 !empty(),否则程序崩溃。
    2. 假溢出问题:普通数组队列易出现「队尾满但队头有空」的假溢出,循环队列通过取模运算解决。

五、链表(Linked List)

1. 底层原理

链表是 非连续内存存储 的线性表,通过指针连接节点,每个节点包含数据域和指针域。核心优势为插入 / 删除操作 O(1) 时间复杂度,劣势为随机访问需 O(n) 遍历。常见分类:单链表、双向链表、循环链表。

2. 核心操作模板(单链表)

struct Node {
    int data;
    Node* next;
    Node(int x) : data(x), next(nullptr) {}
};

Node* head = nullptr;

// 头插法(O(1))
void insert_head(int x) {
    Node* newNode = new Node(x);
    newNode->next = head;
    head = newNode;
}

// 指定值删除(O(n))
void delete_node(int x) {
    Node *cur = head, *pre = nullptr;
    while (cur && cur->data != x) {
        pre = cur;
        cur = cur->next;
    }
    if (!cur) return;
    if (!pre) head = cur->next;
    else pre->next = cur->next;
    delete cur;
}

// 遍历
void traverse() {
    Node* cur = head;
    while (cur) {
        // 处理 cur->data
        cur = cur->next;
    }
}

3. 典型应用与避坑

  • 应用场景:
    1. 频繁插入删除场景:排队记录、动态任务调度。
    2. 多项式运算:用链表存储多项式的项,实现加减运算。
    3. 优化约瑟夫问题:循环链表实现,无需移动元素。
  • 避坑要点:
    1. 内存泄漏:手动实现链表需用 delete 释放删除的节点。
    2. 断链问题:插入 / 删除操作时指针指向错误,导致链表断裂。
    3. 空指针访问:遍历链表时需判断 cur != nullptr,避免空指针异常。

六、四大线性表对比与选型指南

数据结构存储方式随机访问插入 / 删除(指定位置)核心特性适用场景
数组 / Vector连续内存O(1)O(n)随机访问高效数据规模已知、频繁查询、矩阵运算
栈连续 / 非连续不支持O (1)(仅栈顶)后进先出(LIFO)括号匹配、递归模拟、表达式求值
队列连续 / 非连续不支持O (1)(队头 / 队尾)先进先出(FIFO)BFS、排队模拟、进程调度
链表非连续内存O(n)O(1)插入删除高效频繁增删、数据规模未知、动态任务管理

七、线性表学习进阶路线

  1. 基础阶段:掌握数组 / Vector 的 API 与迭代器,理解线性表的基本概念。
  2. 受限线性表:通过栈和队列的生活化案例,理解 LIFO/FIFO 特性,掌握 STL 实现与手动实现。
  3. 进阶阶段:掌握链表的节点定义、插入删除操作,理解链表与数组的优劣对比。
  4. 应用阶段:结合 BFS、DFS、括号匹配、约瑟夫问题等真题,巩固四大线性表的实际应用。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐