栈是数据结构中一类最基础的数据结构,很多人学习栈往往能很快学会,但是却不能理解栈在实际问题的一些应用条件,这篇文章主要围绕栈的结构定义和结构操作进行讲解,并且进一步讲解对栈的深度理解,提高读者对实际问题的解决能力。
函数库:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
数据结构=结构定义+结构操作
首先来对结构定义进行讲解:
typedef struct Stack {
int* data;
int size, top;
}Stack;
我们先弄懂栈是什么再去考虑上面这层代码的意思。
我们可以将栈理解为乒乓球筒:
假设这个筒只有一口可以打开,那么这样形成的一个装球的结构就和栈是一样的,球只能从最顶上装入,最顶上拿出,那么栈在结构实现上如何符合这样的特点呢?
1.元素的进出顺序为先进后出
2.有一个变量作为头指针永远指向栈顶元素
3.有一个表示栈空间大小的变量
由此可以解释上面的代码:
代码第2行:该结构中有一片数据域,表示栈的存储空间
代码第3行:该结构有一个变量作为头指针(top),有一个表示栈空间大小的变量(size)
结构定义完成。
接下来进行结构操作:
首先是栈的创建操作:
Stack* initStack(int n) {//参数n表示你想要创建的栈空间的大小
Stack* s = (Stack*)malloc(sizeof(Stack));//首先为栈开辟一个空间
s->data = (int*)malloc(sizeof(int) * n);//然后为栈的数据域单独开辟n片连续的空间
s->size = n;//用变量size表示栈的空间大小
s->top = -1;//用变量top表示栈顶元素的位置,由于一开始栈中没有元素,因此指向-1
return s;//最后返回栈的地址
}
值得一提的是:为什么将top元素设置为-1呢?
原因很简单,在上面我们所开辟出来的数据域为连续的数据域,我们可以将其当作数组来处理,在数组中的第一个元素的位置可以用下标 [0] 来表示,因此当第一个元素进入时,我们只需要让top加1,就可以用 [top] 来表示栈顶元素的位置了。
然后是栈的清除操作:
void clearStack(Stack* s) {
if (s)return;//判断栈是否为空,如果为空则不需要清除,直接返回
free(s->data);//否则先释放数据域
free(s);//最后释放结构域
return;
}
栈的判空操作:
int empty(Stack* s) {
return s->top == -1;
}//判空操作
直接返回“判断top值是否为-1”程序的真值。
栈的栈顶元素输出操作:
int top(Stack* s) {
if (empty(s))return 0;//如果栈为空,直接结束操作
return s->data[s->top];//如果不为空,输出数据域中以[top]为下标的元素的值
}
接下来两个操作为栈的重头戏:
入栈操作:
int push(Stack* s,int val) {//传入栈的地址和想要入栈的值
if (s->top + 1 == s->size)return 0;//如果栈满了,则不能入栈,直接结束程序
s->top += 1;//否则栈顶指针+1
s->data[s->top] = val;//将值压入栈
return 1;
}
出栈操作:
int pop(Stack* s) {
if (empty(s))return 0;//如果为空,结束程序
s->top -= 1;//栈顶指针-1
return 1;
}
最后是输出栈的元素顺序操作:
void outputStack(Stack* s) {
printf("Stack:");
for (int i = s->top; i != -1; --i) {
printf("%4d", s->data[i]);
}
printf("\n\n");
return;
}
借用变量i依次从栈顶向栈底遍历,相当于从乒乓球筒中一个个拿球的行为。
最后附上整个程序,并借用随机程序测试:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct Stack {
int* data;
int size, top;
}Stack;
Stack* initStack(int n) {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->data = (int*)malloc(sizeof(int) * n);
s->size = n;
s->top = -1;
return s;
}
int empty(Stack* s) {
return s->top == -1;
}//判空操作
int top(Stack* s) {
if (empty(s))return 0;
return s->data[s->top];
}
int push(Stack* s,int val) {
if (s->top + 1 == s->size)return 0;
s->top += 1;
s->data[s->top] = val;
return 1;
}
int pop(Stack* s) {
if (empty(s))return 0;
s->top -= 1;
return 1;
}
void clearStack(Stack* s) {
if (s)return;
free(s->data);
free(s);
return;
}
void outputStack(Stack* s) {
printf("Stack:");
for (int i = s->top; i != -1; --i) {
printf("%4d", s->data[i]);
}
printf("\n\n");
return;
}
int main() {
srand(time(0));
#define MAX_OP 10
Stack* s = initStack(5);
for (int i = 0; i < MAX_OP; i++) {
int op = rand() % 3, val = rand() % 100;
switch (op) {
case 0:
printf("pop stack,item = %d\n", top(s));
pop(s);
break;
case 1:
case 2:
printf("push stack , item = %d\n", val);
push(s, val);
break;
}
outputStack(s);
}
clearStack(s);
return 0;
}
栈的深度理解:
栈的结构本身是非常简单的,但是这样简单的结构能解决什么问题呢?
我们来看一道题:
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/valid-parentheses
我们先来搞懂什么是有效字符串:
{{{}}}
[[(())]][][]{}{}
(({{}}))
{[()]}[]()
以上的括号序列都是有效的,下面的是无效的:
{}{}{}{}{{{
[({))]()[][][]]]]]))))))
{{{((([[[))}}))]]))
如何用栈来解决这样的问题呢?(下面为答案,大家可以先思考一下)
1.首先读入待判断的括号序列;
2.然后从头到尾依次遍历,遇到左括号入栈,遇到右括号就将栈顶元素和该右括号判断;
3.如果判断结果为同类括号,则弹出栈顶元素;
4.如果判断结果为不同类括号,则直接退出程序,提示错误;
5.如果遍历结束之后栈为空,则提示正确。
附上程序:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct Stack {
char* data;
int size, top;
}Stack;
Stack* initStack(int n) {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->data = (char*)malloc(sizeof(char) * n);
s->size = n;
s->top = -1;
return s;
}
int empty(Stack* s) {
return s->top == -1;
}
char top(Stack* s) {
if (empty(s))return 0;
return s->data[s->top];
}
int push(Stack* s, char val) {
if (s->top + 1 == s->size)return 0;
s->top += 1;
s->data[s->top] = val;
return 1;
}
int pop(Stack* s) {
if (empty(s))return 0;
s->top -= 1;
return 1;
}
void clearStack(Stack* s) {
if (s)return;
free(s->data);
free(s);
return;
}
void solve(char str[]) {
int flag = 1;
Stack* s = initStack(100);
for (int i = 0; str[i]; i++) {
if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
push(s, str[i]);
}
else {
switch (str[i]) {
case ')': {
if (!empty(s) && top(s) == '(') pop(s);
else flag = 0;
} break;
case ']': {
if (!empty(s) && top(s) == '[') pop(s);
else flag = 0;
} break;
case '}': {
if (!empty(s) && top(s) == '{') pop(s);
else flag = 0;
} break;
}
if (flag == 0) break;
}
}
if (flag == 0 || !empty(s)) {
printf("error : %s\n", str);
}
else {
printf("success : %s\n", str);
}
clearStack(s);
return;
}
int main() {
Stack* s = initStack(100);
char str[100];
while (~scanf("%s", str)) {//“~”需要输入三次ctrl+z才能推退出
solve(str);
}
clearStack(s);
return 0;
}
从上题的解法中可以看出栈结构对于解决实际问题的作用,我们隐隐约约能感受到栈可以解决上题一类的问题,我们能否用一句话来概括栈可以解决的问题的特点呢?
栈可以处理具有完全包含关系的问题
什么是完全包含关系?
假设下面有一个集合序列:
{{{集合1}{集合2}{集合3}}{{集合a}{集合b}{集合c}}}
在上述集合中套有两个中集合,每个中集合又包含三个小集合123,abc;假设整个大集合为一个待解决的问题,如果我们想要解决这个大问题,就需要先解决两个中集合的问题,要解决中集合的问题,就要先解决小集合的问题,此时我们可以先将每个小集合依次入栈,那么可以将栈中元素看为{”c“,”b“,”a“,”3“,”2“,”1“},我们从栈顶依次解决问题cba,321,当解决问题cba时,意味着中问题2被解决了,当解决321时,意味着中问题1被解决了,当栈为空时,意味着整个问题被解决了 。
在上述括号问题中,我们以{[()]}[]()为例子,整个序列”{[()]}[]()“为大问题”{[()]}“[]”“()”为中问题1、2、3,而中问题1“{[()]}”中又包含三个小问题,即判断“{[(”是否依次和“)]}”依次对应。
在实际的程序设计中,遇到具有完全包含关系的问题,我们可以尝试用栈来解决,而栈结构存在的意义就在于此。