Skip to content

线性结构

约 5389 字大约 18 分钟

2025-06-25

链表:单链表、双向链表、循环链表

一、核心概念

核心概念

1. 单链表

  • 定义:由多个节点组成,每个节点包含「数据域」(存储数据)和「指针域」(存储下一个节点的地址),最后一个节点的指针域为 nullptr(空指针)。
  • 特点:只能从表头向表尾单向遍历,插入/删除时只需修改指针指向,无需移动大量元素。
  • 结构示意
    头节点 → 节点1 → 节点2 → ... → 尾节点 → nullptr

2. 双向链表

  • 定义:每个节点除数据域外,包含两个指针域:「前驱指针」(指向前一个节点)和「后继指针」(指向后一个节点)。
  • 特点:可双向遍历(从头到尾或从尾到头),插入/删除时需同时维护两个指针,但操作更灵活。
  • 结构示意
    nullptr ← 节点1 ↔ 节点2 ↔ ... ↔ 尾节点 → nullptr

3. 循环链表

  • 定义:链表首尾相连,尾节点的指针域指向头节点(单循环链表),或头节点的前驱指针指向尾节点(双循环链表)。
  • 特点:可循环遍历,适合模拟「环形」场景(如约瑟夫问题)。
  • 结构示意
    头节点 → 节点1 → ... → 尾节点 → 头节点(形成环)

二、实战案例(C++ 实现)

实战案例

1. 单链表基础操作(创建、插入、删除、遍历)

c++

2. 循环链表应用:约瑟夫问题

问题:n个人围成一圈,从第1个人开始报数,报到k的人出列,剩下的人继续从1报数,直到最后一人。输出出列顺序。

c++

3. 双向链表(简化实现)

c++

三、信奥赛应用场景

信奥赛应用场景

  1. 模拟类问题:如约瑟夫问题(循环链表)、多项式相加(单链表,每个节点存系数和指数)。
  2. 动态数据处理:需要频繁插入/删除(如链表实现队列、栈,比数组更灵活)。
  3. 内存敏感场景:链表无需预先分配连续内存,适合数据量不确定的情况。
  4. 链表变形题:如判断链表是否有环、求环的入口(循环链表相关)、链表反转等。

四、避坑指南

避坑指南

  1. 指针初始化:未初始化的指针可能指向随机内存(野指针),操作前务必赋值(如 nullptr)。
c++
Node* p; // 危险!应改为 Node* p = nullptr;
  1. 边界条件处理

    • 单链表插入/删除头节点时,需单独处理(头指针会变)。
    • 循环链表遍历终止条件是「回到头节点」,而非 nullptr,避免死循环。
  2. 内存泄漏:删除节点后必须用 delete 释放内存(竞赛中虽不严格扣分,但养成好习惯)。

  3. 双向链表指针同步:插入/删除时,必须同时更新「前驱指针」和「后继指针」,否则链表断裂。

  4. 静态链表替代:普及用组常用「数组模拟链表」(静态链表)避免指针操作风险,用数组下标模拟指针:

c++
struct Node { int data; int next; };
Node nodes[1000]; // 预分配节点数组
int head = -1;    // -1 表示空指针

重要

  • 单链表:结构简单,适合单向遍历,插入删除效率高(O(1),已知前驱时)。
  • 双向链表:支持双向遍历,操作灵活但指针维护复杂。
  • 循环链表:适合环形场景,需注意遍历终止条件。

一、核心概念

栈(Stack)是一种后进先出(LIFO, Last In First Out) 的线性数据结构,只能在一端(称为栈顶,Top)进行插入和删除操作,另一端称为栈底(Bottom)。

核心概念

2. 基本操作

  • 压栈(Push):向栈顶添加元素
  • 出栈(Pop):从栈顶移除元素
  • 取栈顶元素(Top):获取栈顶元素但不删除
  • 判空(Empty):判断栈是否为空
  • 求大小(Size):获取栈中元素个数

3. 存储结构

  • 顺序栈:用数组实现,需预先分配内存
  • 链式栈:用链表实现,动态分配内存,无固定大小限制

4. 结构示意图

栈底 ← 元素1 ← 元素2 ← 元素3(栈顶)

              操作端

二、实战案例(C++ 实现)

实战案例

1. 顺序栈实现

c++

2. 链式栈实现

c++

3. 栈的典型应用:括号匹配

c++

4. 栈的典型应用:表达式求值(后缀表达式)

c++

三、信奥赛应用场景

信奥赛应用场景

  1. 括号匹配问题:判断表达式中的括号是否合法配对
  2. 表达式求值:中缀表达式转后缀表达式并计算结果
  3. 深度优先搜索(DFS):用栈模拟递归过程(避免递归栈溢出)
  4. 单调栈问题:解决"下一个更大元素"、"直方图最大矩形面积"等问题
  5. 迷宫问题:记录路径并实现回溯
  6. 进制转换:将十进制数转换为其他进制(除基取余法)

四、避坑指南

避坑指南

  1. 栈溢出处理:使用顺序栈时,必须先判断栈是否已满再进行压栈操作
c++
// 错误示例
void push(int x) {
    data[++top] = x; // 未判断是否溢出
}

// 正确示例
void push(int x) {
    if (top == MAX_SIZE - 1) {
        // 处理溢出(提示错误或动态扩容)
        return;
    }
    data[++top] = x;
}
  1. 空栈操作防护:出栈和取栈顶元素前必须判断栈是否为空
c++
// 错误示例
int getTop() {
    return data[top]; // 栈为空时会访问非法内存
}

// 正确示例
int getTop() {
    if (isEmpty()) {
        // 处理空栈情况
        return -1;
    }
    return data[top];
}
  1. 链式栈内存管理:出栈时必须释放节点内存,避免内存泄漏
c++
void pop() {
    if (isEmpty()) return;
    Node* temp = top;
    top = top->next;
    delete temp; // 关键:释放内存
}
  1. 操作顺序注意:后缀表达式求值时,注意弹出元素的顺序(先出栈的是第二个操作数)
c++
// 错误:顺序颠倒
int a = st.getTop(); st.pop();
int b = st.getTop(); st.pop();
int res = a - b; // 实际应该是 b - a
  1. 选择合适的实现方式
    • 数据量固定且已知:优先用顺序栈(效率高)
    • 数据量不确定或可能很大:用链式栈(避免溢出)

重要

  • 核心特性:栈是LIFO结构,仅允许在栈顶操作,插入和删除效率为O(1)
  • 实现方式:顺序栈(数组)适合固定大小场景,链式栈(链表)适合动态大小场景
  • 关键操作:压栈、出栈、取栈顶元素,均需注意边界条件(空栈、满栈)
  • 应用核心:利用"后进先出"特性解决具有回溯性的问题(如括号匹配、DFS、表达式求值等)

队列

一、核心概念

队列的定义

队列(Queue)是一种先进先出(FIFO, First In First Out) 的线性数据结构,元素只能从一端(队尾,Rear)插入,从另一端(队头,Front)删除,类似现实中排队的场景。

核心概念

2. 基本操作

  • 入队(Enqueue):向队尾添加元素
  • 出队(Dequeue):从队头移除元素
  • 取队头元素(Front):获取队头元素但不删除
  • 判空(Empty):判断队列是否为空
  • 求大小(Size):获取队列中元素个数

3. 存储结构

  • 顺序队列:用数组实现,需处理"假溢出"问题
  • 循环队列:顺序队列的优化,将数组首尾相连形成环形
  • 链式队列:用链表实现,动态分配内存,无固定大小限制

4. 结构示意图

队头 → 元素1 → 元素2 → 元素3 → 队尾
  ↑                           ↑
出队端                       入队端

二、实战案例(C++ 实现)

实战案例

1. 循环队列实现(解决顺序队列假溢出问题)

c++

2. 链式队列实现

c++

3. 队列的典型应用:广度优先搜索(BFS)

c++

4. 队列的典型应用:滑动窗口最大值

c++

三、信奥赛应用场景

信奥赛应用场景

  1. 广度优先搜索(BFS):层次遍历图或树,如最短路径问题、迷宫求解
  2. 滑动窗口问题:处理固定大小的窗口内的极值或统计
  3. 队列模拟:如银行排队、打印机任务调度等模拟类问题
  4. 缓冲区设计:如字符流处理、数据缓冲等
  5. 单调队列:解决滑动窗口最大值、最小值等问题
  6. 树的层次遍历:按层次输出树的节点
  7. 图的拓扑排序:有向无环图(DAG)的拓扑排序实现

四、避坑指南

避坑指南

  1. 循环队列的边界处理
    • 区分空队列和满队列:通过size变量或预留一个空位置
    • 队头队尾指针移动时必须使用取模运算实现循环
    c++
    // 错误示例:未使用取模运算
    rear++; // 可能超出数组范围
    
    // 正确示例
    rear = (rear + 1) % MAX_SIZE;
  2. 链式队列的空队列处理:出队后若队列变空,需同时将rear置为nullptr
c++
void dequeue() {
    if (isEmpty()) return;
    Node* temp = front;
    front = front->next;
    if (front == nullptr) {
        rear = nullptr; // 关键:队列为空时队尾也为空
    }
    delete temp;
}
  1. 入队出队的顺序性:严格遵守FIFO原则,不能在中间插入或删除元素
c++
// 错误:直接修改队头元素
front->data = x; // 破坏了队列的顺序性
  1. 选择合适的实现方式

    • 数据量固定且已知:用循环队列(效率高)
    • 数据量不确定或可能很大:用链式队列(避免溢出)
    • 需要两端操作:用双端队列(deque)
  2. 标准库队列的使用:信奥赛中可直接使用queuedeque,但需注意:

c++
#include <queue>
queue<int> q; // 普通队列
deque<int> dq; // 双端队列,支持front()、back()、push_back()、push_front()等

重要

  • 核心特性:队列是FIFO结构,元素从队尾入队、队头出队,插入和删除效率为O(1)
  • 实现方式:循环队列适合固定大小场景,链式队列适合动态大小场景,双端队列支持两端操作
  • 关键操作:入队、出队、取队头元素,需注意边界条件(空队列、满队列)
  • 应用核心:利用"先进先出"特性解决具有顺序性的问题(如BFS、滑动窗口、模拟排队等)