数据结构:栈

devtools/2024/9/22 22:20:35/

文章目录

  • 1. 栈的概念和结构
  • 2. 栈的顺序存储实现
    • 2.1 初始化
    • 2.2 入栈
    • 2.3 判断栈是否为空
    • 2.4 出栈
    • 2.5 取栈顶数据
    • 2.6 获取栈中有效元素个数
    • 2.7 打印栈中的数据
    • 2.8 销毁
  • 3. 栈的链式存储实现
    • 3.1 初始化
    • 3.2 判断栈是否为空
    • 3.3 入栈
    • 3.4 出栈
    • 3.5 取栈顶元素
    • 3.6 获取栈中有效元素个数
    • 3.7 打印栈中的数据
    • 3.8 销毁
  • 4. 栈的顺序存储和链式存储的对比

1. 栈的概念和结构

栈(Stack)是一种遵循后进先出(LIFO,Last In First Out)原则的有序集合。在栈中,新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一端就称为栈底。在栈的操作中,最后一个添加进栈的元素会是第一个被移除的。栈也被称为后进先出(LIFO)的线性表

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶

在这里插入图片描述

栈主要有两种存储方式来实现:一是顺序存储结构(数组),二是链式存储结构(链表)

2. 栈的顺序存储实现

栈的顺序存储结构通常由数组和一个记录栈顶元素的下一个位置的变量(下一个入栈的数据插入的位置)组成,但是当数组空间大小不够时还需要扩容,所以还要存储栈的空间大小的变量

用数组实现栈和之前顺序表的实现类似,对顺序表不熟悉的可以看一下我之前的博客:数据结构—顺序表

下面我们来定义一下栈的顺序存储结构和栈具体要实现哪些功能:

//定义栈的结构
typedef int STDataType;
typedef struct Stack {STDataType* arr;int top;//栈顶指针int capacity;//栈的空间大小
}ST;
//初始化
void STInit(ST* ps);
//销毁
void STDestory(ST* ps);
//入栈
void StackPush(ST* ps, STDataType x);
//出栈
void StackPop(ST* ps);
//取栈顶数据
STDataType StackTop(ST* ps);
//判断栈是否为空
bool StackEmpty(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
//打印栈中的数据
void STackPrint(ST* ps);

2.1 初始化

思路:将栈这个变量的地址传过来用一级指针接收即可,这样就实现了形参改变实参,在函数内部将指针置为NULL,指向栈顶元素下一个位置的变量和记录栈的空间大小的变量初始化为0即可。

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

2.2 入栈

思路:入栈前首先判断栈的空间是否足够,如果当ps->capacity == ps->top时,就说明栈的空间大小满了,需要对数组进行扩容realloc(因为是在原本空间大小下再开辟更大的空间),再进行入栈操作。如果栈的空间大小没满,直接将数据进行入栈操作。

void StackPush(ST* ps, STDataType x)
{assert(ps);//ps!=NULL//判断空间是否足够if (ps->capacity == ps->top){//空间不足就申请新空间int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;STDataType* tmp = (STDataType*)realloc(ps->arr, sizeof(STDataType)*newCapacity);//因为申请空间可能会失败,所以这里使用tmp来接收增容后的空间if (tmp == NULL){//申请失败perror("realloc fail!");//打印提示消息exit(1);//异常退出}//申请成功ps->arr = tmp;ps->capacity = newCapacity;}ps->arr[ps->top++] = x;
}

2.3 判断栈是否为空

思路:只需要判断指向栈顶元素下一个位置(下一个入栈的数据插入的位置)的top指针是否为0即可,如果为0,表示栈中没有数据即为空,反之,则栈不为空。

bool StackEmpty(ST* ps)
{assert(ps);//ps!=NULLreturn ps->top == 0;
}

2.4 出栈

思路:出栈前首先要判断栈是否为空,只有当栈不为空时才能出栈,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将top指针减一即可。

void StackPop(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空ps->top--;
}

2.5 取栈顶数据

思路:取栈顶数据前首先要判断栈是否为空,只有当栈不为空时才能取栈顶数据,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将栈顶数据返回即可,需要注意的是,栈顶数据的下标应该是top-1

STDataType StackTop(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));return ps->arr[ps->top - 1];
}

2.6 获取栈中有效元素个数

思路:因为每次入栈时,top都会加一,而出栈时都会减一,所以栈中有效元素的个数就为top

int STSize(ST* ps)
{assert(ps);//ps!=NULLreturn ps->top;
}

2.7 打印栈中的数据

思路:首先判断栈中的数据为不为空,如果为空就打印不了,直接报错。如果不为空,只需每次将栈顶元素打印,再出栈即可,当栈为空就停止循环,所以循环条件为栈不为空:!StackEmpty(ps)

void STackPrint(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空while (!StackEmpty(ps)){STDataType x = StackTop(ps);printf("%d ", x);StackPop(ps);}printf("\n");
}

2.8 销毁

思路:只有当栈不为空时才销毁,再使用free函数将栈的空间释放掉,随后将栈顶指针top和栈的空间大小置为0即可

void STDestory(ST* ps)
{assert(ps);//ps!=NULLif (ps->arr){//栈不为空,释放空间free(ps->arr);}ps->arr = NULL;ps->capacity = ps->top = 0;
}

3. 栈的链式存储实现

栈的链式存储结构为单链表,每次入栈时只需先申请一个新节点再改变指向栈顶的指针,出栈时就先销毁节点再改变指向栈顶的指针即可,所以我们得先定义一个栈的结构体来存放指向栈顶的指针和栈中有效数据个数,然后再定义一个链表节点的结构体来存放数据和指向下一个节点的指针

对链表不熟悉可以看一下我之前的博客:数据结构—单链表

下面我们来定义一下栈的链式存储结构和栈具体要实现哪些功能:

//定义栈的结构
typedef int STDataType;
typedef struct StackNode {STDataType data;struct StackNode* next;
}StackNode;
typedef struct Stack {StackNode* top;//栈顶int size;//有效数据的个数
}ST;
//初始化
void STInit(ST* ps);
//入栈
void StackPush(ST* ps, STDataType x);
//判断栈是否为空
bool StackEmpty(ST* ps);
//出栈
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
//打印栈中的数据
void STackPrint(ST* ps);
//销毁
void STDestory(ST* ps);

3.1 初始化

思路:将栈这个变量的地址传过来用一级指针接收即可,这样就实现了形参改变实参,在函数内部将指针top置为NULL,再将栈中有效数据的个数初始化为0即可。

void STInit(ST* ps)
{assert(ps);//ps!=NULLps->top = NULL;ps->size = 0;
}

3.2 判断栈是否为空

思路:如果栈顶指针ps->top为空(NULL),则栈为空,否则栈不为空

//判断栈是否为空
bool StackEmpty(ST* ps)
{assert(ps);//ps!=NULLreturn ps->top == NULL;
}

3.3 入栈

思路:先申请一个新节点,再调用StackEmpty函数判断栈是否为空,如果栈为空则栈顶就为新节点(将新节点newnode赋给栈顶),如果不为空,先将新节点newnode指向下一个节点的指针next指向栈顶,再更新栈顶指针指向即可,最后栈的有效数据个数再加一

void StackPush(ST* ps, STDataType x)
{assert(ps);//ps!=NULL//申请一个新节点StackNode* newnode = (StackNode*)malloc(sizeof(StackNode));if (newnode == NULL){//申请失败,打印错误信息perror("malloc fail!");exit(1);//异常退出}newnode->data = x;if (StackEmpty(ps)){//如果栈为空,将新节点newnode赋给栈顶ps->top = newnode;ps->top->next = NULL;}else{//如果栈不为空,更新栈顶指针即可newnode->next = ps->top;ps->top = newnode;}//每次入栈时有效数据+1ps->size++;
}

3.4 出栈

思路:因为要出栈,所以栈不能为空,首先要判断栈为不为空,可以使用断言assert(!StackEmpty(ps)),如果栈为空就报错。然后再判断栈是否只有一个数据,如果只有一个数据,直接将栈顶销毁再将栈顶指针置为空即可如果栈的数据个数大于1,可以先改变栈顶指针指向然后再销毁栈顶空间,也可以先销毁栈顶空间然后再改变栈顶指针指向,两种方法都可以,以下代码是第一种。注意每次出栈时有效数据都要减一

void StackPop(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空if (ps->size == 1 || ps->top->next == NULL){//如果栈中只有一个数据,直接销毁栈顶即可free(ps->top);ps->top = NULL;}else{//如果栈的数据个数大于1,先改变栈顶指针指向,再销毁栈顶空间即可StackNode* del = ps->top;ps->top = ps->top->next;free(del);//释放空间del = NULL;//指向空}//每次出栈有效数据减一ps->size--;
}

3.5 取栈顶元素

思路:取栈顶数据前首先要判断栈是否为空,只有当栈不为空时才能取栈顶数据,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将栈顶数据返回即可

STDataType StackTop(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空return ps->top->data;
}

3.6 获取栈中有效元素个数

思路:直接返回栈的有效数据个数size即可

int STSize(ST* ps)
{assert(ps);//ps!=NULLreturn ps->size;
}

3.7 打印栈中的数据

思路:首先判断栈中的数据为不为空,如果为空就打印不了,直接报错。如果不为空,只需每次将栈顶元素打印,再出栈即可,当栈为空就停止循环,所以循环条件为栈不为空:!StackEmpty(ps)

void STackPrint(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空while (!StackEmpty(ps)){STDataType data = ps->top->data;printf("%d ", data);StackPop(ps);}printf("\n");
}

3.8 销毁

思路:只有当栈不为空时才销毁,因为栈是由链表来实现的,所以需要将栈中的节点全部销毁掉。可以使用StackPop函数将每个栈顶数据都出栈,一直到栈为空为止,所以我们可以使用循环,而循环的条件为:!StackEmpty(ps)

void STDestory(ST* ps)
{assert(ps);//ps!=NULLassert(!StackEmpty(ps));//栈不能为空while (!StackEmpty(ps)){StackPop(ps);}
}

4. 栈的顺序存储和链式存储的对比

栈的顺序存储(顺序栈)和链式存储(链式栈)是栈的两种主要实现方式,它们在存储结构、操作效率、内存使用等方面存在一定的差异。以下是对这两种存储方式的详细对比:

一. 存储结构

顺序栈:

使用数组来存储栈中的元素。栈底通常固定为数组的起始位置,栈顶则随着元素的入栈和出栈而动态变化。需要一个额外的变量(如top指针)来记录栈顶元素在数组中的位置

链式栈:

使用链表来存储栈中的元素。栈顶元素通常位于链表的头部,这样方便进行入栈和出栈操作。链式栈不需要像顺序栈那样预先分配固定大小的存储空间,每次入栈时申请一个结点空间,然后再改变top指针指向即可,因此更加灵活。

二. 操作效率

顺序栈:

入栈和出栈操作的时间复杂度通常为O(1),因为只需要在数组的末尾进行插入或删除操作。但是,当栈满时,如果数组空间不足,则需要进行扩容操作,这可能会增加额外的时间开销。

链式栈:

入栈和出栈操作的时间复杂度也通常为O(1),因为只需要申请新节点空间或者销毁栈顶节点空间,然后改变top指针指向即可。链式栈不需要扩容操作,因为链表的大小可以动态调整。

三. 内存使用

顺序栈:

内存使用效率较高,因为数组中的元素在内存中连续存储,有利于缓存的利用。
但是,如果分配的数组空间过大,而实际使用的元素较少,则会造成内存空间的浪费

链式栈:

内存使用效率相对较低,因为链表中每个节点除了存储数据外,还需要额外的空间来存储指向下一个节点的指针。但是,链式栈可以根据需要动态分配内存空间,避免了顺序栈中可能存在的内存浪费问题

四. 适用场景

顺序栈:

适用于栈的大小可以事先确定或栈的大小变化不大的场景。
当对栈的操作主要是入栈和出栈时,顺序栈通常具有更好的性能。

链式栈:

适用于栈的大小变化较大或需要频繁进行扩容操作的场景。
当栈的大小无法事先确定或需要动态调整栈的大小时,链式栈更加灵活方便。

综上所述,顺序栈和链式栈各有优缺点,选择哪种存储方式取决于具体的应用场景和需求。在实际应用中,可以根据栈的大小变化、操作频率、内存使用效率等因素来综合考虑选择哪种存储方式。

对以上内容由不同看法的欢迎各位大佬来讨论,希望对大家学习有帮助,多多支持哦!


http://www.ppmy.cn/devtools/84857.html

相关文章

《企业实战分享 · CodeGeeX 初体验》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗 🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数…

Java面试八股之什么是声明式事务管理,spring怎么实现声明式事务管理?

什么是声明式事务管理,spring怎么实现声明式事务管理? 声明式事务管理是一种编程范式,它允许开发人员通过声明性的配置或注解,而不是硬编码事务处理逻辑,来指定哪些方法或类应该在其上下文中执行事务。这种方法将事务…

Vue学习---vue 防抖处理函数,是处理什么场景

Vue防抖处理函数是用来处理在快速连续操作中,只执行最后一次操作的情况。 例如,在输入框输入时,我们可能希望只在用户完成输入后进行处理,而不是在每次键入时都处理。(n秒后触发一次) 以下是一个简单的Vue防抖处理函数的例子&am…

LeetCode206 反转链表

前言 题目: 206. 反转链表 文档: 代码随想录——反转链表 编程语言: C 解题状态: 有了思路以后没敢尝试 思路 需要注意的是创建指针不会申请额外的内存空间。 代码 方法一: 双指针法/迭代 我的理解是创建了三个指针…

深入理解二叉搜索树:定义、操作及平衡二叉树

引言 二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树结构,每个节点的左子树节点值小于根节点值,而右子树节点值大于根节点值。二叉搜索树在计算机科学中有着广泛的应用,尤其在动态查找表和优先队列…

关于iphone不能下载三方软件

iPhone 不能下载第三方软件的原因主要是因为苹果公司严格控制其应用生态系统,确保所有应用都通过其官方的 App Store 分发。这有几个主要原因: 安全性:苹果公司希望通过这种方式减少恶意软件的传播,保护用户的隐私和数据安全。所…

使用集成线性 LED 驱动器替代分立 LED 电路设计

在转向灯、刹车灯和尾灯等汽车照明中,LED 电路设计通常采用分立元件,如双极结晶体管 (BJT)。分立元件之所以突出有几个常见原因:它们简单、可靠且便宜。然而,随着 LED 数量和项目要求的增加,重新考虑离散设计可能是值得…

vue 一个数组 获取最大值与最小值

<template><div>最小值: {{ minValue }}最大值: {{ maxValue }}</div> </template><script> export default {data() {return {numbers: [10, 2, 33, 4, 55, 6]};},computed: {minValue() {return Math.min(...this.numbers);},maxValue() {retu…