【数据结构】栈的基本概念 | 从零开始实现数组栈 | 画图解析 | 数组栈与链式栈

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本章我们将学习 "栈" ,首先介绍栈的概念和结构,然后我们将着重讲解数组栈的实现。我们从零开始写数组栈的接口,并从零开始步步解读。本章旨在筑牢栈知识点的基础,对后续的刷题有着很大的帮助。

前言


本章我们将学习 "栈" ,首先介绍栈的概念和结构,然后我们将着重讲解数组栈的实现。我们从零开始写数组栈的接口,并从零开始步步解读。本章旨在筑牢栈知识点的基础,对后续的刷题有着很大的帮助。


一、栈(stack)


0x00 栈的概念

📚 栈的概念:


① 栈是一种特殊的线性表,它只允许在固定的一端进行插入和删除元素的操作。


② 进行数据插入的删除和操作的一端,称为 栈顶 。另一端则称为 栈底 。


③ 栈中的元素遵守后进先出的原则,即 LIFO原则(Last In First Out)。


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


出栈:栈的删除操作叫做出栈。出数据也在栈顶。


0x01 栈的结构

🔍 栈的结构:

b6e4536725b9f4153278dffa979ad17b_b01e3d801c704cf79572039d798fa08a.png

0x02 数组栈和链式栈

4cd3f4848babf86638d77c3e1ab5ccb4_dca9cd8039914c668c306b713c615728.png


📚 实现栈无非就两种结构:数组结构 和 链式结构,两种结构都可以实现。


❓ 数组栈和链式栈哪种结构更好?


💡 相对而言数组的结构实现更优,尾插尾删的效率高,缓存利用率高,它的唯一缺点只是增容,但是增容1次扩2倍对栈来说本身就比较合理,是无伤大雅的。而链式栈虽然不会空间浪费,用一个 malloc 申请一个,但是链式栈存在一个致命的缺点:单链表不好出数据,必须要实现双向链表,否则尾上删除数据将会异常麻烦。


📌 如果硬要使用链式栈:


① 如果用尾做栈顶,尾插尾删,要设计成双向链表,否则删数据效率低。


② 如果用头做栈顶,头插头删,就可以设计成单链表。


🔺 总结:本章栈的实现将采用数组结构!


二、栈的定义


0x00 动态栈

📌 注意:本章将采用动态栈实现!


typedef int StackDataType;
typedef struct Stack {
  StackDataType* array;  //数组
  int top;               //栈顶
  int capacity;          //容量
} Stack;

🔑 解读:顺序表相信大家已经很熟了,这里和顺序表没啥两样。创建结构体,结构体有三个变量,array 是用来存放数据的数组。top 用来表示栈顶,这里相当于顺序表中的 size 变量。capacity 表示栈的容量。


0x01 静态栈

📚 简单介绍下静态栈:


typedef char StackDataType;
#define N 10
typedef struct Stack {
  StackDataType _array[N];  //数组
  int _top;                 //栈顶
} Stack;

🔑 解读:N 给多了浪费给少了又不够用,所以静态栈在实际中是不实用的。静态栈满了就不能扩大了,而动态栈是 malloc 出来的,不够了可以 realloc 扩容。虽然不实用,但是我们也得认识它,知道有这么一个东西,以后见到不至于觉得奇怪。


0x02 接口函数

📚 这是需要实现几个接口函数:


void StackInit(Stack* pst);
void StackDestroy(Stack* pst);
bool StackIsEmpty(Stack* pst);
void StackPush(Stack* pst, StackDataType x);
void StackPop(Stack* pst);
StackDataType StackTop(Stack* pst);
int StackSize(Stack* pst);

🔑 解读:相信细心的小伙伴发现了,StackIsEmpty 函数的类型是布尔。C语言如果想使用布尔值需要引入头文件 #include <stdbool.h> 。

601473c6b668edfb6e982e7834ed0672_abb81acb9c5f4895929069baf1c75f50.png


三、栈的实现


0x00 初始化栈(StackInit)

💬 Stack.h


#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int StackDataType;
typedef struct Stack {
  StackDataType* array;  //数组
  int top;               //栈顶
  int capacity;          //容量
} Stack;
void StackInit(Stack* pst);


🔑 解读:大家学到这里想必已然轻车熟路,应该知道需要引入哪些头文件了,动态内存开辟需要引入 stdlib.h,断言需要引入 assert,使用布尔值需要引入 stdbool.h 。


💬 Stack.c


/* 初始化 */
void StackInit(Stack* pst) {
  assert(pst);   // 防止pst为空
  pst->array = NULL;
  pst->top = 0;  // pst->top = -1
  pst->capacity = 0;
}

🔑 解析:初始化和顺序表几乎没有什么区别。首先通过结构体指针(我们定义的Stack) pst 指向 array,将数组为空。因为是初始化,所以将有效数据个数和数组时即能存数据的空间容量一并置为0。


0x01 销毁(StackDestroy)

💬 Stack.h

void StackDestroy(Stack* pst);

💬 Stack.c

/* 销毁 */
void StackDestroy(Stack* pst) {
  assert(pst);   // 防止pst为空
  free(pst->array);
  pst->array = NULL;
  pst->capacity = pst->top = 0;
}

🔑 解读:首先把栈 free 掉,为了防止野指针我们手动把它置为空指针(建议养成习惯)。最后再把 capacity 和 size 全部设为0,做到空着手来,空着手去,销毁部分就实现好了。


0x02 判断栈是否为空(StackIfEmpty)

💬 Stack.h

bool StackIsEmpty(Stack* pst);

🔑 解读:布尔值,返回 true 或 false


💬 Stack.c

/* 判断栈是否为空*/
bool StackIsEmpty(Stack* pst) {
  assert(pst);  //防止pst为空
  if (pst->top == 0)
  return true;
  else
  return false;
}

🔑 解读:首先防止 pst 为空。思路很简单,只需要看 top 是不是 0 即可,如果 top 是 0 就说明栈内没有数据。


⚡ 这里甚至可以直接返回,巧妙地利用布尔类型的特性:

bool StackIfEmpty(Stack* pst) {
  assert(pst);  //防止pst为空
  return pst->top == 0; //等于0就是空,就是真
}

0x03 入栈(StackPush)

💬 Stack.h

void StackPush(Stack* pst, StackDataType x);

💬 Stack.c

/* 进栈 */
void StackPush(Stack* pst, StackDataType x) {
  assert(pst); // 防止pst为空
  // 检查是否需要增容
  if (pst->top == pst->capacity) {
  int new_capacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
  StackDataType* tmp_arr = realloc(pst->array, sizeof(StackDataType) * new_capacity);
  // 防止realloc翻车
  if (tmp_arr == NULL) {
    printf("realloc failed!\n");
    exit(-1);
  }
  // 更新
  pst->array = tmp_arr;
  pst->capacity = new_capacity;
  }
  // 填入数据
  pst->array[pst->top] = x;
  pst->top++;
}

🔑 解读:


这里和顺序表尾插的实现没有任何区别。首先防止 pst 为空,随后检查是否要增容,如果要增容就进行增容操作。最后再填入数据即可。


【顺序表尾插的讲解】根据我们刚才分析的三种情况,首先我们需要判断是空间是否足够。判断思路如下:如果 size == capacity(有效数据个数等于实际能存的最大空间容量),我们进行扩容操作。


如果空间不足,我们就创建一个变量 new_capacity 用来存放 "新容量" ,int new_capacity = pst->capacity 首先把 capacity 的值赋值给这个 "新容量" ,因为考虑到第一次使用 capacity 大小为0,翻倍会出问题(0乘以任何数都是0),这里巧妙地使用三目操作符:int new_capacity = pst->capacity == 0 ? 4 : pst->capacity*2 , 如果 capacity 为 0 (第一次使用大小是0),就把4赋值给它;如果不为0,就把 capacity 的值翻一倍(x2)。


0x04 出栈(StackPop)

💬 Stack.h

void StackPop(Stack* pst);

💬 Stack.c


/* 出栈 */
void StackPop(Stack* pst) {
  assert(pst);  //防止pst为空
  //assert(pst->top > 0);  //防止top为空
  assert(!StackIsEmpty(pst));
  pst->top--;
}

🔑 解读:


① 首先防止 pst 为空。这里要注意的是,不能让栈内没有数据,必须要有东西能 "出栈" 才行。


② 出栈是非常简单的,就是把数据吐出来。直接将 top-- ,直接一刀砍就能直接达到目的。后进先出,后进的直接被 top-- 干掉了。


0x05 返回栈顶数据(StackTop)

💬 Stack.h

StackDataType StackTop(Stack* pst);

🔑 解读:为了方便打印出数据,我们需要设计一个返回栈顶数据的接口。既然是返回数据,我们的函数类型自然是 StackDataType 了。


💬 Stack.c

/* 返回栈顶数据 */
StackDataType StackTop(Stack* pst) {
  assert(pst);  //防止psl为空
  //assert(pst->top > 0);  //防止top为空
  assert(!StackIsEmpty(pst));
  return pst->array[pst->top - 1];
}

🔑 解读:


① 首先防止 pst 为空。同样地,不能让栈内没有数据。


② 我们直接返回栈顶数据就可以了,pst->array[pst->top-1] 。


❓ 为什么这里要 -1?


💡 这里 -1 的原因是我们初始化栈的时候定义 top 时给的值是 0,意味着 top 指向的是栈顶数据的下一个(无数据),所以这里既然要返回的是栈顶数据,自然需要 -1。这里的代码到底要不要 -1,主要看我们初始化 top 的时候给的是什么值,如果我们当时给的是 -1,那么 top 将指向栈顶数据,自然这里就不需要 -1,就应该是 pst->array[pst->top-1] 了。

9d7c64d6089dab1ad50ebbcf1a6daae4_590e402ce105432183610b092467875c.png

(修正:图中的 psl 应该是 pst ,笔误)


💬 Test.c

#include "Stack.h"
void TestStack1() {
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  //StackPop(&st);
  //printf("%d", StackTop(&st));
  StackDestroy(&st);
}
int main() {
  TestStack1();
  return 0;
}


0x06 计算栈的大小(StackSize)

💬 Stack.h

int StackSize(Stack* pst);

💬 Stack.c

/* 计算栈的大小 */
int StackSize(Stack* pst) {
  assert(pst);  //防止pst为空
  // 因为我们设定 top 是指向栈顶的下一个,所以top就是size
  return pst->top; 
}

🔑 解读:首先防止 pst 为空。因为我们之前初始化时 top 给的是0,指向栈顶的下一个。所以 top 就是栈的大小,直接 return top 即可。


0x07 代码测试

① 全部入栈完再出栈

#include "Stack.h"
void TestStack2() {
  // 入栈:1 2 3 4
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  // 出栈:4 3 2 1
  while (!StackIfEmpty(&st)) {
  printf("%d ", StackTop(&st));
  StackPop(&st);
  }
  StackDestroy(&st);
}
int main() {
  TestStack2();
  return 0;
}

🚩 运行代码:

c61635ddbcb0444859b06f53f175bc48_4b70ef5cbe6044e2add50622492827e6.png


② 入栈的同时伴随着出栈

#include "Stack.h"
void TestStack3() {
  // 入栈:1 2 3 4
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  // 提前出栈:4 3
  printf("%d ", StackTop(&st));
  StackPop(&st);
  printf("%d ", StackTop(&st));
  StackPop(&st);
  StackPush(&st, 5);
  StackPush(&st, 6);
  // 出栈:6 5 2 1
  while (!StackIfEmpty(&st)) {
  printf("%d ", StackTop(&st));
  StackPop(&st);
  }
  StackDestroy(&st);
}
int main() {
  TestStack3();
  return 0;
}


🚩 运行结果:

cff1385b8837bd4f30e3f99195cb2669_f525ecb2751d416186e31f25a137df83.png


0x08 完整代码

💬 Stack.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int StackDataType;
typedef struct Stack {
  StackDataType* array;  //数组
  int top;               //栈顶
  int capacity;          //容量
} Stack;
void StackInit(Stack* pst);
void StackDestroy(Stack* pst);
bool StackIsEmpty(Stack* pst);
void StackPush(Stack* pst, StackDataType x);
void StackPop(Stack* pst);
StackDataType StackTop(Stack* pst);
int StackSize(Stack* pst);

💬 Stack.c

#include "Stack.h"
/* 初始化 */
void StackInit(Stack* pst) {
  assert(pst);   // 防止pst为空
  pst->array = NULL;
  pst->top = 0;  // pst->top = -1
  pst->capacity = 0;
}
/* 销毁 */
void StackDestroy(Stack* pst) {
  assert(psl);   // 防止pst为空
  free(pst->array);
  pst->array = NULL;
  pst->capacity = pst->top = 0;
}
/* 判断栈是否为空*/
bool StackIsEmpty(Stack* pst) {
  assert(pst);  //防止pst为空
  return pst->top == 0; //等于0就是空,就是真
}
/* 进栈 */
void StackPush(Stack* pst, StackDataType x) {
  assert(pst); // 防止pst为空
  // 检查是否需要增容
  if (pst->top == pst->capacity) {
  int new_capacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
  StackDataType* tmp_arr = realloc(pst->array, sizeof(StackDataType) * new_capacity);
  // 防止realloc翻车
  if (tmp_arr == NULL) {
    printf("realloc failed!\n");
    exit(-1);
  }
  // 更新
  pst->array = tmp_arr;
  pst->capacity = new_capacity;
  }
  // 填入数据
  pst->array[pst->top] = x;
  pst->top++;
}
/* 出栈 */
void StackPop(Stack* pst) {
  assert(pst);  //防止pst为空
  //assert(pst->top > 0);  //防止top为空
  assert(!StackIsEmpty(pst));
  pst->top--;
}
/* 返回栈顶数据 */
StackDataType StackTop(Stack* pst) {
  assert(pst);  //防止pst为空
  //assert(pst->top > 0);  //防止top为空
  assert(!StackIsEmpty(pst));
  return pst->array[pst->top - 1];
}
/* 计算栈的大小 */
int StackSize(Stack* pst) {
  assert(pst);  //防止pst为空
  // 因为我们设定 top 是指向栈顶的下一个,所以top就是size
  return pst->top; 
}

💬 Test.c(测试用)

#include "Stack.h"
void TestStack1() {
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  //StackPop(&st);
  //printf("%d", StackTop(&st));
  StackDestroy(&st);
}
void TestStack2() {
  // 入栈:1 2 3 4
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  // 出栈:4 3 2 1
  while (!StackIfEmpty(&st)) {
  printf("%d ", StackTop(&st));
  StackPop(&st);
  }
  StackDestroy(&st);
}
void TestStack3() {
  // 入栈:1 2 3 4
  Stack st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  // 提前出栈:4 3
  printf("%d ", StackTop(&st));
  StackPop(&st);
  printf("%d ", StackTop(&st));
  StackPop(&st);
  StackPush(&st, 5);
  StackPush(&st, 6);
  // 出栈:6 5 2 1
  while (!StackIfEmpty(&st)) {
  printf("%d ", StackTop(&st));
  StackPop(&st);
  }
  StackDestroy(&st);
}
int main() {
  TestStack3();
  return 0;
}
相关文章
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
39 1
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
70 5
|
2月前
|
网络协议
网络通信的基石:TCP/IP协议栈的层次结构解析
在现代网络通信中,TCP/IP协议栈是构建互联网的基础。它定义了数据如何在网络中传输,以及如何确保数据的完整性和可靠性。本文将深入探讨TCP/IP协议栈的层次结构,揭示每一层的功能和重要性。
71 5
|
2月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
2月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
60 4
|
2月前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
103 2
|
2月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
53 0
|
2月前
|
存储 网络协议 算法
OSPF基本概念解析:从零开始理解
OSPF基本概念解析:从零开始理解
67 0
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
11天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析

热门文章

最新文章

推荐镜像

更多