数据结构课设报告

1. 设计方案

算法与数据结构

1.单链表的基本操作及应用

序号 名称 函数名
1 创建链表 LinkList::LinkList()
2 插入结点 Create_List(int i)
3 删除结点 Delete_List_Head(); Delete_List_End(); Delet_List_Point(int i, int &e); DeleteAll();
4 查找结点 Find(int i)
5 显示链表 print()
6 通讯录(查找联系人) Address_books::ElemType Find(string info)
7 通讯录(删除联系人) Address_books::Delete()
8 通讯录(修改联系人) Address_books::Change_contact()

2.栈的基本操作及应用

序号 名称 函数名
1 初始化栈 InitStack(Stack &s)
2 进栈 Push(Stack &s, int e, int i)
3 出栈 Pop(Stack &s, int &e, int i)
4 取栈顶元素 GetTop(Stack s, int &e)
5 表达式求值(最终运算操作) EvalueateExpression(const char *expression)
6 表达式求值(对操作数进行四则运算) operate(int a, char theta, int b)

3.数组的基本操作及应用

序号 名称 函数名
1 以三元组方式创建矩阵 CreateSMtrix(TSMatrix & m)
2 显示三元组 PrintSMtrix(TSMatrix m)
3 矩阵乘法 MultSMatrix(TSMatrix A, TSMatrix B, TSMatrix & C)

4.二叉树的基本操作

序号 名称 函数名
1 创建二叉树 CreateBiTree(BiTree &T)
2 先序遍历二叉树 PreOrderTraverse(BiTree T, Status(*Visit)(TElemType e))
3 中序遍历二叉树 InOrderTraverse(BiTree T, Status(*Visit)(TElemType e))
4 后序遍历二叉树 PostOrderTraverse(BiTree T, Status(*Visit)(TElemType e))
5 计算叶子结点数 BiTree_leaf_nodes(BiTree T)
6 树的深度 BiTree_depth(BiTree T)
7 结点的双亲 BiTree_parents(BiTree T, char e)
8 结点的兄弟 BiTree_lbrothers(BiTree T, char e);BiTree_rbrothers(BiTree T, char e)

5.图的基本操作

序号 名称 函数名
1 有向图的创建 CreateDG(ALGraph &G)
2 无向图的创建 CreateUDG(ALGraph &G)
3 有向网的创建 CreateDN(ALGraph &G)
4 无向网的创建 CreateUDN(ALGraph &G)
5 打印邻接表 Print_vertices(ALGraph &G)
6 深度搜索 DFS(ALGraph G, int v)
7 广度搜索 BFSTraverse(ALGraph G)
8 拓扑排序 Topological_sort(ALGraph G)

2. 实现过程

1.单链表的应用(通讯录)

由于通讯录的实现包含了单链表的基本操作,这里只讨论通讯录的实现

①定义一个通讯录类Address_books

在类中定义结构体Book包括:姓名name,定义一个string类型来存储好友姓名。

​ 电话号tel,定义一个string类型来存储好友电话号。

在类中定义结构体Node,在Node结构体中定义一个Book型变量,定义一个Node型指针指向该结构体。

并在类中添加Node*类型的head成员变量

②在构造函数中进行初始化 Address_books()

Address_books()函数中,即初始化head头结点

③定义一个插入结点(联系人)的函数Create_contact()

Create_contact()函数中,当位置参数传入函数体中,首先判断传进的位置参数是否合理,判断合理之后,去要找该位置,断开链子,将结点插入其中。

④定义一个删除结点(联系人)的函数Delete()

Delete()函数中,与插入函数类似,首先判断传入的位置参数是否合理,找到该结点,删除结点之后,将前一个结点的指针域指向下一个结点的空间。

⑤定义一个查找结点(联系人)的函数Find()

Find()函数中,首先输入好友的姓名,判断字符串是否相等找到联系人,并输出该联系人的相关信息。

⑥显示函数print()

print()函数需要传入一个指向链表的头指针,判断头指针不为空时,输出链表里所有的结点元素(联系人信息)。

⑦显示链表菜单函数Show_Books()

Show_Books()函数的菜单项是通过switch()语句实现选择功能。

2.栈的基本操作及应用

①定义一个栈的结构体

结构体中包括:栈底指针base,栈顶指针top。

②定义一个进栈函数

​ 用Push()函数,将元素压入栈低,S.top++;,栈顶指针加一。

③定义一个出栈函数

​ 将后进入的元素弹出,e = *(S.top-1); S.top–;栈顶指针减一。

④定义一个取栈顶元素

保存栈顶元素,并把值带回主函数,top指针不变。

⑤表达式求值的应用

定义一个操作符栈,一个操作数栈,建议优先级表,对输入的表达式进行条件入栈出栈操作,实现表达式求值的功能

3.数组的基本操作及应用

① 创建三元组

要求用户输入稀疏矩阵的行数列数,非零元个数,然后进行判断接下来的输入是否合理和输入完成,把输入的值依次加入Triple data[MAXSIZE]中

② 显示三元组

循环打印三元组的非零元行、列下标及其数据。

③ 矩阵乘法(应用)

为了简化矩阵乘法运算,首先找到矩阵中的非零元,存储于三元组数组data[MAXRL+1]中,然后再将数组中对应行、列的非零元相乘、再相加,将和及对应的行、列下标存储于三元组W中,通过打印三元组函数PrintSMtrix(TSMatrix m),将矩阵相乘结果输出。得到非零元所在的行、列下标及其数据。

4.二叉树的基本操作

① 创建二叉树

构造一个链式存储结构体,其中包括数据域、左、右孩子指针。设计一个以先序方式创建二叉树,通过递归调用CreateBiTree(),T->data=ch;生成根结点CreateBiTree(T->lchild);构造左子树CreateBiTree(T->rchild); 构造右子树,完成构造二叉树。

② 遍历二叉树(先序、中序、后序)

先序遍历二叉树PreOrderTraverse(),通过设计Visit()显示函数,递归InOrderTraverse(T->lchild,Visit)遍历左子树,递归InOrderTraverse(T->rchild,Visit)遍历右子树。访问顺序:根、左、右。

中序遍历二叉树InOrderTraverse(),递归PostOrderTraverse(T->lchild,Visit)遍历左子树,递归PostOrderTraverse(T->rchild,Visit)遍历右子树。访问顺序:左、根、右。

后序遍历二叉树PostOrderTraverse (),递归PostOrderTraverse (T->lchild,Visit)遍历左子树,递归PostOrderTraverse (T->rchild,Visit)遍历右子树。访问顺序:左、右、根。

③ 计算叶子结点个数及树的深度

叶子节点个数:递归调用BiTree_leaf_nodes(),当结点的左右孩子都无,

1
return BiTree_leaf_nodes(T->lchild) + BiTree_leaf_nodes(T->rchild) + 1;

否则

1
return BiTree_leaf_nodes(T->lchild) + BiTree_leaf_nodes(T->rchild);

树的深度:递归调用Depth(),输出左子树、右子树中最大值。

④ 查找指定结点的双亲

非递归BiTree_parents()函数,遍历根结点的左右子树,获取所有结点的情况,在最后进行打印

⑤ 查找指定结点的兄弟

递归调用找左兄弟BiTree_lbrothers()函数,遍历结点的左右子树,看双亲,输出结果。

递归调用找右兄弟BiTree_rbrothers()函数,遍历结点的左右子树,看双亲,输出结果。

5.图的基本操作

①创建无向图

先构造邻接矩阵结构体AdjList vertices; 函数CreateDG(ALGraph & G)要求先输入顶点数与边数,用于输入判断。然后再输入所有顶点信息和边的信息存入邻接表vertices中,输入完成后自动提示,并打印对应的邻接表

②创建有向图

③创建无向网

网相对于图多了一个权重,表结点结构体中的info用于存储权值,在创建网的函数中输入权值的信息。并在创建完成后打印邻接表

④创建有向网

⑤DFS和BFS遍历

深度优先遍历图:构造访问标记数组visited[],递归调用DFS(),直至图中所有结点被访问来实现深度遍历图。

广度优先遍历图:类似于树的层序遍历,同时也需要构造访问数组visited[],个人是调用STL中的队列queue来实现。

⑥拓扑排序

Topological_sort(ALGraph G);在头结点的结构体中加了一个int型的conut成员用于存储结点的入度,入度的获取在创建图(网)的函数中实现。先将入度为0的结点入栈,然后实现拓扑排序的算法原理,计算出最后的顶点数,若小于当前图的顶点数,则说明有回路,打印出结果,若无回路,提示并打印拓扑有序序列

3. 实现代码

通讯录的数据结构及部分实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
struct Book {
string name;
string tel;
};
struct Node {
Book data;
Node * next;
};
Node *head;
Node *_temp; //用作临时结点指针方便函数调用


void Address_books::Change_contact()//修改联系人信息
{
string info;
cout << "输入你要修改的联系人的相关信息(姓名或联系方式):";
cin >> info;
if (!Find(info))
return;

else {
cout << "输入该联系人新的姓名:";
cin >> info;
this->_temp->next->data.name = info;
cout << "输入该联系人新的电话号码:";
cin >> info;
this->_temp->next->data.tel = info;
cout << endl << "联系人信息更新成功!" << endl;
}

}

void Address_books::Delete()//删除联系人
{
string info;
cout << "输入你要删除的联系人的相关信息(姓名或联系方式):";
cin >> info;
if (!Find(info))
return;

else {
cout << "确定删除联系人 "<< _temp->next->data.name <<" 的信息吗? (y or n)?:";
char c;
cin >> c;
if (c == 'y' || c == 'Y') {
Node *q = _temp->next;
this->_temp->next = _temp->next->next;
delete q;
q = NULL;
cout << endl << "联系人信息删除成功!" << endl;
}
else
return;
}
}

void Address_books::Find_contact()//查找联系人
{
string info;
cout << "输入你要查找的联系人的相关信息(姓名或联系方式):";
cin >> info;
Find(info);
}

ElemType Address_books::Find(string info)
{
Node *temp = head->next;
this->_temp = head; //查找第一个结点时没有进入while循环_temp为空的情况
if (head->next == NULL)
{
cout << "空电话本,没有联系人!" << endl;
return FALSE;
}
while (temp != NULL && temp->data.name != info && temp->data.tel!=info)
{
this->_temp = temp; //_temp指向信息结点的前一个,方便删除
temp = temp->next;
}
if (!temp)
{
cout << "查找失败,相关联系人不存在!" << endl;
return FALSE;
}
else
cout << "查找成功,相关联系人信息如下: " << endl
<< "姓名:" << temp->data.name << endl << "电话号码:" << temp->data.tel << endl;
return TRUE;
}

表达式求值的数据结构及部分函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//定义操作数栈
typedef struct OPERANDSTACK {
int *base;
int *top;
int stacksize;
}OPERANDSTACK;

//定义操作符栈
typedef struct OPERATORSTACK {
char *base;
char *top;
int stacksize;
}OPERATORSTACK;

//判断c是否是一个运算操作符
bool isOperator(char c)
{
switch (c) {
case '+':
case '-':
case '*':
case '/':
case '(':
case ')':
case '#': //将'#'作为运算符栈opter的栈底元素,判断表达式是否求值完毕
return true;
break;
default:
return false;
break;
}
}

//如果以*c开始的操作数后仍是数字就加上前面的数字进行合并
const char *getOpnd(const char *c, int *op)
{
//获得以*c开始的操作数,返回后c为操作符
int sum = 0, temp;
while (!isOperator(*c))
{
temp = *c - '0';
sum = sum * 10 + temp;
c++;
}
*op = sum;

return c;
}

char op[7][7] =
{
{ '>', '>', '<', '<', '<', '>', '>' },
{ '>', '>', '<', '<', '<', '>', '>' },
{ '>', '>', '>', '>', '<', '>', '>' },
{ '>', '>', '>', '>', '<', '>', '>' },
{ '<', '<', '<', '<', '<', '=', ' ' },
{ '>', '>', '>', '>', ' ', '>', '>' },
{ '<', '<', '<', '<', '<', ' ', '=' }
};

//优先级的处理
char precede(char op1, char op2)
{
int i, j; //用来记录优先级坐标点位置
switch (op1) {
case '+': i = 0; break;
case '-': i = 1; break;
case '*': i = 2; break;
case '/': i = 3; break;
case '(': i = 4; break;
case ')': i = 5; break;
case '#': i = 6; break;
}
switch (op2) {
case '+': j = 0; break;
case '-': j = 1; break;
case '*': j = 2; break;
case '/': j = 3; break;
case '(': j = 4; break;
case ')': j = 5; break;
case '#': j = 6; break;
}
return op[i][j];
}

稀疏矩阵的数据结构及乘法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*---稀疏矩阵的三元组顺序表存储表示---*/
struct Triple{
int i, j; //非零元的行下标和列下标
ElemType e;
};

struct TSMatrix {
Triple data[MAXSIZE]; //非零元的三元组表
int row, col, total; //矩阵的行数、列数、非零元个数
};

Status MultSMatrix(TSMatrix A, TSMatrix B, TSMatrix & C)
{// 将稀疏矩阵A和B进行乘法运算后赋给矩阵C
// 原理:用A的一行的元素与B的每一列的元素对应相乘再相加求解
// 时间复杂度:O(A.row*B.col+A.total/A.col*B.total)

if (A.col != B.row)
{
cout << "A的列标不等于B的行标!计算失败" << endl;
return ERROR;
}

C.row = A.row;
C.col = B.col;
C.total = 0;

/*memset()用于逐字节的把整个数组设置为一个指定的值*/
int* Anum = new int[A.row + 1];
memset(Anum, 0, (A.row + 1) * sizeof(int));
int* Arpos = new int[A.row + 1];
memset(Arpos, 0, (A.row + 1) * sizeof(int));

int* Bnum = new int[B.row + 1];
memset(Bnum, 0, (B.row + 1) * sizeof(int));
int* Brpos = new int[B.row + 1];
memset(Brpos, 0, (B.row + 1) * sizeof(int));

int i, arow, p, p_up, brow, q, q_up, ccol;

for (i = 0; i < A.total; i++)
Anum[A.data[i].i]++;
//计算A中每行的非零元在data中的起始下标
for (i = 1; i <= A.row; i++)
Arpos[i] = Arpos[i - 1] + Anum[i - 1];

for (i = 0; i < B.total; i++)
Bnum[B.data[i].i]++;
//计算B中每行的非零元在data中的起始下标
for (i = 1; i <= B.row; i++)
Brpos[i] = Brpos[i - 1] + Bnum[i - 1];

int* mult = new int[B.col + 1];

for (arow = 0; arow <= A.row; arow++)
{

p_up = arow < A.row ? Arpos[arow + 1] : A.total; //条件表达式,若arow<A.row,执行:前一条语句,否则后一条
for (p = Arpos[arow]; p < p_up; p++)
{
memset(mult, 0, (B.col + 1) * sizeof(int));

brow = A.data[p].j;
q_up = brow < B.row ? Brpos[brow + 1] : B.total;
for (q = Brpos[brow]; q < q_up; q++)
{
mult[B.data[q].j] += A.data[p].e * B.data[q].e;
}
for (ccol = 1; ccol <= B.col; ccol++)
{
if (fabs(mult[ccol])>1e-7)
{
C.data[C.total].i = arow;
C.data[C.total].j = ccol;
C.data[C.total].e = mult[ccol];

C.total++;
}
}
}
}
delete[] Anum;
delete[] Arpos;
delete[] Bnum;
delete[] Brpos;

return OK;
}

求二叉树叶子结点及双亲的实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <vector>
#include <stack>
int BiTree_leaf_nodes(BiTree T)
{
if (T == NULL)
return 0;
if (T->lchild == 0 && T->rchild == 0)
return BiTree_leaf_nodes(T->lchild) + BiTree_leaf_nodes(T->rchild) + 1;
else
return BiTree_leaf_nodes(T->lchild) + BiTree_leaf_nodes(T->rchild);
}

void BiTree_parents(BiTree T, char e)
{
int count = 0; //记录相同值的结点数
bool root = false; //记录结点是根节点的情况
vector<char> v1; //存储具有双亲结点的双亲结点值
stack<BiTree> S; //非递归先序遍历

if (T == NULL)
return;
if (T->data == e) {
count++;
root = true;
}
while (!S.empty() || T)
{
while (T)
{
S.push(T);
if ((T->lchild && T->lchild->data == e) || (T->rchild && T->rchild->data == e)) {
count++;
v1.push_back(T->data);
}
T = T->lchild;
}
T = S.top(); S.pop();
T = T->rchild;
}
cout << "该值对应的结点共有 " << count << " 个,对应的双亲为:" << endl;
int n = 0;
if (root) {
cout << "第" << ++n <<"个结点为根结点,无双亲结点!" << endl;
}

for (auto i : v1) {
cout << "第" << ++n << "个结点的双亲结点:" << i << endl;
}
}

图的拓扑排序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Topological_sort(ALGraph G)
{
stack<int> s;
int i, k;
//入度为0入栈
for (i = 0; i < G.vexnum; ++i)
{
if (!G.vertices[i].count)//把入度为零的点先放入栈中
s.push(i);
}
int count = 0;
ArcNode *p = new ArcNode;
while (!s.empty())
{
i = s.top();
s.pop();
cout << G.vertices[i].data << "-";
count++;
for (p = G.vertices[i].firstarc; p ; p = p->nextarc)
{
k = p->adjvex;
if (!(--G.vertices[k].count))
s.push(k);
}
}
if (count < G.vexnum)
{
cout << "该图(网)有回路!";
cout << endl;
}
else
cout << "该图(网)无回路";

}

4. 测试

链表

1528951391042

1528952033074

栈的操作

1528952207864

1528952855802

稀疏矩阵乘法

1528953148654

二叉树

1528953287141

1528953337914

图的操作

1528953521869

5. 结论

这个系统人性化方面和界面做得还算不错,但还是有不足的地方,例如在每个函数中得到exit字符的话会退出当前函数以免每次输入错误无法返回上一级;console的清屏效果也没有实现,因为调用system(‘cls’)来实现的话不会保存之前的记录,我想实现linux中的clear会把屏幕下拉的效果,但由于时间关系这些点都没有实现。对于这次课设的设计框架方面也有不足,没有用到面向对象的多态性;虽说技术无优劣,面向过程在这次设计中的应用设计非常简单方便,但实现面向对象编程才是C++的难点,因为其标准之多,对培养语言思维有很大的帮助,在今后还要多多编写有关项目。

6. 难点与收获

• 难点

1. 在我完成的三个应用中,个人认为表达式求解的难度较其余两个(通讯录和稀疏矩阵乘法)更大,要考虑到1数值运算,2运算符优先级,3进栈出栈的条件,4特殊情况(连续两个字符都是操作数要进行合并),对于这个应用来说还有改进的地方:输入字符中带有空格的处理;输入错误的提示信息等等

2. 第一个应用(通讯录的实现),用链表来实现通讯录的原理很简单,即把链表的data结构再用一个数据结构(包含要输入的信息)来表示。因为考虑到代码的精简性,实现过程中唯一的一个问题就是函数的相互调用。比如说当我要删除联系人或修改联系人的时候都会用到查找这个函数,但是接下来如何实现删除和修改,就需要获取指向这个联系人的指针,所以我在类中定义了一个私有成员Node *_temp; 用于指向查找到的前一个结点(因为对于删除操作,要获取前一个结点的指针)。这个简易通讯录还有改进的地方:利用正则表达式实现”电话输入格式判断”,”联想搜索”;多链表进行分组管理等等

3. 在二叉树的设计中,递归的灵活运用可以算是一个难点,个人总结出了一些自己的想法:

把原有的问题分解为一个新问题,而新问题又要用原有问题的解决方案

在实现的过程中,它采用了分治法的思想:即将整体分割成部分,并总是从最小的部分(基本部分)开始入手(输出),其背后的原理在于 当整体递归到部分时,会保留整体的信息,部分满足条件输出的结果会被回溯给整体使用,从而使得整体输出结果。

例如在求树的深度时:我们都知道将左子树与右子树的深度进行比较,深度大的子树深度+1即为整颗二叉树的深度,从最简单的二叉树来看也满足这个条件,而在求左右子树的深度时也是一样的方法,所以我们可以利用递归的方式,一条语句即可实现树的深度求解

1
return (BiTree_depth(T->lchild) > BiTree_depth(T->rchild)) ? (BiTree_depth(T->lchild) + 1) : (BiTree_depth(T->rchild) + 1);

其他的 遍历,求叶子结点,双亲,兄弟都是用到了同样的思想。

这里我要着重说一下双亲(兄弟)的求解,递归固然方便,但也削弱了对流程的可控性。在双亲的求解中可能会出现以下两种情况:1.根节点,无双亲;2.有多个值相同的结点。从人性化方面:我会在最后输出该值对应的结点有几个,然后分别打印出对应结点的情况。如果用递归的话也是可以做到,但无法控制最后一次递归打印信息,可以将获取的信息存到全局静态容器中,再用另一个函数对其进行打印。而如果要在一个函数中实现其功能就必须用非递归算法了,这里我用了栈对其进行循环控制,添加了一个verctor容器存储双亲结点,然后在最后进行打印。实现代码与测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void BiTree_parents(BiTree T, char e)
{
int count = 0; //记录相同值的结点数
bool root = false; //记录结点是根节点的情况
vector<char> v1; //存储具有双亲结点的双亲结点值
stack<BiTree> S; //非递归先序遍历

if (T == NULL)
return;
if (T->data == e) {
count++;
root = true;
}
while (!S.empty() || T)
{
while (T)
{
S.push(T);
if ((T->lchild && T->lchild->data == e) || (T->rchild && T->rchild->data == e)) {
count++;
v1.push_back(T->data);
}
T = T->lchild;
}
T = S.top(); S.pop();
T = T->rchild;
}
cout << "该值对应的结点共有 " << count << " 个,对应的双亲为:" << endl;
int n = 0;
if (root) {
cout << "第" << ++n <<"个结点为根结点,无双亲结点!" << endl;
}
for (auto i : v1) {
cout << "第" << ++n << "个结点的双亲结点:" << i << endl;
}
}

查找双亲

4. 图的邻接表存储表示,涉及到的链表比较多,数据结构间的逻辑也较难理解。我画一个图来表示其间的联系。

绘图1

• 收获

1. 实现了数据结构的dos系统。系统内容包括:带头结点的单链表 2.栈的顺序存储表示 3.稀疏矩阵的三元组顺序表示 4.二叉树的链式结构表示 5.图的邻接表存储表示 及前三项的简单应用

2. 了解了数据结构的本质:数据的各种逻辑结构和存储结构,以及对数据的各种操作。

熟悉了如何建立一个数据结构和使用它。

3. 深刻的感受到了数据结构对算法效率的提高

4. 加强了独自解决问题的能力,培养了程序设计的思路,加深了逻辑推理思维



C++ 数据结构

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!