数据结构课程设计预习与设计
一.整体概述
1.设计背景
中国大学生计算机设计大赛是我国高校面向本科生的计算机应用设计大赛,大赛旨在激发学生学习计算机知识和技能的兴趣与潜能,提高学生运用信息技术解决实际问题的综合能力。通过大赛这种计算机教学实践形式,可展示师生的教与学成果,最终以赛促学,以赛促教,以赛促创。该赛事在历届学生中影响力较大。
2.设计要求
(1)从team.txt中读取参赛队伍的基本信息,能够管理各参赛队的基本信息;包括增加、删除、修改参赛队伍的信息。
(2)实现基于二叉排序树的查找。根据提示输入参赛队编号,若查找成功,输出该赛事类别对应的基本信息同时,输出查找成功时的平均查找长度ASL;否则,输出“查找失败!”。
(3)能够提供按参赛学校查询参赛团队查找,若查找成功输出该学校参赛的所有团队的基本信息,输出的参赛团队按赛事类别有序输出。
(4)为省赛现场设计一个决赛叫号系统。所有参赛队按赛事组织文件中的赛事类别分到9个决赛室,决赛室按顺序叫号,被叫号参赛队进场,比赛结束后,下一参赛队才能进赛场。
(5)赛事系统为参赛者提供赛地的校园导游程序。为参赛者提供各种路径导航的查询服务。
3.设计分析
(1)要求一
1)功能实现:
读取TXT信息、增、删、改操作
2)实现分析:
① TXT文本读取。分析TXT文本内数据特征,参赛队伍信息按行存放,以#作为分隔符,可通过IO流读取数据,并进行读行处理,然后存入数据。
② 关于数组选择。比对LinkList()
和ArrayList()
特点,系统需要实现数据的增删查改基本操作,需实现频繁访问随机数据且增加操作在列表末尾增加即可,可采用ArrayList()
动态数组存放数据。优点在于实现动态存放同时,查找和修改的效率更高。
③ 基础操作。增、删、改时需考虑数据类型和数据重复/冲突等问题
(2)要求二
1)功能实现:
实现基于二叉排序树的查找。查找成功时的平均查找长度ASL,否则输出“查找失败!”
2)实现分析:
① 二叉排序树构建。可考虑两种方案,一是每次查找,针对所有数据进行一次二叉排序树的创建;二是在基础树上实现增删。
② 查找和ASL输出。
(3)要求三
1)功能实现:
查询参赛队伍信息并输出信息
2)实现分析:
选择排序、插入排序、希尔排序、归并排序、堆排序,五种排序方式。考虑角度如下:
① 待排序数目特征。
② 关键字需求。本系统所涉及数据中无关键字需求,无需考虑此因素影响。
③ 时间复杂度。
④ 空间复杂度。
⑤ 算法稳定性。
(4)要求四
1)功能实现:
按类别分9类,参赛队伍决赛室叫号
2)实现分析:
① 分类。按照参赛类别查询所有参赛队伍,并按序添入对应列表
② 叫号。决赛室叫号,防止多线程同时访问改列表对象,使用同步锁锁住当前被叫对象。
(5)要求五
1)功能实现:
为参赛者提供各种路径导航的查询服务
2)实现分析:
最短路径查询算法。采用邻接矩阵存储,顶点数组存储每一个景点,邻接矩阵存储景点之间的距离。邻接矩阵:用二维数组存储,数组的行和列分别代表两个顶点,数组元素的值代表两个顶点之间的距离。顶点数组:用一维数组存储,数组的下标代表顶点编号,数组元素的值代表顶点对象。
二.设计实现方案
1.划分功能模块
按照功能需求可将功能模块划分三个部分,设计如下:
2.信息管理实现
(1)读入参赛队伍信息
1)数组设计
考虑数据及功能特征,采用动态数组存储数据,定义Time
类存放参赛队伍信息
public class Team {
// 参赛队伍编号
private int teamNumber;
// 项目名称
private String projectName;
// 学校名称
private String schoolName;
// 赛事类别
private String eventCategory;
// 参赛队员
private String participants;
// 指导老师
private String guideTeacher;
}
2)读入参赛队伍信息设计
读入流程步骤如下:
S1 读取下一行队伍信息(从首行开始·,即从第二行读取)
S2 去除字符串左右空格
S3 每一行对应信息存入Team对象
S4 将Team对象存入合集
S51 若未读取最后一行,S1
S52 若已读取最后一行,结束
构建类TeamRepository
读取信息,其伪代码如下:
String line = Inputfile("team.txt")
while line != null
if line.startsWith("xxx") // 从第二行开始读取
continue;
end
String[] teamInfo = line.split("#");
for int i = 0; i < teamInfo.length; i++
teamInfo[i].trim(); // 去除字符串左右两边的空格
end
Team team = new Team().builder() // 将 Team 对象存入集合
teamList.add(team);
end
(2)二叉排序树查找
定义基础节点类Node
,其中包含键值和参赛队伍。
public class BSTNode {
// 键值
private int key;
// 参赛队伍
private Team team;
// 左子节点
private BSTNode left;
// 右子节点
private BSTNode right;
}
定义二叉排序树类BST
用于构建、查询数据内容。
public class BST {
// 根节点
private BSTNode root;
// 路径长度(查找时经过的节点数)
private int pathLength;
// 节点数
private int numNodes;
1)二叉排序树构建
设置insert()
方法填充二叉搜索树,其构造思路如下:
① 如果根节点为空,则将新节点作为根节点(创建新节点,并将其作为根节点);如果根节点不为空,则从根节点开始查找要插入的位置,将insert方法重载,添加根节点。
② 如果节点的键值大于要插入的键值,则在左子树中插入, 如果左子节点为空,则将新节点作为左子节点, 创建新节点,并将其作为左子节点并将节点数加一。
③ 如果左子节点不为空,则从左子节点开始查找要插入的位置。
④ 如果节点的键值小于要插入的键值,则在右子树中插入,后续操作同上。
其流程步骤如下:
S1 根节点为空,则将新节点作为根节点;
S2 根节点不为空,则从根节点开始查找要插入的位置
S3 键值大于要插入的键值S41;键值小于要插入的键值S42
S41 左子树插入。若节点不为空S5
S42 右子树插入。若节点不为空S5
S5 创建新节点,并将其作为对应插入的子节点并将节点数加一
S6 仍然有待插入数据S2
S7 结束进程
其实现如下:
public void insert(int teamNumber, Team team){
if (root == null) {
// 创建新节点,并将其作为根节点
……
}
// 如果根节点不为空,则从根节点开始查找要插入的位置
insert(root,teamNumber,team);
}
private void insert(BSTNode node,int key, Team team){
if (node.getKey() > key) {
if(node.getLeft() == null)
……
}
if (node.getKey() < key) {
……
}
2)查找
设计searchRec()
方法查询节点数据,原理如下:设value为要查找的节点的值
① 若 value == 当前节点的value,则查找成功,返回该节点。
② 若value < 当前节点的value,则进一步递归查找左子树。
③ 若value > 当前节点的value,则进一步递归查找右子树
其步骤如下所示:
S1 当前节点键值为查询键值S4
S21 查询键值小于当前键值,查找左子树S3
S22 查询键值大于当前键值,查询右子树S3
S3 当前节点键值不为查询键值S2
S4 输出当前节点值,结束进程
其实现如下:
public Team searchRec(Node root, int teamNumber){
// 根是 null 或者队伍编号匹配
if (root == null || root.getTeam().getTeamNumber() == teamNumber)
return root.getTeam();
...
}
3)计算ASL路径长度
设置calculateASL()
计算平均路径长度(计算公式:路径长度 / 节点数)的方法
public double calculateASL(List<Team> teamList){
...
}
(3)基础操作
增删改可通过向数组添加/删除/修改数据的方式实现,在实现类层定义save()
,delete()
,update()
方法实现
(4)排序
排序算法的比较,五种算法的比较可如下表所示
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 | 待排序数据归纳 |
---|---|---|---|---|---|
选择排序 | O(n2) | O(n2) | O(1) | 不是 | 小 |
插入排序 | O(n2) | O(n2) | O(1) | 是 | 小 |
希尔排序 | O(nlogn) | O(nlogn) | O(1) | 不是 | \ |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 是 | 大 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不是 | 大 |
根据选择排序算法的依据比较各类算法:
① 待排序数据量:其中选择和插入排序适用于数据规模较小情况;希尔排序则都有一定适用;归并和堆排序适用于数据规模较大情况。而本系统考虑未来参赛作品可能涉及上万数据排序,适宜挑选归并和堆排序算法;
② 时间复杂度:从时间复杂度来看,归并、希尔、堆排序在处理大量待排序数据时,要优于选择和插入排,适宜选择希尔、归并、堆排序;
③ 空间复杂度:从空间复杂度来看,选择、插入、希尔、堆排序适用于地址空间内存较小情况,适宜选择插入、选择、堆、希尔排序。
④ 算法稳定性:当元素分布有序,插入排序将大大减少比较次数和移动记录的次数,插入排序的优先性也要高于剩余三者;由于系统录入数据相对来说有序,且需要考虑排序后的次序稳定,需要保证算法的稳定性。因此适宜选择插入和归并排序。
综合以上特征来看,系统待排序数据量大且一定程度有序,在内存允许的情况下要保证排序的稳定,因此考虑选择归并算法。
考虑使用方法mergeSort()
,实现如下:
private static void mergeSort(int[] nums, int left, int right, int[] temp) {
...
}
3.决赛叫号实现
(1)分类
按照参赛类别查询此类所有参赛队伍信息,然后将其存入列表内,定义类FinalsCallSystem
存放基本信息:
public class FinalsCallSystem {
// 决赛室 1
private List<Team> teamList1 = new ArrayList<>();
// 决赛室 2
private List<Team> teamList2 = new ArrayList<>();
// 队伍业务逻辑接口
private TeamService teamService = new TeamServiceImpl();
}
使用addTeam()
方法实现分类,同时采用直接插入排序的方式,添加进列表,实现如下:
public void addTeam(){
// 获取所有参赛队伍
List<Team> teamList = TeamRepository.teamList;
...
}
(2)叫号系统
叫号流程可如下所示:
S1 决赛室开始叫号
S2 比赛开始,请第一组进入决赛室
S3 本组退出决赛室
S4 下一组进入决赛室,等待时间:x
S51 若不为最后一组S3
S52 若为最后一组S6
S6 决赛时叫号结束
定义FinalRoom
类的基本数据类型:
public class FinalRoom implements Runnable{
// 决赛室
private String roomName;
// 决赛室的队伍
private List<Team> teamList;
}
使用run()
方法实现叫号,同时要使用同步锁,防止多个线程同时访问:
public void run() {
// 使用同步锁,防止多个线程同时访问(锁住当前对象)
synchronized(this){
...
}
4.校园导航:
(1)邻接矩阵存储
使用MGraph
类定义基础数据类型:
public class MGraph {
// 最大权值,表示两个景点之间不可达
private static final int INF = Integer.MAX_VALUE;
// 顶点数组
private View[] views;
// 邻接矩阵
private int[][] matrix;
}
顶点数组构造可有1至10表示其各个顶点,景点图为无向图,邻接矩阵的构造表可如下所示:
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | |
---|---|---|---|---|---|---|---|---|---|---|
V1 | 0 | 100 | 0 | 200 | 0 | 0 | 0 | 0 | 0 | 0 |
V2 | 100 | 0 | 80 | 150 | 0 | 0 | 0 | 0 | 0 | 0 |
V3 | 0 | 80 | 0 | 0 | 120 | 110 | 0 | 0 | 0 | 0 |
V4 | 200 | 150 | 0 | 0 | 50 | 0 | 0 | 0 | 0 | 0 |
V5 | 0 | 0 | 120 | 50 | 0 | 0 | 0 | 150 | 230 | 0 |
V6 | 0 | 0 | 110 | 0 | 0 | 0 | 80 | 60 | 0 | 0 |
V7 | 0 | 0 | 0 | 0 | 0 | 80 | 0 | 0 | 0 | 100 |
V8 | 0 | 0 | 0 | 0 | 150 | 60 | 0 | 0 | 90 | 70 |
V9 | 0 | 0 | 0 | 0 | 230 | 0 | 90 | 0 | 0 | 50 |
V10 | 0 | 0 | 0 | 0 | 0 | 0 | 100 | 70 | 50 | 0 |
使用MGraph()
,addView()
,addEdge()
方法初始化/添加节点和边:
/**
* 构造方法(初始化图的顶点和邻接矩阵)
* @param size 图的顶点数
*/
public MGraph(int size) {
...
}
/**
* 添加景点
* @param i 顶点编号
* @param view 顶点对象
*/
public void addView(int i, View view) {
...
}
/**
* 添加边
* @param i 顶点编号
* @param j 顶点编号
* @param weight 权值
*/
public void addEdge(int i, int j, int weight){
...
}
(2)可视实现
使用View
类定义数据类型:
public class View {
// 目的地编号
private int id;
// 目的地名称
private String name;
// 目的地介绍
private String introduction;
}
使用ViewAndMGraphInitial
初始化信息:
public class ViewAndMGraphInitial {
// 景点信息
public static View[] views;
// 图
public static MGraph mGraph;
}
可使用静态代码块初始化景点信息和图。
(3)最短路径查询
1)哈密尔顿路径算法,查看推荐路径
定义实现类TourPathServiceImpl
public class TourPathServiceImpl implements TourPathService {
// 景点图
private MGraph graph;
// 用于存储哈密尔顿路径上的顶点编号
private int[] path;
// 是否找到哈密尔顿路径
private boolean isFind;
}
构造方法hamiltonianCycleUtil()
和hamiltonian()
求推荐路径及其长度
/**
* 哈密尔顿循环的递归函数,用于求解哈密尔顿循环
* @param pos 当前顶点的索引
* @param lastVisited 上一个访问的顶点
* @param visited 记录顶点是否已经访问过
* @return true:找到哈密尔顿循环;false:没有找到哈密尔顿循环
*/
private boolean hamiltonianCycleUtil(int pos, int lastVisited, boolean[] visited){
...
}
/**
* 求哈密尔顿路径
* @param source 起始顶点的名称
*/
@Override
public void hamiltonian(String source){
...
}
2)Floyd路径算法,查看最短路径
定义实现类ShortestPathServiceImpl
:
public class ShortestPathServiceImpl implements ShortestPathService {
// 景点图
private MGraph graph;
// 任意两个顶点之间的最短路径信息(即最短路径上的顶点编号)
private int[][] path;
// 任意两个顶点之间的最短路径长度(即最短路径的权值和)
private int[][] dist;
}
构造方法floyd()
求两点之间最短路径
/**
* 获取任意两个顶点之间的最短路径长度(Floyd算法)
* @param source 源顶点
* @param destination 目的顶点
*/
@Override
public void floyd(String source, String destination){
...
}
三.项目结构
根据设计方案实现已经构造类,可以基本确定实现的功能类和实现方法,可定义项目结构如下图所示: