编译原理中文法递归怎么理解
㈠ 递归算法怎么理解
问题一:递归算法还不是很理解!!高手教一教! 递归(recursion)是指把一个大的问题转化为同样形式但小一些的问题加以解决的方法。C语言允许一个函数调用它本身,这就是递归调用。即在调用一个函数的过程中又直接或间接地调用函数本身。不加控制的递归都是无终止的自身调用,程序中是绝对不应该出现这种情况的。为了防止无休止的递归,程序中应控制递归的次数,在某条件成立时进行递归,条件不成立不进行递归调用。并且在递归的调用过程中,不断改变递归的条件,以使递归条件不再成立。
同一问题可能既可以用递归算法解决,也可以用非递归算法解决,递归往往算法设计简单,出奇制胜,而普通算法(通常用循环解决)往往设计稍复杂。但执行效率递归算法逊于循环算法。递归反复调用自己,需要占用较多内存和计算机时间。但有一些问题只有用递归方法才能解决,如着名的汉诺塔问题。
递归程序设计的关键就是考虑问题的两种情况,一种是普遍情况即函数值等于把问题递推一步后的本函数的调用,一种是极端或端点情况,此时函数值有确定的一个值而无须再调用本函数。递归的过程就是从普遍情况逐步过渡到端点情况的过程。
例子:
5个坐在一起论年龄,问第五个人多少岁?他说比第四个人大两岁。问第四个人多少岁,他说比第三个人大两岁。问第三个人多少岁,他说比第二个人大两岁。问第二个人多少岁,他说比第一个人大两岁。问第一个人多少岁,他说10岁。请问第五个人几岁?
int age(int n)
{ int x;
if(n>1) x=age(n-1)+2;
else if(n==1) x=10;
return x;
}
void main( )
{ printf(%d,age(5));}
问题二:什么是递归算法 递归算法就是一个函数通过不断对自己的调用而求得最终结果滚咐的一种思维巧妙但是开销很大的算法。
比如:
汉诺塔的递归算法:
void move(char x,char y){
printf(%c-->%c\n,x,y);
}
void hanoi(int n,char one,char two,char three){
/*将n个盘从one座借助two座,移到three座*/
if(n==1) move(one,three);
else{
hanoi(n-1,one,three,two);
move(one,three);
hanoi(n-1,two,one,three);
}
}
main(){
int n;
printf(input the number of diskes:);
scanf(%d,&n);
printf(The step to moving %3d diskes:\n,n);
hanoi(n,'A','B','C');
}
我说下递归的理解方法
首先:对于递归这一类函数,你不要纠结于他是干什么的,只要知道他的一个模糊功能是什么就行,等于把他想象成一个能实现某项功能的黑盒子,而不去管它的内部操作先,好,我们来看下汉诺塔是怎么样解决的
首先按我上面说的斗备肆把递归函数想象成某个功能的黑盒子,void hanoi(int n,char one,char two,char three); 这个递归函数的功能是:能将n个由小到大放置的小长方形从one 位置,经过two位置 移动到three位置。那么你的主程序要解决的空轿问题是要将m个的汉诺块由A借助B移动到C,根据我们上面说的汉诺塔的功能,我相信傻子也知道在主函数中写道:hanoi(m,A,B,C)就能实现将m个块由A借助B码放到C,对吧?所以,mian函数里面有hanoi(m,'A','C','B');这个调用。
接下来我们看看要实现hannoi的这个功能,hannoi函数应该干些什么?
在hannoi函数里有这么三行
hanoi(n-1,one,three,two);
move(one,three);
hanoi(n-1,two,one,three);
同样以黑盒子的思想看待他,要想把n个块由A经过B搬到C去,是不是可以分为上面三步呢?
这三部是:第一步将除了最后最长的那一块以外的n-1块由one位置经由three搬到two 也就是从A由C搬到B 然后把最下面最长那一块用move函数把他从A直接搬到C 完事后 第三步再次将刚刚的n-1块借助hanno处函数的功能从B由A搬回到C 这样的三步实习了n块由A经过B到C这样一个功能,同样你不用纠结于hanoi函数到底如何实现这个功能的,只要知道他有这么一个神奇的功能就行
最后:递归都有收尾的时候对吧,收尾就是当只有一块的时候汉诺塔怎么个玩法呢?很简单吧,直接把那一块有Amove到C我们就完成了,所以hanoni这个函数最后还要加上 if(n==1)move(one,three);(当只有一块时,直接有Amove到C位置就行)这么一个条件就能实现hanoin函数n>=1时......>>
问题三:怎么更好地终极理解递归算法 递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。
需注意的是,规模大转化为规模小是核心思想,但递归并非是只做这步转化,而是把规模大的问题分解为规模小的子问题和可以在子问题解决的基础上剩余的可以自行解决的部分。而后者就是归的精髓所在,是在实际解决问题的过程。
问题四:怎样才能深刻理解递归和回溯? 递归的精华就在于大问题的分解,要学会宏观的去看问题,如果这个大问题可分解为若干个性质相同的规模更小的问题,那么我们只要不断地去做分解,当这些小问题分解到我们能够轻易解决的时候,大问题也就能迎刃而解了。如果你能独立写完递归创建二叉树,前序、中序、后序递归遍历以及递归计算二叉树的最大深度,递归就基本能掌握了。
回溯本人用得很少,仅限于八皇后问题,所以帮不上啥了。
问题五:二叉树的递归算法到底该怎么理解 这不就是在二叉排序树上的递归查找,看程序
tree& find(const T& d, tree& t){
if(t==NULL) return t;如果二叉树为空则返回空,查找失败
if(t->data==d) return t;否则,如果当前根结点关键码为d,则查找成功,当前根结点为待查找结点
if(d>t->data) return find(d, t->right);如果比根的关键码大就递归查找右子树
return find(d, t->left);如果比根的关键码小就递归查找左子树
}
二叉树的递归定义的含义就是非空二叉树,除了根以外,左右子树都是二叉树(可以为空)
问题六:怎么理解递归算法?我看了代码但还是不理解? 函数自己调用自己就是递归啊。
从前有座山,山里有座庙,庙里有个老和尚给小和尚讲故事。讲的内容是:
从前有座山,山里有座庙,庙里有个老和尚给小和尚讲故事,讲
从前有座山,山里有座庙,庙里有个老和尚给小和尚讲故事……
跟循环差不多。而且浪费栈空间,效率不高。能够转化为循环最好。
问题七:数据结构中的二叉树中的递归怎么理解? 以中序遍历为例,思想是:
若二叉树为空,则空操作;否则
(1)中序遍历左子树
(中序遍历左子树时也是这三步)
(2)访问根结点
(3)中序遍历右子树
(当然右子树也是重复着三步)
示例代码:
int InOrderTraverse(BiTree T)
{
if(T)
{
InOrderTraverse(T->lchild);
printf(%d\t,T->data);
InOrderTraverse(T->rchild);
}
return OK;
}
问题八:java递归算法,怎么理解??? n! = (n-1)*n!
简单理解,就是目前的所有任务,等于前面所有的任务+现在的任务。
比如你求 1。。。100的加法总和
实际上是 1... 99 的加法总和 + 100 就是了。
这就是递归的来源。
你只需要计算你前一步的任务,然后加上自己,就OK了。
前一步,在再次调用前前一步......
问题九:新手一个,有什么更好理解递归的方法吗?(c++) 递归的话就是重复调用方法直到满足条件为止就停止这个方法,就跟循环类似,不过循环使用的方法一边比较简单
问题十:递归的原理解释 递归的底层实现其实是一个栈.栈的特点是后进先出,也就是最后进入栈的事件是最先被处理的.
递归就是这样运作.比如计算阶乘函数F(n)=n!=n*F(n-1)=....
写成递归,我用java
public static long F(long num){
if(num
㈡ 编译原理-LL1文法详细讲解
我们知道2型文法( CFG ),它的每个产生式类型都是 α→β ,其中 α ∈ VN , β ∈ (VN∪VT)*。
例如, 一个表达式的文法:
最终推导出 id + (id + id) 的句子,那么它的推导过程就会构成一颗树,即 CFG 分析树:
从分析树可以看出,我们从文法开始符号起,不断地利用产生式的右部替换产生式左部的非终结符,最终推导出我们想要的句子。这种方式我们称为自顶向下分析法。
从文法开始符号起,不断用非终结符的候选式(即产生式)替换当前句型中的非终结符,最终得到相应的句子。
在每一步推导过程中,我们需要做两个选择:
因为一个句型中,可能存在多个非终结符,我们就不确定选择那一个非终结符进行替换。
对于这种情况,我们就需要做强制规定,每次都选择句型中第一个非终结符进行替换(或者每次都选择句型中最后一个非终结符进行替换)。
自顶向下的语法分析采用最左推导方式,即总是选择每个句型的最左非终结符进行替换。
最终的结果是要推导出一个特定句子(例如 id + (id + id) )。
我们将特定句子看成一个输入字符串,而每一个非终结符对应一个处理方法,这个处理方法用来匹配输入字符串的部分,算法如下:
方法解析:
这种方式称为递归下降分析( Recursive-Descent Parsing ):
当选择的候选式不正确,就需要回溯( backtracking ),重新选择候选式,进行下一次尝试匹配。因为要不断的回溯,导致分析效率比较低。
这种方式叫做预测分析( Predictive Parsing ):
要实现预测分析,我们必须保证从文法开始符号起,每一个推导过程中,当前句型最左非终结符 A 对于当前输入字符 a ,只能得到唯一的 A 候选式。
根据上面的解决方法,我们首先想到,如果非终结符 A 的候选式只有一个以终结符 a 开头候选式不就行了么。
进而我们可以得出,如果一个非终结符 A ,它的候选式都是以终结符开头,并且这些终结符都各不相同,那么本身就符合预测分析了。
这就是S_文法,满足下面两个条件:
例子:
这就是一个典型的S_文法,它的每一个非终结符遇到任一终结符得到候选式是确定的。如 S -> aA | bAB , 只有遇到终结符 a 和 b 的时候,才能返回 S 的候选式,遇到其他终结符时,直接报错,匹配不成功。
虽然S_文法可以实现预测分析,但是从它的定义上看,S_文法不支持空产生式(ε产生式),极大地限制了它的应用。
什么是空产生式(ε产生式)?
例子
这里 A 有了空产生式,那么 S 的产生式组 S -> aA | bAB ,就可以是 a | bB ,这样 a , bb , bc 就变成这个文法 G 的新句子了。
根据预测分析的定义,非终结符对于任一终结符得到的产生式是确定的,要么能获取唯一的产生式,要么不匹配直接报错。
那么空产生式何时被选择呢?
由此可以引入非终结符 A 的后继符号集的概念:
定义: 由文法 G 推导出来的所有句型,可以出现在非终结符 A 后边的终结符 a 的集合,就是这个非终结符 A 的后继符号集,记为 FOLLOW(A) 。
因此对于 A -> ε 空产生式,只要遇到非终结符 A 的后继符号集中的字符,可以选择这个空产生式。
那么对于 A -> a 这样的产生式,只要遇到终结符 a 就可以选择了。
由此我们引入的产生式可选集概念:
定义: 在进行推导时,选用非终结符 A 一个产生式 A→β 对应的输入符号的集合,记为 SELECT(A→β)
因为预测分析要求非终结符 A 对于输入字符 a ,只能得到唯一的 A 候选式。
那么对于一个文法 G 的所有产生式组,要求有相同左部的产生式,它们的可选集不相交。
在 S_文法基础上,我们允许有空产生式,但是要做限制:
将上面例子中的文法改造:
但是q_文法的产生式不能是非终结符打头,这就限制了其应用,因此引入LL(1)文法。
LL(1)文法允许产生式的右部首字符是非终结符,那么怎么得到这个产生式可选集。
我们知道对于产生式:
定义: 给定一个文法符号串 α , α 的 串首终结符集 FIRST(α) 被定义为可以从 α 推导出的所有串首终结符构成的集合。
定义已经了解清楚了,那么该如何求呢?
例如一个文法符号串 BCDe , 其中 B C D 都是非终结符, e 是终结符。
因此对于一个文法符号串 X1X2 … Xn ,求解 串首终结符集 FIRST(X1X2 … Xn) 算法:
但是这里有一个关键点,如何求非终结符的串首终结符集?
因此对于一个非终结符 A , 求解 串首终结符集 FIRST(A) 算法:
这里大家可能有个疑惑,怎么能将 FIRST(Bβ) 添加到 FIRST(A) 中,如果问文法符号串 Bβ 中包含非终结符 A ,就产生了循环调用的情况,该怎么办?
对于 串首终结符集 ,我想大家疑惑的点就是,串首终结符集到底是针对 文法符号串 的,还是针对 非终结符 的,这个容易弄混。
其实我们应该知道, 非终结符 本身就属于一个特殊的 文法符号串 。
而求解 文法符号串 的串首终结符集,其实就是要知道文法符号串中每个字符的串首终结符集:
上面章节我们知道了,对于非终结符 A 的 后继符号集 :
就是由文法 G 推导出来的所有句型,可以出现在非终结符 A 后边的终结符的集合,记为 FOLLOW(A) 。
仔细想一下,什么样的终结符可以出现在非终结符 A 后面,应该是在产生式中就位于 A 后面的终结符。例如 S -> Aa ,那么终结符 a 肯定属于 FOLLOW(A) 。
因此求非终结符 A 的 后继符号集 算法:
如果非终结符 A 是产生式结尾,那么说明这个产生式左部非终结符后面能出现的终结符,也都可以出现在非终结符 A 后面。
我们可以求出 LL(1) 文法中每个产生式可选集:
根据产生式可选集,我们可以构建一个预测分析表,表中的每一行都是一个非终结符,表中的每一列都是一个终结符,包括结束符号 $ ,而表中的值就是产生式。
这样进行语法推导的时候,非终结符遇到当前输入字符,就可以从预测分析表中获取对应的产生式了。
有了预测分析表,我们就可以进行预测分析了,具体流程:
可以这么理解:
我们知道要实现预测分析,要求相同左部的产生式,它们的可选集是不相交。
但是有的文法结构不符合这个要求,要进行改造。
如果相同左部的多个产生式有共同前缀,那么它们的可选集必然相交。
例如:
那么如何进行改造呢?
其实很简单,进行如下转换:
如此文法的相同左部的产生式,它们的可选集是不相交,符合现预测分析。
这种改造方法称为 提取公因子算法 。
当我们自顶向下的语法分析时,就需要采用最左推导方式。
而这个时候,如果产生式左部和产生式右部首字符一样(即A→Aα),那么推导就可能陷入无限循环。
例如:
因此对于:
文法中不能包含这两种形式,不然最左推导就没办法进行。
例如:
它能够推导出如下:
你会惊奇的发现,它能推导出 b 和 (a)* (即由 0 个 a 或者无数个 a 生成的文法符号串)。其实就可以改造成:
因此消除 直接左递归 算法的一般形式:
例如:
消除间接左递归的方法就是直接带入消除,即
消除间接左递归算法:
这个算法看起来描述很多,其实理解起来很简单:
思考 : 我们通过 Ai -> Ajβ 来判断是不是间接左递归,那如果有产生式 Ai -> BAjβ 且 B -> ε ,那么它是不是间接左递归呢?
间接地我们可以推出如果一个产生式 Ai -> αAjβ 且 FIRST(α) 包括空串ε,那么这个产生式是不是间接左递归。
㈢ 编译原理中 左递归具体解释是什么
定义:
"一个文法是左递归的,若我们可以找出其中存在某非终端符号A,最终会推导出来的句型(sentential form)里面包含以自己为最左符号(left-symbol)的句型"
即
A -> Aa 或
A -> Ba
B -> A
两种形式的文法.
㈣ 【编译原理】第四章:语法分析
从分析树的根节点到叶节点方向构造分析树。
即从开始符号S推导出词串w的过程。
例:
总是选择每个句型的 最左非终结符 进行替换。
总是选择每个句型的 最右非终结符 进行替换。
在自底向上的分析中,总是采用 最左规约 的方式,因此把 最左规约 称为 规范规约 ,对应的 最右推导 称为 规范推导 。
最左推导、最右推导具有唯一性。
自顶向下的语法分析采用最左推导方试,总是选择每个句型的 最左非终结符 进行替换。
由一组 过程 组成,每一个过程对应一个 非终结符 。
从文法开始符号S开始,递归调用文法中的其他非终结符,最终扫描整个输入串,完成分析。
如果其间有不唯一的产生式,就可能需要退回上一步重新尝试的情况,称为 回溯 。
预测分析 是 递归下降分析 技术的一个特例,通过输入中向前看固定个数的符号选择正确的产生式。
如果一个文法可以构造出向前看k个符号的预测分析器,称为LL(k)文法 。
预测分析不需要回溯,具有确定性。
含有 形式产生式的文法称为是 直接左递归 的。
如果一个文法中有一个非终结符A使得对某个串存在推导 ,那么这个文法是 左递归 的。其中,经过两步或以上推导产生的左递归,称为 间接左递归 的。
左递归会使递归下降分析器陷入无限循环。
文法
即
该文法是直接左递归的,会陷入无限循环。
将以上文法转换为:
即可消除左递归。事实上,这个过程把左递归转换成了右递归。
消除直接左递归的一般形式
使用代入法。
对于一个文法,通过改写产生式来 推迟决定 ,等获得足够多的输入信息再做正确的决定。
例:文法:
可以改写为:
从文法的开始符号S开始,每一步推导根据当前句型的最左非终结符A和当前输入符号α,选择正确的A-产生式。为保证分析的确定性,选出的候选式必须是唯一的。
S_文法(简单的确定型文法)
可能在某个举行中紧跟在A后面的终结符a的集合,记为 FOLLOW(A) 。
如果A是某个句型的最右符号,则将结束符“ $ ”添加到FOLLOW(A)中。
例:文法:
中,FOLLOW(B) = {a, c}
产生式 的可选集是指可以选用该产生式进行推导时对应的输入符号的集合,记为 SELECT(A->β) 。
例如
SELECT(A -> aβ)={a}
SELECT(A -> aβ | bγ)={a, b}
SELECT(A -> ε)=FOLLOW(A)
q_文法
文法符号串α串首终结符的集合,记作 FIRST(A) 。