当前位置: 首页 > article >正文

【数据结构与算法】走进数据结构的“时间胶囊”——栈

大家好,我是小卡皮巴拉

文章目录

目录

引言

一.栈的基本概念

1.1 定义

1.2 特性

1.3 基本操作

二.栈的实现方式

2.1 顺序栈

2.2 链栈

三.顺序栈的实现

定义顺序栈的结构

初始化

入栈

检查栈是否为空

出栈

销毁

四.链栈的实现

定义链栈的结构

初始化

 入栈

检查栈是否为空

出栈

销毁

兄弟们共勉!!!  


每篇前言

博客主页:小卡皮巴拉 

咱的口号:🌹小比特,大梦想🌹

作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。

引言

在数据结构的大家庭中,栈以其独特的“后进先出”特性脱颖而出,就像是一个记录时间流逝的“时间胶囊”。每当有新数据加入时,它就被放置在栈顶,而最早加入的数据则静静地躺在栈底,等待被最后一个取出。

栈不仅是一个简单的数据结构,更是算法设计和程序优化中的得力助手。从函数调用栈的管理到递归问题的求解,栈的身影无处不在。它以其简洁明了的数据操作规则和高效的内存利用方式,成为了程序员们解决复杂问题时的首选工具。

在接下来的篇章中,我们将一起探索栈的奥秘,从定义到特性,再到丰富的应用场景,帮助大家真正掌握这一数据结构的核心知识。

一.栈的基本概念

1.1 定义

栈(Stack)是一种只允许在一端进行插入或删除的线性表。这端被称为栈顶(Top),另一端则被称为栈底(Bottom)。栈遵循后进先出(Last In First Out,LIFO)的原则,即最后插入的元素会第一个被删除。

1.2 特性

  • 后进先出:栈中最后一个插入的元素首先被删除。这是栈最显著的特点,也是它与其他线性表(如数组、链表)的主要区别。

  • 栈顶浮动,栈底固定:栈顶的位置随着元素的入栈和出栈而变化,而栈底则保持不变。

  • 不支持随机访问:栈的结构决定了只能在栈顶进行插入和删除操作,无法直接访问和修改栈中间的元素。

1.3 基本操作

栈的基本操作主要包括以下几种:

  1. 初始化栈:创建一个空栈,并设置栈底指针和栈顶指针。

  2. 判断栈是否为空:检查栈中是否包含元素,若栈为空则返回true,否则返回false。

  3. 入栈操作(Push):将新元素添加到栈顶。若栈未满,则将新元素放入栈中,并更新栈顶指针。

  4. 出栈操作(Pop):从栈顶移除元素。若栈非空,则弹出栈顶元素,并更新栈顶指针。

  5. 读取栈顶元素(GetTop/Peek):返回栈顶元素的值,但不删除它。若栈非空,则返回栈顶元素的值。

  6. 销毁栈:释放栈占用的存储空间。

二.栈的实现方式

栈的实现方式主要有两种:

2.1 顺序栈

利用数组实现,附设一个指针(top)指示当前栈顶元素的位置。顺序栈需要预分配一块连续的存储空间,其优点是访问速度快,但缺点是存在栈满上溢的风险。

2.2 链栈

采用链表实现,通常使用单链表,并规定所有操作都在链表的表头进行。链栈的优点是便于多个栈共享存储空间、不存在栈满上溢的情况,且插入和删除操作的时间复杂度为O(1)。但缺点是需要额外的指针存储空间。

三.顺序栈的实现

定义顺序栈的结构

首先,我们定义了一个名为STDataType的类型别名,它基于int类型。这个类型别名将用于栈中存储的数据元素。

接着,我们定义了一个名为Stack的结构体(通过typedef,我们也可以用ST来引用它)。这个结构体代表了一个栈的数据结构,并包含以下三个成员:

  1. arr:这是一个指向STDataType类型的指针。它用于动态分配数组,以存储栈中的元素。

  2. top:这是一个整型变量,表示栈顶的位置。在栈中,我们通常将数组的最后一个有效元素的位置视为栈顶。注意,这里的top成员变量实际上存储的是栈顶元素的索引(即数组中的位置),而不是指向栈顶元素的指针。因此,栈为空时,top的值通常被初始化为-1或栈的容量减1(取决于栈的实现方式)。

  3. capacity:这是一个整型变量,表示栈的容量,即栈能够存储的最大元素数量。这个值在栈的初始化时被设定,并且决定了为arr成员动态分配多少内存。

函数代码:

typedef int STDataType;
//定义栈的数据结构
typedef struct Stack
{
	STDataType* arr;
	int top;//指向栈顶位置
	int capacity;
}ST;

初始化

STInit函数用于初始化一个栈。它接收一个指向栈的指针ps作为参数。

  1. 检查指针:首先,使用assert确保传入的栈指针ps不是NULL

  2. 设置初始状态

    • 将栈的数组指针ps->arr设置为NULL,表示栈尚未分配存储空间。
    • 将栈顶位置ps->top设置为0,表示栈为空。
    • 将栈容量ps->capacity也设置为0,表示栈当前没有预设容量。

这样,栈就被初始化为一个空栈,准备进行后续的栈操作。

函数代码:

//初始化
void STInit(ST* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->top = ps->capacity = 0;
}

入栈

StackPush函数用于将一个元素压入栈顶。它接收两个参数:一个指向栈的指针ps和要压入栈的元素x

  1. 检查指针:首先,使用assert确保传入的栈指针ps不是NULL

  2. 检查容量:接着,检查栈是否已满(即栈顶位置ps->top是否等于栈容量ps->capacity)。

    • 如果栈已满,则进行扩容操作。首先计算新的容量newCapacity,如果当前容量为0,则新容量为4(初始容量),否则新容量为当前容量的两倍。
    • 使用realloc函数尝试为栈的数组ps->arr分配新的内存空间。如果realloc失败(返回NULL),则打印错误信息并退出程序。
    • 如果realloc成功,则更新栈的数组指针ps->arr和栈容量ps->capacity
  3. 入栈操作:在确认栈有足够的空间后,将元素x放入栈顶位置(即ps->arr[ps->top]),并将栈顶位置ps->top加1,以指向下一个空位置。

这样,元素x就被成功压入栈顶,栈的栈顶位置和容量(如果需要的话)也得到了更新。

函数代码:

void StackPush(ST* ps, STDataType x)  
{  
    assert(ps); // 确保栈指针有效  
      
    // 检查栈是否已满  
    if (ps->top == ps->capacity)  
    {  
        // 扩容操作  
        int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;  
        STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));  
        if (tmp == NULL)  
        {  
            perror("realloc fail!\n");  
            exit(1); // 扩容失败,退出程序  
        }  
        ps->arr = tmp; // 更新栈的数组指针  
        ps->capacity = newCapacity; // 更新栈的容量  
    }  
      
    // 入栈操作  
    ps->arr[ps->top++] = x; // 将元素放入栈顶,并更新栈顶位置  
}

检查栈是否为空

StackEmpty函数用于判断一个栈是否为空。它接收一个指向栈的指针ps作为参数,并返回一个布尔值。

  1. 检查指针:首先,使用assert确保传入的栈指针ps不是NULL

  2. 判断栈是否为空:然后,通过比较栈顶位置ps->top是否等于0来判断栈是否为空。如果ps->top等于0,表示栈中没有元素,栈为空;否则,栈不为空。

  3. 返回结果:最后,根据判断结果返回相应的布尔值。如果栈为空,则返回true(或等价地,返回非零值,但在C语言中通常使用true1来表示真);如果栈不为空,则返回false(或等价地,返回零值)。

函数代码:

#include <stdbool.h> // 包含头文件以支持布尔值  
  
bool StackEmpty(ST* ps)  
{  
    assert(ps); // 确保栈指针有效  
    return ps->top == 0; // 判断栈是否为空,并返回结果  
}

出栈

StackPop函数用于从栈中移除栈顶元素。它接收一个指向栈的指针ps作为参数,并不返回任何值(即返回类型为void)。

  1. 检查栈是否为空:首先,使用assert宏和StackEmpty函数来确保栈不为空。如果栈为空,则StackEmpty(ps)将返回true(或非零值),导致assert失败,并可能终止程序(这取决于assert宏的具体实现和编译器的设置)。这是一种调试时的安全措施,用于防止对空栈进行出栈操作。

  2. 出栈操作:在确认栈不为空后,通过将栈顶位置ps->top减1来实现出栈操作。这实际上是将栈顶指针向下移动一位,从而“移除”了栈顶元素(但请注意,元素的值仍然保留在内存中,只是不再被视为栈的一部分)。

 函数代码:

#include <assert.h> // 包含头文件以支持assert宏  
#include "stack.h"  // 假设StackEmpty函数和ST结构体定义在这个头文件中  
  
void StackPop(ST* ps)  
{  
    assert(!StackEmpty(ps)); // 确保栈不为空  
    --ps->top; // 将栈顶位置减1,实现出栈操作  
}

取栈顶元素

StackTop函数用于获取栈顶元素的值,但不移除该元素。它接收一个指向栈的指针ps作为参数,并返回栈顶元素的值。

  1. 检查栈是否为空:首先,使用assert宏和StackEmpty函数来确保栈不为空。如果栈为空,则StackEmpty(ps)将返回true(或非零值),导致assert失败,可能会终止程序(这取决于assert宏的具体实现和编译器的设置)。这是一种调试时的安全措施,用于防止对空栈进行取栈顶元素操作。

  2. 获取栈顶元素:在确认栈不为空后,通过访问栈数组ps->arr中栈顶位置ps->top的前一个元素(即ps->arr[ps->top - 1])来获取栈顶元素的值。由于栈顶位置ps->top是指向栈中下一个空位置的索引,因此栈顶元素实际上是位于ps->top - 1的索引处。

  3. 返回栈顶元素的值:最后,函数返回栈顶元素的值。

 函数代码:

#include <assert.h> // 包含头文件以支持assert宏  
#include "stack.h"  // 假设StackEmpty函数、ST结构体和STDataType类型定义在这个头文件中  
  
STDataType StackTop(ST* ps)  
{  
    assert(!StackEmpty(ps)); // 确保栈不为空  
    return ps->arr[ps->top - 1]; // 返回栈顶元素的值  
}

销毁

STDestroy函数用于释放栈所占用的内存资源,并将栈结构体成员重置为初始状态。它接收一个指向栈的指针ps作为参数,并不返回任何值(即返回类型为void)。

  1. 释放栈数组内存:首先,函数检查栈数组指针ps->arr是否不为NULL。如果不为NULL,说明栈数组已经分配了内存,因此使用free函数释放这块内存。这是为了防止内存泄漏,即程序不再需要的内存没有被释放回系统。

  2. 重置栈结构体成员:在释放栈数组内存后,函数将栈结构体成员ps->arr重置为NULL,表示栈数组指针不再指向任何有效的内存区域。同时,将栈的容量ps->capacity和栈顶位置ps->top都重置为0,表示栈现在是空的,且没有分配任何内存。

函数代码:

#include <stdlib.h> // 包含头文件以支持free函数  
#include "stack.h"  // 假设ST结构体定义在这个头文件中  
  
void STDestroy(ST* ps)  
{  
    if (ps->arr != NULL)  
        free(ps->arr); // 释放栈数组内存  
  
    ps->arr = NULL;    // 重置栈数组指针为NULL  
    ps->capacity = 0;  // 重置栈容量为0  
    ps->top = 0;       // 重置栈顶位置为0  
}

四.链栈的实现

定义链栈的结构

我们定义了两个主要结构来支持链栈的实现:

链栈节点结构 (StackNode):

  • STDataType data;:存储节点中的数据,STDataType 在此例中为 int 类型。
  • struct StackNode* next;:指向链栈中下一个节点的指针。若当前节点为链栈末尾,则此指针为 NULL

链栈结构 (LinkedStack):

  • StackNode* top;:栈顶指针,指向链栈的顶部节点(即最后压入的节点)。若链栈为空,则此指针为 NULL
  • // int capacity;:对于链栈,由于其容量是动态的(随着节点的添加和删除而变化),因此这个容量字段在此场景中通常不需要。它已被注释掉,表明在链栈的实现中不考虑此字段。

链栈(或链式栈)是基于链表实现的栈数据结构,它不需要预先分配固定大小的内存空间,能够动态地增长和缩小。数据压入(push)时,新节点被添加到栈顶;数据弹出(pop)时,栈顶节点被移除。

函数代码:

// 定义栈的数据类型  
typedef int STDataType;

// 定义链栈的节点结构  
typedef struct StackNode {
    STDataType data;
    struct StackNode* next;
} StackNode;

// 定义链栈的结构,包含栈顶指针和栈的容量(对于链栈,容量通常不是必需的,但这里为了完整性保留)  
typedef struct LinkedStack {
    StackNode* top;
    // int capacity; // 对于链栈,容量是动态的,因此不需要这个字段  
} LinkedStack;

初始化

函数名:initStack

目的:初始化链栈,确保栈顶指针指向NULL,从而表明链栈当前为空。

参数:LinkedStack* stack,这是一个指向链栈结构体的指针,用于访问和修改链栈的状态。

函数体详细解析:

 stack->top = NULL;

  • 这行代码执行了链栈初始化的核心操作。
  • stack->top 是对链栈结构体中栈顶指针成员的访问。
  • 将 stack->top 设置为 NULL 表示链栈当前不包含任何节点,即栈为空。

函数代码:

// 初始化链栈  
void initStack(LinkedStack* stack) {
    stack->top = NULL; // 栈顶指针初始化为NULL,表示空栈  
}

 入栈

函数名:push

目的:将新数据压入链栈中,使其成为栈顶元素。

参数:

  • LinkedStack* stack:指向链栈结构体的指针,用于访问和修改链栈的状态。
  • STDataType data:要压入链栈的数据。

函数体详细解析:

  1. 创建新节点
    • StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
    • 这行代码通过malloc函数动态分配内存,用于存储新节点。
    • sizeof(StackNode)计算了StackNode结构体所需的内存大小。
    • (StackNode*)是类型转换,将void*类型的malloc返回值转换为StackNode*类型。
  2. 内存分配失败处理
    • 检查newNode是否为NULL,以判断内存分配是否成功。
    • 若内存分配失败,则打印错误信息并退出程序。
  3. 设置新节点的数据
    • newNode->data = data;
    • 将函数参数data的值赋给新节点的data成员。
  4. 链接新节点到链栈
    • newNode->next = stack->top;
    • 将新节点的next指针指向当前的栈顶节点。
    • 这一步实现了链栈的“后进先出”(LIFO)特性,因为新节点被放在了链栈的顶部。
  5. 更新栈顶指针
    • stack->top = newNode;
    • 将栈顶指针指向新节点,完成压栈操作。

函数代码:

// 压栈操作  
void push(LinkedStack* stack, STDataType data) {
    StackNode* newNode = (StackNode*)malloc(sizeof(StackNode)); // 创建新节点  
    if (!newNode) {
        printf("Memory allocation failed\n");
        exit(EXIT_FAILURE);
    }
    newNode->data = data; // 设置新节点的数据  
    newNode->next = stack->top; // 新节点的next指向当前的栈顶节点  
    stack->top = newNode; // 栈顶指针指向新节点  
}

检查栈是否为空

函数名:isEmpty

目的:判断链栈是否为空。

参数:LinkedStack* stack,指向链栈结构体的指针,用于访问链栈的栈顶指针。

函数体详细解析:

  1. 判断链栈是否为空
    • return stack->top == NULL;
    • 这行代码通过比较栈顶指针stack->top是否为NULL来判断链栈是否为空。
    • stack->topNULL,则链栈为空,函数返回1
    • stack->top不为NULL,则链栈不为空,函数返回0

函数代码:

// 判断链栈是否为空  
int isEmpty(LinkedStack* stack) {
    return stack->top == NULL;
}

出栈

函数名称:pop

函数目的:从链栈中移除栈顶元素,并返回该元素的数据。

函数参数:

  • LinkedStack* stack:指向链栈结构体的指针,包含了链栈的栈顶指针和可能的其他管理信息。

函数返回值:

  • STDataType:被移除的栈顶元素的数据。

函数体解析:

  1. 检查链栈是否为空
    • if (isEmpty(stack)) { ... }
      • 调用isEmpty函数检查链栈是否为空。
      • 如果链栈为空,则打印错误信息“Stack underflow”,表示尝试从空栈中出栈。
      • 通过exit(EXIT_FAILURE)终止程序。这是一种错误处理方式,也可以选择返回一个特殊值来表示错误,但这里选择直接退出程序。
  2. 移除栈顶元素
    • StackNode* tempNode = stack->top;
      • 创建一个临时指针tempNode,并将其指向当前的栈顶节点。
    • STDataType poppedData = tempNode->data;
      • 从栈顶节点中获取数据,并将其存储在局部变量poppedData中。
    • stack->top = tempNode->next;
      • 将栈顶指针更新为指向下一个节点,这样原来的栈顶节点就被从链栈中移除了。
  3. 释放内存
    • free(tempNode);
    • 释放之前被移除的栈顶节点的内存。这是内存管理的重要步骤,防止内存泄漏。
  4. 返回数据
    • return poppedData;
      • 返回被移除的栈顶元素的数据。

函数代码:

// 出栈操作  
STDataType pop(LinkedStack* stack) {
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        exit(EXIT_FAILURE); // 或者返回一个特殊值表示错误,这里选择退出程序  
    }
    StackNode* tempNode = stack->top; // 临时节点指向栈顶节点  
    STDataType poppedData = tempNode->data; // 获取栈顶节点的数据  
    stack->top = tempNode->next; // 栈顶指针指向下一个节点  
    free(tempNode); // 释放栈顶节点的内存  
    return poppedData; // 返回出栈的数据  
}

销毁

函数名称destroyStack

函数目的:释放链栈中所有节点的内存,从而销毁链栈。

函数参数

  • LinkedStack* stack:指向链栈结构体的指针,包含了链栈的栈顶指针和可能的其他管理信息。

函数解析

destroyStack函数通过循环调用pop函数来逐个移除链栈中的节点,并在每次移除节点时释放其内存。这个过程会一直持续到链栈为空。

  1. 循环检查链栈是否为空
    • 使用while (!isEmpty(stack))循环,只要链栈不为空,就继续执行循环体内的操作。
  2. 出栈操作
    • 在循环体内,调用pop(stack)函数。pop函数会移除链栈的栈顶节点,并释放其内存。同时,pop函数还会返回被移除节点的数据,但在destroyStack函数中,我们并不关心这个返回值。
  3. 无需额外销毁操作
    • 由于pop函数已经负责释放了所有节点的内存,因此destroyStack函数本身不需要进行额外的销毁操作。但是,为了保持接口的一致性(例如,与其他数据结构的销毁函数保持类似的签名),destroyStack函数还是被保留了下来。
  4. 函数结束
    • 当链栈为空时,isEmpty(stack)将返回truewhile循环将终止,destroyStack函数也随之结束。此时,链栈中的所有节点都已经被释放,链栈已被销毁。

函数代码:

// 销毁链栈(释放所有节点的内存)  
void destroyStack(LinkedStack* stack) {
    while (!isEmpty(stack)) {
        pop(stack); // 通过出栈操作释放所有节点的内存  
    }
    // 对于链栈,由于我们已经在出栈时释放了所有节点的内存,因此不需要额外的销毁操作  
    // 但为了保持接口的一致性,这里还是保留了这个函数  
}

兄弟们共勉!!!  

码字不易,求个三连

抱拳了兄弟们!


http://www.kler.cn/news/360692.html

相关文章:

  • go开发过程中mapstructure使用,
  • Windows性能监控与调优:让电脑运行如飞
  • vulnhub靶场之digitalworld.local DEVELOPMENT
  • LabVIEW中句柄与引用
  • 基于MATLAB的实现垃圾分类Matlab源码
  • Linux之实战命令41:lshw应用实例(七十五)
  • springboot3.x.x 集成 连接SQL Server 2008 驱动版本和SSL套接字问题的解决
  • 视频网站开发:Spring Boot框架的深入探索
  • 解决MybatisPlus updateById更新数据时将没传的数据也更新成了null
  • 梦熊 CSP—S模拟赛 T2youyou不喜欢夏天
  • vue3 解决背景图与窗口留有间隙的问题
  • 【linux 多进程并发】0301 Linux创建后台服务进程,daemon进程,自己的进程可以被一号进程接管啦
  • 电影评论网站:Spring Boot技术应用案例
  • 银行数字化转型导师坚鹏:2025年银行开门红8大思考
  • 代码训练营 day36|LeetCode 56,LeetCode 738
  • 架构设计笔记-20-补充知识
  • 苍穹外卖--开发记录day07
  • 64-基于TMS320C6455、XC5VSX95T 的6U CPCI无线通信处理平台
  • 数据脱敏方案总结
  • 下载Vue脚手架