一、定义和特点
1.1栈的定义和特点
定义:栈(stack)是限定仅在表尾进行插入或删除操作的线性表。表尾端称为栈顶,表头端称为栈底。不含元素的空表叫空栈。
特点:后进先出的线性表
1.2队列的定义和特点
定义:只允许在表的一端进行插入,而在另一端删除元素的线性表称为队列。允许插入的一端称为队尾,允许删除的一端称为队头。
特点:先进先出的线性表
二、栈的表示和操作的实现
2.1顺序栈的表示和实现
//顺序栈的存储结构
#define MAXSIZE 100 //顺序栈存储空间的初始分配量
typedef struct
{
SElemType *base;//栈底指针
SElemtype *top;//栈顶指针
int stacksize;//栈可用的最大容量
}SqStack;
栈为空的时候base=top
top指针初始指向基地址也就是在数组中的第0个,top指针始终指向栈顶元素的后一个位置
当top-base=stacksize时栈满
1.初始化
【算法步骤】
①为顺序栈动态分配一个最大容量为MAXSIZE的数组空间,使base指向这段空间的基地址,即栈底
②栈顶指针top初始为base,表示栈为空
③stacksize置为栈的最大容量MAXSIZE
【算法描述】
Status InitStack(SqStack &S)
{
S.base=new SElemType[MAXSIZE];//为顺序栈动态分配一个最大容量为MAXSZIE的数组空间
if(!S.base) exit(OVERFLOW);//存储分配失败
S.top=S.base;//top初始为base,空栈
S.stacksize=MAXSIZE;//stacksize置为栈的最大容量MAXSIZE
return OK;
}
如果想求当前栈有多少个元素时,应该用top-base来求得
2.入栈
【算法步骤】
①判断栈是否满,若满则返回ERROR
②将新元素压入栈顶,栈顶指针加1
【算法描述】
Status Push(SqStack &S,SElemType e)
{
if(S.top-S.base==S.stacksize) return ERROR;//栈满
*S.top++=e;//将元素e压入栈顶,栈顶指针加1
return OK;
}
3.出栈
【算法步骤】
①判断栈是否空,若空则返回ERROR
②栈顶指针减1,栈顶元素出栈
【算法描述】
Status Pop(SqStack &S,SElemType &e)
{
if(S.top==S.base) return ERROR;//栈空
e=*--S.top;//栈顶指针减1,将栈顶元素赋给e
return OK;
}
4.取栈顶元素
【算法描述】
SElemType GetTop(SqStack S)
{//返回S的栈顶元素,不修改栈顶指针
if(S.top!=S.base)//栈非空
return *(S.top-1);//返回栈顶元素的值,栈顶指针不变
}
需要注意出栈和取栈顶元素的区别,前者要改变top指针从而改变栈进而做到删除一个栈顶元素;而后者只取出栈顶的元素,相当于复制了一个出来,不改变栈本身。
2.2链栈的表示和实现
//链栈的存储结构
typedef struct StackNode
{
ElemType data;//栈底指针
struct StackNode *next;
}StackNode,*LinkStack;
由于对栈的主要操作是在栈顶插入和删除元素,显然一链表的头部作为栈顶是最方便的,而且没必要附加一个头节点
1.初始化
【算法步骤】
初始化就是建立一个空栈,又不需要头节点,所以直接将栈顶指针置空就好
【算法描述】
Status InitStack(LinkStack &S)
{
S=NULL;
return OK;
}
2.入栈
【算法步骤】
①为入栈元素e分配空间,用指针p指向
②将新节点数据域置为e
③将新节点插入zhand
④修改栈顶指针为p
【算法描述】
Status Push(LinkStack &S,SElemType e)
{
p=new StackNode;//生成新节点
p->data=e;//将新节点数值域置为e
p->next=S;//将新节点插入栈顶
S=p;//修改栈顶指针为p
return OK;
}
3.出栈
【算法步骤】
①判断栈是否为空,若空则返回ERROR
②将栈顶元素赋给e
③临时保存栈顶元素的空间,以备释放
④修改栈顶指针,指向新的栈顶元素
⑤释放原栈顶元素的空间
【算法描述】
Status Pop(LinkStack &S,SElemType &e)
{
if(S==NULL) return ERROR;//栈空
e=S->data;//将栈顶元素赋值给e
p=S;//用p临时保存栈顶元素空间,以备释放
S=S->next;//修改栈顶指针
delete p;//释放原栈顶元素的空间
return OK;
}
4.取栈顶元素
【算法描述】
SElemType GetTop(LinkStack S)
{
if(S!=NULL)//栈非空
return S->data;//返回栈顶元素的值,栈顶指针不变
}
和顺序栈一样,出栈要改变栈顶;而取栈顶元素只是复制栈顶元素出来,不改变栈本身
三、栈和递归
3.1采用递归算法解决的问题
“分治法”:将复杂问题一步一步地分解成几个相对简单且解法相同的子问题来解决的策略。它有以下的使用条件:
(1)能将一个问题转变成一个解法相似的新问题,不同的只是处理对象,且处理对象更小且变化有规律
(2)可以通过上述转化而使问题简化
(3)必须有一个明确的递归出口
一般形式如下:
void p(参数表)
{
if(递归结束条件成立) 可直接求解;//递归终止条件
else p(较小的参数); //递归步骤
}
1.定义是递归的(如:斐波那契数列)
2.数据结构是递归的(如:输出链表节点、广义表、二叉树)
3.问题的解法是递归的(如:Hanoi塔问题、迷宫问题)
3.2递归过程与递归工作栈
可以用栈理解,当一个函数调用另一个函数时,调用函数如何没有完成,就会被压入栈等待被调入函数结束,直到栈顶的函数被处理完后,在栈顶的函数会接收出栈函数的结果,来继续处理自己的指令,最终当栈空时,一个递归过程才结束
通常,一个函数在运行期间调用另一个函数时,在运行被调用函数之前,系统需先完成三件事:
- 将所有的实参、返回地址等信息传递给被调用函数保存
- 为被调用函数的局部变量分配存储区
- 将控制转移到被调函数的入口
而从被调用函数返回调用函数之前,系统也应完成3件事:
- 保存被调用函数的计算结果
- 释放被调用函数的数据区
- 依照被调用函数保存的返回地址将控制转移到调用函数
3.3递归算法的效率分析
1.时间复杂度分析
时间复杂度的分析可以转化为一个递归方程求解,具体可用迭代法求解
以阶乘的递归函数Fact(n)为例:
if(n==0) return 1;的指向时间是O(1),递归调用Fact(n-1)的执行时间是T(n-1),所以else return n*Fact(n-1);的执行时间是O(1)+T(n-1)。考虑到两数相乘和赋值的操作时间为O(1),最终有如下递归方程:
T
(
n
)
=
{
D
C
+
T
(
n
)
,
,
n
=
0
n
⩾
1
\left.T(n)=\right\{\begin{matrix} D \\ C +T(n) \end{matrix} \begin{matrix} ,\\ , \end{matrix} \begin{matrix} n=0\\ n\geqslant 1 \end{matrix}
T(n)={DC+T(n),,n=0n⩾1
设n>2,T(n-1)=C+T(n-2),所以T(n)=2C+T(n-2),依次迭代可得:
T(n)=nC+T(0)=nC+D,所以T(n)=O(n)
2.空间复杂度分析
因为会将被调用函数依次压入“递归工作栈”所以空间复杂度为
S
(
n
)
=
O
(
f
(
n
)
)
S(n)=O(f(n))
S(n)=O(f(n))
f(n)为“递归工作栈”中工作记录的个数与问题规模n的函数关系。
像阶乘问题、斐波那契数列问题的空间复杂度均为O(n)
3.4利用栈将递归转换为非递归的方法
①设置一个工作栈存放递归工作记录(包括实参、返回地址、变量等)
②从第一个递归调用开始将它的工作记录压入工作栈中
③当不满足递归结束条件时,依次将被调用的函数的工作记录压入工作栈中,直到满足递归结束条件
④当满足递归结束条件时,反复退出栈顶记录,并利用记录对所属函数进行进一步的操作,直到栈空
四、队列的表示和操作的实现
4.1循环队列——队列的顺序表示和实现
//队列的顺序存储结构
#define MAXQSIZE 100//队列可能达到的最大长度
typedef struct
{
QElemType *base;//存储空间的基地
int front;//头指针
int rear;//尾指针
)SqQueue;
约定在初始化创建空队列时,令front=rear=0,当加入新元素时rear增1;当删除元素时front增1
当rear和front均指向队列的最后一个空间时,因为分配的空间是固定的,此后无法添加新元素,但队列却是空的,这就是“假溢出”现象。要解决也很简单,只需在增减新元素的时候对指针加1取队列最大长度的模即可,这就形成了一个循环
这时想象一种情况,当一个队列只添加元素但不减元素时,rear指针总会有和front指针汇合的时候,这是队满和队空的判断条件就重合了,也就是front=rear时。对此,我们可以让rear永远指空(就像栈一样),当(rear+1)%MAXQSIZE=front时,队列就满了。当然还可以另设标准位来判断队满
1.初始化
【算法步骤】
①为队列分配一个最大容量为MAXQSIZE的数组空间,base指向数组空间的首地址
②将头指针和尾指针置为0,表示队列为空
这里的头尾指针和栈不同,栈可以直接通过栈顶指针取到元素,但队列取到对应位置的元素要进行base+rear和base+front的操作找到地址,或者base[rear]和base[front]来直接取值
【算法描述】
Status InitQueue(SeQueue &Q)
{
Q.base=new QElemType[MAXSIZE];//为队列分配一个最大容量为MAXQSIZE的数组空间
if(!Q.base) exit(OVERFLOW);//存储分配失败
Q.front=Q.rear=0;//将头指针和尾指针置为0,队列为空
return OK;
}
2.求队列长度
【算法步骤】
当rear大于front时,rear减front的值就是队列的长度,鉴于循环的特性,rear和front的差值会为负数,而负数的取余会产生歧义,所以我们一定要加上MAXQSIZE来转换成正数。可以拿rear为0,front为5来想想
【算法描述】
int QueueLength(SeQueue Q)
{
return(Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
3.入队
【算法步骤】
①判断队列是否满,若满则返回ERROR
②将新元素插入队尾
③队尾指针加1
【算法描述】
Status EnQueue(SqQueue &Q,QElemType e)
{
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;//判断队满
Q.base[Q.rear]=e;//新元素插入队尾
Q.rear=(Q.rear+1)%MAXQSIZE;//队尾指针加1
return OK;
}
4.出队
【算法步骤】
①判断队列是否为空,若空则返回ERROR
②保存队头元素
③队头指针加1
【算法描述】
Status DeQueue(SeQueue &Q,QElemType &e)
{
if(Q.front==Q.rear) return ERROR;//队空
e=Q.base[Q.front];//保存队头元素
Q.front=(Q.front+1)%MAXQSIZE;//队头指针加1
return OK;
}
5.取队头元素
【算法描述】
QElemType GetHead(SqQueue Q)
{//返回Q的队头元素,不修改队头指针
if(Q.front!=Q.rear) //队列非空
return Q.base[Q.front];//返回队头元素的值,队头指针不变
}
4.2链队列——队列的链式表示和实现
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front;//队头指针
QueuePtr rear;//队尾指针
}LinkQueue;
front会一直指向头节点,rear会改变着指向队尾
不用判断队满,但会判断队空front=rear
1.初始化
【算法步骤】
①生成新节点作为头节点,队头和队尾指针指向此节点
②头节点的指针域置空
【算法描述】
Status InitQueue(LinkQueue &Q)
{
Q.front=Q.rear=new QNode;//生成新节点作为头节点,队头和队尾指针指向此节点
Q.front->next=NULL;//头节点的指针域置为空
return OK;
}
2.入队
【算法步骤】
①为入队元素分配节点空间,用指针p指向
②将新节点数据域置为e
③将新节点插入到队尾
④修改队尾指针为p
不需要判断队满
【算法描述】
Status EnQueue(LinkQueue &Q,QElemType e)
{
p=new QNode;//为入队元素分配节点空间,用指针p指向
p->data=e;//将新节点数据域置为e
p->next=NULL;Q.rear->next=p;//将新节点插入队尾
Q.rear=p;//修改队尾指针
return OK;
}
3.出队
【算法步骤】
①判断队列是否为空,若空则返回ERROR
②临时保存队头元素(首元节点)的值,以释放空间
③修改头节点的指针域,指向下个节点
④判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值,指向头节点
⑤是否原队头元素(首元节点)的空间
判断队列是否为空的语句是front=rear
Status DeQueue(LinkQueue &Q,QElemType &e)
{
if(Q.front==Q.rear) return ERROR;//队列为空,则返回ERROR
p=Q.front->next;//p指向队头元素
e=p->data;//e保存队头元素的值
Q.front->next=p->next;//修改头节点的指针域
if(Q.rear==p) Q.rear=Q.front;//最后一个元素被删,队尾指针指向头节点
delete p;//释放原头节点的空间
return OK;
}
4.取队头元素
【算法描述】
QElemType GetHead(LinkQueue Q)
{//返回Q的队头元素,不修改队头指针
if(Q.front!=Q.rear)//队列非空
return Q.front->next->data;//返回队头元素的值,队头指针不变
}
五、案例分析与实现
【例1】将一个十进制整数N转换成八进制
【算法步骤】
①初始化空栈
②当十进制数N非零时,循环执行以下操作:
·把N与8求余得到的八进制数压入栈S;
·N更新为N与8的商
③当栈S非空时,循环执行以下操作:
·弹出栈顶元素e
·输出e
【算法描述】
void conversion(int N)
{
InitStack(S);//初始化空栈S
while(N)//当N非零时,循环
{
Push(S,N%8);//把N与8求余得到的八进制数压入栈S
N=N/8;//N更新为N与8的商
}
while(!StackEmpty(S))//当栈S非空时,循环
{
Pop(S,e);//弹出栈顶元素e
cout<<e;//输出e
}
}
【算法分析】
显然时间和空间复杂度都是O(log8n),因为要进行2*log8n次并且分配log8n个空间
【例2】括号匹配的检验
【算法步骤】
①初始化空栈S
②设置一标记性变量flag,用来标记匹配结果以控制循环及返回结果,1表示正确匹配,0表示错误匹配,flag初值为1
③查找表达式,依次读入字符ch,如果表达式没有查找完毕并且flag非零,则循环执行以下操作:
·若ch是左括号“[”或“(”,则压入栈
·若ch是右括号“)”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“(”,则正确匹配,否则错误匹配,flag置为0;
·若ch是右括号“]”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“[”,则正确匹配,否则错误匹配,flag置为0;
④退出循环后,如果栈空且flag值为1,则匹配成功,返回true,否则返回false
【算法描述】
Status Matching()
{//表达式以“#”结束
InitStack(S);//初始化栈
flag=1;//标记匹配结果以控制循环及返回结果
cin>>ch;//读入第一个字符
while(ch!='#'&&flag)//假设表达式以“#”结尾(这里flag只要匹配错误一个就退出)
{
switch(ch)
{
case'['://若是左括号,则将其压入栈
case'(':
Push(S,ch);
break;
case')'://若是“)”,则根据当前栈顶元素的值的分情况考虑
if(!StackEmpty(S)&&GetTop(S)=='(')
Pop(S,x);//若栈非空其栈顶元素是“(”则匹配成功
else flag=0;
break;
case']'://若是“]”,则根据当前栈顶元素的值的分情况考虑
if(!StackEmpty(S)&&GetTop(S)=='[')
Pop(S,x);//若栈非空其栈顶元素是“[”则匹配成功
else flag=0;
break;
}
cin>>ch;//继续读入下一个字符
}
if(StackEmpty(S)&&flag) return true;//匹配成功
else return false;//匹配失败
}
【算法分析】
很明显,时间和空间复杂度取决于字符串长度n,可见都为O(n)
【例3】表达式求值
表达式求值同样一开始要将“#”压入栈内,因为每匹配成功就要出栈,所以当栈顶元素是“#”并且读入的字符也为“#”时才结束
这里要设两个栈,一个是数组栈OPND一个是符号栈OPTR,在一开始也要设置好运算符的优先度。优先时入栈,不优先时出栈。值得注意的是“(”可以无条件入栈,但一旦入栈它的优先级就变最低了
当不优先出栈时,要在数字栈中取出两个数来操作
【算法描述】
char EvaluateExpression()
{
InitStack(OPND);//初始化OPND栈,操作数栈
InitStack(OPTR);//初始化OPTR栈,运算符栈
Push(OPTR,'#');//将表达式起始符“#”压入OPTR栈
cin>>ch;
while(ch!='#'||GetTop(OPTR)!='#')//表达式没有查找完毕或OPTR的栈顶元素不为“#”
{
if(In(ch)){Push(OPND,ch);cin>>ch;}//ch不是运算符则进OPND栈
else
switch(Precede(GetTop(OPTR),ch))//比较OPTR的栈顶元素和ch的优先级
{
case'<':
Push(OPTR,ch);cin>>ch;//当前字符ch压入OPTR栈,读入下一个字符ch
break;
case'>':
Pop(OPTR,theta);//弹出OPTR栈顶的运算符
Pop(OPND,b);Pop(OPND,a);//弹出OPND栈顶的两个运算符
Push(OPND,Operate(a,theta,b));//将运算结果压入OPND栈
break;
case'='://OPTR的栈顶元素是“(”且ch是“)”
Pop(OPTR,x);cin>>ch;//弹出OPTR栈顶的“(”,读入下一个字符ch
break;
}
}
return GetTop(OPND);//OPND栈顶元素即表达式求值结果
}
【例4】伴舞问题
安排两个队列,按输入顺序依次进入,按男女分类进入对应队列,所有人进入指定队列后各队头的人依次出列,直到一方队列空为止
总结
学习完本章后,读者应掌握栈和队列的特点,熟练掌握顺序栈和链栈的进栈和出栈算法、循环队列和链队列的进队和出队算法。读者应能够灵活运用栈和队列解决实际应用问题,掌握表达式求值算法,深刻理解递归算法执行过程中栈的状态变化过程。