目录
一、栈的定义
1.1相关概念与特点
(1)相关概念
栈(Stack)是只允许在一端进行插入或删除操作的线性表。(仅在表尾进行插入或删除操作的线性表,表尾—栈顶,表头—栈底,不含元素的空表称空栈)
图1.1-1 现实生活中栈的实例
图1.1-2 双栈与超栈
(2)相关特点
先进后出(FILO)或后进先出(LIFO)
(3)栈的运算
置空栈 取栈顶元素 判空栈 入栈 出栈
(4)顺序栈和链式栈的区别
和顺序栈相比,链栈有一个比较明显的优势是:通常不会出现栈满的情况。
因为顺序栈用数组实现,必须事先确定栈的大小,对内存的使用效率并不高,无法避免因数组空间用光而引起的溢出问题;而链栈因为动态申请内存,一般不会出现栈满情况。
二、栈的表示和实现
2.1顺序栈的表示和实现
(1)结构体定义
这些代码实现其实比顺序表链表还要更简单。
代码实现如下:
typedef int ElemType
typedef struct {
ElemType* base;
ElemType* top;
int stacksize;
}sqStack;
定义一个结构体,base是栈底指针,top是栈顶指针,stacksize是当前站的容量。
(2)建立空栈
图2.1-1 建立空栈
代码实现如下:
#define STACK_INIT_SIZE 100
int Init_Stack(sqStack* s)
{
s->base = (ElemType*)malloc(STACK_INIT_SIZE * sizeof(ElemType));//申请一段空间
if (!s->base)//倘若空间申请失败则返回错误
return -1;
s->top = s->base;//最开始栈底就是栈顶,设置初始栈顶指针
s->stacksize = STACK_INIT_SIZE;//设置顺序栈容量
return 0;
}
(3)入栈操作
图2.1-2 入栈操作
代码实现如下:
#define STACKINCRINREMENT 10//每次增加的容量
//若栈s存在,则插入e到s中并成为栈顶元素
int Push(sqStack* s, ElemType e)
{
if (s->top - s->base >= s->stacksize)
{
s->base = (ElemType*)realloc(s->base ,s->stacksize + STACKINCRINREMENT);
s->top = s->base + s->stacksize;
s->stacksize += STACKINCRINREMENT;
}
*(s->top) = e;
s->top++;
return 1;
}
(4)出栈操作
图2.1-3 出栈操作
代码实现如下:
int Pop(sqStack* s, ElemType* e)
{
if(s->stacksize == 0)
return -1;
*e = *(s->top);
s->top--;
return 1;
}
整个过程相对而言比较容易,看懂会自己实现即可。
2.2链式栈的表示和实现
链栈是指采用链式储存结构实现的栈。通常链栈是用单链表来表示。链栈的节点结构与单链表的结构相同。
思路基本和顺序栈相同,此处相关流程不再赘述,直接放代码。
(1)结构体定义
typedef int Status;
typedef struct Node{
int data; //结点的数据域
struct Node *next; //结点的指针域
}Node,*LinkList;
(2)初始化操作
Status Init_Stack(LinkList &S)
{
S = (LinkList)malloc(sizeof(Node));
if(!S)
return -1;
S->next = NULL;
return 1;
}
(3)入栈操作
Status Push_Stack(LinkList &S,int e)
{
LinkList p = (LinkList)malloc(sizeof(Node));
if(!p)
return -1;
p->data = e;
p->next = S->next;
S->next = p;
return 1;
}
(4)出栈操作
Status Pop_Stack(LinkList &S)
{
LinkList p = S->next;
if(!p)
return -1;
S->next = p->next;
free(p);
return 1;
}
(5)输出栈中所有元素
Status All_Stack(LinkList S)
{
int i = 0;
LinkList p = S->next;
if(!p)
return -1;
while(p)
{
printf("第%d个元素为:%d\n",++i,p->data);
p = p->next;
}
return 1;
}
(6)判断是否为空栈
Status qwe(LinkList &S)
{
LinkList p = S->next;
if(!p)
return 0;
else return 1;
}
(7)清空栈
Status Clear_Stack(LinkList &S)
{
LinkList p = S->next;
if(!p)
return -1;
while(p)
{
LinkList q = p;
p = p->next;
free(q);
}
//最后这一步,切记不可忘
S->next = NULL;
return 1;
}
(8)销毁栈
Status DestoryStack(LinkList &S)
{
LinkList p=S;
while(p)
{
LinkList q = p;
p = p->next;
free(q);
}
}
三、栈与递归
递归是一种重要的程序设计方法。简单地说,若在一个函数、过程或数据结构的定义中又应 用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。
它通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归 策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。 但在通常情况下,它的效率并不是太高。
3.1 函数嵌套应用过程
我们先来了解一下函数嵌套应用的过程:
图 3.1-1 函数嵌套栈应用的全过程
当一个函数在运行期间调用另一个函数时,在运行被调用之前,系统则需完成3件事:
1.将所有的实在参数,返回地址等信息传递给被调用函数保存
2.为被调函数的局部变量分配存储区域
3.将控制转移到调用函数入口
而从被调函数返回调用函数之前,系统也会完成3件事:
1.保存被调函数的计算结果
⒉释放被调函数的数据区
3.依照被调函数保存的返回地址将控制转移到调用函数上
当有多个函数结构嵌套调用时,按照“后调用先返回”的原则,上述函数之间的信息传递和控制转移必须通过“栈”来实现。即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区每当一个函数退出时,就释放它的存储区,则当前正运行的函数的数据区必须在栈顶。
3.2 函数递归应用过程
图 3.2-1 函数递归栈应用的全过程
1、当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统需先完成3件事:
(1)将所有的实在参数、返回地址等信息传递给被调用函数保存。
(2)为被调用函数的局部变量分配存储区。
(3)将控制转移到被调函数的入口。
2、从被调函数返回调用函数之前,系统也应该完成3件工作:
(1)保存被调函数的计算结果。
(2)释放被调函数的数据区。
(3)依照被调函数保存的返回地址将控制转移到调用函数。
3、一个递归函数的运行过程类似于多个函数的嵌套调用,只是调用函数和被调函数是同一个函数,因此,和每次调用相关的一个重要的概念是递归函数运行的“层次”。
举个例子:
这里以斐波那契数列举例。
/* 斐波那契的递归函数 */
int Fbi(int i) {
if (i <2)
return i == 0 ? 0 : 1;
/* 这里的 Fbi 就是函数自己,它在调用自己 */
return Fbi(i - 1) + Fbi(i - 2);
}
int main() {
int i;
for (i = 0; i < 40; i++)
printf("%d", Fbi(i));
return 0;
}
递归过程分为两步“递”和“归”,对应着栈的两种操作“进栈”和“出栈”。
图3.2-2 n=5时斐波那契数列递归图
(1)首先n=5时,符合递归条件,放入栈中
(2)由于返回的是Fbi(i - 1) + Fbi(i - 2)。故先判断4,在n=4时符合递归条件,放入栈中。
(3)同理,栈中从n=5一直放到n=1
(4)判断n=1不满足递归条件,进行出栈。返回fib(2)在n=0时继续入栈,n=0也不满足条件,进行出栈,fib(2)进行出栈返回fib(3)指向的右端的fib(1)继续判断,直至全部结束。
四、经典实例
4.1 汉诺塔问题
问题描述:有塔A,B,C;塔A上有64个碟子,按从大到小的顺序从塔底堆放至塔顶.有另外两个塔B,C.将A上碟子移动到塔C上去,期间借助塔B的帮助.每次只能移动一个碟子,任何时候不能把碟子放在比它小的碟子上面。
对于此问题,可以通过以下三个步骤完成:
1)将塔A上n-1个碟子,借助塔C先移动到塔B上;
2)将塔A上最后一个碟子移动到塔C上;
3)借助塔A将塔B上n-1个碟子移动到塔C上.
显然,这是一个递归求解的过程:
void Move(char A, char B)
{
printf("%c -> %c\r\n", A, B);
}
void Hanoi(int n, char A, char B, char C)
{
if (n == 1)
{
Move(A, C);
}else
{
Hanoi(n-1, A, C, B);
Move(A,C);
Hanoi(n-1, B, A, C);
}
}
int main()
{
int m;
printf("请输入总数:");
scanf("%d", &n);
printf("移动步骤为:\n");
Hanoi(n, 'A', 'B', 'C');
return 0;
}
4.2 回文问题
回文指正读和反读都相同的字符序列为“回文”,如“abba”、“abccba”、12321、123321是“回文”,“abcde”和“ababab”则不是“回文”。
接下来我们会用栈的思想判断一个数是不是回文数。(当然也有数学方法,例如双指针法,感兴趣可以去看我的另一篇博客,c语言力扣第九题之回文数)。
相关思路也比较简单
1.读入字符串
2.去掉空格(原串)
3.压入栈
4.出栈字符与原串字符依次比较。若不等,非回文;若直到栈空都相等,回文。
#include <stdio.h>
#include <string.h>//数组长度的头文件
int main() {
char a[101], s[101];
int i, j, mid, top,len;
gets(a);//读入一行字符串
len = strlen(a);//求字符串的长度
mid = len / 2 - 1;//求字符串的中点
top = 0;//初始化栈顶
//初始化栈
for (i = 0; i <= mid; i++)
s[++top] = a[i];
//判断字符串的长度是奇数还是偶数,并找出需要进行字符匹配的起始下标
if (len % 2 == 0)
j = mid + 1;
else
j = mid + 2;
//逐个比较
for (j; j < len; j++) {
if (s[top] != a[j])
break;
top--;
}
//如果比较完毕输出yes,否则no
if (top == 0)
printf("Yes");
else
printf("No");
getchar();
getchar();
return 0;
}
4.3 多进制转换问题
十进制怎么转为二进制?
十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。其实不难发现这也是个用栈解决的问题
#include<stdio.h>
#include<malloc.h>
#include<math.h>
#include<string.h>
#include "process.h"
#define SIZE 100
#define STACKINCREMENT 10
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
typedef struct
{
int a;
} SElemType;
typedef struct
{
SElemType *base;
SElemType *top;
int stacksize;
} SqStack;
SqStack S; //定义全局变量
Status InitStack(SqStack *S)
{
S->base=(SElemType *)malloc(SIZE*sizeof(SElemType));
if(!S->base) exit(OVERFLOW);
S->top=S->base;
S->stacksize=SIZE;
return OK;
}
Status Push(SqStack *S,SElemType e)
{
if(S->top-S->base>=S->stacksize)
{
S->base=(SElemType *)malloc((S->stacksize+STACKINCREMENT)*sizeof(SElemType));
if(!S->base) exit(OVERFLOW);
S->top=S->base+S->stacksize;
S->stacksize+=STACKINCREMENT;
}
*S->top++=e;
//printf("%dwww\n",*--S->top);
return OK;
}
Status Stackempty(SqStack *S)
{
if(S->top==S->base)
return TRUE;
else
return FALSE;
}
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==S->base) return ERROR;
*e=*--S->top;
return OK;
}
Status DtoBTrans(int N,SqStack *S)
{
SElemType e;
while(N)
{
e.a=N%2;
Push(S,e);
N=N/2;
}
while(!Stackempty(S))
{
Pop(S,&e);
printf("%d",e);
}
return OK;
}
void main()
{
int x;
InitStack(&S);
printf("请输入十进制数:");
scanf("%d",&x);
DtoBTrans(x,&S);
}
4.4 括号匹配问题
给定一个只包括 (
,)
,{
,}
,[
,]
的字符串,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
输入: “()”
输出: true
输入: “()[]{}”
输出: true
输入: “([)]”
输出: false
1:遍历输入的括号序列,如果是左括号,进入2,如果是右括号,进入3
2:如果当前遍历到左括号,则入栈
3:如果当前遍历到右括号,则出栈一个元素,看其是否与当前的右括号组成一对,如果不是,则匹配失败。或者在出栈过程中发生异常(从空栈中出栈),也匹配失败
4:若能顺利遍历完成,检查栈中是否还有剩余元素,如果有,则匹配失败;如果没有,则匹配成功。
以下为栈的相关定义
typedef struct
{
char *base;
char *top;
int stacksize;
}Stack;
void InitStack(Stack &s)//初始化栈
{
s.base = (char *)malloc(STACK_SIZE * sizeof(char));
s.top = s.base;
s.stacksize = STACK_SIZE;
}
void Push(Stack &s,char e )//入栈
{
//默认100的空间,相对来说够了,不再分配空间
*s.top++ = e;
}
int pop(Stack &s, char &e)//出栈
{
if (s.base == s.top)
return 0;
else
{
e = *--s.top;
return 1;
}
}
int Gettop(Stack &s,char &e)//查看栈顶元素
{
if (s.top == s.base)
return 0;
else
e = *(s.top - 1);
return 1;
}
bool IsEmpty(Stack s)//判断栈是否为空
{
if (s.base == s.top)
true;
else
return false;
}
以下为括号匹配算法和主函数
int BMT(Stack &s,char str[])
{
int lenth = strlen(str);
int i;
char topelem;//储存栈顶元素
char e;
for (i = 0; i < lenth; i++)
{
if (IsEmpty(s) || str[i] == '[' || str[i] == '(')
Push(s, str[i]);//字符串字符逐一进栈
else
{
Gettop(s, topelem);
if (topelem == '('&& str[i] == ')')
{
pop(s, e);//合法出栈
}
else if (topelem == '['&& str[i] == ']')
{
pop(s, e);//合法出栈
}
else
break;//不合法,结束循环,不用继续进栈了
}
}
if (IsEmpty(s))
{
printf("匹配");
}
else
{
printf("不匹配");
}
return 0;
}
int main()
{
Stack s1;
InitStack(s1);
char str[100];
gets(str);
BMT(s1, str);
return 0;
}
4.5表达式求值问题(重要!)
任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成。
由于算术运算的规则是:先乘除后加减、先左后右和先括弧内后括弧外,则对表达式进行运算不能按其中运算符出现的先后次序进行。
先了解几个概念:
后缀表达式,运算符在两个操作数的后面,如ab+
前缀表达式,运算符在两个操作数在前面,如+ab
例如:若 Exp=a×b+(c-d / e)×f
则它的前缀式为:+×a b×-c / d e f
中缀式为:a×b+c-d / e×f
后缀式为:a b × c d e / -f×+
综合比较它们之间的关系可得下列结论:
1.三式中的 “操作数之间的相对次序相同”;
2.三式中的 “运算符之间的的相对次序不同”;
3. 后缀式的运算规则为:
·运算符在式中出现的顺序恰为表达式的运算顺序;
·每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式;
对于机器来说,后缀运算符更容易阅读,我们要求解某个表达式,首先要将中缀表达式转化成后缀表达式,再对后缀表达式进行入栈出栈等操作。
转化后缀表达式流程:
1.定义栈后,设表达式的结束符为“#”,预设运算符栈的栈底为“#”;
2.若当前字符是操作数,则直接发送给后缀式;
3.若当前字符为运算符且优先数大于栈顶运算符,则进栈,否则退出栈顶运算符发送给后缀式;
4.若当前字符是结束符,则自栈顶至栈底依次将栈中所有运算符发送给后缀式;
5.若当前为左括号,输入后缀表达式,若当前为右括号,则将栈中的表达式输入后缀表达式直至遇到左括号。
现在我们得到了后缀表达式,具体对后缀表达式的操作如下:
1、从左往右扫描下一个元素,直到处理完所有元素
2、若扫描到操作数则压入栈,并回到1,否则执行3
3、若扫描到运算符,则弹出两个栈顶元素,执行相应运算(先出栈的是右操作数,涉及到运算顺序),运算结果压回栈顶,回到1
最后如果表达式合法则最后栈中只会留下一个元素,就是最终结果。
具体主函数实现如下:
int main()
{
stack Pol;//构造后缀表达式所用的栈,为char类型
stackint S;//计算后缀表达式的值所用到的栈,为float类型
ElemType temp1;
ElemType temp2;
ElemType temp3;
ElemType temp4;//临时变量
float a, b,d;
float c;
int length=0;
char PolRear[1000];//后缀表达式,此处不能使用栈结构表示后缀表达式,仔细理解其构造过程和运算过程可知,这应该是个先入先出结构
int temp = 0;
InitStack(Pol);//初始化
polrear(Pol, PolRear,temp1,temp2,length);
for (int i = 0; i < length; i++)
printf("%c", PolRear[i]);
initstack(S);
do//计算后缀表达式的值
{
if (temp+1!=length)
{
temp1 = PolRear[temp];
temp++;
}
else
break;
c = temp1 - 48;//asc码数字与真实数字相差48
if ('0' <= temp1 && '9' >= temp1)
{
push(S, c);//进栈
}
else//注意运算顺序,先出栈的为被操作数
{
pop(S,&a); //出栈
pop(S,&b);
if (temp1 == '+')
c = a + b;
else if (temp1 == '-')
c = b -a;
else if (temp1 == '*')
c = a * b;
else if (temp1 == '/')
c = b / (a * 1.0);
push(S, c);
}
} while (1);
printf("%f", c);
return 0;
}