重言式判别
2017-02-11 13:41
106 查看
原题参见严蔚敏版《数据结构题集(C语言版)》P148 5.1
简单来说,就是写一个程序判断类似于(A|~A)&(B|~B)的逻辑表达式是重言式,矛盾式,还是两者都不是。
自底向上的算符优先法:我个人的理解是,类比通过栈计算算术表达式的算法。一个栈负责存叶子节点,一个栈负责存逻辑运算符。找到两个叶子节点,同时找到当前优先级相对高的符号,组成一棵子树,然后将这棵子树的根节点作为一个新的叶子节点。不断出栈入栈,最后形成一棵代表逻辑表达式的二叉树。运算符之间的优先级关系可以用一个写定的二维数组表示。
自顶向下分割,先序遍历建立二叉树的方法:例如(A|~A)&(B|~B),其先序序列可以为“& | A ~ A | B ~ B”,如果能由逻辑表达式得到这样一个对应的先序序列,然后按照先序遍历建成二叉树就行。
为什么要采用二叉树去表示逻辑表达式?:如果是算数表达式,用二叉树来表示就会显得非常繁琐,讲道理,一边处理就可以一边计算了。反观逻辑表达式,它和算术表达式的不同就很能说明问题,逻辑表达式里的一个命题A,可以为TRUE或FALSE,为了证明这个表达式是否为重言式,就得来回给表达式中的命题带入TRUE或FALSE计算结果,这时候将逻辑表达式建立为一个二叉树就能很方便处理:通过递归。
先,中,后序三种遍历中,用后序序列遍历计算最好:例如(A|~A)&(B|~B),假如它的先,中,后序序列分别是:“& | A ~ A | B ~ B”、“A | ~ A & B | ~ B”、“A A ~ | B B ~ | &”。首先,因为括号是不会出现在二叉树中的,中序序列是没法体现出之前括号给定的优先级关系,所以中序不适合计算。先序和后序能体现括号给定的优先级。先序序列需要从右往左找运算符计算,比如B和~B要向左找‘|’,而后序是从左往右寻,先A,第二个A做‘~’运算,然后前面两个做‘|’运算。显然从左往右更符合我们日常计算的习惯。
以二叉树表示表达式的递归定义如下:若表达式为数或简单的变量,则相应二叉树中仅有一个根结点,其数据域存放该表达式信息;若表达式=(第一操作数)(运算符)(第二操作数),则相应的二叉树中以左子树表示第一操作数;右子树表示第二操作数;根结点的数据域存放运算符(若为一元运算符,则左子树为空)。操作数本身又为表达式。
然后我就打算强行写一个递归函数,每次调用的目的在于寻找第一操作数,运算符和第二操作数,将这三个组合成一棵子二叉树,并返回运算符(根节点)的指针。写的时候发现有很多特殊情况要处理,然后就补充了大量的if语句。最后完成并调试很多遍后,虽然书中的例子我都输入通过了,我依旧觉得我这个算法有问题,不完善。
在建立好二叉树后,考虑如何计算表达式的值并证明该式子是否为重言式。我的想法是去维护一个一维数组,这个数组记录了所有命题和命题的取值(0 or 1,也就是FALSE还是TRUE),每次计算都会去查询这个数组,当前计算的命题对应的取值。我考虑过指针数组,但感觉逻辑表达式中命题经常重名,命题相同但在二叉树中的节点位置不同,存储起来也很麻烦,还不如一维数组加查找。
最后证明逻辑表达式是否为重言式,意味着我很有可能要产生一个真值表,产生一个真值表就意味着我需要在3中提到一维数组中做0和1的全排列,我唯一想到的方法是用递归,每次进入递归前改一下。
最后的最后我偷懒没写释放树申请空间的代码。
先copy一下代码:有些注释是我加的
[b]总体评价:很清晰,很简洁,很漂亮的解决方案(比我靠谱多了!)。通过构建加权字符数组的方法解决二叉树的建立问题。这个算法里我觉得其实不用每次都重新构建二叉树,因为改动的只有命题的值。这个算法产生真值表的方式是用每次产生一个二进制字符串实现的,很新颖。
加权字符数组:简单的来说,先是按照运算符号的优先级,给对应的字符赋予对应的权值,然后在根据括号来调整。上述代码的调整方法非常巧妙,如果在同一个闭合的括号内的符号会因为只调整了一次而增大,在闭合括号外的符号会调整两次,一次增大一次减小而不变,例如上面代码中先+5再-5,这样就有效区分了符号的优先级,使得建立二叉树时更方便,因为每次只需找到当前序列中权值最小的,这个必然是当前子树的根。很聪明,很简洁,很漂亮!这些数值设定时只需产生区分就好。
通过二进制字符串产生真值表的带入值:我使用的方法是递归产生全排列序列,而上述代码使用的方法是先计算出输入的逻辑命题中有几个命题(几个A~Z,例如n个),然后写一个2^n的循环,通过iota()函数去将十进制数转换为二进制字符串,接着一位一位赋值给二叉树的叶子节点。很简单,也很数字逻辑。
iota()函数:一些简单的有趣的笔记,它不是标准C++/C函数,windows上使用比较靠谱,转换后有个特点:例如,十进制的0->二进制0,十进制的3->二进制11,十进制的15->二进制1111,是几位就是几位,不管你字符串原来的长度。
char*itoa(int value,char*string,int radix);
int value 被转换的整数,char *string 转换后储存的字符数组,int radix 转换进制数,如2,8,10,16 进制等
简单来说,就是写一个程序判断类似于(A|~A)&(B|~B)的逻辑表达式是重言式,矛盾式,还是两者都不是。
一、杂乱的知识点笔记
识别逻辑表达式的符号形式并建立二叉树有两种策略:自底向上的算符优先法和自顶向下分割,先序遍历建立二叉树的方法。自底向上的算符优先法:我个人的理解是,类比通过栈计算算术表达式的算法。一个栈负责存叶子节点,一个栈负责存逻辑运算符。找到两个叶子节点,同时找到当前优先级相对高的符号,组成一棵子树,然后将这棵子树的根节点作为一个新的叶子节点。不断出栈入栈,最后形成一棵代表逻辑表达式的二叉树。运算符之间的优先级关系可以用一个写定的二维数组表示。
自顶向下分割,先序遍历建立二叉树的方法:例如(A|~A)&(B|~B),其先序序列可以为“& | A ~ A | B ~ B”,如果能由逻辑表达式得到这样一个对应的先序序列,然后按照先序遍历建成二叉树就行。
为什么要采用二叉树去表示逻辑表达式?:如果是算数表达式,用二叉树来表示就会显得非常繁琐,讲道理,一边处理就可以一边计算了。反观逻辑表达式,它和算术表达式的不同就很能说明问题,逻辑表达式里的一个命题A,可以为TRUE或FALSE,为了证明这个表达式是否为重言式,就得来回给表达式中的命题带入TRUE或FALSE计算结果,这时候将逻辑表达式建立为一个二叉树就能很方便处理:通过递归。
先,中,后序三种遍历中,用后序序列遍历计算最好:例如(A|~A)&(B|~B),假如它的先,中,后序序列分别是:“& | A ~ A | B ~ B”、“A | ~ A & B | ~ B”、“A A ~ | B B ~ | &”。首先,因为括号是不会出现在二叉树中的,中序序列是没法体现出之前括号给定的优先级关系,所以中序不适合计算。先序和后序能体现括号给定的优先级。先序序列需要从右往左找运算符计算,比如B和~B要向左找‘|’,而后序是从左往右寻,先A,第二个A做‘~’运算,然后前面两个做‘|’运算。显然从左往右更符合我们日常计算的习惯。
二、混乱的大致实现思路
首先要解决的问题是,怎么用先序遍历的方式建立二叉树?我考虑过能不能写一个函数将逻辑函数字符串转换成先序序列,然后直接用之前写的先序建立一个二叉树。然后我发现这个方式有两个难点:括号的存在对符号优先级的影响;单纯一个先序不足以建立一棵二叉树。对此毫无思路的我,看到了书中的提示:以二叉树表示表达式的递归定义如下:若表达式为数或简单的变量,则相应二叉树中仅有一个根结点,其数据域存放该表达式信息;若表达式=(第一操作数)(运算符)(第二操作数),则相应的二叉树中以左子树表示第一操作数;右子树表示第二操作数;根结点的数据域存放运算符(若为一元运算符,则左子树为空)。操作数本身又为表达式。
然后我就打算强行写一个递归函数,每次调用的目的在于寻找第一操作数,运算符和第二操作数,将这三个组合成一棵子二叉树,并返回运算符(根节点)的指针。写的时候发现有很多特殊情况要处理,然后就补充了大量的if语句。最后完成并调试很多遍后,虽然书中的例子我都输入通过了,我依旧觉得我这个算法有问题,不完善。
在建立好二叉树后,考虑如何计算表达式的值并证明该式子是否为重言式。我的想法是去维护一个一维数组,这个数组记录了所有命题和命题的取值(0 or 1,也就是FALSE还是TRUE),每次计算都会去查询这个数组,当前计算的命题对应的取值。我考虑过指针数组,但感觉逻辑表达式中命题经常重名,命题相同但在二叉树中的节点位置不同,存储起来也很麻烦,还不如一维数组加查找。
最后证明逻辑表达式是否为重言式,意味着我很有可能要产生一个真值表,产生一个真值表就意味着我需要在3中提到一维数组中做0和1的全排列,我唯一想到的方法是用递归,每次进入递归前改一下。
最后的最后我偷懒没写释放树申请空间的代码。
三、不靠谱的代码实现
#include<stdio.h> #include<stdlib.h> #include<string.h> #define MAX_A_LEN 30 #define MAX_S_NUM 20 typedef struct LSNode{//二叉树节点 char f; int b; struct LSNode* LSubNode; struct LSNode* RSubNode; }LSNode; typedef struct CC{ char f; int b; }CC; typedef struct CCTABLE{//对应命题的一维数组的结构定义 CC count[MAX_S_NUM]; int total; }CCTABLE; LSNode* CLS(char* LogicStatent,int length,char Prec,CCTABLE* C); LSNode* CreateNode(char c,CCTABLE* C); void freeTree(LSNode* T);//偷懒没写的释放申请空间函数 int Judge_S(LSNode* T,CCTABLE* C,int mark); int Caculate_S(LSNode* T,CCTABLE* C); int main() { int slu; LSNode* HEAD; CCTABLE* C; C=malloc(sizeof(CCTABLE)); C->total=0; char LogicStatent[MAX_A_LEN]; memset(LogicStatent,'\0',MAX_A_LEN); gets(LogicStatent); HEAD=CLS(LogicStatent,strlen(LogicStatent),'\0',C); slu=Judge_S(HEAD,C,0); if(slu==-1){ printf("Satisfactible\n"); } else if(slu==0){ printf("False forever\n"); } else if(slu==1){ printf("True forever\n"); } return 0; } int Judge_S(LSNode* T,CCTABLE* C,int mark)//判断函数,返回值-1代表可变式,0为矛盾式,1为重言式 { int slu; static int preslu=-2; if(mark==C->total){ slu=Caculate_S(T,C); if(preslu==-2) preslu=slu; else if(slu!=preslu){ preslu=-1; } } else{ C->count[mark].b=0; Judge_S(T,C,mark+1); if(preslu==-1) return preslu; C->count[mark].b=1; Judge_S(T,C,mark+1); if(preslu==-1) return preslu; } return preslu; } int Caculate_S(LSNode* T,CCTABLE* C)//在给定数组的情况下,计算的函数 { int i=0; if(T==NULL) return -99; if(T->f>='A'&&T->f<='Z'){ while(i<C->total){ if(T->f==C->count[i].f) T->b=C->count[i].b; ++i; } } else{ Caculate_S(T->LSubNode,C); Caculate_S(T->RSubNode,C); if(T->f=='~'&&T->LSubNode!=NULL) T->b=!(T->LSubNode->b); else if(T->f=='~'&&T->RSubNode!=NULL) T->b=!(T->RSubNode->b); else if(T->f=='&') T->b=T->LSubNode->b&T->RSubNode->b; else if(T->f=='|') T->b=T->LSubNode->b|T->RSubNode->b; } return T->b; } LSNode* CLS(char* LogicStatent,int length,char Prec,CCTABLE* C)//生成逻辑命题表达式树的函数 { static int pos = 0; LSNode* FONPtr=NULL,*SONPtr=NULL,*ROOTPtr=NULL; while(pos<=length-1){ if(FONPtr==NULL&&LogicStatent[pos]!='~'&&ROOTPtr==NULL){//找第一操作数 if(LogicStatent[pos]=='(') {++pos;FONPtr=CLS(LogicStatent,length,'(',C);} else if(LogicStatent[pos]<='Z'&&LogicStatent[pos]>='A'){ FONPtr=CreateNode(LogicStatent[pos],C); ++pos; } else{ printf("WORRY WITH FON\n"); } } else if(ROOTPtr==NULL){//找运算符 if(LogicStatent[pos]=='&'||LogicStatent[pos]=='|'||LogicStatent[pos]=='~'){ ROOTPtr=CreateNode(LogicStatent[pos],C); ROOTPtr->LSubNode=FONPtr; ++pos; } else{ printf("WORRY WITH ROOT\n"); } } else if(SONPtr==NULL){//找第二操作数 if(LogicStatent[pos]<='Z'&&LogicStatent[pos]>='A'){ if(ROOTPtr->f=='|'&&LogicStatent[pos+1]=='&') SONPtr=CLS(LogicStatent,length,'|',C); else {SONPtr=CreateNode(LogicStatent[pos],C);++pos;} } else if(LogicStatent[pos]=='(') {++pos;SONPtr=CLS(LogicStatent,length,'(',C);} else if(LogicStatent[pos]=='~') SONPtr=CLS(LogicStatent,length,ROOTPtr->f,C); else{ printf("WORRY WITH SON\n"); } ROOTPtr->RSubNode=SONPtr; } else{//其他情况的处理 if(Prec=='('&&LogicStatent[pos]==')') {++pos;return ROOTPtr;} else if(Prec!='('&&LogicStatent[pos]==')') return ROOTPtr; else if(Prec=='&'&&LogicStatent[pos]=='|') {return ROOTPtr;} else if(pos<=length-1){ FONPtr=ROOTPtr; ROOTPtr=CreateNode(LogicStatent[pos],C); ROOTPtr->LSubNode=FONPtr; SONPtr=NULL; ++pos; } } } if(ROOTPtr==NULL){ ROOTPtr=FONPtr; } return ROOTPtr; } LSNode* CreateNode(char c,CCTABLE* C)//创建二叉树节点的函数 { int i=0; LSNode* n; n=malloc(sizeof(LSNode)); n->f=c; n->b=0; n->LSubNode=NULL; n->RSubNode=NULL; if(c<='Z'&&c>='A'){ while(i<C->total){ if(C->count[i].f==c) break; ++i; } if(i==C->total){ C->count[i].f=c; C->count[i].b=0; C->total+=1; } } return n; }
四、研究研究别人是怎么写的
重言式判别 by Kanone先copy一下代码:有些注释是我加的
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct DutyElement{ char data; int duty; }DutyElement; DutyElement DutyArray[100]; //定义加权数组 typedef struct BiTNode{ char data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree; void InitDutyArray(char *s, int n) //根据输入的序列初始化加权数组 { int i, j; for (i = 0; i < n; i++) { DutyArray[i].data = s[i]; switch (DutyArray[i].data) //分别赋权值 { case '~': DutyArray[i].duty = 3; break; case '&': DutyArray[i].duty = 2; break; case '|': DutyArray[i].duty = 1; break; default: DutyArray[i].duty = 0; break; } } for (i = 0; i < n; i++) { if (DutyArray[i].data == '(') //是左括号的话则对运算符进行加权操作 { for (j = i; j < n; j++) { if (DutyArray[j].duty != 0) DutyArray[j].duty += 5; } } else { if (DutyArray[i].data == ')') //右括号的话对运算符进行减权操作 { for (j = i; j < n; j++) { if (DutyArray[j].duty != 0) DutyArray[j].duty -= 5; } } } } } int SearchMinDutyOperator(int a, int b) //寻找权值最小的运算符,即为二叉树的根节点 { int i, n = a, duty = 1000; for (i = a; i < b + 1; i++) { if (DutyArray[i].duty > 0 && DutyArray[i].duty < duty) { n = i; duty = DutyArray[i].duty; } } return n; } int ParenthesesCloesed(int a, int b) //判断序列是否在最外层有一对括号 { int i, n = 0; for (i = a; i <= b; i++) { if (DutyArray[i].data == '(') n++; if (DutyArray[i].data == ')') n--; if (n == 0) break; } if (i == b) return 1; else return 0; } BiTree Create(int a, int b) //根据加权数组创建二叉树 { BiTree p; if (DutyArray[a].data == '(' && DutyArray.data == ')' && ParenthesesCloesed(a, b) == 1) //去括号 { a += 1; b -= 1; } if (a > b) p = NULL; else { if (a == b) { p = (BiTNode *)malloc(sizeof(BiTNode)); p->data = DutyArray[a].data; p->lchild = NULL; p->rchild = NULL; } else { int n; n = SearchMinDutyOperator(a, b); p = (BiTNode *)malloc(sizeof(BiTNode)); p->data = DutyArray .data; p->lchild = Create(a, n - 1); p->rchild = Create(n + 1, b); } } return p; } void AssignValue(BiTree T, char letter, char binary) //递归为对应的字母赋二进制字符 { if (T) { if (T->data == letter) T->data = binary; AssignValue(T->lchild, letter, binary); AssignValue(T->rchild, letter, binary); } } int Calculate(BiTree T) //递归计算二进制字符二叉树 { switch (T->data) { case '0': return 0; case '1': return 1; case '~': return !Calculate(T->rchild); case '&': return (Calculate(T->lchild) & Calculate(T->rchild)); case '|': return (Calculate(T->lchild) | Calculate(T->rchild)); } } void main() { BiTree T; int m = 0, n, i, j, k, length = 0, strlength, num = 1, flag; char value[20] = {0}; char s[100] = {0}; char c, *str; str = (char *)malloc(sizeof(char) * 20); for (i = 0; i < 20; i++) { *(str + i) = 0; } c = getchar(); i = 0; while (c != 'n') { if (c != ' ') { s[i] = c; i++; } c = getchar(); } for (i = 0; i < 100; i++) { if (s[i] == 0) { n = i; //n为输入序列的长度 break; } } for (i = 0; i < n; i++) //计算逻辑命题中的命题字母数length { if (strchr(s, 'A' + i) != NULL) length++; else break; } length = i; InitDutyArray(s, n); //初始化加权数组 T = Create(0, n - 1); for (i = 0; i < length; i++) { AssignValue(T, 'A' + i, '0'); } flag = Calculate(T); for (i = 0; i < length; i++) //可以认为是真值表函数 num { num *= 2; } for (i = 0; i < num; i++) { T = Create(0, n - 1); //由于每运算一次会将原来的二叉树中的变元改变,所以得重新构造 //本文作者比较倾向这里不需要再次构建 itoa(i, str, 2); //将行数转换成一个二进制数字符串 strlength = strlen(str); for (j = 0; j < length; j++) { if (strlength - j - 1 >= 0) value[length - j - 1] = *(str + strlength - 1 - j);//itoa函数函数的特点决定的,转换后的二进制数是几位就是几位 else value[length - j - 1] = '0'; } for (k = 0; k < length; k++) { AssignValue(T, 'A' + k, value[k]); } if (Calculate(T) != flag) { printf("Satisfactible\n"); break; } else { if (i == num - 1 && flag == 1) { printf("True forever\n"); return; } if (i == num - 1 && flag == 0) { printf("False forever\n"); return; } } } for (i = 0; i < length; i++) //从这里开始有些问题。。。但不影响上面的代码 { printf("%c ", 'A' + i); } printf("\n"); c = getchar(); //输入对各字符的赋值,保存在value数组中 i = 0; while (c != ';') { if (c != ',') { value[i] = c; i++; } c = getchar(); } T = Create(0, n - 1); //重新构造二叉树 for (i = 0; i < length; i++) { AssignValue(T, 'A' + i, value[i]); } printf("The result is %d\n", Calculate(T)); }
[b]总体评价:很清晰,很简洁,很漂亮的解决方案(比我靠谱多了!)。通过构建加权字符数组的方法解决二叉树的建立问题。这个算法里我觉得其实不用每次都重新构建二叉树,因为改动的只有命题的值。这个算法产生真值表的方式是用每次产生一个二进制字符串实现的,很新颖。
加权字符数组:简单的来说,先是按照运算符号的优先级,给对应的字符赋予对应的权值,然后在根据括号来调整。上述代码的调整方法非常巧妙,如果在同一个闭合的括号内的符号会因为只调整了一次而增大,在闭合括号外的符号会调整两次,一次增大一次减小而不变,例如上面代码中先+5再-5,这样就有效区分了符号的优先级,使得建立二叉树时更方便,因为每次只需找到当前序列中权值最小的,这个必然是当前子树的根。很聪明,很简洁,很漂亮!这些数值设定时只需产生区分就好。
通过二进制字符串产生真值表的带入值:我使用的方法是递归产生全排列序列,而上述代码使用的方法是先计算出输入的逻辑命题中有几个命题(几个A~Z,例如n个),然后写一个2^n的循环,通过iota()函数去将十进制数转换为二进制字符串,接着一位一位赋值给二叉树的叶子节点。很简单,也很数字逻辑。
iota()函数:一些简单的有趣的笔记,它不是标准C++/C函数,windows上使用比较靠谱,转换后有个特点:例如,十进制的0->二进制0,十进制的3->二进制11,十进制的15->二进制1111,是几位就是几位,不管你字符串原来的长度。
char*itoa(int value,char*string,int radix);
int value 被转换的整数,char *string 转换后储存的字符数组,int radix 转换进制数,如2,8,10,16 进制等
The end
相关文章推荐
- 重言式判别
- 数据结构实习——重言式的判别(写的不好不要见怪)
- 重言式的判别
- 重言式判别 (数据结构课程设计)
- C++重言式判别
- 全角/半角的判别by正则表达式
- 利用第三方的Jar包内的类和方法来判别文件编码
- ORACLE中判别字符串是不是字母数字型的
- 布尔表达式判别程序
- c 中文判别
- 大小端模式判别
- 判别模型和生成模型的区别
- 判别股票下跌前的逃命信号
- 线性判别分析(LDA), 主成分分析(PCA)
- 函数论_E.C.Tichmarsh_Page 4 级数一致收敛的魏尔斯特拉斯 M-判别法 的推广
- Java 判别TXT文档的编码方式
- 判别能否有flash插件 - gtd03的专栏
- 线性判别分析(Linear Discriminant Analysis)(一)
- 生成学习、高斯判别、朴素贝叶斯—斯坦福ML公开课笔记5
- iOS 判别汉字