什么是栈,为什么函数式编程语言都离不开栈?

简介: 一、什么是栈,什么是FILO​ 栈是一种具有特殊访问方式的存储空间,它的特殊性在于,最后进入这个空间的数据,最先出去,可以画图来描述一下这种操作方式。假设有一个盒子和三本书,依次将三本书他们放入盒子中。

一、什么是栈,什么是FILO

栈是一种具有特殊访问方式的存储空间,它的特殊性在于,最后进入这个空间的数据,最先出去,可以画图来描述一下这种操作方式。

假设有一个盒子和三本书,依次将三本书他们放入盒子中。

入栈模拟图


9e9742afdadb4289badaae1b5a39455d.gif

现在有一个问题,如果一次只能取一本,我们如何将书从盒子中取出来?

显然必须从盒子的最上边开始取,依次取出,和放入的顺序刚好相反。

出栈模拟图

748e34eac86e43c99c2fed6b3473ceed.gif

从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。

如果说,上图中的盒子就是一个栈,我们可以看出栈的两个基本操作:入栈和出栈。入栈就是将一个新元素放到栈顶,出栈就是从栈顶取出一个元素,栈顶的元素总是最后入栈,需要出栈时,有最先被从栈中取出。栈的这种操作被称为:LIFO(last in first out),后进先出

二、栈的作用是什么,为什么编程语言函数调用都选择用栈?

为何现如今所有的编程语言都会采用栈来进行函数调用?函数调用的状态之所以用栈来记录是因为这些数据的存活时间满足“先进后出”(FILO)顺序,而栈的基本操作正好就是支持这种顺序访问的。下面用一组程序举例。

void test1(){}
void test2() {
  test1();
}
void test3() {
  test2();
}
int main() {
  test3();
}

这个程序运行的顺序为:main() ——》 test3() ——》 test2() ——》 test1() ——》 return from test1() ——》 return from test2() ——》 return from test3() ——》 return from test4().

可以看到,调用者的生命周期总是长于被调用者的生命周期,并且后者在前者之内。被调用者的数据总是后于调用者的,而其释放顺序总是先于调用者,所以正好可以满足LIFO顺序,选用栈这种数据结构来实现函数调用则是一种自然而然的选择。

image.gif

三、使用C模拟实现解析栈

前面说过栈的两个基本操作,入栈和出栈,都只是对栈顶进行操作,栈可以用数组实现也可以用链表实现,这里讲解用数组实现,因为数组实现相对来说更优一些,数组实现在尾部插入数据代价比较小。

1.结构体的定义

由于各种操作都只在栈顶进行,选择定义一个top来记录栈顶的位置,为了节省空间选择定义一个capacity来记录最大长度,如果等于top的话进行扩容。

typedef int STDataType;
typedef struct StackNode {
  STDataType* arr;
  int capacity;
  int top;
}STNode;

2.栈的创建及销毁

由于函数内有对参数指针的引用,加上assert预防程序崩溃,易于调试,top初始化为-1(也可以初始化为0,个人偏向初始化为-1),两者的区别就是一个是栈顶指向栈顶数据,一个指向栈顶数据的下一个,其实本质上差不多。

void STInit(STNode* pst)
{
  assert(pst);
  pst->arr = NULL;
  pst->capacity = 0;
  pst->top = -1;
}
void STDestroy(STNode* pst)
{
  assert(pst);
  free(pst->arr);
  pst->arr = NULL;
  pst->capacity = 0;
  pst->top = 0;
}

3.实现入栈操作

如果栈已经满了,则进行扩容,将数据放到栈顶

void STPush(STNode* pst,STDataType x)
{
  assert(pst);
  if (pst->capacity==pst->top+1)
  {
    int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
    STDataType* temp = NULL;
    temp = (STDataType*)realloc(pst->arr,sizeof(STDataType) * newcapacity);
    if (temp == NULL)
    {
      perror("malloc error");
      return;
    }
    pst->arr = temp;
    pst->capacity = newcapacity;
  }
  pst->arr[++pst->top] = x;
}

4.获取栈顶元素及出栈操作

出栈操作将栈顶元素向下移动即可,甚至无需删除数据,因为再次进行入栈会将其覆盖掉。获取栈顶元素要获取top的下一个位置,因为top是先存后加

bool STEmpty(STNode* pst)
{
  return pst->top == -1;
}
void STPop(STNode* pst)
{
  assert(pst);
  assert(!STEmpty(pst));
  pst->top--;
}
STDataType STTop(STNode* pst)
{
  assert(pst);
  assert(!STEmpty(pst));
  return pst->arr[pst->top];
}

5.获取栈中有效元素个数

由于数组偏移度是从0开始存数据,所以指向栈顶的top比实际上个数要少1,真正有效元素个数要将top+1。

int StackSize(STNode* pst)
{
  assert(pst);
  return pst->top+1;
}

源代码分享

//stack.h
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct StackNode {
  STDataType* arr;
  int capacity;
  int top;
}STNode;
void STInit(STNode* pst);
void STPush(STNode* pst,STDataType);
void STPop(STNode* pst);
void STDestroy(STNode* pst);
bool STEmpty(STNode* pst);
STDataType STTop(STNode* pst);
//stack.c
#include "stack.h"
bool STEmpty(STNode* pst)
{
  return pst->top == -1;
}
void STInit(STNode* pst)
{
  assert(pst);
  pst->arr = NULL;
  pst->capacity = 0;
  pst->top = -1;
}
void STDestroy(STNode* pst)
{
  assert(pst);
  free(pst->arr);
  pst->arr = NULL;
  pst->capacity = 0;
  pst->top = 0;
}
void STPush(STNode* pst,STDataType x)
{
  assert(pst);
  if (pst->capacity==pst->top+1)
  {
    int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
    STDataType* temp = NULL;
    temp = (STDataType*)realloc(pst->arr,sizeof(STDataType) * newcapacity);
    if (temp == NULL)
    {
      perror("malloc error");
      return;
    }
    pst->arr = temp;
    pst->capacity = newcapacity;
  }
  pst->arr[++pst->top] = x;
}
void STPop(STNode* pst)
{
  assert(pst);
  assert(!STEmpty(pst));
  pst->top--;
}
STDataType STTop(STNode* pst)
{
  assert(pst);
  assert(!STEmpty(pst));
  return pst->arr[pst->top];
}
int StackSize(STNode* pst)
{
  assert(pst);
  return pst->top+1;
}
//test.c
#include "stack.h"
void test1()
{
  STNode ST;
  STInit(&ST);
  STPush(&ST, 1);
  STPush(&ST, 2);
  STPush(&ST, 3);
  STPush(&ST, 4);
  STPush(&ST, 5);
  STPush(&ST, 6);
  STPush(&ST, 7);
  while (!STEmpty(&ST))
  {
    printf("%d ", STTop(&ST));
    STPop(&ST);
  }
  STDestroy(&ST);
}
int main()
{
  test1();
}

d7d3c9764adc43d09c554697b8c3b851.gif

✨本文收录于数据结构理解与实现

下几期会继续带来栈的练习题以及与栈刚好相反的堆(FIFO)。如果文章对你有帮助记得点赞收藏关注。






































相关文章
|
2天前
|
存储
|
17天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
19天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
123 3
|
20天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
23 0
【数据结构】栈和队列的深度探索,从实现到应用详解
|
1月前
栈的几个经典应用,真的绝了
文章总结了栈的几个经典应用场景,包括使用两个栈来实现队列的功能以及利用栈进行对称匹配,并通过LeetCode上的题目示例展示了栈在实际问题中的应用。
栈的几个经典应用,真的绝了
|
24天前
|
Linux C++ Windows
栈对象返回的问题 RVO / NRVO
具名返回值优化((Name)Return Value Optimization,(N)RVO)是一种优化机制,在函数返回对象时,通过减少临时对象的构造、复制构造及析构调用次数来降低开销。在C++中,通过直接在返回位置构造对象并利用隐藏参数传递地址,可避免不必要的复制操作。然而,Windows和Linux上的RVO与NRVO实现有所不同,且接收栈对象的方式也会影响优化效果。
|
1月前
|
负载均衡 网络协议 安全
DKDP用户态协议栈-kni
DKDP用户态协议栈-kni
|
1月前
|
存储 安全 编译器
缓冲区溢出之栈溢出(Stack Overflow
【8月更文挑战第18天】
64 3
|
1月前
|
负载均衡 网络协议 安全
DPDK用户态协议栈-KNI
DPDK用户态协议栈-KNI
|
1月前
|
测试技术
【初阶数据结构篇】栈的实现(附源码)
在每一个方法的第一排都使用assert宏来判断ps是否为空(避免使用时传入空指针,后续解引用都会报错)。