中缀/后缀/前缀表达式及相互转换的手算详细步骤及C代码实现


1 三种算术表达式

算术表达式由三个部分组成:操作数、运算符、界限符。界限符是必不可少的,也就是括号。括号或者说界限符反映了计算或者说运算符作用的先后顺序。但是有一个波兰数学家想这样做:可以不用界限符也能无歧义地表达运算顺序。于是发明了:① 逆波兰表达式,即后缀表达式;② 波兰表达式,即前缀表达式。

类型规则例1例2
中缀表达式运算符在两个操作数中间a+ba+b-c
后缀表达式运算符在两个操作数后面ab+ab+c-
前缀表达式运算符在两个操作数前面+ab-+abc

2 后缀表达式相关考点

2.1 中缀表达式转后缀表达式

2.1.1 手算

中缀转后缀的手算步骤:

① 确定中缀表达式中各个运算符的运算顺序,但是有时候运算顺序不唯一,因此对应的后缀表达式也不唯一。为了保证手算和机算结果相同,且保证运算顺序唯一,请遵从“左优先”原则:只要左边的运算符能先计算,就优先算左边的。确定完运算符的运算顺序后,如果有界限符即括号,就可以去掉全部的括号了,或者说可以忽略括号的存在继续下面步骤;

② 选择下一个运算符,按照【左操作数 右操作数 运算符】的方式组合成一个新的式子,然后放在原来操作符的位置。如果还有运算符没被处理,就继续②,否则最后得到的式子就是最终结果。下面是一个示例:

转换前的中缀表达式转换后的后缀表达式
((15/(7-(1+1)))*3)-(2+(1+1))15 7 1 1 + - / 3 * 2 1 1 + + -
2+(3+4)*52 3 4 + 5 * +
16+2*30/416 2 30 * 4 / +

2.1.2 机算

实现中缀表达式转后缀表达式的逻辑过程:初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右扫描中缀表达式的各个字符,直到末尾。在扫描时可能遇到三种情况:

① 当前字符是数字:直接加入后缀表达式,然后处理下一个字符;

② 当前字符是括号,又分为两种情况:

  • 1)当前字符是左括号即(则直接将当前字符入栈,然后处理下一个字符;
  • 2)当前字符是右括号即):若栈空则直接处理下一个字符。若栈非空则不断弹出栈顶元素——若弹出左括号(后则停止继续弹出(注意这个左括号(不加入后缀表达式)并直接处理下一个字符,否则将栈顶元素加入后缀表达式,然后继续弹出栈顶元素;

③ 当前字符是运算符,又分为两种情况:

  • 1)栈空则直接把当前字符入栈,然后处理下一个字符;
  • 2)栈非空则不断弹出栈顶元素:若栈顶元素是左括号(或优先级低于当前字符的运算符则停止栈顶元素的弹出,再将栈顶元素再次入栈,之后再把当前字符入栈,然后处理下一个字符。若栈顶元素是运算符且优先级高于或等于当前字符,则将栈顶元素加入后缀表达式,然后继续弹出栈顶元素;

按上述方法处理完所有字符后,若栈非空则将栈中剩余字符依次弹出并依次加入后缀表达式。最后得到的表达式就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的中缀表达式,不能是÷或者×及其他运算
//界限符只能是英文状态的左右括号即'('、')',操作数只能是整数
//本程序不会检查输入的中缀表达式是否正确,因此请您核验好自己的式子是否正确
#include<stdio.h>
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定要转换的中缀表达式的字符数在50个以内
typedef struct Linknode{ //定义链栈及结点
    char data; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool StackEmpty(LiStack S){ //判断栈空
    return S==NULL;
}
bool Push(LiStack &S,char x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL) //内存不足,创建失败
        return false;
    s->data=x;
    s->next=S; //将结点s作为链栈的栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,char &x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    x=S->data;
    Linknode *p=S;
    S=S->next;
    free(p);
    return true;
}
int main(){
    char temp,a[size],b[size]; //静态数组a、b分别存放要转换的中缀表达式和转换后的后缀表达式,字符变量temp存放弹出的栈顶元素
    scanf("%s",&a); //需要您输入中缀表达式
    LiStack S;//初始化一个栈,用于保存括号和暂时还不能确定运算顺序的运算符
    InitStack(S); //初始化链栈
    int i,j,length=strlen(a); //length为输入的中缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=j=0;i<length;i++){
        if(a[i]>=48 && a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i];
            if(a[i+1]=='+'||a[i+1]=='-'||a[i+1]=='*'||a[i+1]=='/') //若下一个字符是运算符,即+、-、*、/,则b加一个空格,以免不同的操作数混在一起
                b[j++]=' ';
        }
        else if(a[i]=='(')
            Push(S,a[i]); //若当前字符是左括号则直接入栈
        else if(a[i]==')'){ //若当前字符是右括号
            while(StackEmpty(S)==0){ //栈非空则不断弹出栈内字符并加入后缀表达式
                Pop(S,temp);
                if(temp=='(') //直到弹出左括号停止,注意这个(不加入后缀表达式
                    break;
                b[j++]=temp;
                b[j++]=' '; //加一个空格,从而将字符隔开
            }
        }
        else switch(a[i]){ //若当前字符是运算符
            case '*': case '/':{
                while(StackEmpty(S)==0){ //若栈非空,则弹出栈中优先级高于或等于当前运算符的所有运算符,并将这些运算符加入后缀表达式
                    Pop(S,temp);
                    if(temp=='/'||temp=='*'){
                        b[j++]=temp;
                        b[j++]=' '; //加一个空格,从而将字符隔开
                    }
                    else if(temp=='('||temp=='-'||temp=='+'){//若栈顶元素是左括号或者是优先级低于当前字符的运算符,则将栈顶元素入栈
                        Push(S,temp);
                        break;
                    }
                }
                Push(S,a[i]); //把当前字符入栈
                break;
            }
            case '-': case '+':{
                while(StackEmpty(S)==0){ //若栈非空,则弹出栈中优先级高于或等于当前运算符的所有运算符,并将这些运算符加入后缀表达式
                    Pop(S,temp);
                    if(temp=='('){//若栈顶元素是左括号,则将栈顶元素入栈
                        Push(S,temp);
                        break;
                    }
                    else if(temp=='/'||temp=='*'||temp=='-'||temp=='+'){
                        b[j++]=temp;
                        b[j++]=' '; //加一个空格,从而将字符隔开
                    }
                }
                Push(S,a[i]); //把当前字符入栈
                break;
            }
        }
    }
    while(StackEmpty(S)==0){ //栈非空时依次弹出栈顶元素并加入后缀表达式
        Pop(S,temp);
        b[j++]=temp;
        b[j++]=' '; //加一个空格,从而将字符隔开
    }
    printf("结果是:\n");
    for(i=0;i<j;i++) //j是数组中下一个可以插入元素的位置下标,因此b中存放字符的索引区间为[0,j-1]
        printf("%c",b[i]); //输出b中的元素
    printf("\n");
    return 0;
}

运行程序后需要输入待转换的中缀表达式,下面是三个输入输出示例:

输入输出
((15/(7-(1+1)))*3)-(2+(1+1))15 7 1 1 + - / 3 * 2 1 1 + + -
2+(3+4)*52 3 4 + 5 * +
16+2*30/416 2 30 * 4 / +

2.2 后缀表达式求值

2.2.1 手算

后缀表达式求值的手算步骤:从左往右扫描后缀表达式的每一个字符,每遇到一个运算符,就选择运算符左面距离最近的两个操作数执行对应运算,执行运算时注意两个操作数的左右顺序,得到计算结果后去掉刚刚的两个操作数,将新得到的计算结果放在刚刚的这个运算符的位置并代替之,继续从左到右扫描字符直到扫描完全部字符。扫描结束最后得到的就是最终结果。下面是一个示例:

求值前求值后
15 7 1 1 + - / 3 * 2 1 1 + + -5
2 3 4 + 5 * +37
16 2 30 * 4 / +31

2.2.2 机算

需要初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数。用栈实现后缀表达式求值的逻辑过程:

① 从左往右扫描后缀表达式的每一个字符,直到扫描完所有字符;

② 若扫描到的字符是操作数则压入栈,并回到①,否则执行③;

③ 若扫描到的字符是运算符,则连续弹出两个栈顶元素(注意:先出栈的是“右操作数”),然后对这两个操作数执行相应运算,然后再将运算结果入栈,回到①。

若被扫描的后缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的后缀表达式,不能是÷或者×及其他运算
//本程序不会检查输入的后缀表达式是否正确,因此请您核验好自己的式子是否正确
//操作数长度在10位以内且必须是整数
//请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开
#include<stdio.h>
#include<math.h> //pow的头文件,用于求乘方数
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定要求值的后缀表达式的字符数在50个以内
#define num_length 10 //假定后缀表达式中的每一个操作数最长为10位数
typedef struct Linknode{ //定义链栈及结点
    float data; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool Push(LiStack &S,float x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL) //内存不足,创建失败
        return false;
    s->data=x;
    s->next=S; //将结点s作为链栈的栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,float &x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    x=S->data;
    Linknode *p=S;
    S=S->next;
    free(p);
    return true;
}
int main(){
    char a[size]; //静态数组a存放要处理的后缀表达式
    float right,left,tmp=0; //right,left为弹出栈的元素,依次为右操作数和左操作数
    int b[num_length];//静态数组b存放操作数的各位数字如231的2、3、1
    scanf("%s",&a); //需要您输入后缀表达式
    LiStack S;//初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数
    InitStack(S); //初始化链栈
    int i,j=0,length=strlen(a); //length为输入的后缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=0;i<length;i++){
        if(a[i]>=48 && a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i]-48; //字符型的数字减去48就是数字本身,如ASCII码为51的字符为"3",而"3"-48=51-48=3就是数字3本身
            if(a[i+1]==','||a[i+1]=='+'||a[i+1]=='-'||a[i+1]=='*'||a[i+1]=='/'){ //若下一个字符是逗号或者运算符,则代表凑够一个操作数了
                int m=j;
                for(int k=0;k<m;k++) // 将分散的各位数字组成一个操作数
                    tmp+=b[k]*int(pow(10,--j));
                Push(S,tmp); //将当前操作数入栈
                tmp=0; //在遇到下一个操作数之前将tmp值归零
            }
        }
        else switch(a[i]){ //若当前字符是运算符
            case '*': case '/': case '-': case '+':{
                Pop(S,right); //先出栈的是“右操作数”
                Pop(S,left);  //后出栈的是“左操作数”
                if(a[i]=='+')
                    Push(S,left+right); //执行运算之后将结果入栈,下面三个也是这样
                else if(a[i]=='-')
                    Push(S,left-right);
                else if(a[i]=='*')
                    Push(S,left*right);
                else if(a[i]=='/')
                    Push(S,left/right);
            }
        }
    }
    Pop(S,tmp); //若被扫描的后缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果
    printf("结果是:%.2f\n",tmp); //输出结果
    return 0;
}

运行程序后需要输入待求值的后缀表达式,注意本程序只能处理有关运算符+、-、*、/的后缀表达式,不能是÷或者×及其他运算,本程序不会检查输入的后缀表达式是否正确,因此请您核验好自己的式子是否正确,操作数长度在10位以内且必须是整数,请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开。下面是三个输入输出示例:

输入输出
15,7,1,1,+,-,/,3,*,2,1,1,+,+,-5.00
2,3,4,+,5,*,+37.00
16,2,30,*,4,/,+31.00

2.3 后缀表达式转中缀表达式

2.3.1 手算

后缀表达式转换成中缀表达式的手算步骤:从左往右扫描后缀表达式的每一个字符,每遇到一个运算符,就选择运算符左面距离最近的两个操作数,将【左操作数 右操作数 运算符】变为(左操作数 运算符 右操作数)的形式。注意两个操作数的左右顺序,得到一个式子后去掉刚刚的两个操作数,将得到的式子放在刚刚的这个运算符的位置并代替之,继续从左到右扫描字符直到扫描完全部字符。扫描结束最后得到的式子就是最终结果。下面是一个示例:

转换前的后缀表达式转换后的中缀表达式
15 7 1 1 + - / 3 * 2 1 1 + + -(((15/(7-(1+1)))*3)-(2+(1+1)))
2 3 4 + 5 * +(2+((3+4)*5))
16 2 30 * 4 / +(16+((2*30)/4))

2.3.2 机算

需要初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数。用栈实现后缀表达式转中缀表达式的逻辑过程:

① 从左往右扫描下一个元素,直到处理完所有元素;

② 若扫描到操作数则压入栈,并回到①,否则执行③;

③ 若扫描到运算符,则弹出两个栈顶元素(注意:先出栈的是“右操作数”),构成(左操作数 运算符 右操作数),然后将其压回栈顶,回到①。

若被扫描的后缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的后缀表达式,不能是÷或者×及其他运算
//本程序不会检查输入的后缀表达式是否正确,因此请您核验好自己的式子是否正确
//操作数长度在10位以内且必须是整数
//请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开
#include<stdio.h>
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定输入的后缀表达式的字符数在50以内
#define num_length 10 //假定每一个操作数最长为10位数
typedef struct Linknode{ //定义链栈及结点
    char data[size]; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool Push(LiStack &S,char *x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL||strlen(x)>size) //内存不足或者长度超过size,则失败
        return false;
    int i;
    for(i=0;i<strlen(x);i++)
        s->data[i]=x[i];
    s->data[i]='\0';
    s->next=S; //将结点s作为栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,char *x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    Linknode *p=S; //p指向栈顶结点
    int i;
    for(i=0;p->data[i]!='\0';i++)
        x[i]=p->data[i];
    x[i]='\0';
    S=p->next;
    free(p);
    return true;
}
int main(){
    char a[size]; //静态数组a存放要处理的后缀表达式
    char right[size],left[size]; //right,left为弹出栈的元素,依次为右操作数和左操作数
    char b[num_length];//静态数组b存放操作数的各位数字如231的'2'、'3'、'1'
    scanf("%s",&a); //需要您输入后缀表达式
    LiStack S;//初始化一个栈,用于存放当前暂时还不能确定运算次序的字符串形式的操作数
    InitStack(S); //初始化链栈
    int i,j=0,length=strlen(a); //length为输入的后缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=0;i<length;i++){
        if(a[i]>=48&&a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i];
            if(a[i+1]==','||a[i+1]=='+'||a[i+1]=='-'||a[i+1]=='*'||a[i+1]=='/'){ //若下一个字符是逗号或者运算符,则代表凑够一个操作数了
                b[j]='\0';
                Push(S,b); //将当前操作数入栈
                j=0; //将j归零
            }
        }
        else switch(a[i]){
            case '*': case '/': case '-': case '+':{ //若当前字符是运算符
                Pop(S,right); //先出栈的是“右操作数”
                Pop(S,left);  //后出栈的是“左操作数”
                if(strlen(left)+strlen(right)+1+2>size) // 左右操作数长度之和再加上操作符、左右括号的长度过长,容不下
                    return false;
                char tmp[size]; //临时变量
                tmp[0]='('; //先存左括号
                int k;
                for(k=0;k<strlen(left);k++)
                    tmp[k+1]=left[k]; //存左操作数
                tmp[strlen(left)+1]=a[i]; //存运算符
                for(k=0;k<strlen(right);k++)
                    tmp[k+strlen(left)+2]=right[k]; //存右操作数
                tmp[strlen(left)+strlen(right)+2]=')'; //最后存右括号
                tmp[strlen(left)+strlen(right)+3]='\0';
                Push(S,tmp); //将结果入栈
            }
        }
    }
    char c[size];
    Pop(S,c); //若被扫描的后缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果
    printf("结果是:%s\n",c); //输出结果
    return 0;
}

运行程序后需要输入待转换的后缀表达式,注意本程序只能处理有关运算符+、-、*、/的后缀表达式,不能是÷或者×及其他运算,本程序不会检查输入的后缀表达式是否正确,因此请您核验好自己的式子是否正确,操作数长度在10位以内且必须是整数,请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开。下面是三个输入输出示例:

输入输出
15,7,1,1,+,-,/,3,*,2,1,1,+,+,-(((15/(7-(1+1)))*3)-(2+(1+1)))
2,3,4,+,5,*,+(2+((3+4)*5))
16,2,30,*,4,/,+(16+((2*30)/4))

3 前缀表达式相关考点

3.1 中缀表达式转前缀表达式

3.1.1 手算

中缀转前缀的手算步骤:

① 确定中缀表达式中各个运算符的运算顺序,但是有时候运算顺序不唯一,因此对应的前缀表达式也不唯一。为了保证手算和机算结果相同,且保证运算顺序唯一,请遵从“右优先”原则:只要右边的运算符能先计算,就优先算右边的。确定完运算符的运算顺序后,如果有界限符即括号,就可以去掉全部的括号了,或者说可以忽略括号的存在继续下面步骤;

② 选择下一个运算符,按照【运算符 左操作数 右操作数】的方式组合成一个新的式子,然后放在原来操作符的位置。如果还有运算符没被处理,就继续②,否则最后得到的式子就是最终结果。下面是一个示例:

转换前的中缀表达式转换后的前缀表达式
((15/(7-(1+1)))*3)-(2+(1+1))- * / 15 - 7 + 1 1 3 + 2 + 1 1
2+(3+4)*5+ 2 * + 3 4 5
16+2*30/4+ 16 * 2 / 30 4

3.1.2 机算

实现中缀表达式转前缀表达式的逻辑过程:初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从右到左扫描中缀表达式的各个字符,直到末尾。在扫描时可能遇到三种情况:

① 当前字符是数字:直接加入前缀表达式,然后处理下一个字符;

② 当前字符是括号,又分为两种情况:

  • 1)当前字符是右括号即)则直接将当前字符入栈,然后处理下一个字符;
  • 2)当前字符是左括号即(:若栈空则直接处理下一个字符。若栈非空则不断弹出栈顶元素——若弹出右括号)后则停止继续弹出(注意这个右括号)不加入前缀表达式)并直接处理下一个字符,否则将栈顶元素加入前缀表达式,然后继续弹出栈顶元素;

③ 当前字符是运算符,又分为两种情况:

  • 1)栈空则直接把当前字符入栈,然后处理下一个字符;
  • 2)栈非空则不断弹出栈顶元素:若栈顶元素是右括号)或优先级高于当前字符的运算符则停止栈顶元素的弹出,再将栈顶元素再次入栈,之后再把当前字符入栈,然后处理下一个字符。若栈顶元素是运算符且优先级低于或等于当前字符,则将栈顶元素加入前缀表达式,然后继续弹出栈顶元素;

按上述方法处理完所有字符后,若栈非空则将栈中剩余字符依次弹出并依次加入前缀表达式。将最后得到的前缀表达式倒置就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的中缀表达式,不能是÷或者×及其他运算
//界限符只能是英文状态的左右括号即'('、')',操作数只能是整数
//本程序不会检查输入的中缀表达式是否正确,因此请您核验好自己的式子是否正确
#include<stdio.h>
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定要转换的中缀表达式的字符数在50个以内
typedef struct Linknode{ //定义链栈及结点
    char data; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool StackEmpty(LiStack S){ //判断栈空
    return S==NULL;
}
bool Push(LiStack &S,char x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL) //内存不足,创建失败
        return false;
    s->data=x;
    s->next=S; //将结点s作为链栈的栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,char &x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    x=S->data;
    Linknode *p=S;
    S=S->next;
    free(p);
    return true;
}
int main(){
    char temp,a[size],b[size]; //静态数组a、b分别存放要转换的中缀表达式和转换后的前缀表达式,字符变量temp存放弹出的栈顶元素
    scanf("%s",&a); //需要您输入中缀表达式
    LiStack S;//初始化一个栈,用于保存括号和暂时还不能确定运算顺序的运算符
    InitStack(S); //初始化链栈
    int i,j,length=strlen(a); //length为输入的中缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=length-1,j=0;i>=0;i--){ //从右到左扫描中缀表达式的各个字符,直到末尾
        if(a[i]>=48 && a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i];
            if(a[i-1]=='+'||a[i-1]=='-'||a[i-1]=='*'||a[i-1]=='/') //若上一个字符是运算符,即+、-、*、/,则b加一个空格,以免不同的操作数混在一起
                b[j++]=' ';
        }
        else if(a[i]==')')
            Push(S,a[i]); //若当前字符是右括号则直接入栈
        else if(a[i]=='('){ //若当前字符是左括号
            while(StackEmpty(S)==0){ //栈非空则不断弹出栈内字符并加入前缀表达式
                Pop(S,temp);
                if(temp==')') //直到弹出右括号停止,注意这个)不加入前缀表达式
                    break;
                b[j++]=temp;
                b[j++]=' '; //加一个空格,从而将字符隔开
            }
        }
        else switch(a[i]){ //若当前字符是运算符
            case '*': case '/':{
                while(StackEmpty(S)==0){ //若栈非空,则弹出栈中优先级低于或等于当前运算符的所有运算符,并将这些运算符加入前缀表达式
                    Pop(S,temp);
                    if(temp=='+'||temp=='-'||temp=='/'||temp=='*'){
                        b[j++]=temp;
                        b[j++]=' '; //加一个空格,从而将字符隔开
                    }
                    else if(temp==')'){ //若栈顶元素是右括号或者是优先级高于当前字符的运算符,则将栈顶元素入栈
                        Push(S,temp);
                        break;
                    }
                }
                Push(S,a[i]); //把当前字符入栈
                break;
            }
            case '-': case '+':{
                while(StackEmpty(S)==0){ //若栈非空,则弹出栈中优先级低于或等于当前运算符的所有运算符,并将这些运算符加入前缀表达式
                    Pop(S,temp);
                    if(temp==')'||temp=='*'||temp=='/'){ //若栈顶元素是右括号或者是优先级高于当前字符的运算符,则将栈顶元素入栈
                        Push(S,temp);
                        break;
                    }
                    else if(temp=='+'||temp=='-'){
                        b[j++]=temp;
                        b[j++]=' '; //加一个空格,从而将字符隔开
                    }
                }
                Push(S,a[i]); //把当前字符入栈
                break;
            }
        }
    }
    while(StackEmpty(S)==0){ //栈非空时依次弹出栈顶元素并加入前缀表达式
        Pop(S,temp);
        b[j++]=temp;
        b[j++]=' '; //加一个空格,从而将字符隔开
    }
    printf("结果是:\n");
    for(i=j-1;i>=0;i--) //b中存放字符的索引区间为[0,j-1]
        printf("%c",b[i]); //倒序输出b中的元素
    printf("\n");
    return 0;
}

运行程序后需要输入待转换的中缀表达式,下面是三个输入输出示例:

输入输出
(((15/(7-(1+1)))*3)-(2+(1+1)))- * /15 -7 +1 1 3 +2 +1 1
(2+((3+4)*5))+2 * +3 4 5
(16+(2*(30/4)))+16 *2 /30 4

3.2 前缀表达式求值

3.2.1 手算

前缀表达式求值的手算步骤:从右往左扫描前缀表达式的每一个字符,每遇到一个运算符,就选择运算符右面距离最近的两个操作数执行对应运算,执行运算时注意两个操作数的左右顺序,得到计算结果后去掉刚刚的两个操作数,将新得到的计算结果放在刚刚的这个运算符的位置并代替之,继续从右到左扫描字符直到扫描完全部字符。扫描结束最后得到的就是最终结果。下面是一个示例:

求值前求值后
- * / 15 - 7 + 1 1 3 + 2 + 1 15
+ 2 * + 3 4 537
+ 16 * 2 / 30 431

3.2.2 机算

需要初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数。用栈实现前缀表达式求值的逻辑过程:

① 从右往左扫描前缀表达式的每一个字符,直到扫描完所有字符;

② 若扫描到的字符是操作数则压入栈,并回到①,否则执行③;

③ 若扫描到的字符是运算符,则连续弹出两个栈顶元素(注意:先出栈的是“左操作数”),然后对这两个操作数执行相应运算,然后再将运算结果入栈,回到①。

若被扫描的前缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的前缀表达式,不能是÷或者×及其他运算
//本程序不会检查输入的前缀表达式是否正确,因此请您核验好自己的式子是否正确
//操作数长度在10位以内且必须是整数
//请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开
#include<stdio.h>
#include<math.h> //pow的头文件,用于求乘方数
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定要求值的前缀表达式的字符数在50个以内
#define num_length 10 //假定前缀表达式中的每一个操作数最长为10位数
typedef struct Linknode{ //定义链栈及结点
    float data; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool Push(LiStack &S,float x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL) //内存不足,创建失败
        return false;
    s->data=x;
    s->next=S; //将结点s作为链栈的栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,float &x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    x=S->data;
    Linknode *p=S;
    S=S->next;
    free(p);
    return true;
}
int main(){
    char a[size]; //静态数组a存放要处理的前缀表达式
    float right,left,tmp=0; //right,left为弹出栈的元素,依次为右操作数和左操作数
    int b[num_length];//静态数组b存放操作数的各位数字如231的2、3、1
    scanf("%s",&a); //需要您输入前缀表达式
    LiStack S;//初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数
    InitStack(S); //初始化链栈
    int i,j=0,length=strlen(a); //length为输入的前缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=length-1;i>=0;i--){ //从右到左扫描
        if(a[i]>=48 && a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i]-48; //字符型的数字减去48就是数字本身,如ASCII码为51的字符为"3",而"3"-48=51-48=3就是数字3本身
            if(a[i-1]==','||a[i-1]=='+'||a[i-1]=='-'||a[i-1]=='*'||a[i-1]=='/'){ //若上一个字符是逗号或者运算符,则代表凑够一个操作数了
                int m=j;
                for(int k=0;k<m;k++) // 将分散的各位数字组成一个操作数
                    tmp+=b[k]*int(pow(10,k));
                Push(S,tmp); //将当前操作数入栈
                tmp=0; //在遇到下一个操作数之前将tmp值归零
                j=0;
            }
        }
        else switch(a[i]){ //若当前字符是运算符
            case '*': case '/': case '-': case '+':{
                Pop(S,left);  //先出栈的是“左操作数”
                Pop(S,right); //后出栈的是“右操作数”
                if(a[i]=='+')
                    Push(S,left+right); //执行运算之后将结果入栈,下面三个也是这样
                else if(a[i]=='-')
                    Push(S,left-right);
                else if(a[i]=='*')
                    Push(S,left*right);
                else if(a[i]=='/')
                    Push(S,left/right);
            }
        }
    }
    Pop(S,tmp); //若被扫描的前缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果
    printf("结果是:%.2f\n",tmp); //输出结果
    return 0;
}

运行程序后需要输入待求值的前缀表达式,注意本程序只能处理有关运算符+、-、*、/的前缀表达式,不能是÷或者×及其他运算,本程序不会检查输入的前缀表达式是否正确,因此请您核验好自己的式子是否正确,操作数长度在10位以内且必须是整数,请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开。下面是三个输入输出示例:

输入输出
+,16,*,2,/,30,431.00
+,2,*,+,3,4,537.00
-,*,/,15,-,7,+,1,1,3,+,2,+,1,15.00

3.3 前缀表达式转中缀表达式

3.3.1 手算

前缀表达式转换成中缀表达式的手算步骤:从右往左扫描前缀表达式的每一个字符,每遇到一个运算符,就选择运算符右面距离最近的两个操作数,将【运算符 左操作数 右操作数】变为(左操作数 运算符 右操作数)的形式。注意两个操作数的左右顺序,得到一个式子后去掉刚刚的两个操作数,将得到的式子放在刚刚的这个运算符的位置并代替之,继续从右到左扫描字符直到扫描完全部字符。扫描结束最后得到的式子就是最终结果。下面是一个示例:

转换前的前缀表达式转换后的中缀表达式
- * / 15 - 7 + 1 1 3 + 2 + 1 1(((15/(7-(1+1)))*3)-(2+(1+1)))
+ 16 * 2 / 30 4(16+(2*(30/4)))
+ 2 * + 3 4 5(2+((3+4)*5))

3.3.2 机算

需要初始化一个栈,用于存放当前暂时还不能确定运算次序的操作数。用栈实现前缀表达式转中缀表达式的逻辑过程:

① 从右往左扫描下一个元素,直到处理完所有元素;

② 若扫描到操作数则压入栈,并回到①,否则执行③;

③ 若扫描到运算符,则弹出两个栈顶元素(注意:先出栈的是“左操作数”),构成(左操作数 运算符 右操作数),然后将其压回栈顶,回到①。

若被扫描的前缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果。示例代码如下:

//本程序只能处理有关运算符+、-、*、/的前缀表达式,不能是÷或者×及其他运算
//本程序不会检查输入的前缀表达式是否正确,因此请您核验好自己的式子是否正确
//操作数长度在10位以内且必须是整数
//请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开
#include<stdio.h>
#include<string.h> //strlen的头文件,用于判断字符串长度
#include<stdlib.h> //malloc、free的头文件
#define size 50//假定输入的前缀表达式的字符数在50以内
#define num_length 10 //假定每一个操作数最长为10位数
typedef struct Linknode{ //定义链栈及结点
    char data[size]; //数据域
    struct Linknode *next; //指针域
}*LiStack;
bool InitStack(LiStack &S){ //链栈的初始化,不带头结点
    S=NULL; //刚开始没有结点
    return true;
}
bool Push(LiStack &S,char *x){ //将元素x入栈
    Linknode *s=(Linknode *)malloc(sizeof(Linknode)); //创建新结点
    if(s==NULL||strlen(x)>size) //内存不足或者长度超过size,则失败
        return false;
    int i;
    for(i=0;i<strlen(x);i++)
        s->data[i]=x[i];
    s->data[i]='\0';
    s->next=S; //将结点s作为栈顶结点
    S=s; //栈顶指针S指向结点s
    return true;
}
bool Pop(LiStack &S,char *x){ //栈顶元素出栈,将值赋给x
    if(S==NULL)
        return false; //栈空则返回NULL
    Linknode *p=S; //p指向栈顶结点
    int i;
    for(i=0;p->data[i]!='\0';i++)
        x[i]=p->data[i];
    x[i]='\0';
    S=p->next;
    free(p);
    return true;
}
int main(){
    char a[size]; //静态数组a存放要处理的前缀表达式
    char right[size],left[size],tmp[size]; //right,left为弹出栈的元素,依次为右操作数和左操作数;tmp是临时变量,用于存储某些字符串
    char b[num_length];//静态数组b存放操作数的各位数字如231的'2'、'3'、'1'
    scanf("%s",&a); //需要您输入前缀表达式
    LiStack S;//初始化一个栈,用于存放当前暂时还不能确定运算次序的字符串形式的操作数
    InitStack(S); //初始化链栈
    int i,j=0,length=strlen(a); //length为输入的前缀表达式的总长度,i、j分别为静态数组a、b的索引下标
    for(i=length-1;i>=0;i--){ //从右到左扫描
        if(a[i]>=48&&a[i]<=57){ //若当前字符是数字,字符0-9的ACSII码范围是[48,57]
            b[j++]=a[i];
            if(a[i-1]==','||a[i-1]=='+'||a[i-1]=='-'||a[i-1]=='*'||a[i-1]=='/'){ //若下一个字符是逗号或者运算符,则代表凑够一个操作数了
                int m;
                for(m=0;m<j;m++)
                    tmp[m]=b[j-1-m]; //因为是从右到左扫描的,因此需要将数字颠倒一下
                tmp[m]='\0';
                Push(S,tmp); //将当前操作数入栈
                j=0; //将j归零
            }
        }
        else switch(a[i]){
            case '*': case '/': case '-': case '+':{ //若当前字符是运算符
                Pop(S,left);  //先出栈的是“左操作数”
                Pop(S,right); //后出栈的是“右操作数”
                if(strlen(left)+strlen(right)+1+2>size) // 左右操作数长度之和再加上操作符、左右括号的长度过长,容不下
                    return false;
                tmp[0]='('; //先存左括号
                int k;
                for(k=0;k<strlen(left);k++)
                    tmp[k+1]=left[k]; //存左操作数
                tmp[strlen(left)+1]=a[i]; //存运算符
                for(k=0;k<strlen(right);k++)
                    tmp[k+strlen(left)+2]=right[k]; //存右操作数
                tmp[strlen(left)+strlen(right)+2]=')'; //最后存右括号
                tmp[strlen(left)+strlen(right)+3]='\0';
                Push(S,tmp); //将结果入栈
            }
        }
    }
    Pop(S,tmp); //若被扫描的前缀表达式是合法的,则最后栈中只会留下一个元素,就是最终结果
    printf("结果是:%s\n",tmp); //输出结果
    return 0;
}

运行程序后需要输入待转换的前缀表达式,注意本程序只能处理有关运算符+、-、*、/的前缀表达式,不能是÷或者×及其他运算,本程序不会检查输入的前缀表达式是否正确,因此请您核验好自己的式子是否正确,操作数长度在10位以内且必须是整数,请将不同的操作数之间、运算符之间、运算符与操作数之间用英文状态的逗号即","隔开。下面是三个输入输出示例:

输入输出
+,16,*,2,/,30,4(16+(2*(30/4)))
+,2,*,+,3,4,5(2+((3+4)*5))
-,*,/,15,-,7,+,1,1,3,+,2,+,1,1(((15/(7-(1+1)))*3)-(2+(1+1)))

END

静态链表示意图:2.2 顺序表与链表的比较存储密度比较:顺序表:只存储数据元素、预分配存储空间链表:指针的结构性开销、链表中的元素个数没有限制按位查找:顺序表:O(1),随机存取链表:O(n),顺序存取插入和删除:顺序表:O(n),平均移动表长一半的元素链表:不用移动元素,合适位置的指针——O(1)时间复杂度:顺序表:若线性表频繁查找却很少进行插入和删除操作链表:若线性表需频繁插入和删除时空间复杂度:顺序表:知道线性表的大致长度,空间效率会更高链表:若线性表中元素个数变化较大或者未知2.3 栈        定义:限定仅在一端(栈顶)进行插入和删除操作的线性表,后进先出。栈示意图:        时间复杂度(插入与删除):顺序栈与链栈均为O(1)        空间复杂度:链栈多一个指针域,结构性开销较大,使用过程中元素个数变化较大时,用链栈;反之顺序栈。        出栈元素不同排列的个数:   (卡特兰数)        共享栈: 两个栈共享一片内存空间, 两个栈从两边往中间增长。卡特兰数的应用:存储结构:顺序栈初始化:top=-1链栈初始化:top=NULL栈的应用:        1) 括号匹配        2) 递归        3) 中缀表达式后缀表达式        4) 中缀表达式:设两个栈(数据栈和运符栈),根据运符栈的优先级进行运。2.4 队列        定义: 只允许在一端插入, 在另一端删除。具有先进先出的特点。队列示意图:        时间复杂度:均为O(1)        空间复杂度:链队列多一个指针域,结构性开销较大;循环队列存在浪费空间和溢出问题。使用过程中元素个数变化较大时,用链队列;反之循环队列。        双端队列: 只允许从两端插入、两端删除的线性表。双端队列示意图: 存储结构:        链队列:队头指针指向队头元素的前一个位置,队尾指针指向队尾元素,先进先出。        循环队列:                1)队空:front=rear                2)队满:(rear+1)%QueueSize=front                3)队列元素个数:(队尾-队头+队长)%队长==(rear-front+QueueSize)%QueueSize队列的应用:        1) 树的层次遍历        2) 图的广度优先遍历2.4 数组与特殊矩阵一维数组的存储结构:二维数组的存储结构: 对称矩阵的压缩(行优先):下三角矩阵的压缩(行优先):  上三角(行优先):三对角矩阵的压缩(行优先):稀疏矩阵压缩:十字链表法压缩稀疏矩阵:2.5 串        串,即字符串(String)是由零个或多个字符组成的有限序列。串是一种特殊的线性表,数据元素之间呈线性关系。字符串模式匹配:        1)朴素模式匹配法        2)KMPKMP的next数组示意图:求next[2] :求next[3]: 求next[4]: 求next[5]: C语言求KMP的next数组代码示例:void Createnext(char *sub, int *next){ assert(sub != NULL && next != NULL); int j = 2; //模式串的next指针 int k = 0; //next数组的回溯值,初始化为next[1]=0 int lenSub = strlen(sub); assert(lenSub != 0); next[0] = -1; next[1] = 0; while (j < lenSub){ if (sub[j-1] == sub[k]){ next[j] = ++k; j++; } else{ k = next[k]; if (k == -1){ k = 0; next[j] = k; j++; } } }}求nextValue:void nextValue(char *sub, int *next) { int lenSub = strlen(sub); for(int j=2;j<lensub; j++){ if(sub[j]==sub[next[j]]) next[j]=next[next[j]] }}备注:         1) 实现next有多种不同方式, 对应不同的next数组使用        2) 根据实现方式不同, next数组整体+1不影响KMP法。第三章 树和二叉树3.1 树和森林        定义(树):n(n≥0)个结点(数据元素)的有限集合,当 n=0 时,称为空树。3.1.1 树的基本术语        结点的度:结点所拥有的子树的个数。        叶子结点:度为 0 的结点,也称为终端结点。        分支结点:度不为 0 的结点,也称为非终端结点。        孩子:树中某结点子树的根结点称为这个结点的孩子结点。        双亲:这个结点称为它孩子结点的双亲结点。        兄弟:具有同一个双亲的孩子结点互称为兄弟。        路径:结点序列 n1, n2, …, nk 称为一条由 n1 至 nk 的路径,当且仅当满足结点 ni 是 ni+1 的双亲(1<=i<k)的关系。        路径长度:路径上经过的边的个数。        祖先、子孙:如果有一条路径从结点 x 到结点 y,则 x 称为 y 的祖先,而 y 称为 x 的子孙。        结点所在层数:根结点的层数为 1;对其余结点,若某结点在第 k 层,则其孩子结点在第 k+1 层。        树的深度(高度):树中所有结点的最大层数。        树的宽度:树中每一层结点个数的最大值。        树的度:树中各结点度的最大值。        树的路径长度:  从根到每个结点的路径长度总和        备注: 在线性结构中,逻辑关系表现为前驱——后继,一对一; 在树结构中,逻辑关系表现为双亲——孩子,一对多。        森林: 森林是m(m≥0)棵互不相交的树的集合, m可为0, 即空森林。3.1.2 树的性质        结点数=总度数+1        度为m的树第 i 层至多有 个结点(i≥1)        高度为h的m叉树至多有 个结点        具有n个结点的m叉树的最小高度为 最小高度推理过程图:3.1.3 树与森林的遍历树的遍历:先根遍历(先根后子树)后根遍历(先子树后根)层序遍历森林的遍历:前序遍历(先根, 后子树)中序遍历(先子树后根, 其实就是后序遍历树)区别与联系:         1) 树的前序遍历等价于其树转化二叉树的前序遍历!        2) 树的后序遍历等价于其树转化二叉树的中序遍历!3.1.4 树的存储结构双亲表示法图:孩子表示法图:孩子兄弟表示法图(树/森林转化为二叉树):3.1.5 树转二叉树在树转为二叉树后, 有以下结论:        1) 树的叶子结点数量 = 二叉树左空指针数量(形象理解为树越宽, 兄弟越多, 越是向右长)        2) 树的非叶子结点数量 = 二叉树右空指针-1(非叶子必有儿子, 右指针由儿子提供, -1是根节点多了一个右空指针)3.2 二叉树3.2.1 二叉树的性质斜树:左斜树:所有结点都只有左子树的二叉树右斜树:所有结点都只有右子树的二叉树        满二叉树:所有分支结点都存在左子树和右子树,且所有叶子都在同一层上的二叉树        完全二叉树:在满二叉树中,从最后一个结点开始,连续去掉任意个结点得到的二叉树完全二叉树特点:叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左面完全二叉树中如果有度为 1 的结点,只可能有一个,且该结点只有左孩子深度为 k 的完全二叉树在 k-1 层上一定是满二叉树在同样结点个数的二叉树中,完全二叉树的深度最小        性质:在二叉树中,如果叶子结点数为 n0,度为 2 的结点数为 n2,则有: n0=n2+1证明: 设 n 为二叉树的结点总数,n1 为二叉树中度为 1 的结点数,则有: n=n0+n1+n2        在二叉树中,除了根结点外,其余结点都有唯一的一个分枝进入,一个度为 1 的结点射出一个分枝,一个度为 2 的结点射出两个分枝,所以有:n=n1+2n2+1        性质:二叉树的第 i 层上最多有个结点(i≥1)        性质:一棵深度为 k 的二叉树中,最多有 个结点        性质:具有 n 个结点的完全二叉树的深度为 向下取整+1 (或向上取整)证明:设具有 n 个结点的完全二叉树的深度为 k,则:≤n  <对不等式取对数,有:k-1 ≤ <k即:<k ≤ +1由于 k 是整数,故必有k= +1         性质:对一棵具有 n 个结点的完全二叉树中从 1 开始按层序编号,对于任意的序号为 i(1≤i≤n)的结点(简称结点 i),有:如果 i>1,则结点 i 的双亲结点的序号为 i/2,否则结点 i 无双亲结点如果 2i≤n,则结点 i 的左孩子的序号为 2i,否则结点 i 无左孩子如果 2i+1≤n,则结点 i 的右孩子的序号为2i+1,否则结点 i 无右孩子        性质:若已知一棵二叉树的前序序列和中序序列,或者中序序列和后序序列,能唯一确定一颗二叉树。 3.2.2 二叉树的遍历        从根结点出发,按照某种次序访问树中所有结点,并且每个结点仅被访问一次。前序遍历(深度优先遍历)中序遍历后序遍历层序遍历(广度优先遍历)3.2.3 二叉树的存储链式存储图:顺序存储图:3.2.4 线索二叉树        利用二叉树中n+1个空指针, 将指针指向结点的前驱和后继。线索二叉树是加上线索的链表结构,  是一种物理结构存储结构:示例图:三种线索化的对比图: 各自特点:3.3 哈夫曼树和哈夫曼编码        带权路径长度(WPL):从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和        最优二叉树(哈夫曼树):给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树特点:权值越大的叶子结点越靠近根结点只有度为 0 和度为 2 的结点,不存在度为 1 的结点构造过程中共新建了n-1个结点, 因此总结点数为2n-1        前缀编码:在一组编码中,任一编码都不是其它任何编码的前缀, 前缀编码保证了在解码时不会有多种可能。         度为m的哈夫曼树: 通过只有度为m和度为0求解非叶子结点 3.4 并查集        存储结构: 双亲表示法        实现功能: 并查(并两个集合, 查根结点)        优化: 小树并到大树, "高树变矮树"(压缩路径)第四章 图        定义:顶点集V和边集E组成,记为G = (V, E)        注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集, 边集E可以为空        子图:若图G=(V, E),G'=(V', E'),如果V' 属于 V 且E' 属于E,则称图G'是G的子图4.1 图的基本概念图的分类:        无向边:表示为(vi,vj),顶点vi和vj之间的边没有方向        有向边(弧):表示为<vi,vj>,从vi 到vj 的边有方向, vi为弧尾, vj为弧头        稠密图:边数很多的图        稀疏图:边数很少的图        无向完全图:无向图中,任意两个顶点之间都存在边        有向完全图:有向图中,任意两个顶点之间都存在方向相反的两条弧度、入度和出度:        顶点的度:在无向图中,顶点 v 的度是指依附于该顶点的边数,通常记为TD (v)        顶点的入度:在有向图中,顶点 v 的入度是指以该顶点为弧头的弧的数目,记为ID (v);        顶点的出度:在有向图中,顶点 v 的出度是指以该顶点为弧尾的弧的数目,记为OD (v);        握定理: 路径:         回路(环):第一个顶点和最后一个顶点相同的路径        简单路径:序列中顶点不重复出现的路径        简单回路(简单环):除第一个和最后一个顶点外,其余顶点不重复出现的回路。        路径长度:非带权图——路径上边的个数        路径长度:带权图——路径上边的权值之和         极大连通子图: 连通的情况下, 包含尽可能多的边和顶点, 也称连通分量        极小连通子图: 删除该子图中任何一条b边, 子图不再连通, 如生成树无向连通图:        连通顶点:在无向图中,如果顶点vi和顶点vj(i≠j)之间有路径,则称顶点vi和vj是连通的        连通图:在无向图中,如果任意两个顶点都是连通的,则称该无向图是连通图        连通分量:非连通图的极大连通子图、连通分量是对无向图的一种划分连通分量示意图:有向强连通图、强连通分量:        强连通顶点:在有向图中,如果从顶点vi到顶点vj和从顶点vj到顶点vi均有路径,则称顶点vi和vj是强连通的        强连通图:在有向图中,如果任意两个顶点都是强连通的,则称该有向图是强连通图        强连通分量:非强连通图的极大连通子图强连通分量示意图: 子图与生成子图:常考点无向图:        所有顶点的度之和=2|E|        若G是连通图,则最少有 n-1 条边(树),若 |E|>n-1,则一定有回路        若G是非连通图,则最多可能有 条边 (n-1个完全图+1个孤点)        无向完全图共有条边有向图:        所有顶点的出度之和=入度之和=|E|        所有顶点的度之和=2|E|        若G是强连通图,则最少有 n 条边(形成回路)        有向完全图共有条边图的遍历:从图中某一顶点出发访问图中所有顶,并且每个结点仅被访问一次。深度优先遍历序列(dfs)广度优先遍历序列(bfs)    备注:  调⽤BFS/DFS函数的次数 = 连通分量数4.2 图的存储结构 邻接矩阵:一维数组:存储图中顶点的信息二维数组(邻接矩阵):存储图中各顶点之间的邻接关系特点:一个图能唯一确定一个邻接矩阵,存储稀疏图时,浪费空间。空间复杂度为: O()。示意图:性质 (行*列) :邻接表:顶点表:所有边表的头指针和存储顶点信息的一维数组边表(邻接表):顶点 v 的所有邻接点链成的单链表示意图:特点:空间复杂度为:O(n+e), 适合存储稀疏图。两者区别:十字链表法图:备注:         1) 十字链表只用于存储有向图        2) 顺着绿色线路找: 找到指定顶点的所有出边        3) 顺着橙色线路找: 找到指定顶点的所有入边        4) 空间复杂度:O(|V|+|E|)邻接多重表图:备注:        1) 邻接多重表只适用于存储无向图        2) 空间复杂度:O(|V|+|E|)四者区别: 4.3 最小生成树        生成树:连通图的生成树是包含全部顶点的一个极小连通子图, 可用DFS和BFS生成。        生成树的代价:在无向连通网中,生成树上各边的权值之和        最小生成树:在无向连通网中,代价最小的生成树        性质: 各边权值互不相等时, 最小生成树是唯一的。边数为顶点数-1生成森林示意图:4.3.1 Prim法        从某⼀个顶点开始构建⽣成树;每次将代价最⼩的新顶点纳⼊⽣成树,直到所有顶点都纳⼊为⽌。基于贪心法的策略。        时间复杂度:O(|V|2) 适合⽤于边稠密图。4.3.2 Kruskal 法(克鲁斯卡尔)        每次选择⼀条权值最⼩的边,使这条边的两头连通(原本已经连通的就不选), 直到所有结点都连通。基于贪心法的策略。        时间复杂度:O( |E|log2|E| ) 适合⽤于边稀疏图。4.4 最短路径        非带权图: 边数最少的路径(广度优先遍历)        带权图: 边上的权值之和最少的路径4.4.1 Dijkstra法        时间复杂度:O(n2)        备注: Dijkstra 法不适⽤于有负权值的带权图4.4.2 Floyd法核心代码:        时间复杂度:O(n3)        备注: 可以⽤于负权值带权图, 不能解决带有“负权回路”的图三者区别:4.5 有向⽆环图(DAG)描述表达式 (简化前) :描述表达式 (简化后) :4.6 拓扑排序        AOV⽹(Activity On Vertex NetWork,⽤顶点表示活动的⽹): ⽤DAG图(有向⽆环图)表示⼀个⼯程。顶点表示活动,有向边表示活动Vi必须先于活动Vj进⾏如图:拓扑排序的实现:        ① 从AOV⽹中选择⼀个没有前驱(⼊度为0)的顶点并输出。        ② 从⽹中删除该顶点和所有以它为起点的有向边。        ③ 重复①和②直到当前的AOV⽹为空或当前⽹中不存在⽆前驱的顶点为⽌。逆拓扑排序(可用DFS实现):        ① 从AOV⽹中选择⼀个没有后继(出度为0)的顶点并输出。        ② 从⽹中删除该顶点和所有以它为终点的有向边。        ③ 重复①和②直到当前的AOV⽹为空备注: 上三角(对角线为0)矩阵, 必不存在环, 拓扑序列必存在, 但拓扑不唯一。(拓扑唯一, 图不唯一)4.7 关键路径        在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为⽤边表示活动的⽹络,简称AOE⽹示意图:        关键活动: 从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径⻓度的路径称为 关键路径,⽽把关键路径上的活动称为关键活动。        事件vk的最早发⽣时间: 决定了所有从vk开始的活动能够开⼯的最早时间。        活动ai的最早开始时间: 指该活动弧的起点所表⽰的事件的最早发⽣时间。        事件vk的最迟发⽣时间: 它是指在不推迟整个⼯程完成的前提下,该事件最迟必须发⽣的时间。        活动ai的最迟开始时间: 它是指该活动弧的终点所表示事件的最迟发⽣时间与该活动所需时间之差。        活动ai的时间余量:表⽰在不增加完成整个⼯程所需总时间的情况下,活动ai可以拖延的时间。d(k)=0的活动就是关键活动, 由关键活动可得关键路径。示例图:第五章 查找        静态查找 :不涉及插入和删除操作的查找        动态查找 :涉及插入和删除操作的查找        查找⻓度: 在查找运中,需要对⽐关键字的次数称为查找⻓度        平均查找长度:衡量查找法的效率公式:5.1 顺序查找(线性查找):        顺序查找适合于存储结构为顺序存储或链接存储的线性表。  基本思想:从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。        时间复杂度: O(n)。有序顺序查找的ASL图:        无序查找失败时的平均查找长度:  n+1次 (带哨兵的情况)5. 2 折半查找:        元素必须是有序的,顺序存储结构。判定树是一颗平衡二叉树, 树高 (由n=-1得来)。        基本思想:用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表。        公式:mid=(low+high)/2, 即mid=low+1/2*(high-low);           1)相等,mid位置的元素即为所求           2)>,low=mid+1;                3)<,high=mid-1。        时间复杂度: 二叉判定树的构造:         备注:对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,不建议使用。失败结点的ASL不是方形结点, 而是其父节点。5.3 分块查找        分块查找,⼜称索引顺序查找。        基本思想:将查找表分为若干子块, 块内的元素可以无序, 块间的元素是有序的, 即前一个快的最大元素小于后一个块的最大元素。再建立索引表, 索引表中的每个元素含有各块的最大关键字和第一个元素的地址。索引表按关键字有序排列。示意图:备注:         1) 设索引查找和块内查找的平均查找⻓度分别为LI、LS,则分块查找的平均查找⻓度为ASL=LI + LS        2) 将长度为n的查找表均匀分为b块, 每块s个记录, 在等概率情况下, 若在块内和索引表中均采用顺序查找, 则平均查找长度为:5.4 二叉排序树        又称二叉查找树(BST,Binary Search Tree), 是具有如下性质的二叉树:左子树结点值 < 根结点值 < 右子树结点值        二叉排序树的插入:  新插入的结点 一定是叶子。二叉排序树的删除        1) 情况一, 删除叶结点, 直接删除        2) 情况二, 待删除结点只有一颗子树, 让子树代替待删除结点        3) 情况三, 待删除结点有左, 右子树, 则令待删除的直接前驱(或直接后继(中序遍历))代替待删除结点。示意图: (30为直接前驱, 60为直接后继)平均查找效率: 主要取决于树的高度。时间复杂度: 5.5 平衡二叉树        简称平衡树(AVL树), 树上任一结点的左子树和右子树的 高度之差不超过1。 结点的平衡因子=左子树高-右子树高。平衡二叉树的插: LL型:RR型:RL型:LR型:        平衡二叉树的删除: 同上考点:        假设以表示深度为h的平衡树中含有的最少结点数。 则有 = 0, = 1, = 2,并且有=  +          时间复杂度: 5.6 红黑树        与AVL树相比, 插入/删除 很多时候不会破坏“红黑”特性,无需频繁调整树的形态。因为AVL是高度差严格要求不超过1, 红黑树高度差不超过2倍, 较为宽泛。定义:        ①每个结点或是红色,或是黑色的        ②根节点是黑色的        ③叶结点(外部结点、NULL结点、失败结点)均是黑色的        ④不存在两个相邻的红结点(即红结点的父节点和孩子结点均是黑色)        ⑤对每个结点,从该节点到任一叶结点的简单路径上,所含黑结点的数目相同        口诀: 左根右,根叶黑 不红红,黑路同示例图:性质:        性质1:从根节点到叶结点的最长路径不大于最短路径的2倍 (红结点只能穿插 在各个黑结点中间)        性质2:有n个内部节点的红黑树高度          结论: 若根节点黑高为h,内部结点数(关键字)最多有 , 最少有示例图:红黑树的插入操作:红黑树的插入示例图:         红黑树的删除: 和“二叉排序树的删除”一样! 具体还是了吧, 放过自己。。。        时间复杂度: 5.7 B树        B树,⼜称多路平衡查找树,B树中所被允许的孩⼦个数的最⼤值称为B树的阶,通常⽤m表示。 m阶B树的特性:        1)树中每个结点⾄多有m棵⼦树,即⾄多含有m-1个关键字。        2)若根结点不是终端结点,则⾄少有两棵⼦树。        3)除根结点外的所有⾮叶结点⾄少有 棵⼦树,即⾄少含有个关键字。         4) 所有的叶结点都出现在同⼀层次上,并且不带信息, ( 指向这些结点的指针为空 ) 。        5) 最小高度:    (n为关键字, 注意区分结点)        6) 最大高度:         7) 所有⼦树⾼度要相同        8) 叶结点对应查找失败的情况, 即n个关键字有n+1个叶子结点示例图: B树的插入(5阶为例):B树的删除:        1) 若被删除关键字在终端节点,则直接删除该关键字 (要注意节点关键字个数是否低于下限 ⌈m/2⌉ − 1)        2) 若被删除关键字在⾮终端节点,则⽤直接前驱或直接后继来替代被删除的关键字 删除77:删除38:删除90:        3) 若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结 点的关键字个数均=⌈m/2⌉ − 1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进⾏合并 删除49: 5.8 B+树⼀棵m阶的B+树需满⾜下列条件        1)每个分⽀结点最多有m棵⼦树(孩⼦结点)。        2)⾮叶根结点⾄少有两棵⼦树,其他每个分⽀结点⾄少有 ⌈m/2⌉ 棵⼦树。        3)结点的⼦树个数与关键字个数相等。        4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按⼤⼩顺序排列,并且相邻叶结点按⼤⼩顺序相互链接起来        5)所有分⽀结点中仅包含它的各个⼦结点中关键字的最⼤值及指向其⼦结点的指针。所有⾮叶结点仅起索引作⽤,        6) 所有⼦树⾼度要相同B+树示例图:B+树与B树的对比图:5.9 哈希表(Hash)        根据数据元素的关键字 计算出它在散列表中的存储地址。        哈希函数: 建⽴了“关键字”→“存储地址”的映射关系。        冲突(碰撞):在散列表中插⼊⼀个数据元素时,需要根据关键字的值确定其存储地址,若 该地址已经存储了其他元素,则称这种情况为“冲突(碰撞)”        同义词:若不同的关键字通过散列函数映射到同⼀个存储地址,则称它们为“同义词”        复杂度分析:对于无冲突的Hash表而言,查找复杂度为O(1) 5.9.1 构造哈希函数        1) 除留余数法 —— H(key) = key % p, 取⼀个不⼤于m但最接近或等于m的质数p        适⽤场景:较为通⽤,只要关键字是整数即可        2) 直接定址法 —— H(key) = key 或 H(key) = a*key + b        适⽤场景:关键字分布基本连续        3) 数字分析法 —— 选取数码分布较为均匀的若⼲位作为散列地        适⽤场景:关键字集合已知,且关键字的某⼏个数码位分布均匀        4) 平⽅取中法(二次探测法)——取关键字的平⽅值的中间⼏位作为散列地址        适⽤场景:关键字的每位取值都不够均匀。5.9.2 处理冲突拉链法示意图:开放定址法:        1) 线性探测法        2) 平⽅探测法        3) 双散列法        4) 伪随机序列法示意图:        删除操作: 采用开放定址法时, 只能逻辑删除。        装填因子: 表中记录数 / 散列表长度 。        备注: 平均查找长度的查找失败包含不放元素的情况。(特殊: 根据散列函数来: 2010真题)        聚集: 处理冲突的方法选取不当,而导致不同关键字的元素对同一散列地址进行争夺的现象第六章 排序        稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;        内排序 :所有排序操作都在内存中完成;        外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。参考博客:超详细十大经典排序法总结(java代码)c或者cpp的也可以明白_Top_Spirit的博客-CSDN博客6.1 直接插入排序动图演示:         优化: 折半插入排序6.2 希尔排序        又称缩小增量排序, 先将待排序表分割成若⼲形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”⼦表,对各个⼦表分别进⾏直接插⼊排序。缩⼩增量d,重复上述过程,直到d=1为⽌。建议每次将增量缩⼩⼀半。示例图:6.3 冒泡排序动图演示:6.4 快速排序法思想:        1) 在待排序表L[1…n]中任取⼀个元素pivot作为枢轴(或基准)        2) 通过⼀趟排序将待排序表划分为独⽴的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素⼩于pivot,L[k+1…n]中的所有元素⼤于等于 pivot,则pivot放在了其最终位置L(k)上,这个过程称为⼀次“划分”。        3) 然后分别递归地对两个⼦表重复上述过程,直每部分内只有⼀个元素或空为⽌,即所有元素放在了其最终位置上。示例图:  6.5 简单选择排序        法思想: 每⼀趟在待排序元素中选取关键字最⼩的元素加⼊有序⼦序列。动画演示:6.6 堆排序        ⼤根堆: 若满⾜:L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2 )        ⼩根堆: 若满⾜:L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2 )大根堆示例图:6.6.1 建立大根堆        思路:从开始, 把所有⾮终端结点都检查⼀遍,是否满足大根堆的要求,如果不满⾜,则进⾏调整。若元素互换破坏了下⼀级的堆,则采⽤相同的方法继续往下调整(⼩元素不断“下坠”)小元素下坠示例图:          结论: 建堆的过程,关键字对⽐次数不超过4n,建堆时间复杂度=O(n)6.6.2 堆的插入与删除        插入: 将新增元素放到表尾, 而后根据大小根堆进行上升调整。        删除: 被删除的元素⽤堆底元素替代,然后让该 元素不断“下坠”,直到⽆法下坠为⽌排序动图演示:6.7 归并排序        该法是采用分治法, 把两个或多个已经有序的序列合并成⼀个。2路归并图:        结论:n个元素进⾏k路归并排序,归并趟数= 6.8 基数排序 (低位优先)        基数排序是非比较的排序法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;动图演示:         时间复杂度: ⼀趟分配O(n),⼀趟收集O(r),总共 d 趟分配、收集,总的时间复杂度=O(d(n+r)) , 其中把d为关键字拆 为d个部分, r为每个部分可能 取得 r 个值。基数排序适用场景:        ①数据元素的关键字可以⽅便地拆分为 d 组,且 d 较⼩        ②每组关键字的取值范围不⼤,即 r 较⼩        ③数据元素个数 n 较⼤如:内部排序总结:         基本有序:  直接插入(比较最少), 冒泡(趟数最少)6.9 外部排序        数据元素太多,⽆法⼀次全部读⼊内存进⾏排序, 读写磁盘的频率成为衡量外部排序法的主要因素。示例图:多路归并:        结论: 采⽤多路归并可以减少归并趟数,从⽽减少磁盘I/O(读写)次数。对 r 个初始归并段,做k路归并,则归并树可⽤ k 叉树表示 若树⾼为h,则归并趟数 = h-1 = 。K越大, r越小, 读写磁盘次数越少。(缺点: k越大, 内部排序时间越大)6.9.1 败者树        使⽤k路平衡归并策略,选出⼀个最小元素需要对⽐关键字 (k-1)次,导致内部归并所需时间增加。因此引入败者树。示例图:        结论: 对于 k 路归并,第⼀次构造败者 树需要对⽐关键字 k-1 次 , 有了败者树,选出最⼩元素,只需对⽐关键字次6.9.2 置换-选择排序        使用置换-选择排序可以减少初始化归并段。示意图: 6.9.3 最佳归并树原理图:        注意:对于k叉归并,若初始归并段的数量⽆法构成严格的 k 叉归并树, 则需要补充⼏个⻓度为 0 的“虚段”,再进⾏ k 叉哈夫曼树的构造。示例图: 添加虚段数目: 难点:结束!  !  !注: 以上部分图片素材来自王道数据结构我要的图文并茂关注
最新发布
03-24
### 数据结构与法的核心知识点总结 #### 什么是数据结构? 数据结构是计算机科学中的一个重要主题,它涉及如何组织和存储数据,以便于在计算机程序中进行访问和操作[^1]。 #### 常见的数据结构 一些常见的数据结构包括但不限于数组、链表、栈、队列、树以及图等。这些数据结构各自有不同的特点和适用场景: - **数组**:一种线性数据结构,用于存储固定大小的相同类型的元素集合。 - **链表**:由一系列节点组成,每个节点包含数据部分和指向下一个节点的链接地址。 - **栈**:遵循后进先出 (LIFO) 的原则,通常只允许在一端执行插入和删除操作。 - **队列**:遵循先进先出 (FIFO) 的原则,在一端插入而在另一端移除元素。 - **树**:层次化的非线性数据结构,具有根节点和其他子节点的关系。二叉树是一种特殊的树形结构,其定义如下: ```c typedef struct BiTNode { TElemType data; // 结点数据域 struct BiTNode *lchild, *rchild; // 左右孩子指针 } BiTNode, *BiTree; ``` 这里展示了二叉树的一个基本实现方式[^2]。 - **图**:由顶点和边组成的抽象模型,用来表示对象之间的关系网络。 #### 法的概念 法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作[^3]。一个好的法应该具备以下几个特性: - 输入/输出明确; - 正确性——能够解决所设计的问题; - 可读性和可维护性强; - 高效的时间复杂度和空间复杂度; #### 时间复杂度与空间复杂度分析 时间复杂度衡量的是运行某个法所需的时间随输入规模增长而变化的趋势,常用大O记号来表达。例如,对于简单的查找操作可能达到 O(n),而对于快速排序则平均为 O(n log n)。 空间复杂度则是评估该法所需的额外内存资源量级情况。 ---
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值