软考算法与数据结构复习指南
Posted 小哈里
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软考算法与数据结构复习指南相关的知识,希望对你有一定的参考价值。
1、算法
根据考试大纲,本章要求考生掌握以下几个方面的知识点。
(1)数据结构设计:线性表、查找表、树、图的顺序存储结构和链表存储结构的设计和实现。
(2)算法设计:迭代、穷举搜索、递推、递归、回溯、贪心、动态规划、分治等算法设计。
从历年的考试情况来看,本章的考点主要集中以下方面。
在数据结构设计中,主要考查基本数据结构如栈,二叉树的常见操作代码实现。
在算法设计中,主要考查动态规划法、分治法、回溯法、递归法、贪心法。
- 算法特性
算法有一些基本特性要求掌握,表14-1是对这些特性的总结
- 算法复杂度分析
算法复杂性包括两个方面,一个是算法效率的度量(时间复杂度),一个是算法运行所需要的计算机资源量的度量(空间复杂度),这也是评价算法优劣的重要依据。
时间复杂度
一个程序的时间复杂度是指程序运行从开始到结束所需要的时间。通常分析时间复杂度的方法是从算法中选取一种对于所研究的问题来说是基本运算的操作,以该操作重复执行的次数作为算法的时间度量。一般来说,算法中原操作重复执行的次数是规模n的某个函数T(n)。由于许多情况下要精确计算T(n)是困难的,因此引入了渐进时间复杂度在数量上估计一个算法的执行时间。我们通常使用“O()”来表示时间复杂度,其定义如下:
也就是说,随着n的增大,f(n)渐进地不大于g(n)。例如,一个程序的实际执行时间为
T(n)=3n3+2n2+n,则T(n)=O(n3)。T(n)和n3的值随n的增长渐近地靠拢。常见的渐进时间复杂度
有:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)。
下面以几个实例来说明具体程序段的时间复杂度。
例1:求以下程序段的时间复杂度。
temp=i;
i=j;
j=temp;
分析该程序段会发现,程序的功能是将i与j的值交换。程序一共3条语句,每条语句的执行次数都为1。即:T(n)=1+1+1,所以整个程序段的时间复杂度为O(1)。
例2:求以下程序段的时间复杂度。
sum=0; //执行1次
for(i=1;i<=n;i++) //执行n次
for(j=1;j<=n;j++) //执行n2次
sum++; //执行n2次
本程序段的T(n)=2n2+n+1,时间复杂度应取指数级别最高的,所以为O(n2)。
空间复杂度
一个程序的空间复杂度是指程序运行从开始到结束所需的存储量。它通常包括固定部分和可变部分两个部分。
在算法的分析与设计中,经常会发现时间复杂度和空间复杂度之间有着微妙的关系,经常可以相互转换,也就是可以利用空间来换时间,也可以用时间来换空间。
渐近符号
前面讲到时间复杂度时,已经提到了“通常我们使用O()来表示时间复杂度”,但在考试时,有时会出现Θ符号,所以在此介绍一下常用的三种渐近符号在算法复杂度中所代表的含义。
O(f(n)),给出了算法运行时间的上界,一般用来表达最坏情况下的时间复杂度,这也是平时最常见的一种表达表式;
Ω(f(n)),给出了算法运行时间的下界,一般用来表达最好情况下的时间复杂度;
Θ(f(n)),给出了算法运行时间的上界和下界,其实并不是所有的算法都能求出Θ(f(n))的。
查找与排序
查找与排序是软件设计师考试中常考的知识点,不仅在上午综合知识部分考查,下午软件设计部分也会涉及。
查找与排序都会涉及到一些算法,例如查找,最笨的方法就是顺序查找,即从头开始,逐一对比,直到找到目标为止,这样如果要找的元素比较靠后,则需要消耗大量时间。如果用折半查找,则可以快速找到目标。折半的方式为:一开始就跟目标序列中的中部元素对比,如果要找的值小于中部元素,则说明要找的元素在前半个队列中,如此一来,一次对比,实排除了一半的元素,所以很高效。本节将详细介绍这些查找与排序的算法。
-
顺序查找
顺序查找又称线性查找,它是最基本的查找技术,它的查找过程是:从表中的第一个记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个记录,都无匹配的,则查找失败。
顺序查找方法既适用于线性表的顺序存储结构,也适用于线性表的链式存储结构。成功时的顺序查找的平均查找长度如下:
在等概率情况下,pi=1/n(1≤i≤n),故成功的平均查找长度为(n+…+2+1)/n=(n+1)/2,即查找成功时的平均比较次数约为表长的一半。若k值不在表中,则需进行(n+1)次比较之后才能确定查找失败(所以表中元素过多时,不宜采用该方法进行查找)。
该算法的时间复杂度为:O(n)。 -
二分查找法
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。
下面具体来看一看二分法查找的具体操作流程:(设R[low,…,high]是当前的查找区间)
(1)确定该区间的中点位置:mid=[(low+high)/2](注意:此处会进行取整操作,在考试时,这些细节往往直接影响得分);
(2)将待查的k值与R[mid].key比较,若相等,则查找成功并返回此位置,否则需确定新的查找区间,继续二分查找,具体方法如下。
若R[mid].key>k,则由表的有序性可知R[mid,…,n].key均大于k,因此若表中存在关键字等于k的结点,则该结点必定是在位置mid左边的子表R[low,…,mid–1]中。因此,新的查找区间是左子表
R[low,…,high],其中high=mid–1。
若R[mid].key<k,则要查找的k必在mid的右子表R[mid+1,…,high]中,即新的查找区间是右子
表R[low,…,high],其中low=mid+1。
若R[mid].key=k,则查找成功,算法结束。
(3)下一次查找是针对新的查找区间进行,重复步骤(1)和(2)。
(4)在查找过程中,low逐步增加,而high逐步减少。如果high<low,则查找失败,算法结束。
二分查找法采用了分治法的思想,后面将详细说明分治法的工作方式。 -
散列表
散列技术 是在记录的存储位置和它的关键字之间建立一个确定的对应关系f ,使得每个关键字key对应一个存储位置f(key)。采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。
使用散列表技术 使得查询能在O(1)时间内完成查找,这是非常难得的。那么他是怎么做到的呢?
简单一点讲就是:在存储时,我们会建立一个函数,利用函数算出存储空间,而取的时候,仍然用同样的方式来取出数据,这样就达到了目的。
下面通过一个实例来理解这样的过程。
例:将一个数列存储起来:52,37,28,41,79,85,93,64;以方便快速查询。我们可以定义函数:f(x)=x mod 10。即存储位置通过数据与10进行取余操作获取。所以,52应存放于2号空间,38存放于7号空间,依此类推。把所有数据存储好以后,若要查询85,只需要将85与10取余,得到5,再直接判断5号存储空间是否为85即可。若要查询99,则用99与10取余,得到9,然后判断9号空间是否为99,结果发现不是99而是79,所以查找失败,数列中没有我们需要的数据。
同时值得我们注意的是,并不是每个数据,我们都可以一次查找到,因为散列函数可能存在冲突。即两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。例如,我们选取的散列函数为:X MOD 10。需要存储的数列为:1,8,11,4,5,9。此时,我们发现,1与11对10取余,余数均为1,此时,产生冲突。产生冲突时,通常有两种解决方案:
(1)线性探查法
该方法的思路很简单,当前空间已被占据,则选下一个空闲空间来存储。还是以上面的数据为例,当存储完数列中的:1、8,需要存储11时,发现冲突产生,此时将11存储于1号空间的下一个空闲位置,即2号空间。
(2)双散列函数法
双散列函数法的思想是采用多个散列函数,当产生冲突时,利用第2个函数再次进行地址计算,得到存储位置。 -
插入排序
插入排序的基本思想是每步将一个待排序的记录按其排序码值的大小,插到前面已经排好的文件中的适当位置,直到全部插入完为止。插入排序方法主要有直接插入排序和希尔排序。
直接插入排序
直接插入排序的基本思想非常简单:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把这两个数按大小插入到有序表中;第二趟把第三个数据取出,然后依次与前两个数比较,把这个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟以后就完成了整个排序过程。
希尔排序
希尔排序也称为缩小增量排序 ,它是在直接插入排序的基础上进行改进而得到的排序方法 。
其基本思想是:每一趟都按照确定的间隔(增量)将元素分组 ,在每一组内进行直接插入排序 ,使得小元素可以跳跃式地向前移动 ,以后逐步缩小增量值,直到增量值为1为止,这时序列中的元素也已经基本有序,再进行直接插入排序时就可以很快,如图14-1所示。
增量通常首先取d1=n/2(n为待排序的数值的总个数),di+1=di/2,如果值为偶数,则加1,以保证di为奇数。 -
选择排序
选择排序的基本思想是每一步都从待排序的记录中选出排序码最小的记录,顺序存放在已排序的记录序列的后面。常见的选择排序有直接选择排序和堆排序两种。
直接选择排序
直接选择排序的过程是,首先在所有记录中选出排序码最小的记录,把它与第1个记录交换,然后在其余的记录内选出排序码最小的记录,与第2个记录交换……依次类推,直到所有记录排完为止。
无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需做n–i次比较,因此,总的比较次数为n(n–1)/2=O(n2)。当初始文件为正序时,移动次数为0;文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n–1)。直接选择排序的平均时间复杂度为O(n2)。直接选择排序是不稳定的。
堆排序
堆排序是利用堆这一特殊的树形结构进行的选择排序,它有效地改进了直接选择排序,提高了算法的效率。堆排序的整个过程是:构造初始堆,将堆的根结点和最后一个结点交换,重新调整成堆,再交换,再调整,直到完成排序。
堆实际上就是一种特殊的完全二叉树,它采用顺序存储。如果从0开始对树的结点进行编号,编号的顺序按层进行,同层则按从左到右的次序;则编号为0~ 的结点为分支结点,编号大于的结点为叶结点,对于每个编号为i的分支结点,它的左子结点的编号为2i+1,右子结点的编号为2i+2。除编号为0的树根结点外,对于每个编号为i的结点,它的父结点的编号为。
假定结点i中存放记录的排序码为Si,则堆的各结点的排序码满足Si≥S2i+1且Si≥S2i+2(0≤i≤)。这种堆称为大顶堆(大根堆),而如果相反(即满足Si≤S2i+1且Si≤S2i+2),则称为小顶堆(小根堆)。
构成堆就是把待排序的元素集合,根据堆的定义整成堆。这个过程需从对应的完全二叉树中编号最大的分支结点(编号为 )起,至根结点(编号为0)止。依次对每个分支结点进行“渗透”运算,形成以该分支结点为根的堆,当最后对二叉树的根结点进行渗透运算后,整个二叉树就成为了堆。下面我们就以序列{42,13,24,91,23,16,05,88}为例,说明堆的构造过程(首先按层次遍历将序列生成对应的完全二叉树),如图14-2所示。
图14-2完成堆构造后,得到堆序列{91,88,24,42,23,16,5,13}。在历年的考试中,经常出现给出序列要求判断是否为堆。例如(10,50,80,30,60,20,15,18)是否为堆?我们应该从编号为 的结点开始,逐个检查是否能够满足“Si≥S2i+1且Si≥S2i+2”或“Si≤S2i+1且Si≤S2i+2”。上例中,共有8个元素,因此从第4个开始。显然,它并不满足定义,因此不是堆。
通过前面的论述,我们可以得知:构建初始堆需要花费比较长的时间,因此对于记录数较少的排序问题并不适合于应用堆排序。
-
交换排序
交换排序的基本思想是:两两比较待排序记录的排序码,并交换不满足顺序要求的那些偶对,直到满足条件为止。交换排序的典型方法包括冒泡排序和快速排序。
冒泡排序
冒泡排序的基本思想是,通过相邻元素之间的比较和交换,将排序码较小的元素逐渐从底部移向顶部。由于整个排序的过程就像水底下的气泡一样逐渐向上冒,因此称为冒泡算法。整个冒泡排序过程如下所述:首先将A[n-1]和A[n-2]元素进行比较,如果A[n-2]>A[n-1],则交换位置,使小的元素上浮,大的元素下沉;当完成一趟排序后,A[0]就成为最小的元素;然后就从A[n-1]~A[1]之间进排序。下面就是一个实际的例子,如图14-4所示。
-
快速排序
快速排序采用的是分治法,其基本思想是将原问题分解成若干个规模更小但结构与原问题相似的子问题。通过递归地解决这些子问题,然后再将这些子问题的解组合成原问题的解。快速排序通
常包括两个步骤:
第一步,在待排序的n个记录中任取一个记录,以该记录的排序码为准,将所有记录都分成两组,第1组都小于该数,第2组都大于该数,如图14-5所示。
第二步,采用相同的方法对左、右两组分别进行排序,直到所有记录都排到相应的位置为止。
-
归并排序
归并也称为合并,是将两个或两个以上的有序子表合并成一个新的有序表。若将两个有序表合并成一个有序表,则称为二路合并。合并的过程是:比较A[i]和A[j]的排序码大小,若A[i]的排序码小于等于A[j]的排序码,则将第一个有序表中的元素A[i]复制到R[k]中,并令i和k分别加1;如此循环下去,直到其中一个有序表比较和复制完,然后再将另一个有序表的剩余元素复制到R中。而归并排序就是使用合并操作完成排序的算法,如果利用二路合并操作,则称为二路合并排序,其过程如下:
首先把待排序区间中的每个元素都看做一个有序表(则有n个有序表),通过两两合并,生成个长度为2(最后一个表的长度可能小于2)的有序表,这也称为一趟合并。
然后再将这 个有序表进行两两合并,生成个长度为4的有序表。如此循环直到得到一个长度为n的有序表,通常需要 趟,如果该值为奇数,则为。
基于磁盘进行的外排序经常使用归并排序的方法。其过程主要可以分为两个阶段。建立外排序所有的内存缓冲区:根据它们的大小将输入的文件划分为若干段,用某种有效的内排序方法,对各段进行排序,这些经过排序的段称为初始归并段,生成后就写到外存中去。使用归并树模式将第一个阶段生成的初始归并段加以归并,一趟趟地扩大归并段或减少归并段个数,直到最后归并成一个大的归并段为止。
使用k路平衡归并时,如果有m个初始归并段,则相应的归并树就有层,需要归并趟。例如,若对27个元素只进行三趟多路归并排序,则选取的归并路数是多少?这其实就是已知=3,求k值,很显然是3。通常只需增加归并路数k,或减少初始归并段个数m,都能够减少归并趟数S,以减少读写磁盘的次数d,达到提高外排序速度的目的。
-
基数排序
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序 的方法。基数排序不是基于关键字比较的排序方法 ,它适合于元素很多而关键字较少的序列 。基数的选择和关键字的分解是根据关键字的类型来决定的,例如关键字是十进制数,则按个位、十位来分解 。
例如,我们需对{135,242,192,93,345,11,24,19}进行排序,因为数据的最高位是百位,所以要分三趟进行分配和收集 ,如图14-7所示。
如图14-7所示,在匹配的过程中显然需要额外的辅助存储空间,通常采用链式存储分配的方式来存放中间结果。 -
排序算法的稳定性和复杂度(如表14-2所示)
表14-2排序算法的稳定性和复杂度
注:基数排序的复杂度中,r代表关键字的基数,d代表长度,n代表关键字的个数。
1.迭代法
迭代法是用于解决数值计算问题中的非线性方程(组)求解或最优解(近似根) 的一种算法设计方法。它的主要思想是:从某个点出发,通过某种方式求出下一个点 ,使得其离要求的点(方程的解)更近一步 ;当两者之差接近到可接受的精度范围时,就认为找到了问题的解。由于它是不断进行这样的过程,因此称为迭代法,同时从中也可以看出使用迭代法必须保证其收敛性。具体来说,迭代法包括简单迭代法、对分法、梯度法、牛顿法 等。对分法是指在某个解空间采用二分搜索,它的优点是只要在该解空间内有根,就能够快速地搜索到;梯度法则又称为最速下降法,它常用于工程问题的解决。
在使用的迭代法的过程中,应该注意两种异常情况。
如果方程无解,那么近似根序列将不会收敛,迭代过程会成为“死循环”,因此在使用时应先判断其是否有解,并应对迭代的次数进行限制;
方程虽然有解,但迭代公式选择不当,或迭代的初始近似根选择不合理,也会导致迭代失败。
迭代法总的来说是一种比较简单的求解方法,但是此类算法还存在两个不足:一是一次只能求方程的一个解,而且需要人工给出近似初值,如果初值选择不好就可能找不到解;二是不易保证程序的收敛性。
2. 穷举搜索法
穷举搜索法是穷举所有可能的情形,并从中找出符合要求的解,即对可能是解的众多候选解按某种顺序逐一枚举和检验,并从中找出那些符合要求的解作为问题的解。对于没有有效的解法的离散型问题,如果规模不大,穷举搜索法是很好的选择。
穷举搜索法通常需要使用多重循环来实现,对每个变量的每个值都进行测试,看其是否满足给定条件,如果满足则说明找到问题的一个解。
3. 递推法
递推法实际上首先需要抽象为一种递推关系,然后再按递推关系来求解。它通常表示为两种方式:
从简单推到一般,这常用于计算级数;将一个复杂问题逐步推到一个具有已知解的简单问题,它常与“回归”配合为递归法。递推法是一种简单有效的方法,通常可以编写出执行效率较高的程序。使用的关键是找出递推关系式,并确定初值。
任何用递推法可以解决的问题,都可以很方便地用递归法解决;但有很多可以使用递归法解决的问题,不一定可以使用递推法解决。但如果是既可以使用递归,又可以使用递推法来解决的问题,则应使用递推法,因为它的效率要高于递归法。
4. 递归法
递归是一种特别有用的工具,不仅在数学中广泛应用,还是设计和描述算法的一种有力工具。它经常用于分解复杂算法:将规模为N的问题分解成为规模较小的问题,然后从这些规模较小的问题的解中构造出大问题的解;而这些规模较小的问题采用同样的分解和综合的方法,分解成规模更小的问题;而特别的,当规模为1时,能够得到解。
从上面的描述中,我们可以看出递归算法包括“递推”和“回归”两个部分:递推是为了得到问题的解,将它推到比原问题简单的问题的求解;而回归则是当小问题得到解后,回归到原问题的解上来。
在使用递推时应该注意以下几点:
递推应该有终止点,终止条件便会使算法失效;“简单问题”表示离递推终止条件更为接近的问题。也就是说简单问题与原问题解的算法是一
致的,差别主要是参数。参数的变化将使问题递推到有明确解的问题。
在使用回归时应该注意:递归到原问题的解时,算法中所涉及的处理对象应是关于当前问题的,即递归算法所涉及的参数与局部处理对象是有层次的。当解一个问题时,有它的一套参数与局部处理对象。当递推进入一“简单问题”时,这套参数与局部对象便隐蔽起来,在解“简单问题”时,又有自己一套。但当回归时,原问题的一套参数与局部处理对象又活跃起来了。有时回归到原问题以得到问题解,回归并不引起其他动作。
采用递归方法定义的数据结构或问题最适合使用递归方法解答。当然,回到实际的开发中,递归的表现形式有两种:函数自己调用自己。两个函数之间相互调用。考试时以函数自己调用自己的方式居多。下面以两个程序实例说明该问题。例:利用递归程序计算n的阶乘。这是一个非常简单的计算问题,只要学过程序设计,都能用一个简单的循环来解决该问题。编写一个循环语句,实现:S=1234…n即可。但在此,我们要求用递归来实现,这便要求我们找出阶乘中隐藏的推荐规则,通过总结可得出规律:也就是说:要求n的阶乘,需要分两种情况分析问题,当n=0时,阶乘的结果为1;当n不等于0时,n的阶乘等于n乘以(n-1)的阶乘。这样就产生了递推过程。下面是将这种思路进行程序实现:接下来看一个更为复杂的例题:编写计算斐波那契(Fibonacci)数列的函数,数列大小为n。无穷数列1,1,2,3,5,8,13,21,35,…,称为斐波那契数列。这种数列有一个规律,数列第1个与第2个元素的值均为1,从第3个值开始,每个数据是前两个数据之和。即,数列可以表示为:1,1,(1+1),(1+(1+1)),((1+1)+(1+(1+1)))…在此,我们可以把这种规律转成递推式:
有了递推式,再来写程序,也就很容易了,直接转化即可,该问题程序实现如下所示:使用递归法写出的程序非常简洁,但其执行过程却并不好理解。在理解这种方法的过程中,建议大家使用手动运行程序的方式来进行分析,先从最简单的程序开始尝试,逐步到复杂程序。递归法的用途非常广泛,图的深度优先搜索、二叉树的前序、中序和后序遍历等可采用递归实现。
5. 回溯法
回溯法(试探法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。其工作机制如图14-8所示。
在使用回溯法时,必须知道以下几个关键的特性:采用回溯法可以求得问题的一个解或全部解,为了不重复搜索已找过的解,通常会使用栈(也可以用位置指针、值的排列顺序等)来记录已经找到的解。要注意的是,回溯法求问题的解时,找到的解不一定是最优解。
程序要注意记录中间每一个项的值,以便回溯;如果回溯到起始处,表示无解。
用回溯法求问题的全部解时,要注意在找到一组解时应及时输出并记录下来,然后马上改变当前项的值继续找下一组解,防止找到的解重复。下面我们通过一个经典的问题来研究回溯法的应用。例:使用回溯法解决迷宫问题,找到迷宫的出路。基本思路分析:进入一个迷宫之所以难以找到出路,是因为迷宫会有多个岔路口,形成多条路径,而成千上万条路径中,仅有1条(或几条)路径可走出迷宫。若采用回溯法,则在尝试走一条路径时,会把这些分岔都记录好,当一条路走不通时,原路返回到最近的一个分岔口,从这个分岔口找下一路进行尝试,这个过程与14-8所示的情况完全一致。接下来可以开始把这种思路转化为数据结构来表达了:设迷宫为m行n列,利用数组maze[m][n]来表示一个迷宫。maze[i][j]=0或1。其中0表示通路,1表示不通。当从某点向下试探时,中间的点有8个方向可以试探,而4个角点只有3个方向,而其他边缘点有5个方向。为使问题简单化,我们用maze[m+2][n+2]来表示迷宫,而迷宫的四周的值全部为1。这样做使问题简单了,每个点的试探方向全部为8,不用再判断当前点的试探方向有几个,同时与迷宫周围是墙壁这一实际问题相一致。如图14-9所示的迷宫是一个6×8的迷宫。入口坐标为(1,1),出口坐标为(6,8)。
迷宫的定义如下:
#define m 6 / 迷宫的实际行 /
#define n 8 / 迷宫的实际列 */
int maze [m+2][n+2] ;
在上述表示迷宫的情况下,每个点有8个方向可以试探。如当前点的坐标为(x,y),与其相邻的8个点的坐标都可根据与该点的相邻方位而得到。因为出口在(m,n),因此试探顺序规定为:从当前位置向前试探的方向为从正东沿顺时针方向进行。为了简化问题,方便地求出新点的坐标,将从正东开始沿顺时针方向进行的这8个方向的坐标增量放在一个结构数组move[8]中,在move 数组中,每个元素由两个域组成,x为横坐标增量,y为纵坐标增量。move数组如图14-10所示。
move数组定义如下:
typedef struct
{
int x,y
} item ;
item move[8] ;
这样对move的设计会很方便地求出从某点(x,y)按某一方向v (0≤v≤7) 到达的新点(i,j)的坐标。
可知,试探点的坐标(i,j)可表示为i=x+move[v].x ;j=y+move[v].y。到达了某点而无路可走时需返回前一点,再从前一点开始向下一个方向继续试探。因此,压入栈中的不仅是顺序到达的各点的坐标,而且还要有从前一点到达本点的方向。对于迷宫,依次入栈如下:
栈中每一组数据是所到达的每点的坐标及从该点沿哪个方向向下走的,对于如图14-9所示的迷
宫,走的路线为:(1,1)1→(2,2)1→(3,3)0→(3,4)0→(3,5)0→(3,6)0(下脚标表示方
向);当从点(3,6)沿方向0到达点(3,7)之后,无路可走,则应回溯,即退回到点(3,6),对应
的操作是出栈,沿下一个方向即方向1继续试探;方向1、2试探失败,在方向3上试探成功,因此将
(3,6,3)压入栈中,即到达了(4,5)点。
栈中元素是一个由行、列、方向组成的三元组,栈元素的设计如下:
typedef struct
{int x , y , d ; /* 横坐标和纵坐标及方向*/
}datatype ;
栈的设计如下:
#define MAXSIZE 1024 /*栈的最大深度*/
typedef struct
{datatype data[MAXSIZE];
int top;/*栈顶指针*/
}SeqStack
一种方法是另外设置一个标志数组mark[m][n],它的所有元素都初始化为0,一旦到达了某一点
(i,j)之后,使mark[i][j]置1,下次再试探这个位置时就不能再走了。另一种方法是当到达某点
(i,j)后使maze[i][j]置-1,以便区别未到达过的点,同样也能起到防止走重复点的目的。本书采用
后者方法,算法结束前可恢复原迷宫。
算法简单描述如下:
栈初始化;
将入口点坐标及到达该点的方向(设为-1)入栈
while (栈不空) {
栈顶元素=>(x , y , d)
出栈;
求出下一个要试探的方向d++;
while(还有剩余试探方向时){
if (d方向可走)
then { (x , y , d)入栈 ;
求新点坐标(i, j);
将新点(i , j)切换为当前点(x , y);
if ( (x ,y)= =(m,n) )结束 ;
else 重置d=0 ;
}
else d++ ;
}
}
该问题算法程序实现如下所示。
#include <stdio.h>
#define m 6 /* 迷宫的实际行 */
#define n 8 /* 迷宫的实际列 */
#define MAXSIZE 1024 /* 栈的最大深度 */
int maze[m+2][n+2]; /* 迷宫数组,初始时为0*/
typedef struct item{ /* 坐标增量数组 */
int x,y;
}item;
item move[8]; /* 方向数组 */
typedef struct datatype{ /* 栈结点数据结构 */
int x,y,d; /* 横坐标和纵坐标及方向 */
}datatype;
typedef struct SeqStack{ /* 栈结构 */
datatype data[MAXSIZE];
int top; /* 栈顶指针 */
}SeqStack;
SeqStack *s;
datatype temp;
int path(int maze[m+2][n+2],item move[8]){
int x,y,d,i,j;
temp.x=1;temp.y=1;temp.d=-1;
Push_SeqStack(s,temp); /* 辅助变量temp表示当前位置,将其入栈 */
while(!Empty_SeqStack(s))
{ Pop_SeqStack(s,&temp); /* 若栈非空,取栈顶元素送temp */
x=temp.x;y=temp.y;d=temp.d+1;
while(d<8) /* 判断当前位置的8个方向是否为通路 */
{ i=x+move[d].x;j=y+move[d].y;
if(maze[i][j]= =0)
{ temp.x=x;temp.y=y;temp.d=d;
Push_SeqStack(s,temp);
x=i;y=j;maze[x][y]=-1;
if(x==m&&y==n)return 1; /* 迷宫有路 */
else d=0;
}
else d++;
} /*while (d<8)*/
} /*while */
return 0 ; /* 迷宫无路 */
}
int Empty_SeqStack(SeqStack *s) /* 判断栈空函数 */
{ if (s->top= =-1)return 1;
else return 0;
}
int Push_SeqStack(SeqStack *s, datatype x) /* 入栈函数 */
{ if(s->top= =MAXSIZE-1)return 0; /* 栈满不能入栈 */
else {s->top++;
s->data[s->top]=x;
return 1;
}
}
int Pop_SeqStack(SeqStack *s,datatype *x) /* 出栈函数 */
{ if(Empty_SeqStack(s))return 0; /* 栈空不能出栈 */
else{*x=s->data[s->top];
s->top--;return 1; } /* 栈顶元素存入*x,返回 */
}
void main(){
int i,j,t;
move[0].x=0;move[0].y=1;
move[1].x=1;move[1].y=1;
move[2].x=1;move[2].y=0;
move[3].x=1;move[3].y=-1;
move[4].x=0;move[4].y=-1;
move[5].x=-1;move[5].y=-1;
move[6].x=-1;move[6].y=0;
move[7].x=-1;move[7].y=1;
for(i=0;i<=n+1;i++){
maze[0][i]=1;
maze[m+1][i]=1;
}
for(i=1;i<=m;i++){
maze[i][0]=1;
maze[i][n+1]=以上是关于软考算法与数据结构复习指南的主要内容,如果未能解决你的问题,请参考以下文章