C语言数据结构——栈

目录

​编辑

0.前言

1.栈的基本概念

2.栈的实现

2.1数组OR链表?

2.2静态栈的实现

2.3动态栈的实现

3.栈的应用

4.结语


(图片由AI生成) 

0.前言

在计算机科学中,数据结构是组织、管理和存储数据的有效方式,以便可以高效地访问和修改数据。栈是一种基本的数据结构,它遵循特定的操作顺序,即后进先出(LIFO)。由于其简单和高效的特性,在许多算法和系统功能中都有应用,如系统调用栈、表达式求值和回溯算法等。

1.栈的基本概念

栈是一种特殊的线性数据结构,它遵循特定的操作顺序,即后进先出(Last In First Out, LIFO)。这意味着最后添加到栈中的元素将是第一个被移除的元素。栈的这种特性使得它在处理具有递归性质的问题和算法中非常有用。

特点

  • 后进先出:栈的基本操作确保了最后被添加的元素总是第一个被移除。
  • 单端操作:所有的添加和移除操作仅在栈的同一端进行,这一端被称为“栈顶”,另一端称为“栈底”。

基本操作

栈的操作通常包括以下几种:

  • Push(入栈):将一个元素添加到栈顶。这是一个添加操作,将元素放置在栈的最上方。
  • Pop(出栈):从栈顶移除一个元素,并返回被移除的元素。这是一个移除操作,它删除并返回栈顶元素。
  • Peek 或 Top(查看栈顶元素):返回栈顶元素,但不从栈中移除它。这允许我们查看栈顶的元素而不改变栈的状态。
  • IsEmpty(检查栈是否为空):确定栈是否为空。如果栈中没有元素,此操作返回 true;否则,返回 false。
  • Size(获取栈的大小):返回栈中元素的数量。这允许我们了解栈中存储了多少元素。

下面这幅图片能够更好地反映栈的结构以及上面的操作:(图源网络,侵删)

2.栈的实现

2.1数组OR链表?

实现栈的两种常见方法是使用数组和链表。每种方法都有其独特的优势和劣势,选择哪种方式取决于具体的应用场景和需求。下面我们将详细探讨使用数组和链表实现栈的优势和劣势,并解释为什么在某些情况下数组可能是更好的选择。

使用数组实现栈的优势:

  • 随机访问:数组提供了O(1)的时间复杂度来访问任何一个元素,这使得访问栈顶元素非常快速。
  • 内存连续性:数组在内存中是连续存储的,这有助于提高空间和缓存的效率。
  • 简单性:使用数组实现栈的代码通常更简单直观,特别是在栈操作不频繁或栈大小固定时。

使用数组实现栈的劣势:

  • 固定大小:数组的大小在初始化时就固定了,这限制了栈的最大容量,可能会导致栈溢出。
  • 扩容问题:如果栈满了需要扩容,就必须创建一个更大的数组并复制原数组的元素,这个过程的时间复杂度是O(n)。

使用链表实现栈的优势:

  • 动态大小:链表实现的栈可以动态地增长和缩小,不受固定大小的限制,从而避免了栈溢出的问题。
  • 内存利用率:链表是非连续存储的,它可以更好地利用分散的内存空间,理论上可以达到系统内存的限制。

使用链表实现栈的劣势:

  • 内存开销:链表的每个元素都需要额外的空间来存储指向下一个元素的指针。
  • 时间开销:相比数组实现,链表访问和操作元素(特别是非栈顶元素)的时间开销较大。
  • 缓存不友好:由于链表的元素在内存中是非连续存储的,它不如数组那样缓存友好。

为什么数组更好?

在特定的应用场景下,数组实现的栈可能更受青睐,主要是因为以下几点:

  • 性能:对于栈顶元素的操作,数组提供了更快的访问速度,尤其是在需要频繁访问或操作栈顶元素的情况下。
  • 简单性和高效性:数组实现的栈在代码上更直观简单,且由于内存连续性,它在空间和缓存效率上通常表现更好。
  • 可预测的性能:数组的固定大小虽然是一个限制,但也意味着栈的性能是可预测的,不会因为动态扩容而出现性能波动。

2.2静态栈的实现

静态栈的实现通常基于一个固定大小的数组,以及一个变量来跟踪栈顶元素的位置。下面是一个简单的C语言实现示例,包括基本的栈操作:入栈(Push)、出栈(Pop)、查看栈顶元素(Peek)和检查栈是否为空(IsEmpty)。

下面是示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> // 用于bool类型#define MAX_SIZE 10 // 定义栈的最大容量// 栈的结构体定义
typedef struct {int items[MAX_SIZE]; // 存储栈元素的数组int top; // 栈顶元素的索引
} Stack;// 创建栈
Stack* createStack() {Stack* S = (Stack*)malloc(sizeof(Stack));S->top = -1; // 初始化栈为空return S;
}// 检查栈是否满
bool isFull(Stack* S) {return S->top == MAX_SIZE - 1;
}// 检查栈是否为空
bool isEmpty(Stack* S) {return S->top == -1;
}// 入栈操作
void push(Stack* S, int element) {if (isFull(S)) {printf("Stack is full. Cannot push %d into the stack\n", element);return;}S->items[++S->top] = element; // 先递增栈顶指针,再存储元素printf("%d pushed to stack\n", element);
}// 出栈操作
int pop(Stack* S) {if (isEmpty(S)) {printf("Stack is empty. Cannot pop element\n");return INT_MIN; // INT_MIN在limits.h中定义,表示int类型可能的最小值}return S->items[S->top--]; // 返回栈顶元素,然后递减栈顶指针
}// 查看栈顶元素
int peek(Stack* S) {if (isEmpty(S)) {printf("Stack is empty\n");return INT_MIN;}return S->items[S->top];
}// 主函数,展示如何使用上述栈
int main() {Stack* myStack = createStack(); // 创建栈push(myStack, 10);push(myStack, 20);push(myStack, 30);printf("%d popped from stack\n", pop(myStack));printf("Top element is %d\n", peek(myStack));if (isEmpty(myStack)) {printf("Stack is empty\n");} else {printf("Stack is not empty\n");}return 0;
}

这段代码定义了一个栈的结构体Stack,包含一个整数数组items用于存储栈的元素,以及一个整数top用于跟踪栈顶元素的索引。createStack函数用于创建并初始化一个栈。push函数实现了向栈中添加一个新元素的功能,pop函数实现了移除栈顶元素的功能,peek函数用于返回栈顶元素但不移除它,isEmpty函数检查栈是否为空。

这个静态栈的实现是基于一个固定大小的数组,因此它有一个最大容量MAX_SIZE,超过这个容量的入栈操作将被拒绝。这个简单的实现演示了静态栈的基本操作和使用方法。

2.3动态栈的实现

动态栈的实现通过动态分配的数组来支持栈的操作,允许栈在运行时根据需要增长和缩小。这种实现方式通过使用realloc函数动态地调整数组的大小,从而克服了静态栈固定容量的限制。下面是动态栈实现的详细介绍:

结构定义

动态栈通过一个结构体stack来定义,包含以下成员:

  • _a:指向动态分配的数组,用于存储栈中的元素。
  • _top:表示栈顶元素的下标。栈为空时,_top为0。
  • _capacity:表示栈的当前容量,即数组_a可以容纳的元素数量。

初始化(stackInit

初始化函数为栈分配初始容量,并将_top设置为0,表示栈为空。初始容量可以根据需要和预期的使用情况进行设置,示例代码中设置为4。

销毁(stackDestroy

销毁函数释放动态分配的数组_a并将指针设置为NULL,同时将_top_capacity重置,以确保栈结构体不再指向已释放的内存。

入栈(stackPush

入栈操作首先检查栈是否已满(即_top等于_capacity)。如果栈已满,则进行扩容操作,将容量加倍,并使用realloc重新分配内存。扩容后,新元素被添加到栈顶位置,_top递增。

出栈(stackPop

出栈操作首先确保栈非空,然后递减_top,移除栈顶元素。注意,出栈操作并不实际删除元素,而是通过调整_top的值来逻辑上移除元素。

栈的大小(stackSize

返回栈的大小,即栈中元素的数量,这通过返回_top的值来实现。

检查栈是否为空(stackEmpty

检查栈是否为空,如果_top为0,则栈为空,返回1;否则,返回0。

查看栈顶元素(stackTop

返回栈顶元素,即位于_top - 1位置的元素。在访问前应确保栈非空。

下面为示例代码:

//stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int STDataType;
typedef struct stack
{STDataType* _a;//动态开辟的数组int _top;//栈顶的下标int _capacity;//容量
}stack;void stackInit(stack* pst);//初始化
void stackDestroy(stack* pst);//销毁
void stackPush(stack* pst, STDataType x);//入栈
void stackPop(stack* pst);//出栈
int stackSize(stack* pst);//返回栈的大小
int stackEmpty(stack* pst);//返回0表示非空,返回1表示空
STDataType stackTop(stack* pst);//返回栈顶元素
#define _CRT_SECURE_NO_WARNINGS 1#include"stack.h"// 初始化函数
void stackInit(stack* pst) {assert(pst); // 确保栈指针有效pst->_a = (STDataType*)malloc(sizeof(STDataType) * 4); // 动态分配初始容量为4的数组pst->_top = 0; // 初始化栈顶为0,表示栈为空pst->_capacity = 4; // 初始化栈的容量为4
}// 销毁栈函数
void stackDestroy(stack* pst) {assert(pst); // 确保栈指针有效free(pst->_a); // 释放动态分配的数组内存pst->_a = NULL; // 将数组指针设置为NULLpst->_top = pst->_capacity = 0; // 重置栈顶和容量
}// 入栈函数
void stackPush(stack* pst, STDataType x) {assert(pst); // 确保栈指针有效if (pst->_top == pst->_capacity) { // 如果栈已满pst->_capacity *= 2; // 容量加倍STDataType* tmp = (STDataType*)realloc(pst->_a, sizeof(STDataType) * pst->_capacity); // 重新分配更大的内存空间if (tmp == NULL) { // 如果重新分配失败printf("realloc fail\n"); // 打印错误信息exit(-1); // 异常退出}pst->_a = tmp; // 更新栈的数组指针}pst->_a[pst->_top] = x; // 在栈顶位置添加新元素pst->_top++; // 栈顶上移
}// 出栈函数
void stackPop(stack* pst) {assert(pst); // 确保栈指针有效assert(pst->_top > 0); // 确保栈非空pst->_top--; // 栈顶下移,移除栈顶元素
}// 获取栈的大小
int stackSize(stack* pst) {assert(pst); // 确保栈指针有效return pst->_top; // 返回栈中元素的数量
}// 检查栈是否为空
int stackEmpty(stack* pst) {assert(pst); // 确保栈指针有效return pst->_top == 0 ? 1 : 0; // 如果栈顶为0,表示栈为空,返回1;否则返回0
}// 获取栈顶元素
STDataType stackTop(stack* pst) {assert(pst); // 确保栈指针有效assert(pst->_top > 0); // 确保栈非空return pst->_a[pst->_top - 1]; // 返回栈顶元素
}

3.栈的应用

栈是一种非常实用的数据结构,它的后进先出(LIFO)特性使得它在多种计算机科学领域和实际应用中非常有用。以下是栈的一些主要应用:

1. 程序执行的函数调用栈

在大多数编程语言中,函数调用的执行是通过栈来管理的。每当一个函数被调用时,一个新的记录(称为"栈帧")被推入调用栈中,包含了函数的参数、局部变量和返回地址。当函数执行完成后,其对应的栈帧就会被弹出栈,并控制权返回到函数的调用点。这个机制支持了函数的嵌套调用和递归执行。

2. 括号匹配

在编程和文档编辑中,括号匹配是一个常见的问题。栈可以用来确保所有的开放括号(如(, [, {)都能找到相应的闭合括号(如), ], })。每次遇到开放括号时,就将其推入栈中,遇到闭合括号时,就检查栈顶的括号是否与之匹配,如果匹配则弹出栈顶括号,否则表示括号不匹配。

3. 深度优先搜索(DFS)

在图和树的遍历中,深度优先搜索(DFS)是一种重要的算法,它可以使用栈来实现。算法从一个起点开始,尽可能深地沿着树的分支进行搜索,直到达到叶节点,然后回溯并探索下一条路径。栈用来保存从起点到当前位置的路径。

4. 递归

尽管递归函数直接由调用栈支持,但在某些情况下,使用栈将递归算法转换为非递归形式是有用的,这可以减少函数调用的开销,特别是对于深层递归的情况。

4.结语

栈是一种简单而强大的数据结构,它在算法设计和系统实现中扮演着重要的角色。通过静态或动态的方式实现栈,可以在不同的应用场景中有效地利用其后进先出的特性。深入理解栈的工作原理和应用,对于学习更复杂的数据结构和算法是非常有帮助的。

另外,我们需要注意的是,我们做算法题需要使用栈、队列等数据结构时不需要每次都“手搓”出一份C语言数据结构代码来。实际上,C++中的STL(标准模板库)中有vector,list,stack,queue等容器接口,我们直接调用即可。这些内容会在我未来的C++博客中作详细介绍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/502573.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全网爆火的 MBTI 测试,是隐藏的割韭菜工具?

小伙伴们&#xff0c;谁能想到&#xff0c;作为一名冲浪老手&#xff0c;果子在网上又被骗了。 事情是这样的&#xff0c;前几天&#xff0c;我刷微博&#xff0c;看到一个推荐&#xff0c;大概如下图&#xff0c;是一个 MBTI 人格测试。 MBTI 测试&#xff0c;果子早就做过了…

Linux进程管理——top字段

目录 1.top下半部分——进程状态 2.top常用内部命令 3.top指定 ①top ②top -d 1 ③top -d 1 -p 10126 ④top -d 1 -p 10126,1 4.使用信号控制进程 1.top下半部分——进程状态 PID&#xff1a;进程号 User&#xff1a;用户 PR/NI&#xff1a;优先级 VIRT&#xff08…

【论文笔记】Improving Language Understanding by Generative Pre-Training

Improving Language Understanding by Generative Pre-Training 文章目录 Improving Language Understanding by Generative Pre-TrainingAbstract1 Introduction2 Related WorkSemi-supervised learning for NLPUnsupervised pre-trainingAuxiliary training objectives 3 Fra…

Vue3使用JSX/TSX

文章目录 1. 什么是 JSX & TSX?JSX&#xff08;JavaScript XML&#xff09;TSX&#xff08;TypeScript XML&#xff09; 2.Vue3 中使用 TSX基本渲染 & 响应式 & 事件 3.JSX 和 template 哪个好呢&#xff1f;总结 1. 什么是 JSX & TSX? 提示&#xff1a;JSX…

IDEA切换 Springboot初始化 URL

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 往期热门专栏回顾 专栏…

零基础小白到底适不适合学鸿蒙,请看完这篇再决定吧~

随着华为鸿蒙系统的问世&#xff0c;不少技术小白在是否学习鸿蒙的问题上犹豫不决。鸿蒙作为华为自主研发的操作系统&#xff0c;拥有许多独特的技术优势和市场前景。但对于小白来说&#xff0c;是否值得投入时间和精力去学习鸿蒙开发呢&#xff1f; 1.鸿蒙系统开发&#xff1…

VR虚拟现实技术应用到猪抗原体检测的好处

利用VR虚拟仿真技术开展猪瘟检测实验教学确保生猪产业健康发展 为了有效提高猪场猪瘟防控意识和检测技术&#xff0c;避免生猪养殖业遭受猪瘟危害&#xff0c;基于VR虚拟仿真技术开展猪瘟检测实验教学数据能大大推动基层畜牧养殖业持续稳步发展保驾护航。 一、提高实验效率 VR虚…

从模型到复合AI系统的转变

2023年,大型语言模型(LLM)吸引了所有人的注意力,它可以通过提示来执行通用任务,例如翻译或编码。这自然导致人们将模型作为AI应用开发的主要成分而密切关注,所有人都在想新的LLM将带来什么能力。然而,随着越来越多的开发者开始使用LLM构建,我们认为这种关注正在迅速改变:最先进…

java中的map集合

1.jdk8 Map接口实现类的特点&#xff1a; ①Map与Collection并列存在&#xff0c;用于保存具有映射关系的数据&#xff1a;Key-Value; ②Map中的key与value可以是任何引用类型的数据&#xff0c;会封装到HashMap$Node对象中&#xff1b; ③Map中的key不允许重复&#xff0c;…

力扣 每日一题 统计树中的合法路径数目

Problem: 2867. 统计树中的合法路径数目 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 灵神题解 代码实现时&#xff0c;为避免反复 DFS 同一个非质数连通块&#xff0c;可以把每个非质数所处的连通块的大小记录下来&#xff08;类似记忆化搜索&#xff09;…

93. 递归实现组合型枚举 刷题笔记

与我前面发的递归实现那一题有点相似 可以看看 94. 递归实现排列型枚举 刷题笔记-CSDN博客 思路 用u记录 选到哪一个位置 一旦选完 就输出 该题 要求升序 我们在选数时加入一个条件 大于上一个选择的数即可 依旧是从小到大搜到符合条件的每一个数 代码 #include<…

如何锁定MYSQL内存在物理内存里?

MYSQL 8.0 这个参数是 OFF 这个参数是啥意思呢? 按英文单词理解是 锁定在内存意思.突然想起来是因为 周报巡检时主库有使用SWAP内存 而从库却使用更多 使用脚本查看SWAP 进程排序 for i in cd /proc;ls |grep "^[0-9]"|awk $0 >100 ;do awk /Swap:/{aa$2} EN…