栈和队列
3.1 栈的基本概念
(一)栈的定义
栈是一种只允许在表的一端进行插入操作和删除操作的线性表。允许操作的一端称为栈顶,栈顶元素的位置由一个称为栈顶位置的变量给出。当表中没有元素时,称之为空栈。
栈的特点:
- 元素间呈线性关系
- 插入删除在一端进行
- 后进先出,先进后出——LIFO(Last-In-First-Out)
(二)栈的基本操作
- 其操作仅仅是一般线性表的操作的一个子集。
- 插入和删除操作的位置受到限制。
3.2 栈的顺序存储结构(顺序栈)
(一)构造原理
描述栈的顺序存储结构最简单的方法是利用一维数组 STACK[ 0…M–1 ] 来表示,同时定义一个整型变量( 不妨取名为top) 给出栈顶元素的位置。
溢出:
上溢:当栈已满时做入栈操作 ( top = M – 1 )
下溢:当栈为空时做出栈操作 ( top = –1 )
类型定义:
#define MAXSIZE 1000
ElemType STACK[MAXSIZE];
int Top;
//初始时,Top = -1
//由于Top变量需要在多个函数间共享,为了保持函数接口简洁,在此定义为全局变量。
(二)顺序栈的基本算法
//1.初始化堆栈:
void initStack( )
{
Top = –1;
}
//2. 测试堆栈是否为空(栈空,返回1,否则,返回0)
int isEmpty( )
{
return Top == –1;
}
//3. 测试堆栈是否已满(栈满,返回1,否则,返回0)
int isFull( )
{
return Top == MAXSIZE–1;
}
//4. 进栈算法
void push( ElemType s[ ] , ElmeType item )
{
if( isFull() )
Error(“Full Stack!”);
else
s[++Top] = item;//入栈成功
}
void Error(char s[])
{
printf(“%s\n”, s);
exit( -1);
}
/*
exit好象在stdio.h里面,所以要有包含头文件
return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
return是返回函数调用,如果返回的是main函数,则为退出程序
exit是在调用处强行退出程序,运行一次程序就结束
-------------------------------------------------------------------
return是返回 , 函数返回
而exit是退出
-------------------------------------------------------------------
exit(1)表示异常退出.这个1是返回给操作系统的不过在DOS好像不需要这个返回值
exit(0)表示正常退出
-------------------------------------------------------------------
无论写在那里,都是程序推出,dos和windows中没有什么不一样,最多是系统处理的不一样。
数字0,1,-1会被写入环境变量ERRORLEVEL,其它程序可以由此判断程序结束状态。
一般0为正常推出,其它数字为异常,其对应的错误可以自己指定。
-------------------------------------------------------------------
返回给操作系统的,0是正常退出,其他值是异常退出,在退出前可以给出一些提示信息,或在调试程序中察看出错原因.
*/
//5. 出栈算法
ElemType pop( ElemType s[ ] )
{
if(isEmpty())
Error(“Empty Stack!”);
else
return s[Top--];
}
(三)多栈共享连续空间问题
以两个栈共享一个数组为例
- STACK[0…M-1]
- Top1、Top2 分别给出第1个与第2个栈的栈顶元素的位置。
进栈:
- 当i=1时,将item 插入第1个栈,
- 当i=2时,将item 插入第2个栈。
栈满的条件是:
top1 == top2–1;
进栈算法:
void push( ElemType s[ ] , int i , ElemType item )
{
if(top1 == top2–1) /* 栈满 */
Error(“Full Stack!”);
else
{
if(i==1) /* 插入第1个栈 */
STACK[++top1]=item;
else /* 插入第2个栈 */
STACK[– –top2]=item;
return;
}
}
出栈:
- 第1栈栈空的条件:top1 == -1
- 第2栈栈空的条件:top2 == MAXSIZE
出栈算法:
EleType pop( ElemType s[ ] , int i)
{
if(i == 1)
{
if(top1 == –1)
Error(“Empty Stack1!”);
else
return s[top1– –];
}
else
{
if(top2 == MAXSIZE)
Error(“Empty Stack2!”);
else
return s[top2++];
}
}
3.3 栈的链式储存结构
(一)构造原理
链接栈就是用一个线性链表来实现一个栈结构, 同时设置一个指针变量(这里
不妨仍用top表示)指出当前栈顶元素所在链结点的位置。栈为空时,有top == NULL。
- 链接栈 / 链栈
- 链栈是一种特殊的链表,其结点的插入(进栈)和删除(出栈)操作始终在链表的头。
类型定义:
struct node{
SElmeType data;
struct node *link;
};
typedef struct node *Nodeptr;
typedef struct node Node;
Nodeptr Top;
//即为链表的头结点指针
//由于Top变量需要在多个函数间共享,为了简化操作在此定义为全局变量。
(二)链栈的基本算法
//1.初始化栈:
void initStack( )
{
Top = NULL;
}
//2. 测试堆栈是否为空(栈空,返回1,否则,返回0)
int isEmpty( )
{
return Top == NULL;//不需要测试栈是否已满
}
//4. 进栈算法
void push( ElemType item )
{
Nodeptr p;
if( (p = (Nodeptr)malloc(sizeof(Node))) == NULL )
Error(“No memory!”);
else
{
p->data = item; /*将item送新结点数据域*/
p->link = Top; /*将新结点插在链表最前面*/
Top = p; /*修改栈顶指针的指向*/
}
}
//5. 出栈算法
ElemType pop( )
{
Nodeptr p;
ElemType item;
if ( isEmpty() )
Error(“Empty Stack!”); /* 栈中无元素*/
else
{
p = Top; /* 暂时保存栈顶结点的地址*/
item = Top->data; /*保存被删栈顶的数据信息*/
Top = Top->link; /* 删除栈顶结点 */
free(p); /* 释放被删除结点*/
return item; /* 返回出栈元素*/
}
}
后缀表达式(postfix) / 逆波兰表达式:Reverse Polish Notation , RPN)
为了方便表达式的(计算机)计算,波兰数学家Lukasiewicz在20世纪50年代发明了一种将运算符写在操作数之后的表达式表示方式(称为后缀表达式(postfix),或逆波兰表示,Reverse Polish Notation , RPN)
后缀表达式的最大好处是没有括号,也不用考虑运算符的优先级!
中缀到后缀的转换规则:
规则:从左至右遍历中缀表达式中每个数字和符号:
- 若是数字直接输出,即成为后缀表达式的一部分;
- 若是符号:
- 若是 ) ,则将栈中元素弹出并输出,直到遇到 “ ( ” ,“ ( ” 弹出但不输出;
- 若是 ( ,+ ,× 等符号,则从栈中弹出并输出优先级高于当前的符号,直到遇到一个优先级低的符号;然后将当前符号压入栈中。
- 优先级 + ,- 最低,* , / 次之,( 最高
- 遍历结束,将栈中所有元素依次弹出,直到栈为空。
后缀表达式计算:
规则:从左至右遍历后缀表达式中每个数字和符号:
- 若是数字直接进栈;
- 若是运算符(+,-,*,/),则从栈中弹出两个元素进行计算(注意:后弹出的是左运算数),并将计算结果进栈。
- 遍历结束,将计算结果从栈中弹出(栈中应只有一个元素,否则表达式有错)。
问题3.1:算法分析:
对于问题3.1我们没有必要象编译程序那样先将中缀表达式转换为后缀表达式,然后再进行计算。
为此,可设两个栈,一个为数据栈,另一个为运算符栈,在转换中缀表达式的同时进行表达式的计算。主要思路为:当一个运算符出栈时,即与数据栈中的数据进行相应计算,计算结果仍存至数据栈中。
算法如下:
- 1.从输入(中缀表达式)中获取一项:
- 若是数字则压入数据栈中;
- 若是符号:
- 若是 ) ,则将符号栈中元素弹出并与数据栈中元素进行计算、计算结果放回数据栈中,直到遇到“ ( ”, “ ( ”弹出但不计算;
- 若是 ( ,+ ,* 等符号,则从符号栈中弹出优先级高于当前的符号并与数据栈中元素进行计算、计算结果放回数据栈中,直到遇到一个优先级低的符号;然后将当前符号压入栈中。
- 若是 = 号,将符号栈中所有元素依次弹出并与数据栈中元素进行计算、计算结果放回数据栈中,直到符号栈为空,此时数据栈中为计算结果;否则转1。
枚举类型(enum)
枚举型变量的取值仅限于规定的一组值之一。
- 定义形式:
- enum 枚举名 { 值表 };
- 例:enum color { red, green, yellow, white, black }; /* 枚举值是标识符 */
- 枚举变量说明
- enum color chair;
- enum color suite[10];
对枚举变量的赋值并不是将标识符字符串传给它,而是把该标识符所对应的各值表中常数值赋与变量。C语言编译程序把值表中的标识符视为从0开始的连续整数。另外,枚举类型变量的作用范围与一般变量的定义相同。
如:enum color { red, green, yellow = 5, white, black };
则:red=0, green=1, yellow=5, white=6, black=7
枚举类型用途:
- 枚举类型通常用来说明变量取值为有限的一组值之一,如:enum Boolean { FALSE, TRUE };
- 用来定义整型常量,如: enum { SIZE=1024 };
- 符号类型变量可用于数组下标。
问题3.1代码实现:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<ctype.h>
#define MAXSIZE 100
typedef int DataType;
enum symbol {NUM, OP, EQ,OTHER};//符号类型
enum oper {EPT,ADD, MIN, MUL, DIV, LEFT, RIGHT}; //运算类型及优先级
int Pri[]={-1,0,0,1,1,2,2}; //运算符优先级
union sym {
DataType num;
enum oper op;
} ; //符号值
enum symbol getSym( union sym *item);
void operate(enum oper op );//操作运算符
void compute(enum oper op ); //进行运算
void pushNum(DataType num);
DataType popNum();
void pushOp(enum oper op);
enum oper popOp();
enum oper topOp();
int main()
{
union sym item;
enum symbol s;
while( (s = getSym(&item)) ! = EQ) {
if(s == NUM)
pushNum(item.num);
else if(s == OP)
operate(item.op);
else {
printf(“Error in the expression!\n”);
return 1;
}
}
while(Otop >=0) //将栈中所有运算符弹出计算
compute(popOp());
if(Ntop == 0) //输出计算结果
printf(“%d\n”, popNum());
else
printf(“Error in the expression!\n”);
return 0;
}
enum symbol getSym( union sym *item)
{
int c, n;
while((c = getchar()) != ‘=‘) {
if(c >= ‘0’ && c <= ‘9’){
for(n=0; c >= ‘0’ && c <= ‘9’; c= getchar())
n = n*10 + c-’0’;
ungetc(c, stdin);
item->num = n;
return NUM;
}
else
switch(c) {
case ‘+’: item->op = ADD; return OP;
case ‘-’: item->op = MIN; return OP;
case ‘*’: item->op = MUL; return OP;
case ‘/’: item->op = DIV; return OP;
case ‘(’: item->op = LEFT; return OP;
case ‘)’: item->op = RIGHT; return OP;
case ‘ ‘: case ‘\t’: case ‘\n’: break;
default: return OTHER;
}
}
return EQ;
}
void compute(enum oper op )
{
DataType tmp;
switch(op) {
case ADD:
pushNum(popNum() + popNum()); break;
case MIN:
tmp = popNum();
pushNum(popNum() - tmp); break;
case MUL:
pushNum(popNum() * popNum()); break;
case DIV:
tmp = popNum();
pushNum(popNum() / tmp); break;
}
}
//数据栈操作
DataType Num_stack[MAXSIZE]; //数据栈
int Ntop=-1; //数据栈顶指示器,初始为空栈
void pushNum(DataType num)
{
if(Ntop == MAXSIZE -1)
error(“Data stack is full!”);
Num_stack[++Ntop] = num;
}
DataType popNum()
{
if(Ntop == -1)
error(“Error in the expression!”);
return Num_stack[Ntop--] ;
}
void error(char s[ ])
{
fprintf(stderr, “%s\n”,s);
exit(1);
}
//运算符栈操作
enum oper Op_stack[MAXSIZE];//符号栈
int Otop=-1; //运算符栈顶指示器,初始为空栈
void pushOp(enum oper op)
{
if(Ntop == MAXSIZE -1)
error(“Operator stack is full!”);
Op_stack[++Otop] = op;
}
enum operator popOp()
{
if(Otop != -1){
return Op_stack[Otop--] ;
}
return EPT;
}
enum operator topOp()
{
return Op_stack[Otop];
}
从本例中看出由于使用了栈这种数据结构,一方面简化了算法复杂性;另一方面程序具有很好的可扩展性(如增加新的优先级运算符非常方便)。
思考:修改该表达式计算程序,为其增加:%(求余), >(大于), <(小于)等运算符。运算符优先级照C语言中定义。
栈与递归
静态变量(static)
- ① 内部静态变量
- 在局部变量前加上‘static’关键字就成为内部静态变量。
- 内部静态变量仍是局部变量,其作用域仍在定义它的函数内。但该变量采用静态存贮分配(由编译程序在编译时分配,而一般的自动变量和函数形参均采用动态存贮分配,即在运行时分配空间),当函数执行完,返回调用点时,该变量并不撤消,其值将继续保留,若下次再进入该函数时,其值仍存在。
- ②外部静态变量
- 在函数外部定义的变量前加上“static”关键字便成了外部静态变量。
- 外部静态变量的作用域为定义它的文件,即成为该文件的的“私有”(private)变量,其它文件上的函数一律不得直接进行访问,除非通过它所在文件上的各种函数来对它进行操作,这可实现数据隐藏。(在C++中提供进一步的数据隐藏。)
例:下列程序打印出什么结果
#include <stdio.h>
int f(int i);
main()
{
int i;
for(i=0; i < 5; i++)
printf("%d ",f(i));
}
int f(int i)
{
static int k = 1;
k += i;
return (k);
}
结果:1 2 4 7 11
3.4 队的基本概念
(一)队的定义
队列简称队。是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。允许插入的一端称为队尾,队尾元素的位置由rear指出; 允许删除的一端称为队头,队头元素的位置由front指出。
- 先进先出——FIFO(First-In-First-Out )
(二)队的基本操作
- 其操作仅是一般线性表的操作的一个子集。
- 插入和删除操作的位置受到限制。
3.5 队的顺序存储结构
(一)构造原理
在实际程序设计过程中,通常借助一个一维数组QUEUE[0…M–1]来描述队的顺序存储结构,同时,设置两个变量 front与rear分别指出当前队头元素与队尾元素的位置。
-
rear 指出实际队尾元素所在的位置
-
front 指出实际队头元素所在位置
-
count 指出实际队中元素个数
-
初始时, 队为空, 有 front= 0 rear= –1 count=0
-
测试队为空的条件是 count==0
(二)循环队列
把队列(数组)设想成头尾相连的循环表,使得数组前部由于删除操作而导致的无用空间尽可能得到重复利用,这样的队列称为循环队列。
在实际应用中,由于队元素需要频繁的进出,单向结构很容易造成溢出,即rear到达数组尾,而实际队中元素并没有超出数组大小。因此,在实际应用中通常将队设计成一个循环队列,从而提高空间利用率
类型定义:
#define MAXSIZE 1000
QElemType QUEUE[MAXSIZE];
int Front, Rear , Count;
//初始时,三个变量为:Front = 0 ; Rear = MAXSIZE – 1 ; Count = 0;
//由于变量Front和Rear需要在多个操作(函数)间共享,为了方便操作,在此将其设为全局变量。Count为队列中元素个数。
(三)(循环队列)基本算法
//1. 初始化队列
void initQueue( )
{
Front = 0;
Rear = MAXSIZE-1;
Count = 0;
}
//2.测试队列是否为空或满
//队空或满,返回1,否则,返回0。
int isEmpty( )
{
return Count == 0;
}
int isFull( )
{
return Count == MAXSIZE;
}
//3. 插入(进队)算法
void enQueue(ElemType queue[ ], ElemType item)
{
if(isFull()) /* 队满,插入失败 */
Error(“Full queue!”);
else
{
Rear = (Rear + 1) % MAXSIZE; //!!!!!!!!!!!!
queue[Rear] = item;
Count++;
/* 队未满,插入成功 */
}
}
//4. 删除(出队)算法
ElemType deQueue(ElemType queue[ ])
{
ElemType e;
if(isEmpty())
Error(“Empty queue!”); /* 队空,删除失败 */
else
{
e = queue[Front];
Count--; /* 队非空,删除成功 */
Front = (Front + 1) % MAXSIZE;//!!!!!!!!!!!!!!!!
return e;
}
}
3.6 队列的链式存储结构
(一)构造原理
队列的链式存储结构是用一个线性链表表示一个队列,指针front与rear分别指向实际队头元素与实际队尾元素所在的链结点。
-
链接队列 / 链队
-
front与rear分别指向实际队头和队尾元素
空队对应的链表为空链表,空队的标志是:front = NULL
类型定义:
struct node{
ElmeType data;
struct node *link;
}
typedef struct node QNode;
typedef struct node *QNodeptr;
//队头及队尾指针front和rear定义如下:
QNodeptr Front, Rear;
//为了操作方便,通常将它们定义为全局变量
(二)基本算法
//1. 初始化队列
void initQueue()
{
Front = NULL;
Rear = NULL;
}
//2. 测试队列是否为空
int isEmpty()//队空,返回1,否则,返回0
{
return Front == NULL;
}
//3. 插入(进队)
void enLQueue(ElemType item)
{
QNodeptr p;
if((p=(QNodeptr)malloc(sizeof(QNode))) ==NULL)/* 申请链结点 */
Error(“No memory! ”);
p->data=item;
p->link=NULL;
if(Front == NULL)
Front=p; /* 插入空队的情况 */
else
Rear->link=p;
Rear=p; /* 插入非空队的情况 */
}
//4. 删除(出队)
ElemType deLQueue( )
{
QNodeptr p;
ElemType item;
if(isEmpty() )
Error(“Empty queue!”); /* 队为空,删除失败 */
else
{
p = Front;
Front = Front->link;
item = p->data;
free(p);
return item; /* 队非空,删除成功 */
}
}
//5. 销毁一个队
//所谓销毁一个队是指将队列所对应的链表中所有结点都删除,并且释放其存储空间,使队成为一个空队(空链表)。
//归结为一个线性链表的删除
void destroyLQueue()
{
while(Front != NULL){ /* 队非空时 */
Rear = Front->link;
free(Front); /* 释放一个结点空间 */
Front = Rear;
}
}
优先队列
在实际应用时,前述简单队列结构是不够的,先入先出机制需要使用某些优先规则来完善。如:
- 在服务行业,通常有残疾人、老人优先
- 在公路上某些特殊车辆(如救护车、消防车)优先
- 在操作系统进程调度中,具有高优先级的进程优先执行
优先队列(Priority Queue):根据元素的优先级及在队列中的当前位置决定出队的顺序。
优先队列的实现:
- 方法一:使用两种变种链表实现。一种链表是所有元素都按进入顺序排列(队),取元素效率为O(n) ;另一种链表是根据元素的优先级决定新增位置(按优先级排序),新增元素效率为O(n)。实现简单。
- 方法二:使用一个链表和一个指针数组,链表用于存放元素,一个指向链表的指针数组用于确定新加入的元素应该在哪个范围中(按优先级),算法的时间复杂度为O( n^( 1/2 ) )。(J.O.Hendriksen提出)。
- 方法三:用一个堆(Heap)结构实现。这是常用的一种高效实现优先队列的方法(原理将在树中讲解),算法的时间复杂度为O(log2 (N) ) 。