数据结构实习
实习内容
一、马踏棋盘
问题描述
将马随机放在国际象棋的8×8棋盘[0~7][0~7]的某个方格中,马按走棋规则进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。编制递归和非递归程序,求出马的行走路线,并按求出的行走路线,将数字1,2,…,64依次填入一个8×8的方阵,输出之。
程序代码
棋盘定义
1 |
|
定义每次的改变的相对坐标
1 |
|
方法1:DFS(递归)
Dfs
函数是深度优先遍历函数,使用递归方式实现。接受二维数组
path
表示棋盘,m
和n
表示当前位置的坐标,edge
表示棋盘边长,count
表示已经访问的格子数。函数首先判断是否已经访问了所有的格子,如果是则直接返回。
然后判断当前位置是否在棋盘范围内且未被访问过,如果是则增加已访问的格子数,将该位置标记为已访问,并进行递归调用,以便继续向下一位置探索。
1 |
|
方法2:DFS+贪心算法(递归)
Dfs_tx
函数在深度优先遍历的基础上加入了贪心算法的思想,使用递归方式实现。根据贪心算法的思想,计算每个可行方向的下一步可行方向的数量,
并选择具有最小可行方向数量的方向作为下一步的移动方向。
最后进行递归调用,继续向下一个位置探索。
1 |
|
方法3:DFS+贪心算法(非递归)
Dfs_tx_s
函数使用非递归方式实现的深度优先遍历算法,加入了贪心算法的思想。该函数使用栈来保存待访问的位置,首先将起始位置入栈,然后进入一个循环,直到栈为空。
在循环中,取出栈顶的位置,判断是否已找到完整路径,如果是则直接返回。
然后判断当前位置是否超出边界或已被访问过,如果是则跳过当前位置。接下来增加已访问的格子数,并标记当前位置为已访问。
然后根据贪心算法的思想,计算每个可行方向的下一步可行方向的数量,并选择具有最小可行方向数量的方向作为下一步的移动方向,
将该位置入栈。最后继续下一轮循环,直到栈为空。
1 |
|
主函数的调用:
定义了两个二维数组
path
和flag
,分别用于记录最终路径和临时路径。初始化变量
m
和n
,表示起始位置的坐标。,edge
,表示棋盘的大小(边长),found
,表示完整路径的标志,初始值为 0。根据用户输入的序号,使用
switch
语句选择相应的解决方法。
1 |
|
二、迷宫问题
问题描述
以一个M×N的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍。设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。
首先实现一个以链表作存储结构的栈类型,然后编写一个求解迷宫的递归和非递归程序。求得的通路以三元组(i,j,d)的形式输出,其中:(i,j)指示迷宫中的一个坐标,d表示走到下一坐标的方向。
程序代码
初始化迷宫
1 |
|
定义栈的基本结构
StackNode 节点
Stack 栈
1 |
|
定义栈的基本方法
initStack ( ) 初始化栈
isStackEmpty ( ) 判断栈是否为空
push ( ) 入栈
pop ( ) 出栈
top()获取栈顶元素
1 |
|
检查坐标是否有效
(x,y)在棋盘上,并且标记为0
1 |
|
递归求解迷宫通路
首先检查当前坐标
(x, y)
是否为有效的路径节点,即判断是否在迷宫范围内且对应的迷宫数组值为 0(通路)。如果当前坐标无效,返回 false,表示没有通路。
将当前坐标
(x, y)
入栈,表示已访问该节点。将迷宫数组中的值标记为 2,表示该节点已访问过。
如果当前坐标是迷宫的出口
(M-1, N-1)
,则返回 true,表示找到了通路。递归尝试四个方向:向右
(x, y+1)
、向下(x+1, y)
、向左(x, y-1)
、向上(x-1, y)
。如果其中任意方向返回 true,表示找到了通路,直接返回 true。
如果以上四个方向都没有找到通路,说明当前节点不在通路上,将其从栈中弹出并清除标记。
返回 false,表示没有找到通路。
1 |
|
非递归求解迷宫通路
- 初始化当前坐标
(x, y)
为起点(0, 0)
,并将初始方向direction
设为 0。- 进入一个循环,直到找到通路或确定没有通路。
- 检查当前坐标
(x, y)
是否为有效的路径节点,即判断是否在迷宫范围内且对应的迷宫数组值为 0(通路)。- 如果当前坐标有效,将其入栈,表示已访问该节点。将迷宫数组中的值标记为 2,表示该节点已访问过。
- 如果当前坐标是迷宫的出口
(M-1, N-1)
,则返回 true,表示找到了通路。- 设置方向
direction
为 0,表示方向。- 更新坐标
(x, y)
为下一个节点,根据方向direction
的不同进行相应的移动操作。- 如果方向小于 3,表示还有方向未尝试,递增方向并继续进行下一次循环。
- 如果方向大于等于 3,表示四个方向都尝试过,说明当前节点不在通路上。
- 检查栈是否为空,如果为空,表示已经回到起点且没有找到通路,返回 false。
- 从栈顶获取上一个节点的坐标和方向,将其作为当前节点继续进行下一次循环。
- 根据方向进行相应的坐标更新操作。
- 回到步骤 3,继续进行下一次循环。
1 |
|
打印迷宫和通路
□ 表示通路
■ 表示障碍
★ 表示通路上的路径
1 |
|
主函数的调用
1 |
|
查找算法比较
问题描述
对以下6种常用的内部排序算法进行比较:顺序查找,折半查找,分块查找,插值查找,斐波那契查找,二叉树查找。
待查找表的表长不小于100;其中的数据要用伪随机数产生程序产生;至少要用5组不同的输入数据作比较;比较的指标为有关关键字参加的比较次数。
最后要对结果作出简单分析,包括对各组数据得出结果波动大小的解释。
程序代码
顺序查找
1 |
|
折半查找(二分查找)
1 |
|
分块查找
1 |
|
插值查找
改进的二分查找算法,它通过对数组进行估算,试图根据查找键的值在数组中的分布情况来预测该键可能出现的位置。公式
int pos = low + ((key - arr[low]) * (high - low)) / (arr[high] - arr[low])
用于计算估算的位置。
1 |
|
斐波那契查找
1 |
|
二叉树查找节点
1 |
|
生成顺序数据集
1 |
|
主函数调用
1 |
|
内部排序算法比较
问题描述
对以下6种常用的内部排序算法进行比较:冒泡排序,直接插入排序,简单选择排序,快速排序,希尔排序,堆排序。
待排序表的表长不小于100;其中的数据要用伪随机数产生程序产生;至少要用5组不同的输入数据作比较;比较的指标为有关关键字参加的比较次数和关键字的移动次数(关键字交换计为3次移动)。
最后要对结果作出简单分析,包括对各组数据得出结果波动大小的解释。
程序代码
冒泡排序
1 |
|
直接插入排序
待排序的数组分为已排序和未排序两部分。初始时,将数组的第一个元素视为已排序部分,其余元素为未排序部分。然后,依次从未排序部分取出一个元素,插入到已排序部分的正确位置,使得已排序部分仍然保持有序。
1 |
|
简单选择排序
算法每次从未排序的部分选择最小的元素,并将其放到已排序部分的末尾。
i
控制已排序部分的末尾位置,初始值为 0。min_idx 存储未排序部分的最小元素的索引。内层循环从
i+1
开始遍历未排序部分,找到最小元素的索引。如果当前元素比已记录的最小元素小,则更新最小元素的索引。比较过程中。
外层循环中,将找到的最小元素与已排序部分的末尾元素进行交换。。这样每一次外层循环都会将未排序部分的最小元素放到已排序部分的末尾。
1 |
|
快速排序
基于分治的思想,通过将数组分割为较小的子数组,然后递归地对子数组进行排序,最终将整个数组排序完成。
- 选择一个基准元素(pivot),通常可以选择数组的最后一个元素作为基准。
- 设置两个指针
i
和j
,初始时分别指向数组的第一个元素和最后一个元素。- 从左边开始,找到第一个大于等于基准元素的元素,记为
arr[i]
。从右边开始,找到第一个小于等于基准元素的元素,记为arr[j]
。- 如果
i
小于j
,交换arr[i]
和arr[j]
。- 重复步骤 3 和步骤 4,直到
i
不再小于j
。- 将基准元素
arr[high]
(初始选择的最后一个元素)与arr[i]
(当前i
指向的元素)交换位置,将基准元素放置在最终的位置上。- 现在,基准元素左边的元素都小于它,右边的元素都大于它。
- 递归地对基准元素左边的子数组(
arr[low]
到arr[i-1]
)和右边的子数组(arr[i+1]
到arr[high]
)进行快速排序。- 重复上述步骤,直到每个子数组的大小为 1 或者为空。
快速排序的核心操作是分区(partition)和递归排序。分区操作根据基准元素将数组划分为两个部分,其中一部分的元素都小于基准元素,另一部分的元素都大于基准元素。递归排序对分区后的子数组进行递归调用,直到数组被完全排序
1 |
|
希尔排序
是插入排序的一种改进算法,通过将待排序的数组按一定间隔分组,对每个分组进行插入排序,然后逐渐缩小间隔,最终完成整个数组的排序。
1 |
|
堆排序
堆排序通过构建最大堆和逐步调整堆实现排序
1 |
|
生成不同的随机数
1 |
|
1 |
|
1 |
|
针对不同的随机数编写不同的比较次数和关键字的移动次数的统计方法
1 |
|
主函数实现
主要为程序的调用
1 |
|
在数据集大小为200的情况下:
- 冒泡排序(Bubble Sort):
- 平均时间复杂度:O(n^2)
- 最坏时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用于小规模数据排序
- 比较次数:最好情况为0,最坏情况为19900
- 移动次数:最好情况为0,最坏情况为59700
- 直接插入排序(Insertion Sort):
- 平均时间复杂度:O(n^2)
- 最坏时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用于基本有序的数据排序
- 比较次数:最好情况为199,最坏情况为19900
- 移动次数:最好情况为199,最坏情况为59700
- 简单选择排序(Selection Sort):
- 平均时间复杂度:O(n^2)
- 最坏时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用于数据量较小的排序
- 比较次数:最好情况为19900,最坏情况为19900
- 移动次数:最好情况为0,最坏情况为59700
- 快速排序(Quick Sort):
- 平均时间复杂度:O(nlogn)
- 最坏时间复杂度:O(n^2)
- 空间复杂度:O(logn)~O(n)
- 稳定性:不稳定
- 适用于大规模数据排序
- 比较次数:最好情况为1526,最坏情况为19900
- 移动次数:最好情况为0,最坏情况为59700
- 希尔排序(Shell Sort):
- 平均时间复杂度:取决于增量序列的选择
- 最坏时间复杂度:取决于增量序列的选择
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用于中等规模数据排序
- 比较次数:最好情况为1247,最坏情况为19900
- 移动次数:最好情况为0,最坏情况为59700
- 堆排序(Heap Sort):
- 平均时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用于大规模数据排序
- 比较次数:最好情况为1296,最坏情况为19900
- 移动次数:最好情况为0,最坏情况为59700
不同数据集的影响
- 冒泡排序(Bubble Sort):
- 随机数据:由于每次比较只交换相邻元素,随机数据的冒泡排序性能较差,需要较多的比较和交换操作,波动较大。
- 大体上升序的随机数:冒泡排序在大体上升序的随机数上性能较好,因为大部分元素已经有序,只需要少量的比较和交换操作,波动较小。
- 大体下降序的随机数:冒泡排序在大体下降序的随机数上性能较差,需要较多的比较和交换操作来逆转递减序列,波动较大。
- 插入排序(Insertion Sort):
- 随机数据:插入排序在随机数据上的性能较好,因为每个元素都与前面有序的部分进行比较,插入到正确的位置,波动较小。
- 大体上升序的随机数:插入排序在大体上升序的随机数上性能较好,只需要少量的比较操作就可以找到正确位置,波动较小。
- 大体下降序的随机数:插入排序在大体下降序的随机数上性能较差,需要较多的比较和移动操作来逆转递减序列,波动较大。
- 快速排序(Quick Sort):
- 随机数据:快速排序在随机数据上的性能通常较好,它通过选择一个基准值并分区来排序,波动较小。
- 大体上升序的随机数:快速排序在大体上升序的随机数上的性能可能会略好一些,因为它能快速划分已经有序的部分,波动较小。
- 大体下降序的随机数:快速排序在大体下降序的随机数上的性能较差,需要较多的比较和划分操作,波动较大。
- 希尔排序(Shell Sort):
- 随机数据:希尔排序在随机数据上的性能通常较好,它通过将数组分成多个子序列进行插入排序,逐渐缩小子序列的间隔,最终完成排序,波动较小。
- 大体上升序的随机数:希尔排序在大体上升序的随机数上的性能较好,由于大部分元素已经有序,插入排序的次数会减少,波动较小。
- 大体下降序的随机数:希尔排序在大体下降序的随机数上的性能较差,需要较多的比较和移动操作来逆转递减序列,波动较大。
- 堆排序(Heap Sort):
- 随机数据:堆排序在随机数据上的性能通常较好,它通过构建最大堆(或最小堆)来进行排序,具有较好的稳定性和较少的波动。
- 大体上升序的随机数:堆排序在大体上升序的随机数上的性能较好,最大堆的构建过程较快,而排序过程中的比较和交换操作相对较少,波动较小。
- 大体下降序的随机数:堆排序在大体下降序的随机数上的性能较好,最小堆的构建过程较快,而排序过程中的比较和交换操作相对较少,波动较小。