1.定义及概念
栈是一种特殊的线性表,它只允许元素在一段插入或者删除,而被插入或者删除的那一段被称之为栈顶相对应的另外一段则被称之为栈底,所以我们不难想象栈是一种先进后出(后进先出)的数据结构
它的结构有点像羽毛球筒(只考虑只有一段能取放),我们像要拿下一个羽毛球时只能先把前面的羽毛球先取出来才能拿到下一个羽毛球:

比如有编号分别为1、2、3、4的元素依次进入栈(进栈)时:

那我们只能根据4、3、2、1的取出(出栈 )里面的元素(已取出的元素不会重新放回去),当元素里的元素被全部取出来时,此时我们称这个栈为一个空栈。
这里有一个误区,当取出的元素不会被再次放入栈时,出栈的顺序此时才是唯一确定的(进入顺序的逆序)
1.1数组与链表不同实现方式的比较
那栈这个数据结构是用数据还是链表来实现更好呢?
我们可以对比一下数组和栈的区别
| 数组(静态顺序表) | 链表(单链表) | |
|---|---|---|
| 存储空间上 | 物理地址连续固定 | 逻辑连续,物理地址随机分散 |
| 随机访问 | 下标直接访问,时间复杂度 O (1) | 必须遍历查找,时间复杂度 O (N) |
| 任意位置插入删除 | 需批量移动元素,效率低 O (N) | 仅修改指针,无需移动元素 O (1) |
| 空间分配 | 编译时固定大小,易浪费 / 溢出 | 运行时动态申请,无空间浪费 |
| 内存利用率 | 低,需预留整块连续内存 | 高,碎片化内存均可利用 |
| 应用场景 | 数据量固定、频繁查询访问 | 数据量不定、频繁插入删除 |
| 缓存利用率 | 高,符合局部性原理 | 低,指针跳转破坏缓存连续性 |
因为栈这个数据结构我们只能只能在一段插入删除元素,所以不需要在中间额外的插入元素,显然就时间复杂度来说,数组更加的适合实现我们的栈,而且因为数据在内存中是连续存储的所以缓存的命中率相对与链表来说更高,是一个更优的选择;

2.栈的模拟实现
其实在大多数语言都已经实现好了现成的栈,比如C++slt中的容器stack就已经帮我们实现好了这个数据结构,但用比较底层的C语言来实现一遍可以帮助我们深入的理解栈这个数据结构也可以顺便练练代码能力
下面是我们接下来实现的功能,我们先在Stack.h这个头文件中声明函数和定义栈:
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack {
STDataType* a;//存放元素
int top;//指向栈顶的下一个位置
int capacity;//空间大小
}ST;
//初始化
void STInit(ST* pst);
//销毁
void STDestroy(ST* pst);
//弹出元素
void STPop(ST* pst);
//插入元素
void STPush(ST* pst, STDataType x);
//返回栈顶元素
STDataType STTop(ST* pst);
//是否为空
bool STEmpty(ST* pst);
//元素个数
int STSize(ST* pst);
其实这里的代码都比较简单,但我为什么要将top定义为指向下一个位置而不是当前位置的元素呢?下面会在实现函数中解答
初始化及销毁:
cpp
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
初始时a指向为空,销毁时先将a指向的空间释放再将a指向为空防止野指针和内存泄漏的问题,这里的top其实是有两种定义方式
(1)top初始为-1,top指向当前的位置
(2)top初始为0, top指向下一个位置
因为当你top初始为0并且top指向当前的位置时候,我就无法确定这个栈是没有元素还是里面有一个0元素。
这两种方式都可以,我这里就使用第二种了
插入和弹出元素:
cpp
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->capacity == pst->top)
{
int newcapacity = (pst->capacity == 0) ? 4 : pst->capacity * 2;
STDataType* tem = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tem == NULL)
{
perror("realloc fail!");
return;
}
pst->a = tem;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
这里没什么好说的,在插入元素时判断空间是否足够,不够就扩容就完了,但是注意需要加入一些断言来处理一些边界的情况防止越界并增强代码的健壮性
返回元素、判断是否为空、返回元素个数:
cpp
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
当你的top定义方式为第一种时,注意一下top的位置关系就好了,这里我top指向的是下一个位置所以刚好为这个栈的个数
栈的数据结构还是比较的简单的,就是需要注意一下边界条件就好了
3.栈的运用
下面这道题可以说是栈的经典题了,也可以说是模板题了
我们可以遇到左括号时将它放入栈中,遇到右括号把栈顶的在括号拿出来,如何它们不匹配的话那这对字符就不可能有效,直接返回false就可以了,但需要添加一点if来处理一些边界情况,比如只有一个符号或者都是同一方向的括号的情况:
cpp
bool isValid(char* s) {
ST pst;
STInit(&pst);
while (*s)
{
if (*s == '(' || *s == '[' || *s == '{')
{
STPush(&pst, *s);
}
else
{
if (STEmpty(&pst))
{
STDestroy(&pst);
return false;
}
char ch = STTop(&pst); STPop(&pst);
if (ch == '(' && *s != ')'
|| ch == '[' && *s != ']'
|| ch == '{' && *s != '}')
{
STDestroy(&pst);
return false;
}
}
s++;
}
if (STSize(&pst) != 0)
{
STDestroy(&pst);
return false;
}
STDestroy(&pst);
return true;
}
可以把之前模拟实现的函数粘贴到上面就可以了,但这样有点难看,所以我提供了C++版本的,因为我取名的逻辑和C++SLT提供的接口一样,所以不懂c++也是可以看懂的而且更好读;
cpp
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(auto e : s)
{
if (e == '(' || e == '[' || e == '{')
{
st.push(e);
}
else
{
if (st.size() == 0)
return false;
char left = st.top();
if (e == ')' && left != '(')
return false;
if (e == ']' && left != '[')
return false;
if (e == '}' && left != '{')
return false;
st.pop();
}
}
return st.empty();
}
};
完