数据结构(1)

文章目录

  • 一、基础
    • (一)结构体的定义方法
    • (二)引用型定义
    • (三)链式存储结构
  • 二、线性表
    • (一)逻辑结构
    • (二)存储结构
    • (三)链表
    • (四)特性对比问题
    • (五)插入和删除
      • (1)链表的插入删除
      • (2)顺序表的插入删除
    • (六)建表
      • (1)顺序表建表
      • (2)链表建表
    • (七)划分
      • (1)第一种题型
      • (2)第二种题型
      • (3)第三种题型
    • (八)移动次数计算和静态链表
      • (1)移动次数计算
      • (2)静态链表
    • (九)归并
      • (1)顺序表归并
      • (2)链表归并
    • (十)逆置
      • (1)顺序表的逆置
      • (2)链表的逆置
      • (3)真题举例
    • (十一)取最值
      • (1)基础代码
      • (2)真题
  • 三、栈和队列
    • (一)栈
      • (1)顺序栈
      • (2)链栈
    • (二)队列
      • (1)顺序队
      • (2)链队
    • (三)输出序列问题
      • (1)由出栈序列判断容量问题
      • (2)常见题型
      • (3)Catalan number(卡特兰数)(记)
    • (四)表达式转换
      • (1)中缀表达式转前缀表达式
      • (2)中缀表达式转后缀表达式
      • (3)后缀表达式转中缀表达式
      • (4)后缀表达式转前缀表达式
    • (五)配置问题
      • (1)顺序队的正常配置
      • (2)顺序队的非正常配置
        • ①第一种非正常配置
        • ②第二种非正常配置
    • (六)用栈实现表达式转换
      • (1)中缀表达式转后缀表达式
      • (2)中缀表达式转前缀表达式
      • (3)后缀表达式转前缀表达式
      • (4)前缀表达式转后缀表达式
    • (七)用栈实现表达式求值
      • (1)用栈求中缀表达式的值
      • (2)用栈求后缀表达式的值
      • (3)用栈求前缀表达式的值
    • (八)双端队列
    • (九)栈的扩展
      • (1)共享栈
      • (2)用栈模拟队列
    • (十)队列的扩展
    • (十一)括号匹配和计算
      • (1)括号匹配
      • (2)计算问题

一、基础

(一)结构体的定义方法

考研中常用的两种结构体定义方法:

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。
序列:表的元素排成一列,体现了一对一的逻辑特性,即每个元素最多只有一个前驱和一个后继。

(二)存储结构

  • 顺序存储结构(顺序表)
  • 链式存储结构(链表)

(三)链表

常用的两类链表:含有头结点的链表(头结点不存数据信息)、不含有有头节点的链表。
数据结构(1)_第1张图片
含有头结点的链表判断链表为空的条件是:

Head->next == NULL;

不含有头结点的链表判断链表为空的条件是:

Head == NULL;

它们之间的共性是:不含有数据结点的链表都是空链表。

双链表,在前面链表的基础上,给每个结点都添加了一个指向前一结点的指针:

数据结构(1)_第2张图片
数据结构(1)_第3张图片循环单链表
数据结构(1)_第4张图片循环双链表

循环链表的判空条件为,当头结点的指针指向自己本身的时候则链表为空:
数据结构(1)_第5张图片
不含头结点的单双循环链,它们的判空条件为Head == NULL:
数据结构(1)_第6张图片

(四)特性对比问题

  • 在顺序表中插入和删除元素可能会导致大量的元素的连带操作(插入或删除操作发生在表尾位置例外),而链表不会。

数据结构(1)_第7张图片

  • 链表的空间利用率可能比顺序表的空间利用率高,一般都是正确的,但只要给顺序表分配的内存空间都占满的话,空间利用率也高。建立顺序表之前需要向内存申请一块连续的内存空间,但是这块内存空间我们并不一定能够存满,这就造成了顺序表的空间利用率一般比链表低。
  • 顺序表的单个存储单元只带有数据,而链表的单个存储单元不仅带有数据,还带有一个指针。在存储相同数据的时候,内存还需要额外划分一块内存空间用来给链表存放指针。因此对于单个存储元素来说,顺序表的空间利用率要比链表的高一些。
    数据结构(1)_第8张图片

(五)插入和删除

(1)链表的插入删除

单链表的插入,在链表中的某一处插入:
数据结构(1)_第9张图片
删除单链表中的某一个结点:
数据结构(1)_第10张图片
插入结点的特殊情况,在链表的第一个元素之前插入一个结点,这里就涉及到头结点的好处了,使用头结点的好处就是尽管是在第一个元素前插入结点,也跟从链表中间插入元素的做法是一样的。
数据结构(1)_第11张图片
而不使用头结点插入结点的话,在链表的第一个元素之前插入一个结点跟从链表中间插入元素的做法有点不一样。(其实差不多)
数据结构(1)_第12张图片
删除结点也一样,使用头结点的话,删除链表的第一个结点跟从中间删除结点的操作是一样的。而不使用头结点的话,就有点不太一样了。
数据结构(1)_第13张图片
双链表结点的插入:
数据结构(1)_第14张图片
双链表结点的删除:
数据结构(1)_第15张图片

(2)顺序表的插入删除

数据结构(1)_第16张图片
顺序表插入元素的代码,使想要插入位置处的元素及其后面的元素从后面开始一个一个往后移:
数据结构(1)_第17张图片
顺序表删除元素,把想要删除的元素后面的元素一个一个往前移,覆盖想要删除的元素即可,清空顺序表把length直接置为0即可,虽然此时数组中的元素还存在,但是这个顺序表是不可见的:
数据结构(1)_第18张图片
顺序表删除元素代码,参数p表示要删除元素的位置,参数e表示要删除元素的值。这里注意判断删除操作不合法的条件不用写length == 0,因为这个条件已经包含在判断条件内了:
数据结构(1)_第19张图片

(六)建表

(1)顺序表建表

顺序表建表代码:

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;
}

(2)链表建表

链表建表实际上就是插入结点的一系列操作,如果对结点插入的位置没有次序要求的话,一般只在链表的头部位置或尾部位置进行插入,即头插法和尾插法两种。

尾插法建表代码,考研题一般都是在内存足够的情况下,方法无需返回标志说明建表失败的情况,因此返回值定义为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;

	}

真题:
数据结构(1)_第20张图片
代码,头插法:

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;
		}
	}	
}

(七)划分

以线性表的某个元素为界限将线性表划分为两个部分,这个元素称为枢轴。有三种题型。

(1)第一种题型

给定一个顺序表,以顺序表的第一个元素为枢轴,将顺序表划分为左右两部分,使得顺序表左边的元素都小于枢轴,右边的元素都大于枢轴,并且枢轴要夹于左右两部分元素之间。

例子,有这么一个数组,枢轴为2,我们借助临时变量temp以及存储数组头部和尾部的下标的两个变量i和j来完成划分。
划分的过程是:

  1. 左移j,当j对应的元素小于枢轴的时候,将j对应的元素赋值给i对应的元素。然后j停止运动,
  2. i右移,当i对应的元素大于枢轴的时候,将i对应的元素赋值给j对应的元素,然后i停止运动。
  3. 重复以上两个步骤,当i跟j对应的元素是同一个的时候,用枢轴覆盖这个元素,完成划分。

数据结构(1)_第21张图片
代码:

//参数分别为数组、数组的长度
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;
}

(2)第二种题型

上述代码进行比较都涉及到枢轴temp,假如新增一个变量comp,将每次比较的值都换成新变量comp,则最终顺序表会划分为两部分,一部分小于变量comp,一部分大于变量comp。也就是顺序表以新变量为标准进行了划分。
数据结构(1)_第22张图片
数据结构(1)_第23张图片
尽管用来比较的元素存在于顺序表中,它跟分界线的位置都无关,当用来比较的元素比枢轴小的时候,分界线在枢轴的左边,当用来比较的元素比枢轴大的时候,分界线在枢轴的右边。而当用来比较的元素跟枢轴相等的时候,分界线就在枢轴处,也就是一开始的那种情况了。
数据结构(1)_第24张图片
数据结构(1)_第25张图片

(3)第三种题型

前两种题型都是以顺序表的第一个元素作为枢轴,第三种题型是以顺序表中的任意以个元素作为枢轴进行划分。其实也是前两种题型的变形,只要将选中的那个元素与顺序表的第一个元素交换位置,则变为了第一种题型了。

代码为:k是数组中任意一个元素的下标
数据结构(1)_第26张图片

(八)移动次数计算和静态链表

(1)移动次数计算

只有顺序表才会涉及到元素移动次数的计算。顺序表的元素插入和删除操作会使元素移动。
数据结构(1)_第27张图片
数据结构(1)_第28张图片
数据结构(1)_第29张图片

(2)静态链表

对于某种存储结构,其存储空间如果是一次性分配就叫静态分配,多次就叫动态分配。顺序表的存储空间一般都是静态分配的,而链表的存储空间一般都是动态分配的、是根据需要再进行分配的。

这里要讲的静态链表的存储空间是静态分配的,也就是我们给它分配了一个顺序表,然后在这一片顺序存储空间通过某种方法实现一种类似于链表的存储结构。

在考研数据结构范围内,常常是给定一个建好的静态链表,然后对其存储过程中的一些细节进行考察。
数据结构(1)_第30张图片
静态链表的实现,创建了一个静态链表(数组),将线性表存储到静态链表中,每个结点不是挨着的,而是分散的,并且每个结点都存储着下一个结点所在位置的下标:
数据结构(1)_第31张图片
静态链表的操作:
数据结构(1)_第32张图片
数据结构(1)_第33张图片

(九)归并

这里只讲二路归并,就是把两个线性表合成一个线性表。

(1)顺序表归并

第一种是将两个有序的顺序表进行有序的归并:
数据结构(1)_第34张图片
两个变量i和j分别存储两个顺序表第一个元素的下标,然后开始进行归并。

  1. 将i和j所对应的元素进行比较,把小的元素取出来放到另外一个线性表的第一个位置,然后指向这个元素的变量右移。
  2. 重复第一个步骤,直至有某一个顺序表的元素全部都被取出,则将剩下的那个顺序表的元素全部取出放到新的线性表的后面。

代码:

//传入三个数组,(两个初始数组,一个结果数组),以及两个顺序表的长度
//这里返回值为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++]; 
}

(2)链表归并

顺序表的归并需要另外开辟一块存储空间来进行归并,而链表不需要。链表归并跟顺序表相似。

归并两个升序链表:

定义两个指针分别指向两个有序链表的第一个结点。
数据结构(1)_第35张图片

  1. 取两个链表中任意一个头结点。
  2. 比较两个指针所指的结点的data域,值小的结点链接到头结点上,然后指向这个结点的指针移向下一个结点。
  3. 重复第二个步骤,当某一链表的值取完了之后,将剩下的链表直接连接到新链表的后面即可。

代码:

//传入三个链表的头指针,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;
	}
}

这样我们就能得到一条归并后的升序链表:
数据结构(1)_第36张图片

假如要得到降序的链表,也跟上面的步骤差不多,但是区别在于每次比较之后,把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;
}

能够发现,归并后得到升序链表的过程类似于链表插入的尾插法,归并后得到降序链表的过程类似于头插法。

(十)逆置

(1)顺序表的逆置

顺序表的逆置只需要定义三个变量,分别是临时变量temp、指向顺序表第一个元素的位置的变量i和指向顺序表最后一个元素的位置的变量j。然后将第一个元素和最后一个元素借助临时变量交换位置,交换完之后i加1,j减一,然后再进行交换,直至i>=j不再进行交换,逆置结束。
数据结构(1)_第37张图片
数据结构(1)_第38张图片
数据结构(1)_第39张图片

数据结构(1)_第40张图片

(2)链表的逆置

一般考题会以以下的形式,给出一个p指针和q指针分别指向某一个结点,然后要求逆置p指针后继结点到q指针指向的结点之间的结点。
在这里插入图片描述
逆置的过程为:
重复操作,将p的后继结点移动到q指向的结点的后面。结束条件为p->next == q。
数据结构(1)_第41张图片
数据结构(1)_第42张图片
数据结构(1)_第43张图片

(3)真题举例

这是顺序表逆置操作的真题,链表操作的比较简单,按照上面的代码去做即可。
数据结构(1)_第44张图片
解题的思路是将前k个(这里演示前4个)与后k个按照逆置的方法分别两两交换位置
数据结构(1)_第45张图片
数据结构(1)_第46张图片
对应的代码,i只需要扫描前k个位置即可,这里条件除了i 数据结构(1)_第47张图片
数据结构(1)_第48张图片
解题思路为将前k个元素逆置,再将这k个元素与后k个元素两两交换位置,也就是调用上一题定义的函数。
数据结构(1)_第49张图片

数据结构(1)_第50张图片
数据结构(1)_第51张图片
代码:
数据结构(1)_第52张图片

数据结构(1)_第53张图片
循环左移p个位置,即把0到p-1位置上的元素保持原序移到了数组的后端,而p到n-1位置的元素保持原序移动到了数组的前端。这个题跟第二题的区别在于数组的前半段跟后半段都需要保持原序。

解题的思路是将前p个元素逆置,然后把p到n-1位置的元素逆置,再把整个数组逆置即可。
数据结构(1)_第54张图片
数据结构(1)_第55张图片
数据结构(1)_第56张图片
数据结构(1)_第57张图片

代码:
数据结构(1)_第58张图片

(十一)取最值

(1)基础代码

顺序表中取最大值的代码:
数据结构(1)_第59张图片
顺序表中取最小值的代码:
数据结构(1)_第60张图片
链表中取最大值的代码:
数据结构(1)_第61张图片
链表中取最小值的代码:
数据结构(1)_第62张图片

(2)真题

数据结构(1)_第63张图片
定义结点结构体:
数据结构(1)_第64张图片

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;
	
}

三、栈和队列

(一)栈

栈是只能在一端进行插入或删除操作的线性表。栈的逻辑结构属于线性表,只不过在操作上加了一些约束。可以插入和删除元素的一端叫栈顶,另一端叫栈底。具有先进后出的特性。栈的存储结构有两种,分别是顺序栈和链栈。

(1)顺序栈

顺序栈最简单的定义:
数据结构(1)_第65张图片
出栈操作不会删除数组中的元素,但是出栈的元素已经不属于栈了,栈的元素范围是0到top。
数据结构(1)_第66张图片
栈满或栈空的条件:
数据结构(1)_第67张图片

(2)链栈

数据结构(1)_第68张图片
元素入栈,让top指针指向新建的结点(即栈顶元素的结点),然后将这个结点插入到头结点之后:
数据结构(1)_第69张图片
元素出栈,定义一个变量接收出栈元素的数据,然后将头结点与下一个元素相连,然后让top指针指向新的栈顶元素:
数据结构(1)_第70张图片
栈空栈满的条件:
数据结构(1)_第71张图片

(二)队列

(1)顺序队

队列是一种插入元素只能在一端进行,删除元素只能在另一端进行的线性表。队列的逻辑结构属于线性表,只不过在操作上增加了一些约束,可以插入元素的一端叫队尾,可以删除元素的一端叫队头。具有先进先出的特性。

队列也具有两种存储结构,分别是顺序队和链队。
顺序队的定义,定义一个数组和两个变量,这两个变量分别指向队头的元素和队尾的元素:
数据结构(1)_第72张图片
顺序队入队,先移动队尾指针(其实只是一个整型变量,存储数组的某一个元素的下标)至下一位,然后将元素存储到队尾指针指向的位置:
数据结构(1)_第73张图片
顺序队出队,将队头指针移动到下一位,然后将队头指针指向的变量赋值给变量x。元素出队之后,它的值仍然存留在数组中(这里为了说明这个元素不属于队列把它删除了),:

数据结构(1)_第74张图片
可以发现队头指针指向的元素的下一个元素才是队列的第一个元素,队尾指针总是指向队列的最后一个元素。

当队尾指针指向了数组的最后一个元素,而队头指针并不指向数组的第一个元素时,此时再往队列增加元素的话会报数组越界异常,但是数组的存储空间并没有占满,这种现象称为假溢出。
数据结构(1)_第75张图片
解决假溢出的办法是,当队尾指针指向最后一个元素时,还想要往队列添加元素的话,要将队尾指针重新指向数组的第一个元素,队头指针也是,到达了最后一个元素之后,要重新指向数组的第一个元素。可以将其想象成一个圆环。
数据结构(1)_第76张图片
利用以下代码能够实现圆环的效果,解决假溢出,由于这种类似圆环的效果,顺序队也称为循环队列:
数据结构(1)_第77张图片
当队头指针跟队尾指针指向同一个元素的时候,代表队列空了(这只是一个规定,具体还要看题目)
数据结构(1)_第78张图片
当队尾指针往后移一位之后与队头指针指向同一个元素的时候,这个时候我们把它称为队满的状态。由于是类似环状的存储结构,还需要在判断条件加上一个%maxSize。之所以不让数组把元素放满是因为这个时候队头指针跟队尾指针都指向同一个元素,跟队空无法进行区分。
数据结构(1)_第79张图片

(2)链队

链队不存在假溢出的情况,也不存在队满的情况,比较简单,只需要一条普通的单链表在加上队头指针跟队尾指针。队头指针指向头结点,队尾指针指向链表的最后一个结点。
数据结构(1)_第80张图片
元素入队就是在队尾指针rear指向的结点后面插入一个新的结点,并让队尾指针指向这个新结点。元素出队就是删除这个链表的第一个结点。队空的条件就是头结点的next指针为NULL。

考研题经常将队头指针跟队尾指针包裹在一个结构体内,并且用来替代头结点,这种情况队空的条件就是front指针为NULL:
数据结构(1)_第81张图片

(三)输出序列问题

给定一堆数据按照某种顺序入栈,然后问一些和出栈顺序有关的问题。

(1)由出栈序列判断容量问题

根据不同的入栈和出栈的操作次序,会导致所需的栈容量不同。
数据结构(1)_第82张图片
数据结构(1)_第83张图片
简单的可进行模拟:
数据结构(1)_第84张图片
可以发现所需的容量至少为3。
数据结构(1)_第85张图片
题目也有可能不告诉出栈序列,可能会说让出栈序列压入到队列后出队的序列是什么,其实也等同于出栈序列。
数据结构(1)_第86张图片
或者将出栈序列改为出栈后再入栈再出栈的序列。也就是出栈序列的逆序。
数据结构(1)_第87张图片

(2)常见题型

数据结构(1)_第88张图片
2.
数据结构(1)_第89张图片
3. i、j、k为升序序列中的三个元素,使用穷举法将所有的可能结果都列出来,然后再一一进行判断结果可不可行。由于i 数据结构(1)_第90张图片
将pi、pj、pk放回到入栈序列进行讨论,假如能够得到正确的出栈序列,那这种结果就是正确的:
数据结构(1)_第91张图片
发现只有第四种结果是不可能的:
数据结构(1)_第92张图片
可以发现结论:
如果最后一个入栈的元素第一个出栈,其余元素的入栈顺序就是固定的(这个结论在p1=n也体现出来了)(若1、2、3,则无3、1、2)。

(3)Catalan number(卡特兰数)(记)

数据结构(1)_第93张图片

(四)表达式转换

表达式转换适合选择题、填空题、简单题。

同一组操作数、同一组运算符,它们有不同的表达式形式,有三种表达式:
数据结构(1)_第94张图片

(1)中缀表达式转前缀表达式

将a和b两个操作数视为两个表达式,用括号将表达式括起来,然后把运算符提取到前面,再把括号去掉,就能从中缀表达式转换成前缀表达式。
数据结构(1)_第95张图片
稍微复杂的也是一样,把表达式一个一个括起来,但是不改变它们原先的运算顺序,再将运算符提取到括号前,然后去掉括号。
数据结构(1)_第96张图片
数据结构(1)_第97张图片
数据结构(1)_第98张图片
数据结构(1)_第99张图片

(2)中缀表达式转后缀表达式

跟中缀表达式转前缀表达式是类似的,不同点只在于把运算符提取到括号的后边。
数据结构(1)_第100张图片
数据结构(1)_第101张图片
数据结构(1)_第102张图片

为什么可以丢掉括号?
对于一个中缀表达式,通过给它加括号导致不同的运算次序,每一种运算次序都会有唯一的后缀表达式与之对应。
数据结构(1)_第103张图片

(3)后缀表达式转中缀表达式

只要将中转表达式转成后缀表达式的过程逆过来就可以了。将操作数视为表达式,寻找两个表达式紧跟一个运算符的地方,然后将运算符提到中间再用括号括起来,重复操作后去掉多余的括号即可。
数据结构(1)_第104张图片

(4)后缀表达式转前缀表达式

把操作数视为表达式,找到两个表达式紧跟一个运算符的地方,先把运算符提到前面再用括号括起来。重复操作最后再将括号去掉即可。
数据结构(1)_第105张图片
数据结构(1)_第106张图片

(五)配置问题

对栈和队列的代码设计能够有多种多样,只要能够实现它们的特性即可。这里不同的设计称为不同的配置,对队列的配置问题考察的比较多,对栈的配置问题考察的比较少。这里对队列的不同配置延伸出来的问题进行讨论。这里主要针对顺序队(循环队列),因为链队很难出不同的考题。

(1)顺序队的正常配置

往年考题中出现的默认的配置称为正常的配置。也就是题目不告诉你队列的信息就让你说出队列的一些特性等,这这种队列为默认的配置,也就是正常配置。而不正常配置题目一般会暗示或明示。配置也就是队空、队满、入队、出队在不同的规定下体现的形式。
数据结构(1)_第107张图片
数据结构(1)_第108张图片
数据结构(1)_第109张图片
正常配置下考察最多的是元素个数计算:
数据结构(1)_第110张图片
数据结构(1)_第111张图片
数据结构(1)_第112张图片
可考题型:

  1. 给定front和rear指针,求再添加几个元素队满
  2. 经过各种入队和出队操作,front和rear指针指在哪些位置

(2)顺序队的非正常配置

这里给出两种考过的非正常配置。

①第一种非正常配置

队空条件不变:
数据结构(1)_第113张图片
入队操作变为先插入元素再移动指针
数据结构(1)_第114张图片
出队操作也是先删除元素再移动指针:
数据结构(1)_第115张图片
队满条件也不变:
数据结构(1)_第116张图片
这种非正常配置跟正常配置的区别在于入队和出队的操作次序不一样。导致的结果就是队头指针front指向队列的第一个元素,而队尾指针rear指向队列的最后一个元素之后的一个位置。恰好跟正常配置相反。

计算元素个数的公式仍然是一样的:
数据结构(1)_第117张图片数据结构(1)_第118张图片
数据结构(1)_第119张图片
可考题型:

  1. 给定非正常配置的队列的队头指针跟队尾指针,计算元素个数
  2. 通过暗示给定这种非正常配置的队列,front和rear指针指向哪
②第二种非正常配置

第二种非正常配置的队空情况有所不同,条件跟前面两种配置的队满条件是一样的。
数据结构(1)_第120张图片
入队操作,可以是先入队元素再移动指针,也可以是先移动指针再入队元素,这里以先移动指针再入队元素为例。
数据结构(1)_第121张图片
出队操作,先出队元素再移动指针。
数据结构(1)_第122张图片
队满条件,此时队头指针跟队尾指针距离为2:
数据结构(1)_第123张图片
计算队列元素个数的公式也有点小区别:
数据结构(1)_第124张图片
数据结构(1)_第125张图片
公式跟前面两种配置相差一个整数1
数据结构(1)_第126张图片

(六)用栈实现表达式转换

前面所讲的表达式转换不适合用代码表示,利用栈来实现表达式转换就能很好得用代码表示了。

(1)中缀表达式转后缀表达式

  1. 先画一个栈,从左到右扫描中缀表达式,遇到操作数就直接写出来,遇到运算符就入栈。
  2. 在入栈之前需要将运算符与栈顶运算符的优先级进行比较,如果当前扫描到的运算符的优先级小于或等于栈顶运算符的优先级的话,就把栈顶运算符出栈,并将其写入到当前所得到的结果表达式中。
  3. 然后扫描的运算符继续跟新的栈顶运算符进行比较,栈顶运算符仍小于等于的话,出栈并写到结果表达式中,直至优先级大于栈顶运算符或栈空的时候才入栈。
  4. 当扫描中缀表达式遇到左括号的时候直接入栈,然后继续扫描,当栈顶元素是括号的时候直接入栈,直至扫描到右括号。
  5. 当扫描到右括号的时候,将栈顶到左括号的元素全部出栈,并且将除了括号外的元素都写到结果表达式中。
  6. 最后当扫描完中缀表达式中的所有字符的时候,若栈中还有运算符,将其全部出栈并写入结果表达式中。
    数据结构(1)_第127张图片

可考题型:

  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--];
}

(2)中缀表达式转前缀表达式

中缀表达式转前缀表达式跟中缀表达式转后缀表达式几乎一样,不同点在于:

  1. 扫描中缀表达式是从右往左扫描,并且遇到右括号入栈,遇到左括号出栈。
  2. 扫描到的运算符与栈顶运算符的比较条件改为小于,小于则栈顶运算符出栈。
  3. 结果表达式从右往左写。
    数据结构(1)_第128张图片
    代码实现:
//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--];
}

(3)后缀表达式转前缀表达式

  1. 从左到右扫描后缀表达式
  2. 遇到字符则入栈,每次扫描到运算符的时候出栈前两个字符,将这两个字符连接起来(按照入栈的顺序排列)并把运算符放在前面。然后把一整个整体入栈。
  3. 重复操作
    数据结构(1)_第129张图片
    数据结构(1)_第130张图片
    数据结构(1)_第131张图片

(4)前缀表达式转后缀表达式

  1. 从右往左扫描,遇到字符则入栈
  2. 遇到运算符出栈前两个字符,将这两个字符连接起来(按照出栈的顺序排列)并把运算符放在后面。然后把一整个整体入栈。
  3. 重复操作即可

(七)用栈实现表达式求值

(1)用栈求中缀表达式的值

用栈求中缀表达式的值需要两个栈s1和s2,一个用来存储操作数,一个用来存储运算符。

  1. 从左到右扫描中缀表达式,当遇到操作数的时候入s1栈,遇到左括号的时候入s2栈。
  2. 遇到运算符的时候对s2栈栈顶元素进行判断,如果s2栈栈顶元素为左括号或者s2栈为空则运算符直接入s2栈。
  3. 如果s2栈不空且s2栈的栈顶元素不为左括号的话,也就是栈顶元素为运算符的情况下,如果扫描到的运算符的优先级大于栈顶运算符的优先级的话则入s2栈,否则出栈栈顶运算符,并且出栈一个运算符的时候同时从s1栈中出栈两个操作数,然后拿这两个操作数和运算符按操作数入栈的顺序进行一次运算然后再压入s1栈。直至扫描到的运算符的优先级大于栈顶运算符的优先级或者栈空。
  4. 继续扫描中缀表达式。如果遇到右括号的话,把s2栈从栈顶到左括号的所有元素都出栈,并对每一个运算符都从s1栈取两个操作数进行运算。最后将括号扔掉。
  5. 当整个中缀表达式都扫描完之后,s2栈依然有运算符,就将所有的运算符都出栈,并将每一个运算符都进行一次前述的运算。
  6. 最后在s1栈的栈底就是整个表达式的运算结果了。
    数据结构(1)_第132张图片
    数据结构(1)_第133张图片
    数据结构(1)_第134张图片
    代码实现:
//判断运算符优先级
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];
}

(2)用栈求后缀表达式的值

数据结构(1)_第135张图片

  1. 从左往右扫描,当遇到操作数的时候就入栈,到遇到运算符的时候就出栈两个操作数结合运算符按入栈的顺序进行运算。
  2. 运算的结果重新入栈,然后继续扫描表达式。

数据结构(1)_第136张图片
数据结构(1)_第137张图片
代码实现:

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];
}

(3)用栈求前缀表达式的值

用栈求前缀表达式的值跟用栈求后缀表达式的值过程是十分类似的,它们的过程是反过来的。

  1. 从右往左扫描,遇到操作数就入栈,遇到运算符就出栈两个操作数按照出栈的顺序进行运算。
  2. 运算的结果重新入栈,然后继续扫描表达式。

数据结构(1)_第138张图片
数据结构(1)_第139张图片
代码实现:

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. 给一个表达式,写出栈的变化情况。

(八)双端队列

队列的两端都可以入队和出队元素。对双端队列的一端做出限制,能出现很多种情况的队列。
数据结构(1)_第140张图片
数据结构(1)_第141张图片
考研中涉及的考点题:
数据结构(1)_第142张图片
首先看第一问,首先封锁一端,求出栈形态下不可能输出的序列的总数目。
数据结构(1)_第143张图片
然后将解放封锁一端的输出,使用穷举法列出栈形态下不可能输出的序列,然后再一个个去进行排除,得到输入受限的双端队列不可能输出的序列。(考题可能会出选择题或简单题、列出几组序列让你判断哪几组可能哪几组不可能)
数据结构(1)_第144张图片
I代表插入,OL代表从左端出队,OR代表从右端出队。
数据结构(1)_第145张图片
数据结构(1)_第146张图片
一个一个进行尝试,发现4、2、1、3序列和4、2、3、1是不行的。

然后看第二问,方法同上:
数据结构(1)_第147张图片
数据结构(1)_第148张图片
数据结构(1)_第149张图片
一个一个进行尝试、发现4、1、3、2和4、2、3、1是不行的。

第三问,就是一二问求结果的交集。

(九)栈的扩展

(1)共享栈

两个栈共享同一片存储空间,两个栈的栈底在存储空间的两端。
数据结构(1)_第150张图片
共享栈如何实现,相比普通栈只是增加了一个栈顶指针:
数据结构(1)_第151张图片
为了表示这两个栈顶指针是属于同一个共享栈的,我们一般定义一个数组来存放这两个指针:
数据结构(1)_第152张图片
判断栈空的条件:
数据结构(1)_第153张图片
如何入栈:
数据结构(1)_第154张图片
栈满的条件:
数据结构(1)_第155张图片

(2)用栈模拟队列

一般使用两个存储空间大小一样的栈,先把元素入栈s1,然后出栈再入栈到s2,这样当s2元素出栈的时候就能模拟队列先进先出的特性了。

用栈模拟队列要遵守以下规则,否则会出错:
数据结构(1)_第156张图片

(十)队列的扩展

(十一)括号匹配和计算

(1)括号匹配

考察在编译器中对括号检测技术的了解。给一段待编译的代码,可能涉及到很多括号,哪些括号出现的情况是匹配的,哪些括号出现的情况是不匹配的,要能成功检测出来。如何检测出来需要知道什么样是匹配的情况什么样是不匹配的情况。
数据结构(1)_第157张图片
那么如何来判断括号匹不匹配呢?这需要用到栈。

  1. 从左至右扫描一个表达式,如果遇到左括号就入栈,如果遇到右括号就出栈栈顶括号。
  2. 如果栈空的说话说明括号不匹配。
  3. 如果栈不为空则检测刚出栈的括号与扫描到的括号匹不匹配。如果匹配则继续扫描下一个,否则说明括号不匹配。
  4. 当表达式扫描完毕之后,如果栈中还有括号的话,则说明括号不匹配。否则说明括号匹配。

代码实现:

//定义一个方法判断括号是否匹配
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;
}

(2)计算问题

最常见的题型的形式,op为运算符
数据结构(1)_第158张图片
给出例子:
把m>0的式子一层一层展开:
数据结构(1)_第159张图片
把每次展开得到的因子压入栈中,实际上这个函数的解决并不需要用到栈来解决,直接将展开后所得的因子相乘即可。之所以运用栈来解决这个问题,是因为这个问题用递归来解决最清晰明了,而递归内部就是根据栈来实现的。这里就是用自己建立的栈来模拟递归内部的实现。可能会遇到考题说让你用栈来解决这种问题。
数据结构(1)_第160张图片
代码实现:

//参数一般都是整数
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;
}

你可能感兴趣的:(数据结构,数据结构)