2021数据结构课程设计预习报告
(申请优秀)
完整源代码链接点这里.
- 前言:该次课程设计共有三次实验,本别是图书信息管理系统的设计与实现、隐式图的搜索问题和基于二叉排序树的低频词过滤系统。预习报告中,实验内容会具体描述出实验问题和想要实现的功能。实验分析与设计中会给出对于问题的分析与设计,实现过程中考虑的问题等,同时还会给出基本的存储的结构体或类的定义以及具体函数的定义等。三级标题最后一个部分会给出一个伪代码的具体实现或是源代码。 另外可能会为了阅读方便,每个实验的标题会有所不同
实验一 图书信息管理系统的设计与实现
1)实验内容
设计并实现一个图书信息管理系统。根据实验要求设计该系统的菜单和交互逻辑,并编码实现增删改查的各项功能。 该系统至少包含以下功能:
(1)根据指定图书个数,逐个输入图书信息;
(2)逐个显示图书表中所有图书的相关信息;
(3)能根据指定的待入库的新图书的位置和信息,将新图书插入到图书表中指定的位置;
(4)根据指定的待出库的旧图书的位置,将该图书从图书表中删除;
(5)能统计表中图书个数;
(6)实现图书信息表的图书去重;
(7)实现最爱书籍查询,根据书名进行折半查找,要求使用非递归算法实现,成功返回此书籍的书号和价格;
(8)图书信息表按指定条件进行批量修改;
(9)利用快速排序按照图书价格降序排序;
(10)实现最贵图书的查找;
2)实验分析与设计、功能描述与伪代码
实验一的内容比较简单,给出了具体的要实现的功能。在数据结构的使用上可以使用顺序存储或使用链式存储。此次使用中使用顺序存储来实现。
使用顺序存储有两个原因:
- 对于第7个功能,要求使用折半查找,因为必须使用顺序存储来实现,如果使用链式存储则需要通过二叉排序树,这样如果只是用二叉排序树进行存储,那么对于其他功能就无法对应实现,如果使用链式存储构造顺序表和二叉排序树结合使用,那么就将实验变得复杂化了。
2)对于第9个功能,要求使用快速排序。如果使用链式存储,就必须使用双链表来实现快速排序。那样也就将实验变得复杂了。因此使用顺序存储更加符合实验要求。
先给出对于结构题的定义
//图书信息的定义:
struct book {
string no[20]; //书号
string name[64]; //书名
double price; //价格
};
其次我们给出满足上述是个功能的类的函数的定义,为了考虑代码的鲁棒性在函数的具体实现中会对于一些非法输入进行反应
class SeqList
{
book* data;
int len; // 记录链表中数据的个数
int maxsize;
public:
SeqList(); // 默认构造函数
SeqList(book arr[], int n); // 带参构造函数,根据一个book数组来初始换链表
~SeqList(); // 析构函数
void Add(); // 增加一本书的信息在链表开头
void Adds(); // 根据指定图书个数,逐个输入图书信息
void Insert(int idx,book& obj); // 根据置顶的待入库的新图书的位置和信息,将新的图书插入到图书表中指定位置
void Print(); // 逐个现实图书表中所有图书的相关信息
void Delete(int idx); // 删除对应下标的数据
void rmDup(); // remove duplicate 图书信息表的图书去重
void Sort(int l,int r); // 利用快速排序按照图书价格降序排序
book BiSearch(int l, int r,double price); // 二分查找
int Size(); // 统计表中图书的个数
void findExp(); // find the most expensive book,如果有多个,则将所有最贵的书籍信息输出
};
1、基于顺序存储/链式存储结构的图书信息表的创建和输出
定义一个包含图书信息(书号、书名、价格)的顺序表。读入相应的图书数据来完成图书信息表的创建,然后统计图书表中的图书个数,同时逐行输出每本图书的信息。
创建这里一共给出了三个函数,分别是用来加入一条信息Add(),加入多条信息Adds()和根据位置插入信息。
void SeqList::Add()
{
if (len == maxsize)
{
book* p = new book[maxsize + 100];
for (int i = 0; i < len; i++)
p[i] = data[i];
delete []data;
data = p;
}
string s;
cout << "请输入书号:";
cin >> s;
if ((int)s.size() > 19)
throw "书号过长";
else
data[len].no = s;
cout << "请输入书名:";
cin >> s;
if ((int)s.size() > 63)
throw "书名过长";
else
data[len].name = s;
cout << "请输入图书价格:";
cin >> data[len].price;
len++;
}
void SeqList::Adds()
{
cout << "请输入图书个数";
int n; cin >> n;
for (int i = 0; i < n; i++) {
cout << "------------------" << endl;
Add();
}
}
// 根据位置和图书信息来将图书插入指定位置,位置信息是序号而非下标
void SeqList::Insert(int idx,book& obj)
{
if (idx > len)
throw "插入位置不合法";
if (idx <= 0)
throw "插入位置不能为负数或者零";
if (len == maxsize) // 如果超出数组范围则增加数组容量
{
book* p = new book[maxsize + 100];
for (int i = 0; i < len; i++)
p[i] = data[i];
delete[]data;
data = p;
}
for (int i = len; i >= idx; i--) { // 后移数组中的元素
data[i + 1] = data[i];
}
data[idx] = obj;
len ++ ;
}
每一条书籍信息的输出
void SeqList::Print()
{
cout << "所有图书的信息如下:" << endl;
cout << "书号\t\t书名\t\t价格\n";
for (int i = 0; i < len; i++)
{
data[i].Print();
}
}
2、基于顺序存储/链式存储结构的图书信息表的最贵图书查找
两次遍历,第一次找出最贵书籍的价格,第二次再逐个遍历输出满足条件的结果。
void SeqList::findExp()
{
double exp = -1;
for (int i = 0; i < len; i++) {
if (data[i].price > exp)
exp = data[i].price;
}
cout << "查找最贵书籍:" << endl;
for (int i = 0; i < len; i++) {
if (abs(data[i].price - exp)<=0.001)
cout<<data[i].name<<endl;
}
}
3、基于顺序存储/链式存储结构的图书信息表种指定价格的书籍查找(二分查找实现)
在对于书籍信息进行二插排序之后,再进行二分查找,找到索要查找价格的书籍。(这里仅仅输出一个书籍的信息),如果没有找到,则提示没有找到
// 降序中查找
book SeqList::BiSearch(int l, int r, double price)
{
while (l < r) {
int mid = (l + r + 1) / 2;
if (mid >= price)
l = mid;
else
r = mid - 1;
}
if (abs(data[l].price - price) > 0.001)
cout << "没有找到相应信息";
else {
cout << "为您找到:" << endl;
data[l].Print();
}
return data[l];
}
4、基于顺序存储/链式存储结构的图书信息表的旧书出库
读入指定的待出库的旧图书的书号,将该图书从图书表中删除,最后输出旧图书出库后所有图书的信息。
干部分使用两个函数来时先 :一个是通过书号对于书籍在线性表中位置信息的搜索功能,另一个则是根据具体的下表信息来对于书籍信息的删除函数。
void SeqList::Delete(int idx)
{
if (idx < 0 || idx >= len) throw "删除的下标参数不合法";
for (int i = idx + 1; i < len; i++)
data[i - 1] = data[i];
len--;
}
// 根据所给的书号来进行搜索找到对应书籍在线性表中的下标
int SeqList::Find(string no) {
for (int i = 0; i < len; i++) {
if (data[i].no == no)
return i;
}
return -1;
}
5、基于顺序存储/链式存储结构的图书信息表的图书去重
出版社出版的任何一本图书的书号(ISBN)都是唯一的,即图书表中不允许包含书号重复的图书。读入相应的图书信息表(事先加入书号重复的记录),然后进行图书的去重,即删除书号重复的图书(只留第一本),最后输出去重后所有图书的信息。
通过双层遍历来完成对于重复信息的删除操作
void SeqList::rmDup()
{
for (int j = 0; j < len; j++) {
int i = j + 1;
while (i < len) {
if (data[j] == data[i])
Delete(i);
else
i++;
}
}
cout << "已经去除重复信息!" << endl;
}
5、基于顺序存储/链式存储结构的图书信息表的图书排序(快速排序)
根据图书的价格降序排序。通过递归来实现
// 快速排序
void SeqList::Sort(int l, int r)
{
if (l >= r)
return;
int i = l + 1, j = r;
while (i < j)
{
while (i < j && data[j].price < data[l].price) j--;
while (i<j && data[i].price > data[l].price) i++;
if (i >= j)
break;
book temp = data[i];
data[i] = data[j];
data[j] = temp;
}
book temp = data[j];
data[j] = data[l];
data[l] = temp;
Sort(l, j-1);
Sort(j+1, r);
}
6、管理系统界面
最后再给出界面的设计,这里只给出界面的运行图,具体代码放到文章最后新的链接中。(文章开头有贴链接)
实验二 隐式图的搜索问题
1)实验内容
编写九宫重排问题的启发式搜索(A*算法)求解程序。
在3х3组成的九宫棋盘上,放置数码为1~8的8个棋子,棋盘中留有一个空格,空格周围的棋子可以移动到空格中,从而改变棋盘的布局。根据给定初始布局和目标布局,编程给出一个最优的走法序列。输出每个状态的棋盘
测试数据:初始状态:123456780 目标状态:012345678
如图:
2)启发式搜索(A*算法)
我们首先来回忆一下BFS算法,我们的 BFS算法每次都是从堆顶取出当前代价最小的状态进行扩展。那最先扩展到的就是最优的。
但我们BFS算法这种贪心思想是不完善的:我们虽然当前的代价小,但不能保证后来的代价也小(正所谓目光短浅)。这样就会使搜索量增大。
我们的A*算法就是加上一个估价函数:不断从堆中取出“当前代价+预估代价”最小的进行扩展。
但我们的预估函数有一个基本准则:
设从当前状态到目标状态所估计的代价值为f。
设未来的搜索中,实际求出的从当前状态到目标状态的实际最小代价为g。
则对于任意的状态: f ⩽ g
否则我们的搜索会出现很大的问题:最优解路径上的状态会被错误的估大,然后被压在堆底出不来。
但大家可能会有疑问了:那我们的非最优解路径不就可能先被扩展吗?
这个确实可能发生。
我们如果估值不准确,非最优解路径上的状态 s就会先被扩展,但是随着“当前代价”的不断累加,在目标状态被取出之前的某个时候:
1.根据 s并非最优,s的当前代价”就会大于从起始状态到目标状态的最小代价。
2.对于最优解搜索路径上的状态 t,因为 f ( t ) ⩽ g ( t ),所以 t的“当前代价”加上f(t)一定小于等于t的“当前代价”加上g(t)。
所以,可知道 t的当前代价加上f(t)小于s的当前代价 。因此, t就会被从堆中取出来进行扩展。
我们的估价函数越准确,我们的速度就会越快。 所以,设计一个优秀的估价函数十分重要。
A-star算法的链接:link.
3)实验分析与设计
在数据结构选择上,OPEN表使用了一个优先队列,CLOSE表使用了一个哈希表,方便状态查重。
在失效函数的选择上,此次选择了 没有归位数字数+搜索深度 作为失效函数值。
状态的表示上,使用了以下的数据结构:
// 节点的定义
class node {
public:
vector<vector<int>> grid;
int f;
int g;
string s;
int r, c; // 标记当前0的位置,初始化为(0,0)
node& operator= (const node& x) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
this->grid[i][j] = x.grid[i][j];
}
}
this->f = x.f;
this->g = x.g;
this->r = x.r;
this->c = x.c;
this->s = x.s;
return *this;
}
// 默认构造函数
node() {
grid.resize(3, vector < int> (3,0));
f = 0;
g = 0;
s = "";
}
};
由于使用了STL容器中的priority_queue,c++中优先队列是大根堆,而这里每一次选择失效函数小的状态进行搜索。因此我们重写了优先队列的比较函数。
class cmp {
public:
// 使用此比较函数,使优先队列变成小根堆
bool operator()(node& a, node& b) {
return a.f > b.f;
}
};
由于使用了STL容器中的unordered_set,也就是哈希表,在存储的时候,我们将关键字设计为字符串,因此就要设计出函数来将九宫格转换成字符串来存储,下边是将九宫格转换成字符串的函数
string getString(node& x) {
string res = "";
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++)
{
res.push_back((char)('0' + x.grid[i][j]));
}
}
return res;
}
4)源代码与运行结果
代码中加入了界面的设计
每一次输出正在搜索的状态,并显示出搜索的次数。最终统计搜索次数。
最后一个数字是表中所有九宫格状态的总和
最后给出具体的解决的函数
void solve() {
cout << "输入初始状态:" << endl;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cin >> init_state[i][j];
cout << "输入目标状态:" << endl;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cin >> goal[i][j];
int cnt = 0;
int past = 1;
node start;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
start.grid[i][j] = init_state[i][j];
if (init_state[i][j] == 0)
{
start.r = i;
start.c = j;
}
}
}
start.g = 0;
start.f = start.g + getH1(start);
start.s = getString(start);
close.emplace(start.s);
open.push(start);
bool jud = false; // 标记是否已经找到最终结果
while (!jud && !open.empty())
{
cnt++;
node cur = open.top();
cout << "______第" << cnt << "次搜索_____" << endl;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++) {
cout << cur.grid[i][j] << " ";
}
cout << endl;
}
cout << cur.f << endl;
open.pop();
close.emplace(cur.s);
for (int d = 0; d < 4; d ++ ) {
int dr = cur.r + dir[d][0];
int dc = cur.c + dir[d][1];
if (dr < 0 || dc < 0 || dr >= N || dc >= N)
continue;
node temp;
temp = cur;
temp.grid[temp.r][temp.c] = temp.grid[dr][dc];
temp.grid[dr][dc] = 0;
temp.s = getString(temp);
if (close.find(temp.s) != close.end() || vis.find(temp.s)!=vis.end())
continue;
temp.r = dr;
temp.c = dc;
temp.g+=1; // *******
temp.f = temp.g + getH1(temp);
open.push(temp);
vis.emplace(temp.s);
past++;
if (getH1(temp) == 0)
{
jud = true;
break;
}
}
}
cnt++;
cout << "______第" << cnt << "次搜索_____" << endl;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++) {
cout << open.top().grid[i][j] << " ";
}
cout << endl;
}
cout << "共计搜索:"<<cnt<<"次" << endl;
cout << "表中的状态共有:" << past<<"种"<<endl;
cout << "搜索深度:"<<open.top().g << endl;
}
这里将关键的函数代码分开解释地贴在了这里,完整代码在文章开头或者结尾的链接中
实验三 基于二叉排序树的低频词过滤系统
1)实验内容
1.对于一篇给定的英文文章,利用线性表和二叉排序树来实现单词频率的统计,实现低频词的过滤,并比较两种方法的效率。具体要求如下:
2.读取英文文章文件(Infile.txt),识别其中的单词。
3.分别利用线性表和二叉排序树构建单词的存储结构。当识别出一个单词后,若线性表或者二叉排序树中没有该单词,则在适当的位置上添加该单词;若该单词已经被识别,则增加其出现的频率。
4.统计结束后,删除出现频率低于五次的单词,并显示该单词和其出现频率。
5.其余单词及其出现频率按照从高到低的次序输出到文件中(Outfile.txt),同时输出用两种方法完成该工作所用的时间。
6.计算查找表的ASL值,分析比较两种方法的效率。
7.系统运行后主菜单如下:
2)实验分析与设计
首先实验分为三个部分。
1)文本的读取函数设计。所给文章中有若干词汇,我们使用c++要自行设计读取函数。
2)将所有的单词和出现次数存入顺序表中,进行后续操作。
3)设计基本的二插排序数,然后设计搜索函数,对于每一个单词进行搜素,并计算ASL。
①文本函数的读取
文本中有空格、回车、标点符号和大小写英文字母。可以发现,每次读到空格或者回车的时候就标志着一个单词输入完毕。
存在两种特殊情况:
1.一种是,输入到句子结尾,那么最后一个单词输入后,将会多出一个标点符号。因此,我们要将标点符号删去。
2.另一种情况是,最后一个标点符号是引号,那么次单词第一个字符也应该删去。
具体代码如下:
// 读取文件的函数
void read(SeqList& obj) {
ifstream ifs;
ifs.open(FILENAME, ios::in);
if (!ifs.is_open())
{
cout << "First Use..." << endl;
ifs.close();
return;
}
char c;
ifs >> c;
if (ifs.eof()) {
cout << "Empty Data..." << endl;
ifs.close();
return;
}
ifs.close();
ifstream ifs1;
ifs1.open(FILENAME, ios::in);
while (!ifs1.eof()) {
string s;
ifs1 >> s;
int len = s.size() - 1;
if ((len>=0) && !((s[len] >= '0' && s[len] <= '9') || (s[len] >= 'A' && s[len] <= 'Z') || (s[len] >= 'a' && s[len] <= 'z')))
{
char temp = s[len];
s.erase(s.begin() + len);
if (temp == '"')
s.erase(s.begin());
}
int idx = obj.Find(s);
if (idx != -1) {
obj.data[idx].cnt++;
}
else {
int end = obj.Size();
obj.Insert(end, s);
}
}
ifs1.close();
}
// 存储函数
void save(SeqList& obj) {
ofstream ofs;
ofs.open(SAVEFILE, ios::out);
int len = obj.Size();
for (int i = 0; i < len; i++) {
ofs << obj.data[i].str << '\t' << obj.data[i].cnt << endl;
}
ofs.close();
}
②顺序表定义
该试验中顺序表的设计中,首先定义一下节点,存储了单词信息和单词出现的频率。结构体定义如下:
struct node {
string str;
int cnt;
node() {
str = "";
cnt = 0;
}
/*
node& operator=(const node& obj) {
this->str = "";
for (int i = 0; i < (int)obj.str.size(); i++)
str.push_back(obj.str[i]);
cnt = obj.cnt;
return *this;
}*/
};
顺序表中,操作可以仅实现该实验需要的部分
下边是对于顺序表类的定义:
const int maxsize = 1000;
class SeqList
{
node data[maxsize];
int len;
public:
SeqList();
~SeqList();
friend class BiTree;
void Insert(int idx, node& obj); // 插入一个节点
void Insert(int idx, string& str); // 插入一个节点,根据一个字符串初始化,次数为首次出现的1次
void Print(); // 打印顺序表中所有的信息
void Delete(int idx); // 删除下标为idx 的节点信息
node Get(int idx); // 获得idx位置的node信息
int Size(); // 或许顺序表长度
int Find(string& obj); // 根据字符串来查找该字符串所在的节点,没有查找到则返回-1
friend void read(SeqList& obj); // 从磁盘中读取单词
friend void save(SeqList& obj); // 将单词和对应的次数存储进磁盘
};
其插入删除等操作与实验一类似,具体源代码在连接中
②二叉排序树
二叉排序树的一种定义:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。
在此实验中,使用二插排序树来存储单词和对应出现的次数。在次数统计中难免会出现两个单词出现的次数相同的情况。这里我们将次数相同的节点放在左子树上。
下边对于二叉树每一个节点定义一个结构体:
struct BiNode {
string str;
int cnt;
BiNode* lchild, * rchild;
BiNode() {
lchild = nullptr;
rchild = nullptr;
str = "";
cnt = 0;
}
};
在此次实验中,我们的二叉排序树只要有四个函数,
分别是在二叉树中插入一个节点、创建一个二叉树、删除一个二叉树和查找单词信息四个操作。
对于二叉排序树类的定义如下:
class BiTree
{
BiNode* root;
public:
BiTree();
~BiTree();
void Insert(node& obj, BiNode*& p);
void Create(SeqList& linear);
void Destory(BiNode* p);
int Find(node obj);
};
3)功能实现代码(完整代码看链接)
实验一已经对于线性表进行了详细的介绍,一些基本功能这里与实验一相同。这里主要给出二叉排序树的四个函数的代码:
①二叉排序树的插入操作
给定一个节点,根据单词出现的次数来进行插入。首先比较当前节点的次数,如果要插入的节点在二叉树中已经出现,则不进行插入,若没有插入则进行正常的插入步骤。如果要插入节点次数的大小小于等于当前节点出现的次数,则将当前节点的左节点作为下次比较的节点。如果次数大于当前节点的次数,则将当前节点的右节点作为下次比较的节点。这里使用递归来实现。(可以看出在插入新的节点的时候 ,我们需要一个函数Find()来判断要插入的节点是否已经存在于二叉排序树中)
void BiTree::Insert(node& obj, BiNode* &p)
{
if (p == nullptr) {
p = new BiNode();
p -> str = obj.str;
p -> cnt = obj.cnt;
return ;
}
if (obj.cnt <= p->cnt)
Insert(obj, p->lchild);
else
Insert(obj, p->rchild);
};
②二叉排序树的查找操作
从上边一个函数我们可以看出,插入一个新的单词节点的时候我们需要进行判断新的节点在二叉排序树中是否已经出现。同样使用递归来实现。通过此时来进行加速查找。同样使用递归来实现:
// if find return the step of search, otherwise return 0
int BiTree::Find(node obj)
{
int step = 0;
BiNode* p = root;
while (p != nullptr) {
step++;
if (p->str == obj.str)
return step;
if (obj.cnt <= p->cnt)
p = p->lchild;
else
p = p->rchild;
}
return step;
};
③二叉排序树的创建
二叉树的创建使用前边的线性表来实现。在此之前我们通过线性表来读取文件中的单词并统计。通过插入函数来构建一个二插排序数。
void BiTree::Create(SeqList& linear)
{
for (int i = 0; i < linear.Size(); i++) {
Insert(linear.data[i], root);
}
};
④二叉排序树的删除
由于是动态分配堆内存,因此我们要自己实现函数来删除整个二叉树。以避免内存泄漏。
void BiTree::Destory(BiNode* p)
{
if (p == nullptr)
return;
Destory(p->lchild);
Destory(p->rchild);
delete[] p;
};
⑤界面设置
完成了这些之后我们进行简单的截面设计,实现实验要求的功能。
具体函数如下:
void solve() {
int ope = 0;
SeqList list;
BiTree tree;
do {
system("cls");
menu();
cin >> ope;
if (ope == 1) {
read(list);
cout << "读取文件成功!" << endl;
list.Print();
}
else if (ope == 2) {
for (int i = 0; i < list.Size(); i++) {
if (list.Get(i).cnt <= 5)
{
cout << "删除" << list.Get(i).str << "\t出现次数:" << list.Get(i).cnt << endl;
list.Delete(i);
i--;
}
}
cout << "剩余单词:" << endl;
list.Print();
}
else if (ope == 3) {
double linear_ASL = 0 , BST_ASL = 0;
tree.Create(list);
for (int i = 0; i < list.Size(); i++) {
linear_ASL += (i + 1);
int step = tree.Find(list.Get(i));
BST_ASL += step;
}
linear_ASL = linear_ASL / double(list.Size());
BST_ASL = BST_ASL / double(list.Size());
cout << "线性表的ASL:" << linear_ASL<<endl;
cout << "二叉排序树的ASL:" << BST_ASL << endl;
}
else if (ope != 4)
{
ope = 0;
cout << "没有此操作,请重新输入" << endl;
}
system("PAUSE");
} while (ope != 4);
}
实验完整源代码链接
https://blog.csdn.net/qq_45812589/article/details/118252981.