您的位置:首页 > 理论基础 > 数据结构算法

重言式判别

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的全排列,我唯一想到的方法是用递归,每次进入递归前改一下。

最后的最后我偷懒没写释放树申请空间的代码。

三、不靠谱的代码实现

#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语言 数据结构