栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
一、基本概念
栈是一种后进先出的数据结构。
允许插入和删除的一段称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表。
1.1 实现方式
根据栈的实现方式,可以分为两类:
1.1.1 顺序栈
用数组的方式存储。
#define MAX_SIZE 100
// 栈的管理结构
typedef struct{
int data[MAX_SIZE]; //存储栈元素的数组
int top; //栈顶序号
}Stack;
/*
初始化栈
*/
void init(Stack * st)
{
st->top = -1; //假设指向下标-1时,表示空栈,top永远直接指向栈顶元素。
}
/*
压栈
返回值 -1:压栈失败 ;0 :压栈成功。
*/
int push(Stack * st, int val)
{
if(st->top == MAX_SIZE - 1) //栈已满
return -1;
st->data[++st->top] = val;
return 0;
}
/*
出栈
返回值 -1:出栈失败 ;0 :出栈成功。
*/
int pop(Stack * st, int * val)
{
if(st->top == -1)
return -1;
*val = st->data[st->top--];
return 0;
}
以数组方式实现栈往往需要事先确定数组容量的大小。
扩展:
假设有两个存储类型相同的栈,如果为这两个栈各自开辟数组空间,极有可能第一个栈满了,再进栈就溢出了,而另一个栈还有很多存储空间。
针对这类情况,我们可以通过一个数组来存储两个栈。
思路:两个栈的栈底分别置于数组的两段,然后两个栈均向数组中间生长。
#define MAX_SIZE 100
// 栈的管理结构
typedef struct{
int data[MAX_SIZE];
int top1;
int top2;
}Stack;
/*
初始化栈
*/
void init(Stack * st)
{
st->top1 = -1;
st->top2 = MAX_SIZE;
}
/*
压栈
返回值 -1:压栈失败 ;0 :压栈成功。
*/
int push(Stack * st, int val, int lrst)
{
if(st->top1 + 1 == st->top2) //栈满了
return -1;
if(lrst == 1) //栈1有元素进栈
st->data[++st->top1] = val;
else if(lrst == 2) //栈2有元素进栈
st->data[--st->top2] = val;
return 0;
}
/*
出栈
返回值 -1:出栈失败 ;0 :出栈成功。
*/
int pop(Stack * st, int * val,int lrst)
{
if(lrst == 1)
{
if(st->top1 == -1)
return -1;
*val = st->data[st->top1--];
}
else if(lrst == 2)
{
if(st->top2 == MAX_SIZE)
return -1;
*val = st->data[st->top2++];
}
return 0;
}
1.2.2 链式栈
用链表的方式实现。
/*
栈元素节点
*/
typedef struct StackNode{
int data;
StackNode * pre; //指向前一个元素, 指向栈底方向。 个人认为用pre比用next形象。
}StackNode;
typedef struct Stack{
StackNode * top;
int cnt; //栈的元素个数
}Stack;
// 初始化栈
void init(Stack * st)
{
st->top = NULL;
st->cnt = 0;
}
/*
压栈
返回值 -1:压栈失败 ;0 :压栈成功。
*/
int push(Stack * st, int val)
{
StackNode * node = new StackNode;
if (!node) //申请内存失败
return -1;
node->data = val;
node->pre = st->top; //新入栈的元素的前一个元素是之前的栈顶元素。
st->top = node; //修改栈顶为新入栈的元素
st->cnt++; //栈元素数量+1
return 0;
}
/*
入栈
返回值 -1:出栈失败 ;0 :出栈成功。
*/
int pop(Stack * st, int * val)
{
if (st->cnt == 0) //空栈
return -1;
*val = (st->top)->data;
StackNode * node = st->top;
st->top = node->pre;
delete node;
st->cnt--;
return 0;
}
二、std::stack
C++ STL 中提供了栈的实现 —— std::stack
。是一个容器适配器,下层容器默认是std::deque
。 可参考 C/C++ 语言参考
std::stack
所有元素的进出都必须符合“先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。stack不提供走访功能,也不提供迭代器。
std::stack
提供的操作有:
- emplace
template <class... Args> void emplace (Args&&... args); // 注意形参是 右值引用
入栈新元素,支持传入右值。
- push
void push (const value_type& val);
void push (value_type&& val);
入栈新元素,支持传入右值 和 左值。
- pop
void pop();
栈顶元素出栈。
- top
reference& top();
const_reference& top() const;
返回栈顶元素。
- empty
bool empty() const;
判断stack是否为空。也就是size()是否为0。
- size
size_type size() const;
返回栈的元素数量。
- swap
void swap (stack& x) noexcept(/*see below*/);
交换两个栈的元素。
三、栈的应用
3.1 斐波那契数列
3.1.1 问题描述
假定你有一雄一雌一对刚出生的兔子,它们在长到一个月大小时开始交配,在第二月结束时,雌兔子产下另一对兔子,过了一个月后它们也开始繁殖,如此这般持续下去。每只雌兔在开始繁殖时每月都产下一对兔子,假定没有兔子死亡,在一年后总共会有多少对兔子?
在一月底,最初的一对兔子交配,但是还只有1对兔子;在二月底,雌兔产下一对兔子,共有2对兔子;在三月底,最老的雌兔产下第二对兔子,共有3对兔子;在四月底,最老的雌兔产下第三对兔子,两个月前生的雌兔产下一对兔子,共有5对兔子;……如此这般计算下去,兔子对数分别是:1, 1, 2, 3, 5, 8, 13, 21, 34, 55,89, 144, …看出规律了吗?从第3个数目开始,每个数目都是前面两个数目之和。这就是著名的斐波那契(Fibonacci)数列。
3.1.2 解析
以上问题可转化为如下数学公式:
代码如下:
#include <iostream>
int fb(int i) // 第i个斐波那契数
{
if (i <= 2)
return 1;
return fb(i - 1) + fb(i - 2);
}
int main()
{
int i;
for (i = 1; i <= 12; i++)
std::cout << fb(i) << " ";
std::cout << std::endl;
return 0;
}
结果如下:
3.2 括号匹配
3.2.1 问题描述
假设有一个字符串str,其中只包含有”(“,”)”,”{“,”}”,”[“,”]”,”<”,”>”这样的字符,或没有。括号要成对匹配,不能有交叉,如(({}))是匹配的,(([])和{([]})是不匹配的。
现给定一个字符串,判断是否匹配。
3.2.2 解析
建立一个空栈.
对字符串中每一个字符做如下操作:
如果字符为左括号:
将字符压栈;
重复操作(操作下一个字符);
如果字符为右括号,则查看当前栈顶的字符是否和此字符匹配:
如果匹配,栈顶元素弹出,操作下一个字符,跳到2
否则,括号不匹配,返回不匹配
其他字符,则操作下一个字符
处理完字符串之后,查看栈是否为空:
如果栈空,返回匹配
否则,返回不匹配
代码如下:
#include <iostream>
#include <string>
#include <stack>
using namespace std;
#define is_left(c) (((c)=='(')||((c)=='{')||((c)=='[')||((c)=='<'))
/*
ASCII值:
'(': 40 ; ')': 41 ;
'<': 60 ; '>': 62 ;
'[': 91 ; ']': 93 ;
'{': 125 ; '}': 125 ;
*/
#define is_pair(l,r) ((l)=='('?(r)==')':(r)==(l)+2)
bool ispair(string & str)
{
if (str.length() == 0)
{
return true;
}
int i = 0;
stack<char> st;
st.push(str.at(i));
i++;
while (i < str.length())
{
if (is_left(str.at(i))) //左括号
{
st.push(str.at(i));
i++;
continue;
}
else //右括号
{
if (st.size() > 0 && is_pair(st.top(), str.at(i)) )
{
st.pop();
i++;
continue;
}
return false;
}
}
if (st.size() == 0)
return true;
else
return false;
}
int main()
{
string str;
while(1)
{
cin >> str;
cout << ispair(str) << endl;
}
return 0;
}
结果:
3.3 回文
3.3.1 问题描述
回文字符串是指从左到右和从右到左相同的字符串。
给定一个字符串,判断是否回文?
3.3.2 解析
做法可以分为两种,一是利用栈的做法,二是不利用栈。
// 不利用栈,首尾相比较
bool ishuiwen1(char * str, int len){
char * beg = str;
char * end = str + len - 1;
while (beg < end)
{
if (*beg != *end)
return false;
beg++;
end--;
}
return true;
}
// 利用栈
bool ishuiwen2(char * str, int len)
{
stack<char> st;
int i = 0;
while (i < len / 2)
{
st.push(str[i++]);
}
if (len / 2 == 1)
i++;
while (str[i] != '\0')
{
if (str[i] == st.top())
st.pop();
i++;
}
return st.size() == 0;
}
3.4 逆波兰表达式( 四则运算 )
3.4.1 问题描述
逆波兰表达式 是一种不需要括号的后缀表达式 。 对于“ 3 + (4 -1) * 3 + 6 / 2”这样常见的四则运算式子,我们称之为 中缀表达式 。当转换成逆波兰表达式时,能方便的处理四则运算。
对于上述的四则运算式子,转换成逆波兰表达式之后将变为:”3 4 1 - 3 * + 6 2 / + ” 。
如何利用逆波兰表达式处理四则运算:
从左到右遍历表达式的每个数字和符号,遇到数字就入栈,遇到符号,就将栈顶两个数字出栈,进行运算,运算结果出栈,一直到最终获得结果。
更多关于逆波兰表达式的介绍参考 维基百科 - 逆波兰表达式
3.4.2 解析
#include <iostream>
#include <stack> //use STL
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
using namespace std;
const int MAXSIZE = 256;
int InfixToPostfix(char *infix, char *postfix);
double Calculate(char *arr);
int main()
{
cout << "四则运算,请输入运算式:" << endl; // prints 四则运算
char in[MAXSIZE] = { 0 };
char postfix[MAXSIZE] = { '\0' };
fgets(in, MAXSIZE, stdin);
if (InfixToPostfix(in, postfix) != 1)
{
cout << "InfixToPostfix wrong!!!";
return -1;
}
puts(in); puts(postfix);
cout << Calculate(postfix);
return 0;
/*
将中缀表达式转换为后缀表达式
参数:infix 指向中缀表达式,以回车键即\n结尾。
postfix 指向后缀表达式临时缓冲区,用来存放转换后的结果。
附转换规则:从左到右遍历中缀表达式的每个数字和符号,若是数字则直接保存在postfix数组中;若是符号,则判断其与栈顶符号的优先级,是右括号或者优先级不大于栈顶符号,则栈顶元素依次出栈并输出,直到遇到左括号或者栈空时,才将刚才的那个符号入栈。
*/
int InfixToPostfix(char *infix, char *postfix)
{
stack<char> s;
char c, e;
int j = 0, i = 0;
c = *(infix + i); //取出中缀表达式中的第一个字符
i++;
while ('\n' != c) //遇到换行符,表示转换结束
{
while (c >= '0'&&c <= '9') //先判断一下取出的字符是否是数字,如果是数字的话,则直接存入postfix数组
{
postfix[j++] = c;
c = *(infix + i);
i++;
if (c<'0' || c>'9') //如果不是数字,则在后面添加空格,以便区分各个符号
{
postfix[j++] = ' ';
}
}
if (')' == c) //不是数字,则判断是否为右括号。[括号的优先级最高,所以,如果是右括号的话,就得先进行括号里的各种运算]
{
e = s.top(); s.pop();
while ('(' != e) //直到遇到左括号为止
{
postfix[j++] = e;
postfix[j++] = ' ';
e = s.top(); s.pop();
}
}
else if ('+' == c || '-' == c) //如果是加减号,因为他俩的优先级最低了,所以此时先将栈里的所有符号出栈后(除非遇到左括号),再把此符号入栈
{
if (!(s.size())) //如果是空栈,则直接将加减号入栈
{
s.push(c);
}
else//如果不是空栈,首先将所有优先级大于加减的出栈,然后再把加减号入栈
{
do{
e = s.top(); s.pop();
if ('(' == e)
{
s.push(e);
}
else
{
postfix[j++] = e;
postfix[j++] = ' ';
}
} while (s.size() && '(' != e); //将栈里的所有符号出栈(除非遇到左括号)
s.push(c); //最后将新来的加减号再入栈
}
}
else if ('*' == c || '/' == c || '(' == c) //如果是乘除号或左括号,因为他们的优先级高,所以直接入栈。
{
s.push(c);
}
else if ('\n' == c) //判断一下,所有符号是否都已转换完成
{
break;
}
else //能走到这个else的,都是我不认识的符号了
{
// printf("\nError:input error,the character %d cann't recognize!\n",c);
return -1;
}
c = *(infix + i); //取出下一个字符进行转换
i++;
}
while (s.size()) //转换完成后,栈里可能还有没出栈的运算符号
{
e = s.top(); s.pop();
postfix[j++] = e;
postfix[j++] = ' ';
}
return true;
}
/*
计算后缀表达式的结果
参数:arr使用空格分隔的后缀表达式字符串。例:arr="31 5 + "
result 保存计算完毕后的结果
注:如何利用栈来计算后缀表达式的结果:依次取出后缀表达式中的符号进行比较,如果是数字,则直接入栈;如果是符号,则出栈两次,弹出两个要计算的因数,进行计算,之后再将计算结果入栈。知道后缀表达式中所有符号都已比较完毕。
*/
double Calculate(char *arr)
{
// printf("%s\n",arr);
double d, e, f; //d,e 存放两个因数。f存放d,e计算后的结果.
stack<double> s;
char *op; //存放后缀表达式中的每个因数或运算符
char *buf = arr; //声明bufhe saveptr两个变量,是strtok_r函数的需要。
char *saveptr = NULL;
while ((op = strtok(buf, " ")) != NULL) //利用strtok_r函数分隔字符串
{
buf = NULL;
switch (op[0])
{
case '+':
d = s.top(); s.pop();
e = s.top(); s.pop();
f = d + e;
s.push(f);
break;
case '-':
d = s.top(); s.pop();
e = s.top(); s.pop();
f = e - d;
s.push(f);
break;
case '*':
d = s.top(); s.pop();
e = s.top(); s.pop();
f = d*e;
s.push(f);
break;
case '/':
d = s.top(); s.pop();
e = s.top(); s.pop();
f = e / d;
s.push(f);
break;
default:
d = atof(op); //不是运算符,就肯定是因数了。所以,用atof函数,将字符串转换为double类型
s.push(d);
break;
}
}
double result = s.top(); s.pop();
return result;
}
3.5 迷宫解题
3.5.1 问题描述
MAP里面0代表墙(通不过),1代表空格(可通过)。求起始点到目的点的一条路径。
3.5.2 解析
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
typedef struct
{
int x; //x坐标
int y; //y坐标
}Postype; //坐标类型
typedef struct
{
//int ord; //通道块在路径上的序号
//Postype seat; //通道块在迷宫中的坐标
//int di; //从此通道块走向下一通道块的“方向”
int x;
int y; //元素坐标
// bool track; //是否已经走过
}ElemType; //栈的元素类型
int MAP[9][9] =
{
//0 1 2 3 4 5 6 7 8
0, 0, 0, 0, 0, 0, 0, 0, 0, // 0
0, 1, 0, 0, 1, 1, 1, 1, 0, // 1
0, 1, 0, 0, 1, 1, 1, 1, 0, // 2
0, 1, 1, 1, 1, 0, 1, 1, 0, // 3
0, 1, 0, 1, 0, 1, 1, 1, 0, // 4
0, 1, 0, 1, 0, 1, 1, 1, 0, // 5
0, 1, 0, 1, 0, 1, 1, 1, 0, // 6
0, 0, 0, 1, 1, 1, 1, 1, 0, // 7
0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
};
/*-------------------------------------栈的元素类型定义完毕-------------------------*/
typedef int Status; //函数返回值
#define STACK_INIT_SIZE 100 // 栈的初始大小
#define STACK_INCREMENT 10 // 每次增加的空间大小
//下面给出栈的相关定义
typedef struct
{
ElemType *base; //在构造栈之前和销毁之后,base的值为NULL
ElemType *top; //栈顶指针
int stacksize; //当前已分配的存储空间,以元素为单位
}ZJC_Stack;
//--------------------------------------栈基本操作的算法部分--------------------------
//栈的初始化
Status InitStack(ZJC_Stack &S)
{
S.base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType)); //分配内存空间
if (!S.base)
exit(OVERFLOW);
else //否则分配成功
{
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return OK;
}
}
//获得栈顶元素
ElemType GetTop(ZJC_Stack S)
{
if (S.top == S.base)
exit(ERROR);
return *(S.top - 1);
}
//压栈
Status Push(ZJC_Stack &S, ElemType e)
{
if (S.top - S.base >= S.stacksize)
{
S.base = (ElemType*)realloc(S.base, (S.stacksize + STACK_INCREMENT) * sizeof(ElemType));
if (!S.base)
exit(OVERFLOW);
S.stacksize += STACK_INCREMENT;
S.top = S.base + S.stacksize;
}
*S.top++ = e;
return OK;
}
void Print_Path(ZJC_Stack S) //打印出栈中的元素
{
printf("\n寻路完成..路径坐标值如下:......\n");
ElemType *p = S.base; //首先p指向栈底指针
ElemType temp;
while (p != S.top) //只要没有到顶端,指针就移动,然后输出元素值
{
temp = *p;
printf("\n路径 x = %d , y = %d", temp.x, temp.y);
p++;
}
printf("\n");
}
//出栈函数
Status Pop(ZJC_Stack &S, ElemType &e)
{
if (S.top == S.base) //空栈,返回错误
return ERROR;
else //不是空栈
{
e = *--S.top;
return OK;
}
}
void PathAddToStack(int i, int j, ElemType temp, ZJC_Stack &robot) //因为要修改值,所以取地址,开始没加取地址符号,栈顶元素一直是(1,1)
{
temp.x = i, temp.y = j;
Push(robot, temp);
MAP[i][j] = 2; //标记已经走过该格子了,当初想是否需要用其他标记,实际上不需要的,既然标记2,那么证明当然可以走过(不是墙)!
}
void MAZH_SOLVE(int endx, int endy) //解决迷宫问题函数,参数为终点的坐标
{
int i = 1, j = 1; //起点坐标
ZJC_Stack robot; //定义栈;
if (InitStack(robot)) //初始化栈
printf("\n栈的初始化完成....\n");
ElemType CurrentPos; //当前位置
ElemType start; //初始位置的相关信息
ElemType temp; //暂时用的
start.x = i;
start.y = j;
temp = start;
//start.track = true; //Robot站在初始位置,初始位置已经走过
MAP[i][j] = 2; //走过的标记为2
Push(robot, start); //初始位置入栈
printf("\n开始寻路....\n");
do //主要寻路算法:
{
CurrentPos = GetTop(robot);
i = CurrentPos.x;
j = CurrentPos.y;
printf(" \n寻路过程如下栈顶元素的 x = %d ,y = %d....\n", i, j);
if (MAP[i][j + 1] == 1) //表明向下一格走得通
{
printf("\n向下能走动"); //向下前进一步,压栈,标记
j++;
PathAddToStack(i, j, temp, robot);
}
else if (MAP[i + 1][j] == 1)
{
printf("\n向右能走动");
i++;
PathAddToStack(i, j, temp, robot);
}
else if (MAP[i - 1][j] == 1)
{
printf("\n向左能走动");
i--;
PathAddToStack(i, j, temp, robot);
}
else if (MAP[i][j - 1] == 1)
{
printf("\n向上能走动");
j--;
PathAddToStack(i, j, temp, robot);
}
else //都走不动
{
printf("\n都走不动,退栈");
Pop(robot, temp);
}
} while (GetTop(robot).x != endx || GetTop(robot).y != endy); //只要栈顶元素的x,y不等于终点坐标的x,y,则一直循环找路径
printf("\n完成!\n");
Print_Path(robot); //打印出坐标值
}
int main() //入口函数
{
MAZH_SOLVE(7, 7);
return 0;
}
结果: