first集和follow集生成算法模拟
- 格式:doc
- 大小:195.00 KB
- 文档页数:25
【编译原理】语法分析LL(1)分析法的FIRST和FOLLOW集 近来复习编译原理,语法分析中的⾃上⽽下LL(1)分析法,需要构造求出⼀个⽂法的FIRST和FOLLOW集,然后构造分析表,利⽤分析表+⼀个栈来做⾃上⽽下的语法分析(递归下降/预测分析),可是这个FIRST集合FOLLOW集看得我头⼤。
教课书上的规则如下,⽤我理解的语⾔描述的:任意符号α的FIRST集求法:1. α为终结符,则把它⾃⾝加⼊FIRSRT(α)2. α为⾮终结符,则:(1)若存在产⽣式α->a...,则把a加⼊FIRST(α),其中a可以为ε(2)若存在⼀串⾮终结符Y1,Y2, ..., Yk-1,且它们的FIRST集都含空串,且有产⽣式α->Y1Y2...Yk...,那么把FIRST(Yk)-{ε}加⼊FIRST(α)。
如果k-1抵达产⽣式末尾,那么把ε加⼊FIRST(α) 注意(2)要连续进⾏,通俗地描述就是:沿途的Yi都能推出空串,则把这⼀路遇到的Yi的FIRST集都加进来,直到遇到第⼀个不能推出空串的Yk为⽌。
重复1,2步骤直⾄每个FIRST集都不再增⼤为⽌。
任意⾮终结符A的FOLLOW集求法:1. A为开始符号,则把#加⼊FOLLOW(A)2. 对于产⽣式A-->αBβ: (1)把FIRST(β)-{ε}加到FOLLOW(B) (2)若β为ε或者ε属于FIRST(β),则把FOLLOW(A)加到FOLLOW(B)重复1,2步骤直⾄每个FOLLOW集都不再增⼤为⽌。
⽼师和同学能很敏锐地求出来,⽽我只能按照规则,像程序⼀样⼀条条执⾏。
于是我把这个过程写成了程序,如下:数据元素的定义:1const int MAX_N = 20;//产⽣式体的最⼤长度2const char nullStr = '$';//空串的字⾯值3 typedef int Type;//符号类型45const Type NON = -1;//⾮法类型6const Type T = 0;//终结符7const Type N = 1;//⾮终结符8const Type NUL = 2;//空串910struct Production//产⽣式11 {12char head;13char* body;14 Production(){}15 Production(char h, char b[]){16 head = h;17 body = (char*)malloc(strlen(b)*sizeof(char));18 strcpy(body, b);19 }20bool operator<(const Production& p)const{//内部const则外部也为const21if(head == p.head) return body[0] < p.body[0];//注意此处只适⽤于LL(1)⽂法,即同⼀VN各候选的⾸符不能有相同的,否则这⾥的⼩于符号还要向前多看⼏个字符,就不是LL(1)⽂法了22return head < p.head;23 }24void print() const{//要加const25 printf("%c -- > %s\n", head, body);26 }27 };2829//以下⼏个集合可以再封装为⼀个⼤结构体--⽂法30set<Production> P;//产⽣式集31set<char> VN, VT;//⾮终结符号集,终结符号集32char S;//开始符号33 map<char, set<char> > FIRST;//FIRST集34 map<char, set<char> > FOLLOW;//FOLLOW集3536set<char>::iterator first;//全局共享的迭代器,其实觉得应该⽤局部变量37set<char>::iterator follow;38set<char>::iterator vn;39set<char>::iterator vt;40set<Production>::iterator p;4142 Type get_type(char alpha){//判读符号类型43if(alpha == '$') return NUL;//空串44else if(VT.find(alpha) != VT.end()) return T;//终结符45else if(VN.find(alpha) != VN.end()) return N;//⾮终结符46else return NON;//⾮法字符47 }主函数的流程很简单,从⽂件读⼊指定格式的⽂法,然后依次求⽂法的FIRST集、FOLLOW集1int main()2 {3 FREAD("grammar2.txt");//从⽂件读取⽂法4int numN = 0;5int numT = 0;6char c = '';7 S = getchar();//开始符号8 printf("%c", S);9 VN.insert(S);10 numN++;11while((c=getchar()) != '\n'){//读⼊⾮终结符12 printf("%c", c);13 VN.insert(c);14 numN++;15 }16 pn();17while((c=getchar()) != '\n'){//读⼊终结符18 printf("%c", c);19 VT.insert(c);20 numT++;21 }22 pn();23 REP(numN){//读⼊产⽣式24 c = getchar();25int n; RINT(n);26while(n--){27char body[MAX_N];28 scanf("%s", body);29 printf("%c --> %s\n", c, body);30 P.insert(Production(c, body));31 }32 getchar();33 }3435 get_first();//⽣成FIRST集36for(vn = VN.begin(); vn != VN.end(); vn++){//打印⾮终结符的FIRST集37 printf("FIRST(%c) = { ", *vn);38for(first = FIRST[*vn].begin(); first != FIRST[*vn].end(); first++){39 printf("%c, ", *first);40 }41 printf("}\n");42 }4344 get_follow();//⽣成⾮终结符的FOLLOW集45for(vn = VN.begin(); vn != VN.end(); vn++){//打印⾮终结符的FOLLOW集46 printf("FOLLOW(%c) = { ", *vn);47for(follow = FOLLOW[*vn].begin(); follow != FOLLOW[*vn].end(); follow++){48 printf("%c, ", *follow);49 }50 printf("}\n");51 }52return0;53 }主函数其中⽂法⽂件的数据格式为(按照平时做题的输⼊格式设计的):第⼀⾏:所有⾮终结符,⽆空格,第⼀个为开始符号;第⼆⾏:所有终结符,⽆空格;剩余⾏:每⾏描述了⼀个⾮终结符的所有产⽣式,第⼀个字符为产⽣式头(⾮终结符),后跟⼀个整数位候选式的个数n,之后是n个以空格分隔的字符串为产⽣式体。
一、是非题(请在括号内,正确的划√,错误的划×)(每个2分,共20分)1.“ 用高级语言书写的源程序都必须通过编译,产生目标代码后才能投入运行”这种说法。
( )2.若一个句型中出现了某产生式的右部,则此右部一定是该句型的句柄。
( )3.一个句型的句柄一定是文法某产生式的右部。
( )4.在程序中标识符的出现仅为使用性的。
( )5.仅考虑一个基本块,不能确定一个赋值是否真是无用的。
( )6.削减运算强度破坏了临时变量在一基本块内仅被定义一次的特性。
( )7.在中间代码优化中循环上的优化主要有不变表达式外提和削减运算强度。
( )8.算符优先关系表不一定存在对应的优先函数。
( )9.数组元素的地址计算与数组的存储方式有关。
( )10.编译程序与具体的机器有关,与具体的语言无关。
( )参考答案:1、×2、×3、√4、×5、√6、√7、×8、×9、×10、×二、选择题(请在前括号内选择最确切的一项作为答案划一个勾,多划按错论)(每个4分,共40分)1.通常一个编译程序中,不仅包含词法分析,语法分析,中间代码生成,代码优化,目标代码生成等五个部分,还应包括_____。
A.( ) 模拟执行器 B.( ) 解释器C.( ) 表格处理和出错处理 D.( ) 符号执行器2.文法 G[N]= ( {b} , {N , B} , N ,{N→b│bB ,B→bN} ),该文法所描述的语言是A.( ) L(G[N])={bi│i≥0} B.( ) L(G[N])={b2i│i≥0}C.( ) L(G[N])={b2i+1│i≥0} D.( ) L(G[N])={b2i+1│i≥1}3.一个句型中的最左_____称为该句型的句柄。
A.( ) 短语 B.( ) 简单短语C.( ) 素短语 D.( ) 终结符号4.设 G 是一个给定的文法, S 是文法的开始符号,如果 S->x( 其中x∈V*), 则称 x 是文法 G 的一个_____。
五、语法分析——自底向上分析法已知文法G:EE+TE TTT*FTFF(E)Fi(1)求文法G中每个非终结符的First集和Follow集。
(2)构造文法G的SLR(1)预测分析表。
(20分)首先构造增广文法:SEEE+TE TTT*FTFF(E)FiFirst(S)=First(E)=First(T)=First(F)={(,I)Follow(S)={#} Follow(E)={+,#,}}Follow(T)={+,},#,*} Follow(F)={+,},#,*}状态Action Gotoi + * ( ) # E T F0 S5 S4 1 2 31 S6 Acc2 r 2 S7 r 2 r 23 r4 r 4 r 4 r44 S5 S4 8 2 35 r6 r 66 S5 9 37 S5 108 S6 S119 r 1 S7 r 1 r 110 r 3 r 3 r 3 r 311 r 5 r 5 r 5 r 5注:识别可归前缀的DFA共12项。
词法分析——确定性有穷自动机为以下字符集编写正规表达式,并构造与之等价的最简DFA(写出详细的具体过程):在字母表{a,b}上的包含偶数个a且含有任意数目b的所有字符串。
(15分)(b*ab*ab*)*b a b1a状态Action GOTOa b d e f $ S R T0 S3 11 acc2 r2 S3 r2 r2 53 S6 S4 24 r4 r4 r4 r45 S10 96 77 S88 r3 r3 r3 r39 r1 r1 r110 r6 S6 S4 r6 r6 1111 S1212 r5 r5 r5五、语法分析——自底向上分析法已知文法G:S’SS bRSTS bRRdSaR eTfRaTf(1)求文法G中每个非终结符的First集和Follow集。
(2)构造文法G的SLR(1)预测分析表。
(20分)frist(s’)={b} follow(s’)={$}frist(s)={b} follow(s)={f,a, $}frist(R) ={d,e} follow( R )={a,b,f, $}frist(T)={t} follow (T)={a,f,#}五、对下面的文法(15分)S->UTa|TbT->S|Sc|dU->US|e判断是否为LR(0),SLR(1),说明理由,并构造相应的分析表。
First集合的求法:First集合最终是对产生式右部的字符串而言的,但其关键是求出非终结符的First集合,由于终结符的First集合就是它自己,所以求出非终结符的First集合后,就可很直观地得到每个字符串的First集合。
1. 直接收取:对形如U-a…的产生式(其中a是终结符),把a收入到First(U)中2. 反复传送:对形入U-P…的产生式(其中P是非终结符),应把First(P)中的全部内容传送到First(U)中。
Follow集合的求法:Follow集合是针对非终结符而言的,Follow(U)所表达的是句型中非终结符U所有可能的后随终结符号的集合,特别地,“#”是识别符号的后随符。
1. 直接收取:注意产生式右部的每一个形如“…Ua…”的组合,把a直接收入到Follow(U)中。
2.直接收取:对形如“…UP…”(P是非终结符)的组合,把First(P)除ε直接收入到Follow(U)中。
3.反复传送:对形如P-…U的产生式(其中U是非终结符),应把Follow(P)中的全部内容传送到Follow(U)中。
(或 P-…UB且First(B)包含ε,则把First(B)除ε直接收入到Follow(U)中,并把Follow(P)中的全部内容传送到Follow(U)中)例1:判断该文法是不是LL(1)文法,说明理由 S→ABc A→a|ε B→b|ε?First集合求法就是:能由非终结符号推出的所有的开头符号或可能的ε,但要求这个开头符号是终结符号。
如此题A可以推导出a和ε,所以FIRST(A)={a,ε};同理FIRST (B)={b,ε};S可以推导出aBc,还可以推导出bc,还可以推导出c,所以FIRST(S)={a,b,c}。
Follow集合的求法是:紧跟随其后面的终结符号或#。
但文法的识别符号包含#,在求的时候还要考虑到ε。
具体做法是把所有包含你要求的符号的产生式都找出来,再看哪个有用。
FIRST集和FOLLOW集求法龙书算法:First:(1)、如果X是终结符,那么First(X) = X;(2)、如果X是⾮终结符,且XàY1Y2......Yk是⼀个产⽣式,其中k>=1;那么如果对于某个I, a在First(Yi)中,且#(空串)在所有的First(Y1)…..First(Yi-1)中,就吧a加⼊到First(X)中。
(3)、如果Xà#(空串)是⼀个产⽣式,那么将#加⼊到First(X)中。
Follow:(1)、将$放⼊到Follow(S)中,其中S是开始符号,⽽$是输⼊右端结束的标记。
(2)、如果存在⼀个产⽣式AàaBb,那么First(b)中除#(空串)外地所有符号都在Follow(B)中。
(3)、如果存在⼀个产⽣式AàaB, 或存在AàaBb且First(b)包含#(空串),那么Follow(A)中的所有符号都在Follow(B)中。
⾃⼰理解:First:(看X的产⽣式)(1)、如果X是终结符,那么First(X)= X;(2)、如果X是⾮终结符,且XàY1Y2......Yk,i=1;1)、将First(Yi)加⼊到First(X)中,2)、如果#包含着First(Yi)中,i++,重复1);3)、如果#不包含在First(Yi)中,First(X)计算完成;(3)、如果Xà#(空串)是⼀个产⽣式,那么将#加⼊到First(X)中。
Follow:(看在右边有B的产⽣式)(1)、将$放⼊到Follow(S)中,其中S是开始符号,⽽$是输⼊右端结束的标记。
(2)、如果存在⼀个产⽣式AàaBb,那么First(b)中除#(空串)外地所有符号都在Follow(B)中。
(3)、如果存在⼀个产⽣式AàaB, 或存在AàaBb且First(b)包含#(空串),那么Follow(A)中的所有符号都在Follow(B)中。
1.#include "stdafx.h"2.#include "LR.h"3.#include "MLR1.h"4.5.#ifdef _DEBUG6.#undef THIS_FILE7.static char THIS_FILE[]=__FILE__;8.#define new DEBUG_NEW9.#endif10.//----调试部分使用的代码11.CString MLR1::GetFirst(int i){12.if(i<0||i>=GetIdentNum())return"";13.return FirstSet5(m_first[i].Fi,m_first[i].flag&2);14.}15.CString MLR1::GetFollow(int i){16.if(i<0||i>=GetIdentNum())return"";17.return FollowSet1(m_first[i].Fo,m_first[i].flag&0x08);18.}19.//----构造部分20.MLR1::MLR1(){21.}22.MLR1::~MLR1(){23.}24.void MLR1::ReSet(FILE* pf){25.//使用文件指针pf来重新驱动程序26.int i;27. p_file=pf;28. list_Express.RemoveAll();29. list_Ident.RemoveAll();30.for(i=0;i<MAP_SIZE;I++) p="(char*)m_first+sizeof(s_first)*MAX_IDENT-1;"for(char* bit_map[i]="0;">=(char*)m_first;p--)31. *p=0;32. Lex3();33. FirstSet6();34.// FollowSet3();35.}36.//----输入分析部分37.bool MLR1::Lex1(){38.//截取一个分号段到tocken中39.//功能字符取其负数40.char ch=0;41.bool end=false;42. token_len=0;43.if(feof(p_file))return false;44.while(!end&&!feof(p_file)){45.if(token_len>=LINE_LENGTH)break;46.if(fread(&ch,1,1,p_file)<=0)break;47.if(ch<=0)goto error;48.switch(ch){49.case';':50. end=true;51.case'<':52.case'>':53.case'=':54. ch=-ch;55.break;56.case'\\':57. fread(&ch,1,1,p_file);58.if(ch<=0)goto error;59.break;60. }61. token[token_len++]=ch;62. }63. token[token_len]=0;64.return true;65.error:66. fprintf(stderr,"must be 1--127");67.return false;68.}69.int MLR1::Lex2_1(char*&s,bool isUse){70.//识别非终结符并加入list_Ident71.char ident[ID_LENGTH+1];72.int t=0;73.if((int)*s++!=-'<')return 0;74.if(isalpha(*s))ident[t++]=*s++;75.else return 0;76.while(isalpha(*s)||isdigit(*s))ident[t++]=*s++;77.while(*s=='\'')ident[t++]=*s++;78.if((int)*s++!=-'>')return 0;79.if(t==0)return 0;80. ident[t]=0;81.for(t=list_Ident.GetSize()-1;t>=0;t--)82.if(list_Ident[t]==(CString)ident)break;83.if(t<0){84.if(list_Ident.GetSize()>=MAX_IDENT)return false;85. list_Ident.Add((CString)ident);86. t=list_Ident.GetSize()-1;87.if(isUse)bit_map[t/8]|=1<<(t%8);88. }89.if(!isUse)bit_map[t/8]&=~(1<<(t%8));90.return t+1;91.}92.bool MLR1::Lex2(){93.//将token中的非终结符用(-1) -- (-127)表示94.//进行语法判断<终结符>=符号表;95.register char *s,*d;96.char * end;97.int i;98. s=d=token;99. end=&token[token_len];100.if(i=Lex2_1(s))*d++=-i;101.else return false;102.if(*s++!=-'=')return false;103.while(s<END){ if(*p while(*p!="0){" *p="X+1;"char const判断表达式X能否推出LR_NULL *X){ MLR1::FirstSet1(const bool * 首先判断某非终结符能否推出LR_NULL ----First集 } true; return false; if(bit_map[i]!="0)return" i="0;i<MAP_SIZE;i++ )"for(int else list_Express.Add((CString)token); if(Lex2()) if(token_ len="=0)continue;"while(Lex1()){ 判断bit_map是否为全零,如果不是则表示有未定义的非终结符循环调用Lex1读入一句,调用Lex2进行语法分析 MLR1::Lex3(){ s<end; *d="0;" *d++="*s++;" }else if(i="Lex2_1(s,true ))*d++=-i;"if((int)*s="=-'<'){"if(*s="=-';')break;">=1){104.return false;105. }else{106.if(*p==*X)return false;107.if(!FirstSet2(*p))108.return false;109. }110. p++;111. }112.return true;113.}114.bool MLR1::FirstSet2(const char X){115.//判断非终结符X能否推出LR_NULL116. CString temp;117.if(m_first[-X-1].flag&0x40)return false;118.if(m_first[-X-1].flag&1)119.return (m_first[-X-1].flag&2)!=0;120. m_first[-X-1].flag|=0x40;121.for(int i=list_Express.GetSize();i>0;i--){122. temp=list_Express.GetAt(i-1);123.if(temp[0]==X){124.if(FirstSet1((LPCSTR)temp)){125. m_first[-X-1].flag|=3;126.return true;127. }128. }129. }130. m_first[-X-1].flag|=1;131. m_first[-X-1].flag^=0x40;132.return false;133.}134.bool MLR1::FirstSet3(const char *X,char*Fi){135.//求产生式X的First集放在F中,如果LR_NULL在First集中则返回true 136.//如果要求符号串的First集,就将X[0]设为0137.//假设X中不出现LR_NULL,LR_EOF和LR_EOS138.//假设F的长度为MAP_SIZE,有128b139.const char *p=X;140. X++;141.while(*X!=0){142.if(*X>=1){143. Fi[(*X)/8]|=1<<(*X)%8;144.return false;145. }else{146.if(*X==*p){147.if(!FirstSet2(*X))148.return false;149. }else if(!FirstSet4(*X,Fi)){150.return false;151. }152. }153. X++;154. }155.return true;156.}157.bool MLR1::FirstSet4(char const X,char*Fi){158.//求非终结符X的First集放在F中159.//如果LR_NULL在其中则返回true160. CString temp;161.if(m_first[-X-1].flag&0x40)return false;162.if((m_first[-X-1].flag&4)==0){163. m_first[-X-1].flag|=0x40;164.for(int i=list_Express.GetSize();i>0;i--){165. temp=list_Express.GetAt(i-1);166.if(temp[0]==X)167. FirstSet3((LPCSTR)temp,m_first[-X-1].Fi);168. }169. m_first[-X-1].flag|=4;170. m_first[-X-1].flag^=0x40;171. }172.if(Fi!=m_first[-X-1].Fi){173.for(int i=0;i<MAP_SIZE;I++) *p="t;"char } return for(i="0;i< MAP_SIZE;i++){" i; int为每个非终结符求First集 MLR1::FirstSet6(){ void (CString)t; if(has_null)*p++="LR_NULL;" *p+ +="i*8+j;"if(Fi[i]&(1<<j)) if(Fi[i])for(j="0;j<8;j++)" i,j; t[128];将集合表示的First变为字符串式 has_null){ char*Fi,bool MLR1::FirstSet5(const CString (m_first[-X-1 ].flag&2); Fi[i]|="m_first[-X-1].Fi[i];">0;i--){174.if((m_first[i-1].flag&1)==0)175. FirstSet2(-i);176. }177.for(i=list_Ident.GetSize();i>0;i--){178.if((m_first[i-1].flag&4)==0)179. FirstSet4(-i,m_first[i-1].Fi);180. }181.}182.CString MLR1::FollowSet1(const char*Fo,bool has_eof){183.//将集合表示的Follow变为字符串式184.char t[128];185.char *p=t;186.int i,j;187.for(i=0;i<MAP_SIZE;I++){ *p="0;"char bool * } return int (CString)t; *p++="i*8+j;" i,j; if(X 如果是非法符号则退出 *p; temp[LINE_LENGTH]; flag,rc="true;" flag.b5该符号的Follow集正在被计算 flag为真表示已经找到X,将找后继的第一个字符必须在执行前将LR_EOF加入识别符号的Follow集中只有在X的Follow集未被计算时才调用该函数求非终结符X的Follow集 Fo){ X,char MLR1::FollowSet2(const if(has_eof)*p++="LR_NULL;"if(Fo[i]&(1<<j)) if(Fo[i])for(j="0;j<8;j++)">=0||X<-MAX_IDENT)return true;188.//如果该符号正被计算则退出189.if(m_first[-X-1].flag&0x20)return false;190.//如果该符号为被计算则计算191.if((m_first[-X-1].flag&0x10)==0){192. m_first[-X-1].flag|=0x20;193.for(i=list_Express.GetSize();i>0;i--){194. sprintf(temp,list_Express.GetAt(i-1));195. flag=false;196. p=temp+1;197.while(*p!=0){198.if(!flag){199.if(*p==X){200.//表达式中出现了符号X201. flag=true;}202. }else{203.if(*p>0){204.//规则2:X后碰上终结符205. flag=false;206. Fo[*p/8]|=1<<(*p%8);207. }else{208.//规则2:X后碰上非终结符则并上它的First集209.for(j=0;j<MAP_SIZE;J++) } return for(i="0;i<M AP_SIZE;i++)" i; int void m_first[0].flag|="0x08;"求各非终结符的Follow 集 MLR1::FollowSet3(){ rc; Fo[i]|="m_first[-X-1].Fo[i];"if(Fo!="m_fir st[-X-1].Fo){" m_first[-X-1].flag^="0x20;"if(rc)m_first[-X-1].flag|="0x10;" m_first[-X-1].flag|="0x08;"if(m_first[-temp[0]-1].flag&0x08) F o[j]|="m_first[-*p-1].Fi[j];"for(j="0;j<MAP_SIZE;j++)" rc="false;"if (!FollowSet2(temp[0],m_first[-temp[0]-1].Fo)) 如果是自反关系则不计算规则3:将temp[0]的Follow集并到Fo上if(flag&&(X!="temp[0])){" p++; flag="false;"if((m_first[-*p-1].fla g&2)="=0)"如果X可以推出LR_NULL则继续>0;i--){210.if((m_first[i-1].flag&0x10)==0){211. FollowSet2(-i,m_first[i-1].Fo);212. }213. }214.}。
第1篇摘要:Follow集合在数据库查询中具有重要的应用价值,本文旨在介绍Follow集合的概念、求法以及在数据库查询中的应用。
通过详细阐述Follow集合的原理和方法,帮助读者深入理解其重要性,并学会在实际应用中有效利用Follow集合。
一、引言在数据库查询过程中,我们常常需要关注某些记录的关联记录,即所谓的“后续记录”。
为了方便查询,我们可以利用Follow集合来表示这些后续记录。
本文将介绍Follow集合的概念、求法以及在数据库查询中的应用。
二、Follow集合的概念1. 定义:Follow集合是指在一个数据表中,对于某一记录,所有与之存在关联关系的记录的集合。
2. 特点:(1)无序性:Follow集合中的记录没有特定的顺序;(2)唯一性:对于某一记录,其Follow集合是唯一的;(3)动态性:Follow集合随着查询条件的改变而改变。
三、Follow集合的求法1. 算法概述求Follow集合的基本思路是:从某一记录出发,遍历整个数据表,查找与之存在关联关系的记录,并将其加入Follow集合。
具体算法如下:(1)初始化Follow集合为空;(2)遍历数据表中的所有记录;(3)对于每一条记录,查找其关联记录,并将其加入Follow集合;(4)重复步骤(2)和(3),直到遍历完所有记录。
2. 算法实现以下是一个基于Python的Follow集合求法示例:```pythondef follow_set(data, record_id):"""求Follow集合的函数:param data: 数据表:param record_id: 记录ID:return: Follow集合"""follow_set = set()for record in data:if record['id'] == record_id:for relation in record['relations']:follow_set.add(relation)else:for relation in record['relations']:if relation in follow_set:follow_set.add(record['id'])return follow_set```3. 算法优化在实际情况中,数据表可能非常大,导致Follow集合求法效率低下。