2024秋季学期 数据结构期末实验报告(无源码版)

前言

这玩意在我看来,p用没有,纯浪费时间,但是沟槽的课有这个要求那我只能花了一点点时间水水了。

如果对里面的内容感兴趣(应该不会有人没事来看这种sb玩意吧),可以私信我~

实验一 疏松多项式

1.1问题描述

使用链表结构储存疏松多项式并实现以下功能:

  1. 输入并创建多项式(按指数升序或降序排列,系数浮点型,指数整型)
  2. 输出多项式,项数+每项系数指数(按指数升序或降序排列)
  3. 求和:多项式相加
  4. 求差:多项式相减
  5. 求值
  6. 销毁
  7. 清空
  8. 修改(①插入新的结点、②删除已有结点、③修改已有结点的系数和指数)
  9. 微分(N阶导数)
  10. 不定积分
  11. 定积分
  12. 乘法和乘方
  13. 除法
  14. 多项式的四则运算(如 “(1+2*3)/4”)
  15. 计算器的仿真界面

1.2算法描述

为方便操作,多项式计算部分将采用返回值方式实现。
故在每次计算后,如未加入多项式组,则需要销毁多项式。

1.2.1数据结构

采用链表储存单个多项式,并使用另一链表储存多项式组。

单个多项式内部节点结构如下:

struct node{
    float coef;
    int expn;
    struct node *next;
};

其中,coef为系数,expn为指数,next为下一个节点的指针。

多项式组节点结构如下:

struct PolynListNode{
    node *head;
    int id;
    struct PolynListNode *next;
};

其中,head为多项式的头指针,id为多项式的id,next为下一个多项式组节点的指针。

1.2.2多项式组操作函数

函数定义如下

int InsertPolynList(PolynListNode *head, node *p);
int DeletePolynList(PolynListNode *head, int id);
node* LocatePolynList(PolynListNode *head, int id);
int ClearPolynList(PolynListNode *head);
int DestroyPolynList(PolynListNode *head);
int PrintPolynList(PolynListNode *head);

InsertPolynList

该函数用于向多项式组中插入一个多项式,需要传入多项式组头指针和待插入多项式的头指针,返回值为1表示成功,0表示失败。

DeletePolynList

该函数用于删除多项式组中的一个多项式,需要传入多项式组头指针和待删除多项式的id,返回值为1表示成功,0表示失败。

LocatePolynList

该函数用于定位多项式组中的一个多项式,需要传入多项式组头指针和待定位多项式的id,返回值为多项式的头指针。

ClearPolynList

该函数用于清空多项式组中的所有多项式,需要传入多项式组头指针,返回值为1表示成功,0表示失败。

DestroyPolynList

该函数用于销毁多项式组,需要传入多项式组头指针,返回值为1表示成功,0表示失败。

PrintPolynList

该函数用于在命令行中输出多项式组中的所有多项式,需要传入多项式组头指针,返回值为1表示成功,0表示失败。

多项式操作函数

函数定义如下

node* NewPolyn();
node* CopyPolyn(node* p);
int InsertPolyn(node* p, int expn, float coef);
int EditPolyn(node* p, int oldexpn, int newexpn, float coef);
int LocatePolyn(node* p, int expn);
int DestroyPolyn(node* p);
int ClearPolyn(node* p);
int DeletePolyn(node* p, int expn);
int PrintPolyn(node* p);
node* ReadPolyn(int n);

NewPolyn

该函数用于创建一个新的多项式,返回值为多项式的头指针。

CopyPolyn

该函数用于复制一个多项式,需要传入多项式的头指针,返回值为新多项式的头指针。

InsertPolyn

该函数用于向多项式中插入一个新的节点,需要传入多项式的头指针、指数和系数,返回值为1表示成功,0表示失败。

EditPolyn

该函数用于修改多项式中的一个节点,需要传入多项式的头指针、原指数、新指数和新系数,返回值为1表示成功,0表示失败。

LocatePolyn

该函数用于查询多项式中的一个节点是否存在,需要传入多项式的头指针和指数,返回值为1表示存在,0表示不存在。

DestroyPolyn

该函数用于销毁多项式,需要传入多项式的头指针,返回值为1表示成功,0表示失败。

ClearPolyn

该函数用于清空多项式,需要传入多项式的头指针,返回值为1表示成功,0表示失败。

DeletePolyn

该函数用于删除多项式中的一个节点,需要传入多项式的头指针和指数,返回值为1表示成功,0表示失败。

PrintPolyn

该函数用于在命令行中输出多项式,需要传入多项式的头指针,返回值为1表示成功,0表示失败。

ReadPolyn

该函数用于从命令行中读取多项式,需要传入多项式的项数,返回值为多项式的头指针。

1.2.3多项式计算函数

函数定义如下

node* AddPolyn(node* pa, node* pb);
node* OppositePolyn(node* p);
node* SubPolyn(node* pa, node* pb);
float ValuePolyn(node* p, float x);
node* DiffPolyn(node* p);
node* IndefiniteIntegratePolyn(node* p);
float DefiniteIntegratePolyn(node* p, float a, float b);
node* MulPolyn(node* pa, node* pb);
node* PowPolyn(node* p, int n);
pair<node*, node*> DivPolyn(node* pa, node* pb);

AddPolyn

该函数用于计算两个多项式的和,需要传入两个多项式的头指针,返回值为新多项式的头指针。

OppositePolyn

该函数用于计算一个多项式的相反数,需要传入多项式的头指针,返回值为新多项式的头指针。

SubPolyn

该函数用于计算两个多项式的差,需要传入两个多项式的头指针,返回值为新多项式的头指针。

ValuePolyn

该函数用于计算多项式在某一点的值,需要传入多项式的头指针和点的值,返回值为多项式在该点的值。

DiffPolyn

该函数用于计算多项式的导数,需要传入多项式的头指针,返回值为新多项式的头指针。

IndefiniteIntegratePolyn

该函数用于计算多项式的不定积分,需要传入多项式的头指针,返回值为新多项式的头指针。

DefiniteIntegratePolyn

该函数用于计算多项式的定积分,需要传入多项式的头指针和积分上下限,返回值为多项式的定积分值。

MulPolyn

该函数用于计算两个多项式的乘积,需要传入两个多项式的头指针,返回值为新多项式的头指针。

PowPolyn

该函数用于计算多项式的乘方,需要传入多项式的头指针和乘方次数,返回值为新多项式的头指针。

DivPolyn

该函数用于计算两个多项式的商和余数,需要传入两个多项式的头指针,返回值为pair,first为商,second为余数。

1.2.4命令行操作函数

函数定义如下

int CommandOperation(int op, PolynListNode *head);
int CommandInterface(PolynListNode *head);

CommandOperation

该函数用于执行命令行操作,需要传入操作码和多项式组头指针,返回值为1表示成功,0表示失败。

CommandInterface

该函数用于执行命令行界面,需要传入多项式组头指针,返回值为1表示成功,0表示失败。

附录:操作码

0. 退出
一、多项式创建&修改
1. 创建多项式
2. 输出多项式
3. 插入项
4. 删除项
5. 修改项
6. 查找项
7. 清空多项式
8. 销毁多项式
9. 查看多项式列表
二、多项式求值
11. 多项式加法
12. 多项式减法
13. 多项式乘法
14. 多项式求值
15. 多项式求导
16. 多项式不定积分
17. 多项式定积分
18. 多项式除法
19. 多项式乘方
20. 多项式运算

1.3调试分析

按各项要求分别逐项进行简单调试,调试过程中发现了以下问题:

  1. 由于程序采用了返回值的实现形式,故在每次计算后需要销每多项式,否则会造成内存泄漏。
  2. 模块化设计上,多项式操作与多项式计算分开,方便调试与维护。

整体调试过程较为顺利,最终实现了所有功能。

1.4算法的时空分析

1.4.1时间复杂度

多项式组操作函数的时间复杂度为 \(O(n)\), 其中 \(n\) 为多项式组中多项式的个数。

多项式操作函数的时间复杂度为 \(O(n)\), 其中 \(n\) 为多项式中节点的个数。

加法、减法、求值、销毁、清空、修改、微分、不定积分、定积分的时间复杂度均为 \(O(n)\), 其中 \(n\) 为多项式中节点的个数。

乘法的时间复杂度为 \(O(n^2)\), 其中 \(n\) 为多项式中节点的个数。

乘方的时间复杂度为 \(O(n ^ 2 \log m)\),其中 \(n\) 为多项式中节点的个数,\(m\) 为乘方次数(已采用快速幂算法)。

除法的时间复杂度为 \(O(n^2)\), 其中 \(n\) 为多项式中节点的个数。

1.4.2空间复杂度

多项式组的空间复杂度为 \(O(n)\), 其中 \(n\) 为多项式组中多项式的个数。

多项式的空间复杂度为 \(O(m)\), 其中 \(m\) 为多项式中节点的个数。

故总空间复杂度为 \(O(nm)\)

1.5测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

1.6实验的收获和体会

通过本次实验,我学会了如何使用链表结构储存多项式,并实现了多项式的各种操作。同时,我也学会了如何模块化设计程序,使得程序更加易于维护与调试。最后,我也学会了如何进行算法的时空复杂度分析,以便更好地优化程序。

实验二 栈与队列的应用

T2括号匹配

2.1.1问题描述

括号匹配问题是指对于一个给定的字符串,判断其中的括号是否匹配。例如,对于字符串 ((())),括号是匹配的;而对于字符串 (())),括号是不匹配的。

输入为一个字符串,输出为 YESNO,分别表示括号匹配与不匹配。

2.1.2算法描述

2.1.2.1数据结构

采用栈结构储存括号,栈的节点结构如下:

2.1.2.1程序结构

简单线性结构实现即可

2.1.3算法的时空分析

2.1.3.1时间复杂度

由于每个字符只需入栈一次,故时间复杂度为 \(O(n)\),其中 \(n\) 为字符串的长度。

2.1.3.1空间复杂度

栈的空间复杂度为 \(O(n)\),其中 \(n\) 为字符串的长度。

2.1.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

2.1.5实验的收获和体会

通过本次实验,我学会了如何使用栈结构判断括号匹配问题。

T3迷宫求解

2.2.1问题描述

给定一个迷宫,求起点到终点的最短路径,并输出路径。

输入:第一行\(n\)\(m\),表示迷宫的行数和列数;接下来输入 \(m \times n\)\(01\) 矩阵,其中 \(0\) 表示可通过,\(1\) 表示不可通过;最后输入起点和终点坐标。
输出:若有解,则输出最短路径,否则输出 -1

2.2.2算法描述

2.2.2.1数据结构

本体采用BFS算法实现,故使用的数据结构为队列。

2.2.2.2程序结构

本题的BFS采用简单线性结构实现即可。

2.2.3算法的时空分析

2.2.3.1时间复杂度

BFS对迷宫中的每个点只需访问一次,故时间复杂度为 \(O(nm)\),其中 \(n\) 为迷宫的行数,\(m\) 为迷宫的列数。

2.2.3.2空间复杂度

存图的空间复杂度为 \(O(nm)\),其中 \(n\) 为迷宫的行数,\(m\) 为迷宫的列数。

队列的空间复杂度最大为 \(O(nm)\),其中 \(n\) 为迷宫的行数,\(m\) 为迷宫的列数。

2.2.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

2.2.5实验的收获和体会

通过本次实验,我学会了如何使用BFS算法解决迷宫问题,并强化了对队列的理解。

T4银行业务模拟

2.3.1问题描述

银行业务模拟是指对于一系列客户的银行业务,模拟银行的业务流程。

输入:第一行 \(n, tot, closeTime, averageTime\),分别表示客户数、初始准备金额、银行关门时间、平均到达时间;接下来输入 \(n\) 行,每行 \(2\) 个整数 \(a, t\),表示客户需要的金额(\(a<0\) 表示存款,\(a>0\) 表示取款)和到达时间。

输出:输出每个客户的等待时间;第 \(n+1\) 行输出银行的平均等待时间。

2.3.2算法描述

2.3.2.1数据结构

按需求理论上需要两个队列,但由于程序本身是线性读入的,故只需一个队列即可。

2.3.2.2程序结构

使用在线模式按照时间顺序读入数据,模拟银行业务流程即可。对于金额不足的用户,存入队列等待。每次有用户存款后,检查队列中是否有可以进行取款的用户,如果有则进行取款。

2.3.3算法的时空分析

2.3.3.1时间复杂度

由于每个用户只需入队一次,但是每次存款后需要检查队列中是否有可以取款的用户,故时间复杂度为 \(O(kn)\),其中 \(n\) 为用户的个数。

2.3.3.2空间复杂度

队列的空间复杂度为 \(O(n)\),其中 \(n\) 为用户的个数。

2.3.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

2.3.5实验的收获和体会

通过本次实验,我学会了如何使用队列模拟银行业务流程,并强化了对队列的理解。

实验三 二叉树的应用

3.1问题描述

对任意文件进行哈夫曼编码与对于已编码文件进行解码。

输入:

  1. 操作码(0 表示编码,1 表示解码,2 表示退出)
  2. 若操作码为 0,则输入文件名
  3. 若操作码为 1,则输入文件名和编码表
  4. 若操作码为 2,则退出

输出:输出一个文件,若操作码为 0,则输出以.huff为后缀的文件;若操作码为 1,则输出解码后的文件。

3.2算法描述

3.2.1数据结构

哈夫曼树的节点结构如下:

struct HNode {
    unsigned char data;
    int weight;
    struct HNode * lChild, * rChild;
};

其中,data 为字符,weight 为权重,lChild 为左孩子,rChild 为右孩子。

3.2.2编码

编码包含以下操作函数:

HNode* initHuffNode(unsigned char data, int weight);
int* readByteFromFile(string filePath);
HNode* createHuffTree(int* f);
void getMapFromHuffTree(HNode* node, unsigned map[][2], unsigned tab, unsigned value);
void encodeFile(unsigned map[][2], HNode* node, string filePath, long long length);
void encodeMain();

initHuffNode

该函数用于初始化一个哈夫曼树节点,需要传入字符和权重,返回值为新节点的指针。

readByteFromFile

该函数用于读取文件中的字节,需要传入文件路径,返回值为字节频率数组。

createHuffTree

该函数用于创建哈夫曼树,需要传入字节频率数组,返回值为哈夫曼树的根节点。

getMapFromHuffTree

该函数用于从哈夫曼树中获取编码表,需要传入哈夫曼树的根节点、编码表、编码表长度、编码值。

encodeFile

该函数用于编码文件,需要传入编码表、哈夫曼树的根节点、文件路径、文件长度。

encodeMain

该函数用于编码文件的主函数。

3.2.3解码

解码包含以下操作函数:

HNode* readHuffTree(unsigned map[][2]);
void decodeFile(string huffPath);
void decodeMain();

readHuffTree

该函数用于从编码表中读取哈夫曼树,需要传入编码表,返回值为哈夫曼树的根节点。

decodeFile

该函数用于解码文件,需要传入文件路径。

decodeMain

该函数用于解码文件的主函数。

3.3调试分析

按各项要求分别逐项进行简单调试,调试过程中发现了以下问题:

  1. 需在huff文件中,除存储编码后的二进制外,还需存储编码表,与文件类型。
  2. 编码后huff文件中最后一个字节可能不足8位,但在计算机中会自动补0,故需在编码时需额外记录文件长度。

整体调试过程较为顺利,最终实现了所有功能。

3.4算法的时空分析

3.4.1时间复杂度

读取文件的时间复杂度为 \(O(n)\),其中 \(n\) 为文件的字节数。

创建哈夫曼树的时间复杂度为 \(O(n \log n)\),其中 \(n\) 为哈夫曼树的节点数。

编码文件的时间复杂度为 \(O(n)\),其中 \(n\) 为文件的字节数。

解码文件的时间复杂度为 \(O(n)\),其中 \(n\) 为文件的字节数。

3.4.2空间复杂度

哈夫曼树的空间复杂度为 \(O(n)\),其中 \(n\) 为哈夫曼树的节点数。

编码表的空间复杂度为 \(O(n)\),其中 \(n\) 为哈夫曼树的节点数。

编码文件的空间复杂度为 \(O(n)\),其中 \(n\) 为文件的字节数。

解码文件的空间复杂度为 \(O(n)\),其中 \(n\) 为文件的字节数。

3.5测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

3.6实验的收获和体会

通过本次实验,我学会了如何使用哈夫曼树对文件进行编码与解码,并强化了对二叉树的理解。

实验四 图的应用

T1图的遍历

4.1.1问题描述

对于一个无向图,求其深度优先遍历与广度优先遍历。

输入:第一行 \(n, m\),分别表示顶点数与边数;接下来输入 \(m\) 行,每行 \(2\) 个整数 \(u, v\),表示一条边;最后输入起点。

输出:第一行为深度优先遍历序列;第二行为广度优先遍历序列。

4.1.2算法描述

4.1.2.1DFS

DFS直接使用递归实现即可。

4.1.2.2BFS

BFS使用队列实现即可。

4.1.2.3数据结构

图采用邻接矩阵存储。

4.1.3算法的时空分析

时间复杂度

DFS的时间复杂度为 \(O(n + m)\),其中 \(n\) 为顶点数,\(m\) 为边数。

BFS的时间复杂度为 \(O(n + m)\),其中 \(n\) 为顶点数,\(m\) 为边数。

空间复杂度

图的空间复杂度为 \(O(n^2)\),其中 \(n\) 为顶点数。

队列的空间复杂度为 \(O(n)\),其中 \(n\) 为顶点数。

4.1.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

4.1.5实验的收获和体会

通过本次实验,我学会了如何使用DFS与BFS对图进行遍历,并强化了对图的理解。

T2最小生成树

4.2.1问题描述

对于一个无向有权图,求其最小生成树。

输入:第一行 \(n, m\),分别表示顶点数与边数;接下来输入 \(m\) 行,每行 \(3\) 个整数 \(u, v, w\),表示一条边;最后输入起点。

输出:输出最小生成树的大小。

4.2.2算法描述

4.2.2.1Kruskal

Kruskal算法使用并查集实现,先对所有边进行排序,然后依次尝试加入最小边,直至加入边的数量为 \(n - 1\)

算法详见附录里的原始代码

4.2.2.2Prim

Prime算法和Dijkstra算法类似,采用了贪心策略。从一个顶点开始,每次选择一个与当前生成树距离最小的顶点加入生成树。

int Prim(int n, int m, Edge* edges, int start) {
    int ans = 0;
    int* lowcost = new int[n];
    int* closest = new int[n];
    bool* vis = new bool[n];
    for (int i = 0; i < n; i++) {
        lowcost[i] = INF;
        closest[i] = -1;
        vis[i] = false;
    }
    lowcost[start] = 0;
    for (int i = 0; i < n; i++) {
        int k = -1;
        for (int j = 0; j < n; j++) {
            if (!vis[j] && (k == -1 || lowcost[j] < lowcost[k])) {
                k = j;
            }
        }
        if (k == -1) {
            break;
        }
        vis[k] = true;
        ans += lowcost[k];
        for (int j = 0; j < m; j++) {
            if (edges[j].u == k && !vis[edges[j].v] && edges[j].w < lowcost[edges[j].v]) {
                lowcost[edges[j].v] = edges[j].w;
                closest[edges[j].v] = k;
            }
            if (edges[j].v == k && !vis[edges[j].u] && edges[j].w < lowcost[edges[j].u]) {
                lowcost[edges[j].u] = edges[j].w;
                closest[edges[j].u] = k;
            }
        }
    }
    return ans;
}

4.2.3算法的时空分析

4.2.3.1时间复杂度

Kruskal算法的时间复杂度为 \(O(m \log m)\),其中 \(m\) 为边数。

Prim算法的时间复杂度为 \(O(n^2)\),其中 \(n\) 为顶点数。

4.2.3.2空间复杂度

Kruskal算法的空间复杂度为 \(O(n)\),其中 \(n\) 为顶点数。

Prim算法的空间复杂度为 \(O(n)\),其中 \(n\) 为顶点数。

存图的空间复杂度为 \(O(m)\),其中 \(m\) 为边数。

4.2.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

4.2.5实验的收获和体会

通过本次实验,我学会了如何使用Kruskal与Prim算法对图进行最小生成树的求解,并强化了对图的理解。

T3单源最短路径

4.3.1问题描述

对于一个无向有权图,求其单源最短路径。

输入:第一行 \(n, m\),分别表示顶点数与边数;接下来输入 \(m\) 行,每行 \(3\) 个整数 \(u, v, w\),表示一条边;最后输入起点,终点。

输出:输出起点到终点的最短路径。

4.3.2算法描述

4.3.2.1Dijkstra

Dijkstra算法采用贪心策略,每次选择一个与起点距离最小的顶点加入最短路径。

为提高效率,本份代码采用了优先队列进行优化。

4.3.3算法的时空分析

4.3.3.1时间复杂度

采用了堆优化后的Dijkstra算法的时间复杂度为 \(O(n \log n + m)\),其中 \(n\) 为顶点数,\(m\) 为边数。

4.3.3.2空间复杂度

采用了堆优化后的Dijkstra算法的空间复杂度为 \(O(n)\),其中 \(n\) 为顶点数。

存图的空间复杂度为 \(O(m)\),其中 \(m\) 为边数。

4.3.4测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

4.3.5实验的收获和体会

通过本次实验,我学会了如何使用Dijkstra算法对图进行单源最短路径的求解,并强化了对图的理解。

实验五 哈希表

5.1问题描述

使用线性探测法解决哈希冲突构建哈希表HT1,再使用链地址法解决哈希冲突构建哈希表HT2。

输入:第一行 \(n\),表示关键字个数;接下来输入 \(n\) 个关键字;最后输入 \(p\),为所使用的余数。

输出:输出哈希表HT1与HT2的结构特点。

5.2算法描述

哈希表结构比较简单,对于HT1,直接使用数组存储即可;对于HT2,需额外使用链表进行存储。

程序结构也较为简单,仅对HT1与HT2进行了简单封装,其内部使用简单线性结构实现即可,故不再赘述。

5.3调试分析

按各项要求分别逐项进行简单调试,调试非常顺利,最终实现了所有功能。

5.4算法的时空分析

5.4.1时间复杂度

哈希表的插入、查找、删除操作的最优时间复杂度为 \(O(1)\),最坏时间复杂度为 \(O(n)\),其中 \(n\) 为关键字个数。

5.4.2空间复杂度

哈希表的空间复杂度为 \(O(n)\),其中 \(n\) 为关键字个数。

如采用链地址法,哈希表的空间复杂度为 \(O(n + m)\),其中 \(n\) 为关键字个数,\(m\) 为链表的长度。

5.5测试结果分析

测试结果在实验课上已被验收,故本部分不再重复计算与展示。

5.6实验的收获和体会

通过本次实验,我学会了如何使用哈希表解决哈希冲突,并强化了对哈希表的理解。

后记

generate by copilot()

posted @ 2024-12-30 19:49  夕落林中  阅读(41)  评论(0)    收藏  举报