本节为栈与队列内容,回到总目录:点击此处
本章节目录
栈 LIFO(后进先出)
–将线性表的插入和删除运算限制只在表的一端进行 栈顶|栈底–
数据元素:可以是任意类型的数据,但必须属于同一个数据对象
关系:栈中数据元素之间是线性关系
基本操作:1. InitStack() 2.ClearStack() 3.IsEmpty() 4.IsFull()5. Push(S,X) 3.Pop(S,x) 7.GetTop(S,x)
栈在计算机中主要有两种基本的存储结构:顺序存储结构(顺序栈)|链式存储结构(链栈)
顺序栈
用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时由于栈的操作的特殊性,还必须附设一个位置指针top(栈顶指针)。 通常以 top = -1 来表示空栈
#define Stack_Size 50
typedef struct
{
StackElementType elem[Stack_Size];
int top; //栈顶元素的下标,top = -1 表示空栈
}SeqStack;
顺序栈的操作
·初始化栈
void InitStack(SeqStack *S)
{
S -> top = -1;
}
·进栈
int Push(SeqStack * S, StackElementType x)
{
if(S -> top == Stack_Size) return false;
S -> top ++;
S -> elem[S -> top] = x;
return true;
}
·出栈
int Pop(SeqStack *S, StackElementType *x)
{
if(S -> top == -1)
{
return false;
}
else
{
*x = S -> elem[S -> top];
S -> top --; //修改栈顶指针
return true;
}
}
·获取栈顶元素
int GetTop(SeqStack *S, StackElementType *x)
{
if (S -> top == -1) return false;
else
{
*x = S -> elem[S -> top];
return true;
}
}
链栈
用链表表示的栈为链栈,一般使用带头指针的单链表,链表的头指针一般就是栈顶指针。
top -> NULL 则表示该栈为空栈
注:在栈使用完毕后应该记得回收空间
typedef struct Node
{
StackElementType data;
struct node * next;
}LinkStackNode;
typedef LinkStackNode *LinkStack;
链栈的操作
·进栈
int Push(LinkStack top, StackElementType x)
{
LinkStackNode * temp;
temp = (LinkStackNode *)malloc(sizeof(LinkStackNode));
if (temp == NULL) return Flase;
temp -> data = x;
temp -> next = top -> next;
top -> next = temp;
return true;
}
·出栈
int Pop(LinkStack top , StackElementType *x)
{
LinkStackNode * temp;
temp = top -> next;
if(temp == NULL) return false //说明栈为空
top -> next = temp -> next;
*x = temp -> data;
free(temp);
return true;
}
多栈共享技术
·在顺序栈道共享技术中,最常用的是两个栈的共享技术,也就是双端栈。两个栈的栈底分别放在一维数组的两端(0,m - 1),由于两个栈顶是动态变化的,这样可以形成互补,使得每个栈可用的最大空间与实际使用情况有关,这样利用率更高。
#define M 100
typedef struct
{
StackElementType Stack[M]; //栈区
StackElementType top[2]; //top[0],top[1]栈顶指示器
}DqStack;
//双端栈的初始化
void InitStack(DqStack *S)
{
S -> top[0] = -1;
S -> top[1] = M;
}
//双端栈的进栈操作
int Push(DqStack *S, StackElementType x, int i)
{
//将元素x压入i号堆栈
if(S -> top[0] + 1 == S -> top[1]) return false //栈已满
switch(i)
{
case 0: //0号栈
S -> top[0] ++;
S -> Stack[S -> top[0]] = x;
break;
case 1: //1号栈
S -> top[1] --;
S -> Stack[S -> top[1]] = x;
break;
default:
return false;
}
return true;
}
//双端栈的出栈操作
int Pop(DqStack * S,StackElementType *x, int i)
{
switch(i)
{
case 0:
if(S -> top[0] == -1) return false;
* x = S -> Stack[S -> top[0]];
S -> top[0] --;
break;
case 1:
if(S -> top[1] == M) return false;
* x = S -> Stack[S -> top[1]];
S -> top[1] ++;
break;
default:
return false;
}
return true;
}
·链栈实现多栈运算,多栈运算//同时使用两个以上的栈实现操作/
#define M 10
typedef struct node
{
StackElementType data;
struct node * next;
}LinkStackNode,* LinkStack;
LinkStack top[M]; //用于指示M个链栈道栈顶指针。
栈的应用举例
括号匹配问题
void BracketMatch(char *str)
{
Stack S;
int i;
char ch;
InitStack(&S);
for(i = 0; str[i] != '\0' ; i++)
{
switch(str[i])
{
case '(':
case '[':
case '{':
push(&S,str[i]);
break;
case ')':
case ']':
case '}':
if(IsEmpty(&S))
{
cout << "右括号多余"; return;
}
else
{
GetTop(&S,&ch); //获取栈顶元素
if(Match(ch,str[i])) //判断是否括号可以匹配。
Pop(&S,&ch);
else
cout<<"对应的左右括号不同类"; return;
}
}
}
if (IsEmpty(S))
cout << "括号匹配";
else
cout << "左括号多余!";
}
表达式求值
任何一个表达式都是由运算对象(Operand)、运算符(Operator)、界限符(Delimiter)组成 ||
为了正确处理表达式,使用栈来实现正确的指令序列是一个重要的技术
【算法思想】 A/B↑C+D*E#
1、规定运算符的优先级表
2、设置两个栈:OVS(运算数栈),OPTR(运算符栈)
3、自左向右,进行处理:
遇到运算数则进入OVS栈,遇到运算符则与OPTR栈的栈顶运算符进行优先度比较:
·如果当前运算符优先度大于OPTR栈顶运算符优先级,则当前运算符进OPTR栈。
·如果当前运算符有限度小于OPTR栈顶运算符优先级,则OPTR退栈一次,得到栈顶运算符θ,连续退OVS栈两次,得到运算数a、b,执行a、b的θ操作,得到结果T(i),将T(i)进OVS栈。
int ExpEvaluation()
{
InitStack(&OPTR); //初始化两个栈
InitStack(&OVS);
Push(&OPTR,'#'); //便于操作先将#压入栈底
cout<<"输入一个表达式";
ch = getchar();
while(ch != '#' || GetTop(OPTR) != '#') //表示表达式结束
{
if(!In(ch,OPSet)) //不是操作符,是操作数,进OVS栈
{
n = GetNumber(ch); //字符转化
push(&OVS,n); //运算数压入OVS
ch = getchar();
}
else
switch(Compare(ch,GetTop(OPTR))) //比较运算符的优先级
{
case '>': //若输入的运算符比栈顶的优先级高
Push(&OPTR,ch); //则压入栈
ch = getchar();
break;
case '=':
case '<':
Pop(&OPTR,&op); //OPTR退栈一次
Pop(&OVS,&b); //OVS退栈两次
Pop(&OVS,&a); //取引用表示直接取他们的值
v = Execute(a,op,b); //执行a、b的θ操作
Push(&OVS,v); //并把获得的操作结果重新压回OVS栈
break;
}
}
}
栈与递归的实现
典例:汉诺塔问题:
void hanoi(int n, char x, char y, char z)
{
if(n == 1) move(x,1,z); //编号为1的圆盘从x移动到z
else
{
hanoi(n-1,x,z,y); //将x上编号为1~n-1的圆盘移动到y,z为辅助
move(x,n,z); //将第n个圆盘从x移动到z
hanoi(n-1,y,x,z); //将y上编号为1~n-1的圆盘移动到z,x为辅助
}
}
递归函数调用时,按照“后调用先返回”的原则处理调用过程。
系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数退出时,就释放它的存储区。//故此,当前正在运行的函数的数据区必须在栈顶
为保证递归函数正确执行,系统需设立一个 递归工作栈 -->>用作整个递归函数运行期间使用的数据存储区
工作记录 -->>每层递归所需信息构成的(其中包括所有的实在参数、局部变量和上一层返回地址)
活动记录 -->> 当前执行层的工作记录必为递归工作栈栈顶的工作记录
当前环境指针 -->>指示活动记录的栈顶指针
递归的特性:
1.递归算法实际上是 分治 将复杂问题化为简单问题的求解方法
2.递归算法的效率较低
递归算法的转化方法:
1.简单递归问题的转换,对于尾递归和单向递归的算法----->可采用循环结构的算法替代
2.基于栈的方法:将递归中隐含的栈机制,转换为有用户直接控制的显式的栈 (利用栈来保存数据,栈的后进先出的特性吻合递归算法的执行过程)
1.单向递归:
斐波那契数列
if(n == 1 || n == 0) return n;
else
{
x = 0, y = 1;
for(i = 2; i <= n;i++)
{
z = y; y = x + y; x = z;
}
}
2.尾递归 ||阶乘if(n == 1) return 1;
return n*fac(n-1);----->
int fac = 1;
for(i = 1;i <= n; i++) fac = fac *i;
return fac;
-
对斐波那契数列的优化(记忆化搜索|动态规划)
因为每次计算的中间结果都一定,不会发生变化,可以用数组存起来,从而得到优化。
int memo[MAX_N + 1]; int fib(int n) { if(n <= 1) return n; if(memo[n] != 0) return memo[n]; return memo[n] = fib(n-1) + fib(n-2); }
队列
先进先出(FIFO) 队尾rear ,队头 front
队头指针永远指向第一个元素,队尾指针永远指向最后一个元素
队列的独特用途:1.离散事件的模拟(模拟事件发生的先后顺序,如CPU芯片中的指令译码队列)
2.操作系统中的作业调度(一个CPU执行多个作业)
抽象数据类型定义
ADT Queue
{
数据元素:可以是任意类型的数据,但必须属同一个数据对象
结构关系:队列中的数据元素之间是线性的
基本操作:
InitQueue(Q) :将队列Q初始化为一个空队列
IsEmpty(Q) :对一个已存在的队列Q,判断是否为空。
IsFull(Q):对一个已存在的队列Q,判断是否满了。
EnterQueue(Q,x):在队列Q队尾插入x
DeleteQueue(Q,x):在队列Q的队头元素出队,并用x带回其值
GetHead(Q,x):取队列Q的队头元素,不出队并用x带回其值
ClearQueue(Q):将队列Q置为空队列
}ADT Queue;
队列的表示和实现
链队列
typedef struct Node
{
QueueElementType data;
struct Node * next;
}LinkQueueNode;
typedef struct
{
LinkQueueNode *front;
LinkQueueNode *rear;
}LinkQueue;
链队的操作:
·链队的初始化
int InitQueue(LinkQueue *Q)
{
Q -> front = (LinkQueueNode *)malloc(sizeof(LinkQueueNode));
if(Q -> front != NULL)
{
Q -> rear = Q -> front;
Q -> front -> next = NULL;
return true;
}
else return false; //溢出
}
·链队列入队操作
//带头节点!
//空的链队列队的队头指针和队尾指针均指向头节点!
int EnterQueue(LinkQueue *Q; QueueElementType x)
{
LinkQueueNode * NewNode;
NewNode = (LinkQueueNode *)malloc(sizeof(LinkQueueNode));
if(NewNode != NULL)
{
NewNode -> data = x;
NewNode -> next = NULL;
Q -> rear -> next = NewNode;
Q -> rear = NewNode; //队尾指针始终指向最后一个元素
return true;
}
else return false //溢出
}
·链队列出队操作算法
int DeleteQueue(LinkQueueNode *Q; QueueElementType *x)
{
LinkQueueNode *p;
if(Q -> front == Q -> rear) return false;
p = Q -> front -> next; //指向队头元素
Q -> front -> next = p -> next; //删除队头元素
if(Q -> rear == p) //表示如果队列中只有一个元素p,则p出队后成为空队
{
Q -> rear = Q -> front;
}
*x = p -> data;
free(p);
return true;
}
循环队列 // 顺序队列
如果是顺序队列,很容易出现的问题就是 “假溢出”
其解决方法:设置更大的队列的元素个数
修改出队算法,每次出队,队列中的剩余元素都向前移动
修改入队,增加判断条件,当出现假溢出时,队列中的元素向对头移动,然后完成入队操作
采用循环队列初始化队列时,令 front = rear = 0;入队时,直接将新元素送入尾指针rear所指的单元,然后尾部指针+1;出队时,直接取队头指针front所指的元素,然后头指针+1。
//当rear = Maxsize的时候 认为队满
//但不一定真的满了,队头可能存在很多空间,但无法使用,这种现象称为 假溢出。
—>一个巧妙的方法是将顺序队列的数组看成一个环状的空间,即规定最后一个单元的后继为第一个单元,形象地称之为循环队列。
循环队列的定义
#define MAXSIZE 50
typedef struct
{
QueueElementType element[MAXSIZE];
int front;
int rear;
//int count; /*可选增加的计数器*/
}SeqQueue;
新问题:循环队列中,队满和队空都有可能存在front = rear;
解决方法:
① 设置一个计时器,用于记录队列中元素个数(队列长度)
队满:count > 0 && rear == front;
队空:count == 0;
②加设标志位,判断是否有过出入队
tag == 1 表示入队 && front == rear
tag == 0 表示出队 && front == rear
③少用一个存储单元,这样rear就永远追不上front
队满:front == (rear + 1) % MaxSize
队空:rear == front
循环队列的初始化操作
void InitQueue(SeqQueue *Q)
{
Q -> front = Q -> rear = 0;
}
循环队列的入队操作
int EnterQueue(SeqQueue * Q, QueueElenmentType x)
{
if ((Q -> rear + 1) % MaxSize == Q -> front)
{
return false; //队满
}
Q -> element[Q -> rear] = x;
Q -> rear = (Q -> rear + 1) % MaxSize; //尾指针向后移动一位,取模运算!
return true;
}
循环队列的出队操作
int DeleteQueue(SeqQueue *Q, QueueElement *x)
{
if (Q -> front == Q -> rear) return false; //队空
* x = Q -> element[Q -> front];
Q -> front = (Q -> front + 1) % MaxSize;
return true;
}
队列的应用举例
杨辉三角形
//最大行数一定小于循环队列的MaxSize值
void YangHuiTriangle()
{
SeqQueue Q;
InitQueue(&Q);
EnterQueue(&Q,1) //第一行元素入队
for(n = 2;n <= N; n ++) //产生第n行元素并入队,同时打印第n-1行的元素
{
EnterQueue(&Q,1); //第n行第一个元素入队
for(i = 1; i <= n - 2; i ++) //利用n - 1行元素去产生第n行中间的n-2个元素并入队
{
DeleteQueue (&Q,&temp);
Print(temp); //打印n-1行的元素
GetHead(Q,&x);
temp = temp + x; //利用n-1行元素产生第n行元素
EnterQueue(&Q,temp);
}
DeleteQueue (&Q, &x);
cout << x; //打印n-1行的最后一个元素
EnterQueue(&Q,1); //第n行的最后一个元素入队
}
while (!IsEmpty(Q)) //打印最后一行元素
{
DeleteQueue (&Q,&x);
cout << x;
}
}
键盘输入循环缓冲区问题
队列的特性保证的输入的字符先输入、先保存、先处理的要求,
#include <cstdio>
#include <conio>
#include <queue>
main()
{
char ch1,ch2;
SeqQueue Q;
int f;
InitQueue (&Q);
for(;;)
{
for(;;)
{
cout << "A";
if(kbhit())
{
ch1 = getchar();
if (ch1 == ';' || ch1 == ',') break;
f = EnterQueue (&Q,ch1);
if (f == FALSE)
{
cout << "队列已满";
break;
}
}
}
while (!IsEmpty(Q))
{
DeleteQueue(&Q,&ch2);
putchar(ch2);
}
if (ch == ';') break;
}
}
◾栈与队列 总结与反思
·LIFO~FIFO
顺序和链式两种存储方式:
·对进栈操作来说,顺序栈受到事先开辟的栈区容量的限制,可能产生上溢。
·循环队列是顺序队列,为了缓解假溢出,可以看作是一个首尾相接的环
·链队列的操作实现与单链表类似,只不过链队中除了头指针,还外设一个尾指针[并通常封装在一个结构体里]