计算机考研数据结构---栈和队列

**

第三章 栈和队列

**

一、栈的基本概念

1.1 栈的定义

栈(Stack)是只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
空栈:不含任何元素的空表。
栈顶:线性表允许进行插入或删除的那一端。
栈底:固定的,线性表不允许进行插入和删除操作的一端。
假如某个栈S={a1,a2,a3,a4,a5},a1为栈底元素,a5为栈顶元素。进栈次序a1->a2->a3->a4->a5,出栈次序:a5->a4->a3->a2->a1。特点就是:后进先出。Last In first Out(LIFO)

1.2 栈的基本操作

InitStack(&S):初始化一个空栈S。
DestroyStack(&L):销毁栈,并释放内存空间。
Push(&S,x):进栈,若栈S未满,将x加入成为新的栈顶。
Pop(&S,&x):出栈,弹出栈顶元素,并用x返回。
GetTop(S,&x):读取栈顶元素,用x返回。
StackEmpty(S):判断是否为空栈,空返回true,否则返回false。

n个不同元素进栈,出栈元素不同排列的个数为1/(n+1)*Cn2n(卡特兰数)

二、栈的顺序存储结构

2.1 顺序栈的实现

采用顺序存储的栈称为顺序栈,利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。

#define MaxSize 10
typedef struct
{
	ElemType data[MaxSize];//存放栈中元素
	int top;//栈顶指针
}SqStack; //Sq--sequence 顺序
void main()
{
	SqStack S;//声明一个顺序栈
}

初始化时由于栈没有存储元素,所以栈顶指针设置为-1。

//初始化栈
void InitStack(SqStack &S)
{
	S.top=-1;//初始化栈顶指针
}

判断栈是否为空

//判空操作
bool StackEmpty(SqStack S)
{
	if(S.top==-1)//栈空
		return true;
	else 
		return false;
}

进栈操作

//新元素进栈
bool Push(SqStack &S, ElemType x)
{
	if(S.top==MaxSize-1)//栈满
		return false;
	//指针先加1
	S.top++;
	//新元素入栈
	S.data[S.top]=x;//等价于 S.data[++S.top]=x;	
	return true;
}

出栈操作

//出栈操作
bool Pop(SqStack &S,ElemType &x)
{
	if(S.top==-1)//栈空
		return false;
	//栈顶元素出栈
	x=S.data[S.top];
	//指针减1
	S.top--;//等价于 x=S.data[S.top--];
	return true;
}

读取栈顶元素

//读取栈顶元素 
bool Pop(SqStack &S,int &x)
{
	if(S.top==-1)//栈空
		return false;
	//读取栈顶元素
	x=S.data[S.top];
	return true;
}

注意:另外一种情况 初始化时top=0;

//初始化栈
void InitStack(SqStack &S)
{
	S.top=0;//初始化栈顶指针
}

top指针不再指向栈顶元素,而是指向可以插入的下一个元素。
入栈操作为数据元素先入栈,top指针再+1。
S.data[S.top]=x;
S.top=S.top+1;
出栈:
S.top=S.top-1;
x=S.data[S.top];

2.2 共享栈

利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一位数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,top1=MaxSize时1号栈为空;两个栈顶指针相邻时top0+1==top1,栈满。当0号栈进栈时top0先加1再赋值,top1先减1再赋值,出栈时相反。(top0先出栈在减1,top1先出栈再加1)。

//共享栈
#define MaxSize 10//定义栈中元素的最大个数
typedef struct
{
	int data[MaxSize];//静态数组存放栈中元素
	int top0;//0号栈栈顶指针
	int top1;//1号栈栈顶指针
}ShStack;
//初始化
void InitStack(ShStack &S)
{
	S.top0=-1;
	S.top1=MaxSize;
}
//栈满的条件
top0+1==top1;

三、栈的链式存储

采用链式存储的栈称为链栈。
采用链式存储,便于结点的插入和删除。链栈的操与链表类似,入栈和出栈的操作都在链表的表头进行。

//链栈的定义
typedef struct Linknode
{
	ElemType data;//数据域
	struct Linknode *next;//指针域
}*LiStack;

四、队列的基本概念

4.1 队列的定义

队列(Queue)简称对,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队或进队;删除元素称为出队或离队。操作特性是先进先出(First In First OUT,FIFO)。
对头:允许删除的一端,或対首。
队尾:允许插入的一端。
空队列:不含任何元素的空表。

4.2 队列的基本操作

InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列,销毁并释放队列Q所占用的内存空间。
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除对头元素,用x返回。
GetHead(Q,&x):读对头元素,若队列Q非空,则将对头元素赋给x。
QueueEmpty(Q):队列判空。

4.3 队列的顺序实现

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:对头指针front指向对头元素,队尾指针rear指向队尾元素的下一个位置。(有的队尾指针是指向队尾元素的,需要注意)

#include
#define MaxSize 10
typedef struct
{
	int data[MaxSize];
	int front,rear;//队头指针和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue &Q)
{
	//初始时,队头 队尾指针指向0
	Q.rear=Q.front=0;
}
void main()
{
	SqQueue Q;//声明一个顺序队列
}

判空操作:

//判空操作
bool QueueEmpty(SqQueue Q)
{
	if(Q.rear==Q.front)//队空条件
		return true;
	else
		return false;
}

4.4 循环队列

能否使用Q.rearMaxsize;作为队列满的条件?
不能,如果先入队列,到Q.rear
Maxsize时再出队,仅留一个元素时,任然满足对满的条件,此时在入队会出现“上溢出”,但这种溢出是假溢出,data数组中任然可以存放数据。
所以采用循环队列,将循序队列逻辑上成为一个环状的空间,即把存储队列元素的表从逻辑上视为一个闭合的环路,称为循环队列。
入队操作:

//入队
bool EnQueue(SqQueue &Q,int x)
{
	//判断栈满
	if((Q.rear+1)%MaxSize==Q.front)//对尾指针的下一个位置为对头指针
		return false;
	Q.data[Q.rear]=x;
	Q.rear=(Q.rear+1)%MaxSize;//队尾指针加1取模
	return true;
}

队尾指针的处理不能直接加1 而是Q.rear=(Q.rear+1)%MaxSize;
对满的条件判断不能直接Q.rearMaxSize;
而是(Q.rear+1)%MaxSize
Q.front 但这样子会牺牲一个存储单元。

出队操作:

//出队(删除一个对头元素,并用x返回)
bool DeQueue(SqQueue &Q,int &x)
{
	if(Q.front==Q.rear)
		return false;//队空报错
	x=Q.data[Q.front];
	Q.front=(Q.front+1)%MaxSize;//队头指针后移
	return true;
}

获取队头元素:

//获取队头元素的值,用x返回
bool GetHead(SqQueue Q,int &x)
{
	if(Q.front==Q.rear)
		return false;//队空报错
	x=Q.data[Q.front];
	return true;
}

4.5 判断队满/队空

方案一:队满条件:(Q.rear+1%MaxSize==Q.front;
        队列元素个数:(rear+MaxSize-front)%MaxSize

方案二:在定义时加入一个记录队列当前长度的size
#define MaxSize 10
typedef struct
{
	int data[MaxSize];
	int front,rear;
	int size;//队列当前长度
}SqQueue;

在初始化时:rear=front=0;size=0;插入成功时size++;删除成功size–。
队满条件为:sizeMaxSize;队空条件为:size0;此时rear指针和front都指向同一个位置。
方案三:定义时增加一个tag。初始化时rear=front=0;tag=0;m每次删除操作成功时,令tag=0,插入成功时令tag=1。队满条件:frontrear && tag1;队空条件:frontrear && tag0;
上述操作都是队尾指针rear指向队尾元素的后一个位置(应该直接插入的位置)

五、队列的链式实现

队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点(注意与顺序存储不同)。

5.1 队列链式初始化定义

队列的链式存储类型可描述为:

typedef struct//链式队列结点  定义一个一个的结点
{
	int data;
	struct LinkNode *next;
}LinkNode;
typedef struct//链式队列 定义两个指针
{
	LinkNode *front,*rear;//队列的队头指针和队尾指针
}LinkQueue;

带头结点的初始化

typedef struct LinkNode//链式队列结点  定义一个一个的结点
{
	int data;
	struct LinkNode *next;
}LinkNode;
typedef struct//链式队列 定义两个指针
{
	LinkNode *front,*rear;//队列的队头指针和队尾指针
}LinkQueue;

void InitQueue(LinkQueue &Q)
{
	//初始化时声明一个头结点 front、rear都指向头结点
	Q.front=Q.rear=(LinkNode *)malloc(sizeof(LinkNode));
	//头结点的next指针指向NULL
	Q.front->next=NULL;
	s
}
void main()
{
	LinkQueue Q;
	InitQueue(Q);
}

判空操作
//判空
bool IsEmpty(LinkQueue Q)
{
	if(Q.front==Q.rear)
		return true;
	else
		return false;
}

不带头结点的初始化:

void InitQueue(LinkQueue &Q)
{
	//初始化时font rear都指向NULL
	Q.front=NULL;
	Q.rear=NULL;
}
//判空
bool IsEmpty(LinkQueue Q)
{
	if(Q.front==NULL)
		return true;
	else
		return false;
}

5.2 入队操作

入队操作(带头结点)

//入队操作,新元素入队(带头结点)
void EnQueue(LinkQueue &Q,int x)
{
	//申请一个新结点s
	LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));
	s->data=x;
    s->next=NULL;
	//将新结点插入rear之后
	Q.rear->next=s;
	//修改表尾指针
	Q.rear=s;
}

入队操作(不带头结点)

//入队操作,新元素入队(不带头结点)
void EnQueue(LinkQueue &Q,int x)
{
	//申请一个新结点s
	LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));
	s->data=x;
	s->next=NULL;
	if(Q.front==NULL)//在空队列中插入第一个元素
	{
		Q.front=s;//队头队尾指针都指向第一个元素
		Q.rear=s;
	}
	else
	{
		Q.rear->next=s;
		Q.rear=s;
	}
}

5.3 出队操作

出队操作(带头结点):

//出队操作
bool DeQueue(LinkQueue &Q,int &x)
{
	if(Q.front==Q.rear)
		return false;//空队
//p结点指向头结点的下一个结点
	LinkNode *p=Q.front->next;
	//用x变量返回队头元素
	x=p->data;
	//修改头结点的next指针
	Q.front->next=p->next;
	//若此时是最后一个元素出队
	if(Q.rear==p)
		Q.rear=Q.front;//修改rear的指针
	free(p);
	return true;
}

出队操作(不带头结点)
//出队操作(不带头结点)
bool DeQueue(LinkQueue &Q,int &x)
{
	if(Q.front==Q.rear)
		return false;//空队
	//p指针指向此次出队的结点
	LinkNode *p=Q.front;
	//用x变量返回队头元素
	x=p->data;
	//修改front指针
	Q.front->next=p->next;
	//此次是最后一个结点出队
	if(Q.rear==p)
	{
		Q.front=NULL;
		Q.rear=NULL;
	}
	free(p);
	return true;
}

5.4 链式队满条件

采用顺序存储时我们是预分配空间耗尽时队满,链式存储一般不会队满,除非内存不足。用链式表示的链式队列特别适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的问题。

六、双端队列

双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性的。将队列的两端分别称为前端和后端,两端都可以入队和出队。

在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。(入队a,b,c,d出队d,c,a,b)
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列。

输入受限的双端队列:允许在一端进行插入和删除,在另一端只允许删除的双端队列称为输入受限的双端队列。若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就是栈。

输入数据元素序列为1,2,3,4,则哪些输出序列是合法的?
4!=24
1,2,3,4 2,1,3,4 3,1,2,4 4,1,2,3
1,2,4,3 2,1,4,3 3,1,4,2 4,1,3,2
1,3,2,4 2,3,1,4 3,2,1,4 4,2,1,3
1,3,4,2 2,3,4,1 3,2,4,1 4,2,3,1
1,4,2,3 2,4,1,3 3,4,1,2 4,3,1,2
1,4,3,2 2,4,3,1 3,4,2,1 4,3,2,1

七、栈的应用

7.1 栈在括号匹配中的应用

假设我们带代码的编程中代码的括号不匹配就会报错。

( ( ( ( ) ) ) )
最后出现的左括号最先匹配(LIFO栈)。
( ( ( ) )( ) )每出现一个右括号,就消耗(出栈)一个左括号。遇到左括号就压入栈底,遇到右括号就弹出。
列如:{ ( ( ) ) [ ] }
1 2 3 4 5 6 7 8
1,2,3入栈,出现4,3出栈匹配,出现5,2出栈匹配,6入栈,7出现,6出栈匹配,8出现,1出栈匹配。
{ ( ( ) ] [ ] }
1 2 3 4 5 6 7 8
1,2,3入栈,4出现,3出栈匹配,5出现,2出栈不匹配。
扫描到右括号且空栈—匹配失败。处理完所有括号后,栈非空—失败。

#include
#include
#define MaxSize 10
typedef struct
{
	char data[MaxSize];//静态数组中存放栈中元素
	int top;//栈顶指针
}SqStack;
void InitStack(SqStack &S)
{
	//初始化栈顶指针
	S.top=-1;
	
}
//判断栈顶是否为空
bool StackEmpty(SqStack S)
{
	if(S.top==-1)
		return true;
	else 
		return false;
}
//新元素入栈
bool Push(SqStack &S,char x)
{
	if(S.top==MaxSize-1)//栈满
		return false;
	//指针先加1
	S.top++;
	//新元素入栈
	S.data[S.top]=x;
	return true;
}
//栈顶元素出栈,用x返回
bool Pop(SqStack &S,char &x)
{
	if(S.top==-1)
		return false;
	//栈顶元素出栈
	x=S.data[S.top];
	S.top--;
	return true;
}
bool bracketCheck(char str[],int length)
{
    SqStack S;//声明一个栈
	InitStack(S);//初始化栈
	for(int i=0;i<length;i++)
	{
		if(str[i]=='(' || str[i]=='[' || str[i]=='{')
		{
			Push(S,str[i]);//扫描到左括号 入栈
		}
		else
		{
			if(StackEmpty(S))//扫描到右括号且栈空
				return false;//匹配失败
			char topData;//存储栈顶元素
			Pop(S,topData);//栈顶元素出栈
			if(str[i]==')' && topData!='(')
				return false;
			if(str[i]==']' && topData!='[')
				return false;
			if(str[i]=='}' && topData!='{')
				return false;
		}
	}
	return StackEmpty(S);//检索完全部括号,栈空说明匹配成功
}
void main()
{
	char str[]={'(',')','(','(',')','}'};
	if(bracketCheck(str,6))
		printf("成功");
	else
		printf("失败");
}

用栈实现括号匹配:依次扫描所有字符,遇到左括号入栈,遇到右括号弹出栈顶元素检查是否匹配。
匹配失败情况:
1、左括号单独。2、右括号单独。3、左右不匹配。

7.2 栈在表达式求值中的应用

前缀表达式又称为波兰表达式,后缀表达式称为逆波兰表达式。
中缀表达式就是我们熟悉的,a+b 后缀为:运算符在两个操作数后面,ab+。 前缀为:运算符在两个操作数的前面,+ab。
中:a+b-c 后:ab+c-或 abc-+ 前:-+abc
中:a+b-cd 后: ab+cd- 前: -+ab*cd

中缀转后缀的手算方法:
1、确定中缀表达式中各个运算符的运算顺序
2、选择下一个运算符,按照【左操作数 右操作数 运算符】的方式组合成一个新的操作数
3、如果还有运算符没有被处理,继续执行2
( ( 15 / ( 7 - ( 1 + 1 ) ) ) * 3 ) - ( 2 + ( 1 + 1 ) )
3 2 1 4 7 6 5
1 2 3 4 5 6 7
1 5 7 1 1 + - / 3 * 2 1 1 + + -

A + B * ( C – D ) – E / F----中
ABCD-+EF/- ABCD-EF/-+ (先算除法 错)
左优先原则:只要左边的运算符能先计算,就优先左边的。
运算顺序不唯一,对应的后缀表达式也不唯一,但是引用左优先原则,可保证唯一。
A + B – C * D / E + F  AB+CD
E/-F+
后缀转换为中缀:
1 5 7 1 1 + - / 3 * 2 1 1 + + -
( ( 15 / ( 7 - ( 1 + 1 ) ) ) * 3 ) - ( 2 + ( 1 + 1 ) )
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数。注意操作数的左右顺序。
用栈实现后缀表达式的计算:
1、从左往右扫描下一个元素,直到处理完所有元素
2、若扫描到操作数则压入栈,并回到1;否则执行3
3、若扫描到运算符,则弹出两个栈顶元素(先出栈的是右操作数),执行相应的运算,运算结果压回栈顶,回到1.
若表达式合法,则最后栈中只会留下一个元素,就是最终的结果。
AB+CD
E/-F+
1 5 7 1 1 + - / 3 * 2 1 1 + + - 动手模拟

中缀转换为前缀:
1、确定中缀表达式中各个运算符的运算顺序。
2、选择下一个运算符,按照【运算符 左操作数 右操作数】的方式组合成为一个新的操作数
3、如果还有运算符没有被处理,就继续2
右优先原则:只要右边的运算符能先计算,就优先算右边的。
A+B*(C-D)-E/F -> +A-*B-CD/EF
( ( 15 / ( 7 - ( 1 + 1 ) ) ) * 3 ) - ( 2 + ( 1 + 1 ) )

    • / 15 - 7 + 1 1 3 + 2 + 1 1
      用栈实现前缀的计算:
      1、从右往左扫描下一个元素,直到处理完所有元素
      2、若扫描到操作数则压入栈,并回到1;否则执行3
      3、若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到1
      注意:先出栈的是左操作数。

用代码实现中缀转后缀:
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾,可能遇到三种情况:
1、遇到操作数。直接加入后缀表达式。
2、遇到界限符。遇到‘(’直接入栈;遇到‘)’则依次弹出栈内运算符并加入后缀表达式,直到弹出‘(’为止。注意:‘(’不加入后缀表达式。
3、遇到运算符。依次弹出栈中优先级(*/优先级高于±)高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到‘(’或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

7.3 栈在递归中的应用

void main()
{
	int a,b,c;
	...
	func1(a,b);
	c=a+b;
	....
}
void func1(int a,int b)
{
	int x;
	...
	func2(x);
	...
}
void func2(int x)
{
	...
}

函数调用的特点:最后被调用的函数最先执行结束(LIFO)。
递归是一种重要的程序设计方法。简单来说,若在一个函数、过程和数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。
适合递归算法解决:可以把原始问题转换为属性相同,但规模较小的问题。
Eg1:计算正整数阶乘n!
Factorial(n)=n*factorial(n-1),n>1  递归表达式(递归体)
1, n=1  边界条件(递归出口)
1, n=0
问题规模从n变为n-1,规模变小同时属性相同(都是计算阶乘)。
Eg2:求斐波那列

Fib(n)=Fib(n-1)+Fib(n-2),n>1
1, n=1
0, n=0
必须注意递归模型不能是循环定义的,必须满足下面的两个条件:
1.递归表达式(递归体)
2.边界条件(递归出口)
递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。

递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。而其效率不高的原因是递归调用过程中包含很多重复的计算。
Eg:
int Fib(int n)
{
if(n0)
return 0;
else if(n
1)
return 1;
else
return Fib(n-1)+Fib(n-1);
}
int main()
{

int x=Fib(4);
}

在递归调用中,Fib(1)被调用3次,Fib(2)被调用2次,Fib(0)被调用2次,所以递归的效率低下,但是优点是代码简单,容易理解。缺点可能包含很多重复计算。
Ps:函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个 函数调用栈 存储:
1、调用返回地址
2、实参
3、局部变量
函数调用时,函数调用栈可称为递归工作栈,每进入一层递归,就将递归调用所需信息压入栈顶;每退出一层递归,就从栈顶弹出相应的信息。
缺点:效率低,太多层递归可能导致栈溢出;可能包含很多重复的计算。

八、队列的应用

8.1 队列在层次遍历中的应用

在信息处理中有一大类问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层或当前行就对下一层或下一行做预处理,把处理顺序安排好,待当前层或当前行处理完毕,就可以处理下一层或下一行。
二叉树层序遍历的描述:
1、根结点入队
2、若队空(所有节点都已经处理完毕),则结束遍历;否则重复3操作
3、队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若有右孩子,则将右孩子入队,返回2.

图的广度优先遍历:

8.2 队列在计算机系统中的应用

多个进程争抢着使用有限的系统资源,FCFS(First Come First Service先来先服务)是一种常用策略。
Eg1:CPU资源的竞争的就是一个典型。操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把CPU分配给対首请求的用户使用。当相应的程序运行结束或用完规定的时间间隔后,令其出队,再把CPU分配给队首请求的用户使用。
Eg2:打印数据缓冲区。主机要把打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区域写入打印数据。

你可能感兴趣的:(计算机考研数据结构,计算机考研数据结构,栈和队列,栈的应用,队列的应用)