程序设计方法包括三个基本步骤 个人的编程体会.docx
《程序设计方法包括三个基本步骤 个人的编程体会.docx》由会员分享,可在线阅读,更多相关《程序设计方法包括三个基本步骤 个人的编程体会.docx(20页珍藏版)》请在冰豆网上搜索。
程序设计方法包括三个基本步骤个人的编程体会
程序设计方法包括三个基本步骤:
第一步:
分析问题。
第二步:
画出程序的基本轮廓。
第三步:
实现该程序。
(1)编写程序;
(2)测试和调试程序;
(3)提供数据打印结果。
下面,我们来说明每一步的具体细节。
第一步:
分析问题
在这一步,你必须:
a.作为解决问题的一种方法,确定要产生的数据(输出)。
作为这一子步的一部分,你应定义表示输出的变量。
b.确定需产生输出的数据(称为输入),作为这一子步的一部分,你应定义表示输入的变量。
c.研制一种算法,从有限步的输入中获取输出。
这种算法定义为结构化的顺序操作,以便在有限步内解决问题。
就数字问题而言,这种算法包括获取输出的计算,但对非数字问题来说,这种算法包括许多文本和图象处理操作。
第二步:
画出程序的基本轮廓
在这一步,你要用一些句子(伪代码)来画出程序的基本轮廓。
每个句子对应一个简单的程序操作。
对一个简单的程序来说,通过列出程序顺序执行的动作,便可直接产生伪代码。
然而,对复杂一些的程序来说,则需要将大致过程有条理地进行组织。
对此,应使用自上而下的设计方法。
当使用自上而下的设计方法时,你要把程序分割成几段来完成。
列出每段要实现的任务,程序的轮廓也就有了,这称之为主模块。
当一项任务列在主模块时,仅用其名加以标识,并未指出该任务将如何完成。
这方面的内容留给程序设计的下一阶段来讨论。
将程序分为几项任务只是对程序的初步设计。
整个程序设计归结为下图所示的流程图1.。
如果把主模块的每项任务扩展成一个模块,并根据子任务进行定义的话,那么,程序设计就更为详细了(见图2.)。
这些模块称为主模块的子模块。
程序中许多子模块之间的关系可象图2.中那样归结为一张图。
这种图称为结构图。
要画出模块的轮廓,你可不考虑细节。
如果这样的话,你必须使用子模块,将各个模块求精,达到第三级设计。
继续这一过程,直至说明程序的全部细节。
这一级一级的设计过程称为逐步求精法。
在编写程序之前,对你的程序进行逐步求精,对你来说,是很好的程序设计实践,会使你养成良好的设计习惯。
我们则才描述了程序设计中自上而下的设计方法。
实际上就是说,我们设计程序是从程序的"顶部"开始一直考虑到程序的"底部"。
第三步:
实现该程序
程序设计的最后一步是编写源码程序。
在这一步,把模块的伪代码翻译成C语句。
对于源程序,你应包含注释方式的文件编制,以描述程序各个部分做何种工作。
此外,源程序还应包含调试程序段,以测试程序的运行情况,并允许查找编程错误。
一旦程序运行情况良好,可去掉调试程序段,然而,文件编制应做为源程序的固定部分保留下来,便于你或其他人维护和修改。
编写一个C程序需要这么麻烦吗?
编写一个简单程序当然不需要这么麻烦,但是这是训练你解决问题的方法,如果你坚持这么做你以后会感到收益非浅,因为我们学程序设计目的是进行软件开发,而现在早已不再是求伯均、王江民软件开发个人英雄时代,而是软件开发的规模化时代,软件设计的规范化是每一个程序员应具备的基本技能。
摘自《TurboC/C++forWindows集成实验与学习环境》的《C语言学习指导》。
详情请访问网站
CU首页>>读书频道>>分类浏览>>C程序设计思想与方法>>阅读内容
C程序设计思想与方法
2.3方案设计
2.3方案设计
方案设计是根据对问题的分析和理解,确定解决问题的方法和策略,为后续的编码提供依据。
方案设计阶段的工作包括计算过程和步骤的规划、计算模型的选择、以及算法和数据结构的选择。
2.3.1解题思路
在明确了对程序的功能、性能等方面的要求之后,接着需要做的是建立解题思路,然后根据解题思路选择和设计算法,构造相应的数据结构。
所谓建立解题思路就是用自然语言描述解题的计算过程和步骤,而算法则是使用具有可操作性的语言,按照一定的规则,对这些过程和步骤进一步细化。
如果用写文章做比喻的话,可以说解题思路解决的是布局和谋篇的问题,而算法描述则是关键章节和段落的构思。
当然,在程序设计的过程中,这两个层面并不是截然分开的,它们之间的界线也不是不可逾越的。
很多时候,对解题思路的考虑要涉及到所拟采用的算法的时空效率,而对一些简单的问题,相应的算法就是解题思路的直接延伸,或者说,解题思路可能直接就导出了相应的算法。
有些时候,题目的求解过程很简单,从对问题的分析就直接可以得到问题的求解思路。
例如【例2-2】多项式运算的主要功能只有读入数据、进行多项式的运算和输出运算结果这三个步骤,而这三个功能从概念上讲,都是比较简单的基本操作步骤。
其中输入数据的读入直接对应了简单的语句,而对多项式的计算和按格式输出计算结果又是和具体的数据结构的选择以及编码中的一些考虑相关的。
因此对这样的问题,就可以省略建立求解思路的过程而直接进入算法和数据结构的设计以及编码了。
对于复杂一些的问题,解题思路可能涉及多个性质不同的计算步骤和过程,而每一个计算步骤所涉及的算法和数据结构也各不相同。
这样,解题思路就与算法和数据结构的设计有一个比较明显的划分。
例如,不少人都玩过一个叫做“连连看”的电子游戏。
这是一个基于图形界面的人机交互游戏,用鼠标点击连接盘面上图案相同的两个棋子时,如果这对棋子可以使用不超过3条线段连接起来,那么这对棋子即可被消除。
当盘面上的棋子不能被消除时,游戏程序会对剩余的棋子重新排列。
游戏的目标是尽可能多地连续消除盘面上的棋子,以便在有限的对剩余棋子的重新排列次数内清除掉所有的棋子。
图2-1是游戏的一个初始状态盘面。
假设我们希望编写一个程序来帮助我们对任何一个游戏状态找到最优的操作序列,那么一个自然的想法就是让程序自动地在游戏的盘面状态上进行搜索。
为实现这一目标,我们需要识别盘面中各个棋子的图案,而要做到这一点,就需要获取计算机终端屏幕上指定区域中的图像,在这个图像中分割出一个个棋子所对应的区域,然后再对这些区域进行识别和记录。
为了便于对盘面状态进行搜索,需要在识别了棋子的图案后把它们转换为一种内部的表示形式,并存储在程序内部表示游戏盘面状态的数据结构中。
在完成了这些步骤之后,就可以选择最适合的搜索算法来求解最优的操作顺序。
然后,再以合适的方式把这一操作顺序通知用户。
这一连串的计算过程分别属于数据采集、图像处理、图像识别、状态空间搜索、以及人机交互等领域,每一个步骤都是相对独立的、有其自身的特点和独立的算法的。
图2-1游戏连连看的一个布局
一般说来,建立解题思路是一个逐步探索、逐步细化的过程,其基本策略是分而治之,也就是把大的问题逐步分解成小的、更容易把握和解决的问题。
对于简单的问题,可能略加思索就可以明确解题的基本步骤。
对于较为复杂的问题,则需要首先明确解题的基本方向和大的步骤,然后再对每个具体步骤逐步细化,直到每一个步骤都是可以解决的基本问题为止。
在这一过程中,重要的是需要抓住问题的关键,并围绕关键的步骤灵活地思考。
在思考的过程中,首先需要评估一下已知条件和所要求的结果,也就是出发点和目标之间的差距。
如果这两者之间的差距很小,可以用已知的方法实现从出发点到目标的跨越,则说明已经找到了解决这一问题的方法。
否则,就需要考虑如何利用已知的方法,从出发点向着目标前进一步。
当然,我们也可以从目标出发,考虑能否找到利用已知方法达到目标且距离出发点较近的中间结果。
不断地针对新的起点和新的目标重复上述过程,就可以构建一条根据已知条件解决问题的思路。
需要注意的是,在这一过程中,在构建思路中的每一个步骤时,都必须考虑到它的可行性,即这一步骤应该不仅在理论上是正确的,而且在计算机上是可以实现的,是在计算机所能提供的有限资源下可以计算的。
有些时候,一个解题思路会受到某些因素的制约,因而在实际上是不可行的。
这时就需要另寻其他的思路。
至于解题思路中每一步骤的大小,并没有固定的标准。
它既取决于问题的规模,也取决于编程人员的能力和经验。
一般来说,规模较大的程序中每一个步骤的粒度要大于规模较小的程序。
有经验的编程人员考虑问题可以粗一些,而经验较少的初学者就需要把问题分解得更细一些。
例如,在做程序练习题时,对于经验较多的人,“根据输入数据建立一个名字-数值对照表”可能就是一个很明确的、可以把握的基本操作步骤,而对于初学者来说,就需要再进一步将其分解为更细的操作步骤。
随着经验的积累和能力的提高,对问题分解的粒度也可以逐渐加大。
下面我们以一个具体的题目为例,来讨论解题思路的建立过程。
【例2-1-1】N!
的分解-解题思路在这个例子中,程序的主要功能是将N!
进行质因数分解,以及记录每一个质因数出现次数。
最直观的解题方法就是首先计算出N!
,然后再对其进行质因数分解。
我们知道如何计算N!
,也知道如何对一个数进行质因数分解。
因此从理论上讲,我们找到了一条解决这一问题的思路。
但是在实际上,这条路是不可行的,它受到了计算机所能提供的数值表达能力和计算能力的限制。
我们知道,N!
随着N的增加呈指数方式迅速增大。
对于32位的计算机来说,它所能直接表示的最大有符号整数是231-1,所能直接表示的最大无符号整数是232-1。
这两个数都介于12!
和13!
之间,64位整数可以表示到20!
。
即使使用double类型的浮点数,也只能表示到1475!
的近似值,而这道题目要求准确分解的最大的数是60000!
,这远远超过了计算机基本数据类型所能直接表示的数值范围。
即使我们可以用其他方法来表示这么大的数值,这样的方法在计算效率方面也会很低,在可以接受的时间内无法得出计算结果。
因此必须寻找其他的解决方法。
因为N!
是由从1到N的N个正整数的乘积,而N的最大取值60000仍然在计算机可以直接表示的范围内。
所以我们可以考虑逐一地对这N个自然数进行质因数分解,在分解的过程中记录每一个质因子出现的次数,并把每个自然数中相同的质因子出现的次数累加起来。
首先,这一方法在理论上是正确的:
中学的数学教科书中就讲过乘法的交换律和结合律;其次,这一方法是可以在计算机上计算的:
对一个60000以内的自然数进行质因数分解,即使用手工计算也不是非常困难的问题。
使用计算机更是可以轻而易举地完成的。
因此我们可以以此作为解决这道题目的一个思路。
在确定了解题思路之后,剩下的问题就是对其中的关键步骤,也就是一个正整数的质因数分解、以及对每个质因数出现的次数计数,进行算法和数据结构的设计。
这里,我们可以认为这两个步骤都比较基本,是用已有的知识就可以完成的。
如果读者认为这两个步骤依然比较复杂,对于设计解题的算法和编程来说还嫌过于粗糙,也可以在解题思路的层面上对这两个步骤进一步细化。
在上面的例子中,解题步骤的实际不可计算性使得初始的解题路线不可行,必须另辟蹊径。
一般来说,对于同一个问题,可能有多个不同的解题思路。
不同的解题思路有可能在描述的繁简、实现的难易、运行的效率、以及对计算资源的要求等方面都不相同,程序的运行环境以及系统所提供的各种基本支持等其他多种因素也常常对解题步骤产生影响。
因此在构思了一个解题思路之后,需要根据这些指标来衡量一下,看看解题思路是否可行,并在遇到难以克服的困难时及时转换解题思路。
有些时候,对问题的进一步分析,特别是对算法和数据结构的设计和分析有可能导出新的解题思路。
因此,在建立解题思路的过程中,需要保持灵活和开放的态度,对已有的解题思路进行认真的分析,看看它是否真的可行,是否还有更好的方法。
2.3.2计算模型
计算模型是对所要求解的问题的一种抽象,它用计算过程中的各种元素,如数据、公式、操作等来描述需要求解的问题。
一些与数值计算和系统软件直接相关的题目,往往直接给出了题目的计算模型或计算公式。
这时编程人员只需要确定适当的计算步骤,选择和设计有效的算法以及相应的数据结构,就可以着手编码了。
另外一类题目,往往是与其他应用领域相关的题目,则只从相关领域的角度描述了计算的前提条件和对计算的要求。
这就像数学习题中求解应用题一样,需要首先建立起相应的计算模型,然后才能进行后续的工作。
计算模型的建立,是把应用领域中的实体和这些实体之间的关系向抽象的数学模型映射的过程。
在这一过程中,需要首先分析题目中给出的与计算相关的实体,这些实体间的关系,以及所要求解的内容。
然后需要对于这些实体、关系和求解要求逐步细化和抽象,生成一个脱离具体应用领域的问题描述,建立这一描述中的计算实体与原始题目中计算实体的对应关系。
根据对问题的抽象描述,就可以在已知的各种数学模型中进行检索,找出最为合适或接近的计算模型,并根据这一模型完成确定计算步骤、算法及数据结构等后续工作。
下面我们看一个例子。
【例2-3】呼叫组(CallingCircles)这是一道1996年国际大学生程序设计竞赛(ACM/ICPC)的题目。
题目的大意是,如果A呼叫过B,B又直接或间接地呼叫过A,则A和B同在一个呼叫组中。
给出一组电话呼叫记录,计算出各个呼叫组及其中的人员。
这个问题描述了电话通信中的呼叫关系,实体是通话的各方。
问题需要求解的是确定哪些通话者属于同一个通话组。
我们可以很直观地用图来表示这个题目中的实体和关系。
通话者可以用顶点来表示,呼叫关系可以用由呼叫者指向被呼叫者的带箭头的弧来表示,即若A呼叫B,则从A引一条指向B的弧。
通过这样的分析和描述,可以很清楚地看到,这道题目中的实体和关系构成了一个有向图,题目中给出的数据就是一个对有向图的具体描述,而题目所要求解的就是根据连通性对有向图中顶点的等价类划分。
这样,就可以利用关于有向图连通性的知识来解决这个问题:
首先从输入文件中读入描述用户呼叫过程的数据,按照对有向图的表示方法生成数据的内部表示形式,并建立用户人名和有向图节点之间的对照表。
然后,根据所选择的关于有向图连通性的计算方法,计算出各个节点之间的连通性,求出节点的分组结果,再参照人名-节点对照表生成分组名单。
最后,根据分组名单,按照规定的格式要求输出分组结果。
至于在计算有向图连通性时采用什么方法,则是在算法设计阶段需要根据题目的要求进一步考虑的。
例如,如果题目中给定的数据规模不大,对计算的时间效率要求不高的话,可以选择邻接矩阵的方式来表示这个有向图,并用邻接矩阵的幂来计算邻接矩阵的连通性。
否则的话,就需要选择效率更高的算法。
在确定了有向图的表示方法之后,数据的读入过程和向内部形式的转换等步骤就可以进一步细化了。
很多问题的关键计算模型并不唯一。
不同模型在模型的直观性、描述的复杂程度、实现的难易、以及计算复杂性方面都可能有较大的差异。
这时就需要根据具体问题来进行分析、比较和选择。
当发现一种计算模型在计算效率或编程复杂度等方面不能满足要求时,可以进一步寻找其他适当的模型。
下面是一个具有不同计算模型的例子:
【例2-4】连词游戏(PlayonWords)这是ACP/ICPC1999年中欧分区赛中的一道题目。
题目大意是,判断N个由小写字母组成的英文单词是否可以构成这样的序列,使得相邻的两个单词中前一个单词的末字母等于后一个单词的首字母。
题目的限制条件是,单词长度在2-1000个字母之间,总的单词数量N小于等于100000。
例如,假设给定三个单词:
mouse、acm、malform,则可以构成这样的序列,即acmmalformmouse。
而对于单词集合:
yes、yes、no、ok,则不能构成这样的序列。
对于这个问题,直观的感觉是可以使用有向图作为计算模型。
但是在有向图的顶点和弧与问题中的实体的对应关系上,却可以有不同的选择。
第一种选择是,有向图中的每个顶点对应一个单词,如果顶点A所对应的单词最后的字母与另一个顶点B所对应的单词的第一个字母相同,则从顶点A建立一条指向顶点B的弧,也就是说,每条弧都从一个单词出发,指向可以接在其后的单词。
图2-2是在这种对应关系下单词集合mouse、acm、malform所对应的有向图:
图2-2连词游戏的一种计算模型
在建立了这样的对应关系之后,问题就转化为判断在有向图中是否存在一条经过所有顶点一次且仅一次的通路,也就是求解有向图中哈密顿通路是否存在的问题。
哈密顿通路是图论中的一个典型问题。
这样看起来,这个问题的计算模型问题似乎就得到了解决。
但是判断在一个图中是否存在哈密顿通路是一个很复杂的计算问题,目前还没有发现求解这一问题的有效算法。
因此,对于100000个顶点的有向图,很难在合理的时间内做出判断。
而这也迫使我们考虑其他的模型。
有向图的顶点和弧与问题中的实体的对应关系的另一种选择是,每个顶点对应26个字母中的一个,每条弧对应一个单词,从该单词的首字母指向末字母。
下面是在这种对应关系下单词集合mouse、acm、malform所对应的有向图:
图2-3连词游戏的另一种计算模型
在建立了这样的对应关系之后,问题就转化为在有向图中求解一条经过所有的弧一次且仅一次的通路是否存在,也就是判断有向图中欧拉通路的存在性问题。
在图论中,对于判断欧拉通路的存在性是有有效算法的,因此使用图2-3所示的计算模型可以满足求解连词游戏这一问题的要求。
很多情况下,计算模型的建立不仅取决于待求解问题本身,而且也取决于对问题分析的切入点和对问题的分解策略。
不同的分析方法可能产生完全不同的计算模型。
下面我们看一个例子:
【例2-5】实数格式识别合法的实数书写格式分普通格式和科学格式。
普通格式的描述方法是:
[<符号>]<整数>[.<整数>]
而科学格式的描述方法是:
[<符号>]<整数>[.<整数>]E[<符号>]<整数>
其中由[]括起的内容为可选项,符号包括'+'和'-'。
例如,+1.23是一个普通格式的实数,-5.1E-2是一个科学格式的实数,而9.1.1不是任何格式的实数。
写一个程序,分析一个给定的字符串是哪种格式的实数。
学过形式语言和自动机的读者可能会敏锐地发现,这两种数据格式都是正则表达式,因此可以使用有限自动机来分析和识别给定的字符串,并可以根据自动机模型写出相应的代码。
但是如果从模式识别的角度来观察,可以发现普通格式与科学格式的显著区别并建立另外一种计算模型:
科学格式中包含有字母E而普通格式中没有。
这样,根据字符串中是否包含字母E,就可以将其划入不同的格式类别,然后再根据该格式的描述对字符串进行检验:
对于科学格式,E的前面是一个可能带有符号的表示普通格式实数的子串,其后面是一个可能带有符号的表示整数的子串。
对于普通格式的实数,可以根据其中是否包含小数点而进一步分解:
包含小数点的实数在小数点前是一个可能带有符号的整数,在小数点后是一个不带符号的整数,而不包含小数点的实数则只有一个可能带有符号的整数。
对这些由E和小数点分隔的子串的检验是很基本的操作。
所有的子串均符合要求即可断定该字符串属于相应的格式类别,任何一个子串不符合规定的格式即可判断该字符串为非法格式。
依据这样模型写出的程序完全不同于依据自动机模型写出的程序。
计算模型的建立是一个涉及面很广的问题,它不仅要求对问题所涉及的应用领域有深入的了解,而且还需要有对各种数学模型的熟练掌握,以及高度的抽象思维能力。
深入讨论计算模型的建立远远超出了本书的范围。
本书中所涉及的领域都是常识所易于理解的,所使用的模型也多是常用的。
对这类问题建立计算模型,一般来说是比较容易的。
有些时候,问题的计算模型并不是很明显的,有些问题可能有多个可选的模型。
如何找到一个最适合的模型是需要认真思考和仔细权衡的事情。
这里更需要的是分析问题的方法和抽象思维的能力,以及勤于实践的习惯和经验。
2.3.3算法分类
在确定了解题思路之后,需要对解题过程中的各个步骤进一步细化和精确描述,确定关键步骤的算法以及所使用的数据结构。
在程序设计中,数据结构与算法是密不可分的。
程序就是使用编程语言在数据的某种特定表示方法和结构的基础上对抽象算法的具体描述。
一方面,不了解施加于数据上的算法,就无法决定如何构造数据。
另一方面,算法的构造和选择也常常在很大程度上依赖于作为算法基础的数据结构。
因此有些人甚至认为数据结构更为基础,也更为重要,因为只有先有了计算对象才有计算的算法。
在实际工作中,数据结构的选择与算法的设计是相辅相成、互相协调的。
数据结构被用来组织和保存数据,而算法描述对这些数据的操作,因此这两者应该放在一起来考虑。
与解题思路相似,算法同样是描述计算的过程。
所不同的是,算法是对具体而且较为复杂的计算步骤的精确描述,是用具有可操作性的方式描述一个解题步骤的具体执行过程。
至于一个计算步骤是否复杂,则需要根据问题的性质、规模、编程人员的经验以及所使用的编程工具等而定。
例如,对于一般计算问题的描述而言,浮点数的四则运算属于基本的操作步骤。
但是当进行计算机运算部件的设计和模拟时,一个浮点数的四则运算就是一个复杂的计算过程。
在学习数据结构课程时,快速排序是一个需要认真分析和讨论的复杂算法,但在C程序设计中,快速排序只是一个对标准库函数的调用。
Hash表的构造和操作对于初学者来说可能是一个复杂的任务,但是对于从事大型系统编程的高级程序员来说,这可能只是对数据处理算法中的一个简单步骤。
算法根据其复杂程度和应用领域,可以分为简单算法、专用算法和策略算法。
无论哪类算法,作为算法都应该满足下面的几个条件:
1.算法的每一步都应是含意确定、可以计算的
2.算法应该在有限的步骤之内产生所需要的计算结果
3.算法应该在有限的步骤内停止
简单算法
对于简单的问题,一些直观的思路、常规的方法和步骤就可以解决问题。
所谓简单算法就是对这些解决问题的直观思路和常规方法的精确描述。
枚举、递推、以及模拟算法等也可以归入这一类。
简单算法一般不涉及复杂的数据结构和计算过程。
例如,【例1-4】获奖人员就可以使用嵌套的循环语句对A~F获奖的各种可能性进行枚举,从中选出符合条件的组合。
设计简单算法时更多需要的是对问题的准确理解和把握,以及对相关领域的基本常识。
下面我们看一个简单算法的例子。
【例2-6】求倒数给出整数a(2<=a<=10000),求1/a。
输入文件有一行,包含两个正整数,分别代表正整数a和所要保留的小数位数N(N<=500)。
输出所求的小数部分,不输出尾部的0。
题目的计算结果要求保存最多500位有效数字,远远超过了C语言中任何基本数据类型的表示范围。
因此这道题的程序需要使用超长有效数字计算方法。
超长有效数字计算也称为高精度计算。
在高精度计算中,可以使用一维数组来表示一个数值,其中每一个数组元素表示该数值的一个十进制位。
有了这样的数据表示方法,我们就可以采用在小学算术中学过的除法竖式计算的方法,逐位计算出1/a的值:
2将1放入余数存储单元r中,将数组元素全部清零。
3根据给定的所要保留的小数位数N,循环执行下列操作序列N次:
2.1.r的内容乘以10。
2.2.r的内容除以a,2.3.将结果保存在数组的当前位中。
2.4.从r中减去数组的当前位中的值与a的乘积。
2.5.如果r等于0,2.6.则停止循环;否则转回2.1。
在计算结束后,从1/a的末尾,也就是数组中第N个元素开始向前检查,记住第一个不等于0的数组元素的位置,然后从数组中第1个元素开始向后顺序输出数组中的各个元素的值。
根据算术教科书可以知道这个算法是正确的。
根据上述算法,也可以很容易地推知对相关数据结构的要求:
保存计算结果的一维数组的长度只要大于500即可。
因为数组中只保存1/a的各位有效数字,所以各元素的最大值不超过9。
因为a<=10000,所以余数的最大值不会超过9999。
这样,a和r都可以使用普通的整型变量。
专用算法
专用算法是对特定领域中的问题进行计算的算法。
依据问题的领域和应用类型,常见的专用算法可以分为数值算法和非数值算法两大类。
初等数学中求最大公约数的辗转相除法、线性代数中