火龙信奥
  • 首页
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

第九章

作者: 作者的头像   zhong , 时间:2025-09-03 14:03:40 , 所有人可见, 阅读  6

编程导论(2024-2025冬季学期)

第9章:数据结构

课程信息

  • 授课教师:亚历山大·克劳斯博士(Dr.-Ing. Alexander Krause)
  • 所属机构:多特蒙德工业大学(Technische Universität Dortmund)应用信息学12系系统软件工作组
  • 课程链接:https://sys.cs.tu-dortmund.de/de/lehre/ws24/eidp/
  • 联系邮箱:eidp-problems@ls12.cs.tu-dortmund.de

第9章核心内容

  1. 数据结构基础
  2. 抽象数据类型(ADT)
  3. 栈(Stapel)
  4. 队列(Schlange)
  5. 列表(Liste)
  6. 二叉搜索树(Binärer Suchbaum)
  7. 图(Graph)
  8. 补充知识:简单文件操作(Einfache Dateibehandlung)

1. 抽象数据类型(ADT)

1.1 定义

抽象数据类型(Abstract Data Type,ADT)是一个三元组 ((T, F, A)),其中: - (T):非空的数据对象集合 - (F):操作集合(对数据对象的操作) - (A):非空的公理集合(解释操作的含义)

1.2 “抽象”的核心含义

  • 不关注实现细节:无需关心数据对象的具体存储方式(如数组、链表)
  • 只描述操作效果:定义操作“做什么”(WAS),而非“怎么做”(WIE)
  • 示例:栈的“push”操作仅定义“将元素加入栈顶”,不规定底层用数组还是链表实现

1.3 ADT的关键特性

  1. 通用性:已知ADT的定义后,可在任意场景复用
  2. 模板实现:通常以模板(Template)形式编写,支持多种数据类型
  3. 分离规范与实现:
  4. 规范(Spezifikation):对外公开ADT的操作接口
  5. 实现(Implementierung):内部逻辑隐藏,后续可替换实现方式而不影响外部使用
  6. 信息隐藏:仅通过预设操作访问数据,避免直接操作底层存储

2. 线性数据结构:栈(Stapel/Stack)

2.1 定义与核心语义

  • 类比:堆叠的盘子——只能从顶部添加或移除盘子
  • 工作语义:LIFO(Last-In-First-Out,后进先出)
  • C++标准库支持:std::stack、std::deque、std::vector(可作为栈的底层容器)

2.2 栈的核心操作

操作 功能描述
create() 创建一个空栈
push(x) 将元素x添加到栈顶
pop() 移除栈顶元素(无返回值)
top() 返回栈顶元素(不移除)
empty() 判断栈是否为空(返回bool值)
full() 判断栈是否已满(仅固定大小栈需要)

2.3 操作示例流程

执行序列:create() → empty() → push(3) → push(5) → push(2) → top() → pop() → top() → empty()
流程解析: 1. create():栈为空([]) 2. empty():返回true 3. push(3):栈变为[3] 4. push(5):栈变为[3,5] 5. push(2):栈变为[3,5,2] 6. top():返回2(栈仍为[3,5,2]) 7. pop():栈变为[3,5] 8. top():返回5(栈仍为[3,5]) 9. empty():返回false

2.4 栈的实现(基于数组)

2.4.1 类定义(模板形式)

template <typename T>
class Stack {
private:
    static constexpr int maxSize = 100;  // 栈的最大容量(固定)
    T data[maxSize];                     // 存储栈元素的数组
    int sz;                              // 栈指针:指向栈顶元素的索引(初始为-1)
    void error(const char* s);           // 错误处理(如栈空/满时操作)

public:
    Stack();                 // 构造函数(创建空栈)
    void push(T& x);         // 入栈:添加元素到栈顶
    void pop();              // 出栈:移除栈顶元素
    T top();                 // 获取栈顶元素
    bool empty();            // 判断栈是否为空
    bool full();             // 判断栈是否已满
};

2.4.2 核心方法实现

  1. 构造函数:初始化栈指针为-1(表示空栈) cpp template <typename T> Stack<T>::Stack() : sz(-1) {}

  2. 空/满判断: ```cpp template bool Stack::empty() { return sz == -1; // 栈指针为-1 → 空栈 }

template bool Stack::full() { return sz == maxSize - 1; // 栈指针达到最大索引 → 满栈 } ```

  1. 错误处理:输出错误信息并终止程序(后续可替换为异常处理) cpp template <typename T> void Stack<T>::error(const char* s) { cerr << s << endl; // 标准错误流输出(无缓冲区,即时显示) exit(1); // 终止程序,返回1给操作系统 }

  2. 入栈(push): cpp template <typename T> void Stack<T>::push(T& x) { if (full()) error("栈已满(push失败)"); // 满栈检查 data[++sz] = x; // 栈指针先自增,再赋值(从索引0开始存储) }

  3. 出栈(pop)与获取栈顶(top): ```cpp template T Stack::top() { if (empty()) error("栈为空(top失败)"); // 空栈检查 return data[sz]; // 返回栈顶元素(栈指针不变) }

template void Stack::pop() { if (empty()) error("栈为空(pop失败)"); // 空栈检查 sz--; // 仅移动栈指针,无需显式删除元素(后续入栈会覆盖) } ```

2.4.3 测试代码

int main() {
    Stack<int> s;
    int i = 1;

    // 填充栈(直到满栈)
    while (!s.full()) {
        s.push(i);
        ++i;
    }
    cout << "栈顶元素:" << s.top() << endl;  // 输出100(maxSize=100)

    // 弹出90个元素
    for (int i = 1; i <= 90; ++i) {
        s.pop();
    }

    // 输出剩余元素(从栈顶到栈底)
    while (!s.empty()) {
        cout << s.top() << endl;  // 输出10、9、...、1
        s.pop();
    }

    // 尝试对空栈执行pop(触发错误)
    s.pop();  // 输出"栈为空(pop失败)"并终止程序
    return 0;
}

2.5 数组实现的优缺点

优点 缺点
实现简单,逻辑清晰 容量固定,无法动态扩展
方法执行效率高(直接数组访问) 栈空/满时操作会终止程序(需优化为异常处理)

2.6 改进方向

  1. 异常处理替代exit(1):使用C++异常(如std::out_of_range),避免程序直接终止(详见第12章)
  2. 动态扩容:当栈满时,创建更大的动态数组(如原容量的2倍),拷贝原元素后释放旧数组(缺点:扩容时耗时较长)
  3. 链表实现:基于指针的动态栈,无固定容量限制(后续队列、列表会采用类似思路)

3. 线性数据结构:队列(Schlange/Queue)

3.1 定义与核心语义

  • 类比:超市排队——新成员从队尾加入,先到者从队首离开
  • 工作语义:FIFO(First-In-First-Out,先进先出)
  • C++标准库支持:std::queue、std::deque、std::list

3.2 队列的核心操作

操作 功能描述
create() 创建一个空队列
enqueue(x) 将元素x添加到队尾
dequeue() 移除队首元素(无返回值)
front() 返回队首元素(不移除)
empty() 判断队列是否为空(返回bool值)
full() 判断队列是否已满(仅固定大小队列需要)

3.3 操作示例流程

执行序列:create() → enqueue(3) → enqueue(5) → dequeue() → enqueue(2) → front() → dequeue() → front()
流程解析: 1. create():队列为空([]) 2. enqueue(3):队列变为[3] 3. enqueue(5):队列变为[3,5] 4. dequeue():队列变为[5] 5. enqueue(2):队列变为[5,2] 6. front():返回5(队列仍为[5,2]) 7. dequeue():队列变为[2] 8. front():返回2

3.4 队列的实现方案

3.4.1 方案1:基于数组的普通实现(存在缺陷)

  • 思路:用数组存储元素,ez(Endzeiger)指向队尾元素(初始为-1)
  • 缺陷:
  • 每次dequeue()后,队首前的空间无法复用(如队列[5,2]执行dequeue()后,索引0的空间闲置)
  • 解决方案1(元素前移):dequeue()时将所有元素向左移1位——效率极低(时间复杂度O(n))
  • 解决方案2(循环数组/环形缓冲区):添加sz(Startzeiger)指向队首,数组视为环形,通过取模(%)实现循环——效率高,但仍为固定容量

3.4.2 方案2:基于指针的动态实现(推荐)

  • 思路:用链表存储元素,通过sz(队首指针)和ez(队尾指针)管理队列,支持动态扩容
  • 类定义(模板形式): ```cpp template class Queue { public: Queue(); // 构造函数(创建空队列) void enqueue(T& x);// 入队:添加元素到队尾 void dequeue(); // 出队:移除队首元素 T front(); // 获取队首元素 bool empty(); // 判断队列是否为空 void clear(); // 清空队列 ~Queue(); // 析构函数(释放内存)

private: // 内部元素结构体:存储数据和下一个元素的指针 struct Element { T data; Element next; }; Element sz; // 队首指针(指向第一个元素) Element ez; // 队尾指针(指向最后一个元素) void error(const char info); // 错误处理 }; ```

3.4.3 核心方法实现

  1. 构造函数与空判断: ```cpp template Queue::Queue() { sz = ez = nullptr; // 初始为空队列,指针均为nullptr }

template bool Queue::empty() { return sz == nullptr; // 队首指针为nullptr → 空队列 } ```

  1. 错误处理与清空队列: ```cpp template void Queue::error(const char* info) { cerr << info << endl; exit(1); // 后续可替换为异常处理 }

template void Queue::clear() { while (!empty()) { dequeue(); // 循环出队,释放所有元素 } } ```

  1. 析构函数: cpp template <typename T> Queue<T>::~Queue() { clear(); // 清空队列,避免内存泄漏 }

  2. 入队(enqueue): cpp template <typename T> void Queue<T>::enqueue(T& x) { // 创建新元素(next初始为nullptr) Element* e = new Element{x, nullptr}; if (empty()) { sz = e; // 空队列时,队首和队尾均指向新元素 } else { ez->next = e; // 非空队列时,队尾元素的next指向新元素 } ez = e; // 更新队尾指针 }

  3. 出队(dequeue)与获取队首(front): ```cpp template T Queue::front() { if (empty()) error("队列为空(front失败)"); return sz->data; // 返回队首元素数据 }

template void Queue::dequeue() { if (empty()) error("队列为空(dequeue失败)"); Element* temp = sz; // 暂存队首指针(避免内存泄漏) sz = sz->next; // 队首指针指向第二个元素 if (sz == nullptr) { // 若队列变为空,更新队尾指针 ez = nullptr; } delete temp; // 释放原队首元素的内存 } ```

3.4.4 测试代码

int main() {
    Queue<int> q;

    // 入队:2~11(共10个元素)
    for (int i = 2; i <= 11; ++i) {
        q.enqueue(i);
    }

    // 出队5次,输出队首元素(2~6)
    for (int i = 1; i <= 5; ++i) {
        cout << q.front() << endl;
        q.dequeue();
    }

    // 输出剩余元素(7~11)
    while (!q.empty()) {
        cout << q.front() << endl;
        q.dequeue();
    }

    // 判断队列是否为空
    if (q.empty()) {
        cout << "队列为空" << endl;
    }

    return 0;
}

4. 深拷贝与浅拷贝:拷贝构造函数与赋值运算符

4.1 问题背景:默认拷贝的缺陷

C++编译器会自动生成浅拷贝(flache Kopie)的拷贝构造函数和赋值运算符: - 浅拷贝仅复制指针的值(而非指针指向的内容) - 导致多个对象共享同一块内存,析构时会重复释放内存(程序崩溃)

示例(队列浅拷贝问题):

int main() {
    Queue<int> q;
    for (int i = 1; i <= 5; ++i) {
        q.enqueue(i);  // q: [1,2,3,4,5]
    }
    Queue<int> q2 = q;  // 浅拷贝:q2.sz = q.sz,q2.ez = q.ez
    q.dequeue();        // q: [2,3,4,5],但q2.sz仍指向原队首(已被释放)
    q2.front();         // 访问非法内存,程序崩溃
    return 0;
}

4.2 解决方案:自定义拷贝构造函数

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

©2026加盟我们 | 关于我们 | ACM课程 | 常见问题 | 成果墙 | 评测记录 | 浙ICP备2021013995号
在线画图 | OI WIki | 打字练习
火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码