Temp
《数据结构》实验报告
学号:202531400222
姓名:方宇祺
班级:软件工程2501
课程编号:20544
实验1:顺序表的操作
1. 实验内容
本实验要求用顺序表完成几个最基本的操作:初始化、插入、删除、按位置访问、按元素值查找、清空顺序表。输入里会给出一组操作命令,程序需要按顺序执行,并在删除、访问、查找这几种操作出现时输出结果。
这次实验的核心其实很直接,就是把顺序表当成一段连续存储空间来处理。插入时要给新元素腾位置,删除时要把后面的元素往前补齐,访问和查找则分别对应按下标取值和顺序扫描。
2. 算法说明
顺序表采用题目给出的结构体定义:
typedef int ElemType;
typedef struct {
ElemType *elem;
int length;
int listsize;
} Sqlist;几个主要操作的处理思路如下:
- 初始化:为顺序表分配一段连续空间,并把长度设为 0。
- 插入
I a b:在第a个位置前插入元素b。做法是从表尾开始,把第a个位置及其后的元素整体向后移动一位,再把b放到a-1下标处。 - 删除
D a:先保存第a个位置的元素值,再把它后面的元素依次向前移动一位,最后表长减 1,并输出被删除的值。 - 访问
G a:直接输出第a个位置的元素。 - 查找
L b:从前往后扫描顺序表,找到值为b的第一个元素后输出其位置。 - 清空
C:不需要释放空间,直接把长度置为 0 就可以。
从时间复杂度来看,访问操作是 O(1),查找是 O(n),插入和删除在最坏情况下都需要移动一段连续元素,所以也是 O(n)。这也是顺序表比较典型的特点,读起来快,改起来如果动到中间位置就要搬数据。
3. 程序清单
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct {
ElemType *elem;
int length;
int listsize;
} Sqlist;
#define LIST_INIT_SIZE 110
void InitList(Sqlist *L) {
L->elem = (ElemType *)malloc(sizeof(ElemType) * LIST_INIT_SIZE);
L->length = 0;
L->listsize = LIST_INIT_SIZE;
}
void ClearList(Sqlist *L) {
L->length = 0;
}
void ListInsert(Sqlist *L, int pos, ElemType value) {
int i;
for (i = L->length; i >= pos; --i) {
L->elem[i] = L->elem[i - 1];
}
L->elem[pos - 1] = value;
L->length++;
}
ElemType ListDelete(Sqlist *L, int pos) {
int i;
ElemType removed = L->elem[pos - 1];
for (i = pos; i < L->length; ++i) {
L->elem[i - 1] = L->elem[i];
}
L->length--;
return removed;
}
ElemType GetElem(Sqlist *L, int pos) {
return L->elem[pos - 1];
}
int LocateElem(Sqlist *L, ElemType value) {
int i;
for (i = 0; i < L->length; ++i) {
if (L->elem[i] == value) {
return i + 1;
}
}
return 0;
}
int main(void) {
int n, m, i;
Sqlist L;
scanf("%d%d", &n, &m);
InitList(&L);
for (i = 0; i < n; ++i) {
scanf("%d", &L.elem[i]);
}
L.length = n;
for (i = 0; i < m; ++i) {
char op;
scanf(" %c", &op);
if (op == 'I') {
int a, b;
scanf("%d%d", &a, &b);
ListInsert(&L, a, b);
} else if (op == 'D') {
int a;
scanf("%d", &a);
printf("%d\n", ListDelete(&L, a));
} else if (op == 'G') {
int a;
scanf("%d", &a);
printf("%d\n", GetElem(&L, a));
} else if (op == 'L') {
int b;
scanf("%d", &b);
printf("%d\n", LocateElem(&L, b));
} else if (op == 'C') {
ClearList(&L);
}
}
free(L.elem);
return 0;
}4. 运行结果
以题目给出的样例为例:
输入:
5 6
1 2 3 4 5
D 3
D 1
G 1
G 2
G 3
L 4输出:
3
1
2
4
5
2运行结果和题目样例一致,说明程序能够正确完成删除、访问和查找等操作。
5. 分析与思考
这次实验比较基础,但顺序表的特点挺明显。它的优点是结构简单、存储紧凑、按位置访问很快,像 G a 这种操作几乎是直接取值;缺点也很明显,只要在中间插入或删除元素,就得把后面的数据整体挪动一遍,所以数据量一大,效率就会掉下来。
另外,这份程序是按题目“操作一定合法”这个前提写的,所以没有额外做越界判断和扩容处理。如果放到更完整一点的场景里,这两件事最好补上,不然表长超出上限或者位置非法时,程序就不够稳。说白了,这份代码适合实验题和基础练习,要做成更通用的线性表实现,还得把状态返回、容量扩展这些细节补齐。
实验2:链表及其多项式相加
1. 实验内容
这次实验要做的是输入两个一元多项式,各自建立成单链表,然后把两个多项式按指数对应相加,最后输出结果多项式。题目已经说明输入的各项是按指数从小到大给出的,这一点挺关键,因为后面做链表合并时就可以直接像归并那样往后走,不用来回找。
这个实验和顺序表那题放在一起看,其实差别一下就出来了。顺序表处理中间插入、删除时要搬动元素,链表这里就不一样,节点一接一断就能完成结构调整,所以拿它来表示多项式这种“项数不固定、还要按指数合并”的东西很合适。
2. 算法说明
单链表节点中保存两个信息,一个是系数 coef,一个是指数 exp,再加一个 next 指针指向下一项。整个多项式用带头结点的单链表表示,链表中的各项始终按指数从小到大排列。
typedef struct Node {
int coef;
int exp;
struct Node *next;
} Node;几个主要函数的处理思路如下:
- 建表函数
CreatePolynomial:先创建头结点,再依次读入每一项,采用尾插法把新结点接到链表末尾。因为输入本身已经按指数递增,所以不需要额外排序。 - 相加函数
AddPolynomial:同时用两个指针分别扫描两个多项式。若某一边指数更小,就把该结点直接接到结果链表后面;若两个指数相同,就把系数相加。相加后如果系数不为 0,就保留该项;如果等于 0,就把这一项删掉,不输出。 - 输出函数
PrintPolynomial:从头结点后的第一个有效结点开始遍历,依次输出每一项的系数和指数。
这个过程本质上就是两个有序链表的合并。由于每个结点最多只会被访问一次,所以多项式相加的时间复杂度是 O(n + m),空间上除了头结点和少量辅助指针,没有再开大块额外存储,整体比较直接。
3. 程序清单
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int coef;
int exp;
struct Node *next;
} Node;
Node *CreatePolynomial(int n) {
Node *head = (Node *)malloc(sizeof(Node));
Node *tail = head;
int i;
head->next = NULL;
for (i = 0; i < n; ++i) {
int coef, exp;
Node *node;
scanf("%d%d", &coef, &exp);
if (coef == 0) {
continue;
}
node = (Node *)malloc(sizeof(Node));
node->coef = coef;
node->exp = exp;
node->next = NULL;
tail->next = node;
tail = node;
}
return head;
}
Node *AddPolynomial(Node *La, Node *Lb) {
Node *pa = La->next;
Node *pb = Lb->next;
Node *head = (Node *)malloc(sizeof(Node));
Node *tail = head;
head->next = NULL;
while (pa != NULL && pb != NULL) {
if (pa->exp < pb->exp) {
tail->next = pa;
tail = pa;
pa = pa->next;
} else if (pa->exp > pb->exp) {
tail->next = pb;
tail = pb;
pb = pb->next;
} else {
int sum = pa->coef + pb->coef;
Node *nexta = pa->next;
Node *nextb = pb->next;
if (sum != 0) {
pa->coef = sum;
tail->next = pa;
tail = pa;
} else {
free(pa);
}
free(pb);
pa = nexta;
pb = nextb;
}
}
if (pa != NULL) {
tail->next = pa;
} else {
tail->next = pb;
}
free(La);
free(Lb);
return head;
}
void PrintPolynomial(Node *L) {
Node *p = L->next;
while (p != NULL) {
printf("%d %d\n", p->coef, p->exp);
p = p->next;
}
}
void DestroyPolynomial(Node *L) {
Node *p = L;
while (p != NULL) {
Node *next = p->next;
free(p);
p = next;
}
}
int main(void) {
int n, m;
Node *La;
Node *Lb;
Node *Lc;
scanf("%d", &n);
La = CreatePolynomial(n);
scanf("%d", &m);
Lb = CreatePolynomial(m);
Lc = AddPolynomial(La, Lb);
PrintPolynomial(Lc);
DestroyPolynomial(Lc);
return 0;
}4. 运行结果
题目样例如下:
输入:
3
1 2
2 3
3 4
3
4 3
2 5
4 6输出:
1 2
6 3
3 4
2 5
4 6本程序测试结果与样例一致,提交后也已经通过评测,说明链表建立、合并相加和结果输出这几个部分都能正常完成。
5. 分析与思考
这题用链表来表示多项式,确实比顺序存储更顺手。因为多项式相加时,真正麻烦的不是按位置访问,而是不同指数项之间的拼接和合并。链表在这方面比较省事,某一项该保留就接上去,该删除就断开并释放,不需要像顺序表那样整体挪数据。
当然,链表也不是没有代价。一方面它不能像数组那样直接按下标访问,查某一项时还是得顺着指针往后走;另一方面每插入一个结点都要动态申请内存,代码量和细节处理也会多一点。这份实现已经能满足题目要求,不过如果要写得更完整些,还可以继续补内存申请失败判断,或者把释放逻辑再封装得更清楚一点,这样程序会更稳。
实验3:多重括号匹配判断
1. 实验内容
这次实验要求判断一个只由圆括号和方括号组成的表达式是否匹配。所谓匹配,说白了就是两件事得同时满足,一是左括号和右括号类型要对应,二是闭合顺序也得对,不能前面开的括号让后面不相干的括号先关掉。
这类题很典型,基本就是栈的直接应用。因为括号匹配天然符合“后进先出”这个特点,最后出现的左括号,应该最先等到和它对应的右括号,所以拿栈来做会很顺。
2. 算法说明
程序里使用顺序栈来保存扫描过程中遇到的左括号,栈中元素类型为字符。遍历输入字符串时,分两种情况处理:
- 如果当前字符是
(或[,就直接入栈。 - 如果当前字符是
)或],先判断栈是否为空。若栈空,说明没有可以匹配的左括号,表达式一定不合法;若栈不空,就弹出栈顶元素,检查括号类型是否对应。
当整个字符串扫描结束后,还要再判断一次栈是否为空。如果栈为空,说明所有左括号都已经正确匹配;如果栈里还有剩余元素,说明存在未闭合的左括号,结果也应该判为不匹配。
程序中另外写了一个 Match 函数,用来判断一对左右括号是否属于同一类型。整个算法只需要把字符串从头扫一遍,所以时间复杂度是 O(n),空间复杂度也是 O(n),这里的 n 是表达式长度。
3. 程序清单
#include <stdio.h>
#include <string.h>
#define MAXN 600
typedef struct {
char data[MAXN];
int top;
} SqStack;
void InitStack(SqStack *S) {
S->top = -1;
}
int StackEmpty(SqStack *S) {
return S->top == -1;
}
void Push(SqStack *S, char ch) {
S->data[++S->top] = ch;
}
char Pop(SqStack *S) {
return S->data[S->top--];
}
int Match(char left, char right) {
if (left == '(' && right == ')') {
return 1;
}
if (left == '[' && right == ']') {
return 1;
}
return 0;
}
int main(void) {
char str[MAXN];
int i, len;
SqStack S;
scanf("%s", str);
len = (int)strlen(str);
InitStack(&S);
for (i = 0; i < len; ++i) {
if (str[i] == '(' || str[i] == '[') {
Push(&S, str[i]);
} else {
if (StackEmpty(&S)) {
printf("no\n");
return 0;
}
if (!Match(Pop(&S), str[i])) {
printf("no\n");
return 0;
}
}
}
if (StackEmpty(&S)) {
printf("yes\n");
} else {
printf("no\n");
}
return 0;
}4. 运行结果
题目给出了两组样例。
样例一输入:
[]()[]输出:
yes样例二输入:
([[]])[)输出:
no本程序测试结果与题目样例一致,提交后已经通过评测,说明括号入栈、出栈以及类型匹配判断都没有问题。
5. 分析与思考
这题其实不复杂,但很能说明栈这种结构到底适合干什么。只要场景里出现“最后进去的东西要先拿出来”这种顺序,栈基本就很好用。括号匹配就是这样,越靠后的左括号,越应该先等到自己的右括号。
另外,这题也能看出顺序栈的实现很直接,定义一个数组和栈顶指针就够了,写起来不重。不过它也有前提,就是要先大致知道数据规模,好提前开够空间。这里字符串长度不超过 550,所以直接开定长数组完全够用。要是数据规模再大一些,或者输入长度不固定,那就可以考虑链栈或者动态扩容的写法。
实验4:循环队列插入与删除操作
1. 实验内容
这次实验要求实现一个环形队列,支持两种基本操作,一个是 enqueue x,表示元素进队,另一个是 dequeue,表示元素出队。队列初始为空,如果执行出队时队列里没有元素,就输出 -1。
这题看起来就是普通队列题,但专门强调了“循环队列”,意思就是数组空间要循环利用。这样做的好处很直接,队头元素出队以后,前面空出来的位置不会浪费掉,后面的新元素还能继续往回放,不然顺序队列用久了很容易出现“后面满了、前面空着却不能用”的情况。
2. 算法说明
程序中用一个数组来模拟循环队列,再配两个指针 front 和 rear 分别表示队头位置和队尾的下一位置。初始化时让二者都等于 0,这时队列为空。
队列操作规则如下:
- 入队
EnQueue:先把新元素存入rear所在位置,再把rear按照循环方式向后移动一格,即rear = (rear + 1) % MAXQ。 - 出队
DeQueue:先取出front所在位置的元素,再把front按照同样的方式向后移动一格。 - 判空
QueueEmpty:当front == rear时,说明队列中没有元素。
因为题目给出的操作总数不超过 31000,而且出队不会导致非法访问,所以这里直接开一个足够大的数组就能满足要求。每次入队和出队都只做常数次操作,所以时间复杂度是 O(1),整题总复杂度就是 O(q)。
3. 程序清单
#include <stdio.h>
#include <string.h>
#define MAXQ 31010
typedef struct {
int data[MAXQ];
int front;
int rear;
} CirQueue;
void InitQueue(CirQueue *Q) {
Q->front = 0;
Q->rear = 0;
}
int QueueEmpty(CirQueue *Q) {
return Q->front == Q->rear;
}
void EnQueue(CirQueue *Q, int x) {
Q->data[Q->rear] = x;
Q->rear = (Q->rear + 1) % MAXQ;
}
int DeQueue(CirQueue *Q) {
int value = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXQ;
return value;
}
int main(void) {
int q, i;
char op[20];
CirQueue Q;
InitQueue(&Q);
scanf("%d", &q);
for (i = 0; i < q; ++i) {
scanf("%s", op);
if (strcmp(op, "enqueue") == 0) {
int x;
scanf("%d", &x);
EnQueue(&Q, x);
} else if (QueueEmpty(&Q)) {
printf("-1\n");
} else {
printf("%d\n", DeQueue(&Q));
}
}
return 0;
}4. 运行结果
题目样例如下:
输入:
4
enqueue 123
dequeue
dequeue
enqueue 1输出:
123
-1本程序测试结果与样例一致,提交后已经通过评测,说明循环队列的进队、出队和空队判断都实现正确。
5. 分析与思考
这题的重点其实不是队列本身,而是“循环”这两个字。普通顺序队列也能入队出队,但如果只是简单地让队头、队尾一直往后走,用久了就会浪费前面已经腾出来的空间。循环队列把数组首尾接起来之后,这个问题就解决了。
当然,循环队列写的时候也有个很容易搞混的地方,就是 front 和 rear 到底表示什么位置。如果这个约定没先想清楚,后面判空、出队和移动指针就容易写乱。这次实现里把 rear 定义成“队尾元素的下一位置”,这样整个逻辑会顺一些,判空条件也比较自然。
实验5:字符串的模式匹配(KMP)
1. 实验内容
这次实验要求给定一个源串 S,再输入若干个模式串,判断每个模式串是不是 S 的子串。题目标题已经点明了要用 KMP,所以重点不只是把结果做出来,而是要按模式匹配的思路来实现。
如果直接用朴素匹配,模式串一旦失配,就只能把模式串整体往后挪一位重新比,这样在某些情况下会有很多重复比较。KMP 的好处就在这儿,它会利用已经匹配过的信息,在失配时把模式串移动到更合适的位置,少做很多没必要的回退。
2. 算法说明
程序分成两个核心部分,一个是求模式串的 next 数组,另一个是利用 next 数组进行匹配。
GetNext:根据模式串本身的前缀和后缀关系,求出每个位置对应的最长相等前后缀长度。这样一来,后面一旦失配,就知道模式串应该跳到哪里继续比较。KMP:用两个指针分别扫描源串和模式串。如果当前字符相同,就一起向后移动;如果失配,就根据next数组调整模式串指针,而不是让源串回退。
当模式串指针走到模式串末尾时,就说明找到了一个完整匹配,返回 1;如果整个源串都扫描完还没匹配成功,就返回 0。因为每个字符最多被处理常数次,所以一次匹配的时间复杂度是 O(|S| + |P|),其中 |S| 是源串长度,|P| 是模式串长度。
3. 程序清单
#include <stdio.h>
#include <string.h>
#define MAXS 100010
#define MAXP 3510
void GetNext(const char *pattern, int *next) {
int i, j;
int m = (int)strlen(pattern);
next[0] = 0;
for (i = 1, j = 0; i < m; ++i) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
if (pattern[i] == pattern[j]) {
++j;
}
next[i] = j;
}
}
int KMP(const char *text, const char *pattern) {
int i, j;
int n = (int)strlen(text);
int m = (int)strlen(pattern);
int next[MAXP];
GetNext(pattern, next);
for (i = 0, j = 0; i < n; ++i) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
if (text[i] == pattern[j]) {
++j;
}
if (j == m) {
return 1;
}
}
return 0;
}
int main(void) {
char s[MAXS];
char str[MAXP];
int n, i;
scanf("%s", s);
scanf("%d", &n);
for (i = 0; i < n; ++i) {
scanf("%s", str);
if (KMP(s, str)) {
printf("yes\n");
} else {
printf("no\n");
}
}
return 0;
}4. 运行结果
题目样例如下:
输入:
acmicpczjnuduzongfei
3
icpc
du
liu输出:
yes
yes
no本程序测试结果与样例一致,提交后已经通过评测,说明 next 数组构造和 KMP 匹配过程都正确。
5. 分析与思考
这题最有意思的地方就在于,KMP 不是单纯“把字符串拿来比一遍”,而是先利用模式串本身的结构信息,给后面的匹配过程铺路。失配以后,模式串不用傻傻地从头再来,而是直接跳到还能继续尝试匹配的位置,这就是它比朴素算法更高效的原因。
当然,KMP 写起来也比普通暴力匹配更容易绕,尤其是 next 数组下标、失配后的跳转位置,还有指针什么时候加一,这几处都很容易写错。这次实验做完以后,对模式匹配这类问题的感觉会更清楚一些,至少再看到“子串查找”这种题,就知道不只是循环硬扫,还可以用更讲究的办法来做。
实验6:稀疏矩阵的建立与转置
1. 实验内容
这次实验要求输入稀疏矩阵的行数、列数以及所有非零元素的信息,先建立它的三元组存储结构,再完成矩阵转置,并把转置前后的矩阵都按普通二维矩阵的形式输出出来。
这题的重点其实不在“打印矩阵”,而在“稀疏矩阵怎么存”。因为矩阵里大部分位置都是 0,如果还按普通二维数组整块存,就有点浪费了。三元组的做法更合适,只记录那些真正非零的位置,也就是行号、列号和值这三样东西。
2. 算法说明
程序中定义了一个三元组结构 Triple,用来保存一个非零元素的行、列和值;再用 TSMatrix 结构保存整个稀疏矩阵的行数、列数、非零元素个数,以及所有三元组数据。
typedef struct {
int row;
int col;
int value;
} Triple;
typedef struct {
Triple data[MAXSIZE];
int rows;
int cols;
int nums;
} TSMatrix;几个主要步骤如下:
- 建立矩阵
CreateMatrix:读入矩阵规模和非零元素个数,再把每个非零元素的行、列和值依次存入三元组数组。 - 转置矩阵
TransposeMatrix:转置后,原来的行和列互换,所以把每个非零元素的row和col对调即可。为了保证转置后的三元组仍然按“从上到下、从左到右”的顺序存储,程序按列扫描原矩阵的三元组,并依次生成转置结果。 - 输出矩阵
PrintMatrix:根据三元组数组中的非零元素位置,把矩阵逐行逐列还原输出;如果当前位置没有对应的三元组,就输出 0,格式统一用%5d。
由于转置部分用了两层循环,时间复杂度大致是 O(cols * nums)。在这道题的数据范围下,矩阵规模很小,这种写法已经完全够用,而且逻辑直观,不容易写乱。
3. 程序清单
#include <stdio.h>
#define MAXSIZE 410
typedef struct {
int row;
int col;
int value;
} Triple;
typedef struct {
Triple data[MAXSIZE];
int rows;
int cols;
int nums;
} TSMatrix;
void CreateMatrix(TSMatrix *M) {
int i;
scanf("%d%d%d", &M->rows, &M->cols, &M->nums);
for (i = 0; i < M->nums; ++i) {
scanf("%d%d%d", &M->data[i].row, &M->data[i].col, &M->data[i].value);
}
}
void TransposeMatrix(TSMatrix *M, TSMatrix *T) {
int col, k, index = 0;
T->rows = M->cols;
T->cols = M->rows;
T->nums = M->nums;
for (col = 0; col < M->cols; ++col) {
for (k = 0; k < M->nums; ++k) {
if (M->data[k].col == col) {
T->data[index].row = M->data[k].col;
T->data[index].col = M->data[k].row;
T->data[index].value = M->data[k].value;
++index;
}
}
}
}
void PrintMatrix(TSMatrix *M) {
int i, j, k = 0;
for (i = 0; i < M->rows; ++i) {
for (j = 0; j < M->cols; ++j) {
if (k < M->nums && M->data[k].row == i && M->data[k].col == j) {
printf("%5d", M->data[k].value);
++k;
} else {
printf("%5d", 0);
}
}
printf("\n");
}
}
int main(void) {
TSMatrix M, T;
CreateMatrix(&M);
TransposeMatrix(&M, &T);
PrintMatrix(&M);
printf("\n");
PrintMatrix(&T);
return 0;
}4. 运行结果
题目样例如下:
输入:
6 7 8
0 1 12
0 2 9
2 1 3
2 5 14
3 2 24
4 1 18
5 0 15
5 3 -7输出:
0 12 9 0 0 0 0
0 0 0 0 0 0 0
0 3 0 0 0 14 0
0 0 24 0 0 0 0
0 18 0 0 0 0 0
15 0 0 -7 0 0 0
0 0 0 0 0 15
12 0 3 0 18 0
9 0 0 24 0 0
0 0 0 0 0 -7
0 0 0 0 0 0
0 0 14 0 0 0
0 0 0 0 0 0本程序测试结果与样例一致,提交后已经通过评测,说明稀疏矩阵建立、转置和按格式输出都没有问题。
5. 分析与思考
这题做完以后,能很明显感觉到三元组存储的价值。对于非零元素很少的矩阵,真正有用的信息其实就那么几项,没必要把一大堆 0 也老老实实存起来。用三元组以后,数据结构会更紧凑,处理起来也更有针对性。
不过这份实现也能看出一个点,就是现在的转置方法虽然直观,但还不算最快。它是按列去扫所有非零元素,所以规模再大一点时,效率就没那么漂亮了。后面如果继续学稀疏矩阵,通常还会碰到“快速转置”这种写法,会进一步利用每一列非零元素的个数来减少扫描次数。对实验题来说,这一版已经够用了,而且思路清楚,比较适合理解三元组和转置的基本过程。