考研中常用的两种结构体定义方法:
typedef struct
{
int a;
float b;
char c;
...
}结构体名
typedef struct 结构体名
{
int a;
float b;
char c;
struct 结构体名 *d;
...
}结构体名
定义了结构体之后,直接使用定义的结构体类型来创建一个变量,并通过点访问结构体中的分量。
typedef struct
{
int a;
float b;
char c;
...
}Stu
Stu s;
s.a;
s.b;
s.c;
c++中,如果要通过函数来使函数外部某个变量的值改变的话,可以使用引用型定义,也就是在定义函数参数的时候给参数名前增加一个&符号。
给出例子,定义一个增加方法,每次调用使变量加一:
int result = 0;
void add(int &r){
r++;
}
调用方法:add(result);
或者给指针变量赋值:
int *p = NULL;
void add(int *&p){
...
p = q;//q为某个地址
...
}
调用方法:add(p);
如果用c语言的话,则不能使用引用型定义,需要通过指针来完成方法的定义,这样的话,以上两种方法需要变为:
int result = 0;
void add(int *r){
(*r)++;
}
调用方法:add(&result);//这里的&是取地址符
int *p = NULL;
void add(int **p){//二级指针,指向指针的地址
...
*p = q;//q为某个地址
...
}
调用方法:add(&p);
定义一个名为LNode的结构体类型,也就是链式存储结构中的每一个结点类型,其中包含了数据data以及指向下一个结点的指针next:
typedef struct LNode
{
int data;
struct LNode *next;
}LNode
然后就是定义一个结点,使用最多的定义结点的方式是使用以下方式定义。
使用了c语言提供的malloc函数来为结点分配一个空间,malloc函数的参数是需要分配空间的大小,而使用运算符sizeof能够计算出某个变量类型的存储大小。malloc函数返回的是已分配空间的地址,将返回值强转成结点指针的类型并赋值给某个结点指针。
使用了这种方式来定义的结构体变量是没有名称的,只是占有一块内存空间,而为了方便称呼,我们将结点指针的名字称为结点的名字,例如以下的L。
LNode *L;
L = (LNode*)malloc(sizeof(LNode));
当我们想要使用结点中的分量需要采用箭头去访问,而不能用点,因为我们使用的实际上不是结构体变量的名称,而是指向这个结构体变量的指针。
L->data;
L->next;
线性表是具有相同特性数据元素的有限序列。
相同特性:同一类事物。
有限:元素个数有限,可以为0。
序列:表的元素排成一列,体现了一对一的逻辑特性,即每个元素最多只有一个前驱和一个后继。
常用的两类链表:含有头结点的链表(头结点不存数据信息)、不含有有头节点的链表。
含有头结点的链表判断链表为空的条件是:
Head->next == NULL;
不含有头结点的链表判断链表为空的条件是:
Head == NULL;
它们之间的共性是:不含有数据结点的链表都是空链表。
双链表,在前面链表的基础上,给每个结点都添加了一个指向前一结点的指针:
|
|
循环链表的判空条件为,当头结点的指针指向自己本身的时候则链表为空:
不含头结点的单双循环链,它们的判空条件为Head == NULL:
单链表的插入,在链表中的某一处插入:
删除单链表中的某一个结点:
插入结点的特殊情况,在链表的第一个元素之前插入一个结点,这里就涉及到头结点的好处了,使用头结点的好处就是尽管是在第一个元素前插入结点,也跟从链表中间插入元素的做法是一样的。
而不使用头结点插入结点的话,在链表的第一个元素之前插入一个结点跟从链表中间插入元素的做法有点不一样。(其实差不多)
删除结点也一样,使用头结点的话,删除链表的第一个结点跟从中间删除结点的操作是一样的。而不使用头结点的话,就有点不太一样了。
双链表结点的插入:
双链表结点的删除:
顺序表插入元素的代码,使想要插入位置处的元素及其后面的元素从后面开始一个一个往后移:
顺序表删除元素,把想要删除的元素后面的元素一个一个往前移,覆盖想要删除的元素即可,清空顺序表把length直接置为0即可,虽然此时数组中的元素还存在,但是这个顺序表是不可见的:
顺序表删除元素代码,参数p表示要删除元素的位置,参数e表示要删除元素的值。这里注意判断删除操作不合法的条件不用写length == 0,因为这个条件已经包含在判断条件内了:
顺序表建表代码:
int A[maxSize];
int length;
int createList(int A[],int& length)
{
std::cin>>length;
if(length > maxSize) return 0;
for(int i = 0;i < length;i++)
std::cin>>A[i];
return 1;
}
链表建表实际上就是插入结点的一系列操作,如果对结点插入的位置没有次序要求的话,一般只在链表的头部位置或尾部位置进行插入,即头插法和尾插法两种。
尾插法建表代码,考研题一般都是在内存足够的情况下,方法无需返回标志说明建表失败的情况,因此返回值定义为void。这里创建含有头结点的链表,因为含有头结点的链表比较方便好用,在考研过程中并没有强制要求使用不含头结点的链表。此外,这里创建的链表是单链表,其他链表如双链表、循环链表在此基础上修改即可。:
void createLinkListR(LNode *&head)
{
head = (LNode*)malloc(sizeof(LNode));
//每次为链表申请一个结点空间之后,把这个结点的next指针设定为NULL能够有
//效避免错误
head->next = NULL;
/*我们使用的是尾插法,需要定义一个指针来指向当前尾部的结点。方便下一次创建。
另外还需要一个指针来指向当前新增的结点。
p指针为指向新增结点的指针。
r指针为指向当前尾部结点的指针。
*/
LNode *p = NULL,*r = head;
//描述要创建的结点的个数
int n;
std::cin>>n;//从键盘输入。scanf("%d",&n);
for(int i = 0;i < n;++i)
{
//每次循环创建一个结点,并从键盘输入传个结点的data域
p = (LNode*)malloc(sizeof(LNode));
p->next = NULL;
std::cin>>p->data;//scanf("%d",&(p->data);
//此行代码删除对尾插法没有影响,增加这行代码能够使这个方法
//适用于从中部插入,也就是适用于r结点指向中部的某一结点
p->next = r->next;
r->next = p;
r = p;
}
头插法跟尾插法相似,只需要做一些修改:
void createLinkListH(LNode *&head)
{
head = (LNode*)malloc(sizeof(LNode));
//每次为链表申请一个结点空间之后,把这个结点的next指针设定为NULL能够有
//效避免错误
head->next = NULL;
/*使用头插法的话则不需要一个指针来指向尾结点了,
只需要一个指向新建结点的指针*/
LNode *p = NULL;
//描述要创建的结点的个数
int n;
std::cin>>n;//从键盘输入。scanf("%d",&n);
for(int i = 0;i < n;++i)
{
//每次循环创建一个结点,并从键盘输入传个结点的data域
p = (LNode*)malloc(sizeof(LNode));
p->next = NULL;
std::cin>>p->data;//scanf("%d",&(p->data);
//让p指向的结点中的指针指向head原先指向的结点
p->next = head->next;
//让头结点的指针指向新增的结点
head->next = p;
}
void createLinkNoSameEle(LNode *&head)
{
head = (LNode*)malloc(sizeof(LNode));
head->next = NULL;
//指向新增的结点
LNode *p;
//数字字母的个数
int n;
std::cin>>n;
//接收每次输入的字母
char ch;
for(int i = 0; i < n; i ++){
std::cin>>ch;
p = head->next;
while(p != NULL){
if(p->data == ch)
break;
p = p->next;
}
if( p == NULL){
p = (LNode*)malloc(sizeof(LNode));
p->next = head->next;
head->next = p;
}
}
}
以线性表的某个元素为界限将线性表划分为两个部分,这个元素称为枢轴。有三种题型。
给定一个顺序表,以顺序表的第一个元素为枢轴,将顺序表划分为左右两部分,使得顺序表左边的元素都小于枢轴,右边的元素都大于枢轴,并且枢轴要夹于左右两部分元素之间。
例子,有这么一个数组,枢轴为2,我们借助临时变量temp以及存储数组头部和尾部的下标的两个变量i和j来完成划分。
划分的过程是:
//参数分别为数组、数组的长度
void partition(int arr[],int n)
{
//定义临时变量
int temp;
int i = 0,j = n - 1;
temp = arr[i];
while(i < j){
//当j对应的元素大于枢轴的时候j往左移
while(i < j && arr[j] >= temp) j--;
if(i < j){
arr[i] = arr[j];i++;
}
while(i < j && arr[i] <= temp) i++;
if(i < j){
arr[j] = arr[i];j--;
}
}
arr[i] = temp;
}
上述代码进行比较都涉及到枢轴temp,假如新增一个变量comp,将每次比较的值都换成新变量comp,则最终顺序表会划分为两部分,一部分小于变量comp,一部分大于变量comp。也就是顺序表以新变量为标准进行了划分。
尽管用来比较的元素存在于顺序表中,它跟分界线的位置都无关,当用来比较的元素比枢轴小的时候,分界线在枢轴的左边,当用来比较的元素比枢轴大的时候,分界线在枢轴的右边。而当用来比较的元素跟枢轴相等的时候,分界线就在枢轴处,也就是一开始的那种情况了。
前两种题型都是以顺序表的第一个元素作为枢轴,第三种题型是以顺序表中的任意以个元素作为枢轴进行划分。其实也是前两种题型的变形,只要将选中的那个元素与顺序表的第一个元素交换位置,则变为了第一种题型了。
只有顺序表才会涉及到元素移动次数的计算。顺序表的元素插入和删除操作会使元素移动。
对于某种存储结构,其存储空间如果是一次性分配就叫静态分配,多次就叫动态分配。顺序表的存储空间一般都是静态分配的,而链表的存储空间一般都是动态分配的、是根据需要再进行分配的。
这里要讲的静态链表的存储空间是静态分配的,也就是我们给它分配了一个顺序表,然后在这一片顺序存储空间通过某种方法实现一种类似于链表的存储结构。
在考研数据结构范围内,常常是给定一个建好的静态链表,然后对其存储过程中的一些细节进行考察。
静态链表的实现,创建了一个静态链表(数组),将线性表存储到静态链表中,每个结点不是挨着的,而是分散的,并且每个结点都存储着下一个结点所在位置的下标:
静态链表的操作:
这里只讲二路归并,就是把两个线性表合成一个线性表。
第一种是将两个有序的顺序表进行有序的归并:
两个变量i和j分别存储两个顺序表第一个元素的下标,然后开始进行归并。
代码:
//传入三个数组,(两个初始数组,一个结果数组),以及两个顺序表的长度
//这里返回值为void,假设传入的结果数组是足够长的,不存在归并失败的情况
void mergearray(int a[],int m,int b[],int n,int c[])
{
//i和j分别指向两个顺序表的首部
int i = 0,j = 0;
//用来指向结果顺序表的尾部
int k = 0;
while(i < m && j < n){
if(a[i] < n[j])
c[k++] = a[i++];
else
c[k++] = b[j++];
}
while(i < m) c[k++] = a[i++];
while(j < n) c[k++] = b[j++];
}
顺序表的归并需要另外开辟一块存储空间来进行归并,而链表不需要。链表归并跟顺序表相似。
归并两个升序链表:
代码:
//传入三个链表的头指针,A和B是原先两个链表的头指针,不需要改变,因此不用引用类型
//而C是我们新得到的头结点的指针,需要进行改变,所以用引用类型
void merge(LNode *A,LNode *B,LNode *&C)
{
//指向两个链表第一个结点的指针
LNode *p = A->next;
LNode *q = B->next;
//用来保存每次比较指向data域较小结点的指针
LNode *s;
//让C指向A链表的头结点
C = A;
C->next = NULL;
//释放B链表的头结点
free(B);
while(p != NULL && q != NULL){
if(p->data < q->data)
{
s = p;
p = p->next;
s->next = C->next;
C->next = s;
}
else
{
s = q;
q = q->next;
s->next = C->next;
C->next = s;
}
}
while (p != NULL)
{
s = p;
p = p->next;
s->next = C->next;
C->next = s;
}
while (q != NULL)
{
s = q;
q = q->next;
s->next = C->next;
C->next = s;
}
}
假如要得到降序的链表,也跟上面的步骤差不多,但是区别在于每次比较之后,把data域小的元素插在第一个结点的位置,也就是头结点之后的位置,并且当某一链表取完结点之后,将剩下的链表的结点一个一个拆下来放到新链表的第一个结点的位置,完成归并。
代码只需要在前面归并的代码基础上进行修改即可:
//传入三个链表的头指针,A和B是原先两个链表的头指针,不需要改变,因此不用引用类型
//而C是我们新得到的头结点的指针,需要进行改变,所以用引用类型
void mergeR(LNode *A,LNode *B,LNode *&C)
{
//指向两个链表第一个结点的指针
LNode *p = A->next;
LNode *q = B->next;
//用来指向结果链表尾部元素的指针
LNode *s;
//让C指向A链表的头结点
C = A;
C->next = NULL;
//释放B链表的头结点
free(B);
while(p != NULL && q != NULL){
if(p->data < q->data)
{
r->next = p;
r = p;
p = p->next;
}
else
{
r->next = q;
r = q;
q = q->next;
}
}
if (p != NULL)
r->next = p;
if (q != NULL)
r->next = q;
}
能够发现,归并后得到升序链表的过程类似于链表插入的尾插法,归并后得到降序链表的过程类似于头插法。
顺序表的逆置只需要定义三个变量,分别是临时变量temp、指向顺序表第一个元素的位置的变量i和指向顺序表最后一个元素的位置的变量j。然后将第一个元素和最后一个元素借助临时变量交换位置,交换完之后i加1,j减一,然后再进行交换,直至i>=j不再进行交换,逆置结束。
一般考题会以以下的形式,给出一个p指针和q指针分别指向某一个结点,然后要求逆置p指针后继结点到q指针指向的结点之间的结点。
逆置的过程为:
重复操作,将p的后继结点移动到q指向的结点的后面。结束条件为p->next == q。
这是顺序表逆置操作的真题,链表操作的比较简单,按照上面的代码去做即可。
解题的思路是将前k个(这里演示前4个)与后k个按照逆置的方法分别两两交换位置
对应的代码,i只需要扫描前k个位置即可,这里条件除了i
解题思路为将前k个元素逆置,再将这k个元素与后k个元素两两交换位置,也就是调用上一题定义的函数。
循环左移p个位置,即把0到p-1位置上的元素保持原序移到了数组的后端,而p到n-1位置的元素保持原序移动到了数组的前端。这个题跟第二题的区别在于数组的前半段跟后半段都需要保持原序。
解题的思路是将前p个元素逆置,然后把p到n-1位置的元素逆置,再把整个数组逆置即可。
顺序表中取最大值的代码:
顺序表中取最小值的代码:
链表中取最大值的代码:
链表中取最小值的代码:
void maxFirst(DLNode *head)
{
DLNode *p = head->rlink,*q = p;
int max = p->data;
//找最值
while(p != NULL){
if(max < p->data){
max = p->data;
q = p;
}
p = p->rlink;
}
//定义两个指针存储data域最大的结点的两个指针,让它们分别指向这个结点的前驱结点和后继结点
DLNode *l = q->llink, *r = q->rlink;
//让前驱结点的右指针指向后继结点
l->rlink = r;
//如果后继结点不为空的话,就将后继结点的左指针指向前驱结点,完成前驱结点跟后继结点的连接
if(r != NULL) r-llink = l;
//data域最大的结点插入到头结点之后
q->llink = head;
q->rlink = head->rlink;
head->rlink = q;
q->rlink->llink = q;
}
栈是只能在一端进行插入或删除操作的线性表。栈的逻辑结构属于线性表,只不过在操作上加了一些约束。可以插入和删除元素的一端叫栈顶,另一端叫栈底。具有先进后出的特性。栈的存储结构有两种,分别是顺序栈和链栈。
顺序栈最简单的定义:
出栈操作不会删除数组中的元素,但是出栈的元素已经不属于栈了,栈的元素范围是0到top。
栈满或栈空的条件:
元素入栈,让top指针指向新建的结点(即栈顶元素的结点),然后将这个结点插入到头结点之后:
元素出栈,定义一个变量接收出栈元素的数据,然后将头结点与下一个元素相连,然后让top指针指向新的栈顶元素:
栈空栈满的条件:
队列是一种插入元素只能在一端进行,删除元素只能在另一端进行的线性表。队列的逻辑结构属于线性表,只不过在操作上增加了一些约束,可以插入元素的一端叫队尾,可以删除元素的一端叫队头。具有先进先出的特性。
队列也具有两种存储结构,分别是顺序队和链队。
顺序队的定义,定义一个数组和两个变量,这两个变量分别指向队头的元素和队尾的元素:
顺序队入队,先移动队尾指针(其实只是一个整型变量,存储数组的某一个元素的下标)至下一位,然后将元素存储到队尾指针指向的位置:
顺序队出队,将队头指针移动到下一位,然后将队头指针指向的变量赋值给变量x。元素出队之后,它的值仍然存留在数组中(这里为了说明这个元素不属于队列把它删除了),:
可以发现队头指针指向的元素的下一个元素才是队列的第一个元素,队尾指针总是指向队列的最后一个元素。
当队尾指针指向了数组的最后一个元素,而队头指针并不指向数组的第一个元素时,此时再往队列增加元素的话会报数组越界异常,但是数组的存储空间并没有占满,这种现象称为假溢出。
解决假溢出的办法是,当队尾指针指向最后一个元素时,还想要往队列添加元素的话,要将队尾指针重新指向数组的第一个元素,队头指针也是,到达了最后一个元素之后,要重新指向数组的第一个元素。可以将其想象成一个圆环。
利用以下代码能够实现圆环的效果,解决假溢出,由于这种类似圆环的效果,顺序队也称为循环队列:
当队头指针跟队尾指针指向同一个元素的时候,代表队列空了(这只是一个规定,具体还要看题目)
当队尾指针往后移一位之后与队头指针指向同一个元素的时候,这个时候我们把它称为队满的状态。由于是类似环状的存储结构,还需要在判断条件加上一个%maxSize。之所以不让数组把元素放满是因为这个时候队头指针跟队尾指针都指向同一个元素,跟队空无法进行区分。
链队不存在假溢出的情况,也不存在队满的情况,比较简单,只需要一条普通的单链表在加上队头指针跟队尾指针。队头指针指向头结点,队尾指针指向链表的最后一个结点。
元素入队就是在队尾指针rear指向的结点后面插入一个新的结点,并让队尾指针指向这个新结点。元素出队就是删除这个链表的第一个结点。队空的条件就是头结点的next指针为NULL。
考研题经常将队头指针跟队尾指针包裹在一个结构体内,并且用来替代头结点,这种情况队空的条件就是front指针为NULL:
给定一堆数据按照某种顺序入栈,然后问一些和出栈顺序有关的问题。
根据不同的入栈和出栈的操作次序,会导致所需的栈容量不同。
简单的可进行模拟:
可以发现所需的容量至少为3。
题目也有可能不告诉出栈序列,可能会说让出栈序列压入到队列后出队的序列是什么,其实也等同于出栈序列。
或者将出栈序列改为出栈后再入栈再出栈的序列。也就是出栈序列的逆序。
2.
3. i、j、k为升序序列中的三个元素,使用穷举法将所有的可能结果都列出来,然后再一一进行判断结果可不可行。由于i
将pi、pj、pk放回到入栈序列进行讨论,假如能够得到正确的出栈序列,那这种结果就是正确的:
发现只有第四种结果是不可能的:
可以发现结论:
如果最后一个入栈的元素第一个出栈,其余元素的入栈顺序就是固定的(这个结论在p1=n也体现出来了)(若1、2、3,则无3、1、2)。
表达式转换适合选择题、填空题、简单题。
同一组操作数、同一组运算符,它们有不同的表达式形式,有三种表达式:
将a和b两个操作数视为两个表达式,用括号将表达式括起来,然后把运算符提取到前面,再把括号去掉,就能从中缀表达式转换成前缀表达式。
稍微复杂的也是一样,把表达式一个一个括起来,但是不改变它们原先的运算顺序,再将运算符提取到括号前,然后去掉括号。
跟中缀表达式转前缀表达式是类似的,不同点只在于把运算符提取到括号的后边。
为什么可以丢掉括号?
对于一个中缀表达式,通过给它加括号导致不同的运算次序,每一种运算次序都会有唯一的后缀表达式与之对应。
只要将中转表达式转成后缀表达式的过程逆过来就可以了。将操作数视为表达式,寻找两个表达式紧跟一个运算符的地方,然后将运算符提到中间再用括号括起来,重复操作后去掉多余的括号即可。
把操作数视为表达式,找到两个表达式紧跟一个运算符的地方,先把运算符提到前面再用括号括起来。重复操作最后再将括号去掉即可。
对栈和队列的代码设计能够有多种多样,只要能够实现它们的特性即可。这里不同的设计称为不同的配置,对队列的配置问题考察的比较多,对栈的配置问题考察的比较少。这里对队列的不同配置延伸出来的问题进行讨论。这里主要针对顺序队(循环队列),因为链队很难出不同的考题。
往年考题中出现的默认的配置称为正常的配置。也就是题目不告诉你队列的信息就让你说出队列的一些特性等,这这种队列为默认的配置,也就是正常配置。而不正常配置题目一般会暗示或明示。配置也就是队空、队满、入队、出队在不同的规定下体现的形式。
正常配置下考察最多的是元素个数计算:
可考题型:
这里给出两种考过的非正常配置。
队空条件不变:
入队操作变为先插入元素再移动指针
出队操作也是先删除元素再移动指针:
队满条件也不变:
这种非正常配置跟正常配置的区别在于入队和出队的操作次序不一样。导致的结果就是队头指针front指向队列的第一个元素,而队尾指针rear指向队列的最后一个元素之后的一个位置。恰好跟正常配置相反。
第二种非正常配置的队空情况有所不同,条件跟前面两种配置的队满条件是一样的。
入队操作,可以是先入队元素再移动指针,也可以是先移动指针再入队元素,这里以先移动指针再入队元素为例。
出队操作,先出队元素再移动指针。
队满条件,此时队头指针跟队尾指针距离为2:
计算队列元素个数的公式也有点小区别:
公式跟前面两种配置相差一个整数1
前面所讲的表达式转换不适合用代码表示,利用栈来实现表达式转换就能很好得用代码表示了。
可考题型:
代码实现:
//infix:中缀表达式
//s2:栈,用来存储转化结果
//top2:栈顶指针
void infixToPostFix(char infix[],char s2[],int &top2)
{ //辅助栈
char s1[maxSize];int top = -1;
int i = 0;
//开始循环中缀表达式
while(infix[i] != '\0')
{
//判断是否是数字字符,是的话直接入保存结果的栈
if('0' <= infix[i] && infix[i] <= '9')
{
s2[++top2] = infix[i];
++i;
}
//判断是否是左括号,是的话则直接入辅助栈
else if(infix[i] == '(')
{
s1[++top1] = '(';
++i;
}
//判断是否是运算符
else if(infix[i] == '+' || infix[i] =='-'
|| infix[i] =='*' || infix[i] == '/')
{
//是运算符的话还要进一步判断,如果栈为空或者栈顶元素为左括号
//或者运算符比栈顶元素的优先级高则入辅助栈
if(top1 == -1 || s1[top1] == '('
|| getPriority(infix[i]) > getPriority(s1[top1])
{
s1[++top1] = infix[i];
++i;
}
//否则的话就将辅助栈的栈顶元素出栈并且存储到结果栈中
else
s2[++top2] = s1[top1--];
}
//判断是否是右括号
else if (infix[i] == ')')
{
//判断栈顶元素是否是左括号,不是的话则栈顶元素一直出栈并存入结果栈中,
//直到栈顶元素为左括号
while(s1[top1] != '(')
s2[++top2] = s1[top1--];
--top1;//把左括号也出栈,但不存入结果栈中
++i;
}
}
//扫描完中缀表达式之后在将辅助栈中剩余的运算符存到结果栈中
while(top1 != -1)
s2[++top2] = s1[top1--];
}
中缀表达式转前缀表达式跟中缀表达式转后缀表达式几乎一样,不同点在于:
//infix:中缀表达式
//len:中缀表达式的长度
//s2:栈,用来存储转化结果
//top2:栈顶指针
void infixToPreFix(char infix[],int len,char s2[],int &top2)
{ //辅助栈
char s1[maxSize];int top = -1;
int i = len - ;
//开始循环中缀表达式
while(infix[i] != '\0')
{
//判断是否是数字字符,是的话直接入保存结果的栈
if('0' <= infix[i] && infix[i] <= '9')
{
s2[++top2] = infix[i];
--i;
}
//判断是否是右括号,是的话则直接入辅助栈
else if(infix[i] == ')')
{
s1[++top1] = ')';
--i;
}
//判断是否是运算符
else if(infix[i] == '+' || infix[i] =='-'
|| infix[i] =='*' || infix[i] == '/')
{
//是运算符的话还要进一步判断,如果栈为空或者栈顶元素为右括号
//或者运算符比栈顶元素的优先级高或等于则入辅助栈
if(top1 == -1 || s1[top1] == ')'
|| getPriority(infix[i]) >= getPriority(s1[top1])
{
s1[++top1] = infix[i];
--i;
}
//否则的话就将辅助栈的栈顶元素出栈并且存储到结果栈中
else
s2[++top2] = s1[top1--];
}
//判断是否是左括号
else if (infix[i] == '(')
{
//判断栈顶元素是否是右括号,不是的话则栈顶元素一直出栈并存入结果栈中,
//直到栈顶元素为右括号
while(s1[top1] != ')')
s2[++top2] = s1[top1--];
--top1;//把左括号也出栈,但不存入结果栈中
--i;
}
}
//扫描完中缀表达式之后在将辅助栈中剩余的运算符存到结果栈中
while(top1 != -1)
s2[++top2] = s1[top1--];
}
用栈求中缀表达式的值需要两个栈s1和s2,一个用来存储操作数,一个用来存储运算符。
//判断运算符优先级
void getPriority(char op)
{
if( op == '+' || == '-')
return 0;
else
return 1;
}
//计算子表达式函数,根据当前出栈的运算符进行相应的计算
//opand1、opand2是操作数,op为运算符
int calSub(float opand1,char op,float opand2,float &result)
{
if(op == '+') result = opand1 + opand2;
if(op == '-') result = opand1 - opand2;
if(op == '*') result = opand1 * opand2;
if(op == '/')
{
//检测除数是否为0
//fabs:求绝对值,MIN:预先定义好的宏,代表一个十分接近0的正数
//这样写是判断浮点数是否为0的标准写法,在十分接近0的时候我们认为它为0
if(fabs(opand2) < MIN)
{
//说明当前运算无法继续下去
return 0;
}
else
{
return opand1/opand2;
}
}
}
求解中缀表达式的代码实现:
//直接将运算结果返回
float calInfix(char exp[])
{
//定义两个栈,一个用来存放操作数,一个用来存放运算符
float s1[maxSize];int top1 = -1;
char s2[maxSize];int top2 = -1;
int i = 0;
//循环中缀表达式
while (exp[i] != '\0')
{
//假设输入的操作数都是一位数的
if('0' <= exp[i] && exp[i] <= '9')
{
//是操作数则入s1栈
s1[++top1] = exp[i] - '0';
++i;
}
//判断是否为左括号
else if(exp[i] == '(')
{
//左括号直接入s2栈
s2[++top2] = '(';
++i;
}
//判断是否为运算符
else if (exp[i] == '+' ||
exp[i] == '-' ||
exp[i] == '*' ||
exp[i] == '/')
{
//优先级大于栈顶运算符或栈为空或栈顶为左括号时则入栈
if(top2 == -1 || s2[top2] == '(' ||
getPriority(exp[i]) > getPriority(s2[top2])
{
s2[++top2] = exp[i];
++i;
}
//否则出栈栈顶运算符和两个操作数并进行一次运算
else
{
//定义临时变量
float opnd1,opnd2,result;
char op;
int flag;
//出栈两个操作数
opnd2 = s1[top1--];
opnd1 = s1[top1--];
//出栈运算符
op = s2[top2--];
//调用计算函数
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式是否求值成功
//求值不成功,函数结束
if(flag == 0)
{
std::cout<<"ERROR"<<std:endl;//puts("ERROR");
//结束函数
return 0;
}
//求值成功,压入s1栈
s1[++top1] = result;
//计算完之后,i不需要加1,因为还要重新对i所指向的运算符与栈顶运算符比较
}
}
//判断是否为右括号
else if(exp[i] ==')')
{
//一直出栈s2栈顶元素进行运算直至遇到左括号
while(s2[top2] != '(')
{
//定义临时变量
float opnd1,opnd2,result;
char op;
int flag;
//出栈两个操作数
opnd2 = s1[top1--];
opnd1 = s1[top1--];
//出栈运算符
op = s2[top2--];
//调用计算函数
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式是否求值成功
//求值不成功,函数结束
if(flag == 0)
{
std::cout<<"ERROR"<<std:endl;//puts("ERROR");
//结束函数
return 0;
}
//求值成功,压入s1栈
s1[++top1] = result;
}
//循环结束,出栈左括号
top2--;
//处理表达式下一个字符
++i;
}
}
//当中缀表达式扫描完之后,s2栈仍有运算符时,不断出栈进行计算
while(top2 != -1)
{
//定义临时变量
float opnd1,opnd2,result;
char op;
int flag;
//出栈两个操作数
opnd2 = s1[top1--];
opnd1 = s1[top1--];
//出栈运算符
op = s2[top2--];
//调用计算函数
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式是否求值成功
//求值不成功,函数结束
if(flag == 0)
{
std::cout<<"ERROR"<<std:endl;//puts("ERROR");
//结束函数
return 0;
}
//求值成功,压入s1栈
s1[++top1] = result;
}
//最后s1栈顶元素即为结果
return s1[top1];
}
仔细看完代码,发现有一块重复的代码写了三遍,也就是进行计算的那块。我们把它提取出来,封装到一个函数内,对代码进行优化。
int calStackTopTwo(float s1[], int &top1, char s2[], int &top2)
{
float opnd1,opnd2,result;
char op;
int flag;
//出栈两个操作数
opnd2 = s1[top1--];
opnd1 = s1[top1--];
//出栈运算符
op = s2[top2--];
//调用计算函数
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式是否求值成功
if(flag == 0)
std::cout<<"ERROR"<<std:endl;//puts("ERROR");
s1[++top1] = result;
return flag;
}
//直接将运算结果返回
float calInfix(char exp[])
{
//定义两个栈,一个用来存放操作数,一个用来存放运算符
float s1[maxSize];int top1 = -1;
char s2[maxSize];int top2 = -1;
int i = 0;
//循环中缀表达式
while (exp[i] != '\0')
{
//假设输入的操作数都是一位数的
if('0' <= exp[i] && exp[i] <= '9')
{
//是操作数则入s1栈
s1[++top1] = exp[i] - '0';
++i;
}
//判断是否为左括号
else if(exp[i] == '(')
{
//左括号直接入s2栈
s2[++top2] = '(';
++i;
}
//判断是否为运算符
else if (exp[i] == '+' ||
exp[i] == '-' ||
exp[i] == '*' ||
exp[i] == '/')
{
//优先级大于栈顶运算符或栈为空或栈顶为左括号时则入栈
if(top2 == -1 || s2[top2] == '(' ||
getPriority(exp[i]) > getPriority(s2[top2])
{
s2[++top2] = exp[i];
++i;
}
//否则出栈栈顶运算符和两个操作数并进行一次运算
else
{
int flag = calStackTopTwo(s1,top1,s2,top2);
//如果返回的标志为0,结束函数
if(flag == 0) return 0;
//计算完之后,i不需要加1,因为还要重新对i所指向的运算符与栈顶运算符比较
}
}
//判断是否为右括号
else if(exp[i] ==')')
{
//一直出栈s2栈顶元素进行运算直至遇到左括号
while(s2[top2] != '(')
{
int flag = calStackTopTwo(s1,top1,s2,top2);
//如果返回的标志为0,结束函数
if(flag == 0) return 0;
}
//循环结束,出栈左括号
top2--;
//处理表达式下一个字符
++i;
}
}
//当中缀表达式扫描完之后,s2栈仍有运算符时,不断出栈进行计算
while(top2 != -1)
{
int flag = calStackTopTwo(s1,top1,s2,top2);
//如果返回的标志为0,结束函数
if(flag == 0) return 0;
}
//最后s1栈顶元素即为结果
return s1[top1];
}
float calPostFix(char exp[])
{
float s[maxSize];int top = -1;
while(exp[i] != '\0')
{
if(exp[i] >= '0' && exp[i] <= '9')
s[++top] = exp[i] -'0';
else if (exp[i] == '+' ||
exp[i] == '-' ||
exp[i] == '*' ||
exp[i] == '/')
{
float opnd1,opnd2,result;
char op;
int flag;
opnd2 = s[top--];
opnd1 = s[top--];
op = exp[i];
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式计算是否成功
if(flag == 0)
{
std::cout<<"ERROR"<<std::endl;//puts("ERROR");
return 0;
}
//成功则把子表达式计算结果压入栈中
s[++top] = result;
}
i++;
}
return s[top];
}
以上的代码等同于以下,while循环改为for循环,else if表达式改为else:
float calPostFix(char exp[])
{
float s[maxSize];int top = -1;
for(int i = 0;exp[i] != '\0';i++)
{
if(exp[i] >= '0' && exp[i] <= '9')
s[++top] = exp[i] -'0';
else
{
float opnd1,opnd2,result;
char op;
int flag;
opnd2 = s[top--];
opnd1 = s[top--];
op = exp[i];
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式计算是否成功
if(flag == 0)
{
std::cout<<"ERROR"<<std::endl;//puts("ERROR");
return 0;
}
//成功则把子表达式计算结果压入栈中
s[++top] = result;
}
}
return s[top];
}
用栈求前缀表达式的值跟用栈求后缀表达式的值过程是十分类似的,它们的过程是反过来的。
float calPreFix(char exp[],int len)
{
float s[maxSize];int top = -1;
for(int i = len - 1;i >= 0;i--)
{
if(exp[i] >= '0' && exp[i] <= '9')
s[++top] = exp[i] -'0';
else
{
float opnd1,opnd2,result;
char op;
int flag;
opnd1 = s[top--];
opnd2 = s[top--];
op = exp[i];
flag = calSub(opnd1,op,opnd2,result);
//判断子表达式计算是否成功
if(flag == 0)
{
std::cout<<"ERROR"<<std::endl;//puts("ERROR");
return 0;
}
//成功则把子表达式计算结果压入栈中
s[++top] = result;
}
}
return s[top];
}
我们发现计算前缀式和后缀式的值的实现过程比计算中缀式的值的实现过程简单的多,能够节省计算机的存储空间,这就是为什么要使用前缀式和后缀式的原因。
考点:
1.完成某个过程(如计算表达式的值,表达式转换)所需要的辅助栈的存储空间至少是多少。
2. 执行到某一步时,栈顶元素是多少。
3. 给一个表达式,写出栈的变化情况。
队列的两端都可以入队和出队元素。对双端队列的一端做出限制,能出现很多种情况的队列。
考研中涉及的考点题:
首先看第一问,首先封锁一端,求出栈形态下不可能输出的序列的总数目。
然后将解放封锁一端的输出,使用穷举法列出栈形态下不可能输出的序列,然后再一个个去进行排除,得到输入受限的双端队列不可能输出的序列。(考题可能会出选择题或简单题、列出几组序列让你判断哪几组可能哪几组不可能)
I代表插入,OL代表从左端出队,OR代表从右端出队。
一个一个进行尝试,发现4、2、1、3序列和4、2、3、1是不行的。
然后看第二问,方法同上:
一个一个进行尝试、发现4、1、3、2和4、2、3、1是不行的。
第三问,就是一二问求结果的交集。
两个栈共享同一片存储空间,两个栈的栈底在存储空间的两端。
共享栈如何实现,相比普通栈只是增加了一个栈顶指针:
为了表示这两个栈顶指针是属于同一个共享栈的,我们一般定义一个数组来存放这两个指针:
判断栈空的条件:
如何入栈:
栈满的条件:
一般使用两个存储空间大小一样的栈,先把元素入栈s1,然后出栈再入栈到s2,这样当s2元素出栈的时候就能模拟队列先进先出的特性了。
考察在编译器中对括号检测技术的了解。给一段待编译的代码,可能涉及到很多括号,哪些括号出现的情况是匹配的,哪些括号出现的情况是不匹配的,要能成功检测出来。如何检测出来需要知道什么样是匹配的情况什么样是不匹配的情况。
那么如何来判断括号匹不匹配呢?这需要用到栈。
代码实现:
//定义一个方法判断括号是否匹配
int isMatch(char left, char right)
{
if(left =='(' && right == ')')
return 1;
else if(left == '[' && right == ']')
return 1;
else if(left == '{' && right == '}')
return 1;
else
return 0;
}
//判断表达式的括号是否匹配
int isParenthesesBalenced(char exp[])
{
char s[maxSize];int top = -1;
for(int i = 0;exp[i] != '\0';i++)
{
//判断是否为左括号,是的话直接入栈
if(exp[i] == '(' || exp[i] == '[' || exp[i] == '{')
s[++top] = exp[i];
//判断是否为右括号,分情况讨论
if(exp[i] == ')' || exp[i] == ']' || exp[i] == '}')
{
//如果栈空,函数结束,括号不匹配
if(top == -1)
return 0;
//栈不空,出栈栈顶括号,判断是否与扫描到的括号匹配
char left = s[top--];
//括号不匹配,函数结束,表达式括号不匹配
if(isMatched(left ,exp[i] == 0)
return 0;
}
}
//扫描完表达式之后,如果栈不为空的话,代表表达式括号不匹配,函数结束。
if(top > -1) return 0;
//表达式括号匹配
return 1;
}
最常见的题型的形式,op为运算符
给出例子:
把m>0的式子一层一层展开:
把每次展开得到的因子压入栈中,实际上这个函数的解决并不需要用到栈来解决,直接将展开后所得的因子相乘即可。之所以运用栈来解决这个问题,是因为这个问题用递归来解决最清晰明了,而递归内部就是根据栈来实现的。这里就是用自己建立的栈来模拟递归内部的实现。可能会遇到考题说让你用栈来解决这种问题。
代码实现:
//参数一般都是整数
int calF(int m)
{
//初值
int cnum = 1;
int s[maxSize],top = -1;
while(m != 0)
{
s[++top] = m;
//用以得到多次展开的因子
m /= 3;
}
while(top != -1)
//将栈中的每个数相乘,也就是将每个因子累乘,得到结果
cnum *= s[top--];
return cnum;
}